3 * Manages the rendering of pages in PMA
6 declare(strict_types
=1);
10 use const JSON_ERROR_CTRL_CHAR
;
11 use const JSON_ERROR_DEPTH
;
12 use const JSON_ERROR_INF_OR_NAN
;
13 use const JSON_ERROR_NONE
;
14 use const JSON_ERROR_RECURSION
;
15 use const JSON_ERROR_STATE_MISMATCH
;
16 use const JSON_ERROR_SYNTAX
;
17 use const JSON_ERROR_UNSUPPORTED_TYPE
;
18 use const JSON_ERROR_UTF8
;
22 use function headers_sent
;
23 use function http_response_code
;
24 use function in_array
;
25 use function is_array
;
26 use function json_encode
;
27 use function json_last_error
;
28 use function mb_strlen
;
29 use function register_shutdown_function
;
33 * Singleton class used to manage the rendering of pages in PMA
44 private static $instance;
53 * HTML data to be used in the response
60 * An array of JSON key-value pairs
61 * to be sent back for ajax requests
68 * PhpMyAdmin\Footer instance
75 * Whether we are servicing an ajax request.
82 * Whether response object is disabled
89 * Whether there were any errors during the processing of the request
90 * Only used for ajax responses
98 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
100 * @var array<int, string>
102 protected static $httpStatusMessages = [
105 101 => 'Switching Protocols',
107 103 => 'Early Hints',
112 203 => 'Non-Authoritative Information',
114 205 => 'Reset Content',
115 206 => 'Partial Content',
116 207 => 'Multi-Status',
117 208 => 'Already Reported',
120 300 => 'Multiple Choices',
121 301 => 'Moved Permanently',
124 304 => 'Not Modified',
126 307 => 'Temporary Redirect',
127 308 => 'Permanent Redirect',
129 400 => 'Bad Request',
130 401 => 'Unauthorized',
131 402 => 'Payment Required',
134 405 => 'Method Not Allowed',
135 406 => 'Not Acceptable',
136 407 => 'Proxy Authentication Required',
137 408 => 'Request Timeout',
140 411 => 'Length Required',
141 412 => 'Precondition Failed',
142 413 => 'Payload Too Large',
143 414 => 'URI Too Long',
144 415 => 'Unsupported Media Type',
145 416 => 'Range Not Satisfiable',
146 417 => 'Expectation Failed',
147 421 => 'Misdirected Request',
148 422 => 'Unprocessable Entity',
150 424 => 'Failed Dependency',
152 426 => 'Upgrade Required',
154 428 => 'Precondition Required',
155 429 => 'Too Many Requests',
157 431 => 'Request Header Fields Too Large',
158 451 => 'Unavailable For Legal Reasons',
160 500 => 'Internal Server Error',
161 501 => 'Not Implemented',
162 502 => 'Bad Gateway',
163 503 => 'Service Unavailable',
164 504 => 'Gateway Timeout',
165 505 => 'HTTP Version Not Supported',
166 506 => 'Variant Also Negotiates',
167 507 => 'Insufficient Storage',
168 508 => 'Loop Detected',
170 510 => 'Not Extended',
171 511 => 'Network Authentication Required',
175 * Creates a new class instance
177 private function __construct()
179 if (! defined('TESTSUITE')) {
180 $buffer = OutputBuffering
::getInstance();
182 register_shutdown_function([$this, 'response']);
184 $this->header
= new Header();
187 $this->footer
= new Footer();
189 $this->isSuccess
= true;
190 $this->isDisabled
= false;
191 $this->setAjax(! empty($_REQUEST['ajax_request']));
195 * Set the ajax flag to indicate whether
196 * we are servicing an ajax request
198 * @param bool $isAjax Whether we are servicing an ajax request
200 public function setAjax(bool $isAjax): void
202 $this->isAjax
= $isAjax;
203 $this->header
->setAjax($this->isAjax
);
204 $this->footer
->setAjax($this->isAjax
);
208 * Returns the singleton Response object
210 * @return Response object
212 public static function getInstance()
214 if (empty(self
::$instance)) {
215 self
::$instance = new Response();
218 return self
::$instance;
222 * Set the status of an ajax response,
223 * whether it is a success or an error
225 * @param bool $state Whether the request was successfully processed
227 public function setRequestStatus(bool $state): void
229 $this->isSuccess
= ($state === true);
233 * Returns true or false depending on whether
234 * we are servicing an ajax request
236 public function isAjax(): bool
238 return $this->isAjax
;
242 * Disables the rendering of the header
243 * and the footer in responses
247 public function disable()
249 $this->header
->disable();
250 $this->footer
->disable();
251 $this->isDisabled
= true;
255 * Returns a PhpMyAdmin\Header object
259 public function getHeader()
261 return $this->header
;
265 * Returns a PhpMyAdmin\Footer object
269 public function getFooter()
271 return $this->footer
;
275 * Add HTML code to the response
277 * @param string $content A string to be appended to
278 * the current output buffer
282 public function addHTML($content)
284 if (is_array($content)) {
285 foreach ($content as $msg) {
286 $this->addHTML($msg);
288 } elseif ($content instanceof Message
) {
289 $this->HTML
.= $content->getDisplay();
291 $this->HTML
.= $content;
296 * Add JSON code to the response
298 * @param mixed $json Either a key (string) or an
299 * array or key-value pairs
300 * @param mixed $value Null, if passing an array in $json otherwise
301 * it's a string value to the key
305 public function addJSON($json, $value = null)
307 if (is_array($json)) {
308 foreach ($json as $key => $value) {
309 $this->addJSON($key, $value);
312 if ($value instanceof Message
) {
313 $this->JSON
[$json] = $value->getDisplay();
315 $this->JSON
[$json] = $value;
321 * Renders the HTML response text
325 private function getDisplay()
327 // The header may contain nothing at all,
328 // if its content was already rendered
329 // and, in this case, the header will be
330 // in the content part of the request
331 $retval = $this->header
->getDisplay();
332 $retval .= $this->HTML
;
333 $retval .= $this->footer
->getDisplay();
339 * Sends an HTML response to the browser
343 private function htmlResponse()
345 echo $this->getDisplay();
349 * Sends a JSON response to the browser
353 private function ajaxResponse()
357 /* Avoid wrapping in case we're disabled */
358 if ($this->isDisabled
) {
359 echo $this->getDisplay();
364 if (! isset($this->JSON
['message'])) {
365 $this->JSON
['message'] = $this->getDisplay();
366 } elseif ($this->JSON
['message'] instanceof Message
) {
367 $this->JSON
['message'] = $this->JSON
['message']->getDisplay();
370 if ($this->isSuccess
) {
371 $this->JSON
['success'] = true;
373 $this->JSON
['success'] = false;
374 $this->JSON
['error'] = $this->JSON
['message'];
375 unset($this->JSON
['message']);
378 if ($this->isSuccess
) {
379 if (! isset($this->JSON
['title'])) {
380 $this->addJSON('title', '<title>' . $this->getHeader()->getPageTitle() . '</title>');
384 $menuHash = $this->getHeader()->getMenu()->getHash();
385 $this->addJSON('menuHash', $menuHash);
387 if (isset($_REQUEST['menuHashes'])) {
388 $hashes = explode('-', $_REQUEST['menuHashes']);
390 if (! in_array($menuHash, $hashes)) {
400 $this->addJSON('scripts', $this->getHeader()->getScripts()->getFiles());
401 $this->addJSON('selflink', $this->getFooter()->getSelfUrl());
402 $this->addJSON('displayMessage', $this->getHeader()->getMessage());
404 $debug = $this->footer
->getDebugMessage();
405 if (empty($_REQUEST['no_debug'])
406 && strlen($debug) > 0
408 $this->addJSON('debug', $debug);
411 $errors = $this->footer
->getErrorMessages();
412 if (strlen($errors) > 0) {
413 $this->addJSON('errors', $errors);
415 $promptPhpErrors = $GLOBALS['error_handler']->hasErrorsForPrompt();
416 $this->addJSON('promptPhpErrors', $promptPhpErrors);
418 if (empty($GLOBALS['error_message'])) {
419 // set current db, table and sql query in the querywindow
420 // (this is for the bottom console)
422 $maxChars = $GLOBALS['cfg']['MaxCharactersInDisplayedSQL'];
423 if (isset($GLOBALS['sql_query'])
424 && mb_strlen($GLOBALS['sql_query']) < $maxChars
426 $query = $GLOBALS['sql_query'];
431 'db' => Core
::ifSetOr($GLOBALS['db'], ''),
432 'table' => Core
::ifSetOr($GLOBALS['table'], ''),
433 'sql_query' => $query,
436 if (! empty($GLOBALS['focus_querywindow'])) {
437 $this->addJSON('_focusQuerywindow', $query);
439 if (! empty($GLOBALS['reload'])) {
440 $this->addJSON('reloadNavigation', 1);
442 $this->addJSON('params', $this->getHeader()->getJsParams());
446 // Set the Content-Type header to JSON so that jQuery parses the
447 // response correctly.
450 $result = json_encode($this->JSON
);
451 if ($result === false) {
452 switch (json_last_error()) {
453 case JSON_ERROR_NONE
:
454 $error = 'No errors';
456 case JSON_ERROR_DEPTH
:
457 $error = 'Maximum stack depth exceeded';
459 case JSON_ERROR_STATE_MISMATCH
:
460 $error = 'Underflow or the modes mismatch';
462 case JSON_ERROR_CTRL_CHAR
:
463 $error = 'Unexpected control character found';
465 case JSON_ERROR_SYNTAX
:
466 $error = 'Syntax error, malformed JSON';
468 case JSON_ERROR_UTF8
:
469 $error = 'Malformed UTF-8 characters, possibly incorrectly encoded';
471 case JSON_ERROR_RECURSION
:
472 $error = 'One or more recursive references in the value to be encoded';
474 case JSON_ERROR_INF_OR_NAN
:
475 $error = 'One or more NAN or INF values in the value to be encoded';
477 case JSON_ERROR_UNSUPPORTED_TYPE
:
478 $error = 'A value of a type that cannot be encoded was given';
481 $error = 'Unknown error';
486 'error' => 'JSON encoding failed: ' . $error,
494 * Sends an HTML response to the browser
498 public function response()
500 $buffer = OutputBuffering
::getInstance();
501 if (empty($this->HTML
)) {
502 $this->HTML
= $buffer->getContents();
504 if ($this->isAjax()) {
505 $this->ajaxResponse();
507 $this->htmlResponse();
514 * Wrapper around PHP's header() function.
516 * @param string $text header string
520 public function header($text)
522 // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly
527 * Wrapper around PHP's headers_sent() function.
531 public function headersSent()
533 return headers_sent();
537 * Wrapper around PHP's http_response_code() function.
539 * @param int $response_code will set the response code.
543 public function httpResponseCode($response_code)
545 http_response_code($response_code);
549 * Sets http response code.
551 * @param int $responseCode will set the response code.
553 public function setHttpResponseCode(int $responseCode): void
555 $this->httpResponseCode($responseCode);
556 $header = 'status: ' . $responseCode . ' ';
557 if (isset(static::$httpStatusMessages[$responseCode])) {
558 $header .= static::$httpStatusMessages[$responseCode];
560 $header .= 'Web server is down';
562 if (PHP_SAPI
=== 'cgi-fcgi') {
566 $this->header($header);
570 * Generate header for 303
572 * @param string $location will set location to redirect.
576 public function generateHeader303($location)
578 $this->setHttpResponseCode(303);
579 $this->header('Location: ' . $location);
580 if (! defined('TESTSUITE')) {
586 * Configures response for the login page
588 * @return bool Whether caller should exit
590 public function loginPage()
592 /* Handle AJAX redirection */
593 if ($this->isAjax()) {
594 $this->setRequestStatus(false);
595 // redirect_flag redirects to the login page
596 $this->addJSON('redirect_flag', '1');
601 $this->getFooter()->setMinimal();
602 $header = $this->getHeader();
603 $header->setBodyId('loginform');
604 $header->setTitle('phpMyAdmin');
605 $header->disableMenuAndConsole();
606 $header->disableWarnings();