Jake Vanderwerf
2025-11-04 42fa8304ddb811b0f725f245130f70c0f5e86a6c
inc/rest/RestRouteManager.php
@@ -50,7 +50,7 @@
        $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']);
    }
@@ -94,6 +94,7 @@
    {
        // 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',
@@ -102,6 +103,7 @@
        }
        $user_id = $request->get_param('user');
        if (!empty($user_id) && !$this->userCheck($user_id)) {
         error_log('Usercheck failed');
            return false;
        }
        // Verify nonces
@@ -180,82 +182,64 @@
        }
    }
    /**
     * @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
    {
@@ -405,27 +389,219 @@
      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);
//}