Error.php 14 KB

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