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/routes/ContentRoutes.php | 1302 +++++++++++++++++++++++------------------------------------
 1 files changed, 508 insertions(+), 794 deletions(-)

diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index dfa9cb6..7d5ef42 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -1,103 +1,142 @@
 <?php
+
 namespace JVBase\rest\routes;
 
-use JVBase\JVB;
-use JVBase\rest\RestRouteManager;
-use JVBase\managers\CacheManager;
-use JVBase\meta\MetaManager;
-use JVBase\utility\Features;
+use JVBase\Instance;
+use JVBase\managers\Cache;
+use JVBase\managers\queue\executors\ContentExecutor;
+use JVBase\managers\queue\Queue;
+use JVBase\managers\queue\TypeConfig;
+use JVBase\meta\Meta;
+use JVBase\registrar\Registrar;
+use JVBase\rest\PermissionHandler;
+use JVBase\rest\Response;
+use JVBase\rest\Rest;
+use JVBase\rest\Route;
 use WP_Post;
 use WP_Query;
-use WP_Error;
 use WP_REST_Request;
 use WP_REST_Response;
-use Exception;
+use WP_Term;
+use WP_Term_Query;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit; // Exit if accessed directly
 }
 
-class ContentRoutes extends RestRouteManager
+class ContentRoutes extends Rest
 {
-    protected array $fields = [];
-    protected array $taxonomies = [];
-    protected MetaManager $meta;
-    protected string $post_type = '';
-    protected string $user_id = '';
+	use Instance;
+	protected array $fields = [];
+	protected array $taxonomies = [];
+	protected string $post_type = '';
+	protected string $user_id = '';
 
 	//For Timeline-specific posts
 	protected array $timelineSharedFields = [];
 	protected array $timelineUniqueFields = [];
+	protected static ?string $action = 'dash-';
+	protected Meta $meta;
+	protected Cache $termCache;
 
-    //TODO: Ensure we are handling the bulk operations for all processes
-    //TODO: Also invalidate feed caches on updates!!
+	public function __construct()
+	{
+		$this->cacheName = 'user_content';
+		parent::__construct();
+		$this->cache = Cache::for($this->cacheName, 'post', DAY_IN_SECONDS);
+		$this->cache->user();
+		$this->termCache = Cache::for($this->cacheName.'_terms', 'term', DAY_IN_SECONDS);
+		$this->termCache->user();
 
-    public function __construct()
-    {
-        $this->cache_name = 'user_content_'.get_current_user_id();
-        parent::__construct();
 
-        $this->action = 'dash-';
-        $this->operation_type = 'content_update';
-        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
-    }
+		if (JVB_TESTING) {
+			$this->cache->flush();
+		}
+		add_action('init', [$this, 'registerContentExecutors'], 5);
+	}
 
-    /**
-     * Registers content routes
-     * @return void
-     */
-    public function registerRoutes():void
-    {
-        // Base content endpoint
-        register_rest_route($this->namespace, "/content", [
-            [
-                'methods' => 'GET',
-                'callback' => [$this, 'handleContentRequest'],
-                'permission_callback' => [$this, 'checkPermission'],
-            ],
-            [
-                'methods' => 'POST',
-                'callback' => [$this, 'handleContentUpdate'],
-                'permission_callback' => [$this, 'checkPermission']
-            ]
-        ]);
+	/**
+	 * Register content operation types with the queue's TypeRegistry
+	 */
+	public function registerContentExecutors(): void
+	{
+		$registry = Queue::getInstance()->registry();
+		$executor = new ContentExecutor();
 
-        //TODO: consolidate create/batch in with create? I don't think we are ever creating a single item
-        register_rest_route($this->namespace, "/create", [
-            [
-                'methods' => 'POST',
-                'callback' => [$this, 'handleContentCreate'],
-                'permission_callback' => [$this, 'checkPermission']
-            ]
-        ]);
-        register_rest_route($this->namespace, "/create/batch", [
-            [
-                'methods' => 'POST',
-                'callback' => [$this, 'handleBatchCreation'],
-                'permission_callback' => [$this, 'checkPermission']
-            ]
-        ]);
-    }
+		// Content updates - chunked at 10 posts
+		$registry->register('content_update', new TypeConfig(
+			executor: $executor,
+			chunkKey: 'posts',
+			chunkSize: 10
+		));
 
-	protected function initTimelineFields(string $content):void
+		// Batch creation (from uploads) TODO: I believe this is all handled by UploadExecutor
+//		$registry->register('batch_creation', new TypeConfig(
+//			executor: $executor
+//		));
+	}
+
+	/**
+	 * Registers content routes
+	 * @return void
+	 */
+	public function registerRoutes(): void
+	{
+		Route::for('content')
+			->get([$this, 'getContent'])
+			->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+			->args([
+				'content' => 'string|required',
+				'status' => 'string|default:all',
+				'page' => 'integer|default:1|min:1',
+				'per_page' => 'integer|default:30|min:1|max:100',
+				'orderby' => 'string|enum:date,alphabetical|default:date',
+				'order' => 'string|enum:asc,desc|default:desc',
+				'search' => 'string',
+				'date-filter' => 'string',
+				'dateFrom' => 'string',
+				'dateTo' => 'string',
+			])
+			->rateLimit(30)
+			->post([$this, 'postContent'])
+			->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+			->rateLimit()
+			->args([
+				'posts' => 'required',
+				'content' => 'string',
+			])
+			->register();
+	}
+
+	protected function initTimelineFields(string $content): void
 	{
 		$content = jvbNoBase($content);
-		if (!Features::forContent($content)->has('is_timeline')){
+
+		$config = Registrar::getInstance($content);
+		if (!$config || !$config->hasFeature('is_timeline')) {
 			return;
 		}
-		$config = Features::getConfig($content);
-		$this->fields = $config['fields'];
+		$this->fields = $config->getFields();
 
-		$this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
-			if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
-				return true;
-			}
-			return false;
-		}));
+		$this->timelineSharedFields = $this->getTimelineSharedFields($content);
 		array_unshift($this->timelineSharedFields, 'post_thumbnail');
 		array_unshift($this->timelineSharedFields, 'post_title');
+		array_unshift($this->timelineSharedFields, 'post_status');
 
-		$this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
+		$this->timelineUniqueFields = $this->getTimelineUniqueFields($content);
+	}
+
+	public function getTimelineUniqueFields(string $content): array
+	{
+		$content = jvbNoBase($content);
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || !$registrar->hasFeature('is_timeline')) {
+			return [];
+		}
+
+		$allFields = $registrar->getFields();
+
+		return array_keys(array_filter($allFields, function ($field) {
 			if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
 				return true;
 			}
@@ -105,806 +144,481 @@
 		}));
 	}
 
-    /**
-     * Handle content update/creation
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleContentUpdate(WP_REST_Request$request):WP_REST_Response
-    {
-        $data = $request->get_params();
-		error_log('Received data: '.print_r($data, true));
-        $user_id = $data['user'];
+	public function getTimelineSharedFields(string $content): array
+	{
+		$content = jvbNoBase($content);
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || !$registrar->hasFeature('is_timeline')) {
+			return [];
+		}
+
+		$allFields = $registrar->getFields()??[];
+
+		return array_keys(array_filter($allFields, function ($field) {
+			if (!array_key_exists('for_all', $field) || is_null($field['for_all']) || $field['for_all'] === false) {
+				return true;
+			}
+			return false;
+		}));
+	}
+
+	/**
+	 * Handle content update/creation
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function postContent(WP_REST_Request $request): WP_REST_Response
+	{
+		$data = $request->get_params();
+		$user_id = get_current_user_id();
+
+		if (!array_key_exists('posts', $data) || !is_array($data['posts'])) {
+			return Response::success(['message'=>'No posts found in request']);
+		}
 
 
-        if (!isset($data['posts']) || !is_array($data['posts'])) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   =>'Invalid request format'
-            ]);
-        }
+		$count = count($data['posts']);
+		$operationId = $data['id'];
+		unset($data['id']);
 
-        $count = count($data['posts']);
-        $operationId = $data['id'];
-        unset($data['user']);
-        unset($data['id']);
+		error_log('[CONTENT]:'.print_r($data, true));
+		$queue = Queue::getInstance();
+		$queue->queueOperation(
+			'content_update',
+			$user_id,
+			$data,
+			[
+				'operation_id' => $operationId
+			]
+		);
 
-        $queue = JVB()->queue();
-        $queue->queueOperation(
-            'content_update',
-            $user_id,
-            $data,
-            [
-                'count'   => $count,
-				'chunk_key'	=> 'posts',
-				'chunk_size' => 10,
-                'operation_id'      => $operationId
-            ]
-        );
-        return new WP_REST_Response([
-            'success'   => true,
-            'message'   => 'Queued for processing',
-            'operation' => $operationId
-        ]);
-    }
-
-    /**
-     * Handle content creation
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleContentCreate(WP_REST_Request$request):WP_REST_Response
-    {
-        $data = $request->get_json_params();
-        $user_id = $data['user'];
-
-        if (!isset($data['posts']) || !is_array($data['posts'])) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   =>  'Invalid request format'
-            ]);
-        }
-
-        $count = count($data['posts']);
-        $operationId = $data['id'];
-        unset($data['user']);
-        unset($data['id']);
-        JVB()->queue()->queueOperation(
-            'batch_creation',
-            $user_id,
-            $data,
-            [
-                'count'   => $count,
-                'operation_id'      => $operationId,
-            ]
-        );
-
-        return new WP_REST_Response([
-            'success'   => true,
-            'message'   => 'Queued for processing',
-            'operation' => $operationId
-        ]);
-    }
+		return Response::queued($operationId);
+	}
 
 
-    /**
-     * Handle request
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleContentRequest(WP_REST_Request $request):WP_REST_Response
-    {
-        $params = $request->get_params();
-		error_log('handleContentRequest params: '.print_r($params, true));
+	/**
+	 * Handle request
+	 * @param WP_REST_Request $request
+	 *
+	 * @return WP_REST_Response
+	 */
+	public function getContent(WP_REST_Request $request): WP_REST_Response
+	{
+		$params = $request->get_params();
+		error_log('getContent::params '.print_r($params, true));
 
-		error_log('Fetching content. Params: '.print_r($params, true));
-        $user_id = $params['user'];
-        if (!$this->userCheck($user_id)) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   => 'User does not match up. Are you a bot?',
-            ]);
-        }
+		$registrar = Registrar::getInstance($params['content']);
+		switch ($registrar->getType()) {
+			case 'term':
+				return $this->getTerms($request, $params, $registrar);
+			case 'user':
+				//TODO maybe do something?
+				break;
+			case 'post':
+				return $this->getPosts($request, $params, $registrar);
+		}
 
-        $post_status = $params['status'];
-        if ($post_status === 'all') {
-            $post_status = ['publish', 'draft'];
-        } else {
-            $post_status = explode(',', $post_status);
-        }
-        $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
+		return $this->error('Something went wrong, this does not appear to have a proper content type');
+	}
+
+	public function getPosts(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+	{
+		$user_id = $params['user'];
+
+		$post_status = $params['status'];
+		if ($post_status === 'all') {
+			$post_status = ['publish', 'draft'];
+		} else {
+			$post_status = explode(',', $post_status);
+		}
+		$post_type = str_replace('-', '_', jvbCheckBase($params['content']));
+
+		// Build query args
+		$args = [
+			'post_type' => $post_type,
+			'posts_per_page' => $params['per_page'] ?? 30,
+			'paged' => $params['page'],
+			'orderby' => 'date',
+			'order' => 'DESC',
+			'author' => $user_id,
+			'post_status' => $post_status
+		];
 
 
-        // Build query args
-        $args = [
-            'post_type' => $post_type,
-            'posts_per_page' => $params['per_page']??30,
-            'paged' => $params['page'],
-            'orderby' => 'date',
-            'order' => 'DESC',
-            'author' => $user_id,
-            'post_status' => $post_status
-        ];
 		//Only top level posts for timeline types
-		if (Features::forContent($post_type)->has('is_timeline')) {
+		if ($registrar?->hasFeature('is_timeline')) {
 			$args['post_parent'] = 0;
 		}
 
 		//Calendar filters
-		if (Features::forContent($post_type)->has('is_calendar'))  {
+		if ($registrar?->hasFeature('is_calendar')) {
 			$args = $this->applyCalendarFilters($args, $params);
 		}
+		$taxonomies = array_filter($params, function ($param) {
+			return str_starts_with($param, 'tax_');
+		}, ARRAY_FILTER_USE_KEY);
+		if (!empty($taxonomies)) {
+			$params['taxonomies'] = [];
+			foreach ($taxonomies as $taxonomy => $terms) {
+				$taxonomy = str_replace('tax_', '', $taxonomy);
+				$params['taxonomies'][$taxonomy] = $terms;
+			}
+		}
 		if (array_key_exists('taxonomies', $params)) {
 			$args = $this->applyTaxonomyFilters($args, $params);
 		}
-		if (array_key_exists('date', $params) && !empty($params['date'])) {
+		if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) {
 			$args = $this->applyDateFilters($args, $params);
 		}
 		if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) {
 			$args = $this->applyOrderFilters($args, $params);
 		}
 
-		if (!empty($params['search'])) {
+		if (array_key_exists('search', $params)) {
 			$args['s'] = sanitize_text_field($params['search']);
 		}
 
-
-
-		error_log('Content Routes final args: '.print_r($args, true));
-
-        $key = $this->cache->generateKey($args);
-		// Check HTTP cache headers with the specific content type
-		$content_type = $params['content'] ?? $params['type'];
-		$cache_check = $this->checkHeaders($request, $content_type, [
-			'filter_hash' => $key,
-		]);
-		if ($cache_check) {
-			return $cache_check;
+		$key = $this->cache->generateKey($args);
+		$cached = $this->cache->get($key);
+		if ($cached) {
+			return $cached;
 		}
 
+		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
 
-        $cache = $this->cache->get($key);
-		$cache = false;
-        if ($cache) {
-            $response = new WP_REST_Response($cache);
-			return $this->addCacheHeaders($response);
-        }
-
-        // Run query
-        $query = new WP_Query($args);
-
-        $this->post_type = $params['content']??$params['type'];
-
-        $this->fields = jvbGetFields(str_replace('-','_',$this->post_type));
-        $this->taxonomies = $this->getTaxonomies($this->post_type);
-        $posts = array_map([$this, 'prepareItem'], $query->posts);
-
-
-        $data = [
-            'items' => $posts,
-            'total' => $query->found_posts,
-            'total_pages' => $query->max_num_pages
-        ];
-
-
-        $this->cache->set($key, $data);
-
-        $response = new WP_REST_Response($data);
-		return $this->addCacheHeaders($response);
-    }
-
-    /**
-     * Gets allowed taxonomies for a particular content
-     * @param string $content
-     *
-     * @return array
-     */
-    protected function getTaxonomies(string $content):array
-    {
-        $taxonomy_for = jvbGlobalTaxonomyFor();
-        $out = [];
-        foreach ($taxonomy_for as $tax => $postTypes) {
-            if (in_array($content, $postTypes)) {
-                $out[BASE.$tax] = [
-                    'label' => JVB_CONTENT[$content]['plural'],
-                    'icon'  => $tax,
-                ];
-            }
-        }
-        return $out;
-    }
-
-    /**
-     * Processes operation from queue
-     * @param object $operation
-     * @param array $data
-     *
-     * @return array
-     */
-    protected function processBatches(object $operation, array $data):array
-    {
-        $this->user_id = $operation->user_id;
-        $posts = $data['posts'];
-
-        if (empty($posts)) {
-            return [
-                'success' => false,
-                'message' => 'No posts to update'
-            ];
-        }
-
-        $results = [];
-
-        foreach ($posts as $ID => $post_data) {
-			if (Features::forContent($post_data['content'])->has('is_timeline')) {
-				$results[$ID] =$this->processTimelinePost($ID, $post_data);
-				continue;
-			}
-			if (str_starts_with($ID, 'new')) {
-
-				error_log('New post detected. Creating... with: '.print_r([
-						'post_author' => $this->user_id,
-						'post_type'		=> jvbCheckBase($post_data['content']),
-						'post_title'	=> $post_data['post_title']??'',
-						'post_status'	=> $post_data['status']??'draft',
-					], true));
-				error_log('Recieved Data: '.print_r($post_data, true));
-				$ID = wp_insert_post([
-					'post_author' => $this->user_id,
-					'post_type'		=> jvbCheckBase($post_data['content']),
-					'post_title'	=> $post_data['post_title']??'',
-					'post_status'	=> $post_data['status']??'draft',
-				]);
-				if (!$ID || is_wp_error($ID)) {
-					$results[$ID] = [
-						'success' => false,
-						'message'	=> 'Couldn\'t Create Post'
-					];
-					continue;
-				}
-				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function($key) use ($fields) {
-					return array_key_exists($key, $fields);
-				}, ARRAY_FILTER_USE_KEY);
-
-				$meta = new MetaManager($ID, 'post');
-				$success = $meta->setAll($allowedFields);
-				$results[$ID] = [
-					'success'	=> $success
-				];
-			} else {
-				if (!$this->verifyOwnership($ID)) {
-					$results[$ID] = [
-						'success' => false,
-						'message' => 'No permission to modify this post'
-					];
-					continue;
-				}
-				error_log('Saving post data: '.print_r($post_data, true));
-
-				if (array_key_exists('post_status', $post_data)) {
-					switch ($post_data['post_status']) {
-						case 'publish':
-							unset($post_data['post_status']);
-							if (user_can($this->user_id, 'manage_options') || user_can($this->user_id, 'skip_moderation')) {
-								$result = wp_update_post(['ID' => $ID, 'post_status' => 'publish']);
-							}
-							break;
-						case 'draft':
-							$result = wp_update_post([
-								'ID'	=> $ID,
-								'post_status' => 'draft'
-							]);
-							break;
-						case 'trash':
-							$result = wp_trash_post($ID);
-							break;
-						case 'delete':
-							$result = wp_delete_post($ID, true);
-							return ['success' => (bool)$result];
-					}
-				}
-				error_log('Updating data: '.print_r($post_data, true));
-				$fields = jvbGetFields($post_data['content']);
-				$allowedFields = array_filter($post_data, function($key) use ($fields) {
-					return array_key_exists($key, $fields);
-				}, ARRAY_FILTER_USE_KEY);
-
-				error_log('Allowed Fields: '.print_r($allowedFields, true));
-				$meta = new MetaManager($ID, 'post');
-				$success = $meta->setAll($allowedFields);
-				error_log('Should be set?');
-				$results[$ID] = [
-					'success'	=> $success
-				];
-
-			}
-
-            CacheManager::for($post_data['content'])->clear();
-			if (jvbSiteUsesFeedBlock()) {
-				CacheManager::for('feed')->clear();
-			}
-        }
-
-		if (jvbSiteHasNotifications()) {
-			$this->notifications = JVB()->notification();
-			$this->notifications->addNotification(
-				$this->user_id,
-				'content_update_complete',
-				null,
-				'Content updates completed!'
-			);
+		if (array_key_exists('s', $args)) {
+			$args = $this->applySearchFilters($args, $params);
 		}
 
+		// Run query
+		$query = new WP_Query($args);
 
-        return [
-            'success' => true,
-            'result' => $results
-        ];
-    }
+		$registrar = Registrar::getInstance($this->post_type);
+		$this->fields = $registrar->getFields()??[];
+		$this->taxonomies = $this->getTaxonomies($this->post_type);
 
-	/**
-	 * Extracts the postdata for timeline post child posts from the pseudo-repeater element
-	 * @param int $parent_id
-	 * @param array $post_data
-	 * @return array|true[]
-	 */
-	protected function processTimelinePost(int $parent_id, array $post_data):array
+		$posts = array_map([$this, 'preparePost'], $query->posts);
+
+		$data = [
+			'items' => $posts,
+			'total' => $query->found_posts,
+			'total_pages' => $query->max_num_pages,
+			'has_more'	=> $args['paged']??1 < $query->max_num_pages,
+		];
+
+
+		$this->cache->set($key, $data);
+
+		return Response::success($data);
+	}
+	public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
 	{
-		if (!$this->verifyOwnership($parent_id)) {
-			return ['success' => false, 'message' => 'No permission'];
+		// Build query args
+		$args = [
+			'taxonomy' 		=> jvbCheckBase($params['content']),
+			'number'		=> $params['per_page'] ?? 30,
+			'orderby' 		=> 'name',
+			'order' 		=> 'DESC',
+			'hide_empty'	=> false,
+		];
+		$paged = $params['page']??1;
+		$args['page'] = $paged;
+		if ($paged > 1) {
+			$args['offset'] = ($paged-1) * $args['number'];
 		}
 
-		$this->fields = jvbGetFields($post_data['content']);
-		$this->initTimelineFields($post_data['content']);
-		error_log('Received Data: '.print_r($post_data, true));
+		//TODO
+//		if (array_key_exists('taxonomies', $params)) {
+//			$args = $this->applyTaxonomyFilters($args, $params);
+//		}
+//		if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) {
+//			$args = $this->applyDateFilters($args, $params);
+//		}
+		if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) {
+			$args = $this->applyOrderFilters($args, $params);
+		}
 
-		// First, process the main fields that will apply to all posts
-		$sharedData = array_filter($post_data, function ($key) {
-			return in_array($key, $this->timelineSharedFields);
-		}, ARRAY_FILTER_USE_KEY);
+		if (array_key_exists('search', $params)) {
+			$args['s'] = sanitize_text_field($params['search']);
+		}
 
-		//Next, process any individual posts, including any menu order changes
-		if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
-			$sharedTaxonomies = $sharedData;
-			unset($sharedTaxonomies['post_title']);
+		$key = $this->cache->generateKey($args);
+		// Check HTTP cache headers with the specific content type
+//		$cache_check = $this->checkHeaders($request, $key);
+//		if ($cache_check) {
+//			return $cache_check;
+//		}
 
-			//Ensure the parent post exists and is still first in the array
-			$index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
-			error_log('Found index: '.print_r($index, true));
-			if ($index === false) {
-				return [
-					'success' => false,
-					'message'	=> 'Missing parent id. This should not have happened'
-				];
-			} elseif ($index !== 0) {
-				// Move that element to the start of the array
-				$item = $post_data['timeline'][$index];
-				unset($post_data['timeline'][$index]);
-				array_unshift($post_data['timeline'], $item);
-			}
-			$errors = [];
-			$success = [];
-			// Get existing children to track deletions
-			$existing_children = get_children([
-				'post_parent' => $parent_id,
-				'post_type' => jvbCheckBase($post_data['content']),
-				'fields' => 'ids'
+		$cache = $this->cache->get($key);
+		if ($cache) {
+			return Response::success($cache);
+		}
+		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
+		// Only expand search to taxonomies if we're actually going to query
+		if (array_key_exists('s', $args)) {
+			$args = $this->applySearchFilters($args, $params);
+		}
+
+		// Run query
+		$query = new WP_Term_Query($args);
+
+		$terms = $query->get_terms();
+		$data = [
+			'total'			=> 0,
+			'total_pages' 	=> 0,
+			'has_more' 		=> false
+		];
+
+		if (!is_wp_error($terms) && !empty($terms))
+		{
+			$total = get_terms([
+				'taxonomy'		=> $args['taxonomy'],
+				'hide_empty'	=> false,
+				'fields'		=> 'count'
 			]);
-			//Iterate through the timeline posts
-			foreach($post_data['timeline'] as $order => $timeline) {
-				$allowedFields = array_filter($timeline, function($key) {
-					return in_array($key, $this->timelineUniqueFields);
-				}, ARRAY_FILTER_USE_KEY);
-
-				$allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order;
-				$allowedFields = array_merge($allowedFields, $sharedTaxonomies);
-				if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) {
-					$newChild = wp_insert_post([
-						'post_author'	=> $this->user_id,
-						'post_type'		=> jvbCheckBase($post_data['content']),
-						'post_title'	=> $allowedFields['post_title'],
-						'post_parent'	=> $parent_id,
-						'menu_order'	=> $order
-					]);
-					if (!$newChild || is_wp_error($newChild)) {
-						$errors[] = [
-							'message'	=> 'Could not create child post',
-							'data'		=> $timeline
-						];
-						continue;
-					}
-					$timeline['id'] = $newChild;
-				}
-
-				if (in_array((int)$timeline['id'], $existing_children)) {
-					unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
-				}
-
-				//Determine which fields to update
-				$meta = new MetaManager($timeline['id'], 'post');
-				$oldValues = $meta->getAll(array_keys($allowedFields));
-				$updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
-					return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
-				}, ARRAY_FILTER_USE_BOTH);
-				$meta->setAll($updateValues);
-				//Update Menu Order, if applicable
-				if ((int) $timeline['id'] !== $parent_id) {
-					$post = get_post((int) $timeline['id']);
-					if ($post && $post->menu_order !== $order) {
-						$updated = wp_update_post([
-							'ID'			=> $post->ID,
-							'menu_order'	=> $order,
-						]);
-						if (!$updated || is_wp_error($updated)) {
-							$errors[] = [
-								'message'	=> 'Could not update timeline order for post',
-								'data'		=> $timeline
-							];
-						}
-					}
-				}
-				$success[] = $timeline['id'];
-			}
-		}
-		//Delete any remaining children that no longer exist
-		if (!empty($existing_children)) {
-			foreach ($existing_children as $ID) {
-				wp_delete_post($ID);
-			}
+			$data['total'] = $total;
+			$data['total_pages'] = max($total/$args['number'], 1);
+			$data['has_more'] = ($args['page'] * $args['number']) < $total;
+		} else {
+			$terms = [];
 		}
 
-		return ['success' => true, 'data' => [
-			'success'	=> $success,
-			'errors'	=> $errors
-		]];
+		$this->fields = $registrar->getFields()??[];
+
+		$this->taxonomies = [];
+		$data['items'] =array_map([$this, 'prepareTerm'], $terms);
+
+		$this->cache->set($key, $data);
+
+		return Response::success($data);
 	}
 
-    /**
-     * Handle batch content creation from uploads
-     * @param WP_REST_Request $request
-     *
-     * @return WP_REST_Response
-     */
-    public function handleBatchCreation(WP_REST_Request $request):WP_REST_Response
-    {
-        //Operation has two parts
-        //First, queue image processing
-        //Then queue post creation from the stored IDs, depending on mode
-            //if direct, each image becomes a new post
-            //if selection, each group becomes its own post,
-                // and ungrouped items each become their own post
-        if (!isset($_FILES['files'])) {
-            return new WP_REST_Response([
-                'success'   => false,
-                'message'   => 'No files uploaded...',
-            ]);
-        }
+	protected function applySearchFilters(array $args, array $params): array
+	{
+		$search_term = sanitize_text_field($params['search']);
 
-        $data = $request->get_params();
+		// Search term is already in $args['s'] from earlier
+
+		// Get all taxonomies registered to this post type
+		$taxonomies = get_object_taxonomies($this->post_type, 'names');
+
+		if (empty($taxonomies)) {
+			return $args;
+		}
+
+		// Cache the taxonomy term lookup per search term + post type
+		$term_cache_key = 'search_terms_' . md5($search_term . $this->post_type);
+		$matching_term_ids = $this->cache->get($term_cache_key);
+
+		if ($matching_term_ids === false) {
+			$matching_term_ids = [];
+
+			foreach ($taxonomies as $taxonomy) {
+				$terms = get_terms([
+					'taxonomy' => $taxonomy,
+					'search' => $search_term,
+					'hide_empty' => false,
+					'fields' => 'ids'
+				]);
+
+				if (!is_wp_error($terms) && !empty($terms)) {
+					$matching_term_ids = array_merge($matching_term_ids, $terms);
+				}
+			}
+
+			// Cache term IDs for 1 hour
+			$this->cache->set($term_cache_key, $matching_term_ids, 3600);
+		}
+
+		if (empty($matching_term_ids)) {
+			return $args;
+		}
+
+		// Build tax_query for matching terms
+		$term_queries = [];
+
+		foreach ($taxonomies as $taxonomy) {
+			$taxonomy_term_ids = array_filter($matching_term_ids, function ($term_id) use ($taxonomy) {
+				$term = get_term($term_id);
+				return !is_wp_error($term) && $term->taxonomy === $taxonomy;
+			});
+
+			if (!empty($taxonomy_term_ids)) {
+				$term_queries[] = [
+					'taxonomy' => $taxonomy,
+					'field' => 'term_id',
+					'terms' => array_values($taxonomy_term_ids),
+					'operator' => 'IN'
+				];
+			}
+		}
+
+		if (!empty($term_queries)) {
+			if (isset($args['tax_query'])) {
+				$args['tax_query'] = [
+					'relation' => 'OR',
+					$args['tax_query'],
+					[
+						'relation' => 'OR',
+						...$term_queries
+					]
+				];
+			} else {
+				$args['tax_query'] = [
+					'relation' => 'OR',
+					...$term_queries
+				];
+			}
+		}
+
+		return $args;
+	}
+
+	/**
+	 * Gets allowed taxonomies for a particular content
+	 * @param string $content
+	 *
+	 * @return array
+	 */
+	protected function getTaxonomies(string $content): array
+	{
+		$registrar = Registrar::getInstance($content);
+		if (!$registrar || $registrar->getType()!== 'post') {
+			return [];
+		}
+		$out = [];
+		foreach ($registrar->registrar->taxonomies as $tax) {
+			$taxReg = Registrar::getInstance($tax);
+			$out[jvbCheckBase($tax)] = [
+				'label'	=> $taxReg->getPlural(),
+				'icon'	=> $taxReg->getIcon()??jvbDefaultIcon()
+			];
+		}
+
+		return $out;
+	}
 
 
-        $user_id = $data['user'];
-        if (!$this->userCheck($user_id)) {
-            return new WP_REST_Response([
-                'success'   => 'false',
-                'message'   => 'Invalid user match... are you a bot?'
-            ]);
-        }
-        $operation_id = $data['id'];
-        $response = new WP_REST_Response([
-            'success' => true,
-            'message' => 'Successfully sent to server. Added to queue.',
-            'operation_id' => $operation_id,
-            'status' => 'pending'
-        ]);
-        $this->queue = JVB()->queue();
-        JVB()->routes('uploads')->handleUploadRequest($request, false);
-        $this->queue->queueOperation(
-            'batch_creation',
-            $user_id,
-            [
-                'content' => $request->get_param('content'),
-                'mode'      => $request->get_param('mode') ?: 'direct',
-                'files_data'=> $request->get_param('files_data')
-            ],
-            [
-                'operation_id'  => $operation_id,
-                'priority'      => 'high',
-                'notification'  => true,
-                'depends_on'    => $operation_id.'_upload'
-            ]
-        );
 
-        return $response;
-    }
+	/**
+	 * Generates a post title, based on content type
+	 * @param string $content the post type
+	 *
+	 * @return string
+	 */
+	protected function generatePostTitle(string $content): string
+	{
+		$username = get_user_meta($this->user_id, 'first_name', true);
+		$link = get_user_meta($this->user_id, BASE . 'link', true);
+		$city = jvbArtistCity($link);
+		return ucfirst($content) . ' by ' . $city . ' artist ' . $username;
+	}
 
-    /**
-     * Generates a post title, based on content type
-     * @param string $content the post type
-     *
-     * @return string
-     */
-    protected function generatePostTitle(string $content):string
-    {
-        $username = get_user_meta($this->user_id, 'first_name', true);
-        $link = get_user_meta($this->user_id, BASE.'link', true);
-        $city = jvbArtistCity($link);
-        return ucfirst($content).' by '.$city.' artist '.$username;
-    }
-
-    /**
-     * @param WP_Post $post the wordpress post object
-     *
-     * @return array
-     */
-    protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array
-    {
-		if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
+	/**
+	 * @param WP_Post $post the post object
+	 *
+	 * @return array
+	 */
+	protected function preparePost(WP_Post $post, bool $skip = false, bool $fields = true): array
+	{
+		$registrar = Registrar::getInstance($post->post_type);
+		if (!$skip && $registrar && $registrar->hasFeature('is_timeline')) {
 			$this->initTimelineFields($post->post_type);
 			return $this->formatTimeline($post);
 		}
-        $this->meta = new MetaManager($post->ID, 'post');
-        $data = [
-            'id'        => $post->ID,
-            'status'    => $post->post_status,
-            'date'      => $post->post_date,
-            'modified'  => $post->post_modified,
-            'thumbnail' => get_the_post_thumbnail_url($post->ID),
-            'alt'       => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
-            'icon'      => $this->post_type,
-            'taxonomies'=> [],
-            'fields'    => ($fields) ? $this->meta->getAll() : [],
-			'images'	=> [],
-        ];
+		$this->meta = Meta::forPost($post->ID);
+		$fields = ($fields) ? $this->meta->getAll() : [];
+		$data = [
+			'id' => $post->ID,
+			'title' => $post->post_title,
+			'status' => $post->post_status,
+			'date' => $post->post_date,
+			'modified' => $post->post_modified,
+			'thumbnail' => get_the_post_thumbnail_url($post->ID),
+			'alt' => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
+			'icon' => $registrar->getIcon(),
+			'taxonomies' => [],
+			'fields' => $fields,
+			'images' => [],
+		];
 
-        // Add taxonomy terms
-        foreach ($this->taxonomies as $taxonomy => $options) {
-            $tax = str_replace(BASE, '', $taxonomy);
-            $terms = wp_get_object_terms(
-                $post->ID,
-                $taxonomy,
-                ['fields' => 'id=>name']
-            );
-            $data['taxonomies'][$tax] = [
-                'terms' => (is_wp_error($terms))? [] : $terms,
-                'name'  => $options['label'],
-                'icon'  => $tax
-            ];
-        }
-
-		$images = $this->extractImages();
-
-
-        if (!empty($images)) {
-            $data['images'] = $images;
-        }
-
-        return $data;
-    }
-	protected function extractImages(array $fields = []):array
-	{
-		//Extract images
-		$images = [];
-		$get = [];
-		$fields = (empty($fields)) ? $this->fields : $fields;
-		foreach ($fields as $field => $config) {
-			if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
-				$get[] = $field;
-			}
+		$images = $this->extractImages($fields, $this->meta);
+		if (!empty($images)) {
+			$data['images'] = $images;
 		}
 
-		if (!empty($get)) {
-			$allImages = $this->meta->getAll($get);
-			foreach($allImages as $k => $imgs){
-				$temp = explode(',', $imgs);
-				foreach($temp as $img) {
-					if (is_numeric($img) && !array_key_exists($img, $images)) {
-						$images[$img] = jvbImageData((int) $img);
-					}
-				}
-			}
+
+		$taxonomies = $this->extractTerms($fields, $this->meta);
+		if (!empty($taxonomies)) {
+			$data['taxonomies'] = $taxonomies;
 		}
-		return $images;
+		return $data;
 	}
 
-	protected function formatTimeline(WP_Post $post):array
+	/**
+	 * @param WP_Term $post the post object
+	 *
+	 * @return array
+	 */
+	protected function prepareTerm(WP_Term $post, bool $fields = true): array
 	{
-		$item = $this->prepareItem($post, true, false);
+		$registrar = Registrar::getInstance($post->taxonomy);
+
+		$this->meta = Meta::forTerm($post->term_id);
+		$fields = ($fields) ? $this->meta->getAll() : [];
+		$data = [
+			'id' => $post->term_id,
+			'title' => $post->name,
+			'date' => $fields['created_date']??'',
+			'modified' => $fields['modified_date']??'',
+			'thumbnail' => '',
+			'icon' => $registrar->getIcon(),
+			'taxonomies' => [],
+			'fields' => $fields,
+			'images' => [],
+		];
+
+		$images = $this->extractImages($fields, $this->meta);
+		if (!empty($images)) {
+			$data['images'] = $images;
+		}
+
+		$taxonomies = $this->extractTerms($fields, $this->meta);
+		if (!empty($taxonomies)) {
+			$data['taxonomies'] = $taxonomies;
+		}
+
+		error_log('Term data: '.print_r($data, true));
+		return $data;
+	}
+
+
+	public function formatTimeline(WP_Post $post): array
+	{
+		$item = $this->preparePost($post, true, false);
 		//Step 1: Get the fields that apply to all posts
-		$mainMeta = new MetaManager($post->ID, 'post');
+		$mainMeta = Meta::forPost($post->ID);
 		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
 		//Step 2: Get the fields for each individual posts
-		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
+		$children = jvbTimelinePoints($post->ID, $post->post_type,['publish', 'draft']);
 		array_unshift($children, $post->ID);
 
 		$subFields = [];
 		$images = [];
 		foreach ($children as $child) {
-			$meta = new MetaManager($child, 'post');
+			$meta = Meta::forPost($child);
 			$f = $meta->getAll($this->timelineUniqueFields);
-			$f =  ['id' => $child] + $f;
-			$subFields[$f['post_thumbnail']] = $f;
+			$f = ['id' => $child] + $f;
+			$subFields[] = $f;
 
-			$images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
+			$images[$f['post_thumbnail']] = jvbImageData((int)$f['post_thumbnail']);
 		}
-		$item['fields']['timeline'] = $subFields;
+		$item['fields']['timeline_gallery'] = $subFields;
 		$item['images'] = $item['images'] + $images;
+		$item['number'] = $mainMeta->get('number');
 
 		return $item;
 	}
-
-    /**
-     * Builds the taxonomy query
-     * @param array $taxonomies
-     *
-     * @return array|string[]
-     */
-    protected function buildTaxQuery(array $taxonomies):array
-    {
-        $tax_query = [];
-		error_log('Taxonomies in query: '.print_r($taxonomies, true));
-
-        foreach ($taxonomies as $taxonomy => $terms) {
-            if (!empty($terms)) {
-                $tax_query[] = [
-                    'taxonomy' => jvbCheckBase($taxonomy),
-                    'field' => 'term_id',
-                    'terms' => array_map('absint', (array)$terms)
-                ];
-            }
-        }
-
-
-        return count($tax_query) > 1
-            ? array_merge(['relation' => 'AND'], $tax_query)
-            : $tax_query;
-    }
-
-    /**
-     * Builds the date query
-     * @param array $date_params
-     *
-     * @return array
-     */
-    protected function buildDateQuery(array $date_params):array
-    {
-        $query = [];
-
-        if (!empty($date_params['after'])) {
-            $query['after'] = sanitize_text_field($date_params['after']);
-        }
-
-        if (!empty($date_params['before'])) {
-            $query['before'] = sanitize_text_field($date_params['before']);
-        }
-
-        if (isset($date_params['inclusive'])) {
-            $query['inclusive'] = (bool)$date_params['inclusive'];
-        }
-
-        return empty($query) ? [] : [$query];
-    }
-
-    /**
-     * @param int $post_id
-     *
-     * @return bool
-     */
-    protected function verifyOwnership(int $post_id):bool
-    {
-        $post = get_post($post_id);
-        return $post && $post->post_author == $this->user_id;
-    }
-
-    /**
-     * Processes operation from Operation Queue
-     * @param WP_Error|array $result
-     * @param object $operation
-     * @param array $data
-     *
-     * @return array|WP_Error
-     */
-    public function processOperation(WP_Error|array $result, object $operation, array $data):array|WP_Error
-    {
-        if ($operation->type === 'batch_creation') {
-            $JVB = JVB();
-            $queue = $JVB->queue();
-
-            $images = $queue->getOperationValue($operation->id.'_upload', 'result')??false;
-
-            $this->user_id = $operation->user_id;
-            $this->post_type = BASE.$data['content'];
-            try {
-                $results = [];
-                if ($images) {
-                    if ($data['mode'] == 'selection') {
-                        $total = count($images);
-                        foreach ($images as $group => $files) {
-                            $settings = json_decode($data['files_data'][$group]);
-
-                            switch ($settings->type) {
-                                case 'group':
-                                    $featuredIndex = $settings->metadata->featuredFile??0;
-                                    $title = $settings->metadata->title??$this->generatePostTitle($data['content']);
-                                    $new = wp_insert_post([
-                                        'post_type'     => BASE.$data['content'],
-                                        'post_title'    => $title,
-                                        'post_status'   => 'draft',
-                                        'post_author'   => $operation->user_id
-                                    ]);
-                                    if ($new && !is_wp_error($new)) {
-                                        set_post_thumbnail($new, $files[$featuredIndex]['attachment_id']);
-                                        unset($files[$featuredIndex]);
-                                        if (!empty($files)) {
-                                            $meta = new MetaManager($new, 'post');
-                                            $IDs = array_column($files, 'attachment_id');
-                                            $meta->updateValue('gallery', implode(',', $IDs));
-                                        }
-                                        $results[] = $new;
-//                                        $queue->updateOperationProgress($operation->id, $group + 1, $total);
-                                    }
-                                    break;
-                                default:
-                                    foreach ($files as $img) {
-                                        $new = wp_insert_post([
-                                            'post_type'     => BASE. $data['content'],
-                                            'post_title'    => $this->generatePostTitle($data['content']),
-                                            'post_status'   => 'draft',
-                                            'post_author'   => $operation->user_id
-                                        ]);
-
-                                        if ($new && !is_wp_error($new)) {
-                                            set_post_thumbnail($new, $img['attachment_id']);
-                                            $results[] = $new;
-//                                            $queue->updateOperationProgress($operation->id, $group + 1, $total);
-                                        }
-                                    }
-                                    break;
-                            }
-                        }
-                    } else {
-                        $total = count($images);
-                        foreach ($images as $key => $img) {
-                            $new = wp_insert_post([
-                                'post_type'     => BASE.$data['content'],
-                                'post_title'    => $this->generatePostTitle($data['content']),
-                                'post_status'   => 'draft',
-                                'post_author'   => $operation->user_id
-                            ]);
-                            if ($new && !is_wp_error($new)) {
-                                set_post_thumbnail($new, $img['attachment_id']);
-                            }
-                            $results[] = $new;
-//                            $queue->updateOperationProgress($operation->id, $key + 1, $total);
-                        }
-                    }
-
-                    //Clear cache
-					CacheManager::for($data['content'])->clear();
-                    CacheManager::for('feed')->clear();
-                }
-
-				return [
-					'success'	=> true,
-					'result'	=> $results
-				];
-            } catch (Exception $e) {
-                $JVB->error()->log(
-                    '[ContentRoutes]:processOperation',
-                    $e->getMessage()
-                );
-            }
-
-            return $results;
-        } elseif ($operation->type == 'content_update') {
-            $result = $this->processBatches($operation, $data);
-        }
-
-        return $result;
-    }
 }

--
Gitblit v1.10.0