Subversion-Projekte lars-tiefland.webanos.marine-sales.de

Revision

Revision 270 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
2 lars 1
<?php
2
 
3
    namespace App\Http\Controllers;
4
 
5
    use Illuminate\Http\Request;
6
 
7
    class MediumController extends Controller
8
    {
9
 
10
        protected $options;
11
 
12
        // PHP File Upload error message codes:
13
        // https://php.net/manual/en/features.file-upload.errors.php
14
        protected $error_messages = array(
15
            1                     => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
16
            2                     => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
17
            3                     => 'The uploaded file was only partially uploaded',
18
            4                     => 'No file was uploaded',
19
            6                     => 'Missing a temporary folder',
20
            7                     => 'Failed to write file to disk',
21
            8                     => 'A PHP extension stopped the file upload',
22
            'post_max_size'       => 'The uploaded file exceeds the post_max_size directive in php.ini',
23
            'max_file_size'       => 'File is too big',
24
            'min_file_size'       => 'File is too small',
25
            'accept_file_types'   => 'Filetype not allowed',
26
            'max_number_of_files' => 'Maximum number of files exceeded',
27
            'invalid_file_type'   => 'Invalid file type',
28
            'max_width'           => 'Image exceeds maximum width',
29
            'min_width'           => 'Image requires a minimum width',
30
            'max_height'          => 'Image exceeds maximum height',
31
            'min_height'          => 'Image requires a minimum height',
32
            'abort'               => 'File upload aborted',
33
            'image_resize'        => 'Failed to resize image'
34
        );
35
 
36
        const IMAGETYPE_GIF  = 'image/gif';
37
        const IMAGETYPE_JPEG = 'image/jpeg';
38
        const IMAGETYPE_PNG  = 'image/png';
39
 
40
        protected $image_objects = array();
41
        protected $response      = array();
42
 
43
        public function __construct( $options = null, $initialize = true, $error_messages = null )
44
        {
45
            $this->options = array(
46
                'script_url'                       => $this->get_full_url() . '/' . $this->basename( $this->get_server_var( 'SCRIPT_NAME' ) ),
47
                'upload_dir'                       => dirname( $this->get_server_var( 'SCRIPT_FILENAME' ) ) . '/files/',
48
                'upload_url'                       => $this->get_full_url() . '/files/',
49
                'input_stream'                     => 'php://input',
50
                'user_dirs'                        => false,
51
                'mkdir_mode'                       => 0755,
52
                'param_name'                       => 'files',
53
                // Set the following option to 'POST', if your server does not support
54
                // DELETE requests. This is a parameter sent to the client:
55
                'delete_type'                      => 'DELETE',
56
                'access_control_allow_origin'      => '*',
57
                'access_control_allow_credentials' => false,
58
                'access_control_allow_methods'     => array(
59
                    'OPTIONS',
60
                    'HEAD',
61
                    'GET',
62
                    'POST',
63
                    'PUT',
64
                    'PATCH',
65
                    'DELETE'
66
                ),
67
                'access_control_allow_headers'     => array(
68
                    'Content-Type',
69
                    'Content-Range',
70
                    'Content-Disposition'
71
                ),
72
                // By default, allow redirects to the referer protocol+host:
73
                'redirect_allow_target'            => '/^' . preg_quote(
74
                        parse_url( $this->get_server_var( 'HTTP_REFERER' ), PHP_URL_SCHEME )
75
                        . '://'
76
                        . parse_url( $this->get_server_var( 'HTTP_REFERER' ), PHP_URL_HOST )
77
                        . '/', // Trailing slash to not match subdomains by mistake
78
                        '/' // preg_quote delimiter param
79
                    ) . '/',
80
                // Enable to provide file downloads via GET requests to the PHP script:
81
                //     1. Set to 1 to download files via readfile method through PHP
82
                //     2. Set to 2 to send a X-Sendfile header for lighttpd/Apache
83
                //     3. Set to 3 to send a X-Accel-Redirect header for nginx
84
                // If set to 2 or 3, adjust the upload_url option to the base path of
85
                // the redirect parameter, e.g. '/files/'.
86
                'download_via_php'                 => false,
87
                // Read files in chunks to avoid memory limits when download_via_php
88
                // is enabled, set to 0 to disable chunked reading of files:
89
                'readfile_chunk_size'              => 10 * 1024 * 1024, // 10 MiB
90
                // Defines which files can be displayed inline when downloaded:
91
                'inline_file_types'                => '/\.(gif|jpe?g|png)$/i',
92
                // Defines which files (based on their names) are accepted for upload.
93
                // By default, only allows file uploads with image file extensions.
94
                // Only change this setting after making sure that any allowed file
95
                // types cannot be executed by the webserver in the files directory,
96
                // e.g. PHP scripts, nor executed by the browser when downloaded,
97
                // e.g. HTML files with embedded JavaScript code.
98
                // Please also read the SECURITY.md document in this repository.
99
                'accept_file_types'                => '/\.(gif|jpe?g|png)$/i',
100
                // Replaces dots in filenames with the given string.
101
                // Can be disabled by setting it to false or an empty string.
102
                // Note that this is a security feature for servers that support
103
                // multiple file extensions, e.g. the Apache AddHandler Directive:
104
                // https://httpd.apache.org/docs/current/mod/mod_mime.html#addhandler
105
                // Before disabling it, make sure that files uploaded with multiple
106
                // extensions cannot be executed by the webserver, e.g.
107
                // "example.php.png" with embedded PHP code, nor executed by the
108
                // browser when downloaded, e.g. "example.html.gif" with embedded
109
                // JavaScript code.
110
                'replace_dots_in_filenames'        => '-',
111
                // The php.ini settings upload_max_filesize and post_max_size
112
                // take precedence over the following max_file_size setting:
113
                'max_file_size'                    => null,
114
                'min_file_size'                    => 1,
115
                // The maximum number of files for the upload directory:
116
                'max_number_of_files'              => null,
117
                // Reads first file bytes to identify and correct file extensions:
118
                'correct_image_extensions'         => false,
119
                // Image resolution restrictions:
120
                'max_width'                        => null,
121
                'max_height'                       => null,
122
                'min_width'                        => 1,
123
                'min_height'                       => 1,
124
                // Set the following option to false to enable resumable uploads:
125
                'discard_aborted_uploads'          => true,
126
                // Set to 0 to use the GD library to scale and orient images,
127
                // set to 1 to use imagick (if installed, falls back to GD),
128
                // set to 2 to use the ImageMagick convert binary directly:
129
                'image_library'                    => 1,
130
                // Uncomment the following to define an array of resource limits
131
                // for imagick:
132
                /*
133
            'imagick_resource_limits' => array(
134
                imagick::RESOURCETYPE_MAP => 32,
135
                imagick::RESOURCETYPE_MEMORY => 32
136
            ),
137
            */
138
                // Command or path for to the ImageMagick convert binary:
139
                'convert_bin'                      => 'convert',
140
                // Uncomment the following to add parameters in front of each
141
                // ImageMagick convert call (the limit constraints seem only
142
                // to have an effect if put in front):
143
                /*
144
            'convert_params' => '-limit memory 32MiB -limit map 32MiB',
145
            */
146
                // Command or path for to the ImageMagick identify binary:
147
                'identify_bin'                     => 'identify',
148
                'image_versions'                   => array(
149
                    // The empty image version key defines options for the original image.
150
                    // Keep in mind: these image manipulations are inherited by all other image versions from this point onwards.
151
                    // Also note that the property 'no_cache' is not inherited, since it's not a manipulation.
152
                    ''          => array(
153
                        // Automatically rotate images based on EXIF meta data:
154
                        'auto_orient' => true
155
                    ),
156
                    // You can add arrays to generate different versions.
157
                    // The name of the key is the name of the version (example: 'medium').
158
                    // the array contains the options to apply.
159
                    /*
160
                'medium' => array(
161
                    'max_width' => 800,
162
                    'max_height' => 600
163
                ),
164
                */
165
                    'thumbnail' => array(
166
                        // Uncomment the following to use a defined directory for the thumbnails
167
                        // instead of a subdirectory based on the version identifier.
168
                        // Make sure that this directory doesn't allow execution of files if you
169
                        // don't pose any restrictions on the type of uploaded files, e.g. by
170
                        // copying the .htaccess file from the files directory for Apache:
171
                        //'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')).'/thumb/',
172
                        //'upload_url' => $this->get_full_url().'/thumb/',
173
                        // Uncomment the following to force the max
174
                        // dimensions and e.g. create square thumbnails:
175
                        // 'auto_orient' => true,
176
                        // 'crop' => true,
177
                        // 'jpeg_quality' => 70,
178
                        // 'no_cache' => true, (there's a caching option, but this remembers thumbnail sizes from a previous action!)
179
                        // 'strip' => true, (this strips EXIF tags, such as geolocation)
180
                        'max_width'  => 80, // either specify width, or set to 0. Then width is automatically adjusted - keeping aspect ratio to a specified max_height.
181
                        'max_height' => 80 // either specify height, or set to 0. Then height is automatically adjusted - keeping aspect ratio to a specified max_width.
182
                    )
183
                ),
184
                'print_response'                   => true
185
            );
186
            if ( $options )
187
            {
188
                $this->options = $options + $this->options;
189
            }
190
            if ( $error_messages )
191
            {
192
                $this->error_messages = $error_messages + $this->error_messages;
193
            }
194
        }
195
 
196
        public function index()
197
        {
198
 
199
        }
270 lars 200
 
2 lars 201
        //
202
        public function create()
203
        {
204
            $this->get( $this->options['print_response'] );
205
        }
206
 
207
        public function store()
208
        {
209
            $this->post( $this->options['print_response'] );
210
        }
211
 
212
        public function update()
213
        {
214
            $this->post( $this->options['print_response'] );
215
        }
216
 
217
        public function destroy()
218
        {
219
            $this->delete( $this->options['print_response'] );
220
        }
221
 
222
        protected function get_full_url(): string
223
        {
270 lars 224
            if ( php_sapi_name() != "cli" )
225
            {
226
                $https = !empty( $_SERVER['HTTPS'] ) && strcasecmp( $_SERVER['HTTPS'], 'on' ) === 0 ||
227
                    !empty( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) &&
228
                    strcasecmp( $_SERVER['HTTP_X_FORWARDED_PROTO'], 'https' ) === 0;
229
                return
230
                    ( $https ? 'https://' : 'http://' ) .
231
                    ( !empty( $_SERVER['REMOTE_USER'] ) ? $_SERVER['REMOTE_USER'] . '@' : '' ) .
232
                    ( $_SERVER['HTTP_HOST'] ?? ( $_SERVER['SERVER_NAME'] .
233
                        ( $https && $_SERVER['SERVER_PORT'] === 443 ||
234
                        $_SERVER['SERVER_PORT'] === 80 ? '' : ':' . $_SERVER['SERVER_PORT'] ) ) ) .
235
                    substr( $_SERVER['SCRIPT_NAME'], 0, strrpos( $_SERVER['SCRIPT_NAME'], '/' ) );
236
            }
271 lars 237
            return "";
2 lars 238
        }
239
 
240
        protected function get_user_id()
241
        {
242
            @session_start();
243
            return session_id();
244
        }
245
 
246
        protected function get_user_path(): string
247
        {
248
            if ( $this->options['user_dirs'] )
249
            {
250
                return $this->get_user_id() . '/';
251
            }
252
            return '';
253
        }
254
 
255
        protected function get_upload_path( $file_name = null, $version = null ): string
256
        {
257
            $file_name = $file_name ? $file_name : '';
258
            if ( empty( $version ) )
259
            {
260
                $version_path = '';
261
            }
262
            else
263
            {
264
                $version_dir = @$this->options['image_versions'][$version]['upload_dir'];
265
                if ( $version_dir )
266
                {
267
                    return $version_dir . $this->get_user_path() . $file_name;
268
                }
269
                $version_path = $version . '/';
270
            }
271
            return $this->options['upload_dir'] . $this->get_user_path()
272
                . $version_path . $file_name;
273
        }
274
 
275
        protected function get_query_separator( $url ): string
276
        {
277
            return !str_contains( $url, '?' ) ? '?' : '&';
278
        }
279
 
280
        protected function get_download_url( $file_name, $version = null, $direct = false ): string
281
        {
282
            if ( !$direct && $this->options['download_via_php'] )
283
            {
284
                $url = $this->options['script_url']
285
                    . $this->get_query_separator( $this->options['script_url'] )
286
                    . $this->get_singular_param_name()
287
                    . '=' . rawurlencode( $file_name );
288
                if ( $version )
289
                {
290
                    $url .= '&version=' . rawurlencode( $version );
291
                }
292
                return $url . '&download=1';
293
            }
294
            if ( empty( $version ) )
295
            {
296
                $version_path = '';
297
            }
298
            else
299
            {
300
                $version_url = @$this->options['image_versions'][$version]['upload_url'];
301
                if ( $version_url )
302
                {
303
                    return $version_url . $this->get_user_path() . rawurlencode( $file_name );
304
                }
305
                $version_path = rawurlencode( $version ) . '/';
306
            }
307
            return $this->options['upload_url'] . $this->get_user_path()
308
                . $version_path . rawurlencode( $file_name );
309
        }
310
 
311
        protected function set_additional_file_properties( $file ): void
312
        {
313
            $file->deleteUrl = $this->options['script_url']
314
                . $this->get_query_separator( $this->options['script_url'] )
315
                . $this->get_singular_param_name()
316
                . '=' . rawurlencode( $file->name );
317
            $file->deleteType = $this->options['delete_type'];
318
            if ( $file->deleteType !== 'DELETE' )
319
            {
320
                $file->deleteUrl .= '&_method=DELETE';
321
            }
322
            if ( $this->options['access_control_allow_credentials'] )
323
            {
324
                $file->deleteWithCredentials = true;
325
            }
326
        }
327
 
328
        // Fix for overflowing signed 32 bit integers,
329
        // works for sizes up to 2^32-1 bytes (4 GiB - 1):
330
        protected function fix_integer_overflow( $size ): float
331
        {
332
            if ( $size < 0 )
333
            {
334
                $size += 2.0 * ( PHP_INT_MAX + 1 );
335
            }
336
            return $size;
337
        }
338
 
339
        protected function get_file_size( $file_path, $clear_stat_cache = false ): float
340
        {
341
            if ( $clear_stat_cache )
342
            {
343
                if ( version_compare( PHP_VERSION, '5.3.0' ) >= 0 )
344
                {
345
                    clearstatcache( true, $file_path );
346
                }
347
                else
348
                {
349
                    clearstatcache();
350
                }
351
            }
352
            return $this->fix_integer_overflow( filesize( $file_path ) );
353
        }
354
 
355
        protected function is_valid_file_object( $file_name ): bool
356
        {
357
            $file_path = $this->get_upload_path( $file_name );
358
            if ( strlen( $file_name ) > 0 && $file_name[0] !== '.' && is_file( $file_path ) )
359
            {
360
                return true;
361
            }
362
            return false;
363
        }
364
 
365
        protected function get_file_object( $file_name ): ?\stdClass
366
        {
367
            if ( $this->is_valid_file_object( $file_name ) )
368
            {
369
                $file = new \stdClass();
370
                $file->name = $file_name;
371
                $file->size = $this->get_file_size(
372
                    $this->get_upload_path( $file_name )
373
                );
374
                $file->url = $this->get_download_url( $file->name );
375
                foreach ( $this->options['image_versions'] as $version => $options )
376
                {
377
                    if ( !empty( $version ) )
378
                    {
379
                        if ( is_file( $this->get_upload_path( $file_name, $version ) ) )
380
                        {
381
                            $file->{$version . 'Url'} = $this->get_download_url(
382
                                $file->name,
383
                                $version
384
                            );
385
                        }
386
                    }
387
                }
388
                $this->set_additional_file_properties( $file );
389
                return $file;
390
            }
391
            return null;
392
        }
393
 
394
        protected function get_file_objects( $iteration_method = 'get_file_object' ): array
395
        {
396
            $upload_dir = $this->get_upload_path();
397
            if ( !is_dir( $upload_dir ) )
398
            {
399
                return array();
400
            }
401
            return array_values( array_filter( array_map(
402
                array( $this, $iteration_method ),
403
                scandir( $upload_dir )
404
            ) ) );
405
        }
406
 
407
        protected function count_file_objects(): int
408
        {
409
            return count( $this->get_file_objects( 'is_valid_file_object' ) );
410
        }
411
 
412
        protected function get_error_message( $error )
413
        {
414
            return $this->error_messages[$error] ?? $error;
415
        }
416
 
417
        public function get_config_bytes( $val ): float
418
        {
419
            $val = trim( $val );
420
            $last = strtolower( $val[strlen( $val ) - 1] );
421
            if ( is_numeric( $val ) )
422
            {
423
                $val = (int)$val;
424
            }
425
            else
426
            {
427
                $val = (int)substr( $val, 0, -1 );
428
            }
429
            switch ( $last )
430
            {
431
                case 'g':
432
                    $val *= 1024;
433
                case 'm':
434
                    $val *= 1024;
435
                case 'k':
436
                    $val *= 1024;
437
            }
438
            return $this->fix_integer_overflow( $val );
439
        }
440
 
441
        protected function validate_image_file( $uploaded_file, $file, $error, $index ): bool
442
        {
443
            if ( $this->imagetype( $uploaded_file ) !== $this->get_file_type( $file->name ) )
444
            {
445
                $file->error = $this->get_error_message( 'invalid_file_type' );
446
                return false;
447
            }
448
            $max_width = @$this->options['max_width'];
449
            $max_height = @$this->options['max_height'];
450
            $min_width = @$this->options['min_width'];
451
            $min_height = @$this->options['min_height'];
452
            if ( $max_width || $max_height || $min_width || $min_height )
453
            {
454
                list( $img_width, $img_height ) = $this->get_image_size( $uploaded_file );
455
                // If we are auto rotating the image by default, do the checks on
456
                // the correct orientation
457
                if (
458
                    @$this->options['image_versions']['']['auto_orient'] &&
459
                    function_exists( 'exif_read_data' ) &&
460
                    ( $exif = @exif_read_data( $uploaded_file ) ) &&
461
                    ( ( (int)@$exif['Orientation'] ) >= 5 )
462
                )
463
                {
464
                    $tmp = $img_width;
465
                    $img_width = $img_height;
466
                    $img_height = $tmp;
467
                    unset( $tmp );
468
                }
469
                if ( !empty( $img_width ) && !empty( $img_height ) )
470
                {
471
                    if ( $max_width && $img_width > $max_width )
472
                    {
473
                        $file->error = $this->get_error_message( 'max_width' );
474
                        return false;
475
                    }
476
                    if ( $max_height && $img_height > $max_height )
477
                    {
478
                        $file->error = $this->get_error_message( 'max_height' );
479
                        return false;
480
                    }
481
                    if ( $min_width && $img_width < $min_width )
482
                    {
483
                        $file->error = $this->get_error_message( 'min_width' );
484
                        return false;
485
                    }
486
                    if ( $min_height && $img_height < $min_height )
487
                    {
488
                        $file->error = $this->get_error_message( 'min_height' );
489
                        return false;
490
                    }
491
                }
492
            }
493
            return true;
494
        }
495
 
496
        protected function validate_file( $uploaded_file, $file, $error, $index, $content_range ): bool
497
        {
498
            if ( $error )
499
            {
500
                $file->error = $this->get_error_message( $error );
501
                return false;
502
            }
503
            $content_length = $this->fix_integer_overflow(
504
                (int)$this->get_server_var( 'CONTENT_LENGTH' )
505
            );
506
            $post_max_size = $this->get_config_bytes( ini_get( 'post_max_size' ) );
507
            if ( $post_max_size && ( $content_length > $post_max_size ) )
508
            {
509
                $file->error = $this->get_error_message( 'post_max_size' );
510
                return false;
511
            }
512
            if ( !preg_match( $this->options['accept_file_types'], $file->name ) )
513
            {
514
                $file->error = $this->get_error_message( 'accept_file_types' );
515
                return false;
516
            }
517
            if ( $uploaded_file && is_uploaded_file( $uploaded_file ) )
518
            {
519
                $file_size = $this->get_file_size( $uploaded_file );
520
            }
521
            else
522
            {
523
                $file_size = $content_length;
524
            }
525
            if (
526
                $this->options['max_file_size'] && (
527
                    $file_size > $this->options['max_file_size'] ||
528
                    $file->size > $this->options['max_file_size'] )
529
            )
530
            {
531
                $file->error = $this->get_error_message( 'max_file_size' );
532
                return false;
533
            }
534
            if (
535
                $this->options['min_file_size'] &&
536
                $file_size < $this->options['min_file_size']
537
            )
538
            {
539
                $file->error = $this->get_error_message( 'min_file_size' );
540
                return false;
541
            }
542
            if (
543
                is_int( $this->options['max_number_of_files'] ) &&
544
                ( $this->count_file_objects() >= $this->options['max_number_of_files'] ) &&
545
                // Ignore additional chunks of existing files:
546
                !is_file( $this->get_upload_path( $file->name ) )
547
            )
548
            {
549
                $file->error = $this->get_error_message( 'max_number_of_files' );
550
                return false;
551
            }
552
            if ( !$content_range && $this->has_image_file_extension( $file->name ) )
553
            {
554
                return $this->validate_image_file( $uploaded_file, $file, $error, $index );
555
            }
556
            return true;
557
        }
558
 
559
        protected function upcount_name_callback( $matches ): string
560
        {
561
            $index = isset( $matches[1] ) ? ( (int)$matches[1] ) + 1 : 1;
562
            $ext = isset( $matches[2] ) ? $matches[2] : '';
563
            return ' (' . $index . ')' . $ext;
564
        }
565
 
566
        protected function upcount_name( $name )
567
        {
568
            return preg_replace_callback(
569
                '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/',
570
                array( $this, 'upcount_name_callback' ),
571
                $name,
572
                1
573
            );
574
        }
575
 
576
        protected function get_unique_filename( $file_path, $name, $size, $type, $error,
577
                                                $index, $content_range )
578
        {
579
            while ( is_dir( $this->get_upload_path( $name ) ) )
580
            {
581
                $name = $this->upcount_name( $name );
582
            }
583
            // Keep an existing filename if this is part of a chunked upload:
584
            $uploaded_bytes = $this->fix_integer_overflow( (int)@$content_range[1] );
585
            while ( is_file( $this->get_upload_path( $name ) ) )
586
            {
587
                if (
588
                    $uploaded_bytes === $this->get_file_size(
589
                        $this->get_upload_path( $name ) )
590
                )
591
                {
592
                    break;
593
                }
594
                $name = $this->upcount_name( $name );
595
            }
596
            return $name;
597
        }
598
 
599
        protected function get_valid_image_extensions( $file_path )
600
        {
601
            switch ( $this->imagetype( $file_path ) )
602
            {
603
                case self::IMAGETYPE_JPEG:
604
                    return array( 'jpg', 'jpeg' );
605
                case self::IMAGETYPE_PNG:
606
                    return array( 'png' );
607
                case self::IMAGETYPE_GIF:
608
                    return array( 'gif' );
609
            }
610
        }
611
 
612
        protected function fix_file_extension( $file_path, $name, $size, $type, $error,
613
                                               $index, $content_range )
614
        {
615
            // Add missing file extension for known image types:
616
            if (
617
                strpos( $name, '.' ) === false &&
618
                preg_match( '/^image\/(gif|jpe?g|png)/', $type, $matches )
619
            )
620
            {
621
                $name .= '.' . $matches[1];
622
            }
623
            if ( $this->options['correct_image_extensions'] )
624
            {
625
                $extensions = $this->get_valid_image_extensions( $file_path );
626
                // Adjust incorrect image file extensions:
627
                if ( !empty( $extensions ) )
628
                {
629
                    $parts = explode( '.', $name );
630
                    $extIndex = count( $parts ) - 1;
631
                    $ext = strtolower( @$parts[$extIndex] );
632
                    if ( !in_array( $ext, $extensions ) )
633
                    {
634
                        $parts[$extIndex] = $extensions[0];
635
                        $name = implode( '.', $parts );
636
                    }
637
                }
638
            }
639
            return $name;
640
        }
641
 
642
        protected function trim_file_name( $file_path, $name, $size, $type, $error,
643
                                           $index, $content_range ): array|string
644
        {
645
            // Remove path information and dots around the filename, to prevent uploading
646
            // into different directories or replacing hidden system files.
647
            // Also remove control characters and spaces (\x00..\x20) around the filename:
648
            $name = trim( $this->basename( stripslashes( $name ) ), ".\x00..\x20" );
649
            // Replace dots in filenames to avoid security issues with servers
650
            // that interpret multiple file extensions, e.g. "example.php.png":
651
            $replacement = $this->options['replace_dots_in_filenames'];
652
            if ( !empty( $replacement ) )
653
            {
654
                $parts = explode( '.', $name );
655
                if ( count( $parts ) > 2 )
656
                {
657
                    $ext = array_pop( $parts );
658
                    $name = implode( $replacement, $parts ) . '.' . $ext;
659
                }
660
            }
661
            // Use a timestamp for empty filenames:
662
            if ( !$name )
663
            {
664
                $name = str_replace( '.', '-', microtime( true ) );
665
            }
666
            return $name;
667
        }
668
 
669
        protected function get_file_name( $file_path, $name, $size, $type, $error,
670
                                          $index, $content_range )
671
        {
672
            $name = $this->trim_file_name( $file_path, $name, $size, $type, $error,
673
                $index, $content_range );
674
            return $this->get_unique_filename(
675
                $file_path,
676
                $this->fix_file_extension( $file_path, $name, $size, $type, $error,
677
                    $index, $content_range ),
678
                $size,
679
                $type,
680
                $error,
681
                $index,
682
                $content_range
683
            );
684
        }
685
 
686
        protected function get_scaled_image_file_paths( $file_name, $version )
687
        {
688
            $file_path = $this->get_upload_path( $file_name );
689
            if ( !empty( $version ) )
690
            {
691
                $version_dir = $this->get_upload_path( null, $version );
692
                if ( !is_dir( $version_dir ) )
693
                {
694
                    mkdir( $version_dir, $this->options['mkdir_mode'], true );
695
                }
696
                $new_file_path = $version_dir . '/' . $file_name;
697
            }
698
            else
699
            {
700
                $new_file_path = $file_path;
701
            }
702
            return array( $file_path, $new_file_path );
703
        }
704
 
705
        protected function gd_get_image_object( $file_path, $func, $no_cache = false )
706
        {
707
            if ( empty( $this->image_objects[$file_path] ) || $no_cache )
708
            {
709
                $this->gd_destroy_image_object( $file_path );
710
                $this->image_objects[$file_path] = $func( $file_path );
711
            }
712
            return $this->image_objects[$file_path];
713
        }
714
 
715
        protected function gd_set_image_object( $file_path, $image )
716
        {
717
            $this->gd_destroy_image_object( $file_path );
718
            $this->image_objects[$file_path] = $image;
719
        }
720
 
721
        protected function gd_destroy_image_object( $file_path )
722
        {
723
            $image = ( isset( $this->image_objects[$file_path] ) ) ? $this->image_objects[$file_path] : null;
724
            return $image && imagedestroy( $image );
725
        }
726
 
727
        protected function gd_imageflip( $image, $mode )
728
        {
729
            if ( function_exists( 'imageflip' ) )
730
            {
731
                return imageflip( $image, $mode );
732
            }
733
            $new_width = $src_width = imagesx( $image );
734
            $new_height = $src_height = imagesy( $image );
735
            $new_img = imagecreatetruecolor( $new_width, $new_height );
736
            $src_x = 0;
737
            $src_y = 0;
738
            switch ( $mode )
739
            {
740
                case '1': // flip on the horizontal axis
741
                    $src_y = $new_height - 1;
742
                    $src_height = -$new_height;
743
                    break;
744
                case '2': // flip on the vertical axis
745
                    $src_x = $new_width - 1;
746
                    $src_width = -$new_width;
747
                    break;
748
                case '3': // flip on both axes
749
                    $src_y = $new_height - 1;
750
                    $src_height = -$new_height;
751
                    $src_x = $new_width - 1;
752
                    $src_width = -$new_width;
753
                    break;
754
                default:
755
                    return $image;
756
            }
757
            imagecopyresampled(
758
                $new_img,
759
                $image,
760
                0,
761
                0,
762
                $src_x,
763
                $src_y,
764
                $new_width,
765
                $new_height,
766
                $src_width,
767
                $src_height
768
            );
769
            return $new_img;
770
        }
771
 
772
        protected function gd_orient_image( $file_path, $src_img )
773
        {
774
            if ( !function_exists( 'exif_read_data' ) )
775
            {
776
                return false;
777
            }
778
            $exif = @exif_read_data( $file_path );
779
            if ( $exif === false )
780
            {
781
                return false;
782
            }
783
            $orientation = (int)@$exif['Orientation'];
784
            if ( $orientation < 2 || $orientation > 8 )
785
            {
786
                return false;
787
            }
788
            switch ( $orientation )
789
            {
790
                case 2:
791
                    $new_img = $this->gd_imageflip(
792
                        $src_img,
793
                        defined( 'IMG_FLIP_VERTICAL' ) ? IMG_FLIP_VERTICAL : 2
794
                    );
795
                    break;
796
                case 3:
797
                    $new_img = imagerotate( $src_img, 180, 0 );
798
                    break;
799
                case 4:
800
                    $new_img = $this->gd_imageflip(
801
                        $src_img,
802
                        defined( 'IMG_FLIP_HORIZONTAL' ) ? IMG_FLIP_HORIZONTAL : 1
803
                    );
804
                    break;
805
                case 5:
806
                    $tmp_img = $this->gd_imageflip(
807
                        $src_img,
808
                        defined( 'IMG_FLIP_HORIZONTAL' ) ? IMG_FLIP_HORIZONTAL : 1
809
                    );
810
                    $new_img = imagerotate( $tmp_img, 270, 0 );
811
                    imagedestroy( $tmp_img );
812
                    break;
813
                case 6:
814
                    $new_img = imagerotate( $src_img, 270, 0 );
815
                    break;
816
                case 7:
817
                    $tmp_img = $this->gd_imageflip(
818
                        $src_img,
819
                        defined( 'IMG_FLIP_VERTICAL' ) ? IMG_FLIP_VERTICAL : 2
820
                    );
821
                    $new_img = imagerotate( $tmp_img, 270, 0 );
822
                    imagedestroy( $tmp_img );
823
                    break;
824
                case 8:
825
                    $new_img = imagerotate( $src_img, 90, 0 );
826
                    break;
827
                default:
828
                    return false;
829
            }
830
            $this->gd_set_image_object( $file_path, $new_img );
831
            return true;
832
        }
833
 
834
        protected function gd_create_scaled_image( $file_name, $version, $options )
835
        {
836
            if ( !function_exists( 'imagecreatetruecolor' ) )
837
            {
838
                error_log( 'Function not found: imagecreatetruecolor' );
839
                return false;
840
            }
841
            list( $file_path, $new_file_path ) =
842
                $this->get_scaled_image_file_paths( $file_name, $version );
843
            $type = strtolower( substr( strrchr( $file_name, '.' ), 1 ) );
844
            switch ( $type )
845
            {
846
                case 'jpg':
847
                case 'jpeg':
848
                    $src_func = 'imagecreatefromjpeg';
849
                    $write_func = 'imagejpeg';
850
                    $image_quality = $options['jpeg_quality'] ?? 75;
851
                    break;
852
                case 'gif':
853
                    $src_func = 'imagecreatefromgif';
854
                    $write_func = 'imagegif';
855
                    $image_quality = null;
856
                    break;
857
                case 'png':
858
                    $src_func = 'imagecreatefrompng';
859
                    $write_func = 'imagepng';
860
                    $image_quality = $options['png_quality'] ?? 9;
861
                    break;
862
                default:
863
                    return false;
864
            }
865
            $src_img = $this->gd_get_image_object(
866
                $file_path,
867
                $src_func,
868
                !empty( $options['no_cache'] )
869
            );
870
            $image_oriented = false;
871
            if (
872
                !empty( $options['auto_orient'] ) && $this->gd_orient_image(
873
                    $file_path,
874
                    $src_img
875
                )
876
            )
877
            {
878
                $image_oriented = true;
879
                $src_img = $this->gd_get_image_object(
880
                    $file_path,
881
                    $src_func
882
                );
883
            }
884
            $max_width = $img_width = imagesx( $src_img );
885
            $max_height = $img_height = imagesy( $src_img );
886
            if ( !empty( $options['max_width'] ) )
887
            {
888
                $max_width = $options['max_width'];
889
            }
890
            if ( !empty( $options['max_height'] ) )
891
            {
892
                $max_height = $options['max_height'];
893
            }
894
            $scale = min(
895
                $max_width / $img_width,
896
                $max_height / $img_height
897
            );
898
            if ( $scale >= 1 )
899
            {
900
                if ( $image_oriented )
901
                {
902
                    return $write_func( $src_img, $new_file_path, $image_quality );
903
                }
904
                if ( $file_path !== $new_file_path )
905
                {
906
                    return copy( $file_path, $new_file_path );
907
                }
908
                return true;
909
            }
910
            if ( empty( $options['crop'] ) )
911
            {
912
                $new_width = $img_width * $scale;
913
                $new_height = $img_height * $scale;
914
                $dst_x = 0;
915
                $dst_y = 0;
916
                $new_img = imagecreatetruecolor( $new_width, $new_height );
917
            }
918
            else
919
            {
920
                if ( ( $img_width / $img_height ) >= ( $max_width / $max_height ) )
921
                {
922
                    $new_width = $img_width / ( $img_height / $max_height );
923
                    $new_height = $max_height;
924
                }
925
                else
926
                {
927
                    $new_width = $max_width;
928
                    $new_height = $img_height / ( $img_width / $max_width );
929
                }
930
                $dst_x = 0 - ( $new_width - $max_width ) / 2;
931
                $dst_y = 0 - ( $new_height - $max_height ) / 2;
932
                $new_img = imagecreatetruecolor( $max_width, $max_height );
933
            }
934
            // Handle transparency in GIF and PNG images:
935
            switch ( $type )
936
            {
937
                case 'gif':
938
                    imagecolortransparent( $new_img, imagecolorallocate( $new_img, 0, 0, 0 ) );
939
                    break;
940
                case 'png':
941
                    imagecolortransparent( $new_img, imagecolorallocate( $new_img, 0, 0, 0 ) );
942
                    imagealphablending( $new_img, false );
943
                    imagesavealpha( $new_img, true );
944
                    break;
945
            }
946
            $success = imagecopyresampled(
947
                    $new_img,
948
                    $src_img,
949
                    $dst_x,
950
                    $dst_y,
951
                    0,
952
                    0,
953
                    $new_width,
954
                    $new_height,
955
                    $img_width,
956
                    $img_height
957
                ) && $write_func( $new_img, $new_file_path, $image_quality );
958
            $this->gd_set_image_object( $file_path, $new_img );
959
            return $success;
960
        }
961
 
962
        protected function imagick_get_image_object( $file_path, $no_cache = false )
963
        {
964
            if ( empty( $this->image_objects[$file_path] ) || $no_cache )
965
            {
966
                $this->imagick_destroy_image_object( $file_path );
967
                $image = new \Imagick();
968
                if ( !empty( $this->options['imagick_resource_limits'] ) )
969
                {
970
                    foreach ( $this->options['imagick_resource_limits'] as $type => $limit )
971
                    {
972
                        $image->setResourceLimit( $type, $limit );
973
                    }
974
                }
975
                try
976
                {
977
                    $image->readImage( $file_path );
978
                }
979
                catch ( ImagickException $e )
980
                {
981
                    error_log( $e->getMessage() );
982
                    return null;
983
                }
984
                $this->image_objects[$file_path] = $image;
985
            }
986
            return $this->image_objects[$file_path];
987
        }
988
 
989
        protected function imagick_set_image_object( $file_path, $image )
990
        {
991
            $this->imagick_destroy_image_object( $file_path );
992
            $this->image_objects[$file_path] = $image;
993
        }
994
 
995
        protected function imagick_destroy_image_object( $file_path )
996
        {
997
            $image = ( isset( $this->image_objects[$file_path] ) ) ? $this->image_objects[$file_path] : null;
998
            return $image && $image->destroy();
999
        }
1000
 
1001
        protected function imagick_orient_image( $image ): bool
1002
        {
1003
            $orientation = $image->getImageOrientation();
1004
            $background = new \ImagickPixel( 'none' );
1005
            switch ( $orientation )
1006
            {
1007
                case \imagick::ORIENTATION_TOPRIGHT: // 2
1008
                    $image->flopImage(); // horizontal flop around y-axis
1009
                    break;
1010
                case \imagick::ORIENTATION_BOTTOMRIGHT: // 3
1011
                    $image->rotateImage( $background, 180 );
1012
                    break;
1013
                case \imagick::ORIENTATION_BOTTOMLEFT: // 4
1014
                    $image->flipImage(); // vertical flip around x-axis
1015
                    break;
1016
                case \imagick::ORIENTATION_LEFTTOP: // 5
1017
                    $image->flopImage(); // horizontal flop around y-axis
1018
                    $image->rotateImage( $background, 270 );
1019
                    break;
1020
                case \imagick::ORIENTATION_RIGHTTOP: // 6
1021
                    $image->rotateImage( $background, 90 );
1022
                    break;
1023
                case \imagick::ORIENTATION_RIGHTBOTTOM: // 7
1024
                    $image->flipImage(); // vertical flip around x-axis
1025
                    $image->rotateImage( $background, 270 );
1026
                    break;
1027
                case \imagick::ORIENTATION_LEFTBOTTOM: // 8
1028
                    $image->rotateImage( $background, 270 );
1029
                    break;
1030
                default:
1031
                    return false;
1032
            }
1033
            $image->setImageOrientation( \imagick::ORIENTATION_TOPLEFT ); // 1
1034
            return true;
1035
        }
1036
 
1037
        protected function imagick_create_scaled_image( $file_name, $version, $options ): bool
1038
        {
1039
            list( $file_path, $new_file_path ) =
1040
                $this->get_scaled_image_file_paths( $file_name, $version );
1041
            $image = $this->imagick_get_image_object(
1042
                $file_path,
1043
                !empty( $options['crop'] ) || !empty( $options['no_cache'] )
1044
            );
1045
            if ( is_null( $image ) )
1046
            {
1047
                return false;
1048
            }
1049
            if ( $image->getImageFormat() === 'GIF' )
1050
            {
1051
                // Handle animated GIFs:
1052
                $images = $image->coalesceImages();
1053
                foreach ( $images as $frame )
1054
                {
1055
                    $image = $frame;
1056
                    $this->imagick_set_image_object( $file_name, $image );
1057
                    break;
1058
                }
1059
            }
1060
            $image_oriented = false;
1061
            if ( !empty( $options['auto_orient'] ) )
1062
            {
1063
                $image_oriented = $this->imagick_orient_image( $image );
1064
            }
1065
            $image_resize = false;
1066
            $new_width = $max_width = $img_width = $image->getImageWidth();
1067
            $new_height = $max_height = $img_height = $image->getImageHeight();
1068
            // use isset(). User might be setting max_width = 0 (auto in regular resizing). Value 0 would be considered empty when you use empty()
1069
            if ( isset( $options['max_width'] ) )
1070
            {
1071
                $image_resize = true;
1072
                $new_width = $max_width = $options['max_width'];
1073
            }
1074
            if ( isset( $options['max_height'] ) )
1075
            {
1076
                $image_resize = true;
1077
                $new_height = $max_height = $options['max_height'];
1078
            }
1079
            $image_strip = ( isset( $options['strip'] ) ? $options['strip'] : false );
1080
            if ( !$image_oriented && ( $max_width >= $img_width ) && ( $max_height >= $img_height ) && !$image_strip && empty( $options["jpeg_quality"] ) )
1081
            {
1082
                if ( $file_path !== $new_file_path )
1083
                {
1084
                    return copy( $file_path, $new_file_path );
1085
                }
1086
                return true;
1087
            }
1088
            $crop = ( isset( $options['crop'] ) ? $options['crop'] : false );
1089
 
1090
            if ( $crop )
1091
            {
1092
                $x = 0;
1093
                $y = 0;
1094
                if ( ( $img_width / $img_height ) >= ( $max_width / $max_height ) )
1095
                {
1096
                    $new_width = 0; // Enables proportional scaling based on max_height
1097
                    $x = ( $img_width / ( $img_height / $max_height ) - $max_width ) / 2;
1098
                }
1099
                else
1100
                {
1101
                    $new_height = 0; // Enables proportional scaling based on max_width
1102
                    $y = ( $img_height / ( $img_width / $max_width ) - $max_height ) / 2;
1103
                }
1104
            }
1105
            $success = $image->resizeImage(
1106
                $new_width,
1107
                $new_height,
1108
                isset( $options['filter'] ) ? $options['filter'] : \imagick::FILTER_LANCZOS,
1109
                isset( $options['blur'] ) ? $options['blur'] : 1,
1110
                $new_width && $new_height // fit image into constraints if not to be cropped
1111
            );
1112
            if ( $success && $crop )
1113
            {
1114
                $success = $image->cropImage(
1115
                    $max_width,
1116
                    $max_height,
1117
                    $x,
1118
                    $y
1119
                );
1120
                if ( $success )
1121
                {
1122
                    $success = $image->setImagePage( $max_width, $max_height, 0, 0 );
1123
                }
1124
            }
1125
            $type = strtolower( substr( strrchr( $file_name, '.' ), 1 ) );
1126
            switch ( $type )
1127
            {
1128
                case 'jpg':
1129
                case 'jpeg':
1130
                    if ( !empty( $options['jpeg_quality'] ) )
1131
                    {
1132
                        $image->setImageCompression( \imagick::COMPRESSION_JPEG );
1133
                        $image->setImageCompressionQuality( $options['jpeg_quality'] );
1134
                    }
1135
                    break;
1136
            }
1137
            if ( $image_strip )
1138
            {
1139
                $image->stripImage();
1140
            }
1141
            return $success && $image->writeImage( $new_file_path );
1142
        }
1143
 
1144
        protected function imagemagick_create_scaled_image( $file_name, $version, $options ): bool
1145
        {
1146
            list( $file_path, $new_file_path ) =
1147
                $this->get_scaled_image_file_paths( $file_name, $version );
1148
            $resize = @$options['max_width']
1149
                . ( empty( $options['max_height'] ) ? '' : 'X' . $options['max_height'] );
1150
            if ( !$resize && empty( $options['auto_orient'] ) )
1151
            {
1152
                if ( $file_path !== $new_file_path )
1153
                {
1154
                    return copy( $file_path, $new_file_path );
1155
                }
1156
                return true;
1157
            }
1158
            $cmd = $this->options['convert_bin'];
1159
            if ( !empty( $this->options['convert_params'] ) )
1160
            {
1161
                $cmd .= ' ' . $this->options['convert_params'];
1162
            }
1163
            $cmd .= ' ' . escapeshellarg( $file_path );
1164
            if ( !empty( $options['auto_orient'] ) )
1165
            {
1166
                $cmd .= ' -auto-orient';
1167
            }
1168
            if ( $resize )
1169
            {
1170
                // Handle animated GIFs:
1171
                $cmd .= ' -coalesce';
1172
                if ( empty( $options['crop'] ) )
1173
                {
1174
                    $cmd .= ' -resize ' . escapeshellarg( $resize . '>' );
1175
                }
1176
                else
1177
                {
1178
                    $cmd .= ' -resize ' . escapeshellarg( $resize . '^' );
1179
                    $cmd .= ' -gravity center';
1180
                    $cmd .= ' -crop ' . escapeshellarg( $resize . '+0+0' );
1181
                }
1182
                // Make sure the page dimensions are correct (fixes offsets of animated GIFs):
1183
                $cmd .= ' +repage';
1184
            }
1185
            if ( !empty( $options['convert_params'] ) )
1186
            {
1187
                $cmd .= ' ' . $options['convert_params'];
1188
            }
1189
            $cmd .= ' ' . escapeshellarg( $new_file_path );
1190
            exec( $cmd, $output, $error );
1191
            if ( $error )
1192
            {
1193
                error_log( implode( '\n', $output ) );
1194
                return false;
1195
            }
1196
            return true;
1197
        }
1198
 
1199
        protected function get_image_size( $file_path ): array|bool
1200
        {
1201
            if ( $this->options['image_library'] )
1202
            {
1203
                if ( extension_loaded( 'imagick' ) )
1204
                {
1205
                    $image = new \Imagick();
1206
                    try
1207
                    {
1208
                        if ( @$image->pingImage( $file_path ) )
1209
                        {
1210
                            $dimensions = array( $image->getImageWidth(), $image->getImageHeight() );
1211
                            $image->destroy();
1212
                            return $dimensions;
1213
                        }
1214
                        return false;
1215
                    }
1216
                    catch ( \Exception $e )
1217
                    {
1218
                        error_log( $e->getMessage() );
1219
                    }
1220
                }
1221
                if ( $this->options['image_library'] === 2 )
1222
                {
1223
                    $cmd = $this->options['identify_bin'];
1224
                    $cmd .= ' -ping ' . escapeshellarg( $file_path );
1225
                    exec( $cmd, $output, $error );
1226
                    if ( !$error && !empty( $output ) )
1227
                    {
1228
                        // image.jpg JPEG 1920x1080 1920x1080+0+0 8-bit sRGB 465KB 0.000u 0:00.000
1229
                        $infos = preg_split( '/\s+/', substr( $output[0], strlen( $file_path ) ) );
1230
                        $dimensions = preg_split( '/x/', $infos[2] );
1231
                        return $dimensions;
1232
                    }
1233
                    return false;
1234
                }
1235
            }
1236
            if ( !function_exists( 'getimagesize' ) )
1237
            {
1238
                error_log( 'Function not found: getimagesize' );
1239
                return false;
1240
            }
1241
            return @getimagesize( $file_path );
1242
        }
1243
 
1244
        protected function create_scaled_image( $file_name, $version, $options )
1245
        {
1246
            try
1247
            {
1248
                if ( $this->options['image_library'] === 2 )
1249
                {
1250
                    return $this->imagemagick_create_scaled_image( $file_name, $version, $options );
1251
                }
1252
                if ( $this->options['image_library'] && extension_loaded( 'imagick' ) )
1253
                {
1254
                    return $this->imagick_create_scaled_image( $file_name, $version, $options );
1255
                }
1256
                return $this->gd_create_scaled_image( $file_name, $version, $options );
1257
            }
1258
            catch ( \Exception $e )
1259
            {
1260
                error_log( $e->getMessage() );
1261
                return false;
1262
            }
1263
        }
1264
 
1265
        protected function destroy_image_object( $file_path )
1266
        {
1267
            if ( $this->options['image_library'] && extension_loaded( 'imagick' ) )
1268
            {
1269
                return $this->imagick_destroy_image_object( $file_path );
1270
            }
1271
        }
1272
 
1273
        protected function imagetype( $file_path ): bool|string
1274
        {
1275
            $fp = fopen( $file_path, 'r' );
1276
            $data = fread( $fp, 4 );
1277
            fclose( $fp );
1278
            // GIF: 47 49 46 38
1279
            if ( $data === 'GIF8' )
1280
            {
1281
                return self::IMAGETYPE_GIF;
1282
            }
1283
            // JPG: FF D8 FF
1284
            if ( bin2hex( substr( $data, 0, 3 ) ) === 'ffd8ff' )
1285
            {
1286
                return self::IMAGETYPE_JPEG;
1287
            }
1288
            // PNG: 89 50 4E 47
1289
            if ( bin2hex( @$data[0] ) . substr( $data, 1, 4 ) === '89PNG' )
1290
            {
1291
                return self::IMAGETYPE_PNG;
1292
            }
1293
            return false;
1294
        }
1295
 
1296
        protected function is_valid_image_file( $file_path ): bool
1297
        {
1298
            return !!$this->imagetype( $file_path );
1299
        }
1300
 
1301
        protected function has_image_file_extension( $file_path ): bool
1302
        {
1303
            return !!preg_match( '/\.(gif|jpe?g|png)$/i', $file_path );
1304
        }
1305
 
1306
        protected function handle_image_file( $file_path, $file ): void
1307
        {
1308
            $failed_versions = array();
1309
            foreach ( $this->options['image_versions'] as $version => $options )
1310
            {
1311
                if ( $this->create_scaled_image( $file->name, $version, $options ) )
1312
                {
1313
                    if ( !empty( $version ) )
1314
                    {
1315
                        $file->{$version . 'Url'} = $this->get_download_url(
1316
                            $file->name,
1317
                            $version
1318
                        );
1319
                    }
1320
                    else
1321
                    {
1322
                        $file->size = $this->get_file_size( $file_path, true );
1323
                    }
1324
                }
1325
                else
1326
                {
1327
                    $failed_versions[] = $version ? $version : 'original';
1328
                }
1329
            }
1330
            if ( count( $failed_versions ) )
1331
            {
1332
                $file->error = $this->get_error_message( 'image_resize' )
1333
                    . ' (' . implode( ', ', $failed_versions ) . ')';
1334
            }
1335
            // Free memory:
1336
            $this->destroy_image_object( $file_path );
1337
        }
1338
 
1339
        protected function handle_file_upload( $uploaded_file, $name, $size, $type, $error,
1340
                                               $index = null, $content_range = null ): stdClass
1341
        {
1342
            $file = new \stdClass();
1343
            $file->name = $this->get_file_name( $uploaded_file, $name, $size, $type, $error,
1344
                $index, $content_range );
1345
            $file->size = $this->fix_integer_overflow( (int)$size );
1346
            $file->type = $type;
1347
            if ( $this->validate_file( $uploaded_file, $file, $error, $index, $content_range ) )
1348
            {
1349
                $this->handle_form_data( $file, $index );
1350
                $upload_dir = $this->get_upload_path();
1351
                if ( !is_dir( $upload_dir ) )
1352
                {
1353
                    mkdir( $upload_dir, $this->options['mkdir_mode'], true );
1354
                }
1355
                $file_path = $this->get_upload_path( $file->name );
1356
                $append_file = $content_range && is_file( $file_path ) &&
1357
                    $file->size > $this->get_file_size( $file_path );
1358
                if ( $uploaded_file && is_uploaded_file( $uploaded_file ) )
1359
                {
1360
                    // multipart/formdata uploads (POST method uploads)
1361
                    if ( $append_file )
1362
                    {
1363
                        file_put_contents(
1364
                            $file_path,
1365
                            fopen( $uploaded_file, 'r' ),
1366
                            FILE_APPEND
1367
                        );
1368
                    }
1369
                    else
1370
                    {
1371
                        move_uploaded_file( $uploaded_file, $file_path );
1372
                    }
1373
                }
1374
                else
1375
                {
1376
                    // Non-multipart uploads (PUT method support)
1377
                    file_put_contents(
1378
                        $file_path,
1379
                        fopen( $this->options['input_stream'], 'r' ),
1380
                        $append_file ? FILE_APPEND : 0
1381
                    );
1382
                }
1383
                $file_size = $this->get_file_size( $file_path, $append_file );
1384
                if ( $file_size === $file->size )
1385
                {
1386
                    $file->url = $this->get_download_url( $file->name );
1387
                    if ( $this->has_image_file_extension( $file->name ) )
1388
                    {
1389
                        if ( $content_range && !$this->validate_image_file( $file_path, $file, $error, $index ) )
1390
                        {
1391
                            unlink( $file_path );
1392
                        }
1393
                        else
1394
                        {
1395
                            $this->handle_image_file( $file_path, $file );
1396
                        }
1397
                    }
1398
                }
1399
                else
1400
                {
1401
                    $file->size = $file_size;
1402
                    if ( !$content_range && $this->options['discard_aborted_uploads'] )
1403
                    {
1404
                        unlink( $file_path );
1405
                        $file->error = $this->get_error_message( 'abort' );
1406
                    }
1407
                }
1408
                $this->set_additional_file_properties( $file );
1409
            }
1410
            return $file;
1411
        }
1412
 
1413
        protected function readfile( $file_path ): float|bool|int
1414
        {
1415
            $file_size = $this->get_file_size( $file_path );
1416
            $chunk_size = $this->options['readfile_chunk_size'];
1417
            if ( $chunk_size && $file_size > $chunk_size )
1418
            {
1419
                $handle = fopen( $file_path, 'rb' );
1420
                while ( !feof( $handle ) )
1421
                {
1422
                    echo fread( $handle, $chunk_size );
1423
                    @ob_flush();
1424
                    @flush();
1425
                }
1426
                fclose( $handle );
1427
                return $file_size;
1428
            }
1429
            return readfile( $file_path );
1430
        }
1431
 
1432
        protected function body( $str ): void
1433
        {
1434
            echo $str;
1435
        }
1436
 
1437
        protected function header( $str ): void
1438
        {
1439
            header( $str );
1440
        }
1441
 
1442
        protected function get_upload_data( $id )
1443
        {
1444
            return @$_FILES[$id];
1445
        }
1446
 
1447
        protected function get_post_param( $id )
1448
        {
1449
            return @$_POST[$id];
1450
        }
1451
 
1452
        protected function get_query_param( $id )
1453
        {
1454
            return @$_GET[$id];
1455
        }
1456
 
1457
        protected function get_server_var( $id )
1458
        {
1459
            return @$_SERVER[$id];
1460
        }
1461
 
1462
        protected function handle_form_data( $file, $index )
1463
        {
1464
            // Handle form data, e.g. $_POST['description'][$index]
1465
        }
1466
 
1467
        protected function get_version_param(): string
1468
        {
1469
            return $this->basename( stripslashes( $this->get_query_param( 'version' ) ) );
1470
        }
1471
 
1472
        protected function get_singular_param_name(): string
1473
        {
1474
            return substr( $this->options['param_name'], 0, -1 );
1475
        }
1476
 
1477
        protected function get_file_name_param(): string
1478
        {
1479
            $name = $this->get_singular_param_name();
1480
            return $this->basename( stripslashes( $this->get_query_param( $name ) ) );
1481
        }
1482
 
1483
        protected function get_file_names_params()
1484
        {
1485
            $params = $this->get_query_param( $this->options['param_name'] );
1486
            if ( !$params )
1487
            {
1488
                return null;
1489
            }
1490
            foreach ( $params as $key => $value )
1491
            {
1492
                $params[$key] = $this->basename( stripslashes( $value ) );
1493
            }
1494
            return $params;
1495
        }
1496
 
1497
        protected function get_file_type( $file_path ): string
1498
        {
1499
            return match ( strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) ) )
1500
            {
1501
                'jpeg', 'jpg' => self::IMAGETYPE_JPEG,
1502
                'png'         => self::IMAGETYPE_PNG,
1503
                'gif'         => self::IMAGETYPE_GIF,
1504
                default       => '',
1505
            };
1506
        }
1507
 
1508
        protected function download()
1509
        {
1510
            switch ( $this->options['download_via_php'] )
1511
            {
1512
                case 1:
1513
                    $redirect_header = null;
1514
                    break;
1515
                case 2:
1516
                    $redirect_header = 'X-Sendfile';
1517
                    break;
1518
                case 3:
1519
                    $redirect_header = 'X-Accel-Redirect';
1520
                    break;
1521
                default:
1522
                    return $this->header( 'HTTP/1.1 403 Forbidden' );
1523
            }
1524
            $file_name = $this->get_file_name_param();
1525
            if ( !$this->is_valid_file_object( $file_name ) )
1526
            {
1527
                return $this->header( 'HTTP/1.1 404 Not Found' );
1528
            }
1529
            if ( $redirect_header )
1530
            {
1531
                return $this->header(
1532
                    $redirect_header . ': ' . $this->get_download_url(
1533
                        $file_name,
1534
                        $this->get_version_param(),
1535
                        true
1536
                    )
1537
                );
1538
            }
1539
            $file_path = $this->get_upload_path( $file_name, $this->get_version_param() );
1540
            // Prevent browsers from MIME-sniffing the content-type:
1541
            $this->header( 'X-Content-Type-Options: nosniff' );
1542
            if ( !preg_match( $this->options['inline_file_types'], $file_name ) )
1543
            {
1544
                $this->header( 'Content-Type: application/octet-stream' );
1545
                $this->header( 'Content-Disposition: attachment; filename="' . $file_name . '"' );
1546
            }
1547
            else
1548
            {
1549
                $this->header( 'Content-Type: ' . $this->get_file_type( $file_path ) );
1550
                $this->header( 'Content-Disposition: inline; filename="' . $file_name . '"' );
1551
            }
1552
            $this->header( 'Content-Length: ' . $this->get_file_size( $file_path ) );
1553
            $this->header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s T', filemtime( $file_path ) ) );
1554
            $this->readfile( $file_path );
1555
        }
1556
 
1557
        protected function send_content_type_header(): void
1558
        {
1559
            $this->header( 'Vary: Accept' );
1560
            if ( str_contains( $this->get_server_var( 'HTTP_ACCEPT' ), 'application/json' ) )
1561
            {
1562
                $this->header( 'Content-type: application/json' );
1563
            }
1564
            else
1565
            {
1566
                $this->header( 'Content-type: text/plain' );
1567
            }
1568
        }
1569
 
1570
        protected function send_access_control_headers(): void
1571
        {
1572
            $this->header( 'Access-Control-Allow-Origin: ' . $this->options['access_control_allow_origin'] );
1573
            $this->header( 'Access-Control-Allow-Credentials: '
1574
                . ( $this->options['access_control_allow_credentials'] ? 'true' : 'false' ) );
1575
            $this->header( 'Access-Control-Allow-Methods: '
1576
                . implode( ', ', $this->options['access_control_allow_methods'] ) );
1577
            $this->header( 'Access-Control-Allow-Headers: '
1578
                . implode( ', ', $this->options['access_control_allow_headers'] ) );
1579
        }
1580
 
1581
        public function generate_response( $content, $print_response = true )
1582
        {
1583
            $this->response = $content;
1584
            if ( $print_response )
1585
            {
1586
                $json = json_encode( $content );
1587
                $redirect = stripslashes( $this->get_post_param( 'redirect' ) );
1588
                if ( $redirect && preg_match( $this->options['redirect_allow_target'], $redirect ) )
1589
                {
1590
                    return $this->header( 'Location: ' . sprintf( $redirect, rawurlencode( $json ) ) );
1591
                }
1592
                $this->head();
1593
                if ( $this->get_server_var( 'HTTP_CONTENT_RANGE' ) )
1594
                {
1595
                    $files = $content[$this->options['param_name']] ?? null;
1596
                    if ( $files && is_array( $files ) && is_object( $files[0] ) && $files[0]->size )
1597
                    {
1598
                        $this->header( 'Range: 0-' . (
1599
                                $this->fix_integer_overflow( (int)$files[0]->size ) - 1
1600
                            ) );
1601
                    }
1602
                }
1603
                $this->body( $json );
1604
            }
1605
            return $content;
1606
        }
1607
 
1608
        public function get_response()
1609
        {
1610
            return $this->response;
1611
        }
1612
 
1613
        public function head(): void
1614
        {
1615
            $this->header( 'Pragma: no-cache' );
1616
            $this->header( 'Cache-Control: no-store, no-cache, must-revalidate' );
1617
            $this->header( 'Content-Disposition: inline; filename="files.json"' );
1618
            // Prevent Internet Explorer from MIME-sniffing the content-type:
1619
            $this->header( 'X-Content-Type-Options: nosniff' );
1620
            if ( $this->options['access_control_allow_origin'] )
1621
            {
1622
                $this->send_access_control_headers();
1623
            }
1624
            $this->send_content_type_header();
1625
        }
1626
 
1627
        public function get( $print_response = true )
1628
        {
1629
            if ( $print_response && $this->get_query_param( 'download' ) )
1630
            {
1631
                return $this->download();
1632
            }
1633
            $file_name = $this->get_file_name_param();
1634
            if ( $file_name )
1635
            {
1636
                $response = array(
1637
                    $this->get_singular_param_name() => $this->get_file_object( $file_name )
1638
                );
1639
            }
1640
            else
1641
            {
1642
                $response = array(
1643
                    $this->options['param_name'] => $this->get_file_objects()
1644
                );
1645
            }
1646
            return $this->generate_response( $response, $print_response );
1647
        }
1648
 
1649
        public function post( $print_response = true )
1650
        {
1651
            if ( $this->get_query_param( '_method' ) === 'DELETE' )
1652
            {
1653
                return $this->delete( $print_response );
1654
            }
1655
            $upload = $this->get_upload_data( $this->options['param_name'] );
1656
            // Parse the Content-Disposition header, if available:
1657
            $content_disposition_header = $this->get_server_var( 'HTTP_CONTENT_DISPOSITION' );
1658
            $file_name = $content_disposition_header ?
1659
                rawurldecode( preg_replace(
1660
                    '/(^[^"]+")|("$)/',
1661
                    '',
1662
                    $content_disposition_header
1663
                ) ) : null;
1664
            // Parse the Content-Range header, which has the following form:
1665
            // Content-Range: bytes 0-524287/2000000
1666
            $content_range_header = $this->get_server_var( 'HTTP_CONTENT_RANGE' );
1667
            $content_range = $content_range_header ?
1668
                preg_split( '/[^0-9]+/', $content_range_header ) : null;
1669
            $size = @$content_range[3];
1670
            $files = array();
1671
            if ( $upload )
1672
            {
1673
                if ( is_array( $upload['tmp_name'] ) )
1674
                {
1675
                    // param_name is an array identifier like "files[]",
1676
                    // $upload is a multi-dimensional array:
1677
                    foreach ( $upload['tmp_name'] as $index => $value )
1678
                    {
1679
                        $files[] = $this->handle_file_upload(
1680
                            $upload['tmp_name'][$index],
1681
                            $file_name ? $file_name : $upload['name'][$index],
1682
                            $size ? $size : $upload['size'][$index],
1683
                            $upload['type'][$index],
1684
                            $upload['error'][$index],
1685
                            $index,
1686
                            $content_range
1687
                        );
1688
                    }
1689
                }
1690
                else
1691
                {
1692
                    // param_name is a single object identifier like "file",
1693
                    // $upload is a one-dimensional array:
1694
                    $files[] = $this->handle_file_upload(
1695
                        $upload['tmp_name'] ?? null,
1696
                        $file_name ? $file_name : ( $upload['name'] ?? null ),
1697
                        $size ? $size : ( $upload['size'] ?? $this->get_server_var( 'CONTENT_LENGTH' ) ),
1698
                        $upload['type'] ?? $this->get_server_var( 'CONTENT_TYPE' ),
1699
                        $upload['error'] ?? null,
1700
                        null,
1701
                        $content_range
1702
                    );
1703
                }
1704
            }
1705
            $response = array( $this->options['param_name'] => $files );
1706
            return $this->generate_response( $response, $print_response );
1707
        }
1708
 
1709
        public function delete( $print_response = true )
1710
        {
1711
            $file_names = $this->get_file_names_params();
1712
            if ( empty( $file_names ) )
1713
            {
1714
                $file_names = array( $this->get_file_name_param() );
1715
            }
1716
            $response = array();
1717
            foreach ( $file_names as $file_name )
1718
            {
1719
                $file_path = $this->get_upload_path( $file_name );
1720
                $success = strlen( $file_name ) > 0 && $file_name[0] !== '.' && is_file( $file_path ) && unlink( $file_path );
1721
                if ( $success )
1722
                {
1723
                    foreach ( $this->options['image_versions'] as $version => $options )
1724
                    {
1725
                        if ( !empty( $version ) )
1726
                        {
1727
                            $file = $this->get_upload_path( $file_name, $version );
1728
                            if ( is_file( $file ) )
1729
                            {
1730
                                unlink( $file );
1731
                            }
1732
                        }
1733
                    }
1734
                }
1735
                $response[$file_name] = $success;
1736
            }
1737
            return $this->generate_response( $response, $print_response );
1738
        }
1739
 
1740
        protected function basename( $filepath, $suffix = null ): string
1741
        {
1742
            $splited = preg_split( '/\//', rtrim( $filepath, '/ ' ) );
1743
            return substr( basename( 'X' . $splited[count( $splited ) - 1], $suffix ), 1 );
1744
        }
1745
    }