├── Test ├── Fixture │ ├── PostFixture.php │ ├── ArticleFixture.php │ └── AttachmentFixture.php └── Case │ └── Model │ └── Behavior │ └── UploadBehaviorTest.php ├── composer.json ├── Model ├── Attachment.php └── Behavior │ └── UploadBehavior.php ├── Config ├── Migration │ ├── 1384466066_adding_file_size_original_name.php │ └── 1384466065_create_attachments_table.php └── Schema │ └── schema.php ├── View └── Helper │ └── AttachHelper.php └── README.markdown /Test/Fixture/PostFixture.php: -------------------------------------------------------------------------------- 1 | array( 8 | 'type' => 'integer', 9 | 'null' => false, 10 | 'default' => null, 11 | 'key' => 'primary', 12 | 'collate' => null, 13 | 'comment' => '' 14 | ), 15 | 'name' => array( 16 | 'type' => 'string', 17 | 'null' => false, 18 | 'default' => null, 19 | 'key' => 'primary', 20 | 'collate' => null, 21 | 'comment' => '' 22 | ), 23 | ); 24 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cobaia/attach", 3 | "description": "Attach Plugin, that make upload easy in CakePHP 2.0 ", 4 | "type": "cakephp-plugin", 5 | "keywords": ["attach", "upload", "cakephp", "plugin", "thumbnail"], 6 | "homepage": "https://github.com/krolow/Attach", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Vinícius Krolow", 11 | "homepage": "http://krolow.com.br" 12 | } 13 | ], 14 | "require": { 15 | "php": ">=5.3.0", 16 | "composer/installers": "*", 17 | "imagine/imagine": "*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Test/Fixture/ArticleFixture.php: -------------------------------------------------------------------------------- 1 | array( 8 | 'type' => 'integer', 9 | 'null' => false, 10 | 'default' => null, 11 | 'key' => 'primary', 12 | 'collate' => null, 13 | 'comment' => '' 14 | ), 15 | 'name' => array( 16 | 'type' => 'string', 17 | 'null' => false, 18 | 'default' => null, 19 | 'key' => 'primary', 20 | 'collate' => null, 21 | 'comment' => '' 22 | ), 23 | ); 24 | } -------------------------------------------------------------------------------- /Model/Attachment.php: -------------------------------------------------------------------------------- 1 | 13 | * @license MIT License (http://www.opensource.org/licenses/mit-license.php) 14 | */ 15 | class Attachment extends AppModel { 16 | 17 | /** 18 | * Display field 19 | * 20 | * @var string 21 | */ 22 | public $displayField = 'id'; 23 | 24 | /** 25 | * Validation rules 26 | * 27 | * @var array 28 | */ 29 | public $validate = array( 30 | 'filename' => array( 31 | 'notempty' => array( 32 | 'rule' => array('notempty'), 33 | 'message' => 'Filename cannot be empty', 34 | ), 35 | ), 36 | 'model' => array( 37 | 'notempty' => array( 38 | 'rule' => array('notempty'), 39 | ), 40 | ), 41 | 'foreign_key' => array( 42 | 'numeric' => array( 43 | 'rule' => array('numeric'), 44 | ), 45 | ), 46 | ); 47 | } 48 | 49 | -------------------------------------------------------------------------------- /Config/Migration/1384466066_adding_file_size_original_name.php: -------------------------------------------------------------------------------- 1 | array( 20 | 'create_field' => array( 21 | 'attachments' => array( 22 | 'original_name' => array('type' => 'string', 'null' => false, 'default' => NULL), 23 | 'size' => array('type' => 'integer', 'null' => false, 'default' => NULL), 24 | ) 25 | ), 26 | ), 27 | 'down' => array( 28 | 'drop_field' => array( 29 | 'attachments' => array( 30 | 'original_name', 31 | 'size', 32 | ) 33 | ) 34 | ), 35 | ); 36 | 37 | 38 | /** 39 | * Before migration callback 40 | * 41 | * @param string $direction, up or down direction of migration process 42 | * @return boolean Should process continue 43 | * @access public 44 | */ 45 | public function before($direction) { 46 | return true; 47 | } 48 | 49 | /** 50 | * After migration callback 51 | * 52 | * @param string $direction, up or down direction of migration process 53 | * @return boolean Should process continue 54 | * @access public 55 | */ 56 | public function after($direction) { 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /View/Helper/AttachHelper.php: -------------------------------------------------------------------------------- 1 | 11 | * @license MIT License (http://www.opensource.org/licenses/mit-license.php) 12 | */ 13 | class AttachHelper extends AppHelper { 14 | 15 | /** 16 | * Load Helpers 17 | * 18 | * @var array 19 | */ 20 | public $helpers = array( 21 | 'Html' 22 | ); 23 | 24 | /** 25 | * Render image 26 | * 27 | * @throws RunTimeException When no model is set in data or class does not exists 28 | * 29 | * @return string 30 | */ 31 | public function image($attach, $type = null, $options = array()) { 32 | if (!isset($attach['model'])) { 33 | throw new RunTimeException('Seems that the given attach is not really from the Attachment model'); 34 | } 35 | 36 | if (!class_exists($attach['model'])) { 37 | throw new RunTimeException('Seems that there is no class for the given attach'); 38 | } 39 | 40 | $model = ClassRegistry::init($attach['model']); 41 | $path = str_replace( 42 | WWW_ROOT, 43 | '/', 44 | $model->getUploadFolder($attach['type']) 45 | ); 46 | 47 | if (!is_null($type)) { 48 | $type = $type . '.'; 49 | } 50 | 51 | return $this->Html->image($path . $type . $attach['filename'], $options); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /Test/Fixture/AttachmentFixture.php: -------------------------------------------------------------------------------- 1 | array( 8 | 'type' => 'integer', 9 | 'null' => false, 10 | 'default' => null, 11 | 'key' => 'primary', 12 | 'collate' => null, 13 | 'comment' => '' 14 | ), 15 | 'filename' => array( 16 | 'type' => 'string', 17 | 'null' => false, 18 | 'default' => null, 19 | 'length' => 150, 20 | 'collate' => 'utf8_general_ci', 21 | 'comment' => '', 22 | 'charset' => 'utf8' 23 | ), 24 | 'model' => array( 25 | 'type' => 'string', 26 | 'null' => false, 27 | 'default' => null, 28 | 'length' => 150, 29 | 'collate' => 'utf8_general_ci', 30 | 'comment' => '', 31 | 'charset' => 'utf8' 32 | ), 33 | 'foreign_key' => array( 34 | 'type' => 'integer', 35 | 'null' => false, 36 | 'default' => null, 37 | 'collate' => null, 38 | 'comment' => '' 39 | ), 40 | 'type' => array( 41 | 'type' => 'string', 42 | 'null' => false, 43 | 'default' => null, 44 | 'length' => 100, 45 | 'collate' => 'utf8_general_ci', 46 | 'comment' => '', 47 | 'charset' => 'utf8' 48 | ), 49 | 'indexes' => array( 50 | 'PRIMARY' => array('column' => 'id', 'unique' => 1) 51 | ), 52 | 'tableParameters' => array( 53 | 'charset' => 'utf8', 54 | 'collate' => 'utf8_general_ci', 55 | 'engine' => 'InnoDB' 56 | ) 57 | ); 58 | 59 | public $records = array( 60 | array( 61 | 'id' => 1, 62 | 'filename' => 'home_1.jpg', 63 | 'model' => 'Campaign', 64 | 'foreign_key' => 1, 65 | 'type' => 'home' 66 | ) 67 | ); 68 | 69 | } -------------------------------------------------------------------------------- /Config/Migration/1384466065_create_attachments_table.php: -------------------------------------------------------------------------------- 1 | array( 20 | 'create_table' => array( 21 | 'attachments' => array( 22 | 'id' => array( 23 | 'type' => 'integer', 24 | 'null' => false, 25 | 'default' => null, 26 | 'key' => 'primary' 27 | ), 28 | 'filename' => array( 29 | 'type' => 'string', 30 | 'null' => false, 31 | 'length' => 150 32 | ), 33 | 'model' => array( 34 | 'type' => 'string', 35 | 'null' => false, 36 | 'length' => 150 37 | ), 38 | 'foreign_key' => array( 39 | 'type' => 'integer', 40 | 'null' => false 41 | ), 42 | 'type' => array( 43 | 'type' => 'string', 44 | 'null' => false, 45 | 'length' => 100 46 | ), 47 | 'indexes' => array( 48 | 'PRIMARY' => array( 49 | 'column' => 'id', 50 | 'unique' => 1 51 | ), 52 | ), 53 | 'tableParameters' => array( 54 | 'charset' => 'latin1', 55 | 'collate' => 'latin1_swedish_ci', 56 | 'engine' => 'InnoDB' 57 | ), 58 | ), 59 | ), 60 | ), 61 | 'down' => array( 62 | 'drop_table' => array( 63 | 'attachments' 64 | ), 65 | ), 66 | ); 67 | 68 | 69 | /** 70 | * Before migration callback 71 | * 72 | * @param string $direction, up or down direction of migration process 73 | * @return boolean Should process continue 74 | * @access public 75 | */ 76 | public function before($direction) { 77 | return true; 78 | } 79 | 80 | /** 81 | * After migration callback 82 | * 83 | * @param string $direction, up or down direction of migration process 84 | * @return boolean Should process continue 85 | * @access public 86 | */ 87 | public function after($direction) { 88 | return true; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Config/Schema/schema.php: -------------------------------------------------------------------------------- 1 | 11 | * @license MIT License (http://www.opensource.org/licenses/mit-license.php) 12 | */ 13 | class AttachSchema extends CakeSchema 14 | { 15 | 16 | /** 17 | * Attachments table 18 | * 19 | * @var array 20 | */ 21 | public $attachments = array( 22 | 'id' => array( 23 | 'type' => 'integer', 24 | 'null' => false, 25 | 'default' => null, 26 | 'key' => 'primary', 27 | 'collate' => null, 28 | 'comment' => '' 29 | ), 30 | 'filename' => array( 31 | 'type' => 'string', 32 | 'null' => false, 33 | 'default' => null, 34 | 'length' => 150, 35 | 'collate' => 'utf8_general_ci', 36 | 'comment' => '', 37 | 'charset' => 'utf8' 38 | ), 39 | 'model' => array( 40 | 'type' => 'string', 41 | 'null' => false, 42 | 'default' => null, 43 | 'length' => 150, 44 | 'collate' => 'utf8_general_ci', 45 | 'comment' => '', 46 | 'charset' => 'utf8' 47 | ), 48 | 'foreign_key' => array( 49 | 'type' => 'integer', 50 | 'null' => false, 51 | 'default' => null, 52 | 'collate' => null, 53 | 'comment' => '' 54 | ), 55 | 'type' => array( 56 | 'type' => 'string', 57 | 'null' => false, 58 | 'default' => null, 59 | 'length' => 100, 60 | 'collate' => 'utf8_general_ci', 61 | 'comment' => '', 62 | 'charset' => 'utf8' 63 | ), 64 | 'original_name' => array( 65 | 'type' => 'string', 66 | 'null' => false, 67 | 'default' => NULL 68 | ), 69 | 'size' => array( 70 | 'type' => 'integer', 71 | 'null' => false, 72 | 'default' => NULL 73 | ), 74 | 'indexes' => array( 75 | 'PRIMARY' => array('column' => 'id', 'unique' => 1) 76 | ), 77 | 'tableParameters' => array( 78 | 'charset' => 'utf8', 79 | 'collate' => 'utf8_general_ci', 80 | 'engine' => 'InnoDB' 81 | ) 82 | ); 83 | 84 | /** 85 | * Before callback 86 | * 87 | * @param Array $event Event 88 | * 89 | * @return bool 90 | */ 91 | public function before($event = array()) { 92 | return true; 93 | } 94 | 95 | /** 96 | * After callback 97 | * 98 | * @param Array $event Event 99 | * 100 | * @return bool 101 | */ 102 | public function after($event = array()) { 103 | return true; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /Test/Case/Model/Behavior/UploadBehaviorTest.php: -------------------------------------------------------------------------------- 1 | array( 9 | 'home' => array( 10 | 'dir' => 'tmp{DS}Attach{DS}', 11 | 'thumbs' => array( 12 | 'thumb' => array( 13 | 'w' => 100, 14 | 'h' => 100, 15 | 'crop' => true 16 | ) 17 | ) 18 | ) 19 | ) 20 | ); 21 | 22 | } 23 | 24 | class Article extends CakeTestModel { 25 | 26 | public $actsAs = array( 27 | 'Attach.Upload' => array( 28 | 'thumb' => array( 29 | 'dir' => 'tmp{DS}Attach{DS}', 30 | 'thumbs' => array( 31 | 'thumb' => array( 32 | 'w' => 100, 33 | 'h' => 100, 34 | 'crop' => true 35 | ) 36 | ) 37 | ) 38 | ) 39 | ); 40 | 41 | } 42 | 43 | class UploadBehaviorTest extends CakeTestCase { 44 | 45 | /** 46 | * Folder object 47 | * 48 | * @var Folder 49 | */ 50 | public $folder; 51 | 52 | public $fixtures = array( 53 | 'plugin.attach.attachment', 54 | 'core.article' 55 | ); 56 | 57 | public function setUp() { 58 | parent::setUp(); 59 | $this->folder = new Folder(); 60 | $this->folder->create(APP . 'tmp' . DS . 'Attach'); 61 | $this->Article = ClassRegistry::init('Article'); 62 | } 63 | 64 | protected function getConfig() { 65 | return array( 66 | 'home' => array( 67 | 'dir' => 'tmp{DS}Attach{DS}', 68 | 'thumbs' => array( 69 | 'thumb' => array( 70 | 'w' => 100, 71 | 'h' => 100, 72 | 'crop' => true 73 | ) 74 | ) 75 | ) 76 | ); 77 | } 78 | 79 | public function tearDown() { 80 | unset($this->Article); 81 | $this->folder->delete(APP . 'tmp' . DS . 'Attach'); 82 | parent::tearDown(); 83 | } 84 | 85 | public function testSetup() { 86 | $this->Article-> 87 | } 88 | 89 | public function testUpload() { 90 | 91 | } 92 | 93 | public function testIfPostFileDataIsEmpty() { 94 | 95 | } 96 | 97 | public function testIfFileIsRequired() { 98 | 99 | } 100 | 101 | public function testValidateExtension() { 102 | 103 | } 104 | 105 | public function testValidateMime() { 106 | 107 | } 108 | 109 | public function testValidateSize() { 110 | 111 | } 112 | 113 | public function testValidateMaxDimensions() { 114 | 115 | } 116 | 117 | public function testValidateMinDimensions() { 118 | 119 | } 120 | 121 | public function testFileMime() { 122 | 123 | } 124 | 125 | public function testFileExtension() { 126 | 127 | } 128 | 129 | public function testUploadFolder() { 130 | 131 | } 132 | 133 | public function testIfDirectoryIsWritable() { 134 | 135 | } 136 | 137 | public function testGenerateName() { 138 | 139 | } 140 | 141 | public function testThumbCreation() { 142 | 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Attach 1.0 2 | 3 | Attach is a CakePHP 2.0 Plugin, that makes uploads a simple task! 4 | 5 | Attach contains a behavior that does everything for you, uploads your file, and resizes your images. 6 | 7 | ## Requirements 8 | 9 | - PHP 5.3 or > 10 | - CakePHP 2.0 or > 11 | 12 | ## Installation 13 | - Clone from github : in your app directory type `git clone git@github.com:krolow/Attach.git Plugin/Attach` 14 | - Download an archive from github and extract it in `app/Plugin/Attach` 15 | 16 | * If you require thumbnails for image generation, you should install the dependencies using composer, **and make sure to call the autoload of composer in your CakePHP application** 17 | 18 | 19 | ## Usage 20 | In a model that needs uploads, replace the class declaration with something similar to the following: 21 | 22 | 23 | It's important to remember that your model class can have your own fields, and it will have a extra relation with Attachment model with the fields that are upload. 24 | 25 | ```php 26 | array( 33 | 'extension' => array( 34 | 'rule' => array( 35 | 'extension', array( 36 | 'jpg', 37 | 'jpeg', 38 | 'bmp', 39 | 'gif', 40 | 'png', 41 | 'jpg' 42 | ) 43 | ), 44 | 'message' => 'File extension is not supported', 45 | 'on' => 'create' 46 | ), 47 | 'mime' => array( 48 | 'rule' => array('mime', array( 49 | 'image/jpeg', 50 | 'image/pjpeg', 51 | 'image/bmp', 52 | 'image/x-ms-bmp', 53 | 'image/gif', 54 | 'image/png' 55 | )), 56 | 'on' => 'create' 57 | ), 58 | 'size' => array( 59 | 'rule' => array('size', 2097152), 60 | 'on' => 'create' 61 | ) 62 | ), 63 | 'swf' => array( 64 | 'extension' => array( 65 | 'rule' => array( 66 | 'extension', array( 67 | 'swf', 68 | ) 69 | ), 70 | 'message' => 'File extension is not supported', 71 | 'on' => 'create' 72 | ), 73 | 'mime' => array( 74 | 'rule' => array('mime', array( 75 | 'application/x-shockwave-flash', 76 | )), 77 | 'on' => 'create' 78 | ), 79 | 'size' => array( 80 | 'rule' => array('size', 53687091200), 81 | 'on' => 'create' 82 | ) 83 | ), 84 | 'zip' => array( 85 | 'extension' => array( 86 | 'rule' => array( 87 | 'extension', array( 88 | 'zip', 89 | ) 90 | ), 91 | 'message' => 'File extension is not supported', 92 | 'on' => 'create' 93 | ), 94 | 'mime' => array( 95 | 'rule' => array('mime', array( 96 | 'application/zip', 97 | 'multipart/x-zip' 98 | )), 99 | 'on' => 'create' 100 | ), 101 | 'size' => array( 102 | 'rule' => array('size', 53687091200), 103 | 'on' => 'create' 104 | ) 105 | ), 106 | ); 107 | 108 | public $actsAs = array( 109 | 'Attach.Upload' => array( 110 | 'Attach.type' => 'Imagick', //you can choose btw Imagick or Gd to handle the thumbnails, in case you do not pass that default is GD 111 | 'swf' => array( 112 | 'dir' => 'webroot{DS}uploads{DS}media{DS}swf' 113 | ), 114 | 'image' => array( 115 | 'dir' => 'webroot{DS}uploads{DS}media{DS}image', 116 | 'thumbs' => array( 117 | 'thumb' => array( 118 | 'w' => 190, 119 | 'h' => 158, 120 | 'crop' => true, 121 | ), 122 | ), 123 | ), 124 | 'zip' => array( 125 | 'dir' => 'webroot{DS}uploads{DS}media{DS}zip' 126 | ), 127 | ), 128 | ); 129 | ``` 130 | 131 | You also must create one table in your database: 132 | 133 | You can do this with a schema: 134 | 135 | ``` 136 | cake.php schema create --plugin Attach 137 | ``` 138 | 139 | 140 | Or you can do it with SQL: 141 | ```sql 142 | CREATE TABLE `attachments` ( 143 | `id` int(11) NOT NULL AUTO_INCREMENT, 144 | `filename` varchar(150) NOT NULL, 145 | `model` varchar(150) NOT NULL, 146 | `foreign_key` int(11) NOT NULL, 147 | `type` varchar(100) NOT NULL, 148 | `size` int(11) NOT NULL, 149 | `original_name` varchar(150) NOT NULL, 150 | PRIMARY KEY (`id`) 151 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 152 | ``` 153 | 154 | Create your upload view, make sure it's a multipart/form-data form, and that the filename field is of the type 'file': 155 | 156 | ```php 157 | Form->create('Media', array('type' => 'file')); 159 | echo $this->Form->input('name'); 160 | echo $this->Form->input('image', array('type' => 'file')); 161 | echo $this->Form->input('swf', array('type' => 'file')); 162 | echo $this->Form->input('zip', array('type' => 'file')); 163 | echo $this->Form->input('status'); 164 | echo $this->Form->end(__('Submit')); 165 | ``` 166 | 167 | 168 | 169 | Attach automatically creates the relationship with the model Attachment, for each type that you define: 170 | 171 | ```php 172 | var_dump($this->Media->AttachmentImage); 173 | var_dump($this->Media->AttachmentSwf); 174 | var_dump($this->Media->AttachmentZip); 175 | ``` 176 | 177 | It will be always "Attachment" plus the type! 178 | 179 | ## License 180 | 181 | Licensed under The MIT License 182 | Redistributions of files must retain the above copyright notice. 183 | 184 | ## Author 185 | 186 | Vinícius Krolow - krolow[at]gmail.com 187 | -------------------------------------------------------------------------------- /Model/Behavior/UploadBehavior.php: -------------------------------------------------------------------------------- 1 | 15 | * @license MIT License (http://www.opensource.org/licenses/mit-license.php) 16 | */ 17 | 18 | App::uses('Attachment', 'Attach.Model'); 19 | 20 | class UploadBehavior extends ModelBehavior 21 | { 22 | 23 | /** 24 | * Imagine Github URL 25 | * 26 | * @var string 27 | */ 28 | const IMAGINE_URL = 'https://github.com/avalanche123/Imagine'; 29 | 30 | /** 31 | * Set what are the multiple models 32 | * 33 | * @var array 34 | */ 35 | private $__multiple = array(); 36 | 37 | /** 38 | * Setup this behavior with the specified configuration settings. 39 | * 40 | * @param Model $model Model using this behavior 41 | * @param array $config Configuration settings for $model 42 | * 43 | * @return void 44 | */ 45 | public function setup(Model $model, $config = array()) { 46 | $this->config[$model->alias] = $config; 47 | 48 | $this->types[$model->alias] = array_keys($this->config[$model->alias]); 49 | $typeIndex = array_search('Attach.type', $this->types[$model->alias]); 50 | 51 | if ($typeIndex !== false) { 52 | unset($this->types[$model->alias][$typeIndex]); 53 | } 54 | 55 | foreach ($this->types[$model->alias] as $index => $type) { 56 | 57 | $folder = $this->getUploadFolder($model, $type); 58 | $this->isWritable($this->getUploadFolder($model, $type)); 59 | $this->_setRelationModel( 60 | $model, 61 | $this->types[$model->alias][$index] 62 | ); 63 | } 64 | } 65 | 66 | /** 67 | * Create the relation bettween the model and the attachment model for each 68 | * type of file setted in the config 69 | * 70 | * @param Model $model Model using this behavior 71 | * @param string $type Type of the file upload 72 | * 73 | * @return void 74 | */ 75 | protected function _setRelationModel(Model $model, $type) { 76 | $relation = 'hasOne'; 77 | 78 | //case is defined multiple is a hasMany 79 | if ($this->isMultiple($model, $type)) { 80 | $relation = 'hasMany'; 81 | } 82 | 83 | $type = Inflector::camelize($type); 84 | 85 | $model->{$relation}['Attachment' . $type] = array( 86 | 'className' => 'Attachment', 87 | 'foreignKey' => 'foreign_key', 88 | 'dependent' => true, 89 | 'conditions' => array( 90 | 'Attachment' . $type . '.model' => $model->alias, 91 | 'Attachment' . $type . '.type' => Inflector::underscore($type)), 92 | 'fields' => '', 93 | 'order' => '' 94 | ); 95 | } 96 | 97 | /** 98 | * Check if the given file type is multiple or not 99 | * 100 | * @param Model $model Model using this behavior 101 | * @param string $type Type of the file upload 102 | * 103 | * @return bool 104 | */ 105 | public function isMultiple(Model $model, $type) { 106 | return isset($this->config[$model->alias][$type]['multiple']) 107 | && $this->config[$model->alias][$type]['multiple'] == true; 108 | } 109 | 110 | /** 111 | * Check if it's necessary validate the file 112 | * 113 | * @param Model $model Model using this behavior 114 | * @param string $validation Name of the validation 115 | * @param array $check Data array of file 116 | * 117 | * @return bool 118 | */ 119 | public function shouldValidate($model, $validation, $check) { 120 | if ($this->isPostFileDataEmpty($model, $check)) { 121 | return !$this->isRequired($model, $validation, $check); 122 | } 123 | 124 | return false; 125 | } 126 | 127 | /** 128 | * Check if the given data is empty 129 | * 130 | * @param Model $model Model using this behavior 131 | * @param array $file File data 132 | * 133 | * @return bool 134 | */ 135 | public function isPostFileDataEmpty($model, $file) { 136 | if (!is_array($file)) { 137 | return false; 138 | } 139 | $file = array_shift($file); 140 | 141 | return empty($file['name']) && $file['size'] === 0; 142 | } 143 | 144 | /** 145 | * Check if the file is required 146 | * 147 | * @param Model $model Model using this behavior 148 | * @param stirng $validation Method name 149 | * @param array $check Data arary of file 150 | * 151 | * @return bool 152 | */ 153 | public function isRequired($model, $validation, $check) { 154 | $key = key($check); 155 | 156 | if (!isset($model->validate[$key]) 157 | || !isset($model->validate[$key]['required']) 158 | ) { 159 | return false; 160 | } 161 | 162 | return (bool)$model->validate[$key]['required']; 163 | } 164 | 165 | /** 166 | * Check if the file extension it's correct 167 | * 168 | * @param Model $model Model using this behavior 169 | * @param array $check File to be checked 170 | * @param array $extensions The list of allowed extensions 171 | * 172 | * @return bool Return true in case of valid and false in case of invalid 173 | */ 174 | public function extension(Model $model, $check, $extensions) { 175 | if ($this->shouldValidate($model, __METHOD__, $check)) { 176 | return true; 177 | } 178 | 179 | $check = array_shift($check); 180 | 181 | if (isset($check['name'])) { 182 | return in_array( 183 | $this->getFileExtension( 184 | $model, 185 | $check['name'] 186 | ), 187 | $extensions 188 | ); 189 | } 190 | 191 | return false; 192 | } 193 | 194 | /** 195 | * Check if the mime type it's correct 196 | * 197 | * @param Model $model Model using this behavior 198 | * @param array $check File to be checked 199 | * @param array $mimes The list of allowed mime types 200 | * 201 | * @return bool Return true in case of valid and false in case of invalid 202 | */ 203 | public function mime(Model $model, $check, $mimes) { 204 | if ($this->shouldValidate($model, __METHOD__, $check)) { 205 | return true; 206 | } 207 | 208 | $check = array_shift($check); 209 | 210 | if (isset($check['tmp_name']) && file_exists($check['tmp_name'])) { 211 | $info = $this->getFileMime($model, $check['tmp_name']); 212 | 213 | return in_array($info, $mimes); 214 | } 215 | 216 | return false; 217 | } 218 | 219 | /** 220 | * Check if the file size it's correct 221 | * 222 | * @param Model $model Model using this behavior 223 | * @param array $check File to be checked 224 | * @param array $size The max size allowed 225 | * 226 | * @return bool Return true in case of valid and false in case of invalid 227 | */ 228 | public function size(Model $model, $check, $size) { 229 | if ($this->shouldValidate($model, __METHOD__, $check)) { 230 | return true; 231 | } 232 | 233 | $check = array_shift($check); 234 | 235 | return $size >= $check['size']; 236 | } 237 | 238 | /** 239 | * Check if the image fits within given dimensions 240 | * 241 | * @param Model $model Model using this behavior 242 | * @param array $check File to be checked 243 | * @param int $width Maximum width in pixels 244 | * @param int $height Maximum height in pixels 245 | * 246 | * @return bool Return true if image fits withing given dimensions 247 | */ 248 | public function maxDimensions(Model $model, $check, $width, $height) { 249 | if ($this->shouldValidate($model, __METHOD__, $check)) { 250 | return true; 251 | } 252 | 253 | $check = array_shift($check); 254 | 255 | if (isset($check['tmp_name']) && file_exists($check['tmp_name'])) { 256 | $info = getimagesize($check['tmp_name']); 257 | 258 | return ($info && $info[0] <= $width && $info[1] <= $height); 259 | } 260 | 261 | return false; 262 | } 263 | 264 | /** 265 | * Check if the image fits within given dimensions 266 | * 267 | * @param Model $model Model using this behavior 268 | * @param mixed $check File to be checked 269 | * @param mixed $width Minimum width in pixels 270 | * @param mixed $height Minimum height in pixels 271 | * 272 | * @return bool Return true if image fits within given dimensions 273 | */ 274 | public function minDimensions(Model $model, $check, $width, $height) { 275 | if ($this->shouldValidate($model, __METHOD__, $check)) { 276 | return true; 277 | } 278 | 279 | $check = array_shift($check); 280 | 281 | if (isset($check['tmp_name']) && file_exists($check['tmp_name'])) { 282 | $info = getimagesize($check['tmp_name']); 283 | 284 | return ($info && $info[0] >= $width && $info[1] >= $height); 285 | } 286 | 287 | return false; 288 | } 289 | 290 | /** 291 | * Return the mime type of the given file 292 | * 293 | * @param Model $model Model using this behavior 294 | * @param string $file Path of file 295 | * 296 | * @return string Mimetype 297 | */ 298 | public function getFileMime(Model $model, $file) { 299 | $finfo = finfo_open(FILEINFO_MIME_TYPE); 300 | $info = finfo_file($finfo, $file); 301 | 302 | return $info; 303 | } 304 | 305 | /** 306 | * Get the file extension 307 | * 308 | * @param string $file File to be checked 309 | * 310 | * @return string File extension 311 | */ 312 | public function getFileExtension(Model $model, $file) { 313 | return strtolower(pathinfo($file, PATHINFO_EXTENSION)); 314 | } 315 | 316 | /** 317 | * Return the upload folder that was set for the given type 318 | * 319 | * @param Model $model Model using this behavior 320 | * @param string $type Type of the file upload 321 | * 322 | * @return string Path for the upload folder 323 | */ 324 | public function getUploadFolder($model, $type) { 325 | return APP . str_replace( 326 | '{DS}', 327 | DS, 328 | $this->config[$model->alias][$type]['dir'] 329 | ) . DS; 330 | } 331 | 332 | /** 333 | * Return if the folder is writable 334 | * 335 | * @param string $dir Path of folder 336 | * 337 | * @throws CakeException case the folder is not writable 338 | * 339 | * @return bool return if the folder is writable 340 | */ 341 | public function isWritable($dir) { 342 | if (is_dir($dir) && is_writable($dir)) { 343 | return true; 344 | } 345 | 346 | throw new CakeException(sprintf('Folder is not writable: %s', $dir)); 347 | } 348 | 349 | /** 350 | * afterSave is called after a model is saved. 351 | * 352 | * @param Model $model Model using this behavior 353 | * @param boolean $created True if this save created a new record 354 | * 355 | * @return bool 356 | */ 357 | public function afterSave(Model $model, $created, $options = array()) { 358 | parent::afterSave($model, $created); 359 | 360 | foreach ($this->types[$model->alias] as $type) { 361 | 362 | $data = $model->data; 363 | 364 | //set multiple as false by standard 365 | $this->__multiple[$model->alias] = false; 366 | 367 | if ($this->isMultiple($model, $type)) { 368 | $this->__multiple[$model->alias] = true; 369 | 370 | $check = isset($data[$model->alias]) 371 | && isset($data[$model->alias][$type]) 372 | && is_array($data[$model->alias][$type]); 373 | } else { 374 | $check = isset($data[$model->alias][$type]['tmp_name']) 375 | && !empty($data[$model->alias][$type]['tmp_name']); 376 | } 377 | 378 | //case has the file update :) 379 | if ($check) { 380 | if (isset($this->__multiple[$model->alias]) && $this->__multiple[$model->alias]) { 381 | foreach ($data[$model->alias][$type] as $index => $value) { 382 | $this->saveFile($model, $type, $index); 383 | } 384 | } else { 385 | $this->saveFile($model, $type); 386 | } 387 | } 388 | } 389 | } 390 | 391 | /** 392 | * Before delete is called before any delete occurs on the attached model, 393 | * but after the model's beforeDelete is called. 394 | * Returning false from a beforeDelete will abort the delete. 395 | * 396 | * @param Model $model Model using this behavior 397 | * @param boolean $cascade If true records that depend on this record will also be deleted 398 | * 399 | * @return mixed False if the operation should abort. Any other result will continue. 400 | */ 401 | public function beforeDelete(Model $model, $cascade = true) { 402 | //no delete for us ;) 403 | if ($cascade === false) { 404 | return; 405 | } 406 | 407 | foreach ($this->types[$model->alias] as $type) { 408 | $className = 'Attachment' . Inflector::camelize($type); 409 | $attachments = $model->{$className}->find( 410 | 'all', 411 | array( 412 | 'conditions' => array( 413 | 'model' => $model->alias, 414 | 'foreign_key' => $model->id, 415 | ), 416 | ) 417 | ); 418 | 419 | foreach ($attachments as $attach) { 420 | $this->deleteAllFiles($model, $attach); 421 | } 422 | } 423 | 424 | return $cascade; 425 | } 426 | 427 | /** 428 | * Save the given type of file 429 | * 430 | * @param Model $model Model using this behavior 431 | * @param string $type Type of the file upload 432 | * @param int $index Case is multiple send the index of data 433 | * 434 | * @throws CakeException case the file is not one image 435 | * 436 | * @return void 437 | */ 438 | public function saveFile(Model $model, $type, $index = null) { 439 | $uploadData = $model->data[$model->alias][$type]; 440 | 441 | if (!is_null($index)) { 442 | $uploadData = $uploadData[$index]; 443 | } 444 | 445 | if (!isset($uploadData['tmp_name']) || empty($uploadData['tmp_name'])) { 446 | return; 447 | } 448 | 449 | $file = $model->generateName($type, $index); 450 | $attach = $this->saveAttachment( 451 | $model, 452 | $type, 453 | $file, 454 | $uploadData['name'], 455 | $uploadData['size'] 456 | ); 457 | 458 | if (empty($uploadData['tmp_name'])) { 459 | return; 460 | } 461 | 462 | //move file 463 | copy($uploadData['tmp_name'], $file); 464 | $this->deleteFile($uploadData['tmp_name']); 465 | 466 | if (!isset($this->config[$model->alias][$type]['thumbs'])) { 467 | return; 468 | } 469 | $info = getimagesize($file); 470 | if (!$info) { 471 | throw new CakeException( 472 | sprintf('The file %s is not an image', $file) 473 | ); 474 | } 475 | 476 | //generate thumbs 477 | $model->createThumbs($type, $file); 478 | } 479 | 480 | /** 481 | * Save the given type of file 482 | * 483 | * @param Model $model Model using this behavior 484 | * @param mixed $attachment Attachment to be deleted 485 | * 486 | * @return void 487 | */ 488 | public function deleteAllFiles(Model $model, $attachment) { 489 | $attachment = array_shift($attachment); 490 | 491 | $dir = $this->getUploadFolder($model, $attachment['type']); 492 | 493 | //delete the original file 494 | $this->deleteFile($dir . $attachment['filename']); 495 | 496 | //check if exists thumbs to be deleted too 497 | $files = glob($dir . '*.' . $attachment['filename']); 498 | if (!is_array($files)) { 499 | return; 500 | } 501 | foreach ($files as $fileToDelete) { 502 | $this->deleteFile($fileToDelete); 503 | } 504 | } 505 | 506 | /** 507 | * Delete the specific given file 508 | * 509 | * @param string $file File to be checked 510 | * 511 | * @return bool true case was deleted with success 512 | */ 513 | public function deleteFile($file) { 514 | if (!file_exists($file)) { 515 | return false; 516 | } 517 | 518 | return unlink($file); 519 | } 520 | 521 | /** 522 | * Insert attachment into the database 523 | * 524 | * @param Model $model Model using this behavior 525 | * @param string $type Type of the file upload 526 | * @param string $filename Filename to be saved 527 | * 528 | * @return void 529 | */ 530 | public function saveAttachment(Model $model, $type, $filename, $originalName = null, $size = null) { 531 | $className = 'Attachment' . Inflector::camelize($type); 532 | $attachment = false; 533 | 534 | $attachment = $model->{$className}->find( 535 | 'first', 536 | array( 537 | 'conditions' => array( 538 | 'foreign_key' => $model->id, 539 | 'model' => $model->alias, 540 | 'type' => $type, 541 | ), 542 | ) 543 | ); 544 | 545 | $data = array( 546 | $className => array( 547 | 'model' => $model->alias, 548 | 'foreign_key' => $model->id, 549 | 'filename' => basename($filename), 550 | 'type' => $type, 551 | 'original_name' => $originalName, 552 | 'size' => $size, 553 | ), 554 | ); 555 | 556 | if (!empty($attachment) && $attachment !== false) { 557 | $this->deleteAllFiles($model, $attachment); 558 | $data[$className]['id'] = $attachment[$className]['id']; 559 | } else { 560 | $model->{$className}->create(); 561 | } 562 | 563 | $model->data += $model->{$className}->save($data); 564 | } 565 | 566 | /** 567 | * Generate an unique name to save the file 568 | * 569 | * @param Model $model Model using this behavior 570 | * @param string $type Type of the file upload 571 | * @param int $index Case is multiple send the index of data 572 | * 573 | * @return string Generated name 574 | * @access public 575 | */ 576 | public function generateName(Model $model, $type, $index = null) { 577 | $dir = $this->getUploadFolder($model, $type); 578 | 579 | if (is_null($index)) { 580 | $extension = $this->getFileExtension( 581 | $model, 582 | $model->data[$model->alias][$type]['name'] 583 | ); 584 | } else { 585 | $extension = $this->getFileExtension( 586 | $model, 587 | $model->data[$model->alias][$type][$index]['name'] 588 | ); 589 | } 590 | 591 | if (!is_null($index)) { 592 | return $dir . $type . '_' . $index . '_' . $model->id . '.' . $extension; 593 | } 594 | 595 | return $dir . $type . '_' . $model->id . '.' . $extension; 596 | } 597 | 598 | /** 599 | * Create thumbs for the given image based in the config 600 | * defined in the model 601 | * 602 | * @param Model $model Model using this behavior 603 | * @param string $type Type of the file upload 604 | * @param string $file Image file 605 | * 606 | * @return void 607 | */ 608 | public function createThumbs(Model $model, $type, $file) { 609 | $imagine = $model->getImagine(); 610 | $image = $imagine->open($file); 611 | $thumbName = basename($file); 612 | $thumbs = $this->config[$model->alias][$type]['thumbs']; 613 | 614 | foreach ($thumbs as $key => $values) { 615 | if (!isset($values['crop'])) { 616 | $values['crop'] = false; 617 | } 618 | 619 | $this->_generateThumb( 620 | array( 621 | 'name' => str_replace( 622 | $thumbName, 623 | $key . '.' . $thumbName, 624 | $file 625 | ), 626 | 'w' => $values['w'], 627 | 'h' => $values['h'], 628 | ), 629 | $image, 630 | $values['crop'] 631 | ); 632 | } 633 | } 634 | 635 | /** 636 | * Create a thumb for the given image file based in the parameters passed 637 | * 638 | * @param string $file Image file 639 | * @param string $name Name of the thumb 640 | * @param float $width Width of thumb 641 | * @param float $height Height of thumb 642 | * @param bool $crop Crop the image 643 | * 644 | * @return void 645 | */ 646 | public function createThumb(Model $model, $file, $name, $width, $height, $crop = false) { 647 | $imagine = $this->getImagine($model); 648 | $image = $imagine->open($file); 649 | 650 | $this->_generateThumb( 651 | array( 652 | 'w' => $width, 653 | 'h' => $height, 654 | 'name' => $name, 655 | ), 656 | $image, 657 | $crop 658 | ); 659 | } 660 | 661 | /** 662 | * Load the imagine library 663 | * 664 | * @throws CakeException 665 | * 666 | * @return \Imagine\Gd\Imagine 667 | */ 668 | public function getImagine(Model $model) { 669 | if (!interface_exists('Imagine\Image\ImageInterface')) { 670 | if (is_file(APP . 'Vendor' . 'autoload.php')) { 671 | require APP . 'Vendor' . 'autoload.php'; 672 | } 673 | 674 | throw new RuntimeException('We could not autoload imagine, please set the PSR-0 autoload'); 675 | } 676 | 677 | if (isset($this->config[$model->alias]['Attach.type']) && $this->config[$model->alias]['Attach.type'] == 'Imagick') { 678 | return new \Imagine\Imagick\Imagine(); 679 | } 680 | 681 | return new \Imagine\Gd\Imagine(); 682 | } 683 | 684 | /** 685 | * Generate the thumb 686 | * 687 | * @param mixed $thumb 'width', 'height' and 'name' 688 | * @param string $image image file 689 | * @param bool $crop Crop the image 690 | * 691 | * @return void 692 | */ 693 | protected function _generateThumb($thumb, $image, $crop = false) { 694 | if ($crop) { 695 | $mode = Imagine\Image\ImageInterface::THUMBNAIL_OUTBOUND; 696 | } else { 697 | $mode = Imagine\Image\ImageInterface::THUMBNAIL_INSET; 698 | } 699 | 700 | $thumbnail = $image->thumbnail( 701 | new Imagine\Image\Box( 702 | $thumb['w'], 703 | $thumb['h'] 704 | ), 705 | $mode 706 | ); 707 | 708 | $thumbnail->save($thumb['name']); 709 | } 710 | 711 | } 712 | --------------------------------------------------------------------------------