#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 [
// 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),
// Error handler
App\Http\ErrorHandler::class => DI\autowire(),
Slim\Interfaces\ErrorHandlerInterface::class => DI\Get(App\Http\ErrorHandler::class),
// HTTP client
GuzzleHttp\Client::class => function (Psr\Log\LoggerInterface $logger) {
$stack = GuzzleHttp\HandlerStack::create();
$stack->unshift(function (callable $handler) {
return function (Psr\Http\Message\RequestInterface $request, array $options) use ($handler) {
$options[GuzzleHttp\RequestOptions::VERIFY] = Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
return $handler($request, $options);
};
}, 'ssl_verify');
$stack->unshift(
function (callable $handler) {
return function (Psr\Http\Message\RequestInterface $request, array $options) use ($handler) {
$options[GuzzleHttp\RequestOptions::VERIFY] = Composer\CaBundle\CaBundle::getSystemCaRootBundlePath(
);
return $handler($request, $options);
};
},
'ssl_verify'
);
$stack->push(GuzzleHttp\Middleware::log(
$logger,
new GuzzleHttp\MessageFormatter('HTTP client {method} call to {uri} produced response {code}'),
Psr\Log\LogLevel::DEBUG
));
$stack->push(
GuzzleHttp\Middleware::log(
$logger,
new GuzzleHttp\MessageFormatter('HTTP client {method} call to {uri} produced response {code}'),
Psr\Log\LogLevel::DEBUG
)
);
return new GuzzleHttp\Client([
'handler' => $stack,
GuzzleHttp\RequestOptions::HTTP_ERRORS => false,
GuzzleHttp\RequestOptions::TIMEOUT => 3.0,
]);
return new GuzzleHttp\Client(
[
'handler' => $stack,
GuzzleHttp\RequestOptions::HTTP_ERRORS => false,
GuzzleHttp\RequestOptions::TIMEOUT => 3.0,
]
);
},
// DBAL
@ -128,13 +127,15 @@ return [
$eventManager->addEventSubscriber($eventAuditLog);
$eventManager->addEventSubscriber($eventChangeTracking);
return new App\Doctrine\DecoratedEntityManager(function () use (
$connectionOptions,
$config,
$eventManager
) {
return Doctrine\ORM\EntityManager::create($connectionOptions, $config, $eventManager);
});
return new App\Doctrine\DecoratedEntityManager(
function () use (
$connectionOptions,
$config,
$eventManager
) {
return Doctrine\ORM\EntityManager::create($connectionOptions, $config, $eventManager);
}
);
} catch (Exception $e) {
throw new App\Exception\BootstrapException($e->getMessage());
}
@ -143,11 +144,6 @@ return [
App\Doctrine\ReloadableEntityManagerInterface::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::class => function (Environment $environment) {
$redis_host = $environment->isDocker() ? 'redis' : 'localhost';
@ -303,7 +299,9 @@ return [
},
// 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 (
Doctrine\Common\Annotations\Reader $annotation_reader,
@ -323,7 +321,6 @@ return [
App\Plugins $plugins,
Environment $environment
) {
// Configure message sending middleware
$sendMessageMiddleware = new Symfony\Component\Messenger\Middleware\SendMessageMiddleware($queueManager);
$sendMessageMiddleware->setLogger($logger);
@ -355,17 +352,21 @@ return [
// On testing, messages are handled directly when called
if ($environment->isTesting()) {
return new Symfony\Component\Messenger\MessageBus([
$handleMessageMiddleware,
]);
return new Symfony\Component\Messenger\MessageBus(
[
$handleMessageMiddleware,
]
);
}
// Compile finished message bus.
return new Symfony\Component\Messenger\MessageBus([
$sendMessageMiddleware,
$uniqueMiddleware,
$handleMessageMiddleware,
]);
return new Symfony\Component\Messenger\MessageBus(
[
$sendMessageMiddleware,
$uniqueMiddleware,
$handleMessageMiddleware,
]
);
},
// Supervisor manager
@ -401,66 +402,4 @@ return [
},
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_SETTINGS = 'administer settings';
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_CUSTOM_FIELDS = 'administer custom fields';
public const GLOBAL_BACKUPS = 'administer backups';

View File

@ -3,6 +3,7 @@
namespace App;
use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use function base64_encode;
@ -43,41 +44,35 @@ class Assets
public function __construct(
Environment $environment,
array $libraries = [],
array $versioned_files = [],
array $vueComponents = []
Config $config,
?ServerRequestInterface $request
) {
$this->environment = $environment;
$this->request = $request;
$libraries = $config->get('assets');
foreach ($libraries as $library_name => $library) {
$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;
$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->csp_nonce = preg_replace('/[^A-Za-z0-9\+\/=]/', '', base64_encode(random_bytes(18)));
$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
{
if (!empty($vueComponents['entrypoints'])) {
@ -162,14 +157,16 @@ class Assets
*/
public function addJs($js_script): self
{
$this->load([
'order' => 100,
'files' => [
'js' => [
(is_array($js_script)) ? $js_script : ['src' => $js_script],
$this->load(
[
'order' => 100,
'files' => [
'js' => [
(is_array($js_script)) ? $js_script : ['src' => $js_script],
],
],
],
]);
]
);
return $this;
}
@ -250,12 +247,14 @@ class Assets
*/
public function addInlineJs($js_script, int $order = 100): self
{
$this->load([
'order' => $order,
'inline' => [
'js' => (is_array($js_script)) ? $js_script : [$js_script],
],
]);
$this->load(
[
'order' => $order,
'inline' => [
'js' => (is_array($js_script)) ? $js_script : [$js_script],
],
]
);
return $this;
}
@ -268,14 +267,16 @@ class Assets
*/
public function addCss($css_script, int $order = 100): self
{
$this->load([
'order' => $order,
'files' => [
'css' => [
(is_array($css_script)) ? $css_script : ['src' => $css_script],
$this->load(
[
'order' => $order,
'files' => [
'css' => [
(is_array($css_script)) ? $css_script : ['src' => $css_script],
],
],
],
]);
]
);
return $this;
}
@ -287,12 +288,14 @@ class Assets
*/
public function addInlineCss($css_script): self
{
$this->load([
'order' => 100,
'inline' => [
'css' => (is_array($css_script)) ? $css_script : [$css_script],
],
]);
$this->load(
[
'order' => 100,
'inline' => [
'css' => (is_array($css_script)) ? $css_script : [$css_script],
],
]
);
return $this;
}
@ -310,10 +313,13 @@ class Assets
foreach ($this->loaded as $item) {
if (!empty($item['files']['css'])) {
foreach ($item['files']['css'] as $file) {
$compiled_attributes = $this->compileAttributes($file, [
'rel' => 'stylesheet',
'type' => 'text/css',
]);
$compiled_attributes = $this->compileAttributes(
$file,
[
'rel' => 'stylesheet',
'type' => 'text/css',
]
);
$result[] = '<link ' . implode(' ', $compiled_attributes) . ' />';
}
@ -349,9 +355,12 @@ class Assets
foreach ($this->loaded as $item) {
if (!empty($item['files']['js'])) {
foreach ($item['files']['js'] as $file) {
$compiled_attributes = $this->compileAttributes($file, [
'type' => 'text/javascript',
]);
$compiled_attributes = $this->compileAttributes(
$file,
[
'type' => 'text/javascript',
]
);
$result[] = '<script ' . implode(' ', $compiled_attributes) . '></script>';
}
@ -397,9 +406,12 @@ class Assets
protected function sort(): void
{
if (!$this->is_sorted) {
uasort($this->loaded, function ($a, $b): int {
return $a['order'] <=> $b['order']; // SPACESHIP!
});
uasort(
$this->loaded,
function ($a, $b): int {
return $a['order'] <=> $b['order']; // SPACESHIP!
}
);
$this->is_sorted = true;
}
@ -480,4 +492,23 @@ class Assets
$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(
SymfonyStyle $io,
Entity\Repository\SettingsTableRepository $settingsTableRepo
Entity\Repository\SettingsRepository $settingsTableRepo
): int {
$io->title(__('AzuraCast Settings'));

View File

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

View File

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

View File

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

View File

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

View File

@ -20,23 +20,18 @@ class SettingsController
protected ValidatorInterface $validator;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo;
protected Entity\Settings $settings;
protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct(
EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsTableRepo,
Entity\Settings $settings,
Entity\Repository\SettingsRepository $settingsRepo,
Serializer $serializer,
ValidatorInterface $validator
) {
$this->em = $em;
$this->serializer = $serializer;
$this->validator = $validator;
$this->settingsTableRepo = $settingsTableRepo;
$this->settings = $settings;
$this->settingsRepo = $settingsRepo;
}
/**
@ -55,7 +50,8 @@ class SettingsController
*/
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
{
$this->settingsTableRepo->writeSettings($request->getParsedBody());
$this->settingsRepo->writeSettings($request->getParsedBody());
return $response->withJson(new Entity\Api\Status());
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,11 +23,11 @@ class OverviewController
public function __construct(
EntityManagerInterface $em,
Entity\Settings $settings,
Entity\Repository\SettingsRepository $settingsRepo,
Entity\Repository\AnalyticsRepository $analyticsRepo
) {
$this->em = $em;
$this->settings = $settings;
$this->settings = $settingsRepo->readSettings();
$this->analyticsRepo = $analyticsRepo;
}
@ -237,25 +237,32 @@ class OverviewController
$songs[] = $song_row;
}
usort($songs, function ($a_arr, $b_arr) {
$a = $a_arr['stat_delta'];
$b = $b_arr['stat_delta'];
usort(
$songs,
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', [
'charts' => [
'daily' => json_encode($daily_data, JSON_THROW_ON_ERROR),
'daily_alt' => implode('', $daily_alt),
'hourly' => json_encode($hourly_data, JSON_THROW_ON_ERROR),
'hourly_alt' => implode('', $hourly_alt),
'day_of_week' => json_encode($day_of_week_data, JSON_THROW_ON_ERROR),
'day_of_week_alt' => implode('', $day_of_week_alt),
],
'song_totals' => $song_totals,
'best_performing_songs' => array_reverse(array_slice($songs, -5)),
'worst_performing_songs' => array_slice($songs, 0, 5),
]);
return $request->getView()->renderToResponse(
$response,
'stations/reports/overview',
[
'charts' => [
'daily' => json_encode($daily_data, JSON_THROW_ON_ERROR),
'daily_alt' => implode('', $daily_alt),
'hourly' => json_encode($hourly_data, JSON_THROW_ON_ERROR),
'hourly_alt' => implode('', $hourly_alt),
'day_of_week' => json_encode($day_of_week_data, JSON_THROW_ON_ERROR),
'day_of_week_alt' => implode('', $day_of_week_alt),
],
'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(
EntityManagerInterface $em,
AzuraCastCentral $ac_central,
Entity\Settings $settings
Entity\Repository\SettingsRepository $settingsRepo
) {
$this->em = $em;
$this->ac_central = $ac_central;
$this->settings = $settings;
$this->settings = $settingsRepo->readSettings();
}
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
@ -60,12 +60,16 @@ class StreamersController
$be_settings = $station->getBackendConfig();
return $view->renderToResponse($response, 'stations/streamers/index', [
'server_url' => $this->settings->getBaseUrl(),
'stream_port' => $backend->getStreamPort($station),
'ip' => $this->ac_central->getIp(),
'dj_mount_point' => $be_settings['dj_mount_point'] ?? '/',
'station_tz' => $station->getTimezone(),
]);
return $view->renderToResponse(
$response,
'stations/streamers/index',
[
'server_url' => $this->settings->getBaseUrl(),
'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 = '';
public function __construct(
Entity\Settings $settings,
Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment,
ServerRequestInterface $request
) {
$this->settings = $settings;
$this->settings = $settingsRepo->readSettings();
$this->environment = $environment;
$this->instanceName = $settings->getInstanceName() ?? '';
$this->instanceName = $this->settings->getInstanceName() ?? '';
// Register current user
$this->user = $request->getAttribute(ServerRequest::ATTR_USER);
@ -52,7 +52,7 @@ class Customization
if (!empty($queryParams['theme'])) {
$this->publicTheme = $this->theme = $queryParams['theme'];
} else {
$this->publicTheme = $settings->getPublicTheme() ?? $this->publicTheme;
$this->publicTheme = $this->settings->getPublicTheme() ?? $this->publicTheme;
if (null !== $this->user && !empty($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\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_TTL = 600;
@ -23,6 +25,8 @@ class SettingsTableRepository extends Repository
protected ValidatorInterface $validator;
protected string $entityClass = Entity\SettingsTable::class;
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
@ -37,32 +41,18 @@ class SettingsTableRepository extends Repository
$this->validator = $validator;
}
public function readSettings(): Entity\Settings
public function readSettings(bool $reload = false): Entity\Settings
{
if (Entity\Settings::hasInstance()) {
return Entity\Settings::getInstance();
} else {
$settings = $this->arrayToObject($this->readSettingsArray());
Entity\Settings::setInstance($settings);
return $settings;
if ($reload || null === self::$instance) {
self::$instance = $this->arrayToObject($this->readSettingsArray());
}
return self::$instance;
}
/**
* 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
public function clearSettingsInstance(): void
{
if (null === $newData) {
$newData = $this->readSettingsArray();
}
$settings = $this->arrayToObject($newData, $this->readSettings());
return $settings;
self::$instance = null;
}
/**
@ -70,6 +60,10 @@ class SettingsTableRepository extends Repository
*/
public function readSettingsArray(): array
{
if ($this->cache->has(self::CACHE_KEY)) {
return $this->cache->get(self::CACHE_KEY);
}
$allRecords = [];
foreach ($this->repository->findAll() as $record) {
/** @var Entity\SettingsTable $record */
@ -87,7 +81,7 @@ class SettingsTableRepository extends Repository
public function writeSettings($settingsObj): void
{
if (is_array($settingsObj)) {
$settingsObj = $this->updateSettings($settingsObj);
$settingsObj = $this->arrayToObject($settingsObj, $this->readSettings(true));
}
$errors = $this->validator->validate($settingsObj);
@ -155,9 +149,12 @@ class SettingsTableRepository extends Repository
protected function arrayToObject(array $settings, ?Entity\Settings $existingSettings = null): Entity\Settings
{
$settings = array_filter($settings, function ($value) {
return null !== $value;
});
$settings = array_filter(
$settings,
function ($value) {
return null !== $value;
}
);
$context = [];
if (null !== $existingSettings) {

View File

@ -34,28 +34,29 @@ class StationRepository extends Repository
protected StorageLocationRepository $storageLocationRepo;
protected Entity\Settings $settings;
protected SettingsRepository $settingsRepo;
public function __construct(
EntityManagerInterface $em,
Serializer $serializer,
Environment $environment,
SettingsRepository $settingsRepo,
StorageLocationRepository $storageLocationRepo,
LoggerInterface $logger,
CheckMediaTask $mediaSync,
Adapters $adapters,
Configuration $configuration,
ValidatorInterface $validator,
CacheInterface $cache,
Entity\Settings $settings
CacheInterface $cache
) {
$this->mediaSync = $mediaSync;
$this->adapters = $adapters;
$this->configuration = $configuration;
$this->validator = $validator;
$this->cache = $cache;
$this->settingsRepo = $settingsRepo;
$this->storageLocationRepo = $storageLocationRepo;
$this->settings = $settings;
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)) {
return new Uri($custom_url);

View File

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

View File

@ -11,13 +11,13 @@ abstract class AbstractSettingsForm extends Form
{
protected EntityManagerInterface $em;
protected Entity\Repository\SettingsTableRepository $settingsTableRepo;
protected Entity\Repository\SettingsRepository $settingsRepo;
protected Environment $environment;
public function __construct(
EntityManagerInterface $em,
Entity\Repository\SettingsTableRepository $settingsTableRepo,
Entity\Repository\SettingsRepository $settingsRepo,
Environment $environment,
array $formConfig
) {
@ -25,7 +25,7 @@ abstract class AbstractSettingsForm extends Form
$this->em = $em;
$this->environment = $environment;
$this->settingsTableRepo = $settingsTableRepo;
$this->settingsRepo = $settingsRepo;
}
public function getEntityManager(): EntityManagerInterface
@ -33,9 +33,9 @@ abstract class AbstractSettingsForm extends Form
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
@ -46,7 +46,7 @@ abstract class AbstractSettingsForm extends Form
public function process(ServerRequest $request): bool
{
// 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.
if (empty($defaults['baseUrl'])) {
@ -59,7 +59,7 @@ abstract class AbstractSettingsForm extends Form
// Handle submission.
if ('POST' === $request->getMethod() && $this->isValid($request->getParsedBody())) {
$data = $this->getValues();
$this->settingsTableRepo->writeSettings($data);
$this->settingsRepo->writeSettings($data);
return true;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,17 +2,17 @@
namespace App\MessageQueue;
use App\Entity\Repository\SettingsTableRepository;
use App\Entity\Repository\SettingsRepository;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
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
{
$this->settingsTableRepo->updateSettings();
$this->settingsRepo->clearSettingsInstance();
}
}

View File

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

View File

@ -2,9 +2,7 @@
namespace App\Middleware;
use App\Assets;
use App\Entity;
use App\Http\Response;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@ -19,18 +17,14 @@ class EnforceSecurity implements MiddlewareInterface
{
protected ResponseFactoryInterface $responseFactory;
protected Assets $assets;
protected Entity\Settings $settings;
protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct(
App $app,
Assets $assets,
Entity\Settings $settings
Entity\Repository\SettingsRepository $settingsRepo
) {
$this->responseFactory = $app->getResponseFactory();
$this->assets = $assets;
$this->settings = $settings;
$this->settingsRepo = $settingsRepo;
}
/**
@ -39,20 +33,17 @@ class EnforceSecurity implements MiddlewareInterface
*/
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;
// Assemble Content Security Policy (CSP)
$csp = [];
$add_hsts_header = false;
$addHstsHeader = false;
if ('https' === $request->getUri()->getScheme()) {
// Enforce secure cookies.
ini_set('session.cookie_secure', '1');
$csp[] = 'upgrade-insecure-requests';
$add_hsts_header = true;
$addHstsHeader = true;
} elseif ($always_use_ssl && !$internal_api_url) {
return $this->responseFactory->createResponse(307)
->withHeader('Location', (string)$request->getUri()->withScheme('https'));
@ -60,7 +51,7 @@ class EnforceSecurity implements MiddlewareInterface
$response = $handler->handle($request);
if ($add_hsts_header) {
if ($addHstsHeader) {
$response = $response->withHeader('Strict-Transport-Security', 'max-age=3600');
}
@ -72,19 +63,6 @@ class EnforceSecurity implements MiddlewareInterface
$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;
}
}

View File

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

View File

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

View File

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

View File

@ -11,14 +11,16 @@ use Carbon\CarbonImmutable;
class RecentBackupCheck
{
protected Entity\Settings $settings;
protected Environment $environment;
public function __construct(Entity\Settings $settings, Environment $environment)
{
$this->settings = $settings;
protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct(
Environment $environment,
Entity\Repository\SettingsRepository $settingsRepo
) {
$this->environment = $environment;
$this->settingsRepo = $settingsRepo;
}
public function __invoke(GetNotifications $event): void
@ -37,27 +39,31 @@ class RecentBackupCheck
$threshold = CarbonImmutable::now()->subWeeks(2)->getTimestamp();
// Don't show backup warning for freshly created installations.
$setupComplete = $this->settings->getSetupCompleteTime();
$settings = $this->settingsRepo->readSettings();
$setupComplete = $settings->getSetupCompleteTime();
if ($setupComplete >= $threshold) {
return;
}
$backupLastRun = $this->settings->getBackupLastRun();
$backupLastRun = $settings->getBackupLastRun();
if ($backupLastRun < $threshold) {
$router = $request->getRouter();
$backupUrl = $router->named('admin:backups:index');
$event->addNotification(new Notification(
__('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
),
// phpcs:enable
Notification::INFO
));
$event->addNotification(
new Notification(
__('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
),
// phpcs:enable
Notification::INFO
)
);
}
}
}

View File

@ -12,14 +12,14 @@ class SyncTaskCheck
{
protected Runner $syncRunner;
protected Entity\Settings $settings;
protected Entity\Repository\SettingsRepository $settingsRepo;
public function __construct(
Runner $syncRunner,
Entity\Settings $settings
Entity\Repository\SettingsRepository $settingsRepo
) {
$this->syncRunner = $syncRunner;
$this->settings = $settings;
$this->settingsRepo = $settingsRepo;
}
public function __invoke(GetNotifications $event): void
@ -31,7 +31,9 @@ class SyncTaskCheck
return;
}
$setupComplete = $this->settings->isSetupComplete();
$settings = $this->settingsRepo->readSettings();
$setupComplete = $settings->isSetupComplete();
$syncTasks = $this->syncRunner->getSyncTimes();
foreach ($syncTasks as $taskKey => $task) {
@ -48,17 +50,19 @@ class SyncTaskCheck
$router = $request->getRouter();
$backupUrl = $router->named('admin:debug:sync', ['type' => $taskKey]);
$event->addNotification(new Notification(
__('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'],
$backupUrl
),
// phpcs:enable
Notification::ERROR
));
$event->addNotification(
new Notification(
__('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'],
$backupUrl
),
// phpcs:enable
Notification::ERROR
)
);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,10 +11,35 @@ class TaskLocator
protected array $tasks;
public function __construct(ContainerInterface $di, array $tasks)
public function __construct(ContainerInterface $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

View File

@ -2,12 +2,132 @@
namespace App;
use App\Http\Response;
use App\Http\ServerRequest;
use DI\FactoryInterface;
use Doctrine\Inflector\InflectorFactory;
use League\Plates\Engine;
use League\Plates\Template\Data;
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
{
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
{
$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.
*
* @param ResponseInterface $response
* @param string $template_name
* @param array $template_args
* @param string $templateName
* @param array $templateArgs
*/
public function renderToResponse(
ResponseInterface $response,
$template_name,
array $template_args = []
string $templateName,
array $templateArgs = []
): ResponseInterface {
$template = $this->render($template_name, $template_args);
$template = $this->render($templateName, $templateArgs);
$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;
use App\Config;
use App\Webhook\Connector\ConnectorInterface;
use InvalidArgumentException;
use Psr\Container\ContainerInterface;
@ -12,8 +13,16 @@ class ConnectorLocator
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->connectors = $connectors;
}

View File

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

View File

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