From 3d25ad7de4fdbb3eba2cde2a62422e6be5d79a0a Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 25 Aug 2026 22:57:22 +0000
Subject: [PATCH] =It's been a minute. I went through the Routes and associated Managers and ensured they played together well. Reworked the Cache.php, and I think I caught all the references. Moving away from JVBase() root class, to individuall classes that are Instanced. Double checked for the ABSPATH check in every file, and added index.php to every directory that didn't already have it. Also still working on the MenuBlock to work with WooCommerce for now for Northeh

---
 inc/rest/RestRouteManager.php |  495 ++++++++++++++++++++++++++----------------------------
 1 files changed, 241 insertions(+), 254 deletions(-)

diff --git a/inc/rest/RestRouteManager.php b/inc/rest/RestRouteManager.php
index f017e9c..e2efa84 100644
--- a/inc/rest/RestRouteManager.php
+++ b/inc/rest/RestRouteManager.php
@@ -3,12 +3,11 @@
 
 use DateTime;
 use DateTimeZone;
-use JVBase\JVB;
-use JVBase\rest\RateLimiter;
+use JVBase\registrar\Registrar;
 use JVBase\managers\OperationQueue;
-use JVBase\managers\CacheManager;
+use JVBase\managers\Cache;
 use JVBase\managers\NotificationManager;
-use JVBase\utility\Features;
+use JVBase\base\Site;
 use WP_REST_Request;
 use WP_Error;
 use Exception;
@@ -19,6 +18,7 @@
 }
 
 /**
+ * @deprecated use Rest.php
  * Handles route registration and high-level coordination
  */
 abstract class RestRouteManager
@@ -30,12 +30,12 @@
     protected string $route;
     protected string $base;
     protected string $content_type; //the registered post type
-    protected string $type; //post, user, term, for MetaManager
+    protected string $type; //post, user, term, for Meta
     protected string $action = ''; //optional additional nonce to check
     protected array $callback; //route->callback array
     protected string $operation_type; // from QueueManager.js and OperationQueue.php
     protected OperationQueue $queue;
-    protected CacheManager $cache;
+    protected Cache $cache;
     protected NotificationManager $notifications;
     protected string $cache_name ='';
     protected int $cache_ttl = 3600; //1 hour default
@@ -53,7 +53,7 @@
         $this->base = BASE;
         $this->rate_limiter = new RateLimiter();
         if ($this->cache_name !== '') {
-            $this->cache = CacheManager::for($this->cache_name, $this->cache_ttl);
+            $this->cache = Cache::for($this->cache_name, $this->cache_ttl);
         }
         add_action('rest_api_init', [$this, 'registerRoutes']);
     }
@@ -158,11 +158,7 @@
 
 	protected function checkContent(string $content, bool $bool = false):string|bool
 	{
-		$result = JVB_CONTENT[$content]??JVB_TAXONOMY[$content]??JVB_USER[$content]??'';
-		if ($bool) {
-			return $result !== '';
-		}
-		return $result;
+		return (bool)Registrar::getInstance($content);
 	}
 
 
@@ -229,11 +225,11 @@
 	 */
 	protected function checkUser(int $userID): bool
 	{
-		$cache = CacheManager::for('users');
+		$cache = Cache::for('checkUser', DAY_IN_SECONDS)->connect('user');
 
-		return $cache->remember("user_exists_{$userID}", function() use ($userID) {
+		return $cache->remember($userID, function() use ($userID) {
 			return (bool)get_userdata($userID);
-		}, DAY_IN_SECONDS);
+		});
 	}
 
 	/**
@@ -241,11 +237,11 @@
 	 */
 	protected function checkShop(int $shopID): bool
 	{
-		$cache = CacheManager::for('shop');
+		$cache = Cache::for('checkShop',DAY_IN_SECONDS)->connect('taxonomy');
 
-		return $cache->remember("shop_exists_{$shopID}", function() use ($shopID) {
+		return $cache->remember($shopID, function() use ($shopID) {
 			return (bool)term_exists($shopID, BASE . 'shop');
-		}, DAY_IN_SECONDS);
+		});
 	}
 
 	/**
@@ -264,11 +260,11 @@
 		}
 
 		$taxonomy = jvbCheckBase($taxonomy);
-		$cache = CacheManager::for($taxonomy);
+		$cache = Cache::for('checkTerm', DAY_IN_SECONDS)->connect('taxonomy');
 
-		return $cache->remember("term_exists_{$termID}", function() use ($termID, $taxonomy) {
+		return $cache->remember($termID, function() use ($termID, $taxonomy) {
 			return (bool)term_exists($termID, $taxonomy);
-		}, DAY_IN_SECONDS);
+		});
 	}
 
 	/**
@@ -276,63 +272,101 @@
 	 */
 	public function isVerifiedUser(int $user_id): bool
 	{
-		$cache = CacheManager::forUser($user_id);
+		$cache = Cache::for('verifiedUsers', DAY_IN_SECONDS)->connect('user');
 
-		return $cache->remember('is_verified', function() use ($user_id) {
+		return $cache->remember($user_id, function() use ($user_id) {
 			return user_can($user_id, 'skip_moderation');
-		}, DAY_IN_SECONDS);
+		});
 	}
 
-    protected function applyTaxonomyFilters(array $args, array $data):array
-    {
-        $taxQuery = [];
-		foreach($data['taxonomies']??[] as $taxonomy => $terms) {
-			if (array_key_exists(jvbNoBase($taxonomy), JVB_TAXONOMY)) {
-				$taxQuery[] = [
-					'taxonomy'	=> jvbCheckBase($taxonomy),
-					'terms'		=> array_map(
-						'absint',
-						is_array($terms) ? $terms : explode(',', $terms)
-					)
-				];
-			}
+	/**
+	 * @deprecated
+	 * @param array $args
+	 * @param array $data
+	 * @return array
+	 */
+	protected function applyTaxonomyFilters(array $args, array $data):array
+	{
+		// Handle JSON-encoded taxonomy data
+		if (array_key_exists('taxonomy', $data) && is_string($data['taxonomy'])) {
+			$data['taxonomy'] = json_decode($data['taxonomy'], true);
 		}
 
-        if (!empty($taxQuery)) {
-            $args['tax_query'] = array_merge([
-                'relation'  => (array_key_exists('match', $data)) ? 'AND' : 'OR',
-            ], $taxQuery);
-        }
+		$taxonomies = $data['taxonomies'] ?? $data['taxonomy'] ?? [];
+		$taxQuery = [];
 
-        $authorQuery = [];
-        foreach (jvbAuthorUsers() as $type) {
-            if (array_key_exists($type, $data)) {
-                $artist_ids = array_map(
-                    'absint',
-                    is_array($data[$type]) ?
-                        $data[$type] :
-                        explode(',', $data[$type])
-                );
-                $authorQuery = array_merge($authorQuery, $artist_ids);
-            }
-        }
-        if (!empty($authorQuery)) {
-            $args['author__in'] = array_unique($authorQuery);
-        }
+		foreach($taxonomies as $taxonomy => $terms) {
+			// Better validation: check if taxonomy actually exists
+			if (!taxonomy_exists(jvbCheckBase($taxonomy))) {
+				continue;
+			}
 
-        return $args;
-    }
+			$taxQuery[] = [
+				'taxonomy'	=> jvbCheckBase($taxonomy),
+				'field'     => 'term_id',
+				'terms'		=> array_map(
+					'absint',
+					is_array($terms) ? $terms : explode(',', $terms)
+				),
+				'operator'  => 'IN'
+			];
+		}
+
+		if (!empty($taxQuery)) {
+			// Match 'all' = AND, anything else = OR
+			$relation = ($data['match'] ?? 'all') === 'all' ? 'AND' : 'OR';
+
+			$args['tax_query'] = array_merge([
+				'relation'  => $relation,
+			], $taxQuery);
+		}
+
+		// Keep existing author filtering logic
+		$authorQuery = [];
+		foreach (Registrar::withFeature('can_create', 'user') as $type) {
+			if (array_key_exists($type, $data)) {
+				$artist_ids = array_map(
+					'absint',
+					is_array($data[$type]) ?
+						$data[$type] :
+						explode(',', $data[$type])
+				);
+				$authorQuery = array_merge($authorQuery, $artist_ids);
+			}
+		}
+		if (!empty($authorQuery)) {
+			$args['author__in'] = array_unique($authorQuery);
+		}
+
+		return $args;
+	}
 
     protected function applyOrderFilters(array $args, array $data):array
     {
+		// Check for custom order first
+		$customArgs = $this->applyCustomOrder($args, $data);
+		if ($customArgs !== null) {
+			$order = (array_key_exists('order', $data)) ? strtoupper($data['order']) : 'DESC';
+			$customArgs['order'] = (in_array($order, ['ASC', 'DESC'])) ? $order : 'DESC';
+			return $customArgs;
+		}
+
+		//Handle random
         if (array_key_exists('orderby', $data) && $data['orderby'] === 'random') {
-            $current_seed = jvbGetRandomSeed();
+            $current_seed = floor(time() / 1800);
             $args['orderby'] = 'RAND(' . $current_seed . ')';
             unset($args['order']);
             return $args;
         }
-        if (in_array($data['orderby'], ['date', 'title', 'alphabetical'])) {
-            $args['orderby'] = ($data['orderby'] === 'alphabetical') ? 'title' : $data['orderby'];
+
+        if (in_array($data['orderby'], ['date', 'modified', 'title', 'alphabetical'])) {
+			if ($data['orderby'] === 'date' && $this->isTimeline($args, $data)) {
+				$args['meta_key'] = BASE . 'latest_date';
+				$args['orderby'] = 'meta_value_num';
+			} else {
+				$args['orderby'] = ($data['orderby'] === 'alphabetical') ? 'title' : $data['orderby'];
+			}
+
         } else {
             switch ($data['orderby']) {
                 case 'popularity':
@@ -343,8 +377,18 @@
                     $args['meta_key'] = BASE.'karma';
                     $args['orderby'] = 'meta_value_num';
                     break;
-                default:
-                    $args['orderby'] = 'date';
+				case 'unpopularity':
+					$args['meta_key'] = BASE.'downvotes';
+					$args['orderby'] = 'meta_value_num';
+					break;
+				case 'favourites':
+					$args['meta_key'] = BASE.'total_favourites';
+					$args['orderby'] = 'meta_value_num';
+					break;
+				case 'date':
+				default:
+					$args['orderby'] = 'date';
+					break;
             }
         }
 		$order = (array_key_exists('order', $data)) ? strtoupper($data['order']) : 'DESC';
@@ -353,6 +397,92 @@
         return $args;
     }
 
+	/**
+	 * Apply custom order if defined in content/taxonomy/user config
+	 *
+	 * @param array $args WP_Query args
+	 * @param array $data Request data
+	 * @return array|null Modified args if custom order found, null otherwise
+	 */
+	protected function applyCustomOrder(array $args, array $data): ?array
+	{
+		$orderby = $data['orderby'] ?? '';
+
+		// Skip if no orderby or it's a standard order
+		if (empty($orderby) || in_array($orderby, ['date', 'modified', 'title', 'alphabetical', 'random', 'popularity', 'karma', 'unpopularity', 'favourites'])) {
+			return null;
+		}
+
+		// Determine content type
+		$post_type = is_array($args['post_type']) ? $args['post_type'][0] : $args['post_type'];
+		$content = jvbNoBase($post_type);
+
+		// Get config for this content type
+		$config = Registrar::getInstance($content);
+		if (!$config) {
+			return null;
+		}
+
+		// Check if this orderby is a custom order
+		$customOrders = $config->custom_order ?? [];
+		if (empty($customOrders) || !isset($customOrders[$orderby])) {
+			return null;
+		}
+
+		// Get field definition
+		$fields = $config->getFields()??[];
+		if (!isset($fields[$orderby])) {
+			return null;
+		}
+
+		$field = $fields[$orderby];
+
+		// Set meta_key
+		$args['meta_key'] = BASE . $orderby;
+
+		// Determine orderby and meta_type based on field type
+		$fieldType = $field['type'] ?? 'text';
+		$subtype = $field['subtype'] ?? '';
+
+		switch ($fieldType) {
+			case 'number':
+				$args['orderby'] = 'meta_value_num';
+				break;
+
+			case 'text':
+				$args['orderby'] = ($subtype === 'number') ? 'meta_value_num' : 'meta_value';
+				break;
+
+			case 'date':
+				$args['orderby'] = 'meta_value';
+				$args['meta_type'] = 'DATE';
+				break;
+
+			case 'datetime':
+				$args['orderby'] = 'meta_value';
+				$args['meta_type'] = 'DATETIME';
+				break;
+
+			case 'true_false':
+			case 'checkbox':
+				$args['orderby'] = 'meta_value';
+				$args['meta_type'] = 'BINARY';
+				break;
+
+			default:
+				$args['orderby'] = 'meta_value';
+		}
+
+		return $args;
+	}
+
+	protected function isTimeline($args, $data):bool
+	{
+		$post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']];
+		$areTimeline = array_map(function($type) { return BASE.$type; },Registrar::withFeature('is_timeline', 'post'));
+		return !empty(array_intersect($post_types, $areTimeline));
+	}
+
     protected function applyDateFilters(array $args, array $data):array
     {
         if (!array_key_exists('date-filter', $data) && !array_key_exists('dateFrom', $data)) {
@@ -444,55 +574,49 @@
 	 */
 	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 === $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) {
-			$if_modified_timestamp = strtotime($if_modified_since);
-			if ($last_modified <= $if_modified_timestamp) {
-				return $this->createNotModifiedResponse($etag, $last_modified);
-			}
-		}
-
-		// 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
+		int|string|array $key,
+		string|array $group = ''
+	): WP_REST_Response|false
 	{
-		// Combine all data that makes this response unique
-		$etag_data = array_merge(
-			$params,
-			$additional,
-			['t' => $timestamp]
-		);
+		$group = ($group!=='') ? $group : $this->cache_name;
+		$cache = $this->cache_name !== $group ? Cache::for($group) : $this->cache;
+		if (!$cache) {
+			return false;
+		}
+		if (is_array($key)) {
+			$key = $cache->generateKey($key);
+		}
 
-		return '"' . md5(serialize($etag_data)) . '"';
+
+
+		$lastModified = $cache::lastModified($group);
+
+		if (!$lastModified) {
+			return false;
+		}
+
+
+		$etag = '"' . sha1($group . ':' . $key . ':' . $lastModified) . '"';
+
+		// ETag check
+		$ifNoneMatch = $request->get_header('if-none-match');
+		if ($ifNoneMatch && trim($ifNoneMatch) === $etag) {
+			return new WP_REST_Response(null, 304);
+		}
+
+		// Last-Modified check
+		$ifModifiedSince = $request->get_header('if-modified-since');
+		if ($ifModifiedSince && strtotime($ifModifiedSince) >= $lastModified) {
+			return new WP_REST_Response(null, 304);
+		}
+
+		// Store headers for response phase
+		$this->response_headers = [
+			'ETag' => $etag,
+			'Last-Modified' => gmdate('D, d M Y H:i:s', $lastModified) . ' GMT',
+		];
+
+		return false;
 	}
 
 	/**
@@ -535,35 +659,16 @@
 	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
+			foreach ($this->response_headers as $name => $value) {
+				$response->header($name, $value);
+			}
+			$this->response_headers = [];
 		}
+
 		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
@@ -940,7 +1045,7 @@
 
 	protected function verifyTurnstile(string $token): bool
 	{
-		if (!Features::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
+		if (!Site::hasIntegration('cloudflare') || !JVB()->connect('cloudflare')->isSetUp()) {
 			return true;
 		}
 
@@ -951,121 +1056,3 @@
 		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');
- *     }
- * }
- */

--
Gitblit v1.10.0