= \strlen( self::PRETTY_ROUTE ) && self::PRETTY_ROUTE === substr( $path, -\strlen( self::PRETTY_ROUTE ) ) ); } /** * Response headers that keep the ping out of proxy and server caches. * * The fast path answers before WP REST exists, so Rest_Cors never adds * its cache-defeating headers here; without these an origin page cache * served one host's ping (timestamp and pressure bucket) frozen for its * whole TTL (measured 2026-09-16). Same Cache-Control value as Rest_Cors. * * @return array */ public static function cache_defeating_headers(): array { return array( 'Cache-Control' => 'private, no-store', 'X-LiteSpeed-Cache-Control' => 'no-cache', ); } /** Belt and braces for drop-in page caches that finalise at shutdown and read constants, not headers. */ private static function forbid_page_cache(): void { foreach ( array( 'DONOTCACHEPAGE', 'LSCACHE_NO_CACHE' ) as $constant ) { if ( ! \defined( $constant ) ) { \define( $constant, true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- third-party constant. } } } /** Serve a matching request before the remaining plugins load. */ public static function maybe_serve(): void { $method = isset( $_SERVER['REQUEST_METHOD'] ) && \is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'GET' !== $method && 'HEAD' !== $method ) { return; } $request_uri = isset( $_SERVER['REQUEST_URI'] ) && \is_string( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; if ( false === strpos( $request_uri, 'wcpos' ) ) { return; } $rest_route = isset( $_GET['rest_route'] ) && \is_string( $_GET['rest_route'] ) ? sanitize_text_field( wp_unslash( $_GET['rest_route'] ) ) : null; if ( ! self::matches_request( $method, $request_uri, $rest_route ) ) { return; } $data = self::payload(); self::forbid_page_cache(); http_response_code( 200 ); header( 'Content-Type: application/json; charset=UTF-8' ); foreach ( self::cache_defeating_headers() as $name => $value ) { header( $name . ': ' . $value ); } header( 'Access-Control-Allow-Origin: *' ); // Deliberately just the one header this fast path can emit, not the // full Rest_Cors::EXPOSE_HEADERS set: this short-circuits before the // autoloader and WP REST exist. The OPTIONS preflight for this route // is answered by Rest_Cors on the normal REST lane. header( 'Access-Control-Expose-Headers: X-WCPOS-Pressure' ); if ( isset( $data['pressure'] ) ) { header( 'X-WCPOS-Pressure: ' . $data['pressure'] ); } if ( 'HEAD' !== $method ) { echo wp_json_encode( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON HTTP response. } // Ship the response before exit's shutdown handlers run: the OTel // wordpress instrumentation reads conditional tags (is_404) at shutdown, // and with no query having run, WP_DEBUG_DISPLAY sites would append a // _doing_it_wrong notice after the JSON body (#1582). Under FPM, closing // the request first makes late output unreachable; elsewhere, mute // display so shutdown notices cannot corrupt the payload. if ( \function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); } else { @ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed, WordPress.PHP.NoSilencedErrors.Discouraged -- last-resort mute on non-FPM SAPIs; the response is already emitted. } exit; } /** Register the canonical REST fallback. */ public function register_routes(): void { register_rest_route( 'wcpos/v2', '/ping', array( 'methods' => 'GET, HEAD', 'callback' => array( $this, 'get_ping' ), 'permission_callback' => '__return_true', ) ); } /** @return array */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- compact typed classification. public function wcpos_route_classifications(): array { return array( 'public' => array( self::ROUTE ) ); } /** Return the canonical REST response. */ public function get_ping(): WP_REST_Response { $data = self::payload(); $response = new WP_REST_Response( $data, 200 ); if ( isset( $data['pressure'] ) ) { $response->header( 'X-WCPOS-Pressure', $data['pressure'] ); } return $response; } /** Convert normalized load to a pressure bucket, or read host load when omitted (memoized per request so body and header always agree). */ public static function pressure_bucket( ?float $load = null ): ?string { if ( null === $load ) { if ( ! self::$host_pressure_checked ) { self::$host_pressure_checked = true; self::$host_pressure_bucket = self::read_host_pressure_bucket(); } return self::$host_pressure_bucket; } if ( $load < 0.9 ) { return 'low'; } return $load <= 1.8 ? 'elevated' : 'high'; } /** * Use only /proc/cpuinfo because sys_getloadavg() reads host-wide /proc/loadavg, * so its CPU divisor must share the host namespace rather than a container quota. */ public static function cpu_count_from_cpuinfo( ?string $cpuinfo ): ?int { $found = null !== $cpuinfo ? preg_match_all( '/^processor\s*:/m', $cpuinfo ) : false; return \is_int( $found ) && $found > 0 ? $found : null; } /** * Normalize host load using the /proc/cpuinfo CPU count. * Unknown counts yield null (no header), rather than misleading pressure from a guessed divisor. */ private static function read_host_pressure_bucket(): ?string { if ( ! \function_exists( 'sys_getloadavg' ) || ! \is_array( $average = @sys_getloadavg() ) || ! isset( $average[0] ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure -- call only after availability check. return null; } if ( ! self::$host_cpu_count_resolved ) { $cpuinfo = @file_get_contents( '/proc/cpuinfo' ); self::$host_cpu_count = self::cpu_count_from_cpuinfo( false === $cpuinfo ? null : $cpuinfo ); self::$host_cpu_count_resolved = true; } return null === self::$host_cpu_count ? null : self::pressure_bucket( (float) $average[0] / self::$host_cpu_count ); } /** @return array */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- compact typed payload. private static function payload(): array { $data = array( 'ok' => true, 'ts' => time(), 'v' => VERSION ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound -- fixed four-field maximum. $pressure = self::pressure_bucket(); if ( null !== $pressure ) { $data['pressure'] = $pressure; } return $data; } }