PageRenderTime 35ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/Model/Behavior/UploadBehavior.php

http://github.com/krolow/Attach
PHP | 711 lines | 352 code | 96 blank | 263 comment | 59 complexity | 6e41bacc6a6c6ac3185a164dbd331135 MD5 | raw file
  1. <?php
  2. /**
  3. * Upload for CakePHP.
  4. *
  5. * PHP 5.3
  6. *
  7. *
  8. * Licensed under The MIT License
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @version 1.0
  12. * @link https://github.com/krolow/Attach
  13. * @package Attach.Model.Behavior
  14. * @author Vinícius Krolow <krolow@gmail.com>
  15. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  16. */
  17. App::uses('Attachment', 'Attach.Model');
  18. class UploadBehavior extends ModelBehavior
  19. {
  20. /**
  21. * Imagine Github URL
  22. *
  23. * @var string
  24. */
  25. const IMAGINE_URL = 'https://github.com/avalanche123/Imagine';
  26. /**
  27. * Set what are the multiple models
  28. *
  29. * @var array
  30. */
  31. private $__multiple = array();
  32. /**
  33. * Setup this behavior with the specified configuration settings.
  34. *
  35. * @param Model $model Model using this behavior
  36. * @param array $config Configuration settings for $model
  37. *
  38. * @return void
  39. */
  40. public function setup(Model $model, $config = array()) {
  41. $this->config[$model->alias] = $config;
  42. $this->types[$model->alias] = array_keys($this->config[$model->alias]);
  43. $typeIndex = array_search('Attach.type', $this->types[$model->alias]);
  44. if ($typeIndex !== false) {
  45. unset($this->types[$model->alias][$typeIndex]);
  46. }
  47. foreach ($this->types[$model->alias] as $index => $type) {
  48. $folder = $this->getUploadFolder($model, $type);
  49. $this->isWritable($this->getUploadFolder($model, $type));
  50. $this->_setRelationModel(
  51. $model,
  52. $this->types[$model->alias][$index]
  53. );
  54. }
  55. }
  56. /**
  57. * Create the relation bettween the model and the attachment model for each
  58. * type of file setted in the config
  59. *
  60. * @param Model $model Model using this behavior
  61. * @param string $type Type of the file upload
  62. *
  63. * @return void
  64. */
  65. protected function _setRelationModel(Model $model, $type) {
  66. $relation = 'hasOne';
  67. //case is defined multiple is a hasMany
  68. if ($this->isMultiple($model, $type)) {
  69. $relation = 'hasMany';
  70. }
  71. $type = Inflector::camelize($type);
  72. $model->{$relation}['Attachment' . $type] = array(
  73. 'className' => 'Attachment',
  74. 'foreignKey' => 'foreign_key',
  75. 'dependent' => true,
  76. 'conditions' => array(
  77. 'Attachment' . $type . '.model' => $model->alias,
  78. 'Attachment' . $type . '.type' => Inflector::underscore($type)),
  79. 'fields' => '',
  80. 'order' => ''
  81. );
  82. }
  83. /**
  84. * Check if the given file type is multiple or not
  85. *
  86. * @param Model $model Model using this behavior
  87. * @param string $type Type of the file upload
  88. *
  89. * @return bool
  90. */
  91. public function isMultiple(Model $model, $type) {
  92. return isset($this->config[$model->alias][$type]['multiple'])
  93. && $this->config[$model->alias][$type]['multiple'] == true;
  94. }
  95. /**
  96. * Check if it's necessary validate the file
  97. *
  98. * @param Model $model Model using this behavior
  99. * @param string $validation Name of the validation
  100. * @param array $check Data array of file
  101. *
  102. * @return bool
  103. */
  104. public function shouldValidate($model, $validation, $check) {
  105. if ($this->isPostFileDataEmpty($model, $check)) {
  106. return !$this->isRequired($model, $validation, $check);
  107. }
  108. return false;
  109. }
  110. /**
  111. * Check if the given data is empty
  112. *
  113. * @param Model $model Model using this behavior
  114. * @param array $file File data
  115. *
  116. * @return bool
  117. */
  118. public function isPostFileDataEmpty($model, $file) {
  119. if (!is_array($file)) {
  120. return false;
  121. }
  122. $file = array_shift($file);
  123. return empty($file['name']) && $file['size'] === 0;
  124. }
  125. /**
  126. * Check if the file is required
  127. *
  128. * @param Model $model Model using this behavior
  129. * @param stirng $validation Method name
  130. * @param array $check Data arary of file
  131. *
  132. * @return bool
  133. */
  134. public function isRequired($model, $validation, $check) {
  135. $key = key($check);
  136. if (!isset($model->validate[$key])
  137. || !isset($model->validate[$key]['required'])
  138. ) {
  139. return false;
  140. }
  141. return (bool)$model->validate[$key]['required'];
  142. }
  143. /**
  144. * Check if the file extension it's correct
  145. *
  146. * @param Model $model Model using this behavior
  147. * @param array $check File to be checked
  148. * @param array $extensions The list of allowed extensions
  149. *
  150. * @return bool Return true in case of valid and false in case of invalid
  151. */
  152. public function extension(Model $model, $check, $extensions) {
  153. if ($this->shouldValidate($model, __METHOD__, $check)) {
  154. return true;
  155. }
  156. $check = array_shift($check);
  157. if (isset($check['name'])) {
  158. return in_array(
  159. $this->getFileExtension(
  160. $model,
  161. $check['name']
  162. ),
  163. $extensions
  164. );
  165. }
  166. return false;
  167. }
  168. /**
  169. * Check if the mime type it's correct
  170. *
  171. * @param Model $model Model using this behavior
  172. * @param array $check File to be checked
  173. * @param array $mimes The list of allowed mime types
  174. *
  175. * @return bool Return true in case of valid and false in case of invalid
  176. */
  177. public function mime(Model $model, $check, $mimes) {
  178. if ($this->shouldValidate($model, __METHOD__, $check)) {
  179. return true;
  180. }
  181. $check = array_shift($check);
  182. if (isset($check['tmp_name']) && file_exists($check['tmp_name'])) {
  183. $info = $this->getFileMime($model, $check['tmp_name']);
  184. return in_array($info, $mimes);
  185. }
  186. return false;
  187. }
  188. /**
  189. * Check if the file size it's correct
  190. *
  191. * @param Model $model Model using this behavior
  192. * @param array $check File to be checked
  193. * @param array $size The max size allowed
  194. *
  195. * @return bool Return true in case of valid and false in case of invalid
  196. */
  197. public function size(Model $model, $check, $size) {
  198. if ($this->shouldValidate($model, __METHOD__, $check)) {
  199. return true;
  200. }
  201. $check = array_shift($check);
  202. return $size >= $check['size'];
  203. }
  204. /**
  205. * Check if the image fits within given dimensions
  206. *
  207. * @param Model $model Model using this behavior
  208. * @param array $check File to be checked
  209. * @param int $width Maximum width in pixels
  210. * @param int $height Maximum height in pixels
  211. *
  212. * @return bool Return true if image fits withing given dimensions
  213. */
  214. public function maxDimensions(Model $model, $check, $width, $height) {
  215. if ($this->shouldValidate($model, __METHOD__, $check)) {
  216. return true;
  217. }
  218. $check = array_shift($check);
  219. if (isset($check['tmp_name']) && file_exists($check['tmp_name'])) {
  220. $info = getimagesize($check['tmp_name']);
  221. return ($info && $info[0] <= $width && $info[1] <= $height);
  222. }
  223. return false;
  224. }
  225. /**
  226. * Check if the image fits within given dimensions
  227. *
  228. * @param Model $model Model using this behavior
  229. * @param mixed $check File to be checked
  230. * @param mixed $width Minimum width in pixels
  231. * @param mixed $height Minimum height in pixels
  232. *
  233. * @return bool Return true if image fits within given dimensions
  234. */
  235. public function minDimensions(Model $model, $check, $width, $height) {
  236. if ($this->shouldValidate($model, __METHOD__, $check)) {
  237. return true;
  238. }
  239. $check = array_shift($check);
  240. if (isset($check['tmp_name']) && file_exists($check['tmp_name'])) {
  241. $info = getimagesize($check['tmp_name']);
  242. return ($info && $info[0] >= $width && $info[1] >= $height);
  243. }
  244. return false;
  245. }
  246. /**
  247. * Return the mime type of the given file
  248. *
  249. * @param Model $model Model using this behavior
  250. * @param string $file Path of file
  251. *
  252. * @return string Mimetype
  253. */
  254. public function getFileMime(Model $model, $file) {
  255. $finfo = finfo_open(FILEINFO_MIME_TYPE);
  256. $info = finfo_file($finfo, $file);
  257. return $info;
  258. }
  259. /**
  260. * Get the file extension
  261. *
  262. * @param string $file File to be checked
  263. *
  264. * @return string File extension
  265. */
  266. public function getFileExtension(Model $model, $file) {
  267. return strtolower(pathinfo($file, PATHINFO_EXTENSION));
  268. }
  269. /**
  270. * Return the upload folder that was set for the given type
  271. *
  272. * @param Model $model Model using this behavior
  273. * @param string $type Type of the file upload
  274. *
  275. * @return string Path for the upload folder
  276. */
  277. public function getUploadFolder($model, $type) {
  278. return APP . str_replace(
  279. '{DS}',
  280. DS,
  281. $this->config[$model->alias][$type]['dir']
  282. ) . DS;
  283. }
  284. /**
  285. * Return if the folder is writable
  286. *
  287. * @param string $dir Path of folder
  288. *
  289. * @throws CakeException case the folder is not writable
  290. *
  291. * @return bool return if the folder is writable
  292. */
  293. public function isWritable($dir) {
  294. if (is_dir($dir) && is_writable($dir)) {
  295. return true;
  296. }
  297. throw new CakeException(sprintf('Folder is not writable: %s', $dir));
  298. }
  299. /**
  300. * afterSave is called after a model is saved.
  301. *
  302. * @param Model $model Model using this behavior
  303. * @param boolean $created True if this save created a new record
  304. *
  305. * @return bool
  306. */
  307. public function afterSave(Model $model, $created, $options = array()) {
  308. parent::afterSave($model, $created);
  309. foreach ($this->types[$model->alias] as $type) {
  310. $data = $model->data;
  311. //set multiple as false by standard
  312. $this->__multiple[$model->alias] = false;
  313. if ($this->isMultiple($model, $type)) {
  314. $this->__multiple[$model->alias] = true;
  315. $check = isset($data[$model->alias])
  316. && isset($data[$model->alias][$type])
  317. && is_array($data[$model->alias][$type]);
  318. } else {
  319. $check = isset($data[$model->alias][$type]['tmp_name'])
  320. && !empty($data[$model->alias][$type]['tmp_name']);
  321. }
  322. //case has the file update :)
  323. if ($check) {
  324. if (isset($this->__multiple[$model->alias]) && $this->__multiple[$model->alias]) {
  325. foreach ($data[$model->alias][$type] as $index => $value) {
  326. $this->saveFile($model, $type, $index);
  327. }
  328. } else {
  329. $this->saveFile($model, $type);
  330. }
  331. }
  332. }
  333. }
  334. /**
  335. * Before delete is called before any delete occurs on the attached model,
  336. * but after the model's beforeDelete is called.
  337. * Returning false from a beforeDelete will abort the delete.
  338. *
  339. * @param Model $model Model using this behavior
  340. * @param boolean $cascade If true records that depend on this record will also be deleted
  341. *
  342. * @return mixed False if the operation should abort. Any other result will continue.
  343. */
  344. public function beforeDelete(Model $model, $cascade = true) {
  345. //no delete for us ;)
  346. if ($cascade === false) {
  347. return;
  348. }
  349. foreach ($this->types[$model->alias] as $type) {
  350. $className = 'Attachment' . Inflector::camelize($type);
  351. $attachments = $model->{$className}->find(
  352. 'all',
  353. array(
  354. 'conditions' => array(
  355. 'model' => $model->alias,
  356. 'foreign_key' => $model->id,
  357. ),
  358. )
  359. );
  360. foreach ($attachments as $attach) {
  361. $this->deleteAllFiles($model, $attach);
  362. }
  363. }
  364. return $cascade;
  365. }
  366. /**
  367. * Save the given type of file
  368. *
  369. * @param Model $model Model using this behavior
  370. * @param string $type Type of the file upload
  371. * @param int $index Case is multiple send the index of data
  372. *
  373. * @throws CakeException case the file is not one image
  374. *
  375. * @return void
  376. */
  377. public function saveFile(Model $model, $type, $index = null) {
  378. $uploadData = $model->data[$model->alias][$type];
  379. if (!is_null($index)) {
  380. $uploadData = $uploadData[$index];
  381. }
  382. if (!isset($uploadData['tmp_name']) || empty($uploadData['tmp_name'])) {
  383. return;
  384. }
  385. $file = $model->generateName($type, $index);
  386. $attach = $this->saveAttachment(
  387. $model,
  388. $type,
  389. $file,
  390. $uploadData['name'],
  391. $uploadData['size']
  392. );
  393. if (empty($uploadData['tmp_name'])) {
  394. return;
  395. }
  396. //move file
  397. copy($uploadData['tmp_name'], $file);
  398. $this->deleteFile($uploadData['tmp_name']);
  399. if (!isset($this->config[$model->alias][$type]['thumbs'])) {
  400. return;
  401. }
  402. $info = getimagesize($file);
  403. if (!$info) {
  404. throw new CakeException(
  405. sprintf('The file %s is not an image', $file)
  406. );
  407. }
  408. //generate thumbs
  409. $model->createThumbs($type, $file);
  410. }
  411. /**
  412. * Save the given type of file
  413. *
  414. * @param Model $model Model using this behavior
  415. * @param mixed $attachment Attachment to be deleted
  416. *
  417. * @return void
  418. */
  419. public function deleteAllFiles(Model $model, $attachment) {
  420. $attachment = array_shift($attachment);
  421. $dir = $this->getUploadFolder($model, $attachment['type']);
  422. //delete the original file
  423. $this->deleteFile($dir . $attachment['filename']);
  424. //check if exists thumbs to be deleted too
  425. $files = glob($dir . '*.' . $attachment['filename']);
  426. if (!is_array($files)) {
  427. return;
  428. }
  429. foreach ($files as $fileToDelete) {
  430. $this->deleteFile($fileToDelete);
  431. }
  432. }
  433. /**
  434. * Delete the specific given file
  435. *
  436. * @param string $file File to be checked
  437. *
  438. * @return bool true case was deleted with success
  439. */
  440. public function deleteFile($file) {
  441. if (!file_exists($file)) {
  442. return false;
  443. }
  444. return unlink($file);
  445. }
  446. /**
  447. * Insert attachment into the database
  448. *
  449. * @param Model $model Model using this behavior
  450. * @param string $type Type of the file upload
  451. * @param string $filename Filename to be saved
  452. *
  453. * @return void
  454. */
  455. public function saveAttachment(Model $model, $type, $filename, $originalName = null, $size = null) {
  456. $className = 'Attachment' . Inflector::camelize($type);
  457. $attachment = false;
  458. $attachment = $model->{$className}->find(
  459. 'first',
  460. array(
  461. 'conditions' => array(
  462. 'foreign_key' => $model->id,
  463. 'model' => $model->alias,
  464. 'type' => $type,
  465. ),
  466. )
  467. );
  468. $data = array(
  469. $className => array(
  470. 'model' => $model->alias,
  471. 'foreign_key' => $model->id,
  472. 'filename' => basename($filename),
  473. 'type' => $type,
  474. 'original_name' => $originalName,
  475. 'size' => $size,
  476. ),
  477. );
  478. if (!empty($attachment) && $attachment !== false) {
  479. $this->deleteAllFiles($model, $attachment);
  480. $data[$className]['id'] = $attachment[$className]['id'];
  481. } else {
  482. $model->{$className}->create();
  483. }
  484. $model->data += $model->{$className}->save($data);
  485. }
  486. /**
  487. * Generate an unique name to save the file
  488. *
  489. * @param Model $model Model using this behavior
  490. * @param string $type Type of the file upload
  491. * @param int $index Case is multiple send the index of data
  492. *
  493. * @return string Generated name
  494. * @access public
  495. */
  496. public function generateName(Model $model, $type, $index = null) {
  497. $dir = $this->getUploadFolder($model, $type);
  498. if (is_null($index)) {
  499. $extension = $this->getFileExtension(
  500. $model,
  501. $model->data[$model->alias][$type]['name']
  502. );
  503. } else {
  504. $extension = $this->getFileExtension(
  505. $model,
  506. $model->data[$model->alias][$type][$index]['name']
  507. );
  508. }
  509. if (!is_null($index)) {
  510. return $dir . $type . '_' . $index . '_' . $model->id . '.' . $extension;
  511. }
  512. return $dir . $type . '_' . $model->id . '.' . $extension;
  513. }
  514. /**
  515. * Create thumbs for the given image based in the config
  516. * defined in the model
  517. *
  518. * @param Model $model Model using this behavior
  519. * @param string $type Type of the file upload
  520. * @param string $file Image file
  521. *
  522. * @return void
  523. */
  524. public function createThumbs(Model $model, $type, $file) {
  525. $imagine = $model->getImagine();
  526. $image = $imagine->open($file);
  527. $thumbName = basename($file);
  528. $thumbs = $this->config[$model->alias][$type]['thumbs'];
  529. foreach ($thumbs as $key => $values) {
  530. if (!isset($values['crop'])) {
  531. $values['crop'] = false;
  532. }
  533. $this->_generateThumb(
  534. array(
  535. 'name' => str_replace(
  536. $thumbName,
  537. $key . '.' . $thumbName,
  538. $file
  539. ),
  540. 'w' => $values['w'],
  541. 'h' => $values['h'],
  542. ),
  543. $image,
  544. $values['crop']
  545. );
  546. }
  547. }
  548. /**
  549. * Create a thumb for the given image file based in the parameters passed
  550. *
  551. * @param string $file Image file
  552. * @param string $name Name of the thumb
  553. * @param float $width Width of thumb
  554. * @param float $height Height of thumb
  555. * @param bool $crop Crop the image
  556. *
  557. * @return void
  558. */
  559. public function createThumb(Model $model, $file, $name, $width, $height, $crop = false) {
  560. $imagine = $this->getImagine($model);
  561. $image = $imagine->open($file);
  562. $this->_generateThumb(
  563. array(
  564. 'w' => $width,
  565. 'h' => $height,
  566. 'name' => $name,
  567. ),
  568. $image,
  569. $crop
  570. );
  571. }
  572. /**
  573. * Load the imagine library
  574. *
  575. * @throws CakeException
  576. *
  577. * @return \Imagine\Gd\Imagine
  578. */
  579. public function getImagine(Model $model) {
  580. if (!interface_exists('Imagine\Image\ImageInterface')) {
  581. if (is_file(APP . 'Vendor' . 'autoload.php')) {
  582. require APP . 'Vendor' . 'autoload.php';
  583. }
  584. throw new RuntimeException('We could not autoload imagine, please set the PSR-0 autoload');
  585. }
  586. if (isset($this->config[$model->alias]['Attach.type']) && $this->config[$model->alias]['Attach.type'] == 'Imagick') {
  587. return new \Imagine\Imagick\Imagine();
  588. }
  589. return new \Imagine\Gd\Imagine();
  590. }
  591. /**
  592. * Generate the thumb
  593. *
  594. * @param mixed $thumb 'width', 'height' and 'name'
  595. * @param string $image image file
  596. * @param bool $crop Crop the image
  597. *
  598. * @return void
  599. */
  600. protected function _generateThumb($thumb, $image, $crop = false) {
  601. if ($crop) {
  602. $mode = Imagine\Image\ImageInterface::THUMBNAIL_OUTBOUND;
  603. } else {
  604. $mode = Imagine\Image\ImageInterface::THUMBNAIL_INSET;
  605. }
  606. $thumbnail = $image->thumbnail(
  607. new Imagine\Image\Box(
  608. $thumb['w'],
  609. $thumb['h']
  610. ),
  611. $mode
  612. );
  613. $thumbnail->save($thumb['name']);
  614. }
  615. }