wiki-grav/plugins/admin/classes/plugin/Controllers/AbstractController.php

415 lines
11 KiB
PHP
Raw Permalink Normal View History

<?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;
}
}