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)
This commit is contained in:
Buster Neece 2023-12-17 10:32:42 -06:00
parent afc1f2fde9
commit c90b217e73
No known key found for this signature in database
56 changed files with 261 additions and 277 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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':

View File

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

View File

@ -24,7 +24,7 @@ trait HasLogViewer
clearstatcache();
if (!is_file($logPath)) {
throw new NotFoundException('Log file not found!');
throw NotFoundException::file();
}
if (!$tailFile) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ class Repository
{
$record = $this->find($id);
if (null === $record) {
throw new NotFoundException();
throw NotFoundException::generic();
}
return $record;
}

View File

@ -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 = '<b>' . $errorHeader . ':</b> ' . $e->getFormattedMessage();
$messageFormatted = $e->getFormattedMessage();
$extraData = $e->getExtraData();
} else {
$messageFormatted = '<b>' . $errorHeader . ':</b> ' . $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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Exception;
use App\Exception;
use Monolog\Level;
use Throwable;
final class CannotCompleteActionException extends Exception
{
public function __construct(
string $message = 'Cannot complete action.',
int $code = 0,
Throwable $previous = null,
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}
public static function submitRequest(string $reason): self
{
return new self(
sprintf(
__('Cannot submit request: %s'),
$reason
)
);
}
}

View File

@ -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 CannotProcessMediaException extends Exception
@ -16,7 +16,7 @@ final class CannotProcessMediaException extends Exception
string $message = 'Cannot process media file.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::WARNING
Level $loggerLevel = Level::Warning
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -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 CsrfValidationException extends Exception
@ -14,7 +14,7 @@ final class CsrfValidationException extends Exception
string $message = 'CSRF Validation Error',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Exception;
use App\Exception;
use Psr\Log\LogLevel;
use Throwable;
final class InvalidPodcastMediaFileException extends Exception
{
public function __construct(
string $message = 'Invalid Podcast Media mime type.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}
}

View File

@ -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 InvalidRequestAttribute extends Exception
@ -14,7 +14,7 @@ final class InvalidRequestAttribute extends Exception
string $message = 'Invalid request attribute.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::DEBUG
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -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 NoFileUploadedException extends Exception
@ -14,7 +14,7 @@ final class NoFileUploadedException extends Exception
string $message = 'No file was uploaded.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -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 NotFoundException extends Exception
@ -14,8 +14,28 @@ final class NotFoundException extends Exception
string $message = 'Record not found.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::DEBUG
Level $loggerLevel = Level::Debug
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}
public static function generic(): self
{
return new self(__('Record not found.'));
}
public static function file(): self
{
return new self(__('File not found.'));
}
public static function station(): self
{
return new self(__('Station not found.'));
}
public static function podcast(): self
{
return new self(__('Podcast not found.'));
}
}

View File

@ -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 NotLoggedInException extends Exception
@ -14,7 +14,7 @@ final class NotLoggedInException extends Exception
string $message = 'Not logged in.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::DEBUG
Level $loggerLevel = Level::Debug
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -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 PermissionDeniedException extends Exception
@ -14,7 +14,7 @@ final class PermissionDeniedException extends Exception
string $message = 'Permission denied.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Exception;
use App\Exception;
use Psr\Log\LogLevel;
use Throwable;
final class PodcastNotFoundException extends Exception
{
public function __construct(
string $message = 'Podcast not found.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}
}

View File

@ -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 RateLimitExceededException extends Exception
@ -14,7 +14,7 @@ final class RateLimitExceededException extends Exception
string $message = 'You have exceeded the rate limit for this application.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -1,21 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Exception;
use App\Exception;
use Psr\Log\LogLevel;
use Throwable;
final class StationNotFoundException extends Exception
{
public function __construct(
string $message = 'Station not found.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}
}

View File

@ -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 StationUnsupportedException extends Exception
@ -14,8 +14,29 @@ final class StationUnsupportedException extends Exception
string $message = 'This feature is not currently supported on this station.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}
public static function generic(): self
{
return new self(
__('This station does not currently support this functionality.')
);
}
public static function onDemand(): self
{
return new self(
__('This station does not currently support on-demand media.')
);
}
public static function requests(): self
{
return new self(
__('This station does not currently accept requests.')
);
}
}

View File

@ -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 StorageLocationFullException extends Exception
@ -14,7 +14,7 @@ final class StorageLocationFullException extends Exception
string $message = 'Storage location is full.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Exception\Supervisor;
use App\Exception\SupervisorException;
use Psr\Log\LogLevel;
use Monolog\Level;
use Throwable;
final class AlreadyRunningException extends SupervisorException
@ -14,7 +14,7 @@ final class AlreadyRunningException extends SupervisorException
string $message = 'Process was already running.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Exception\Supervisor;
use App\Exception\SupervisorException;
use Psr\Log\LogLevel;
use Monolog\Level;
use Throwable;
final class NotRunningException extends SupervisorException
@ -14,7 +14,7 @@ final class NotRunningException extends SupervisorException
string $message = 'Process was not running yet.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace App\Exception;
use App\Exception;
use Psr\Log\LogLevel;
use Monolog\Level;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Throwable;
@ -17,7 +17,7 @@ final class ValidationException extends Exception
string $message = 'Validation error.',
int $code = 0,
Throwable $previous = null,
string $loggerLevel = LogLevel::INFO
Level $loggerLevel = Level::Info
) {
parent::__construct($message, $code, $previous, $loggerLevel);
}

View File

@ -14,10 +14,10 @@ use App\Middleware\InjectSession;
use App\Session\Flash;
use App\View;
use Mezzio\Session\Session;
use Monolog\Level;
use Monolog\Logger;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LogLevel;
use Slim\App;
use Slim\Exception\HttpException;
use Slim\Handlers\ErrorHandler as SlimErrorHandler;
@ -33,7 +33,7 @@ final class ErrorHandler extends SlimErrorHandler
private bool $showDetailed = false;
private string $loggerLevel = LogLevel::ERROR;
private Level $loggerLevel = Level::Error;
public function __construct(
private readonly View $view,
@ -59,7 +59,7 @@ final class ErrorHandler extends SlimErrorHandler
if ($exception instanceof Exception) {
$this->loggerLevel = $exception->getLoggerLevel();
} elseif ($exception instanceof HttpException) {
$this->loggerLevel = LogLevel::INFO;
$this->loggerLevel = Level::Info;
}
$this->showDetailed = $this->environment->showDetailedErrors();

View File

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

View File

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

View File

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

View File

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