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_metadata' => $this->processMetadataUpdate($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 = []; $securedFiles = $data['secured_files'] ?? []; foreach ($securedFiles as $securedFile) { try { $result = $uploader->processUpload( $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'] ?? '', ] ); if (!is_wp_error($result)) { $standardized = [ 'attachment_id' => $result['attachment_id'], 'url' => $result['url'], 'file' => $result['file'], 'upload_id' => $securedFile['upload_id'] ?? null, ]; if ($standardized['upload_id']) { $processedResults[$standardized['upload_id']] = $standardized; } else { $processedResults[] = $standardized; } // Apply frontend metadata if provided if (!empty($securedFile['metadata'])) { $this->applyMeta($standardized['attachment_id'], $securedFile['metadata']); } $progress->advance(1); } else { $progress->failItem($securedFile, $result->get_error_message()); } } catch (Exception $e) { $progress->failItem($securedFile, $e->getMessage()); $errors[] = $e->getMessage(); } } // Handle destination (meta, post, post_group) $this->handleUploadDestination($data, $processedResults); // Cleanup temp files $this->cleanupTempFiles($securedFiles, $operation->userId); $outcome = 'success'; if (!empty($operation->failedItems)) { $outcome = count($operation->failedItems) === count($securedFiles) ? 'failed' : 'partial'; } return new Result( outcome: $outcome, result: $processedResults ); } /** * Attach upload results to content */ private function processAttachToContent(Operation $operation, array $data, Progress $progress): Result { $uploadOpId = $data['upload'] ?? null; if (!$uploadOpId) { throw new Exception('No upload operation ID provided'); } // Get results from the dependency $uploadOp = JVB()->queue()->get($uploadOpId); if (!$uploadOp || $uploadOp->outcome !== 'success') { throw new Exception("Upload operation {$uploadOpId} not completed successfully"); } $uploadResults = $uploadOp->result ?? []; if (empty($uploadResults)) { throw new Exception('No upload results found'); } // Attach to content via field if (!empty($data['field_name'])) { $this->updateFieldValue($data, $uploadResults); } $progress->advance(1); return new Result( outcome: 'success', result: ['attached' => count($uploadResults)] ); } /** * Process metadata updates for attachments */ private function processMetadataUpdate(Operation $operation, array $data, Progress $progress): Result { $updatedCount = 0; $errors = []; foreach ($data as $uploadId => $info) { if (!is_array($info) || empty($info['depends_on'])) { continue; } try { // 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; } $this->applyMeta($attachmentId, $info); $updatedCount++; $progress->advance(1); } catch (Exception $e) { $progress->failItem($uploadId, $e->getMessage()); $errors[] = $e->getMessage(); } } $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 = []; foreach ($dependencies as $dependency) { $res = JVB()->queue()->getOperationValue($dependency, 'result'); if (empty($res)) { continue; } // Results are stored at root level, keyed by upload_id // Filter to only include actual upload results (arrays with attachment_id) foreach ($res as $key => $value) { if (is_array($value) && isset($value['attachment_id'])) { $uploads[$key] = $value; } } } 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 = (int)$operation->userId; $createdPosts = []; $usedUploads = []; foreach($data['posts'] as $index => $post) { $progress->advance(); $post_title = array_key_exists('post_title', $post['fields']) ? sanitize_text_field($post['fields']['post_title']) : 'New '. JVB_CONTENT[$data['content']]['singular'].' '.($index + 1); $post_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' => $post_title, 'post_excerpt' => $post_excerpt ]; $newPostID = wp_insert_post($args); if ($newPostID && !is_wp_error($newPostID)) { $createdPosts[] = $newPostID; $featured_upload_id = $post['fields']['featured']??null; $featured_attachment_id = null; $gallery_attachment_ids = []; foreach ($post['images'] as $img) { $uploadId = $img['upload_id']; $usedUploads[] = $uploadId; if (array_key_exists($uploadId, $all_uploads)) { $attachmentId = $all_uploads[$uploadId]['attachment_id']; if ($uploadId === $featured_upload_id) { $featured_attachment_id = $attachmentId; } else { $gallery_attachment_ids[] = $attachmentId; } } } if ($featured_attachment_id) { set_post_thumbnail($newPostID, $featured_attachment_id); } elseif (!empty($gallery_attachment_ids)) { set_post_thumbnail($newPostID, $gallery_attachment_ids[0]); array_shift($gallery_attachment_ids); } if (!empty($gallery_attachment_ids)) { $meta = new MetaManager($newPostID, 'post'); $fields = jvbGetFields($content, 'post'); foreach($fields as $name => $config) { if ($config['type'] === 'gallery') { $meta->updateValue($name, implode(',', $gallery_attachment_ids)); break; } } } } } return new Result( outcome: !empty($createdPosts) ? 'success' : 'failed', result: ['posts' => $createdPosts] ); } private function processTimelineUploads(Operation $operation, array $data, Progress $progress, array $uploads):Result { $user = $operation->userId; $createdPosts = []; $usedUploads = []; $content = jvbCheckBase($data['content']); $config = Features::getConfig($content); $defaultTitle = 'New '.$config['singular']. ' '; foreach($data['posts'] as $index => $post) { $progress->advance(); $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); $progress->advance(); if ($parent && !is_wp_error($parent)) { $childPosts = []; $featured = $post['fields']['featured']??null; $featuredID = null; foreach ($post['images'] as $img) { $uploadId = $img['upload_id']; $usedUploads[] = $uploadId; if (array_key_exists($uploadId, $uploads)) { $attachmentId = (int)$uploads[$uploadId]['attachment_id']; if ($uploadId === $featured) { $featuredID = $attachmentId;; } else { $childPosts[] = $attachmentId; } } } if ($featuredID) { set_post_thumbnail($parent, $featuredID); } elseif (!empty($childPosts)) { set_post_thumbnail($parent, (int)$childPosts[0]); array_shift($childPosts); } if (!empty($childPosts)) { $args['post_parent'] = $parent; $args['post_excerpt'] = ''; $createdPosts[$parent] = []; 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)) { $createdPosts[$parent][] = $child; set_post_thumbnail($child, $imgID); } } } $this->updateTimelineMetadata($parent); } } return new Result( outcome: !empty($createdPosts) ? 'success' : 'failed', result: ['posts' => $createdPosts] ); } /** * 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 (!empty($metadata['title'])) { wp_update_post([ 'ID' => $attachmentId, 'post_title' => sanitize_text_field($metadata['title']), ]); } if (!empty($metadata['alt'])) { update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['alt'])); } if (!empty($metadata['caption'])) { wp_update_post([ 'ID' => $attachmentId, 'post_excerpt' => sanitize_textarea_field($metadata['caption']), ]); } } 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; } $existing = $meta->getValue($data['field_name']); $existingIds = !empty($existing) ? explode(',', $existing) : []; $allIds = array_unique(array_merge($existingIds, $attachmentIds)); $meta->updateValue($data['field_name'], implode(',', $allIds)); } 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->getValue($data['field_name']); $existingIds = !empty($existing) ? explode(',', $existing) : []; $allIds = array_unique(array_merge($existingIds, $attachmentIds)); $meta->updateValue($data['field_name'], implode(',', $allIds)); } private function getMetaManager(array $data): ?MetaManager { if (!empty($data['post_id'])) { return new MetaManager($data['post_id'], 'post'); } if (!empty($data['term_id'])) { return new MetaManager($data['term_id'], 'term'); } if (!empty($data['user'])) { $link = (int)get_user_meta($data['user'], BASE . 'link', true); if ($link) { return new MetaManager($link, 'post'); } } 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 = new MetaManager($postId, 'post'); $meta->updateValue('video', $attachmentId); } else { $meta = new MetaManager($postId, 'post'); $existing = $meta->getValue('documents'); $existingIds = !empty($existing) ? explode(',', $existing) : []; $existingIds[] = $attachmentId; $meta->updateValue('documents', implode(',', $existingIds)); } } 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; } }