| | |
| | | <?php |
| | | namespace JVBase\rest\routes; |
| | | |
| | | use JVBase\managers\queue\executors\UploadExecutor; |
| | | use JVBase\managers\queue\mergers\UploadMerger; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use JVBase\Instance; |
| | | use JVBase\managers\Errors; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\registrar\Registrar; |
| | | use JVBase\rest\PermissionHandler; |
| | | use JVBase\rest\Rest; |
| | |
| | | use JVBase\rest\Route; |
| | | use WP_REST_Request; |
| | | use WP_REST_Response; |
| | | use WP_Error; |
| | | use Exception; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | } |
| | | class UploadRoutes extends Rest |
| | | { |
| | | use Instance; |
| | | |
| | | public function __construct() |
| | | { |
| | | parent::__construct(); |
| | | |
| | | 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(); |
| | | $merger = new UploadMerger('secured_files'); |
| | | |
| | | // Image uploads - chunked at 5 files |
| | | $registry->register('image_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 3 |
| | | )); |
| | | |
| | | // Video uploads - one at a time (heavy processing) |
| | | $registry->register('video_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 1 |
| | | )); |
| | | |
| | | // Document uploads - chunked at 10 |
| | | $registry->register('document_upload', new TypeConfig( |
| | | mergeable: $merger, |
| | | executor: $executor, |
| | | chunkKey: 'secured_files', |
| | | chunkSize: 5 |
| | | )); |
| | | |
| | | // 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 |
| | |
| | | * Build the main $context for UploadManager from the $request params |
| | | * @param WP_REST_Request $request |
| | | * @return array |
| | | * @throws Exception |
| | | * @throws Exception |
| | | */ |
| | | protected function buildUploadArgs(WP_REST_Request $request):array |
| | | { |
| | |
| | | |
| | | } catch (Exception $e) { |
| | | // Error handling... |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleUploadRequest', |
| | | $e->getMessage(), |
| | | [ |
| | |
| | | */ |
| | | protected function secureFiles(array $files, array $args): array |
| | | { |
| | | $uploader = new UploadManager(); |
| | | $uploader = UploadManager::getInstance(); |
| | | $secured_files = []; |
| | | $errors = []; |
| | | |
| | |
| | | |
| | | error_log('Queueing Operation: '.print_r($operation_type, true)); |
| | | error_log('With ID: '.print_r($args['upload'], true)); |
| | | $queuedProcessing = JVB()->queue()->queueOperation( |
| | | $queuedProcessing = Queue::getInstance()->queueOperation( |
| | | $operation_type, |
| | | get_current_user_id(), |
| | | array_merge( |
| | |
| | | // When merged, the original upload's attach_upload_to_content |
| | | // will handle all files after the merged image_upload completes. |
| | | if (!$queuedProcessing['updated_existing']) { |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | 'attach_upload_to_content', |
| | | get_current_user_id(), |
| | | $args, |
| | |
| | | } |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | 'temporary_cleanup', |
| | | get_current_user_id(), |
| | | [ |
| | | 'files' => $secured_data['files'], |
| | | 'context' => $args, |
| | | ], |
| | | [ |
| | | 'priority' => 'low', |
| | | 'chunk_size' => 5, |
| | | 'chunk_key' => 'files', |
| | | 'depends_on' => [$uploadOpId] |
| | | ] |
| | | ); |
| | | |
| | | return $args['id']; |
| | | } |
| | | |
| | |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * 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 { |
| | | // Get the results from the upload operation |
| | | $upload_results = JVB()->queue()->getOperationValue($data['upload'], 'result', true); |
| | | |
| | | if (empty($upload_results)) { |
| | | throw new Exception('No upload results found for operation: ' . $data['upload']); |
| | | } |
| | | |
| | | if (empty($data['post_id']) || str_starts_with((string)($data['item_id'] ?? ''), 'new')) { |
| | | foreach ($operation->dependencies as $depId) { |
| | | $dep = JVB()->queue()->get($depId); |
| | | if ($dep && $dep->type === 'content_update' && !empty($dep->result['new_posts'])) { |
| | | $itemId = $data['item_id'] ?? null; |
| | | if ($itemId && isset($dep->result['new_posts'][$itemId])) { |
| | | $data['post_id'] = $dep->result['new_posts'][$itemId]; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (empty($data['post_id'])) { |
| | | throw new Exception('Could not resolve post_id from dependencies'); |
| | | } |
| | | } |
| | | |
| | | // Now attach to the specified content |
| | | if (!empty($data['field_name'])) { |
| | | $this->updateFieldValue($data, $upload_results); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | '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 |
| | |
| | | $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]: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, |
| | | 'user_id' => $operation->user_id |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Handle metadata update requests |
| | |
| | | } |
| | | |
| | | |
| | | |
| | | |
| | | return $this->sendResponse( |
| | | false, |
| | | ['error_code' => 'missing_identifiers'], |
| | |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleMetadataUpdate', |
| | | $e->getMessage(), |
| | | [ |
| | |
| | | */ |
| | | protected function queueMetaUpdate(array $data, int $user, ?string $operationId = null): WP_REST_Response |
| | | { |
| | | $queue = JVB()->queue(); |
| | | $queue = Queue::getInstance(); |
| | | $depends_on = []; |
| | | $errors = []; |
| | | $original = count($data); |
| | |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * 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, |
| | | 'original_operation_id' => $data['original_operation_id'] ?? 'unknown' |
| | | ] |
| | | ); |
| | | |
| | | return [ |
| | | 'success' => false, |
| | | 'result' => $e->getMessage() |
| | | ]; |
| | | } |
| | | } |
| | | |
| | | protected function applyMeta(int $attachment_id, array $metadata): void |
| | | { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | | * @param int $attachment_id |
| | | * @param int $user_id |
| | | * @return bool |
| | | */ |
| | | protected function verifyAttachmentAccess(int $attachment_id, int $user_id): bool |
| | | { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Get field configuration for upload processing |
| | | */ |
| | | protected function getFieldConfig(array $data): array |
| | | { |
| | | 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']; |
| | | $registrar = Registrar::getInstance($content_type); |
| | | if ($registrar) { |
| | | $content_fields = $registrar->getFields(); |
| | | 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 array $files |
| | | * @return array |
| | | */ |
| | | protected function getFilesInfo(array $files): array |
| | | { |
| | |
| | | $chunkSize = 10; |
| | | } |
| | | |
| | | JVB()->queue()->queueOperation( |
| | | Queue::getInstance()->queueOperation( |
| | | $operation_type, |
| | | $args['user'], |
| | | array_merge( |
| | |
| | | ] |
| | | ); |
| | | |
| | | $ID = JVB()->queue()->queueOperation( |
| | | $ID = Queue::getInstance()->queueOperation( |
| | | 'process_upload_groups', |
| | | $args['user'], |
| | | $args, |
| | |
| | | |
| | | return $this->queued($ID['operation_id'], 'Files uploaded and posts queued for creation'); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | Errors::getInstance()->log( |
| | | '[UploadRoutes]:handleGroupingRequest', |
| | | $e->getMessage(), |
| | | [ |