userId = $operation->userId; if (!in_array($operation->type, self::HANDLED_TYPES)) { throw new Exception("ContentExecutor cannot handle type: {$operation->type}"); } error_log('Executing ContentExecutor.php'); try { $data = $operation->requestData; return match($operation->type) { 'content_update' => $this->processContentUpdate($operation, $data, $progress), default => throw new Exception("Unknown type: {$operation->type}") }; } catch (Exception $e) { JVB()->error()->log( '[ContentExecutor]:execute', $e->getMessage(), [ 'operation_id' => $operation->id, 'operation_type' => $operation->type, 'user_id' => $operation->userId, ] ); return new Result( outcome: 'failed', result: ['error' => $e->getMessage()] ); } } // ───────────────────────────────────────────────────────────── // Content Update // ───────────────────────────────────────────────────────────── private function processContentUpdate(Operation $operation, array $data, Progress $progress): Result { $posts = $data['posts'] ?? []; if (empty($posts)) { return new Result( outcome: 'failed', result: ['message' => 'No posts to update'] ); } $results = [ 'errors' => [], 'success' => [], 'newPosts' => [], 'timelineParents' => [], 'timelineStatus' => [], 'timelineSharedFields' => [], ]; $errors = []; foreach ($posts as $id => $postData) { try { $content = $postData['content'] ?? false; if (!$content) continue; $registrar = Registrar::getInstance($content); switch ($registrar->getType()) { case 'post': $results = $this->handlePost($id, $postData, $registrar, $results, $progress); break; case 'term': $results = $this->handleTerm($id, $postData, $registrar, $results, $progress); break; case 'user': $results = $this->handleUser($id, $postData, $registrar, $results, $progress); break; } } catch (Exception $e) { $progress->failItem($id, $e->getMessage()); $results['errors'][$id] = $e->getMessage(); } } error_log('Final Results: '.print_r($results, true)); try { if (!empty($results['timelineSharedFields'])) { $this->checkSharedFields($results['timelineSharedFields']); } if (!empty($results['timelineStatus'])) { $this->handleTimelineStatusChange($results['timelineStatus']); } if (!empty($results['timelineParents'])) { $this->maybeReorderTimelines($results['timelineParents']); } } catch (Exception $e) { $results['errors'][] = $e->getMessage(); } // Send notification // if (jvbSiteHasNotifications()) { // JVB()->notification()->addNotification( // $this->userId, // 'content_update_complete', // null, // 'Content updates completed!' // ); // } $outcome = 'success'; if (!empty($errors)) { $outcome = count($errors) === count($posts) ? 'failed' : 'partial'; } return new Result( outcome: $outcome, result: $results, ); } private function savePostFields(int $postId, array $postData): bool { $content = $postData['content'] ?? ''; $fields = Registrar::getFieldsFor($content); $allowedFields = array_filter($postData, function ($key) use ($fields) { return array_key_exists($key, $fields); }, ARRAY_FILTER_USE_KEY); //Remove values that are already saved $check = Meta::forPost($postId)->getAll(array_keys($allowedFields)); error_log('Stored values: '.print_r($check, true)); $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) { return $allowedFields[$key] !== $check[$key]; }, ARRAY_FILTER_USE_KEY); if (empty($allowedFields)) { return true; } return Meta::forPost($postId) ->setAll($allowedFields); } private function saveTermFields(int $termId, array $data): bool { $content = $data['content'] ?? ''; error_log('Saving term fields: '.print_r($data, true)); $fields = Registrar::getFieldsFor($content); $allowedFields = array_filter($data, function ($key) use ($fields) { return array_key_exists($key, $fields); }, ARRAY_FILTER_USE_KEY); //Remove values that are already saved $check = Meta::forTerm($termId)->getAll(array_keys($allowedFields)); error_log('Stored values: '.print_r($check, true)); $allowedFields = array_filter($allowedFields, function ($value, $key) use ($check) { error_log('Sent value: '.print_r($value, true)); error_log('Stored Value: '.print_r($check[$key], true)); return $value !== $check[$key]; }, ARRAY_FILTER_USE_BOTH); if (empty($allowedFields)) { return true; } error_log('Allowed fields: '.print_r($allowedFields, true)); return Meta::forTerm($termId) ->setAll($allowedFields); } private function saveUserFields(int $userId, array $data): bool { $content = $data['content'] ?? ''; $fields = Registrar::getFieldsFor($content); $allowedFields = array_filter($data, function ($key) use ($fields) { return array_key_exists($key, $fields); }, ARRAY_FILTER_USE_KEY); //Remove values that are already saved $check = Meta::forUser($userId)->getAll(array_keys($allowedFields)); $allowedFields = array_filter($allowedFields, function ($key) use ($allowedFields, $check) { return $allowedFields[$key] !== $check[$key]; }, ARRAY_FILTER_USE_KEY); if (empty($allowedFields)) { return true; } return Meta::forUser($userId) ->setAll($allowedFields); } // ───────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────── private function verifyOwnership(int $postId): bool { $post = get_post($postId); return $post && (int)$post->post_author === $this->userId; } /************************************************************* * TIMELINE HELPERS *************************************************************/ protected function maybeReorderTimelines(array $parentIDs):void { foreach ($parentIDs as $parentId) { try { $this->maybeReorderTimeline((int)$parentId); } catch (Exception $e) { error_log("Timeline reorder failed for parent {$parentId}: " . $e->getMessage()); } } } protected function maybeReorderTimeline(int $parentID):void { // clean_post_cache($parentID); $parent = get_post($parentID); if (!$parent) { return; } $children = get_children([ 'post_parent' => $parentID, 'posts_per_page' => -1, 'post_status' => ['publish', 'draft'], ]); if (count($children) === 0) { return; } $allPosts = array_merge([$parent], $children); // Sort by post_date usort($allPosts, function($a, $b) { return strtotime($a->post_date) <=> strtotime($b->post_date); }); // Check if order changed $needsReorder = false; foreach ($allPosts as $index => $post) { if ($index === 0 && $post->ID !== $parent->ID) { $needsReorder = true; break; } if ($index > 0 && (int)$post->menu_order !== $index) { $needsReorder = true; break; } } if (!$needsReorder) { // Just recalculate timelines without reordering $this->recalculateTimelines($allPosts); return; } // Handle parent swap if needed $newParent = $allPosts[0]; if ($newParent->ID !== $parent->ID) { $this->swapTimelineParent($parent, $newParent, $allPosts); } else { // Just update menu orders and timelines foreach ($allPosts as $index => $post) { if ($index === 0) continue; // Skip parent $success = jvb_update_post([ 'ID' => $post->ID, 'post_parent' => $newParent->ID, 'menu_order' => $index ]); } $this->recalculateTimelines($allPosts); } } private function recalculateTimelines(array $posts): void { $previousPost = null; $latestTimestamp = 0; $lastKey = array_key_last($posts); foreach ($posts as $index => $post) { $meta = Meta::forPost($post->ID); if ($index === 0) { $meta->set('timeline', ''); $previousPost = $post; continue; // Parent has no timeline } // Calculate timeline from previous post if ($previousPost) { $timeline = $this->calculateTimeline($previousPost, $post); if ($timeline) { $termId = $this->getOrCreateTerm($timeline, 'timeline'); if ($termId) { $success = $meta->set('timeline', $termId); } } } if ($lastKey === $index) { $latestTimestamp = strtotime($post->post_date); } $previousPost = $post; } // Update parent's latest_date if ($latestTimestamp > 0) { $success = update_post_meta($posts[0]->ID, BASE . 'latest_date', $latestTimestamp); } } private function calculateTimeline(\WP_POST $previous, \WP_POST $current): ?string { $previousDate = strtotime($previous->post_date); $currentDate = strtotime($current->post_date); if (!$previousDate || !$currentDate || $currentDate <= $previousDate) { return null; } $daysDiff = floor(($currentDate - $previousDate) / (60*60*24)); $weeks = floor($daysDiff / 7); if ($weeks === 0) { return 'Less than 1 Week'; } if ($weeks < 16) { return $weeks === 1 ? '1 Week' : $weeks . ' Weeks'; } $previousDateTime = new \DateTime($previous->post_date); $currentDateTime = new \DateTime($current->post_date); $interval = $previousDateTime->diff($currentDateTime); $months = ($interval->y * 12) + $interval->m; if ($months === 0) { return $weeks . ' Weeks'; } return ($months === 1) ? '1 Month' : $months . ' Months'; } private function swapTimelineParent(\WP_Post $oldParent, \WP_Post $newParent, array $allPosts): void { // Swap titles and content $originalTitle = $oldParent->post_title; $originalSlug = $oldParent->post_name; $originalContent = $oldParent->post_content; $updateParent = jvb_update_post([ 'ID' => $oldParent->ID, 'post_title' => 'Treatment', 'post_name' => sanitize_title('Treatment ' . $newParent->ID), 'post_content' => '', ]); $updateNewParent = jvb_update_post([ 'ID' => $newParent->ID, 'post_title' => $originalTitle, 'post_name' => $originalSlug, 'post_content' => $originalContent, 'post_parent' => 0, 'menu_order' => 0 ]); // Clear timeline taxonomy from new parent wp_set_object_terms($newParent->ID, [], BASE . 'timeline', false); // Update all other posts to new parent foreach ($allPosts as $index => $post) { if ($index === 0) continue; // Skip new parent $title = $post->post_title; if (str_starts_with($title, 'Treatment #')) { $title = 'Treatment #' . $index; } $childUpdate = jvb_update_post([ 'ID' => $post->ID, 'post_title' => $title, 'post_parent' => $newParent->ID, 'menu_order' => $index, ]); } // Recalculate timelines for all posts $this->recalculateTimelines($allPosts); } private function getOrCreateTerm(string $termName, string $taxonomy): ?int { $taxonomy = jvbCheckBase($taxonomy); $term = get_term_by('name', $termName, $taxonomy); if (!$term) { $result = wp_insert_term($termName, $taxonomy); if (is_wp_error($result)) { return null; } return $result['term_id']; } return $term->term_id; } protected function checkSharedFields(array $fields): void { foreach ($fields as $parentID => $shared) { $meta = Meta::forPost($parentID); $values = $meta->getAll($shared); $children = get_children([ 'post_parent' => $parentID, 'posts_per_page' => -1, 'fields' => 'ids', ]); if (empty($children)) { continue; } foreach ($children as $child) { Meta::forPost($child)->setAll($values); } } } protected function handleTimelineStatusChange(array $updates):void { $updates = array_filter($updates, function ($status) { return in_array($status, ['trash', 'delete', 'publish', 'draft']); }); foreach ($updates as $parentID => $status) { $children = get_children([ 'post_parent' => $parentID, 'posts_per_page' => -1, 'fields' => 'ids' ]); if (!empty($children)) { foreach($children as $child) { if ($status === 'trash') { wp_trash_post($child); } elseif ($status === 'delete') { wp_delete_post($child, true); }else { jvb_update_post([ 'ID' => $child, 'post_status' => $status ]); } } } } } protected function handlePost(string|int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array { // New post creation if (str_starts_with((string)$ID, 'new')) { $newId = wp_insert_post([ 'post_author' => $this->userId, 'post_type' => $registrar->getBased(), 'post_title' => $data['post_title'] ?? apply_filters('jvbDefaultTitle', '', $registrar->getSlug()), 'post_status' => $data['status'] ?? 'draft', ]); if (!$newId || is_wp_error($newId)) { $results['errors'][$ID] = 'Could not create post'; $progress->failItem($ID, 'Could not create post'); return $results; } $results['newPosts'][$ID] = $newId; $this->savePostFields($newId, $data); unset($data['content']); $results['success'][$newId] = $data; $progress->advance(); return $results; } //Existing post update if (!$this->verifyOwnership((int)$ID)) { $progress->failItem($ID, 'No permission to modify this post'); $results['errors'][$ID] = 'No permission'; return $results; } $result = $this->savePostFields((int)$ID, $data); unset($data['content']); if ($result) { $results['success'][$ID] = $data; } else { $results['errors'][$ID] = 'Could not update post data'; } if ($registrar && $registrar->hasFeature('is_timeline')) { $post = get_post((int)$ID); $parentId = $post->post_parent > 0 ? $post->post_parent : $post->ID; $fields = $registrar->getFields(); $sharedFields = array_keys(array_filter($fields, function ($field) { return !array_key_exists('for_all', $field) || !$field['for_all']; })); if (array_key_exists('post_date', $data) && !in_array($parentId, $results['timelineParents'])) { $results['timelineParents'][] = $parentId; } if ($parentId === $ID) { if (array_key_exists('post_status', $data) && !array_key_exists($parentId, $results['timelineStatus'])) { $results['timelineStatus'][$parentId] = $data['post_status']; } if (count(array_intersect($sharedFields, array_keys($data))) > 0) { if (!array_key_exists($parentId, $results['timelineSharedFields'])) { $results['timelineSharedFields'][$parentId] = []; } $temp = array_intersect($sharedFields, array_keys($data)); $results['timelineSharedFields'][$parentId] = array_unique(array_merge($results['timelineSharedFields'][$parentId], $temp)); } } } $progress->advance(); return $results; } protected function handleTerm(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array { error_log('Handling term '.$ID.' with data: '.print_r($data, true)); //Existing term update if ($registrar->hasFeature('is_ownable') && (!JVB()->roles()->isOwner($this->userId, $ID) && !JVB()->roles()->isManager($this->userId, $ID))) { error_log('Term is ownable. User does not own this term.'); $progress->failItem($ID, 'No permission to modify this term'); $results['errors'][$ID] = 'No permission'; return $results; } $result = $this->saveTermFields($ID, $data); unset($data['content']); if ($result) { $results['success'][$ID] = $data; } else { $results['errors'][$ID] = 'Could not update term data'; } $progress->advance(); return $results; } protected function handleUser(int $ID, array $data, Registrar $registrar, array $results, Progress $progress):array { //Existing term update if ($ID !== $this->userId || !user_can($this->userId, 'manage_options')) { $progress->failItem($ID, 'No permission to modify this term'); $results['errors'][$ID] = 'No permission'; return $results; } $result = $this->saveUserFields($ID, $data); unset($data['content']); if ($result) { $results['success'][$ID] = $data; } else { $results['errors'][$ID] = 'Could not update post data'; } $progress->advance(); return $results; } }