ILIAS  trunk Revision v13.0_alpha-62-g3b72d83f2e5
StreamDelivery.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
32
36final class StreamDelivery extends BaseDelivery
37{
41 public const SUBREQUEST_SEPARATOR = '/-/';
42
43 public function __construct(
44 private DataSigning $data_signer,
46 ResponseBuilder $response_builder,
47 ResponseBuilder $fallback_response_builder,
48 IsolationConfig $isolation = new IsolationConfig(false, null, null),
49 ) {
50 parent::__construct($http, $response_builder, $fallback_response_builder, $isolation);
51 }
52
53 public function attached(
54 FileStream $stream,
56 ?string $mime_type = null
57 ): never {
58 $this->deliver(
59 $stream,
62 Disposition::ATTACHMENT
63 );
64 }
65
66 public function inline(
67 FileStream $stream,
69 ?string $mime_type = null
70 ): never {
71 $this->deliver(
72 $stream,
75 Disposition::INLINE
76 );
77 }
78
79 public function deliver(
80 FileStream $stream,
82 ?string $mime_type = null,
83 Disposition $disposition = Disposition::INLINE
84 ): never {
85 $r = $this->http->response();
86 $uri = $stream->getMetadata()['uri'];
87
88 // Temporary files (e.g. ZIPs built by ILIAS\Filesystem\Util\Archive\Zip) are removed by a
89 // shutdown function as soon as this request exits. Header based delivery (X-Sendfile,
90 // X-Accel-Redirect) opens the file only after PHP has finished, so it would no longer exist.
91 // Such files must be streamed by PHP itself, see https://mantis.ilias.de/view.php?id=48367
92 if (
93 $stream instanceof ZIPStream
94 || $uri === 'php://memory'
95 || $this->isTemporaryFile((string) $uri)
96 ) {
97 $this->response_builder = $this->fallback_response_builder;
98 }
99
100 $r = $this->setGeneralHeaders(
101 $r,
102 $uri,
103 $mime_type ?? mime_content_type($uri),
106 );
107
108 $r = $this->response_builder->buildForStream(
109 $this->http->request(),
110 $r,
111 $stream
112 );
113 $this->saveAndClose($r);
114 }
115
116 private function isTemporaryFile(string $uri): bool
117 {
118 $real_path = realpath($uri);
119 if ($real_path === false) {
120 return false;
121 }
122 $temp_dirs = array_filter([
123 defined('CLIENT_DATA_DIR') ? realpath(\CLIENT_DATA_DIR . '/temp') : false,
124 realpath(sys_get_temp_dir()),
125 ]);
126 foreach ($temp_dirs as $temp_dir) {
127 if (str_starts_with($real_path, rtrim($temp_dir, '/') . '/')) {
128 return true;
129 }
130 }
131 return false;
132 }
133
134 public function deliverFromToken(string $token): never
135 {
136 $r = $this->http->response();
137
138 // check if $token has a sub-request, such as .../index.html
139 $parts = explode(self::SUBREQUEST_SEPARATOR, $token);
140 $sub_request = null;
141 if (count($parts) > 1) {
142 $token = $parts[0];
143 $sub_request = implode('/', array_slice($parts, 1));
144 }
145
146 $payload = $this->data_signer->verifyStreamToken($token);
147
148 // a token signed for the ILIAS domain is served there only, all others only on the content domain
149 $isolated = !$payload instanceof ShortFilePayload || $payload->isIsolated();
150 $host_allowed = $isolated ? $this->isRequestHostAllowed() : !$this->isRequestOnContentHost();
151 if (!$host_allowed) {
152 $this->notFound($r);
153 }
154
155 switch (true) {
156 case $payload instanceof FilePayload:
157 $uri = $payload->getUri();
158 $mime_type = $payload->getMimeType();
159 $file_name = $payload->getFilename();
160 $disposition = Disposition::tryFrom($payload->getDisposition()) ?? Disposition::INLINE;
161 break;
162 case $payload instanceof ShortFilePayload:
163 $uri = $payload->getUri();
164 $mime_type = $this->determineMimeType($uri);
165 $file_name = $payload->getFilename();
166 $disposition = Disposition::INLINE;
167 break;
168 default:
169 $this->notFound($r);
170 }
171 unset($payload);
172
173 // handle direct access to file
174
175 if ($sub_request === null) {
176 $r = $this->setGeneralHeaders(
177 $r,
178 $uri,
179 $mime_type,
180 $file_name,
181 $disposition,
182 $isolated
183 );
184
185 $this->http->saveResponse(
186 $this->response_builder->buildForStream(
187 $this->http->request(),
188 $r,
189 Streams::ofResource(fopen($uri, 'rb'))
190 )
191 );
192 } else { // handle subrequest, aka file in a ZIP
193 $requested_zip = $uri;
194 $sub_request = urldecode($sub_request);
195 // remove query
196 $sub_request = explode('?', $sub_request)[0];
197
198 try {
199 $file_inside_ZIP = Streams::ofFileInsideZIP($requested_zip, $sub_request);
200 } catch (\Throwable) {
201 $this->notFound($r);
202 }
203 $file_inside_zip_uri = $file_inside_ZIP->getMetadata()['uri'];
204 $file_inside_zip_stream = fopen($file_inside_zip_uri, 'rb');
205
206 if ($file_inside_zip_stream === false) {
207 $this->notFound($r);
208 }
209
210 // we must use PHPResponseBuilder here, because the streams inside zips cant be delivered using XSendFile or others
211 $this->response_builder = $this->fallback_response_builder;
212
213 $mime_type = $this->determineMimeType($file_inside_zip_uri);
214 $r = $this->setGeneralHeaders(
215 $r,
216 $file_inside_zip_uri,
217 $mime_type,
218 basename($sub_request),
219 Disposition::INLINE, // subrequests are always inline per default, browsers may change this to download
220 $isolated
221 );
222
223
224 $this->http->saveResponse(
225 $this->response_builder->buildForStream(
226 $this->http->request(),
227 $r,
228 $file_inside_ZIP
229 )
230 );
231 }
232 $this->http->sendResponse();
233 $this->http->close();
234 }
235
236 private function determineMimeType(string $filename): string
237 {
238 $suffix = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
239 if (isset($this->mime_type_map[$suffix])) {
240 if (is_array($this->mime_type_map[$suffix]) && isset($this->mime_type_map[$suffix][0])) {
241 return $this->mime_type_map[$suffix][0];
242 }
243
244 return $this->mime_type_map[$suffix];
245 }
246
247 $mime_type = mime_content_type($filename);
248 if ($mime_type === 'application/octet-stream') {
249 $mime_type = mime_content_type(substr($filename, 64));
250 }
251 return $mime_type ?: 'application/octet-stream';
252 }
253}
$filename
Definition: buildRTE.php:78
deliver(FileStream $stream, string $download_file_name, ?string $mime_type=null, Disposition $disposition=Disposition::INLINE)
__construct(private DataSigning $data_signer, GlobalHttpState $http, ResponseBuilder $response_builder, ResponseBuilder $fallback_response_builder, IsolationConfig $isolation=new IsolationConfig(false, null, null),)
attached(FileStream $stream, string $download_file_name, ?string $mime_type=null)
Read-only configuration of the IRSS User Content Isolation feature.
isIsolated()
Whether the file is delivered from the content domain when User Content Isolation is active (the defa...
Stream factory which enables the user to create streams without the knowledge of the concrete class.
Definition: Streams.php:32
const CLIENT_DATA_DIR
Definition: constants.php:46
The base interface for all filesystem streams.
Definition: FileStream.php:32
Interface GlobalHttpState.
static http()
Fetches the global http state from ILIAS.
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
$token
Definition: xapitoken.php:69