| | |
| | | <?php |
| | | namespace JVBase\managers; |
| | | |
| | | use JVBase\JVB; |
| | | use JVBase\base\Site; |
| | | use JVBase\inc\managers\ErrorHelper; |
| | | use JVBase\Instance; |
| | | use Exception; |
| | | use Imagick; |
| | | use JVBase\managers\queue\executors\UploadExecutor; |
| | | use JVBase\managers\queue\mergers\UploadMerger; |
| | | use JVBase\managers\queue\Queue; |
| | | use JVBase\managers\queue\TypeConfig; |
| | | use WP_Error; |
| | | |
| | | if (!defined('ABSPATH')) { |
| | |
| | | } |
| | | |
| | | /** |
| | | * Flexible file upload manager for images, videos, and documents |
| | | * Supports configurable directory structures and processing options |
| | | * The goal here was mainly for membership sites, to organize files by userID, rather than the date structure. |
| | | * This way, if a user deletes their account, or performs a data request, we can simply copy their directory. |
| | | * |
| | | * I started with user name so we could search by name, but issues arose if a user changed their name/display name. So I set it to go to user_id. |
| | | * |
| | | * The general flow is: |
| | | * -> Upload, ensure files are stored in appropriate directory, usually {root}/tmp |
| | | * -> Conversion is queued with Queue, as with the /tmp cleanup |
| | | * |
| | | * Proposed directory structure: |
| | | * Site::has('membership') || Site::has('is_directory') |
| | | * -> defaults to {user_id}/{post_type}/file |
| | | * Default: |
| | | * -> defaults to {post_type}/file |
| | | * Drawback: if a post is uploaded to one post type, then used in another, the organization is slightly flawed. |
| | | * |
| | | * Also, we wanted to ensure images were converted to webp, for speed. |
| | | * -> The front-end javascript attempts to do this in browser |
| | | * -> If this couldn't be handled, it is queued for server processing |
| | | * Of course, some integrations (even google, sillies) only accept jpeg/png, so in some cases we need a jpeg fallback |
| | | * |
| | | * |
| | | */ |
| | | class UploadManager |
| | | { |
| | | use Instance, ErrorHelper; |
| | | protected array $allowedTypes = [ |
| | | // Images |
| | | 'image/jpeg' => 'image', |
| | |
| | | 'original_retention' => 2592000, // 30 days |
| | | ]; |
| | | |
| | | /****************************************************************** |
| | | * UPLOAD SETTINGS |
| | | ******************************************************************/ |
| | | /** |
| | | * @var array|int[] Max size of file types |
| | | */ |
| | | protected array $maxSize = [ |
| | | 'image' => 5242880, // 5MB |
| | | 'video' => 104857600, // 100MB |
| | | 'document' => 15728640 // 15MB |
| | | ]; |
| | | //Image Specific |
| | | /** |
| | | * @var bool Whether to convert the image |
| | | */ |
| | | protected bool $convertImage = true; |
| | | /** |
| | | * @var string Default image conversion |
| | | */ |
| | | protected string $convertImageTo = 'webp'; |
| | | /** |
| | | * @var int Default image quality |
| | | */ |
| | | protected int $convertQuality = 80; |
| | | /** |
| | | * @var bool Whether to optimize images |
| | | */ |
| | | protected bool $optimizeImages = true; |
| | | /** |
| | | * @var bool Whether to create image thumbnails |
| | | */ |
| | | protected bool $createThumbnails = true; |
| | | //Video specific |
| | | /** |
| | | * @var bool Whether to automatically extract a thumbnail from a video |
| | | */ |
| | | protected bool $extractVideoThumbnail = true; |
| | | /** |
| | | * @var int Time, in microseconds, to grab thumbnail from |
| | | */ |
| | | protected int $videoThumbnailPosition = 0; |
| | | //Document specific |
| | | /** |
| | | * @var bool Whether to extract thumbnail from document. |
| | | */ |
| | | protected bool $extractDocumentThumbnail = false; |
| | | /** |
| | | * @var int How long to keep originals (default 3 months) |
| | | */ |
| | | protected int $keep_originals; |
| | | /****************************************************************** |
| | | * UPLOAD DIRECTORIES |
| | | ******************************************************************/ |
| | | /** |
| | | * @var string WordPress' base upload directory |
| | | */ |
| | | protected string $upload_dir; |
| | | /** |
| | | * @var string WordPress' base upload directory url |
| | | */ |
| | | protected string $upload_url; |
| | | public function __construct() |
| | | protected string $dir_pattern; |
| | | /***************************************************************** |
| | | * FEATURE FLAG |
| | | *****************************************************************/ |
| | | protected bool $has_imagick = false; |
| | | private function __construct() |
| | | { |
| | | $upload = wp_upload_dir(); |
| | | $this->upload_dir = $upload['basedir']; |
| | | $this->upload_url = $upload['baseurl']; |
| | | |
| | | // Check for Imagick |
| | | $this->defaultConfig['use_imagick'] = extension_loaded('imagick'); |
| | | if (Site::has('membership') || Site::has('is_directory')) { |
| | | $this->dir_pattern = '{user_id}/{post_type}'; |
| | | } else { |
| | | $this->dir_pattern = '{post_type}'; |
| | | } |
| | | $this->has_imagick = extension_loaded('imagick'); |
| | | $this->keep_originals = MONTH_IN_SECONDS * 3; |
| | | |
| | | add_action('init', [$this, 'registerUploadExecutors'], 5); |
| | | } |
| | | |
| | | /** |
| | | * Register upload operation types with the queue's TypeRegistry |
| | | */ |
| | | public function registerUploadExecutors(): void |
| | | { |
| | | $registry = Queue::getInstance()->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 |
| | | )); |
| | | } |
| | | |
| | | /** |
| | | * @param string|array $file_path |
| | | * @param array $context |
| | | * @return ?string |
| | | * @throws Exception |
| | | */ |
| | | protected function testFile(string|array $file_path, array $context):?string |
| | | { |
| | | $file_type = null; |
| | | if (is_array($file_path)) { |
| | | if (!isset($file_path['tmp_name']) || $file_path['error'] !== UPLOAD_ERR_OK) { |
| | | throw new Exception('Invalid file upload'); |
| | | } |
| | | $file_path = $file_path['tmp_name']; |
| | | } |
| | | // Validate file upload |
| | | if (!isset($file_path)) { |
| | | throw new Exception('Invalid file upload'); |
| | | } |
| | | |
| | | $mime_type = mime_content_type($file_path); |
| | | $file_type = $this->allowedTypes[$mime_type] ?? 'unknown'; |
| | | if ($file_type === 'unknown') { |
| | | throw new Exception('Unsupported file type: ' . $mime_type); |
| | | } |
| | | |
| | | // Check allowed types if specified |
| | | if (array_key_exists('allowed_types', $context) && !in_array($mime_type, $context['allowed_types'])) { |
| | | throw new Exception('File type not allowed: ' . $mime_type); |
| | | } |
| | | |
| | | return $file_type; |
| | | } |
| | | |
| | | /** |
| | |
| | | public function secureUploadedFile(array $file_data, array $context): array |
| | | { |
| | | try { |
| | | // Validate file upload |
| | | if (!isset($file_data['tmp_name']) || $file_data['error'] !== UPLOAD_ERR_OK) { |
| | | throw new Exception('Invalid file upload'); |
| | | } |
| | | $file_type = $this->testFile($file_data, $context); |
| | | |
| | | $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); |
| | | } |
| | | |
| | | // 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); |
| | | } |
| | | |
| | | $temp_dir = $this->generateDirectory($file_type, "{user_id}/temp", $context); |
| | | |
| | | // Ensure directories exist |
| | | $this->ensureDirectories($temp_dir); |
| | | $context['pattern'] = $this->dir_pattern.'/tmp'; |
| | | $temp_dir = $this->generateDirectory($file_type, $context); |
| | | |
| | | // Generate a unique filename for temporary storage |
| | | $temp_filename = uniqid() . '_' . sanitize_file_name($file_data['name']); |
| | |
| | | |
| | | // Return metadata about the stored file |
| | | return [ |
| | | 'temp_path' => $temp_path, |
| | | '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') |
| | | '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)); |
| | | $this->logError('Failed to store uploaded file', 'secureUploadedFile', ['error' => $e->getMessage()]); |
| | | return []; |
| | | } |
| | | } |
| | |
| | | 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); |
| | | $file_type = $this->testFile($temp_path, $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); |
| | | $this->validateFileSize($temp_path, $file_type, $context); |
| | | |
| | | // 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); |
| | | $directory = $this->generateDirectory($file_type, $context); |
| | | |
| | | // 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), |
| | | 'image' => $this->processImage($temp_path, $directory, $filename, $post_id, $context), |
| | | 'video' => $this->processVideo($temp_path, $directory, $filename, $post_id, $context), |
| | | 'document' => $this->processDocument($temp_path, $directory, $filename, $post_id, $context), |
| | | default => throw new Exception('Unknown file type') |
| | | }; |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log( |
| | | '[UploadManager]:processUpload', |
| | | $e->getMessage(), |
| | | ['temp_path' => $temp_path, 'context' => $context] |
| | | ); |
| | | $this->logError($e->getMessage(), 'processUpload'); |
| | | |
| | | return new WP_Error('upload_failed', $e->getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 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 |
| | |
| | | @rmdir($temp_base); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Generate directory structure based on pattern |
| | | * @param string $file_type |
| | | * @param array $config |
| | | * @return string |
| | | * @throws Exception |
| | | */ |
| | | protected function generateDirectory(string $file_type, string $pattern, array $context): string |
| | | public function generateDirectory(string $file_type, array $config): 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'), |
| | | ]; |
| | | $pattern = array_key_exists('pattern', $config) && is_string($config['pattern']) ? $config['pattern'] : $this->dir_pattern; |
| | | |
| | | $directory = str_replace(array_keys($replacements), array_values($replacements), $pattern); |
| | | $pattern = explode('/', $pattern); |
| | | $parts = []; |
| | | foreach ($pattern as $part) { |
| | | $newPart = match($part) { |
| | | '{user_id}' => $context['user_id'] ?? null, |
| | | '{post_type}', '{content}' => $context['content'] ?? null, |
| | | '{tax}', '{taxonomy}' => $context['taxonomy'] ?? null, |
| | | '{term}' => $context['term'] ?? null, |
| | | '{file_type}' => $file_type, |
| | | '{year}' => date('Y'), |
| | | '{month}' => date('m'), |
| | | '{day}' => date('d'), |
| | | default => $part |
| | | }; |
| | | if (is_null($newPart)) { |
| | | error_log('[Uploader] Pattern key was not provided for '.$part); |
| | | } |
| | | if (str_contains($newPart, '{')) { |
| | | error_log('[Uploader] Unhandled pattern found: '. $newPart); |
| | | } |
| | | $newPart = $this->sanitizeFileName($newPart); |
| | | $parts[] = $newPart; |
| | | } |
| | | //Remove any null values |
| | | $parts = array_filter($parts); |
| | | |
| | | $directory = implode('/', $parts); |
| | | |
| | | // Allow filtering |
| | | return apply_filters('jvb_upload_directory', $directory, $file_type, $context); |
| | | $directory = apply_filters('jvb_upload_directory', $directory, $file_type, $config); |
| | | |
| | | // Ensure directories exist |
| | | $this->ensureDirectories($directory); |
| | | return $directory; |
| | | } |
| | | |
| | | /** |
| | |
| | | protected function ensureDirectories(string $rel_path): void |
| | | { |
| | | $dirs = [ |
| | | "{$this->upload_dir}/{$rel_path}", |
| | | "{$this->upload_dir}/{$rel_path}/originals" |
| | | "{$this->upload_dir}/{$rel_path}" |
| | | ]; |
| | | |
| | | foreach ($dirs as $dir) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Store original file |
| | | */ |
| | | protected function storeOriginal(string $tmp_file, string $directory, string $filename): string |
| | | { |
| | | $ext = pathinfo($tmp_file, PATHINFO_EXTENSION); |
| | | $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'); |
| | | } |
| | | |
| | | return $original_path; |
| | | } |
| | | |
| | | /** |
| | | * Validate file size based on type and config |
| | | * @param string $file_path |
| | | * @param string $file_type |
| | | * @param array $config |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function validateFileSize(string $file_path, string $file_type, array $config): void |
| | | { |
| | | $size = filesize($file_path); |
| | | $max_size = $config['max_size'][$file_type] ?? $this->defaultConfig['max_size'][$file_type]; |
| | | $max_size = $this->maxSize[$file_type]; |
| | | if (array_key_exists('max_size', $config)) { |
| | | if (is_int($config['max_size'])) { |
| | | $max_size = $config['max_size']; |
| | | } else if (is_array($config['max_size']) && array_key_exists($file_type, $config['max_size'])) { |
| | | $max_size = $config['max_size'][$file_type]; |
| | | } |
| | | } |
| | | |
| | | if ($size > $max_size) { |
| | | $max_mb = round($max_size / 1048576, 2); |
| | |
| | | * Process image file |
| | | * @throws Exception |
| | | */ |
| | | protected function processImage(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array |
| | | protected function processImage(string $original_path, string $directory, string $filename, int $post_id, array $context): array |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $original_mime = mime_content_type($original_path); |
| | |
| | | $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}"); |
| | | if ((array_key_exists('convert', $context) && $context['convert'] !== false) || $this->convertImage) { |
| | | $target_format = empty($context['convertTo']??'') ? $this->convertImage : $context['convertTo']; |
| | | if (!empty($target_format)) { |
| | | // Validate format |
| | | if (!isset($this->supportedFormats[$target_format])) { |
| | | error_log('[Uploader] Unsupported conversion format: '.$target_format.'... will not convert'); |
| | | } else { |
| | | // Check if conversion is needed |
| | | $target_mime = $this->supportedFormats[$target_format]; |
| | | $should_convert = ($original_mime !== $target_mime); |
| | | } |
| | | } |
| | | |
| | | // Check if conversion is needed |
| | | $target_mime = $this->supportedFormats[$target_format]; |
| | | $should_convert = ($original_mime !== $target_mime); |
| | | } |
| | | |
| | | // Convert or copy based on configuration |
| | |
| | | $final_path = "{$full_dir}/{$filename}.{$target_format}"; |
| | | |
| | | try { |
| | | |
| | | $this->convertImage( |
| | | $original_path, |
| | | $final_path, |
| | | $target_format, |
| | | $config['quality'], |
| | | $config['use_imagick'] |
| | | array_key_exists('quality', $context) && is_int($context['quality']) && $context['quality'] <= 100 ? |
| | | $context['quality'] : |
| | | $this->convertQuality, |
| | | ); |
| | | |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Image conversion failed: ' . print_r($e->getMessage(), true)); |
| | | $this->logError($e->getMessage(), 'processImage'); |
| | | // Fallback to original |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | | $final_path = "{$full_dir}/{$filename}.{$ext}"; |
| | |
| | | } |
| | | |
| | | // Generate thumbnails |
| | | if ($config['create_thumbnails']) { |
| | | $hasOption = array_key_exists('create_thumbnails', $context); |
| | | if (($hasOption && $context['create_thumbnails']) || (!$hasOption && $this->createThumbnails)) { |
| | | $this->generateThumbnails($attachment_id); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'success' => true, |
| | | 'attachment_id' => $attachment_id, |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'image', |
| | | 'format' => $target_format ?: pathinfo($final_path, PATHINFO_EXTENSION) |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'image', |
| | | 'format' => $target_format ?: pathinfo($final_path, PATHINFO_EXTENSION), |
| | | 'context' => $context |
| | | ]; |
| | | } |
| | | |
| | |
| | | * Process video file |
| | | * @throws Exception |
| | | */ |
| | | protected function processVideo(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array |
| | | protected function processVideo(string $original_path, string $directory, string $filename, int $post_id, array $context): array |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | |
| | | try { |
| | | $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Attachment creation failed: '.print_r($e->getMessage(), true)); |
| | | $this->logError($e->getMessage(), 'processVideo'); |
| | | return ['success' => false]; |
| | | } |
| | | |
| | | |
| | | // Extract thumbnail if enabled |
| | | if ($config['extract_video_thumbnail']) { |
| | | $this->extractVideoThumbnail($attachment_id, $final_path, $config['video_thumbnail_time']); |
| | | $hasOption = array_key_exists('extract_thumbnail', $context); |
| | | if (($hasOption && $context['extract_thumbnail']) || (!$hasOption && $this->extractVideoThumbnail)) { |
| | | $time = $context['thumbnail_time']??$this->videoThumbnailPosition; |
| | | $this->extractVideoThumbnail($attachment_id, $final_path, $time); |
| | | } |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'success' => true, |
| | | 'attachment_id' => $attachment_id, |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'video' |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'video', |
| | | 'context' => $context |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Process document file |
| | | */ |
| | | protected function processDocument(string $original_path, string $directory, string $filename, int $post_id, array $config, array $context): array |
| | | protected function processDocument(string $original_path, string $directory, string $filename, int $post_id, array $context): array |
| | | { |
| | | $full_dir = "{$this->upload_dir}/{$directory}"; |
| | | $ext = pathinfo($original_path, PATHINFO_EXTENSION); |
| | |
| | | try { |
| | | $attachment_id = $this->createAttachment($final_path, $filename, $context, $post_id); |
| | | } catch (Exception $e) { |
| | | JVB()->error()->log('Conversion failed: '.print_r($e->getMessage(), true)); |
| | | $this->logError($e->getMessage(), 'processDocument'); |
| | | return ['success' => false]; |
| | | } |
| | | |
| | | |
| | | return [ |
| | | 'success' => true, |
| | | 'success' => true, |
| | | 'attachment_id' => $attachment_id, |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'document' |
| | | 'url' => wp_get_attachment_url($attachment_id), |
| | | 'file' => $final_path, |
| | | 'type' => 'document', |
| | | 'context' => $context |
| | | ]; |
| | | } |
| | | |
| | | /** |
| | | * Generate SEO-friendly filename |
| | | */ |
| | | protected function generateFilename(string $original_name, array $context): string |
| | | public function generateFilename(string $original_name, array $context): string |
| | | { |
| | | $name_parts = pathinfo($original_name); |
| | | $base_name = sanitize_title($name_parts['filename']); |
| | | $base_name = $this->sanitizeFileName($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( |
| | | return $this->sanitizeFileName(apply_filters( |
| | | 'jvb_upload_filename', |
| | | "{$base_name}-{$timestamp}", |
| | | $context |
| | | ); |
| | | )); |
| | | } |
| | | |
| | | protected function sanitizeFileName(string $filename): string |
| | |
| | | * @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 |
| | | public function convertImageTo(int $attachment_id, string $format, int $quality = 80, bool $replace = false): array|WP_Error |
| | | { |
| | | try { |
| | | // Validate 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); |
| | | $this->convertImage($original_path, $new_path, $format, $quality); |
| | | |
| | | if ($replace) { |
| | | // Replace the original file |
| | |
| | | /** |
| | | * Generic image conversion method |
| | | */ |
| | | protected function convertImage(string $source, string $destination, string $format, int $quality, bool $use_imagick): void |
| | | /** |
| | | * @param string $source |
| | | * @param string $destination |
| | | * @param string $format |
| | | * @param int $quality |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function convertImage(string $source, string $destination, string $format, int $quality): void |
| | | { |
| | | if ($use_imagick) { |
| | | if ($this->has_imagick) { |
| | | $this->convertWithImagick($source, $destination, $format, $quality); |
| | | } else { |
| | | $this->convertWithGd($source, $destination, $format, $quality); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Convert image with Imagick (supports multiple formats) |
| | | * Convert image with Imagick |
| | | * @param string $source |
| | | * @param string $destination |
| | | * @param string $format |
| | | * @param int $quality |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function convertWithImagick(string $source, string $destination, string $format, int $quality): void |
| | | { |
| | |
| | | |
| | | $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) |
| | | * Convert image with GD |
| | | * @param string $source |
| | | * @param string $destination |
| | | * @param string $format |
| | | * @param int $quality |
| | | * @return void |
| | | * @throws Exception |
| | | */ |
| | | protected function convertWithGd(string $source, string $destination, string $format, int $quality): void |
| | | { |
| | |
| | | // Placeholder for future implementation |
| | | apply_filters('jvb_extract_video_thumbnail', null, $attachment_id, $video_path, $time); |
| | | } |
| | | |
| | | } |