Translated using Weblate (Portuguese)
[phpmyadmin.git] / src / ResponseRenderer.php
blob6231f5090dbdf68ef50aa18356c614ecf8853bdc
1 <?php
3 declare(strict_types=1);
5 namespace PhpMyAdmin;
7 use Fig\Http\Message\StatusCodeInterface;
8 use PhpMyAdmin\Bookmarks\BookmarkRepository;
9 use PhpMyAdmin\ConfigStorage\Relation;
10 use PhpMyAdmin\Error\ErrorHandler;
11 use PhpMyAdmin\Exceptions\ExitException;
12 use PhpMyAdmin\Html\MySQLDocumentation;
13 use PhpMyAdmin\Http\Factory\ResponseFactory;
14 use PhpMyAdmin\Http\Response;
16 use function __;
17 use function is_array;
18 use function json_encode;
19 use function json_last_error_msg;
20 use function mb_strlen;
21 use function str_starts_with;
22 use function substr;
24 /**
25 * Singleton class used to manage the rendering of pages in PMA
27 class ResponseRenderer
29 private static ResponseRenderer|null $instance = null;
31 /**
32 * Header instance
34 protected Header $header;
35 /**
36 * HTML data to be used in the response
38 private string $HTML = '';
39 /**
40 * An array of JSON key-value pairs
41 * to be sent back for ajax requests
43 * @var mixed[]
45 private array $JSON = [];
46 /**
47 * PhpMyAdmin\Footer instance
49 protected Footer $footer;
50 /**
51 * Whether we are servicing an ajax request.
53 protected bool $isAjax = false;
54 /**
55 * Whether response object is disabled
57 protected bool $isDisabled = false;
58 /**
59 * Whether there were any errors during the processing of the request
60 * Only used for ajax responses
62 protected bool $isSuccess = true;
64 /**
65 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
67 * @var array<int, string>
69 protected static array $httpStatusMessages = [
70 // Informational
71 100 => 'Continue',
72 101 => 'Switching Protocols',
73 102 => 'Processing',
74 103 => 'Early Hints',
75 // Success
76 200 => 'OK',
77 201 => 'Created',
78 202 => 'Accepted',
79 203 => 'Non-Authoritative Information',
80 204 => 'No Content',
81 205 => 'Reset Content',
82 206 => 'Partial Content',
83 207 => 'Multi-Status',
84 208 => 'Already Reported',
85 226 => 'IM Used',
86 // Redirection
87 300 => 'Multiple Choices',
88 301 => 'Moved Permanently',
89 302 => 'Found',
90 303 => 'See Other',
91 304 => 'Not Modified',
92 305 => 'Use Proxy',
93 307 => 'Temporary Redirect',
94 308 => 'Permanent Redirect',
95 // Client Error
96 400 => 'Bad Request',
97 401 => 'Unauthorized',
98 402 => 'Payment Required',
99 403 => 'Forbidden',
100 404 => 'Not Found',
101 405 => 'Method Not Allowed',
102 406 => 'Not Acceptable',
103 407 => 'Proxy Authentication Required',
104 408 => 'Request Timeout',
105 409 => 'Conflict',
106 410 => 'Gone',
107 411 => 'Length Required',
108 412 => 'Precondition Failed',
109 413 => 'Payload Too Large',
110 414 => 'URI Too Long',
111 415 => 'Unsupported Media Type',
112 416 => 'Range Not Satisfiable',
113 417 => 'Expectation Failed',
114 421 => 'Misdirected Request',
115 422 => 'Unprocessable Entity',
116 423 => 'Locked',
117 424 => 'Failed Dependency',
118 425 => 'Too Early',
119 426 => 'Upgrade Required',
120 427 => 'Unassigned',
121 428 => 'Precondition Required',
122 429 => 'Too Many Requests',
123 430 => 'Unassigned',
124 431 => 'Request Header Fields Too Large',
125 451 => 'Unavailable For Legal Reasons',
126 // Server Error
127 500 => 'Internal Server Error',
128 501 => 'Not Implemented',
129 502 => 'Bad Gateway',
130 503 => 'Service Unavailable',
131 504 => 'Gateway Timeout',
132 505 => 'HTTP Version Not Supported',
133 506 => 'Variant Also Negotiates',
134 507 => 'Insufficient Storage',
135 508 => 'Loop Detected',
136 509 => 'Unassigned',
137 510 => 'Not Extended',
138 511 => 'Network Authentication Required',
141 protected Response $response;
143 protected Template $template;
144 protected Config $config;
146 private function __construct()
148 $this->config = Config::getInstance();
149 $this->template = new Template();
150 $dbi = DatabaseInterface::getInstance();
151 $relation = new Relation($dbi);
152 $this->header = new Header(
153 $this->template,
154 new Console($relation, $this->template, new BookmarkRepository($dbi, $relation)),
155 $this->config,
157 $this->footer = new Footer($this->template, $this->config);
158 $this->response = ResponseFactory::create()->createResponse();
160 $this->setAjax(! empty($_REQUEST['ajax_request']));
164 * Set the ajax flag to indicate whether
165 * we are servicing an ajax request
167 * @param bool $isAjax Whether we are servicing an ajax request
169 public function setAjax(bool $isAjax): void
171 $this->isAjax = $isAjax;
172 $this->header->setAjax($this->isAjax);
173 $this->footer->setAjax($this->isAjax);
177 * Returns the singleton object
179 public static function getInstance(): ResponseRenderer
181 if (self::$instance === null) {
182 self::$instance = new ResponseRenderer();
185 return self::$instance;
189 * Set the status of an ajax response,
190 * whether it is a success or an error
192 * @param bool $state Whether the request was successfully processed
194 public function setRequestStatus(bool $state): void
196 $this->isSuccess = $state;
200 * Returns true or false depending on whether
201 * we are servicing an ajax request
203 public function isAjax(): bool
205 return $this->isAjax;
209 * Disables the rendering of the header
210 * and the footer in responses
212 public function disable(): void
214 $this->header->disable();
215 $this->footer->disable();
216 $this->isDisabled = true;
220 * Returns a PhpMyAdmin\Header object
222 public function getHeader(): Header
224 return $this->header;
228 * Append HTML code to the current output buffer
230 public function addHTML(string $content): void
232 $this->HTML .= $content;
236 * Add JSON code to the response
238 * @param string|int|mixed[] $json Either a key (string) or an array or key-value pairs
239 * @param mixed|null $value Null, if passing an array in $json otherwise
240 * it's a string value to the key
242 public function addJSON(string|int|array $json, mixed $value = null): void
244 if (is_array($json)) {
245 foreach ($json as $key => $value) {
246 $this->addJSON($key, $value);
248 } elseif ($value instanceof Message) {
249 $this->JSON[$json] = $value->getDisplay();
250 } else {
251 $this->JSON[$json] = $value;
256 * Renders the HTML response text
258 private function getDisplay(): string
260 // The header may contain nothing at all,
261 // if its content was already rendered
262 // and, in this case, the header will be
263 // in the content part of the request
264 return $this->template->render('base', [
265 'header' => $this->header->getDisplay(),
266 'content' => $this->HTML,
267 'footer' => $this->footer->getDisplay(),
272 * Sends a JSON response to the browser
274 private function ajaxResponse(): string
276 /* Avoid wrapping in case we're disabled */
277 if ($this->isDisabled) {
278 return $this->getDisplay();
281 if (! isset($this->JSON['message'])) {
282 $this->JSON['message'] = $this->getDisplay();
283 } elseif ($this->JSON['message'] instanceof Message) {
284 $this->JSON['message'] = $this->JSON['message']->getDisplay();
287 if ($this->isSuccess) {
288 $this->JSON['success'] = true;
289 } else {
290 $this->JSON['success'] = false;
291 $this->JSON['error'] = $this->JSON['message'];
292 unset($this->JSON['message']);
295 if ($this->isSuccess) {
296 if (! isset($this->JSON['title'])) {
297 $this->addJSON('title', '<title>' . $this->getHeader()->getPageTitle() . '</title>');
300 if (DatabaseInterface::getInstance()->isConnected()) {
301 $this->addJSON('menu', $this->getHeader()->getMenu()->getDisplay());
304 $this->addJSON('scripts', $this->getHeader()->getScripts()->getFiles());
305 $this->addJSON('selflink', $this->footer->getSelfUrl());
306 $this->addJSON('displayMessage', $this->getHeader()->getMessage());
308 $debug = $this->footer->getDebugMessage();
309 if (empty($_REQUEST['no_debug']) && $debug !== '') {
310 $this->addJSON('debug', $debug);
313 $errors = $this->footer->getErrorMessages();
314 if ($errors !== '') {
315 $this->addJSON('errors', $errors);
318 $promptPhpErrors = ErrorHandler::getInstance()->hasErrorsForPrompt();
319 $this->addJSON('promptPhpErrors', $promptPhpErrors);
321 if (empty($GLOBALS['error_message'])) {
322 // set current db, table and sql query in the querywindow
323 // (this is for the bottom console)
324 $query = '';
325 $maxChars = $this->config->settings['MaxCharactersInDisplayedSQL'];
326 if (isset($GLOBALS['sql_query']) && mb_strlen($GLOBALS['sql_query']) < $maxChars) {
327 $query = $GLOBALS['sql_query'];
330 $this->addJSON(
331 'reloadQuerywindow',
332 ['db' => Current::$database, 'table' => Current::$table, 'sql_query' => $query],
334 if (! empty($GLOBALS['focus_querywindow'])) {
335 $this->addJSON('_focusQuerywindow', $query);
338 if (! empty($GLOBALS['reload'])) {
339 $this->addJSON('reloadNavigation', 1);
342 $this->addJSON('params', $this->getHeader()->getJsParams());
346 // Set the Content-Type header to JSON so that jQuery parses the
347 // response correctly.
348 foreach (Core::headerJSON() as $name => $value) {
349 $this->addHeader($name, $value);
352 $result = json_encode($this->JSON);
353 if ($result === false) {
354 return (string) json_encode([
355 'success' => false,
356 'error' => 'JSON encoding failed: ' . json_last_error_msg(),
360 return $result;
363 public function response(): Response
365 $this->response->getBody()->write($this->isAjax() ? $this->ajaxResponse() : $this->getDisplay());
367 return $this->response;
370 public function addHeader(string $name, string $value): void
372 $this->response = $this->response->withHeader($name, $value);
375 /** @psalm-param StatusCodeInterface::STATUS_* $code */
376 public function setStatusCode(int $code): void
378 if (isset(static::$httpStatusMessages[$code])) {
379 $this->response = $this->response->withStatus($code, static::$httpStatusMessages[$code]);
380 } else {
381 $this->response = $this->response->withStatus($code);
386 * Configures response for the login page
388 * @return bool Whether caller should exit
390 public function loginPage(): bool
392 /* Handle AJAX redirection */
393 if ($this->isAjax()) {
394 $this->setRequestStatus(false);
395 // redirect_flag redirects to the login page
396 $this->addJSON('redirect_flag', '1');
398 return true;
401 $this->setMinimalFooter();
402 $header = $this->getHeader();
403 $header->setBodyId('loginform');
404 $header->setTitle('phpMyAdmin');
405 $header->disableMenuAndConsole();
406 $header->disableWarnings();
408 return false;
411 public function setMinimalFooter(): void
413 $this->footer->setMinimal();
416 public function getSelfUrl(): string
418 return $this->footer->getSelfUrl();
421 public function getFooterScripts(): Scripts
423 return $this->footer->getScripts();
426 public function callExit(string $message = ''): never
428 throw new ExitException($message);
432 * @psalm-param non-empty-string $url
433 * @psalm-param StatusCodeInterface::STATUS_* $statusCode
435 public function redirect(string $url, int $statusCode = StatusCodeInterface::STATUS_FOUND): void
438 * Avoid relative path redirect problems in case user entered URL
439 * like /phpmyadmin/index.php/ which some web servers happily accept.
441 if (str_starts_with($url, '.')) {
442 $url = $this->config->getRootPath() . substr($url, 2);
445 $this->addHeader('Location', $url);
446 $this->setStatusCode($statusCode);
449 /** @param array<string, mixed> $params */
450 public function redirectToRoute(string $route, array $params = []): void
452 $this->redirect('./index.php?route=' . $route . Url::getCommonRaw($params, '&'));
455 /** @psalm-param list<string> $files */
456 public function addScriptFiles(array $files): void
458 $this->getHeader()->getScripts()->addFiles($files);
462 * Function added to avoid path disclosures.
463 * Called by each script that needs parameters.
465 * @param bool $request Check parameters in request
466 * @psalm-param non-empty-list<non-empty-string> $params The names of the parameters needed by the calling script
468 public function checkParameters(array $params, bool $request = false): bool
470 $foundError = false;
471 $errorMessage = '';
472 $array = $request ? $_REQUEST : $GLOBALS;
474 foreach ($params as $param) {
475 if (isset($array[$param]) && $array[$param] !== '') {
476 continue;
479 if (! $request && $param === 'server' && Current::$server > 0) {
480 continue;
483 if (! $request && $param === 'db' && Current::$database !== '') {
484 continue;
487 if (! $request && $param === 'table' && Current::$table !== '') {
488 continue;
491 $errorMessage .=
492 __('Missing parameter:') . ' '
493 . $param
494 . MySQLDocumentation::showDocumentation('faq', 'faqmissingparameters', true)
495 . '[br]';
496 $foundError = true;
499 if ($foundError) {
500 $this->setStatusCode(StatusCodeInterface::STATUS_BAD_REQUEST);
501 $this->setRequestStatus(false);
502 $this->addHTML(Message::error($errorMessage)->getDisplay());
505 return ! $foundError;
508 /** @param array<string, mixed> $templateData */
509 public function render(string $templatePath, array $templateData = []): void
511 $this->addHTML($this->template->render($templatePath, $templateData));