GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Downloader::resumeable()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
1
<?php
2
/**
3
 * This file is part of the O2System Framework package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 *
8
 * @author         Steeve Andrian Salim
9
 * @copyright      Copyright (c) Steeve Andrian Salim
10
 */
11
12
// ------------------------------------------------------------------------
13
14
namespace O2System\Filesystem\Handlers;
15
16
// ------------------------------------------------------------------------
17
18
use O2System\Filesystem\File;
19
20
/**
21
 * Class Downloader
22
 *
23
 * @package O2System\Filesystem\Handlers
24
 */
25
class Downloader
26
{
27
    /**
28
     * Downloader::MODE_FILESTREAM
29
     *
30
     * @var int
31
     */
32
    const MODE_FILESTREAM = 1;
33
34
    /**
35
     * Downloader::MODE_DATASTREAM
36
     *
37
     * @var int
38
     */
39
    const MODE_DATASTREAM = 2;
40
41
    /**
42
     * Downloader::$mode
43
     *
44
     * @var int
45
     */
46
    protected $mode = 1;
47
48
    /**
49
     * Downloader::$filedata
50
     *
51
     * @var string
52
     */
53
    protected $filedata;
54
55
    /**
56
     * Downloader::$fileinfo
57
     *
58
     * @var array
59
     */
60
    protected $fileinfo;
61
62
    /**
63
     * Downloader::$filesize
64
     *
65
     * @var int
66
     */
67
    protected $filesize;
68
69
    /**
70
     * Downloader::$filemime
71
     *
72
     * @var string
73
     */
74
    protected $filemime;
75
76
    /**
77
     * Downloader::$lastModified
78
     *
79
     * @var int
80
     */
81
    protected $lastModified;
82
83
    /**
84
     * Downloader::$resumeable
85
     *
86
     * @var bool
87
     */
88
    protected $resumeable = true;
89
90
    /**
91
     * Downloader::$partialRequest
92
     *
93
     * @var bool
94
     */
95
    protected $partialRequest = true;
96
97
    /**
98
     * Downloader::$seekStart
99
     *
100
     * @var int
101
     */
102
    protected $seekStart = 0;
103
104
    /**
105
     * Downloader::$seekEnd
106
     *
107
     * @var int
108
     */
109
    protected $seekEnd;
110
111
    /**
112
     * Downloader::$seekFileSize
113
     *
114
     * @var int
115
     */
116
    protected $seekFileSize;
117
118
    /**
119
     * Downloader::$downloadedFileSize
120
     *
121
     * @var int
122
     */
123
    protected $downloadedFileSize = 0;
124
125
    /**
126
     * Downloader::$speedLimit
127
     *
128
     * @var int
129
     */
130
    protected $speedLimit = 512;
131
132
    /**
133
     * Downloader::$bufferSize
134
     *
135
     * @var int
136
     */
137
    protected $bufferSize = 2048;
138
139
    // ------------------------------------------------------------------------
140
141
    /**
142
     * Downloader::__construct
143
     *
144
     * @param string $filePath
145
     * @param int    $mode
146
     */
147
    public function __construct($filePath, $mode = self::MODE_FILESTREAM)
148
    {
149
        global $HTTP_SERVER_VARS;
150
151
        $this->mode = $mode;
152
153
        // disables apache compression mod_deflate || mod_gzip
154
        if (function_exists('apache_setenv')) {
155
            @apache_setenv('no-gzip', 1);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for apache_setenv(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

155
            /** @scrutinizer ignore-unhandled */ @apache_setenv('no-gzip', 1);

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...
156
        }
157
158
        // disable php cpmpression
159
        @ini_set('zlib.output_compression', 'Off');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ini_set(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

159
        /** @scrutinizer ignore-unhandled */ @ini_set('zlib.output_compression', 'Off');

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...
160
161
        if ($mode === self::MODE_FILESTREAM) {
162
            // Check if File exists and is file or not
163
            if ( ! is_file($filePath)) {
164
                output()
165
                    ->withStatus(404, 'File Not Found')
0 ignored issues
show
Bug introduced by
The method withStatus() does not exist on O2System\Kernel\Cli\Output. ( Ignorable by Annotation )

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

165
                    ->/** @scrutinizer ignore-call */ withStatus(404, 'File Not Found')

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
166
                    ->send('File Not Found');
0 ignored issues
show
Bug introduced by
The method send() does not exist on O2System\Kernel\Cli\Output. ( Ignorable by Annotation )

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

166
                    ->/** @scrutinizer ignore-call */ send('File Not Found');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
167
            }// Try To Open File for read
168
            elseif ( ! is_readable($filePath) || ! ($this->filedata = fopen($filePath, 'rb'))) {
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($filePath, 'rb') of type false or resource is incompatible with the declared type string of property $filedata.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
169
                output()
170
                    ->withStatus(403, 'Forbidden')
171
                    ->send('File Not Accessible');
172
            }
173
174
            $this->fileinfo = pathinfo($filePath);
175
            $this->filesize = filesize($filePath);
176
            $this->filemime = mime_content_type($filePath);
177
            $this->lastModified = filemtime($filePath);
178
179
        } elseif ($mode === self::MODE_DATASTREAM) {
180
            if (is_file($filePath)) {
181
                $this->filedata = file_get_contents($filePath);
182
                $this->fileinfo = pathinfo($filePath);
183
            } else {
184
                $this->filedata = $filePath;
185
                $this->fileinfo = [
186
                    'dirname'   => null,
187
                    'basename'  => 'file.stream',
188
                    'extension' => 'stream',
189
                    'filename'  => 'file',
190
                ];
191
            }
192
193
            $this->filesize = strlen($this->filedata);
194
            $this->filemime = mime_content_type($this->fileinfo[ 'filename' ]);
195
            $this->lastModified = time();
196
197
        } else {
198
            output()
199
                ->withStatus(400, 'Bad Request')
200
                ->send('Undefined Download Mode');
201
        }
202
203
        // Range
204
        if (isset($_SERVER[ 'HTTP_RANGE' ]) || isset($HTTP_SERVER_VARS[ 'HTTP_RANGE' ])) {
205
            $this->partialRequest = true;
206
            $http_range = isset($_SERVER[ 'HTTP_RANGE' ]) ? $_SERVER[ 'HTTP_RANGE' ] : $HTTP_SERVER_VARS[ 'HTTP_RANGE' ];
207
            if (stripos($http_range, 'bytes') === false) {
208
                output()
209
                    ->withStatus(416, 'Requested Range Not Satisfiable')
210
                    ->send('Requested Range Not Satisfiable');
211
            }
212
213
            $range = substr($http_range, strlen('bytes='));
214
            $range = explode('-', $range, 3);
215
            $this->seekStart = ($range[ 0 ] > 0 && $range[ 0 ] < $this->filesize - 1) ? $range[ 0 ] : 0;
0 ignored issues
show
Documentation Bug introduced by
It seems like $range[0] > 0 && $range[...ize - 1 ? $range[0] : 0 can also be of type string. However, the property $seekStart is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
216
            $this->seekEnd = ($range[ 1 ] > 0 && $range[ 1 ] < $this->filesize && $range[ 1 ] > $this->seekStart) ? $range[ 1 ] : $this->filesize - 1;
0 ignored issues
show
Documentation Bug introduced by
It seems like $range[1] > 0 && $range[...] : $this->filesize - 1 can also be of type string. However, the property $seekEnd is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
217
            $this->seekFileSize = $this->seekEnd - $this->seekStart + 1;
218
        } else {
219
            $this->partialRequest = false;
220
            $this->seekStart = 0;
221
            $this->seekEnd = $this->filesize - 1;
222
            $this->seekFileSize = $this->filesize;
223
        }
224
    }
225
226
    // ------------------------------------------------------------------------
227
228
    /**
229
     * Downloader::forceDownload
230
     *
231
     * @param string|null $filename
232
     * @param string      $filemime
233
     */
234
    public function forceDownload($filename = null, $filemime = 'application/octet-stream')
235
    {
236
        // Force mime
237
        $this->filemime = $filemime;
238
        $this->download($filename);
239
    }
240
241
    // ------------------------------------------------------------------------
242
243
    /**
244
     * Downloader::download
245
     *
246
     * @param string|null $filename
247
     */
248
    public function download($filename = null)
249
    {
250
        $filename = isset($filename) ? $filename : $this->fileinfo[ 'basename' ];
251
252
        if ($this->partialRequest) {
253
            if ($this->resumeable) {
254
                // Turn on resume capability
255
                output()
256
                    ->sendHeaderStatus(206, 'Partial Content', '1.0')
0 ignored issues
show
Bug introduced by
The method sendHeaderStatus() does not exist on O2System\Kernel\Cli\Output. ( Ignorable by Annotation )

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

256
                    ->/** @scrutinizer ignore-call */ sendHeaderStatus(206, 'Partial Content', '1.0')

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
257
                    ->sendHeader('Status', '206 Partial Content')
0 ignored issues
show
Bug introduced by
The method sendHeader() does not exist on O2System\Kernel\Cli\Output. ( Ignorable by Annotation )

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

257
                    ->/** @scrutinizer ignore-call */ sendHeader('Status', '206 Partial Content')

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
258
                    ->sendHeader('Accept-Ranges', 'bytes');
259
260
                output()->sendHeader('Content-range',
261
                    'bytes ' . $this->seekStart . '-' . $this->seekEnd . '/' . $this->filesize);
262
            } else {
263
                // Turn off resume capability
264
                $this->seekStart = 0;
265
                $this->seekEnd = $this->filesize - 1;
266
                $this->seekFileSize = $this->filesize;
267
            }
268
        }
269
270
        // Common Download Headers content type, content disposition, content length and last modified
271
        output()
272
            ->sendHeader('Content-Type', $this->filemime)
273
            ->sendHeader('Content-Disposition', 'attachment; filename=' . $filename)
274
            ->sendHeader('Content-Length', $this->seekFileSize)
275
            ->sendHeader('Last-Modified', date('D, d M Y H:i:s \G\M\T', $this->lastModified));
276
        // End Headers Stage
277
278
        // Work On Download Speed Limit
279
        if ($this->speedLimit) {
280
            // how many buffers ticks per second
281
            $bufferTicks = 10;    //10
282
            // how long one buffering tick takes by micro second
283
            $bufferMicroTime = 150; // 100
284
            // Calculate sleep micro time after each tick
285
            $sleepMicroTime = round((1000000 - ($bufferTicks * $bufferMicroTime)) / $bufferTicks);
286
            // Calculate required buffer per one tick, make sure it is integer so round the result
287
            $this->bufferSize = round($this->speedLimit * 1024 / $bufferTicks);
288
        }
289
        // Immediatly Before Downloading
290
        // clean any output buffer
291
        @ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_end_clean(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

291
        /** @scrutinizer ignore-unhandled */ @ob_end_clean();

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...
292
293
        // get oignore_user_abort value, then change it to yes
294
        $oldUserAbortSetting = ignore_user_abort();
295
        ignore_user_abort(true);
296
        // set script execution time to be unlimited
297
        @set_time_limit(0);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for set_time_limit(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

297
        /** @scrutinizer ignore-unhandled */ @set_time_limit(0);

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...
298
299
300
        // Download According Download Mode
301
        if ($this->mode === self::MODE_FILESTREAM) {
302
            // Download Data by fopen
303
            $downloadFileBytes = $this->seekFileSize;
304
            $downloaded = 0;
305
            // goto the position of the first byte to download
306
            fseek($this->filedata, $this->seekStart);
0 ignored issues
show
Bug introduced by
$this->filedata of type string is incompatible with the type resource expected by parameter $handle of fseek(). ( Ignorable by Annotation )

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

306
            fseek(/** @scrutinizer ignore-type */ $this->filedata, $this->seekStart);
Loading history...
307
            while ($downloadFileBytes > 0 && ! (connection_aborted() || connection_status() == 1)) {
308
                // still Downloading
309
                if ($downloadFileBytes > $this->bufferSize) {
310
                    // send buffer size
311
                    echo fread($this->filedata, $this->bufferSize); // this also will seek to after last read byte
0 ignored issues
show
Bug introduced by
$this->filedata of type string is incompatible with the type resource expected by parameter $handle of fread(). ( Ignorable by Annotation )

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

311
                    echo fread(/** @scrutinizer ignore-type */ $this->filedata, $this->bufferSize); // this also will seek to after last read byte
Loading history...
312
                    $downloaded += $this->bufferSize;    // updated downloaded
313
                    $downloadFileBytes -= $this->bufferSize;    // update remaining bytes
314
                } else {
315
                    // send required size
316
                    // this will happens when we reaches the end of the file normally we wll download remaining bytes
317
                    echo fread($this->filedata, $downloadFileBytes);    // this also will seek to last reat
318
319
                    $downloaded += $downloadFileBytes;    // Add to downloaded
320
321
322
                    $downloadFileBytes = 0;    // Here last bytes have been written
323
                }
324
                // send to buffer
325
                flush();
326
                // Check For Download Limit
327
                if ($this->speedLimit) {
328
                    usleep($sleepMicroTime);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $sleepMicroTime does not seem to be defined for all execution paths leading up to this point.
Loading history...
329
                }
330
331
332
            }
333
            // all bytes have been sent to user
334
            // Close File
335
            fclose($this->filedata);
0 ignored issues
show
Bug introduced by
$this->filedata of type string is incompatible with the type resource expected by parameter $handle of fclose(). ( Ignorable by Annotation )

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

335
            fclose(/** @scrutinizer ignore-type */ $this->filedata);
Loading history...
336
        } elseif ($this->mode === self::MODE_DATASTREAM) {
337
            // Download Data String
338
            $downloadFileBytes = $this->seekFileSize;
339
340
            $downloaded = 0;
341
            $offset = $this->seekStart;
342
            while ($downloadFileBytes > 0 && ( ! connection_aborted())) {
343
                if ($downloadFileBytes > $this->bufferSize) {
344
                    // Download by buffer
345
                    echo mb_strcut($this->filedata, $offset, $this->bufferSize);
346
                    $downloadFileBytes -= $this->bufferSize;
347
                    $downloaded += $this->bufferSize;
348
                    $offset += $this->bufferSize;
349
                } else {
350
                    // download last bytes
351
                    echo mb_strcut($this->filedata, $offset, $downloadFileBytes);
352
                    $downloaded += $downloadFileBytes;
353
                    $offset += $downloadFileBytes;
354
                    $downloadFileBytes = 0;
355
                }
356
                // Send Data to Buffer
357
                flush();
358
                // Check Limit
359
                if ($this->speedLimit) {
360
                    usleep($sleepMicroTime);
361
                }
362
363
            }
364
        }
365
366
        // Set Downloaded Bytes
367
        $this->downloadedFileSize = $downloaded;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $downloaded does not seem to be defined for all execution paths leading up to this point.
Loading history...
368
        ignore_user_abort($oldUserAbortSetting); // Restore old user abort settings
0 ignored issues
show
Bug introduced by
$oldUserAbortSetting of type integer is incompatible with the type boolean expected by parameter $value of ignore_user_abort(). ( Ignorable by Annotation )

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

368
        ignore_user_abort(/** @scrutinizer ignore-type */ $oldUserAbortSetting); // Restore old user abort settings
Loading history...
369
        set_time_limit(ini_get('max_execution_time')); // Restore Default script max execution Time
0 ignored issues
show
Bug introduced by
ini_get('max_execution_time') of type string is incompatible with the type integer expected by parameter $seconds of set_time_limit(). ( Ignorable by Annotation )

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

369
        set_time_limit(/** @scrutinizer ignore-type */ ini_get('max_execution_time')); // Restore Default script max execution Time
Loading history...
370
371
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
372
    }
373
374
    // ------------------------------------------------------------------------
375
376
    /**
377
     * Downloader::resumeable
378
     *
379
     * @param bool $status
380
     *
381
     * @return static
382
     */
383
    public function resumeable($status = true)
384
    {
385
        $this->partialRequest = $this->resumeable = ( bool )$status;
386
387
        return $this;
388
    }
389
390
    // ------------------------------------------------------------------------
391
392
    /**
393
     * Downloader::speedLimit
394
     *
395
     * @param int $limit
396
     *
397
     * @return static
398
     */
399
    public function speedLimit($limit)
400
    {
401
        $limit = intval($limit);
402
        $this->speedLimit = $limit;
403
404
        return $this;
405
    }
406
}