Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 176
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
tao_helpers_Http
0.00% covered (danger)
0.00%
0 / 176
0.00% covered (danger)
0.00%
0 / 10
5112
0.00% covered (danger)
0.00%
0 / 1
 getDigest
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 parseDigest
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 getHeaders
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 getFiles
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 hasUploadedFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUploadedFile
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
156
 acceptHeader
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 returnFile
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
462
 returnStream
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
132
 getContentDetector
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2008-2010 (original work) Deutsche Institut für Internationale Pädagogische Forschung
19 *                         (under the project TAO-TRANSFER);
20 *               2009-2012 (update and modification) Public Research Centre Henri Tudor
21 *                         (under the project TAO-SUSTAIN & TAO-DEV);
22 *               2020 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
23 */
24
25use oat\generis\Helper\SystemHelper;
26use oat\oatbox\service\ServiceManager;
27use oat\tao\helpers\FileUploadException;
28use oat\tao\model\http\ContentDetector;
29use oat\tao\model\stream\StreamRange;
30use oat\tao\model\stream\StreamRangeException;
31use Psr\Http\Message\StreamInterface;
32use Psr\Http\Message\ServerRequestInterface;
33
34/**
35 * Description of class
36 *
37 * @author "Patrick Plichart, <patrick@taotesting.com>"
38 */
39class tao_helpers_Http
40{
41    public const BYTES_BY_CYCLE =  5242880; //1024 * 1024 * 5
42
43    public static $headers;
44
45    /**
46     * @author "Patrick Plichart, <patrick@taotesting.com>"
47     * @return boolean|Ambigous <unknown, string>
48     */
49    public static function getDigest()
50    {
51        // seems apache-php is absorbing the header
52        if (isset($_SERVER['PHP_AUTH_DIGEST'])) {
53            $digest = $_SERVER['PHP_AUTH_DIGEST'];
54        // most other servers
55        } elseif (isset($_SERVER['HTTP_AUTHENTICATION'])) {
56            if (strpos(strtolower($_SERVER['HTTP_AUTHENTICATION']), 'digest') === 0) {
57                $digest = substr($_SERVER['HTTP_AUTHORIZATION'], 7);
58            }
59        } else {
60            return false;
61        }
62
63        return $digest;
64    }
65
66    /**
67     * @author "Patrick Plichart, <patrick@taotesting.com>"
68     * @param string $digest
69     * @return Ambigous <boolean, multitype:unknown >
70     */
71    public static function parseDigest($digest)
72    {
73        // protect against missing data
74        $needed_parts = [
75            'nonce' => 1,
76            'nc' => 1,
77            'cnonce' => 1,
78            'qop' => 1,
79            'username' => 1,
80            'uri' => 1,
81            'response' => 1
82        ];
83        $data = [];
84        $keys = implode('|', array_keys($needed_parts));
85
86        preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $digest, $matches, PREG_SET_ORDER);
87
88        foreach ($matches as $m) {
89            $data[$m[1]] = $m[3] ? $m[3] : $m[4];
90            unset($needed_parts[$m[1]]);
91        }
92        return $needed_parts ? false : $data;
93    }
94
95    /**
96     * Return array of HTTP headers from the current request
97     * @return array|false
98     */
99    public static function getHeaders()
100    {
101        if (self::$headers === null) {
102            if (function_exists('apache_request_headers')) {
103                $headers = apache_request_headers();
104            } else {
105                $headers = [];
106                if (isset($_SERVER['CONTENT_TYPE'])) {
107                    $headers['Content-Type'] = $_SERVER['CONTENT_TYPE'];
108                }
109                if (isset($_ENV['CONTENT_TYPE'])) {
110                    $headers['Content-Type'] = $_ENV['CONTENT_TYPE'];
111                }
112                foreach ($_SERVER as $key => $value) {
113                    if (substr($key, 0, 5) == "HTTP_") {
114                        // this is chaos, basically it is just there to capitalize the first
115                        // letter of every word that is not an initial HTTP and strip HTTP
116                        // code from przemek
117                        $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
118                        $headers[$key] = $value;
119                    }
120                }
121            }
122            self::$headers = $headers;
123        }
124
125        return self::$headers;
126    }
127
128    /**
129     * @author "Patrick Plichart, <patrick@taotesting.com>"
130     * @return string[]
131     */
132    public static function getFiles()
133    {
134        if (isset($_POST['contentName'])) {
135            $_FILES['content']['name'] = urldecode($_POST['contentName']);
136        }
137        return $_FILES;
138    }
139
140    /**
141     * verify if file uploads exists.
142     * return true if key $name exists in $_FILES
143     *
144     * @author Christophe GARCIA <christopheg@taotesting.com>
145     * @param string $name
146     * @return boolean
147     */
148    public static function hasUploadedFile($name)
149    {
150        return array_key_exists($name, self::getFiles());
151    }
152
153    /**
154     * Get the files data from an HTTP file upload (ie. from the $_FILES)
155     * @author "Bertrand Chevrier <bertrand@taotesting.com>
156     * @param string the file field name
157     * @return array the file data
158     * @throws common_exception_Error in case of wrong upload
159     */
160    public static function getUploadedFile($name)
161    {
162
163        // for large file, the $_FILES may be empty so see this before checking for other updates
164        $limit = SystemHelper::getFileUploadLimit();
165        $contentLength = intval($_SERVER['CONTENT_LENGTH']);
166        if ($limit > 0 && $contentLength > $limit && count(self::getFiles()) === 0) {
167            throw new FileUploadException('Exceeded filesize limit of ' . $limit);
168        }
169
170        $files = self::getFiles();
171        $fileData = $files[$name];
172        if (isset($files[$name])) {
173            //check for upload errors
174            if (isset($fileData['error']) && $fileData['error'] != UPLOAD_ERR_OK) {
175                switch ($fileData['error']) {
176                    case UPLOAD_ERR_NO_FILE:
177                        throw new FileUploadException('No file sent.');
178                    case UPLOAD_ERR_INI_SIZE:
179                    case UPLOAD_ERR_FORM_SIZE:
180                        throw new FileUploadException('Exceeded filesize limit of ' . $limit);
181                    default:
182                        throw new common_exception_Error('Upload fails, check errors');
183                }
184            }
185        }
186        if (!is_uploaded_file($fileData['tmp_name'])) {
187            throw new common_exception_Error('Non uploaded file in filedata, potential attack');
188        }
189        return $fileData;
190    }
191
192    /**
193     * @author "Patrick Plichart, <patrick@taotesting.com>"
194     * @param string $supportedMimeTypes
195     * @param string $requestedMimeTypes
196     * @throws common_exception_NotAcceptable
197     * @return string|NULL
198     */
199    public static function acceptHeader($supportedMimeTypes = null, $requestedMimeTypes = null)
200    {
201        $acceptTypes = [];
202        $accept = strtolower($requestedMimeTypes);
203        $accept = explode(',', $accept);
204        foreach ($accept as $a) {
205            // the default quality is 1.
206            $q = 1;
207            // check if there is a different quality
208            if (strpos($a, ';q=')) {
209                // divide "mime/type;q=X" into two parts: "mime/type" i "X"
210                list($a, $q) = explode(';q=', $a);
211            }
212            // mime-type $a is accepted with the quality $q
213            // WARNING: $q == 0 means, that mime-type isn’t supported!
214            $acceptTypes[$a] = $q;
215        }
216        arsort($acceptTypes);
217        if (!$supportedMimeTypes) {
218            return reset($acceptTypes);
219        }
220        $supportedMimeTypes = array_map('strtolower', (array) $supportedMimeTypes);
221        // let’s check our supported types:
222        foreach ($acceptTypes as $mime => $q) {
223            if ($mime === '*/*') {
224                return null;
225            }
226
227            if ($q && in_array(trim($mime), $supportedMimeTypes)) {
228                return trim($mime);
229            }
230        }
231        throw new common_exception_NotAcceptable();
232    }
233
234    /**
235     * Sends file content to the client(browser or video/audio player in the browser), it serves images, video/audio
236     * files and any other type of file.<br />
237     * If the client asks for partial contents, then partial contents are served, if not, the whole file is send.<br />
238     * Works well with big files, without eating up memory.
239     * @author "Martin for OAT <code@taotesting.com>"
240     * @param string $filename the file name
241     * @param boolean $contenttype whether to add content type header or not
242     * @param boolean $svgzSupport whether to add content encoding header or not
243     * @throws common_exception_Error
244     */
245    public static function returnFile($filename, $contenttype = true, $svgzSupport = false)
246    {
247        if (tao_helpers_File::securityCheck($filename, true)) {
248            if (file_exists($filename)) {
249                if ($contenttype) {
250                    header('Content-Type: ' . tao_helpers_File::getMimeType($filename));
251                }
252                $fp = fopen($filename, 'rb');
253                if ($fp === false) {
254                    header("HTTP/1.0 404 Not Found");
255                } else {
256                    $pathinfo = pathinfo($filename);
257                    if (isset($pathinfo['extension']) && $pathinfo['extension'] === 'svgz' && !$svgzSupport) {
258                        header('Content-Encoding: gzip');
259                    }
260
261                    // session must be closed because, for example, video files might take a while to be sent to the
262                    // client and we need the client to be able to make other calls to the server during that time
263                    session_write_close();
264
265                    $http416RequestRangeNotSatisfiable = 'HTTP/1.1 416 Requested Range Not Satisfiable';
266                    $http206PartialContent = 'HTTP/1.1 206 Partial Content';
267                    $http200OK = 'HTTP/1.1 200 OK';
268                    $filesize = filesize($filename);
269                    $offset = 0;
270                    $length = $filesize;
271                    $useFpassthru = false;
272                    $partialContent = false;
273                    header('Accept-Ranges: bytes');
274
275                    if (isset($_SERVER['HTTP_RANGE'])) {
276                        $partialContent = true;
277                        preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $matches);
278                        $offset = intval($matches[1]);
279                        if (!isset($matches[2])) {
280                            // no end position is given, so we serve the file from the start position to the end
281                            $useFpassthru = true;
282                        } else {
283                            $length = intval($matches[2]) - $offset;
284                        }
285                    }
286
287                    fseek($fp, $offset);
288
289                    if ($partialContent) {
290                        if (($offset < 0) || ($offset > $filesize)) {
291                            header($http416RequestRangeNotSatisfiable);
292                        } else {
293                            if ($useFpassthru) {
294                                // only a starting position is given
295                                header($http206PartialContent);
296                                header("Content-Length: " . ($filesize - $offset));
297                                header('Content-Range: bytes ' . $offset . '-' . ($filesize - 1) . '/' . $filesize);
298                                if (ob_get_level() > 0) {
299                                    ob_end_flush();
300                                }
301                                fpassthru($fp);
302                            } else {
303                                // we are given a starting position and how many bytes the client asks for
304                                $endPosition = $offset + $length;
305                                if ($endPosition > $filesize) {
306                                    header($http416RequestRangeNotSatisfiable);
307                                } else {
308                                    header($http206PartialContent);
309                                    header("Content-Length: " . ($length));
310                                    header(
311                                        'Content-Range: bytes ' . $offset . '-' . ($offset + $length - 1) . '/'
312                                            . $filesize
313                                    );
314                                    // send 500KB per cycle
315                                    $bytesPerCycle = (1024 * 1024) * 0.5;
316                                    $currentPosition = $offset;
317                                    if (ob_get_level() > 0) {
318                                        ob_end_flush();
319                                    }
320                                    // because the client might ask for the whole file, we split the serving into little
321                                    // pieces this is also good in case someone with bad intentions tries to get the
322                                    // whole file many times and eat up the server memory, we are not loading the whole
323                                    // file into the memory.
324                                    while (!feof($fp)) {
325                                        if (($currentPosition + $bytesPerCycle) <= $endPosition) {
326                                            $data = fread($fp, $bytesPerCycle);
327                                            $currentPosition = $currentPosition + $bytesPerCycle;
328                                            echo $data;
329                                        } else {
330                                            $data = fread($fp, ($endPosition - $currentPosition));
331                                            echo $data;
332                                        }
333                                    }
334                                }
335                            }
336                        }
337                    } else {
338                        // client does not want partial contents so we just serve the whole file
339                        header($http200OK);
340                        header("Content-Length: " . $filesize);
341                        if (ob_get_level() > 0) {
342                            ob_end_flush();
343                        }
344                        fpassthru($fp);
345                    }
346                    fclose($fp);
347                }
348            } else {
349                if (class_exists('common_Logger')) {
350                    common_Logger::w('File ' . $filename . ' not found');
351                }
352                header("HTTP/1.0 404 Not Found");
353            }
354        } else {
355            throw new common_exception_Error('Security exception for path ' . $filename);
356        }
357    }
358
359    public static function returnStream(
360        StreamInterface $stream,
361        string $mimeType = null,
362        ServerRequestInterface $request = null
363    ): void {
364        header('Accept-Ranges: bytes');
365        if (!is_null($mimeType)) {
366            header('Content-Type: ' . $mimeType);
367        }
368
369        if (self::getContentDetector()->isGzipableMime($mimeType) && self::getContentDetector()->isGzip($stream)) {
370            header('Content-Encoding: gzip');
371        }
372
373        try {
374            $ranges = StreamRange::createFromRequest($stream, $request);
375            $contentLength = 0;
376            if (!empty($ranges)) {
377                header('HTTP/1.1 206 Partial Content');
378                foreach ($ranges as $range) {
379                    $contentLength += (($range->getLastPos() - $range->getFirstPos()) + 1);
380                }
381                //@todo Content-Range for multiple ranges?
382                header(
383                    'Content-Range: bytes ' . $ranges[0]->getFirstPos() . '-' . $ranges[0]->getLastPos()
384                        . '/' . $stream->getSize()
385                );
386            } else {
387                $contentLength = $stream->getSize();
388                header('HTTP/1.1 200 OK');
389            }
390
391            header("Content-Length: " . $contentLength);
392
393            if (empty($ranges)) {
394                while (!$stream->eof()) {
395                    echo $stream->read(self::BYTES_BY_CYCLE);
396                }
397            } else {
398                foreach ($ranges as $range) {
399                    $pos = $range->getFirstPos();
400                    $stream->seek($pos);
401                    while ($pos <= $range->getLastPos()) {
402                        $length = min((($range->getLastPos() - $pos) + 1), self::BYTES_BY_CYCLE);
403                        echo $stream->read($length);
404                        $pos += $length;
405                    }
406                }
407            }
408        } catch (StreamRangeException $e) {
409            header('HTTP/1.1 416 Requested Range Not Satisfiable');
410        }
411    }
412
413    private static function getContentDetector(): ContentDetector
414    {
415        /** @noinspection PhpIncompatibleReturnTypeInspection */
416        return ServiceManager::getServiceManager()->get(ContentDetector::class);
417    }
418}