From 9fe90b1d3c7cd439c603919800c4268733e41d32 Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Mon, 4 Dec 2023 00:48:40 -0600 Subject: [PATCH] Merge commit '8ea5b1b85e9477cd6f1bbff475839b4e88986be5' --- .editorconfig | 3 + phpstan.neon | 4 + src/Acl.php | 5 +- src/AppFactory.php | 3 + src/Console/Command/Backup/BackupCommand.php | 13 +- src/Console/Command/Backup/RestoreCommand.php | 3 +- .../Command/MessageQueue/ClearCommand.php | 5 +- .../Command/MessageQueue/ProcessCommand.php | 5 +- src/Console/Command/ReprocessMediaCommand.php | 5 +- src/Console/Command/RestartRadioCommand.php | 5 +- src/Console/Command/RollbackDbCommand.php | 3 +- src/Console/Command/Settings/SetCommand.php | 5 +- .../Sync/NowPlayingPerStationCommand.php | 3 +- .../Command/Sync/SingleTaskCommand.php | 5 +- .../Command/Users/LoginTokenCommand.php | 3 +- .../Command/Users/ResetPasswordCommand.php | 2 +- .../Command/Users/SetAdministratorCommand.php | 3 +- .../Api/Frontend/Dashboard/ChartsAction.php | 5 +- src/Controller/Api/NowPlayingController.php | 11 +- .../Api/Stations/PlaylistsController.php | 8 +- src/Controller/Api/Traits/CanSortResults.php | 11 +- src/Controller/Frontend/IndexAction.php | 12 +- .../Frontend/PublicPages/OnDemandAction.php | 1 + src/Customization.php | 4 +- src/Doctrine/Event/AuditLog.php | 4 +- src/Entity/CacheItem.php | 10 +- src/Entity/ListenerLocation.php | 9 +- src/Entity/PodcastMedia.php | 8 +- .../Repository/SongHistoryRepository.php | 2 +- src/Entity/Settings.php | 38 +-- src/Entity/StationBackendConfiguration.php | 84 ++--- src/Entity/StationBrandingConfiguration.php | 13 +- src/Entity/StationFrontendConfiguration.php | 34 ++- src/Entity/StationMedia.php | 77 ++--- src/Entity/User.php | 2 +- src/Enums/SupportedLocales.php | 13 +- src/Environment.php | 289 ++++++++++-------- src/Http/ErrorHandler.php | 4 + src/Http/Router.php | 2 +- src/Http/RouterInterface.php | 5 +- src/Http/ServerRequest.php | 7 +- src/Installer/Command/InstallCommand.php | 21 +- src/Installer/EnvFiles/AbstractEnvFile.php | 10 +- src/Middleware/AbstractMiddleware.php | 26 ++ src/Middleware/ApplyXForwardedProto.php | 11 +- src/Middleware/Auth/AbstractAuth.php | 15 +- src/Middleware/Auth/ApiAuth.php | 22 +- src/Middleware/Auth/StandardAuth.php | 7 +- src/Middleware/EnableView.php | 6 +- src/Middleware/EnforceSecurity.php | 11 +- src/Middleware/GetCurrentUser.php | 11 +- src/Middleware/GetStation.php | 6 +- src/Middleware/HandleMultipartJson.php | 11 +- src/Middleware/InjectRateLimit.php | 10 +- src/Middleware/InjectRouter.php | 10 +- src/Middleware/InjectSession.php | 8 +- src/Middleware/Module/Api.php | 10 +- src/Middleware/Module/PanelLayout.php | 3 +- src/Middleware/Permissions.php | 2 +- src/Middleware/RateLimit.php | 2 +- src/Middleware/RemoveSlashes.php | 7 +- .../ReopenEntityManagerMiddleware.php | 7 +- src/Middleware/RequireLogin.php | 2 +- ...quirePublishedPodcastEpisodeMiddleware.php | 2 +- src/Middleware/RequireStation.php | 2 +- src/Middleware/StationSupportsFeature.php | 2 +- .../WrapExceptionsWithRequestData.php | 7 +- src/Nginx/HlsListeners.php | 1 + src/Paginator.php | 23 +- .../Liquidsoap/Command/AbstractCommand.php | 3 +- src/Radio/Configuration.php | 1 + src/Sync/Task/CheckMediaTask.php | 7 +- src/Tests/Module.php | 5 +- src/Traits/RequestAwareTrait.php | 8 +- src/Translations/JsonGenerator.php | 3 +- src/Utilities/Strings.php | 18 -- src/Utilities/Types.php | 107 +++++++ src/Version.php | 2 +- src/View.php | 9 +- src/Webhook/Connector/AbstractConnector.php | 8 +- src/Webhook/Connector/Email.php | 9 +- src/Webhook/Connector/Generic.php | 3 +- src/Webhook/Connector/GoogleAnalyticsV4.php | 8 +- src/Webhook/Connector/Mastodon.php | 9 +- src/Webhook/Connector/MatomoAnalytics.php | 14 +- src/Webhook/Connector/Telegram.php | 15 +- templates/partials/toasts.phtml | 9 +- util/phpstan-doctrine.php | 18 ++ util/phpstan.php | 4 +- 89 files changed, 739 insertions(+), 504 deletions(-) create mode 100644 src/Middleware/AbstractMiddleware.php create mode 100644 src/Utilities/Types.php create mode 100644 util/phpstan-doctrine.php diff --git a/.editorconfig b/.editorconfig index 64f8fc023..b4013cfa7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -900,3 +900,6 @@ ij_yaml_sequence_on_new_line = false ij_yaml_space_before_colon = false ij_yaml_spaces_within_braces = true ij_yaml_spaces_within_brackets = true + +[*.neon] +indent_style = tab \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index bbf388322..cd6f7b389 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,12 +1,16 @@ includes: - phpstan-baseline.neon - vendor/phpstan/phpstan-doctrine/extension.neon + - vendor/phpstan/phpstan-doctrine/rules.neon parameters: level: 8 checkMissingIterableValueType: false + doctrine: + objectManagerLoader: util/phpstan-doctrine.php + paths: - bin - config diff --git a/src/Acl.php b/src/Acl.php index a7b88a93c..cb112a207 100644 --- a/src/Acl.php +++ b/src/Acl.php @@ -14,7 +14,6 @@ use App\Enums\StationPermissions; use App\Http\ServerRequest; use App\Traits\RequestAwareTrait; use Psr\EventDispatcher\EventDispatcherInterface; -use Psr\Http\Message\ServerRequestInterface; use function in_array; use function is_array; @@ -106,8 +105,8 @@ final class Acl array|string|PermissionInterface $action, Station|int $stationId = null ): bool { - if ($this->request instanceof ServerRequestInterface) { - $user = $this->request->getAttribute(ServerRequest::ATTR_USER); + if ($this->request instanceof ServerRequest) { + $user = $this->request->getUser(); return $this->userAllowed($user, $action, $stationId); } diff --git a/src/AppFactory.php b/src/AppFactory.php index 030f3ffbd..3f2af7f9e 100644 --- a/src/AppFactory.php +++ b/src/AppFactory.php @@ -141,6 +141,9 @@ final class AppFactory : $environment->getTempDirectory() . '/php_errors.log' ); + mb_internal_encoding('UTF-8'); + ini_set('default_charset', 'utf-8'); + if (!headers_sent()) { ini_set('session.use_only_cookies', '1'); ini_set('session.cookie_httponly', '1'); diff --git a/src/Console/Command/Backup/BackupCommand.php b/src/Console/Command/Backup/BackupCommand.php index c67c2ab5a..8c4593faf 100644 --- a/src/Console/Command/Backup/BackupCommand.php +++ b/src/Console/Command/Backup/BackupCommand.php @@ -9,6 +9,7 @@ use App\Entity\Enums\StorageLocationTypes; use App\Entity\Repository\StorageLocationRepository; use App\Entity\Station; use App\Entity\StorageLocation; +use App\Utilities\Types; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -45,16 +46,14 @@ final class BackupCommand extends AbstractDatabaseCommand $io = new SymfonyStyle($input, $output); $fsUtils = new Filesystem(); - $path = $input->getArgument('path'); - $excludeMedia = (bool)$input->getOption('exclude-media'); - $storageLocationId = $input->getOption('storage-location-id'); + $path = Types::stringOrNull($input->getArgument('path'), true) + ?? 'manual_backup_' . gmdate('Ymd_Hi') . '.zip'; + + $excludeMedia = Types::bool($input->getOption('exclude-media')); + $storageLocationId = Types::intOrNull($input->getOption('storage-location-id')); $startTime = microtime(true); - if (empty($path)) { - $path = 'manual_backup_' . gmdate('Ymd_Hi') . '.zip'; - } - $fileExt = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if (Path::isAbsolute($path)) { diff --git a/src/Console/Command/Backup/RestoreCommand.php b/src/Console/Command/Backup/RestoreCommand.php index ccea4e822..cc64b1236 100644 --- a/src/Console/Command/Backup/RestoreCommand.php +++ b/src/Console/Command/Backup/RestoreCommand.php @@ -6,6 +6,7 @@ namespace App\Console\Command\Backup; use App\Console\Command\AbstractDatabaseCommand; use App\Entity\StorageLocation; +use App\Utilities\Types; use Exception; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -34,7 +35,7 @@ final class RestoreCommand extends AbstractDatabaseCommand { $io = new SymfonyStyle($input, $output); - $path = $input->getArgument('path'); + $path = Types::stringOrNull($input->getArgument('path'), true); $startTime = microtime(true); $io->title('AzuraCast Restore'); diff --git a/src/Console/Command/MessageQueue/ClearCommand.php b/src/Console/Command/MessageQueue/ClearCommand.php index ed468e0ff..9d416a41f 100644 --- a/src/Console/Command/MessageQueue/ClearCommand.php +++ b/src/Console/Command/MessageQueue/ClearCommand.php @@ -7,6 +7,7 @@ namespace App\Console\Command\MessageQueue; use App\Console\Command\CommandAbstract; use App\MessageQueue\QueueManagerInterface; use App\MessageQueue\QueueNames; +use App\Utilities\Types; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -35,9 +36,9 @@ final class ClearCommand extends CommandAbstract { $io = new SymfonyStyle($input, $output); - $queueName = $input->getArgument('queue'); + $queueName = Types::stringOrNull($input->getArgument('queue'), true); - if (!empty($queueName)) { + if (null !== $queueName) { $queue = QueueNames::tryFrom($queueName); if (null !== $queue) { diff --git a/src/Console/Command/MessageQueue/ProcessCommand.php b/src/Console/Command/MessageQueue/ProcessCommand.php index 46fad5cb3..036ddafa2 100644 --- a/src/Console/Command/MessageQueue/ProcessCommand.php +++ b/src/Console/Command/MessageQueue/ProcessCommand.php @@ -13,6 +13,7 @@ use App\MessageQueue\LogWorkerExceptionSubscriber; use App\MessageQueue\QueueManagerInterface; use App\MessageQueue\ResetArrayCacheSubscriber; use App\Service\HighAvailability; +use App\Utilities\Types; use Psr\Log\LogLevel; use Psr\Log\NullLogger; use Symfony\Component\Console\Attribute\AsCommand; @@ -55,8 +56,8 @@ final class ProcessCommand extends AbstractSyncCommand { $this->logToExtraFile('app_worker.log'); - $runtime = (int)$input->getArgument('runtime'); - $workerName = $input->getOption('worker-name'); + $runtime = Types::int($input->getArgument('runtime')); + $workerName = Types::stringOrNull($input->getOption('worker-name'), true); if (!$this->highAvailability->isActiveServer()) { $this->logger->error('This instance is not the current active instance.'); diff --git a/src/Console/Command/ReprocessMediaCommand.php b/src/Console/Command/ReprocessMediaCommand.php index c562b70d1..507437fd9 100644 --- a/src/Console/Command/ReprocessMediaCommand.php +++ b/src/Console/Command/ReprocessMediaCommand.php @@ -8,6 +8,7 @@ use App\Container\EntityManagerAwareTrait; use App\Entity\Repository\StationRepository; use App\Entity\Station; use App\Entity\StationMedia; +use App\Utilities\Types; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -37,11 +38,11 @@ final class ReprocessMediaCommand extends CommandAbstract { $io = new SymfonyStyle($input, $output); - $stationName = $input->getArgument('station-name'); + $stationName = Types::stringOrNull($input->getArgument('station-name'), true); $io->title('Manually Reprocess Media'); - if (empty($stationName)) { + if (null === $stationName) { $io->section('Reprocessing media for all stations...'); $storageLocation = null; diff --git a/src/Console/Command/RestartRadioCommand.php b/src/Console/Command/RestartRadioCommand.php index 59abe4c7e..a3f5a58a8 100644 --- a/src/Console/Command/RestartRadioCommand.php +++ b/src/Console/Command/RestartRadioCommand.php @@ -8,6 +8,7 @@ use App\Entity\Repository\StationRepository; use App\Entity\Station; use App\Nginx\Nginx; use App\Radio\Configuration; +use App\Utilities\Types; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -45,8 +46,8 @@ final class RestartRadioCommand extends CommandAbstract { $io = new SymfonyStyle($input, $output); - $stationName = $input->getArgument('station-name'); - $noSupervisorRestart = (bool)$input->getOption('no-supervisor-restart'); + $stationName = Types::stringOrNull($input->getArgument('station-name')); + $noSupervisorRestart = Types::bool($input->getOption('no-supervisor-restart')); if (!empty($stationName)) { $station = $this->stationRepo->findByIdentifier($stationName); diff --git a/src/Console/Command/RollbackDbCommand.php b/src/Console/Command/RollbackDbCommand.php index 6e592a1bb..5a40f3176 100644 --- a/src/Console/Command/RollbackDbCommand.php +++ b/src/Console/Command/RollbackDbCommand.php @@ -7,6 +7,7 @@ namespace App\Console\Command; use App\Container\ContainerAwareTrait; use App\Container\EnvironmentAwareTrait; use App\Entity\Attributes\StableMigration; +use App\Utilities\Types; use Exception; use FilesystemIterator; use InvalidArgumentException; @@ -44,7 +45,7 @@ final class RollbackDbCommand extends AbstractDatabaseCommand // Pull migration corresponding to the stable version specified. try { - $version = $input->getArgument('version'); + $version = Types::string($input->getArgument('version')); $migrationVersion = $this->findMigration($version); } catch (Throwable $e) { $io->error($e->getMessage()); diff --git a/src/Console/Command/Settings/SetCommand.php b/src/Console/Command/Settings/SetCommand.php index 57719a3b1..b66b42cda 100644 --- a/src/Console/Command/Settings/SetCommand.php +++ b/src/Console/Command/Settings/SetCommand.php @@ -6,6 +6,7 @@ namespace App\Console\Command\Settings; use App\Console\Command\CommandAbstract; use App\Container\SettingsAwareTrait; +use App\Utilities\Types; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -30,8 +31,8 @@ final class SetCommand extends CommandAbstract { $io = new SymfonyStyle($input, $output); - $settingKey = $input->getArgument('setting-key'); - $settingValue = $input->getArgument('setting-value'); + $settingKey = Types::string($input->getArgument('setting-key')); + $settingValue = Types::string($input->getArgument('setting-value')); $io->title('AzuraCast Settings'); diff --git a/src/Console/Command/Sync/NowPlayingPerStationCommand.php b/src/Console/Command/Sync/NowPlayingPerStationCommand.php index 4ed165e51..8d8f201c3 100644 --- a/src/Console/Command/Sync/NowPlayingPerStationCommand.php +++ b/src/Console/Command/Sync/NowPlayingPerStationCommand.php @@ -9,6 +9,7 @@ use App\Entity\Repository\StationRepository; use App\Entity\Station; use App\Sync\NowPlaying\Task\BuildQueueTask; use App\Sync\NowPlaying\Task\NowPlayingTask; +use App\Utilities\Types; use Monolog\LogRecord; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -43,7 +44,7 @@ final class NowPlayingPerStationCommand extends AbstractSyncCommand $this->logToExtraFile('app_nowplaying.log'); $io = new SymfonyStyle($input, $output); - $stationName = $input->getArgument('station'); + $stationName = Types::string($input->getArgument('station')); $station = $this->stationRepo->findByIdentifier($stationName); if (!($station instanceof Station)) { diff --git a/src/Console/Command/Sync/SingleTaskCommand.php b/src/Console/Command/Sync/SingleTaskCommand.php index d9e750e5b..b9c76e079 100644 --- a/src/Console/Command/Sync/SingleTaskCommand.php +++ b/src/Console/Command/Sync/SingleTaskCommand.php @@ -8,6 +8,7 @@ use App\Cache\DatabaseCache; use App\Container\ContainerAwareTrait; use App\Container\LoggerAwareTrait; use App\Sync\Task\AbstractTask; +use App\Utilities\Types; use InvalidArgumentException; use Monolog\LogRecord; use ReflectionClass; @@ -42,7 +43,9 @@ final class SingleTaskCommand extends AbstractSyncCommand $this->logToExtraFile('app_sync.log'); $io = new SymfonyStyle($input, $output); - $task = $input->getArgument('task'); + + /** @var class-string $task */ + $task = Types::string($input->getArgument('task')); try { $this->runTask($task); diff --git a/src/Console/Command/Users/LoginTokenCommand.php b/src/Console/Command/Users/LoginTokenCommand.php index 2095fefcf..d44490baf 100644 --- a/src/Console/Command/Users/LoginTokenCommand.php +++ b/src/Console/Command/Users/LoginTokenCommand.php @@ -9,6 +9,7 @@ use App\Container\EntityManagerAwareTrait; use App\Entity\Repository\UserLoginTokenRepository; use App\Entity\User; use App\Http\RouterInterface; +use App\Utilities\Types; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -39,7 +40,7 @@ final class LoginTokenCommand extends CommandAbstract { $io = new SymfonyStyle($input, $output); - $email = $input->getArgument('email'); + $email = Types::string($input->getArgument('email')); $io->title('Generate Account Login Recovery URL'); diff --git a/src/Console/Command/Users/ResetPasswordCommand.php b/src/Console/Command/Users/ResetPasswordCommand.php index 9daf23b2a..d1b28d767 100644 --- a/src/Console/Command/Users/ResetPasswordCommand.php +++ b/src/Console/Command/Users/ResetPasswordCommand.php @@ -31,7 +31,7 @@ final class ResetPasswordCommand extends CommandAbstract { $io = new SymfonyStyle($input, $output); - $email = $input->getArgument('email'); + $email = Utilities\Types::string($input->getArgument('email')); $io->title('Reset Account Password'); diff --git a/src/Console/Command/Users/SetAdministratorCommand.php b/src/Console/Command/Users/SetAdministratorCommand.php index f3dfa053e..aebd01350 100644 --- a/src/Console/Command/Users/SetAdministratorCommand.php +++ b/src/Console/Command/Users/SetAdministratorCommand.php @@ -8,6 +8,7 @@ use App\Console\Command\CommandAbstract; use App\Container\EntityManagerAwareTrait; use App\Entity\Repository\RolePermissionRepository; use App\Entity\User; +use App\Utilities\Types; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -37,7 +38,7 @@ final class SetAdministratorCommand extends CommandAbstract { $io = new SymfonyStyle($input, $output); - $email = $input->getArgument('email'); + $email = Types::string($input->getArgument('email')); $io->title('Set Administrator'); diff --git a/src/Controller/Api/Frontend/Dashboard/ChartsAction.php b/src/Controller/Api/Frontend/Dashboard/ChartsAction.php index 7ea7bd44b..55019a9d4 100644 --- a/src/Controller/Api/Frontend/Dashboard/ChartsAction.php +++ b/src/Controller/Api/Frontend/Dashboard/ChartsAction.php @@ -66,6 +66,7 @@ final class ChartsAction implements SingleActionInterface } else { $threshold = CarbonImmutable::parse('-180 days'); + /** @var array $stats */ $stats = $this->em->createQuery( <<<'DQL' SELECT a.station_id, a.moment, a.number_avg, a.number_unique @@ -88,14 +89,12 @@ final class ChartsAction implements SingleActionInterface foreach ($stats as $row) { $stationId = $row['station_id']; - - /** @var CarbonImmutable $moment */ $moment = $row['moment']; $sortableKey = $moment->format('Y-m-d'); $jsTimestamp = $moment->getTimestamp() * 1000; - $average = round((float)$row['number_avg'], 2); + $average = round($row['number_avg'], 2); $unique = $row['number_unique']; $rawStats['average'][$stationId][$sortableKey] = [ diff --git a/src/Controller/Api/NowPlayingController.php b/src/Controller/Api/NowPlayingController.php index 477c1f4cc..dea760962 100644 --- a/src/Controller/Api/NowPlayingController.php +++ b/src/Controller/Api/NowPlayingController.php @@ -7,6 +7,7 @@ namespace App\Controller\Api; use App\Cache\NowPlayingCache; use App\Entity\Api\Error; use App\Entity\Api\NowPlaying\NowPlaying; +use App\Exception\InvalidRequestAttribute; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; @@ -83,9 +84,13 @@ final class NowPlayingController $baseUrl = $router->getBaseUrl(); // If unauthenticated, hide non-public stations from full view. - $np = $this->nowPlayingCache->getForAllStations( - $request->getAttribute('user') === null - ); + try { + $user = $request->getUser(); + } catch (InvalidRequestAttribute) { + $user = null; + } + + $np = $this->nowPlayingCache->getForAllStations(null === $user); $np = array_map( function (NowPlaying $npRow) use ($baseUrl) { diff --git a/src/Controller/Api/Stations/PlaylistsController.php b/src/Controller/Api/Stations/PlaylistsController.php index a8ea2bc16..2e23fa0f5 100644 --- a/src/Controller/Api/Stations/PlaylistsController.php +++ b/src/Controller/Api/Stations/PlaylistsController.php @@ -13,6 +13,7 @@ use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; use Carbon\CarbonInterface; +use Doctrine\ORM\AbstractQuery; use InvalidArgumentException; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -244,6 +245,7 @@ final class PlaylistsController extends AbstractScheduledEntityController $return = $this->toArray($record); + /** @var array{num_songs: int, total_length: string} $songTotals */ $songTotals = $this->em->createQuery( <<<'DQL' SELECT count(sm.id) AS num_songs, sum(sm.length) AS total_length @@ -252,12 +254,12 @@ final class PlaylistsController extends AbstractScheduledEntityController WHERE spm.playlist = :playlist DQL )->setParameter('playlist', $record) - ->getArrayResult(); + ->getSingleResult(AbstractQuery::HYDRATE_SCALAR); $return['short_name'] = StationPlaylist::generateShortName($return['name']); - $return['num_songs'] = (int)$songTotals[0]['num_songs']; - $return['total_length'] = (int)$songTotals[0]['total_length']; + $return['num_songs'] = $songTotals['num_songs']; + $return['total_length'] = round((float)$songTotals['total_length']); $isInternal = ('true' === $request->getParam('internal', 'false')); $router = $request->getRouter(); diff --git a/src/Controller/Api/Traits/CanSortResults.php b/src/Controller/Api/Traits/CanSortResults.php index f6b2363c5..e0e4b3637 100644 --- a/src/Controller/Api/Traits/CanSortResults.php +++ b/src/Controller/Api/Traits/CanSortResults.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Api\Traits; use App\Http\ServerRequest; +use App\Utilities\Types; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\QueryBuilder; use Symfony\Component\PropertyAccess\PropertyAccess; @@ -59,21 +60,25 @@ trait CanSortResults return $results; } + /** + * @return string[] + */ protected function getSortFromRequest( ServerRequest $request, string $defaultSortOrder = Criteria::ASC ): array { + $sortOrder = Types::stringOrNull($request->getParam('sortOrder'), true) ?? $defaultSortOrder; return [ $request->getParam('sort'), - ('desc' === strtolower($request->getParam('sortOrder', $defaultSortOrder))) + ('desc' === $sortOrder) ? Criteria::DESC : Criteria::ASC, ]; } protected static function sortByDotNotation( - mixed $a, - mixed $b, + object|array $a, + object|array $b, PropertyAccessorInterface $propertyAccessor, string $sortValue, string $sortOrder diff --git a/src/Controller/Frontend/IndexAction.php b/src/Controller/Frontend/IndexAction.php index 605f51cb8..38937b5ca 100644 --- a/src/Controller/Frontend/IndexAction.php +++ b/src/Controller/Frontend/IndexAction.php @@ -6,7 +6,7 @@ namespace App\Controller\Frontend; use App\Container\SettingsAwareTrait; use App\Controller\SingleActionInterface; -use App\Entity\User; +use App\Exception\InvalidRequestAttribute; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -27,9 +27,12 @@ final class IndexAction implements SingleActionInterface } // Redirect to login screen if the user isn't logged in. - $user = $request->getAttribute(ServerRequest::ATTR_USER); + try { + $request->getUser(); - if (!($user instanceof User)) { + // Redirect to dashboard if no other custom redirection rules exist. + return $response->withRedirect($request->getRouter()->named('dashboard')); + } catch (InvalidRequestAttribute) { // Redirect to a custom homepage URL if specified in settings. $homepageRedirect = $settings->getHomepageRedirectUrl(); if (null !== $homepageRedirect) { @@ -38,8 +41,5 @@ final class IndexAction implements SingleActionInterface return $response->withRedirect($request->getRouter()->named('account:login')); } - - // Redirect to dashboard if no other custom redirection rules exist. - return $response->withRedirect($request->getRouter()->named('dashboard')); } } diff --git a/src/Controller/Frontend/PublicPages/OnDemandAction.php b/src/Controller/Frontend/PublicPages/OnDemandAction.php index e52512f8c..5191211ea 100644 --- a/src/Controller/Frontend/PublicPages/OnDemandAction.php +++ b/src/Controller/Frontend/PublicPages/OnDemandAction.php @@ -35,6 +35,7 @@ final class OnDemandAction implements SingleActionInterface } // Get list of custom fields. + /** @var array $customFieldsRaw */ $customFieldsRaw = $this->em->createQuery( <<<'DQL' SELECT cf.id, cf.short_name, cf.name diff --git a/src/Customization.php b/src/Customization.php index 64382c387..5e50bfc9c 100644 --- a/src/Customization.php +++ b/src/Customization.php @@ -12,8 +12,8 @@ use App\Entity\Settings; use App\Entity\Station; use App\Enums\SupportedLocales; use App\Enums\SupportedThemes; +use App\Http\ServerRequest; use App\Traits\RequestAwareTrait; -use Psr\Http\Message\ServerRequestInterface; final class Customization { @@ -37,7 +37,7 @@ final class Customization $this->locale = SupportedLocales::default(); } - public function setRequest(?ServerRequestInterface $request): void + public function setRequest(?ServerRequest $request): void { $this->request = $request; diff --git a/src/Doctrine/Event/AuditLog.php b/src/Doctrine/Event/AuditLog.php index a25bfeb13..cfb7fba39 100644 --- a/src/Doctrine/Event/AuditLog.php +++ b/src/Doctrine/Event/AuditLog.php @@ -106,10 +106,10 @@ final class AuditLog implements EventSubscriber } // Check if either field value is an object. - if ($this->isEntity($em, $fieldPrev)) { + if (is_object($fieldPrev) && $this->isEntity($em, $fieldPrev)) { $fieldPrev = $this->getIdentifier($fieldPrev); } - if ($this->isEntity($em, $fieldNow)) { + if (is_object($fieldNow) && $this->isEntity($em, $fieldNow)) { $fieldNow = $this->getIdentifier($fieldNow); } diff --git a/src/Entity/CacheItem.php b/src/Entity/CacheItem.php index 61fec822c..4ad091ad5 100644 --- a/src/Entity/CacheItem.php +++ b/src/Entity/CacheItem.php @@ -7,20 +7,22 @@ namespace App\Entity; use Doctrine\ORM\Mapping as ORM; #[ - ORM\Entity, - ORM\Table(name: 'cache_items') + ORM\Entity(readOnly: true), + ORM\Table(name: 'cache_items'), ] class CacheItem { + /** @var resource $item_id */ #[ ORM\Column(type: 'binary', length: 255, nullable: false), ORM\Id, ORM\GeneratedValue(strategy: 'NONE') ] - protected string $item_id; + protected mixed $item_id; + /** @var resource $item_data */ #[ORM\Column(type: 'blob', length: 16777215, nullable: false)] - protected string $item_data; + protected mixed $item_data; #[ORM\Column(type: 'integer', nullable: true, options: ['unsigned' => true])] protected ?int $item_lifetime; diff --git a/src/Entity/ListenerLocation.php b/src/Entity/ListenerLocation.php index 4527095e9..e1944cd58 100644 --- a/src/Entity/ListenerLocation.php +++ b/src/Entity/ListenerLocation.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use App\Utilities\Types; use Doctrine\ORM\Mapping as ORM; use JsonSerializable; @@ -23,10 +24,10 @@ class ListenerLocation implements JsonSerializable protected ?string $country = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 6, nullable: true)] - protected ?float $lat = null; + protected ?string $lat = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 6, nullable: true)] - protected ?float $lon = null; + protected ?string $lon = null; public function getDescription(): string { @@ -50,12 +51,12 @@ class ListenerLocation implements JsonSerializable public function getLat(): ?float { - return $this->lat; + return Types::floatOrNull($this->lat); } public function getLon(): ?float { - return $this->lon; + return Types::floatOrNull($this->lon); } public function jsonSerialize(): array diff --git a/src/Entity/PodcastMedia.php b/src/Entity/PodcastMedia.php index 1cdc60022..76284cc5e 100644 --- a/src/Entity/PodcastMedia.php +++ b/src/Entity/PodcastMedia.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Entity; use App\Entity\Interfaces\IdentifiableEntityInterface; -use App\Entity\Traits; +use App\Utilities\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -32,7 +32,7 @@ class PodcastMedia implements IdentifiableEntityInterface protected string $original_name; #[ORM\Column(type: 'decimal', precision: 7, scale: 2)] - protected float $length = 0.00; + protected string $length = '0.0'; /** @var string The formatted podcast media's duration (in mm:ss format) */ #[ORM\Column(length: 10)] @@ -89,7 +89,7 @@ class PodcastMedia implements IdentifiableEntityInterface public function getLength(): float { - return $this->length; + return Types::float($this->length); } public function setLength(float $length): self @@ -97,7 +97,7 @@ class PodcastMedia implements IdentifiableEntityInterface $lengthMin = floor($length / 60); $lengthSec = (int)$length % 60; - $this->length = $length; + $this->length = (string)$length; $this->length_text = $lengthMin . ':' . str_pad((string)$lengthSec, 2, '0', STR_PAD_LEFT); return $this; diff --git a/src/Entity/Repository/SongHistoryRepository.php b/src/Entity/Repository/SongHistoryRepository.php index 1ed06807d..ab09e435e 100644 --- a/src/Entity/Repository/SongHistoryRepository.php +++ b/src/Entity/Repository/SongHistoryRepository.php @@ -137,7 +137,7 @@ final class SongHistoryRepository extends AbstractStationBasedRepository * @param int $start * @param int $end * - * @return mixed[] [int $minimumListeners, int $maximumListeners, float $averageListeners] + * @return array{int, int, float} */ public function getStatsByTimeRange(Station $station, int $start, int $end): array { diff --git a/src/Entity/Settings.php b/src/Entity/Settings.php index f47ce687a..12c7502c9 100644 --- a/src/Entity/Settings.php +++ b/src/Entity/Settings.php @@ -10,7 +10,7 @@ use App\Entity\Enums\IpSources; use App\Enums\SupportedThemes; use App\OpenApi; use App\Service\Avatar; -use App\Utilities\Strings; +use App\Utilities\Types; use App\Utilities\Urls; use Doctrine\ORM\Mapping as ORM; use OpenApi\Attributes as OA; @@ -352,13 +352,13 @@ class Settings implements Stringable public function getHomepageRedirectUrl(): ?string { - return Strings::nonEmptyOrNull($this->homepage_redirect_url); + return Types::stringOrNull($this->homepage_redirect_url); } public function setHomepageRedirectUrl(?string $homepageRedirectUrl): void { $this->homepage_redirect_url = $this->truncateNullableString( - Strings::nonEmptyOrNull($homepageRedirectUrl) + Types::stringOrNull($homepageRedirectUrl) ); } @@ -371,7 +371,7 @@ class Settings implements Stringable public function getDefaultAlbumArtUrl(): ?string { - return Strings::nonEmptyOrNull($this->default_album_art_url); + return Types::stringOrNull($this->default_album_art_url); } public function getDefaultAlbumArtUrlAsUri(): ?UriInterface @@ -386,7 +386,7 @@ class Settings implements Stringable public function setDefaultAlbumArtUrl(?string $defaultAlbumArtUrl): void { $this->default_album_art_url = $this->truncateNullableString( - Strings::nonEmptyOrNull($defaultAlbumArtUrl) + Types::stringOrNull($defaultAlbumArtUrl) ); } @@ -442,13 +442,13 @@ class Settings implements Stringable public function getLastFmApiKey(): ?string { - return Strings::nonEmptyOrNull($this->last_fm_api_key); + return Types::stringOrNull($this->last_fm_api_key, true); } public function setLastFmApiKey(?string $lastFmApiKey): void { $this->last_fm_api_key = $this->truncateNullableString( - Strings::nonEmptyOrNull($lastFmApiKey) + Types::stringOrNull($lastFmApiKey, true) ); } @@ -478,12 +478,12 @@ class Settings implements Stringable public function getPublicCustomCss(): ?string { - return Strings::nonEmptyOrNull($this->public_custom_css); + return Types::stringOrNull($this->public_custom_css, true); } public function setPublicCustomCss(?string $publicCustomCss): void { - $this->public_custom_css = Strings::nonEmptyOrNull($publicCustomCss); + $this->public_custom_css = Types::stringOrNull($publicCustomCss, true); } #[ @@ -495,12 +495,12 @@ class Settings implements Stringable public function getPublicCustomJs(): ?string { - return Strings::nonEmptyOrNull($this->public_custom_js); + return Types::stringOrNull($this->public_custom_js, true); } public function setPublicCustomJs(?string $publicCustomJs): void { - $this->public_custom_js = Strings::nonEmptyOrNull($publicCustomJs); + $this->public_custom_js = Types::stringOrNull($publicCustomJs, true); } #[ @@ -512,12 +512,12 @@ class Settings implements Stringable public function getInternalCustomCss(): ?string { - return Strings::nonEmptyOrNull($this->internal_custom_css); + return Types::stringOrNull($this->internal_custom_css, true); } public function setInternalCustomCss(?string $internalCustomCss): void { - $this->internal_custom_css = Strings::nonEmptyOrNull($internalCustomCss); + $this->internal_custom_css = Types::stringOrNull($internalCustomCss, true); } #[ @@ -549,12 +549,12 @@ class Settings implements Stringable public function getBackupTimeCode(): ?string { - return Strings::nonEmptyOrNull($this->backup_time_code); + return Types::stringOrNull($this->backup_time_code, true); } public function setBackupTimeCode(?string $backupTimeCode): void { - $this->backup_time_code = Strings::nonEmptyOrNull($backupTimeCode); + $this->backup_time_code = Types::stringOrNull($backupTimeCode, true); } #[ @@ -617,12 +617,12 @@ class Settings implements Stringable public function getBackupFormat(): ?string { - return Strings::nonEmptyOrNull($this->backup_format); + return Types::stringOrNull($this->backup_format, true); } public function setBackupFormat(?string $backupFormat): void { - $this->backup_format = Strings::nonEmptyOrNull($backupFormat); + $this->backup_format = Types::stringOrNull($backupFormat, true); } #[ @@ -1016,12 +1016,12 @@ class Settings implements Stringable public function getAcmeDomains(): ?string { - return Strings::nonEmptyOrNull($this->acme_domains); + return Types::stringOrNull($this->acme_domains, true); } public function setAcmeDomains(?string $acmeDomains): void { - $acmeDomains = Strings::nonEmptyOrNull($acmeDomains); + $acmeDomains = Types::stringOrNull($acmeDomains, true); if (null !== $acmeDomains) { $acmeDomains = implode( diff --git a/src/Entity/StationBackendConfiguration.php b/src/Entity/StationBackendConfiguration.php index 7bd06b9d7..58440a3f2 100644 --- a/src/Entity/StationBackendConfiguration.php +++ b/src/Entity/StationBackendConfiguration.php @@ -9,6 +9,7 @@ use App\Radio\Enums\AudioProcessingMethods; use App\Radio\Enums\CrossfadeModes; use App\Radio\Enums\MasterMePresets; use App\Radio\Enums\StreamFormats; +use App\Utilities\Types; use InvalidArgumentException; use LogicException; @@ -18,7 +19,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getCharset(): string { - return $this->get(self::CHARSET) ?? 'UTF-8'; + return Types::stringOrNull($this->get(self::CHARSET), true) ?? 'UTF-8'; } public function setCharset(?string $charset): void @@ -30,8 +31,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getDjPort(): ?int { - $port = $this->get(self::DJ_PORT); - return is_numeric($port) ? (int)$port : null; + return Types::intOrNull($this->get(self::DJ_PORT)); } public function setDjPort(?int $port): void @@ -43,8 +43,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getTelnetPort(): ?int { - $port = $this->get(self::TELNET_PORT); - return is_numeric($port) ? (int)$port : null; + return Types::intOrNull($this->get(self::TELNET_PORT)); } public function setTelnetPort(?int $port): void @@ -56,7 +55,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function recordStreams(): bool { - return (bool)($this->get(self::RECORD_STREAMS) ?? false); + return Types::boolOrNull($this->get(self::RECORD_STREAMS)) ?? false; } public function setRecordStreams(?bool $recordStreams): void @@ -73,7 +72,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getRecordStreamsFormatEnum(): StreamFormats { - return StreamFormats::tryFrom($this->get(self::RECORD_STREAMS_FORMAT) ?? '') + return StreamFormats::tryFrom( + Types::stringOrNull($this->get(self::RECORD_STREAMS_FORMAT)) ?? '' + ) ?? StreamFormats::Mp3; } @@ -94,7 +95,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getRecordStreamsBitrate(): int { - return (int)($this->get(self::RECORD_STREAMS_BITRATE) ?? 128); + return Types::intOrNull($this->get(self::RECORD_STREAMS_BITRATE)) ?? 128; } public function setRecordStreamsBitrate(?int $bitrate): void @@ -106,7 +107,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function useManualAutoDj(): bool { - return (bool)($this->get(self::USE_MANUAL_AUTODJ) ?? false); + return Types::boolOrNull($this->get(self::USE_MANUAL_AUTODJ)) ?? false; } public function setUseManualAutoDj(?bool $useManualAutoDj): void @@ -120,7 +121,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getAutoDjQueueLength(): int { - return (int)($this->get(self::AUTODJ_QUEUE_LENGTH) ?? self::DEFAULT_QUEUE_LENGTH); + return Types::intOrNull($this->get(self::AUTODJ_QUEUE_LENGTH)) ?? self::DEFAULT_QUEUE_LENGTH; } public function setAutoDjQueueLength(?int $queueLength): void @@ -132,7 +133,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getDjMountPoint(): string { - return $this->get(self::DJ_MOUNT_POINT) ?? '/'; + return Types::stringOrNull($this->get(self::DJ_MOUNT_POINT)) ?? '/'; } public function setDjMountPoint(?string $mountPoint): void @@ -146,7 +147,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getDjBuffer(): int { - return (int)$this->get(self::DJ_BUFFER, self::DEFAULT_DJ_BUFFER); + return Types::intOrNull($this->get(self::DJ_BUFFER)) ?? self::DEFAULT_DJ_BUFFER; } public function setDjBuffer(?int $buffer): void @@ -163,8 +164,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getAudioProcessingMethodEnum(): AudioProcessingMethods { - return AudioProcessingMethods::tryFrom($this->get(self::AUDIO_PROCESSING_METHOD) ?? '') - ?? AudioProcessingMethods::default(); + return AudioProcessingMethods::tryFrom( + Types::stringOrNull($this->get(self::AUDIO_PROCESSING_METHOD)) ?? '' + ) ?? AudioProcessingMethods::default(); } public function isAudioProcessingEnabled(): bool @@ -189,7 +191,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getPostProcessingIncludeLive(): bool { - return $this->get(self::POST_PROCESSING_INCLUDE_LIVE, false); + return Types::boolOrNull($this->get(self::POST_PROCESSING_INCLUDE_LIVE)) ?? false; } public function setPostProcessingIncludeLive(bool $postProcessingIncludeLive): void @@ -201,7 +203,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getStereoToolLicenseKey(): ?string { - return $this->get(self::STEREO_TOOL_LICENSE_KEY); + return Types::stringOrNull($this->get(self::STEREO_TOOL_LICENSE_KEY), true); } public function setStereoToolLicenseKey(?string $licenseKey): void @@ -213,7 +215,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getStereoToolConfigurationPath(): ?string { - return $this->get(self::STEREO_TOOL_CONFIGURATION_PATH); + return Types::stringOrNull($this->get(self::STEREO_TOOL_CONFIGURATION_PATH), true); } public function setStereoToolConfigurationPath(?string $stereoToolConfigurationPath): void @@ -225,12 +227,12 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getMasterMePreset(): ?string { - return $this->get(self::MASTER_ME_PRESET); + return Types::stringOrNull($this->get(self::MASTER_ME_PRESET), true); } public function getMasterMePresetEnum(): MasterMePresets { - return MasterMePresets::tryFrom($this->get(self::MASTER_ME_PRESET) ?? '') + return MasterMePresets::tryFrom($this->getMasterMePreset() ?? '') ?? MasterMePresets::default(); } @@ -253,7 +255,8 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getMasterMeLoudnessTarget(): float { - return (float)$this->get(self::MASTER_ME_LOUDNESS_TARGET, self::MASTER_ME_DEFAULT_LOUDNESS_TARGET); + return Types::floatOrNull($this->get(self::MASTER_ME_LOUDNESS_TARGET)) + ?? self::MASTER_ME_DEFAULT_LOUDNESS_TARGET; } public function setMasterMeLoudnessTarget(?float $masterMeLoudnessTarget): void @@ -265,7 +268,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function useReplayGain(): bool { - return $this->get(self::USE_REPLAYGAIN) ?? false; + return Types::boolOrNull($this->get(self::USE_REPLAYGAIN)) ?? false; } public function setUseReplayGain(?bool $useReplayGain): void @@ -277,8 +280,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getCrossfadeTypeEnum(): CrossfadeModes { - return CrossfadeModes::tryFrom($this->get(self::CROSSFADE_TYPE) ?? '') - ?? CrossfadeModes::default(); + return CrossfadeModes::tryFrom( + Types::stringOrNull($this->get(self::CROSSFADE_TYPE)) ?? '' + ) ?? CrossfadeModes::default(); } public function getCrossfadeType(): string @@ -297,7 +301,10 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getCrossfade(): float { - return round((float)($this->get(self::CROSSFADE) ?? self::DEFAULT_CROSSFADE_DURATION), 1); + return round( + Types::floatOrNull($this->get(self::CROSSFADE)) ?? self::DEFAULT_CROSSFADE_DURATION, + 1 + ); } public function setCrossfade(?float $crossfade): void @@ -328,9 +335,8 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getDuplicatePreventionTimeRange(): int { - return (int)( - $this->get(self::DUPLICATE_PREVENTION_TIME_RANGE) ?? self::DEFAULT_DUPLICATE_PREVENTION_TIME_RANGE - ); + return Types::intOrNull($this->get(self::DUPLICATE_PREVENTION_TIME_RANGE)) + ?? self::DEFAULT_DUPLICATE_PREVENTION_TIME_RANGE; } public function setDuplicatePreventionTimeRange(?int $duplicatePreventionTimeRange): void @@ -347,8 +353,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getPerformanceModeEnum(): StationBackendPerformanceModes { - return StationBackendPerformanceModes::tryFrom($this->get(self::PERFORMANCE_MODE) ?? '') - ?? StationBackendPerformanceModes::default(); + return StationBackendPerformanceModes::tryFrom( + Types::stringOrNull($this->get(self::PERFORMANCE_MODE)) ?? '' + ) ?? StationBackendPerformanceModes::default(); } public function setPerformanceMode(?string $performanceMode): void @@ -365,7 +372,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getHlsSegmentLength(): int { - return $this->get(self::HLS_SEGMENT_LENGTH, 4); + return Types::intOrNull($this->get(self::HLS_SEGMENT_LENGTH)) ?? 4; } public function setHlsSegmentLength(?int $length): void @@ -377,7 +384,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getHlsSegmentsInPlaylist(): int { - return $this->get(self::HLS_SEGMENTS_IN_PLAYLIST, 5); + return Types::intOrNull($this->get(self::HLS_SEGMENTS_IN_PLAYLIST)) ?? 5; } public function setHlsSegmentsInPlaylist(?int $value): void @@ -389,7 +396,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getHlsSegmentsOverhead(): int { - return $this->get(self::HLS_SEGMENTS_OVERHEAD, 2); + return Types::intOrNull($this->get(self::HLS_SEGMENTS_OVERHEAD)) ?? 2; } public function setHlsSegmentsOverhead(?int $value): void @@ -401,7 +408,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getHlsEnableOnPublicPlayer(): bool { - return $this->get(self::HLS_ENABLE_ON_PUBLIC_PLAYER, false); + return Types::boolOrNull($this->get(self::HLS_ENABLE_ON_PUBLIC_PLAYER)) ?? false; } public function setHlsEnableOnPublicPlayer(?bool $enable): void @@ -413,7 +420,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getHlsIsDefault(): bool { - return $this->get(self::HLS_IS_DEFAULT, false); + return Types::boolOrNull($this->get(self::HLS_IS_DEFAULT)) ?? false; } public function setHlsIsDefault(?bool $value): void @@ -425,11 +432,8 @@ class StationBackendConfiguration extends AbstractStationConfiguration public function getLiveBroadcastText(): string { - $text = $this->get(self::LIVE_BROADCAST_TEXT); - - return (!empty($text)) - ? $text - : 'Live Broadcast'; + return Types::stringOrNull($this->get(self::LIVE_BROADCAST_TEXT), true) + ?? 'Live Broadcast'; } public function setLiveBroadcastText(?string $text): void @@ -464,7 +468,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration throw new LogicException('Invalid custom configuration section.'); } - return $this->get($section); + return Types::stringOrNull($this->get($section), true); } public function setCustomConfigurationSection(string $section, ?string $value = null): void diff --git a/src/Entity/StationBrandingConfiguration.php b/src/Entity/StationBrandingConfiguration.php index 48b5b547a..4bd3cb230 100644 --- a/src/Entity/StationBrandingConfiguration.php +++ b/src/Entity/StationBrandingConfiguration.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use App\Utilities\Types; use App\Utilities\Urls; use Psr\Http\Message\UriInterface; @@ -13,7 +14,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration public function getDefaultAlbumArtUrl(): ?string { - return $this->get(self::DEFAULT_ALBUM_ART_URL); + return Types::stringOrNull($this->get(self::DEFAULT_ALBUM_ART_URL), true); } public function getDefaultAlbumArtUrlAsUri(): ?UriInterface @@ -34,7 +35,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration public function getPublicCustomCss(): ?string { - return $this->get(self::PUBLIC_CUSTOM_CSS); + return Types::stringOrNull($this->get(self::PUBLIC_CUSTOM_CSS), true); } public function setPublicCustomCss(?string $css): void @@ -46,7 +47,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration public function getPublicCustomJs(): ?string { - return $this->get(self::PUBLIC_CUSTOM_JS); + return Types::stringOrNull($this->get(self::PUBLIC_CUSTOM_JS), true); } public function setPublicCustomJs(?string $js): void @@ -58,11 +59,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration public function getOfflineText(): ?string { - $message = $this->get(self::OFFLINE_TEXT); - - return (!empty($message)) - ? $message - : null; + return Types::stringOrNull($this->get(self::OFFLINE_TEXT), true); } public function setOfflineText(?string $message): void diff --git a/src/Entity/StationFrontendConfiguration.php b/src/Entity/StationFrontendConfiguration.php index 2dd66d854..a531c40a9 100644 --- a/src/Entity/StationFrontendConfiguration.php +++ b/src/Entity/StationFrontendConfiguration.php @@ -5,6 +5,8 @@ declare(strict_types=1); namespace App\Entity; use App\Utilities\Strings; +use App\Utilities\Types; +use LogicException; class StationFrontendConfiguration extends AbstractStationConfiguration { @@ -31,7 +33,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getCustomConfiguration(): ?string { - return $this->get(self::CUSTOM_CONFIGURATION); + return Types::stringOrNull($this->get(self::CUSTOM_CONFIGURATION), true); } public function setCustomConfiguration(?string $config): void @@ -43,7 +45,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getSourcePassword(): string { - return $this->get(self::SOURCE_PASSWORD); + return Types::stringOrNull($this->get(self::SOURCE_PASSWORD), true) + ?? throw new LogicException('Password not generated'); } public function setSourcePassword(string $pw): void @@ -55,7 +58,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getAdminPassword(): string { - return $this->get(self::ADMIN_PASSWORD); + return Types::stringOrNull($this->get(self::ADMIN_PASSWORD), true) + ?? throw new LogicException('Password not generated'); } public function setAdminPassword(string $pw): void @@ -67,7 +71,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getRelayPassword(): string { - return $this->get(self::RELAY_PASSWORD); + return Types::stringOrNull($this->get(self::RELAY_PASSWORD), true) + ?? throw new LogicException('Password not generated'); } public function setRelayPassword(string $pw): void @@ -79,7 +84,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getStreamerPassword(): string { - return $this->get(self::STREAMER_PASSWORD); + return Types::stringOrNull($this->get(self::STREAMER_PASSWORD)) + ?? throw new LogicException('Password not generated'); } public function setStreamerPassword(string $pw): void @@ -91,8 +97,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getPort(): ?int { - $port = $this->get(self::PORT); - return is_numeric($port) ? (int)$port : null; + return Types::intOrNull($this->get(self::PORT)); } public function setPort(?int $port): void @@ -104,8 +109,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getMaxListeners(): ?int { - $listeners = $this->get(self::MAX_LISTENERS); - return is_numeric($listeners) ? (int)$listeners : null; + return Types::intOrNull($this->get(self::MAX_LISTENERS)); } public function setMaxListeners(?int $listeners): void @@ -117,7 +121,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getBannedIps(): ?string { - return $this->get(self::BANNED_IPS); + return Types::stringOrNull($this->get(self::BANNED_IPS), true); } public function setBannedIps(?string $ips): void @@ -129,7 +133,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getBannedUserAgents(): ?string { - return $this->get(self::BANNED_USER_AGENTS); + return Types::stringOrNull($this->get(self::BANNED_USER_AGENTS), true); } public function setBannedUserAgents(?string $userAgents): void @@ -141,7 +145,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getBannedCountries(): ?array { - return $this->get(self::BANNED_COUNTRIES); + return Types::arrayOrNull($this->get(self::BANNED_COUNTRIES)); } public function setBannedCountries(?array $countries): void @@ -153,7 +157,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getAllowedIps(): ?string { - return $this->get(self::ALLOWED_IPS); + return Types::stringOrNull($this->get(self::ALLOWED_IPS), true); } public function setAllowedIps(?string $ips): void @@ -165,7 +169,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getScLicenseId(): ?string { - return $this->get(self::SC_LICENSE_ID); + return Types::stringOrNull($this->get(self::SC_LICENSE_ID), true); } public function setScLicenseId(?string $licenseId): void @@ -177,7 +181,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration public function getScUserId(): ?string { - return $this->get(self::SC_USER_ID); + return Types::stringOrNull($this->get(self::SC_USER_ID), true); } public function setScUserId(?string $userId): void diff --git a/src/Entity/StationMedia.php b/src/Entity/StationMedia.php index e0d9f4498..b9af530e3 100644 --- a/src/Entity/StationMedia.php +++ b/src/Entity/StationMedia.php @@ -10,6 +10,7 @@ use App\Media\MetadataInterface; use App\Normalizer\Attributes\DeepNormalize; use App\OpenApi; use App\Utilities\Time; +use App\Utilities\Types; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -97,7 +98,7 @@ class StationMedia implements ), ORM\Column(type: 'decimal', precision: 7, scale: 2, nullable: true) ] - protected ?float $length = 0.00; + protected ?string $length = '0.00'; #[ OA\Property( @@ -133,7 +134,7 @@ class StationMedia implements ), ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ] - protected ?float $amplify = null; + protected ?string $amplify = null; #[ OA\Property( @@ -142,7 +143,7 @@ class StationMedia implements ), ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ] - protected ?float $fade_overlap = null; + protected ?string $fade_overlap = null; #[ OA\Property( @@ -151,7 +152,7 @@ class StationMedia implements ), ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ] - protected ?float $fade_in = null; + protected ?string $fade_in = null; #[ OA\Property( @@ -160,7 +161,7 @@ class StationMedia implements ), ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ] - protected ?float $fade_out = null; + protected ?string $fade_out = null; #[ OA\Property( @@ -169,7 +170,7 @@ class StationMedia implements ), ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ] - protected ?float $cue_in = null; + protected ?string $cue_in = null; #[ OA\Property( @@ -178,7 +179,7 @@ class StationMedia implements ), ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ] - protected ?float $cue_out = null; + protected ?string $cue_out = null; #[ OA\Property( @@ -294,18 +295,15 @@ class StationMedia implements public function getLength(): ?float { - return $this->length; + return Types::floatOrNull($this->length); } - /** - * @param int $length - */ - public function setLength(int $length): void + public function setLength(float $length): void { $lengthMin = floor($length / 60); $lengthSec = $length % 60; - $this->length = (float)$length; + $this->length = (string)$length; $this->length_text = $lengthMin . ':' . str_pad((string)$lengthSec, 2, '0', STR_PAD_LEFT); } @@ -341,62 +339,62 @@ class StationMedia implements public function getAmplify(): ?float { - return $this->amplify; + return Types::floatOrNull($this->amplify); } public function setAmplify(?float $amplify = null): void { - $this->amplify = $amplify; + $this->amplify = (string)$amplify; } public function getFadeOverlap(): ?float { - return $this->fade_overlap; + return Types::floatOrNull($this->fade_overlap); } public function setFadeOverlap(?float $fadeOverlap = null): void { - $this->fade_overlap = $fadeOverlap; + $this->fade_overlap = (string)$fadeOverlap; } public function getFadeIn(): ?float { - return $this->fade_in; + return Types::floatOrNull($this->fade_in); } public function setFadeIn(string|int|float $fadeIn = null): void { - $this->fade_in = Time::displayTimeToSeconds($fadeIn); + $this->fade_in = Types::stringOrNull(Time::displayTimeToSeconds($fadeIn)); } public function getFadeOut(): ?float { - return $this->fade_out; + return Types::floatOrNull($this->fade_out); } public function setFadeOut(string|int|float $fadeOut = null): void { - $this->fade_out = Time::displayTimeToSeconds($fadeOut); + $this->fade_out = Types::stringOrNull(Time::displayTimeToSeconds($fadeOut)); } public function getCueIn(): ?float { - return $this->cue_in; + return Types::floatOrNull($this->cue_in); } public function setCueIn(string|int|float $cueIn = null): void { - $this->cue_in = Time::displayTimeToSeconds($cueIn); + $this->cue_in = Types::stringOrNull(Time::displayTimeToSeconds($cueIn)); } public function getCueOut(): ?float { - return $this->cue_out; + return Types::floatOrNull($this->cue_out); } public function setCueOut(string|int|float $cueOut = null): void { - $this->cue_out = Time::displayTimeToSeconds($cueOut); + $this->cue_out = Types::stringOrNull(Time::displayTimeToSeconds($cueOut)); } /** @@ -404,17 +402,20 @@ class StationMedia implements */ public function getCalculatedLength(): int { - $length = (int)$this->length; + $length = $this->getLength() ?? 0.0; - if ((int)$this->cue_out > 0) { - $lengthRemoved = $length - (int)$this->cue_out; + $cueOut = $this->getCueOut(); + if ($cueOut > 0) { + $lengthRemoved = $length - $cueOut; $length -= $lengthRemoved; } - if ((int)$this->cue_in > 0) { - $length -= $this->cue_in; + + $cueIn = $this->getCueIn(); + if ($cueIn > 0) { + $length -= $cueIn; } - return (int)$length; + return (int)floor($length); } public function getArtUpdatedAt(): int @@ -474,27 +475,27 @@ class StationMedia implements public function fromMetadata(MetadataInterface $metadata): void { - $this->setLength((int)$metadata->getDuration()); + $this->setLength($metadata->getDuration()); $tags = $metadata->getTags(); if (isset($tags['title'])) { - $this->setTitle($tags['title']); + $this->setTitle(Types::stringOrNull($tags['title'])); } if (isset($tags['artist'])) { - $this->setArtist($tags['artist']); + $this->setArtist(Types::stringOrNull($tags['artist'])); } if (isset($tags['album'])) { - $this->setAlbum($tags['album']); + $this->setAlbum(Types::stringOrNull($tags['album'])); } if (isset($tags['genre'])) { - $this->setGenre($tags['genre']); + $this->setGenre(Types::stringOrNull($tags['genre'])); } if (isset($tags['unsynchronised_lyric'])) { - $this->setLyrics($tags['unsynchronised_lyric']); + $this->setLyrics(Types::stringOrNull($tags['unsynchronised_lyric'])); } if (isset($tags['isrc'])) { - $this->setIsrc($tags['isrc']); + $this->setIsrc(Types::stringOrNull($tags['isrc'])); } $this->updateSongId(); diff --git a/src/Entity/User.php b/src/Entity/User.php index 67c45a94e..b98a0cc89 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -127,7 +127,7 @@ class User implements Stringable, IdentifiableEntityInterface /** @var Collection */ #[ - ORM\OneToMany(mappedBy: 'user', targetEntity: ApiKey::class), + ORM\OneToMany(mappedBy: 'user', targetEntity: UserPasskey::class), Groups([EntityGroupsInterface::GROUP_ADMIN, EntityGroupsInterface::GROUP_ALL]), DeepNormalize(true) ] diff --git a/src/Enums/SupportedLocales.php b/src/Enums/SupportedLocales.php index 79e52352e..537bf03b2 100644 --- a/src/Enums/SupportedLocales.php +++ b/src/Enums/SupportedLocales.php @@ -5,11 +5,11 @@ declare(strict_types=1); namespace App\Enums; use App\Environment; +use App\Exception\InvalidRequestAttribute; use App\Http\ServerRequest; use Gettext\Translator; use Gettext\TranslatorFunctions; use Locale; -use Psr\Http\Message\ServerRequestInterface; enum SupportedLocales: string { @@ -126,14 +126,17 @@ enum SupportedLocales: string public static function createFromRequest( Environment $environment, - ServerRequestInterface $request + ServerRequest $request ): self { $possibleLocales = []; // Prefer user-based profile locale. - $user = $request->getAttribute(ServerRequest::ATTR_USER); - if (null !== $user && !empty($user->getLocale()) && 'default' !== $user->getLocale()) { - $possibleLocales[] = $user->getLocale(); + try { + $user = $request->getUser(); + if (!empty($user->getLocale()) && 'default' !== $user->getLocale()) { + $possibleLocales[] = $user->getLocale(); + } + } catch (InvalidRequestAttribute) { } $serverParams = $request->getServerParams(); diff --git a/src/Environment.php b/src/Environment.php index 02a5e0462..a2d034299 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -8,6 +8,7 @@ use App\Enums\ApplicationEnvironment; use App\Enums\ReleaseChannel; use App\Radio\Configuration; use App\Utilities\File; +use App\Utilities\Types; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\UriInterface; use Psr\Log\LogLevel; @@ -22,6 +23,7 @@ final class Environment private readonly bool $isDocker; private readonly ApplicationEnvironment $appEnv; + /** @var array */ private readonly array $data; // Core settings values @@ -71,47 +73,18 @@ final class Environment public const REDIS_PORT = 'REDIS_PORT'; public const REDIS_DB = 'REDIS_DB'; - // Default settings - private array $defaults = [ - self::APP_NAME => 'AzuraCast', - - self::LOG_LEVEL => LogLevel::NOTICE, - self::IS_DOCKER => true, - self::IS_CLI => ('cli' === PHP_SAPI), - - self::ASSET_URL => '/static', - - self::AUTO_ASSIGN_PORT_MIN => 8000, - self::AUTO_ASSIGN_PORT_MAX => 8499, - - self::ENABLE_REDIS => true, - - self::SYNC_SHORT_EXECUTION_TIME => 600, - self::SYNC_LONG_EXECUTION_TIME => 1800, - self::NOW_PLAYING_DELAY_TIME => 0, - self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES => 5, - - self::PROFILING_EXTENSION_ENABLED => 0, - self::PROFILING_EXTENSION_ALWAYS_ON => 0, - self::PROFILING_EXTENSION_HTTP_KEY => 'dev', - - self::ENABLE_WEB_UPDATER => false, - ]; - public function __construct(array $elements = []) { $this->baseDir = dirname(__DIR__); $this->parentDir = dirname($this->baseDir); $this->isDocker = file_exists($this->parentDir . '/.docker'); - $this->data = array_merge($this->defaults, $elements); - $this->appEnv = ApplicationEnvironment::tryFrom($this->data[self::APP_ENV] ?? '') - ?? ApplicationEnvironment::default(); + $this->data = $elements; + $this->appEnv = ApplicationEnvironment::tryFrom( + Types::string($this->data[self::APP_ENV] ?? null, '', true) + ) ?? ApplicationEnvironment::default(); } - /** - * @return mixed[] - */ public function toArray(): array { return $this->data; @@ -122,6 +95,27 @@ final class Environment return $this->appEnv; } + /** + * @return string The base directory of the application, i.e. `/var/app/www` for Docker installations. + */ + public function getBaseDirectory(): string + { + return $this->baseDir; + } + + /** + * @return string The parent directory the application is within, i.e. `/var/azuracast`. + */ + public function getParentDirectory(): string + { + return $this->parentDir; + } + + public function isDocker(): bool + { + return $this->isDocker; + } + public function isProduction(): bool { return ApplicationEnvironment::Production === $this->getAppEnvironmentEnum(); @@ -139,47 +133,37 @@ final class Environment public function showDetailedErrors(): bool { - if (self::envToBool($this->data[self::SHOW_DETAILED_ERRORS] ?? false)) { - return true; - } - - return !$this->isProduction(); - } - - public function isDocker(): bool - { - return $this->isDocker; + return Types::bool( + $this->data[self::SHOW_DETAILED_ERRORS] ?? null, + !$this->isProduction(), + true + ); } public function isCli(): bool { - return $this->data[self::IS_CLI] ?? ('cli' === PHP_SAPI); + return Types::bool( + $this->data[self::IS_CLI] ?? null, + ('cli' === PHP_SAPI) + ); } public function getAppName(): string { - return $this->data[self::APP_NAME] ?? 'Application'; + return Types::string( + $this->data[self::APP_NAME] ?? null, + 'AzuraCast', + true + ); } public function getAssetUrl(): ?string { - return $this->data[self::ASSET_URL] ?? ''; - } - - /** - * @return string The base directory of the application, i.e. `/var/app/www` for Docker installations. - */ - public function getBaseDirectory(): string - { - return $this->baseDir; - } - - /** - * @return string The parent directory the application is within, i.e. `/var/azuracast`. - */ - public function getParentDirectory(): string - { - return $this->parentDir; + return Types::string( + $this->data[self::ASSET_URL] ?? null, + '/static', + true + ); } /** @@ -187,8 +171,11 @@ final class Environment */ public function getTempDirectory(): string { - return $this->data[self::TEMP_DIR] - ?? $this->getParentDirectory() . '/www_tmp'; + return Types::string( + $this->data[self::TEMP_DIR] ?? null, + $this->getParentDirectory() . '/www_tmp', + true + ); } /** @@ -196,10 +183,14 @@ final class Environment */ public function getUploadsDirectory(): string { - return $this->data[self::UPLOADS_DIR] ?? File::getFirstExistingDirectory([ - $this->getParentDirectory() . '/storage/uploads', - $this->getParentDirectory() . '/uploads', - ]); + return Types::string( + $this->data[self::UPLOADS_DIR] ?? null, + File::getFirstExistingDirectory([ + $this->getParentDirectory() . '/storage/uploads', + $this->getParentDirectory() . '/uploads', + ]), + true + ); } /** @@ -222,56 +213,65 @@ final class Environment public function getLang(): ?string { - return $this->data[self::LANG]; + return Types::stringOrNull($this->data[self::LANG]); } public function getReleaseChannelEnum(): ReleaseChannel { - return ReleaseChannel::tryFrom($this->data[self::RELEASE_CHANNEL] ?? '') + return ReleaseChannel::tryFrom(Types::string($this->data[self::RELEASE_CHANNEL] ?? null)) ?? ReleaseChannel::default(); } public function getSftpPort(): int { - return (int)($this->data[self::SFTP_PORT] ?? 2022); + return Types::int( + $this->data[self::SFTP_PORT] ?? null, + 2022 + ); } public function getAutoAssignPortMin(): int { - return (int)($this->data[self::AUTO_ASSIGN_PORT_MIN] ?? Configuration::DEFAULT_PORT_MIN); + return Types::int( + $this->data[self::AUTO_ASSIGN_PORT_MIN] ?? null, + Configuration::DEFAULT_PORT_MIN + ); } public function getAutoAssignPortMax(): int { - return (int)($this->data[self::AUTO_ASSIGN_PORT_MAX] ?? Configuration::DEFAULT_PORT_MAX); + return Types::int( + $this->data[self::AUTO_ASSIGN_PORT_MAX] ?? null, + Configuration::DEFAULT_PORT_MAX + ); } public function getSyncShortExecutionTime(): int { - return (int)( - $this->data[self::SYNC_SHORT_EXECUTION_TIME] ?? $this->defaults[self::SYNC_SHORT_EXECUTION_TIME] + return Types::int( + $this->data[self::SYNC_SHORT_EXECUTION_TIME] ?? null, + 600 ); } public function getSyncLongExecutionTime(): int { - return (int)( - $this->data[self::SYNC_LONG_EXECUTION_TIME] ?? $this->defaults[self::SYNC_LONG_EXECUTION_TIME] + return Types::int( + $this->data[self::SYNC_LONG_EXECUTION_TIME] ?? null, + 1800 ); } public function getNowPlayingDelayTime(): int { - return (int)( - $this->data[self::NOW_PLAYING_DELAY_TIME] ?? $this->defaults[self::NOW_PLAYING_DELAY_TIME] - ); + return Types::int($this->data[self::NOW_PLAYING_DELAY_TIME] ?? null); } public function getNowPlayingMaxConcurrentProcesses(): int { - return (int)( - $this->data[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES] - ?? $this->defaults[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES] + return Types::int( + $this->data[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES] ?? null, + 5 ); } @@ -280,9 +280,9 @@ final class Environment */ public function getLogLevel(): string { - if (!empty($this->data[self::LOG_LEVEL])) { - $loggingLevel = strtolower($this->data[self::LOG_LEVEL]); - + $logLevelRaw = Types::stringOrNull($this->data[self::LOG_LEVEL] ?? null, true); + if (null !== $logLevelRaw) { + $loggingLevel = strtolower($logLevelRaw); $allowedLogLevels = [ LogLevel::DEBUG, LogLevel::INFO, @@ -305,16 +305,42 @@ final class Environment } /** - * @return mixed[] + * @return array{ + * host: string, + * port: int, + * dbname: string, + * user: string, + * password: string, + * unix_socket?: string + * } */ public function getDatabaseSettings(): array { $dbSettings = [ - 'host' => $this->data[self::DB_HOST] ?? 'localhost', - 'port' => (int)($this->data[self::DB_PORT] ?? 3306), - 'dbname' => $this->data[self::DB_NAME] ?? 'azuracast', - 'user' => $this->data[self::DB_USER] ?? 'azuracast', - 'password' => $this->data[self::DB_PASSWORD] ?? 'azur4c457', + 'host' => Types::string( + $this->data[self::DB_HOST] ?? null, + 'localhost', + true + ), + 'port' => Types::int( + $this->data[self::DB_PORT] ?? null, + 3306 + ), + 'dbname' => Types::string( + $this->data[self::DB_NAME] ?? null, + 'azuracast', + true + ), + 'user' => Types::string( + $this->data[self::DB_USER] ?? null, + 'azuracast', + true + ), + 'password' => Types::string( + $this->data[self::DB_PASSWORD] ?? null, + 'azur4c457', + true + ), ]; if ('localhost' === $dbSettings['host'] && $this->isDocker()) { @@ -326,23 +352,42 @@ final class Environment public function useLocalDatabase(): bool { - return 'localhost' === ($this->data[self::DB_HOST] ?? 'localhost'); + return 'localhost' === $this->getDatabaseSettings()['host']; } public function enableRedis(): bool { - return self::envToBool($this->data[self::ENABLE_REDIS] ?? true); + return Types::bool( + $this->data[self::ENABLE_REDIS], + true, + true + ); } /** - * @return mixed[] + * @return array{ + * host: string, + * port: int, + * db: int, + * socket?: string + * } */ public function getRedisSettings(): array { $redisSettings = [ - 'host' => $this->data[self::REDIS_HOST] ?? 'localhost', - 'port' => (int)($this->data[self::REDIS_PORT] ?? 6379), - 'db' => (int)($this->data[self::REDIS_DB] ?? 1), + 'host' => Types::string( + $this->data[self::REDIS_HOST] ?? null, + 'localhost', + true + ), + 'port' => Types::int( + $this->data[self::REDIS_PORT] ?? null, + 6379 + ), + 'db' => Types::int( + $this->data[self::REDIS_DB] ?? null, + 1 + ), ]; if ('localhost' === $redisSettings['host'] && $this->isDocker()) { @@ -354,27 +399,47 @@ final class Environment public function useLocalRedis(): bool { - return $this->enableRedis() && 'localhost' === ($this->data[self::REDIS_HOST] ?? 'localhost'); + return $this->enableRedis() && 'localhost' === $this->getRedisSettings()['host']; } public function isProfilingExtensionEnabled(): bool { - return self::envToBool($this->data[self::PROFILING_EXTENSION_ENABLED] ?? false); + return Types::bool( + $this->data[self::PROFILING_EXTENSION_ENABLED] ?? null, + false, + true + ); } public function isProfilingExtensionAlwaysOn(): bool { - return self::envToBool($this->data[self::PROFILING_EXTENSION_ALWAYS_ON] ?? false); + return Types::bool( + $this->data[self::PROFILING_EXTENSION_ALWAYS_ON] ?? null, + false, + true + ); } public function getProfilingExtensionHttpKey(): string { - return $this->data[self::PROFILING_EXTENSION_HTTP_KEY] ?? 'dev'; + return Types::string( + $this->data[self::PROFILING_EXTENSION_HTTP_KEY] ?? null, + 'dev', + true + ); } public function enableWebUpdater(): bool { - return $this->isDocker() && self::envToBool($this->data[self::ENABLE_WEB_UPDATER] ?? false); + if (!$this->isDocker()) { + return false; + } + + return Types::bool( + $this->data[self::ENABLE_WEB_UPDATER] ?? null, + false, + true + ); } public static function getDefaultsForEnvironment(Environment $existingEnv): self @@ -385,24 +450,6 @@ final class Environment ]); } - public static function envToBool(mixed $value): bool - { - if (is_bool($value)) { - return $value; - } - if (is_int($value)) { - return 0 !== $value; - } - if (null === $value) { - return false; - } - - $value = (string)$value; - return str_starts_with(strtolower($value), 'y') - || 'true' === strtolower($value) - || '1' === $value; - } - public static function getInstance(): Environment { return self::$instance; diff --git a/src/Http/ErrorHandler.php b/src/Http/ErrorHandler.php index 4c7eb5315..9c6b03b7f 100644 --- a/src/Http/ErrorHandler.php +++ b/src/Http/ErrorHandler.php @@ -134,6 +134,10 @@ final class ErrorHandler extends SlimErrorHandler return $response; } + if (!($this->request instanceof ServerRequest)) { + return parent::respond(); + } + if ($this->exception instanceof HttpException) { /** @var Response $response */ $response = $this->responseFactory->createResponse($this->exception->getCode()); diff --git a/src/Http/Router.php b/src/Http/Router.php index 7c0f54d1f..eb4de0dae 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -29,7 +29,7 @@ final class Router implements RouterInterface ) { } - public function setRequest(?ServerRequestInterface $request): void + public function setRequest(?ServerRequest $request): void { $this->request = $request; $this->baseUrl = null; diff --git a/src/Http/RouterInterface.php b/src/Http/RouterInterface.php index c731f2247..7cfa2d901 100644 --- a/src/Http/RouterInterface.php +++ b/src/Http/RouterInterface.php @@ -4,14 +4,13 @@ declare(strict_types=1); namespace App\Http; -use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; interface RouterInterface { - public function setRequest(?ServerRequestInterface $request): void; + public function setRequest(?ServerRequest $request): void; - public function withRequest(?ServerRequestInterface $request): self; + public function withRequest(?ServerRequest $request): self; public function getBaseUrl(): UriInterface; diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index 800bc5aa0..f8f19e838 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -129,12 +129,15 @@ final class ServerRequest extends SlimServerRequest } /** + * @template T of object + * * @param string $attr - * @param string $className + * @param class-string $className + * @return T * * @throws InvalidRequestAttribute */ - private function getAttributeOfClass(string $attr, string $className): mixed + private function getAttributeOfClass(string $attr, string $className): object { $object = $this->serverRequest->getAttribute($attr); diff --git a/src/Installer/Command/InstallCommand.php b/src/Installer/Command/InstallCommand.php index f9bbbd097..dcc57e8c3 100644 --- a/src/Installer/Command/InstallCommand.php +++ b/src/Installer/Command/InstallCommand.php @@ -12,6 +12,7 @@ use App\Installer\EnvFiles\AzuraCastEnvFile; use App\Installer\EnvFiles\EnvFile; use App\Radio\Configuration; use App\Utilities\Strings; +use App\Utilities\Types; use InvalidArgumentException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -45,12 +46,12 @@ final class InstallCommand extends Command { $io = new SymfonyStyle($input, $output); - $baseDir = $input->getArgument('base-dir') ?? self::DEFAULT_BASE_DIRECTORY; - $update = (bool)$input->getOption('update'); - $defaults = (bool)$input->getOption('defaults'); - $httpPort = $input->getOption('http-port'); - $httpsPort = $input->getOption('https-port'); - $releaseChannel = $input->getOption('release-channel'); + $baseDir = Types::string($input->getArgument('base-dir'), self::DEFAULT_BASE_DIRECTORY); + $update = Types::bool($input->getOption('update')); + $defaults = Types::bool($input->getOption('defaults')); + $httpPort = Types::intOrNull($input->getOption('http-port')); + $httpsPort = Types::intOrNull($input->getOption('https-port')); + $releaseChannel = Types::stringOrNull($input->getOption('release-channel')); $devMode = ($baseDir !== self::DEFAULT_BASE_DIRECTORY); @@ -222,7 +223,11 @@ final class InstallCommand extends Command foreach ($simplePorts as $port) { $env[$port] = (int)$io->ask( - $envConfig[$port]['name'] . ' - ' . $envConfig[$port]['description'], + sprintf( + '%s - %s', + $envConfig[$port]['name'], + $envConfig[$port]['description'] ?? '' + ), (string)$env[$port] ); } @@ -301,7 +306,7 @@ final class InstallCommand extends Command $ports = $env['AZURACAST_STATION_PORTS'] ?? ''; $envConfig = $env::getConfiguration($this->environment); - $defaultPorts = $envConfig['AZURACAST_STATION_PORTS']['default']; + $defaultPorts = $envConfig['AZURACAST_STATION_PORTS']['default'] ?? ''; if (!empty($ports) && 0 !== strcmp($ports, $defaultPorts)) { $yamlPorts = []; diff --git a/src/Installer/EnvFiles/AbstractEnvFile.php b/src/Installer/EnvFiles/AbstractEnvFile.php index 91356cdc5..8671ef549 100644 --- a/src/Installer/EnvFiles/AbstractEnvFile.php +++ b/src/Installer/EnvFiles/AbstractEnvFile.php @@ -6,6 +6,7 @@ namespace App\Installer\EnvFiles; use App\Environment; use App\Utilities\Strings; +use App\Utilities\Types; use ArrayAccess; use DateTimeImmutable; use DateTimeZone; @@ -58,10 +59,7 @@ abstract class AbstractEnvFile implements ArrayAccess public function getAsBool(string $key, bool $default): bool { - if (isset($this->data[$key])) { - return Environment::envToBool($this->data[$key]); - } - return $default; + return Types::bool($this->data[$key], $default, true); } public function offsetExists(mixed $offset): bool @@ -96,7 +94,7 @@ abstract class AbstractEnvFile implements ArrayAccess ]; foreach (static::getConfiguration($environment) as $key => $keyInfo) { - $envFile[] = '# ' . ($keyInfo['name'] ?? $key); + $envFile[] = sprintf('# %s', $keyInfo['name']); if (!empty($keyInfo['description'])) { $desc = Strings::mbWordwrap($keyInfo['description']); @@ -191,7 +189,7 @@ abstract class AbstractEnvFile implements ArrayAccess } /** - * @return mixed[] + * @return array */ abstract public static function getConfiguration(Environment $environment): array; diff --git a/src/Middleware/AbstractMiddleware.php b/src/Middleware/AbstractMiddleware.php new file mode 100644 index 000000000..87dbf44d9 --- /dev/null +++ b/src/Middleware/AbstractMiddleware.php @@ -0,0 +1,26 @@ +__invoke($request, $handler); + } + + abstract public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface; +} diff --git a/src/Middleware/ApplyXForwardedProto.php b/src/Middleware/ApplyXForwardedProto.php index 2f0b2e2dd..3bbc77660 100644 --- a/src/Middleware/ApplyXForwardedProto.php +++ b/src/Middleware/ApplyXForwardedProto.php @@ -4,21 +4,16 @@ declare(strict_types=1); namespace App\Middleware; +use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; /** * Apply the "X-Forwarded-Proto" header if it exists. */ -final class ApplyXForwardedProto implements MiddlewareInterface +final class ApplyXForwardedProto extends AbstractMiddleware { - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { if ($request->hasHeader('X-Forwarded-Proto')) { $uri = $request->getUri(); diff --git a/src/Middleware/Auth/AbstractAuth.php b/src/Middleware/Auth/AbstractAuth.php index 874494266..ffb0be05c 100644 --- a/src/Middleware/Auth/AbstractAuth.php +++ b/src/Middleware/Auth/AbstractAuth.php @@ -9,13 +9,13 @@ use App\Container\EnvironmentAwareTrait; use App\Customization; use App\Entity\AuditLog; use App\Entity\Repository\UserRepository; +use App\Exception\InvalidRequestAttribute; use App\Http\ServerRequest; +use App\Middleware\AbstractMiddleware; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -abstract class AbstractAuth implements MiddlewareInterface +abstract class AbstractAuth extends AbstractMiddleware { use EnvironmentAwareTrait; @@ -26,7 +26,7 @@ abstract class AbstractAuth implements MiddlewareInterface ) { } - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $customization = $this->customization->withRequest($request); @@ -39,7 +39,12 @@ abstract class AbstractAuth implements MiddlewareInterface ->withAttribute(ServerRequest::ATTR_ACL, $acl); // Set the Audit Log user. - AuditLog::setCurrentUser($request->getAttribute(ServerRequest::ATTR_USER)); + try { + $currentUser = $request->getUser(); + } catch (InvalidRequestAttribute) { + $currentUser = null; + } + AuditLog::setCurrentUser($currentUser); $response = $handler->handle($request); diff --git a/src/Middleware/Auth/ApiAuth.php b/src/Middleware/Auth/ApiAuth.php index 7421261d0..adf97f663 100644 --- a/src/Middleware/Auth/ApiAuth.php +++ b/src/Middleware/Auth/ApiAuth.php @@ -13,7 +13,6 @@ use App\Entity\User; use App\Exception\CsrfValidationException; use App\Http\ServerRequest; use App\Security\SplitToken; -use App\Session\Csrf; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -31,17 +30,17 @@ final class ApiAuth extends AbstractAuth parent::__construct($userRepo, $acl, $customization); } - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { // Initialize the Auth for this request. $user = $this->getApiUser($request); $request = $request->withAttribute(ServerRequest::ATTR_USER, $user); - return parent::process($request, $handler); + return parent::__invoke($request, $handler); } - private function getApiUser(ServerRequestInterface $request): ?User + private function getApiUser(ServerRequest $request): ?User { $apiKey = $this->getApiKey($request); @@ -55,7 +54,7 @@ final class ApiAuth extends AbstractAuth // Fallback to session login if available. $auth = new Auth( userRepo: $this->userRepo, - session: $request->getAttribute(ServerRequest::ATTR_SESSION), + session: $request->getSession(), ); $auth->setEnvironment($this->environment); @@ -70,14 +69,11 @@ final class ApiAuth extends AbstractAuth return null; } - $csrf = $request->getAttribute(ServerRequest::ATTR_SESSION_CSRF); - - if ($csrf instanceof Csrf) { - try { - $csrf->verify($csrfKey, self::API_CSRF_NAMESPACE); - return $user; - } catch (CsrfValidationException) { - } + $csrf = $request->getCsrf(); + try { + $csrf->verify($csrfKey, self::API_CSRF_NAMESPACE); + return $user; + } catch (CsrfValidationException) { } } diff --git a/src/Middleware/Auth/StandardAuth.php b/src/Middleware/Auth/StandardAuth.php index 2a794a7eb..9570f76c1 100644 --- a/src/Middleware/Auth/StandardAuth.php +++ b/src/Middleware/Auth/StandardAuth.php @@ -7,17 +7,16 @@ namespace App\Middleware\Auth; use App\Auth; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; final class StandardAuth extends AbstractAuth { - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { // Initialize the Auth for this request. $auth = new Auth( userRepo: $this->userRepo, - session: $request->getAttribute(ServerRequest::ATTR_SESSION), + session: $request->getSession(), ); $auth->setEnvironment($this->environment); @@ -27,6 +26,6 @@ final class StandardAuth extends AbstractAuth ->withAttribute(ServerRequest::ATTR_AUTH, $auth) ->withAttribute(ServerRequest::ATTR_USER, $user); - return parent::process($request, $handler); + return parent::__invoke($request, $handler); } } diff --git a/src/Middleware/EnableView.php b/src/Middleware/EnableView.php index 2d9babf8f..da468d6df 100644 --- a/src/Middleware/EnableView.php +++ b/src/Middleware/EnableView.php @@ -7,21 +7,19 @@ namespace App\Middleware; use App\Http\ServerRequest; use App\View; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; /** * Inject the view object into the request and prepare it for rendering templates. */ -final class EnableView implements MiddlewareInterface +final class EnableView extends AbstractMiddleware { public function __construct( private readonly View $view ) { } - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $view = $this->view->withRequest($request); diff --git a/src/Middleware/EnforceSecurity.php b/src/Middleware/EnforceSecurity.php index a3604e985..326b1ff1c 100644 --- a/src/Middleware/EnforceSecurity.php +++ b/src/Middleware/EnforceSecurity.php @@ -5,17 +5,16 @@ declare(strict_types=1); namespace App\Middleware; use App\Container\SettingsAwareTrait; +use App\Http\ServerRequest; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\App; /** * Remove trailing slash from all URLs when routing. */ -final class EnforceSecurity implements MiddlewareInterface +final class EnforceSecurity extends AbstractMiddleware { use SettingsAwareTrait; @@ -27,11 +26,7 @@ final class EnforceSecurity implements MiddlewareInterface $this->responseFactory = $app->getResponseFactory(); } - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $alwaysUseSsl = $this->readSettings()->getAlwaysUseSsl(); diff --git a/src/Middleware/GetCurrentUser.php b/src/Middleware/GetCurrentUser.php index 769244c0c..d77e809da 100644 --- a/src/Middleware/GetCurrentUser.php +++ b/src/Middleware/GetCurrentUser.php @@ -12,14 +12,12 @@ use App\Entity\AuditLog; use App\Entity\Repository\UserRepository; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; /** * Get the current user entity object and assign it into the request if it exists. */ -final class GetCurrentUser implements MiddlewareInterface +final class GetCurrentUser extends AbstractMiddleware { use EnvironmentAwareTrait; @@ -30,12 +28,12 @@ final class GetCurrentUser implements MiddlewareInterface ) { } - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { // Initialize the Auth for this request. $auth = new Auth( userRepo: $this->userRepo, - session: $request->getAttribute(ServerRequest::ATTR_SESSION), + session: $request->getSession(), ); $auth->setEnvironment($this->environment); @@ -43,8 +41,7 @@ final class GetCurrentUser implements MiddlewareInterface $request = $request ->withAttribute(ServerRequest::ATTR_AUTH, $auth) - ->withAttribute(ServerRequest::ATTR_USER, $user) - ->withAttribute('is_logged_in', (null !== $user)); + ->withAttribute(ServerRequest::ATTR_USER, $user); // Initialize Customization (timezones, locales, etc) based on the current logged in user. $customization = $this->customization->withRequest($request); diff --git a/src/Middleware/GetStation.php b/src/Middleware/GetStation.php index 23bab197d..fab6290dc 100644 --- a/src/Middleware/GetStation.php +++ b/src/Middleware/GetStation.php @@ -8,22 +8,20 @@ use App\Entity\Repository\StationRepository; use App\Entity\Station; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Routing\RouteContext; /** * Retrieve the station specified in the request parameters, and throw an error if none exists but one is required. */ -final class GetStation implements MiddlewareInterface +final class GetStation extends AbstractMiddleware { public function __construct( private readonly StationRepository $stationRepo ) { } - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $routeArgs = RouteContext::fromRequest($request)->getRoute()?->getArguments(); diff --git a/src/Middleware/HandleMultipartJson.php b/src/Middleware/HandleMultipartJson.php index 4d40639c5..579383b44 100644 --- a/src/Middleware/HandleMultipartJson.php +++ b/src/Middleware/HandleMultipartJson.php @@ -4,10 +4,9 @@ declare(strict_types=1); namespace App\Middleware; +use App\Http\ServerRequest; use JsonException; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use const JSON_THROW_ON_ERROR; @@ -21,13 +20,9 @@ use const JSON_THROW_ON_ERROR; * attribute of the PSR-7 request. This implementation is transparent to any controllers * using this code. */ -final class HandleMultipartJson implements MiddlewareInterface +final class HandleMultipartJson extends AbstractMiddleware { - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $parsedBody = $request->getParsedBody(); diff --git a/src/Middleware/InjectRateLimit.php b/src/Middleware/InjectRateLimit.php index 454cb47b8..1b7aa9d4f 100644 --- a/src/Middleware/InjectRateLimit.php +++ b/src/Middleware/InjectRateLimit.php @@ -7,25 +7,19 @@ namespace App\Middleware; use App\Http\ServerRequest; use App\RateLimit; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; /** * Inject core services into the request object for use further down the stack. */ -final class InjectRateLimit implements MiddlewareInterface +final class InjectRateLimit extends AbstractMiddleware { public function __construct( private readonly RateLimit $rateLimit ) { } - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $request = $request->withAttribute(ServerRequest::ATTR_RATE_LIMIT, $this->rateLimit); diff --git a/src/Middleware/InjectRouter.php b/src/Middleware/InjectRouter.php index 8b15aefe2..a3c453ab0 100644 --- a/src/Middleware/InjectRouter.php +++ b/src/Middleware/InjectRouter.php @@ -7,25 +7,19 @@ namespace App\Middleware; use App\Http\RouterInterface; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; /** * Set the current route on the URL object, and inject the URL object into the router. */ -final class InjectRouter implements MiddlewareInterface +final class InjectRouter extends AbstractMiddleware { public function __construct( private readonly RouterInterface $router ) { } - /** - * @param ServerRequestInterface $request - * @param RequestHandlerInterface $handler - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $router = $this->router->withRequest($request); diff --git a/src/Middleware/InjectSession.php b/src/Middleware/InjectSession.php index df65142c5..6aa9f0a5b 100644 --- a/src/Middleware/InjectSession.php +++ b/src/Middleware/InjectSession.php @@ -15,8 +15,6 @@ use Mezzio\Session\LazySession; use Mezzio\Session\SessionPersistenceInterface; use Psr\Cache\CacheItemPoolInterface; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter; @@ -24,7 +22,7 @@ use Symfony\Component\Cache\Adapter\ProxyAdapter; /** * Inject the session object into the request. */ -final class InjectSession implements MiddlewareInterface +final class InjectSession extends AbstractMiddleware { use SettingsAwareTrait; @@ -41,7 +39,7 @@ final class InjectSession implements MiddlewareInterface $this->cachePool = new ProxyAdapter($dbCache, 'session.'); } - public function getSessionPersistence(ServerRequestInterface $request): SessionPersistenceInterface + public function getSessionPersistence(ServerRequest $request): SessionPersistenceInterface { $alwaysUseSsl = $this->readSettings()->getAlwaysUseSsl(); $isHttpsUrl = ('https' === $request->getUri()->getScheme()); @@ -59,7 +57,7 @@ final class InjectSession implements MiddlewareInterface ); } - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $sessionPersistence = $this->getSessionPersistence($request); $session = new LazySession($sessionPersistence, $request); diff --git a/src/Middleware/Module/Api.php b/src/Middleware/Module/Api.php index db419631c..e927e3b14 100644 --- a/src/Middleware/Module/Api.php +++ b/src/Middleware/Module/Api.php @@ -7,8 +7,10 @@ namespace App\Middleware\Module; use App\Container\EnvironmentAwareTrait; use App\Container\SettingsAwareTrait; use App\Entity\User; +use App\Exception\InvalidRequestAttribute; use App\Http\Response; use App\Http\ServerRequest; +use App\Middleware\AbstractMiddleware; use App\Utilities\Urls; use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -20,7 +22,7 @@ use Symfony\Component\VarDumper\VarDumper; /** * Handle API calls and wrap exceptions in JSON formatting. */ -final class Api +final class Api extends AbstractMiddleware { use EnvironmentAwareTrait; use SettingsAwareTrait; @@ -40,7 +42,11 @@ final class Api } // Attempt API key auth if a key exists. - $apiUser = $request->getAttribute(ServerRequest::ATTR_USER); + try { + $apiUser = $request->getUser(); + } catch (InvalidRequestAttribute) { + $apiUser = null; + } // Set default cache control for API pages. $settings = $this->readSettings(); diff --git a/src/Middleware/Module/PanelLayout.php b/src/Middleware/Module/PanelLayout.php index 16b1bba69..f3c367b25 100644 --- a/src/Middleware/Module/PanelLayout.php +++ b/src/Middleware/Module/PanelLayout.php @@ -7,6 +7,7 @@ namespace App\Middleware\Module; use App\Container\EnvironmentAwareTrait; use App\Enums\GlobalPermissions; use App\Http\ServerRequest; +use App\Middleware\AbstractMiddleware; use App\Middleware\Auth\ApiAuth; use App\Version; use Psr\Http\Message\ResponseInterface; @@ -15,7 +16,7 @@ use Psr\Http\Server\RequestHandlerInterface; use const PHP_MAJOR_VERSION; use const PHP_MINOR_VERSION; -final class PanelLayout +final class PanelLayout extends AbstractMiddleware { use EnvironmentAwareTrait; diff --git a/src/Middleware/Permissions.php b/src/Middleware/Permissions.php index 816f4e758..5fa27c852 100644 --- a/src/Middleware/Permissions.php +++ b/src/Middleware/Permissions.php @@ -14,7 +14,7 @@ use Psr\Http\Server\RequestHandlerInterface; /** * Get the current user entity object and assign it into the request if it exists. */ -final class Permissions +final class Permissions extends AbstractMiddleware { public function __construct( private readonly string|PermissionInterface $action, diff --git a/src/Middleware/RateLimit.php b/src/Middleware/RateLimit.php index 76be311bb..f781aba8c 100644 --- a/src/Middleware/RateLimit.php +++ b/src/Middleware/RateLimit.php @@ -11,7 +11,7 @@ use Psr\Http\Server\RequestHandlerInterface; /** * Apply a rate limit for requests on this page and throw an exception if the limit is exceeded. */ -final class RateLimit +final class RateLimit extends AbstractMiddleware { public function __construct( private readonly string $rlGroup = 'default', diff --git a/src/Middleware/RemoveSlashes.php b/src/Middleware/RemoveSlashes.php index dacd68285..69cea70d0 100644 --- a/src/Middleware/RemoveSlashes.php +++ b/src/Middleware/RemoveSlashes.php @@ -4,18 +4,17 @@ declare(strict_types=1); namespace App\Middleware; +use App\Http\ServerRequest; use GuzzleHttp\Psr7\Response; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; /** * Remove trailing slash from all URLs when routing. */ -final class RemoveSlashes implements MiddlewareInterface +final class RemoveSlashes extends AbstractMiddleware { - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $uri = $request->getUri(); $path = $uri->getPath(); diff --git a/src/Middleware/ReopenEntityManagerMiddleware.php b/src/Middleware/ReopenEntityManagerMiddleware.php index af17877c3..b81268512 100644 --- a/src/Middleware/ReopenEntityManagerMiddleware.php +++ b/src/Middleware/ReopenEntityManagerMiddleware.php @@ -6,12 +6,11 @@ namespace App\Middleware; use App\Container\EnvironmentAwareTrait; use App\Doctrine\DecoratedEntityManager; +use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -final class ReopenEntityManagerMiddleware implements MiddlewareInterface +final class ReopenEntityManagerMiddleware extends AbstractMiddleware { use EnvironmentAwareTrait; @@ -20,7 +19,7 @@ final class ReopenEntityManagerMiddleware implements MiddlewareInterface ) { } - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { $this->em->open(); diff --git a/src/Middleware/RequireLogin.php b/src/Middleware/RequireLogin.php index b6da4cfc0..6a5c87dd6 100644 --- a/src/Middleware/RequireLogin.php +++ b/src/Middleware/RequireLogin.php @@ -14,7 +14,7 @@ use Psr\Http\Server\RequestHandlerInterface; /** * Require that the user be logged in to view this page. */ -final class RequireLogin +final class RequireLogin extends AbstractMiddleware { public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { diff --git a/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php b/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php index 9ffd65b48..a63a1597e 100644 --- a/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php +++ b/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php @@ -21,7 +21,7 @@ use Slim\Routing\RouteContext; /** * Require that the podcast has a published episode for public access */ -final class RequirePublishedPodcastEpisodeMiddleware +final class RequirePublishedPodcastEpisodeMiddleware extends AbstractMiddleware { public function __construct( private readonly PodcastRepository $podcastRepository diff --git a/src/Middleware/RequireStation.php b/src/Middleware/RequireStation.php index 5c1d8b644..c011a8f03 100644 --- a/src/Middleware/RequireStation.php +++ b/src/Middleware/RequireStation.php @@ -13,7 +13,7 @@ use Psr\Http\Server\RequestHandlerInterface; /** * Require that the user be logged in to view this page. */ -final class RequireStation +final class RequireStation extends AbstractMiddleware { public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { diff --git a/src/Middleware/StationSupportsFeature.php b/src/Middleware/StationSupportsFeature.php index 6900333c7..e08788130 100644 --- a/src/Middleware/StationSupportsFeature.php +++ b/src/Middleware/StationSupportsFeature.php @@ -10,7 +10,7 @@ use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\RequestHandlerInterface; -final class StationSupportsFeature +final class StationSupportsFeature extends AbstractMiddleware { public function __construct( private readonly StationFeatures $feature diff --git a/src/Middleware/WrapExceptionsWithRequestData.php b/src/Middleware/WrapExceptionsWithRequestData.php index 85036f493..7b3a6d1af 100644 --- a/src/Middleware/WrapExceptionsWithRequestData.php +++ b/src/Middleware/WrapExceptionsWithRequestData.php @@ -5,18 +5,17 @@ declare(strict_types=1); namespace App\Middleware; use App\Exception\WrappedException; +use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Throwable; /** * Wrap all exceptions thrown past this point with rich metadata. */ -final class WrapExceptionsWithRequestData implements MiddlewareInterface +final class WrapExceptionsWithRequestData extends AbstractMiddleware { - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { try { return $handler->handle($request); diff --git a/src/Nginx/HlsListeners.php b/src/Nginx/HlsListeners.php index 1f960ac3d..92d180812 100644 --- a/src/Nginx/HlsListeners.php +++ b/src/Nginx/HlsListeners.php @@ -110,6 +110,7 @@ final class HlsListeners } try { + /** @var array $rowJson */ $rowJson = json_decode($row, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException) { return null; diff --git a/src/Paginator.php b/src/Paginator.php index aca606988..b80950d96 100644 --- a/src/Paginator.php +++ b/src/Paginator.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App; +use App\Exception\InvalidRequestAttribute; use App\Http\Response; use App\Http\RouterInterface; use App\Http\ServerRequest; @@ -19,7 +20,6 @@ use Pagerfanta\Doctrine\Collections\CollectionAdapter; use Pagerfanta\Doctrine\ORM\QueryAdapter; use Pagerfanta\Pagerfanta; use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; /** * @template TKey of array-key @@ -47,11 +47,16 @@ final class Paginator implements IteratorAggregate, Countable */ public function __construct( private readonly Pagerfanta $paginator, - ServerRequestInterface $request + ServerRequest $request ) { - $this->router = $request->getAttribute(ServerRequest::ATTR_ROUTER); + $this->router = $request->getRouter(); + + try { + $user = $request->getUser(); + } catch (InvalidRequestAttribute) { + $user = null; + } - $user = $request->getAttribute(ServerRequest::ATTR_USER); $this->isAuthenticated = ($user !== null); $params = $request->getQueryParams(); @@ -194,7 +199,7 @@ final class Paginator implements IteratorAggregate, Countable */ public static function fromAdapter( AdapterInterface $adapter, - ServerRequestInterface $request + ServerRequest $request ): self { return new self( new Pagerfanta($adapter), @@ -209,7 +214,7 @@ final class Paginator implements IteratorAggregate, Countable * @param array $input * @return static */ - public static function fromArray(array $input, ServerRequestInterface $request): self + public static function fromArray(array $input, ServerRequest $request): self { return self::fromAdapter(new ArrayAdapter($input), $request); } @@ -221,7 +226,7 @@ final class Paginator implements IteratorAggregate, Countable * @param Collection $collection * @return static */ - public static function fromCollection(Collection $collection, ServerRequestInterface $request): self + public static function fromCollection(Collection $collection, ServerRequest $request): self { return self::fromAdapter(new CollectionAdapter($collection), $request); } @@ -229,7 +234,7 @@ final class Paginator implements IteratorAggregate, Countable /** * @return static */ - public static function fromQueryBuilder(QueryBuilder $qb, ServerRequestInterface $request): self + public static function fromQueryBuilder(QueryBuilder $qb, ServerRequest $request): self { return self::fromAdapter(new QueryAdapter($qb), $request); } @@ -237,7 +242,7 @@ final class Paginator implements IteratorAggregate, Countable /** * @return static */ - public static function fromQuery(Query $query, ServerRequestInterface $request): self + public static function fromQuery(Query $query, ServerRequest $request): self { return self::fromAdapter(new QueryAdapter($query), $request); } diff --git a/src/Radio/Backend/Liquidsoap/Command/AbstractCommand.php b/src/Radio/Backend/Liquidsoap/Command/AbstractCommand.php index 5a4689f26..8795ed961 100644 --- a/src/Radio/Backend/Liquidsoap/Command/AbstractCommand.php +++ b/src/Radio/Backend/Liquidsoap/Command/AbstractCommand.php @@ -7,6 +7,7 @@ namespace App\Radio\Backend\Liquidsoap\Command; use App\Container\LoggerAwareTrait; use App\Entity\Station; use App\Radio\Enums\BackendAdapters; +use App\Utilities\Types; use Monolog\LogRecord; use ReflectionClass; use Throwable; @@ -54,7 +55,7 @@ abstract class AbstractCommand return 'false'; } - return (string)$result; + return Types::string($result); } catch (Throwable $e) { $this->logger->error( sprintf( diff --git a/src/Radio/Configuration.php b/src/Radio/Configuration.php index 87c9d30e2..b72429f3a 100644 --- a/src/Radio/Configuration.php +++ b/src/Radio/Configuration.php @@ -352,6 +352,7 @@ final class Configuration DQL )->getArrayResult(); + /** @var array $row */ foreach ($stationConfigs as $row) { $stationReference = ['id' => $row['id'], 'name' => $row['name']]; diff --git a/src/Sync/Task/CheckMediaTask.php b/src/Sync/Task/CheckMediaTask.php index 9d74b9ca8..e95334259 100644 --- a/src/Sync/Task/CheckMediaTask.php +++ b/src/Sync/Task/CheckMediaTask.php @@ -205,9 +205,10 @@ final class CheckMediaTask extends AbstractTask DQL )->setParameter('storageLocation', $storageLocation); + /** @var array $mediaRow */ foreach ($existingMediaQuery->toIterable([], AbstractQuery::HYDRATE_ARRAY) as $mediaRow) { // Check if media file still exists. - $path = $mediaRow['path']; + $path = (string)$mediaRow['path']; $pathHash = md5($path); if (isset($musicFiles[$pathHash])) { @@ -216,11 +217,11 @@ final class CheckMediaTask extends AbstractTask if ( empty($mediaRow['unique_id']) - || StationMedia::needsReprocessing($mtime, $mediaRow['mtime'] ?? 0) + || StationMedia::needsReprocessing($mtime, (int)$mediaRow['mtime']) ) { $message = new ReprocessMediaMessage(); $message->storage_location_id = $storageLocation->getIdRequired(); - $message->media_id = $mediaRow['id']; + $message->media_id = (int)$mediaRow['id']; $message->force = empty($mediaRow['unique_id']); $this->messageBus->dispatch($message); diff --git a/src/Tests/Module.php b/src/Tests/Module.php index 7c6ad4c58..d4aaf1a95 100644 --- a/src/Tests/Module.php +++ b/src/Tests/Module.php @@ -52,7 +52,10 @@ class Module extends Framework implements DoctrineProvider } $this->container = $container; - $this->em = $this->container->get(ReloadableEntityManagerInterface::class); + + /** @var ReloadableEntityManagerInterface $em */ + $em = $this->container->get(ReloadableEntityManagerInterface::class); + $this->em = $em; parent::_initialize(); } diff --git a/src/Traits/RequestAwareTrait.php b/src/Traits/RequestAwareTrait.php index 96e805098..372db8cd7 100644 --- a/src/Traits/RequestAwareTrait.php +++ b/src/Traits/RequestAwareTrait.php @@ -4,18 +4,18 @@ declare(strict_types=1); namespace App\Traits; -use Psr\Http\Message\ServerRequestInterface; +use App\Http\ServerRequest; trait RequestAwareTrait { - protected ?ServerRequestInterface $request = null; + protected ?ServerRequest $request = null; - public function setRequest(?ServerRequestInterface $request): void + public function setRequest(?ServerRequest $request): void { $this->request = $request; } - public function withRequest(?ServerRequestInterface $request): self + public function withRequest(?ServerRequest $request): self { $newInstance = clone $this; $newInstance->setRequest($request); diff --git a/src/Translations/JsonGenerator.php b/src/Translations/JsonGenerator.php index a460c399d..2638c9765 100644 --- a/src/Translations/JsonGenerator.php +++ b/src/Translations/JsonGenerator.php @@ -26,9 +26,10 @@ final class JsonGenerator extends Generator public function generateArray(Translations $translations): array { $pluralForm = $translations->getHeaders()->getPluralForm(); - $pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null; + $pluralSize = is_array($pluralForm) ? (int)($pluralForm[0] - 1) : null; $messages = []; + /** @var Translation $translation */ foreach ($translations as $translation) { if (!$translation->getTranslation() || $translation->isDisabled()) { continue; diff --git a/src/Utilities/Strings.php b/src/Utilities/Strings.php index ba602722c..e024bc08a 100644 --- a/src/Utilities/Strings.php +++ b/src/Utilities/Strings.php @@ -9,29 +9,11 @@ use voku\helper\UTF8; final class Strings { - /** - * Given a nullable string value, return null if the string is null or otherwise - * is considered "empty", or the trimmed value otherwise. - */ - public static function nonEmptyOrNull(?string $input): ?string - { - if (null === $input || '' === $input) { - return null; - } - - $input = trim($input); - return (!empty($input)) - ? $input - : null; - } - /** * Truncate text (adding "..." if needed) */ public static function truncateText(string $text, int $limit = 80, string $pad = '...'): string { - mb_internal_encoding('UTF-8'); - if (mb_strlen($text) <= $limit) { return $text; } diff --git a/src/Utilities/Types.php b/src/Utilities/Types.php new file mode 100644 index 000000000..53c611f59 --- /dev/null +++ b/src/Utilities/Types.php @@ -0,0 +1,107 @@ + */ + /** @var ArrayCollection */ private ArrayCollection $globalProps; public function __construct( @@ -151,7 +150,7 @@ final class View extends Engine $dispatcher->dispatch(new Event\BuildView($this)); } - public function setRequest(?ServerRequestInterface $request): void + public function setRequest(?ServerRequest $request): void { $this->request = $request; @@ -169,7 +168,7 @@ final class View extends Engine } $customization = $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION); - if (null !== $customization) { + if ($customization instanceof Customization) { $requestData['customization'] = $customization; $this->globalProps->set( @@ -246,7 +245,7 @@ final class View extends Engine return $this->sections; } - /** @return ArrayCollection */ + /** @return ArrayCollection */ public function getGlobalProps(): ArrayCollection { return $this->globalProps; diff --git a/src/Webhook/Connector/AbstractConnector.php b/src/Webhook/Connector/AbstractConnector.php index f3cca3b4b..05ab55758 100644 --- a/src/Webhook/Connector/AbstractConnector.php +++ b/src/Webhook/Connector/AbstractConnector.php @@ -94,7 +94,7 @@ abstract class AbstractConnector implements ConnectorInterface // Replaces {{ var.name }} with the flattened $values['var.name'] $vars[$varKey] = preg_replace_callback( "/\{\{(\s*)([a-zA-Z\d\-_.]+)(\s*)}}/", - static function ($matches) use ($values) { + static function (array $matches) use ($values): string { $innerValue = strtolower(trim($matches[2])); return $values[$innerValue] ?? ''; }, @@ -107,11 +107,11 @@ abstract class AbstractConnector implements ConnectorInterface /** * Determine if a passed URL is valid and return it if so, or return null otherwise. - * - * @param string|null $urlString */ - protected function getValidUrl(?string $urlString = null): ?string + protected function getValidUrl(mixed $urlString = null): ?string { + $urlString = Utilities\Types::stringOrNull($urlString, true); + $uri = Utilities\Urls::tryParseUserUrl( $urlString, 'Webhook' diff --git a/src/Webhook/Connector/Email.php b/src/Webhook/Connector/Email.php index fd1647c35..2e9803c3a 100644 --- a/src/Webhook/Connector/Email.php +++ b/src/Webhook/Connector/Email.php @@ -8,6 +8,7 @@ use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Station; use App\Entity\StationWebhook; use App\Service\Mail; +use App\Utilities\Types; use GuzzleHttp\Client; use RuntimeException; @@ -34,11 +35,11 @@ final class Email extends AbstractConnector } $config = $webhook->getConfig(); - $emailTo = $config['to']; - $emailSubject = $config['subject']; - $emailBody = $config['message']; + $emailTo = Types::stringOrNull($config['to'], true); + $emailSubject = Types::stringOrNull($config['subject'], true); + $emailBody = Types::stringOrNull($config['message'], true); - if (empty($emailTo) || empty($emailSubject) || empty($emailBody)) { + if (null === $emailTo || null === $emailSubject || null === $emailBody) { throw $this->incompleteConfigException($webhook); } diff --git a/src/Webhook/Connector/Generic.php b/src/Webhook/Connector/Generic.php index e9dc0b954..e51d93d5d 100644 --- a/src/Webhook/Connector/Generic.php +++ b/src/Webhook/Connector/Generic.php @@ -7,6 +7,7 @@ namespace App\Webhook\Connector; use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Station; use App\Entity\StationWebhook; +use App\Utilities\Types; final class Generic extends AbstractConnector { @@ -32,7 +33,7 @@ final class Generic extends AbstractConnector 'Content-Type' => 'application/json', ], 'json' => $np, - 'timeout' => (float)($config['timeout'] ?? 5.0), + 'timeout' => Types::floatOrNull($config['timeout']) ?? 5.0, ]; if (!empty($config['basic_auth_username']) && !empty($config['basic_auth_password'])) { diff --git a/src/Webhook/Connector/GoogleAnalyticsV4.php b/src/Webhook/Connector/GoogleAnalyticsV4.php index 4b281ab95..e07d431d4 100644 --- a/src/Webhook/Connector/GoogleAnalyticsV4.php +++ b/src/Webhook/Connector/GoogleAnalyticsV4.php @@ -7,6 +7,7 @@ namespace App\Webhook\Connector; use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Station; use App\Entity\StationWebhook; +use App\Utilities\Types; use Br33f\Ga4\MeasurementProtocol\Dto\Event\BaseEvent; use Br33f\Ga4\MeasurementProtocol\Dto\Request\BaseRequest; use Br33f\Ga4\MeasurementProtocol\HttpClient as Ga4HttpClient; @@ -25,7 +26,10 @@ final class GoogleAnalyticsV4 extends AbstractGoogleAnalyticsConnector ): void { $config = $webhook->getConfig(); - if (empty($config['api_secret']) || empty($config['measurement_id'])) { + $apiSecret = Types::stringOrNull($config['api_secret'], true); + $measurementId = Types::stringOrNull($config['measurement_id'], true); + + if (null === $apiSecret) { throw $this->incompleteConfigException($webhook); } @@ -36,7 +40,7 @@ final class GoogleAnalyticsV4 extends AbstractGoogleAnalyticsConnector $gaHttpClient = new Ga4HttpClient(); $gaHttpClient->setClient($this->httpClient); - $ga4Service = new Service($config['api_secret'], $config['measurement_id']); + $ga4Service = new Service($apiSecret, $measurementId); $ga4Service->setHttpClient($gaHttpClient); // Get all current listeners diff --git a/src/Webhook/Connector/Mastodon.php b/src/Webhook/Connector/Mastodon.php index 28eb89ea1..de2437a87 100644 --- a/src/Webhook/Connector/Mastodon.php +++ b/src/Webhook/Connector/Mastodon.php @@ -7,6 +7,7 @@ namespace App\Webhook\Connector; use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Station; use App\Entity\StationWebhook; +use App\Utilities\Types; use App\Utilities\Urls; /** @@ -30,15 +31,15 @@ final class Mastodon extends AbstractSocialConnector ): void { $config = $webhook->getConfig(); - $instanceUrl = trim($config['instance_url'] ?? ''); - $accessToken = trim($config['access_token'] ?? ''); + $instanceUrl = Types::stringOrNull($config['instance_url'], true); + $accessToken = Types::stringOrNull($config['access_token'], true); - if (empty($instanceUrl) || empty($accessToken)) { + if (null === $instanceUrl || null === $accessToken) { throw $this->incompleteConfigException($webhook); } $instanceUri = Urls::parseUserUrl($instanceUrl, 'Mastodon Instance URL'); - $visibility = $config['visibility'] ?? 'public'; + $visibility = Types::stringOrNull($config['visibility'], true) ?? 'public'; $this->logger->debug( 'Posting to Mastodon...', diff --git a/src/Webhook/Connector/MatomoAnalytics.php b/src/Webhook/Connector/MatomoAnalytics.php index b41388636..a26796f41 100644 --- a/src/Webhook/Connector/MatomoAnalytics.php +++ b/src/Webhook/Connector/MatomoAnalytics.php @@ -9,6 +9,7 @@ use App\Entity\Repository\ListenerRepository; use App\Entity\Station; use App\Entity\StationWebhook; use App\Http\RouterInterface; +use App\Utilities\Types; use App\Utilities\Urls; use GuzzleHttp\Client; use Psr\Http\Message\UriInterface; @@ -39,7 +40,10 @@ final class MatomoAnalytics extends AbstractConnector ): void { $config = $webhook->getConfig(); - if (empty($config['matomo_url']) || empty($config['site_id'])) { + $matomoUrl = Types::stringOrNull($config['matomo_url'], true); + $siteId = Types::intOrNull($config['site_id']); + + if (null === $matomoUrl || null === $siteId) { throw $this->incompleteConfigException($webhook); } @@ -66,17 +70,17 @@ final class MatomoAnalytics extends AbstractConnector // Build Matomo URI $apiUrl = Urls::parseUserUrl( - $config['matomo_url'], + $matomoUrl, 'Matomo Analytics URL', )->withPath('/matomo.php'); - $apiToken = $config['token'] ?? null; + $apiToken = Types::stringOrNull($config['token'], true); $stationName = $station->getName(); // Get all current listeners $liveListeners = $this->listenerRepo->iterateLiveListenersArray($station); - $webhookLastSent = (int)$webhook->getMetadataKey($webhook::LAST_SENT_TIMESTAMP_KEY, 0); + $webhookLastSent = Types::intOrNull($webhook->getMetadataKey($webhook::LAST_SENT_TIMESTAMP_KEY)) ?? 0; $i = 0; $entries = []; @@ -98,7 +102,7 @@ final class MatomoAnalytics extends AbstractConnector } $entry = [ - 'idsite' => (int)$config['site_id'], + 'idsite' => $siteId, 'rec' => 1, 'action_name' => 'Listeners / ' . $stationName . ' / ' . $streamName, 'url' => $listenerUrl, diff --git a/src/Webhook/Connector/Telegram.php b/src/Webhook/Connector/Telegram.php index 3de291938..71c577a80 100644 --- a/src/Webhook/Connector/Telegram.php +++ b/src/Webhook/Connector/Telegram.php @@ -7,6 +7,7 @@ namespace App\Webhook\Connector; use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Station; use App\Entity\StationWebhook; +use App\Utilities\Types; /** * Telegram web hook connector. @@ -26,10 +27,10 @@ final class Telegram extends AbstractConnector ): void { $config = $webhook->getConfig(); - $botToken = trim($config['bot_token'] ?? ''); - $chatId = trim($config['chat_id'] ?? ''); + $botToken = Types::stringOrNull($config['bot_token'], true); + $chatId = Types::stringOrNull($config['chat_id'], true); - if (empty($botToken) || empty($chatId)) { + if (null === $botToken || null === $chatId) { throw $this->incompleteConfigException($webhook); } @@ -40,13 +41,17 @@ final class Telegram extends AbstractConnector $np ); - $apiUrl = (!empty($config['api'])) ? rtrim($config['api'], '/') : 'https://api.telegram.org'; + $apiUrl = Types::stringOrNull($config['api'], true); + $apiUrl = (null !== $apiUrl) + ? rtrim($apiUrl, '/') + : 'https://api.telegram.org'; + $webhookUrl = $apiUrl . '/bot' . $botToken . '/sendMessage'; $requestParams = [ 'chat_id' => $chatId, 'text' => $messages['text'], - 'parse_mode' => $config['parse_mode'] ?? 'Markdown', // Markdown or HTML + 'parse_mode' => Types::stringOrNull($config['parse_mode'], true) ?? 'Markdown', // Markdown or HTML ]; $response = $this->httpClient->request( diff --git a/templates/partials/toasts.phtml b/templates/partials/toasts.phtml index 88fecf681..b9b0b992b 100644 --- a/templates/partials/toasts.phtml +++ b/templates/partials/toasts.phtml @@ -1,8 +1,11 @@ getAttribute(App\Http\ServerRequest::ATTR_SESSION_FLASH); +/** @var \App\Http\ServerRequest $request */ +try { + $flashObj = $request->getFlash(); +} catch (App\Exception\InvalidRequestAttribute) { + $flashObj = null; +} $notifies = []; ?> diff --git a/util/phpstan-doctrine.php b/util/phpstan-doctrine.php new file mode 100644 index 000000000..73ab0a01f --- /dev/null +++ b/util/phpstan-doctrine.php @@ -0,0 +1,18 @@ + $tempDir, + App\Environment::UPLOADS_DIR => $tempDir, +]); + +return $ci->get(Doctrine\ORM\EntityManagerInterface::class); diff --git a/util/phpstan.php b/util/phpstan.php index db548c3ce..c50fdd51f 100644 --- a/util/phpstan.php +++ b/util/phpstan.php @@ -14,9 +14,11 @@ const AZURACAST_API_NAME = 'Testing API'; $tempDir = sys_get_temp_dir(); -App\AppFactory::createCli( +$app = App\AppFactory::createCli( [ App\Environment::TEMP_DIR => $tempDir, App\Environment::UPLOADS_DIR => $tempDir, ] ); + +return $app;