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