Generated nonces cache */ private array $nonceCache = []; /** * Generate nonce for specific action * * @param string $action Action name * @param int|null $userId User ID (defaults to current user) * @return string Generated nonce * @since 1.0.0 */ public function generateNonce(string $action, ?int $userId = null): string { $userId = $userId ?? get_current_user_id(); $nonceAction = $this->getNonceAction($action, $userId); // Check cache first $cacheKey = $this->getCacheKey($nonceAction); if (isset($this->nonceCache[$cacheKey])) { return $this->nonceCache[$cacheKey]; } $nonce = wp_create_nonce($nonceAction); $this->nonceCache[$cacheKey] = $nonce; return $nonce; } /** * Validate nonce from request * * @param array $request Request data * @param string $action Action name * @param int|null $userId User ID (defaults to current user) * @return ValidationLayerResult * @since 1.0.0 */ public function validateNonce(array $request, string $action, ?int $userId = null): ValidationLayerResult { $result = new ValidationLayerResult(); $userId = $userId ?? get_current_user_id(); // Check if nonce field exists $nonceField = $this->getNonceFieldName($action); if (!isset($request[$nonceField])) { $result->setValid(false); $result->setError("Missing nonce field: {$nonceField}"); return $result; } $nonce = $request[$nonceField]; if (!is_string($nonce)) { $result->setValid(false); $result->setError('Invalid nonce format'); return $result; } // Validate nonce $nonceAction = $this->getNonceAction($action, $userId); $isValid = wp_verify_nonce($nonce, $nonceAction); if ($isValid === false) { $result->setValid(false); $result->setError('Nonce validation failed'); return $result; } // Check if nonce is about to expire (and needs refresh) if ($isValid === 1) { // Nonce is valid but in second half of its lifetime $result->setWarning('Nonce approaching expiration'); $result->setMetadata(['refresh_recommended' => true]); } $result->setValid(true); $result->setMetadata([ 'nonce_age' => $isValid, 'action' => $action, 'user_id' => $userId ]); return $result; } /** * Generate nonce field HTML * * @param string $action Action name * @param bool $referer Include referer field * @return string HTML nonce field * @since 1.0.0 */ public function generateNonceField(string $action, bool $referer = true): string { $nonce = $this->generateNonce($action); $fieldName = $this->getNonceFieldName($action); $html = sprintf( '', esc_attr($fieldName), esc_attr($nonce) ); if ($referer) { $html .= wp_referer_field(false); } return $html; } /** * Generate nonce URL parameter * * @param string $url Base URL * @param string $action Action name * @return string URL with nonce parameter * @since 1.0.0 */ public function generateNonceUrl(string $url, string $action): string { $nonce = $this->generateNonce($action); $fieldName = $this->getNonceFieldName($action); return add_query_arg($fieldName, $nonce, $url); } /** * Validate nonce from URL * * @param string $action Action name * @param int|null $userId User ID (defaults to current user) * @return ValidationLayerResult * @since 1.0.0 */ public function validateNonceUrl(string $action, ?int $userId = null): ValidationLayerResult { $fieldName = $this->getNonceFieldName($action); $nonce = $_GET[$fieldName] ?? ''; return $this->validateNonce([$fieldName => $nonce], $action, $userId); } /** * Check if nonce needs refresh * * @param array $request Request data * @param string $action Action name * @return bool * @since 1.0.0 */ public function needsRefresh(array $request, string $action): bool { $result = $this->validateNonce($request, $action); if (!$result->isValid()) { return false; } $metadata = $result->getMetadata(); return isset($metadata['refresh_recommended']) && $metadata['refresh_recommended']; } /** * Generate AJAX nonce for JavaScript * * @param string $action Action name * @return array Nonce data for AJAX * @since 1.0.0 */ public function generateAjaxNonce(string $action): array { return [ 'nonce' => $this->generateNonce($action), 'field_name' => $this->getNonceFieldName($action), 'action' => $action, 'expires_in' => self::NONCE_LIFETIME ]; } /** * Validate AJAX nonce * * @param string $action Action name * @return ValidationLayerResult * @since 1.0.0 */ public function validateAjaxNonce(string $action): ValidationLayerResult { $nonce = $_POST['security'] ?? $_POST[$this->getNonceFieldName($action)] ?? ''; if (empty($nonce)) { $result = new ValidationLayerResult(); $result->setValid(false); $result->setError('Missing AJAX nonce'); return $result; } return $this->validateNonce([$this->getNonceFieldName($action) => $nonce], $action); } /** * Get nonce action name * * @param string $action Base action * @param int $userId User ID * @return string Full nonce action * @since 1.0.0 */ private function getNonceAction(string $action, int $userId): string { return "care_book_ultimate_{$action}_{$userId}"; } /** * Get nonce field name * * @param string $action Action name * @return string Field name * @since 1.0.0 */ private function getNonceFieldName(string $action): string { return self::NONCE_FIELD_PREFIX . $action; } /** * Get cache key for nonce * * @param string $nonceAction Nonce action * @return string Cache key * @since 1.0.0 */ private function getCacheKey(string $nonceAction): string { return md5($nonceAction . floor(time() / 300)); // 5-minute buckets } /** * Clear nonce cache * * @return void * @since 1.0.0 */ public function clearCache(): void { $this->nonceCache = []; } /** * Get nonce statistics * * @return array * @since 1.0.0 */ public function getStats(): array { return [ 'cache_size' => count($this->nonceCache), 'lifetime' => self::NONCE_LIFETIME, 'prefix' => self::NONCE_FIELD_PREFIX ]; } /** * Batch validate multiple nonces * * @param array $request Request data * @param array $actions Actions to validate * @return array * @since 1.0.0 */ public function validateMultipleNonces(array $request, array $actions): array { $results = []; foreach ($actions as $action) { $results[$action] = $this->validateNonce($request, $action); } return $results; } }