Completed
Push — master ( fd0c8a...c024a6 )
by
unknown
03:33 queued 01:53
created

UploadHandler::get_file_object()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
nc 5
nop 1
dl 0
loc 19
rs 9.3222
c 0
b 0
f 0
1
<?php namespace Xoopsmodules\smallworld;
2
/*
3
 * jQuery File Upload Plugin PHP Class 6.1.2
4
 * https://github.com/blueimp/jQuery-File-Upload
5
 *
6
 * Copyright 2010, Sebastian Tschan
7
 * https://blueimp.net
8
 *
9
 * Licensed under the MIT license:
10
 * http://www.opensource.org/licenses/MIT
11
 */
12
13
/**
14
 * Class UploadHandler
15
 */
16
class UploadHandler
17
{
18
    public $xoopsUser;
19
    protected $options;
20
21
    // PHP File Upload error message codes:
22
    // http://php.net/manual/en/features.file-upload.errors.php
23
    protected $error_messages = [
24
        1                     => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
25
        2                     => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
26
        3                     => 'The uploaded file was only partially uploaded',
27
        4                     => 'No file was uploaded',
28
        6                     => 'Missing a temporary folder',
29
        7                     => 'Failed to write file to disk',
30
        8                     => 'A PHP extension stopped the file upload',
31
        'post_max_size'       => 'The uploaded file exceeds the post_max_size directive in php.ini',
32
        'max_file_size'       => 'File is too big',
33
        'min_file_size'       => 'File is too small',
34
        'accept_file_types'   => 'Filetype not allowed',
35
        'max_number_of_files' => 'Maximum number of files exceeded',
36
        'max_width'           => 'Image exceeds maximum width',
37
        'min_width'           => 'Image requires a minimum width',
38
        'max_height'          => 'Image exceeds maximum height',
39
        'min_height'          => 'Image requires a minimum height'
40
    ];
41
42
    /**
43
     * UploadHandler constructor.
44
     * @param null $options
45
     * @param bool $initialize
46
     */
47
    public function __construct($options = null, $initialize = true)
48
    {
49
        global $xoopsUser;
50
        $userID        = $xoopsUser->getVar('uid');
51
        $this->options = [
52
            'script_url'                       => $this->get_full_url() . '/imgupload.php',
53
            'upload_dir'                       => XOOPS_ROOT_PATH . '/uploads/albums_smallworld/' . $userID . '/',
54
            'upload_url'                       => XOOPS_URL . '/uploads/albums_smallworld/' . $userID . '/',
55
            'user_dirs'                        => false,
56
            'mkdir_mode'                       => 0755,
57
            'param_name'                       => 'files',
58
            // Set the following option to 'POST', if your server does not support
59
            // DELETE requests. This is a parameter sent to the client:
60
            'delete_type'                      => 'DELETE',
61
            'access_control_allow_origin'      => '*',
62
            'access_control_allow_credentials' => false,
63
            'access_control_allow_methods'     => [
64
                'OPTIONS',
65
                'HEAD',
66
                'GET',
67
                'POST',
68
                'PUT',
69
                'PATCH',
70
                'DELETE'
71
            ],
72
            'access_control_allow_headers'     => [
73
                'Content-Type',
74
                'Content-Range',
75
                'Content-Disposition'
76
            ],
77
            // Enable to provide file downloads via GET requests to the PHP script:
78
            'download_via_php'                 => false,
79
            // Defines which files can be displayed inline when downloaded:
80
            'inline_file_types'                => '/\.(gif|jpe?g|png|JPE?G)$/i',
81
            // Defines which files (based on their names) are accepted for upload:
82
            'accept_file_types'                => '/.+$/i',
83
            // The php.ini settings upload_max_filesize and post_max_size
84
            // take precedence over the following max_file_size setting:
85
            'max_file_size'                    => null,
86
            'min_file_size'                    => 1,
87
            // The maximum number of files for the upload directory:
88
            'max_number_of_files'              => null,
89
            // Image resolution restrictions:
90
            'max_width'                        => null,
91
            'max_height'                       => null,
92
            'min_width'                        => 1,
93
            'min_height'                       => 1,
94
            // Set the following option to false to enable resumable uploads:
95
            'discard_aborted_uploads'          => true,
96
            // Set to true to rotate images based on EXIF meta data, if available:
97
            'orient_image'                     => false,
98
            'image_versions'                   => [
99
                // Uncomment the following version to restrict the size of
100
                // uploaded images:
101
                /*
102
                '' => array(
103
                    'max_width' => 1920,
104
                    'max_height' => 1200,
105
                    'jpeg_quality' => 95
106
                ),
107
                */
108
                // Uncomment the following to create medium sized images:
109
                /*
110
                'medium' => array(
111
                    'max_width' => 800,
112
                    'max_height' => 600,
113
                    'jpeg_quality' => 80
114
                ),
115
                */
116
                'thumbnails' => [
117
                    'upload_dir' => XOOPS_ROOT_PATH . '/uploads/albums_smallworld' . '/' . $userID . '/thumbnails/',
118
                    'upload_url' => XOOPS_URL . '/uploads/albums_smallworld' . '/' . $userID . '/thumbnails/',
119
                    'max_width'  => 80,
120
                    'max_height' => 80
121
                ]
122
            ]
123
        ];
124
        if ($options) {
125
            $this->options = array_merge($this->options, $options);
126
        }
127
        if ($initialize) {
128
            $this->initialize();
129
        }
130
    }
131
132
    protected function initialize()
133
    {
134
        switch ($_SERVER['REQUEST_METHOD']) {
135
            case 'OPTIONS':
136
            case 'HEAD':
137
                $this->head();
138
                break;
139
            case 'GET':
140
                $this->get();
141
                break;
142
            case 'PATCH':
143
            case 'PUT':
144
            case 'POST':
145
                $this->post();
146
                break;
147
            case 'DELETE':
148
                $this->delete();
149
                break;
150
            default:
151
                $this->header('HTTP/1.1 405 Method Not Allowed');
152
        }
153
    }
154
155
    /**
156
     * @return string
157
     */
158
    protected function get_full_url()
159
    {
160
        $https = !empty($_SERVER['HTTPS']) && 'off' !== $_SERVER['HTTPS'];
161
        return ($https ? 'https://' : 'http://')
162
               . (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] . '@' : '')
163
               . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'] . ($https && 443 === $_SERVER['SERVER_PORT'] || 80 === $_SERVER['SERVER_PORT'] ? '' : ':' . $_SERVER['SERVER_PORT'])))
164
               . substr($_SERVER['SCRIPT_NAME'], 0, strrpos($_SERVER['SCRIPT_NAME'], '/'));
165
    }
166
167
    /**
168
     * @return string
169
     */
170
    protected function get_user_id()
171
    {
172
        @session_start();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
173
        return session_id();
174
    }
175
176
    /**
177
     * @return string
178
     */
179
    protected function get_user_path()
180
    {
181
        if ($this->options['user_dirs']) {
182
            return $this->get_user_id() . '/';
183
        }
184
        return '';
185
    }
186
187
    /**
188
     * @param null|string $file_name
189
     * @param null $version
190
     * @return string
191
     */
192
    protected function get_upload_path($file_name = null, $version = null)
193
    {
194
        $file_name    = $file_name ?: '';
195
        $version_path = empty($version) ? '' : $version . '/';
196
        return $this->options['upload_dir'] . $this->get_user_path() . $version_path . $file_name;
197
    }
198
199
    /**
200
     * @param $url
201
     * @return string
202
     */
203
    protected function get_query_separator($url)
204
    {
205
        return false === strpos($url, '?') ? '?' : '&';
206
    }
207
208
    /**
209
     * @param      $file_name
210
     * @param null $version
211
     * @return string
212
     */
213
    protected function get_download_url($file_name, $version = null)
214
    {
215
        if ($this->options['download_via_php']) {
216
            $url = $this->options['script_url'] . $this->get_query_separator($this->options['script_url']) . 'file=' . rawurlencode($file_name);
217
            if ($version) {
218
                $url .= '&version=' . rawurlencode($version);
219
            }
220
            return $url . '&download=1';
221
        }
222
        $version_path = empty($version) ? '' : rawurlencode($version) . '/';
223
        return $this->options['upload_url'] . $this->get_user_path() . $version_path . rawurlencode($file_name);
224
    }
225
226
    /**
227
     * @param $file
228
     */
229
    protected function set_file_delete_properties($file)
230
    {
231
        $file->delete_url  = $this->options['script_url'] . $this->get_query_separator($this->options['script_url']) . 'file=' . rawurlencode($file->name);
232
        $file->delete_type = $this->options['delete_type'];
233
        if ('DELETE' !== $file->delete_type) {
234
            $file->delete_url .= '&_method=DELETE';
235
        }
236
        if ($this->options['access_control_allow_credentials']) {
237
            $file->delete_with_credentials = true;
238
        }
239
    }
240
241
    // Fix for overflowing signed 32 bit integers,
242
    // works for sizes up to 2^32-1 bytes (4 GiB - 1):
243
    /**
244
     * @param $size
245
     * @return float|int
246
     */
247
    protected function fix_integer_overflow($size)
248
    {
249
        if ($size < 0) {
250
            $size += 2.0 * (PHP_INT_MAX + 1);
251
        }
252
        return $size;
253
    }
254
255
    /**
256
     * @param      $file_path
257
     * @param bool $clear_stat_cache
258
     * @return float|int
259
     */
260
    protected function get_file_size($file_path, $clear_stat_cache = false)
261
    {
262
        if ($clear_stat_cache) {
263
            clearstatcache(true, $file_path);
264
        }
265
        return $this->fix_integer_overflow(filesize($file_path));
266
    }
267
268
    /**
269
     * @param $file_name
270
     * @return bool
271
     */
272
    protected function is_valid_file_object($file_name)
273
    {
274
        $file_path = $this->get_upload_path($file_name);
275
        return is_file($file_path) && '.' !== $file_name[0];
276
    }
277
278
    /**
279
     * @param $file_name
280
     * @return null|\stdClass
281
     */
282
    protected function get_file_object($file_name)
283
    {
284
        if ($this->is_valid_file_object($file_name)) {
285
            $file       = new \stdClass();
286
            $file->name = $file_name;
287
            $file->size = $this->get_file_size($this->get_upload_path($file_name));
288
            $file->url  = $this->get_download_url($file->name);
289
            foreach ($this->options['image_versions'] as $version => $options) {
0 ignored issues
show
Bug introduced by
The expression $this->options['image_versions'] of type string|integer|array<int...ight\":\"integer\"}>"}> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
290
                if (!empty($version)) {
291
                    if (is_file($this->get_upload_path($file_name, $version))) {
292
                        $file->{$version . '_url'} = $this->get_download_url($file->name, $version);
293
                    }
294
                }
295
            }
296
            $this->set_file_delete_properties($file);
297
            return $file;
298
        }
299
        return null;
300
    }
301
302
    /**
303
     * @param string $iteration_method
304
     * @return array
305
     */
306
    protected function get_file_objects($iteration_method = 'get_file_object')
307
    {
308
        $upload_dir = $this->get_upload_path();
309
        if (!is_dir($upload_dir)) {
310
            return [];
311
        }
312
        return array_values(array_filter(array_map([$this, $iteration_method], scandir($upload_dir, SCANDIR_SORT_NONE))));
313
    }
314
315
    /**
316
     * @return int
317
     */
318
    protected function count_file_objects()
319
    {
320
        return count($this->get_file_objects('is_valid_file_object'));
321
    }
322
323
    /**
324
     * @param $file_name
325
     * @param $version
326
     * @param $options
327
     * @return bool
328
     */
329
    protected function create_scaled_image($file_name, $version, $options)
330
    {
331
        $file_path = $this->get_upload_path($file_name);
332
        if (!empty($version)) {
333
            $version_dir = $this->get_upload_path(null, $version);
334
            if (!is_dir($version_dir)) {
335
                mkdir($version_dir, $this->options['mkdir_mode'], true);
336
            }
337
            $new_file_path = $version_dir . '/' . $file_name;
338
        } else {
339
            $new_file_path = $file_path;
340
        }
341
        list($img_width, $img_height) = @getimagesize($file_path);
342
        if (!$img_width || !$img_height) {
343
            return false;
344
        }
345
        $scale = min($options['max_width'] / $img_width, $options['max_height'] / $img_height);
346
        if ($scale >= 1) {
347
            if ($file_path !== $new_file_path) {
348
                return copy($file_path, $new_file_path);
349
            }
350
            return true;
351
        }
352
        $new_width  = $img_width * $scale;
353
        $new_height = $img_height * $scale;
354
        $new_img    = @imagecreatetruecolor($new_width, $new_height);
355
        switch (strtolower(substr(strrchr($file_name, '.'), 1))) {
356
            case 'jpg':
357
            case 'jpeg':
358
                $src_img       = @imagecreatefromjpeg($file_path);
359
                $write_image   = 'imagejpeg';
360
                $image_quality = isset($options['jpeg_quality']) ? $options['jpeg_quality'] : 75;
361
                break;
362
            case 'gif':
363
                @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
364
                $src_img       = @imagecreatefromgif($file_path);
365
                $write_image   = 'imagegif';
366
                $image_quality = null;
367
                break;
368
            case 'png':
369
                @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0));
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
370
                @imagealphablending($new_img, false);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
371
                @imagesavealpha($new_img, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
372
                $src_img       = @imagecreatefrompng($file_path);
373
                $write_image   = 'imagepng';
374
                $image_quality = isset($options['png_quality']) ? $options['png_quality'] : 9;
375
                break;
376
            default:
377
                $src_img = null;
378
        }
379
        $success = $src_img && @imagecopyresampled($new_img, $src_img, 0, 0, 0, 0, $new_width, $new_height, $img_width, $img_height) && $write_image($new_img, $new_file_path, $image_quality);
0 ignored issues
show
Bug introduced by
The variable $write_image does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $image_quality does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
380
        // Free up memory (imagedestroy does not delete files):
381
        @imagedestroy($src_img);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
382
        @imagedestroy($new_img);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
383
        return $success;
384
    }
385
386
    /**
387
     * @param $error
388
     * @return mixed
389
     */
390
    protected function get_error_message($error)
391
    {
392
        return array_key_exists($error, $this->error_messages) ? $this->error_messages[$error] : $error;
393
    }
394
395
    /**
396
     * @param $val
397
     * @return float|int
398
     */
399
    public function get_config_bytes($val)
400
    {
401
        $val  = trim($val);
402
        $last = strtolower($val[strlen($val) - 1]);
403
        switch ($last) {
404
            case 'g':
405
                $val *= 1024;
406
                // no break
407
            case 'm':
408
                $val *= 1024;
409
                // no break
410
            case 'k':
411
                $val *= 1024;
412
        }
413
        return $this->fix_integer_overflow($val);
414
    }
415
416
    /**
417
     * @param $uploaded_file
418
     * @param $file
419
     * @param $error
420
     * @param $index
421
     * @return bool
422
     */
423
    protected function validate($uploaded_file, $file, $error, $index)
0 ignored issues
show
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
424
    {
425
        if ($error) {
426
            $file->error = $this->get_error_message($error);
427
            return false;
428
        }
429
        $content_length = $this->fix_integer_overflow((int)$_SERVER['CONTENT_LENGTH']);
430
        $post_max_size  = $this->get_config_bytes(ini_get('post_max_size'));
431
        if ($post_max_size && ($content_length > $post_max_size)) {
432
            $file->error = $this->get_error_message('post_max_size');
433
            return false;
434
        }
435
        if (!preg_match($this->options['accept_file_types'], $file->name)) {
436
            $file->error = $this->get_error_message('accept_file_types');
437
            return false;
438
        }
439
        if ($uploaded_file && is_uploaded_file($uploaded_file)) {
440
            $file_size = $this->get_file_size($uploaded_file);
441
        } else {
442
            $file_size = $content_length;
443
        }
444
        if ($this->options['max_file_size'] && ($file_size > $this->options['max_file_size'] || $file->size > $this->options['max_file_size'])) {
445
            $file->error = $this->get_error_message('max_file_size');
446
            return false;
447
        }
448
        if ($this->options['min_file_size'] && $file_size < $this->options['min_file_size']) {
449
            $file->error = $this->get_error_message('min_file_size');
450
            return false;
451
        }
452
        if (is_int($this->options['max_number_of_files']) && ($this->count_file_objects() >= $this->options['max_number_of_files'])) {
453
            $file->error = $this->get_error_message('max_number_of_files');
454
            return false;
455
        }
456
        list($img_width, $img_height) = @getimagesize($uploaded_file);
457
        if (is_int($img_width)) {
458 View Code Duplication
            if ($this->options['max_width'] && $img_width > $this->options['max_width']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
459
                $file->error = $this->get_error_message('max_width');
460
                return false;
461
            }
462 View Code Duplication
            if ($this->options['max_height'] && $img_height > $this->options['max_height']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
463
                $file->error = $this->get_error_message('max_height');
464
                return false;
465
            }
466 View Code Duplication
            if ($this->options['min_width'] && $img_width < $this->options['min_width']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
467
                $file->error = $this->get_error_message('min_width');
468
                return false;
469
            }
470 View Code Duplication
            if ($this->options['min_height'] && $img_height < $this->options['min_height']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
471
                $file->error = $this->get_error_message('min_height');
472
                return false;
473
            }
474
        }
475
        return true;
476
    }
477
478
    /**
479
     * @param $matches
480
     * @return string
481
     */
482
    protected function upcount_name_callback($matches)
483
    {
484
        $index = isset($matches[1]) ? (int)$matches[1] + 1 : 1;
485
        $ext   = isset($matches[2]) ? $matches[2] : '';
486
        return ' (' . $index . ')' . $ext;
487
    }
488
489
    /**
490
     * @param $name
491
     * @return null|string|string[]
492
     */
493
    protected function upcount_name($name)
494
    {
495
        return preg_replace_callback('/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/', [$this, 'upcount_name_callback'], $name, 1);
496
    }
497
498
    /**
499
     * @param $name
500
     * @param $type
501
     * @param $index
502
     * @param $content_range
503
     * @return null|string|string[]
504
     */
505
    protected function get_unique_filename($name, $type, $index, $content_range)
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
506
    {
507
        while (is_dir($this->get_upload_path($name))) {
508
            $name = $this->upcount_name($name);
509
        }
510
        // Keep an existing filename if this is part of a chunked upload:
511
        $uploaded_bytes = $this->fix_integer_overflow((int)$content_range[1]);
512
        while (is_file($this->get_upload_path($name))) {
513
            if ($uploaded_bytes === $this->get_file_size($this->get_upload_path($name))) {
514
                break;
515
            }
516
            $name = $this->upcount_name($name);
517
        }
518
        return $name;
519
    }
520
521
    /**
522
     * @param $name
523
     * @param $type
524
     * @param $index
525
     * @param $content_range
526
     * @return mixed|string
527
     */
528
    protected function trim_file_name($name, $type, $index, $content_range)
0 ignored issues
show
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $content_range is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
529
    {
530
        // Remove path information and dots around the filename, to prevent uploading
531
        // into different directories or replacing hidden system files.
532
        // Also remove control characters and spaces (\x00..\x20) around the filename:
533
        $name = trim(basename(stripslashes($name)), ".\x00..\x20");
534
        // Use a timestamp for empty filenames:
535
        if (!$name) {
536
            $name = str_replace('.', '-', microtime(true));
537
        }
538
        // Add missing file extension for known image types:
539
        if (false === strpos($name, '.') && preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches)) {
540
            $name .= '.' . $matches[1];
541
        }
542
        return $name;
543
    }
544
545
    /**
546
     * @param $name
547
     * @param $type
548
     * @param $index
549
     * @param $content_range
550
     * @return null|string|string[]
551
     */
552
    protected function get_file_name($name, $type, $index, $content_range)
553
    {
554
        return $this->get_unique_filename($this->trim_file_name($name, $type, $index, $content_range), $type, $index, $content_range);
555
    }
556
557
    /**
558
     * @param $file
559
     * @param $index
560
     */
561
    protected function handle_form_data($file, $index)
0 ignored issues
show
Unused Code introduced by
The parameter $file is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $index is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
562
    {
563
        // Handle form data, e.g. $_REQUEST['description'][$index]
564
    }
565
566
    /**
567
     * @param $file_path
568
     * @return bool
569
     */
570
    protected function orient_image($file_path)
571
    {
572
        if (!function_exists('exif_read_data')) {
573
            return false;
574
        }
575
        $exif = @exif_read_data($file_path);
576
        if (false === $exif) {
577
            return false;
578
        }
579
        $orientation = (int)(@$exif['Orientation']);
580
        if (!in_array($orientation, [3, 6, 8])) {
581
            return false;
582
        }
583
        $image = @imagecreatefromjpeg($file_path);
584
        switch ($orientation) {
585
            case 3:
586
                $image = @imagerotate($image, 180, 0);
587
                break;
588
            case 6:
589
                $image = @imagerotate($image, 270, 0);
590
                break;
591
            case 8:
592
                $image = @imagerotate($image, 90, 0);
593
                break;
594
            default:
595
                return false;
596
        }
597
        $success = imagejpeg($image, $file_path);
598
        // Free up memory (imagedestroy does not delete files):
599
        @imagedestroy($image);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
600
        return $success;
601
    }
602
603
    /**
604
     * @param      $uploaded_file
605
     * @param      $name
606
     * @param      $size
607
     * @param      $type
608
     * @param      $error
609
     * @param null $index
610
     * @param null $content_range
611
     * @return \Xoopsmodules\smallworld|\stdClass
612
     */
613
    protected function handle_file_upload(
614
        $uploaded_file,
615
        $name,
616
        $size,
617
        $type,
618
        $error,
619
        $index = null,
620
        $content_range = null
621
    ) {
622
        global $xoopsUser, $SmallWorldDB,$xoopsLogger;
623
		$xoopsLogger->activated = false;
624
        $file = new \stdClass();
625
626
        $file->name = $this->get_file_name($name, $type, $index, $content_range);
627
        $file->size = $this->fix_integer_overflow((int)$size);
628
        $file->type = $type;
629
630
        // Save to database for later use
631
        $db     = new SmallWorldDB;
632
        $userid = $xoopsUser->getVar('uid');
633
634
        // Generate new name for file
635
        $file->name = basename(stripslashes($name));
636
        $file->name = time() . mt_rand(0, 99999) . '.' . $this->getFileExtension($name);
637
        $img        = XOOPS_URL . '/uploads/albums_smallworld/' . $userid . '/' . $file->name;
638
        $db->saveImage("null, '" . $userid . "', '" . $file->name . "', '" . addslashes($img) . "', '" . time() . "', null");
639
640
        if ($this->validate($uploaded_file, $file, $error, $index)) {
641
            $this->handle_form_data($file, $index);
642
            $upload_dir = $this->get_upload_path();
643
            if (!is_dir($upload_dir)) {
644
                mkdir($upload_dir, $this->options['mkdir_mode'], true);
645
            }
646
            $file_path   = $this->get_upload_path($file->name);
647
            $append_file = $content_range && is_file($file_path) && $file->size > $this->get_file_size($file_path);
648
            if ($uploaded_file && is_uploaded_file($uploaded_file)) {
649
                // multipart/formdata uploads (POST method uploads)
650
                if ($append_file) {
651
                    file_put_contents($file_path, fopen($uploaded_file, 'r'), FILE_APPEND);
652
                } else {
653
                    move_uploaded_file($uploaded_file, $file_path);
654
                }
655
            } else {
656
                // Non-multipart uploads (PUT method support)
657
                file_put_contents($file_path, fopen('php://input', 'r'), $append_file ? FILE_APPEND : 0);
658
            }
659
            $file_size = $this->get_file_size($file_path, $append_file);
660
            if ($file_size === $file->size) {
661
                if ($this->options['orient_image']) {
662
                    $this->orient_image($file_path);
663
                }
664
                $file->url = $this->get_download_url($file->name);
665
                foreach ($this->options['image_versions'] as $version => $options) {
0 ignored issues
show
Bug introduced by
The expression $this->options['image_versions'] of type string|integer|array<int...ight\":\"integer\"}>"}> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
666
                    if ($this->create_scaled_image($file->name, $version, $options)) {
667
                        if (!empty($version)) {
668
                            $file->{$version . '_url'} = $this->get_download_url($file->name, $version);
669
                        } else {
670
                            $file_size = $this->get_file_size($file_path, true);
671
                        }
672
                    }
673
                }
674
            } elseif (!$content_range && $this->options['discard_aborted_uploads']) {
675
                unlink($file_path);
676
                $file->error = 'abort';
677
            }
678
            $file->size = $file_size;
679
            $this->set_file_delete_properties($file);
680
        }
681
        return $file;
682
    }
683
684
    //function to return file extension from a path or file name
685
686
    /**
687
     * @param $path
688
     * @return mixed
689
     */
690
    public function getFileExtension($path)
691
    {
692
        $parts = pathinfo($path);
693
        return $parts['extension'];
694
    }
695
696
    /**
697
     * @param $file_path
698
     * @return false|int
699
     */
700
    protected function readfile($file_path)
701
    {
702
        return readfile($file_path);
703
    }
704
705
    /**
706
     * @param $str
707
     */
708
    protected function body($str)
709
    {
710
        echo $str;
711
    }
712
713
    /**
714
     * @param $str
715
     */
716
    protected function header($str)
717
    {
718
        header($str);
719
    }
720
721
    /**
722
     * @param      $content
723
     * @param bool $print_response
724
     */
725
    protected function generate_response($content, $print_response = true)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
726
    {
727
        if ($print_response) {
728
            $json     = json_encode($content);
729
            $redirect = isset($_REQUEST['redirect']) ? stripslashes($_REQUEST['redirect']) : null;
730
            if ($redirect) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $redirect of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
731
                $this->header('Location: ' . sprintf($redirect, rawurlencode($json)));
732
                return;
733
            }
734
            $this->head();
735
            if (isset($_SERVER['HTTP_CONTENT_RANGE'])) {
736
                $files = isset($content[$this->options['param_name']]) ? $content[$this->options['param_name']] : null;
737
                if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) {
738
                    $this->header('Range: 0-' . ($this->fix_integer_overflow((int)$files[0]->size) - 1));
739
                }
740
            }
741
            $this->body($json);
742
        }
743
        return $content;
744
    }
745
746
    /**
747
     * @return null|string
748
     */
749
    protected function get_version_param()
750
    {
751
        return isset($_GET['version']) ? basename(stripslashes($_GET['version'])) : null;
752
    }
753
754
    /**
755
     * @return null|string
756
     */
757
    protected function get_file_name_param()
758
    {
759
        return isset($_GET['file']) ? basename(stripslashes($_GET['file'])) : null;
760
    }
761
762
    /**
763
     * @param $file_path
764
     * @return string
765
     */
766
    protected function get_file_type($file_path)
767
    {
768
        switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) {
769
            case 'jpeg':
770
            case 'jpg':
771
                return 'image/jpeg';
772
            case 'png':
773
                return 'image/png';
774
            case 'gif':
775
                return 'image/gif';
776
            default:
777
                return '';
778
        }
779
    }
780
781
    protected function download()
782
    {
783
        if (!$this->options['download_via_php']) {
784
            $this->header('HTTP/1.1 403 Forbidden');
785
            return;
786
        }
787
        $file_name = $this->get_file_name_param();
788
        if ($this->is_valid_file_object($file_name)) {
789
            $file_path = $this->get_upload_path($file_name, $this->get_version_param());
0 ignored issues
show
Bug introduced by
It seems like $this->get_version_param() targeting Xoopsmodules\smallworld\...er::get_version_param() can also be of type string; however, Xoopsmodules\smallworld\...dler::get_upload_path() does only seem to accept null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
790
            if (is_file($file_path)) {
791
                if (!preg_match($this->options['inline_file_types'], $file_name)) {
792
                    $this->header('Content-Description: File Transfer');
793
                    $this->header('Content-Type: application/octet-stream');
794
                    $this->header('Content-Disposition: attachment; filename="' . $file_name . '"');
795
                    $this->header('Content-Transfer-Encoding: binary');
796
                } else {
797
                    // Prevent Internet Explorer from MIME-sniffing the content-type:
798
                    $this->header('X-Content-Type-Options: nosniff');
799
                    $this->header('Content-Type: ' . $this->get_file_type($file_path));
800
                    $this->header('Content-Disposition: inline; filename="' . $file_name . '"');
801
                }
802
                $this->header('Content-Length: ' . $this->get_file_size($file_path));
803
                $this->header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file_path)));
804
                $this->readfile($file_path);
805
            }
806
        }
807
    }
808
809
    protected function send_content_type_header()
810
    {
811
        $this->header('Vary: Accept');
812
        if (isset($_SERVER['HTTP_ACCEPT']) && (false !== strpos($_SERVER['HTTP_ACCEPT'], 'application/json'))) {
813
            $this->header('Content-type: application/json');
814
        } else {
815
            $this->header('Content-type: text/plain');
816
        }
817
    }
818
819
    protected function send_access_control_headers()
820
    {
821
        $this->header('Access-Control-Allow-Origin: ' . $this->options['access_control_allow_origin']);
822
        $this->header('Access-Control-Allow-Credentials: ' . ($this->options['access_control_allow_credentials'] ? 'true' : 'false'));
823
        $this->header('Access-Control-Allow-Methods: ' . implode(', ', $this->options['access_control_allow_methods']));
824
        $this->header('Access-Control-Allow-Headers: ' . implode(', ', $this->options['access_control_allow_headers']));
825
    }
826
827
    public function head()
828
    {
829
        $this->header('Pragma: no-cache');
830
        $this->header('Cache-Control: no-store, no-cache, must-revalidate');
831
        $this->header('Content-Disposition: inline; filename="files.json"');
832
        // Prevent Internet Explorer from MIME-sniffing the content-type:
833
        $this->header('X-Content-Type-Options: nosniff');
834
        if ($this->options['access_control_allow_origin']) {
835
            $this->send_access_control_headers();
836
        }
837
        $this->send_content_type_header();
838
    }
839
840
    /**
841
     * @param bool $print_response
842
     */
843
    public function get($print_response = true)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
844
    {
845
        if ($print_response && isset($_GET['download'])) {
846
            return $this->download();
847
        }
848
        $file_name = $this->get_file_name_param();
849
        if (null !== $file_name) {
850
            $response = [
851
                substr($this->options['param_name'], 0, -1) => $this->get_file_object($file_name)
852
            ];
853
        } else {
854
            $response = [
855
                $this->options['param_name'] => $this->get_file_objects()
856
            ];
857
        }
858
        return $this->generate_response($response, $print_response);
859
    }
860
861
    /**
862
     * @param bool $print_response
863
     */
864
    public function post($print_response = true)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
865
    {
866
        if (isset($_REQUEST['_method']) && 'DELETE' === $_REQUEST['_method']) {
867
            return $this->delete($print_response);
868
        }
869
        $upload = isset($_FILES[$this->options['param_name']]) ? $_FILES[$this->options['param_name']] : null;
870
        // Parse the Content-Disposition header, if available:
871
        $file_name = isset($_SERVER['HTTP_CONTENT_DISPOSITION']) ? rawurldecode(preg_replace('/(^[^"]+")|("$)/', '', $_SERVER['HTTP_CONTENT_DISPOSITION'])) : null;
872
        // Parse the Content-Range header, which has the following form:
873
        // Content-Range: bytes 0-524287/2000000
874
        $content_range = isset($_SERVER['HTTP_CONTENT_RANGE']) ? preg_split('/[^0-9]+/', $_SERVER['HTTP_CONTENT_RANGE']) : null;
875
        $size          = $content_range ? $content_range[3] : null;
876
        $files         = [];
877
        if ($upload && is_array($upload['tmp_name'])) {
878
            // param_name is an array identifier like "files[]",
879
            // $_FILES is a multi-dimensional array:
880
            foreach ($upload['tmp_name'] as $index => $value) {
881
                $files[] = $this->handle_file_upload($upload['tmp_name'][$index], $file_name ?: $upload['name'][$index], $size ?: $upload['size'][$index], $upload['type'][$index], $upload['error'][$index], $index, $content_range);
0 ignored issues
show
Bug introduced by
It seems like $content_range defined by isset($_SERVER['HTTP_CON...CONTENT_RANGE']) : null on line 874 can also be of type array; however, Xoopsmodules\smallworld\...r::handle_file_upload() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
882
            }
883
        } else {
884
            // param_name is a single object identifier like "file",
885
            // $_FILES is a one-dimensional array:
886
            $files[] = $this->handle_file_upload(
887
                isset($upload['tmp_name']) ? $upload['tmp_name'] : null,
888
                $file_name ?: (isset($upload['name']) ? $upload['name'] : null),
889
                $size ?: (isset($upload['size']) ? $upload['size'] : $_SERVER['CONTENT_LENGTH']),
890
                                                 isset($upload['type']) ? $upload['type'] : $_SERVER['CONTENT_TYPE'],
891
                isset($upload['error']) ? $upload['error'] : null,
892
                null,
893
                $content_range
0 ignored issues
show
Bug introduced by
It seems like $content_range defined by isset($_SERVER['HTTP_CON...CONTENT_RANGE']) : null on line 874 can also be of type array; however, Xoopsmodules\smallworld\...r::handle_file_upload() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
894
            );
895
        }
896
        return $this->generate_response([$this->options['param_name'] => $files], $print_response);
897
    }
898
899
    /**
900
     * @param bool $print_response
901
     */
902
    public function delete($print_response = true)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
903
    {
904
        global $xoopsUser;
905
        $userid    = $xoopsUser->getVar('uid');
906
        $db        = new SmallWorldDB;
907
        $file_name = $this->get_file_name_param();
908
        $file_path = $this->get_upload_path($file_name);
909
        $success   = is_file($file_path) && '.' !== $file_name[0] && unlink($file_path);
910
911
        // Delete file based on user and filename
912
        $db->DeleteImage($userid, $file_name);
913
        $db->DeleteImage($userid, 'Thumbs.db');
914
915
        if ($success) {
916
            foreach ($this->options['image_versions'] as $version => $options) {
0 ignored issues
show
Bug introduced by
The expression $this->options['image_versions'] of type string|integer|array<int...ight\":\"integer\"}>"}> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
917
                if (!empty($version)) {
918
                    $file = $this->get_upload_path($file_name, $version);
919
                    if (is_file($file)) {
920
                        unlink($file);
921
                    }
922
                }
923
            }
924
        }
925
        return $this->generate_response(['success' => $success], $print_response);
926
    }
927
}
928