<?php
|
|
namespace JVBase\rest\routes;
|
|
use JVBase\JVB;
|
use JVBase\managers\queue\executors\ContentExecutor;
|
use JVBase\managers\queue\TypeConfig;
|
use JVBase\rest\RestRouteManager;
|
use JVBase\managers\Cache;
|
use JVBase\meta\MetaManager;
|
use JVBase\utility\Features;
|
use WP_Post;
|
use WP_Query;
|
use WP_Error;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
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 = '';
|
|
//For Timeline-specific posts
|
protected array $timelineSharedFields = [];
|
protected array $timelineUniqueFields = [];
|
|
public function __construct()
|
{
|
$this->cache_name = 'user_content_' . get_current_user_id();
|
parent::__construct();
|
if (JVB_TESTING) {
|
$this->cache->flush();
|
}
|
|
$this->action = 'dash-';
|
$this->operation_type = 'content_update';
|
add_action('init', [$this, 'registerContentExecutors'], 5);
|
}
|
|
/**
|
* Register content operation types with the queue's TypeRegistry
|
*/
|
public function registerContentExecutors(): void
|
{
|
$registry = JVB()->queue()->registry();
|
$executor = new ContentExecutor();
|
|
// Content updates - chunked at 10 posts
|
$registry->register('content_update', new TypeConfig(
|
executor: $executor,
|
chunkKey: 'posts',
|
chunkSize: 10
|
));
|
|
// Batch creation (from uploads)
|
$registry->register('batch_creation', new TypeConfig(
|
executor: $executor
|
));
|
}
|
|
/**
|
* 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']
|
]
|
]);
|
}
|
|
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 = $this->getTimelineSharedFields($content);
|
array_unshift($this->timelineSharedFields, 'post_thumbnail');
|
array_unshift($this->timelineSharedFields, 'post_title');
|
array_unshift($this->timelineSharedFields, 'post_status');
|
|
$this->timelineUniqueFields = $this->getTimelineUniqueFields($content);
|
}
|
|
public function getTimelineUniqueFields(string $content): array
|
{
|
$content = jvbNoBase($content);
|
if (!Features::forContent($content)->has('is_timeline')) {
|
return [];
|
}
|
$config = Features::getConfig($content);
|
$allFields = $config['fields'];
|
|
return array_keys(array_filter($allFields, function ($field) {
|
if (array_key_exists('for_all', $field) && $field['for_all'] === true) {
|
return true;
|
}
|
return false;
|
}));
|
}
|
|
public function getTimelineSharedFields(string $content): array
|
{
|
$content = jvbNoBase($content);
|
if (!Features::forContent($content)->has('is_timeline')) {
|
return [];
|
}
|
$config = Features::getConfig($content);
|
if (!$config || empty($config)) {
|
return [];
|
}
|
$allFields = $config['fields'] ?? [];
|
|
return array_keys(array_filter($allFields, function ($field) {
|
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();
|
$user_id = $data['user'];
|
|
if (!$this->userCheck($user_id)) {
|
return new WP_REST_Response([
|
'success' => true,
|
'message' => 'You for real?'
|
]);
|
}
|
|
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']);
|
|
$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
|
]);
|
}
|
|
|
/**
|
* 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']));
|
|
|
// 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')) {
|
$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-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
|
$cache_check = $this->checkHeaders($request, $key);
|
if ($cache_check) {
|
return $cache_check;
|
}
|
|
|
$cache = $this->cache->get($key);
|
if ($cache) {
|
$response = new WP_REST_Response($cache);
|
return $this->addCacheHeaders($response);
|
}
|
|
$this->post_type = jvbCheckBase($params['content'] ?? $params['type']);
|
// Only expand search to taxonomies if we're actually going to query
|
if (array_key_exists('s', $args)) {
|
$args = $this->applySearchFilters($args, $params);
|
}
|
|
// Run query
|
$query = new WP_Query($args);
|
|
|
$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);
|
}
|
|
protected function applySearchFilters(array $args, array $params): array
|
{
|
$search_term = sanitize_text_field($params['search']);
|
|
// Search term is already in $args['s'] from earlier
|
|
// Get all taxonomies registered to this post type
|
$taxonomies = get_object_taxonomies($this->post_type, 'names');
|
|
if (empty($taxonomies)) {
|
return $args;
|
}
|
|
// Cache the taxonomy term lookup per search term + post type
|
$term_cache_key = 'search_terms_' . md5($search_term . $this->post_type);
|
$matching_term_ids = $this->cache->get($term_cache_key);
|
|
if ($matching_term_ids === false) {
|
$matching_term_ids = [];
|
|
foreach ($taxonomies as $taxonomy) {
|
$terms = get_terms([
|
'taxonomy' => $taxonomy,
|
'search' => $search_term,
|
'hide_empty' => false,
|
'fields' => 'ids'
|
]);
|
|
if (!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)) {
|
// 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([
|
'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
|
];
|
|
}
|
}
|
|
if (jvbSiteHasNotifications()) {
|
$this->notifications = JVB()->notification();
|
$this->notifications->addNotification(
|
$this->user_id,
|
'content_update_complete',
|
null,
|
'Content updates completed!'
|
);
|
}
|
|
|
return [
|
'success' => true,
|
'result' => $results
|
];
|
}
|
|
/**
|
* 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'];
|
}
|
|
error_log('[Processing Timeline Post...');
|
|
$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'
|
]);
|
|
$prevDate = null;
|
$latest_date = null;
|
$earliest_date = 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);
|
|
|
$meta->setAll($updateValues);
|
$timeline['id'] = (int)$timeline['id'];
|
|
$success[] = $timeline['id'];
|
}
|
}
|
|
// Delete any remaining children that no longer exist
|
if (!empty($existing_children)) {
|
foreach ($existing_children as $ID) {
|
wp_delete_post($ID);
|
}
|
}
|
|
if ($clearParent) {
|
$this->cache->flush();
|
Cache::onPostChange($parent_id, $parent_post);
|
}
|
|
|
return ['success' => true, 'data' => [
|
'success' => $success,
|
'errors' => $errors
|
]];
|
}
|
|
/**
|
* 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();
|
|
|
$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;
|
}
|
|
/**
|
* @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' => [],
|
];
|
|
// 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;
|
}
|
}
|
|
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);
|
}
|
}
|
}
|
}
|
return $images;
|
}
|
|
public 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);
|
|
//Step 2: Get the fields for each individual posts
|
$children = get_children(['post_parent' => $post->ID, 'orderby' => 'date', 'order' => 'ASC', 'post_status' => ['publish', 'draft'], 'fields' => 'ids']);
|
array_unshift($children, $post->ID);
|
|
$subFields = [];
|
$images = [];
|
foreach ($children as $child) {
|
$meta = new MetaManager($child, 'post');
|
$f = $meta->getAll($this->timelineUniqueFields);
|
$f = ['id' => $child] + $f;
|
$subFields[] = $f;
|
|
$images[$f['post_thumbnail']] = jvbImageData((int)$f['post_thumbnail']);
|
}
|
$item['fields']['timeline'] = $subFields;
|
$item['images'] = $item['images'] + $images;
|
$item['number'] = $mainMeta->getValue('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);
|
}
|
}
|
}
|
|
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;
|
}
|
// Add to ContentRoutes.php
|
|
/**
|
* One-time migration: Set latest_date meta for all timeline posts
|
* Call this once via WP-CLI or a temporary admin page
|
*
|
* Usage: add_action('admin_init', function() {
|
* if (current_user_can('manage_options')) {
|
* JVB()->routes('content')->migrateTimelineLatestDates();
|
* }
|
* });
|
*/
|
public function migrateTimelineLatestDates(): array
|
{
|
global $wpdb;
|
|
$results = [
|
'processed' => 0,
|
'updated' => 0,
|
'skipped' => 0,
|
'errors' => []
|
];
|
|
// Get all timeline post types
|
$timeline_types = [];
|
foreach (JVB_CONTENT as $type => $config) {
|
if (Features::forContent($type)->has('is_timeline')) {
|
$timeline_types[] = BASE . $type;
|
}
|
}
|
|
if (empty($timeline_types)) {
|
return $results;
|
}
|
|
// Get all parent timeline posts
|
$args = [
|
'post_type' => $timeline_types,
|
'post_status' => ['publish', 'draft'],
|
'post_parent' => 0,
|
'posts_per_page' => -1,
|
'fields' => 'ids'
|
];
|
|
$parent_ids = get_posts($args);
|
|
foreach ($parent_ids as $parent_id) {
|
$results['processed']++;
|
|
try {
|
// Get all children including the parent
|
$children = get_children([
|
'post_parent' => $parent_id,
|
'post_status' => ['publish', 'draft'],
|
'orderby' => 'menu_order',
|
'order' => 'ASC',
|
'fields' => 'ids'
|
]);
|
|
// Add parent to the list
|
array_unshift($children, $parent_id);
|
|
// Find latest date among all posts
|
$latest_timestamp = 0;
|
|
foreach ($children as $post_id) {
|
$date = get_post_meta($post_id, BASE . 'date', true);
|
|
if ($date) {
|
$timestamp = strtotime($date);
|
if ($timestamp > $latest_timestamp) {
|
$latest_timestamp = $timestamp;
|
}
|
}
|
}
|
|
// Update parent with latest date
|
if ($latest_timestamp > 0) {
|
update_post_meta($parent_id, BASE . 'latest_date', $latest_timestamp);
|
$results['updated']++;
|
error_log("Updated post {$parent_id} with latest_date: {$latest_timestamp}");
|
} else {
|
// Fallback to parent post's post_date
|
$parent_post = get_post($parent_id);
|
$fallback_timestamp = strtotime($parent_post->post_date);
|
|
if ($fallback_timestamp > 0) {
|
update_post_meta($parent_id, BASE . 'latest_date', $fallback_timestamp);
|
$results['updated']++;
|
error_log("Updated post {$parent_id} with fallback latest_date: {$fallback_timestamp} (from post_date)");
|
} else {
|
$results['skipped']++;
|
error_log("No dates found for post {$parent_id}");
|
}
|
}
|
|
} catch (Exception $e) {
|
$results['errors'][] = [
|
'post_id' => $parent_id,
|
'error' => $e->getMessage()
|
];
|
}
|
}
|
|
error_log('Timeline migration complete: ' . print_r($results, true));
|
return $results;
|
}
|
}
|
|
|
//add_action('init', function() {
|
//// delete_option('jvb_timeline_migrated');
|
// if (get_option('jvb_timeline_migrated')) {
|
// return;
|
// }
|
// JVB()->routes('content')->migrateTimelineLatestDates();
|
// update_option('jvb_timeline_migrated', true);
|
//});
|