mirror of
				https://github.com/getgrav/grav-plugin-admin.git
				synced 2025-11-03 20:05:53 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			415 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			415 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
/**
 | 
						|
 * @package    Grav\Plugin\Admin
 | 
						|
 *
 | 
						|
 * @copyright  Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
 | 
						|
 * @license    MIT License; see LICENSE file for details.
 | 
						|
 */
 | 
						|
 | 
						|
declare(strict_types=1);
 | 
						|
 | 
						|
namespace Grav\Plugin\Admin\Controllers;
 | 
						|
 | 
						|
use Grav\Common\Debugger;
 | 
						|
use Grav\Common\Grav;
 | 
						|
use Grav\Common\Inflector;
 | 
						|
use Grav\Common\Language\Language;
 | 
						|
use Grav\Common\Utils;
 | 
						|
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
 | 
						|
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) {
 | 
						|
            /** @var Debugger $debugger */
 | 
						|
            $debugger = $this->grav['debugger'];
 | 
						|
            $debugger->addException($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;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @return FlexObjectInterface
 | 
						|
     */
 | 
						|
    abstract public function getObject();
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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' => htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8')
 | 
						|
        ];
 | 
						|
 | 
						|
        $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($message, $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;
 | 
						|
    }
 | 
						|
}
 |