Jake Vanderwerf
2026-01-19 0113d2e9c9ff34a6ffb10707cc76d34b67a0c367
inc/rest/routes/ContentRoutes.php
@@ -1,4 +1,5 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
@@ -14,75 +15,75 @@
use Exception;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
   exit; // Exit if accessed directly
}
class ContentRoutes extends RestRouteManager
{
    protected array $fields = [];
    protected array $taxonomies = [];
    protected MetaManager $meta;
    protected string $post_type = '';
    protected string $user_id = '';
   protected array $fields = [];
   protected array $taxonomies = [];
   protected MetaManager $meta;
   protected string $post_type = '';
   protected string $user_id = '';
   //For Timeline-specific posts
   protected array $timelineSharedFields = [];
   protected array $timelineUniqueFields = [];
    //TODO: Ensure we are handling the bulk operations for all processes
    //TODO: Also invalidate feed caches on updates!!
   //TODO: Ensure we are handling the bulk operations for all processes
   //TODO: Also invalidate feed caches on updates!!
    public function __construct()
    {
        $this->cache_name = 'user_content_'.get_current_user_id();
        parent::__construct();
   public function __construct()
   {
      $this->cache_name = 'user_content_' . get_current_user_id();
      parent::__construct();
      $this->cache->clear();
      $this->action = 'dash-';
      $this->operation_type = 'content_update';
      add_filter(BASE . 'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
   }
        $this->action = 'dash-';
        $this->operation_type = 'content_update';
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
    }
   /**
    * 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']
         ]
      ]);
    /**
     * 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']
            ]
        ]);
      //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']
         ]
      ]);
   }
        //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']
            ]
        ]);
    }
   protected function initTimelineFields(string $content):void
   protected function initTimelineFields(string $content): void
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
      if (!Features::forContent($content)->has('is_timeline')) {
         return;
      }
      $config = Features::getConfig($content);
@@ -95,10 +96,11 @@
      $this->timelineUniqueFields = $this->getTimelineUniqueFields($content);
   }
   public function getTimelineUniqueFields(string $content):array
   public function getTimelineUniqueFields(string $content): array
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
      if (!Features::forContent($content)->has('is_timeline')) {
         return [];
      }
      $config = Features::getConfig($content);
@@ -112,170 +114,186 @@
      }));
   }
   public function getTimelineSharedFields(string $content):array
   public function getTimelineSharedFields(string $content): array
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
      if (!Features::forContent($content)->has('is_timeline')) {
         return [];
      }
      $config = Features::getConfig($content);
      if (!$config || empty($config)) {
         return [];
      }
      $allFields = $config['fields']??[];
      $allFields = $config['fields'] ?? [];
      return array_keys(array_filter($allFields, function ($field) {
         if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
         if (!array_key_exists('for_all', $field) || $field['for_all'] === false) {
            return true;
         }
         return false;
      }));
   }
    /**
     * 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'];
   /**
    * 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'];
      if (!$this->userCheck($user_id)) {
         return new WP_REST_Response([
            'success' => true,
            'message' => 'You for real?'
         ]);
      }
        if (!isset($data['posts']) || !is_array($data['posts'])) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   =>'Invalid request format'
            ]);
        }
      if (!array_key_exists('posts', $data) || !is_array($data['posts'])) {
         return new WP_REST_Response([
            'success' => true,
            'message' => 'No posts found'
         ]);
      }
        $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']);
        $queue = JVB()->queue();
        $queue->queueOperation(
            'content_update',
            $user_id,
            $data,
            [
                'count'   => $count,
            'chunk_key' => 'posts',
      $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
        ]);
    }
            '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'];
   /**
    * 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'
            ]);
        }
      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,
            ]
        );
      $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 new WP_REST_Response([
         'success' => true,
         'message' => 'Queued for processing',
         'operation' => $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();
        $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?',
            ]);
        }
   /**
    * 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();
      $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?',
         ]);
      }
        $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']));
      $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')) {
         $args['post_parent'] = 0;
      }
      //Calendar filters
      if (Features::forContent($post_type)->has('is_calendar'))  {
      if (Features::forContent($post_type)->has('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']);
      }
        $key = $this->cache->generateKey($args);
      $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, [
@@ -286,114 +304,212 @@
      }
        $cache = $this->cache->get($key);
      $cache = false;
        if ($cache) {
            $response = new WP_REST_Response($cache);
      $cache = $this->cache->get($key);
      if ($cache) {
         $response = new WP_REST_Response($cache);
         return $this->addCacheHeaders($response);
        }
      }
        // Run query
        $query = new WP_Query($args);
      $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);
      }
        $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);
      // Run query
      $query = new WP_Query($args);
        $data = [
            'items' => $posts,
            'total' => $query->found_posts,
            'total_pages' => $query->max_num_pages
        ];
      $this->fields = jvbGetFields(str_replace('-', '_', $this->post_type));
      $this->taxonomies = $this->getTaxonomies($this->post_type);
      $posts = array_map([$this, 'prepareItem'], $query->posts);
        $this->cache->set($key, $data);
      $data = [
         'items' => $posts,
         'total' => $query->found_posts,
         'total_pages' => $query->max_num_pages
      ];
        $response = new WP_REST_Response($data);
      $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;
    }
   protected function applySearchFilters(array $args, array $params): array
   {
      $search_term = sanitize_text_field($params['search']);
    /**
     * 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'];
      // Search term is already in $args['s'] from earlier
        if (empty($posts)) {
            return [
                'success' => false,
                'message' => 'No posts to update'
            ];
        }
      // Get all taxonomies registered to this post type
      $taxonomies = get_object_taxonomies($this->post_type, 'names');
        $results = [];
      if (empty($taxonomies)) {
         return $args;
      }
        foreach ($posts as $ID => $post_data) {
      // 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
   {
      $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') && array_key_exists('timeline', $post_data)) {
            $results[$ID] =$this->processTimelinePost($ID, $post_data);
            // Handle timeline posts - ensure we have a valid integer ID
            $parent_id = (int)$ID;
            // Skip if ID is invalid (0, 'null', etc would become 0)
            if ($parent_id === 0) {
               error_log('Invalid timeline parent ID: ' . $ID);
               $results[$ID] = [
                  'success' => false,
                  'message' => 'Invalid parent post ID for timeline'
               ];
               continue;
            }
            $results[$ID] = $this->processTimelinePost($parent_id, $post_data);
            continue;
         }
         if (str_starts_with($ID, 'new')) {
            error_log('New post detected. Creating... with: '.print_r([
            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',
                  '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));
            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',
               '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'
                  'message' => 'Couldn\'t Create Post'
               ];
               continue;
            }
            $fields = jvbGetFields($post_data['content']);
            $allowedFields = array_filter($post_data, function($key) use ($fields) {
            $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
               'success' => $success
            ];
         } else {
            if (!$this->verifyOwnership($ID)) {
@@ -403,7 +519,7 @@
               ];
               continue;
            }
            error_log('Saving post data: '.print_r($post_data, true));
            error_log('Saving post data: ' . print_r($post_data, true));
            if (array_key_exists('post_status', $post_data)) {
               switch ($post_data['post_status']) {
@@ -415,7 +531,7 @@
                     break;
                  case 'draft':
                     $result = wp_update_post([
                        'ID'  => $ID,
                        'ID' => $ID,
                        'post_status' => 'draft'
                     ]);
                     break;
@@ -427,26 +543,26 @@
                     return ['success' => (bool)$result];
               }
            }
            error_log('Updating data: '.print_r($post_data, true));
            error_log('Updating data: ' . print_r($post_data, true));
            $fields = jvbGetFields($post_data['content']);
            $allowedFields = array_filter($post_data, function($key) use ($fields) {
            $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));
            error_log('Allowed Fields: ' . print_r($allowedFields, true));
            $meta = new MetaManager($ID, 'post');
            $success = $meta->setAll($allowedFields);
            $results[$ID] = [
               'success'   => $success
               'success' => $success
            ];
         }
            CacheManager::for($post_data['content'])->clear();
         CacheManager::for($post_data['content'])->clear();
         if (jvbSiteUsesFeedBlock()) {
            CacheManager::for('feed')->clear();
         }
        }
      }
      if (jvbSiteHasNotifications()) {
         $this->notifications = JVB()->notification();
@@ -459,11 +575,11 @@
      }
        return [
            'success' => true,
            'result' => $results
        ];
    }
      return [
         'success' => true,
         'result' => $results
      ];
   }
   /**
    * Extracts the postdata for timeline post child posts from the pseudo-repeater element
@@ -471,7 +587,7 @@
    * @param array $post_data
    * @return array|true[]
    */
   protected function processTimelinePost(int $parent_id, array $post_data):array
   protected function processTimelinePost(int $parent_id, array $post_data): array
   {
      if (!$this->verifyOwnership($parent_id)) {
         return ['success' => false, 'message' => 'No permission'];
@@ -500,7 +616,7 @@
      $clearParent = false;
      if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
         // Remove post_title and post_thumbnail from shared taxonomies
         $sharedTaxonomies = array_filter($sharedData, function($key) {
         $sharedTaxonomies = array_filter($sharedData, function ($key) {
            return $key !== 'post_title' && $key !== 'post_thumbnail';
         }, ARRAY_FILTER_USE_KEY);
@@ -561,14 +677,14 @@
            'post_parent' => $parent_id,
            'orderby' => 'menu_order',
            'post_status' => ['publish', 'draft'],
            'fields'=> 'ids'
            'fields' => 'ids'
         ]);
         $prevDate = null;
         foreach($post_data['timeline'] as $order => $timeline) {
         foreach ($post_data['timeline'] as $order => $timeline) {
            // Get unique fields for this specific timeline entry
            $allowedFields = array_filter($timeline, function($key) use ($ignore) {
            $allowedFields = array_filter($timeline, function ($key) use ($ignore) {
               return in_array($key, $this->timelineUniqueFields) && !in_array($key, $ignore);
            }, ARRAY_FILTER_USE_KEY);
@@ -636,7 +752,7 @@
            if (count($post_updates) > 1) {
               $result = wp_update_post($post_updates);
               error_log('Updated post '.$timeline['id'].' with: '.print_r($post_updates, true).' Result: '.$result);
               error_log('Updated post ' . $timeline['id'] . ' with: ' . print_r($post_updates, true) . ' Result: ' . $result);
               $clearParent = true;
            }
@@ -647,9 +763,9 @@
            // Set number taxonomy to menu_order (always update for reordering)
            if (!$is_parent) {
               $number_value = $order;
               $term = get_term_by('name', (string)$number_value, BASE.'number');
               $term = get_term_by('name', (string)$number_value, BASE . 'number');
               if (!$term) {
                  $result = wp_insert_term((string)$number_value, BASE.'number');
                  $result = wp_insert_term((string)$number_value, BASE . 'number');
                  if ($result && !is_wp_error($result)) {
                     $term = $result['term_id'];
                  }
@@ -667,10 +783,10 @@
                  $date2 = new \DateTime($newDate);
                  $weeks = floor($date1->diff($date2)->days / 7);
                  if ($weeks > 0) {
                     $termToCheck = $weeks.' Weeks';
                     $term = get_term_by('name', $termToCheck, BASE.'timeline');
                     $termToCheck = $weeks . ' Weeks';
                     $term = get_term_by('name', $termToCheck, BASE . 'timeline');
                     if (!$term) {
                        $result = wp_insert_term($termToCheck, BASE.'timeline');
                        $result = wp_insert_term($termToCheck, BASE . 'timeline');
                        if ($result && !is_wp_error($result)) {
                           $term = $result['term_id'];
                        }
@@ -683,13 +799,13 @@
            }
            $prevDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : $prevDate);
            $updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
            $updateValues = array_filter($allowedFields, function ($value, $key) use ($oldValues) {
               return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
            }, ARRAY_FILTER_USE_BOTH);
            error_log('Setting values for '.$timeline['id'].': '.print_r($updateValues, true));
            error_log('Setting values for ' . $timeline['id'] . ': ' . print_r($updateValues, true));
            $meta->setAll($updateValues);
            $timeline['id'] = (int) $timeline['id'];
            $timeline['id'] = (int)$timeline['id'];
            $success[] = $timeline['id'];
         }
@@ -714,130 +830,131 @@
      ]];
   }
    /**
     * 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...',
            ]);
        }
   /**
    * 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...',
         ]);
      }
        $data = $request->get_params();
      $data = $request->get_params();
        $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'
            ]
        );
      $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;
    }
      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
    {
   /**
    * @param WP_Post $post the 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')) {
         $this->initTimelineFields($post->post_type);
         return $this->formatTimeline($post);
      }
        $this->meta = new MetaManager($post->ID, 'post');
        $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'      => $this->post_type,
            'taxonomies'=> [],
            'fields'    => ($fields) ? $this->meta->getAll() : [],
         'images' => [],
        ];
      $this->meta = new MetaManager($post->ID, 'post');
      $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' => $this->post_type,
         'taxonomies' => [],
         'fields' => ($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
            ];
        }
      // 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;
        }
      if (!empty($images)) {
         $data['images'] = $images;
      }
        return $data;
    }
   protected function extractImages(array $fields = []):array
      return $data;
   }
   protected function extractImages(array $fields = []): array
   {
      //Extract images
      $images = [];
@@ -851,11 +968,11 @@
      if (!empty($get)) {
         $allImages = $this->meta->getAll($get);
         foreach($allImages as $k => $imgs){
         foreach ($allImages as $k => $imgs) {
            $temp = explode(',', $imgs);
            foreach($temp as $img) {
            foreach ($temp as $img) {
               if (is_numeric($img) && !array_key_exists($img, $images)) {
                  $images[$img] = jvbImageData((int) $img);
                  $images[$img] = jvbImageData((int)$img);
               }
            }
         }
@@ -863,7 +980,7 @@
      return $images;
   }
   protected function formatTimeline(WP_Post $post):array
   public function formatTimeline(WP_Post $post): array
   {
      $item = $this->prepareItem($post, true, false);
      //Step 1: Get the fields that apply to all posts
@@ -871,7 +988,7 @@
      $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
      //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']);
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields' => 'ids']);
      array_unshift($children, $post->ID);
      $subFields = [];
@@ -879,184 +996,185 @@
      foreach ($children as $child) {
         $meta = new MetaManager($child, 'post');
         $f = $meta->getAll($this->timelineUniqueFields);
         $f =  ['id' => $child] + $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['images'] = $item['images'] + $images;
      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));
   /**
    * 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)
                ];
            }
        }
      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;
    }
      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 = [];
   /**
    * 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['after'])) {
         $query['after'] = sanitize_text_field($date_params['after']);
      }
        if (!empty($date_params['before'])) {
            $query['before'] = sanitize_text_field($date_params['before']);
        }
      if (!empty($date_params['before'])) {
         $query['before'] = sanitize_text_field($date_params['before']);
      }
        if (isset($date_params['inclusive'])) {
            $query['inclusive'] = (bool)$date_params['inclusive'];
        }
      if (isset($date_params['inclusive'])) {
         $query['inclusive'] = (bool)$date_params['inclusive'];
      }
        return empty($query) ? [] : [$query];
    }
      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;
    }
   /**
    * @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();
   /**
    * 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;
         $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]);
         $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;
                     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
                                        ]);
                           }
                           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;
                              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;
                              }
                           }
                           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
               //Clear cache
               CacheManager::for($data['content'])->clear();
                    CacheManager::for('feed')->clear();
                }
               CacheManager::for('feed')->clear();
            }
            return [
               'success'   => true,
               'result' => $results
               'success' => true,
               'result' => $results
            ];
            } catch (Exception $e) {
                $JVB->error()->log(
                    '[ContentRoutes]:processOperation',
                    $e->getMessage()
                );
            }
         } catch (Exception $e) {
            $JVB->error()->log(
               '[ContentRoutes]:processOperation',
               $e->getMessage()
            );
         }
            return $results;
        } elseif ($operation->type == 'content_update') {
            $result = $this->processBatches($operation, $data);
        }
         return $results;
      } elseif ($operation->type == 'content_update') {
         $result = $this->processBatches($operation, $data);
      }
        return $result;
    }
      return $result;
   }
}