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