Completed
Branch prettify (8163f7)
by samayo
01:59
created

Image::upload()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 4
nc 5
nop 0
1
<?php 
2
/**
3
 * BULLETPROOF.
4
 * 
5
 * A single-class PHP library to upload images securely.
6
 * 
7
 * PHP support 5.3+
8
 * 
9
 * @version     4.0.0
10
 * @author      https://twitter.com/_samayo
11
 * @link        https://github.com/samayo/bulletproof
12
 * @license     MIT
13
 */
14
namespace Bulletproof;
15
16
class Image implements \ArrayAccess
17
{
18
    /**
19
     * @var string The new image name, to be provided or will be generated
20
     */
21
    protected $name;
22
23
    /**
24
     * @var int The image width in pixels
25
     */
26
    protected $width;
27
28
    /**
29
     * @var int The image height in pixels
30
     */
31
    protected $height;
32
33
    /**
34
     * @var string The image mime type (extension)
35
     */
36
    protected $mime;
37
38
    /**
39
     * @var string The full image path (dir + image + mime)
40
     */
41
    protected $fullPath;
42
43
    /**
44
     * @var string The folder or image storage location
45
     */
46
    protected $location;
47
48
    /**
49
     * @var array The min and max image size allowed for upload (in bytes)
50
     */
51
    protected $size = array(100, 500000);
52
53
    /**
54
     * @var array The max height and width image allowed
55
     */
56
    protected $dimensions = array(5000, 5000);
57
58
    /**
59
     * @var array The mime types allowed for upload
60
     */
61
    protected $mimeTypes = array('jpeg', 'png', 'gif', 'jpg');
62
63
    /**
64
     * @var array list of known image types
65
     */
66
    protected $acceptedMimes = array(
67
      1 => 'gif', 'jpeg', 'png', 'swf', 'psd',
68
      'bmp', 'tiff', 'tiff', 'jpc', 'jp2', 'jpx',
69
      'jb2', 'swc', 'iff', 'wbmp', 'xbm', 'ico',
70
    );
71
72
    /**
73
     * @var array error messages strings
74
     */
75
    protected $commonUploadErrors = array(
76
      UPLOAD_ERR_OK => '',
77
      UPLOAD_ERR_INI_SIZE => 'Image is larger than the specified amount set by the server',
78
      UPLOAD_ERR_FORM_SIZE => 'Image is larger than the specified amount specified by browser',
79
      UPLOAD_ERR_PARTIAL => 'Image could not be fully uploaded. Please try again later',
80
      UPLOAD_ERR_NO_FILE => 'Image is not found',
81
      UPLOAD_ERR_NO_TMP_DIR => 'Can\'t write to disk, due to server configuration ( No tmp dir found )',
82
      UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk. Please check you file permissions',
83
      UPLOAD_ERR_EXTENSION => 'A PHP extension has halted this file upload process',
84
    );
85
86
    /**
87
     * @var array storage for the global array
88
     */
89
    private $_files = array();
90
91
    /**
92
     * @var string storage for any errors
93
     */
94
    private $error = '';
95
96
    /**
97
     * @param array $_files represents the $_FILES array passed as dependency
98
     */
99
    public function __construct(array $_files = array())
100
    {
101
      if (!function_exists('exif_imagetype')) {
102
        $this->error = 'Function \'exif_imagetype\' Not found. Please enable \'php_exif\' in your php.ini';
103
      }
104
105
      $this->_files = $_files;
106
    }
107
108
    /**
109
     * \ArrayAccess unused method
110
     * 
111
     * @param mixed $offset
112
     * @param mixed $value
113
     */
114
    public function offsetSet($offset, $value) {}
115
116
    /**
117
     * \ArrayAccess unused method
118
     * 
119
     * @param mixed $offset
120
     */
121
    public function offsetExists($offset){}
122
123
    /**
124
     * \ArrayAccess unused method
125
     * 
126
     * @param mixed $offset
127
     */
128
    public function offsetUnset($offset){}
129
130
    /**
131
     * \ArrayAccess - get array value from object
132
     *
133
     * @param mixed $offset
134
     *
135
     * @return string|bool
136
     */
137
    public function offsetGet($offset)
138
    {
139
      // return false if $_FILES['key'] isn't found
140
      if (!isset($this->_files[$offset])) {
141
        $this->error = sprintf('No file input found with name: (%s)', $offset);
142
        return false;
143
      }
144
145
      $this->_files = $this->_files[$offset];
146
147
      // check for common upload errors
148
      if (isset($this->_files['error'])) {
149
        $this->error = $this->commonUploadErrors[$this->_files['error']];
150
      }
151
152
      return true;
153
    }
154
155
    /**
156
     * Sets max image height and width limit.
157
     *
158
     * @param $maxWidth int max width value
159
     * @param $maxHeight int max height value
160
     *
161
     * @return $this
162
     */
163
    public function setDimension($maxWidth, $maxHeight)
164
    {
165
      if ((int) $maxWidth && (int) $maxHeight) {
166
        $this->dimensions = array($maxWidth, $maxHeight);
167
      } else {
168
        $this->error = 'Invalid dimension! Values must be integers';
169
      }
170
171
      return $this;
172
    }
173
174
    /**
175
     * Returns the full path of the image ex 'location/image.mime'.
176
     *
177
     * @return string
178
     */
179
    public function getFullPath()
180
    {
181
      return $this->fullPath = $this->getLocation().'/'.$this->getName().'.'.$this->getMime();
182
    }
183
184
    /**
185
     * Returns the image size in bytes.
186
     *
187
     * @return int
188
     */
189
    public function getSize()
190
    {
191
      return (int) $this->_files['size'];
192
    }
193
194
    /**
195
     * Define a min and max image size for uploading.
196
     *
197
     * @param $min int minimum value in bytes
198
     * @param $max int maximum value in bytes
199
     *
200
     * @return $this
201
     */
202
    public function setSize($min, $max)
203
    {
204
      $this->size = array($min, $max);
205
      return $this;
206
    }
207
208
    /**
209
     * Returns a JSON format of the image width, height, name, mime ...
210
     *
211
     * @return string
212
     */
213
    public function getJson()
214
    {
215
      return json_encode(
216
        array(
217
          'name' => $this->name,
218
          'mime' => $this->mime,
219
          'height' => $this->height,
220
          'width' => $this->width,
221
          'size' => $this->_files['size'],
222
          'location' => $this->location,
223
          'fullpath' => $this->fullPath,
224
        )
225
      );
226
    }
227
228
    /**
229
     * Returns the image mime type.
230
     *
231
     * @return null|string
232
     */
233
    public function getMime()
234
    {
235
      if (!$this->mime) {
236
        $this->mime = $this->getImageMime($this->_files['tmp_name']);
237
      }
238
239
      return $this->mime;
240
    }
241
242
    /**
243
     * Define a mime type for uploading.
244
     *
245
     * @param array $fileTypes
246
     *
247
     * @return $this
248
     */
249
    public function setMime(array $fileTypes)
250
    {
251
      $this->mimeTypes = $fileTypes;
252
      return $this;
253
    }
254
255
    /**
256
     * Gets the real image mime type.
257
     *
258
     * @param $tmp_name string The upload tmp directory
259
     *
260
     * @return null|string
261
     */
262
    protected function getImageMime($tmp_name)
263
    {
264
      $this->mime = @$this->acceptedMimes[exif_imagetype($tmp_name)];
265
      if (!$this->mime) {
266
        return null;
267
      }
268
269
      return $this->mime;
270
    }
271
272
    /**
273
     * Returns error string or false if no errors occurred.
274
     *
275
     * @return string|false
276
     */
277
    public function getError()
278
    {
279
      return $this->error;
280
    }
281
282
    /**
283
     * Returns the image name.
284
     *
285
     * @return string
286
     */
287
    public function getName()
288
    {
289
      if (!$this->name) {
290
        $this->name = uniqid('', true).'_'.str_shuffle(implode(range('e', 'q')));
291
      }
292
293
      return $this->name;
294
    }
295
296
    /**
297
     * Provide image name if not provided.
298
     *
299
     * @param null $isNameProvided
300
     *
301
     * @return $this
302
     */
303
    public function setName($isNameProvided = null)
304
    {
305
      if ($isNameProvided) {
306
        $this->name = filter_var($isNameProvided, FILTER_SANITIZE_STRING);
307
      }
308
309
      return $this;
310
    }
311
312
    /**
313
     * Returns the image width.
314
     *
315
     * @return int
316
     */
317
    public function getWidth()
318
    {
319
      if ($this->width != null) {
320
        return $this->width;
321
      }
322
323
      list($width) = getimagesize($this->_files['tmp_name']);
324
325
      return $width;
326
    }
327
328
    /**
329
     * Returns the image height in pixels.
330
     *
331
     * @return int
332
     */
333
    public function getHeight()
334
    {
335
      if ($this->height != null) {
336
        return $this->height;
337
      }
338
339
      list(, $height) = getimagesize($this->_files['tmp_name']);
340
341
      return $height;
342
    }
343
344
    /**
345
     * Returns the storage / folder name.
346
     *
347
     * @return string
348
     */
349
    public function getLocation()
350
    {
351
      if (!$this->location) {
352
        $this->setLocation();
353
      }
354
355
      return $this->location;
356
    }
357
358
    /**
359
     * Validate directory/permission before creating a folder.
360
     *
361
     * @param $dir string the folder name to check
362
     *
363
     * @return bool
364
     */
365
    private function isDirectoryValid($dir)
366
    {
367
      return !file_exists($dir) && !is_dir($dir) || is_writable($dir);
368
    }
369
370
    /**
371
     * Creates a location for upload storage.
372
     *
373
     * @param $dir string the folder name to create
374
     * @param int $permission chmod permission
375
     *
376
     * @return $this
377
     */
378
    public function setLocation($dir = 'bulletproof', $permission = 0666)
379
    {
380
      $isDirectoryValid = $this->isDirectoryValid($dir);
381
382
      if (!$isDirectoryValid) {
383
        $this->error = 'Can not create a directory  \''.$dir.'\', please check write permission';
384
        return false;
385
      }
386
387
      $create = !is_dir($dir) ? @mkdir(''.$dir, (int) $permission, true) : true;
388
389
      if (!$create) {
390
        $this->error = 'Error! directory \''.$dir.'\' could not be created';
391
        return false;
392
      }
393
394
      $this->location = $dir;
395
396
      return $this;
397
    }
398
399
    /**
400
     * Validate image size, dimension or mimetypes
401
     *
402
     * @return boolean
403
     */
404
    protected function contraintsValidator()
405
    {
406
      /* check image for valid mime types and return mime */
407
      $this->getImageMime($this->_files['tmp_name']);
408
      /* validate image mime type */
409
      if (!in_array($this->mime, $this->mimeTypes)) {
410
        $this->error = sprintf('Invalid File! Only (%s) image types are allowed', implode(', ', $this->mimeTypes));
411
        return false;
412
      }
413
414
      /* get image sizes */
415
      list($minSize, $maxSize) = $this->size;
416
417
      /* check image size based on the settings */
418
      if ($this->_files['size'] < $minSize || $this->_files['size'] > $maxSize) {
419
        $min = $minSize.' bytes ('.intval($minSize / 1000).' kb)';
420
        $max = $maxSize.' bytes ('.intval($maxSize / 1000).' kb)';
421
        $this->error = 'Image size should be minumum '.$min.', upto maximum '.$max;
422
        return false;
423
      }
424
425
      /* check image dimension */
426
      list($maxWidth, $maxHeight) = $this->dimensions;
427
      $this->width = $this->getWidth();
428
      $this->height = $this->getHeight();
429
430
      if ($this->height > $maxHeight || $this->width > $maxWidth) {
431
        $this->error = 'Image height/width should be less than '.$maxHeight.'/'.$maxWidth.' pixels';
432
        return false;
433
      }
434
435
      return true;
436
    }
437
438
    /**
439
     * Validate and save (upload) file
440
     *
441
     * @return false|Image
442
     */
443
    public function upload()
444
    {
445
      if ($this->error) {
446
        return false;
447
      }
448
449
      $isValid = $this->contraintsValidator();
450
451
      $isSuccess = $isValid && $this->isSaved($this->_files['tmp_name'], $this->getFullPath());
452
453
      return $isSuccess ? $this : false;
454
    }
455
456
    /**
457
     * Final upload method to be called, isolated for testing purposes.
458
     *
459
     * @param $tmp_name int the temporary location of the image file
460
     * @param $destination int upload destination
461
     *
462
     * @return bool
463
     */
464
    protected function isSaved($tmp_name, $destination)
465
    {
466
      return move_uploaded_file($tmp_name, $destination);
467
    }
468
}