phpmyadmin/src/ResponseRenderer.php
Maurício Meneghini Fauth a858f08fec Move error related classes to the Error namespace
Signed-off-by: Maurício Meneghini Fauth <mauricio@fauth.dev>
2024-01-05 13:28:23 -03:00

448 lines
13 KiB
PHP

<?php
/**
* Manages the rendering of pages in PMA
*/
declare(strict_types=1);
namespace PhpMyAdmin;
use Fig\Http\Message\StatusCodeInterface;
use PhpMyAdmin\Bookmarks\BookmarkRepository;
use PhpMyAdmin\ConfigStorage\Relation;
use PhpMyAdmin\Error\ErrorHandler;
use PhpMyAdmin\Exceptions\ExitException;
use PhpMyAdmin\Http\Factory\ResponseFactory;
use PhpMyAdmin\Http\Response;
use function is_array;
use function json_encode;
use function json_last_error_msg;
use function mb_strlen;
use function str_starts_with;
use function strlen;
use function substr;
/**
* Singleton class used to manage the rendering of pages in PMA
*/
class ResponseRenderer
{
private static ResponseRenderer|null $instance = null;
/**
* Header instance
*/
protected Header $header;
/**
* HTML data to be used in the response
*/
private string $HTML = '';
/**
* An array of JSON key-value pairs
* to be sent back for ajax requests
*
* @var mixed[]
*/
private array $JSON = [];
/**
* PhpMyAdmin\Footer instance
*/
protected Footer $footer;
/**
* Whether we are servicing an ajax request.
*/
protected bool $isAjax = false;
/**
* Whether response object is disabled
*/
protected bool $isDisabled = false;
/**
* Whether there were any errors during the processing of the request
* Only used for ajax responses
*/
protected bool $isSuccess = true;
/**
* @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
*
* @var array<int, string>
*/
protected static array $httpStatusMessages = [
// Informational
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
103 => 'Early Hints',
// Success
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
// Redirection
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
// Client Error
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Payload Too Large',
414 => 'URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Range Not Satisfiable',
417 => 'Expectation Failed',
421 => 'Misdirected Request',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Too Early',
426 => 'Upgrade Required',
427 => 'Unassigned',
428 => 'Precondition Required',
429 => 'Too Many Requests',
430 => 'Unassigned',
431 => 'Request Header Fields Too Large',
451 => 'Unavailable For Legal Reasons',
// Server Error
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
509 => 'Unassigned',
510 => 'Not Extended',
511 => 'Network Authentication Required',
];
protected Response $response;
protected Template $template;
private function __construct()
{
$this->template = new Template();
$dbi = DatabaseInterface::getInstance();
$relation = new Relation($dbi);
$this->header = new Header(
$this->template,
new Console($relation, $this->template, new BookmarkRepository($dbi, $relation)),
);
$this->footer = new Footer($this->template);
$this->response = ResponseFactory::create()->createResponse();
$this->setAjax(! empty($_REQUEST['ajax_request']));
}
/**
* Set the ajax flag to indicate whether
* we are servicing an ajax request
*
* @param bool $isAjax Whether we are servicing an ajax request
*/
public function setAjax(bool $isAjax): void
{
$this->isAjax = $isAjax;
$this->header->setAjax($this->isAjax);
$this->footer->setAjax($this->isAjax);
}
/**
* Returns the singleton object
*/
public static function getInstance(): ResponseRenderer
{
if (self::$instance === null) {
self::$instance = new ResponseRenderer();
}
return self::$instance;
}
/**
* Set the status of an ajax response,
* whether it is a success or an error
*
* @param bool $state Whether the request was successfully processed
*/
public function setRequestStatus(bool $state): void
{
$this->isSuccess = $state;
}
/**
* Returns true or false depending on whether
* we are servicing an ajax request
*/
public function isAjax(): bool
{
return $this->isAjax;
}
/**
* Disables the rendering of the header
* and the footer in responses
*/
public function disable(): void
{
$this->header->disable();
$this->footer->disable();
$this->isDisabled = true;
}
/**
* Returns a PhpMyAdmin\Header object
*/
public function getHeader(): Header
{
return $this->header;
}
/**
* Append HTML code to the current output buffer
*/
public function addHTML(string $content): void
{
$this->HTML .= $content;
}
/**
* Add JSON code to the response
*
* @param string|int|mixed[] $json Either a key (string) or an array or key-value pairs
* @param mixed|null $value Null, if passing an array in $json otherwise
* it's a string value to the key
*/
public function addJSON(string|int|array $json, mixed $value = null): void
{
if (is_array($json)) {
foreach ($json as $key => $value) {
$this->addJSON($key, $value);
}
} elseif ($value instanceof Message) {
$this->JSON[$json] = $value->getDisplay();
} else {
$this->JSON[$json] = $value;
}
}
/**
* Renders the HTML response text
*/
private function getDisplay(): string
{
// The header may contain nothing at all,
// if its content was already rendered
// and, in this case, the header will be
// in the content part of the request
return $this->template->render('base', [
'header' => $this->header->getDisplay(),
'content' => $this->HTML,
'footer' => $this->footer->getDisplay(),
]);
}
/**
* Sends a JSON response to the browser
*/
private function ajaxResponse(): string
{
/* Avoid wrapping in case we're disabled */
if ($this->isDisabled) {
return $this->getDisplay();
}
if (! isset($this->JSON['message'])) {
$this->JSON['message'] = $this->getDisplay();
} elseif ($this->JSON['message'] instanceof Message) {
$this->JSON['message'] = $this->JSON['message']->getDisplay();
}
if ($this->isSuccess) {
$this->JSON['success'] = true;
} else {
$this->JSON['success'] = false;
$this->JSON['error'] = $this->JSON['message'];
unset($this->JSON['message']);
}
if ($this->isSuccess) {
if (! isset($this->JSON['title'])) {
$this->addJSON('title', '<title>' . $this->getHeader()->getPageTitle() . '</title>');
}
if (DatabaseInterface::getInstance()->isConnected()) {
$this->addJSON('menu', $this->getHeader()->getMenu()->getDisplay());
}
$this->addJSON('scripts', $this->getHeader()->getScripts()->getFiles());
$this->addJSON('selflink', $this->footer->getSelfUrl());
$this->addJSON('displayMessage', $this->getHeader()->getMessage());
$debug = $this->footer->getDebugMessage();
if (empty($_REQUEST['no_debug']) && strlen($debug) > 0) {
$this->addJSON('debug', $debug);
}
$errors = $this->footer->getErrorMessages();
if (strlen($errors) > 0) {
$this->addJSON('errors', $errors);
}
$promptPhpErrors = ErrorHandler::getInstance()->hasErrorsForPrompt();
$this->addJSON('promptPhpErrors', $promptPhpErrors);
if (empty($GLOBALS['error_message'])) {
// set current db, table and sql query in the querywindow
// (this is for the bottom console)
$query = '';
$maxChars = Config::getInstance()->settings['MaxCharactersInDisplayedSQL'];
if (isset($GLOBALS['sql_query']) && mb_strlen($GLOBALS['sql_query']) < $maxChars) {
$query = $GLOBALS['sql_query'];
}
$this->addJSON(
'reloadQuerywindow',
['db' => Current::$database, 'table' => Current::$table, 'sql_query' => $query],
);
if (! empty($GLOBALS['focus_querywindow'])) {
$this->addJSON('_focusQuerywindow', $query);
}
if (! empty($GLOBALS['reload'])) {
$this->addJSON('reloadNavigation', 1);
}
$this->addJSON('params', $this->getHeader()->getJsParams());
}
}
// Set the Content-Type header to JSON so that jQuery parses the
// response correctly.
foreach (Core::headerJSON() as $name => $value) {
$this->addHeader($name, $value);
}
$result = json_encode($this->JSON);
if ($result === false) {
return (string) json_encode([
'success' => false,
'error' => 'JSON encoding failed: ' . json_last_error_msg(),
]);
}
return $result;
}
public function response(): Response
{
$this->response->getBody()->write($this->isAjax() ? $this->ajaxResponse() : $this->getDisplay());
return $this->response;
}
public function addHeader(string $name, string $value): void
{
$this->response = $this->response->withHeader($name, $value);
}
/** @psalm-param StatusCodeInterface::STATUS_* $code */
public function setStatusCode(int $code): void
{
if (isset(static::$httpStatusMessages[$code])) {
$this->response = $this->response->withStatus($code, static::$httpStatusMessages[$code]);
} else {
$this->response = $this->response->withStatus($code);
}
}
/**
* Configures response for the login page
*
* @return bool Whether caller should exit
*/
public function loginPage(): bool
{
/* Handle AJAX redirection */
if ($this->isAjax()) {
$this->setRequestStatus(false);
// redirect_flag redirects to the login page
$this->addJSON('redirect_flag', '1');
return true;
}
$this->setMinimalFooter();
$header = $this->getHeader();
$header->setBodyId('loginform');
$header->setTitle('phpMyAdmin');
$header->disableMenuAndConsole();
$header->disableWarnings();
return false;
}
public function setMinimalFooter(): void
{
$this->footer->setMinimal();
}
public function getSelfUrl(): string
{
return $this->footer->getSelfUrl();
}
public function getFooterScripts(): Scripts
{
return $this->footer->getScripts();
}
public function callExit(string $message = ''): never
{
throw new ExitException($message);
}
/**
* @psalm-param non-empty-string $url
* @psalm-param StatusCodeInterface::STATUS_* $statusCode
*/
public function redirect(string $url, int $statusCode = StatusCodeInterface::STATUS_FOUND): void
{
/**
* Avoid relative path redirect problems in case user entered URL
* like /phpmyadmin/index.php/ which some web servers happily accept.
*/
if (str_starts_with($url, '.')) {
$url = Config::getInstance()->getRootPath() . substr($url, 2);
}
$this->addHeader('Location', $url);
$this->setStatusCode($statusCode);
}
}