| | |
| | | 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->cache->clear(); |
| | | $this->action = 'dash-'; |
| | | $this->operation_type = 'content_update'; |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | |
| | | ]); |
| | | } |
| | | |
| | | 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 |
| | |
| | | public function handleContentRequest(WP_REST_Request $request):WP_REST_Response |
| | | { |
| | | $params = $request->get_params(); |
| | | error_log('handleContentRequest params: '.print_r($params, true)); |
| | | |
| | | error_log('Fetching content. Params: '.print_r($params, true)); |
| | | $user_id = $params['user']; |
| | | if (!$this->userCheck($user_id)) { |
| | | return new WP_REST_Response([ |
| | |
| | | } |
| | | $post_type = str_replace('-', '_',jvbCheckBase($params['content'])); |
| | | |
| | | $config = Features::getConfig($params['content']); |
| | | |
| | | |
| | | |
| | | // Build query args |
| | | $args = [ |
| | |
| | | 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); |
| | | } |
| | |
| | | $args['s'] = sanitize_text_field($params['search']); |
| | | } |
| | | |
| | | |
| | | |
| | | error_log('Content Routes final args: '.print_r($args, true)); |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | // Check HTTP cache headers with the specific content type |
| | | $content_type = $params['content'] ?? $params['type']; |
| | |
| | | |
| | | |
| | | $cache = $this->cache->get($key); |
| | | $cache = false; |
| | | if ($cache) { |
| | | $response = new WP_REST_Response($cache); |
| | | return $this->addCacheHeaders($response); |
| | |
| | | $results = []; |
| | | |
| | | foreach ($posts as $ID => $post_data) { |
| | | if (Features::forContent($post_data['content'])->has('is_timeline')) { |
| | | $results[$ID] =$this->processTimelinePost($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('Allowed Fields: '.print_r($allowedFields, true)); |
| | | $meta = new MetaManager($ID, 'post'); |
| | | $success = $meta->setAll($allowedFields); |
| | | error_log('Should be set?'); |
| | | $results[$ID] = [ |
| | | 'success' => $success |
| | | ]; |
| | | |
| | | } |
| | | |
| | | 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( |
| | |
| | | return ['success' => false, 'message' => 'No permission']; |
| | | } |
| | | |
| | | $rows = $post_data['fields'] ?? []; |
| | | if (empty($rows)) { |
| | | return ['success' => false, 'message' => 'No data']; |
| | | } |
| | | $ignore = ['content', 'user']; |
| | | $this->fields = jvbGetFields($post_data['content']); |
| | | $this->initTimelineFields($post_data['content']); |
| | | |
| | | $fields = jvbGetFields($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'); |
| | | |
| | | // 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); |
| | | // 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); |
| | | |
| | | $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'; |
| | | // 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); |
| | | |
| | | $childMeta = new MetaManager($child_id, 'post'); |
| | | $childMeta->setAll($allowedChildFields); |
| | | // Ensure the parent post exists and is still first in the array |
| | | $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id')); |
| | | |
| | | $processed_ids[] = $child_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; |
| | | |
| | | 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']; |
| | | } |
| | | } |
| | | |
| | | // Delete removed children |
| | | $deleted_ids = array_diff($existing_children, $processed_ids); |
| | | foreach ($deleted_ids as $delete_id) { |
| | | wp_delete_post($delete_id, true); |
| | | // Delete any remaining children that no longer exist |
| | | if (!empty($existing_children)) { |
| | | foreach ($existing_children as $ID) { |
| | | wp_delete_post($ID); |
| | | } |
| | | } |
| | | |
| | | return ['success' => true, 'processed' => $processed_ids]; |
| | | if ($clearParent) { |
| | | $this->cache->clear(); |
| | | CacheManager::onPostSave($parent_id, $parent_post); |
| | | } |
| | | |
| | | |
| | | return ['success' => true, 'data' => [ |
| | | 'success' => $success, |
| | | 'errors' => $errors |
| | | ]]; |
| | | } |
| | | |
| | | /** |
| | |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function prepareItem(WP_Post $post, $skip = false):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, |
| | |
| | | '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' => [], |
| | | ]; |
| | | |
| | |
| | | |
| | | return $data; |
| | | } |
| | | protected function extractImages():array |
| | | protected function extractImages(array $fields = []):array |
| | | { |
| | | //Extract images |
| | | $images = []; |
| | | $get = []; |
| | | foreach ($this->fields as $field => $config) { |
| | | $fields = (empty($fields)) ? $this->fields : $fields; |
| | | foreach ($fields as $field => $config) { |
| | | if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') { |
| | | $get[] = $field; |
| | | } |
| | |
| | | return $images; |
| | | } |
| | | |
| | | protected function formatTimeline(WP_Post $post):array |
| | | public function formatTimeline(WP_Post $post):array |
| | | { |
| | | $data = $this->prepareItem($post, true); |
| | | $firstRow = $data['fields']; |
| | | $firstRow['id'] = $post->ID; |
| | | $firstRow['draggable'] = false; |
| | | $fields = [$firstRow]; |
| | | $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); |
| | | |
| | | $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order']); |
| | | $allImages = []; |
| | | //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) { |
| | | $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 |
| | | $meta = new MetaManager($child, 'post'); |
| | | $f = $meta->getAll($this->timelineUniqueFields); |
| | | $f = ['id' => $child] + $f; |
| | | $subFields[] = $f; |
| | | |
| | | $images = $this->extractImages(); |
| | | if (!empty($images)) { |
| | | $allImages = $allImages + $images; |
| | | } |
| | | $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']); |
| | | } |
| | | $item['fields']['timeline'] = $subFields; |
| | | $item['images'] = $item['images'] + $images; |
| | | |
| | | if (!empty($allImages)) { |
| | | if (!array_key_exists('images', $data)) { |
| | | $data['images'] = []; |
| | | } |
| | | $data['images'] = $data['images'] + $allImages; |
| | | } |
| | | $data['fields']['timeline'] = $fields; |
| | | return $data; |
| | | return $item; |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | //Clear cache |
| | | CacheManager::invalidateGroup($data['content']); |
| | | CacheManager::invalidateGroup('feed'); |
| | | CacheManager::invalidateGroup('user_content'); |
| | | CacheManager::for($data['content'])->clear(); |
| | | CacheManager::for('feed')->clear(); |
| | | } |
| | | |
| | | return [ |