PageRenderTime 54ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Whoops/Run.php

https://gitlab.com/lighty/whoops
PHP | 397 lines | 200 code | 53 blank | 144 comment | 32 complexity | 016d4f42010999d005043f26d299d53b 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;
  7. use InvalidArgumentException;
  8. use Whoops\Exception\ErrorException;
  9. use Whoops\Exception\Inspector;
  10. use Whoops\Handler\CallbackHandler;
  11. use Whoops\Handler\Handler;
  12. use Whoops\Handler\HandlerInterface;
  13. use Whoops\Util\Misc;
  14. final class Run
  15. {
  16. const EXCEPTION_HANDLER = "handleException";
  17. const ERROR_HANDLER = "handleError";
  18. const SHUTDOWN_HANDLER = "handleShutdown";
  19. protected $isRegistered;
  20. protected $allowQuit = true;
  21. protected $sendOutput = true;
  22. /**
  23. * @var integer|false
  24. */
  25. protected $sendHttpCode = 500;
  26. /**
  27. * @var HandlerInterface[]
  28. */
  29. protected $handlerStack = array();
  30. protected $silencedPatterns = array();
  31. /**
  32. * Pushes a handler to the end of the stack
  33. *
  34. * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface
  35. * @param Callable|HandlerInterface $handler
  36. * @return Run
  37. */
  38. public function pushHandler($handler)
  39. {
  40. if (is_callable($handler)) {
  41. $handler = new CallbackHandler($handler);
  42. }
  43. if (!$handler instanceof HandlerInterface) {
  44. throw new InvalidArgumentException(
  45. "Argument to " . __METHOD__ . " must be a callable, or instance of "
  46. . "Whoops\\Handler\\HandlerInterface"
  47. );
  48. }
  49. $this->handlerStack[] = $handler;
  50. return $this;
  51. }
  52. /**
  53. * Removes the last handler in the stack and returns it.
  54. * Returns null if there"s nothing else to pop.
  55. * @return null|HandlerInterface
  56. */
  57. public function popHandler()
  58. {
  59. return array_pop($this->handlerStack);
  60. }
  61. /**
  62. * Returns an array with all handlers, in the
  63. * order they were added to the stack.
  64. * @return array
  65. */
  66. public function getHandlers()
  67. {
  68. return $this->handlerStack;
  69. }
  70. /**
  71. * Clears all handlers in the handlerStack, including
  72. * the default PrettyPage handler.
  73. * @return Run
  74. */
  75. public function clearHandlers()
  76. {
  77. $this->handlerStack = array();
  78. return $this;
  79. }
  80. /**
  81. * @param \Throwable $exception
  82. * @return Inspector
  83. */
  84. protected function getInspector($exception)
  85. {
  86. return new Inspector($exception);
  87. }
  88. /**
  89. * Registers this instance as an error handler.
  90. * @return Run
  91. */
  92. public function register()
  93. {
  94. if (!$this->isRegistered) {
  95. // Workaround PHP bug 42098
  96. // https://bugs.php.net/bug.php?id=42098
  97. class_exists("\\Whoops\\Exception\\ErrorException");
  98. class_exists("\\Whoops\\Exception\\FrameCollection");
  99. class_exists("\\Whoops\\Exception\\Frame");
  100. class_exists("\\Whoops\\Exception\\Inspector");
  101. set_error_handler(array($this, self::ERROR_HANDLER));
  102. set_exception_handler(array($this, self::EXCEPTION_HANDLER));
  103. register_shutdown_function(array($this, self::SHUTDOWN_HANDLER));
  104. $this->isRegistered = true;
  105. }
  106. return $this;
  107. }
  108. /**
  109. * Unregisters all handlers registered by this Whoops\Run instance
  110. * @return Run
  111. */
  112. public function unregister()
  113. {
  114. if ($this->isRegistered) {
  115. restore_exception_handler();
  116. restore_error_handler();
  117. $this->isRegistered = false;
  118. }
  119. return $this;
  120. }
  121. /**
  122. * Should Whoops allow Handlers to force the script to quit?
  123. * @param bool|int $exit
  124. * @return bool
  125. */
  126. public function allowQuit($exit = null)
  127. {
  128. if (func_num_args() == 0) {
  129. return $this->allowQuit;
  130. }
  131. return $this->allowQuit = (bool) $exit;
  132. }
  133. /**
  134. * Silence particular errors in particular files
  135. * @param array|string $patterns List or a single regex pattern to match
  136. * @param int $levels Defaults to E_STRICT | E_DEPRECATED
  137. * @return \Whoops\Run
  138. */
  139. public function silenceErrorsInPaths($patterns, $levels = 10240)
  140. {
  141. $this->silencedPatterns = array_merge(
  142. $this->silencedPatterns,
  143. array_map(
  144. function ($pattern) use ($levels) {
  145. return array(
  146. "pattern" => $pattern,
  147. "levels" => $levels,
  148. );
  149. },
  150. (array) $patterns
  151. )
  152. );
  153. return $this;
  154. }
  155. /*
  156. * Should Whoops send HTTP error code to the browser if possible?
  157. * Whoops will by default send HTTP code 500, but you may wish to
  158. * use 502, 503, or another 5xx family code.
  159. *
  160. * @param bool|int $code
  161. * @return int|false
  162. */
  163. public function sendHttpCode($code = null)
  164. {
  165. if (func_num_args() == 0) {
  166. return $this->sendHttpCode;
  167. }
  168. if (!$code) {
  169. return $this->sendHttpCode = false;
  170. }
  171. if ($code === true) {
  172. $code = 500;
  173. }
  174. if ($code < 400 || 600 <= $code) {
  175. throw new InvalidArgumentException(
  176. "Invalid status code '$code', must be 4xx or 5xx"
  177. );
  178. }
  179. return $this->sendHttpCode = $code;
  180. }
  181. /**
  182. * Should Whoops push output directly to the client?
  183. * If this is false, output will be returned by handleException
  184. * @param bool|int $send
  185. * @return bool
  186. */
  187. public function writeToOutput($send = null)
  188. {
  189. if (func_num_args() == 0) {
  190. return $this->sendOutput;
  191. }
  192. return $this->sendOutput = (bool) $send;
  193. }
  194. /**
  195. * Handles an exception, ultimately generating a Whoops error
  196. * page.
  197. *
  198. * @param \Throwable $exception
  199. * @return string Output generated by handlers
  200. */
  201. public function handleException($exception)
  202. {
  203. // Walk the registered handlers in the reverse order
  204. // they were registered, and pass off the exception
  205. $inspector = $this->getInspector($exception);
  206. // Capture output produced while handling the exception,
  207. // we might want to send it straight away to the client,
  208. // or return it silently.
  209. ob_start();
  210. // Just in case there are no handlers:
  211. $handlerResponse = null;
  212. foreach (array_reverse($this->handlerStack) as $handler) {
  213. $handler->setRun($this);
  214. $handler->setInspector($inspector);
  215. $handler->setException($exception);
  216. // The HandlerInterface does not require an Exception passed to handle()
  217. // and neither of our bundled handlers use it.
  218. // However, 3rd party handlers may have already relied on this parameter,
  219. // and removing it would be possibly breaking for users.
  220. $handlerResponse = $handler->handle($exception);
  221. if (in_array($handlerResponse, array(Handler::LAST_HANDLER, Handler::QUIT))) {
  222. // The Handler has handled the exception in some way, and
  223. // wishes to quit execution (Handler::QUIT), or skip any
  224. // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
  225. // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
  226. break;
  227. }
  228. }
  229. $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
  230. $output = ob_get_clean();
  231. // If we're allowed to, send output generated by handlers directly
  232. // to the output, otherwise, and if the script doesn't quit, return
  233. // it so that it may be used by the caller
  234. if ($this->writeToOutput()) {
  235. // @todo Might be able to clean this up a bit better
  236. // If we're going to quit execution, cleanup all other output
  237. // buffers before sending our own output:
  238. if ($willQuit) {
  239. while (ob_get_level() > 0) {
  240. ob_end_clean();
  241. }
  242. }
  243. $this->writeToOutputNow($output);
  244. }
  245. if ($willQuit) {
  246. flush(); // HHVM fix for https://github.com/facebook/hhvm/issues/4055
  247. exit(1);
  248. }
  249. return $output;
  250. }
  251. /**
  252. * Converts generic PHP errors to \ErrorException
  253. * instances, before passing them off to be handled.
  254. *
  255. * This method MUST be compatible with set_error_handler.
  256. *
  257. * @param int $level
  258. * @param string $message
  259. * @param string $file
  260. * @param int $line
  261. *
  262. * @return bool
  263. * @throws ErrorException
  264. */
  265. public function handleError($level, $message, $file = null, $line = null)
  266. {
  267. if ($level & error_reporting()) {
  268. foreach ($this->silencedPatterns as $entry) {
  269. $pathMatches = (bool) preg_match($entry["pattern"], $file);
  270. $levelMatches = $level & $entry["levels"];
  271. if ($pathMatches && $levelMatches) {
  272. // Ignore the error, abort handling
  273. return true;
  274. }
  275. }
  276. // XXX we pass $level for the "code" param only for BC reasons.
  277. // see https://github.com/filp/whoops/issues/267
  278. $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
  279. if ($this->canThrowExceptions) {
  280. throw $exception;
  281. } else {
  282. $this->handleException($exception);
  283. }
  284. // Do not propagate errors which were already handled by Whoops.
  285. return true;
  286. }
  287. // Propagate error to the next handler, allows error_get_last() to
  288. // work on silenced errors.
  289. return false;
  290. }
  291. /**
  292. * Special case to deal with Fatal errors and the like.
  293. */
  294. public function handleShutdown()
  295. {
  296. // If we reached this step, we are in shutdown handler.
  297. // An exception thrown in a shutdown handler will not be propagated
  298. // to the exception handler. Pass that information along.
  299. $this->canThrowExceptions = false;
  300. $error = error_get_last();
  301. if ($error && Misc::isLevelFatal($error['type'])) {
  302. // If there was a fatal error,
  303. // it was not handled in handleError yet.
  304. $this->handleError(
  305. $error['type'],
  306. $error['message'],
  307. $error['file'],
  308. $error['line']
  309. );
  310. }
  311. }
  312. /**
  313. * In certain scenarios, like in shutdown handler, we can not throw exceptions
  314. * @var bool
  315. */
  316. private $canThrowExceptions = true;
  317. /**
  318. * Echo something to the browser
  319. * @param string $output
  320. * @return $this
  321. */
  322. private function writeToOutputNow($output)
  323. {
  324. if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
  325. $httpCode = $this->sendHttpCode();
  326. if (function_exists('http_response_code')) {
  327. http_response_code($httpCode);
  328. } else {
  329. // http_response_code is added in 5.4.
  330. // For compatibility with 5.3 we use the third argument in header call
  331. // First argument must be a real header.
  332. // If it is empty, PHP will ignore the third argument.
  333. // If it is invalid, such as a single space, Apache will handle it well,
  334. // but the PHP development server will hang.
  335. // Setting a full status line would require us to hardcode
  336. // string values for all different status code, and detect the protocol.
  337. // which is an extra error-prone complexity.
  338. header('X-Ignore-This: 1', true, $httpCode);
  339. }
  340. }
  341. echo $output;
  342. return $this;
  343. }
  344. }