diff --git a/src/applications/files/controller/PhabricatorFileController.php b/src/applications/files/controller/PhabricatorFileController.php index a77525c84f..bbdbd21e07 100644 --- a/src/applications/files/controller/PhabricatorFileController.php +++ b/src/applications/files/controller/PhabricatorFileController.php @@ -1,40 +1,29 @@ addAction( - id(new PHUIListItemView()) - ->setName(pht('Upload File')) - ->setIcon('fa-upload') - ->setHref($this->getApplicationURI('/upload/'))); - - return $crumbs; - } - protected function buildSideNavView() { $menu = $this->buildMenu($for_devices = false); return AphrontSideNavFilterView::newFromMenu($menu); } protected function buildApplicationMenu() { return $this->buildMenu($for_devices = true); } private function buildMenu($for_devices) { $menu = new PHUIListView(); if ($for_devices) { $menu->newLink(pht('Upload File'), $this->getApplicationURI('/upload/')); } id(new PhabricatorFileSearchEngine()) ->setViewer($this->getRequest()->getUser()) ->addNavigationItems($menu); return $menu; } } diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index d6cb0ff0d6..75b62b5dc1 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -1,160 +1,183 @@ phid = $data['phid']; $this->key = $data['key']; $this->token = idx($data, 'token'); } public function shouldRequireLogin() { return false; } protected function checkFileAndToken($file) { if (!$file) { return new Aphront404Response(); } if (!$file->validateSecretKey($this->key)) { return new Aphront403Response(); } return null; } public function processRequest() { $request = $this->getRequest(); $alt = PhabricatorEnv::getEnvConfig('security.alternate-file-domain'); $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $alt_uri = new PhutilURI($alt); $alt_domain = $alt_uri->getDomain(); $req_domain = $request->getHost(); $main_domain = id(new PhutilURI($base_uri))->getDomain(); $cache_response = true; if (empty($alt) || $main_domain == $alt_domain) { // Alternate files domain isn't configured or it's set // to the same as the default domain // load the file with permissions checks; $file = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) ->withPHIDs(array($this->phid)) ->executeOne(); $error_response = $this->checkFileAndToken($file); if ($error_response) { return $error_response; } // when the file is not CDNable, don't allow cache $cache_response = $file->getCanCDN(); } else if ($req_domain != $alt_domain) { // Alternate domain is configured but this request isn't using it // load the file with permissions checks; $file = id(new PhabricatorFileQuery()) ->setViewer($request->getUser()) ->withPHIDs(array($this->phid)) ->executeOne(); $error_response = $this->checkFileAndToken($file); if ($error_response) { return $error_response; } // if the user can see the file, generate a token; // redirect to the alt domain with the token; return id(new AphrontRedirectResponse()) ->setURI($file->getCDNURIWithToken()); } else { // We are using the alternate domain // load the file, bypassing permission checks; $file = id(new PhabricatorFileQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($this->phid)) ->executeOne(); $error_response = $this->checkFileAndToken($file); if ($error_response) { return $error_response; } + $acquire_token_uri = id(new PhutilURI($file->getViewURI())) + ->setDomain($main_domain); + + if ($this->token) { // validate the token, if it is valid, continue $validated_token = $file->validateOneTimeToken($this->token); if (!$validated_token) { - return new Aphront403Response(); + $dialog = $this->newDialog() + ->setShortTitle(pht('Expired File')) + ->setTitle(pht('File Link Has Expired')) + ->appendParagraph( + pht( + 'The link you followed to view this file is invalid or '. + 'expired.')) + ->appendParagraph( + pht( + 'Continue to generate a new link to the file. You may be '. + 'required to log in.')) + ->addCancelButton( + $acquire_token_uri, + pht('Continue')); + + // Build an explicit response so we can respond with HTTP/403 instead + // of HTTP/200. + $response = id(new AphrontDialogResponse()) + ->setDialog($dialog) + ->setHTTPResponseCode(403); + + return $response; } // return the file data without cache headers $cache_response = false; } else if (!$file->getCanCDN()) { // file cannot be served via cdn, and no token given // redirect to the main domain to aquire a token - $file_uri = id(new PhutilURI($file->getViewURI())) - ->setDomain($main_domain); return id(new AphrontRedirectResponse()) - ->setURI($file_uri); + ->setURI($acquire_token_uri); } } $data = $file->loadFileData(); $response = new AphrontFileResponse(); $response->setContent($data); if ($cache_response) { $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); } // NOTE: It's important to accept "Range" requests when playing audio. // If we don't, Safari has difficulty figuring out how long sounds are // and glitches when trying to loop them. In particular, Safari sends // an initial request for bytes 0-1 of the audio file, and things go south // if we can't respond with a 206 Partial Content. $range = $request->getHTTPHeader('range'); if ($range) { $matches = null; if (preg_match('/^bytes=(\d+)-(\d+)$/', $range, $matches)) { $response->setHTTPResponseCode(206); $response->setRange((int)$matches[1], (int)$matches[2]); } } else if (isset($validated_token)) { // consume the one-time token if we have one. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $validated_token->delete(); unset($unguarded); } $is_viewable = $file->isViewableInBrowser(); $force_download = $request->getExists('download'); if ($is_viewable && !$force_download) { $response->setMimeType($file->getViewableMimeType()); } else { if (!$request->isHTTPPost() && !$alt_domain) { // NOTE: Require POST to download files from the primary domain. We'd // rather go full-bore and do a real CSRF check, but can't currently // authenticate users on the file domain. This should blunt any // attacks based on iframes, script tags, applet tags, etc., at least. // Send the user to the "info" page if they're using some other method. return id(new AphrontRedirectResponse()) ->setURI(PhabricatorEnv::getProductionURI($file->getBestURI())); } $response->setMimeType($file->getMimeType()); $response->setDownload($file->getName()); } return $response; } } diff --git a/src/applications/files/controller/PhabricatorFileListController.php b/src/applications/files/controller/PhabricatorFileListController.php index 9cd50ccb97..feea6db3a6 100644 --- a/src/applications/files/controller/PhabricatorFileListController.php +++ b/src/applications/files/controller/PhabricatorFileListController.php @@ -1,25 +1,36 @@ key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); $controller = id(new PhabricatorApplicationSearchController($request)) ->setQueryKey($this->key) ->setSearchEngine(new PhabricatorFileSearchEngine()) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } + public function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Upload File')) + ->setIcon('fa-upload') + ->setHref($this->getApplicationURI('/upload/'))); + + return $crumbs; + } + }