mockApplication(); } public function testAssureMessagesSetOnInit(): void { $val = new FileValidator(); foreach (['message', 'uploadRequired', 'tooMany', 'wrongExtension', 'tooBig', 'tooSmall', 'wrongMimeType'] as $attr) { $this->assertIsString($val->$attr); } } public function testTypeSplitOnInit(): void { $val = new FileValidator(['extensions' => 'jpeg, jpg, gif']); $this->assertEquals(['jpeg', 'jpg', 'gif'], $val->extensions); $val = new FileValidator(['extensions' => 'jpeg']); $this->assertEquals(['jpeg'], $val->extensions); $val = new FileValidator(['extensions' => '']); $this->assertEquals([], $val->extensions); $val = new FileValidator(['extensions' => []]); $this->assertEquals([], $val->extensions); $val = new FileValidator(); $this->assertEquals([], $val->extensions); $val = new FileValidator(['extensions' => ['jpeg', 'exe']]); $this->assertEquals(['jpeg', 'exe'], $val->extensions); } public function testMimeTypeSplitOnInit(): void { $val = new FileValidator(['mimeTypes' => 'text/plain, image/png']); $this->assertEquals(['text/plain', 'image/png'], $val->mimeTypes); $val = new FileValidator(['mimeTypes' => 'text/plain']); $this->assertEquals(['text/plain'], $val->mimeTypes); $val = new FileValidator(['mimeTypes' => '']); $this->assertEquals([], $val->mimeTypes); $val = new FileValidator(['mimeTypes' => []]); $this->assertEquals([], $val->mimeTypes); $val = new FileValidator(); $this->assertEquals([], $val->mimeTypes); $val = new FileValidator(['mimeTypes' => ['text/plain', 'image/png']]); $this->assertEquals(['text/plain', 'image/png'], $val->mimeTypes); } public function testGetSizeLimit(): void { $size = min($this->sizeToBytes(ini_get('upload_max_filesize')), $this->sizeToBytes(ini_get('post_max_size'))); $val = new FileValidator(); $this->assertEquals($size, $val->getSizeLimit()); $val->maxSize = $size + 1; // set and test if value is overridden $this->assertEquals($size, $val->getSizeLimit()); $val->maxSize = abs($size - 1); $this->assertEquals($size - 1, $val->getSizeLimit()); $_POST['MAX_FILE_SIZE'] = $size + 1; $this->assertEquals($size - 1, $val->getSizeLimit()); $_POST['MAX_FILE_SIZE'] = abs($size - 2); $this->assertSame($_POST['MAX_FILE_SIZE'], $val->getSizeLimit()); } protected function sizeToBytes($sizeStr) { return match (substr((string) $sizeStr, -1)) { 'M', 'm' => (int) $sizeStr * 1_048_576, 'K', 'k' => (int) $sizeStr * 1024, 'G', 'g' => (int) $sizeStr * 1_073_741_824, default => (int) $sizeStr, }; } public function testValidateAttributeMultiple(): void { $val = new FileValidator([ 'maxFiles' => 2, ]); $m = FakedValidationModel::createWithAttributes(['attr_files' => 'path']); $val->validateAttribute($m, 'attr_files'); $this->assertTrue($m->hasErrors('attr_files')); $m = FakedValidationModel::createWithAttributes(['attr_files' => []]); $val->validateAttribute($m, 'attr_files'); $this->assertTrue($m->hasErrors('attr_files')); $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files'))); $m = FakedValidationModel::createWithAttributes( [ 'attr_files' => $this->createTestFiles( [ [ 'name' => 'test_up_1.txt', 'size' => 1024, ], [ 'error' => UPLOAD_ERR_NO_FILE, ], ] ), ] ); $val->validateAttribute($m, 'attr_files'); $this->assertFalse($m->hasErrors('attr_files')); $m = FakedValidationModel::createWithAttributes([ 'attr_files' => $this->createTestFiles([ [''], [''], [''], ]), ]); $val->validateAttribute($m, 'attr_files'); $this->assertTrue($m->hasErrors()); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_files')), 'you can upload at most')); $files = [ 'file_1' => [ 'name' => 'test_up_1.txt', 'size' => 1024, ], 'file_2' => [ 'name' => 'test_up_2.txt', 'size' => 1024, ] ]; $m = FakedValidationModel::createWithAttributes( [ 'attr_files' => $this->createTestFiles( $files ), ] ); $val->validateAttribute($m, 'attr_files'); $this->assertFalse($m->hasErrors()); $this->assertEquals(array_keys($m->attr_files), array_keys($files)); $val->maxFiles = 0; $m->clearErrors(); $val->validateAttribute($m, 'attr_files'); $this->assertFalse($m->hasErrors()); $m = FakedValidationModel::createWithAttributes( [ 'attr_images' => $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'name' => 'text.txt', 'size' => 1024, ], ] ), ] ); $m->setScenario('validateMultipleFiles'); $this->assertFalse($m->validate()); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_images')), 'Only files with these extensions are allowed')); $m = FakedValidationModel::createWithAttributes( [ 'attr_images' => $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], ] ), ] ); $m->setScenario('validateMultipleFiles'); $this->assertTrue($m->validate()); $m = FakedValidationModel::createWithAttributes( [ 'attr_image' => $this->createTestFiles( [ [ 'name' => 'text.txt', 'size' => 1024, ], ] ), ] ); $m->setScenario('validateFile'); $this->assertFalse($m->validate()); } public function testValidateAttribute_minFilesGreaterThanOneMaxFilesUnlimited_notError(): void { $validator = new FileValidator(['minFiles' => 2, 'maxFiles' => 0]); $model = FakedValidationModel::createWithAttributes( [ 'attr_images' => $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], ] ) ] ); $validator->validateAttribute($model, 'attr_images'); $this->assertFalse($model->hasErrors('attr_images')); } public function testValidateAttribute_minFilesTwoMaxFilesFour_notError(): void { $validator = new FileValidator(['minFiles' => 2, 'maxFiles' => 4]); $model = FakedValidationModel::createWithAttributes( [ 'attr_images' => $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], ] ) ] ); $validator->validateAttribute($model, 'attr_images'); $this->assertFalse($model->hasErrors('attr_images')); } public function testValidateAttribute_minFilesTwoMaxFilesUnlimited_hasError(): void { $validator = new FileValidator(['minFiles' => 2, 'maxFiles' => 0]); $model = FakedValidationModel::createWithAttributes( [ 'attr_images' => $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'error' => UPLOAD_ERR_NO_FILE, ], ] ) ] ); $validator->validateAttribute($model, 'attr_images'); $this->assertTrue($model->hasErrors('attr_images')); } /** * https://github.com/yiisoft/yii2/issues/19855 */ public function testValidateArrayAttributeWithMinMaxOneAndOneFile() { $validator = new FileValidator(['maxFiles' => 1, 'minFiles' => 0]); $files = $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], ] )[0]; // <-- only one file $model = FakedValidationModel::createWithAttributes(['attr_images' => [$files]]); $validator->validateAttribute($model, 'attr_images'); $this->assertFalse($model->hasErrors('attr_images')); } /** * @param array $params * @return UploadedFile[] */ protected function createTestFiles($params = []) { $rndString = function ($len = 10) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $randomString = ''; for ($i = 0; $i < $len; $i++) { $randomString .= $characters[random_int(0, strlen($characters) - 1)]; } return $randomString; }; $files = []; foreach ($params as $key => $param) { if (empty($param) && count($params) != 1) { $files[$key] = ['no instance of UploadedFile']; continue; } $name = $param['name'] ?? $rndString(); $tempName = \Yii::getAlias('@yiiunit/runtime/validators/file/tmp/') . $name; if (is_readable($tempName)) { $size = filesize($tempName); } else { $size = $param['size'] ?? random_int( 1, $this->sizeToBytes(ini_get('upload_max_filesize')) ); } $type = $param['type'] ?? 'text/plain'; $error = $param['error'] ?? UPLOAD_ERR_OK; if (count($params) == 1) { $error = empty($param) ? UPLOAD_ERR_NO_FILE : $error; return new UploadedFile([ 'name' => $name, 'tempName' => $tempName, 'type' => $type, 'size' => $size, 'error' => $error, ]); } $files[$key] = new UploadedFile([ 'name' => $name, 'tempName' => $tempName, 'type' => $type, 'size' => $size, 'error' => $error, ]); } return $files; } /** * @param $fileName * @return UploadedFile */ protected function getRealTestFile($fileName) { $filePath = \Yii::getAlias('@yiiunit/framework/validators/data/mimeType/') . $fileName; return new UploadedFile([ 'name' => $fileName, 'tempName' => $filePath, 'type' => FileHelper::getMimeType($filePath), 'size' => filesize($filePath), 'error' => UPLOAD_ERR_OK, ]); } public function testValidateAttribute(): void { // single File $val = new FileValidator(); $m = $this->createModelForAttributeTest(); $val->validateAttribute($m, 'attr_files'); $this->assertFalse($m->hasErrors()); $val->validateAttribute($m, 'attr_files_empty'); $this->assertTrue($m->hasErrors('attr_files_empty')); $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); // single File with skipOnEmpty = false $val = new FileValidator(['skipOnEmpty' => false]); $m = $this->createModelForAttributeTest(); $val->validateAttribute($m, 'attr_files'); $this->assertFalse($m->hasErrors()); $val->validateAttribute($m, 'attr_files_empty'); $this->assertTrue($m->hasErrors('attr_files_empty')); $this->assertSame($val->uploadRequired, current($m->getErrors('attr_files_empty'))); $m = $this->createModelForAttributeTest(); // too big $val = new FileValidator(['maxSize' => 128]); $val->validateAttribute($m, 'attr_files'); $this->assertTrue($m->hasErrors('attr_files')); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_files')), 'too big')); // to Small $m = $this->createModelForAttributeTest(); $val = new FileValidator(['minSize' => 2048]); $val->validateAttribute($m, 'attr_files'); $this->assertTrue($m->hasErrors('attr_files')); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_files')), 'too small')); // UPLOAD_ERR_INI_SIZE/UPLOAD_ERR_FORM_SIZE $m = $this->createModelForAttributeTest(); $val = new FileValidator(); $val->validateAttribute($m, 'attr_err_ini'); $this->assertTrue($m->hasErrors('attr_err_ini')); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_err_ini')), 'too big')); // UPLOAD_ERR_PARTIAL $m = $this->createModelForAttributeTest(); $val = new FileValidator(); $val->validateAttribute($m, 'attr_err_part'); $this->assertTrue($m->hasErrors('attr_err_part')); $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); } public function testValidateAttributeType(): void { $val = new FileValidator([ 'extensions' => 'jpeg, jpg', 'checkExtensionByMimeType' => false, ]); $m = FakedValidationModel::createWithAttributes( [ 'attr_jpg' => $this->createTestFiles([['name' => 'one.jpeg']]), 'attr_exe' => $this->createTestFiles([['name' => 'bad.exe']]), ] ); $val->validateAttribute($m, 'attr_jpg'); $this->assertFalse($m->hasErrors('attr_jpg')); $val->validateAttribute($m, 'attr_exe'); $this->assertTrue($m->hasErrors('attr_exe')); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_exe')), 'Only files with these extensions ')); } public function testValidateEmptyExtension(): void { $val = new FileValidator([ 'extensions' => ['txt', ''], 'checkExtensionByMimeType' => false, ]); $m = FakedValidationModel::createWithAttributes( [ 'attr_txt' => $this->createTestFiles([['name' => 'one.txt']]), 'attr_empty' => $this->createTestFiles([['name' => 'bad.']]), 'attr_empty2' => $this->createTestFiles([['name' => 'bad']]), ] ); $val->validateAttribute($m, 'attr_txt'); $this->assertFalse($m->hasErrors('attr_txt')); $val->validateAttribute($m, 'attr_empty'); $this->assertFalse($m->hasErrors('attr_empty')); $val->validateAttribute($m, 'attr_empty2'); $this->assertFalse($m->hasErrors('attr_empty2')); } public function testValidateAttributeDoubleType(): void { $val = new FileValidator([ 'extensions' => 'tar.gz, tar.xz', 'checkExtensionByMimeType' => false, ]); $m = FakedValidationModel::createWithAttributes( [ 'attr_tar' => $this->createTestFiles([['name' => 'one.tar.gz']]), 'attr_bar' => $this->createTestFiles([['name' => 'bad.bar.xz']]), 'attr_badtar' => $this->createTestFiles([['name' => 'badtar.xz']]), ] ); $val->validateAttribute($m, 'attr_tar'); $this->assertFalse($m->hasErrors('attr_tar')); $val->validateAttribute($m, 'attr_bar'); $this->assertTrue($m->hasErrors('attr_bar')); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_bar')), 'Only files with these extensions ')); $val->validateAttribute($m, 'attr_badtar'); $this->assertTrue($m->hasErrors('attr_badtar')); $this->assertNotFalse(stripos((string) current($m->getErrors('attr_badtar')), 'Only files with these extensions ')); } public function testIssue11012(): void { $baseName = '飛兒樂團光茫'; /** @var UploadedFile $file */ $file = $this->createTestFiles([ ['name' => $baseName . '.txt'], ]); $this->assertEquals($baseName, $file->getBaseName()); } /** * @param string $fileName * @param string $mask * @dataProvider validMimeTypes */ public function testValidateMimeTypeMaskValid($fileName, $mask): void { $validator = new FileValidator(['mimeTypes' => $mask]); $file = $this->getRealTestFile($fileName); $this->assertTrue($validator->validate($file)); } /** * @dataProvider invalidMimeTypes */ public function testValidateMimeTypeMaskInvalid(string $fileName, string $mask): void { $validator = new FileValidator(['mimeTypes' => $mask]); $file = $this->getRealTestFile($fileName); $this->assertFalse($validator->validate($file)); } public static function validMimeTypes() { $validMimeTypes = array_filter([ ['test.svg', 'image/*', 'svg'], ['test.jpg', 'image/*', 'jpg'], ['test.png', 'image/*', 'png'], ['test.png', 'IMAGE/*', 'png'], ['test.txt', 'text/*', 'txt'], ['test.xml', '*/xml', 'xml'], ['test.odt', 'application/vnd*', 'odt'], ['test.tar.xz', 'application/x-xz', 'tar.xz'], ]); # fix for bundled libmagic bug, see also https://github.com/yiisoft/yii2/issues/19925 if ((PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80122) || (PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80209)) { $v81_zx = ['test.tar.xz', 'application/octet-stream', 'tar.xz']; array_pop($validMimeTypes); $validMimeTypes[] = $v81_zx; } return $validMimeTypes; } public static function invalidMimeTypes() { return [ ['test.txt', 'image/*', 'png, jpg'], ['test.odt', 'text/*', 'txt'], ['test.xml', '*/svg+xml', 'svg'], ['test.png', 'image/x-iso9660-image', 'bmp'], ['test.svg', 'application/*', 'jpg'], ]; } /** * @param string $fileName * @param string|array $allowedExtensions * @dataProvider validMimeTypes */ public function testValidateFileByExtensionUsingMimeType($fileName, mixed $_, $allowedExtensions): void { $validator = new FileValidator(['extensions' => (array) $allowedExtensions]); $file = $this->getRealTestFile($fileName); $detectedMimeType = FileHelper::getMimeType($file->tempName, null, false); $this->assertTrue($validator->validate($file), "Mime type detected was \"$detectedMimeType\". Consider adding it to MimeTypeController::\$aliases."); } /** * @param mixed $_ * @param string|array $allowedExtensions * @dataProvider invalidMimeTypes */ public function testValidateFileByExtensionUsingMimeTypeInvalid(string $fileName, string $_, string $allowedExtensions): void { $validator = new FileValidator(['extensions' => (array) $allowedExtensions]); $file = $this->getRealTestFile($fileName); $this->assertFalse($validator->validate($file)); } protected function createModelForAttributeTest() { return FakedValidationModel::createWithAttributes( [ 'attr_files' => $this->createTestFiles([ ['name' => 'abc.jpg', 'size' => 1024, 'type' => 'image/jpeg'], ]), 'attr_files_empty' => $this->createTestFiles([[]]), 'attr_err_ini' => $this->createTestFiles([['error' => UPLOAD_ERR_INI_SIZE]]), 'attr_err_part' => $this->createTestFiles([['error' => UPLOAD_ERR_PARTIAL]]), 'attr_err_tmp' => $this->createTestFiles([['error' => UPLOAD_ERR_NO_TMP_DIR]]), 'attr_err_write' => $this->createTestFiles([['error' => UPLOAD_ERR_CANT_WRITE]]), 'attr_err_ext' => $this->createTestFiles([['error' => UPLOAD_ERR_EXTENSION]]), ] ); } public function testValidateAttributeErrPartial(): void { $m = $this->createModelForAttributeTest(); $val = new FileValidator(); $val->validateAttribute($m, 'attr_err_part'); $this->assertTrue($m->hasErrors('attr_err_part')); $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_part'))); } public function testValidateAttributeErrCantWrite(): void { $m = $this->createModelForAttributeTest(); $val = new FileValidator(); $val->validateAttribute($m, 'attr_err_write'); $this->assertTrue($m->hasErrors('attr_err_write')); $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_write'))); } public function testValidateAttributeErrExtension(): void { $m = $this->createModelForAttributeTest(); $val = new FileValidator(); $val->validateAttribute($m, 'attr_err_ext'); $this->assertTrue($m->hasErrors('attr_err_ext')); $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_ext'))); } public function testValidateAttributeErrNoTmpDir(): void { $m = $this->createModelForAttributeTest(); $val = new FileValidator(); $val->validateAttribute($m, 'attr_err_tmp'); $this->assertTrue($m->hasErrors('attr_err_tmp')); $this->assertSame(Yii::t('yii', 'File upload failed.'), current($m->getErrors('attr_err_tmp'))); } /** * @dataProvider mimeTypeCaseInsensitive */ public function testValidateMimeTypeCaseInsensitive(string $mask, string $fileMimeType, bool $expected): void { $validator = $this->getMockBuilder(\yii\validators\FileValidator::class) ->onlyMethods(['getMimeTypeByFile']) ->getMock(); $validator->method('getMimeTypeByFile')->willReturn($fileMimeType); $validator->mimeTypes = [$mask]; $file = $this->getRealTestFile('test.txt'); $this->assertEquals($expected, $validator->validate($file), sprintf('Mime type validate fail: "%s" / "%s"', $mask, $fileMimeType)); } public static function mimeTypeCaseInsensitive() { return [ ['Image/*', 'image/jp2', true], ['image/*', 'Image/jp2', true], ['application/vnd.ms-word.document.macroEnabled.12', 'application/vnd.ms-word.document.macroenabled.12', true], ['image/jxra', 'image/jxrA', true], ]; } public function testValidateTypedAttributeNoErrors() { if (version_compare(PHP_VERSION, '7.4', '<')) { $this->markTestSkipped('Requires typed properties'); } $validator = new FileValidator(['minFiles' => 0, 'maxFiles' => 2]); $file = $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ] ] ); $model = new FakedValidationTypedModel(); $model->single = $file; $model->multiple = [$file]; $validator->validateAttribute($model, 'single'); $this->assertFalse($model->hasErrors('single')); $validator->validateAttribute($model, 'multiple'); $this->assertFalse($model->hasErrors('multiple')); } public function testValidateTypedAttributeExactMinNoErrors() { if (version_compare(PHP_VERSION, '7.4', '<')) { $this->markTestSkipped('Requires typed properties'); } $validator = new FileValidator(['minFiles' => 1]); $file = $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ] ] ); $model = new FakedValidationTypedModel(); $model->single = $file; $model->multiple = [$file]; $validator->validateAttribute($model, 'single'); $this->assertFalse($model->hasErrors('single')); $validator->validateAttribute($model, 'multiple'); $this->assertFalse($model->hasErrors('multiple')); } public function testValidateTypedAttributeExactMaxNoErrors() { if (version_compare(PHP_VERSION, '7.4', '<')) { $this->markTestSkipped('Requires typed properties'); } $validator = new FileValidator(['maxFiles' => 1]); $file = $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ] ] ); $model = new FakedValidationTypedModel(); $model->single = $file; $model->multiple = [$file]; $validator->validateAttribute($model, 'single'); $this->assertFalse($model->hasErrors('single')); $validator->validateAttribute($model, 'multiple'); $this->assertFalse($model->hasErrors('multiple')); } public function testValidateTypedAttributeMinError() { if (version_compare(PHP_VERSION, '7.4', '<')) { $this->markTestSkipped('Requires typed properties'); } $validator = new FileValidator(['minFiles' => 2]); $file = $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ] ] ); $model = new FakedValidationTypedModel(); $model->single = $file; $model->multiple = [$file]; $validator->validateAttribute($model, 'single'); $this->assertTrue($model->hasErrors('single')); $validator->validateAttribute($model, 'multiple'); $this->assertTrue($model->hasErrors('multiple')); } public function testValidateTypedAttributeMaxError() { if (version_compare(PHP_VERSION, '7.4', '<')) { $this->markTestSkipped('Requires typed properties'); } $validator = new FileValidator(['maxFiles' => 1]); $files = $this->createTestFiles( [ [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ], [ 'name' => 'image.png', 'size' => 1024, 'type' => 'image/png', ] ] ); $model = new FakedValidationTypedModel(); // single attribute cannot be checked because maxFiles = 0 === no limits $model->multiple = $files; $validator->validateAttribute($model, 'multiple'); $this->assertTrue($model->hasErrors('multiple')); } }