Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 1549 | Zur aktuellen Revision | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

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