SystemFineUploadHandler::getName()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 3
eloc 6
c 3
b 1
f 0
nc 3
nop 0
dl 0
loc 10
rs 10
1
<?php
2
3
use Xmf\Request;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Request. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
4
5
/**
6
 * Base SystemFineUploadHandler class to work with ajaxfineupload.php endpoint
7
 *
8
 * Upload files as specified
9
 *
10
 * Do not use or reference this directly from your client-side code.
11
 * Instead, this should be required via the endpoint.php or endpoint-cors.php
12
 * file(s).
13
 *
14
 * @license   MIT License (MIT)
15
 * @copyright Copyright (c) 2015-present, Widen Enterprises, Inc.
16
 * @link      https://github.com/FineUploader/php-traditional-server
17
 *
18
 * The MIT License (MIT)
19
 *
20
 * Copyright (c) 2015-present, Widen Enterprises, Inc.
21
 *
22
 * Permission is hereby granted, free of charge, to any person obtaining a copy
23
 * of this software and associated documentation files (the "Software"), to deal
24
 * in the Software without restriction, including without limitation the rights
25
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
26
 * copies of the Software, and to permit persons to whom the Software is
27
 * furnished to do so, subject to the following conditions:
28
 *
29
 * The above copyright notice and this permission notice shall be included in all
30
 * copies or substantial portions of the Software.
31
 *
32
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
33
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
35
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
37
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
38
 * SOFTWARE.
39
 */
40
41
abstract class SystemFineUploadHandler
42
{
43
    public $allowedExtensions = [];
44
    public $allowedMimeTypes = ['(none)']; // must specify!
45
    public $sizeLimit = null;
46
    public $inputName = 'qqfile';
47
    public $chunksFolder = 'chunks';
48
49
    public $chunksCleanupProbability = 0.001; // Once in 1000 requests on avg
50
    public $chunksExpireIn = 604800; // One week
51
52
    public $uploadName;
53
    public $claims;
54
55
    /**
56
     * XoopsFineUploadHandler constructor.
57
     * @param stdClass $claims claims passed in JWT header
58
     */
59
    public function __construct(\stdClass $claims)
60
    {
61
        $this->claims = $claims;
62
    }
63
64
    /**
65
     * Get the original filename
66
     */
67
    public function getName()
68
    {
69
        if (Request::hasVar('qqfilename', 'REQUEST')) {
70
            $qqfilename = Request::getString('qqfilename', '', 'REQUEST');
71
            return $qqfilename;
72
        }
73
74
        if (Request::hasVar($this->inputName, 'FILES')) {
75
            $file = Request::getArray($this->inputName, null, 'FILES');
76
            return $file ;
77
        }
78
    }
79
80
    /**
81
     * Get the name of the uploaded file
82
     * @return string
83
     */
84
    public function getUploadName()
85
    {
86
        return $this->uploadName;
87
    }
88
89
    /**
90
     * Combine chunks into a single file
91
     *
92
     * @param string      $uploadDirectory upload directory
93
     * @param string|null $name            name
94
     * @return array response to be json encoded and returned to client
95
     */
96
    public function combineChunks($uploadDirectory, $name = null)
97
    {
98
        $uuid = Request::getString('qquuid', '', 'POST');
99
        if ('' === $name) {
100
            $name = $this->getName();
101
        }
102
        $targetFolder = $this->chunksFolder . DIRECTORY_SEPARATOR . $uuid;
103
        $totalParts = Request::getInt('qqtotalparts', 1, 'REQUEST');
104
105
        $targetPath = implode(DIRECTORY_SEPARATOR, [$uploadDirectory, $uuid, $name]);
106
        $this->uploadName = $name;
107
108
        if (!file_exists($targetPath)) {
109
            if (!mkdir($concurrentDirectory = dirname($targetPath), 0777, true) && !is_dir($concurrentDirectory)) {
110
                throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
111
            }
112
        }
113
        $target = fopen($targetPath, 'wb');
114
115
        for ($i = 0; $i < $totalParts; $i++) {
116
            $chunk = fopen($targetFolder . DIRECTORY_SEPARATOR . $i, 'rb');
117
            stream_copy_to_stream($chunk, $target);
118
            fclose($chunk);
119
        }
120
121
        // Success
122
        fclose($target);
123
124
        for ($i = 0; $i < $totalParts; $i++) {
125
            unlink($targetFolder . DIRECTORY_SEPARATOR . $i);
126
        }
127
128
        rmdir($targetFolder);
129
130
        if (null !== $this->sizeLimit && filesize($targetPath) > $this->sizeLimit) {
131
            unlink($targetPath);
132
            //http_response_code(413);
133
            header('HTTP/1.0 413 Request Entity Too Large');
134
            return ['success' => false, 'uuid' => $uuid, 'preventRetry' => true];
135
        }
136
137
        return ['success' => true, 'uuid' => $uuid];
138
    }
139
140
    /**
141
     * Process the upload.
142
     * @param string $uploadDirectory Target directory.
143
     * @param string $name Overwrites the name of the file.
144
     * @return array response to be json encoded and returned to client
145
     */
146
    public function handleUpload($uploadDirectory, $name = null)
147
    {
148
        if (is_writable($this->chunksFolder) &&
149
            1 == mt_rand(1, 1 / $this->chunksCleanupProbability)) {
0 ignored issues
show
Bug introduced by
1 / $this->chunksCleanupProbability of type double is incompatible with the type integer expected by parameter $max of mt_rand(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

149
            1 == mt_rand(1, /** @scrutinizer ignore-type */ 1 / $this->chunksCleanupProbability)) {
Loading history...
150
            // Run garbage collection
151
            $this->cleanupChunks();
152
        }
153
154
        // Check that the max upload size specified in class configuration does not
155
        // exceed size allowed by server config
156
        if ($this->toBytes(ini_get('post_max_size')) < $this->sizeLimit ||
157
            $this->toBytes(ini_get('upload_max_filesize')) < $this->sizeLimit) {
158
            $neededRequestSize = max(1, $this->sizeLimit / 1024 / 1024) . 'M';
159
            return [
160
                'error' => 'Server error. Increase post_max_size and upload_max_filesize to ' . $neededRequestSize,
161
            ];
162
        }
163
164
        if ($this->isInaccessible($uploadDirectory)) {
165
            return ['error' => "Server error. Uploads directory isn't writable"];
166
        }
167
168
        $type = $_SERVER['HTTP_CONTENT_TYPE'] ?? $_SERVER['CONTENT_TYPE'];
169
170
        if (!isset($type)) {
171
            return ['error' => "No files were uploaded."];
172
        }
173
174
        if (strpos(strtolower($type), 'multipart/') !== 0) {
175
            return [
176
                'error' => "Server error. Not a multipart request. Please set forceMultipart to default value (true).",
177
            ];
178
        }
179
180
        // Get size and name
181
        $file = Request::getArray($this->inputName, [], 'FILES');
182
        $size = $file['size'];
183
        if (Request::hasVar('qqtotalfilesize')) {
184
            $size = Request::getInt('qqtotalfilesize');
185
        }
186
187
        if (null === $name) {
188
            $name = $this->getName();
189
        }
190
191
        // check file error
192
        if ($file['error']) {
193
            return ['error' => 'Upload Error #' . $file['error']];
194
        }
195
196
        // Validate name
197
        if (null === $name || '' === $name) {
198
            return ['error' => 'File name empty.'];
199
        }
200
201
        // Validate file size
202
        if (0 == $size) {
203
            return ['error' => 'File is empty.'];
204
        }
205
206
        if (null !== $this->sizeLimit && $size > $this->sizeLimit) {
207
            return ['error' => 'File is too large.', 'preventRetry' => true];
208
        }
209
210
        // Validate file extension
211
        $pathinfo = pathinfo((string) $name);
212
        $ext = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : '';
213
214
        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...
215
            && !in_array(strtolower($ext), array_map('strtolower', $this->allowedExtensions))) {
216
            $these = implode(', ', $this->allowedExtensions);
217
            return [
218
                'error' => 'File has an invalid extension, it should be one of ' . $these . '.',
219
                'preventRetry' => true,
220
            ];
221
        }
222
223
        $mimeType = '';
224
        if (!empty($this->allowedMimeTypes)) {
225
226
            $temp = $this->inputName;
227
            $mimeType = mime_content_type(Request::getArray($temp, [], 'FILES')['tmp_name']);
228
            if (!in_array($mimeType, $this->allowedMimeTypes)) {
229
                return ['error' => 'File is of an invalid type.', 'preventRetry' => true];
230
            }
231
        }
232
233
        // Save a chunk
234
        $totalParts = 1;
235
        if (Request::hasVar('qqtotalparts')) {
236
            $totalParts = (int) Request::getString('qqtotalparts');
237
        }
238
239
        $uuid = Request::getString('qquuid');
240
        if ($totalParts > 1) {
241
            # chunked upload
242
243
            $chunksFolder = $this->chunksFolder;
244
            $partIndex = (int) Request::getString('qqpartindex');
245
246
            if (!is_writable($chunksFolder) && !is_executable($uploadDirectory)) {
247
                return ['error' => "Server error. Chunks directory isn't writable or executable."];
248
            }
249
250
            $targetFolder = $this->chunksFolder . DIRECTORY_SEPARATOR . $uuid;
251
252
            if (!file_exists($targetFolder)) {
253
                if (!mkdir($targetFolder, 0775, true) && !is_dir($targetFolder)) {
254
                    throw new \RuntimeException(sprintf('Directory "%s" was not created', $targetFolder));
255
                }
256
            }
257
258
            $target = $targetFolder . '/' . $partIndex;
259
260
            $storeResult = $this->storeUploadedFile($target, $mimeType, $uuid);
261
            if (false !== $storeResult) {
262
                return $storeResult;
263
            }
264
        } else {
265
            # non-chunked upload
266
267
            $target = implode(DIRECTORY_SEPARATOR, [$uploadDirectory, $uuid, $name]);
268
269
            if ($target) {
270
                $this->uploadName = basename($target);
271
272
                $storeResult = $this->storeUploadedFile($target, $mimeType, $uuid);
273
                if (false !== $storeResult) {
274
                    return $storeResult;
275
                }
276
            }
277
278
            return ['error' => 'Could not save uploaded file.' .
279
                               'The upload was cancelled, or server error encountered',
280
            ];
281
        }
282
    }
283
284
    protected function storeUploadedFile($target, $mimeType, $uuid)
285
    {
286
        if (!is_dir(dirname((string) $target))) {
287
            if (!mkdir($concurrentDirectory = dirname((string) $target), 0775, true) && !is_dir($concurrentDirectory)) {
288
                throw new \RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
289
            }
290
        }
291
        $file = Request::getArray($this->inputName, null, 'FILES');
292
        if (null !== $file && move_uploaded_file($file['tmp_name'], $target)) {
293
            return ['success' => true, 'uuid' => $uuid];
294
        }
295
        return false;
296
    }
297
298
    /**
299
     * Process a delete.
300
     * @param string      $uploadDirectory Target directory.
301
     * @param string|null $name            Overwrites the name of the file.
302
     * @return array response to be json encoded and returned to client
303
     */
304
    public function handleDelete($uploadDirectory, $name = null)
305
    {
306
        if ($this->isInaccessible($uploadDirectory)) {
307
            return [
308
                'error' => "Server error. Uploads directory isn't writable"
309
                           . ((!$this->isWindows()) ? ' or executable.' : '.'),
310
            ];
311
        }
312
313
        $uuid = false;
314
        $method = Request::getString('REQUEST_METHOD', 'GET', 'SERVER');
315
316
        if ('DELETE' === $method) {
317
            $url = Request::getString('REQUEST_URI', '', 'SERVER');
318
            if ('' !== $url) {
319
                $url    = parse_url($url, PHP_URL_PATH);
320
                $tokens = explode('/', $url);
321
                $uuid = $tokens[count($tokens) - 1];
322
            }
323
        } elseif ('POST' === $method) {
324
            $uuid = Request::getString('qquuid', '', 'REQUEST');
325
        } else {
326
            return ['success' => false,
327
                'error'   => 'Invalid request method! ' . $method,
328
            ];
329
        }
330
331
        $target = implode(DIRECTORY_SEPARATOR, [$uploadDirectory, $uuid]);
332
333
        if (is_dir($target)) {
334
            $this->removeDir($target);
335
            return ['success' => true, 'uuid' => $uuid];
336
        } else {
337
            return [
338
                'success' => false,
339
                'error'   => 'File not found! Unable to delete.' . $url,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $url does not seem to be defined for all execution paths leading up to this point.
Loading history...
340
                'path'    => $uuid,
341
            ];
342
        }
343
    }
344
345
    /**
346
     * Returns a path to use with this upload. Check that the name does not exist,
347
     * and appends a suffix otherwise.
348
     * @param string $uploadDirectory Target directory
349
     * @param string $filename The name of the file to use.
350
     *
351
     * @return string|false path or false if path could not be determined
352
     */
353
    protected function getUniqueTargetPath($uploadDirectory, $filename)
354
    {
355
        // Allow only one process at the time to get a unique file name, otherwise
356
        // if multiple people would upload a file with the same name at the same time
357
        // only the latest would be saved.
358
359
        if (function_exists('sem_acquire')) {
360
            $lock = sem_get(ftok(__FILE__, 'u'));
361
            sem_acquire($lock);
362
        }
363
364
        $pathinfo = pathinfo($filename);
365
        $base = $pathinfo['filename'];
366
        $ext = $pathinfo['extension'] ?? '';
367
        $ext = '' == $ext ? $ext : '.' . $ext;
368
369
        $unique = $base;
370
        $suffix = 0;
371
372
        // Get unique file name for the file, by appending random suffix.
373
374
        while (file_exists($uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext)) {
375
            $suffix += random_int(1, 999);
376
            $unique = $base . '-' . $suffix;
377
        }
378
379
        $result =  $uploadDirectory . DIRECTORY_SEPARATOR . $unique . $ext;
380
381
        // Create an empty target file
382
        if (!touch($result)) {
383
            // Failed
384
            $result = false;
385
        }
386
387
        if (function_exists('sem_acquire')) {
388
            sem_release($lock);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $lock does not seem to be defined for all execution paths leading up to this point.
Loading history...
389
        }
390
391
        return $result;
392
    }
393
394
    /**
395
     * Deletes all file parts in the chunks folder for files uploaded
396
     * more than chunksExpireIn seconds ago
397
     *
398
     * @return void
399
     */
400
    protected function cleanupChunks()
401
    {
402
        foreach (scandir($this->chunksFolder) as $item) {
403
            if ('.' == $item || '..' == $item) {
404
                continue;
405
            }
406
407
            $path = $this->chunksFolder . DIRECTORY_SEPARATOR . $item;
408
409
            if (!is_dir($path)) {
410
                continue;
411
            }
412
413
            if (time() - filemtime($path) > $this->chunksExpireIn) {
414
                $this->removeDir($path);
415
            }
416
        }
417
    }
418
419
    /**
420
     * Removes a directory and all files contained inside
421
     * @param string $dir
422
     * @return void
423
     */
424
    protected function removeDir($dir)
425
    {
426
        foreach (scandir($dir) as $item) {
427
            if ('.' == $item || '..' == $item) {
428
                continue;
429
            }
430
431
            if (is_dir($item)) {
432
                $this->removeDir($item);
433
            } else {
434
                unlink(implode(DIRECTORY_SEPARATOR, [$dir, $item]));
435
            }
436
        }
437
        rmdir($dir);
438
    }
439
440
    /**
441
     * Converts a given size with units to bytes.
442
     * @param string $str
443
     * @return int
444
     */
445
    protected function toBytes($str)
446
    {
447
        $str = trim($str);
448
        $last = strtolower($str[strlen($str) - 1]);
449
        if(is_numeric($last)) {
450
            $val = (int) $str;
451
        } else {
452
            $val = (int) substr($str, 0, -1);
453
        }
454
        switch ($last) {
455
            case 'g':
456
                $val *= 1024; // fall thru
457
                // no break
458
            case 'm':
459
                $val *= 1024; // fall thru
460
                // no break
461
            case 'k':
462
                $val *= 1024; // fall thru
463
        }
464
        return $val;
465
    }
466
467
    /**
468
     * Determines whether a directory can be accessed.
469
     *
470
     * is_executable() is not reliable on Windows prior PHP 5.0.0
471
     *  (https://www.php.net/manual/en/function.is-executable.php)
472
     * The following tests if the current OS is Windows and if so, merely
473
     * checks if the folder is writable;
474
     * otherwise, it checks additionally for executable status (like before).
475
     *
476
     * @param string $directory The target directory to test access
477
     * @return bool true if directory is NOT accessible
478
     */
479
    protected function isInaccessible($directory)
480
    {
481
        $isWin = $this->isWindows();
482
        $folderInaccessible =
483
            ($isWin) ? !is_writable($directory) : (!is_writable($directory) && !is_executable($directory));
484
        return $folderInaccessible;
485
    }
486
487
    /**
488
     * Determines is the OS is Windows or not
489
     *
490
     * @return bool
491
     */
492
493
    protected function isWindows()
494
    {
495
        $isWin = (stripos(PHP_OS, 'WIN') === 0);
496
        return $isWin;
497
    }
498
}
499