| | |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\managers\CacheManager; |
| | | use JVBase\meta\MetaManager; |
| | | use JVBase\utility\Features; |
| | | use WP_Post; |
| | | use WP_Query; |
| | | use WP_Error; |
| | | use WP_REST_Request; |
| | |
| | | protected string $post_type = ''; |
| | | protected string $user_id = ''; |
| | | |
| | | //For Timeline-specific posts |
| | | protected array $timelineSharedFields = []; |
| | | protected array $timelineUniqueFields = []; |
| | | |
| | | //TODO: Ensure we are handling the bulk operations for all processes |
| | | //TODO: be sure to clear cache ($this->>cache->invalidateGroup($this->>cache_name)) on content update/create |
| | | //TODO: Also invalidate feed caches on updates!! |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->cache_name = 'user_content_'.get_current_user_id(); |
| | | parent::__construct(); |
| | | |
| | | $this->action = 'dash-'; |
| | | $this->operation_type = 'content_update'; |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | |
| | | ]); |
| | | } |
| | | |
| | | protected function initTimelineFields(string $content):void |
| | | { |
| | | $content = jvbNoBase($content); |
| | | if (!Features::forContent($content)->has('is_timeline')){ |
| | | return; |
| | | } |
| | | $config = Features::getConfig($content); |
| | | $this->fields = $config['fields']; |
| | | |
| | | $this->timelineSharedFields = array_keys(array_filter($this->fields, function ($field) { |
| | | if (!array_key_exists('for_all', $field) || $field['for_all'] === false){ |
| | | return true; |
| | | } |
| | | return false; |
| | | })); |
| | | array_unshift($this->timelineSharedFields, 'post_thumbnail'); |
| | | array_unshift($this->timelineSharedFields, 'post_title'); |
| | | |
| | | $this->timelineUniqueFields = array_keys(array_filter($this->fields, function ($field) { |
| | | if (array_key_exists('for_all', $field) && $field['for_all'] === true) { |
| | | return true; |
| | | } |
| | | return false; |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * Handle content update/creation |
| | | * @param WP_REST_Request $request |
| | |
| | | } |
| | | $post_type = str_replace('-', '_',jvbCheckBase($params['content'])); |
| | | |
| | | $config = (array_key_exists($params['content'], JVB_CONTENT) && !empty(JVB_CONTENT[$params['content']])) ? JVB_CONTENT[$params['content']] : []; |
| | | |
| | | |
| | | |
| | | // Build query args |
| | | $args = [ |
| | |
| | | 'author' => $user_id, |
| | | 'post_status' => $post_status |
| | | ]; |
| | | //Only top level posts for timeline types |
| | | if (Features::forContent($post_type)->has('is_timeline')) { |
| | | $args['post_parent'] = 0; |
| | | } |
| | | |
| | | //Calendar filters |
| | | if (jvbCheck('is_calendar', $config)) { |
| | | if (Features::forContent($post_type)->has('is_calendar')) { |
| | | $args = $this->applyCalendarFilters($args, $params); |
| | | } |
| | | if (array_key_exists('taxonomies', $params)) { |
| | |
| | | $args['s'] = sanitize_text_field($params['search']); |
| | | } |
| | | |
| | | |
| | | |
| | | error_log('Content Routes final args: '.print_r($args, true)); |
| | | |
| | | $key = $this->cache->generateKey($args); |
| | | $lastModified = $this->cache->getTimestamp($key); |
| | | if ($lastModified !== false) { |
| | | $headerCheck = $this->ifModifiedSince($lastModified, $args, $request); |
| | | if (!is_null($headerCheck)) { |
| | | return $headerCheck; |
| | | } |
| | | } else { |
| | | // No timestamp yet, but we can still set ETag |
| | | $etag = '"' . md5(serialize($args)) . '"'; |
| | | header('ETag: ' . $etag); |
| | | header('Cache-Control: private, max-age=30'); |
| | | // Check HTTP cache headers with the specific content type |
| | | $content_type = $params['content'] ?? $params['type']; |
| | | $cache_check = $this->checkHeaders($request, $content_type, [ |
| | | 'filter_hash' => $key, |
| | | ]); |
| | | if ($cache_check) { |
| | | return $cache_check; |
| | | } |
| | | |
| | | |
| | | $cache = $this->cache->get($key); |
| | | $cache = false; |
| | | if ($cache) { |
| | | return new WP_REST_Response($cache); |
| | | $response = new WP_REST_Response($cache); |
| | | return $this->addCacheHeaders($response); |
| | | } |
| | | |
| | | // Run query |
| | |
| | | |
| | | $this->cache->set($key, $data); |
| | | |
| | | return new WP_REST_Response($data); |
| | | $response = new WP_REST_Response($data); |
| | | 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); |
| | | continue; |
| | | } |
| | | if (str_starts_with($ID, 'new')) { |
| | | |
| | | error_log('New post detected. Creating... with: '.print_r([ |
| | |
| | | |
| | | } |
| | | |
| | | 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( |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Extracts the postdata for timeline post child posts from the pseudo-repeater element |
| | | * @param int $parent_id |
| | | * @param array $post_data |
| | | * @return array|true[] |
| | | */ |
| | | protected function processTimelinePost(int $parent_id, array $post_data):array |
| | | { |
| | | if (!$this->verifyOwnership($parent_id)) { |
| | | return ['success' => false, 'message' => 'No permission']; |
| | | } |
| | | |
| | | $this->fields = jvbGetFields($post_data['content']); |
| | | $this->initTimelineFields($post_data['content']); |
| | | error_log('Received Data: '.print_r($post_data, true)); |
| | | |
| | | // First, process the main fields that will apply to all posts |
| | | $sharedData = array_filter($post_data, function ($key) { |
| | | return in_array($key, $this->timelineSharedFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | //Next, process any individual posts, including any menu order changes |
| | | if (array_key_exists('timeline', $post_data) && is_array($post_data['timeline'])) { |
| | | $sharedTaxonomies = $sharedData; |
| | | unset($sharedTaxonomies['post_title']); |
| | | |
| | | //Ensure the parent post exists and is still first in the array |
| | | $index = array_search((string)$parent_id, array_column($post_data['timeline'], 'id')); |
| | | error_log('Found index: '.print_r($index, true)); |
| | | if ($index === false) { |
| | | return [ |
| | | 'success' => false, |
| | | 'message' => 'Missing parent id. This should not have happened' |
| | | ]; |
| | | } elseif ($index !== 0) { |
| | | // Move that element to the start of the array |
| | | $item = $post_data['timeline'][$index]; |
| | | unset($post_data['timeline'][$index]); |
| | | array_unshift($post_data['timeline'], $item); |
| | | } |
| | | $errors = []; |
| | | $success = []; |
| | | // Get existing children to track deletions |
| | | $existing_children = get_children([ |
| | | 'post_parent' => $parent_id, |
| | | 'post_type' => jvbCheckBase($post_data['content']), |
| | | 'fields' => 'ids' |
| | | ]); |
| | | //Iterate through the timeline posts |
| | | foreach($post_data['timeline'] as $order => $timeline) { |
| | | $allowedFields = array_filter($timeline, function($key) { |
| | | return in_array($key, $this->timelineUniqueFields); |
| | | }, ARRAY_FILTER_USE_KEY); |
| | | |
| | | $allowedFields['post_title'] = $allowedFields['post_title'] ?? $sharedData['post_title'].' - Treatment #'.$order; |
| | | $allowedFields = array_merge($allowedFields, $sharedTaxonomies); |
| | | if (!array_key_exists('id', $timeline) || !is_numeric($timeline['id'])) { |
| | | $newChild = wp_insert_post([ |
| | | 'post_author' => $this->user_id, |
| | | 'post_type' => jvbCheckBase($post_data['content']), |
| | | 'post_title' => $allowedFields['post_title'], |
| | | 'post_parent' => $parent_id, |
| | | 'menu_order' => $order |
| | | ]); |
| | | if (!$newChild || is_wp_error($newChild)) { |
| | | $errors[] = [ |
| | | 'message' => 'Could not create child post', |
| | | 'data' => $timeline |
| | | ]; |
| | | continue; |
| | | } |
| | | $timeline['id'] = $newChild; |
| | | } |
| | | |
| | | if (in_array((int)$timeline['id'], $existing_children)) { |
| | | unset($existing_children[array_search((int)$timeline['id'], $existing_children)]); |
| | | } |
| | | |
| | | //Determine which fields to update |
| | | $meta = new MetaManager($timeline['id'], 'post'); |
| | | $oldValues = $meta->getAll(array_keys($allowedFields)); |
| | | $updateValues = array_filter($allowedFields, function($value, $key) use ($oldValues) { |
| | | return (!array_key_exists($key, $oldValues) || $value !== $oldValues[$key]); |
| | | }, ARRAY_FILTER_USE_BOTH); |
| | | $meta->setAll($updateValues); |
| | | //Update Menu Order, if applicable |
| | | if ((int) $timeline['id'] !== $parent_id) { |
| | | $post = get_post((int) $timeline['id']); |
| | | if ($post && $post->menu_order !== $order) { |
| | | $updated = wp_update_post([ |
| | | 'ID' => $post->ID, |
| | | 'menu_order' => $order, |
| | | ]); |
| | | if (!$updated || is_wp_error($updated)) { |
| | | $errors[] = [ |
| | | 'message' => 'Could not update timeline order for post', |
| | | 'data' => $timeline |
| | | ]; |
| | | } |
| | | } |
| | | } |
| | | $success[] = $timeline['id']; |
| | | } |
| | | } |
| | | //Delete any remaining children that no longer exist |
| | | if (!empty($existing_children)) { |
| | | foreach ($existing_children as $ID) { |
| | | wp_delete_post($ID); |
| | | } |
| | | } |
| | | |
| | | return ['success' => true, 'data' => [ |
| | | 'success' => $success, |
| | | 'errors' => $errors |
| | | ]]; |
| | | } |
| | | |
| | | /** |
| | | * Handle batch content creation from uploads |
| | | * @param WP_REST_Request $request |
| | |
| | | } |
| | | |
| | | /** |
| | | * @param object $post the wordpress post object |
| | | * @param WP_Post $post the wordpress post object |
| | | * |
| | | * @return array |
| | | */ |
| | | protected function prepareItem(object $post):array |
| | | protected function prepareItem(WP_Post $post, bool $skip = false, bool $fields = true):array |
| | | { |
| | | if (!$skip && Features::forContent($post->post_type)->has('is_timeline')) { |
| | | $this->initTimelineFields($post->post_type); |
| | | return $this->formatTimeline($post); |
| | | } |
| | | $this->meta = new MetaManager($post->ID, 'post'); |
| | | $data = [ |
| | | 'id' => $post->ID, |
| | |
| | | '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' => [], |
| | | ]; |
| | | |
| | |
| | | ]; |
| | | } |
| | | |
| | | $images = $this->extractImages(); |
| | | |
| | | //Extract images |
| | | |
| | | if (!empty($images)) { |
| | | $data['images'] = $images; |
| | | } |
| | | |
| | | return $data; |
| | | } |
| | | protected function extractImages(array $fields = []):array |
| | | { |
| | | //Extract images |
| | | $images = []; |
| | | $get = []; |
| | | foreach ($this->fields as $field => $config) { |
| | | if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') { |
| | | $fields = (empty($fields)) ? $this->fields : $fields; |
| | | foreach ($fields as $field => $config) { |
| | | if ($config['type'] === 'gallery' || $config['type'] === 'image' || $field === 'post_thumbnail') { |
| | | $get[] = $field; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (!empty($get)) { |
| | | $allImages = $this->meta->getAll($get); |
| | |
| | | } |
| | | } |
| | | } |
| | | return $images; |
| | | } |
| | | |
| | | if (!empty($images)) { |
| | | $data['images'] = $images; |
| | | } |
| | | protected function formatTimeline(WP_Post $post):array |
| | | { |
| | | $item = $this->prepareItem($post, true, false); |
| | | //Step 1: Get the fields that apply to all posts |
| | | $mainMeta = new MetaManager($post->ID, 'post'); |
| | | $item['fields'] = $mainMeta->getAll($this->timelineSharedFields); |
| | | |
| | | return $data; |
| | | } |
| | | //Step 2: Get the fields for each individual posts |
| | | $children = get_children(['post_parent' => $post->ID, 'orderby' => 'menu_order', 'post_status' => ['publish', 'draft'], 'fields'=> 'ids']); |
| | | array_unshift($children, $post->ID); |
| | | |
| | | $subFields = []; |
| | | $images = []; |
| | | foreach ($children as $child) { |
| | | $meta = new MetaManager($child, 'post'); |
| | | $f = $meta->getAll($this->timelineUniqueFields); |
| | | $f = ['id' => $child] + $f; |
| | | $subFields[$f['post_thumbnail']] = $f; |
| | | |
| | | $images[$f['post_thumbnail']] = jvbImageData((int) $f['post_thumbnail']); |
| | | } |
| | | $item['fields']['timeline'] = $subFields; |
| | | $item['images'] = $item['images'] + $images; |
| | | |
| | | return $item; |
| | | } |
| | | |
| | | /** |
| | | * Builds the taxonomy query |
| | |
| | | } |
| | | |
| | | //Clear cache |
| | | CacheManager::invalidateGroup($data['content']); |
| | | CacheManager::invalidateGroup('feed'); |
| | | CacheManager::invalidateGroup('user_content'); |
| | | CacheManager::for($data['content'])->clear(); |
| | | CacheManager::for('feed')->clear(); |
| | | } |
| | | |
| | | return [ |