From 42fa8304ddb811b0f725f245130f70c0f5e86a6c Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Tue, 04 Nov 2025 06:12:02 +0000
Subject: [PATCH] =Refactored LoginManager to be more extensible and configurable, as well as an AjaxRateLimiter

---
 inc/rest/routes/ContentRoutes.php |  197 ++++++++++++++++++++++++++++++++++++++++++------
 1 files changed, 170 insertions(+), 27 deletions(-)

diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index 6916093..ff056a9 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/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;
@@ -185,7 +187,7 @@
         }
         $post_type = str_replace('-', '_',jvbCheckBase($params['content']));
 
-		$config = (array_key_exists($params['content'], JVB_CONTENT) && !empty(JVB_CONTENT[$params['content']])) ? JVB_CONTENT[$params['content']] : [];
+		$config = Features::getConfig($params['content']);
 
 
 
@@ -199,9 +201,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 +224,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 +265,8 @@
 
         $this->cache->set($key, $data);
 
-        return new WP_REST_Response($data);
+        $response = new WP_REST_Response($data);
+		return $this->addCacheHeaders($response);
     }
 
     /**
@@ -306,6 +312,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([
@@ -411,6 +421,97 @@
         ];
     }
 
+	/**
+	 * 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'];
+		}
+
+		$rows = $post_data['fields'] ?? [];
+		if (empty($rows)) {
+			return ['success' => false, 'message' => 'No data'];
+		}
+
+		$fields = jvbGetFields($post_data['content']);
+
+		// First row = parent post
+		$parent_row = array_shift($rows);
+		if (($parent_row['id'] ?? null) != $parent_id) {
+			return ['success' => false, 'message' => 'Parent ID mismatch'];
+		}
+
+		$allowedFields = array_filter($parent_row, function($key) use ($fields) {
+			return array_key_exists($key, $fields);
+		}, ARRAY_FILTER_USE_KEY);
+
+		$parentMeta = new MetaManager($parent_id, 'post');
+		$parentMeta->setAll($allowedFields);
+
+		// Get existing children to track deletions
+		$existing_children = get_children([
+			'post_parent' => $parent_id,
+			'post_type' => jvbCheckBase($post_data['content']),
+			'fields' => 'ids'
+		]);
+
+		$processed_ids = [];
+
+		// Process remaining rows as children
+		foreach ($rows as $index => $row_data) {
+			$row_id = $row_data['id'] ?? null;
+
+			// New child post
+			if (!$row_id || str_starts_with($row_id, 'new')) {
+				$child_id = wp_insert_post([
+					'post_type' => jvbCheckBase($post_data['content']),
+					'post_parent' => $parent_id,
+					'post_author' => $this->user_id,
+					'post_status' => $post_data['status'] ?? 'draft',
+					'menu_order' => $index
+				]);
+			}
+			// Existing child post
+			else {
+				$child_id = (int) $row_id;
+
+				// Verify ownership via parent
+				if (!in_array($child_id, $existing_children)) {
+					continue; // Skip if not actually a child of this parent
+				}
+
+				// Update menu_order (position may have changed)
+				wp_update_post([
+					'ID' => $child_id,
+					'menu_order' => $index
+				]);
+			}
+
+			// Update child meta
+			$allowedChildFields = array_filter($row_data, function($key) use ($fields) {
+				return array_key_exists($key, $fields) && $key !== 'id' && $key !== 'draggable';
+			}, ARRAY_FILTER_USE_KEY);
+
+			$childMeta = new MetaManager($child_id, 'post');
+			$childMeta->setAll($allowedChildFields);
+
+			$processed_ids[] = $child_id;
+		}
+
+		// Delete removed children
+		$deleted_ids = array_diff($existing_children, $processed_ids);
+		foreach ($deleted_ids as $delete_id) {
+			wp_delete_post($delete_id, true);
+		}
+
+		return ['success' => true, 'processed' => $processed_ids];
+	}
+
     /**
      * Handle batch content creation from uploads
      * @param WP_REST_Request $request
@@ -485,12 +586,15 @@
     }
 
     /**
-     * @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, $skip = false):array
     {
+		if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
+			return $this->formatTimeline($post);
+		}
         $this->meta = new MetaManager($post->ID, 'post');
         $data = [
             'id'        => $post->ID,
@@ -520,15 +624,25 @@
             ];
         }
 
+		$images = $this->extractImages();
 
-        //Extract images
+
+        if (!empty($images)) {
+            $data['images'] = $images;
+        }
+
+        return $data;
+    }
+	protected function extractImages():array
+	{
+		//Extract images
 		$images = [];
 		$get = [];
-        foreach ($this->fields as $field => $config) {
-            if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
+		foreach ($this->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 +655,42 @@
 				}
 			}
 		}
+		return $images;
+	}
 
-        if (!empty($images)) {
-            $data['images'] = $images;
-        }
+	protected function formatTimeline(WP_Post $post):array
+	{
+		$data = $this->prepareItem($post, true);
+		$firstRow = $data['fields'];
+		$firstRow['id'] = $post->ID;
+		$firstRow['draggable'] = false;
+		$fields = [$firstRow];
 
-        return $data;
-    }
+		$children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order']);
+		$allImages = [];
+
+		foreach ($children as $child) {
+			$this->meta = new MetaManager($child->ID, 'post');
+			$row = $this->meta->getAll();  // Store in variable first
+			$row['id'] = $child->ID;       // Add ID to the row
+			$row['draggable'] = true;      // Mark as draggable
+			$fields[] = $row;              // Then append to fields
+
+			$images = $this->extractImages();
+			if (!empty($images)) {
+				$allImages = $allImages + $images;
+			}
+		}
+
+		if (!empty($allImages)) {
+			if (!array_key_exists('images', $data)) {
+				$data['images'] = [];
+			}
+			$data['images'] = $data['images'] + $allImages;
+		}
+		$data['fields']['timeline'] = $fields;
+		return $data;
+	}
 
     /**
      * Builds the taxonomy query

--
Gitblit v1.10.0