PageRenderTime 50ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fCore.php

https://bitbucket.org/wbond/flourish/
PHP | 1225 lines | 598 code | 156 blank | 471 comment | 119 complexity | a40c380ee1dba4e69551158d791a6608 MD5 | raw file
  1. <?php
  2. /**
  3. * Provides low-level debugging, error and exception functionality
  4. *
  5. * @copyright Copyright (c) 2007-2011 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Will Bond, iMarc LLC [wb-imarc] <will@imarc.net>
  8. * @author Nick Trew [nt]
  9. * @license http://flourishlib.com/license
  10. *
  11. * @package Flourish
  12. * @link http://flourishlib.com/fCore
  13. *
  14. * @version 1.0.0b24
  15. * @changes 1.0.0b24 Backwards Compatibility Break - moved ::detectOpcodeCache() to fLoader::hasOpcodeCache() [wb, 2011-08-26]
  16. * @changes 1.0.0b23 Backwards Compatibility Break - changed the email subject of error/exception emails to include relevant file info, instead of the timestamp, for better email message threading [wb, 2011-06-20]
  17. * @changes 1.0.0b22 Fixed a bug with dumping arrays containing integers [wb, 2011-05-26]
  18. * @changes 1.0.0b21 Changed ::startErrorCapture() to allow "stacking" it via multiple calls, fixed a couple of bugs with ::dump() mangling strings in the form `int(1)`, fixed mispelling of `occurred` [wb, 2011-05-09]
  19. * @changes 1.0.0b20 Backwards Compatibility Break - Updated ::expose() to not wrap the data in HTML when running via CLI, and instead just append a newline [wb, 2011-02-24]
  20. * @changes 1.0.0b19 Added detection of AIX to ::checkOS() [wb, 2011-01-19]
  21. * @changes 1.0.0b18 Updated ::expose() to be able to accept multiple parameters [wb, 2011-01-10]
  22. * @changes 1.0.0b17 Fixed a bug with ::backtrace() triggering notices when an argument is not UTF-8 [wb, 2010-08-17]
  23. * @changes 1.0.0b16 Added the `$types` and `$regex` parameters to ::startErrorCapture() and the `$regex` parameter to ::stopErrorCapture() [wb, 2010-08-09]
  24. * @changes 1.0.0b15 Added ::startErrorCapture() and ::stopErrorCapture() [wb, 2010-07-05]
  25. * @changes 1.0.0b14 Changed ::enableExceptionHandling() to only call fException::printMessage() when the destination is not `html` and no callback has been defined, added ::configureSMTP() to allow using fSMTP for error and exception emails [wb, 2010-06-04]
  26. * @changes 1.0.0b13 Added the `$backtrace` parameter to ::backtrace() [wb, 2010-03-05]
  27. * @changes 1.0.0b12 Added ::getDebug() to check for the global debugging flag, added more specific BSD checks to ::checkOS() [wb, 2010-03-02]
  28. * @changes 1.0.0b11 Added ::detectOpcodeCache() [nt+wb, 2009-10-06]
  29. * @changes 1.0.0b10 Fixed ::expose() to properly display when output includes non-UTF-8 binary data [wb, 2009-06-29]
  30. * @changes 1.0.0b9 Added ::disableContext() to remove context info for exception/error handling, tweaked output for exceptions/errors [wb, 2009-06-28]
  31. * @changes 1.0.0b8 ::enableErrorHandling() and ::enableExceptionHandling() now accept multiple email addresses, and a much wider range of emails [wb-imarc, 2009-06-01]
  32. * @changes 1.0.0b7 ::backtrace() now properly replaces document root with {doc_root} on Windows [wb, 2009-05-02]
  33. * @changes 1.0.0b6 Fixed a bug with getting the server name for error messages when running on the command line [wb, 2009-03-11]
  34. * @changes 1.0.0b5 Fixed a bug with checking the error/exception destination when a log file is specified [wb, 2009-03-07]
  35. * @changes 1.0.0b4 Backwards compatibility break - ::getOS() and ::getPHPVersion() removed, replaced with ::checkOS() and ::checkVersion() [wb, 2009-02-16]
  36. * @changes 1.0.0b3 ::handleError() now displays what kind of error occurred as the heading [wb, 2009-02-15]
  37. * @changes 1.0.0b2 Added ::registerDebugCallback() [wb, 2009-02-07]
  38. * @changes 1.0.0b The initial implementation [wb, 2007-09-25]
  39. */
  40. class fCore
  41. {
  42. // The following constants allow for nice looking callbacks to static methods
  43. const backtrace = 'fCore::backtrace';
  44. const call = 'fCore::call';
  45. const callback = 'fCore::callback';
  46. const checkOS = 'fCore::checkOS';
  47. const checkVersion = 'fCore::checkVersion';
  48. const configureSMTP = 'fCore::configureSMTP';
  49. const debug = 'fCore::debug';
  50. const disableContext = 'fCore::disableContext';
  51. const dump = 'fCore::dump';
  52. const enableDebugging = 'fCore::enableDebugging';
  53. const enableDynamicConstants = 'fCore::enableDynamicConstants';
  54. const enableErrorHandling = 'fCore::enableErrorHandling';
  55. const enableExceptionHandling = 'fCore::enableExceptionHandling';
  56. const expose = 'fCore::expose';
  57. const getDebug = 'fCore::getDebug';
  58. const handleError = 'fCore::handleError';
  59. const handleException = 'fCore::handleException';
  60. const registerDebugCallback = 'fCore::registerDebugCallback';
  61. const reset = 'fCore::reset';
  62. const sendMessagesOnShutdown = 'fCore::sendMessagesOnShutdown';
  63. const startErrorCapture = 'fCore::startErrorCapture';
  64. const stopErrorCapture = 'fCore::stopErrorCapture';
  65. /**
  66. * The nesting level of error capturing
  67. *
  68. * @var integer
  69. */
  70. static private $captured_error_level = 0;
  71. /**
  72. * A stack of regex to match errors to capture, one string per level
  73. *
  74. * @var array
  75. */
  76. static private $captured_error_regex = array();
  77. /**
  78. * A stack of the types of errors to capture, one integer per level
  79. *
  80. * @var array
  81. */
  82. static private $captured_error_types = array();
  83. /**
  84. * A stack of arrays of errors that have been captured, one array per level
  85. *
  86. * @var array
  87. */
  88. static private $captured_errors = array();
  89. /**
  90. * A stack of the previous error handler, one callback per level
  91. *
  92. * @var array
  93. */
  94. static private $captured_errors_previous_handler = array();
  95. /**
  96. * If the context info has been shown
  97. *
  98. * @var boolean
  99. */
  100. static private $context_shown = FALSE;
  101. /**
  102. * If global debugging is enabled
  103. *
  104. * @var boolean
  105. */
  106. static private $debug = NULL;
  107. /**
  108. * A callback to pass debug messages to
  109. *
  110. * @var callback
  111. */
  112. static private $debug_callback = NULL;
  113. /**
  114. * If dynamic constants should be created
  115. *
  116. * @var boolean
  117. */
  118. static private $dynamic_constants = FALSE;
  119. /**
  120. * Error destination
  121. *
  122. * @var string
  123. */
  124. static private $error_destination = 'html';
  125. /**
  126. * An array of errors to be send to the destination upon page completion
  127. *
  128. * @var array
  129. */
  130. static private $error_message_queue = array();
  131. /**
  132. * Exception destination
  133. *
  134. * @var string
  135. */
  136. static private $exception_destination = 'html';
  137. /**
  138. * Exception handler callback
  139. *
  140. * @var mixed
  141. */
  142. static private $exception_handler_callback = NULL;
  143. /**
  144. * Exception handler callback parameters
  145. *
  146. * @var array
  147. */
  148. static private $exception_handler_parameters = array();
  149. /**
  150. * The message generated by the uncaught exception
  151. *
  152. * @var string
  153. */
  154. static private $exception_message = NULL;
  155. /**
  156. * If this class is handling errors
  157. *
  158. * @var boolean
  159. */
  160. static private $handles_errors = FALSE;
  161. /**
  162. * If this class is handling exceptions
  163. *
  164. * @var boolean
  165. */
  166. static private $handles_exceptions = FALSE;
  167. /**
  168. * If the context info should be shown with errors/exceptions
  169. *
  170. * @var boolean
  171. */
  172. static private $show_context = TRUE;
  173. /**
  174. * An array of the most significant lines from error and exception backtraces
  175. *
  176. * @var array
  177. */
  178. static private $significant_error_lines = array();
  179. /**
  180. * An SMTP connection for sending error and exception emails
  181. *
  182. * @var fSMTP
  183. */
  184. static private $smtp_connection = NULL;
  185. /**
  186. * The email address to send error emails from
  187. *
  188. * @var string
  189. */
  190. static private $smtp_from_email = NULL;
  191. /**
  192. * Creates a nicely formatted backtrace to the the point where this method is called
  193. *
  194. * @param integer $remove_lines The number of trailing lines to remove from the backtrace
  195. * @param array $backtrace A backtrace from [http://php.net/backtrace `debug_backtrace()`] to format - this is not usually required or desired
  196. * @return string The formatted backtrace
  197. */
  198. static public function backtrace($remove_lines=0, $backtrace=NULL)
  199. {
  200. if ($remove_lines !== NULL && !is_numeric($remove_lines)) {
  201. $remove_lines = 0;
  202. }
  203. settype($remove_lines, 'integer');
  204. $doc_root = realpath($_SERVER['DOCUMENT_ROOT']);
  205. $doc_root .= (substr($doc_root, -1) != DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
  206. if ($backtrace === NULL) {
  207. $backtrace = debug_backtrace();
  208. }
  209. while ($remove_lines > 0) {
  210. array_shift($backtrace);
  211. $remove_lines--;
  212. }
  213. $backtrace = array_reverse($backtrace);
  214. $bt_string = '';
  215. $i = 0;
  216. foreach ($backtrace as $call) {
  217. if ($i) {
  218. $bt_string .= "\n";
  219. }
  220. if (isset($call['file'])) {
  221. $bt_string .= str_replace($doc_root, '{doc_root}' . DIRECTORY_SEPARATOR, $call['file']) . '(' . $call['line'] . '): ';
  222. } else {
  223. $bt_string .= '[internal function]: ';
  224. }
  225. if (isset($call['class'])) {
  226. $bt_string .= $call['class'] . $call['type'];
  227. }
  228. if (isset($call['class']) || isset($call['function'])) {
  229. $bt_string .= $call['function'] . '(';
  230. $j = 0;
  231. if (!isset($call['args'])) {
  232. $call['args'] = array();
  233. }
  234. foreach ($call['args'] as $arg) {
  235. if ($j) {
  236. $bt_string .= ', ';
  237. }
  238. if (is_bool($arg)) {
  239. $bt_string .= ($arg) ? 'true' : 'false';
  240. } elseif (is_null($arg)) {
  241. $bt_string .= 'NULL';
  242. } elseif (is_array($arg)) {
  243. $bt_string .= 'Array';
  244. } elseif (is_object($arg)) {
  245. $bt_string .= 'Object(' . get_class($arg) . ')';
  246. } elseif (is_string($arg)) {
  247. // Shorten the UTF-8 string if it is too long
  248. if (strlen(utf8_decode($arg)) > 18) {
  249. // If we can't match as unicode, try single byte
  250. if (!preg_match('#^(.{0,15})#us', $arg, $short_arg)) {
  251. preg_match('#^(.{0,15})#s', $arg, $short_arg);
  252. }
  253. $arg = $short_arg[0] . '...';
  254. }
  255. $bt_string .= "'" . $arg . "'";
  256. } else {
  257. $bt_string .= (string) $arg;
  258. }
  259. $j++;
  260. }
  261. $bt_string .= ')';
  262. }
  263. $i++;
  264. }
  265. return $bt_string;
  266. }
  267. /**
  268. * Performs a [http://php.net/call_user_func call_user_func()], while translating PHP 5.2 static callback syntax for PHP 5.1 and 5.0
  269. *
  270. * Parameters can be passed either as a single array of parameters or as
  271. * multiple parameters.
  272. *
  273. * {{{
  274. * #!php
  275. * // Passing multiple parameters in a normal fashion
  276. * fCore::call('Class::method', TRUE, 0, 'test');
  277. *
  278. * // Passing multiple parameters in a parameters array
  279. * fCore::call('Class::method', array(TRUE, 0, 'test'));
  280. * }}}
  281. *
  282. * To pass parameters by reference they must be assigned to an
  283. * array by reference and the function/method being called must accept those
  284. * parameters by reference. If either condition is not met, the parameter
  285. * will be passed by value.
  286. *
  287. * {{{
  288. * #!php
  289. * // Passing parameters by reference
  290. * fCore::call('Class::method', array(&$var1, &$var2));
  291. * }}}
  292. *
  293. * @param callback $callback The function or method to call
  294. * @param array $parameters The parameters to pass to the function/method
  295. * @return mixed The return value of the called function/method
  296. */
  297. static public function call($callback, $parameters=array())
  298. {
  299. // Fix PHP 5.0 and 5.1 static callback syntax
  300. if (is_string($callback) && strpos($callback, '::') !== FALSE) {
  301. $callback = explode('::', $callback);
  302. }
  303. $parameters = array_slice(func_get_args(), 1);
  304. if (sizeof($parameters) == 1 && is_array($parameters[0])) {
  305. $parameters = $parameters[0];
  306. }
  307. return call_user_func_array($callback, $parameters);
  308. }
  309. /**
  310. * Translates a Class::method style static method callback to array style for compatibility with PHP 5.0 and 5.1 and built-in PHP functions
  311. *
  312. * @param callback $callback The callback to translate
  313. * @return array The translated callback
  314. */
  315. static public function callback($callback)
  316. {
  317. if (is_string($callback) && strpos($callback, '::') !== FALSE) {
  318. return explode('::', $callback);
  319. }
  320. return $callback;
  321. }
  322. /**
  323. * Checks an error/exception destination to make sure it is valid
  324. *
  325. * @param string $destination The destination for the exception. An email, file or the string `'html'`.
  326. * @return string|boolean `'email'`, `'file'`, `'html'` or `FALSE`
  327. */
  328. static private function checkDestination($destination)
  329. {
  330. if ($destination == 'html') {
  331. return 'html';
  332. }
  333. if (preg_match('~^(?: # Allow leading whitespace
  334. (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+") # An "atom" or a quoted string
  335. (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))* # A . plus another "atom" or a quoted string, any number of times
  336. )@(?: # The @ symbol
  337. (?:[a-z0-9\\-]+\.)+[a-z]{2,}| # Domain name
  338. (?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5]) # (or) IP addresses
  339. )
  340. (?:\s*,\s* # Any number of other emails separated by a comma with surrounding spaces
  341. (?:
  342. (?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+")
  343. (?:\.[ \t]*(?:[^\x00-\x20\(\)<>@,;:\\\\"\.\[\]]+|"[^"\\\\\n\r]+"[ \t]*))*
  344. )@(?:
  345. (?:[a-z0-9\\-]+\.)+[a-z]{2,}|
  346. (?:(?:[01]?\d?\d|2[0-4]\d|25[0-5])\.){3}(?:[01]?\d?\d|2[0-4]\d|25[0-5])
  347. )
  348. )*$~xiD', $destination)) {
  349. return 'email';
  350. }
  351. $path_info = pathinfo($destination);
  352. $dir_exists = file_exists($path_info['dirname']);
  353. $dir_writable = ($dir_exists) ? is_writable($path_info['dirname']) : FALSE;
  354. $file_exists = file_exists($destination);
  355. $file_writable = ($file_exists) ? is_writable($destination) : FALSE;
  356. if (!$dir_exists || ($dir_exists && ((!$file_exists && !$dir_writable) || ($file_exists && !$file_writable)))) {
  357. return FALSE;
  358. }
  359. return 'file';
  360. }
  361. /**
  362. * Returns is the current OS is one of the OSes passed as a parameter
  363. *
  364. * Valid OS strings are:
  365. * - `'linux'`
  366. * - `'aix'`
  367. * - `'bsd'`
  368. * - `'freebsd'`
  369. * - `'netbsd'`
  370. * - `'openbsd'`
  371. * - `'osx'`
  372. * - `'solaris'`
  373. * - `'windows'`
  374. *
  375. * @param string $os The operating system to check - see method description for valid OSes
  376. * @param string ...
  377. * @return boolean If the current OS is included in the list of OSes passed as parameters
  378. */
  379. static public function checkOS($os)
  380. {
  381. $oses = func_get_args();
  382. $valid_oses = array('linux', 'aix', 'bsd', 'freebsd', 'openbsd', 'netbsd', 'osx', 'solaris', 'windows');
  383. if ($invalid_oses = array_diff($oses, $valid_oses)) {
  384. throw new fProgrammerException(
  385. 'One or more of the OSes specified, %$1s, is invalid. Must be one of: %2$s.',
  386. join(' ', $invalid_oses),
  387. join(', ', $valid_oses)
  388. );
  389. }
  390. $uname = php_uname('s');
  391. if (stripos($uname, 'linux') !== FALSE) {
  392. return in_array('linux', $oses);
  393. } elseif (stripos($uname, 'aix') !== FALSE) {
  394. return in_array('aix', $oses);
  395. } elseif (stripos($uname, 'netbsd') !== FALSE) {
  396. return in_array('netbsd', $oses) || in_array('bsd', $oses);
  397. } elseif (stripos($uname, 'openbsd') !== FALSE) {
  398. return in_array('openbsd', $oses) || in_array('bsd', $oses);
  399. } elseif (stripos($uname, 'freebsd') !== FALSE) {
  400. return in_array('freebsd', $oses) || in_array('bsd', $oses);
  401. } elseif (stripos($uname, 'solaris') !== FALSE || stripos($uname, 'sunos') !== FALSE) {
  402. return in_array('solaris', $oses);
  403. } elseif (stripos($uname, 'windows') !== FALSE) {
  404. return in_array('windows', $oses);
  405. } elseif (stripos($uname, 'darwin') !== FALSE) {
  406. return in_array('osx', $oses);
  407. }
  408. throw new fEnvironmentException('Unable to determine the current OS');
  409. }
  410. /**
  411. * Checks to see if the running version of PHP is greater or equal to the version passed
  412. *
  413. * @return boolean If the running version of PHP is greater or equal to the version passed
  414. */
  415. static public function checkVersion($version)
  416. {
  417. static $running_version = NULL;
  418. if ($running_version === NULL) {
  419. $running_version = preg_replace(
  420. '#^(\d+\.\d+\.\d+).*$#D',
  421. '\1',
  422. PHP_VERSION
  423. );
  424. }
  425. return version_compare($running_version, $version, '>=');
  426. }
  427. /**
  428. * Composes text using fText if loaded
  429. *
  430. * @param string $message The message to compose
  431. * @param mixed $component A string or number to insert into the message
  432. * @param mixed ...
  433. * @return string The composed and possible translated message
  434. */
  435. static private function compose($message)
  436. {
  437. $args = array_slice(func_get_args(), 1);
  438. if (class_exists('fText', FALSE)) {
  439. return call_user_func_array(
  440. array('fText', 'compose'),
  441. array($message, $args)
  442. );
  443. } else {
  444. return vsprintf($message, $args);
  445. }
  446. }
  447. /**
  448. * Sets an fSMTP object to be used for sending error and exception emails
  449. *
  450. * @param fSMTP $smtp The SMTP connection to send emails over
  451. * @param string $from_email The email address to use in the `From:` header
  452. * @return void
  453. */
  454. static public function configureSMTP($smtp, $from_email)
  455. {
  456. self::$smtp_connection = $smtp;
  457. self::$smtp_from_email = $from_email;
  458. }
  459. /**
  460. * Prints a debugging message if global or code-specific debugging is enabled
  461. *
  462. * @param string $message The debug message
  463. * @param boolean $force If debugging should be forced even when global debugging is off
  464. * @return void
  465. */
  466. static public function debug($message, $force=FALSE)
  467. {
  468. if ($force || self::$debug) {
  469. if (self::$debug_callback) {
  470. call_user_func(self::$debug_callback, $message);
  471. } else {
  472. self::expose($message);
  473. }
  474. }
  475. }
  476. /**
  477. * Creates a string representation of any variable using predefined strings for booleans, `NULL` and empty strings
  478. *
  479. * The string output format of this method is very similar to the output of
  480. * [http://php.net/print_r print_r()] except that the following values
  481. * are represented as special strings:
  482. *
  483. * - `TRUE`: `'{true}'`
  484. * - `FALSE`: `'{false}'`
  485. * - `NULL`: `'{null}'`
  486. * - `''`: `'{empty_string}'`
  487. *
  488. * @param mixed $data The value to dump
  489. * @return string The string representation of the value
  490. */
  491. static public function dump($data)
  492. {
  493. if (is_bool($data)) {
  494. return ($data) ? '{true}' : '{false}';
  495. } elseif (is_null($data)) {
  496. return '{null}';
  497. } elseif ($data === '') {
  498. return '{empty_string}';
  499. } elseif (is_array($data) || is_object($data)) {
  500. ob_start();
  501. var_dump($data);
  502. $output = ob_get_contents();
  503. ob_end_clean();
  504. // Make the var dump more like a print_r
  505. $output = preg_replace('#=>\n( )+(?=[a-zA-Z]|&)#m', ' => ', $output);
  506. $output = str_replace('string(0) ""', '{empty_string}', $output);
  507. $output = preg_replace('#=> (&)?NULL#', '=> \1{null}', $output);
  508. $output = preg_replace('#=> (&)?bool\((false|true)\)#', '=> \1{\2}', $output);
  509. $output = preg_replace('#(?<=^|\] => )(?:float|int)\((-?\d+(?:.\d+)?)\)#', '\1', $output);
  510. $output = preg_replace('#string\(\d+\) "#', '', $output);
  511. $output = preg_replace('#"(\n( )*)(?=\[|\})#', '\1', $output);
  512. $output = preg_replace('#((?: )+)\["(.*?)"\]#', '\1[\2]', $output);
  513. $output = preg_replace('#(?:&)?array\(\d+\) \{\n((?: )*)((?: )(?=\[)|(?=\}))#', "Array\n\\1(\n\\1\\2", $output);
  514. $output = preg_replace('/object\((\w+)\)#\d+ \(\d+\) {\n((?: )*)((?: )(?=\[)|(?=\}))/', "\\1 Object\n\\2(\n\\2\\3", $output);
  515. $output = preg_replace('#^((?: )+)}(?=\n|$)#m', "\\1)\n", $output);
  516. $output = substr($output, 0, -2) . ')';
  517. // Fix indenting issues with the var dump output
  518. $output_lines = explode("\n", $output);
  519. $new_output = array();
  520. $stack = 0;
  521. foreach ($output_lines as $line) {
  522. if (preg_match('#^((?: )*)([^ ])#', $line, $match)) {
  523. $spaces = strlen($match[1]);
  524. if ($spaces && $match[2] == '(') {
  525. $stack += 1;
  526. }
  527. $new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
  528. if ($spaces && $match[2] == ')') {
  529. $stack -= 1;
  530. }
  531. } else {
  532. $new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
  533. }
  534. }
  535. return join("\n", $new_output);
  536. } else {
  537. return (string) $data;
  538. }
  539. }
  540. /**
  541. * Disables including the context information with exception and error messages
  542. *
  543. * The context information includes the following superglobals:
  544. *
  545. * - `$_SERVER`
  546. * - `$_POST`
  547. * - `$_GET`
  548. * - `$_SESSION`
  549. * - `$_FILES`
  550. * - `$_COOKIE`
  551. *
  552. * @return void
  553. */
  554. static public function disableContext()
  555. {
  556. self::$show_context = FALSE;
  557. }
  558. /**
  559. * Enables debug messages globally, i.e. they will be shown for any call to ::debug()
  560. *
  561. * @param boolean $flag If debugging messages should be shown
  562. * @return void
  563. */
  564. static public function enableDebugging($flag)
  565. {
  566. self::$debug = (boolean) $flag;
  567. }
  568. /**
  569. * Turns on a feature where undefined constants are automatically created with the string value equivalent to the name
  570. *
  571. * This functionality only works if ::enableErrorHandling() has been
  572. * called first. This functionality may have a very slight performance
  573. * impact since a `E_STRICT` error message must be captured and then a
  574. * call to [http://php.net/define define()] is made.
  575. *
  576. * @return void
  577. */
  578. static public function enableDynamicConstants()
  579. {
  580. if (!self::$handles_errors) {
  581. throw new fProgrammerException(
  582. 'Dynamic constants can not be enabled unless error handling has been enabled via %s',
  583. __CLASS__ . '::enableErrorHandling()'
  584. );
  585. }
  586. self::$dynamic_constants = TRUE;
  587. }
  588. /**
  589. * Turns on developer-friendly error handling that includes context information including a backtrace and superglobal dumps
  590. *
  591. * All errors that match the current
  592. * [http://php.net/error_reporting error_reporting()] level will be
  593. * redirected to the destination and will include a full backtrace. In
  594. * addition, dumps of the following superglobals will be made to aid in
  595. * debugging:
  596. *
  597. * - `$_SERVER`
  598. * - `$_POST`
  599. * - `$_GET`
  600. * - `$_SESSION`
  601. * - `$_FILES`
  602. * - `$_COOKIE`
  603. *
  604. * The superglobal dumps are only done once per page, however a backtrace
  605. * in included for each error.
  606. *
  607. * If an email address is specified for the destination, only one email
  608. * will be sent per script execution. If both error and
  609. * [enableExceptionHandling() exception handling] are set to the same
  610. * email address, the email will contain both errors and exceptions.
  611. *
  612. * @param string $destination The destination for the errors and context information - an email address, a file path or the string `'html'`
  613. * @return void
  614. */
  615. static public function enableErrorHandling($destination)
  616. {
  617. if (!self::checkDestination($destination)) {
  618. return;
  619. }
  620. self::$error_destination = $destination;
  621. self::$handles_errors = TRUE;
  622. set_error_handler(self::callback(self::handleError));
  623. }
  624. /**
  625. * Turns on developer-friendly uncaught exception handling that includes context information including a backtrace and superglobal dumps
  626. *
  627. * Any uncaught exception will be redirected to the destination specified,
  628. * and the page will execute the `$closing_code` callback before exiting.
  629. * The destination will receive a message with the exception messaage, a
  630. * full backtrace and dumps of the following superglobals to aid in
  631. * debugging:
  632. *
  633. * - `$_SERVER`
  634. * - `$_POST`
  635. * - `$_GET`
  636. * - `$_SESSION`
  637. * - `$_FILES`
  638. * - `$_COOKIE`
  639. *
  640. * The superglobal dumps are only done once per page, however a backtrace
  641. * in included for each error.
  642. *
  643. * If an email address is specified for the destination, only one email
  644. * will be sent per script execution.
  645. *
  646. * If an email address is specified for the destination, only one email
  647. * will be sent per script execution. If both exception and
  648. * [enableErrorHandling() error handling] are set to the same
  649. * email address, the email will contain both exceptions and errors.
  650. *
  651. * @param string $destination The destination for the exception and context information - an email address, a file path or the string `'html'`
  652. * @param callback $closing_code This callback will happen after the exception is handled and before page execution stops. Good for printing a footer. If no callback is provided and the exception extends fException, fException::printMessage() will be called.
  653. * @param array $parameters The parameters to send to `$closing_code`
  654. * @return void
  655. */
  656. static public function enableExceptionHandling($destination, $closing_code=NULL, $parameters=array())
  657. {
  658. if (!self::checkDestination($destination)) {
  659. return;
  660. }
  661. self::$handles_exceptions = TRUE;
  662. self::$exception_destination = $destination;
  663. self::$exception_handler_callback = $closing_code;
  664. if (!is_object($parameters)) {
  665. settype($parameters, 'array');
  666. } else {
  667. $parameters = array($parameters);
  668. }
  669. self::$exception_handler_parameters = $parameters;
  670. set_exception_handler(self::callback(self::handleException));
  671. }
  672. /**
  673. * Prints the ::dump() of a value
  674. *
  675. * The dump will be printed in a `<pre>` tag with the class `exposed` if
  676. * PHP is running anywhere but via the command line (cli). If PHP is
  677. * running via the cli, the data will be printed, followed by a single
  678. * line break (`\n`).
  679. *
  680. * If multiple parameters are passed, they are exposed as an array.
  681. *
  682. * @param mixed $data The value to show
  683. * @param mixed ...
  684. * @return void
  685. */
  686. static public function expose($data)
  687. {
  688. $args = func_get_args();
  689. if (count($args) > 1) {
  690. $data = $args;
  691. }
  692. if (PHP_SAPI != 'cli') {
  693. echo '<pre class="exposed">' . htmlspecialchars((string) self::dump($data), ENT_QUOTES) . '</pre>';
  694. } else {
  695. echo self::dump($data) . "\n";
  696. }
  697. }
  698. /**
  699. * Generates some information about the context of an error or exception
  700. *
  701. * @return string A string containing `$_SERVER`, `$_GET`, `$_POST`, `$_FILES`, `$_SESSION` and `$_COOKIE`
  702. */
  703. static private function generateContext()
  704. {
  705. return self::compose('Context') . "\n-------" .
  706. "\n\n\$_SERVER: " . self::dump($_SERVER) .
  707. "\n\n\$_POST: " . self::dump($_POST) .
  708. "\n\n\$_GET: " . self::dump($_GET) .
  709. "\n\n\$_FILES: " . self::dump($_FILES) .
  710. "\n\n\$_SESSION: " . self::dump((isset($_SESSION)) ? $_SESSION : NULL) .
  711. "\n\n\$_COOKIE: " . self::dump($_COOKIE);
  712. }
  713. /**
  714. * If debugging is enabled
  715. *
  716. * @param boolean $force If debugging is forced
  717. * @return boolean If debugging is enabled
  718. */
  719. static public function getDebug($force=FALSE)
  720. {
  721. return self::$debug || $force;
  722. }
  723. /**
  724. * Handles an error, creating the necessary context information and sending it to the specified destination
  725. *
  726. * @internal
  727. *
  728. * @param integer $error_number The error type
  729. * @param string $error_string The message for the error
  730. * @param string $error_file The file the error occurred in
  731. * @param integer $error_line The line the error occurred on
  732. * @param array $error_context A references to all variables in scope at the occurence of the error
  733. * @return void
  734. */
  735. static public function handleError($error_number, $error_string, $error_file=NULL, $error_line=NULL, $error_context=NULL)
  736. {
  737. if (self::$dynamic_constants && $error_number == E_NOTICE) {
  738. if (preg_match("#^Use of undefined constant (\w+) - assumed '\w+'\$#D", $error_string, $matches)) {
  739. define($matches[1], $matches[1]);
  740. return;
  741. }
  742. }
  743. $capturing = (bool) self::$captured_error_level;
  744. $level_match = (bool) (error_reporting() & $error_number);
  745. if (!$capturing && !$level_match) {
  746. return;
  747. }
  748. $doc_root = realpath($_SERVER['DOCUMENT_ROOT']);
  749. $doc_root .= (substr($doc_root, -1) != '/' && substr($doc_root, -1) != '\\') ? '/' : '';
  750. $backtrace = self::backtrace(1);
  751. // Remove the reference to handleError
  752. $backtrace = preg_replace('#: fCore::handleError\(.*?\)$#', '', $backtrace);
  753. $error_string = preg_replace('# \[<a href=\'.*?</a>\]: #', ': ', $error_string);
  754. // This was added in 5.2
  755. if (!defined('E_RECOVERABLE_ERROR')) {
  756. define('E_RECOVERABLE_ERROR', 4096);
  757. }
  758. // These were added in 5.3
  759. if (!defined('E_DEPRECATED')) {
  760. define('E_DEPRECATED', 8192);
  761. }
  762. if (!defined('E_USER_DEPRECATED')) {
  763. define('E_USER_DEPRECATED', 16384);
  764. }
  765. switch ($error_number) {
  766. case E_WARNING: $type = self::compose('Warning'); break;
  767. case E_NOTICE: $type = self::compose('Notice'); break;
  768. case E_USER_ERROR: $type = self::compose('User Error'); break;
  769. case E_USER_WARNING: $type = self::compose('User Warning'); break;
  770. case E_USER_NOTICE: $type = self::compose('User Notice'); break;
  771. case E_STRICT: $type = self::compose('Strict'); break;
  772. case E_RECOVERABLE_ERROR: $type = self::compose('Recoverable Error'); break;
  773. case E_DEPRECATED: $type = self::compose('Deprecated'); break;
  774. case E_USER_DEPRECATED: $type = self::compose('User Deprecated'); break;
  775. }
  776. if ($capturing) {
  777. $type_to_capture = (bool) (self::$captured_error_types[self::$captured_error_level] & $error_number);
  778. $string_to_capture = !self::$captured_error_regex[self::$captured_error_level] || (self::$captured_error_regex[self::$captured_error_level] && preg_match(self::$captured_error_regex[self::$captured_error_level], $error_string));
  779. if ($type_to_capture && $string_to_capture) {
  780. self::$captured_errors[self::$captured_error_level][] = array(
  781. 'number' => $error_number,
  782. 'type' => $type,
  783. 'string' => $error_string,
  784. 'file' => str_replace($doc_root, '{doc_root}/', $error_file),
  785. 'line' => $error_line,
  786. 'backtrace' => $backtrace,
  787. 'context' => $error_context
  788. );
  789. return;
  790. }
  791. // If the old handler is not this method, then we must have been trying to match a regex and failed
  792. // so we pass the error on to the original handler to do its thing
  793. if (self::$captured_errors_previous_handler[self::$captured_error_level] != array('fCore', 'handleError')) {
  794. if (self::$captured_errors_previous_handler[self::$captured_error_level] === NULL) {
  795. return FALSE;
  796. }
  797. return call_user_func(self::$captured_errors_previous_handler[self::$captured_error_level], $error_number, $error_string, $error_file, $error_line, $error_context);
  798. // If we get here, this method is the error handler, but we don't want to actually report the error so we return
  799. } elseif (!$level_match) {
  800. return;
  801. }
  802. }
  803. $error = $type . "\n" . str_pad('', strlen($type), '-') . "\n" . $backtrace . "\n" . $error_string;
  804. $backtrace_lines = explode("\n", $backtrace);
  805. self::sendMessageToDestination('error', $error, end($backtrace_lines));
  806. }
  807. /**
  808. * Handles an uncaught exception, creating the necessary context information, sending it to the specified destination and finally executing the closing callback
  809. *
  810. * @internal
  811. *
  812. * @param object $exception The uncaught exception to handle
  813. * @return void
  814. */
  815. static public function handleException($exception)
  816. {
  817. $message = ($exception->getMessage()) ? $exception->getMessage() : '{no message}';
  818. if ($exception instanceof fException) {
  819. $trace = $exception->formatTrace();
  820. } else {
  821. $trace = $exception->getTraceAsString();
  822. }
  823. $code = ($exception->getCode()) ? ' (code ' . $exception->getCode() . ')' : '';
  824. $info = $trace . "\n" . $message . $code;
  825. $headline = self::compose("Uncaught") . " " . get_class($exception);
  826. $info_block = $headline . "\n" . str_pad('', strlen($headline), '-') . "\n" . trim($info);
  827. $trace_lines = explode("\n", $trace);
  828. self::sendMessageToDestination('exception', $info_block, end($trace_lines));
  829. if (self::$exception_handler_callback === NULL) {
  830. if (self::$exception_destination != 'html' && $exception instanceof fException) {
  831. $exception->printMessage();
  832. }
  833. return;
  834. }
  835. try {
  836. self::call(self::$exception_handler_callback, self::$exception_handler_parameters);
  837. } catch (Exception $e) {
  838. trigger_error(
  839. self::compose(
  840. 'An exception was thrown in the %s closing code callback',
  841. 'setExceptionHandling()'
  842. ),
  843. E_USER_ERROR
  844. );
  845. }
  846. }
  847. /**
  848. * Registers a callback to handle debug messages instead of the default action of calling ::expose() on the message
  849. *
  850. * @param callback $callback A callback that accepts a single parameter, the string debug message to handle
  851. * @return void
  852. */
  853. static public function registerDebugCallback($callback)
  854. {
  855. self::$debug_callback = self::callback($callback);
  856. }
  857. /**
  858. * Resets the configuration of the class
  859. *
  860. * @internal
  861. *
  862. * @return void
  863. */
  864. static public function reset()
  865. {
  866. if (self::$handles_errors) {
  867. restore_error_handler();
  868. }
  869. if (self::$handles_exceptions) {
  870. restore_exception_handler();
  871. }
  872. if (is_array(self::$captured_errors)) {
  873. restore_error_handler();
  874. }
  875. self::$captured_error_level = 0;
  876. self::$captured_error_regex = array();
  877. self::$captured_error_types = array();
  878. self::$captured_errors = array();
  879. self::$captured_errors_previous_handler = array();
  880. self::$context_shown = FALSE;
  881. self::$debug = NULL;
  882. self::$debug_callback = NULL;
  883. self::$dynamic_constants = FALSE;
  884. self::$error_destination = 'html';
  885. self::$error_message_queue = array();
  886. self::$exception_destination = 'html';
  887. self::$exception_handler_callback = NULL;
  888. self::$exception_handler_parameters = array();
  889. self::$exception_message = NULL;
  890. self::$handles_errors = FALSE;
  891. self::$handles_exceptions = FALSE;
  892. self::$significant_error_lines = array();
  893. self::$show_context = TRUE;
  894. self::$smtp_connection = NULL;
  895. self::$smtp_from_email = NULL;
  896. }
  897. /**
  898. * Sends an email or writes a file with messages generated during the page execution
  899. *
  900. * This method prevents multiple emails from being sent or a log file from
  901. * being written multiple times for one script execution.
  902. *
  903. * @internal
  904. *
  905. * @return void
  906. */
  907. static public function sendMessagesOnShutdown()
  908. {
  909. $messages = array();
  910. if (self::$error_message_queue) {
  911. $message = join("\n\n", self::$error_message_queue);
  912. $messages[self::$error_destination] = $message;
  913. }
  914. if (self::$exception_message) {
  915. if (isset($messages[self::$exception_destination])) {
  916. $messages[self::$exception_destination] .= "\n\n";
  917. } else {
  918. $messages[self::$exception_destination] = '';
  919. }
  920. $messages[self::$exception_destination] .= self::$exception_message;
  921. }
  922. $hash = md5(join('', self::$significant_error_lines), TRUE);
  923. $hash = strtr(base64_encode($hash), '/', '-');
  924. $hash = substr(rtrim($hash, '='), 0, 8);
  925. $first_file_line = preg_replace(
  926. '#^.*[/\\\\](.*)$#',
  927. '\1',
  928. reset(self::$significant_error_lines)
  929. );
  930. $subject = self::compose(
  931. '[%1$s] %2$s error(s) beginning at %3$s {%4$s}',
  932. isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : php_uname('n'),
  933. count($messages),
  934. $first_file_line,
  935. $hash
  936. );
  937. foreach ($messages as $destination => $message) {
  938. if (self::$show_context) {
  939. $message .= "\n\n" . self::generateContext();
  940. }
  941. if (self::checkDestination($destination) == 'email') {
  942. if (self::$smtp_connection) {
  943. $email = new fEmail();
  944. foreach (explode(',', $destination) as $recipient) {
  945. $email->addRecipient($recipient);
  946. }
  947. $email->setFromEmail(self::$smtp_from_email);
  948. $email->setSubject($subject);
  949. $email->setBody($message);
  950. $email->send(self::$smtp_connection);
  951. } else {
  952. mail($destination, $subject, $message);
  953. }
  954. } else {
  955. $handle = fopen($destination, 'a');
  956. fwrite($handle, $subject . "\n\n");
  957. fwrite($handle, $message . "\n\n");
  958. fclose($handle);
  959. }
  960. }
  961. }
  962. /**
  963. * Handles sending a message to a destination
  964. *
  965. * If the destination is an email address or file, the messages will be
  966. * spooled up until the end of the script execution to prevent multiple
  967. * emails from being sent or a log file being written to multiple times.
  968. *
  969. * @param string $type If the message is an error or an exception
  970. * @param string $message The message to send to the destination
  971. * @param string $significant_line The most significant line from an error or exception backtrace
  972. * @return void
  973. */
  974. static private function sendMessageToDestination($type, $message, $significant_line)
  975. {
  976. $destination = ($type == 'exception') ? self::$exception_destination : self::$error_destination;
  977. if ($destination == 'html') {
  978. if (self::$show_context && !self::$context_shown) {
  979. self::expose(self::generateContext());
  980. self::$context_shown = TRUE;
  981. }
  982. self::expose($message);
  983. return;
  984. }
  985. static $registered_function = FALSE;
  986. if (!$registered_function) {
  987. register_shutdown_function(self::callback(self::sendMessagesOnShutdown));
  988. $registered_function = TRUE;
  989. }
  990. if ($type == 'error') {
  991. self::$error_message_queue[] = $message;
  992. } else {
  993. self::$exception_message = $message;
  994. }
  995. self::$significant_error_lines[] = $significant_line;
  996. }
  997. /**
  998. * Temporarily enables capturing error messages
  999. *
  1000. * @param integer $types The error types to capture - this should be as specific as possible - defaults to all (E_ALL | E_STRICT)
  1001. * @param string $regex A PCRE regex to match against the error message
  1002. * @return void
  1003. */
  1004. static public function startErrorCapture($types=NULL, $regex=NULL)
  1005. {
  1006. if ($types === NULL) {
  1007. $types = E_ALL | E_STRICT;
  1008. }
  1009. self::$captured_error_level++;
  1010. self::$captured_error_regex[self::$captured_error_level] = $regex;
  1011. self::$captured_error_types[self::$captured_error_level] = $types;
  1012. self::$captured_errors[self::$captured_error_level] = array();
  1013. self::$captured_errors_previous_handler[self::$captured_error_level] = set_error_handler(self::callback(self::handleError));
  1014. }
  1015. /**
  1016. * Stops capturing error messages, returning all that have been captured
  1017. *
  1018. * @param string $regex A PCRE regex to filter messages by
  1019. * @return array The captured error messages
  1020. */
  1021. static public function stopErrorCapture($regex=NULL)
  1022. {
  1023. $captures = self::$captured_errors[self::$captured_error_level];
  1024. self::$captured_error_level--;
  1025. self::$captured_error_regex = array_slice(self::$captured_error_regex, 0, self::$captured_error_level, TRUE);
  1026. self::$captured_error_types = array_slice(self::$captured_error_types, 0, self::$captured_error_level, TRUE);
  1027. self::$captured_errors = array_slice(self::$captured_errors, 0, self::$captured_error_level, TRUE);
  1028. self::$captured_errors_previous_handler = array_slice(self::$captured_errors_previous_handler, 0, self::$captured_error_level, TRUE);
  1029. restore_error_handler();
  1030. if ($regex) {
  1031. $new_captures = array();
  1032. foreach ($captures as $capture) {
  1033. if (!preg_match($regex, $capture['string'])) { continue; }
  1034. $new_captures[] = $capture;
  1035. }
  1036. $captures = $new_captures;
  1037. }
  1038. return $captures;
  1039. }
  1040. /**
  1041. * Forces use as a static class
  1042. *
  1043. * @return fCore
  1044. */
  1045. private function __construct() { }
  1046. }
  1047. /**
  1048. * Copyright (c) 2007-2011 Will Bond <will@flourishlib.com>, others
  1049. *
  1050. * Permission is hereby granted, free of charge, to any person obtaining a copy
  1051. * of this software and associated documentation files (the "Software"), to deal
  1052. * in the Software without restriction, including without limitation the rights
  1053. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  1054. * copies of the Software, and to permit persons to whom the Software is
  1055. * furnished to do so, subject to the following conditions:
  1056. *
  1057. * The above copyright notice and this permission notice shall be included in
  1058. * all copies or substantial portions of the Software.
  1059. *
  1060. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  1061. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  1062. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  1063. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  1064. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  1065. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  1066. * THE SOFTWARE.
  1067. */