From 2bb9aaaf24b794b528e3894ee9f9c42ca6d7fe93 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Thu, 01 Jan 2026 21:08:58 +0000
Subject: [PATCH] =FeedRoutes: extractTaxonomies added

---
 inc/managers/UploadManager.php | 1256 +++++++++++++++++++++++++++++------------------------------
 1 files changed, 625 insertions(+), 631 deletions(-)

diff --git a/inc/managers/UploadManager.php b/inc/managers/UploadManager.php
index 6c9655a..fdc3e2e 100644
--- a/inc/managers/UploadManager.php
+++ b/inc/managers/UploadManager.php
@@ -2,337 +2,294 @@
 namespace JVBase\managers;
 
 use JVBase\JVB;
-use JVBase\meta\MetaManager;
 use Exception;
 use Imagick;
 use WP_Error;
 
 if (!defined('ABSPATH')) {
-    exit; // Exit if accessed directly
+	exit;
 }
+
 /**
- * Handles file uploads for edmonton.ink dashboard
- * Includes image processing, validation, optimization, and SEO-friendly naming
+ * Flexible file upload manager for images, videos, and documents
+ * Supports configurable directory structures and processing options
  */
 class UploadManager
 {
-    /**
-     * @var array Default configuration
-     */
-	protected array $config = [
-		'allowed_types' => [
-			'image/jpeg',
-			'image/png',
-			'image/gif',
-			'image/webp',
-		],
-		'max_size' => 5242880, // 5MB
-		'convert_to_webp' => true,
-		'webp_quality' => 80,
-		'optimize_images' => true,
-		'create_thumbnails' => true,
-		'original_retention' => 2592000, // 30 days in seconds
-		'use_imagick' => null // Will be set in constructor
+	protected array $allowedTypes = [
+		// Images
+		'image/jpeg'    => 'image',
+		'image/png'     => 'image',
+		'image/gif'     => 'image',
+		'image/webp'    => 'image',
+		// Videos
+		'video/mp4'     => 'video',
+		'video/webm'    => 'video',
+		'video/ogg'     => 'video',
+		'video/ogv'     => 'video',
+		'video/quicktime' => 'video',
+		'video/x-msvideo' => 'video',
+		// Documents
+		'application/pdf' => 'document',
+		'application/msword' => 'document',
+		'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'document',
+		'application/vnd.ms-excel' => 'document',
+		'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'document',
+		'text/plain'    => 'document',
+		'text/csv'      => 'document',
+		'application/rtf' => 'document'
 	];
 
-    protected string $post_type;
-    protected int $user_id;
-	protected int $term_id;
-    protected string $upload_dir;
-    protected string $upload_url;
-    protected object $cache;
-    protected object $notifications;
-    protected int $max_retries = 3;
-    protected int $retry_delay = 300; // 5 minutes
 
-    public function __construct($post_type, $user_id, $config = [])
-    {
-        $this->post_type = $post_type;
-        $this->user_id = $user_id;
-        $this->config = wp_parse_args($config, $this->config);
-        // Check for Imagick availability
-        $this->config['use_imagick'] = extension_loaded('imagick');
+	protected array $supportedFormats = [
+		'webp' => 'image/webp',
+		'jpeg' => 'image/jpeg',
+		'jpg'  => 'image/jpeg',
+		'png'  => 'image/png',
+		'gif'  => 'image/gif'
+	];
 
-        $this->setupUploadDirs();
+	protected array $defaultConfig = [
+		'allowed_types' => null, // null = all types allowed
+		'max_size' => [
+			'image' => 5242880,    // 5MB
+			'video' => 104857600,  // 100MB
+			'document' => 10485760 // 10MB
+		],
+		// Image settings
+		'convert' => 'webp',
+		'quality' => 80,
+		'optimize_images' => true,
+		'create_thumbnails' => true,
+		'use_imagick' => null,
+		// Video settings
+		'extract_video_thumbnail' => true,
+		'video_thumbnail_time' => 0,
+		// Document settings
+		'extract_document_preview' => false,
+		// Directory structure
+		'directory_pattern' => '{user_id}/{post_type}',
+		// General
+		'original_retention' => 2592000, // 30 days
+	];
 
-
-        // Schedule cleanup of original files
-//        if (!wp_next_scheduled('jvb_cleanup_original_uploads')) {
-//            wp_schedule_event(time(), 'daily', 'jvb_cleanup_original_uploads');
-//        }
-//        add_action('jvb_cleanup_original_uploads', [$this, 'cleanupOriginalFiles']);
-
-
-        // Track upload statistics
-        add_action('jvb_upload_complete', [$this, 'trackUploadStats']);
-        add_action('jvb_upload_failed', [$this, 'track_failed_upload']);
-    }
-
-    /**
-     * @param array $file_data
-     *
-     * @return array
-     * @throws Exception
-     */
-	public function secureUploadedFile(array $file_data): array
+	protected string $upload_dir;
+	protected string $upload_url;
+	public function __construct()
 	{
-		// Create a persistent temporary directory if it doesn't exist
-		$temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$this->user_id}";
-		wp_mkdir_p($temp_dir);
+		$upload = wp_upload_dir();
+		$this->upload_dir = $upload['basedir'];
+		$this->upload_url = $upload['baseurl'];
 
-		// Generate a unique filename for temporary storage
-		$temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']);
-		$temp_path = "{$temp_dir}/{$temp_filename}";
-
-		// Move the uploaded file to our temporary storage
-		if (!move_uploaded_file($file_data['tmp_name'], $temp_path)) {
-			// Fallback to copy if needed
-			if (!copy($file_data['tmp_name'], $temp_path)) {
-				throw new Exception('Failed to store uploaded file');
-			}
-		}
-
-		// Return metadata about the stored file
-		return [
-			'temp_path' => $temp_path,
-			'original_name' => $file_data['name'],
-			'mime_type' => $file_data['type'],
-			'file_size' => $file_data['size'],
-			'stored_at' => current_time('mysql')
-		];
-	}
-
-    /**
-     * @param string $temp_path
-     * @param string $post_type
-     * @param int $post_id
-     * @param int $term_id
-     * @param string $original_filename
-     *
-     * @return array
-     * @throws Exception
-     */
-	public function processImageFromStorage(string $temp_path, string $post_type, int $post_id = 0, int $term_id = 0, string $original_filename = ''): array
-	{
-		if (!file_exists($temp_path)) {
-			throw new Exception('Stored file no longer exists: ' . $temp_path);
-		}
-
-		$this->term_id = $term_id;
-		$this->post_type = $post_type;
-
-		// Get base upload directory based on content type
-		$rel_path = $this->getUploadDirectory();
-
-		// Ensure the directory exists
-		$full_upload_path = $this->upload_dir . '/' . $rel_path;
-		if (!wp_mkdir_p($full_upload_path)) {
-			throw new Exception("Failed to create upload directory: {$full_upload_path}");
-		}
-
-		// Generate filename WITHOUT extension (generateFilename should not include extension)
-		$base_filename = $this->generateFilename($original_filename, get_userdata($this->user_id));
-
-		// Process the image directly from our temporary storage
-		return $this->processImage($temp_path, $rel_path, $base_filename, $post_id);
-	}
-
-    /**
-     * @return void
-     * @throws Exception
-     */
-    protected function setupUploadDirs():void
-    {
-        $upload_info = wp_upload_dir();
-        if ($upload_info['error']) {
-            throw new Exception($upload_info['error']);
-        }
-
-        $this->upload_dir = $upload_info['basedir'];
-        $this->upload_url = $upload_info['baseurl'];
-    }
-
-    /**
-     * @return string
-     */
-    public function getUploadDirectory():string
-    {
-		// Default WordPress organization: year/month
-		$default_path = date('Y/m');
-
-		/**
-		 * Filter the upload directory structure
-		 *
-		 * @param string $path The default upload path
-		 * @param string $post_type The post type being uploaded
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_directory', $default_path, $this->post_type, $this->user_id, $this->term_id);
-    }
-
-	/**
-	 * Generate filename with extensible filtering
-	 * Default: generic SEO-friendly format
-	 *
-	 * @param string $original_name
-	 * @param object $user_data
-	 * @return string
-	 */
-	public function generateFilename(string $original_name, object $user_data): string
-	{
-		// Default generic filename: {post_type}-{user_id}-{counter}
-		$filename = sprintf(
-			'%s-%s',
-			sanitize_title($this->post_type),
-			$this->user_id
-		);
-
-		/**
-		 * Filter the generated filename (without extension)
-		 *
-		 * @param string $filename The generated filename
-		 * @param string $original_name The original filename
-		 * @param object $user_data WordPress user data object
-		 * @param string $post_type The post type
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_filename', $filename, $original_name, $user_data, $this->post_type, $this->user_id, $this->term_id).'-'.$this->getNextFileCounter();
-	}
-
-    /**
-     * @return string
-     */
-	protected function getNextFileCounter(): string
-	{
-		// Get counter key for this post type
-		$counter_key = 'upload_counter_' . str_replace(['/', '\\'], '_', $this->post_type);
-
-		// Get current counter value, default to 0
-		$counter = (int)get_user_meta($this->user_id, $counter_key, true) ?: 0;
-
-		// Increment counter
-		$counter++;
-
-		// Update counter in user meta
-		update_user_meta($this->user_id, $counter_key, $counter);
-
-		// Return formatted counter
-		return sprintf('%08d', $counter);
-	}
-
-
-	/**
-	 * Generate alt text with filtering for customization
-	 * Default: basic or empty alt text
-	 *
-	 * @param string $file
-	 * @param object $user_data
-	 * @param int|null $post_id
-	 * @return string
-	 */
-	protected function generateAltText(string $file, object $user_data, int|null $post_id = null): string
-	{
-		// Default: basic alt text or empty
-		$alt_text = '';
-
-		/**
-		 * Filter the generated alt text
-		 *
-		 * @param string $alt_text The generated alt text
-		 * @param string $file The file path
-		 * @param object $user_data WordPress user data object
-		 * @param int|null $post_id The post ID (if applicable)
-		 * @param string $post_type The post type
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_alt_text', $alt_text, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id);
+		// Check for Imagick
+		$this->defaultConfig['use_imagick'] = extension_loaded('imagick');
 	}
 
 	/**
-	 * Generate image title with filtering for customization
-	 * Default: WordPress default behavior (filename-based)
-	 *
-	 * @param string $file
-	 * @param object $user_data
-	 * @param int|null $post_id
-	 * @return string
+	 * Initial, basic processing of upload data for later processing through OperationQueue.php
+	 * @param array $file_data
+	 * @param array $context
+	 * @return array
 	 */
-	protected function generateImageTitle(string $file, object $user_data, int|null $post_id = null): string
-	{
-		// Default: Use filename without extension (WordPress default behavior)
-		$title = pathinfo($file, PATHINFO_FILENAME);
-
-		/**
-		 * Filter the generated image title
-		 *
-		 * @param string $title The generated title
-		 * @param string $file The file path
-		 * @param object $user_data WordPress user data object
-		 * @param int|null $post_id The post ID (if applicable)
-		 * @param string $post_type The post type
-		 * @param int $user_id The user ID
-		 * @param int $term_id The term ID (if applicable)
-		 */
-		return apply_filters('jvb_upload_image_title', $title, $file, $user_data, $post_id, $this->post_type, $this->user_id, $this->term_id);
-	}
-
-    /**
-     * @param array $file_data
-     * @param array $options
-     *
-     * @return array|WP_Error
-     */
-	public function handleContentUpload(array $file_data, array $options = []): array|WP_Error
+	public function secureUploadedFile(array $file_data, array $context): array
 	{
 		try {
-			if (!isset($file_data['tmp_name']) || !is_uploaded_file($file_data['tmp_name'])) {
-				throw new Exception('No valid file uploaded.');
+			// Validate file upload
+			if (!isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) {
+				throw new Exception('Invalid file upload');
 			}
 
-			$user_data = get_userdata($this->user_id);
-			$post_id = $options['post_id'] ?? 0;
-			$this->term_id = $options['term_id'] ?? 0;
+			$mime_type = mime_content_type($file_data['tmp_name']);
+			$file_type = $this->allowedTypes[$mime_type] ?? 'unknown';
+			if ($file_type === 'unknown') {
+				throw new Exception('Unsupported file type: ' . $mime_type);
+			}
 
-			// Get base upload directory based on content type
-			$base_dir = $this->getUploadDirectory();
-			$original_dir = 'originals';
+			// Check allowed types if specified
+			if ($this->defaultConfig['allowed_types'] && !in_array($mime_type, $this->defaultConfig['allowed_types'])) {
+				throw new Exception('File type not allowed: ' . $mime_type);
+			}
 
-			$rel_path = $base_dir;
-			$this->ensureUploadDirs($rel_path, $original_dir);
+			$temp_dir = $this->generateDirectory($file_type, "{user_id}/temp", $context);
 
-			$filename = $this->generateFilename($file_data['name'], $user_data);
+			// Ensure directories exist
+			$this->ensureDirectories($temp_dir);
 
-			// Store original file
-			$original_file = $this->storeOriginalFile(
-				$file_data['tmp_name'],
-				$base_dir,
-				$original_dir,
-				$filename
-			);
+			// Generate a unique filename for temporary storage
+			$temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']);
+			$temp_path = "{$this->upload_dir}/{$temp_dir}/{$temp_filename}";
 
-			// Process immediately
-			return $this->processImage($original_file, $rel_path, $filename, $post_id);
+			// Move the uploaded file to our temporary storage
+			if (!move_uploaded_file($file_data['tmp_name'], $temp_path)) {
+				// Fallback to copy if needed
+				if (!copy($file_data['tmp_name'], $temp_path)) {
+					throw new Exception('Failed to store uploaded file');
+				}
+			}
+
+			// Return metadata about the stored file
+			return [
+				'temp_path' => $temp_path,
+				'original_name' => $file_data['name'],
+				'mime_type' => $file_data['type'],
+				'file_type' => $file_type,
+				'file_size' => $file_data['size'],
+				'stored_at' => current_time('mysql')
+			];
+		} catch (Exception $e) {
+			JVB()->error()->log('Failed to store uploaded file: ' . print_r($e->getMessage(), true));
+			return [];
+		}
+	}
+
+	/**
+	 * Process an uploaded file with full context
+	 *
+	 * @param string $temp_path
+	 * @param array $context Upload context and configuration
+	 * @return array|WP_Error Result with attachment_id, url, file path
+	 */
+	public function processUpload(string $temp_path, array $context): array|WP_Error
+	{
+		try {
+			// Validate temp file exists
+			if (!file_exists($temp_path)) {
+				throw new Exception('Temporary file not found: ' . $temp_path);
+			}
+
+			// Validate MIME type
+			$mime_type = mime_content_type($temp_path);
+			if (!$this->validateMimeType($temp_path, $mime_type)) {
+				throw new Exception('Invalid or potentially dangerous file type');
+			}
+			$file_type = $this->allowedTypes[$mime_type] ?? 'unknown';
+
+			if ($file_type === 'unknown') {
+				throw new Exception('Unsupported file type: ' . $mime_type);
+			}
+
+			// Merge config
+			$config = array_merge($this->defaultConfig, $context['config'] ?? [], $context);
+
+			// Extract context
+			$user_id = $context['user_id'] ?? get_current_user_id();
+			$post_id = $context['post_id'] ?? 0;
+
+			// Check allowed types if specified
+			if (!empty($config['allowed_types']) && !in_array($mime_type, $config['allowed_types'])) {
+				throw new Exception('File type not allowed: ' . $mime_type);
+			}
+
+			// Validate file size
+			$this->validateFileSize($temp_path, $file_type, $config);
+
+			// Generate directory structure for final storage
+			$directory = $this->generateDirectory($file_type, $config['directory_pattern'] ?? '{user_id}/{content}/{file_type}', $context);
+
+			// Ensure directories exist
+			$this->ensureDirectories($directory);
+
+			// Generate filename
+			$original_name = $context['original_name'] ?? basename($temp_path);
+			$filename = $this->generateFilename($original_name, $context);
+
+			// Store original from temp
+			$original_path = $this->storeOriginalFromTemp($temp_path, $directory, $filename);
+
+			// Process based on file type
+			return match($file_type) {
+				'image' => $this->processImage($original_path, $directory, $filename, $post_id, $config, $context),
+				'video' => $this->processVideo($original_path, $directory, $filename, $post_id, $config, $context),
+				'document' => $this->processDocument($original_path, $directory, $filename, $post_id, $config, $context),
+				default => throw new Exception('Unknown file type')
+			};
 
 		} catch (Exception $e) {
+			JVB()->error()->log(
+				'[UploadManager]:processUpload',
+				$e->getMessage(),
+				['temp_path' => $temp_path, 'context' => $context]
+			);
+
 			return new WP_Error('upload_failed', $e->getMessage());
 		}
 	}
 
-    /**
-     * @param string $rel_path
-     * @param string $original_dir
-     *
-     * @return void
-     * @throws Exception
-     */
-	protected function ensureUploadDirs($rel_path, $original_dir): void
+	/**
+	 * Store original file from temp location (not from PHP upload)
+	 */
+	protected function storeOriginalFromTemp(string $temp_path, string $directory, string $filename): string
+	{
+		$ext = pathinfo($temp_path, PATHINFO_EXTENSION);
+		$original_path = "{$this->upload_dir}/{$directory}/originals/{$filename}.{$ext}";
+
+		// Copy from temp to final location
+		if (!copy($temp_path, $original_path)) {
+			throw new Exception('Failed to store original file');
+		}
+
+		return $original_path;
+	}
+
+	/**
+	 * Clean up empty temporary directories
+	 */
+	public function cleanupEmptyTempDirs(int $user_id): void
+	{
+		$temp_base = "{$this->upload_dir}/{$user_id}/temp";
+
+		if (!is_dir($temp_base)) {
+			return;
+		}
+
+		// Get all subdirectories
+		$dirs = glob($temp_base . '/*', GLOB_ONLYDIR);
+
+		foreach ($dirs as $dir) {
+			// Check if directory is empty
+			$files = scandir($dir);
+			$files = array_diff($files, ['.', '..']);
+
+			if (empty($files)) {
+				@rmdir($dir);
+			}
+		}
+
+		// Try to remove temp base if empty
+		$files = scandir($temp_base);
+		$files = array_diff($files, ['.', '..']);
+		if (empty($files)) {
+			@rmdir($temp_base);
+		}
+	}
+	/**
+	 * Generate directory structure based on pattern
+	 */
+	protected function generateDirectory(string $file_type, string $pattern, array $context): string
+	{
+		$replacements = [
+			'{user_id}' => $context['user_id'] ?? get_current_user_id(),
+			'{post_type}' => $context['content'] ?? '',
+			'{content}' => $context['content'] ?? '',
+			'{file_type}' => $file_type,
+			'{year}' => date('Y'),
+			'{month}' => date('m'),
+		];
+
+		$directory = str_replace(array_keys($replacements), array_values($replacements), $pattern);
+
+		// Allow filtering
+		return apply_filters('jvb_upload_directory', $directory, $file_type, $context);
+	}
+
+	/**
+	 * Ensure directory structure exists
+	 */
+	protected function ensureDirectories(string $rel_path): void
 	{
 		$dirs = [
 			"{$this->upload_dir}/{$rel_path}",
-			"{$this->upload_dir}/{$rel_path}/{$original_dir}"
+			"{$this->upload_dir}/{$rel_path}/originals"
 		];
 
 		foreach ($dirs as $dir) {
@@ -342,19 +299,13 @@
 		}
 	}
 
-
 	/**
-	 * @param string $tmp_file
-	 * @param string $base_dir
-	 * @param string $original_dir
-	 * @param string $filename
-	 * @return string
-	 * @throws Exception
+	 * Store original file
 	 */
-	protected function storeOriginalFile(string $tmp_file, string $base_dir, string $original_dir, string $filename): string
+	protected function storeOriginal(string $tmp_file, string $directory, string $filename): string
 	{
 		$ext = pathinfo($tmp_file, PATHINFO_EXTENSION);
-		$original_path = "{$this->upload_dir}/{$base_dir}/{$original_dir}/{$filename}.{$ext}";
+		$original_path = "{$this->upload_dir}/{$directory}/originals/{$filename}.{$ext}";
 
 		if (!move_uploaded_file($tmp_file, $original_path)) {
 			throw new Exception('Failed to store original file');
@@ -364,275 +315,424 @@
 	}
 
 	/**
-	 * Process image with better error handling
+	 * Validate file size based on type and config
 	 */
-	protected function processImage(string $original_file, string $rel_path, string $filename, int $post_id): array
+	protected function validateFileSize(string $file_path, string $file_type, array $config): void
 	{
-		// Validate the original file still exists
-		if (!file_exists($original_file)) {
-			throw new Exception('Original file no longer exists: ' . $original_file);
+		$size = filesize($file_path);
+		$max_size = $config['max_size'][$file_type] ?? $this->defaultConfig['max_size'][$file_type];
+
+		if ($size > $max_size) {
+			$max_mb = round($max_size / 1048576, 2);
+			$actual_mb = round($size / 1048576, 2);
+			throw new Exception("File too large: {$actual_mb}MB (max: {$max_mb}MB)");
+		}
+	}
+
+	protected function validateMimeType(string $file_path, string $declared_type): bool
+	{
+		// Get actual MIME type
+		$actual_type = mime_content_type($file_path);
+
+		// Check if actual type is allowed
+		if (!array_key_exists($actual_type, $this->allowedTypes)) {
+			return false;
 		}
 
-		// Verify file type before processing
-		$mime_type = mime_content_type($original_file);
-		if (!in_array($mime_type, $this->config['allowed_types'])) {
-			throw new Exception('Invalid file type detected during processing: ' . $mime_type);
+		// For images, verify it's actually an image
+		if (str_starts_with($actual_type, 'image/')) {
+			$image_info = @getimagesize($file_path);
+			if ($image_info === false) {
+				return false;
+			}
 		}
 
-		// Ensure the upload directory exists
-		$full_upload_dir = $this->upload_dir . '/' . $rel_path;
-		if (!wp_mkdir_p($full_upload_dir)) {
-			throw new Exception("Failed to create upload directory: {$full_upload_dir}");
+		return true;
+	}
+
+	/**
+	 * Process image file
+	 * @throws Exception
+	 */
+	protected function processImage(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array
+	{
+		$full_dir = "{$this->upload_dir}/{$directory}";
+		$original_mime = mime_content_type($original_path);
+
+		// Determine if conversion is needed
+		$should_convert = false;
+		$target_format = false;
+
+		if ($config['convert'] && $config['convert'] !== false) {
+			$target_format = strtolower($config['convert']);
+
+			// Validate format
+			if (!isset($this->supportedFormats[$target_format])) {
+				throw new Exception("Unsupported conversion format: {$target_format}");
+			}
+
+			// Check if conversion is needed
+			$target_mime = $this->supportedFormats[$target_format];
+			$should_convert = ($original_mime !== $target_mime);
 		}
 
-		// Convert to WebP if enabled
-		if ($this->config['convert_to_webp'] && $mime_type !== 'image/webp') {
-			$final_path = "{$full_upload_dir}/{$filename}.webp";
+		// Convert or copy based on configuration
+		if ($should_convert) {
+			$final_path = "{$full_dir}/{$filename}.{$target_format}";
 
-			if ($this->config['use_imagick']) {
-				$this->convertWithImagick($original_file, $final_path);
-			} else {
-				$this->convertWithGd($original_file, $final_path);
+			try {
+				$this->convertImage(
+					$original_path,
+					$final_path,
+					$target_format,
+					$config['quality'],
+					$config['use_imagick']
+				);
+			} catch (Exception $e) {
+				JVB()->error()->log('Image conversion failed: ' . print_r($e->getMessage(), true));
+				// Fallback to original
+				$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+				$final_path = "{$full_dir}/{$filename}.{$ext}";
+				copy($original_path, $final_path);
 			}
 		} else {
-			// Just copy the original with its extension
-			$original_ext = pathinfo($original_file, PATHINFO_EXTENSION);
-			$final_path = "{$full_upload_dir}/{$filename}.{$original_ext}";
-
-			if (!copy($original_file, $final_path)) {
-				throw new Exception("Failed to copy file from {$original_file} to {$final_path}");
-			}
+			// Keep original format
+			$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+			$final_path = "{$full_dir}/{$filename}.{$ext}";
+			copy($original_path, $final_path);
 		}
 
-		// Verify the final file was created
-		if (!file_exists($final_path)) {
-			throw new Exception("Final processed file was not created: {$final_path}");
+		// Create attachment
+		$attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
+
+		// Set alt text if provided
+		$alt_text = apply_filters('jvb_upload_alt_text', '', $context);
+		if ($alt_text !== '') {
+			update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
 		}
 
-		// Generate title text
-		$title = $this->generateImageTitle(
-			$final_path,
-			get_userdata($this->user_id),
-			$post_id
-		);
-
-		// Create attachment with title
-		$attachment_id = $this->createAttachment($final_path, $title, $post_id);
-
 		// Generate thumbnails
-		if ($this->config['create_thumbnails']) {
+		if ($config['create_thumbnails']) {
 			$this->generateThumbnails($attachment_id);
 		}
 
-		// Update post attachments with new file info
-		$this->updatePostAttachments($attachment_id, $final_path);
-
 		return [
 			'success' => true,
 			'attachment_id' => $attachment_id,
 			'url' => wp_get_attachment_url($attachment_id),
-			'file' => $final_path
+			'file' => $final_path,
+			'type' => 'image',
+			'format' => $target_format ?: pathinfo($final_path, PATHINFO_EXTENSION)
 		];
 	}
 
-    /**
-     * @param string $source
-     * @param string $destination
-     *
-     * @return void
-     * @throws Exception
-     */
-	protected function convertWithImagick(string $source, string $destination, string $toType = 'webp'): void
+	/**
+	 * Process video file
+	 * @throws Exception
+	 */
+	protected function processVideo(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array
 	{
-		$allowed = ['webp', 'jpeg', 'jpg', 'png'];
-		if (!in_array($toType, $allowed)) {
-			return;
-		}
+		$full_dir = "{$this->upload_dir}/{$directory}";
+		$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+		$final_path = "{$full_dir}/{$filename}.{$ext}";
+
+		// Copy video to final location
+		copy($original_path, $final_path);
+
+		// Create attachment
 		try {
-			$image = new Imagick($source);
-			$image->setImageFormat('webp');
-			$image->setImageCompressionQuality($this->config['webp_quality']);
-			$image->writeImage($destination);
-			$image->clear();
+			$attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
 		} catch (Exception $e) {
-			throw new Exception('WebP conversion with Imagick failed: ' . $e->getMessage());
+			JVB()->error()->log('Attachment creation failed: '.print_r($e->getMessage(), true));
+			return ['success' => false];
+		}
+
+
+		// Extract thumbnail if enabled
+		if ($config['extract_video_thumbnail']) {
+			$this->extractVideoThumbnail($attachment_id, $final_path, $config['video_thumbnail_time']);
+		}
+
+		return [
+			'success' => true,
+			'attachment_id' => $attachment_id,
+			'url' => wp_get_attachment_url($attachment_id),
+			'file' => $final_path,
+			'type' => 'video'
+		];
+	}
+
+	/**
+	 * Process document file
+	 */
+	protected function processDocument(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array
+	{
+		$full_dir = "{$this->upload_dir}/{$directory}";
+		$ext = pathinfo($original_path, PATHINFO_EXTENSION);
+		$final_path = "{$full_dir}/{$filename}.{$ext}";
+
+		// Copy document to final location
+		copy($original_path, $final_path);
+
+		// Create attachment
+		try {
+			$attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id);
+		} catch (Exception $e) {
+			JVB()->error()->log('Conversion failed: '.print_r($e->getMessage(), true));
+			return ['success' => false];
+		}
+
+
+		return [
+			'success' => true,
+			'attachment_id' => $attachment_id,
+			'url' => wp_get_attachment_url($attachment_id),
+			'file' => $final_path,
+			'type' => 'document'
+		];
+	}
+
+	/**
+	 * Generate SEO-friendly filename
+	 */
+	protected function generateFilename(string $original_name, array $context): string
+	{
+		$name_parts = pathinfo($original_name);
+		$base_name = sanitize_title($name_parts['filename']);
+		$user_data = array_key_exists('user_id', $context) ? get_userdata((int)$context['user_id']) : get_current_user();
+		$username = $user_data ? sanitize_title($user_data->display_name) : 'user';
+		$timestamp = time();
+
+		return apply_filters(
+			'jvb_upload_filename',
+			"{$base_name}-{$timestamp}",
+			$context
+		);
+	}
+
+	protected function sanitizeFileName(string $filename): string
+	{
+		// Remove path info
+		$filename = basename($filename);
+
+		// Remove special characters
+		$filename = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
+
+		// Remove multiple underscores
+		$filename = preg_replace('/_+/', '_', $filename);
+
+		// Ensure it's not too long (255 char limit in most filesystems)
+		if (strlen($filename) > 200) {
+			$ext = pathinfo($filename, PATHINFO_EXTENSION);
+			$name = pathinfo($filename, PATHINFO_FILENAME);
+			$name = substr($name, 0, 200 - strlen($ext) - 1);
+			$filename = $name . '.' . $ext;
+		}
+
+		return $filename;
+	}
+
+	/**
+	 * Create WordPress attachment
+	 */
+	protected function createAttachment(string $file_path, string $title, array $context, int $post_id = 0): int
+	{
+		$file_url = str_replace($this->upload_dir, $this->upload_url, $file_path);
+		$mime_type = mime_content_type($file_path);
+
+		$title = apply_filters('jvb_upload_title', $title, $file_path, $context);
+
+		$attachment_id = wp_insert_attachment([
+			'guid' => $file_url,
+			'post_mime_type' => $mime_type,
+			'post_title' => $title,
+			'post_status' => 'inherit'
+		], $file_path, $post_id);
+
+		if (is_wp_error($attachment_id)) {
+			throw new Exception('Failed to create attachment');
+		}
+
+		require_once(ABSPATH . 'wp-admin/includes/image.php');
+		$metadata = wp_generate_attachment_metadata($attachment_id, $file_path);
+		wp_update_attachment_metadata($attachment_id, $metadata);
+
+		return $attachment_id;
+	}
+
+
+	/**
+	 * Convert an existing attachment to a different format
+	 *
+	 * @param int $attachment_id WordPress attachment ID
+	 * @param string $format Target format: 'webp', 'jpeg', 'png', 'gif'
+	 * @param int $quality Conversion quality (1-100)
+	 * @param bool $replace Replace original file (default: true)
+	 * @return array|WP_Error Result with new attachment details
+	 */
+	public function convertImageTo(int $attachment_id, string $format, int $quality = 80, bool $replace = true): array|WP_Error
+	{
+		try {
+			// Validate format
+			$format = strtolower($format);
+			if (!isset($this->supportedFormats[$format])) {
+				throw new Exception("Unsupported format: {$format}");
+			}
+
+			// Get original file
+			$original_path = get_attached_file($attachment_id);
+			if (!$original_path || !file_exists($original_path)) {
+				throw new Exception('Original file not found');
+			}
+
+			// Check if it's an image
+			$mime_type = get_post_mime_type($attachment_id);
+			if (!str_starts_with($mime_type, 'image/')) {
+				throw new Exception('Attachment is not an image');
+			}
+
+			// Don't convert if already in target format
+			$current_mime = $this->supportedFormats[$format];
+			if ($mime_type === $current_mime) {
+				return [
+					'success' => true,
+					'message' => 'Already in target format',
+					'attachment_id' => $attachment_id,
+					'url' => wp_get_attachment_url($attachment_id)
+				];
+			}
+
+			// Generate new filename
+			$path_info = pathinfo($original_path);
+			$new_filename = $path_info['filename'] . '.' . $format;
+			$new_path = $path_info['dirname'] . '/' . $new_filename;
+
+			// Perform conversion
+			$use_imagick = $this->defaultConfig['use_imagick'];
+			$this->convertImage($original_path, $new_path, $format, $quality, $use_imagick);
+
+			if ($replace) {
+				// Replace the original file
+				$this->replaceAttachmentFile($attachment_id, $new_path, $current_mime);
+				$result_attachment_id = $attachment_id;
+			} else {
+				// Create new attachment
+				$post_id = wp_get_post_parent_id($attachment_id);
+				$result_attachment_id = $this->createAttachment(
+					$new_path,
+					get_the_title($attachment_id),
+					['user_id' => get_post_field('post_author', $attachment_id)],
+					$post_id
+				);
+			}
+
+			// Regenerate thumbnails
+			$this->generateThumbnails($result_attachment_id);
+
+			return [
+				'success' => true,
+				'attachment_id' => $result_attachment_id,
+				'url' => wp_get_attachment_url($result_attachment_id),
+				'format' => $format,
+				'file' => $new_path
+			];
+
+		} catch (Exception $e) {
+			return new WP_Error('conversion_failed', $e->getMessage());
 		}
 	}
 
-    /**
-     * Fixed convertWithGd method with better error handling
-     */
-	protected function convertWithGd(string $source, string $destination, string $toType = 'webp'): void
+	/**
+	 * Generic image conversion method
+	 */
+	protected function convertImage(string $source, string $destination, string $format, int $quality, bool $use_imagick): void
+	{
+		if ($use_imagick) {
+			$this->convertWithImagick($source, $destination, $format, $quality);
+		} else {
+			$this->convertWithGd($source, $destination, $format, $quality);
+		}
+	}
+	/**
+	 * Convert image with Imagick (supports multiple formats)
+	 */
+	protected function convertWithImagick(string $source, string $destination, string $format, int $quality): void
+	{
+		try {
+			$image = new Imagick($source);
+
+			// Set format
+			$image->setImageFormat($format);
+
+			// Set quality
+			$image->setImageCompressionQuality($quality);
+
+			// Format-specific optimizations
+			if ($format === 'webp') {
+				$image->setOption('webp:method', '6'); // Better compression
+			} elseif ($format === 'jpeg' || $format === 'jpg') {
+				$image->setImageCompression(Imagick::COMPRESSION_JPEG);
+			} elseif ($format === 'png') {
+				$image->setImageCompression(Imagick::COMPRESSION_ZIP);
+			}
+
+			$image->writeImage($destination);
+			$image->clear();
+			$image->destroy();
+		} catch (Exception $e) {
+			throw new Exception("Imagick conversion to {$format} failed: " . $e->getMessage());
+		}
+	}
+
+	/**
+	 * Convert image with GD (supports multiple formats)
+	 */
+	protected function convertWithGd(string $source, string $destination, string $format, int $quality): void
 	{
 		$mime_type = mime_content_type($source);
 
-		// Ensure destination directory exists
-		$dest_dir = dirname($destination);
-		if (!wp_mkdir_p($dest_dir)) {
-			throw new Exception("Failed to create destination directory: {$dest_dir}");
+		// Create image resource from source
+		$image = match($mime_type) {
+			'image/jpeg' => imagecreatefromjpeg($source),
+			'image/png' => imagecreatefrompng($source),
+			'image/gif' => imagecreatefromgif($source),
+			'image/webp' => imagecreatefromwebp($source),
+			default => throw new Exception('Unsupported source image type for GD conversion')
+		};
+
+		if (!$image) {
+			throw new Exception('Failed to create image resource');
 		}
 
-		switch ($mime_type) {
-			case 'image/webp':
-				$image = imagecreatefromwebp($source);
-				break;
-			case 'image/jpeg':
-				$image = imagecreatefromjpeg($source);
-				break;
-			case 'image/png':
-				$image = imagecreatefrompng($source);
-				if ($image !== false) {
-					imagepalettetotruecolor($image);
-					imagealphablending($image, true);
-					imagesavealpha($image, true);
-				}
-				break;
-			case 'image/gif':
-				$image = imagecreatefromgif($source);
-				if ($image !== false) {
-					imagepalettetotruecolor($image);
-				}
-				break;
-			default:
-				throw new Exception('Unsupported image type for GD conversion: ' . $mime_type);
-		}
+		// Convert to target format
+		$result = match($format) {
+			'webp' => imagewebp($image, $destination, $quality),
+			'jpeg', 'jpg' => imagejpeg($image, $destination, $quality),
+			'png' => imagepng($image, $destination, $this->qualityToPngCompression($quality)),
+			'gif' => imagegif($image, $destination),
+			default => throw new Exception("Unsupported target format for GD: {$format}")
+		};
 
-		if ($image === false) {
-			throw new Exception('Failed to create image resource from source file: ' . $source);
-		}
-
-
-		// Convert to WebP
-		$result = imagewebp($image, $destination, $this->config['webp_quality']);
-
-		// Clean up memory
 		imagedestroy($image);
 
 		if (!$result) {
-			throw new Exception('WebP conversion with GD failed - imagewebp returned false');
-		}
-
-		// Verify the file was actually created
-		if (!file_exists($destination)) {
-			throw new Exception('WebP file was not created despite imagewebp returning true');
+			throw new Exception("GD conversion to {$format} failed");
 		}
 	}
 
 	/**
-	 * @return void
+	 * Convert quality (1-100) to PNG compression level (0-9)
 	 */
-	public function cleanupOriginalFiles(): void
+	protected function qualityToPngCompression(int $quality): int
 	{
-		$cutoff = time() - $this->config['original_retention'];
-
-		// Get upload directory and find original directories
-		$pattern = "{$this->upload_dir}/**/originals";
-		$original_dirs = glob($pattern, GLOB_ONLYDIR);
-
-		foreach ($original_dirs as $original_dir) {
-			$files = glob("{$original_dir}/*");
-			foreach ($files as $file) {
-				if (filemtime($file) < $cutoff) {
-					unlink($file);
-				}
-			}
-		}
+		// PNG compression is inverted: 0 = no compression, 9 = max compression
+		// Quality: 100 = best quality, 1 = worst quality
+		// So: quality 100 -> compression 0, quality 1 -> compression 9
+		return (int) round((100 - $quality) / 11.11);
 	}
 
 	/**
-	 * @param string|null $temp_dir
-	 * @return void
-	 */
-	protected function cleanupTempFiles(string|null $temp_dir = null): void
-	{
-		if (is_null($temp_dir)) {
-			$temp_dir = $this->upload_dir . '/tmp';
-		}
-
-		if (is_dir($temp_dir)) {
-			$files = glob($temp_dir . '/*');
-			foreach ($files as $file) {
-				if (is_file($file)) {
-					@unlink($file);
-				}
-			}
-			@rmdir($temp_dir);
-		}
-	}
-
-
-	/**
-	 * @param int $retention_days
-	 * @return void
-	 */
-	public function cleanupUserFiles(int $retention_days = 30): void
-	{
-		$cutoff = time() - ($retention_days * DAY_IN_SECONDS);
-		$user_dir = $this->getUploadDirectory();
-
-		if (!is_dir($user_dir)) {
-			return;
-		}
-
-		$this->cleanupDirectory($user_dir, $cutoff);
-	}
-
-    /**
-	 * @param string $dir
-	 * @param int $cutoff
-	 * @return void
-	 */
-	protected function cleanupDirectory(string $dir, int $cutoff): void
-	{
-		$files = glob($dir . '/*');
-		foreach ($files as $file) {
-			if (is_dir($file)) {
-				$this->cleanupDirectory($file, $cutoff);
-			} elseif (filemtime($file) < $cutoff) {
-				@unlink($file);
-			}
-		}
-	}
-
-	/**
-	 * @param string $file
-	 * @param string $title
-	 * @param int $post_id
-	 * @return int|WP_Error
-	 * @throws Exception
-	 */
-	protected function createAttachment(string $file, string $title, int $post_id): int|WP_Error
-	{
-		$file_url = str_replace($this->upload_dir, $this->upload_url, $file);
-
-		$attachment = [
-			'post_mime_type' => mime_content_type($file),
-			'post_title' => $title,
-			'post_name' => sanitize_title($title),
-			'post_content' => '',
-			'post_status' => 'inherit',
-			'guid' => $file_url
-		];
-
-		$attach_id = wp_insert_attachment($attachment, $file, $post_id);
-		if (is_wp_error($attach_id)) {
-			throw new Exception($attach_id->get_error_message());
-		}
-
-		// Generate and set alt text
-		$alt_text = $this->generateAltText($file, get_userdata($this->user_id), $post_id);
-		update_post_meta($attach_id, '_wp_attachment_image_alt', $alt_text);
-
-		return $attach_id;
-	}
-
-	/**
-	 * Generate thumbnails using WordPress's built-in image size system
-	 * This will create all registered image sizes (thumbnail, medium, large, and any custom sizes)
-	 * Sites can register custom image sizes using add_image_size()
-	 *
-	 * @param int $attachment_id
-	 * @return void
+	 * Generate image thumbnails
 	 */
 	protected function generateThumbnails(int $attachment_id): void
 	{
@@ -642,119 +742,13 @@
 	}
 
 	/**
-	 * @param $result
-	 * @return void
+	 * Extract video thumbnail (placeholder - requires ffmpeg)
 	 */
-	protected function trackUploadStats($result)
+	protected function extractVideoThumbnail(int $attachment_id, string $video_path, int $time): void
 	{
-		$stats_key = "upload_stats_{$this->user_id}";
-		$stats = wp_cache_get($stats_key) ?: [
-			'total_uploads' => 0,
-			'successful_uploads' => 0,
-			'failed_uploads' => 0,
-			'total_size' => 0,
-			'last_upload' => null
-		];
-
-		if ($result['success']) {
-			$stats['successful_uploads']++;
-			$stats['total_size'] += filesize($result['file']);
-		} else {
-			$stats['failed_uploads']++;
-		}
-
-		$stats['total_uploads']++;
-		$stats['last_upload'] = current_time('mysql');
-
-		wp_cache_set($stats_key, $stats, '', DAY_IN_SECONDS);
+		// This would require ffmpeg or similar
+		// Placeholder for future implementation
+		apply_filters('jvb_extract_video_thumbnail', null, $attachment_id, $video_path, $time);
 	}
 
-	/**
-	 * @param int $attachment_id
-	 * @param string $new_file_path
-	 * @return void
-	 */
-	protected function updatePostAttachments(int $attachment_id, string $new_file_path): void
-	{
-		// Update attachment post
-		$file_url = str_replace($this->upload_dir, $this->upload_url, $new_file_path);
-		$filename = basename($new_file_path);
-
-		wp_update_post([
-			'ID' => $attachment_id,
-			'guid' => $file_url,
-			'post_mime_type' => mime_content_type($new_file_path),
-			'post_title' => $filename
-		]);
-
-		// Update attachment metadata
-		update_post_meta($attachment_id, '_wp_attached_file', str_replace($this->upload_dir . '/', '', $new_file_path));
-
-		// Update attachment metadata including sizes
-		$metadata = wp_get_attachment_metadata($attachment_id);
-		if ($metadata) {
-			$metadata['file'] = str_replace($this->upload_dir . '/', '', $new_file_path);
-
-			// Update thumbnail paths if they exist
-			if (!empty($metadata['sizes'])) {
-				foreach ($metadata['sizes'] as $size => $info) {
-					$old_file = $info['file'];
-					$new_file = preg_replace(
-						'/\.(jpe?g|png|gif)$/i',
-						'.webp',
-						$old_file
-					);
-					$metadata['sizes'][$size]['file'] = $new_file;
-					$metadata['sizes'][$size]['mime-type'] = 'image/webp';
-				}
-			}
-
-			wp_update_attachment_metadata($attachment_id, $metadata);
-		}
-
-		// If this is a profile/featured image, update those references
-		$post_id = wp_get_post_parent_id($attachment_id);
-		if ($post_id) {
-			$featured_image_id = get_post_thumbnail_id($post_id);
-			if ($featured_image_id === $attachment_id) {
-				// Re-set the featured image to trigger any necessary updates
-				set_post_thumbnail($post_id, $attachment_id);
-			}
-		}
-
-		// Clear any caches
-		clean_attachment_cache($attachment_id);
-		clean_post_cache($post_id);
-	}
-
-	/**
-	 * Clean up empty temporary directories
-	 *
-	 * @param int $user_id
-	 * @return void
-	 */
-	public function cleanupEmptyTempDirs(int $user_id): void
-	{
-		$temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$user_id}";
-
-		if (is_dir($temp_dir)) {
-			// Check if directory is empty
-			$files = scandir($temp_dir);
-			$is_empty = (count($files) <= 2); // Only . and .. entries
-
-			if ($is_empty) {
-				// Try to remove the empty directory
-				@rmdir($temp_dir);
-
-				// Also check if parent temp directory is empty
-				$parent_temp_dir = "{$this->upload_dir}/jvb_temp_uploads";
-				$parent_files = scandir($parent_temp_dir);
-				$parent_is_empty = (count($parent_files) <= 2); // Only . and .. entries
-
-				if ($parent_is_empty) {
-					@rmdir($parent_temp_dir);
-				}
-			}
-		}
-	}
 }

--
Gitblit v1.10.0