From 5cbacd5df642acc56e4468cb0111d1e9cf6e2d8a Mon Sep 17 00:00:00 2001 From: "Buster \"Silver Eagle\" Neece" Date: Mon, 19 Jul 2021 00:53:45 -0500 Subject: [PATCH] System-Wide Strict Types (#4405) --- bin/console | 5 +- bin/installer | 5 +- bin/uptime_wait | 5 +- composer.lock | 12 ++- config/assets.php | 4 +- config/menus/admin.php | 30 +++--- config/menus/station.php | 54 +++++------ phpcs.xml | 4 + phpstan.neon | 12 ++- src/Acl.php | 14 ++- src/AppFactory.php | 54 +++++++++-- src/Assets.php | 33 +++---- src/Auth.php | 11 ++- src/Config.php | 4 +- src/Console/Application.php | 8 +- src/Console/Command/Backup/BackupCommand.php | 4 +- src/Console/Command/Backup/RestoreCommand.php | 2 + src/Console/Command/ClearCacheCommand.php | 2 + src/Console/Command/CommandAbstract.php | 4 +- .../Command/Debug/OptimizeTablesCommand.php | 2 + .../Command/GenerateApiDocsCommand.php | 2 + src/Console/Command/InitializeCommand.php | 2 + .../Command/Internal/DjAuthCommand.php | 2 + src/Console/Command/Internal/DjOffCommand.php | 2 + src/Console/Command/Internal/DjOnCommand.php | 2 + .../Command/Internal/FeedbackCommand.php | 2 + src/Console/Command/Internal/GetIpCommand.php | 4 +- .../Command/Internal/NextSongCommand.php | 2 + .../Command/Internal/SftpAuthCommand.php | 8 +- .../Command/Internal/SftpEventCommand.php | 16 +++- .../Command/Locale/GenerateCommand.php | 2 + src/Console/Command/Locale/ImportCommand.php | 2 + .../Command/MessageQueue/ClearCommand.php | 2 + .../Command/MessageQueue/ProcessCommand.php | 2 + src/Console/Command/MigrateConfigCommand.php | 2 + src/Console/Command/ReprocessMediaCommand.php | 2 + src/Console/Command/RestartRadioCommand.php | 2 + src/Console/Command/Settings/ListCommand.php | 2 + src/Console/Command/Settings/SetCommand.php | 2 + src/Console/Command/SetupCommand.php | 2 + src/Console/Command/SetupFixturesCommand.php | 2 + src/Console/Command/SyncCommand.php | 2 + .../Command/Traits/PassThruProcess.php | 6 +- src/Console/Command/Users/ListCommand.php | 2 + .../Command/Users/ResetPasswordCommand.php | 2 + .../Command/Users/SetAdministratorCommand.php | 20 +++- src/Console/ErrorHandler.php | 2 + .../AbstractLogViewerController.php | 16 +++- .../Admin/AbstractAdminCrudController.php | 10 +- src/Controller/Admin/ApiController.php | 30 ++++-- src/Controller/Admin/AuditLogController.php | 2 + src/Controller/Admin/BackupsController.php | 18 ++-- src/Controller/Admin/BrandingController.php | 2 + .../Admin/CustomFieldsController.php | 30 ++++-- src/Controller/Admin/DebugController.php | 6 +- src/Controller/Admin/IndexController.php | 4 +- .../Admin/InstallGeoLiteController.php | 6 +- .../Admin/InstallShoutcastController.php | 13 ++- src/Controller/Admin/LogsController.php | 16 +++- .../Admin/PermissionsController.php | 30 ++++-- src/Controller/Admin/RelaysController.php | 2 + src/Controller/Admin/SettingsController.php | 2 + src/Controller/Admin/StationsController.php | 42 +++++---- .../Admin/StorageLocationsController.php | 2 + src/Controller/Admin/UsersController.php | 40 +++++--- .../Api/AbstractApiCrudController.php | 68 +++++++++----- .../Admin/AbstractAdminApiCrudController.php | 25 ++--- .../Api/Admin/AuditLogController.php | 2 + .../Api/Admin/CustomFieldsController.php | 5 + .../Api/Admin/PermissionsController.php | 2 + src/Controller/Api/Admin/RelaysController.php | 9 +- src/Controller/Api/Admin/RolesController.php | 5 + .../Api/Admin/SettingsController.php | 7 +- .../Api/Admin/StationsController.php | 26 +++++- .../Api/Admin/StorageLocationsController.php | 8 +- src/Controller/Api/Admin/UsersController.php | 8 +- .../Api/Frontend/Account/GetMeAction.php | 2 + .../Api/Frontend/Account/PutMeAction.php | 4 +- .../Api/Frontend/Dashboard/ChartsAction.php | 4 +- .../Dashboard/NotificationsAction.php | 2 + .../Api/Frontend/Dashboard/StationsAction.php | 2 + src/Controller/Api/IndexController.php | 2 + src/Controller/Api/InternalController.php | 2 + src/Controller/Api/NowplayingController.php | 11 ++- src/Controller/Api/OpenApiController.php | 4 +- .../AbstractScheduledEntityController.php | 22 ++++- .../AbstractStationApiCrudController.php | 44 +++++---- .../Api/Stations/Art/DeleteArtAction.php | 12 ++- .../Api/Stations/Art/GetArtAction.php | 4 +- .../Api/Stations/Art/PostArtAction.php | 14 ++- .../Api/Stations/Files/BatchAction.php | 6 +- .../Api/Stations/Files/DownloadAction.php | 6 +- .../Api/Stations/Files/FlowUploadAction.php | 2 + .../Api/Stations/Files/ListAction.php | 8 +- .../Stations/Files/ListDirectoriesAction.php | 2 + .../Stations/Files/MakeDirectoryAction.php | 2 + .../Api/Stations/Files/PlayAction.php | 4 +- .../Api/Stations/Files/RenameAction.php | 2 + .../Api/Stations/FilesController.php | 13 ++- .../Api/Stations/HistoryController.php | 2 + .../Api/Stations/IndexController.php | 2 + .../Api/Stations/ListenersAction.php | 10 +- .../Api/Stations/MountsController.php | 5 + .../Api/Stations/OnDemand/DownloadAction.php | 4 +- .../Api/Stations/OnDemand/ListAction.php | 2 + .../Playlists/AbstractPlaylistsAction.php | 2 + .../Api/Stations/Playlists/CloneAction.php | 6 +- .../Stations/Playlists/DeleteQueueAction.php | 4 +- .../Api/Stations/Playlists/ExportAction.php | 6 +- .../Api/Stations/Playlists/GetOrderAction.php | 4 +- .../Api/Stations/Playlists/GetQueueAction.php | 4 +- .../Api/Stations/Playlists/ImportAction.php | 6 +- .../Api/Stations/Playlists/PutOrderAction.php | 4 +- .../Stations/Playlists/ReshuffleAction.php | 4 +- .../Api/Stations/Playlists/ToggleAction.php | 4 +- .../Api/Stations/PlaylistsController.php | 31 +++++-- .../Stations/PodcastEpisodesController.php | 40 ++++---- .../Stations/Podcasts/Art/PostArtAction.php | 2 +- .../Podcasts/Episodes/Art/DeleteArtAction.php | 2 +- .../Podcasts/Episodes/Art/PostArtAction.php | 2 +- .../Episodes/Media/DeleteMediaAction.php | 2 +- .../Episodes/Media/GetMediaAction.php | 2 +- .../Episodes/Media/PostMediaAction.php | 2 +- .../Api/Stations/PodcastsController.php | 54 +++++++---- .../Api/Stations/ProfileController.php | 2 + .../Api/Stations/QueueController.php | 7 +- .../Api/Stations/RemotesController.php | 8 +- .../Reports/Overview/BestAndWorstAction.php | 2 + .../Reports/Overview/ChartsAction.php | 4 +- .../Reports/Overview/MostPlayedAction.php | 2 + .../Api/Stations/RequestsController.php | 2 + .../Api/Stations/ScheduleController.php | 2 + .../Api/Stations/ServicesController.php | 2 + .../Streamers/BroadcastsController.php | 26 +++--- .../Api/Stations/StreamersController.php | 16 ++-- .../Api/Stations/UpdateMetadataController.php | 2 + .../Stations/Waveform/GetWaveformAction.php | 4 +- .../Api/Stations/WebhooksController.php | 5 + .../Frontend/Account/EndMasqueradeAction.php | 4 +- .../Frontend/Account/ForgotPasswordAction.php | 4 +- .../Frontend/Account/LoginAction.php | 14 +-- .../Frontend/Account/LogoutAction.php | 4 +- .../Frontend/Account/RecoverAction.php | 6 +- .../Frontend/Account/TwoFactorAction.php | 8 +- src/Controller/Frontend/ApiKeysController.php | 32 ++++--- src/Controller/Frontend/DashboardAction.php | 2 + src/Controller/Frontend/IndexAction.php | 10 +- .../Frontend/PWA/AppManifestAction.php | 2 + .../Frontend/PWA/ServiceWorkerAction.php | 2 + .../Profile/DisableTwoFactorAction.php | 4 +- .../Frontend/Profile/EditAction.php | 4 +- .../Profile/EnableTwoFactorAction.php | 6 +- .../Frontend/Profile/IndexAction.php | 2 + .../Frontend/Profile/ThemeAction.php | 2 + .../Frontend/PublicPages/HistoryAction.php | 2 + .../Frontend/PublicPages/OnDemandAction.php | 2 + .../Frontend/PublicPages/PlayerAction.php | 2 + .../Frontend/PublicPages/PlaylistAction.php | 5 +- .../PublicPages/PodcastEpisodeController.php | 5 +- .../PublicPages/PodcastEpisodesController.php | 4 +- .../PublicPages/PodcastFeedController.php | 34 +++---- .../Frontend/PublicPages/RequestsAction.php | 2 + .../Frontend/PublicPages/ScheduleAction.php | 2 + .../Frontend/PublicPages/WebDjAction.php | 4 +- src/Controller/Frontend/SetupController.php | 18 ++-- .../AbstractStationCrudController.php | 6 +- .../Stations/AutomationController.php | 8 +- .../Stations/EditLiquidsoapConfigAction.php | 4 +- src/Controller/Stations/FilesAction.php | 2 + src/Controller/Stations/LogsController.php | 8 +- src/Controller/Stations/PlaylistsAction.php | 2 + src/Controller/Stations/ProfileController.php | 10 +- src/Controller/Stations/QueueAction.php | 2 + src/Controller/Stations/RemotesController.php | 28 +++--- .../Stations/Reports/ListenersController.php | 2 + .../Stations/Reports/OverviewController.php | 2 + .../Reports/PerformanceController.php | 4 +- .../Stations/Reports/RequestsController.php | 12 ++- .../Reports/SoundExchangeController.php | 28 ++++-- .../Stations/Reports/TimelineController.php | 2 + .../Stations/SftpUsersController.php | 26 ++++-- src/Controller/Stations/StreamersAction.php | 4 +- .../Stations/WebhooksController.php | 60 +++++++----- src/Customization.php | 6 +- src/DeferredCallable.php | 1 + src/Doctrine/DecoratedEntityManager.php | 23 ++++- src/Doctrine/Event/AuditLog.php | 33 +++++-- .../Event/SetExplicitChangeTracking.php | 2 + src/Doctrine/Event/StationRequiresRestart.php | 2 + src/Doctrine/Generator/UuidV6Generator.php | 2 + .../ClearEntityManagerSubscriber.php | 2 + .../ReloadableEntityManagerInterface.php | 21 +++-- src/Doctrine/Repository.php | 31 ++++--- src/Entity/Analytics.php | 11 ++- src/Entity/Api/Admin/Relay.php | 18 ++-- src/Entity/Api/Admin/StorageLocation.php | 2 + src/Entity/Api/BatchResult.php | 2 + src/Entity/Api/Dashboard.php | 2 + src/Entity/Api/DetailedSongHistory.php | 2 + src/Entity/Api/Error.php | 37 ++++++-- src/Entity/Api/FileList.php | 2 + src/Entity/Api/FileListMedia.php | 2 + src/Entity/Api/Listener.php | 2 + src/Entity/Api/NewRecord.php | 2 + src/Entity/Api/Notification.php | 2 + src/Entity/Api/NowPlaying.php | 4 +- src/Entity/Api/NowPlayingCurrentSong.php | 2 + src/Entity/Api/NowPlayingListeners.php | 2 + src/Entity/Api/NowPlayingLive.php | 13 ++- src/Entity/Api/Podcast.php | 2 + src/Entity/Api/PodcastEpisode.php | 2 + src/Entity/Api/PodcastMedia.php | 2 + src/Entity/Api/ResolvableUrlInterface.php | 2 + src/Entity/Api/Song.php | 2 + src/Entity/Api/SongHistory.php | 2 + src/Entity/Api/Station.php | 2 + src/Entity/Api/StationMount.php | 2 + src/Entity/Api/StationOnDemand.php | 2 + src/Entity/Api/StationPlaylistQueue.php | 2 + src/Entity/Api/StationProfile.php | 2 + src/Entity/Api/StationQueue.php | 2 + src/Entity/Api/StationQueueDetailed.php | 2 + src/Entity/Api/StationRemote.php | 2 + src/Entity/Api/StationRequest.php | 2 + src/Entity/Api/StationSchedule.php | 2 + src/Entity/Api/StationServiceStatus.php | 8 +- src/Entity/Api/Status.php | 15 +-- src/Entity/Api/SystemStatus.php | 2 + src/Entity/Api/Time.php | 2 + src/Entity/Api/Traits/HasLinks.php | 2 + src/Entity/Api/UploadFile.php | 2 + .../ApiGenerator/NowPlayingApiGenerator.php | 13 ++- src/Entity/ApiGenerator/SongApiGenerator.php | 2 + .../ApiGenerator/SongHistoryApiGenerator.php | 4 +- .../ApiGenerator/StationApiGenerator.php | 8 +- .../ApiGenerator/StationQueueApiGenerator.php | 7 +- src/Entity/ApiKey.php | 12 +-- src/Entity/Attributes/AuditIgnore.php | 2 + src/Entity/Attributes/Auditable.php | 2 + src/Entity/AuditLog.php | 5 +- src/Entity/CustomField.php | 15 +-- src/Entity/Fixture/Analytics.php | 2 + src/Entity/Fixture/ApiKey.php | 2 + src/Entity/Fixture/Podcast.php | 2 + src/Entity/Fixture/PodcastEpisode.php | 5 + src/Entity/Fixture/Role.php | 2 + src/Entity/Fixture/RolePermission.php | 2 + src/Entity/Fixture/Settings.php | 8 +- src/Entity/Fixture/Station.php | 2 + src/Entity/Fixture/StationMedia.php | 2 + src/Entity/Fixture/StationMount.php | 2 + src/Entity/Fixture/StationPlaylist.php | 2 + src/Entity/Fixture/User.php | 2 + .../IdentifiableEntityInterface.php | 12 +++ src/Entity/Interfaces/PathAwareInterface.php | 2 + .../Interfaces/ProcessableMediaInterface.php | 2 + src/Entity/Interfaces/SongInterface.php | 2 + .../Interfaces/StationCloneAwareInterface.php | 2 + .../Interfaces/StationMountInterface.php | 2 + src/Entity/Listener.php | 5 +- src/Entity/MessengerMessage.php | 2 + src/Entity/Metadata.php | 2 + .../Migration/Version20161003041904.php | 2 + .../Migration/Version20161006030903.php | 2 + .../Migration/Version20161007021719.php | 2 + .../Migration/Version20161007195027.php | 2 + .../Migration/Version20161117000718.php | 2 + .../Migration/Version20161117161959.php | 2 + .../Migration/Version20161120032434.php | 2 + .../Migration/Version20161122035237.php | 2 + .../Migration/Version20170412210654.php | 2 + .../Migration/Version20170414205418.php | 2 + .../Migration/Version20170423202805.php | 2 + .../Migration/Version20170424042111.php | 2 + .../Migration/Version20170502202418.php | 2 + .../Migration/Version20170510082607.php | 2 + .../Migration/Version20170510085226.php | 2 + .../Migration/Version20170510091820.php | 2 + .../Migration/Version20170512023527.php | 2 + .../Migration/Version20170512082741.php | 2 + .../Migration/Version20170512094523.php | 2 + .../Migration/Version20170516073708.php | 2 + .../Migration/Version20170516205418.php | 2 + .../Migration/Version20170516214120.php | 2 + .../Migration/Version20170516215536.php | 2 + .../Migration/Version20170518100549.php | 2 + .../Migration/Version20170522052114.php | 2 + .../Migration/Version20170524090814.php | 2 + .../Migration/Version20170606173152.php | 2 + .../Migration/Version20170618013019.php | 2 + .../Migration/Version20170619044014.php | 2 + .../Migration/Version20170619171323.php | 2 + .../Migration/Version20170622223025.php | 2 + .../Migration/Version20170719045113.php | 2 + .../Migration/Version20170803050109.php | 2 + .../Migration/Version20170823204230.php | 2 + .../Migration/Version20170829030442.php | 4 +- .../Migration/Version20170906080352.php | 2 + .../Migration/Version20170917175534.php | 2 + .../Migration/Version20171022005913.php | 2 + .../Migration/Version20171103075821.php | 2 + .../Migration/Version20171104014701.php | 2 + .../Migration/Version20180425050351.php | 2 +- .../Migration/Version20180826043500.php | 2 +- .../Migration/Version20201204043539.php | 4 +- .../Migration/Version20210717164419.php | 78 ++++++++++++++++ src/Entity/Podcast.php | 3 +- src/Entity/PodcastCategory.php | 3 +- src/Entity/PodcastEpisode.php | 3 +- src/Entity/PodcastMedia.php | 24 +---- src/Entity/Relay.php | 9 +- .../AbstractSplitTokenRepository.php | 6 ++ src/Entity/Repository/AnalyticsRepository.php | 2 + src/Entity/Repository/ApiKeyRepository.php | 2 + .../Repository/CustomFieldRepository.php | 2 + src/Entity/Repository/ListenerRepository.php | 2 + .../Repository/MessengerMessageRepository.php | 2 + .../Repository/PodcastEpisodeRepository.php | 4 +- src/Entity/Repository/PodcastRepository.php | 29 +++--- .../Repository/RolePermissionRepository.php | 5 + src/Entity/Repository/RoleRepository.php | 6 ++ src/Entity/Repository/SettingsRepository.php | 2 + .../Repository/SongHistoryRepository.php | 4 +- .../Repository/StationMediaRepository.php | 6 +- .../Repository/StationMountRepository.php | 2 + .../StationPlaylistFolderRepository.php | 2 + .../StationPlaylistMediaRepository.php | 2 + .../Repository/StationQueueRepository.php | 2 + .../Repository/StationRemoteRepository.php | 2 + src/Entity/Repository/StationRepository.php | 9 +- .../Repository/StationRequestRepository.php | 6 +- .../Repository/StationScheduleRepository.php | 20 ++-- .../StationStreamerBroadcastRepository.php | 5 + .../Repository/StationStreamerRepository.php | 2 + .../Repository/StorageLocationRepository.php | 5 + .../UnprocessableMediaRepository.php | 2 + .../Repository/UserLoginTokenRepository.php | 2 + src/Entity/Repository/UserRepository.php | 5 + src/Entity/Role.php | 10 +- src/Entity/RolePermission.php | 9 +- src/Entity/Settings.php | 12 ++- src/Entity/SftpUser.php | 9 +- src/Entity/Song.php | 4 +- src/Entity/SongHistory.php | 19 ++-- src/Entity/Station.php | 87 ++++++++++++------ src/Entity/StationBackendConfiguration.php | 2 + src/Entity/StationFrontendConfiguration.php | 2 + src/Entity/StationMedia.php | 63 ++++--------- src/Entity/StationMediaCustomField.php | 5 +- src/Entity/StationMount.php | 13 ++- src/Entity/StationPlaylist.php | 9 +- src/Entity/StationPlaylistFolder.php | 7 +- src/Entity/StationPlaylistMedia.php | 5 +- src/Entity/StationQueue.php | 7 +- src/Entity/StationRemote.php | 45 +++++---- src/Entity/StationRequest.php | 5 +- src/Entity/StationSchedule.php | 15 +-- src/Entity/StationStreamer.php | 10 +- src/Entity/StationStreamerBroadcast.php | 5 +- src/Entity/StationWebhook.php | 9 +- src/Entity/StorageLocation.php | 45 ++++++--- src/Entity/Traits/HasAutoIncrementId.php | 11 +++ src/Entity/Traits/HasSongFields.php | 13 ++- src/Entity/Traits/HasSplitTokenFields.php | 2 + src/Entity/Traits/HasUniqueId.php | 11 +++ src/Entity/Traits/TruncateInts.php | 4 +- src/Entity/Traits/TruncateStrings.php | 2 + src/Entity/UnprocessableMedia.php | 5 +- src/Entity/User.php | 30 +++--- src/Entity/UserLoginToken.php | 2 +- src/Environment.php | 7 +- src/Event/AbstractBuildMenu.php | 2 + src/Event/BuildAdminMenu.php | 2 + src/Event/BuildConsoleCommands.php | 2 + src/Event/BuildDoctrineMappingPaths.php | 2 + .../BuildMigrationConfigurationArray.php | 2 + src/Event/BuildPermissions.php | 2 + src/Event/BuildRoutes.php | 2 + src/Event/BuildStationMenu.php | 2 + src/Event/BuildView.php | 2 + src/Event/GetNotifications.php | 2 + src/Event/GetSyncTasks.php | 2 + src/Event/Media/GetAlbumArt.php | 2 + src/Event/Media/ReadMetadata.php | 2 + src/Event/Media/WriteMetadata.php | 2 + src/Event/Radio/AnnotateNextSong.php | 2 + src/Event/Radio/BuildQueue.php | 2 + src/Event/Radio/GenerateRawNowPlaying.php | 2 + src/Event/Radio/LoadNowPlaying.php | 4 +- .../Radio/WriteLiquidsoapConfiguration.php | 2 + src/EventDispatcher.php | 2 + src/Exception.php | 2 + src/Exception/AdvancedFeatureException.php | 2 + src/Exception/BootstrapException.php | 2 + src/Exception/CannotProcessMediaException.php | 2 + src/Exception/CsrfValidationException.php | 2 + src/Exception/InvalidRequestAttribute.php | 2 + src/Exception/NoFileUploadedException.php | 2 + src/Exception/NoGetterAvailableException.php | 2 + src/Exception/NotFoundException.php | 2 + src/Exception/NotLoggedInException.php | 2 + src/Exception/PermissionDeniedException.php | 2 + src/Exception/RateLimitExceededException.php | 2 + src/Exception/StationNotFoundException.php | 2 + src/Exception/StationUnsupportedException.php | 2 + .../Supervisor/AlreadyRunningException.php | 2 + src/Exception/Supervisor/BadNameException.php | 2 + .../Supervisor/NotRunningException.php | 2 + src/Exception/SupervisorException.php | 2 + src/Exception/ValidationException.php | 2 + src/Exception/WrappedException.php | 2 + src/Flysystem/StationFilesystems.php | 2 + src/Form/AbstractSettingsForm.php | 2 + src/Form/ApiKeyForm.php | 2 + src/Form/BackupSettingsForm.php | 2 + src/Form/BrandingSettingsForm.php | 2 + src/Form/CustomFieldForm.php | 2 + src/Form/EntityForm.php | 62 +++++++++---- src/Form/Field/Csrf.php | 2 + src/Form/Field/File.php | 2 + src/Form/Field/PlaylistTime.php | 2 + src/Form/Form.php | 2 + src/Form/GeoLiteSettingsForm.php | 2 + src/Form/PermissionsForm.php | 5 + src/Form/SettingsForm.php | 2 + src/Form/SftpUserForm.php | 2 + src/Form/StationCloneForm.php | 4 +- src/Form/StationForm.php | 57 ++++++++---- src/Form/StationRemoteForm.php | 2 + src/Form/StationWebhookForm.php | 2 + src/Form/UserForm.php | 2 + src/Form/UserProfileForm.php | 2 + src/Http/ErrorHandler.php | 12 +-- src/Http/Factory/ResponseFactory.php | 2 + src/Http/Factory/ServerRequestFactory.php | 2 + src/Http/Response.php | 12 ++- src/Http/Router.php | 73 ++++++--------- src/Http/RouterInterface.php | 22 +++-- src/Http/ServerRequest.php | 10 +- src/Installer/Command/InstallCommand.php | 10 +- src/Installer/EnvFiles/AbstractEnvFile.php | 16 +++- src/Installer/EnvFiles/AzuraCastEnvFile.php | 2 + src/Installer/EnvFiles/EnvFile.php | 2 + src/Locale.php | 11 ++- src/LockFactory.php | 4 +- .../AbstractAlbumArtHandler.php | 2 + .../AlbumArtHandler/LastFmAlbumArtHandler.php | 2 + .../MusicBrainzAlbumArtHandler.php | 2 + src/Media/BatchUtilities.php | 2 + src/Media/MetadataManager.php | 2 + .../MetadataService/GetId3MetadataService.php | 9 +- src/Media/MimeType.php | 2 + src/Media/RemoteAlbumArt.php | 4 +- src/Message/AbstractMessage.php | 2 + src/Message/AbstractUniqueMessage.php | 2 + src/Message/AddNewMediaMessage.php | 2 + src/Message/BackupMessage.php | 2 + src/Message/DispatchWebhookMessage.php | 3 + src/Message/ReprocessMediaMessage.php | 2 + src/Message/RunSyncTaskMessage.php | 2 + src/Message/UpdateNowPlayingMessage.php | 2 + src/Message/WritePlaylistFileMessage.php | 2 + .../LogWorkerExceptionSubscriber.php | 2 + src/MessageQueue/QueueManager.php | 9 +- .../ResetArrayCacheMiddleware.php | 2 + src/MessageQueue/UniqueMessageInterface.php | 2 + src/Middleware/ApplyXForwardedProto.php | 2 + src/Middleware/EnableView.php | 2 + src/Middleware/EnforceSecurity.php | 2 + src/Middleware/GetCurrentUser.php | 2 + src/Middleware/GetStation.php | 2 + src/Middleware/HandleMultipartJson.php | 4 +- src/Middleware/InjectRateLimit.php | 2 + src/Middleware/InjectRouter.php | 2 + src/Middleware/InjectSession.php | 2 + src/Middleware/Module/Admin.php | 4 +- src/Middleware/Module/Api.php | 8 +- src/Middleware/Module/StationFiles.php | 2 + src/Middleware/Module/Stations.php | 4 +- src/Middleware/Permissions.php | 2 + src/Middleware/RateLimit.php | 2 + src/Middleware/RemoveSlashes.php | 2 + src/Middleware/RequireLogin.php | 2 + ...quirePublishedPodcastEpisodeMiddleware.php | 2 +- src/Middleware/RequireStation.php | 2 + .../WrapExceptionsWithRequestData.php | 2 + src/Normalizer/Attributes/DeepNormalize.php | 2 + src/Normalizer/DoctrineEntityNormalizer.php | 91 +++++++++---------- .../Check/ComposeVersionCheck.php | 2 + .../Check/ProfilerAdvisorCheck.php | 2 + src/Notification/Check/RecentBackupCheck.php | 2 + src/Notification/Check/SyncTaskCheck.php | 2 + src/Notification/Check/UpdateCheck.php | 2 + src/Paginator.php | 2 + src/Plugins.php | 8 +- src/Radio/AbstractAdapter.php | 4 +- src/Radio/Adapters.php | 2 + src/Radio/AutoDJ.php | 2 + src/Radio/AutoDJ/Annotations.php | 16 ++-- src/Radio/AutoDJ/Queue.php | 13 ++- src/Radio/AutoDJ/Scheduler.php | 30 ++++-- src/Radio/Backend/AbstractBackend.php | 2 + src/Radio/Backend/Liquidsoap.php | 8 +- src/Radio/Backend/Liquidsoap/ConfigWriter.php | 30 +++--- src/Radio/Backend/None.php | 2 + src/Radio/Certificate.php | 2 + src/Radio/CertificateLocator.php | 2 + src/Radio/Configuration.php | 10 +- src/Radio/Frontend/AbstractFrontend.php | 11 ++- src/Radio/Frontend/Icecast.php | 31 ++++--- src/Radio/Frontend/Remote.php | 2 + src/Radio/Frontend/SHOUTcast.php | 6 +- src/Radio/PlaylistParser.php | 2 + src/Radio/Quota.php | 16 ++-- src/Radio/Remote/AbstractRemote.php | 4 +- src/Radio/Remote/AdapterProxy.php | 2 + src/Radio/Remote/AzuraRelay.php | 6 +- src/Radio/Remote/Icecast.php | 2 + src/Radio/Remote/SHOUTcast1.php | 2 + src/Radio/Remote/SHOUTcast2.php | 2 + src/RateLimit.php | 2 + src/Security/SplitToken.php | 2 + src/Service/AudioWaveform.php | 11 +-- src/Service/Avatar.php | 8 +- src/Service/Avatar/AvatarServiceInterface.php | 2 + src/Service/Avatar/Disabled.php | 4 +- src/Service/Avatar/Gravatar.php | 4 +- src/Service/Avatar/Libravatar.php | 4 +- src/Service/AzuraCastCentral.php | 2 + src/Service/DeviceDetector.php | 2 + src/Service/Flow.php | 27 +++++- src/Service/Flow/UploadedFile.php | 18 +++- src/Service/IpGeolocation.php | 13 ++- .../IpGeolocator/AbstractIpGeolocator.php | 2 + src/Service/IpGeolocator/DbIp.php | 2 + src/Service/IpGeolocator/GeoLite.php | 2 + .../IpGeolocator/IpGeolocatorInterface.php | 2 + src/Service/LastFm.php | 2 + src/Service/Mail.php | 2 + src/Service/MusicBrainz.php | 10 +- src/Service/NChan.php | 6 +- src/Service/SftpGo.php | 2 + src/Session/Csrf.php | 11 ++- src/Session/Flash.php | 10 +- src/Sync/Runner.php | 2 + src/Sync/Task/AbstractTask.php | 2 + src/Sync/Task/BuildQueueTask.php | 2 + src/Sync/Task/CheckFolderPlaylistsTask.php | 2 + src/Sync/Task/CheckMediaTask.php | 18 ++-- src/Sync/Task/CheckRequests.php | 2 + src/Sync/Task/CheckUpdatesTask.php | 2 + src/Sync/Task/CleanupHistoryTask.php | 2 + src/Sync/Task/CleanupLoginTokensTask.php | 2 + src/Sync/Task/CleanupRelaysTask.php | 2 + src/Sync/Task/CleanupStorageTask.php | 6 +- src/Sync/Task/NowPlayingTask.php | 6 +- src/Sync/Task/ReactivateStreamerTask.php | 2 + src/Sync/Task/RotateLogsTask.php | 6 +- src/Sync/Task/RunAnalyticsTask.php | 2 + src/Sync/Task/RunAutomatedAssignmentTask.php | 2 + src/Sync/Task/RunBackupTask.php | 2 + src/Sync/Task/UpdateGeoLiteTask.php | 2 + src/Sync/TaskLocator.php | 2 + src/Tests/Connector.php | 1 + src/Tests/Module.php | 16 +++- src/Traits/AvailableStaticallyTrait.php | 2 + src/Traits/LoadFromParentObject.php | 5 + src/Traits/RequestAwareTrait.php | 2 + src/Utilities/Arrays.php | 16 ++-- src/Utilities/Csv.php | 4 +- src/Utilities/File.php | 23 ++++- src/Utilities/Json.php | 38 ++++++++ src/Utilities/Strings.php | 28 +++--- src/Utilities/Time.php | 32 +++++++ src/Utilities/Xml.php | 10 +- .../Constraints/StationPortChecker.php | 2 + .../StationPortCheckerValidator.php | 2 + src/Validator/Constraints/StorageLocation.php | 2 + .../Constraints/StorageLocationValidator.php | 2 + src/Validator/Constraints/UniqueEntity.php | 29 +++--- .../Constraints/UniqueEntityValidator.php | 23 ++--- src/Version.php | 2 + src/View.php | 4 +- src/Webhook/Connector/AbstractConnector.php | 19 +++- src/Webhook/Connector/ConnectorInterface.php | 6 +- src/Webhook/Connector/Discord.php | 5 + src/Webhook/Connector/Email.php | 5 + src/Webhook/Connector/Generic.php | 5 + src/Webhook/Connector/GoogleAnalytics.php | 5 + src/Webhook/Connector/Telegram.php | 5 + src/Webhook/Connector/TuneIn.php | 11 ++- src/Webhook/Connector/Twitter.php | 5 + src/Webhook/ConnectorLocator.php | 2 + src/Webhook/Dispatcher.php | 2 + src/Webhook/LocalWebhookHandler.php | 2 + src/Xml/Reader.php | 2 + src/Xml/Writer.php | 21 +++-- templates/admin/backups/run.phtml | 2 +- templates/admin/debug/sync.phtml | 2 +- templates/admin/storage_locations/index.phtml | 3 +- templates/frontend/account/login.phtml | 2 +- templates/frontend/public/index.phtml | 2 +- templates/frontend/public/ondemand.phtml | 2 +- templates/frontend/public/podcasts.phtml | 4 +- templates/stations/files/index.phtml | 13 +-- templates/stations/mounts/index.phtml | 3 +- templates/stations/playlists/index.phtml | 7 +- templates/stations/podcasts/index.phtml | 7 +- templates/stations/profile/index.phtml | 43 +++++---- templates/stations/queue/index.phtml | 3 +- templates/stations/reports/overview.phtml | 1 + templates/stations/streamers/index.phtml | 7 +- web/index.php | 5 +- 613 files changed, 3144 insertions(+), 1291 deletions(-) create mode 100644 src/Entity/Interfaces/IdentifiableEntityInterface.php create mode 100644 src/Entity/Migration/Version20210717164419.php create mode 100644 src/Utilities/Json.php create mode 100644 src/Utilities/Time.php diff --git a/bin/console b/bin/console index 2ab156a60..1440605bf 100644 --- a/bin/console +++ b/bin/console @@ -1,7 +1,10 @@ #!/usr/bin/env php =7.4" + "league/mime-type-detection": "^1.7", + "php": ">=7.4", + "psr/http-message": ">1.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^0.5.0", @@ -165,7 +167,7 @@ "type": "patreon" } ], - "time": "2021-04-26T10:41:32+00:00" + "time": "2021-07-18T17:02:47+00:00" }, { "name": "azuracast/flysystem-v2-extensions", diff --git a/config/assets.php b/config/assets.php index 2ad9c3b7e..c0c0b402a 100644 --- a/config/assets.php +++ b/config/assets.php @@ -148,12 +148,12 @@ return [ return '$(function () { ' . implode('', $notifies) . ' });'; }, function (Request $request) { - /** @var Locale|null $locale */ + /** @var App\Locale|null $locale */ $localeObj = $request->getAttribute(ServerRequest::ATTR_LOCALE); $locale = ($localeObj instanceof App\Locale) ? (string)$localeObj - : Locale::DEFAULT_LOCALE; + : App\Locale::DEFAULT_LOCALE; $locale = explode('.', $locale, 2)[0]; $localeShort = substr($locale, 0, 2); diff --git a/config/menus/admin.php b/config/menus/admin.php index 432cd68eb..af62f1cb2 100644 --- a/config/menus/admin.php +++ b/config/menus/admin.php @@ -17,32 +17,32 @@ return function (App\Event\BuildAdminMenu $e) { 'items' => [ 'settings' => [ 'label' => __('System Settings'), - 'url' => $router->named('admin:settings:index'), + 'url' => (string)$router->named('admin:settings:index'), 'permission' => Acl::GLOBAL_SETTINGS, ], 'branding' => [ 'label' => __('Custom Branding'), - 'url' => $router->named('admin:branding:index'), + 'url' => (string)$router->named('admin:branding:index'), 'permission' => Acl::GLOBAL_SETTINGS, ], 'logs' => [ 'label' => __('System Logs'), - 'url' => $router->named('admin:logs:index'), + 'url' => (string)$router->named('admin:logs:index'), 'permission' => Acl::GLOBAL_LOGS, ], 'storage_locations' => [ 'label' => __('Storage Locations'), - 'url' => $router->named('admin:storage_locations:index'), + 'url' => (string)$router->named('admin:storage_locations:index'), 'permission' => Acl::GLOBAL_STORAGE_LOCATIONS, ], 'backups' => [ 'label' => __('Backups'), - 'url' => $router->named('admin:backups:index'), + 'url' => (string)$router->named('admin:backups:index'), 'permission' => Acl::GLOBAL_BACKUPS, ], 'debug' => [ 'label' => __('System Debugger'), - 'url' => $router->named('admin:debug:index'), + 'url' => (string)$router->named('admin:debug:index'), 'permission' => Acl::GLOBAL_ALL, ], ], @@ -53,22 +53,22 @@ return function (App\Event\BuildAdminMenu $e) { 'items' => [ 'manage_users' => [ 'label' => __('User Accounts'), - 'url' => $router->named('admin:users:index'), + 'url' => (string)$router->named('admin:users:index'), 'permission' => Acl::GLOBAL_ALL, ], 'permissions' => [ 'label' => __('Permissions'), - 'url' => $router->named('admin:permissions:index'), + 'url' => (string)$router->named('admin:permissions:index'), 'permission' => Acl::GLOBAL_ALL, ], 'auditlog' => [ 'label' => __('Audit Log'), - 'url' => $router->named('admin:auditlog:index'), + 'url' => (string)$router->named('admin:auditlog:index'), 'permission' => Acl::GLOBAL_LOGS, ], 'api_keys' => [ 'label' => __('API Keys'), - 'url' => $router->named('admin:api:index'), + 'url' => (string)$router->named('admin:api:index'), 'permission' => Acl::GLOBAL_API_KEYS, ], ], @@ -79,27 +79,27 @@ return function (App\Event\BuildAdminMenu $e) { 'items' => [ 'manage_stations' => [ 'label' => __('Stations'), - 'url' => $router->named('admin:stations:index'), + 'url' => (string)$router->named('admin:stations:index'), 'permission' => Acl::GLOBAL_STATIONS, ], 'custom_fields' => [ 'label' => __('Custom Fields'), - 'url' => $router->named('admin:custom_fields:index'), + 'url' => (string)$router->named('admin:custom_fields:index'), 'permission' => Acl::GLOBAL_CUSTOM_FIELDS, ], 'relays' => [ 'label' => __('Connected AzuraRelays'), - 'url' => $router->named('admin:relays:index'), + 'url' => (string)$router->named('admin:relays:index'), 'permission' => Acl::GLOBAL_STATIONS, ], 'shoutcast' => [ 'label' => __('Install SHOUTcast'), - 'url' => $router->named('admin:install_shoutcast:index'), + 'url' => (string)$router->named('admin:install_shoutcast:index'), 'permission' => Acl::GLOBAL_ALL, ], 'geolite' => [ 'label' => __('Install GeoLite IP Database'), - 'url' => $router->named('admin:install_geolite:index'), + 'url' => (string)$router->named('admin:install_geolite:index'), 'permission' => Acl::GLOBAL_ALL, ], ], diff --git a/config/menus/station.php b/config/menus/station.php index 91f212727..8315eb98d 100644 --- a/config/menus/station.php +++ b/config/menus/station.php @@ -21,7 +21,7 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Start Station'), 'title' => __('Ready to start broadcasting? Click to start your station.'), 'icon' => 'refresh', - 'url' => $router->fromHere('api:stations:restart'), + 'url' => (string)$router->fromHere('api:stations:restart'), 'class' => 'api-call text-success', 'confirm' => __('Restart broadcasting? This will disconnect any current listeners.'), 'visible' => !$station->getHasStarted(), @@ -31,7 +31,7 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Restart to Apply Changes'), 'title' => __('Click to restart your station and apply configuration changes.'), 'icon' => 'refresh', - 'url' => $router->fromHere('api:stations:restart'), + 'url' => (string)$router->fromHere('api:stations:restart'), 'class' => 'api-call text-warning', 'confirm' => __('Restart broadcasting? This will disconnect any current listeners.'), 'visible' => $station->getHasStarted() && $station->getNeedsRestart(), @@ -40,53 +40,53 @@ return function (App\Event\BuildStationMenu $e) { 'profile' => [ 'label' => __('Profile'), 'icon' => 'image', - 'url' => $router->fromHere('stations:profile:index'), + 'url' => (string)$router->fromHere('stations:profile:index'), ], 'public' => [ 'label' => __('Public Page'), 'icon' => 'public', - 'url' => $router->named('public:index', ['station_id' => $station->getShortName()]), + 'url' => (string)$router->named('public:index', ['station_id' => $station->getShortName()]), 'external' => true, 'visible' => $station->getEnablePublicPage(), ], 'ondemand' => [ 'label' => __('On-Demand Media'), 'icon' => 'cloud_download', - 'url' => $router->named('public:ondemand', ['station_id' => $station->getShortName()]), + 'url' => (string)$router->named('public:ondemand', ['station_id' => $station->getShortName()]), 'external' => true, 'visible' => $station->getEnableOnDemand(), ], 'files' => [ 'label' => __('Music Files'), 'icon' => 'library_music', - 'url' => $router->fromHere('stations:files:index'), + 'url' => (string)$router->fromHere('stations:files:index'), 'visible' => $backend->supportsMedia(), 'permission' => Acl::STATION_MEDIA, ], 'playlists' => [ 'label' => __('Playlists'), 'icon' => 'queue_music', - 'url' => $router->fromHere('stations:playlists:index'), + 'url' => (string)$router->fromHere('stations:playlists:index'), 'visible' => $backend->supportsMedia(), 'permission' => Acl::STATION_MEDIA, ], 'podcasts' => [ 'label' => __('Podcasts (Beta)'), 'icon' => 'cast', - 'url' => $router->fromHere('stations:podcasts:index'), + 'url' => (string)$router->fromHere('stations:podcasts:index'), 'permission' => Acl::STATION_PODCASTS, ], 'streamers' => [ 'label' => __('Streamer/DJ Accounts'), 'icon' => 'mic', - 'url' => $router->fromHere('stations:streamers:index'), + 'url' => (string)$router->fromHere('stations:streamers:index'), 'visible' => $backend->supportsStreamers(), 'permission' => Acl::STATION_STREAMERS, ], 'web_dj' => [ 'label' => __('Web DJ'), 'icon' => 'surround_sound', - 'url' => $router->named('public:dj', ['station_id' => $station->getShortName()], [], true) + 'url' => (string)$router->named('public:dj', ['station_id' => $station->getShortName()], [], true) ->withScheme('https'), 'visible' => $station->getEnablePublicPage() && $station->getEnableStreamers(), 'external' => true, @@ -94,20 +94,20 @@ return function (App\Event\BuildStationMenu $e) { 'mounts' => [ 'label' => __('Mount Points'), 'icon' => 'wifi_tethering', - 'url' => $router->fromHere('stations:mounts:index'), + 'url' => (string)$router->fromHere('stations:mounts:index'), 'visible' => $frontend->supportsMounts(), 'permission' => Acl::STATION_MOUNTS, ], 'remotes' => [ 'label' => __('Remote Relays'), 'icon' => 'router', - 'url' => $router->fromHere('stations:remotes:index'), + 'url' => (string)$router->fromHere('stations:remotes:index'), 'permission' => Acl::STATION_REMOTES, ], 'webhooks' => [ 'label' => __('Web Hooks'), 'icon' => 'code', - 'url' => $router->fromHere('stations:webhooks:index'), + 'url' => (string)$router->fromHere('stations:webhooks:index'), 'permission' => Acl::STATION_WEB_HOOKS, ], 'reports' => [ @@ -117,40 +117,40 @@ return function (App\Event\BuildStationMenu $e) { 'items' => [ 'reports_overview' => [ 'label' => __('Statistics Overview'), - 'url' => $router->fromHere('stations:reports:overview'), + 'url' => (string)$router->fromHere('stations:reports:overview'), ], 'reports_listeners' => [ 'label' => __('Listeners'), - 'url' => $router->fromHere('stations:reports:listeners'), + 'url' => (string)$router->fromHere('stations:reports:listeners'), 'visible' => $frontend->supportsListenerDetail(), ], 'reports_requests' => [ 'label' => __('Song Requests'), - 'url' => $router->fromHere('stations:reports:requests'), + 'url' => (string)$router->fromHere('stations:reports:requests'), 'visible' => $station->getEnableRequests(), ], 'reports_timeline' => [ 'label' => __('Song Playback Timeline'), - 'url' => $router->fromHere('stations:reports:timeline'), + 'url' => (string)$router->fromHere('stations:reports:timeline'), ], 'reports_performance' => [ 'label' => __('Song Listener Impact'), - 'url' => $router->fromHere('stations:reports:performance'), + 'url' => (string)$router->fromHere('stations:reports:performance'), 'visible' => $backend->supportsMedia(), ], 'reports_duplicates' => [ 'label' => __('Duplicate Songs'), - 'url' => $router->fromHere('stations:files:index') . '#special:duplicates', + 'url' => (string)$router->fromHere('stations:files:index') . '#special:duplicates', 'visible' => $backend->supportsMedia(), ], 'reports_unprocessable' => [ 'label' => __('Unprocessable Files'), - 'url' => $router->fromHere('stations:files:index') . '#special:unprocessable', + 'url' => (string)$router->fromHere('stations:files:index') . '#special:unprocessable', 'visible' => $backend->supportsMedia(), ], 'reports_soundexchange' => [ 'label' => __('SoundExchange Royalties'), - 'url' => $router->fromHere('stations:reports:soundexchange'), + 'url' => (string)$router->fromHere('stations:reports:soundexchange'), 'visible' => $frontend->supportsListenerDetail(), ], ], @@ -161,36 +161,36 @@ return function (App\Event\BuildStationMenu $e) { 'items' => [ 'sftp_users' => [ 'label' => __('SFTP Users'), - 'url' => $router->fromHere('stations:sftp_users:index'), + 'url' => (string)$router->fromHere('stations:sftp_users:index'), 'visible' => App\Service\SftpGo::isSupportedForStation($station), 'permission' => Acl::STATION_MEDIA, ], 'automation' => [ 'label' => __('Automated Assignment'), - 'url' => $router->fromHere('stations:automation:index'), + 'url' => (string)$router->fromHere('stations:automation:index'), 'visible' => $backend->supportsMedia(), 'permission' => Acl::STATION_AUTOMATION, ], 'ls_config' => [ 'label' => __('Edit Liquidsoap Configuration'), - 'url' => $router->fromHere('stations:util:ls_config'), + 'url' => (string)$router->fromHere('stations:util:ls_config'), 'visible' => $settings->getEnableAdvancedFeatures() && $backend instanceof App\Radio\Backend\Liquidsoap, 'permission' => Acl::STATION_BROADCASTING, ], 'logs' => [ 'label' => __('Log Viewer'), - 'url' => $router->fromHere('stations:logs:index'), + 'url' => (string)$router->fromHere('stations:logs:index'), 'permission' => Acl::STATION_LOGS, ], 'queue' => [ 'label' => __('Upcoming Song Queue'), - 'url' => $router->fromHere('stations:queue:index'), + 'url' => (string)$router->fromHere('stations:queue:index'), 'permission' => Acl::STATION_BROADCASTING, ], 'restart' => [ 'label' => __('Restart Broadcasting'), - 'url' => $router->fromHere('api:stations:restart'), + 'url' => (string)$router->fromHere('api:stations:restart'), 'class' => 'api-call', 'confirm' => __('Restart broadcasting? This will disconnect any current listeners.'), 'permission' => Acl::STATION_BROADCASTING, diff --git a/phpcs.xml b/phpcs.xml index b86ec0128..f2b1c9a0e 100755 --- a/phpcs.xml +++ b/phpcs.xml @@ -30,6 +30,10 @@ src/Installer/EnvFiles/*.php + + + + diff --git a/phpstan.neon b/phpstan.neon index 283b543d8..1a956d530 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,8 @@ parameters: - level: 3 + level: 8 + + checkGenericClassInNonGenericObjectType: false + checkMissingIterableValueType: false paths: - src @@ -15,6 +18,13 @@ parameters: # Caused by Symfony Validator (perhaps wrongly) returning the interface. - '#Cannot cast Symfony\\Component\\Validator\\ConstraintViolationListInterface to string.#' + # Some doctrine migrations fail because of these + - '#Parameter \#3 \$criteria of method Doctrine\\DBAL\\Connection::update\(\) expects array, array given.#' + - '#Parameter \#2 \$criteria of method Doctrine\\DBAL\\Connection::delete\(\) expects array, array given.#' + + # Known upstream issue with Plates template engine + - '#Parameter \#2 \$callback of method League\\Plates\\Engine::registerFunction\(\) expects League\\Plates\\callback, Closure given.#' + parallel: maximumNumberOfProcesses: 1 diff --git a/src/Acl.php b/src/Acl.php index 54a9e66a4..9d1e02967 100644 --- a/src/Acl.php +++ b/src/Acl.php @@ -1,5 +1,7 @@ permissions)) { + /** @var array $permissions */ $permissions = [ 'global' => [ self::GLOBAL_ALL => __('All Permissions'), @@ -118,7 +121,7 @@ class Acl * @param array|string $action * @param int|Entity\Station|null $stationId */ - public function isAllowed(array|string $action, $stationId = null): bool + public function isAllowed(array|string $action, Entity\Station|int $stationId = null): bool { if ($this->request instanceof ServerRequestInterface) { $user = $this->request->getAttribute(ServerRequest::ATTR_USER); @@ -135,8 +138,11 @@ class Acl * @param array|string $action * @param int|Entity\Station|null $stationId */ - public function userAllowed(?Entity\User $user = null, array|string $action, $stationId = null): bool - { + public function userAllowed( + ?Entity\User $user = null, + array|string $action, + Entity\Station|int $stationId = null + ): bool { if (null === $user) { return false; } @@ -172,7 +178,7 @@ class Acl * @param array|string $action * @param int|Entity\Station|null $station_id */ - public function roleAllowed(array|int $role_id, array|string $action, $station_id = null): bool + public function roleAllowed(array|int $role_id, array|string $action, Entity\Station|int $station_id = null): bool { if ($station_id instanceof Entity\Station) { $station_id = $station_id->getId(); diff --git a/src/AppFactory.php b/src/AppFactory.php index 1daf853a0..a50cb05b7 100644 --- a/src/AppFactory.php +++ b/src/AppFactory.php @@ -1,10 +1,13 @@ $appEnvironment + * @param array $diDefinitions + * + */ + public static function createApp( + ?ClassLoader $autoloader = null, + array $appEnvironment = [], + array $diDefinitions = [] + ): App { $di = self::buildContainer($autoloader, $appEnvironment, $diDefinitions); return self::buildAppFromContainer($di); } - public static function createCli($autoloader = null, $appEnvironment = [], $diDefinitions = []): Application - { + /** + * @param ClassLoader|null $autoloader + * @param array $appEnvironment + * @param array $diDefinitions + * + */ + public static function createCli( + ?ClassLoader $autoloader = null, + array $appEnvironment = [], + array $diDefinitions = [] + ): Application { $di = self::buildContainer($autoloader, $appEnvironment, $diDefinitions); self::buildAppFromContainer($di); @@ -83,11 +104,18 @@ class AppFactory return $app; } - /** @noinspection SummerTimeUnsafeTimeManipulationInspection */ + /** + * @param ClassLoader|null $autoloader + * @param array $appEnvironment + * @param array $diDefinitions + * + * @noinspection SummerTimeUnsafeTimeManipulationInspection + * + */ public static function buildContainer( - $autoloader = null, - $appEnvironment = [], - $diDefinitions = [] + ?ClassLoader $autoloader = null, + array $appEnvironment = [], + array $diDefinitions = [] ): DI\Container { // Register Annotation autoloader if (null !== $autoloader) { @@ -165,7 +193,10 @@ class AppFactory return $di; } - public static function buildEnvironment(array $environment): Environment + /** + * @param array $environment + */ + public static function buildEnvironment(array $environment = []): Environment { if (!isset($environment[Environment::BASE_DIR])) { throw new Exception\BootstrapException('No base directory specified!'); @@ -180,7 +211,10 @@ class AppFactory $environment[Environment::VIEWS_DIR] ??= $environment[Environment::BASE_DIR] . '/templates'; if (file_exists($environment[Environment::BASE_DIR] . '/env.ini')) { - $_ENV = array_merge($_ENV, parse_ini_file($environment[Environment::BASE_DIR] . '/env.ini')); + $envIni = parse_ini_file($environment[Environment::BASE_DIR] . '/env.ini'); + if (false !== $envIni) { + $_ENV = array_merge($_ENV, $envIni); + } } else { $_ENV = getenv(); } diff --git a/src/Assets.php b/src/Assets.php index 663dcb848..3be392ea3 100644 --- a/src/Assets.php +++ b/src/Assets.php @@ -1,11 +1,14 @@ Known libraries loaded in initialization. */ protected array $libraries = []; - /** @var array An optional array lookup for versioned files. */ + /** @var array An optional array lookup for versioned files. */ protected array $versioned_files = []; - /** @var array Loaded libraries. */ + /** @var array Loaded libraries. */ protected array $loaded = []; /** @var bool Whether the current loaded libraries have been sorted by order. */ @@ -40,9 +43,6 @@ class Assets /** @var array The loaded domains that should be included in the CSP header. */ protected array $csp_domains; - /** @var ServerRequestInterface|null The current request (if it's available) */ - protected ?ServerRequestInterface $request = null; - public function __construct( protected Environment $environment, Config $config @@ -51,18 +51,10 @@ class Assets $this->addLibrary($library, $library_name); } - $versioned_files = []; - $assets_file = $environment->getBaseDirectory() . '/web/static/assets.json'; - if (is_file($assets_file)) { - $versioned_files = json_decode(file_get_contents($assets_file), true, 512, JSON_THROW_ON_ERROR); - } + $versioned_files = Json::loadFromFile($environment->getBaseDirectory() . '/web/static/assets.json'); $this->versioned_files = $versioned_files; - $vueComponents = []; - $assets_file = $environment->getBaseDirectory() . '/web/static/webpack.json'; - if (is_file($assets_file)) { - $vueComponents = json_decode(file_get_contents($assets_file), true, 512, JSON_THROW_ON_ERROR); - } + $vueComponents = Json::loadFromFile($environment->getBaseDirectory() . '/web/static/webpack.json'); $this->addVueComponents($vueComponents); $this->csp_nonce = (string)preg_replace('/[^A-Za-z0-9\+\/=]/', '', base64_encode(random_bytes(18))); @@ -538,10 +530,9 @@ class Assets */ protected function addDomainToCsp(string $src): void { - $src_parts = parse_url($src); - - $domain = $src_parts['scheme'] . '://' . $src_parts['host']; + $uri = new Uri($src); + $domain = $uri->getScheme() . '://' . $uri->getHost(); if (!isset($this->csp_domains[$domain])) { $this->csp_domains[$domain] = $domain; } @@ -550,7 +541,7 @@ class Assets public function writeCsp(ResponseInterface $response): ResponseInterface { $csp = []; - if ('https' === $this->request->getUri()->getScheme()) { + if (null !== $this->request && 'https' === $this->request->getUri()->getScheme()) { $csp[] = 'upgrade-insecure-requests'; } diff --git a/src/Auth.php b/src/Auth.php index a14506fc3..b1334e3c5 100644 --- a/src/Auth.php +++ b/src/Auth.php @@ -1,5 +1,7 @@ isMasqueraded()) { return $this->getMasquerade(); @@ -193,13 +195,16 @@ class Auth */ public function getMasquerade(): ?User { - return $this->masqueraded_user; + if ($this->masqueraded_user instanceof User) { + return $this->masqueraded_user; + } + return null; } /** * Become a different user across the application. * - * @param array|User $user_info + * @param array|User $user_info */ public function masqueradeAsUser(User|array $user_info): void { diff --git a/src/Config.php b/src/Config.php index 4d2fd6018..153042dc0 100644 --- a/src/Config.php +++ b/src/Config.php @@ -1,5 +1,7 @@ * @noinspection PhpIncludeInspection * @noinspection UselessUnsetInspection */ diff --git a/src/Console/Application.php b/src/Console/Application.php index f9ed57973..563b9417e 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -1,5 +1,7 @@ setInteractive(false); $temp_stream = fopen($outputFile, 'wb+'); + if (false === $temp_stream) { + throw new \RuntimeException(sprintf('Could not open output file: "%s"', $outputFile)); + } + $output = new StreamOutput($temp_stream); $command = $this->find($command); @@ -31,7 +37,7 @@ class Application extends \Silly\Edition\PhpDi\Application $result_output = stream_get_contents($temp_stream); fclose($temp_stream); - $result_output = trim($result_output); + $result_output = trim((string)$result_output); return [ $result_code, diff --git a/src/Console/Command/Backup/BackupCommand.php b/src/Console/Command/Backup/BackupCommand.php index e420b1c54..5e92a9c00 100644 --- a/src/Console/Command/Backup/BackupCommand.php +++ b/src/Console/Command/Backup/BackupCommand.php @@ -1,5 +1,7 @@ application; } - protected function runCommand(OutputInterface $output, $command_name, $command_args = []): void + protected function runCommand(OutputInterface $output, string $command_name, array $command_args = []): void { $command = $this->getApplication()->find($command_name); diff --git a/src/Console/Command/Debug/OptimizeTablesCommand.php b/src/Console/Command/Debug/OptimizeTablesCommand.php index 7564f5d27..3963ad099 100644 --- a/src/Console/Command/Debug/OptimizeTablesCommand.php +++ b/src/Console/Command/Debug/OptimizeTablesCommand.php @@ -1,5 +1,7 @@ write($acCentral->getIp()); + $io->write($acCentral->getIp() ?? 'Unknown'); return 0; } } diff --git a/src/Console/Command/Internal/NextSongCommand.php b/src/Console/Command/Internal/NextSongCommand.php index 34890fb4f..d6835d861 100644 --- a/src/Console/Command/Internal/NextSongCommand.php +++ b/src/Console/Command/Internal/NextSongCommand.php @@ -1,5 +1,7 @@ getRepository(SftpUser::class)->findOneBy(['username' => $username]); diff --git a/src/Console/Command/Internal/SftpEventCommand.php b/src/Console/Command/Internal/SftpEventCommand.php index 615c0721e..844ea08eb 100644 --- a/src/Console/Command/Internal/SftpEventCommand.php +++ b/src/Console/Command/Internal/SftpEventCommand.php @@ -1,5 +1,7 @@ logger->error('No path specified for action.'); + return 1; + } + return match ($action) { 'upload' => $this->handleNewUpload($storageLocation, $path), 'pre-delete' => $this->handleDelete($storageLocation, $path), @@ -99,7 +106,7 @@ class SftpEventCommand extends CommandAbstract ); $message = new Message\AddNewMediaMessage(); - $message->storage_location_id = $storageLocation->getId(); + $message->storage_location_id = $storageLocation->getIdRequired(); $message->path = $relativePath; $this->messageBus->dispatch($message); @@ -148,8 +155,13 @@ class SftpEventCommand extends CommandAbstract protected function handleRename( Entity\StorageLocation $storageLocation, string $path, - string $newPath + ?string $newPath ): int { + if (null === $newPath) { + $this->logger->error('No new path specified for rename.'); + return 1; + } + $pathPrefixer = new PathPrefixer($storageLocation->getPath(), DIRECTORY_SEPARATOR); $from = $pathPrefixer->stripPrefix($path); diff --git a/src/Console/Command/Locale/GenerateCommand.php b/src/Console/Command/Locale/GenerateCommand.php index 9252d2841..649b62e78 100644 --- a/src/Console/Command/Locale/GenerateCommand.php +++ b/src/Console/Command/Locale/GenerateCommand.php @@ -1,5 +1,7 @@ getRepository(Entity\Role::class) ->find(Entity\Role::SUPER_ADMINISTRATOR_ROLE_ID); - $perms_repo->setActionsForRole($admin_role, [ - 'actions_global' => [ - Acl::GLOBAL_ALL, - ], - ]); + if (null === $admin_role) { + $io->error('Administrator role not found.'); + return 1; + } + + $perms_repo->setActionsForRole( + $admin_role, + [ + 'actions_global' => [ + Acl::GLOBAL_ALL, + ], + ] + ); $user_roles = $user->getRoles(); diff --git a/src/Console/ErrorHandler.php b/src/Console/ErrorHandler.php index a32d31469..3e4afe4f0 100644 --- a/src/Console/ErrorHandler.php +++ b/src/Console/ErrorHandler.php @@ -1,5 +1,7 @@ processLog($request, $log); return $response->withJson([ @@ -55,8 +57,12 @@ abstract class AbstractLogViewerController if ($log_visible_size > 0) { $fp = fopen($log_path, 'rb'); + if (false === $fp) { + throw new \RuntimeException(sprintf('Could not open file at path "%s".', $log_path)); + } + fseek($fp, -$log_visible_size, SEEK_END); - $log_contents_raw = fread($fp, $log_visible_size); + $log_contents_raw = fread($fp, $log_visible_size) ?: ''; fclose($fp); $log_contents = $this->processLog($request, $log_contents_raw, $cut_first_line, true); @@ -91,7 +97,7 @@ abstract class AbstractLogViewerController } /** - * @return mixed[] + * @return array */ protected function getStationLogs(Entity\Station $station): array { diff --git a/src/Controller/Admin/AbstractAdminCrudController.php b/src/Controller/Admin/AbstractAdminCrudController.php index 2a47892f0..283ee792a 100644 --- a/src/Controller/Admin/AbstractAdminCrudController.php +++ b/src/Controller/Admin/AbstractAdminCrudController.php @@ -1,5 +1,7 @@ getRecord($id); return $this->form->process($request, $record); } /** - * @param string|int|null $id + * @param int|string|null $id */ - protected function getRecord($id = null): ?object + protected function getRecord(int|string $id = null): ?object { if (null === $id) { return null; diff --git a/src/Controller/Admin/ApiController.php b/src/Controller/Admin/ApiController.php index 531e5a445..df685c9b0 100644 --- a/src/Controller/Admin/ApiController.php +++ b/src/Controller/Admin/ApiController.php @@ -1,5 +1,7 @@ doEdit($request, $id)) { $request->getFlash()->addMessage(__('API Key updated.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:api:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:api:index')); } - return $request->getView()->renderToResponse($response, 'system/form_page', [ - 'form' => $this->form, - 'render_mode' => 'edit', - 'title' => __('Edit API Key'), - ]); + return $request->getView()->renderToResponse( + $response, + 'system/form_page', + [ + 'form' => $this->form, + 'render_mode' => 'edit', + 'title' => __('Edit API Key'), + ] + ); } - public function deleteAction(ServerRequest $request, Response $response, $id, $csrf): ResponseInterface - { + public function deleteAction( + ServerRequest $request, + Response $response, + string $id, + string $csrf + ): ResponseInterface { $this->doDelete($request, $id, $csrf); $request->getFlash()->addMessage(__('API Key deleted.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:api:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:api:index')); } } diff --git a/src/Controller/Admin/AuditLogController.php b/src/Controller/Admin/AuditLogController.php index a78ca1d67..02f3abe0a 100644 --- a/src/Controller/Admin/AuditLogController.php +++ b/src/Controller/Admin/AuditLogController.php @@ -1,5 +1,7 @@ process($request)) { $request->getFlash()->addMessage(__('Changes saved.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('admin:backups:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('admin:backups:index')); } return $request->getView()->renderToResponse( @@ -123,7 +125,7 @@ class BackupsController extends AbstractLogViewerController ); // Handle submission. - if ($request->isPost() && $runForm->isValid($request->getParsedBody())) { + if ($runForm->isValid($request)) { $data = $runForm->getValues(); $tempFile = File::generateTempPath('backup.log'); @@ -176,7 +178,7 @@ class BackupsController extends AbstractLogViewerController public function downloadAction( ServerRequest $request, Response $response, - $path + string $path ): ResponseInterface { [$path, $fs] = $this->getFile($path); @@ -186,8 +188,12 @@ class BackupsController extends AbstractLogViewerController ->streamFilesystemFile($fs, $path); } - public function deleteAction(ServerRequest $request, Response $response, $path, $csrf): ResponseInterface - { + public function deleteAction( + ServerRequest $request, + Response $response, + string $path, + string $csrf + ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrfNamespace); [$path, $fs] = $this->getFile($path); @@ -196,7 +202,7 @@ class BackupsController extends AbstractLogViewerController $fs->delete($path); $request->getFlash()->addMessage('' . __('Backup deleted.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:backups:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:backups:index')); } /** diff --git a/src/Controller/Admin/BrandingController.php b/src/Controller/Admin/BrandingController.php index 03e252aa3..d643ba5ef 100644 --- a/src/Controller/Admin/BrandingController.php +++ b/src/Controller/Admin/BrandingController.php @@ -1,5 +1,7 @@ doEdit($request, $id)) { $request->getFlash()->addMessage( ($id ? __('Custom Field updated.') : __('Custom Field added.')), Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->named('admin:custom_fields:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:custom_fields:index')); } - return $request->getView()->renderToResponse($response, 'system/form_page', [ - 'form' => $this->form, - 'render_mode' => 'edit', - 'title' => $id ? __('Edit Custom Field') : __('Add Custom Field'), - ]); + return $request->getView()->renderToResponse( + $response, + 'system/form_page', + [ + 'form' => $this->form, + 'render_mode' => 'edit', + 'title' => $id ? __('Edit Custom Field') : __('Add Custom Field'), + ] + ); } - public function deleteAction(ServerRequest $request, Response $response, $id, $csrf): ResponseInterface - { + public function deleteAction( + ServerRequest $request, + Response $response, + int $id, + string $csrf + ): ResponseInterface { $this->doDelete($request, $id, $csrf); $request->getFlash()->addMessage('' . __('Custom Field deleted.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:custom_fields:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:custom_fields:index')); } } diff --git a/src/Controller/Admin/DebugController.php b/src/Controller/Admin/DebugController.php index b0901dbda..1ede312e2 100644 --- a/src/Controller/Admin/DebugController.php +++ b/src/Controller/Admin/DebugController.php @@ -1,5 +1,7 @@ getFlash()->addMessage($resultOutput, Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('admin:debug:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('admin:debug:index')); } public function clearQueueAction( @@ -187,6 +189,6 @@ class DebugController extends AbstractLogViewerController // Flash an update to ensure the session is recreated. $request->getFlash()->addMessage($resultOutput, Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('admin:debug:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('admin:debug:index')); } } diff --git a/src/Controller/Admin/IndexController.php b/src/Controller/Admin/IndexController.php index 81b1fe347..6505b36e3 100644 --- a/src/Controller/Admin/IndexController.php +++ b/src/Controller/Admin/IndexController.php @@ -1,5 +1,7 @@ minus($spaceFree); // Get memory info. - $meminfoRaw = file("/proc/meminfo", FILE_IGNORE_NEW_LINES); + $meminfoRaw = file("/proc/meminfo", FILE_IGNORE_NEW_LINES) ?: []; $meminfo = []; foreach ($meminfoRaw as $line) { if (str_contains($line, ':')) { diff --git a/src/Controller/Admin/InstallGeoLiteController.php b/src/Controller/Admin/InstallGeoLiteController.php index bcf91c2b9..51cbdd748 100644 --- a/src/Controller/Admin/InstallGeoLiteController.php +++ b/src/Controller/Admin/InstallGeoLiteController.php @@ -1,5 +1,7 @@ getCsrf()->verify($csrf, $this->csrf_namespace); @@ -74,6 +76,6 @@ class InstallGeoLiteController @unlink(GeoLite::getDatabasePath()); $request->getFlash()->addMessage(__('GeoLite database uninstalled.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('admin:install_geolite:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('admin:install_geolite:index')); } } diff --git a/src/Controller/Admin/InstallShoutcastController.php b/src/Controller/Admin/InstallShoutcastController.php index 4b0b90170..6e516bcf9 100644 --- a/src/Controller/Admin/InstallShoutcastController.php +++ b/src/Controller/Admin/InstallShoutcastController.php @@ -1,5 +1,7 @@ isPost() && $form->isValid($request->getParsedBody())) { + if ($form->isValid($request)) { try { $sc_base_dir = $environment->getParentDirectory() . '/servers/shoutcast2'; - $files = $request->getUploadedFiles(); - /** @var UploadedFileInterface $import_file */ - $import_file = $files['binary']; + $values = $form->getValues(); - if (UPLOAD_ERR_OK === $import_file->getError()) { + $import_file = $values['binary'] ?? null; + if ($import_file instanceof UploadedFileInterface) { $sc_tgz_path = $sc_base_dir . '/sc_serv.tar.gz'; if (is_file($sc_tgz_path)) { unlink($sc_tgz_path); diff --git a/src/Controller/Admin/LogsController.php b/src/Controller/Admin/LogsController.php index 89a4dea9c..59bfb17be 100644 --- a/src/Controller/Admin/LogsController.php +++ b/src/Controller/Admin/LogsController.php @@ -1,5 +1,7 @@ */ protected function getGlobalLogs(): array { @@ -82,8 +84,12 @@ class LogsController extends AbstractLogViewerController return $logPaths; } - public function viewAction(ServerRequest $request, Response $response, $station_id, $log): ResponseInterface - { + public function viewAction( + ServerRequest $request, + Response $response, + string|int $station_id, + string $log + ): ResponseInterface { if ('global' === $station_id) { $log_areas = $this->getGlobalLogs(); } else { @@ -94,7 +100,7 @@ class LogsController extends AbstractLogViewerController throw new Exception('Invalid log file specified.'); } - $log = $log_areas[$log]; - return $this->view($request, $response, $log['path'], $log['tail'] ?? true); + $logArea = $log_areas[$log]; + return $this->view($request, $response, $logArea['path'], $logArea['tail'] ?? true); } } diff --git a/src/Controller/Admin/PermissionsController.php b/src/Controller/Admin/PermissionsController.php index b5e62b8ef..d6e9f1ff1 100644 --- a/src/Controller/Admin/PermissionsController.php +++ b/src/Controller/Admin/PermissionsController.php @@ -1,5 +1,7 @@ doEdit($request, $id)) { $request->getFlash()->addMessage( '' . ($id ? __('Permission updated.') : __('Permission added.')) . '', Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->named('admin:permissions:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:permissions:index')); } - return $request->getView()->renderToResponse($response, 'system/form_page', [ - 'form' => $this->form, - 'render_mode' => 'edit', - 'title' => $id ? __('Edit Permission') : __('Add Permission'), - ]); + return $request->getView()->renderToResponse( + $response, + 'system/form_page', + [ + 'form' => $this->form, + 'render_mode' => 'edit', + 'title' => $id ? __('Edit Permission') : __('Add Permission'), + ] + ); } - public function deleteAction(ServerRequest $request, Response $response, $id, $csrf): ResponseInterface - { + public function deleteAction( + ServerRequest $request, + Response $response, + int $id, + string $csrf + ): ResponseInterface { $this->doDelete($request, $id, $csrf); $request->getFlash()->addMessage('' . __('Permission deleted.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:permissions:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:permissions:index')); } } diff --git a/src/Controller/Admin/RelaysController.php b/src/Controller/Admin/RelaysController.php index 409eb4e72..24ea43879 100644 --- a/src/Controller/Admin/RelaysController.php +++ b/src/Controller/Admin/RelaysController.php @@ -1,5 +1,7 @@ doEdit($request, $id)) { $request->getFlash()->addMessage(($id ? __('Station updated.') : __('Station added.')), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:stations:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:stations:index')); } - return $request->getView()->renderToResponse($response, 'admin/stations/edit', [ - 'form' => $this->form, - 'title' => $id ? __('Edit Station') : 'Add Station', - ]); + return $request->getView()->renderToResponse( + $response, + 'admin/stations/edit', + [ + 'form' => $this->form, + 'title' => $id ? __('Edit Station') : 'Add Station', + ] + ); } - public function deleteAction(ServerRequest $request, Response $response, $id, $csrf): ResponseInterface - { + public function deleteAction( + ServerRequest $request, + Response $response, + int $id, + string $csrf + ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); - $record = $this->record_repo->find((int)$id); + $record = $this->record_repo->find($id); if ($record instanceof Entity\Station) { $this->stationRepo->destroy($record); } $request->getFlash()->addMessage(__('Station deleted.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:stations:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:stations:index')); } - public function cloneAction(ServerRequest $request, Response $response, $id): ResponseInterface + public function cloneAction(ServerRequest $request, Response $response, int $id): ResponseInterface { $cloneForm = $this->factory->make(Form\StationCloneForm::class); - $record = $this->record_repo->find((int)$id); + $record = $this->record_repo->find($id); if (!($record instanceof Entity\Station)) { throw new NotFoundException(__('Station not found.')); } if (false !== $cloneForm->process($request, $record)) { $request->getFlash()->addMessage(__('Changes saved.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:stations:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:stations:index')); } return $request->getView()->renderToResponse( $response, 'system/form_page', [ - 'form' => $cloneForm, - 'render_mode' => 'edit', - 'title' => __('Clone Station: %s', $record->getName()), + 'form' => $cloneForm, + 'render_mode' => 'edit', + 'title' => __('Clone Station: %s', $record->getName()), ] ); } diff --git a/src/Controller/Admin/StorageLocationsController.php b/src/Controller/Admin/StorageLocationsController.php index 55db38969..874d46ec5 100644 --- a/src/Controller/Admin/StorageLocationsController.php +++ b/src/Controller/Admin/StorageLocationsController.php @@ -1,5 +1,7 @@ doEdit($request, $id)) { $request->getFlash()->addMessage(($id ? __('User updated.') : __('User added.')), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('admin:users:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:users:index')); } } catch (UniqueConstraintViolationException) { $request->getFlash()->addMessage( @@ -54,18 +56,26 @@ class UsersController extends AbstractAdminCrudController ); } - return $request->getView()->renderToResponse($response, 'system/form_page', [ - 'form' => $this->form, - 'render_mode' => 'edit', - 'title' => $id ? __('Edit User') : __('Add User'), - ]); + return $request->getView()->renderToResponse( + $response, + 'system/form_page', + [ + 'form' => $this->form, + 'render_mode' => 'edit', + 'title' => $id ? __('Edit User') : __('Add User'), + ] + ); } - public function deleteAction(ServerRequest $request, Response $response, $id, $csrf): ResponseInterface - { + public function deleteAction( + ServerRequest $request, + Response $response, + int $id, + string $csrf + ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); - $user = $this->record_repo->find((int)$id); + $user = $this->record_repo->find($id); $current_user = $request->getUser(); @@ -78,18 +88,18 @@ class UsersController extends AbstractAdminCrudController $request->getFlash()->addMessage('' . __('User deleted.') . '', Flash::SUCCESS); } - return $response->withRedirect($request->getRouter()->named('admin:users:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:users:index')); } public function impersonateAction( ServerRequest $request, Response $response, - $id, - $csrf + int $id, + string $csrf ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); - $user = $this->record_repo->find((int)$id); + $user = $this->record_repo->find($id); if (!($user instanceof Entity\User)) { throw new NotFoundException(__('User not found.')); @@ -103,6 +113,6 @@ class UsersController extends AbstractAdminCrudController Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } } diff --git a/src/Controller/Api/AbstractApiCrudController.php b/src/Controller/Api/AbstractApiCrudController.php index 962d04f04..b4e7b69d7 100644 --- a/src/Controller/Api/AbstractApiCrudController.php +++ b/src/Controller/Api/AbstractApiCrudController.php @@ -1,15 +1,16 @@ The fully-qualified (::class) class name of the entity being managed. */ protected string $entityClass; /** @var string The route name used to generate the "self" links for each record. */ @@ -59,7 +63,7 @@ abstract class AbstractApiCrudController } /** - * @param object $record + * @param TEntity $record * @param ServerRequest $request * */ @@ -74,21 +78,28 @@ abstract class AbstractApiCrudController $isInternal = ('true' === $request->getParam('internal', 'false')); $router = $request->getRouter(); - $return['links'] = [ - 'self' => $router->fromHere($this->resourceRouteName, ['id' => $record->getId()], [], !$isInternal), - ]; + if ($record instanceof IdentifiableEntityInterface) { + $return['links'] = [ + 'self' => (string)$router->fromHere( + $this->resourceRouteName, + ['id' => $record->getIdRequired()], + [], + !$isInternal + ), + ]; + } return $return; } /** - * @param object $record - * @param array $context + * @param TEntity $record + * @param array $context * - * @return mixed[] + * @return array */ protected function toArray(object $record, array $context = []): array { - return $this->serializer->normalize( + return (array)$this->serializer->normalize( $record, null, array_merge( @@ -126,15 +137,25 @@ abstract class AbstractApiCrudController return $object->getName(); } - return $object->getId(); + if ($object instanceof IdentifiableEntityInterface) { + return $object->getIdRequired(); + } + + if ($object instanceof \Stringable) { + return (string)$object; + } + + return get_class($object) . ': ' . spl_object_hash($object); } /** - * @param array|null $data - * @param object|null $record - * @param array $context + * @param array|null $data + * @param TEntity|null $record + * @param array $context + * + * @return TEntity */ - protected function editRecord(?array $data, $record = null, array $context = []): object + protected function editRecord(?array $data, ?object $record = null, array $context = []): object { if (null === $data) { throw new InvalidArgumentException('Could not parse input data.'); @@ -156,11 +177,13 @@ abstract class AbstractApiCrudController } /** - * @param array $data - * @param object|null $record - * @param array $context + * @param array $data + * @param TEntity|null $record + * @param array $context + * + * @return TEntity */ - protected function fromArray(array $data, $record = null, array $context = []): object + protected function fromArray(array $data, ?object $record = null, array $context = []): object { if (null !== $record) { $context[ObjectNormalizer::OBJECT_TO_POPULATE] = $record; @@ -170,10 +193,7 @@ abstract class AbstractApiCrudController } /** - * @param object $record - * - * @throws ORMException - * @throws OptimisticLockException + * @param TEntity $record */ protected function deleteRecord(object $record): void { diff --git a/src/Controller/Api/Admin/AbstractAdminApiCrudController.php b/src/Controller/Api/Admin/AbstractAdminApiCrudController.php index 4db76ec32..680e7ea4f 100644 --- a/src/Controller/Api/Admin/AbstractAdminApiCrudController.php +++ b/src/Controller/Api/Admin/AbstractAdminApiCrudController.php @@ -1,5 +1,7 @@ + */ abstract class AbstractAdminApiCrudController extends AbstractApiCrudController { public function listAction(ServerRequest $request, Response $response): ResponseInterface @@ -29,7 +32,7 @@ abstract class AbstractAdminApiCrudController extends AbstractApiCrudController */ public function createAction(ServerRequest $request, Response $response): ResponseInterface { - $row = $this->createRecord($request->getParsedBody()); + $row = $this->createRecord((array)$request->getParsedBody()); $return = $this->viewRecord($row, $request); return $response->withJson($return); @@ -37,6 +40,8 @@ abstract class AbstractAdminApiCrudController extends AbstractApiCrudController /** * @param array $data + * + * @return TEntity */ protected function createRecord(array $data): object { @@ -54,7 +59,7 @@ abstract class AbstractAdminApiCrudController extends AbstractApiCrudController if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $return = $this->viewRecord($record, $request); @@ -64,9 +69,7 @@ abstract class AbstractAdminApiCrudController extends AbstractApiCrudController /** * @param mixed $id * - * @throws ORMException - * @throws OptimisticLockException - * @throws TransactionRequiredException + * @return TEntity|null */ protected function getRecord(mixed $id): ?object { @@ -84,10 +87,10 @@ abstract class AbstractAdminApiCrudController extends AbstractApiCrudController if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } - $this->editRecord($request->getParsedBody(), $record); + $this->editRecord((array)$request->getParsedBody(), $record); return $response->withJson(new Entity\Api\Status(true, __('Changes saved successfully.'))); } @@ -103,7 +106,7 @@ abstract class AbstractAdminApiCrudController extends AbstractApiCrudController if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $this->deleteRecord($record); diff --git a/src/Controller/Api/Admin/AuditLogController.php b/src/Controller/Api/Admin/AuditLogController.php index 83bcc03b7..823cd549b 100644 --- a/src/Controller/Api/Admin/AuditLogController.php +++ b/src/Controller/Api/Admin/AuditLogController.php @@ -1,5 +1,7 @@ + */ class CustomFieldsController extends AbstractAdminApiCrudController { protected string $entityClass = Entity\CustomField::class; diff --git a/src/Controller/Api/Admin/PermissionsController.php b/src/Controller/Api/Admin/PermissionsController.php index 2a676ba3b..918e59721 100644 --- a/src/Controller/Api/Admin/PermissionsController.php +++ b/src/Controller/Api/Admin/PermissionsController.php @@ -1,5 +1,7 @@ adapters->getFrontendAdapter($station); $row = new Entity\Api\Admin\Relay(); - $row->id = $station->getId(); + $row->id = $station->getIdRequired(); $row->name = $station->getName(); $row->shortcode = $station->getShortName(); $row->description = $station->getDescription(); @@ -108,14 +110,13 @@ class RelaysController { $relay_repo = $this->em->getRepository(Entity\Relay::class); - $body = $request->getParsedBody(); + $body = (array)$request->getParsedBody(); if (!empty($body['base_url'])) { $base_url = $body['base_url']; } else { - $serverParams = $request->getServerParams(); /** @noinspection HttpUrlsUsage */ - $base_url = 'http://' . $serverParams('REMOTE_ADDR'); + $base_url = 'http://' . $request->getIp(); } $relay = $relay_repo->findOneBy(['base_url' => $base_url]); diff --git a/src/Controller/Api/Admin/RolesController.php b/src/Controller/Api/Admin/RolesController.php index 51e6b4203..0c422cdfa 100644 --- a/src/Controller/Api/Admin/RolesController.php +++ b/src/Controller/Api/Admin/RolesController.php @@ -1,5 +1,7 @@ + */ class RolesController extends AbstractAdminApiCrudController { protected string $entityClass = Entity\Role::class; diff --git a/src/Controller/Api/Admin/SettingsController.php b/src/Controller/Api/Admin/SettingsController.php index 775f84787..449a0ff79 100644 --- a/src/Controller/Api/Admin/SettingsController.php +++ b/src/Controller/Api/Admin/SettingsController.php @@ -1,5 +1,7 @@ + */ class SettingsController extends AbstractApiCrudController { public function __construct( @@ -66,7 +71,7 @@ class SettingsController extends AbstractApiCrudController public function updateAction(ServerRequest $request, Response $response): ResponseInterface { $settings = $this->settingsRepo->readSettings(); - $this->editRecord($request->getParsedBody(), $settings); + $this->editRecord((array)$request->getParsedBody(), $settings); return $response->withJson(new Entity\Api\Status()); } diff --git a/src/Controller/Api/Admin/StationsController.php b/src/Controller/Api/Admin/StationsController.php index 410c0163d..8fc91fa1e 100644 --- a/src/Controller/Api/Admin/StationsController.php +++ b/src/Controller/Api/Admin/StationsController.php @@ -1,5 +1,7 @@ + */ class StationsController extends AbstractAdminApiCrudController { protected string $entityClass = Entity\Station::class; @@ -104,7 +109,12 @@ class StationsController extends AbstractAdminApiCrudController * ) */ - /** @inheritDoc */ + /** + * @param Entity\Station $record + * @param array $context + * + * @return array + */ protected function toArray(object $record, array $context = []): array { return parent::toArray( @@ -122,8 +132,14 @@ class StationsController extends AbstractAdminApiCrudController ); } - /** @inheritDoc */ - protected function editRecord(?array $data, $record = null, array $context = []): object + /** + * @param array|null $data + * @param Entity\Station|null $record + * @param array $context + * + * @return Entity\Station + */ + protected function editRecord(?array $data, object $record = null, array $context = []): object { $create_mode = (null === $record); @@ -150,7 +166,9 @@ class StationsController extends AbstractAdminApiCrudController return $this->station_repo->edit($record); } - /** @inheritDoc */ + /** + * @param Entity\Station $record + */ protected function deleteRecord(object $record): void { $this->station_repo->destroy($record); diff --git a/src/Controller/Api/Admin/StorageLocationsController.php b/src/Controller/Api/Admin/StorageLocationsController.php index 6aa08fed5..acd26bac1 100644 --- a/src/Controller/Api/Admin/StorageLocationsController.php +++ b/src/Controller/Api/Admin/StorageLocationsController.php @@ -1,5 +1,7 @@ + */ class StorageLocationsController extends AbstractAdminApiCrudController { protected string $entityClass = Entity\StorageLocation::class; @@ -125,9 +130,8 @@ class StorageLocationsController extends AbstractAdminApiCrudController } /** @inheritDoc */ - protected function viewRecord(object $record, ServerRequest $request): Entity\Api\Admin\StorageLocation + protected function viewRecord(object $record, ServerRequest $request): object { - /** @var Entity\StorageLocation $record */ $original = parent::viewRecord($record, $request); $return = new Entity\Api\Admin\StorageLocation(); diff --git a/src/Controller/Api/Admin/UsersController.php b/src/Controller/Api/Admin/UsersController.php index 3a2ac7225..41edfbf8b 100644 --- a/src/Controller/Api/Admin/UsersController.php +++ b/src/Controller/Api/Admin/UsersController.php @@ -1,5 +1,7 @@ + */ class UsersController extends AbstractAdminApiCrudController { protected string $entityClass = Entity\User::class; @@ -97,12 +102,11 @@ class UsersController extends AbstractAdminApiCrudController */ public function deleteAction(ServerRequest $request, Response $response, mixed $id): ResponseInterface { - /** @var Entity\User|null $record */ $record = $this->getRecord($id); if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $current_user = $request->getUser(); diff --git a/src/Controller/Api/Frontend/Account/GetMeAction.php b/src/Controller/Api/Frontend/Account/GetMeAction.php index 228acd473..255ba51cb 100644 --- a/src/Controller/Api/Frontend/Account/GetMeAction.php +++ b/src/Controller/Api/Frontend/Account/GetMeAction.php @@ -1,5 +1,7 @@ getUser(); - $this->editRecord($request->getParsedBody(), $user); + $this->editRecord((array)$request->getParsedBody(), $user); return $response->withJson(new Entity\Api\Status(true, __('Changes saved successfully.'))); } diff --git a/src/Controller/Api/Frontend/Dashboard/ChartsAction.php b/src/Controller/Api/Frontend/Dashboard/ChartsAction.php index 55f7aa293..76e9c15d3 100644 --- a/src/Controller/Api/Frontend/Dashboard/ChartsAction.php +++ b/src/Controller/Api/Frontend/Dashboard/ChartsAction.php @@ -1,5 +1,7 @@ format('Y-m-d'); $jsTimestamp = $moment->getTimestamp() * 1000; - $average = round($row['number_avg'], 2); + $average = round((float)$row['number_avg'], 2); $unique = $row['number_unique']; $rawStats['average'][$stationId][$sortableKey] = [ diff --git a/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php b/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php index df3b5f7be..cee991923 100644 --- a/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php +++ b/src/Controller/Api/Frontend/Dashboard/NotificationsAction.php @@ -1,5 +1,7 @@ getRouter(); // Pull NP data from the fastest/first available source using the EventDispatcher. @@ -101,7 +106,7 @@ class NowplayingController implements EventSubscriberInterface } return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, 'Station not found.')); + ->withJson(Entity\Api\Error::notFound()); } // If unauthenticated, hide non-public stations from full view. diff --git a/src/Controller/Api/OpenApiController.php b/src/Controller/Api/OpenApiController.php index 2ccc58d92..066b65baa 100644 --- a/src/Controller/Api/OpenApiController.php +++ b/src/Controller/Api/OpenApiController.php @@ -1,5 +1,7 @@ getRouter()->fromHere(null, [], [], true); + $api_base_url = (string)$request->getRouter()->fromHere(absolute: true); $api_base_url = str_replace('/openapi.yml', '', $api_base_url); define('AZURACAST_API_URL', $api_base_url); diff --git a/src/Controller/Api/Stations/AbstractScheduledEntityController.php b/src/Controller/Api/Stations/AbstractScheduledEntityController.php index 75b0048a6..5d4ac25cb 100644 --- a/src/Controller/Api/Stations/AbstractScheduledEntityController.php +++ b/src/Controller/Api/Stations/AbstractScheduledEntityController.php @@ -1,5 +1,7 @@ + */ abstract class AbstractScheduledEntityController extends AbstractStationApiCrudController { public function __construct( @@ -37,11 +43,21 @@ abstract class AbstractScheduledEntityController extends AbstractStationApiCrudC $params = $request->getQueryParams(); $startDateStr = substr($params['start'], 0, 10); - $startDate = CarbonImmutable::createFromFormat('Y-m-d', $startDateStr, $tz)->subDay(); + $startDate = CarbonImmutable::createFromFormat('Y-m-d', $startDateStr, $tz); + + if (false === $startDate) { + throw new \InvalidArgumentException(sprintf('Could not parse start date: "%s"', $startDateStr)); + } + + $startDate = $startDate->subDay(); $endDateStr = substr($params['end'], 0, 10); $endDate = CarbonImmutable::createFromFormat('Y-m-d', $endDateStr, $tz); + if (false === $endDate) { + throw new \InvalidArgumentException(sprintf('Could not parse end date: "%s"', $endDateStr)); + } + $events = []; foreach ($scheduleItems as $scheduleItem) { @@ -73,13 +89,13 @@ abstract class AbstractScheduledEntityController extends AbstractStationApiCrudC return $response->withJson($events); } - protected function editRecord(?array $data, $record = null, array $context = []): object + protected function editRecord(?array $data, object $record = null, array $context = []): object { if (null === $data) { throw new InvalidArgumentException('Could not parse input data.'); } - $scheduleItems = $data['schedule_items'] ?? null; + $scheduleItems = $data['schedule_items'] ?? []; unset($data['schedule_items']); $record = $this->fromArray($data, $record, $context); diff --git a/src/Controller/Api/Stations/AbstractStationApiCrudController.php b/src/Controller/Api/Stations/AbstractStationApiCrudController.php index a3f1579b5..79e15d5f3 100644 --- a/src/Controller/Api/Stations/AbstractStationApiCrudController.php +++ b/src/Controller/Api/Stations/AbstractStationApiCrudController.php @@ -1,5 +1,7 @@ + */ abstract class AbstractStationApiCrudController extends AbstractApiCrudController { /** @@ -46,7 +52,7 @@ abstract class AbstractStationApiCrudController extends AbstractApiCrudControlle public function createAction(ServerRequest $request, Response $response): ResponseInterface { $station = $this->getStation($request); - $row = $this->createRecord($request->getParsedBody(), $station); + $row = $this->createRecord((array)$request->getParsedBody(), $station); $return = $this->viewRecord($row, $request); @@ -56,6 +62,8 @@ abstract class AbstractStationApiCrudController extends AbstractApiCrudControlle /** * @param array $data * @param Entity\Station $station + * + * @return TEntity */ protected function createRecord(array $data, Entity\Station $station): object { @@ -75,23 +83,23 @@ abstract class AbstractStationApiCrudController extends AbstractApiCrudControlle /** * @param ServerRequest $request * @param Response $response - * @param int|string $station_id - * @param int|string $id + * @param int $station_id + * @param int $id * * @throws Exception */ public function getAction( ServerRequest $request, Response $response, - int|string $station_id, - int|string $id + int $station_id, + int $id ): ResponseInterface { $station = $this->getStation($request); $record = $this->getRecord($station, $id); if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $return = $this->viewRecord($record, $request); @@ -101,6 +109,8 @@ abstract class AbstractStationApiCrudController extends AbstractApiCrudControlle /** * @param Entity\Station $station * @param int|string $id + * + * @return TEntity */ protected function getRecord(Entity\Station $station, int|string $id): ?object { @@ -115,23 +125,23 @@ abstract class AbstractStationApiCrudController extends AbstractApiCrudControlle /** * @param ServerRequest $request * @param Response $response - * @param int|string $station_id - * @param int|string $id + * @param int $station_id + * @param int $id */ public function editAction( ServerRequest $request, Response $response, - int|string $station_id, - int|string $id + int $station_id, + int $id ): ResponseInterface { $record = $this->getRecord($this->getStation($request), $id); if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } - $this->editRecord($request->getParsedBody(), $record); + $this->editRecord((array)$request->getParsedBody(), $record); return $response->withJson(new Entity\Api\Status(true, __('Changes saved successfully.'))); } @@ -139,20 +149,20 @@ abstract class AbstractStationApiCrudController extends AbstractApiCrudControlle /** * @param ServerRequest $request * @param Response $response - * @param int|string $station_id - * @param int|string $id + * @param int $station_id + * @param int $id */ public function deleteAction( ServerRequest $request, Response $response, - int|string $station_id, - int|string $id + int $station_id, + int $id ): ResponseInterface { $record = $this->getRecord($this->getStation($request), $id); if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $this->deleteRecord($record); diff --git a/src/Controller/Api/Stations/Art/DeleteArtAction.php b/src/Controller/Api/Stations/Art/DeleteArtAction.php index e5b4f0024..2df1533c3 100644 --- a/src/Controller/Api/Stations/Art/DeleteArtAction.php +++ b/src/Controller/Api/Stations/Art/DeleteArtAction.php @@ -1,5 +1,7 @@ find($media_id, $station); if (!($media instanceof Entity\StationMedia)) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found.'))); + ->withJson(Entity\Api\Error::notFound()); } $mediaRepo->removeAlbumArt($media); diff --git a/src/Controller/Api/Stations/Art/GetArtAction.php b/src/Controller/Api/Stations/Art/GetArtAction.php index b765c975e..7d18fd45f 100644 --- a/src/Controller/Api/Stations/Art/GetArtAction.php +++ b/src/Controller/Api/Stations/Art/GetArtAction.php @@ -1,5 +1,7 @@ getMediaFilesystem(); - $defaultArtRedirect = $response->withRedirect($stationRepo->getDefaultAlbumArtUrl($station), 302); + $defaultArtRedirect = $response->withRedirect((string)$stationRepo->getDefaultAlbumArtUrl($station), 302); // If a timestamp delimiter is added, strip it automatically. $media_id = explode('-', $media_id, 2)[0]; diff --git a/src/Controller/Api/Stations/Art/PostArtAction.php b/src/Controller/Api/Stations/Art/PostArtAction.php index 0235adf8b..9a8edc542 100644 --- a/src/Controller/Api/Stations/Art/PostArtAction.php +++ b/src/Controller/Api/Stations/Art/PostArtAction.php @@ -1,5 +1,7 @@ find($media_id, $station); if (!($media instanceof Entity\StationMedia)) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found.'))); + ->withJson(Entity\Api\Error::notFound()); } $flowResponse = Flow::process($request, $response, $station->getRadioTempDir()); diff --git a/src/Controller/Api/Stations/Files/BatchAction.php b/src/Controller/Api/Stations/Files/BatchAction.php index 2e7cb7f3e..7c68996e1 100644 --- a/src/Controller/Api/Stations/Files/BatchAction.php +++ b/src/Controller/Api/Stations/Files/BatchAction.php @@ -1,5 +1,7 @@ em->flush(); foreach ($playlists as $playlistRecord) { + /** @var Entity\StationPlaylist $playlist */ $playlist = $this->em->refetchAsReference($playlistRecord); $playlistWeights[$playlist->getId()]++; @@ -170,6 +173,7 @@ class BatchAction } } + /** @var Entity\Station $station */ $station = $this->em->refetch($station); foreach ($result->directories as $dir) { @@ -226,7 +230,7 @@ class BatchAction $toMove = [ $this->batchUtilities->iterateMediaInDirectory($storageLocation, $dirPath), $this->batchUtilities->iterateUnprocessableMediaInDirectory($storageLocation, $dirPath), - $this->batchUtilities->iteratePlaylistFoldersInDirectory($station, $dirPath), + $this->batchUtilities->iteratePlaylistFoldersInDirectory($storageLocation, $dirPath), ]; foreach ($toMove as $iterator) { diff --git a/src/Controller/Api/Stations/Files/DownloadAction.php b/src/Controller/Api/Stations/Files/DownloadAction.php index 36b99570d..9811ced15 100644 --- a/src/Controller/Api/Stations/Files/DownloadAction.php +++ b/src/Controller/Api/Stations/Files/DownloadAction.php @@ -1,8 +1,10 @@ fileExists($path)) { return $response->withStatus(404) - ->withJson(new Error(404, 'File not found.')); + ->withJson(Entity\Api\Error::notFound()); } return $response->streamFilesystemFile($fsMedia, $path); diff --git a/src/Controller/Api/Stations/Files/FlowUploadAction.php b/src/Controller/Api/Stations/Files/FlowUploadAction.php index 389ca37c7..cfa4e173d 100644 --- a/src/Controller/Api/Stations/Files/FlowUploadAction.php +++ b/src/Controller/Api/Stations/Files/FlowUploadAction.php @@ -1,5 +1,7 @@ withStatus(400) - ->withJson(new Entity\Api\Error('Playlist not found.')); + ->withJson(new Entity\Api\Error(400, 'Playlist not found.')); } $mediaQueryBuilder->andWhere( @@ -177,7 +179,7 @@ class ListAction $media->genre = (string)$row['genre']; $media->is_playable = ($row['length'] !== 0); - $media->length = $row['length']; + $media->length = (int)$row['length']; $media->length_text = $row['length_text']; $media->media_id = $row['id']; @@ -315,7 +317,7 @@ class ListAction $paginator = Paginator::fromArray($result, $request); // Add processor-intensive data for just this page. - $stationId = $station->getId(); + $stationId = $station->getIdRequired(); $isInternal = (bool)$request->getParam('internal', false); $defaultAlbumArtUrl = (string)$stationRepo->getDefaultAlbumArtUrl($station); diff --git a/src/Controller/Api/Stations/Files/ListDirectoriesAction.php b/src/Controller/Api/Stations/Files/ListDirectoriesAction.php index d621fa8de..a73006744 100644 --- a/src/Controller/Api/Stations/Files/ListDirectoriesAction.php +++ b/src/Controller/Api/Stations/Files/ListDirectoriesAction.php @@ -1,5 +1,7 @@ withStatus(404) - ->withJson(new Entity\Api\Error(404, 'Not Found')); + ->withJson(Entity\Api\Error::notFound()); } $fsMedia = (new StationFilesystems($station))->getMediaFilesystem(); diff --git a/src/Controller/Api/Stations/Files/RenameAction.php b/src/Controller/Api/Stations/Files/RenameAction.php index 69ab2b5a2..6b581bad3 100644 --- a/src/Controller/Api/Stations/Files/RenameAction.php +++ b/src/Controller/Api/Stations/Files/RenameAction.php @@ -1,5 +1,7 @@ + */ class FilesController extends AbstractStationApiCrudController { protected string $entityClass = Entity\StationMedia::class; @@ -179,19 +184,19 @@ class FilesController extends AbstractStationApiCrudController public function editAction( ServerRequest $request, Response $response, - int|string $station_id, - int|string $id + int $station_id, + int $id ): ResponseInterface { $station = $this->getStation($request); $record = $this->getRecord($station, $id); if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $data = $request->getParsedBody(); - if (null === $data) { + if (!is_array($data)) { throw new InvalidArgumentException('Could not parse input data.'); } diff --git a/src/Controller/Api/Stations/HistoryController.php b/src/Controller/Api/Stations/HistoryController.php index 25ea8cc26..58f93171a 100644 --- a/src/Controller/Api/Stations/HistoryController.php +++ b/src/Controller/Api/Stations/HistoryController.php @@ -1,5 +1,7 @@ parse($userAgent); if ($dd->isBot()) { - $clientBot = $dd->getBot(); + $clientBot = (array)$dd->getBot(); $clientBotName = $clientBot['name'] ?? 'Unknown Crawler'; $clientBotType = $clientBot['category'] ?? 'Generic Crawler'; $client = $clientBotName . ' (' . $clientBotType . ')'; } else { - $clientInfo = $dd->getClient(); + $clientInfo = (array)$dd->getClient(); $clientBrowser = $clientInfo['name'] ?? 'Unknown Browser'; $clientVersion = $clientInfo['version'] ?? '0.00'; - $clientOsInfo = $dd->getOs(); + $clientOsInfo = (array)$dd->getOs(); $clientOs = $clientOsInfo['name'] ?? 'Unknown OS'; $client = $clientBrowser . ' ' . $clientVersion . ', ' . $clientOs; @@ -237,7 +239,7 @@ class ListenersAction array $listeners, string $filename ): ResponseInterface { - $tempFile = tmpfile(); + $tempFile = tmpfile() ?: throw new \RuntimeException('Could not create temp file.'); $csv = Writer::createFromStream($tempFile); $tz = $station->getTimezoneObject(); diff --git a/src/Controller/Api/Stations/MountsController.php b/src/Controller/Api/Stations/MountsController.php index c28d2b2c3..2511870a1 100644 --- a/src/Controller/Api/Stations/MountsController.php +++ b/src/Controller/Api/Stations/MountsController.php @@ -1,5 +1,7 @@ + */ class MountsController extends AbstractStationApiCrudController { protected string $entityClass = Entity\StationMount::class; diff --git a/src/Controller/Api/Stations/OnDemand/DownloadAction.php b/src/Controller/Api/Stations/OnDemand/DownloadAction.php index c7bce7455..4ae975111 100644 --- a/src/Controller/Api/Stations/OnDemand/DownloadAction.php +++ b/src/Controller/Api/Stations/OnDemand/DownloadAction.php @@ -1,5 +1,7 @@ withStatus(404) - ->withJson(new Entity\Api\Error(404, __('File not found.'))); + ->withJson(Entity\Api\Error::notFound()); } $fsMedia = (new StationFilesystems($station))->getMediaFilesystem(); diff --git a/src/Controller/Api/Stations/OnDemand/ListAction.php b/src/Controller/Api/Stations/OnDemand/ListAction.php index 2f0639590..18fa9055d 100644 --- a/src/Controller/Api/Stations/OnDemand/ListAction.php +++ b/src/Controller/Api/Stations/OnDemand/ListAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); - $data = $request->getParsedBody(); + $data = (array)$request->getParsedBody(); $copier = new DeepCopy\DeepCopy(); $copier->addFilter( diff --git a/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php b/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php index 84c10ee7d..40e6a00b7 100644 --- a/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php +++ b/src/Controller/Api/Stations/Playlists/DeleteQueueAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); diff --git a/src/Controller/Api/Stations/Playlists/ExportAction.php b/src/Controller/Api/Stations/Playlists/ExportAction.php index f9237b433..ef4131f1a 100644 --- a/src/Controller/Api/Stations/Playlists/ExportAction.php +++ b/src/Controller/Api/Stations/Playlists/ExportAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); diff --git a/src/Controller/Api/Stations/Playlists/GetOrderAction.php b/src/Controller/Api/Stations/Playlists/GetOrderAction.php index 464efa5bb..ea266eeb8 100644 --- a/src/Controller/Api/Stations/Playlists/GetOrderAction.php +++ b/src/Controller/Api/Stations/Playlists/GetOrderAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); diff --git a/src/Controller/Api/Stations/Playlists/GetQueueAction.php b/src/Controller/Api/Stations/Playlists/GetQueueAction.php index 105a08083..681b23920 100644 --- a/src/Controller/Api/Stations/Playlists/GetQueueAction.php +++ b/src/Controller/Api/Stations/Playlists/GetQueueAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); diff --git a/src/Controller/Api/Stations/Playlists/ImportAction.php b/src/Controller/Api/Stations/Playlists/ImportAction.php index b22fa43ed..19f319f52 100644 --- a/src/Controller/Api/Stations/Playlists/ImportAction.php +++ b/src/Controller/Api/Stations/Playlists/ImportAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); @@ -31,7 +33,7 @@ class ImportAction extends AbstractPlaylistsAction if (UPLOAD_ERR_OK !== $file->getError()) { return $response->withStatus(500) - ->withJson(new Entity\Api\Error(500, $file->getError())); + ->withJson(Entity\Api\Error::fromFileError($file->getError())); } $playlistFile = $file->getStream()->getContents(); diff --git a/src/Controller/Api/Stations/Playlists/PutOrderAction.php b/src/Controller/Api/Stations/Playlists/PutOrderAction.php index 7c8e37554..bd8928b17 100644 --- a/src/Controller/Api/Stations/Playlists/PutOrderAction.php +++ b/src/Controller/Api/Stations/Playlists/PutOrderAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); diff --git a/src/Controller/Api/Stations/Playlists/ReshuffleAction.php b/src/Controller/Api/Stations/Playlists/ReshuffleAction.php index 1db87e88a..b38e74fab 100644 --- a/src/Controller/Api/Stations/Playlists/ReshuffleAction.php +++ b/src/Controller/Api/Stations/Playlists/ReshuffleAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); diff --git a/src/Controller/Api/Stations/Playlists/ToggleAction.php b/src/Controller/Api/Stations/Playlists/ToggleAction.php index 727894d9b..68fb9a7bb 100644 --- a/src/Controller/Api/Stations/Playlists/ToggleAction.php +++ b/src/Controller/Api/Stations/Playlists/ToggleAction.php @@ -1,5 +1,7 @@ requireRecord($request->getStation(), $id); diff --git a/src/Controller/Api/Stations/PlaylistsController.php b/src/Controller/Api/Stations/PlaylistsController.php index faa626cab..19a115bfc 100644 --- a/src/Controller/Api/Stations/PlaylistsController.php +++ b/src/Controller/Api/Stations/PlaylistsController.php @@ -1,5 +1,7 @@ + */ class PlaylistsController extends AbstractScheduledEntityController { protected string $entityClass = Entity\StationPlaylist::class; @@ -201,33 +206,43 @@ class PlaylistsController extends AbstractScheduledEntityController $router = $request->getRouter(); $return['links'] = [ - 'toggle' => $router->fromHere('api:stations:playlist:toggle', ['id' => $record->getId()], [], !$isInternal), - 'order' => $router->fromHere('api:stations:playlist:order', ['id' => $record->getId()], [], !$isInternal), - 'reshuffle' => $router->fromHere( + 'toggle' => (string)$router->fromHere( + 'api:stations:playlist:toggle', + ['id' => $record->getId()], + [], + !$isInternal + ), + 'order' => (string)$router->fromHere( + 'api:stations:playlist:order', + ['id' => $record->getId()], + [], + !$isInternal + ), + 'reshuffle' => (string)$router->fromHere( route_name: 'api:stations:playlist:reshuffle', route_params: ['id' => $record->getId()], absolute: !$isInternal ), - 'queue' => $router->fromHere( + 'queue' => (string)$router->fromHere( route_name: 'api:stations:playlist:queue', route_params: ['id' => $record->getId()], absolute: !$isInternal ), - 'import' => $router->fromHere( + 'import' => (string)$router->fromHere( route_name: 'api:stations:playlist:import', route_params: ['id' => $record->getId()], absolute: !$isInternal ), - 'clone' => $router->fromHere( + 'clone' => (string)$router->fromHere( route_name: 'api:stations:playlist:clone', route_params: ['id' => $record->getId()], absolute: !$isInternal ), - 'self' => $router->fromHere($this->resourceRouteName, ['id' => $record->getId()], [], !$isInternal), + 'self' => (string)$router->fromHere($this->resourceRouteName, ['id' => $record->getId()], [], !$isInternal), ]; foreach (['pls', 'm3u'] as $format) { - $return['links']['export'][$format] = $router->fromHere( + $return['links']['export'][$format] = (string)$router->fromHere( route_name: 'api:stations:playlist:export', route_params: ['id' => $record->getId(), 'format' => $format], absolute: !$isInternal diff --git a/src/Controller/Api/Stations/PodcastEpisodesController.php b/src/Controller/Api/Stations/PodcastEpisodesController.php index 9e42cb36d..92cd4464a 100644 --- a/src/Controller/Api/Stations/PodcastEpisodesController.php +++ b/src/Controller/Api/Stations/PodcastEpisodesController.php @@ -17,6 +17,9 @@ use Psr\Http\Message\ResponseInterface; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\Validator\ValidatorInterface; +/** + * @extends AbstractApiCrudController + */ class PodcastEpisodesController extends AbstractApiCrudController { protected string $entityClass = Entity\PodcastEpisode::class; @@ -153,9 +156,6 @@ class PodcastEpisodesController extends AbstractApiCrudController * ) */ - /** - * @inheritDoc - */ public function listAction( ServerRequest $request, Response $response, @@ -193,7 +193,7 @@ class PodcastEpisodesController extends AbstractApiCrudController if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $return = $this->viewRecord($record, $request); @@ -208,11 +208,14 @@ class PodcastEpisodesController extends AbstractApiCrudController $station = $request->getStation(); $podcast = $this->podcastRepository->fetchPodcastForStation($station, $podcast_id); - $parsedBody = $request->getParsedBody(); + if (null === $podcast) { + throw new \RuntimeException('Podcast not found.'); + } + + $parsedBody = (array)$request->getParsedBody(); - /** @var Entity\PodcastEpisode $record */ $record = $this->editRecord( - $request->getParsedBody(), + $parsedBody, new Entity\PodcastEpisode($podcast) ); @@ -249,10 +252,10 @@ class PodcastEpisodesController extends AbstractApiCrudController if ($podcast === null) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } - $this->editRecord($request->getParsedBody(), $podcast); + $this->editRecord((array)$request->getParsedBody(), $podcast); return $response->withJson(new Entity\Api\Status(true, __('Changes saved successfully.'))); } @@ -267,7 +270,7 @@ class PodcastEpisodesController extends AbstractApiCrudController if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $fsStation = new StationFilesystems($station); @@ -279,12 +282,17 @@ class PodcastEpisodesController extends AbstractApiCrudController /** * @param Entity\Station $station * @param string $id + * + * @return Entity\PodcastEpisode|null */ protected function getRecord(Entity\Station $station, string $id): ?object { return $this->episodeRepository->fetchEpisodeForStation($station, $id); } + /** + * @inheritDoc + */ protected function viewRecord(object $record, ServerRequest $request): mixed { if (!($record instanceof Entity\PodcastEpisode)) { @@ -321,24 +329,24 @@ class PodcastEpisodesController extends AbstractApiCrudController $return->art_updated_at = $record->getArtUpdatedAt(); $return->has_custom_art = (0 !== $return->art_updated_at); - $return->art = $router->fromHere( + $return->art = (string)$router->fromHere( route_name: 'api:stations:podcast:episode:art', route_params: ['episode_id' => $record->getId() . '|' . $record->getArtUpdatedAt()], absolute: true ); $return->links = [ - 'self' => $router->fromHere( + 'self' => (string)$router->fromHere( route_name: $this->resourceRouteName, route_params: ['episode_id' => $record->getId()], absolute: !$isInternal ), - 'public' => $router->fromHere( + 'public' => (string)$router->fromHere( route_name: 'public:podcast:episode', route_params: ['episode_id' => $record->getId()], absolute: !$isInternal ), - 'download' => $router->fromHere( + 'download' => (string)$router->fromHere( route_name: 'api:stations:podcast:episode:download', route_params: ['episode_id' => $record->getId()], absolute: !$isInternal @@ -349,12 +357,12 @@ class PodcastEpisodesController extends AbstractApiCrudController $station = $request->getStation(); if ($acl->isAllowed(Acl::STATION_PODCASTS, $station)) { - $return->links['art'] = $router->fromHere( + $return->links['art'] = (string)$router->fromHere( route_name: 'api:stations:podcast:episode:art-internal', route_params: ['episode_id' => $record->getId()], absolute: !$isInternal ); - $return->links['media'] = $router->fromHere( + $return->links['media'] = (string)$router->fromHere( route_name: 'api:stations:podcast:episode:media-internal', route_params: ['episode_id' => $record->getId()], absolute: !$isInternal diff --git a/src/Controller/Api/Stations/Podcasts/Art/PostArtAction.php b/src/Controller/Api/Stations/Podcasts/Art/PostArtAction.php index 70dcb2983..2d31cda2c 100644 --- a/src/Controller/Api/Stations/Podcasts/Art/PostArtAction.php +++ b/src/Controller/Api/Stations/Podcasts/Art/PostArtAction.php @@ -30,7 +30,7 @@ class PostArtAction if (null === $podcast) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Podcast not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $podcastRepo->writePodcastArt( diff --git a/src/Controller/Api/Stations/Podcasts/Episodes/Art/DeleteArtAction.php b/src/Controller/Api/Stations/Podcasts/Episodes/Art/DeleteArtAction.php index 8263f0f1c..59ccf0a4b 100644 --- a/src/Controller/Api/Stations/Podcasts/Episodes/Art/DeleteArtAction.php +++ b/src/Controller/Api/Stations/Podcasts/Episodes/Art/DeleteArtAction.php @@ -24,7 +24,7 @@ class DeleteArtAction $episode = $episodeRepo->fetchEpisodeForStation($station, $episode_id); if ($episode === null) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Episode not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $episodeRepo->removeEpisodeArt($episode); diff --git a/src/Controller/Api/Stations/Podcasts/Episodes/Art/PostArtAction.php b/src/Controller/Api/Stations/Podcasts/Episodes/Art/PostArtAction.php index bd9776877..5eeca05f1 100644 --- a/src/Controller/Api/Stations/Podcasts/Episodes/Art/PostArtAction.php +++ b/src/Controller/Api/Stations/Podcasts/Episodes/Art/PostArtAction.php @@ -30,7 +30,7 @@ class PostArtAction if (null === $episode) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Episode not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $episodeRepo->writeEpisodeArt( diff --git a/src/Controller/Api/Stations/Podcasts/Episodes/Media/DeleteMediaAction.php b/src/Controller/Api/Stations/Podcasts/Episodes/Media/DeleteMediaAction.php index 3d143fc67..1365f06e9 100644 --- a/src/Controller/Api/Stations/Podcasts/Episodes/Media/DeleteMediaAction.php +++ b/src/Controller/Api/Stations/Podcasts/Episodes/Media/DeleteMediaAction.php @@ -23,7 +23,7 @@ class DeleteMediaAction if (!($episode instanceof Entity\PodcastEpisode)) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, 'Media file not found.')); + ->withJson(Entity\Api\Error::notFound()); } $podcastMedia = $episode->getMedia(); diff --git a/src/Controller/Api/Stations/Podcasts/Episodes/Media/GetMediaAction.php b/src/Controller/Api/Stations/Podcasts/Episodes/Media/GetMediaAction.php index 4df891cc7..669c660b7 100644 --- a/src/Controller/Api/Stations/Podcasts/Episodes/Media/GetMediaAction.php +++ b/src/Controller/Api/Stations/Podcasts/Episodes/Media/GetMediaAction.php @@ -42,6 +42,6 @@ class GetMediaAction } return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, 'Media file not found.')); + ->withJson(Entity\Api\Error::notFound()); } } diff --git a/src/Controller/Api/Stations/Podcasts/Episodes/Media/PostMediaAction.php b/src/Controller/Api/Stations/Podcasts/Episodes/Media/PostMediaAction.php index aade4969a..879267eee 100644 --- a/src/Controller/Api/Stations/Podcasts/Episodes/Media/PostMediaAction.php +++ b/src/Controller/Api/Stations/Podcasts/Episodes/Media/PostMediaAction.php @@ -33,7 +33,7 @@ class PostMediaAction if (null === $episode) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Episode not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $fsStation = new StationFilesystems($station); diff --git a/src/Controller/Api/Stations/PodcastsController.php b/src/Controller/Api/Stations/PodcastsController.php index 6ba95b5b5..e586fb9b7 100644 --- a/src/Controller/Api/Stations/PodcastsController.php +++ b/src/Controller/Api/Stations/PodcastsController.php @@ -19,6 +19,9 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Validator\Validator\ValidatorInterface; +/** + * @extends AbstractApiCrudController + */ class PodcastsController extends AbstractApiCrudController { protected string $entityClass = Entity\Podcast::class; @@ -152,7 +155,7 @@ class PodcastsController extends AbstractApiCrudController if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $return = $this->viewRecord($record, $request); @@ -163,11 +166,11 @@ class PodcastsController extends AbstractApiCrudController { $station = $request->getStation(); - $parsedBody = $request->getParsedBody(); + $parsedBody = (array)$request->getParsedBody(); /** @var Entity\Podcast $record */ $record = $this->editRecord( - $request->getParsedBody(), + $parsedBody, new Entity\Podcast($station->getPodcastsStorageLocation()) ); @@ -194,10 +197,10 @@ class PodcastsController extends AbstractApiCrudController if ($podcast === null) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } - $this->editRecord($request->getParsedBody(), $podcast); + $this->editRecord((array)$request->getParsedBody(), $podcast); return $response->withJson(new Entity\Api\Status(true, __('Changes saved successfully.'))); } @@ -212,7 +215,7 @@ class PodcastsController extends AbstractApiCrudController if (null === $record) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $fsStation = new StationFilesystems($station); @@ -224,12 +227,20 @@ class PodcastsController extends AbstractApiCrudController /** * @param Entity\Station $station * @param string $id + * + * @return Entity\Podcast|null */ protected function getRecord(Entity\Station $station, string $id): ?object { - return $this->podcastRepository->fetchPodcastForStation($station, $id); + $record = $this->podcastRepository->fetchPodcastForStation($station, $id); + return $record; } + /** + * @param Entity\Podcast $record + * @param ServerRequest $request + * + */ protected function viewRecord(object $record, ServerRequest $request): mixed { if (!($record instanceof Entity\Podcast)) { @@ -242,7 +253,7 @@ class PodcastsController extends AbstractApiCrudController $return = new Entity\Api\Podcast(); $return->id = $record->getId(); - $return->storage_location_id = $record->getStorageLocation()?->getId(); + $return->storage_location_id = $record->getStorageLocation()->getId(); $return->title = $record->getTitle(); $return->link = $record->getLink(); $return->description = $record->getDescription(); @@ -261,29 +272,29 @@ class PodcastsController extends AbstractApiCrudController $return->episodes = $episodes; $return->has_custom_art = (0 !== $record->getArtUpdatedAt()); - $return->art = $router->fromHere( + $return->art = (string)$router->fromHere( route_name: 'api:stations:podcast:art', route_params: ['podcast_id' => $record->getId() . '|' . $record->getArtUpdatedAt()], absolute: true ); $return->links = [ - 'self' => $router->fromHere( + 'self' => (string)$router->fromHere( route_name: $this->resourceRouteName, route_params: ['podcast_id' => $record->getId()], absolute: !$isInternal ), - 'episodes' => $router->fromHere( + 'episodes' => (string)$router->fromHere( route_name: 'api:stations:podcast:episodes', route_params: ['podcast_id' => $record->getId()], absolute: !$isInternal ), - 'public_episodes' => $router->fromHere( + 'public_episodes' => (string)$router->fromHere( route_name: 'public:podcast:episodes', route_params: ['podcast_id' => $record->getId()], absolute: !$isInternal ), - 'public_feed' => $router->fromHere( + 'public_feed' => (string)$router->fromHere( route_name: 'public:podcast:feed', route_params: ['podcast_id' => $record->getId()], absolute: !$isInternal @@ -293,27 +304,34 @@ class PodcastsController extends AbstractApiCrudController $acl = $request->getAcl(); if ($acl->isAllowed(Acl::STATION_PODCASTS, $station)) { - $return->links['art'] = $router->fromHere( + $return->links['art'] = (string)$router->fromHere( route_name: 'api:stations:podcast:art-internal', route_params: ['podcast_id' => $record->getId()], absolute: !$isInternal ); - $return->links['episode_new_art'] = $router->fromHere( + $return->links['episode_new_art'] = (string)$router->fromHere( route_name: 'api:stations:podcast:episodes:new-art', route_params: ['podcast_id' => $record->getId()], absolute: !$isInternal ); - $return->links['episode_new_media'] = $router->fromHere( - route_name: 'api:stations:podcast:episodes:new-media', + $return->links['episode_new_media'] = (string)$router->fromHere( + route_name: 'api:stations:podcast:episodes:new-media', route_params: ['podcast_id' => $record->getId()], - absolute: !$isInternal + absolute: !$isInternal ); } return $return; } + /** + * @param mixed[] $data + * @param Entity\Podcast|null $record + * @param array $context + * + * @return Entity\Podcast + */ protected function fromArray($data, $record = null, array $context = []): object { return parent::fromArray( diff --git a/src/Controller/Api/Stations/ProfileController.php b/src/Controller/Api/Stations/ProfileController.php index 49633eea5..db14983c4 100644 --- a/src/Controller/Api/Stations/ProfileController.php +++ b/src/Controller/Api/Stations/ProfileController.php @@ -1,5 +1,7 @@ + */ class QueueController extends AbstractStationApiCrudController { protected string $entityClass = Entity\StationQueue::class; @@ -124,7 +129,7 @@ class QueueController extends AbstractStationApiCrudController $apiResponse->log = $record->getLog(); $apiResponse->links = [ - 'self' => $router->fromHere($this->resourceRouteName, ['id' => $record->getId()], [], !$isInternal), + 'self' => (string)$router->fromHere($this->resourceRouteName, ['id' => $record->getId()], [], !$isInternal), ]; return $apiResponse; diff --git a/src/Controller/Api/Stations/RemotesController.php b/src/Controller/Api/Stations/RemotesController.php index fa4bebb67..3c018bd5e 100644 --- a/src/Controller/Api/Stations/RemotesController.php +++ b/src/Controller/Api/Stations/RemotesController.php @@ -1,12 +1,16 @@ + */ class RemotesController extends AbstractStationApiCrudController { protected string $entityClass = Entity\StationRemote::class; @@ -103,7 +107,7 @@ class RemotesController extends AbstractStationApiCrudController { $record = parent::getRecord($station, $id); - if ($record instanceof StationRemote && !$record->isEditable()) { + if ($record instanceof Entity\StationRemote && !$record->isEditable()) { throw new PermissionDeniedException('This record cannot be edited.'); } diff --git a/src/Controller/Api/Stations/Reports/Overview/BestAndWorstAction.php b/src/Controller/Api/Stations/Reports/Overview/BestAndWorstAction.php index c520be08d..7f1964ae6 100644 --- a/src/Controller/Api/Stations/Reports/Overview/BestAndWorstAction.php +++ b/src/Controller/Api/Stations/Reports/Overview/BestAndWorstAction.php @@ -1,5 +1,7 @@ t = $statTime->getTimestamp() * 1000; - $avg_row->y = round($stat['number_avg'], 2); + $avg_row->y = round((float)$stat['number_avg'], 2); $daily_averages[] = $avg_row; $row_date = $statTime->format('Y-m-d'); diff --git a/src/Controller/Api/Stations/Reports/Overview/MostPlayedAction.php b/src/Controller/Api/Stations/Reports/Overview/MostPlayedAction.php index 9f94846c7..fca189744 100644 --- a/src/Controller/Api/Stations/Reports/Overview/MostPlayedAction.php +++ b/src/Controller/Api/Stations/Reports/Overview/MostPlayedAction.php @@ -1,5 +1,7 @@ + */ class BroadcastsController extends AbstractApiCrudController { protected string $entityClass = Entity\StationStreamerBroadcast::class; @@ -19,13 +24,11 @@ class BroadcastsController extends AbstractApiCrudController /** * @param ServerRequest $request * @param Response $response - * @param int|string $station_id * @param int|null $id */ public function listAction( ServerRequest $request, Response $response, - int|string $station_id, ?int $id = null ): ResponseInterface { $station = $request->getStation(); @@ -35,7 +38,7 @@ class BroadcastsController extends AbstractApiCrudController if (null === $streamer) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $query = $this->em->createQuery( @@ -68,7 +71,6 @@ class BroadcastsController extends AbstractApiCrudController $paginator->setPostprocessor( function ($row) use ($id, $is_bootgrid, $router, $fsRecordings) { - /** @var Entity\StationStreamerBroadcast $row */ $return = $this->toArray($row); unset($return['recordingPath']); @@ -95,13 +97,13 @@ class BroadcastsController extends AbstractApiCrudController 'path' => $recordingPath, 'size' => $fsRecordings->fileSize($recordingPath), 'links' => [ - 'download' => $router->fromHere( + 'download' => (string)$router->fromHere( 'api:stations:streamer:broadcast:download', $routeParams, [], true ), - 'delete' => $router->fromHere( + 'delete' => (string)$router->fromHere( 'api:stations:streamer:broadcast:delete', $routeParams, [], @@ -127,15 +129,11 @@ class BroadcastsController extends AbstractApiCrudController /** * @param ServerRequest $request * @param Response $response - * @param int|string $station_id - * @param int $id * @param int $broadcast_id */ public function downloadAction( ServerRequest $request, Response $response, - int|string $station_id, - int $id, int $broadcast_id ): ResponseInterface { $station = $request->getStation(); @@ -143,7 +141,7 @@ class BroadcastsController extends AbstractApiCrudController if (null === $broadcast) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $recordingPath = $broadcast->getRecordingPath(); @@ -167,16 +165,14 @@ class BroadcastsController extends AbstractApiCrudController public function deleteAction( ServerRequest $request, Response $response, - $station_id, - $id, - $broadcast_id + int $broadcast_id ): ResponseInterface { $station = $request->getStation(); $broadcast = $this->getRecord($station, $broadcast_id); if (null === $broadcast) { return $response->withStatus(404) - ->withJson(new Entity\Api\Error(404, __('Record not found!'))); + ->withJson(Entity\Api\Error::notFound()); } $recordingPath = $broadcast->getRecordingPath(); diff --git a/src/Controller/Api/Stations/StreamersController.php b/src/Controller/Api/Stations/StreamersController.php index 88d5aaab2..8e2675930 100644 --- a/src/Controller/Api/Stations/StreamersController.php +++ b/src/Controller/Api/Stations/StreamersController.php @@ -1,5 +1,7 @@ + */ class StreamersController extends AbstractScheduledEntityController { protected string $entityClass = Entity\StationStreamer::class; @@ -149,7 +154,7 @@ class StreamersController extends AbstractScheduledEntityController } /** - * @param object $record + * @param Entity\StationStreamer $record * @param ServerRequest $request * * @return mixed[] @@ -159,11 +164,10 @@ class StreamersController extends AbstractScheduledEntityController $return = parent::viewRecord($record, $request); $isInternal = ('true' === $request->getParam('internal', 'false')); - $return['links']['broadcasts'] = $request->getRouter()->fromHere( - 'api:stations:streamer:broadcasts', - ['id' => $record->getId()], - [], - !$isInternal + $return['links']['broadcasts'] = (string)$request->getRouter()->fromHere( + route_name: 'api:stations:streamer:broadcasts', + route_params: ['id' => $record->getId()], + absolute: !$isInternal ); return $return; diff --git a/src/Controller/Api/Stations/UpdateMetadataController.php b/src/Controller/Api/Stations/UpdateMetadataController.php index 99c91d53a..08e116be2 100644 --- a/src/Controller/Api/Stations/UpdateMetadataController.php +++ b/src/Controller/Api/Stations/UpdateMetadataController.php @@ -1,5 +1,7 @@ withCacheLifetime(Response::CACHE_ONE_YEAR); diff --git a/src/Controller/Api/Stations/WebhooksController.php b/src/Controller/Api/Stations/WebhooksController.php index d8f169282..4953ac11a 100644 --- a/src/Controller/Api/Stations/WebhooksController.php +++ b/src/Controller/Api/Stations/WebhooksController.php @@ -1,10 +1,15 @@ + */ class WebhooksController extends AbstractStationApiCrudController { protected string $entityClass = Entity\StationWebhook::class; diff --git a/src/Controller/Frontend/Account/EndMasqueradeAction.php b/src/Controller/Frontend/Account/EndMasqueradeAction.php index c31d6d4a6..163710998 100644 --- a/src/Controller/Frontend/Account/EndMasqueradeAction.php +++ b/src/Controller/Frontend/Account/EndMasqueradeAction.php @@ -1,5 +1,7 @@ getAuth(); $auth->endMasquerade(); - return $response->withRedirect($request->getRouter()->named('admin:users:index')); + return $response->withRedirect((string)$request->getRouter()->named('admin:users:index')); } } diff --git a/src/Controller/Frontend/Account/ForgotPasswordAction.php b/src/Controller/Frontend/Account/ForgotPasswordAction.php index 3940d343f..fdf947a8d 100644 --- a/src/Controller/Frontend/Account/ForgotPasswordAction.php +++ b/src/Controller/Frontend/Account/ForgotPasswordAction.php @@ -1,5 +1,7 @@ withRedirect($request->getRouter()->named('account:login')); + return $response->withRedirect((string)$request->getRouter()->named('account:login')); } return $view->renderToResponse($response, 'frontend/account/forgot'); diff --git a/src/Controller/Frontend/Account/LoginAction.php b/src/Controller/Frontend/Account/LoginAction.php index 6a2f96312..3b470aebc 100644 --- a/src/Controller/Frontend/Account/LoginAction.php +++ b/src/Controller/Frontend/Account/LoginAction.php @@ -1,5 +1,7 @@ getSingleScalarResult(); if (0 === $num_users) { - return $response->withRedirect($request->getRouter()->named('setup:index')); + return $response->withRedirect((string)$request->getRouter()->named('setup:index')); } } if ($auth->isLoggedIn()) { - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } $flash = $request->getFlash(); @@ -82,7 +84,7 @@ class LoginAction // Redirect for 2FA. if (!$auth->isLoginComplete()) { - return $response->withRedirect($request->getRouter()->named('account:login:2fa')); + return $response->withRedirect((string)$request->getRouter()->named('account:login:2fa')); } // Redirect to complete setup if it's not completed yet. @@ -95,7 +97,7 @@ class LoginAction ), Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->named('setup:index')); + return $response->withRedirect((string)$request->getRouter()->named('setup:index')); } $flash->addMessage( @@ -108,7 +110,7 @@ class LoginAction return $response->withRedirect($referrer); } - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } $flash->addMessage( @@ -116,7 +118,7 @@ class LoginAction Flash::ERROR ); - return $response->withRedirect($request->getUri()); + return $response->withRedirect((string)$request->getUri()); } return $request->getView()->renderToResponse($response, 'frontend/account/login'); diff --git a/src/Controller/Frontend/Account/LogoutAction.php b/src/Controller/Frontend/Account/LogoutAction.php index de6be5f38..4311487f2 100644 --- a/src/Controller/Frontend/Account/LogoutAction.php +++ b/src/Controller/Frontend/Account/LogoutAction.php @@ -1,5 +1,7 @@ getAuth(); $auth->logout(); - return $response->withRedirect($request->getRouter()->named('account:login')); + return $response->withRedirect((string)$request->getRouter()->named('account:login')); } } diff --git a/src/Controller/Frontend/Account/RecoverAction.php b/src/Controller/Frontend/Account/RecoverAction.php index 823f93812..529787000 100644 --- a/src/Controller/Frontend/Account/RecoverAction.php +++ b/src/Controller/Frontend/Account/RecoverAction.php @@ -1,5 +1,7 @@ withRedirect($request->getRouter()->named('account:login')); + return $response->withRedirect((string)$request->getRouter()->named('account:login')); } if ($request->isPost()) { @@ -54,7 +56,7 @@ class RecoverAction Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } return $request->getView()->renderToResponse($response, 'frontend/account/recover'); diff --git a/src/Controller/Frontend/Account/TwoFactorAction.php b/src/Controller/Frontend/Account/TwoFactorAction.php index 4e1bb01a9..d47c820c1 100644 --- a/src/Controller/Frontend/Account/TwoFactorAction.php +++ b/src/Controller/Frontend/Account/TwoFactorAction.php @@ -1,7 +1,10 @@ getParam('otp'); if ($auth->verifyTwoFactor($otp)) { + /** @var User $user */ $user = $auth->getUser(); $flash->addMessage( @@ -32,7 +36,7 @@ class TwoFactorAction return $response->withRedirect($referrer); } - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } $flash->addMessage( @@ -40,7 +44,7 @@ class TwoFactorAction Flash::ERROR ); - return $response->withRedirect($request->getUri()); + return $response->withRedirect((string)$request->getUri()); } return $request->getView()->renderToResponse($response, 'frontend/account/two_factor'); diff --git a/src/Controller/Frontend/ApiKeysController.php b/src/Controller/Frontend/ApiKeysController.php index 2f5dffcd7..988e1617d 100644 --- a/src/Controller/Frontend/ApiKeysController.php +++ b/src/Controller/Frontend/ApiKeysController.php @@ -1,5 +1,7 @@ getUser(); $view = $request->getView(); @@ -57,7 +59,7 @@ class ApiKeysController $record = null; } - if ($_POST && $form->isValid($_POST)) { + if ($form->isValid($request)) { $data = $form->getValues(); $key = null; @@ -80,18 +82,26 @@ class ApiKeysController } $request->getFlash()->addMessage(__('API Key updated.'), 'green'); - return $response->withRedirect($request->getRouter()->named('api_keys:index')); + return $response->withRedirect((string)$request->getRouter()->named('api_keys:index')); } - return $view->renderToResponse($response, 'system/form_page', [ - 'form' => $form, - 'render_mode' => 'edit', - 'title' => $id ? __('Edit API Key') : __('Add API Key'), - ]); + return $view->renderToResponse( + $response, + 'system/form_page', + [ + 'form' => $form, + 'render_mode' => 'edit', + 'title' => $id ? __('Edit API Key') : __('Add API Key'), + ] + ); } - public function deleteAction(ServerRequest $request, Response $response, $id, $csrf): ResponseInterface - { + public function deleteAction( + ServerRequest $request, + Response $response, + string $id, + string $csrf + ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); /** @var Entity\User $user */ @@ -108,6 +118,6 @@ class ApiKeysController $request->getFlash()->addMessage(__('API Key deleted.'), 'green'); - return $response->withRedirect($request->getRouter()->named('api_keys:index')); + return $response->withRedirect((string)$request->getRouter()->named('api_keys:index')); } } diff --git a/src/Controller/Frontend/DashboardAction.php b/src/Controller/Frontend/DashboardAction.php index 75de667bd..d02f3a4ff 100644 --- a/src/Controller/Frontend/DashboardAction.php +++ b/src/Controller/Frontend/DashboardAction.php @@ -1,5 +1,7 @@ readSettings(); if (!$settings->isSetupComplete()) { - return $response->withRedirect($request->getRouter()->named('setup:index')); + return $response->withRedirect((string)$request->getRouter()->named('setup:index')); } // Redirect to login screen if the user isn't logged in. @@ -25,16 +27,16 @@ class IndexAction if (!($user instanceof Entity\User)) { // Redirect to a custom homepage URL if specified in settings. - $homepage_redirect = trim($settings->getHomepageRedirectUrl()); + $homepage_redirect = trim($settings->getHomepageRedirectUrl() ?? ''); if (!empty($homepage_redirect)) { return $response->withRedirect($homepage_redirect, 302); } - return $response->withRedirect($request->getRouter()->named('account:login')); + return $response->withRedirect((string)$request->getRouter()->named('account:login')); } // Redirect to dashboard if no other custom redirection rules exist. - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } } diff --git a/src/Controller/Frontend/PWA/AppManifestAction.php b/src/Controller/Frontend/PWA/AppManifestAction.php index 5351c8ad1..657faba43 100644 --- a/src/Controller/Frontend/PWA/AppManifestAction.php +++ b/src/Controller/Frontend/PWA/AppManifestAction.php @@ -1,5 +1,7 @@ getFlash()->addMessage(__('Two-factor authentication disabled.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('profile:index')); + return $response->withRedirect((string)$request->getRouter()->named('profile:index')); } } diff --git a/src/Controller/Frontend/Profile/EditAction.php b/src/Controller/Frontend/Profile/EditAction.php index 087547db9..f48e8ec8d 100644 --- a/src/Controller/Frontend/Profile/EditAction.php +++ b/src/Controller/Frontend/Profile/EditAction.php @@ -1,5 +1,7 @@ process($request)) { $request->getFlash()->addMessage(__('Profile saved!'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('profile:index')); + return $response->withRedirect((string)$request->getRouter()->named('profile:index')); } return $request->getView()->renderToResponse( diff --git a/src/Controller/Frontend/Profile/EnableTwoFactorAction.php b/src/Controller/Frontend/Profile/EnableTwoFactorAction.php index 08d4a5e8a..d67bd28fe 100644 --- a/src/Controller/Frontend/Profile/EnableTwoFactorAction.php +++ b/src/Controller/Frontend/Profile/EnableTwoFactorAction.php @@ -1,5 +1,7 @@ isPost() && $form->isValid($request->getParsedBody())) { + if ($form->isValid($request)) { $user->setTwoFactorSecret($totp->getProvisioningUri()); $em->persist($user); @@ -56,7 +58,7 @@ class EnableTwoFactorAction $request->getFlash()->addMessage(__('Two-factor authentication enabled.'), Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->named('profile:index')); + return $response->withRedirect((string)$request->getRouter()->named('profile:index')); } // Further customize TOTP code (with metadata that won't be stored in the DB) diff --git a/src/Controller/Frontend/Profile/IndexAction.php b/src/Controller/Frontend/Profile/IndexAction.php index 445de37c2..12f46ded0 100644 --- a/src/Controller/Frontend/Profile/IndexAction.php +++ b/src/Controller/Frontend/Profile/IndexAction.php @@ -1,5 +1,7 @@ getStation(); diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodeController.php b/src/Controller/Frontend/PublicPages/PodcastEpisodeController.php index 896a384c9..73f1e80fd 100644 --- a/src/Controller/Frontend/PublicPages/PodcastEpisodeController.php +++ b/src/Controller/Frontend/PublicPages/PodcastEpisodeController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Controller\Frontend\PublicPages; +use App\Entity\PodcastEpisode; use App\Entity\Repository\PodcastEpisodeRepository; use App\Entity\Repository\PodcastRepository; use App\Exception\PodcastNotFoundException; @@ -50,12 +51,12 @@ class PodcastEpisodeController ] ); - if (!$episode->isPublished()) { + if (!($episode instanceof PodcastEpisode) || !$episode->isPublished()) { $request->getFlash()->addMessage(__('Episode not found.'), Flash::ERROR); return $response->withRedirect($podcastEpisodesLink); } - $feedLink = $router->named( + $feedLink = (string)$router->named( 'public:podcast:feed', [ 'station_id' => $station->getId(), diff --git a/src/Controller/Frontend/PublicPages/PodcastEpisodesController.php b/src/Controller/Frontend/PublicPages/PodcastEpisodesController.php index 88ef44adc..4fb52093b 100644 --- a/src/Controller/Frontend/PublicPages/PodcastEpisodesController.php +++ b/src/Controller/Frontend/PublicPages/PodcastEpisodesController.php @@ -52,7 +52,7 @@ class PodcastEpisodesController } ); - $podcastsLink = $router->fromHere( + $podcastsLink = (string)$router->fromHere( 'public:podcasts', [ 'station_id' => $station->getId(), @@ -64,7 +64,7 @@ class PodcastEpisodesController return $response->withRedirect($podcastsLink); } - $feedLink = $router->named( + $feedLink = (string)$router->named( 'public:podcast:feed', [ 'station_id' => $station->getId(), diff --git a/src/Controller/Frontend/PublicPages/PodcastFeedController.php b/src/Controller/Frontend/PublicPages/PodcastFeedController.php index de8cf1b12..91c832999 100644 --- a/src/Controller/Frontend/PublicPages/PodcastFeedController.php +++ b/src/Controller/Frontend/PublicPages/PodcastFeedController.php @@ -132,7 +132,7 @@ class PodcastFeedController $channelLink = $podcast->getLink(); if (empty($channelLink)) { - $channelLink = $serverRequest->getRouter()->fromHere( + $channelLink = (string)$serverRequest->getRouter()->fromHere( route_name: 'public:podcast:episodes', absolute: true ); @@ -217,11 +217,11 @@ class PodcastFeedController $this->stationRepository->getDefaultAlbumArtUrl($station) ); - if ($podcastsFilesystem->fileExists(Podcast::getArtPath($podcast->getId()))) { - $podcastArtworkSrc = $this->router->fromHere( - route_name: 'api:stations:podcast:art', - route_params: ['podcast_id' => $podcast->getId() . '|' . $podcast->getArtUpdatedAt()], - absolute: true + if ($podcastsFilesystem->fileExists(Podcast::getArtPath($podcast->getIdRequired()))) { + $podcastArtworkSrc = (string)$this->router->fromHere( + route_name: 'api:stations:podcast:art', + route_params: ['podcast_id' => $podcast->getIdRequired() . '|' . $podcast->getArtUpdatedAt()], + absolute: true ); } @@ -256,7 +256,7 @@ class PodcastFeedController $episodeLink = $episode->getLink(); if (empty($episodeLink)) { - $episodeLink = $this->router->fromHere( + $episodeLink = (string)$this->router->fromHere( route_name: 'public:podcast:episode', route_params: ['episode_id' => $episode->getId()], absolute: true @@ -299,17 +299,19 @@ class PodcastFeedController ): RssEnclosure { $rssEnclosure = new RssEnclosure(); - $podcastMediaPlayUrl = $this->router->fromHere( - route_name: 'api:stations:podcast:episode:download', + $podcastMediaPlayUrl = (string)$this->router->fromHere( + route_name: 'api:stations:podcast:episode:download', route_params: ['episode_id' => $episode->getId()], - absolute: true + absolute: true ); $rssEnclosure->setUrl($podcastMediaPlayUrl); $podcastMedia = $episode->getMedia(); - $rssEnclosure->setType($podcastMedia->getMimeType()); - $rssEnclosure->setLength($podcastMedia->getLength()); + if (null !== $podcastMedia) { + $rssEnclosure->setType($podcastMedia->getMimeType()); + $rssEnclosure->setLength($podcastMedia->getLength()); + } return $rssEnclosure; } @@ -323,11 +325,11 @@ class PodcastFeedController $this->stationRepository->getDefaultAlbumArtUrl($station) ); - if ($podcastsFilesystem->fileExists(PodcastEpisode::getArtPath($episode->getId()))) { - $episodeArtworkSrc = $this->router->fromHere( - route_name: 'api:stations:podcast:episode:art', + if ($podcastsFilesystem->fileExists(PodcastEpisode::getArtPath($episode->getIdRequired()))) { + $episodeArtworkSrc = (string)$this->router->fromHere( + route_name: 'api:stations:podcast:episode:art', route_params: ['episode_id' => $episode->getId() . '|' . $episode->getArtUpdatedAt()], - absolute: true + absolute: true ); } diff --git a/src/Controller/Frontend/PublicPages/RequestsAction.php b/src/Controller/Frontend/PublicPages/RequestsAction.php index 085f036eb..c271c7dff 100644 --- a/src/Controller/Frontend/PublicPages/RequestsAction.php +++ b/src/Controller/Frontend/PublicPages/RequestsAction.php @@ -1,5 +1,7 @@ withHeader('X-Frame-Options', '*'); diff --git a/src/Controller/Frontend/SetupController.php b/src/Controller/Frontend/SetupController.php index 512e80094..a9edb923f 100644 --- a/src/Controller/Frontend/SetupController.php +++ b/src/Controller/Frontend/SetupController.php @@ -1,5 +1,7 @@ getSetupStep($request); - return $response->withRedirect($request->getRouter()->named('setup:' . $current_step)); + return $response->withRedirect((string)$request->getRouter()->named('setup:' . $current_step)); } /** @@ -89,7 +91,7 @@ class SetupController { $request->getFlash()->addMessage('' . __('Setup has already been completed!') . '', Flash::ERROR); - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } /** @@ -101,7 +103,7 @@ class SetupController // Verify current step. $current_step = $this->getSetupStep($request); if ($current_step !== 'register' && $this->environment->isProduction()) { - return $response->withRedirect($request->getRouter()->named('setup:' . $current_step)); + return $response->withRedirect((string)$request->getRouter()->named('setup:' . $current_step)); } // Create first account form. @@ -137,7 +139,7 @@ class SetupController $acl = $request->getAcl(); $acl->reload(); - return $response->withRedirect($request->getRouter()->named('setup:index')); + return $response->withRedirect((string)$request->getRouter()->named('setup:index')); } return $request->getView() @@ -158,11 +160,11 @@ class SetupController // Verify current step. $current_step = $this->getSetupStep($request); if ($current_step !== 'station' && $this->environment->isProduction()) { - return $response->withRedirect($request->getRouter()->named('setup:' . $current_step)); + return $response->withRedirect((string)$request->getRouter()->named('setup:' . $current_step)); } if (false !== $stationForm->process($request)) { - return $response->withRedirect($request->getRouter()->named('setup:settings')); + return $response->withRedirect((string)$request->getRouter()->named('setup:settings')); } return $request->getView()->renderToResponse( @@ -188,7 +190,7 @@ class SetupController // Verify current step. $current_step = $this->getSetupStep($request); if ($current_step !== 'settings' && $this->environment->isProduction()) { - return $response->withRedirect($request->getRouter()->named('setup:' . $current_step)); + return $response->withRedirect((string)$request->getRouter()->named('setup:' . $current_step)); } if ($settingsForm->process($request)) { @@ -206,7 +208,7 @@ class SetupController Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->named('dashboard')); + return $response->withRedirect((string)$request->getRouter()->named('dashboard')); } return $request->getView()->renderToResponse( diff --git a/src/Controller/Stations/AbstractStationCrudController.php b/src/Controller/Stations/AbstractStationCrudController.php index d865f9efd..2fec87b14 100644 --- a/src/Controller/Stations/AbstractStationCrudController.php +++ b/src/Controller/Stations/AbstractStationCrudController.php @@ -1,5 +1,7 @@ getStation(); $this->form->setStation($station); @@ -49,7 +51,7 @@ abstract class AbstractStationCrudController * @param Station $station * @param string|int|null $id */ - protected function getRecord(Station $station, $id = null): ?object + protected function getRecord(Station $station, string|int $id = null): ?object { if (null === $id) { return null; diff --git a/src/Controller/Stations/AutomationController.php b/src/Controller/Stations/AutomationController.php index be53bdc83..a5fd4913b 100644 --- a/src/Controller/Stations/AutomationController.php +++ b/src/Controller/Stations/AutomationController.php @@ -1,5 +1,7 @@ form_config); $form->populate($automation_settings); - if (!empty($_POST) && $form->isValid($_POST)) { + if ($form->isValid($request)) { $data = $form->getValues(); $station->setAutomationSettings($data); @@ -43,7 +45,7 @@ class AutomationController $request->getFlash()->addMessage(__('Changes saved.'), Flash::SUCCESS); - return $response->withRedirect($request->getUri()); + return $response->withRedirect((string)$request->getUri()); } return $request->getView()->renderToResponse($response, 'stations/automation/index', [ @@ -66,6 +68,6 @@ class AutomationController ); } - return $response->withRedirect($request->getRouter()->fromHere('stations:automation:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:automation:index')); } } diff --git a/src/Controller/Stations/EditLiquidsoapConfigAction.php b/src/Controller/Stations/EditLiquidsoapConfigAction.php index 024c175e0..32d48cc05 100644 --- a/src/Controller/Stations/EditLiquidsoapConfigAction.php +++ b/src/Controller/Stations/EditLiquidsoapConfigAction.php @@ -1,5 +1,7 @@ getBackendConfig(); $form = new Form($formConfig, ['backend_config' => $backendConfig->toArray()]); - if ($request->isPost() && $form->isValid($request->getParsedBody())) { + if ($form->isValid($request)) { $data = $form->getValues(); foreach ($data['backend_config'] as $configKey => $configValue) { diff --git a/src/Controller/Stations/FilesAction.php b/src/Controller/Stations/FilesAction.php index 0917ab409..993bd7659 100644 --- a/src/Controller/Stations/FilesAction.php +++ b/src/Controller/Stations/FilesAction.php @@ -1,5 +1,7 @@ getStation(); $log_areas = $this->getStationLogs($station); @@ -28,8 +30,8 @@ class LogsController extends AbstractLogViewerController throw new Exception('Invalid log file specified.'); } - $log = $log_areas[$log]; - return $this->view($request, $response, $log['path'], $log['tail'] ?? true); + $logArea = $log_areas[$log]; + return $this->view($request, $response, $logArea['path'], $logArea['tail'] ?? true); } protected function processLog( diff --git a/src/Controller/Stations/PlaylistsAction.php b/src/Controller/Stations/PlaylistsAction.php index 174cfa411..e256fb6a9 100644 --- a/src/Controller/Stations/PlaylistsAction.php +++ b/src/Controller/Stations/PlaylistsAction.php @@ -1,5 +1,7 @@ factory->make(StationForm::class); if (false !== $stationForm->process($request, $station)) { - return $response->withRedirect($request->getRouter()->fromHere('stations:profile:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:profile:index')); } return $request->getView()->renderToResponse( @@ -87,8 +89,8 @@ class ProfileController public function toggleAction( ServerRequest $request, Response $response, - $feature, - $csrf + string $feature, + string $csrf ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); @@ -111,6 +113,6 @@ class ProfileController $this->em->persist($station); $this->em->flush(); - return $response->withRedirect($request->getRouter()->fromHere('stations:profile:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:profile:index')); } } diff --git a/src/Controller/Stations/QueueAction.php b/src/Controller/Stations/QueueAction.php index 2bf8a7979..82eeef637 100644 --- a/src/Controller/Stations/QueueAction.php +++ b/src/Controller/Stations/QueueAction.php @@ -1,5 +1,7 @@ doEdit($request, $id)) { $request->getFlash()->addMessage( '' . ($id ? __('Remote Relay updated.') : __('Remote Relay added.')) . '', Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->fromHere('stations:remotes:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:remotes:index')); } - return $request->getView()->renderToResponse($response, 'stations/remotes/edit', [ - 'form' => $this->form, - 'render_mode' => 'edit', - 'title' => $id ? __('Edit Remote Relay') : __('Add Remote Relay'), - ]); + return $request->getView()->renderToResponse( + $response, + 'stations/remotes/edit', + [ + 'form' => $this->form, + 'render_mode' => 'edit', + 'title' => $id ? __('Edit Remote Relay') : __('Add Remote Relay'), + ] + ); } public function deleteAction( ServerRequest $request, Response $response, - $id, - $csrf + int $id, + string $csrf ): ResponseInterface { $this->doDelete($request, $id, $csrf); $request->getFlash()->addMessage('' . __('Remote Relay deleted.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('stations:remotes:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:remotes:index')); } - protected function getRecord(Station $station, $id = null): ?object + protected function getRecord(Station $station, int|string|null $id = null): ?object { $record = parent::getRecord($station, $id); diff --git a/src/Controller/Stations/Reports/ListenersController.php b/src/Controller/Stations/Reports/ListenersController.php index 3f2244fe2..7b1e585f5 100644 --- a/src/Controller/Stations/Reports/ListenersController.php +++ b/src/Controller/Stations/Reports/ListenersController.php @@ -1,5 +1,7 @@ getStation(); diff --git a/src/Controller/Stations/Reports/RequestsController.php b/src/Controller/Stations/Reports/RequestsController.php index 21bec9017..d8f1edd9f 100644 --- a/src/Controller/Stations/Reports/RequestsController.php +++ b/src/Controller/Stations/Reports/RequestsController.php @@ -1,5 +1,7 @@ getCsrf()->verify($csrf, $this->csrf_namespace); @@ -62,13 +64,13 @@ class RequestsController $request->getFlash()->addMessage('Request deleted!', Flash::SUCCESS); } - return $response->withRedirect($request->getRouter()->fromHere('stations:reports:requests')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:reports:requests')); } public function clearAction( ServerRequest $request, Response $response, - $csrf + string $csrf ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); @@ -85,6 +87,6 @@ class RequestsController $request->getFlash()->addMessage('All pending requests cleared.', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('stations:reports:requests')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:reports:requests')); } } diff --git a/src/Controller/Stations/Reports/SoundExchangeController.php b/src/Controller/Stations/Reports/SoundExchangeController.php index 2fc6f2e94..be2904818 100644 --- a/src/Controller/Stations/Reports/SoundExchangeController.php +++ b/src/Controller/Stations/Reports/SoundExchangeController.php @@ -1,5 +1,7 @@ getStation(); + $tzObject = $station->getTimezoneObject(); + + $startDate = CarbonImmutable::parse('first day of last month', $tzObject); + $endDate = CarbonImmutable::parse('last day of last month', $tzObject); + $form = new Form($this->form_config); $form->populate( [ - 'start_date' => date('Y-m-d', strtotime('first day of last month')), - 'end_date' => date('Y-m-d', strtotime('last day of last month')), + 'start_date' => $startDate->format('Y-m-d'), + 'end_date' => $endDate->format('Y-m-d'), ] ); - if ($request->isPost() && $form->isValid($request->getParsedBody())) { + if ($form->isValid($request)) { $data = $form->getValues(); - $start_date = strtotime($data['start_date'] . ' 00:00:00'); - $end_date = strtotime($data['end_date'] . ' 23:59:59'); + $startDate = CarbonImmutable::parse($data['start_date'] . ' 00:00:00', $tzObject); + $endDate = CarbonImmutable::parse($data['end_date'] . ' 23:59:59', $tzObject); $fetchIsrc = $data['fetch_isrc']; @@ -87,8 +95,8 @@ class SoundExchangeController GROUP BY sh.song_id DQL )->setParameter('station', $station) - ->setParameter('time_start', $start_date) - ->setParameter('time_end', $end_date) + ->setParameter('time_start', $startDate->getTimestamp()) + ->setParameter('time_end', $endDate->getTimestamp()) ->getArrayResult(); $history_rows_by_id = array_column($history_rows, null, 'media_id'); @@ -149,8 +157,8 @@ class SoundExchangeController // Example: WABC01012009-31012009_A.txt $export_filename = strtoupper($station->getShortName()) - . date('dmY', $start_date) . '-' - . date('dmY', $end_date) . '_A.txt'; + . $startDate->format('dmY') . '-' + . $endDate->format('dmY') . '_A.txt'; return $response->renderStringAsFile($export_txt, 'text/plain', $export_filename); } @@ -162,7 +170,7 @@ class SoundExchangeController ]); } - protected function findISRC($song_row): ?string + protected function findISRC(array $song_row): ?string { $song = Entity\Song::createFromArray($song_row); diff --git a/src/Controller/Stations/Reports/TimelineController.php b/src/Controller/Stations/Reports/TimelineController.php index d0b56bef0..8d1d8dc91 100644 --- a/src/Controller/Stations/Reports/TimelineController.php +++ b/src/Controller/Stations/Reports/TimelineController.php @@ -1,5 +1,7 @@ doEdit($request, $id)) { $request->getFlash()->addMessage('' . __('Changes saved.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('stations:sftp_users:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:sftp_users:index')); } - return $request->getView()->renderToResponse($response, 'system/form_page', [ - 'form' => $this->form, - 'render_mode' => 'edit', - 'title' => $id ? __('Edit SFTP User') : __('Add SFTP User'), - ]); + return $request->getView()->renderToResponse( + $response, + 'system/form_page', + [ + 'form' => $this->form, + 'render_mode' => 'edit', + 'title' => $id ? __('Edit SFTP User') : __('Add SFTP User'), + ] + ); } public function deleteAction( ServerRequest $request, Response $response, - $id, - $csrf + int $id, + string $csrf ): ResponseInterface { $this->doDelete($request, $id, $csrf); $request->getFlash()->addMessage('' . __('SFTP User deleted.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('stations:sftp_users:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:sftp_users:index')); } } diff --git a/src/Controller/Stations/StreamersAction.php b/src/Controller/Stations/StreamersAction.php index 627652b2c..352223238 100644 --- a/src/Controller/Stations/StreamersAction.php +++ b/src/Controller/Stations/StreamersAction.php @@ -1,5 +1,7 @@ withRedirect($request->getRouter()->fromHere('stations:streamers:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:streamers:index')); } return $view->renderToResponse($response, 'stations/streamers/disabled'); diff --git a/src/Controller/Stations/WebhooksController.php b/src/Controller/Stations/WebhooksController.php index f2dfc344a..c6b5d76aa 100644 --- a/src/Controller/Stations/WebhooksController.php +++ b/src/Controller/Stations/WebhooksController.php @@ -1,5 +1,7 @@ getView(); if ($type === null) { - return $view->renderToResponse($response, 'stations/webhooks/add', [ - 'connectors' => array_filter( - $this->webhook_config['webhooks'], - static function ($webhook) { - return !empty($webhook['name']); - } - ), - ]); + return $view->renderToResponse( + $response, + 'stations/webhooks/add', + [ + 'connectors' => array_filter( + $this->webhook_config['webhooks'], + static function ($webhook) { + return !empty($webhook['name']); + } + ), + ] + ); } $record = new Entity\StationWebhook($request->getStation(), $type); if (false !== $this->form->process($request, $record)) { $request->getFlash()->addMessage('' . __('Web Hook added.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('stations:webhooks:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index')); } return $view->renderToResponse($response, 'system/form_page', [ @@ -66,25 +72,29 @@ class WebhooksController extends AbstractStationCrudController ]); } - public function editAction(ServerRequest $request, Response $response, $id): ResponseInterface + public function editAction(ServerRequest $request, Response $response, int $id): ResponseInterface { if (false !== $this->doEdit($request, $id)) { $request->getFlash()->addMessage('' . __('Web Hook updated.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('stations:webhooks:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index')); } - return $request->getView()->renderToResponse($response, 'system/form_page', [ - 'form' => $this->form, - 'render_mode' => 'edit', - 'title' => __('Edit Web Hook'), - ]); + return $request->getView()->renderToResponse( + $response, + 'system/form_page', + [ + 'form' => $this->form, + 'render_mode' => 'edit', + 'title' => __('Edit Web Hook'), + ] + ); } public function toggleAction( ServerRequest $request, Response $response, - $id, - $csrf + int $id, + string $csrf ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); @@ -100,14 +110,14 @@ class WebhooksController extends AbstractStationCrudController '' . ($new_status ? __('Web hook enabled.') : __('Web Hook disabled.')) . '', Flash::SUCCESS ); - return $response->withRedirect($request->getRouter()->fromHere('stations:webhooks:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index')); } public function testAction( ServerRequest $request, Response $response, - $id, - $csrf + int $id, + string $csrf ): ResponseInterface { $request->getCsrf()->verify($csrf, $this->csrf_namespace); @@ -127,13 +137,13 @@ class WebhooksController extends AbstractStationCrudController public function deleteAction( ServerRequest $request, Response $response, - $id, - $csrf + int $id, + string $csrf ): ResponseInterface { $this->doDelete($request, $id, $csrf); $request->getFlash()->addMessage('' . __('Web Hook deleted.') . '', Flash::SUCCESS); - return $response->withRedirect($request->getRouter()->fromHere('stations:webhooks:index')); + return $response->withRedirect((string)$request->getRouter()->fromHere('stations:webhooks:index')); } } diff --git a/src/Customization.php b/src/Customization.php index 6533ab64e..1af19bfa8 100644 --- a/src/Customization.php +++ b/src/Customization.php @@ -1,5 +1,7 @@ hideProductName()) { if ($title) { @@ -137,7 +139,7 @@ class Customization $title = '(' . ucfirst($this->environment->getAppEnvironment()) . ') ' . $title; } - return $title; + return $title ?? ''; } /** diff --git a/src/DeferredCallable.php b/src/DeferredCallable.php index 8d726c681..80c4bb66b 100644 --- a/src/DeferredCallable.php +++ b/src/DeferredCallable.php @@ -33,6 +33,7 @@ class DeferredCallable } /** + * @param mixed ...$args */ public function __invoke(...$args): mixed { diff --git a/src/Doctrine/DecoratedEntityManager.php b/src/Doctrine/DecoratedEntityManager.php index b7961b411..8d22d631d 100644 --- a/src/Doctrine/DecoratedEntityManager.php +++ b/src/Doctrine/DecoratedEntityManager.php @@ -1,7 +1,10 @@ getId(); $this->wrapped->persist($object); @@ -48,12 +51,19 @@ class DecoratedEntityManager extends EntityManagerDecorator implements Reloadabl /** * @inheritDoc + * + * @template TEntity as object + * + * @param TEntity $entity + * + * @return TEntity */ - public function refetch(mixed $entity) + public function refetch(object $entity): object { // phpcs:enable $metadata = $this->wrapped->getClassMetadata(get_class($entity)); + /** @var TEntity|null $freshValue */ $freshValue = $this->wrapped->find($metadata->getName(), $metadata->getIdentifierValues($entity)); if (!$freshValue) { throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'refetch'); @@ -64,12 +74,19 @@ class DecoratedEntityManager extends EntityManagerDecorator implements Reloadabl /** * @inheritDoc + * + * @template TEntity as object + * + * @param TEntity $entity + * + * @return TEntity */ - public function refetchAsReference(mixed $entity) + public function refetchAsReference(object $entity): object { // phpcs:enable $metadata = $this->wrapped->getClassMetadata(get_class($entity)); + /** @var TEntity|null $freshValue */ $freshValue = $this->wrapped->getReference($metadata->getName(), $metadata->getIdentifierValues($entity)); if (!$freshValue) { throw ORMInvalidArgumentException::entityHasNoIdentity($entity, 'refetch'); diff --git a/src/Doctrine/Event/AuditLog.php b/src/Doctrine/Event/AuditLog.php index e5af48744..db0a8b7ff 100644 --- a/src/Doctrine/Event/AuditLog.php +++ b/src/Doctrine/Event/AuditLog.php @@ -1,5 +1,7 @@ getIdentifier($entity); - if (null === $identifier) { - continue; - } $newRecords[] = new Entity\AuditLog( $changeType, @@ -142,6 +140,10 @@ class AuditLog implements EventSubscriber /** @var PersistentCollection $collection */ $owner = $collection->getOwner(); + if (null === $owner) { + continue; + } + $reflectionClass = new ReflectionObject($owner); if (!$this->isAuditable($reflectionClass)) { continue; @@ -149,6 +151,9 @@ class AuditLog implements EventSubscriber // Ignore inverse side or one to many relations $mapping = $collection->getMapping(); + if (null === $mapping) { + continue; + } if (!$mapping['isOwningSide'] || $mapping['type'] !== ClassMetadataInfo::MANY_TO_MANY) { continue; } @@ -190,6 +195,9 @@ class AuditLog implements EventSubscriber // Ignore inverse side or one to many relations $mapping = $collection->getMapping(); + if (null === $mapping) { + continue; + } if (!$mapping['isOwningSide'] || $mapping['type'] !== ClassMetadataInfo::MANY_TO_MANY) { continue; } @@ -238,7 +246,9 @@ class AuditLog implements EventSubscriber $class = ($class instanceof Proxy || $class instanceof GhostObjectInterface) ? get_parent_class($class) : get_class($class); - } elseif (!is_string($class)) { + } + + if (!is_string($class)) { return false; } @@ -249,7 +259,7 @@ class AuditLog implements EventSubscriber return !$em->getMetadataFactory()->isTransient($class); } - #[Pure] protected function isAuditable(ReflectionClass $refl): bool + protected function isAuditable(ReflectionClass $refl): bool { $auditable = $refl->getAttributes(Auditable::class); return !empty($auditable); @@ -260,7 +270,7 @@ class AuditLog implements EventSubscriber * * @param object $entity */ - protected function getIdentifier(object $entity): ?string + protected function getIdentifier(object $entity): string { if ($entity instanceof Stringable) { return (string)$entity; @@ -270,6 +280,13 @@ class AuditLog implements EventSubscriber return $entity->getName(); } - return null; + if ($entity instanceof Entity\Interfaces\IdentifiableEntityInterface) { + $entityId = $entity->getId(); + if (null !== $entityId) { + return (string)$entityId; + } + } + + return spl_object_hash($entity); } } diff --git a/src/Doctrine/Event/SetExplicitChangeTracking.php b/src/Doctrine/Event/SetExplicitChangeTracking.php index 7148b8738..fb4297ea7 100644 --- a/src/Doctrine/Event/SetExplicitChangeTracking.php +++ b/src/Doctrine/Event/SetExplicitChangeTracking.php @@ -1,5 +1,7 @@ */ protected string $entityClass; + /** @var ObjectRepository */ protected ObjectRepository $repository; public function __construct( @@ -22,18 +29,16 @@ class Repository protected LoggerInterface $logger ) { if (!isset($this->entityClass)) { - $this->entityClass = $this->getEntityClass(); + /** @var class-string $defaultClass */ + $defaultClass = str_replace(['Repository', '\\\\'], ['', '\\'], static::class); + $this->entityClass = $defaultClass; } + if (!isset($this->repository)) { $this->repository = $em->getRepository($this->entityClass); } } - protected function getEntityClass(): string - { - return str_replace(['Repository', '\\\\'], ['', '\\'], static::class); - } - public function getRepository(): ObjectRepository { return $this->repository; @@ -48,7 +53,7 @@ class Repository * * @return mixed[] */ - public function fetchArray($cached = true, $order_by = null, $order_dir = 'ASC'): array + public function fetchArray(bool $cached = true, ?string $order_by = null, string $order_dir = 'ASC'): array { $qb = $this->em->createQueryBuilder() ->select('e') @@ -71,8 +76,12 @@ class Repository * * @return mixed[] */ - public function fetchSelect($add_blank = false, Closure $display = null, $pk = 'id', $order_by = 'name'): array - { + public function fetchSelect( + bool|string $add_blank = false, + Closure $display = null, + string $pk = 'id', + string $order_by = 'name' + ): array { $select = []; // Specify custom text in the $add_blank parameter to override. @@ -127,9 +136,9 @@ class Repository * * @return mixed[] */ - public function toArray(object $entity, $deep = false, $form_mode = false): array + public function toArray(object $entity, bool $deep = false, bool $form_mode = false): array { - return $this->serializer->normalize( + return (array)$this->serializer->normalize( $entity, null, [ diff --git a/src/Entity/Analytics.php b/src/Entity/Analytics.php index dfca7d936..34198e026 100644 --- a/src/Entity/Analytics.php +++ b/src/Entity/Analytics.php @@ -1,9 +1,10 @@ station) { + throw new \RuntimeException('Cannot get moment in station timezone; no station associated.'); + } + $tz = $this->station->getTimezoneObject(); return CarbonImmutable::parse($this->moment, $tz)->shiftTimezone($tz); } diff --git a/src/Entity/Api/Admin/Relay.php b/src/Entity/Api/Admin/Relay.php index f63510a43..cc35345d0 100644 --- a/src/Entity/Api/Admin/Relay.php +++ b/src/Entity/Api/Admin/Relay.php @@ -1,5 +1,7 @@ code = (int)$code; - $this->message = (string)$message; + $this->code = $code; + $this->message = $message; $this->formatted_message = ($formatted_message ?? $message); - $this->extra_data = (array)$extra_data; + $this->extra_data = $extra_data; $this->success = false; } + public static function notFound(): self + { + return new self(404, __('Record not found')); + } + + public static function fromFileError(int $fileError): self + { + $errorMessage = match ($fileError) { + UPLOAD_ERR_INI_SIZE => __('The uploaded file exceeds the upload_max_filesize directive in php.ini.'), + UPLOAD_ERR_FORM_SIZE => __('The uploaded file exceeds the MAX_FILE_SIZE directive from the HTML form.'), + UPLOAD_ERR_PARTIAL => __('The uploaded file was only partially uploaded.'), + UPLOAD_ERR_NO_FILE => __('No file was uploaded.'), + UPLOAD_ERR_NO_TMP_DIR => __('No temporary directory is available.'), + UPLOAD_ERR_CANT_WRITE => __('Could not write to filesystem.'), + UPLOAD_ERR_EXTENSION => __('Upload halted by a PHP extension.'), + default => __('Unspecified error.'), + }; + + return new self(500, $errorMessage); + } + public static function fromException(Throwable $e, bool $includeTrace = false): self { $code = $e->getCode(); diff --git a/src/Entity/Api/FileList.php b/src/Entity/Api/FileList.php index 856e1b7b2..87cb76f69 100644 --- a/src/Entity/Api/FileList.php +++ b/src/Entity/Api/FileList.php @@ -1,5 +1,7 @@ now_playing->recalculate(); + $this->now_playing?->recalculate(); } /** diff --git a/src/Entity/Api/NowPlayingCurrentSong.php b/src/Entity/Api/NowPlayingCurrentSong.php index e284bae8f..2a9d68c82 100644 --- a/src/Entity/Api/NowPlayingCurrentSong.php +++ b/src/Entity/Api/NowPlayingCurrentSong.php @@ -1,5 +1,7 @@ is_live = (bool)$is_live; - $this->streamer_name = (string)$streamer_name; + public function __construct( + bool $is_live = false, + string $streamer_name = '', + ?int $broadcast_start = null + ) { + $this->is_live = $is_live; + $this->streamer_name = $streamer_name; $this->broadcast_start = $broadcast_start; } } diff --git a/src/Entity/Api/Podcast.php b/src/Entity/Api/Podcast.php index dca9f6781..dc57e9502 100644 --- a/src/Entity/Api/Podcast.php +++ b/src/Entity/Api/Podcast.php @@ -1,5 +1,7 @@ backend_running = (bool)$backend_running; - $this->frontend_running = (bool)$frontend_running; + $this->backend_running = $backend_running; + $this->frontend_running = $frontend_running; } } diff --git a/src/Entity/Api/Status.php b/src/Entity/Api/Status.php index 4388f5ca3..8c5b1be7b 100644 --- a/src/Entity/Api/Status.php +++ b/src/Entity/Api/Status.php @@ -1,5 +1,7 @@ success = (bool)$success; - $this->message = (string)$message; - - $this->formatted_message = ($formatted_message ?? $message); + $this->success = $success; + $this->message = $message; + $this->formatted_message = $formatted_message ?? $message; } } diff --git a/src/Entity/Api/SystemStatus.php b/src/Entity/Api/SystemStatus.php index e37153950..026e9dd51 100644 --- a/src/Entity/Api/SystemStatus.php +++ b/src/Entity/Api/SystemStatus.php @@ -1,5 +1,7 @@ tracksMatch($npResult, $npOld)) { - $previousHistory = $this->historyRepo->getCurrent($station) - ?? Entity\Song::createFromApiSong($npOld->now_playing->song); + $previousHistory = $this->historyRepo->getCurrent($station); + + if (null === $previousHistory) { + $previousHistory = ($npOld->now_playing?->song) + ? Entity\Song::createFromApiSong($npOld->now_playing->song) + : Entity\Song::createOffline(); + } $sh_obj = $this->historyRepo->register($previousHistory, $station, $np); @@ -175,6 +182,6 @@ class NowPlayingApiGenerator Entity\Api\NowPlaying $npOld ): bool { $current_song_hash = Entity\Song::getSongHash($npResult->currentSong); - return (0 === strcmp($current_song_hash, $npOld->now_playing->song->id)); + return (0 === strcmp($current_song_hash, $npOld->now_playing?->song?->id ?? '')); } } diff --git a/src/Entity/ApiGenerator/SongApiGenerator.php b/src/Entity/ApiGenerator/SongApiGenerator.php index 54396b3c8..01be33e63 100644 --- a/src/Entity/ApiGenerator/SongApiGenerator.php +++ b/src/Entity/ApiGenerator/SongApiGenerator.php @@ -1,5 +1,7 @@ sh_id = $record->getId(); + $response->sh_id = $record->getIdRequired(); $response->played_at = (0 === $record->getTimestampStart()) ? 0 : $record->getTimestampStart() + Entity\SongHistory::PLAYBACK_DELAY_SECONDS; diff --git a/src/Entity/ApiGenerator/StationApiGenerator.php b/src/Entity/ApiGenerator/StationApiGenerator.php index 1f394ce12..862772e54 100644 --- a/src/Entity/ApiGenerator/StationApiGenerator.php +++ b/src/Entity/ApiGenerator/StationApiGenerator.php @@ -1,5 +1,7 @@ is_public = $station->getEnablePublicPage(); $response->listen_url = $fa->getStreamUrl($station, $baseUri); - $response->public_player_url = $this->router->named( + $response->public_player_url = (string)$this->router->named( 'public:index', ['station_id' => $station->getShortName()] ); - $response->playlist_pls_url = $this->router->named( + $response->playlist_pls_url = (string)$this->router->named( 'public:playlist', ['station_id' => $station->getShortName(), 'format' => 'pls'] ); - $response->playlist_m3u_url = $this->router->named( + $response->playlist_m3u_url = (string)$this->router->named( 'public:playlist', ['station_id' => $station->getShortName(), 'format' => 'm3u'] ); diff --git a/src/Entity/ApiGenerator/StationQueueApiGenerator.php b/src/Entity/ApiGenerator/StationQueueApiGenerator.php index 27329ab5e..43bd621a9 100644 --- a/src/Entity/ApiGenerator/StationQueueApiGenerator.php +++ b/src/Entity/ApiGenerator/StationQueueApiGenerator.php @@ -1,5 +1,7 @@ playlist = ''; } - if ($record->getMedia()) { + $recordMedia = $record->getMedia(); + if (null !== $recordMedia) { $response->song = ($this->songApiGenerator)( - $record->getMedia(), + $recordMedia, $record->getStation(), $baseUri, $allowRemoteArt diff --git a/src/Entity/ApiKey.php b/src/Entity/ApiKey.php index 506f0a82f..1b1a3061d 100644 --- a/src/Entity/ApiKey.php +++ b/src/Entity/ApiKey.php @@ -1,6 +1,6 @@ user; } - public function getComment(): ?string + public function getComment(): string { return $this->comment; } - public function setComment(?string $comment): void + public function setComment(string $comment): void { - $this->comment = $this->truncateNullableString($comment); + $this->comment = $this->truncateString($comment); } /** diff --git a/src/Entity/Attributes/AuditIgnore.php b/src/Entity/Attributes/AuditIgnore.php index 2c85fceef..b86ca6dec 100644 --- a/src/Entity/Attributes/AuditIgnore.php +++ b/src/Entity/Attributes/AuditIgnore.php @@ -1,5 +1,7 @@ short_name)) ? $this->short_name : Station::getStationShortName($this->name); } - public function setShortName(?string $short_name): void + public function setShortName(string $short_name): void { $short_name = trim($short_name); if (!empty($short_name)) { @@ -79,6 +80,6 @@ class CustomField implements Stringable public function __toString(): string { - return $this->name; + return $this->short_name; } } diff --git a/src/Entity/Fixture/Analytics.php b/src/Entity/Fixture/Analytics.php index 33817c332..13636ebb8 100644 --- a/src/Entity/Fixture/Analytics.php +++ b/src/Entity/Fixture/Analytics.php @@ -1,5 +1,7 @@ setTitle('Episode ' . $i . ': ' . sprintf($podcastName, $podcastFiller)); diff --git a/src/Entity/Fixture/Role.php b/src/Entity/Fixture/Role.php index f925e0f79..f79ecb9c1 100644 --- a/src/Entity/Fixture/Role.php +++ b/src/Entity/Fixture/Role.php @@ -1,5 +1,7 @@ setBaseUrl(getenv('INIT_BASE_URL') ?? 'docker.local'); - $settings->setInstanceName(getenv('INIT_INSTANCE_NAME') ?? 'local test'); - $settings->setGeoliteLicenseKey(getenv('INIT_GEOLITE_LICENSE_KEY') ?? ''); + $settings->setBaseUrl((string)(getenv('INIT_BASE_URL') ?? 'docker.local')); + $settings->setInstanceName((string)(getenv('INIT_INSTANCE_NAME') ?? 'local test')); + $settings->setGeoliteLicenseKey((string)(getenv('INIT_GEOLITE_LICENSE_KEY') ?? '')); $settings->setSetupCompleteTime(time()); $settings->setPreferBrowserUrl(true); diff --git a/src/Entity/Fixture/Station.php b/src/Entity/Fixture/Station.php index 3ee4d9426..5b0d676fb 100644 --- a/src/Entity/Fixture/Station.php +++ b/src/Entity/Fixture/Station.php @@ -1,5 +1,7 @@ changeCharset('utf8mb4', 'utf8mb4_unicode_ci'); } - private function changeCharset($charset, $collate): void + private function changeCharset(string $charset, string $collate): void { $db_name = $this->connection->getDatabase(); diff --git a/src/Entity/Migration/Version20170906080352.php b/src/Entity/Migration/Version20170906080352.php index 4c3b2e92a..df1052aa6 100644 --- a/src/Entity/Migration/Version20170906080352.php +++ b/src/Entity/Migration/Version20170906080352.php @@ -1,5 +1,7 @@ changeCharset('utf8mb4', 'utf8mb4_bin'); } - private function changeCharset($charset, $collate): void + private function changeCharset(string $charset, string $collate): void { $sqlLines = [ 'ALTER TABLE `station_media` DROP FOREIGN KEY FK_32AADE3AA0BDB2F3', diff --git a/src/Entity/Migration/Version20180826043500.php b/src/Entity/Migration/Version20180826043500.php index 4cd957335..878cd6733 100644 --- a/src/Entity/Migration/Version20180826043500.php +++ b/src/Entity/Migration/Version20180826043500.php @@ -20,7 +20,7 @@ final class Version20180826043500 extends AbstractMigration $this->changeCharset('utf8mb4', 'utf8mb4_general_ci'); } - private function changeCharset($charset, $collate): void + private function changeCharset(string $charset, string $collate): void { $db_name = $this->connection->getDatabase(); diff --git a/src/Entity/Migration/Version20201204043539.php b/src/Entity/Migration/Version20201204043539.php index 477db8f6b..faec8b0f7 100644 --- a/src/Entity/Migration/Version20201204043539.php +++ b/src/Entity/Migration/Version20201204043539.php @@ -84,12 +84,12 @@ final class Version20201204043539 extends AbstractMigration } } - private function toInt($value): ?int + private function toInt(mixed $value): ?int { return (null === $value) ? null : (int)$value; } - private function toBool($value): ?bool + private function toBool(mixed $value): ?bool { return (null === $value) ? null : (bool)$value; } diff --git a/src/Entity/Migration/Version20210717164419.php b/src/Entity/Migration/Version20210717164419.php new file mode 100644 index 000000000..1dea35d38 --- /dev/null +++ b/src/Entity/Migration/Version20210717164419.php @@ -0,0 +1,78 @@ +setEmptyWhereNull('api_keys', 'comment'); + $this->setEmptyWhereNull('custom_field', 'short_name'); + $this->setEmptyWhereNull('station', 'name'); + $this->setEmptyWhereNull('station', 'short_name'); + $this->setEmptyWhereNull('users', 'email'); + $this->setEmptyWhereNull('users', 'auth_password'); + $this->setEmptyWhereNull('storage_location', 'path'); + $this->setEmptyWhereNull('station_remotes', 'url'); + } + + protected function setEmptyWhereNull(string $table, string $field): void + { + $this->connection->update( + $table, + [$field => ''], + [$field => null] + ); + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE api_keys CHANGE comment comment VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE custom_field CHANGE short_name short_name VARCHAR(100) NOT NULL'); + $this->addSql( + 'ALTER TABLE station CHANGE name name VARCHAR(100) NOT NULL, CHANGE short_name short_name VARCHAR(100) NOT NULL' + ); + $this->addSql( + 'ALTER TABLE users CHANGE email email VARCHAR(100) NOT NULL, CHANGE auth_password auth_password VARCHAR(255) NOT NULL' + ); + $this->addSql('ALTER TABLE storage_location CHANGE path path VARCHAR(255) NOT NULL'); + $this->addSql('ALTER TABLE station_remotes CHANGE url url VARCHAR(255) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql( + 'ALTER TABLE api_keys CHANGE comment comment VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`' + ); + $this->addSql( + 'ALTER TABLE custom_field CHANGE short_name short_name VARCHAR(100) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`' + ); + $this->addSql( + 'ALTER TABLE station CHANGE name name VARCHAR(100) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`, CHANGE short_name short_name VARCHAR(100) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`' + ); + $this->addSql( + 'ALTER TABLE users CHANGE email email VARCHAR(100) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`, CHANGE auth_password auth_password VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`' + ); + $this->addSql( + 'ALTER TABLE storage_location CHANGE path path VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_general_ci`' + ); + $this->addSql( + 'ALTER TABLE station_remotes CHANGE url url VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_unicode_ci`' + ); + } +} diff --git a/src/Entity/Podcast.php b/src/Entity/Podcast.php index bca84cf12..d0041c476 100644 --- a/src/Entity/Podcast.php +++ b/src/Entity/Podcast.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use App\Entity\Interfaces\IdentifiableEntityInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; @@ -14,7 +15,7 @@ use Symfony\Component\Validator\Constraints as Assert; ORM\Table(name: 'podcast'), Attributes\Auditable ] -class Podcast +class Podcast implements IdentifiableEntityInterface { use Traits\HasUniqueId; use Traits\TruncateStrings; diff --git a/src/Entity/PodcastCategory.php b/src/Entity/PodcastCategory.php index b2846aae9..ca595174d 100644 --- a/src/Entity/PodcastCategory.php +++ b/src/Entity/PodcastCategory.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use App\Entity\Interfaces\IdentifiableEntityInterface; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -12,7 +13,7 @@ use Symfony\Component\Validator\Constraints as Assert; ORM\Table(name: 'podcast_category'), Attributes\Auditable ] -class PodcastCategory +class PodcastCategory implements IdentifiableEntityInterface { use Traits\HasAutoIncrementId; use Traits\TruncateStrings; diff --git a/src/Entity/PodcastEpisode.php b/src/Entity/PodcastEpisode.php index 3af5b60e2..2ea1cfa9a 100644 --- a/src/Entity/PodcastEpisode.php +++ b/src/Entity/PodcastEpisode.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use App\Entity\Interfaces\IdentifiableEntityInterface; use App\Entity\Traits; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -13,7 +14,7 @@ use Symfony\Component\Validator\Constraints as Assert; ORM\Table(name: 'podcast_episode'), Attributes\Auditable ] -class PodcastEpisode +class PodcastEpisode implements IdentifiableEntityInterface { use Traits\HasUniqueId; use Traits\TruncateStrings; diff --git a/src/Entity/PodcastMedia.php b/src/Entity/PodcastMedia.php index 817932732..4c172a638 100644 --- a/src/Entity/PodcastMedia.php +++ b/src/Entity/PodcastMedia.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Entity; +use App\Entity\Interfaces\IdentifiableEntityInterface; use App\Entity\Traits; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -13,7 +14,7 @@ use Symfony\Component\Validator\Constraints as Assert; ORM\Table(name: 'podcast_media'), Attributes\Auditable ] -class PodcastMedia +class PodcastMedia implements IdentifiableEntityInterface { use Traits\HasUniqueId; use Traits\TruncateStrings; @@ -161,25 +162,4 @@ class PodcastMedia return $this; } - - /** - * @param string|float|null $seconds - */ - protected function parseSeconds($seconds = null): ?float - { - if ($seconds === '') { - return null; - } - - if (str_contains($seconds, ':')) { - $sec = 0; - foreach (array_reverse(explode(':', $seconds)) as $k => $v) { - $sec += (60 ** (int)$k) * (int)$v; - } - - return $sec; - } - - return $seconds; - } } diff --git a/src/Entity/Relay.php b/src/Entity/Relay.php index c79641ade..6042d64a8 100644 --- a/src/Entity/Relay.php +++ b/src/Entity/Relay.php @@ -1,9 +1,10 @@ updated_at = time(); } - public function getBaseUrl(): ?string + public function getBaseUrl(): string { return $this->base_url; } @@ -92,7 +93,7 @@ class Relay return $this->nowplaying; } - public function setNowplaying($nowplaying): void + public function setNowplaying(mixed $nowplaying): void { $this->nowplaying = $nowplaying; } diff --git a/src/Entity/Repository/AbstractSplitTokenRepository.php b/src/Entity/Repository/AbstractSplitTokenRepository.php index a97380b60..e9e7e5bb8 100644 --- a/src/Entity/Repository/AbstractSplitTokenRepository.php +++ b/src/Entity/Repository/AbstractSplitTokenRepository.php @@ -1,11 +1,17 @@ + */ abstract class AbstractSplitTokenRepository extends Repository { /** diff --git a/src/Entity/Repository/AnalyticsRepository.php b/src/Entity/Repository/AnalyticsRepository.php index b03e74232..bd064c3d2 100644 --- a/src/Entity/Repository/AnalyticsRepository.php +++ b/src/Entity/Repository/AnalyticsRepository.php @@ -1,5 +1,7 @@ getId()); + $episodeArtworkPath = PodcastEpisode::getArtPath($episode->getIdRequired()); $episodeArtworkStream = $episodeArtwork->stream('jpg'); $fsPodcasts = $episode->getPodcast()->getStorageLocation()->getFilesystem(); @@ -102,7 +102,7 @@ class PodcastEpisodeRepository extends Repository PodcastEpisode $episode, ?ExtendedFilesystemInterface $fs = null ): void { - $artworkPath = PodcastEpisode::getArtPath($episode->getId()); + $artworkPath = PodcastEpisode::getArtPath($episode->getIdRequired()); $fs ??= $episode->getPodcast()->getStorageLocation()->getFilesystem(); diff --git a/src/Entity/Repository/PodcastRepository.php b/src/Entity/Repository/PodcastRepository.php index de2774aa5..7ad9c216e 100644 --- a/src/Entity/Repository/PodcastRepository.php +++ b/src/Entity/Repository/PodcastRepository.php @@ -6,9 +6,7 @@ namespace App\Entity\Repository; use App\Doctrine\ReloadableEntityManagerInterface; use App\Doctrine\Repository; -use App\Entity\Podcast; -use App\Entity\Station; -use App\Entity\StorageLocation; +use App\Entity; use App\Environment; use Azura\Files\ExtendedFilesystemInterface; use Intervention\Image\Constraint; @@ -17,6 +15,9 @@ use League\Flysystem\UnableToDeleteFile; use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\Serializer; +/** + * @extends Repository + */ class PodcastRepository extends Repository { public function __construct( @@ -30,15 +31,15 @@ class PodcastRepository extends Repository parent::__construct($entityManager, $serializer, $environment, $logger); } - public function fetchPodcastForStation(Station $station, string $podcastId): ?Podcast + public function fetchPodcastForStation(Entity\Station $station, string $podcastId): ?Entity\Podcast { return $this->fetchPodcastForStorageLocation($station->getPodcastsStorageLocation(), $podcastId); } public function fetchPodcastForStorageLocation( - StorageLocation $storageLocation, + Entity\StorageLocation $storageLocation, string $podcastId - ): ?Podcast { + ): ?Entity\Podcast { return $this->repository->findOneBy( [ 'id' => $podcastId, @@ -48,9 +49,9 @@ class PodcastRepository extends Repository } /** - * @return Podcast[] + * @return Entity\Podcast[] */ - public function fetchPublishedPodcastsForStation(Station $station): array + public function fetchPublishedPodcastsForStation(Entity\Station $station): array { $podcasts = $this->em->createQuery( <<<'DQL' @@ -64,7 +65,7 @@ class PodcastRepository extends Repository return array_filter( $podcasts, - static function (Podcast $podcast) { + static function (Entity\Podcast $podcast) { foreach ($podcast->getEpisodes() as $episode) { if ($episode->isPublished()) { return true; @@ -77,7 +78,7 @@ class PodcastRepository extends Repository } public function writePodcastArt( - Podcast $podcast, + Entity\Podcast $podcast, string $rawArtworkString, ?ExtendedFilesystemInterface $fs = null ): void { @@ -92,7 +93,7 @@ class PodcastRepository extends Repository } ); - $podcastArtworkPath = Podcast::getArtPath($podcast->getId()); + $podcastArtworkPath = Entity\Podcast::getArtPath($podcast->getIdRequired()); $podcastArtworkStream = $podcastArtwork->stream('jpg'); $fs->writeStream($podcastArtworkPath, $podcastArtworkStream->detach()); @@ -101,12 +102,12 @@ class PodcastRepository extends Repository } public function removePodcastArt( - Podcast $podcast, + Entity\Podcast $podcast, ?ExtendedFilesystemInterface $fs = null ): void { $fs ??= $podcast->getStorageLocation()->getFilesystem(); - $artworkPath = Podcast::getArtPath($podcast->getId()); + $artworkPath = Entity\Podcast::getArtPath($podcast->getIdRequired()); try { $fs->delete($artworkPath); @@ -117,7 +118,7 @@ class PodcastRepository extends Repository } public function delete( - Podcast $podcast, + Entity\Podcast $podcast, ?ExtendedFilesystemInterface $fs = null ): void { $fs ??= $podcast->getStorageLocation()->getFilesystem(); diff --git a/src/Entity/Repository/RolePermissionRepository.php b/src/Entity/Repository/RolePermissionRepository.php index d5035a58b..5687a0b9d 100644 --- a/src/Entity/Repository/RolePermissionRepository.php +++ b/src/Entity/Repository/RolePermissionRepository.php @@ -1,10 +1,15 @@ + */ class RolePermissionRepository extends Repository { /** diff --git a/src/Entity/Repository/RoleRepository.php b/src/Entity/Repository/RoleRepository.php index bed368ace..8f5b7389e 100644 --- a/src/Entity/Repository/RoleRepository.php +++ b/src/Entity/Repository/RoleRepository.php @@ -1,9 +1,15 @@ + */ class RoleRepository extends Repository { diff --git a/src/Entity/Repository/SettingsRepository.php b/src/Entity/Repository/SettingsRepository.php index c876d3178..58c93023c 100644 --- a/src/Entity/Repository/SettingsRepository.php +++ b/src/Entity/Repository/SettingsRepository.php @@ -1,5 +1,7 @@ 0) { $delta_positive += $delta_delta; } elseif ($delta_delta < 0) { - $delta_negative += abs($delta_delta); + $delta_negative += (int)abs($delta_delta); } } diff --git a/src/Entity/Repository/StationMediaRepository.php b/src/Entity/Repository/StationMediaRepository.php index 1b96671b4..e3e9e0d1b 100644 --- a/src/Entity/Repository/StationMediaRepository.php +++ b/src/Entity/Repository/StationMediaRepository.php @@ -1,5 +1,7 @@ findByUniqueId($id, $source); if ($media instanceof Entity\StationMedia) { return $media; @@ -106,7 +108,7 @@ class StationMediaRepository extends Repository return $media; } - public function iteratePaths(array $paths, $source): Generator + public function iteratePaths(array $paths, Entity\Station|Entity\StorageLocation $source): Generator { $storageLocation = $this->getStorageLocation($source); diff --git a/src/Entity/Repository/StationMountRepository.php b/src/Entity/Repository/StationMountRepository.php index a92f579eb..5b07c619a 100644 --- a/src/Entity/Repository/StationMountRepository.php +++ b/src/Entity/Repository/StationMountRepository.php @@ -1,5 +1,7 @@ + */ class StationRepository extends Repository { public function __construct( @@ -243,7 +248,7 @@ class StationRepository extends Repository public function getDefaultAlbumArtUrl(?Entity\Station $station = null): UriInterface { if (null !== $station) { - $stationCustomUrl = trim($station->getDefaultAlbumArtUrl()); + $stationCustomUrl = trim($station->getDefaultAlbumArtUrl() ?? ''); if (!empty($stationCustomUrl)) { return new Uri($stationCustomUrl); @@ -251,7 +256,7 @@ class StationRepository extends Repository } $settings = $this->settingsRepo->readSettings(); - $custom_url = trim($settings->getDefaultAlbumArtUrl()); + $custom_url = trim($settings->getDefaultAlbumArtUrl() ?? ''); if (!empty($custom_url)) { return new Uri($custom_url); diff --git a/src/Entity/Repository/StationRequestRepository.php b/src/Entity/Repository/StationRequestRepository.php index fd884549d..ea03e4407 100644 --- a/src/Entity/Repository/StationRequestRepository.php +++ b/src/Entity/Repository/StationRequestRepository.php @@ -1,5 +1,7 @@ em->persist($record); $this->em->flush(); - return $record->getId(); + return $record->getIdRequired(); } /** @@ -209,7 +211,7 @@ class StationRequestRepository extends Repository ->getArrayResult(); $eligibleTrack = new Entity\Api\StationPlaylistQueue(); - $eligibleTrack->media_id = $media->getId(); + $eligibleTrack->media_id = $media->getIdRequired(); $eligibleTrack->song_id = $media->getSongId(); $eligibleTrack->title = $media->getTitle() ?? ''; $eligibleTrack->artist = $media->getArtist() ?? ''; diff --git a/src/Entity/Repository/StationScheduleRepository.php b/src/Entity/Repository/StationScheduleRepository.php index 8d0faf20d..31f6ceadd 100644 --- a/src/Entity/Repository/StationScheduleRepository.php +++ b/src/Entity/Repository/StationScheduleRepository.php @@ -1,5 +1,7 @@ + */ class StationScheduleRepository extends Repository { protected Scheduler $scheduler; @@ -30,9 +35,9 @@ class StationScheduleRepository extends Repository /** * @param Entity\StationPlaylist|Entity\StationStreamer $relation - * @param array|null $items + * @param array $items */ - public function setScheduleItems(Entity\StationPlaylist|Entity\StationStreamer $relation, ?array $items): void + public function setScheduleItems(Entity\StationPlaylist|Entity\StationStreamer $relation, array $items = []): void { $rawScheduleItems = $this->findByRelation($relation); @@ -135,21 +140,20 @@ class StationScheduleRepository extends Repository } $row = new Entity\Api\StationSchedule(); - $row->id = $scheduleItem->getId(); + $row->id = $scheduleItem->getIdRequired(); $row->start_timestamp = $start->getTimestamp(); $row->start = $start->toIso8601String(); $row->end_timestamp = $end->getTimestamp(); $row->end = $end->toIso8601String(); $row->is_now = $start->lessThanOrEqualTo($now); - if ($scheduleItem->getPlaylist() instanceof Entity\StationPlaylist) { - $playlist = $scheduleItem->getPlaylist(); + $playlist = $scheduleItem->getPlaylist(); + $streamer = $scheduleItem->getStreamer(); + if ($playlist instanceof Entity\StationPlaylist) { $row->type = Entity\Api\StationSchedule::TYPE_PLAYLIST; $row->name = $playlist->getName(); - } elseif ($scheduleItem->getStreamer() instanceof Entity\StationStreamer) { - $streamer = $scheduleItem->getStreamer(); - + } elseif ($streamer instanceof Entity\StationStreamer) { $row->type = Entity\Api\StationSchedule::TYPE_STREAMER; $row->name = $streamer->getDisplayName(); } diff --git a/src/Entity/Repository/StationStreamerBroadcastRepository.php b/src/Entity/Repository/StationStreamerBroadcastRepository.php index f0a60ba17..852ac93f5 100644 --- a/src/Entity/Repository/StationStreamerBroadcastRepository.php +++ b/src/Entity/Repository/StationStreamerBroadcastRepository.php @@ -1,10 +1,15 @@ + */ class StationStreamerBroadcastRepository extends Repository { public function getLatestBroadcast(Entity\Station $station): ?Entity\StationStreamerBroadcast diff --git a/src/Entity/Repository/StationStreamerRepository.php b/src/Entity/Repository/StationStreamerRepository.php index affa3e53b..f3a78e6de 100644 --- a/src/Entity/Repository/StationStreamerRepository.php +++ b/src/Entity/Repository/StationStreamerRepository.php @@ -1,5 +1,7 @@ + */ class StorageLocationRepository extends Repository { public function findByType(string $type, int $id): ?Entity\StorageLocation diff --git a/src/Entity/Repository/UnprocessableMediaRepository.php b/src/Entity/Repository/UnprocessableMediaRepository.php index 45a51f816..4a46ff026 100644 --- a/src/Entity/Repository/UnprocessableMediaRepository.php +++ b/src/Entity/Repository/UnprocessableMediaRepository.php @@ -1,5 +1,7 @@ + */ class UserRepository extends Repository { public function find(int $id): ?Entity\User diff --git a/src/Entity/Role.php b/src/Entity/Role.php index cf4d0284f..0c478051f 100644 --- a/src/Entity/Role.php +++ b/src/Entity/Role.php @@ -1,9 +1,10 @@ permissions as $permission) { /** @var RolePermission $permission */ - if ($permission->hasStation()) { - $return['permissions']['station'][$permission->getStation()->getId()][] = $permission->getActionName(); + $station = $permission->getStation(); + if (null !== $station) { + $return['permissions']['station'][$station->getIdRequired()][] = $permission->getActionName(); } else { $return['permissions']['global'][] = $permission->getActionName(); } diff --git a/src/Entity/RolePermission.php b/src/Entity/RolePermission.php index dc77fb938..0795e5580 100644 --- a/src/Entity/RolePermission.php +++ b/src/Entity/RolePermission.php @@ -1,6 +1,6 @@ role = $role; $this->station = $station; diff --git a/src/Entity/Settings.php b/src/Entity/Settings.php index 4b32f5655..90111427f 100644 --- a/src/Entity/Settings.php +++ b/src/Entity/Settings.php @@ -1,5 +1,7 @@ app_unique_identifier ?? null; + if (!isset($this->app_unique_identifier)) { + throw new \RuntimeException('Application Unique ID not generated yet.'); + } + + return $this->app_unique_identifier; } /** @@ -418,7 +424,7 @@ class Settings implements Stringable public function setLastFmApiKey(?string $lastFmApiKey): void { - $lastFmApiKey = trim($lastFmApiKey); + $lastFmApiKey = trim($lastFmApiKey ?? ''); $lastFmApiKey = (!empty($lastFmApiKey)) ? $lastFmApiKey : null; $this->last_fm_api_key = $this->truncateNullableString($lastFmApiKey); diff --git a/src/Entity/SftpUser.php b/src/Entity/SftpUser.php index ebed1bdc7..2a1240396 100644 --- a/src/Entity/SftpUser.php +++ b/src/Entity/SftpUser.php @@ -1,10 +1,11 @@ publicKeys) { + return []; + } + $pubKeysRaw = trim($this->publicKeys); if (!empty($pubKeysRaw)) { return array_filter(array_map('trim', explode("\n", $pubKeysRaw))); diff --git a/src/Entity/Song.php b/src/Entity/Song.php index 4a551751e..61aa0a556 100644 --- a/src/Entity/Song.php +++ b/src/Entity/Song.php @@ -1,5 +1,7 @@ getText()); + return self::getSongHash($songText->getText() ?? ''); } if ($songText instanceof CurrentSong) { return self::getSongHash($songText->text); diff --git a/src/Entity/SongHistory.php b/src/Entity/SongHistory.php index e069f5a36..fa69c3c1f 100644 --- a/src/Entity/SongHistory.php +++ b/src/Entity/SongHistory.php @@ -1,16 +1,17 @@ request; } - public function setRequest($request): void + public function setRequest(?StationRequest $request): void { $this->request = $request; } @@ -159,7 +160,7 @@ class SongHistory implements SongInterface return $this->duration; } - public function setDuration($duration): void + public function setDuration(?int $duration): void { $this->duration = $duration; } @@ -169,7 +170,7 @@ class SongHistory implements SongInterface return $this->listeners_start; } - public function setListenersStart($listeners_start): void + public function setListenersStart(?int $listeners_start): void { $this->listeners_start = $listeners_start; } @@ -198,7 +199,7 @@ class SongHistory implements SongInterface return $this->listeners_end; } - public function setListenersEnd($listeners_end): void + public function setListenersEnd(?int $listeners_end): void { $this->listeners_end = $listeners_end; } @@ -208,7 +209,7 @@ class SongHistory implements SongInterface return $this->unique_listeners; } - public function setUniqueListeners($unique_listeners): void + public function setUniqueListeners(?int $unique_listeners): void { $this->unique_listeners = $unique_listeners; } @@ -248,14 +249,12 @@ class SongHistory implements SongInterface $this->delta_negative = $this->truncateSmallInt($delta_negative); } - /** - */ public function getDeltaPoints(): mixed { return $this->delta_points; } - public function addDeltaPoint($delta_point): void + public function addDeltaPoint(mixed $delta_point): void { $delta_points = (array)$this->delta_points; $delta_points[] = $delta_point; diff --git a/src/Entity/Station.php b/src/Entity/Station.php index 288d762bb..b5b475238 100644 --- a/src/Entity/Station.php +++ b/src/Entity/Station.php @@ -1,10 +1,10 @@ name; } - public function setName(?string $name = null): void + public function setName(string $name): void { - $this->name = $this->truncateNullableString($name, 100); + $this->name = $this->truncateString($name, 100); if (empty($this->short_name) && !empty($name)) { - $this->setShortName(null); + $this->setShortName(self::getStationShortName($name)); } } - public function getShortName(): ?string + public function getShortName(): string { return (!empty($this->short_name)) ? $this->short_name : self::getStationShortName($this->name); } - public function setShortName(?string $shortName): void + public function setShortName(string $shortName): void { $shortName = trim($shortName); if (empty($shortName)) { @@ -489,7 +489,7 @@ class Station implements Stringable */ public function validateAdapterApiKey(string $api_key): bool { - return hash_equals($api_key, $this->adapter_api_key); + return hash_equals($api_key, $this->adapter_api_key ?? ''); } public function getDescription(): ?string @@ -509,7 +509,7 @@ class Station implements Stringable public function setUrl(string $url = null): void { - $this->url = $this->truncateString($url); + $this->url = $this->truncateNullableString($url); } public function getGenre(): ?string @@ -522,14 +522,18 @@ class Station implements Stringable $this->genre = $this->truncateNullableString($genre, 150); } - public function getRadioBaseDir(): ?string + public function getRadioBaseDir(): string { - return $this->radio_base_dir; + if (null === $this->radio_base_dir) { + $this->setRadioBaseDir(); + } + + return (string)$this->radio_base_dir; } public function setRadioBaseDir(?string $newDir = null): void { - $newDir = $this->truncateNullableString(trim($newDir)); + $newDir = $this->truncateNullableString(trim($newDir ?? '')); if (empty($newDir)) { $stationsBaseDir = Environment::getInstance()->getStationDirectory(); @@ -541,10 +545,6 @@ class Station implements Stringable public function ensureDirectoriesExist(): void { - if (null === $this->radio_base_dir) { - $this->setRadioBaseDir(); - } - // Flysystem adapters will automatically create the main directory. $this->ensureDirectoryExists($this->getRadioBaseDir()); $this->ensureDirectoryExists($this->getRadioPlaylistsDir()); @@ -700,12 +700,12 @@ class Station implements Stringable $this->request_threshold = $request_threshold; } - public function getDisconnectDeactivateStreamer(): int + public function getDisconnectDeactivateStreamer(): ?int { return $this->disconnect_deactivate_streamer; } - public function setDisconnectDeactivateStreamer(int $disconnect_deactivate_streamer): void + public function setDisconnectDeactivateStreamer(?int $disconnect_deactivate_streamer): void { $this->disconnect_deactivate_streamer = $disconnect_deactivate_streamer; } @@ -794,7 +794,7 @@ class Station implements Stringable return $this->api_history_items ?? self::DEFAULT_API_HISTORY_ITEMS; } - public function setApiHistoryItems(?int $api_history_items): void + public function setApiHistoryItems(int $api_history_items): void { $this->api_history_items = $api_history_items; } @@ -831,11 +831,17 @@ class Station implements Stringable $this->default_album_art_url = $default_album_art_url; } + /** + * @return Collection + */ public function getHistory(): Collection { return $this->history; } + /** + * @return Collection + */ public function getStreamers(): Collection { return $this->streamers; @@ -855,6 +861,10 @@ class Station implements Stringable public function getMediaStorageLocation(): StorageLocation { + if (null === $this->media_storage_location) { + throw new \RuntimeException('Media storage location has not been configured yet.'); + } + return $this->media_storage_location; } @@ -869,6 +879,10 @@ class Station implements Stringable public function getRecordingsStorageLocation(): StorageLocation { + if (null === $this->recordings_storage_location) { + throw new \RuntimeException('Recordings storage location has not been configured yet.'); + } + return $this->recordings_storage_location; } @@ -883,6 +897,10 @@ class Station implements Stringable public function getPodcastsStorageLocation(): StorageLocation { + if (null === $this->podcasts_storage_location) { + throw new \RuntimeException('Podcasts storage location has not been configured yet.'); + } + return $this->podcasts_storage_location; } @@ -905,21 +923,24 @@ class Station implements Stringable ]; } + /** + * @return Collection + */ public function getPermissions(): Collection { return $this->permissions; } /** - * @return StationMedia[]|Collection + * @return Collection */ public function getMedia(): Collection { - return $this->media_storage_location->getMedia(); + return $this->getMediaStorageLocation()->getMedia(); } /** - * @return StationPlaylist[]|Collection + * @return Collection */ public function getPlaylists(): Collection { @@ -927,7 +948,7 @@ class Station implements Stringable } /** - * @return StationMount[]|Collection + * @return Collection */ public function getMounts(): Collection { @@ -935,18 +956,24 @@ class Station implements Stringable } /** - * @return StationRemote[]|Collection + * @return Collection */ public function getRemotes(): Collection { return $this->remotes; } + /** + * @return Collection + */ public function getWebhooks(): Collection { return $this->webhooks; } + /** + * @return Collection + */ public function getSftpUsers(): Collection { return $this->sftp_users; @@ -972,7 +999,7 @@ class Station implements Stringable public function __clone() { $this->id = null; - $this->short_name = null; + $this->short_name = ''; $this->radio_base_dir = null; $this->adapter_api_key = null; $this->nowplaying = null; diff --git a/src/Entity/StationBackendConfiguration.php b/src/Entity/StationBackendConfiguration.php index 71908e70f..30af5b1b4 100644 --- a/src/Entity/StationBackendConfiguration.php +++ b/src/Entity/StationBackendConfiguration.php @@ -1,5 +1,7 @@ isrc = $this->truncateNullableString($isrc, 15); } - public function getLength(): float + public function getLength(): ?float { return $this->length; } @@ -356,12 +358,9 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar return $this->fade_in; } - /** - * @param string|float|null $fade_in - */ - public function setFadeIn($fade_in = null): void + public function setFadeIn(string|int|float $fade_in = null): void { - $this->fade_in = $this->parseSeconds($fade_in); + $this->fade_in = Time::displayTimeToSeconds($fade_in); } public function getFadeOut(): ?float @@ -369,12 +368,9 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar return $this->fade_out; } - /** - * @param string|float|null $fade_out - */ - public function setFadeOut($fade_out = null): void + public function setFadeOut(string|int|float $fade_out = null): void { - $this->fade_out = $this->parseSeconds($fade_out); + $this->fade_out = Time::displayTimeToSeconds($fade_out); } public function getCueIn(): ?float @@ -382,12 +378,9 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar return $this->cue_in; } - /** - * @param string|float|null $cue_in - */ - public function setCueIn($cue_in = null): void + public function setCueIn(string|int|float $cue_in = null): void { - $this->cue_in = $this->parseSeconds($cue_in); + $this->cue_in = Time::displayTimeToSeconds($cue_in); } public function getCueOut(): ?float @@ -395,33 +388,9 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar return $this->cue_out; } - /** - * @param string|float|null $cue_out - */ - public function setCueOut($cue_out = null): void + public function setCueOut(string|int|float $cue_out = null): void { - $this->cue_out = $this->parseSeconds($cue_out); - } - - /** - * @param string|float|null $seconds - */ - protected function parseSeconds($seconds = null): ?float - { - if ($seconds === '') { - return null; - } - - if (str_contains($seconds, ':')) { - $sec = 0; - foreach (array_reverse(explode(':', $seconds)) as $k => $v) { - $sec += (60 ** (int)$k) * (int)$v; - } - - return $sec; - } - - return $seconds; + $this->cue_out = Time::displayTimeToSeconds($cue_out); } /** @@ -439,7 +408,7 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar $length -= $this->cue_in; } - return $length; + return (int)$length; } public function getArtUpdatedAt(): int @@ -493,7 +462,7 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar public function fromMetadata(Metadata $metadata): void { - $this->setLength($metadata->getDuration()); + $this->setLength((int)$metadata->getDuration()); $tags = $metadata->getTags(); @@ -522,7 +491,7 @@ class StationMedia implements SongInterface, ProcessableMediaInterface, PathAwar public function toMetadata(): Metadata { $metadata = new Metadata(); - $metadata->setDuration($this->getLength()); + $metadata->setDuration($this->getLength() ?? 0.0); $tagsToSet = array_filter( [ diff --git a/src/Entity/StationMediaCustomField.php b/src/Entity/StationMediaCustomField.php index 752e1c2c4..f3151d5e3 100644 --- a/src/Entity/StationMediaCustomField.php +++ b/src/Entity/StationMediaCustomField.php @@ -1,16 +1,17 @@ enable_autodj) { - return $this->autodj_bitrate . 'kbps ' . strtoupper($this->autodj_format); + return $this->autodj_bitrate . 'kbps ' . strtoupper($this->autodj_format ?? ''); } return $this->name; @@ -348,7 +351,7 @@ class StationMount implements Stringable, Interfaces\StationMountInterface, Inte ): Api\StationMount { $response = new Api\StationMount(); - $response->id = $this->id; + $response->id = $this->getIdRequired(); $response->name = $this->getDisplayName(); $response->path = $this->getName(); $response->is_default = $this->is_default; diff --git a/src/Entity/StationPlaylist.php b/src/Entity/StationPlaylist.php index acda9ae60..9cdc69ce6 100644 --- a/src/Entity/StationPlaylist.php +++ b/src/Entity/StationPlaylist.php @@ -1,6 +1,6 @@ backend_options); + return explode(',', $this->backend_options ?? ''); } /** diff --git a/src/Entity/StationPlaylistFolder.php b/src/Entity/StationPlaylistFolder.php index 2da2bf789..69c58f35b 100644 --- a/src/Entity/StationPlaylistFolder.php +++ b/src/Entity/StationPlaylistFolder.php @@ -1,6 +1,6 @@ request; } - public function setRequest($request): void + public function setRequest(?StationRequest $request): void { $this->request = $request; } diff --git a/src/Entity/StationRemote.php b/src/Entity/StationRemote.php index b2975f21c..86f34fa20 100644 --- a/src/Entity/StationRemote.php +++ b/src/Entity/StationRemote.php @@ -1,6 +1,6 @@ custom_listen_url; } - public function setCustomListenUrl(string $custom_listen_url = null): void + public function setCustomListenUrl(?string $custom_listen_url = null): void { - $this->custom_listen_url = $this->truncateString($custom_listen_url); + $this->custom_listen_url = $this->truncateNullableString($custom_listen_url); } - /** @inheritdoc */ public function getAutodjUsername(): ?string { return $this->getSourceUsername(); @@ -212,7 +214,6 @@ class StationRemote implements Stringable, Interfaces\StationMountInterface, Int $this->source_username = $this->truncateNullableString($source_username, 100); } - /** @inheritdoc */ public function getAutodjPassword(): ?string { $password = $this->getSourcePassword(); @@ -296,25 +297,29 @@ class StationRemote implements Stringable, Interfaces\StationMountInterface, Int return $this->getMount(); } - /** @inheritdoc */ public function getAutodjHost(): ?string { - return parse_url($this->getUrl(), PHP_URL_HOST); + return $this->getUrlAsUri()->getHost(); } - public function getUrl(): ?string + public function getUrl(): string { return $this->url; } - public function setUrl(?string $url): void + public function getUrlAsUri(): UriInterface + { + return new Uri($this->url); + } + + public function setUrl(string $url): void { if (!empty($url) && !str_starts_with($url, 'http')) { /** @noinspection HttpUrlsUsage */ $url = 'http://' . $url; } - $this->url = $this->truncateNullableString($url); + $this->url = $this->truncateString($url); } /* @@ -324,7 +329,7 @@ class StationRemote implements Stringable, Interfaces\StationMountInterface, Int /** @inheritdoc */ public function getAutodjPort(): ?int { - return $this->getSourcePort() ?? parse_url($this->getUrl(), PHP_URL_PORT); + return $this->getSourcePort() ?? $this->getUrlAsUri()->getPort(); } public function getSourcePort(): ?int @@ -347,7 +352,7 @@ class StationRemote implements Stringable, Interfaces\StationMountInterface, Int return self::PROTOCOL_ICY; } - $urlScheme = parse_url($this->getUrl(), PHP_URL_SCHEME); + $urlScheme = $this->getUrlAsUri()->getScheme(); return ('https' === $urlScheme) ? self::PROTOCOL_HTTPS : self::PROTOCOL_HTTP; @@ -406,7 +411,7 @@ class StationRemote implements Stringable, Interfaces\StationMountInterface, Int ): Api\StationRemote { $response = new Api\StationRemote(); - $response->id = $this->id; + $response->id = $this->getIdRequired(); $response->name = $this->getDisplayName(); $response->url = $adapter->getPublicUrl($this); @@ -430,7 +435,7 @@ class StationRemote implements Stringable, Interfaces\StationMountInterface, Int } if ($this->enable_autodj) { - return $this->autodj_bitrate . 'kbps ' . strtoupper($this->autodj_format); + return $this->autodj_bitrate . 'kbps ' . strtoupper($this->autodj_format ?? ''); } return Utilities\Strings::truncateUrl($this->url); diff --git a/src/Entity/StationRequest.php b/src/Entity/StationRequest.php index d2b24102c..147a34561 100644 --- a/src/Entity/StationRequest.php +++ b/src/Entity/StationRequest.php @@ -1,9 +1,10 @@ days = implode(',', (array)$days); + $this->days = implode(',', $days); } public function getLoopOnce(): bool @@ -238,13 +239,13 @@ class StationSchedule $now = CarbonImmutable::now(new DateTimeZone('UTC')); } - $timeCode = str_pad($timeCode, 4, '0', STR_PAD_LEFT); + $timeCode = str_pad((string)$timeCode, 4, '0', STR_PAD_LEFT); return $now->setTime((int)substr($timeCode, 0, 2), (int)substr($timeCode, 2)); } - public static function displayTimeCode($timeCode): string + public static function displayTimeCode(string|int $timeCode): string { - $timeCode = str_pad($timeCode, 4, '0', STR_PAD_LEFT); + $timeCode = str_pad((string)$timeCode, 4, '0', STR_PAD_LEFT); $hours = (int)substr($timeCode, 0, 2); $mins = substr($timeCode, 2); diff --git a/src/Entity/StationStreamer.php b/src/Entity/StationStreamer.php index 4845b750c..62ab43fcc 100644 --- a/src/Entity/StationStreamer.php +++ b/src/Entity/StationStreamer.php @@ -1,10 +1,9 @@ streamer_password = password_hash($streamer_password, PASSWORD_ARGON2ID); diff --git a/src/Entity/StationStreamerBroadcast.php b/src/Entity/StationStreamerBroadcast.php index 484569fb9..1c1d86a8a 100644 --- a/src/Entity/StationStreamerBroadcast.php +++ b/src/Entity/StationStreamerBroadcast.php @@ -1,9 +1,10 @@ 'utf8mb4', 'collate' => 'utf8mb4_unicode_ci']), Attributes\Auditable ] -class StationWebhook implements Stringable, Interfaces\StationCloneAwareInterface +class StationWebhook implements + Stringable, + Interfaces\StationCloneAwareInterface, + Interfaces\IdentifiableEntityInterface { use Traits\HasAutoIncrementId; use Traits\TruncateStrings; @@ -94,7 +97,7 @@ class StationWebhook implements Stringable, Interfaces\StationCloneAwareInterfac #[Attributes\AuditIgnore] protected ?array $metadata = null; - public function __construct(Station $station, $type) + public function __construct(Station $station, string $type) { $this->station = $station; $this->type = $type; diff --git a/src/Entity/StorageLocation.php b/src/Entity/StorageLocation.php index 6e0db44c6..97f3aa6e3 100644 --- a/src/Entity/StorageLocation.php +++ b/src/Entity/StorageLocation.php @@ -1,9 +1,10 @@ adapter; } - public function getPath(): ?string + public function getPath(): string { return $this->path; } - public function getFilteredPath(): ?string + public function getFilteredPath(): string { return match ($this->adapter) { self::ADAPTER_S3, self::ADAPTER_DROPBOX => trim($this->path, '/'), @@ -143,9 +144,9 @@ class StorageLocation implements Stringable return $this->path . $suffix; } - public function setPath(?string $path): void + public function setPath(string $path): void { - $this->path = $this->truncateNullableString($path); + $this->path = $this->truncateString($path); } public function getS3CredentialKey(): ?string @@ -383,14 +384,19 @@ class StorageLocation implements Stringable { $path = $this->applyPath($suffix); + $bucket = $this->s3Bucket; + if (null === $bucket) { + return 'No S3 Bucket Specified'; + } + try { $client = $this->getS3Client(); if (empty($path)) { - $objectUrl = $client->getObjectUrl($this->s3Bucket, '/'); + $objectUrl = $client->getObjectUrl($bucket, '/'); return rtrim($objectUrl, '/'); } - return $client->getObjectUrl($this->s3Bucket, ltrim($path, '/')); + return $client->getObjectUrl($bucket, ltrim($path, '/')); } catch (InvalidArgumentException $e) { return 'Invalid URI (' . $e->getMessage() . ')'; } @@ -416,11 +422,20 @@ class StorageLocation implements Stringable { $filteredPath = $this->getFilteredPath(); - return match ($this->adapter) { - self::ADAPTER_S3 => new AwsS3Adapter($this->getS3Client(), $this->s3Bucket, $filteredPath), - self::ADAPTER_DROPBOX => new DropboxAdapter($this->getDropboxClient(), $filteredPath), - default => new LocalFilesystemAdapter($filteredPath) - }; + switch ($this->adapter) { + case self::ADAPTER_S3: + $bucket = $this->s3Bucket; + if (null === $bucket) { + throw new \RuntimeException('Amazon S3 bucket is empty.'); + } + return new AwsS3Adapter($this->getS3Client(), $bucket, $filteredPath); + + case self::ADAPTER_DROPBOX: + return new DropboxAdapter($this->getDropboxClient(), $filteredPath); + + default: + return new LocalFilesystemAdapter($filteredPath); + } } protected function getS3Client(): S3Client diff --git a/src/Entity/Traits/HasAutoIncrementId.php b/src/Entity/Traits/HasAutoIncrementId.php index 38c4e0d78..6f02b92eb 100644 --- a/src/Entity/Traits/HasAutoIncrementId.php +++ b/src/Entity/Traits/HasAutoIncrementId.php @@ -1,5 +1,7 @@ id; } + + public function getIdRequired(): int + { + if (null === $this->id) { + throw new \RuntimeException('An ID was not generated for this object.'); + } + + return $this->id; + } } diff --git a/src/Entity/Traits/HasSongFields.php b/src/Entity/Traits/HasSongFields.php index a25a211e2..312c8e1a9 100644 --- a/src/Entity/Traits/HasSongFields.php +++ b/src/Entity/Traits/HasSongFields.php @@ -1,5 +1,7 @@ song_id = Song::getSongHash($this->getText()); + $text = $this->getText(); + $this->song_id = (null !== $text) + ? Song::getSongHash($text) + : Song::createOffline()->getSongId(); } public function getText(): ?string @@ -65,7 +70,7 @@ trait HasSongFields $oldText = $this->text; $this->text = $this->truncateNullableString($text, 303); - if (0 !== strcmp($oldText, $this->text)) { + if (0 !== strcmp($oldText ?? '', $this->text ?? '')) { $this->updateSongId(); } } @@ -80,7 +85,7 @@ trait HasSongFields $oldArtist = $this->artist; $this->artist = $this->truncateNullableString($artist, 150); - if (0 !== strcmp($oldArtist, $this->artist)) { + if (0 !== strcmp($oldArtist ?? '', $this->artist ?? '')) { $this->setTextFromArtistAndTitle(); } } @@ -95,7 +100,7 @@ trait HasSongFields $oldTitle = $this->title; $this->title = $this->truncateNullableString($title, 150); - if (0 !== strcmp($oldTitle, $this->title)) { + if (0 !== strcmp($oldTitle ?? '', $this->title ?? '')) { $this->setTextFromArtistAndTitle(); } } diff --git a/src/Entity/Traits/HasSplitTokenFields.php b/src/Entity/Traits/HasSplitTokenFields.php index d2ffdbbc3..d91d2436f 100644 --- a/src/Entity/Traits/HasSplitTokenFields.php +++ b/src/Entity/Traits/HasSplitTokenFields.php @@ -1,5 +1,7 @@ id; } + + public function getIdRequired(): string + { + if (null === $this->id) { + throw new \RuntimeException('An ID was not generated for this object.'); + } + + return $this->id; + } } diff --git a/src/Entity/Traits/TruncateInts.php b/src/Entity/Traits/TruncateInts.php index 5a2a73811..4c0585944 100644 --- a/src/Entity/Traits/TruncateInts.php +++ b/src/Entity/Traits/TruncateInts.php @@ -1,5 +1,7 @@ name = $this->truncateNullableString($name, 100); } - public function getEmail(): ?string + public function getEmail(): string { return $this->email; } - public function setEmail(?string $email = null): void + public function setEmail(string $email): void { - $this->email = $this->truncateNullableString($email, 100); + $this->email = $this->truncateString($email, 100); } public function verifyPassword(string $password): bool @@ -154,9 +156,9 @@ class User implements Stringable return [PASSWORD_BCRYPT, []]; } - public function setNewPassword(string $password): void + public function setNewPassword(?string $password): void { - if (trim($password)) { + if (null !== $password && trim($password)) { [$algo, $algo_opts] = $this->getPasswordAlgorithm(); $this->auth_password = password_hash($password, $algo, $algo_opts); } @@ -164,7 +166,7 @@ class User implements Stringable public function generateRandomPassword(): void { - $this->setNewPassword(bin2hex(random_bytes(20))); + $this->setNewPassword(Strings::generatePassword()); } public function getLocale(): ?string @@ -217,7 +219,7 @@ class User implements Stringable } /** - * @return Collection|Role[] + * @return Collection */ public function getRoles(): Collection { @@ -225,7 +227,7 @@ class User implements Stringable } /** - * @return Collection|ApiKey[] + * @return Collection */ public function getApiKeys(): Collection { diff --git a/src/Entity/UserLoginToken.php b/src/Entity/UserLoginToken.php index d44a19f41..39a121bf1 100644 --- a/src/Entity/UserLoginToken.php +++ b/src/Entity/UserLoginToken.php @@ -1,6 +1,6 @@ data; } - protected function envToBool(string|bool $value): bool + protected function envToBool(string|bool|int $value): bool { if (is_bool($value)) { return $value; } + if (is_int($value)) { + return 0 !== $value; + } return str_starts_with(strtolower($value), 'y') || 'true' === strtolower($value) diff --git a/src/Event/AbstractBuildMenu.php b/src/Event/AbstractBuildMenu.php index 413d7a0af..34e91dffe 100644 --- a/src/Event/AbstractBuildMenu.php +++ b/src/Event/AbstractBuildMenu.php @@ -1,5 +1,7 @@ The fully-qualified (::class) class name of the entity being managed. */ protected string $entityClass; /** @var array The default context sent to form normalization/denormalization functions. */ @@ -42,11 +47,17 @@ class EntityForm extends Form parent::__construct($options, $defaults); } + /** + * @return class-string + */ public function getEntityClass(): string { return $this->entityClass; } + /** + * @param class-string $entityClass + */ public function setEntityClass(string $entityClass): void { $this->entityClass = $entityClass; @@ -57,6 +68,9 @@ class EntityForm extends Form return $this->em; } + /** + * @return ObjectRepository + */ public function getEntityRepository(): ObjectRepository { if (!isset($this->entityClass)) { @@ -68,11 +82,11 @@ class EntityForm extends Form /** * @param ServerRequest $request - * @param object|null $record + * @param TEntity|null $record * - * @return object|bool The modified object if edited/created, or `false` if not processed. + * @return TEntity|bool The modified object if edited/created, or `false` if not processed. */ - public function process(ServerRequest $request, $record = null): object|bool + public function process(ServerRequest $request, ?object $record = null): object|bool { if (!isset($this->entityClass)) { throw new Exception('Entity class name is not specified.'); @@ -88,7 +102,7 @@ class EntityForm extends Form } // Handle submission. - if ('POST' === $request->getMethod() && $this->isValid($request->getParsedBody())) { + if ($this->isValid($request)) { $data = $this->getValues(); $record = $this->denormalizeToRecord($data, $record); @@ -100,9 +114,9 @@ class EntityForm extends Form $field_name = $error->getPropertyPath(); if (isset($this->fields[$field_name])) { - $this->fields[$field_name]->addError($error->getMessage()); + $this->fields[$field_name]->addError((string)$error->getMessage()); } else { - $this->addError($error->getMessage()); + $this->addError((string)$error->getMessage()); } } return false; @@ -125,7 +139,7 @@ class EntityForm extends Form /** * The old ->toArray(). * - * @param object $record + * @param TEntity $record * @param array $context * * @return mixed[] @@ -147,17 +161,17 @@ class EntityForm extends Form ) { return $this->displayShortenedObject($innerObject); }, - ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ( - $object, - string $format = null, - array $context = [] - ) { - return $this->displayShortenedObject($object); - }, + ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ( + $object, + string $format = null, + array $context = [] + ) { + return $this->displayShortenedObject($object); + }, ] ); - return $this->serializer->normalize($record, null, $context); + return (array)$this->serializer->normalize($record, null, $context); } /** @@ -170,17 +184,27 @@ class EntityForm extends Form return $object->getName(); } - return $object->getId(); + if ($object instanceof IdentifiableEntityInterface) { + return $object->getIdRequired(); + } + + if ($object instanceof \Stringable) { + return (string)$object; + } + + return get_class($object) . ': ' . spl_object_hash($object); } /** * The old ->fromArray(). * * @param array $data - * @param object|null $record + * @param TEntity|null $record * @param array $context + * + * @return TEntity */ - protected function denormalizeToRecord(array $data, $record = null, array $context = []): object + protected function denormalizeToRecord(array $data, ?object $record = null, array $context = []): object { $context = array_merge($this->defaultContext, $context); diff --git a/src/Form/Field/Csrf.php b/src/Form/Field/Csrf.php index 8a59f41bc..7f5d6de21 100644 --- a/src/Form/Field/Csrf.php +++ b/src/Form/Field/Csrf.php @@ -1,5 +1,7 @@ + */ class PermissionsForm extends EntityForm { protected bool $set_permissions = true; diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index d06474278..b4619265f 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -1,5 +1,7 @@ getMethod() && $this->isValid($request->getParsedBody())) { + if ($this->isValid($request)) { $data = $this->getValues(); $toClone = $data['clone']; diff --git a/src/Form/StationForm.php b/src/Form/StationForm.php index 85881b17f..451999737 100644 --- a/src/Form/StationForm.php +++ b/src/Form/StationForm.php @@ -1,5 +1,7 @@ + */ class StationForm extends EntityForm { public function __construct( @@ -75,7 +80,7 @@ class StationForm extends EntityForm if (!isset($installedFrontends[Adapters::FRONTEND_SHOUTCAST])) { $frontendDesc = __( 'Want to use SHOUTcast 2? Install it here, then reload this page.', - $request->getRouter()->named('admin:install_shoutcast:index') + (string)$request->getRouter()->named('admin:install_shoutcast:index') ); $this->getField('frontend_type')->setOption('description', $frontendDesc); @@ -94,7 +99,7 @@ class StationForm extends EntityForm if ($canSeeAdministration) { $storageLocationsDesc = __( 'Manage storage locations and storage quota here.', - $request->getRouter()->named('admin:storage_locations:index') + (string)$request->getRouter()->named('admin:storage_locations:index') ); if ($this->hasField('media_storage_location_id')) { @@ -137,7 +142,7 @@ class StationForm extends EntityForm } } - if ('POST' === $request->getMethod() && $this->isValid($request->getParsedBody())) { + if ($this->isValid($request)) { $data = $this->getValues(); /** @var Entity\Station $record */ @@ -145,28 +150,40 @@ class StationForm extends EntityForm if ($canSeeAdministration) { if (!empty($data['media_storage_location_id'])) { - $record->setMediaStorageLocation( - $this->storageLocationRepo->findByType( - Entity\StorageLocation::TYPE_STATION_MEDIA, - $data['media_storage_location_id'] - ) + $sl = $this->storageLocationRepo->findByType( + Entity\StorageLocation::TYPE_STATION_MEDIA, + (int)$data['media_storage_location_id'] ); + + if (null === $sl) { + $this->addError('Media storage location not found.'); + } else { + $record->setMediaStorageLocation($sl); + } } if (!empty($data['recordings_storage_location_id'])) { - $record->setRecordingsStorageLocation( - $this->storageLocationRepo->findByType( - Entity\StorageLocation::TYPE_STATION_RECORDINGS, - $data['recordings_storage_location_id'] - ) + $sl = $this->storageLocationRepo->findByType( + Entity\StorageLocation::TYPE_STATION_RECORDINGS, + (int)$data['recordings_storage_location_id'] ); + + if (null === $sl) { + $this->addError('Recordings storage location not found.'); + } else { + $record->setRecordingsStorageLocation($sl); + } } if (!empty($data['podcasts_storage_location_id'])) { - $record->setPodcastsStorageLocation( - $this->storageLocationRepo->findByType( - Entity\StorageLocation::TYPE_STATION_PODCASTS, - $data['podcasts_storage_location_id'] - ) + $sl = $this->storageLocationRepo->findByType( + Entity\StorageLocation::TYPE_STATION_PODCASTS, + (int)$data['podcasts_storage_location_id'] ); + + if (null === $sl) { + $this->addError('Podcasts storage location not found.'); + } else { + $record->setPodcastsStorageLocation($sl); + } } } @@ -177,9 +194,9 @@ class StationForm extends EntityForm $field_name = $error->getPropertyPath(); if (isset($this->fields[$field_name])) { - $this->fields[$field_name]->addError($error->getMessage()); + $this->fields[$field_name]->addError((string)$error->getMessage()); } else { - $this->addError($error->getMessage()); + $this->addError((string)$error->getMessage()); } } return false; diff --git a/src/Form/StationRemoteForm.php b/src/Form/StationRemoteForm.php index 02ae1f9c3..0f88a7841 100644 --- a/src/Form/StationRemoteForm.php +++ b/src/Form/StationRemoteForm.php @@ -1,5 +1,7 @@ getPrevious(); + $exception = $exception->getPrevious() ?? $exception; } if ($exception instanceof Exception) { @@ -253,12 +255,4 @@ class ErrorHandler extends \Slim\Handlers\ErrorHandler return parent::respond(); } } - - protected function withJson(ResponseInterface $response, $data): ResponseInterface - { - $json = (string)json_encode($data, JSON_THROW_ON_ERROR); - $response->getBody()->write($json); - - return $response->withHeader('Content-Type', 'application/json;charset=utf-8'); - } } diff --git a/src/Http/Factory/ResponseFactory.php b/src/Http/Factory/ResponseFactory.php index d87f13219..5cff1192e 100644 --- a/src/Http/Factory/ResponseFactory.php +++ b/src/Http/Factory/ResponseFactory.php @@ -1,5 +1,7 @@ withHeader('Pragma', 'public') ->withHeader('Expires', '0') ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->withHeader('Content-Type', mime_content_type($file_path)) + ->withHeader('Content-Type', mime_content_type($file_path) ?: '') ->withHeader('Content-Length', (string)filesize($file_path)) ->withHeader('Content-Disposition', 'attachment; filename=' . $file_name) ->withBody($stream); @@ -117,7 +119,7 @@ final class Response extends \Slim\Http\Response * * @return static */ - public function renderStringAsFile(string $file_data, string $content_type, $file_name = null): static + public function renderStringAsFile(string $file_data, string $content_type, ?string $file_name = null): static { $response = $this->response ->withHeader('Pragma', 'public') @@ -189,7 +191,7 @@ final class Response extends \Slim\Http\Response } $response = $this->withHeader('Content-Disposition', $disposition) - ->withHeader('Content-Length', $fileMeta->fileSize()) + ->withHeader('Content-Length', (string)$fileMeta->fileSize()) ->withHeader('X-Accel-Buffering', 'no'); $adapter = $filesystem->getAdapter(); @@ -206,12 +208,12 @@ final class Response extends \Slim\Http\Response if (str_starts_with($localPath, $diskPath)) { $accelPath = str_replace($diskPath, $nginxPath, $localPath); - return $response->withHeader('Content-Type', $fileMeta->mimeType()) + return $response->withHeader('Content-Type', $fileMeta->mimeType() ?? '') ->withHeader('X-Accel-Redirect', $accelPath); } } } - return $response->withFile($filesystem->readStream($path), $fileMeta->mimeType()); + return $response->withFile($filesystem->readStream($path), $fileMeta->mimeType() ?? ''); } } diff --git a/src/Http/Router.php b/src/Http/Router.php index db54f5202..55ba0b952 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -1,5 +1,7 @@ request = $request; $this->baseUrl = null; - $this->currentRequest = $request; } public function getBaseUrl(bool $useRequest = true): UriInterface @@ -62,8 +62,8 @@ class Router implements RouterInterface $useHttps = $settings->getAlwaysUseSsl(); - if ($this->currentRequest instanceof ServerRequestInterface) { - $currentUri = $this->currentRequest->getUri(); + if ($this->request instanceof ServerRequestInterface) { + $currentUri = $this->request->getUri(); if ('https' === $currentUri->getScheme()) { $useHttps = true; @@ -99,54 +99,44 @@ class Router implements RouterInterface } /** - * Same as $this->fromHere(), but merging the current GET query parameters into the request as well. - * - * @param null $route_name - * @param array $route_params - * @param array $query_params - * @param bool $absolute + * @inheritDoc */ public function fromHereWithQuery( - $route_name = null, + ?string $route_name = null, array $route_params = [], array $query_params = [], - $absolute = false - ): string { - if ($this->currentRequest instanceof ServerRequestInterface) { - $query_params = array_merge($this->currentRequest->getQueryParams(), $query_params); + bool $absolute = false + ): UriInterface { + if ($this->request instanceof ServerRequestInterface) { + $query_params = array_merge($this->request->getQueryParams(), $query_params); } return $this->fromHere($route_name, $route_params, $query_params, $absolute); } /** - * Return a named route based on the current page and its route arguments. - * - * @param null $route_name - * @param array $route_params - * @param array $query_params - * @param bool $absolute + * @inheritDoc */ public function fromHere( - $route_name = null, + ?string $route_name = null, array $route_params = [], array $query_params = [], - $absolute = false - ): string { - if ($this->currentRequest instanceof ServerRequestInterface) { - $route = RouteContext::fromRequest($this->currentRequest)->getRoute(); + bool $absolute = false + ): UriInterface { + if ($this->request instanceof ServerRequestInterface) { + $route = RouteContext::fromRequest($this->request)->getRoute(); } else { $route = null; } - if ($route_name === null) { - if ($route instanceof RouteInterface) { - $route_name = $route->getName(); - } else { - throw new InvalidArgumentException( - 'Cannot specify a null route name if no existing route is configured.' - ); - } + if (null === $route_name && $route instanceof RouteInterface) { + $route_name = $route->getName(); + } + + if (null === $route_name) { + throw new InvalidArgumentException( + 'Cannot specify a null route name if no existing route is configured.' + ); } if ($route instanceof RouteInterface) { @@ -157,18 +147,13 @@ class Router implements RouterInterface } /** - * Simpler format for calling "named" routes with parameters. - * - * @param string $route_name - * @param array $route_params - * @param array $query_params - * @param boolean $absolute Whether to include the full URL. + * @inheritDoc */ public function named( string $route_name, - $route_params = [], + array $route_params = [], array $query_params = [], - $absolute = false + bool $absolute = false ): UriInterface { return self::resolveUri( $this->getBaseUrl(), diff --git a/src/Http/RouterInterface.php b/src/Http/RouterInterface.php index 46f449a1e..7f3a73d60 100644 --- a/src/Http/RouterInterface.php +++ b/src/Http/RouterInterface.php @@ -1,5 +1,7 @@ fromHere(), but merging the current GET query parameters into the request as well. @@ -52,9 +54,9 @@ interface RouterInterface * @param bool $absolute */ public function fromHereWithQuery( - $route_name = null, + ?string $route_name = null, array $route_params = [], array $query_params = [], - $absolute = false - ): string; + bool $absolute = false + ): UriInterface; } diff --git a/src/Http/ServerRequest.php b/src/Http/ServerRequest.php index deb7de697..8dc1d5b70 100644 --- a/src/Http/ServerRequest.php +++ b/src/Http/ServerRequest.php @@ -1,5 +1,7 @@ serverRequest->getServerParams(); @@ -165,7 +167,11 @@ final class ServerRequest extends \Slim\Http\ServerRequest ?? $params['HTTP_FORWARDED_FOR'] ?? $params['HTTP_FORWARDED'] ?? $params['REMOTE_ADDR'] - ?? ''; + ?? null; + + if (null === $ip) { + throw new \RuntimeException('No IP address attached to this request.'); + } // Handle the IP being separated by commas. $ipParts = explode(',', $ip); diff --git a/src/Installer/Command/InstallCommand.php b/src/Installer/Command/InstallCommand.php index be66f7bdc..c24fb87c7 100644 --- a/src/Installer/Command/InstallCommand.php +++ b/src/Installer/Command/InstallCommand.php @@ -1,5 +1,7 @@ ask( $envConfig[$port]['name'] . ' - ' . $envConfig[$port]['description'], - (int)$env[$port] + (string)$env[$port] ); } $azuracastEnv[Environment::AUTO_ASSIGN_PORT_MIN] = (int)$io->ask( $azuracastEnvConfig[Environment::AUTO_ASSIGN_PORT_MIN]['name'], - (int)$azuracastEnv[Environment::AUTO_ASSIGN_PORT_MIN] + (string)$azuracastEnv[Environment::AUTO_ASSIGN_PORT_MIN] ); $azuracastEnv[Environment::AUTO_ASSIGN_PORT_MAX] = (int)$io->ask( $azuracastEnvConfig[Environment::AUTO_ASSIGN_PORT_MAX]['name'], - (int)$azuracastEnv[Environment::AUTO_ASSIGN_PORT_MAX] + (string)$azuracastEnv[Environment::AUTO_ASSIGN_PORT_MAX] ); $stationPorts = Configuration::enumerateDefaultPorts( diff --git a/src/Installer/EnvFiles/AbstractEnvFile.php b/src/Installer/EnvFiles/AbstractEnvFile.php index 8f8f5afbe..001f93f8c 100644 --- a/src/Installer/EnvFiles/AbstractEnvFile.php +++ b/src/Installer/EnvFiles/AbstractEnvFile.php @@ -1,13 +1,19 @@ + */ +abstract class AbstractEnvFile implements ArrayAccess { final public function __construct( protected string $path, @@ -39,22 +45,22 @@ abstract class AbstractEnvFile implements \ArrayAccess $this->data = array_merge($defaults, $currentVars); } - public function offsetExists($offset): bool + public function offsetExists(mixed $offset): bool { return isset($this->data[$offset]); } - public function offsetGet($offset): mixed + public function offsetGet(mixed $offset): mixed { return $this->data[$offset]; } - public function offsetSet($offset, $value): void + public function offsetSet(mixed $offset, mixed $value): void { $this->data[$offset] = $value; } - public function offsetUnset($offset): void + public function offsetUnset(mixed $offset): void { unset($this->data[$offset]); } diff --git a/src/Installer/EnvFiles/AzuraCastEnvFile.php b/src/Installer/EnvFiles/AzuraCastEnvFile.php index 4a59d6f25..ab04bff7c 100644 --- a/src/Installer/EnvFiles/AzuraCastEnvFile.php +++ b/src/Installer/EnvFiles/AzuraCastEnvFile.php @@ -1,5 +1,7 @@ getServerParams(); - $browser_locale = \Locale::acceptFromHttp($server_params['HTTP_ACCEPT_LANGUAGE'] ?? null); + $browser_locale = \Locale::acceptFromHttp($server_params['HTTP_ACCEPT_LANGUAGE'] ?? ''); if (!empty($browser_locale)) { if (2 === strlen($browser_locale)) { @@ -136,7 +138,10 @@ class Locale } // Attempt to load from environment variable. - $possibleLocales[] = $environment->getLang(); + $envLang = $environment->getLang(); + if (null !== $envLang) { + $possibleLocales[] = $envLang; + } return new self($environment, $possibleLocales); } @@ -146,7 +151,7 @@ class Locale ): self { return new self( $environment, - $environment->getLang() + $environment->getLang() ?? self::DEFAULT_LOCALE ); } diff --git a/src/LockFactory.php b/src/LockFactory.php index c4d607432..81786a3f1 100644 --- a/src/LockFactory.php +++ b/src/LockFactory.php @@ -1,5 +1,7 @@ createLock($resource, $ttl, $autoRelease); if ($force) { diff --git a/src/Media/AlbumArtHandler/AbstractAlbumArtHandler.php b/src/Media/AlbumArtHandler/AbstractAlbumArtHandler.php index 0e7ce5c5c..7301e206e 100644 --- a/src/Media/AlbumArtHandler/AbstractAlbumArtHandler.php +++ b/src/Media/AlbumArtHandler/AbstractAlbumArtHandler.php @@ -1,5 +1,7 @@ readMetadata($event->getPath()); $event->setMetadata($metadata); } elseif ($event instanceof WriteMetadata) { - if ($this->writeMetadata($event->getMetadata(), $event->getPath())) { + $metadata = $event->getMetadata(); + if (null !== $metadata && $this->writeMetadata($metadata, $event->getPath())) { $event->stopPropagation(); } } @@ -50,7 +53,9 @@ class GetId3MetadataService $metadata = new Entity\Metadata(); if (is_numeric($info['playtime_seconds'])) { - $metadata->setDuration($info['playtime_seconds']); + $metadata->setDuration( + Utilities\Time::displayTimeToSeconds($info['playtime_seconds']) ?? 0.0 + ); } $metaTags = $metadata->getTags(); diff --git a/src/Media/MimeType.php b/src/Media/MimeType.php index fefc6c446..d741e1473 100644 --- a/src/Media/MimeType.php +++ b/src/Media/MimeType.php @@ -1,5 +1,7 @@ getText(), 'AzuraCast')) { + if (false !== mb_stripos($song->getText() ?? '', 'AzuraCast')) { return null; } diff --git a/src/Message/AbstractMessage.php b/src/Message/AbstractMessage.php index efd6d351a..60a86000f 100644 --- a/src/Message/AbstractMessage.php +++ b/src/Message/AbstractMessage.php @@ -1,5 +1,7 @@ */ public array $triggers = []; public function getIdentifier(): string diff --git a/src/Message/ReprocessMediaMessage.php b/src/Message/ReprocessMediaMessage.php index 78a9813cc..81cee262c 100644 --- a/src/Message/ReprocessMediaMessage.php +++ b/src/Message/ReprocessMediaMessage.php @@ -1,5 +1,7 @@ */ public function getMessagesInTransport(string $queueName): Generator { foreach ($this->getTransport($queueName)->all() as $envelope) { - yield $envelope->getMessage(); + $message = $envelope->getMessage(); + if ($message instanceof AbstractMessage) { + yield $message; + } } } diff --git a/src/MessageQueue/ResetArrayCacheMiddleware.php b/src/MessageQueue/ResetArrayCacheMiddleware.php index 9d7f9b416..3824b6a61 100644 --- a/src/MessageQueue/ResetArrayCacheMiddleware.php +++ b/src/MessageQueue/ResetArrayCacheMiddleware.php @@ -1,5 +1,7 @@ getRoute(); if ($current_route instanceof RouteInterface) { - $route_parts = explode(':', $current_route->getName()); + $route_parts = explode(':', $current_route->getName() ?? ''); $active_tab = $route_parts[1]; } diff --git a/src/Middleware/Module/Api.php b/src/Middleware/Module/Api.php index 9eb3edd7c..d4e56db23 100644 --- a/src/Middleware/Module/Api.php +++ b/src/Middleware/Module/Api.php @@ -1,5 +1,7 @@ getBaseUrl(); + + $baseUrl = $settings->getBaseUrl(); + if (null !== $baseUrl) { + $rawOrigins[] = $baseUrl; + } $origins = []; foreach ($rawOrigins as $rawOrigin) { diff --git a/src/Middleware/Module/StationFiles.php b/src/Middleware/Module/StationFiles.php index faa673dcb..9ed5a2b6e 100644 --- a/src/Middleware/Module/StationFiles.php +++ b/src/Middleware/Module/StationFiles.php @@ -1,5 +1,7 @@ getRoute(); if ($current_route instanceof RouteInterface) { - $route_parts = explode(':', $current_route->getName()); + $route_parts = explode(':', $current_route->getName() ?? ''); $active_tab = $route_parts[1]; } diff --git a/src/Middleware/Permissions.php b/src/Middleware/Permissions.php index 150ebefc1..dc6fca747 100644 --- a/src/Middleware/Permissions.php +++ b/src/Middleware/Permissions.php @@ -1,5 +1,7 @@ inflector = InflectorFactory::create()->build(); } - /** - * @param DenormalizerInterface|NormalizerInterface|SerializerInterface $serializer - */ - public function setSerializer($serializer): void - { - if (!$serializer instanceof DenormalizerInterface || !$serializer instanceof NormalizerInterface) { - throw new InvalidArgumentException( - 'Expected a serializer that also implements DenormalizerInterface and NormalizerInterface.' - ); - } - - $this->serializer = $serializer; - } - /** * Replicates the "toArray" functionality previously present in Doctrine 1. * @@ -113,7 +97,7 @@ class DoctrineEntityNormalizer extends AbstractNormalizer * Replicates the "fromArray" functionality previously present in Doctrine 1. * * @param mixed $data - * @param string $type + * @param class-string $type * @param string|null $format * @param array $context */ @@ -184,15 +168,20 @@ class DoctrineEntityNormalizer extends AbstractNormalizer } /** - * @param object|string $classOrObject + * @param object|class-string $classOrObject * @param array $context * @param bool $attributesAsString * - * @return bool|string[]|AttributeMetadataInterface[] */ - protected function getAllowedAttributes($classOrObject, array $context, $attributesAsString = false): bool|array - { - $meta = $this->classMetadataFactory->getMetadataFor($classOrObject)->getAttributesMetadata(); + protected function getAllowedAttributes( + $classOrObject, + array $context, + bool $attributesAsString = false + ): array|false { + $meta = $this->classMetadataFactory?->getMetadataFor($classOrObject)?->getAttributesMetadata(); + if (null === $meta) { + throw new \RuntimeException('Class metadata factory not specified.'); + } $props_raw = (new ReflectionClass($classOrObject))->getProperties( ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED @@ -222,15 +211,12 @@ class DoctrineEntityNormalizer extends AbstractNormalizer return $allowedAttributes; } - /** - * @param object $object - * @param string $prop_name - * @param null $format - * @param array $context - * - */ - protected function getAttributeValue(object $object, string $prop_name, $format = null, array $context = []): mixed - { + protected function getAttributeValue( + object $object, + string $prop_name, + string $format = null, + array $context = [] + ): mixed { $form_mode = $context[self::NORMALIZE_TO_IDENTIFIERS] ?? false; if (isset($context[self::CLASS_METADATA]->associationMappings[$prop_name])) { @@ -268,14 +254,14 @@ class DoctrineEntityNormalizer extends AbstractNormalizer $return_val[] = $this->getProperty($val_obj, $id_field[0]); } } else { - $return_val[] = $this->serializer->normalize($val_obj, $format, $context); + $return_val[] = $this->normalizer->normalize($val_obj, $format, $context); } } } return $return_val; } - return $this->serializer->normalize($prop_val, $format, $context); + return $this->normalizer->normalize($prop_val, $format, $context); } $value = $this->getProperty($object, $prop_name); @@ -314,7 +300,7 @@ class DoctrineEntityNormalizer extends AbstractNormalizer * @param string $var * @param string $prefix */ - protected function getMethodName(string $var, $prefix = ''): string + protected function getMethodName(string $var, string $prefix = ''): string { return $this->inflector->camelize(($prefix ? $prefix . '_' : '') . $var); } @@ -330,7 +316,7 @@ class DoctrineEntityNormalizer extends AbstractNormalizer object $object, string $field, mixed $value, - $format = null, + ?string $format = null, array $context = [] ): void { if (isset($context[self::ASSOCIATION_MAPPINGS][$field])) { @@ -340,8 +326,12 @@ class DoctrineEntityNormalizer extends AbstractNormalizer if ('one' === $mapping['type']) { if (empty($value)) { $this->setProperty($object, $field, null); - } elseif (($field_item = $this->em->find($mapping['entity'], $value)) instanceof $mapping['entity']) { - $this->setProperty($object, $field, $field_item); + } else { + /** @var class-string $entity */ + $entity = $mapping['entity']; + if (($field_item = $this->em->find($entity, $value)) instanceof $entity) { + $this->setProperty($object, $field, $field_item); + } } } elseif ($mapping['is_owning_side']) { $collection = $this->getProperty($object, $field); @@ -351,8 +341,11 @@ class DoctrineEntityNormalizer extends AbstractNormalizer if ($value) { foreach ((array)$value as $field_id) { - $field_item = $this->em->find($mapping['entity'], $field_id); - if ($field_item instanceof $mapping['entity']) { + /** @var class-string $entity */ + $entity = $mapping['entity']; + + $field_item = $this->em->find($entity, $field_id); + if ($field_item instanceof $entity) { $collection->add($field_item); } } @@ -401,7 +394,7 @@ class DoctrineEntityNormalizer extends AbstractNormalizer } $dt = new DateTime(); - $dt->setTimestamp($value); + $dt->setTimestamp((int)$value); $value = $dt; } break; @@ -447,7 +440,9 @@ class DoctrineEntityNormalizer extends AbstractNormalizer $class = ($class instanceof Proxy || $class instanceof GhostObjectInterface) ? get_parent_class($class) : get_class($class); - } elseif (!is_string($class)) { + } + + if (!is_string($class)) { return false; } diff --git a/src/Notification/Check/ComposeVersionCheck.php b/src/Notification/Check/ComposeVersionCheck.php index 3101b262d..911d49ddc 100644 --- a/src/Notification/Check/ComposeVersionCheck.php +++ b/src/Notification/Check/ComposeVersionCheck.php @@ -1,5 +1,7 @@ inflector = InflectorFactory::create() ->build(); - $this->loadDirectory($base_dir); + $this->loadDirectory($baseDir); } - public function loadDirectory($dir): void + public function loadDirectory(string $dir): void { $plugins = (new Finder()) ->ignoreUnreadableDirs() diff --git a/src/Radio/AbstractAdapter.php b/src/Radio/AbstractAdapter.php index d34a99d0c..22d012b82 100644 --- a/src/Radio/AbstractAdapter.php +++ b/src/Radio/AbstractAdapter.php @@ -1,5 +1,7 @@ getStation()->getNowplaying(); if ($np instanceof Entity\Api\NowPlaying) { - $event->addAnnotations([ - 'title' => $np->now_playing->song->title, - 'artist' => $np->now_playing->song->artist, - 'playlist_id' => null, - 'media_id' => null, - ]); + $event->addAnnotations( + [ + 'title' => $np->now_playing?->song?->title, + 'artist' => $np->now_playing?->song?->artist, + 'playlist_id' => null, + 'media_id' => null, + ] + ); } } else { $event->addAnnotations([ diff --git a/src/Radio/AutoDJ/Queue.php b/src/Radio/AutoDJ/Queue.php index 4059edc34..19ed457ca 100644 --- a/src/Radio/AutoDJ/Queue.php +++ b/src/Radio/AutoDJ/Queue.php @@ -1,5 +1,7 @@ cache->get($queueCacheKey); if (empty($mediaQueue)) { - $playlistRaw = file_get_contents($playlist->getRemoteUrl()); - $mediaQueue = PlaylistParser::getSongs($playlistRaw); + $mediaQueue = []; + + $playlistRemoteUrl = $playlist->getRemoteUrl(); + if (null !== $playlistRemoteUrl) { + $playlistRaw = file_get_contents($playlistRemoteUrl); + if (false !== $playlistRaw) { + $mediaQueue = PlaylistParser::getSongs($playlistRaw); + } + } } $mediaId = null; diff --git a/src/Radio/AutoDJ/Scheduler.php b/src/Radio/AutoDJ/Scheduler.php index 726abbda6..cc5972c5a 100644 --- a/src/Radio/AutoDJ/Scheduler.php +++ b/src/Radio/AutoDJ/Scheduler.php @@ -1,5 +1,7 @@ equalTo($endTime)) { - if (!$this->wasPlaylistPlayedInLastXMinutes($schedule->getPlaylist(), $now, 30)) { + $playlist = $schedule->getPlaylist(); + if (null !== $playlist && !$this->wasPlaylistPlayedInLastXMinutes($playlist, $now, 30)) { return true; } } else { @@ -304,6 +307,11 @@ class Scheduler $playlist = $schedule->getPlaylist(); + if (null === $playlist) { + $this->logger->error('Attempting to check playlist loop status on a non-playlist-based schedule item.'); + return false; + } + $playlistPlayedAt = CarbonImmutable::createFromTimestamp( $playlist->getPlayedAt(), $now->getTimezone() @@ -358,20 +366,24 @@ class Scheduler $endDate = $schedule->getEndDate(); if (!empty($startDate)) { - $startDate = CarbonImmutable::createFromFormat('Y-m-d', $startDate, $now->getTimezone()) - ->setTime(0, 0); + $startDate = CarbonImmutable::createFromFormat('Y-m-d', $startDate, $now->getTimezone()); - if ($now->lt($startDate)) { - return false; + if (false !== $startDate) { + $startDate = $startDate->setTime(0, 0); + if ($now->lt($startDate)) { + return false; + } } } if (!empty($endDate)) { - $endDate = CarbonImmutable::createFromFormat('Y-m-d', $endDate, $now->getTimezone()) - ->setTime(23, 59, 59); + $endDate = CarbonImmutable::createFromFormat('Y-m-d', $endDate, $now->getTimezone()); - if ($now->gt($endDate)) { - return false; + if (false !== $endDate) { + $endDate = $endDate->setTime(23, 59, 59); + if ($now->gt($endDate)) { + return false; + } } } diff --git a/src/Radio/Backend/AbstractBackend.php b/src/Radio/Backend/AbstractBackend.php index 12cce8c64..327beccd7 100644 --- a/src/Radio/Backend/AbstractBackend.php +++ b/src/Radio/Backend/AbstractBackend.php @@ -1,5 +1,7 @@ command( $station, diff --git a/src/Radio/Backend/Liquidsoap/ConfigWriter.php b/src/Radio/Backend/Liquidsoap/ConfigWriter.php index ff3b67d35..b46da01c7 100644 --- a/src/Radio/Backend/Liquidsoap/ConfigWriter.php +++ b/src/Radio/Backend/Liquidsoap/ConfigWriter.php @@ -1,5 +1,7 @@ getRemoteUrl(); - $remote_url_scheme = parse_url($remote_url, PHP_URL_SCHEME); - $remote_url_function = ('https' === $remote_url_scheme) ? 'input.https' : 'input.http'; + if (null !== $remote_url) { + $remote_url_scheme = parse_url($remote_url, PHP_URL_SCHEME); + $remote_url_function = ('https' === $remote_url_scheme) ? 'input.https' : 'input.http'; - $buffer = $playlist->getRemoteBuffer(); - $buffer = ($buffer < 1) ? Entity\StationPlaylist::DEFAULT_REMOTE_BUFFER : $buffer; + $buffer = $playlist->getRemoteBuffer(); + $buffer = ($buffer < 1) ? Entity\StationPlaylist::DEFAULT_REMOTE_BUFFER : $buffer; - $playlistConfigLines[] = $playlistVarName . ' = mksafe(' . $remote_url_function + $playlistConfigLines[] = $playlistVarName . ' = mksafe(' . $remote_url_function . '(max=' . $buffer . '., "' . self::cleanUpString($remote_url) . '"))'; + } break; } } @@ -533,9 +537,9 @@ class ConfigWriter implements EventSubscriberInterface * @param Entity\StationPlaylist $playlist * @param bool $notify * - * @return string The full path that was written to. + * @return string|null The full path that was written to. */ - public function writePlaylistFile(Entity\StationPlaylist $playlist, $notify = true): ?string + public function writePlaylistFile(Entity\StationPlaylist $playlist, bool $notify = true): ?string { $station = $playlist->getStation(); @@ -675,7 +679,7 @@ class ConfigWriter implements EventSubscriberInterface * @param string $endpoint * @param array $params */ - protected function getApiUrlCommand(Entity\Station $station, string $endpoint, $params = []): string + protected function getApiUrlCommand(Entity\Station $station, string $endpoint, array $params = []): string { // Docker cURL-based API URL call with API authentication. if ($this->environment->isDocker()) { @@ -1003,7 +1007,7 @@ class ConfigWriter implements EventSubscriberInterface $charset = $station->getBackendConfig()->getCharset(); $output_format = $this->getOutputFormatString( - $mount->getAutodjFormat(), + $mount->getAutodjFormat() ?? $mount::FORMAT_MP3, $mount->getAutodjBitrate() ?? 128 ); @@ -1136,12 +1140,12 @@ class ConfigWriter implements EventSubscriberInterface * @param float|int|string $number * @param int $decimals */ - public static function toFloat(float|int|string $number, $decimals = 2): string + public static function toFloat(float|int|string $number, int $decimals = 2): string { return number_format((float)$number, $decimals, '.', ''); } - public static function formatTimeCode($time_code): string + public static function formatTimeCode(int $time_code): string { $hours = floor($time_code / 100); $mins = $time_code % 100; @@ -1170,11 +1174,11 @@ class ConfigWriter implements EventSubscriberInterface public static function cleanUpVarName(string $str): string { $str = strip_tags($str); - $str = preg_replace(['/[\r\n\t ]+/', '/[\"\*\/\:\<\>\?\'\|]+/'], ' ', $str); + $str = preg_replace(['/[\r\n\t ]+/', '/[\"\*\/\:\<\>\?\'\|]+/'], ' ', $str) ?? ''; $str = strtolower($str); $str = html_entity_decode($str, ENT_QUOTES, "utf-8"); $str = htmlentities($str, ENT_QUOTES, "utf-8"); - $str = preg_replace("/(&)([a-z])([a-z]+;)/i", '$2', $str); + $str = preg_replace("/(&)([a-z])([a-z]+;)/i", '$2', $str) ?? ''; $str = str_replace(' ', '_', $str); $str = rawurlencode($str); $str = str_replace(['%', '-'], ['', '_'], $str); diff --git a/src/Radio/Backend/None.php b/src/Radio/Backend/None.php index 4e7b5f5bb..f12f0dcfe 100644 --- a/src/Radio/Backend/None.php +++ b/src/Radio/Backend/None.php @@ -1,5 +1,7 @@ getId(); $affected_groups = $this->reloadSupervisor(); @@ -250,7 +252,7 @@ class Configuration * @param Station $station * @param bool $force */ - public function assignRadioPorts(Station $station, $force = false): void + public function assignRadioPorts(Station $station, bool $force = false): void { if ( $station->getFrontendType() !== Adapters::FRONTEND_REMOTE @@ -388,13 +390,13 @@ class Configuration protected function writeConfigurationSection( Station $station, AbstractAdapter $adapter, - $priority + ?int $priority ): string { [, $program_name] = explode(':', $adapter->getProgramName($station)); $config_lines = [ 'user' => 'azuracast', - 'priority' => $priority, + 'priority' => $priority ?? 50, 'command' => $adapter->getCommand($station), 'directory' => $station->getRadioConfigDir(), 'environment' => 'TZ="' . $station->getTimezone() . '"', diff --git a/src/Radio/Frontend/AbstractFrontend.php b/src/Radio/Frontend/AbstractFrontend.php index bed809d34..da067ba10 100644 --- a/src/Radio/Frontend/AbstractFrontend.php +++ b/src/Radio/Frontend/AbstractFrontend.php @@ -1,5 +1,7 @@ fromString('' . $custom_config_raw . ''); + $xmlConfig = (new Reader())->fromString('' . $custom_config_raw . ''); + return (false !== $xmlConfig) + ? (array)$xmlConfig + : false; } } catch (Exception $e) { $this->logger->error( diff --git a/src/Radio/Frontend/Icecast.php b/src/Radio/Frontend/Icecast.php index 6ed4f3e8d..88b242bef 100644 --- a/src/Radio/Frontend/Icecast.php +++ b/src/Radio/Frontend/Icecast.php @@ -1,5 +1,7 @@ getRelayUrl()) { - $relay_parts = parse_url($mount_row->getRelayUrl()); + $mountRelayUrl = $mount_row->getRelayUrl(); + if (null !== $mountRelayUrl) { + $mountRelayUri = new Uri($mountRelayUrl); $config['relay'][] = [ - 'server' => $relay_parts['host'], - 'port' => $relay_parts['port'], - 'mount' => $relay_parts['path'], + 'server' => $mountRelayUri->getHost(), + 'port' => $mountRelayUri->getPort(), + 'mount' => $mountRelayUri->getPath(), 'local-mount' => $mount_row->getName(), ]; } @@ -221,16 +224,16 @@ class Icecast extends AbstractFrontend if (!empty($customConfig)) { $customConfParsed = $this->processCustomConfig($customConfig); - // Special handling for aliases. - if (isset($customConfParsed['paths']['alias'])) { - $alias = (array)$customConfParsed['paths']['alias']; - if (!is_numeric(key($alias))) { - $alias = [$alias]; - } - $customConfParsed['paths']['alias'] = $alias; - } - if (false !== $customConfParsed) { + // Special handling for aliases. + if (isset($customConfParsed['paths']['alias'])) { + $alias = (array)$customConfParsed['paths']['alias']; + if (!is_numeric(key($alias))) { + $alias = [$alias]; + } + $customConfParsed['paths']['alias'] = $alias; + } + $config = Utilities\Arrays::arrayMergeRecursiveDistinct($config, $customConfParsed); } } diff --git a/src/Radio/Frontend/Remote.php b/src/Radio/Frontend/Remote.php index 868d29a8e..5df4cff9d 100644 --- a/src/Radio/Frontend/Remote.php +++ b/src/Radio/Frontend/Remote.php @@ -1,5 +1,7 @@ toInt(); } - public static function getReadableSize(Math\BigInteger $bytes, $decimals = 1): string + public static function getReadableSize(Math\BigInteger $bytes, int $decimals = 1): string { $bytes_str = (string)$bytes; @@ -39,7 +41,7 @@ class Quota return $bytes_str; } - public static function convertFromReadableSize($size): ?Math\BigInteger + public static function convertFromReadableSize(Math\BigInteger|string|null $size): ?Math\BigInteger { if ($size instanceof Math\BigInteger) { return $size; @@ -50,18 +52,20 @@ class Quota } // Remove the non-unit characters from the size. - $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); + $unit = preg_replace('/[^bkmgtpezy]/i', '', $size) ?? ''; // Remove the non-numeric characters from the size. - $size = preg_replace('/[^0-9\\.]/', '', $size); + $size = preg_replace('/[^0-9\\.]/', '', $size) ?? ''; if ($unit) { // Find the position of the unit in the ordered string which is the power // of magnitude to multiply a kilobyte by. + + /** @noinspection StringFragmentMisplacedInspection */ $byte_power = stripos( haystack: 'bkmgtpezy', - needle: $unit[0] - ); + needle: $unit[0] + ) ?: 0; $byte_multiplier = Math\BigInteger::of(1000)->power($byte_power); return Math\BigDecimal::of($size) diff --git a/src/Radio/Remote/AbstractRemote.php b/src/Radio/Remote/AbstractRemote.php index c15b44a40..8768b5b5e 100644 --- a/src/Radio/Remote/AbstractRemote.php +++ b/src/Radio/Remote/AbstractRemote.php @@ -1,5 +1,7 @@ getUrl()); diff --git a/src/Radio/Remote/AdapterProxy.php b/src/Radio/Remote/AdapterProxy.php index 42dabb7fe..898e7ff67 100644 --- a/src/Radio/Remote/AdapterProxy.php +++ b/src/Radio/Remote/AdapterProxy.php @@ -1,5 +1,7 @@ setListenersTotal($result->listeners->total); - $remote->setListenersUnique($result->listeners->unique); + $remote->setListenersUnique($result->listeners->unique ?? 0); $this->em->persist($remote); $this->em->flush(); @@ -87,6 +89,6 @@ class AzuraRelay extends AbstractRemote // Remove port number and other decorations. return (string)$base_url ->withPort($radio_port) - ->withPath($remote->getMount()); + ->withPath($remote->getMount() ?? ''); } } diff --git a/src/Radio/Remote/Icecast.php b/src/Radio/Remote/Icecast.php index a355abef6..d57250595 100644 --- a/src/Radio/Remote/Icecast.php +++ b/src/Radio/Remote/Icecast.php @@ -1,5 +1,7 @@ mustRun(); - if (!is_file($jsonOutPath)) { - throw new RuntimeException('Audio waveform JSON was not generated.'); - } - - $inputRaw = file_get_contents($jsonOutPath); - $input = json_decode($inputRaw, true, 512, JSON_THROW_ON_ERROR); + $input = Json::loadFromFile($jsonOutPath); // Limit all input to a range from 0 to 1. $data = $input['data']; diff --git a/src/Service/Avatar.php b/src/Service/Avatar.php index 6acdb7e73..915954f4b 100644 --- a/src/Service/Avatar.php +++ b/src/Service/Avatar.php @@ -1,5 +1,7 @@ getAvatarService(); $default = $this->settingsRepo->readSettings()->getAvatarDefaultUrl(); + if (empty($email)) { + return $default; + } + return $avatarService->getAvatar($email, $size, $default); } } diff --git a/src/Service/Avatar/AvatarServiceInterface.php b/src/Service/Avatar/AvatarServiceInterface.php index 1261e2511..0e8df4f37 100644 --- a/src/Service/Avatar/AvatarServiceInterface.php +++ b/src/Service/Avatar/AvatarServiceInterface.php @@ -1,5 +1,7 @@ getUploadedPath(); $fp = fopen($finalPath, 'wb+'); + if (false === $fp) { + throw new \RuntimeException( + sprintf( + 'Could not open final path "%s" for writing.', + $finalPath + ) + ); + } + for ($i = 1; $i <= $numChunks; $i++) { - fwrite($fp, file_get_contents($chunkBaseDir . '/' . $chunkIdentifier . '.part' . $i)); + $chunkContents = file_get_contents($chunkBaseDir . '/' . $chunkIdentifier . '.part' . $i); + if (empty($chunkContents)) { + throw new \RuntimeException( + sprintf( + 'Could not load chunk "%d" for writing.', + $i + ) + ); + } + + fwrite($fp, $chunkContents); } fclose($fp); @@ -215,7 +236,7 @@ class Flow protected static function rrmdir(string $dir): void { if (is_dir($dir)) { - $objects = array_diff(scandir($dir, SCANDIR_SORT_NONE), ['.', '..']); + $objects = array_diff(scandir($dir, SCANDIR_SORT_NONE) ?: [], ['.', '..']); foreach ($objects as $object) { if (is_dir($dir . '/' . $object)) { self::rrmdir($dir . '/' . $object); diff --git a/src/Service/Flow/UploadedFile.php b/src/Service/Flow/UploadedFile.php index ff7810831..ec5408994 100644 --- a/src/Service/Flow/UploadedFile.php +++ b/src/Service/Flow/UploadedFile.php @@ -18,8 +18,12 @@ final class UploadedFile implements \JsonSerializable ?string $tempDir ) { $tempDir ??= sys_get_temp_dir(); - $originalFilename ??= tempnam($tempDir, 'upload'); + + if (!$originalFilename || !$tempDir) { + throw new \RuntimeException('Could not generate original filename.'); + } + $this->originalFilename = self::filterOriginalFilename($originalFilename); if (null === $uploadedPath) { @@ -27,6 +31,9 @@ final class UploadedFile implements \JsonSerializable $this->uploadedPath = $tempDir . '/' . $prefix . '_' . $originalFilename; } else { $uploadedPath = realpath($uploadedPath); + if (false === $uploadedPath) { + throw new \InvalidArgumentException('Could not determine real path of specified path.'); + } if (!str_starts_with($uploadedPath, $tempDir)) { throw new \InvalidArgumentException('Uploaded path is not inside specified temporary directory.'); } @@ -51,7 +58,12 @@ final class UploadedFile implements \JsonSerializable public function getUploadedSize(): int { - return filesize($this->uploadedPath); + $size = filesize($this->uploadedPath); + if (false === $size) { + throw new \RuntimeException('Could not get file size of uploaded path.'); + } + + return $size; } public function readAndDeleteUploadedFile(): string @@ -59,7 +71,7 @@ final class UploadedFile implements \JsonSerializable $contents = file_get_contents($this->uploadedPath); @unlink($this->uploadedPath); - return $contents; + return $contents ?: ''; } /** @return mixed[] */ diff --git a/src/Service/IpGeolocation.php b/src/Service/IpGeolocation.php index c31fcf5c1..933374fff 100644 --- a/src/Service/IpGeolocation.php +++ b/src/Service/IpGeolocation.php @@ -1,5 +1,7 @@ initialize(); } - if (null === $this->reader) { + $reader = $this->reader; + if (null === $reader) { return [ 'status' => 'error', 'message' => $this->getAttribution(), @@ -86,12 +89,12 @@ class IpGeolocation $ipInfo = $this->cache->get( $cacheKey, - function (CacheItem $item) use ($ip) { + function (CacheItem $item) use ($ip, $reader) { /** @noinspection SummerTimeUnsafeTimeManipulationInspection */ $item->expiresAfter(86400 * 7); try { - $ipInfo = $this->reader->get($ip); + $ipInfo = $reader->get($ip); if (!empty($ipInfo)) { return $ipInfo; } @@ -125,7 +128,7 @@ class IpGeolocation ]; } - protected function getLocalizedString($names, string $locale): string + protected function getLocalizedString(?array $names, string $locale): string { if (empty($names)) { return ''; diff --git a/src/Service/IpGeolocator/AbstractIpGeolocator.php b/src/Service/IpGeolocator/AbstractIpGeolocator.php index ebdfec6a6..fa37e0c70 100644 --- a/src/Service/IpGeolocator/AbstractIpGeolocator.php +++ b/src/Service/IpGeolocator/AbstractIpGeolocator.php @@ -1,5 +1,7 @@ quoteQuery($song->getTitle()); + if (!empty($song->getTitle())) { + $query[] = $this->quoteQuery($song->getTitle()); + } if (!empty($song->getArtist())) { $query[] = 'artist:' . $this->quoteQuery($song->getArtist()); @@ -114,6 +118,10 @@ class MusicBrainz } } + if (empty($query)) { + return []; + } + $response = $this->makeRequest( 'recording/', [ diff --git a/src/Service/NChan.php b/src/Service/NChan.php index 1e35a8f09..e3cfe0a0f 100644 --- a/src/Service/NChan.php +++ b/src/Service/NChan.php @@ -1,5 +1,7 @@ getSessionIdentifier($namespace); if ($this->session->has($sessionKey)) { - return $this->session->get($sessionKey); + $csrf = $this->session->get($sessionKey); + if (!empty($csrf)) { + return (string)$csrf; + } } $key = $this->randomString(); @@ -71,7 +76,7 @@ class Csrf $sessionKey = $this->session->get($sessionIdentifier); - if (0 !== strcmp($key, $sessionKey)) { + if (0 !== strcmp($key, (string)$sessionKey)) { throw new Exception\CsrfValidationException('Invalid CSRF token supplied.'); } } diff --git a/src/Session/Flash.php b/src/Session/Flash.php index 8aa1c508b..bbedd4582 100644 --- a/src/Session/Flash.php +++ b/src/Session/Flash.php @@ -1,5 +1,7 @@ addMessage($message, $level, $saveInSession); } @@ -45,7 +47,7 @@ class Flash * @param string $level * @param bool $saveInSession */ - public function addMessage(string $message, $level = self::INFO, $saveInSession = true): void + public function addMessage(string $message, string $level = self::INFO, bool $saveInSession = true): void { $colorChart = [ 'green' => self::SUCCESS, @@ -65,9 +67,7 @@ class Flash 'color' => $colorChart[$level] ?? $level, ]; - if (null === $this->messages) { - $this->getMessages(); - } + $this->getMessages(); $this->messages[] = $messageRow; if ($saveInSession) { diff --git a/src/Sync/Runner.php b/src/Sync/Runner.php index e50d8ffa5..9ae51dbcc 100644 --- a/src/Sync/Runner.php +++ b/src/Sync/Runner.php @@ -1,5 +1,7 @@ fileSize(); - $total_size = $total_size->plus($size); + if (null !== $size) { + $total_size = $total_size->plus($size); + } } catch (UnableToRetrieveMetadata) { continue; } @@ -209,10 +213,10 @@ class CheckMediaTask extends AbstractTask unset($musicFiles[$pathHash]); } else { - $this->mediaRepo->remove( - $this->em->find(Entity\StationMedia::class, $mediaRow['id']), - false - ); + $media = $this->em->find(Entity\StationMedia::class, $mediaRow['id']); + if ($media instanceof Entity\StationMedia) { + $this->mediaRepo->remove($media, false); + } $stats['deleted']++; } @@ -243,7 +247,7 @@ class CheckMediaTask extends AbstractTask if (Entity\UnprocessableMedia::needsReprocessing($mtime, $unprocessableRow['mtime'] ?? 0)) { $message = new Message\AddNewMediaMessage(); - $message->storage_location_id = $storageLocation->getId(); + $message->storage_location_id = $storageLocation->getIdRequired(); $message->path = $unprocessableRow['path']; $this->messageBus->dispatch($message); @@ -285,7 +289,7 @@ class CheckMediaTask extends AbstractTask $stats['already_queued']++; } else { $message = new Message\AddNewMediaMessage(); - $message->storage_location_id = $storageLocation->getId(); + $message->storage_location_id = $storageLocation->getIdRequired(); $message->path = $path; $this->messageBus->dispatch($message); diff --git a/src/Sync/Task/CheckRequests.php b/src/Sync/Task/CheckRequests.php index 6375eca8c..4745a995b 100644 --- a/src/Sync/Task/CheckRequests.php +++ b/src/Sync/Task/CheckRequests.php @@ -1,5 +1,7 @@ getRealPath(); - @unlink($file_path); + if (false !== $file_path) { + @unlink($file_path); + } } } diff --git a/src/Sync/Task/NowPlayingTask.php b/src/Sync/Task/NowPlayingTask.php index fcdfced4d..995c7990c 100644 --- a/src/Sync/Task/NowPlayingTask.php +++ b/src/Sync/Task/NowPlayingTask.php @@ -1,5 +1,7 @@ station_id = $station->getId(); + $message->station_id = $station->getIdRequired(); $this->messageBus->dispatch( $message, @@ -305,7 +307,7 @@ class NowPlayingTask extends AbstractTask implements EventSubscriberInterface ]; if ($npOld instanceof Entity\Api\NowPlaying) { - if ($npOld->now_playing->song->id !== $np->now_playing->song->id) { + if ($npOld->now_playing?->song?->id !== $np->now_playing?->song?->id) { $triggers[] = Entity\StationWebhook::TRIGGER_SONG_CHANGED; } diff --git a/src/Sync/Task/ReactivateStreamerTask.php b/src/Sync/Task/ReactivateStreamerTask.php index ef615bada..4e1a1687a 100644 --- a/src/Sync/Task/ReactivateStreamerTask.php +++ b/src/Sync/Task/ReactivateStreamerTask.php @@ -1,5 +1,7 @@ getRealPath(); - @unlink($file_path); + if ($file_path) { + @unlink($file_path); + } } } } diff --git a/src/Sync/Task/RunAnalyticsTask.php b/src/Sync/Task/RunAnalyticsTask.php index ec76a8be6..56d16dba4 100644 --- a/src/Sync/Task/RunAnalyticsTask.php +++ b/src/Sync/Task/RunAnalyticsTask.php @@ -1,5 +1,7 @@ requiredFields = ['container']; + } public function _initialize(): void { @@ -42,7 +49,12 @@ class Module extends Framework implements DoctrineProvider ] ); - $this->container = $this->app->getContainer(); + $container = $this->app->getContainer(); + if (null === $container) { + throw new \RuntimeException('Container was not set on App.'); + } + + $this->container = $container; $this->em = $this->container->get(ReloadableEntityManagerInterface::class); parent::_initialize(); diff --git a/src/Traits/AvailableStaticallyTrait.php b/src/Traits/AvailableStaticallyTrait.php index 034b69018..cd46fc7fe 100644 --- a/src/Traits/AvailableStaticallyTrait.php +++ b/src/Traits/AvailableStaticallyTrait.php @@ -1,5 +1,7 @@ $obj + */ public function fromParentObject(object|array $obj): void { if (is_object($obj)) { diff --git a/src/Traits/RequestAwareTrait.php b/src/Traits/RequestAwareTrait.php index b30e26137..96e805098 100644 --- a/src/Traits/RequestAwareTrait.php +++ b/src/Traits/RequestAwareTrait.php @@ -1,5 +1,7 @@ getRealPath(); + if (false === $realPath) { + return false; + } + if ('link' !== $fileinfo->getType() && $fileinfo->isDir()) { - if (!rmdir($fileinfo->getRealPath())) { + if (!rmdir($realPath)) { return false; } - } elseif (!unlink($fileinfo->getRealPath())) { + } elseif (!unlink($realPath)) { return false; } } diff --git a/src/Utilities/Json.php b/src/Utilities/Json.php new file mode 100644 index 000000000..7f36e793c --- /dev/null +++ b/src/Utilities/Json.php @@ -0,0 +1,38 @@ +getMessage() + ) + ); + } + } + } elseif ($throwOnError) { + throw new \RuntimeException(sprintf('Error reading file: "%s"', $path)); + } + } elseif ($throwOnError) { + throw new \RuntimeException(sprintf('File not found: "%s"', $path)); + } + + return []; + } +} diff --git a/src/Utilities/Strings.php b/src/Utilities/Strings.php index 7a3b82c18..795c8ffa0 100644 --- a/src/Utilities/Strings.php +++ b/src/Utilities/Strings.php @@ -1,5 +1,7 @@ $v) { + $sec += (60 ** (int)$k) * (int)$v; + } + + return $sec; + } + + return (float)$seconds; + } +} diff --git a/src/Utilities/Xml.php b/src/Utilities/Xml.php index 6b0da5589..ed89001fa 100644 --- a/src/Utilities/Xml.php +++ b/src/Utilities/Xml.php @@ -1,5 +1,7 @@ $value) { + foreach ($array as $key => $value) { $key = is_numeric($key) ? "item$key" : $key; if (is_array($value)) { $subnode = $xml->addChild((string)$key); self::arrToXml($value, $subnode); } else { - $xml->addChild((string)$key, htmlspecialchars($value)); + $xml->addChild((string)$key, htmlspecialchars($value, ENT_QUOTES | ENT_HTML5)); } } } diff --git a/src/Validator/Constraints/StationPortChecker.php b/src/Validator/Constraints/StationPortChecker.php index e29f82a85..841672ffa 100644 --- a/src/Validator/Constraints/StationPortChecker.php +++ b/src/Validator/Constraints/StationPortChecker.php @@ -1,5 +1,7 @@ entityClass = $entityClass ?? $this->entityClass; + if (is_array($fields) && is_string(key($fields))) { $options = array_merge($fields, $options); } else { @@ -53,11 +55,6 @@ class UniqueEntity extends Constraint } parent::__construct($options, $groups, $payload); - - $this->entityClass = $entityClass ?? $this->entityClass; - $this->repositoryMethod = $repositoryMethod ?? $this->repositoryMethod; - $this->errorPath = $errorPath ?? $this->errorPath; - $this->ignoreNull = $ignoreNull ?? $this->ignoreNull; } /** diff --git a/src/Validator/Constraints/UniqueEntityValidator.php b/src/Validator/Constraints/UniqueEntityValidator.php index 4cfa0907b..99876d099 100644 --- a/src/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Validator/Constraints/UniqueEntityValidator.php @@ -1,5 +1,7 @@ em = $em; + public function __construct( + protected EntityManagerInterface $em + ) { } /** - * @param object $value + * @param mixed $value * * @throws UnexpectedTypeException * @throws ConstraintDefinitionException @@ -86,7 +87,7 @@ class UniqueEntityValidator extends ConstraintValidator ); } - $fieldValue = $class->reflFields[$fieldName]->getValue($value); + $fieldValue = $class->reflFields[$fieldName]?->getValue($value); if (null === $fieldValue) { $hasNullValue = true; @@ -179,13 +180,13 @@ class UniqueEntityValidator extends ConstraintValidator $this->context->buildViolation($message) ->atPath($errorPath) - ->setParameter('{{ value }}', $this->formatWithIdentifiers($this->em, $class, $invalidValue)) + ->setParameter('{{ value }}', $this->formatWithIdentifiers($class, $invalidValue)) ->setInvalidValue($invalidValue) ->setCause($result) ->addViolation(); } - private function formatWithIdentifiers($em, $class, $value): string + private function formatWithIdentifiers(ClassMetadata $class, mixed $value): string { if (!is_object($value) || $value instanceof DateTimeInterface) { return $this->formatValue($value, self::PRETTY_DATE); @@ -197,8 +198,8 @@ class UniqueEntityValidator extends ConstraintValidator if ($class->getName() !== $idClass = get_class($value)) { // non unique value might be a composite PK that consists of other entity objects - if ($em->getMetadataFactory()->hasMetadataFor($idClass)) { - $identifiers = $em->getClassMetadata($idClass)->getIdentifierValues($value); + if ($this->em->getMetadataFactory()->hasMetadataFor($idClass)) { + $identifiers = $this->em->getClassMetadata($idClass)->getIdentifierValues($value); } else { // this case might happen if the non unique column has a custom doctrine type and its value is an object // in which case we cannot get any identifiers for it diff --git a/src/Version.php b/src/Version.php index 53b766529..8eb1f8bae 100644 --- a/src/Version.php +++ b/src/Version.php @@ -1,5 +1,7 @@ ' . htmlspecialchars($dumpedValue) . ''; + return '
' . htmlspecialchars($dumpedValue ?? '', ENT_QUOTES | ENT_HTML5) . '
'; } ); diff --git a/src/Webhook/Connector/AbstractConnector.php b/src/Webhook/Connector/AbstractConnector.php index a41221632..30ae343e6 100644 --- a/src/Webhook/Connector/AbstractConnector.php +++ b/src/Webhook/Connector/AbstractConnector.php @@ -1,11 +1,12 @@ webhookShouldTrigger($webhook, $triggers)) { @@ -45,7 +49,12 @@ abstract class AbstractConnector implements ConnectorInterface return true; } - #[Pure] protected function webhookShouldTrigger(Entity\StationWebhook $webhook, array $triggers = []): bool + /** + * @param Entity\StationWebhook $webhook + * @param array $triggers + * + */ + protected function webhookShouldTrigger(Entity\StationWebhook $webhook, array $triggers = []): bool { $webhookTriggers = $webhook->getTriggers(); if (empty($webhookTriggers)) { @@ -69,10 +78,10 @@ abstract class AbstractConnector implements ConnectorInterface /** * Replace variables in the format {{ blah }} with the flattened contents of the NowPlaying API array. * - * @param array $raw_vars + * @param array $raw_vars * @param Entity\Api\NowPlaying $np * - * @return mixed[] + * @return array */ public function replaceVariables(array $raw_vars, Entity\Api\NowPlaying $np): array { @@ -101,7 +110,7 @@ abstract class AbstractConnector implements ConnectorInterface */ protected function getValidUrl(?string $url_string = null): ?string { - $url = trim($url_string); + $url = trim($url_string ?? ''); $pattern = sprintf(UrlValidator::PATTERN, 'http|https'); return (preg_match($pattern, $url)) ? $url : null; } diff --git a/src/Webhook/Connector/ConnectorInterface.php b/src/Webhook/Connector/ConnectorInterface.php index 2b81dd5db..d63972377 100644 --- a/src/Webhook/Connector/ConnectorInterface.php +++ b/src/Webhook/Connector/ConnectorInterface.php @@ -1,5 +1,7 @@ $triggers * * @return bool Whether the given webhook should dispatch with these triggers. */ @@ -26,7 +28,7 @@ interface ConnectorInterface * @param Entity\Station $station * @param Entity\StationWebhook $webhook * @param Entity\Api\NowPlaying $np - * @param array $triggers + * @param array $triggers * * @return bool Whether the webhook actually dispatched. */ diff --git a/src/Webhook/Connector/Discord.php b/src/Webhook/Connector/Discord.php index 5090779b6..3c062e68d 100644 --- a/src/Webhook/Connector/Discord.php +++ b/src/Webhook/Connector/Discord.php @@ -1,5 +1,7 @@ $config['partner_id'], 'partnerKey' => $config['partner_key'], 'id' => $config['station_id'], - 'title' => $np->now_playing->song->title, - 'artist' => $np->now_playing->song->artist, - 'album' => $np->now_playing->song->album, + 'title' => $np->now_playing?->song?->title, + 'artist' => $np->now_playing?->song?->artist, + 'album' => $np->now_playing?->song?->album, ], ] ); diff --git a/src/Webhook/Connector/Twitter.php b/src/Webhook/Connector/Twitter.php index 4a594fe88..a39c5b316 100644 --- a/src/Webhook/Connector/Twitter.php +++ b/src/Webhook/Connector/Twitter.php @@ -1,5 +1,7 @@ writeElement($branchName, (string)$value); } - } elseif (is_array($value)) { - $this->addBranch($key, $value, $writer); - } elseif (str_starts_with($key, '@')) { - $writer->writeAttribute(substr($key, 1), (string)$value); } else { - $writer->writeElement($key, (string)$value); + /** @var string $key */ + if (is_array($value)) { + $this->addBranch($key, $value, $writer); + } elseif (str_starts_with($key, '@')) { + $writer->writeAttribute(substr($key, 1), (string)$value); + } else { + $writer->writeElement($key, (string)$value); + } } } @@ -118,13 +123,13 @@ class Writer extends Xml } } - protected function attributesFirst($a, $b): int + protected function attributesFirst(mixed $a, mixed $b): int { - if (str_starts_with($a, '@')) { + if (str_starts_with((string)$a, '@')) { return -1; } - if (str_starts_with($b, '@')) { + if (str_starts_with((string)$b, '@')) { return 1; } diff --git a/templates/admin/backups/run.phtml b/templates/admin/backups/run.phtml index 8fce4a022..5d295b1ab 100644 --- a/templates/admin/backups/run.phtml +++ b/templates/admin/backups/run.phtml @@ -26,7 +26,7 @@ $this->layout('main', [

fetch('partials/log_inline', [ - 'url' => $router->fromHere('admin:backups:log', ['path' => $outputLog]), + 'url' => (string)$router->fromHere('admin:backups:log', ['path' => $outputLog]), ])?> diff --git a/templates/admin/debug/sync.phtml b/templates/admin/debug/sync.phtml index c680bff20..32f93ce20 100644 --- a/templates/admin/debug/sync.phtml +++ b/templates/admin/debug/sync.phtml @@ -25,7 +25,7 @@ $this->layout('main', [

fetch('partials/log_inline', [ - 'url' => $router->fromHere('admin:debug:log', ['path' => $outputLog]), + 'url' => (string)$router->fromHere('admin:debug:log', ['path' => $outputLog]), ])?> diff --git a/templates/admin/storage_locations/index.phtml b/templates/admin/storage_locations/index.phtml index 9171cdc32..1d7f41bae 100644 --- a/templates/admin/storage_locations/index.phtml +++ b/templates/admin/storage_locations/index.phtml @@ -8,8 +8,9 @@ $this->layout( ] ); +/** @var App\Http\RouterInterface $router */ $props = [ - 'listUrl' => $router->fromHere('api:admin:storage_locations'), + 'listUrl' => (string)$router->fromHere('api:admin:storage_locations'), ]; /** @var \App\Assets $assets */ diff --git a/templates/frontend/account/login.phtml b/templates/frontend/account/login.phtml index 3766f0dcc..a30a14ef6 100644 --- a/templates/frontend/account/login.phtml +++ b/templates/frontend/account/login.phtml @@ -70,7 +70,7 @@ $this->layout(

%s', - $router->named('account:forgot'), + (string)$router->named('account:forgot'), __('Forgot your password?') )?>

diff --git a/templates/frontend/public/index.phtml b/templates/frontend/public/index.phtml index 5543368aa..ca017bfa7 100644 --- a/templates/frontend/public/index.phtml +++ b/templates/frontend/public/index.phtml @@ -53,7 +53,7 @@ $assets->addInlineJs( $this->push('head'); ?> - + diff --git a/templates/frontend/public/ondemand.phtml b/templates/frontend/public/ondemand.phtml index b60f52cd7..0e948c11b 100644 --- a/templates/frontend/public/ondemand.phtml +++ b/templates/frontend/public/ondemand.phtml @@ -15,7 +15,7 @@ $this->layout( ); $props = [ - 'listUrl' => $router->fromHere('api:stations:ondemand:list'), + 'listUrl' => (string)$router->fromHere('api:stations:ondemand:list'), 'showDownloadButton' => $station->getEnableOnDemandDownload(), 'customFields' => $custom_fields, 'stationName' => $station->getName(), diff --git a/templates/frontend/public/podcasts.phtml b/templates/frontend/public/podcasts.phtml index febb83c13..b2c01645c 100644 --- a/templates/frontend/public/podcasts.phtml +++ b/templates/frontend/public/podcasts.phtml @@ -19,7 +19,7 @@ $this->layout('minimal', [ named( + $episodesPageLink = (string)$router->named( 'public:podcast:episodes', [ 'station_id' => $station->getId(), @@ -27,7 +27,7 @@ $this->layout('minimal', [ ] ) ?> named( + $feedLink = (string)$router->named( 'public:podcast:feed', ['station_id' => $station->getId(), 'podcast_id' => $podcast->getId()] ) ?> diff --git a/templates/stations/files/index.phtml b/templates/stations/files/index.phtml index 303913ca0..c589310af 100644 --- a/templates/stations/files/index.phtml +++ b/templates/stations/files/index.phtml @@ -10,13 +10,14 @@ $this->layout( ] ); +/** @var App\Http\RouterInterface $router */ $props = [ - 'listUrl' => $router->fromHere('api:stations:files:list'), - 'batchUrl' => $router->fromHere('api:stations:files:batch'), - 'uploadUrl' => $router->fromHere('api:stations:files:upload'), - 'listDirectoriesUrl' => $router->fromHere('api:stations:files:directories'), - 'mkdirUrl' => $router->fromHere('api:stations:files:mkdir'), - 'renameUrl' => $router->fromHere('api:stations:files:rename'), + 'listUrl' => (string)$router->fromHere('api:stations:files:list'), + 'batchUrl' => (string)$router->fromHere('api:stations:files:batch'), + 'uploadUrl' => (string)$router->fromHere('api:stations:files:upload'), + 'listDirectoriesUrl' => (string)$router->fromHere('api:stations:files:directories'), + 'mkdirUrl' => (string)$router->fromHere('api:stations:files:mkdir'), + 'renameUrl' => (string)$router->fromHere('api:stations:files:rename'), 'initialPlaylists' => $playlists, 'customFields' => $custom_fields, 'validMimeTypes' => $mime_types, diff --git a/templates/stations/mounts/index.phtml b/templates/stations/mounts/index.phtml index 3d8c59a56..03fc9fe59 100644 --- a/templates/stations/mounts/index.phtml +++ b/templates/stations/mounts/index.phtml @@ -8,8 +8,9 @@ $this->layout( ] ); +/** @var App\Http\RouterInterface $router */ $props = [ - 'listUrl' => $router->fromHere('api:stations:mounts'), + 'listUrl' => (string)$router->fromHere('api:stations:mounts'), 'stationFrontendType' => $station->getFrontendType(), 'enableAdvancedFeatures' => $enableAdvancedFeatures, ]; diff --git a/templates/stations/playlists/index.phtml b/templates/stations/playlists/index.phtml index 93236e59a..107d59369 100644 --- a/templates/stations/playlists/index.phtml +++ b/templates/stations/playlists/index.phtml @@ -8,11 +8,12 @@ $this->layout( ] ); +/** @var App\Http\RouterInterface $router */ $props = [ - 'listUrl' => $router->fromHere('api:stations:playlists'), - 'scheduleUrl' => $router->fromHere('api:stations:playlists:schedule'), + 'listUrl' => (string)$router->fromHere('api:stations:playlists'), + 'scheduleUrl' => (string)$router->fromHere('api:stations:playlists:schedule'), 'locale' => substr($customization->getLocale()->getLocale(), 0, 2), - 'filesUrl' => $router->fromHere('stations:files:index'), + 'filesUrl' => (string)$router->fromHere('stations:files:index'), 'stationTimeZone' => $station_tz, 'enableAdvancedFeatures' => $enableAdvancedFeatures, ]; diff --git a/templates/stations/podcasts/index.phtml b/templates/stations/podcasts/index.phtml index f71b30178..9ea4d89ae 100644 --- a/templates/stations/podcasts/index.phtml +++ b/templates/stations/podcasts/index.phtml @@ -4,10 +4,11 @@ $this->layout('main', [ 'manual' => true, ]); +/** @var App\Http\RouterInterface $router */ $props = [ - 'listUrl' => $router->fromHere('api:stations:podcasts'), - 'newArtUrl' => $router->fromHere('api:stations:podcasts:new-art'), - 'stationUrl' => $router->fromHere('stations:index:index', [$stationId]), + 'listUrl' => (string)$router->fromHere('api:stations:podcasts'), + 'newArtUrl' => (string)$router->fromHere('api:stations:podcasts:new-art'), + 'stationUrl' => (string)$router->fromHere('stations:index:index', [$stationId]), 'locale' => substr($customization->getLocale(), 0, 2), 'stationTimeZone' => $stationTz, 'languageOptions' => $languageOptions, diff --git a/templates/stations/profile/index.phtml b/templates/stations/profile/index.phtml index 13b508827..e9a611dae 100644 --- a/templates/stations/profile/index.phtml +++ b/templates/stations/profile/index.phtml @@ -1,7 +1,8 @@ layout( @@ -25,7 +26,7 @@ $props = [ 'enableStreamers' => $station->getEnableStreamers(), 'enablePublicPage' => $station->getEnablePublicPage(), 'enableOnDemand' => $station->getEnableOnDemand(), - 'profileApiUri' => $router->fromHere('api:stations:profile'), + 'profileApiUri' => (string)$router->fromHere('api:stations:profile'), // ACL 'userCanManageMedia' => $acl->isAllowed(App\Acl::STATION_MEDIA, $station->getId()), @@ -37,19 +38,25 @@ $props = [ // Header 'stationName' => $station->getName(), 'stationDescription' => $station->getDescription(), - 'manageProfileUri' => $router->fromHere('stations:profile:edit'), + 'manageProfileUri' => (string)$router->fromHere('stations:profile:edit'), // Now Playing - 'backendSkipSongUri' => $router->fromHere('api:stations:backend', ['do' => 'skip']), - 'backendDisconnectStreamerUri' => $router->fromHere('api:stations:backend', ['do' => 'disconnect']), + 'backendSkipSongUri' => (string)$router->fromHere('api:stations:backend', ['do' => 'skip']), + 'backendDisconnectStreamerUri' => (string)$router->fromHere('api:stations:backend', ['do' => 'disconnect']), // Requests - 'requestsViewUri' => $router->fromHere('stations:reports:requests'), - 'requestsToggleUri' => $router->fromHere('stations:profile:toggle', ['feature' => 'requests', 'csrf' => $csrf]), + 'requestsViewUri' => (string)$router->fromHere('stations:reports:requests'), + 'requestsToggleUri' => (string)$router->fromHere( + 'stations:profile:toggle', + ['feature' => 'requests', 'csrf' => $csrf] + ), // Streamers - 'streamersViewUri' => $router->fromHere('stations:streamers:index'), - 'streamersToggleUri' => $router->fromHere('stations:profile:toggle', ['feature' => 'streamers', 'csrf' => $csrf]), + 'streamersViewUri' => (string)$router->fromHere('stations:streamers:index'), + 'streamersToggleUri' => (string)$router->fromHere( + 'stations:profile:toggle', + ['feature' => 'streamers', 'csrf' => $csrf] + ), // Public Pages 'publicPageUri' => (string)$router->named('public:index', ['station_id' => $station->getShortName()], [], true), @@ -103,7 +110,7 @@ $props = [ true ), - 'togglePublicPageUri' => $router->fromHere( + 'togglePublicPageUri' => (string)$router->fromHere( 'stations:profile:toggle', ['feature' => 'public', 'csrf' => $csrf] ), @@ -113,18 +120,18 @@ $props = [ 'frontendAdminPassword' => $frontendConfig->getAdminPassword(), 'frontendSourcePassword' => $frontendConfig->getSourcePassword(), 'frontendRelayPassword' => $frontendConfig->getRelayPassword(), - 'frontendRestartUri' => $router->fromHere('api:stations:frontend', ['do' => 'restart']), - 'frontendStartUri' => $router->fromHere('api:stations:frontend', ['do' => 'start']), - 'frontendStopUri' => $router->fromHere('api:stations:frontend', ['do' => 'stop']), + 'frontendRestartUri' => (string)$router->fromHere('api:stations:frontend', ['do' => 'restart']), + 'frontendStartUri' => (string)$router->fromHere('api:stations:frontend', ['do' => 'start']), + 'frontendStopUri' => (string)$router->fromHere('api:stations:frontend', ['do' => 'stop']), // Backend 'numSongs' => (int)$num_songs, 'numPlaylists' => (int)$num_playlists, - 'manageMediaUri' => $router->fromHere('stations:files:index'), - 'managePlaylistsUri' => $router->fromHere('stations:playlists:index'), - 'backendRestartUri' => $router->fromHere('api:stations:backend', ['do' => 'restart']), - 'backendStartUri' => $router->fromHere('api:stations:backend', ['do' => 'start']), - 'backendStopUri' => $router->fromHere('api:stations:backend', ['do' => 'stop']), + 'manageMediaUri' => (string)$router->fromHere('stations:files:index'), + 'managePlaylistsUri' => (string)$router->fromHere('stations:playlists:index'), + 'backendRestartUri' => (string)$router->fromHere('api:stations:backend', ['do' => 'restart']), + 'backendStartUri' => (string)$router->fromHere('api:stations:backend', ['do' => 'start']), + 'backendStopUri' => (string)$router->fromHere('api:stations:backend', ['do' => 'stop']), ]; $assets diff --git a/templates/stations/queue/index.phtml b/templates/stations/queue/index.phtml index 65751abc4..989adbc8f 100644 --- a/templates/stations/queue/index.phtml +++ b/templates/stations/queue/index.phtml @@ -2,8 +2,9 @@ $this->layout('main', ['title' => __('Upcoming Song Queue'), 'manual' => true]); +/** @var App\Http\RouterInterface $router */ $props = [ - 'listUrl' => $router->fromHere('api:stations:queue'), + 'listUrl' => (string)$router->fromHere('api:stations:queue'), 'locale' => substr($customization->getLocale(), 0, 2), 'stationTimeZone' => $stationTz, ]; diff --git a/templates/stations/reports/overview.phtml b/templates/stations/reports/overview.phtml index b030859cd..4c7f8a7cf 100644 --- a/templates/stations/reports/overview.phtml +++ b/templates/stations/reports/overview.phtml @@ -6,6 +6,7 @@ $this->layout('main', ['title' => __('Statistics Overview'), 'manual' => true]); +/** @var App\Http\RouterInterface $router */ $props = [ 'chartsUrl' => (string)$router->fromHere('api:stations:reports:overview-charts'), 'bestAndWorstUrl' => (string)$router->fromHere('api:stations:reports:best-and-worst'), diff --git a/templates/stations/streamers/index.phtml b/templates/stations/streamers/index.phtml index ec83fbcd2..95afa2f99 100644 --- a/templates/stations/streamers/index.phtml +++ b/templates/stations/streamers/index.phtml @@ -2,14 +2,15 @@ $this->layout('main', ['title' => __('Streamer/DJ Accounts'), 'manual' => true]); +/** @var App\Http\RouterInterface $router */ $props = [ - 'listUrl' => $router->fromHere('api:stations:streamers'), - 'scheduleUrl' => $router->fromHere('api:stations:streamers:schedule'), + 'listUrl' => (string)$router->fromHere('api:stations:streamers'), + 'scheduleUrl' => (string)$router->fromHere('api:stations:streamers:schedule'), 'locale' => substr($customization->getLocale(), 0, 2), 'stationTimeZone' => $station_tz, ]; -/** @var \App\Assets $assets */ +/** @var App\Assets $assets */ $assets->addVueRender('Vue_StationsStreamers', '#station-streamers', $props); ?> diff --git a/web/index.php b/web/index.php index eaffdec72..40f8e8d2e 100755 --- a/web/index.php +++ b/web/index.php @@ -1,6 +1,9 @@