# 404-solution/trunk/includes/core/ErrorHandler.php

404 Solution, version trunk. 226 lines.

- Page: https://pluginprobe.com/plugins/404-solution/trunk/code/includes/core/ErrorHandler.php
- Raw: https://pluginprobe.com/plugins/404-solution/trunk/raw/includes/core/ErrorHandler.php
- Modified: 2026-08-31T06:59:34+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/404-solution/trunk/code/includes/core/ErrorHandler.php#L10-L20`.

```php
<?php


if (!defined('ABSPATH')) {
    exit;
}

require_once __DIR__ . '/../policies/ErrorTypeClassifier.php';
require_once __DIR__ . '/../services/ErrorDiagnosticsReporter.php';
require_once __DIR__ . '/../services/AdminFatalErrorResponder.php';
require_once __DIR__ . '/../services/AjaxFatalErrorResponder.php';
require_once __DIR__ . '/../services/FatalErrorProcessor.php';
require_once __DIR__ . '/../diagnostics/CrashBeaconStore.php';

/* Functions in this class should only be for plugging into WordPress listeners (filters, actions, etc).  */

class ABJ_404_Solution_ErrorHandler {

    /** Keep a reference to the original error handler so we can use it later.
     * @var callable|null
     */
    static $originalErrorHandler = null;

    /**
     * Reserved memory released during fatal shutdown handling so OOM errors can still render fallback output.
     *
     * @var string|null
     */
    private static $reservedMemory = null;

    /** Setup.
     * @return void
     */
    static function init(): void {
        // store the original error handler.
        self::$originalErrorHandler = set_error_handler(function(int $errno, string $errstr, string $errfile = '', int $errline = 0): bool { return false; });
        restore_error_handler();

        // set to the user defined error handler
        set_error_handler("ABJ_404_Solution_ErrorHandler::NormalErrorHandler");
        if (self::$reservedMemory === null) {
            // Keep a small memory reserve so shutdown handling can render a fallback page on memory exhaustion.
            self::$reservedMemory = str_repeat('R', 262144);
        }
        self::precomputeCrashBeaconPath();
        register_shutdown_function('ABJ_404_Solution_ErrorHandler::FatalErrorHandler');
    }

    /** Try to capture PHP errors.
     * @param int $errno
     * @param string $errstr
     * @param string $errfile
     * @param int $errline
     * @return boolean
     */
    static function NormalErrorHandler($errno, $errstr, $errfile, $errline) {
        // Respect PHP's `@` error-suppression operator. Up to PHP 7.x the @
        // operator zeroed error_reporting() for the duration of the call;
        // PHP 8.0+ instead leaves error_reporting() non-zero but masks out
        // the specific level being silenced. A custom handler that ignores
        // this state still escalates intentionally-suppressed warnings to
        // error-level logging, defeating the suppression.
        // Concrete case: @opcache_invalidate() at PluginLogic.php:1036 on
        // hosts with opcache.restrict_api set produces an E_WARNING. The
        // legacy `error_reporting() === 0` check matched on PHP 7.x but
        // not on PHP 8.0+ (where the @ leaves the mask non-zero), so 24
        // such ERROR entries reached the email reporter in the May 10
        // debug zip, including 2 from current 4.1.17 sites on PHP 8.3.
        // The bitwise form covers both eras: on PHP 7.x error_reporting()
        // is 0 and `& $errno` is 0; on PHP 8.0+ the specific level bit is
        // cleared and `& $errno` is also 0. Returning false here lets
        // PHP's default handler honour the suppression.
        if ((error_reporting() & $errno) === 0) {
            return false;
        }

        $abj404logging = abj_service('logging');
        $f = abj_service('functions');
        $errorTypeClassifier = new ABJ_404_Solution_ErrorTypeClassifier();
        $onlyAWarning = false;

        try {
            // if the error file does not contain the name of our plugin then we ignore it.
            $slashPos = $f->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<string, mixed>|null $lasterror
     * @return bool
     */
    public static function processFatalError($lasterror): bool {
        $processor = new ABJ_404_Solution_FatalErrorProcessor();
        return $processor->process($lasterror);
    }
}

```
