*/ public const ALLOWED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp']; /** @var array> */ public const MIME_TO_EXTENSIONS = [ 'image/jpeg' => ['jpg', 'jpeg'], 'image/png' => ['png'], 'image/gif' => ['gif'], 'image/webp' => ['webp'], ]; /** * @return array{path: string, mime: string, extension: string} */ public static function store(UploadedFile $file, string $directory, int $maxKilobytes): array { $checked = self::assertAcceptable($file, $maxKilobytes); Storage::disk('public')->makeDirectory($directory); $name = Str::uuid()->toString().'.'.$checked['extension']; $path = Storage::disk('public')->putFileAs($directory, $file, $name); if ($path === false) { throw new RuntimeException('public disk putFileAs returned false'); } return [ 'path' => str_replace('\\', '/', $path), 'mime' => $checked['mime'], 'extension' => $checked['extension'], ]; } /** * @return array{extension: string, mime: string} */ public static function assertAcceptable(UploadedFile $file, int $maxKilobytes): array { if (! $file->isValid()) { throw ValidationException::withMessages([ 'file' => ['上传未通过校验:'.$file->getErrorMessage()], ]); } $size = (int) $file->getSize(); if ($size <= 0 || $size > $maxKilobytes * 1024) { throw ValidationException::withMessages([ 'file' => ['单张图片不能超过 '.$maxKilobytes.'KB'], ]); } $extension = UploadFilenameGuard::lastAllowedExtension( (string) $file->getClientOriginalName(), self::ALLOWED_EXTENSIONS ); $realPath = $file->getRealPath(); if ($realPath === false || ! is_readable($realPath)) { throw ValidationException::withMessages([ 'file' => ['无法读取上传文件'], ]); } $magicMime = self::mimeFromMagicBytes($realPath); if ($magicMime === null || ! self::extensionMatchesMime($extension, $magicMime)) { throw ValidationException::withMessages([ 'file' => ['不支持的文件类型'], ]); } if (class_exists(\finfo::class)) { try { $finfo = new \finfo(FILEINFO_MIME_TYPE); $detected = $finfo->file($realPath); } catch (\Throwable) { $detected = false; } if (! is_string($detected) || ! self::extensionMatchesMime($extension, $detected)) { throw ValidationException::withMessages([ 'file' => ['不支持的文件类型'], ]); } $mime = $detected; } else { $mime = $magicMime; } return [ 'extension' => $extension, 'mime' => $mime, ]; } public static function mimeFromMagicBytes(string $path): ?string { $handle = fopen($path, 'rb'); if ($handle === false) { return null; } $header = fread($handle, 16); fclose($handle); if (! is_string($header) || strlen($header) < 3) { return null; } if (str_starts_with($header, "\xFF\xD8\xFF")) { return 'image/jpeg'; } if (str_starts_with($header, "\x89PNG\r\n\x1A\n")) { return 'image/png'; } if (str_starts_with($header, 'GIF87a') || str_starts_with($header, 'GIF89a')) { return 'image/gif'; } if (strlen($header) >= 12 && str_starts_with($header, 'RIFF') && substr($header, 8, 4) === 'WEBP') { return 'image/webp'; } return null; } public static function extensionMatchesMime(string $extension, string $mime): bool { $mime = strtolower(trim($mime)); $allowed = self::MIME_TO_EXTENSIONS[$mime] ?? null; return is_array($allowed) && in_array($extension, $allowed, true); } }