リビジョン b821d57d

差分を見る:

app/Config/bootstrap.php
63 63
 * Uncomment one of the lines below, as you need. Make sure you read the documentation on CakePlugin to use more
64 64
 * advanced ways of loading plugins
65 65
 *
66
*CakePlugin::load('DebugKit'); // Loads a single plugin named DebugKit
66 67
 * CakePlugin::loadAll(); // Loads all plugins at once
67 68
 */
68
CakePlugin::load('DebugKit'); // Loads a single plugin named DebugKit
69
  CakePlugin::loadAll(); // Loads all plugins at once
69 70

  
70 71
/**
71 72
 * To prefer app translation over plugin translation, you can set
app/Plugin/UploadPack/Model/Behavior/UploadBehavior.php
1
<?php
2
App::uses('HttpSocket', 'Network/Http');
3
/**
4
 * This file is a part of UploadPack - a plugin that makes file uploads in CakePHP as easy as possible.
5
 *
6
 * UploadBehavior
7
 *
8
 * UploadBehavior does all the job of saving files to disk while saving records to database. For more info read UploadPack documentation.
9
 *
10
 * joe bartlett's lovingly handcrafted tweaks add several resize modes. see "more on styles" in the documentation.
11
 *
12
 * @author Michał Szajbe (michal.szajbe@gmail.com) and joe bartlett (contact@jdbartlett.com)
13
 * @link http://github.com/szajbus/uploadpack
14
 */
15
class UploadBehavior extends ModelBehavior {
16

  
17
    private static $__settings = array();
18

  
19
    private $toWrite = array();
20

  
21
    private $toDelete = array();
22

  
23
    private $maxWidthSize = false;
24

  
25
    public function setup(Model $model, $settings = array()) {
26
        $defaults = array(
27
            'path' => ':webroot/upload/:model/:id/:basename_:style.:extension',
28
            'styles' => array(),
29
            'resizeToMaxWidth' => false,
30
            'quality' => 75,
31
            'alpha' => false
32
        );
33

  
34
        foreach ($settings as $field => $array) {
35
            self::$__settings[$model->name][$field] = array_merge($defaults, $array);
36
        }
37
    }
38

  
39
    public function beforeSave(Model $model, $options = array()) {
40
        $this->_reset();
41
        foreach (self::$__settings[$model->name] as $field => $settings) {
42
            if (!empty($model->data[$model->name][$field]) && is_array($model->data[$model->name][$field]) && file_exists($model->data[$model->name][$field]['tmp_name'])) {
43
                if (!empty($model->id)) {
44
                    $this->_prepareToDeleteFiles($model, $field, true);
45
                }
46
                $this->_prepareToWriteFiles($model, $field);
47
                unset($model->data[$model->name][$field]);
48
                $model->data[$model->name][$field.'_file_name'] = $this->toWrite[$field]['name'];
49
                $model->data[$model->name][$field.'_file_size'] = $this->toWrite[$field]['size'];
50
                $model->data[$model->name][$field.'_content_type'] = $this->toWrite[$field]['type'];
51
            } elseif (array_key_exists($field, $model->data[$model->name]) && $model->data[$model->name][$field] === null) {
52
                if (!empty($model->id)) {
53
                    $this->_prepareToDeleteFiles($model, $field, true);
54
                }
55
                unset($model->data[$model->name][$field]);
56
                $model->data[$model->name][$field.'_file_name'] = null;
57
                $model->data[$model->name][$field.'_file_size'] = null;
58
                $model->data[$model->name][$field.'_content_type'] = null;
59
            }
60
        }
61
        return true;
62
    }
63

  
64
    public function afterSave(Model $model, $create, $options = array()) {
65
        if (!$create) {
66
            $this->_deleteFiles($model);
67
        }
68
        $this->_writeFiles($model);
69
    }
70

  
71
    public function beforeDelete(Model $model, $cascade = true) {
72
        $this->_reset();
73
        $this->_prepareToDeleteFiles($model);
74
        return true;
75
    }
76

  
77
    public function afterDelete(Model $model) {
78
        $this->_deleteFiles($model);
79
    }
80

  
81
    public function beforeValidate(Model $model, $options = array()) {
82
        foreach (self::$__settings[$model->name] as $field => $settings) {
83
            if (isset($model->data[$model->name][$field])) {
84
                $data = $model->data[$model->name][$field];
85

  
86
                if ((empty($data) || is_array($data) && empty($data['tmp_name'])) && !empty($settings['urlField']) && !empty($model->data[$model->name][$settings['urlField']])) {
87
                    $data = $model->data[$model->name][$settings['urlField']];
88
                }
89

  
90
                if (!is_array($data)) {
91
                    $model->data[$model->name][$field] = $this->_fetchFromUrl($data);
92
                }
93
            }
94
        }
95
        return true;
96
    }
97

  
98
    private function _reset() {
99
        $this->toWrite = null;
100
        $this->toDelete = null;
101
    }
102

  
103
    private function _fetchFromUrl($url) {
104
        $path_chunks = explode('/', $url);
105
        $filename_chunks = explode('.', $url);
106
        
107
        $data = array('remote' => true);
108
        $data['name'] = end($path_chunks);
109
        $data['tmp_name'] = tempnam(sys_get_temp_dir(), $data['name']) . '.' . end($filename_chunks);
110

  
111
        $httpSocket = new HttpSocket();
112
        $raw = $httpSocket->get($url);
113
        $response = $httpSocket->response;
114
        $content_types = explode(';', $response['header']['Content-Type']);
115
        $data['size'] = strlen($raw);
116
        $data['type'] = reset($content_types);
117

  
118
        file_put_contents($data['tmp_name'], $raw);
119
        return $data;
120
    }
121

  
122
    private function _prepareToWriteFiles(&$model, $field) {
123
        $this->toWrite[$field] = $model->data[$model->name][$field];
124
        // make filename URL friendly by using Cake's Inflector
125
        $this->toWrite[$field]['name'] =
126
            Inflector::slug(substr($this->toWrite[$field]['name'], 0, strrpos($this->toWrite[$field]['name'], '.'))). // filename
127
            substr($this->toWrite[$field]['name'], strrpos($this->toWrite[$field]['name'], '.')); // extension
128
    }
129

  
130
    protected function afterMove($file) {
131
        // do nothing here
132
    }
133

  
134
    private function _writeFiles(&$model) {
135
        if (!empty($this->toWrite)) {
136
            foreach ($this->toWrite as $field => $toWrite) {
137
                $settings = $this->_interpolate($model, $field, $toWrite['name'], 'original');
138
                $destDir = dirname($settings['path']);
139
                if (!file_exists($destDir)) {
140
                    @mkdir($destDir, 0777, true);
141
                    @chmod($destDir, 0777);
142
                }
143
                if (is_dir($destDir) && is_writable($destDir)) {
144
                    $move = !empty($toWrite['remote']) ? 'rename' : 'move_uploaded_file';
145
                    if (@$move($toWrite['tmp_name'], $settings['path'])) {
146
                        $this->afterMove($settings['path']);  // <==== Calling afterMove() callback method
147
                        if($this->maxWidthSize) {
148
                            $this->_resize($settings['path'], $settings['path'], $this->maxWidthSize.'w', $settings['quality'], $settings['alpha']);
149
                        }
150
                        foreach ($settings['styles'] as $style => $geometry) {
151
                            $newSettings = $this->_interpolate($model, $field, $toWrite['name'], $style);
152
                            $this->_resize($settings['path'], $newSettings['path'], $geometry, $settings['quality'], $settings['alpha']);
153
                        }
154
                    }
155
                }
156
            }
157
        }
158
    }
159

  
160
    private function _prepareToDeleteFiles(&$model, $field = null, $forceRead = false) {
161
        $needToRead = true;
162
        if ($field === null) {
163
            $fields = array_keys(self::$__settings[$model->name]);
164
            foreach ($fields as &$field) {
165
                $field .= '_file_name';
166
            }
167
        } else {
168
            $field .= '_file_name';
169
            $fields = array($field);
170
        }
171

  
172
        if (!$forceRead && !empty($model->data[$model->alias])) {
173
            $needToRead = false;
174
            foreach ($fields as $field) {
175
                if (!array_key_exists($field, $model->data[$model->alias])) {
176
                    $needToRead = true;
177
                    break;
178
                }
179
            }
180
        }
181
        if ($needToRead) {
182
            $data = $model->find('first', array('conditions' => array($model->alias.'.'.$model->primaryKey => $model->id), 'fields' => $fields, 'callbacks' => false));
183
        } else {
184
            $data = $model->data;
185
        }
186
        if (is_array($this->toDelete)) {
187
            $this->toDelete = array_merge($this->toDelete, $data[$model->alias]);
188
        } else {
189
            $this->toDelete = $data[$model->alias];
190
        }
191
        $this->toDelete['id'] = $model->id;
192
    }
193

  
194
    private function _deleteFiles(&$model) {
195
        foreach (self::$__settings[$model->name] as $field => $settings) {
196
            if (!empty($this->toDelete[$field.'_file_name'])) {
197
                $styles = array_keys($settings['styles']);
198
                $styles[] = 'original';
199
                foreach ($styles as $style) {
200
                    $settings = $this->_interpolate($model, $field, $this->toDelete[$field.'_file_name'], $style);
201
                    if (file_exists($settings['path'])) {
202
                        @unlink($settings['path']);
203
                    }
204
                }
205
            }
206
        }
207
    }
208

  
209
    private function _interpolate(&$model, $field, $filename, $style) {
210
        return self::interpolate($model->name, $model->id, $field, $filename, $style);
211
    }
212

  
213
    static public function interpolate($modelName, $modelId, $field, $filename, $style = 'original', $defaults = array()) {
214
        $pathinfo = UploadBehavior::_pathinfo($filename);
215
        $interpolations = array_merge(array(
216
            'app' => preg_replace('/\/$/', '', APP),
217
            'webroot' => preg_replace('/\/$/', '', WWW_ROOT),
218
            'model' => Inflector::tableize($modelName),
219
            'basename' => !empty($filename) ? $pathinfo['filename'] : null,
220
            'extension' => !empty($filename) ? $pathinfo['extension'] : null,
221
            'id' => $modelId,
222
            'style' => $style,
223
            'attachment' => Inflector::pluralize($field),
224
            'hash' => md5((!empty($filename) ? $pathinfo['filename'] : "") . Configure::read('Security.salt'))
225
        ), $defaults);
226
        $settings = self::$__settings[$modelName][$field];
227
        $keys = array('path', 'url', 'default_url');
228
        foreach ($interpolations as $k => $v) {
229
            foreach ($keys as $key) {
230
                if (isset($settings[$key])) {
231
                    $settings[$key] = preg_replace('/\/{2,}/', '/', str_replace(":$k", $v, $settings[$key]));
232
                }
233
            }
234
        }
235
        return $settings;
236
    }
237

  
238
    static private function _pathinfo($filename) {
239
        $pathinfo = pathinfo($filename);
240
        // PHP < 5.2.0 doesn't include 'filename' key in pathinfo. Let's try to fix this.
241
        if (empty($pathinfo['filename'])) {
242
            $suffix = !empty($pathinfo['extension']) ? '.'.$pathinfo['extension'] : '';
243
            $pathinfo['filename'] = basename($pathinfo['basename'], $suffix);
244
        }
245
        return $pathinfo;
246
    }
247

  
248
    private function _resize($srcFile, $destFile, $geometry, $quality = 75, $alpha = false) {
249
        copy($srcFile, $destFile);
250
        @chmod($destFile, 0777);
251
        $pathinfo = UploadBehavior::_pathinfo($srcFile);
252
        $src = null;
253
        $createHandler = null;
254
        $outputHandler = null;
255
        switch (strtolower($pathinfo['extension'])) {
256
        case 'gif':
257
            $createHandler = 'imagecreatefromgif';
258
            $outputHandler = 'imagegif';
259
            break;
260
        case 'jpg':
261
        case 'jpeg':
262
            $createHandler = 'imagecreatefromjpeg';
263
            $outputHandler = 'imagejpeg';
264
            break;
265
        case 'png':
266
            $createHandler = 'imagecreatefrompng';
267
            $outputHandler = 'imagepng';
268
            $quality = null;
269
            break;
270
        default:
271
            return false;
272
        }
273
        if ($src = $createHandler($destFile)) {
274
            $srcW = imagesx($src);
275
            $srcH = imagesy($src);
276

  
277
            // determine destination dimensions and resize mode from provided geometry
278
            if (preg_match('/^\\[[\\d]+x[\\d]+\\]$/', $geometry)) {
279
                // resize with banding
280
                list($destW, $destH) = explode('x', substr($geometry, 1, strlen($geometry)-2));
281
                $resizeMode = 'band';
282
            } elseif (preg_match('/^[\\d]+x[\\d]+$/', $geometry)) {
283
                // cropped resize (best fit)
284
                list($destW, $destH) = explode('x', $geometry);
285
                $resizeMode = 'best';
286
            } elseif (preg_match('/^[\\d]+w$/', $geometry)) {
287
                // calculate heigh according to aspect ratio
288
                $destW = (int)$geometry-1;
289
                $resizeMode = false;
290
            } elseif (preg_match('/^[\\d]+h$/', $geometry)) {
291
                // calculate width according to aspect ratio
292
                $destH = (int)$geometry-1;
293
                $resizeMode = false;
294
            } elseif (preg_match('/^[\\d]+l$/', $geometry)) {
295
                // calculate shortest side according to aspect ratio
296
                if ($srcW > $srcH) $destW = (int)$geometry-1;
297
                else $destH = (int)$geometry-1;
298
                $resizeMode = false;
299
            }
300
            if (!isset($destW)) $destW = ($destH/$srcH) * $srcW;
301
            if (!isset($destH)) $destH = ($destW/$srcW) * $srcH;
302

  
303
            // determine resize dimensions from appropriate resize mode and ratio
304
            if ($resizeMode == 'best') {
305
                // "best fit" mode
306
                if ($srcW > $srcH) {
307
                    if ($srcH/$destH > $srcW/$destW) $ratio = $destW/$srcW;
308
                    else $ratio = $destH/$srcH;
309
                } else {
310
                    if ($srcH/$destH < $srcW/$destW) $ratio = $destH/$srcH;
311
                    else $ratio = $destW/$srcW;
312
                }
313
                $resizeW = $srcW*$ratio;
314
                $resizeH = $srcH*$ratio;
315
            }
316
            elseif ($resizeMode == 'band') {
317
                // "banding" mode
318
                if ($srcW > $srcH) $ratio = $destW/$srcW;
319
                else $ratio = $destH/$srcH;
320
                $resizeW = $srcW*$ratio;
321
                $resizeH = $srcH*$ratio;
322
            }
323
            else {
324
                // no resize ratio
325
                $resizeW = $destW;
326
                $resizeH = $destH;
327
            }
328

  
329
            $img = imagecreatetruecolor($destW, $destH);
330
            if ($alpha === true) {
331
                switch (strtolower($pathinfo['extension'])) {
332
                case 'gif':
333
                    $alphaColor = imagecolortransparent($src);
334
                    imagefill($img, 0, 0, $alphaColor);
335
                    imagecolortransparent($img, $alphaColor);
336
                    break;
337
                case 'png':
338
                    imagealphablending($img, false);
339
                    imagesavealpha($img, true);
340
                    break;
341
                default:
342
                    imagefill($img, 0, 0, imagecolorallocate($img, 255, 255, 255));
343
                    break;
344
                }
345
            } else {
346
                imagefill($img, 0, 0, imagecolorallocate($img, 255, 255, 255));
347
            }
348
            imagecopyresampled($img, $src, ($destW-$resizeW)/2, ($destH-$resizeH)/2, 0, 0, $resizeW, $resizeH, $srcW, $srcH);
349
            $outputHandler($img, $destFile, $quality);
350
            return true;
351
        }
352
        return false;
353
    }
354

  
355
    public function attachmentMinSize(Model $model, $value, $min, $options = array()) {
356
        $value = array_shift($value);
357
        if (!empty($value['tmp_name'])) {
358
            return (int)$min <= (int)$value['size'];
359
        } else if (isset($options['allowEmpty']) && $options['allowEmpty'] === false) {
360
          return false;
361
        }
362
        return true;
363
    }
364

  
365
    public function attachmentMaxSize(Model $model, $value, $max, $options = array()) {
366
        $value = array_shift($value);
367
        if (!empty($value['tmp_name'])) {
368
            return (int)$value['size'] <= (int)$max;
369
        } else if (isset($options['allowEmpty']) && $options['allowEmpty'] === false) {
370
          return false;
371
        }
372
        return true;
373
    }
374

  
375
    public function attachmentContentType(Model $model, $value, $contentTypes, $options = array()) {
376
        $value = array_shift($value);
377
        if (!is_array($contentTypes)) {
378
            $contentTypes = array($contentTypes);
379
        }
380
        if (!empty($value['tmp_name'])) {
381
            foreach ($contentTypes as $contentType) {
382
                if (substr($contentType, 0, 1) == '/') {
383
                    if (preg_match($contentType, $value['type'])) {
384
                        return true;
385
                    }
386
                } elseif ($contentType == $value['type']) {
387
                    return true;
388
                }
389
            }
390
            return false;
391
        } else if (isset($options['allowEmpty']) && $options['allowEmpty'] === false) {
392
          return false;
393
        }
394
        return true;
395
    }
396

  
397
    public function attachmentPresence(Model $model, $value, $options = array()) {
398
        $keys = array_keys($value);
399
        $field = $keys[0];
400
        $value = array_shift($value);
401

  
402
        if (!empty($value['tmp_name'])) {
403
            return true;
404
        }
405

  
406
        if (!empty($model->id)) {
407
            if (!empty($model->data[$model->alias][$field.'_file_name'])) {
408
                return true;
409
            } elseif (!isset($model->data[$model->alias][$field.'_file_name'])) {
410
                $existingFile = $model->field($field.'_file_name', array($model->primaryKey => $model->id));
411
                if (!empty($existingFile)) {
412
                    return true;
413
                }
414
            }
415
        }
416
        return false;
417
    }
418

  
419
    public function minWidth(Model $model, $value, $minWidth, $options = array()) {
420
        return $this->_validateDimension($value, 'min', 'x', $minWidth, $options);
421
    }
422

  
423
    public function minHeight(Model $model, $value, $minHeight, $options = array()) {
424
        return $this->_validateDimension($value, 'min', 'y', $minHeight, $options);
425
    }
426

  
427
    public function maxWidth(Model $model, $value, $maxWidth, $options = array()) {
428
        $keys = array_keys($value);
429
        $field = $keys[0];
430
        $settings = self::$__settings[$model->name][$field];
431
        if($settings['resizeToMaxWidth'] && !$this->_validateDimension($value, 'max', 'x', $maxWidth, $options)) {
432
            $this->maxWidthSize = $maxWidth;
433
            return true;
434
        } else {
435
            return $this->_validateDimension($value, 'max', 'x', $maxWidth, $options);
436
        }
437
    }
438

  
439
    public function maxHeight(Model $model, $value, $maxHeight, $options = array()) {
440
        return $this->_validateDimension($value, 'max', 'y', $maxHeight, $options);
441
    }
442

  
443
    private function _validateDimension($upload, $mode, $axis, $value, $options) {
444
        $upload = array_shift($upload);
445
        $func = 'images'.$axis;
446
        if(!empty($upload['tmp_name'])) {
447
            $createHandler = null;
448
            if($upload['type'] == 'image/jpeg') {
449
                $createHandler = 'imagecreatefromjpeg';
450
            } else if($upload['type'] == 'image/gif') {
451
                $createHandler = 'imagecreatefromgif';
452
            } else if($upload['type'] == 'image/png') {
453
                $createHandler = 'imagecreatefrompng';
454
            } else {
455
                return false;
456
            }
457

  
458
            if($img = $createHandler($upload['tmp_name'])) {
459
                switch ($mode) {
460
                case 'min':
461
                    return $func($img) >= $value;
462
                    break;
463
                case 'max':
464
                    return $func($img) <= $value;
465
                    break;
466
                }
467
            }
468
        } else if (isset($options['allowEmpty']) && $options['allowEmpty'] === true) {
469
            return true;
470
        }
471
        return false;
472
    }
473

  
474
    public function phpUploadError(Model $model, $value, $uploadErrors = array('UPLOAD_ERR_INI_SIZE', 'UPLOAD_ERR_FORM_SIZE', 'UPLOAD_ERR_PARTIAL', 'UPLOAD_ERR_NO_FILE', 'UPLOAD_ERR_NO_TMP_DIR', 'UPLOAD_ERR_CANT_WRITE', 'UPLOAD_ERR_EXTENSION'), $options = array()) {
475
        $value = array_shift($value);
476
        if (!is_array($uploadErrors)) {
477
            $uploadErrors = array($uploadErrors);
478
        }
479
        if (!empty($value['error'])) {
480
            return !in_array($value['error'], $uploadErrors);
481
        } else if (isset($options['allowEmpty']) && $options['allowEmpty'] === false) {
482
            return false;
483
        }
484
        return true;
485
    }
486
}
app/Plugin/UploadPack/README.textile
1
h1. UploadPack
2

  
3
"!https://pledgie.com/campaigns/7880.png?skin_name=chrome!(Using it in production? Share some love...)":https://www.pledgie.com/campaigns/7880
4

  
5
UploadPack is a plugin that makes file uploads in CakePHP as easy as possible. It works with almost no configuration, but if you need more flexibility you can easily override default settings.
6

  
7
What's included:
8

  
9
h4. UploadBehavior
10

  
11
Attach it to your model, it will detect any uploaded file and save it to disk. It can even automatically generate thumbnails of uploaded images.
12

  
13
h4. UploadHelper
14

  
15
Use it in your views to display uploaded images or links to files in general.
16

  
17
h2. Installation
18

  
19
# Download this: _http://github.com/szajbus/uploadpack/zipball/master_
20
# Unzip that download.
21
# Copy the resulting folder to _app/plugins_
22
# Rename the folder you just copied to _upload_pack_
23

  
24
h2. Usage
25

  
26
Look at an example.
27

  
28
Scenario: Let users upload their avatars and then display them in two styles - original size and thumbnail.
29

  
30
Solution:
31

  
32
We'll need @User@ model with @avatar_file_name@ field.
33

  
34
<pre><code>CREATE table users (
35
	id int(10) unsigned NOT NULL auto_increment,
36
	login varchar(20) NOT NULL,
37
	avatar_file_name varchar(255)
38
);
39
</code></pre>
40

  
41
Attach @UploadBehavior@ to @User@ model and set it up to handle avatars.
42

  
43
<pre><code><?php
44
	class User extends AppModel {
45
		var $name = 'User';
46
		var $actsAs = array(
47
			'UploadPack.Upload' => array(
48
				'avatar' => array(
49
					'styles' => array(
50
						'thumb' => '80x80'
51
					)
52
				)
53
			)
54
		);
55
	}
56
?>
57
</code></pre>
58

  
59
That's all we need to do with our model. We defined one thumbnail style named 'thumb' which means that uploaded image's thumnbnail of 80x80 pixels size will be generated and saved to disk together with original image.
60

  
61
We didn't touch any other configuration settings so files will be saved as @webroot/upload/:model/:id/:basename_:style.:extension@ (with :keys appropriately substituted at run time). Make sure that @webroot/upload/users@ folder is writeable.
62

  
63
Let's upload a file now. We need to add a file field to a standard "create user" form. Your form must have the right enctype attribute to support file uploads, e.g. @$form->create('Users', array('type' => 'file'));@. Note that we omit the field's @_file_name@ suffix here.
64

  
65
<pre><code><?php echo $form->file('User.avatar') ?></code></pre>
66

  
67
The last thing to do is to handle form-submit in a controller.
68

  
69
<pre><code><?php
70
class UsersController extends AppController {
71
	var $name = 'Users';
72
	var $uses = array('User');
73
	var $helpers = array('Form', 'UploadPack.Upload');
74

  
75
	function create() {
76
		if (!empty($this->data)) {
77
			$this->User->create($this->data);
78
			if ($this->User->save()) {
79
				$this->redirect('/users/show/'.$this->User->id);
80
			}
81
		}
82
	}
83

  
84
	function show($id) {
85
		$this->set('user', $this->User->findById($id));
86
	}
87
}
88
?>
89
</code></pre>
90

  
91
Let's create @users/show.ctp@ view to see the results. Note that we've included UploadHelper in controller's $helpers.
92

  
93
<pre><code>That would be the original file:
94
<?php echo $this->Upload->uploadImage($user, 'User.avatar') ?>
95

  
96
And now it's thumbnail:
97
<?php echo $this->Upload->uploadImage($user, 'User.avatar', array('style' => 'thumb')) ?>
98
</code></pre>
99

  
100
That's how you create new records with uploaded files. Updating existing record would delete a file attached to it from disk.
101

  
102
Could it be any easier? Probably not. Is there more to offer? Yes.
103

  
104
h3. Advanced configuration
105

  
106
h4. You can validate uploaded files
107

  
108
@UploadBehavior@ provides some validation rules for you to use together with standard CakePHP validation mechanism.
109

  
110
Validate attachment's size:
111

  
112
<pre><code>var $validate = array(
113
	'avatar' => array(
114
		'maxSize' => array(
115
			'rule' => array('attachmentMaxSize', 1048576),
116
			'message' => 'Avatar can\'t be larger than 1MB'
117
		),
118
		'minSize' => array(
119
			'rule' => array('attachmentMinSize', 1024),
120
			'message' => 'Avatar can\'t be smaller than 1KB'
121
		)
122
	)
123
);
124
</code></pre>
125

  
126
Validate attachment's content type:
127

  
128
<pre><code>var $validate = array(
129
	'avatar' => array(
130
		'image1 => array(
131
			'rule' => array('attachmentContentType', 'image/jpeg'),
132
			'message' => 'Only jpegs please'
133
		),
134
		'image2' => array(
135
			'rule' => array('attachmentContentType', array('image/jpeg', 'image/gif')),
136
			'message' => 'Only jpegs or gifs please'
137
		),
138
		'image3' => array(
139
			'rule' => array('attachmentContentType', array('document/pdf', '/^image\/.+/')),
140
			'message' => 'Only pdfs or images please'
141
		)
142
	)
143
);
144
</code></pre>
145

  
146
Validate attachment's presence:
147

  
148
<pre><code>var $validate = array(
149
	'avatar' => array(
150
		'rule' => array('attachmentPresence'),
151
		'message' => 'Avatar is required'
152
	)
153
);
154
</code></pre>
155

  
156
Validate image size:
157
<pre><code>var $validate = array(
158
	'avatar' => array(
159
		'minWidth' => array(
160
			'rule' => array('minWidth', '100'),
161
			'message' => 'Photo must be at least 100 pixels wide'
162
		),
163
		'maxWidth' => array(
164
			'rule' => array('maxWidth', '600'),
165
			'message' => 'Photo can\'t be over 600 pixels wide'
166
		),
167
		'minHeight' => array(
168
			'rule' => array('minHeight', '100'),
169
			'message' => 'Photo must be at least 100 pixels wide'
170
		),
171
		'maxHeight' => array(
172
			'rule' => array('maxHeight', '600'),
173
			'message' => 'Photo can\'t be over 600 pixels wide'
174
		)
175
	)
176
);
177
</code></pre>
178

  
179
If you're editing a record that already has avatar attached and you don't supply a new one, record will be valid.
180

  
181
Validate php upload errors:
182
PHP: Error Messages Explained - Manual (http://www.php.net/manual/features.file-upload.errors.php)
183
<pre><code>var $validate = array(
184
	'avatar' => array(
185
		'checkIniSizeError' => array(
186
			'rule' => array('phpUploadError', UPLOAD_ERR_INI_SIZE),
187
			'message' => 'The uploaded file exceeds the upload_max_filesize directive in php.ini'
188
		),
189
		'checkSizesError' => array(
190
			'rule' => array('phpUploadError', array(UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE)),
191
			'message' => 'The uploaded file exceeds the upload_max_filesize directive in php.ini or MAX_FILE_SIZE directive that was specified in the HTML form'
192
		),
193
		'checkAllError' => array(
194
			'rule' => array('phpUploadError'),
195
			'message' => 'Can\'t upload'
196
		)
197
	)
198
);
199
</code></pre>
200

  
201
Validation options:
202

  
203
You can pass additional options to validation rules:
204

  
205
* *allowEmpty* - if set to _false_ will cause validation to fail if the file is not present. This option is not available for _attachmentPresence_ rule.
206

  
207
The example below return true if there is no upload files or jpeg.
208
<pre><code>var $validate = array(
209
	'avatar' => array(
210
		'rule' => array('attachmentContentType', 'image/jpeg'),
211
		'message' => 'Only jpegs please',
212
		'allowEmpty' => true
213
	)
214
);
215
</code></pre>
216

  
217
h4. You can change the path where the files are saved
218

  
219
<pre><code>var $actsAs = array(
220
	'UploadPack.Upload' => array(
221
		'avatar' => array(
222
			'path' => 'your/new/path'
223
		)
224
	)
225
);
226
</code></pre>
227

  
228
The path string can contain special :keys that will be substituted at run time with appropriate values. Here's the list of available @:keys@ with the values they're substituted with.
229

  
230
* *:app* - path to your app dir
231
* *:webroot* - path to your app's webroot dir
232
* *:model* - name of the model tableized with Inflector::tableize (would be 'users' for User model)
233
* *:id* - ID of the record
234
* *:basename* - basename of the uploaded file's original name (would be 'photo' for photo.jpg)
235
* *:extension* - extension of the uploaded file's original name (would be 'jpg' for photo.jpg)
236
* *:style* - name of the thumbnail's style
237
* *:attachment* - pluralized name of the attachment (for example 'avatars')
238
* *:hash* - md5 hash of the original filename + Security.salt
239

  
240
Default value for path is @:webroot/upload/:model/:id/:basename_:style.:extension@.
241

  
242
h4. You can change the url the helper points to when displaying or linking to uploaded files
243

  
244
This setting accepts all the :keys mentioned above and it's default value depends on path setting. For default path it would be defaults @/upload/:model/:id/:basename_:style.:extension@.
245

  
246
You probably won't need to change it too often, though.
247

  
248
h4. You can point to default url if the uploaded file is missing
249

  
250
If uploading an avatar is only a option to your users, you would probably want to have some default image being displayed if no image is uploaded by a user.
251

  
252
<pre><code>var $actsAs = array(
253
	'UploadPack.Upload' => array(
254
		'avatar' => array(
255
			'default_url' => 'path/to/default/image'
256
		)
257
	)
258
);
259
</code></pre>
260

  
261
This setting accepts all the @:keys@ mentioned above, but it's not set by default which results in usual url being returned even it the file does not exist.
262

  
263
h4. You can choose to automatically scale down images that are wider than the <code>maxWidth</code> specified in validation.
264

  
265
<pre><code>var $actsAs = array(
266
	'UploadPack.Upload' => array(
267
		'avatar' => array(
268
			'resizeToMaxWidth' => true
269
		)
270
	)
271
);
272
</code></pre>
273

  
274
h4. JPEG Quality
275

  
276
The jpeg quality can be set with the <code>quality</code> setting.
277

  
278
<pre><code>var $actsAs = array(
279
	'UploadPack.Upload' => array(
280
		'avatar' => array(
281
			'quality' => 95
282
		)
283
	)
284
);
285
</code></pre>
286

  
287
h4. Alpha Channel(PNG, GIF only)
288

  
289
The png and gif can use alpha channel <code>alpha</code> setting.
290

  
291
<pre><code>var $actsAs = array(
292
	'UploadPack.Upload' => array(
293
		'avatar' => array(
294
			'alpha' => true
295
		)
296
	)
297
);
298
</code></pre>
299

  
300
h4. You can choose another field for an external url
301

  
302
<pre><code>var $actsAs = array(
303
	'UploadPack.Upload' => array(
304
		'avatar' => array(
305
			'urlField' => 'gravatar'
306
		)
307
	)
308
);
309
</code></pre>
310

  
311
This way the user can paste an url or choose a file from their filesystem. The url will mimick usual uploading so it will still be validated and resized.
312

  
313
h4. Deleting a file attached to field
314

  
315
<pre><code>$model->data['avatar'] = null;
316
	$model->save();
317
</code></pre>
318

  
319
h3. More on styles
320

  
321
Styles are the definition of thumbnails that will be generated for original image. You can define as many as you want.
322

  
323
<pre><code>var $actsAs = array(
324
	'UploadPack.Upload' => array(
325
		'avatar' => array(
326
			'styles' => array(
327
				'big' => '200x200',
328
				'small' => '120x120',
329
				'thumb' => '80x80'
330
			)
331
		)
332
	)
333
);
334
</code></pre>
335

  
336
Be sure to have @:style@ key included in your path setting to differentiate file names. The original file is also saved and has 'original' as @:style@ value, so you don't need it to define it yourself.
337

  
338
If you want non-image files to be uploaded, define no styles at all. It does not make much sense to generate thumbnails for zip or pdf files.
339

  
340
You can specify any of the following resize modes for your styles:
341

  
342
* *100x80* - resize for best fit into these dimensions, with overlapping edges trimmed if original aspect ratio differs
343
* *[100x80]* - resize to fit these dimensions, with white banding if original aspect ratio differs (Michał's original resize method)
344
* *100w* - maintain original aspect ratio, resize to 100 pixels wide
345
* *80h* - maintain original aspect ratio, resize to 80 pixels high
346
* *80l* - maintain original aspect ratio, resize so that longest side is 80 pixels
347

  
348
h3. More on database table structure
349

  
350
The only database field you need to add to your model's table is @[field]_file_name@. Replace @[field]@ with chosen name (avatar for example).
351

  
352
There are two optional fields you can add: @[field]_content_type@ and @[field]_file_size@. If they're present they will be populated with uploaded file's content type and size in bytes respectively.
353

  
354
Model can have many uploaded files at once. For example define two fields in database table: @avatar_file_name@ and @logo_file_name@. Set up behavior:
355

  
356
<pre><code>var $actsAs = array(
357
	'UploadPack.Upload' => array(
358
		'avatar' => array(),
359
		'logo' => array()
360
		)
361
	)
362
);
363
</code></pre>
364

  
365
h3. Using the helper
366

  
367
There are two methods of UploadHelper you can use:
368

  
369
h4. UploadHelper::uploadUrl($data, $field, $options = array())
370

  
371
Returns url to the uploaded file
372

  
373
* *$data* - record from database (would be @$user@ here)
374
* *$field* - name of a field, like this @Modelname.fieldname@ (would be @User.avatar@ here)
375
* *$options* - url options
376
** *'style'* - name of a thumbnail's style
377
** *'urlize'* - if true returned url is wrapped with @$html->url()@
378

  
379
h4. UploadHelper::uploadImage($data, $field, $options = array(), $htmlOptions = array())
380

  
381
Returns image tag pointing to uploaded file.
382

  
383
* *$data* - record from database (would be @$user@ here)
384
* *$field* - name of a field, like this @Modelname.fieldname@ (would be @User.avatar@ here)
385
* *$options* - url options
386
** *'style'* - name of a thumbnail's style
387
** *'urlize'* - if true returned url is wrapped with @$html->url()@
388
* *$htmlOptions* - array of HTML attributes passed to @$html->image()@
389

  
390
Assuming that you have read a user from database and it's available in @$user@ variable in view.
391

  
392
<pre><code><?php echo $this->Upload->uploadImage($user, 'User.avatar', array('style' => 'thumb')); ?></code></pre>
393

  
394
When you fetch user from database you would usually get:
395

  
396
<pre><code>$user = array(
397
	'User' => array(
398
		'id' => 1,
399
		'avatar_file_name' => 'photo.jpg'
400
	)
401
);
402
</code></pre>
403

  
404
But when the user is fetched as one of many users associated by hasMany association to another model it could be something like:
405

  
406
<pre><code>$data = array(
407
	'User' => array(
408
		0 => array(
409
			'id' => 1,
410
			'avatar_file_name' => 'photo.jpg'
411
		),
412
		1 => array(
413
			'id' => 2,
414
			'avatar_file_name' => 'photo2.jpg'
415
		)
416
	)
417
);
418
</code></pre>
419

  
420
Then you should do:
421

  
422
<pre><code><?php echo $this->Upload->uploadImage($data['User'][0], 'User.avatar', array('style' => 'thumb')) ?></code></pre>
423

  
424
The helper is smart enough to figure out the structure of data you pass to it.
425

  
426
h3. Requirements
427

  
428
UploadPack was developed with CakePHP 1.2 RC3, so it's not guaranteed to work with previous versions. You'll need GD library if you plan to generate image thumbnails. It works OK with 2.0.34 version at least, earlier versions may work too.
429

  
430
h2. Plans for future
431

  
432
There is still something missing in UploadPack, here's what will be added soon:
433

  
434
* file name normalization
435

  
436
The plans for more distant future:
437

  
438
* test coverage
439
* allow uploads to S3
440
* provide a method to regenerate all thumbnails, helpful if you later decide to have different sizes of thumbnails or more styles
441

  
442
I you want to help implementing these features, feel free to submit your patches.
443

  
444
h2. Copyright
445

  
446
Copyright (c) 2008 Michał Szajbe (http://codetunes.com), released under the MIT license.
447

  
448
joe bartlett's (http://jdbartlett.com) tweaks aren't under copyright. Run free, little tweaks!
app/Plugin/UploadPack/VERSION
1
UploadPack 0.7.2
app/Plugin/UploadPack/View/Helper/UploadHelper.php
1
<?php
2
App::uses('UploadBehavior', 'UploadPack.Model/Behavior');
3
/**
4
 * This file is a part of UploadPack - a plugin that makes file uploads in CakePHP as easy as possible.
5
 *
6
 * UploadHelper
7
 *
8
 * UploadHelper provides fine access to files uploaded with UploadBehavior. It generates url for those files and can display image tags of uploaded images. For more info read UploadPack documentation.
9
 *
10
 * @author Michał Szajbe (michal.szajbe@gmail.com)
11
 * @link http://github.com/szajbus/uploadpack
12
 */
13
class UploadHelper extends AppHelper {
14

  
15
    public $helpers = array('Html');
16

  
17
    public function uploadImage($data, $path, $options = array(), $htmlOptions = array())
18
    {
19
        $options += array('urlize' => false);
20
        return $this->output($this->Html->image($this->uploadUrl($data, $path, $options), $htmlOptions));
21
    }
22

  
23
    public function uploadLink($title, $data, $field, $urlOptions = array(), $htmlOptions = array())
24
    {
25
        $urlOptions += array('style' => 'original', 'urlize' => true);
26
        return $this->Html->link($title, $this->uploadUrl($data, $field, $urlOptions), $htmlOptions);
27
    }
28

  
29
    public function uploadUrl($data, $field, $options = array())
30
    {
31
        $options += array('style' => 'original', 'urlize' => true);
32
        list($model, $field) = explode('.', $field);
33
        if(is_array($data))
34
        {
35
            if(isset($data[$model]))
36
            {
37
                if(isset($data[$model]['id']))
38
                {
39
                    $id = $data[$model]['id'];
40
                    $filename = $data[$model][$field.'_file_name'];
41
                }
42
            }
43
            elseif(isset($data['id']))
44
            {
45
                $id = $data['id'];
46
                $filename = $data[$field.'_file_name'];
47
            }
48
        }
49

  
50
        if(isset($id) && !empty($filename))
51
        {
52
            $settings = UploadBehavior::interpolate($model, $id, $field, $filename, $options['style'], array('webroot' => ''));
53
            $url = isset($settings['url']) ? $settings['url'] : $settings['path'];
54
        }
55
        else
56
        {
57
            $settings = UploadBehavior::interpolate($model, null, $field, null, $options['style'], array('webroot' => ''));
58
            $url = isset($settings['default_url']) ? $settings['default_url'] : null;
59
        }
60

  
61
        return $options['urlize'] ? $this->Html->url($url) : $url;
62
    }
63

  
64
    /**
65
     * Returns appropriate extension for given mimetype.
66
     *
67
     * @param string $mime Mimetype
68
     * @return void
69
     * @author Bjorn Post
70
     */
71
    public function extension($mimeType = null)
72
    {
73
        $knownMimeTypes = array(
74
            'ai' => 'application/postscript', 'bcpio' => 'application/x-bcpio', 'bin' => 'application/octet-stream',
75
            'ccad' => 'application/clariscad', 'cdf' => 'application/x-netcdf', 'class' => 'application/octet-stream',
76
            'cpio' => 'application/x-cpio', 'cpt' => 'application/mac-compactpro', 'csh' => 'application/x-csh',
77
            'csv' => 'application/csv', 'dcr' => 'application/x-director', 'dir' => 'application/x-director',
78
            'dms' => 'application/octet-stream', 'doc' => 'application/msword', 'drw' => 'application/drafting',
79
            'dvi' => 'application/x-dvi', 'dwg' => 'application/acad', 'dxf' => 'application/dxf', 'dxr' => 'application/x-director',
80
            'eps' => 'application/postscript', 'exe' => 'application/octet-stream', 'ez' => 'application/andrew-inset',
81
            'flv' => 'video/x-flv', 'gtar' => 'application/x-gtar', 'gz' => 'application/x-gzip',
82
            'bz2' => 'application/x-bzip', '7z' => 'application/x-7z-compressed', 'hdf' => 'application/x-hdf',
83
            'hqx' => 'application/mac-binhex40', 'ips' => 'application/x-ipscript', 'ipx' => 'application/x-ipix',
84
            'js' => 'application/x-javascript', 'latex' => 'application/x-latex', 'lha' => 'application/octet-stream',
85
            'lsp' => 'application/x-lisp', 'lzh' => 'application/octet-stream', 'man' => 'application/x-troff-man',
86
            'me' => 'application/x-troff-me', 'mif' => 'application/vnd.mif', 'ms' => 'application/x-troff-ms',
87
            'nc' => 'application/x-netcdf', 'oda' => 'application/oda', 'pdf' => 'application/pdf',
88
            'pgn' => 'application/x-chess-pgn', 'pot' => 'application/mspowerpoint', 'pps' => 'application/mspowerpoint',
89
            'ppt' => 'application/mspowerpoint', 'ppz' => 'application/mspowerpoint', 'pre' => 'application/x-freelance',
90
            'prt' => 'application/pro_eng', 'ps' => 'application/postscript', 'roff' => 'application/x-troff',
91
            'scm' => 'application/x-lotusscreencam', 'set' => 'application/set', 'sh' => 'application/x-sh',
92
            'shar' => 'application/x-shar', 'sit' => 'application/x-stuffit', 'skd' => 'application/x-koan',
93
            'skm' => 'application/x-koan', 'skp' => 'application/x-koan', 'skt' => 'application/x-koan',
94
            'smi' => 'application/smil', 'smil' => 'application/smil', 'sol' => 'application/solids',
95
            'spl' => 'application/x-futuresplash', 'src' => 'application/x-wais-source', 'step' => 'application/STEP',
96
            'stl' => 'application/SLA', 'stp' => 'application/STEP', 'sv4cpio' => 'application/x-sv4cpio',
97
            'sv4crc' => 'application/x-sv4crc', 'svg' => 'image/svg+xml', 'svgz' => 'image/svg+xml',
98
            'swf' => 'application/x-shockwave-flash', 't' => 'application/x-troff',
99
            'tar' => 'application/x-tar', 'tcl' => 'application/x-tcl', 'tex' => 'application/x-tex',
100
            'texi' => 'application/x-texinfo', 'texinfo' => 'application/x-texinfo', 'tr' => 'application/x-troff',
101
            'tsp' => 'application/dsptype', 'unv' => 'application/i-deas', 'ustar' => 'application/x-ustar',
102
            'vcd' => 'application/x-cdlink', 'vda' => 'application/vda', 'xlc' => 'application/vnd.ms-excel',
103
            'xll' => 'application/vnd.ms-excel', 'xlm' => 'application/vnd.ms-excel', 'xls' => 'application/vnd.ms-excel',
104
            'xlw' => 'application/vnd.ms-excel', 'zip' => 'application/zip', 'aif' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff',
105
            'aiff' => 'audio/x-aiff', 'au' => 'audio/basic', 'kar' => 'audio/midi', 'mid' => 'audio/midi',
106
            'midi' => 'audio/midi', 'mp2' => 'audio/mpeg', 'mp3' => 'audio/mpeg', 'mpga' => 'audio/mpeg',
107
            'ra' => 'audio/x-realaudio', 'ram' => 'audio/x-pn-realaudio', 'rm' => 'audio/x-pn-realaudio',
108
            'rpm' => 'audio/x-pn-realaudio-plugin', 'snd' => 'audio/basic', 'tsi' => 'audio/TSP-audio', 'wav' => 'audio/x-wav',
109
            'asc' => 'text/plain', 'c' => 'text/plain', 'cc' => 'text/plain', 'css' => 'text/css', 'etx' => 'text/x-setext',
110
            'f' => 'text/plain', 'f90' => 'text/plain', 'h' => 'text/plain', 'hh' => 'text/plain', 'htm' => 'text/html',
111
            'html' => 'text/html', 'm' => 'text/plain', 'rtf' => 'text/rtf', 'rtx' => 'text/richtext', 'sgm' => 'text/sgml',
112
            'sgml' => 'text/sgml', 'tsv' => 'text/tab-separated-values', 'tpl' => 'text/template', 'txt' => 'text/plain',
113
            'xml' => 'text/xml', 'avi' => 'video/x-msvideo', 'fli' => 'video/x-fli', 'mov' => 'video/quicktime',
114
            'movie' => 'video/x-sgi-movie', 'mpe' => 'video/mpeg', 'mpeg' => 'video/mpeg', 'mpg' => 'video/mpeg',
115
            'qt' => 'video/quicktime', 'viv' => 'video/vnd.vivo', 'vivo' => 'video/vnd.vivo', 'gif' => 'image/gif',
116
            'ief' => 'image/ief', 'jpe' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg',
117
            'pbm' => 'image/x-portable-bitmap', 'pgm' => 'image/x-portable-graymap', 'png' => 'image/png',
118
            'pnm' => 'image/x-portable-anymap', 'ppm' => 'image/x-portable-pixmap', 'ras' => 'image/cmu-raster',
119
            'rgb' => 'image/x-rgb', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'xbm' => 'image/x-xbitmap',
120
            'xpm' => 'image/x-xpixmap', 'xwd' => 'image/x-xwindowdump', 'ice' => 'x-conference/x-cooltalk',
121
            'iges' => 'model/iges', 'igs' => 'model/iges', 'mesh' => 'model/mesh', 'msh' => 'model/mesh',
122
            'silo' => 'model/mesh', 'vrml' => 'model/vrml', 'wrl' => 'model/vrml',
123
            'mime' => 'www/mime', 'pdb' => 'chemical/x-pdb', 'xyz' => 'chemical/x-pdb'
124
        );
125

  
126
        return array_search($mimeType, $knownMimeTypes);
127
    }
128
}
app/Plugin/UploadPack/composer.json
1
{
2
    "name": "szajbus/uploadpack",
3
    "description": "Easy way to handle file uploads in CakePHP",
4
    "version": "0.7.2",
5
    "type": "library",
6
    "keywords": ["upload", "cakephp", "image processing"],
7
    "homepage": "https://github.com/szajbus/uploadpack",
8
    "license": "MIT",
9
    "authors": [
10
        {
11
            "name": "Michał Szajbe",
12
            "email": "michal.szajbe@gmail.com"
13
        }
14
    ],
15
    "support": {
16
        "issues": "https://github.com/szajbus/uploadpack/issues",
17
        "source": "https://github.com/szajbus/uploadpack"
18
    },
19
    "require": {
20
        "cakephp/cakephp": ">=1.2"
21
    },
22
    "require-dev": {
23
        "phpunit/phpunit": "*",
24
        "cakephp/cakephp-codesniffer": "dev-master"
25
    }
26
}
app/Plugin/UploadPack/license
1
Copyright (c) 2010 Michał Szajbe
2

  
3
Permission is hereby granted, free of charge, to any person
4
obtaining a copy of this software and associated documentation
5
files (the "Software"), to deal in the Software without
6
restriction, including without limitation the rights to use,
7
copy, modify, merge, publish, distribute, sublicense, and/or sell
8
copies of the Software, and to permit persons to whom the
9
Software is furnished to do so, subject to the following
10
conditions:
11

  
12
The above copyright notice and this permission notice shall be
13
included in all copies or substantial portions of the Software.
14

  
15
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
OTHER DEALINGS IN THE SOFTWARE.

他の形式にエクスポート: Unified diff