Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add base64 upload #495

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/File/Path/Base64Processor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
namespace Josegonzalez\Upload\File\Path;

use Cake\Utility\Hash;
use Cake\Utility\Text;

class Base64Processor extends DefaultProcessor
{
/**
* Returns the filename for the current field/data combination.
* If a `nameCallback` is specified in settings, then that callable
* will be invoked with the current upload data.
*
* @return string
*/
public function filename()
{
$processor = Hash::get($this->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";
}
}
59 changes: 59 additions & 0 deletions src/File/Transformer/Base64Transformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
namespace Josegonzalez\Upload\File\Transformer;

class Base64Transformer extends DefaultTransformer
{
/**
* Path where the file will be writen
*
* @var string
*/
private $path;

/**
* 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.
* Example:
*
* [
* '/tmp/path/to/file/on/disk' => '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;
}
}
39 changes: 35 additions & 4 deletions src/Model/Behavior/UploadBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

if (Hash::get($settings, 'restoreValueOnFailure', true)) {
$entity->set($field, $entity->getOriginal($field));
$entity->setDirty($field, false);
Expand All @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this?

Copy link
Author

@mandricmihai mandricmihai Aug 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When upload base64 $data is just a string f.ex
Y2FrZXBocA==
It's not an array and doesn't have an name.
Could you think about a better solution to this problem?
Here is the same thing https://github.com/FriendsOfCake/cakephp-upload/pull/495/files#diff-84c66ea1e7013949dabde1157612da57R122
$data['size'] and $data['type'] are not set when upload base64, when doing regular upload they are already provided by php.
Maybe implement another 2 interface?
The if's are there just to detect that were trying to upload base64

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also because of the https://github.com/FriendsOfCake/cakephp-upload/pull/495/files#diff-84c66ea1e7013949dabde1157612da57R122
When uploading base64 we can't use the size and type features to have them set in the database

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you show how this would be used in practice? It's not clear whats going on here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean with the 2 extra interfaces?

Copy link
Author

@mandricmihai mandricmihai Aug 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

            $data = $entity->get($field);
            $path = $this->getPathProcessor($entity, $data, $field, $settings);
            $basepath = $path->basepath();
            $filename = $path->filename();

            $dataFormatter = $this->getDataFormatter($field, $filename);
            $data = $dataFormatter->formatData();
            $files = $this->constructFiles($entity, $data, $field, $settings, $basepath);

            $writer = $this->getWriter($entity, $data, $field, $settings);
            $success = $writer->write($files);

            if ((new Collection($success))->contains(false)) {
                return false;
            }

            $entity->set($field, $filename);
            $metaDataProcessor = $this->getGetMetaDataProcessor($files);
            $entity->set(Hash::get($settings, 'fields.dir', 'dir'), $basepath);
            $entity->set(Hash::get($settings, 'fields.size', 'size'), $metaDataProcessor->getSize());
            $entity->set(Hash::get($settings, 'fields.type', 'type'), $metaDataProcessor->getType());

Something like this.Maybe with some better names 😄

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could come with something better?I tried to think of a design pattern to apply, but I could only think of template or strategy but is pretty much like what we already have with the interfaces in place

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant in terms of an end user. How are you planning on using this/why should this go into the core?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using https://foliotek.github.io/Croppie/ so that the user can select his profile picture and the result is in base64.

} else {
$data['name'] = $filename;
}
$files = $this->constructFiles($entity, $data, $field, $settings, $basepath);

$writer = $this->getWriter($entity, $data, $field, $settings);
Expand All @@ -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']);
}
}
}

Expand Down Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions src/UploadValidator/Base64UploadValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
namespace Josegonzalez\Upload\UploadValidator;

use Josegonzalez\Upload\UploadValidator\DefaultUploadValidator;

class Base64UploadValidator extends DefaultUploadValidator
{

/**
* 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 !base64_decode($this->entity->get($this->field), true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can one do other validations like for e.g checking for particular mimetype / file type?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I now see the comments regarding adding methods/interfaces for checking size and mime type.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think is possible to do check on mimetype / file type without having the file already written on the disk

Copy link
Member

@ADmad ADmad Aug 15, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be possible having validation methods using the example code provided in this comment #495 (comment) ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just tried and it works. I wasn't aware of such method to determine the mimetype.

}
}
46 changes: 46 additions & 0 deletions src/UploadValidator/DefaultUploadValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
namespace Josegonzalez\Upload\UploadValidator;

use Cake\ORM\Entity;
use Cake\Utility\Hash;
use Josegonzalez\Upload\UploadValidator\UploadValidatorInterface;

class DefaultUploadValidator implements UploadValidatorInterface
{
/**
* Entity instance.
*
* @var \Cake\ORM\Entity
*/
protected $entity;

/**
* Name of field
*
* @var string
*/
protected $field;

/**
* Constructor
*
* @param \Cake\ORM\Entity $entity the entity to construct a path for.
* @param string $field the field for which data will be saved
*/
public function __construct(Entity $entity, $field)
{
$this->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;
}
}
23 changes: 23 additions & 0 deletions src/UploadValidator/UploadValidatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
namespace Josegonzalez\Upload\UploadValidator;

use Cake\ORM\Entity;

interface UploadValidatorInterface
{
/**
* Constructor.
*
* @param \Cake\ORM\Entity $entity the entity to construct a path for.
* @param string $field the field for which data will be saved
*/
public function __construct(Entity $entity, $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();
}
45 changes: 45 additions & 0 deletions tests/TestCase/File/Path/Base64ProcessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
namespace Josegonzalez\Upload\Test\TestCase\File\Path;

use Cake\TestSuite\TestCase;
use Josegonzalez\Upload\File\Path\Base64Processor;

class Base64ProcessorTest extends TestCase
{
public function testIsProcessorInterface()
{
$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);
$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);
}
}
48 changes: 48 additions & 0 deletions tests/TestCase/File/Transformer/Base64TransformerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php
namespace Josegonzalez\Upload\Test\TestCase\File\Transformer;

use Cake\ORM\Entity;
use Cake\ORM\Table;
use Cake\TestSuite\TestCase;
use Josegonzalez\Upload\File\Transformer\Base64Transformer;
use VirtualFileSystem\FileSystem as Vfs;

class Base64TransformerTest extends TestCase
{
public function setup()
{
$this->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'];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should keep in mind that using a custom array format like this makes validation tricker. Most validation methods for uploaded files won't be usable. I haven't reviewed the full PR but you need to ensure proper validation is done for this array format and someone can't sneak in whatever they want using this feature.

$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);
}
}
Loading