1 <?php
2 3 4 5 6 7 8 9 10 11
12
13 class UploadHandler
14 {
15 protected $options;
16
17
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) {
38 $this->options = array(
39 'script_url' => $this->get_full_url().'/',
40 'upload_dir' => dirname($_SERVER['SCRIPT_FILENAME']).'/files/',
41 'upload_url' => $this->get_full_url().'/files/',
42 'user_dirs' => false,
43 'mkdir_mode' => 0755,
44 'param_name' => 'files',
45
46
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
65 'download_via_php' => false,
66
67 'inline_file_types' => '/\.(gif|jpe?g|png)$/i',
68
69 'accept_file_types' => '/.+$/i',
70
71
72 'max_file_size' => null,
73 'min_file_size' => 1,
74
75 'max_number_of_files' => null,
76
77 'max_width' => null,
78 'max_height' => null,
79 'min_width' => 1,
80 'min_height' => 1,
81
82 'discard_aborted_uploads' => true,
83
84 'orient_image' => false,
85 'image_versions' => array(
86
87
88 89 90 91 92 93 94
95
96 97 98 99 100 101 102
103 'thumbnail' => array(
104 'max_width' => 80,
105 'max_height' => 80
106 )
107 )
108 );
109 if ($options) {
110 $this->options = array_merge($this->options, $options);
111 }
112 if ($initialize) {
113 $this->initialize();
114 }
115 }
116
117 protected function initialize() {
118 switch ($_SERVER['REQUEST_METHOD']) {
119 case 'OPTIONS':
120 case 'HEAD':
121 $this->head();
122 break;
123 case 'GET':
124 $this->get();
125 break;
126 case 'PATCH':
127 case 'PUT':
128 case 'POST':
129 $this->post();
130 break;
131 case 'DELETE':
132 $this->delete();
133 break;
134 default:
135 $this->header('HTTP/1.1 405 Method Not Allowed');
136 }
137 }
138
139 protected function get_full_url() {
140 $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';
141 return
142 ($https ? 'https://' : 'http://').
143 (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'].'@' : '').
144 (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'].
145 ($https && $_SERVER['SERVER_PORT'] === 443 ||
146 $_SERVER['SERVER_PORT'] === 80 ? '' : ':'.$_SERVER['SERVER_PORT']))).
147 substr($_SERVER['SCRIPT_NAME'],0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
148 }
149
150 protected function get_user_id() {
151 @session_start();
152 return session_id();
153 }
154
155 protected function get_user_path() {
156 if ($this->options['user_dirs']) {
157 return $this->get_user_id().'/';
158 }
159 return '';
160 }
161
162 protected function get_upload_path($file_name = null, $version = null) {
163 $file_name = $file_name ? $file_name : '';
164 $version_path = empty($version) ? '' : $version.'/';
165 return $this->options['upload_dir'].$this->get_user_path()
166 .$version_path.$file_name;
167 }
168
169 protected function get_query_separator($url) {
170 return strpos($url, '?') === false ? '?' : '&';
171 }
172
173 protected function get_download_url($file_name, $version = null) {
174 if ($this->options['download_via_php']) {
175 $url = $this->options['script_url']
176 .$this->get_query_separator($this->options['script_url'])
177 .'file='.rawurlencode($file_name);
178 if ($version) {
179 $url .= '&version='.rawurlencode($version);
180 }
181 return $url.'&download=1';
182 }
183 $version_path = empty($version) ? '' : rawurlencode($version).'/';
184 return $this->options['upload_url'].$this->get_user_path()
185 .$version_path.rawurlencode($file_name);
186 }
187
188 protected function set_file_delete_properties($file) {
189 $file->delete_url = $this->options['script_url']
190 .$this->get_query_separator($this->options['script_url'])
191 .'file='.rawurlencode($file->name);
192 $file->delete_type = $this->options['delete_type'];
193 if ($file->delete_type !== 'DELETE') {
194 $file->delete_url .= '&_method=DELETE';
195 }
196 if ($this->options['access_control_allow_credentials']) {
197 $file->delete_with_credentials = true;
198 }
199 }
200
201
202
203 protected function fix_integer_overflow($size) {
204 if ($size < 0) {
205 $size += 2.0 * (PHP_INT_MAX + 1);
206 }
207 return $size;
208 }
209
210 protected function get_file_size($file_path, $clear_stat_cache = false) {
211 if ($clear_stat_cache) {
212 clearstatcache(true, $file_path);
213 }
214 return $this->fix_integer_overflow(filesize($file_path));
215
216 }
217
218 protected function is_valid_file_object($file_name) {
219 $file_path = $this->get_upload_path($file_name);
220 if (is_file($file_path) && $file_name[0] !== '.') {
221 return true;
222 }
223 return false;
224 }
225
226 protected function get_file_object($file_name) {
227 if ($this->is_valid_file_object($file_name)) {
228 $file = new stdClass();
229 $file->name = $file_name;
230 $file->size = $this->get_file_size(
231 $this->get_upload_path($file_name)
232 );
233 $file->url = $this->get_download_url($file->name);
234 foreach($this->options['image_versions'] as $version => $options) {
235 if (!empty($version)) {
236 if (is_file($this->get_upload_path($file_name, $version))) {
237 $file->{$version.'_url'} = $this->get_download_url(
238 $file->name,
239 $version
240 );
241 }
242 }
243 }
244 $this->set_file_delete_properties($file);
245 return $file;
246 }
247 return null;
248 }
249
250 protected function get_file_objects($iteration_method = 'get_file_object') {
251 $upload_dir = $this->get_upload_path();
252 if (!is_dir($upload_dir)) {
253 return array();
254 }
255 return array_values(array_filter(array_map(
256 array($this, $iteration_method),
257 scandir($upload_dir)
258 )));
259 }
260
261 protected function count_file_objects() {
262 return count($this->get_file_objects('is_valid_file_object'));
263 }
264
265 protected function create_scaled_image($file_name, $version, $options) {
266 $file_path = $this->get_upload_path($file_name);
267 if (!empty($version)) {
268 $version_dir = $this->get_upload_path(null, $version);
269 if (!is_dir($version_dir)) {
270 mkdir($version_dir, $this->options['mkdir_mode'], true);
271 }
272 $new_file_path = $version_dir.'/'.$file_name;
273 } else {
274 $new_file_path = $file_path;
275 }
276 list($img_width, $img_height) = @getimagesize($file_path);
277 if (!$img_width || !$img_height) {
278 return false;
279 }
280 $scale = min(
281 $options['max_width'] / $img_width,
282 $options['max_height'] / $img_height
283 );
284 if ($scale >= 1) {
285 if ($file_path !== $new_file_path) {
286 return copy($file_path, $new_file_path);
287 }
288 return true;
289 }
290 $new_width = $img_width * $scale;
291 $new_height = $img_height * $scale;
292 $new_img = @imagecreatetruecolor($new_width, $new_height);
293 switch (strtolower(substr(strrchr($file_name, '.'), 1))) {
294 case 'jpg':
295 case 'jpeg':
296 $src_img = @imagecreatefromjpeg($file_path);
297 $write_image = 'imagejpeg';
298 $image_quality = isset($options['jpeg_quality']) ?
299 $options['jpeg_quality'] : 75;
300 break;
301 case 'gif':
302 @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0));
303 $src_img = @imagecreatefromgif($file_path);
304 $write_image = 'imagegif';
305 $image_quality = null;
306 break;
307 case 'png':
308 @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0));
309 @imagealphablending($new_img, false);
310 @imagesavealpha($new_img, true);
311 $src_img = @imagecreatefrompng($file_path);
312 $write_image = 'imagepng';
313 $image_quality = isset($options['png_quality']) ?
314 $options['png_quality'] : 9;
315 break;
316 default:
317 $src_img = null;
318 }
319 $success = $src_img && @imagecopyresampled(
320 $new_img,
321 $src_img,
322 0, 0, 0, 0,
323 $new_width,
324 $new_height,
325 $img_width,
326 $img_height
327 ) && $write_image($new_img, $new_file_path, $image_quality);
328
329 @imagedestroy($src_img);
330 @imagedestroy($new_img);
331 return $success;
332 }
333
334 protected function get_error_message($error) {
335 return array_key_exists($error, $this->error_messages) ?
336 $this->error_messages[$error] : $error;
337 }
338
339 function get_config_bytes($val) {
340 $val = trim($val);
341 $last = strtolower($val[strlen($val)-1]);
342 switch($last) {
343 case 'g':
344 $val *= 1024;
345 case 'm':
346 $val *= 1024;
347 case 'k':
348 $val *= 1024;
349 }
350 return $this->fix_integer_overflow($val);
351 }
352
353 protected function validate($uploaded_file, $file, $error, $index) {
354 if ($error) {
355 $file->error = $this->get_error_message($error);
356 return false;
357 }
358 $content_length = $this->fix_integer_overflow(intval($_SERVER['CONTENT_LENGTH']));
359 $post_max_size = $this->get_config_bytes(ini_get('post_max_size'));
360 if ($post_max_size && ($content_length > $post_max_size)) {
361 $file->error = $this->get_error_message('post_max_size');
362 return false;
363 }
364 if (!preg_match($this->options['accept_file_types'], $file->name)) {
365 $file->error = $this->get_error_message('accept_file_types');
366 return false;
367 }
368 if ($uploaded_file && is_uploaded_file($uploaded_file)) {
369 $file_size = $this->get_file_size($uploaded_file);
370 } else {
371 $file_size = $content_length;
372 }
373 if ($this->options['max_file_size'] && (
374 $file_size > $this->options['max_file_size'] ||
375 $file->size > $this->options['max_file_size'])
376 ) {
377 $file->error = $this->get_error_message('max_file_size');
378 return false;
379 }
380 if ($this->options['min_file_size'] &&
381 $file_size < $this->options['min_file_size']) {
382 $file->error = $this->get_error_message('min_file_size');
383 return false;
384 }
385 if (is_int($this->options['max_number_of_files']) && (
386 $this->count_file_objects() >= $this->options['max_number_of_files'])
387 ) {
388 $file->error = $this->get_error_message('max_number_of_files');
389 return false;
390 }
391 list($img_width, $img_height) = @getimagesize($uploaded_file);
392 if (is_int($img_width)) {
393 if ($this->options['max_width'] && $img_width > $this->options['max_width']) {
394 $file->error = $this->get_error_message('max_width');
395 return false;
396 }
397 if ($this->options['max_height'] && $img_height > $this->options['max_height']) {
398 $file->error = $this->get_error_message('max_height');
399 return false;
400 }
401 if ($this->options['min_width'] && $img_width < $this->options['min_width']) {
402 $file->error = $this->get_error_message('min_width');
403 return false;
404 }
405 if ($this->options['min_height'] && $img_height < $this->options['min_height']) {
406 $file->error = $this->get_error_message('min_height');
407 return false;
408 }
409 }
410 return true;
411 }
412
413 protected function upcount_name_callback($matches) {
414 $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
415 $ext = isset($matches[2]) ? $matches[2] : '';
416 return ' ('.$index.')'.$ext;
417 }
418
419 protected function upcount_name($name) {
420 return preg_replace_callback(
421 '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
422 array($this, 'upcount_name_callback'),
423 $name,
424 1
425 );
426 }
427
428 protected function get_unique_filename($name, $type, $index, $content_range) {
429 while(is_dir($this->get_upload_path($name))) {
430 $name = $this->upcount_name($name);
431 }
432
433 $uploaded_bytes = $this->fix_integer_overflow(intval($content_range[1]));
434 while(is_file($this->get_upload_path($name))) {
435 if ($uploaded_bytes === $this->get_file_size(
436 $this->get_upload_path($name))) {
437 break;
438 }
439 $name = $this->upcount_name($name);
440 }
441 return $name;
442 }
443
444 protected function trim_file_name($name, $type, $index, $content_range) {
445
446
447
448 $name = trim(basename(stripslashes($name)), ".\x00..\x20");
449
450 if (!$name) {
451 $name = str_replace('.', '-', microtime(true));
452 }
453
454 if (strpos($name, '.') === false &&
455 preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
456 $name .= '.'.$matches[1];
457 }
458 return $name;
459 }
460
461 protected function get_file_name($name, $type, $index, $content_range) {
462 return $this->get_unique_filename(
463 $this->trim_file_name($name, $type, $index, $content_range),
464 $type,
465 $index,
466 $content_range
467 );
468 }
469
470 protected function handle_form_data($file, $index) {
471
472 }
473
474 protected function orient_image($file_path) {
475 if (!function_exists('exif_read_data')) {
476 return false;
477 }
478 $exif = @exif_read_data($file_path);
479 if ($exif === false) {
480 return false;
481 }
482 $orientation = intval(@$exif['Orientation']);
483 if (!in_array($orientation, array(3, 6, 8))) {
484 return false;
485 }
486 $image = @imagecreatefromjpeg($file_path);
487 switch ($orientation) {
488 case 3:
489 $image = @imagerotate($image, 180, 0);
490 break;
491 case 6:
492 $image = @imagerotate($image, 270, 0);
493 break;
494 case 8:
495 $image = @imagerotate($image, 90, 0);
496 break;
497 default:
498 return false;
499 }
500 $success = imagejpeg($image, $file_path);
501
502 @imagedestroy($image);
503 return $success;
504 }
505
506 protected function handle_file_upload($uploaded_file, $name, $size, $type, $error,
507 $index = null, $content_range = null) {
508 $file = new stdClass();
509 $file->name = $this->get_file_name($name, $type, $index, $content_range);
510 $file->size = $this->fix_integer_overflow(intval($size));
511 $file->type = $type;
512 if ($this->validate($uploaded_file, $file, $error, $index)) {
513 $this->handle_form_data($file, $index);
514 $upload_dir = $this->get_upload_path();
515 if (!is_dir($upload_dir)) {
516 mkdir($upload_dir, $this->options['mkdir_mode'], true);
517 }
518 $file_path = $this->get_upload_path($file->name);
519 $append_file = $content_range && is_file($file_path) &&
520 $file->size > $this->get_file_size($file_path);
521 if ($uploaded_file && is_uploaded_file($uploaded_file)) {
522
523 if ($append_file) {
524 file_put_contents(
525 $file_path,
526 fopen($uploaded_file, 'r'),
527 FILE_APPEND
528 );
529 } else {
530 move_uploaded_file($uploaded_file, $file_path);
531 }
532 } else {
533
534 file_put_contents(
535 $file_path,
536 fopen('php://input', 'r'),
537 $append_file ? FILE_APPEND : 0
538 );
539 }
540 $file_size = $this->get_file_size($file_path, $append_file);
541 if ($file_size === $file->size) {
542 if ($this->options['orient_image']) {
543 $this->orient_image($file_path);
544 }
545 $file->url = $this->get_download_url($file->name);
546 foreach($this->options['image_versions'] as $version => $options) {
547 if ($this->create_scaled_image($file->name, $version, $options)) {
548 if (!empty($version)) {
549 $file->{$version.'_url'} = $this->get_download_url(
550 $file->name,
551 $version
552 );
553 } else {
554 $file_size = $this->get_file_size($file_path, true);
555 }
556 }
557 }
558 } else if (!$content_range && $this->options['discard_aborted_uploads']) {
559 unlink($file_path);
560 $file->error = 'abort';
561 }
562 $file->size = $file_size;
563 $this->set_file_delete_properties($file);
564 }
565 return $file;
566 }
567
568 protected function readfile($file_path) {
569 return readfile($file_path);
570 }
571
572 protected function body($str) {
573 echo $str;
574 }
575
576 protected function header($str) {
577 header($str);
578 }
579
580 protected function generate_response($content, $print_response = true) {
581 if ($print_response) {
582 $json = json_encode($content);
583 $redirect = isset($_REQUEST['redirect']) ?
584 stripslashes($_REQUEST['redirect']) : null;
585 if ($redirect) {
586 $this->header('Location: '.sprintf($redirect, rawurlencode($json)));
587 return;
588 }
589 $this->head();
590 if (isset($_SERVER['HTTP_CONTENT_RANGE'])) {
591 $files = isset($content[$this->options['param_name']]) ?
592 $content[$this->options['param_name']] : null;
593 if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) {
594 $this->header('Range: 0-'.($this->fix_integer_overflow(intval($files[0]->size)) - 1));
595 }
596 }
597 $this->body($json);
598 }
599 return $content;
600 }
601
602 protected function get_version_param() {
603 return isset($_GET['version']) ? basename(stripslashes($_GET['version'])) : null;
604 }
605
606 protected function get_file_name_param() {
607 return isset($_GET['file']) ? basename(stripslashes($_GET['file'])) : null;
608 }
609
610 protected function get_file_type($file_path) {
611 switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) {
612 case 'jpeg':
613 case 'jpg':
614 return 'image/jpeg';
615 case 'png':
616 return 'image/png';
617 case 'gif':
618 return 'image/gif';
619 default:
620 return '';
621 }
622 }
623
624 protected function download() {
625 if (!$this->options['download_via_php']) {
626 $this->header('HTTP/1.1 403 Forbidden');
627 return;
628 }
629 $file_name = $this->get_file_name_param();
630 if ($this->is_valid_file_object($file_name)) {
631 $file_path = $this->get_upload_path($file_name, $this->get_version_param());
632 if (is_file($file_path)) {
633 if (!preg_match($this->options['inline_file_types'], $file_name)) {
634 $this->header('Content-Description: File Transfer');
635 $this->header('Content-Type: application/octet-stream');
636 $this->header('Content-Disposition: attachment; filename="'.$file_name.'"');
637 $this->header('Content-Transfer-Encoding: binary');
638 } else {
639
640 $this->header('X-Content-Type-Options: nosniff');
641 $this->header('Content-Type: '.$this->get_file_type($file_path));
642 $this->header('Content-Disposition: inline; filename="'.$file_name.'"');
643 }
644 $this->header('Content-Length: '.$this->get_file_size($file_path));
645 $this->header('Last-Modified: '.gmdate('D, d M Y H:i:s T', filemtime($file_path)));
646 $this->readfile($file_path);
647 }
648 }
649 }
650
651 protected function send_content_type_header() {
652 $this->header('Vary: Accept');
653 if (isset($_SERVER['HTTP_ACCEPT']) &&
654 (strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false)) {
655 $this->header('Content-type: application/json');
656 } else {
657 $this->header('Content-type: text/plain');
658 }
659 }
660
661 protected function send_access_control_headers() {
662 $this->header('Access-Control-Allow-Origin: '.$this->options['access_control_allow_origin']);
663 $this->header('Access-Control-Allow-Credentials: '
664 .($this->options['access_control_allow_credentials'] ? 'true' : 'false'));
665 $this->header('Access-Control-Allow-Methods: '
666 .implode(', ', $this->options['access_control_allow_methods']));
667 $this->header('Access-Control-Allow-Headers: '
668 .implode(', ', $this->options['access_control_allow_headers']));
669 }
670
671 public function head() {
672 $this->header('Pragma: no-cache');
673 $this->header('Cache-Control: no-store, no-cache, must-revalidate');
674 $this->header('Content-Disposition: inline; filename="files.json"');
675
676 $this->header('X-Content-Type-Options: nosniff');
677 if ($this->options['access_control_allow_origin']) {
678 $this->send_access_control_headers();
679 }
680 $this->send_content_type_header();
681 }
682
683 public function get($print_response = true) {
684 if ($print_response && isset($_GET['download'])) {
685 return $this->download();
686 }
687 $file_name = $this->get_file_name_param();
688 if ($file_name) {
689 $response = array(
690 substr($this->options['param_name'], 0, -1) => $this->get_file_object($file_name)
691 );
692 } else {
693 $response = array(
694 $this->options['param_name'] => $this->get_file_objects()
695 );
696 }
697 return $this->generate_response($response, $print_response);
698 }
699
700 public function post($print_response = true) {
701 if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') {
702 return $this->delete($print_response);
703 }
704 $upload = isset($_FILES[$this->options['param_name']]) ?
705 $_FILES[$this->options['param_name']] : null;
706
707 $file_name = isset($_SERVER['HTTP_CONTENT_DISPOSITION']) ?
708 rawurldecode(preg_replace(
709 '/(^[^"]+")|("$)/',
710 '',
711 $_SERVER['HTTP_CONTENT_DISPOSITION']
712 )) : null;
713
714
715 $content_range = isset($_SERVER['HTTP_CONTENT_RANGE']) ?
716 preg_split('/[^0-9]+/', $_SERVER['HTTP_CONTENT_RANGE']) : null;
717 $size = $content_range ? $content_range[3] : null;
718 $files = array();
719 if ($upload && is_array($upload['tmp_name'])) {
720
721
722 foreach ($upload['tmp_name'] as $index => $value) {
723 $files[] = $this->handle_file_upload(
724 $upload['tmp_name'][$index],
725 $file_name ? $file_name : $upload['name'][$index],
726 $size ? $size : $upload['size'][$index],
727 $upload['type'][$index],
728 $upload['error'][$index],
729 $index,
730 $content_range
731 );
732 }
733 } else {
734
735
736 $files[] = $this->handle_file_upload(
737 isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
738 $file_name ? $file_name : (isset($upload['name']) ?
739 $upload['name'] : null),
740 $size ? $size : (isset($upload['size']) ?
741 $upload['size'] : $_SERVER['CONTENT_LENGTH']),
742 isset($upload['type']) ?
743 $upload['type'] : $_SERVER['CONTENT_TYPE'],
744 isset($upload['error']) ? $upload['error'] : null,
745 null,
746 $content_range
747 );
748 }
749 return $this->generate_response(
750 array($this->options['param_name'] => $files),
751 $print_response
752 );
753 }
754
755 public function delete($print_response = true) {
756 $file_name = $this->get_file_name_param();
757 $file_path = $this->get_upload_path($file_name);
758 $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path);
759 if ($success) {
760 foreach($this->options['image_versions'] as $version => $options) {
761 if (!empty($version)) {
762 $file = $this->get_upload_path($file_name, $version);
763 if (is_file($file)) {
764 unlink($file);
765 }
766 }
767 }
768 }
769 return $this->generate_response(array('success' => $success), $print_response);
770 }
771
772 }
773