1<?php 2/** 3 * Manages the rendering of pages in PMA 4 */ 5 6declare(strict_types=1); 7 8namespace PhpMyAdmin; 9 10use const JSON_ERROR_CTRL_CHAR; 11use const JSON_ERROR_DEPTH; 12use const JSON_ERROR_INF_OR_NAN; 13use const JSON_ERROR_NONE; 14use const JSON_ERROR_RECURSION; 15use const JSON_ERROR_STATE_MISMATCH; 16use const JSON_ERROR_SYNTAX; 17use const JSON_ERROR_UNSUPPORTED_TYPE; 18use const JSON_ERROR_UTF8; 19use const PHP_SAPI; 20use function defined; 21use function explode; 22use function headers_sent; 23use function http_response_code; 24use function in_array; 25use function is_array; 26use function json_encode; 27use function json_last_error; 28use function mb_strlen; 29use function register_shutdown_function; 30use function strlen; 31 32/** 33 * Singleton class used to manage the rendering of pages in PMA 34 */ 35class Response 36{ 37 /** 38 * Response instance 39 * 40 * @access private 41 * @static 42 * @var Response 43 */ 44 private static $instance; 45 /** 46 * Header instance 47 * 48 * @access private 49 * @var Header 50 */ 51 protected $header; 52 /** 53 * HTML data to be used in the response 54 * 55 * @access private 56 * @var string 57 */ 58 private $HTML; 59 /** 60 * An array of JSON key-value pairs 61 * to be sent back for ajax requests 62 * 63 * @access private 64 * @var array 65 */ 66 private $JSON; 67 /** 68 * PhpMyAdmin\Footer instance 69 * 70 * @access private 71 * @var Footer 72 */ 73 protected $footer; 74 /** 75 * Whether we are servicing an ajax request. 76 * 77 * @access private 78 * @var bool 79 */ 80 protected $isAjax; 81 /** 82 * Whether response object is disabled 83 * 84 * @access private 85 * @var bool 86 */ 87 private $isDisabled; 88 /** 89 * Whether there were any errors during the processing of the request 90 * Only used for ajax responses 91 * 92 * @access private 93 * @var bool 94 */ 95 protected $isSuccess; 96 97 /** 98 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 99 * 100 * @var array<int, string> 101 */ 102 protected static $httpStatusMessages = [ 103 // Informational 104 100 => 'Continue', 105 101 => 'Switching Protocols', 106 102 => 'Processing', 107 103 => 'Early Hints', 108 // Success 109 200 => 'OK', 110 201 => 'Created', 111 202 => 'Accepted', 112 203 => 'Non-Authoritative Information', 113 204 => 'No Content', 114 205 => 'Reset Content', 115 206 => 'Partial Content', 116 207 => 'Multi-Status', 117 208 => 'Already Reported', 118 226 => 'IM Used', 119 // Redirection 120 300 => 'Multiple Choices', 121 301 => 'Moved Permanently', 122 302 => 'Found', 123 303 => 'See Other', 124 304 => 'Not Modified', 125 305 => 'Use Proxy', 126 307 => 'Temporary Redirect', 127 308 => 'Permanent Redirect', 128 // Client Error 129 400 => 'Bad Request', 130 401 => 'Unauthorized', 131 402 => 'Payment Required', 132 403 => 'Forbidden', 133 404 => 'Not Found', 134 405 => 'Method Not Allowed', 135 406 => 'Not Acceptable', 136 407 => 'Proxy Authentication Required', 137 408 => 'Request Timeout', 138 409 => 'Conflict', 139 410 => 'Gone', 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', 149 423 => 'Locked', 150 424 => 'Failed Dependency', 151 425 => 'Too Early', 152 426 => 'Upgrade Required', 153 427 => 'Unassigned', 154 428 => 'Precondition Required', 155 429 => 'Too Many Requests', 156 430 => 'Unassigned', 157 431 => 'Request Header Fields Too Large', 158 451 => 'Unavailable For Legal Reasons', 159 // Server Error 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', 169 509 => 'Unassigned', 170 510 => 'Not Extended', 171 511 => 'Network Authentication Required', 172 ]; 173 174 /** 175 * Creates a new class instance 176 */ 177 private function __construct() 178 { 179 if (! defined('TESTSUITE')) { 180 $buffer = OutputBuffering::getInstance(); 181 $buffer->start(); 182 register_shutdown_function([$this, 'response']); 183 } 184 $this->header = new Header(); 185 $this->HTML = ''; 186 $this->JSON = []; 187 $this->footer = new Footer(); 188 189 $this->isSuccess = true; 190 $this->isDisabled = false; 191 $this->setAjax(! empty($_REQUEST['ajax_request'])); 192 } 193 194 /** 195 * Set the ajax flag to indicate whether 196 * we are servicing an ajax request 197 * 198 * @param bool $isAjax Whether we are servicing an ajax request 199 */ 200 public function setAjax(bool $isAjax): void 201 { 202 $this->isAjax = $isAjax; 203 $this->header->setAjax($this->isAjax); 204 $this->footer->setAjax($this->isAjax); 205 } 206 207 /** 208 * Returns the singleton Response object 209 * 210 * @return Response object 211 */ 212 public static function getInstance() 213 { 214 if (empty(self::$instance)) { 215 self::$instance = new Response(); 216 } 217 218 return self::$instance; 219 } 220 221 /** 222 * Set the status of an ajax response, 223 * whether it is a success or an error 224 * 225 * @param bool $state Whether the request was successfully processed 226 */ 227 public function setRequestStatus(bool $state): void 228 { 229 $this->isSuccess = ($state === true); 230 } 231 232 /** 233 * Returns true or false depending on whether 234 * we are servicing an ajax request 235 */ 236 public function isAjax(): bool 237 { 238 return $this->isAjax; 239 } 240 241 /** 242 * Disables the rendering of the header 243 * and the footer in responses 244 * 245 * @return void 246 */ 247 public function disable() 248 { 249 $this->header->disable(); 250 $this->footer->disable(); 251 $this->isDisabled = true; 252 } 253 254 /** 255 * Returns a PhpMyAdmin\Header object 256 * 257 * @return Header 258 */ 259 public function getHeader() 260 { 261 return $this->header; 262 } 263 264 /** 265 * Returns a PhpMyAdmin\Footer object 266 * 267 * @return Footer 268 */ 269 public function getFooter() 270 { 271 return $this->footer; 272 } 273 274 /** 275 * Add HTML code to the response 276 * 277 * @param string $content A string to be appended to 278 * the current output buffer 279 * 280 * @return void 281 */ 282 public function addHTML($content) 283 { 284 if (is_array($content)) { 285 foreach ($content as $msg) { 286 $this->addHTML($msg); 287 } 288 } elseif ($content instanceof Message) { 289 $this->HTML .= $content->getDisplay(); 290 } else { 291 $this->HTML .= $content; 292 } 293 } 294 295 /** 296 * Add JSON code to the response 297 * 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 302 * 303 * @return void 304 */ 305 public function addJSON($json, $value = null) 306 { 307 if (is_array($json)) { 308 foreach ($json as $key => $value) { 309 $this->addJSON($key, $value); 310 } 311 } else { 312 if ($value instanceof Message) { 313 $this->JSON[$json] = $value->getDisplay(); 314 } else { 315 $this->JSON[$json] = $value; 316 } 317 } 318 } 319 320 /** 321 * Renders the HTML response text 322 * 323 * @return string 324 */ 325 private function getDisplay() 326 { 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(); 334 335 return $retval; 336 } 337 338 /** 339 * Sends an HTML response to the browser 340 * 341 * @return void 342 */ 343 private function htmlResponse() 344 { 345 echo $this->getDisplay(); 346 } 347 348 /** 349 * Sends a JSON response to the browser 350 * 351 * @return void 352 */ 353 private function ajaxResponse() 354 { 355 global $dbi; 356 357 /* Avoid wrapping in case we're disabled */ 358 if ($this->isDisabled) { 359 echo $this->getDisplay(); 360 361 return; 362 } 363 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(); 368 } 369 370 if ($this->isSuccess) { 371 $this->JSON['success'] = true; 372 } else { 373 $this->JSON['success'] = false; 374 $this->JSON['error'] = $this->JSON['message']; 375 unset($this->JSON['message']); 376 } 377 378 if ($this->isSuccess) { 379 if (! isset($this->JSON['title'])) { 380 $this->addJSON('title', '<title>' . $this->getHeader()->getPageTitle() . '</title>'); 381 } 382 383 if (isset($dbi)) { 384 $menuHash = $this->getHeader()->getMenu()->getHash(); 385 $this->addJSON('menuHash', $menuHash); 386 $hashes = []; 387 if (isset($_REQUEST['menuHashes'])) { 388 $hashes = explode('-', $_REQUEST['menuHashes']); 389 } 390 if (! in_array($menuHash, $hashes)) { 391 $this->addJSON( 392 'menu', 393 $this->getHeader() 394 ->getMenu() 395 ->getDisplay() 396 ); 397 } 398 } 399 400 $this->addJSON('scripts', $this->getHeader()->getScripts()->getFiles()); 401 $this->addJSON('selflink', $this->getFooter()->getSelfUrl()); 402 $this->addJSON('displayMessage', $this->getHeader()->getMessage()); 403 404 $debug = $this->footer->getDebugMessage(); 405 if (empty($_REQUEST['no_debug']) 406 && strlen($debug) > 0 407 ) { 408 $this->addJSON('debug', $debug); 409 } 410 411 $errors = $this->footer->getErrorMessages(); 412 if (strlen($errors) > 0) { 413 $this->addJSON('errors', $errors); 414 } 415 $promptPhpErrors = $GLOBALS['error_handler']->hasErrorsForPrompt(); 416 $this->addJSON('promptPhpErrors', $promptPhpErrors); 417 418 if (empty($GLOBALS['error_message'])) { 419 // set current db, table and sql query in the querywindow 420 // (this is for the bottom console) 421 $query = ''; 422 $maxChars = $GLOBALS['cfg']['MaxCharactersInDisplayedSQL']; 423 if (isset($GLOBALS['sql_query']) 424 && mb_strlen($GLOBALS['sql_query']) < $maxChars 425 ) { 426 $query = $GLOBALS['sql_query']; 427 } 428 $this->addJSON( 429 'reloadQuerywindow', 430 [ 431 'db' => Core::ifSetOr($GLOBALS['db'], ''), 432 'table' => Core::ifSetOr($GLOBALS['table'], ''), 433 'sql_query' => $query, 434 ] 435 ); 436 if (! empty($GLOBALS['focus_querywindow'])) { 437 $this->addJSON('_focusQuerywindow', $query); 438 } 439 if (! empty($GLOBALS['reload'])) { 440 $this->addJSON('reloadNavigation', 1); 441 } 442 $this->addJSON('params', $this->getHeader()->getJsParams()); 443 } 444 } 445 446 // Set the Content-Type header to JSON so that jQuery parses the 447 // response correctly. 448 Core::headerJSON(); 449 450 $result = json_encode($this->JSON); 451 if ($result === false) { 452 switch (json_last_error()) { 453 case JSON_ERROR_NONE: 454 $error = 'No errors'; 455 break; 456 case JSON_ERROR_DEPTH: 457 $error = 'Maximum stack depth exceeded'; 458 break; 459 case JSON_ERROR_STATE_MISMATCH: 460 $error = 'Underflow or the modes mismatch'; 461 break; 462 case JSON_ERROR_CTRL_CHAR: 463 $error = 'Unexpected control character found'; 464 break; 465 case JSON_ERROR_SYNTAX: 466 $error = 'Syntax error, malformed JSON'; 467 break; 468 case JSON_ERROR_UTF8: 469 $error = 'Malformed UTF-8 characters, possibly incorrectly encoded'; 470 break; 471 case JSON_ERROR_RECURSION: 472 $error = 'One or more recursive references in the value to be encoded'; 473 break; 474 case JSON_ERROR_INF_OR_NAN: 475 $error = 'One or more NAN or INF values in the value to be encoded'; 476 break; 477 case JSON_ERROR_UNSUPPORTED_TYPE: 478 $error = 'A value of a type that cannot be encoded was given'; 479 break; 480 default: 481 $error = 'Unknown error'; 482 break; 483 } 484 echo json_encode([ 485 'success' => false, 486 'error' => 'JSON encoding failed: ' . $error, 487 ]); 488 } else { 489 echo $result; 490 } 491 } 492 493 /** 494 * Sends an HTML response to the browser 495 * 496 * @return void 497 */ 498 public function response() 499 { 500 $buffer = OutputBuffering::getInstance(); 501 if (empty($this->HTML)) { 502 $this->HTML = $buffer->getContents(); 503 } 504 if ($this->isAjax()) { 505 $this->ajaxResponse(); 506 } else { 507 $this->htmlResponse(); 508 } 509 $buffer->flush(); 510 exit; 511 } 512 513 /** 514 * Wrapper around PHP's header() function. 515 * 516 * @param string $text header string 517 * 518 * @return void 519 */ 520 public function header($text) 521 { 522 // phpcs:ignore SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly 523 header($text); 524 } 525 526 /** 527 * Wrapper around PHP's headers_sent() function. 528 * 529 * @return bool 530 */ 531 public function headersSent() 532 { 533 return headers_sent(); 534 } 535 536 /** 537 * Wrapper around PHP's http_response_code() function. 538 * 539 * @param int $response_code will set the response code. 540 * 541 * @return void 542 */ 543 public function httpResponseCode($response_code) 544 { 545 http_response_code($response_code); 546 } 547 548 /** 549 * Sets http response code. 550 * 551 * @param int $responseCode will set the response code. 552 */ 553 public function setHttpResponseCode(int $responseCode): void 554 { 555 $this->httpResponseCode($responseCode); 556 $header = 'status: ' . $responseCode . ' '; 557 if (isset(static::$httpStatusMessages[$responseCode])) { 558 $header .= static::$httpStatusMessages[$responseCode]; 559 } else { 560 $header .= 'Web server is down'; 561 } 562 if (PHP_SAPI === 'cgi-fcgi') { 563 return; 564 } 565 566 $this->header($header); 567 } 568 569 /** 570 * Generate header for 303 571 * 572 * @param string $location will set location to redirect. 573 * 574 * @return void 575 */ 576 public function generateHeader303($location) 577 { 578 $this->setHttpResponseCode(303); 579 $this->header('Location: ' . $location); 580 if (! defined('TESTSUITE')) { 581 exit; 582 } 583 } 584 585 /** 586 * Configures response for the login page 587 * 588 * @return bool Whether caller should exit 589 */ 590 public function loginPage() 591 { 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'); 597 598 return true; 599 } 600 601 $this->getFooter()->setMinimal(); 602 $header = $this->getHeader(); 603 $header->setBodyId('loginform'); 604 $header->setTitle('phpMyAdmin'); 605 $header->disableMenuAndConsole(); 606 $header->disableWarnings(); 607 608 return false; 609 } 610} 611