Merge pull request #15817 from colemanw/Fix
[civicrm-core.git] / CRM / Queue / ErrorPolicy.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
5 | |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
10 */
11
12 /**
13 * To ensure that PHP errors or unhandled exceptions are reported in JSON
14 * format, wrap this around your code. For example:
15 *
16 * @code
17 * $errorContainer = new CRM_Queue_ErrorPolicy();
18 * $errorContainer->call(function() {
19 * ...include some files, do some work, etc...
20 * });
21 * @endcode
22 *
23 * Note: Most of the code in this class is pretty generic vis-a-vis error
24 * handling -- except for 'reportError', whose message format is only
25 * appropriate for use with the CRM_Queue_Page_AJAX. Some kind of cleanup
26 * will be necessary to get reuse from the other parts of this class.
27 */
28 class CRM_Queue_ErrorPolicy {
29 public $active;
30
31 /**
32 * @param null|int $level
33 * PHP error level to capture (e.g. E_PARSE|E_USER_ERROR).
34 */
35 public function __construct($level = NULL) {
36 register_shutdown_function([$this, 'onShutdown']);
37 if ($level === NULL) {
38 $level = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR;
39 }
40 $this->level = $level;
41 }
42
43 /**
44 * Enable the error policy.
45 */
46 public function activate() {
47 $this->active = TRUE;
48 $this->backup = [];
49 foreach ([
50 'display_errors',
51 'html_errors',
52 'xmlrpc_errors',
53 ] as $key) {
54 $this->backup[$key] = ini_get($key);
55 ini_set($key, 0);
56 }
57 set_error_handler([$this, 'onError'], $this->level);
58 // FIXME make this temporary/reversible
59 $this->errorScope = CRM_Core_TemporaryErrorScope::useException();
60 }
61
62 /**
63 * Disable the error policy.
64 */
65 public function deactivate() {
66 $this->errorScope = NULL;
67 restore_error_handler();
68 foreach ([
69 'display_errors',
70 'html_errors',
71 'xmlrpc_errors',
72 ] as $key) {
73 ini_set($key, $this->backup[$key]);
74 }
75 $this->active = FALSE;
76 }
77
78 /**
79 * Execute the callable. Activate and deactivate the error policy
80 * automatically.
81 *
82 * @param callable|array|string $callable
83 * A callback function.
84 *
85 * @return mixed
86 */
87 public function call($callable) {
88 $this->activate();
89 try {
90 $result = $callable();
91 }
92 catch (Exception$e) {
93 $this->reportException($e);
94 }
95 $this->deactivate();
96 return $result;
97 }
98
99 /**
100 * Receive (semi) recoverable error notices.
101 *
102 * @see set_error_handler
103 *
104 * @param string $errno
105 * @param string $errstr
106 * @param string $errfile
107 * @param int $errline
108 *
109 * @return bool
110 * @throws \Exception
111 */
112 public function onError($errno, $errstr, $errfile, $errline) {
113 if (!(error_reporting() & $errno)) {
114 return TRUE;
115 }
116 throw new Exception(sprintf('PHP Error %s at %s:%s: %s', $errno, $errfile, $errline, $errstr));
117 }
118
119 /**
120 * Receive non-recoverable error notices
121 *
122 * @see register_shutdown_function
123 * @see error_get_last
124 */
125 public function onShutdown() {
126 if (!$this->active) {
127 return;
128 }
129 $error = error_get_last();
130 if (is_array($error) && ($error['type'] & $this->level)) {
131 $this->reportError($error);
132 }
133 }
134
135 /**
136 * Print a fatal error.
137 *
138 * @param array $error
139 * The PHP error (with "type", "message", etc).
140 */
141 public function reportError($error) {
142 $response = [
143 'is_error' => 1,
144 'is_continue' => 0,
145 'exception' => htmlentities(sprintf('Error %s: %s in %s, line %s', $error['type'], $error['message'], $error['file'], $error['line'])),
146 ];
147 global $activeQueueRunner;
148 if (is_object($activeQueueRunner)) {
149 $response['last_task_title'] = $activeQueueRunner->lastTaskTitle;
150 }
151 CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportError', $response);
152 echo json_encode($response);
153 // civiExit() is unnecessary -- we're only called as part of abend
154 }
155
156 /**
157 * Print an unhandled exception.
158 *
159 * @param Exception $e
160 * The unhandled exception.
161 */
162 public function reportException(Exception $e) {
163 CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportException', CRM_Core_Error::formatTextException($e));
164
165 $response = [
166 'is_error' => 1,
167 'is_continue' => 0,
168 ];
169
170 $config = CRM_Core_Config::singleton();
171 if ($config->backtrace || CRM_Core_Config::isUpgradeMode()) {
172 $response['exception'] = CRM_Core_Error::formatHtmlException($e);
173 }
174 else {
175 $response['exception'] = htmlentities($e->getMessage());
176 }
177
178 global $activeQueueRunner;
179 if (is_object($activeQueueRunner)) {
180 $response['last_task_title'] = $activeQueueRunner->lastTaskTitle;
181 }
182 CRM_Utils_JSON::output($response);
183 }
184
185 }