Fixes for PHP5.6 -> PHP7 upgrade
[libpress/php7-plugin-fixes.git] / plugins / all-in-one-event-calendar / lib / exception / handler.php
1 <?php
2
3 /**
4  * Handles exception and errors
5  *
6  * @author     Time.ly Network Inc.
7  * @since      2.0
8  *
9  * @package    AI1EC
10  * @subpackage AI1EC.Exception
11  */
12 class Ai1ec_Exception_Handler {
13
14         /**
15          * @var string The option for the messgae in the db
16          */
17         const DB_DEACTIVATE_MESSAGE = 'ai1ec_deactivate_message';
18
19         /**
20          * @var string The GET parameter to reactivate the plugin
21          */
22         const DB_REACTIVATE_PLUGIN  = 'ai1ec_reactivate_plugin';
23
24         /**
25          * @var callable|null Previously set exception handler if any
26          */
27         protected $_prev_ex_handler;
28
29         /**
30          * @var callable|null Previously set error handler if any
31          */
32         protected $_prev_er_handler;
33
34         /**
35          * @var string The name of the Exception class to handle
36          */
37         protected $_exception_class;
38
39         /**
40          * @var string The name of the ErrorException class to handle
41          */
42         protected $_error_exception_class;
43
44         /**
45          * @var string The message to display in the admin notice
46          */
47         protected $_message;
48
49         /**
50          * @var array Mapped list of errors that are non-fatal, to be ignored
51          *            in production.
52          */
53         protected $_nonfatal_errors = null;
54
55         /**
56          * Store exception handler that was previously set
57          *
58          * @param callable|null $_prev_ex_handler
59          *
60          * @return void Method does not return
61          */
62         public function set_prev_ex_handler( $prev_ex_handler ) {
63                 $this->_prev_ex_handler = $prev_ex_handler;
64         }
65
66         /**
67          * Store error handler that was previously set
68          *
69          * @param callable|null $_prev_er_handler
70          *
71          * @return void Method does not return
72          */
73         public function set_prev_er_handler( $prev_er_handler ) {
74                 $this->_prev_er_handler = $prev_er_handler;
75         }
76
77         /**
78          * Constructor accepts names of classes to be handled
79          *
80          * @param string $exception_class Name of exceptions base class to handle
81          * @param string $error_class     Name of errors base class to handle
82          *
83          * @return void Constructor newer returns
84          */
85         public function __construct( $exception_class, $error_class ) {
86                 $this->_exception_class       = $exception_class;
87                 $this->_error_exception_class = $error_class;
88                 $this->_nonfatal_errors       = array(
89                         E_USER_WARNING => true,
90                         E_WARNING      => true,
91                         E_USER_NOTICE  => true,
92                         E_NOTICE       => true,
93                         E_STRICT       => true,
94                 );
95                 if ( version_compare( PHP_VERSION, '5.3.0' ) >= 0 ) {
96                         // wrapper `constant( 'XXX' )` is used to avoid compile notices
97                         // on earlier PHP versions.
98                         $this->_nonfatal_errors[constant( 'E_DEPRECATED' )]      = true;
99                         $this->_nonfatal_errors[constant( 'E_USER_DEPRECATED') ] = true;
100                 }
101         }
102
103         /**
104          * Return add-on, which caused the exception or null if it was Core.
105          *
106          * Relies on `plugin_to_disable` method which may be implemented by
107          * an exception. If it returns non empty value - it is returned.
108          *
109          * @param Exception $exception Actual exception which was thrown.
110          *
111          * @return string|null Add-on identifier (plugin url), or null.
112          */
113         public function is_caused_by_addon( Exception $exception ) {
114                 $addon = null;
115                 if ( method_exists( $exception, 'plugin_to_disable' ) ) {
116                         $addon = $exception->plugin_to_disable();
117                         if ( empty( $addon ) ) {
118                                 $addon = null;
119                         }
120                 }
121                 if ( null === $addon ) {
122                         $position   = strlen( dirname( AI1EC_PATH ) ) + 1;
123                         $length     = strlen( AI1EC_PLUGIN_NAME );
124                         $trace_list = $exception->getTrace();
125                         array_unshift(
126                                 $trace_list,
127                                 array( 'file' => $exception->getFile() )
128                         );
129                         foreach ( $trace_list as $trace ) {
130                                 if (
131                                         ! isset( $trace['file'] ) ||
132                                         ! isset( $trace['file'][$position] )
133                                 ) {
134                                         continue;
135                                 }
136                                 $file = substr(
137                                         $trace['file'],
138                                         $position,
139                                         strpos( $trace['file'], '/', $position ) - $position
140                                 );
141                                 if ( 0 === strncmp( AI1EC_PLUGIN_NAME, $file, $length ) ) {
142                                         if ( AI1EC_PLUGIN_NAME !== $file ) {
143                                                 $addon = $file . '/' . $file . '.php';
144                                         }
145                                 }
146                         }
147                 }
148                 if ( 'core' === strtolower( $addon ) ) {
149                         return null;
150                 }
151                 return $addon;
152         }
153
154         /**
155          * Get tag-line for disabling.
156          *
157          * Extracts plugin name from file.
158          *
159          * @param string $addon Name of disabled add-on.
160          *
161          * @return string Message to display before full trace.
162          */
163         public function get_disabled_line( $addon ) {
164                 $file = dirname( AI1EC_PATH ) . DIRECTORY_SEPARATOR . $addon;
165                 $line = '';
166                 if (
167                         is_file( $file ) &&
168                         preg_match(
169                                 '|Plugin Name:\s*(.+)|',
170                                 file_get_contents( $file ),
171                                 $matches
172                         )
173                 ) {
174                         $line = '<p><strong>' .
175                                 sprintf(
176                                         __( 'The add-on "%s" has been disabled due to an error:' ),
177                                         __( trim( $matches[1] ), dirname( $addon ) )
178                                 ) .
179                                 '</strong></p>';
180                 }
181                 return $line;
182         }
183
184         /**
185          * Global exceptions handling method
186          *
187          * @param Exception $exception Previously thrown exception to handle
188          *
189          * @return void Exception handler is not expected to return
190          */
191         public function handle_exception( $exception ) {
192                 if( $exception instanceof \Error ) {
193                         throw new \Exception( 'Exception re-throw', 0, $exception );
194                 }
195                 if ( defined( 'AI1EC_DEBUG' ) && true === AI1EC_DEBUG ) {
196                         echo '<pre>';
197                         $this->var_debug( $exception );
198                         echo '</pre>';
199                         die();
200                 }
201                 // if it's something we handle, handle it
202                 $backtrace = $this->_get_backtrace( $exception );
203                 if ( $exception instanceof $this->_exception_class ) {
204                         // check if it's a plugin instead of core
205                         $disable_addon = $this->is_caused_by_addon( $exception );
206                         $message       = method_exists( $exception, 'get_html_message' )
207                                 ? $exception->get_html_message()
208                                 : $exception->getMessage();
209                         $message = '<p>' . $message . '</p>';
210                         if ( $exception->display_backtrace() ) {
211                                 $message .= $backtrace;
212                         }
213                         if ( null !== $disable_addon ) {
214                                 include_once ABSPATH . 'wp-admin/includes/plugin.php';
215                                 // deactivate the plugin. Fire handlers to hide options.
216                                 deactivate_plugins( $disable_addon );
217                                 global $ai1ec_registry;
218                                 $ai1ec_registry->get( 'notification.admin' )
219                                         ->store(
220                                                 $this->get_disabled_line( $disable_addon ) . $message,
221                                                 'error',
222                                                 2,
223                                                 array( Ai1ec_Notification_Admin::RCPT_ADMIN ),
224                                                 true
225                                         );
226                                 $this->redirect( $exception->get_redirect_url() );
227                         } else {
228                                 // check if it has a methof for deatiled html
229                                 $this->soft_deactivate_plugin( $message );
230                         }
231
232                 }
233                 // if it's a PHP error in our plugin files, deactivate and redirect
234                 else if ( $exception instanceof $this->_error_exception_class ) {
235                         $this->soft_deactivate_plugin(
236                                 $exception->getMessage() . $backtrace
237                         );
238                 }
239                 // if another handler was set, let it handle the exception
240                 if ( is_callable( $this->_prev_ex_handler ) ) {
241                         call_user_func( $this->_prev_ex_handler, $exception );
242                 }
243         }
244
245         /**
246          * Throws an Ai1ec_Error_Exception if the error comes from our plugin
247          *
248          * @param int    $errno      Error level as integer
249          * @param string $errstr     Error message raised
250          * @param string $errfile    File in which error was raised
251          * @param string $errline    Line in which error was raised
252          * @param array  $errcontext Error context symbols table copy
253          *
254          * @throws Ai1ec_Error_Exception If error originates from within Ai1EC
255          *
256          * @return boolean|void Nothing when error is ours, false when no
257          *                      other handler exists
258          */
259         public function handle_error(
260                 $errno,
261                 $errstr,
262                 $errfile,
263                 $errline,
264                 $errcontext = array()
265         ) {
266                 // if the error is not in our plugin, let PHP handle things.
267                 $position = strpos( $errfile, AI1EC_PLUGIN_NAME );
268                 if ( false === $position ) {
269                         if ( is_callable( $this->_prev_er_handler ) ) {
270                                 return call_user_func_array(
271                                         $this->_prev_er_handler,
272                                         func_get_args()
273                                 );
274                         }
275                         return false;
276                 }
277                 // do not disable plugin in production if the error is rather low
278                 if (
279                         isset( $this->_nonfatal_errors[$errno] ) && (
280                                 ! defined( 'AI1EC_DEBUG' ) || false === AI1EC_DEBUG
281                         )
282                 ) {
283                         $message = sprintf(
284                                 'All-in-One Event Calendar: %s @ %s:%d #%d',
285                                 $errstr,
286                                 $errfile,
287                                 $errline,
288                                 $errno
289                         );
290                         return error_log( $message, 0 );
291                 }
292                 // let's get the plugin folder
293                 $tail = substr( $errfile, $position );
294                 $exploded = explode( DIRECTORY_SEPARATOR, $tail );
295                 $plugin_dir = $exploded[0];
296                 // if the error doesn't belong to core, throw the plugin exception to trigger disabling
297                 // of the plugin in the exception handler
298                 if ( AI1EC_PLUGIN_NAME !== $plugin_dir ) {
299                         $exc = implode(
300                                 array_map(
301                                         array( $this, 'return_first_char' ),
302                                         explode( '-', $plugin_dir )
303                                 )
304                         );
305                         // all plugins should implement an exception based on this convention
306                         // which is the same convention we use for constants, only with just first letter uppercase
307                         $exc = str_replace( 'aioec', 'Ai1ec', $exc ) . '_Exception';
308                         if ( class_exists( $exc ) ) {
309                                 $message = sprintf(
310                                         'All-in-One Event Calendar: %s @ %s:%d #%d',
311                                         $errstr,
312                                         $errfile,
313                                         $errline,
314                                         $errno
315                                 );
316                                 throw new $exc( $message );
317                         }
318                 }
319                 throw new Ai1ec_Error_Exception(
320                         $errstr,
321                         $errno,
322                         0,
323                         $errfile,
324                         $errline
325                 );
326         }
327
328         public function return_first_char( $name ) {
329                 return $name[0];
330         }
331         /**
332          * Perform what's needed to deactivate the plugin softly
333          *
334          * @param string $message Error message to be displayed to admin
335          *
336          * @return void Method does not return
337          */
338         protected function soft_deactivate_plugin( $message ) {
339                 add_option( self::DB_DEACTIVATE_MESSAGE, $message );
340                 $this->redirect();
341         }
342
343         /**
344          * Perform what's needed to reactivate the plugin
345          *
346          * @return boolean Success
347          */
348         public function reactivate_plugin() {
349                 return delete_option( self::DB_DEACTIVATE_MESSAGE );
350         }
351
352         /**
353          * Get message to be displayed to admin if any
354          *
355          * @return string|boolean Error message or false if plugin is not disabled
356          */
357         public function get_disabled_message() {
358                 global $wpdb;
359                 $row = $wpdb->get_row(
360                         $wpdb->prepare(
361                                 "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
362                                 self::DB_DEACTIVATE_MESSAGE
363                         )
364                 );
365                 if ( is_object( $row ) ) {
366                         return $row->option_value;
367                 } else { // option does not exist, so we must cache its non-existence
368                         return false;
369                 }
370         }
371
372         /**
373          * Add an admin notice
374          *
375          * @param string $message Message to be displayed to admin
376          *
377          * @return void Method does not return
378          */
379         public function show_notices( $message ) {
380                 // save the message to use it later
381                 $this->_message = $message;
382                 add_action( 'admin_notices', array( $this, 'render_admin_notice' ) );
383         }
384
385         /**
386          * Render HTML snipped to be displayd as a notice to admin
387          *
388          * @hook admin_notices When plugin is soft-disabled
389          *
390          * @return void Method does not return
391          */
392         public function render_admin_notice() {
393                 $redirect_url = esc_url( add_query_arg(
394                         self::DB_REACTIVATE_PLUGIN,
395                         'true',
396                         get_admin_url()
397                 ) );
398                 $label = __(
399                         'All-in-One Event Calendar has been disabled due to an error:',
400                         AI1EC_PLUGIN_NAME
401                 );
402                 $message  = '<div class="message error">';
403                 $message .= '<p><strong>' . $label . '</strong></p>';
404                 $message .= $this->_message;
405                 $message .= ' <a href="' . $redirect_url .
406                         '" class="button button-primary ai1ec-dismissable">' .
407                         __(
408                                 'Try reactivating plugin',
409                                 AI1EC_PLUGIN_NAME
410                         );
411                 $message .= '</a>';
412                 $message .= '<p></p></div>';
413                 echo $message;
414         }
415
416         /**
417          * Redirect the user either to the front page or the dashbord page
418          *
419          * @return void Method does not return
420          */
421         protected function redirect( $suggested_url = null ) {
422                 $url = ai1ec_get_site_url();
423                 if ( is_admin() ) {
424                         $url = null !== $suggested_url
425                                 ? $suggested_url
426                                 : ai1ec_get_admin_url();
427                 }
428                 Ai1ec_Http_Response_Helper::redirect( $url );
429         }
430         /**
431          * Had to add it as var_dump was locking my browser.
432          *
433          * Taken from http://www.leaseweblabs.com/2013/10/smart-alternative-phps-var_dump-function/
434          *
435          * @param mixed $variable
436          * @param int $strlen
437          * @param int $width
438          * @param int $depth
439          * @param int $i
440          * @param array $objects
441          *
442          * @return string
443          */
444         public function var_debug(
445                 $variable,
446                 $strlen = 400,
447                 $width = 25,
448                 $depth = 10,
449                 $i = 0,
450                 &$objects = array()
451         ) {
452                 $search  = array( "\0", "\a", "\b", "\f", "\n", "\r", "\t", "\v" );
453                 $replace = array( '\0', '\a', '\b', '\f', '\n', '\r', '\t', '\v' );
454                 $string  = '';
455
456                 switch ( gettype( $variable ) ) {
457                         case 'boolean' :
458                                 $string .= $variable ? 'true' : 'false';
459                                 break;
460                         case 'integer' :
461                                 $string .= $variable;
462                                 break;
463                         case 'double' :
464                                 $string .= $variable;
465                                 break;
466                         case 'resource' :
467                                 $string .= '[resource]';
468                                 break;
469                         case 'NULL' :
470                                 $string .= "null";
471                                 break;
472                         case 'unknown type' :
473                                 $string .= '???';
474                                 break;
475                         case 'string' :
476                                 $len = strlen( $variable );
477                                 $variable = str_replace(
478                                         $search,
479                                         $replace,
480                                         substr( $variable, 0, $strlen ),
481                                         $count );
482                                 $variable = substr( $variable, 0, $strlen );
483                                 if ( $len < $strlen ) {
484                                         $string .= '"' . $variable . '"';
485                                 } else {
486                                         $string .= 'string(' . $len . '): "' . $variable . '"...';
487                                 }
488                                 break;
489                         case 'array' :
490                                 $len = count( $variable );
491                                 if ( $i == $depth ) {
492                                         $string .= 'array(' . $len . ') {...}';
493                                 } elseif ( ! $len) {
494                                         $string .= 'array(0) {}';
495                                 } else {
496                                         $keys    = array_keys( $variable );
497                                         $spaces  = str_repeat( ' ', $i * 2 );
498                                         $string .= "array($len)\n" . $spaces . '{';
499                                         $count   = 0;
500                                         foreach ( $keys as $key ) {
501                                                 if ( $count == $width ) {
502                                                         $string .= "\n" . $spaces . "  ...";
503                                                         break;
504                                                 }
505                                                 $string .= "\n" . $spaces . "  [$key] => ";
506                                                 $string .= $this->var_debug(
507                                                         $variable[$key],
508                                                         $strlen,
509                                                         $width,
510                                                         $depth,
511                                                         $i + 1,
512                                                         $objects
513                                                 );
514                                                 $count ++;
515                                         }
516                                         $string .= "\n" . $spaces . '}';
517                                 }
518                                 break;
519                         case 'object':
520                                 $id = array_search( $variable, $objects, true );
521                                 if ( $id !== false ) {
522                                         $string .= get_class( $variable ) . '#' . ( $id + 1 ) . ' {...}';
523                                 } else if ( $i == $depth ) {
524                                         $string .= get_class( $variable ) . ' {...}';
525                                 } else {
526                                         $id = array_push( $objects, $variable );
527                                         $array = ( array ) $variable;
528                                         $spaces = str_repeat( ' ', $i * 2 );
529                                         $string .= get_class( $variable ) . "#$id\n" . $spaces . '{';
530                                         $properties = array_keys( $array );
531                                         foreach ( $properties as $property ) {
532                                                 $name    = str_replace( "\0", ':', trim( $property ) );
533                                                 $string .= "\n" . $spaces . "  [$name] => ";
534                                                 $string .= $this->var_debug(
535                                                         $array[$property],
536                                                         $strlen,
537                                                         $width,
538                                                         $depth,
539                                                         $i + 1,
540                                                         $objects
541                                                 );
542                                         }
543                                         $string .= "\n" . $spaces . '}';
544                                 }
545                                 break;
546                 }
547
548                 if ( $i > 0 ) {
549                         return $string;
550                 }
551
552                 $backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
553                 do {
554                         $caller = array_shift( $backtrace );
555                 } while (
556                         $caller &&
557                         ! isset( $caller['file'] )
558                 );
559                 if ( $caller ) {
560                         $string = $caller['file'] . ':' . $caller['line'] . "\n" . $string;
561                 }
562
563                 echo nl2br( str_replace( ' ', '&nbsp;', htmlentities( $string ) ) );
564         }
565
566         /**
567          * Get HTML code with backtrace information for given exception.
568          *
569          * @param Exception $exception
570          *
571          * @return string HTML code.
572          */
573         protected function _get_backtrace( Exception $exception ) {
574                 $backtrace = '';
575                 $trace     = nl2br( $exception->getTraceAsString() );
576                 $ident     = sha1( $trace );
577                 if ( ! empty( $trace ) ) {
578                         $request_uri = '';
579                         if ( isset( $_SERVER['REQUEST_URI'] ) ) {
580                                 // Remove all whitespaces
581                                 $request_uri = preg_replace( '/\s+/', '', $_SERVER['REQUEST_URI'] );
582                                 // Convert request URI and strip tags
583                                 $request_uri  = strip_tags( htmlspecialchars_decode( $request_uri ) );
584                                 // Limit URL to 100 characters
585                                 $request_uri = substr($request_uri, 0, 100);
586                         }
587                         $button_label = __( 'Toggle error details', AI1EC_PLUGIN_NAME );
588                         $title        = __( 'Error Details:', AI1EC_PLUGIN_NAME );
589                         $backtrace    = <<<JAVASCRIPT
590                         <script type="text/javascript">
591                         jQuery( function($) {
592                                 $( "a[data-rel='$ident']" ).click( function() {
593                                         jQuery( "#ai1ec-error-$ident" ).slideToggle( "fast" );
594                                         return false;
595                                 });
596                         });
597                         </script>
598                         <blockquote id="ai1ec-error-$ident" style="display: none;">
599                                 <strong>$title</strong>
600                                 <p>$trace</p>
601                                 <p>Request Uri: $request_uri</p>
602                         </blockquote>
603                         <a href="#" data-rel="$ident" class="button">$button_label</a>
604 JAVASCRIPT;
605                 }
606                 return $backtrace;
607         }
608
609 }