Subversion-Projekte lars-tiefland.webanos.faltradxxs.de

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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