diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php index 01288c50a4..08f3b82cb5 100644 --- a/src/aphront/response/AphrontFileResponse.php +++ b/src/aphront/response/AphrontFileResponse.php @@ -8,6 +8,8 @@ final class AphrontFileResponse extends AphrontResponse { private $content; private $mimeType; private $download; + private $rangeMin; + private $rangeMax; public function setDownload($download) { $download = preg_replace('/[^A-Za-z0-9_.-]/', '_', $download); @@ -37,15 +39,33 @@ final class AphrontFileResponse extends AphrontResponse { } public function buildResponseString() { - return $this->content; + if ($this->rangeMin || $this->rangeMax) { + $length = ($this->rangeMax - $this->rangeMin) + 1; + return substr($this->content, $this->rangeMin, $length); + } else { + return $this->content; + } + } + + public function setRange($min, $max) { + $this->rangeMin = $min; + $this->rangeMax = $max; + return $this; } public function getHeaders() { $headers = array( array('Content-Type', $this->getMimeType()), - array('Content-Length', strlen($this->content)), + array('Content-Length', strlen($this->buildResponseString())), ); + if ($this->rangeMin || $this->rangeMax) { + $len = strlen($this->content); + $min = $this->rangeMin; + $max = $this->rangeMax; + $headers[] = array('Content-Range', "bytes {$min}-{$max}/{$len}"); + } + if (strlen($this->getDownload())) { $headers[] = array('X-Download-Options', 'noopen'); diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index fb3189c72c..8423277955 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -41,6 +41,20 @@ final class PhabricatorFileDataController extends PhabricatorFileController { $response->setContent($data); $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]); + } + } + $is_viewable = $file->isViewableInBrowser(); $force_download = $request->getExists('download'); diff --git a/src/applications/macro/remarkup/PhabricatorRemarkupRuleImageMacro.php b/src/applications/macro/remarkup/PhabricatorRemarkupRuleImageMacro.php index 25c6ce3fc0..9fd17a5cd4 100644 --- a/src/applications/macro/remarkup/PhabricatorRemarkupRuleImageMacro.php +++ b/src/applications/macro/remarkup/PhabricatorRemarkupRuleImageMacro.php @@ -6,7 +6,9 @@ final class PhabricatorRemarkupRuleImageMacro extends PhutilRemarkupRule { - private $images; + private $macros; + + const KEY_RULE_MACRO = 'rule.macro'; public function apply($text) { return preg_replace_callback( @@ -16,102 +18,149 @@ final class PhabricatorRemarkupRuleImageMacro } public function markupImageMacro($matches) { - if ($this->images === null) { - $this->images = array(); + if ($this->macros === null) { + $this->macros = array(); $viewer = $this->getEngine()->getConfig('viewer'); $rows = id(new PhabricatorMacroQuery()) ->setViewer($viewer) ->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE) ->execute(); - foreach ($rows as $row) { - $spec = array( - 'image' => $row->getFilePHID(), - ); - $behavior_none = PhabricatorFileImageMacro::AUDIO_BEHAVIOR_NONE; - if ($row->getAudioPHID()) { - if ($row->getAudioBehavior() != $behavior_none) { - $spec += array( - 'audio' => $row->getAudioPHID(), - 'audioBehavior' => $row->getAudioBehavior(), - ); - } - } - - $this->images[$row->getName()] = $spec; - } + $this->macros = mpull($rows, 'getPHID', 'getName'); } $name = (string)$matches[1]; + if (empty($this->macros[$name])) { + return $matches[1]; + } - if (array_key_exists($name, $this->images)) { - $phid = $this->images[$name]['image']; + $engine = $this->getEngine(); - $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $phid); - if ($this->getEngine()->isTextMode()) { + $metadata_key = self::KEY_RULE_MACRO; + $metadata = $engine->getTextMetadata($metadata_key, array()); + + $token = $engine->storeText(''); + $metadata[] = array( + 'token' => $token, + 'phid' => $this->macros[$name], + 'original' => $name, + ); + + $engine->setTextMetadata($metadata_key, $metadata); + + return $token; + } + + public function didMarkupText() { + $engine = $this->getEngine(); + $metadata_key = self::KEY_RULE_MACRO; + $metadata = $engine->getTextMetadata($metadata_key, array()); + + if (!$metadata) { + return; + } + + $phids = ipull($metadata, 'phid'); + $viewer = $this->getEngine()->getConfig('viewer'); + + // Load all the macros. + $macros = id(new PhabricatorMacroQuery()) + ->setViewer($viewer) + ->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE) + ->withPHIDs($phids) + ->execute(); + $macros = mpull($macros, null, 'getPHID'); + + // Load all the images and audio. + $file_phids = array_merge( + array_values(mpull($macros, 'getFilePHID')), + array_values(mpull($macros, 'getAudioPHID'))); + + $file_phids = array_filter($file_phids); + + $files = array(); + if ($file_phids) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($file_phids) + ->execute(); + $files = mpull($files, null, 'getPHID'); + } + + // Replace any macros that we couldn't load the macro or image for with + // the original text. + foreach ($metadata as $key => $spec) { + $macro = idx($macros, $spec['phid']); + if ($macro) { + $file = idx($files, $macro->getFilePHID()); if ($file) { - $name .= ' <'.$file->getBestURI().'>'; + continue; } - return $this->getEngine()->storeText($name); } + $engine->overwriteStoredText($spec['token'], $spec['original']); + unset($metadata[$key]); + } + + foreach ($metadata as $spec) { + $macro = $macros[$spec['phid']]; + $file = $files[$macro->getFilePHID()]; + $src_uri = $file->getBestURI(); + + if ($this->getEngine()->isTextMode()) { + $result = $spec['original'].' <'.$src_uri.'>'; + $engine->overwriteStoredText($spec['token'], $result); + continue; + } + + $file_data = $file->getMetadata(); $style = null; - $src_uri = null; - if ($file) { - $src_uri = $file->getBestURI(); - $file_data = $file->getMetadata(); - $height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT); - $width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH); - if ($height && $width) { - $style = sprintf( - 'height: %dpx; width: %dpx;', - $height, - $width); - } + $height = idx($file_data, PhabricatorFile::METADATA_IMAGE_HEIGHT); + $width = idx($file_data, PhabricatorFile::METADATA_IMAGE_WIDTH); + if ($height && $width) { + $style = sprintf( + 'height: %dpx; width: %dpx;', + $height, + $width); } $id = null; - $audio_phid = idx($this->images[$name], 'audio'); - if ($audio_phid) { + $audio = idx($files, $macro->getAudioPHID()); + if ($audio) { $id = celerity_generate_unique_node_id(); $loop = null; - switch (idx($this->images[$name], 'audioBehavior')) { + switch ($macro->getAudioBehavior()) { case PhabricatorFileImageMacro::AUDIO_BEHAVIOR_LOOP: $loop = true; break; } - $file = id(new PhabricatorFile())->loadOneWhere( - 'phid = %s', - $audio_phid); - if ($file) { - Javelin::initBehavior( - 'audio-source', - array( - 'sourceID' => $id, - 'audioURI' => $file->getBestURI(), - 'loop' => $loop, - )); - } + Javelin::initBehavior( + 'audio-source', + array( + 'sourceID' => $id, + 'audioURI' => $audio->getBestURI(), + 'loop' => $loop, + )); } - $img = phutil_tag( + $result = phutil_tag( 'img', array( 'id' => $id, 'src' => $src_uri, - 'alt' => $matches[1], - 'title' => $matches[1], + 'alt' => $spec['original'], + 'title' => $spec['original'], 'style' => $style, )); - return $this->getEngine()->storeText($img); - } else { - return $matches[1]; + $engine->overwriteStoredText($spec['token'], $result); } + + $engine->setTextMetadata($metadata_key, array()); } }