Merge pull request #13974 from eileenmcnaughton/array_format7
[civicrm-core.git] / CRM / Queue / ErrorPolicy.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
fee14197 4 | CiviCRM version 5 |
6a488035 5 +--------------------------------------------------------------------+
6b83d5bd 6 | Copyright CiviCRM LLC (c) 2004-2019 |
6a488035
TO
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
d25dd0ee 26 */
6a488035
TO
27
28/**
4523a2f5
TO
29 * To ensure that PHP errors or unhandled exceptions are reported in JSON
30 * format, wrap this around your code. For example:
6a488035
TO
31 *
32 * @code
33 * $errorContainer = new CRM_Queue_ErrorPolicy();
9b873358 34 * $errorContainer->call(function() {
6a488035
TO
35 * ...include some files, do some work, etc...
36 * });
37 * @endcode
38 *
39 * Note: Most of the code in this class is pretty generic vis-a-vis error
40 * handling -- except for 'reportError', whose message format is only
41 * appropriate for use with the CRM_Queue_Page_AJAX. Some kind of cleanup
42 * will be necessary to get reuse from the other parts of this class.
43 */
44class CRM_Queue_ErrorPolicy {
4523a2f5 45 public $active;
e0ef6999
EM
46
47 /**
4523a2f5
TO
48 * @param null|int $level
49 * PHP error level to capture (e.g. E_PARSE|E_USER_ERROR).
e0ef6999 50 */
4523a2f5 51 public function __construct($level = NULL) {
be2fb01f 52 register_shutdown_function([$this, 'onShutdown']);
6a488035
TO
53 if ($level === NULL) {
54 $level = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR;
55 }
56 $this->level = $level;
57 }
58
4523a2f5
TO
59 /**
60 * Enable the error policy.
61 */
62 public function activate() {
6a488035 63 $this->active = TRUE;
be2fb01f
CW
64 $this->backup = [];
65 foreach ([
353ffa53
TO
66 'display_errors',
67 'html_errors',
28a04ea9 68 'xmlrpc_errors',
be2fb01f 69 ] as $key) {
6a488035
TO
70 $this->backup[$key] = ini_get($key);
71 ini_set($key, 0);
72 }
be2fb01f 73 set_error_handler([$this, 'onError'], $this->level);
6a488035
TO
74 // FIXME make this temporary/reversible
75 $this->errorScope = CRM_Core_TemporaryErrorScope::useException();
76 }
77
4523a2f5
TO
78 /**
79 * Disable the error policy.
80 */
81 public function deactivate() {
6a488035
TO
82 $this->errorScope = NULL;
83 restore_error_handler();
be2fb01f 84 foreach ([
353ffa53
TO
85 'display_errors',
86 'html_errors',
28a04ea9 87 'xmlrpc_errors',
be2fb01f 88 ] as $key) {
6a488035
TO
89 ini_set($key, $this->backup[$key]);
90 }
91 $this->active = FALSE;
92 }
93
e0ef6999 94 /**
4523a2f5
TO
95 * Execute the callable. Activate and deactivate the error policy
96 * automatically.
97 *
98 * @param callable|array|string $callable
99 * A callback function.
e0ef6999
EM
100 *
101 * @return mixed
102 */
4523a2f5 103 public function call($callable) {
6a488035
TO
104 $this->activate();
105 try {
106 $result = $callable();
107 }
353ffa53 108 catch (Exception$e) {
6a488035
TO
109 $this->reportException($e);
110 }
111 $this->deactivate();
112 return $result;
113 }
114
115 /**
ad37ac8e 116 * Receive (semi) recoverable error notices.
6a488035
TO
117 *
118 * @see set_error_handler
ad37ac8e 119 *
120 * @param string $errno
121 * @param string $errstr
122 * @param string $errfile
123 * @param int $errline
124 *
125 * @return bool
126 * @throws \Exception
6a488035 127 */
4523a2f5 128 public function onError($errno, $errstr, $errfile, $errline) {
6a488035
TO
129 if (!(error_reporting() & $errno)) {
130 return TRUE;
131 }
132 throw new Exception(sprintf('PHP Error %s at %s:%s: %s', $errno, $errfile, $errline, $errstr));
133 }
134
135 /**
136 * Receive non-recoverable error notices
137 *
138 * @see register_shutdown_function
139 * @see error_get_last
140 */
4523a2f5 141 public function onShutdown() {
6a488035
TO
142 if (!$this->active) {
143 return;
144 }
145 $error = error_get_last();
146 if (is_array($error) && ($error['type'] & $this->level)) {
147 $this->reportError($error);
148 }
149 }
150
151 /**
fe482240 152 * Print a fatal error.
6a488035 153 *
4523a2f5
TO
154 * @param array $error
155 * The PHP error (with "type", "message", etc).
6a488035 156 */
4523a2f5 157 public function reportError($error) {
be2fb01f 158 $response = [
6a488035
TO
159 'is_error' => 1,
160 'is_continue' => 0,
161 'exception' => htmlentities(sprintf('Error %s: %s in %s, line %s', $error['type'], $error['message'], $error['file'], $error['line'])),
be2fb01f 162 ];
6a488035
TO
163 global $activeQueueRunner;
164 if (is_object($activeQueueRunner)) {
165 $response['last_task_title'] = $activeQueueRunner->lastTaskTitle;
166 }
167 CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportError', $response);
168 echo json_encode($response);
169 // civiExit() is unnecessary -- we're only called as part of abend
170 }
171
172 /**
fe482240 173 * Print an unhandled exception.
6a488035 174 *
4523a2f5
TO
175 * @param Exception $e
176 * The unhandled exception.
6a488035 177 */
4523a2f5 178 public function reportException(Exception $e) {
6a488035
TO
179 CRM_Core_Error::debug_var('CRM_Queue_ErrorPolicy_reportException', CRM_Core_Error::formatTextException($e));
180
be2fb01f 181 $response = [
6a488035
TO
182 'is_error' => 1,
183 'is_continue' => 0,
be2fb01f 184 ];
6a488035
TO
185
186 $config = CRM_Core_Config::singleton();
187 if ($config->backtrace || CRM_Core_Config::isUpgradeMode()) {
188 $response['exception'] = CRM_Core_Error::formatHtmlException($e);
189 }
190 else {
191 $response['exception'] = htmlentities($e->getMessage());
192 }
193
194 global $activeQueueRunner;
195 if (is_object($activeQueueRunner)) {
196 $response['last_task_title'] = $activeQueueRunner->lastTaskTitle;
197 }
ecdef330 198 CRM_Utils_JSON::output($response);
6a488035 199 }
96025800 200
6a488035 201}