mirror of
				https://github.com/getgrav/grav-plugin-admin.git
				synced 2025-10-31 02:16:26 +01:00 
			
		
		
		
	
		
			
	
	
		
			397 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
		
		
			
		
	
	
			397 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
|  | <?php | ||
|  | 
 | ||
|  | declare(strict_types=1); | ||
|  | 
 | ||
|  | namespace Grav\Plugin\Admin\Controllers; | ||
|  | 
 | ||
|  | use Grav\Common\Grav; | ||
|  | use Grav\Common\Inflector; | ||
|  | use Grav\Common\Language\Language; | ||
|  | use Grav\Common\Utils; | ||
|  | use Grav\Framework\Form\Interfaces\FormInterface; | ||
|  | use Grav\Framework\Psr7\Response; | ||
|  | use Grav\Framework\RequestHandler\Exception\NotFoundException; | ||
|  | use Grav\Framework\RequestHandler\Exception\PageExpiredException; | ||
|  | use Grav\Framework\RequestHandler\Exception\RequestException; | ||
|  | use Grav\Framework\Route\Route; | ||
|  | use Grav\Framework\Session\SessionInterface; | ||
|  | use Psr\Http\Message\ResponseInterface; | ||
|  | use Psr\Http\Message\ServerRequestInterface; | ||
|  | use Psr\Http\Server\RequestHandlerInterface; | ||
|  | use RocketTheme\Toolbox\Event\Event; | ||
|  | use RocketTheme\Toolbox\Session\Message; | ||
|  | 
 | ||
|  | abstract class AbstractController implements RequestHandlerInterface | ||
|  | { | ||
|  |     /** @var string */ | ||
|  |     protected $nonce_action = 'admin-form'; | ||
|  | 
 | ||
|  |     /** @var string */ | ||
|  |     protected $nonce_name = 'admin-nonce'; | ||
|  | 
 | ||
|  |     /** @var ServerRequestInterface */ | ||
|  |     protected $request; | ||
|  | 
 | ||
|  |     /** @var Grav */ | ||
|  |     protected $grav; | ||
|  | 
 | ||
|  |     /** @var string */ | ||
|  |     protected $type; | ||
|  | 
 | ||
|  |     /** @var string */ | ||
|  |     protected $key; | ||
|  | 
 | ||
|  |     /** | ||
|  |      * Handle request. | ||
|  |      * | ||
|  |      * Fires event: admin.[directory].[task|action].[command] | ||
|  |      * | ||
|  |      * @param ServerRequestInterface $request | ||
|  |      * @return Response | ||
|  |      */ | ||
|  |     public function handle(ServerRequestInterface $request): ResponseInterface | ||
|  |     { | ||
|  |         $attributes = $request->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; | ||
|  |     } | ||
|  | } |