1<?php 2 3namespace ILIAS\FileDelivery; 4 5require_once('./Services/Utilities/classes/class.ilMimeTypeUtil.php'); 6require_once('./Services/Utilities/classes/class.ilUtil.php'); // This include is needed since WAC can use ilFileDelivery without Initialisation 7require_once('./Services/Context/classes/class.ilContext.php'); 8require_once('./Services/Http/classes/class.ilHTTPS.php'); 9require_once('./Services/FileDelivery/classes/FileDeliveryTypes/FileDeliveryTypeFactory.php'); 10require_once './Services/FileDelivery/classes/FileDeliveryTypes/DeliveryMethod.php'; 11 12use ILIAS\DI\HTTPServices; 13use ILIAS\FileDelivery\FileDeliveryTypes\DeliveryMethod; 14use ILIAS\FileDelivery\FileDeliveryTypes\FileDeliveryTypeFactory; 15use ILIAS\HTTP\GlobalHttpState; 16use ILIAS\HTTP\Response\ResponseHeader; 17 18/** 19 * Class Delivery 20 * 21 * @author Fabian Schmid <fs@studer-raimann.ch> 22 * @version 2.0.0 23 * @since 5.3 24 * 25 * @Internal 26 */ 27final class Delivery 28{ 29 const DIRECT_PHP_OUTPUT = 'php://output'; 30 const DISP_ATTACHMENT = 'attachment'; 31 const DISP_INLINE = 'inline'; 32 const EXPIRES_IN = '+5 days'; 33 /** 34 * @var integer 35 */ 36 private static $delivery_type_static = null; 37 /** 38 * @var string 39 */ 40 private $delivery_type = DeliveryMethod::PHP; 41 /** 42 * @var string 43 */ 44 private $mime_type = ''; 45 /** 46 * @var string 47 */ 48 private $path_to_file = ''; 49 /** 50 * @var string 51 */ 52 private $download_file_name = ''; 53 /** 54 * @var string 55 */ 56 private $disposition = self::DISP_ATTACHMENT; 57 /** 58 * @var bool 59 */ 60 private $send_mime_type = true; 61 /** 62 * @var bool 63 */ 64 private $exit_after = true; 65 /** 66 * @var bool 67 */ 68 private $convert_file_name_to_asci = true; 69 /** 70 * @var string 71 */ 72 private $etag = ''; 73 /** 74 * @var bool 75 */ 76 private $show_last_modified = true; 77 /** 78 * @var bool 79 */ 80 private $has_context = true; 81 /** 82 * @var bool 83 */ 84 private $cache = false; 85 /** 86 * @var bool 87 */ 88 private $hash_filename = false; 89 /** 90 * @var bool 91 */ 92 private $delete_file = false; 93 /** 94 * @var bool 95 */ 96 private static $DEBUG = false; 97 /** 98 * @var HTTPServices $httpService 99 */ 100 private $httpService; 101 /** 102 * @var FileDeliveryTypeFactory $fileDeliveryTypeFactory 103 */ 104 private $fileDeliveryTypeFactory; 105 106 107 /** 108 * @param string $path_to_file 109 * @param GlobalHttpState $httpState 110 */ 111 public function __construct($path_to_file, GlobalHttpState $httpState) 112 { 113 assert(is_string($path_to_file)); 114 $this->httpService = $httpState; 115 if ($path_to_file == self::DIRECT_PHP_OUTPUT) { 116 $this->setPathToFile(self::DIRECT_PHP_OUTPUT); 117 } else { 118 $this->setPathToFile($path_to_file); 119 $this->detemineDeliveryType(); 120 $this->determineMimeType(); 121 $this->determineDownloadFileName(); 122 } 123 $this->setHasContext(\ilContext::getType() !== null); 124 $this->fileDeliveryTypeFactory = new FileDeliveryTypeFactory($httpState); 125 } 126 127 128 public function stream() 129 { 130 if (!$this->delivery()->supportsStreaming()) { 131 $this->setDeliveryType(DeliveryMethod::PHP_CHUNKED); 132 } 133 $this->deliver(); 134 } 135 136 137 private function delivery() 138 { 139 return $this->fileDeliveryTypeFactory->getInstance($this->getDeliveryType()); 140 } 141 142 143 public function deliver() 144 { 145 $response = $this->httpService->response()->withHeader('X-ILIAS-FileDelivery-Method', $this->getDeliveryType()); 146 if (!$this->delivery()->doesFileExists($this->path_to_file)) { 147 $response = $this->httpService->response()->withStatus(404); 148 $this->httpService->saveResponse($response); 149 $this->httpService->sendResponse(); 150 $this->close(); 151 } 152 $this->httpService->saveResponse($response); 153 154 $this->clearBuffer(); 155 $this->checkCache(); 156 $this->setGeneralHeaders(); 157 $this->delivery()->prepare($this->getPathToFile()); 158 $this->delivery()->deliver($this->getPathToFile(), $this->isDeleteFile()); 159 if ($this->isDeleteFile()) { 160 $this->delivery()->handleFileDeletion($this->getPathToFile()); 161 } 162 if ($this->isExitAfter()) { 163 $this->close(); 164 } 165 } 166 167 168 public function setGeneralHeaders() 169 { 170 $this->checkExisting(); 171 if ($this->isSendMimeType()) { 172 $response = $this->httpService->response()->withHeader(ResponseHeader::CONTENT_TYPE, $this->getMimeType()); 173 $this->httpService->saveResponse($response); 174 } 175 if ($this->isConvertFileNameToAsci()) { 176 $this->cleanDownloadFileName(); 177 } 178 if ($this->hasHashFilename()) { 179 $this->setDownloadFileName(md5($this->getDownloadFileName())); 180 } 181 $this->setDispositionHeaders(); 182 $response = $this->httpService->response()->withHeader(ResponseHeader::ACCEPT_RANGES, 'bytes'); 183 $this->httpService->saveResponse($response); 184 if ($this->getDeliveryType() == DeliveryMethod::PHP 185 && $this->getPathToFile() != self::DIRECT_PHP_OUTPUT 186 ) { 187 $response = $this->httpService->response()->withHeader(ResponseHeader::CONTENT_LENGTH, (string) filesize($this->getPathToFile())); 188 $this->httpService->saveResponse($response); 189 } 190 $response = $this->httpService->response()->withHeader(ResponseHeader::CONNECTION, "close"); 191 $this->httpService->saveResponse($response); 192 } 193 194 195 public function setCachingHeaders() 196 { 197 $response = $this->httpService->response()->withHeader(ResponseHeader::CACHE_CONTROL, 'must-revalidate, post-check=0, pre-check=0')->withHeader(ResponseHeader::PRAGMA, 'public'); 198 199 $this->httpService->saveResponse($response->withHeader(ResponseHeader::EXPIRES, date("D, j M Y H:i:s", strtotime(self::EXPIRES_IN)) . " GMT")); 200 $this->sendEtagHeader(); 201 $this->sendLastModified(); 202 } 203 204 205 public function generateEtag() 206 { 207 $this->setEtag(md5(filemtime($this->getPathToFile()) . filesize($this->getPathToFile()))); 208 } 209 210 211 public function close() 212 { 213 exit; 214 } 215 216 217 /** 218 * @return bool 219 */ 220 private function determineMimeType() 221 { 222 $info = \ilMimeTypeUtil::lookupMimeType($this->getPathToFile(), \ilMimeTypeUtil::APPLICATION__OCTET_STREAM); 223 if ($info) { 224 $this->setMimeType($info); 225 226 return true; 227 } 228 $finfo = finfo_open(FILEINFO_MIME_TYPE); 229 $info = finfo_file($finfo, $this->getPathToFile()); 230 finfo_close($finfo); 231 if ($info) { 232 $this->setMimeType($info); 233 234 return true; 235 } 236 237 return false; 238 } 239 240 241 /** 242 * @return void 243 */ 244 private function determineDownloadFileName() 245 { 246 if (!$this->getDownloadFileName()) { 247 $download_file_name = basename($this->getPathToFile()); 248 $this->setDownloadFileName($download_file_name); 249 } 250 } 251 252 253 /** 254 * @return bool 255 */ 256 private function detemineDeliveryType() 257 { 258 if (self::$delivery_type_static) { 259 \ilWACLog::getInstance()->write('used cached delivery type'); 260 $this->setDeliveryType(self::$delivery_type_static); 261 262 return true; 263 } 264 265 if (function_exists('apache_get_modules') 266 && in_array('mod_xsendfile', apache_get_modules()) 267 ) { 268 $this->setDeliveryType(DeliveryMethod::XSENDFILE); 269 } 270 271 if (is_file('./Services/FileDelivery/classes/override.php')) { 272 $override_delivery_type = false; 273 require_once('./Services/FileDelivery/classes/override.php'); 274 if ($override_delivery_type) { 275 $this->setDeliveryType($override_delivery_type); 276 } 277 } 278 279 require_once('./Services/Environment/classes/class.ilRuntime.php'); 280 $ilRuntime = \ilRuntime::getInstance(); 281 if ((!$ilRuntime->isFPM() && !$ilRuntime->isHHVM()) 282 && $this->getDeliveryType() == DeliveryMethod::XACCEL 283 ) { 284 $this->setDeliveryType(DeliveryMethod::PHP); 285 } 286 287 if ($this->getDeliveryType() == DeliveryMethod::XACCEL 288 && strpos($this->getPathToFile(), './data') !== 0 289 ) { 290 $this->setDeliveryType(DeliveryMethod::PHP); 291 } 292 293 self::$delivery_type_static = $this->getDeliveryType(); 294 295 return true; 296 } 297 298 299 /** 300 * @return string 301 */ 302 public function getDeliveryType() 303 { 304 return $this->delivery_type; 305 } 306 307 308 /** 309 * @param string $delivery_type 310 */ 311 public function setDeliveryType($delivery_type) 312 { 313 $this->delivery_type = $delivery_type; 314 } 315 316 317 /** 318 * @return string 319 */ 320 public function getMimeType() 321 { 322 return $this->mime_type; 323 } 324 325 326 /** 327 * @param string $mime_type 328 */ 329 public function setMimeType($mime_type) 330 { 331 $this->mime_type = $mime_type; 332 } 333 334 335 /** 336 * @return string 337 */ 338 public function getPathToFile() 339 { 340 return $this->path_to_file; 341 } 342 343 344 /** 345 * @param string $path_to_file 346 */ 347 public function setPathToFile($path_to_file) 348 { 349 $this->path_to_file = $path_to_file; 350 } 351 352 353 /** 354 * @return string 355 */ 356 public function getDownloadFileName() 357 { 358 return $this->download_file_name; 359 } 360 361 362 /** 363 * @param string $download_file_name 364 */ 365 public function setDownloadFileName($download_file_name) 366 { 367 $this->download_file_name = $download_file_name; 368 } 369 370 371 /** 372 * @return string 373 */ 374 public function getDisposition() 375 { 376 return $this->disposition; 377 } 378 379 380 /** 381 * @param string $disposition 382 */ 383 public function setDisposition($disposition) 384 { 385 $this->disposition = $disposition; 386 } 387 388 389 /** 390 * @return boolean 391 */ 392 public function isSendMimeType() 393 { 394 return $this->send_mime_type; 395 } 396 397 398 /** 399 * @param boolean $send_mime_type 400 */ 401 public function setSendMimeType($send_mime_type) 402 { 403 $this->send_mime_type = $send_mime_type; 404 } 405 406 407 /** 408 * @return boolean 409 */ 410 public function isExitAfter() 411 { 412 return $this->exit_after; 413 } 414 415 416 /** 417 * @param boolean $exit_after 418 */ 419 public function setExitAfter($exit_after) 420 { 421 $this->exit_after = $exit_after; 422 } 423 424 425 /** 426 * @return boolean 427 */ 428 public function isConvertFileNameToAsci() 429 { 430 return $this->convert_file_name_to_asci; 431 } 432 433 434 /** 435 * @param boolean $convert_file_name_to_asci 436 */ 437 public function setConvertFileNameToAsci($convert_file_name_to_asci) 438 { 439 $this->convert_file_name_to_asci = $convert_file_name_to_asci; 440 } 441 442 443 /** 444 * @return string 445 */ 446 public function getEtag() 447 { 448 return $this->etag; 449 } 450 451 452 /** 453 * @param string $etag 454 */ 455 public function setEtag($etag) 456 { 457 $this->etag = $etag; 458 } 459 460 461 /** 462 * @return boolean 463 */ 464 public function getShowLastModified() 465 { 466 return $this->show_last_modified; 467 } 468 469 470 /** 471 * @param boolean $show_last_modified 472 */ 473 public function setShowLastModified($show_last_modified) 474 { 475 $this->show_last_modified = $show_last_modified; 476 } 477 478 479 /** 480 * @return boolean 481 */ 482 public function isHasContext() 483 { 484 return $this->has_context; 485 } 486 487 488 /** 489 * @param boolean $has_context 490 */ 491 public function setHasContext($has_context) 492 { 493 $this->has_context = $has_context; 494 } 495 496 497 /** 498 * @return boolean 499 */ 500 public function hasCache() 501 { 502 return $this->cache; 503 } 504 505 506 /** 507 * @param boolean $cache 508 */ 509 public function setCache($cache) 510 { 511 $this->cache = $cache; 512 } 513 514 515 /** 516 * @return boolean 517 */ 518 public function hasHashFilename() 519 { 520 return $this->hash_filename; 521 } 522 523 524 /** 525 * @param boolean $hash_filename 526 */ 527 public function setHashFilename($hash_filename) 528 { 529 $this->hash_filename = $hash_filename; 530 } 531 532 533 private function sendEtagHeader() 534 { 535 if ($this->getEtag()) { 536 $response = $this->httpService->response()->withHeader('ETag', $this->getEtag()); 537 $this->httpService->saveResponse($response); 538 } 539 } 540 541 542 private function sendLastModified() 543 { 544 if ($this->getShowLastModified()) { 545 $response = $this->httpService->response()->withHeader( 546 'Last-Modified', 547 date("D, j M Y H:i:s", filemtime($this->getPathToFile())) 548 . " GMT" 549 ); 550 $this->httpService->saveResponse($response); 551 } 552 } 553 554 // /** 555 // * @return bool 556 // */ 557 // private function isNonModified() { 558 // if (self::$DEBUG) { 559 // return false; 560 // } 561 // 562 // if (!isset($_SERVER['HTTP_IF_NONE_MATCH']) || !isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { 563 // return false; 564 // } 565 // 566 // $http_if_none_match = $_SERVER['HTTP_IF_NONE_MATCH']; 567 // $http_if_modified_since = $_SERVER['HTTP_IF_MODIFIED_SINCE']; 568 // 569 // switch (true) { 570 // case ($http_if_none_match != $this->getEtag()): 571 // return false; 572 // case (@strtotime($http_if_modified_since) <= filemtime($this->getPathToFile())): 573 // return false; 574 // } 575 // 576 // return true; 577 // } 578 579 /** 580 * @return bool 581 */ 582 public static function isDEBUG() 583 { 584 return (bool) self::$DEBUG; 585 } 586 587 588 /** 589 * @param bool $DEBUG 590 */ 591 public static function setDEBUG($DEBUG) 592 { 593 assert(is_bool($DEBUG)); 594 self::$DEBUG = $DEBUG; 595 } 596 597 598 /** 599 * @return void 600 */ 601 public function checkCache() 602 { 603 if ($this->hasCache()) { 604 $this->generateEtag(); 605 $this->sendEtagHeader(); 606 $this->setShowLastModified(true); 607 $this->setCachingHeaders(); 608 } 609 } 610 611 612 /** 613 * @return void 614 */ 615 public function clearBuffer() 616 { 617 $ob_get_contents = ob_get_contents(); 618 if ($ob_get_contents) { 619 // \ilWACLog::getInstance()->write(__CLASS__ . ' had output before file delivery: ' 620 // . $ob_get_contents); 621 } 622 ob_end_clean(); // fixed 0016469, 0016467, 0016468 623 } 624 625 626 /** 627 * @return void 628 */ 629 private function checkExisting() 630 { 631 if ($this->getPathToFile() != self::DIRECT_PHP_OUTPUT 632 && !file_exists($this->getPathToFile()) 633 ) { 634 $this->close(); 635 } 636 } 637 638 639 /** 640 * Converts the filename to ASCII 641 * 642 * @return void 643 */ 644 private function cleanDownloadFileName() 645 { 646 $download_file_name = self::returnASCIIFileName($this->getDownloadFileName()); 647 $this->setDownloadFileName($download_file_name); 648 } 649 650 651 /** 652 * Converts a UTF-8 filename to ASCII 653 * 654 * @param $original_filename string UFT8-Filename 655 * 656 * @return string ASCII-Filename 657 */ 658 public static function returnASCIIFileName($original_filename) 659 { 660 // The filename must be converted to ASCII, as of RFC 2183, 661 // section 2.3. 662 663 /// Implementation note: 664 /// The proper way to convert charsets is mb_convert_encoding. 665 /// Unfortunately Multibyte String functions are not an 666 /// installation requirement for ILIAS 3. 667 /// Codelines behind three slashes '///' show how we would do 668 /// it using mb_convert_encoding. 669 /// Note that mb_convert_encoding has the bad habit of 670 /// substituting unconvertable characters with HTML 671 /// entitities. Thats why we need a regular expression which 672 /// replaces HTML entities with their first character. 673 /// e.g. ä => a 674 675 /// $ascii_filename = mb_convert_encoding($a_filename,'US-ASCII','UTF-8'); 676 /// $ascii_filename = preg_replace('/\&(.)[^;]*;/','\\1', $ascii_filename); 677 678 // #15914 - try to fix german umlauts 679 $umlauts = array( 680 "Ä" => "Ae", 681 "Ö" => "Oe", 682 "Ü" => "Ue", 683 "ä" => "ae", 684 "ö" => "oe", 685 "ü" => "ue", 686 "ß" => "ss", 687 ); 688 foreach ($umlauts as $src => $tgt) { 689 $original_filename = str_replace($src, $tgt, $original_filename); 690 } 691 692 $ascii_filename = htmlentities($original_filename, ENT_NOQUOTES, 'UTF-8'); 693 $ascii_filename = preg_replace('/\&(.)[^;]*;/', '\\1', $ascii_filename); 694 $ascii_filename = preg_replace('/[\x7f-\xff]/', '_', $ascii_filename); 695 696 // OS do not allow the following characters in filenames: \/:*?"<>| 697 $ascii_filename = preg_replace('/[:\x5c\/\*\?\"<>\|]/', '_', $ascii_filename); 698 699 return (string) $ascii_filename; 700 // return iconv("UTF-8", "ASCII//TRANSLIT", $original_name); // proposal 701 } 702 703 704 /** 705 * @return bool 706 */ 707 public function isDeleteFile() 708 { 709 return (bool) $this->delete_file; 710 } 711 712 713 /** 714 * @param bool $delete_file 715 * 716 * @return void 717 */ 718 public function setDeleteFile($delete_file) 719 { 720 assert(is_bool($delete_file)); 721 $this->delete_file = $delete_file; 722 } 723 724 725 private function setDispositionHeaders() 726 { 727 $response = $this->httpService->response(); 728 $response = $response->withHeader( 729 ResponseHeader::CONTENT_DISPOSITION, 730 $this->getDisposition() 731 . '; filename="' 732 . $this->getDownloadFileName() 733 . '"' 734 ); 735 $response = $response->withHeader('Content-Description', $this->getDownloadFileName()); 736 $this->httpService->saveResponse($response); 737 } 738} 739