Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/files/controller/PhabricatorFileDataController.php
12242 views
1
<?php
2
3
final class PhabricatorFileDataController extends PhabricatorFileController {
4
5
private $phid;
6
private $key;
7
private $file;
8
9
public function shouldRequireLogin() {
10
return false;
11
}
12
13
public function shouldAllowPartialSessions() {
14
return true;
15
}
16
17
public function handleRequest(AphrontRequest $request) {
18
$viewer = $request->getViewer();
19
$this->phid = $request->getURIData('phid');
20
$this->key = $request->getURIData('key');
21
22
$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');
23
$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');
24
$alt_uri = new PhutilURI($alt);
25
$alt_domain = $alt_uri->getDomain();
26
$req_domain = $request->getHost();
27
$main_domain = id(new PhutilURI($base_uri))->getDomain();
28
29
$request_kind = $request->getURIData('kind');
30
$is_download = ($request_kind === 'download');
31
32
if (($alt === null || !strlen($alt)) || $main_domain == $alt_domain) {
33
// No alternate domain.
34
$should_redirect = false;
35
$is_alternate_domain = false;
36
} else if ($req_domain != $alt_domain) {
37
// Alternate domain, but this request is on the main domain.
38
$should_redirect = true;
39
$is_alternate_domain = false;
40
} else {
41
// Alternate domain, and on the alternate domain.
42
$should_redirect = false;
43
$is_alternate_domain = true;
44
}
45
46
$response = $this->loadFile();
47
if ($response) {
48
return $response;
49
}
50
51
$file = $this->getFile();
52
53
if ($should_redirect) {
54
return id(new AphrontRedirectResponse())
55
->setIsExternal(true)
56
->setURI($file->getCDNURI($request_kind));
57
}
58
59
$response = new AphrontFileResponse();
60
$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);
61
$response->setCanCDN($file->getCanCDN());
62
63
$begin = null;
64
$end = null;
65
66
// NOTE: It's important to accept "Range" requests when playing audio.
67
// If we don't, Safari has difficulty figuring out how long sounds are
68
// and glitches when trying to loop them. In particular, Safari sends
69
// an initial request for bytes 0-1 of the audio file, and things go south
70
// if we can't respond with a 206 Partial Content.
71
$range = $request->getHTTPHeader('range');
72
if ($range !== null && strlen($range)) {
73
list($begin, $end) = $response->parseHTTPRange($range);
74
}
75
76
if (!$file->isViewableInBrowser()) {
77
$is_download = true;
78
}
79
80
$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');
81
$is_lfs = ($request_type == 'git-lfs');
82
83
if (!$is_download) {
84
$response->setMimeType($file->getViewableMimeType());
85
} else {
86
$is_post = $request->isHTTPPost();
87
$is_public = !$viewer->isLoggedIn();
88
89
// NOTE: Require POST to download files from the primary domain. If the
90
// request is not a POST request but arrives on the primary domain, we
91
// render a confirmation dialog. For discussion, see T13094.
92
93
// There are two exceptions to this rule:
94
95
// Git LFS requests can download with GET. This is safe (Git LFS won't
96
// execute files it downloads) and necessary to support Git LFS.
97
98
// Requests with no credentials may also download with GET. This
99
// primarily supports downloading files with `arc download` or other
100
// API clients. This is only "mostly" safe: if you aren't logged in, you
101
// are likely immune to XSS and CSRF. However, an attacker may still be
102
// able to set cookies on this domain (for example, to fixate your
103
// session). For now, we accept these risks because users running
104
// Phabricator in this mode are knowingly accepting a security risk
105
// against setup advice, and there's significant value in having
106
// API development against test and production installs work the same
107
// way.
108
109
$is_safe = ($is_alternate_domain || $is_post || $is_lfs || $is_public);
110
if (!$is_safe) {
111
return $this->newDialog()
112
->setSubmitURI($file->getDownloadURI())
113
->setTitle(pht('Download File'))
114
->appendParagraph(
115
pht(
116
'Download file %s (%s)?',
117
phutil_tag('strong', array(), $file->getName()),
118
phutil_format_bytes($file->getByteSize())))
119
->addCancelButton($file->getURI())
120
->addSubmitButton(pht('Download File'));
121
}
122
123
$response->setMimeType($file->getMimeType());
124
$response->setDownload($file->getName());
125
}
126
127
$iterator = $file->getFileDataIterator($begin, $end);
128
129
$response->setContentLength($file->getByteSize());
130
$response->setContentIterator($iterator);
131
132
// In Chrome, we must permit this domain in "object-src" CSP when serving a
133
// PDF or the browser will refuse to render it.
134
if (!$is_download && $file->isPDF()) {
135
$request_uri = id(clone $request->getAbsoluteRequestURI())
136
->setPath(null)
137
->setFragment(null)
138
->removeAllQueryParams();
139
140
$response->addContentSecurityPolicyURI(
141
'object-src',
142
(string)$request_uri);
143
}
144
145
if ($this->shouldCompressFileDataResponse($file)) {
146
$response->setCompressResponse(true);
147
}
148
149
return $response;
150
}
151
152
private function loadFile() {
153
// Access to files is provided by knowledge of a per-file secret key in
154
// the URI. Knowledge of this secret is sufficient to retrieve the file.
155
156
// For some requests, we also have a valid viewer. However, for many
157
// requests (like alternate domain requests or Git LFS requests) we will
158
// not. Even if we do have a valid viewer, use the omnipotent viewer to
159
// make this logic simpler and more consistent.
160
161
// Beyond making the policy check itself more consistent, this also makes
162
// sure we're consistent about returning HTTP 404 on bad requests instead
163
// of serving HTTP 200 with a login page, which can mislead some clients.
164
165
$viewer = PhabricatorUser::getOmnipotentUser();
166
167
$file = id(new PhabricatorFileQuery())
168
->setViewer($viewer)
169
->withPHIDs(array($this->phid))
170
->withIsDeleted(false)
171
->executeOne();
172
173
if (!$file) {
174
return new Aphront404Response();
175
}
176
177
// We may be on the CDN domain, so we need to use a fully-qualified URI
178
// here to make sure we end up back on the main domain.
179
$info_uri = PhabricatorEnv::getURI($file->getInfoURI());
180
181
182
if (!$file->validateSecretKey($this->key)) {
183
$dialog = $this->newDialog()
184
->setTitle(pht('Invalid Authorization'))
185
->appendParagraph(
186
pht(
187
'The link you followed to access this file is no longer '.
188
'valid. The visibility of the file may have changed after '.
189
'the link was generated.'))
190
->appendParagraph(
191
pht(
192
'You can continue to the file detail page to get more '.
193
'information and attempt to access the file.'))
194
->addCancelButton($info_uri, pht('Continue'));
195
196
return id(new AphrontDialogResponse())
197
->setDialog($dialog)
198
->setHTTPResponseCode(404);
199
}
200
201
if ($file->getIsPartial()) {
202
$dialog = $this->newDialog()
203
->setTitle(pht('Partial Upload'))
204
->appendParagraph(
205
pht(
206
'This file has only been partially uploaded. It must be '.
207
'uploaded completely before you can download it.'))
208
->appendParagraph(
209
pht(
210
'You can continue to the file detail page to monitor the '.
211
'upload progress of the file.'))
212
->addCancelButton($info_uri, pht('Continue'));
213
214
return id(new AphrontDialogResponse())
215
->setDialog($dialog)
216
->setHTTPResponseCode(404);
217
}
218
219
$this->file = $file;
220
221
return null;
222
}
223
224
private function getFile() {
225
if (!$this->file) {
226
throw new PhutilInvalidStateException('loadFile');
227
}
228
return $this->file;
229
}
230
231
private function shouldCompressFileDataResponse(PhabricatorFile $file) {
232
// If the client sends "Accept-Encoding: gzip", we have the option of
233
// compressing the response.
234
235
// We generally expect this to be a good idea if the file compresses well,
236
// but maybe not such a great idea if the file is already compressed (like
237
// an image or video) or compresses poorly: the CPU cost of compressing and
238
// decompressing the stream may exceed the bandwidth savings during
239
// transfer.
240
241
// Ideally, we'd probably make this decision by compressing files when
242
// they are uploaded, storing the compressed size, and then doing a test
243
// here using the compression savings and estimated transfer speed.
244
245
// For now, just guess that we shouldn't compress images or videos or
246
// files that look like they are already compressed, and should compress
247
// everything else.
248
249
if ($file->isViewableImage()) {
250
return false;
251
}
252
253
if ($file->isAudio()) {
254
return false;
255
}
256
257
if ($file->isVideo()) {
258
return false;
259
}
260
261
$compressed_types = array(
262
'application/x-gzip',
263
'application/x-compress',
264
'application/x-compressed',
265
'application/x-zip-compressed',
266
'application/zip',
267
);
268
$compressed_types = array_fuse($compressed_types);
269
270
$mime_type = $file->getMimeType();
271
if (isset($compressed_types[$mime_type])) {
272
return false;
273
}
274
275
return true;
276
}
277
278
}
279
280