| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\rest\RestRouteManager; |
| | | use JVBase\meta\MetaManager; |
| | | use JVBase\managers\queue\executors\UploadExecutor; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | | use JVBase\meta\Meta; |
| | | use JVBase\managers\UploadManager; |
| | | use JVBase\rest\Route; |
| | | use JVBase\utility\Features; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | |
| | | if (!defined('ABSPATH')) { |
| | | exit; // Exit if accessed directly |
| | | } |
| | | class UploadRoutes extends RestRouteManager |
| | | class UploadRoutes extends Rest |
| | | { |
| | | |
| | | public function __construct() |
| | | { |
| | | $this->action = 'dash-'; |
| | | parent::__construct(); |
| | | add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); |
| | | |
| | | add_action('init', [$this, 'registerUploadExecutors'], 5); |
| | | } |
| | | |
| | | /** |
| | | * Register upload operation types with the queue's TypeRegistry |
| | | */ |
| | | public function registerUploadExecutors(): void |
| | | { |
| | | $registry = JVB()->queue()->registry(); |
| | | $executor = new UploadExecutor(); |
| | | |
| | | // Image uploads - chunked at 5 files |
| | | $registry->register('image_upload', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 5 |
| | | )); |
| | | |
| | | // Video uploads - one at a time (heavy processing) |
| | | $registry->register('video_upload', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 1 |
| | | )); |
| | | |
| | | // Document uploads - chunked at 10 |
| | | $registry->register('document_upload', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 10 |
| | | )); |
| | | |
| | | // Metadata updates |
| | | $registry->register('update_image_meta', new TypeConfig( |
| | | executor: $executor |
| | | )); |
| | | |
| | | // Cleanup - chunked at 5 |
| | | $registry->register('temporary_cleanup', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'files', |
| | | chunkSize: 5 |
| | | )); |
| | | |
| | | // Attach to content (depends on upload completing) |
| | | $registry->register('attach_upload_to_content', new TypeConfig( |
| | | executor: $executor |
| | | )); |
| | | |
| | | // Process upload groups into posts |
| | | $registry->register('process_upload_groups', new TypeConfig( |
| | | executor: $executor, |
| | | chunkKey: 'posts', |
| | | chunkSize: 5 |
| | | )); |
| | | } |
| | | |
| | | /** |
| | | * Registers upload routes |
| | | * @return void |
| | | */ |
| | | public function registerRoutes():void |
| | | { |
| | | register_rest_route($this->namespace, '/uploads', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleUploadRequest'], |
| | | 'permission_callback' => [$this, 'checkPermission'], |
| | | 'args' => [ |
| | | 'content' => [ |
| | | 'required' => true, |
| | | 'type' => 'string' |
| | | ], |
| | | 'user' => [ |
| | | 'required' => true, |
| | | 'type' => 'int' |
| | | ], |
| | | 'post_id' => [ |
| | | 'required' => false, |
| | | 'type' => 'int' |
| | | ], |
| | | 'term_id' => [ |
| | | 'required' => false, |
| | | 'type' => 'int' |
| | | ], |
| | | 'field_name' => [ |
| | | 'required' => false, |
| | | 'type' => 'string' |
| | | ], |
| | | 'id' => [ |
| | | 'required' => false, |
| | | 'type' => 'string' |
| | | ], |
| | | 'mode' => [ |
| | | 'required' => false, |
| | | 'type' => 'string', |
| | | 'default' => 'direct' |
| | | ] |
| | | ] |
| | | ]); |
| | | Route::for('uploads') |
| | | ->post([$this, 'handleUpload']) |
| | | ->auth(PermissionHandler::combine(['nonce'])) |
| | | ->rateLimit(30) |
| | | ->register(); |
| | | |
| | | register_rest_route($this->namespace, '/uploads/groups', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleGroupingRequest'], |
| | | 'permission_callback' => [$this, 'checkPermission'], |
| | | 'args' => [ |
| | | 'id' => [ |
| | | 'required' => true, |
| | | 'type' => 'string', |
| | | 'description' => 'Original upload operation ID' |
| | | ], |
| | | 'content' => [ |
| | | 'required' => true, |
| | | 'type' => 'string' |
| | | ], |
| | | 'user' => [ |
| | | 'required' => true, |
| | | 'type' => 'integer' |
| | | ], |
| | | ] |
| | | ]); |
| | | Route::for('uploads/groups') |
| | | ->post([$this, 'handleGroupingRequest']) |
| | | ->auth(PermissionHandler::combine(['nonce'])) |
| | | ->rateLimit(30) |
| | | ->args([ |
| | | 'id' => 'string|required', |
| | | 'content' => 'string|required', |
| | | 'user' => 'int|required' |
| | | ]) |
| | | ->register(); |
| | | |
| | | register_rest_route($this->namespace, '/uploads/meta', [ |
| | | 'methods' => 'POST', |
| | | 'callback' => [$this, 'handleMetadataUpdate'], |
| | | 'permission_callback' => [$this, 'checkPermission'], |
| | | 'args' => [ |
| | | 'id' => [ |
| | | 'type' => 'string', |
| | | 'required' => false, |
| | | 'description' => 'Original upload operation ID (for updates during processing)' |
| | | ], |
| | | 'attachment_ids' => [ |
| | | 'type' => 'array', |
| | | 'required' => false, |
| | | 'description' => 'Direct attachment IDs (for updates after completion)', |
| | | 'items' => ['type' => 'integer'] |
| | | ], |
| | | 'upload_ids' => [ |
| | | 'type' => 'array', |
| | | 'required' => false, |
| | | 'description' => 'Frontend upload IDs to map to attachments', |
| | | 'items' => ['type' => 'string'] |
| | | ], |
| | | 'metadata' => [ |
| | | 'type' => 'object', |
| | | 'required' => true, |
| | | 'description' => 'Metadata changes to apply' |
| | | ], |
| | | 'user' => [ |
| | | 'type' => 'integer', |
| | | 'required' => true |
| | | ] |
| | | ] |
| | | ]); |
| | | Route::for('uploads/meta') |
| | | ->post([$this, 'handleMetadataUpdate']) |
| | | ->auth(PermissionHandler::combine(['nonce'])) |
| | | ->rateLimit(30) |
| | | ->args([ |
| | | 'user' => 'int|required', |
| | | 'items' => 'array|required' |
| | | ]) |
| | | ->register(); |
| | | } |
| | | |
| | | /** |
| | | * Build the main $context for UploadManager from the $request params |
| | | * @param WP_REST_Request $request |
| | | * @return array |
| | | */ |
| | | protected function buildUploadArgs(WP_REST_Request $request):array |
| | | { |
| | | $data = $request->get_params(); |
| | | error_log('Upload Args: '.print_r($data, true)); |
| | | $args = [ |
| | | 'content' => (array_key_exists('content', $data) && (array_key_exists(jvbGetValidType($data['content']), JVB_CONTENT) || $data['content'] === 'options')) ? jvbGetValidType($data['content']) : false, |
| | | 'user' => (array_key_exists('user', $data) && $this->userCheck($data['user'])) ? (int)$data['user'] : false, |
| | | 'id' => (array_key_exists('id', $data) && is_string($data['id'])) ? sanitize_text_field(str_replace('_upload', '', $data['id'])) : false, |
| | | 'post_id' => (array_key_exists('post_id', $data) && is_numeric($data['post_id'])) ? absint($data['post_id']) : false, |
| | | 'term_id' => (array_key_exists('term_id', $data) && is_numeric($data['term_id'])) ? absint($data['term_id']) : false, |
| | | 'field_name' => (array_key_exists('field_name', $data) && is_string($data['field_name'])) ? sanitize_text_field($data['field_name']) : false, |
| | | 'mode' => (array_key_exists('mode', $data) && in_array($data['mode'], ['direct', 'selection'])) ? sanitize_text_field($data['mode']) : false, |
| | | ]; |
| | | $args = []; |
| | | foreach ($data as $key => $value) { |
| | | switch ($key) { |
| | | case 'depends_on': |
| | | if (is_string($value) && !empty($value)) { |
| | | $args['depends_on'] = sanitize_text_field($value); |
| | | } |
| | | break; |
| | | case 'item_id': |
| | | if (is_numeric($value)) { |
| | | $args['item_id'] = absint($value); |
| | | if (!array_key_exists('post_id', $args)) { |
| | | $args['post_id'] = absint($value); |
| | | } |
| | | } |
| | | break; |
| | | // Post Type/Taxonomy |
| | | case 'content': |
| | | $key = str_replace('-', '_', $key); |
| | | if ($value === 'options' || array_key_exists($value, JVB_CONTENT) || Features::forTaxonomy($key)->has('is_content')) { |
| | | $args['content'] = $value; |
| | | } |
| | | break; |
| | | case 'destination': |
| | | if (in_array($value, ['meta', 'post', 'post_group'])) { |
| | | $args['destination'] = sanitize_text_field($value); |
| | | if (in_array($value, ['post', 'post_group']) && empty($data['content'])) { |
| | | throw new Exception("Content type required for destination: {$value}"); |
| | | } |
| | | } |
| | | break; |
| | | // User ID |
| | | case 'user': |
| | | if ($this->userCheck($value)) { |
| | | $args['user'] = (int) $value; |
| | | if (!array_key_exists('post_id', $args) && |
| | | !array_key_exists('post_id', $data) && |
| | | !array_key_exists('term_id', $data) && |
| | | !array_key_exists('item_id', $data)) { |
| | | $args['post_id'] = (int)get_user_meta((int) $value, BASE.'link', true); |
| | | } |
| | | } |
| | | break; |
| | | // Operation ID |
| | | case 'id': |
| | | if (is_string($value)) { |
| | | $value = sanitize_text_field($value); |
| | | $args['id'] = $value; |
| | | $args['upload'] = $value.'_upload'; |
| | | } |
| | | break; |
| | | // Post ID |
| | | case 'post_id': |
| | | if (is_numeric($value)) { |
| | | $args['post_id'] = absint($value); |
| | | } |
| | | break; |
| | | // Term ID |
| | | case 'term_id': |
| | | if (is_numeric($value)) { |
| | | $args['term_id'] = absint($value); |
| | | } |
| | | break; |
| | | // Field Name, as defined for Meta.php |
| | | case 'field_name': |
| | | if (is_string($value)) { |
| | | $args['field_name'] = sanitize_text_field($value); |
| | | } |
| | | break; |
| | | // Upload Mode |
| | | case 'mode': |
| | | if (in_array($value, ['direct', 'selection'])) { |
| | | $args['mode'] = sanitize_text_field($value); |
| | | } |
| | | break; |
| | | case 'upload_ids': |
| | | if (is_string($value)) { |
| | | // Parse JSON array |
| | | $decoded = json_decode($value, true); |
| | | if (is_array($decoded)) { |
| | | $args['upload_ids'] = $decoded; |
| | | } |
| | | } elseif (is_array($value)) { |
| | | // Already an array (shouldn't happen with FormData JSON, but handle it) |
| | | $args['upload_ids'] = $value; |
| | | } |
| | | break; |
| | | case 'metadata': |
| | | if (!empty($value)) { |
| | | $metadata = is_string($value) ? json_decode($value, true) : $value; |
| | | if (is_array($metadata)) { |
| | | foreach ($metadata as $k => $v) { |
| | | if (in_array($k, ['title', 'caption', 'alt', 'depends_on'])) { |
| | | $args['metadata'][$k] = sanitize_text_field($v); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | break; |
| | | case 'posts': |
| | | if (is_string($value)) { |
| | | $decoded = json_decode($value, true); |
| | | if (is_array($decoded)) { |
| | | $args['posts'] = $decoded; |
| | | } |
| | | } |
| | | break; |
| | | |
| | | if ((!$args['post_id'] && !$args['term_id']) && contentIsJVBUserType($args['content'])) { |
| | | $args['post_id'] = (int)get_user_meta($args['user'], BASE.'link', true); |
| | | } |
| | | |
| | | $args['upload'] = ($args['id']) ? $args['id'].'_upload' : false; |
| | | case 'group_titles': |
| | | if (is_string($value)) { |
| | | $decoded = json_decode($value, true); |
| | | if (is_array($decoded)) { |
| | | $args['group_titles'] = array_map('sanitize_text_field', $decoded); |
| | | } |
| | | } |
| | | break; |
| | | // Other field info |
| | | case 'field_key': |
| | | case 'field_type': |
| | | case 'subtype': |
| | | case 'item_id': |
| | | case 'context': |
| | | if (is_string($value)) { |
| | | $args[$key] = sanitize_text_field($value); |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | return $args; |
| | | } |
| | | |
| | |
| | | * |
| | | * @return WP_REST_Response |
| | | */ |
| | | public function handleUploadRequest(WP_REST_Request $request): WP_REST_Response |
| | | public function handleUpload(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | try { |
| | | if (!isset($_FILES['files'])) { |
| | | return $this->createStandardResponse( |
| | | false, |
| | | [], |
| | | __('No files uploaded.', 'jvb') |
| | | ); |
| | | } |
| | | |
| | | |
| | | $files = $request->get_file_params(); |
| | | $args = $this->buildUploadArgs($request); |
| | | error_log('Validating upload args: '.print_r($args, true)); |
| | | // Validation... |
| | | $validation_errors = []; |
| | | if (!$args['content'] && !$args['term_id'] && !$args['post_id']) $validation_errors[] = 'content or term or post id required'; |
| | | if (!$args['user']) $validation_errors[] = 'user'; |
| | | if (!$args['id']) $validation_errors[] = 'operation_id'; |
| | | if (!empty($validation_errors)) { |
| | | error_log('Validation Errors: '.print_r($validation_errors, true)); |
| | | return $this->createStandardResponse( |
| | | false, |
| | | ['missing_fields' => $validation_errors], |
| | | 'Required fields missing: ' . implode(', ', $validation_errors) |
| | | ); |
| | | |
| | | |
| | | if (!$args['user']) { |
| | | return $this->unauthorized(); |
| | | } |
| | | if (!$args['content']) { |
| | | return $this->validationError(['message' => 'Missing content']); |
| | | } |
| | | |
| | | $secured_files = $this->processFileUploads($_FILES['files'], $args); |
| | | // Step 1: Secure all uploaded files |
| | | $secured_files = $this->secureFiles($files, $args); |
| | | |
| | | if (empty($secured_files)) { |
| | | error_log('Images not processed'); |
| | | return $this->createStandardResponse( |
| | | false, |
| | | [], |
| | | 'No valid files could be processed' |
| | | ); |
| | | $this->logError('No valid files to upload'); |
| | | return $this->error('No valid files to upload'); |
| | | } |
| | | |
| | | // Step 2: Queue for processing via OperationQueue |
| | | $operation_id = $this->queueProcessing($secured_files, $args); |
| | | |
| | | |
| | | $queue = JVB()->queue(); |
| | | error_log('Queuing Operation...'); |
| | | $queue->queueOperation( |
| | | 'image_upload', |
| | | $args['user'], |
| | | [ |
| | | 'secured_files' => $secured_files, |
| | | 'upload_map' => explode(',',$request->get_param('upload_map')), |
| | | 'content' => $args['content'], |
| | | 'post_id' => $args['post_id'], |
| | | 'term_id' => $args['term_id'], |
| | | 'field_name' => $args['field_name'], |
| | | 'mode' => $args['mode'], |
| | | 'operation_id' => $args['id'] |
| | | ], |
| | | [ |
| | | 'operation_id' => $args['upload'], |
| | | 'chunk_key' => 'secured_files', |
| | | 'chunk_size' => 5, |
| | | 'priority' => 'high', |
| | | 'notification' => true, |
| | | ] |
| | | ); |
| | | |
| | | error_log('Adding dependent operations...'); |
| | | |
| | | |
| | | if ($args['mode'] !== 'selection') { |
| | | $queue->queueOperation( |
| | | 'attach_upload_to_content', |
| | | $args['user'], |
| | | $args, |
| | | [ |
| | | 'operation_id' => $args['id'], |
| | | 'priority' => 'high', |
| | | 'depends_on' => $args['upload'] |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | $queue->queueOperation( |
| | | 'temporary_cleanup', |
| | | $args['user'], |
| | | [ |
| | | 'files' => $secured_files, |
| | | 'content' => $args['content'], |
| | | 'user' => $args['user'] |
| | | ], |
| | | [ |
| | | 'priority' => 'low', |
| | | 'chunk_size' => 5, |
| | | 'chunk_key' => 'files', |
| | | 'depends_on' => $args['upload'], |
| | | ] |
| | | ); |
| | | |
| | | return $this->createStandardResponse( |
| | | true, |
| | | [ |
| | | 'files_count' => count($secured_files), |
| | | 'operation_id' => $args['id'], |
| | | 'upload_operation_id' => $args['upload'] |
| | | ], |
| | | 'Files secured and queued for processing' |
| | | ); |
| | | return $this->queued($operation_id, 'Files secured and queued for processing'); |
| | | |
| | | } catch (Exception $e) { |
| | | // Error handling... |
| | |
| | | 'trace' => $e->getTraceAsString() |
| | | ] |
| | | ); |
| | | |
| | | return $this->createStandardResponse( |
| | | false, |
| | | ['error_code' => 'upload_failed'], |
| | | 'Upload processing failed: ' . $e->getMessage() |
| | | ); |
| | | return $this->error($e->getMessage()); |
| | | } |
| | | } |
| | | |
| | | protected function processFileUploads(array $files, array $args): array |
| | | /** |
| | | * Secure uploaded files to temporary storage |
| | | */ |
| | | protected function secureFiles(array $files, array $args): array |
| | | { |
| | | $uploader = new UploadManager($args['content'], $args['user']); |
| | | $uploader = new UploadManager(); |
| | | $secured_files = []; |
| | | $errors = []; |
| | | |
| | | // Handle different file array structures |
| | | // if ($args['mode'] === 'selection') { |
| | | // // Selection mode: files[group][index] |
| | | // foreach ($files['tmp_name'] as $group => $images) { |
| | | // if (is_array($images)) { |
| | | // foreach ($images as $index => $tmp_name) { |
| | | // if ($files['error'][$group][$index] === UPLOAD_ERR_OK) { |
| | | // $file = [ |
| | | // 'name' => $files['name'][$group][$index], |
| | | // 'type' => $files['type'][$group][$index], |
| | | // 'tmp_name' => $tmp_name, |
| | | // 'error' => $files['error'][$group][$index], |
| | | // 'size' => $files['size'][$group][$index] |
| | | // ]; |
| | | // $secured_files[$group][] = $uploader->secureUploadedFile($file); |
| | | // } |
| | | // } |
| | | // } |
| | | // } |
| | | // } else { |
| | | // Direct mode: files[index] or files[0][index] |
| | | $tmp_names = isset($files['tmp_name'][0]) && is_array($files['tmp_name'][0]) |
| | | ? $files['tmp_name'][0] |
| | | : $files['tmp_name']; |
| | | $context = $args; |
| | | unset($context['upload_ids']); |
| | | |
| | | foreach ($tmp_names as $index => $tmp_name) { |
| | | $file_data = isset($files['tmp_name'][0]) && is_array($files['tmp_name'][0]) ? [ |
| | | 'name' => $files['name'][0][$index], |
| | | 'type' => $files['type'][0][$index], |
| | | 'tmp_name' => $tmp_name, |
| | | 'error' => $files['error'][0][$index], |
| | | 'size' => $files['size'][0][$index] |
| | | ] : [ |
| | | 'name' => $files['name'][$index], |
| | | 'type' => $files['type'][$index], |
| | | 'tmp_name' => $tmp_name, |
| | | 'error' => $files['error'][$index], |
| | | 'size' => $files['size'][$index] |
| | | ]; |
| | | $file_array = $files['files'] ?? $files; |
| | | if (!is_array($file_array['tmp_name'])) { |
| | | $file_array = [ |
| | | 'name' => [$file_array['name']], |
| | | 'type' => [$file_array['type']], |
| | | 'tmp_name' => [$file_array['tmp_name']], |
| | | 'error' => [$file_array['error']], |
| | | 'size' => [$file_array['size']] |
| | | ]; |
| | | } |
| | | $tmp_names = $file_array['tmp_name'] ?? []; |
| | | |
| | | if ($file_data['error'] === UPLOAD_ERR_OK) { |
| | | $secured_files[] = $uploader->secureUploadedFile($file_data); |
| | | } |
| | | |
| | | |
| | | foreach ($tmp_names as $index => $tmp_name) { |
| | | $file_data = [ |
| | | 'name' => $file_array['name'][$index], |
| | | 'type' => $file_array['type'][$index], |
| | | 'tmp_name' => $tmp_name, |
| | | 'error' => $file_array['error'][$index], |
| | | 'size' => $file_array['size'][$index] |
| | | ]; |
| | | |
| | | if ($file_data['error'] !== UPLOAD_ERR_OK) { |
| | | $errors[$index] = $this->getUploadErrorMessage($file_data['error']); |
| | | continue; |
| | | } |
| | | // } |
| | | |
| | | return $secured_files; |
| | | try { |
| | | $secured = $uploader->secureUploadedFile($file_data, $context); |
| | | |
| | | if (empty($secured)) { |
| | | throw new Exception('Failed to secure file'); |
| | | } |
| | | |
| | | // Embed upload_id directly in the secured file data |
| | | $secured['upload_id'] = $args['upload_ids'][$index] ?? 'upload_' . $index; |
| | | |
| | | $secured_files[] = $secured; |
| | | } catch (Exception $e) { |
| | | $errors[$index] = $e->getMessage(); |
| | | } |
| | | } |
| | | |
| | | return [ |
| | | 'files' => $secured_files, |
| | | 'errors' => $errors |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Process upload operation in the background |
| | | */ |
| | | /** |
| | | * @param WP_Error|array $result |
| | | * @param object $operation |
| | | * @param array $data |
| | | * |
| | | * @return array|WP_Error |
| | | */ |
| | | public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | | { |
| | | switch ($operation->type) { |
| | | case 'image_upload': |
| | | return $this->processImageUpload($result, $operation, $data); |
| | | case 'process_upload_groups': |
| | | return $this->processUploadGroups($result, $operation, $data); |
| | | case 'update_metadata': |
| | | return $this->processMetadataUpdate($result, $operation, $data); |
| | | case 'temporary_cleanup': |
| | | return $this->processTemporaryCleanup($result, $operation, $data); |
| | | case 'attach_upload_to_content': |
| | | return $this->attachUploadToContent($result, $operation, $data); |
| | | default: |
| | | return $result; |
| | | } |
| | | } |
| | | protected function getUploadErrorMessage(int $error_code): string |
| | | { |
| | | return match($error_code) { |
| | | UPLOAD_ERR_INI_SIZE => 'File exceeds maximum upload size', |
| | | UPLOAD_ERR_FORM_SIZE => 'File exceeds form maximum size', |
| | | UPLOAD_ERR_PARTIAL => 'File was only partially uploaded', |
| | | UPLOAD_ERR_NO_FILE => 'No file was uploaded', |
| | | UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder', |
| | | UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk', |
| | | UPLOAD_ERR_EXTENSION => 'Upload stopped by extension', |
| | | default => 'Unknown upload error' |
| | | }; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Process attachment metadata updates |
| | | * Queue files for processing |
| | | */ |
| | | protected function processAttachmentMetadata(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | protected function queueProcessing(array $secured_data, array $args): string |
| | | { |
| | | $operation_type = $this->determineOperationType($secured_data['files'][0] ?? []); |
| | | |
| | | $chunkSize = 5; |
| | | if ($operation_type === 'video') { |
| | | $chunkSize = 1; |
| | | } elseif ($operation_type === 'document') { |
| | | $chunkSize = 10; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | $operation_type, |
| | | $args['user'], |
| | | array_merge( |
| | | ['secured_files' => $secured_data['files']], |
| | | $args |
| | | ), |
| | | [ |
| | | 'operation_id' => $args['upload'], |
| | | 'chunk_key' => 'secured_files', |
| | | 'chunk_size' => $chunkSize |
| | | ] |
| | | ); |
| | | |
| | | if ($args['mode'] !== 'selection') { |
| | | $dependencies = [$args['upload']]; |
| | | if (!empty($args['depends_on'])) { |
| | | $dependencies[] = $args['depends_on']; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'attach_upload_to_content', |
| | | $args['user'], |
| | | $args, |
| | | [ |
| | | 'priority' => 'high', |
| | | 'operation_id' => $args['id'], |
| | | 'depends_on' => $dependencies |
| | | ] |
| | | ); |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'temporary_cleanup', |
| | | $args['user'], |
| | | [ |
| | | 'files' => $secured_data['files'], |
| | | 'context' => $args, |
| | | ], |
| | | [ |
| | | 'priority' => 'low', |
| | | 'chunk_size' => 5, |
| | | 'chunk_key' => 'files', |
| | | 'depends_on' => $args['upload'] |
| | | ] |
| | | ); |
| | | |
| | | return $args['id']; |
| | | } |
| | | |
| | | /** |
| | | * Determine operation type from file data |
| | | */ |
| | | protected function determineOperationType(array $file): string |
| | | { |
| | | $file_type = $file['file_type'] ?? 'image'; |
| | | |
| | | return match($file_type) { |
| | | 'video' => 'video_upload', |
| | | 'document' => 'document_upload', |
| | | default => 'image_upload' |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Step 3: Process operation from queue |
| | | * Called by OperationQueue |
| | | * @param WP_Error|array $result |
| | | * @param object $operation |
| | | * @param array $data |
| | | * |
| | | * @return array|WP_Error |
| | | */ |
| | | public function processOperation(array $result, object $operation, array $data): array |
| | | { |
| | | // Only handle our operation types |
| | | $handled_types = [ |
| | | 'image_upload', |
| | | 'video_upload', |
| | | 'document_upload', |
| | | 'update_metadata', |
| | | 'temporary_cleanup', |
| | | 'attach_upload_to_content', |
| | | 'process_upload_groups' |
| | | ]; |
| | | |
| | | if (!in_array($operation->type, $handled_types)) { |
| | | return $result; // Not our operation, pass through |
| | | } |
| | | |
| | | try { |
| | | // Route to appropriate handler |
| | | $handler_result = match($operation->type) { |
| | | 'image_upload' => $this->processImageUpload($operation, $data), |
| | | 'video_upload' => $this->processVideoUpload($operation, $data), |
| | | 'document_upload' => $this->processDocumentUpload($operation, $data), |
| | | 'update_metadata' => $this->processUploadMeta($result, $operation, $data), |
| | | 'temporary_cleanup' => $this->processTemporaryCleanup($result, $operation, $data), |
| | | 'attach_upload_to_content' => $this->processAttachToContent($operation, $data), |
| | | 'process_upload_groups' => $this->processUploadGroups($result, $operation, $data), |
| | | default => new WP_Error('unknown_type', 'Unknown operation type') |
| | | }; |
| | | |
| | | // Handle WP_Error |
| | | if (is_wp_error($handler_result)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $handler_result->get_error_message() |
| | | ]; |
| | | } |
| | | |
| | | return $handler_result; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processOperation', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'operation_type' => $operation->type |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | /** |
| | | * Standardize processing result |
| | | */ |
| | | protected function standardizeResult(array $result): array |
| | | { |
| | | return [ |
| | | 'attachment_id' => $result['attachment_id'], |
| | | 'url' => $result['url'], |
| | | 'file' => $result['file'], |
| | | 'upload_id' => $result['upload_id'] ?? null |
| | | ]; |
| | | } |
| | | |
| | | protected function processAttachToContent(object $operation, array $data): array |
| | | { |
| | | try { |
| | | $metadata_updates = $data['metadata_updates']; |
| | | $updated_count = 0; |
| | | $failed_count = 0; |
| | | $results = []; |
| | | // Get the results from the upload operation |
| | | $upload_results = JVB()->queue()->getOperationValue($data['upload'], 'result', true); |
| | | |
| | | foreach ($metadata_updates as $update) { |
| | | try { |
| | | $attachment_id = (int)$update['attachment_id']; |
| | | $upload_id = $update['upload_id']; |
| | | if (empty($upload_results)) { |
| | | throw new Exception('No upload results found for operation: ' . $data['upload']); |
| | | } |
| | | |
| | | // Verify attachment exists and user has permission |
| | | if (!$this->verifyAttachmentAccess($attachment_id, $operation->user_id)) { |
| | | $failed_count++; |
| | | $results[] = [ |
| | | 'upload_id' => $upload_id, |
| | | 'attachment_id' => $attachment_id, |
| | | 'success' => false, |
| | | 'error' => 'Attachment not found or no permission' |
| | | ]; |
| | | continue; |
| | | 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; |
| | | } |
| | | } |
| | | |
| | | // Apply metadata updates |
| | | $updated_fields = []; |
| | | |
| | | // Update alt text |
| | | if (isset($update['alt'])) { |
| | | $alt_text = sanitize_text_field($update['alt']); |
| | | update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text); |
| | | $updated_fields[] = 'alt'; |
| | | } |
| | | |
| | | // Update title |
| | | if (isset($update['title'])) { |
| | | $title = sanitize_text_field($update['title']); |
| | | wp_update_post([ |
| | | 'ID' => $attachment_id, |
| | | 'post_title' => $title |
| | | ]); |
| | | $updated_fields[] = 'title'; |
| | | } |
| | | |
| | | // Update caption |
| | | if (isset($update['caption'])) { |
| | | $caption = sanitize_textarea_field($update['caption']); |
| | | wp_update_post([ |
| | | 'ID' => $attachment_id, |
| | | 'post_excerpt' => $caption |
| | | ]); |
| | | $updated_fields[] = 'caption'; |
| | | } |
| | | |
| | | // Update description |
| | | if (isset($update['description'])) { |
| | | $description = sanitize_textarea_field($update['description']); |
| | | wp_update_post([ |
| | | 'ID' => $attachment_id, |
| | | 'post_content' => $description |
| | | ]); |
| | | $updated_fields[] = 'description'; |
| | | } |
| | | |
| | | $updated_count++; |
| | | $results[] = [ |
| | | 'upload_id' => $upload_id, |
| | | 'attachment_id' => $attachment_id, |
| | | 'success' => true, |
| | | 'updated_fields' => $updated_fields |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | $failed_count++; |
| | | $results[] = [ |
| | | 'upload_id' => $update['upload_id'] ?? 'unknown', |
| | | 'attachment_id' => $update['attachment_id'] ?? 0, |
| | | 'success' => false, |
| | | 'error' => $e->getMessage() |
| | | ]; |
| | | } |
| | | |
| | | if (empty($data['post_id'])) { |
| | | throw new Exception('Could not resolve post_id from dependencies'); |
| | | } |
| | | } |
| | | |
| | | // Now attach to the specified content |
| | | if (!empty($data['field_name'])) { |
| | | $this->updateFieldValue($data, $upload_results); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'data' => $results, |
| | | 'updated_count' => $updated_count, |
| | | 'failed_count' => $failed_count, |
| | | 'message' => "Metadata updated: {$updated_count} succeeded, {$failed_count} failed" |
| | | 'result' => 'Attachments linked to content' |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Cleanup temporary files after processing |
| | | */ |
| | | protected function cleanupTempFiles(array $secured_files, int $user_id): void |
| | | { |
| | | $uploader = new UploadManager(); |
| | | |
| | | foreach ($secured_files as $secured) { |
| | | if (!empty($secured['temp_path']) && file_exists($secured['temp_path'])) { |
| | | @unlink($secured['temp_path']); |
| | | } |
| | | } |
| | | |
| | | // Clean up empty directories |
| | | $uploader->cleanupEmptyTempDirs($user_id); |
| | | } |
| | | |
| | | /** |
| | | * Update field value with new attachment IDs |
| | | */ |
| | | protected function updateFieldValue(array $data, array $results): void |
| | | { |
| | | if ((!array_key_exists('post_id', $data) && !array_key_exists('term_id', $data) && !array_key_exists('user', $data)) && !array_key_exists('field_name', $data)) { |
| | | return; |
| | | } |
| | | |
| | | $attachment_ids = array_column($results, 'attachment_id'); |
| | | if (array_key_exists('post_id', $data)) { |
| | | $meta = Meta::forPost($data['post_id']); |
| | | } elseif (array_key_exists('term_id', $data)) { |
| | | $meta = Meta::forTerm($data['term_id']); |
| | | } else { |
| | | $link = (int)get_user_meta($data['user'], BASE.'link'); |
| | | $meta = Meta::forPost($link); |
| | | } |
| | | |
| | | // Get existing value |
| | | $existing = $meta->get($data['field_name']); |
| | | $existing_ids = !empty($existing) ? explode(',', $existing) : []; |
| | | |
| | | // Merge with new IDs |
| | | $all_ids = array_unique(array_merge($existing_ids, $attachment_ids)); |
| | | |
| | | // Update with comma-separated string |
| | | $meta->set($data['field_name'], implode(',', $all_ids)); |
| | | } |
| | | |
| | | /** |
| | | * Generic file upload processor - works for images, videos, documents |
| | | */ |
| | | protected function processFileUpload(object $operation, array $data, string $file_type): WP_Error|array |
| | | { |
| | | try { |
| | | $uploader = new UploadManager(); |
| | | $processed_results = []; |
| | | $config = $this->getFieldConfig($data); |
| | | |
| | | $args = $data; |
| | | unset($args['secured_files']); |
| | | |
| | | foreach ($data['secured_files'] as $secured_file) { |
| | | $result = $uploader->processUpload( |
| | | $secured_file['temp_path'], |
| | | array_merge( |
| | | $config, |
| | | [ |
| | | 'file_type' => $file_type, |
| | | 'user_id' => $operation->user_id, |
| | | 'post_id' => (int)($data['post_id'] ?? 0), |
| | | 'term_id' => (int)($data['term_id'] ?? 0), |
| | | 'original_name' => $secured_file['original_name'], |
| | | 'mime_type' => $secured_file['mime_type'], |
| | | 'content' => $data['content'] ?? '', |
| | | ] |
| | | ) |
| | | ); |
| | | |
| | | if (!is_wp_error($result)) { |
| | | $standardized = $this->standardizeResult($result); |
| | | |
| | | // Use embedded upload_id from the secured file itself |
| | | $standardized['upload_id'] = $secured_file['upload_id'] ?? null; |
| | | |
| | | if ($standardized['upload_id']) { |
| | | $processed_results[$standardized['upload_id']] = $standardized; |
| | | } else { |
| | | $processed_results[] = $standardized; |
| | | } |
| | | |
| | | // Apply frontend metadata if provided |
| | | if (!empty($secured_file['metadata'])) { |
| | | $this->applyMeta($standardized['attachment_id'], $secured_file['metadata']); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $this->handleUploadDestination($data, $processed_results); |
| | | |
| | | // Cleanup temporary files |
| | | $this->cleanupTempFiles($data['secured_files'], $operation->user_id); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => $processed_results |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processAttachmentMetadata', |
| | | '[UploadRoutes]:processFileUpload', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'file_type' => $file_type, |
| | | 'user_id' => $operation->user_id |
| | | ] |
| | | ); |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Process image uploads |
| | | */ |
| | | protected function processImageUpload(object $operation, array $data): WP_Error|array |
| | | { |
| | | return $this->processFileUpload($operation, $data, 'image'); |
| | | } |
| | | |
| | | /** |
| | | * Process video uploads |
| | | */ |
| | | protected function processVideoUpload(object $operation, array $data): WP_Error|array |
| | | { |
| | | return $this->processFileUpload($operation, $data, 'video'); |
| | | } |
| | | |
| | | /** |
| | | * Process document uploads |
| | | */ |
| | | protected function processDocumentUpload(object $operation, array $data): WP_Error|array |
| | | { |
| | | return $this->processFileUpload($operation, $data, 'document'); |
| | | } |
| | | |
| | | /** |
| | | * Process temporary cleanup operation |
| | | * Called by OperationQueue for 'temporary_cleanup' operations |
| | | * |
| | | * @param array $result |
| | | * @param object $operation |
| | | * @param array $data |
| | | * @return array |
| | | */ |
| | | protected function processTemporaryCleanup(array $result, object $operation, array $data): array |
| | | { |
| | | try { |
| | | // Cleanup temporary files if they exist |
| | | if (!empty($data['secured_files'])) { |
| | | $this->cleanupTempFiles($data['secured_files'], $operation->user_id); |
| | | } |
| | | |
| | | // If specific temp paths provided |
| | | if (!empty($data['temp_paths']) && is_array($data['temp_paths'])) { |
| | | foreach ($data['temp_paths'] as $temp_path) { |
| | | if (file_exists($temp_path)) { |
| | | @unlink($temp_path); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // Cleanup empty temp directories for this user |
| | | if (!empty($operation->user_id)) { |
| | | $uploader = new UploadManager(); |
| | | $uploader->cleanupEmptyTempDirs($operation->user_id); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => 'Temporary files cleaned up successfully' |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processTemporaryCleanup', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | |
| | | ] |
| | | ); |
| | | |
| | | return new WP_Error( |
| | | 'metadata_processing_failed', |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle metadata update requests |
| | | */ |
| | | public function handleMetadataUpdate(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | try { |
| | | $data = $request->get_params(); |
| | | |
| | | error_log('Received data for meta change: '.print_r($data, true)); |
| | | |
| | | |
| | | $items = $data['items']??false; |
| | | if (!$items) { |
| | | return $this->sendResponse( |
| | | true, |
| | | [ |
| | | ], |
| | | 'No items to update' |
| | | ); |
| | | } |
| | | $pending = []; |
| | | $attachments = array_filter($items, function ($item) { |
| | | return array_key_exists('attachmentId', $item) || array_key_exists('uploadId', $item); |
| | | }); |
| | | |
| | | |
| | | if (!empty($attachments)) { |
| | | error_log('Attachments: '.print_r($attachments, true)); |
| | | return $this->queueMetaUpdate($attachments, $data['user']); |
| | | } |
| | | |
| | | |
| | | return $this->sendResponse( |
| | | false, |
| | | ['error_code' => 'missing_identifiers'], |
| | | 'Must provide either attachment_ids or operation_id' |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:handleMetadataUpdate', |
| | | $e->getMessage(), |
| | | [ |
| | | 'request_data' => $request->get_params(), |
| | | 'trace' => $e->getTraceAsString() |
| | | ] |
| | | ); |
| | | |
| | | return $this->sendResponse( |
| | | false, |
| | | ['error_code' => 'metadata_update_failed'], |
| | | 'Metadata update failed: ' . $e->getMessage() |
| | | ); |
| | | } |
| | | } |
| | | /** |
| | | * Update metadata directly on completed attachments |
| | | */ |
| | | protected function updateMeta(array $data, int $user): WP_REST_Response |
| | | { |
| | | $updated_count = 0; |
| | | $errors = []; |
| | | $ids = []; |
| | | foreach ($data as $info) { |
| | | try { |
| | | $attachment_id = $info['attachmentId']; |
| | | error_log('Updating attachment ID:'.print_r($attachment_id,true)); |
| | | $ids[] = $attachment_id; |
| | | unset($info['attachmentId']); |
| | | // Verify attachment exists and user has permission |
| | | if (!$this->verifyAttachmentAccess($attachment_id, $user)) { |
| | | $errors[] = "No permission to edit attachment {$attachment_id}"; |
| | | continue; |
| | | } |
| | | |
| | | $this->applyMeta($attachment_id, $info); |
| | | $updated_count++; |
| | | |
| | | } catch (Exception $e) { |
| | | $errors[] = "Failed to update attachment {$attachment_id}: " . $e->getMessage(); |
| | | } |
| | | } |
| | | |
| | | return $this->sendResponse( |
| | | $updated_count > 0, |
| | | [ |
| | | 'updated_count' => $updated_count, |
| | | 'errors' => $errors, |
| | | 'attachment_ids' => $ids |
| | | ], |
| | | $updated_count > 0 ? |
| | | "Updated metadata for {$updated_count} attachment(s)" : |
| | | 'No attachments were updated' |
| | | ); |
| | | } |
| | | /** |
| | | * Queue metadata update with dependency on upload operation |
| | | */ |
| | | protected function queueMetaUpdate(array $data, int $user): WP_REST_Response |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $depends_on = []; |
| | | $errors = []; |
| | | $original = count($data); |
| | | foreach ($data as $uploadID => $info) { |
| | | if (array_key_exists('depends_on', $info) && !in_array($info['depends_on'], $depends_on)) { |
| | | $depends_on[] = $info['depends_on']; |
| | | } |
| | | } |
| | | $operationID = $queue->queueOperation( |
| | | 'update_image_meta', |
| | | $user, |
| | | $data, |
| | | [ |
| | | 'depends_on' => $depends_on, |
| | | ] |
| | | ); |
| | | |
| | | return $this->sendResponse( |
| | | true, |
| | | [ |
| | | 'operation_id' => $operationID, |
| | | 'message' => "Successfully queued ".count($data)." of {$original} meta updates" |
| | | ], |
| | | 'Metadata update queued - will apply after upload completes' |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Process metadata update operation (called by queue processor) |
| | | */ |
| | | public function processUploadMeta(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | { |
| | | try { |
| | | if (!is_array($operation->depends_on)) { |
| | | $operation->depends_on = [$operation->depends_on]; |
| | | } |
| | | $updated_count = 0; |
| | | $errors = []; |
| | | foreach ($operation->depends_on as $dependency) { |
| | | $operationData = JVB()->queue()->getOperation($dependency); |
| | | if (!$operationData || $operationData->state !== 'completed') { |
| | | throw new Exception('Original upload operation not found or not completed'); |
| | | } |
| | | |
| | | $uploadResults = json_decode($operationData->result, true); |
| | | if (!$uploadResults || !$uploadResults['success']) { |
| | | throw new Exception('Original upload operation failed'); |
| | | } |
| | | $attachmentsToUpdate = array_filter($data, function ($item) use ($dependency) { |
| | | return $item['depends_on'] === $dependency; |
| | | }); |
| | | |
| | | $uploadIDToAttachment = $this->buildUploadToAttachmentMapping( |
| | | array_keys($attachmentsToUpdate), |
| | | $uploadResults['data'] |
| | | ); |
| | | |
| | | if (empty($uploadIDToAttachment)) { |
| | | throw new Exception('No valid upload ID to attachment ID mappings found'); |
| | | } |
| | | |
| | | foreach ($uploadIDToAttachment as $uploadId => $attachmentId) { |
| | | try { |
| | | $this->applyMeta($attachmentId, $attachmentsToUpdate[$uploadId]); |
| | | $updated_count++; |
| | | } catch (Exception $e) { |
| | | $errors[] = "Upload {$uploadId} (Attachment {$attachmentId}): " . $e->getMessage(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | return [ |
| | | 'success' => $updated_count > 0, |
| | | 'result' => [ |
| | | 'updated_count' => $updated_count, |
| | | 'mappings' => $uploadIDToAttachment, |
| | | 'errors' => $errors, |
| | | 'message' => "Applied metadata to {$updated_count} attachment(s)" |
| | | ] |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processUploadMeta', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'error_code' => 'metadata_update_error' |
| | | 'original_operation_id' => $data['original_operation_id'] ?? 'unknown' |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | protected function applyMeta(int $attachment_id, array $metadata): void |
| | | { |
| | | // Update alt text |
| | | if (!empty($metadata['image-alt-text'])) { |
| | | update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($metadata['image-alt-text'])); |
| | | } |
| | | $postUpdates = []; |
| | | // Update title |
| | | if (!empty($metadata['image-title'])) { |
| | | $postUpdates['post_title'] = $metadata['image-title']; |
| | | } |
| | | |
| | | // Update caption |
| | | if (!empty($metadata['image-caption'])) { |
| | | $postUpdates['post_excerpt'] = sanitize_textarea_field($metadata['image-caption']); |
| | | } |
| | | |
| | | if (!empty($postUpdates)) { |
| | | $postUpdates['ID'] = $attachment_id; |
| | | wp_update_post($postUpdates); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Build mapping from frontend upload IDs to WordPress attachment IDs |
| | | */ |
| | | protected function buildUploadToAttachmentMapping(array $upload_ids, array $results): array |
| | | { |
| | | $mapping = []; |
| | | |
| | | foreach ($results as $result) { |
| | | if (!isset($result['upload_id']) || !isset($result['attachment_id'])) { |
| | | continue; |
| | | } |
| | | |
| | | $upload_id = $result['upload_id']; |
| | | $attachment_id = $result['attachment_id']; |
| | | |
| | | // Validate that this upload_id was requested |
| | | if (in_array($upload_id, $upload_ids)) { |
| | | $mapping[$upload_id] = $attachment_id; |
| | | } |
| | | } |
| | | |
| | | return $mapping; |
| | | } |
| | | |
| | | /** |
| | | * Verify user has access to attachment |
| | | */ |
| | |
| | | return ($attachment->post_author == $user_id) || current_user_can('manage_options'); |
| | | } |
| | | |
| | | protected function processImageUpload(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | { |
| | | try { |
| | | $uploader = new UploadManager($data['content'], $operation->user_id); |
| | | $processed_results = []; |
| | | |
| | | // Extract frontend metadata if available |
| | | $frontend_metadata = $this->extractFrontendMetadata($data); |
| | | |
| | | |
| | | foreach ($data['secured_files'] as $index => $secured_file) { |
| | | $result = $uploader->processImageFromStorage( |
| | | $secured_file['temp_path'], |
| | | $data['content'], |
| | | (int)($data['post_id'] ?? 0), |
| | | (int)($data['term_id'] ?? 0), |
| | | $secured_file['original_name'] ?? '' |
| | | ); |
| | | |
| | | if (!is_wp_error($result)) { |
| | | $standardized = $this->standardizeImageResult($result); |
| | | |
| | | // FIX: Proper mapping using consistent naming |
| | | if (isset($data['upload_map'][$index])) { |
| | | $standardized['upload_id'] = $data['upload_map'][$index]; |
| | | } else { |
| | | error_log("Warning: No upload_map found for index {$index}"); |
| | | $standardized['upload_id'] = 'unknown_' . $index; |
| | | } |
| | | |
| | | $file_metadata = $frontend_metadata[$index] ?? null; |
| | | if ($file_metadata) { |
| | | $this->applyFrontendMetadata($standardized['attachment_id'], $file_metadata); |
| | | } |
| | | |
| | | $processed_results[] = $standardized; |
| | | } |
| | | } |
| | | |
| | | // Update field metadata if specified |
| | | if ($data['field_name']) { |
| | | $this->updateFieldMetadata($data, $processed_results); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => $processed_results |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processImageUpload', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'user_id' => $operation->user_id, |
| | | 'content' => $data['content'], |
| | | 'upload_map' => $data['upload_map'] ?? 'missing' |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | protected function extractFrontendMetadata(array $data): array |
| | | { |
| | | $metadata = []; |
| | | |
| | | // Check if we have metadata in the request |
| | | if (isset($_POST['metadata'])) { |
| | | foreach ($_POST['metadata'] as $index => $meta_json) { |
| | | $decoded = json_decode($meta_json, true); |
| | | if ($decoded) { |
| | | $metadata[$index] = $decoded; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return $metadata; |
| | | } |
| | | |
| | | protected function applyFrontendMetadata(int $attachment_id, array $metadata): void |
| | | { |
| | | // Update alt text |
| | | if (!empty($metadata['alt'])) { |
| | | update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($metadata['alt'])); |
| | | } |
| | | |
| | | // Update title |
| | | if (!empty($metadata['title'])) { |
| | | wp_update_post([ |
| | | 'ID' => $attachment_id, |
| | | 'post_title' => sanitize_text_field($metadata['title']) |
| | | ]); |
| | | } |
| | | |
| | | // Update caption |
| | | if (!empty($metadata['caption'])) { |
| | | wp_update_post([ |
| | | 'ID' => $attachment_id, |
| | | 'post_excerpt' => sanitize_textarea_field($metadata['caption']) |
| | | ]); |
| | | } |
| | | } |
| | | protected function countProcessedFiles(array $processed_results): int |
| | | { |
| | | $count = 0; |
| | | foreach ($processed_results as $result) { |
| | | if (is_array($result) && isset($result[0])) { |
| | | // Grouped results |
| | | $count += count($result); |
| | | } else { |
| | | // Single result |
| | | $count++; |
| | | } |
| | | } |
| | | return $count; |
| | | } |
| | | protected function updateFieldMetadata(array $data, array $processed_results): void |
| | | { |
| | | if (!$data['field_name']) return; |
| | | |
| | | $type = ($data['post_id']) ? 'post' : (($data['term_id']) ? 'term' : false); |
| | | if (!$type) return; |
| | | |
| | | $ID = ($type == 'post') ? $data['post_id'] : $data['term_id']; |
| | | $meta = new MetaManager($ID, $type); |
| | | |
| | | // Get attachment IDs from processed results |
| | | $attachment_ids = []; |
| | | if ($data['mode'] == 'selection') { |
| | | // Selection mode: grouped results |
| | | foreach ($processed_results as $group => $files) { |
| | | foreach ($files as $file_result) { |
| | | $attachment_ids[] = $file_result['attachment_id']; |
| | | } |
| | | } |
| | | } else { |
| | | // Direct mode: flat array |
| | | foreach ($processed_results as $file_result) { |
| | | $attachment_ids[] = $file_result['attachment_id']; |
| | | } |
| | | } |
| | | |
| | | if (!empty($attachment_ids)) { |
| | | $value = implode(',', array_map('absint', $attachment_ids)); |
| | | $meta->updateValue($data['field_name'], $value); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Standardize image processing results for frontend |
| | | * Get field configuration for upload processing |
| | | */ |
| | | protected function standardizeImageResult(array $result): array |
| | | protected function getFieldConfig(array $data): array |
| | | { |
| | | return [ |
| | | 'attachment_id' => $result['attachment_id'], |
| | | 'url' => $result['url'], |
| | | 'file_path' => $result['file'] ?? '', |
| | | 'success' => $result['success'] ?? true, |
| | | 'metadata' => [ |
| | | 'alt_text' => get_post_meta($result['attachment_id'], '_wp_attachment_image_alt', true), |
| | | 'title' => get_the_title($result['attachment_id']), |
| | | 'mime_type' => get_post_mime_type($result['attachment_id']) |
| | | ] |
| | | static $config_cache = []; |
| | | |
| | | $cache_key = md5(json_encode([ |
| | | $args['content'] ?? '', |
| | | $args['field_name'] ?? '' |
| | | ])); |
| | | |
| | | if (isset($config_cache[$cache_key])) { |
| | | return $config_cache[$cache_key]; |
| | | } |
| | | |
| | | $config = [ |
| | | 'allowed_types' => null, |
| | | 'max_size' => null, |
| | | 'convert' => 'webp', |
| | | 'quality' => 80, |
| | | 'create_thumbnails' => true, |
| | | ]; |
| | | |
| | | // Get field definition from registry if available |
| | | if (!empty($args['content']) && !empty($args['field_name'])) { |
| | | $content_type = $args['content']; |
| | | $field_name = $args['field_name']; |
| | | |
| | | if (array_key_exists($content_type, JVB_CONTENT)) { |
| | | $content_fields = JVB_CONTENT[$content_type]['fields'] ?? []; |
| | | if (array_key_exists($field_name, $content_fields)) { |
| | | $field_def = $content_fields[$field_name]; |
| | | |
| | | // Extract relevant config from field definition |
| | | $config = array_merge($config, [ |
| | | 'allowed_types' => $field_def['accepted_types'] ?? null, |
| | | 'max_size' => $field_def['max_size'] ?? null, |
| | | 'convert' => $field_def['convert'] ?? 'webp', |
| | | 'quality' => $field_def['quality'] ?? 80, |
| | | 'create_thumbnails' => $field_def['create_thumbnails'] ?? true, |
| | | ]); |
| | | } |
| | | } |
| | | } |
| | | |
| | | $config_cache[$cache_key] = $config; |
| | | return $config; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Get files info for error logging |
| | | */ |
| | |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * @param WP_Error $result |
| | | * @param object $operation |
| | | * @param array $data |
| | | * |
| | | * @return array|WP_Error |
| | | */ |
| | | public function processTemporaryCleanup(WP_Error|array $result, object $operation, array $data):WP_Error|array |
| | | { |
| | | error_log('Processing Temporary Cleanup...'); |
| | | $JVB = JVB(); |
| | | $logger = $JVB->error(); |
| | | |
| | | $uploader = new UploadManager($data['content'], $data['user']); |
| | | |
| | | // Check if we have files to clean up |
| | | if (empty($data['files']) || !is_array($data['files'])) { |
| | | return ['success' => true, 'result' => 'No files to clean up']; |
| | | } |
| | | |
| | | $success_count = 0; |
| | | $error_count = 0; |
| | | $skipped_count = 0; |
| | | |
| | | foreach ($data['files'] as $file_data) { |
| | | // Skip if we don't have a temp path |
| | | if (empty($file_data['temp_path'])) { |
| | | $skipped_count++; |
| | | continue; |
| | | } |
| | | |
| | | $temp_path = $file_data['temp_path']; |
| | | |
| | | try { |
| | | // Only remove if the file exists |
| | | if (file_exists($temp_path)) { |
| | | if (@unlink($temp_path)) { |
| | | $success_count++; |
| | | } else { |
| | | $error_count++; |
| | | $logger->log( |
| | | 'temp_cleanup', |
| | | 'Failed to delete temporary file', |
| | | ['file' => $temp_path], |
| | | 'warning' |
| | | ); |
| | | } |
| | | } else { |
| | | // File already gone, count as success |
| | | $skipped_count++; |
| | | } |
| | | } catch (Exception $e) { |
| | | $error_count++; |
| | | $logger->log( |
| | | '[UploadManager]:processTempCleanupOperation', |
| | | 'Error during temp file cleanup: ' . $e->getMessage(), |
| | | ['file' => $temp_path], |
| | | 'warning' |
| | | ); |
| | | } |
| | | } |
| | | |
| | | // After cleaning individual files, check if directory is empty and can be removed |
| | | $uploader->cleanupEmptyTempDirs($operation->user_id); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'removed' => $success_count, |
| | | 'errors' => $error_count, |
| | | 'skipped' => $skipped_count, |
| | | 'message' => "Temporary files cleanup completed: {$success_count} removed, {$error_count} errors, {$skipped_count} skipped" |
| | | ] |
| | | ]; |
| | | } |
| | | |
| | | protected function attachUploadToContent($result, $operation, $args):array|false |
| | | { |
| | | error_log('STARTING ATTACHING UPLOAD'); |
| | | error_log('Upload ID: '.print_r($args['upload'], true)); |
| | | $uploadOperation = JVB()->queue()->getOperation($args['upload'], true); |
| | | error_log('Upload Operation from Attach Image to Content: '.print_r($uploadOperation, true)); |
| | | error_log('Args: '.print_r($args, true)); |
| | | |
| | | |
| | | $field = ($args['field_name']??false) ? $args['field_name'] : 'post_thumbnail'; |
| | | $ID = $args['post_id']??$args['term_id']; |
| | | $return = [ |
| | | 'success' => false, |
| | | 'results' => [] |
| | | ]; |
| | | $images = array_map('absint', array_column(json_decode($uploadOperation->result, true),'attachment_id')); |
| | | error_log('Images: '.print_r($images, true)); |
| | | error_log('Parsed ID: '.print_r($ID, true)); |
| | | if (array_key_exists('content', $args) && $args['content'] === 'options') { |
| | | $meta = new MetaManager(null, 'options'); |
| | | if (str_contains($args['field_name'], ':')) { |
| | | $result = $meta->updateRepeaterRowField($args['field_name'], (count($images) === 1) ? $images[0] : $images); |
| | | } else { |
| | | $result = $meta->updateValue($args['field_name'], (count($images) === 1) ? $images[0] : $images); |
| | | } |
| | | $return = [ |
| | | 'success' => $result, |
| | | ]; |
| | | } elseif (str_starts_with($field, 'new_') && !$ID) { |
| | | //Create post |
| | | $success = [ |
| | | 'created' => [], |
| | | 'errors' => [] |
| | | ]; |
| | | $i = 1; |
| | | foreach ($images as $img) { |
| | | $ID = wp_insert_post([ |
| | | 'post_type' => jvbCheckBase($args['content']), |
| | | 'post_author' => $operation->user_id, |
| | | 'post_title' => 'New '.JVB_CONTENT[$args['content']]['singular'].' '.$i, |
| | | 'post_content' => ' ', |
| | | 'post_excerpt' => ' ' |
| | | ], true); |
| | | |
| | | error_log('Created Post: '.print_r($ID, true)); |
| | | |
| | | if ($ID && !is_wp_error($ID)) { |
| | | $success['created'][] = $ID; |
| | | set_post_thumbnail($ID, $img); |
| | | }else { |
| | | $success['errors'][] = $ID; |
| | | } |
| | | $i++; |
| | | } |
| | | $return = [ |
| | | 'success' => true, |
| | | 'result' => $success |
| | | ]; |
| | | } else { |
| | | if ($args['post_id']) { |
| | | $type = 'post'; |
| | | } elseif ($args['term_id']) { |
| | | $type = 'term'; |
| | | } |
| | | error_log('ID: '.print_r($ID, true)); |
| | | error_log('Type: '.print_r($type, true)); |
| | | $meta = new MetaManager($ID, $type); |
| | | |
| | | $images = implode(',', $images); |
| | | error_log('Processed Image IDs: '.$images); |
| | | $success = $meta->updateValue($field, $images); |
| | | |
| | | $return = [ |
| | | 'success' => $success, |
| | | 'result' => 'Field '.$field.' updated successfully' |
| | | ]; |
| | | } |
| | | |
| | | return $return; |
| | | } |
| | | |
| | | protected function createStandardResponse(bool $success, array $data = [], string $message = '', string $operation_id = ''): WP_REST_Response |
| | | protected function sendResponse(bool $success, array $data = [], string $message = '', string $operation_id = ''): WP_REST_Response |
| | | { |
| | | $response = [ |
| | | 'success' => $success, |
| | |
| | | $response['operation_id'] = $operation_id; |
| | | } |
| | | |
| | | return new WP_REST_Response($response); |
| | | return $this->success($response); |
| | | } |
| | | |
| | | /** |
| | | * Handle metadata update requests |
| | | */ |
| | | public function handleMetadataUpdate(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | try { |
| | | $data = $request->get_params(); |
| | | |
| | | // Validate user permissions |
| | | if (!$this->userCheck($data['user'])) { |
| | | return $this->createStandardResponse( |
| | | false, |
| | | ['error_code' => 'invalid_user'], |
| | | 'Invalid user specified' |
| | | ); |
| | | } |
| | | |
| | | if (!empty($data['attachment_ids'])) { |
| | | // Phase 2B: Direct attachment update (images already processed) |
| | | return $this->updateAttachmentMetadataDirect($data); |
| | | } |
| | | elseif (!empty($data['operation_id'])) { |
| | | // Phase 2A: Queue metadata update with dependency on upload operation |
| | | return $this->queueMetadataUpdate($data); |
| | | } |
| | | |
| | | return $this->createStandardResponse( |
| | | false, |
| | | ['error_code' => 'missing_identifiers'], |
| | | 'Must provide either attachment_ids or operation_id' |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:handleMetadataUpdate', |
| | | $e->getMessage(), |
| | | [ |
| | | 'request_data' => $request->get_params(), |
| | | 'trace' => $e->getTraceAsString() |
| | | ] |
| | | ); |
| | | |
| | | return $this->createStandardResponse( |
| | | false, |
| | | ['error_code' => 'metadata_update_failed'], |
| | | 'Metadata update failed: ' . $e->getMessage() |
| | | ); |
| | | } |
| | | } |
| | | /** |
| | | * Update metadata directly on completed attachments |
| | | */ |
| | | protected function updateAttachmentMetadataDirect(array $data): WP_REST_Response |
| | | { |
| | | $updated_count = 0; |
| | | $errors = []; |
| | | |
| | | foreach ($data['attachment_ids'] as $attachment_id) { |
| | | try { |
| | | // Verify attachment exists and user has permission |
| | | if (!$this->canUserEditAttachment($data['user'], $attachment_id)) { |
| | | $errors[] = "No permission to edit attachment {$attachment_id}"; |
| | | continue; |
| | | } |
| | | |
| | | $this->applyMetadataToAttachment($attachment_id, $data['metadata']); |
| | | $updated_count++; |
| | | |
| | | } catch (Exception $e) { |
| | | $errors[] = "Failed to update attachment {$attachment_id}: " . $e->getMessage(); |
| | | } |
| | | } |
| | | |
| | | return $this->createStandardResponse( |
| | | $updated_count > 0, |
| | | [ |
| | | 'updated_count' => $updated_count, |
| | | 'errors' => $errors, |
| | | 'attachment_ids' => $data['attachment_ids'] |
| | | ], |
| | | $updated_count > 0 ? |
| | | "Updated metadata for {$updated_count} attachment(s)" : |
| | | 'No attachments were updated' |
| | | ); |
| | | } |
| | | /** |
| | | * Queue metadata update with dependency on upload operation |
| | | */ |
| | | protected function queueMetadataUpdate(array $data): WP_REST_Response |
| | | { |
| | | $queue = JVB()->queue(); |
| | | |
| | | // Generate unique operation ID for this metadata update |
| | | $metadata_operation_id = 'meta_' . uniqid(); |
| | | |
| | | $queue->queueOperation( |
| | | 'update_metadata', |
| | | $data['user'], |
| | | [ |
| | | 'original_operation_id' => $data['operation_id'], |
| | | 'upload_ids' => $data['upload_ids'] ?? [], |
| | | 'metadata' => $data['metadata'] |
| | | ], |
| | | [ |
| | | 'operation_id' => $metadata_operation_id, |
| | | 'depends_on' => $data['operation_id'], // Wait for upload completion |
| | | 'priority' => 'medium', |
| | | 'notification' => false // Don't spam notifications for metadata updates |
| | | ] |
| | | ); |
| | | |
| | | return $this->createStandardResponse( |
| | | true, |
| | | [ |
| | | 'metadata_operation_id' => $metadata_operation_id, |
| | | 'depends_on' => $data['operation_id'], |
| | | 'upload_ids' => $data['upload_ids'] ?? [] |
| | | ], |
| | | 'Metadata update queued - will apply after upload completes' |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * Process metadata update operation (called by queue processor) |
| | | */ |
| | | public function processMetadataUpdate(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | { |
| | | try { |
| | | // Get the completed upload operation to map upload IDs to attachment IDs |
| | | $uploadOperation = JVB()->queue()->getOperation($data['original_operation_id']); |
| | | |
| | | if (!$uploadOperation || $uploadOperation->status !== 'completed') { |
| | | throw new Exception('Original upload operation not found or not completed'); |
| | | } |
| | | |
| | | $uploadResults = json_decode($uploadOperation->result, true); |
| | | if (!$uploadResults || !$uploadResults['success']) { |
| | | throw new Exception('Original upload operation failed'); |
| | | } |
| | | |
| | | // Build mapping from upload IDs to attachment IDs |
| | | $uploadIdToAttachment = $this->buildUploadToAttachmentMapping( |
| | | $data['upload_ids'], |
| | | $uploadResults['data'] |
| | | ); |
| | | |
| | | if (empty($uploadIdToAttachment)) { |
| | | throw new Exception('No valid upload ID to attachment ID mappings found'); |
| | | } |
| | | |
| | | // Apply metadata to each mapped attachment |
| | | $updated_count = 0; |
| | | $errors = []; |
| | | |
| | | foreach ($uploadIdToAttachment as $uploadId => $attachmentId) { |
| | | try { |
| | | $this->applyMetadataToAttachment($attachmentId, $data['metadata']); |
| | | $updated_count++; |
| | | } catch (Exception $e) { |
| | | $errors[] = "Upload {$uploadId} (Attachment {$attachmentId}): " . $e->getMessage(); |
| | | } |
| | | } |
| | | |
| | | return [ |
| | | 'success' => $updated_count > 0, |
| | | 'result' => [ |
| | | 'updated_count' => $updated_count, |
| | | 'mappings' => $uploadIdToAttachment, |
| | | 'errors' => $errors, |
| | | 'message' => "Applied metadata to {$updated_count} attachment(s)" |
| | | ] |
| | | ]; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processMetadataUpdate', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'original_operation_id' => $data['original_operation_id'] ?? 'unknown' |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Build mapping from frontend upload IDs to WordPress attachment IDs |
| | | */ |
| | | protected function buildUploadToAttachmentMapping(array $uploadIds, array $uploadResults): array |
| | | { |
| | | $mapping = []; |
| | | |
| | | // Handle both flat array and grouped results |
| | | if (isset($uploadResults[0]) && is_array($uploadResults[0]) && isset($uploadResults[0]['attachment_id'])) { |
| | | // Flat array of results |
| | | foreach ($uploadResults as $index => $result) { |
| | | if (isset($uploadIds[$index]) && isset($result['attachment_id'])) { |
| | | $mapping[$uploadIds[$index]] = $result['attachment_id']; |
| | | } |
| | | } |
| | | } else { |
| | | // Grouped results (selection mode) |
| | | $resultIndex = 0; |
| | | foreach ($uploadResults as $group) { |
| | | if (is_array($group)) { |
| | | foreach ($group as $result) { |
| | | if (isset($uploadIds[$resultIndex]) && isset($result['attachment_id'])) { |
| | | $mapping[$uploadIds[$resultIndex]] = $result['attachment_id']; |
| | | } |
| | | $resultIndex++; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | return $mapping; |
| | | } |
| | | |
| | | /** |
| | | * Apply metadata to a WordPress attachment |
| | | */ |
| | | protected function applyMetadataToAttachment(int $attachment_id, array $metadata): void |
| | | { |
| | | $updates = []; |
| | | |
| | | // Handle title update |
| | | if (isset($metadata['title']) && !empty($metadata['title'])) { |
| | | $updates['post_title'] = sanitize_text_field($metadata['title']); |
| | | } |
| | | |
| | | // Handle caption update |
| | | if (isset($metadata['caption'])) { |
| | | $updates['post_excerpt'] = sanitize_textarea_field($metadata['caption']); |
| | | } |
| | | |
| | | // Update post if we have title or caption changes |
| | | if (!empty($updates)) { |
| | | $updates['ID'] = $attachment_id; |
| | | $result = wp_update_post($updates); |
| | | |
| | | if (is_wp_error($result)) { |
| | | throw new Exception('Failed to update attachment post: ' . $result->get_error_message()); |
| | | } |
| | | } |
| | | |
| | | // Handle alt text update (stored as post meta) |
| | | if (isset($metadata['alt'])) { |
| | | $alt_result = update_post_meta( |
| | | $attachment_id, |
| | | '_wp_attachment_image_alt', |
| | | sanitize_text_field($metadata['alt']) |
| | | ); |
| | | |
| | | if ($alt_result === false) { |
| | | throw new Exception('Failed to update alt text meta'); |
| | | } |
| | | } |
| | | |
| | | // Handle any custom metadata fields |
| | | if (isset($metadata['custom']) && is_array($metadata['custom'])) { |
| | | foreach ($metadata['custom'] as $key => $value) { |
| | | $meta_key = 'jvb_' . sanitize_key($key); |
| | | update_post_meta($attachment_id, $meta_key, sanitize_text_field($value)); |
| | | } |
| | | } |
| | | |
| | | // Clear any attachment caches |
| | | clean_attachment_cache($attachment_id); |
| | | } |
| | | |
| | | /** |
| | | * Check if user can edit attachment |
| | | */ |
| | | protected function canUserEditAttachment(int $user_id, int $attachment_id): bool |
| | | { |
| | | // Basic permission check - attachment must exist |
| | | $attachment = get_post($attachment_id); |
| | | if (!$attachment || $attachment->post_type !== 'attachment') { |
| | | return false; |
| | | } |
| | | |
| | | // User must be the author or have edit capabilities |
| | | if ($attachment->post_author == $user_id) { |
| | | return true; |
| | | } |
| | | |
| | | // Check if user has edit_posts capability |
| | | $user = get_user_by('id', $user_id); |
| | | return $user && user_can($user, 'skip_moderation'); |
| | | } |
| | | |
| | | public function handleGroupingRequest(WP_REST_Request $request): WP_REST_Response |
| | | { |
| | | error_log('Handling Group Request from UploadRoutes.php'); |
| | | try { |
| | | $data = $request->get_params(); |
| | | error_log('Processing Data: '.print_r($data, true)); |
| | | $files = $request->get_file_params(); |
| | | $args = $this->buildUploadArgs($request); |
| | | |
| | | if (!array_key_exists($data['content'], JVB_CONTENT)) { |
| | | return $this->createStandardResponse( |
| | | false, |
| | | ['error_code' => 'invalid_content'], |
| | | 'Invalid content type specified' |
| | | ); |
| | | if (!array_key_exists('user', $args) || $args['user'] === 0){ |
| | | return $this->unauthorized(); |
| | | } |
| | | if (!array_key_exists('content', $args) || empty($args['content'])) { |
| | | return $this->validationError(['message'=>'Missing required content']); |
| | | } |
| | | if (!array_key_exists('posts', $args) || empty($args['posts'])) { |
| | | return $this->validationError(['message' => 'Missing posts required']); |
| | | } |
| | | |
| | | // Secure files to temporary storage |
| | | $secured_files = $this->secureFiles($files, $args); |
| | | |
| | | //Get the operationIDs of the image uploads for the depends on |
| | | $operationIDs = []; |
| | | foreach ($data['posts'] as $index => $post) { |
| | | foreach ($post['images'] as $img) { |
| | | if (array_key_exists('operationId', $img)) { |
| | | $operationIDs[] = $img['operationId']; |
| | | } |
| | | } |
| | | if (empty($secured_files['files'])) { |
| | | return $this->error('No valid files to upload'); |
| | | } |
| | | $operationIDs = array_unique($operationIDs); |
| | | |
| | | // Queue file upload operation |
| | | $operation_type = $this->determineOperationType($secured_files['files'][0] ?? []); |
| | | $chunkSize = 5; |
| | | if ($operation_type === 'video') { |
| | | $chunkSize = 1; |
| | | } elseif ($operation_type === 'document') { |
| | | $chunkSize = 10; |
| | | } |
| | | |
| | | $queue = JVB()->queue(); |
| | | $queue->queueOperation( |
| | | 'process_upload_groups', |
| | | $data['user'], |
| | | $data, |
| | | JVB()->queue()->queueOperation( |
| | | $operation_type, |
| | | $args['user'], |
| | | array_merge( |
| | | ['secured_files' => $secured_files['files']], |
| | | $args |
| | | ), |
| | | [ |
| | | 'operation_id' => $data['id'], |
| | | 'depends_on' => $operationIDs |
| | | 'operation_id' => $args['upload'], |
| | | 'chunk_key' => 'secured_files', |
| | | 'chunk_size' => $chunkSize |
| | | ] |
| | | ); |
| | | |
| | | return $this->createStandardResponse( |
| | | true, |
| | | $ID = JVB()->queue()->queueOperation( |
| | | 'process_upload_groups', |
| | | $args['user'], |
| | | $args, |
| | | [ |
| | | 'operation_id' => $data['id'], |
| | | 'count' => count($data['posts'] ?? []) |
| | | ], |
| | | 'Operation queued successfully' |
| | | 'operation_id' => $args['id'], |
| | | 'depends_on' => [$args['upload']], |
| | | 'priority' => 'high', |
| | | 'chunk_key' => 'posts' |
| | | ] |
| | | ); |
| | | |
| | | return $this->queued($ID['operation_id'], 'Files uploaded and posts queued for creation'); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:handleGroupingRequest', |
| | |
| | | 'trace' => $e->getTraceAsString() |
| | | ] |
| | | ); |
| | | |
| | | return $this->createStandardResponse( |
| | | false, |
| | | ['error_code' => 'grouping_failed'], |
| | | 'Grouping operation failed: ' . $e->getMessage() |
| | | ); |
| | | return $this->error('Grouping operation failed: '.$e->getMessage()); |
| | | } |
| | | } |
| | | |
| | | protected function processUploadGroups(WP_Error|array $result, object $operation, array $data): WP_Error|array |
| | | { |
| | | try { |
| | | error_log('Processing Upload Groups - Data: ' . print_r($data, true)); |
| | | |
| | | $queue = JVB()->queue(); |
| | | $all_uploaded_images = []; |
| | | $used_upload_ids = []; |
| | | |
| | | // Collect all uploaded images from dependencies |
| | | foreach(json_decode($operation->dependencies, true) as $operationID) { |
| | | $o = $queue->getOperation($operationID); |
| | | $r = json_decode($o->result, true); |
| | | // Get the upload operation ID from dependencies |
| | | $dependencies = json_decode($operation->dependencies, true); |
| | | |
| | | error_log('Saved operation'.print_r($r, true)); |
| | | if ($r['success'] && isset($r['data'])) { |
| | | foreach ($r['data'] as $img) { |
| | | $uploadKey = $img['upload_id']; |
| | | $all_uploaded_images[$uploadKey] = [ |
| | | 'upload_id' => $img['upload_id'], |
| | | 'attachment_id' => (int)$img['attachment_id'], |
| | | 'operation_id' => $operationID |
| | | ]; |
| | | if (empty($dependencies)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => 'No dependencies found' |
| | | ]; |
| | | } |
| | | |
| | | // Collect uploads from all dependency operations |
| | | $uploads = []; |
| | | foreach ($dependencies as $dependency) { |
| | | // Use getOperationValue like ContentRoutes does |
| | | $res = $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)) { |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => 'No uploads to process' |
| | | ]; |
| | | } |
| | | |
| | | // Build map of upload_id => attachment_id |
| | | foreach ($uploads as $upload_id => $img) { |
| | | $all_uploaded_images[$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($data, $uploads, $all_uploaded_images, $operation); |
| | | } |
| | | |
| | | $user = (int)$data['user']; |
| | | $created_posts = []; |
| | | $used_upload_ids = []; |
| | | |
| | | // Create posts from groups |
| | | foreach ($data['posts'] as $index => $post) { |
| | | $new_post_id = wp_insert_post([ |
| | | $post_title = !empty($post['fields']['post_title']) |
| | | ? sanitize_text_field($post['fields']['post_title']) |
| | | : 'New ' . JVB_CONTENT[$data['content']]['singular'] . ' ' . ($index + 1); |
| | | |
| | | $post_excerpt = !empty($post['fields']['post_excerpt']) |
| | | ? sanitize_textarea_field($post['fields']['post_excerpt']) |
| | | : ''; |
| | | |
| | | $args = [ |
| | | 'post_type' => $content, |
| | | 'post_author' => $user, |
| | | 'post_status' => 'draft', |
| | | 'post_title' => array_key_exists('post_title', $post['fields']) |
| | | ? sanitize_text_field($post['fields']['post_title']) |
| | | : 'New ' . JVB_CONTENT[$data['content']]['singular'], |
| | | 'post_excerpt' => array_key_exists('post_excerpt', $post['fields']) |
| | | ? sanitize_text_field($post['fields']['post_excerpt']) |
| | | : '', |
| | | ]); |
| | | 'post_title' => $post_title, |
| | | 'post_excerpt' => $post_excerpt, |
| | | ]; |
| | | |
| | | $new_post_id = wp_insert_post($args); |
| | | |
| | | if ($new_post_id && !is_wp_error($new_post_id)) { |
| | | $created_posts[] = $new_post_id; |
| | | |
| | | // Extract featured image |
| | | $featured_upload_id = array_key_exists('featured', $post['fields']) |
| | | ? $post['fields']['featured'] |
| | | : $post['images'][0]['upload_id'] ?? null; |
| | | |
| | | // Get featured image upload_id - string, not int! |
| | | $featured_upload_id = $post['fields']['featured'] ?? null; |
| | | $featured_attachment_id = null; |
| | | $gallery_attachment_ids = []; |
| | | |
| | |
| | | } else { |
| | | $gallery_attachment_ids[] = $attachment_id; |
| | | } |
| | | } else { |
| | | error_log("Warning: Could not find attachment for upload_id: {$upload_id}"); |
| | | } |
| | | } |
| | | |
| | | // Set featured image |
| | | if ($featured_attachment_id) { |
| | | set_post_thumbnail($new_post_id, (int)$featured_attachment_id); |
| | | set_post_thumbnail($new_post_id, $featured_attachment_id); |
| | | } elseif (!empty($gallery_attachment_ids)) { |
| | | set_post_thumbnail($new_post_id, $gallery_attachment_ids[0]); |
| | | array_shift($gallery_attachment_ids); |
| | | } |
| | | |
| | | // Set gallery images |
| | | if (!empty($gallery_attachment_ids)) { |
| | | $meta = new MetaManager($new_post_id, 'post'); |
| | | $fields = jvbGetFields($data['content'], 'post'); |
| | | $meta = Meta::forPost($new_post_id); |
| | | $fields = jvbGetFields($content, 'post'); |
| | | |
| | | foreach ($fields as $name => $config) { |
| | | if ($config['type'] === 'gallery') { |
| | | $meta->updateValue($name, implode(',', $gallery_attachment_ids)); |
| | | $meta->set($name, implode(',', $gallery_attachment_ids)); |
| | | break; |
| | | } |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | // Cleanup unused uploaded images |
| | | $unused_images = array_diff_key($all_uploaded_images, array_flip($used_upload_ids)); |
| | | $cleanup_result = $this->cleanupUnusedImages($unused_images); |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'result' => [ |
| | | 'created_posts' => $created_posts, |
| | | 'total_posts' => count($created_posts), |
| | | 'used_images' => count($used_upload_ids), |
| | | 'cleaned_up_images' => $cleanup_result['cleaned_count'], |
| | | 'cleanup_errors' => $cleanup_result['errors'], |
| | | 'message' => "Created " . count($created_posts) . " post(s) from uploads" |
| | | ] |
| | | ]; |
| | |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'user_id' => $operation->user_id, |
| | | 'posts_count' => count($data['posts'] ?? []) |
| | | 'user_id' => $operation->user_id |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | protected function processTimelineUploads(array $data, array $uploads, array $uploadMap, object $operation):array |
| | | { |
| | | try { |
| | | $user = (int)$data['user']; |
| | | $created_posts = []; |
| | | $used_upload_ids = []; |
| | | |
| | | $content = jvbCheckBase($data['content']); |
| | | |
| | | $config = Features::getConfig($content); |
| | | |
| | | $defaultTitle = 'New '.$config['singular']. ' '; |
| | | foreach ($data['posts'] as $index=> $post) { |
| | | $title = !empty($post['fields']['post_title']) |
| | | ? sanitize_text_field($post['fields']['post_title']) |
| | | : $defaultTitle.($index + 1); |
| | | $excerpt = !empty($post['fields']['post_excerpt']) |
| | | ? sanitize_textarea_field($post['fields']['post_excerpt']) |
| | | : ''; |
| | | |
| | | $args =[ |
| | | 'post_type' => $content, |
| | | 'post_author' => $user, |
| | | 'post_status' => 'draft', |
| | | 'post_title' => $title, |
| | | 'post_excerpt' => $excerpt |
| | | ]; |
| | | $parent = wp_insert_post($args); |
| | | |
| | | if ($parent && !is_wp_error($parent)) { |
| | | //Get the attachment IDs first |
| | | $childPosts = []; |
| | | $featured = $post['fields']['featured']??null; |
| | | $featuredID = null; |
| | | foreach ($post['images'] as $key => $img) { |
| | | $upload_id = $img['upload_id']; |
| | | $used_upload_ids[] = $upload_id; |
| | | |
| | | if (isset($uploadMap[$upload_id])) { |
| | | $attachment_id = (int)$uploadMap[$upload_id]['attachment_id']; |
| | | if ($upload_id === $featured) { |
| | | $featuredID = $attachment_id; |
| | | } else { |
| | | $childPosts[] = $attachment_id; |
| | | } |
| | | } |
| | | } |
| | | // Set the featured image for the parent |
| | | if ($featuredID) { |
| | | set_post_thumbnail($parent, $featuredID); |
| | | } elseif (!empty($childPosts)) { |
| | | //use first image if no set featured |
| | | set_post_thumbnail($parent, (int)$childPosts[0]); |
| | | array_shift($childPosts); |
| | | } |
| | | |
| | | //Create Child Posts |
| | | if (!empty($childPosts)) { |
| | | $args['post_parent'] = $parent; |
| | | $created_posts[$parent] = []; |
| | | foreach ($childPosts as $i => $imgID) { |
| | | $treatment = $i + 1; |
| | | $childTitle = $title.' - Treatment '.$treatment; |
| | | $childDesc = ''; |
| | | $args['post_title'] = $childTitle; |
| | | $args['post_excerpt'] = $childDesc; |
| | | $child = wp_insert_post($args); |
| | | if ($child && !is_wp_error($child)) { |
| | | $created_posts[$parent][] = $child; |
| | | set_post_thumbnail($child, $imgID); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return [ |
| | | 'success' => true, |
| | | 'result' => [ |
| | | 'created_posts' => $created_posts, |
| | | 'used_images' => $used_upload_ids |
| | | ] |
| | | ]; |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadRoutes]:processTimelineUploads', |
| | | $e->getMessage(), |
| | | [ |
| | | 'operation_id' => $operation->id, |
| | | 'user_id' => $operation->user_id |
| | | ] |
| | | ); |
| | | |
| | |
| | | |
| | | if ($deleted) { |
| | | $cleaned_count++; |
| | | error_log("Cleaned up unused image: attachment_id {$attachment_id}, upload_id {$upload_id}"); |
| | | } else { |
| | | $errors[] = "Failed to delete attachment {$attachment_id} for upload {$upload_id}"; |
| | | } |
| | |
| | | |
| | | function getAttachmentID(array $image, array $storedResults): int|false |
| | | { |
| | | error_log('Looking for upload_id: ' . ($image['upload_id'] ?? 'NOT SET')); |
| | | error_log('Available results: ' . print_r(array_keys($storedResults), true)); |
| | | |
| | | foreach ($storedResults as $operationID => $uploads) { |
| | | foreach ($uploads as $upload) { |
| | | // FIX: Handle both case variations |
| | |
| | | $search_upload_id = $image['upload_id']; |
| | | |
| | | if ($stored_upload_id === $search_upload_id) { |
| | | error_log("Found match! upload_id: {$search_upload_id} -> attachment_id: {$upload['attachment_id']}"); |
| | | return (int)$upload['attachment_id']; |
| | | } |
| | | } |
| | | } |
| | | |
| | | error_log("No match found for upload_id: " . ($image['upload_id'] ?? 'MISSING')); |
| | | return false; |
| | | } |
| | | |
| | |
| | | |
| | | return $title; |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | /** |
| | | * Determine how to save uploaded files based on configuration |
| | | */ |
| | | protected function handleUploadDestination(array $data, array $results): void |
| | | { |
| | | // Determine destination from config |
| | | $destination = $data['destination'] ?? 'meta'; |
| | | |
| | | switch ($destination) { |
| | | case 'meta': |
| | | // Save to post/term/user meta |
| | | $this->saveToMeta($data, $results); |
| | | break; |
| | | |
| | | case 'post': |
| | | // Create individual posts for each file |
| | | $this->createIndividualPosts($data, $results); |
| | | break; |
| | | |
| | | case 'post_group': |
| | | // Create posts with grouped files |
| | | $this->createGroupedPosts($data, $results); |
| | | break; |
| | | |
| | | default: |
| | | // No destination specified - files processed but not attached |
| | | break; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Infer destination from existing data (backward compatibility) |
| | | */ |
| | | protected function inferDestination(array $data): string |
| | | { |
| | | // If field_name exists → saving to meta |
| | | if (!empty($data['field_name'])) { |
| | | return 'meta'; |
| | | } |
| | | |
| | | // If post_type exists without field_name → creating posts |
| | | if (!empty($data['content'])) { |
| | | return 'post'; |
| | | } |
| | | |
| | | // No destination |
| | | return 'none'; |
| | | } |
| | | |
| | | 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; |
| | | } |
| | | /** |
| | | * Save attachment IDs to meta field |
| | | */ |
| | | 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)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Create individual posts from uploads |
| | | */ |
| | | protected function createIndividualPosts(array $data, array $results): array |
| | | { |
| | | if (empty($data['content'])) { |
| | | return []; |
| | | } |
| | | |
| | | $created_posts = []; |
| | | |
| | | foreach ($results as $result) { |
| | | $attachment_id = $result['attachment_id']; |
| | | $attachment = get_post($attachment_id); |
| | | |
| | | // Create post |
| | | $post_data = [ |
| | | 'post_type' => jvbCheckBase($data['content']), |
| | | 'post_title' => $attachment->post_title, |
| | | 'post_status' => 'draft', |
| | | 'post_author' => $data['user'] ?? get_current_user_id(), |
| | | ]; |
| | | |
| | | $post_id = wp_insert_post($post_data); |
| | | |
| | | if (!is_wp_error($post_id)) { |
| | | // Set as featured image or attach to gallery |
| | | $this->attachFileToPost($post_id, $attachment_id, $data); |
| | | |
| | | $created_posts[] = [ |
| | | 'post_id' => $post_id, |
| | | 'attachment_id' => $attachment_id, |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | return $created_posts; |
| | | } |
| | | |
| | | /** |
| | | * Create posts with grouped uploads |
| | | */ |
| | | protected function createGroupedPosts(array $data, array $results): array |
| | | { |
| | | if (empty($data['content'])) { |
| | | return []; |
| | | } |
| | | |
| | | $id_map = []; |
| | | foreach ($results as $result) { |
| | | if (isset($result['upload_id'], $result['attachment_id'])) { |
| | | $id_map[$result['upload_id']] = $result['attachment_id']; |
| | | } |
| | | } |
| | | |
| | | // Groups come from frontend as metadata |
| | | $groups = $data['groups'] ?? [array_column($results, 'attachment_id')]; |
| | | $created_posts = []; |
| | | |
| | | foreach ($groups as $group_index => $group_upload_ids) { |
| | | $group_attachment_ids = array_filter( |
| | | array_map(fn($uid) => $id_map[$uid] ?? null, $group_upload_ids) |
| | | ); |
| | | |
| | | if (empty($group_attachment_ids)) continue; |
| | | // Create post for this group |
| | | $first_attachment = get_post($group_attachment_ids[0]); |
| | | |
| | | $post_data = [ |
| | | 'post_type' => jvbCheckBase($data['content']), |
| | | 'post_title' => $data['group_titles'][$group_index] ?? $first_attachment->post_title, |
| | | 'post_status' => $data['post_status'] ?? 'draft', |
| | | 'post_author' => $data['user'] ?? get_current_user_id(), |
| | | ]; |
| | | |
| | | $post_id = wp_insert_post($post_data); |
| | | |
| | | if (!is_wp_error($post_id)) { |
| | | // Attach all files in group |
| | | foreach ($group_attachment_ids as $index => $attachment_id) { |
| | | if ($index === 0) { |
| | | // First is featured |
| | | set_post_thumbnail($post_id, $attachment_id); |
| | | } else { |
| | | // Others go to gallery |
| | | $meta = Meta::forPost($post_id); |
| | | $existing = $meta->get('gallery'); |
| | | $existing_ids = !empty($existing) ? explode(',', $existing) : []; |
| | | $existing_ids[] = $attachment_id; |
| | | $meta->set('gallery', implode(',', $existing_ids)); |
| | | } |
| | | } |
| | | |
| | | $created_posts[] = [ |
| | | 'post_id' => $post_id, |
| | | 'attachment_ids' => $group_attachment_ids, |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | return $created_posts; |
| | | } |
| | | |
| | | /** |
| | | * Attach file to post based on file type |
| | | */ |
| | | protected function attachFileToPost(int $post_id, int $attachment_id, array $data): void |
| | | { |
| | | $attachment = get_post($attachment_id); |
| | | $mime_type = $attachment->post_mime_type; |
| | | |
| | | // Determine file type |
| | | if (str_starts_with($mime_type, 'image/')) { |
| | | // Set as featured image |
| | | set_post_thumbnail($post_id, $attachment_id); |
| | | } elseif (str_starts_with($mime_type, 'video/')) { |
| | | // Save to video field |
| | | $meta = Meta::forPost($post_id); |
| | | $meta->set('video', $attachment_id); |
| | | } else { |
| | | // Documents - save to documents field |
| | | $meta = Meta::forPost($post_id); |
| | | $existing = $meta->get('documents'); |
| | | $existing_ids = !empty($existing) ? explode(',', $existing) : []; |
| | | $existing_ids[] = $attachment_id; |
| | | $meta->set('documents', implode(',', $existing_ids)); |
| | | } |
| | | } |
| | | } |