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

Revision

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