<?php
|
namespace JVBase\managers;
|
|
use JVBase\JVB;
|
use Exception;
|
use Imagick;
|
use WP_Error;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
/**
|
* Flexible file upload manager for images, videos, and documents
|
* Supports configurable directory structures and processing options
|
*/
|
class UploadManager
|
{
|
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 array $supportedFormats = [
|
'webp' => 'image/webp',
|
'jpeg' => 'image/jpeg',
|
'jpg' => 'image/jpeg',
|
'png' => 'image/png',
|
'gif' => 'image/gif'
|
];
|
|
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
|
];
|
|
protected string $upload_dir;
|
protected string $upload_url;
|
public 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');
|
}
|
|
/**
|
* Initial, basic processing of upload data for later processing through OperationQueue.php
|
* @param array $file_data
|
* @param array $context
|
* @return array
|
*/
|
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');
|
}
|
|
$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);
|
|
// 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}";
|
|
// 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());
|
}
|
}
|
|
/**
|
* 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}/originals"
|
];
|
|
foreach ($dirs as $dir) {
|
if (!wp_mkdir_p($dir)) {
|
throw new Exception("Failed to create directory: {$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
|
*/
|
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];
|
|
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;
|
}
|
|
// 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;
|
}
|
}
|
|
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 or copy based on configuration
|
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: ' . 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 {
|
// Keep original format
|
$ext = pathinfo($original_path, PATHINFO_EXTENSION);
|
$final_path = "{$full_dir}/{$filename}.{$ext}";
|
copy($original_path, $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 thumbnails
|
if ($config['create_thumbnails']) {
|
$this->generateThumbnails($attachment_id);
|
}
|
|
return [
|
'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)
|
];
|
}
|
|
/**
|
* Process video file
|
* @throws Exception
|
*/
|
protected function processVideo(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 video 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('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());
|
}
|
}
|
|
/**
|
* 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);
|
|
// 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');
|
}
|
|
// 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}")
|
};
|
|
imagedestroy($image);
|
|
if (!$result) {
|
throw new Exception("GD conversion to {$format} failed");
|
}
|
}
|
|
/**
|
* Convert quality (1-100) to PNG compression level (0-9)
|
*/
|
protected function qualityToPngCompression(int $quality): int
|
{
|
// 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);
|
}
|
|
/**
|
* Generate image thumbnails
|
*/
|
protected function generateThumbnails(int $attachment_id): void
|
{
|
require_once(ABSPATH . 'wp-admin/includes/image.php');
|
$metadata = wp_generate_attachment_metadata($attachment_id, get_attached_file($attachment_id));
|
wp_update_attachment_metadata($attachment_id, $metadata);
|
}
|
|
/**
|
* Extract video thumbnail (placeholder - requires ffmpeg)
|
*/
|
protected function extractVideoThumbnail(int $attachment_id, string $video_path, int $time): void
|
{
|
// This would require ffmpeg or similar
|
// Placeholder for future implementation
|
apply_filters('jvb_extract_video_thumbnail', null, $attachment_id, $video_path, $time);
|
}
|
|
|
|
|
/**
|
* Phase 1: Validate and process file on disk (no DB writes)
|
* Safe to run outside transactions — only does file I/O
|
*
|
* @return array Prepared file info for registerFile()
|
*/
|
public function prepareFile(string $temp_path, array $context): array
|
{
|
if (!file_exists($temp_path)) {
|
throw new Exception('Temporary file not found: ' . $temp_path);
|
}
|
|
$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);
|
}
|
|
$config = array_merge($this->defaultConfig, $context['config'] ?? [], $context);
|
$post_id = $context['post_id'] ?? 0;
|
|
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;
|
}
|
}
|