action = 'dash-'; parent::__construct(); add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3); } /** * 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' ] ] ]); 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' ], ] ]); 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 ] ] ]); } 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, ]; 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; return $args; } /** * Handle upload request with immediate feedback */ /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function handleUploadRequest(WP_REST_Request $request): WP_REST_Response { try { if (!isset($_FILES['files'])) { return $this->createStandardResponse( false, [], __('No files uploaded.', 'jvb') ); } $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) ); } $secured_files = $this->processFileUploads($_FILES['files'], $args); if (empty($secured_files)) { error_log('Images not processed'); return $this->createStandardResponse( false, [], 'No valid files could be processed' ); } $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' ); } catch (Exception $e) { // Error handling... JVB()->error()->log( '[UploadRoutes]:handleUploadRequest', $e->getMessage(), [ 'request_data' => $request->get_params(), 'files_info' => $this->getFilesInfo($_FILES), 'trace' => $e->getTraceAsString() ] ); return $this->createStandardResponse( false, ['error_code' => 'upload_failed'], 'Upload processing failed: ' . $e->getMessage() ); } } protected function processFileUploads(array $files, array $args): array { $uploader = new UploadManager($args['content'], $args['user']); $secured_files = []; // 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']; 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] ]; if ($file_data['error'] === UPLOAD_ERR_OK) { $secured_files[] = $uploader->secureUploadedFile($file_data); } } // } return $secured_files; } /** * 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; } } /** * Process attachment metadata updates */ protected function processAttachmentMetadata(WP_Error|array $result, object $operation, array $data): WP_Error|array { try { $metadata_updates = $data['metadata_updates']; $updated_count = 0; $failed_count = 0; $results = []; foreach ($metadata_updates as $update) { try { $attachment_id = (int)$update['attachment_id']; $upload_id = $update['upload_id']; // 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; } // 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() ]; } } return [ 'success' => true, 'data' => $results, 'updated_count' => $updated_count, 'failed_count' => $failed_count, 'message' => "Metadata updated: {$updated_count} succeeded, {$failed_count} failed" ]; } catch (Exception $e) { JVB()->error()->log( '[UploadRoutes]:processAttachmentMetadata', $e->getMessage(), [ 'operation_id' => $operation->id, 'user_id' => $operation->user_id ] ); return new WP_Error( 'metadata_processing_failed', $e->getMessage(), [ 'operation_id' => $operation->id, 'error_code' => 'metadata_update_error' ] ); } } /** * Verify user has access to attachment */ protected function verifyAttachmentAccess(int $attachment_id, int $user_id): bool { $attachment = get_post($attachment_id); if (!$attachment || $attachment->post_type !== 'attachment') { return false; } // Check if user owns the attachment or has admin privileges 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 */ protected function standardizeImageResult(array $result): 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']) ] ]; } /** * Get files info for error logging */ protected function getFilesInfo(array $files): array { return [ 'files_count' => is_array($files['name']) ? count($files['name']) : 1, 'total_size' => is_array($files['size']) ? array_sum($files['size']) : $files['size'], 'file_types' => is_array($files['type']) ? array_unique($files['type']) : [$files['type']] ]; } /** * @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 { $response = [ 'success' => $success, 'message' => $message, 'data' => $data, 'timestamp' => current_time('mysql') ]; if ($operation_id) { $response['operation_id'] = $operation_id; } return new WP_REST_Response($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)); if (!array_key_exists($data['content'], JVB_CONTENT)) { return $this->createStandardResponse( false, ['error_code' => 'invalid_content'], 'Invalid content type specified' ); } //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']; } } } $operationIDs = array_unique($operationIDs); $queue = JVB()->queue(); $queue->queueOperation( 'process_upload_groups', $data['user'], $data, [ 'operation_id' => $data['id'], 'depends_on' => $operationIDs ] ); return $this->createStandardResponse( true, [ 'operation_id' => $data['id'], 'count' => count($data['posts'] ?? []) ], 'Operation queued successfully' ); } catch (Exception $e) { JVB()->error()->log( '[UploadRoutes]:handleGroupingRequest', $e->getMessage(), [ 'request_data' => $request->get_params(), 'trace' => $e->getTraceAsString() ] ); return $this->createStandardResponse( false, ['error_code' => 'grouping_failed'], '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); 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 ]; } } } $content = jvbCheckBase($data['content']); $user = (int)$data['user']; $created_posts = []; foreach ($data['posts'] as $index => $post) { $new_post_id = wp_insert_post([ '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']) : '', ]); 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; $featured_attachment_id = null; $gallery_attachment_ids = []; // Process all images for this post foreach ($post['images'] as $img) { $upload_id = $img['upload_id']; $used_upload_ids[] = $upload_id; if (isset($all_uploaded_images[$upload_id])) { $attachment_id = $all_uploaded_images[$upload_id]['attachment_id']; if ($upload_id === $featured_upload_id) { $featured_attachment_id = $attachment_id; } 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 gallery images if (!empty($gallery_attachment_ids)) { $meta = new MetaManager($new_post_id, 'post'); $fields = jvbGetFields($data['content'], 'post'); foreach ($fields as $name => $config) { if ($config['type'] === 'gallery') { $meta->updateValue($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' => [ '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" ] ]; } catch (Exception $e) { JVB()->error()->log( '[UploadRoutes]:processUploadGroups', $e->getMessage(), [ 'operation_id' => $operation->id, 'user_id' => $operation->user_id, 'posts_count' => count($data['posts'] ?? []) ] ); return [ 'success' => false, 'result' => $e->getMessage() ]; } } protected function cleanupUnusedImages(array $unused_images): array { $cleaned_count = 0; $errors = []; foreach ($unused_images as $upload_id => $image_data) { try { $attachment_id = $image_data['attachment_id']; // Verify this attachment exists and wasn't already deleted if (get_post($attachment_id)) { // Delete the attachment and its files $deleted = wp_delete_attachment($attachment_id, true); 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}"; } } else { // Attachment already doesn't exist, count as cleaned $cleaned_count++; } } catch (Exception $e) { $errors[] = "Error cleaning up upload {$upload_id}: " . $e->getMessage(); } } return [ 'cleaned_count' => $cleaned_count, 'errors' => $errors ]; } 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 $stored_upload_id = $upload['upload_id']; $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; } function extractFeaturedItem(array &$items, string $meta_key = 'featured'): array { // Handle empty array if (empty($items)) { return [ 'featured' => null, 'remaining' => [] ]; } $featured_index = null; // First pass: look for featured item foreach ($items as $index => $item) { if (isset($item['meta'][$meta_key])) { $featured_index = $index; break; } } // If no featured item found, use first item (index 0) if ($featured_index === null) { $featured_index = 0; } // Extract the featured/first item $featured = $items[$featured_index]; // Remove the item from the original array unset($items[$featured_index]); // Re-index the array to maintain sequential indices $remaining = array_values($items); return [ 'featured' => $featured, 'remaining' => $remaining ]; } protected function mapUploadIdsToAttachments(array $uploadIds, array $uploadedImages): array { $mappedImages = []; foreach ($uploadIds as $uploadId) { $imageData = $this->findImageByUploadId($uploadId, $uploadedImages); if ($imageData) { $mappedImages[] = $imageData; } } return $mappedImages; } protected function findImageByUploadId(string $uploadId, array $uploadedImages): ?array { // Handle both flat array and grouped results foreach ($uploadedImages as $image) { if (is_array($image)) { // If it's a grouped result, check each image in the group if (isset($image[0]) && is_array($image[0])) { foreach ($image as $groupImage) { if (isset($groupImage['upload_id']) && $groupImage['upload_id'] === $uploadId) { return $groupImage; } } } else { // Single image result if (isset($image['upload_id']) && $image['upload_id'] === $uploadId) { return $image; } } } } return null; } protected function determineFeaturedImage(array $group, array $groupImages): ?int { // Check if user has starred a specific image if (!empty($group['featured_upload_id'])) { foreach ($groupImages as $image) { if (isset($image['upload_id']) && $image['upload_id'] === $group['featured_upload_id']) { return $image['attachment_id']; } } } // Default to first image return !empty($groupImages) ? $groupImages[0]['attachment_id'] : null; } protected function sanitizeGroupMetadata(array $metadata): array { $sanitized = []; foreach ($metadata as $key => $value) { $sanitizedKey = sanitize_key($key); if (is_string($value)) { $sanitized[$sanitizedKey] = sanitize_text_field($value); } elseif (is_array($value)) { $sanitized[$sanitizedKey] = array_map('sanitize_text_field', $value); } else { $sanitized[$sanitizedKey] = $value; } } return $sanitized; } protected function generatePostTitle(string $content, int $userId): string { $username = get_user_meta($userId, 'first_name', true) ?: get_user_meta($userId, 'display_name', true); $link = get_user_meta($userId, BASE.'link', true); $city = function_exists('jvbArtistCity') ? jvbArtistCity($link) : ''; $title = ucfirst($content); if ($username) { $title .= ' by ' . $username; } if ($city) { $title .= ' from ' . $city; } return $title; } }