Response.php 16 KB

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