Skip to content

Commit

Permalink
[10.x] Added File Validation extensions (#49082)
Browse files Browse the repository at this point in the history
* [10.x] Added Extension Validation

* Updated Tests and Comments

* formatting

---------

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
eusonlito and taylorotwell authored Nov 22, 2023
1 parent 2967d89 commit 4ae1ef6
Show file tree
Hide file tree
Showing 7 changed files with 140 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/Illuminate/Translation/lang/en/validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
'ends_with' => 'The :attribute field must end with one of the following: :values.',
'enum' => 'The selected :attribute is invalid.',
'exists' => 'The selected :attribute is invalid.',
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
'file' => 'The :attribute field must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
Expand Down
14 changes: 14 additions & 0 deletions src/Illuminate/Validation/Concerns/ReplacesAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ protected function replaceDigitsBetween($message, $attribute, $rule, $parameters
return $this->replaceBetween($message, $attribute, $rule, $parameters);
}

/**
* Replace all place-holders for the extensions rule.
*
* @param string $message
* @param string $attribute
* @param string $rule
* @param array<int,string> $parameters
* @return string
*/
protected function replaceExtensions($message, $attribute, $rule, $parameters)
{
return str_replace(':values', implode(', ', $parameters), $message);
}

/**
* Replace all place-holders for the min rule.
*
Expand Down
21 changes: 21 additions & 0 deletions src/Illuminate/Validation/Concerns/ValidatesAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,27 @@ protected function getExtraConditions(array $segments)
return $extra;
}

/**
* Validate the extension of a file upload attribute is in a set of defined extensions.
*
* @param string $attribute
* @param mixed $value
* @param array<int, int|string> $parameters
* @return bool
*/
public function validateExtensions($attribute, $value, $parameters)
{
if (! $this->isValidFileInstance($value)) {
return false;
}

if ($this->shouldBlockPhpUpload($value, $parameters)) {
return false;
}

return in_array(strtolower($value->getClientOriginalExtension()), $parameters);
}

/**
* Validate the given value is a valid file.
*
Expand Down
25 changes: 25 additions & 0 deletions src/Illuminate/Validation/Rules/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class File implements Rule, DataAwareRule, ValidatorAwareRule
*/
protected $allowedMimetypes = [];

/**
* The extensions that the given file should match.
*
* @var array
*/
protected $allowedExtensions = [];

/**
* The minimum size in kilobytes that the file can be.
*
Expand Down Expand Up @@ -129,6 +136,20 @@ public static function types($mimetypes)
return tap(new static(), fn ($file) => $file->allowedMimetypes = (array) $mimetypes);
}


/**
* Limit the uploaded file to the given file extensions.
*
* @param string|array<int, string> $extensions
* @return $this
*/
public function extensions($extensions)
{
$this->allowedExtensions = (array) $extensions;

return $this;
}

/**
* Indicate that the uploaded file should be exactly a certain size in kilobytes.
*
Expand Down Expand Up @@ -256,6 +277,10 @@ protected function buildValidationRules()

$rules = array_merge($rules, $this->buildMimetypes());

if (! empty($this->allowedExtensions)) {
$rules[] = 'extensions:'.implode(',', array_map('strtolower', $this->allowedExtensions));
}

$rules[] = match (true) {
is_null($this->minimumFileSize) && is_null($this->maximumFileSize) => null,
is_null($this->maximumFileSize) => "min:{$this->minimumFileSize}",
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/Validation/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class Validator implements ValidatorContract
protected $fileRules = [
'Between',
'Dimensions',
'Extensions',
'File',
'Image',
'Max',
Expand Down
46 changes: 46 additions & 0 deletions tests/Validation/ValidationFileRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,52 @@ public function testMixOfMimetypesAndMimes()
);
}

public function testSingleExtension()
{
$this->fails(
File::default()->extensions('png'),
UploadedFile::fake()->createWithContent('foo', file_get_contents(__DIR__.'/fixtures/image.png')),
['validation.extensions']
);

$this->fails(
File::default()->extensions('png'),
UploadedFile::fake()->createWithContent('foo.jpg', file_get_contents(__DIR__.'/fixtures/image.png')),
['validation.extensions']
);

$this->fails(
File::default()->extensions('jpeg'),
UploadedFile::fake()->createWithContent('foo.jpg', file_get_contents(__DIR__.'/fixtures/image.png')),
['validation.extensions']
);

$this->passes(
File::default()->extensions('png'),
UploadedFile::fake()->createWithContent('foo.png', file_get_contents(__DIR__.'/fixtures/image.png')),
);
}

public function testMultipleExtensions()
{
$this->fails(
File::default()->extensions(['png', 'jpeg', 'jpg']),
UploadedFile::fake()->createWithContent('foo', file_get_contents(__DIR__.'/fixtures/image.png')),
['validation.extensions']
);

$this->fails(
File::default()->extensions(['png', 'jpeg']),
UploadedFile::fake()->createWithContent('foo.jpg', file_get_contents(__DIR__.'/fixtures/image.png')),
['validation.extensions']
);

$this->passes(
File::default()->extensions(['png', 'jpeg', 'jpg']),
UploadedFile::fake()->createWithContent('foo.png', file_get_contents(__DIR__.'/fixtures/image.png')),
);
}

public function testImage()
{
$this->fails(
Expand Down
32 changes: 32 additions & 0 deletions tests/Validation/ValidationValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4697,6 +4697,38 @@ public function testValidateMime()
$this->assertTrue($v->passes());
}

public function testValidateExtension()
{
$trans = $this->getIlluminateArrayTranslator();
$uploadedFile = [__FILE__, '', null, null, true];

$file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock();
$file->expects($this->any())->method('getClientOriginalExtension')->willReturn('pdf');
$v = new Validator($trans, ['x' => $file], ['x' => 'extensions:pdf']);
$this->assertTrue($v->passes());

$file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock();
$file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg');
$v = new Validator($trans, ['x' => $file], ['x' => 'extensions:jpg']);
$this->assertTrue($v->passes());

$file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock();
$file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg');
$v = new Validator($trans, ['x' => $file], ['x' => 'extensions:jpeg,jpg']);
$this->assertTrue($v->passes());

$file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock();
$file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpg');
$v = new Validator($trans, ['x' => $file], ['x' => 'extensions:jpeg']);
$this->assertFalse($v->passes());

$file = $this->getMockBuilder(UploadedFile::class)->onlyMethods(['guessExtension', 'getClientOriginalExtension'])->setConstructorArgs($uploadedFile)->getMock();
$file->expects($this->any())->method('guessExtension')->willReturn('jpg');
$file->expects($this->any())->method('getClientOriginalExtension')->willReturn('jpeg');
$v = new Validator($trans, ['x' => $file], ['x' => 'mimes:jpg|extensions:jpg']);
$this->assertFalse($v->passes());
}

public function testValidateMimeEnforcesPhpCheck()
{
$trans = $this->getIlluminateArrayTranslator();
Expand Down

0 comments on commit 4ae1ef6

Please sign in to comment.