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_spaces_within_braces = true
ij_yaml_spaces_within_brackets = true
[*.neon]
indent_style = tab

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ namespace App\Controller\Frontend;
use App\Container\SettingsAwareTrait;
use App\Controller\SingleActionInterface;
use App\Entity\User;
use App\Exception\InvalidRequestAttribute;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
@ -27,9 +27,12 @@ final class IndexAction implements SingleActionInterface
}
// Redirect to login screen if the user isn't logged in.
$user = $request->getAttribute(ServerRequest::ATTR_USER);
try {
$request->getUser();
if (!($user instanceof User)) {
// Redirect to dashboard if no other custom redirection rules exist.
return $response->withRedirect($request->getRouter()->named('dashboard'));
} catch (InvalidRequestAttribute) {
// Redirect to a custom homepage URL if specified in settings.
$homepageRedirect = $settings->getHomepageRedirectUrl();
if (null !== $homepageRedirect) {
@ -38,8 +41,5 @@ final class IndexAction implements SingleActionInterface
return $response->withRedirect($request->getRouter()->named('account:login'));
}
// Redirect to dashboard if no other custom redirection rules exist.
return $response->withRedirect($request->getRouter()->named('dashboard'));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -137,7 +137,7 @@ final class SongHistoryRepository extends AbstractStationBasedRepository
* @param int $start
* @param int $end
*
* @return mixed[] [int $minimumListeners, int $maximumListeners, float $averageListeners]
* @return array{int, int, float}
*/
public function getStatsByTimeRange(Station $station, int $start, int $end): array
{

View File

@ -10,7 +10,7 @@ use App\Entity\Enums\IpSources;
use App\Enums\SupportedThemes;
use App\OpenApi;
use App\Service\Avatar;
use App\Utilities\Strings;
use App\Utilities\Types;
use App\Utilities\Urls;
use Doctrine\ORM\Mapping as ORM;
use OpenApi\Attributes as OA;
@ -352,13 +352,13 @@ class Settings implements Stringable
public function getHomepageRedirectUrl(): ?string
{
return Strings::nonEmptyOrNull($this->homepage_redirect_url);
return Types::stringOrNull($this->homepage_redirect_url);
}
public function setHomepageRedirectUrl(?string $homepageRedirectUrl): void
{
$this->homepage_redirect_url = $this->truncateNullableString(
Strings::nonEmptyOrNull($homepageRedirectUrl)
Types::stringOrNull($homepageRedirectUrl)
);
}
@ -371,7 +371,7 @@ class Settings implements Stringable
public function getDefaultAlbumArtUrl(): ?string
{
return Strings::nonEmptyOrNull($this->default_album_art_url);
return Types::stringOrNull($this->default_album_art_url);
}
public function getDefaultAlbumArtUrlAsUri(): ?UriInterface
@ -386,7 +386,7 @@ class Settings implements Stringable
public function setDefaultAlbumArtUrl(?string $defaultAlbumArtUrl): void
{
$this->default_album_art_url = $this->truncateNullableString(
Strings::nonEmptyOrNull($defaultAlbumArtUrl)
Types::stringOrNull($defaultAlbumArtUrl)
);
}
@ -442,13 +442,13 @@ class Settings implements Stringable
public function getLastFmApiKey(): ?string
{
return Strings::nonEmptyOrNull($this->last_fm_api_key);
return Types::stringOrNull($this->last_fm_api_key, true);
}
public function setLastFmApiKey(?string $lastFmApiKey): void
{
$this->last_fm_api_key = $this->truncateNullableString(
Strings::nonEmptyOrNull($lastFmApiKey)
Types::stringOrNull($lastFmApiKey, true)
);
}
@ -478,12 +478,12 @@ class Settings implements Stringable
public function getPublicCustomCss(): ?string
{
return Strings::nonEmptyOrNull($this->public_custom_css);
return Types::stringOrNull($this->public_custom_css, true);
}
public function setPublicCustomCss(?string $publicCustomCss): void
{
$this->public_custom_css = Strings::nonEmptyOrNull($publicCustomCss);
$this->public_custom_css = Types::stringOrNull($publicCustomCss, true);
}
#[
@ -495,12 +495,12 @@ class Settings implements Stringable
public function getPublicCustomJs(): ?string
{
return Strings::nonEmptyOrNull($this->public_custom_js);
return Types::stringOrNull($this->public_custom_js, true);
}
public function setPublicCustomJs(?string $publicCustomJs): void
{
$this->public_custom_js = Strings::nonEmptyOrNull($publicCustomJs);
$this->public_custom_js = Types::stringOrNull($publicCustomJs, true);
}
#[
@ -512,12 +512,12 @@ class Settings implements Stringable
public function getInternalCustomCss(): ?string
{
return Strings::nonEmptyOrNull($this->internal_custom_css);
return Types::stringOrNull($this->internal_custom_css, true);
}
public function setInternalCustomCss(?string $internalCustomCss): void
{
$this->internal_custom_css = Strings::nonEmptyOrNull($internalCustomCss);
$this->internal_custom_css = Types::stringOrNull($internalCustomCss, true);
}
#[
@ -549,12 +549,12 @@ class Settings implements Stringable
public function getBackupTimeCode(): ?string
{
return Strings::nonEmptyOrNull($this->backup_time_code);
return Types::stringOrNull($this->backup_time_code, true);
}
public function setBackupTimeCode(?string $backupTimeCode): void
{
$this->backup_time_code = Strings::nonEmptyOrNull($backupTimeCode);
$this->backup_time_code = Types::stringOrNull($backupTimeCode, true);
}
#[
@ -617,12 +617,12 @@ class Settings implements Stringable
public function getBackupFormat(): ?string
{
return Strings::nonEmptyOrNull($this->backup_format);
return Types::stringOrNull($this->backup_format, true);
}
public function setBackupFormat(?string $backupFormat): void
{
$this->backup_format = Strings::nonEmptyOrNull($backupFormat);
$this->backup_format = Types::stringOrNull($backupFormat, true);
}
#[
@ -1016,12 +1016,12 @@ class Settings implements Stringable
public function getAcmeDomains(): ?string
{
return Strings::nonEmptyOrNull($this->acme_domains);
return Types::stringOrNull($this->acme_domains, true);
}
public function setAcmeDomains(?string $acmeDomains): void
{
$acmeDomains = Strings::nonEmptyOrNull($acmeDomains);
$acmeDomains = Types::stringOrNull($acmeDomains, true);
if (null !== $acmeDomains) {
$acmeDomains = implode(

View File

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

View File

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

View File

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

View File

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

View File

@ -127,7 +127,7 @@ class User implements Stringable, IdentifiableEntityInterface
/** @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]),
DeepNormalize(true)
]

View File

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

View File

@ -8,6 +8,7 @@ use App\Enums\ApplicationEnvironment;
use App\Enums\ReleaseChannel;
use App\Radio\Configuration;
use App\Utilities\File;
use App\Utilities\Types;
use GuzzleHttp\Psr7\Uri;
use Psr\Http\Message\UriInterface;
use Psr\Log\LogLevel;
@ -22,6 +23,7 @@ final class Environment
private readonly bool $isDocker;
private readonly ApplicationEnvironment $appEnv;
/** @var array<string, string|int|bool|float> */
private readonly array $data;
// Core settings values
@ -71,47 +73,18 @@ final class Environment
public const REDIS_PORT = 'REDIS_PORT';
public const REDIS_DB = 'REDIS_DB';
// Default settings
private array $defaults = [
self::APP_NAME => 'AzuraCast',
self::LOG_LEVEL => LogLevel::NOTICE,
self::IS_DOCKER => true,
self::IS_CLI => ('cli' === PHP_SAPI),
self::ASSET_URL => '/static',
self::AUTO_ASSIGN_PORT_MIN => 8000,
self::AUTO_ASSIGN_PORT_MAX => 8499,
self::ENABLE_REDIS => true,
self::SYNC_SHORT_EXECUTION_TIME => 600,
self::SYNC_LONG_EXECUTION_TIME => 1800,
self::NOW_PLAYING_DELAY_TIME => 0,
self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES => 5,
self::PROFILING_EXTENSION_ENABLED => 0,
self::PROFILING_EXTENSION_ALWAYS_ON => 0,
self::PROFILING_EXTENSION_HTTP_KEY => 'dev',
self::ENABLE_WEB_UPDATER => false,
];
public function __construct(array $elements = [])
{
$this->baseDir = dirname(__DIR__);
$this->parentDir = dirname($this->baseDir);
$this->isDocker = file_exists($this->parentDir . '/.docker');
$this->data = array_merge($this->defaults, $elements);
$this->appEnv = ApplicationEnvironment::tryFrom($this->data[self::APP_ENV] ?? '')
?? ApplicationEnvironment::default();
$this->data = $elements;
$this->appEnv = ApplicationEnvironment::tryFrom(
Types::string($this->data[self::APP_ENV] ?? null, '', true)
) ?? ApplicationEnvironment::default();
}
/**
* @return mixed[]
*/
public function toArray(): array
{
return $this->data;
@ -122,6 +95,27 @@ final class Environment
return $this->appEnv;
}
/**
* @return string The base directory of the application, i.e. `/var/app/www` for Docker installations.
*/
public function getBaseDirectory(): string
{
return $this->baseDir;
}
/**
* @return string The parent directory the application is within, i.e. `/var/azuracast`.
*/
public function getParentDirectory(): string
{
return $this->parentDir;
}
public function isDocker(): bool
{
return $this->isDocker;
}
public function isProduction(): bool
{
return ApplicationEnvironment::Production === $this->getAppEnvironmentEnum();
@ -139,47 +133,37 @@ final class Environment
public function showDetailedErrors(): bool
{
if (self::envToBool($this->data[self::SHOW_DETAILED_ERRORS] ?? false)) {
return true;
}
return !$this->isProduction();
}
public function isDocker(): bool
{
return $this->isDocker;
return Types::bool(
$this->data[self::SHOW_DETAILED_ERRORS] ?? null,
!$this->isProduction(),
true
);
}
public function isCli(): bool
{
return $this->data[self::IS_CLI] ?? ('cli' === PHP_SAPI);
return Types::bool(
$this->data[self::IS_CLI] ?? null,
('cli' === PHP_SAPI)
);
}
public function getAppName(): string
{
return $this->data[self::APP_NAME] ?? 'Application';
return Types::string(
$this->data[self::APP_NAME] ?? null,
'AzuraCast',
true
);
}
public function getAssetUrl(): ?string
{
return $this->data[self::ASSET_URL] ?? '';
}
/**
* @return string The base directory of the application, i.e. `/var/app/www` for Docker installations.
*/
public function getBaseDirectory(): string
{
return $this->baseDir;
}
/**
* @return string The parent directory the application is within, i.e. `/var/azuracast`.
*/
public function getParentDirectory(): string
{
return $this->parentDir;
return Types::string(
$this->data[self::ASSET_URL] ?? null,
'/static',
true
);
}
/**
@ -187,8 +171,11 @@ final class Environment
*/
public function getTempDirectory(): string
{
return $this->data[self::TEMP_DIR]
?? $this->getParentDirectory() . '/www_tmp';
return Types::string(
$this->data[self::TEMP_DIR] ?? null,
$this->getParentDirectory() . '/www_tmp',
true
);
}
/**
@ -196,10 +183,14 @@ final class Environment
*/
public function getUploadsDirectory(): string
{
return $this->data[self::UPLOADS_DIR] ?? File::getFirstExistingDirectory([
$this->getParentDirectory() . '/storage/uploads',
$this->getParentDirectory() . '/uploads',
]);
return Types::string(
$this->data[self::UPLOADS_DIR] ?? null,
File::getFirstExistingDirectory([
$this->getParentDirectory() . '/storage/uploads',
$this->getParentDirectory() . '/uploads',
]),
true
);
}
/**
@ -222,56 +213,65 @@ final class Environment
public function getLang(): ?string
{
return $this->data[self::LANG];
return Types::stringOrNull($this->data[self::LANG]);
}
public function getReleaseChannelEnum(): ReleaseChannel
{
return ReleaseChannel::tryFrom($this->data[self::RELEASE_CHANNEL] ?? '')
return ReleaseChannel::tryFrom(Types::string($this->data[self::RELEASE_CHANNEL] ?? null))
?? ReleaseChannel::default();
}
public function getSftpPort(): int
{
return (int)($this->data[self::SFTP_PORT] ?? 2022);
return Types::int(
$this->data[self::SFTP_PORT] ?? null,
2022
);
}
public function getAutoAssignPortMin(): int
{
return (int)($this->data[self::AUTO_ASSIGN_PORT_MIN] ?? Configuration::DEFAULT_PORT_MIN);
return Types::int(
$this->data[self::AUTO_ASSIGN_PORT_MIN] ?? null,
Configuration::DEFAULT_PORT_MIN
);
}
public function getAutoAssignPortMax(): int
{
return (int)($this->data[self::AUTO_ASSIGN_PORT_MAX] ?? Configuration::DEFAULT_PORT_MAX);
return Types::int(
$this->data[self::AUTO_ASSIGN_PORT_MAX] ?? null,
Configuration::DEFAULT_PORT_MAX
);
}
public function getSyncShortExecutionTime(): int
{
return (int)(
$this->data[self::SYNC_SHORT_EXECUTION_TIME] ?? $this->defaults[self::SYNC_SHORT_EXECUTION_TIME]
return Types::int(
$this->data[self::SYNC_SHORT_EXECUTION_TIME] ?? null,
600
);
}
public function getSyncLongExecutionTime(): int
{
return (int)(
$this->data[self::SYNC_LONG_EXECUTION_TIME] ?? $this->defaults[self::SYNC_LONG_EXECUTION_TIME]
return Types::int(
$this->data[self::SYNC_LONG_EXECUTION_TIME] ?? null,
1800
);
}
public function getNowPlayingDelayTime(): int
{
return (int)(
$this->data[self::NOW_PLAYING_DELAY_TIME] ?? $this->defaults[self::NOW_PLAYING_DELAY_TIME]
);
return Types::int($this->data[self::NOW_PLAYING_DELAY_TIME] ?? null);
}
public function getNowPlayingMaxConcurrentProcesses(): int
{
return (int)(
$this->data[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES]
?? $this->defaults[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES]
return Types::int(
$this->data[self::NOW_PLAYING_MAX_CONCURRENT_PROCESSES] ?? null,
5
);
}
@ -280,9 +280,9 @@ final class Environment
*/
public function getLogLevel(): string
{
if (!empty($this->data[self::LOG_LEVEL])) {
$loggingLevel = strtolower($this->data[self::LOG_LEVEL]);
$logLevelRaw = Types::stringOrNull($this->data[self::LOG_LEVEL] ?? null, true);
if (null !== $logLevelRaw) {
$loggingLevel = strtolower($logLevelRaw);
$allowedLogLevels = [
LogLevel::DEBUG,
LogLevel::INFO,
@ -305,16 +305,42 @@ final class Environment
}
/**
* @return mixed[]
* @return array{
* host: string,
* port: int,
* dbname: string,
* user: string,
* password: string,
* unix_socket?: string
* }
*/
public function getDatabaseSettings(): array
{
$dbSettings = [
'host' => $this->data[self::DB_HOST] ?? 'localhost',
'port' => (int)($this->data[self::DB_PORT] ?? 3306),
'dbname' => $this->data[self::DB_NAME] ?? 'azuracast',
'user' => $this->data[self::DB_USER] ?? 'azuracast',
'password' => $this->data[self::DB_PASSWORD] ?? 'azur4c457',
'host' => Types::string(
$this->data[self::DB_HOST] ?? null,
'localhost',
true
),
'port' => Types::int(
$this->data[self::DB_PORT] ?? null,
3306
),
'dbname' => Types::string(
$this->data[self::DB_NAME] ?? null,
'azuracast',
true
),
'user' => Types::string(
$this->data[self::DB_USER] ?? null,
'azuracast',
true
),
'password' => Types::string(
$this->data[self::DB_PASSWORD] ?? null,
'azur4c457',
true
),
];
if ('localhost' === $dbSettings['host'] && $this->isDocker()) {
@ -326,23 +352,42 @@ final class Environment
public function useLocalDatabase(): bool
{
return 'localhost' === ($this->data[self::DB_HOST] ?? 'localhost');
return 'localhost' === $this->getDatabaseSettings()['host'];
}
public function enableRedis(): bool
{
return self::envToBool($this->data[self::ENABLE_REDIS] ?? true);
return Types::bool(
$this->data[self::ENABLE_REDIS],
true,
true
);
}
/**
* @return mixed[]
* @return array{
* host: string,
* port: int,
* db: int,
* socket?: string
* }
*/
public function getRedisSettings(): array
{
$redisSettings = [
'host' => $this->data[self::REDIS_HOST] ?? 'localhost',
'port' => (int)($this->data[self::REDIS_PORT] ?? 6379),
'db' => (int)($this->data[self::REDIS_DB] ?? 1),
'host' => Types::string(
$this->data[self::REDIS_HOST] ?? null,
'localhost',
true
),
'port' => Types::int(
$this->data[self::REDIS_PORT] ?? null,
6379
),
'db' => Types::int(
$this->data[self::REDIS_DB] ?? null,
1
),
];
if ('localhost' === $redisSettings['host'] && $this->isDocker()) {
@ -354,27 +399,47 @@ final class Environment
public function useLocalRedis(): bool
{
return $this->enableRedis() && 'localhost' === ($this->data[self::REDIS_HOST] ?? 'localhost');
return $this->enableRedis() && 'localhost' === $this->getRedisSettings()['host'];
}
public function isProfilingExtensionEnabled(): bool
{
return self::envToBool($this->data[self::PROFILING_EXTENSION_ENABLED] ?? false);
return Types::bool(
$this->data[self::PROFILING_EXTENSION_ENABLED] ?? null,
false,
true
);
}
public function isProfilingExtensionAlwaysOn(): bool
{
return self::envToBool($this->data[self::PROFILING_EXTENSION_ALWAYS_ON] ?? false);
return Types::bool(
$this->data[self::PROFILING_EXTENSION_ALWAYS_ON] ?? null,
false,
true
);
}
public function getProfilingExtensionHttpKey(): string
{
return $this->data[self::PROFILING_EXTENSION_HTTP_KEY] ?? 'dev';
return Types::string(
$this->data[self::PROFILING_EXTENSION_HTTP_KEY] ?? null,
'dev',
true
);
}
public function enableWebUpdater(): bool
{
return $this->isDocker() && self::envToBool($this->data[self::ENABLE_WEB_UPDATER] ?? false);
if (!$this->isDocker()) {
return false;
}
return Types::bool(
$this->data[self::ENABLE_WEB_UPDATER] ?? null,
false,
true
);
}
public static function getDefaultsForEnvironment(Environment $existingEnv): self
@ -385,24 +450,6 @@ final class Environment
]);
}
public static function envToBool(mixed $value): bool
{
if (is_bool($value)) {
return $value;
}
if (is_int($value)) {
return 0 !== $value;
}
if (null === $value) {
return false;
}
$value = (string)$value;
return str_starts_with(strtolower($value), 'y')
|| 'true' === strtolower($value)
|| '1' === $value;
}
public static function getInstance(): Environment
{
return self::$instance;

View File

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

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->baseUrl = null;

View File

@ -4,14 +4,13 @@ declare(strict_types=1);
namespace App\Http;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
interface RouterInterface
{
public function setRequest(?ServerRequestInterface $request): void;
public function setRequest(?ServerRequest $request): void;
public function withRequest(?ServerRequestInterface $request): self;
public function withRequest(?ServerRequest $request): self;
public function getBaseUrl(): UriInterface;

View File

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

View File

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

View File

@ -6,6 +6,7 @@ namespace App\Installer\EnvFiles;
use App\Environment;
use App\Utilities\Strings;
use App\Utilities\Types;
use ArrayAccess;
use DateTimeImmutable;
use DateTimeZone;
@ -58,10 +59,7 @@ abstract class AbstractEnvFile implements ArrayAccess
public function getAsBool(string $key, bool $default): bool
{
if (isset($this->data[$key])) {
return Environment::envToBool($this->data[$key]);
}
return $default;
return Types::bool($this->data[$key], $default, true);
}
public function offsetExists(mixed $offset): bool
@ -96,7 +94,7 @@ abstract class AbstractEnvFile implements ArrayAccess
];
foreach (static::getConfiguration($environment) as $key => $keyInfo) {
$envFile[] = '# ' . ($keyInfo['name'] ?? $key);
$envFile[] = sprintf('# %s', $keyInfo['name']);
if (!empty($keyInfo['description'])) {
$desc = Strings::mbWordwrap($keyInfo['description']);
@ -191,7 +189,7 @@ abstract class AbstractEnvFile implements ArrayAccess
}
/**
* @return mixed[]
* @return array<string, array{name: string, description?: string, options?: array, default?: string, required?: bool}>
*/
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;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Apply the "X-Forwarded-Proto" header if it exists.
*/
final class ApplyXForwardedProto implements MiddlewareInterface
final class ApplyXForwardedProto extends AbstractMiddleware
{
/**
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($request->hasHeader('X-Forwarded-Proto')) {
$uri = $request->getUri();

View File

@ -9,13 +9,13 @@ use App\Container\EnvironmentAwareTrait;
use App\Customization;
use App\Entity\AuditLog;
use App\Entity\Repository\UserRepository;
use App\Exception\InvalidRequestAttribute;
use App\Http\ServerRequest;
use App\Middleware\AbstractMiddleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
abstract class AbstractAuth implements MiddlewareInterface
abstract class AbstractAuth extends AbstractMiddleware
{
use EnvironmentAwareTrait;
@ -26,7 +26,7 @@ abstract class AbstractAuth implements MiddlewareInterface
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$customization = $this->customization->withRequest($request);
@ -39,7 +39,12 @@ abstract class AbstractAuth implements MiddlewareInterface
->withAttribute(ServerRequest::ATTR_ACL, $acl);
// Set the Audit Log user.
AuditLog::setCurrentUser($request->getAttribute(ServerRequest::ATTR_USER));
try {
$currentUser = $request->getUser();
} catch (InvalidRequestAttribute) {
$currentUser = null;
}
AuditLog::setCurrentUser($currentUser);
$response = $handler->handle($request);

View File

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

View File

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

View File

@ -7,21 +7,19 @@ namespace App\Middleware;
use App\Http\ServerRequest;
use App\View;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Inject the view object into the request and prepare it for rendering templates.
*/
final class EnableView implements MiddlewareInterface
final class EnableView extends AbstractMiddleware
{
public function __construct(
private readonly View $view
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$view = $this->view->withRequest($request);

View File

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

View File

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

View File

@ -8,22 +8,20 @@ use App\Entity\Repository\StationRepository;
use App\Entity\Station;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;
/**
* Retrieve the station specified in the request parameters, and throw an error if none exists but one is required.
*/
final class GetStation implements MiddlewareInterface
final class GetStation extends AbstractMiddleware
{
public function __construct(
private readonly StationRepository $stationRepo
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$routeArgs = RouteContext::fromRequest($request)->getRoute()?->getArguments();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.
*/
final class Permissions
final class Permissions extends AbstractMiddleware
{
public function __construct(
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.
*/
final class RateLimit
final class RateLimit extends AbstractMiddleware
{
public function __construct(
private readonly string $rlGroup = 'default',

View File

@ -4,18 +4,17 @@ declare(strict_types=1);
namespace App\Middleware;
use App\Http\ServerRequest;
use GuzzleHttp\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Remove trailing slash from all URLs when routing.
*/
final class RemoveSlashes implements MiddlewareInterface
final class RemoveSlashes extends AbstractMiddleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$uri = $request->getUri();
$path = $uri->getPath();

View File

@ -6,12 +6,11 @@ namespace App\Middleware;
use App\Container\EnvironmentAwareTrait;
use App\Doctrine\DecoratedEntityManager;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class ReopenEntityManagerMiddleware implements MiddlewareInterface
final class ReopenEntityManagerMiddleware extends AbstractMiddleware
{
use EnvironmentAwareTrait;
@ -20,7 +19,7 @@ final class ReopenEntityManagerMiddleware implements MiddlewareInterface
) {
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$this->em->open();

View File

@ -14,7 +14,7 @@ use Psr\Http\Server\RequestHandlerInterface;
/**
* Require that the user be logged in to view this page.
*/
final class RequireLogin
final class RequireLogin extends AbstractMiddleware
{
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{

View File

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

View File

@ -13,7 +13,7 @@ use Psr\Http\Server\RequestHandlerInterface;
/**
* Require that the user be logged in to view this page.
*/
final class RequireStation
final class RequireStation extends AbstractMiddleware
{
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{

View File

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

View File

@ -5,18 +5,17 @@ declare(strict_types=1);
namespace App\Middleware;
use App\Exception\WrappedException;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
/**
* Wrap all exceptions thrown past this point with rich metadata.
*/
final class WrapExceptionsWithRequestData implements MiddlewareInterface
final class WrapExceptionsWithRequestData extends AbstractMiddleware
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
return $handler->handle($request);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,29 +9,11 @@ use voku\helper\UTF8;
final class Strings
{
/**
* Given a nullable string value, return null if the string is null or otherwise
* is considered "empty", or the trimmed value otherwise.
*/
public static function nonEmptyOrNull(?string $input): ?string
{
if (null === $input || '' === $input) {
return null;
}
$input = trim($input);
return (!empty($input))
? $input
: null;
}
/**
* Truncate text (adding "..." if needed)
*/
public static function truncateText(string $text, int $limit = 80, string $pad = '...'): string
{
mb_internal_encoding('UTF-8');
if (mb_strlen($text) <= $limit) {
return $text;
}

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.
*
* @return mixed[]
* @return string[]
*/
public function getDetails(): array
{

View File

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

View File

@ -94,7 +94,7 @@ abstract class AbstractConnector implements ConnectorInterface
// Replaces {{ var.name }} with the flattened $values['var.name']
$vars[$varKey] = preg_replace_callback(
"/\{\{(\s*)([a-zA-Z\d\-_.]+)(\s*)}}/",
static function ($matches) use ($values) {
static function (array $matches) use ($values): string {
$innerValue = strtolower(trim($matches[2]));
return $values[$innerValue] ?? '';
},
@ -107,11 +107,11 @@ abstract class AbstractConnector implements ConnectorInterface
/**
* Determine if a passed URL is valid and return it if so, or return null otherwise.
*
* @param string|null $urlString
*/
protected function getValidUrl(?string $urlString = null): ?string
protected function getValidUrl(mixed $urlString = null): ?string
{
$urlString = Utilities\Types::stringOrNull($urlString, true);
$uri = Utilities\Urls::tryParseUserUrl(
$urlString,
'Webhook'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
<?php
/** @var Psr\Http\Message\RequestInterface $request */
/** @var App\Session\Flash|null $flashObj */
$flashObj = $request->getAttribute(App\Http\ServerRequest::ATTR_SESSION_FLASH);
/** @var \App\Http\ServerRequest $request */
try {
$flashObj = $request->getFlash();
} catch (App\Exception\InvalidRequestAttribute) {
$flashObj = null;
}
$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();
App\AppFactory::createCli(
$app = App\AppFactory::createCli(
[
App\Environment::TEMP_DIR => $tempDir,
App\Environment::UPLOADS_DIR => $tempDir,
]
);
return $app;