hjg
2024-10-30 8cf23534166c07e711aac2a25911ada317ba01f0
提交 | 用户 | 时间
58d006 1 <?php
A 2 /*
3  * jQuery File Upload Plugin PHP Class 6.9.0
4  * https://github.com/blueimp/jQuery-File-Upload
5  *
6  * Copyright 2010, Sebastian Tschan
7  * https://blueimp.net
8  *
9  * Licensed under the MIT license:
10  * http://www.opensource.org/licenses/MIT
11  */
12
13 class UploadHandler
14 {
15     protected $options;
16     // PHP File Upload error message codes:
17     // http://php.net/manual/en/features.file-upload.errors.php
18     protected $error_messages = array(
19         1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
20         2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
21         3 => 'The uploaded file was only partially uploaded',
22         4 => 'No file was uploaded',
23         6 => 'Missing a temporary folder',
24         7 => 'Failed to write file to disk',
25         8 => 'A PHP extension stopped the file upload',
26         'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini',
27         'max_file_size' => 'File is too big',
28         'min_file_size' => 'File is too small',
29         'accept_file_types' => 'Filetype not allowed',
30         'max_number_of_files' => 'Maximum number of files exceeded',
31         'max_width' => 'Image exceeds maximum width',
32         'min_width' => 'Image requires a minimum width',
33         'max_height' => 'Image exceeds maximum height',
34         'min_height' => 'Image requires a minimum height'
35     );
36
37     function __construct($options = null, $initialize = true, $error_messages = null) {
38         $this->options = array(
39             'script_url' => $this->get_full_url().'/',
40             'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/files/',
41             'upload_url' => $this->get_full_url().'/files/',
42             'user_dirs' => false,
43             'mkdir_mode' => 0755,
44             'param_name' => 'files',
45             // Set the following option to 'POST', if your server does not support
46             // DELETE requests. This is a parameter sent to the client:
47             'delete_type' => 'DELETE',
48             'access_control_allow_origin' => '*',
49             'access_control_allow_credentials' => false,
50             'access_control_allow_methods' => array(
51                 'OPTIONS',
52                 'HEAD',
53                 'GET',
54                 'POST',
55                 'PUT',
56                 'PATCH',
57                 'DELETE'
58             ),
59             'access_control_allow_headers' => array(
60                 'Content-Type',
61                 'Content-Range',
62                 'Content-Disposition'
63             ),
64             // Enable to provide file downloads via GET requests to the PHP script:
65             //     1. Set to 1 to download files via readfile method through PHP
66             //     2. Set to 2 to send a X-Sendfile header for lighttpd/Apache
67             //     3. Set to 3 to send a X-Accel-Redirect header for nginx
68             // If set to 2 or 3, adjust the upload_url option to the base path of
69             // the redirect parameter, e.g. '/files/'.
70             'download_via_php' => false,
71             // Read files in chunks to avoid memory limits when download_via_php
72             // is enabled, set to 0 to disable chunked reading of files:
73             'readfile_chunk_size' => 10 * 1024 * 1024, // 10 MiB
74             // Defines which files can be displayed inline when downloaded:
75             'inline_file_types' => '/\.(gif|jpe?g|png)$/i',
76             // Defines which files (based on their names) are accepted for upload:
77             'accept_file_types' => '/.+$/i',
78             // The php.ini settings upload_max_filesize and post_max_size
79             // take precedence over the following max_file_size setting:
80             'max_file_size' => null,
81             'min_file_size' => 1,
82             // The maximum number of files for the upload directory:
83             'max_number_of_files' => null,
84             // Image resolution restrictions:
85             'max_width' => null,
86             'max_height' => null,
87             'min_width' => 1,
88             'min_height' => 1,
89             // Set the following option to false to enable resumable uploads:
90             'discard_aborted_uploads' => true,
91             // Set to false to disable rotating images based on EXIF meta data:
92             'orient_image' => true,
93             'image_versions' => array(
94                 // Uncomment the following version to restrict the size of
95                 // uploaded images:
96                 /*
97                 '' => array(
98                     'max_width' => 1920,
99                     'max_height' => 1200,
100                     'jpeg_quality' => 95
101                 ),
102                 */
103                 // Uncomment the following to create medium sized images:
104                 /*
105                 'medium' => array(
106                     'max_width' => 800,
107                     'max_height' => 600,
108                     'jpeg_quality' => 80
109                 ),
110                 */
111                 'thumbnail' => array(
112                     // Uncomment the following to use a defined directory for the thumbnails
113                     // instead of a subdirectory based on the version identifier.
114                     // Make sure that this directory doesn't allow execution of files if you
115                     // don't pose any restrictions on the type of uploaded files, e.g. by
116                     // copying the .htaccess file from the files directory for Apache:
117                     //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/',
118                     //'upload_url' => $this->get_full_url().'/thumb/',
119                     // Uncomment the following to force the max
120                     // dimensions and e.g. create square thumbnails:
121                     //'crop' => true,
122                     'max_width' => 80,
123                     'max_height' => 80
124                 )
125             )
126         );
127         if ($options) {
128             $this->options = array_merge($this->options, $options);
129         }
130         if ($error_messages) {
131             $this->error_messages = array_merge($this->error_messages, $error_messages);
132         }
133         if ($initialize) {
134             $this->initialize();
135         }
136     }
137
138     protected function initialize() {
139         switch ($this->get_server_var('REQUEST_METHOD')) {
140             case 'OPTIONS':
141             case 'HEAD':
142                 $this->head();
143                 break;
144             case 'GET':
145                 $this->get();
146                 break;
147             case 'PATCH':
148             case 'PUT':
149             case 'POST':
150                 $this->post();
151                 break;
152             case 'DELETE':
153                 $this->delete();
154                 break;
155             default:
156                 $this->header('HTTP/1.1 405 Method Not Allowed');
157         }
158     }
159
160     protected function get_full_url() {
161         $https = !empty($_SERVER['HTTPS']) && strcasecmp($_SERVER['HTTPS'], 'on') === 0;
162         return
163             ($https ? 'https://' : 'http://').
164             (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
165             (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
166             ($https && $_SERVER['SERVER_PORT'] === 443 ||
167             $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
168             substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
169     }
170
171     protected function get_user_id() {
172         @session_start();
173         return session_id();
174     }
175
176     protected function get_user_path() {
177         if ($this->options['user_dirs']) {
178             return $this->get_user_id().'/';
179         }
180         return '';
181     }
182
183     protected function get_upload_path($file_name = null, $version = null) {
184         $file_name = $file_name ? $file_name : '';
185         if (empty($version)) {
186             $version_path = '';
187         } else {
188             $version_dir = @$this->options['image_versions'][$version]['upload_dir'];
189             if ($version_dir) {
190                 return $version_dir.$this->get_user_path().$file_name;
191             }
192             $version_path = $version.'/';
193         }
194         return $this->options['upload_dir'].$this->get_user_path()
195             .$version_path.$file_name;
196     }
197
198     protected function get_query_separator($url) {
199         return strpos($url, '?') === false ? '?' : '&';
200     }
201
202     protected function get_download_url($file_name, $version = null, $direct = false) {
203         if (!$direct && $this->options['download_via_php']) {
204             $url = $this->options['script_url']
205                 .$this->get_query_separator($this->options['script_url'])
206                 .'file='.rawurlencode($file_name);
207             if ($version) {
208                 $url .= '&version='.rawurlencode($version);
209             }
210             return $url.'&download=1';
211         }
212         if (empty($version)) {
213             $version_path = '';
214         } else {
215             $version_url = @$this->options['image_versions'][$version]['upload_url'];
216             if ($version_url) {
217                 return $version_url.$this->get_user_path().rawurlencode($file_name);
218             }
219             $version_path = rawurlencode($version).'/';
220         }
221         return $this->options['upload_url'].$this->get_user_path()
222             .$version_path.rawurlencode($file_name);
223     }
224
225     protected function set_additional_file_properties($file) {
226         $file->deleteUrl = $this->options['script_url']
227             .$this->get_query_separator($this->options['script_url'])
228             .$this->get_singular_param_name()
229             .'='.rawurlencode($file->name);
230         $file->deleteType = $this->options['delete_type'];
231         if ($file->deleteType !== 'DELETE') {
232             $file->deleteUrl .= '&_method=DELETE';
233         }
234         if ($this->options['access_control_allow_credentials']) {
235             $file->deleteWithCredentials = true;
236         }
237     }
238
239     // Fix for overflowing signed 32 bit integers,
240     // works for sizes up to 2^32-1 bytes (4 GiB - 1):
241     protected function fix_integer_overflow($size) {
242         if ($size < 0) {
243             $size += 2.0 * (PHP_INT_MAX + 1);
244         }
245         return $size;
246     }
247
248     protected function get_file_size($file_path, $clear_stat_cache = false) {
249         if ($clear_stat_cache) {
250             clearstatcache(true, $file_path);
251         }
252         return $this->fix_integer_overflow(filesize($file_path));
253
254     }
255
256     protected function is_valid_file_object($file_name) {
257         $file_path = $this->get_upload_path($file_name);
258         if (is_file($file_path) && $file_name[0] !== '.') {
259             return true;
260         }
261         return false;
262     }
263
264     protected function get_file_object($file_name) {
265         if ($this->is_valid_file_object($file_name)) {
266             $file = new stdClass();
267             $file->name = $file_name;
268             $file->size = $this->get_file_size(
269                 $this->get_upload_path($file_name)
270             );
271             $file->url = $this->get_download_url($file->name);
272             foreach($this->options['image_versions'] as $version => $options) {
273                 if (!empty($version)) {
274                     if (is_file($this->get_upload_path($file_name, $version))) {
275                         $file->{$version.'Url'} = $this->get_download_url(
276                             $file->name,
277                             $version
278                         );
279                     }
280                 }
281             }
282             $this->set_additional_file_properties($file);
283             return $file;
284         }
285         return null;
286     }
287
288     protected function get_file_objects($iteration_method = 'get_file_object') {
289         $upload_dir = $this->get_upload_path();
290         if (!is_dir($upload_dir)) {
291             return array();
292         }
293         return array_values(array_filter(array_map(
294             array($this, $iteration_method),
295             scandir($upload_dir)
296         )));
297     }
298
299     protected function count_file_objects() {
300         return count($this->get_file_objects('is_valid_file_object'));
301     }
302
303     protected function create_scaled_image($file_name, $version, $options) {
304         $file_path = $this->get_upload_path($file_name);
305         if (!empty($version)) {
306             $version_dir = $this->get_upload_path(null, $version);
307             if (!is_dir($version_dir)) {
308                 mkdir($version_dir, $this->options['mkdir_mode'], true);
309             }
310             $new_file_path = $version_dir.'/'.$file_name;
311         } else {
312             $new_file_path = $file_path;
313         }
314         if (!function_exists('getimagesize')) {
315             error_log('Function not found: getimagesize');
316             return false;
317         }
318         list($img_width, $img_height) = @getimagesize($file_path);
319         if (!$img_width || !$img_height) {
320             return false;
321         }
322         $max_width = $options['max_width'];
323         $max_height = $options['max_height'];
324         $scale = min(
325             $max_width / $img_width,
326             $max_height / $img_height
327         );
328         if ($scale >= 1) {
329             if ($file_path !== $new_file_path) {
330                 return copy($file_path, $new_file_path);
331             }
332             return true;
333         }
334         if (!function_exists('imagecreatetruecolor')) {
335             error_log('Function not found: imagecreatetruecolor');
336             return false;
337         }
338         if (empty($options['crop'])) {
339             $new_width = $img_width * $scale;
340             $new_height = $img_height * $scale;
341             $dst_x = 0;
342             $dst_y = 0;
343             $new_img = imagecreatetruecolor($new_width, $new_height);
344         } else {
345             if (($img_width / $img_height) >= ($max_width / $max_height)) {
346                 $new_width = $img_width / ($img_height / $max_height);
347                 $new_height = $max_height;
348             } else {
349                 $new_width = $max_width;
350                 $new_height = $img_height / ($img_width / $max_width);
351             }
352             $dst_x = 0 - ($new_width - $max_width) / 2;
353             $dst_y = 0 - ($new_height - $max_height) / 2;
354             $new_img = imagecreatetruecolor($max_width, $max_height);
355         }
356         switch (strtolower(substr(strrchr($file_name, '.'), 1))) {
357             case 'jpg':
358             case 'jpeg':
359                 $src_img = imagecreatefromjpeg($file_path);
360                 $write_image = 'imagejpeg';
361                 $image_quality = isset($options['jpeg_quality']) ?
362                     $options['jpeg_quality'] : 75;
363                 break;
364             case 'gif':
365                 imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
366                 $src_img = imagecreatefromgif($file_path);
367                 $write_image = 'imagegif';
368                 $image_quality = null;
369                 break;
370             case 'png':
371                 imagecolortransparent($new_img, imagecolorallocate($new_img, 0, 0, 0));
372                 imagealphablending($new_img, false);
373                 imagesavealpha($new_img, true);
374                 $src_img = imagecreatefrompng($file_path);
375                 $write_image = 'imagepng';
376                 $image_quality = isset($options['png_quality']) ?
377                     $options['png_quality'] : 9;
378                 break;
379             default:
380                 imagedestroy($new_img);
381                 return false;
382         }
383         $success = imagecopyresampled(
384             $new_img,
385             $src_img,
386             $dst_x,
387             $dst_y,
388             0,
389             0,
390             $new_width,
391             $new_height,
392             $img_width,
393             $img_height
394         ) && $write_image($new_img, $new_file_path, $image_quality);
395         // Free up memory (imagedestroy does not delete files):
396         imagedestroy($src_img);
397         imagedestroy($new_img);
398         return $success;
399     }
400
401     protected function get_error_message($error) {
402         return array_key_exists($error, $this->error_messages) ?
403             $this->error_messages[$error] : $error;
404     }
405
406     function get_config_bytes($val) {
407         $val = trim($val);
408         $last = strtolower($val[strlen($val)-1]);
409         switch($last) {
410             case 'g':
411                 $val *= 1024;
412             case 'm':
413                 $val *= 1024;
414             case 'k':
415                 $val *= 1024;
416         }
417         return $this->fix_integer_overflow($val);
418     }
419
420     protected function validate($uploaded_file, $file, $error, $index) {
421         if ($error) {
422             $file->error = $this->get_error_message($error);
423             return false;
424         }
425         $content_length = $this->fix_integer_overflow(intval(
426             $this->get_server_var('CONTENT_LENGTH')
427         ));
428         $post_max_size = $this->get_config_bytes(ini_get('post_max_size'));
429         if ($post_max_size && ($content_length > $post_max_size)) {
430             $file->error = $this->get_error_message('post_max_size');
431             return false;
432         }
433         if (!preg_match($this->options['accept_file_types'], $file->name)) {
434             $file->error = $this->get_error_message('accept_file_types');
435             return false;
436         }
437         if ($uploaded_file && is_uploaded_file($uploaded_file)) {
438             $file_size = $this->get_file_size($uploaded_file);
439         } else {
440             $file_size = $content_length;
441         }
442         if ($this->options['max_file_size'] && (
443                 $file_size > $this->options['max_file_size'] ||
444                 $file->size > $this->options['max_file_size'])
445             ) {
446             $file->error = $this->get_error_message('max_file_size');
447             return false;
448         }
449         if ($this->options['min_file_size'] &&
450             $file_size < $this->options['min_file_size']) {
451             $file->error = $this->get_error_message('min_file_size');
452             return false;
453         }
454         if (is_int($this->options['max_number_of_files']) && (
455                 $this->count_file_objects() >= $this->options['max_number_of_files'])
456             ) {
457             $file->error = $this->get_error_message('max_number_of_files');
458             return false;
459         }
460         list($img_width, $img_height) = @getimagesize($uploaded_file);
461         if (is_int($img_width)) {
462             if ($this->options['max_width'] && $img_width > $this->options['max_width']) {
463                 $file->error = $this->get_error_message('max_width');
464                 return false;
465             }
466             if ($this->options['max_height'] && $img_height > $this->options['max_height']) {
467                 $file->error = $this->get_error_message('max_height');
468                 return false;
469             }
470             if ($this->options['min_width'] && $img_width < $this->options['min_width']) {
471                 $file->error = $this->get_error_message('min_width');
472                 return false;
473             }
474             if ($this->options['min_height'] && $img_height < $this->options['min_height']) {
475                 $file->error = $this->get_error_message('min_height');
476                 return false;
477             }
478         }
479         return true;
480     }
481
482     protected function upcount_name_callback($matches) {
483         $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
484         $ext = isset($matches[2]) ? $matches[2] : '';
485         return ' ('.$index.')'.$ext;
486     }
487
488     protected function upcount_name($name) {
489         return preg_replace_callback(
490             '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
491             array($this, 'upcount_name_callback'),
492             $name,
493             1
494         );
495     }
496
497     protected function get_unique_filename($name,
498             $type = null, $index = null, $content_range = null) {
499         while(is_dir($this->get_upload_path($name))) {
500             $name = $this->upcount_name($name);
501         }
502         // Keep an existing filename if this is part of a chunked upload:
503         $uploaded_bytes = $this->fix_integer_overflow(intval($content_range[1]));
504         while(is_file($this->get_upload_path($name))) {
505             if ($uploaded_bytes === $this->get_file_size(
506                     $this->get_upload_path($name))) {
507                 break;
508             }
509             $name = $this->upcount_name($name);
510         }
511         return $name;
512     }
513
514     protected function trim_file_name($name,
515             $type = null, $index = null, $content_range = null) {
516         // Remove path information and dots around the filename, to prevent uploading
517         // into different directories or replacing hidden system files.
518         // Also remove control characters and spaces (\x00..\x20) around the filename:
519         $name = trim(basename(stripslashes($name)), ".\x00..\x20");
520         // Use a timestamp for empty filenames:
521         if (!$name) {
522             $name = str_replace('.', '-', microtime(true));
523         }
524         // Add missing file extension for known image types:
525         if (strpos($name, '.') === false &&
526             preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
527             $name .= '.'.$matches[1];
528         }
529         return $name;
530     }
531
532     protected function get_file_name($name,
533             $type = null, $index = null, $content_range = null) {
534         return $this->get_unique_filename(
535             $this->trim_file_name($name, $type, $index, $content_range),
536             $type,
537             $index,
538             $content_range
539         );
540     }
541
542     protected function handle_form_data($file, $index) {
543         // Handle form data, e.g. $_REQUEST['description'][$index]
544     }
545
546     protected function imageflip($image, $mode) {
547         if (function_exists('imageflip')) {
548             return imageflip($image, $mode);
549         }
550         $new_width = $src_width = imagesx($image);
551         $new_height = $src_height = imagesy($image);
552         $new_img = imagecreatetruecolor($new_width, $new_height);
553         $src_x = 0;
554         $src_y = 0;
555         switch ($mode) {
556             case '1': // flip on the horizontal axis
557                 $src_y = $new_height - 1;
558                 $src_height = -$new_height;
559                 break;
560             case '2': // flip on the vertical axis
561                 $src_x  = $new_width - 1;
562                 $src_width = -$new_width;
563                 break;
564             case '3': // flip on both axes
565                 $src_y = $new_height - 1;
566                 $src_height = -$new_height;
567                 $src_x  = $new_width - 1;
568                 $src_width = -$new_width;
569                 break;
570             default:
571                 return $image;
572         }
573         imagecopyresampled(
574             $new_img,
575             $image,
576             0,
577             0,
578             $src_x,
579             $src_y,
580             $new_width,
581             $new_height,
582             $src_width,
583             $src_height
584         );
585         // Free up memory (imagedestroy does not delete files):
586         imagedestroy($image);
587         return $new_img;
588     }
589
590     protected function orient_image($file_path) {
591         if (!function_exists('exif_read_data')) {
592             return false;
593         }
594         $exif = @exif_read_data($file_path);
595         if ($exif === false) {
596             return false;
597         }
598         $orientation = intval(@$exif['Orientation']);
599         if ($orientation < 2 || $orientation > 8) {
600             return false;
601         }
602         $image = imagecreatefromjpeg($file_path);
603         switch ($orientation) {
604             case 2:
605                 $image = $this->imageflip(
606                     $image,
607                     defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
608                 );
609                 break;
610             case 3:
611                 $image = imagerotate($image, 180, 0);
612                 break;
613             case 4:
614                 $image = $this->imageflip(
615                     $image,
616                     defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
617                 );
618                 break;
619             case 5:
620                 $image = $this->imageflip(
621                     $image,
622                     defined('IMG_FLIP_HORIZONTAL') ? IMG_FLIP_HORIZONTAL : 1
623                 );
624                 $image = imagerotate($image, 270, 0);
625                 break;
626             case 6:
627                 $image = imagerotate($image, 270, 0);
628                 break;
629             case 7:
630                 $image = $this->imageflip(
631                     $image,
632                     defined('IMG_FLIP_VERTICAL') ? IMG_FLIP_VERTICAL : 2
633                 );
634                 $image = imagerotate($image, 270, 0);
635                 break;
636             case 8:
637                 $image = imagerotate($image, 90, 0);
638                 break;
639             default:
640                 return false;
641         }
642         $success = imagejpeg($image, $file_path);
643         // Free up memory (imagedestroy does not delete files):
644         imagedestroy($image);
645         return $success;
646     }
647
648     protected function handle_image_file($file_path, $file) {
649         if ($this->options['orient_image']) {
650             $this->orient_image($file_path);
651         }
652         $failed_versions = array();
653         foreach($this->options['image_versions'] as $version => $options) {
654             if ($this->create_scaled_image($file->name, $version, $options)) {
655                 if (!empty($version)) {
656                     $file->{$version.'Url'} = $this->get_download_url(
657                         $file->name,
658                         $version
659                     );
660                 } else {
661                     $file->size = $this->get_file_size($file_path, true);
662                 }
663             } else {
664                 $failed_versions[] = $version;
665             }
666         }
667         switch (count($failed_versions)) {
668             case 0:
669                 break;
670             case 1:
671                 $file->error = 'Failed to create scaled version: '
672                     .$failed_versions[0];
673                 break;
674             default:
675                 $file->error = 'Failed to create scaled versions: '
676                     .implode($failed_versions,', ');
677         }
678     }
679
680     protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
681             $index = null, $content_range = null) {
682         $file = new stdClass();
683         $file->name = $this->get_file_name($name, $type, $index, $content_range);
684         $file->size = $this->fix_integer_overflow(intval($size));
685         $file->type = $type;
686         if ($this->validate($uploaded_file, $file, $error, $index)) {
687             $this->handle_form_data($file, $index);
688             $upload_dir = $this->get_upload_path();
689             if (!is_dir($upload_dir)) {
690                 mkdir($upload_dir, $this->options['mkdir_mode'], true);
691             }
692             $file_path = $this->get_upload_path($file->name);
693             $append_file = $content_range && is_file($file_path) &&
694                 $file->size > $this->get_file_size($file_path);
695             if ($uploaded_file && is_uploaded_file($uploaded_file)) {
696                 // multipart/formdata uploads (POST method uploads)
697                 if ($append_file) {
698                     file_put_contents(
699                         $file_path,
700                         fopen($uploaded_file, 'r'),
701                         FILE_APPEND
702                     );
703                 } else {
704                     move_uploaded_file($uploaded_file, $file_path);
705                 }
706             } else {
707                 // Non-multipart uploads (PUT method support)
708                 file_put_contents(
709                     $file_path,
710                     fopen('php://input', 'r'),
711                     $append_file ? FILE_APPEND : 0
712                 );
713             }
714             $file_size = $this->get_file_size($file_path, $append_file);
715             if ($file_size === $file->size) {
716                 $file->url = $this->get_download_url($file->name);
717                 list($img_width, $img_height) = @getimagesize($file_path);
718                 if (is_int($img_width) &&
719                         preg_match($this->options['inline_file_types'], $file->name)) {
720                     $this->handle_image_file($file_path, $file);
721                 }
722             } else {
723                 $file->size = $file_size;
724                 if (!$content_range && $this->options['discard_aborted_uploads']) {
725                     unlink($file_path);
726                     $file->error = 'abort';
727                 }
728             }
729             $this->set_additional_file_properties($file);
730         }
731         return $file;
732     }
733
734     protected function readfile($file_path) {
735         $file_size = $this->get_file_size($file_path);
736         $chunk_size = $this->options['readfile_chunk_size'];
737         if ($chunk_size && $file_size > $chunk_size) {
738             $handle = fopen($file_path, 'rb');
739             while (!feof($handle)) {
740                 echo fread($handle, $chunk_size);
741                 ob_flush();
742                 flush();
743             }
744             fclose($handle);
745             return $file_size;
746         }
747         return readfile($file_path);
748     }
749
750     protected function body($str) {
751         echo $str;
752     }
753     
754     protected function header($str) {
755         header($str);
756     }
757
758     protected function get_server_var($id) {
759         return isset($_SERVER[$id]) ? $_SERVER[$id] : '';
760     }
761
762     protected function generate_response($content, $print_response = true) {
763         if ($print_response) {
764             $json = json_encode($content);
765             $redirect = isset($_REQUEST['redirect']) ?
766                 stripslashes($_REQUEST['redirect']) : null;
767             if ($redirect) {
768                 $this->header('Location: '.sprintf($redirect, rawurlencode($json)));
769                 return;
770             }
771             $this->head();
772             if ($this->get_server_var('HTTP_CONTENT_RANGE')) {
773                 $files = isset($content[$this->options['param_name']]) ?
774                     $content[$this->options['param_name']] : null;
775                 if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) {
776                     $this->header('Range: 0-'.(
777                         $this->fix_integer_overflow(intval($files[0]->size)) - 1
778                     ));
779                 }
780             }
781             $this->body($json);
782         }
783         return $content;
784     }
785
786     protected function get_version_param() {
787         return isset($_GET['version']) ? basename(stripslashes($_GET['version'])) : null;
788     }
789
790     protected function get_singular_param_name() {
791         return substr($this->options['param_name'], 0, -1);
792     }
793
794     protected function get_file_name_param() {
795         $name = $this->get_singular_param_name();
796         return isset($_GET[$name]) ? basename(stripslashes($_GET[$name])) : null;
797     }
798
799     protected function get_file_names_params() {
800         $params = isset($_GET[$this->options['param_name']]) ?
801             $_GET[$this->options['param_name']] : array();
802         foreach ($params as $key => $value) {
803             $params[$key] = basename(stripslashes($value));
804         }
805         return $params;
806     }
807
808     protected function get_file_type($file_path) {
809         switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) {
810             case 'jpeg':
811             case 'jpg':
812                 return 'image/jpeg';
813             case 'png':
814                 return 'image/png';
815             case 'gif':
816                 return 'image/gif';
817             default:
818                 return '';
819         }
820     }
821
822     protected function download() {
823         switch ($this->options['download_via_php']) {
824             case 1:
825                 $redirect_header = null;
826                 break;
827             case 2:
828                 $redirect_header = 'X-Sendfile';
829                 break;
830             case 3:
831                 $redirect_header = 'X-Accel-Redirect';
832                 break;
833             default:
834                 return $this->header('HTTP/1.1 403 Forbidden');
835         }
836         $file_name = $this->get_file_name_param();
837         if (!$this->is_valid_file_object($file_name)) {
838             return $this->header('HTTP/1.1 404 Not Found');
839         }
840         if ($redirect_header) {
841             return $this->header(
842                 $redirect_header.': '.$this->get_download_url(
843                     $file_name,
844                     $this->get_version_param(),
845                     true
846                 )
847             );
848         }
849         $file_path = $this->get_upload_path($file_name, $this->get_version_param());
850         // Prevent browsers from MIME-sniffing the content-type:
851         $this->header('X-Content-Type-Options: nosniff');
852         if (!preg_match($this->options['inline_file_types'], $file_name)) {
853             $this->header('Content-Type: application/octet-stream');
854             $this->header('Content-Disposition: attachment; filename="'.$file_name.'"');
855         } else {
856             $this->header('Content-Type: '.$this->get_file_type($file_path));
857             $this->header('Content-Disposition: inline; filename="'.$file_name.'"');
858         }
859         $this->header('Content-Length: '.$this->get_file_size($file_path));
860         $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path)));
861         $this->readfile($file_path);
862     }
863
864     protected function send_content_type_header() {
865         $this->header('Vary: Accept');
866         if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) {
867             $this->header('Content-type: application/json');
868         } else {
869             $this->header('Content-type: text/plain');
870         }
871     }
872
873     protected function send_access_control_headers() {
874         $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']);
875         $this->header('Access-Control-Allow-Credentials: '
876             .($this->options['access_control_allow_credentials'] ? 'true' : 'false'));
877         $this->header('Access-Control-Allow-Methods: '
878             .implode(', ', $this->options['access_control_allow_methods']));
879         $this->header('Access-Control-Allow-Headers: '
880             .implode(', ', $this->options['access_control_allow_headers']));
881     }
882
883     public function head() {
884         $this->header('Pragma: no-cache');
885         $this->header('Cache-Control: no-store, no-cache, must-revalidate');
886         $this->header('Content-Disposition: inline; filename="files.json"');
887         // Prevent Internet Explorer from MIME-sniffing the content-type:
888         $this->header('X-Content-Type-Options: nosniff');
889         if ($this->options['access_control_allow_origin']) {
890             $this->send_access_control_headers();
891         }
892         $this->send_content_type_header();
893     }
894
895     public function get($print_response = true) {
896         if ($print_response && isset($_GET['download'])) {
897             return $this->download();
898         }
899         $file_name = $this->get_file_name_param();
900         if ($file_name) {
901             $response = array(
902                 $this->get_singular_param_name() => $this->get_file_object($file_name)
903             );
904         } else {
905             $response = array(
906                 $this->options['param_name'] => $this->get_file_objects()
907             );
908         }
909         return $this->generate_response($response, $print_response);
910     }
911
912     public function post($print_response = true) {
913         if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') {
914             return $this->delete($print_response);
915         }
916         $upload = isset($_FILES[$this->options['param_name']]) ?
917             $_FILES[$this->options['param_name']] : null;
918         // Parse the Content-Disposition header, if available:
919         $file_name = $this->get_server_var('HTTP_CONTENT_DISPOSITION') ?
920             rawurldecode(preg_replace(
921                 '/(^[^"]+")|("$)/',
922                 '',
923                 $this->get_server_var('HTTP_CONTENT_DISPOSITION')
924             )) : null;
925         // Parse the Content-Range header, which has the following form:
926         // Content-Range: bytes 0-524287/2000000
927         $content_range = $this->get_server_var('HTTP_CONTENT_RANGE') ?
928             preg_split('/[^0-9]+/', $this->get_server_var('HTTP_CONTENT_RANGE')) : null;
929         $size =  $content_range ? $content_range[3] : null;
930         $files = array();
931         if ($upload && is_array($upload['tmp_name'])) {
932             // param_name is an array identifier like "files[]",
933             // $_FILES is a multi-dimensional array:
934             foreach ($upload['tmp_name'] as $index => $value) {
935                 $files[] = $this->handle_file_upload(
936                     $upload['tmp_name'][$index],
937                     $file_name ? $file_name : $upload['name'][$index],
938                     $size ? $size : $upload['size'][$index],
939                     $upload['type'][$index],
940                     $upload['error'][$index],
941                     $index,
942                     $content_range
943                 );
944             }
945         } else {
946             // param_name is a single object identifier like "file",
947             // $_FILES is a one-dimensional array:
948             $files[] = $this->handle_file_upload(
949                 isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
950                 $file_name ? $file_name : (isset($upload['name']) ?
951                         $upload['name'] : null),
952                 $size ? $size : (isset($upload['size']) ?
953                         $upload['size'] : $this->get_server_var('CONTENT_LENGTH')),
954                 isset($upload['type']) ?
955                         $upload['type'] : $this->get_server_var('CONTENT_TYPE'),
956                 isset($upload['error']) ? $upload['error'] : null,
957                 null,
958                 $content_range
959             );
960         }
961         return $this->generate_response(
962             array($this->options['param_name'] => $files),
963             $print_response
964         );
965     }
966
967     public function delete($print_response = true) {
968         $file_names = $this->get_file_names_params();
969         if (empty($file_names)) {
970             $file_names = array($this->get_file_name_param());
971         }
972         $response = array();
973         foreach($file_names as $file_name) {
974             $file_path = $this->get_upload_path($file_name);
975             $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path);
976             if ($success) {
977                 foreach($this->options['image_versions'] as $version => $options) {
978                     if (!empty($version)) {
979                         $file = $this->get_upload_path($file_name, $version);
980                         if (is_file($file)) {
981                             unlink($file);
982                         }
983                     }
984                 }
985             }
986             $response[$file_name] = $success;
987         }
988         return $this->generate_response($response, $print_response);
989     }
990
991 }