strpos(ABJ404_NAME, '/'); $pluginFolder = $f->substr(ABJ404_NAME, 0, ($slashPos !== false ? $slashPos : null)); if ($f->strpos($errfile, $pluginFolder) === false) { // let the normal error handler handle it. // this would display the error for other plugins but show @author user // stacktrace from this plugin. // // try calling the original error handler. // if (is_callable(self::$originalErrorHandler)) { // return call_user_func_array(self::$originalErrorHandler, // array($errno, $errstr, $errfile, $errline)); // } return false; } else { // for our own plugin errors make sure we see them. if ($GLOBALS['abj404_display_errors']) { error_reporting(E_ALL); ini_set('display_errors', '1'); } } // PHP emits this warning in two forms: with a "by (output started // at /file:line)" suffix when it can attribute the earlier output, // and without that suffix (bare "...headers already sent") when it // cannot. Match the stable prefix so both forms downgrade to a // warning. The " by" variant alone missed the bare form, which then // reached error level and triggered a feedback report (production // report 104, chasalford.com, PHP 8.4). if ($errno == 2 && $f->strpos($errstr, "Cannot modify header information - headers already sent") !== false) { $onlyAWarning = true; } $extraInfo = "(none)"; $ctxDebugInfo = abj_service('request_context')->debug_info; if ($ctxDebugInfo !== '') { $extraInfo = stripcslashes(wp_kses_post((string)json_encode($ctxDebugInfo))); } $errmsg = "ABJ404-SOLUTION Normal error handler error: errno: " . wp_kses_post((string)json_encode($errno)) . ", errstr: " . wp_kses_post((string)json_encode($errstr)) . ", \nerrfile: " . stripcslashes(wp_kses_post((string)json_encode($errfile))) . ", \nerrline: " . wp_kses_post((string)json_encode($errline)) . ', \nAdditional info: ' . $extraInfo . ", mbstring: " . (extension_loaded('mbstring') ? 'true' : 'false'); if ($abj404logging != null) { if ($errno === E_NOTICE) { $serverName = array_key_exists('SERVER_NAME', $_SERVER) ? $_SERVER['SERVER_NAME'] : (array_key_exists('HTTP_HOST', $_SERVER) ? $_SERVER['HTTP_HOST'] : '(not found)'); $whitelist = isset($GLOBALS['abj404_whitelist']) && is_array($GLOBALS['abj404_whitelist']) ? $GLOBALS['abj404_whitelist'] : array(); if (in_array($serverName, $whitelist, true)) { $e = new Exception; $abj404logging->debugMessage($errmsg . ', Trace:' . $e->getTraceAsString()); } } elseif ($onlyAWarning) { $abj404logging->debugMessage($errmsg); } else { $abj404logging->errorMessage($errmsg); } } else { echo $errmsg; } } catch (Throwable $ex) { // Last-resort breadcrumb: the inner logging path itself failed, // so we can't go through $abj404logging. Match the pattern used // by AdminRuntimeErrorNotice::reportAdminRuntimeError() and // Ajax_SuggestionCompute::handleShutdown(). Widening from // Exception to Throwable is intentional because Error types are // exactly the case the outer handler exists for. abj404_logPhpFallback( 'fatal-handler-fallback', 'error handler itself failed (code ' . $ex->getCode() . '): ' . $ex->getMessage() ); } // show all warnings and errors. if ($GLOBALS['abj404_display_errors']) { error_reporting(E_ALL); ini_set('display_errors', '1'); } // let the original error handler handle it. return false; } /** @return bool */ static function FatalErrorHandler(): bool { $lasterror = error_get_last(); return self::processFatalError($lasterror); } /** * Release the OOM reserve before fatal fallback rendering. * * @return void */ public static function releaseReservedMemory(): void { self::$reservedMemory = null; } /** * Process a fatal error (shutdown handler). * Public for unit tests (allows injecting a fake last error). * * @param array|null $lasterror * @return bool */ public static function processFatalError($lasterror): bool { $processor = new ABJ_404_Solution_FatalErrorProcessor(); return $processor->process($lasterror); } }