diff --git a/src/Controller/Api/AbstractApiCrudController.php b/src/Controller/Api/AbstractApiCrudController.php index d2d148740..95e753a73 100644 --- a/src/Controller/Api/AbstractApiCrudController.php +++ b/src/Controller/Api/AbstractApiCrudController.php @@ -12,6 +12,7 @@ use App\Exception\ValidationException; use App\Http\Response; use App\Http\ServerRequest; use App\Paginator; +use App\Utilities\Types; use Doctrine\ORM\Query; use InvalidArgumentException; use Psr\Http\Message\ResponseInterface; @@ -124,7 +125,7 @@ abstract class AbstractApiCrudController $return = $this->toArray($record); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = Types::bool($request->getParam('internal'), false, true); $router = $request->getRouter(); if ($record instanceof IdentifiableEntityInterface) { diff --git a/src/Controller/Api/Admin/Debug/TelnetAction.php b/src/Controller/Api/Admin/Debug/TelnetAction.php index 41f757dc6..fbad64ff5 100644 --- a/src/Controller/Api/Admin/Debug/TelnetAction.php +++ b/src/Controller/Api/Admin/Debug/TelnetAction.php @@ -9,6 +9,7 @@ use App\Controller\SingleActionInterface; use App\Http\Response; use App\Http\ServerRequest; use App\Radio\Adapters; +use App\Utilities\Types; use Monolog\Handler\TestHandler; use Monolog\Level; use Psr\Http\Message\ResponseInterface; @@ -33,7 +34,7 @@ final class TelnetAction implements SingleActionInterface $station = $request->getStation(); $backend = $this->adapters->requireBackendAdapter($station); - $command = $request->getParam('command'); + $command = Types::string($request->getParam('command')); $telnetResponse = $backend->command($station, $command); $this->logger->debug( diff --git a/src/Controller/Api/Admin/RolesController.php b/src/Controller/Api/Admin/RolesController.php index eecdaf049..71331968f 100644 --- a/src/Controller/Api/Admin/RolesController.php +++ b/src/Controller/Api/Admin/RolesController.php @@ -6,6 +6,7 @@ namespace App\Controller\Api\Admin; use App\Acl; use App\Controller\Api\AbstractApiCrudController; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Entity\Repository\RolePermissionRepository; use App\Entity\Role; @@ -138,6 +139,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; final class RolesController extends AbstractApiCrudController { use CanSortResults; + use CanSearchResults; protected string $entityClass = Role::class; protected string $resourceRouteName = 'api:admin:role'; @@ -174,11 +176,13 @@ final class RolesController extends AbstractApiCrudController 'r.name' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('(r.name LIKE :name)') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'r.name', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } diff --git a/src/Controller/Api/Admin/SendTestMessageAction.php b/src/Controller/Api/Admin/SendTestMessageAction.php index dbda27d72..e8eccff0b 100644 --- a/src/Controller/Api/Admin/SendTestMessageAction.php +++ b/src/Controller/Api/Admin/SendTestMessageAction.php @@ -11,6 +11,7 @@ use App\Exception\ValidationException; use App\Http\Response; use App\Http\ServerRequest; use App\Service\Mail; +use App\Utilities\Types; use Psr\Http\Message\ResponseInterface; use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Validator\Constraints\Email; @@ -30,7 +31,7 @@ final class SendTestMessageAction implements SingleActionInterface Response $response, array $params ): ResponseInterface { - $emailAddress = $request->getParam('email', ''); + $emailAddress = Types::string($request->getParam('email')); $errors = $this->validator->validate( $emailAddress, diff --git a/src/Controller/Api/Admin/StationsController.php b/src/Controller/Api/Admin/StationsController.php index 6dbff41cc..262adfaf8 100644 --- a/src/Controller/Api/Admin/StationsController.php +++ b/src/Controller/Api/Admin/StationsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Api\Admin; use App\Controller\Api\AbstractApiCrudController; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Entity\Repository\StationQueueRepository; use App\Entity\Repository\StationRepository; @@ -142,6 +143,7 @@ use Throwable; class StationsController extends AbstractApiCrudController { use CanSortResults; + use CanSearchResults; protected string $entityClass = Station::class; protected string $resourceRouteName = 'api:admin:station'; @@ -175,11 +177,14 @@ class StationsController extends AbstractApiCrudController 'e.name' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('(e.name LIKE :name OR e.short_name LIKE :name)') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'e.name', + 'e.short_name', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } @@ -192,7 +197,7 @@ class StationsController extends AbstractApiCrudController $return = $this->toArray($record); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = $request->isInternal(); $router = $request->getRouter(); $return['links'] = [ diff --git a/src/Controller/Api/Admin/UsersController.php b/src/Controller/Api/Admin/UsersController.php index 28a0b1a2e..6ba8340c5 100644 --- a/src/Controller/Api/Admin/UsersController.php +++ b/src/Controller/Api/Admin/UsersController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Api\Admin; use App\Controller\Api\AbstractApiCrudController; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Controller\Frontend\Account\MasqueradeAction; use App\Entity\Api\Error; @@ -134,6 +135,7 @@ use Psr\Http\Message\ResponseInterface; class UsersController extends AbstractApiCrudController { use CanSortResults; + use CanSearchResults; protected string $entityClass = User::class; protected string $resourceRouteName = 'api:admin:user'; @@ -156,11 +158,14 @@ class UsersController extends AbstractApiCrudController 'e.name' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('(e.name LIKE :name OR e.email LIKE :name)') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'e.name', + 'e.email', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } @@ -173,7 +178,7 @@ class UsersController extends AbstractApiCrudController $return = $this->toArray($record); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = $request->isInternal(); $router = $request->getRouter(); $csrf = $request->getCsrf(); $currentUser = $request->getUser(); diff --git a/src/Controller/Api/Frontend/Dashboard/StationsAction.php b/src/Controller/Api/Frontend/Dashboard/StationsAction.php index 2267ffd7d..537d290c8 100644 --- a/src/Controller/Api/Frontend/Dashboard/StationsAction.php +++ b/src/Controller/Api/Frontend/Dashboard/StationsAction.php @@ -6,6 +6,8 @@ namespace App\Controller\Api\Frontend\Dashboard; use App\Container\EntityManagerAwareTrait; use App\Container\SettingsAwareTrait; +use App\Controller\Api\Traits\CanSearchResults; +use App\Controller\Api\Traits\CanSortResults; use App\Controller\SingleActionInterface; use App\Entity\Api\Dashboard; use App\Entity\ApiGenerator\NowPlayingApiGenerator; @@ -20,6 +22,8 @@ final class StationsAction implements SingleActionInterface { use EntityManagerAwareTrait; use SettingsAwareTrait; + use CanSortResults; + use CanSearchResults; public function __construct( private readonly NowPlayingApiGenerator $npApiGenerator @@ -69,8 +73,8 @@ final class StationsAction implements SingleActionInterface $viewStations[] = $row; } - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { + $searchPhrase = $this->getSearchPhrase($request); + if (null !== $searchPhrase) { $viewStations = array_filter( $viewStations, static function (Dashboard $row) use ($searchPhrase) { @@ -79,22 +83,15 @@ final class StationsAction implements SingleActionInterface ); } - $sort = $request->getParam('sort'); - usort( + $viewStations = $this->sortArray( + $request, $viewStations, - static function (Dashboard $a, Dashboard $b) use ($sort) { - if ('listeners' === $sort) { - return $a->listeners->current <=> $b->listeners->current; - } - - return $a->station->name <=> $b->station->name; - } + [ + 'listeners' => 'listeners.current', + ], + 'station.name' ); - if ('desc' === strtolower($request->getParam('sortOrder', 'asc'))) { - $viewStations = array_reverse($viewStations); - } - return Paginator::fromArray($viewStations, $request)->write($response); } } diff --git a/src/Controller/Api/Internal/ListenerAuthAction.php b/src/Controller/Api/Internal/ListenerAuthAction.php index 6819e4a73..d0ae4779a 100644 --- a/src/Controller/Api/Internal/ListenerAuthAction.php +++ b/src/Controller/Api/Internal/ListenerAuthAction.php @@ -11,6 +11,7 @@ use App\Exception\PermissionDeniedException; use App\Http\Response; use App\Http\ServerRequest; use App\Radio\Frontend\Blocklist\BlocklistParser; +use App\Utilities\Types; use Psr\Http\Message\ResponseInterface; final class ListenerAuthAction implements SingleActionInterface @@ -46,7 +47,7 @@ final class ListenerAuthAction implements SingleActionInterface } $station = $request->getStation(); - $listenerIp = $request->getParam('ip') ?? ''; + $listenerIp = Types::string($request->getParam('ip')); if ($this->blocklistParser->isAllowed($station, $listenerIp)) { return $response->withHeader('icecast-auth-user', '1'); diff --git a/src/Controller/Api/Stations/Files/BatchAction.php b/src/Controller/Api/Stations/Files/BatchAction.php index c95fe0029..f87f05e26 100644 --- a/src/Controller/Api/Stations/Files/BatchAction.php +++ b/src/Controller/Api/Stations/Files/BatchAction.php @@ -66,7 +66,7 @@ final class BatchAction implements SingleActionInterface $fsMedia = $this->stationFilesystems->getMediaFilesystem($station); - $result = match ($request->getParam('do')) { + $result = match (Types::string($request->getParam('do'))) { 'delete' => $this->doDelete($request, $station, $storageLocation, $fsMedia), 'playlist' => $this->doPlaylist($request, $station, $storageLocation, $fsMedia), 'move' => $this->doMove($request, $station, $storageLocation, $fsMedia), @@ -141,10 +141,15 @@ final class BatchAction implements SingleActionInterface /** @var array $affectedPlaylistIds */ $affectedPlaylistIds = []; - foreach ($request->getParam('playlists') as $playlistId) { + /** @var string[] $requestPlaylists */ + $requestPlaylists = Types::array($request->getParam('playlists')); + + foreach ($requestPlaylists as $playlistId) { if ('new' === $playlistId) { $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->flush(); @@ -216,8 +221,8 @@ final class BatchAction implements SingleActionInterface ): BatchResult { $result = $this->parseRequest($request, $fs); - $from = $request->getParam('currentDirectory', ''); - $to = $request->getParam('directory', ''); + $from = Types::string($request->getParam('currentDirectory')); + $to = Types::string($request->getParam('directory')); $toMove = [ $this->batchUtilities->iterateMedia($storageLocation, $result->files), diff --git a/src/Controller/Api/Stations/Files/ListAction.php b/src/Controller/Api/Stations/Files/ListAction.php index 0a2099848..4d22f8a8b 100644 --- a/src/Controller/Api/Stations/Files/ListAction.php +++ b/src/Controller/Api/Stations/Files/ListAction.php @@ -74,7 +74,7 @@ final class ListAction implements SingleActionInterface $cacheKey = implode('.', $cacheKeyParts); - $flushCache = Types::bool($request->getParam('flushCache')); + $flushCache = Types::bool($request->getParam('flushCache'), false, true); if (!$flushCache && $this->cache->has($cacheKey)) { /** @var array $result */ diff --git a/src/Controller/Api/Stations/Files/MakeDirectoryAction.php b/src/Controller/Api/Stations/Files/MakeDirectoryAction.php index 3cebb2f4a..6eedf3520 100644 --- a/src/Controller/Api/Stations/Files/MakeDirectoryAction.php +++ b/src/Controller/Api/Stations/Files/MakeDirectoryAction.php @@ -10,6 +10,7 @@ use App\Entity\Api\Status; use App\Flysystem\StationFilesystems; use App\Http\Response; use App\Http\ServerRequest; +use App\Utilities\Types; use League\Flysystem\UnableToCreateDirectory; use Psr\Http\Message\ResponseInterface; @@ -25,8 +26,8 @@ final class MakeDirectoryAction implements SingleActionInterface Response $response, array $params ): ResponseInterface { - $currentDir = $request->getParam('currentDirectory', ''); - $newDirName = $request->getParam('name', ''); + $currentDir = Types::string($request->getParam('currentDirectory')); + $newDirName = Types::string($request->getParam('name')); if (empty($newDirName)) { return $response->withStatus(400) diff --git a/src/Controller/Api/Stations/MountsController.php b/src/Controller/Api/Stations/MountsController.php index 0bfaf0f05..f11704799 100644 --- a/src/Controller/Api/Stations/MountsController.php +++ b/src/Controller/Api/Stations/MountsController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Entity\Repository\StationMountRepository; use App\Entity\StationMount; @@ -144,6 +145,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; final class MountsController extends AbstractStationApiCrudController { use CanSortResults; + use CanSearchResults; protected string $entityClass = StationMount::class; protected string $resourceRouteName = 'api:stations:mount'; @@ -180,11 +182,14 @@ final class MountsController extends AbstractStationApiCrudController 'e.display_name' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('(e.name LIKE :name OR e.display_name LIKE :name)') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'e.name', + 'e.display_name', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } diff --git a/src/Controller/Api/Stations/Playlists/PutOrderAction.php b/src/Controller/Api/Stations/Playlists/PutOrderAction.php index a507ad05b..53b5e39e9 100644 --- a/src/Controller/Api/Stations/Playlists/PutOrderAction.php +++ b/src/Controller/Api/Stations/Playlists/PutOrderAction.php @@ -12,6 +12,7 @@ use App\Entity\Repository\StationPlaylistRepository; use App\Exception; use App\Http\Response; use App\Http\ServerRequest; +use App\Utilities\Types; use Psr\Http\Message\ResponseInterface; final class PutOrderAction implements SingleActionInterface @@ -39,7 +40,7 @@ final class PutOrderAction implements SingleActionInterface 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); return $response->withJson($order); diff --git a/src/Controller/Api/Stations/PlaylistsController.php b/src/Controller/Api/Stations/PlaylistsController.php index 2e23fa0f5..a289ec3a4 100644 --- a/src/Controller/Api/Stations/PlaylistsController.php +++ b/src/Controller/Api/Stations/PlaylistsController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Entity\Enums\PlaylistOrders; use App\Entity\Enums\PlaylistSources; @@ -12,6 +13,7 @@ use App\Entity\StationSchedule; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; +use App\Utilities\Types; use Carbon\CarbonInterface; use Doctrine\ORM\AbstractQuery; use InvalidArgumentException; @@ -145,6 +147,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; final class PlaylistsController extends AbstractScheduledEntityController { use CanSortResults; + use CanSearchResults; protected string $entityClass = StationPlaylist::class; protected string $resourceRouteName = 'api:stations:playlist'; @@ -172,11 +175,13 @@ final class PlaylistsController extends AbstractScheduledEntityController 'sp.name' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('sp.name LIKE :name') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'sp.name', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } @@ -261,7 +266,7 @@ final class PlaylistsController extends AbstractScheduledEntityController $return['num_songs'] = $songTotals['num_songs']; $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(); $return['links'] = [ diff --git a/src/Controller/Api/Stations/PodcastEpisodesController.php b/src/Controller/Api/Stations/PodcastEpisodesController.php index 4b1fe95fb..27b3ffe55 100644 --- a/src/Controller/Api/Stations/PodcastEpisodesController.php +++ b/src/Controller/Api/Stations/PodcastEpisodesController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; use App\Controller\Api\AbstractApiCrudController; +use App\Controller\Api\Traits\CanSearchResults; use App\Entity\Api\PodcastEpisode as ApiPodcastEpisode; use App\Entity\Api\PodcastMedia as ApiPodcastMedia; use App\Entity\PodcastEpisode; @@ -16,6 +17,7 @@ use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; use App\Service\Flow\UploadedFile; +use App\Utilities\Types; use InvalidArgumentException; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -185,6 +187,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; ] final class PodcastEpisodesController extends AbstractApiCrudController { + use CanSearchResults; + protected string $entityClass = PodcastEpisode::class; protected string $resourceRouteName = 'api:stations:podcast:episode'; @@ -218,11 +222,13 @@ final class PodcastEpisodesController extends AbstractApiCrudController ->orderBy('e.created_at', 'DESC') ->setParameter('podcast', $podcast); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $queryBuilder->andWhere('e.title LIKE :title') - ->setParameter('title', '%' . $searchPhrase . '%'); - } + $queryBuilder = $this->searchQueryBuilder( + $request, + $queryBuilder, + [ + 'e.title', + ] + ); 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)); } - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = Types::bool($request->getParam('internal'), false, true); $router = $request->getRouter(); $return = new ApiPodcastEpisode(); diff --git a/src/Controller/Api/Stations/PodcastsController.php b/src/Controller/Api/Stations/PodcastsController.php index 0324e4ae8..78e965337 100644 --- a/src/Controller/Api/Stations/PodcastsController.php +++ b/src/Controller/Api/Stations/PodcastsController.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; use App\Controller\Api\AbstractApiCrudController; +use App\Controller\Api\Traits\CanSearchResults; use App\Entity\Api\Podcast as ApiPodcast; use App\Entity\Podcast; use App\Entity\PodcastCategory; @@ -14,6 +15,7 @@ use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; use App\Service\Flow\UploadedFile; +use App\Utilities\Types; use InvalidArgumentException; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -147,6 +149,8 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; ] final class PodcastsController extends AbstractApiCrudController { + use CanSearchResults; + protected string $entityClass = Podcast::class; protected string $resourceRouteName = 'api:stations:podcast'; @@ -173,11 +177,13 @@ final class PodcastsController extends AbstractApiCrudController ->orderBy('p.title', 'ASC') ->setParameter('storageLocation', $station->getPodcastsStorageLocation()); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $queryBuilder->andWhere('p.title LIKE :title') - ->setParameter('title', '%' . $searchPhrase . '%'); - } + $queryBuilder = $this->searchQueryBuilder( + $request, + $queryBuilder, + [ + 'p.title', + ] + ); 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)); } - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = Types::bool($request->getParam('internal'), false, true); $router = $request->getRouter(); $station = $request->getStation(); diff --git a/src/Controller/Api/Stations/QueueController.php b/src/Controller/Api/Stations/QueueController.php index ea47b92d3..3c9ea44c9 100644 --- a/src/Controller/Api/Stations/QueueController.php +++ b/src/Controller/Api/Stations/QueueController.php @@ -13,6 +13,7 @@ use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; use App\Radio\AutoDJ\Queue; +use App\Utilities\Types; use InvalidArgumentException; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -147,7 +148,7 @@ final class QueueController extends AbstractStationApiCrudController $row = ($this->queueApiGenerator)($record); $row->resolveUrls($router->getBaseUrl()); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = Types::bool($request->getParam('internal'), false, true); $apiResponse = new StationQueueDetailed(); $apiResponse->fromParentObject($row); diff --git a/src/Controller/Api/Stations/RemotesController.php b/src/Controller/Api/Stations/RemotesController.php index 00782ec8b..9678e586d 100644 --- a/src/Controller/Api/Stations/RemotesController.php +++ b/src/Controller/Api/Stations/RemotesController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Entity\Api\StationRemote as ApiStationRemote; use App\Entity\StationRemote; @@ -11,6 +12,7 @@ use App\Exception\PermissionDeniedException; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; +use App\Utilities\Types; use InvalidArgumentException; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -143,6 +145,7 @@ use Psr\Http\Message\ResponseInterface; final class RemotesController extends AbstractStationApiCrudController { use CanSortResults; + use CanSearchResults; protected string $entityClass = StationRemote::class; protected string $resourceRouteName = 'api:stations:remote'; @@ -170,11 +173,13 @@ final class RemotesController extends AbstractStationApiCrudController 'e.display_name' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('(e.display_name LIKE :name)') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'e.display_name', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } @@ -192,7 +197,7 @@ final class RemotesController extends AbstractStationApiCrudController $return = new ApiStationRemote(); $return->fromParentObject($returnArray); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = Types::bool($request->getParam('internal'), false, true); $router = $request->getRouter(); $return->is_editable = $record->isEditable(); diff --git a/src/Controller/Api/Stations/Reports/RequestsController.php b/src/Controller/Api/Stations/Reports/RequestsController.php index bc5090e4c..5e6890d7e 100644 --- a/src/Controller/Api/Stations/Reports/RequestsController.php +++ b/src/Controller/Api/Stations/Reports/RequestsController.php @@ -5,18 +5,23 @@ declare(strict_types=1); namespace App\Controller\Api\Stations\Reports; use App\Container\EntityManagerAwareTrait; +use App\Controller\Api\Traits\CanSearchResults; +use App\Controller\Api\Traits\CanSortResults; use App\Entity\Api\Status; use App\Entity\Repository\StationRequestRepository; use App\Entity\StationRequest; use App\Http\Response; use App\Http\ServerRequest; use App\Paginator; +use App\Utilities\Types; use Doctrine\ORM\AbstractQuery; use Psr\Http\Message\ResponseInterface; final class RequestsController { use EntityManagerAwareTrait; + use CanSortResults; + use CanSearchResults; public function __construct( private readonly StationRequestRepository $requestRepo @@ -36,33 +41,35 @@ final class RequestsController ->where('sr.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'), default => $qb->andWhere('sr.played_at = 0'), }; - $queryParams = $request->getQueryParams(); - $searchPhrase = trim($queryParams['searchPhrase'] ?? ''); + $qb = $this->sortQueryBuilder( + $request, + $qb, + [ + 'name' => 'sm.title', + 'title' => 'sm.title', + 'artist' => 'sm.artist', + 'album' => 'sm.album', + 'genre' => 'sm.genre', + ], + 'sr.timestamp', + 'DESC' + ); - $sortField = (string)($queryParams['sort'] ?? ''); - $sortDirection = strtolower($queryParams['sortOrder'] ?? 'asc'); - - if (!empty($sortField)) { - match ($sortField) { - 'name', 'title' => $qb->addOrderBy('sm.title', $sortDirection), - 'artist' => $qb->addOrderBy('sm.artist', $sortDirection), - '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 . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'sm.title', + 'sm.artist', + 'sm.album', + ] + ); $query = $qb->getQuery() ->setHydrationMode(AbstractQuery::HYDRATE_ARRAY); diff --git a/src/Controller/Api/Stations/Streamers/BroadcastsController.php b/src/Controller/Api/Stations/Streamers/BroadcastsController.php index 05ec8b669..1e7b55da2 100644 --- a/src/Controller/Api/Stations/Streamers/BroadcastsController.php +++ b/src/Controller/Api/Stations/Streamers/BroadcastsController.php @@ -76,7 +76,7 @@ final class BroadcastsController extends AbstractApiCrudController $paginator = Paginator::fromQuery($query, $request); $router = $request->getRouter(); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = $request->isInternal(); $fsRecordings = $this->stationFilesystems->getRecordingsFilesystem($station); $paginator->setPostprocessor( diff --git a/src/Controller/Api/Stations/StreamersController.php b/src/Controller/Api/Stations/StreamersController.php index d6463df02..edc80fa69 100644 --- a/src/Controller/Api/Stations/StreamersController.php +++ b/src/Controller/Api/Stations/StreamersController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Entity\Repository\StationScheduleRepository; use App\Entity\Repository\StationStreamerRepository; @@ -14,6 +15,7 @@ use App\Http\ServerRequest; use App\OpenApi; use App\Radio\AutoDJ\Scheduler; use App\Service\Flow\UploadedFile; +use App\Utilities\Types; use Carbon\CarbonInterface; use InvalidArgumentException; use OpenApi\Attributes as OA; @@ -149,6 +151,7 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; final class StreamersController extends AbstractScheduledEntityController { use CanSortResults; + use CanSearchResults; protected string $entityClass = StationStreamer::class; protected string $resourceRouteName = 'api:stations:streamer'; @@ -186,11 +189,14 @@ final class StreamersController extends AbstractScheduledEntityController 'e.streamer_username' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('(e.streamer_username LIKE :name OR e.display_name LIKE :name)') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'e.streamer_username', + 'e.display_name', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } @@ -275,7 +281,7 @@ final class StreamersController extends AbstractScheduledEntityController $return = parent::viewRecord($record, $request); $router = $request->getRouter(); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = Types::bool($request->getParam('internal'), false, true); $return['has_custom_art'] = (0 !== $record->getArtUpdatedAt()); diff --git a/src/Controller/Api/Stations/WebhooksController.php b/src/Controller/Api/Stations/WebhooksController.php index c16781889..0da00b6d5 100644 --- a/src/Controller/Api/Stations/WebhooksController.php +++ b/src/Controller/Api/Stations/WebhooksController.php @@ -4,11 +4,13 @@ declare(strict_types=1); namespace App\Controller\Api\Stations; +use App\Controller\Api\Traits\CanSearchResults; use App\Controller\Api\Traits\CanSortResults; use App\Entity\StationWebhook; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; +use App\Utilities\Types; use InvalidArgumentException; use OpenApi\Attributes as OA; use Psr\Http\Message\ResponseInterface; @@ -141,6 +143,7 @@ use Psr\Http\Message\ResponseInterface; final class WebhooksController extends AbstractStationApiCrudController { use CanSortResults; + use CanSearchResults; protected string $entityClass = StationWebhook::class; protected string $resourceRouteName = 'api:stations:webhook'; @@ -171,11 +174,13 @@ final class WebhooksController extends AbstractStationApiCrudController 'e.name' ); - $searchPhrase = trim($request->getParam('searchPhrase', '')); - if (!empty($searchPhrase)) { - $qb->andWhere('(e.name LIKE :name)') - ->setParameter('name', '%' . $searchPhrase . '%'); - } + $qb = $this->searchQueryBuilder( + $request, + $qb, + [ + 'e.name', + ] + ); return $this->listPaginatedFromQuery($request, $response, $qb->getQuery()); } @@ -188,7 +193,7 @@ final class WebhooksController extends AbstractStationApiCrudController $return = $this->toArray($record); - $isInternal = ('true' === $request->getParam('internal', 'false')); + $isInternal = Types::bool($request->getParam('internal'), false, true); $router = $request->getRouter(); $return['links'] = [ diff --git a/src/Controller/Api/Traits/CanSearchResults.php b/src/Controller/Api/Traits/CanSearchResults.php new file mode 100644 index 000000000..cf73ff377 --- /dev/null +++ b/src/Controller/Api/Traits/CanSearchResults.php @@ -0,0 +1,46 @@ +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 + ); + } +} diff --git a/src/Controller/Frontend/Account/LoginAction.php b/src/Controller/Frontend/Account/LoginAction.php index e86f23f7d..f00fe3ead 100644 --- a/src/Controller/Frontend/Account/LoginAction.php +++ b/src/Controller/Frontend/Account/LoginAction.php @@ -12,6 +12,7 @@ use App\Exception\RateLimitExceededException; use App\Http\Response; use App\Http\ServerRequest; use App\RateLimit; +use App\Utilities\Types; use Mezzio\Session\SessionCookiePersistenceInterface; use Psr\Http\Message\ResponseInterface; @@ -69,14 +70,17 @@ final class LoginAction implements SingleActionInterface 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) { $session = $request->getSession(); // If user selects "remember me", extend the cookie/session lifetime. if ($session instanceof SessionCookiePersistenceInterface) { - $rememberMe = (bool)$request->getParam('remember', 0); + $rememberMe = Types::bool($request->getParam('remember'), false, true); /** @noinspection SummerTimeUnsafeTimeManipulationInspection */ $session->persistSessionFor(($rememberMe) ? 86400 * 14 : 0); } diff --git a/src/Controller/Frontend/Account/TwoFactorAction.php b/src/Controller/Frontend/Account/TwoFactorAction.php index 0b377db51..4a4e11ba3 100644 --- a/src/Controller/Frontend/Account/TwoFactorAction.php +++ b/src/Controller/Frontend/Account/TwoFactorAction.php @@ -8,6 +8,7 @@ use App\Controller\SingleActionInterface; use App\Entity\User; use App\Http\Response; use App\Http\ServerRequest; +use App\Utilities\Types; use Psr\Http\Message\ResponseInterface; final class TwoFactorAction implements SingleActionInterface @@ -21,7 +22,7 @@ final class TwoFactorAction implements SingleActionInterface if ($request->isPost()) { $flash = $request->getFlash(); - $otp = $request->getParam('otp'); + $otp = Types::string($request->getParam('otp')); if ($auth->verifyTwoFactor($otp)) { /** @var User $user */ diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index f8f19e838..2638dedc4 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -13,6 +13,7 @@ use App\Enums\SupportedLocales; use App\Exception\InvalidRequestAttribute; use App\RateLimit; use App\Session; +use App\Utilities\Types; use App\View; use Mezzio\Session\SessionInterface; use Slim\Http\ServerRequest as SlimServerRequest; @@ -162,4 +163,13 @@ final class ServerRequest extends SlimServerRequest return $object; } + + public function isInternal(): bool + { + return Types::bool( + $this->getParam('internal', false), + false, + true + ); + } } diff --git a/src/Media/BatchUtilities.php b/src/Media/BatchUtilities.php index 053a93a7c..c3a24eaa9 100644 --- a/src/Media/BatchUtilities.php +++ b/src/Media/BatchUtilities.php @@ -126,7 +126,7 @@ final class BatchUtilities * @param StorageLocation $storageLocation * @param array $paths * - * @return iterable|StationMedia[] + * @return iterable */ public function iterateMedia(StorageLocation $storageLocation, array $paths): iterable { @@ -141,7 +141,7 @@ final class BatchUtilities * @param StorageLocation $storageLocation * @param string $dir * - * @return iterable|StationMedia[] + * @return iterable */ public function iterateMediaInDirectory(StorageLocation $storageLocation, string $dir): iterable { @@ -166,7 +166,7 @@ final class BatchUtilities * @param StorageLocation $storageLocation * @param array $paths * - * @return iterable|UnprocessableMedia[] + * @return iterable */ public function iterateUnprocessableMedia(StorageLocation $storageLocation, array $paths): iterable { @@ -181,7 +181,7 @@ final class BatchUtilities * @param StorageLocation $storageLocation * @param string $dir * - * @return iterable|UnprocessableMedia[] + * @return iterable */ public function iterateUnprocessableMediaInDirectory( StorageLocation $storageLocation, @@ -204,7 +204,7 @@ final class BatchUtilities * @param StorageLocation $storageLocation * @param string $dir * - * @return iterable|StationPlaylistFolder[] + * @return iterable */ public function iteratePlaylistFoldersInDirectory( StorageLocation $storageLocation,