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 | 1406 ++++++++++++++++++++--------------------------------------
1 files changed, 482 insertions(+), 924 deletions(-)
diff --git a/inc/rest/routes/ContentRoutes.php b/inc/rest/routes/ContentRoutes.php
index e1b457a..dd25f8e 100644
--- a/inc/rest/routes/ContentRoutes.php
+++ b/inc/rest/routes/ContentRoutes.php
@@ -1,92 +1,114 @@
<?php
+
namespace JVBase\rest\routes;
-use JVBase\JVB;
-use JVBase\rest\RestRouteManager;
-use JVBase\managers\CacheManager;
-use JVBase\meta\MetaManager;
-use JVBase\utility\Features;
+use JVBase\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 = '';
//For Timeline-specific posts
protected array $timelineSharedFields = [];
protected array $timelineUniqueFields = [];
+ protected static ?string $action = 'dash-';
+ protected Meta $meta;
- //TODO: Ensure we are handling the bulk operations for all processes
- //TODO: Also invalidate feed caches on updates!!
+ public function __construct()
+ {
+ $this->cacheName = 'user_content_' . 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);
+ }
- public function __construct()
- {
- $this->cache_name = 'user_content_'.get_current_user_id();
- parent::__construct();
+ /**
+ * Register content operation types with the queue's TypeRegistry
+ */
+ public function registerContentExecutors(): void
+ {
+ $registry = JVB()->queue()->registry();
+ $executor = new ContentExecutor();
- $this->action = 'dash-';
- $this->operation_type = 'content_update';
- add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
- }
+ // Content updates - chunked at 10 posts
+ $registry->register('content_update', new TypeConfig(
+ executor: $executor,
+ chunkKey: 'posts',
+ chunkSize: 10
+ ));
- /**
- * 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']
- ]
- ]);
+ // Batch creation (from uploads) TODO: I believe this is all handled by UploadExecutor
+// $registry->register('batch_creation', new TypeConfig(
+// executor: $executor
+// ));
+ }
- //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']
- ]
- ]);
- }
+ /**
+ * 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
+ protected function initTimelineFields(string $content): void
{
$content = jvbNoBase($content);
- if (!Features::forContent($content)->has('is_timeline')){
+
+ $config = Registrar::getInstance($content);
+ if (!$config || !$config->hasFeature('is_timeline')) {
return;
}
- $config = Features::getConfig($content);
- $this->fields = $config['fields'];
+ $this->fields = $config->getFields();
$this->timelineSharedFields = $this->getTimelineSharedFields($content);
array_unshift($this->timelineSharedFields, 'post_thumbnail');
@@ -95,14 +117,16 @@
$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')){
+ $registrar = Registrar::getInstance($content);
+ if (!$registrar || !$registrar->hasFeature('is_timeline')) {
return [];
}
- $config = Features::getConfig($content);
- $allFields = $config['fields'];
+
+ $allFields = $registrar->getFields();
return array_keys(array_filter($allFields, function ($field) {
if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
@@ -112,951 +136,485 @@
}));
}
- public function getTimelineSharedFields(string $content):array
+ public function getTimelineSharedFields(string $content): array
{
$content = jvbNoBase($content);
- if (!Features::forContent($content)->has('is_timeline')){
+ $registrar = Registrar::getInstance($content);
+ if (!$registrar || !$registrar->hasFeature('is_timeline')) {
return [];
}
- $config = Features::getConfig($content);
- if (!$config || empty($config)) {
- return [];
- }
- $allFields = $config['fields']??[];
+
+ $allFields = $registrar->getFields()??[];
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) || 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 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 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();
- $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 getContent(WP_REST_Request $request): WP_REST_Response
+ {
+ $params = $request->get_params();
+ error_log('getContent::params '.print_r($params, true));
- $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']));
+ $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);
+ }
+
+ return $this->error('Something went wrong, this does not appear to have a proper content type');
+ }
+
+ public function getPosts(WP_REST_Request $request, array $params, Registrar $registrar):WP_REST_Response
+ {
+ $user_id = $params['user'];
+
+ $post_status = $params['status'];
+ if ($post_status === 'all') {
+ $post_status = ['publish', 'draft'];
+ } else {
+ $post_status = explode(',', $post_status);
+ }
+ $post_type = str_replace('-', '_', jvbCheckBase($params['content']));
+
+ // Build query args
+ $args = [
+ 'post_type' => $post_type,
+ 'posts_per_page' => $params['per_page'] ?? 30,
+ 'paged' => $params['page'],
+ 'orderby' => 'date',
+ 'order' => 'DESC',
+ 'author' => $user_id,
+ 'post_status' => $post_status
+ ];
- // Build query args
- $args = [
- 'post_type' => $post_type,
- 'posts_per_page' => $params['per_page']??30,
- 'paged' => $params['page'],
- 'orderby' => 'date',
- 'order' => 'DESC',
- 'author' => $user_id,
- 'post_status' => $post_status
- ];
//Only top level posts for timeline types
- if (Features::forContent($post_type)->has('is_timeline')) {
+ if ($registrar?->hasFeature('is_timeline')) {
$args['post_parent'] = 0;
}
//Calendar filters
- if (Features::forContent($post_type)->has('is_calendar')) {
+ if ($registrar?->hasFeature('is_calendar')) {
$args = $this->applyCalendarFilters($args, $params);
}
+ $taxonomies = array_filter($params, function ($param) {
+ return str_starts_with($param, 'tax_');
+ }, ARRAY_FILTER_USE_KEY);
+ if (!empty($taxonomies)) {
+ $params['taxonomies'] = [];
+ foreach ($taxonomies as $taxonomy => $terms) {
+ $taxonomy = str_replace('tax_', '', $taxonomy);
+ $params['taxonomies'][$taxonomy] = $terms;
+ }
+ }
if (array_key_exists('taxonomies', $params)) {
$args = $this->applyTaxonomyFilters($args, $params);
}
- if (array_key_exists('date', $params) && !empty($params['date'])) {
+ if (array_key_exists('date-filter', $params) || array_key_exists('dateFrom', $params)) {
$args = $this->applyDateFilters($args, $params);
}
if (array_key_exists('orderby', $params) || array_key_exists('order', $params)) {
$args = $this->applyOrderFilters($args, $params);
}
- if (!empty($params['search'])) {
+ if (array_key_exists('search', $params)) {
$args['s'] = sanitize_text_field($params['search']);
}
- $key = $this->cache->generateKey($args);
+ $key = $this->cache->generateKey($args);
+ $cached = $this->checkCache($key, $request);
+ if ($cached) {
+ return $cached;
+ }
+
+ $this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
+
+ if (array_key_exists('s', $args)) {
+ $args = $this->applySearchFilters($args, $params);
+ }
+
+ // Run query
+ $query = new WP_Query($args);
+
+ $registrar = Registrar::getInstance($this->post_type);
+ $this->fields = $registrar->getFields()??[];
+ $this->taxonomies = $this->getTaxonomies($this->post_type);
+
+ $posts = array_map([$this, 'preparePost'], $query->posts);
+
+ $data = [
+ 'items' => $posts,
+ 'total' => $query->found_posts,
+ 'total_pages' => $query->max_num_pages,
+ 'has_more' => $args['paged']??1 < $query->max_num_pages,
+ ];
+
+
+ $this->cache->set($key, $data);
+
+ $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'];
+ }
+
+ //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);
+ }
+
+ if (array_key_exists('search', $params)) {
+ $args['s'] = sanitize_text_field($params['search']);
+ }
+
+ $key = $this->cache->generateKey($args);
// Check HTTP cache headers with the specific content type
- $content_type = $params['content'] ?? $params['type'];
- $cache_check = $this->checkHeaders($request, $content_type, [
- 'filter_hash' => $key,
- ]);
+ $cache_check = $this->checkHeaders($request, $key);
if ($cache_check) {
return $cache_check;
}
-
- $cache = $this->cache->get($key);
- $cache = false;
- if ($cache) {
- $response = new WP_REST_Response($cache);
+ $cache = $this->cache->get($key);
+ if ($cache) {
+ $response = Response::success($cache);
return $this->addCacheHeaders($response);
- }
-
- // Run query
- $query = new WP_Query($args);
-
- $this->post_type = $params['content']??$params['type'];
-
- $this->fields = jvbGetFields(str_replace('-','_',$this->post_type));
- $this->taxonomies = $this->getTaxonomies($this->post_type);
- $posts = array_map([$this, 'prepareItem'], $query->posts);
-
-
- $data = [
- 'items' => $posts,
- 'total' => $query->found_posts,
- 'total_pages' => $query->max_num_pages
- ];
-
-
- $this->cache->set($key, $data);
-
- $response = new WP_REST_Response($data);
- return $this->addCacheHeaders($response);
- }
-
- /**
- * Gets allowed taxonomies for a particular content
- * @param string $content
- *
- * @return array
- */
- protected function getTaxonomies(string $content):array
- {
- $taxonomy_for = jvbGlobalTaxonomyFor();
- $out = [];
- foreach ($taxonomy_for as $tax => $postTypes) {
- if (in_array($content, $postTypes)) {
- $out[BASE.$tax] = [
- 'label' => JVB_CONTENT[$content]['plural'],
- 'icon' => $tax,
- ];
- }
- }
- return $out;
- }
-
- /**
- * Processes operation from queue
- * @param object $operation
- * @param array $data
- *
- * @return array
- */
- protected function processBatches(object $operation, array $data):array
- {
- $this->user_id = $operation->user_id;
- $posts = $data['posts'];
-
- if (empty($posts)) {
- return [
- 'success' => false,
- 'message' => 'No posts to update'
- ];
- }
-
- $results = [];
-
- foreach ($posts as $ID => $post_data) {
- if (Features::forContent($post_data['content'])->has('is_timeline') && array_key_exists('timeline', $post_data)) {
- $results[$ID] =$this->processTimelinePost($ID, $post_data);
- continue;
- }
- if (str_starts_with($ID, 'new')) {
-
- error_log('New post detected. Creating... with: '.print_r([
- 'post_author' => $this->user_id,
- 'post_type' => jvbCheckBase($post_data['content']),
- 'post_title' => $post_data['post_title']??'',
- 'post_status' => $post_data['status']??'draft',
- ], true));
- error_log('Recieved Data: '.print_r($post_data, true));
- $ID = wp_insert_post([
- 'post_author' => $this->user_id,
- 'post_type' => jvbCheckBase($post_data['content']),
- 'post_title' => $post_data['post_title']??'',
- 'post_status' => $post_data['status']??'draft',
- ]);
- if (!$ID || is_wp_error($ID)) {
- $results[$ID] = [
- 'success' => false,
- 'message' => 'Couldn\'t Create Post'
- ];
- continue;
- }
- $fields = jvbGetFields($post_data['content']);
- $allowedFields = array_filter($post_data, function($key) use ($fields) {
- return array_key_exists($key, $fields);
- }, ARRAY_FILTER_USE_KEY);
-
- $meta = new MetaManager($ID, 'post');
- $success = $meta->setAll($allowedFields);
- $results[$ID] = [
- 'success' => $success
- ];
- } else {
- if (!$this->verifyOwnership($ID)) {
- $results[$ID] = [
- 'success' => false,
- 'message' => 'No permission to modify this post'
- ];
- continue;
- }
- error_log('Saving post data: '.print_r($post_data, true));
-
- if (array_key_exists('post_status', $post_data)) {
- switch ($post_data['post_status']) {
- case 'publish':
- unset($post_data['post_status']);
- if (user_can($this->user_id, 'manage_options') || user_can($this->user_id, 'skip_moderation')) {
- $result = wp_update_post(['ID' => $ID, 'post_status' => 'publish']);
- }
- break;
- case 'draft':
- $result = wp_update_post([
- 'ID' => $ID,
- 'post_status' => 'draft'
- ]);
- break;
- case 'trash':
- $result = wp_trash_post($ID);
- break;
- case 'delete':
- $result = wp_delete_post($ID, true);
- return ['success' => (bool)$result];
- }
- }
- error_log('Updating data: '.print_r($post_data, true));
- $fields = jvbGetFields($post_data['content']);
- $allowedFields = array_filter($post_data, function($key) use ($fields) {
- return array_key_exists($key, $fields);
- }, ARRAY_FILTER_USE_KEY);
-
- error_log('Allowed Fields: '.print_r($allowedFields, true));
- $meta = new MetaManager($ID, 'post');
- $success = $meta->setAll($allowedFields);
- $results[$ID] = [
- 'success' => $success
- ];
-
- }
-
- CacheManager::for($post_data['content'])->clear();
- if (jvbSiteUsesFeedBlock()) {
- CacheManager::for('feed')->clear();
- }
- }
-
- if (jvbSiteHasNotifications()) {
- $this->notifications = JVB()->notification();
- $this->notifications->addNotification(
- $this->user_id,
- 'content_update_complete',
- null,
- 'Content updates completed!'
- );
+ }
+ $this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
+ // Only expand search to taxonomies if we're actually going to query
+ if (array_key_exists('s', $args)) {
+ $args = $this->applySearchFilters($args, $params);
}
+ // Run query
+ $query = new WP_Term_Query($args);
- return [
- 'success' => true,
- 'result' => $results
- ];
- }
+ $terms = $query->get_terms();
+ $data = [
+ 'total' => 0,
+ 'total_pages' => 0,
+ 'has_more' => false
+ ];
- /**
- * 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'];
- }
-
- $ignore = ['content', 'user'];
- $this->fields = jvbGetFields($post_data['content']);
- $this->initTimelineFields($post_data['content']);
-
- // Get parent post details
- $parent_post = get_post($parent_id);
- $parent_title = $parent_post->post_title;
- $parent_is_published = ($parent_post->post_status === 'publish');
-
- // Extract shared data from top level (excluding post_thumbnail which is unique per post)
- $sharedData = array_filter($post_data, function ($key) use ($ignore) {
- return in_array($key, $this->timelineSharedFields)
- && !in_array($key, $ignore)
- && $key !== 'post_thumbnail';
- }, ARRAY_FILTER_USE_KEY);
-
- // If no shared post_title at top level, extract from first timeline entry
- if (!isset($sharedData['post_title']) && isset($post_data['timeline'][0]['post_title'])) {
- $sharedData['post_title'] = $post_data['timeline'][0]['post_title'];
- }
- $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) {
- return $key !== 'post_title' && $key !== 'post_thumbnail';
- }, ARRAY_FILTER_USE_KEY);
-
- // Ensure the parent post exists and is still first in the array
- $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id'));
-
- if ($index === false) {
- return [
- 'success' => false,
- 'message' => 'Missing parent id. This should not have happened'
- ];
- }
-
- if ($index !== 0) {
- $new_parent_id = $post_data['timeline'][0]['id'];
-
- if (is_numeric($new_parent_id) && (int)$new_parent_id > 0) {
- $new_parent_id = (int)$new_parent_id;
- wp_update_post([
- 'ID' => $new_parent_id,
- 'post_parent' => 0
- ]);
-
- wp_update_post([
- 'ID' => $parent_id,
- 'post_parent' => $new_parent_id
- ]);
-
- $existing_children = get_children([
- 'post_parent' => $parent_id,
- 'fields' => 'ids'
- ]);
-
- foreach ($existing_children as $child_id) {
- if ($child_id !== $new_parent_id) {
- wp_update_post([
- 'ID' => $child_id,
- 'post_parent' => $new_parent_id
- ]);
- }
- }
-
- // Update parent references
- $parent_id = $new_parent_id;
- $parent_post = get_post($parent_id);
- $parent_title = $parent_post->post_title;
- $parent_is_published = ($parent_post->post_status === 'publish');
- } else {
- $item = $post_data['timeline'][$index];
- unset($post_data['timeline'][$index]);
- array_unshift($post_data['timeline'], $item);
- }
- }
-
- $errors = [];
- $success = [];
- $existing_children = get_children([
- 'post_parent' => $parent_id,
- 'orderby' => 'menu_order',
- 'post_status' => ['publish', 'draft'],
- 'fields'=> 'ids'
+ if (!is_wp_error($terms) && !empty($terms))
+ {
+ $total = get_terms([
+ 'taxonomy' => $args['taxonomy'],
+ 'hide_empty' => false,
+ 'fields' => 'count'
]);
-
- $prevDate = null;
-
- foreach($post_data['timeline'] as $order => $timeline) {
- // Get unique fields for this specific timeline entry
- $allowedFields = array_filter($timeline, function($key) use ($ignore) {
- return in_array($key, $this->timelineUniqueFields) && !in_array($key, $ignore);
- }, ARRAY_FILTER_USE_KEY);
-
- // Determine the post title
- $is_parent = ((int)$timeline['id'] === $parent_id);
- $provided_title = $timeline['post_title'] ?? '';
- $auto_generated_pattern = '/^.+Treatment #?\d+$/'; // Matches "Title - Treatment #1" or "Title - Treatment 1"
-
- if ($is_parent) {
- // Parent keeps its own title or uses shared title
- $allowedFields['post_title'] = $provided_title ?: ($sharedData['post_title'] ?? $parent_title);
- } else {
- // For child posts, auto-generate if:
- // 1. No title provided, OR
- // 2. Title matches auto-generated pattern (meaning it wasn't customized)
- if (empty($provided_title) || preg_match($auto_generated_pattern, $provided_title)) {
- $allowedFields['post_title'] = 'Treatment ' . $order;
- } else {
- // Keep custom title
- $allowedFields['post_title'] = $provided_title;
- }
- }
-
- // Merge with shared taxonomies AFTER setting unique fields
- $allowedFields = array_merge($sharedTaxonomies, $allowedFields);
-
- // Handle post creation if needed
- 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,
- 'post_status' => $parent_is_published ? 'publish' : 'draft'
- ]);
- 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)]);
- }
-
- // Update post status and menu order
- $post_updates = ['ID' => $timeline['id']];
-
- if (!$is_parent) {
- $post_updates['menu_order'] = $order;
-
- // Auto-publish child if parent is published
- if ($parent_is_published) {
- $current_post = get_post($timeline['id']);
- if ($current_post && $current_post->post_status !== 'publish') {
- $post_updates['post_status'] = 'publish';
- }
- }
- }
-
- 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);
- $clearParent = true;
- }
-
- // Update metadata
- $meta = new MetaManager($timeline['id'], 'post');
- $oldValues = $meta->getAll(array_keys($allowedFields));
-
- // 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');
- if (!$term) {
- $result = wp_insert_term((string)$number_value, BASE.'number');
- if ($result && !is_wp_error($result)) {
- $term = $result['term_id'];
- }
- } else {
- $term = $term->term_id;
- }
- $allowedFields['number'] = $term;
- }
-
- // Auto-timeline logic
- if ($prevDate) {
- $newDate = array_key_exists('date', $oldValues) ? $oldValues['date'] : ((array_key_exists('date', $allowedFields)) ? $allowedFields['date'] : null);
- if ($newDate) {
- $date1 = new \DateTime($prevDate);
- $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');
- if (!$term) {
- $result = wp_insert_term($termToCheck, BASE.'timeline');
- if ($result && !is_wp_error($result)) {
- $term = $result['term_id'];
- }
- } else {
- $term = $term->term_id;
- }
- $allowedFields['timeline'] = $term;
- }
- }
- }
- $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) {
- return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]);
- }, ARRAY_FILTER_USE_BOTH);
- error_log('Setting values for '.$timeline['id'].': '.print_r($updateValues, true));
-
- $meta->setAll($updateValues);
- $timeline['id'] = (int) $timeline['id'];
-
- $success[] = $timeline['id'];
- }
+ $data['total'] = $total;
+ $data['total_pages'] = max($total/$args['number'], 1);
+ $data['has_more'] = ($args['page'] * $args['number']) < $total;
+ } else {
+ $terms = [];
}
- // Delete any remaining children that no longer exist
- if (!empty($existing_children)) {
- foreach ($existing_children as $ID) {
- wp_delete_post($ID);
- }
- }
+ $this->fields = $registrar->getFields()??[];
- if ($clearParent) {
- $this->cache->clear();
- CacheManager::onPostSave($parent_id, $parent_post);
- }
+ $this->taxonomies = [];
+ $data['items'] =array_map([$this, 'prepareTerm'], $terms);
+ $this->cache->set($key, $data);
- return ['success' => true, 'data' => [
- 'success' => $success,
- 'errors' => $errors
- ]];
+ $response = Response::success($data);
+ return $this->addCacheHeaders($response);
}
- /**
- * Handle batch content creation from uploads
- * @param WP_REST_Request $request
- *
- * @return WP_REST_Response
- */
- public function handleBatchCreation(WP_REST_Request $request):WP_REST_Response
- {
- //Operation has two parts
- //First, queue image processing
- //Then queue post creation from the stored IDs, depending on mode
- //if direct, each image becomes a new post
- //if selection, each group becomes its own post,
- // and ungrouped items each become their own post
- if (!isset($_FILES['files'])) {
- return new WP_REST_Response([
- 'success' => false,
- 'message' => 'No files uploaded...',
- ]);
- }
+ protected function applySearchFilters(array $args, array $params): array
+ {
+ $search_term = sanitize_text_field($params['search']);
- $data = $request->get_params();
+ // Search term is already in $args['s'] from earlier
+
+ // Get all taxonomies registered to this post type
+ $taxonomies = get_object_taxonomies($this->post_type, 'names');
+
+ if (empty($taxonomies)) {
+ return $args;
+ }
+
+ // Cache the taxonomy term lookup per search term + post type
+ $term_cache_key = 'search_terms_' . md5($search_term . $this->post_type);
+ $matching_term_ids = $this->cache->get($term_cache_key);
+
+ if ($matching_term_ids === false) {
+ $matching_term_ids = [];
+
+ foreach ($taxonomies as $taxonomy) {
+ $terms = get_terms([
+ 'taxonomy' => $taxonomy,
+ 'search' => $search_term,
+ 'hide_empty' => false,
+ 'fields' => 'ids'
+ ]);
+
+ if (!is_wp_error($terms) && !empty($terms)) {
+ $matching_term_ids = array_merge($matching_term_ids, $terms);
+ }
+ }
+
+ // Cache term IDs for 1 hour
+ $this->cache->set($term_cache_key, $matching_term_ids, 3600);
+ }
+
+ if (empty($matching_term_ids)) {
+ return $args;
+ }
+
+ // Build tax_query for matching terms
+ $term_queries = [];
+
+ foreach ($taxonomies as $taxonomy) {
+ $taxonomy_term_ids = array_filter($matching_term_ids, function ($term_id) use ($taxonomy) {
+ $term = get_term($term_id);
+ return !is_wp_error($term) && $term->taxonomy === $taxonomy;
+ });
+
+ if (!empty($taxonomy_term_ids)) {
+ $term_queries[] = [
+ 'taxonomy' => $taxonomy,
+ 'field' => 'term_id',
+ 'terms' => array_values($taxonomy_term_ids),
+ 'operator' => 'IN'
+ ];
+ }
+ }
+
+ if (!empty($term_queries)) {
+ if (isset($args['tax_query'])) {
+ $args['tax_query'] = [
+ 'relation' => 'OR',
+ $args['tax_query'],
+ [
+ 'relation' => 'OR',
+ ...$term_queries
+ ]
+ ];
+ } else {
+ $args['tax_query'] = [
+ 'relation' => 'OR',
+ ...$term_queries
+ ];
+ }
+ }
+
+ return $args;
+ }
+
+ /**
+ * Gets allowed taxonomies for a particular content
+ * @param string $content
+ *
+ * @return array
+ */
+ protected function getTaxonomies(string $content): array
+ {
+ $registrar = Registrar::getInstance($content);
+ if (!$registrar || $registrar->getType()!== 'post') {
+ return [];
+ }
+ $out = [];
+ foreach ($registrar->registrar->taxonomies as $tax) {
+ $taxReg = Registrar::getInstance($tax);
+ $out[jvbCheckBase($tax)] = [
+ 'label' => $taxReg->getPlural(),
+ 'icon' => $taxReg->getIcon()??jvbDefaultIcon()
+ ];
+ }
+
+ return $out;
+ }
- $user_id = $data['user'];
- if (!$this->userCheck($user_id)) {
- return new WP_REST_Response([
- 'success' => 'false',
- 'message' => 'Invalid user match... are you a bot?'
- ]);
- }
- $operation_id = $data['id'];
- $response = new WP_REST_Response([
- 'success' => true,
- 'message' => 'Successfully sent to server. Added to queue.',
- 'operation_id' => $operation_id,
- 'status' => 'pending'
- ]);
- $this->queue = JVB()->queue();
- JVB()->routes('uploads')->handleUploadRequest($request, false);
- $this->queue->queueOperation(
- 'batch_creation',
- $user_id,
- [
- 'content' => $request->get_param('content'),
- 'mode' => $request->get_param('mode') ?: 'direct',
- 'files_data'=> $request->get_param('files_data')
- ],
- [
- 'operation_id' => $operation_id,
- 'priority' => 'high',
- 'notification' => true,
- 'depends_on' => $operation_id.'_upload'
- ]
- );
- return $response;
- }
+ /**
+ * Generates a post title, based on content type
+ * @param string $content the post type
+ *
+ * @return string
+ */
+ protected function generatePostTitle(string $content): string
+ {
+ $username = get_user_meta($this->user_id, 'first_name', true);
+ $link = get_user_meta($this->user_id, BASE . 'link', true);
+ $city = jvbArtistCity($link);
+ return ucfirst($content) . ' by ' . $city . ' artist ' . $username;
+ }
- /**
- * Generates a post title, based on content type
- * @param string $content the post type
- *
- * @return string
- */
- protected function generatePostTitle(string $content):string
- {
- $username = get_user_meta($this->user_id, 'first_name', true);
- $link = get_user_meta($this->user_id, BASE.'link', true);
- $city = jvbArtistCity($link);
- return ucfirst($content).' by '.$city.' artist '.$username;
- }
-
- /**
- * @param WP_Post $post the wordpress post object
- *
- * @return array
- */
- protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array
- {
- if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) {
+ /**
+ * @param WP_Post $post the post object
+ *
+ * @return array
+ */
+ protected function preparePost(WP_Post $post, bool $skip = false, bool $fields = true): array
+ {
+ $registrar = Registrar::getInstance($post->post_type);
+ if (!$skip && $registrar && $registrar->hasFeature('is_timeline')) {
$this->initTimelineFields($post->post_type);
return $this->formatTimeline($post);
}
- $this->meta = new MetaManager($post->ID, 'post');
- $data = [
- 'id' => $post->ID,
- '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 = Meta::forPost($post->ID);
+ $fields = ($fields) ? $this->meta->getAll() : [];
+ $data = [
+ 'id' => $post->ID,
+ 'title' => $post->post_title,
+ 'status' => $post->post_status,
+ 'date' => $post->post_date,
+ 'modified' => $post->post_modified,
+ 'thumbnail' => get_the_post_thumbnail_url($post->ID),
+ 'alt' => get_post_meta(get_post_thumbnail_id(), '_wp_attachment_image_alt', true),
+ 'icon' => $registrar->getIcon(),
+ 'taxonomies' => [],
+ 'fields' => $fields,
+ 'images' => [],
+ ];
- // Add taxonomy terms
- foreach ($this->taxonomies as $taxonomy => $options) {
- $tax = str_replace(BASE, '', $taxonomy);
- $terms = wp_get_object_terms(
- $post->ID,
- $taxonomy,
- ['fields' => 'id=>name']
- );
- $data['taxonomies'][$tax] = [
- 'terms' => (is_wp_error($terms))? [] : $terms,
- 'name' => $options['label'],
- 'icon' => $tax
- ];
- }
-
- $images = $this->extractImages();
-
-
- if (!empty($images)) {
- $data['images'] = $images;
- }
-
- return $data;
- }
- protected function extractImages(array $fields = []):array
- {
- //Extract images
- $images = [];
- $get = [];
- $fields = (empty($fields)) ? $this->fields : $fields;
- foreach ($fields as $field => $config) {
- if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') {
- $get[] = $field;
- }
+ $images = $this->extractImages($fields, $this->meta);
+ if (!empty($images)) {
+ $data['images'] = $images;
}
- if (!empty($get)) {
- $allImages = $this->meta->getAll($get);
- foreach($allImages as $k => $imgs){
- $temp = explode(',', $imgs);
- foreach($temp as $img) {
- if (is_numeric($img) && !array_key_exists($img, $images)) {
- $images[$img] = jvbImageData((int) $img);
- }
- }
- }
+
+ $taxonomies = $this->extractTerms($fields, $this->meta);
+ if (!empty($taxonomies)) {
+ $data['taxonomies'] = $taxonomies;
}
- return $images;
+ return $data;
}
- protected function formatTimeline(WP_Post $post):array
+ /**
+ * @param WP_Term $post the post object
+ *
+ * @return array
+ */
+ protected function prepareTerm(WP_Term $post, bool $fields = true): array
{
- $item = $this->prepareItem($post, true, false);
+ $registrar = Registrar::getInstance($post->taxonomy);
+
+ $this->meta = Meta::forTerm($post->term_id);
+ $fields = ($fields) ? $this->meta->getAll() : [];
+ $data = [
+ 'id' => $post->term_id,
+ 'title' => $post->name,
+ 'date' => $fields['created_date']??'',
+ 'modified' => $fields['modified_date']??'',
+ 'thumbnail' => '',
+ 'icon' => $registrar->getIcon(),
+ 'taxonomies' => [],
+ 'fields' => $fields,
+ 'images' => [],
+ ];
+
+ $images = $this->extractImages($fields, $this->meta);
+ if (!empty($images)) {
+ $data['images'] = $images;
+ }
+
+ $taxonomies = $this->extractTerms($fields, $this->meta);
+ if (!empty($taxonomies)) {
+ $data['taxonomies'] = $taxonomies;
+ }
+
+ error_log('Term data: '.print_r($data, true));
+ return $data;
+ }
+
+
+ public function formatTimeline(WP_Post $post): array
+ {
+ $item = $this->preparePost($post, true, false);
//Step 1: Get the fields that apply to all posts
- $mainMeta = new MetaManager($post->ID, 'post');
+ $mainMeta = Meta::forPost($post->ID);
$item['fields'] = $mainMeta->getAll($this->timelineSharedFields);
//Step 2: Get the fields for each individual posts
- $children = get_children(['post_parent' => $post->ID, 'orderby' => '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 = [];
$images = [];
foreach ($children as $child) {
- $meta = new MetaManager($child, 'post');
+ $meta = Meta::forPost($child);
$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['fields']['timeline_gallery'] = $subFields;
$item['images'] = $item['images'] + $images;
+ $item['number'] = $mainMeta->get('number');
return $item;
}
-
- /**
- * Builds the taxonomy query
- * @param array $taxonomies
- *
- * @return array|string[]
- */
- protected function buildTaxQuery(array $taxonomies):array
- {
- $tax_query = [];
- error_log('Taxonomies in query: '.print_r($taxonomies, true));
-
- foreach ($taxonomies as $taxonomy => $terms) {
- if (!empty($terms)) {
- $tax_query[] = [
- 'taxonomy' => jvbCheckBase($taxonomy),
- 'field' => 'term_id',
- 'terms' => array_map('absint', (array)$terms)
- ];
- }
- }
-
-
- return count($tax_query) > 1
- ? array_merge(['relation' => 'AND'], $tax_query)
- : $tax_query;
- }
-
- /**
- * Builds the date query
- * @param array $date_params
- *
- * @return array
- */
- protected function buildDateQuery(array $date_params):array
- {
- $query = [];
-
- if (!empty($date_params['after'])) {
- $query['after'] = sanitize_text_field($date_params['after']);
- }
-
- if (!empty($date_params['before'])) {
- $query['before'] = sanitize_text_field($date_params['before']);
- }
-
- if (isset($date_params['inclusive'])) {
- $query['inclusive'] = (bool)$date_params['inclusive'];
- }
-
- return empty($query) ? [] : [$query];
- }
-
- /**
- * @param int $post_id
- *
- * @return bool
- */
- protected function verifyOwnership(int $post_id):bool
- {
- $post = get_post($post_id);
- return $post && $post->post_author == $this->user_id;
- }
-
- /**
- * Processes operation from Operation Queue
- * @param WP_Error|array $result
- * @param object $operation
- * @param array $data
- *
- * @return array|WP_Error
- */
- public function processOperation(WP_Error|array $result, object $operation, array $data):array|WP_Error
- {
- if ($operation->type === 'batch_creation') {
- $JVB = JVB();
- $queue = $JVB->queue();
-
- $images = $queue->getOperationValue($operation->id.'_upload', 'result')??false;
-
- $this->user_id = $operation->user_id;
- $this->post_type = BASE.$data['content'];
- try {
- $results = [];
- if ($images) {
- if ($data['mode'] == 'selection') {
- $total = count($images);
- foreach ($images as $group => $files) {
- $settings = json_decode($data['files_data'][$group]);
-
- switch ($settings->type) {
- case 'group':
- $featuredIndex = $settings->metadata->featuredFile??0;
- $title = $settings->metadata->title??$this->generatePostTitle($data['content']);
- $new = wp_insert_post([
- 'post_type' => BASE.$data['content'],
- 'post_title' => $title,
- 'post_status' => 'draft',
- 'post_author' => $operation->user_id
- ]);
- if ($new && !is_wp_error($new)) {
- set_post_thumbnail($new, $files[$featuredIndex]['attachment_id']);
- unset($files[$featuredIndex]);
- if (!empty($files)) {
- $meta = new MetaManager($new, 'post');
- $IDs = array_column($files, 'attachment_id');
- $meta->updateValue('gallery', implode(',', $IDs));
- }
- $results[] = $new;
-// $queue->updateOperationProgress($operation->id, $group + 1, $total);
- }
- break;
- default:
- foreach ($files as $img) {
- $new = wp_insert_post([
- 'post_type' => BASE. $data['content'],
- 'post_title' => $this->generatePostTitle($data['content']),
- 'post_status' => 'draft',
- 'post_author' => $operation->user_id
- ]);
-
- if ($new && !is_wp_error($new)) {
- set_post_thumbnail($new, $img['attachment_id']);
- $results[] = $new;
-// $queue->updateOperationProgress($operation->id, $group + 1, $total);
- }
- }
- break;
- }
- }
- } else {
- $total = count($images);
- foreach ($images as $key => $img) {
- $new = wp_insert_post([
- 'post_type' => BASE.$data['content'],
- 'post_title' => $this->generatePostTitle($data['content']),
- 'post_status' => 'draft',
- 'post_author' => $operation->user_id
- ]);
- if ($new && !is_wp_error($new)) {
- set_post_thumbnail($new, $img['attachment_id']);
- }
- $results[] = $new;
-// $queue->updateOperationProgress($operation->id, $key + 1, $total);
- }
- }
-
- //Clear cache
- CacheManager::for($data['content'])->clear();
- CacheManager::for('feed')->clear();
- }
-
- return [
- 'success' => true,
- 'result' => $results
- ];
- } catch (Exception $e) {
- $JVB->error()->log(
- '[ContentRoutes]:processOperation',
- $e->getMessage()
- );
- }
-
- return $results;
- } elseif ($operation->type == 'content_update') {
- $result = $this->processBatches($operation, $data);
- }
-
- return $result;
- }
}
--
Gitblit v1.10.0