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/Rest.php | 915 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 915 insertions(+), 0 deletions(-)
diff --git a/inc/rest/Rest.php b/inc/rest/Rest.php
index e69de29..4741e66 100644
--- a/inc/rest/Rest.php
+++ b/inc/rest/Rest.php
@@ -0,0 +1,915 @@
+<?php
+namespace JVBase\rest;
+
+use JVBase\Instance;
+use JVBase\integrations\services\Cloudflare;
+use JVBase\managers\Cache;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\base\Site;
+use WP_REST_Request;
+use WP_REST_Response;
+use Exception;
+
+if (!defined('ABSPATH')) {
+ exit;
+}
+
+/**
+ * Base REST Route Manager
+ *
+ * Provides shared utilities for route handlers. Route registration
+ * should use the Route builder class.
+ *
+ * Responsibilities:
+ * - Cache management (headers, invalidation)
+ * - Query building helpers
+ * - Validation utilities
+ * - Audit logging
+ */
+abstract class Rest
+{
+ protected string $namespace = 'jvb/v1';
+ protected ?Cache $cache = null;
+ protected string $cacheName = '';
+ protected int $cacheTtl = HOUR_IN_SECONDS;
+ protected static ?string $action;
+
+ public function __construct()
+ {
+ if ($this->cacheName !== '') {
+ $this->cache = Cache::for($this->cacheName, $this->cacheTtl);
+ }
+
+ add_action('rest_api_init', [$this, 'registerRoutes'], 10);
+ add_action('rest_api_init', [$this, 'ensureDefinitions'], 9);
+ }
+
+ public function ensureDefinitions():void
+ {
+ Site::getInstance();
+ Registrar::ensureInstanced();
+ }
+ /**
+ * Register routes - implement using Route builder
+ */
+ abstract public function registerRoutes(): void;
+
+ // =========================================================================
+ // RESPONSE HELPERS
+ // =========================================================================
+
+ protected function success(array $data = [], int $status = 200): WP_REST_Response
+ {
+ return Response::success($data, $status);
+ }
+
+ protected function error(string $message, string $code = 'error', int $status = 400, ?string $field = null): WP_REST_Response
+ {
+ return Response::error($message, $code, $status, $field);
+ }
+
+ protected function validationError(array $errors): WP_REST_Response
+ {
+ return Response::validationError($errors);
+ }
+
+ protected function notFound(string $message = 'Not found'): WP_REST_Response
+ {
+ return Response::notFound($message);
+ }
+
+ protected function forbidden(string $message = 'Forbidden'): WP_REST_Response
+ {
+ return Response::forbidden($message);
+ }
+
+ protected function unauthorized(string $message = 'Unauthorized'): WP_REST_Response
+ {
+ return Response::unauthorized($message);
+ }
+
+ protected function queued(string $operationId, string $message = 'Queued for processing'): WP_REST_Response
+ {
+ return Response::queued($operationId, $message);
+ }
+
+ // =========================================================================
+ // CACHE MANAGEMENT
+ // =========================================================================
+
+ protected function checkCache(string $key, $request):WP_REST_Response|false
+ {
+ // Check HTTP cache headers with the specific content type
+ $cache_check = $this->checkHeaders($request, $key);
+ if ($cache_check) {
+ return $cache_check;
+ }
+
+ $cache = $this->cache->get($key);
+ if ($cache) {
+ $response = Response::success($cache);
+ return $this->addCacheHeaders($response);
+ }
+ return false;
+ }
+ /**
+ * Check request headers for conditional caching (ETag, If-Modified-Since)
+ */
+ protected function checkHeaders(WP_REST_Request $request, string $cacheKey): ?WP_REST_Response
+ {
+ if (!$this->cache) {
+ return null;
+ }
+
+ $cached = $this->cache->get($cacheKey);
+ if (!$cached) {
+ return null;
+ }
+
+ $etag = $request->get_header('If-None-Match');
+ $cachedEtag = $cached['etag'] ?? null;
+
+ if ($etag && $cachedEtag && $etag === $cachedEtag) {
+ return new WP_REST_Response(null, 304);
+ }
+
+ $ifModifiedSince = $request->get_header('If-Modified-Since');
+ $lastModified = $cached['last_modified'] ?? null;
+
+ if ($ifModifiedSince && $lastModified) {
+ if (strtotime($ifModifiedSince) >= strtotime($lastModified)) {
+ return new WP_REST_Response(null, 304);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Add cache headers to response
+ */
+ protected function addCacheHeaders(WP_REST_Response $response, int $maxAge = 300): WP_REST_Response
+ {
+ $response->header('Cache-Control', "private, max-age={$maxAge}");
+ $response->header('Vary', 'Cookie');
+ $response->header('ETag', '"' . md5(serialize($response->get_data())) . '"');
+ $response->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
+
+ return $response;
+ }
+
+ /**
+ * Store response in cache with metadata
+ */
+ protected function cacheResponse(string $key, array $data): void
+ {
+ if (!$this->cache) {
+ return;
+ }
+
+ $this->cache->set($key, [
+ 'data' => $data,
+ 'etag' => '"' . md5(serialize($data)) . '"',
+ 'last_modified' => gmdate('D, d M Y H:i:s') . ' GMT',
+ ]);
+ }
+
+ // =========================================================================
+ // TIMESTAMP FORMATTING
+ // =========================================================================
+
+ /**
+ * Convert MySQL datetime to ISO 8601 timestamp
+ */
+ public function formatTimestamp(?string $mysqlDatetime): ?string
+ {
+ return Response::formatTimestamp($mysqlDatetime);
+ }
+
+ // =========================================================================
+ // QUERY BUILDING
+ // =========================================================================
+
+ /**
+ * Apply taxonomy filters to WP_Query args
+ */
+ 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);
+ }
+
+ $taxonomies = $data['taxonomies'] ?? $data['taxonomy'] ?? [];
+ $taxQuery = [];
+
+ foreach($taxonomies as $taxonomy => $terms) {
+ // Better validation: check if taxonomy actually exists
+ if (!taxonomy_exists(jvbCheckBase($taxonomy))) {
+ continue;
+ }
+
+ $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;
+ }
+
+ /**
+ * Apply order/sort filters to WP_Query 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 = floor(time() / 1800);
+ $args['orderby'] = 'RAND(' . $current_seed . ')';
+ unset($args['order']);
+ return $args;
+ }
+
+ 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':
+ $args['meta_key'] = BASE.'upvotes';
+ $args['orderby'] = 'meta_value_num';
+ break;
+ case 'karma':
+ $args['meta_key'] = BASE.'karma';
+ $args['orderby'] = 'meta_value_num';
+ break;
+ 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';
+ $args['order'] = (in_array($order, ['ASC', 'DESC'])) ? $order : 'DESC';
+
+ 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
+ $registrar = Registrar::getInstance($content);
+ if (!$registrar) {
+ return null;
+ }
+
+ // Check if this orderby is a custom order
+ $customOrders = $registrar->custom_order??[];
+ if (empty($customOrders) || !isset($customOrders[$orderby])) {
+ return null;
+ }
+
+ // Get field definition
+ $fields = $registrar->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 applyDateFilters(array $args, array $data):array
+ {
+ if (!array_key_exists('date-filter', $data) && !array_key_exists('dateFrom', $data)) {
+ return $args;
+ }
+ if (array_key_exists('dateFrom', $data)) {
+ $dateFrom = strtotime(sanitize_text_field($data['dateFrom']));
+ $dateTo = strtotime(sanitize_text_field($data['dateTo']));
+ if ($dateFrom && $dateTo) {
+ $args['date_query'] = [
+ [
+ 'after' => date('c', $dateFrom),
+ 'before' => date('c', $dateTo),
+ 'inclusive' => true,
+ ]
+ ];
+ }
+ } else {
+ switch ($data['date-filter']) {
+ case 'today':
+ $args['date_query'] = [['after' => '1 day ago']];
+ break;
+ case 'week':
+ $args['date_query'] = [['after' => '1 week ago']];
+ break;
+ case 'month':
+ $args['date_query'] = [['after' => '1 month ago']];
+ break;
+ case 'year':
+ $args['date_query'] = [['after' => '1 year ago']];
+ break;
+ }
+ }
+ return $args;
+ }
+
+ protected function applyCalendarFilters(array $args, array $data):array
+ {
+ $meta_query = [];
+ $today = date('Y-m-d');
+ if (in_array('future', $args['post_status'])) {
+ $meta_query[] = [
+ 'key' => 'jvb_start_date',
+ 'value' => $today,
+ 'compare' => '>=',
+ 'type' => 'DATE'
+ ];
+ }
+ if (in_array('past', $args['post_status'])) {
+ $meta_query[] = [
+ 'key' => 'jvb_end_date',
+ 'value' => $today,
+ 'compare' => '<',
+ 'type' => 'DATE'
+ ];
+ }
+ if (in_array('recurring', $args['post_status'])) {
+ $meta_query[] = [
+ 'key' => 'jvb_is_recurring',
+ 'value' => true,
+ 'compare' => '='
+ ];
+ }
+ if (!empty($meta_query)) {
+ $args['meta_query'] = (array_key_exists('meta_query', $args)) ? array_merge($args['meta_query'], $meta_query) : $meta_query;
+ }
+ return $args;
+
+ }
+
+ /**
+ * Apply pagination to WP_Query args
+ */
+ protected function applyPagination(array $args, array $data): array
+ {
+ $args['posts_per_page'] = min(absint($data['per_page'] ?? 20), 100);
+ $args['paged'] = max(absint($data['page'] ?? 1), 1);
+ return $args;
+ }
+
+ // =========================================================================
+ // VALIDATION
+ // =========================================================================
+
+ /**
+ * Check if user ID matches current logged-in user
+ */
+ protected function userCheck(int $userId): bool
+ {
+ return $userId === get_current_user_id();
+ }
+
+ /**
+ * Check if user exists (cached)
+ */
+ protected function checkUser(int $userId): bool
+ {
+ $cache = Cache::for('checkUser', DAY_IN_SECONDS)->connect('user');
+ return $cache->remember($userId, fn() => (bool) get_userdata($userId));
+ }
+
+ /**
+ * Check if term exists (cached)
+ */
+ protected function checkTerm(array $args): bool
+ {
+ $termId = $args['term_id'] ?? $args['to_term'] ?? false;
+ $taxonomy = $args['taxonomy'] ?? false;
+
+ if (!$termId || !$taxonomy) {
+ return false;
+ }
+
+ $cache = Cache::for('checkTerm', DAY_IN_SECONDS)->connect('taxonomy');
+ return $cache->remember($termId, fn() => (bool) term_exists($termId, jvbCheckBase($taxonomy)));
+ }
+
+ /**
+ * Check if user is verified
+ */
+ protected function isVerifiedUser(int $userId): bool
+ {
+ $cache = Cache::for('verifiedUsers', DAY_IN_SECONDS)->connect('user');
+ return $cache->remember($userId, fn() => user_can($userId, 'skip_moderation'));
+ }
+
+ /**
+ * Sanitize array of IDs
+ */
+ protected function sanitizeIds(array $ids): array
+ {
+ return array_values(array_filter(array_map('absint', $ids), fn($id) => $id > 0));
+ }
+
+ /**
+ * Get and validate meta values
+ */
+ protected function getMetaValues(mixed $value): mixed
+ {
+ $decoded = is_string($value) ? json_decode($value, true) : $value;
+
+ if (!is_array($decoded)) {
+ return $value;
+ }
+
+ return array_map(fn($item) => is_object($item) ? (array) $item : $item, $decoded);
+ }
+
+ /***************************************************************************
+ * UTILITY
+ ***************************************************************************/
+ protected function isTimeline($args, $data):bool
+ {
+ if (!array_key_exists('post_type', $args)) {
+ return false;
+ }
+ $post_types = is_array($args['post_type']) ? $args['post_type'] : [$args['post_type']];
+ $hasTimeline = array_map(function($item) { return jvbCheckBase($item); },Registrar::withFeature('is_timeline', 'post'));
+ return !empty(array_intersect($post_types, $hasTimeline));
+ }
+ // =========================================================================
+ // SECURITY
+ // =========================================================================
+
+ /**
+ * Verify Cloudflare Turnstile token
+ */
+ protected function verifyTurnstile(string $token): bool
+ {
+ if (!Site::hasIntegration('cloudflare') || !Cloudflare::getInstance()->isSetUp()) {
+ return true;
+ }
+
+ return !empty($token) && Cloudflare::getInstance()->verifyTurnstile($token);
+ }
+
+ /**
+ * Generate CSRF token for user
+ */
+ protected function generateCsrfToken(int $userId): string
+ {
+ $token = wp_generate_password(32, false);
+ set_transient(BASE . 'csrf_' . $userId, $token, HOUR_IN_SECONDS);
+ return $token;
+ }
+
+ /**
+ * Validate CSRF token from request header
+ */
+ protected function validateCsrfToken(WP_REST_Request $request): bool
+ {
+ if (!is_user_logged_in() || in_array($request->get_method(), ['GET', 'HEAD', 'OPTIONS'])) {
+ return true;
+ }
+
+ $userId = get_current_user_id();
+ $token = $request->get_header('X-CSRF-Token');
+ $stored = get_transient(BASE . 'csrf_' . $userId);
+
+ return !empty($stored) && !empty($token) && hash_equals($stored, $token);
+ }
+
+ // =========================================================================
+ // OPERATION LOCKING
+ // =========================================================================
+
+ /**
+ * Prevent concurrent requests for the same operation
+ */
+ protected function acquireOperationLock(string $key, int $duration = 5): bool
+ {
+ $lockKey = 'op_lock_' . md5($key);
+
+ if (get_transient($lockKey)) {
+ return false;
+ }
+
+ set_transient($lockKey, true, $duration);
+ return true;
+ }
+
+ /**
+ * Release operation lock
+ */
+ protected function releaseOperationLock(string $key): void
+ {
+ delete_transient('op_lock_' . md5($key));
+ }
+
+ // =========================================================================
+ // LOGGING
+ // =========================================================================
+
+ /**
+ * Log security-relevant events
+ */
+ protected function auditLog(string $event, array $data = []): void
+ {
+ $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'] ?? '',
+ ]);
+
+ try {
+ JVB()->error()->log('security_audit', $event, $context, 'info');
+ } catch (Exception $e) {
+ error_log("Security Audit: {$event} - " . json_encode($context));
+ }
+ }
+
+ /**
+ * Log errors with proper context
+ */
+ protected function logError(string $message, array $context = [], string $severity = 'error'): void
+ {
+ try {
+ JVB()->error()->log(static::class, $message, $context, $severity);
+ } catch (Exception $e) {
+ error_log(static::class . " Error: {$message} - " . json_encode($context));
+ }
+ }
+
+ /************************************************************
+ 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');
+ }
+
+ /*******************************
+ * META HELPERS
+ *******************************/
+ public function getFieldsOfType(array $fields, string|array $type, Meta $meta, array $subType = []):array
+ {
+ $gotFields = [];
+ if (is_string($type)) {
+ $type = [$type];
+ }
+ foreach ($fields as $field => $value) {
+ //Skip empty values
+ if (empty($value)) {
+ continue;
+ }
+ $config = $meta->config($field);
+ if (in_array($config['type'], ['group', 'repeater', 'tagList'])) {
+ foreach ($config['fields'] as $subfield => $subConfig) {
+ if (is_numeric($subfield) && array_key_exists('name', $subConfig)) {
+ $subfield = $subConfig['name'];
+ }
+ if (is_numeric($subfield)) continue;
+ if (array_key_exists('type', $subConfig) && in_array($subConfig['type'], $type)) {
+ $gotFields[] = $field.':'.$subfield;
+ }
+ }
+ } elseif (in_array($config['type'], $type)) {
+ $gotFields[] = $field;
+ } else if ((!empty($subType) && in_array($config['type'], array_keys($subType)) && in_array($config['subtype'], array_values($subType)))) {
+ $gotFields[] = $field;
+ }
+ }
+ return $gotFields;
+ }
+
+
+ protected function extractImages(array $fields, Meta $meta):array
+ {
+ $images = [];
+ $get = $this->getFieldsOfType($fields, ['upload', 'gallery','image'], $meta);
+ if (!empty($get)) {
+ $baseFields = array_map(function($fieldName) {
+ return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName;
+ }, $get);
+
+ $temp = array_map(
+ function($item) {
+ return explode(':', $item);
+ },
+ array_filter($get, function($fieldName) {
+ return str_contains($fieldName, ':');
+ })
+ );
+ $complex = [];
+ foreach ($temp as $tmp) {
+ $complex[$tmp[0]] = $tmp[1];
+ }
+
+ $fields = array_filter($fields, function ($field) use ($baseFields) {
+ return in_array($field, $baseFields);
+ }, ARRAY_FILTER_USE_KEY);
+
+ foreach ($fields as $fieldName => $value) {
+ //Check if it's a complex field
+ if (array_key_exists($fieldName, $complex)) {
+ $check = $complex[$fieldName];
+ foreach ($value as $row) {
+ foreach ($row as $fName => $fValue) {
+ if ($fName === $check && !empty($fValue)) {
+ $images = $this->addImages($fValue, $images);
+ }
+ }
+ }
+ } else {
+ $images = $this->addImages($value, $images);
+ }
+ }
+ }
+ return $images;
+ }
+ public function addImages(string $imgs, array $images):array
+ {
+ $temp = explode(',', $imgs);
+ foreach ($temp as $img) {
+ if (is_numeric($img) && !array_key_exists($img, $images) && $img > 0) {
+ $images[$img] = jvbImageData((int)$img);
+ }
+ }
+ return $images;
+ }
+
+ protected function extractTerms(array $fields, Meta $meta):array
+ {
+ $terms = [];
+ $get = $this->getFieldsOfType($fields, ['taxonomy'], $meta, ['selector' => 'taxonomy']);
+ if (!empty($get)) {
+ $baseFields = array_map(function($fieldName) {
+ return (str_contains($fieldName, ':')) ? strtok($fieldName, ':') : $fieldName;
+ }, $get);
+
+ $complex = array_map(
+ function($item) {
+ return explode(':', $item);
+ },
+ array_filter($get, function($fieldName) {
+ return str_contains($fieldName, ':');
+ })
+ );
+
+ $fields = array_filter($fields, function ($field) use ($baseFields) {
+ return in_array($field, $baseFields);
+ }, ARRAY_FILTER_USE_KEY);
+
+ foreach ($fields as $fieldName => $value) {
+ $config = $meta->config($fieldName);
+ //Check if it's a complex field
+ if (array_key_exists($fieldName, $complex)) {
+ foreach ($value as $row) {
+ foreach ($row as $fName => $fValue) {
+ if (in_array($fName, $complex[$fieldName])) {
+ $terms = $this->addTerms($fValue, $terms, $config);
+ }
+ }
+ }
+ } else {
+
+ $terms = $this->addTerms($value, $terms, $config);
+ }
+ }
+ }
+ return $terms;
+
+ }
+
+ protected function addTerms(string $value, array $terms, array $config):array
+ {
+ $taxonomy = jvbNoBase($config['taxonomy']);
+ if (empty($value)) {
+ return $terms;
+ }
+ $ids = array_map('absint', explode(',',$value));
+ $cache = Cache::for('term_data')->connect('taxonomy');
+ $cache->flush();
+ if (!array_key_exists($taxonomy, $terms)) {
+ $terms[$taxonomy] = [];
+ $registrar = Registrar::getInstance($taxonomy);
+ $terms[$taxonomy]['icon'] = $registrar ? $registrar->getIcon() : jvbDefaultIcon();;
+ }
+ foreach ($ids as $id) {
+ $data = $cache->remember(
+ $id,
+ function () use ($id, $taxonomy) {
+ $term = get_term($id, $taxonomy);
+ if ($term && !is_wp_error($term)) {
+ return [
+ 'id' => $term->term_id,
+ 'name' => $term->name,
+ 'slug' => $term->slug,
+ 'parent' => $term->parent,
+ 'path' => JVB()->routes('term')->getTermPath($term->term_id, $term->name, $taxonomy),
+ 'taxonomy' => jvbNoBase($term->taxonomy),
+ 'count' => $term->count,
+ ];
+ }
+ return [];
+ }
+ );
+ if (!empty($data)) {
+ $terms[$taxonomy][$id] = $data;
+ }
+ }
+ return $terms;
+ }
+}
--
Gitblit v1.10.0