├── MIT-LICENSE ├── README.textile └── attachment.php /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | == MIT License 2 | 3 | Copyright (c) 2009 [http://sabbour.wordpress.com/] 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | *Attachment component for CakePHP* handles file uploads to the file system. If 2 | it's an image file, it creates thumbnail copies in 3 | @/app/webroot/attachments/photos/{required_sizes}@ folder; while other files 4 | are stored in @/app/webroot/attachments/files@. 5 | 6 | I've started it from the public copy at 7 | "@http://sabbour.wordpress.com/2008/07/18/enhanced-image-upload-component-for-cakephp-12/@":http://sabbour.wordpress.com/2008/07/18/enhanced-image-upload-component-for-cakephp-12/ 8 | to have a standard way of uploading files to CakePHP projects. 9 | 10 | See branch cake1.3 for Cake 1.3 version; branch cake2 for CakePHP 2 version. 11 | 12 | h2. Requirements 13 | 14 | # PHP GD library installed and enabled. 15 | # @/app/webroot/attachments/@ must be writable by the web server. 16 | 17 | h2. Installation 18 | 19 | # Copy @attachment.php@ to @/app/controllers/components/@ 20 | # Add component to your controller: @var $components = array('Attachment');@ 21 | # Add DB columns {prefix}_file_path, {prefix}_file_name, {prefix}_file_size 22 | and {prefix}_content_type to the model. The prefix is the model name, in 23 | lowercase, words seperated by underscores. 24 | 25 | h2. Model setup 26 | 27 | We'll asume a Model named 'Pet' for a sample installation. The code relies on 28 | four columns in the Model table: 29 | 30 | @pet_file_path, pet_file_name, pet_file_size@ and @pet_content_type@ 31 | 32 | Models with multi-word names should have each word seperated by underscores. 33 | For example, the model "GalleryPhoto" would use the table "gallery_photos" and 34 | the following fields: 35 | 36 | @gallery_photo_file_path, gallery_photo_file_name, gallery_photo_file_size@ and @gallery_photo_content_type@ 37 | 38 | These fields are automagically updated when you call @save()@ on the @$this->data@ array 39 | 40 | h2. View setup 41 | 42 | h3. On forms 43 | 44 | @Form->create('Pet', array('type' => 'file'));@ 45 | @Form->input('pet' , array('type' => 'file')); ?>@ 46 | @Form->end('Save my pooch'); ?>@ 47 | 48 | The (file) input 'pet' is the lower case model-name, multi-word model names 49 | seperated by underscores. 50 | 51 | h3. Show files (after they are saved) 52 | 53 | For images: @Html->image('/attachments/files_dir/{size}/' . $data['Pet']['pet_file_path']); ?>@ 54 | For files: @Html->link('Grab file', '/attachments/files_dir/' . $data['Pet']['pet_file_path']); ?>@ 55 | 56 | h2. Controller setup 57 | 58 | Configuration options (default values between parentheses): 59 | 60 | * @files_dir@: Where to save the files (inside @/app/webroot/attachments/@) (@photos@). 61 | * @rm_tmp_file@: Remove temporal image after resizing (@false@). 62 | * @allow_non_image_files@, self descriptive ;-) (@true@). 63 | * @default_col@: Column prefix for file related data (lowercase modelname, words seperated by underscores). 64 | * @images_size@: Array of different file sizes required by your app. Each 65 | element is itself an array, like: @'folder_name' => array($width, $height, $crop)@. 66 | (You may define only width or height, and the image will scale appropriately). 67 | 68 | You can override the default configuration passing an array of options while 69 | including the component, like: 70 | 71 | @var $components = array('Attachment' => array( 72 | 'files_dir' => 'pets', 73 | 'images_size' => array( 74 | 'avatar' => array(75, 75, 'resizeCrop') 75 | ) 76 | ));@ 77 | 78 | h3. Controller methods: 79 | 80 | h4. upload($data) 81 | 82 | * @data@: Raw data from form. 83 | 84 | Simply call the following on the form data: 85 | 86 | @$this->Attachment->upload($this->data['Pet']);@ 87 | 88 | You may choose different column prefixes than the model name, as long as you 89 | specify it on the upload method, like so: 90 | 91 | @$this->Attachment->upload($this->data['Pet'], 'dog');@ 92 | 93 | h4. thumbnail($data, $upload_dir, $maxw, $maxh, $crop = 'resize') 94 | 95 | Used by the more general @upload@ method. 96 | 97 | * @data@: image data array 98 | * @upload_dir@: where to save the file (inside @attachments/files_dir@) 99 | * @maxw/maxh@: maximum width/height for resizing images 100 | * @crop@: Crop image? (one of @resize@, @resizeCrop@ or @crop@) 101 | 102 | h4. delete_files($filename) 103 | 104 | * @filename@: file-to-delete name 105 | 106 | h2. Validations 107 | 108 | You may wish to validate from the model. e.g: 109 | 110 | @var $validate = array(@ 111 | @'pet_file_size' => array(@ 112 | @'rule' => array('maxLength', 6),@ 113 | @'message' => 'Image size is waaaaaayyy too big. Try resizing first'@ 114 | @)@ 115 | @);@ 116 | 117 | h3. Show validation errors in the view 118 | 119 | @if(isset($this->Form->validationErrors)) {@ 120 | @foreach ($this->Form->validationErrors as $model => $columns) {@ 121 | @foreach ($columns as $err_msg) {@ 122 | @echo $this->Html->div('error-message', $err_msg);@ 123 | @}@ 124 | @}@ 125 | @}@ 126 | 127 | h4. Only for the file input: 128 | 129 | @if (isset($this->Form->validationErrors['Pet']['pet_file_name'])) {@ 130 | @echo $this->Html->div('error-message', $this->Form->validationErrors['Pet']['pet_file_name']);@ 131 | @}@ 132 | -------------------------------------------------------------------------------- /attachment.php: -------------------------------------------------------------------------------- 1 | 'photos', 23 | 'rm_tmp_file' => false, 24 | 'allow_non_image_files' => true, 25 | 'images_size' => array( 26 | /* You may define as many options as you like */ 27 | 'big' => array(640, 480, 'resize'), 28 | 'med' => array(263, 263, 'resizeCrop'), 29 | 'small' => array( 90, 90, 'resizeCrop') 30 | ) 31 | ); 32 | 33 | /** 34 | * Initialization method. You may override configuration options from a controller 35 | * 36 | * @param $controller object 37 | * @param $config array 38 | */ 39 | function initialize(&$controller, $config) { 40 | $this->controller = $controller; 41 | $model_prefix = Inflector::tableize($controller->modelClass); // lower case, studley caps -> underscores 42 | $prefix = Inflector::singularize($model_prefix); // make singular. 'GalleryImage' becomes 'gallery_image' 43 | $this->config = array_merge( 44 | array('default_col' => $prefix), /* default column prefix is lowercase, singular model name */ 45 | $this->config, /* default general configuration */ 46 | $config /* overriden configurations */ 47 | ); 48 | } 49 | 50 | /** 51 | * Uploads file to file system, according to $config. 52 | * Example usage: 53 | * $this->Attachment->upload($this->data['Model']['Attachment']); 54 | * 55 | * @return mixed boolean true on success, or error string 56 | * @param $data array the file input array 57 | * @param $column_prefix string The prefix of the fields used to store the uploaded file data 58 | * 59 | */ 60 | function upload(&$data, $column_prefix = null) { 61 | if ($column_prefix == null) { 62 | $column_prefix = $this->config['default_col']; 63 | } else { 64 | $this->config['default_col'] = $column_prefix; 65 | } 66 | 67 | $file = $data[$this->config['default_col']]; 68 | if ($file['error'] === UPLOAD_ERR_OK) { 69 | return $this->upload_FS($data); 70 | } else { 71 | return $this->log_proper_error($file['error']); 72 | } 73 | } 74 | 75 | /** 76 | * Creates the relevant dir's and processes the file 77 | * 78 | * @return mixed boolean true on success, or error string 79 | * @param $data array The array of data from the controlle 80 | */ 81 | function upload_FS(&$data) { 82 | $column_prefix = $this->config['default_col']; 83 | $error = 0; 84 | $tmpuploaddir = WWW_ROOT.'attachments'.DS.'tmp'; // /tmp/ folder (should delete image after upload) 85 | $fileuploaddir = WWW_ROOT.'attachments'.DS.'files'; 86 | 87 | // Make sure the required directories exist, and create them if necessary 88 | if (!is_dir($tmpuploaddir)) mkdir($tmpuploaddir, 0755, true); 89 | if (!is_dir($fileuploaddir)) mkdir($fileuploaddir, 0755, true); 90 | 91 | /* Generate a unique name for the file */ 92 | $filetype = end(split('\.', $data[$column_prefix]['name'])); 93 | $filename = String::uuid(); 94 | settype($filename, 'string'); 95 | $filename .= '.' . $filetype; 96 | 97 | /* Security check */ 98 | if (!is_uploaded_file($data[$column_prefix]['tmp_name'])) { 99 | return $this->log_cakephp_error_and_return('Error uploading file (sure it was a POST request?).'); 100 | } 101 | 102 | /* If it's image get image size and make thumbnail copies. */ 103 | if ($this->is_image($filetype)) { 104 | $this->copy_or_log_error($data[$column_prefix]['tmp_name'], $tmpuploaddir, $filename); 105 | /* Create each thumbnail_size */ 106 | foreach ($this->config['images_size'] as $dir => $opts) { 107 | $this->thumbnail($tmpuploaddir.DS.$filename, $dir, $opts[0], $opts[1], $opts[2]); 108 | } 109 | if ($this->config['rm_tmp_file']) 110 | unlink($tmpuploaddir.DS.$filename); 111 | } else { 112 | if (!$this->config['allow_non_image_files']) { 113 | return $this->log_cakephp_error_and_return('File type not allowed (only images files).'); 114 | } else { 115 | $this->copy_or_log_error($data[$column_prefix]['tmp_name'], $fileuploaddir, $filename); 116 | } 117 | } 118 | 119 | /* File uploaded, return modified data array */ 120 | $res[$column_prefix.'_file_path'] = $filename; 121 | $res[$column_prefix.'_file_name'] = $data[$column_prefix]['name']; 122 | $res[$column_prefix.'_file_size'] = $data[$column_prefix]['size']; 123 | $res[$column_prefix.'_content_type'] = $data[$column_prefix]['type']; 124 | unset($data[$column_prefix]); /* delete $_FILES indirection */ 125 | $data = array_merge($data, $res); /* add default fields */ 126 | 127 | return true; 128 | } 129 | 130 | /** 131 | * Creates resized copies of input image 132 | * E.g; 133 | * $this->Attachment->thumbnail($this->data['Model']['Attachment'], $upload_dir, 640, 480, false); 134 | * 135 | * @param $tmpfile array The image data array from the form 136 | * @param upload_dir string The name of the parent folder of the images 137 | * @param $maxw int Maximum width for resizing thumbnails 138 | * @param $maxh int Maximum height for resizing thumbnails 139 | * @param $crop string either 'resize', 'resizeCrop' or 'crop' 140 | */ 141 | function thumbnail($tmpfile, $upload_dir, $maxw, $maxh, $crop = 'resize') { 142 | // Make sure the required directory exist; create it if necessary 143 | $upload_dir = WWW_ROOT.'attachments'.DS.$this->config['files_dir'].DS.$upload_dir; 144 | if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true); 145 | 146 | /* Directory Separator for windows users */ 147 | $ds = (strcmp('\\', DS) == 0) ? '\\\\' : DS; 148 | $file_name = end(split($ds, $tmpfile)); 149 | $this->resize_image($crop, $tmpfile, $upload_dir, $file_name, $maxw, $maxh, 85); 150 | } 151 | 152 | /** 153 | * Deletes file, or image and associated thumbnail 154 | * e.g; 155 | * $this->Attachment->delete_files('file_name.jpg'); 156 | * 157 | * @param $filename string The file to delete 158 | */ 159 | function delete_files($filename) { 160 | /* Non image files */ 161 | if (is_file(WWW_ROOT.'attachments'.DS.'files'.DS.$filename)) { 162 | unlink(WWW_ROOT.'attachments'.DS.'files'.DS.$filename); 163 | } 164 | /* tmp files (if not pruned while uploading) */ 165 | if (is_file(WWW_ROOT.'attachments'.DS.'tmp'.DS.$filename)) { 166 | unlink(WWW_ROOT.'attachments'.DS.'tmp'.DS.$filename); 167 | } 168 | /* Thumbnail copies */ 169 | foreach ($this->config['images_size'] as $size => $opts) { 170 | $photo = WWW_ROOT.'attachments'.DS.$this->config['files_dir'].DS.$size.DS.$filename; 171 | if (is_file($photo)) unlink($photo); 172 | } 173 | } 174 | 175 | /* 176 | * Creates resized image copy 177 | * 178 | * Parameters: 179 | * cType: Conversion type {resize (default) | resizeCrop (square) | crop (from center)} 180 | * tmpfile: original (tmp) file name 181 | * newName: include extension (if desired) 182 | * newWidth: the max width or crop width 183 | * newHeight: the max height or crop height 184 | * quality: the quality of the image 185 | */ 186 | function resize_image($cType = 'resize', $tmpfile, $dst_folder, $dstname = false, $newWidth=false, $newHeight=false, $quality = 75) { 187 | $srcimg = $tmpfile; 188 | list($oldWidth, $oldHeight, $type) = getimagesize($srcimg); 189 | $ext = $this->image_type_to_extension($type); 190 | 191 | // If file is writeable, create destination (tmp) image 192 | if (is_writeable($dst_folder)) { 193 | $dstimg = $dst_folder.DS.$dstname; 194 | } else { 195 | // if dst_folder not writeable, let developer know 196 | debug('You must allow proper permissions for image processing. And the folder has to be writable.'); 197 | debug("Run 'chmod 755 $dst_folder', and make sure the web server is it's owner."); 198 | return $this->log_cakephp_error_and_return('No write permissions on attachments folder.'); 199 | } 200 | 201 | /* Check if something is requested, otherwise do not resize */ 202 | if ($newWidth or $newHeight) { 203 | /* Delete tmp file if it exists */ 204 | if (file_exists($dstimg)) { 205 | unlink($dstimg); 206 | } else { 207 | switch ($cType) { 208 | default: 209 | case 'resize': 210 | // Maintains the aspect ratio of the image and makes sure 211 | // that it fits within the maxW and maxH 212 | $widthScale = 2; 213 | $heightScale = 2; 214 | 215 | /* Check if we're overresizing (or set new scale) */ 216 | if ($newWidth) { 217 | if ($newWidth > $oldWidth) $newWidth = $oldWidth; 218 | $widthScale = $newWidth / $oldWidth; 219 | } 220 | if ($newHeight) { 221 | if ($newHeight > $oldHeight) $newHeight = $oldHeight; 222 | $heightScale = $newHeight / $oldHeight; 223 | } 224 | if ($widthScale < $heightScale) { 225 | $maxWidth = $newWidth; 226 | $maxHeight = false; 227 | } elseif ($widthScale > $heightScale ) { 228 | $maxHeight = $newHeight; 229 | $maxWidth = false; 230 | } else { 231 | $maxHeight = $newHeight; 232 | $maxWidth = $newWidth; 233 | } 234 | 235 | if ($maxWidth > $maxHeight){ 236 | $applyWidth = $maxWidth; 237 | $applyHeight = ($oldHeight*$applyWidth)/$oldWidth; 238 | } elseif ($maxHeight > $maxWidth) { 239 | $applyHeight = $maxHeight; 240 | $applyWidth = ($applyHeight*$oldWidth)/$oldHeight; 241 | } else { 242 | $applyWidth = $maxWidth; 243 | $applyHeight = $maxHeight; 244 | } 245 | $startX = 0; 246 | $startY = 0; 247 | break; 248 | 249 | case 'resizeCrop': 250 | /* Check if we're overresizing (or set new scale) */ 251 | /* resize to max, then crop to center */ 252 | if ($newWidth > $oldWidth) $newWidth = $oldWidth; 253 | $ratioX = $newWidth / $oldWidth; 254 | 255 | if ($newHeight > $oldHeight) $newHeight = $oldHeight; 256 | $ratioY = $newHeight / $oldHeight; 257 | 258 | if ($ratioX < $ratioY) { 259 | $startX = round(($oldWidth - ($newWidth / $ratioY))/2); 260 | $startY = 0; 261 | $oldWidth = round($newWidth / $ratioY); 262 | $oldHeight = $oldHeight; 263 | } else { 264 | $startX = 0; 265 | $startY = round(($oldHeight - ($newHeight / $ratioX))/2); 266 | $oldWidth = $oldWidth; 267 | $oldHeight = round($newHeight / $ratioX); 268 | } 269 | $applyWidth = $newWidth; 270 | $applyHeight = $newHeight; 271 | break; 272 | 273 | case 'crop': 274 | // straight centered crop 275 | $startY = ($oldHeight - $newHeight)/2; 276 | $startX = ($oldWidth - $newWidth)/2; 277 | $oldHeight = $newHeight; 278 | $applyHeight = $newHeight; 279 | $oldWidth = $newWidth; 280 | $applyWidth = $newWidth; 281 | break; 282 | } 283 | 284 | switch($ext) { 285 | case 'gif' : 286 | $oldImage = imagecreatefromgif($srcimg); 287 | break; 288 | case 'png' : 289 | $oldImage = imagecreatefrompng($srcimg); 290 | break; 291 | case 'jpg' : 292 | case 'jpeg' : 293 | $oldImage = imagecreatefromjpeg($srcimg); 294 | break; 295 | default : 296 | // image type is not a possible option 297 | return false; 298 | break; 299 | } 300 | 301 | // Create new image 302 | $newImage = imagecreatetruecolor($applyWidth, $applyHeight); 303 | // Put old image on top of new image 304 | imagealphablending($newImage, false); 305 | imagesavealpha($newImage, true); 306 | imagecopyresampled($newImage, $oldImage, 0, 0, $startX, $startY, $applyWidth, $applyHeight, $oldWidth, $oldHeight); 307 | 308 | switch($ext) { 309 | case 'gif' : 310 | imagegif($newImage, $dstimg, $quality); 311 | break; 312 | case 'png' : 313 | imagepng($newImage, $dstimg, round($quality/10)); 314 | break; 315 | case 'jpg' : 316 | case 'jpeg' : 317 | imagejpeg($newImage, $dstimg, $quality); 318 | break; 319 | default : 320 | return false; 321 | break; 322 | } 323 | 324 | imagedestroy($newImage); 325 | imagedestroy($oldImage); 326 | return true; 327 | } 328 | } else { /* Nothing requested */ 329 | return false; 330 | } 331 | } 332 | 333 | 334 | /* Many helper functions */ 335 | 336 | function copy_or_log_error($tmp_name, $dst_folder, $dst_filename) { 337 | if (is_writeable($dst_folder)) { 338 | if (!copy($tmp_name, $dst_folder.DS.$dst_filename)) { 339 | unset($dst_filename); 340 | return $this->log_cakephp_error_and_return('Error uploading file.', 'publicaciones'); 341 | } 342 | } else { 343 | // if dst_folder not writeable, let developer know 344 | debug('You must allow proper permissions for image processing. And the folder has to be writable.'); 345 | debug("Run 'chmod 755 $dst_folder', and make sure the web server is it's owner."); 346 | return $this->log_cakephp_error_and_return('No write permissions on attachments folder.'); 347 | } 348 | } 349 | 350 | function is_image($file_type) { 351 | $image_types = array('jpeg', 'jpg', 'gif', 'png'); 352 | return in_array(strtolower($file_type), $image_types); 353 | } 354 | 355 | function log_proper_error($err_code) { 356 | switch ($err_code) { 357 | case UPLOAD_ERR_NO_FILE: 358 | return 0; 359 | case UPLOAD_ERR_INI_SIZE: 360 | $e = 'The uploaded file exceeds the upload_max_filesize directive in php.ini.'; 361 | break; 362 | case UPLOAD_ERR_FORM_SIZE: 363 | $e = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.'; 364 | break; 365 | case UPLOAD_ERR_PARTIAL: 366 | $e = 'The uploaded file was only partially uploaded.'; 367 | break; 368 | case UPLOAD_ERR_NO_TMP_DIR: 369 | $e = 'Missing a temporary folder.'; 370 | break; 371 | case UPLOAD_ERR_CANT_WRITE: 372 | $e = 'Failed to write file to disk.'; 373 | break; 374 | case UPLOAD_ERR_EXTENSION: 375 | $e = 'File upload stopped by extension.'; 376 | break; 377 | default: 378 | $e = 'Unknown upload error. Did you add array(\'type\' => \'file\') to your form?'; 379 | } 380 | return $this->log_cakephp_error_and_return($e); 381 | } 382 | 383 | function log_cakephp_error_and_return($msg) { 384 | $_error["{$this->config['default_col']}_file_name"] = $msg; 385 | $this->controller->{$this->controller->modelClass}->validationErrors = array_merge($_error, $this->controller->{$this->controller->modelClass}->validationErrors); 386 | $this->log($msg, 'attachment-component'); 387 | return false; 388 | } 389 | 390 | function image_type_to_extension($imagetype) { 391 | if (empty($imagetype)) return false; 392 | switch($imagetype) { 393 | case IMAGETYPE_TIFF_II : return 'tiff'; 394 | case IMAGETYPE_TIFF_MM : return 'tiff'; 395 | case IMAGETYPE_GIF : return 'gif'; 396 | case IMAGETYPE_JPEG : return 'jpg'; 397 | case IMAGETYPE_PNG : return 'png'; 398 | case IMAGETYPE_SWF : return 'swf'; 399 | case IMAGETYPE_PSD : return 'psd'; 400 | case IMAGETYPE_BMP : return 'bmp'; 401 | case IMAGETYPE_JPC : return 'jpc'; 402 | case IMAGETYPE_JP2 : return 'jp2'; 403 | case IMAGETYPE_JPX : return 'jpf'; 404 | case IMAGETYPE_JB2 : return 'jb2'; 405 | case IMAGETYPE_SWC : return 'swc'; 406 | case IMAGETYPE_IFF : return 'aiff'; 407 | case IMAGETYPE_WBMP : return 'wbmp'; 408 | case IMAGETYPE_XBM : return 'xbm'; 409 | default : return false; 410 | } 411 | } 412 | } 413 | --------------------------------------------------------------------------------