Translated using Weblate (Russian)
[phpmyadmin.git] / src / ResponseRenderer.php
blob305970355b47fadd80660d9ffffeb9a9f60f3b9d
1 <?php
2 /**
3 * Manages the rendering of pages in PMA
4 */
6 declare(strict_types=1);
8 namespace PhpMyAdmin;
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;
23 use function strlen;
24 use function substr;
26 /**
27 * Singleton class used to manage the rendering of pages in PMA
29 class ResponseRenderer
31 private static ResponseRenderer|null $instance = null;
33 /**
34 * Header instance
36 protected Header $header;
37 /**
38 * HTML data to be used in the response
40 private string $HTML = '';
41 /**
42 * An array of JSON key-value pairs
43 * to be sent back for ajax requests
45 * @var mixed[]
47 private array $JSON = [];
48 /**
49 * PhpMyAdmin\Footer instance
51 protected Footer $footer;
52 /**
53 * Whether we are servicing an ajax request.
55 protected bool $isAjax = false;
56 /**
57 * Whether response object is disabled
59 protected bool $isDisabled = false;
60 /**
61 * Whether there were any errors during the processing of the request
62 * Only used for ajax responses
64 protected bool $isSuccess = true;
66 /**
67 * @see http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
69 * @var array<int, string>
71 protected static array $httpStatusMessages = [
72 // Informational
73 100 => 'Continue',
74 101 => 'Switching Protocols',
75 102 => 'Processing',
76 103 => 'Early Hints',
77 // Success
78 200 => 'OK',
79 201 => 'Created',
80 202 => 'Accepted',
81 203 => 'Non-Authoritative Information',
82 204 => 'No Content',
83 205 => 'Reset Content',
84 206 => 'Partial Content',
85 207 => 'Multi-Status',
86 208 => 'Already Reported',
87 226 => 'IM Used',
88 // Redirection
89 300 => 'Multiple Choices',
90 301 => 'Moved Permanently',
91 302 => 'Found',
92 303 => 'See Other',
93 304 => 'Not Modified',
94 305 => 'Use Proxy',
95 307 => 'Temporary Redirect',
96 308 => 'Permanent Redirect',
97 // Client Error
98 400 => 'Bad Request',
99 401 => 'Unauthorized',
100 402 => 'Payment Required',
101 403 => 'Forbidden',
102 404 => 'Not Found',
103 405 => 'Method Not Allowed',
104 406 => 'Not Acceptable',
105 407 => 'Proxy Authentication Required',
106 408 => 'Request Timeout',
107 409 => 'Conflict',
108 410 => 'Gone',
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',
118 423 => 'Locked',
119 424 => 'Failed Dependency',
120 425 => 'Too Early',
121 426 => 'Upgrade Required',
122 427 => 'Unassigned',
123 428 => 'Precondition Required',
124 429 => 'Too Many Requests',
125 430 => 'Unassigned',
126 431 => 'Request Header Fields Too Large',
127 451 => 'Unavailable For Legal Reasons',
128 // Server Error
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',
138 509 => 'Unassigned',
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(
153 $this->template,
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();
249 } else {
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;
288 } else {
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)
323 $query = '';
324 $maxChars = Config::getInstance()->settings['MaxCharactersInDisplayedSQL'];
325 if (isset($GLOBALS['sql_query']) && mb_strlen($GLOBALS['sql_query']) < $maxChars) {
326 $query = $GLOBALS['sql_query'];
329 $this->addJSON(
330 'reloadQuerywindow',
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([
360 'success' => false,
361 'error' => 'JSON encoding failed: ' . json_last_error_msg(),
365 return $result;
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]);
385 } else {
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');
403 return true;
406 $this->setMinimalFooter();
407 $header = $this->getHeader();
408 $header->setBodyId('loginform');
409 $header->setTitle('phpMyAdmin');
410 $header->disableMenuAndConsole();
411 $header->disableWarnings();
413 return false;
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);