made a copy
[atutor.git] / include / classes / ErrorHandler / ErrorHandler.class.php
1 <?php 
2 /************************************************************************/
3 /* ATutor                                                                                                                               */
4 /************************************************************************/
5 /* Copyright (c) 2002-2008 by Greg Gay, Joel Kronenberg & Heidi Hazelton*/
6 /* Adaptive Technology Resource Centre / University of Toronto                  */
7 /* http://atutor.ca                                                                                                             */
8 /*                                                                                                                                              */
9 /* This program is free software. You can redistribute it and/or                */
10 /* modify it under the terms of the GNU General Public License                  */
11 /* as published by the Free Software Foundation.                                                */
12 /************************************************************************/
13                 
14 /**
15 * ErrorHandler
16 * Custom ErrorHandler for php. Ability to log and send errors over e-mail
17 * @access       public
18 * @author       Jacek Materna
19 */
20
21 class ErrorHandler { 
22
23         /** 
24         * Log errors to file?
25         * 
26         * @var Boolean 
27         * @access public 
28         */ 
29         var $LOG_ERR_TO_FILE; 
30         
31         /** 
32         * Log warnings to file? 
33         * 
34         * @var Boolean 
35         * @access public  
36         */ 
37         var $LOG_WARN_TO_FILE;   
38         
39         /**
40          * Message object
41          *
42          * @var object
43          * @access public
44          */
45         var $msg;
46         
47         /**
48          * Container to store errors until we decide to print them all
49          *
50          * @var array
51          * @access public
52          */
53         var $container;
54          
55         /** 
56         * Constructor for this class
57         * @return void 
58         * @access public 
59         */ 
60         function ErrorHandler() {  
61                 
62                 $this->setFlags(); // false by default
63                 set_error_handler(array(&$this, 'ERROR_HOOK')); 
64                 $this->container = array();
65                 
66                 /**
67                  * check first if the log directory is setup, if not then create a logs dir with a+w && a-r
68                  */
69                 if (!file_exists(AT_CONTENT_DIR . 'logs/') || !realpath(AT_CONTENT_DIR. 'logs/')) {
70                         $this->makeLogDir();
71                 } else if (!is_dir(AT_CONTENT_DIR .'logs/')) {
72                         $this->makeLogDir();
73                 } 
74         }
75         
76         /** 
77         * The error handling routine set by set_error_handler(). Mimicking and Exception implementation in OOA
78         * Must be as quick as possible. Note a few: '\n' -> chr(10) - avoids inline translation in php engine
79         *                                                                                        'single quotes', avoid translation again
80         * Ability define custom errors. See case 'VITAL':
81         * 
82         * eg call from script, trigger_error('VITAL#There was a problem with the database.','E_USER_ERROR');
83         *
84         * @param string $error_type The type of error being handled. 
85         * @param string $error_msg The error message being handled. 
86         * @param string $error_file The file in which the error occurred. 
87         * @param integer $error_ln The line in which the error occurred. 
88         * @param string $error_context The context in which the error occurred.
89         * @return Boolean 
90         * @access public 
91         */ 
92         function ERROR_HOOK($error_type, $error_msg, $error_file, $error_ln, $error_context) { 
93                 if ($error_type == E_NOTICE || $error_type == E_USER_NOTICE) return;
94                 
95                 $val_phpinfo = '';
96                 $val_phpinfo_foot = '';
97                 $val_phpinfo_printed  = false; // used to track for the scope of this method whether the server
98                                                                                 // has been attached to a log file or e-mail buffer previously
99                 
100                 /**
101                  * Only produce the server configuration once per file
102                  */
103                 if ($this->todayLogFileExists() === false) {
104                         // lets get some info about the system used by all error codes
105                         ob_start();
106                         
107                         // grab usefull data from php_info
108                         phpinfo(INFO_GENERAL ^ INFO_CONFIGURATION);
109                         $val_phpinfo .= ob_get_contents();
110                         ob_end_clean();
111                         
112                         /*
113                          * Parse $val_phpinfo
114                          */
115                         
116                         // get a substring of the php info to get rid of the html, head, title, etc.
117                         $val_phpinfo = substr($val_phpinfo, 760, -19);
118                         $val_phpinfo = substr($val_phpinfo, 552);
119                         $val_phpinfo = substr($val_phpinfo, strpos($val_phpinfo, 'System'));
120                         $val_phpinfo .= chr(10);
121                         
122                         $msql_str = '';
123                         if (defined('MYSQL_NUM'))
124                                 $msql_str = "Yes";
125                         else
126                                 $msql_str = "No";
127                         
128                         $val_phpinfo .= 'MySQL installed? ' . $msql_str . '<br/><br/>';
129                         
130                         // replace the </td>'s with tabs and the $nbsp;'s with spaces
131                         $val_phpinfo = str_replace( '</td>', '    ', $val_phpinfo);
132                         $val_phpinfo = str_replace( '&nbsp;', ' ', $val_phpinfo);
133                         $val_phpinfo = str_replace( '</body>', ' ', $val_phpinfo);
134                         $val_phpinfo = str_replace( '</html>', ' ', $val_phpinfo);
135                         
136                         $val_phpinfo = str_replace('This program makes use of the Zend Scripting Language Engine:<br />Zend Engine v1.3.0, Copyright (c) 1998-2003 Zend Technologies', '', $val_phpinfo);
137                 
138                         $val_phpinfo_foot .= '$_ENV:' . chr(10) . $this->debug($_ENV) . '<br/><br/>';
139                 } 
140                 
141                 // Everytime
142                 $val_phpuser = '$_SESSION:' . chr(10) . $this->debug($_SESSION);
143                 $val_phpuser .= '$_REQUEST:' . chr(10) . $this->debug($_REQUEST);
144                 $val_phpuser .= '$_COOKIE:' . chr(10) . $this->debug($_COOKIE);
145                 $val_phpuser .= '$_GET:' . chr(10) . $this->debug($_GET);
146                 $val_phpuser .= '$_POST:' . chr(10) . $this->debug($_POST);
147                 
148                 // replace the </td>'s with tabs and the $nbsp;'s with spaces
149                 $val_phpuser = str_replace( '</td>', '    ', $val_phpuser);
150                 $val_phpuser = str_replace( '&nbsp;', ' ', $val_phpuser) . '<br/>';
151                 
152                 switch($error_type) {
153                         
154                         case E_ERROR: // caught below
155                         case E_USER_ERROR: 
156
157                                 if (substr_count($error_msg, "#") > 0) {
158                                         $_error = explode("#", $error_msg);
159                                 } else {
160                                         $_error = array('', $error_msg);
161                                 }
162
163                                 /**
164                                  * eg call, trigger_error('VITAL;There was a problem with the database.',E_USER_ERROR);
165                                  *
166                                  * List of custom errors go here and the appropriate action is taken
167                                  *@
168                                  */
169                                 switch($_error[0]) {
170                                         /**
171                                          * Custom errors are not guaranteed to be printed for example in footer.inc.php
172                                          * Hanlde on a case-by-case basis
173                                          */
174                                         case 'VITAL': // @see vital.inc.php
175                                                 if ($this->LOG_ERR_TO_FILE) { 
176                                                                 if ($val_phpinfo_printed === true) {
177                                                                         $val_phpinfo = '';
178                                                                 }
179                                                                 $this->log_to_files($val_phpinfo, $val_phpinfo_foot, 'ATutor v' . VERSION . '<br/>'. 'PHP ERROR MESSAGE:' . '<br/><p>'
180                                                                                 . $error_msg . ' (error type ' . $error_type . ' in ' 
181                                                                                 . $error_file . ' on line ' . $error_ln . ') [context: ' 
182                                                                                 . $error_context . ']</p>' . chr(10) .chr(10) . $val_phpuser );
183                                                                                 
184                                                                 $val_phpinfo_printed = true;
185                                                                 
186                                                 }                                       
187                                                 
188                                                 $this->printError('<strong>ATutor has detected an Error<strong> - ' .
189                                                                                                                 $_error[1]);
190
191                                                 exit; // done here
192                                                 break;
193                                                 
194                                         case 'BKP_MEMBER': // @see TableBackup.class.php
195                                                 if ($this->LOG_ERR_TO_FILE) { 
196                                                                 if ($val_phpinfo_printed === true) {
197                                                                         $val_phpinfo = '';
198                                                                 }
199                                                                 $this->log_to_files($val_phpinfo, $val_phpinfo_foot, 'ATutor v' . VERSION . '<br/>'. 'PHP ERROR MESSAGE:' . '<br/><p>'
200                                                                                 . $error_msg . ' (error type ' . $error_type . ' in ' 
201                                                                                 . $error_file . ' on line ' . $error_ln . ') [context: ' 
202                                                                                 . $error_context . ']</p>' . chr(10) .chr(10) . $val_phpuser );
203                                                                                 
204                                                                 $val_phpinfo_printed = true;
205                                                                 
206                                                 }
207                                                 
208                                                 $this->printError('<strong>ATutor has detected an Error<strong> - ' .
209                                                                                                                 $_error[1]);
210                                         
211                                                 exit;
212                                                 break;
213                                         
214                                         default: // standard user error without custom prefix
215                                                 if ($this->LOG_ERR_TO_FILE) { 
216                                                                 if ($val_phpinfo_printed === true) {
217                                                                         $val_phpinfo = '';
218                                                                 }
219                                                                 
220                                                                 $this->log_to_files($val_phpinfo, $val_phpinfo_foot, 'ATutor v' . VERSION . '<br/>'. 'PHP ERROR MESSAGE:' . '<br/><p>'
221                                                                                 . $error_msg . ' (error type ' . $error_type . ' in ' 
222                                                                                 . $error_file . ' on line ' . $error_ln . ') [context: ' 
223                                                                                 . $error_context . ']</p>' . chr(10) .chr(10) . $val_phpuser);
224                                                                 
225                                                                 $val_phpinfo_printed = true;
226                                                 }                                       
227                                 }
228                                 
229                                 //$this->printError('<strong>ATutor has detected an Error<strong> - ' . 'Problem spot: ' . $error_msg . ' in ' 
230                                 //                              . $this->stripbase($error_file) . ' on line ' . $error_ln);
231                                 array_push($this->container, 'Problem spot: ' . $error_msg . ' in ' . $this->stripbase($error_file) . ' on line ' . $error_ln);
232                                                                         
233                                 break;
234                         
235                         case E_WARNING: 
236                         case E_USER_WARNING: 
237                                 if ($this->LOG_WARN_TO_FILE) { 
238                                         if ($val_phpinfo_printed === true) {
239                                                 $val_phpinfo = '';
240                                         }
241                                                                 
242                                         $this->log_to_files($val_phpinfo, $val_phpinfo_foot, 'ATutor v' . VERSION . '<br/>'. 'PHP ERROR MESSAGE:' . '<br/><p>'
243                                                         . $error_msg . ' (error type ' . $error_type . ' in ' 
244                                                         . $error_file . ' on line ' . $error_ln . ') [context: ' 
245                                                         . $error_context . ']</p>' . chr(10) . chr(10) . $val_phpuser);                 
246                                 
247                                         $val_phpinfo_printed = true;
248                                 }
249
250                                 //$this->printError('<strong>ATutor has detected an Error</strong> - ' . 'Problem spot: ' . $error_msg . ' in ' 
251                                 //                              . $this->stripbase($error_file) . ' on line ' . $error_ln);
252                                 array_push($this->container, 'Problem spot: ' . $error_msg . ' in ' . $this->stripbase($error_file) . ' on line ' . $error_ln);
253         
254                                 break;
255                          default:
256                 }
257
258                 return true; 
259         }
260         
261         /** 
262 ÊÊ      * Dump the current error into a file along with an updated profile for that error
263 ÊÊ      * 
264         * @param string the profile to log
265         * @param string the bug to log 
266 ÊÊ      * @access public
267 ÊÊ      */
268         function log_to_files($profile, $profile_foot, $buf) {
269                 
270                 if ($profile == '' || $profile_foor = '' || $buf == '') return;
271                 
272                 /**
273                  * Redundancy control for profile/error log creation
274                  */
275                  $profile_created = true;
276                  $error_created = true;
277                  
278                 $php_head = '<?php echo \'Only viewable as Admin user\'; exit; ?>' . chr(10);
279                 
280                 // Lets make a unqiue profile key, strip away circumventors of the md5 hashing algo. @see md5 algo src
281                 $temp = strip_tags($profile);
282                 $temp = stripslashes($temp);
283                 $temp = str_replace('/', ' ', $temp);
284                 $temp = str_replace('\$', ' ', $temp);
285                 $temp = str_replace('$', ' ', $temp);
286                 $temp = str_replace('\&' , ' ', $temp);
287                 $temp = str_replace('&' , ' ', $temp);
288                 $temp = str_replace('*' , ' ', $temp);
289                 $temp = str_replace('~' , ' ', $temp);
290                 $temp = str_replace('.' , ' ', $temp);
291                 $temp = str_replace(';' , ' ', $temp);
292                 $temp = str_replace(':' , ' ', $temp);
293                 $temp = str_replace('-' , ' ', $temp);
294                 $temp = str_replace('_' , ' ', $temp);
295                 $temp = str_replace('\'' , ' ', $temp);
296                 $temp = str_replace(',' , ' ', $temp);
297                 $temp = str_replace('@' , ' ', $temp);
298                 $temp = str_replace('#' , ' ', $temp);
299
300                 $profile_key = md5($temp);
301                 
302                 $today = getdate(); 
303                 
304                 // Uniqueness assumend to be coupled to epoch timestamp
305                 $timestamp_ = $today['mon'] . '-' . $today['mday'] . '-' . $today['year'];
306                 
307                 /**
308                  * Lets make sure we have a log directory made for today
309                  */ 
310                 if (!is_dir(AT_CONTENT_DIR . 'logs/' . $timestamp_)) { // create it
311                         $result = @mkdir(AT_CONTENT_DIR . 'logs/' . $timestamp_, 0771); // r+w for owner
312         
313                         if ($result == 0) {
314                                 $this->printError('Fatal. Could not create /content/logs' . '/' . $timestamp_ . '. Please resolve');
315                         }
316                 } // else already there
317                 
318                 /**
319                  * Go through all the profiles in our directory and lets try and map our md5 key to one of them,
320                  * if its not found then we must be dealing with a new profile, thus create it
321                  */
322                  $dir_ = AT_CONTENT_DIR . 'logs/' . $timestamp_;
323                 
324                 if (!($dir = opendir($dir_))) {
325                         $msg->printNoLookupFeedback('Could not access /content/logs/' . $timestamp_ . '. Check that the permission for the <strong>Server</string> user are r+w to it');
326                         require(AT_INCLUDE_PATH.'footer.inc.php'); 
327                         
328                         exit;
329                 }
330                 
331                 /**
332                  * Run through the todays logs directory and lets get all the profiles
333                  */ 
334                 $use_profile = null;
335                 
336                 // loop through folder todays log folder and try and match a profile to our error profile md5 key
337                 while (($file = readdir($dir)) !== false) {
338                 
339                         /* if the name is not a directory */
340                         if( ($file == '.') || ($file == '..') || is_dir($file) ) {
341                                 continue;
342                         }
343                 
344                         if (strpos($file, 'profile') >= 0) {
345                                 $check_key = substr($file, strpos($file, '_') + 1);
346                                 $check_key = substr($check_key, 0, strpos($check_key, '.log.php'));
347
348                                 if ($check_key === $profile_key) { // found!
349                                         $use_profile = $file;
350                                         $profile_created = true;
351                                         break;
352                                 }
353                         }
354                 }
355                 closedir($dir); // clean it up
356                 
357                 // if $use_profile == null here then we must create a new profile for this error
358                 if ($use_profile == null) {
359                         $use_profile = 'profile_' . $profile_key . '.log.php';
360                         if ($file_handle = fopen(AT_CONTENT_DIR . 'logs/' . $timestamp_ . '/' . $use_profile, "w")) {
361                                 if (!fwrite($file_handle, $php_head . chr(10) . $profile . $profile_foot)) { $profile_created = false; }
362                         } else { $profile_created = false; }
363                         fclose($file_handle);
364                 } // else just use $use_profile as the profile for this error
365                 
366                 // if the creation of the profile_created = false then creation failed and we didnt have an already
367                 // existant one in the dir, profile must exist
368                 if ($profile_created === false) return;
369                 
370                 $timestamp = $timestamp_ . '_' . $today[0];
371                                         
372                 // create a unique error filename including the epoch timestamp + and the profile mapping
373                 $unique_error_log = $timestamp . '_pr' . $profile_key;
374
375                 if (is_file(AT_CONTENT_DIR . 'logs/' . $timestamp_ . '/' . $unique_error_log)) {
376                         $unique_error_log .= rand(); // should be enough
377                 }
378                 
379                 $unique_error_log .= '.log.php'; // append suffix
380                 
381                 /* Create error log file */
382                 if ($file_handle = fopen(AT_CONTENT_DIR . 'logs/' . $timestamp_ . '/' . $unique_error_log, "w")) {
383                         if (!fwrite($file_handle, $php_head . chr(10) . $buf)) {  $error_created = false;  }
384                 } else {
385                         $error_created = false;
386                 }
387                 fclose($file_handle);
388                 
389                 // check that we created a profile and its error or used an existing profile and created its error
390                 if ($profile_created === true && $error_created === true) { // ok
391                         chmod(AT_CONTENT_DIR . 'logs/' . $timestamp_ . '/' . $unique_error_log, 0771);
392                         chmod(AT_CONTENT_DIR . 'logs/' . $timestamp_ . '/' . $use_profile, 0771);
393                 } else if ($profile_create === true && $error_created === false) { // remove profile
394                         unlink(AT_CONTENT_DIR . 'logs/' . $timestamp_ . '/' . $use_profile);
395                 } 
396         }
397
398         /** 
399 ÊÊ      * Restores the error handler to the default error handler 
400 ÊÊ      * 
401 ÊÊ      * @return void 
402 ÊÊ      * @access public
403 ÊÊ      */
404         function restoreOrigHandler() {
405                 restore_error_handler();
406         }
407
408         /** 
409 Ê       * Returns the error handler to ERROR_HOOK() 
410 Ê       * 
411 ÊÊ      * @return void 
412 ÊÊ      * @access public  
413 ÊÊ      */
414         function returnHandler() {
415                 set_error_handler(array(&$this, 'ERROR_HOOK'));
416         }
417         
418         /** 
419 ÊÊ      * Changes the logging preferences
420 ÊÊ      * 
421         * @param Boolean $error_flag Log errors to file? 
422 ÊÊ      * @param Boolean $warning_flag Log warnings to file? 
423 ÊÊ      * @return void 
424 ÊÊ      * @access public 
425 ÊÊ      */
426         function setFlags($error_flag = true, $warning_flag = true) {                            
427                 
428                 $this->LOG_ERR_TO_FILE = $error_flag;
429                 $this->LOG_WARN_TO_FILE = $warning_flag;
430         }
431         
432         /**
433          * Construct a nicely formatted tree view of a variable
434          * @param var String is the varialbe to construct the output from
435          * @access private
436          */
437         function debug($var) {          
438                 $str_ = '<pre>';
439                 
440                 ob_start();
441                 print_r($var);
442                 $str = ob_get_contents();
443                 ob_end_clean();
444         
445                 $str = str_replace('<', '&lt;', $str);
446         
447                 $str = str_replace('[', '<span style="color: red; font-weight: bold;">[', $str);
448                 $str = str_replace(']', ']</span>', $str);
449                 $str = str_replace('=>', '<span style="color: blue; font-weight: bold;">=></span>', $str);
450                 $str = str_replace('Array', '<span style="color: purple; font-weight: bold;">Array</span>', $str);
451                 $str .= '</pre>';
452                 
453                 $str = $str_ . $str;
454                 return $str;
455         }
456         
457         /**
458          * Function which strips the path base off a file URL since it is a security risk
459          * @param String str is the path string
460          * @return String only the script filename where the error occured
461          */
462         function stripbase($str) {
463                 
464                 $to_root = $_SERVER["PATH_TRANSLATED"];
465                 
466                 $pos_last = strrpos($to_root, "/");
467                 $to_root = substr($to_root, $pos_last + 1);
468                 return $to_root;
469         }
470         
471         /**
472          * Print the error to the browser, dont use any templates or css sheets for flexibility
473          * @access private
474          */
475         function printError($str) {
476                 if (!AT_DEVEL) return;
477                 
478                 echo '<br />';
479                 echo '<table bgcolor="#FF0000" border="0" cellpadding="3" cellspacing="2" width="90%" summary="" align="center">';
480                 echo '<tr bgcolor="#FEF1F1" align="top">';
481                 echo '<td>';
482                 echo '<h3><span style="font-family: arial verdana">Internal Error Detected</span></small></h3>';
483                 echo '<ul>';
484                 echo '<li><small><span style="font-family: arial verdana">'. $str .'</span></small></li>';
485                 echo'</ul>';
486                 echo '</td>';
487                 echo '</tr>';
488                 echo '</table>';
489                 echo '<br />';
490         }
491         
492         /**
493          * Create restricted access logs dir
494          */
495         function makeLogDir() {
496                 
497                 $result = @mkdir(AT_CONTENT_DIR . 'logs', 0771); // r+w for owner               
498                 if ($result == 0) {
499                         $this->printError('Fatal. Could not create /content/logs. Please resolve');
500                 }
501         
502         }
503         
504         /**
505          * Determine wheter a log file exists for today
506          * @access private
507          */
508         function todayLogFileExists() {
509                 $today = getdate(); 
510
511                 $timestamp = $today['mon'] . '-' . $today['mday'] . '-' . $today['year'];
512                 
513                 return (is_file(AT_CONTENT_DIR . 'logs/' . $timestamp . '.log'));
514         }
515         
516         /**
517          * Run through $container and print all the errors on this page.
518          * Used to prevent errors from breaking content on the page
519          * @access public
520          */
521         function showErrors() {
522
523                 foreach($this->container as $elem) {
524                         $this->printError('<strong>ATutor has detected an Error<strong> - ' .
525                                                                                                                         $elem);
526                         unset($elem);
527                 }
528         }
529
530 ?>