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\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) {

View File

@ -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(

View File

@ -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());
}

View File

@ -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,

View File

@ -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'] = [

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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');

View File

@ -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<int, int> $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),

View File

@ -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<int, FileList> $result */

View File

@ -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)

View File

@ -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());
}

View File

@ -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);

View File

@ -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'] = [

View File

@ -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();

View File

@ -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();

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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(

View File

@ -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());

View File

@ -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'] = [

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\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);
}

View File

@ -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 */

View File

@ -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
);
}
}

View File

@ -126,7 +126,7 @@ final class BatchUtilities
* @param StorageLocation $storageLocation
* @param array $paths
*
* @return iterable|StationMedia[]
* @return iterable<StationMedia>
*/
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<StationMedia>
*/
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<UnprocessableMedia>
*/
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<UnprocessableMedia>
*/
public function iterateUnprocessableMediaInDirectory(
StorageLocation $storageLocation,
@ -204,7 +204,7 @@ final class BatchUtilities
* @param StorageLocation $storageLocation
* @param string $dir
*
* @return iterable|StationPlaylistFolder[]
* @return iterable<StationPlaylistFolder>
*/
public function iteratePlaylistFoldersInDirectory(
StorageLocation $storageLocation,