diff --git a/app/helpers/uploads.php b/app/helpers/uploads.php new file mode 100644 index 0000000..235015b --- /dev/null +++ b/app/helpers/uploads.php @@ -0,0 +1,201 @@ +> + */ + function core_normalize_upload_files_array(?array $fileInput): array + { + if (empty($fileInput)) { + return []; + } + if (isset($fileInput['name']) && is_array($fileInput['name'])) { + $normalized = []; + foreach ($fileInput['name'] as $idx => $name) { + $normalized[] = [ + 'name' => $name, + 'type' => $fileInput['type'][$idx] ?? null, + 'tmp_name' => $fileInput['tmp_name'][$idx] ?? null, + 'error' => $fileInput['error'][$idx] ?? UPLOAD_ERR_NO_FILE, + 'size' => $fileInput['size'][$idx] ?? 0, + ]; + } + return $normalized; + } + return [$fileInput]; + } +} + +if (!function_exists('core_store_upload_files')) { + /** + * Validate and persist uploaded files according to provided options. + * + * @param array $fileInput Raw $_FILES entry (single or multiple) + * @param array $options Behavior overrides: limit, config key, validation, naming, etc. + * + * @return array Relative paths of stored files + */ + function core_store_upload_files(array $fileInput, array $options): array + { + $defaults = [ + 'limit' => 1, + 'user_id' => 0, + 'config_key' => 'uploads_path', + 'default_subdir' => 'uploads/', + 'allowed_extensions' => ['jpg', 'jpeg', 'png'], + 'allowed_mime' => ['image/jpeg', 'image/png'], + 'max_size' => 2 * 1024 * 1024, + 'name_prefix' => 'upload-', + ]; + $options = array_merge($defaults, $options); + + $stored = []; + $normalizedFiles = core_normalize_upload_files_array($fileInput); + if (empty($normalizedFiles)) { + return $stored; + } + + // Resolve filesystem + relative directories once to avoid repeated IO operations. + $relativeDir = core_upload_relative_dir($options['config_key'], $options['default_subdir']); + $absoluteDir = core_upload_absolute_dir($options['config_key'], $options['default_subdir']); + if (!is_dir($absoluteDir) && !@mkdir($absoluteDir, 0755, true) && !is_dir($absoluteDir)) { + return $stored; + } + if (!is_writable($absoluteDir)) { + return $stored; + } + + $finfo = class_exists('finfo') ? new finfo(FILEINFO_MIME_TYPE) : null; + + foreach ($normalizedFiles as $file) { + if (count($stored) >= (int)$options['limit']) { + break; + } + $error = (int)($file['error'] ?? UPLOAD_ERR_NO_FILE); + if ($error !== UPLOAD_ERR_OK) { + continue; + } + $tmpName = (string)($file['tmp_name'] ?? ''); + if ($tmpName === '' || !is_uploaded_file($tmpName)) { + continue; + } + $size = (int)($file['size'] ?? 0); + if ($size <= 0 || $size > (int)$options['max_size']) { + continue; + } + + $extension = strtolower((string)pathinfo((string)($file['name'] ?? ''), PATHINFO_EXTENSION)); + if (!in_array($extension, $options['allowed_extensions'], true)) { + continue; + } + + $mime = $finfo && $tmpName ? $finfo->file($tmpName) : null; + if ($mime && !in_array($mime, $options['allowed_mime'], true)) { + continue; + } + + $unique = $options['name_prefix'] . $options['user_id'] . '-' . bin2hex(random_bytes(4)) . '-' . time(); + $fileName = $unique . '.' . $extension; + $destPath = $absoluteDir . $fileName; + + if (!move_uploaded_file($tmpName, $destPath)) { + continue; + } + + $stored[] = $relativeDir . $fileName; + } + + return $stored; + } +} + diff --git a/public_html/index.php b/public_html/index.php index 6538d9a..b543aa4 100644 --- a/public_html/index.php +++ b/public_html/index.php @@ -29,6 +29,8 @@ use App\Core\PluginRouteRegistry; // Load the core datetime helper for all user-facing dates/times require_once APP_PATH . 'helpers/datetime.php'; +// Load shared upload utilities for any feature needing filesystem storage +require_once APP_PATH . 'helpers/uploads.php'; // Load configuration $config = ConfigLoader::loadConfig([