Path: blob/master/src/applications/files/controller/PhabricatorFileDataController.php
12242 views
<?php12final class PhabricatorFileDataController extends PhabricatorFileController {34private $phid;5private $key;6private $file;78public function shouldRequireLogin() {9return false;10}1112public function shouldAllowPartialSessions() {13return true;14}1516public function handleRequest(AphrontRequest $request) {17$viewer = $request->getViewer();18$this->phid = $request->getURIData('phid');19$this->key = $request->getURIData('key');2021$alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain');22$base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri');23$alt_uri = new PhutilURI($alt);24$alt_domain = $alt_uri->getDomain();25$req_domain = $request->getHost();26$main_domain = id(new PhutilURI($base_uri))->getDomain();2728$request_kind = $request->getURIData('kind');29$is_download = ($request_kind === 'download');3031if (($alt === null || !strlen($alt)) || $main_domain == $alt_domain) {32// No alternate domain.33$should_redirect = false;34$is_alternate_domain = false;35} else if ($req_domain != $alt_domain) {36// Alternate domain, but this request is on the main domain.37$should_redirect = true;38$is_alternate_domain = false;39} else {40// Alternate domain, and on the alternate domain.41$should_redirect = false;42$is_alternate_domain = true;43}4445$response = $this->loadFile();46if ($response) {47return $response;48}4950$file = $this->getFile();5152if ($should_redirect) {53return id(new AphrontRedirectResponse())54->setIsExternal(true)55->setURI($file->getCDNURI($request_kind));56}5758$response = new AphrontFileResponse();59$response->setCacheDurationInSeconds(60 * 60 * 24 * 30);60$response->setCanCDN($file->getCanCDN());6162$begin = null;63$end = null;6465// NOTE: It's important to accept "Range" requests when playing audio.66// If we don't, Safari has difficulty figuring out how long sounds are67// and glitches when trying to loop them. In particular, Safari sends68// an initial request for bytes 0-1 of the audio file, and things go south69// if we can't respond with a 206 Partial Content.70$range = $request->getHTTPHeader('range');71if ($range !== null && strlen($range)) {72list($begin, $end) = $response->parseHTTPRange($range);73}7475if (!$file->isViewableInBrowser()) {76$is_download = true;77}7879$request_type = $request->getHTTPHeader('X-Phabricator-Request-Type');80$is_lfs = ($request_type == 'git-lfs');8182if (!$is_download) {83$response->setMimeType($file->getViewableMimeType());84} else {85$is_post = $request->isHTTPPost();86$is_public = !$viewer->isLoggedIn();8788// NOTE: Require POST to download files from the primary domain. If the89// request is not a POST request but arrives on the primary domain, we90// render a confirmation dialog. For discussion, see T13094.9192// There are two exceptions to this rule:9394// Git LFS requests can download with GET. This is safe (Git LFS won't95// execute files it downloads) and necessary to support Git LFS.9697// Requests with no credentials may also download with GET. This98// primarily supports downloading files with `arc download` or other99// API clients. This is only "mostly" safe: if you aren't logged in, you100// are likely immune to XSS and CSRF. However, an attacker may still be101// able to set cookies on this domain (for example, to fixate your102// session). For now, we accept these risks because users running103// Phabricator in this mode are knowingly accepting a security risk104// against setup advice, and there's significant value in having105// API development against test and production installs work the same106// way.107108$is_safe = ($is_alternate_domain || $is_post || $is_lfs || $is_public);109if (!$is_safe) {110return $this->newDialog()111->setSubmitURI($file->getDownloadURI())112->setTitle(pht('Download File'))113->appendParagraph(114pht(115'Download file %s (%s)?',116phutil_tag('strong', array(), $file->getName()),117phutil_format_bytes($file->getByteSize())))118->addCancelButton($file->getURI())119->addSubmitButton(pht('Download File'));120}121122$response->setMimeType($file->getMimeType());123$response->setDownload($file->getName());124}125126$iterator = $file->getFileDataIterator($begin, $end);127128$response->setContentLength($file->getByteSize());129$response->setContentIterator($iterator);130131// In Chrome, we must permit this domain in "object-src" CSP when serving a132// PDF or the browser will refuse to render it.133if (!$is_download && $file->isPDF()) {134$request_uri = id(clone $request->getAbsoluteRequestURI())135->setPath(null)136->setFragment(null)137->removeAllQueryParams();138139$response->addContentSecurityPolicyURI(140'object-src',141(string)$request_uri);142}143144if ($this->shouldCompressFileDataResponse($file)) {145$response->setCompressResponse(true);146}147148return $response;149}150151private function loadFile() {152// Access to files is provided by knowledge of a per-file secret key in153// the URI. Knowledge of this secret is sufficient to retrieve the file.154155// For some requests, we also have a valid viewer. However, for many156// requests (like alternate domain requests or Git LFS requests) we will157// not. Even if we do have a valid viewer, use the omnipotent viewer to158// make this logic simpler and more consistent.159160// Beyond making the policy check itself more consistent, this also makes161// sure we're consistent about returning HTTP 404 on bad requests instead162// of serving HTTP 200 with a login page, which can mislead some clients.163164$viewer = PhabricatorUser::getOmnipotentUser();165166$file = id(new PhabricatorFileQuery())167->setViewer($viewer)168->withPHIDs(array($this->phid))169->withIsDeleted(false)170->executeOne();171172if (!$file) {173return new Aphront404Response();174}175176// We may be on the CDN domain, so we need to use a fully-qualified URI177// here to make sure we end up back on the main domain.178$info_uri = PhabricatorEnv::getURI($file->getInfoURI());179180181if (!$file->validateSecretKey($this->key)) {182$dialog = $this->newDialog()183->setTitle(pht('Invalid Authorization'))184->appendParagraph(185pht(186'The link you followed to access this file is no longer '.187'valid. The visibility of the file may have changed after '.188'the link was generated.'))189->appendParagraph(190pht(191'You can continue to the file detail page to get more '.192'information and attempt to access the file.'))193->addCancelButton($info_uri, pht('Continue'));194195return id(new AphrontDialogResponse())196->setDialog($dialog)197->setHTTPResponseCode(404);198}199200if ($file->getIsPartial()) {201$dialog = $this->newDialog()202->setTitle(pht('Partial Upload'))203->appendParagraph(204pht(205'This file has only been partially uploaded. It must be '.206'uploaded completely before you can download it.'))207->appendParagraph(208pht(209'You can continue to the file detail page to monitor the '.210'upload progress of the file.'))211->addCancelButton($info_uri, pht('Continue'));212213return id(new AphrontDialogResponse())214->setDialog($dialog)215->setHTTPResponseCode(404);216}217218$this->file = $file;219220return null;221}222223private function getFile() {224if (!$this->file) {225throw new PhutilInvalidStateException('loadFile');226}227return $this->file;228}229230private function shouldCompressFileDataResponse(PhabricatorFile $file) {231// If the client sends "Accept-Encoding: gzip", we have the option of232// compressing the response.233234// We generally expect this to be a good idea if the file compresses well,235// but maybe not such a great idea if the file is already compressed (like236// an image or video) or compresses poorly: the CPU cost of compressing and237// decompressing the stream may exceed the bandwidth savings during238// transfer.239240// Ideally, we'd probably make this decision by compressing files when241// they are uploaded, storing the compressed size, and then doing a test242// here using the compression savings and estimated transfer speed.243244// For now, just guess that we shouldn't compress images or videos or245// files that look like they are already compressed, and should compress246// everything else.247248if ($file->isViewableImage()) {249return false;250}251252if ($file->isAudio()) {253return false;254}255256if ($file->isVideo()) {257return false;258}259260$compressed_types = array(261'application/x-gzip',262'application/x-compress',263'application/x-compressed',264'application/x-zip-compressed',265'application/zip',266);267$compressed_types = array_fuse($compressed_types);268269$mime_type = $file->getMimeType();270if (isset($compressed_types[$mime_type])) {271return false;272}273274return true;275}276277}278279280