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