diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php index 9e5bc50b48..bf58421c68 100644 --- a/src/aphront/response/AphrontFileResponse.php +++ b/src/aphront/response/AphrontFileResponse.php @@ -1,93 +1,142 @@ allowOrigins[] = $origin; return $this; } public function setDownload($download) { if (!strlen($download)) { $download = 'untitled'; } $this->download = $download; return $this; } public function getDownload() { return $this->download; } public function setMimeType($mime_type) { $this->mimeType = $mime_type; return $this; } public function getMimeType() { return $this->mimeType; } public function setContent($content) { + $this->setContentLength(strlen($content)); $this->content = $content; return $this; } + public function setContentIterator($iterator) { + $this->contentIterator = $iterator; + return $this; + } + public function buildResponseString() { - if ($this->rangeMin || $this->rangeMax) { - $length = ($this->rangeMax - $this->rangeMin) + 1; - return substr($this->content, $this->rangeMin, $length); - } else { - return $this->content; + return $this->content; + } + + public function getContentIterator() { + if ($this->contentIterator) { + return $this->contentIterator; } + return parent::getContentIterator(); + } + + public function setContentLength($length) { + $this->contentLength = $length; + return $this; + } + + public function getContentLength() { + return $this->contentLength; } public function setRange($min, $max) { $this->rangeMin = $min; $this->rangeMax = $max; return $this; } + public function setTemporaryFileToken(PhabricatorAuthTemporaryToken $token) { + $this->fileToken = $token; + return $this; + } + + public function getTemporaryFileToken() { + return $this->fileToken; + } + public function getHeaders() { $headers = array( array('Content-Type', $this->getMimeType()), - array('Content-Length', strlen($this->buildResponseString())), + // This tells clients that we can support requests with a "Range" header, + // which allows downloads to be resumed, in some browsers, some of the + // time, if the stars align. + array('Accept-Ranges', 'bytes'), ); if ($this->rangeMin || $this->rangeMax) { - $len = strlen($this->content); + $len = $this->getContentLength(); $min = $this->rangeMin; $max = $this->rangeMax; $headers[] = array('Content-Range', "bytes {$min}-{$max}/{$len}"); + $content_len = ($max - $min) + 1; + } else { + $content_len = $this->getContentLength(); } + $headers[] = array('Content-Length', $this->getContentLength()); + if (strlen($this->getDownload())) { $headers[] = array('X-Download-Options', 'noopen'); $filename = $this->getDownload(); $filename = addcslashes($filename, '"\\'); $headers[] = array( 'Content-Disposition', 'attachment; filename="'.$filename.'"', ); } if ($this->allowOrigins) { $headers[] = array( 'Access-Control-Allow-Origin', implode(',', $this->allowOrigins), ); } $headers = array_merge(parent::getHeaders(), $headers); return $headers; } + public function didCompleteWrite($aborted) { + if (!$aborted) { + $token = $this->getTemporaryFileToken(); + if ($token) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $token->delete(); + unset($unguarded); + } + } + } + } diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 27c603b091..4cf420f066 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -1,170 +1,188 @@ request = $request; return $this; } public function getRequest() { return $this->request; } + +/* -( Content )------------------------------------------------------------ */ + + + public function getContentIterator() { + return array($this->buildResponseString()); + } + + public function buildResponseString() { + throw new PhutilMethodNotImplementedException(); + } + + +/* -( Metadata )----------------------------------------------------------- */ + + public function getHeaders() { $headers = array(); if (!$this->frameable) { $headers[] = array('X-Frame-Options', 'Deny'); } if ($this->getRequest() && $this->getRequest()->isHTTPS()) { $hsts_key = 'security.strict-transport-security'; $use_hsts = PhabricatorEnv::getEnvConfig($hsts_key); if ($use_hsts) { $duration = phutil_units('365 days in seconds'); } else { // If HSTS has been disabled, tell browsers to turn it off. This may // not be effective because we can only disable it over a valid HTTPS // connection, but it best represents the configured intent. $duration = 0; } $headers[] = array( 'Strict-Transport-Security', "max-age={$duration}; includeSubdomains; preload", ); } return $headers; } public function setCacheDurationInSeconds($duration) { $this->cacheable = $duration; return $this; } public function setLastModified($epoch_timestamp) { $this->lastModified = $epoch_timestamp; return $this; } public function setHTTPResponseCode($code) { $this->responseCode = $code; return $this; } public function getHTTPResponseCode() { return $this->responseCode; } public function getHTTPResponseMessage() { return ''; } public function setFrameable($frameable) { $this->frameable = $frameable; return $this; } public static function processValueForJSONEncoding(&$value, $key) { if ($value instanceof PhutilSafeHTMLProducerInterface) { // This renders the producer down to PhutilSafeHTML, which will then // be simplified into a string below. $value = hsprintf('%s', $value); } if ($value instanceof PhutilSafeHTML) { // TODO: Javelin supports implicity conversion of '__html' objects to // JX.HTML, but only for Ajax responses, not behaviors. Just leave things // as they are for now (where behaviors treat responses as HTML or plain // text at their discretion). $value = $value->getHTMLContent(); } } public static function encodeJSONForHTTPResponse(array $object) { array_walk_recursive( $object, array('AphrontResponse', 'processValueForJSONEncoding')); $response = json_encode($object); // Prevent content sniffing attacks by encoding "<" and ">", so browsers // won't try to execute the document as HTML even if they ignore // Content-Type and X-Content-Type-Options. See T865. $response = str_replace( array('<', '>'), array('\u003c', '\u003e'), $response); return $response; } protected function addJSONShield($json_response) { // Add a shield to prevent "JSON Hijacking" attacks where an attacker // requests a JSON response using a normal