2016-05-06 10:57:34 +02:00
|
|
|
<?php
|
2018-08-05 00:05:14 +02:00
|
|
|
namespace App\Radio\Frontend;
|
2016-05-06 10:57:34 +02:00
|
|
|
|
2018-08-05 00:05:14 +02:00
|
|
|
use App\Entity;
|
2020-02-12 17:53:14 +01:00
|
|
|
use App\Logger;
|
2020-06-29 23:26:48 +02:00
|
|
|
use App\Radio\CertificateLocator;
|
2019-09-12 07:31:01 +02:00
|
|
|
use App\Settings;
|
2019-08-07 06:33:55 +02:00
|
|
|
use App\Utilities;
|
2019-09-04 20:00:51 +02:00
|
|
|
use App\Xml\Reader;
|
|
|
|
use App\Xml\Writer;
|
2020-07-08 09:03:50 +02:00
|
|
|
use Exception;
|
2020-05-16 23:53:29 +02:00
|
|
|
use GuzzleHttp\Psr7\Uri;
|
2020-07-03 22:24:04 +02:00
|
|
|
use NowPlaying\Adapter\AdapterFactory;
|
|
|
|
use NowPlaying\Result\Result;
|
2018-10-05 01:12:12 +02:00
|
|
|
use Psr\Http\Message\UriInterface;
|
2016-05-06 10:57:34 +02:00
|
|
|
|
2018-12-27 09:24:07 +01:00
|
|
|
class Icecast extends AbstractFrontend
|
2016-05-06 10:57:34 +02:00
|
|
|
{
|
2018-09-09 20:23:03 +02:00
|
|
|
public const LOGLEVEL_DEBUG = 4;
|
|
|
|
public const LOGLEVEL_INFO = 3;
|
|
|
|
public const LOGLEVEL_WARN = 2;
|
|
|
|
public const LOGLEVEL_ERROR = 1;
|
2018-01-21 08:11:49 +01:00
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
public function getNowPlaying(Entity\Station $station, bool $includeClients = true): Result
|
2017-01-24 01:17:50 +01:00
|
|
|
{
|
2020-07-03 22:24:04 +02:00
|
|
|
$feConfig = $station->getFrontendConfig();
|
|
|
|
$radioPort = $feConfig->getPort();
|
2018-08-26 05:14:53 +02:00
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
$baseUrl = 'http://' . (Settings::getInstance()->isDocker() ? 'stations' : 'localhost') . ':' . $radioPort;
|
2017-01-24 01:17:50 +01:00
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
$npAdapter = $this->adapterFactory->getAdapter(
|
|
|
|
AdapterFactory::ADAPTER_ICECAST,
|
|
|
|
$baseUrl,
|
|
|
|
$feConfig->getAdminPassword()
|
|
|
|
);
|
2017-01-24 01:17:50 +01:00
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
$defaultResult = Result::blank();
|
|
|
|
$otherResults = [];
|
2017-08-20 09:35:41 +02:00
|
|
|
|
2018-10-02 16:53:58 +02:00
|
|
|
try {
|
2019-09-04 20:00:51 +02:00
|
|
|
foreach ($station->getMounts() as $mount) {
|
2018-10-02 16:53:58 +02:00
|
|
|
/** @var Entity\StationMount $mount */
|
2020-07-03 22:24:04 +02:00
|
|
|
$result = $npAdapter->getNowPlaying($mount->getName(), $includeClients);
|
|
|
|
|
|
|
|
$mount->setListenersTotal($result->listeners->total);
|
|
|
|
$mount->setListenersUnique($result->listeners->unique);
|
|
|
|
$this->em->persist($mount);
|
|
|
|
|
|
|
|
if ($mount->getIsDefault()) {
|
|
|
|
$defaultResult = $result;
|
|
|
|
} else {
|
|
|
|
$otherResults[] = $result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->em->flush();
|
|
|
|
|
|
|
|
foreach ($otherResults as $otherResult) {
|
|
|
|
$defaultResult = $defaultResult->merge($otherResult);
|
2017-05-16 09:46:43 +02:00
|
|
|
}
|
2020-07-08 09:03:50 +02:00
|
|
|
} catch (Exception $e) {
|
2019-09-12 07:31:01 +02:00
|
|
|
Logger::getInstance()->error(sprintf('NowPlaying adapter error: %s', $e->getMessage()));
|
2017-05-26 10:58:23 +02:00
|
|
|
}
|
2018-10-02 16:53:58 +02:00
|
|
|
|
2020-07-03 22:24:04 +02:00
|
|
|
return $defaultResult;
|
2017-01-24 01:17:50 +01:00
|
|
|
}
|
|
|
|
|
2018-09-22 13:52:43 +02:00
|
|
|
public function read(Entity\Station $station): bool
|
2016-05-06 10:57:34 +02:00
|
|
|
{
|
2018-09-22 13:52:43 +02:00
|
|
|
$config = $this->_getConfig($station);
|
|
|
|
$station->setFrontendConfigDefaults($this->_loadFromConfig($station, $config));
|
2016-05-07 11:13:17 +02:00
|
|
|
return true;
|
2016-05-06 10:57:34 +02:00
|
|
|
}
|
|
|
|
|
2018-09-22 13:52:43 +02:00
|
|
|
protected function _getConfig(Entity\Station $station)
|
2016-05-06 10:57:34 +02:00
|
|
|
{
|
2018-09-22 13:52:43 +02:00
|
|
|
$config_path = $station->getRadioConfigDir();
|
2017-01-24 01:35:16 +01:00
|
|
|
$icecast_path = $config_path . '/icecast.xml';
|
2016-05-19 16:56:21 +02:00
|
|
|
|
2018-09-22 13:52:43 +02:00
|
|
|
$defaults = $this->_getDefaults($station);
|
2016-05-19 16:56:21 +02:00
|
|
|
|
2017-01-24 01:35:16 +01:00
|
|
|
if (file_exists($icecast_path)) {
|
2019-09-04 20:00:51 +02:00
|
|
|
$reader = new Reader;
|
2016-05-19 16:56:21 +02:00
|
|
|
$data = $reader->fromFile($icecast_path);
|
|
|
|
|
2019-05-14 16:23:06 +02:00
|
|
|
return self::arrayMergeRecursiveDistinct($defaults, $data);
|
2016-05-19 16:56:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $defaults;
|
|
|
|
}
|
|
|
|
|
2019-09-04 20:00:51 +02:00
|
|
|
/*
|
|
|
|
* Process Management
|
|
|
|
*/
|
2017-01-24 01:17:50 +01:00
|
|
|
|
2020-07-08 09:03:50 +02:00
|
|
|
protected function _getDefaults(Entity\Station $station): array
|
2016-05-19 16:56:21 +02:00
|
|
|
{
|
2018-09-22 13:52:43 +02:00
|
|
|
$config_dir = $station->getRadioConfigDir();
|
2020-04-24 04:47:09 +02:00
|
|
|
$settings = Settings::getInstance();
|
2020-06-29 23:26:48 +02:00
|
|
|
|
2020-05-16 23:53:29 +02:00
|
|
|
$settingsBaseUrl = $this->settingsRepo->getSetting(Entity\Settings::BASE_URL, 'http://localhost');
|
|
|
|
if (strpos($settingsBaseUrl, 'http') !== 0) {
|
|
|
|
$settingsBaseUrl = 'http://' . $settingsBaseUrl;
|
|
|
|
}
|
|
|
|
$baseUrl = new Uri($settingsBaseUrl);
|
2016-05-06 10:57:34 +02:00
|
|
|
|
2020-06-29 23:26:48 +02:00
|
|
|
$certPaths = CertificateLocator::findCertificate();
|
|
|
|
|
2016-11-17 04:15:34 +01:00
|
|
|
$defaults = [
|
2016-09-03 03:41:17 +02:00
|
|
|
'location' => 'AzuraCast',
|
2016-05-19 16:56:21 +02:00
|
|
|
'admin' => 'icemaster@localhost',
|
2020-05-16 23:53:29 +02:00
|
|
|
'hostname' => $baseUrl->getHost(),
|
2016-05-19 16:56:21 +02:00
|
|
|
'limits' => [
|
2019-07-12 20:45:43 +02:00
|
|
|
'clients' => 2500,
|
2018-09-22 13:52:43 +02:00
|
|
|
'sources' => $station->getMounts()->count(),
|
2017-05-17 06:11:45 +02:00
|
|
|
// 'threadpool' => 5,
|
2016-05-19 16:56:21 +02:00
|
|
|
'queue-size' => 524288,
|
|
|
|
'client-timeout' => 30,
|
|
|
|
'header-timeout' => 15,
|
|
|
|
'source-timeout' => 10,
|
2017-05-17 06:11:45 +02:00
|
|
|
// 'burst-on-connect' => 1,
|
2016-05-19 16:56:21 +02:00
|
|
|
'burst-size' => 65535,
|
|
|
|
],
|
|
|
|
'authentication' => [
|
|
|
|
'source-password' => Utilities::generatePassword(),
|
|
|
|
'relay-password' => Utilities::generatePassword(),
|
|
|
|
'admin-user' => 'admin',
|
|
|
|
'admin-password' => Utilities::generatePassword(),
|
|
|
|
],
|
2016-09-02 06:53:50 +02:00
|
|
|
|
2016-05-19 16:56:21 +02:00
|
|
|
'listen-socket' => [
|
2018-09-22 13:52:43 +02:00
|
|
|
'port' => $this->_getRadioPort($station),
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
2016-09-02 06:53:50 +02:00
|
|
|
|
2016-11-17 04:15:34 +01:00
|
|
|
'mount' => [],
|
2016-05-19 16:56:21 +02:00
|
|
|
'fileserve' => 1,
|
|
|
|
'paths' => [
|
2017-05-17 06:11:45 +02:00
|
|
|
'basedir' => '/usr/local/share/icecast',
|
2016-05-19 16:56:21 +02:00
|
|
|
'logdir' => $config_dir,
|
2017-05-17 06:11:45 +02:00
|
|
|
'webroot' => '/usr/local/share/icecast/web',
|
|
|
|
'adminroot' => '/usr/local/share/icecast/admin',
|
2017-01-24 01:35:16 +01:00
|
|
|
'pidfile' => $config_dir . '/icecast.pid',
|
2016-05-19 16:56:21 +02:00
|
|
|
'alias' => [
|
|
|
|
'@source' => '/',
|
2017-05-17 06:11:45 +02:00
|
|
|
'@dest' => '/status.xsl',
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
2020-06-29 23:26:48 +02:00
|
|
|
'ssl-private-key' => $certPaths->getKeyPath(),
|
|
|
|
'ssl-certificate' => $certPaths->getCertPath(),
|
2018-01-02 01:59:54 +01:00
|
|
|
'ssl-allowed-ciphers' => 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS',
|
2020-03-02 01:03:03 +01:00
|
|
|
'deny-ip' => $this->writeIpBansFile($station),
|
2020-04-24 04:47:09 +02:00
|
|
|
'x-forwarded-for' => $settings->isDocker() ? '172.*.*.*' : '127.0.0.1',
|
2016-05-19 16:56:21 +02:00
|
|
|
],
|
|
|
|
'logging' => [
|
|
|
|
'accesslog' => 'icecast_access.log',
|
2019-03-08 03:01:51 +01:00
|
|
|
'errorlog' => '/dev/stderr',
|
2020-04-24 04:47:09 +02:00
|
|
|
'loglevel' => $settings->isProduction() ? self::LOGLEVEL_WARN : self::LOGLEVEL_INFO,
|
2016-05-19 16:56:21 +02:00
|
|
|
'logsize' => 10000,
|
|
|
|
],
|
|
|
|
'security' => [
|
|
|
|
'chroot' => 0,
|
|
|
|
],
|
|
|
|
];
|
2016-11-17 04:15:34 +01:00
|
|
|
|
2018-09-22 13:52:43 +02:00
|
|
|
foreach ($station->getMounts() as $mount_row) {
|
2017-08-17 20:28:48 +02:00
|
|
|
/** @var Entity\StationMount $mount_row */
|
|
|
|
|
2016-11-17 04:15:34 +01:00
|
|
|
$mount = [
|
2017-01-24 01:35:16 +01:00
|
|
|
'@type' => 'normal',
|
2017-08-17 20:28:48 +02:00
|
|
|
'mount-name' => $mount_row->getName(),
|
2017-10-19 23:55:02 +02:00
|
|
|
'charset' => 'UTF8',
|
2018-11-26 09:16:55 +01:00
|
|
|
|
|
|
|
'stream-name' => $station->getName(),
|
|
|
|
'stream-description' => $station->getDescription(),
|
|
|
|
'stream-url' => $station->getUrl(),
|
2019-05-17 10:18:24 +02:00
|
|
|
'genre' => $station->getGenre(),
|
2016-11-17 04:15:34 +01:00
|
|
|
];
|
|
|
|
|
2017-08-17 20:28:48 +02:00
|
|
|
if (!empty($mount_row->getFallbackMount())) {
|
|
|
|
$mount['fallback-mount'] = $mount_row->getFallbackMount();
|
2016-11-17 04:15:34 +01:00
|
|
|
$mount['fallback-override'] = 1;
|
|
|
|
}
|
|
|
|
|
2017-08-17 20:28:48 +02:00
|
|
|
if ($mount_row->getFrontendConfig()) {
|
2018-01-14 03:17:43 +01:00
|
|
|
|
2017-08-17 20:28:48 +02:00
|
|
|
$mount_conf = $this->_processCustomConfig($mount_row->getFrontendConfig());
|
2018-01-14 03:17:43 +01:00
|
|
|
|
2017-01-24 01:35:16 +01:00
|
|
|
if (!empty($mount_conf)) {
|
2019-05-14 16:23:06 +02:00
|
|
|
$mount = self::arrayMergeRecursiveDistinct($mount, $mount_conf);
|
2017-01-24 01:35:16 +01:00
|
|
|
}
|
2016-11-17 19:26:43 +01:00
|
|
|
}
|
|
|
|
|
2017-08-17 20:28:48 +02:00
|
|
|
if ($mount_row->getRelayUrl()) {
|
|
|
|
$relay_parts = parse_url($mount_row->getRelayUrl());
|
2017-04-13 00:19:02 +02:00
|
|
|
|
|
|
|
$defaults['relay'][] = [
|
|
|
|
'server' => $relay_parts['host'],
|
|
|
|
'port' => $relay_parts['port'],
|
|
|
|
'mount' => $relay_parts['path'],
|
2017-08-17 20:28:48 +02:00
|
|
|
'local-mount' => $mount_row->getName(),
|
2017-04-13 00:19:02 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2016-11-17 19:26:43 +01:00
|
|
|
$defaults['mount'][] = $mount;
|
2016-11-17 04:15:34 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return $defaults;
|
2016-05-06 10:57:34 +02:00
|
|
|
}
|
2017-05-19 08:38:17 +02:00
|
|
|
|
2019-05-14 16:23:06 +02:00
|
|
|
/**
|
|
|
|
* array_merge_recursive does indeed merge arrays, but it converts values with duplicate
|
|
|
|
* keys to arrays rather than overwriting the value in the first array with the duplicate
|
|
|
|
* value in the second array, as array_merge does. I.e., with array_merge_recursive,
|
|
|
|
* this happens (documented behavior):
|
|
|
|
*
|
|
|
|
* array_merge_recursive(array('key' => 'org value'), array('key' => 'new value'));
|
|
|
|
* => array('key' => array('org value', 'new value'));
|
|
|
|
*
|
|
|
|
* array_merge_recursive_distinct does not change the datatypes of the values in the arrays.
|
|
|
|
* Matching keys' values in the second array overwrite those in the first array, as is the
|
|
|
|
* case with array_merge, i.e.:
|
|
|
|
*
|
|
|
|
* array_merge_recursive_distinct(array('key' => 'org value'), array('key' => 'new value'));
|
|
|
|
* => array('key' => array('new value'));
|
|
|
|
*
|
|
|
|
* Parameters are passed by reference, though only for performance reasons. They're not
|
|
|
|
* altered by this function.
|
|
|
|
*
|
|
|
|
* @param array $array1
|
|
|
|
* @param array $array2
|
2019-09-20 18:44:38 +02:00
|
|
|
*
|
2019-05-14 16:23:06 +02:00
|
|
|
* @return array
|
|
|
|
* @author Daniel <daniel (at) danielsmedegaardbuus (dot) dk>
|
|
|
|
* @author Gabriel Sobrinho <gabriel (dot) sobrinho (at) gmail (dot) com>
|
2020-07-08 09:03:50 +02:00
|
|
|
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection
|
2019-05-14 16:23:06 +02:00
|
|
|
*/
|
|
|
|
public static function arrayMergeRecursiveDistinct(array &$array1, array &$array2): array
|
|
|
|
{
|
|
|
|
$merged = $array1;
|
|
|
|
foreach ($array2 as $key => &$value) {
|
|
|
|
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) {
|
|
|
|
$merged[$key] = self::arrayMergeRecursiveDistinct($merged[$key], $value);
|
|
|
|
} else {
|
|
|
|
$merged[$key] = $value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $merged;
|
|
|
|
}
|
2019-09-04 20:00:51 +02:00
|
|
|
|
|
|
|
/*
|
|
|
|
* Configuration
|
|
|
|
*/
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
protected function _loadFromConfig(Entity\Station $station, $config): array
|
2019-09-04 20:00:51 +02:00
|
|
|
{
|
2020-05-02 09:58:59 +02:00
|
|
|
$frontend_config = $station->getFrontendConfig();
|
2019-09-04 20:00:51 +02:00
|
|
|
|
|
|
|
return [
|
2020-05-02 09:58:59 +02:00
|
|
|
Entity\StationFrontendConfiguration::CUSTOM_CONFIGURATION => $frontend_config->getCustomConfiguration(),
|
|
|
|
Entity\StationFrontendConfiguration::SOURCE_PASSWORD => $config['authentication']['source-password'],
|
|
|
|
Entity\StationFrontendConfiguration::ADMIN_PASSWORD => $config['authentication']['admin-password'],
|
|
|
|
Entity\StationFrontendConfiguration::RELAY_PASSWORD => $config['authentication']['relay-password'],
|
|
|
|
Entity\StationFrontendConfiguration::STREAMER_PASSWORD => $config['mount'][0]['password'],
|
|
|
|
Entity\StationFrontendConfiguration::MAX_LISTENERS => $config['limits']['clients'],
|
2019-09-04 20:00:51 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
public function write(Entity\Station $station): bool
|
|
|
|
{
|
|
|
|
$config = $this->_getDefaults($station);
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$frontend_config = $station->getFrontendConfig();
|
2019-09-04 20:00:51 +02:00
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$port = $frontend_config->getPort();
|
|
|
|
if (null !== $port) {
|
|
|
|
$config['listen-socket']['port'] = $port;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$sourcePw = $frontend_config->getSourcePassword();
|
|
|
|
if (!empty($sourcePw)) {
|
|
|
|
$config['authentication']['source-password'] = $sourcePw;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$adminPw = $frontend_config->getAdminPassword();
|
|
|
|
if (!empty($adminPw)) {
|
|
|
|
$config['authentication']['admin-password'] = $adminPw;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$relayPw = $frontend_config->getRelayPassword();
|
|
|
|
if (!empty($relayPw)) {
|
|
|
|
$config['authentication']['relay-password'] = $relayPw;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$streamerPw = $frontend_config->getStreamerPassword();
|
|
|
|
if (!empty($streamerPw)) {
|
2019-09-04 20:00:51 +02:00
|
|
|
foreach ($config['mount'] as &$mount) {
|
|
|
|
if (!empty($mount['password'])) {
|
2020-05-02 09:58:59 +02:00
|
|
|
$mount['password'] = $streamerPw;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$maxListeners = $frontend_config->getMaxListeners();
|
|
|
|
if (null !== $maxListeners) {
|
|
|
|
$config['limits']['clients'] = $maxListeners;
|
2019-09-04 20:00:51 +02:00
|
|
|
}
|
|
|
|
|
2020-05-02 09:58:59 +02:00
|
|
|
$customConfig = $frontend_config->getCustomConfiguration();
|
|
|
|
if (!empty($customConfig)) {
|
|
|
|
$custom_conf = $this->_processCustomConfig($customConfig);
|
2019-09-04 20:00:51 +02:00
|
|
|
if (!empty($custom_conf)) {
|
|
|
|
$config = self::arrayMergeRecursiveDistinct($config, $custom_conf);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set any unset values back to the DB config.
|
|
|
|
$station->setFrontendConfigDefaults($this->_loadFromConfig($station, $config));
|
|
|
|
|
|
|
|
$this->em->persist($station);
|
|
|
|
$this->em->flush();
|
|
|
|
|
|
|
|
$config_path = $station->getRadioConfigDir();
|
|
|
|
$icecast_path = $config_path . '/icecast.xml';
|
|
|
|
|
|
|
|
$writer = new Writer;
|
|
|
|
$icecast_config_str = $writer->toString($config, 'icecast');
|
|
|
|
|
|
|
|
// Strip the first line (the XML charset)
|
|
|
|
$icecast_config_str = substr($icecast_config_str, strpos($icecast_config_str, "\n") + 1);
|
|
|
|
|
|
|
|
file_put_contents($icecast_path, $icecast_config_str);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getCommand(Entity\Station $station): ?string
|
|
|
|
{
|
|
|
|
if ($binary = self::getBinary()) {
|
|
|
|
$config_path = $station->getRadioConfigDir() . '/icecast.xml';
|
|
|
|
return $binary . ' -c ' . $config_path;
|
|
|
|
}
|
|
|
|
return '/bin/false';
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getBinary()
|
|
|
|
{
|
|
|
|
$new_path = '/usr/local/bin/icecast';
|
|
|
|
$legacy_path = '/usr/bin/icecast2';
|
|
|
|
|
2019-09-12 07:31:01 +02:00
|
|
|
if (Settings::getInstance()->isDocker() || file_exists($new_path)) {
|
2019-09-04 20:00:51 +02:00
|
|
|
return $new_path;
|
2019-12-07 01:57:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if (file_exists($legacy_path)) {
|
2019-09-04 20:00:51 +02:00
|
|
|
return $legacy_path;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getAdminUrl(Entity\Station $station, UriInterface $base_url = null): UriInterface
|
|
|
|
{
|
|
|
|
$public_url = $this->getPublicUrl($station, $base_url);
|
|
|
|
return $public_url
|
|
|
|
->withPath($public_url->getPath() . '/admin.html');
|
|
|
|
}
|
2018-07-15 05:10:04 +02:00
|
|
|
}
|