Jake Vanderwerf
2026-08-28 a9654f00c8fbc9934289d761843ddd071025013f
inc/managers/queue/executors/UploadExecutor.php
@@ -1,11 +1,14 @@
<?php
namespace JVBase\managers\queue\executors;
use JVBase\managers\queue\{Executor, Operation, Progress, Result};
use JVBase\managers\queue\{Executor, Operation, Progress, Queue, Result};
use JVBase\managers\Cache;
use JVBase\managers\Errors;
use JVBase\managers\UploadManager;
use JVBase\meta\Meta;
use Exception;
use JVBase\utility\Features;
use JVBase\registrar\Registrar;
use WP_Error;
if (!defined('ABSPATH')) {
   exit;
@@ -28,10 +31,16 @@
      'process_upload_groups'
   ];
   /**
    * @param Operation $operation
    * @param Progress $progress
    * @return Result
    * @throws Exception
    */
   public function execute(Operation $operation, Progress $progress): Result
   {
      if (!in_array($operation->type, self::HANDLED_TYPES)) {
         throw new Exception("UploadExecutor cannot handle type: {$operation->type}");
         throw new Exception("UploadExecutor cannot handle type: $operation->type");
      }
      try {
@@ -45,11 +54,11 @@
            'temporary_cleanup'       => $this->processTemporaryCleanup($operation, $data, $progress),
            'attach_upload_to_content'=> $this->processAttachToContent($operation, $data, $progress),
            'process_upload_groups'   => $this->processUploadGroups($operation, $data, $progress),
            default                   => throw new Exception("Unknown type: {$operation->type}")
            default                   => throw new Exception("Unknown type: $operation->type")
         };
      } catch (Exception $e) {
         JVB()->error()->log(
         Errors::getInstance()->log(
            '[UploadExecutor]:execute',
            $e->getMessage(),
            [
@@ -71,89 +80,66 @@
    */
   private function processFileUpload(Operation $operation, array $data, string $fileType, Progress $progress): Result
   {
      $uploader = new UploadManager();
      $processedResults = [];
      $uploader = UploadManager::getInstance();
      $errors = [];
      $uploadIds = [];
      $securedFiles = $data['secured_files'] ?? [];
      $args = $data;
      unset($args['secured_files']);
      $prepared = [];
      foreach ($securedFiles as $securedFile) {
         $uploadIds[] = $securedFile['upload_id'];
         try {
            $result = $uploader->processUpload(
            $prepared[$securedFile['upload_id']] = $uploader->processUpload(
               $securedFile['temp_path'],
               [
                  'file_type'     => $fileType,
                  'user_id'       => $operation->userId,
                  'post_id'       => (int)($data['post_id'] ?? 0),
                  'term_id'       => (int)($data['term_id'] ?? 0),
                  'original_name' => $securedFile['original_name'],
                  'mime_type'     => $securedFile['mime_type'],
                  'content'       => $data['content'] ?? '',
               ]
               $args
            );
            if (is_wp_error($result)) {
               $progress->failItem($securedFile['upload_id'], $result->get_error_message());
               $errors[] = $result->get_error_message();
               continue;
            }
            $processedResults[$securedFile['upload_id']] = [
               'upload_id' => $securedFile['upload_id'],
               'attachment_id' => $result['attachment_id'] ?? 0,
               'url' => $result['url'] ?? '',
               'sizes' => $result['sizes'] ?? [],
            ];
            if (!empty($securedFile['metadata'])) {
               $this->applyMeta($securedFile['attachment_id'], $securedFile['metadata']);
            }
            $progress->advance();
         } catch (Exception $e) {
            $progress->failItem($securedFile['upload_id'], $e->getMessage());
            $errors[] = $e->getMessage();
         }
      }
      // Handle destination (meta, post, post_group)
      $this->handleUploadDestination($data, $processedResults);
      // Cleanup temp files
      $this->cleanupTempFiles($securedFiles, $operation->userId);
      $outcome = count($processedResults) > 0 ? 'success' : 'failed';
      if (count($processedResults) > 0 && !empty($errors)) {
      $outcome = count($prepared) > 0 ? 'success' : 'failed';
      if (count($prepared) > 0 && !empty($errors)) {
         $outcome = 'partial';
      }
      return new Result(
         outcome: $outcome,
         result: [
            'upload_ids'   => $uploadIds,
            'uploads'      => $processedResults,
            'processed_count'=> count($processedResults),
            'total_count'  => count($uploadIds),
            'errors'    => $errors
            'upload_ids'      => $uploadIds,
            'uploads'         => $prepared,
            'processed_count' => count($prepared),
            'total_count'     => count($uploadIds),
            'errors'          => $errors,
         ]
      );
   }
   /**
    * Attach upload results to content
    * @param Operation $operation
    * @param array $data
    * @param Progress $progress
    * @return Result
    * @throws Exception
    */
   private function processAttachToContent(Operation $operation, array $data, Progress $progress): Result
   {
      $uploadOpId = $data['upload'] ?? null;
      error_log('processing attached to content: '.print_r($data, true));
      if (!$uploadOpId) {
         throw new Exception('No upload operation ID provided');
      }
      $uploadOp = JVB()->queue()->get($uploadOpId);
      $uploadOp = Queue::getInstance()->get($uploadOpId);
      if (!$uploadOp || !in_array($uploadOp->outcome, ['success', 'partial'])) {
         throw new Exception("Upload operation {$uploadOpId} not completed successfully");
         throw new Exception("Upload operation $uploadOpId not completed successfully");
      }
      $uploadResults = $uploadOp->result['uploads'] ?? [];
@@ -164,7 +150,7 @@
      // Resolve post_id from content_update dependency for new posts
      if (empty($data['post_id']) || str_starts_with((string)($data['item_id'] ?? ''), 'new')) {
         foreach ($operation->dependencies as $depId) {
            $dep = JVB()->queue()->get($depId);
            $dep = Queue::getInstance()->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])) {
@@ -180,10 +166,39 @@
      }
      if (!empty($data['field_name'])) {
         $this->saveToMeta($data, $uploadResults);
         if ($data['field_name'] === 'timeline_gallery') {
            $registrar = Registrar::getInstance($data['content']);
            if ($registrar) {
               $meta = match ($registrar->getType()) {
                  'post' => Meta::forPost($data['item_id']),
                  'term' => Meta::forTerm($data['item_id']),
                  'user' => Meta::forUser($data['item_id']),
                  default => false,
               };
               if ($meta) {
                  $title = $meta->get('post_title');
                  $current = $meta->get('number');
                  $i = empty($current) ? 1 : $current + 1;
                  foreach ($data['upload_ids'] as $uploadID) {
                     if (!array_key_exists($uploadID, $uploadResults)) {
                        continue;
                     }
                     $imgID = $uploadResults[$uploadID]['attachment_id'] ?? false;
                     if (!$imgID) {
                        continue;
                     }
                     $this->createTimelinePoint($imgID, $data['item_id'], $data['user'], $data['content'], $title, $i);
                     $i++;
                  }
               }
            }
         } else {
            $this->saveToMeta($data, $uploadResults);
         }
      }
      $progress->advance(1);
      $progress->advance();
      return new Result(
         outcome: 'success',
@@ -191,6 +206,25 @@
      );
   }
      protected function createTimelinePoint(int $imgID, int $parentID, int $user, string $postType, string $baseTitle, int $index):int|WP_Error
      {
         $title = $baseTitle.' - Treatment #'.$index;
         $args = [
            'post_type'    => jvbCheckBase($postType),
            'post_author'  => $user,
            'post_status'  => 'draft',
            'post_parent'  => $parentID,
            'post_title'   => $title,
            'post_name'    => sanitize_title($title)
         ];
         $child = wp_insert_post($args);
         if ($child && !is_wp_error($child)) {
            set_post_thumbnail($child, $imgID);
         }
         return $child;
      }
   /**
    * Process metadata updates for attachments
    */
@@ -199,15 +233,18 @@
      $updatedCount = 0;
      $errors = [];
      $postsAttachedTo =[];
      error_log('Processing Meta Update with data: '.print_r($data, true));
      foreach ($data as $uploadId => $info) {
         if (!is_array($info)) {
            error_log('Not an array for processing meta update: '.print_r($info, true));
            continue;
         }
         $success = true;
         try {
            if (array_key_exists('depends_on', $info)) {
               // Get the dependency operation to find attachment ID
               $depOp = JVB()->queue()->get($info['depends_on']);
               $depOp = Queue::getInstance()->get($info['depends_on']);
               if (!$depOp || !$depOp->result) {
                  $errors[] = "Dependency {$info['depends_on']} not found or has no result";
                  continue;
@@ -225,14 +262,17 @@
               $errors[] = "No attachment found for: ".print_r($info, true);
               continue;
            }
            $postID = wp_get_post_parent_id($attachmentId);
            if ($postID && !in_array($postID, $postsAttachedTo)){
               Cache::clearPost($postID);
            }
            $this->applyMeta($attachmentId, $info);
            $updatedCount++;
            $progress->advance(1);
            $progress->advance();
         } catch (Exception $e) {
            $success = false;
            $progress->failItem($uploadId, $e->getMessage());
            $errors[] = $e->getMessage();
         }
@@ -253,11 +293,14 @@
   }
   /**
    * Cleanup temporary upload files
    * @param Operation $operation
    * @param array $data
    * @param Progress $progress
    * @return Result
    */
   private function processTemporaryCleanup(Operation $operation, array $data, Progress $progress): Result
   {
      $uploader = new UploadManager();
      $uploader = UploadManager::getInstance();
      // Cleanup secured files
      if (!empty($data['files'])) {
@@ -265,7 +308,7 @@
            if (!empty($file['temp_path']) && file_exists($file['temp_path'])) {
               @unlink($file['temp_path']);
            }
            $progress->advance(1);
            $progress->advance();
         }
      }
@@ -301,7 +344,7 @@
      $uploads = [];
      $uploadIds = [];
      foreach ($dependencies as $dependency) {
         $res = JVB()->queue()->getOperationValue($dependency, 'result');
         $res = Queue::getInstance()->getOperationValue($dependency, 'result');
         if (empty($res)) {
            continue;
         }
@@ -340,7 +383,8 @@
      }
      $content = jvbCheckBase($data['content']);
      if (Features::forContent($data['content'])->has('is_timeline')) {
      $registrar = Registrar::getInstance($data['content']);
      if ($registrar && $registrar->hasFeature('is_timeline')) {
         return $this->processTimelineUploads($operation, $data, $progress, $all_uploads);
      }
@@ -368,7 +412,7 @@
               }
               $usedUploads = array_merge($usedUploads, $created['usedUploads']);
               $progress->advance(1);
               $progress->advance();
            }
         } catch (Exception $e) {
            $errors[] = $e->getMessage();
@@ -394,14 +438,11 @@
   protected function createPostFromGroup(array $post, int $index, string $content, array $uploads, Operation $op):array|false
   {
      $config = JVB_CONTENT[jvbNoBase($content)]??false;
      if (!$config) {
         throw new Exception('No config found for content: '.$content.'.');
      }
      $registrar = Registrar::getInstance($content);
      $post_title = array_key_exists('post_title', $post['fields'])
         ? sanitize_text_field($post['fields']['post_title'])
         : 'New '. $config['singular'].' '.($index + 1);
         : ($registrar ? 'New '. $registrar->getSingular().' '.($index + 1) : 'New Item '.($index + 1));
      $post_excerpt = array_key_exists('post_excerpt', $post['fields'])
         ? sanitize_textarea_field($post['fields']['post_excerpt'])
@@ -445,11 +486,11 @@
      if (!empty($gallery)) {
         $meta = Meta::forPost($ID);
         $fields = jvbGetFields($content, 'post');
         $fields = Registrar::getFieldsFor($content);
         //add images to first found gallery field
         $found = false;
         foreach ($fields as $name =>$config) {
            if ($config['type'] === 'upload' && (array_key_exists('multiple', $config) && $config['multiple'] === true)) {
            if ($config['type'] === 'gallery' || ($config['type'] === 'upload' && (array_key_exists('multiple', $config) && $config['multiple'] === true))) {
               $found = true;
               $meta->set($name, implode(',', $gallery));
               break;
@@ -459,7 +500,6 @@
            error_log('Could not find a gallery upload field for post '.$ID);
         }
         $meta->save();
      }
@@ -477,9 +517,10 @@
      $errors = [];
      $content = jvbCheckBase($data['content']);
      $config = Features::getConfig($content);
      $registrar = Registrar::getInstance($data['content']);
      $defaultTitle = 'New '.$config['singular']. ' ';
      $defaultTitle = ($registrar) ? 'New '.$registrar->getSingular(). ' ' : 'New Item ';
      foreach($data['posts'] as $index => $post) {
         try {
            $title = array_key_exists('post_title', $post['fields'])
@@ -495,7 +536,7 @@
               'post_author'  => $user,
               'post_status'  => 'draft',
               'post_title'   => $title,
               'post_slug'    => sanitize_title($title),
               'post_name'    => sanitize_title($title),
               'post_excerpt' => $excerpt
            ];
@@ -516,7 +557,7 @@
               if (array_key_exists($uploadId, $uploads)) {
                  $attachmentId = (int)$uploads[$uploadId]['attachment_id'];
                  if ($uploadId === $featured) {
                     $featuredID = $attachmentId;;
                     $featuredID = $attachmentId;
                  } else {
                     $childPosts[] = $attachmentId;
                  }
@@ -539,12 +580,10 @@
               foreach($childPosts as $i => $imgID) {
                  $treatment = $i + 1;
                  $args ['post_title'] = $title.' - Treatment #'.$treatment;
                  $child = wp_insert_post($args);
                  if ($child && !is_wp_error($child)) {
                     $createdChildren = $child;
                  $child = $this->createTimelinePoint($imgID, $parent, $args['post_author'], $args['post_type'], $title, $treatment);
                  if ($child && !is_wp_error($child) && $child> 0 ) {
                     $createdChildren[] = $child;
                     $usedUploads[] = $imgID;
                     set_post_thumbnail($child, $imgID);
                  }
               }
            }
@@ -584,14 +623,7 @@
   private function updateTimelineMetadata(int $parentId): void
   {
      // Get all child posts
      $children = get_children([
         'post_parent' => $parentId,
         'post_type' => get_post_type($parentId),
         'post_status' => ['publish', 'draft'],
         'orderby' => 'date',
         'order' => 'DESC',
         'fields' => 'ids'
      ]);
      $children = jvbTimelinePoints($parentId, get_post_type($parentId), ['any']);
      // Count includes parent + children
      $number = count($children) + 1;
@@ -605,24 +637,48 @@
   // Helper methods
   // ─────────────────────────────────────────────────────────────
   /**
    * @param int $attachmentId
    * @param array $metadata
    * @return void
    */
   private function applyMeta(int $attachmentId, array $metadata): void
   {
      if (!empty($metadata['image-title'])) {
         wp_update_post([
            'ID'         => $attachmentId,
            'post_title' => sanitize_text_field($metadata['image-title']),
         ]);
      $postUpdates = [];
      foreach ($metadata as $key => $value) {
         //Remove the image_data| from the  meta's group formatting
         $base = 'image_data|';
         $key = str_replace($base, '', $key);
         if ($key === 'image-alt-text') {
            error_log('Updating Image Alt Text: '.print_r($value, true));
            update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($value));
            continue;
         }
         $k = match ($key) {
            'image-title'  => 'post_title',
            'image-caption' => 'post_excerpt',
            'image-description' => 'post_content',
            default => false
         };
         if (!$k) {
            error_log('No matching key for '.$key.' with value: '.print_r($value, true));
            continue;
         }
         $v = match ($key) {
            'image-title'  => sanitize_text_field($value),
            default        => sanitize_textarea_field($value)
         };
         $postUpdates[$k] = $v;
      }
      if (!empty($metadata['image-alt-text'])) {
         update_post_meta($attachmentId, '_wp_attachment_image_alt', sanitize_text_field($metadata['image-alt-text']));
      }
      if (!empty($postUpdates)){
         $postUpdates['ID'] = $attachmentId;
         $postUpdates['post_type'] = 'attachment';
      if (!empty($metadata['image-caption'])) {
         wp_update_post([
            'ID'           => $attachmentId,
            'post_excerpt' => sanitize_textarea_field($metadata['image-caption']),
         ]);
         error_log('Updating Image post with data: '.print_r($postUpdates, true));
         wp_update_post($postUpdates);
      }
   }
@@ -668,27 +724,6 @@
         $meta->set($data['field_name'], implode(',', $allIds));
      }
      $meta->save();
   }
   private function updateFieldValue(array $data, array $results): void
   {
      if (empty($data['field_name'])) {
         return;
      }
      $attachmentIds = array_column($results, 'attachment_id');
      $meta = $this->getMetaManager($data);
      if (!$meta) {
         return;
      }
      $existing = $meta->get($data['field_name']);
      $existingIds = !empty($existing) ? explode(',', $existing) : [];
      $allIds = array_unique(array_merge($existingIds, $attachmentIds));
      $meta->set($data['field_name'], implode(',', $allIds));
      $meta->save();
   }
   private function getMetaManager(array $data): ?Meta
@@ -708,6 +743,11 @@
      return null;
   }
   /**
    * @param array $data
    * @param array $results
    * @return array
    */
   private function createIndividualPosts(array $data, array $results): array
   {
      if (empty($data['content'])) {
@@ -751,20 +791,18 @@
      } elseif (str_starts_with($mimeType, 'video/')) {
         $meta = Meta::forPost($postId);
         $meta->set('video', $attachmentId);
         $meta->save();
      } else {
         $meta = Meta::forPost($postId);
         $existing = $meta->get('documents');
         $existingIds = !empty($existing) ? explode(',', $existing) : [];
         $existingIds[] = $attachmentId;
         $meta->set('documents', implode(',', $existingIds));
         $meta->save();
      }
   }
   private function cleanupTempFiles(array $securedFiles, int $userId): void
   {
      $uploader = new UploadManager();
      $uploader = UploadManager::getInstance();
      foreach ($securedFiles as $secured) {
         if (!empty($secured['temp_path']) && file_exists($secured['temp_path'])) {