root/fCore.php

Revision 758, 32.1 kB (checked in by wbond, 1 week ago)

Fixed ticket #397 - fFile, fImage and fDirectory all handle re-used filenames better and produce more usable exceptions when trying to perform actions on objects that have been deleted

InternalBackwardsCompatibilityBreak - fFilesystem::hookExceptionMap() was changed to fFilesystem::hookDeletedMap() and fFilesystem::updateExceptionMap() was changed to fFilesystem::updateDeletedMap()

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