1<?php
2
3declare(strict_types=1);
4
5namespace PhpMyAdmin;
6
7use Throwable;
8use const DIRECTORY_SEPARATOR;
9use const E_COMPILE_ERROR;
10use const E_COMPILE_WARNING;
11use const E_CORE_ERROR;
12use const E_CORE_WARNING;
13use const E_DEPRECATED;
14use const E_ERROR;
15use const E_NOTICE;
16use const E_PARSE;
17use const E_RECOVERABLE_ERROR;
18use const E_STRICT;
19use const E_USER_DEPRECATED;
20use const E_USER_ERROR;
21use const E_USER_NOTICE;
22use const E_USER_WARNING;
23use const E_WARNING;
24use const PATH_SEPARATOR;
25use function array_pop;
26use function array_slice;
27use function basename;
28use function count;
29use function debug_backtrace;
30use function explode;
31use function function_exists;
32use function get_class;
33use function gettype;
34use function htmlspecialchars;
35use function implode;
36use function in_array;
37use function is_object;
38use function is_scalar;
39use function is_string;
40use function mb_substr;
41use function md5;
42use function realpath;
43use function serialize;
44use function str_replace;
45use function var_export;
46
47/**
48 * a single error
49 */
50class Error extends Message
51{
52    /**
53     * Error types
54     *
55     * @var array
56     */
57    public static $errortype =  [
58        0                    => 'Internal error',
59        E_ERROR              => 'Error',
60        E_WARNING            => 'Warning',
61        E_PARSE              => 'Parsing Error',
62        E_NOTICE             => 'Notice',
63        E_CORE_ERROR         => 'Core Error',
64        E_CORE_WARNING       => 'Core Warning',
65        E_COMPILE_ERROR      => 'Compile Error',
66        E_COMPILE_WARNING    => 'Compile Warning',
67        E_USER_ERROR         => 'User Error',
68        E_USER_WARNING       => 'User Warning',
69        E_USER_NOTICE        => 'User Notice',
70        E_STRICT             => 'Runtime Notice',
71        E_DEPRECATED         => 'Deprecation Notice',
72        E_USER_DEPRECATED    => 'Deprecation Notice',
73        E_RECOVERABLE_ERROR  => 'Catchable Fatal Error',
74    ];
75
76    /**
77     * Error levels
78     *
79     * @var array
80     */
81    public static $errorlevel =  [
82        0                    => 'error',
83        E_ERROR              => 'error',
84        E_WARNING            => 'error',
85        E_PARSE              => 'error',
86        E_NOTICE             => 'notice',
87        E_CORE_ERROR         => 'error',
88        E_CORE_WARNING       => 'error',
89        E_COMPILE_ERROR      => 'error',
90        E_COMPILE_WARNING    => 'error',
91        E_USER_ERROR         => 'error',
92        E_USER_WARNING       => 'error',
93        E_USER_NOTICE        => 'notice',
94        E_STRICT             => 'notice',
95        E_DEPRECATED         => 'notice',
96        E_USER_DEPRECATED    => 'notice',
97        E_RECOVERABLE_ERROR  => 'error',
98    ];
99
100    /**
101     * The file in which the error occurred
102     *
103     * @var string
104     */
105    protected $file = '';
106
107    /**
108     * The line in which the error occurred
109     *
110     * @var int
111     */
112    protected $line = 0;
113
114    /**
115     * Holds the backtrace for this error
116     *
117     * @var array
118     */
119    protected $backtrace = [];
120
121    /**
122     * Hide location of errors
123     *
124     * @var bool
125     */
126    protected $hideLocation = false;
127
128    /**
129     * @param int    $errno   error number
130     * @param string $errstr  error message
131     * @param string $errfile file
132     * @param int    $errline line
133     */
134    public function __construct(int $errno, string $errstr, string $errfile, int $errline)
135    {
136        parent::__construct();
137        $this->setNumber($errno);
138        $this->setMessage($errstr, false);
139        $this->setFile($errfile);
140        $this->setLine($errline);
141
142        // This function can be disabled in php.ini
143        if (function_exists('debug_backtrace')) {
144            $backtrace = @debug_backtrace();
145            // remove last three calls:
146            // debug_backtrace(), handleError() and addError()
147            $backtrace = array_slice($backtrace, 3);
148        } else {
149            $backtrace = [];
150        }
151
152        $this->setBacktrace($backtrace);
153    }
154
155    /**
156     * Process backtrace to avoid path disclosures, objects and so on
157     *
158     * @param array $backtrace backtrace
159     *
160     * @return array
161     */
162    public static function processBacktrace(array $backtrace): array
163    {
164        $result = [];
165
166        $members = [
167            'line',
168            'function',
169            'class',
170            'type',
171        ];
172
173        foreach ($backtrace as $idx => $step) {
174            /* Create new backtrace entry */
175            $result[$idx] = [];
176
177            /* Make path relative */
178            if (isset($step['file'])) {
179                $result[$idx]['file'] = self::relPath($step['file']);
180            }
181
182            /* Store members we want */
183            foreach ($members as $name) {
184                if (! isset($step[$name])) {
185                    continue;
186                }
187
188                $result[$idx][$name] = $step[$name];
189            }
190
191            /* Store simplified args */
192            if (! isset($step['args'])) {
193                continue;
194            }
195
196            foreach ($step['args'] as $key => $arg) {
197                $result[$idx]['args'][$key] = self::getArg($arg, $step['function']);
198            }
199        }
200
201        return $result;
202    }
203
204    /**
205     * Toggles location hiding
206     *
207     * @param bool $hide Whether to hide
208     */
209    public function setHideLocation(bool $hide): void
210    {
211        $this->hideLocation = $hide;
212    }
213
214    /**
215     * sets PhpMyAdmin\Error::$_backtrace
216     *
217     * We don't store full arguments to avoid wakeup or memory problems.
218     *
219     * @param array $backtrace backtrace
220     */
221    public function setBacktrace(array $backtrace): void
222    {
223        $this->backtrace = self::processBacktrace($backtrace);
224    }
225
226    /**
227     * sets PhpMyAdmin\Error::$_line
228     *
229     * @param int $line the line
230     */
231    public function setLine(int $line): void
232    {
233        $this->line = $line;
234    }
235
236    /**
237     * sets PhpMyAdmin\Error::$_file
238     *
239     * @param string $file the file
240     */
241    public function setFile(string $file): void
242    {
243        $this->file = self::relPath($file);
244    }
245
246    /**
247     * returns unique PhpMyAdmin\Error::$hash, if not exists it will be created
248     *
249     * @return string PhpMyAdmin\Error::$hash
250     */
251    public function getHash(): string
252    {
253        try {
254            $backtrace = serialize($this->getBacktrace());
255        } catch (Throwable $e) {
256            $backtrace = '';
257        }
258        if ($this->hash === null) {
259            $this->hash = md5(
260                $this->getNumber() .
261                $this->getMessage() .
262                $this->getFile() .
263                $this->getLine() .
264                $backtrace
265            );
266        }
267
268        return $this->hash;
269    }
270
271    /**
272     * returns PhpMyAdmin\Error::$_backtrace for first $count frames
273     * pass $count = -1 to get full backtrace.
274     * The same can be done by not passing $count at all.
275     *
276     * @param int $count Number of stack frames.
277     *
278     * @return array PhpMyAdmin\Error::$_backtrace
279     */
280    public function getBacktrace(int $count = -1): array
281    {
282        if ($count != -1) {
283            return array_slice($this->backtrace, 0, $count);
284        }
285
286        return $this->backtrace;
287    }
288
289    /**
290     * returns PhpMyAdmin\Error::$file
291     *
292     * @return string PhpMyAdmin\Error::$file
293     */
294    public function getFile(): string
295    {
296        return $this->file;
297    }
298
299    /**
300     * returns PhpMyAdmin\Error::$line
301     *
302     * @return int PhpMyAdmin\Error::$line
303     */
304    public function getLine(): int
305    {
306        return $this->line;
307    }
308
309    /**
310     * returns type of error
311     *
312     * @return string type of error
313     */
314    public function getType(): string
315    {
316        return self::$errortype[$this->getNumber()];
317    }
318
319    /**
320     * returns level of error
321     *
322     * @return string level of error
323     */
324    public function getLevel(): string
325    {
326        return self::$errorlevel[$this->getNumber()];
327    }
328
329    /**
330     * returns title prepared for HTML Title-Tag
331     *
332     * @return string HTML escaped and truncated title
333     */
334    public function getHtmlTitle(): string
335    {
336        return htmlspecialchars(
337            mb_substr($this->getTitle(), 0, 100)
338        );
339    }
340
341    /**
342     * returns title for error
343     */
344    public function getTitle(): string
345    {
346        return $this->getType() . ': ' . $this->getMessage();
347    }
348
349    /**
350     * Get HTML backtrace
351     */
352    public function getBacktraceDisplay(): string
353    {
354        return self::formatBacktrace(
355            $this->getBacktrace(),
356            "<br>\n",
357            "<br>\n"
358        );
359    }
360
361    /**
362     * return formatted backtrace field
363     *
364     * @param array  $backtrace Backtrace data
365     * @param string $separator Arguments separator to use
366     * @param string $lines     Lines separator to use
367     *
368     * @return string formatted backtrace
369     */
370    public static function formatBacktrace(
371        array $backtrace,
372        string $separator,
373        string $lines
374    ): string {
375        $retval = '';
376
377        foreach ($backtrace as $step) {
378            if (isset($step['file'], $step['line'])) {
379                $retval .= self::relPath($step['file'])
380                    . '#' . $step['line'] . ': ';
381            }
382            if (isset($step['class'])) {
383                $retval .= $step['class'] . $step['type'];
384            }
385            $retval .= self::getFunctionCall($step, $separator);
386            $retval .= $lines;
387        }
388
389        return $retval;
390    }
391
392    /**
393     * Formats function call in a backtrace
394     *
395     * @param array  $step      backtrace step
396     * @param string $separator Arguments separator to use
397     */
398    public static function getFunctionCall(array $step, string $separator): string
399    {
400        $retval = $step['function'] . '(';
401        if (isset($step['args'])) {
402            if (count($step['args']) > 1) {
403                $retval .= $separator;
404                foreach ($step['args'] as $arg) {
405                    $retval .= "\t";
406                    $retval .= $arg;
407                    $retval .= ',' . $separator;
408                }
409            } elseif (count($step['args']) > 0) {
410                foreach ($step['args'] as $arg) {
411                    $retval .= $arg;
412                }
413            }
414        }
415
416        return $retval . ')';
417    }
418
419    /**
420     * Get a single function argument
421     *
422     * if $function is one of include/require
423     * the $arg is converted to a relative path
424     *
425     * @param string $arg      argument to process
426     * @param string $function function name
427     */
428    public static function getArg($arg, string $function): string
429    {
430        $retval = '';
431        $include_functions = [
432            'include',
433            'include_once',
434            'require',
435            'require_once',
436        ];
437        $connect_functions = [
438            'mysql_connect',
439            'mysql_pconnect',
440            'mysqli_connect',
441            'mysqli_real_connect',
442            'connect',
443            '_realConnect',
444        ];
445
446        if (in_array($function, $include_functions)) {
447            $retval .= self::relPath($arg);
448        } elseif (in_array($function, $connect_functions)
449            && is_string($arg)
450        ) {
451            $retval .= gettype($arg) . ' ********';
452        } elseif (is_scalar($arg)) {
453            $retval .= gettype($arg) . ' '
454                . htmlspecialchars(var_export($arg, true));
455        } elseif (is_object($arg)) {
456            $retval .= '<Class:' . get_class($arg) . '>';
457        } else {
458            $retval .= gettype($arg);
459        }
460
461        return $retval;
462    }
463
464    /**
465     * Gets the error as string of HTML
466     */
467    public function getDisplay(): string
468    {
469        $this->isDisplayed(true);
470
471        $context = 'primary';
472        $level = $this->getLevel();
473        if ($level === 'error') {
474            $context = 'danger';
475        }
476
477        $retval = '<div class="alert alert-' . $context . '" role="alert">';
478        if (! $this->isUserError()) {
479            $retval .= '<strong>' . $this->getType() . '</strong>';
480            $retval .= ' in ' . $this->getFile() . '#' . $this->getLine();
481            $retval .= "<br>\n";
482        }
483        $retval .= $this->getMessage();
484        if (! $this->isUserError()) {
485            $retval .= "<br>\n";
486            $retval .= "<br>\n";
487            $retval .= "<strong>Backtrace</strong><br>\n";
488            $retval .= "<br>\n";
489            $retval .= $this->getBacktraceDisplay();
490        }
491        $retval .= '</div>';
492
493        return $retval;
494    }
495
496    /**
497     * whether this error is a user error
498     */
499    public function isUserError(): bool
500    {
501        return $this->hideLocation ||
502            ($this->getNumber() & (E_USER_WARNING | E_USER_ERROR | E_USER_NOTICE | E_USER_DEPRECATED));
503    }
504
505    /**
506     * return short relative path to phpMyAdmin basedir
507     *
508     * prevent path disclosure in error message,
509     * and make users feel safe to submit error reports
510     *
511     * @param string $path path to be shorten
512     *
513     * @return string shortened path
514     */
515    public static function relPath(string $path): string
516    {
517        $dest = @realpath($path);
518
519        /* Probably affected by open_basedir */
520        if ($dest === false) {
521            return basename($path);
522        }
523
524        $Ahere = explode(
525            DIRECTORY_SEPARATOR,
526            (string) realpath(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..')
527        );
528        $Adest = explode(DIRECTORY_SEPARATOR, $dest);
529
530        $result = '.';
531        // && count ($Adest)>0 && count($Ahere)>0 )
532        while (implode(DIRECTORY_SEPARATOR, $Adest) != implode(DIRECTORY_SEPARATOR, $Ahere)) {
533            if (count($Ahere) > count($Adest)) {
534                array_pop($Ahere);
535                $result .= DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . '..';
536            } else {
537                array_pop($Adest);
538            }
539        }
540        $path = $result . str_replace(implode(DIRECTORY_SEPARATOR, $Adest), '', $dest);
541
542        return str_replace(
543            DIRECTORY_SEPARATOR . PATH_SEPARATOR,
544            DIRECTORY_SEPARATOR,
545            $path
546        );
547    }
548}
549