$startedAt, 'channel' => $channel, 'action' => $action, 'pid' => $pid, ); self::$ownPhase = self::PHASE_BOOT; register_shutdown_function(array(__CLASS__, 'leave')); return $optionName; } catch (Throwable $e) { self::reportFailure('same-site census join failed: ' . $e->getMessage()); return ''; } } /** * Record that this request is ENTERING the named segment of its lifecycle. * * Call it immediately before the segment runs, never after it returns: the * rows worth reading belong to requests that never came back, so a phase * written on the way out is the one phase a stranded row can never carry. * * A healthy request deletes its row at shutdown and leaves nothing behind, * so this costs one UPDATE per transition and stores nothing long-term. * An abandoned row keeps the last phase its request survived long enough * to write, which is the segment it was inside when it stopped. * * Never throws: a request must not fail because the census could not * describe it. * * @param string $phase one of self::PHASES. * @return bool whether the phase was recorded. */ public static function markPhase(string $phase): bool { try { if (self::$ownEntry === '' || self::$ownIdentity === null || !in_array($phase, self::PHASES, true)) { return false; } if ($phase === self::$ownPhase) { // Re-entering the same segment says nothing new and the write // is paid on the path being measured. return true; } $identity = self::$ownIdentity; $recorded = ABJ_404_Solution_SameSiteRequestRegistry::advance( self::$ownEntry, $identity['started_at_ms'], $identity['channel'], $identity['action'], $identity['pid'], $phase ); if ($recorded) { self::$ownPhase = $phase; } return $recorded; } catch (Throwable $e) { self::reportFailure('same-site census phase update failed: ' . $e->getMessage()); return false; } } /** * Remove this request's entry. Idempotent, and safe to call when the * request never joined. Never throws. */ public static function leave(): void { try { if (self::$ownEntry === '') { return; } $optionName = self::$ownEntry; self::$ownEntry = ''; self::$ownIdentity = null; self::$ownPhase = ''; ABJ_404_Solution_SameSiteRequestRegistry::remove(array($optionName)); } catch (Throwable $e) { self::reportFailure('same-site census leave failed: ' . $e->getMessage()); } } /** * Return this class to the state a freshly started PHP process is in: * no census identity, no memoized reading. * * One web request is one process, so a normal request never needs this. * A process that serves several requests in sequence does -- a persistent * SAPI, or any harness that drives more than one request through one * interpreter -- because the second request would otherwise inherit the * first request's census identity and report another request's row as its * own. This is the seam for returning that state, not a back door into it. */ public static function resetRequestState(): void { self::$ownEntry = ''; self::$ownIdentity = null; self::$ownPhase = ''; } /** * The option name this request registered under, or '' when it did not * join. * * Public because a reading has to tell this request's own row apart from a * competitor's, and that is the ONLY thing it needs from this class's * private state. Exposing the name rather than letting the reading reach * into the identity is what keeps the dependency one-directional. */ public static function ownEntryName(): string { return self::$ownEntry; } /** * Which census channel this request belongs to, or '' when it is out of * scope. WP-CLI is excluded on purpose: a CLI process does not consume a * web worker slot, so counting it would overstate the contention. */ public static function channelForThisRequest(): string { if (defined('WP_CLI') && WP_CLI) { return ''; } if (function_exists('wp_doing_ajax') && wp_doing_ajax()) { return 'ajax'; } // wp_doing_cron() rather than the DOING_CRON constant: it is // filterable the way WordPress itself lets other code correct the // signal, and it can be substituted in a test instead of leaking a // process-wide constant the moment one test defines it. Same reasoning // as AjaxDiagnosticRequestPolicy::bootWaypointRequestId()'s wp_doing_ajax() call. if (function_exists('wp_doing_cron') && wp_doing_cron()) { return 'cron'; } if (function_exists('is_admin') && is_admin()) { return 'admin'; } return ''; } /** The WordPress action this request names, bounded and character-restricted. */ private static function actionForThisRequest(): string { $raw = isset($_REQUEST['action']) && is_scalar($_REQUEST['action']) ? (string)$_REQUEST['action'] : ''; return preg_match('/^[A-Za-z0-9_-]{1,64}$/', $raw) === 1 ? $raw : ''; } /** * Seconds as a float from the injected clock, or null when there is no * clock yet. * * The census can be reached during the boot window (a boot-lifecycle * checkpoint fires before the service locator file is even required), and * reading the raw system clock there would both defeat the deterministic * test seam and silently mix two time sources inside one reading. Null * instead: a census with no clock cannot age its entries, and an * unavailable reading is the honest answer. The same window has no DAO * either, so nothing is lost that was otherwise obtainable. * * Public because deciding what time means for a census -- one injected * clock, null rather than a raw fallback during boot -- is this class's * policy, and ABJ_404_Solution_SameSiteCensusReading has to age entries * against the same one. A second time source inside one reading is exactly * what this null is here to prevent. */ public static function nowFloat(): ?float { if (!function_exists('abj_clock')) { return null; } try { return abj_clock()->nowFloat(); } catch (Throwable $e) { self::reportFailure('same-site census clock unavailable: ' . $e->getMessage()); return null; } } /** Milliseconds from the census clock, or null when there is none yet. */ public static function nowMs(): ?int { $now = self::nowFloat(); return $now === null ? null : (int)round($now * 1000); } private static function reportFailure(string $message): void { if (function_exists('abj404_logPhpFallback')) { abj404_logPhpFallback('same-site-census', $message); } } }