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. &auml; => 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