<?php
|
namespace JVBase\managers\queue\executors;
|
|
use JVBase\managers\queue\{Executor, Operation, Progress, Result};
|
use JVBase\managers\UploadManager;
|
use JVBase\meta\Meta;
|
use Exception;
|
use JVBase\utility\Features;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Executor for upload-related queue operations.
|
* Handles: image_upload, video_upload, document_upload,
|
* update_image_meta, temporary_cleanup, attach_upload_to_content, process_upload_groups
|
*/
|
final class UploadExecutor implements Executor
|
{
|
private const HANDLED_TYPES = [
|
'image_upload',
|
'video_upload',
|
'document_upload',
|
'update_image_meta',
|
'temporary_cleanup',
|
'attach_upload_to_content',
|
'process_upload_groups'
|
];
|
|
public function execute(Operation $operation, Progress $progress): Result
|
{
|
if (!in_array($operation->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'] ?? [];
|
|
foreach ($securedFiles as $securedFile) {
|
$uploadIds[] = $securedFile['upload_id'];
|
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)) {
|
$progress->failItem($securedFile['upload_id'], $result->get_error_message());
|
$errors[] = $result->get_error_message();
|
continue;
|
}
|
|
$processedResults[$securedFile['upload_id']] = [
|
'upload_id' => $securedFile['upload_id'],
|
'attachment_id' => $result['attachment_id'] ?? 0,
|
'url' => $result['url'] ?? '',
|
'sizes' => $result['sizes'] ?? [],
|
];
|
if (!empty($securedFile['metadata'])) {
|
$this->applyMeta($securedFile['attachment_id'], $securedFile['metadata']);
|
}
|
|
$progress->advance();
|
|
} catch (Exception $e) {
|
$progress->failItem($securedFile['upload_id'], $e->getMessage());
|
$errors[] = $e->getMessage();
|
}
|
}
|
|
// Handle destination (meta, post, post_group)
|
$this->handleUploadDestination($data, $processedResults);
|
|
// Cleanup temp files
|
$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
|
]
|
);
|
}
|
|
/**
|
* 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');
|
}
|
|
$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 = [];
|
|
foreach ($data as $uploadId => $info) {
|
if (!is_array($info)) {
|
continue;
|
}
|
|
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) {
|
$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 = [];
|
$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'] === '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);
|
}
|
}
|
|
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 (!empty($metadata['image-title'])) {
|
wp_update_post([
|
'ID' => $attachmentId,
|
'post_title' => sanitize_text_field($metadata['image-title']),
|
]);
|
}
|
|
if (!empty($metadata['image-alt-text'])) {
|
update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['image-alt-text']));
|
}
|
|
if (!empty($metadata['image-caption'])) {
|
wp_update_post([
|
'ID' => $attachmentId,
|
'post_excerpt' => sanitize_textarea_field($metadata['image-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;
|
}
|
|
$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));
|
}
|
}
|
|
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));
|
}
|
|
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);
|
} else {
|
$meta = Meta::forPost($postId);
|
$existing = $meta->get('documents');
|
$existingIds = !empty($existing) ? explode(',', $existing) : [];
|
$existingIds[] = $attachmentId;
|
$meta->set('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;
|
}
|
}
|