From 902447a50c2d1c2fb9ad7dc4bcb65200df785ff9 Mon Sep 17 00:00:00 2001 From: Matias Griese Date: Tue, 18 Jun 2019 12:08:57 +0300 Subject: [PATCH] WIP: Added new controller for admin --- admin.php | 22 +- .../plugin/Controllers/AbstractController.php | 396 ++++++++++++++++++ classes/plugin/Router.php | 58 +++ 3 files changed, 475 insertions(+), 1 deletion(-) create mode 100644 classes/plugin/Controllers/AbstractController.php create mode 100644 classes/plugin/Router.php diff --git a/admin.php b/admin.php index 88d7e15b..ee26e0d1 100644 --- a/admin.php +++ b/admin.php @@ -13,6 +13,7 @@ use Grav\Common\Page\Interfaces\PageInterface; use Grav\Common\Page\Page; use Grav\Common\Page\Pages; use Grav\Common\Plugin; +use Grav\Common\Processors\Events\RequestHandlerEvent; use Grav\Common\Session; use Grav\Common\Uri; use Grav\Common\User\Interfaces\UserCollectionInterface; @@ -21,6 +22,7 @@ use Grav\Framework\Psr7\Response; use Grav\Framework\Session\Exceptions\SessionException; use Grav\Plugin\Admin\Admin; use Grav\Plugin\Admin\Popularity; +use Grav\Plugin\Admin\Router; use Grav\Plugin\Admin\Themes; use Grav\Plugin\Admin\AdminController; use Grav\Plugin\Admin\Twig\AdminTwigExtension; @@ -78,6 +80,9 @@ class AdminPlugin extends Plugin ['setup', 100000], ['onPluginsInitialized', 1001] ], + 'onRequestHandlerInit' => [ + ['onRequestHandlerInit', 100000] + ], 'onPageInitialized' => ['onPageInitialized', 0], 'onFormProcessed' => ['onFormProcessed', 0], 'onShutdown' => ['onShutdown', 1000], @@ -199,7 +204,7 @@ class AdminPlugin extends Plugin } // Turn on Twig autoescaping - if (method_exists($this->grav['twig'], 'setAutoescape') && $this->grav['uri']->param('task') !== 'processmarkdown') { + if ($this->grav['uri']->param('task') !== 'processmarkdown') { $this->grav['twig']->setAutoescape(true); } @@ -225,6 +230,21 @@ class AdminPlugin extends Plugin $this->grav->fireEvent('onAdminRegisterPermissions', new Event(['admin' => $this->admin])); } + /** + * [onRequestHandlerInit:100000] + * + * @param RequestHandlerEvent $event + */ + public function onRequestHandlerInit(RequestHandlerEvent $event) + { + $route = $event->getRoute(); + $base = $route->getRoute(0, 1); + + if ($base === $this->base) { + $event->addMiddleware('admin_router', new Router($this->grav)); + } + } + /** * [onPageInitialized:0] */ diff --git a/classes/plugin/Controllers/AbstractController.php b/classes/plugin/Controllers/AbstractController.php new file mode 100644 index 00000000..da30c82f --- /dev/null +++ b/classes/plugin/Controllers/AbstractController.php @@ -0,0 +1,396 @@ +getAttributes(); + $this->request = $request; + $this->grav = $attributes['grav'] ?? Grav::instance(); + $this->type = $attributes['type'] ?? null; + $this->key = $attributes['key'] ?? null; + + /** @var Route $route */ + $route = $attributes['route']; + $post = $this->getPost(); + + if ($this->isFormSubmit()) { + $form = $this->getForm(); + $this->nonce_name = $attributes['nonce_name'] ?? $form->getNonceName(); + $this->nonce_action = $attributes['nonce_action'] ?? $form->getNonceAction(); + } + + try { + $task = $request->getAttribute('task') ?? $post['task'] ?? $route->getParam('task'); + if ($task) { + if (empty($attributes['forwarded'])) { + $this->checkNonce($task); + } + $type = 'task'; + $command = $task; + } else { + $type = 'action'; + $command = $request->getAttribute('action') ?? $post['action'] ?? $route->getParam('action') ?? 'display'; + } + $command = strtolower($command); + + $event = new Event( + [ + 'controller' => $this, + 'response' => null + ] + ); + + $this->grav->fireEvent("admin.{$this->type}.{$type}.{$command}", $event); + + $response = $event['response']; + if (!$response) { + /** @var Inflector $inflector */ + $inflector = $this->grav['inflector']; + $method = $type . $inflector::camelize($command); + if ($method && method_exists($this, $method)) { + $response = $this->{$method}($request); + } else { + throw new NotFoundException($request); + } + } + } catch (\Exception $e) { + $response = $this->createErrorResponse($e); + } + + if ($response instanceof Response) { + return $response; + } + + return $this->createJsonResponse($response); + } + + /** + * Get request. + * + * @return ServerRequestInterface + */ + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + /** + * @param string|null $name + * @param mixed $default + * @return mixed + */ + public function getPost(string $name = null, $default = null) + { + $body = $this->request->getParsedBody(); + + if ($name) { + return $body[$name] ?? $default; + } + + return $body; + } + + /** + * Check if a form has been submitted. + * + * @return bool + */ + public function isFormSubmit(): bool + { + return (bool)$this->getPost('__form-name__'); + } + + /** + * Get form. + * + * @param string|null $type + * @return FormInterface + */ + public function getForm(string $type = null): FormInterface + { + $object = $this->getObject(); + if (!$object) { + throw new \RuntimeException('Not Found', 404); + } + + $formName = $this->getPost('__form-name__'); + $uniqueId = $this->getPost('__unique_form_id__') ?: $formName; + + $form = $object->getForm($type ?? 'edit'); + if ($uniqueId) { + $form->setUniqueId($uniqueId); + } + + return $form; + } + + /** + * Get Grav instance. + * + * @return Grav + */ + public function getGrav(): Grav + { + return $this->grav; + } + + /** + * Get session. + * + * @return SessionInterface + */ + public function getSession(): SessionInterface + { + return $this->getGrav()['session']; + } + + /** + * Display the current admin page. + * + * @return Response + */ + public function createDisplayResponse(): ResponseInterface + { + return new Response(418); + } + + /** + * Create custom HTML response. + * + * @param string $content + * @param int $code + * @return Response + */ + public function createHtmlResponse(string $content, int $code = null): ResponseInterface + { + return new Response($code ?: 200, [], $content); + } + + /** + * Create JSON response. + * + * @param array $content + * @return Response + */ + public function createJsonResponse(array $content): ResponseInterface + { + $code = $content['code'] ?? 200; + if ($code >= 301 && $code <= 307) { + $code = 200; + } + + return new Response($code, ['Content-Type' => 'application/json'], json_encode($content)); + } + + /** + * Create redirect response. + * + * @param string $url + * @param int $code + * @return Response + */ + public function createRedirectResponse(string $url, int $code = null): ResponseInterface + { + if (null === $code || $code < 301 || $code > 307) { + $code = $this->grav['config']->get('system.pages.redirect_default_code', 302); + } + + $accept = $this->getAccept(['application/json', 'text/html']); + + if ($accept === 'application/json') { + return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]); + } + + return new Response($code, ['Location' => $url]); + } + + /** + * Create error response. + * + * @param \Exception $exception + * @return Response + */ + public function createErrorResponse(\Exception $exception): ResponseInterface + { + $validCodes = [ + 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, + 422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511 + ]; + + if ($exception instanceof RequestException) { + $code = $exception->getHttpCode(); + $reason = $exception->getHttpReason(); + } else { + $code = $exception->getCode(); + $reason = null; + } + + if (!in_array($code, $validCodes, true)) { + $code = 500; + } + + $message = $exception->getMessage(); + $response = [ + 'code' => $code, + 'status' => 'error', + 'message' => $message + ]; + + $accept = $this->getAccept(['application/json', 'text/html']); + + if ($accept === 'text/html') { + $method = $this->getRequest()->getMethod(); + + // On POST etc, redirect back to the previous page. + if ($method !== 'GET' && $method !== 'HEAD') { + $this->setMessage($message, 'error'); + $referer = $this->request->getHeaderLine('Referer'); + return $this->createRedirectResponse($referer, 303); + } + + // TODO: improve error page + return $this->createHtmlResponse($response['message']); + } + + return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason); + } + + /** + * Translate a string. + * + * @param string $string + * @return string + */ + public function translate(string $string): string + { + /** @var Language $language */ + $language = $this->grav['language']; + + return $language->translate($string); + } + + /** + * Set message to be shown in the admin. + * + * @param string $message + * @param string $type + * @return $this + */ + public function setMessage(string $message, string $type = 'info') + { + /** @var Message $messages */ + $messages = $this->grav['messages']; + $messages->add($message, $type); + + return $this; + } + + /** + * Check if request nonce is valid. + * + * @param string $task + * @throws PageExpiredException If nonce is not valid. + */ + protected function checkNonce(string $task): void + { + $nonce = null; + + if (\in_array(strtoupper($this->request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) { + $nonce = $this->getPost($this->nonce_name); + } + + if (!$nonce) { + $nonce = $this->grav['uri']->param($this->nonce_name); + } + + if (!$nonce) { + $nonce = $this->grav['uri']->query($this->nonce_name); + } + + if (!$nonce || !Utils::verifyNonce($nonce, $this->nonce_action)) { + throw new PageExpiredException($this->request); + } + } + + /** + * Return the best matching mime type for the request. + * + * @param string[] $compare + * @return string|null + */ + protected function getAccept(array $compare): ?string + { + $accepted = []; + foreach ($this->request->getHeader('Accept') as $accept) { + foreach (explode(',', $accept) as $item) { + if (!$item) { + continue; + } + + $split = explode(';q=', $item); + $mime = array_shift($split); + $priority = array_shift($split) ?? 1.0; + + $accepted[$mime] = $priority; + } + } + + arsort($accepted); + + // TODO: add support for image/* etc + $list = array_intersect($compare, array_keys($accepted)); + if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) { + return reset($compare) ?: null; + } + + return reset($list) ?: null; + } +} diff --git a/classes/plugin/Router.php b/classes/plugin/Router.php new file mode 100644 index 00000000..9b359e0a --- /dev/null +++ b/classes/plugin/Router.php @@ -0,0 +1,58 @@ +startTimer(); + + $context = $request->getAttributes(); + + /** @var Route $route */ + $route = $context['route']; + $normalized = mb_strtolower(trim($route->getRoute(), '/')); + $parts = explode('/', $normalized); + array_shift($parts); + $key = array_shift($parts); + $path = implode('/', $parts); + + $request = $request->withAttribute('admin', ['path' => $path, 'parts' => $parts]); + + $response = null; + /* + if ($key === '__TODO__') { + + $controller = new TodoController(); + + $response = $controller->handle($request); + } + */ + + if (!$response) { + // Fallback to the old admin behavior. + $response = $handler->handle($request); + } + + $this->stopTimer(); + + return $response; + } +}