From 47e77f9fac1155c536b2b87fec552c7fcce66fa6 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 01 Jun 2026 18:06:34 +0000
Subject: [PATCH] =Timeline block fixes. Next up: adding article schema classes
---
inc/rest/RestRouteManager.php | 498 +++++++++++++++++++++++++++----------------------------
1 files changed, 244 insertions(+), 254 deletions(-)
diff --git a/inc/rest/RestRouteManager.php b/inc/rest/RestRouteManager.php
index f017e9c..ff72f13 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::getFeatured('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::getFeatured('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,52 @@
*/
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)) . '"';
+ // Prefer tag freshness if available
+ $tags = $cache->getTags();
+
+ $lastModified = $tags
+ ? $cache->getLastModifiedForTags($tags)
+ : $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 +662,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 +1048,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 +1059,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