| | |
| | | <?php |
| | | namespace JVBase\rest; |
| | | |
| | | use DateTime; |
| | | use DateTimeZone; |
| | | use JVBase\JVB; |
| | | use JVBase\rest\RateLimiter; |
| | | use JVBase\managers\OperationQueue; |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\managers\NotificationManager; |
| | | use JVBase\utility\Features; |
| | | use WP_REST_Request; |
| | | use WP_Error; |
| | | use Exception; |
| | |
| | | protected NotificationManager $notifications; |
| | | protected string $cache_name =''; |
| | | protected int $cache_ttl = 3600; //1 hour default |
| | | |
| | | protected array $response_headers = []; |
| | | |
| | | // Error code constants for consistency |
| | | const ERROR_MISSING_PARAMS = 'missing_parameters'; |
| | |
| | | $this->base = BASE; |
| | | $this->rate_limiter = new RateLimiter(); |
| | | if ($this->cache_name !== '') { |
| | | $this->cache = new CacheManager($this->cache_name, $this->cache_ttl); |
| | | $this->cache = CacheManager::for($this->cache_name, $this->cache_ttl); |
| | | } |
| | | add_action('rest_api_init', [$this, 'registerRoutes']); |
| | | } |
| | |
| | | { |
| | | // Check rate limits first |
| | | if (!$this->rate_limiter->checkLimit($request)) { |
| | | error_log('Rate Limit Reached'); |
| | | return new WP_Error( |
| | | 'rate_limit_reached', |
| | | 'Rate limit reached', |
| | |
| | | } |
| | | $user_id = $request->get_param('user'); |
| | | if (!empty($user_id) && !$this->userCheck($user_id)) { |
| | | error_log('Usercheck failed'); |
| | | return false; |
| | | } |
| | | // Verify nonces |
| | |
| | | return true; |
| | | } |
| | | |
| | | public function checkRateLimit(WP_REST_Request $request):bool|WP_Error |
| | | { |
| | | if (!$this->rate_limiter->checkLimit($request)) { |
| | | error_log('Rate Limit Reached'); |
| | | return new WP_Error( |
| | | 'rate_limit', |
| | | 'Too many attempts. Please wait a moment before trying again.', |
| | | ['status' => 429] |
| | | ); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Convert MySQL datetime to ISO 8601 timestamp with proper timezone |
| | | */ |
| | | public function formatTimestamp(?string $mysql_datetime): ?string |
| | | { |
| | | if (empty($mysql_datetime)) { |
| | | return null; |
| | | } |
| | | |
| | | try { |
| | | // Get WordPress timezone - dates are stored in this timezone |
| | | $wp_timezone = wp_timezone(); |
| | | |
| | | // Parse the datetime in WordPress timezone |
| | | $date = new DateTime($mysql_datetime, $wp_timezone); |
| | | |
| | | // Convert to UTC for API consistency |
| | | $date->setTimezone(new DateTimeZone('UTC')); |
| | | |
| | | // Return ISO 8601 format |
| | | return $date->format('c'); |
| | | } catch (Exception $e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | protected function checkContent(string $content, bool $bool = false):string|bool |
| | | { |
| | | $result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??''; |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @param int $userID The user ID to check |
| | | * |
| | | * @return bool whether user exists |
| | | */ |
| | | protected function checkUser(int $userID):bool |
| | | { |
| | | $checked = $this->cache->get($userID, 'checked_users'); |
| | | if ($checked) { |
| | | return $checked; |
| | | } |
| | | $test = (bool)get_userdata($userID); |
| | | /** |
| | | * Check if user exists (cached) |
| | | */ |
| | | protected function checkUser(int $userID): bool |
| | | { |
| | | $cache = CacheManager::for('users'); |
| | | |
| | | $this->cache->set($userID, $test, null, 'checked_users'); |
| | | return $test; |
| | | } |
| | | return $cache->remember("user_exists_{$userID}", function() use ($userID) { |
| | | return (bool)get_userdata($userID); |
| | | }, DAY_IN_SECONDS); |
| | | } |
| | | |
| | | /** |
| | | * @param int $shopID the shop ID to check |
| | | * |
| | | * @return bool whether the shop exists |
| | | */ |
| | | protected function checkShop(int $shopID):bool |
| | | { |
| | | $checked = $this->cache->get($shopID, 'checked_shops'); |
| | | if ($checked) { |
| | | return (bool)$checked; |
| | | } |
| | | $test = term_exists($shopID, BASE . 'shop'); |
| | | $this->cache->set($shopID, (int)$test, null, 'checked_shops'); |
| | | return $test; |
| | | } |
| | | /** |
| | | * Check if shop exists (cached) |
| | | */ |
| | | protected function checkShop(int $shopID): bool |
| | | { |
| | | $cache = CacheManager::for('shop'); |
| | | |
| | | protected function checkTerm(array $args) { |
| | | $termID = $args['to_term']??$args['term_id']??false; |
| | | return $cache->remember("shop_exists_{$shopID}", function() use ($shopID) { |
| | | return (bool)term_exists($shopID, BASE . 'shop'); |
| | | }, DAY_IN_SECONDS); |
| | | } |
| | | |
| | | /** |
| | | * Check if term exists (cached) |
| | | */ |
| | | protected function checkTerm(array $args): bool |
| | | { |
| | | $termID = $args['to_term'] ?? $args['term_id'] ?? false; |
| | | if (!$termID) { |
| | | return false; |
| | | } |
| | | $taxonomy = $args['taxonomy']??false; |
| | | |
| | | $taxonomy = $args['taxonomy'] ?? false; |
| | | if (!$taxonomy) { |
| | | return false; |
| | | } |
| | | $checked = $this->cache->get($termID, 'checked_'.$taxonomy); |
| | | if ($checked) { |
| | | return (bool) $checked; |
| | | } |
| | | $test = term_exists($termID, jvbCheckBase($taxonomy)); |
| | | $this->cache->set($termID, (int)$test, null, 'checked_'.$taxonomy); |
| | | return (bool)$test; |
| | | |
| | | $taxonomy = jvbCheckBase($taxonomy); |
| | | $cache = CacheManager::for($taxonomy); |
| | | |
| | | return $cache->remember("term_exists_{$termID}", function() use ($termID, $taxonomy) { |
| | | return (bool)term_exists($termID, $taxonomy); |
| | | }, DAY_IN_SECONDS); |
| | | } |
| | | |
| | | /** |
| | | * Check if an artist is verified |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return bool True if verified |
| | | */ |
| | | public function isVerifiedUser(int $user_id):bool |
| | | { |
| | | // Cache result to avoid repeated checks |
| | | $cache_key = "verified_users"; |
| | | $verified = $this->cache->get($cache_key, 'users'); |
| | | $verified = ($verified) ?: []; |
| | | if (array_key_exists($user_id, $verified)) { |
| | | return (bool) $verified[$user_id]; |
| | | } |
| | | /** |
| | | * Check if an artist is verified |
| | | */ |
| | | public function isVerifiedUser(int $user_id): bool |
| | | { |
| | | $cache = CacheManager::forUser($user_id); |
| | | |
| | | // Check if user has the skip_moderation capability |
| | | $is_verified = user_can($user_id, 'skip_moderation'); |
| | | |
| | | $verified[$user_id] = $is_verified; |
| | | // Cache for a day |
| | | $this->cache->set($cache_key, $verified, DAY_IN_SECONDS, 'users'); |
| | | |
| | | return $is_verified; |
| | | } |
| | | return $cache->remember('is_verified', function() use ($user_id) { |
| | | return user_can($user_id, 'skip_moderation'); |
| | | }, DAY_IN_SECONDS); |
| | | } |
| | | |
| | | protected function applyTaxonomyFilters(array $args, array $data):array |
| | | { |
| | |
| | | return $wpdb->get_var("SHOW TABLES LIKE '{$tableName}'") !== $tableName; |
| | | } |
| | | |
| | | protected function ifModifiedSince($lastModified, $params, $request):WP_REST_Response|null { |
| | | $etag = '"' . md5(serialize($params)) . '"'; |
| | | // Check ETag |
| | | // ========== HTTP CACHING METHODS ========== |
| | | |
| | | /** |
| | | * Check HTTP caching headers (ETag and If-Modified-Since) |
| | | * Returns 304 Not Modified if content hasn't changed |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @param string|array $content_types Content type(s) to check timestamps for |
| | | * @param array $additional_params Additional params for ETag uniqueness (e.g., user_id, filters) |
| | | * @return WP_REST_Response|null Returns 304 response if not modified, null to continue processing |
| | | */ |
| | | protected function checkHeaders( |
| | | WP_REST_Request $request, |
| | | string|array $content_types, |
| | | array $additional_params = [] |
| | | ): WP_REST_Response|null { |
| | | |
| | | // Get latest timestamp for the content type(s) |
| | | $last_modified = CacheManager::getTimestamp($content_types); |
| | | |
| | | // Generate ETag from request params + timestamp |
| | | $etag = $this->generateETag($request->get_params(), $additional_params, $last_modified); |
| | | |
| | | // Check If-None-Match (ETag) header |
| | | $if_none_match = $request->get_header('If-None-Match'); |
| | | if ($if_none_match && $if_none_match === $etag) { |
| | | return new WP_REST_Response(null, 304); |
| | | if ($if_none_match === $etag) { |
| | | return $this->createNotModifiedResponse($etag, $last_modified); |
| | | } |
| | | |
| | | // Check If-Modified-Since header |
| | | $if_modified_since = $request->get_header('If-Modified-Since'); |
| | | if ($if_modified_since && $lastModified) { |
| | | if ($if_modified_since) { |
| | | $if_modified_timestamp = strtotime($if_modified_since); |
| | | if ($lastModified <= $if_modified_timestamp) { |
| | | return new WP_REST_Response(null, 304); |
| | | if ($last_modified <= $if_modified_timestamp) { |
| | | return $this->createNotModifiedResponse($etag, $last_modified); |
| | | } |
| | | } |
| | | |
| | | header('ETag: ' . $etag); // Add this line |
| | | if ($lastModified) { |
| | | header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); |
| | | // Content has changed - store headers to add to successful response |
| | | $this->response_headers = $this->buildCacheHeaders($etag, $last_modified); |
| | | |
| | | return null; // Continue processing |
| | | } |
| | | |
| | | /** |
| | | * Generate ETag from request parameters and timestamp |
| | | * |
| | | * @param array $params Request parameters |
| | | * @param array $additional Additional parameters for uniqueness |
| | | * @param int $timestamp Last modified timestamp |
| | | * @return string ETag value with quotes |
| | | */ |
| | | private function generateETag(array $params, array $additional, int $timestamp): string |
| | | { |
| | | // Combine all data that makes this response unique |
| | | $etag_data = array_merge( |
| | | $params, |
| | | $additional, |
| | | ['t' => $timestamp] |
| | | ); |
| | | |
| | | return '"' . md5(serialize($etag_data)) . '"'; |
| | | } |
| | | |
| | | /** |
| | | * Create 304 Not Modified response with proper headers |
| | | * |
| | | * @param string $etag ETag value |
| | | * @param int $last_modified Last modified timestamp |
| | | * @return WP_REST_Response 304 response |
| | | */ |
| | | private function createNotModifiedResponse(string $etag, int $last_modified): WP_REST_Response |
| | | { |
| | | $response = new WP_REST_Response(null, 304); |
| | | $response->set_headers($this->buildCacheHeaders($etag, $last_modified)); |
| | | return $response; |
| | | } |
| | | |
| | | /** |
| | | * Build cache headers array |
| | | * |
| | | * @param string $etag ETag value |
| | | * @param int $last_modified Last modified timestamp |
| | | * @return array Headers array |
| | | */ |
| | | private function buildCacheHeaders(string $etag, int $last_modified): array |
| | | { |
| | | return [ |
| | | 'ETag' => $etag, |
| | | 'Last-Modified' => gmdate('D, d M Y H:i:s', $last_modified) . ' GMT', |
| | | 'Cache-Control' => 'private, max-age=60, must-revalidate' |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Add stored cache headers to a response |
| | | * Call this on your final WP_REST_Response before returning |
| | | * |
| | | * @param WP_REST_Response $response The response to add headers to |
| | | * @return WP_REST_Response The response with headers added |
| | | */ |
| | | protected function addCacheHeaders(WP_REST_Response $response): WP_REST_Response |
| | | { |
| | | if (!empty($this->response_headers)) { |
| | | $response->set_headers($this->response_headers); |
| | | $this->response_headers = []; // Clear after use |
| | | } |
| | | header('Cache-Control: private, max-age=30'); |
| | | return null; |
| | | return $response; |
| | | } |
| | | |
| | | /** |
| | | * Helper: Check headers for user-specific endpoints |
| | | * Automatically includes user_id in ETag |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @param int $user_id User ID |
| | | * @param string|array $content_types Content type(s) |
| | | * @return WP_REST_Response|null |
| | | */ |
| | | protected function checkUserHeaders( |
| | | WP_REST_Request $request, |
| | | int $user_id, |
| | | string|array $content_types = 'user' |
| | | ): WP_REST_Response|null { |
| | | |
| | | // Include user-specific timestamp |
| | | $types = is_array($content_types) ? $content_types : [$content_types]; |
| | | $types[] = "user_{$user_id}"; |
| | | |
| | | return $this->checkHeaders($request, $types, ['user_id' => $user_id]); |
| | | } |
| | | |
| | | /** |
| | | * Helper to return error response |
| | | */ |
| | | protected function error(string $message, string $code, int $status = 400, ?string $field = null): WP_REST_Response |
| | | { |
| | | $data = [ |
| | | 'success' => false, |
| | | 'message' => $message, |
| | | 'code' => $code |
| | | ]; |
| | | |
| | | if ($field) { |
| | | $data['field'] = $field; |
| | | } |
| | | |
| | | return new WP_REST_Response($data, $status); |
| | | } |
| | | /** |
| | | * Helper to return success response |
| | | */ |
| | | protected function success(array $data, int $status = 200): WP_REST_Response |
| | | { |
| | | $data['success'] = true; |
| | | return new WP_REST_Response($data, $status); |
| | | } |
| | | |
| | | /************************************************************ |
| | | SESSION FINGERPRINT |
| | | ************************************************************/ |
| | | /** |
| | | * Store session fingerprint for hijacking detection |
| | | */ |
| | | protected function storeSessionFingerprint(int $user_id, WP_REST_Request $request): void |
| | | { |
| | | if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) { |
| | | return; |
| | | } |
| | | |
| | | $fingerprint = $this->generateSessionFingerprint($request); |
| | | update_user_meta($user_id, BASE . 'session_fingerprint', $fingerprint); |
| | | update_user_meta($user_id, BASE . 'session_timestamp', time()); |
| | | } |
| | | /** |
| | | * Generate session fingerprint for hijacking detection |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @return string Hashed fingerprint |
| | | */ |
| | | protected function generateSessionFingerprint(WP_REST_Request $request): string |
| | | { |
| | | return hash('sha256', implode('|', [ |
| | | $request->get_header('User-Agent') ?? '', |
| | | // Use IP class instead of full IP to allow for mobile network changes |
| | | $this->getIPClass( |
| | | $request->get_header('X-Forwarded-For') |
| | | ?: $request->get_header('X-Real-IP') |
| | | ?: $_SERVER['REMOTE_ADDR'] ?? '' |
| | | ) |
| | | ])); |
| | | } |
| | | |
| | | /** |
| | | * Get IP class (first 3 octets) for session validation |
| | | * This allows for minor IP changes (common with mobile networks) |
| | | * |
| | | * @param string $ip IP address |
| | | * @return string First 3 octets |
| | | */ |
| | | protected function getIPClass(string $ip): string |
| | | { |
| | | $parts = explode('.', $ip); |
| | | return implode('.', array_slice($parts, 0, 3)); |
| | | } |
| | | |
| | | /** |
| | | * Validate session fingerprint against stored value |
| | | * |
| | | * @param int $user_id User ID to validate |
| | | * @param WP_REST_Request $request Current request |
| | | * @return bool True if valid, false if potential hijacking |
| | | */ |
| | | protected function validateSessionFingerprint(int $user_id, WP_REST_Request $request): bool |
| | | { |
| | | // Only enforce if enabled in config |
| | | if (!defined('JVB_SESSION_FINGERPRINT') || !JVB_SESSION_FINGERPRINT) { |
| | | return true; |
| | | } |
| | | |
| | | $stored = get_user_meta($user_id, BASE . 'session_fingerprint', true); |
| | | $current = $this->generateSessionFingerprint($request); |
| | | |
| | | if (empty($stored)) { |
| | | // First request - store fingerprint |
| | | update_user_meta($user_id, BASE . 'session_fingerprint', $current); |
| | | update_user_meta($user_id, BASE . 'session_timestamp', time()); |
| | | return true; |
| | | } |
| | | |
| | | // Compare using timing-safe comparison |
| | | return hash_equals($stored, $current); |
| | | } |
| | | |
| | | /** |
| | | * Clear session fingerprint (call on logout) |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return void |
| | | */ |
| | | protected function clearSessionFingerprint(int $user_id): void |
| | | { |
| | | delete_user_meta($user_id, BASE . 'session_fingerprint'); |
| | | delete_user_meta($user_id, BASE . 'session_timestamp'); |
| | | } |
| | | |
| | | /****************************************************************** |
| | | * CSRF PROTECTION |
| | | ******************************************************************/ |
| | | /** |
| | | * Generate CSRF token for a user |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return string CSRF token |
| | | */ |
| | | protected function generateCSRFToken(int $user_id): string |
| | | { |
| | | $token = wp_generate_password(32, false); |
| | | set_transient(BASE . 'csrf_' . $user_id, $token, HOUR_IN_SECONDS); |
| | | return $token; |
| | | } |
| | | |
| | | /** |
| | | * Validate CSRF token from request header |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @return bool True if valid or not required |
| | | */ |
| | | protected function validateCSRFToken(WP_REST_Request $request): bool |
| | | { |
| | | // Only for authenticated requests |
| | | if (!is_user_logged_in()) { |
| | | return true; |
| | | } |
| | | |
| | | // Only for state-changing operations |
| | | if (in_array($request->get_method(), ['GET', 'HEAD', 'OPTIONS'])) { |
| | | return true; |
| | | } |
| | | |
| | | $user_id = get_current_user_id(); |
| | | $token = $request->get_header('X-CSRF-Token'); |
| | | $stored = get_transient(BASE . 'csrf_' . $user_id); |
| | | |
| | | if (empty($stored) || empty($token)) { |
| | | return false; |
| | | } |
| | | |
| | | return hash_equals($stored, $token); |
| | | } |
| | | |
| | | /** |
| | | * Get current CSRF token for user (for frontend to use) |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return string|null CSRF token or null |
| | | */ |
| | | protected function getCSRFToken(int $user_id): ?string |
| | | { |
| | | $token = get_transient(BASE . 'csrf_' . $user_id); |
| | | |
| | | if (!$token) { |
| | | $token = $this->generateCSRFToken($user_id); |
| | | } |
| | | |
| | | return $token; |
| | | } |
| | | |
| | | /*********************************************************** |
| | | * REQUEST SIGNATURE VERIFICATION |
| | | ***********************************************************/ |
| | | /** |
| | | * Verify request signature for sensitive operations |
| | | * Adds an additional layer of security for critical endpoints |
| | | * |
| | | * @param WP_REST_Request $request The REST request |
| | | * @param array $sensitive_params Params to include in signature |
| | | * @return bool True if valid signature |
| | | */ |
| | | protected function verifyRequestSignature(WP_REST_Request $request, array $sensitive_params): bool |
| | | { |
| | | $signature = $request->get_header('X-Request-Signature'); |
| | | if (empty($signature)) { |
| | | return false; |
| | | } |
| | | |
| | | // Get user's signing key (rotated on each login) |
| | | $user_id = get_current_user_id(); |
| | | if (!$user_id) { |
| | | return false; |
| | | } |
| | | |
| | | $signing_key = get_user_meta($user_id, BASE . 'signing_key', true); |
| | | if (empty($signing_key)) { |
| | | // Generate new key if missing |
| | | $signing_key = wp_generate_password(64, false); |
| | | update_user_meta($user_id, BASE . 'signing_key', $signing_key); |
| | | } |
| | | |
| | | // Build signature from sensitive params |
| | | ksort($sensitive_params); |
| | | $message = json_encode($sensitive_params); |
| | | $expected = hash_hmac('sha256', $message, $signing_key); |
| | | |
| | | return hash_equals($expected, $signature); |
| | | } |
| | | |
| | | /** |
| | | * Rotate signing key (call on login) |
| | | * |
| | | * @param int $user_id User ID |
| | | * @return string New signing key |
| | | */ |
| | | protected function rotateSigningKey(int $user_id): string |
| | | { |
| | | $new_key = wp_generate_password(64, false); |
| | | update_user_meta($user_id, BASE . 'signing_key', $new_key); |
| | | return $new_key; |
| | | } |
| | | /********************************************************** |
| | | * AUDIT LOGGING |
| | | **********************************************************/ |
| | | /** |
| | | * Log security-relevant events |
| | | * |
| | | * @param string $event Event name |
| | | * @param array $data Additional event data |
| | | * @return void |
| | | */ |
| | | protected function auditLog(string $event, array $data = []): void |
| | | { |
| | | // Add standard context |
| | | $context = array_merge($data, [ |
| | | 'timestamp' => current_time('mysql'), |
| | | 'user_id' => get_current_user_id() ?: 0, |
| | | 'ip' => $_SERVER['REMOTE_ADDR'] ?? '', |
| | | 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', |
| | | ]); |
| | | |
| | | // Use existing error handler |
| | | try { |
| | | JVB()->error()->log( |
| | | 'security_audit', |
| | | $event, |
| | | $context, |
| | | 'info' |
| | | ); |
| | | } catch (\Exception $e) { |
| | | // Fallback to error_log if handler fails |
| | | error_log("Security Audit: {$event} - " . json_encode($context)); |
| | | } |
| | | } |
| | | |
| | | /*************************************************************** |
| | | * SANITIZATION HELPERS |
| | | ***************************************************************/ |
| | | /** |
| | | * Sanitize array of IDs |
| | | * |
| | | * @param array $ids Array of IDs |
| | | * @return array Sanitized array of integers |
| | | */ |
| | | protected function sanitizeIDs(array $ids): array |
| | | { |
| | | return array_map('absint', array_filter($ids, function($id) { |
| | | return is_numeric($id) && $id > 0; |
| | | })); |
| | | } |
| | | |
| | | /*************************************************************** |
| | | * RESPONSE HELPERS |
| | | ***************************************************************/ |
| | | /** |
| | | * Return validation error response |
| | | * |
| | | * @param array $errors Validation errors (field => message) |
| | | * @return WP_REST_Response 422 response with errors |
| | | */ |
| | | protected function validationError(array $errors): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => 'Validation failed', |
| | | 'errors' => $errors |
| | | ], 422); |
| | | } |
| | | |
| | | /** |
| | | * Return unauthorized error response |
| | | * |
| | | * @param string $message Error message |
| | | * @return WP_REST_Response 401 response |
| | | */ |
| | | protected function unauthorized(string $message = 'Unauthorized'): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => $message |
| | | ], 401); |
| | | } |
| | | |
| | | /** |
| | | * Return forbidden error response |
| | | * |
| | | * @param string $message Error message |
| | | * @return WP_REST_Response 403 response |
| | | */ |
| | | protected function forbidden(string $message = 'Forbidden'): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => $message |
| | | ], 403); |
| | | } |
| | | |
| | | /** |
| | | * Return not found error response |
| | | * |
| | | * @param string $message Error message |
| | | * @return WP_REST_Response 404 response |
| | | */ |
| | | protected function notFound(string $message = 'Not found'): WP_REST_Response |
| | | { |
| | | return new WP_REST_Response([ |
| | | 'success' => false, |
| | | 'message' => $message |
| | | ], 404); |
| | | } |
| | | |
| | | /***************************************************************** |
| | | * CONCURRENT CHECKS |
| | | *****************************************************************/ |
| | | /** |
| | | * Prevent concurrent requests for the same operation |
| | | * Useful for preventing double-submissions |
| | | * |
| | | * @param string $operation_key Unique key for this operation |
| | | * @param int $lock_duration Lock duration in seconds |
| | | * @return bool True if lock acquired, false if already locked |
| | | */ |
| | | protected function acquireOperationLock(string $operation_key, int $lock_duration = 5): bool |
| | | { |
| | | $lock_key = 'op_lock_' . md5($operation_key); |
| | | |
| | | // Try to acquire lock |
| | | $locked = get_transient($lock_key); |
| | | if ($locked) { |
| | | return false; // Already locked |
| | | } |
| | | |
| | | // Set lock |
| | | set_transient($lock_key, true, $lock_duration); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Release operation lock |
| | | * |
| | | * @param string $operation_key Unique key for this operation |
| | | * @return void |
| | | */ |
| | | protected function releaseOperationLock(string $operation_key): void |
| | | { |
| | | $lock_key = 'op_lock_' . md5($operation_key); |
| | | delete_transient($lock_key); |
| | | } |
| | | |
| | | protected function verifyTurnstile(string $token): bool |
| | | { |
| | | if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) { |
| | | return true; |
| | | } |
| | | |
| | | if (empty($token)) { |
| | | return false; |
| | | } |
| | | |
| | | return JVB()->connect('cloudflare')->verifyTurnstile($token); |
| | | } |
| | | } |
| | | // |
| | | //Simple example: |
| | | //public function getTattoos(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // // Check HTTP cache headers first |
| | | // $cache_check = $this->checkHeaders($request, 'tattoo'); |
| | | // if ($cache_check) { |
| | | // return $cache_check; // Returns 304 Not Modified |
| | | // } |
| | | // |
| | | // // Get data (use CacheManager for data caching too!) |
| | | // $filters = $request->get_params(); |
| | | // $cache = CacheManager::for('tattoo'); |
| | | // |
| | | // $tattoos = $cache->remember($filters, function() use ($filters) { |
| | | // return $this->queryTattoos($filters); |
| | | // }, 300); |
| | | // |
| | | // $response = new WP_REST_Response(['items' => $tattoos]); |
| | | // return $this->addCacheHeaders($response); // Add ETag and Last-Modified |
| | | //} |
| | | // |
| | | //Multiple Content Types: |
| | | //public function getTermsWithContent(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // $taxonomy = $request->get_param('taxonomy'); |
| | | // |
| | | // // Check both taxonomy and its content types |
| | | // $cache_check = $this->checkHeaders($request, [$taxonomy, 'tattoo', 'artwork']); |
| | | // if ($cache_check) { |
| | | // return $cache_check; |
| | | // } |
| | | // |
| | | // // ... fetch data ... |
| | | // |
| | | // $response = new WP_REST_Response($data); |
| | | // return $this->addCacheHeaders($response); |
| | | //} |
| | | // |
| | | //User-specific: |
| | | //public function getUserFavorites(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // $user_id = $request->get_param('user'); |
| | | // |
| | | // // Automatically checks user_{$user_id} timestamp + includes user_id in ETag |
| | | // $cache_check = $this->checkUserHeaders($request, $user_id); |
| | | // if ($cache_check) { |
| | | // return $cache_check; |
| | | // } |
| | | // |
| | | // // Get user's favorites (cached per user) |
| | | // $favorites = CacheManager::forUser($user_id)->remember('favorites', function() use ($user_id) { |
| | | // return $this->getUserFavorites($user_id); |
| | | // }, 1800); |
| | | // |
| | | // $response = new WP_REST_Response(['items' => $favorites]); |
| | | // return $this->addCacheHeaders($response); |
| | | //} |
| | | // |
| | | //Complex with additional params: |
| | | //public function getFilteredContent(WP_REST_Request $request): WP_REST_Response |
| | | //{ |
| | | // $user_id = get_current_user_id(); |
| | | // $filters = $request->get_params(); |
| | | // |
| | | // // Include custom params in ETag for uniqueness |
| | | // $cache_check = $this->checkHeaders( |
| | | // $request, |
| | | // 'tattoo', |
| | | // [ |
| | | // 'user_id' => $user_id, |
| | | // 'is_verified' => $this->isVerifiedUser($user_id) |
| | | // ] |
| | | // ); |
| | | // |
| | | // if ($cache_check) { |
| | | // return $cache_check; |
| | | // } |
| | | // |
| | | // // ... fetch filtered data ... |
| | | // |
| | | // $response = new WP_REST_Response($data); |
| | | // return $this->addCacheHeaders($response); |
| | | //} |
| | | |
| | | |
| | | |
| | | /** |
| | | * Use operation lock in your methods like this: |
| | | * |
| | | * public function updateContent(WP_REST_Request $request): WP_REST_Response |
| | | * { |
| | | * $user_id = get_current_user_id(); |
| | | * $content_id = $request->get_param('content_id'); |
| | | * |
| | | * // Prevent concurrent updates |
| | | * $lock_key = "update_{$user_id}_{$content_id}"; |
| | | * if (!$this->acquireOperationLock($lock_key)) { |
| | | * return $this->error( |
| | | * 'An update is already in progress. Please wait.', |
| | | * 'concurrent_operation', |
| | | * 409 |
| | | * ); |
| | | * } |
| | | * |
| | | * try { |
| | | * // Do your operation |
| | | * $result = $this->doUpdate($content_id); |
| | | * |
| | | * $this->releaseOperationLock($lock_key); |
| | | * return $this->success($result); |
| | | * |
| | | * } catch (\Exception $e) { |
| | | * $this->releaseOperationLock($lock_key); |
| | | * return $this->error($e->getMessage(), 'operation_failed'); |
| | | * } |
| | | * } |
| | | */ |