call(function() { * ...include some files, do some work, etc... * }); * @endcode * * Note: Most of the code in this class is pretty generic vis-a-vis error * handling -- except for 'reportError', whose message format is only * appropriate for use with the CRM_Queue_Page_AJAX. Some kind of cleanup * will be necessary to get reuse from the other parts of this class. */ class CRM_Queue_ErrorPolicy { public $active; /** * @param null|int $level * PHP error level to capture (e.g. E_PARSE|E_USER_ERROR). */ public function __construct($level = NULL) { register_shutdown_function([$this, 'onShutdown']); if ($level === NULL) { $level = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR; } $this->level = $level; } /** * Enable the error policy. */ public function activate() { $this->active = TRUE; $this->backup = []; foreach ([ 'display_errors', 'html_errors', 'xmlrpc_errors', ] as $key) { $this->backup[$key] = ini_get($key); ini_set($key, 0); } set_error_handler([$this, 'onError'], $this->level); // FIXME make this temporary/reversible $this->errorScope = CRM_Core_TemporaryErrorScope::useException(); } /** * Disable the error policy. */ public function deactivate() { $this->errorScope = NULL; restore_error_handler(); foreach ([ 'display_errors', 'html_errors', 'xmlrpc_errors', ] as $key) { ini_set($key, $this->backup[$key]); } $this->active = FALSE; } /** * Execute the callable. Activate and deactivate the error policy * automatically. * * @param callable|array|string $callable * A callback function. * * @return mixed */ public function call($callable) { $this->activate(); try { $result = $callable(); } catch (Exception$e) { $this->reportException($e); } $this->deactivate(); return $result; } /** * Receive (semi) recoverable error notices. * * @see set_error_handler * * @param string $errno * @param string $errstr * @param string $errfile * @param int $errline * * @return bool * @throws \Exception */ public function onError($errno, $errstr, $errfile, $errline) { if (!(error_reporting() & $errno)) { return TRUE; } throw new Exception(sprintf('PHP Error %s at %s:%s: %s', $errno, $errfile, $errline, $errstr)); } /** * Receive non-recoverable error notices * * @see register_shutdown_function * @see error_get_last */ public function onShutdown() { if (!$this->active) { return; } $error = error_get_last(); if (is_array($error) && ($error['type'] & $this->level)) { $this->reportError($error); } } /** * Print a fatal error. * * @param array $error * The PHP error (with "type", "message", etc). */ public function reportError($error) { $response = [ 'is_error' => 1, 'is_continue' => 0, 'exception' => htmlentities(sprintf('Error %s: %s in %s, line %s', $error['type'], $error['message'], $error['file'], $error['line'])), ]; global $activeQueueRunner; if (is_object($activeQueueRunner)) { $response['last_task_title'] = $activeQueueRunner->lastTaskTitle; } CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportError', $response); echo json_encode($response); // civiExit() is unnecessary -- we're only called as part of abend } /** * Print an unhandled exception. * * @param Exception $e * The unhandled exception. */ public function reportException(Exception $e) { CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportException', CRM_Core_Error::formatTextException($e)); $response = [ 'is_error' => 1, 'is_continue' => 0, ]; $config = CRM_Core_Config::singleton(); if ($config->backtrace || CRM_Core_Config::isUpgradeMode()) { $response['exception'] = CRM_Core_Error::formatHtmlException($e); } else { $response['exception'] = htmlentities($e->getMessage()); } global $activeQueueRunner; if (is_object($activeQueueRunner)) { $response['last_task_title'] = $activeQueueRunner->lastTaskTitle; } CRM_Utils_JSON::output($response); } }