07.26 php saved
The_Cake_Eater
Tags add more
 
Note
Version 1.4.1 of upload behaviour
  1. <?php
  2. /**
  3. *  Improved Upload Behaviour
  4. *
  5. *  This behaviour is based on Chris Partridge's upload behaviour (http://bin.cakephp.org/saved/17539)
  6. *  @author Chris Partridge http://blog.chris-partridge.info/2007/03/16/uploadbehavior-is-here/
  7. *  @author Tane Piper (digitalspaghetti@gmail.com)
  8. *  @author Tijs Teulings
  9. *  @author The_Cake_Eater
  10. *
  11. *  @link http://www.digitalspaghetti.me.uk
  12. *
  13. *  @todo fields dir, mimetype, filesize will break if more than one fields are used for upload
  14. *  @todo option to only overwrite files that were referenced by the same field before
  15. *  @todo add naming mode 'modelid' where the file name is model->id + extension ==> is tricky since the id is not available in beforeSave for new records
  16. *
  17. *  @version 1.4
  18. *
  19. *  How to use
  20. *  ==========
  21. *  1) Download this behaviour and place it in your models/behaviours/upload.php
  22. *  2) Insert the following SQL into your database.  This is a basic model you can expand on:
  23. *        CREATE TABLE `images` (
  24. *          `id` int(8) unsigned NOT NULL auto_increment,
  25. *          `filename` varchar(255) default NULL,
  26. *          `dir` varchar(255) default NULL,
  27. *          `mimetype` varchar(255) NULL,
  28. *          `filesize` int(11) unsigned default NULL,
  29. *          `created` datetime default NULL,
  30. *          `modified` datetime default NULL,
  31. *          PRIMARY KEY  (`id`) ) ENGINE=MyISAM  DEFAULT CHARSET=utf8;
  32. *  3) In your model that you want to have the upload behavior work, place the below code.  This example is for an Image model:
  33. *
  34. *     var $actsAs = array('Upload' => array(
  35. *        'filename' => array(
  36. *           'dir' => 'files/images',
  37. *           'overwrite_existing' => false,
  38. *           'delete_obsolete_files' => true,
  39. *           'naming' => 'filename',
  40. *           'naming_if_exists' => 'increment',
  41. *           'create_directory' => false,
  42. *           'allowed_mime' => array('image/jpeg', 'image/pjpeg', 'image/gif', 'image/png', 'image/x-png'),
  43. *           'allowed_ext'  => array('.jpg', '.jpeg', '.png', '.gif'),
  44. *           'thumbsizes' => array(
  45. *              'small'   =>    array('width'=>100, 'height'=>100),
  46. *              'medium'  =>    array('width'=>220, 'height'=>220),
  47. *              'large'   =>   array('width'=>800, 'height'=>600)
  48. *           )
  49. *        )
  50. *    ));
  51. *
  52. *  The above code will save the uploaded file's name in the 'filename' field in database,
  53. *  it will not overwrite existing files, instead it will create a random filename if
  54. *  it already exists.
  55. *  Allowed Mimetypes and extentions should be pretty explanitory
  56. *  For thumbnails, when the file is uploaded, it will create 3 thumbnail sizes and prepend the name
  57. *  to the thumbfiles (i.e. image_001.jpg will produced thumb.small.image_001.jpg, thumb.medium.image_001.jpg, etc)
  58. *
  59. *  4) Create your upload view, make sure it's a multipart/form-data form, and the filename field is of type $form->file
  60. *  5) Make sure your directory is at least CHMOD 775, also check your php.ini MAX_UPLOAD_SIZE is enough to support the filesizes you are uploading
  61. *
  62. *  Version History
  63. *  ===============
  64. *  1.4.1 26/7/2007 The_Cake_Eater http://bin.cakephp.org/saved/22189
  65. *    - replacing files with files having the same name failed
  66. *  1.4 25/7/2007 The_Cake_Eater http://bin.cakephp.org/saved/22189
  67. *    - fixed the merging of defauilt options with given options
  68. *    + introduced 'naming' option specifying how the name is generatd (filename, random)
  69. *    + introduced 'naming_if_exists' option specifying how alternate names are generated (random, increment)
  70. *    + introduced 'delete_obsolete_files' option specifying if files that are no longer referenced by a field (replaced with a new file)
  71. *                  or files referenced by a deleted record are deleted
  72. *    * refactoring of parts of the code
  73. *    - some minor fixes
  74. *    - some performance improvements
  75. *
  76. *  1.3 12/7/2007 Tijs Teulings (tijs@automatique.nl) http://bin.cakephp.org/saved/21813
  77. *    - default quality to 'high'
  78. *    - fixed proportional resizing
  79. *    - fixed permissions for both thumbs and originals
  80. *
  81. *  1.2 22/6/2007 Tijs Teulings http://bin.cakephp.org/saved/21697
  82. *    * made filename checks case insensitive
  83. *    * removed thumb component dependency
  84. *    - fixes permissions for original file now
  85. *    - fixed handling of file extensions (supports filenames with dots in them now)
  86. *    * rounding new sizes for more reliable results
  87. *
  88. *  1.1
  89. *    * Improved Image scaling code
  90. *    - Fixed check to see if file exists and rename file to unique name
  91. *    * Improved model actsAs to allow more thumbnail sizes
  92. *
  93. *  1.0
  94. *    + Initial release with thumbnail code.
  95. */
  96.  
  97. uses('folder');
  98. uses('file');
  99.  
  100. class UploadBehavior extends ModelBehavior
  101. {
  102.    static $default_options = array('dir' => '',
  103.    'allowed_mime'          => array(),
  104.    'allowed_ext'           => array(),
  105.    'delete_obsolete_files' => true,
  106.    'overwrite_existing'    => false,
  107.    'naming'                => 'filename', // random, filename
  108.    'naming_if_exists'      => 'random',   // random, increment
  109.    'create_directory'      => true,
  110.    'thumbsizes'            => array()
  111.    );
  112.  
  113.    /*
  114.     * temporary registry of obsolete file names, required since obsolete files are identified in beforeSave and beforeDelete
  115.     * but are removed in afterSave and afterDelete
  116.     */
  117.    var $__obsoleteFiles = array();
  118.  
  119.    /*
  120.     * parses the configuration array and sets up the $this->settings property
  121.     */
  122.    function setup(&$model, $config=array())
  123.    {
  124.       $folder = &new Folder;
  125.  
  126.       // iterate over all upload enabled fields
  127.       foreach($config as $field => $options)
  128.       {
  129.          // Check if given field exists
  130.          if(!$model->hasField($field)) {
  131.             unset($config[$field]);
  132.             unset($model->data[$model->name][$field]);
  133.             continue;
  134.          }
  135.  
  136.          // Merge given options with defaults
  137.          $options = array_merge(UploadBehavior::$default_options, $options);
  138.          $config[$field] = $options;
  139.  
  140.          // FIXME this looks odd
  141.          // Generate temporary directory if none provided
  142.          if(empty($options['dir'])) {
  143.             $options['dir'] = 'img' . DS . 'uploads' . DS . $model->name;
  144.          }
  145.  
  146.          // Check if directory exists and create it recursively if required
  147.          if(!is_dir($options['dir'])) {
  148.             if($options['create_directory'] && !$folder->create($options['dir'])) {
  149.                unset($config[$field]);
  150.                unset($model->data[$model->name][$field]);
  151.                continue;
  152.             }
  153.          }
  154.  
  155.          // Check if directory is writable
  156.          if(!is_writable($options['dir'])) {
  157.             unset($config[$field]);
  158.             unset($model->data[$model->name][$field]);
  159.             continue;
  160.          }
  161.  
  162.          // Check that the given directory does not have a DS on the end
  163.          if($options['dir'][strlen($options['dir'])-1] == DS) {
  164.             $options['dir'] = substr($options['dir'],0,strlen($options['dir'])-2);
  165.          }
  166.       }
  167.       $this->settings[$model->name] = $config;
  168.    }
  169.  
  170.    function beforeSave(&$model)
  171.    {
  172.       // return if no fields are configured for file upload
  173.       if(count($this->settings[$model->name]) == 0) return;
  174.  
  175.       foreach($this->settings[$model->name] as $field=>$options)
  176.       {
  177.          if (isset($model->data[$model->name][$field]))
  178.          {
  179.             // Check for upload
  180.             if(isset($model->data) && !is_array($model->data[$model->name][$field]))
  181.             {
  182.                unset($model->data[$model->name][$field]);
  183.                continue;
  184.             }
  185.  
  186.             // Check for error
  187.             if($model->data[$model->name][$field]['error'] > 0)
  188.             {
  189.                unset($model->data[$model->name][$field]);
  190.                continue;
  191.             }
  192.  
  193.             // Fix the file name
  194.             $model->data[$model->name][$field]['name'] = $this->__getFilteredFileName($model->data[$model->name][$field]['name']);
  195.            
  196.             // Check mime type
  197.             if(count($options['allowed_mime']) > 0 && !in_array($model->data[$model->name][$field]['type'], $options['allowed_mime']))
  198.             {
  199.                unset($model->data[$model->name][$field]);
  200.                continue;
  201.             }
  202.  
  203.             // Check if file extension is allowed
  204.             if(count($options['allowed_ext']) > 0)
  205.             {
  206.                $extension = false;
  207.                foreach($options['allowed_ext'] as $allowed_ext)
  208.                {
  209.                   if(substr(strtolower($model->data[$model->name][$field]['name']),-strlen($allowed_ext)) == $allowed_ext)
  210.                   {
  211.                      $extension = $allowed_ext;
  212.                      break;
  213.                   }
  214.                }
  215.  
  216.                if(!$extension)
  217.                {
  218.                   unset($model->data[$model->name][$field]);
  219.                   continue;
  220.                }
  221.             }
  222.  
  223.             $model->data[$model->name][$field]['name'] = $this->__getTargetFileName($options, $model, $field, $extension);
  224.             $saveAs = $options['dir'] . DS . $model->data[$model->name][$field]['name'];
  225.  
  226.             // Attempt to move uploaded file
  227.             if(!move_uploaded_file($model->data[$model->name][$field]['tmp_name'], $saveAs))
  228.             {
  229.                // if moving fails clear the variables and move on to the next field
  230.                unset($model->data[$model->name][$field]);
  231.                unset($saveAs);
  232.                continue;
  233.             }
  234.  
  235.             // Create thumbnail of uploaded image
  236.             $this->__createThumbs($options, $model, $field, $saveAs);
  237.      
  238.             // Attempt to set correct file permissions
  239.             if(!chmod($saveAs, 0755))
  240.             {
  241.                // if chmod fails clear the variable and move on to the next field
  242.                unset($saveAs);
  243.                continue;
  244.             }
  245.  
  246.             // Update model data
  247.             // FIXME dir, mimetype, filesize, dir are the same for multiple upload field
  248.             $model->data[$model->name]['dir'] = $options['dir'];
  249.             $model->data[$model->name]['mimetype'] = $model->data[$model->name][$field]['type'];
  250.             $model->data[$model->name]['filesize'] = $model->data[$model->name][$field]['size'];
  251.             $model->data[$model->name][$field] = $model->data[$model->name][$field]['name'];
  252.  
  253.             if($options['delete_obsolete_files'] && $model->id)
  254.             {
  255.                
  256.                // retrieve the previous file associated with the current field
  257.                $oldFile = $this->__getFieldValueFromDB($model, $field);
  258.                if(empty($oldFile)) continue;
  259.                
  260.                if($oldFile != $model->data[$model->name][$field])
  261.                {
  262.                   // register the file for garbage collection after successful saving of the model
  263.                   $key = $model->name.'#'.$model->id;
  264.                   if(!isset($this->__obsoleteFiles[$key])) $this->__obsoleteFiles[$key] = array();
  265.                   $this->__obsoleteFiles[$key][$field] = $this->__getFieldValueFromDB($model, $field);
  266.                }
  267.             }
  268.          }
  269.       }
  270.    }
  271.  
  272.    function onError(&$model, $error)
  273.    {
  274.       // if an error occured during save operations precautionary unregister files marked as obsolete related to the current model
  275.       if($model->id)
  276.       {
  277.          $key = $model->name.'#'.$model->id;
  278.          unset($this->__obsoleteFiles[$key]);
  279.       }
  280.    }
  281.  
  282.    function afterSave(&$model, $created)
  283.    {
  284.       // check if any upload fields are defined
  285.       if(count($this->settings[$model->name]) == 0) return;
  286.  
  287.       if(!$created) $this->__deleteObsoleteFiles($model)
  288.    }
  289.    
  290.    function afterDelete(&$model)
  291.    {
  292.       // return if no fields are configured for file upload
  293.       if(count($this->settings[$model->name]) == 0) return;
  294.  
  295.       $this->__deleteObsoleteFiles($model);
  296.    }
  297.    
  298.    function beforeDelete(&$model)
  299.    {
  300.       // return if no fields are configured for file upload
  301.       if(count($this->settings[$model->name]) == 0) return;
  302.      
  303.       $fieldNames = array_keys($this->settings[$model->name]);
  304.       $fieldValues = $this->__getFieldValuesFromDB($model, $fieldNames);
  305.  
  306.       foreach($this->settings[$model->name] as $fieldName=>$options)
  307.       {
  308.          if($options['delete_obsolete_files'])
  309.          {
  310.             $dir = $options['dir'];
  311.  
  312.             $fileName = $fieldValues[$fieldName];
  313.            
  314.             // register the file for garbage collection after successful saving of the model
  315.             $key = $model->name.'#'.$model->id;
  316.             if(!isset($this->__obsoleteFiles[$key])) $this->__obsoleteFiles[$key] = array();
  317.             $this->__obsoleteFiles[$key][$fieldName] = $fileName;
  318.          }
  319.       }
  320.       return true;
  321.    }
  322.  
  323.    /**
  324.     * creates a thumbnail image
  325.     */
  326.    function __createThumb($name, $filename, $new_w, $new_h)
  327.    {
  328.       $path_info = pathinfo($name);
  329.       $ext = $path_info['extension'];
  330.  
  331.       if (preg_match('/jpg|jpeg/i', $ext))
  332.       {
  333.          $src_img = imagecreatefromjpeg($name);
  334.       }
  335.  
  336.       if (preg_match('/png/i', $ext))
  337.       {
  338.          $src_img = imagecreatefrompng($name);
  339.       }
  340.  
  341.       $old_x = imagesx($src_img);
  342.       $old_y = imagesy($src_img);
  343.  
  344.       $newSizes = $this->__proportionalResize($old_x, $old_y, $new_w, $new_h);
  345.  
  346.       $thumb_w = $newSizes[0];
  347.       $thumb_h = $newSizes[1];
  348.        
  349.       $dst_img = imagecreatetruecolor($thumb_w, $thumb_h);
  350.       imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $thumb_w, $thumb_h, $old_x, $old_y);
  351.  
  352.       if (preg_match("/png/i", $ext))
  353.       {
  354.          imagepng($dst_img, $filename, 0); // no compression
  355.       } else {
  356.          imagejpeg($dst_img, $filename, 100); // 100% quality
  357.       }
  358.  
  359.       imagedestroy($dst_img);
  360.       imagedestroy($src_img);
  361.    }
  362.  
  363.    function __createThumbs(&$options, &$model, $fieldName, $saveAs)
  364.    {
  365.       // This is hard-coded to only support JPEG + PNG at this time
  366.       // Code unable to handle other formats
  367.       if (count($options['allowed_ext']) > 0 && in_array($model->data[$model->name][$fieldName]['type'], array('image/jpeg', 'image/pjpeg', 'image/png')))
  368.       {
  369.          foreach ($options['thumbsizes'] as $key => $value)
  370.          {
  371.             $this->__createThumb($saveAs, $options['dir'] . DS . 'thumb.' . $key . '.' . $model->data[$model->name][$fieldName]['name'], $value['width'], $value['height']);
  372.          }
  373.       }
  374.    }
  375.  
  376.    function __deleteObsoleteFiles(&$model)
  377.    {
  378.       // check if obsolete files for the current model exist
  379.       $key = $model->name.'#'.$model->id;
  380.       if(!isset($this->__obsoleteFiles[$key])) return;
  381.  
  382.       // get a handle to the obsolete files
  383.       $obsoleteFilesByFieldName = $this->__obsoleteFiles[$key];
  384.       unset($this->__obsoleteFiles[$key]);
  385.  
  386.       foreach($this->settings[$model->name] as $fieldName=>$options)
  387.       {
  388.          if(!isset($obsoleteFilesByFieldName[$fieldName])) continue;
  389.  
  390.          $obsoleteFileForField = $obsoleteFilesByFieldName[$fieldName];
  391.          $dir = $options['dir'];
  392.          
  393.          if(file_exists($dir.DS.$obsoleteFileForField)) unlink($dir.DS.$obsoleteFileForField);
  394.          
  395.          foreach ($options['thumbsizes'] as $key => $value)
  396.          {
  397.             unlink($dir.DS.'thumb.' . $key . '.' . $obsoleteFileForField);
  398.          }
  399.       }
  400.    }
  401.    
  402.    function __getFilteredFileName($filename)
  403.    {
  404.       $filtered_filename  = '';
  405.  
  406.       $patterns = array(
  407.       '/\s/', // Whitespace
  408.       '/\&/', // Ampersand
  409.       '/\+/'  // Plus
  410.       );
  411.       $replacements = array(
  412.       '_',    // Whitespace
  413.       'and'// Ampersand
  414.       'plus'  // Plus
  415.       );
  416.        
  417.       $filename = preg_replace($patterns, $replacements, $filename);
  418.  
  419.       for ($i=0;$i<strlen($filename);$i++)
  420.       {
  421.          $current_char = substr($filename,$i,1);
  422.          if (ctype_alnum($current_char) == TRUE || $current_char == "_" || $current_char == ".")
  423.          {
  424.             $filtered_filename .= $current_char;
  425.          }
  426.       }
  427.  
  428.       return $filtered_filename;
  429.    }
  430.  
  431.    function __getFieldValueFromDB(&$model, $fieldName)
  432.    {
  433.       $rs = $model->find(array($model->escapeField() => $model->id), $fieldName);
  434.       return $rs[$model->name][$fieldName];
  435.    }
  436.    
  437.    function __getFieldValuesFromDB(&$model, $fieldNames)
  438.    {
  439.       $rs = $model->find(array($model->escapeField() => $model->id), $fieldNames);
  440.       return $rs[$model->name];
  441.    }
  442.    
  443.    function __getTargetFileName(&$options, &$model, $field, $extension)
  444.    {
  445.       // Calculate a new name if a naming mode other than 'filename' has been selected
  446.       switch($options['naming'])
  447.       {
  448.          case 'random'$fileName = uniqid() . $extension; break;
  449.          default:        $fileName = substr($model->data[$model->name][$field]['name'], 0, -strlen($extension)) . $extension;
  450.       }
  451.  
  452.       // Calculate final save path
  453.       $saveAs = $options['dir'] . DS . $fileName;
  454.  
  455.       // Check if file with the desired name exists already
  456.       if(file_exists($saveAs))
  457.       {
  458.          // if overwrite is enabled and removing the existing file succeeded, then return the file name directly
  459.          if($options['overwrite_existing'] && unlink($saveAs)) return $fileName;
  460.          
  461.          // find a unique filename by appending a counter to the base name
  462.          if($options['naming_if_exists']=='increment')
  463.          {
  464.             $basename = substr($fileName, 0, -strlen($extension));
  465.             $inc=1;
  466.             do
  467.             {
  468.                $fileName = $basename . '_' . $inc . $extension;
  469.                $saveAs = $options['dir'] . DS . $candidate;
  470.                $inc++;
  471.             }
  472.             while(file_exists($saveAs));
  473.  
  474.             return $fileName;
  475.          }
  476.              
  477.          // find a unique file name by using a UID as the base name
  478.          else
  479.          {
  480.             do
  481.             {
  482.                $fileName = uniqid('') . $extension;
  483.                $saveAs = $options['dir'] . DS . $fileName;
  484.             }
  485.             while(file_exists($saveAs));
  486.          }
  487.       }
  488.       return $fileName;
  489.    }
  490.    
  491.    function __proportionalResize($old_width, $old_height, $new_width = false, $new_height = false)
  492.    {
  493.       if (($new_width === false) && ($new_height === false)) return false;
  494.        
  495.       $old_aspect_ratio = $old_width / $old_height;
  496.        
  497.       if ($new_width === false) {
  498.          $new_width = $new_height * $old_aspect_ratio;
  499.       } elseif ($new_height === false) {
  500.          $new_height = $new_width / $old_aspect_ratio;
  501.       }
  502.       $new_aspect_ratio = $new_width / $new_height;
  503.       if ($new_aspect_ratio == $old_aspect_ratio) {
  504.          // great, done
  505.       } elseif ($new_aspect_ratio < $old_aspect_ratio) {
  506.          // limited by width
  507.          $new_height = $new_width / $old_aspect_ratio;
  508.       } elseif ($new_aspect_ratio > $old_aspect_ratio) {
  509.          // limited by height
  510.          $new_width = $new_height * $old_aspect_ratio;
  511.       }
  512.       return array(round($new_width), round($new_height));
  513.    }
  514. }
  515. ?>
Parsed in 0.691 seconds, using GeSHi 1.0.7.14

Modify this Paste