1<?php
2
3namespace ILIAS\FileDelivery\FileDeliveryTypes;
4
5use ILIAS\FileDelivery\ilFileDeliveryType;
6use ILIAS\HTTP\GlobalHttpState;
7use ILIAS\HTTP\Response\ResponseHeader;
8
9require_once('./Services/FileDelivery/interfaces/int.ilFileDeliveryType.php');
10
11/**
12 * Class PHPChunked
13 *
14 * @author  Fabian Schmid <fs@studer-raimann.ch>
15 * @since   5.3
16 * @version 1.0
17 */
18final class PHPChunked implements ilFileDeliveryType
19{
20
21    /**
22     * @var GlobalHttpState $httpService
23     */
24    private $httpService;
25
26
27    /**
28     * PHP constructor.
29     *
30     * @param GlobalHttpState $httpState
31     */
32    public function __construct(GlobalHttpState $httpState)
33    {
34        $this->httpService = $httpState;
35    }
36
37
38    /**
39     * @inheritDoc
40     */
41    public function doesFileExists($path_to_file)
42    {
43        return is_readable($path_to_file);
44    }
45
46
47    /**
48     * @inheritdoc
49     */
50    public function prepare($path_to_file)
51    {
52        return true;
53    }
54
55
56    /**
57     * @inheritdoc
58     */
59    public function deliver($path_to_file, $file_marked_to_delete)
60    {
61        $file = $path_to_file;
62        $fp = @fopen($file, 'rb');
63
64        $size = filesize($file); // File size
65        $length = $size;           // Content length
66        $start = 0;               // Start byte
67        $end = $size - 1;       // End byte
68        // Now that we've gotten so far without errors we send the accept range header
69        /* At the moment we only support single ranges.
70         * Multiple ranges requires some more work to ensure it works correctly
71         * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
72         *
73         * Multirange support annouces itself with:
74         * header('Accept-Ranges: bytes');
75         *
76         * Multirange content must be sent with multipart/byteranges mediatype,
77         * (mediatype = mimetype)
78         * as well as a boundry header to indicate the various chunks of data.
79         */
80        $response = $this->httpService->response()->withHeader("Accept-Ranges", "0-$length");
81        $this->httpService->saveResponse($response);
82        $server = $this->httpService->request()->getServerParams();
83        // header('Accept-Ranges: bytes');
84        // multipart/byteranges
85        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2
86        if (isset($server['HTTP_RANGE'])) {
87            $c_start = $start;
88            $c_end = $end;
89
90            // Extract the range string
91            list(, $range) = explode('=', $server['HTTP_RANGE'], 2);
92            // Make sure the client hasn't sent us a multibyte range
93            if (strpos($range, ',') !== false) {
94                // (?) Shoud this be issued here, or should the first
95                // range be used? Or should the header be ignored and
96                // we output the whole content?
97                $response = $this->httpService->response()->withStatus(416)->withHeader(ResponseHeader::CONTENT_RANGE, "bytes $start-$end/$size");
98                $this->httpService->saveResponse($response);
99
100                //header("Content-Range: bytes $start-$end/$size");
101                // (?) Echo some info to the client?
102                $this->close();
103            } // fim do if
104            // If the range starts with an '-' we start from the beginning
105            // If not, we forward the file pointer
106            // And make sure to get the end byte if spesified
107            if ($range[0] == '-') {
108                // The n-number of the last bytes is requested
109                $c_start = $size - substr($range, 1);
110            } else {
111                $range = explode('-', $range);
112                $c_start = $range[0];
113                $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size;
114            } // fim do if
115            /* Check the range and make sure it's treated according to the specs.
116             * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
117             */
118            // End bytes can not be larger than $end.
119            $c_end = ($c_end > $end) ? $end : $c_end;
120            // Validate the requested range and return an error if it's not correct.
121            if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) {
122                $response = $this->httpService->response()->withStatus(416)->withHeader(ResponseHeader::CONTENT_RANGE, "bytes $start-$end/$size");
123
124                $this->httpService->saveResponse($response);
125                // (?) Echo some info to the client?
126                $this->close();
127            } // fim do if
128
129            $start = $c_start;
130            $end = $c_end;
131            $length = $end - $start + 1; // Calculate new content length
132            fseek($fp, $start);
133
134            $response = $this->httpService->response()->withStatus(206);
135
136            $this->httpService->saveResponse($response);
137        } // fim do if
138
139        // Notify the client the byte range we'll be outputting
140        $response = $this->httpService->response()->withHeader(ResponseHeader::CONTENT_RANGE, "bytes $start-$end/$size")->withHeader(ResponseHeader::CONTENT_LENGTH, $length);
141
142        $this->httpService->saveResponse($response);
143
144        //render response and start buffered download
145        $this->httpService->sendResponse();
146
147        // Start buffered download
148        $buffer = 1024 * 8;
149        while (!feof($fp) && ($p = ftell($fp)) <= $end) {
150            if ($p + $buffer > $end) {
151                // In case we're only outputtin a chunk, make sure we don't
152                // read past the length
153                $buffer = $end - $p + 1;
154            } // fim do if
155
156            set_time_limit(0); // Reset time limit for big files
157            echo fread($fp, $buffer);
158            flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
159        } // fim do while
160
161        fclose($fp);
162
163        return true;
164    }
165
166
167    /**
168     * @inheritdoc
169     */
170    public function supportsInlineDelivery()
171    {
172        return true;
173    }
174
175
176    /**
177     * @inheritdoc
178     */
179    public function supportsAttachmentDelivery()
180    {
181        return true;
182    }
183
184
185    /**
186     * @inheritdoc
187     */
188    public function supportsStreaming()
189    {
190        return true;
191    }
192
193
194    private function close()
195    {
196        //render response
197        $this->httpService->sendResponse();
198        exit;
199    }
200
201
202    /**
203     * @inheritdoc
204     */
205    public function handleFileDeletion($path_to_file)
206    {
207        return unlink($path_to_file);
208    }
209}
210