#3525 -- Rework settings to be fetched on-demand to avoid collisions.

This commit is contained in:
Buster "Silver Eagle" Neece 2020-12-10 16:46:03 -06:00
parent d9b828a821
commit 4aa1902dae
No known key found for this signature in database
GPG key ID: 6D9E12FF03411F4E
62 changed files with 977 additions and 850 deletions

View file

@ -10,42 +10,41 @@ use Psr\Container\ContainerInterface;
return [ return [
// URL Router helper // URL Router helper
App\Http\Router::class => function (
Environment $environment,
Slim\App $app,
App\Entity\Settings $settings
) {
$route_parser = $app->getRouteCollector()->getRouteParser();
return new App\Http\Router($environment, $route_parser, $settings);
},
App\Http\RouterInterface::class => DI\Get(App\Http\Router::class), App\Http\RouterInterface::class => DI\Get(App\Http\Router::class),
// Error handler // Error handler
App\Http\ErrorHandler::class => DI\autowire(),
Slim\Interfaces\ErrorHandlerInterface::class => DI\Get(App\Http\ErrorHandler::class), Slim\Interfaces\ErrorHandlerInterface::class => DI\Get(App\Http\ErrorHandler::class),
// HTTP client // HTTP client
GuzzleHttp\Client::class => function (Psr\Log\LoggerInterface $logger) { GuzzleHttp\Client::class => function (Psr\Log\LoggerInterface $logger) {
$stack = GuzzleHttp\HandlerStack::create(); $stack = GuzzleHttp\HandlerStack::create();
$stack->unshift(function (callable $handler) { $stack->unshift(
return function (Psr\Http\Message\RequestInterface $request, array $options) use ($handler) { function (callable $handler) {
$options[GuzzleHttp\RequestOptions::VERIFY] = Composer\CaBundle\CaBundle::getSystemCaRootBundlePath(); return function (Psr\Http\Message\RequestInterface $request, array $options) use ($handler) {
return $handler($request, $options); $options[GuzzleHttp\RequestOptions::VERIFY] = Composer\CaBundle\CaBundle::getSystemCaRootBundlePath(
}; );
}, 'ssl_verify'); return $handler($request, $options);
};
},
'ssl_verify'
);
$stack->push(GuzzleHttp\Middleware::log( $stack->push(
$logger, GuzzleHttp\Middleware::log(
new GuzzleHttp\MessageFormatter('HTTP client {method} call to {uri} produced response {code}'), $logger,
Psr\Log\LogLevel::DEBUG new GuzzleHttp\MessageFormatter('HTTP client {method} call to {uri} produced response {code}'),
)); Psr\Log\LogLevel::DEBUG
)
);
return new GuzzleHttp\Client([ return new GuzzleHttp\Client(
'handler' => $stack, [
GuzzleHttp\RequestOptions::HTTP_ERRORS => false, 'handler' => $stack,
GuzzleHttp\RequestOptions::TIMEOUT => 3.0, GuzzleHttp\RequestOptions::HTTP_ERRORS => false,
]); GuzzleHttp\RequestOptions::TIMEOUT => 3.0,
]
);
}, },
// DBAL // DBAL
@ -128,13 +127,15 @@ return [
$eventManager->addEventSubscriber($eventAuditLog); $eventManager->addEventSubscriber($eventAuditLog);
$eventManager->addEventSubscriber($eventChangeTracking); $eventManager->addEventSubscriber($eventChangeTracking);
return new App\Doctrine\DecoratedEntityManager(function () use ( return new App\Doctrine\DecoratedEntityManager(
$connectionOptions, function () use (
$config, $connectionOptions,
$eventManager $config,
) { $eventManager
return Doctrine\ORM\EntityManager::create($connectionOptions, $config, $eventManager); ) {
}); return Doctrine\ORM\EntityManager::create($connectionOptions, $config, $eventManager);
}
);
} catch (Exception $e) { } catch (Exception $e) {
throw new App\Exception\BootstrapException($e->getMessage()); throw new App\Exception\BootstrapException($e->getMessage());
} }
@ -143,11 +144,6 @@ return [
App\Doctrine\ReloadableEntityManagerInterface::class => DI\Get(App\Doctrine\DecoratedEntityManager::class), App\Doctrine\ReloadableEntityManagerInterface::class => DI\Get(App\Doctrine\DecoratedEntityManager::class),
Doctrine\ORM\EntityManagerInterface::class => DI\Get(App\Doctrine\DecoratedEntityManager::class), Doctrine\ORM\EntityManagerInterface::class => DI\Get(App\Doctrine\DecoratedEntityManager::class),
// Database settings
App\Entity\Settings::class => function (App\Entity\Repository\SettingsTableRepository $settingsTableRepo) {
return $settingsTableRepo->readSettings();
},
// Redis cache // Redis cache
Redis::class => function (Environment $environment) { Redis::class => function (Environment $environment) {
$redis_host = $environment->isDocker() ? 'redis' : 'localhost'; $redis_host = $environment->isDocker() ? 'redis' : 'localhost';
@ -303,7 +299,9 @@ return [
}, },
// Symfony Validator // Symfony Validator
Symfony\Component\Validator\ConstraintValidatorFactoryInterface::class => DI\autowire(App\Validator\ConstraintValidatorFactory::class), Symfony\Component\Validator\ConstraintValidatorFactoryInterface::class => DI\autowire(
App\Validator\ConstraintValidatorFactory::class
),
Symfony\Component\Validator\Validator\ValidatorInterface::class => function ( Symfony\Component\Validator\Validator\ValidatorInterface::class => function (
Doctrine\Common\Annotations\Reader $annotation_reader, Doctrine\Common\Annotations\Reader $annotation_reader,
@ -323,7 +321,6 @@ return [
App\Plugins $plugins, App\Plugins $plugins,
Environment $environment Environment $environment
) { ) {
// Configure message sending middleware // Configure message sending middleware
$sendMessageMiddleware = new Symfony\Component\Messenger\Middleware\SendMessageMiddleware($queueManager); $sendMessageMiddleware = new Symfony\Component\Messenger\Middleware\SendMessageMiddleware($queueManager);
$sendMessageMiddleware->setLogger($logger); $sendMessageMiddleware->setLogger($logger);
@ -355,17 +352,21 @@ return [
// On testing, messages are handled directly when called // On testing, messages are handled directly when called
if ($environment->isTesting()) { if ($environment->isTesting()) {
return new Symfony\Component\Messenger\MessageBus([ return new Symfony\Component\Messenger\MessageBus(
$handleMessageMiddleware, [
]); $handleMessageMiddleware,
]
);
} }
// Compile finished message bus. // Compile finished message bus.
return new Symfony\Component\Messenger\MessageBus([ return new Symfony\Component\Messenger\MessageBus(
$sendMessageMiddleware, [
$uniqueMiddleware, $sendMessageMiddleware,
$handleMessageMiddleware, $uniqueMiddleware,
]); $handleMessageMiddleware,
]
);
}, },
// Supervisor manager // Supervisor manager
@ -401,66 +402,4 @@ return [
}, },
App\Media\MetadataManagerInterface::class => DI\get(App\Media\GetId3\GetId3MetadataManager::class), App\Media\MetadataManagerInterface::class => DI\get(App\Media\GetId3\GetId3MetadataManager::class),
// Asset Management
App\Assets::class => function (App\Config $config, Environment $environment) {
$libraries = $config->get('assets');
$versioned_files = [];
$assets_file = $environment->getBaseDirectory() . '/web/static/assets.json';
if (file_exists($assets_file)) {
$versioned_files = json_decode(file_get_contents($assets_file), true, 512, JSON_THROW_ON_ERROR);
}
$vueComponents = [];
$assets_file = $environment->getBaseDirectory() . '/web/static/webpack.json';
if (file_exists($assets_file)) {
$vueComponents = json_decode(file_get_contents($assets_file), true, 512, JSON_THROW_ON_ERROR);
}
return new App\Assets($environment, $libraries, $versioned_files, $vueComponents);
},
// Synchronized (Cron) Tasks
App\Sync\TaskLocator::class => function (ContainerInterface $di) {
return new App\Sync\TaskLocator($di, [
App\Event\GetSyncTasks::SYNC_NOWPLAYING => [
App\Sync\Task\BuildQueueTask::class,
App\Sync\Task\NowPlayingTask::class,
App\Sync\Task\ReactivateStreamerTask::class,
],
App\Event\GetSyncTasks::SYNC_SHORT => [
App\Sync\Task\CheckRequests::class,
App\Sync\Task\RunBackupTask::class,
App\Sync\Task\CleanupRelaysTask::class,
],
App\Event\GetSyncTasks::SYNC_MEDIUM => [
App\Sync\Task\CheckMediaTask::class,
App\Sync\Task\CheckFolderPlaylistsTask::class,
App\Sync\Task\CheckUpdatesTask::class,
],
App\Event\GetSyncTasks::SYNC_LONG => [
App\Sync\Task\RunAnalyticsTask::class,
App\Sync\Task\RunAutomatedAssignmentTask::class,
App\Sync\Task\CleanupHistoryTask::class,
App\Sync\Task\CleanupStorageTask::class,
App\Sync\Task\RotateLogsTask::class,
App\Sync\Task\UpdateGeoLiteTask::class,
],
]);
},
// Web Hooks
App\Webhook\ConnectorLocator::class => function (
ContainerInterface $di,
App\Config $config
) {
$webhooks = $config->get('webhooks');
$services = [];
foreach ($webhooks['webhooks'] as $webhook_key => $webhook_info) {
$services[$webhook_key] = $webhook_info['class'];
}
return new App\Webhook\ConnectorLocator($di, $services);
},
]; ];

View file

@ -14,8 +14,6 @@ class Acl
public const GLOBAL_LOGS = 'view system logs'; public const GLOBAL_LOGS = 'view system logs';
public const GLOBAL_SETTINGS = 'administer settings'; public const GLOBAL_SETTINGS = 'administer settings';
public const GLOBAL_API_KEYS = 'administer api keys'; public const GLOBAL_API_KEYS = 'administer api keys';
public const GLOBAL_USERS = 'administer user accounts';
public const GLOBAL_PERMISSIONS = 'administer permissions';
public const GLOBAL_STATIONS = 'administer stations'; public const GLOBAL_STATIONS = 'administer stations';
public const GLOBAL_CUSTOM_FIELDS = 'administer custom fields'; public const GLOBAL_CUSTOM_FIELDS = 'administer custom fields';
public const GLOBAL_BACKUPS = 'administer backups'; public const GLOBAL_BACKUPS = 'administer backups';

View file

@ -3,6 +3,7 @@
namespace App; namespace App;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use function base64_encode; use function base64_encode;
@ -43,41 +44,35 @@ class Assets
public function __construct( public function __construct(
Environment $environment, Environment $environment,
array $libraries = [], Config $config,
array $versioned_files = [], ?ServerRequestInterface $request
array $vueComponents = []
) { ) {
$this->environment = $environment; $this->environment = $environment;
$this->request = $request;
$libraries = $config->get('assets');
foreach ($libraries as $library_name => $library) { foreach ($libraries as $library_name => $library) {
$this->addLibrary($library, $library_name); $this->addLibrary($library, $library_name);
} }
$versioned_files = [];
$assets_file = $environment->getBaseDirectory() . '/web/static/assets.json';
if (file_exists($assets_file)) {
$versioned_files = json_decode(file_get_contents($assets_file), true, 512, JSON_THROW_ON_ERROR);
}
$this->versioned_files = $versioned_files; $this->versioned_files = $versioned_files;
$vueComponents = [];
$assets_file = $environment->getBaseDirectory() . '/web/static/webpack.json';
if (file_exists($assets_file)) {
$vueComponents = json_decode(file_get_contents($assets_file), true, 512, JSON_THROW_ON_ERROR);
}
$this->addVueComponents($vueComponents); $this->addVueComponents($vueComponents);
$this->csp_nonce = preg_replace('/[^A-Za-z0-9\+\/=]/', '', base64_encode(random_bytes(18))); $this->csp_nonce = preg_replace('/[^A-Za-z0-9\+\/=]/', '', base64_encode(random_bytes(18)));
$this->csp_domains = []; $this->csp_domains = [];
} }
/**
* Create a new copy of this object for a specific request.
*
* @param ServerRequestInterface $request
*/
public function withRequest(ServerRequestInterface $request): self
{
$newAssets = clone $this;
$newAssets->setRequest($request);
return $newAssets;
}
public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
protected function addVueComponents(array $vueComponents = []): void protected function addVueComponents(array $vueComponents = []): void
{ {
if (!empty($vueComponents['entrypoints'])) { if (!empty($vueComponents['entrypoints'])) {
@ -162,14 +157,16 @@ class Assets
*/ */
public function addJs($js_script): self public function addJs($js_script): self
{ {
$this->load([ $this->load(
'order' => 100, [
'files' => [ 'order' => 100,
'js' => [ 'files' => [
(is_array($js_script)) ? $js_script : ['src' => $js_script], 'js' => [
(is_array($js_script)) ? $js_script : ['src' => $js_script],
],
], ],
], ]
]); );
return $this; return $this;
} }
@ -250,12 +247,14 @@ class Assets
*/ */
public function addInlineJs($js_script, int $order = 100): self public function addInlineJs($js_script, int $order = 100): self
{ {
$this->load([ $this->load(
'order' => $order, [
'inline' => [ 'order' => $order,
'js' => (is_array($js_script)) ? $js_script : [$js_script], 'inline' => [
], 'js' => (is_array($js_script)) ? $js_script : [$js_script],
]); ],
]
);
return $this; return $this;
} }
@ -268,14 +267,16 @@ class Assets
*/ */
public function addCss($css_script, int $order = 100): self public function addCss($css_script, int $order = 100): self
{ {
$this->load([ $this->load(
'order' => $order, [
'files' => [ 'order' => $order,
'css' => [ 'files' => [
(is_array($css_script)) ? $css_script : ['src' => $css_script], 'css' => [
(is_array($css_script)) ? $css_script : ['src' => $css_script],
],
], ],
], ]
]); );
return $this; return $this;
} }
@ -287,12 +288,14 @@ class Assets
*/ */
public function addInlineCss($css_script): self public function addInlineCss($css_script): self
{ {
$this->load([ $this->load(
'order' => 100, [
'inline' => [ 'order' => 100,
'css' => (is_array($css_script)) ? $css_script : [$css_script], 'inline' => [
], 'css' => (is_array($css_script)) ? $css_script : [$css_script],
]); ],
]
);
return $this; return $this;
} }
@ -310,10 +313,13 @@ class Assets
foreach ($this->loaded as $item) { foreach ($this->loaded as $item) {
if (!empty($item['files']['css'])) { if (!empty($item['files']['css'])) {
foreach ($item['files']['css'] as $file) { foreach ($item['files']['css'] as $file) {
$compiled_attributes = $this->compileAttributes($file, [ $compiled_attributes = $this->compileAttributes(
'rel' => 'stylesheet', $file,
'type' => 'text/css', [
]); 'rel' => 'stylesheet',
'type' => 'text/css',
]
);
$result[] = '<link ' . implode(' ', $compiled_attributes) . ' />'; $result[] = '<link ' . implode(' ', $compiled_attributes) . ' />';
} }
@ -349,9 +355,12 @@ class Assets
foreach ($this->loaded as $item) { foreach ($this->loaded as $item) {
if (!empty($item['files']['js'])) { if (!empty($item['files']['js'])) {
foreach ($item['files']['js'] as $file) { foreach ($item['files']['js'] as $file) {
$compiled_attributes = $this->compileAttributes($file, [ $compiled_attributes = $this->compileAttributes(
'type' => 'text/javascript', $file,
]); [
'type' => 'text/javascript',
]
);
$result[] = '<script ' . implode(' ', $compiled_attributes) . '></script>'; $result[] = '<script ' . implode(' ', $compiled_attributes) . '></script>';
} }
@ -397,9 +406,12 @@ class Assets
protected function sort(): void protected function sort(): void
{ {
if (!$this->is_sorted) { if (!$this->is_sorted) {
uasort($this->loaded, function ($a, $b): int { uasort(
return $a['order'] <=> $b['order']; // SPACESHIP! $this->loaded,
}); function ($a, $b): int {
return $a['order'] <=> $b['order']; // SPACESHIP!
}
);
$this->is_sorted = true; $this->is_sorted = true;
} }
@ -480,4 +492,23 @@ class Assets
$this->csp_domains[$domain] = $domain; $this->csp_domains[$domain] = $domain;
} }
} }
public function writeCsp(ResponseInterface $response): ResponseInterface
{
$csp = [];
if ('https' === $this->request->getUri()->getScheme()) {
$csp[] = 'upgrade-insecure-requests';
}
// CSP JavaScript policy
// Note: unsafe-eval included for Vue template compiling
$csp_script_src = $this->getCspDomains();
$csp_script_src[] = "'self'";
$csp_script_src[] = "'unsafe-eval'";
$csp_script_src[] = "'nonce-" . $this->getCspNonce() . "'";
$csp[] = 'script-src ' . implode(' ', $csp_script_src);
return $response->withHeader('Content-Security-Policy', implode('; ', $csp));
}
} }

View file

@ -11,7 +11,7 @@ class ListCommand extends CommandAbstract
{ {
public function __invoke( public function __invoke(
SymfonyStyle $io, SymfonyStyle $io,
Entity\Repository\SettingsTableRepository $settingsTableRepo Entity\Repository\SettingsRepository $settingsTableRepo
): int { ): int {
$io->title(__('AzuraCast Settings')); $io->title(__('AzuraCast Settings'));

View file

@ -10,7 +10,7 @@ class SetCommand extends CommandAbstract
{ {
public function __invoke( public function __invoke(
SymfonyStyle $io, SymfonyStyle $io,
Entity\Repository\SettingsTableRepository $settingsTableRepo, Entity\Repository\SettingsRepository $settingsTableRepo,
string $settingKey, string $settingKey,
string $settingValue string $settingValue
): int { ): int {

View file

@ -17,7 +17,7 @@ class SetupCommand extends CommandAbstract
OutputInterface $output, OutputInterface $output,
Environment $environment, Environment $environment,
ContainerInterface $di, ContainerInterface $di,
Entity\Repository\SettingsTableRepository $settingsTableRepo, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\StationRepository $stationRepo, Entity\Repository\StationRepository $stationRepo,
AzuraCastCentral $acCentral, AzuraCastCentral $acCentral,
bool $update = false, bool $update = false,
@ -78,7 +78,7 @@ class SetupCommand extends CommandAbstract
$this->runCommand($output, 'queue:clear'); $this->runCommand($output, 'queue:clear');
$settings = $settingsTableRepo->updateSettings(); $settings = $settingsRepo->readSettings(true);
$settings->setNowplaying(null); $settings->setNowplaying(null);
$stationRepo->clearNowPlaying(); $stationRepo->clearNowPlaying();
@ -97,7 +97,7 @@ class SetupCommand extends CommandAbstract
$settings->setAppUniqueIdentifier(null); $settings->setAppUniqueIdentifier(null);
} }
$settingsTableRepo->writeSettings($settings); $settingsRepo->writeSettings($settings);
$io->newLine(); $io->newLine();

View file

@ -4,9 +4,7 @@ namespace App\Controller\Admin;
use App\Config; use App\Config;
use App\Controller\AbstractLogViewerController; use App\Controller\AbstractLogViewerController;
use App\Entity\Repository\StorageLocationRepository; use App\Entity;
use App\Entity\Settings;
use App\Entity\StorageLocation;
use App\Exception\NotFoundException; use App\Exception\NotFoundException;
use App\File; use App\File;
use App\Flysystem\Filesystem; use App\Flysystem\Filesystem;
@ -22,9 +20,9 @@ use Symfony\Component\Messenger\MessageBus;
class BackupsController extends AbstractLogViewerController class BackupsController extends AbstractLogViewerController
{ {
protected Settings $settings; protected Entity\Settings $settings;
protected StorageLocationRepository $storageLocationRepo; protected Entity\Repository\StorageLocationRepository $storageLocationRepo;
protected RunBackupTask $backupTask; protected RunBackupTask $backupTask;
@ -33,14 +31,14 @@ class BackupsController extends AbstractLogViewerController
protected string $csrfNamespace = 'admin_backups'; protected string $csrfNamespace = 'admin_backups';
public function __construct( public function __construct(
StorageLocationRepository $storageLocationRepo, Entity\Repository\SettingsRepository $settingsRepo,
Settings $settings, Entity\Repository\StorageLocationRepository $storageLocationRepo,
RunBackupTask $backup_task, RunBackupTask $backup_task,
MessageBus $messageBus MessageBus $messageBus
) { ) {
$this->storageLocationRepo = $storageLocationRepo; $this->storageLocationRepo = $storageLocationRepo;
$this->settings = $settingsRepo->readSettings();
$this->settings = $settings;
$this->backupTask = $backup_task; $this->backupTask = $backup_task;
$this->messageBus = $messageBus; $this->messageBus = $messageBus;
} }
@ -48,7 +46,8 @@ class BackupsController extends AbstractLogViewerController
public function __invoke(ServerRequest $request, Response $response): ResponseInterface public function __invoke(ServerRequest $request, Response $response): ResponseInterface
{ {
$backups = []; $backups = [];
foreach ($this->storageLocationRepo->findAllByType(StorageLocation::TYPE_BACKUP) as $storageLocation) { $storageLocations = $this->storageLocationRepo->findAllByType(Entity\StorageLocation::TYPE_BACKUP);
foreach ($storageLocations as $storageLocation) {
$fs = $storageLocation->getFilesystem(); $fs = $storageLocation->getFilesystem();
foreach ($fs->listContents('', true) as $file) { foreach ($fs->listContents('', true) as $file) {
$file['storageLocationId'] = $storageLocation->getId(); $file['storageLocationId'] = $storageLocation->getId();
@ -58,14 +57,18 @@ class BackupsController extends AbstractLogViewerController
} }
$backups = array_reverse($backups); $backups = array_reverse($backups);
return $request->getView()->renderToResponse($response, 'admin/backups/index', [ return $request->getView()->renderToResponse(
'backups' => $backups, $response,
'is_enabled' => $this->settings->isBackupEnabled(), 'admin/backups/index',
'last_run' => $this->settings->getBackupLastRun(), [
'last_result' => $this->settings->getBackupLastResult(), 'backups' => $backups,
'last_output' => $this->settings->getBackupLastOutput(), 'is_enabled' => $this->settings->isBackupEnabled(),
'csrf' => $request->getCsrf()->generate($this->csrfNamespace), 'last_run' => $this->settings->getBackupLastRun(),
]); 'last_result' => $this->settings->getBackupLastResult(),
'last_output' => $this->settings->getBackupLastOutput(),
'csrf' => $request->getCsrf()->generate($this->csrfNamespace),
]
);
} }
public function configureAction( public function configureAction(
@ -78,11 +81,15 @@ class BackupsController extends AbstractLogViewerController
return $response->withRedirect($request->getRouter()->fromHere('admin:backups:index')); return $response->withRedirect($request->getRouter()->fromHere('admin:backups:index'));
} }
return $request->getView()->renderToResponse($response, 'system/form_page', [ return $request->getView()->renderToResponse(
'form' => $settingsForm, $response,
'render_mode' => 'edit', 'system/form_page',
'title' => __('Configure Backups'), [
]); 'form' => $settingsForm,
'render_mode' => 'edit',
'title' => __('Configure Backups'),
]
);
} }
public function runAction( public function runAction(
@ -90,9 +97,17 @@ class BackupsController extends AbstractLogViewerController
Response $response, Response $response,
Config $config Config $config
): ResponseInterface { ): ResponseInterface {
$runForm = new Form($config->get('forms/backup_run', [ $runForm = new Form(
'storageLocations' => $this->storageLocationRepo->fetchSelectByType(StorageLocation::TYPE_BACKUP, true), $config->get(
])); 'forms/backup_run',
[
'storageLocations' => $this->storageLocationRepo->fetchSelectByType(
Entity\StorageLocation::TYPE_BACKUP,
true
),
]
)
);
// Handle submission. // Handle submission.
if ($request->isPost() && $runForm->isValid($request->getParsedBody())) { if ($request->isPost() && $runForm->isValid($request->getParsedBody())) {
@ -113,18 +128,26 @@ class BackupsController extends AbstractLogViewerController
$this->messageBus->dispatch($message); $this->messageBus->dispatch($message);
return $request->getView()->renderToResponse($response, 'admin/backups/run', [ return $request->getView()->renderToResponse(
'title' => __('Run Manual Backup'), $response,
'path' => $data['path'], 'admin/backups/run',
'outputLog' => basename($tempFile), [
]); 'title' => __('Run Manual Backup'),
'path' => $data['path'],
'outputLog' => basename($tempFile),
]
);
} }
return $request->getView()->renderToResponse($response, 'system/form_page', [ return $request->getView()->renderToResponse(
'form' => $runForm, $response,
'render_mode' => 'edit', 'system/form_page',
'title' => __('Run Manual Backup'), [
]); 'form' => $runForm,
'render_mode' => 'edit',
'title' => __('Run Manual Backup'),
]
);
} }
public function logAction( public function logAction(
@ -173,12 +196,12 @@ class BackupsController extends AbstractLogViewerController
[$storageLocationId, $path] = explode('|', $pathStr); [$storageLocationId, $path] = explode('|', $pathStr);
$storageLocation = $this->storageLocationRepo->findByType( $storageLocation = $this->storageLocationRepo->findByType(
StorageLocation::TYPE_BACKUP, Entity\StorageLocation::TYPE_BACKUP,
(int)$storageLocationId (int)$storageLocationId
); );
if (!($storageLocation instanceof StorageLocation)) { if (!($storageLocation instanceof Entity\StorageLocation)) {
throw new \InvalidArgumentException('Invalid storage location.'); throw new \InvalidArgumentException('Invalid storage location.');
} }

View file

@ -2,8 +2,7 @@
namespace App\Controller\Admin; namespace App\Controller\Admin;
use App\Entity\Repository\SettingsTableRepository; use App\Entity\Repository\SettingsRepository;
use App\Entity\Settings;
use App\Form\GeoLiteSettingsForm; use App\Form\GeoLiteSettingsForm;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
@ -27,13 +26,17 @@ class InstallGeoLiteController
$flash = $request->getFlash(); $flash = $request->getFlash();
try { try {
$syncTask->updateDatabase(); $settings = $form->getEntityRepository()->readSettings();
$syncTask->updateDatabase($settings->getGeoliteLicenseKey() ?? '');
$flash->addMessage(__('Changes saved.'), Flash::SUCCESS); $flash->addMessage(__('Changes saved.'), Flash::SUCCESS);
} catch (Exception $e) { } catch (Exception $e) {
$flash->addMessage(__( $flash->addMessage(
'An error occurred while downloading the GeoLite database: %s', __(
$e->getMessage() . ' (' . $e->getFile() . ' L' . $e->getLine() . ')' 'An error occurred while downloading the GeoLite database: %s',
), Flash::ERROR); $e->getMessage() . ' (' . $e->getFile() . ' L' . $e->getLine() . ')'
),
Flash::ERROR
);
} }
return $response->withRedirect($request->getUri()->getPath()); return $response->withRedirect($request->getUri()->getPath());
@ -41,25 +44,29 @@ class InstallGeoLiteController
$version = GeoLite::getVersion(); $version = GeoLite::getVersion();
return $request->getView()->renderToResponse($response, 'admin/install_geolite/index', [ return $request->getView()->renderToResponse(
'form' => $form, $response,
'title' => __('Install GeoLite IP Database'), 'admin/install_geolite/index',
'version' => $version, [
'csrf' => $request->getCsrf()->generate($this->csrf_namespace), 'form' => $form,
]); 'title' => __('Install GeoLite IP Database'),
'version' => $version,
'csrf' => $request->getCsrf()->generate($this->csrf_namespace),
]
);
} }
public function uninstallAction( public function uninstallAction(
ServerRequest $request, ServerRequest $request,
Response $response, Response $response,
Settings $settings, SettingsRepository $settingsRepo,
SettingsTableRepository $settingsTableRepo,
$csrf $csrf
): ResponseInterface { ): ResponseInterface {
$request->getCsrf()->verify($csrf, $this->csrf_namespace); $request->getCsrf()->verify($csrf, $this->csrf_namespace);
$settings = $settingsRepo->readSettings();
$settings->setGeoliteLicenseKey(null); $settings->setGeoliteLicenseKey(null);
$settingsTableRepo->writeSettings($settings); $settingsRepo->writeSettings($settings);
@unlink(GeoLite::getDatabasePath()); @unlink(GeoLite::getDatabasePath());

View file

@ -20,23 +20,18 @@ class SettingsController
protected ValidatorInterface $validator; protected ValidatorInterface $validator;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo; protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Settings $settings;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsTableRepo, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Settings $settings,
Serializer $serializer, Serializer $serializer,
ValidatorInterface $validator ValidatorInterface $validator
) { ) {
$this->em = $em; $this->em = $em;
$this->serializer = $serializer; $this->serializer = $serializer;
$this->validator = $validator; $this->validator = $validator;
$this->settingsRepo = $settingsRepo;
$this->settingsTableRepo = $settingsTableRepo;
$this->settings = $settings;
} }
/** /**
@ -55,7 +50,8 @@ class SettingsController
*/ */
public function listAction(ServerRequest $request, Response $response): ResponseInterface public function listAction(ServerRequest $request, Response $response): ResponseInterface
{ {
return $response->withJson($this->serializer->normalize($this->settings, null)); $settings = $this->settingsRepo->readSettings();
return $response->withJson($this->serializer->normalize($settings, null));
} }
/** /**
@ -79,7 +75,7 @@ class SettingsController
*/ */
public function updateAction(ServerRequest $request, Response $response): ResponseInterface public function updateAction(ServerRequest $request, Response $response): ResponseInterface
{ {
$this->settingsTableRepo->writeSettings($request->getParsedBody()); $this->settingsRepo->writeSettings($request->getParsedBody());
return $response->withJson(new Entity\Api\Status()); return $response->withJson(new Entity\Api\Status());
} }

View file

@ -25,12 +25,12 @@ class NowplayingController implements EventSubscriberInterface
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
CacheInterface $cache, CacheInterface $cache,
EventDispatcher $dispatcher EventDispatcher $dispatcher
) { ) {
$this->em = $em; $this->em = $em;
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
$this->cache = $cache; $this->cache = $cache;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
} }
@ -122,9 +122,12 @@ class NowplayingController implements EventSubscriberInterface
// If unauthenticated, hide non-public stations from full view. // If unauthenticated, hide non-public stations from full view.
if ($request->getAttribute('user') === null) { if ($request->getAttribute('user') === null) {
$np = array_filter($np, function ($np_row) { $np = array_filter(
return $np_row->station->is_public; $np,
}); function ($np_row) {
return $np_row->station->is_public;
}
);
// Prevent NP array from returning as an object. // Prevent NP array from returning as an object.
$np = array_values($np); $np = array_values($np);

View file

@ -2,7 +2,7 @@
namespace App\Controller\Frontend\Account; namespace App\Controller\Frontend\Account;
use App\Entity\Settings; use App\Entity\Repository\SettingsRepository;
use App\Entity\User; use App\Entity\User;
use App\Exception\RateLimitExceededException; use App\Exception\RateLimitExceededException;
use App\Http\Response; use App\Http\Response;
@ -20,12 +20,14 @@ class LoginAction
Response $response, Response $response,
EntityManagerInterface $em, EntityManagerInterface $em,
RateLimit $rateLimit, RateLimit $rateLimit,
Settings $settings SettingsRepository $settingsRepo
): ResponseInterface { ): ResponseInterface {
$auth = $request->getAuth(); $auth = $request->getAuth();
$acl = $request->getAcl(); $acl = $request->getAcl();
// Check installation completion progress. // Check installation completion progress.
$settings = $settingsRepo->readSettings();
if (!$settings->isSetupComplete()) { if (!$settings->isSetupComplete()) {
$num_users = (int)$em->createQuery( $num_users = (int)$em->createQuery(
<<<'DQL' <<<'DQL'

View file

@ -34,14 +34,14 @@ class DashboardController
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Acl $acl, Acl $acl,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
CacheInterface $cache, CacheInterface $cache,
Adapters $adapter_manager, Adapters $adapter_manager,
EventDispatcher $dispatcher EventDispatcher $dispatcher
) { ) {
$this->em = $em; $this->em = $em;
$this->acl = $acl; $this->acl = $acl;
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
$this->cache = $cache; $this->cache = $cache;
$this->adapter_manager = $adapter_manager; $this->adapter_manager = $adapter_manager;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
@ -59,11 +59,14 @@ class DashboardController
$stations = $this->em->getRepository(Entity\Station::class)->findAll(); $stations = $this->em->getRepository(Entity\Station::class)->findAll();
// Don't show stations the user can't manage. // Don't show stations the user can't manage.
$stations = array_filter($stations, function ($station) use ($user) { $stations = array_filter(
/** @var Entity\Station $station */ $stations,
return $station->isEnabled() && function ($station) use ($user) {
$this->acl->userAllowed($user, Acl::STATION_VIEW, $station->getId()); /** @var Entity\Station $station */
}); return $station->isEnabled() &&
$this->acl->userAllowed($user, Acl::STATION_VIEW, $station->getId());
}
);
if (empty($stations) && !$show_admin) { if (empty($stations) && !$show_admin) {
return $view->renderToResponse($response, 'frontend/index/noaccess'); return $view->renderToResponse($response, 'frontend/index/noaccess');
@ -136,13 +139,17 @@ class DashboardController
} }
} }
return $view->renderToResponse($response, 'frontend/index/index', [ return $view->renderToResponse(
'stations' => ['stations' => $view_stations], $response,
'station_ids' => $station_ids, 'frontend/index/index',
'show_admin' => $show_admin, [
'metrics' => $metrics, 'stations' => ['stations' => $view_stations],
'notifications' => $notifications, 'station_ids' => $station_ids,
]); 'show_admin' => $show_admin,
'metrics' => $metrics,
'notifications' => $notifications,
]
);
} }
/** /**

View file

@ -12,9 +12,10 @@ class IndexController
public function indexAction( public function indexAction(
ServerRequest $request, ServerRequest $request,
Response $response, Response $response,
Entity\Settings $settings Entity\Repository\SettingsRepository $settingsRepo
): ResponseInterface { ): ResponseInterface {
// Redirect to complete setup, if it hasn't been completed yet. // Redirect to complete setup, if it hasn't been completed yet.
$settings = $settingsRepo->readSettings();
if (!$settings->isSetupComplete()) { if (!$settings->isSetupComplete()) {
return $response->withRedirect($request->getRouter()->named('setup:index')); return $response->withRedirect($request->getRouter()->named('setup:index'));
} }

View file

@ -17,22 +17,18 @@ class SetupController
{ {
protected EntityManagerInterface $em; protected EntityManagerInterface $em;
protected Entity\Settings $settings; protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo;
protected Environment $environment; protected Environment $environment;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsRepository, Entity\Repository\SettingsRepository $settingsRepository,
Environment $environment, Environment $environment
Entity\Settings $settings
) { ) {
$this->em = $em; $this->em = $em;
$this->settingsTableRepo = $settingsRepository; $this->settingsRepo = $settingsRepository;
$this->environment = $environment; $this->environment = $environment;
$this->settings = $settings;
} }
/** /**
@ -54,7 +50,8 @@ class SetupController
*/ */
protected function getSetupStep(ServerRequest $request): string protected function getSetupStep(ServerRequest $request): string
{ {
if ($this->settings->isSetupComplete()) { $settings = $this->settingsRepo->readSettings();
if ($settings->isSetupComplete()) {
return 'complete'; return 'complete';
} }
@ -182,9 +179,13 @@ class SetupController
return $response->withRedirect($request->getRouter()->named('setup:settings')); return $response->withRedirect($request->getRouter()->named('setup:settings'));
} }
return $request->getView()->renderToResponse($response, 'frontend/setup/station', [ return $request->getView()->renderToResponse(
'form' => $stationForm, $response,
]); 'frontend/setup/station',
[
'form' => $stationForm,
]
);
} }
/** /**
@ -208,8 +209,9 @@ class SetupController
} }
if ($settingsForm->process($request)) { if ($settingsForm->process($request)) {
$this->settings->updateSetupComplete(); $settings = $this->settingsRepo->readSettings();
$this->settingsTableRepo->writeSettings($this->settings); $settings->updateSetupComplete();
$this->settingsRepo->writeSettings($settings);
// Notify the user and redirect to homepage. // Notify the user and redirect to homepage.
$request->getFlash()->addMessage( $request->getFlash()->addMessage(
@ -224,8 +226,12 @@ class SetupController
return $response->withRedirect($request->getRouter()->named('dashboard')); return $response->withRedirect($request->getRouter()->named('dashboard'));
} }
return $request->getView()->renderToResponse($response, 'frontend/setup/settings', [ return $request->getView()->renderToResponse(
'form' => $settingsForm, $response,
]); 'frontend/setup/settings',
[
'form' => $settingsForm,
]
);
} }
} }

View file

@ -13,11 +13,12 @@ class ListenersController
public function __invoke( public function __invoke(
ServerRequest $request, ServerRequest $request,
Response $response, Response $response,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
IpGeolocation $ipGeo IpGeolocation $ipGeo
): ResponseInterface { ): ResponseInterface {
$view = $request->getView(); $view = $request->getView();
$settings = $settingsRepo->readSettings();
$analytics_level = $settings->getAnalytics(); $analytics_level = $settings->getAnalytics();
if ($analytics_level !== Entity\Analytics::LEVEL_ALL) { if ($analytics_level !== Entity\Analytics::LEVEL_ALL) {
@ -25,8 +26,12 @@ class ListenersController
} }
$attribution = $ipGeo->getAttribution(); $attribution = $ipGeo->getAttribution();
return $view->renderToResponse($response, 'stations/reports/listeners', [ return $view->renderToResponse(
'attribution' => $attribution, $response,
]); 'stations/reports/listeners',
[
'attribution' => $attribution,
]
);
} }
} }

View file

@ -23,11 +23,11 @@ class OverviewController
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\AnalyticsRepository $analyticsRepo Entity\Repository\AnalyticsRepository $analyticsRepo
) { ) {
$this->em = $em; $this->em = $em;
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
$this->analyticsRepo = $analyticsRepo; $this->analyticsRepo = $analyticsRepo;
} }
@ -237,25 +237,32 @@ class OverviewController
$songs[] = $song_row; $songs[] = $song_row;
} }
usort($songs, function ($a_arr, $b_arr) { usort(
$a = $a_arr['stat_delta']; $songs,
$b = $b_arr['stat_delta']; function ($a_arr, $b_arr) {
$a = $a_arr['stat_delta'];
$b = $b_arr['stat_delta'];
return $a <=> $b; return $a <=> $b;
}); }
);
return $request->getView()->renderToResponse($response, 'stations/reports/overview', [ return $request->getView()->renderToResponse(
'charts' => [ $response,
'daily' => json_encode($daily_data, JSON_THROW_ON_ERROR), 'stations/reports/overview',
'daily_alt' => implode('', $daily_alt), [
'hourly' => json_encode($hourly_data, JSON_THROW_ON_ERROR), 'charts' => [
'hourly_alt' => implode('', $hourly_alt), 'daily' => json_encode($daily_data, JSON_THROW_ON_ERROR),
'day_of_week' => json_encode($day_of_week_data, JSON_THROW_ON_ERROR), 'daily_alt' => implode('', $daily_alt),
'day_of_week_alt' => implode('', $day_of_week_alt), 'hourly' => json_encode($hourly_data, JSON_THROW_ON_ERROR),
], 'hourly_alt' => implode('', $hourly_alt),
'song_totals' => $song_totals, 'day_of_week' => json_encode($day_of_week_data, JSON_THROW_ON_ERROR),
'best_performing_songs' => array_reverse(array_slice($songs, -5)), 'day_of_week_alt' => implode('', $day_of_week_alt),
'worst_performing_songs' => array_slice($songs, 0, 5), ],
]); 'song_totals' => $song_totals,
'best_performing_songs' => array_reverse(array_slice($songs, -5)),
'worst_performing_songs' => array_slice($songs, 0, 5),
]
);
} }
} }

View file

@ -22,11 +22,11 @@ class StreamersController
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
AzuraCastCentral $ac_central, AzuraCastCentral $ac_central,
Entity\Settings $settings Entity\Repository\SettingsRepository $settingsRepo
) { ) {
$this->em = $em; $this->em = $em;
$this->ac_central = $ac_central; $this->ac_central = $ac_central;
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
} }
public function __invoke(ServerRequest $request, Response $response): ResponseInterface public function __invoke(ServerRequest $request, Response $response): ResponseInterface
@ -60,12 +60,16 @@ class StreamersController
$be_settings = $station->getBackendConfig(); $be_settings = $station->getBackendConfig();
return $view->renderToResponse($response, 'stations/streamers/index', [ return $view->renderToResponse(
'server_url' => $this->settings->getBaseUrl(), $response,
'stream_port' => $backend->getStreamPort($station), 'stations/streamers/index',
'ip' => $this->ac_central->getIp(), [
'dj_mount_point' => $be_settings['dj_mount_point'] ?? '/', 'server_url' => $this->settings->getBaseUrl(),
'station_tz' => $station->getTimezone(), 'stream_port' => $backend->getStreamPort($station),
]); 'ip' => $this->ac_central->getIp(),
'dj_mount_point' => $be_settings['dj_mount_point'] ?? '/',
'station_tz' => $station->getTimezone(),
]
);
} }
} }

View file

@ -32,14 +32,14 @@ class Customization
protected string $instanceName = ''; protected string $instanceName = '';
public function __construct( public function __construct(
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment, Environment $environment,
ServerRequestInterface $request ServerRequestInterface $request
) { ) {
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
$this->environment = $environment; $this->environment = $environment;
$this->instanceName = $settings->getInstanceName() ?? ''; $this->instanceName = $this->settings->getInstanceName() ?? '';
// Register current user // Register current user
$this->user = $request->getAttribute(ServerRequest::ATTR_USER); $this->user = $request->getAttribute(ServerRequest::ATTR_USER);
@ -52,7 +52,7 @@ class Customization
if (!empty($queryParams['theme'])) { if (!empty($queryParams['theme'])) {
$this->publicTheme = $this->theme = $queryParams['theme']; $this->publicTheme = $this->theme = $queryParams['theme'];
} else { } else {
$this->publicTheme = $settings->getPublicTheme() ?? $this->publicTheme; $this->publicTheme = $this->settings->getPublicTheme() ?? $this->publicTheme;
if (null !== $this->user && !empty($this->user->getTheme())) { if (null !== $this->user && !empty($this->user->getTheme())) {
$this->theme = (string)$this->user->getTheme(); $this->theme = (string)$this->user->getTheme();

View file

@ -13,8 +13,10 @@ use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
class SettingsTableRepository extends Repository class SettingsRepository extends Repository
{ {
protected static ?Entity\Settings $instance = null;
protected const CACHE_KEY = 'settings'; protected const CACHE_KEY = 'settings';
protected const CACHE_TTL = 600; protected const CACHE_TTL = 600;
@ -23,6 +25,8 @@ class SettingsTableRepository extends Repository
protected ValidatorInterface $validator; protected ValidatorInterface $validator;
protected string $entityClass = Entity\SettingsTable::class;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Serializer $serializer, Serializer $serializer,
@ -37,32 +41,18 @@ class SettingsTableRepository extends Repository
$this->validator = $validator; $this->validator = $validator;
} }
public function readSettings(): Entity\Settings public function readSettings(bool $reload = false): Entity\Settings
{ {
if (Entity\Settings::hasInstance()) { if ($reload || null === self::$instance) {
return Entity\Settings::getInstance(); self::$instance = $this->arrayToObject($this->readSettingsArray());
} else {
$settings = $this->arrayToObject($this->readSettingsArray());
Entity\Settings::setInstance($settings);
return $settings;
} }
return self::$instance;
} }
/** public function clearSettingsInstance(): void
* Given a long-running process, update the Settings entity to have the latest data.
*
* @param array|null $newData
*
*/
public function updateSettings(?array $newData = null): Entity\Settings
{ {
if (null === $newData) { self::$instance = null;
$newData = $this->readSettingsArray();
}
$settings = $this->arrayToObject($newData, $this->readSettings());
return $settings;
} }
/** /**
@ -70,6 +60,10 @@ class SettingsTableRepository extends Repository
*/ */
public function readSettingsArray(): array public function readSettingsArray(): array
{ {
if ($this->cache->has(self::CACHE_KEY)) {
return $this->cache->get(self::CACHE_KEY);
}
$allRecords = []; $allRecords = [];
foreach ($this->repository->findAll() as $record) { foreach ($this->repository->findAll() as $record) {
/** @var Entity\SettingsTable $record */ /** @var Entity\SettingsTable $record */
@ -87,7 +81,7 @@ class SettingsTableRepository extends Repository
public function writeSettings($settingsObj): void public function writeSettings($settingsObj): void
{ {
if (is_array($settingsObj)) { if (is_array($settingsObj)) {
$settingsObj = $this->updateSettings($settingsObj); $settingsObj = $this->arrayToObject($settingsObj, $this->readSettings(true));
} }
$errors = $this->validator->validate($settingsObj); $errors = $this->validator->validate($settingsObj);
@ -155,9 +149,12 @@ class SettingsTableRepository extends Repository
protected function arrayToObject(array $settings, ?Entity\Settings $existingSettings = null): Entity\Settings protected function arrayToObject(array $settings, ?Entity\Settings $existingSettings = null): Entity\Settings
{ {
$settings = array_filter($settings, function ($value) { $settings = array_filter(
return null !== $value; $settings,
}); function ($value) {
return null !== $value;
}
);
$context = []; $context = [];
if (null !== $existingSettings) { if (null !== $existingSettings) {

View file

@ -34,28 +34,29 @@ class StationRepository extends Repository
protected StorageLocationRepository $storageLocationRepo; protected StorageLocationRepository $storageLocationRepo;
protected Entity\Settings $settings; protected SettingsRepository $settingsRepo;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Serializer $serializer, Serializer $serializer,
Environment $environment, Environment $environment,
SettingsRepository $settingsRepo,
StorageLocationRepository $storageLocationRepo, StorageLocationRepository $storageLocationRepo,
LoggerInterface $logger, LoggerInterface $logger,
CheckMediaTask $mediaSync, CheckMediaTask $mediaSync,
Adapters $adapters, Adapters $adapters,
Configuration $configuration, Configuration $configuration,
ValidatorInterface $validator, ValidatorInterface $validator,
CacheInterface $cache, CacheInterface $cache
Entity\Settings $settings
) { ) {
$this->mediaSync = $mediaSync; $this->mediaSync = $mediaSync;
$this->adapters = $adapters; $this->adapters = $adapters;
$this->configuration = $configuration; $this->configuration = $configuration;
$this->validator = $validator; $this->validator = $validator;
$this->cache = $cache; $this->cache = $cache;
$this->settingsRepo = $settingsRepo;
$this->storageLocationRepo = $storageLocationRepo; $this->storageLocationRepo = $storageLocationRepo;
$this->settings = $settings;
parent::__construct($em, $serializer, $environment, $logger); parent::__construct($em, $serializer, $environment, $logger);
} }
@ -341,7 +342,8 @@ class StationRepository extends Repository
} }
} }
$custom_url = trim($this->settings->getDefaultAlbumArtUrl()); $settings = $this->settingsRepo->readSettings();
$custom_url = trim($settings->getDefaultAlbumArtUrl());
if (!empty($custom_url)) { if (!empty($custom_url)) {
return new Uri($custom_url); return new Uri($custom_url);

View file

@ -664,7 +664,9 @@ class Settings
public function getGeoliteLicenseKey(): ?string public function getGeoliteLicenseKey(): ?string
{ {
return $this->geoliteLicenseKey; return (null === $this->geoliteLicenseKey)
? null
: trim($this->geoliteLicenseKey);
} }
public function setGeoliteLicenseKey(?string $geoliteLicenseKey): void public function setGeoliteLicenseKey(?string $geoliteLicenseKey): void

View file

@ -11,13 +11,13 @@ abstract class AbstractSettingsForm extends Form
{ {
protected EntityManagerInterface $em; protected EntityManagerInterface $em;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo; protected Entity\Repository\SettingsRepository $settingsRepo;
protected Environment $environment; protected Environment $environment;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsTableRepo, Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment, Environment $environment,
array $formConfig array $formConfig
) { ) {
@ -25,7 +25,7 @@ abstract class AbstractSettingsForm extends Form
$this->em = $em; $this->em = $em;
$this->environment = $environment; $this->environment = $environment;
$this->settingsTableRepo = $settingsTableRepo; $this->settingsRepo = $settingsRepo;
} }
public function getEntityManager(): EntityManagerInterface public function getEntityManager(): EntityManagerInterface
@ -33,9 +33,9 @@ abstract class AbstractSettingsForm extends Form
return $this->em; return $this->em;
} }
public function getEntityRepository(): Entity\Repository\SettingsTableRepository public function getEntityRepository(): Entity\Repository\SettingsRepository
{ {
return $this->settingsTableRepo; return $this->settingsRepo;
} }
public function getEnvironment(): Environment public function getEnvironment(): Environment
@ -46,7 +46,7 @@ abstract class AbstractSettingsForm extends Form
public function process(ServerRequest $request): bool public function process(ServerRequest $request): bool
{ {
// Populate the form with existing values (if they exist). // Populate the form with existing values (if they exist).
$defaults = $this->settingsTableRepo->readSettingsArray(); $defaults = $this->settingsRepo->readSettingsArray();
// Use current URI from request if the base URL isn't set. // Use current URI from request if the base URL isn't set.
if (empty($defaults['baseUrl'])) { if (empty($defaults['baseUrl'])) {
@ -59,7 +59,7 @@ abstract class AbstractSettingsForm extends Form
// Handle submission. // Handle submission.
if ('POST' === $request->getMethod() && $this->isValid($request->getParsedBody())) { if ('POST' === $request->getMethod() && $this->isValid($request->getParsedBody())) {
$data = $this->getValues(); $data = $this->getValues();
$this->settingsTableRepo->writeSettings($data); $this->settingsRepo->writeSettings($data);
return true; return true;
} }

View file

@ -11,19 +11,25 @@ class BackupSettingsForm extends AbstractSettingsForm
{ {
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsTableRepo, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\StorageLocationRepository $storageLocationRepo, Entity\Repository\StorageLocationRepository $storageLocationRepo,
Environment $environment, Environment $environment,
Config $config Config $config
) { ) {
$formConfig = $config->get('forms/backup', [ $formConfig = $config->get(
'settings' => $environment, 'forms/backup',
'storageLocations' => $storageLocationRepo->fetchSelectByType(Entity\StorageLocation::TYPE_BACKUP, true), [
]); 'settings' => $environment,
'storageLocations' => $storageLocationRepo->fetchSelectByType(
Entity\StorageLocation::TYPE_BACKUP,
true
),
]
);
parent::__construct( parent::__construct(
$em, $em,
$settingsTableRepo, $settingsRepo,
$environment, $environment,
$formConfig $formConfig
); );

View file

@ -11,13 +11,16 @@ class BrandingSettingsForm extends AbstractSettingsForm
{ {
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsRepo, Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment, Environment $environment,
Config $config Config $config
) { ) {
$formConfig = $config->get('forms/branding', [ $formConfig = $config->get(
'settings' => $environment, 'forms/branding',
]); [
'settings' => $environment,
]
);
parent::__construct( parent::__construct(
$em, $em,

View file

@ -14,7 +14,7 @@ class GeoLiteSettingsForm extends AbstractSettingsForm
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsRepo, Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment, Environment $environment,
Config $config, Config $config,
UpdateGeoLiteTask $syncTask UpdateGeoLiteTask $syncTask

View file

@ -13,15 +13,18 @@ class SettingsForm extends AbstractSettingsForm
{ {
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsRepo, Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment, Environment $environment,
Version $version, Version $version,
Config $config Config $config
) { ) {
$formConfig = $config->get('forms/settings', [ $formConfig = $config->get(
'settings' => $environment, 'forms/settings',
'version' => $version, [
]); 'settings' => $environment,
'version' => $version,
]
);
parent::__construct( parent::__construct(
$em, $em,

View file

@ -8,7 +8,8 @@ use App\Exception;
use App\Exception\NotLoggedInException; use App\Exception\NotLoggedInException;
use App\Exception\PermissionDeniedException; use App\Exception\PermissionDeniedException;
use App\Session\Flash; use App\Session\Flash;
use App\ViewFactory; use App\View;
use DI\FactoryInterface;
use Gettext\Translator; use Gettext\Translator;
use Mezzio\Session\SessionInterface; use Mezzio\Session\SessionInterface;
use Monolog\Logger; use Monolog\Logger;
@ -31,21 +32,21 @@ class ErrorHandler extends \Slim\Handlers\ErrorHandler
protected Router $router; protected Router $router;
protected ViewFactory $viewFactory; protected FactoryInterface $factory;
protected Environment $environment; protected Environment $environment;
public function __construct( public function __construct(
App $app, App $app,
FactoryInterface $factory,
Logger $logger, Logger $logger,
Router $router, Router $router,
ViewFactory $viewFactory,
Environment $environment Environment $environment
) { ) {
parent::__construct($app->getCallableResolver(), $app->getResponseFactory(), $logger); parent::__construct($app->getCallableResolver(), $app->getResponseFactory(), $logger);
$this->environment = $environment; $this->environment = $environment;
$this->viewFactory = $viewFactory; $this->factory = $factory;
$this->router = $router; $this->router = $router;
} }
@ -130,12 +131,14 @@ class ErrorHandler extends \Slim\Handlers\ErrorHandler
if (false !== stripos($ua, 'curl')) { if (false !== stripos($ua, 'curl')) {
$response = $this->responseFactory->createResponse($this->statusCode); $response = $this->responseFactory->createResponse($this->statusCode);
$response->getBody()->write(sprintf( $response->getBody()->write(
'Error: %s on %s L%s', sprintf(
$this->exception->getMessage(), 'Error: %s on %s L%s',
$this->exception->getFile(), $this->exception->getMessage(),
$this->exception->getLine() $this->exception->getFile(),
)); $this->exception->getLine()
)
);
return $response; return $response;
} }
@ -150,7 +153,12 @@ class ErrorHandler extends \Slim\Handlers\ErrorHandler
} }
try { try {
$view = $this->viewFactory->create($this->request); $view = $this->factory->make(
View::class,
[
'request' => $this->request,
]
);
return $view->renderToResponse( return $view->renderToResponse(
$response, $response,
@ -238,7 +246,12 @@ class ErrorHandler extends \Slim\Handlers\ErrorHandler
} }
try { try {
$view = $this->viewFactory->create($this->request); $view = $this->factory->make(
View::class,
[
'request' => $this->request,
]
);
return $view->renderToResponse( return $view->renderToResponse(
$response, $response,

View file

@ -9,6 +9,7 @@ use GuzzleHttp\Psr7\UriResolver;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
use Slim\App;
use Slim\Interfaces\RouteInterface; use Slim\Interfaces\RouteInterface;
use Slim\Interfaces\RouteParserInterface; use Slim\Interfaces\RouteParserInterface;
use Slim\Routing\RouteContext; use Slim\Routing\RouteContext;
@ -25,12 +26,12 @@ class Router implements RouterInterface
public function __construct( public function __construct(
Environment $environment, Environment $environment,
RouteParserInterface $routeParser, App $app,
Entity\Settings $settings Entity\Repository\SettingsRepository $settingsRepo
) { ) {
$this->environment = $environment; $this->environment = $environment;
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
$this->routeParser = $routeParser; $this->routeParser = $app->getRouteCollector()->getRouteParser();
} }
/** /**

View file

@ -2,17 +2,17 @@
namespace App\MessageQueue; namespace App\MessageQueue;
use App\Entity\Repository\SettingsTableRepository; use App\Entity\Repository\SettingsRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent; use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
class ReloadSettingsMiddleware implements EventSubscriberInterface class ReloadSettingsMiddleware implements EventSubscriberInterface
{ {
protected SettingsTableRepository $settingsTableRepo; protected SettingsRepository $settingsRepo;
public function __construct(SettingsTableRepository $settingsTableRepo) public function __construct(SettingsRepository $settingsRepo)
{ {
$this->settingsTableRepo = $settingsTableRepo; $this->settingsRepo = $settingsRepo;
} }
/** /**
@ -27,6 +27,6 @@ class ReloadSettingsMiddleware implements EventSubscriberInterface
public function resetSettings(WorkerMessageReceivedEvent $event): void public function resetSettings(WorkerMessageReceivedEvent $event): void
{ {
$this->settingsTableRepo->updateSettings(); $this->settingsRepo->clearSettingsInstance();
} }
} }

View file

@ -3,7 +3,8 @@
namespace App\Middleware; namespace App\Middleware;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\ViewFactory; use App\View;
use DI\FactoryInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
@ -14,16 +15,21 @@ use Psr\Http\Server\RequestHandlerInterface;
*/ */
class EnableView implements MiddlewareInterface class EnableView implements MiddlewareInterface
{ {
protected ViewFactory $viewFactory; protected FactoryInterface $factory;
public function __construct(ViewFactory $viewFactory) public function __construct(FactoryInterface $factory)
{ {
$this->viewFactory = $viewFactory; $this->factory = $factory;
} }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$view = $this->viewFactory->create($request); $view = $this->factory->make(
View::class,
[
'request' => $request,
]
);
$request = $request->withAttribute(ServerRequest::ATTR_VIEW, $view); $request = $request->withAttribute(ServerRequest::ATTR_VIEW, $view);
return $handler->handle($request); return $handler->handle($request);

View file

@ -2,9 +2,7 @@
namespace App\Middleware; namespace App\Middleware;
use App\Assets;
use App\Entity; use App\Entity;
use App\Http\Response;
use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@ -19,18 +17,14 @@ class EnforceSecurity implements MiddlewareInterface
{ {
protected ResponseFactoryInterface $responseFactory; protected ResponseFactoryInterface $responseFactory;
protected Assets $assets; protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Settings $settings;
public function __construct( public function __construct(
App $app, App $app,
Assets $assets, Entity\Repository\SettingsRepository $settingsRepo
Entity\Settings $settings
) { ) {
$this->responseFactory = $app->getResponseFactory(); $this->responseFactory = $app->getResponseFactory();
$this->assets = $assets; $this->settingsRepo = $settingsRepo;
$this->settings = $settings;
} }
/** /**
@ -39,20 +33,17 @@ class EnforceSecurity implements MiddlewareInterface
*/ */
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$always_use_ssl = $this->settings->getAlwaysUseSsl(); $settings = $this->settingsRepo->readSettings();
$always_use_ssl = $settings->getAlwaysUseSsl();
$internal_api_url = mb_stripos($request->getUri()->getPath(), '/api/internal') === 0; $internal_api_url = mb_stripos($request->getUri()->getPath(), '/api/internal') === 0;
// Assemble Content Security Policy (CSP) $addHstsHeader = false;
$csp = [];
$add_hsts_header = false;
if ('https' === $request->getUri()->getScheme()) { if ('https' === $request->getUri()->getScheme()) {
// Enforce secure cookies. // Enforce secure cookies.
ini_set('session.cookie_secure', '1'); ini_set('session.cookie_secure', '1');
$csp[] = 'upgrade-insecure-requests'; $addHstsHeader = true;
$add_hsts_header = true;
} elseif ($always_use_ssl && !$internal_api_url) { } elseif ($always_use_ssl && !$internal_api_url) {
return $this->responseFactory->createResponse(307) return $this->responseFactory->createResponse(307)
->withHeader('Location', (string)$request->getUri()->withScheme('https')); ->withHeader('Location', (string)$request->getUri()->withScheme('https'));
@ -60,7 +51,7 @@ class EnforceSecurity implements MiddlewareInterface
$response = $handler->handle($request); $response = $handler->handle($request);
if ($add_hsts_header) { if ($addHstsHeader) {
$response = $response->withHeader('Strict-Transport-Security', 'max-age=3600'); $response = $response->withHeader('Strict-Transport-Security', 'max-age=3600');
} }
@ -72,19 +63,6 @@ class EnforceSecurity implements MiddlewareInterface
$response = $response->withHeader('X-Frame-Options', 'DENY'); $response = $response->withHeader('X-Frame-Options', 'DENY');
} }
if (($response instanceof Response) && !$response->hasCacheLifetime()) {
// CSP JavaScript policy
// Note: unsafe-eval included for Vue template compiling
$csp_script_src = $this->assets->getCspDomains();
$csp_script_src[] = "'self'";
$csp_script_src[] = "'unsafe-eval'";
$csp_script_src[] = "'nonce-" . $this->assets->getCspNonce() . "'";
$csp[] = 'script-src ' . implode(' ', $csp_script_src);
$response = $response->withHeader('Content-Security-Policy', implode('; ', $csp));
}
return $response; return $response;
} }
} }

View file

@ -5,8 +5,8 @@ namespace App\Middleware;
use App\Auth; use App\Auth;
use App\Customization; use App\Customization;
use App\Entity; use App\Entity;
use App\Environment;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use DI\FactoryInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\MiddlewareInterface;
@ -17,29 +17,21 @@ use Psr\Http\Server\RequestHandlerInterface;
*/ */
class GetCurrentUser implements MiddlewareInterface class GetCurrentUser implements MiddlewareInterface
{ {
protected Entity\Repository\UserRepository $userRepo; protected FactoryInterface $factory;
protected Entity\Settings $settings; public function __construct(FactoryInterface $factory)
{
protected Environment $environment; $this->factory = $factory;
public function __construct(
Entity\Repository\UserRepository $userRepo,
Environment $environment,
Entity\Settings $settings
) {
$this->userRepo = $userRepo;
$this->environment = $environment;
$this->settings = $settings;
} }
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
// Initialize the Auth for this request. // Initialize the Auth for this request.
$auth = new Auth( $auth = $this->factory->make(
$this->userRepo, Auth::class,
$request->getAttribute(ServerRequest::ATTR_SESSION), [
$this->environment 'session' => $request->getAttribute(ServerRequest::ATTR_SESSION),
]
); );
$user = ($auth->isLoggedIn()) ? $auth->getLoggedInUser() : null; $user = ($auth->isLoggedIn()) ? $auth->getLoggedInUser() : null;
@ -49,7 +41,12 @@ class GetCurrentUser implements MiddlewareInterface
->withAttribute('is_logged_in', (null !== $user)); ->withAttribute('is_logged_in', (null !== $user));
// Initialize Customization (timezones, locales, etc) based on the current logged in user. // Initialize Customization (timezones, locales, etc) based on the current logged in user.
$customization = new Customization($this->settings, $this->environment, $request); $customization = $this->factory->make(
Customization::class,
[
'request' => $request,
]
);
$request = $request $request = $request
->withAttribute('locale', $customization->getLocale()) ->withAttribute('locale', $customization->getLocale())

View file

@ -3,7 +3,6 @@
namespace App\Middleware; namespace App\Middleware;
use App\Acl; use App\Acl;
use App\Entity\Repository\RolePermissionRepository;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
@ -15,12 +14,10 @@ use Psr\Http\Server\RequestHandlerInterface;
*/ */
class InjectAcl implements MiddlewareInterface class InjectAcl implements MiddlewareInterface
{ {
protected RolePermissionRepository $rolePermRepo;
protected Acl $acl; protected Acl $acl;
public function __construct(RolePermissionRepository $rolePermRepo, Acl $acl) public function __construct(Acl $acl)
{ {
$this->rolePermRepo = $rolePermRepo;
$this->acl = $acl; $this->acl = $acl;
} }

View file

@ -16,14 +16,14 @@ class Api
{ {
protected Entity\Repository\ApiKeyRepository $api_repo; protected Entity\Repository\ApiKeyRepository $api_repo;
protected Entity\Settings $settings; protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct( public function __construct(
Entity\Repository\ApiKeyRepository $apiKeyRepository, Entity\Repository\ApiKeyRepository $apiKeyRepository,
Entity\Settings $settings Entity\Repository\SettingsRepository $settingsRepo
) { ) {
$this->api_repo = $apiKeyRepository; $this->api_repo = $apiKeyRepository;
$this->settings = $settings; $this->settingsRepo = $settingsRepo;
} }
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
@ -40,12 +40,14 @@ class Api
} }
// Set default cache control for API pages. // Set default cache control for API pages.
$prefer_browser_url = $this->settings->getPreferBrowserUrl(); $settings = $this->settingsRepo->readSettings();
$prefer_browser_url = $settings->getPreferBrowserUrl();
$response = $handler->handle($request); $response = $handler->handle($request);
// Check for a user-set CORS header override. // Check for a user-set CORS header override.
$acao_header = trim($this->settings->getApiAccessControl()); $acao_header = trim($settings->getApiAccessControl());
if (!empty($acao_header)) { if (!empty($acao_header)) {
if ('*' === $acao_header) { if ('*' === $acao_header) {
$response = $response->withHeader('Access-Control-Allow-Origin', '*'); $response = $response->withHeader('Access-Control-Allow-Origin', '*');
@ -55,7 +57,7 @@ class Api
if (!empty($origin)) { if (!empty($origin)) {
$rawOrigins = array_map('trim', explode(',', $acao_header)); $rawOrigins = array_map('trim', explode(',', $acao_header));
$rawOrigins[] = $this->settings->getBaseUrl(); $rawOrigins[] = $settings->getBaseUrl();
$origins = []; $origins = [];
foreach ($rawOrigins as $rawOrigin) { foreach ($rawOrigins as $rawOrigin) {

View file

@ -11,14 +11,16 @@ use Carbon\CarbonImmutable;
class RecentBackupCheck class RecentBackupCheck
{ {
protected Entity\Settings $settings;
protected Environment $environment; protected Environment $environment;
public function __construct(Entity\Settings $settings, Environment $environment) protected Entity\Repository\SettingsRepository $settingsRepo;
{
$this->settings = $settings; public function __construct(
Environment $environment,
Entity\Repository\SettingsRepository $settingsRepo
) {
$this->environment = $environment; $this->environment = $environment;
$this->settingsRepo = $settingsRepo;
} }
public function __invoke(GetNotifications $event): void public function __invoke(GetNotifications $event): void
@ -37,27 +39,31 @@ class RecentBackupCheck
$threshold = CarbonImmutable::now()->subWeeks(2)->getTimestamp(); $threshold = CarbonImmutable::now()->subWeeks(2)->getTimestamp();
// Don't show backup warning for freshly created installations. // Don't show backup warning for freshly created installations.
$setupComplete = $this->settings->getSetupCompleteTime(); $settings = $this->settingsRepo->readSettings();
$setupComplete = $settings->getSetupCompleteTime();
if ($setupComplete >= $threshold) { if ($setupComplete >= $threshold) {
return; return;
} }
$backupLastRun = $this->settings->getBackupLastRun(); $backupLastRun = $settings->getBackupLastRun();
if ($backupLastRun < $threshold) { if ($backupLastRun < $threshold) {
$router = $request->getRouter(); $router = $request->getRouter();
$backupUrl = $router->named('admin:backups:index'); $backupUrl = $router->named('admin:backups:index');
$event->addNotification(new Notification( $event->addNotification(
__('Installation Not Recently Backed Up'), new Notification(
// phpcs:disable Generic.Files.LineLength __('Installation Not Recently Backed Up'),
__( // phpcs:disable Generic.Files.LineLength
'This installation has not been backed up in the last two weeks. Visit the <a href="%s" target="_blank">Backups</a> page to run a new backup.', __(
$backupUrl 'This installation has not been backed up in the last two weeks. Visit the <a href="%s" target="_blank">Backups</a> page to run a new backup.',
), $backupUrl
// phpcs:enable ),
Notification::INFO // phpcs:enable
)); Notification::INFO
)
);
} }
} }
} }

View file

@ -12,14 +12,14 @@ class SyncTaskCheck
{ {
protected Runner $syncRunner; protected Runner $syncRunner;
protected Entity\Settings $settings; protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct( public function __construct(
Runner $syncRunner, Runner $syncRunner,
Entity\Settings $settings Entity\Repository\SettingsRepository $settingsRepo
) { ) {
$this->syncRunner = $syncRunner; $this->syncRunner = $syncRunner;
$this->settings = $settings; $this->settingsRepo = $settingsRepo;
} }
public function __invoke(GetNotifications $event): void public function __invoke(GetNotifications $event): void
@ -31,7 +31,9 @@ class SyncTaskCheck
return; return;
} }
$setupComplete = $this->settings->isSetupComplete(); $settings = $this->settingsRepo->readSettings();
$setupComplete = $settings->isSetupComplete();
$syncTasks = $this->syncRunner->getSyncTimes(); $syncTasks = $this->syncRunner->getSyncTimes();
foreach ($syncTasks as $taskKey => $task) { foreach ($syncTasks as $taskKey => $task) {
@ -48,17 +50,19 @@ class SyncTaskCheck
$router = $request->getRouter(); $router = $request->getRouter();
$backupUrl = $router->named('admin:debug:sync', ['type' => $taskKey]); $backupUrl = $router->named('admin:debug:sync', ['type' => $taskKey]);
$event->addNotification(new Notification( $event->addNotification(
__('Synchronized Task Not Recently Run'), new Notification(
// phpcs:disable Generic.Files.LineLength __('Synchronized Task Not Recently Run'),
__( // phpcs:disable Generic.Files.LineLength
'The "%s" synchronization task has not run recently. This may indicate an error with your installation. <a href="%s" target="_blank">Manually run the task</a> to check for errors.', __(
$task['name'], 'The "%s" synchronization task has not run recently. This may indicate an error with your installation. <a href="%s" target="_blank">Manually run the task</a> to check for errors.',
$backupUrl $task['name'],
), $backupUrl
// phpcs:enable ),
Notification::ERROR // phpcs:enable
)); Notification::ERROR
)
);
} }
} }
} }

View file

@ -10,14 +10,14 @@ use App\Version;
class UpdateCheck class UpdateCheck
{ {
protected Entity\Settings $settings;
protected Version $version; protected Version $version;
public function __construct(Entity\Settings $settings, Version $version) protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct(Version $version, Entity\Repository\SettingsRepository $settingsRepo)
{ {
$this->settings = $settings;
$this->version = $version; $this->version = $version;
$this->settingsRepo = $settingsRepo;
} }
public function __invoke(GetNotifications $event): void public function __invoke(GetNotifications $event): void
@ -29,12 +29,14 @@ class UpdateCheck
return; return;
} }
$checkForUpdates = $this->settings->getCheckForUpdates(); $settings = $this->settingsRepo->readSettings();
$checkForUpdates = $settings->getCheckForUpdates();
if (!$checkForUpdates) { if (!$checkForUpdates) {
return; return;
} }
$updateData = $this->settings->getUpdateResults(); $updateData = $settings->getUpdateResults();
if (empty($updateData)) { if (empty($updateData)) {
return; return;
} }
@ -61,11 +63,13 @@ class UpdateCheck
$instructions_string, $instructions_string,
]; ];
$event->addNotification(new Notification( $event->addNotification(
__('New AzuraCast Release Version Available'), new Notification(
implode(' ', $notification_parts), __('New AzuraCast Release Version Available'),
Notification::INFO implode(' ', $notification_parts),
)); Notification::INFO
)
);
return; return;
} }
@ -73,10 +77,12 @@ class UpdateCheck
$notification_parts = []; $notification_parts = [];
if ($updateData['rolling_updates_available'] < 15 && !empty($updateData['rolling_updates_list'])) { if ($updateData['rolling_updates_available'] < 15 && !empty($updateData['rolling_updates_list'])) {
$notification_parts[] = __('The following improvements have been made since your last update:'); $notification_parts[] = __('The following improvements have been made since your last update:');
$notification_parts[] = nl2br('<ul><li>' . implode( $notification_parts[] = nl2br(
'</li><li>', '<ul><li>' . implode(
$updateData['rolling_updates_list'] '</li><li>',
) . '</li></ul>'); $updateData['rolling_updates_list']
) . '</li></ul>'
);
} else { } else {
$notification_parts[] = '<b>' . __( $notification_parts[] = '<b>' . __(
'Your installation is currently %d update(s) behind the latest version.', 'Your installation is currently %d update(s) behind the latest version.',
@ -87,11 +93,13 @@ class UpdateCheck
$notification_parts[] = $instructions_string; $notification_parts[] = $instructions_string;
$event->addNotification(new Notification( $event->addNotification(
__('New AzuraCast Updates Available'), new Notification(
implode(' ', $notification_parts), __('New AzuraCast Updates Available'),
Notification::INFO implode(' ', $notification_parts),
)); Notification::INFO
)
);
return; return;
} }
} }

View file

@ -39,8 +39,8 @@ abstract class AbstractFrontend extends AbstractAdapter
AdapterFactory $adapterFactory, AdapterFactory $adapterFactory,
Client $client, Client $client,
Router $router, Router $router,
Entity\Repository\StationMountRepository $stationMountRepo, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Settings $settings Entity\Repository\StationMountRepository $stationMountRepo
) { ) {
parent::__construct($environment, $em, $supervisor, $dispatcher); parent::__construct($environment, $em, $supervisor, $dispatcher);
@ -49,7 +49,7 @@ abstract class AbstractFrontend extends AbstractAdapter
$this->router = $router; $this->router = $router;
$this->stationMountRepo = $stationMountRepo; $this->stationMountRepo = $stationMountRepo;
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
} }
/** /**

View file

@ -25,13 +25,13 @@ abstract class AbstractRemote
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
Client $http_client, Client $http_client,
Logger $logger, Logger $logger,
AdapterFactory $adapterFactory AdapterFactory $adapterFactory
) { ) {
$this->em = $em; $this->em = $em;
$this->settings = $settings; $this->settings = $settingsRepo->readSettings();
$this->http_client = $http_client; $this->http_client = $http_client;
$this->logger = $logger; $this->logger = $logger;
$this->adapterFactory = $adapterFactory; $this->adapterFactory = $adapterFactory;

View file

@ -18,9 +18,7 @@ class AzuraCastCentral
protected Client $httpClient; protected Client $httpClient;
protected Entity\Settings $settings; protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo;
protected Version $version; protected Version $version;
@ -31,15 +29,13 @@ class AzuraCastCentral
Version $version, Version $version,
Client $httpClient, Client $httpClient,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo
Entity\Repository\SettingsTableRepository $settingsTableRepo
) { ) {
$this->environment = $environment; $this->environment = $environment;
$this->version = $version; $this->version = $version;
$this->httpClient = $httpClient; $this->httpClient = $httpClient;
$this->logger = $logger; $this->logger = $logger;
$this->settings = $settings; $this->settingsRepo = $settingsRepo;
$this->settingsTableRepo = $settingsTableRepo;
} }
/** /**
@ -83,13 +79,14 @@ class AzuraCastCentral
public function getUniqueIdentifier(): string public function getUniqueIdentifier(): string
{ {
$appUuid = $this->settings->getAppUniqueIdentifier(); $settings = $this->settingsRepo->readSettings();
$appUuid = $settings->getAppUniqueIdentifier();
if (empty($appUuid)) { if (empty($appUuid)) {
$appUuid = Uuid::uuid4()->toString(); $appUuid = Uuid::uuid4()->toString();
$this->settings->setAppUniqueIdentifier($appUuid); $settings->setAppUniqueIdentifier($appUuid);
$this->settingsTableRepo->writeSettings($this->settings); $this->settingsRepo->writeSettings($settings);
} }
return $appUuid; return $appUuid;
@ -102,8 +99,9 @@ class AzuraCastCentral
*/ */
public function getIp(bool $cached = true): ?string public function getIp(bool $cached = true): ?string
{ {
$settings = $this->settingsRepo->readSettings();
$ip = ($cached) $ip = ($cached)
? $this->settings->getExternalIp() ? $settings->getExternalIp()
: null; : null;
if (empty($ip)) { if (empty($ip)) {
@ -123,8 +121,8 @@ class AzuraCastCentral
} }
if (!empty($ip) && $cached) { if (!empty($ip) && $cached) {
$this->settings->setExternalIp($ip); $settings->setExternalIp($ip);
$this->settingsTableRepo->writeSettings($this->settings); $this->settingsRepo->writeSettings($settings);
} }
} }

View file

@ -2,8 +2,7 @@
namespace App\Sync; namespace App\Sync;
use App\Entity; use App\Entity\Repository\SettingsRepository;
use App\Entity\Repository\SettingsTableRepository;
use App\Environment; use App\Environment;
use App\Event\GetSyncTasks; use App\Event\GetSyncTasks;
use App\EventDispatcher; use App\EventDispatcher;
@ -20,26 +19,22 @@ class Runner
{ {
protected Logger $logger; protected Logger $logger;
protected Entity\Settings $settings;
protected Environment $environment; protected Environment $environment;
protected SettingsTableRepository $settingsTableRepo; protected SettingsRepository $settingsRepo;
protected LockFactory $lockFactory; protected LockFactory $lockFactory;
protected EventDispatcher $eventDispatcher; protected EventDispatcher $eventDispatcher;
public function __construct( public function __construct(
SettingsTableRepository $settingsRepo, SettingsRepository $settingsRepo,
Entity\Settings $settings,
Environment $environment, Environment $environment,
Logger $logger, Logger $logger,
LockFactory $lockFactory, LockFactory $lockFactory,
EventDispatcher $eventDispatcher EventDispatcher $eventDispatcher
) { ) {
$this->settingsTableRepo = $settingsRepo; $this->settingsRepo = $settingsRepo;
$this->settings = $settings;
$this->environment = $environment; $this->environment = $environment;
$this->logger = $logger; $this->logger = $logger;
$this->lockFactory = $lockFactory; $this->lockFactory = $lockFactory;
@ -67,7 +62,8 @@ class Runner
public function runSyncTask(string $type, bool $force = false): void public function runSyncTask(string $type, bool $force = false): void
{ {
// Immediately halt if setup is not complete. // Immediately halt if setup is not complete.
if (!$this->settings->isSetupComplete()) { $settings = $this->settingsRepo->readSettings();
if (!$settings->isSetupComplete()) {
$this->logger->notice( $this->logger->notice(
sprintf('Skipping sync task %s; setup not complete.', $type) sprintf('Skipping sync task %s; setup not complete.', $type)
); );
@ -123,15 +119,18 @@ class Runner
$end_time = microtime(true); $end_time = microtime(true);
$time_diff = $end_time - $start_time; $time_diff = $end_time - $start_time;
$this->logger->debug(sprintf( $this->logger->debug(
'Timer "%s" completed in %01.3f second(s).', sprintf(
$taskClass, 'Timer "%s" completed in %01.3f second(s).',
round($time_diff, 3) $taskClass,
)); round($time_diff, 3)
)
);
} }
$this->settings->updateSyncLastRunTime($type); $settings = $this->settingsRepo->readSettings(true);
$this->settingsTableRepo->writeSettings($this->settings); $settings->updateSyncLastRunTime($type);
$this->settingsRepo->writeSettings($settings);
} finally { } finally {
$lock->release(); $lock->release();
} }
@ -145,6 +144,8 @@ class Runner
$shortTaskTimeout = $this->environment->getSyncShortExecutionTime(); $shortTaskTimeout = $this->environment->getSyncShortExecutionTime();
$longTaskTimeout = $this->environment->getSyncLongExecutionTime(); $longTaskTimeout = $this->environment->getSyncLongExecutionTime();
$settings = $this->settingsRepo->readSettings();
$syncs = [ $syncs = [
GetSyncTasks::SYNC_NOWPLAYING => [ GetSyncTasks::SYNC_NOWPLAYING => [
'name' => __('Now Playing Data'), 'name' => __('Now Playing Data'),
@ -152,7 +153,7 @@ class Runner
__('Now Playing Data'), __('Now Playing Data'),
], ],
'timeout' => $shortTaskTimeout, 'timeout' => $shortTaskTimeout,
'latest' => $this->settings->getSyncNowplayingLastRun(), 'latest' => $settings->getSyncNowplayingLastRun(),
'interval' => 15, 'interval' => 15,
], ],
GetSyncTasks::SYNC_SHORT => [ GetSyncTasks::SYNC_SHORT => [
@ -161,7 +162,7 @@ class Runner
__('Song Requests Queue'), __('Song Requests Queue'),
], ],
'timeout' => $shortTaskTimeout, 'timeout' => $shortTaskTimeout,
'latest' => $this->settings->getSyncShortLastRun(), 'latest' => $settings->getSyncShortLastRun(),
'interval' => 60, 'interval' => 60,
], ],
GetSyncTasks::SYNC_MEDIUM => [ GetSyncTasks::SYNC_MEDIUM => [
@ -170,7 +171,7 @@ class Runner
__('Check Media Folders'), __('Check Media Folders'),
], ],
'timeout' => $shortTaskTimeout, 'timeout' => $shortTaskTimeout,
'latest' => $this->settings->getSyncMediumLastRun(), 'latest' => $settings->getSyncMediumLastRun(),
'interval' => 300, 'interval' => 300,
], ],
GetSyncTasks::SYNC_LONG => [ GetSyncTasks::SYNC_LONG => [
@ -180,7 +181,7 @@ class Runner
__('Cleanup'), __('Cleanup'),
], ],
'timeout' => $longTaskTimeout, 'timeout' => $longTaskTimeout,
'latest' => $this->settings->getSyncLongLastRun(), 'latest' => $settings->getSyncLongLastRun(),
'interval' => 3600, 'interval' => 3600,
], ],
]; ];

View file

@ -2,7 +2,6 @@
namespace App\Sync\Task; namespace App\Sync\Task;
use App\Entity;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -12,16 +11,12 @@ abstract class AbstractTask
protected LoggerInterface $logger; protected LoggerInterface $logger;
protected Entity\Settings $settings;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger
Entity\Settings $settings
) { ) {
$this->em = $em; $this->em = $em;
$this->logger = $logger; $this->logger = $logger;
$this->settings = $settings;
} }
abstract public function run(bool $force = false): void; abstract public function run(bool $force = false): void;

View file

@ -17,11 +17,10 @@ class BuildQueueTask extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
AutoDJ $autoDJ, AutoDJ $autoDJ,
LockFactory $lockFactory LockFactory $lockFactory
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->autoDJ = $autoDJ; $this->autoDJ = $autoDJ;
$this->lockFactory = $lockFactory; $this->lockFactory = $lockFactory;
@ -33,6 +32,7 @@ class BuildQueueTask extends AbstractTask
->findBy(['is_enabled' => 1]); ->findBy(['is_enabled' => 1]);
foreach ($stations as $station) { foreach ($stations as $station) {
/** @var Entity\Station $station */
$this->processStation($station, $force); $this->processStation($station, $force);
} }
} }

View file

@ -19,12 +19,11 @@ class CheckFolderPlaylistsTask extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Entity\Repository\StationPlaylistMediaRepository $spmRepo, Entity\Repository\StationPlaylistMediaRepository $spmRepo,
Entity\Repository\StationPlaylistFolderRepository $folderRepo, Entity\Repository\StationPlaylistFolderRepository $folderRepo,
FilesystemManager $filesystem FilesystemManager $filesystem
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->spmRepo = $spmRepo; $this->spmRepo = $spmRepo;
$this->folderRepo = $folderRepo; $this->folderRepo = $folderRepo;
@ -45,9 +44,12 @@ class CheckFolderPlaylistsTask extends AbstractTask
foreach ($stations as $station) { foreach ($stations as $station) {
/** @var Entity\Station $station */ /** @var Entity\Station $station */
$this->logger->info('Processing auto-assigning folders for station...', [ $this->logger->info(
'station' => $station->getName(), 'Processing auto-assigning folders for station...',
]); [
'station' => $station->getName(),
]
);
$this->syncPlaylistFolders($station); $this->syncPlaylistFolders($station);
gc_collect_cycles(); gc_collect_cycles();
@ -88,7 +90,7 @@ class CheckFolderPlaylistsTask extends AbstractTask
SELECT sm SELECT sm
FROM App\Entity\StationMedia sm FROM App\Entity\StationMedia sm
WHERE sm.storage_location = :storageLocation WHERE sm.storage_location = :storageLocation
AND sm.path LIKE :path AND sm.path LIKE :path
DQL DQL
)->setParameter('storageLocation', $station->getMediaStorageLocation()); )->setParameter('storageLocation', $station->getMediaStorageLocation());

View file

@ -32,14 +32,13 @@ class CheckMediaTask extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Entity\Repository\StationMediaRepository $mediaRepo, Entity\Repository\StationMediaRepository $mediaRepo,
Entity\Repository\StorageLocationRepository $storageLocationRepo, Entity\Repository\StorageLocationRepository $storageLocationRepo,
FilesystemManager $filesystem, FilesystemManager $filesystem,
MessageBus $messageBus, MessageBus $messageBus,
QueueManager $queueManager QueueManager $queueManager
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->storageLocationRepo = $storageLocationRepo; $this->storageLocationRepo = $storageLocationRepo;
$this->mediaRepo = $mediaRepo; $this->mediaRepo = $mediaRepo;

View file

@ -21,12 +21,11 @@ class CheckRequests extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Entity\Repository\StationRequestRepository $requestRepo, Entity\Repository\StationRequestRepository $requestRepo,
Adapters $adapters, Adapters $adapters,
EventDispatcher $dispatcher EventDispatcher $dispatcher
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->requestRepo = $requestRepo; $this->requestRepo = $requestRepo;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
@ -67,10 +66,12 @@ class CheckRequests extends AbstractTask
} }
// Check for an existing SongHistory record and skip if one exists. // Check for an existing SongHistory record and skip if one exists.
$sq = $this->em->getRepository(Entity\StationQueue::class)->findOneBy([ $sq = $this->em->getRepository(Entity\StationQueue::class)->findOneBy(
'station' => $station, [
'request' => $request, 'station' => $station,
]); 'request' => $request,
]
);
if (!$sq instanceof Entity\StationQueue) { if (!$sq instanceof Entity\StationQueue) {
// Log the item in SongHistory. // Log the item in SongHistory.

View file

@ -15,25 +15,26 @@ class CheckUpdatesTask extends AbstractTask
protected AzuraCastCentral $azuracastCentral; protected AzuraCastCentral $azuracastCentral;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo; protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\SettingsTableRepository $settingsTableRepo,
AzuraCastCentral $azuracastCentral AzuraCastCentral $azuracastCentral
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->settingsTableRepo = $settingsTableRepo; $this->settingsRepo = $settingsRepo;
$this->azuracastCentral = $azuracastCentral; $this->azuracastCentral = $azuracastCentral;
} }
public function run(bool $force = false): void public function run(bool $force = false): void
{ {
$settings = $this->settingsRepo->readSettings();
if (!$force) { if (!$force) {
$update_last_run = $this->settings->getUpdateLastRun(); $update_last_run = $settings->getUpdateLastRun();
if ($update_last_run > (time() - self::UPDATE_THRESHOLD)) { if ($update_last_run > (time() - self::UPDATE_THRESHOLD)) {
$this->logger->debug('Not checking for updates; checked too recently.'); $this->logger->debug('Not checking for updates; checked too recently.');
@ -50,8 +51,7 @@ class CheckUpdatesTask extends AbstractTask
$updates = $this->azuracastCentral->checkForUpdates(); $updates = $this->azuracastCentral->checkForUpdates();
if (!empty($updates)) { if (!empty($updates)) {
$this->settings->setUpdateResults($updates); $settings->setUpdateResults($updates);
$this->settingsTableRepo->writeSettings($this->settings);
$this->logger->info('Successfully checked for updates.', ['results' => $updates]); $this->logger->info('Successfully checked for updates.', ['results' => $updates]);
} else { } else {
@ -62,7 +62,7 @@ class CheckUpdatesTask extends AbstractTask
return; return;
} }
$this->settings->updateUpdateLastRun(); $settings->updateUpdateLastRun();
$this->settingsTableRepo->writeSettings($this->settings); $this->settingsRepo->writeSettings($settings);
} }
} }

View file

@ -12,22 +12,26 @@ class CleanupHistoryTask extends AbstractTask
protected Entity\Repository\ListenerRepository $listenerRepo; protected Entity\Repository\ListenerRepository $listenerRepo;
protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\SongHistoryRepository $historyRepo, Entity\Repository\SongHistoryRepository $historyRepo,
Entity\Repository\ListenerRepository $listenerRepo Entity\Repository\ListenerRepository $listenerRepo
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->settingsRepo = $settingsRepo;
$this->historyRepo = $historyRepo; $this->historyRepo = $historyRepo;
$this->listenerRepo = $listenerRepo; $this->listenerRepo = $listenerRepo;
} }
public function run(bool $force = false): void public function run(bool $force = false): void
{ {
$daysToKeep = $this->settings->getHistoryKeepDays(); $settings = $this->settingsRepo->readSettings();
$daysToKeep = $settings->getHistoryKeepDays();
if ($daysToKeep !== 0) { if ($daysToKeep !== 0) {
$this->historyRepo->cleanup($daysToKeep); $this->historyRepo->cleanup($daysToKeep);

View file

@ -16,10 +16,9 @@ class CleanupStorageTask extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
FilesystemManager $filesystem FilesystemManager $filesystem
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->filesystem = $filesystem; $this->filesystem = $filesystem;
} }

View file

@ -41,7 +41,7 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
protected Entity\Repository\ListenerRepository $listenerRepo; protected Entity\Repository\ListenerRepository $listenerRepo;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo; protected Entity\Repository\SettingsRepository $settingsRepo;
protected LockFactory $lockFactory; protected LockFactory $lockFactory;
@ -49,12 +49,9 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
protected RouterInterface $router; protected RouterInterface $router;
protected string $analyticsLevel = Entity\Analytics::LEVEL_ALL;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Adapters $adapters, Adapters $adapters,
AutoDJ $autodj, AutoDJ $autodj,
CacheInterface $cache, CacheInterface $cache,
@ -64,10 +61,10 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
RouterInterface $router, RouterInterface $router,
Entity\Repository\ListenerRepository $listenerRepository, Entity\Repository\ListenerRepository $listenerRepository,
Entity\Repository\StationQueueRepository $queueRepo, Entity\Repository\StationQueueRepository $queueRepo,
Entity\Repository\SettingsTableRepository $settingsTableRepo, Entity\Repository\SettingsRepository $settingsRepo,
Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGenerator Entity\ApiGenerator\NowPlayingApiGenerator $nowPlayingApiGenerator
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->adapters = $adapters; $this->adapters = $adapters;
$this->autodj = $autodj; $this->autodj = $autodj;
@ -79,11 +76,9 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
$this->listenerRepo = $listenerRepository; $this->listenerRepo = $listenerRepository;
$this->queueRepo = $queueRepo; $this->queueRepo = $queueRepo;
$this->settingsTableRepo = $settingsTableRepo; $this->settingsRepo = $settingsRepo;
$this->nowPlayingApiGenerator = $nowPlayingApiGenerator; $this->nowPlayingApiGenerator = $nowPlayingApiGenerator;
$this->analyticsLevel = $settings->getAnalytics();
} }
/** /**
@ -109,8 +104,9 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
$this->cache->set('nowplaying', $nowplaying, 120); $this->cache->set('nowplaying', $nowplaying, 120);
$this->settings->setNowplaying($nowplaying); $settings = $this->settingsRepo->readSettings(true);
$this->settingsTableRepo->writeSettings($this->settings); $settings->setNowplaying($nowplaying);
$this->settingsRepo->writeSettings($settings);
} }
/** /**
@ -118,7 +114,7 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
* *
* @return Entity\Api\NowPlaying[] * @return Entity\Api\NowPlaying[]
*/ */
protected function loadNowPlaying($force = false): array protected function loadNowPlaying(bool $force = false): array
{ {
$stations = $this->em->getRepository(Entity\Station::class) $stations = $this->em->getRepository(Entity\Station::class)
->findBy(['is_enabled' => 1]); ->findBy(['is_enabled' => 1]);
@ -131,16 +127,9 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
return $nowplaying; return $nowplaying;
} }
/**
* Generate Structured NowPlaying Data for a given station.
*
* @param Entity\Station $station
* @param bool $standalone Whether the request is for this station alone or part of the regular sync process.
*
*/
public function processStation( public function processStation(
Entity\Station $station, Entity\Station $station,
$standalone = false bool $standalone = false
): Entity\Api\NowPlaying { ): Entity\Api\NowPlaying {
$lock = $this->getLockForStation($station); $lock = $this->getLockForStation($station);
$lock->acquire(true); $lock->acquire(true);
@ -149,15 +138,18 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
/** @var Logger $logger */ /** @var Logger $logger */
$logger = $this->logger; $logger = $this->logger;
$logger->pushProcessor(function ($record) use ($station) { $logger->pushProcessor(
$record['extra']['station'] = [ function ($record) use ($station) {
'id' => $station->getId(), $record['extra']['station'] = [
'name' => $station->getName(), 'id' => $station->getId(),
]; 'name' => $station->getName(),
return $record; ];
}); return $record;
}
);
$include_clients = ($this->analyticsLevel === Entity\Analytics::LEVEL_ALL); $settings = $this->settingsRepo->readSettings();
$include_clients = (Entity\Analytics::LEVEL_NONE !== $settings->getAnalytics());
$frontend_adapter = $this->adapters->getFrontendAdapter($station); $frontend_adapter = $this->adapters->getFrontendAdapter($station);
$remote_adapters = $this->adapters->getRemoteAdapters($station); $remote_adapters = $this->adapters->getRemoteAdapters($station);
@ -174,20 +166,27 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
$npResult = $event->getResult(); $npResult = $event->getResult();
} catch (Exception $e) { } catch (Exception $e) {
$this->logger->log(Logger::ERROR, $e->getMessage(), [ $this->logger->log(
'file' => $e->getFile(), Logger::ERROR,
'line' => $e->getLine(), $e->getMessage(),
'code' => $e->getCode(), [
]); 'file' => $e->getFile(),
'line' => $e->getLine(),
'code' => $e->getCode(),
]
);
$npResult = Result::blank(); $npResult = Result::blank();
} }
$this->logger->debug('Final NowPlaying Response for Station', [ $this->logger->debug(
'id' => $station->getId(), 'Final NowPlaying Response for Station',
'name' => $station->getName(), [
'np' => $npResult, 'id' => $station->getId(),
]); 'name' => $station->getName(),
'np' => $npResult,
]
);
// Update detailed listener statistics, if they exist for the station // Update detailed listener statistics, if they exist for the station
if ($include_clients && null !== $npResult->clients) { if ($include_clients && null !== $npResult->clients) {
@ -267,9 +266,12 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface
$message = new Message\UpdateNowPlayingMessage(); $message = new Message\UpdateNowPlayingMessage();
$message->station_id = $station->getId(); $message->station_id = $station->getId();
$this->messageBus->dispatch($message, [ $this->messageBus->dispatch(
new DelayStamp(2000), $message,
]); [
new DelayStamp(2000),
]
);
} finally { } finally {
$lock->release(); $lock->release();
} }

View file

@ -13,10 +13,9 @@ class ReactivateStreamerTask extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Entity\Repository\StationStreamerRepository $streamerRepo Entity\Repository\StationStreamerRepository $streamerRepo
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->streamerRepo = $streamerRepo; $this->streamerRepo = $streamerRepo;
} }

View file

@ -19,22 +19,26 @@ class RotateLogsTask extends AbstractTask
protected Supervisor $supervisor; protected Supervisor $supervisor;
protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Repository\StorageLocationRepository $storageLocationRepo; protected Entity\Repository\StorageLocationRepository $storageLocationRepo;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Environment $environment, Environment $environment,
Adapters $adapters, Adapters $adapters,
Supervisor $supervisor, Supervisor $supervisor,
Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\StorageLocationRepository $storageLocationRepo Entity\Repository\StorageLocationRepository $storageLocationRepo
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->environment = $environment; $this->environment = $environment;
$this->adapters = $adapters; $this->adapters = $adapters;
$this->supervisor = $supervisor; $this->supervisor = $supervisor;
$this->settingsRepo = $settingsRepo;
$this->storageLocationRepo = $storageLocationRepo; $this->storageLocationRepo = $storageLocationRepo;
} }
@ -63,10 +67,12 @@ class RotateLogsTask extends AbstractTask
$rotate->run(); $rotate->run();
// Rotate the automated backups. // Rotate the automated backups.
$backups_to_keep = $this->settings->getBackupKeepCopies(); $settings = $this->settingsRepo->readSettings();
$backups_to_keep = $settings->getBackupKeepCopies();
if ($backups_to_keep > 0) { if ($backups_to_keep > 0) {
$backupStorageId = (int)$this->settings->getBackupStorageLocation(); $backupStorageId = (int)$settings->getBackupStorageLocation();
if ($backupStorageId > 0) { if ($backupStorageId > 0) {
$storageLocation = $this->storageLocationRepo->findByType( $storageLocation = $this->storageLocationRepo->findByType(

View file

@ -9,6 +9,8 @@ use Psr\Log\LoggerInterface;
class RunAnalyticsTask extends AbstractTask class RunAnalyticsTask extends AbstractTask
{ {
protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Repository\AnalyticsRepository $analyticsRepo; protected Entity\Repository\AnalyticsRepository $analyticsRepo;
protected Entity\Repository\ListenerRepository $listenerRepo; protected Entity\Repository\ListenerRepository $listenerRepo;
@ -18,13 +20,14 @@ class RunAnalyticsTask extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\AnalyticsRepository $analyticsRepo, Entity\Repository\AnalyticsRepository $analyticsRepo,
Entity\Repository\ListenerRepository $listenerRepo, Entity\Repository\ListenerRepository $listenerRepo,
Entity\Repository\SongHistoryRepository $historyRepo Entity\Repository\SongHistoryRepository $historyRepo
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->settingsRepo = $settingsRepo;
$this->analyticsRepo = $analyticsRepo; $this->analyticsRepo = $analyticsRepo;
$this->listenerRepo = $listenerRepo; $this->listenerRepo = $listenerRepo;
$this->historyRepo = $historyRepo; $this->historyRepo = $historyRepo;
@ -32,7 +35,8 @@ class RunAnalyticsTask extends AbstractTask
public function run(bool $force = false): void public function run(bool $force = false): void
{ {
$analytics_level = $this->settings->getAnalytics(); $settings = $this->settingsRepo->readSettings();
$analytics_level = $settings->getAnalytics();
switch ($analytics_level) { switch ($analytics_level) {
case Entity\Analytics::LEVEL_NONE: case Entity\Analytics::LEVEL_NONE:

View file

@ -21,11 +21,10 @@ class RunAutomatedAssignmentTask extends AbstractTask
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Entity\Repository\StationMediaRepository $mediaRepo, Entity\Repository\StationMediaRepository $mediaRepo,
Adapters $adapters Adapters $adapters
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->mediaRepo = $mediaRepo; $this->mediaRepo = $mediaRepo;
$this->adapters = $adapters; $this->adapters = $adapters;
@ -125,9 +124,12 @@ class RunAutomatedAssignmentTask extends AbstractTask
$mediaReport = $this->generateReport($station, $threshold_days); $mediaReport = $this->generateReport($station, $threshold_days);
// Remove songs that weren't already in auto-assigned playlists. // Remove songs that weren't already in auto-assigned playlists.
$mediaReport = array_filter($mediaReport, function ($media) use ($mediaToUpdate) { $mediaReport = array_filter(
return (isset($mediaToUpdate[$media['id']])); $mediaReport,
}); function ($media) use ($mediaToUpdate) {
return (isset($mediaToUpdate[$media['id']]));
}
);
// Place all songs with 0 plays back in their original playlists. // Place all songs with 0 plays back in their original playlists.
foreach ($mediaReport as $song_id => $media) { foreach ($mediaReport as $song_id => $media) {
@ -137,9 +139,12 @@ class RunAutomatedAssignmentTask extends AbstractTask
} }
// Sort songs by ratio descending. // Sort songs by ratio descending.
uasort($mediaReport, function ($a_media, $b_media) { uasort(
return (int)$b_media['ratio'] <=> (int)$a_media['ratio']; $mediaReport,
}); function ($a_media, $b_media) {
return (int)$b_media['ratio'] <=> (int)$a_media['ratio'];
}
);
// Distribute media across the enabled playlists and assign media to playlist. // Distribute media across the enabled playlists and assign media to playlist.
$numSongs = count($mediaReport); $numSongs = count($mediaReport);

View file

@ -13,27 +13,24 @@ use Symfony\Component\Messenger\MessageBus;
class RunBackupTask extends AbstractTask class RunBackupTask extends AbstractTask
{ {
public const BASE_DIR = '/var/azuracast/backups';
protected MessageBus $messageBus; protected MessageBus $messageBus;
protected Application $console; protected Application $console;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo; protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
MessageBus $messageBus, MessageBus $messageBus,
Application $console, Application $console,
Entity\Repository\SettingsTableRepository $settingsTableRepo Entity\Repository\SettingsRepository $settingsRepo
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->messageBus = $messageBus; $this->messageBus = $messageBus;
$this->console = $console; $this->console = $console;
$this->settingsTableRepo = $settingsTableRepo; $this->settingsRepo = $settingsRepo;
} }
/** /**
@ -44,8 +41,10 @@ class RunBackupTask extends AbstractTask
public function __invoke(Message\AbstractMessage $message): void public function __invoke(Message\AbstractMessage $message): void
{ {
if ($message instanceof Message\BackupMessage) { if ($message instanceof Message\BackupMessage) {
$this->settings->updateBackupLastRun(); $settings = $this->settingsRepo->readSettings(true);
$this->settingsTableRepo->writeSettings($this->settings); $settings->updateBackupLastRun();
$this->settingsRepo->writeSettings($settings);
[$result_code, $result_output] = $this->runBackup( [$result_code, $result_output] = $this->runBackup(
$message->path, $message->path,
@ -54,9 +53,10 @@ class RunBackupTask extends AbstractTask
$message->storageLocationId $message->storageLocationId
); );
$this->settings->setBackupLastResult($result_code); $settings = $this->settingsRepo->readSettings(true);
$this->settings->setBackupLastOutput($result_output); $settings->setBackupLastResult($result_code);
$this->settingsTableRepo->writeSettings($this->settings); $settings->setBackupLastOutput($result_output);
$this->settingsRepo->writeSettings($settings);
} }
} }
@ -94,7 +94,9 @@ class RunBackupTask extends AbstractTask
public function run(bool $force = false): void public function run(bool $force = false): void
{ {
$backup_enabled = $this->settings->isBackupEnabled(); $settings = $this->settingsRepo->readSettings();
$backup_enabled = $settings->isBackupEnabled();
if (!$backup_enabled) { if (!$backup_enabled) {
$this->logger->debug('Automated backups disabled; skipping...'); $this->logger->debug('Automated backups disabled; skipping...');
return; return;
@ -103,11 +105,11 @@ class RunBackupTask extends AbstractTask
$now_utc = CarbonImmutable::now('UTC'); $now_utc = CarbonImmutable::now('UTC');
$threshold = $now_utc->subDay()->getTimestamp(); $threshold = $now_utc->subDay()->getTimestamp();
$last_run = $this->settings->getBackupLastRun(); $last_run = $settings->getBackupLastRun();
if ($last_run <= $threshold) { if ($last_run <= $threshold) {
// Check if the backup time matches (if it's set). // Check if the backup time matches (if it's set).
$backupTimecode = $this->settings->getBackupTimeCode(); $backupTimecode = $settings->getBackupTimeCode();
if (null !== $backupTimecode && '' !== $backupTimecode) { if (null !== $backupTimecode && '' !== $backupTimecode) {
$isWithinTimecode = false; $isWithinTimecode = false;
@ -134,7 +136,7 @@ class RunBackupTask extends AbstractTask
} }
// Trigger a new backup. // Trigger a new backup.
$storageLocationId = (int)($this->settings->getBackupStorageLocation() ?? 0); $storageLocationId = $settings->getBackupStorageLocation() ?? 0;
if ($storageLocationId <= 0) { if ($storageLocationId <= 0) {
$storageLocationId = null; $storageLocationId = null;
} }
@ -142,7 +144,7 @@ class RunBackupTask extends AbstractTask
$message = new Message\BackupMessage(); $message = new Message\BackupMessage();
$message->storageLocationId = $storageLocationId; $message->storageLocationId = $storageLocationId;
$message->path = 'automatic_backup.zip'; $message->path = 'automatic_backup.zip';
$message->excludeMedia = $this->settings->getBackupExcludeMedia(); $message->excludeMedia = $settings->getBackupExcludeMedia();
$this->messageBus->dispatch($message); $this->messageBus->dispatch($message);
} }

View file

@ -21,28 +21,28 @@ class UpdateGeoLiteTask extends AbstractTask
protected IpGeolocation $geoLite; protected IpGeolocation $geoLite;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo; protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
LoggerInterface $logger, LoggerInterface $logger,
Entity\Settings $settings,
Client $httpClient, Client $httpClient,
IpGeolocation $geoLite, IpGeolocation $geoLite,
Entity\Repository\SettingsTableRepository $settingsTableRepo Entity\Repository\SettingsRepository $settingsRepo
) { ) {
parent::__construct($em, $logger, $settings); parent::__construct($em, $logger);
$this->httpClient = $httpClient; $this->httpClient = $httpClient;
$this->geoLite = $geoLite; $this->geoLite = $geoLite;
$this->settingsTableRepo = $settingsTableRepo; $this->settingsRepo = $settingsRepo;
} }
public function run(bool $force = false): void public function run(bool $force = false): void
{ {
if (!$force) { $settings = $this->settingsRepo->readSettings();
$lastRun = $this->settings->getGeoliteLastRun();
if (!$force) {
$lastRun = $settings->getGeoliteLastRun();
if ($lastRun > (time() - self::UPDATE_THRESHOLD)) { if ($lastRun > (time() - self::UPDATE_THRESHOLD)) {
$this->logger->debug('Not checking for updates; checked too recently.'); $this->logger->debug('Not checking for updates; checked too recently.');
return; return;
@ -50,23 +50,25 @@ class UpdateGeoLiteTask extends AbstractTask
} }
try { try {
$this->updateDatabase(); $this->updateDatabase($settings->getGeoliteLicenseKey() ?? '');
} catch (Exception $e) { } catch (Exception $e) {
$this->logger->error('Error updating GeoLite database.', [ $this->logger->error(
'error' => $e->getMessage(), 'Error updating GeoLite database.',
'file' => $e->getFile(), [
'line' => $e->getLine(), 'error' => $e->getMessage(),
]); 'file' => $e->getFile(),
'line' => $e->getLine(),
]
);
} }
$this->settings->updateGeoliteLastRun(); $settings = $this->settingsRepo->readSettings(true);
$this->settingsTableRepo->writeSettings($this->settings); $settings->updateGeoliteLastRun();
$this->settingsRepo->writeSettings($settings);
} }
public function updateDatabase(): void public function updateDatabase(string $licenseKey): void
{ {
$licenseKey = trim($this->settings->getGeoliteLicenseKey());
if (empty($licenseKey)) { if (empty($licenseKey)) {
$this->logger->info('Not checking for GeoLite updates; no license key provided.'); $this->logger->info('Not checking for GeoLite updates; no license key provided.');
return; return;
@ -77,28 +79,34 @@ class UpdateGeoLiteTask extends AbstractTask
set_time_limit(900); set_time_limit(900);
$this->httpClient->get('https://download.maxmind.com/app/geoip_download', [ $this->httpClient->get(
RequestOptions::HTTP_ERRORS => true, 'https://download.maxmind.com/app/geoip_download',
RequestOptions::QUERY => [ [
'license_key' => $licenseKey, RequestOptions::HTTP_ERRORS => true,
'edition_id' => 'GeoLite2-City', RequestOptions::QUERY => [
'suffix' => 'tar.gz', 'license_key' => $licenseKey,
], 'edition_id' => 'GeoLite2-City',
RequestOptions::DECODE_CONTENT => false, 'suffix' => 'tar.gz',
RequestOptions::SINK => $downloadPath, ],
RequestOptions::TIMEOUT => 600, RequestOptions::DECODE_CONTENT => false,
]); RequestOptions::SINK => $downloadPath,
RequestOptions::TIMEOUT => 600,
]
);
if (!file_exists($downloadPath)) { if (!file_exists($downloadPath)) {
throw new RuntimeException('New GeoLite database .tar.gz file not found.'); throw new RuntimeException('New GeoLite database .tar.gz file not found.');
} }
$process = new Process([ $process = new Process(
'tar', [
'xvzf', 'tar',
$downloadPath, 'xvzf',
'--strip-components=1', $downloadPath,
], $baseDir); '--strip-components=1',
],
$baseDir
);
$process->mustRun(); $process->mustRun();

View file

@ -11,10 +11,35 @@ class TaskLocator
protected array $tasks; protected array $tasks;
public function __construct(ContainerInterface $di, array $tasks) public function __construct(ContainerInterface $di)
{ {
$this->di = $di; $this->di = $di;
$this->tasks = $tasks;
$this->tasks = [
GetSyncTasks::SYNC_NOWPLAYING => [
Task\BuildQueueTask::class,
Task\NowPlayingTask::class,
Task\ReactivateStreamerTask::class,
],
GetSyncTasks::SYNC_SHORT => [
Task\CheckRequests::class,
Task\RunBackupTask::class,
Task\CleanupRelaysTask::class,
],
GetSyncTasks::SYNC_MEDIUM => [
Task\CheckMediaTask::class,
Task\CheckFolderPlaylistsTask::class,
Task\CheckUpdatesTask::class,
],
GetSyncTasks::SYNC_LONG => [
Task\RunAnalyticsTask::class,
Task\RunAutomatedAssignmentTask::class,
Task\CleanupHistoryTask::class,
Task\CleanupStorageTask::class,
Task\RotateLogsTask::class,
Task\UpdateGeoLiteTask::class,
],
];
} }
public function __invoke(GetSyncTasks $event): void public function __invoke(GetSyncTasks $event): void

View file

@ -2,12 +2,132 @@
namespace App; namespace App;
use App\Http\Response;
use App\Http\ServerRequest;
use DI\FactoryInterface;
use Doctrine\Inflector\InflectorFactory;
use League\Plates\Engine; use League\Plates\Engine;
use League\Plates\Template\Data; use League\Plates\Template\Data;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
class View extends Engine class View extends Engine
{ {
public function __construct(
FactoryInterface $factory,
Environment $environment,
EventDispatcher $dispatcher,
Version $version,
ServerRequestInterface $request
) {
parent::__construct($environment->getViewsDirectory(), 'phtml');
// Add non-request-dependent content.
$this->addData(
[
'environment' => $environment,
'version' => $version,
]
);
// Add request-dependent content.
$assets = $factory->make(
Assets::class,
[
'request' => $request,
]
);
$this->addData(
[
'request' => $request,
'router' => $request->getAttribute(ServerRequest::ATTR_ROUTER),
'auth' => $request->getAttribute(ServerRequest::ATTR_AUTH),
'acl' => $request->getAttribute(ServerRequest::ATTR_ACL),
'customization' => $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION),
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
'assets' => $assets,
]
);
$this->registerFunction(
'escapeJs',
function ($string) {
return json_encode($string, JSON_THROW_ON_ERROR, 512);
}
);
$this->registerFunction(
'dump',
function ($value) {
if (class_exists(VarCloner::class)) {
$varCloner = new VarCloner();
$dumper = new CliDumper();
$dumpedValue = $dumper->dump($varCloner->cloneVar($value), true);
} else {
$dumpedValue = json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
}
return '<pre>' . htmlspecialchars($dumpedValue) . '</pre>';
}
);
$this->registerFunction(
'mailto',
function ($address, $link_text = null) {
$address = substr(chunk_split(bin2hex(" $address"), 2, ';&#x'), 3, -3);
$link_text = $link_text ?? $address;
return '<a href="mailto:' . $address . '">' . $link_text . '</a>';
}
);
$this->registerFunction(
'pluralize',
function ($word, $num = 0) {
if ((int)$num === 1) {
return $word;
}
$inflector = InflectorFactory::create()->build();
return $inflector->pluralize($word);
}
);
$this->registerFunction(
'truncate',
function ($text, $length = 80) {
return Utilities::truncateText($text, $length);
}
);
$this->registerFunction(
'truncateUrl',
function ($url) {
return Utilities::truncateUrl($url);
}
);
$this->registerFunction(
'link',
function ($url, $external = true, $truncate = true) {
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$a = ['href="' . $url . '"'];
if ($external) {
$a[] = 'target="_blank"';
}
$a_body = ($truncate) ? Utilities::truncateUrl($url) : $url;
return '<a ' . implode(' ', $a) . '>' . $a_body . '</a>';
}
);
$dispatcher->dispatch(new Event\BuildView($this));
}
public function reset(): void public function reset(): void
{ {
$this->data = new Data(); $this->data = new Data();
@ -26,17 +146,25 @@ class View extends Engine
* Trigger rendering of template and write it directly to the PSR-7 compatible Response object. * Trigger rendering of template and write it directly to the PSR-7 compatible Response object.
* *
* @param ResponseInterface $response * @param ResponseInterface $response
* @param string $template_name * @param string $templateName
* @param array $template_args * @param array $templateArgs
*/ */
public function renderToResponse( public function renderToResponse(
ResponseInterface $response, ResponseInterface $response,
$template_name, string $templateName,
array $template_args = [] array $templateArgs = []
): ResponseInterface { ): ResponseInterface {
$template = $this->render($template_name, $template_args); $template = $this->render($templateName, $templateArgs);
$response->getBody()->write($template); $response->getBody()->write($template);
return $response->withHeader('Content-type', 'text/html; charset=utf-8'); $response = $response->withHeader('Content-type', 'text/html; charset=utf-8');
if ($response instanceof Response && !$response->hasCacheLifetime()) {
/** @var Assets $assets */
$assets = $this->getData('assets');
$response = $assets->writeCsp($response);
}
return $response;
} }
} }

View file

@ -1,123 +0,0 @@
<?php
namespace App;
use App\Http\ServerRequest;
use Doctrine\Inflector\InflectorFactory;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
use const JSON_PRETTY_PRINT;
class ViewFactory
{
protected ContainerInterface $di;
protected Environment $environment;
protected EventDispatcher $dispatcher;
protected Version $version;
protected Assets $assets;
public function __construct(
ContainerInterface $di,
Environment $environment,
EventDispatcher $dispatcher,
Version $version,
Assets $assets
) {
$this->di = $di;
$this->environment = $environment;
$this->dispatcher = $dispatcher;
$this->version = $version;
$this->assets = $assets;
}
public function create(ServerRequestInterface $request): View
{
$view = new View($this->environment->getViewsDirectory(), 'phtml');
// Add non-request-dependent content.
$view->addData([
'environment' => $this->environment,
'version' => $this->version,
]);
// Add request-dependent content.
$assets = $this->assets->withRequest($request);
$view->addData([
'request' => $request,
'router' => $request->getAttribute(ServerRequest::ATTR_ROUTER),
'auth' => $request->getAttribute(ServerRequest::ATTR_AUTH),
'acl' => $request->getAttribute(ServerRequest::ATTR_ACL),
'customization' => $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION),
'flash' => $request->getAttribute(ServerRequest::ATTR_SESSION_FLASH),
'assets' => $assets,
]);
$view->registerFunction('service', function ($service) {
return $this->di->get($service);
});
$view->registerFunction('escapeJs', function ($string) {
return json_encode($string, JSON_THROW_ON_ERROR, 512);
});
$view->registerFunction('dump', function ($value) {
if (class_exists(VarCloner::class)) {
$varCloner = new VarCloner();
$dumper = new CliDumper();
$dumpedValue = $dumper->dump($varCloner->cloneVar($value), true);
} else {
$dumpedValue = json_encode($value, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT);
}
return '<pre>' . htmlspecialchars($dumpedValue) . '</pre>';
});
$view->registerFunction('mailto', function ($address, $link_text = null) {
$address = substr(chunk_split(bin2hex(" $address"), 2, ';&#x'), 3, -3);
$link_text = $link_text ?? $address;
return '<a href="mailto:' . $address . '">' . $link_text . '</a>';
});
$view->registerFunction('pluralize', function ($word, $num = 0) {
if ((int)$num === 1) {
return $word;
}
$inflector = InflectorFactory::create()->build();
return $inflector->pluralize($word);
});
$view->registerFunction('truncate', function ($text, $length = 80) {
return Utilities::truncateText($text, $length);
});
$view->registerFunction('truncateUrl', function ($url) {
return Utilities::truncateUrl($url);
});
$view->registerFunction('link', function ($url, $external = true, $truncate = true) {
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$a = ['href="' . $url . '"'];
if ($external) {
$a[] = 'target="_blank"';
}
$a_body = ($truncate) ? Utilities::truncateUrl($url) : $url;
return '<a ' . implode(' ', $a) . '>' . $a_body . '</a>';
});
$this->dispatcher->dispatch(new Event\BuildView($view));
return $view;
}
}

View file

@ -2,6 +2,7 @@
namespace App\Webhook; namespace App\Webhook;
use App\Config;
use App\Webhook\Connector\ConnectorInterface; use App\Webhook\Connector\ConnectorInterface;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -12,8 +13,16 @@ class ConnectorLocator
protected array $connectors; protected array $connectors;
public function __construct(ContainerInterface $di, array $connectors) public function __construct(
{ ContainerInterface $di,
Config $config
) {
$webhooks = $config->get('webhooks');
$connectors = [];
foreach ($webhooks['webhooks'] as $webhook_key => $webhook_info) {
$connectors[$webhook_key] = $webhook_info['class'];
}
$this->di = $di; $this->di = $di;
$this->connectors = $connectors; $this->connectors = $connectors;
} }

View file

@ -23,22 +23,18 @@ class LocalWebhookHandler
protected CacheInterface $cache; protected CacheInterface $cache;
protected Entity\Settings $settings; protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo;
public function __construct( public function __construct(
Logger $logger, Logger $logger,
Client $httpClient, Client $httpClient,
CacheInterface $cache, CacheInterface $cache,
Entity\Settings $settings, Entity\Repository\SettingsRepository $settingsRepo
Entity\Repository\SettingsTableRepository $settingsTableRepo
) { ) {
$this->logger = $logger; $this->logger = $logger;
$this->httpClient = $httpClient; $this->httpClient = $httpClient;
$this->cache = $cache; $this->cache = $cache;
$this->settings = $settings; $this->settingsRepo = $settingsRepo;
$this->settingsTableRepo = $settingsTableRepo;
} }
public function dispatch(SendWebhooks $event): void public function dispatch(SendWebhooks $event): void
@ -65,8 +61,9 @@ class LocalWebhookHandler
$this->cache->set('nowplaying', $np_new, 120); $this->cache->set('nowplaying', $np_new, 120);
$this->settings->setNowplaying($np_new); $settings = $this->settingsRepo->readSettings(true);
$this->settingsTableRepo->writeSettings($this->settings); $settings->setNowplaying($np_new);
$this->settingsRepo->writeSettings($settings);
} }
} }
@ -76,10 +73,15 @@ class LocalWebhookHandler
$config_dir = $station->getRadioConfigDir(); $config_dir = $station->getRadioConfigDir();
$np_file = $config_dir . '/nowplaying.txt'; $np_file = $config_dir . '/nowplaying.txt';
$np_text = implode(' - ', array_filter([ $np_text = implode(
$np->now_playing->song->artist ?? null, ' - ',
$np->now_playing->song->title ?? null, array_filter(
])); [
$np->now_playing->song->artist ?? null,
$np->now_playing->song->title ?? null,
]
)
);
if (empty($np_text)) { if (empty($np_text)) {
$np_text = $station->getName(); $np_text = $station->getName();
@ -107,9 +109,12 @@ class LocalWebhookHandler
if (NChan::isSupported()) { if (NChan::isSupported()) {
$this->logger->debug('Dispatching Nchan notification...'); $this->logger->debug('Dispatching Nchan notification...');
$this->httpClient->post('http://localhost:9010/pub/' . urlencode($station->getShortName()), [ $this->httpClient->post(
'json' => $np, 'http://localhost:9010/pub/' . urlencode($station->getShortName()),
]); [
'json' => $np,
]
);
} }
} }
} }

View file

@ -10,7 +10,7 @@ abstract class CestAbstract
protected App\Environment $environment; protected App\Environment $environment;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo; protected Entity\Repository\SettingsRepository $settingsRepo;
protected Entity\Repository\StationRepository $stationRepo; protected Entity\Repository\StationRepository $stationRepo;
@ -25,7 +25,7 @@ abstract class CestAbstract
$this->di = $tests_module->container; $this->di = $tests_module->container;
$this->em = $tests_module->em; $this->em = $tests_module->em;
$this->settingsTableRepo = $this->di->get(Entity\Repository\SettingsTableRepository::class); $this->settingsRepo = $this->di->get(Entity\Repository\SettingsRepository::class);
$this->stationRepo = $this->di->get(Entity\Repository\StationRepository::class); $this->stationRepo = $this->di->get(Entity\Repository\StationRepository::class);
$this->environment = $this->di->get(App\Environment::class); $this->environment = $this->di->get(App\Environment::class);
} }
@ -48,9 +48,10 @@ abstract class CestAbstract
{ {
$I->wantTo('Start with an incomplete setup.'); $I->wantTo('Start with an incomplete setup.');
$settings = $this->settingsTableRepo->updateSettings(); $settings = $this->settingsRepo->readSettings(true);
$settings->setSetupCompleteTime(0); $settings->setSetupCompleteTime(0);
$this->settingsTableRepo->writeSettings($settings);
$this->settingsRepo->writeSettings($settings);
$this->_cleanTables(); $this->_cleanTables();
} }
@ -92,10 +93,10 @@ abstract class CestAbstract
$this->test_station = $this->stationRepo->create($test_station); $this->test_station = $this->stationRepo->create($test_station);
// Set settings. // Set settings.
$settings = $this->settingsTableRepo->updateSettings(); $settings = $this->settingsRepo->readSettings(true);
$settings->updateSetupComplete(); $settings->updateSetupComplete();
$settings->setBaseUrl('localhost'); $settings->setBaseUrl('localhost');
$this->settingsTableRepo->writeSettings($settings); $this->settingsRepo->writeSettings($settings);
} }
protected function getTestStation(): Entity\Station protected function getTestStation(): Entity\Station
@ -149,10 +150,13 @@ abstract class CestAbstract
$I->amOnPage('/'); $I->amOnPage('/');
$I->seeInCurrentUrl('/login'); $I->seeInCurrentUrl('/login');
$I->submitForm('#login-form', [ $I->submitForm(
'username' => $this->login_username, '#login-form',
'password' => $this->login_password, [
]); 'username' => $this->login_username,
'password' => $this->login_password,
]
);
$I->seeInSource('Logged In'); $I->seeInSource('Logged In');
} }