type, self::HANDLED_TYPES)) { throw new Exception("UploadExecutor cannot handle type: {$operation->type}"); } try { $data = $operation->requestData; return match($operation->type) { 'image_upload' => $this->processFileUpload($operation, $data, 'image', $progress), 'video_upload' => $this->processFileUpload($operation, $data, 'video', $progress), 'document_upload' => $this->processFileUpload($operation, $data, 'document', $progress), 'update_image_meta' => $this->processMetaUpdate($operation, $data, $progress), 'temporary_cleanup' => $this->processTemporaryCleanup($operation, $data, $progress), 'attach_upload_to_content'=> $this->processAttachToContent($operation, $data, $progress), 'process_upload_groups' => $this->processUploadGroups($operation, $data, $progress), default => throw new Exception("Unknown type: {$operation->type}") }; } catch (Exception $e) { JVB()->error()->log( '[UploadExecutor]:execute', $e->getMessage(), [ 'operation_id' => $operation->id, 'operation_type' => $operation->type, 'user_id' => $operation->userId, ] ); return new Result( outcome: 'failed', result: ['error' => $e->getMessage()] ); } } /** * Process file uploads (images, videos, documents) */ private function processFileUpload(Operation $operation, array $data, string $fileType, Progress $progress): Result { $uploader = new UploadManager(); $processedResults = []; $errors = []; $uploadIds = []; $securedFiles = $data['secured_files'] ?? []; // Phase 1: File I/O — no DB, no transactions $prepared = []; foreach ($securedFiles as $securedFile) { $uploadIds[] = $securedFile['upload_id']; try { $prepared[$securedFile['upload_id']] = $uploader->prepareFile( $securedFile['temp_path'], [ 'file_type' => $fileType, 'user_id' => $operation->userId, 'post_id' => (int)($data['post_id'] ?? 0), 'term_id' => (int)($data['term_id'] ?? 0), 'original_name' => $securedFile['original_name'], 'mime_type' => $securedFile['mime_type'], 'content' => $data['content'] ?? '', ] ); } catch (Exception $e) { $progress->failItem($securedFile['upload_id'], $e->getMessage()); $errors[] = $e->getMessage(); } } // Phase 2: DB registration — quick per-file writes foreach ($prepared as $uploadId => $fileInfo) { try { $result = $uploader->registerFile($fileInfo); if (!$result['success']) { $progress->failItem($uploadId, 'Registration failed'); $errors[] = "Failed to register {$uploadId}"; continue; } $processedResults[$uploadId] = [ 'upload_id' => $uploadId, 'attachment_id' => $result['attachment_id'] ?? 0, 'url' => $result['url'] ?? '', 'sizes' => $result['sizes'] ?? [], ]; // Apply frontend metadata if provided $securedFile = $this->findSecuredFile($securedFiles, $uploadId); if ($securedFile && !empty($securedFile['metadata'])) { $this->applyMeta($result['attachment_id'], $securedFile['metadata']); } $progress->advance(); } catch (Exception $e) { $progress->failItem($uploadId, $e->getMessage()); $errors[] = $e->getMessage(); } } // Destination + cleanup unchanged $this->handleUploadDestination($data, $processedResults); $this->cleanupTempFiles($securedFiles, $operation->userId); $outcome = count($processedResults) > 0 ? 'success' : 'failed'; if (count($processedResults) > 0 && !empty($errors)) { $outcome = 'partial'; } return new Result( outcome: $outcome, result: [ 'upload_ids' => $uploadIds, 'uploads' => $processedResults, 'processed_count' => count($processedResults), 'total_count' => count($uploadIds), 'errors' => $errors, ] ); } /** * Find a secured file entry by upload_id */ private function findSecuredFile(array $securedFiles, string $uploadId): ?array { foreach ($securedFiles as $file) { if (($file['upload_id'] ?? null) === $uploadId) { return $file; } } return null; } /** * Attach upload results to content */ private function processAttachToContent(Operation $operation, array $data, Progress $progress): Result { $uploadOpId = $data['upload'] ?? null; error_log('processing attached to content: '.print_r($data, true)); if (!$uploadOpId) { throw new Exception('No upload operation ID provided'); } $uploadOp = JVB()->queue()->get($uploadOpId); if (!$uploadOp || !in_array($uploadOp->outcome, ['success', 'partial'])) { throw new Exception("Upload operation {$uploadOpId} not completed successfully"); } $uploadResults = $uploadOp->result['uploads'] ?? []; if (empty($uploadResults)) { throw new Exception('No upload results found'); } // Resolve post_id from content_update dependency for new posts if (empty($data['post_id']) || str_starts_with((string)($data['item_id'] ?? ''), 'new')) { foreach ($operation->dependencies as $depId) { $dep = JVB()->queue()->get($depId); if ($dep && $dep->type === 'content_update' && !empty($dep->result['new_posts'])) { $itemId = $data['item_id'] ?? null; if ($itemId && isset($dep->result['new_posts'][$itemId])) { $data['post_id'] = $dep->result['new_posts'][$itemId]; break; } } } if (empty($data['post_id'])) { throw new Exception('Could not resolve post_id from dependencies'); } } if (!empty($data['field_name'])) { $this->saveToMeta($data, $uploadResults); } $progress->advance(1); return new Result( outcome: 'success', result: ['attached' => count($uploadResults), 'post_id' => $data['post_id']] ); } /** * Process metadata updates for attachments */ private function processMetaUpdate(Operation $operation, array $data, Progress $progress): Result { $updatedCount = 0; $errors = []; $postsAttachedTo =[]; error_log('Processing Meta Update with data: '.print_r($data, true)); foreach ($data as $uploadId => $info) { if (!is_array($info)) { continue; } $success = true; try { if (array_key_exists('depends_on', $info)) { // Get the dependency operation to find attachment ID $depOp = JVB()->queue()->get($info['depends_on']); if (!$depOp || !$depOp->result) { $errors[] = "Dependency {$info['depends_on']} not found or has no result"; continue; } $attachmentId = $this->findAttachmentByUploadId($uploadId, $depOp->result); if (!$attachmentId) { $errors[] = "No attachment found for upload ID: {$uploadId}"; continue; } } else { $attachmentId = $info['attachmentId']??false; } if (!$attachmentId) { $errors[] = "No attachment found for: ".print_r($info, true); continue; } $this->applyMeta($attachmentId, $info); $updatedCount++; $progress->advance(1); } catch (Exception $e) { $success = false; $progress->failItem($uploadId, $e->getMessage()); $errors[] = $e->getMessage(); } if ($success) { $postID = wp_get_post_parent_id($attachmentId); if ($postID && !in_array($postID, $postsAttachedTo)){ $postsAttachedTo[] = $postID; } } } if (!empty ($postsAttachedTo)) { foreach ($postsAttachedTo as $postId) { Cache::invalidateItem('post', $postId); } } $outcome = $updatedCount > 0 ? 'success' : 'failed'; if ($updatedCount > 0 && !empty($errors)) { $outcome = 'partial'; } return new Result( outcome: $outcome, result: [ 'updated' => $updatedCount, 'errors' => $errors, ] ); } /** * Cleanup temporary upload files */ private function processTemporaryCleanup(Operation $operation, array $data, Progress $progress): Result { $uploader = new UploadManager(); // Cleanup secured files if (!empty($data['files'])) { foreach ($data['files'] as $file) { if (!empty($file['temp_path']) && file_exists($file['temp_path'])) { @unlink($file['temp_path']); } $progress->advance(1); } } // Cleanup specific temp paths if (!empty($data['temp_paths']) && is_array($data['temp_paths'])) { foreach ($data['temp_paths'] as $tempPath) { if (file_exists($tempPath)) { @unlink($tempPath); } } } // Cleanup empty directories $uploader->cleanupEmptyTempDirs($operation->userId); return new Result( outcome: 'success', result: ['cleaned' => true] ); } /** * Process grouped uploads into posts */ private function processUploadGroups(Operation $operation, array $data, Progress $progress): Result { $dependencies = $operation->dependencies; if (empty($dependencies)) { throw new Exception('No dependencies found for group uploads.'); } $uploads = []; $uploadIds = []; foreach ($dependencies as $dependency) { $res = JVB()->queue()->getOperationValue($dependency, 'result'); if (empty($res)) { continue; } // Check if dependency result has upload_ids if (isset($res['upload_ids'])) { $uploadIds = array_merge($uploadIds, $res['upload_ids']); } // Results are stored in 'uploads', keyed by upload_id // Filter to only include actual upload results (arrays with attachment_id) foreach ($res['uploads'] as $key => $value) { if (is_array($value) && isset($value['attachment_id'])) { $uploads[$key] = $value; // If we didn't get upload_ids from result, track them from keys if (!isset($res['upload_ids']) && !in_array($key, $uploadIds)) { $uploadIds[] = $key; } } } } if (empty($uploads)) { throw new Exception('No uploads found for group uploads.'); } $all_uploads = []; foreach($uploads as $upload_id => $img) { if (!array_key_exists('attachment_id', $img) || (int)$img['attachment_id'] === 0){ continue; } $all_uploads[$upload_id] = [ 'upload_id' => $upload_id, 'attachment_id'=> (int)$img['attachment_id'] ]; } $content = jvbCheckBase($data['content']); if (Features::forContent($data['content'])->has('is_timeline')) { return $this->processTimelineUploads($operation, $data, $progress, $all_uploads); } $user = $operation->userId; $createdPosts = []; $errors = []; $groupMappings = []; $usedUploads = []; foreach($data['posts'] as $index => $post) { try { $groupId = $post['groupId'] ?? null; // Create post for this group $created = $this->createPostFromGroup($post, $index+1, $content, $uploads, $operation); if ($created) { $postId = $created['ID']; $createdPosts[] = [ 'post_id' => $postId, 'group_id' => $groupId, ]; if ($groupId) { $groupMappings[$groupId] = $postId; } $usedUploads = array_merge($usedUploads, $created['usedUploads']); $progress->advance(1); } } catch (Exception $e) { $errors[] = $e->getMessage(); $progress->failItem($index ?? 'unknown', $e->getMessage()); } } $outcome = !empty($createdPosts) ? 'success' : 'failed'; if (!empty($createdPosts) && !empty($errors)) { $outcome = 'partial'; } return new Result( outcome: $outcome, result: [ 'upload_ids' => $usedUploads, 'created_posts' => $createdPosts, 'group_mappings' => $groupMappings, 'post_count' => count($createdPosts), 'processed_uploads' => count($uploads), 'errors' => $errors, ] ); } protected function createPostFromGroup(array $post, int $index, string $content, array $uploads, Operation $op):array|false { $config = JVB_CONTENT[jvbNoBase($content)]??false; if (!$config) { throw new Exception('No config found for content: '.$content.'.'); } $post_title = array_key_exists('post_title', $post['fields']) ? sanitize_text_field($post['fields']['post_title']) : 'New '. $config['singular'].' '.($index + 1); $post_excerpt = array_key_exists('post_excerpt', $post['fields']) ? sanitize_textarea_field($post['fields']['post_excerpt']) : ''; $ID = wp_insert_post([ 'post_type' => $content, 'post_author' => $op->userId, 'post_status' => 'draft', 'post_title' => $post_title, 'post_excerpt' => $post_excerpt, ]); if (!$ID || is_wp_error($ID)) { throw new Exception('Could not create post: '.$ID?->get_error_message()); } $uploadIds = []; $featured_upload_id = $post['fields']['featured']??null; $featured_attachment_id = null; $gallery = []; foreach ($post['images'] as $img) { $uploadId = $img['upload_id']; if (array_key_exists($uploadId, $uploads)){ $imgID = $uploads[$uploadId]['attachment_id']; if ($uploadId === $featured_upload_id) { $featured_attachment_id = $imgID; } else { $gallery[] = $imgID; } $uploadIds[] = $uploadId; } } if ($featured_attachment_id) { set_post_thumbnail($ID, $featured_attachment_id); } elseif (!empty($gallery)) { set_post_thumbnail($ID, $gallery[0]); array_shift($gallery); } if (!empty($gallery)) { $meta = Meta::forPost($ID); $fields = jvbGetFields($content, 'post'); //add images to first found gallery field $found = false; foreach ($fields as $name =>$config) { if ($config['type'] === 'gallery' || ($config['type'] === 'upload' && (array_key_exists('multiple', $config) && $config['multiple'] === true))) { $found = true; $meta->set($name, implode(',', $gallery)); break; } } if (!$found) { error_log('Could not find a gallery upload field for post '.$ID); } $meta->save(); } return [ 'ID' => $ID, 'usedUploads' => $uploadIds ]; } private function processTimelineUploads(Operation $operation, array $data, Progress $progress, array $uploads):Result { $user = $operation->userId; $createdPosts = []; $usedUploads = []; $errors = []; $content = jvbCheckBase($data['content']); $config = Features::getConfig($content); $defaultTitle = 'New '.$config['singular']. ' '; foreach($data['posts'] as $index => $post) { try { $title = array_key_exists('post_title', $post['fields']) ? sanitize_text_field($post['fields']['post_title']) : $defaultTitle . ($index + 1); $excerpt = array_key_exists('post_excerpt', $post['fields']) ? sanitize_textarea_field($post['fields']['post_excerpt']) : ''; $args = [ 'post_type' => $content, 'post_author' => $user, 'post_status' => 'draft', 'post_title' => $title, 'post_slug' => sanitize_title($title), 'post_excerpt' => $excerpt ]; $parent = wp_insert_post($args); if (!$parent || is_wp_error($parent)) { throw new Exception('Could not create post: '.$parent->get_error_message()); } $childPosts = []; $featured = $post['fields']['featured']??null; $featuredID = null; foreach ($post['images'] as $img) { $uploadId = $img['upload_id']; if (array_key_exists($uploadId, $uploads)) { $attachmentId = (int)$uploads[$uploadId]['attachment_id']; if ($uploadId === $featured) { $featuredID = $attachmentId;; } else { $childPosts[] = $attachmentId; } } } if ($featuredID) { $usedUploads[] = $featuredID; set_post_thumbnail($parent, $featuredID); } elseif (!empty($childPosts)) { set_post_thumbnail($parent, (int)$childPosts[0]); $usedUploads[] = (int)$childPosts[0]; array_shift($childPosts); } $createdChildren = []; if (!empty($childPosts)) { $args['post_parent'] = $parent; $args['post_excerpt'] = ''; foreach($childPosts as $i => $imgID) { $treatment = $i + 1; $args ['post_title'] = $title.' - Treatment #'.$treatment; $child = wp_insert_post($args); if ($child && !is_wp_error($child)) { $createdChildren = $child; $usedUploads[] = $imgID; set_post_thumbnail($child, $imgID); } } } $createdPosts[] = [ 'parent' => $parent, 'children' => $createdChildren ]; $this->updateTimelineMetadata($parent); $progress->advance(); } catch (Exception $e) { $errors[] = $e->getMessage(); $progress->failItem($index ?? 'unknown', $e->getMessage()); } } $outcome = !empty($createdPosts) ? 'success' : 'failed'; if (!empty($createdPosts) && !empty($errors)) { $outcome = 'partial'; } return new Result( outcome: $outcome, result: [ 'upload_ids' => $usedUploads, 'created_posts' => $createdPosts, 'post_count' => count($createdPosts), 'processed_uploads' => count($uploads), 'errors' => $errors ] ); } /** * Update timeline parent post with count and latest date * @param int $parentId Parent timeline post ID */ private function updateTimelineMetadata(int $parentId): void { // Get all child posts $children = get_children([ 'post_parent' => $parentId, 'post_type' => get_post_type($parentId), 'post_status' => ['publish', 'draft'], 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids' ]); // Count includes parent + children $number = count($children) + 1; // Update both meta fields update_post_meta($parentId, BASE . 'number', $number); update_post_meta($parentId, BASE . 'latest_date', time()); } // ───────────────────────────────────────────────────────────── // Helper methods // ───────────────────────────────────────────────────────────── private function applyMeta(int $attachmentId, array $metadata): void { if (array_key_exists('image-alt-text', $metadata)) { update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['image-alt-text'])); } $postUpdates = []; if (array_key_exists('image-title', $metadata)) { $postUpdates['post_title'] = sanitize_text_field($metadata['image-title']); } if (array_key_exists('image-caption', $metadata)) { $postUpdates['post_excerpt'] = sanitize_textarea_field($metadata['image-caption']); } if (array_key_exists('image-description', $metadata)) { $postUpdates['post_excerpt']= sanitize_textarea_field($metadata['image-caption']); } if (!empty($postUpdates)){ $postUpdates['ID'] = $attachmentId; wp_update_post($postUpdates); } } private function handleUploadDestination(array $data, array $results): void { $destination = $data['destination'] ?? 'meta'; switch ($destination) { case 'meta': $this->saveToMeta($data, $results); break; case 'post': $this->createIndividualPosts($data, $results); break; case 'post_group': // Handled by process_upload_groups break; } } private function saveToMeta(array $data, array $results): void { if (empty($data['field_name'])) { return; } $attachmentIds = array_column($results, 'attachment_id'); $meta = $this->getMetaManager($data); if (!$meta) { return; } $fieldType = $data['field_type'] ?? 'single'; if ($fieldType === 'single') { // Single field: replace with latest upload $meta->set($data['field_name'], end($attachmentIds)); } else { // Multi field: merge with existing $existing = $meta->get($data['field_name']); $existingIds = !empty($existing) ? explode(',', $existing) : []; $allIds = array_unique(array_merge($existingIds, $attachmentIds)); $meta->set($data['field_name'], implode(',', $allIds)); } $meta->save(); } private function updateFieldValue(array $data, array $results): void { if (empty($data['field_name'])) { return; } $attachmentIds = array_column($results, 'attachment_id'); $meta = $this->getMetaManager($data); if (!$meta) { return; } $existing = $meta->get($data['field_name']); $existingIds = !empty($existing) ? explode(',', $existing) : []; $allIds = array_unique(array_merge($existingIds, $attachmentIds)); $meta->set($data['field_name'], implode(',', $allIds)); $meta->save(); } private function getMetaManager(array $data): ?Meta { if (!empty($data['post_id'])) { return Meta::forPost($data['post_id']); } if (!empty($data['term_id'])) { return Meta::forTerm($data['term_id']); } if (!empty($data['user'])) { $link = (int)get_user_meta($data['user'], BASE . 'link', true); if ($link) { return Meta::forPost($link); } } return null; } private function createIndividualPosts(array $data, array $results): array { if (empty($data['content'])) { return []; } $createdPosts = []; foreach ($results as $result) { $attachmentId = $result['attachment_id']; $attachment = get_post($attachmentId); $postData = [ 'post_type' => jvbCheckBase($data['content']), 'post_title' => $attachment->post_title, 'post_status' => 'draft', 'post_author' => $data['user'] ?? get_current_user_id(), ]; $postId = wp_insert_post($postData); if (!is_wp_error($postId)) { $this->attachFileToPost($postId, $attachmentId, $data); $createdPosts[] = [ 'post_id' => $postId, 'attachment_id' => $attachmentId, ]; } } return $createdPosts; } private function attachFileToPost(int $postId, int $attachmentId, array $data): void { $attachment = get_post($attachmentId); $mimeType = $attachment->post_mime_type; if (str_starts_with($mimeType, 'image/')) { set_post_thumbnail($postId, $attachmentId); } elseif (str_starts_with($mimeType, 'video/')) { $meta = Meta::forPost($postId); $meta->set('video', $attachmentId); $meta->save(); } else { $meta = Meta::forPost($postId); $existing = $meta->get('documents'); $existingIds = !empty($existing) ? explode(',', $existing) : []; $existingIds[] = $attachmentId; $meta->set('documents', implode(',', $existingIds)); $meta->save(); } } private function cleanupTempFiles(array $securedFiles, int $userId): void { $uploader = new UploadManager(); foreach ($securedFiles as $secured) { if (!empty($secured['temp_path']) && file_exists($secured['temp_path'])) { @unlink($secured['temp_path']); } } $uploader->cleanupEmptyTempDirs($userId); } private function findAttachmentByUploadId(string $uploadId, array $results): ?int { if (isset($results[$uploadId]['attachment_id'])) { return (int)$results[$uploadId]['attachment_id']; } foreach ($results as $result) { if (isset($result['upload_id']) && $result['upload_id'] === $uploadId) { return (int)$result['attachment_id']; } } return null; } }