| | |
| | | $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 |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * @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]); |
| | | } |
| | | } |
| | | // |
| | | //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); |
| | | //} |