From a9b3b28d001941921aa70d37fdc87c758a163a44 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 05 Jun 2026 16:47:03 +0000
Subject: [PATCH] =Some hefty changes to FeedBlock. Transitioning to loading first page in php to save on extra requests. Got a bit to do yet, but I have to work on Northeh for a bit here.
---
inc/managers/UploadManager.php | 1383 +++++++++++++++++++++++++++++++-------------------------
1 files changed, 766 insertions(+), 617 deletions(-)
diff --git a/inc/managers/UploadManager.php b/inc/managers/UploadManager.php
index 6c9655a..00bc05f 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,168 @@
}
/**
- * @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
+ * Phase 1: Validate and process file on disk (no DB writes)
+ * Safe to run outside transactions — only does file I/O
*
- * @param int $user_id
- * @return void
+ * @return array Prepared file info for registerFile()
*/
- public function cleanupEmptyTempDirs(int $user_id): void
+ public function prepareFile(string $temp_path, array $context): array
{
- $temp_dir = "{$this->upload_dir}/jvb_temp_uploads/{$user_id}";
+ if (!file_exists($temp_path)) {
+ throw new Exception('Temporary file not found: ' . $temp_path);
+ }
- if (is_dir($temp_dir)) {
- // Check if directory is empty
- $files = scandir($temp_dir);
- $is_empty = (count($files) <= 2); // Only . and .. entries
+ $mime_type = mime_content_type($temp_path);
+ if (!$this->validateMimeType($temp_path, $mime_type)) {
+ throw new Exception('Invalid or potentially dangerous file type');
+ }
- if ($is_empty) {
- // Try to remove the empty directory
- @rmdir($temp_dir);
+ $file_type = $this->allowedTypes[$mime_type] ?? 'unknown';
+ if ($file_type === 'unknown') {
+ throw new Exception('Unsupported file type: ' . $mime_type);
+ }
- // 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
+ $config = array_merge($this->defaultConfig, $context['config'] ?? [], $context);
+ $post_id = $context['post_id'] ?? 0;
- if ($parent_is_empty) {
- @rmdir($parent_temp_dir);
- }
+ if (!empty($config['allowed_types']) && !in_array($mime_type, $config['allowed_types'])) {
+ throw new Exception('File type not allowed: ' . $mime_type);
+ }
+
+ $this->validateFileSize($temp_path, $file_type, $config);
+
+ $directory = $this->generateDirectory(
+ $file_type,
+ $config['directory_pattern'] ?? '{user_id}/{content}/{file_type}',
+ $context
+ );
+ $this->ensureDirectories($directory);
+
+ $original_name = $context['original_name'] ?? basename($temp_path);
+ $filename = $this->generateFilename($original_name, $context);
+ $original_path = $this->storeOriginalFromTemp($temp_path, $directory, $filename);
+
+ // File-type-specific I/O (conversion, copying) — no DB
+ $final_path = match ($file_type) {
+ 'image' => $this->prepareImage($original_path, $directory, $filename, $config),
+ 'video' => $this->prepareMedia($original_path, $directory, $filename),
+ 'document' => $this->prepareMedia($original_path, $directory, $filename),
+ default => throw new Exception('Unknown file type'),
+ };
+
+ return [
+ 'final_path' => $final_path,
+ 'file_type' => $file_type,
+ 'filename' => $filename,
+ 'post_id' => $post_id,
+ 'config' => $config,
+ 'context' => $context,
+ ];
+ }
+
+ /**
+ * Phase 2: Register processed file in WordPress (DB writes only)
+ * Quick operation — individual WP functions handle their own queries
+ *
+ * @param array $prepared Output from prepareFile()
+ * @return array Standard upload result
+ */
+ public function registerFile(array $prepared): array
+ {
+ $attachment_id = $this->createAttachment(
+ $prepared['final_path'],
+ $prepared['filename'],
+ $prepared['context'],
+ $prepared['post_id']
+ );
+
+ // Type-specific post-registration
+ if ($prepared['file_type'] === 'image') {
+ $alt_text = apply_filters('jvb_upload_alt_text', '', $prepared['context']);
+ if ($alt_text !== '') {
+ update_post_meta($attachment_id, '_wp_attachment_image_alt', $alt_text);
}
}
+
+ if ($prepared['file_type'] === 'video' && ($prepared['config']['extract_video_thumbnail'] ?? false)) {
+ $this->extractVideoThumbnail(
+ $attachment_id,
+ $prepared['final_path'],
+ $prepared['config']['video_thumbnail_time'] ?? 0
+ );
+ }
+
+ return [
+ 'success' => true,
+ 'attachment_id' => $attachment_id,
+ 'url' => wp_get_attachment_url($attachment_id),
+ 'file' => $prepared['final_path'],
+ 'type' => $prepared['file_type'],
+ ];
+ }
+
+ /**
+ * Image file I/O: convert or copy to final location
+ */
+ protected function prepareImage(string $original_path, string $directory, string $filename, array $config): string
+ {
+ $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']);
+ if (!isset($this->supportedFormats[$target_format])) {
+ throw new Exception("Unsupported conversion format: {$target_format}");
+ }
+ $should_convert = ($original_mime !== $this->supportedFormats[$target_format]);
+ }
+
+ if ($should_convert) {
+ $final_path = "{$full_dir}/{$filename}.{$target_format}";
+ try {
+ $this->convertImage($original_path, $final_path, $target_format, $config['quality'], $config['use_imagick']);
+ } catch (Exception $e) {
+ JVB()->error()->log('Image conversion failed: ' . $e->getMessage());
+ $ext = pathinfo($original_path, PATHINFO_EXTENSION);
+ $final_path = "{$full_dir}/{$filename}.{$ext}";
+ copy($original_path, $final_path);
+ }
+ } else {
+ $ext = pathinfo($original_path, PATHINFO_EXTENSION);
+ $final_path = "{$full_dir}/{$filename}.{$ext}";
+ copy($original_path, $final_path);
+ }
+
+ return $final_path;
+ }
+
+ /**
+ * Video/document file I/O: copy to final location
+ */
+ protected function prepareMedia(string $original_path, string $directory, string $filename): string
+ {
+ $full_dir = "{$this->upload_dir}/{$directory}";
+ $ext = pathinfo($original_path, PATHINFO_EXTENSION);
+ $final_path = "{$full_dir}/{$filename}.{$ext}";
+
+ copy($original_path, $final_path);
+
+ return $final_path;
}
}
--
Gitblit v1.10.0