ErrorReport.php 8.9 KB

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