PluginProbe
404 Solution / 4.3.0
404 Solution v4.3.0
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / core / ErrorHandler.php

ErrorHandler.php in 404 Solution 4.3.0, at includes/core/ErrorHandler.php

195 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 require_once __DIR__ . '/../policies/ErrorTypeClassifier.php';
9 require_once __DIR__ . '/../services/ErrorDiagnosticsReporter.php';
10 require_once __DIR__ . '/../services/AdminFatalErrorResponder.php';
11 require_once __DIR__ . '/../services/AjaxFatalErrorResponder.php';
12 require_once __DIR__ . '/../services/FatalErrorProcessor.php';
13
14 /* Functions in this class should only be for plugging into WordPress listeners (filters, actions, etc). */
15
16 class ABJ_404_Solution_ErrorHandler {
17
18 /** Keep a reference to the original error handler so we can use it later.
19 * @var callable|null
20 */
21 static $originalErrorHandler = null;
22
23 /**
24 * Reserved memory released during fatal shutdown handling so OOM errors can still render fallback output.
25 *
26 * @var string|null
27 */
28 private static $reservedMemory = null;
29
30 /** Setup.
31 * @return void
32 */
33 static function init(): void {
34 // store the original error handler.
35 self::$originalErrorHandler = set_error_handler(function(int $errno, string $errstr, string $errfile = '', int $errline = 0): bool { return false; });
36 restore_error_handler();
37
38 // set to the user defined error handler
39 set_error_handler("ABJ_404_Solution_ErrorHandler::NormalErrorHandler");
40 if (self::$reservedMemory === null) {
41 // Keep a small memory reserve so shutdown handling can render a fallback page on memory exhaustion.
42 self::$reservedMemory = str_repeat('R', 262144);
43 }
44 register_shutdown_function('ABJ_404_Solution_ErrorHandler::FatalErrorHandler');
45 }
46
47 /** Try to capture PHP errors.
48 * @param int $errno
49 * @param string $errstr
50 * @param string $errfile
51 * @param int $errline
52 * @return boolean
53 */
54 static function NormalErrorHandler($errno, $errstr, $errfile, $errline) {
55 // Respect PHP's `@` error-suppression operator. Up to PHP 7.x the @
56 // operator zeroed error_reporting() for the duration of the call;
57 // PHP 8.0+ instead leaves error_reporting() non-zero but masks out
58 // the specific level being silenced. A custom handler that ignores
59 // this state still escalates intentionally-suppressed warnings to
60 // error-level logging, defeating the suppression.
61 // Concrete case: @opcache_invalidate() at PluginLogic.php:1036 on
62 // hosts with opcache.restrict_api set produces an E_WARNING. The
63 // legacy `error_reporting() === 0` check matched on PHP 7.x but
64 // not on PHP 8.0+ (where the @ leaves the mask non-zero), so 24
65 // such ERROR entries reached the email reporter in the May 10
66 // debug zip, including 2 from current 4.1.17 sites on PHP 8.3.
67 // The bitwise form covers both eras: on PHP 7.x error_reporting()
68 // is 0 and `& $errno` is 0; on PHP 8.0+ the specific level bit is
69 // cleared and `& $errno` is also 0. Returning false here lets
70 // PHP's default handler honour the suppression.
71 if ((error_reporting() & $errno) === 0) {
72 return false;
73 }
74
75 $abj404logging = abj_service('logging');
76 $f = abj_service('functions');
77 $onlyAWarning = false;
78
79 try {
80 // if the error file does not contain the name of our plugin then we ignore it.
81 $slashPos = $f->strpos(ABJ404_NAME, '/');
82 $pluginFolder = $f->substr(ABJ404_NAME, 0, ($slashPos !== false ? $slashPos : null));
83 if ($f->strpos($errfile, $pluginFolder) === false) {
84 // let the normal error handler handle it.
85
86 // this would display the error for other plugins but show @author user
87 // stacktrace from this plugin.
88 // // try calling the original error handler.
89 // if (is_callable(self::$originalErrorHandler)) {
90 // return call_user_func_array(self::$originalErrorHandler,
91 // array($errno, $errstr, $errfile, $errline));
92 // }
93 return false;
94
95 } else {
96 // for our own plugin errors make sure we see them.
97 if ($GLOBALS['abj404_display_errors']) {
98 error_reporting(E_ALL);
99 ini_set('display_errors', '1');
100 }
101 }
102
103 // PHP emits this warning in two forms: with a "by (output started
104 // at /file:line)" suffix when it can attribute the earlier output,
105 // and without that suffix (bare "...headers already sent") when it
106 // cannot. Match the stable prefix so both forms downgrade to a
107 // warning. The " by" variant alone missed the bare form, which then
108 // reached error level and triggered a feedback report (production
109 // report 104, chasalford.com, PHP 8.4).
110 if ($errno == 2 &&
111 $f->strpos($errstr,
112 "Cannot modify header information - headers already sent") !== false) {
113
114 $onlyAWarning = true;
115 }
116
117 $extraInfo = "(none)";
118 $ctxDebugInfo = abj_service('request_context')->debug_info;
119 if ($ctxDebugInfo !== '') {
120 $extraInfo = stripcslashes(wp_kses_post((string)json_encode($ctxDebugInfo)));
121 }
122 $errmsg = "ABJ404-SOLUTION Normal error handler error: errno: " .
123 wp_kses_post((string)json_encode($errno)) . ", errstr: " . wp_kses_post((string)json_encode($errstr)) .
124 ", \nerrfile: " . stripcslashes(wp_kses_post((string)json_encode($errfile))) .
125 ", \nerrline: " . wp_kses_post((string)json_encode($errline)) .
126 ', \nAdditional info: ' . $extraInfo . ", mbstring: " .
127 (extension_loaded('mbstring') ? 'true' : 'false');
128
129 if ($abj404logging != null) {
130 if ($errno === E_NOTICE) {
131 $serverName = array_key_exists('SERVER_NAME', $_SERVER) ? $_SERVER['SERVER_NAME'] : (array_key_exists('HTTP_HOST', $_SERVER) ? $_SERVER['HTTP_HOST'] : '(not found)');
132 $whitelist = isset($GLOBALS['abj404_whitelist']) && is_array($GLOBALS['abj404_whitelist'])
133 ? $GLOBALS['abj404_whitelist'] : array();
134 if (in_array($serverName, $whitelist, true)) {
135 $e = new Exception;
136 $abj404logging->debugMessage($errmsg . ', Trace:' . $e->getTraceAsString());
137 }
138 } elseif ($onlyAWarning) {
139 $abj404logging->debugMessage($errmsg);
140 } else {
141 $abj404logging->errorMessage($errmsg);
142 }
143 } else {
144 echo $errmsg;
145 }
146 } catch (Throwable $ex) {
147 // Last-resort breadcrumb: the inner logging path itself failed,
148 // so we can't go through $abj404logging. Match the pattern used
149 // by AdminRuntimeErrorNotice::reportAdminRuntimeError() and
150 // Ajax_SuggestionCompute::handleShutdown(). Widening from
151 // Exception to Throwable is intentional because Error types are
152 // exactly the case the outer handler exists for.
153 abj404_logPhpFallback(
154 'fatal-handler-fallback',
155 'error handler itself failed (code ' . $ex->getCode() . '): ' . $ex->getMessage()
156 );
157 }
158
159 // show all warnings and errors.
160 if ($GLOBALS['abj404_display_errors']) {
161 error_reporting(E_ALL);
162 ini_set('display_errors', '1');
163 }
164 // let the original error handler handle it.
165 return false;
166 }
167
168 /** @return bool */
169 static function FatalErrorHandler(): bool {
170 $lasterror = error_get_last();
171 return self::processFatalError($lasterror);
172 }
173
174 /**
175 * Release the OOM reserve before fatal fallback rendering.
176 *
177 * @return void
178 */
179 public static function releaseReservedMemory(): void {
180 self::$reservedMemory = null;
181 }
182
183 /**
184 * Process a fatal error (shutdown handler).
185 * Public for unit tests (allows injecting a fake last error).
186 *
187 * @param array<string, mixed>|null $lasterror
188 * @return bool
189 */
190 public static function processFatalError($lasterror): bool {
191 $processor = new ABJ_404_Solution_FatalErrorProcessor();
192 return $processor->process($lasterror);
193 }
194 }
195