Jake Vanderwerf
2025-11-10 e9967fa22781d922ba4eb8fb44fe72d200ac4b14
inc/rest/routes/ContentRoutes.php
@@ -5,6 +5,8 @@
use JVBase\rest\RestRouteManager;
use JVBase\managers\CacheManager;
use JVBase\meta\MetaManager;
use JVBase\utility\Features;
use WP_Post;
use WP_Query;
use WP_Error;
use WP_REST_Request;
@@ -23,14 +25,18 @@
    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: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create
    //TODO: Also invalidate feed caches on updates!!
    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);
@@ -73,6 +79,32 @@
        ]);
    }
   protected function initTimelineFields(string $content):void
   {
      $content = jvbNoBase($content);
      if (!Features::forContent($content)->has('is_timeline')){
         return;
      }
      $config = Features::getConfig($content);
      $this->fields = $config['fields'];
      $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) {
         if (!array_key_exists('for_all', $field) || $field['for_all'] === false){
            return true;
         }
         return false;
      }));
      array_unshift($this->timelineSharedFields, 'post_thumbnail');
      array_unshift($this->timelineSharedFields, 'post_title');
      $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) {
         if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
            return true;
         }
         return false;
      }));
   }
    /**
     * Handle content update/creation
     * @param WP_REST_Request $request
@@ -185,9 +217,6 @@
        }
        $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
      $config = (array_key_exists($params['content'], JVB_CONTENT) && !empty(JVB_CONTENT[$params['content']])) ? JVB_CONTENT[$params['content']] : [];
        // Build query args
        $args = [
@@ -199,9 +228,13 @@
            '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 (jvbCheck('is_calendar', $config))  {
      if (Features::forContent($post_type)->has('is_calendar'))  {
         $args = $this->applyCalendarFilters($args, $params);
      }
      if (array_key_exists('taxonomies', $params)) {
@@ -218,27 +251,26 @@
         $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');
      // Check HTTP cache headers with the specific content type
      $content_type = $params['content'] ?? $params['type'];
      $cache_check = $this->checkHeaders($request, $content_type, [
         'filter_hash' => $key,
      ]);
      if ($cache_check) {
         return $cache_check;
      }
        $cache = $this->cache->get($key);
      $cache = false;
        if ($cache) {
            return new WP_REST_Response($cache);
            $response = new WP_REST_Response($cache);
         return $this->addCacheHeaders($response);
        }
        // Run query
@@ -260,7 +292,8 @@
        $this->cache->set($key, $data);
        return new WP_REST_Response($data);
        $response = new WP_REST_Response($data);
      return $this->addCacheHeaders($response);
    }
    /**
@@ -306,6 +339,10 @@
        $results = [];
        foreach ($posts as $ID => $post_data) {
         if (Features::forContent($post_data['content'])->has('is_timeline')) {
            $results[$ID] =$this->processTimelinePost($ID, $post_data);
            continue;
         }
         if (str_starts_with($ID, 'new')) {
            error_log('New post detected. Creating... with: '.print_r([
@@ -386,14 +423,12 @@
         }
            CacheManager::invalidateGroup($post_data['content']);
            CacheManager::for($post_data['content'])->clear();
         if (jvbSiteUsesFeedBlock()) {
            CacheManager::invalidateGroup($post_data['feed']);
            CacheManager::for('feed')->clear();
         }
        }
        CacheManager::invalidateGroup('user_content');
      if (jvbSiteHasNotifications()) {
         $this->notifications = JVB()->notification();
         $this->notifications->addNotification(
@@ -411,6 +446,123 @@
        ];
    }
   /**
    * Extracts the postdata for timeline post child posts from the pseudo-repeater element
    * @param int $parent_id
    * @param array $post_data
    * @return array|true[]
    */
   protected function processTimelinePost(int $parent_id, array $post_data):array
   {
      if (!$this->verifyOwnership($parent_id)) {
         return ['success' => false, 'message' => 'No permission'];
      }
      $this->fields = jvbGetFields($post_data['content']);
      $this->initTimelineFields($post_data['content']);
      error_log('Received Data: '.print_r($post_data, true));
      // First, process the main fields that will apply to all posts
      $sharedData = array_filter($post_data, function ($key) {
         return in_array($key, $this->timelineSharedFields);
      }, ARRAY_FILTER_USE_KEY);
      //Next, process any individual posts, including any menu order changes
      if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) {
         $sharedTaxonomies = $sharedData;
         unset($sharedTaxonomies['post_title']);
         //Ensure the parent post exists and is still first in the array
         $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
         error_log('Found index: '.print_r($index, true));
         if ($index === false) {
            return [
               'success' => false,
               'message'   => 'Missing parent id. This should not have happened'
            ];
         } elseif ($index !== 0) {
            // Move that element to the start of the array
            $item = $post_data['timeline'][$index];
            unset($post_data['timeline'][$index]);
            array_unshift($post_data['timeline'], $item);
         }
         $errors = [];
         $success = [];
         // Get existing children to track deletions
         $existing_children = get_children([
            'post_parent' => $parent_id,
            'post_type' => jvbCheckBase($post_data['content']),
            'fields' => 'ids'
         ]);
         //Iterate through the timeline posts
         foreach($post_data['timeline'] as $order => $timeline) {
            $allowedFields = array_filter($timeline, function($key) {
               return in_array($key, $this->timelineUniqueFields);
            }, ARRAY_FILTER_USE_KEY);
            $allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order;
            $allowedFields = array_merge($allowedFields, $sharedTaxonomies);
            if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) {
               $newChild = wp_insert_post([
                  'post_author'  => $this->user_id,
                  'post_type'    => jvbCheckBase($post_data['content']),
                  'post_title'   => $allowedFields['post_title'],
                  'post_parent'  => $parent_id,
                  'menu_order'   => $order
               ]);
               if (!$newChild || is_wp_error($newChild)) {
                  $errors[] = [
                     'message'   => 'Could not create child post',
                     'data'      => $timeline
                  ];
                  continue;
               }
               $timeline['id'] = $newChild;
            }
            if (in_array((int)$timeline['id'], $existing_children)) {
               unset($existing_children[array_search((int)$timeline['id'], $existing_children)]);
            }
            //Determine which fields to update
            $meta = new MetaManager($timeline['id'], 'post');
            $oldValues = $meta->getAll(array_keys($allowedFields));
            $updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) {
               return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
            }, ARRAY_FILTER_USE_BOTH);
            $meta->setAll($updateValues);
            //Update Menu Order, if applicable
            if ((int) $timeline['id'] !== $parent_id) {
               $post = get_post((int) $timeline['id']);
               if ($post && $post->menu_order !== $order) {
                  $updated = wp_update_post([
                     'ID'        => $post->ID,
                     'menu_order'   => $order,
                  ]);
                  if (!$updated || is_wp_error($updated)) {
                     $errors[] = [
                        'message'   => 'Could not update timeline order for post',
                        'data'      => $timeline
                     ];
                  }
               }
            }
            $success[] = $timeline['id'];
         }
      }
      //Delete any remaining children that no longer exist
      if (!empty($existing_children)) {
         foreach ($existing_children as $ID) {
            wp_delete_post($ID);
         }
      }
      return ['success' => true, 'data' => [
         'success'   => $success,
         'errors' => $errors
      ]];
   }
    /**
     * Handle batch content creation from uploads
     * @param WP_REST_Request $request
@@ -485,12 +637,16 @@
    }
    /**
     * @param object $post the wordpress post object
     * @param WP_Post $post the wordpress post object
     *
     * @return array
     */
    protected function prepareItem(object $post):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,
@@ -501,7 +657,7 @@
            'alt'       => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
            'icon'      => $this->post_type,
            'taxonomies'=> [],
            'fields'    => $this->meta->getAll(),
            'fields'    => ($fields) ? $this->meta->getAll() : [],
         'images' => [],
        ];
@@ -520,15 +676,26 @@
            ];
        }
      $images = $this->extractImages();
        //Extract images
        if (!empty($images)) {
            $data['images'] = $images;
        }
        return $data;
    }
   protected function extractImages(array $fields = []):array
   {
      //Extract images
      $images = [];
      $get = [];
        foreach ($this->fields as $field => $config) {
            if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
      $fields = (empty($fields)) ? $this->fields : $fields;
      foreach ($fields as $field => $config) {
         if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
            $get[] = $field;
            }
        }
         }
      }
      if (!empty($get)) {
         $allImages = $this->meta->getAll($get);
@@ -541,13 +708,35 @@
            }
         }
      }
      return $images;
   }
        if (!empty($images)) {
            $data['images'] = $images;
        }
   protected function formatTimeline(WP_Post $post):array
   {
      $item = $this->prepareItem($post, true, false);
      //Step 1: Get the fields that apply to all posts
      $mainMeta = new MetaManager($post->ID, 'post');
      $item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
        return $data;
    }
      //Step 2: Get the fields for each individual posts
      $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']);
      array_unshift($children, $post->ID);
      $subFields = [];
      $images = [];
      foreach ($children as $child) {
         $meta = new MetaManager($child, 'post');
         $f = $meta->getAll($this->timelineUniqueFields);
         $f =  ['id' => $child] + $f;
         $subFields[$f['post_thumbnail']] = $f;
         $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']);
      }
      $item['fields']['timeline'] = $subFields;
      $item['images'] = $item['images'] + $images;
      return $item;
   }
    /**
     * Builds the taxonomy query
@@ -696,9 +885,8 @@
                    }
                    //Clear cache
                    CacheManager::invalidateGroup($data['content']);
                    CacheManager::invalidateGroup('feed');
                    CacheManager::invalidateGroup('user_content');
               CacheManager::for($data['content'])->clear();
                    CacheManager::for('feed')->clear();
                }
            return [