├── .readthedocs.yaml ├── LICENSE.txt ├── README.md ├── composer.json └── src ├── Database └── Type │ └── FileType.php ├── File ├── Path │ ├── Basepath │ │ └── DefaultTrait.php │ ├── DefaultProcessor.php │ ├── Filename │ │ └── DefaultTrait.php │ └── ProcessorInterface.php ├── Transformer │ ├── DefaultTransformer.php │ ├── SlugTransformer.php │ └── TransformerInterface.php └── Writer │ ├── DefaultWriter.php │ └── WriterInterface.php ├── Model └── Behavior │ └── UploadBehavior.php ├── UploadPlugin.php └── Validation ├── DefaultValidation.php ├── ImageValidation.php ├── Traits ├── ImageValidationTrait.php └── UploadValidationTrait.php └── UploadValidation.php /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.9" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | formats: 27 | - pdf 28 | - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: docs/requirements.txt 36 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2010 Jose Diaz-Gonzalez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/github/actions/workflow/status/FriendsOfCake/cakephp-upload/ci.yml?style=flat-square)](https://github.com/FriendsOfCake/cakephp-upload/actions?query=workflow%3ACI+branch%3Amaster) 2 | [![Coverage Status](https://img.shields.io/codecov/c/github/FriendsOfCake/cakephp-upload/master?style=flat-square)](https://codecov.io/gh/FriendsOfCake/cakephp-upload) 3 | [![Total Downloads](https://img.shields.io/packagist/dt/josegonzalez/cakephp-upload.svg?style=flat-square)](https://packagist.org/packages/josegonzalez/cakephp-upload) 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/josegonzalez/cakephp-upload.svg?style=flat-square)](https://packagist.org/packages/josegonzalez/cakephp-upload) 5 | [![Documentation Status](https://readthedocs.org/projects/cakephp-upload/badge/?version=latest&style=flat-square)](https://readthedocs.org/projects/cakephp-upload/?badge=latest) 6 | 7 | # Upload Plugin 8 | 9 | The Upload Plugin is an attempt to easily handle file uploads with CakePHP. 10 | 11 | See [7.x branch](https://github.com/FriendsOfCake/cakephp-upload/tree/7.x) for CakePHP 4.x documentation. 12 | 13 | See [4.x branch](https://github.com/FriendsOfCake/cakephp-upload/tree/4.x) for CakePHP 3.x documentation. 14 | 15 | See [2.x branch](https://github.com/FriendsOfCake/cakephp-upload/tree/2.x) for CakePHP 2.x documentation. 16 | 17 | See [this blog post](http://josediazgonzalez.com/2015/12/05/uploading-files-and-images/) for a tutorial on using the 3.x version. 18 | 19 | ## Documentation 20 | For documentation, please see [the docs](http://cakephp-upload.readthedocs.org/en/latest/). 21 | 22 | ## License 23 | 24 | The MIT License (MIT) 25 | 26 | Copyright (c) 2010 Jose Diaz-Gonzalez 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to deal 30 | in the Software without restriction, including without limitation the rights 31 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 32 | copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in 36 | all copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 43 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 44 | THE SOFTWARE. 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "josegonzalez/cakephp-upload", 3 | "description": "CakePHP plugin to handle file uploading sans ridiculous automagic", 4 | "type": "cakephp-plugin", 5 | "keywords": ["cakephp", "upload", "files", "behavior", "orm"], 6 | "homepage": "https://github.com/FriendsOfCake/cakephp-upload", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Jose Diaz-Gonzalez", 11 | "email": "cakephp+upload@josediazgonzalez.com" 12 | } 13 | ], 14 | "require": { 15 | "cakephp/orm": "^5.0", 16 | "league/flysystem": "^3.15.1.0" 17 | }, 18 | "require-dev": { 19 | "cakephp/cakephp": "^5.0", 20 | "phpunit/phpunit": "^10.1.0", 21 | "cakephp/cakephp-codesniffer": "^5.0", 22 | "league/flysystem-memory": "^3.15", 23 | "mikey179/vfsstream": "^1.6.10", 24 | "cakephp/migrations": "^4.1" 25 | }, 26 | "scripts": { 27 | "cs-check": "phpcs --colors --parallel=16 -p src/ tests/", 28 | "cs-fix": "phpcbf --colors --parallel=16 -p src/ tests/", 29 | "phpstan": "tools/phpstan analyse", 30 | "psalm": "tools/psalm --show-info=false", 31 | "stan": [ 32 | "@phpstan", 33 | "@psalm" 34 | ], 35 | "stan-baseline": "tools/phpstan --generate-baseline", 36 | "psalm-baseline": "tools/psalm --set-baseline=psalm-baseline.xml", 37 | "stan-setup": "phive install", 38 | "test": "phpunit" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "Josegonzalez\\Upload\\": "src", 43 | "Josegonzalez\\Upload\\Test\\Fixture\\": "tests\\Fixture" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Josegonzalez\\Upload\\Test\\": "tests" 49 | } 50 | }, 51 | "config": { 52 | "allow-plugins": { 53 | "dealerdirect/phpcodesniffer-composer-installer": true 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Database/Type/FileType.php: -------------------------------------------------------------------------------- 1 | settings, 'path', $defaultPath); 26 | if (str_contains($path, '{primaryKey}')) { 27 | if ($this->entity->isNew()) { 28 | throw new LogicException('{primaryKey} substitution not allowed for new entities'); 29 | } 30 | if (is_array($this->table->getPrimaryKey())) { 31 | throw new LogicException('{primaryKey} substitution not valid for composite primary keys'); 32 | } 33 | } 34 | 35 | $replacements = [ 36 | '{model}' => $this->table->getAlias(), 37 | '{table}' => $this->table->getTable(), 38 | '{field}' => $this->field, 39 | '{year}' => date('Y'), 40 | '{month}' => date('m'), 41 | '{day}' => date('d'), 42 | '{time}' => time(), 43 | '{microtime}' => microtime(true), 44 | '{DS}' => DIRECTORY_SEPARATOR, 45 | ]; 46 | if (str_contains($path, '{primaryKey}')) { 47 | $replacements['{primaryKey}'] = $this->entity->get($this->table->getPrimaryKey()); 48 | } 49 | 50 | if (preg_match_all("/{field-value:(\w+)}/", $path, $matches)) { 51 | foreach ($matches[1] as $field) { 52 | $value = $this->entity->get($field); 53 | if ($value === null) { 54 | throw new LogicException(sprintf( 55 | 'Field value for substitution is missing: %s', 56 | $field, 57 | )); 58 | } elseif (!is_scalar($value)) { 59 | throw new LogicException(sprintf( 60 | 'Field value for substitution must be a integer, float, string or boolean: %s', 61 | $field, 62 | )); 63 | } elseif (strlen((string)$value) < 1) { 64 | throw new LogicException(sprintf( 65 | 'Field value for substitution must be non-zero in length: %s', 66 | $field, 67 | )); 68 | } 69 | 70 | $replacements[sprintf('{field-value:%s}', $field)] = $value; 71 | } 72 | } 73 | 74 | return str_replace( 75 | array_keys($replacements), 76 | array_values($replacements), 77 | $path, 78 | ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/File/Path/DefaultProcessor.php: -------------------------------------------------------------------------------- 1 | table = $table; 69 | $this->entity = $entity; 70 | $this->data = $data; 71 | $this->field = $field; 72 | $this->settings = $settings; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/File/Path/Filename/DefaultTrait.php: -------------------------------------------------------------------------------- 1 | settings, 'nameCallback'); 20 | if (is_callable($processor)) { 21 | return $processor( 22 | $this->table, 23 | $this->entity, 24 | $this->data, 25 | $this->field, 26 | $this->settings, 27 | ); 28 | } 29 | 30 | if (is_string($this->data)) { 31 | return $this->data; 32 | } 33 | 34 | return (string)$this->data->getClientFilename(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/File/Path/ProcessorInterface.php: -------------------------------------------------------------------------------- 1 | 'file.pdf', 37 | * '/tmp/path/to/file/on/disk-2' => 'file-preview.png', 38 | * ] 39 | * 40 | * @param string $filename Filename. 41 | * @return array key/value pairs of temp files mapping to their names 42 | */ 43 | public function transform(string $filename): array 44 | { 45 | return [$this->data->getStream()->getMetadata('uri') => $filename]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/File/Transformer/SlugTransformer.php: -------------------------------------------------------------------------------- 1 | 'file.pdf', 20 | * '/tmp/path/to/file/on/disk-2' => 'file-preview.png', 21 | * ] 22 | * ``` 23 | * 24 | * @param string $filename Filename. 25 | * @return array key/value pairs of temp files mapping to their names 26 | */ 27 | public function transform(string $filename): array 28 | { 29 | $ext = pathinfo($filename, PATHINFO_EXTENSION); 30 | $filename = pathinfo($filename, PATHINFO_FILENAME); 31 | 32 | $filename = Text::slug($filename, '-'); 33 | if (!empty($ext)) { 34 | $filename = $filename . '.' . $ext; 35 | } 36 | 37 | return [$this->data->getStream()->getMetadata('uri') => strtolower($filename)]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/File/Transformer/TransformerInterface.php: -------------------------------------------------------------------------------- 1 | 'file.pdf', 36 | * '/tmp/path/to/file/on/disk-2' => 'file-preview.png', 37 | * ] 38 | * 39 | * @param string $filename Filename. 40 | * @return array key/value pairs of temp files mapping to their names 41 | */ 42 | public function transform(string $filename): array; 43 | } 44 | -------------------------------------------------------------------------------- /src/File/Writer/DefaultWriter.php: -------------------------------------------------------------------------------- 1 | getFilesystem($this->field, $this->settings); 47 | $results = []; 48 | foreach ($files as $file => $path) { 49 | $results[] = $this->writeFile($filesystem, $file, $path); 50 | } 51 | 52 | return $results; 53 | } 54 | 55 | /** 56 | * Deletes a set of files to an output 57 | * 58 | * @param array $files the files being written out 59 | * @return array array of results 60 | */ 61 | public function delete(array $files): array 62 | { 63 | $filesystem = $this->getFilesystem($this->field, $this->settings); 64 | $results = []; 65 | foreach ($files as $path) { 66 | $results[] = $this->deletePath($filesystem, $path); 67 | } 68 | 69 | return $results; 70 | } 71 | 72 | /** 73 | * Writes a set of files to an output 74 | * 75 | * @param \League\Flysystem\FilesystemOperator $filesystem a filesystem wrapper 76 | * @param string $file a full path to a temp file 77 | * @param string $path that path to which the file should be written 78 | * @return bool 79 | */ 80 | public function writeFile(FilesystemOperator $filesystem, string $file, string $path): bool 81 | { 82 | // phpcs:ignore 83 | $stream = @fopen($file, 'r'); 84 | if ($stream === false) { 85 | return false; 86 | } 87 | 88 | $success = false; 89 | $tempPath = $path . '.temp'; 90 | $this->deletePath($filesystem, $tempPath); 91 | try { 92 | $filesystem->writeStream($tempPath, $stream); 93 | $this->deletePath($filesystem, $path); 94 | try { 95 | $filesystem->move($tempPath, $path); 96 | $success = true; 97 | } catch (FilesystemException) { 98 | // noop 99 | } 100 | } catch (FilesystemException) { 101 | // noop 102 | } 103 | 104 | $this->deletePath($filesystem, $tempPath); 105 | is_resource($stream) && fclose($stream); 106 | 107 | return $success; 108 | } 109 | 110 | /** 111 | * Deletes a path from a filesystem 112 | * 113 | * @param \League\Flysystem\FilesystemOperator $filesystem a filesystem writer 114 | * @param string $path the path that should be deleted 115 | * @return bool 116 | */ 117 | public function deletePath(FilesystemOperator $filesystem, string $path): bool 118 | { 119 | $success = true; 120 | try { 121 | $filesystem->delete($path); 122 | } catch (FilesystemException) { 123 | $success = false; 124 | // TODO: log this? 125 | } 126 | 127 | return $success; 128 | } 129 | 130 | /** 131 | * Retrieves a configured filesystem for the given field 132 | * 133 | * @param string $field the field for which data will be saved 134 | * @param array $settings the settings for the current field 135 | * @return \League\Flysystem\FilesystemOperator 136 | */ 137 | public function getFilesystem(string $field, array $settings = []): FilesystemOperator 138 | { 139 | $adapter = new LocalFilesystemAdapter(Hash::get($settings, 'filesystem.root', ROOT . DS)); 140 | $adapter = Hash::get($settings, 'filesystem.adapter', $adapter); 141 | if (is_callable($adapter)) { 142 | $adapter = $adapter(); 143 | } 144 | 145 | if ($adapter instanceof FilesystemAdapter) { 146 | return new Filesystem($adapter, Hash::get($settings, 'filesystem.options', [ 147 | 'visibility' => Visibility::PUBLIC, 148 | 'directory_visibility' => Visibility::PUBLIC, 149 | ])); 150 | } 151 | 152 | throw new UnexpectedValueException(sprintf('Invalid Adapter for field %s', $field)); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/File/Writer/WriterInterface.php: -------------------------------------------------------------------------------- 1 | $settings) { 45 | if (is_int($field)) { 46 | $configs[$settings] = []; 47 | } else { 48 | $configs[$field] = $settings; 49 | } 50 | } 51 | 52 | $this->setConfig($configs); 53 | $this->setConfig('className'); 54 | 55 | $schema = $this->_table->getSchema(); 56 | /** @var string $field */ 57 | foreach (array_keys($this->getConfig()) as $field) { 58 | if ($schema->hasColumn($field)) { 59 | $schema->setColumnType($field, 'upload.file'); 60 | } 61 | } 62 | $this->_table->setSchema($schema); 63 | } 64 | 65 | /** 66 | * Modifies the data being marshalled to ensure invalid upload data is not inserted 67 | * 68 | * @param \Cake\Event\EventInterface $event an event instance 69 | * @param \ArrayObject $data data being marshalled 70 | * @param \ArrayObject $options options for the current event 71 | * @return void 72 | */ 73 | public function beforeMarshal(EventInterface $event, ArrayObject $data, ArrayObject $options): void 74 | { 75 | $validator = $this->_table->getValidator(); 76 | $dataArray = $data->getArrayCopy(); 77 | /** @var string $field */ 78 | foreach (array_keys($this->getConfig(null, [])) as $field) { 79 | if (!$validator->isEmptyAllowed($field, false)) { 80 | continue; 81 | } 82 | if ( 83 | !empty($dataArray[$field]) && 84 | ($dataArray[$field] instanceof UploadedFileInterface 85 | ? $dataArray[$field]->getError() 86 | : $dataArray[$field]['error'] 87 | ) !== UPLOAD_ERR_NO_FILE 88 | ) { 89 | continue; 90 | } 91 | if (isset($data[$field])) { 92 | unset($data[$field]); 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Modifies the entity before it is saved so that uploaded file data is persisted 99 | * in the database too. 100 | * 101 | * @param \Cake\Event\EventInterface $event The beforeSave event that was fired 102 | * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved 103 | * @param \ArrayObject $options the options passed to the save method 104 | * @return void 105 | */ 106 | public function beforeSave(EventInterface $event, EntityInterface $entity, ArrayObject $options): void 107 | { 108 | foreach ($this->getConfig(null, []) as $field => $settings) { 109 | if ( 110 | in_array($field, $this->protectedFieldNames, true) 111 | || !$entity->isDirty($field) 112 | ) { 113 | continue; 114 | } 115 | 116 | $data = $entity->get($field); 117 | if (!$data instanceof UploadedFileInterface) { 118 | continue; 119 | } 120 | 121 | if ($entity->get($field)->getError() !== UPLOAD_ERR_OK) { 122 | if (Hash::get($settings, 'restoreValueOnFailure', true)) { 123 | $entity->set($field, $entity->getOriginal($field)); 124 | $entity->setDirty($field, false); 125 | } 126 | continue; 127 | } 128 | 129 | $path = $this->getPathProcessor($entity, $data, $field, $settings); 130 | $basepath = $path->basepath(); 131 | $filename = $path->filename(); 132 | $pathinfo = [ 133 | 'basepath' => $basepath, 134 | 'filename' => $filename, 135 | ]; 136 | 137 | $files = $this->constructFiles($entity, $data, $field, $settings, $pathinfo); 138 | 139 | $writer = $this->getWriter($entity, $data, $field, $settings); 140 | $success = $writer->write($files); 141 | if ((new Collection($success))->contains(false)) { 142 | return; 143 | } 144 | 145 | $entity->set($field, $filename); 146 | $entity->set(Hash::get($settings, 'fields.dir', 'dir'), $basepath); 147 | $entity->set(Hash::get($settings, 'fields.size', 'size'), $data->getSize()); 148 | $entity->set(Hash::get($settings, 'fields.type', 'type'), $data->getClientMediaType()); 149 | $entity->set(Hash::get($settings, 'fields.ext', 'ext'), pathinfo($filename, PATHINFO_EXTENSION)); 150 | } 151 | } 152 | 153 | /** 154 | * Deletes the files after the entity is deleted 155 | * 156 | * @param \Cake\Event\EventInterface $event The afterDelete event that was fired 157 | * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted 158 | * @param \ArrayObject $options the options passed to the delete method 159 | * @return void 160 | */ 161 | public function afterDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options): void 162 | { 163 | $result = true; 164 | 165 | foreach ($this->getConfig(null, []) as $field => $settings) { 166 | if ( 167 | in_array($field, $this->protectedFieldNames) 168 | || Hash::get($settings, 'keepFilesOnDelete', true) 169 | || $entity->get($field) === null 170 | ) { 171 | continue; 172 | } 173 | 174 | $dirField = Hash::get($settings, 'fields.dir', 'dir'); 175 | if ($entity->has($dirField)) { 176 | $path = $entity->get($dirField); 177 | } else { 178 | $path = $this->getPathProcessor($entity, $entity->get($field), $field, $settings)->basepath(); 179 | } 180 | 181 | $callback = Hash::get($settings, 'deleteCallback'); 182 | if ($callback && is_callable($callback)) { 183 | $files = $callback($path, $entity, $field, $settings); 184 | } else { 185 | $files = [$path . $entity->get($field)]; 186 | } 187 | 188 | $writer = $this->getWriter($entity, null, $field, $settings); 189 | $success = $writer->delete($files); 190 | 191 | if ($result && (new Collection($success))->contains(false)) { 192 | $result = false; 193 | } 194 | } 195 | 196 | $event->setResult($result); 197 | } 198 | 199 | /** 200 | * Retrieves an instance of a path processor which knows how to build paths 201 | * for a given file upload 202 | * 203 | * @param \Cake\Datasource\EntityInterface $entity an entity 204 | * @param \Psr\Http\Message\UploadedFileInterface|string $data the data being submitted for a save or the filename 205 | * @param string $field the field for which data will be saved 206 | * @param array $settings the settings for the current field 207 | * @return \Josegonzalez\Upload\File\Path\ProcessorInterface 208 | */ 209 | public function getPathProcessor( 210 | EntityInterface $entity, 211 | string|UploadedFileInterface $data, 212 | string $field, 213 | array $settings, 214 | ): ProcessorInterface { 215 | /** @var class-string<\Josegonzalez\Upload\File\Path\ProcessorInterface> $processorClass */ 216 | $processorClass = Hash::get($settings, 'pathProcessor', DefaultProcessor::class); 217 | 218 | return new $processorClass($this->_table, $entity, $data, $field, $settings); 219 | } 220 | 221 | /** 222 | * Retrieves an instance of a file writer which knows how to write files to disk 223 | * 224 | * @param \Cake\Datasource\EntityInterface $entity an entity 225 | * @param \Psr\Http\Message\UploadedFileInterface|null $data the data being submitted for a save 226 | * @param string $field the field for which data will be saved 227 | * @param array $settings the settings for the current field 228 | * @return \Josegonzalez\Upload\File\Writer\WriterInterface 229 | */ 230 | public function getWriter( 231 | EntityInterface $entity, 232 | ?UploadedFileInterface $data, 233 | string $field, 234 | array $settings, 235 | ): WriterInterface { 236 | /** @var class-string<\Josegonzalez\Upload\File\Writer\WriterInterface> $writerClass */ 237 | $writerClass = Hash::get($settings, 'writer', DefaultWriter::class); 238 | 239 | return new $writerClass($this->_table, $entity, $data, $field, $settings); 240 | } 241 | 242 | /** 243 | * Creates a set of files from the initial data and returns them as key/value 244 | * pairs, where the path on disk maps to name which each file should have. 245 | * This is done through an intermediate transformer, which should return 246 | * said array. Example: 247 | * 248 | * [ 249 | * '/tmp/path/to/file/on/disk' => 'file.pdf', 250 | * '/tmp/path/to/file/on/disk-2' => 'file-preview.png', 251 | * ] 252 | * 253 | * A user can specify a callable in the `transformer` setting, which can be 254 | * used to construct this key/value array. This processor can be used to 255 | * create the source files. 256 | * 257 | * @param \Cake\Datasource\EntityInterface $entity an entity 258 | * @param \Psr\Http\Message\UploadedFileInterface $data the data being submitted for a save 259 | * @param string $field the field for which data will be saved 260 | * @param array $settings the settings for the current field 261 | * @param array $pathinfo Path info. 262 | * @return array key/value pairs of temp files mapping to their names 263 | */ 264 | public function constructFiles( 265 | EntityInterface $entity, 266 | UploadedFileInterface $data, 267 | string $field, 268 | array $settings, 269 | array $pathinfo, 270 | ): array { 271 | $basepath = $pathinfo['basepath']; 272 | $filename = $pathinfo['filename']; 273 | 274 | $basepath = substr($basepath, -1) == DS ? $basepath : $basepath . DS; 275 | $transformerClass = Hash::get($settings, 'transformer', DefaultTransformer::class); 276 | $results = []; 277 | if (is_subclass_of($transformerClass, TransformerInterface::class)) { 278 | $transformer = new $transformerClass($this->_table, $entity, $data, $field, $settings); 279 | $results = $transformer->transform($filename); 280 | foreach ($results as $key => $value) { 281 | $results[$key] = $basepath . $value; 282 | } 283 | } elseif (is_callable($transformerClass)) { 284 | $results = $transformerClass($this->_table, $entity, $data, $field, $settings, $filename); 285 | foreach ($results as $key => $value) { 286 | $results[$key] = $basepath . $value; 287 | } 288 | } else { 289 | throw new UnexpectedValueException(sprintf( 290 | "'transformer' not set to instance of TransformerInterface: %s", 291 | $transformerClass, 292 | )); 293 | } 294 | 295 | return $results; 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/UploadPlugin.php: -------------------------------------------------------------------------------- 1 | getStream()->getMetadata('uri'); 21 | } else { 22 | // Non-file uploads also mean the height is too big 23 | if (!isset($check['tmp_name']) || !strlen($check['tmp_name'])) { 24 | return false; 25 | } 26 | $file = $check['tmp_name']; 27 | } 28 | [$imgWidth] = getimagesize($file); 29 | 30 | return $width > 0 && $imgWidth >= $width; 31 | } 32 | 33 | /** 34 | * Check that the file is below the maximum width requirement 35 | * 36 | * @param mixed $check Value to check 37 | * @param int $width Width of Image 38 | * @return bool Success 39 | */ 40 | public static function isBelowMaxWidth(mixed $check, int $width): bool 41 | { 42 | if ($check instanceof UploadedFileInterface) { 43 | $file = $check->getStream()->getMetadata('uri'); 44 | } else { 45 | // Non-file uploads also mean the height is too big 46 | if (!isset($check['tmp_name']) || !strlen($check['tmp_name'])) { 47 | return false; 48 | } 49 | 50 | $file = $check['tmp_name']; 51 | } 52 | [$imgWidth] = getimagesize($file); 53 | 54 | return $width > 0 && $imgWidth <= $width; 55 | } 56 | 57 | /** 58 | * Check that the file is above the minimum height requirement 59 | * 60 | * @param mixed $check Value to check 61 | * @param int $height Height of Image 62 | * @return bool Success 63 | */ 64 | public static function isAboveMinHeight(mixed $check, int $height): bool 65 | { 66 | if ($check instanceof UploadedFileInterface) { 67 | $file = $check->getStream()->getMetadata('uri'); 68 | } else { 69 | // Non-file uploads also mean the height is too big 70 | if (!isset($check['tmp_name']) || !strlen($check['tmp_name'])) { 71 | return false; 72 | } 73 | $file = $check['tmp_name']; 74 | } 75 | [, $imgHeight] = getimagesize($file); 76 | 77 | return $height > 0 && $imgHeight >= $height; 78 | } 79 | 80 | /** 81 | * Check that the file is below the maximum height requirement 82 | * 83 | * @param mixed $check Value to check 84 | * @param int $height Height of Image 85 | * @return bool Success 86 | */ 87 | public static function isBelowMaxHeight(mixed $check, int $height): bool 88 | { 89 | if ($check instanceof UploadedFileInterface) { 90 | $file = $check->getStream()->getMetadata('uri'); 91 | } else { 92 | // Non-file uploads also mean the height is too big 93 | if (!isset($check['tmp_name']) || !strlen($check['tmp_name'])) { 94 | return false; 95 | } 96 | $file = $check['tmp_name']; 97 | } 98 | [, $imgHeight] = getimagesize($file); 99 | 100 | return $height > 0 && $imgHeight <= $height; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Validation/Traits/UploadValidationTrait.php: -------------------------------------------------------------------------------- 1 | getError() !== UPLOAD_ERR_INI_SIZE; 22 | } 23 | 24 | return Hash::get($check, 'error') !== UPLOAD_ERR_INI_SIZE; 25 | } 26 | 27 | /** 28 | * Check that the file does not exceed the max 29 | * file size specified in the HTML Form 30 | * 31 | * @param mixed $check Value to check 32 | * @return bool Success 33 | */ 34 | public static function isUnderFormSizeLimit(mixed $check): bool 35 | { 36 | if ($check instanceof UploadedFileInterface) { 37 | return $check->getError() !== UPLOAD_ERR_FORM_SIZE; 38 | } 39 | 40 | return Hash::get($check, 'error') !== UPLOAD_ERR_FORM_SIZE; 41 | } 42 | 43 | /** 44 | * Check that the file was completely uploaded 45 | * 46 | * @param mixed $check Value to check 47 | * @return bool Success 48 | */ 49 | public static function isCompletedUpload(mixed $check): bool 50 | { 51 | if ($check instanceof UploadedFileInterface) { 52 | return $check->getError() !== UPLOAD_ERR_PARTIAL; 53 | } 54 | 55 | return Hash::get($check, 'error') !== UPLOAD_ERR_PARTIAL; 56 | } 57 | 58 | /** 59 | * Check that a file was uploaded 60 | * 61 | * @param mixed $check Value to check 62 | * @return bool Success 63 | */ 64 | public static function isFileUpload(mixed $check): bool 65 | { 66 | if ($check instanceof UploadedFileInterface) { 67 | return $check->getError() !== UPLOAD_ERR_NO_FILE; 68 | } 69 | 70 | return Hash::get($check, 'error') !== UPLOAD_ERR_NO_FILE; 71 | } 72 | 73 | /** 74 | * Check that the file was successfully written to the server 75 | * 76 | * @param mixed $check Value to check 77 | * @return bool Success 78 | */ 79 | public static function isSuccessfulWrite(mixed $check): bool 80 | { 81 | if ($check instanceof UploadedFileInterface) { 82 | return $check->getError() !== UPLOAD_ERR_CANT_WRITE; 83 | } 84 | 85 | return Hash::get($check, 'error') !== UPLOAD_ERR_CANT_WRITE; 86 | } 87 | 88 | /** 89 | * Check that the file is above the minimum file upload size 90 | * 91 | * @param mixed $check Value to check 92 | * @param int $size Minimum file size 93 | * @return bool Success 94 | */ 95 | public static function isAboveMinSize(mixed $check, int $size): bool 96 | { 97 | if ($check instanceof UploadedFileInterface) { 98 | return $check->getSize() >= $size; 99 | } 100 | 101 | return !empty($check['size']) && $check['size'] >= $size; 102 | } 103 | 104 | /** 105 | * Check that the file is below the maximum file upload size 106 | * 107 | * @param mixed $check Value to check 108 | * @param int $size Maximum file size 109 | * @return bool Success 110 | */ 111 | public static function isBelowMaxSize(mixed $check, int $size): bool 112 | { 113 | if ($check instanceof UploadedFileInterface) { 114 | return $check->getSize() <= $size; 115 | } 116 | 117 | return !empty($check['size']) && $check['size'] <= $size; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Validation/UploadValidation.php: -------------------------------------------------------------------------------- 1 |