diff --git a/Model/Behavior/UploadBehavior.php b/Model/Behavior/UploadBehavior.php index dc09bd6c..2c247a5c 100644 --- a/Model/Behavior/UploadBehavior.php +++ b/Model/Behavior/UploadBehavior.php @@ -45,6 +45,7 @@ class UploadBehavior extends ModelBehavior { 'deleteOnUpdate' => false, 'mediaThumbnailType'=> 'png', 'saveDir' => true, + 'customName' => false ); protected $_imageMimetypes = array( @@ -216,7 +217,9 @@ public function beforeSave(Model $model) { $removing = isset($model->data[$model->alias][$field]['remove']); if ($removing || ($this->settings[$model->alias][$field]['deleteOnUpdate'] && isset($model->data[$model->alias][$field]['name']) - && strlen($model->data[$model->alias][$field]['name']))) { + && strlen($model->data[$model->alias][$field]['name']) + && isset($this->runtime[$model->alias][$field]['tmp_name']) + && strlen($this->runtime[$model->alias][$field]['tmp_name']))) { // We're updating the file, remove old versions if (!empty($model->id)) { $data = $model->find('first', array( @@ -250,19 +253,29 @@ public function beforeSave(Model $model) { unset($model->data[$model->alias][$field]); continue; } - + $model->data[$model->alias] = array_merge($model->data[$model->alias], array( $field => $this->runtime[$model->alias][$field]['name'], $options['fields']['type'] => $this->runtime[$model->alias][$field]['type'], $options['fields']['size'] => $this->runtime[$model->alias][$field]['size'] )); + + if (isset($model->data[$model->alias][$field]) + && !empty($model->data[$model->alias][$field]) + && $this->settings[$model->alias][$field]['customName'] != false + && isset($this->runtime[$model->alias][$field]['tmp_name']) + && strlen($this->runtime[$model->alias][$field]['tmp_name'])) { + $model->data[$model->alias][$field] = $this->_customName($model,$this->settings[$model->alias][$field]['customName'],$model->data[$model->alias][$field]); + } } + return true; } public function afterSave(Model $model, $created) { + $temp = array($model->alias => array()); - foreach ($this->settings[$model->alias] as $field => $options) { + foreach ($this->settings[$model->alias] as $field => $options) { if (!in_array($field, array_keys($model->data[$model->alias]))) continue; if (empty($this->runtime[$model->alias][$field])) continue; if (isset($this->_removingOnly[$field])) continue; @@ -277,7 +290,9 @@ public function afterSave(Model $model, $created) { $thumbnailPath .= $tempPath . DS; } $tmp = $this->runtime[$model->alias][$field]['tmp_name']; - $filePath = $path . $model->data[$model->alias][$field]; + + $filePath = $path . $this->_sanitizeFilename($model->data[$model->alias][$field]); + if (!$this->handleUploadedFile($model->alias, $field, $tmp, $filePath)) { $model->invalidate($field, 'Unable to move the uploaded file to '.$filePath); throw new UploadException('Unable to upload file'); @@ -1255,7 +1270,11 @@ public function _getMimeType($filePath) { public function _prepareFilesForDeletion(Model $model, $field, $data, $options) { if (!strlen($data[$model->alias][$field])) return $this->__filesToRemove; + $dir = ''; + if (isset($options['fields']['dir']) + && isset ($data[$model->alias][$options['fields']['dir']])) { $dir = $data[$model->alias][$options['fields']['dir']]; + } $filePathDir = $this->settings[$model->alias][$field]['path'] . $dir . DS; $filePath = $filePathDir.$data[$model->alias][$field]; $pathInfo = $this->_pathinfo($filePath); @@ -1345,5 +1364,65 @@ public function _pathinfo($filename) { } return $pathInfo; } + + public function _customName(Model $model, $customName ,$filename ){ + $filename = $this->_pathinfo($filename); + $customName = str_replace('{#NAME}',$filename['filename'],$customName); + preg_match_all("/(\{.*?})/", $customName, $matches); + if ($matches){ + foreach ($matches[0] as $row){ + $cm = substr($row,1,-1); + if ($cm[0] == '!'){ + $cm = substr($cm,1); + $customName = str_replace($row,$model->$cm($filename['filename']),$customName); + + }else{ + $customName = str_replace($row,$model->$cm,$customName); + } + } + } + return $this->_sanitizeFilename("{$customName}.{$filename['extension']}"); + } + + /** + * Make a filename safe to use in any function. (Accents, spaces, special chars...) + * The iconv function must be activated. + * + * @param string $fileName The filename to sanitize (with or without extension) + * @param string $defaultIfEmpty The default string returned for a non valid filename (only special chars or separators) + * @param string $separator The default separator + * @param boolean $lowerCase Tells if the string must converted to lower case + * + * @author COil + * @see http://stackoverflow.com/questions/2668854/sanitizing-strings-to-make-them-url-and-filename-safe + * + * @return string + */ + public function _sanitizeFilename($fileName, $defaultIfEmpty = 'default', $separator = '_', $lowerCase = true) + { + // Gather file informations and store its extension + // use _pathinfo instead pathinfo + $fileInfos = $this->_pathinfo($fileName); + $fileExt = array_key_exists('extension', $fileInfos) ? '.'. strtolower($fileInfos['extension']) : ''; + + // Removes accents + $fileName = @iconv('UTF-8', 'us-ascii//TRANSLIT', $fileInfos['filename']); + + // Removes all characters that are not separators, letters, numbers, dots or whitespaces + $fileName = preg_replace("/[^ a-zA-Z". preg_quote($separator). "\d\.\s]/", '', $lowerCase ? strtolower($fileName) : $fileName); + + // Replaces all successive separators into a single one + $fileName = preg_replace('!['. preg_quote($separator).'\s]+!u', $separator, $fileName); + + // Trim beginning and ending seperators + $fileName = trim($fileName, $separator); + + // If empty use the default string + if (empty($fileName)) { + $fileName = $defaultIfEmpty; + } + + return $fileName. $fileExt; + } } diff --git a/README.markdown b/README.markdown index 00a4507a..dc4e0774 100644 --- a/README.markdown +++ b/README.markdown @@ -43,16 +43,16 @@ In your `Plugin` directory type: To enable Imagick support, you need to have imagick installed: - # Debian systems - sudo apt-get install php-imagick + # Debian systems + sudo apt-get install php-imagick - # OS X Homebrew - brew tap homebrew/dupes - brew tap josegonzalez/homebrew-php - brew install php54-imagick + # OS X Homebrew + brew tap homebrew/dupes + brew tap josegonzalez/homebrew-php + brew install php54-imagick - # From pecl - pecl install imagick + # From pecl + pecl install imagick If you cannot install imagick, please do not use imagick, and instead configure the plugin with `'thumbnailMethod' => 'php'` in your setup options. @@ -248,6 +248,91 @@ We would also need a similar relationship in our `Message` model: Please note that this is not the only way to represent file uploads, but it is documented here for reference. +### Custom Filename + +Add `customName` option to your Model. + + array( + 'photo' => array( + 'fields' => array( + 'dir' => 'photo_dir', + 'customName' => 'PATTERN' + ) + ) + ) + ); + } + +`PATTERN` is simple text, you can use any text you want. there is 3 `Block` types. + + * `{#NAME}` : will be replaced by orginal file name + * `{fieldname}` : will be replaced by `Model`.`fieldname` value + * `{!method}` : will be replaced by `Model`->`method` result + +#### PATTERN Usage + + array( + 'photo' => array( + 'fields' => array( + 'dir' => 'photo_dir', + ), + 'customName' => 'prefix_{#NAME}_suffix' + ) + ) + ); + } + // File's orginal name : logo3w.png + // File's finally name : prefix_logo3w_suffix.png + + array( + 'photo' => array( + 'fields' => array( + 'dir' => 'photo_dir' + ), + 'customName' => '{name}_photo' + ) + ) + ); + + public $name = "Dani"; + } + // File's orginal name : logo3w.png + // File's finally name : Dani_photo.png + + array( + 'photo' => array( + 'fields' => array( + 'dir' => 'photo_dir', + ), + 'customName' => '{!getName}' + ) + ) + ); + + /** + * @param string $filename File's orginal name. its optional. + */ + public function getName($filename = ''){ + return md5($filename); + } + } + // File's orginal name : logo3w.png + // File's finally name : 29068a87847a81f8bc00b600421ddbd6.png + +Note that `Blocks` are `optional`. + ### Alternative Behaviors The Upload plugin also comes with a `FileImport` behavior and a `FileGrabber` behavior. @@ -281,87 +366,87 @@ Please note: FileGrabber input field does not need to have `'type' => 'file'` se ## Behavior options: * `pathMethod`: The method to use for file paths. This is appended to the `path` option below - * Default: (string) `primaryKey` - * Options: - * flat: Does not create a path for each record. Files are moved to the value of the 'path' option. - * primaryKey: Path based upon the record's primaryKey is generated. Persists across a record update. - * random: Random path is generated for each file upload. Does not persist across a record update. - * randomCombined: Random path - with model id - is generated for each file upload. Does not persist across a record update. + * Default: (string) `primaryKey` + * Options: + * flat: Does not create a path for each record. Files are moved to the value of the 'path' option. + * primaryKey: Path based upon the record's primaryKey is generated. Persists across a record update. + * random: Random path is generated for each file upload. Does not persist across a record update. + * randomCombined: Random path - with model id - is generated for each file upload. Does not persist across a record update. * `path`: A path relative to the `APP_PATH`. Should end in `{DS}` - * Default: (string) `'{ROOT}webroot{DS}files{DS}{model}{DS}{field}{DS}'` - * Tokens: - * {ROOT}: Replaced by a `rootDir` option - * {DS}: Replaced by a `DIRECTORY_SEPARATOR` - * {model}: Replaced by the Model Alias. - * {field}: Replaced by the field name. - * {primaryKey}: Replaced by the record primary key, when available. If used on a new record being created, will have undefined behavior. - * {size}: Replaced by a zero-length string (the empty string) when used for the regular file upload path. Only available for resized thumbnails. - * {geometry}: Replaced by a zero-length string (the empty string) when used for the regular file upload path. Only available for resized thumbnails. + * Default: (string) `'{ROOT}webroot{DS}files{DS}{model}{DS}{field}{DS}'` + * Tokens: + * {ROOT}: Replaced by a `rootDir` option + * {DS}: Replaced by a `DIRECTORY_SEPARATOR` + * {model}: Replaced by the Model Alias. + * {field}: Replaced by the field name. + * {primaryKey}: Replaced by the record primary key, when available. If used on a new record being created, will have undefined behavior. + * {size}: Replaced by a zero-length string (the empty string) when used for the regular file upload path. Only available for resized thumbnails. + * {geometry}: Replaced by a zero-length string (the empty string) when used for the regular file upload path. Only available for resized thumbnails. * `fields`: An array of fields to use when uploading files - * Default: (array) `array('dir' => 'dir', 'type' => 'type', 'size' => 'size')` - * Options: - * dir: Field to use for storing the directory - * type: Field to use for storing the filetype - * size: Field to use for storing the filesize + * Default: (array) `array('dir' => 'dir', 'type' => 'type', 'size' => 'size')` + * Options: + * dir: Field to use for storing the directory + * type: Field to use for storing the filetype + * size: Field to use for storing the filesize * `rootDir`: Root directory for moving images. Auto-prepended to `path` and `thumbnailPath` where necessary - * Default (string) `ROOT . DS . APP_DIR . DS` + * Default (string) `ROOT . DS . APP_DIR . DS` * `mimetypes`: Array of mimetypes to use for validation - * Default: (array) empty + * Default: (array) empty * `extensions`: Array of extensions to use for validation - * Default: (array) empty + * Default: (array) empty * `maxSize`: Max filesize in bytes for validation - * Default: (int) `2097152` + * Default: (int) `2097152` * `minSize`: Minimum filesize in bytes for validation - * Default: (int) `8` + * Default: (int) `8` * `maxHeight`: Maximum image height for validation - * Default: (int) `0` + * Default: (int) `0` * `minHeight`: Minimum image height for validation - * Default: (int) `0` + * Default: (int) `0` * `maxWidth`: Maximum image width for validation - * Default: (int) `0` + * Default: (int) `0` * `minWidth`: Minimum image width for validation - * Default: (int) `0` + * Default: (int) `0` * `deleteOnUpdate`: Whether to delete files when uploading new versions (potentially dangerous due to naming conflicts) - * Default: (boolean) `false` + * Default: (boolean) `false` * `thumbnails`: Whether to create thumbnails or not - * Default: (boolean) `true` + * Default: (boolean) `true` * `thumbnailMethod`: The method to use for resizing thumbnails - * Default: (string) `imagick` - * Options: - * imagick: Uses the PHP `imagick` extension to generate thumbnails - * php: Uses the built-in PHP methods (`GD` extension) to generate thumbnails. Does not support BMP images. + * Default: (string) `imagick` + * Options: + * imagick: Uses the PHP `imagick` extension to generate thumbnails + * php: Uses the built-in PHP methods (`GD` extension) to generate thumbnails. Does not support BMP images. * `thumbnailName`: Naming style for a thumbnail - * Default: `NULL` - * Note: The tokens `{size}` and `{filename}` are both valid for naming and will be auto-replaced with the actual terms. - * Note: As well, the extension of the file will be automatically added. - * Note: When left unspecified, will be set to `{size}_{filename}` or `{filename}_{size}` depending upon the value of `thumbnailPrefixStyle` + * Default: `NULL` + * Note: The tokens `{size}` and `{filename}` are both valid for naming and will be auto-replaced with the actual terms. + * Note: As well, the extension of the file will be automatically added. + * Note: When left unspecified, will be set to `{size}_{filename}` or `{filename}_{size}` depending upon the value of `thumbnailPrefixStyle` * `thumbnailPath`: A path relative to the `rootDir` where thumbnails will be saved. Should end in `{DS}`. If not set, thumbnails will be saved at `path`. - * Default: `NULL` - * Tokens: - * {ROOT}: Replaced by a `rootDir` option - * {DS}: Replaced by a `DIRECTORY_SEPARATOR` - * {model}: Replaced by the Model Alias - * {field}: Replaced by the field name - * {size}: Replaced by the size key specified by a given `thumbnailSize` - * {geometry}: Replaced by the geometry value specified by a given `thumbnailSize` + * Default: `NULL` + * Tokens: + * {ROOT}: Replaced by a `rootDir` option + * {DS}: Replaced by a `DIRECTORY_SEPARATOR` + * {model}: Replaced by the Model Alias + * {field}: Replaced by the field name + * {size}: Replaced by the size key specified by a given `thumbnailSize` + * {geometry}: Replaced by the geometry value specified by a given `thumbnailSize` * `thumbnailPrefixStyle`: Whether to prefix or suffix the style onto thumbnails - * Default: (boolean) `true` prefix the thumbnail - * Note that this overrides `thumbnailName` when `thumbnailName` is not specified in your config + * Default: (boolean) `true` prefix the thumbnail + * Note that this overrides `thumbnailName` when `thumbnailName` is not specified in your config * `thumbnailQuality`: Quality of thumbnails that will be generated, on a scale of 0-100. Not supported gif images when using GD for image manipulation. - * Default: (int) `75` + * Default: (int) `75` * `thumbnailSizes`: Array of thumbnail sizes, with the size-name mapping to a geometry - * Default: (array) empty + * Default: (array) empty * `thumbnailType`: Override the type of the generated thumbnail - * Default: (mixed) `false` or `png` when the upload is a Media file - * Options: - * Any valid image type + * Default: (mixed) `false` or `png` when the upload is a Media file + * Options: + * Any valid image type * `mediaThumbnailType`: Override the type of the generated thumbnail for a non-image media (`pdfs`). Overrides `thumbnailType` - * Default: (mixed) `png` - * Options: - * Any valid image type + * Default: (mixed) `png` + * Options: + * Any valid image type * `saveDir`: Can be used to turn off saving the directory - * Default: (boolean) `true` - * Note: Because of the way in which the directory is saved, if you are using a `pathMethod` other than flat and you set `saveDir` to false, you may end up in situations where the file is in a location that you cannot predict. This is more of an issue for a `pathMethod` of `random` and `randomCombined` than `primaryKey`, but keep this in mind when fiddling with this option + * Default: (boolean) `true` + * Note: Because of the way in which the directory is saved, if you are using a `pathMethod` other than flat and you set `saveDir` to false, you may end up in situations where the file is in a location that you cannot predict. This is more of an issue for a `pathMethod` of `random` and `randomCombined` than `primaryKey`, but keep this in mind when fiddling with this option ## Thumbnail Sizes and Styles