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); ABJ_404_Solution_PhpRuntimeCapabilityAdapter::setIni( array('directive' => 'display_errors', 'value' => '1')); } } // Recoverable host/infrastructure warnings (headers-already-sent, // open_basedir restriction, ...) are E_WARNINGs the plugin can only // degrade past, never fix -- the host itself forbids the operation. // Downgrade them below error level so getLatestErrorLine() does not // count them and the plugin does not phone home a type='error' // report about a server-side condition only the site owner can // resolve. The allowlist lives in the classifier so a future // host-only warning is added in one place, not as another special // case here (ErrorTypeClassifier::isRecoverableHostWarning: report // 104 headers-already-sent, report 149 open_basedir). $onlyAWarning = $errorTypeClassifier->isRecoverableHostWarning($errno, $errstr); $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); ABJ_404_Solution_PhpRuntimeCapabilityAdapter::setIni( array('directive' => 'display_errors', 'value' => '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; } /** * Resolve the crash-beacon file path while the request is HEALTHY and cache * it in a primitive global, so the fatal handler can write the beacon using * only that string (the fatal handler must NOT call wp_upload_dir()/options/ * the service container during an OOM shutdown; doing so risks re-entering * the exact failure class it is trying to report). Ensures the directory * exists now, while there is memory to do so. * * @return void */ private static function precomputeCrashBeaconPath(): void { try { if (!function_exists('abj404_getUploadsDir')) { return; } $dir = abj404_getUploadsDir(); if (function_exists('wp_mkdir_p')) { wp_mkdir_p($dir); } $GLOBALS['abj404_crash_beacon_path'] = rtrim($dir, '/\\') . DIRECTORY_SEPARATOR . ABJ_404_Solution_CrashBeaconStore::FILE_NAME; } catch (\Throwable $e) { // Precompute is best-effort: if it fails the fatal handler simply // skips beacon capture (it never resolves the path itself). abj404_logPhpFallback('crash-beacon-precompute', $e->getMessage()); } } /** * 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); } }