606 lines
20 KiB
PHP
606 lines
20 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Handles exception and errors
|
|
*
|
|
* @author Time.ly Network Inc.
|
|
* @since 2.0
|
|
*
|
|
* @package AI1EC
|
|
* @subpackage AI1EC.Exception
|
|
*/
|
|
class Ai1ec_Exception_Handler {
|
|
|
|
/**
|
|
* @var string The option for the messgae in the db
|
|
*/
|
|
const DB_DEACTIVATE_MESSAGE = 'ai1ec_deactivate_message';
|
|
|
|
/**
|
|
* @var string The GET parameter to reactivate the plugin
|
|
*/
|
|
const DB_REACTIVATE_PLUGIN = 'ai1ec_reactivate_plugin';
|
|
|
|
/**
|
|
* @var callable|null Previously set exception handler if any
|
|
*/
|
|
protected $_prev_ex_handler;
|
|
|
|
/**
|
|
* @var callable|null Previously set error handler if any
|
|
*/
|
|
protected $_prev_er_handler;
|
|
|
|
/**
|
|
* @var string The name of the Exception class to handle
|
|
*/
|
|
protected $_exception_class;
|
|
|
|
/**
|
|
* @var string The name of the ErrorException class to handle
|
|
*/
|
|
protected $_error_exception_class;
|
|
|
|
/**
|
|
* @var string The message to display in the admin notice
|
|
*/
|
|
protected $_message;
|
|
|
|
/**
|
|
* @var array Mapped list of errors that are non-fatal, to be ignored
|
|
* in production.
|
|
*/
|
|
protected $_nonfatal_errors = null;
|
|
|
|
/**
|
|
* Store exception handler that was previously set
|
|
*
|
|
* @param callable|null $_prev_ex_handler
|
|
*
|
|
* @return void Method does not return
|
|
*/
|
|
public function set_prev_ex_handler( $prev_ex_handler ) {
|
|
$this->_prev_ex_handler = $prev_ex_handler;
|
|
}
|
|
|
|
/**
|
|
* Store error handler that was previously set
|
|
*
|
|
* @param callable|null $_prev_er_handler
|
|
*
|
|
* @return void Method does not return
|
|
*/
|
|
public function set_prev_er_handler( $prev_er_handler ) {
|
|
$this->_prev_er_handler = $prev_er_handler;
|
|
}
|
|
|
|
/**
|
|
* Constructor accepts names of classes to be handled
|
|
*
|
|
* @param string $exception_class Name of exceptions base class to handle
|
|
* @param string $error_class Name of errors base class to handle
|
|
*
|
|
* @return void Constructor newer returns
|
|
*/
|
|
public function __construct( $exception_class, $error_class ) {
|
|
$this->_exception_class = $exception_class;
|
|
$this->_error_exception_class = $error_class;
|
|
$this->_nonfatal_errors = array(
|
|
E_USER_WARNING => true,
|
|
E_WARNING => true,
|
|
E_USER_NOTICE => true,
|
|
E_NOTICE => true,
|
|
E_STRICT => true,
|
|
);
|
|
if ( version_compare( PHP_VERSION, '5.3.0' ) >= 0 ) {
|
|
// wrapper `constant( 'XXX' )` is used to avoid compile notices
|
|
// on earlier PHP versions.
|
|
$this->_nonfatal_errors[constant( 'E_DEPRECATED' )] = true;
|
|
$this->_nonfatal_errors[constant( 'E_USER_DEPRECATED') ] = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return add-on, which caused the exception or null if it was Core.
|
|
*
|
|
* Relies on `plugin_to_disable` method which may be implemented by
|
|
* an exception. If it returns non empty value - it is returned.
|
|
*
|
|
* @param Exception $exception Actual exception which was thrown.
|
|
*
|
|
* @return string|null Add-on identifier (plugin url), or null.
|
|
*/
|
|
public function is_caused_by_addon( $exception ) {
|
|
$addon = null;
|
|
if ( method_exists( $exception, 'plugin_to_disable' ) ) {
|
|
$addon = $exception->plugin_to_disable();
|
|
if ( empty( $addon ) ) {
|
|
$addon = null;
|
|
}
|
|
}
|
|
if ( null === $addon ) {
|
|
$position = strlen( dirname( AI1EC_PATH ) ) + 1;
|
|
$length = strlen( AI1EC_PLUGIN_NAME );
|
|
$trace_list = $exception->getTrace();
|
|
array_unshift(
|
|
$trace_list,
|
|
array( 'file' => $exception->getFile() )
|
|
);
|
|
foreach ( $trace_list as $trace ) {
|
|
if (
|
|
! isset( $trace['file'] ) ||
|
|
! isset( $trace['file'][$position] )
|
|
) {
|
|
continue;
|
|
}
|
|
$file = substr(
|
|
$trace['file'],
|
|
$position,
|
|
strpos( $trace['file'], '/', $position ) - $position
|
|
);
|
|
if ( 0 === strncmp( AI1EC_PLUGIN_NAME, $file, $length ) ) {
|
|
if ( AI1EC_PLUGIN_NAME !== $file ) {
|
|
$addon = $file . '/' . $file . '.php';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( 'core' === strtolower( $addon ) ) {
|
|
return null;
|
|
}
|
|
return $addon;
|
|
}
|
|
|
|
/**
|
|
* Get tag-line for disabling.
|
|
*
|
|
* Extracts plugin name from file.
|
|
*
|
|
* @param string $addon Name of disabled add-on.
|
|
*
|
|
* @return string Message to display before full trace.
|
|
*/
|
|
public function get_disabled_line( $addon ) {
|
|
$file = dirname( AI1EC_PATH ) . DIRECTORY_SEPARATOR . $addon;
|
|
$line = '';
|
|
if (
|
|
is_file( $file ) &&
|
|
preg_match(
|
|
'|Plugin Name:\s*(.+)|',
|
|
file_get_contents( $file ),
|
|
$matches
|
|
)
|
|
) {
|
|
$line = '<p><strong>' .
|
|
sprintf(
|
|
__( 'The add-on "%s" has been disabled due to an error:' ),
|
|
__( trim( $matches[1] ), dirname( $addon ) )
|
|
) .
|
|
'</strong></p>';
|
|
}
|
|
return $line;
|
|
}
|
|
|
|
/**
|
|
* Global exceptions handling method
|
|
*
|
|
* @param Exception $exception Previously thrown exception to handle
|
|
*
|
|
* @return void Exception handler is not expected to return
|
|
*/
|
|
public function handle_exception( $exception ) {
|
|
if ( defined( 'AI1EC_DEBUG' ) && true === AI1EC_DEBUG ) {
|
|
echo '<pre>';
|
|
$this->var_debug( $exception );
|
|
echo '</pre>';
|
|
die();
|
|
}
|
|
// if it's something we handle, handle it
|
|
$backtrace = $this->_get_backtrace( $exception );
|
|
if ( $exception instanceof $this->_exception_class ) {
|
|
// check if it's a plugin instead of core
|
|
$disable_addon = $this->is_caused_by_addon( $exception );
|
|
$message = method_exists( $exception, 'get_html_message' )
|
|
? $exception->get_html_message()
|
|
: $exception->getMessage();
|
|
$message = '<p>' . $message . '</p>';
|
|
if ( $exception->display_backtrace() ) {
|
|
$message .= $backtrace;
|
|
}
|
|
if ( null !== $disable_addon ) {
|
|
include_once ABSPATH . 'wp-admin/includes/plugin.php';
|
|
// deactivate the plugin. Fire handlers to hide options.
|
|
deactivate_plugins( $disable_addon );
|
|
global $ai1ec_registry;
|
|
$ai1ec_registry->get( 'notification.admin' )
|
|
->store(
|
|
$this->get_disabled_line( $disable_addon ) . $message,
|
|
'error',
|
|
2,
|
|
array( Ai1ec_Notification_Admin::RCPT_ADMIN ),
|
|
true
|
|
);
|
|
$this->redirect( $exception->get_redirect_url() );
|
|
} else {
|
|
// check if it has a methof for deatiled html
|
|
$this->soft_deactivate_plugin( $message );
|
|
}
|
|
|
|
}
|
|
// if it's a PHP error in our plugin files, deactivate and redirect
|
|
else if ( $exception instanceof $this->_error_exception_class ) {
|
|
$this->soft_deactivate_plugin(
|
|
$exception->getMessage() . $backtrace
|
|
);
|
|
}
|
|
// if another handler was set, let it handle the exception
|
|
if ( is_callable( $this->_prev_ex_handler ) ) {
|
|
call_user_func( $this->_prev_ex_handler, $exception );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Throws an Ai1ec_Error_Exception if the error comes from our plugin
|
|
*
|
|
* @param int $errno Error level as integer
|
|
* @param string $errstr Error message raised
|
|
* @param string $errfile File in which error was raised
|
|
* @param string $errline Line in which error was raised
|
|
* @param array $errcontext Error context symbols table copy
|
|
*
|
|
* @throws Ai1ec_Error_Exception If error originates from within Ai1EC
|
|
*
|
|
* @return boolean|void Nothing when error is ours, false when no
|
|
* other handler exists
|
|
*/
|
|
public function handle_error(
|
|
$errno,
|
|
$errstr,
|
|
$errfile,
|
|
$errline,
|
|
$errcontext = array()
|
|
) {
|
|
// if the error is not in our plugin, let PHP handle things.
|
|
$position = strpos( $errfile, AI1EC_PLUGIN_NAME );
|
|
if ( false === $position ) {
|
|
if ( is_callable( $this->_prev_er_handler ) ) {
|
|
return call_user_func_array(
|
|
$this->_prev_er_handler,
|
|
func_get_args()
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
// do not disable plugin in production if the error is rather low
|
|
if (
|
|
isset( $this->_nonfatal_errors[$errno] ) && (
|
|
! defined( 'AI1EC_DEBUG' ) || false === AI1EC_DEBUG
|
|
)
|
|
) {
|
|
$message = sprintf(
|
|
'All-in-One Event Calendar: %s @ %s:%d #%d',
|
|
$errstr,
|
|
$errfile,
|
|
$errline,
|
|
$errno
|
|
);
|
|
return error_log( $message, 0 );
|
|
}
|
|
// let's get the plugin folder
|
|
$tail = substr( $errfile, $position );
|
|
$exploded = explode( DIRECTORY_SEPARATOR, $tail );
|
|
$plugin_dir = $exploded[0];
|
|
// if the error doesn't belong to core, throw the plugin exception to trigger disabling
|
|
// of the plugin in the exception handler
|
|
if ( AI1EC_PLUGIN_NAME !== $plugin_dir ) {
|
|
$exc = implode(
|
|
array_map(
|
|
array( $this, 'return_first_char' ),
|
|
explode( '-', $plugin_dir )
|
|
)
|
|
);
|
|
// all plugins should implement an exception based on this convention
|
|
// which is the same convention we use for constants, only with just first letter uppercase
|
|
$exc = str_replace( 'aioec', 'Ai1ec', $exc ) . '_Exception';
|
|
if ( class_exists( $exc ) ) {
|
|
$message = sprintf(
|
|
'All-in-One Event Calendar: %s @ %s:%d #%d',
|
|
$errstr,
|
|
$errfile,
|
|
$errline,
|
|
$errno
|
|
);
|
|
throw new $exc( $message );
|
|
}
|
|
}
|
|
throw new Ai1ec_Error_Exception(
|
|
$errstr,
|
|
$errno,
|
|
0,
|
|
$errfile,
|
|
$errline
|
|
);
|
|
}
|
|
|
|
public function return_first_char( $name ) {
|
|
return $name[0];
|
|
}
|
|
/**
|
|
* Perform what's needed to deactivate the plugin softly
|
|
*
|
|
* @param string $message Error message to be displayed to admin
|
|
*
|
|
* @return void Method does not return
|
|
*/
|
|
protected function soft_deactivate_plugin( $message ) {
|
|
add_option( self::DB_DEACTIVATE_MESSAGE, $message );
|
|
$this->redirect();
|
|
}
|
|
|
|
/**
|
|
* Perform what's needed to reactivate the plugin
|
|
*
|
|
* @return boolean Success
|
|
*/
|
|
public function reactivate_plugin() {
|
|
return delete_option( self::DB_DEACTIVATE_MESSAGE );
|
|
}
|
|
|
|
/**
|
|
* Get message to be displayed to admin if any
|
|
*
|
|
* @return string|boolean Error message or false if plugin is not disabled
|
|
*/
|
|
public function get_disabled_message() {
|
|
global $wpdb;
|
|
$row = $wpdb->get_row(
|
|
$wpdb->prepare(
|
|
"SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1",
|
|
self::DB_DEACTIVATE_MESSAGE
|
|
)
|
|
);
|
|
if ( is_object( $row ) ) {
|
|
return $row->option_value;
|
|
} else { // option does not exist, so we must cache its non-existence
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add an admin notice
|
|
*
|
|
* @param string $message Message to be displayed to admin
|
|
*
|
|
* @return void Method does not return
|
|
*/
|
|
public function show_notices( $message ) {
|
|
// save the message to use it later
|
|
$this->_message = $message;
|
|
add_action( 'admin_notices', array( $this, 'render_admin_notice' ) );
|
|
}
|
|
|
|
/**
|
|
* Render HTML snipped to be displayed as a notice to admin
|
|
*
|
|
* @hook admin_notices When plugin is soft-disabled
|
|
*
|
|
* @return void Method does not return
|
|
*/
|
|
public function render_admin_notice() {
|
|
$redirect_url = esc_url( add_query_arg(
|
|
self::DB_REACTIVATE_PLUGIN,
|
|
'true',
|
|
get_admin_url()
|
|
) );
|
|
$label = __(
|
|
'All-in-One Event Calendar has been disabled due to an error:',
|
|
AI1EC_PLUGIN_NAME
|
|
);
|
|
$message = '<div class="message error">';
|
|
$message .= '<p><strong>' . $label . '</strong></p>';
|
|
$message .= $this->_message;
|
|
$message .= ' <a href="' . $redirect_url .
|
|
'" class="button button-primary ai1ec-dismissable">' .
|
|
__(
|
|
'Try reactivating plugin',
|
|
AI1EC_PLUGIN_NAME
|
|
);
|
|
$message .= '</a>';
|
|
$message .= '<p></p></div>';
|
|
echo $message;
|
|
}
|
|
|
|
/**
|
|
* Redirect the user either to the front page or the dashbord page
|
|
*
|
|
* @return void Method does not return
|
|
*/
|
|
protected function redirect( $suggested_url = null ) {
|
|
$url = ai1ec_get_site_url();
|
|
if ( is_admin() ) {
|
|
$url = null !== $suggested_url
|
|
? $suggested_url
|
|
: ai1ec_get_admin_url();
|
|
}
|
|
Ai1ec_Http_Response_Helper::redirect( $url );
|
|
}
|
|
/**
|
|
* Had to add it as var_dump was locking my browser.
|
|
*
|
|
* Taken from http://www.leaseweblabs.com/2013/10/smart-alternative-phps-var_dump-function/
|
|
*
|
|
* @param mixed $variable
|
|
* @param int $strlen
|
|
* @param int $width
|
|
* @param int $depth
|
|
* @param int $i
|
|
* @param array $objects
|
|
*
|
|
* @return string
|
|
*/
|
|
public function var_debug(
|
|
$variable,
|
|
$strlen = 400,
|
|
$width = 25,
|
|
$depth = 10,
|
|
$i = 0,
|
|
&$objects = array()
|
|
) {
|
|
$search = array( "\0", "\a", "\b", "\f", "\n", "\r", "\t", "\v" );
|
|
$replace = array( '\0', '\a', '\b', '\f', '\n', '\r', '\t', '\v' );
|
|
$string = '';
|
|
|
|
switch ( gettype( $variable ) ) {
|
|
case 'boolean' :
|
|
$string .= $variable ? 'true' : 'false';
|
|
break;
|
|
case 'integer' :
|
|
$string .= $variable;
|
|
break;
|
|
case 'double' :
|
|
$string .= $variable;
|
|
break;
|
|
case 'resource' :
|
|
$string .= '[resource]';
|
|
break;
|
|
case 'NULL' :
|
|
$string .= "null";
|
|
break;
|
|
case 'unknown type' :
|
|
$string .= '???';
|
|
break;
|
|
case 'string' :
|
|
$len = strlen( $variable );
|
|
$variable = str_replace(
|
|
$search,
|
|
$replace,
|
|
substr( $variable, 0, $strlen ),
|
|
$count );
|
|
$variable = substr( $variable, 0, $strlen );
|
|
if ( $len < $strlen ) {
|
|
$string .= '"' . $variable . '"';
|
|
} else {
|
|
$string .= 'string(' . $len . '): "' . $variable . '"...';
|
|
}
|
|
break;
|
|
case 'array' :
|
|
$len = count( $variable );
|
|
if ( $i == $depth ) {
|
|
$string .= 'array(' . $len . ') {...}';
|
|
} elseif ( ! $len) {
|
|
$string .= 'array(0) {}';
|
|
} else {
|
|
$keys = array_keys( $variable );
|
|
$spaces = str_repeat( ' ', $i * 2 );
|
|
$string .= "array($len)\n" . $spaces . '{';
|
|
$count = 0;
|
|
foreach ( $keys as $key ) {
|
|
if ( $count == $width ) {
|
|
$string .= "\n" . $spaces . " ...";
|
|
break;
|
|
}
|
|
$string .= "\n" . $spaces . " [$key] => ";
|
|
$string .= $this->var_debug(
|
|
$variable[$key],
|
|
$strlen,
|
|
$width,
|
|
$depth,
|
|
$i + 1,
|
|
$objects
|
|
);
|
|
$count ++;
|
|
}
|
|
$string .= "\n" . $spaces . '}';
|
|
}
|
|
break;
|
|
case 'object':
|
|
$id = array_search( $variable, $objects, true );
|
|
if ( $id !== false ) {
|
|
$string .= get_class( $variable ) . '#' . ( $id + 1 ) . ' {...}';
|
|
} else if ( $i == $depth ) {
|
|
$string .= get_class( $variable ) . ' {...}';
|
|
} else {
|
|
$id = array_push( $objects, $variable );
|
|
$array = ( array ) $variable;
|
|
$spaces = str_repeat( ' ', $i * 2 );
|
|
$string .= get_class( $variable ) . "#$id\n" . $spaces . '{';
|
|
$properties = array_keys( $array );
|
|
foreach ( $properties as $property ) {
|
|
$name = str_replace( "\0", ':', trim( $property ) );
|
|
$string .= "\n" . $spaces . " [$name] => ";
|
|
$string .= $this->var_debug(
|
|
$array[$property],
|
|
$strlen,
|
|
$width,
|
|
$depth,
|
|
$i + 1,
|
|
$objects
|
|
);
|
|
}
|
|
$string .= "\n" . $spaces . '}';
|
|
}
|
|
break;
|
|
}
|
|
|
|
if ( $i > 0 ) {
|
|
return $string;
|
|
}
|
|
|
|
$backtrace = debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS );
|
|
do {
|
|
$caller = array_shift( $backtrace );
|
|
} while (
|
|
$caller &&
|
|
! isset( $caller['file'] )
|
|
);
|
|
if ( $caller ) {
|
|
$string = $caller['file'] . ':' . $caller['line'] . "\n" . $string;
|
|
}
|
|
|
|
echo nl2br( str_replace( ' ', ' ', htmlentities( $string ) ) );
|
|
}
|
|
|
|
/**
|
|
* Get HTML code with backtrace information for given exception.
|
|
*
|
|
* @param Exception $exception
|
|
*
|
|
* @return string HTML code.
|
|
*/
|
|
protected function _get_backtrace( $exception ) {
|
|
$backtrace = '';
|
|
$trace = nl2br( $exception->getTraceAsString() );
|
|
$ident = sha1( $trace );
|
|
if ( ! empty( $trace ) ) {
|
|
$request_uri = '';
|
|
if ( isset( $_SERVER['REQUEST_URI'] ) ) {
|
|
// Remove all whitespaces
|
|
$request_uri = preg_replace( '/\s+/', '', $_SERVER['REQUEST_URI'] );
|
|
// Convert request URI and strip tags
|
|
$request_uri = strip_tags( htmlspecialchars_decode( $request_uri ) );
|
|
// Limit URL to 100 characters
|
|
$request_uri = substr($request_uri, 0, 100);
|
|
}
|
|
$button_label = __( 'Toggle error details', AI1EC_PLUGIN_NAME );
|
|
$title = __( 'Error Details:', AI1EC_PLUGIN_NAME );
|
|
$backtrace = <<<JAVASCRIPT
|
|
<script type="text/javascript">
|
|
jQuery( function($) {
|
|
$( "a[data-rel='$ident']" ).click( function() {
|
|
jQuery( "#ai1ec-error-$ident" ).slideToggle( "fast" );
|
|
return false;
|
|
});
|
|
});
|
|
</script>
|
|
<blockquote id="ai1ec-error-$ident" style="display: none;">
|
|
<strong>$title</strong>
|
|
<p>$trace</p>
|
|
<p>Request Uri: $request_uri</p>
|
|
</blockquote>
|
|
<a href="#" data-rel="$ident" class="button">$button_label</a>
|
|
JAVASCRIPT;
|
|
}
|
|
return $backtrace;
|
|
}
|
|
|
|
}
|