diff --git a/lang/en/errors.php b/lang/en/errors.php index 5e64e739..9275aa5e 100644 --- a/lang/en/errors.php +++ b/lang/en/errors.php @@ -5,6 +5,7 @@ return [ 'file' => [ 'upload' => 'The file could not be uploaded!', + 'upload_validation' => 'The uploaded file is invalid!', 'rename' => 'The file could not be renamed!', 'delete' => 'The file could not be deleted!', ], diff --git a/src/Contracts/Support/InteractsWithFilesystem.php b/src/Contracts/Support/InteractsWithFilesystem.php index 9b0474d0..fce8f7a1 100644 --- a/src/Contracts/Support/InteractsWithFilesystem.php +++ b/src/Contracts/Support/InteractsWithFilesystem.php @@ -11,7 +11,7 @@ /** * @property ?\Closure $filesystemCallback */ -interface InteractsWithFilesystem +interface InteractsWithFilesystem extends ResolvesUrl { public function filesystem(Closure $callback): static; @@ -71,5 +71,21 @@ public function canDeleteFile(Closure $callback): static; public function resolveCanDeleteFile(NovaRequest $request): bool; + public function hasUploadValidator(): bool; + + public function getUploadValidator(): ?Closure; + + /** + * Set the validation rules for the upload. + * + * @param callable|array|string ...$rules + * @return $this + */ + public function uploadRules($rules): static; + + public function getUploadRules(): array; + + public function validateUploadUsing(Closure $callback): static; + public function options(): array; } diff --git a/src/FileManager.php b/src/FileManager.php index 75a09718..0cc1f763 100644 --- a/src/FileManager.php +++ b/src/FileManager.php @@ -6,17 +6,15 @@ use BBSLab\NovaFileManager\Contracts\Services\FileManagerContract; use BBSLab\NovaFileManager\Contracts\Support\InteractsWithFilesystem; -use BBSLab\NovaFileManager\Contracts\Support\ResolvesUrl; use BBSLab\NovaFileManager\Support\Asset; use Closure; use JsonException; use Laravel\Nova\Fields\Field; use Laravel\Nova\Http\Requests\NovaRequest; -class FileManager extends Field implements InteractsWithFilesystem, ResolvesUrl +class FileManager extends Field implements InteractsWithFilesystem { use Traits\Support\InteractsWithFilesystem; - use Traits\Support\ResolvesUrl; public $component = 'nova-file-manager-field'; diff --git a/src/Filesystem/Upload/Uploader.php b/src/Filesystem/Upload/Uploader.php index d422adf1..cd04c280 100644 --- a/src/Filesystem/Upload/Uploader.php +++ b/src/Filesystem/Upload/Uploader.php @@ -8,6 +8,7 @@ use BBSLab\NovaFileManager\Events\FileUploaded; use BBSLab\NovaFileManager\Http\Requests\UploadFileRequest; use Illuminate\Http\UploadedFile; +use Illuminate\Validation\ValidationException; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; use Pion\Laravel\ChunkUpload\Handler\HandlerFactory; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; @@ -20,6 +21,12 @@ class Uploader implements UploaderContract */ public function handle(UploadFileRequest $request, string $index = 'file'): array { + if (! $request->validateUpload()) { + throw ValidationException::withMessages([ + 'file' => [__('nova-file-manager::errors.file.upload_validation')], + ]); + } + $receiver = new FileReceiver($index, $request, HandlerFactory::classFromRequest($request)); if ($receiver->isUploaded() === false) { @@ -42,6 +49,12 @@ public function handle(UploadFileRequest $request, string $index = 'file'): arra public function saveFile(UploadFileRequest $request, UploadedFile $file): array { + if (! $request->validateUpload($file, true)) { + throw ValidationException::withMessages([ + 'file' => [__('nova-file-manager::errors.file.upload_validation')], + ]); + } + $path = $request->manager()->filesystem()->putFileAs( path: $request->path, file: $file, diff --git a/src/Http/Requests/UploadFileRequest.php b/src/Http/Requests/UploadFileRequest.php index 84ab45ce..67b0b888 100644 --- a/src/Http/Requests/UploadFileRequest.php +++ b/src/Http/Requests/UploadFileRequest.php @@ -4,14 +4,16 @@ namespace BBSLab\NovaFileManager\Http\Requests; +use BBSLab\NovaFileManager\Filesystem\Support\GetID3; use BBSLab\NovaFileManager\Rules\DiskExistsRule; use BBSLab\NovaFileManager\Rules\ExistsInFilesystem; use BBSLab\NovaFileManager\Rules\FileMissingInFilesystem; +use Illuminate\Http\UploadedFile; /** * @property-read string|null $disk * @property-read string $path - * @property-read string $file + * @property-read \Illuminate\Http\UploadedFile $file */ class UploadFileRequest extends BaseRequest { @@ -25,7 +27,27 @@ public function rules(): array return [ 'disk' => ['sometimes', 'string', new DiskExistsRule()], 'path' => ['required', 'string', new ExistsInFilesystem($this)], - 'file' => ['required', 'file', new FileMissingInFilesystem($this)], + 'file' => array_merge( + ['required', 'file', new FileMissingInFilesystem($this)], + $this->element()->getUploadRules(), + ), ]; } + + public function validateUpload(?UploadedFile $file = null, bool $saving = false): bool + { + if (!$this->element()->hasUploadValidator()) { + return true; + } + + $file ??= $this->file('file'); + + return call_user_func( + $this->element()->getUploadValidator(), + $this, + $file, + (new GetID3())->analyze($file->path()), + $saving, + ); + } } diff --git a/src/NovaFileManager.php b/src/NovaFileManager.php index 96e5ff55..80cb5fd7 100644 --- a/src/NovaFileManager.php +++ b/src/NovaFileManager.php @@ -5,15 +5,13 @@ namespace BBSLab\NovaFileManager; use BBSLab\NovaFileManager\Contracts\Support\InteractsWithFilesystem; -use BBSLab\NovaFileManager\Contracts\Support\ResolvesUrl; use Illuminate\Http\Request; use Laravel\Nova\Menu\MenuSection; use Laravel\Nova\Tool; -class NovaFileManager extends Tool implements InteractsWithFilesystem, ResolvesUrl +class NovaFileManager extends Tool implements InteractsWithFilesystem { use Traits\Support\InteractsWithFilesystem; - use Traits\Support\ResolvesUrl; public function menu(Request $request): mixed { diff --git a/src/Traits/Support/InteractsWithFilesystem.php b/src/Traits/Support/InteractsWithFilesystem.php index e7646f51..963a75d3 100644 --- a/src/Traits/Support/InteractsWithFilesystem.php +++ b/src/Traits/Support/InteractsWithFilesystem.php @@ -6,10 +6,13 @@ use Closure; use Illuminate\Contracts\Filesystem\Filesystem; +use Illuminate\Contracts\Validation\Rule; use Laravel\Nova\Http\Requests\NovaRequest; trait InteractsWithFilesystem { + use ResolvesUrl; + protected ?Closure $filesystemCallback = null; protected ?Closure $showCreateFolder = null; @@ -38,6 +41,10 @@ trait InteractsWithFilesystem protected ?Closure $canDeleteFile = null; + protected array $uploadRules = []; + + protected ?Closure $uploadValidator = null; + public function filesystem(Closure $callback): static { $this->filesystemCallback = $callback; @@ -239,6 +246,45 @@ public function resolveCanDeleteFile(NovaRequest $request): bool : $this->shouldShowDeleteFile($request); } + public function hasUploadValidator(): bool + { + return $this->uploadValidator !== null && is_callable($this->uploadValidator); + } + + public function getUploadValidator(): ?Closure + { + return $this->uploadValidator; + } + + /** + * Set the validation rules for the upload. + * + * @param callable|array|string ...$rules + * @return $this + */ + public function uploadRules($rules): static + { + if ($rules instanceof Closure) { + $this->uploadRules = [$rules]; + } else { + $this->uploadRules = ($rules instanceof Rule || is_string($rules)) ? func_get_args() : $rules; + } + + return $this; + } + + public function getUploadRules(): array + { + return $this->uploadRules; + } + + public function validateUploadUsing(Closure $callback): static + { + $this->uploadValidator = $callback; + + return $this; + } + public function options(): array { return with(app(NovaRequest::class), function (NovaRequest $request) { diff --git a/tests/Feature/File/UploadFilePermissionsTest.php b/tests/Feature/File/UploadFilePermissionsTest.php index c74cc1eb..86a467f1 100644 --- a/tests/Feature/File/UploadFilePermissionsTest.php +++ b/tests/Feature/File/UploadFilePermissionsTest.php @@ -5,10 +5,12 @@ use BBSLab\NovaFileManager\Http\Requests\UploadFileRequest; use BBSLab\NovaFileManager\NovaFileManager; use Illuminate\Foundation\Auth\User; +use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; use Illuminate\Validation\ValidationException; use Laravel\Nova\Http\Requests\NovaRequest; use Laravel\Nova\Nova; +use function Pest\Laravel\actingAs; beforeEach(function () { $this->disk = 'public'; @@ -88,3 +90,61 @@ $this->performUnauthorizedUploadChecks($message); }); + +it('can validate upload', function () { + Nova::$tools = [ + NovaFileManager::make() + ->validateUploadUsing(function (UploadFileRequest $request, UploadedFile $file, array $meta, bool $saving) { + return str_contains($file->getClientOriginalName(), 'foo'); + }), + ]; + + actingAs($this->user) + ->postJson( + uri: route('nova-file-manager.files.upload'), + data: [ + 'disk' => $this->disk, + 'path' => '/', + 'file' => UploadedFile::fake()->image($path = 'image.jpeg'), + ], + ) + ->assertUnprocessable() + ->assertJsonValidationErrors([ + 'file' => [__('nova-file-manager::errors.file.upload_validation')], + ]); + + Storage::disk($this->disk)->assertMissing($path); +}); + +it('can throw a custom validation message using validateUploadUsing', function () { + $message = 'File name must contains `foo`'; + + Nova::$tools = [ + NovaFileManager::make() + ->validateUploadUsing(function (UploadFileRequest $request, UploadedFile $file, array $meta, bool $saving) use ($message) { + if (!str_contains($request->path, 'foo')) { + throw ValidationException::withMessages([ + 'file' => [$message], + ]); + } + + return true; + }), + ]; + + actingAs($this->user) + ->postJson( + uri: route('nova-file-manager.files.upload'), + data: [ + 'disk' => $this->disk, + 'path' => '/', + 'file' => UploadedFile::fake()->image($path = 'image.jpeg'), + ], + ) + ->assertUnprocessable() + ->assertJsonValidationErrors([ + 'file' => [$message], + ]); + + Storage::disk($this->disk)->assertMissing($path); +});