From c90b217e738e4682272f29077030cbe650462723 Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Sun, 17 Dec 2023 10:32:42 -0600 Subject: [PATCH] Fixes #6804 -- Clean up exceptions: - Make more exceptions translated - Consolidate duplicate exception classes - Make public-facing exceptions friendlier (don't show "Exception on File LXX" except in console log) --- config/routes/api_station.php | 4 ++- config/routes/public.php | 6 ++-- frontend/src/vendor/axios.ts | 5 +-- .../Api/Admin/Backups/AbstractFileAction.php | 2 +- .../Api/Admin/Debug/TelnetAction.php | 7 +--- .../Stations/AbstractSearchableListAction.php | 5 --- .../Api/Stations/OnDemand/DownloadAction.php | 7 ---- .../Api/Stations/OnDemand/ListAction.php | 14 +++----- .../Api/Stations/Requests/ListAction.php | 14 +++----- .../Api/Stations/Requests/SubmitAction.php | 30 +++++++---------- .../Api/Stations/ServicesController.php | 13 ++------ .../Api/Stations/UpdateMetadataAction.php | 7 +--- src/Controller/Api/Traits/HasLogViewer.php | 2 +- .../Frontend/PWA/AppManifestAction.php | 4 +-- .../Frontend/PublicPages/HistoryAction.php | 4 +-- .../Frontend/PublicPages/OEmbedAction.php | 4 +-- .../Frontend/PublicPages/OnDemandAction.php | 9 ++--- .../Frontend/PublicPages/PlayerAction.php | 4 +-- .../PublicPages/PodcastEpisodeAction.php | 7 ++-- .../PublicPages/PodcastEpisodesAction.php | 7 ++-- .../PublicPages/PodcastFeedAction.php | 9 +++-- .../Frontend/PublicPages/PodcastsAction.php | 4 +-- .../Frontend/PublicPages/RequestsAction.php | 4 +-- .../Frontend/PublicPages/ScheduleAction.php | 4 +-- .../Frontend/PublicPages/WebDjAction.php | 15 +++------ src/Doctrine/Repository.php | 2 +- src/Entity/Api/Error.php | 13 ++++---- .../AbstractStationBasedRepository.php | 2 +- .../Repository/PodcastEpisodeRepository.php | 6 ++-- .../Repository/StationMediaRepository.php | 4 +-- .../Repository/StationRequestRepository.php | 25 +++++++++----- src/Enums/StationFeatures.php | 19 +++++++++++ src/Exception.php | 33 +++---------------- src/Exception/BootstrapException.php | 4 +-- .../CannotCompleteActionException.php | 31 +++++++++++++++++ src/Exception/CannotProcessMediaException.php | 4 +-- src/Exception/CsrfValidationException.php | 4 +-- .../InvalidPodcastMediaFileException.php | 21 ------------ src/Exception/InvalidRequestAttribute.php | 4 +-- src/Exception/NoFileUploadedException.php | 4 +-- src/Exception/NotFoundException.php | 24 ++++++++++++-- src/Exception/NotLoggedInException.php | 4 +-- src/Exception/PermissionDeniedException.php | 4 +-- src/Exception/PodcastNotFoundException.php | 21 ------------ src/Exception/RateLimitExceededException.php | 4 +-- src/Exception/StationNotFoundException.php | 21 ------------ src/Exception/StationUnsupportedException.php | 25 ++++++++++++-- .../StorageLocationFullException.php | 4 +-- .../Supervisor/AlreadyRunningException.php | 4 +-- .../Supervisor/NotRunningException.php | 4 +-- src/Exception/ValidationException.php | 4 +-- src/Http/ErrorHandler.php | 6 ++-- ...quirePublishedPodcastEpisodeMiddleware.php | 4 +-- src/Middleware/RequireStation.php | 4 +-- src/Middleware/StationSupportsFeature.php | 5 +-- src/Radio/Adapters.php | 33 ++++++++++++++++++- 56 files changed, 261 insertions(+), 277 deletions(-) create mode 100644 src/Exception/CannotCompleteActionException.php delete mode 100644 src/Exception/InvalidPodcastMediaFileException.php delete mode 100644 src/Exception/PodcastNotFoundException.php delete mode 100644 src/Exception/StationNotFoundException.php diff --git a/config/routes/api_station.php b/config/routes/api_station.php index 86175b017..7ce2f9ce5 100644 --- a/config/routes/api_station.php +++ b/config/routes/api_station.php @@ -40,10 +40,12 @@ return static function (RouteCollectorProxy $group) { // On-Demand Streaming $group->get('/ondemand', Controller\Api\Stations\OnDemand\ListAction::class) - ->setName('api:stations:ondemand:list'); + ->setName('api:stations:ondemand:list') + ->add(new Middleware\StationSupportsFeature(StationFeatures::OnDemand)); $group->get('/ondemand/download/{media_id}', Controller\Api\Stations\OnDemand\DownloadAction::class) ->setName('api:stations:ondemand:download') + ->add(new Middleware\StationSupportsFeature(StationFeatures::OnDemand)) ->add(new Middleware\RateLimit('ondemand', 1, 2)); // Podcast Public Pages diff --git a/config/routes/public.php b/config/routes/public.php index 00ad458e7..55d3437c2 100644 --- a/config/routes/public.php +++ b/config/routes/public.php @@ -38,7 +38,8 @@ return static function (RouteCollectorProxy $app) { ->setName('public:manifest'); $group->get('/embed-requests', Controller\Frontend\PublicPages\RequestsAction::class) - ->setName('public:embedrequests'); + ->setName('public:embedrequests') + ->add(new Middleware\StationSupportsFeature(App\Enums\StationFeatures::Requests)); $group->get('/playlist[.{format}]', Controller\Frontend\PublicPages\PlaylistAction::class) ->setName('public:playlist'); @@ -50,7 +51,8 @@ return static function (RouteCollectorProxy $app) { ->setName('public:dj'); $group->get('/ondemand[/{embed:embed}]', Controller\Frontend\PublicPages\OnDemandAction::class) - ->setName('public:ondemand'); + ->setName('public:ondemand') + ->add(new Middleware\StationSupportsFeature(App\Enums\StationFeatures::OnDemand)); $group->get('/schedule[/{embed:embed}]', Controller\Frontend\PublicPages\ScheduleAction::class) ->setName('public:schedule'); diff --git a/frontend/src/vendor/axios.ts b/frontend/src/vendor/axios.ts index cafb4c806..6949f16f4 100644 --- a/frontend/src/vendor/axios.ts +++ b/frontend/src/vendor/axios.ts @@ -37,8 +37,9 @@ export default function installAxios(vueApp: App) { let notifyMessage = $gettext('An error occurred and your request could not be completed.'); if (error.response) { // Request made and server responded - notifyMessage = error.response.data.message; - console.error(notifyMessage); + const responseJson = error.response.data ?? {}; + notifyMessage = responseJson.message ?? notifyMessage; + console.error(responseJson); } else if (error.request) { // The request was made but no response was received console.error(error.request); diff --git a/src/Controller/Api/Admin/Backups/AbstractFileAction.php b/src/Controller/Api/Admin/Backups/AbstractFileAction.php index a2121ccff..cd531abb5 100644 --- a/src/Controller/Api/Admin/Backups/AbstractFileAction.php +++ b/src/Controller/Api/Admin/Backups/AbstractFileAction.php @@ -36,7 +36,7 @@ abstract class AbstractFileAction implements SingleActionInterface ->getFilesystem(); if (!$fs->fileExists($path)) { - throw new NotFoundException(__('Backup not found.')); + throw NotFoundException::file(); } return [$path, $fs]; diff --git a/src/Controller/Api/Admin/Debug/TelnetAction.php b/src/Controller/Api/Admin/Debug/TelnetAction.php index a7c52ab65..41f757dc6 100644 --- a/src/Controller/Api/Admin/Debug/TelnetAction.php +++ b/src/Controller/Api/Admin/Debug/TelnetAction.php @@ -6,7 +6,6 @@ namespace App\Controller\Api\Admin\Debug; use App\Container\LoggerAwareTrait; use App\Controller\SingleActionInterface; -use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; use App\Radio\Adapters; @@ -32,11 +31,7 @@ final class TelnetAction implements SingleActionInterface $this->logger->pushHandler($testHandler); $station = $request->getStation(); - $backend = $this->adapters->getBackendAdapter($station); - - if (null === $backend) { - throw new StationUnsupportedException(); - } + $backend = $this->adapters->requireBackendAdapter($station); $command = $request->getParam('command'); diff --git a/src/Controller/Api/Stations/AbstractSearchableListAction.php b/src/Controller/Api/Stations/AbstractSearchableListAction.php index 09ddbeaa3..0be92ddb3 100644 --- a/src/Controller/Api/Stations/AbstractSearchableListAction.php +++ b/src/Controller/Api/Stations/AbstractSearchableListAction.php @@ -11,7 +11,6 @@ use App\Entity\StationMedia; use App\Http\ServerRequest; use App\Paginator; use Psr\Cache\CacheItemPoolInterface; -use RuntimeException; abstract class AbstractSearchableListAction implements SingleActionInterface { @@ -31,10 +30,6 @@ abstract class AbstractSearchableListAction implements SingleActionInterface ServerRequest $request, array $playlists ): Paginator { - if (empty($playlists)) { - throw new RuntimeException('This station has no qualifying playlists for this feature.'); - } - $station = $request->getStation(); $queryParams = $request->getQueryParams(); diff --git a/src/Controller/Api/Stations/OnDemand/DownloadAction.php b/src/Controller/Api/Stations/OnDemand/DownloadAction.php index 971fcc422..a575df0bd 100644 --- a/src/Controller/Api/Stations/OnDemand/DownloadAction.php +++ b/src/Controller/Api/Stations/OnDemand/DownloadAction.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Controller\Api\Stations\OnDemand; use App\Controller\SingleActionInterface; -use App\Entity\Api\Error; use App\Entity\Repository\StationMediaRepository; use App\Flysystem\StationFilesystems; use App\Http\Response; @@ -30,12 +29,6 @@ final class DownloadAction implements SingleActionInterface $station = $request->getStation(); - // Verify that the station supports on-demand streaming. - if (!$station->getEnableOnDemand()) { - return $response->withStatus(403) - ->withJson(new Error(403, __('This station does not support on-demand streaming.'))); - } - $media = $this->mediaRepo->requireByUniqueId($mediaId, $station); $fsMedia = $this->stationFilesystems->getMediaFilesystem($station); diff --git a/src/Controller/Api/Stations/OnDemand/ListAction.php b/src/Controller/Api/Stations/OnDemand/ListAction.php index 5d62c6f3a..a4080be5c 100644 --- a/src/Controller/Api/Stations/OnDemand/ListAction.php +++ b/src/Controller/Api/Stations/OnDemand/ListAction.php @@ -5,10 +5,10 @@ declare(strict_types=1); namespace App\Controller\Api\Stations\OnDemand; use App\Controller\Api\Stations\AbstractSearchableListAction; -use App\Entity\Api\Error; use App\Entity\Api\StationOnDemand; use App\Entity\Station; use App\Entity\StationMedia; +use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -22,16 +22,12 @@ final class ListAction extends AbstractSearchableListAction ): ResponseInterface { $station = $request->getStation(); - // Verify that the station supports on-demand streaming. - if (!$station->getEnableOnDemand()) { - return $response->withStatus(403) - ->withJson(new Error(403, __('This station does not support on-demand streaming.'))); + $playlists = $this->getPlaylists($station); + if (empty($playlists)) { + throw StationUnsupportedException::onDemand(); } - $paginator = $this->getPaginator( - $request, - $this->getPlaylists($station) - ); + $paginator = $this->getPaginator($request, $playlists); $router = $request->getRouter(); diff --git a/src/Controller/Api/Stations/Requests/ListAction.php b/src/Controller/Api/Stations/Requests/ListAction.php index 1926074d5..bd7aee850 100644 --- a/src/Controller/Api/Stations/Requests/ListAction.php +++ b/src/Controller/Api/Stations/Requests/ListAction.php @@ -5,12 +5,12 @@ declare(strict_types=1); namespace App\Controller\Api\Stations\Requests; use App\Controller\Api\Stations\AbstractSearchableListAction; -use App\Entity\Api\Error; use App\Entity\Api\StationRequest; use App\Entity\ApiGenerator\SongApiGenerator; use App\Entity\Station; use App\Entity\StationMedia; use App\Entity\StationPlaylist; +use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; use App\OpenApi; @@ -61,16 +61,12 @@ final class ListAction extends AbstractSearchableListAction ): ResponseInterface { $station = $request->getStation(); - // Verify that the station supports on-demand streaming. - if (!$station->getEnableRequests()) { - return $response->withStatus(403) - ->withJson(new Error(403, __('This station does not support requests.'))); + $playlists = $this->getPlaylists($station); + if (empty($playlists)) { + throw StationUnsupportedException::requests(); } - $paginator = $this->getPaginator( - $request, - $this->getPlaylists($station) - ); + $paginator = $this->getPaginator($request, $playlists); $router = $request->getRouter(); diff --git a/src/Controller/Api/Stations/Requests/SubmitAction.php b/src/Controller/Api/Stations/Requests/SubmitAction.php index 321f5927c..1bbe020c5 100644 --- a/src/Controller/Api/Stations/Requests/SubmitAction.php +++ b/src/Controller/Api/Stations/Requests/SubmitAction.php @@ -9,7 +9,6 @@ use App\Controller\SingleActionInterface; use App\Entity\Api\Status; use App\Entity\Repository\StationRequestRepository; use App\Entity\User; -use App\Exception; use App\Exception\InvalidRequestAttribute; use App\Http\Response; use App\Http\ServerRequest; @@ -66,25 +65,18 @@ final class SubmitAction implements SingleActionInterface $user = null; } - $isAuthenticated = ($user instanceof User); + $ip = $this->readSettings()->getIp($request); - try { - $ip = $this->readSettings()->getIp($request); + $this->requestRepo->submit( + $station, + $mediaId, + ($user instanceof User), + $ip, + $request->getHeaderLine('User-Agent') + ); - $this->requestRepo->submit( - $station, - $mediaId, - $isAuthenticated, - $ip, - $request->getHeaderLine('User-Agent') - ); - - return $response->withJson( - new Status(true, __('Your request has been submitted and will be played soon.')) - ); - } catch (Exception $e) { - return $response->withStatus(400) - ->withJson(new Status(false, $e->getMessage())); - } + return $response->withJson( + new Status(true, __('Your request has been submitted and will be played soon.')) + ); } } diff --git a/src/Controller/Api/Stations/ServicesController.php b/src/Controller/Api/Stations/ServicesController.php index 15efaeee2..2b78b9903 100644 --- a/src/Controller/Api/Stations/ServicesController.php +++ b/src/Controller/Api/Stations/ServicesController.php @@ -8,7 +8,6 @@ use App\Container\EntityManagerAwareTrait; use App\Entity\Api\Error; use App\Entity\Api\StationServiceStatus; use App\Entity\Api\Status; -use App\Exception\StationUnsupportedException; use App\Exception\Supervisor\NotRunningException; use App\Http\Response; use App\Http\ServerRequest; @@ -196,11 +195,7 @@ final class ServicesController $do = $params['do'] ?? 'restart'; $station = $request->getStation(); - $frontend = $this->adapters->getFrontendAdapter($station); - - if (null === $frontend) { - throw new StationUnsupportedException(); - } + $frontend = $this->adapters->requireFrontendAdapter($station); switch ($do) { case 'stop': @@ -242,11 +237,7 @@ final class ServicesController $do = $params['do'] ?? 'restart'; $station = $request->getStation(); - $backend = $this->adapters->getBackendAdapter($station); - - if (null === $backend) { - throw new StationUnsupportedException(); - } + $backend = $this->adapters->requireBackendAdapter($station); switch ($do) { case 'skip': diff --git a/src/Controller/Api/Stations/UpdateMetadataAction.php b/src/Controller/Api/Stations/UpdateMetadataAction.php index 91fb0efa5..ea7643882 100644 --- a/src/Controller/Api/Stations/UpdateMetadataAction.php +++ b/src/Controller/Api/Stations/UpdateMetadataAction.php @@ -6,7 +6,6 @@ namespace App\Controller\Api\Stations; use App\Controller\SingleActionInterface; use App\Entity\Api\Status; -use App\Exception\StationUnsupportedException; use App\Http\Response; use App\Http\ServerRequest; use App\Radio\Adapters; @@ -28,11 +27,7 @@ final class UpdateMetadataAction implements SingleActionInterface ): ResponseInterface { $station = $request->getStation(); - $backend = $this->adapters->getBackendAdapter($station); - - if (null === $backend) { - throw new StationUnsupportedException(); - } + $backend = $this->adapters->requireBackendAdapter($station); $allowedMetaFields = [ 'title', diff --git a/src/Controller/Api/Traits/HasLogViewer.php b/src/Controller/Api/Traits/HasLogViewer.php index 95821574d..2f54a92ff 100644 --- a/src/Controller/Api/Traits/HasLogViewer.php +++ b/src/Controller/Api/Traits/HasLogViewer.php @@ -24,7 +24,7 @@ trait HasLogViewer clearstatcache(); if (!is_file($logPath)) { - throw new NotFoundException('Log file not found!'); + throw NotFoundException::file(); } if (!$tailFile) { diff --git a/src/Controller/Frontend/PWA/AppManifestAction.php b/src/Controller/Frontend/PWA/AppManifestAction.php index 838b37ed0..7d52e0d53 100644 --- a/src/Controller/Frontend/PWA/AppManifestAction.php +++ b/src/Controller/Frontend/PWA/AppManifestAction.php @@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PWA; use App\Controller\SingleActionInterface; use App\Enums\SupportedThemes; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -21,7 +21,7 @@ final class AppManifestAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $customization = $request->getCustomization(); diff --git a/src/Controller/Frontend/PublicPages/HistoryAction.php b/src/Controller/Frontend/PublicPages/HistoryAction.php index fc5ce2961..61b810d95 100644 --- a/src/Controller/Frontend/PublicPages/HistoryAction.php +++ b/src/Controller/Frontend/PublicPages/HistoryAction.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use App\VueComponent\NowPlayingComponent; @@ -26,7 +26,7 @@ final class HistoryAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $view = $request->getView(); diff --git a/src/Controller/Frontend/PublicPages/OEmbedAction.php b/src/Controller/Frontend/PublicPages/OEmbedAction.php index b667e3bfe..cb1746a95 100644 --- a/src/Controller/Frontend/PublicPages/OEmbedAction.php +++ b/src/Controller/Frontend/PublicPages/OEmbedAction.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use App\Xml\Writer; @@ -21,7 +21,7 @@ final class OEmbedAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $format = $params['format'] ?? 'json'; diff --git a/src/Controller/Frontend/PublicPages/OnDemandAction.php b/src/Controller/Frontend/PublicPages/OnDemandAction.php index 5191211ea..93e59f551 100644 --- a/src/Controller/Frontend/PublicPages/OnDemandAction.php +++ b/src/Controller/Frontend/PublicPages/OnDemandAction.php @@ -6,8 +6,7 @@ namespace App\Controller\Frontend\PublicPages; use App\Container\EntityManagerAwareTrait; use App\Controller\SingleActionInterface; -use App\Exception\StationNotFoundException; -use App\Exception\StationUnsupportedException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -27,11 +26,7 @@ final class OnDemandAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); - } - - if (!$station->getEnableOnDemand()) { - throw new StationUnsupportedException(); + throw NotFoundException::station(); } // Get list of custom fields. diff --git a/src/Controller/Frontend/PublicPages/PlayerAction.php b/src/Controller/Frontend/PublicPages/PlayerAction.php index 2eef6cd28..8ebe1aecf 100644 --- a/src/Controller/Frontend/PublicPages/PlayerAction.php +++ b/src/Controller/Frontend/PublicPages/PlayerAction.php @@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; use App\Entity\Repository\CustomFieldRepository; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use App\VueComponent\NowPlayingComponent; @@ -35,7 +35,7 @@ final class PlayerAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } // Build Vue props. diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php b/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php index b30184622..45e6d48fc 100644 --- a/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php +++ b/src/Controller/Frontend/PublicPages/PodcastEpisodeAction.php @@ -8,8 +8,7 @@ use App\Controller\SingleActionInterface; use App\Entity\PodcastEpisode; use App\Entity\Repository\PodcastEpisodeRepository; use App\Entity\Repository\PodcastRepository; -use App\Exception\PodcastNotFoundException; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -37,13 +36,13 @@ final class PodcastEpisodeAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId); if ($podcast === null) { - throw new PodcastNotFoundException(); + throw NotFoundException::podcast(); } $episode = $this->episodeRepository->fetchEpisodeForStation($station, $episodeId); diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php b/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php index 98933f5aa..53b05b9d8 100644 --- a/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php +++ b/src/Controller/Frontend/PublicPages/PodcastEpisodesAction.php @@ -8,8 +8,7 @@ use App\Controller\SingleActionInterface; use App\Entity\PodcastEpisode; use App\Entity\Repository\PodcastEpisodeRepository; use App\Entity\Repository\PodcastRepository; -use App\Exception\PodcastNotFoundException; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -34,13 +33,13 @@ final class PodcastEpisodesAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId); if ($podcast === null) { - throw new PodcastNotFoundException(); + throw NotFoundException::podcast(); } $publishedEpisodes = $this->episodeRepository->fetchPublishedEpisodesForPodcast($podcast); diff --git a/src/Controller/Frontend/PublicPages/PodcastFeedAction.php b/src/Controller/Frontend/PublicPages/PodcastFeedAction.php index 3ee5ac8af..9a35ea2a8 100644 --- a/src/Controller/Frontend/PublicPages/PodcastFeedAction.php +++ b/src/Controller/Frontend/PublicPages/PodcastFeedAction.php @@ -11,8 +11,7 @@ use App\Entity\PodcastEpisode; use App\Entity\Repository\PodcastRepository; use App\Entity\Repository\StationRepository; use App\Entity\Station; -use App\Exception\PodcastNotFoundException; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Flysystem\StationFilesystems; use App\Http\Response; use App\Http\RouterInterface; @@ -65,17 +64,17 @@ final class PodcastFeedAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcastId); if ($podcast === null) { - throw new PodcastNotFoundException(); + throw NotFoundException::podcast(); } if (!$this->checkHasPublishedEpisodes($podcast)) { - throw new PodcastNotFoundException(); + throw NotFoundException::podcast(); } $generatedRss = $this->generateRssFeed($podcast, $station, $request); diff --git a/src/Controller/Frontend/PublicPages/PodcastsAction.php b/src/Controller/Frontend/PublicPages/PodcastsAction.php index d1b7109ac..5622d52e8 100644 --- a/src/Controller/Frontend/PublicPages/PodcastsAction.php +++ b/src/Controller/Frontend/PublicPages/PodcastsAction.php @@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; use App\Entity\Repository\PodcastRepository; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -26,7 +26,7 @@ final class PodcastsAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $publishedPodcasts = $this->podcastRepository->fetchPublishedPodcastsForStation($station); diff --git a/src/Controller/Frontend/PublicPages/RequestsAction.php b/src/Controller/Frontend/PublicPages/RequestsAction.php index b83ddc690..42bd2a5b6 100644 --- a/src/Controller/Frontend/PublicPages/RequestsAction.php +++ b/src/Controller/Frontend/PublicPages/RequestsAction.php @@ -6,7 +6,7 @@ namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; use App\Entity\Repository\CustomFieldRepository; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -26,7 +26,7 @@ final class RequestsAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $router = $request->getRouter(); diff --git a/src/Controller/Frontend/PublicPages/ScheduleAction.php b/src/Controller/Frontend/PublicPages/ScheduleAction.php index a5f7664ec..d1dccb419 100644 --- a/src/Controller/Frontend/PublicPages/ScheduleAction.php +++ b/src/Controller/Frontend/PublicPages/ScheduleAction.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; @@ -23,7 +23,7 @@ final class ScheduleAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } $router = $request->getRouter(); diff --git a/src/Controller/Frontend/PublicPages/WebDjAction.php b/src/Controller/Frontend/PublicPages/WebDjAction.php index f2493d8ff..77666b734 100644 --- a/src/Controller/Frontend/PublicPages/WebDjAction.php +++ b/src/Controller/Frontend/PublicPages/WebDjAction.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace App\Controller\Frontend\PublicPages; use App\Controller\SingleActionInterface; -use App\Exception\StationNotFoundException; -use App\Exception\StationUnsupportedException; +use App\Enums\StationFeatures; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use App\Radio\Adapters; @@ -27,17 +27,12 @@ final class WebDjAction implements SingleActionInterface $station = $request->getStation(); if (!$station->getEnablePublicPage()) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } - if (!$station->getEnableStreamers()) { - throw new StationUnsupportedException(); - } + StationFeatures::Streamers->assertSupportedForStation($station); - $backend = $this->adapters->getBackendAdapter($station); - if (null === $backend) { - throw new StationUnsupportedException(); - } + $backend = $this->adapters->requireBackendAdapter($station); $wssUrl = (string)$backend->getWebStreamingUrl($station, $request->getRouter()->getBaseUrl()); diff --git a/src/Doctrine/Repository.php b/src/Doctrine/Repository.php index aa31df0d6..e189af238 100644 --- a/src/Doctrine/Repository.php +++ b/src/Doctrine/Repository.php @@ -58,7 +58,7 @@ class Repository { $record = $this->find($id); if (null === $record) { - throw new NotFoundException(); + throw NotFoundException::generic(); } return $record; } diff --git a/src/Entity/Api/Error.php b/src/Entity/Api/Error.php index 1ce88703b..9e456cf7a 100644 --- a/src/Entity/Api/Error.php +++ b/src/Entity/Api/Error.php @@ -96,21 +96,22 @@ final class Error $className = (new ReflectionClass($e))->getShortName(); - $errorHeader = $className . ' at ' . $e->getFile() . ' L' . $e->getLine(); - $message = $errorHeader . ': ' . $e->getMessage(); - if ($e instanceof Exception) { - $messageFormatted = '' . $errorHeader . ': ' . $e->getFormattedMessage(); + $messageFormatted = $e->getFormattedMessage(); $extraData = $e->getExtraData(); } else { - $messageFormatted = '' . $errorHeader . ': ' . $e->getMessage(); + $messageFormatted = $e->getMessage(); $extraData = []; } + $extraData['class'] = $className; + $extraData['file'] = $e->getFile(); + $extraData['line'] = $e->getLine(); + if ($includeTrace) { $extraData['trace'] = $e->getTrace(); } - return new self($code, $message, $messageFormatted, $extraData, $className); + return new self($code, $e->getMessage(), $messageFormatted, $extraData, $className); } } diff --git a/src/Entity/Repository/AbstractStationBasedRepository.php b/src/Entity/Repository/AbstractStationBasedRepository.php index 1064c79df..0566fbdc9 100644 --- a/src/Entity/Repository/AbstractStationBasedRepository.php +++ b/src/Entity/Repository/AbstractStationBasedRepository.php @@ -36,7 +36,7 @@ abstract class AbstractStationBasedRepository extends Repository { $record = $this->findForStation($id, $station); if (null === $record) { - throw new NotFoundException(); + throw NotFoundException::generic(); } return $record; } diff --git a/src/Entity/Repository/PodcastEpisodeRepository.php b/src/Entity/Repository/PodcastEpisodeRepository.php index 687708ce1..edf557abb 100644 --- a/src/Entity/Repository/PodcastEpisodeRepository.php +++ b/src/Entity/Repository/PodcastEpisodeRepository.php @@ -10,11 +10,11 @@ use App\Entity\PodcastEpisode; use App\Entity\PodcastMedia; use App\Entity\Station; use App\Entity\StorageLocation; -use App\Exception\InvalidPodcastMediaFileException; use App\Exception\StorageLocationFullException; use App\Flysystem\ExtendedFilesystemInterface; use App\Media\AlbumArt; use App\Media\MetadataManager; +use http\Exception\InvalidArgumentException; use League\Flysystem\UnableToDeleteFile; use League\Flysystem\UnableToRetrieveMetadata; @@ -152,8 +152,8 @@ final class PodcastEpisodeRepository extends Repository $metadata = $this->metadataManager->read($uploadPath); if (!in_array($metadata->getMimeType(), ['audio/x-m4a', 'audio/mpeg'])) { - throw new InvalidPodcastMediaFileException( - 'Invalid Podcast Media mime type: ' . $metadata->getMimeType() + throw new InvalidArgumentException( + sprintf('Invalid Podcast Media mime type: %s', $metadata->getMimeType()) ); } diff --git a/src/Entity/Repository/StationMediaRepository.php b/src/Entity/Repository/StationMediaRepository.php index 6d8d15ea2..2d497a03b 100644 --- a/src/Entity/Repository/StationMediaRepository.php +++ b/src/Entity/Repository/StationMediaRepository.php @@ -69,7 +69,7 @@ final class StationMediaRepository extends Repository { $record = $this->findForStation($id, $station); if (null === $record) { - throw new NotFoundException(); + throw NotFoundException::generic(); } return $record; } @@ -136,7 +136,7 @@ final class StationMediaRepository extends Repository ): StationMedia { $record = $this->findByUniqueId($uniqueId, $source); if (null === $record) { - throw new NotFoundException(); + throw NotFoundException::generic(); } return $record; } diff --git a/src/Entity/Repository/StationRequestRepository.php b/src/Entity/Repository/StationRequestRepository.php index aeb19d1d4..66477308c 100644 --- a/src/Entity/Repository/StationRequestRepository.php +++ b/src/Entity/Repository/StationRequestRepository.php @@ -8,6 +8,7 @@ use App\Entity\Api\StationPlaylistQueue; use App\Entity\Station; use App\Entity\StationMedia; use App\Entity\StationRequest; +use App\Enums\StationFeatures; use App\Exception; use App\Radio\AutoDJ; use App\Radio\Frontend\Blocklist\BlocklistParser; @@ -62,27 +63,31 @@ final class StationRequestRepository extends AbstractStationBasedRepository string $userAgent ): int { // Verify that the station supports requests. - if (!$station->getEnableRequests()) { - throw new Exception(__('This station does not accept requests currently.')); - } + StationFeatures::Requests->assertSupportedForStation($station); // Forbid web crawlers from using this feature. $dd = $this->deviceDetector->parse($userAgent); if ($dd->isBot) { - throw new Exception(__('Search engine crawlers are not permitted to use this feature.')); + throw Exception\CannotCompleteActionException::submitRequest( + __('Search engine crawlers are not permitted to use this feature.') + ); } // Check frontend blocklist and apply it to requests. if (!$this->blocklistParser->isAllowed($station, $ip, $userAgent)) { - throw new Exception(__('You are not permitted to submit requests.')); + throw Exception\CannotCompleteActionException::submitRequest( + __('You are not permitted to submit requests.') + ); } // Verify that Track ID exists with station. $mediaItem = $this->mediaRepo->requireByUniqueId($trackId, $station); if (!$mediaItem->isRequestable()) { - throw new Exception(__('The song ID you specified cannot be requested for this station.')); + throw Exception\CannotCompleteActionException::submitRequest( + __('This track is not requestable.') + ); } // Check if the song is already enqueued as a request. @@ -112,7 +117,7 @@ final class StationRequestRepository extends AbstractStationBasedRepository ->getSingleScalarResult(); if ($recentRequests > 0) { - throw new Exception( + throw Exception\CannotCompleteActionException::submitRequest( __('You have submitted a request too recently! Please wait before submitting another one.') ); } @@ -158,7 +163,9 @@ final class StationRequestRepository extends AbstractStationBasedRepository } if ($pendingRequest > 0) { - throw new Exception(__('Duplicate request: this song was already requested and will play soon.')); + throw Exception\CannotCompleteActionException::submitRequest( + __('This song was already requested and will play soon.') + ); } return true; @@ -236,7 +243,7 @@ final class StationRequestRepository extends AbstractStationBasedRepository $isDuplicate = (null === $this->duplicatePrevention->getDistinctTrack([$eligibleTrack], $recentTracks)); if ($isDuplicate) { - throw new Exception( + throw Exception\CannotCompleteActionException::submitRequest( __('This song or artist has been played too recently. Wait a while before requesting it again.') ); } diff --git a/src/Enums/StationFeatures.php b/src/Enums/StationFeatures.php index 8b8768333..287d4d5d4 100644 --- a/src/Enums/StationFeatures.php +++ b/src/Enums/StationFeatures.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Enums; use App\Entity\Station; +use App\Exception\StationUnsupportedException; enum StationFeatures { @@ -17,6 +18,7 @@ enum StationFeatures case Streamers; case Webhooks; case Podcasts; + case OnDemand; case Requests; public function supportedForStation(Station $station): bool @@ -30,7 +32,24 @@ enum StationFeatures self::MountPoints => $station->getFrontendType()->supportsMounts(), self::HlsStreams => $backendEnabled && $station->getEnableHls(), self::Requests => $backendEnabled && $station->getEnableRequests(), + self::OnDemand => $station->getEnableOnDemand(), self::Webhooks, self::Podcasts, self::RemoteRelays => true, }; } + + /** + * @param Station $station + * @return void + * @throws StationUnsupportedException + */ + public function assertSupportedForStation(Station $station): void + { + if (!$this->supportedForStation($station)) { + throw match ($this) { + self::Requests => StationUnsupportedException::requests(), + self::OnDemand => StationUnsupportedException::onDemand(), + default => StationUnsupportedException::generic(), + }; + } + } } diff --git a/src/Exception.php b/src/Exception.php index 71356d6ad..99a1409c5 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -5,37 +5,28 @@ declare(strict_types=1); namespace App; use Exception as PhpException; -use Psr\Log\LogLevel; +use Monolog\Level; use Throwable; class Exception extends PhpException { - /** @var string The logging severity of the exception. */ - protected string $loggerLevel; - /** @var array Any additional data that can be displayed in debugging. */ protected array $extraData = []; /** @var array Additional data supplied to the logger class when handling the exception. */ protected array $loggingContext = []; - /** @var string|null */ protected ?string $formattedMessage; public function __construct( string $message = '', int $code = 0, Throwable $previous = null, - string $loggerLevel = LogLevel::ERROR + protected Level $loggerLevel = Level::Error ) { parent::__construct($message, $code, $previous); - - $this->loggerLevel = $loggerLevel; } - /** - * @param string $message - */ public function setMessage(string $message): void { $this->message = $message; @@ -52,31 +43,22 @@ class Exception extends PhpException /** * Set a display-formatted message (if one exists). - * - * @param string|null $message */ public function setFormattedMessage(?string $message): void { $this->formattedMessage = $message; } - public function getLoggerLevel(): string + public function getLoggerLevel(): Level { return $this->loggerLevel; } - /** - * @param string $loggerLevel - */ - public function setLoggerLevel(string $loggerLevel): void + public function setLoggerLevel(Level $loggerLevel): void { $this->loggerLevel = $loggerLevel; } - /** - * @param int|string $legend - * @param mixed $data - */ public function addExtraData(int|string $legend, mixed $data): void { if (is_array($data)) { @@ -92,18 +74,11 @@ class Exception extends PhpException return $this->extraData; } - /** - * @param int|string $key - * @param mixed $data - */ public function addLoggingContext(int|string $key, mixed $data): void { $this->loggingContext[$key] = $data; } - /** - * @return mixed[] - */ public function getLoggingContext(): array { return $this->loggingContext; diff --git a/src/Exception/BootstrapException.php b/src/Exception/BootstrapException.php index 415f786be..3bf12243f 100644 --- a/src/Exception/BootstrapException.php +++ b/src/Exception/BootstrapException.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace App\Exception; use App\Exception; -use Psr\Log\LogLevel; +use Monolog\Level; use Throwable; final class BootstrapException extends Exception @@ -14,7 +14,7 @@ final class BootstrapException extends Exception string $message = '', int $code = 0, Throwable $previous = null, - string $loggerLevel = LogLevel::ALERT + Level $loggerLevel = Level::Alert ) { parent::__construct($message, $code, $previous, $loggerLevel); } diff --git a/src/Exception/CannotCompleteActionException.php b/src/Exception/CannotCompleteActionException.php new file mode 100644 index 000000000..83451fd64 --- /dev/null +++ b/src/Exception/CannotCompleteActionException.php @@ -0,0 +1,31 @@ +loggerLevel = $exception->getLoggerLevel(); } elseif ($exception instanceof HttpException) { - $this->loggerLevel = LogLevel::INFO; + $this->loggerLevel = Level::Info; } $this->showDetailed = $this->environment->showDetailedErrors(); diff --git a/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php b/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php index a63a1597e..eea394162 100644 --- a/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php +++ b/src/Middleware/RequirePublishedPodcastEpisodeMiddleware.php @@ -10,7 +10,7 @@ use App\Entity\Repository\PodcastRepository; use App\Entity\Station; use App\Entity\User; use App\Enums\StationPermissions; -use App\Exception\PodcastNotFoundException; +use App\Exception\NotFoundException; use App\Http\Response; use App\Http\ServerRequest; use Exception; @@ -44,7 +44,7 @@ final class RequirePublishedPodcastEpisodeMiddleware extends AbstractMiddleware $podcastId = $this->getPodcastIdFromRequest($request); if ($podcastId === null || !$this->checkPodcastHasPublishedEpisodes($station, $podcastId)) { - throw new PodcastNotFoundException(); + throw NotFoundException::podcast(); } $response = $handler->handle($request); diff --git a/src/Middleware/RequireStation.php b/src/Middleware/RequireStation.php index c011a8f03..4b4cf5df6 100644 --- a/src/Middleware/RequireStation.php +++ b/src/Middleware/RequireStation.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace App\Middleware; -use App\Exception\StationNotFoundException; +use App\Exception\NotFoundException; use App\Http\ServerRequest; use Exception; use Psr\Http\Message\ResponseInterface; @@ -20,7 +20,7 @@ final class RequireStation extends AbstractMiddleware try { $request->getStation(); } catch (Exception) { - throw new StationNotFoundException(); + throw NotFoundException::station(); } return $handler->handle($request); diff --git a/src/Middleware/StationSupportsFeature.php b/src/Middleware/StationSupportsFeature.php index e08788130..503bb270c 100644 --- a/src/Middleware/StationSupportsFeature.php +++ b/src/Middleware/StationSupportsFeature.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace App\Middleware; use App\Enums\StationFeatures; -use App\Exception; use App\Http\ServerRequest; use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -19,9 +18,7 @@ final class StationSupportsFeature extends AbstractMiddleware public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface { - if (!$this->feature->supportedForStation($request->getStation())) { - throw new Exception\StationUnsupportedException(); - } + $this->feature->assertSupportedForStation($request->getStation()); return $handler->handle($request); } diff --git a/src/Radio/Adapters.php b/src/Radio/Adapters.php index 783040189..1b68f455f 100644 --- a/src/Radio/Adapters.php +++ b/src/Radio/Adapters.php @@ -8,6 +8,7 @@ use App\Container\ContainerAwareTrait; use App\Entity\Station; use App\Entity\StationRemote; use App\Exception\NotFoundException; +use App\Exception\StationUnsupportedException; use App\Radio\Backend\Liquidsoap; use App\Radio\Enums\AdapterTypeInterface; use App\Radio\Enums\BackendAdapters; @@ -30,6 +31,20 @@ final class Adapters : null; } + /** + * @throws StationUnsupportedException + */ + public function requireFrontendAdapter(Station $station): Frontend\AbstractFrontend + { + $frontend = $this->getFrontendAdapter($station); + + if (null === $frontend) { + throw StationUnsupportedException::generic(); + } + + return $frontend; + } + /** * @param bool $checkInstalled * @return mixed[] @@ -48,6 +63,20 @@ final class Adapters : null; } + /** + * @throws StationUnsupportedException + */ + public function requireBackendAdapter(Station $station): Liquidsoap + { + $backend = $this->getBackendAdapter($station); + + if (null === $backend) { + throw StationUnsupportedException::generic(); + } + + return $backend; + } + /** * @param bool $checkInstalled * @return mixed[] @@ -64,7 +93,9 @@ final class Adapters return $this->di->get($className); } - throw new NotFoundException('Adapter not found: ' . $className); + throw new NotFoundException( + sprintf('Adapter not found: %s', $className) + ); } /**