Files
test/system/src/Grav/Framework/Controller/Traits/ControllerResponseTrait.php
T
2026-01-16 16:29:06 +01:00

308 lines
9.1 KiB
PHP

<?php
/**
* @package Grav\Framework\Controller
*
* @copyright Copyright (c) 2015 - 2025 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
declare(strict_types=1);
namespace Grav\Framework\Controller\Traits;
use Grav\Common\Config\Config;
use Grav\Common\Data\ValidationException;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Utils;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Grav\Framework\Route\Route;
use JsonSerializable;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Throwable;
use function get_class;
use function in_array;
/**
* Trait ControllerResponseTrait
* @package Grav\Framework\Controller\Traits
*/
trait ControllerResponseTrait
{
/**
* Display the current page.
*
* @return Response
*/
protected function createDisplayResponse(): ResponseInterface
{
return new Response(418);
}
/**
* @param string $content
* @param int|null $code
* @param array|null $headers
* @return Response
*/
protected function createHtmlResponse(string $content, int $code = null, array $headers = null): ResponseInterface
{
$code = $code ?? 200;
if ($code < 100 || $code > 599) {
$code = 500;
}
$headers = $headers ?? [];
return new Response($code, $headers, $content);
}
/**
* @param array $content
* @param int|null $code
* @param array|null $headers
* @return Response
*/
protected function createJsonResponse(array $content, int $code = null, array $headers = null): ResponseInterface
{
$code = $code ?? $content['code'] ?? 200;
if (null === $code || $code < 100 || $code > 599) {
$code = 200;
}
$headers = ($headers ?? []) + [
'Content-Type' => 'application/json',
'Cache-Control' => 'no-store, max-age=0'
];
return new Response($code, $headers, json_encode($content));
}
/**
* @param string $filename
* @param string|resource|StreamInterface $resource
* @param array|null $headers
* @param array|null $options
* @return ResponseInterface
*/
protected function createDownloadResponse(string $filename, $resource, array $headers = null, array $options = null): ResponseInterface
{
// Required for IE, otherwise Content-Disposition may be ignored
if (ini_get('zlib.output_compression')) {
@ini_set('zlib.output_compression', 'Off');
}
$headers = $headers ?? [];
$options = $options ?? ['force_download' => true];
$file_parts = Utils::pathinfo($filename);
if (!isset($headers['Content-Type'])) {
$mimetype = Utils::getMimeByExtension($file_parts['extension']);
$headers['Content-Type'] = $mimetype;
}
// TODO: add multipart download support.
//$headers['Accept-Ranges'] = 'bytes';
if (!empty($options['force_download'])) {
$headers['Content-Disposition'] = 'attachment; filename="' . $file_parts['basename'] . '"';
}
if (!isset($headers['Content-Length'])) {
$realpath = realpath($filename);
if ($realpath) {
$headers['Content-Length'] = filesize($realpath);
}
}
$headers += [
'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
'Cache-Control' => 'no-store, no-cache, must-revalidate',
'Pragma' => 'no-cache'
];
return new Response(200, $headers, $resource);
}
/**
* @param string $url
* @param int|null $code
* @return Response
*/
protected function createRedirectResponse(string $url, int $code = null): ResponseInterface
{
if (null === $code || $code < 301 || $code > 307) {
$code = (int)$this->getConfig()->get('system.pages.redirect_default_code', 302);
}
$ext = Utils::pathinfo($url, PATHINFO_EXTENSION);
$accept = $this->getAccept(['application/json', 'text/html']);
if ($ext === 'json' || $accept === 'application/json') {
return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]);
}
return new Response($code, ['Location' => $url]);
}
/**
* @param Throwable $e
* @return ResponseInterface
*/
protected function createErrorResponse(Throwable $e): ResponseInterface
{
$response = $this->getErrorJson($e);
$message = $response['message'];
$code = $response['code'];
$reason = $e instanceof RequestException ? $e->getHttpReason() : null;
$accept = $this->getAccept(['application/json', 'text/html']);
$request = $this->getRequest();
$context = $request->getAttributes();
/** @var Route $route */
$route = $context['route'] ?? null;
$ext = $route ? $route->getExtension() : null;
if ($ext !== 'json' && $accept === 'text/html') {
$method = $request->getMethod();
// On POST etc, redirect back to the previous page.
if ($method !== 'GET' && $method !== 'HEAD') {
$this->setMessage($message, 'error');
$referer = $request->getHeaderLine('Referer');
return $this->createRedirectResponse($referer, 303);
}
// TODO: improve error page
return $this->createHtmlResponse($response['message'], $code);
}
return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
}
/**
* @param Throwable $e
* @return ResponseInterface
*/
protected function createJsonErrorResponse(Throwable $e): ResponseInterface
{
$response = $this->getErrorJson($e);
$reason = $e instanceof RequestException ? $e->getHttpReason() : null;
return new Response($response['code'], ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
}
/**
* @param Throwable $e
* @return array
*/
protected function getErrorJson(Throwable $e): array
{
$code = $this->getErrorCode($e instanceof RequestException ? $e->getHttpCode() : $e->getCode());
if ($e instanceof ValidationException) {
$message = $e->getMessage();
} else {
$message = htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
$extra = $e instanceof JsonSerializable ? $e->jsonSerialize() : [];
$response = [
'code' => $code,
'status' => 'error',
'message' => $message,
'redirect' => null,
'error' => [
'code' => $code,
'message' => $message
] + $extra
];
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
if ($debugger->enabled()) {
$response['error'] += [
'type' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => explode("\n", $e->getTraceAsString())
];
}
return $response;
}
/**
* @param int $code
* @return int
*/
protected function getErrorCode(int $code): int
{
static $errorCodes = [
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 (!in_array($code, $errorCodes, true)) {
$code = 500;
}
return $code;
}
/**
* @param array $compare
* @return mixed
*/
protected function getAccept(array $compare)
{
$accepted = [];
foreach ($this->getRequest()->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);
}
return reset($list);
}
/**
* @return ServerRequestInterface
*/
abstract protected function getRequest(): ServerRequestInterface;
/**
* @param string $message
* @param string $type
* @return $this
*/
abstract protected function setMessage(string $message, string $type = 'info');
/**
* @return Config
*/
abstract protected function getConfig(): Config;
}