Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Adding an UploadValidatorBehavior and a FileStorageUtils class beside…

…s minor improvements
  • Loading branch information...
commit 4b2504c4413aea0c319d9f1e1f0e415578dbe869 1 parent fe8e51a
@burzum authored
View
2  Config/bootstrap.php
@@ -1,3 +1,3 @@
<?php
App::uses('GaufretteLoader', 'FileStorage.Lib');
-spl_autoload_register(__NAMESPACE__ .'\GaufretteLoader::load');
+spl_autoload_register(__NAMESPACE__ .'\GaufretteLoader::load');
View
77 Lib/Utility/FileStorageUtils.php
@@ -0,0 +1,77 @@
+<?php
+/**
+ * Utility methods for which I could not find a better place
+ *
+ *
+ */
+class FileStorageUtils {
+
+/**
+ * Return file extension from a given filename
+ *
+ * @param string
+ * @return boolean string or false
+ */
+ public static function fileExtension($name) {
+ $list = explode('.', $name);
+ if (count($list) > 1) {
+ $ext = $list[count($list)-1];
+ return $ext;
+ }
+ return false;
+ }
+
+/**
+ * Builds a semi-random path based on a given string to avoid having thousands of files
+ * or directories in one directory. This would result in a slowdown on most file systems.
+ *
+ * Works up to 5 level deep
+ *
+ * @param mixed $string
+ * @param integer $level 1 to 5
+ * @return mixed
+ */
+ public static function randomPath($string, $level = 3) {
+ if (!$string) {
+ throw new \InvalidArgumentException('First argument is not a string!');
+ }
+ $string = crc32($string);
+
+ $decrement = 0;
+ $path = null;
+ for ($i = 0; $i < $level; $i++) {
+ $decrement = $decrement -2;
+ $path .= sprintf("%02d" . DS, substr(str_pad('', 2 * $level, '0') . $string, $decrement, 2));
+ }
+ return $path;
+ }
+
+/**
+ * Helper method to trim last trailing slash in file path
+ *
+ * @param string $path Path to trim
+ * @return string Trimmed path
+ */
+ public static function trimPath($path) {
+ $len = strlen($path);
+ if ($path[$len - 1] == '\\' || $path[$len - 1] == '/') {
+ $path = substr($path, 0, $len - 1);
+ }
+ return $path;
+ }
+
+/**
+ * Converts windows to linux pathes and vice versa
+ *
+ * @param string
+ * @return string
+ */
+ public static function sanitizePath($string) {
+ if (DS == '\\') {
+ return str_replace('\\', '', $string);
+ } else {
+ return str_replace('/', '\\', $string);
+ }
+ }
+
+}
View
234 Model/Behavior/UploadValidatorBehavior.php
@@ -0,0 +1,234 @@
+<?php
+/**
+ * Upload Validation Behavior
+ *
+ * Validates file uploads
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://cakephp.org CakePHP(tm) Project
+ * @package Cake.Model.Behavior
+ * @since CakePHP(tm) v 2.2.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+App::uses('File', 'Utility');
+/**
+ * This behavior will validate uploaded files, nothing more, it won't take care of storage.
+ *
+ * @package Cake.Model.Behavior
+ */
+class UploadValidatorBehavior extends ModelBehavior {
+/**
+ * Settings array
+ *
+ * @var array
+ */
+ public $settings = array();
+
+/**
+ * Default settings array
+ *
+ * @var array
+ */
+ protected $_defaults = array(
+ 'fileField' => 'file',
+ 'validate' => true,
+ 'allowedMime' => null,
+ 'allowedExtensions' => null,
+ 'localFile' => false
+ );
+
+/**
+ * Error message
+ *
+ * If something fails this is populated with an errormsg that can be passed to the view
+ *
+ * @var string
+ */
+ public $uploadError = null;
+
+/**
+ * Behavior setup
+ *
+ * Merge settings with default config, then it is checking if the target directory
+ * exists and if it is writeable. It will throw an error if one of both fails.
+ *
+ * @param AppModel $Model
+ * @param array $settings
+ */
+ public function setup(Model $Model, $settings = array()) {
+ if (!is_array($settings)) {
+ throw new InvalidArgumentException(__d('cake_dev', 'Settings must be passed as array!'));
+ }
+
+ $this->settings = array_merge($this->_defaults, $settings);
+ }
+
+/**
+ * Before validation callback
+ *
+ * Check if the file is really an uploaded file and run custom checks for file
+ * extensions and / or mime type if configured to do so.
+ *
+ * @param AppModel $Model
+ * @return boolean True on success
+ */
+ public function beforeValidate(Model $Model) {
+ extract($this->settings);
+ if ($validate === true && isset($Model->data[$Model->alias][$fileField]) && is_array($Model->data[$Model->alias][$fileField])) {
+
+ if ($Model->validateUploadError($Model->data[$Model->alias][$fileField]['error']) === false) {
+ $Model->validationErrors[$fileField] = $this->uploadError;
+ return false;
+ }
+
+ if (!empty($Model->data[$Model->alias][$fileField])) {
+ if (empty($localFile) && !is_uploaded_file($Model->data[$Model->alias][$fileField]['tmp_name'])) {
+ $this->uploadError = __d('cake_dev', 'The uploaded file is no valid upload.');
+ $Model->invalidate($fileField, $this->uploadError);
+ return false;
+ }
+ }
+
+ if (is_array($allowedMime)) {
+ if (!$this->validateAllowedMimeTypes($Model, $allowedMime)) {
+ return false;
+ }
+ }
+
+ }
+ return true;
+ }
+
+/**
+ * Validates the extension
+ *
+ * @param Model $Model
+ * @return boolean True if the extension is allowed
+ */
+ public function validateUploadExtension(Model $Model) {
+ extract($this->settings);
+ $extension = $this->fileExtension($Model, $Model->data[$Model->alias][$fileField]['name']);
+
+ if (!in_array($extension, $allowedExtensions)) {
+ $this->uploadError = __d('cake_dev', 'You are not allowed to upload files of this type.');
+ $Model->invalidate($fileField, $this->uploadError);
+ return false;
+ }
+ return true;
+ }
+
+/**
+ * Validates if the mime type of an uploaded file is allowed
+ *
+ * @param array Array of allowed mime types
+ * @return boolean
+ */
+ public function validateAllowedMimeTypes(Model $Model, $mimeTypes = array()) {
+ extract($this->settings);
+ if (!empty($mimeTypes)) {
+ $allowedMime = $mimeTypes;
+ }
+ $File = new File($Model->data[$Model->alias][$fileField]['tmp_name']);
+ $mimeType = $File->mime();
+ if (!in_array($mimeType, $allowedMime)) {
+ $this->uploadError = __d('cake_dev', 'You are not allowed to upload files of this type.');
+ $Model->invalidate($fileField, $this->uploadError);
+ return false;
+ }
+ return true;
+ }
+
+/**
+ * Valdates the error value that comes with the file input file
+ *
+ * @param object Model instance
+ * @param integer Error value from the form input [file_field][error]
+ * @return boolean True on success, if false the error message is set to the models field and also set in $this->uploadError
+ */
+ public function validateUploadError(Model $Model, $error = null) {
+ if (!is_null($error)) {
+ switch ($error) {
+ case UPLOAD_ERR_OK:
+ return true;
+ break;
+ case UPLOAD_ERR_INI_SIZE:
+ $this->uploadError = __d('cake_dev', 'The uploaded file exceeds limit of ('.ini_get('upload_max_filesize').').');
+ break;
+ case UPLOAD_ERR_FORM_SIZE:
+ $this->uploadError = __d('cake_dev', 'The uploaded file is to big, please choose a smaller file or try to compress it.');
+ break;
+ case UPLOAD_ERR_PARTIAL:
+ $this->uploadError = __d('cake_dev', 'The uploaded file was only partially uploaded.');
+ break;
+ case UPLOAD_ERR_NO_FILE:
+ $this->uploadError = __d('cake_dev', 'No file was uploaded.');
+ break;
+ case UPLOAD_ERR_NO_TMP_DIR:
+ $this->uploadError = __d('cake_dev', 'The remote server has no temporary folder for file uploads. Please contact the site admin.');
+ break;
+ case UPLOAD_ERR_CANT_WRITE:
+ $this->uploadError = __d('cake_dev', 'Failed to write file to disk. Please contact the site admin.');
+ break;
+ case UPLOAD_ERR_EXTENSION:
+ $this->uploadError = __d('cake_dev', 'File upload stopped by extension. Please contact the site admin.');
+ break;
+ default:
+ $this->uploadError = __d('cake_dev', 'Unknown File Error. Please contact the site admin.');
+ break;
+ }
+ return false;
+ }
+ return true;
+ }
+
+/**
+ * Returns the latest error message
+ *
+ * @param AppModel $Model
+ * @return string
+ * @access public
+ */
+ public function uploadError(Model $Model) {
+ return $this->uploadError;
+ }
+
+/**
+ * Returns an array that matches the structure of a regular upload for a local file
+ *
+ * @param string File with path
+ * @return array Array that matches the structure of a regular upload
+ */
+ public function uploadArray(Model $Model, $file, $filename = null) {
+ $File = new File($file);
+
+ if (empty($fileName)) {
+ $filename = basename($file);
+ }
+
+ return array(
+ 'name' => $filename,
+ 'tmp_name' => $file,
+ 'error' => 0,
+ 'type' => $File->mime(),
+ 'size' => $File->size());
+ }
+
+/**
+ * Return file extension from a given filename
+ *
+ * @param string
+ * @return boolean string or false
+ */
+ public function fileExtension(Model $Model, $name) {
+ return pathinfo($name, PATHINFO_EXTENSION);
+ }
+
+}
View
17 Model/FileStorage.php
@@ -210,4 +210,21 @@ public function stripUuid($uuid) {
return str_replace('-', '', $uuid);
}
+ public function fsPath($type, $string, $idFolder = true) {
+ $string = str_replace('-', '', $string);
+ $path = $type . DS . FileStorageUtils::randomPath($string);
+ if ($idFolder) {
+ $path .= $string . DS;
+ }
+ return $path;
+ }
+
+ public function fileExtension($name = '') {
+ $list = explode('.', $name);
+ if (count($list) > 1) {
+ $ext = $list[count($list)-1];
+ return $ext;
+ }
+ return false;
+ }
}
View
2  Model/Image.php
@@ -124,7 +124,7 @@ public function afterDelete() {
* @return boolean True on success
*/
protected function afterDeleteLocalAdapter() {
- $path = $this->fsBase() . $this->record[$this->alias]['path'];
+ $path = Configure::read('Media.basePath') . $this->record[$this->alias]['path'];
if (is_dir($path)) {
App::uses('Folder', 'Utility');
$Folder = new Folder($path);
View
27 Test/Case/Lib/FileStorageUtilsTest.php
@@ -0,0 +1,27 @@
+<?php
+App::uses('FileStorageUtils', 'FileStorage.Utility');
+/**
+ *
+ */
+class FileStorageUtilsTest extends CakeTestCase {
+/**
+ * testRandomPath
+ *
+ * @return void
+ */
+ public function testRandomPath() {
+ $result = FileStorageUtils::randomPath('someteststring');
+ $this->assertEqual($result, '38\88\98\\');
+ }
+
+/**
+ * testTrimPath
+ *
+ * @return void
+ */
+ public function testTrimPath() {
+ $result = FileStorageUtils::trimPath('foobar/');
+ $this->assertEqual($result, 'foobar');
+ }
+
+}
View
189 Test/Case/Model/Behavior/UploadValidatorBehaviorTest.php
@@ -0,0 +1,189 @@
+<?php
+/**
+ * UploadValidatorBehaviorTest file
+ *
+ * PHP 5
+ *
+ * CakePHP(tm) Tests <http://book.cakephp.org/view/1196/Testing>
+ * Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * Redistributions of files must retain the above copyright notice
+ *
+ * @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link http://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests
+ * @package Cake.Test.Case.Model.Behavior
+ * @since CakePHP(tm) v 2.2.0
+ * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+ */
+App::uses('Model', 'Model');
+App::uses('AppModel', 'Model');
+App::uses('UploadValidatorBehavior', 'FileStorage.Model\Behavior');
+require_once dirname(dirname(__FILE__)) . DS . 'models.php';
+
+/**
+ * UploadValidatorBehaviorTest class
+ *
+ * @package Cake.Test.Case.Model.Behavior
+ */
+class UploadValidatorBehaviorTest extends CakeTestCase {
+
+/**
+ * Holds the instance of the model
+ *
+ * @var mixed
+ */
+ public $Article = null;
+
+/**
+ * Fixtures
+ *
+ * @var array
+ */
+ public $fixtures = array();
+
+/**
+ * startTest
+ *
+ * @return void
+ */
+ public function setUp() {
+ $this->Model = new TheVoid();
+ $this->Model->Behaviors->load('FileStorage.UploadValidator', array(
+ 'localFile' => true));
+ $this->FileUpload = $this->Model->Behaviors->UploadValidator;
+ $this->testFilePath = WEBROOT_DIR . DS . 'img' . DS;
+ }
+
+/**
+ * endTest
+ *
+ * @return void
+ */
+ public function tearDown() {
+ unset($this->Model);
+ ClassRegistry::flush();
+ }
+
+/**
+ * testValidateMime
+ *
+ * @return void
+ */
+ public function testValidateUploadExtension() {
+ $this->Model->Behaviors->unload('FileStorage.UploadValidator');
+ $this->Model->Behaviors->load('FileStorage.UploadValidator', array(
+ 'localFile' => true,
+ 'allowedExtensions' => array('png')));
+ $this->Model->data[$this->Model->alias]['file']['name'] = $this->testFilePath . 'cake.icon.jpg';
+ $this->assertFalse($this->Model->validateUploadExtension());
+
+ $this->Model->data[$this->Model->alias]['file']['name'] = $this->testFilePath . 'cake.icon.png';
+ $this->assertTrue($this->Model->validateUploadExtension());
+ }
+
+/**
+ * testValidateMime
+ *
+ * @return void
+ */
+ public function testValidateMime() {
+ $this->Model->data[$this->Model->alias]['file']['tmp_name'] = $this->testFilePath . 'cake.icon.png';
+ $this->assertFalse($this->Model->validateAllowedMimeTypes(array('application/json')));
+
+ $this->Model->data[$this->Model->alias]['file']['tmp_name'] = $this->testFilePath . 'cake.icon.png';
+ $this->assertTrue($this->Model->validateAllowedMimeTypes(array('image/png')));
+ }
+
+/**
+ * testBeforeValidate
+ *
+ * @return void
+ */
+ public function testBeforeValidate() {
+ $post = array(
+ $this->Model->alias => array(
+ 'file' => array(
+ 'name' => 'cake.power.gif',
+ 'type' => 'image/gif',
+ 'tmp_name' => $this->testFilePath . 'cake.icon.png',
+ 'error' => 0,
+ 'size' => 1212)));
+
+ $post[$this->Model->alias]['file']['error'] = 1;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 2;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 3;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 4;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 6;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 7;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 8;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 8;
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 42; // Unknow code
+ $this->Model->data = $post;
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $post[$this->Model->alias]['file']['error'] = 0;
+ $this->Model->data = $post;
+ $this->assertTrue($this->FileUpload->beforeValidate($this->Model));
+
+ $post[$this->Model->alias]['file']['error'] = null;
+ $this->Model->data = $post;
+ $this->assertTrue($this->FileUpload->beforeValidate($this->Model));
+
+ // Test errors
+ $this->Model->data = $post;
+ $this->FileUpload->setup($this->Model, array('localFile' => false));
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+
+ $this->Model->data = $post;
+ $this->FileUpload->setup($this->Model, array('localFile' => true, 'allowedMime' => array('jpg')));
+ $this->assertFalse($this->FileUpload->beforeValidate($this->Model));
+ $this->assertTrue(isset($this->Model->validationErrors['file']));
+ unset($this->Model->validationErrors['file']);
+ }
+
+}
View
11 View/Helper/FileStorageHelper.php
@@ -0,0 +1,11 @@
+<?php
+class FileStorageHelper extends AppHelper {
+
+ public function link() {
+ $this->__link{$record['adapter']}($record, $)
+ }
+
+ public function _linkLocal() {
+
+ }
+}
View
21 readme.md
@@ -4,6 +4,12 @@ This is work in progress, the code might and very likely will change for some mo
So please do not ask yet or create issue tickets.
+## Requirements
+
+ * CakePHP 2.x
+ * PHP 5.3+
+ * CakeDC Imagine Image processing plugin https://github.com/cakedc/imagine if you want to process and storage images
+
## Installation
To be able to simply autoload Gaufrette load the plugin with bootstrap enabled. The bootstrap file will register the SPL classloader.
@@ -78,8 +84,15 @@ Calling generateHashes is important, it will create the hash values for each ver
If you do not want to have the script generated the hashes each time its execute it is up to you to store it persistant. This plugin just provides you the tools.
-## Requirements
+## Support
- * CakePHP 2.x
- * PHP 5.3+
- * CakeDC Imagine Image processing plugin https://github.com/cakedc/imagine if you want to process and storage images
+For support and feature request, please visit the FileStorage issue page
+
+https://github.com/burzum/FileStorage/issues
+
+## License
+
+Copyright 2012, Florian Krömer
+
+Licensed under The MIT License
+Redistributions of files must retain the above copyright notice.
Please sign in to comment.
Something went wrong with that request. Please try again.