cakebook / branches / master / models / behaviors / upload.php
history
<?php
/**
* Short description for upload.php
*
* Long description for upload.php
*
* PHP versions 4 and 5
*
* Copyright (c) 2008, Andy Dawson
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @filesource
* @copyright Copyright (c) 2008, Andy Dawson
* @link www.ad7six.com
* @package base
* @subpackage base.models.behaviors
* @since v 1.0
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
/**
* UploadBehavior class
*
* A behavior adding validation and automatic processing for file uploads
* The design of the behavior is to store meta data for uploaded files in a database table (can just be a file
* field in your products table, or a dedicated 'attachments' table) although the behavior can be used with a
* table-less model. Unmodified uploaded files are by default stored OUTSIDE the webroot. This allows the
* application to use the original file as input to generate any different 'versions' that might be required.
* Assuming that generating versions will fail if the file type is not what is expected; this helps defend against
* malicious intentions such as phising
* Suggested example setup would be:
* app
* controllers
* models
* views
* uploads <- destination for pristine uploaded files
* Post
* PostId
* uploadedFile.jpg
* uploadedFile.pdf
* uploadedFile.zip
* webroot <- document root
* files <- root destination for (none-image) uploaded files
* Post
* PostId
* uploadedFile.pdf
* (sanitized)extractedFromZip.doc
* (sanitized)extractedFromZip.rtf
* (sanitized)uploadedFile.zip
* img <- root destination for (image) upload versions
* Post
* PostId
* uploadedFile_small.jpg (see image upload behavior)
* uploadedFile_big.jpg (see image upload behavior)
*
* @uses AppBehavior
* @package base
* @subpackage base.models.behaviors
*/
class UploadBehavior extends ModelBehavior {
/**
* name property
*
* @var string 'Upload'
* @access public
*/
var $name = 'Upload';
/**
* errors property
*
* Array of (system-type) errors encountered when processing an upload
*
* @var array
* @access public
*/
var $errors = array();
/**
* behaviorMap property
*
* Map of which more specific upload behaviors exist
* Used in factoryMode to automatically load the more specific behavior if a match is found based on the data
*
* @var array
* @access private
*/
var $__behaviorMap = array(
'pdf' => array('extension' => 'pdf'),
'archive' => array(
'extension' => array('bz2', 'gz', 'tar', 'zip'),
//'mime' => array('application/zip', 'application/x-tar', 'application/gzip')
),
'image' => array('mime' => 'image/*'),
);
/**
* contentMap property
*
* @var array
* @access private
*/
var $__extContentMap = array(
'bmp' => 'image/bmp',
'bz2' => 'application/x-bzip',
'csv' => 'application/vnd.ms-excel',
'doc' => 'application/msword',
'gif' => 'image/gif',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'pdf' => 'application/pdf',
'png' => 'image/png',
'psd' => 'image/x-psd',
'sql' => 'text/x-sql',
'swf' => 'application/x-shockwave-flash',
'tar' => 'application/x-tar',
'txt' => 'text/plain',
'xls' => 'application/vnd.ms-excel',
'xml' => 'application/xml',
'zip' => 'application/x-zip',
'*' => 'application/octet-stream'
);
/**
* defaultSettings property
*
* @var array
* @access protected
*/
var $_defaultSettings = array(
'dirField' => 'dir',
'fileField' => 'filename',
'extField' => 'ext',
'checksumField' => 'checksum',
'mustUploadFile' => true,
'allowedMime' => '*',
'allowedExt' => '*',
'allowedSize' => '8',// '*' for no limit (in any event limited by php settings)
'allowedSizeUnits' => 'MB',
'overwriteExisting' => true,
'autoCreateVersions' => true,
'baseDir' => '{APP}uploads',
'dirFormat' => '{$class}/{$foreign_id}',// include {$baseDir} to have absolute paths (not recomended)
'fileFormat' => '{$filename}',// include {$dir} to store the dir & filename in one field
'pathReplacements' => array(),
'versions' => array(
'thumb' => array(
'vBaseDir' => '{IMAGES}',
'vDirFormat' => 'types/',
'vFileFormat' => '{$ext}.png',
),
/*
'copy' => array(
'vBaseDir' => '{WWW_ROOT}files/',
'vDirFormat' => '{$dir}/',
'vFileFormat' => '{$filename}',
'callback' => array('copy', '{$vAbsolute}')
)
*/
),
'factoryMode' => true,
);
/**
* autoConfig property
*
* Look for a config file for loading settings
*
* @var bool true
* @access public
*/
var $autoConfig = true;
/**
* setup method
*
* Initialize the component, setup validation rules/messages and check that the base directory is writable
* If the base directory is not writable an error is triggered and the behavior is disabled
*
* @param mixed $model
* @param array $config
* @return void
* @access public
*/
function setup(&$model, $config = array()) {
if ($this->autoConfig && function_exists('autoConfig')) {
autoConfig($this, $model->alias);
}
$this->settings[$model->alias] = am ($this->_defaultSettings, $config);
extract ($this->settings[$model->alias]);
uses('Folder');
$baseDir = $this->_replacePseudoConstants($model, $baseDir);
if (!file_exists($baseDir)) {
new Folder($baseDir, true);
if (!file_exists($baseDir)) {
trigger_error('UploadBehavior::setup Base directory ' . $baseDir . ' doesn\'t exist and cannot be created.');
$model->Behaviors->disable($this->name);
return;
}
} elseif(!is_writable($baseDir)) {
trigger_error('UploadBehavior::setup Base directory ' . $baseDir . ' is not writable.');
$model->Behaviors->disable($this->name);
return;
}
$this->settings[$model->alias]['baseDir'] = $baseDir;
foreach ($this->settings[$model->alias]['versions'] as $key => $settings) {
$this->version($model, $key, $settings);
}
}
/**
* uploadErrors method
*
* @return void
* @access public
*/
function uploadErrors() {
return $this->errors;
}
/**
* version method
*
* Get, Add, modify or delete version settings
*
* @param mixed $model
* @param mixed $key
* @param array $options
* @return void
* @access public
*/
function version(&$model, $key = null, $options = array()) {
if (!$key) {
return $this->settings[$model->alias]['versions'];
}
if ($options === false) {
unset ($this->settings[$model->alias]['versions'][$key]);
return true;
} elseif ($options) {
extract($this->settings[$model->alias]);
$options = am(array('vBaseDir' => $baseDir, 'vDirFormat' => $dirFormat, 'vFileFormat' => $fileFormat . '_' . $key), $options);
if (isset($this->settings[$model->alias]['versions'][$key])) {
$this->settings[$model->alias]['versions'][$key] = am($options, $this->settings[$model->alias]['versions'][$key]);
} else {
$this->settings[$model->alias]['versions'][$key] = $options;
}
}
return $this->settings[$model->alias]['versions'][$key];
}
/**
* absolutePath method
*
* Convenience method
*
* @see path
* @param mixed $model
* @param mixed $id
* @param string $to
* @return void
* @access public
*/
function absolutePath(&$model, $id = null, $to = 'file') {
if ($to == 'file' && isset($data[$model->alias]['original'])) {
return $data[$model->alias]['original'];
}
return $this->_path($model, $id, $to, true);
}
/**
* afterDelete method
*
* Reset if running in factoryMode
*
* @return void
* @access public
*/
function afterDelete(&$model) {
if ($this->name != 'Upload' && $factoryMode && $model->Behaviors->attached('Upload')) {
$model->Behaviors->detatch($this->name);
$model->Behaviors->enable('Upload');
}
}
/**
* afterFind method
*
* If running in factory mode, and a single result is returned (a read/find) delegate to more specific
* behavior if it exists so any methods specific to the type of file to be available
* For each result, add the version info for convenience
*
* @param mixed $model
* @param mixed $results
* @param boolean $primary
* @return void
* @access public
*/
function afterFind(&$model, $results, $primary = false) {
extract ($this->settings[$model->alias]);
if ($factoryMode && $this->name == 'Upload' && count($results) == 1) {
$behavior = $this->__detectBehavior($model, $results[0]);
if ($behavior && $model->Behaviors->attach($behavior, array('factoryMode' => true))) {
$model->Behaviors->disable('Upload');
$model->Behaviors->$behavior->setup($model);
return $model->Behaviors->$behavior->afterFind($model, $results, $primary);
}
}
if ($model->findQueryType != 'list') {
$data = $model->data;
foreach ($results as $i => $result) {
if (!isset($result[$model->alias][$fileField])
|| isset($result[$model->alias]['versions'])) {
return $results;
}
$model->id = $result[$model->alias][$model->primaryKey];
$model->data = $result;
$results[$i][$model->alias]['versions'] = $this->_path($model, null, 'versions');
}
$model->data = $data;
}
return $results;
}
/**
* afterSave method
*
* Reset if running in factoryMode
*
* @param mixed $model
* @param mixed $created
* @return void
* @access public
*/
function afterSave(&$model, $created) {
extract ($this->settings[$model->alias]);
if ($this->name != 'Upload' && $factoryMode && $model->Behaviors->attached('Upload')) {
$model->Behaviors->detach($this->name);
$model->Behaviors->enable('Upload');
}
}
/**
* beforeDelete method
*
* Before deleting the record, delete the associated file(s)
* If running in factory mode, delegate to more specific behavior if it exists
*
* @param mixed $model
* @access public
* @return void
*/
function beforeDelete(&$model) {
extract ($this->settings[$model->alias]);
if ($factoryMode && $this->name == 'Upload') {
$behavior = $this->__detectBehavior($model, $model->data);
if ($behavior && $model->Behaviors->attach($behavior, array('factoryMode' => true))) {
$model->Behaviors->disable('Upload');
$model->Behaviors->$behavior->setup($model);
$model->Behaviors->$behavior->beforeDelete($model);
}
}
return $this->deleteFiles($model);
}
/**
* beforeSave method
*
* If the file field is an array process the file uploaded. No action if there is no file uploaded
* Will prevent saving of 0byte files
* If running in factory mode, delegate to more specific behavior if it exists
*
* @param mixed $model
* @access public
* @return void
*/
function beforeSave(&$model) {
return $this->process($model, $model->data, false);
}
/**
* beforeValidate method
*
* If the associated model is tableless setup the model schema to allow validation errors to be used
* If running in factory mode, delegate to more specific behavior if it exists
*
* @param mixed $model
* @return void
* @access public
*/
function beforeValidate(&$model) {
extract ($this->settings[$model->alias]);
if ($factoryMode && $this->name == 'Upload') {
$behavior = $this->__detectBehavior($model, $model->data);
if ($behavior && $model->Behaviors->attach($behavior, array('factoryMode' => true))) {
$model->Behaviors->disable('Upload');
$model->Behaviors->attach($behavior);
$model->Behaviors->$behavior->setup($model);
return $model->Behaviors->$behavior->beforeValidate($model);
}
}
$this->_setupSchema($model);
$this->_setupValidation($model);
return true;
}
/**
* deleteFiles method
*
* Delete the files for this row - $which can either be 'all', 'original' or 'versions'
* Will automatically delete empty folders after processing if permissions allow ( will not
* raise an error if not possible to delete empty folders)
* If running in factory mode, delegate to more specific behavior if it exists
*
* @param mixed $model
* @param string $which
* @return boolean True on success, false on failure
* @access public
*/
function deleteFiles(&$model, $id = null, $which = 'all') {
if ($id && !is_int($id)) {
$idSchema = $model->schema($model->primaryKey);
if (!is_numeric($id) && $idSchema['length'] != 36) {
$to = $id;
$id = null;
} elseif (is_array($id)) {
extract (array_merge(array('id' => null), $id));
}
}
if (!$id) {
$id = $model->id;
}
extract ($this->settings[$model->alias]);
if ($factoryMode && $this->name == 'Upload') {
$behavior = $this->__detectBehavior($model, $model->data);
if ($behavior && $model->Behaviors->attach($behavior, array('factoryMode' => true))) {
$model->Behaviors->attach($behavior);
$model->Behaviors->$behavior->setup($model);
$return = $model->Behaviors->$behavior->deleteFiles($model, $id, $which);
$model->Behaviors->detach($behavior);
return $return;
}
}
$paths = $this->_path($model, null, 'all', true);
$folders = array();
if (in_array($which, array('all', 'versions'))) {
foreach ($paths['version'] as $file) {
if (file_exists($file) && !unlink($file)) {
$this->errors[] = 'Couldn\'t delete file ' . $file;
}
$folder = dirname($file);
if (!in_array($folder, $folders)) {
$folders[] = $folder;
}
}
}
if (in_array($which, array('all', 'original'))) {
if (file_exists($paths['original']) && !unlink($paths['original'])) {
$this->errors[] = 'Couldn\'t delete file ' . $paths['original'];
}
$folder = dirname($file);
if (!in_array($folder, $folders)) {
$folders[] = $folder;
}
}
foreach ($folders as $folder) {
$dir = new Folder($folder);
if ($dir->read() == array(array(), array())) {
$dir->delete();
}
}
return !$this->errors;
}
/**
* checkUploadedAFile method
*
* Prevent saving a record if no file was uploaded
*
* @param mixed $model
* @param mixed $fieldData
* @return void
* @access public
*/
function checkUploadedAFile(&$model, $fieldData) {
extract ($this->settings[$model->alias]);
if (is_array($fieldData[$fileField]) && $fieldData[$fileField]['error'] == 4) {
return false;
}
return true;
}
/**
* copy method
*
* @param mixed $model
* @param mixed $id
* @param mixed $from
* @param mixed $to
* @return void
* @access public
*/
function copy(&$model, $id = null, $from = null, $to = null) {
if (!$to && !$from) {
$from = $this->absolutePath($model);
$to = $id;
} elseif(!$to && strpos($id, DS) !== false) {
$to = $from;
$from = $id;
}
$path = dirname($to);
new Folder($path, true);
return copy($from, $to);
}
/**
* checkUploadError method
*
* @param mixed $model
* @param mixed $fieldData
* @return boolean true if a file was uploaded successfully, false if an error was encountered
* @access public
*/
function checkUploadError (&$model, $fieldData) {
extract ($this->settings[$model->alias]);
if (isset($fieldData[$fileField]) && is_array($fieldData[$fileField])) {
$fieldData = $fieldData[$fileField];
} else {
return true;
}
if ($fieldData['size'] && $fieldData['error']) {
return false;
}
return true;
}
/**
* checkUploadMime method
*
* Based on the config settings, check the uploaded mime type and reject if not an allowed mime type
* Warning: the mimetype is set by the browser and may be inaccurate/manipulated
*
* @param mixed $model
* @param mixed $fieldData
* @return boolean true if a file is an acceptable mime type, false otherwise
* @access public
*/
function checkUploadMime (&$model, $fieldData) {
extract ($this->settings[$model->alias]);
if (isset($fieldData[$fileField]) && is_array($fieldData[$fileField])) {
$fieldData = $fieldData[$fileField];
} else {
return true;
}
if (!$fieldData['size'] || $allowedMime == '*') {
return true;
}
if (is_array($allowedMime)) {
if (in_array($fieldData['type'], $allowedMime)) {
return true;
}
} elseif ($fieldData['type'] == $allowedMime) {
return true;
}
return false;
}
/**
* checkUploadSize method
*
* If the uploaded file exceeds the config settings - reject.
* Note that file uploads are limited primarily by php's settings
*
* @param mixed $model
* @param mixed $fieldData
* @return boolean true if a file is smaller than the max file size, false otherwise
* @access public
*/
function checkUploadSize (&$model, $fieldData) {
extract ($this->settings[$model->alias]);
if (isset($fieldData[$fileField]) && is_array($fieldData[$fileField])) {
$fieldData = $fieldData[$fileField];
} else {
return true;
}
if (!$fieldData['size']) {
return false;
} elseif( $allowedSize == '*') {
return true;
}
$factor = 1;
switch ($allowedSizeUnits) {
case 'KB':
$factor = 1024;
case 'MB':
$factor = 1024 * 1024;
}
if ($fieldData['size'] < ($allowedSize * $factor)) {
return true;
}
return false;
}
/**
* hasChanged method
*
* Check if the file changed since it was uploaded - by checking if it exists and whether the checksum
* still matches
*
* @param mixed $model
* @param mixed $id
* @return void
* @access public
*/
function hasChanged(&$model, $id = null) {
extract ($this->settings[$model->alias]);
if (!$id) {
$id = $model->id;
}
if (!$model->hasField($checksumField)) {
return false;
}
$file = $this->_path($model, $id, 'file', true);
if (!file_exists($file)) {
return true;
}
return (md5_file($file) != $model->field($checksumField));
}
/**
* metadata method
*
* Get the metadata directly from the file
*
* @param mixed $model
* @param mixed $id
* @param mixed $file
* @param mixed $data
* @return void
* @access public
*/
function metadata(&$model, $id = null, $filename = null, &$data = array()) {
extract ($this->settings[$model->alias]);
if (!$id) {
$id = $model->id;
}
if (!$filename) {
$filename = $this->_path($model, $id, 'file', true);
}
$bits = explode('.', $filename);
if (count($bits) > 1) {
$ext = low(array_pop($bits));
$data[$model->alias][$extField] = $ext;
} else {
$ext = false;
}
if ($ext && isset($this->__extContentMap[$ext])) {
$data[$model->alias]['mimetype'] = $this->__extContentMap[$ext];
} else {
$data[$model->alias]['mimetype'] = $this->__extContentMap['*'];
}
$data[$model->alias]['extension'] = $ext;
if (file_exists($filename)) {
$data[$model->alias]['filesize'] = filesize($filename);
$data[$model->alias]['checksum'] = md5_file($filename);
$data[$model->alias][$fileField] = basename($filename);
}
return $data[$model->alias];
}
/**
* process method
*
* @param mixed $model
* @param array $data
* @param bool $direct
* @return void
* @access public
*/
function process(&$model, &$data = array(), $direct = true) {
extract ($this->settings[$model->alias]);
if ($data) {
$model->data = $data;
}
if ($direct && !$model->validates()) {
return false;
}
if (!isset($model->data[$model->alias]['tempFile'])) {
if (!isset($model->data[$model->alias][$fileField])) {
return true;
} elseif (!is_array($model->data[$model->alias][$fileField])) {
return true;
} elseif (!$model->data[$model->alias][$fileField]['size']) {
return false;
}
}
if ($factoryMode && $this->name == 'Upload') {
$behavior = $this->__detectBehavior($model, $model->data);
if ($behavior && $model->Behaviors->attach($behavior, array('factoryMode' => true))) {
$model->Behaviors->disable('Upload');
$model->Behaviors->attach($behavior);
$model->Behaviors->$behavior->setup($model);
return $model->Behaviors->$behavior->beforeSave($model);
}
}
$import = true;
if (!isset($model->data[$model->alias]['tempFile'])) {
$import = false;
$model->data[$model->alias]['tempFile'] = $model->data[$model->alias][$fileField]['tmp_name'];
}
if (!$this->_beforeProcessUpload($model, $model->data)) {
return false;
}
if ($import) {
if ($model->data[$model->alias]['tempFile'] != $model->data[$model->alias]['original']) {
copy($model->data[$model->alias]['tempFile'], $model->data[$model->alias]['original']);
unlink($model->data[$model->alias]['tempFile']);
}
} else {
if(!move_uploaded_file($model->data[$model->alias]['tempFile'], $model->data[$model->alias]['original'])) {
$this->errors[] = 'Couldn\'t move the uploaded file.';
}
}
$this->_afterProcessUpload($model, $model->data);
return true;
}
/**
* relativePath method
*
* Convenience method
*
* @see path
* @param mixed $model
* @param mixed $id
* @param string $to
* @return void
* @access public
*/
function relativePath(&$model, $id = null, $to = 'file') {
return $this->_path($model, $id, $to, false);
}
/**
* reprocess method
*
* Does not affect the original upload file
* If $clearFolders is true, will delete the containing folder for versions before processing
* useful only if files are organized such that all versions for one file are in the same
* folder, and that folder only contains files for the same upload
* Using the original upload file as the input, regenerate versions and reset size and checksum values
* Useful if behavior settings change (e.g. image thumb size is changed system wide) or the original file
* is overwritten with an updated version
* If running in factory mode, delegate to more specific behavior if it exists
*
* @param mixed $model
* @param mixed $id
* @param boolean $clearFolder
* @return void
* @access public
*/
function reprocess(&$model, $id = null, $clearFolders = false) {
if ($id && !is_int($id)) {
$idSchema = $model->schema($model->primaryKey);
if (!is_numeric($id) && $idSchema['length'] != 36) {
$clearFolders = $id;
$id = null;
} elseif (is_array($id)) {
extract (array_merge(array('id' => null), $id));
}
}
if (!$id) {
$id = $model->id;
}
if (!$id) {
return false;
}
extract ($this->settings[$model->alias]);
if ($factoryMode && $this->name == 'Upload') {
$behavior = $this->__detectBehavior($model, $model->data);
if ($behavior && $model->Behaviors->attach($behavior, array('factoryMode' => true))) {
$model->Behaviors->attach($behavior);
$model->Behaviors->$behavior->setup($model);
$return = $model->Behaviors->$behavior->reprocess($model, $id, $clearFolders);
$model->Behaviors->detach($behavior);
return $return;
}
}
if ($clearFolders) {
$path = $this->_path($model, null, 'all', true);
$folders = array_unique($path['versionDir']);
foreach ($folders as $path) {
if (!file_exists($path)) {
continue;
}
$folder = new Folder($path, false);
$folder->delete();
}
}
$this->_afterProcessUpload($model, $model->read(null, $id));
$data = $this->metaData($model, $id);
$data[$model->primaryKey] = $id;
$model->save($data);
return !$this->errors;
}
/**
* afterProcessUpload method
*
* Process any configured versions
*
* @param mixed $model
* @param mixed $data
* @return boolean
* @access protected
*/
function _afterProcessUpload(&$model, &$data) {
extract($this->settings[$model->alias]);
if (isset($data[$model->alias]['original'])) {
$original = $data[$model->alias]['original'];
} else {
$original = $model->absolutePath();
}
if (file_exists($original)) {
foreach($versions as $id => $vData) {
$this->_clearReplace($model);
$callback = false;
extract($vData);
if (!$callback) {
continue;
}
$vAbsolute = $vBaseDir;
if ($vDirFormat) {
$vAbsolute .= DS . $vDirFormat . DS;
}
$vAbsolute .= $vFileFormat;
$vData['vAbsolute'] = $vAbsolute;
$this->__addReplace($model, '{$vBaseDir}', $vBaseDir);
$this->__addReplace($model, '{$vDirFormat}', $vDirFormat);
$this->__addReplace($model, '{$vFileFormat}', $vFileFormat);
$this->__addReplace($model, '{$vAbsolute}', $vAbsolute);
$vData = $this->_replacePseudoConstants($model, $vData);
extract($vData);
if (!is_array($callback[0])) {
$callback = array($callback);
}
foreach ($callback as $params) {
$method = array_shift($params);
array_unshift($params, $id);
if (!call_user_func_array(array(&$model, $method), $params)) {
array_shift($params);
$this->errors[] = 'failed to perform ' . $method . ' (' . implode ($params, ', ') . ')';
}
}
}
} else {
$this->errors[] = 'Couldn\'t open the original file ' . $original;
}
return !$this->errors;
}
/**
* beforeProcessUpload method
*
* Anything to process before uploading a file
* Set up all the meta data for saving, determine the filename to be saved
*
* @param mixed $model
* @param mixed $data
* @access protected
* @return void
*/
function _beforeProcessUpload(&$model, &$data) {
$this->errors = array();
$this->_clearReplace($model);
extract ($this->settings[$model->alias]);
if (is_array($data[$model->alias][$fileField])) {
$file = $data[$model->alias]['tempFile'] = $data[$model->alias][$fileField]['tmp_name'];
$filename = $data[$model->alias][$fileField]['name'];
$data[$model->alias]['mimetype'] = $data[$model->alias][$fileField]['type'];
$data[$model->alias]['filesize'] = $data[$model->alias][$fileField]['size'];
} else {
$file = $data[$model->alias]['tempFile'];
$this->metaData($model, null, $file, $data);
$filename = $data[$model->alias][$fileField];
}
list($filenameOnly, $extension, $filename) = $this->__filename($model, $fileFormat);
$dir = $this->__path($model, $dirFormat);
uses('Sanitize');
$relativePath = $dir . DS . $filename;
$path = $baseDir . DS . $relativePath;
if(file_exists($path)) {
if($overwriteExisting) {
if(!unlink($path)) {
$this->errors[] = 'The file ' . $relativePath . ' already exists and cannot be deleted.';
}
} else {
$count = 2;
while(file_exists($baseDir . $dir . DS . $filenameOnly . '_' . $count . $extension)) {
$count++;
}
$filename = $filenameOnly .= '_' . $count;
if ($extension) {
$fielname .= '.' . $extension;
}
list($filenameOnly, $extension, $filename) = $this->__filename($model, $filename);
$relativePath = $dir . DS . $filename;
$path = $baseDir . DS . $relativePath;
}
}
$conditions = array();
if ($dirField) {
$conditions[$dirField] = $dir;
}
if ($fileField) {
$conditions[$fileField] = $filename;
}
if ($conditions && $id = $model->field($model->primaryKey, $conditions)) {
if($overwriteExisting) {
$model->id = $id;
} else {
$this->errors[] = 'The file is already in the system';
return false;
}
}
$folder = dirname($path);
if (!new Folder($folder, true)) {
$this->errors[] = 'Could not create the folder ' . $folder;
}
if ($dirField) {
$data[$model->alias][$dirField] = $dir;
}
if ($fileField) {
$data[$model->alias][$fileField] = $filename;
}
if ($extField) {
$data[$model->alias][$extField] = $extension;
}
if ($checksumField) {
$data[$model->alias][$checksumField] = md5_file($file);
}
$data[$model->alias]['relativePath'] = $relativePath;
$data[$model->alias]['original'] = $path;
return !$this->errors;
}
/**
* clearReplace method
*
* Remove existing path replacements in preparation for the next (if any) upload
*
* @param mixed $model
* @return void
* @access protected
*/
function _clearReplace(&$model) {
$this->settings[$model->alias]['pathReplacements'] = array();
extract ($this->settings[$model->alias]);
$this->settings[$model->alias]['baseDir'] = $this->_replacePseudoConstants($model, $baseDir);
$original = $this->absolutePath($model);
if ($original) {
$this->__addReplace($model, '{$original}', $original);
$file = basename($original);
$bits = explode('.', $file);
if (count($bits) > 1) {
$extension = low(array_pop($bits));
$this->__addReplace($model, '{$extension}', $extension);
$file = implode('.', $bits);
$this->__addReplace($model, '{$filenameOnly}', $file);
} else {
$this->__addReplace($model, '{$extension}', '');
$this->__addReplace($model, '{$filenameOnly}', '');
}
}
}
/**
* path method
*
* Return the path to the file, the containing folder (for the original file)
* the versions, a specific version or all
*
* @param mixed $model
* @param mixed $id
* @param string $to 'file', 'folder', 'versions', or 'all'
* @param boolean $absolute
* @return mixed string the file or folder path, or array for versions or all
* @access protected
*/
function _path(&$model, $id = null, $to = null, $absolute = false) {
if ($id && !is_int($id)) {
$idSchema = $model->schema($model->primaryKey);
if (!is_numeric($id) && $idSchema['length'] != 36) {
$absolute = $to;
$to = $id;
$id = null;
} elseif (is_array($id)) {
extract (array_merge(array('id' => null), $id));
}
}
if (!$id) {
$id = $model->id;
}
if ($to === null) {
$to = 'file';
}
if (isset($model->data[$model->alias][$model->primaryKey]) &&
$model->data[$model->alias][$model->primaryKey] != $id) {
$model->read(null, $id);
}
extract ($this->settings[$model->alias]);
if ($factoryMode && $this->name == 'Upload') {
$behavior = $this->__detectBehavior($model, $model->data);
if ($behavior && $model->Behaviors->attach($behavior, array('factoryMode' => true))) {
$model->Behaviors->attach($behavior);
$model->Behaviors->$behavior->setup($model);
$return = $model->Behaviors->$behavior->_path($model, $id, $to, $absolute);
$model->Behaviors->detach($behavior);
return $return;
}
}
if (in_array($to, array('file', 'folder', 'all'))) {
if ($absolute) {
$folder = $baseDir . DS;
} else {
$folder = '';
}
if ($dirField && $model->hasField($dirField)) {
if (isset($model->data[$model->alias][$dirField])) {
$folder .= $model->data[$model->alias][$dirField];
} else {
$folder .= $model->field($dirField);
}
} else {
$folder .= $this->_replacePseudoConstants($model, $dirFormat);
}
if ($to == 'folder') {
return $folder;
}
}
if (in_array($to, array('file', 'all'))) {
if ($absolute) {
$original = $folder . DS;
} else {
$original = '';
}
if ($fileField && isset($model->data[$model->alias][$fileField])) {
if (is_string($model->data[$model->alias][$fileField])) {
$original .= $model->data[$model->alias][$fileField];
} else {
$original .= $model->data[$model->alias][$fileField]['name'];
}
} else {
$original .= $model->field($fileField);
}
if ($to == 'file') {
return $original;
}
}
$this->__filename($model, $model->data[$model->alias][$fileField]);
if (in_array($to, array('versions', 'all')) || isset($versions[$to])) {
if (isset($versions[$to])) {
$versions = array($to => $versions[$to]);
}
foreach ($versions as $key => $details) {
$vBaseDir = $baseDir;
$vDirFormat = $dirFormat;
$vFileFormat = $fileFormat;
extract($details);
if ($absolute) {
$versionDir[$key] = $this->_replacePseudoConstants($model, $vBaseDir) . DS;
} else {
$versionDir[$key] = '';
}
$vDir = $this->_replacePseudoConstants($model, $vDirFormat);
if ($vDir) {
$version[$key] = $versionDir[$key] .= $vDir . DS;
}
$version[$key] .= $this->_replacePseudoConstants($model, $vFileFormat);
}
if ($to == 'versions') {
return $version;
}
}
if (isset($version[$to])) {
return $version[$to];
}
return compact('file', 'folder', 'version', 'versionDir');
}
/**
* replacePseudoConstants method
*
* for the passed string look for and replace any pseudo constants.
* {CONSTANT} will be replaced with the defined CONSTANT (if it's defined)
* {$dataVariable} will be replaced with $this->data['ModelAlias']['dataVariable'] if it is set
* {$databaseField} will be replaced with $model->field('databaseField');
* {$random} will be replaced with a random 5 digit number, regenerated each time this method is called
*
* @param mixed $model
* @param mixed $string
* @return boolean true on success, false on error
* @access protected
*/
function _replacePseudoConstants(&$model, $string) {
extract($this->settings[$model->alias]);
if (is_array($string)) {
foreach ($string as $i => $str) {
$string[$i] = $this->_replacePseudoConstants($model, $str);
}
return $string;
}
$_replacements = $this->settings[$model->alias]['pathReplacements'];
$random = uniqid('');
$random = substr($random, strlen($random) -5, strlen($random));
preg_match_all('@{\$?([^{}]*)}@', $string, $r);
foreach ($r[1] as $i => $match) {
$_found = false;
if (!isset($this->settings[$model->alias]['pathReplacements'][$r[0][$i]])) {
if (up($match) == $match) {
$constants = get_defined_constants();
if (isset($constants[$match])) {
$this->__addReplace($model, $r[0][$i], $constants[$match]);
$_found = true;
}
if (!$_found) {
$this->errors[] = 'Cannot replace ' . $match . ' as the constant ' . $match . ' is not defined.';
}
} else {
if (isset($$match)) {
$this->__addReplace($model, $r[0][$i], $$match);
$_found = true;
} elseif (isset($model->data[$model->alias][$match])) {
$this->__addReplace($model, $r[0][$i], $model->data[$model->alias][$match]);
$_found = true;
} elseif ($model->id && $model->hasField($match)) {
$this->__addReplace($model, $r[0][$i], $model->field($match));
$_found = true;
}
if (!$_found) {
$this->errors[] = 'Cannot replace ' . $match . ' as the variable $' . $match . ' cannot be determined.';
$this->errors[] = $model->data;
}
}
}
}
$markers = array_keys($this->settings[$model->alias]['pathReplacements']);
$replacements = array_values($this->settings[$model->alias]['pathReplacements']);
$this->settings[$model->alias]['pathReplacements'] = $_replacements;
return str_replace ($markers, $replacements, $string);
}
/**
* setupSchema method
*
* @TODO How to do this without directly accessing the _schema field
* @param mixed $model
* @return void
* @access protected
*/
function _setupSchema($model) {
$schema = $model->schema();
extract ($this->settings[$model->alias]);
if (!$schema) {
$model->_schema = $schema[$fileField] = array(
'type' => 'string',
'null' => null,
'default' => null,
'length' => 100
);
}
}
/**
* setupValidation method
*
* Add validation rules specific to this behavior. Prepend the behaviors validation rules
* To allow the behavior to modify the model's data for any other validation rules
*
* @param mixed $model
* @return void
* @access protected
*/
function _setupValidation(&$model) {
extract ($this->settings[$model->alias]);
if (isset($model->validate[$fileField])) {
$existingValidations = $model->validate[$fileField];
if (!is_array($existingValidations)) {
$existingValidations = array($existingValidations);
}
} else {
$existingValidations = array();
}
if ($mustUploadFile) {
$validations['uploadAFile'] = array(
'on' => 'create',
'rule' => 'checkUploadedAFile',
'message' => 'Please select a file to upload.',
'last' => true
);
}
$validations['uploadError'] = array(
'rule' => 'checkUploadError',
'message' => 'An error was generated during the upload.',
'last' => true
);
if (is_array($allowedMime)) {
$allowedMimes = implode(',', $allowedMime);
} else {
$allowedMimes = $allowedMime;
}
$validations['uploadMime'] = array(
'rule' => 'checkUploadMime',
'message' => 'The submitted mime type is not permitted, only ' . $allowedMimes . ' permitted.',
'last' => true
);
if ($allowedExt != '*') {
if (is_array($allowedExt)) {
$allowedExts = implode(',', $allowedExt);
} else {
$allowedExts = $allowedExt;
$allowedExt = array($allowedExt);
}
$validations['uploadExt'] = array(
'rule' => array('extension', $allowedExt),
'message' => 'The submitted file extension is not permitted, only ' . $allowedExts . ' permitted.',
'last' => true
);
}
$validations['uploadSize'] = array(
'rule' => 'checkUploadSize',
'message' => 'The file uploaded is too big, only files less than ' . $allowedSize . ' ' . $allowedSizeUnits .' permitted.',
'last' => true
);
$model->validate[$fileField] = am($validations, $existingValidations);
}
/**
* addReplace method
*
* Add a find and replace pair
*
* @param mixed $model
* @param mixed $find
* @param string $replace
* @return void
* @access private
*/
function __addReplace(&$model, $find, $replace = '') {
if (is_array($find)) {
foreach ($find as $f => $r) {
$this->__addReplace($model, $f, $r);
}
return;
}
if (is_array($replace)) {
$replace = array_shift($replace);
}
$replace = $this->_replacePseudoConstants($model, $replace);
$this->settings[$model->alias]['pathReplacements'][$find] = $replace;
}
/**
* detectBehavior method
*
* Based on the passed data, check if a more specific behavior exists and if so return its name
*
* @param mixed $model
* @param mixed $data
* @return mixed false if no behavior matches, the name of the matched behavior otherwise
* @access private
*/
function __detectBehavior(&$model, &$data = null) {
extract ($this->settings[$model->alias]);
if (!$data && $model->id) {
$_data = $model->data;
$data = $model->read();
$model->data = $_data;
}
$mime = false;
if (isset($data[$model->alias][$fileField]) && is_array($data[$model->alias][$fileField])) {
$mime = $data[$model->alias][$fileField]['type'];
} elseif (isset($data[$model->alias]['mimetype'])) {
$mime = $data[$model->alias]['mimetype'];
}
$extension = false;
if (isset($data[$model->alias][$fileField]) && is_array($data[$model->alias][$fileField])) {
$file = $data[$model->alias][$fileField]['name'];
} elseif (isset($data[$model->alias][$fileField])) {
$file = $data[$model->alias][$fileField];
} else {
return false;
}
$bits = explode('.', $file);
if (count($bits) > 1) {
$extension = low(array_pop($bits));
}
$behavior = false;
foreach ($this->__behaviorMap as $type => $tests) {
$match = 0;
foreach ($tests as $field => $value) {
switch ($field) {
case 'mime':
$value = str_replace('*', '', $value);
if (strpos($mime, $value) !== false) {
$match++;
}
case 'extension':
if (is_string($value)) {
if ($extension == $value) {
$match++;
}
} elseif (in_array($extension, $value)) {
$match++;
}
}
}
if ($match == count($tests)) {
$behavior = $type;
break;
}
}
if ($behavior) {
$behavior = Inflector::classify($behavior) . 'Upload';
} else {
return false;
}
$behaviors = Configure::listObjects('behavior');
if (in_array($behavior, $behaviors)) {
return $behavior;
}
return false;
}
/**
* filename method
*
* Clean the string and return something suitable to use as a filename
* Allows double file extensions (.tar.gz)
*
* @param mixed $model
* @param mixed $string
* @return array a 'filename safe' string, the extension, the full filename
* @access private
*/
function __filename(&$model, $string) {
extract ($this->settings[$model->alias]);
if (strpos($string,'{') !== false) {
$string = low($this->_replacePseudoConstants($model, $string));
}
$string = str_replace('__dot__', '.', Inflector::slug(str_replace('.', '__dot__', $string)));
$bits = explode('.', $string);
if (count($bits) > 1) {
$ext = low(array_pop($bits));
} else {
$ext = false;
}
$filename = $full = implode('.', $bits);
if($ext) {
$full .= '.' . $ext;
}
$this->__addReplace($model, '{$filenameOnly}', $filename);
$this->__addReplace($model, '{$extension}', $ext);
$this->__addReplace($model, '{$filename}', $full);
return array($filename, $ext, $full);
}
/**
* path method
*
* Replace any pseudo constants and create the folder
*
* @param mixed $model
* @param mixed $path
* @return string the path to the folder
* @access private
*/
function __path (&$model, $path) {
extract ($this->settings[$model->alias]);
if (strpos($path,'{') !== false) {
$path = $this->_replacePseudoConstants($model, $path);
}
if (!$path) {
$this->errors[] = 'Couldn\'t determine the path ' . $dir;
return false;
} else {
if (!(new Folder ($baseDir . DS . $path, true))) {
$this->errors[] = 'Couldn\'t create the path ' . $dir;
return false;
};
}
$this->__addReplace($model, '{$dir}', $path);
return $path;
}
}
?>