ErrorReport.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use PhpMyAdmin\Utils\HttpRequest;
  5. use const E_USER_WARNING;
  6. use const JSON_PRETTY_PRINT;
  7. use const JSON_UNESCAPED_SLASHES;
  8. use const PHP_VERSION;
  9. use function count;
  10. use function http_build_query;
  11. use function is_array;
  12. use function json_encode;
  13. use function mb_strlen;
  14. use function mb_substr;
  15. use function parse_str;
  16. use function parse_url;
  17. use function preg_match;
  18. use function str_replace;
  19. /**
  20. * Error reporting functions used to generate and submit error reports
  21. */
  22. class ErrorReport
  23. {
  24. /**
  25. * The URL where to submit reports to
  26. *
  27. * @var string
  28. */
  29. private $submissionUrl = 'https://reports.phpmyadmin.net/incidents/create';
  30. /** @var HttpRequest */
  31. private $httpRequest;
  32. /** @var Relation */
  33. private $relation;
  34. /** @var Template */
  35. public $template;
  36. /**
  37. * @param HttpRequest $httpRequest HttpRequest instance
  38. * @param Relation $relation Relation instance
  39. * @param Template $template Template instance
  40. */
  41. public function __construct(HttpRequest $httpRequest, Relation $relation, Template $template)
  42. {
  43. $this->httpRequest = $httpRequest;
  44. $this->relation = $relation;
  45. $this->template = $template;
  46. }
  47. /**
  48. * Set the URL where to submit reports to
  49. *
  50. * @param string $submissionUrl Submission URL
  51. */
  52. public function setSubmissionUrl(string $submissionUrl): void
  53. {
  54. $this->submissionUrl = $submissionUrl;
  55. }
  56. /**
  57. * Returns the pretty printed error report data collected from the
  58. * current configuration or from the request parameters sent by the
  59. * error reporting js code.
  60. *
  61. * @return string the report
  62. */
  63. private function getPrettyData(): string
  64. {
  65. $report = $this->getData();
  66. return json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  67. }
  68. /**
  69. * Returns the error report data collected from the current configuration or
  70. * from the request parameters sent by the error reporting js code.
  71. *
  72. * @param string $exceptionType whether exception is 'js' or 'php'
  73. *
  74. * @return array error report if success, Empty Array otherwise
  75. */
  76. public function getData(string $exceptionType = 'js'): array
  77. {
  78. global $PMA_Config;
  79. $relParams = $this->relation->getRelationsParam();
  80. // common params for both, php & js exceptions
  81. $report = [
  82. 'pma_version' => PMA_VERSION,
  83. 'browser_name' => PMA_USR_BROWSER_AGENT,
  84. 'browser_version' => PMA_USR_BROWSER_VER,
  85. 'user_os' => PMA_USR_OS,
  86. 'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? null,
  87. 'user_agent_string' => $_SERVER['HTTP_USER_AGENT'],
  88. 'locale' => $PMA_Config->getCookie('pma_lang'),
  89. 'configuration_storage' =>
  90. $relParams['db'] === null ? 'disabled' : 'enabled',
  91. 'php_version' => PHP_VERSION,
  92. ];
  93. if ($exceptionType === 'js') {
  94. if (empty($_POST['exception'])) {
  95. return [];
  96. }
  97. $exception = $_POST['exception'];
  98. if (isset($exception['stack'])) {
  99. $exception['stack'] = $this->translateStacktrace($exception['stack']);
  100. }
  101. if (isset($exception['url'])) {
  102. [$uri, $scriptName] = $this->sanitizeUrl($exception['url']);
  103. $exception['uri'] = $uri;
  104. $report['script_name'] = $scriptName;
  105. unset($exception['url']);
  106. } elseif (isset($_POST['url'])) {
  107. [$uri, $scriptName] = $this->sanitizeUrl($_POST['url']);
  108. $exception['uri'] = $uri;
  109. $report['script_name'] = $scriptName;
  110. unset($_POST['url']);
  111. } else {
  112. $report['script_name'] = null;
  113. }
  114. $report['exception_type'] = 'js';
  115. $report['exception'] = $exception;
  116. if (isset($_POST['microhistory'])) {
  117. $report['microhistory'] = $_POST['microhistory'];
  118. }
  119. if (! empty($_POST['description'])) {
  120. $report['steps'] = $_POST['description'];
  121. }
  122. } elseif ($exceptionType === 'php') {
  123. $errors = [];
  124. // create php error report
  125. $i = 0;
  126. if (! isset($_SESSION['prev_errors'])
  127. || $_SESSION['prev_errors'] == ''
  128. ) {
  129. return [];
  130. }
  131. foreach ($_SESSION['prev_errors'] as $errorObj) {
  132. /** @var Error $errorObj */
  133. if (! $errorObj->getLine()
  134. || ! $errorObj->getType()
  135. || $errorObj->getNumber() == E_USER_WARNING
  136. ) {
  137. continue;
  138. }
  139. $errors[$i++] = [
  140. 'lineNum' => $errorObj->getLine(),
  141. 'file' => $errorObj->getFile(),
  142. 'type' => $errorObj->getType(),
  143. 'msg' => $errorObj->getOnlyMessage(),
  144. 'stackTrace' => $errorObj->getBacktrace(5),
  145. 'stackhash' => $errorObj->getHash(),
  146. ];
  147. }
  148. // if there were no 'actual' errors to be submitted.
  149. if ($i == 0) {
  150. return []; // then return empty array
  151. }
  152. $report['exception_type'] = 'php';
  153. $report['errors'] = $errors;
  154. } else {
  155. return [];
  156. }
  157. return $report;
  158. }
  159. /**
  160. * Sanitize a url to remove the identifiable host name and extract the
  161. * current script name from the url fragment
  162. *
  163. * It returns two things in an array. The first is the uri without the
  164. * hostname and identifying query params. The second is the name of the
  165. * php script in the url
  166. *
  167. * @param string $url the url to sanitize
  168. *
  169. * @return array the uri and script name
  170. */
  171. private function sanitizeUrl(string $url): array
  172. {
  173. $components = parse_url($url);
  174. if (! is_array($components)) {
  175. $components = [];
  176. }
  177. if (isset($components['fragment'])
  178. && preg_match('<PMAURL-\d+:>', $components['fragment'], $matches)
  179. ) {
  180. $uri = str_replace($matches[0], '', $components['fragment']);
  181. $url = 'https://example.com/' . $uri;
  182. $components = parse_url($url);
  183. if (! is_array($components)) {
  184. $components = [];
  185. }
  186. }
  187. // get script name
  188. preg_match('<([a-zA-Z\-_\d\.]*\.php|js\/[a-zA-Z\-_\d\/\.]*\.js)$>', $components['path'] ?? '', $matches);
  189. if (count($matches) < 2) {
  190. $scriptName = 'index.php';
  191. } else {
  192. $scriptName = $matches[1];
  193. }
  194. // remove deployment specific details to make uri more generic
  195. if (isset($components['query'])) {
  196. parse_str($components['query'], $queryArray);
  197. unset($queryArray['db'], $queryArray['table'], $queryArray['token'], $queryArray['server']);
  198. $query = http_build_query($queryArray);
  199. } else {
  200. $query = '';
  201. }
  202. $uri = $scriptName . '?' . $query;
  203. return [
  204. $uri,
  205. $scriptName,
  206. ];
  207. }
  208. /**
  209. * Sends report data to the error reporting server
  210. *
  211. * @param array $report the report info to be sent
  212. *
  213. * @return string|bool|null the reply of the server
  214. */
  215. public function send(array $report)
  216. {
  217. return $this->httpRequest->create(
  218. $this->submissionUrl,
  219. 'POST',
  220. false,
  221. json_encode($report),
  222. 'Content-Type: application/json'
  223. );
  224. }
  225. /**
  226. * Translates the cumulative line numbers in the stack trace as well as sanitize
  227. * urls and trim long lines in the context
  228. *
  229. * @param array $stack the stack trace
  230. *
  231. * @return array the modified stack trace
  232. */
  233. private function translateStacktrace(array $stack): array
  234. {
  235. foreach ($stack as &$level) {
  236. foreach ($level['context'] as &$line) {
  237. if (mb_strlen($line) <= 80) {
  238. continue;
  239. }
  240. $line = mb_substr($line, 0, 75) . '//...';
  241. }
  242. [$uri, $scriptName] = $this->sanitizeUrl($level['url']);
  243. $level['uri'] = $uri;
  244. $level['scriptname'] = $scriptName;
  245. unset($level['url']);
  246. }
  247. unset($level);
  248. return $stack;
  249. }
  250. /**
  251. * Generates the error report form to collect user description and preview the
  252. * report before being sent
  253. *
  254. * @return string the form
  255. */
  256. public function getForm(): string
  257. {
  258. $datas = [
  259. 'report_data' => $this->getPrettyData(),
  260. 'hidden_inputs' => Url::getHiddenInputs(),
  261. 'hidden_fields' => null,
  262. ];
  263. $reportData = $this->getData();
  264. if (! empty($reportData)) {
  265. $datas['hidden_fields'] = Url::getHiddenFields($reportData, '', true);
  266. }
  267. return $this->template->render('error/report_form', $datas);
  268. }
  269. }