Merge pull request #21525 from eileenmcnaughton/cont_dep
[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 * ```
17 * $errorContainer = new CRM_Queue_ErrorPolicy();
18 * $errorContainer->call(function() {
19 * ...include some files, do some work, etc...
20 * });
21 * ```
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 }
60
61 /**
62 * Disable the error policy.
63 */
64 public function deactivate() {
65 $this->errorScope = NULL;
66 restore_error_handler();
67 foreach ([
68 'display_errors',
69 'html_errors',
70 'xmlrpc_errors',
71 ] as $key) {
72 ini_set($key, $this->backup[$key]);
73 }
74 $this->active = FALSE;
75 }
76
77 /**
78 * Execute the callable. Activate and deactivate the error policy
79 * automatically.
80 *
81 * @param callable|array|string $callable
82 * A callback function.
83 *
84 * @return mixed
85 */
86 public function call($callable) {
87 $this->activate();
88 try {
89 $result = $callable();
90 }
91 catch (Exception$e) {
92 $this->reportException($e);
93 }
94 $this->deactivate();
95 return $result;
96 }
97
98 /**
99 * Receive (semi) recoverable error notices.
100 *
101 * @see set_error_handler
102 *
103 * @param string $errno
104 * @param string $errstr
105 * @param string $errfile
106 * @param int $errline
107 *
108 * @return bool
109 * @throws \Exception
110 */
111 public function onError($errno, $errstr, $errfile, $errline) {
112 if (!(error_reporting() & $errno)) {
113 return TRUE;
114 }
115 throw new Exception(sprintf('PHP Error %s at %s:%s: %s', $errno, $errfile, $errline, $errstr));
116 }
117
118 /**
119 * Receive non-recoverable error notices
120 *
121 * @see register_shutdown_function
122 * @see error_get_last
123 */
124 public function onShutdown() {
125 if (!$this->active) {
126 return;
127 }
128 $error = error_get_last();
129 if (is_array($error) && ($error['type'] & $this->level)) {
130 $this->reportError($error);
131 }
132 }
133
134 /**
135 * Print a fatal error.
136 *
137 * @param array $error
138 * The PHP error (with "type", "message", etc).
139 */
140 public function reportError($error) {
141 $response = [
142 'is_error' => 1,
143 'is_continue' => 0,
144 'exception' => htmlentities(sprintf('Error %s: %s in %s, line %s', $error['type'], $error['message'], $error['file'], $error['line'])),
145 ];
146 global $activeQueueRunner;
147 if (is_object($activeQueueRunner)) {
148 $response['last_task_title'] = $activeQueueRunner->lastTaskTitle;
149 }
150 CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportError', $response);
151 echo json_encode($response);
152 // civiExit() is unnecessary -- we're only called as part of abend
153 }
154
155 /**
156 * Print an unhandled exception.
157 *
158 * @param Exception $e
159 * The unhandled exception.
160 */
161 public function reportException(Exception $e) {
162 CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportException', CRM_Core_Error::formatTextException($e));
163
164 $response = [
165 'is_error' => 1,
166 'is_continue' => 0,
167 ];
168
169 $config = CRM_Core_Config::singleton();
170 if ($config->backtrace || CRM_Core_Config::isUpgradeMode()) {
171 $response['exception'] = CRM_Core_Error::formatHtmlException($e);
172 }
173 else {
174 $response['exception'] = htmlentities($e->getMessage());
175 }
176
177 global $activeQueueRunner;
178 if (is_object($activeQueueRunner)) {
179 $response['last_task_title'] = $activeQueueRunner->lastTaskTitle;
180 }
181 CRM_Utils_JSON::output($response);
182 }
183
184 }