'error', 'elapsed_ms' => self::elapsedMilliseconds($startedAt), 'error' => self::errorSummary($e), )) ); throw $e; } ABJ_404_Solution_AjaxCheckpointLogger::recordFrequent( $requestId, 'rate_limit_operation_end', array_merge($fields, array( 'status' => 'complete', 'elapsed_ms' => self::elapsedMilliseconds($startedAt), 'result' => self::resultSummary($result, $backendSelection), )) ); return $result; } private static function requestId(): string { if (!class_exists('ABJ_404_Solution_AjaxRequestIdScopes')) { return ''; } return ABJ_404_Solution_AjaxRequestIdScopes::fromGlobalContext()->checkpoint(); } private static function beginOperationBudget(): void { self::$budgetRequestId = self::requestId(); self::$budgetOperations = 0; } private static function claimOperationBudget(string $requestId): bool { if (self::$budgetRequestId !== $requestId || self::$budgetOperations >= self::MAX_OPERATIONS_PER_CALL) { return false; } self::$budgetOperations++; return true; } /** * @return array */ private static function operationFields( string $requestId, string $operation, string $key, string $group ): array { $backend = self::backendSnapshot(); $fields = array( 'operation_id' => self::operationId($requestId, $operation), 'operation' => $operation, 'backend_class' => $backend['class'], 'capabilities' => $backend['capabilities'], 'connection' => $backend['connection'], 'max_operations' => self::MAX_OPERATIONS_PER_CALL, ); if ($key !== '') { $fields['key'] = self::hashedValue($key, 'key'); } if ($group !== '') { $fields['group'] = self::hashedValue($group, 'group'); } return $fields; } /** * @return array{class: string, capabilities: array, connection: array} */ private static function backendSnapshot(): array { $cache = $GLOBALS['wp_object_cache'] ?? null; $isObject = is_object($cache); return array( 'class' => $isObject ? self::safeClassName(get_class($cache)) : 'unavailable', 'capabilities' => array( 'wp_using_ext_object_cache' => function_exists('wp_using_ext_object_cache'), 'wp_cache_add' => function_exists('wp_cache_add'), 'wp_cache_incr' => function_exists('wp_cache_incr'), 'backend_add' => $isObject && is_callable(array($cache, 'add')), 'backend_incr' => $isObject && is_callable(array($cache, 'incr')), ), 'connection' => $isObject ? self::connectionIdentity($cache) : array('status' => 'unavailable'), ); } /** * @return array */ private static function connectionIdentity(object $cache): array { $public = get_object_vars($cache); foreach (array('redis', 'client', 'connection', 'conn', 'memcached', 'mc', 'store') as $name) { if (!array_key_exists($name, $public)) { continue; } $identity = self::identityForValue($public[$name], $name); if ($identity !== null) { return $identity; } } return array('status' => 'unavailable'); } /** * @param mixed $value * @return array|null */ private static function identityForValue($value, string $source): ?array { if (is_object($value)) { $class = self::safeClassName(get_class($value)); return array( 'status' => 'available', 'source' => $source, 'class' => $class, 'id' => 'connection#' . substr( hash('sha256', $class . '|' . spl_object_id($value)), 0, 12 ), ); } if (is_resource($value)) { $type = get_resource_type($value); return array( 'status' => 'available', 'source' => $source, 'class' => 'resource:' . self::safeToken($type), 'id' => 'connection#' . substr( // Casting a resource to int is the PHP 7.4-compatible // resource identity primitive; get_resource_id() starts // at PHP 8.0, above this plugin's supported floor. hash('sha256', $type . '|' . (int)$value), 0, 12 ), ); } return null; } /** * @param mixed $result * @return array */ private static function resultSummary($result, bool $backendSelection): array { if ($backendSelection) { return array( 'type' => 'backend', 'value' => $result ? 'persistent_cache' : 'database_fallback', ); } if (is_bool($result)) { return array('type' => 'boolean', 'value' => $result); } if (is_int($result)) { return array('type' => 'integer', 'value' => $result); } if (is_float($result)) { return array('type' => 'float', 'value' => $result); } if ($result === null) { return array('type' => 'null', 'value' => null); } if (is_object($result)) { return array('type' => 'object', 'class' => self::safeClassName(get_class($result))); } $encoded = json_encode($result); $encoded = is_string($encoded) ? $encoded : gettype($result); return array( 'type' => gettype($result), 'value_hash' => substr(hash('sha256', $encoded), 0, 12), 'value_length' => strlen($encoded), ); } /** * @return array */ private static function errorSummary(Throwable $error): array { $message = $error->getMessage(); return array( 'class' => self::safeClassName(get_class($error)), 'code' => is_int($error->getCode()) ? $error->getCode() : 0, 'message' => 'message#' . substr(hash('sha256', $message), 0, 12), 'message_length' => strlen($message), ); } private static function operationId(string $requestId, string $operation): string { self::$operationSequence++; return substr(hash( 'sha256', $requestId . '|' . $operation . '|' . self::$operationSequence ), 0, 12); } private static function hashedValue(string $value, string $kind): string { return $kind . '#' . substr(hash('sha256', $value), 0, 12); } private static function safeClassName(string $class): string { return preg_match('/^[A-Za-z_\\\\][A-Za-z0-9_\\\\]{0,159}$/', $class) === 1 ? $class : 'class#' . substr(hash('sha256', $class), 0, 12); } private static function safeToken(string $value): string { return preg_match('/^[A-Za-z0-9_.:-]{1,64}$/', $value) === 1 ? $value : 'token#' . substr(hash('sha256', $value), 0, 12); } private static function nowFloat(): ?float { if (function_exists('abj_clock')) { return abj_clock()->nowFloat(); } if (class_exists('ABJ_404_Solution_SystemClock')) { return (new ABJ_404_Solution_SystemClock())->nowFloat(); } return null; } private static function elapsedMilliseconds(?float $startedAt): ?int { if ($startedAt === null) { return null; } $finishedAt = self::nowFloat(); return $finishedAt === null ? null : max(0, (int)round(($finishedAt - $startedAt) * 1000)); } }