Test Failed
Push — master ( ce0b35...038e9c )
by Michael
12:03
created

SystemFineUploadHandler::toBytes()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 15
nc 8
nop 1
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Base SystemFineUploadHandler class to work with ajaxfineupload.php endpoint
5
 *
6
 * Upload files as specified
7
 *
8
 * Do not use or reference this directly from your client-side code.
9
 * Instead, this should be required via the endpoint.php or endpoint-cors.php
10
 * file(s).
11
 *
12
 * @license   MIT License (MIT)
13
 * @copyright Copyright (c) 2015-present, Widen Enterprises, Inc.
14
 * @link      https://github.com/FineUploader/php-traditional-server
15
 *
16
 * The MIT License (MIT)
17
 *
18
 * Copyright (c) 2015-present, Widen Enterprises, Inc.
19
 *
20
 * Permission is hereby granted, free of charge, to any person obtaining a copy
21
 * of this software and associated documentation files (the "Software"), to deal
22
 * in the Software without restriction, including without limitation the rights
23
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
24
 * copies of the Software, and to permit persons to whom the Software is
25
 * furnished to do so, subject to the following conditions:
26
 *
27
 * The above copyright notice and this permission notice shall be included in all
28
 * copies or substantial portions of the Software.
29
 *
30
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
31
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
36
 * SOFTWARE.
37
 */
38
39
class SystemFineUploadHandler
40
{
41
42
    public $allowedExtensions = array();
43
    public $allowedMimeTypes = array();
44
    public $sizeLimit = null;
45
    public $inputName = 'qqfile';
46
    public $chunksFolder = 'chunks';
47
48
    public $chunksCleanupProbability = 0.001; // Once in 1000 requests on avg
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $chunksCleanupProbability exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
49
    public $chunksExpireIn = 604800; // One week
50
51
    protected $uploadName;
52
    public $claims;
53
54
    /**
55
     * XoopsFineUploadHandler constructor.
56
     * @param stdClass $claims claims passed in JWT header
57
     */
58
    public function __construct(\stdClass $claims)
59
    {
60
        $this->claims = $claims;
61
    }
62
63
    /**
64
     * Get the original filename
65
     */
66
    public function getName()
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...
67
    {
68
        if (isset($_REQUEST['qqfilename'])) {
69
            return $_REQUEST['qqfilename'];
70
        }
71
72
        if (isset($_FILES[$this->inputName])) {
73
            return $_FILES[$this->inputName]['name'];
74
        }
75
    }
76
77
    /**
78
     * Get the name of the uploaded file
79
     * @return string
80
     */
81
    public function getUploadName()
82
    {
83
        return $this->uploadName;
84
    }
85
86
    /**
87
     * Combine chunks into a single file
88
     *
89
     * @param string      $uploadDirectory upload directory
90
     * @param string|null $name            name
91
     * @return array response to be json encoded and returned to client
92
     */
93
    public function combineChunks($uploadDirectory, $name = null)
94
    {
95
        $uuid = $_POST['qquuid'];
96
        if ($name === null) {
97
            $name = $this->getName();
98
        }
99
        $targetFolder = $this->chunksFolder.DIRECTORY_SEPARATOR.$uuid;
100
        $totalParts = isset($_REQUEST['qqtotalparts']) ? (int)$_REQUEST['qqtotalparts'] : 1;
101
102
        $targetPath = join(DIRECTORY_SEPARATOR, array($uploadDirectory, $uuid, $name));
103
        $this->uploadName = $name;
104
105
        if (!file_exists($targetPath)) {
106
            mkdir(dirname($targetPath), 0777, true);
107
        }
108
        $target = fopen($targetPath, 'wb');
109
110
        for ($i=0; $i<$totalParts; $i++) {
111
            $chunk = fopen($targetFolder.DIRECTORY_SEPARATOR.$i, "rb");
112
            stream_copy_to_stream($chunk, $target);
113
            fclose($chunk);
114
        }
115
116
        // Success
117
        fclose($target);
118
119
        for ($i=0; $i<$totalParts; $i++) {
120
            unlink($targetFolder.DIRECTORY_SEPARATOR.$i);
121
        }
122
123
        rmdir($targetFolder);
124
125
        if (!is_null($this->sizeLimit) && filesize($targetPath) > $this->sizeLimit) {
126
            unlink($targetPath);
127
            //http_response_code(413);
128
            header("HTTP/1.0 413 Request Entity Too Large");
129
            return array("success" => false, "uuid" => $uuid, "preventRetry" => true);
130
        }
131
132
        return array("success" => true, "uuid" => $uuid);
133
    }
134
135
    /**
136
     * Process the upload.
137
     * @param string $uploadDirectory Target directory.
138
     * @param string $name Overwrites the name of the file.
139
     * @return array response to be json encoded and returned to client
0 ignored issues
show
Documentation introduced by
Should the return type not be array<string,string>|arr...ing|boolean>|array|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
140
     */
141
    public function handleUpload($uploadDirectory, $name = null)
142
    {
143
        if (is_writable($this->chunksFolder) &&
144
            1 == mt_rand(1, 1/$this->chunksCleanupProbability)) {
145
            // Run garbage collection
146
            $this->cleanupChunks();
147
        }
148
149
        // Check that the max upload size specified in class configuration does not
150
        // exceed size allowed by server config
151
        if ($this->toBytes(ini_get('post_max_size')) < $this->sizeLimit ||
152
            $this->toBytes(ini_get('upload_max_filesize')) < $this->sizeLimit) {
153
            $neededRequestSize = max(1, $this->sizeLimit / 1024 / 1024) . 'M';
154
            return array(
155
                'error'=>"Server error. Increase post_max_size and upload_max_filesize to ".$neededRequestSize
156
            );
157
        }
158
159
        if ($this->isInaccessible($uploadDirectory)) {
160
            return array('error' => "Server error. Uploads directory isn't writable");
161
        }
162
163
        $type = $_SERVER['CONTENT_TYPE'];
164
        if (isset($_SERVER['HTTP_CONTENT_TYPE'])) {
165
            $type = $_SERVER['HTTP_CONTENT_TYPE'];
166
        }
167
168
        if (!isset($type)) {
169
            return array('error' => "No files were uploaded.");
170
        } elseif (strpos(strtolower($type), 'multipart/') !== 0) {
171
            return array(
172
                'error' => "Server error. Not a multipart request. Please set forceMultipart to default value (true)."
173
            );
174
        }
175
176
        // Get size and name
177
        $file = $_FILES[$this->inputName];
178
        $size = $file['size'];
179
        if (isset($_REQUEST['qqtotalfilesize'])) {
180
            $size = $_REQUEST['qqtotalfilesize'];
181
        }
182
183
        if ($name === null) {
184
            $name = $this->getName();
185
        }
186
187
        // check file error
188
        if ($file['error']) {
189
            return array('error' => 'Upload Error #'.$file['error']);
190
        }
191
192
        // Validate name
193
        if ($name === null || $name === '') {
194
            return array('error' => 'File name empty.');
195
        }
196
197
        // Validate file size
198
        if ($size == 0) {
199
            return array('error' => 'File is empty.');
200
        }
201
202
        if (!is_null($this->sizeLimit) && $size > $this->sizeLimit) {
203
            return array('error' => 'File is too large.', 'preventRetry' => true);
204
        }
205
206
        // Validate file extension
207
        $pathinfo = pathinfo($name);
208
        $ext = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : '';
209
210
        if ($this->allowedExtensions
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->allowedExtensions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
211
            && !in_array(strtolower($ext), array_map("strtolower", $this->allowedExtensions))) {
212
            $these = implode(', ', $this->allowedExtensions);
213
            return array(
214
                'error' => 'File has an invalid extension, it should be one of '. $these . '.',
215
                'preventRetry' => true
216
            );
217
        }
218
219
        $mimeType = '';
220
        if (!empty($this->allowedMimeTypes)) {
221
            $mimeType = mime_content_type($_FILES[$this->inputName]['tmp_name']);
222
            if (!in_array($mimeType, $this->allowedMimeTypes)) {
223
                return array('error' => 'File is of an invalid type.', 'preventRetry' => true);
224
            }
225
        }
226
227
        // Save a chunk
228
        $totalParts = isset($_REQUEST['qqtotalparts']) ? (int)$_REQUEST['qqtotalparts'] : 1;
229
230
        $uuid = $_REQUEST['qquuid'];
231
        if ($totalParts > 1) {
232
            # chunked upload
233
234
            $chunksFolder = $this->chunksFolder;
235
            $partIndex = (int)$_REQUEST['qqpartindex'];
236
237
            if (!is_writable($chunksFolder) && !is_executable($uploadDirectory)) {
238
                return array('error' => "Server error. Chunks directory isn't writable or executable.");
239
            }
240
241
            $targetFolder = $this->chunksFolder.DIRECTORY_SEPARATOR.$uuid;
242
243
            if (!file_exists($targetFolder)) {
244
                mkdir($targetFolder, 0775, true);
245
            }
246
247
            $target = $targetFolder.'/'.$partIndex;
248
249
            $storeResult = $this->storeUploadedFile($target, $mimeType, $uuid);
250
            if (false !== $storeResult) {
251
                return $storeResult;
252
            }
253
        } else {
254
            # non-chunked upload
255
256
            $target = join(DIRECTORY_SEPARATOR, array($uploadDirectory, $uuid, $name));
257
258
            if ($target) {
259
                $this->uploadName = basename($target);
260
261
                $storeResult = $this->storeUploadedFile($target, $mimeType, $uuid);
262
                if (false !== $storeResult) {
263
                    return $storeResult;
264
                }
265
            }
266
267
            return array('error'=> 'Could not save uploaded file.' .
268
                'The upload was cancelled, or server error encountered');
269
        }
270
    }
271
272
    protected function storeUploadedFile($target, $mimeType, $uuid)
273
    {
274
        if (!is_dir(dirname($target))) {
275
            mkdir(dirname($target), 0775, true);
276
        }
277
        if (move_uploaded_file($_FILES[$this->inputName]['tmp_name'], $target)) {
278
            return array('success'=> true, "uuid" => $uuid);
279
        }
280
        return false;
281
    }
282
283
    /**
284
     * Process a delete.
285
     * @param string      $uploadDirectory Target directory.
286
     * @param string|null $name            Overwrites the name of the file.
287
     * @return array response to be json encoded and returned to client
288
     */
289
    public function handleDelete($uploadDirectory, $name = null)
0 ignored issues
show
Unused Code introduced by
The parameter $name 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...
290
    {
291
        if ($this->isInaccessible($uploadDirectory)) {
292
            return array(
293
                'error' => "Server error. Uploads directory isn't writable"
294
                            . ((!$this->isWindows()) ? " or executable." : ".")
295
            );
296
        }
297
298
        $targetFolder = $uploadDirectory;
299
        $uuid = false;
0 ignored issues
show
Unused Code introduced by
$uuid is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
300
        $method = $_SERVER["REQUEST_METHOD"];
301
        if ($method == "DELETE") {
302
            $url = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
303
            $tokens = explode('/', $url);
304
            $uuid = $tokens[sizeof($tokens)-1];
305
        } elseif ($method == "POST") {
306
            $uuid = $_REQUEST['qquuid'];
307
        } else {
308
            return array("success" => false,
309
                "error" => "Invalid request method! ".$method
310
            );
311
        }
312
313
        $target = join(DIRECTORY_SEPARATOR, array($targetFolder, $uuid));
314
315
        if (is_dir($target)) {
316
            $this->removeDir($target);
317
            return array("success" => true, "uuid" => $uuid);
318
        } else {
319
            return array("success" => false,
320
                "error" => "File not found! Unable to delete.".$url,
0 ignored issues
show
Bug introduced by
The variable $url 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...
321
                "path" => $uuid
322
            );
323
        }
324
    }
325
326
    /**
327
     * Returns a path to use with this upload. Check that the name does not exist,
328
     * and appends a suffix otherwise.
329
     * @param string $uploadDirectory Target directory
330
     * @param string $filename The name of the file to use.
331
     *
332
     * @return string|false path or false if path could not be determined
333
     */
334
    protected function getUniqueTargetPath($uploadDirectory, $filename)
335
    {
336
        // Allow only one process at the time to get a unique file name, otherwise
337
        // if multiple people would upload a file with the same name at the same time
338
        // only the latest would be saved.
339
340
        if (function_exists('sem_acquire')) {
341
            $lock = sem_get(ftok(__FILE__, 'u'));
342
            sem_acquire($lock);
343
        }
344
345
        $pathinfo = pathinfo($filename);
346
        $base = $pathinfo['filename'];
347
        $ext = isset($pathinfo['extension']) ? $pathinfo['extension'] : '';
348
        $ext = $ext == '' ? $ext : '.' . $ext;
349
350
        $unique = $base;
351
        $suffix = 0;
352
353
        // Get unique file name for the file, by appending random suffix.
354
355
        while (file_exists($uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext)) {
356
            $suffix += rand(1, 999);
357
            $unique = $base.'-'.$suffix;
358
        }
359
360
        $result =  $uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext;
361
362
        // Create an empty target file
363
        if (!touch($result)) {
364
            // Failed
365
            $result = false;
366
        }
367
368
        if (function_exists('sem_acquire')) {
369
            sem_release($lock);
0 ignored issues
show
Bug introduced by
The variable $lock 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...
370
        }
371
372
        return $result;
373
    }
374
375
    /**
376
     * Deletes all file parts in the chunks folder for files uploaded
377
     * more than chunksExpireIn seconds ago
378
     *
379
     * @return void
380
     */
381
    protected function cleanupChunks()
382
    {
383
        foreach (scandir($this->chunksFolder) as $item) {
384
            if ($item == "." || $item == "..") {
385
                continue;
386
            }
387
388
            $path = $this->chunksFolder.DIRECTORY_SEPARATOR.$item;
389
390
            if (!is_dir($path)) {
391
                continue;
392
            }
393
394
            if (time() - filemtime($path) > $this->chunksExpireIn) {
395
                $this->removeDir($path);
396
            }
397
        }
398
    }
399
400
    /**
401
     * Removes a directory and all files contained inside
402
     * @param string $dir
403
     * @return void
404
     */
405
    protected function removeDir($dir)
406
    {
407
        foreach (scandir($dir) as $item) {
408
            if ($item == "." || $item == "..") {
409
                continue;
410
            }
411
412
            if (is_dir($item)) {
413
                $this->removeDir($item);
414
            } else {
415
                unlink(join(DIRECTORY_SEPARATOR, array($dir, $item)));
416
            }
417
        }
418
        rmdir($dir);
419
    }
420
421
    /**
422
     * Converts a given size with units to bytes.
423
     * @param string $str
424
     * @return int
425
     */
426
    protected function toBytes($str)
427
    {
428
        $str = trim($str);
429
        $last = strtolower($str[strlen($str)-1]);
430
        if(is_numeric($last)) {
431
            $val = (int) $str;
432
        } else {
433
            $val = (int) substr($str, 0, -1);
434
        }
435
        switch ($last) {
436
            case 'g':
437
                $val *= 1024; // fall thru
438
            case 'm':
439
                $val *= 1024; // fall thru
440
            case 'k':
441
                $val *= 1024; // fall thru
442
        }
443
        return $val;
444
    }
445
446
    /**
447
     * Determines whether a directory can be accessed.
448
     *
449
     * is_executable() is not reliable on Windows prior PHP 5.0.0
450
     *  (http://www.php.net/manual/en/function.is-executable.php)
451
     * The following tests if the current OS is Windows and if so, merely
452
     * checks if the folder is writable;
453
     * otherwise, it checks additionally for executable status (like before).
454
     *
455
     * @param string $directory The target directory to test access
456
     * @return bool true if directory is NOT accessible
457
     */
458
    protected function isInaccessible($directory)
459
    {
460
        $isWin = $this->isWindows();
461
        $folderInaccessible =
462
            ($isWin) ? !is_writable($directory) : (!is_writable($directory) && !is_executable($directory));
463
        return $folderInaccessible;
464
    }
465
466
    /**
467
     * Determines is the OS is Windows or not
468
     *
469
     * @return boolean
470
     */
471
472
    protected function isWindows()
473
    {
474
        $isWin = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
475
        return $isWin;
476
    }
477
}
478