diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ 'conpherence.pkg.css' => 'e68cf1fa', 'conpherence.pkg.js' => '15191c65', 'core.pkg.css' => '2fa91e14', - 'core.pkg.js' => '7aa5bd92', + 'core.pkg.js' => 'e7ce7bba', 'darkconsole.pkg.js' => '1f9a31bc', 'differential.pkg.css' => '113e692c', 'differential.pkg.js' => 'f6d809c0', @@ -255,7 +255,7 @@ 'rsrc/externals/javelin/lib/URI.js' => 'c989ade3', 'rsrc/externals/javelin/lib/Vector.js' => '2caa8fb8', 'rsrc/externals/javelin/lib/WebSocket.js' => '3ffe32d6', - 'rsrc/externals/javelin/lib/Workflow.js' => '1e911d0f', + 'rsrc/externals/javelin/lib/Workflow.js' => '0eb1db0c', 'rsrc/externals/javelin/lib/__tests__/Cookie.js' => '5ed109e8', 'rsrc/externals/javelin/lib/__tests__/DOM.js' => 'c984504b', 'rsrc/externals/javelin/lib/__tests__/JSON.js' => '837a7d68', @@ -757,7 +757,7 @@ 'javelin-workboard-card' => 'c587b80f', 'javelin-workboard-column' => '758b4758', 'javelin-workboard-controller' => '26167537', - 'javelin-workflow' => '1e911d0f', + 'javelin-workflow' => '0eb1db0c', 'maniphest-report-css' => '9b9580b7', 'maniphest-task-edit-css' => 'fda62a9b', 'maniphest-task-summary-css' => '11cc5344', @@ -977,6 +977,17 @@ 'javelin-dom', 'javelin-router', ), + '0eb1db0c' => array( + 'javelin-stratcom', + 'javelin-request', + 'javelin-dom', + 'javelin-vector', + 'javelin-install', + 'javelin-util', + 'javelin-mask', + 'javelin-uri', + 'javelin-routable', + ), '0f764c35' => array( 'javelin-install', 'javelin-util', @@ -1035,17 +1046,6 @@ 'javelin-request', 'javelin-uri', ), - '1e911d0f' => array( - 'javelin-stratcom', - 'javelin-request', - 'javelin-dom', - 'javelin-vector', - 'javelin-install', - 'javelin-util', - 'javelin-mask', - 'javelin-uri', - 'javelin-routable', - ), '1f6794f6' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/aphront/response/AphrontRedirectResponse.php b/src/aphront/response/AphrontRedirectResponse.php --- a/src/aphront/response/AphrontRedirectResponse.php +++ b/src/aphront/response/AphrontRedirectResponse.php @@ -8,6 +8,7 @@ private $uri; private $stackWhenCreated; private $isExternal; + private $closeDialogBeforeRedirect; public function setIsExternal($external) { $this->isExternal = $external; @@ -37,6 +38,15 @@ return PhabricatorEnv::getEnvConfig('debug.stop-on-redirect'); } + public function setCloseDialogBeforeRedirect($close) { + $this->closeDialogBeforeRedirect = $close; + return $this; + } + + public function getCloseDialogBeforeRedirect() { + return $this->closeDialogBeforeRedirect; + } + public function getHeaders() { $headers = array(); if (!$this->shouldStopForDebugging()) { diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -147,6 +147,13 @@ // Block relics of the old world: Flash, Java applets, and so on. $csp[] = "object-src 'none'"; + // Don't allow forms to submit offsite. + + // This can result in some trickiness with file downloads if applications + // try to start downloads by submitting a dialog. Redirect to the file's + // download URI instead of submitting a form to it. + $csp[] = "form-action 'self'"; + $csp = implode('; ', $csp); return $csp; diff --git a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php --- a/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php +++ b/src/applications/auth/controller/PhabricatorAuthSSHKeyGenerateController.php @@ -24,10 +24,12 @@ $keys = PhabricatorSSHKeyGenerator::generateKeypair(); list($public_key, $private_key) = $keys; + $key_name = $default_name.'.key'; + $file = PhabricatorFile::newFromFileData( $private_key, array( - 'name' => $default_name.'.key', + 'name' => $key_name, 'ttl.relative' => phutil_units('10 minutes in seconds'), 'viewPolicy' => $viewer->getPHID(), )); @@ -62,25 +64,33 @@ ->setContentSourceFromRequest($request) ->applyTransactions($key, $xactions); - // NOTE: We're disabling workflow on submit so the download works. We're - // disabling workflow on cancel so the page reloads, showing the new - // key. + $download_link = phutil_tag( + 'a', + array( + 'href' => $file->getDownloadURI(), + ), + array( + id(new PHUIIconView())->setIcon('fa-download'), + ' ', + pht('Download Private Key (%s)', $key_name), + )); + $download_link = phutil_tag('strong', array(), $download_link); + + // NOTE: We're disabling workflow on cancel so the page reloads, showing + // the new key. return $this->newDialog() ->setTitle(pht('Download Private Key')) - ->setDisableWorkflowOnCancel(true) - ->setDisableWorkflowOnSubmit(true) - ->setSubmitURI($file->getDownloadURI()) ->appendParagraph( pht( 'A keypair has been generated, and the public key has been '. - 'added as a recognized key. Use the button below to download '. - 'the private key.')) + 'added as a recognized key.')) + ->appendParagraph($download_link) ->appendParagraph( pht( 'After you download the private key, it will be destroyed. '. 'You will not be able to retrieve it if you lose your copy.')) - ->addSubmitButton(pht('Download Private Key')) + ->setDisableWorkflowOnCancel(true) ->addCancelButton($cancel_uri, pht('Done')); } diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -298,6 +298,7 @@ ->setContent( array( 'redirect' => $response->getURI(), + 'close' => $response->getCloseDialogBeforeRedirect(), )); } } diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -1046,7 +1046,7 @@ // $no_authorization = 'Basic '.base64_encode('none'); - $get_uri = $file->getCDNURI(); + $get_uri = $file->getCDNURI('data'); $actions['download'] = array( 'href' => $get_uri, 'header' => array( diff --git a/src/applications/files/application/PhabricatorFilesApplication.php b/src/applications/files/application/PhabricatorFilesApplication.php --- a/src/applications/files/application/PhabricatorFilesApplication.php +++ b/src/applications/files/application/PhabricatorFilesApplication.php @@ -101,7 +101,7 @@ private function getResourceSubroutes() { return array( - 'data/'. + '(?Pdata|download)/'. '(?:@(?P[^/]+)/)?'. '(?P[^/]+)/'. '(?P[^/]+)/'. @@ -132,7 +132,7 @@ public function getQuicksandURIPatternBlacklist() { return array( - '/file/data/.*', + '/file/(data|download)/.*', ); } diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -26,6 +26,9 @@ $req_domain = $request->getHost(); $main_domain = id(new PhutilURI($base_uri))->getDomain(); + $request_kind = $request->getURIData('kind'); + $is_download = ($request_kind === 'download'); + if (!strlen($alt) || $main_domain == $alt_domain) { // No alternate domain. $should_redirect = false; @@ -50,7 +53,7 @@ if ($should_redirect) { return id(new AphrontRedirectResponse()) ->setIsExternal(true) - ->setURI($file->getCDNURI()); + ->setURI($file->getCDNURI($request_kind)); } $response = new AphrontFileResponse(); @@ -71,34 +74,30 @@ } $is_viewable = $file->isViewableInBrowser(); - $force_download = $request->getExists('download'); - $request_type = $request->getHTTPHeader('X-Phabricator-Request-Type'); $is_lfs = ($request_type == 'git-lfs'); - if ($is_viewable && !$force_download) { + if ($is_viewable && !$is_download) { $response->setMimeType($file->getViewableMimeType()); } else { - $is_public = !$viewer->isLoggedIn(); $is_post = $request->isHTTPPost(); - // NOTE: Require POST to download files from the primary domain if the - // request includes credentials. The "Download File" links we generate - // in the web UI are forms which use POST to satisfy this requirement. - - // The intent is to make attacks based on tags like "