diff --git a/src/File/Path/Base64Processor.php b/src/File/Path/Base64Processor.php new file mode 100644 index 00000000..33993fa7 --- /dev/null +++ b/src/File/Path/Base64Processor.php @@ -0,0 +1,31 @@ +settings, 'nameCallback', null); + $extension = Hash::get($this->settings, 'base64_extension', '.png'); + if (is_callable($processor)) { + $numberOfParameters = (new \ReflectionFunction($processor))->getNumberOfParameters(); + if ($numberOfParameters == 2) { + return $processor($this->data, $this->settings); + } + + return $processor($this->table, $this->entity, $this->data, $this->field, $this->settings); + } + + return Text::uuid() . "$extension"; + } +} diff --git a/src/File/Transformer/Base64Transformer.php b/src/File/Transformer/Base64Transformer.php new file mode 100644 index 00000000..a5373a22 --- /dev/null +++ b/src/File/Transformer/Base64Transformer.php @@ -0,0 +1,59 @@ + 'file.pdf', + * '/tmp/path/to/file/on/disk-2' => 'file-preview.png', + * ] + * + * @return array key/value pairs of temp files mapping to their names + */ + public function transform() + { + $decoded = base64_decode($this->data['data']); + file_put_contents($this->getPath(), $decoded); + + return [ + $this->getPath() => $this->data['name'], + ]; + } + + /** + * Sets the path for the file to be written + * + * @param string $path Path to write the file + * @return void + */ + public function setPath($path) + { + $this->path = $path; + } + + /** + * Returns the path where the file will be written + * + * @return string|empty + */ + public function getPath() + { + if (empty($this->path)) { + return $this->path = tempnam(sys_get_temp_dir(), 'upload'); + } + + return $this->path; + } +} diff --git a/src/Model/Behavior/UploadBehavior.php b/src/Model/Behavior/UploadBehavior.php index 876bc5ff..4876b8ae 100644 --- a/src/Model/Behavior/UploadBehavior.php +++ b/src/Model/Behavior/UploadBehavior.php @@ -87,7 +87,8 @@ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) continue; } - if (Hash::get((array)$entity->get($field), 'error') !== UPLOAD_ERR_OK) { + $uploadValidator = $this->getUploadValidator($entity, $settings, $field); + if ($uploadValidator->hasUploadFailed()) { if (Hash::get($settings, 'restoreValueOnFailure', true)) { $entity->set($field, $entity->getOriginal($field)); $entity->setDirty($field, false); @@ -99,7 +100,14 @@ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) $path = $this->getPathProcessor($entity, $data, $field, $settings); $basepath = $path->basepath(); $filename = $path->filename(); - $data['name'] = $filename; + if (is_string($data)) { + $temp = []; + $temp['name'] = $filename; + $temp['data'] = $data; + $data = $temp; + } else { + $data['name'] = $filename; + } $files = $this->constructFiles($entity, $data, $field, $settings, $basepath); $writer = $this->getWriter($entity, $data, $field, $settings); @@ -111,8 +119,10 @@ public function beforeSave(Event $event, Entity $entity, ArrayObject $options) $entity->set($field, $filename); $entity->set(Hash::get($settings, 'fields.dir', 'dir'), $basepath); - $entity->set(Hash::get($settings, 'fields.size', 'size'), $data['size']); - $entity->set(Hash::get($settings, 'fields.type', 'type'), $data['type']); + if (!isset($temp)) { + $entity->set(Hash::get($settings, 'fields.size', 'size'), $data['size']); + $entity->set(Hash::get($settings, 'fields.type', 'type'), $data['type']); + } } } @@ -205,6 +215,27 @@ public function getWriter(Entity $entity, $data, $field, $settings) )); } + /** + * Retrieves an instance of a validator that validates that the current upload has succeded + * + * @param \Cake\ORM\Entity $entity an entity + * @param array $data the data being submitted for a save + * @return \Josegonzalez\Upload\UploadValidator\UploadValidatorInterface + */ + public function getUploadValidator(Entity $entity, $settings, $field) + { + $default = 'Josegonzalez\Upload\UploadValidator\DefaultUploadValidator'; + $uploadValidatorClass = Hash::get($settings, 'uploadValidator', $default); + if (is_subclass_of($uploadValidatorClass, 'Josegonzalez\Upload\UploadValidator\UploadValidatorInterface')) { + return new $uploadValidatorClass($entity, $field); + } + + throw new UnexpectedValueException(sprintf( + "'uploadValidator' not set to instance of UploadValidatorInterface: %s", + $uploadValidatorClass + )); + } + /** * Creates a set of files from the initial data and returns them as key/value * pairs, where the path on disk maps to name which each file should have. diff --git a/src/UploadValidator/Base64UploadValidator.php b/src/UploadValidator/Base64UploadValidator.php new file mode 100644 index 00000000..2e286537 --- /dev/null +++ b/src/UploadValidator/Base64UploadValidator.php @@ -0,0 +1,19 @@ +entity->get($this->field), true); + } +} diff --git a/src/UploadValidator/DefaultUploadValidator.php b/src/UploadValidator/DefaultUploadValidator.php new file mode 100644 index 00000000..da01911d --- /dev/null +++ b/src/UploadValidator/DefaultUploadValidator.php @@ -0,0 +1,46 @@ +entity = $entity; + $this->field = $field; + } + + /** + * Check's data for any upload errors. + * pairs, where the path on disk maps to name which each file should have. + * + * @return bool `true` if upload failed + */ + public function hasUploadFailed() + { + return Hash::get((array)$this->entity->get($this->field), 'error') !== UPLOAD_ERR_OK; + } +} diff --git a/src/UploadValidator/UploadValidatorInterface.php b/src/UploadValidator/UploadValidatorInterface.php new file mode 100644 index 00000000..d6e5ef82 --- /dev/null +++ b/src/UploadValidator/UploadValidatorInterface.php @@ -0,0 +1,23 @@ +getMockBuilder('Cake\ORM\Entity')->getMock(); + $table = $this->getMockBuilder('Cake\ORM\Table')->getMock(); + $data = ['name' => 'filename']; + $field = 'field'; + $settings = []; + $processor = new Base64Processor($table, $entity, $data, $field, $settings); + $this->assertInstanceOf('Josegonzalez\Upload\File\Path\ProcessorInterface', $processor); + } + + public function testRandomFileNameDefaultExtension() + { + $entity = $this->getMockBuilder('Cake\ORM\Entity')->getMock(); + $table = $this->getMockBuilder('Cake\ORM\Table')->getMock(); + $data = ['name' => 'filename']; + $field = 'field'; + $settings = []; + $processor = new Base64Processor($table, $entity, $data, $field, $settings); + $fileName = $processor->filename(); + $found = strpos($fileName, '.png'); + $this->assertNotFalse($found); + } + + public function testRandomFileNameCustomExtension() + { + $entity = $this->getMockBuilder('Cake\ORM\Entity')->getMock(); + $table = $this->getMockBuilder('Cake\ORM\Table')->getMock(); + $data = ['name' => 'filename']; + $field = 'field'; + $settings = ['base64_extension' => '.cake']; + $processor = new Base64Processor($table, $entity, $data, $field, $settings); + $fileName = $processor->filename(); + $found = strpos($fileName, '.cake'); + $this->assertNotFalse($found); + } +} diff --git a/tests/TestCase/File/Transformer/Base64TransformerTest.php b/tests/TestCase/File/Transformer/Base64TransformerTest.php new file mode 100644 index 00000000..1fffcb76 --- /dev/null +++ b/tests/TestCase/File/Transformer/Base64TransformerTest.php @@ -0,0 +1,48 @@ +entity = $this->getMockBuilder('Cake\ORM\Entity')->getMock(); + $this->table = $this->getMockBuilder('Cake\ORM\Table')->getMock(); + $this->data = ['data' => 'Y2FrZXBocA==', 'name' => '5a2e69ff-c2c0-44c1-94a7-d791202f0067.txt']; + $this->field = 'field'; + $this->settings = []; + $this->transformer = new Base64Transformer( + $this->table, + $this->entity, + $this->data, + $this->field, + $this->settings + ); + + $this->vfs = new Vfs; + mkdir($this->vfs->path('/tmp')); + file_put_contents($this->vfs->path('/tmp/tempfile'), $this->data['data']); + } + + public function teardown() + { + unset($this->transformer); + } + + public function testTransform() + { + $this->transformer->setPath($this->vfs->path('/tmp/tempfile')); + $expected = [$this->vfs->path('/tmp/tempfile') => '5a2e69ff-c2c0-44c1-94a7-d791202f0067.txt']; + $this->assertEquals($expected, $this->transformer->transform()); + } + + public function testIsTransformerInterface() + { + $this->assertInstanceOf('Josegonzalez\Upload\File\Transformer\TransformerInterface', $this->transformer); + } +} diff --git a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php index e3aaa9aa..91a85446 100644 --- a/tests/TestCase/Model/Behavior/UploadBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/UploadBehaviorTest.php @@ -53,6 +53,10 @@ public function setup() ->setMethods([]) ->setConstructorArgs([$this->table, $this->entity, $this->dataOk, $this->field, $this->settings]) ->getMock(); + $this->uploadValidator = $this->getMockBuilder('Josegonzalez\Upload\UploadValidator\DefaultUploadValidator') + ->setMethods([]) + ->setConstructorArgs([$this->entity, $this->field]) + ->getMock(); $this->behaviorMethods = get_class_methods('Josegonzalez\Upload\Model\Behavior\UploadBehavior'); } @@ -265,6 +269,12 @@ public function testBeforeSaveUploadError() ->method('get') ->with('field') ->will($this->returnValue($this->dataError['field'])); + $behavior->expects($this->any()) + ->method('getUploadValidator') + ->will($this->returnValue($this->uploadValidator)); + $this->uploadValidator->expects($this->any()) + ->method('hasUploadFailed') + ->will($this->returnValue(true)); $this->entity->expects($this->any()) ->method('getOriginal') ->with('field') @@ -293,6 +303,12 @@ public function testBeforeSaveWriteFail() $behavior->expects($this->any()) ->method('getPathProcessor') ->will($this->returnValue($this->processor)); + $behavior->expects($this->any()) + ->method('getUploadValidator') + ->will($this->returnValue($this->uploadValidator)); + $this->uploadValidator->expects($this->any()) + ->method('hasUploadFailed') + ->will($this->returnValue(false)); $behavior->expects($this->any()) ->method('getWriter') ->will($this->returnValue($this->writer)); @@ -318,6 +334,12 @@ public function testBeforeSaveOk() ->method('get') ->with('field') ->will($this->returnValue($this->dataOk['field'])); + $behavior->expects($this->any()) + ->method('getUploadValidator') + ->will($this->returnValue($this->uploadValidator)); + $this->uploadValidator->expects($this->any()) + ->method('hasUploadFailed') + ->will($this->returnValue(false)); $behavior->expects($this->any()) ->method('getPathProcessor') ->will($this->returnValue($this->processor)); @@ -345,6 +367,12 @@ public function testBeforeSaveDoesNotRestoreOriginalValue() ->setConstructorArgs([$this->table, $this->settings]) ->getMock(); $behavior->setConfig($settings); + $behavior->expects($this->any()) + ->method('getUploadValidator') + ->will($this->returnValue($this->uploadValidator)); + $this->uploadValidator->expects($this->any()) + ->method('hasUploadFailed') + ->will($this->returnValue(true)); $this->entity->expects($this->never())->method('getOriginal'); $this->entity->expects($this->never())->method('set'); @@ -362,6 +390,12 @@ public function testBeforeSaveWithProtectedFieldName() ->setConstructorArgs([$this->table, $this->settings]) ->getMock(); $behavior->setConfig($settings); + $behavior->expects($this->any()) + ->method('getUploadValidator') + ->will($this->returnValue($this->uploadValidator)); + $this->uploadValidator->expects($this->any()) + ->method('hasUploadFailed') + ->will($this->returnValue(true)); $this->assertNull($behavior->beforeSave(new Event('fake.event'), $this->entity, new ArrayObject)); } diff --git a/tests/TestCase/UploadValidator/Base64UploadValidatorTest.php b/tests/TestCase/UploadValidator/Base64UploadValidatorTest.php new file mode 100644 index 00000000..33ba0452 --- /dev/null +++ b/tests/TestCase/UploadValidator/Base64UploadValidatorTest.php @@ -0,0 +1,41 @@ +entity = $this->getMockBuilder('Cake\ORM\Entity')->getMock(); + $this->dataOk = "Y2FrZXBocA=="; + $this->dataError = ' '; + $this->field = 'field'; + $this->base64UploadValidator = new Base64UploadValidator($this->entity, $this->field); + } + + public function testOK() + { + $this->entity->expects($this->any()) + ->method('get') + ->with('field') + ->will($this->returnValue($this->dataOk)); + $this->assertFalse($this->base64UploadValidator->hasUploadFailed()); + } + + public function testFail() + { + $this->entity->expects($this->any()) + ->method('get') + ->with('field') + ->will($this->returnValue($this->dataError)); + $this->assertTrue($this->base64UploadValidator->hasUploadFailed()); + } + + public function testIsUploadValidatorInterface() + { + $interface = 'Josegonzalez\Upload\UploadValidator\UploadValidatorInterface'; + $this->assertInstanceOf($interface, $this->base64UploadValidator); + } +} diff --git a/tests/TestCase/UploadValidator/DefaultUploadValidatorTest.php b/tests/TestCase/UploadValidator/DefaultUploadValidatorTest.php new file mode 100644 index 00000000..5817d706 --- /dev/null +++ b/tests/TestCase/UploadValidator/DefaultUploadValidatorTest.php @@ -0,0 +1,60 @@ +entity = $this->getMockBuilder('Cake\ORM\Entity')->getMock(); + $this->dataOk = [ + 'field' => [ + 'tmp_name' => 'path/to/file', + 'name' => 'derp', + 'error' => UPLOAD_ERR_OK, + 'size' => 1, + 'type' => 'text', + 'keepFilesOnDelete' => false, + 'deleteCallback' => null + ] + ]; + $this->dataError = [ + 'field' => [ + 'tmp_name' => 'path/to/file', + 'name' => 'derp', + 'error' => UPLOAD_ERR_NO_FILE, + 'size' => 0, + 'type' => '', + ] + ]; + $this->field = 'field'; + + $this->defaultUploadValidator = new DefaultUploadValidator($this->entity, $this->field); + } + + public function testOK() + { + $this->entity->expects($this->any()) + ->method('get') + ->with('field') + ->will($this->returnValue($this->dataOk['field'])); + $this->assertFalse($this->defaultUploadValidator->hasUploadFailed()); + } + + public function testFail() + { + $this->entity->expects($this->any()) + ->method('get') + ->with('field') + ->will($this->returnValue($this->dataError['field'])); + $this->assertTrue($this->defaultUploadValidator->hasUploadFailed()); + } + + public function testIsUploadValidatorInterface() + { + $interface = 'Josegonzalez\Upload\UploadValidator\UploadValidatorInterface'; + $this->assertInstanceOf($interface, $this->defaultUploadValidator); + } +} diff --git a/tests/TestCase/Validation/Base64ValidationTest.php b/tests/TestCase/Validation/Base64ValidationTest.php new file mode 100644 index 00000000..e8f520da --- /dev/null +++ b/tests/TestCase/Validation/Base64ValidationTest.php @@ -0,0 +1,27 @@ +assertTrue(Base64Validation::isMimeType($png, 'image/png')); + } + + public function testIsMimeTypeInvalid() + { + $phpCode = 'PD9waHAgZWNobyAnQ2FrZVBocCc7ID8+'; + $this->assertFalse(Base64Validation::isMimeType($phpCode, 'image/png')); + } +}