From ac444cba221832c012c0435fdc8339fe9f37febb Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Mon, 11 May 2026 18:35:04 +0000
Subject: [PATCH] =Some changes to the CRUD.js editing, timeline post configuration

---
 inc/rest/routes/ContentRoutes.php | 1176 ++++++++++++++++++++++++++-------------------------------
 1 files changed, 537 insertions(+), 639 deletions(-)

diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 6916093..dd25f8e 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -1,722 +1,620 @@
 <?php
+
 namespace JVBase\rest\routes;
 
-use JVBase\JVB;
-use JVBase\rest\RestRouteManager;
-use JVBase\managers\CacheManager;
-use JVBase\meta\MetaManager;
+use JVBase\managers\queue\executors\ContentExecutor;
+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 = '';
+	protected array $fields = [];
+	protected array $taxonomies = [];
+	protected string $post_type = '';
+	protected string $user_id = '';
 
-    //TODO: Ensure we are handling the bulk operations for all processes
-    //TODO: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create
-    //TODO: Also invalidate feed caches on updates!!
+	//For Timeline-specific posts
+	protected array $timelineSharedFields = [];
+	protected array $timelineUniqueFields = [];
+	protected static ?string $action = 'dash-';
+	protected Meta $meta;
 
-    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);
-    }
+	public function __construct()
+	{
+		$this->cacheName = 'user_content_' . get_current_user_id();
+		parent::__construct();
+		if (JVB_TESTING) {
+			$this->cache->flush();
+		}
+		$this->cache->connect('post', true);
+		$this->cache->connect('term', true);
+		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 = JVB()->queue()->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
+		));
 
-    /**
-     * 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'];
+		// 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(20)
+			->post([$this, 'postContent'])
+			->auth(PermissionHandler::combine(['user', 'nonce', ['actionNonce'=>'dash-']]))
+			->rateLimit(30)
+			->args([
+				'user'	=> 'int|required',
+				'posts' => 'required',
+				'content' => 'string',
+			])
+			->register();
+	}
+
+	protected function initTimelineFields(string $content): void
+	{
+		$content = jvbNoBase($content);
+
+		$config = Registrar::getInstance($content);
+		if (!$config || !$config->hasFeature('is_timeline')) {
+			return;
+		}
+		$this->fields = $config->getFields();
+
+		$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 = $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;
+			}
+			return false;
+		}));
+	}
+
+	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 = $data['user'];
+
+		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['user']);
+		unset($data['id']);
 
-        $count = count($data['posts']);
-        $operationId = $data['id'];
-        unset($data['user']);
-        unset($data['id']);
+		error_log('[CONTENT]:'.print_r($data, true));
+		$queue = JVB()->queue();
+		$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');
+	}
 
-		$config = (array_key_exists($params['content'], JVB_CONTENT) && !empty(JVB_CONTENT[$params['content']])) ? JVB_CONTENT[$params['content']] : [];
+	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 ($registrar?->hasFeature('is_timeline')) {
+			$args['post_parent'] = 0;
+		}
 
 		//Calendar filters
-		if (jvbCheck('is_calendar', $config))  {
+		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);
-		$lastModified = $this->cache->getTimestamp($key);
-		if ($lastModified !== false) {
-			$headerCheck = $this->ifModifiedSince($lastModified, $args, $request);
-			if (!is_null($headerCheck)) {
-				return $headerCheck;
-			}
-		} else {
-			// No timestamp yet, but we can still set ETag
-			$etag = '"' . md5(serialize($args)) . '"';
-			header('ETag: ' . $etag);
-			header('Cache-Control: private, max-age=30');
+		$key = $this->cache->generateKey($args);
+		$cached = $this->checkCache($key, $request);
+		if ($cached) {
+			return $cached;
 		}
 
+		$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
 
-        $cache = $this->cache->get($key);
-		$cache = false;
-        if ($cache) {
-            return new WP_REST_Response($cache);
-        }
+		if (array_key_exists('s', $args)) {
+			$args = $this->applySearchFilters($args, $params);
+		}
 
-        // Run query
-        $query = new WP_Query($args);
+		// Run query
+		$query = new WP_Query($args);
 
-        $this->post_type = $params['content']??$params['type'];
+		$registrar = Registrar::getInstance($this->post_type);
+		$this->fields = $registrar->getFields()??[];
+		$this->taxonomies = $this->getTaxonomies($this->post_type);
 
-        $this->fields = jvbGetFields(str_replace('-','_',$this->post_type));
-        $this->taxonomies = $this->getTaxonomies($this->post_type);
-        $posts = array_map([$this, 'prepareItem'], $query->posts);
+		$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,
+		];
 
 
-        $data = [
-            'items' => $posts,
-            'total' => $query->found_posts,
-            'total_pages' => $query->max_num_pages
-        ];
+		$this->cache->set($key, $data);
 
+		$response = Response::success($data);
+		return $this->addCacheHeaders($response);
+	}
+	public function getTerms(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+	{
+		// 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->cache->set($key, $data);
+		//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);
+		}
 
-        return new WP_REST_Response($data);
-    }
+		if (array_key_exists('search', $params)) {
+			$args['s'] = sanitize_text_field($params['search']);
+		}
 
-    /**
-     * 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;
-    }
+		$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;
+		}
 
-    /**
-     * 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'];
+		$cache = $this->cache->get($key);
+		if ($cache) {
+			$response = Response::success($cache);
+			return $this->addCacheHeaders($response);
+		}
+		$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);
+		}
 
-        if (empty($posts)) {
-            return [
-                'success' => false,
-                'message' => 'No posts to update'
-            ];
-        }
+		// Run query
+		$query = new WP_Term_Query($args);
 
-        $results = [];
+		$terms = $query->get_terms();
+		$data = [
+			'total'			=> 0,
+			'total_pages' 	=> 0,
+			'has_more' 		=> false
+		];
 
-        foreach ($posts as $ID => $post_data) {
-			if (str_starts_with($ID, 'new')) {
+		if (!is_wp_error($terms) && !empty($terms))
+		{
+			$total = get_terms([
+				'taxonomy'		=> $args['taxonomy'],
+				'hide_empty'	=> false,
+				'fields'		=> 'count'
+			]);
+			$data['total'] = $total;
+			$data['total_pages'] = max($total/$args['number'], 1);
+			$data['has_more'] = ($args['page'] * $args['number']) < $total;
+		} else {
+			$terms = [];
+		}
 
-				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',
+		$this->fields = $registrar->getFields()??[];
+
+		$this->taxonomies = [];
+		$data['items'] =array_map([$this, 'prepareTerm'], $terms);
+
+		$this->cache->set($key, $data);
+
+		$response = Response::success($data);
+		return $this->addCacheHeaders($response);
+	}
+
+	protected function applySearchFilters(array $args, array $params): array
+	{
+		$search_term = sanitize_text_field($params['search']);
+
+		// 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 (!$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
+				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 {
-				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
+				$args['tax_query'] = [
+					'relation' => 'OR',
+					...$term_queries
 				];
-
 			}
+		}
 
-            CacheManager::invalidateGroup($post_data['content']);
-			if (jvbSiteUsesFeedBlock()) {
-				CacheManager::invalidateGroup($post_data['feed']);
-			}
-        }
+		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;
+	}
 
 
-        CacheManager::invalidateGroup('user_content');
-		if (jvbSiteHasNotifications()) {
-			$this->notifications = JVB()->notification();
-			$this->notifications->addNotification(
-				$this->user_id,
-				'content_update_complete',
-				null,
-				'Content updates completed!'
-			);
+
+	/**
+	 * 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 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 = 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' => [],
+		];
+
+		$images = $this->extractImages($fields, $this->meta);
+		if (!empty($images)) {
+			$data['images'] = $images;
 		}
 
 
-        return [
-            'success' => true,
-            'result' => $results
-        ];
-    }
+		$taxonomies = $this->extractTerms($fields, $this->meta);
+		if (!empty($taxonomies)) {
+			$data['taxonomies'] = $taxonomies;
+		}
+		return $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...',
-            ]);
-        }
+	/**
+	 * @param WP_Term $post the post object
+	 *
+	 * @return array
+	 */
+	protected function prepareTerm(WP_Term $post, bool $fields = true): array
+	{
+		$registrar = Registrar::getInstance($post->taxonomy);
 
-        $data = $request->get_params();
+		$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;
+	}
 
 
-        $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'
-            ]
-        );
+	public function formatTimeline(WP_Post $post): array
+	{
+		$item = $this->preparePost($post, true, false);
+		//Step 1: Get the fields that apply to all posts
+		$mainMeta = Meta::forPost($post->ID);
+		$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
 
-        return $response;
-    }
+		//Step 2: Get the fields for each individual posts
+		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields' => 'ids']);
+		array_unshift($children, $post->ID);
 
-    /**
-     * 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 object $post the wordpress post object
-     *
-     * @return array
-     */
-    protected function prepareItem(object $post):array
-    {
-        $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'    => $this->meta->getAll(),
-			'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
-            ];
-        }
-
-
-        //Extract images
+		$subFields = [];
 		$images = [];
-		$get = [];
-        foreach ($this->fields as $field => $config) {
-            if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
-				$get[] = $field;
-            }
-        }
+		foreach ($children as $child) {
+			$meta = Meta::forPost($child);
+			$f = $meta->getAll($this->timelineUniqueFields);
+			$f = ['id' => $child] + $f;
+			$subFields[] = $f;
 
-		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);
-					}
-				}
-			}
+			$images[$f['post_thumbnail']] = jvbImageData((int)$f['post_thumbnail']);
 		}
+		$item['fields']['timeline_gallery'] = $subFields;
+		$item['images'] = $item['images'] + $images;
+		$item['number'] = $mainMeta->get('number');
 
-        if (!empty($images)) {
-            $data['images'] = $images;
-        }
-
-        return $data;
-    }
-
-    /**
-     * 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::invalidateGroup($data['content']);
-                    CacheManager::invalidateGroup('feed');
-                    CacheManager::invalidateGroup('user_content');
-                }
-
-				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;
-    }
+		return $item;
+	}
 }

--
Gitblit v1.10.0