Merge commit '8ea5b1b85e9477cd6f1bbff475839b4e88986be5'

This commit is contained in:
Buster Neece 2023-12-04 00:48:40 -06:00
parent a9f7a16b01
commit 9fe90b1d3c
No known key found for this signature in database
89 changed files with 739 additions and 504 deletions

View File

@ -900,3 +900,6 @@ ij_yaml_sequence_on_new_line = false
ij_yaml_space_before_colon = false ij_yaml_space_before_colon = false
ij_yaml_spaces_within_braces = true ij_yaml_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true ij_yaml_spaces_within_brackets = true
[*.neon]
indent_style = tab

View File

@ -1,12 +1,16 @@
includes: includes:
- phpstan-baseline.neon - phpstan-baseline.neon
- vendor/phpstan/phpstan-doctrine/extension.neon - vendor/phpstan/phpstan-doctrine/extension.neon
- vendor/phpstan/phpstan-doctrine/rules.neon
parameters: parameters:
level: 8 level: 8
checkMissingIterableValueType: false checkMissingIterableValueType: false
doctrine:
objectManagerLoader: util/phpstan-doctrine.php
paths: paths:
- bin - bin
- config - config

View File

@ -14,7 +14,6 @@ use App\Enums\StationPermissions;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Traits\RequestAwareTrait; use App\Traits\RequestAwareTrait;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use function in_array; use function in_array;
use function is_array; use function is_array;
@ -106,8 +105,8 @@ final class Acl
array|string|PermissionInterface $action, array|string|PermissionInterface $action,
Station|int $stationId = null Station|int $stationId = null
): bool { ): bool {
if ($this->request instanceof ServerRequestInterface) { if ($this->request instanceof ServerRequest) {
$user = $this->request->getAttribute(ServerRequest::ATTR_USER); $user = $this->request->getUser();
return $this->userAllowed($user, $action, $stationId); return $this->userAllowed($user, $action, $stationId);
} }

View File

@ -141,6 +141,9 @@ final class AppFactory
: $environment->getTempDirectory() . '/php_errors.log' : $environment->getTempDirectory() . '/php_errors.log'
); );
mb_internal_encoding('UTF-8');
ini_set('default_charset', 'utf-8');
if (!headers_sent()) { if (!headers_sent()) {
ini_set('session.use_only_cookies', '1'); ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1'); ini_set('session.cookie_httponly', '1');

View File

@ -9,6 +9,7 @@ use App\Entity\Enums\StorageLocationTypes;
use App\Entity\Repository\StorageLocationRepository; use App\Entity\Repository\StorageLocationRepository;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StorageLocation; use App\Entity\StorageLocation;
use App\Utilities\Types;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -45,16 +46,14 @@ final class BackupCommand extends AbstractDatabaseCommand
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$fsUtils = new Filesystem(); $fsUtils = new Filesystem();
$path = $input->getArgument('path'); $path = Types::stringOrNull($input->getArgument('path'), true)
$excludeMedia = (bool)$input->getOption('exclude-media'); ?? 'manual_backup_' . gmdate('Ymd_Hi') . '.zip';
$storageLocationId = $input->getOption('storage-location-id');
$excludeMedia = Types::bool($input->getOption('exclude-media'));
$storageLocationId = Types::intOrNull($input->getOption('storage-location-id'));
$startTime = microtime(true); $startTime = microtime(true);
if (empty($path)) {
$path = 'manual_backup_' . gmdate('Ymd_Hi') . '.zip';
}
$fileExt = strtolower(pathinfo($path, PATHINFO_EXTENSION)); $fileExt = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (Path::isAbsolute($path)) { if (Path::isAbsolute($path)) {

View File

@ -6,6 +6,7 @@ namespace App\Console\Command\Backup;
use App\Console\Command\AbstractDatabaseCommand; use App\Console\Command\AbstractDatabaseCommand;
use App\Entity\StorageLocation; use App\Entity\StorageLocation;
use App\Utilities\Types;
use Exception; use Exception;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -34,7 +35,7 @@ final class RestoreCommand extends AbstractDatabaseCommand
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$path = $input->getArgument('path'); $path = Types::stringOrNull($input->getArgument('path'), true);
$startTime = microtime(true); $startTime = microtime(true);
$io->title('AzuraCast Restore'); $io->title('AzuraCast Restore');

View File

@ -7,6 +7,7 @@ namespace App\Console\Command\MessageQueue;
use App\Console\Command\CommandAbstract; use App\Console\Command\CommandAbstract;
use App\MessageQueue\QueueManagerInterface; use App\MessageQueue\QueueManagerInterface;
use App\MessageQueue\QueueNames; use App\MessageQueue\QueueNames;
use App\Utilities\Types;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -35,9 +36,9 @@ final class ClearCommand extends CommandAbstract
{ {
$io = new SymfonyStyle($input, $output); $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); $queue = QueueNames::tryFrom($queueName);
if (null !== $queue) { if (null !== $queue) {

View File

@ -13,6 +13,7 @@ use App\MessageQueue\LogWorkerExceptionSubscriber;
use App\MessageQueue\QueueManagerInterface; use App\MessageQueue\QueueManagerInterface;
use App\MessageQueue\ResetArrayCacheSubscriber; use App\MessageQueue\ResetArrayCacheSubscriber;
use App\Service\HighAvailability; use App\Service\HighAvailability;
use App\Utilities\Types;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
use Psr\Log\NullLogger; use Psr\Log\NullLogger;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
@ -55,8 +56,8 @@ final class ProcessCommand extends AbstractSyncCommand
{ {
$this->logToExtraFile('app_worker.log'); $this->logToExtraFile('app_worker.log');
$runtime = (int)$input->getArgument('runtime'); $runtime = Types::int($input->getArgument('runtime'));
$workerName = $input->getOption('worker-name'); $workerName = Types::stringOrNull($input->getOption('worker-name'), true);
if (!$this->highAvailability->isActiveServer()) { if (!$this->highAvailability->isActiveServer()) {
$this->logger->error('This instance is not the current active instance.'); $this->logger->error('This instance is not the current active instance.');

View File

@ -8,6 +8,7 @@ use App\Container\EntityManagerAwareTrait;
use App\Entity\Repository\StationRepository; use App\Entity\Repository\StationRepository;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StationMedia; use App\Entity\StationMedia;
use App\Utilities\Types;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -37,11 +38,11 @@ final class ReprocessMediaCommand extends CommandAbstract
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$stationName = $input->getArgument('station-name'); $stationName = Types::stringOrNull($input->getArgument('station-name'), true);
$io->title('Manually Reprocess Media'); $io->title('Manually Reprocess Media');
if (empty($stationName)) { if (null === $stationName) {
$io->section('Reprocessing media for all stations...'); $io->section('Reprocessing media for all stations...');
$storageLocation = null; $storageLocation = null;

View File

@ -8,6 +8,7 @@ use App\Entity\Repository\StationRepository;
use App\Entity\Station; use App\Entity\Station;
use App\Nginx\Nginx; use App\Nginx\Nginx;
use App\Radio\Configuration; use App\Radio\Configuration;
use App\Utilities\Types;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -45,8 +46,8 @@ final class RestartRadioCommand extends CommandAbstract
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$stationName = $input->getArgument('station-name'); $stationName = Types::stringOrNull($input->getArgument('station-name'));
$noSupervisorRestart = (bool)$input->getOption('no-supervisor-restart'); $noSupervisorRestart = Types::bool($input->getOption('no-supervisor-restart'));
if (!empty($stationName)) { if (!empty($stationName)) {
$station = $this->stationRepo->findByIdentifier($stationName); $station = $this->stationRepo->findByIdentifier($stationName);

View File

@ -7,6 +7,7 @@ namespace App\Console\Command;
use App\Container\ContainerAwareTrait; use App\Container\ContainerAwareTrait;
use App\Container\EnvironmentAwareTrait; use App\Container\EnvironmentAwareTrait;
use App\Entity\Attributes\StableMigration; use App\Entity\Attributes\StableMigration;
use App\Utilities\Types;
use Exception; use Exception;
use FilesystemIterator; use FilesystemIterator;
use InvalidArgumentException; use InvalidArgumentException;
@ -44,7 +45,7 @@ final class RollbackDbCommand extends AbstractDatabaseCommand
// Pull migration corresponding to the stable version specified. // Pull migration corresponding to the stable version specified.
try { try {
$version = $input->getArgument('version'); $version = Types::string($input->getArgument('version'));
$migrationVersion = $this->findMigration($version); $migrationVersion = $this->findMigration($version);
} catch (Throwable $e) { } catch (Throwable $e) {
$io->error($e->getMessage()); $io->error($e->getMessage());

View File

@ -6,6 +6,7 @@ namespace App\Console\Command\Settings;
use App\Console\Command\CommandAbstract; use App\Console\Command\CommandAbstract;
use App\Container\SettingsAwareTrait; use App\Container\SettingsAwareTrait;
use App\Utilities\Types;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -30,8 +31,8 @@ final class SetCommand extends CommandAbstract
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$settingKey = $input->getArgument('setting-key'); $settingKey = Types::string($input->getArgument('setting-key'));
$settingValue = $input->getArgument('setting-value'); $settingValue = Types::string($input->getArgument('setting-value'));
$io->title('AzuraCast Settings'); $io->title('AzuraCast Settings');

View File

@ -9,6 +9,7 @@ use App\Entity\Repository\StationRepository;
use App\Entity\Station; use App\Entity\Station;
use App\Sync\NowPlaying\Task\BuildQueueTask; use App\Sync\NowPlaying\Task\BuildQueueTask;
use App\Sync\NowPlaying\Task\NowPlayingTask; use App\Sync\NowPlaying\Task\NowPlayingTask;
use App\Utilities\Types;
use Monolog\LogRecord; use Monolog\LogRecord;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -43,7 +44,7 @@ final class NowPlayingPerStationCommand extends AbstractSyncCommand
$this->logToExtraFile('app_nowplaying.log'); $this->logToExtraFile('app_nowplaying.log');
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$stationName = $input->getArgument('station'); $stationName = Types::string($input->getArgument('station'));
$station = $this->stationRepo->findByIdentifier($stationName); $station = $this->stationRepo->findByIdentifier($stationName);
if (!($station instanceof Station)) { if (!($station instanceof Station)) {

View File

@ -8,6 +8,7 @@ use App\Cache\DatabaseCache;
use App\Container\ContainerAwareTrait; use App\Container\ContainerAwareTrait;
use App\Container\LoggerAwareTrait; use App\Container\LoggerAwareTrait;
use App\Sync\Task\AbstractTask; use App\Sync\Task\AbstractTask;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use Monolog\LogRecord; use Monolog\LogRecord;
use ReflectionClass; use ReflectionClass;
@ -42,7 +43,9 @@ final class SingleTaskCommand extends AbstractSyncCommand
$this->logToExtraFile('app_sync.log'); $this->logToExtraFile('app_sync.log');
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$task = $input->getArgument('task');
/** @var class-string $task */
$task = Types::string($input->getArgument('task'));
try { try {
$this->runTask($task); $this->runTask($task);

View File

@ -9,6 +9,7 @@ use App\Container\EntityManagerAwareTrait;
use App\Entity\Repository\UserLoginTokenRepository; use App\Entity\Repository\UserLoginTokenRepository;
use App\Entity\User; use App\Entity\User;
use App\Http\RouterInterface; use App\Http\RouterInterface;
use App\Utilities\Types;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -39,7 +40,7 @@ final class LoginTokenCommand extends CommandAbstract
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email'); $email = Types::string($input->getArgument('email'));
$io->title('Generate Account Login Recovery URL'); $io->title('Generate Account Login Recovery URL');

View File

@ -31,7 +31,7 @@ final class ResetPasswordCommand extends CommandAbstract
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email'); $email = Utilities\Types::string($input->getArgument('email'));
$io->title('Reset Account Password'); $io->title('Reset Account Password');

View File

@ -8,6 +8,7 @@ use App\Console\Command\CommandAbstract;
use App\Container\EntityManagerAwareTrait; use App\Container\EntityManagerAwareTrait;
use App\Entity\Repository\RolePermissionRepository; use App\Entity\Repository\RolePermissionRepository;
use App\Entity\User; use App\Entity\User;
use App\Utilities\Types;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
@ -37,7 +38,7 @@ final class SetAdministratorCommand extends CommandAbstract
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$email = $input->getArgument('email'); $email = Types::string($input->getArgument('email'));
$io->title('Set Administrator'); $io->title('Set Administrator');

View File

@ -66,6 +66,7 @@ final class ChartsAction implements SingleActionInterface
} else { } else {
$threshold = CarbonImmutable::parse('-180 days'); $threshold = CarbonImmutable::parse('-180 days');
/** @var array<array{station_id: int, moment: CarbonImmutable, number_avg: float, number_unique: int}> $stats */
$stats = $this->em->createQuery( $stats = $this->em->createQuery(
<<<'DQL' <<<'DQL'
SELECT a.station_id, a.moment, a.number_avg, a.number_unique 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) { foreach ($stats as $row) {
$stationId = $row['station_id']; $stationId = $row['station_id'];
/** @var CarbonImmutable $moment */
$moment = $row['moment']; $moment = $row['moment'];
$sortableKey = $moment->format('Y-m-d'); $sortableKey = $moment->format('Y-m-d');
$jsTimestamp = $moment->getTimestamp() * 1000; $jsTimestamp = $moment->getTimestamp() * 1000;
$average = round((float)$row['number_avg'], 2); $average = round($row['number_avg'], 2);
$unique = $row['number_unique']; $unique = $row['number_unique'];
$rawStats['average'][$stationId][$sortableKey] = [ $rawStats['average'][$stationId][$sortableKey] = [

View File

@ -7,6 +7,7 @@ namespace App\Controller\Api;
use App\Cache\NowPlayingCache; use App\Cache\NowPlayingCache;
use App\Entity\Api\Error; use App\Entity\Api\Error;
use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Api\NowPlaying\NowPlaying;
use App\Exception\InvalidRequestAttribute;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
@ -83,9 +84,13 @@ final class NowPlayingController
$baseUrl = $router->getBaseUrl(); $baseUrl = $router->getBaseUrl();
// If unauthenticated, hide non-public stations from full view. // If unauthenticated, hide non-public stations from full view.
$np = $this->nowPlayingCache->getForAllStations( try {
$request->getAttribute('user') === null $user = $request->getUser();
); } catch (InvalidRequestAttribute) {
$user = null;
}
$np = $this->nowPlayingCache->getForAllStations(null === $user);
$np = array_map( $np = array_map(
function (NowPlaying $npRow) use ($baseUrl) { function (NowPlaying $npRow) use ($baseUrl) {

View File

@ -13,6 +13,7 @@ use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Doctrine\ORM\AbstractQuery;
use InvalidArgumentException; use InvalidArgumentException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -244,6 +245,7 @@ final class PlaylistsController extends AbstractScheduledEntityController
$return = $this->toArray($record); $return = $this->toArray($record);
/** @var array{num_songs: int, total_length: string} $songTotals */
$songTotals = $this->em->createQuery( $songTotals = $this->em->createQuery(
<<<'DQL' <<<'DQL'
SELECT count(sm.id) AS num_songs, sum(sm.length) AS total_length 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 WHERE spm.playlist = :playlist
DQL DQL
)->setParameter('playlist', $record) )->setParameter('playlist', $record)
->getArrayResult(); ->getSingleResult(AbstractQuery::HYDRATE_SCALAR);
$return['short_name'] = StationPlaylist::generateShortName($return['name']); $return['short_name'] = StationPlaylist::generateShortName($return['name']);
$return['num_songs'] = (int)$songTotals[0]['num_songs']; $return['num_songs'] = $songTotals['num_songs'];
$return['total_length'] = (int)$songTotals[0]['total_length']; $return['total_length'] = round((float)$songTotals['total_length']);
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = ('true' === $request->getParam('internal', 'false'));
$router = $request->getRouter(); $router = $request->getRouter();

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Traits; namespace App\Controller\Api\Traits;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Utilities\Types;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\QueryBuilder;
use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccess;
@ -59,21 +60,25 @@ trait CanSortResults
return $results; return $results;
} }
/**
* @return string[]
*/
protected function getSortFromRequest( protected function getSortFromRequest(
ServerRequest $request, ServerRequest $request,
string $defaultSortOrder = Criteria::ASC string $defaultSortOrder = Criteria::ASC
): array { ): array {
$sortOrder = Types::stringOrNull($request->getParam('sortOrder'), true) ?? $defaultSortOrder;
return [ return [
$request->getParam('sort'), $request->getParam('sort'),
('desc' === strtolower($request->getParam('sortOrder', $defaultSortOrder))) ('desc' === $sortOrder)
? Criteria::DESC ? Criteria::DESC
: Criteria::ASC, : Criteria::ASC,
]; ];
} }
protected static function sortByDotNotation( protected static function sortByDotNotation(
mixed $a, object|array $a,
mixed $b, object|array $b,
PropertyAccessorInterface $propertyAccessor, PropertyAccessorInterface $propertyAccessor,
string $sortValue, string $sortValue,
string $sortOrder string $sortOrder

View File

@ -6,7 +6,7 @@ namespace App\Controller\Frontend;
use App\Container\SettingsAwareTrait; use App\Container\SettingsAwareTrait;
use App\Controller\SingleActionInterface; use App\Controller\SingleActionInterface;
use App\Entity\User; use App\Exception\InvalidRequestAttribute;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; 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. // 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. // Redirect to a custom homepage URL if specified in settings.
$homepageRedirect = $settings->getHomepageRedirectUrl(); $homepageRedirect = $settings->getHomepageRedirectUrl();
if (null !== $homepageRedirect) { if (null !== $homepageRedirect) {
@ -38,8 +41,5 @@ final class IndexAction implements SingleActionInterface
return $response->withRedirect($request->getRouter()->named('account:login')); 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'));
} }
} }

View File

@ -35,6 +35,7 @@ final class OnDemandAction implements SingleActionInterface
} }
// Get list of custom fields. // Get list of custom fields.
/** @var array<array{id: int, short_name: string, name: string}> $customFieldsRaw */
$customFieldsRaw = $this->em->createQuery( $customFieldsRaw = $this->em->createQuery(
<<<'DQL' <<<'DQL'
SELECT cf.id, cf.short_name, cf.name SELECT cf.id, cf.short_name, cf.name

View File

@ -12,8 +12,8 @@ use App\Entity\Settings;
use App\Entity\Station; use App\Entity\Station;
use App\Enums\SupportedLocales; use App\Enums\SupportedLocales;
use App\Enums\SupportedThemes; use App\Enums\SupportedThemes;
use App\Http\ServerRequest;
use App\Traits\RequestAwareTrait; use App\Traits\RequestAwareTrait;
use Psr\Http\Message\ServerRequestInterface;
final class Customization final class Customization
{ {
@ -37,7 +37,7 @@ final class Customization
$this->locale = SupportedLocales::default(); $this->locale = SupportedLocales::default();
} }
public function setRequest(?ServerRequestInterface $request): void public function setRequest(?ServerRequest $request): void
{ {
$this->request = $request; $this->request = $request;

View File

@ -106,10 +106,10 @@ final class AuditLog implements EventSubscriber
} }
// Check if either field value is an object. // 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); $fieldPrev = $this->getIdentifier($fieldPrev);
} }
if ($this->isEntity($em, $fieldNow)) { if (is_object($fieldNow) && $this->isEntity($em, $fieldNow)) {
$fieldNow = $this->getIdentifier($fieldNow); $fieldNow = $this->getIdentifier($fieldNow);
} }

View File

@ -7,20 +7,22 @@ namespace App\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
#[ #[
ORM\Entity, ORM\Entity(readOnly: true),
ORM\Table(name: 'cache_items') ORM\Table(name: 'cache_items'),
] ]
class CacheItem class CacheItem
{ {
/** @var resource $item_id */
#[ #[
ORM\Column(type: 'binary', length: 255, nullable: false), ORM\Column(type: 'binary', length: 255, nullable: false),
ORM\Id, ORM\Id,
ORM\GeneratedValue(strategy: 'NONE') ORM\GeneratedValue(strategy: 'NONE')
] ]
protected string $item_id; protected mixed $item_id;
/** @var resource $item_data */
#[ORM\Column(type: 'blob', length: 16777215, nullable: false)] #[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])] #[ORM\Column(type: 'integer', nullable: true, options: ['unsigned' => true])]
protected ?int $item_lifetime; protected ?int $item_lifetime;

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use App\Utilities\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use JsonSerializable; use JsonSerializable;
@ -23,10 +24,10 @@ class ListenerLocation implements JsonSerializable
protected ?string $country = null; protected ?string $country = null;
#[ORM\Column(type: 'decimal', precision: 10, scale: 6, nullable: true)] #[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)] #[ORM\Column(type: 'decimal', precision: 10, scale: 6, nullable: true)]
protected ?float $lon = null; protected ?string $lon = null;
public function getDescription(): string public function getDescription(): string
{ {
@ -50,12 +51,12 @@ class ListenerLocation implements JsonSerializable
public function getLat(): ?float public function getLat(): ?float
{ {
return $this->lat; return Types::floatOrNull($this->lat);
} }
public function getLon(): ?float public function getLon(): ?float
{ {
return $this->lon; return Types::floatOrNull($this->lon);
} }
public function jsonSerialize(): array public function jsonSerialize(): array

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use App\Entity\Interfaces\IdentifiableEntityInterface; use App\Entity\Interfaces\IdentifiableEntityInterface;
use App\Entity\Traits; use App\Utilities\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
@ -32,7 +32,7 @@ class PodcastMedia implements IdentifiableEntityInterface
protected string $original_name; protected string $original_name;
#[ORM\Column(type: 'decimal', precision: 7, scale: 2)] #[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) */ /** @var string The formatted podcast media's duration (in mm:ss format) */
#[ORM\Column(length: 10)] #[ORM\Column(length: 10)]
@ -89,7 +89,7 @@ class PodcastMedia implements IdentifiableEntityInterface
public function getLength(): float public function getLength(): float
{ {
return $this->length; return Types::float($this->length);
} }
public function setLength(float $length): self public function setLength(float $length): self
@ -97,7 +97,7 @@ class PodcastMedia implements IdentifiableEntityInterface
$lengthMin = floor($length / 60); $lengthMin = floor($length / 60);
$lengthSec = (int)$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); $this->length_text = $lengthMin . ':' . str_pad((string)$lengthSec, 2, '0', STR_PAD_LEFT);
return $this; return $this;

View File

@ -137,7 +137,7 @@ final class SongHistoryRepository extends AbstractStationBasedRepository
* @param int $start * @param int $start
* @param int $end * @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 public function getStatsByTimeRange(Station $station, int $start, int $end): array
{ {

View File

@ -10,7 +10,7 @@ use App\Entity\Enums\IpSources;
use App\Enums\SupportedThemes; use App\Enums\SupportedThemes;
use App\OpenApi; use App\OpenApi;
use App\Service\Avatar; use App\Service\Avatar;
use App\Utilities\Strings; use App\Utilities\Types;
use App\Utilities\Urls; use App\Utilities\Urls;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@ -352,13 +352,13 @@ class Settings implements Stringable
public function getHomepageRedirectUrl(): ?string public function getHomepageRedirectUrl(): ?string
{ {
return Strings::nonEmptyOrNull($this->homepage_redirect_url); return Types::stringOrNull($this->homepage_redirect_url);
} }
public function setHomepageRedirectUrl(?string $homepageRedirectUrl): void public function setHomepageRedirectUrl(?string $homepageRedirectUrl): void
{ {
$this->homepage_redirect_url = $this->truncateNullableString( $this->homepage_redirect_url = $this->truncateNullableString(
Strings::nonEmptyOrNull($homepageRedirectUrl) Types::stringOrNull($homepageRedirectUrl)
); );
} }
@ -371,7 +371,7 @@ class Settings implements Stringable
public function getDefaultAlbumArtUrl(): ?string public function getDefaultAlbumArtUrl(): ?string
{ {
return Strings::nonEmptyOrNull($this->default_album_art_url); return Types::stringOrNull($this->default_album_art_url);
} }
public function getDefaultAlbumArtUrlAsUri(): ?UriInterface public function getDefaultAlbumArtUrlAsUri(): ?UriInterface
@ -386,7 +386,7 @@ class Settings implements Stringable
public function setDefaultAlbumArtUrl(?string $defaultAlbumArtUrl): void public function setDefaultAlbumArtUrl(?string $defaultAlbumArtUrl): void
{ {
$this->default_album_art_url = $this->truncateNullableString( $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 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 public function setLastFmApiKey(?string $lastFmApiKey): void
{ {
$this->last_fm_api_key = $this->truncateNullableString( $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 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 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 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 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 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 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 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 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 public function getBackupFormat(): ?string
{ {
return Strings::nonEmptyOrNull($this->backup_format); return Types::stringOrNull($this->backup_format, true);
} }
public function setBackupFormat(?string $backupFormat): void 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 public function getAcmeDomains(): ?string
{ {
return Strings::nonEmptyOrNull($this->acme_domains); return Types::stringOrNull($this->acme_domains, true);
} }
public function setAcmeDomains(?string $acmeDomains): void public function setAcmeDomains(?string $acmeDomains): void
{ {
$acmeDomains = Strings::nonEmptyOrNull($acmeDomains); $acmeDomains = Types::stringOrNull($acmeDomains, true);
if (null !== $acmeDomains) { if (null !== $acmeDomains) {
$acmeDomains = implode( $acmeDomains = implode(

View File

@ -9,6 +9,7 @@ use App\Radio\Enums\AudioProcessingMethods;
use App\Radio\Enums\CrossfadeModes; use App\Radio\Enums\CrossfadeModes;
use App\Radio\Enums\MasterMePresets; use App\Radio\Enums\MasterMePresets;
use App\Radio\Enums\StreamFormats; use App\Radio\Enums\StreamFormats;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use LogicException; use LogicException;
@ -18,7 +19,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getCharset(): string 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 public function setCharset(?string $charset): void
@ -30,8 +31,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getDjPort(): ?int public function getDjPort(): ?int
{ {
$port = $this->get(self::DJ_PORT); return Types::intOrNull($this->get(self::DJ_PORT));
return is_numeric($port) ? (int)$port : null;
} }
public function setDjPort(?int $port): void public function setDjPort(?int $port): void
@ -43,8 +43,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getTelnetPort(): ?int public function getTelnetPort(): ?int
{ {
$port = $this->get(self::TELNET_PORT); return Types::intOrNull($this->get(self::TELNET_PORT));
return is_numeric($port) ? (int)$port : null;
} }
public function setTelnetPort(?int $port): void public function setTelnetPort(?int $port): void
@ -56,7 +55,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function recordStreams(): bool 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 public function setRecordStreams(?bool $recordStreams): void
@ -73,7 +72,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getRecordStreamsFormatEnum(): StreamFormats 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; ?? StreamFormats::Mp3;
} }
@ -94,7 +95,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getRecordStreamsBitrate(): int 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 public function setRecordStreamsBitrate(?int $bitrate): void
@ -106,7 +107,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function useManualAutoDj(): bool 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 public function setUseManualAutoDj(?bool $useManualAutoDj): void
@ -120,7 +121,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getAutoDjQueueLength(): int 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 public function setAutoDjQueueLength(?int $queueLength): void
@ -132,7 +133,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getDjMountPoint(): string 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 public function setDjMountPoint(?string $mountPoint): void
@ -146,7 +147,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getDjBuffer(): int 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 public function setDjBuffer(?int $buffer): void
@ -163,8 +164,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getAudioProcessingMethodEnum(): AudioProcessingMethods public function getAudioProcessingMethodEnum(): AudioProcessingMethods
{ {
return AudioProcessingMethods::tryFrom($this->get(self::AUDIO_PROCESSING_METHOD) ?? '') return AudioProcessingMethods::tryFrom(
?? AudioProcessingMethods::default(); Types::stringOrNull($this->get(self::AUDIO_PROCESSING_METHOD)) ?? ''
) ?? AudioProcessingMethods::default();
} }
public function isAudioProcessingEnabled(): bool public function isAudioProcessingEnabled(): bool
@ -189,7 +191,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getPostProcessingIncludeLive(): bool 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 public function setPostProcessingIncludeLive(bool $postProcessingIncludeLive): void
@ -201,7 +203,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getStereoToolLicenseKey(): ?string 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 public function setStereoToolLicenseKey(?string $licenseKey): void
@ -213,7 +215,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getStereoToolConfigurationPath(): ?string 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 public function setStereoToolConfigurationPath(?string $stereoToolConfigurationPath): void
@ -225,12 +227,12 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getMasterMePreset(): ?string 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 public function getMasterMePresetEnum(): MasterMePresets
{ {
return MasterMePresets::tryFrom($this->get(self::MASTER_ME_PRESET) ?? '') return MasterMePresets::tryFrom($this->getMasterMePreset() ?? '')
?? MasterMePresets::default(); ?? MasterMePresets::default();
} }
@ -253,7 +255,8 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getMasterMeLoudnessTarget(): float 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 public function setMasterMeLoudnessTarget(?float $masterMeLoudnessTarget): void
@ -265,7 +268,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function useReplayGain(): bool 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 public function setUseReplayGain(?bool $useReplayGain): void
@ -277,8 +280,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getCrossfadeTypeEnum(): CrossfadeModes public function getCrossfadeTypeEnum(): CrossfadeModes
{ {
return CrossfadeModes::tryFrom($this->get(self::CROSSFADE_TYPE) ?? '') return CrossfadeModes::tryFrom(
?? CrossfadeModes::default(); Types::stringOrNull($this->get(self::CROSSFADE_TYPE)) ?? ''
) ?? CrossfadeModes::default();
} }
public function getCrossfadeType(): string public function getCrossfadeType(): string
@ -297,7 +301,10 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getCrossfade(): float 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 public function setCrossfade(?float $crossfade): void
@ -328,9 +335,8 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getDuplicatePreventionTimeRange(): int public function getDuplicatePreventionTimeRange(): int
{ {
return (int)( return Types::intOrNull($this->get(self::DUPLICATE_PREVENTION_TIME_RANGE))
$this->get(self::DUPLICATE_PREVENTION_TIME_RANGE) ?? self::DEFAULT_DUPLICATE_PREVENTION_TIME_RANGE ?? self::DEFAULT_DUPLICATE_PREVENTION_TIME_RANGE;
);
} }
public function setDuplicatePreventionTimeRange(?int $duplicatePreventionTimeRange): void public function setDuplicatePreventionTimeRange(?int $duplicatePreventionTimeRange): void
@ -347,8 +353,9 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getPerformanceModeEnum(): StationBackendPerformanceModes public function getPerformanceModeEnum(): StationBackendPerformanceModes
{ {
return StationBackendPerformanceModes::tryFrom($this->get(self::PERFORMANCE_MODE) ?? '') return StationBackendPerformanceModes::tryFrom(
?? StationBackendPerformanceModes::default(); Types::stringOrNull($this->get(self::PERFORMANCE_MODE)) ?? ''
) ?? StationBackendPerformanceModes::default();
} }
public function setPerformanceMode(?string $performanceMode): void public function setPerformanceMode(?string $performanceMode): void
@ -365,7 +372,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getHlsSegmentLength(): int 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 public function setHlsSegmentLength(?int $length): void
@ -377,7 +384,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getHlsSegmentsInPlaylist(): int 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 public function setHlsSegmentsInPlaylist(?int $value): void
@ -389,7 +396,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getHlsSegmentsOverhead(): int 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 public function setHlsSegmentsOverhead(?int $value): void
@ -401,7 +408,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getHlsEnableOnPublicPlayer(): bool 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 public function setHlsEnableOnPublicPlayer(?bool $enable): void
@ -413,7 +420,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getHlsIsDefault(): bool 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 public function setHlsIsDefault(?bool $value): void
@ -425,11 +432,8 @@ class StationBackendConfiguration extends AbstractStationConfiguration
public function getLiveBroadcastText(): string public function getLiveBroadcastText(): string
{ {
$text = $this->get(self::LIVE_BROADCAST_TEXT); return Types::stringOrNull($this->get(self::LIVE_BROADCAST_TEXT), true)
?? 'Live Broadcast';
return (!empty($text))
? $text
: 'Live Broadcast';
} }
public function setLiveBroadcastText(?string $text): void public function setLiveBroadcastText(?string $text): void
@ -464,7 +468,7 @@ class StationBackendConfiguration extends AbstractStationConfiguration
throw new LogicException('Invalid custom configuration section.'); 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 public function setCustomConfigurationSection(string $section, ?string $value = null): void

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use App\Utilities\Types;
use App\Utilities\Urls; use App\Utilities\Urls;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
@ -13,7 +14,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration
public function getDefaultAlbumArtUrl(): ?string 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 public function getDefaultAlbumArtUrlAsUri(): ?UriInterface
@ -34,7 +35,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration
public function getPublicCustomCss(): ?string 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 public function setPublicCustomCss(?string $css): void
@ -46,7 +47,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration
public function getPublicCustomJs(): ?string 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 public function setPublicCustomJs(?string $js): void
@ -58,11 +59,7 @@ class StationBrandingConfiguration extends AbstractStationConfiguration
public function getOfflineText(): ?string public function getOfflineText(): ?string
{ {
$message = $this->get(self::OFFLINE_TEXT); return Types::stringOrNull($this->get(self::OFFLINE_TEXT), true);
return (!empty($message))
? $message
: null;
} }
public function setOfflineText(?string $message): void public function setOfflineText(?string $message): void

View File

@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use App\Utilities\Strings; use App\Utilities\Strings;
use App\Utilities\Types;
use LogicException;
class StationFrontendConfiguration extends AbstractStationConfiguration class StationFrontendConfiguration extends AbstractStationConfiguration
{ {
@ -31,7 +33,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getCustomConfiguration(): ?string 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 public function setCustomConfiguration(?string $config): void
@ -43,7 +45,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getSourcePassword(): string 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 public function setSourcePassword(string $pw): void
@ -55,7 +58,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getAdminPassword(): string 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 public function setAdminPassword(string $pw): void
@ -67,7 +71,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getRelayPassword(): string 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 public function setRelayPassword(string $pw): void
@ -79,7 +84,8 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getStreamerPassword(): string 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 public function setStreamerPassword(string $pw): void
@ -91,8 +97,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getPort(): ?int public function getPort(): ?int
{ {
$port = $this->get(self::PORT); return Types::intOrNull($this->get(self::PORT));
return is_numeric($port) ? (int)$port : null;
} }
public function setPort(?int $port): void public function setPort(?int $port): void
@ -104,8 +109,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getMaxListeners(): ?int public function getMaxListeners(): ?int
{ {
$listeners = $this->get(self::MAX_LISTENERS); return Types::intOrNull($this->get(self::MAX_LISTENERS));
return is_numeric($listeners) ? (int)$listeners : null;
} }
public function setMaxListeners(?int $listeners): void public function setMaxListeners(?int $listeners): void
@ -117,7 +121,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getBannedIps(): ?string 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 public function setBannedIps(?string $ips): void
@ -129,7 +133,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getBannedUserAgents(): ?string 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 public function setBannedUserAgents(?string $userAgents): void
@ -141,7 +145,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getBannedCountries(): ?array public function getBannedCountries(): ?array
{ {
return $this->get(self::BANNED_COUNTRIES); return Types::arrayOrNull($this->get(self::BANNED_COUNTRIES));
} }
public function setBannedCountries(?array $countries): void public function setBannedCountries(?array $countries): void
@ -153,7 +157,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getAllowedIps(): ?string 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 public function setAllowedIps(?string $ips): void
@ -165,7 +169,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getScLicenseId(): ?string 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 public function setScLicenseId(?string $licenseId): void
@ -177,7 +181,7 @@ class StationFrontendConfiguration extends AbstractStationConfiguration
public function getScUserId(): ?string 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 public function setScUserId(?string $userId): void

View File

@ -10,6 +10,7 @@ use App\Media\MetadataInterface;
use App\Normalizer\Attributes\DeepNormalize; use App\Normalizer\Attributes\DeepNormalize;
use App\OpenApi; use App\OpenApi;
use App\Utilities\Time; use App\Utilities\Time;
use App\Utilities\Types;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@ -97,7 +98,7 @@ class StationMedia implements
), ),
ORM\Column(type: 'decimal', precision: 7, scale: 2, nullable: true) ORM\Column(type: 'decimal', precision: 7, scale: 2, nullable: true)
] ]
protected ?float $length = 0.00; protected ?string $length = '0.00';
#[ #[
OA\Property( OA\Property(
@ -133,7 +134,7 @@ class StationMedia implements
), ),
ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true)
] ]
protected ?float $amplify = null; protected ?string $amplify = null;
#[ #[
OA\Property( OA\Property(
@ -142,7 +143,7 @@ class StationMedia implements
), ),
ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true)
] ]
protected ?float $fade_overlap = null; protected ?string $fade_overlap = null;
#[ #[
OA\Property( OA\Property(
@ -151,7 +152,7 @@ class StationMedia implements
), ),
ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true)
] ]
protected ?float $fade_in = null; protected ?string $fade_in = null;
#[ #[
OA\Property( OA\Property(
@ -160,7 +161,7 @@ class StationMedia implements
), ),
ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true)
] ]
protected ?float $fade_out = null; protected ?string $fade_out = null;
#[ #[
OA\Property( OA\Property(
@ -169,7 +170,7 @@ class StationMedia implements
), ),
ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true)
] ]
protected ?float $cue_in = null; protected ?string $cue_in = null;
#[ #[
OA\Property( OA\Property(
@ -178,7 +179,7 @@ class StationMedia implements
), ),
ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true) ORM\Column(type: 'decimal', precision: 6, scale: 1, nullable: true)
] ]
protected ?float $cue_out = null; protected ?string $cue_out = null;
#[ #[
OA\Property( OA\Property(
@ -294,18 +295,15 @@ class StationMedia implements
public function getLength(): ?float public function getLength(): ?float
{ {
return $this->length; return Types::floatOrNull($this->length);
} }
/** public function setLength(float $length): void
* @param int $length
*/
public function setLength(int $length): void
{ {
$lengthMin = floor($length / 60); $lengthMin = floor($length / 60);
$lengthSec = $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); $this->length_text = $lengthMin . ':' . str_pad((string)$lengthSec, 2, '0', STR_PAD_LEFT);
} }
@ -341,62 +339,62 @@ class StationMedia implements
public function getAmplify(): ?float public function getAmplify(): ?float
{ {
return $this->amplify; return Types::floatOrNull($this->amplify);
} }
public function setAmplify(?float $amplify = null): void public function setAmplify(?float $amplify = null): void
{ {
$this->amplify = $amplify; $this->amplify = (string)$amplify;
} }
public function getFadeOverlap(): ?float public function getFadeOverlap(): ?float
{ {
return $this->fade_overlap; return Types::floatOrNull($this->fade_overlap);
} }
public function setFadeOverlap(?float $fadeOverlap = null): void public function setFadeOverlap(?float $fadeOverlap = null): void
{ {
$this->fade_overlap = $fadeOverlap; $this->fade_overlap = (string)$fadeOverlap;
} }
public function getFadeIn(): ?float public function getFadeIn(): ?float
{ {
return $this->fade_in; return Types::floatOrNull($this->fade_in);
} }
public function setFadeIn(string|int|float $fadeIn = null): void 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 public function getFadeOut(): ?float
{ {
return $this->fade_out; return Types::floatOrNull($this->fade_out);
} }
public function setFadeOut(string|int|float $fadeOut = null): void 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 public function getCueIn(): ?float
{ {
return $this->cue_in; return Types::floatOrNull($this->cue_in);
} }
public function setCueIn(string|int|float $cueIn = null): void 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 public function getCueOut(): ?float
{ {
return $this->cue_out; return Types::floatOrNull($this->cue_out);
} }
public function setCueOut(string|int|float $cueOut = null): void 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 public function getCalculatedLength(): int
{ {
$length = (int)$this->length; $length = $this->getLength() ?? 0.0;
if ((int)$this->cue_out > 0) { $cueOut = $this->getCueOut();
$lengthRemoved = $length - (int)$this->cue_out; if ($cueOut > 0) {
$lengthRemoved = $length - $cueOut;
$length -= $lengthRemoved; $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 public function getArtUpdatedAt(): int
@ -474,27 +475,27 @@ class StationMedia implements
public function fromMetadata(MetadataInterface $metadata): void public function fromMetadata(MetadataInterface $metadata): void
{ {
$this->setLength((int)$metadata->getDuration()); $this->setLength($metadata->getDuration());
$tags = $metadata->getTags(); $tags = $metadata->getTags();
if (isset($tags['title'])) { if (isset($tags['title'])) {
$this->setTitle($tags['title']); $this->setTitle(Types::stringOrNull($tags['title']));
} }
if (isset($tags['artist'])) { if (isset($tags['artist'])) {
$this->setArtist($tags['artist']); $this->setArtist(Types::stringOrNull($tags['artist']));
} }
if (isset($tags['album'])) { if (isset($tags['album'])) {
$this->setAlbum($tags['album']); $this->setAlbum(Types::stringOrNull($tags['album']));
} }
if (isset($tags['genre'])) { if (isset($tags['genre'])) {
$this->setGenre($tags['genre']); $this->setGenre(Types::stringOrNull($tags['genre']));
} }
if (isset($tags['unsynchronised_lyric'])) { if (isset($tags['unsynchronised_lyric'])) {
$this->setLyrics($tags['unsynchronised_lyric']); $this->setLyrics(Types::stringOrNull($tags['unsynchronised_lyric']));
} }
if (isset($tags['isrc'])) { if (isset($tags['isrc'])) {
$this->setIsrc($tags['isrc']); $this->setIsrc(Types::stringOrNull($tags['isrc']));
} }
$this->updateSongId(); $this->updateSongId();

View File

@ -127,7 +127,7 @@ class User implements Stringable, IdentifiableEntityInterface
/** @var Collection<int, UserPasskey> */ /** @var Collection<int, UserPasskey> */
#[ #[
ORM\OneToMany(mappedBy: 'user', targetEntity: ApiKey::class), ORM\OneToMany(mappedBy: 'user', targetEntity: UserPasskey::class),
Groups([EntityGroupsInterface::GROUP_ADMIN, EntityGroupsInterface::GROUP_ALL]), Groups([EntityGroupsInterface::GROUP_ADMIN, EntityGroupsInterface::GROUP_ALL]),
DeepNormalize(true) DeepNormalize(true)
] ]

View File

@ -5,11 +5,11 @@ declare(strict_types=1);
namespace App\Enums; namespace App\Enums;
use App\Environment; use App\Environment;
use App\Exception\InvalidRequestAttribute;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Gettext\Translator; use Gettext\Translator;
use Gettext\TranslatorFunctions; use Gettext\TranslatorFunctions;
use Locale; use Locale;
use Psr\Http\Message\ServerRequestInterface;
enum SupportedLocales: string enum SupportedLocales: string
{ {
@ -126,14 +126,17 @@ enum SupportedLocales: string
public static function createFromRequest( public static function createFromRequest(
Environment $environment, Environment $environment,
ServerRequestInterface $request ServerRequest $request
): self { ): self {
$possibleLocales = []; $possibleLocales = [];
// Prefer user-based profile locale. // Prefer user-based profile locale.
$user = $request->getAttribute(ServerRequest::ATTR_USER); try {
if (null !== $user && !empty($user->getLocale()) && 'default' !== $user->getLocale()) { $user = $request->getUser();
$possibleLocales[] = $user->getLocale(); if (!empty($user->getLocale()) && 'default' !== $user->getLocale()) {
$possibleLocales[] = $user->getLocale();
}
} catch (InvalidRequestAttribute) {
} }
$serverParams = $request->getServerParams(); $serverParams = $request->getServerParams();

View File

@ -8,6 +8,7 @@ use App\Enums\ApplicationEnvironment;
use App\Enums\ReleaseChannel; use App\Enums\ReleaseChannel;
use App\Radio\Configuration; use App\Radio\Configuration;
use App\Utilities\File; use App\Utilities\File;
use App\Utilities\Types;
use GuzzleHttp\Psr7\Uri; use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
use Psr\Log\LogLevel; use Psr\Log\LogLevel;
@ -22,6 +23,7 @@ final class Environment
private readonly bool $isDocker; private readonly bool $isDocker;
private readonly ApplicationEnvironment $appEnv; private readonly ApplicationEnvironment $appEnv;
/** @var array<string, string|int|bool|float> */
private readonly array $data; private readonly array $data;
// Core settings values // Core settings values
@ -71,47 +73,18 @@ final class Environment
public const REDIS_PORT = 'REDIS_PORT'; public const REDIS_PORT = 'REDIS_PORT';
public const REDIS_DB = 'REDIS_DB'; 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 = []) public function __construct(array $elements = [])
{ {
$this->baseDir = dirname(__DIR__); $this->baseDir = dirname(__DIR__);
$this->parentDir = dirname($this->baseDir); $this->parentDir = dirname($this->baseDir);
$this->isDocker = file_exists($this->parentDir . '/.docker'); $this->isDocker = file_exists($this->parentDir . '/.docker');
$this->data = array_merge($this->defaults, $elements); $this->data = $elements;
$this->appEnv = ApplicationEnvironment::tryFrom($this->data[self::APP_ENV] ?? '') $this->appEnv = ApplicationEnvironment::tryFrom(
?? ApplicationEnvironment::default(); Types::string($this->data[self::APP_ENV] ?? null, '', true)
) ?? ApplicationEnvironment::default();
} }
/**
* @return mixed[]
*/
public function toArray(): array public function toArray(): array
{ {
return $this->data; return $this->data;
@ -122,6 +95,27 @@ final class Environment
return $this->appEnv; 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 public function isProduction(): bool
{ {
return ApplicationEnvironment::Production === $this->getAppEnvironmentEnum(); return ApplicationEnvironment::Production === $this->getAppEnvironmentEnum();
@ -139,47 +133,37 @@ final class Environment
public function showDetailedErrors(): bool public function showDetailedErrors(): bool
{ {
if (self::envToBool($this->data[self::SHOW_DETAILED_ERRORS] ?? false)) { return Types::bool(
return true; $this->data[self::SHOW_DETAILED_ERRORS] ?? null,
} !$this->isProduction(),
true
return !$this->isProduction(); );
}
public function isDocker(): bool
{
return $this->isDocker;
} }
public function isCli(): bool 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 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 public function getAssetUrl(): ?string
{ {
return $this->data[self::ASSET_URL] ?? ''; return Types::string(
} $this->data[self::ASSET_URL] ?? null,
'/static',
/** true
* @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;
} }
/** /**
@ -187,8 +171,11 @@ final class Environment
*/ */
public function getTempDirectory(): string public function getTempDirectory(): string
{ {
return $this->data[self::TEMP_DIR] return Types::string(
?? $this->getParentDirectory() . '/www_tmp'; $this->data[self::TEMP_DIR] ?? null,
$this->getParentDirectory() . '/www_tmp',
true
);
} }
/** /**
@ -196,10 +183,14 @@ final class Environment
*/ */
public function getUploadsDirectory(): string public function getUploadsDirectory(): string
{ {
return $this->data[self::UPLOADS_DIR] ?? File::getFirstExistingDirectory([ return Types::string(
$this->getParentDirectory() . '/storage/uploads', $this->data[self::UPLOADS_DIR] ?? null,
$this->getParentDirectory() . '/uploads', File::getFirstExistingDirectory([
]); $this->getParentDirectory() . '/storage/uploads',
$this->getParentDirectory() . '/uploads',
]),
true
);
} }
/** /**
@ -222,56 +213,65 @@ final class Environment
public function getLang(): ?string public function getLang(): ?string
{ {
return $this->data[self::LANG]; return Types::stringOrNull($this->data[self::LANG]);
} }
public function getReleaseChannelEnum(): ReleaseChannel 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(); ?? ReleaseChannel::default();
} }
public function getSftpPort(): int 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 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 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 public function getSyncShortExecutionTime(): int
{ {
return (int)( return Types::int(
$this->data[self::SYNC_SHORT_EXECUTION_TIME] ?? $this->defaults[self::SYNC_SHORT_EXECUTION_TIME] $this->data[self::SYNC_SHORT_EXECUTION_TIME] ?? null,
600
); );
} }
public function getSyncLongExecutionTime(): int public function getSyncLongExecutionTime(): int
{ {
return (int)( return Types::int(
$this->data[self::SYNC_LONG_EXECUTION_TIME] ?? $this->defaults[self::SYNC_LONG_EXECUTION_TIME] $this->data[self::SYNC_LONG_EXECUTION_TIME] ?? null,
1800
); );
} }
public function getNowPlayingDelayTime(): int public function getNowPlayingDelayTime(): int
{ {
return (int)( return Types::int($this->data[self::NOW_PLAYING_DELAY_TIME] ?? null);
$this->data[self::NOW_PLAYING_DELAY_TIME] ?? $this->defaults[self::NOW_PLAYING_DELAY_TIME]
);
} }
public function getNowPlayingMaxConcurrentProcesses(): int public function getNowPlayingMaxConcurrentProcesses(): int
{ {
return (int)( return Types::int(
$this->data[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES] $this->data[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES] ?? null,
?? $this->defaults[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES] 5
); );
} }
@ -280,9 +280,9 @@ final class Environment
*/ */
public function getLogLevel(): string public function getLogLevel(): string
{ {
if (!empty($this->data[self::LOG_LEVEL])) { $logLevelRaw = Types::stringOrNull($this->data[self::LOG_LEVEL] ?? null, true);
$loggingLevel = strtolower($this->data[self::LOG_LEVEL]); if (null !== $logLevelRaw) {
$loggingLevel = strtolower($logLevelRaw);
$allowedLogLevels = [ $allowedLogLevels = [
LogLevel::DEBUG, LogLevel::DEBUG,
LogLevel::INFO, 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 public function getDatabaseSettings(): array
{ {
$dbSettings = [ $dbSettings = [
'host' => $this->data[self::DB_HOST] ?? 'localhost', 'host' => Types::string(
'port' => (int)($this->data[self::DB_PORT] ?? 3306), $this->data[self::DB_HOST] ?? null,
'dbname' => $this->data[self::DB_NAME] ?? 'azuracast', 'localhost',
'user' => $this->data[self::DB_USER] ?? 'azuracast', true
'password' => $this->data[self::DB_PASSWORD] ?? 'azur4c457', ),
'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()) { if ('localhost' === $dbSettings['host'] && $this->isDocker()) {
@ -326,23 +352,42 @@ final class Environment
public function useLocalDatabase(): bool public function useLocalDatabase(): bool
{ {
return 'localhost' === ($this->data[self::DB_HOST] ?? 'localhost'); return 'localhost' === $this->getDatabaseSettings()['host'];
} }
public function enableRedis(): bool 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 public function getRedisSettings(): array
{ {
$redisSettings = [ $redisSettings = [
'host' => $this->data[self::REDIS_HOST] ?? 'localhost', 'host' => Types::string(
'port' => (int)($this->data[self::REDIS_PORT] ?? 6379), $this->data[self::REDIS_HOST] ?? null,
'db' => (int)($this->data[self::REDIS_DB] ?? 1), '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()) { if ('localhost' === $redisSettings['host'] && $this->isDocker()) {
@ -354,27 +399,47 @@ final class Environment
public function useLocalRedis(): bool 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 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 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 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 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 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 public static function getInstance(): Environment
{ {
return self::$instance; return self::$instance;

View File

@ -134,6 +134,10 @@ final class ErrorHandler extends SlimErrorHandler
return $response; return $response;
} }
if (!($this->request instanceof ServerRequest)) {
return parent::respond();
}
if ($this->exception instanceof HttpException) { if ($this->exception instanceof HttpException) {
/** @var Response $response */ /** @var Response $response */
$response = $this->responseFactory->createResponse($this->exception->getCode()); $response = $this->responseFactory->createResponse($this->exception->getCode());

View File

@ -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->request = $request;
$this->baseUrl = null; $this->baseUrl = null;

View File

@ -4,14 +4,13 @@ declare(strict_types=1);
namespace App\Http; namespace App\Http;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
interface RouterInterface 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; public function getBaseUrl(): UriInterface;

View File

@ -129,12 +129,15 @@ final class ServerRequest extends SlimServerRequest
} }
/** /**
* @template T of object
*
* @param string $attr * @param string $attr
* @param string $className * @param class-string<T> $className
* @return T
* *
* @throws InvalidRequestAttribute * @throws InvalidRequestAttribute
*/ */
private function getAttributeOfClass(string $attr, string $className): mixed private function getAttributeOfClass(string $attr, string $className): object
{ {
$object = $this->serverRequest->getAttribute($attr); $object = $this->serverRequest->getAttribute($attr);

View File

@ -12,6 +12,7 @@ use App\Installer\EnvFiles\AzuraCastEnvFile;
use App\Installer\EnvFiles\EnvFile; use App\Installer\EnvFiles\EnvFile;
use App\Radio\Configuration; use App\Radio\Configuration;
use App\Utilities\Strings; use App\Utilities\Strings;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -45,12 +46,12 @@ final class InstallCommand extends Command
{ {
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$baseDir = $input->getArgument('base-dir') ?? self::DEFAULT_BASE_DIRECTORY; $baseDir = Types::string($input->getArgument('base-dir'), self::DEFAULT_BASE_DIRECTORY);
$update = (bool)$input->getOption('update'); $update = Types::bool($input->getOption('update'));
$defaults = (bool)$input->getOption('defaults'); $defaults = Types::bool($input->getOption('defaults'));
$httpPort = $input->getOption('http-port'); $httpPort = Types::intOrNull($input->getOption('http-port'));
$httpsPort = $input->getOption('https-port'); $httpsPort = Types::intOrNull($input->getOption('https-port'));
$releaseChannel = $input->getOption('release-channel'); $releaseChannel = Types::stringOrNull($input->getOption('release-channel'));
$devMode = ($baseDir !== self::DEFAULT_BASE_DIRECTORY); $devMode = ($baseDir !== self::DEFAULT_BASE_DIRECTORY);
@ -222,7 +223,11 @@ final class InstallCommand extends Command
foreach ($simplePorts as $port) { foreach ($simplePorts as $port) {
$env[$port] = (int)$io->ask( $env[$port] = (int)$io->ask(
$envConfig[$port]['name'] . ' - ' . $envConfig[$port]['description'], sprintf(
'%s - %s',
$envConfig[$port]['name'],
$envConfig[$port]['description'] ?? ''
),
(string)$env[$port] (string)$env[$port]
); );
} }
@ -301,7 +306,7 @@ final class InstallCommand extends Command
$ports = $env['AZURACAST_STATION_PORTS'] ?? ''; $ports = $env['AZURACAST_STATION_PORTS'] ?? '';
$envConfig = $env::getConfiguration($this->environment); $envConfig = $env::getConfiguration($this->environment);
$defaultPorts = $envConfig['AZURACAST_STATION_PORTS']['default']; $defaultPorts = $envConfig['AZURACAST_STATION_PORTS']['default'] ?? '';
if (!empty($ports) && 0 !== strcmp($ports, $defaultPorts)) { if (!empty($ports) && 0 !== strcmp($ports, $defaultPorts)) {
$yamlPorts = []; $yamlPorts = [];

View File

@ -6,6 +6,7 @@ namespace App\Installer\EnvFiles;
use App\Environment; use App\Environment;
use App\Utilities\Strings; use App\Utilities\Strings;
use App\Utilities\Types;
use ArrayAccess; use ArrayAccess;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
@ -58,10 +59,7 @@ abstract class AbstractEnvFile implements ArrayAccess
public function getAsBool(string $key, bool $default): bool public function getAsBool(string $key, bool $default): bool
{ {
if (isset($this->data[$key])) { return Types::bool($this->data[$key], $default, true);
return Environment::envToBool($this->data[$key]);
}
return $default;
} }
public function offsetExists(mixed $offset): bool public function offsetExists(mixed $offset): bool
@ -96,7 +94,7 @@ abstract class AbstractEnvFile implements ArrayAccess
]; ];
foreach (static::getConfiguration($environment) as $key => $keyInfo) { foreach (static::getConfiguration($environment) as $key => $keyInfo) {
$envFile[] = '# ' . ($keyInfo['name'] ?? $key); $envFile[] = sprintf('# %s', $keyInfo['name']);
if (!empty($keyInfo['description'])) { if (!empty($keyInfo['description'])) {
$desc = Strings::mbWordwrap($keyInfo['description']); $desc = Strings::mbWordwrap($keyInfo['description']);
@ -191,7 +189,7 @@ abstract class AbstractEnvFile implements ArrayAccess
} }
/** /**
* @return mixed[] * @return array<string, array{name: string, description?: string, options?: array, default?: string, required?: bool}>
*/ */
abstract public static function getConfiguration(Environment $environment): array; abstract public static function getConfiguration(Environment $environment): array;

View File

@ -0,0 +1,26 @@
<?php
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;
use TypeError;
abstract class AbstractMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if (!($request instanceof ServerRequest)) {
throw new TypeError('Invalid server request.');
}
return $this->__invoke($request, $handler);
}
abstract public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface;
}

View File

@ -4,21 +4,16 @@ declare(strict_types=1);
namespace App\Middleware; namespace App\Middleware;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Apply the "X-Forwarded-Proto" header if it exists. * Apply the "X-Forwarded-Proto" header if it exists.
*/ */
final class ApplyXForwardedProto implements MiddlewareInterface final class ApplyXForwardedProto extends AbstractMiddleware
{ {
/** public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
if ($request->hasHeader('X-Forwarded-Proto')) { if ($request->hasHeader('X-Forwarded-Proto')) {
$uri = $request->getUri(); $uri = $request->getUri();

View File

@ -9,13 +9,13 @@ use App\Container\EnvironmentAwareTrait;
use App\Customization; use App\Customization;
use App\Entity\AuditLog; use App\Entity\AuditLog;
use App\Entity\Repository\UserRepository; use App\Entity\Repository\UserRepository;
use App\Exception\InvalidRequestAttribute;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Middleware\AbstractMiddleware;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
abstract class AbstractAuth implements MiddlewareInterface abstract class AbstractAuth extends AbstractMiddleware
{ {
use EnvironmentAwareTrait; 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); $customization = $this->customization->withRequest($request);
@ -39,7 +39,12 @@ abstract class AbstractAuth implements MiddlewareInterface
->withAttribute(ServerRequest::ATTR_ACL, $acl); ->withAttribute(ServerRequest::ATTR_ACL, $acl);
// Set the Audit Log user. // 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); $response = $handler->handle($request);

View File

@ -13,7 +13,6 @@ use App\Entity\User;
use App\Exception\CsrfValidationException; use App\Exception\CsrfValidationException;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Security\SplitToken; use App\Security\SplitToken;
use App\Session\Csrf;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
@ -31,17 +30,17 @@ final class ApiAuth extends AbstractAuth
parent::__construct($userRepo, $acl, $customization); 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. // Initialize the Auth for this request.
$user = $this->getApiUser($request); $user = $this->getApiUser($request);
$request = $request->withAttribute(ServerRequest::ATTR_USER, $user); $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); $apiKey = $this->getApiKey($request);
@ -55,7 +54,7 @@ final class ApiAuth extends AbstractAuth
// Fallback to session login if available. // Fallback to session login if available.
$auth = new Auth( $auth = new Auth(
userRepo: $this->userRepo, userRepo: $this->userRepo,
session: $request->getAttribute(ServerRequest::ATTR_SESSION), session: $request->getSession(),
); );
$auth->setEnvironment($this->environment); $auth->setEnvironment($this->environment);
@ -70,14 +69,11 @@ final class ApiAuth extends AbstractAuth
return null; return null;
} }
$csrf = $request->getAttribute(ServerRequest::ATTR_SESSION_CSRF); $csrf = $request->getCsrf();
try {
if ($csrf instanceof Csrf) { $csrf->verify($csrfKey, self::API_CSRF_NAMESPACE);
try { return $user;
$csrf->verify($csrfKey, self::API_CSRF_NAMESPACE); } catch (CsrfValidationException) {
return $user;
} catch (CsrfValidationException) {
}
} }
} }

View File

@ -7,17 +7,16 @@ namespace App\Middleware\Auth;
use App\Auth; use App\Auth;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
final class StandardAuth extends AbstractAuth 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. // Initialize the Auth for this request.
$auth = new Auth( $auth = new Auth(
userRepo: $this->userRepo, userRepo: $this->userRepo,
session: $request->getAttribute(ServerRequest::ATTR_SESSION), session: $request->getSession(),
); );
$auth->setEnvironment($this->environment); $auth->setEnvironment($this->environment);
@ -27,6 +26,6 @@ final class StandardAuth extends AbstractAuth
->withAttribute(ServerRequest::ATTR_AUTH, $auth) ->withAttribute(ServerRequest::ATTR_AUTH, $auth)
->withAttribute(ServerRequest::ATTR_USER, $user); ->withAttribute(ServerRequest::ATTR_USER, $user);
return parent::process($request, $handler); return parent::__invoke($request, $handler);
} }
} }

View File

@ -7,21 +7,19 @@ namespace App\Middleware;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\View; use App\View;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Inject the view object into the request and prepare it for rendering templates. * 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( public function __construct(
private readonly View $view 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); $view = $this->view->withRequest($request);

View File

@ -5,17 +5,16 @@ declare(strict_types=1);
namespace App\Middleware; namespace App\Middleware;
use App\Container\SettingsAwareTrait; use App\Container\SettingsAwareTrait;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Slim\App; use Slim\App;
/** /**
* Remove trailing slash from all URLs when routing. * Remove trailing slash from all URLs when routing.
*/ */
final class EnforceSecurity implements MiddlewareInterface final class EnforceSecurity extends AbstractMiddleware
{ {
use SettingsAwareTrait; use SettingsAwareTrait;
@ -27,11 +26,7 @@ final class EnforceSecurity implements MiddlewareInterface
$this->responseFactory = $app->getResponseFactory(); $this->responseFactory = $app->getResponseFactory();
} }
/** public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$alwaysUseSsl = $this->readSettings()->getAlwaysUseSsl(); $alwaysUseSsl = $this->readSettings()->getAlwaysUseSsl();

View File

@ -12,14 +12,12 @@ use App\Entity\AuditLog;
use App\Entity\Repository\UserRepository; use App\Entity\Repository\UserRepository;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Get the current user entity object and assign it into the request if it exists. * 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; 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. // Initialize the Auth for this request.
$auth = new Auth( $auth = new Auth(
userRepo: $this->userRepo, userRepo: $this->userRepo,
session: $request->getAttribute(ServerRequest::ATTR_SESSION), session: $request->getSession(),
); );
$auth->setEnvironment($this->environment); $auth->setEnvironment($this->environment);
@ -43,8 +41,7 @@ final class GetCurrentUser implements MiddlewareInterface
$request = $request $request = $request
->withAttribute(ServerRequest::ATTR_AUTH, $auth) ->withAttribute(ServerRequest::ATTR_AUTH, $auth)
->withAttribute(ServerRequest::ATTR_USER, $user) ->withAttribute(ServerRequest::ATTR_USER, $user);
->withAttribute('is_logged_in', (null !== $user));
// Initialize Customization (timezones, locales, etc) based on the current logged in user. // Initialize Customization (timezones, locales, etc) based on the current logged in user.
$customization = $this->customization->withRequest($request); $customization = $this->customization->withRequest($request);

View File

@ -8,22 +8,20 @@ use App\Entity\Repository\StationRepository;
use App\Entity\Station; use App\Entity\Station;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext; use Slim\Routing\RouteContext;
/** /**
* Retrieve the station specified in the request parameters, and throw an error if none exists but one is required. * 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( public function __construct(
private readonly StationRepository $stationRepo 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(); $routeArgs = RouteContext::fromRequest($request)->getRoute()?->getArguments();

View File

@ -4,10 +4,9 @@ declare(strict_types=1);
namespace App\Middleware; namespace App\Middleware;
use App\Http\ServerRequest;
use JsonException; use JsonException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use const JSON_THROW_ON_ERROR; 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 * attribute of the PSR-7 request. This implementation is transparent to any controllers
* using this code. * using this code.
*/ */
final class HandleMultipartJson implements MiddlewareInterface final class HandleMultipartJson extends AbstractMiddleware
{ {
/** public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$parsedBody = $request->getParsedBody(); $parsedBody = $request->getParsedBody();

View File

@ -7,25 +7,19 @@ namespace App\Middleware;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\RateLimit; use App\RateLimit;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Inject core services into the request object for use further down the stack. * 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( public function __construct(
private readonly RateLimit $rateLimit private readonly RateLimit $rateLimit
) { ) {
} }
/** public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$request = $request->withAttribute(ServerRequest::ATTR_RATE_LIMIT, $this->rateLimit); $request = $request->withAttribute(ServerRequest::ATTR_RATE_LIMIT, $this->rateLimit);

View File

@ -7,25 +7,19 @@ namespace App\Middleware;
use App\Http\RouterInterface; use App\Http\RouterInterface;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Set the current route on the URL object, and inject the URL object into the router. * 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( public function __construct(
private readonly RouterInterface $router private readonly RouterInterface $router
) { ) {
} }
/** public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{ {
$router = $this->router->withRequest($request); $router = $this->router->withRequest($request);

View File

@ -15,8 +15,6 @@ use Mezzio\Session\LazySession;
use Mezzio\Session\SessionPersistenceInterface; use Mezzio\Session\SessionPersistenceInterface;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter;
@ -24,7 +22,7 @@ use Symfony\Component\Cache\Adapter\ProxyAdapter;
/** /**
* Inject the session object into the request. * Inject the session object into the request.
*/ */
final class InjectSession implements MiddlewareInterface final class InjectSession extends AbstractMiddleware
{ {
use SettingsAwareTrait; use SettingsAwareTrait;
@ -41,7 +39,7 @@ final class InjectSession implements MiddlewareInterface
$this->cachePool = new ProxyAdapter($dbCache, 'session.'); $this->cachePool = new ProxyAdapter($dbCache, 'session.');
} }
public function getSessionPersistence(ServerRequestInterface $request): SessionPersistenceInterface public function getSessionPersistence(ServerRequest $request): SessionPersistenceInterface
{ {
$alwaysUseSsl = $this->readSettings()->getAlwaysUseSsl(); $alwaysUseSsl = $this->readSettings()->getAlwaysUseSsl();
$isHttpsUrl = ('https' === $request->getUri()->getScheme()); $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); $sessionPersistence = $this->getSessionPersistence($request);
$session = new LazySession($sessionPersistence, $request); $session = new LazySession($sessionPersistence, $request);

View File

@ -7,8 +7,10 @@ namespace App\Middleware\Module;
use App\Container\EnvironmentAwareTrait; use App\Container\EnvironmentAwareTrait;
use App\Container\SettingsAwareTrait; use App\Container\SettingsAwareTrait;
use App\Entity\User; use App\Entity\User;
use App\Exception\InvalidRequestAttribute;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Middleware\AbstractMiddleware;
use App\Utilities\Urls; use App\Utilities\Urls;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
@ -20,7 +22,7 @@ use Symfony\Component\VarDumper\VarDumper;
/** /**
* Handle API calls and wrap exceptions in JSON formatting. * Handle API calls and wrap exceptions in JSON formatting.
*/ */
final class Api final class Api extends AbstractMiddleware
{ {
use EnvironmentAwareTrait; use EnvironmentAwareTrait;
use SettingsAwareTrait; use SettingsAwareTrait;
@ -40,7 +42,11 @@ final class Api
} }
// Attempt API key auth if a key exists. // 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. // Set default cache control for API pages.
$settings = $this->readSettings(); $settings = $this->readSettings();

View File

@ -7,6 +7,7 @@ namespace App\Middleware\Module;
use App\Container\EnvironmentAwareTrait; use App\Container\EnvironmentAwareTrait;
use App\Enums\GlobalPermissions; use App\Enums\GlobalPermissions;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Middleware\AbstractMiddleware;
use App\Middleware\Auth\ApiAuth; use App\Middleware\Auth\ApiAuth;
use App\Version; use App\Version;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -15,7 +16,7 @@ use Psr\Http\Server\RequestHandlerInterface;
use const PHP_MAJOR_VERSION; use const PHP_MAJOR_VERSION;
use const PHP_MINOR_VERSION; use const PHP_MINOR_VERSION;
final class PanelLayout final class PanelLayout extends AbstractMiddleware
{ {
use EnvironmentAwareTrait; use EnvironmentAwareTrait;

View File

@ -14,7 +14,7 @@ use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Get the current user entity object and assign it into the request if it exists. * 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( public function __construct(
private readonly string|PermissionInterface $action, private readonly string|PermissionInterface $action,

View File

@ -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. * 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( public function __construct(
private readonly string $rlGroup = 'default', private readonly string $rlGroup = 'default',

View File

@ -4,18 +4,17 @@ declare(strict_types=1);
namespace App\Middleware; namespace App\Middleware;
use App\Http\ServerRequest;
use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Remove trailing slash from all URLs when routing. * 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(); $uri = $request->getUri();
$path = $uri->getPath(); $path = $uri->getPath();

View File

@ -6,12 +6,11 @@ namespace App\Middleware;
use App\Container\EnvironmentAwareTrait; use App\Container\EnvironmentAwareTrait;
use App\Doctrine\DecoratedEntityManager; use App\Doctrine\DecoratedEntityManager;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
final class ReopenEntityManagerMiddleware implements MiddlewareInterface final class ReopenEntityManagerMiddleware extends AbstractMiddleware
{ {
use EnvironmentAwareTrait; 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(); $this->em->open();

View File

@ -14,7 +14,7 @@ use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Require that the user be logged in to view this page. * 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 public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{ {

View File

@ -21,7 +21,7 @@ use Slim\Routing\RouteContext;
/** /**
* Require that the podcast has a published episode for public access * Require that the podcast has a published episode for public access
*/ */
final class RequirePublishedPodcastEpisodeMiddleware final class RequirePublishedPodcastEpisodeMiddleware extends AbstractMiddleware
{ {
public function __construct( public function __construct(
private readonly PodcastRepository $podcastRepository private readonly PodcastRepository $podcastRepository

View File

@ -13,7 +13,7 @@ use Psr\Http\Server\RequestHandlerInterface;
/** /**
* Require that the user be logged in to view this page. * 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 public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{ {

View File

@ -10,7 +10,7 @@ use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
final class StationSupportsFeature final class StationSupportsFeature extends AbstractMiddleware
{ {
public function __construct( public function __construct(
private readonly StationFeatures $feature private readonly StationFeatures $feature

View File

@ -5,18 +5,17 @@ declare(strict_types=1);
namespace App\Middleware; namespace App\Middleware;
use App\Exception\WrappedException; use App\Exception\WrappedException;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface; use Psr\Http\Server\RequestHandlerInterface;
use Throwable; use Throwable;
/** /**
* Wrap all exceptions thrown past this point with rich metadata. * 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 { try {
return $handler->handle($request); return $handler->handle($request);

View File

@ -110,6 +110,7 @@ final class HlsListeners
} }
try { try {
/** @var array<array-key, string> $rowJson */
$rowJson = json_decode($row, true, 512, JSON_THROW_ON_ERROR); $rowJson = json_decode($row, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException) { } catch (JsonException) {
return null; return null;

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App; namespace App;
use App\Exception\InvalidRequestAttribute;
use App\Http\Response; use App\Http\Response;
use App\Http\RouterInterface; use App\Http\RouterInterface;
use App\Http\ServerRequest; use App\Http\ServerRequest;
@ -19,7 +20,6 @@ use Pagerfanta\Doctrine\Collections\CollectionAdapter;
use Pagerfanta\Doctrine\ORM\QueryAdapter; use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Pagerfanta; use Pagerfanta\Pagerfanta;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/** /**
* @template TKey of array-key * @template TKey of array-key
@ -47,11 +47,16 @@ final class Paginator implements IteratorAggregate, Countable
*/ */
public function __construct( public function __construct(
private readonly Pagerfanta $paginator, 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); $this->isAuthenticated = ($user !== null);
$params = $request->getQueryParams(); $params = $request->getQueryParams();
@ -194,7 +199,7 @@ final class Paginator implements IteratorAggregate, Countable
*/ */
public static function fromAdapter( public static function fromAdapter(
AdapterInterface $adapter, AdapterInterface $adapter,
ServerRequestInterface $request ServerRequest $request
): self { ): self {
return new self( return new self(
new Pagerfanta($adapter), new Pagerfanta($adapter),
@ -209,7 +214,7 @@ final class Paginator implements IteratorAggregate, Countable
* @param array<XKey, X> $input * @param array<XKey, X> $input
* @return static<XKey, X> * @return static<XKey, X>
*/ */
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); return self::fromAdapter(new ArrayAdapter($input), $request);
} }
@ -221,7 +226,7 @@ final class Paginator implements IteratorAggregate, Countable
* @param Collection<XKey, X> $collection * @param Collection<XKey, X> $collection
* @return static<XKey, X> * @return static<XKey, X>
*/ */
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); return self::fromAdapter(new CollectionAdapter($collection), $request);
} }
@ -229,7 +234,7 @@ final class Paginator implements IteratorAggregate, Countable
/** /**
* @return static<int, mixed> * @return static<int, mixed>
*/ */
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); return self::fromAdapter(new QueryAdapter($qb), $request);
} }
@ -237,7 +242,7 @@ final class Paginator implements IteratorAggregate, Countable
/** /**
* @return static<int, mixed> * @return static<int, mixed>
*/ */
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); return self::fromAdapter(new QueryAdapter($query), $request);
} }

View File

@ -7,6 +7,7 @@ namespace App\Radio\Backend\Liquidsoap\Command;
use App\Container\LoggerAwareTrait; use App\Container\LoggerAwareTrait;
use App\Entity\Station; use App\Entity\Station;
use App\Radio\Enums\BackendAdapters; use App\Radio\Enums\BackendAdapters;
use App\Utilities\Types;
use Monolog\LogRecord; use Monolog\LogRecord;
use ReflectionClass; use ReflectionClass;
use Throwable; use Throwable;
@ -54,7 +55,7 @@ abstract class AbstractCommand
return 'false'; return 'false';
} }
return (string)$result; return Types::string($result);
} catch (Throwable $e) { } catch (Throwable $e) {
$this->logger->error( $this->logger->error(
sprintf( sprintf(

View File

@ -352,6 +352,7 @@ final class Configuration
DQL DQL
)->getArrayResult(); )->getArrayResult();
/** @var array<array-key, int|string|array> $row */
foreach ($stationConfigs as $row) { foreach ($stationConfigs as $row) {
$stationReference = ['id' => $row['id'], 'name' => $row['name']]; $stationReference = ['id' => $row['id'], 'name' => $row['name']];

View File

@ -205,9 +205,10 @@ final class CheckMediaTask extends AbstractTask
DQL DQL
)->setParameter('storageLocation', $storageLocation); )->setParameter('storageLocation', $storageLocation);
/** @var array<array-key, int|string> $mediaRow */
foreach ($existingMediaQuery->toIterable([], AbstractQuery::HYDRATE_ARRAY) as $mediaRow) { foreach ($existingMediaQuery->toIterable([], AbstractQuery::HYDRATE_ARRAY) as $mediaRow) {
// Check if media file still exists. // Check if media file still exists.
$path = $mediaRow['path']; $path = (string)$mediaRow['path'];
$pathHash = md5($path); $pathHash = md5($path);
if (isset($musicFiles[$pathHash])) { if (isset($musicFiles[$pathHash])) {
@ -216,11 +217,11 @@ final class CheckMediaTask extends AbstractTask
if ( if (
empty($mediaRow['unique_id']) empty($mediaRow['unique_id'])
|| StationMedia::needsReprocessing($mtime, $mediaRow['mtime'] ?? 0) || StationMedia::needsReprocessing($mtime, (int)$mediaRow['mtime'])
) { ) {
$message = new ReprocessMediaMessage(); $message = new ReprocessMediaMessage();
$message->storage_location_id = $storageLocation->getIdRequired(); $message->storage_location_id = $storageLocation->getIdRequired();
$message->media_id = $mediaRow['id']; $message->media_id = (int)$mediaRow['id'];
$message->force = empty($mediaRow['unique_id']); $message->force = empty($mediaRow['unique_id']);
$this->messageBus->dispatch($message); $this->messageBus->dispatch($message);

View File

@ -52,7 +52,10 @@ class Module extends Framework implements DoctrineProvider
} }
$this->container = $container; $this->container = $container;
$this->em = $this->container->get(ReloadableEntityManagerInterface::class);
/** @var ReloadableEntityManagerInterface $em */
$em = $this->container->get(ReloadableEntityManagerInterface::class);
$this->em = $em;
parent::_initialize(); parent::_initialize();
} }

View File

@ -4,18 +4,18 @@ declare(strict_types=1);
namespace App\Traits; namespace App\Traits;
use Psr\Http\Message\ServerRequestInterface; use App\Http\ServerRequest;
trait RequestAwareTrait 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; $this->request = $request;
} }
public function withRequest(?ServerRequestInterface $request): self public function withRequest(?ServerRequest $request): self
{ {
$newInstance = clone $this; $newInstance = clone $this;
$newInstance->setRequest($request); $newInstance->setRequest($request);

View File

@ -26,9 +26,10 @@ final class JsonGenerator extends Generator
public function generateArray(Translations $translations): array public function generateArray(Translations $translations): array
{ {
$pluralForm = $translations->getHeaders()->getPluralForm(); $pluralForm = $translations->getHeaders()->getPluralForm();
$pluralSize = is_array($pluralForm) ? ($pluralForm[0] - 1) : null; $pluralSize = is_array($pluralForm) ? (int)($pluralForm[0] - 1) : null;
$messages = []; $messages = [];
/** @var Translation $translation */
foreach ($translations as $translation) { foreach ($translations as $translation) {
if (!$translation->getTranslation() || $translation->isDisabled()) { if (!$translation->getTranslation() || $translation->isDisabled()) {
continue; continue;

View File

@ -9,29 +9,11 @@ use voku\helper\UTF8;
final class Strings 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) * Truncate text (adding "..." if needed)
*/ */
public static function truncateText(string $text, int $limit = 80, string $pad = '...'): string public static function truncateText(string $text, int $limit = 80, string $pad = '...'): string
{ {
mb_internal_encoding('UTF-8');
if (mb_strlen($text) <= $limit) { if (mb_strlen($text) <= $limit) {
return $text; return $text;
} }

107
src/Utilities/Types.php Normal file
View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace App\Utilities;
final class Types
{
public static function string(
mixed $input,
string $defaultIfNull = '',
bool $countEmptyAsNull = false
): string {
return self::stringOrNull($input, $countEmptyAsNull) ?? $defaultIfNull;
}
public static function stringOrNull(
mixed $input,
bool $countEmptyAsNull = false
): ?string {
if (null === $input) {
return null;
}
if ($countEmptyAsNull) {
if ('' === $input) {
return null;
}
$input = trim((string)$input);
return (!empty($input)) ? $input : null;
}
return (string)$input;
}
public static function int(mixed $input, int $defaultIfNull = 0): int
{
return self::intOrNull($input) ?? $defaultIfNull;
}
public static function intOrNull(mixed $input): ?int
{
if (null === $input || is_int($input)) {
return $input;
}
return (is_numeric($input))
? (int)$input
: null;
}
public static function float(mixed $input, float $defaultIfNull = 0.0): float
{
return self::floatOrNull($input) ?? $defaultIfNull;
}
public static function floatOrNull(mixed $input): ?float
{
if (null === $input || is_float($input)) {
return $input;
}
return (is_numeric($input))
? (float)$input
: null;
}
public static function bool(mixed $input, bool $defaultIfNull = false, bool $broadenValidBools = false): bool
{
return self::boolOrNull($input, $broadenValidBools) ?? $defaultIfNull;
}
public static function boolOrNull(mixed $input, bool $broadenValidBools = false): ?bool
{
if (null === $input || is_bool($input)) {
return $input;
}
if (is_int($input)) {
return 0 !== $input;
}
if ($broadenValidBools) {
$value = trim((string)$input);
return str_starts_with(strtolower($value), 'y')
|| 'true' === strtolower($value)
|| '1' === $value;
}
return (bool)$input;
}
public static function array(mixed $input, array $defaultIfNull = []): array
{
return self::arrayOrNull($input) ?? $defaultIfNull;
}
public static function arrayOrNull(mixed $input): ?array
{
if (null === $input || is_array($input)) {
return $input;
}
return (array)$input;
}
}

View File

@ -53,7 +53,7 @@ final class Version
/** /**
* Load cache or generate new repository details from the underlying Git repository. * Load cache or generate new repository details from the underlying Git repository.
* *
* @return mixed[] * @return string[]
*/ */
public function getDetails(): array public function getDetails(): array
{ {

View File

@ -17,7 +17,6 @@ use League\Plates\Engine;
use League\Plates\Template\Data; use League\Plates\Template\Data;
use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use stdClass; use stdClass;
use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper; use Symfony\Component\VarDumper\Dumper\CliDumper;
@ -28,7 +27,7 @@ final class View extends Engine
private GlobalSections $sections; private GlobalSections $sections;
/** @var ArrayCollection<string, array|object|string|int> */ /** @var ArrayCollection<string, array|object|string|int|bool> */
private ArrayCollection $globalProps; private ArrayCollection $globalProps;
public function __construct( public function __construct(
@ -151,7 +150,7 @@ final class View extends Engine
$dispatcher->dispatch(new Event\BuildView($this)); $dispatcher->dispatch(new Event\BuildView($this));
} }
public function setRequest(?ServerRequestInterface $request): void public function setRequest(?ServerRequest $request): void
{ {
$this->request = $request; $this->request = $request;
@ -169,7 +168,7 @@ final class View extends Engine
} }
$customization = $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION); $customization = $request->getAttribute(ServerRequest::ATTR_CUSTOMIZATION);
if (null !== $customization) { if ($customization instanceof Customization) {
$requestData['customization'] = $customization; $requestData['customization'] = $customization;
$this->globalProps->set( $this->globalProps->set(
@ -246,7 +245,7 @@ final class View extends Engine
return $this->sections; return $this->sections;
} }
/** @return ArrayCollection<string, array|object|string|int> */ /** @return ArrayCollection<string, array|object|string|int|bool> */
public function getGlobalProps(): ArrayCollection public function getGlobalProps(): ArrayCollection
{ {
return $this->globalProps; return $this->globalProps;

View File

@ -94,7 +94,7 @@ abstract class AbstractConnector implements ConnectorInterface
// Replaces {{ var.name }} with the flattened $values['var.name'] // Replaces {{ var.name }} with the flattened $values['var.name']
$vars[$varKey] = preg_replace_callback( $vars[$varKey] = preg_replace_callback(
"/\{\{(\s*)([a-zA-Z\d\-_.]+)(\s*)}}/", "/\{\{(\s*)([a-zA-Z\d\-_.]+)(\s*)}}/",
static function ($matches) use ($values) { static function (array $matches) use ($values): string {
$innerValue = strtolower(trim($matches[2])); $innerValue = strtolower(trim($matches[2]));
return $values[$innerValue] ?? ''; 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. * 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( $uri = Utilities\Urls::tryParseUserUrl(
$urlString, $urlString,
'Webhook' 'Webhook'

View File

@ -8,6 +8,7 @@ use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StationWebhook; use App\Entity\StationWebhook;
use App\Service\Mail; use App\Service\Mail;
use App\Utilities\Types;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use RuntimeException; use RuntimeException;
@ -34,11 +35,11 @@ final class Email extends AbstractConnector
} }
$config = $webhook->getConfig(); $config = $webhook->getConfig();
$emailTo = $config['to']; $emailTo = Types::stringOrNull($config['to'], true);
$emailSubject = $config['subject']; $emailSubject = Types::stringOrNull($config['subject'], true);
$emailBody = $config['message']; $emailBody = Types::stringOrNull($config['message'], true);
if (empty($emailTo) || empty($emailSubject) || empty($emailBody)) { if (null === $emailTo || null === $emailSubject || null === $emailBody) {
throw $this->incompleteConfigException($webhook); throw $this->incompleteConfigException($webhook);
} }

View File

@ -7,6 +7,7 @@ namespace App\Webhook\Connector;
use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StationWebhook; use App\Entity\StationWebhook;
use App\Utilities\Types;
final class Generic extends AbstractConnector final class Generic extends AbstractConnector
{ {
@ -32,7 +33,7 @@ final class Generic extends AbstractConnector
'Content-Type' => 'application/json', 'Content-Type' => 'application/json',
], ],
'json' => $np, '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'])) { if (!empty($config['basic_auth_username']) && !empty($config['basic_auth_password'])) {

View File

@ -7,6 +7,7 @@ namespace App\Webhook\Connector;
use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StationWebhook; use App\Entity\StationWebhook;
use App\Utilities\Types;
use Br33f\Ga4\MeasurementProtocol\Dto\Event\BaseEvent; use Br33f\Ga4\MeasurementProtocol\Dto\Event\BaseEvent;
use Br33f\Ga4\MeasurementProtocol\Dto\Request\BaseRequest; use Br33f\Ga4\MeasurementProtocol\Dto\Request\BaseRequest;
use Br33f\Ga4\MeasurementProtocol\HttpClient as Ga4HttpClient; use Br33f\Ga4\MeasurementProtocol\HttpClient as Ga4HttpClient;
@ -25,7 +26,10 @@ final class GoogleAnalyticsV4 extends AbstractGoogleAnalyticsConnector
): void { ): void {
$config = $webhook->getConfig(); $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); throw $this->incompleteConfigException($webhook);
} }
@ -36,7 +40,7 @@ final class GoogleAnalyticsV4 extends AbstractGoogleAnalyticsConnector
$gaHttpClient = new Ga4HttpClient(); $gaHttpClient = new Ga4HttpClient();
$gaHttpClient->setClient($this->httpClient); $gaHttpClient->setClient($this->httpClient);
$ga4Service = new Service($config['api_secret'], $config['measurement_id']); $ga4Service = new Service($apiSecret, $measurementId);
$ga4Service->setHttpClient($gaHttpClient); $ga4Service->setHttpClient($gaHttpClient);
// Get all current listeners // Get all current listeners

View File

@ -7,6 +7,7 @@ namespace App\Webhook\Connector;
use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StationWebhook; use App\Entity\StationWebhook;
use App\Utilities\Types;
use App\Utilities\Urls; use App\Utilities\Urls;
/** /**
@ -30,15 +31,15 @@ final class Mastodon extends AbstractSocialConnector
): void { ): void {
$config = $webhook->getConfig(); $config = $webhook->getConfig();
$instanceUrl = trim($config['instance_url'] ?? ''); $instanceUrl = Types::stringOrNull($config['instance_url'], true);
$accessToken = trim($config['access_token'] ?? ''); $accessToken = Types::stringOrNull($config['access_token'], true);
if (empty($instanceUrl) || empty($accessToken)) { if (null === $instanceUrl || null === $accessToken) {
throw $this->incompleteConfigException($webhook); throw $this->incompleteConfigException($webhook);
} }
$instanceUri = Urls::parseUserUrl($instanceUrl, 'Mastodon Instance URL'); $instanceUri = Urls::parseUserUrl($instanceUrl, 'Mastodon Instance URL');
$visibility = $config['visibility'] ?? 'public'; $visibility = Types::stringOrNull($config['visibility'], true) ?? 'public';
$this->logger->debug( $this->logger->debug(
'Posting to Mastodon...', 'Posting to Mastodon...',

View File

@ -9,6 +9,7 @@ use App\Entity\Repository\ListenerRepository;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StationWebhook; use App\Entity\StationWebhook;
use App\Http\RouterInterface; use App\Http\RouterInterface;
use App\Utilities\Types;
use App\Utilities\Urls; use App\Utilities\Urls;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Psr\Http\Message\UriInterface; use Psr\Http\Message\UriInterface;
@ -39,7 +40,10 @@ final class MatomoAnalytics extends AbstractConnector
): void { ): void {
$config = $webhook->getConfig(); $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); throw $this->incompleteConfigException($webhook);
} }
@ -66,17 +70,17 @@ final class MatomoAnalytics extends AbstractConnector
// Build Matomo URI // Build Matomo URI
$apiUrl = Urls::parseUserUrl( $apiUrl = Urls::parseUserUrl(
$config['matomo_url'], $matomoUrl,
'Matomo Analytics URL', 'Matomo Analytics URL',
)->withPath('/matomo.php'); )->withPath('/matomo.php');
$apiToken = $config['token'] ?? null; $apiToken = Types::stringOrNull($config['token'], true);
$stationName = $station->getName(); $stationName = $station->getName();
// Get all current listeners // Get all current listeners
$liveListeners = $this->listenerRepo->iterateLiveListenersArray($station); $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; $i = 0;
$entries = []; $entries = [];
@ -98,7 +102,7 @@ final class MatomoAnalytics extends AbstractConnector
} }
$entry = [ $entry = [
'idsite' => (int)$config['site_id'], 'idsite' => $siteId,
'rec' => 1, 'rec' => 1,
'action_name' => 'Listeners / ' . $stationName . ' / ' . $streamName, 'action_name' => 'Listeners / ' . $stationName . ' / ' . $streamName,
'url' => $listenerUrl, 'url' => $listenerUrl,

View File

@ -7,6 +7,7 @@ namespace App\Webhook\Connector;
use App\Entity\Api\NowPlaying\NowPlaying; use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station; use App\Entity\Station;
use App\Entity\StationWebhook; use App\Entity\StationWebhook;
use App\Utilities\Types;
/** /**
* Telegram web hook connector. * Telegram web hook connector.
@ -26,10 +27,10 @@ final class Telegram extends AbstractConnector
): void { ): void {
$config = $webhook->getConfig(); $config = $webhook->getConfig();
$botToken = trim($config['bot_token'] ?? ''); $botToken = Types::stringOrNull($config['bot_token'], true);
$chatId = trim($config['chat_id'] ?? ''); $chatId = Types::stringOrNull($config['chat_id'], true);
if (empty($botToken) || empty($chatId)) { if (null === $botToken || null === $chatId) {
throw $this->incompleteConfigException($webhook); throw $this->incompleteConfigException($webhook);
} }
@ -40,13 +41,17 @@ final class Telegram extends AbstractConnector
$np $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'; $webhookUrl = $apiUrl . '/bot' . $botToken . '/sendMessage';
$requestParams = [ $requestParams = [
'chat_id' => $chatId, 'chat_id' => $chatId,
'text' => $messages['text'], '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( $response = $this->httpClient->request(

View File

@ -1,8 +1,11 @@
<?php <?php
/** @var Psr\Http\Message\RequestInterface $request */
/** @var App\Session\Flash|null $flashObj */ /** @var \App\Http\ServerRequest $request */
$flashObj = $request->getAttribute(App\Http\ServerRequest::ATTR_SESSION_FLASH); try {
$flashObj = $request->getFlash();
} catch (App\Exception\InvalidRequestAttribute) {
$flashObj = null;
}
$notifies = []; $notifies = [];
?> ?>

18
util/phpstan-doctrine.php Normal file
View File

@ -0,0 +1,18 @@
<?php
/**
* PHPStan Bootstrap File
*/
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT);
ini_set('display_errors', 1);
require dirname(__DIR__) . '/vendor/autoload.php';
$tempDir = sys_get_temp_dir();
$ci = App\AppFactory::buildContainer([
App\Environment::TEMP_DIR => $tempDir,
App\Environment::UPLOADS_DIR => $tempDir,
]);
return $ci->get(Doctrine\ORM\EntityManagerInterface::class);

View File

@ -14,9 +14,11 @@ const AZURACAST_API_NAME = 'Testing API';
$tempDir = sys_get_temp_dir(); $tempDir = sys_get_temp_dir();
App\AppFactory::createCli( $app = App\AppFactory::createCli(
[ [
App\Environment::TEMP_DIR => $tempDir, App\Environment::TEMP_DIR => $tempDir,
App\Environment::UPLOADS_DIR => $tempDir, App\Environment::UPLOADS_DIR => $tempDir,
] ]
); );
return $app;