Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/files/config/PhabricatorFilesConfigOptions.php b/src/applications/files/config/PhabricatorFilesConfigOptions.php
index 43e12eddec..063a6d9138 100644
--- a/src/applications/files/config/PhabricatorFilesConfigOptions.php
+++ b/src/applications/files/config/PhabricatorFilesConfigOptions.php
@@ -1,183 +1,216 @@
<?php
final class PhabricatorFilesConfigOptions
extends PhabricatorApplicationConfigOptions {
public function getName() {
return pht('Files');
}
public function getDescription() {
return pht('Configure files and file storage.');
}
public function getIcon() {
return 'fa-file';
}
public function getGroup() {
return 'apps';
}
public function getOptions() {
$viewable_default = array(
'image/jpeg' => 'image/jpeg',
'image/jpg' => 'image/jpg',
'image/png' => 'image/png',
'image/gif' => 'image/gif',
'text/plain' => 'text/plain; charset=utf-8',
'text/x-diff' => 'text/plain; charset=utf-8',
// ".ico" favicon files, which have mime type diversity. See:
// http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type
'image/x-ico' => 'image/x-icon',
'image/x-icon' => 'image/x-icon',
'image/vnd.microsoft.icon' => 'image/x-icon',
- 'audio/x-wav' => 'audio/x-wav',
+ // This is a generic type for both OGG video and OGG audio.
'application/ogg' => 'application/ogg',
- 'audio/mpeg' => 'audio/mpeg',
+
+ 'audio/x-wav' => 'audio/x-wav',
+ 'audio/mpeg' => 'audio/mpeg',
+ 'audio/ogg' => 'audio/ogg',
+
+ 'video/mp4' => 'video/mp4',
+ 'video/ogg' => 'video/ogg',
+ 'video/webm' => 'video/webm',
);
$image_default = array(
'image/jpeg' => true,
'image/jpg' => true,
'image/png' => true,
'image/gif' => true,
'image/x-ico' => true,
'image/x-icon' => true,
'image/vnd.microsoft.icon' => true,
);
+
+ // The "application/ogg" type is listed as both an audio and video type,
+ // because it may contain either type of content.
+
$audio_default = array(
- 'audio/x-wav' => true,
+ 'audio/x-wav' => true,
+ 'audio/mpeg' => true,
+ 'audio/ogg' => true,
+
+ // These are video or ambiguous types, but can be forced to render as
+ // audio with `media=audio`, which seems to work properly in browsers.
+ // (For example, you can embed a music video as audio if you just want
+ // to set the mood for your task without distracting viewers.)
+ 'video/mp4' => true,
+ 'video/ogg' => true,
+ 'application/ogg' => true,
+ );
+
+ $video_default = array(
+ 'video/mp4' => true,
+ 'video/ogg' => true,
+ 'video/webm' => true,
'application/ogg' => true,
- 'audio/mpeg' => true,
);
// largely lifted from http://en.wikipedia.org/wiki/Internet_media_type
$icon_default = array(
// audio file icon
'audio/basic' => 'fa-file-audio-o',
'audio/L24' => 'fa-file-audio-o',
'audio/mp4' => 'fa-file-audio-o',
'audio/mpeg' => 'fa-file-audio-o',
'audio/ogg' => 'fa-file-audio-o',
'audio/vorbis' => 'fa-file-audio-o',
'audio/vnd.rn-realaudio' => 'fa-file-audio-o',
'audio/vnd.wave' => 'fa-file-audio-o',
'audio/webm' => 'fa-file-audio-o',
// movie file icon
'video/mpeg' => 'fa-file-movie-o',
'video/mp4' => 'fa-file-movie-o',
+ 'application/ogg' => 'fa-file-movie-o',
'video/ogg' => 'fa-file-movie-o',
'video/quicktime' => 'fa-file-movie-o',
'video/webm' => 'fa-file-movie-o',
'video/x-matroska' => 'fa-file-movie-o',
'video/x-ms-wmv' => 'fa-file-movie-o',
'video/x-flv' => 'fa-file-movie-o',
// pdf file icon
'application/pdf' => 'fa-file-pdf-o',
// zip file icon
'application/zip' => 'fa-file-zip-o',
// msword icon
'application/msword' => 'fa-file-word-o',
// msexcel
'application/vnd.ms-excel' => 'fa-file-excel-o',
// mspowerpoint
'application/vnd.ms-powerpoint' => 'fa-file-powerpoint-o',
) + array_fill_keys(array_keys($image_default), 'fa-file-image-o');
// NOTE: These options are locked primarily because adding "text/plain"
// as an image MIME type increases SSRF vulnerability by allowing users
// to load text files from remote servers as "images" (see T6755 for
// discussion).
return array(
$this->newOption('files.viewable-mime-types', 'wild', $viewable_default)
->setLocked(true)
->setSummary(
pht('Configure which MIME types are viewable in the browser.'))
->setDescription(
pht(
"Configure which uploaded file types may be viewed directly ".
"in the browser. Other file types will be downloaded instead ".
"of displayed. This is mainly a usability consideration, since ".
"browsers tend to freak out when viewing enormous binary files.".
"\n\n".
"The keys in this map are viewable MIME types; the values are ".
"the MIME types they are delivered as when they are viewed in ".
"the browser.")),
$this->newOption('files.image-mime-types', 'set', $image_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are images.'))
->setDescription(
pht(
'List of MIME types which can be used as the `%s` for an `%s` tag.',
'src',
'<img />')),
$this->newOption('files.audio-mime-types', 'set', $audio_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types are audio.'))
->setDescription(
pht(
- 'List of MIME types which can be used to render an `%s` tag.',
+ 'List of MIME types which can be rendered with an `%s` tag.',
'<audio />')),
+ $this->newOption('files.video-mime-types', 'set', $video_default)
+ ->setSummary(pht('Configure which MIME types are video.'))
+ ->setDescription(
+ pht(
+ 'List of MIME types which can be rendered with a `%s` tag.',
+ '<video />')),
$this->newOption('files.icon-mime-types', 'wild', $icon_default)
->setLocked(true)
->setSummary(pht('Configure which MIME types map to which icons.'))
->setDescription(
pht(
'Map of MIME type to icon name. MIME types which can not be '.
'found default to icon `%s`.',
'doc_files')),
$this->newOption('storage.mysql-engine.max-size', 'int', 1000000)
->setSummary(
pht(
'Configure the largest file which will be put into the MySQL '.
'storage engine.')),
$this->newOption('storage.local-disk.path', 'string', null)
->setLocked(true)
->setSummary(pht('Local storage disk path.'))
->setDescription(
pht(
"Phabricator provides a local disk storage engine, which just ".
"writes files to some directory on local disk. The webserver ".
"must have read/write permissions on this directory. This is ".
"straightforward and suitable for most installs, but will not ".
"scale past one web frontend unless the path is actually an NFS ".
"mount, since you'll end up with some of the files written to ".
"each web frontend and no way for them to share. To use the ".
"local disk storage engine, specify the path to a directory ".
"here. To disable it, specify null.")),
$this->newOption('storage.s3.bucket', 'string', null)
->setSummary(pht('Amazon S3 bucket.'))
->setDescription(
pht(
"Set this to a valid Amazon S3 bucket to store files there. You ".
"must also configure S3 access keys in the 'Amazon Web Services' ".
"group.")),
$this->newOption(
'metamta.files.subject-prefix',
'string',
'[File]')
->setDescription(pht('Subject prefix for Files email.')),
$this->newOption('files.enable-imagemagick', 'bool', false)
->setBoolOptions(
array(
pht('Enable'),
pht('Disable'),
))
->setDescription(
pht(
'This option will use Imagemagick to rescale images, so animated '.
'GIFs can be thumbnailed and set as profile pictures. Imagemagick '.
'must be installed and the "%s" binary must be available to '.
'the webserver for this to work.',
'convert')),
);
}
}
diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php
index 66050b697e..5f60c5ec78 100644
--- a/src/applications/files/controller/PhabricatorFileInfoController.php
+++ b/src/applications/files/controller/PhabricatorFileInfoController.php
@@ -1,377 +1,405 @@
<?php
final class PhabricatorFileInfoController extends PhabricatorFileController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$id = $request->getURIData('id');
$phid = $request->getURIData('phid');
if ($phid) {
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withPHIDs(array($phid))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
return id(new AphrontRedirectResponse())->setURI($file->getInfoURI());
}
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs(array($id))
->executeOne();
if (!$file) {
return new Aphront404Response();
}
$phid = $file->getPHID();
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setPolicyObject($file)
->setHeader($file->getName())
->setHeaderIcon('fa-file-o');
$ttl = $file->getTTL();
if ($ttl !== null) {
$ttl_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_YELLOW)
->setName(pht('Temporary'));
$header->addTag($ttl_tag);
}
$partial = $file->getIsPartial();
if ($partial) {
$partial_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_STATE)
->setBackgroundColor(PHUITagView::COLOR_ORANGE)
->setName(pht('Partial Upload'));
$header->addTag($partial_tag);
}
$curtain = $this->buildCurtainView($file);
$timeline = $this->buildTransactionView($file);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(
'F'.$file->getID(),
$this->getApplicationURI("/info/{$phid}/"));
$crumbs->setBorder(true);
$object_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('File'))
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
$this->buildPropertyViews($object_box, $file);
$title = $file->getName();
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setCurtain($curtain)
->setMainColumn(array(
$object_box,
$timeline,
));
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->setPageObjectPHIDs(array($file->getPHID()))
->appendChild($view);
}
private function buildTransactionView(PhabricatorFile $file) {
$viewer = $this->getViewer();
$timeline = $this->buildTransactionTimeline(
$file,
new PhabricatorFileTransactionQuery());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$add_comment_header = $is_serious
? pht('Add Comment')
: pht('Question File Integrity');
$draft = PhabricatorDraft::newFromUserAndKey($viewer, $file->getPHID());
$add_comment_form = id(new PhabricatorApplicationTransactionCommentView())
->setUser($viewer)
->setObjectPHID($file->getPHID())
->setDraft($draft)
->setHeaderText($add_comment_header)
->setAction($this->getApplicationURI('/comment/'.$file->getID().'/'))
->setSubmitButtonName(pht('Add Comment'));
return array(
$timeline,
$add_comment_form,
);
}
private function buildCurtainView(PhabricatorFile $file) {
$viewer = $this->getViewer();
$id = $file->getID();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$file,
PhabricatorPolicyCapability::CAN_EDIT);
$curtain = $this->newCurtainView($file);
$can_download = !$file->getIsPartial();
if ($file->isViewableInBrowser()) {
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View File'))
->setIcon('fa-file-o')
->setHref($file->getViewURI())
->setDisabled(!$can_download)
->setWorkflow(!$can_download));
} else {
$curtain->addAction(
id(new PhabricatorActionView())
->setUser($viewer)
->setRenderAsForm($can_download)
->setDownload($can_download)
->setName(pht('Download File'))
->setIcon('fa-download')
->setHref($file->getViewURI())
->setDisabled(!$can_download)
->setWorkflow(!$can_download));
}
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit File'))
->setIcon('fa-pencil')
->setHref($this->getApplicationURI("/edit/{$id}/"))
->setWorkflow(!$can_edit)
->setDisabled(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('Delete File'))
->setIcon('fa-times')
->setHref($this->getApplicationURI("/delete/{$id}/"))
->setWorkflow(true)
->setDisabled(!$can_edit));
$curtain->addAction(
id(new PhabricatorActionView())
->setName(pht('View Transforms'))
->setIcon('fa-crop')
->setHref($this->getApplicationURI("/transforms/{$id}/")));
return $curtain;
}
private function buildPropertyViews(
PHUIObjectBoxView $box,
PhabricatorFile $file) {
$request = $this->getRequest();
$viewer = $request->getUser();
$properties = id(new PHUIPropertyListView());
$box->addPropertyList($properties, pht('Details'));
if ($file->getAuthorPHID()) {
$properties->addProperty(
pht('Author'),
$viewer->renderHandle($file->getAuthorPHID()));
}
$properties->addProperty(
pht('Created'),
phabricator_datetime($file->getDateCreated(), $viewer));
$finfo = id(new PHUIPropertyListView());
$box->addPropertyList($finfo, pht('File Info'));
$finfo->addProperty(
pht('Size'),
phutil_format_bytes($file->getByteSize()));
$finfo->addProperty(
pht('Mime Type'),
$file->getMimeType());
$width = $file->getImageWidth();
if ($width) {
$finfo->addProperty(
pht('Width'),
pht('%s px', new PhutilNumber($width)));
}
$height = $file->getImageHeight();
if ($height) {
$finfo->addProperty(
pht('Height'),
pht('%s px', new PhutilNumber($height)));
}
$is_image = $file->isViewableImage();
if ($is_image) {
$image_string = pht('Yes');
$cache_string = $file->getCanCDN() ? pht('Yes') : pht('No');
} else {
$image_string = pht('No');
$cache_string = pht('Not Applicable');
}
- $finfo->addProperty(pht('Viewable Image'), $image_string);
- $finfo->addProperty(pht('Cacheable'), $cache_string);
+ $types = array();
+ if ($file->isViewableImage()) {
+ $types[] = pht('Image');
+ }
- $builtin = $file->getBuiltinName();
- if ($builtin === null) {
- $builtin_string = pht('No');
- } else {
- $builtin_string = $builtin;
+ if ($file->isVideo()) {
+ $types[] = pht('Video');
+ }
+
+ if ($file->isAudio()) {
+ $types[] = pht('Audio');
}
- $finfo->addProperty(pht('Builtin'), $builtin_string);
+ if ($file->getCanCDN()) {
+ $types[] = pht('Can CDN');
+ }
+
+ $builtin = $file->getBuiltinName();
+ if ($builtin !== null) {
+ $types[] = pht('Builtin ("%s")', $builtin);
+ }
- $is_profile = $file->getIsProfileImage()
- ? pht('Yes')
- : pht('No');
+ if ($file->getIsProfileImage()) {
+ $types[] = pht('Profile');
+ }
- $finfo->addProperty(pht('Profile'), $is_profile);
+ $types = implode(', ', $types);
+ $finfo->addProperty(pht('Attributes'), $types);
$storage_properties = new PHUIPropertyListView();
$box->addPropertyList($storage_properties, pht('Storage'));
$storage_properties->addProperty(
pht('Engine'),
$file->getStorageEngine());
$storage_properties->addProperty(
pht('Format'),
$file->getStorageFormat());
$storage_properties->addProperty(
pht('Handle'),
$file->getStorageHandle());
$phids = $file->getObjectPHIDs();
if ($phids) {
$attached = new PHUIPropertyListView();
$box->addPropertyList($attached, pht('Attached'));
$attached->addProperty(
pht('Attached To'),
$viewer->renderHandleList($phids));
}
if ($file->isViewableImage()) {
$image = phutil_tag(
'img',
array(
'src' => $file->getViewURI(),
'class' => 'phui-property-list-image',
));
$linked_image = phutil_tag(
'a',
array(
'href' => $file->getViewURI(),
),
$image);
$media = id(new PHUIPropertyListView())
->addImageContent($linked_image);
+ $box->addPropertyList($media);
+ } else if ($file->isVideo()) {
+ $video = phutil_tag(
+ 'video',
+ array(
+ 'controls' => 'controls',
+ 'class' => 'phui-property-list-video',
+ ),
+ phutil_tag(
+ 'source',
+ array(
+ 'src' => $file->getViewURI(),
+ 'type' => $file->getMimeType(),
+ )));
+ $media = id(new PHUIPropertyListView())
+ ->addImageContent($video);
+
$box->addPropertyList($media);
} else if ($file->isAudio()) {
$audio = phutil_tag(
'audio',
array(
'controls' => 'controls',
'class' => 'phui-property-list-audio',
),
phutil_tag(
'source',
array(
'src' => $file->getViewURI(),
'type' => $file->getMimeType(),
)));
$media = id(new PHUIPropertyListView())
->addImageContent($audio);
$box->addPropertyList($media);
}
$engine = null;
try {
$engine = $file->instantiateStorageEngine();
} catch (Exception $ex) {
// Don't bother raising this anywhere for now.
}
if ($engine) {
if ($engine->isChunkEngine()) {
$chunkinfo = new PHUIPropertyListView();
$box->addPropertyList($chunkinfo, pht('Chunks'));
$chunks = id(new PhabricatorFileChunkQuery())
->setViewer($viewer)
->withChunkHandles(array($file->getStorageHandle()))
->execute();
$chunks = msort($chunks, 'getByteStart');
$rows = array();
$completed = array();
foreach ($chunks as $chunk) {
$is_complete = $chunk->getDataFilePHID();
$rows[] = array(
$chunk->getByteStart(),
$chunk->getByteEnd(),
($is_complete ? pht('Yes') : pht('No')),
);
if ($is_complete) {
$completed[] = $chunk;
}
}
$table = id(new AphrontTableView($rows))
->setHeaders(
array(
pht('Offset'),
pht('End'),
pht('Complete'),
))
->setColumnClasses(
array(
'',
'',
'wide',
));
$chunkinfo->addProperty(
pht('Total Chunks'),
count($chunks));
$chunkinfo->addProperty(
pht('Completed Chunks'),
count($completed));
$chunkinfo->addRawContent($table);
}
}
}
}
diff --git a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
index 22459a34ad..15b8771978 100644
--- a/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
+++ b/src/applications/files/markup/PhabricatorEmbedFileRemarkupRule.php
@@ -1,252 +1,295 @@
<?php
final class PhabricatorEmbedFileRemarkupRule
extends PhabricatorObjectRemarkupRule {
const KEY_EMBED_FILE_PHIDS = 'phabricator.embedded-file-phids';
protected function getObjectNamePrefix() {
return 'F';
}
protected function loadObjects(array $ids) {
$engine = $this->getEngine();
$viewer = $engine->getConfig('viewer');
$objects = id(new PhabricatorFileQuery())
->setViewer($viewer)
->withIDs($ids)
->needTransforms(
array(
PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW,
))
->execute();
$phids_key = self::KEY_EMBED_FILE_PHIDS;
$phids = $engine->getTextMetadata($phids_key, array());
foreach (mpull($objects, 'getPHID') as $phid) {
$phids[] = $phid;
}
$engine->setTextMetadata($phids_key, $phids);
return $objects;
}
protected function renderObjectEmbed(
$object,
PhabricatorObjectHandle $handle,
$options) {
$options = $this->getFileOptions($options) + array(
'name' => $object->getName(),
);
$is_viewable_image = $object->isViewableImage();
$is_audio = $object->isAudio();
+ $is_video = $object->isVideo();
$force_link = ($options['layout'] == 'link');
- $options['viewable'] = ($is_viewable_image || $is_audio);
+ // If a file is both audio and video, as with "application/ogg" by default,
+ // render it as video but allow the user to specify `media=audio` if they
+ // want to force it to render as audio.
+ if ($is_audio && $is_video) {
+ $media = $options['media'];
+ if ($media == 'audio') {
+ $is_video = false;
+ } else {
+ $is_audio = false;
+ }
+ }
+
+ $options['viewable'] = ($is_viewable_image || $is_audio || $is_video);
if ($is_viewable_image && !$force_link) {
return $this->renderImageFile($object, $handle, $options);
+ } else if ($is_video && !$force_link) {
+ return $this->renderVideoFile($object, $handle, $options);
} else if ($is_audio && !$force_link) {
return $this->renderAudioFile($object, $handle, $options);
} else {
return $this->renderFileLink($object, $handle, $options);
}
}
private function getFileOptions($option_string) {
$options = array(
- 'size' => null,
- 'layout' => 'left',
- 'float' => false,
- 'width' => null,
- 'height' => null,
+ 'size' => null,
+ 'layout' => 'left',
+ 'float' => false,
+ 'width' => null,
+ 'height' => null,
'alt' => null,
+ 'media' => null,
+ 'autoplay' => null,
+ 'loop' => null,
);
if ($option_string) {
$option_string = trim($option_string, ', ');
$parser = new PhutilSimpleOptions();
$options = $parser->parse($option_string) + $options;
}
return $options;
}
private function renderImageFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
require_celerity_resource('lightbox-attachment-css');
$attrs = array();
$image_class = 'phabricator-remarkup-embed-image';
$use_size = true;
if (!$options['size']) {
$width = $this->parseDimension($options['width']);
$height = $this->parseDimension($options['height']);
if ($width || $height) {
$use_size = false;
$attrs += array(
'src' => $file->getBestURI(),
'width' => $width,
'height' => $height,
);
}
}
if ($use_size) {
switch ((string)$options['size']) {
case 'full':
$attrs += array(
'src' => $file->getBestURI(),
'height' => $file->getImageHeight(),
'width' => $file->getImageWidth(),
);
$image_class = 'phabricator-remarkup-embed-image-full';
break;
// Displays "full" in normal Remarkup, "wide" in Documents
case 'wide':
$attrs += array(
'src' => $file->getBestURI(),
'width' => $file->getImageWidth(),
);
$image_class = 'phabricator-remarkup-embed-image-wide';
break;
case 'thumb':
default:
$preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW;
$xform = PhabricatorFileTransform::getTransformByKey($preview_key);
$existing_xform = $file->getTransform($preview_key);
if ($existing_xform) {
$xform_uri = $existing_xform->getCDNURI();
} else {
$xform_uri = $file->getURIForTransform($xform);
}
$attrs['src'] = $xform_uri;
$dimensions = $xform->getTransformedDimensions($file);
if ($dimensions) {
list($x, $y) = $dimensions;
$attrs['width'] = $x;
$attrs['height'] = $y;
}
break;
}
}
if (isset($options['alt'])) {
$attrs['alt'] = $options['alt'];
}
$img = phutil_tag('img', $attrs);
$embed = javelin_tag(
'a',
array(
'href' => $file->getBestURI(),
'class' => $image_class,
'sigil' => 'lightboxable',
'meta' => array(
'phid' => $file->getPHID(),
'uri' => $file->getBestURI(),
'dUri' => $file->getDownloadURI(),
'viewable' => true,
),
),
$img);
switch ($options['layout']) {
case 'right':
case 'center':
case 'inline':
case 'left':
$layout_class = 'phabricator-remarkup-embed-layout-'.$options['layout'];
break;
default:
$layout_class = 'phabricator-remarkup-embed-layout-left';
break;
}
if ($options['float']) {
switch ($options['layout']) {
case 'center':
case 'inline':
break;
case 'right':
$layout_class .= ' phabricator-remarkup-embed-float-right';
break;
case 'left':
default:
$layout_class .= ' phabricator-remarkup-embed-float-left';
break;
}
}
return phutil_tag(
($options['layout'] == 'inline' ? 'span' : 'div'),
array(
'class' => $layout_class,
),
$embed);
}
private function renderAudioFile(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
+ return $this->renderMediaFile('audio', $file, $handle, $options);
+ }
+
+ private function renderVideoFile(
+ PhabricatorFile $file,
+ PhabricatorObjectHandle $handle,
+ array $options) {
+ return $this->renderMediaFile('video', $file, $handle, $options);
+ }
+
+ private function renderMediaFile(
+ $tag,
+ PhabricatorFile $file,
+ PhabricatorObjectHandle $handle,
+ array $options) {
+
+ $is_video = ($tag == 'video');
if (idx($options, 'autoplay')) {
$preload = 'auto';
$autoplay = 'autoplay';
} else {
- $preload = 'none';
+ // If we don't preload video, the user can't see the first frame and
+ // has no clue what they're looking at, so always preload.
+ if ($is_video) {
+ $preload = 'auto';
+ } else {
+ $preload = 'none';
+ }
$autoplay = null;
}
return $this->newTag(
- 'audio',
+ $tag,
array(
'controls' => 'controls',
'preload' => $preload,
'autoplay' => $autoplay,
'loop' => idx($options, 'loop') ? 'loop' : null,
+ 'alt' => $options['alt'],
+ 'class' => 'phabricator-media',
),
$this->newTag(
'source',
array(
'src' => $file->getBestURI(),
'type' => $file->getMimeType(),
)));
}
private function renderFileLink(
PhabricatorFile $file,
PhabricatorObjectHandle $handle,
array $options) {
return id(new PhabricatorFileLinkView())
->setFilePHID($file->getPHID())
->setFileName($this->assertFlatText($options['name']))
->setFileDownloadURI($file->getDownloadURI())
->setFileViewURI($file->getBestURI())
->setFileViewable((bool)$options['viewable']);
}
private function parseDimension($string) {
$string = trim($string);
if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) {
return $string;
}
return null;
}
}
diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php
index d545aa1de0..b21b7dbe81 100644
--- a/src/applications/files/storage/PhabricatorFile.php
+++ b/src/applications/files/storage/PhabricatorFile.php
@@ -1,1349 +1,1359 @@
<?php
/**
* Parameters
* ==========
*
* When creating a new file using a method like @{method:newFromFileData}, these
* parameters are supported:
*
* | name | Human readable filename.
* | authorPHID | User PHID of uploader.
* | ttl | Temporary file lifetime, in seconds.
* | viewPolicy | File visibility policy.
* | isExplicitUpload | Used to show users files they explicitly uploaded.
* | canCDN | Allows the file to be cached and delivered over a CDN.
* | mime-type | Optional, explicit file MIME type.
* | builtin | Optional filename, identifies this as a builtin.
*
*/
final class PhabricatorFile extends PhabricatorFileDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorTokenReceiverInterface,
PhabricatorSubscribableInterface,
PhabricatorFlaggableInterface,
PhabricatorPolicyInterface,
PhabricatorDestructibleInterface {
const STORAGE_FORMAT_RAW = 'raw';
const METADATA_IMAGE_WIDTH = 'width';
const METADATA_IMAGE_HEIGHT = 'height';
const METADATA_CAN_CDN = 'canCDN';
const METADATA_BUILTIN = 'builtin';
const METADATA_PARTIAL = 'partial';
const METADATA_PROFILE = 'profile';
protected $name;
protected $mimeType;
protected $byteSize;
protected $authorPHID;
protected $secretKey;
protected $contentHash;
protected $metadata = array();
protected $mailKey;
protected $storageEngine;
protected $storageFormat;
protected $storageHandle;
protected $ttl;
protected $isExplicitUpload = 1;
protected $viewPolicy = PhabricatorPolicies::POLICY_USER;
protected $isPartial = 0;
private $objects = self::ATTACHABLE;
private $objectPHIDs = self::ATTACHABLE;
private $originalFile = self::ATTACHABLE;
private $transforms = self::ATTACHABLE;
public static function initializeNewFile() {
$app = id(new PhabricatorApplicationQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withClasses(array('PhabricatorFilesApplication'))
->executeOne();
$view_policy = $app->getPolicy(
FilesDefaultViewCapability::CAPABILITY);
return id(new PhabricatorFile())
->setViewPolicy($view_policy)
->setIsPartial(0)
->attachOriginalFile(null)
->attachObjects(array())
->attachObjectPHIDs(array());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'metadata' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'text255?',
'mimeType' => 'text255?',
'byteSize' => 'uint64',
'storageEngine' => 'text32',
'storageFormat' => 'text32',
'storageHandle' => 'text255',
'authorPHID' => 'phid?',
'secretKey' => 'bytes20?',
'contentHash' => 'bytes40?',
'ttl' => 'epoch?',
'isExplicitUpload' => 'bool?',
'mailKey' => 'bytes20',
'isPartial' => 'bool',
),
self::CONFIG_KEY_SCHEMA => array(
'key_phid' => null,
'phid' => array(
'columns' => array('phid'),
'unique' => true,
),
'authorPHID' => array(
'columns' => array('authorPHID'),
),
'contentHash' => array(
'columns' => array('contentHash'),
),
'key_ttl' => array(
'columns' => array('ttl'),
),
'key_dateCreated' => array(
'columns' => array('dateCreated'),
),
'key_partial' => array(
'columns' => array('authorPHID', 'isPartial'),
),
),
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorFileFilePHIDType::TYPECONST);
}
public function save() {
if (!$this->getSecretKey()) {
$this->setSecretKey($this->generateSecretKey());
}
if (!$this->getMailKey()) {
$this->setMailKey(Filesystem::readRandomCharacters(20));
}
return parent::save();
}
public function getMonogram() {
return 'F'.$this->getID();
}
public function scrambleSecret() {
return $this->setSecretKey($this->generateSecretKey());
}
public static function readUploadedFileData($spec) {
if (!$spec) {
throw new Exception(pht('No file was uploaded!'));
}
$err = idx($spec, 'error');
if ($err) {
throw new PhabricatorFileUploadException($err);
}
$tmp_name = idx($spec, 'tmp_name');
$is_valid = @is_uploaded_file($tmp_name);
if (!$is_valid) {
throw new Exception(pht('File is not an uploaded file.'));
}
$file_data = Filesystem::readFile($tmp_name);
$file_size = idx($spec, 'size');
if (strlen($file_data) != $file_size) {
throw new Exception(pht('File size disagrees with uploaded size.'));
}
return $file_data;
}
public static function newFromPHPUpload($spec, array $params = array()) {
$file_data = self::readUploadedFileData($spec);
$file_name = nonempty(
idx($params, 'name'),
idx($spec, 'name'));
$params = array(
'name' => $file_name,
) + $params;
return self::newFromFileData($file_data, $params);
}
public static function newFromXHRUpload($data, array $params = array()) {
return self::newFromFileData($data, $params);
}
/**
* Given a block of data, try to load an existing file with the same content
* if one exists. If it does not, build a new file.
*
* This method is generally used when we have some piece of semi-trusted data
* like a diff or a file from a repository that we want to show to the user.
* We can't just dump it out because it may be dangerous for any number of
* reasons; instead, we need to serve it through the File abstraction so it
* ends up on the CDN domain if one is configured and so on. However, if we
* simply wrote a new file every time we'd potentially end up with a lot
* of redundant data in file storage.
*
* To solve these problems, we use file storage as a cache and reuse the
* same file again if we've previously written it.
*
* NOTE: This method unguards writes.
*
* @param string Raw file data.
* @param dict Dictionary of file information.
*/
public static function buildFromFileDataOrHash(
$data,
array $params = array()) {
$file = id(new PhabricatorFile())->loadOneWhere(
'name = %s AND contentHash = %s LIMIT 1',
idx($params, 'name'),
self::hashFileContent($data));
if (!$file) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = self::newFromFileData($data, $params);
unset($unguarded);
}
return $file;
}
public static function newFileFromContentHash($hash, array $params) {
// Check to see if a file with same contentHash exist
$file = id(new PhabricatorFile())->loadOneWhere(
'contentHash = %s LIMIT 1',
$hash);
if ($file) {
// copy storageEngine, storageHandle, storageFormat
$copy_of_storage_engine = $file->getStorageEngine();
$copy_of_storage_handle = $file->getStorageHandle();
$copy_of_storage_format = $file->getStorageFormat();
$copy_of_byte_size = $file->getByteSize();
$copy_of_mime_type = $file->getMimeType();
$new_file = self::initializeNewFile();
$new_file->setByteSize($copy_of_byte_size);
$new_file->setContentHash($hash);
$new_file->setStorageEngine($copy_of_storage_engine);
$new_file->setStorageHandle($copy_of_storage_handle);
$new_file->setStorageFormat($copy_of_storage_format);
$new_file->setMimeType($copy_of_mime_type);
$new_file->copyDimensions($file);
$new_file->readPropertiesFromParameters($params);
$new_file->save();
return $new_file;
}
return $file;
}
public static function newChunkedFile(
PhabricatorFileStorageEngine $engine,
$length,
array $params) {
$file = self::initializeNewFile();
$file->setByteSize($length);
// TODO: We might be able to test the first chunk in order to figure
// this out more reliably, since MIME detection usually examines headers.
// However, enormous files are probably always either actually raw data
// or reasonable to treat like raw data.
$file->setMimeType('application/octet-stream');
$chunked_hash = idx($params, 'chunkedHash');
if ($chunked_hash) {
$file->setContentHash($chunked_hash);
} else {
// See PhabricatorChunkedFileStorageEngine::getChunkedHash() for some
// discussion of this.
$seed = Filesystem::readRandomBytes(64);
$hash = PhabricatorChunkedFileStorageEngine::getChunkedHashForInput(
$seed);
$file->setContentHash($hash);
}
$file->setStorageEngine($engine->getEngineIdentifier());
$file->setStorageHandle(PhabricatorFileChunk::newChunkHandle());
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
$file->setIsPartial(1);
$file->readPropertiesFromParameters($params);
return $file;
}
private static function buildFromFileData($data, array $params = array()) {
if (isset($params['storageEngines'])) {
$engines = $params['storageEngines'];
} else {
$size = strlen($data);
$engines = PhabricatorFileStorageEngine::loadStorageEngines($size);
if (!$engines) {
throw new Exception(
pht(
'No configured storage engine can store this file. See '.
'"Configuring File Storage" in the documentation for '.
'information on configuring storage engines.'));
}
}
assert_instances_of($engines, 'PhabricatorFileStorageEngine');
if (!$engines) {
throw new Exception(pht('No valid storage engines are available!'));
}
$file = self::initializeNewFile();
$data_handle = null;
$engine_identifier = null;
$exceptions = array();
foreach ($engines as $engine) {
$engine_class = get_class($engine);
try {
list($engine_identifier, $data_handle) = $file->writeToEngine(
$engine,
$data,
$params);
// We stored the file somewhere so stop trying to write it to other
// places.
break;
} catch (PhabricatorFileStorageConfigurationException $ex) {
// If an engine is outright misconfigured (or misimplemented), raise
// that immediately since it probably needs attention.
throw $ex;
} catch (Exception $ex) {
phlog($ex);
// If an engine doesn't work, keep trying all the other valid engines
// in case something else works.
$exceptions[$engine_class] = $ex;
}
}
if (!$data_handle) {
throw new PhutilAggregateException(
pht('All storage engines failed to write file:'),
$exceptions);
}
$file->setByteSize(strlen($data));
$file->setContentHash(self::hashFileContent($data));
$file->setStorageEngine($engine_identifier);
$file->setStorageHandle($data_handle);
// TODO: This is probably YAGNI, but allows for us to do encryption or
// compression later if we want.
$file->setStorageFormat(self::STORAGE_FORMAT_RAW);
$file->readPropertiesFromParameters($params);
if (!$file->getMimeType()) {
$tmp = new TempFile();
Filesystem::writeFile($tmp, $data);
$file->setMimeType(Filesystem::getMimeType($tmp));
}
try {
$file->updateDimensions(false);
} catch (Exception $ex) {
// Do nothing
}
$file->save();
return $file;
}
public static function newFromFileData($data, array $params = array()) {
$hash = self::hashFileContent($data);
$file = self::newFileFromContentHash($hash, $params);
if ($file) {
return $file;
}
return self::buildFromFileData($data, $params);
}
public function migrateToEngine(PhabricatorFileStorageEngine $engine) {
if (!$this->getID() || !$this->getStorageHandle()) {
throw new Exception(
pht("You can not migrate a file which hasn't yet been saved."));
}
$data = $this->loadFileData();
$params = array(
'name' => $this->getName(),
);
list($new_identifier, $new_handle) = $this->writeToEngine(
$engine,
$data,
$params);
$old_engine = $this->instantiateStorageEngine();
$old_identifier = $this->getStorageEngine();
$old_handle = $this->getStorageHandle();
$this->setStorageEngine($new_identifier);
$this->setStorageHandle($new_handle);
$this->save();
$this->deleteFileDataIfUnused(
$old_engine,
$old_identifier,
$old_handle);
return $this;
}
private function writeToEngine(
PhabricatorFileStorageEngine $engine,
$data,
array $params) {
$engine_class = get_class($engine);
$data_handle = $engine->writeFile($data, $params);
if (!$data_handle || strlen($data_handle) > 255) {
// This indicates an improperly implemented storage engine.
throw new PhabricatorFileStorageConfigurationException(
pht(
"Storage engine '%s' executed %s but did not return a valid ".
"handle ('%s') to the data: it must be nonempty and no longer ".
"than 255 characters.",
$engine_class,
'writeFile()',
$data_handle));
}
$engine_identifier = $engine->getEngineIdentifier();
if (!$engine_identifier || strlen($engine_identifier) > 32) {
throw new PhabricatorFileStorageConfigurationException(
pht(
"Storage engine '%s' returned an improper engine identifier '{%s}': ".
"it must be nonempty and no longer than 32 characters.",
$engine_class,
$engine_identifier));
}
return array($engine_identifier, $data_handle);
}
/**
* Download a remote resource over HTTP and save the response body as a file.
*
* This method respects `security.outbound-blacklist`, and protects against
* HTTP redirection (by manually following "Location" headers and verifying
* each destination). It does not protect against DNS rebinding. See
* discussion in T6755.
*/
public static function newFromFileDownload($uri, array $params = array()) {
$timeout = 5;
$redirects = array();
$current = $uri;
while (true) {
try {
if (count($redirects) > 10) {
throw new Exception(
pht('Too many redirects trying to fetch remote URI.'));
}
$resolved = PhabricatorEnv::requireValidRemoteURIForFetch(
$current,
array(
'http',
'https',
));
list($resolved_uri, $resolved_domain) = $resolved;
$current = new PhutilURI($current);
if ($current->getProtocol() == 'http') {
// For HTTP, we can use a pre-resolved URI to defuse DNS rebinding.
$fetch_uri = $resolved_uri;
$fetch_host = $resolved_domain;
} else {
// For HTTPS, we can't: cURL won't verify the SSL certificate if
// the domain has been replaced with an IP. But internal services
// presumably will not have valid certificates for rebindable
// domain names on attacker-controlled domains, so the DNS rebinding
// attack should generally not be possible anyway.
$fetch_uri = $current;
$fetch_host = null;
}
$future = id(new HTTPSFuture($fetch_uri))
->setFollowLocation(false)
->setTimeout($timeout);
if ($fetch_host !== null) {
$future->addHeader('Host', $fetch_host);
}
list($status, $body, $headers) = $future->resolve();
if ($status->isRedirect()) {
// This is an HTTP 3XX status, so look for a "Location" header.
$location = null;
foreach ($headers as $header) {
list($name, $value) = $header;
if (phutil_utf8_strtolower($name) == 'location') {
$location = $value;
break;
}
}
// HTTP 3XX status with no "Location" header, just treat this like
// a normal HTTP error.
if ($location === null) {
throw $status;
}
if (isset($redirects[$location])) {
throw new Exception(
pht('Encountered loop while following redirects.'));
}
$redirects[$location] = $location;
$current = $location;
// We'll fall off the bottom and go try this URI now.
} else if ($status->isError()) {
// This is something other than an HTTP 2XX or HTTP 3XX status, so
// just bail out.
throw $status;
} else {
// This is HTTP 2XX, so use the response body to save the
// file data.
$params = $params + array(
'name' => basename($uri),
);
return self::newFromFileData($body, $params);
}
} catch (Exception $ex) {
if ($redirects) {
throw new PhutilProxyException(
pht(
'Failed to fetch remote URI "%s" after following %s redirect(s) '.
'(%s): %s',
$uri,
phutil_count($redirects),
implode(' > ', array_keys($redirects)),
$ex->getMessage()),
$ex);
} else {
throw $ex;
}
}
}
}
public static function normalizeFileName($file_name) {
$pattern = "@[\\x00-\\x19#%&+!~'\$\"\/=\\\\?<> ]+@";
$file_name = preg_replace($pattern, '_', $file_name);
$file_name = preg_replace('@_+@', '_', $file_name);
$file_name = trim($file_name, '_');
$disallowed_filenames = array(
'.' => 'dot',
'..' => 'dotdot',
'' => 'file',
);
$file_name = idx($disallowed_filenames, $file_name, $file_name);
return $file_name;
}
public function delete() {
// We want to delete all the rows which mark this file as the transformation
// of some other file (since we're getting rid of it). We also delete all
// the transformations of this file, so that a user who deletes an image
// doesn't need to separately hunt down and delete a bunch of thumbnails and
// resizes of it.
$outbound_xforms = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms(
array(
array(
'originalPHID' => $this->getPHID(),
'transform' => true,
),
))
->execute();
foreach ($outbound_xforms as $outbound_xform) {
$outbound_xform->delete();
}
$inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere(
'transformedPHID = %s',
$this->getPHID());
$this->openTransaction();
foreach ($inbound_xforms as $inbound_xform) {
$inbound_xform->delete();
}
$ret = parent::delete();
$this->saveTransaction();
$this->deleteFileDataIfUnused(
$this->instantiateStorageEngine(),
$this->getStorageEngine(),
$this->getStorageHandle());
return $ret;
}
/**
* Destroy stored file data if there are no remaining files which reference
* it.
*/
public function deleteFileDataIfUnused(
PhabricatorFileStorageEngine $engine,
$engine_identifier,
$handle) {
// Check to see if any files are using storage.
$usage = id(new PhabricatorFile())->loadAllWhere(
'storageEngine = %s AND storageHandle = %s LIMIT 1',
$engine_identifier,
$handle);
// If there are no files using the storage, destroy the actual storage.
if (!$usage) {
try {
$engine->deleteFile($handle);
} catch (Exception $ex) {
// In the worst case, we're leaving some data stranded in a storage
// engine, which is not a big deal.
phlog($ex);
}
}
}
public static function hashFileContent($data) {
return sha1($data);
}
public function loadFileData() {
$engine = $this->instantiateStorageEngine();
$data = $engine->readFile($this->getStorageHandle());
switch ($this->getStorageFormat()) {
case self::STORAGE_FORMAT_RAW:
$data = $data;
break;
default:
throw new Exception(pht('Unknown storage format.'));
}
return $data;
}
/**
* Return an iterable which emits file content bytes.
*
* @param int Offset for the start of data.
* @param int Offset for the end of data.
* @return Iterable Iterable object which emits requested data.
*/
public function getFileDataIterator($begin = null, $end = null) {
$engine = $this->instantiateStorageEngine();
return $engine->getFileDataIterator($this, $begin, $end);
}
public function getViewURI() {
if (!$this->getPHID()) {
throw new Exception(
pht('You must save a file before you can generate a view URI.'));
}
return $this->getCDNURI();
}
public function getCDNURI() {
$name = self::normalizeFileName($this->getName());
$name = phutil_escape_uri($name);
$parts = array();
$parts[] = 'file';
$parts[] = 'data';
// If this is an instanced install, add the instance identifier to the URI.
// Instanced configurations behind a CDN may not be able to control the
// request domain used by the CDN (as with AWS CloudFront). Embedding the
// instance identity in the path allows us to distinguish between requests
// originating from different instances but served through the same CDN.
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $this->getSecretKey();
$parts[] = $this->getPHID();
$parts[] = $name;
$path = '/'.implode('/', $parts);
// If this file is only partially uploaded, we're just going to return a
// local URI to make sure that Ajax works, since the page is inevitably
// going to give us an error back.
if ($this->getIsPartial()) {
return PhabricatorEnv::getURI($path);
} else {
return PhabricatorEnv::getCDNURI($path);
}
}
public function getInfoURI() {
return '/'.$this->getMonogram();
}
public function getBestURI() {
if ($this->isViewableInBrowser()) {
return $this->getViewURI();
} else {
return $this->getInfoURI();
}
}
public function getDownloadURI() {
$uri = id(new PhutilURI($this->getViewURI()))
->setQueryParam('download', true);
return (string)$uri;
}
public function getURIForTransform(PhabricatorFileTransform $transform) {
return $this->getTransformedURI($transform->getTransformKey());
}
private function getTransformedURI($transform) {
$parts = array();
$parts[] = 'file';
$parts[] = 'xform';
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
if (strlen($instance)) {
$parts[] = '@'.$instance;
}
$parts[] = $transform;
$parts[] = $this->getPHID();
$parts[] = $this->getSecretKey();
$path = implode('/', $parts);
$path = $path.'/';
return PhabricatorEnv::getCDNURI($path);
}
public function isViewableInBrowser() {
return ($this->getViewableMimeType() !== null);
}
public function isViewableImage() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
public function isAudio() {
if (!$this->isViewableInBrowser()) {
return false;
}
$mime_map = PhabricatorEnv::getEnvConfig('files.audio-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type);
}
+ public function isVideo() {
+ if (!$this->isViewableInBrowser()) {
+ return false;
+ }
+
+ $mime_map = PhabricatorEnv::getEnvConfig('files.video-mime-types');
+ $mime_type = $this->getMimeType();
+ return idx($mime_map, $mime_type);
+ }
+
public function isTransformableImage() {
// NOTE: The way the 'gd' extension works in PHP is that you can install it
// with support for only some file types, so it might be able to handle
// PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup
// warns you if you don't have complete support.
$matches = null;
$ok = preg_match(
'@^image/(gif|png|jpe?g)@',
$this->getViewableMimeType(),
$matches);
if (!$ok) {
return false;
}
switch ($matches[1]) {
case 'jpg';
case 'jpeg':
return function_exists('imagejpeg');
break;
case 'png':
return function_exists('imagepng');
break;
case 'gif':
return function_exists('imagegif');
break;
default:
throw new Exception(pht('Unknown type matched as image MIME type.'));
}
}
public static function getTransformableImageFormats() {
$supported = array();
if (function_exists('imagejpeg')) {
$supported[] = 'jpg';
}
if (function_exists('imagepng')) {
$supported[] = 'png';
}
if (function_exists('imagegif')) {
$supported[] = 'gif';
}
return $supported;
}
public function getDragAndDropDictionary() {
return array(
'id' => $this->getID(),
'phid' => $this->getPHID(),
'uri' => $this->getBestURI(),
);
}
public function instantiateStorageEngine() {
return self::buildEngine($this->getStorageEngine());
}
public static function buildEngine($engine_identifier) {
$engines = self::buildAllEngines();
foreach ($engines as $engine) {
if ($engine->getEngineIdentifier() == $engine_identifier) {
return $engine;
}
}
throw new Exception(
pht(
"Storage engine '%s' could not be located!",
$engine_identifier));
}
public static function buildAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass('PhabricatorFileStorageEngine')
->execute();
}
public function getViewableMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types');
$mime_type = $this->getMimeType();
$mime_parts = explode(';', $mime_type);
$mime_type = trim(reset($mime_parts));
return idx($mime_map, $mime_type);
}
public function getDisplayIconForMimeType() {
$mime_map = PhabricatorEnv::getEnvConfig('files.icon-mime-types');
$mime_type = $this->getMimeType();
return idx($mime_map, $mime_type, 'fa-file-o');
}
public function validateSecretKey($key) {
return ($key == $this->getSecretKey());
}
public function generateSecretKey() {
return Filesystem::readRandomCharacters(20);
}
public function updateDimensions($save = true) {
if (!$this->isViewableImage()) {
throw new Exception(pht('This file is not a viewable image.'));
}
if (!function_exists('imagecreatefromstring')) {
throw new Exception(pht('Cannot retrieve image information.'));
}
$data = $this->loadFileData();
$img = imagecreatefromstring($data);
if ($img === false) {
throw new Exception(pht('Error when decoding image.'));
}
$this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img);
$this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img);
if ($save) {
$this->save();
}
return $this;
}
public function copyDimensions(PhabricatorFile $file) {
$metadata = $file->getMetadata();
$width = idx($metadata, self::METADATA_IMAGE_WIDTH);
if ($width) {
$this->metadata[self::METADATA_IMAGE_WIDTH] = $width;
}
$height = idx($metadata, self::METADATA_IMAGE_HEIGHT);
if ($height) {
$this->metadata[self::METADATA_IMAGE_HEIGHT] = $height;
}
return $this;
}
/**
* Load (or build) the {@class:PhabricatorFile} objects for builtin file
* resources. The builtin mechanism allows files shipped with Phabricator
* to be treated like normal files so that APIs do not need to special case
* things like default images or deleted files.
*
* Builtins are located in `resources/builtin/` and identified by their
* name.
*
* @param PhabricatorUser Viewing user.
* @param list<PhabricatorFilesBuiltinFile> List of builtin file specs.
* @return dict<string, PhabricatorFile> Dictionary of named builtins.
*/
public static function loadBuiltins(PhabricatorUser $user, array $builtins) {
$builtins = mpull($builtins, null, 'getBuiltinFileKey');
$specs = array();
foreach ($builtins as $key => $buitin) {
$specs[] = array(
'originalPHID' => PhabricatorPHIDConstants::PHID_VOID,
'transform' => $key,
);
}
// NOTE: Anyone is allowed to access builtin files.
$files = id(new PhabricatorFileQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withTransforms($specs)
->execute();
$results = array();
foreach ($files as $file) {
$builtin_key = $file->getBuiltinName();
if ($builtin_key !== null) {
$results[$builtin_key] = $file;
}
}
$build = array();
foreach ($builtins as $key => $builtin) {
if (isset($results[$key])) {
continue;
}
$data = $builtin->loadBuiltinFileData();
$params = array(
'name' => $builtin->getBuiltinDisplayName(),
'ttl' => time() + (60 * 60 * 24 * 7),
'canCDN' => true,
'builtin' => $key,
);
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$file = self::newFromFileData($data, $params);
$xform = id(new PhabricatorTransformedFile())
->setOriginalPHID(PhabricatorPHIDConstants::PHID_VOID)
->setTransform($key)
->setTransformedPHID($file->getPHID())
->save();
unset($unguarded);
$file->attachObjectPHIDs(array());
$file->attachObjects(array());
$results[$key] = $file;
}
return $results;
}
/**
* Convenience wrapper for @{method:loadBuiltins}.
*
* @param PhabricatorUser Viewing user.
* @param string Single builtin name to load.
* @return PhabricatorFile Corresponding builtin file.
*/
public static function loadBuiltin(PhabricatorUser $user, $name) {
$builtin = id(new PhabricatorFilesOnDiskBuiltinFile())
->setName($name);
$key = $builtin->getBuiltinFileKey();
return idx(self::loadBuiltins($user, array($builtin)), $key);
}
public function getObjects() {
return $this->assertAttached($this->objects);
}
public function attachObjects(array $objects) {
$this->objects = $objects;
return $this;
}
public function getObjectPHIDs() {
return $this->assertAttached($this->objectPHIDs);
}
public function attachObjectPHIDs(array $object_phids) {
$this->objectPHIDs = $object_phids;
return $this;
}
public function getOriginalFile() {
return $this->assertAttached($this->originalFile);
}
public function attachOriginalFile(PhabricatorFile $file = null) {
$this->originalFile = $file;
return $this;
}
public function getImageHeight() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_HEIGHT);
}
public function getImageWidth() {
if (!$this->isViewableImage()) {
return null;
}
return idx($this->metadata, self::METADATA_IMAGE_WIDTH);
}
public function getCanCDN() {
if (!$this->isViewableImage()) {
return false;
}
return idx($this->metadata, self::METADATA_CAN_CDN);
}
public function setCanCDN($can_cdn) {
$this->metadata[self::METADATA_CAN_CDN] = $can_cdn ? 1 : 0;
return $this;
}
public function isBuiltin() {
return ($this->getBuiltinName() !== null);
}
public function getBuiltinName() {
return idx($this->metadata, self::METADATA_BUILTIN);
}
public function setBuiltinName($name) {
$this->metadata[self::METADATA_BUILTIN] = $name;
return $this;
}
public function getIsProfileImage() {
return idx($this->metadata, self::METADATA_PROFILE);
}
public function setIsProfileImage($value) {
$this->metadata[self::METADATA_PROFILE] = $value;
return $this;
}
/**
* Write the policy edge between this file and some object.
*
* @param phid Object PHID to attach to.
* @return this
*/
public function attachToObject($phid) {
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->addEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Remove the policy edge between this file and some object.
*
* @param phid Object PHID to detach from.
* @return this
*/
public function detachFromObject($phid) {
$edge_type = PhabricatorObjectHasFileEdgeType::EDGECONST;
id(new PhabricatorEdgeEditor())
->removeEdge($phid, $edge_type, $this->getPHID())
->save();
return $this;
}
/**
* Configure a newly created file object according to specified parameters.
*
* This method is called both when creating a file from fresh data, and
* when creating a new file which reuses existing storage.
*
* @param map<string, wild> Bag of parameters, see @{class:PhabricatorFile}
* for documentation.
* @return this
*/
private function readPropertiesFromParameters(array $params) {
$file_name = idx($params, 'name');
$this->setName($file_name);
$author_phid = idx($params, 'authorPHID');
$this->setAuthorPHID($author_phid);
$file_ttl = idx($params, 'ttl');
$this->setTtl($file_ttl);
$view_policy = idx($params, 'viewPolicy');
if ($view_policy) {
$this->setViewPolicy($params['viewPolicy']);
}
$is_explicit = (idx($params, 'isExplicitUpload') ? 1 : 0);
$this->setIsExplicitUpload($is_explicit);
$can_cdn = idx($params, 'canCDN');
if ($can_cdn) {
$this->setCanCDN(true);
}
$builtin = idx($params, 'builtin');
if ($builtin) {
$this->setBuiltinName($builtin);
}
$profile = idx($params, 'profile');
if ($profile) {
$this->setIsProfileImage(true);
}
$mime_type = idx($params, 'mime-type');
if ($mime_type) {
$this->setMimeType($mime_type);
}
return $this;
}
public function getRedirectResponse() {
$uri = $this->getBestURI();
// TODO: This is a bit iffy. Sometimes, getBestURI() returns a CDN URI
// (if the file is a viewable image) and sometimes a local URI (if not).
// For now, just detect which one we got and configure the response
// appropriately. In the long run, if this endpoint is served from a CDN
// domain, we can't issue a local redirect to an info URI (which is not
// present on the CDN domain). We probably never actually issue local
// redirects here anyway, since we only ever transform viewable images
// right now.
$is_external = strlen(id(new PhutilURI($uri))->getDomain());
return id(new AphrontRedirectResponse())
->setIsExternal($is_external)
->setURI($uri);
}
public function attachTransforms(array $map) {
$this->transforms = $map;
return $this;
}
public function getTransform($key) {
return $this->assertAttachedKey($this->transforms, $key);
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorFileEditor();
}
public function getApplicationTransactionObject() {
return $this;
}
public function getApplicationTransactionTemplate() {
return new PhabricatorFileTransaction();
}
public function willRenderTimeline(
PhabricatorApplicationTransactionView $timeline,
AphrontRequest $request) {
return $timeline;
}
/* -( PhabricatorPolicyInterface Implementation )-------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
if ($this->isBuiltin()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
if ($this->getIsProfileImage()) {
return PhabricatorPolicies::getMostOpenPolicy();
}
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return PhabricatorPolicies::POLICY_NOONE;
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
$viewer_phid = $viewer->getPHID();
if ($viewer_phid) {
if ($this->getAuthorPHID() == $viewer_phid) {
return true;
}
}
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
// If you can see the file this file is a transform of, you can see
// this file.
if ($this->getOriginalFile()) {
return true;
}
// If you can see any object this file is attached to, you can see
// the file.
return (count($this->getObjects()) > 0);
}
return false;
}
public function describeAutomaticCapability($capability) {
$out = array();
$out[] = pht('The user who uploaded a file can always view and edit it.');
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
$out[] = pht(
'Files attached to objects are visible to users who can view '.
'those objects.');
$out[] = pht(
'Thumbnails are visible only to users who can view the original '.
'file.');
break;
}
return $out;
}
/* -( PhabricatorSubscribableInterface Implementation )-------------------- */
public function isAutomaticallySubscribed($phid) {
return ($this->authorPHID == $phid);
}
/* -( PhabricatorTokenReceiverInterface )---------------------------------- */
public function getUsersToNotifyOfTokenGiven() {
return array(
$this->getAuthorPHID(),
);
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
}
diff --git a/src/docs/user/userguide/remarkup.diviner b/src/docs/user/userguide/remarkup.diviner
index 606a9db3e7..8c6c7e1bec 100644
--- a/src/docs/user/userguide/remarkup.diviner
+++ b/src/docs/user/userguide/remarkup.diviner
@@ -1,662 +1,673 @@
@title Remarkup Reference
@group userguide
Explains how to make bold text; this makes your words louder so you can win
arguments.
= Overview =
Phabricator uses a lightweight markup language called "Remarkup", similar to
other lightweight markup languages like Markdown and Wiki markup.
This document describes how to format text using Remarkup.
= Quick Reference =
All the syntax is explained in more detail below, but this is a quick guide to
formatting text in Remarkup.
These are inline styles, and can be applied to most text:
**bold** //italic// `monospaced` ##monospaced## ~~deleted~~ __underlined__
!!highlighted!!
D123 T123 rX123 # Link to Objects
{D123} {T123} # Link to Objects (Full Name)
{F123} # Embed Images
{M123} # Embed Pholio Mock
@username # Mention a User
#project # Mention a Project
[[wiki page]] # Link to Phriction
[[wiki page | name]] # Named link to Phriction
http://xyz/ # Link to web
[[http://xyz/ | name]] # Named link to web
[name](http://xyz/) # Alternate Link
These are block styles, and must be separated from surrounding text by
empty lines:
= Large Header =
== Smaller Header ==
## This is a Header As Well
Also a Large Header
===================
Also a Smaller Header
---------------------
> Quoted Text
Use `- ` or `* ` for bulleted lists, and `# ` for numbered lists.
Use ``` or indent two spaces for code.
Use %%% for a literal block.
Use | ... | ... for tables.
= Basic Styling =
Format **basic text styles** like this:
**bold text**
//italic text//
`monospaced text`
##monospaced text##
~~deleted text~~
__underlined text__
!!highlighted text!!
Those produce **bold text**, //italic text//, `monospaced text`, ##monospaced
text##, ~~deleted text~~, __underlined text__, and !!highlighted text!!
respectively.
= Layout =
Make **headers** like this:
= Large Header =
== Smaller Header ==
===== Very Small Header =====
Alternate Large Header
======================
Alternate Smaller Header
------------------------
You can optionally omit the trailing `=` signs -- that is, these are the same:
== Smaller Header ==
== Smaller Header
This produces headers like the ones in this document. Make sure you have an
empty line before and after the header.
Lists
=====
Make **lists** by beginning each item with a `-` or a `*`:
lang=text
- milk
- eggs
- bread
* duck
* duck
* goose
This produces a list like this:
- milk
- eggs
- bread
(Note that you need to put a space after the `-` or `*`.)
You can make numbered lists with a `#` instead of `-` or `*`:
# Articuno
# Zapdos
# Moltres
Numbered lists can also be started with `1.` or `1)`. If you use a number other
than `1`, the list will start at that number instead. For example, this:
```
200) OK
201) Created
202) Accepted
```
...produces this:
200) OK
201) Created
202) Accepted
You can also nest lists:
```- Body
- Head
- Arm
- Elbow
- Hand
# Thumb
# Index
# Middle
# Ring
# Pinkie
- Leg
- Knee
- Foot```
...which produces:
- Body
- Head
- Arm
- Elbow
- Hand
# Thumb
# Index
# Middle
# Ring
# Pinkie
- Leg
- Knee
- Foot
If you prefer, you can indent lists using multiple characters to show indent
depth, like this:
```- Tree
-- Branch
--- Twig```
As expected, this produces:
- Tree
-- Branch
--- Twig
You can add checkboxes to items by prefacing them with `[ ]` or `[X]`, like
this:
```
- [X] Preheat oven to 450 degrees.
- [ ] Zest 35 lemons.
```
When rendered, this produces:
- [X] Preheat oven to 450 degrees.
- [ ] Zest 35 lemons.
Make **code blocks** by indenting two spaces:
f(x, y);
You can also use three backticks to enclose the code block:
```f(x, y);
g(f);```
You can specify a language for syntax highlighting with `lang=xxx`:
lang=text
lang=html
<a href="#">...</a>
This will highlight the block using a highlighter for that language, if one is
available (in most cases, this means you need to configure Pygments):
lang=html
<a href="#">...</a>
You can also use a `COUNTEREXAMPLE` header to show that a block of code is
bad and shouldn't be copied:
lang=text
COUNTEREXAMPLE
function f() {
global $$variable_variable;
}
This produces a block like this:
COUNTEREXAMPLE
function f() {
global $$variable_variable;
}
You can use `lines=N` to limit the vertical size of a chunk of code, and
`name=some_name.ext` to give it a name. For example, this:
lang=text
lang=html, name=example.html, lines=12, counterexample
...
...produces this:
lang=html, name=example.html, lines=12, counterexample
<p>Apple</p>
<p>Apricot</p>
<p>Avocado</p>
<p>Banana</p>
<p>Bilberry</p>
<p>Blackberry</p>
<p>Blackcurrant</p>
<p>Blueberry</p>
<p>Currant</p>
<p>Cherry</p>
<p>Cherimoya</p>
<p>Clementine</p>
<p>Date</p>
<p>Damson</p>
<p>Durian</p>
<p>Eggplant</p>
<p>Elderberry</p>
<p>Feijoa</p>
<p>Gooseberry</p>
<p>Grape</p>
<p>Grapefruit</p>
<p>Guava</p>
<p>Huckleberry</p>
<p>Jackfruit</p>
<p>Jambul</p>
<p>Kiwi fruit</p>
<p>Kumquat</p>
<p>Legume</p>
<p>Lemon</p>
<p>Lime</p>
<p>Lychee</p>
<p>Mandarine</p>
<p>Mango</p>
<p>Mangostine</p>
<p>Melon</p>
You can use the `NOTE:`, `WARNING:` or `IMPORTANT:` elements to call attention
to an important idea.
For example, write this:
```
NOTE: Best practices in proton pack operation include not crossing the streams.
```
...to produce this:
NOTE: Best practices in proton pack operation include not crossing the streams.
Using `WARNING:` or `IMPORTANT:` at the beginning of the line changes the
color of the callout:
WARNING: Crossing the streams can result in total protonic reversal!
IMPORTANT: Don't cross the streams!
In addition, you can use `(NOTE)`, `(WARNING)`, or `(IMPORTANT)` to get the
same effect but without `(NOTE)`, `(WARNING)`, or `(IMPORTANT)` appearing in
the rendered result. For example, this callout uses `(NOTE)`:
(NOTE) Dr. Egon Spengler is the best resource for additional proton pack
questions.
= Linking URIs =
URIs are automatically linked: http://phabricator.org/
If you have a URI with problematic characters in it, like
"`http://comma.org/,`", you can surround it with angle brackets:
<http://comma.org/,>
This will force the parser to consume the whole URI: <http://comma.org/,>
You can also use create named links, where you choose the displayed text. These
work within Phabricator or on the internet at large:
[[/herald/transcript/ | Herald Transcripts]]
[[http://www.boring-legal-documents.com/ | exciting legal documents]]
Markdown-style links are also supported:
[Toil](http://www.trouble.com)
= Linking to Objects =
You can link to Phabricator objects, such as Differential revisions, Diffusion
commits and Maniphest tasks, by mentioning the name of an object:
D123 # Link to Differential revision D123
rX123 # Link to SVN commit 123 from the "X" repository
rXaf3192cd5 # Link to Git commit "af3192cd5..." from the "X" repository.
# You must specify at least 7 characters of the hash.
T123 # Link to Maniphest task T123
You can also link directly to a comment in Maniphest and Differential:
T123#4 # Link to comment #4 of T123
See the Phabricator configuraton setting `remarkup.ignored-object-names` to
modify this behavior.
= Embedding Objects
You can also generate full-name references to some objects by using braces:
{D123} # Link to Differential revision D123 with the full name
{T123} # Link to Maniphest task T123 with the full name
These references will also show when an object changes state (for instance, a
task or revision is closed). Some types of objects support rich embedding.
== Linking to Project Tags
Projects can be linked to with the use of a hashtag `#`. This works by default
using the name of the Project (lowercase, underscored). Additionally you
can set multiple additional hashtags by editing the Project details.
#qa, #quality_assurance
== Embedding Mocks (Pholio)
You can embed a Pholio mock by using braces to refer to it:
{M123}
By default the first four images from the mock set are displayed. This behavior
can be overridden with the **image** option. With the **image** option you can
provide one or more image IDs to display.
You can set the image (or images) to display like this:
{M123, image=12345}
{M123, image=12345 & 6789}
== Embedding Pastes
You can embed a Paste using braces:
{P123}
You can adjust the embed height with the `lines` option:
{P123, lines=15}
You can highlight specific lines with the `highlight` option:
{P123, highlight=15}
{P123, highlight="23-25, 31"}
== Embedding Images
You can embed an image or other file by using braces to refer to it:
{F123}
In most interfaces, you can drag-and-drop an image from your computer into the
text area to upload and reference it.
Some browsers (e.g. Chrome) support uploading an image data just by pasting them
from clipboard into the text area.
You can set file display options like this:
{F123, layout=left, float, size=full, alt="a duckling"}
-Valid options are:
+Valid options for all files are:
- **layout** left (default), center, right, inline, link (render a link
instead of a thumbnail for images)
+ - **name** with `layout=link` or for non-images, use this name for the link
+ text
+ - **alt** Provide alternate text for assistive technologies.
+
+Image files support these options:
+
- **float** If layout is set to left or right, the image will be floated so
text wraps around it.
- **size** thumb (default), full
- - **name** with `layout=link` or for non-images, use this name for the link
- text
- **width** Scale image to a specific width.
- **height** Scale image to a specific height.
- - **alt** Provide alternate text for assistive technologies.
+
+Audio and video files support these options:
+
+ - **media**: Specify the media type as `audio` or `video`. This allows you
+ to disambiguate how file format which may contain either audio or video
+ should be rendered.
+ - **loop**: Loop this media.
+ - **autoplay**: Automatically begin playing this media.
== Embedding Countdowns
You can embed a countdown by using braces:
{C123}
= Quoting Text =
To quote text, preface it with an `>`:
> This is quoted text.
This appears like this:
> This is quoted text.
= Embedding Media =
If you set a configuration flag, you can embed media directly in text:
- **remarkup.enable-embedded-youtube**: allows you to paste in YouTube videos
and have them render inline.
This option is disabled by default because it has security and/or
silliness implications. Carefully read the description before enabling it.
= Image Macros =
You can upload image macros (More Stuff -> Macro) which will replace text
strings with the image you specify. For instance, you could upload an image of a
dancing banana to create a macro named "peanutbutterjellytime", and then any
time you type that string on a separate line it will be replaced with the image
of a dancing banana.
= Memes =
You can also use image macros in the context of memes. For example, if you
have an image macro named `grumpy`, you can create a meme by doing the
following:
{meme, src = grumpy, above = toptextgoeshere, below = bottomtextgoeshere}
By default, the font used to create the text for the meme is `tuffy.ttf`. For
the more authentic feel of `impact.ttf`, you simply have to place the Impact
TrueType font in the Phabricator subfolder `/resources/font/`. If Remarkup
detects the presence of `impact.ttf`, it will automatically use it.
= Mentioning Users =
In Differential and Maniphest, you can mention another user by writing:
@username
When you submit your comment, this will add them as a CC on the revision or task
if they aren't already CC'd.
Icons
=====
You can add icons to comments using the `{icon ...}` syntax. For example:
{icon camera}
This renders: {icon camera}
You can select a color for icons:
{icon camera color=blue}
This renders: {icon camera color=blue}
For a list of available icons and colors, check the UIExamples application.
(The icons are sourced from
[[ http://fortawesome.github.io/Font-Awesome/ | FontAwesome ]], so you can also
browse the collection there.)
You can add `spin` to make the icon spin:
{icon cog spin}
This renders: {icon cog spin}
= Phriction Documents =
You can link to Phriction documents with a name or path:
Make sure you sign and date your [[legal/Letter of Marque and Reprisal]]!
By default, the link will render with the document title as the link name.
With a pipe (`|`), you can retitle the link. Use this to mislead your
opponents:
Check out these [[legal/boring_documents/ | exciting legal documents]]!
Links to pages which do not exist are shown in red. Links to pages which exist
but which the viewer does not have permission to see are shown with a lock
icon, and the link will not disclose the page title.
If you begin a link path with `./` or `../`, the remainder of the path will be
evaluated relative to the current wiki page. For example, if you are writing
content for the document `fruit/` a link to `[[./guava]]` is the same as a link
to `[[fruit/guava]]` from elsewhere.
Relative links may use `../` to transverse up the document tree. From the
`produce/vegetables/` page, you can use `[[../fruit/guava]]` to link to the
`produce/fruit/guava` page.
Relative links do not work when used outside of wiki pages. For example,
you can't use a relative link in a comment on a task, because there is no
reasonable place for the link to start resolving from.
When documents are moved, relative links are not automatically updated: they
are preserved as currently written. After moving a document, you may need to
review and adjust any relative links it contains.
= Literal Blocks =
To place text in a literal block use `%%%`:
%%%Text that won't be processed by remarkup
[[http://www.example.com | example]]
%%%
Remarkup will not process the text inside of literal blocks (other than to
escape HTML and preserve line breaks).
= Tables =
Remarkup supports simple table syntax. For example, this:
```
| Fruit | Color | Price | Peel?
| ----- | ----- | ----- | -----
| Apple | red | `$0.93` | no
| Banana | yellow | `$0.19` | **YES**
```
...produces this:
| Fruit | Color | Price | Peel?
| ----- | ----- | ----- | -----
| Apple | red | `$0.93` | no
| Banana | yellow | `$0.19` | **YES**
Remarkup also supports a simplified HTML table syntax. For example, this:
```
<table>
<tr>
<th>Fruit</th>
<th>Color</th>
<th>Price</th>
<th>Peel?</th>
</tr>
<tr>
<td>Apple</td>
<td>red</td>
<td>`$0.93`</td>
<td>no</td>
</tr>
<tr>
<td>Banana</td>
<td>yellow</td>
<td>`$0.19`</td>
<td>**YES**</td>
</tr>
</table>
```
...produces this:
<table>
<tr>
<th>Fruit</th>
<th>Color</th>
<th>Price</th>
<th>Peel?</th>
</tr>
<tr>
<td>Apple</td>
<td>red</td>
<td>`$0.93`</td>
<td>no</td>
</tr>
<tr>
<td>Banana</td>
<td>yellow</td>
<td>`$0.19`</td>
<td>**YES**</td>
</tr>
</table>
Some general notes about this syntax:
- your tags must all be properly balanced;
- your tags must NOT include attributes (`<td>` is OK, `<td style="...">` is
not);
- you can use other Remarkup rules (like **bold**, //italics//, etc.) inside
table cells.
Navigation Sequences
====================
You can use `{nav ...}` to render a stylized navigation sequence when helping
someone to locate something. This can be useful when writing documentation.
For example, you could give someone directions to purchase lemons:
{nav icon=home, name=Home >
Grocery Store >
Produce Section >
icon=lemon-o, name=Lemons}
To render this example, use this markup:
```
{nav icon=home, name=Home >
Grocery Store >
Produce Section >
icon=lemon-o, name=Lemons}
```
In general:
- Separate sections with `>`.
- Each section can just have a name to add an element to the navigation
sequence, or a list of key-value pairs.
- Supported keys are `icon`, `name`, `type` and `href`.
- The `type` option can be set to `instructions` to indicate that an element
is asking the user to make a choice or follow specific instructions.
= Fullscreen Mode =
Remarkup editors provide a fullscreen composition mode. This can make it easier
to edit large blocks of text, or improve focus by removing distractions. You can
exit **Fullscreen** mode by clicking the button again or by pressing escape.
diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css
index b4fa18ac8d..085fddf876 100644
--- a/webroot/rsrc/css/core/remarkup.css
+++ b/webroot/rsrc/css/core/remarkup.css
@@ -1,651 +1,662 @@
/**
* @provides phabricator-remarkup-css
*/
.phabricator-remarkup {
line-height: 1.51em;
word-break: break-word;
}
.phabricator-remarkup p {
margin: 0 0 12px;
}
.PhabricatorMonospaced,
.phabricator-remarkup .remarkup-code-block .remarkup-code {
font: 11px/15px "Menlo", "Consolas", "Monaco", monospace;
}
.platform-windows .PhabricatorMonospaced,
.platform-windows .phabricator-remarkup .remarkup-code-block .remarkup-code {
font: 12px/15px "Menlo", "Consolas", "Monaco", monospace;
}
.phabricator-remarkup .remarkup-code-block {
margin: 12px 0;
white-space: pre;
}
.phabricator-remarkup .remarkup-code-header {
padding: 6px 12px;
font-size: 13px;
font-weight: bold;
background: rgba({$alphablue},0.08);
display: inline-block;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
.phabricator-remarkup .code-block-counterexample .remarkup-code-header {
background-color: {$sh-redbackground};
}
.phabricator-remarkup .remarkup-code-block .remarkup-code-header + pre {
border-top-left-radius: 0;
}
.phabricator-remarkup .remarkup-code-block pre {
background: rgba({$alphablue},0.08);
display: block;
color: #000;
overflow: auto;
padding: 12px;
border-radius: 3px;
}
.phabricator-remarkup pre.remarkup-counterexample {
background-color: {$sh-redbackground};
}
.phabricator-remarkup tt.remarkup-monospaced {
color: #000;
background: rgba({$alphablue},0.1);
padding: 1px 4px;
border-radius: 3px;
white-space: pre-wrap;
}
/* NOTE: You can currently produce this with [[link | `name`]]. Restore the
link color. */
.phabricator-remarkup a tt.remarkup-monospaced {
color: {$anchor};
}
.phabricator-remarkup .remarkup-header tt.remarkup-monospaced {
font-weight: normal;
}
.phabricator-remarkup ul.remarkup-list {
list-style: disc;
margin: 12px 0 12px 30px;
}
.phabricator-remarkup ol.remarkup-list {
list-style: decimal;
margin: 12px 0 12px 30px;
}
.phabricator-remarkup ol ol.remarkup-list {
list-style: upper-alpha;
}
.phabricator-remarkup ol ol ol.remarkup-list {
list-style: lower-alpha;
}
.phabricator-remarkup ol ol ol ol.remarkup-list {
list-style: lower-roman;
}
.phabricator-remarkup .remarkup-list-with-checkmarks .remarkup-checked-item,
.phabricator-remarkup .remarkup-list-with-checkmarks .remarkup-unchecked-item {
list-style: none;
margin-left: -18px;
}
.phabricator-remarkup .remarkup-list-with-checkmarks input {
margin-right: 2px;
opacity: 1;
}
.phabricator-remarkup .remarkup-list-with-checkmarks .remarkup-checked-item {
text-decoration: line-through;
}
.phabricator-remarkup ul.remarkup-list ol.remarkup-list,
.phabricator-remarkup ul.remarkup-list ul.remarkup-list,
.phabricator-remarkup ol.remarkup-list ol.remarkup-list,
.phabricator-remarkup ol.remarkup-list ul.remarkup-list {
margin: 4px 0 4px 24px;
}
.phabricator-remarkup .remarkup-list-item {
line-height: 1.7em;
}
.phabricator-remarkup li.phantom-item,
.phabricator-remarkup li.phantom-item {
list-style-type: none;
}
.phabricator-remarkup h1.remarkup-header {
font-size: 24px;
line-height: 1.625em;
margin: 24px 0 4px;
}
.phabricator-remarkup h2.remarkup-header {
font-size: 20px;
line-height: 1.5em;
margin: 20px 0 4px;
}
.phabricator-remarkup h3.remarkup-header {
font-size: 18px;
line-height: 1.375em;
margin: 20px 0 4px;
}
.phabricator-remarkup h4.remarkup-header {
font-size: 16px;
line-height: 1.25em;
margin: 12px 0 4px;
}
.phabricator-remarkup h5.remarkup-header {
font-size: 15px;
line-height: 1.125em;
margin: 8px 0 4px;
}
.phabricator-remarkup h6.remarkup-header {
font-size: 14px;
line-height: 1em;
margin: 4px 0;
}
.phabricator-remarkup blockquote {
border-left: 3px solid {$sh-blueborder};
color: {$darkbluetext};
font-style: italic;
margin: 4px 0 12px 0;
padding: 8px 12px;
background-color: {$lightbluebackground};
}
.phabricator-remarkup blockquote *:last-child {
margin-bottom: 0;
}
.phabricator-remarkup blockquote blockquote {
background-color: rgba(175,175,175, .1);
}
.phabricator-remarkup blockquote em {
/* In blockquote bodies, default text is italic so emphasized text should
be normal. */
font-style: normal;
}
.phabricator-remarkup blockquote div.remarkup-reply-head {
font-style: normal;
padding-bottom: 4px;
}
.phabricator-remarkup blockquote div.remarkup-reply-head em {
/* In blockquote headers, default text is normal so emphasized text should
be italic. See T10686. */
font-style: italic;
}
.phabricator-remarkup blockquote div.remarkup-reply-head
.phui-tag-core {
background-color: transparent;
border: none;
padding: 0;
color: {$darkbluetext};
}
.phabricator-remarkup img.remarkup-proxy-image {
max-width: 640px;
max-height: 640px;
}
.phabricator-remarkup audio {
display: block;
margin: 16px auto;
min-width: 240px;
width: 50%;
}
+video.phabricator-media {
+ background: {$greybackground};
+}
+
+.phabricator-remarkup video {
+ display: block;
+ margin: 0 auto;
+ min-width: 240px;
+ width: 90%;
+}
+
.phabricator-remarkup-mention-exists {
font-weight: bold;
background: #e6f3ff;
}
.phabricator-remarkup-mention-disabled {
font-weight: bold;
background: #dddddd;
}
.phui-remarkup-preview .phabricator-remarkup-mention-unknown,
.aphront-panel-preview .phabricator-remarkup-mention-unknown {
font-weight: bold;
background: #ffaaaa;
}
.phabricator-remarkup .phriction-link {
font-weight: bold;
}
.phabricator-remarkup .phriction-link-missing {
color: {$red};
}
.phabricator-remarkup .phriction-link-lock {
color: {$greytext};
}
.phabricator-remarkup-mention-nopermission .phui-tag-core {
background: {$lightgreybackground};
color: {$lightgreytext};
}
.phabricator-remarkup .remarkup-note {
margin: 16px 0;
padding: 12px;
border-left: 3px solid {$blue};
background: {$lightblue};
}
.phabricator-remarkup .remarkup-warning {
margin: 16px 0;
padding: 12px;
border-left: 3px solid {$yellow};
background: {$lightyellow};
}
.phabricator-remarkup .remarkup-important {
margin: 16px 0;
padding: 12px;
border-left: 3px solid {$red};
background: {$lightred};
}
.phabricator-remarkup .remarkup-note .remarkup-monospaced,
.phabricator-remarkup .remarkup-important .remarkup-monospaced,
.phabricator-remarkup .remarkup-warning .remarkup-monospaced {
background-color: rgba(150,150,150,.2);
}
.phabricator-remarkup .remarkup-note-word {
font-weight: bold;
color: {$darkbluetext};
}
.phabricator-remarkup-toc {
float: right;
border-left: 1px solid {$lightblueborder};
background: #fff;
width: 160px;
padding-left: 8px;
margin: 0 0 4px 8px;
}
.phabricator-remarkup-toc-header {
font-size: 13px;
line-height: 13px;
color: {$darkbluetext};
font-weight: bold;
margin-bottom: 4px;
}
.phabricator-remarkup-toc ul {
padding: 0;
margin: 0;
list-style: none;
overflow: hidden;
}
.phabricator-remarkup-toc ul ul {
margin: 0 0 0 8px;
}
.phabricator-remarkup-toc ul li {
padding: 0;
margin: 0;
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.phabricator-remarkup-embed-layout-right {
text-align: right;
}
.phabricator-remarkup-embed-layout-center {
text-align: center;
}
.phabricator-remarkup-embed-layout-inline {
display: inline;
}
.phabricator-remarkup-embed-float-right {
float: right;
margin: .5em 1em 0;
}
.phabricator-remarkup-embed-layout-link {
padding-left: 20px;
background: url(/rsrc/image/icon/fatcow/page_white_put.png) 0 0 no-repeat;
}
.phabricator-remarkup-embed-float-left {
float: left;
margin: .5em 1em 0;
}
.phabricator-remarkup-embed-image {
display: inline-block;
border: 3px solid white;
box-shadow: 1px 1px 2px rgba({$alphablack}, 0.20);
}
.phabricator-remarkup-embed-image-full,
.phabricator-remarkup-embed-image-wide {
display: inline-block;
max-width: 100%;
}
.phabricator-remarkup-embed-image-full img,
.phabricator-remarkup-embed-image-wide img {
height: auto;
max-width: 100%;
}
.phabricator-remarkup .remarkup-table-wrap {
overflow-x: auto;
}
.phabricator-remarkup table.remarkup-table {
border-collapse: separate;
border-spacing: 1px;
background: {$lightblueborder};
margin: 12px 0;
word-break: normal;
}
.phabricator-remarkup table.remarkup-table th {
font-weight: bold;
padding: 4px 6px;
background: {$lightbluebackground};
}
.phabricator-remarkup table.remarkup-table td {
background: #ffffff;
padding: 3px 6px;
}
body div.phabricator-remarkup.remarkup-has-toc
.phabricator-remarkup-toc + .remarkup-header {
margin-top: 0;
padding-top: 0;
}
body .phabricator-standard-page div.phabricator-remarkup *:first-child,
body .phabricator-standard-page div.phabricator-remarkup .remarkup-header + * {
margin-top: 0;
}
body div.phabricator-remarkup > *:last-child {
margin-bottom: 0;
}
.remarkup-assist-textarea {
border-left-color: {$blueborder};
border-right-color: {$blueborder};
border-bottom-color: {$blueborder};
border-top-color: {$thinblueborder};
border-radius: 0;
box-shadow: none;
-webkit-box-shadow: none;
/* Set line height explicitly so the metrics <var /> and the real textarea
are forced to the same value. */
line-height: 1.25em;
/* Prevent Safari and Chrome users from dragging the textarea any wider,
because the top bar won't resize along with it. */
resize: vertical;
}
var.remarkup-assist-textarea {
/* This is an invisible element used to measure the size of text in the
textarea so we can float typeaheads over the cursor position. */
display: block;
border-color: orange;
box-sizing: border-box;
padding: 4px 6px;
white-space: pre-wrap;
visibility: hidden;
}
.remarkup-assist-textarea:focus {
border: 1px solid rgba(82, 168, 236, 0.8);
}
.remarkup-assist-bar {
height: 26px;
border-width: 1px 1px 0;
border-style: solid;
border-top-color: {$blueborder};
border-left-color: {$blueborder};
border-right-color: {$blueborder};
background: {$lightbluebackground};
overflow: hidden;
}
.remarkup-assist-button {
display: block;
padding: 4px 5px;
float: left;
}
.remarkup-assist-button:hover {
background-color: rgba(100,100,100,.15);
}
.remarkup-assist-button:hover .phui-icon-view.phui-font-fa {
color: {$darkbluetext};
}
.remarkup-assist-button:active {
outline: none;
}
.remarkup-assist-button:focus {
outline: none;
}
.remarkup-assist-separator {
display: block;
float: left;
margin: 7px 4px;
height: 14px;
width: 0px;
border-right: 1px solid #cccccc;
}
.remarkup-interpreter-error {
padding: 8px;
border: 1px solid {$sh-redborder};
background-color: {$sh-redbackground};
}
.remarkup-cowsay {
white-space: pre-wrap;
}
.remarkup-figlet {
white-space: pre-wrap;
}
.remarkup-assist {
width: 14px;
height: 14px;
overflow: hidden;
text-align: center;
vertical-align: middle;
}
.remarkup-assist-right {
float: right;
}
.jx-order-mask {
background: white;
opacity: 1.0;
}
.remarkup-control-fullscreen-mode {
position: fixed;
top: -1px;
bottom: -1px;
left: -1px;
right: -1px;
}
.remarkup-control-fullscreen-mode textarea.remarkup-assist-textarea {
position: absolute;
top: 27px;
left: 0;
right: 0;
bottom: 0;
/* NOTE: This doesn't work in Firefox, there's a JS behavior to correct it. */
height: auto;
border-width: 1px 0 0 0;
outline: none;
resize: none;
}
.phabricator-image-macro-hero {
margin: auto;
max-width: 95%;
}
.phabricator-remarkup-macro {
height: auto;
max-width: 100%;
}
.remarkup-nav-sequence-arrow {
color: {$lightgreytext};
}
.phabricator-remarkup hr {
background: {$thinblueborder};
margin: 24px 0;
clear: both;
}
.phabricator-remarkup .remarkup-highlight {
background-color: {$lightviolet};
padding: 0 4px;
}
.remarkup-inline-preview {
display: block;
position: relative;
background: #fff;
overflow-y: auto;
box-sizing: border-box;
width: 100%;
border: 1px solid {$sky};
resize: vertical;
padding: 4px 6px;
}
.remarkup-control-fullscreen-mode .remarkup-inline-preview {
resize: none;
}
.remarkup-inline-preview * {
resize: none;
}
.remarkup-assist-button.preview-active {
background: {$sky};
}
.remarkup-assist-button.preview-active .phui-icon-view {
color: #ffffff;
}
.remarkup-assist-button.preview-active:hover .phui-icon-view {
color: {$lightsky};
}
.device .remarkup-assist-nodevice {
display: none;
}
.phuix-autocomplete {
position: absolute;
width: 300px;
box-shadow: {$dropshadow};
background: #ffffff;
border: 1px solid {$lightgreyborder};
border-radius: 3px;
}
.phuix-autocomplete-head {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 6px 8px;
background: {$lightgreybackground};
color: {$darkgreytext};
border-radius: 3px;
}
.phuix-autocomplete-head .phui-icon-view {
margin-right: 4px;
color: {$lightgreytext};
}
.phuix-autocomplete-echo {
margin-left: 4px;
color: {$lightgreytext};
}
.phuix-autocomplete-list a.jx-result {
display: block;
padding: 5px 8px;
font-size: {$normalfontsize};
border-top: 1px solid {$thinblueborder};
font-weight: bold;
color: {$darkgreytext};
}
.phuix-autocomplete-list a.jx-result .phui-icon-view {
margin-right: 4px;
color: {$lightbluetext};
}
.phuix-autocomplete-list a.jx-result:hover {
text-decoration: none;
background: {$sh-bluebackground};
color: #000;
}
.phuix-autocomplete-list a.jx-result.focused,
.phuix-autocomplete-list a.jx-result.focused:hover {
background: {$sh-bluebackground};
color: #000;
}
diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css
index ab250a3cc5..da6ff63dff 100644
--- a/webroot/rsrc/css/phui/phui-property-list-view.css
+++ b/webroot/rsrc/css/phui/phui-property-list-view.css
@@ -1,214 +1,221 @@
/**
* @provides phui-property-list-view-css
*/
.phui-property-list-view .keyboard-shortcuts-available {
float: right;
height: 16px;
margin: 12px 10px -28px 0px;
padding: 0px 20px 0px 0px;
vertical-align: middle;
color: {$greytext};
text-align: right;
font-size: {$smallestfontsize};
background:
url('/rsrc/image/icon/fatcow/key_question.png') right center no-repeat;
}
.device .keyboard-shortcuts-available {
display: none;
}
.phui-property-group-noninitial,
.phui-property-list-section-noninitial {
border-color: {$thinblueborder};
border-style: solid;
border-width: 1px 0 0;
}
.device-desktop .phui-property-list-container {
padding: 12px 0 12px 0;
width: 100%;
}
.device .phui-property-list-container {
padding: 12px 0 4px 0;
}
.phui-property-list-key {
color: {$bluetext};
font-weight: bold;
overflow: hidden;
white-space: nowrap;
}
.device-desktop .phui-property-list-key {
width: 12%;
margin-left: 1%;
text-align: right;
float: left;
clear: left;
margin-bottom: 4px;
}
.device-desktop .phui-property-list-has-actions .phui-property-list-key {
width: 18%;
}
.phui-property-list-properties-wrap.phui-property-list-stacked {
width: auto;
float: none;
}
.device .phui-property-list-key,
.phui-property-list-stacked .phui-property-list-properties
.phui-property-list-key {
padding-left: 4px;
text-align: left;
margin-left: 0;
width: auto;
float: none;
}
.phui-property-list-value {
color: {$darkgreytext};
}
.device-desktop .phui-property-list-value {
width: 84%;
margin-left: 1%;
float: left;
margin-bottom: 4px;
}
.device-desktop .phui-property-list-has-actions .phui-property-list-value {
width: 78%;
}
.device .phui-property-list-value,
.phui-property-list-stacked .phui-property-list-properties
.phui-property-list-value {
padding: 0 8px;
margin-bottom: 8px;
width: auto;
word-break: break-word;
float: none;
}
.phui-property-list-section-header {
color: {$bluetext};
padding: 16px 4px 0px;
text-transform: uppercase;
font-weight: 700;
border-color: {$thinblueborder};
border-style: solid;
border-width: 1px 0 0;
}
.phui-property-list-container + .phui-property-list-text-content {
border-color: {$thinblueborder};
border-style: solid;
border-width: 1px 0 0;
}
.phui-property-list-section-noninitial .phui-property-list-section-header {
border-top: none;
}
.device .phui-property-list-section-header {
padding-left: 4px;
}
.phui-property-list-section-header-icon .phui-icon-view {
display: inline-block;
margin: -2px 4px -2px 0;
}
.phui-property-list-text-content {
padding: 16px 4px;
overflow: hidden;
}
.phui-property-list-raw-content {
padding: 0px;
overflow: hidden;
}
/* In the common case where we immediately follow a header, move back up 30px
so we snuggle next to the header. */
.device-desktop .phui-header-view
+ .phabricator-action-list-view {
margin-top: -30px;
}
.device-desktop .phui-header-view
+ .phabricator-action-list-view
+ .phui-property-list-view {
margin-top: 0px;
}
.phui-property-list-image {
margin: auto;
max-width: 95%;
}
.phui-property-list-audio {
display: block;
margin: 16px auto;
width: 50%;
min-width: 240px;
}
+.phui-property-list-video {
+ display: block;
+ margin: 0 auto;
+ width: 90%;
+ min-width: 240px;
+}
+
/* When tags appear in property lists, give them a little more vertical
spacing. */
.phui-property-list-view .phui-tag-view {
margin: 2px 0;
}
.phui-property-list-has-actions .phui-property-list-properties-wrap {
float: left;
width: 78%;
}
.device .phui-property-list-properties-wrap {
width: auto;
border: none;
float: none;
overflow: auto;
}
.phui-property-list-actions {
width: 20%;
float: right;
margin-right: 12px;
border-left: 1px solid {$thinblueborder};
}
!print .phui-property-list-actions {
display: none;
}
.device .phui-property-list-actions {
float: none;
width: auto;
margin: -12px 0 12px 0;
border: none;
}
.phui-property-list-image-content img {
margin: 20px auto;
background: url('/rsrc/image/checker_light.png');
}
.device-desktop .phui-property-list-image-content img:hover {
background: url('/rsrc/image/checker_dark.png');
}
/* - Dashboards ------------------------------------------------------------ */
.dashboard-panel .phui-property-list-section {
border-left: 1px solid {$lightblueborder};
border-right: 1px solid {$lightblueborder};
border-bottom: 1px solid {$blueborder};
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Mar 17, 12:31 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
72220
Default Alt Text
(104 KB)

Event Timeline