PageRenderTime 29ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/models/behaviors/s3_upload.php

http://github.com/abbasali/S3Upload-Behavior
PHP | 312 lines | 164 code | 25 blank | 123 comment | 27 complexity | daf38f0af80d49e14f9dec95c6721b4e MD5 | raw file
  1. <?php
  2. /**
  3. * Copyright (c) 2010, Abbas Ali. All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. *
  8. * - Redistributions of source code must retain the above copyright notice,
  9. * this list of conditions and the following disclaimer.
  10. * - Redistributions in binary form must reproduce the above copyright
  11. * notice, this list of conditions and the following disclaimer in the
  12. * documentation and/or other materials provided with the distribution.
  13. *
  14. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  15. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  16. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  17. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  18. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  19. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  20. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  21. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  22. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  23. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  24. * POSSIBILITY OF SUCH DAMAGE.
  25. *
  26. * Amazon S3 is a trademark of Amazon.com, Inc. or its affiliates.
  27. */
  28. /**
  29. * S3 Upload Behavior.
  30. *
  31. * This behavior uploads the file to amazon s3 using a third party S3 class written
  32. * by Donovan Schönknecht - download from http://undesigned.org.za/2007/10/22/amazon-s3-php-class.
  33. * Place the S3 class in your vendors directory before using this behavior
  34. *
  35. * As the S3 class itself requires PHP > 5.2, the requirement for this behavior is also PHP > 5.2 and cURL
  36. *
  37. * This behavior uses code from the following
  38. * - http://bin.cakephp.org/view/82605077 by Tane Piper
  39. * - http://www.ad7six.com/entries/view/69/Generic-File-Upload-Behavior by Andy Dawson
  40. *
  41. * @author Abbas Ali <abbas@sanisoft.com>
  42. * @link http://www.sanisoft.com/blog/2010/03/29/amazon-s3-upload-behavior-cakephp
  43. * @version 1.0.0
  44. * @lastmodified 2010-03-29
  45. */
  46. class S3UploadBehavior extends ModelBehavior
  47. {
  48. /**
  49. * Variable to hold the files to be upload to S3
  50. *
  51. * @var array
  52. */
  53. var $files = array();
  54. /**
  55. * AWS access key
  56. *
  57. * @var string
  58. */
  59. var $__accessKey = 'set your aws access key here';
  60. /**
  61. * AWS secret key
  62. *
  63. * @var string
  64. */
  65. var $__secretKey = 'set your aws secret key here';
  66. /**
  67. * Method called automatically by model's constructor
  68. *
  69. * @param object $model Object of model
  70. * @param array $settings Settings for behavior
  71. */
  72. function setup(&$model, $settings = array()) {
  73. // allow to use a config file in app/config/s3.php instead of editing the class directly.
  74. Configure::load('s3');
  75. // Initialize behavior's default settings
  76. $default = array(
  77. 's3_access_key' => Configure::read('s3.access_key'),
  78. 's3_secret_key' => Configure::read('s3.secret_key'),
  79. 'formfield' => '',
  80. 's3_path' => '',
  81. 'allowed_ext' => array('jpg', 'jpeg', 'png', 'gif'),
  82. 's3_request_headers' => array(
  83. 'Expires' => 'Fri, 30 Oct 2030 14:19:41 GMT', //Far future date
  84. 'Cache-control' => 'public',
  85. ),
  86. 's3_meta_headers' => array(),
  87. 's3_acl' => 'public-read',
  88. 'append_salt' => false,
  89. 's3_bucket' => '',
  90. 'required' => false,
  91. 'unique' => true,
  92. );
  93. foreach ($settings as $field => $options) {
  94. // Merge behavior's default settings and model field's settings
  95. $settings = am($default, ife(is_array($options), $options, array()));
  96. // Put the settings in class variable
  97. $this->settings[$model->name][$field] = $settings;
  98. }
  99. }//end setup()
  100. /**
  101. * Convinient method to set AWS credentials
  102. *
  103. * @param string $accessKey AWS access key
  104. * @param string $secretKey AWS secret key
  105. */
  106. function setS3Credentials(&$model, $accessKey, $secretKey) {
  107. $this->__accessKey = $accessKey;
  108. $this->__secretKey = $secretKey;
  109. }//end setS3Credentials()
  110. /**
  111. * Method called automatically by model's save
  112. *
  113. * @param object $model Object of model
  114. * @return boolean Return's true if save should continue else false
  115. */
  116. function beforeSave(&$model) {
  117. foreach ($this->settings[$model->name] as $field => $options) {
  118. $formfield = $field;
  119. if (!empty($options['formfield'])) {
  120. $formfield = $options['formfield'];
  121. }
  122. // If the field is required and file name is empty then invalidate the field
  123. if ($options['required'] && empty($model->data[$model->name][$formfield]['name']) && empty($model->{$model->primaryKey})) {
  124. $model->invalidate($options['formfield'], 'required');
  125. return false;
  126. }
  127. // If no file was selected to upload then continue
  128. if (empty($model->data[$model->name][$formfield]['name'])) {
  129. unset($model->data[$model->name][$formfield]);
  130. continue;
  131. }
  132. // Self explainatory
  133. if (!is_uploaded_file($model->data[$model->name][$formfield]['tmp_name'])) {
  134. $model->invalidate($formfield, 'not_uploaded_file');
  135. return false;
  136. }
  137. // If no bucket for this field has been specified then invalidate the field
  138. if (empty($options['s3_bucket'])) {
  139. $model->invalidate($options['formfield'], 'missing_bucket');
  140. return false;
  141. }
  142. // Check if there is an error in file upload and invalidate the field accordingly
  143. if ($model->data[$model->name][$formfield]['error'] != 0) {
  144. switch($model->data[$model->name][$formfield]['error']) {
  145. case 1:
  146. $model->invalidate($formfield, 'php_max_filesize');
  147. break;
  148. case 2:
  149. $model->invalidate($formfield, 'html_max_filesize');
  150. break;
  151. case 3:
  152. $model->invalidate($formfield, 'partially_uploaded');
  153. break;
  154. case 4:
  155. default:
  156. $model->invalidate($formfield, 'no_file_uploaded');
  157. break;
  158. }
  159. // Return false after invalidating field
  160. return false;
  161. }
  162. // Split the filename to get the name and extension separated
  163. preg_match("/(.+)\.(.*?)\Z/", $model->data[$model->name][$formfield]['name'], $matches);
  164. // If allowed_ext has been set then check that the selected file has a valid extension
  165. if(count($options['allowed_ext'])) {
  166. if (!in_array(low($matches[2]), $options['allowed_ext'])) {
  167. $model->invalidate($formfield, 'forbidden_ext');
  168. return false;
  169. }
  170. }
  171. App::import('Core', 'Sanitize');
  172. // Sanitize the filename. We will only keep letters, numbers, (.), - and _ in filename
  173. $filename = Sanitize::paranoid($model->data[$model->name][$formfield]['name'], array('.', '-', '_'));
  174. // Again split the filename
  175. preg_match("/(.+)\.(.*?)\Z/", $filename, $matches);
  176. // Append a unique salt to the filename. This hopefully will give unique filenames
  177. if ($options['append_salt']) {
  178. $uniqueString = substr(md5(uniqid(mt_rand(), true)), 0, 8);
  179. $filename = $matches[1].'-'.$uniqueString. '.' . $matches[2];
  180. $matches[1] = $matches[1].'-'.$uniqueString;
  181. }
  182. // If the S3 path is set then append it to the filename. S3 has virtual directories
  183. if ($options['s3_path']) {
  184. if (substr($options['s3_path'], -1) != DS) {
  185. $options['s3_path'] .= DS;
  186. }
  187. $filename = $options['s3_path'] . $filename;
  188. $matches[1] = $options['s3_path'] . $matches[1];
  189. }
  190. // If this is an update operation and file is being replaced then we need to remove earlier one
  191. $oldFilename = '';
  192. if (!empty($model->{$model->primaryKey})) {
  193. // Get the current filename
  194. $oldFilename = $model->field($model->name . '.' . $field);
  195. $uniqueConditions[$model->name . '.' . $field . ' <>'] = $model->{$model->primaryKey};
  196. }
  197. // Get unique filename only if append_salt is not true. append_salt should hopefully give unique filename anyways.
  198. // We will query the db table to see if filename already exists
  199. if ($options['unique'] && !$options['append_salt']) {
  200. $uniqueConditions[$model->name . '.' . $field] = $filename;
  201. $i = 1;
  202. while ($model->hasAny($uniqueConditions)) {
  203. $filename = $matches[1] . '-' . $i++ . '.' . $matches[2];
  204. $uniqueConditions[$model->name . '.' . $field] = $filename;
  205. }
  206. }
  207. // Put the file in queue to be uploaded to S3
  208. $this->files[$field] = array(
  209. 'tmp_name' => $model->data[$model->name][$formfield]['tmp_name'],
  210. 'name' => $filename,
  211. 'old_filename' => $oldFilename,
  212. );
  213. }
  214. return $this->__uploadToS3($model);
  215. }//end beforeSave()
  216. /**
  217. * Method to upload file to S3.
  218. * This method also deletes the old files from S3.
  219. *
  220. * @param object $model Object of current model
  221. * @return boolean
  222. */
  223. function __uploadToS3(&$model) {
  224. App::import('Vendor', 'S3', array('file' => 'S3.php'));
  225. // Run a loop on all files to be uploaded to S3
  226. foreach ($this->files as $field => $file) {
  227. $accessKey = $this->__accessKey;
  228. $secretKey = $this->__secretKey;
  229. // If we have S3 credentials for this field/file
  230. if (!empty($this->settings[$model->name][$field]['s3_access_key']) && !empty($this->settings[$model->name][$field]['s3_secret_key'])) {
  231. $accessKey = $this->settings[$model->name][$field]['s3_access_key'];
  232. $secretKey = $this->settings[$model->name][$field]['s3_secret_key'];
  233. }
  234. // Instantiate the class
  235. $aws = new S3($accessKey, $secretKey);
  236. // If there is an old file to be removed
  237. if (!empty($file['old_filename'])) {
  238. $aws->deleteObject($this->settings[$model->name][$field]['s3_bucket'], $file['old_filename']);
  239. }
  240. // Put the object on S3
  241. $isUploaded = $aws->putObject(
  242. $aws->inputResource(fopen($file['tmp_name'], 'rb'), filesize($file['tmp_name'])),
  243. $this->settings[$model->name][$field]['s3_bucket'],
  244. $file['name'],
  245. $this->settings[$model->name][$field]['s3_acl'],
  246. $this->settings[$model->name][$field]['s3_meta_headers'],
  247. $this->settings[$model->name][$field]['s3_request_headers']
  248. );
  249. // If S3 upload failed then set the model error
  250. if ($isUploaded == false) {
  251. $model->invalidate($this->settings[$model->name][$field]['formfield'], 's3_upload_error');
  252. return false;
  253. }
  254. // Set the field values to be saved in table
  255. $model->data[$model->name][$field] = $file['name'];
  256. }
  257. return true;
  258. }//end __uploadToS3()
  259. /**
  260. * Method called automatically by model's delete
  261. *
  262. * @param object $model Object of model
  263. * @return boolean Return's true if delete should continue, false otherwise
  264. */
  265. function beforeDelete(&$model) {
  266. App::import('Vendor', 'S3', array('file' => 'S3.php'));
  267. foreach ($this->settings[$model->name] as $field => $options) {
  268. $accessKey = $this->__accessKey;
  269. $secretKey = $this->__secretKey;
  270. // If we have S3 credentials for this field/file
  271. if (!empty($options['s3_access_key']) && !empty($options['s3_secret_key'])) {
  272. $accessKey = $options['s3_access_key'];
  273. $secretKey = $options['s3_secret_key'];
  274. }
  275. // Instantiate the class
  276. $aws = new S3($accessKey, $secretKey);
  277. // Get model's data for filename of photo
  278. $filename = $model->field($model->name . '.' . $field);
  279. // If filename is found then delete original photo
  280. if (!empty($filename)) {
  281. $aws->deleteObject($options['s3_bucket'], $filename);
  282. }
  283. }
  284. // Return true by default
  285. return true;
  286. }//end beforeDelete()
  287. }//end class