3 * Manages the rendering of pages in PMA
6 declare(strict_types
=1);
10 use Fig\Http\Message\StatusCodeInterface
;
11 use PhpMyAdmin\Bookmarks\BookmarkRepository
;
12 use PhpMyAdmin\ConfigStorage\Relation
;
13 use PhpMyAdmin\Exceptions\ExitException
;
14 use PhpMyAdmin\Http\Factory\ResponseFactory
;
15 use PhpMyAdmin\Http\Response
;
17 use function is_array
;
18 use function is_scalar
;
19 use function json_encode
;
20 use function json_last_error_msg
;
21 use function mb_strlen
;
22 use function str_starts_with
;
27 * Singleton class used to manage the rendering of pages in PMA
29 class ResponseRenderer
31 private static ResponseRenderer|
null $instance = null;
36 protected Header
$header;
38 * HTML data to be used in the response
40 private string $HTML = '';
42 * An array of JSON key-value pairs
43 * to be sent back for ajax requests
47 private array $JSON = [];
49 * PhpMyAdmin\Footer instance
51 protected Footer
$footer;
53 * Whether we are servicing an ajax request.
55 protected bool $isAjax = false;
57 * Whether response object is disabled
59 protected bool $isDisabled = false;
61 * Whether there were any errors during the processing of the request
62 * Only used for ajax responses
64 protected bool $isSuccess = true;
67 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
69 * @var array<int, string>
71 protected static array $httpStatusMessages = [
74 101 => 'Switching Protocols',
81 203 => 'Non-Authoritative Information',
83 205 => 'Reset Content',
84 206 => 'Partial Content',
85 207 => 'Multi-Status',
86 208 => 'Already Reported',
89 300 => 'Multiple Choices',
90 301 => 'Moved Permanently',
93 304 => 'Not Modified',
95 307 => 'Temporary Redirect',
96 308 => 'Permanent Redirect',
99 401 => 'Unauthorized',
100 402 => 'Payment Required',
103 405 => 'Method Not Allowed',
104 406 => 'Not Acceptable',
105 407 => 'Proxy Authentication Required',
106 408 => 'Request Timeout',
109 411 => 'Length Required',
110 412 => 'Precondition Failed',
111 413 => 'Payload Too Large',
112 414 => 'URI Too Long',
113 415 => 'Unsupported Media Type',
114 416 => 'Range Not Satisfiable',
115 417 => 'Expectation Failed',
116 421 => 'Misdirected Request',
117 422 => 'Unprocessable Entity',
119 424 => 'Failed Dependency',
121 426 => 'Upgrade Required',
123 428 => 'Precondition Required',
124 429 => 'Too Many Requests',
126 431 => 'Request Header Fields Too Large',
127 451 => 'Unavailable For Legal Reasons',
129 500 => 'Internal Server Error',
130 501 => 'Not Implemented',
131 502 => 'Bad Gateway',
132 503 => 'Service Unavailable',
133 504 => 'Gateway Timeout',
134 505 => 'HTTP Version Not Supported',
135 506 => 'Variant Also Negotiates',
136 507 => 'Insufficient Storage',
137 508 => 'Loop Detected',
139 510 => 'Not Extended',
140 511 => 'Network Authentication Required',
143 protected Response
$response;
145 protected Template
$template;
147 private function __construct()
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)),
156 $this->footer
= new Footer($this->template
);
157 $this->response
= ResponseFactory
::create()->createResponse();
159 $this->setAjax(! empty($_REQUEST['ajax_request']));
163 * Set the ajax flag to indicate whether
164 * we are servicing an ajax request
166 * @param bool $isAjax Whether we are servicing an ajax request
168 public function setAjax(bool $isAjax): void
170 $this->isAjax
= $isAjax;
171 $this->header
->setAjax($this->isAjax
);
172 $this->footer
->setAjax($this->isAjax
);
176 * Returns the singleton object
178 public static function getInstance(): ResponseRenderer
180 if (self
::$instance === null) {
181 self
::$instance = new ResponseRenderer();
184 return self
::$instance;
188 * Set the status of an ajax response,
189 * whether it is a success or an error
191 * @param bool $state Whether the request was successfully processed
193 public function setRequestStatus(bool $state): void
195 $this->isSuccess
= $state;
199 * Returns true or false depending on whether
200 * we are servicing an ajax request
202 public function isAjax(): bool
204 return $this->isAjax
;
208 * Disables the rendering of the header
209 * and the footer in responses
211 public function disable(): void
213 $this->header
->disable();
214 $this->footer
->disable();
215 $this->isDisabled
= true;
219 * Returns a PhpMyAdmin\Header object
221 public function getHeader(): Header
223 return $this->header
;
227 * Append HTML code to the current output buffer
229 public function addHTML(string $content): void
231 $this->HTML
.= $content;
235 * Add JSON code to the response
237 * @param string|int|mixed[] $json Either a key (string) or an array or key-value pairs
238 * @param mixed|null $value Null, if passing an array in $json otherwise
239 * it's a string value to the key
241 public function addJSON(string|
int|
array $json, mixed $value = null): void
243 if (is_array($json)) {
244 foreach ($json as $key => $value) {
245 $this->addJSON($key, $value);
247 } elseif ($value instanceof Message
) {
248 $this->JSON
[$json] = $value->getDisplay();
250 $this->JSON
[$json] = $value;
255 * Renders the HTML response text
257 private function getDisplay(): string
259 // The header may contain nothing at all,
260 // if its content was already rendered
261 // and, in this case, the header will be
262 // in the content part of the request
263 return $this->template
->render('base', [
264 'header' => $this->header
->getDisplay(),
265 'content' => $this->HTML
,
266 'footer' => $this->footer
->getDisplay(),
271 * Sends a JSON response to the browser
273 private function ajaxResponse(): string
275 /* Avoid wrapping in case we're disabled */
276 if ($this->isDisabled
) {
277 return $this->getDisplay();
280 if (! isset($this->JSON
['message'])) {
281 $this->JSON
['message'] = $this->getDisplay();
282 } elseif ($this->JSON
['message'] instanceof Message
) {
283 $this->JSON
['message'] = $this->JSON
['message']->getDisplay();
286 if ($this->isSuccess
) {
287 $this->JSON
['success'] = true;
289 $this->JSON
['success'] = false;
290 $this->JSON
['error'] = $this->JSON
['message'];
291 unset($this->JSON
['message']);
294 if ($this->isSuccess
) {
295 if (! isset($this->JSON
['title'])) {
296 $this->addJSON('title', '<title>' . $this->getHeader()->getPageTitle() . '</title>');
299 if (DatabaseInterface
::getInstance()->isConnected()) {
300 $this->addJSON('menu', $this->getHeader()->getMenu()->getDisplay());
303 $this->addJSON('scripts', $this->getHeader()->getScripts()->getFiles());
304 $this->addJSON('selflink', $this->footer
->getSelfUrl());
305 $this->addJSON('displayMessage', $this->getHeader()->getMessage());
307 $debug = $this->footer
->getDebugMessage();
308 if (empty($_REQUEST['no_debug']) && strlen($debug) > 0) {
309 $this->addJSON('debug', $debug);
312 $errors = $this->footer
->getErrorMessages();
313 if (strlen($errors) > 0) {
314 $this->addJSON('errors', $errors);
317 $promptPhpErrors = ErrorHandler
::getInstance()->hasErrorsForPrompt();
318 $this->addJSON('promptPhpErrors', $promptPhpErrors);
320 if (empty($GLOBALS['error_message'])) {
321 // set current db, table and sql query in the querywindow
322 // (this is for the bottom console)
324 $maxChars = Config
::getInstance()->settings
['MaxCharactersInDisplayedSQL'];
325 if (isset($GLOBALS['sql_query']) && mb_strlen($GLOBALS['sql_query']) < $maxChars) {
326 $query = $GLOBALS['sql_query'];
332 'db' => isset($GLOBALS['db']) && is_scalar($GLOBALS['db'])
333 ?
(string) $GLOBALS['db'] : '',
334 'table' => isset($GLOBALS['table']) && is_scalar($GLOBALS['table'])
335 ?
(string) $GLOBALS['table'] : '',
336 'sql_query' => $query,
339 if (! empty($GLOBALS['focus_querywindow'])) {
340 $this->addJSON('_focusQuerywindow', $query);
343 if (! empty($GLOBALS['reload'])) {
344 $this->addJSON('reloadNavigation', 1);
347 $this->addJSON('params', $this->getHeader()->getJsParams());
351 // Set the Content-Type header to JSON so that jQuery parses the
352 // response correctly.
353 foreach (Core
::headerJSON() as $name => $value) {
354 $this->addHeader($name, $value);
357 $result = json_encode($this->JSON
);
358 if ($result === false) {
359 return (string) json_encode([
361 'error' => 'JSON encoding failed: ' . json_last_error_msg(),
368 public function response(): Response
370 $this->response
->getBody()->write($this->isAjax() ?
$this->ajaxResponse() : $this->getDisplay());
372 return $this->response
;
375 public function addHeader(string $name, string $value): void
377 $this->response
= $this->response
->withHeader($name, $value);
380 /** @psalm-param StatusCodeInterface::STATUS_* $code */
381 public function setStatusCode(int $code): void
383 if (isset(static::$httpStatusMessages[$code])) {
384 $this->response
= $this->response
->withStatus($code, static::$httpStatusMessages[$code]);
386 $this->response
= $this->response
->withStatus($code);
391 * Configures response for the login page
393 * @return bool Whether caller should exit
395 public function loginPage(): bool
397 /* Handle AJAX redirection */
398 if ($this->isAjax()) {
399 $this->setRequestStatus(false);
400 // redirect_flag redirects to the login page
401 $this->addJSON('redirect_flag', '1');
406 $this->setMinimalFooter();
407 $header = $this->getHeader();
408 $header->setBodyId('loginform');
409 $header->setTitle('phpMyAdmin');
410 $header->disableMenuAndConsole();
411 $header->disableWarnings();
416 public function setMinimalFooter(): void
418 $this->footer
->setMinimal();
421 public function getSelfUrl(): string
423 return $this->footer
->getSelfUrl();
426 public function getFooterScripts(): Scripts
428 return $this->footer
->getScripts();
431 public function callExit(string $message = ''): never
433 throw new ExitException($message);
437 * @psalm-param non-empty-string $url
438 * @psalm-param StatusCodeInterface::STATUS_* $statusCode
440 public function redirect(string $url, int $statusCode = StatusCodeInterface
::STATUS_FOUND
): void
443 * Avoid relative path redirect problems in case user entered URL
444 * like /phpmyadmin/index.php/ which some web servers happily accept.
446 if (str_starts_with($url, '.')) {
447 $url = Config
::getInstance()->getRootPath() . substr($url, 2);
450 $this->addHeader('Location', $url);
451 $this->setStatusCode($statusCode);