PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Whoops/Handler/PrettyPageHandler.php

https://gitlab.com/lighty/whoops
PHP | 559 lines | 270 code | 67 blank | 222 comment | 38 complexity | 106467f9cc13028fdfada5eb78953a84 MD5 | raw file
  1. <?php
  2. /**
  3. * Whoops - php errors for cool kids
  4. * @author Filipe Dobreira <http://github.com/filp>
  5. */
  6. namespace Whoops\Handler;
  7. use InvalidArgumentException;
  8. use RuntimeException;
  9. use UnexpectedValueException;
  10. use Whoops\Exception\Formatter;
  11. use Whoops\Util\Misc;
  12. use Whoops\Util\TemplateHelper;
  13. class PrettyPageHandler extends Handler
  14. {
  15. /**
  16. * Search paths to be scanned for resources, in the reverse
  17. * order they're declared.
  18. *
  19. * @var array
  20. */
  21. private $searchPaths = array();
  22. /**
  23. * Fast lookup cache for known resource locations.
  24. *
  25. * @var array
  26. */
  27. private $resourceCache = array();
  28. /**
  29. * The name of the custom css file.
  30. *
  31. * @var string
  32. */
  33. private $customCss = null;
  34. /**
  35. * @var array[]
  36. */
  37. private $extraTables = array();
  38. /**
  39. * @var bool
  40. */
  41. private $handleUnconditionally = false;
  42. /**
  43. * @var string
  44. */
  45. private $pageTitle = "Whoops! There was an error.";
  46. /**
  47. * A string identifier for a known IDE/text editor, or a closure
  48. * that resolves a string that can be used to open a given file
  49. * in an editor. If the string contains the special substrings
  50. * %file or %line, they will be replaced with the correct data.
  51. *
  52. * @example
  53. * "txmt://open?url=%file&line=%line"
  54. * @var mixed $editor
  55. */
  56. protected $editor;
  57. /**
  58. * A list of known editor strings
  59. * @var array
  60. */
  61. protected $editors = array(
  62. "sublime" => "subl://open?url=file://%file&line=%line",
  63. "textmate" => "txmt://open?url=file://%file&line=%line",
  64. "emacs" => "emacs://open?url=file://%file&line=%line",
  65. "macvim" => "mvim://open/?url=file://%file&line=%line",
  66. "phpstorm" => "phpstorm://open?file=%file&line=%line",
  67. );
  68. /**
  69. * Constructor.
  70. */
  71. public function __construct()
  72. {
  73. if (ini_get('xdebug.file_link_format') || extension_loaded('xdebug')) {
  74. // Register editor using xdebug's file_link_format option.
  75. $this->editors['xdebug'] = function ($file, $line) {
  76. return str_replace(array('%f', '%l'), array($file, $line), ini_get('xdebug.file_link_format'));
  77. };
  78. }
  79. // Add the default, local resource search path:
  80. $this->searchPaths[] = __DIR__ . "/../Resources";
  81. }
  82. /**
  83. * @return int|null
  84. */
  85. public function handle()
  86. {
  87. if (!$this->handleUnconditionally()) {
  88. // Check conditions for outputting HTML:
  89. // @todo: Make this more robust
  90. if (php_sapi_name() === 'cli') {
  91. // Help users who have been relying on an internal test value
  92. // fix their code to the proper method
  93. if (isset($_ENV['whoops-test'])) {
  94. throw new \Exception(
  95. 'Use handleUnconditionally instead of whoops-test'
  96. .' environment variable'
  97. );
  98. }
  99. return Handler::DONE;
  100. }
  101. }
  102. // @todo: Make this more dynamic
  103. $helper = new TemplateHelper();
  104. $templateFile = $this->getResource("views/layout.html.php");
  105. $cssFile = $this->getResource("css/whoops.base.css");
  106. $zeptoFile = $this->getResource("js/zepto.min.js");
  107. $clipboard = $this->getResource("js/clipboard.min.js");
  108. $jsFile = $this->getResource("js/whoops.base.js");
  109. if ($this->customCss) {
  110. $customCssFile = $this->getResource($this->customCss);
  111. }
  112. $inspector = $this->getInspector();
  113. $frames = $inspector->getFrames();
  114. $code = $inspector->getException()->getCode();
  115. if ($inspector->getException() instanceof \ErrorException) {
  116. // ErrorExceptions wrap the php-error types within the "severity" property
  117. $code = Misc::translateErrorCode($inspector->getException()->getSeverity());
  118. }
  119. // List of variables that will be passed to the layout template.
  120. $vars = array(
  121. "page_title" => $this->getPageTitle(),
  122. // @todo: Asset compiler
  123. "stylesheet" => file_get_contents($cssFile),
  124. "zepto" => file_get_contents($zeptoFile),
  125. "clipboard" => file_get_contents($clipboard),
  126. "javascript" => file_get_contents($jsFile),
  127. // Template paths:
  128. "header" => $this->getResource("views/header.html.php"),
  129. "frame_list" => $this->getResource("views/frame_list.html.php"),
  130. "frame_code" => $this->getResource("views/frame_code.html.php"),
  131. "env_details" => $this->getResource("views/env_details.html.php"),
  132. "title" => $this->getPageTitle(),
  133. "name" => explode("\\", $inspector->getExceptionName()),
  134. "message" => $inspector->getException()->getMessage(),
  135. "code" => $code,
  136. "plain_exception" => Formatter::formatExceptionPlain($inspector),
  137. "frames" => $frames,
  138. "has_frames" => !!count($frames),
  139. "handler" => $this,
  140. "handlers" => $this->getRun()->getHandlers(),
  141. "tables" => array(
  142. "GET Data" => $_GET,
  143. "POST Data" => $_POST,
  144. "Files" => $_FILES,
  145. "Cookies" => $_COOKIE,
  146. "Session" => isset($_SESSION) ? $_SESSION : array(),
  147. "Server/Request Data" => $_SERVER,
  148. "Environment Variables" => $_ENV,
  149. ),
  150. );
  151. if (isset($customCssFile)) {
  152. $vars["stylesheet"] .= file_get_contents($customCssFile);
  153. }
  154. // Add extra entries list of data tables:
  155. // @todo: Consolidate addDataTable and addDataTableCallback
  156. $extraTables = array_map(function ($table) {
  157. return $table instanceof \Closure ? $table() : $table;
  158. }, $this->getDataTables());
  159. $vars["tables"] = array_merge($extraTables, $vars["tables"]);
  160. if (\Whoops\Util\Misc::canSendHeaders()) {
  161. header('Content-Type: text/html');
  162. }
  163. $helper->setVariables($vars);
  164. $helper->render($templateFile);
  165. return Handler::QUIT;
  166. }
  167. /**
  168. * Adds an entry to the list of tables displayed in the template.
  169. * The expected data is a simple associative array. Any nested arrays
  170. * will be flattened with print_r
  171. * @param string $label
  172. * @param array $data
  173. */
  174. public function addDataTable($label, array $data)
  175. {
  176. $this->extraTables[$label] = $data;
  177. }
  178. /**
  179. * Lazily adds an entry to the list of tables displayed in the table.
  180. * The supplied callback argument will be called when the error is rendered,
  181. * it should produce a simple associative array. Any nested arrays will
  182. * be flattened with print_r.
  183. *
  184. * @throws InvalidArgumentException If $callback is not callable
  185. * @param string $label
  186. * @param callable $callback Callable returning an associative array
  187. */
  188. public function addDataTableCallback($label, /* callable */ $callback)
  189. {
  190. if (!is_callable($callback)) {
  191. throw new InvalidArgumentException('Expecting callback argument to be callable');
  192. }
  193. $this->extraTables[$label] = function () use ($callback) {
  194. try {
  195. $result = call_user_func($callback);
  196. // Only return the result if it can be iterated over by foreach().
  197. return is_array($result) || $result instanceof \Traversable ? $result : array();
  198. } catch (\Exception $e) {
  199. // Don't allow failure to break the rendering of the original exception.
  200. return array();
  201. }
  202. };
  203. }
  204. /**
  205. * Returns all the extra data tables registered with this handler.
  206. * Optionally accepts a 'label' parameter, to only return the data
  207. * table under that label.
  208. * @param string|null $label
  209. * @return array[]|callable
  210. */
  211. public function getDataTables($label = null)
  212. {
  213. if ($label !== null) {
  214. return isset($this->extraTables[$label]) ?
  215. $this->extraTables[$label] : array();
  216. }
  217. return $this->extraTables;
  218. }
  219. /**
  220. * Allows to disable all attempts to dynamically decide whether to
  221. * handle or return prematurely.
  222. * Set this to ensure that the handler will perform no matter what.
  223. * @param bool|null $value
  224. * @return bool|null
  225. */
  226. public function handleUnconditionally($value = null)
  227. {
  228. if (func_num_args() == 0) {
  229. return $this->handleUnconditionally;
  230. }
  231. $this->handleUnconditionally = (bool) $value;
  232. }
  233. /**
  234. * Adds an editor resolver, identified by a string
  235. * name, and that may be a string path, or a callable
  236. * resolver. If the callable returns a string, it will
  237. * be set as the file reference's href attribute.
  238. *
  239. * @example
  240. * $run->addEditor('macvim', "mvim://open?url=file://%file&line=%line")
  241. * @example
  242. * $run->addEditor('remove-it', function($file, $line) {
  243. * unlink($file);
  244. * return "http://stackoverflow.com";
  245. * });
  246. * @param string $identifier
  247. * @param string $resolver
  248. */
  249. public function addEditor($identifier, $resolver)
  250. {
  251. $this->editors[$identifier] = $resolver;
  252. }
  253. /**
  254. * Set the editor to use to open referenced files, by a string
  255. * identifier, or a callable that will be executed for every
  256. * file reference, with a $file and $line argument, and should
  257. * return a string.
  258. *
  259. * @example
  260. * $run->setEditor(function($file, $line) { return "file:///{$file}"; });
  261. * @example
  262. * $run->setEditor('sublime');
  263. *
  264. * @throws InvalidArgumentException If invalid argument identifier provided
  265. * @param string|callable $editor
  266. */
  267. public function setEditor($editor)
  268. {
  269. if (!is_callable($editor) && !isset($this->editors[$editor])) {
  270. throw new InvalidArgumentException(
  271. "Unknown editor identifier: $editor. Known editors:" .
  272. implode(",", array_keys($this->editors))
  273. );
  274. }
  275. $this->editor = $editor;
  276. }
  277. /**
  278. * Given a string file path, and an integer file line,
  279. * executes the editor resolver and returns, if available,
  280. * a string that may be used as the href property for that
  281. * file reference.
  282. *
  283. * @throws InvalidArgumentException If editor resolver does not return a string
  284. * @param string $filePath
  285. * @param int $line
  286. * @return string|bool
  287. */
  288. public function getEditorHref($filePath, $line)
  289. {
  290. $editor = $this->getEditor($filePath, $line);
  291. if (!$editor) {
  292. return false;
  293. }
  294. // Check that the editor is a string, and replace the
  295. // %line and %file placeholders:
  296. if (!isset($editor['url']) || !is_string($editor['url'])) {
  297. throw new UnexpectedValueException(
  298. __METHOD__ . " should always resolve to a string or a valid editor array; got something else instead."
  299. );
  300. }
  301. $editor['url'] = str_replace("%line", rawurlencode($line), $editor['url']);
  302. $editor['url'] = str_replace("%file", rawurlencode($filePath), $editor['url']);
  303. return $editor['url'];
  304. }
  305. /**
  306. * Given a boolean if the editor link should
  307. * act as an Ajax request. The editor must be a
  308. * valid callable function/closure
  309. *
  310. * @throws UnexpectedValueException If editor resolver does not return a boolean
  311. * @param string $filePath
  312. * @param int $line
  313. * @return bool
  314. */
  315. public function getEditorAjax($filePath, $line)
  316. {
  317. $editor = $this->getEditor($filePath, $line);
  318. // Check that the ajax is a bool
  319. if (!isset($editor['ajax']) || !is_bool($editor['ajax'])) {
  320. throw new UnexpectedValueException(
  321. __METHOD__ . " should always resolve to a bool; got something else instead."
  322. );
  323. }
  324. return $editor['ajax'];
  325. }
  326. /**
  327. * Given a boolean if the editor link should
  328. * act as an Ajax request. The editor must be a
  329. * valid callable function/closure
  330. *
  331. * @throws UnexpectedValueException If editor resolver does not return a boolean
  332. * @param string $filePath
  333. * @param int $line
  334. * @return mixed
  335. */
  336. protected function getEditor($filePath, $line)
  337. {
  338. if ($this->editor === null && !is_string($this->editor) && !is_callable($this->editor))
  339. {
  340. return false;
  341. }
  342. else if(is_string($this->editor) && isset($this->editors[$this->editor]) && !is_callable($this->editors[$this->editor]))
  343. {
  344. return array(
  345. 'ajax' => false,
  346. 'url' => $this->editors[$this->editor],
  347. );
  348. }
  349. else if(is_callable($this->editor) || (isset($this->editors[$this->editor]) && is_callable($this->editors[$this->editor])))
  350. {
  351. if(is_callable($this->editor))
  352. {
  353. $callback = call_user_func($this->editor, $filePath, $line);
  354. }
  355. else
  356. {
  357. $callback = call_user_func($this->editors[$this->editor], $filePath, $line);
  358. }
  359. return array(
  360. 'ajax' => isset($callback['ajax']) ? $callback['ajax'] : false,
  361. 'url' => (is_array($callback) ? $callback['url'] : $callback),
  362. );
  363. }
  364. return false;
  365. }
  366. /**
  367. * @param string $title
  368. * @return void
  369. */
  370. public function setPageTitle($title)
  371. {
  372. $this->pageTitle = (string) $title;
  373. }
  374. /**
  375. * @return string
  376. */
  377. public function getPageTitle()
  378. {
  379. return $this->pageTitle;
  380. }
  381. /**
  382. * Adds a path to the list of paths to be searched for
  383. * resources.
  384. *
  385. * @throws InvalidArgumnetException If $path is not a valid directory
  386. *
  387. * @param string $path
  388. * @return void
  389. */
  390. public function addResourcePath($path)
  391. {
  392. if (!is_dir($path)) {
  393. throw new InvalidArgumentException(
  394. "'$path' is not a valid directory"
  395. );
  396. }
  397. array_unshift($this->searchPaths, $path);
  398. }
  399. /**
  400. * Adds a custom css file to be loaded.
  401. *
  402. * @param string $name
  403. * @return void
  404. */
  405. public function addCustomCss($name)
  406. {
  407. $this->customCss = $name;
  408. }
  409. /**
  410. * @return array
  411. */
  412. public function getResourcePaths()
  413. {
  414. return $this->searchPaths;
  415. }
  416. /**
  417. * Finds a resource, by its relative path, in all available search paths.
  418. * The search is performed starting at the last search path, and all the
  419. * way back to the first, enabling a cascading-type system of overrides
  420. * for all resources.
  421. *
  422. * @throws RuntimeException If resource cannot be found in any of the available paths
  423. *
  424. * @param string $resource
  425. * @return string
  426. */
  427. protected function getResource($resource)
  428. {
  429. // If the resource was found before, we can speed things up
  430. // by caching its absolute, resolved path:
  431. if (isset($this->resourceCache[$resource])) {
  432. return $this->resourceCache[$resource];
  433. }
  434. // Search through available search paths, until we find the
  435. // resource we're after:
  436. foreach ($this->searchPaths as $path) {
  437. $fullPath = $path . "/$resource";
  438. if (is_file($fullPath)) {
  439. // Cache the result:
  440. $this->resourceCache[$resource] = $fullPath;
  441. return $fullPath;
  442. }
  443. }
  444. // If we got this far, nothing was found.
  445. throw new RuntimeException(
  446. "Could not find resource '$resource' in any resource paths."
  447. . "(searched: " . join(", ", $this->searchPaths). ")"
  448. );
  449. }
  450. /**
  451. * @deprecated
  452. *
  453. * @return string
  454. */
  455. public function getResourcesPath()
  456. {
  457. $allPaths = $this->getResourcePaths();
  458. // Compat: return only the first path added
  459. return end($allPaths) ?: null;
  460. }
  461. /**
  462. * @deprecated
  463. *
  464. * @param string $resourcesPath
  465. * @return void
  466. */
  467. public function setResourcesPath($resourcesPath)
  468. {
  469. $this->addResourcePath($resourcesPath);
  470. }
  471. /**
  472. * separate message for research
  473. */
  474. public static function serializeMessage($message)
  475. {
  476. return str_replace(" ", "+", $message);
  477. }
  478. /**
  479. * google search
  480. */
  481. public static function searchGoogle($message)
  482. {
  483. return "https://www.google.com/#newwindow=1&q=".self::serializeMessage($message);
  484. }
  485. /**
  486. * stackoverflow search
  487. */
  488. public static function searchStackoverflow($message)
  489. {
  490. return "http://stackoverflow.com/search?q=".self::serializeMessage($message);
  491. }
  492. }