diff --git a/formwork/config/system.yaml b/formwork/config/system.yaml index bca777770..d2ac36b4d 100644 --- a/formwork/config/system.yaml +++ b/formwork/config/system.yaml @@ -32,9 +32,6 @@ debug: editorUri: 'editor://file/{{filename}}:{{line}}' contextLines: 5 -errors: - setHandlers: true - fields: path: '${%SYSTEM_PATH%}/fields' dynamic: diff --git a/formwork/routes.php b/formwork/routes.php index f60fe62b3..739f52faa 100644 --- a/formwork/routes.php +++ b/formwork/routes.php @@ -1,7 +1,7 @@ [ 'request.validateSize' => [ - 'action' => static function (Config $config, Request $request, Router $router, ErrorHandlers $errorHandlers) { + 'action' => static function (Config $config, Request $request, Router $router, ErrorsControllerInterface $errorsController) { if ($config->get('system.panel.enabled') && $router->requestHasPrefix($config->get('system.panel.root'))) { return; } @@ -57,7 +57,7 @@ $maxSize = FileSystem::shorthandToBytes(ini_get('post_max_size') ?: '0'); if ($request->contentLength() > $maxSize && $maxSize > 0) { - $errorHandlers->displayErrorPage(ResponseStatus::PayloadTooLarge); + return $errorsController->error(ResponseStatus::PayloadTooLarge); } } }, @@ -66,7 +66,7 @@ ], 'request.validateCsrf' => [ - 'action' => static function (Config $config, Request $request, Router $router, CsrfToken $csrfToken, ErrorHandlers $errorHandlers) { + 'action' => static function (Config $config, Request $request, Router $router, CsrfToken $csrfToken, ErrorsControllerInterface $errorsController) { if ($config->get('system.panel.enabled') && $router->requestHasPrefix($config->get('system.panel.root'))) { return; } @@ -76,7 +76,7 @@ if (!($csrfToken->validate($tokenName, $token))) { $csrfToken->destroy($tokenName); - $errorHandlers->displayErrorPage(ResponseStatus::Forbidden); + return $errorsController->error(ResponseStatus::Forbidden); } }, 'methods' => ['POST'], diff --git a/formwork/src/App.php b/formwork/src/App.php index 547437924..664e0f85c 100644 --- a/formwork/src/App.php +++ b/formwork/src/App.php @@ -3,9 +3,12 @@ namespace Formwork; use BadMethodCallException; +use ErrorException; use Formwork\Cache\AbstractCache; use Formwork\Cache\FilesCache; use Formwork\Config\Config; +use Formwork\Controllers\ErrorsController; +use Formwork\Controllers\ErrorsControllerInterface; use Formwork\Fields\Dynamic\DynamicFieldValue; use Formwork\Files\FileFactory; use Formwork\Files\FileUriGenerator; @@ -20,7 +23,6 @@ use Formwork\Security\CsrfToken; use Formwork\Services\Container; use Formwork\Services\Loaders\ConfigServiceLoader; -use Formwork\Services\Loaders\ErrorHandlersServiceLoader; use Formwork\Services\Loaders\LanguagesServiceLoader; use Formwork\Services\Loaders\PanelServiceLoader; use Formwork\Services\Loaders\SchemesServiceLoader; @@ -37,6 +39,7 @@ use Formwork\Users\Users; use Formwork\Utils\Str; use Formwork\View\ViewFactory; +use Throwable; final class App { @@ -57,8 +60,6 @@ public function __construct() $this->initializeSingleton(); $this->container = new Container(); - - $this->loadServices($this->container); } /** @@ -134,15 +135,22 @@ public function getService(string $name): object */ public function run(): Response { - $this->loadRoutes(); + $this->loadErrorHandler(); + + try { + $this->loadServices($this->container); - DynamicFieldValue::$vars = $this->container->call(require $this->config()->get('system.fields.dynamic.vars.file')); + $this->loadRoutes(); - $request = $this->request(); + DynamicFieldValue::$vars = $this->container->call(require $this->config()->get('system.fields.dynamic.vars.file')); - $response = $this->router()->dispatch(); + $response = $this->router()->dispatch(); + } catch (Throwable $throwable) { + $controller = $this->container->get(ErrorsControllerInterface::class); + $response = $controller->error(throwable: $throwable); + } - $response->prepare($request)->send(); + $response->prepare($this->request())->send(); return $response; } @@ -163,9 +171,9 @@ protected function loadServices(Container $container): void $container->define(Request::class, fn () => Request::fromGlobals()) ->alias('request'); - $container->define(ErrorHandlers::class) - ->loader(ErrorHandlersServiceLoader::class) - ->lazy(!$this->config()->get('system.errors.setHandlers', true)); + $container->define(ErrorsController::class) + ->alias(ErrorsControllerInterface::class) + ->lazy(false); $container->define(CsrfToken::class) ->alias('csrfToken'); @@ -243,4 +251,16 @@ protected function loadRoutes(): void $this->router()->loadFromFile($this->config()->get('system.routes.files.system')); } + + protected function loadErrorHandler(): void + { + ini_set('display_errors', 0); + + set_error_handler(function (int $severity, string $message, string $file, int $line): bool { + if (!(error_reporting() & $severity) || $severity === E_USER_DEPRECATED) { + return false; + } + throw new ErrorException($message, 0, $severity, $file, $line); + }); + } } diff --git a/formwork/src/Controllers/AbstractController.php b/formwork/src/Controllers/AbstractController.php index b65879814..ff7a6f874 100644 --- a/formwork/src/Controllers/AbstractController.php +++ b/formwork/src/Controllers/AbstractController.php @@ -2,7 +2,14 @@ namespace Formwork\Controllers; +use Formwork\App; +use Formwork\Config\Config; +use Formwork\Http\Request; +use Formwork\Http\Response; +use Formwork\Services\Container; use Formwork\Utils\Str; +use Formwork\View\ViewFactory; +use InvalidArgumentException; abstract class AbstractController { @@ -11,8 +18,38 @@ abstract class AbstractController */ protected string $name; - public function __construct() - { + public function __construct( + private readonly Container $container, + protected App $app, + protected Config $config, + protected ViewFactory $viewFactory, + protected Request $request, + ) { $this->name = strtolower(Str::beforeLast(Str::afterLast(static::class, '\\'), 'Controller')); } + + /** + * Render a view + * + * @param array $data + */ + protected function view(string $name, array $data = []): string + { + return $this->viewFactory->make($name, $data)->render(); + } + + /** + * Forward the request to another controller + * + * @param class-string $controller + * @param array $parameters + */ + protected function forward(string $controller, string $action, array $parameters = []): Response + { + if (!is_subclass_of($controller, AbstractController::class)) { + throw new InvalidArgumentException(sprintf('Controllers must extend %s', AbstractController::class)); + } + $instance = $this->container->build($controller); + return $this->container->call($instance->$action(...), $parameters); + } } diff --git a/formwork/src/Controllers/AssetController.php b/formwork/src/Controllers/AssetController.php index c2861dfad..7f6530f29 100644 --- a/formwork/src/Controllers/AssetController.php +++ b/formwork/src/Controllers/AssetController.php @@ -2,17 +2,16 @@ namespace Formwork\Controllers; -use Formwork\Config\Config; use Formwork\Http\FileResponse; use Formwork\Router\RouteParams; use Formwork\Utils\Exceptions\FileNotFoundException; use Formwork\Utils\FileSystem; -class AssetController +class AssetController extends AbstractController { - public function load(RouteParams $routeParams, Config $config): FileResponse + public function load(RouteParams $routeParams): FileResponse { - $path = FileSystem::joinPaths($config->get('system.images.processPath'), $routeParams->get('id'), $routeParams->get('name')); + $path = FileSystem::joinPaths($this->config->get('system.images.processPath'), $routeParams->get('id'), $routeParams->get('name')); if (FileSystem::isFile($path)) { return new FileResponse($path); diff --git a/formwork/src/Controllers/ErrorsController.php b/formwork/src/Controllers/ErrorsController.php new file mode 100644 index 000000000..2a24d33a0 --- /dev/null +++ b/formwork/src/Controllers/ErrorsController.php @@ -0,0 +1,55 @@ +request->isXmlHttpRequest() + ? JsonResponse::error('Error', $responseStatus) + : new Response($this->view( + 'errors.error', + [ + 'status' => $responseStatus->code(), + 'message' => $responseStatus->message(), + 'throwable' => $throwable, + ] + ), $responseStatus); + + if ($throwable !== null) { + error_log(sprintf( + "Uncaught %s: %s in %s:%s\nStack trace:\n%s\n", + $throwable::class, + $throwable->getMessage(), + $throwable->getFile(), + $throwable->getLine(), + $throwable->getTraceAsString() + )); + } + + return $response; + } + + public function notFound(): Response + { + return $this->error(ResponseStatus::NotFound); + } + + public function internalServerError(Throwable $throwable): Response + { + return $this->error(ResponseStatus::InternalServerError, $throwable); + } + + public function forbidden(): Response + { + return $this->error(ResponseStatus::Forbidden); + } +} diff --git a/formwork/src/Controllers/ErrorsControllerInterface.php b/formwork/src/Controllers/ErrorsControllerInterface.php new file mode 100644 index 000000000..0163bafa4 --- /dev/null +++ b/formwork/src/Controllers/ErrorsControllerInterface.php @@ -0,0 +1,12 @@ +container->call(parent::__construct(...)); } - public function load(RouteParams $routeParams, ViewFactory $viewFactory, Statistics $statistics): Response + public function load(RouteParams $routeParams, Statistics $statistics): Response { if ($this->site->get('maintenance.enabled') && !$this->app->panel()?->isLoggedIn()) { if ($this->site->get('maintenance.page') !== null) { $route = $this->site->get('maintenance.page')->route(); } else { $status = ResponseStatus::ServiceUnavailable; - $view = $viewFactory->make('errors.maintenance', ['status' => $status->code(), 'message' => $status->message()]); - return new Response($view->render(), $status); + return new Response($this->view('errors.maintenance', ['status' => $status->code(), 'message' => $status->message()]), $status); } } diff --git a/formwork/src/ErrorHandlers.php b/formwork/src/ErrorHandlers.php deleted file mode 100644 index e2f3e33cc..000000000 --- a/formwork/src/ErrorHandlers.php +++ /dev/null @@ -1,72 +0,0 @@ -getExceptionHandler(...)); - set_error_handler($this->getErrorHandler(...)); - } - - /** - * Display error page - */ - public function displayErrorPage(ResponseStatus $responseStatus = ResponseStatus::InternalServerError, ?Throwable $throwable = null): void - { - Response::cleanOutputBuffers(); - - if ($this->request->isXmlHttpRequest()) { - JsonResponse::error('Error', $responseStatus)->prepare($this->request)->send(); - } else { - $view = $this->viewFactory->make('errors.error', ['status' => $responseStatus->code(), 'message' => $responseStatus->message(), 'throwable' => $throwable]); - $response = new Response($view->render(), $responseStatus); - $response->prepare($this->request)->send(); - // Don't exit, otherwise the error will not be logged - } - } - - /** - * Display error page on exception - */ - public function getExceptionHandler(Throwable $throwable): void - { - $this->displayErrorPage(throwable: $throwable); - error_log(sprintf( - "Uncaught %s: %s in %s:%s\nStack trace:\n%s\n", - $throwable::class, - $throwable->getMessage(), - $throwable->getFile(), - $throwable->getLine(), - $throwable->getTraceAsString() - )); - } - - /** - * Handle error throwing an ErrorException - */ - public function getErrorHandler(int $severity, string $message, string $file, int $line): bool - { - if (!(error_reporting() & $severity) || $severity === E_USER_DEPRECATED) { - return false; - } - throw new ErrorException($message, 0, $severity, $file, $line); - } -} diff --git a/formwork/src/Panel/Controllers/AbstractController.php b/formwork/src/Panel/Controllers/AbstractController.php index 84e12e1d7..c444ed459 100644 --- a/formwork/src/Panel/Controllers/AbstractController.php +++ b/formwork/src/Panel/Controllers/AbstractController.php @@ -2,11 +2,8 @@ namespace Formwork\Panel\Controllers; -use Formwork\App; -use Formwork\Config\Config; use Formwork\Controllers\AbstractController as BaseAbstractController; use Formwork\Http\RedirectResponse; -use Formwork\Http\Request; use Formwork\Http\ResponseStatus; use Formwork\Panel\Modals\Modal; use Formwork\Panel\Modals\ModalCollection; @@ -21,7 +18,6 @@ use Formwork\Users\User; use Formwork\Utils\Date; use Formwork\Utils\Uri; -use Formwork\View\ViewFactory; use Stringable; abstract class AbstractController extends BaseAbstractController @@ -30,18 +26,14 @@ abstract class AbstractController extends BaseAbstractController public function __construct( private readonly Container $container, - protected App $app, - protected Config $config, - protected ModalFactory $modalFactory, - protected ViewFactory $viewFactory, - protected Request $request, protected Router $router, protected CsrfToken $csrfToken, protected Translations $translations, + protected ModalFactory $modalFactory, protected Site $site, protected Panel $panel ) { - parent::__construct(); + $this->container->call(parent::__construct(...)); } /** @@ -217,17 +209,11 @@ protected function user(): User } /** - * Ensure current user has a permission + * Get if current user has a permission */ - protected function ensurePermission(string $permission): void + protected function hasPermission(string $permission): bool { - if (!$this->user()->permissions()->has($permission)) { - $this->container->build(ErrorsController::class) - ->forbidden() - ->prepare($this->request) - ->send(); - exit; - } + return $this->user()->permissions()->has($permission); } protected function modals(): ModalCollection diff --git a/formwork/src/Panel/Controllers/AuthenticationController.php b/formwork/src/Panel/Controllers/AuthenticationController.php index 7b5787878..5cae80858 100644 --- a/formwork/src/Panel/Controllers/AuthenticationController.php +++ b/formwork/src/Panel/Controllers/AuthenticationController.php @@ -3,13 +3,11 @@ namespace Formwork\Panel\Controllers; use Formwork\Http\RedirectResponse; -use Formwork\Http\Request; use Formwork\Http\RequestMethod; use Formwork\Http\Response; use Formwork\Log\Log; use Formwork\Log\Registry; use Formwork\Panel\Security\AccessLimiter; -use Formwork\Security\CsrfToken; use Formwork\Utils\FileSystem; use RuntimeException; @@ -18,24 +16,24 @@ class AuthenticationController extends AbstractController /** * Authentication@login action */ - public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $accessLimiter): Response + public function login(AccessLimiter $accessLimiter): Response { $csrfTokenName = $this->panel()->getCsrfTokenName(); if ($accessLimiter->hasReachedLimit()) { $minutes = round($this->config->get('system.panel.loginResetTime') / 60); - $csrfToken->generate($csrfTokenName); + $this->csrfToken->generate($csrfTokenName); return $this->error($this->translate('panel.login.attempt.tooMany', $minutes)); } - switch ($request->method()) { + switch ($this->request->method()) { case RequestMethod::GET: - if ($request->session()->has('FORMWORK_USERNAME')) { + if ($this->request->session()->has('FORMWORK_USERNAME')) { return $this->redirect($this->generateRoute('panel.index')); } // Always generate a new CSRF token - $csrfToken->generate($csrfTokenName); + $this->csrfToken->generate($csrfTokenName); return new Response($this->view('authentication.login', [ 'title' => $this->translate('panel.login.login'), @@ -45,11 +43,11 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc // Delay request processing for 0.5-1s usleep(random_int(500, 1000) * 1000); - $data = $request->input(); + $data = $this->request->input(); // Ensure no required data is missing if (!$data->hasMultiple(['username', 'password'])) { - $csrfToken->generate($csrfTokenName); + $this->csrfToken->generate($csrfTokenName); $this->error($this->translate('panel.login.attempt.failed')); } @@ -59,11 +57,11 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc // Authenticate user if ($user !== null && $user->authenticate($data->get('password'))) { - $request->session()->regenerate(); - $request->session()->set('FORMWORK_USERNAME', $data->get('username')); + $this->request->session()->regenerate(); + $this->request->session()->set('FORMWORK_USERNAME', $data->get('username')); // Regenerate CSRF token - $csrfToken->generate($csrfTokenName); + $this->csrfToken->generate($csrfTokenName); $accessLog = new Log(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'access.json')); $lastAccessRegistry = new Registry(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'lastAccess.json')); @@ -73,15 +71,15 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc $accessLimiter->resetAttempts(); - if (($destination = $request->session()->get('FORMWORK_REDIRECT_TO')) !== null) { - $request->session()->remove('FORMWORK_REDIRECT_TO'); + if (($destination = $this->request->session()->get('FORMWORK_REDIRECT_TO')) !== null) { + $this->request->session()->remove('FORMWORK_REDIRECT_TO'); return new RedirectResponse($this->panel->uri($destination)); } return $this->redirect($this->generateRoute('panel.index')); } - $csrfToken->generate($csrfTokenName); + $this->csrfToken->generate($csrfTokenName); return $this->error($this->translate('panel.login.attempt.failed'), [ 'username' => $data->get('username'), 'error' => true, @@ -94,11 +92,11 @@ public function login(Request $request, CsrfToken $csrfToken, AccessLimiter $acc /** * Authentication@logout action */ - public function logout(Request $request, CsrfToken $csrfToken): RedirectResponse + public function logout(): RedirectResponse { - $csrfToken->destroy($this->panel()->getCsrfTokenName()); - $request->session()->remove('FORMWORK_USERNAME'); - $request->session()->destroy(); + $this->csrfToken->destroy($this->panel()->getCsrfTokenName()); + $this->request->session()->remove('FORMWORK_USERNAME'); + $this->request->session()->destroy(); if ($this->config->get('system.panel.logoutRedirect') === 'home') { return $this->redirect('/'); diff --git a/formwork/src/Panel/Controllers/BackupController.php b/formwork/src/Panel/Controllers/BackupController.php index fabc211b2..47223c6cd 100644 --- a/formwork/src/Panel/Controllers/BackupController.php +++ b/formwork/src/Panel/Controllers/BackupController.php @@ -3,7 +3,6 @@ namespace Formwork\Panel\Controllers; use Formwork\Backupper; -use Formwork\Config\Config; use Formwork\Exceptions\TranslatedException; use Formwork\Http\FileResponse; use Formwork\Http\JsonResponse; @@ -19,9 +18,12 @@ class BackupController extends AbstractController /** * Backup@make action */ - public function make(Config $config): JsonResponse + public function make(): JsonResponse|Response { - $this->ensurePermission('backup.make'); + if (!$this->hasPermission('backup.make')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + $backupper = new Backupper($this->config); try { $file = $backupper->backup(); @@ -33,10 +35,10 @@ public function make(Config $config): JsonResponse return JsonResponse::success($this->translate('panel.backup.ready'), data: [ 'filename' => $filename, 'uri' => $this->panel()->uri('/backup/download/' . $uriName . '/'), - 'date' => Date::formatTimestamp(FileSystem::lastModifiedTime($file), $config->get('system.date.datetimeFormat')), + 'date' => Date::formatTimestamp(FileSystem::lastModifiedTime($file), $this->config->get('system.date.datetimeFormat')), 'size' => FileSystem::formatSize(FileSystem::size($file)), 'deleteUri' => $this->panel()->uri('/backup/delete/' . $uriName . '/'), - 'maxFiles' => $config->get('system.backup.maxFiles'), + 'maxFiles' => $this->config->get('system.backup.maxFiles'), ]); } @@ -45,7 +47,10 @@ public function make(Config $config): JsonResponse */ public function download(RouteParams $routeParams): Response { - $this->ensurePermission('backup.download'); + if (!$this->hasPermission('backup.download')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + $file = FileSystem::joinPaths($this->config->get('system.backup.path'), basename(base64_decode((string) $routeParams->get('backup')))); try { if (FileSystem::isFile($file, assertExists: false)) { @@ -63,7 +68,10 @@ public function download(RouteParams $routeParams): Response */ public function delete(RouteParams $routeParams): Response { - $this->ensurePermission('backup.download'); + if (!$this->hasPermission('backup.download')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + $file = FileSystem::joinPaths($this->config->get('system.backup.path'), basename(base64_decode((string) $routeParams->get('backup')))); try { if (FileSystem::isFile($file, assertExists: false)) { diff --git a/formwork/src/Panel/Controllers/CacheController.php b/formwork/src/Panel/Controllers/CacheController.php index 2b0fcafff..bd499dd9c 100644 --- a/formwork/src/Panel/Controllers/CacheController.php +++ b/formwork/src/Panel/Controllers/CacheController.php @@ -4,6 +4,7 @@ use Formwork\Cache\AbstractCache; use Formwork\Http\JsonResponse; +use Formwork\Http\Response; use Formwork\Router\RouteParams; use Formwork\Utils\FileSystem; @@ -12,9 +13,11 @@ class CacheController extends AbstractController /** * Cache@clear action */ - public function clear(RouteParams $routeParams, AbstractCache $cache): JsonResponse + public function clear(RouteParams $routeParams, AbstractCache $cache): JsonResponse|Response { - $this->ensurePermission('cache.clear'); + if (!$this->hasPermission('cache.clear')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } switch ($type = $routeParams->get('type', 'default')) { case 'default': diff --git a/formwork/src/Panel/Controllers/DashboardController.php b/formwork/src/Panel/Controllers/DashboardController.php index 96ae1d29f..c1ae06cce 100644 --- a/formwork/src/Panel/Controllers/DashboardController.php +++ b/formwork/src/Panel/Controllers/DashboardController.php @@ -4,7 +4,6 @@ use Formwork\Http\Response; use Formwork\Parsers\Json; -use Formwork\Schemes\Schemes; use Formwork\Statistics\Statistics; class DashboardController extends AbstractController @@ -12,9 +11,11 @@ class DashboardController extends AbstractController /** * Dashboard@index action */ - public function index(Schemes $schemes, Statistics $statistics): Response + public function index(Statistics $statistics): Response { - $this->ensurePermission('dashboard'); + if (!$this->hasPermission('dashboard')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $this->modal('newPage'); diff --git a/formwork/src/Panel/Controllers/ErrorsController.php b/formwork/src/Panel/Controllers/ErrorsController.php index 49c8bb433..78df9edb7 100644 --- a/formwork/src/Panel/Controllers/ErrorsController.php +++ b/formwork/src/Panel/Controllers/ErrorsController.php @@ -2,13 +2,22 @@ namespace Formwork\Panel\Controllers; +use Formwork\Controllers\ErrorsControllerInterface; use Formwork\Http\JsonResponse; use Formwork\Http\Response; use Formwork\Http\ResponseStatus; use Throwable; -class ErrorsController extends AbstractController +class ErrorsController extends AbstractController implements ErrorsControllerInterface { + public function error(ResponseStatus $responseStatus = ResponseStatus::InternalServerError, ?Throwable $throwable = null): Response + { + return $this->makeErrorResponse($responseStatus, 'internalServerError', [ + 'href' => $this->makeGitHubIssueUri($throwable), + 'label' => $this->translate('panel.errors.action.reportToGithub'), + ], ['throwable' => $throwable]); + } + /** * Errors@notFound action */ @@ -70,8 +79,12 @@ protected function makeErrorResponse(ResponseStatus $responseStatus, string $nam /** * Make a URI to a new GitHub issue with pre-filled data from an (uncaught) exception */ - protected function makeGitHubIssueUri(Throwable $throwable): string + protected function makeGitHubIssueUri(?Throwable $throwable): string { + if ($throwable === null) { + return 'https://github.com/getformwork/formwork/issues/'; + } + $query = http_build_query([ 'labels' => 'bug', 'title' => $throwable->getMessage(), diff --git a/formwork/src/Panel/Controllers/OptionsController.php b/formwork/src/Panel/Controllers/OptionsController.php index 787e3cabc..8a7d3242d 100644 --- a/formwork/src/Panel/Controllers/OptionsController.php +++ b/formwork/src/Panel/Controllers/OptionsController.php @@ -3,7 +3,6 @@ namespace Formwork\Panel\Controllers; use Formwork\Fields\FieldCollection; -use Formwork\Http\RedirectResponse; use Formwork\Http\RequestMethod; use Formwork\Http\Response; use Formwork\Parsers\Yaml; @@ -24,9 +23,12 @@ class OptionsController extends AbstractController /** * Options@index action */ - public function index(): RedirectResponse + public function index(): Response { - $this->ensurePermission('options.site'); + if (!$this->hasPermission('options.site')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + return $this->redirect($this->generateRoute('panel.options.site')); } @@ -35,7 +37,9 @@ public function index(): RedirectResponse */ public function systemOptions(Schemes $schemes): Response { - $this->ensurePermission('options.system'); + if (!$this->hasPermission('options.system')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $scheme = $schemes->get('config.system'); $fields = $scheme->fields(); @@ -79,7 +83,9 @@ public function systemOptions(Schemes $schemes): Response */ public function siteOptions(Schemes $schemes): Response { - $this->ensurePermission('options.site'); + if (!$this->hasPermission('options.site')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $scheme = $schemes->get('config.site'); $fields = $scheme->fields(); diff --git a/formwork/src/Panel/Controllers/PagesController.php b/formwork/src/Panel/Controllers/PagesController.php index 5fa7998d1..c725bd570 100644 --- a/formwork/src/Panel/Controllers/PagesController.php +++ b/formwork/src/Panel/Controllers/PagesController.php @@ -10,8 +10,6 @@ use Formwork\Files\Services\FileUploader; use Formwork\Http\Files\UploadedFile; use Formwork\Http\JsonResponse; -use Formwork\Http\RedirectResponse; -use Formwork\Http\Request; use Formwork\Http\RequestData; use Formwork\Http\RequestMethod; use Formwork\Http\Response; @@ -49,7 +47,9 @@ class PagesController extends AbstractController */ public function index(): Response { - $this->ensurePermission('pages.index'); + if (!$this->hasPermission('pages.index')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $this->modal('newPage'); @@ -79,9 +79,11 @@ public function index(): Response /** * Pages@create action */ - public function create(): RedirectResponse + public function create(): Response { - $this->ensurePermission('pages.create'); + if (!$this->hasPermission('pages.create')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $requestData = $this->request->input(); @@ -115,7 +117,9 @@ public function create(): RedirectResponse */ public function edit(RouteParams $routeParams): Response { - $this->ensurePermission('pages.edit'); + if (!$this->hasPermission('pages.edit')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $page = $this->site()->findPage($routeParams->get('page')); @@ -263,9 +267,11 @@ public function preview(RouteParams $routeParams): Response /** * Pages@reorder action */ - public function reorder(): JsonResponse + public function reorder(): JsonResponse|Response { - $this->ensurePermission('pages.reorder'); + if (!$this->hasPermission('pages.reorder')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $requestData = $this->request->input(); @@ -306,9 +312,11 @@ public function reorder(): JsonResponse /** * Pages@delete action */ - public function delete(RouteParams $routeParams): RedirectResponse + public function delete(RouteParams $routeParams): Response { - $this->ensurePermission('pages.delete'); + if (!$this->hasPermission('pages.delete')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $page = $this->site()->findPage($routeParams->get('page')); @@ -353,9 +361,11 @@ public function delete(RouteParams $routeParams): RedirectResponse /** * Pages@uploadFile action */ - public function uploadFile(RouteParams $routeParams): RedirectResponse + public function uploadFile(RouteParams $routeParams): Response { - $this->ensurePermission('pages.uploadFiles'); + if (!$this->hasPermission('pages.uploadFiles')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $page = $this->site()->findPage($routeParams->get('page')); @@ -380,9 +390,11 @@ public function uploadFile(RouteParams $routeParams): RedirectResponse /** * Pages@deleteFile action */ - public function deleteFile(RouteParams $routeParams): RedirectResponse + public function deleteFile(RouteParams $routeParams): Response { - $this->ensurePermission('pages.deleteFiles'); + if (!$this->hasPermission('pages.deleteFiles')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $page = $this->site()->findPage($routeParams->get('page')); @@ -405,9 +417,11 @@ public function deleteFile(RouteParams $routeParams): RedirectResponse /** * Pages@renameFile action */ - public function renameFile(RouteParams $routeParams, Request $request): RedirectResponse + public function renameFile(RouteParams $routeParams): Response { - $this->ensurePermission('pages.renameFiles'); + if (!$this->hasPermission('pages.renameFiles')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $page = $this->site()->findPage($routeParams->get('page')); @@ -421,7 +435,7 @@ public function renameFile(RouteParams $routeParams, Request $request): Redirect return $this->redirect($this->generateRoute('panel.pages.edit', ['page' => $routeParams->get('page')])); } - $name = Str::slug(FileSystem::name($request->input()->get('filename'))); + $name = Str::slug(FileSystem::name($this->request->input()->get('filename'))); $extension = FileSystem::extension($routeParams->get('filename')); $newName = $name . '.' . $extension; @@ -439,7 +453,7 @@ public function renameFile(RouteParams $routeParams, Request $request): Redirect $previousFileRoute = $this->generateRoute('panel.pages.file', ['page' => $routeParams->get('page'), 'filename' => $previousName]); - if (Str::removeEnd((string) Uri::path($request->referer()), '/') === $this->site()->uri($previousFileRoute)) { + if (Str::removeEnd((string) Uri::path($this->request->referer()), '/') === $this->site()->uri($previousFileRoute)) { return $this->redirect($this->generateRoute('panel.pages.file', ['page' => $routeParams->get('page'), 'filename' => $newName])); } @@ -449,9 +463,11 @@ public function renameFile(RouteParams $routeParams, Request $request): Redirect /** * Pages@replaceFile action */ - public function replaceFile(RouteParams $routeParams): RedirectResponse + public function replaceFile(RouteParams $routeParams): Response { - $this->ensurePermission('pages.replaceFiles'); + if (!$this->hasPermission('pages.replaceFiles')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $page = $this->site()->findPage($routeParams->get('page')); @@ -492,7 +508,9 @@ public function replaceFile(RouteParams $routeParams): RedirectResponse */ public function file(RouteParams $routeParams): Response { - $this->ensurePermission('pages.file'); + if (!$this->hasPermission('pages.file')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $page = $this->site()->findPage($routeParams->get('page')); diff --git a/formwork/src/Panel/Controllers/RegisterController.php b/formwork/src/Panel/Controllers/RegisterController.php index bb04f8c90..b30fcc1ea 100644 --- a/formwork/src/Panel/Controllers/RegisterController.php +++ b/formwork/src/Panel/Controllers/RegisterController.php @@ -3,7 +3,6 @@ namespace Formwork\Panel\Controllers; use Formwork\Fields\Exceptions\ValidationException; -use Formwork\Http\Request; use Formwork\Http\RequestMethod; use Formwork\Http\Response; use Formwork\Log\Log; @@ -11,7 +10,6 @@ use Formwork\Panel\Security\Password; use Formwork\Parsers\Yaml; use Formwork\Schemes\Schemes; -use Formwork\Security\CsrfToken; use Formwork\Utils\FileSystem; use RuntimeException; @@ -20,17 +18,17 @@ class RegisterController extends AbstractController /** * Register@register action */ - public function register(Request $request, Schemes $schemes, CsrfToken $csrfToken): Response + public function register(Schemes $schemes): Response { if (!$this->site->users()->isEmpty()) { return $this->redirectToReferer(); } - $csrfToken->generate($this->panel()->getCsrfTokenName()); + $this->csrfToken->generate($this->panel()->getCsrfTokenName()); $fields = $schemes->get('forms.register')->fields(); - switch ($request->method()) { + switch ($this->request->method()) { case RequestMethod::GET: return new Response($this->view('register.register', [ 'title' => $this->translate('panel.register.register'), @@ -39,7 +37,7 @@ public function register(Request $request, Schemes $schemes, CsrfToken $csrfToke case RequestMethod::POST: try { - $fields->setValues($request->input())->validate(); + $fields->setValues($this->request->input())->validate(); } catch (ValidationException) { $this->panel()->notify($this->translate('panel.users.user.cannotCreate.varMissing'), 'error'); return $this->redirect($this->generateRoute('panel.index')); @@ -58,8 +56,8 @@ public function register(Request $request, Schemes $schemes, CsrfToken $csrfToke Yaml::encodeToFile($userData, FileSystem::joinPaths($this->config->get('system.users.paths.accounts'), $username . '.yaml')); - $request->session()->regenerate(); - $request->session()->set('FORMWORK_USERNAME', $username); + $this->request->session()->regenerate(); + $this->request->session()->set('FORMWORK_USERNAME', $username); $accessLog = new Log(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'access.json')); $lastAccessRegistry = new Registry(FileSystem::joinPaths($this->config->get('system.panel.paths.logs'), 'lastAccess.json')); diff --git a/formwork/src/Panel/Controllers/StatisticsController.php b/formwork/src/Panel/Controllers/StatisticsController.php index 93302cffe..9091bce19 100644 --- a/formwork/src/Panel/Controllers/StatisticsController.php +++ b/formwork/src/Panel/Controllers/StatisticsController.php @@ -13,7 +13,9 @@ class StatisticsController extends AbstractController */ public function index(Statistics $statistics): Response { - $this->ensurePermission('statistics'); + if (!$this->hasPermission('statistics')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $pageViews = $statistics->getPageViews(); diff --git a/formwork/src/Panel/Controllers/ToolsController.php b/formwork/src/Panel/Controllers/ToolsController.php index 2f62c6302..033b54814 100644 --- a/formwork/src/Panel/Controllers/ToolsController.php +++ b/formwork/src/Panel/Controllers/ToolsController.php @@ -6,7 +6,6 @@ use Formwork\Data\Collection; use Formwork\Http\Response; use Formwork\Parsers\Json; -use Formwork\Router\RouteParams; use Formwork\Utils\Arr; use Formwork\Utils\FileSystem; use Formwork\Utils\Str; @@ -25,16 +24,21 @@ class ToolsController extends AbstractController */ public function index(): Response { - $this->ensurePermission('tools.backups'); + if (!$this->hasPermission('tools.backups')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + return $this->redirect($this->generateRoute('panel.tools.backups')); } /** * Tools@backups action */ - public function backups(RouteParams $routeParams): Response + public function backups(): Response { - $this->ensurePermission('tools.backups'); + if (!$this->hasPermission('tools.backups')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $backupper = new Backupper($this->config); @@ -62,7 +66,9 @@ public function backups(RouteParams $routeParams): Response */ public function updates(): Response { - $this->ensurePermission('tools.updates'); + if (!$this->hasPermission('tools.updates')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } return new Response($this->view('tools.updates', [ 'title' => $this->translate('panel.tools.updates'), @@ -79,7 +85,9 @@ public function updates(): Response */ public function info(): Response { - $this->ensurePermission('tools.info'); + if (!$this->hasPermission('tools.info')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $opcacheStatus = extension_loaded('zend opcache') ? (opcache_get_status(false) ?: []) : []; diff --git a/formwork/src/Panel/Controllers/UpdatesController.php b/formwork/src/Panel/Controllers/UpdatesController.php index c22ea314a..45b9fd84b 100644 --- a/formwork/src/Panel/Controllers/UpdatesController.php +++ b/formwork/src/Panel/Controllers/UpdatesController.php @@ -6,6 +6,7 @@ use Formwork\Cache\AbstractCache; use Formwork\Exceptions\TranslatedException; use Formwork\Http\JsonResponse; +use Formwork\Http\Response; use Formwork\Http\ResponseStatus; use Formwork\Updater\Updater; use RuntimeException; @@ -15,9 +16,12 @@ class UpdatesController extends AbstractController /** * Updates@check action */ - public function check(Updater $updater): JsonResponse + public function check(Updater $updater): JsonResponse|Response { - $this->ensurePermission('updates.check'); + if (!$this->hasPermission('updates.check')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + try { $upToDate = $updater->checkUpdates(); } catch (RuntimeException) { @@ -39,9 +43,12 @@ public function check(Updater $updater): JsonResponse /** * Updates@update action */ - public function update(Updater $updater, AbstractCache $cache): JsonResponse + public function update(Updater $updater, AbstractCache $cache): JsonResponse|Response { - $this->ensurePermission('updates.update'); + if (!$this->hasPermission('updates.update')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + if ($this->config->get('system.updates.backupBefore')) { $backupper = new Backupper($this->config); try { diff --git a/formwork/src/Panel/Controllers/UsersController.php b/formwork/src/Panel/Controllers/UsersController.php index 89232c72e..ea9b2c129 100644 --- a/formwork/src/Panel/Controllers/UsersController.php +++ b/formwork/src/Panel/Controllers/UsersController.php @@ -8,7 +8,6 @@ use Formwork\Files\Services\FileUploader; use Formwork\Http\FileResponse; use Formwork\Http\Files\UploadedFile; -use Formwork\Http\RedirectResponse; use Formwork\Http\RequestMethod; use Formwork\Http\Response; use Formwork\Images\Image; @@ -16,7 +15,6 @@ use Formwork\Panel\Security\Password; use Formwork\Parsers\Yaml; use Formwork\Router\RouteParams; -use Formwork\Schemes\Schemes; use Formwork\Users\User; use Formwork\Utils\Arr; use Formwork\Utils\Exceptions\FileNotFoundException; @@ -27,9 +25,11 @@ class UsersController extends AbstractController /** * Users@index action */ - public function index(Schemes $schemes): Response + public function index(): Response { - $this->ensurePermission('users.index'); + if (!$this->hasPermission('users.index')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $this->modal('newUser'); @@ -44,9 +44,11 @@ public function index(Schemes $schemes): Response /** * Users@create action */ - public function create(Schemes $schemes): RedirectResponse + public function create(): Response { - $this->ensurePermission('users.create'); + if (!$this->hasPermission('users.create')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $requestData = $this->request->input(); @@ -83,9 +85,11 @@ public function create(Schemes $schemes): RedirectResponse /** * Users@delete action */ - public function delete(RouteParams $routeParams): RedirectResponse + public function delete(RouteParams $routeParams): Response { - $this->ensurePermission('users.delete'); + if (!$this->hasPermission('users.delete')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } $user = $this->site->users()->get($routeParams->get('user')); @@ -121,8 +125,12 @@ public function delete(RouteParams $routeParams): RedirectResponse /** * Users@deleteImage action */ - public function deleteImage(RouteParams $routeParams): RedirectResponse + public function deleteImage(RouteParams $routeParams): Response { + if (!$this->hasPermission('users.deleteImage')) { + return $this->forward(ErrorsController::class, 'forbidden'); + } + $user = $this->site->users()->get($routeParams->get('user')); if ($user === null) { diff --git a/formwork/src/Services/Loaders/ErrorHandlersServiceLoader.php b/formwork/src/Services/Loaders/ErrorHandlersServiceLoader.php deleted file mode 100644 index e4a799e84..000000000 --- a/formwork/src/Services/Loaders/ErrorHandlersServiceLoader.php +++ /dev/null @@ -1,23 +0,0 @@ -build(ErrorHandlers::class); - } - - /** - * @param ErrorHandlers $service - */ - public function onResolved(object $service, Container $container): void - { - $service->setHandlers(); - } -} diff --git a/formwork/src/Services/Loaders/PanelServiceLoader.php b/formwork/src/Services/Loaders/PanelServiceLoader.php index 485ee737f..78d2571c6 100644 --- a/formwork/src/Services/Loaders/PanelServiceLoader.php +++ b/formwork/src/Services/Loaders/PanelServiceLoader.php @@ -3,6 +3,7 @@ namespace Formwork\Services\Loaders; use Formwork\Config\Config; +use Formwork\Controllers\ErrorsControllerInterface; use Formwork\Http\Request; use Formwork\Log\Registry; use Formwork\Panel\Controllers\ErrorsController; @@ -16,7 +17,6 @@ use Formwork\Updater\Updater; use Formwork\Utils\FileSystem; use Formwork\View\ViewFactory; -use Throwable; class PanelServiceLoader implements ResolutionAwareServiceLoaderInterface { @@ -63,12 +63,10 @@ public function onResolved(object $service, Container $container): void $this->translations->setCurrent($this->config->get('system.panel.translation')); } - if ($service->isLoggedIn() && $this->config->get('system.errors.setHandlers')) { - $errorsController = $this->container->build(ErrorsController::class); - set_exception_handler(function (Throwable $throwable) use ($errorsController): never { - $errorsController->internalServerError($throwable)->prepare($this->request)->send(); - throw $throwable; - }); + if ($service->isLoggedIn()) { + $container->define(ErrorsController::class) + ->alias(ErrorsControllerInterface::class) + ->lazy(false); } } }