Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 176 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
| tao_helpers_Http | |
0.00% |
0 / 176 |
|
0.00% |
0 / 10 |
5112 | |
0.00% |
0 / 1 |
| getDigest | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
| parseDigest | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
| getHeaders | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
| getFiles | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| hasUploadedFile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getUploadedFile | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
156 | |||
| acceptHeader | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 | |||
| returnFile | |
0.00% |
0 / 68 |
|
0.00% |
0 / 1 |
462 | |||
| returnStream | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
132 | |||
| getContentDetector | |
0.00% |
0 / 1 |
|
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 | |
| 25 | use oat\generis\Helper\SystemHelper; |
| 26 | use oat\oatbox\service\ServiceManager; |
| 27 | use oat\tao\helpers\FileUploadException; |
| 28 | use oat\tao\model\http\ContentDetector; |
| 29 | use oat\tao\model\stream\StreamRange; |
| 30 | use oat\tao\model\stream\StreamRangeException; |
| 31 | use Psr\Http\Message\StreamInterface; |
| 32 | use Psr\Http\Message\ServerRequestInterface; |
| 33 | |
| 34 | /** |
| 35 | * Description of class |
| 36 | * |
| 37 | * @author "Patrick Plichart, <patrick@taotesting.com>" |
| 38 | */ |
| 39 | class 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 | } |