Use common trait for searching across controllers; add type strictness to several $request->getParam calls on controllers.

This commit is contained in:
Buster Neece 2024-01-15 11:24:27 -06:00
parent b8e5a4bfa7
commit 4ccce52705
No known key found for this signature in database
27 changed files with 247 additions and 118 deletions

View File

@ -12,6 +12,7 @@ use App\Exception\ValidationException;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Paginator; use App\Paginator;
use App\Utilities\Types;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -124,7 +125,7 @@ abstract class AbstractApiCrudController
$return = $this->toArray($record); $return = $this->toArray($record);
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$router = $request->getRouter(); $router = $request->getRouter();
if ($record instanceof IdentifiableEntityInterface) { if ($record instanceof IdentifiableEntityInterface) {

View File

@ -9,6 +9,7 @@ use App\Controller\SingleActionInterface;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Radio\Adapters; use App\Radio\Adapters;
use App\Utilities\Types;
use Monolog\Handler\TestHandler; use Monolog\Handler\TestHandler;
use Monolog\Level; use Monolog\Level;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -33,7 +34,7 @@ final class TelnetAction implements SingleActionInterface
$station = $request->getStation(); $station = $request->getStation();
$backend = $this->adapters->requireBackendAdapter($station); $backend = $this->adapters->requireBackendAdapter($station);
$command = $request->getParam('command'); $command = Types::string($request->getParam('command'));
$telnetResponse = $backend->command($station, $command); $telnetResponse = $backend->command($station, $command);
$this->logger->debug( $this->logger->debug(

View File

@ -6,6 +6,7 @@ namespace App\Controller\Api\Admin;
use App\Acl; use App\Acl;
use App\Controller\Api\AbstractApiCrudController; use App\Controller\Api\AbstractApiCrudController;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Entity\Repository\RolePermissionRepository; use App\Entity\Repository\RolePermissionRepository;
use App\Entity\Role; use App\Entity\Role;
@ -138,6 +139,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
final class RolesController extends AbstractApiCrudController final class RolesController extends AbstractApiCrudController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = Role::class; protected string $entityClass = Role::class;
protected string $resourceRouteName = 'api:admin:role'; protected string $resourceRouteName = 'api:admin:role';
@ -174,11 +176,13 @@ final class RolesController extends AbstractApiCrudController
'r.name' 'r.name'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('(r.name LIKE :name)') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'r.name',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }

View File

@ -11,6 +11,7 @@ use App\Exception\ValidationException;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Service\Mail; use App\Service\Mail;
use App\Utilities\Types;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Validator\Constraints\Email; use Symfony\Component\Validator\Constraints\Email;
@ -30,7 +31,7 @@ final class SendTestMessageAction implements SingleActionInterface
Response $response, Response $response,
array $params array $params
): ResponseInterface { ): ResponseInterface {
$emailAddress = $request->getParam('email', ''); $emailAddress = Types::string($request->getParam('email'));
$errors = $this->validator->validate( $errors = $this->validator->validate(
$emailAddress, $emailAddress,

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Admin; namespace App\Controller\Api\Admin;
use App\Controller\Api\AbstractApiCrudController; use App\Controller\Api\AbstractApiCrudController;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Entity\Repository\StationQueueRepository; use App\Entity\Repository\StationQueueRepository;
use App\Entity\Repository\StationRepository; use App\Entity\Repository\StationRepository;
@ -142,6 +143,7 @@ use Throwable;
class StationsController extends AbstractApiCrudController class StationsController extends AbstractApiCrudController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = Station::class; protected string $entityClass = Station::class;
protected string $resourceRouteName = 'api:admin:station'; protected string $resourceRouteName = 'api:admin:station';
@ -175,11 +177,14 @@ class StationsController extends AbstractApiCrudController
'e.name' 'e.name'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('(e.name LIKE :name OR e.short_name LIKE :name)') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'e.name',
'e.short_name',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }
@ -192,7 +197,7 @@ class StationsController extends AbstractApiCrudController
$return = $this->toArray($record); $return = $this->toArray($record);
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = $request->isInternal();
$router = $request->getRouter(); $router = $request->getRouter();
$return['links'] = [ $return['links'] = [

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Admin; namespace App\Controller\Api\Admin;
use App\Controller\Api\AbstractApiCrudController; use App\Controller\Api\AbstractApiCrudController;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Controller\Frontend\Account\MasqueradeAction; use App\Controller\Frontend\Account\MasqueradeAction;
use App\Entity\Api\Error; use App\Entity\Api\Error;
@ -134,6 +135,7 @@ use Psr\Http\Message\ResponseInterface;
class UsersController extends AbstractApiCrudController class UsersController extends AbstractApiCrudController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = User::class; protected string $entityClass = User::class;
protected string $resourceRouteName = 'api:admin:user'; protected string $resourceRouteName = 'api:admin:user';
@ -156,11 +158,14 @@ class UsersController extends AbstractApiCrudController
'e.name' 'e.name'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('(e.name LIKE :name OR e.email LIKE :name)') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'e.name',
'e.email',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }
@ -173,7 +178,7 @@ class UsersController extends AbstractApiCrudController
$return = $this->toArray($record); $return = $this->toArray($record);
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = $request->isInternal();
$router = $request->getRouter(); $router = $request->getRouter();
$csrf = $request->getCsrf(); $csrf = $request->getCsrf();
$currentUser = $request->getUser(); $currentUser = $request->getUser();

View File

@ -6,6 +6,8 @@ namespace App\Controller\Api\Frontend\Dashboard;
use App\Container\EntityManagerAwareTrait; use App\Container\EntityManagerAwareTrait;
use App\Container\SettingsAwareTrait; use App\Container\SettingsAwareTrait;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults;
use App\Controller\SingleActionInterface; use App\Controller\SingleActionInterface;
use App\Entity\Api\Dashboard; use App\Entity\Api\Dashboard;
use App\Entity\ApiGenerator\NowPlayingApiGenerator; use App\Entity\ApiGenerator\NowPlayingApiGenerator;
@ -20,6 +22,8 @@ final class StationsAction implements SingleActionInterface
{ {
use EntityManagerAwareTrait; use EntityManagerAwareTrait;
use SettingsAwareTrait; use SettingsAwareTrait;
use CanSortResults;
use CanSearchResults;
public function __construct( public function __construct(
private readonly NowPlayingApiGenerator $npApiGenerator private readonly NowPlayingApiGenerator $npApiGenerator
@ -69,8 +73,8 @@ final class StationsAction implements SingleActionInterface
$viewStations[] = $row; $viewStations[] = $row;
} }
$searchPhrase = trim($request->getParam('searchPhrase', '')); $searchPhrase = $this->getSearchPhrase($request);
if (!empty($searchPhrase)) { if (null !== $searchPhrase) {
$viewStations = array_filter( $viewStations = array_filter(
$viewStations, $viewStations,
static function (Dashboard $row) use ($searchPhrase) { static function (Dashboard $row) use ($searchPhrase) {
@ -79,22 +83,15 @@ final class StationsAction implements SingleActionInterface
); );
} }
$sort = $request->getParam('sort'); $viewStations = $this->sortArray(
usort( $request,
$viewStations, $viewStations,
static function (Dashboard $a, Dashboard $b) use ($sort) { [
if ('listeners' === $sort) { 'listeners' => 'listeners.current',
return $a->listeners->current <=> $b->listeners->current; ],
} 'station.name'
return $a->station->name <=> $b->station->name;
}
); );
if ('desc' === strtolower($request->getParam('sortOrder', 'asc'))) {
$viewStations = array_reverse($viewStations);
}
return Paginator::fromArray($viewStations, $request)->write($response); return Paginator::fromArray($viewStations, $request)->write($response);
} }
} }

View File

@ -11,6 +11,7 @@ use App\Exception\PermissionDeniedException;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Radio\Frontend\Blocklist\BlocklistParser; use App\Radio\Frontend\Blocklist\BlocklistParser;
use App\Utilities\Types;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
final class ListenerAuthAction implements SingleActionInterface final class ListenerAuthAction implements SingleActionInterface
@ -46,7 +47,7 @@ final class ListenerAuthAction implements SingleActionInterface
} }
$station = $request->getStation(); $station = $request->getStation();
$listenerIp = $request->getParam('ip') ?? ''; $listenerIp = Types::string($request->getParam('ip'));
if ($this->blocklistParser->isAllowed($station, $listenerIp)) { if ($this->blocklistParser->isAllowed($station, $listenerIp)) {
return $response->withHeader('icecast-auth-user', '1'); return $response->withHeader('icecast-auth-user', '1');

View File

@ -66,7 +66,7 @@ final class BatchAction implements SingleActionInterface
$fsMedia = $this->stationFilesystems->getMediaFilesystem($station); $fsMedia = $this->stationFilesystems->getMediaFilesystem($station);
$result = match ($request->getParam('do')) { $result = match (Types::string($request->getParam('do'))) {
'delete' => $this->doDelete($request, $station, $storageLocation, $fsMedia), 'delete' => $this->doDelete($request, $station, $storageLocation, $fsMedia),
'playlist' => $this->doPlaylist($request, $station, $storageLocation, $fsMedia), 'playlist' => $this->doPlaylist($request, $station, $storageLocation, $fsMedia),
'move' => $this->doMove($request, $station, $storageLocation, $fsMedia), 'move' => $this->doMove($request, $station, $storageLocation, $fsMedia),
@ -141,10 +141,15 @@ final class BatchAction implements SingleActionInterface
/** @var array<int, int> $affectedPlaylistIds */ /** @var array<int, int> $affectedPlaylistIds */
$affectedPlaylistIds = []; $affectedPlaylistIds = [];
foreach ($request->getParam('playlists') as $playlistId) { /** @var string[] $requestPlaylists */
$requestPlaylists = Types::array($request->getParam('playlists'));
foreach ($requestPlaylists as $playlistId) {
if ('new' === $playlistId) { if ('new' === $playlistId) {
$playlist = new StationPlaylist($station); $playlist = new StationPlaylist($station);
$playlist->setName($request->getParam('new_playlist_name')); $playlist->setName(
Types::string($request->getParam('new_playlist_name'))
);
$this->em->persist($playlist); $this->em->persist($playlist);
$this->em->flush(); $this->em->flush();
@ -216,8 +221,8 @@ final class BatchAction implements SingleActionInterface
): BatchResult { ): BatchResult {
$result = $this->parseRequest($request, $fs); $result = $this->parseRequest($request, $fs);
$from = $request->getParam('currentDirectory', ''); $from = Types::string($request->getParam('currentDirectory'));
$to = $request->getParam('directory', ''); $to = Types::string($request->getParam('directory'));
$toMove = [ $toMove = [
$this->batchUtilities->iterateMedia($storageLocation, $result->files), $this->batchUtilities->iterateMedia($storageLocation, $result->files),

View File

@ -74,7 +74,7 @@ final class ListAction implements SingleActionInterface
$cacheKey = implode('.', $cacheKeyParts); $cacheKey = implode('.', $cacheKeyParts);
$flushCache = Types::bool($request->getParam('flushCache')); $flushCache = Types::bool($request->getParam('flushCache'), false, true);
if (!$flushCache && $this->cache->has($cacheKey)) { if (!$flushCache && $this->cache->has($cacheKey)) {
/** @var array<int, FileList> $result */ /** @var array<int, FileList> $result */

View File

@ -10,6 +10,7 @@ use App\Entity\Api\Status;
use App\Flysystem\StationFilesystems; use App\Flysystem\StationFilesystems;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Utilities\Types;
use League\Flysystem\UnableToCreateDirectory; use League\Flysystem\UnableToCreateDirectory;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -25,8 +26,8 @@ final class MakeDirectoryAction implements SingleActionInterface
Response $response, Response $response,
array $params array $params
): ResponseInterface { ): ResponseInterface {
$currentDir = $request->getParam('currentDirectory', ''); $currentDir = Types::string($request->getParam('currentDirectory'));
$newDirName = $request->getParam('name', ''); $newDirName = Types::string($request->getParam('name'));
if (empty($newDirName)) { if (empty($newDirName)) {
return $response->withStatus(400) return $response->withStatus(400)

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations; namespace App\Controller\Api\Stations;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Entity\Repository\StationMountRepository; use App\Entity\Repository\StationMountRepository;
use App\Entity\StationMount; use App\Entity\StationMount;
@ -144,6 +145,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
final class MountsController extends AbstractStationApiCrudController final class MountsController extends AbstractStationApiCrudController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = StationMount::class; protected string $entityClass = StationMount::class;
protected string $resourceRouteName = 'api:stations:mount'; protected string $resourceRouteName = 'api:stations:mount';
@ -180,11 +182,14 @@ final class MountsController extends AbstractStationApiCrudController
'e.display_name' 'e.display_name'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('(e.name LIKE :name OR e.display_name LIKE :name)') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'e.name',
'e.display_name',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }

View File

@ -12,6 +12,7 @@ use App\Entity\Repository\StationPlaylistRepository;
use App\Exception; use App\Exception;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Utilities\Types;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
final class PutOrderAction implements SingleActionInterface final class PutOrderAction implements SingleActionInterface
@ -39,7 +40,7 @@ final class PutOrderAction implements SingleActionInterface
throw new Exception(__('This playlist is not a sequential playlist.')); throw new Exception(__('This playlist is not a sequential playlist.'));
} }
$order = $request->getParam('order'); $order = Types::array($request->getParam('order'));
$this->spmRepo->setMediaOrder($record, $order); $this->spmRepo->setMediaOrder($record, $order);
return $response->withJson($order); return $response->withJson($order);

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations; namespace App\Controller\Api\Stations;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Entity\Enums\PlaylistOrders; use App\Entity\Enums\PlaylistOrders;
use App\Entity\Enums\PlaylistSources; use App\Entity\Enums\PlaylistSources;
@ -12,6 +13,7 @@ use App\Entity\StationSchedule;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use App\Utilities\Types;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\AbstractQuery;
use InvalidArgumentException; use InvalidArgumentException;
@ -145,6 +147,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
final class PlaylistsController extends AbstractScheduledEntityController final class PlaylistsController extends AbstractScheduledEntityController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = StationPlaylist::class; protected string $entityClass = StationPlaylist::class;
protected string $resourceRouteName = 'api:stations:playlist'; protected string $resourceRouteName = 'api:stations:playlist';
@ -172,11 +175,13 @@ final class PlaylistsController extends AbstractScheduledEntityController
'sp.name' 'sp.name'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('sp.name LIKE :name') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'sp.name',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }
@ -261,7 +266,7 @@ final class PlaylistsController extends AbstractScheduledEntityController
$return['num_songs'] = $songTotals['num_songs']; $return['num_songs'] = $songTotals['num_songs'];
$return['total_length'] = round((float)$songTotals['total_length']); $return['total_length'] = round((float)$songTotals['total_length']);
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$router = $request->getRouter(); $router = $request->getRouter();
$return['links'] = [ $return['links'] = [

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations; namespace App\Controller\Api\Stations;
use App\Controller\Api\AbstractApiCrudController; use App\Controller\Api\AbstractApiCrudController;
use App\Controller\Api\Traits\CanSearchResults;
use App\Entity\Api\PodcastEpisode as ApiPodcastEpisode; use App\Entity\Api\PodcastEpisode as ApiPodcastEpisode;
use App\Entity\Api\PodcastMedia as ApiPodcastMedia; use App\Entity\Api\PodcastMedia as ApiPodcastMedia;
use App\Entity\PodcastEpisode; use App\Entity\PodcastEpisode;
@ -16,6 +17,7 @@ use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use App\Service\Flow\UploadedFile; use App\Service\Flow\UploadedFile;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -185,6 +187,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
] ]
final class PodcastEpisodesController extends AbstractApiCrudController final class PodcastEpisodesController extends AbstractApiCrudController
{ {
use CanSearchResults;
protected string $entityClass = PodcastEpisode::class; protected string $entityClass = PodcastEpisode::class;
protected string $resourceRouteName = 'api:stations:podcast:episode'; protected string $resourceRouteName = 'api:stations:podcast:episode';
@ -218,11 +222,13 @@ final class PodcastEpisodesController extends AbstractApiCrudController
->orderBy('e.created_at', 'DESC') ->orderBy('e.created_at', 'DESC')
->setParameter('podcast', $podcast); ->setParameter('podcast', $podcast);
$searchPhrase = trim($request->getParam('searchPhrase', '')); $queryBuilder = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$queryBuilder->andWhere('e.title LIKE :title') $queryBuilder,
->setParameter('title', '%' . $searchPhrase . '%'); [
} 'e.title',
]
);
return $this->listPaginatedFromQuery($request, $response, $queryBuilder->getQuery()); return $this->listPaginatedFromQuery($request, $response, $queryBuilder->getQuery());
} }
@ -299,7 +305,7 @@ final class PodcastEpisodesController extends AbstractApiCrudController
throw new InvalidArgumentException(sprintf('Record must be an instance of %s.', $this->entityClass)); throw new InvalidArgumentException(sprintf('Record must be an instance of %s.', $this->entityClass));
} }
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$router = $request->getRouter(); $router = $request->getRouter();
$return = new ApiPodcastEpisode(); $return = new ApiPodcastEpisode();

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations; namespace App\Controller\Api\Stations;
use App\Controller\Api\AbstractApiCrudController; use App\Controller\Api\AbstractApiCrudController;
use App\Controller\Api\Traits\CanSearchResults;
use App\Entity\Api\Podcast as ApiPodcast; use App\Entity\Api\Podcast as ApiPodcast;
use App\Entity\Podcast; use App\Entity\Podcast;
use App\Entity\PodcastCategory; use App\Entity\PodcastCategory;
@ -14,6 +15,7 @@ use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use App\Service\Flow\UploadedFile; use App\Service\Flow\UploadedFile;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -147,6 +149,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
] ]
final class PodcastsController extends AbstractApiCrudController final class PodcastsController extends AbstractApiCrudController
{ {
use CanSearchResults;
protected string $entityClass = Podcast::class; protected string $entityClass = Podcast::class;
protected string $resourceRouteName = 'api:stations:podcast'; protected string $resourceRouteName = 'api:stations:podcast';
@ -173,11 +177,13 @@ final class PodcastsController extends AbstractApiCrudController
->orderBy('p.title', 'ASC') ->orderBy('p.title', 'ASC')
->setParameter('storageLocation', $station->getPodcastsStorageLocation()); ->setParameter('storageLocation', $station->getPodcastsStorageLocation());
$searchPhrase = trim($request->getParam('searchPhrase', '')); $queryBuilder = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$queryBuilder->andWhere('p.title LIKE :title') $queryBuilder,
->setParameter('title', '%' . $searchPhrase . '%'); [
} 'p.title',
]
);
return $this->listPaginatedFromQuery($request, $response, $queryBuilder->getQuery()); return $this->listPaginatedFromQuery($request, $response, $queryBuilder->getQuery());
} }
@ -228,7 +234,7 @@ final class PodcastsController extends AbstractApiCrudController
throw new InvalidArgumentException(sprintf('Record must be an instance of %s.', $this->entityClass)); throw new InvalidArgumentException(sprintf('Record must be an instance of %s.', $this->entityClass));
} }
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$router = $request->getRouter(); $router = $request->getRouter();
$station = $request->getStation(); $station = $request->getStation();

View File

@ -13,6 +13,7 @@ use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use App\Radio\AutoDJ\Queue; use App\Radio\AutoDJ\Queue;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -147,7 +148,7 @@ final class QueueController extends AbstractStationApiCrudController
$row = ($this->queueApiGenerator)($record); $row = ($this->queueApiGenerator)($record);
$row->resolveUrls($router->getBaseUrl()); $row->resolveUrls($router->getBaseUrl());
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$apiResponse = new StationQueueDetailed(); $apiResponse = new StationQueueDetailed();
$apiResponse->fromParentObject($row); $apiResponse->fromParentObject($row);

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations; namespace App\Controller\Api\Stations;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Entity\Api\StationRemote as ApiStationRemote; use App\Entity\Api\StationRemote as ApiStationRemote;
use App\Entity\StationRemote; use App\Entity\StationRemote;
@ -11,6 +12,7 @@ use App\Exception\PermissionDeniedException;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -143,6 +145,7 @@ use Psr\Http\Message\ResponseInterface;
final class RemotesController extends AbstractStationApiCrudController final class RemotesController extends AbstractStationApiCrudController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = StationRemote::class; protected string $entityClass = StationRemote::class;
protected string $resourceRouteName = 'api:stations:remote'; protected string $resourceRouteName = 'api:stations:remote';
@ -170,11 +173,13 @@ final class RemotesController extends AbstractStationApiCrudController
'e.display_name' 'e.display_name'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('(e.display_name LIKE :name)') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'e.display_name',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }
@ -192,7 +197,7 @@ final class RemotesController extends AbstractStationApiCrudController
$return = new ApiStationRemote(); $return = new ApiStationRemote();
$return->fromParentObject($returnArray); $return->fromParentObject($returnArray);
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$router = $request->getRouter(); $router = $request->getRouter();
$return->is_editable = $record->isEditable(); $return->is_editable = $record->isEditable();

View File

@ -5,18 +5,23 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations\Reports; namespace App\Controller\Api\Stations\Reports;
use App\Container\EntityManagerAwareTrait; use App\Container\EntityManagerAwareTrait;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults;
use App\Entity\Api\Status; use App\Entity\Api\Status;
use App\Entity\Repository\StationRequestRepository; use App\Entity\Repository\StationRequestRepository;
use App\Entity\StationRequest; use App\Entity\StationRequest;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Paginator; use App\Paginator;
use App\Utilities\Types;
use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\AbstractQuery;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
final class RequestsController final class RequestsController
{ {
use EntityManagerAwareTrait; use EntityManagerAwareTrait;
use CanSortResults;
use CanSearchResults;
public function __construct( public function __construct(
private readonly StationRequestRepository $requestRepo private readonly StationRequestRepository $requestRepo
@ -36,33 +41,35 @@ final class RequestsController
->where('sr.station = :station') ->where('sr.station = :station')
->setParameter('station', $station); ->setParameter('station', $station);
$qb = match ($request->getParam('type', 'recent')) { $type = Types::string($request->getParam('type', 'recent'));
$qb = match ($type) {
'history' => $qb->andWhere('sr.played_at != 0'), 'history' => $qb->andWhere('sr.played_at != 0'),
default => $qb->andWhere('sr.played_at = 0'), default => $qb->andWhere('sr.played_at = 0'),
}; };
$queryParams = $request->getQueryParams(); $qb = $this->sortQueryBuilder(
$searchPhrase = trim($queryParams['searchPhrase'] ?? ''); $request,
$qb,
[
'name' => 'sm.title',
'title' => 'sm.title',
'artist' => 'sm.artist',
'album' => 'sm.album',
'genre' => 'sm.genre',
],
'sr.timestamp',
'DESC'
);
$sortField = (string)($queryParams['sort'] ?? ''); $qb = $this->searchQueryBuilder(
$sortDirection = strtolower($queryParams['sortOrder'] ?? 'asc'); $request,
$qb,
if (!empty($sortField)) { [
match ($sortField) { 'sm.title',
'name', 'title' => $qb->addOrderBy('sm.title', $sortDirection), 'sm.artist',
'artist' => $qb->addOrderBy('sm.artist', $sortDirection), 'sm.album',
'album' => $qb->addOrderBy('sm.album', $sortDirection), ]
'genre' => $qb->addOrderBy('sm.genre', $sortDirection), );
default => null,
};
} else {
$qb->addOrderBy('sr.timestamp', 'DESC');
}
if (!empty($searchPhrase)) {
$qb->andWhere('(sm.title LIKE :query OR sm.artist LIKE :query OR sm.album LIKE :query)')
->setParameter('query', '%' . $searchPhrase . '%');
}
$query = $qb->getQuery() $query = $qb->getQuery()
->setHydrationMode(AbstractQuery::HYDRATE_ARRAY); ->setHydrationMode(AbstractQuery::HYDRATE_ARRAY);

View File

@ -76,7 +76,7 @@ final class BroadcastsController extends AbstractApiCrudController
$paginator = Paginator::fromQuery($query, $request); $paginator = Paginator::fromQuery($query, $request);
$router = $request->getRouter(); $router = $request->getRouter();
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = $request->isInternal();
$fsRecordings = $this->stationFilesystems->getRecordingsFilesystem($station); $fsRecordings = $this->stationFilesystems->getRecordingsFilesystem($station);
$paginator->setPostprocessor( $paginator->setPostprocessor(

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations; namespace App\Controller\Api\Stations;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Entity\Repository\StationScheduleRepository; use App\Entity\Repository\StationScheduleRepository;
use App\Entity\Repository\StationStreamerRepository; use App\Entity\Repository\StationStreamerRepository;
@ -14,6 +15,7 @@ use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use App\Radio\AutoDJ\Scheduler; use App\Radio\AutoDJ\Scheduler;
use App\Service\Flow\UploadedFile; use App\Service\Flow\UploadedFile;
use App\Utilities\Types;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use InvalidArgumentException; use InvalidArgumentException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@ -149,6 +151,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface;
final class StreamersController extends AbstractScheduledEntityController final class StreamersController extends AbstractScheduledEntityController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = StationStreamer::class; protected string $entityClass = StationStreamer::class;
protected string $resourceRouteName = 'api:stations:streamer'; protected string $resourceRouteName = 'api:stations:streamer';
@ -186,11 +189,14 @@ final class StreamersController extends AbstractScheduledEntityController
'e.streamer_username' 'e.streamer_username'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('(e.streamer_username LIKE :name OR e.display_name LIKE :name)') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'e.streamer_username',
'e.display_name',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }
@ -275,7 +281,7 @@ final class StreamersController extends AbstractScheduledEntityController
$return = parent::viewRecord($record, $request); $return = parent::viewRecord($record, $request);
$router = $request->getRouter(); $router = $request->getRouter();
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$return['has_custom_art'] = (0 !== $record->getArtUpdatedAt()); $return['has_custom_art'] = (0 !== $record->getArtUpdatedAt());

View File

@ -4,11 +4,13 @@ declare(strict_types=1);
namespace App\Controller\Api\Stations; namespace App\Controller\Api\Stations;
use App\Controller\Api\Traits\CanSearchResults;
use App\Controller\Api\Traits\CanSortResults; use App\Controller\Api\Traits\CanSortResults;
use App\Entity\StationWebhook; use App\Entity\StationWebhook;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\OpenApi; use App\OpenApi;
use App\Utilities\Types;
use InvalidArgumentException; use InvalidArgumentException;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -141,6 +143,7 @@ use Psr\Http\Message\ResponseInterface;
final class WebhooksController extends AbstractStationApiCrudController final class WebhooksController extends AbstractStationApiCrudController
{ {
use CanSortResults; use CanSortResults;
use CanSearchResults;
protected string $entityClass = StationWebhook::class; protected string $entityClass = StationWebhook::class;
protected string $resourceRouteName = 'api:stations:webhook'; protected string $resourceRouteName = 'api:stations:webhook';
@ -171,11 +174,13 @@ final class WebhooksController extends AbstractStationApiCrudController
'e.name' 'e.name'
); );
$searchPhrase = trim($request->getParam('searchPhrase', '')); $qb = $this->searchQueryBuilder(
if (!empty($searchPhrase)) { $request,
$qb->andWhere('(e.name LIKE :name)') $qb,
->setParameter('name', '%' . $searchPhrase . '%'); [
} 'e.name',
]
);
return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery());
} }
@ -188,7 +193,7 @@ final class WebhooksController extends AbstractStationApiCrudController
$return = $this->toArray($record); $return = $this->toArray($record);
$isInternal = ('true' === $request->getParam('internal', 'false')); $isInternal = Types::bool($request->getParam('internal'), false, true);
$router = $request->getRouter(); $router = $request->getRouter();
$return['links'] = [ $return['links'] = [

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Traits;
use App\Http\ServerRequest;
use App\Utilities\Types;
use Doctrine\ORM\QueryBuilder;
trait CanSearchResults
{
/**
* @param string[] $fieldsToSearch
*/
protected function searchQueryBuilder(
ServerRequest $request,
QueryBuilder $queryBuilder,
array $fieldsToSearch,
string $searchParam = 'searchPhrase'
): QueryBuilder {
$searchPhrase = $this->getSearchPhrase($request, $searchParam);
if (null === $searchPhrase) {
return $queryBuilder;
}
$searchQuery = [];
foreach ($fieldsToSearch as $field) {
$searchQuery[] = $field . ' LIKE :search';
}
return $queryBuilder->andWhere(
implode(' OR ', $searchQuery)
)->setParameter('search', '%' . $searchPhrase . '%');
}
protected function getSearchPhrase(
ServerRequest $request,
string $searchParam = 'searchPhrase'
): ?string {
return Types::stringOrNull(
$request->getParam($searchParam),
true
);
}
}

View File

@ -12,6 +12,7 @@ use App\Exception\RateLimitExceededException;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\RateLimit; use App\RateLimit;
use App\Utilities\Types;
use Mezzio\Session\SessionCookiePersistenceInterface; use Mezzio\Session\SessionCookiePersistenceInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
@ -69,14 +70,17 @@ final class LoginAction implements SingleActionInterface
return $response->withRedirect($request->getUri()->getPath()); return $response->withRedirect($request->getUri()->getPath());
} }
$user = $auth->authenticate($request->getParam('username'), $request->getParam('password')); $user = $auth->authenticate(
Types::string($request->getParam('username')),
Types::string($request->getParam('password'))
);
if ($user instanceof User) { if ($user instanceof User) {
$session = $request->getSession(); $session = $request->getSession();
// If user selects "remember me", extend the cookie/session lifetime. // If user selects "remember me", extend the cookie/session lifetime.
if ($session instanceof SessionCookiePersistenceInterface) { if ($session instanceof SessionCookiePersistenceInterface) {
$rememberMe = (bool)$request->getParam('remember', 0); $rememberMe = Types::bool($request->getParam('remember'), false, true);
/** @noinspection SummerTimeUnsafeTimeManipulationInspection */ /** @noinspection SummerTimeUnsafeTimeManipulationInspection */
$session->persistSessionFor(($rememberMe) ? 86400 * 14 : 0); $session->persistSessionFor(($rememberMe) ? 86400 * 14 : 0);
} }

View File

@ -8,6 +8,7 @@ use App\Controller\SingleActionInterface;
use App\Entity\User; use App\Entity\User;
use App\Http\Response; use App\Http\Response;
use App\Http\ServerRequest; use App\Http\ServerRequest;
use App\Utilities\Types;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
final class TwoFactorAction implements SingleActionInterface final class TwoFactorAction implements SingleActionInterface
@ -21,7 +22,7 @@ final class TwoFactorAction implements SingleActionInterface
if ($request->isPost()) { if ($request->isPost()) {
$flash = $request->getFlash(); $flash = $request->getFlash();
$otp = $request->getParam('otp'); $otp = Types::string($request->getParam('otp'));
if ($auth->verifyTwoFactor($otp)) { if ($auth->verifyTwoFactor($otp)) {
/** @var User $user */ /** @var User $user */

View File

@ -13,6 +13,7 @@ use App\Enums\SupportedLocales;
use App\Exception\InvalidRequestAttribute; use App\Exception\InvalidRequestAttribute;
use App\RateLimit; use App\RateLimit;
use App\Session; use App\Session;
use App\Utilities\Types;
use App\View; use App\View;
use Mezzio\Session\SessionInterface; use Mezzio\Session\SessionInterface;
use Slim\Http\ServerRequest as SlimServerRequest; use Slim\Http\ServerRequest as SlimServerRequest;
@ -162,4 +163,13 @@ final class ServerRequest extends SlimServerRequest
return $object; return $object;
} }
public function isInternal(): bool
{
return Types::bool(
$this->getParam('internal', false),
false,
true
);
}
} }

View File

@ -126,7 +126,7 @@ final class BatchUtilities
* @param StorageLocation $storageLocation * @param StorageLocation $storageLocation
* @param array $paths * @param array $paths
* *
* @return iterable|StationMedia[] * @return iterable<StationMedia>
*/ */
public function iterateMedia(StorageLocation $storageLocation, array $paths): iterable public function iterateMedia(StorageLocation $storageLocation, array $paths): iterable
{ {
@ -141,7 +141,7 @@ final class BatchUtilities
* @param StorageLocation $storageLocation * @param StorageLocation $storageLocation
* @param string $dir * @param string $dir
* *
* @return iterable|StationMedia[] * @return iterable<StationMedia>
*/ */
public function iterateMediaInDirectory(StorageLocation $storageLocation, string $dir): iterable public function iterateMediaInDirectory(StorageLocation $storageLocation, string $dir): iterable
{ {
@ -166,7 +166,7 @@ final class BatchUtilities
* @param StorageLocation $storageLocation * @param StorageLocation $storageLocation
* @param array $paths * @param array $paths
* *
* @return iterable|UnprocessableMedia[] * @return iterable<UnprocessableMedia>
*/ */
public function iterateUnprocessableMedia(StorageLocation $storageLocation, array $paths): iterable public function iterateUnprocessableMedia(StorageLocation $storageLocation, array $paths): iterable
{ {
@ -181,7 +181,7 @@ final class BatchUtilities
* @param StorageLocation $storageLocation * @param StorageLocation $storageLocation
* @param string $dir * @param string $dir
* *
* @return iterable|UnprocessableMedia[] * @return iterable<UnprocessableMedia>
*/ */
public function iterateUnprocessableMediaInDirectory( public function iterateUnprocessableMediaInDirectory(
StorageLocation $storageLocation, StorageLocation $storageLocation,
@ -204,7 +204,7 @@ final class BatchUtilities
* @param StorageLocation $storageLocation * @param StorageLocation $storageLocation
* @param string $dir * @param string $dir
* *
* @return iterable|StationPlaylistFolder[] * @return iterable<StationPlaylistFolder>
*/ */
public function iteratePlaylistFoldersInDirectory( public function iteratePlaylistFoldersInDirectory(
StorageLocation $storageLocation, StorageLocation $storageLocation,