3 declare(strict_types
=1);
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
;
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
;
25 * Singleton class used to manage the rendering of pages in PMA
27 class ResponseRenderer
29 private static ResponseRenderer|
null $instance = null;
34 protected Header
$header;
36 * HTML data to be used in the response
38 private string $HTML = '';
40 * An array of JSON key-value pairs
41 * to be sent back for ajax requests
45 private array $JSON = [];
47 * PhpMyAdmin\Footer instance
49 protected Footer
$footer;
51 * Whether we are servicing an ajax request.
53 protected bool $isAjax = false;
55 * Whether response object is disabled
57 protected bool $isDisabled = false;
59 * Whether there were any errors during the processing of the request
60 * Only used for ajax responses
62 protected bool $isSuccess = true;
65 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
67 * @var array<int, string>
69 protected static array $httpStatusMessages = [
72 101 => 'Switching Protocols',
79 203 => 'Non-Authoritative Information',
81 205 => 'Reset Content',
82 206 => 'Partial Content',
83 207 => 'Multi-Status',
84 208 => 'Already Reported',
87 300 => 'Multiple Choices',
88 301 => 'Moved Permanently',
91 304 => 'Not Modified',
93 307 => 'Temporary Redirect',
94 308 => 'Permanent Redirect',
97 401 => 'Unauthorized',
98 402 => 'Payment Required',
101 405 => 'Method Not Allowed',
102 406 => 'Not Acceptable',
103 407 => 'Proxy Authentication Required',
104 408 => 'Request Timeout',
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',
117 424 => 'Failed Dependency',
119 426 => 'Upgrade Required',
121 428 => 'Precondition Required',
122 429 => 'Too Many Requests',
124 431 => 'Request Header Fields Too Large',
125 451 => 'Unavailable For Legal Reasons',
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',
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(
154 new Console($relation, $this->template
, new BookmarkRepository($dbi, $relation)),
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();
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;
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)
325 $maxChars = $this->config
->settings
['MaxCharactersInDisplayedSQL'];
326 if (isset($GLOBALS['sql_query']) && mb_strlen($GLOBALS['sql_query']) < $maxChars) {
327 $query = $GLOBALS['sql_query'];
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([
356 'error' => 'JSON encoding failed: ' . json_last_error_msg(),
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]);
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');
401 $this->setMinimalFooter();
402 $header = $this->getHeader();
403 $header->setBodyId('loginform');
404 $header->setTitle('phpMyAdmin');
405 $header->disableMenuAndConsole();
406 $header->disableWarnings();
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
472 $array = $request ?
$_REQUEST : $GLOBALS;
474 foreach ($params as $param) {
475 if (isset($array[$param]) && $array[$param] !== '') {
479 if (! $request && $param === 'server' && Current
::$server > 0) {
483 if (! $request && $param === 'db' && Current
::$database !== '') {
487 if (! $request && $param === 'table' && Current
::$table !== '') {
492 __('Missing parameter:') . ' '
494 . MySQLDocumentation
::showDocumentation('faq', 'faqmissingparameters', true)
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));