@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorEmbedFileRemarkupRule
4 extends PhabricatorObjectRemarkupRule {
5
6 private $viewer;
7
8 const KEY_ATTACH_INTENT_FILE_PHIDS = 'files.attach-intent';
9
10 protected function getObjectNamePrefix() {
11 return 'F';
12 }
13
14 protected function loadObjects(array $ids) {
15 $engine = $this->getEngine();
16
17 $this->viewer = $engine->getConfig('viewer');
18 $objects = id(new PhabricatorFileQuery())
19 ->setViewer($this->viewer)
20 ->withIDs($ids)
21 ->needTransforms(
22 array(
23 PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW,
24 ))
25 ->execute();
26 $objects = mpull($objects, null, 'getID');
27
28
29 // Identify files embedded in the block with "attachment intent", i.e.
30 // those files which the user appears to want to attach to the object.
31 // Files referenced inside quoted blocks are not considered to have this
32 // attachment intent.
33
34 $metadata_key = self::KEY_RULE_OBJECT.'.'.$this->getObjectNamePrefix();
35 $metadata = $engine->getTextMetadata($metadata_key, array());
36
37 $attach_key = self::KEY_ATTACH_INTENT_FILE_PHIDS;
38 $attach_phids = $engine->getTextMetadata($attach_key, array());
39
40 foreach ($metadata as $item) {
41
42 // If this reference was inside a quoted block, don't count it. Quoting
43 // someone else doesn't establish an intent to attach a file.
44 $depth = idx($item, 'quote.depth');
45 if ($depth > 0) {
46 continue;
47 }
48
49 $id = $item['id'];
50 $file = idx($objects, $id);
51
52 if (!$file) {
53 continue;
54 }
55
56 $attach_phids[] = $file->getPHID();
57 }
58
59 $attach_phids = array_fuse($attach_phids);
60 $attach_phids = array_keys($attach_phids);
61
62 $engine->setTextMetadata($attach_key, $attach_phids);
63
64
65 return $objects;
66 }
67
68 protected function renderObjectEmbed(
69 $object,
70 PhabricatorObjectHandle $handle,
71 $options) {
72
73 $options = $this->getFileOptions($options) + array(
74 'name' => $object->getName(),
75 );
76
77 $is_viewable_image = $object->isViewableImage();
78 $is_audio = $object->isAudio();
79 $is_video = $object->isVideo();
80 $force_link = ($options['layout'] == 'link');
81
82 // If a file is both audio and video, as with "application/ogg" by default,
83 // render it as video but allow the user to specify `media=audio` if they
84 // want to force it to render as audio.
85 if ($is_audio && $is_video) {
86 $media = $options['media'];
87 if ($media == 'audio') {
88 $is_video = false;
89 } else {
90 $is_audio = false;
91 }
92 }
93
94 $options['viewable'] = ($is_viewable_image || $is_audio || $is_video);
95
96 if ($is_viewable_image && !$force_link) {
97 return $this->renderImageFile($object, $handle, $options);
98 } else if ($is_video && !$force_link) {
99 return $this->renderVideoFile($object, $handle, $options);
100 } else if ($is_audio && !$force_link) {
101 return $this->renderAudioFile($object, $handle, $options);
102 } else {
103 return $this->renderFileLink($object, $handle, $options);
104 }
105 }
106
107 /**
108 * @param string $option_string File display options. See "Embedding Images"
109 * in @{article:Remarkup Reference}
110 */
111 private function getFileOptions($option_string) {
112 $options = array(
113 'size' => null,
114 'layout' => 'left',
115 'float' => false,
116 'width' => null,
117 'height' => null,
118 'alt' => null,
119 'media' => null,
120 'autoplay' => null,
121 'loop' => null,
122 );
123
124 if ($option_string) {
125 $option_string = trim($option_string, ', ');
126 $parser = new PhutilSimpleOptions();
127 $options = $parser->parse($option_string) + $options;
128 }
129
130 return $options;
131 }
132
133 private function renderImageFile(
134 PhabricatorFile $file,
135 PhabricatorObjectHandle $handle,
136 array $options) {
137
138 require_celerity_resource('phui-lightbox-css');
139
140 $attrs = array();
141 $image_class = 'phabricator-remarkup-embed-image';
142
143 $use_size = true;
144 if (!$options['size']) {
145 $width = $this->parseDimension($options['width']);
146 $height = $this->parseDimension($options['height']);
147 if ($width || $height) {
148 $use_size = false;
149 $attrs += array(
150 'src' => $file->getBestURI(),
151 'width' => $width,
152 'height' => $height,
153 );
154 }
155 }
156
157 if ($use_size) {
158 switch ((string)$options['size']) {
159 case 'full':
160 $attrs += array(
161 'src' => $file->getBestURI(),
162 'height' => $file->getImageHeight(),
163 'width' => $file->getImageWidth(),
164 'loading' => 'lazy',
165 );
166 $image_class = 'phabricator-remarkup-embed-image-full';
167 break;
168 // Displays "full" in normal Remarkup, "wide" in Documents
169 case 'wide':
170 $attrs += array(
171 'src' => $file->getBestURI(),
172 'width' => $file->getImageWidth(),
173 );
174 $image_class = 'phabricator-remarkup-embed-image-wide';
175 break;
176 case 'thumb':
177 default:
178 $preview_key = PhabricatorFileThumbnailTransform::TRANSFORM_PREVIEW;
179 $xform = PhabricatorFileTransform::getTransformByKey($preview_key);
180
181 $existing_xform = $file->getTransform($preview_key);
182 if ($existing_xform) {
183 $xform_uri = $existing_xform->getCDNURI('data');
184 } else {
185 $xform_uri = $file->getURIForTransform($xform);
186 }
187
188 $attrs['src'] = $xform_uri;
189
190 $dimensions = $xform->getTransformedDimensions($file);
191 if ($dimensions) {
192 list($x, $y) = $dimensions;
193 $attrs['width'] = $x;
194 $attrs['height'] = $y;
195 }
196 break;
197 }
198 }
199
200 $alt = null;
201 if (isset($options['alt'])) {
202 $alt = $options['alt'];
203 }
204
205 // PhutilSimpleOptions returns a bool if the option is set without a value
206 if (is_bool($alt) || !phutil_nonempty_string($alt)) {
207 $alt = $file->getAltText();
208 }
209
210 $attrs['alt'] = $alt;
211
212 $img = phutil_tag('img', $attrs);
213
214 $embed = javelin_tag(
215 'a',
216 array(
217 'href' => $file->getBestURI(),
218 'class' => $image_class,
219 'sigil' => 'lightboxable',
220 'meta' => array(
221 'phid' => $file->getPHID(),
222 'uri' => $file->getBestURI(),
223 'dUri' => $file->getDownloadURI(),
224 'alt' => $alt,
225 'viewable' => true,
226 'monogram' => $file->getMonogram(),
227 ),
228 ),
229 $img);
230
231 switch ($options['layout']) {
232 case 'right':
233 case 'center':
234 case 'inline':
235 case 'left':
236 $layout_class = 'phabricator-remarkup-embed-layout-'.$options['layout'];
237 break;
238 default:
239 $layout_class = 'phabricator-remarkup-embed-layout-left';
240 break;
241 }
242
243 if ($options['float']) {
244 switch ($options['layout']) {
245 case 'center':
246 case 'inline':
247 break;
248 case 'right':
249 $layout_class .= ' phabricator-remarkup-embed-float-right';
250 break;
251 case 'left':
252 default:
253 $layout_class .= ' phabricator-remarkup-embed-float-left';
254 break;
255 }
256 }
257
258 return phutil_tag(
259 ($options['layout'] == 'inline' ? 'span' : 'div'),
260 array(
261 'class' => $layout_class,
262 ),
263 $embed);
264 }
265
266 private function renderAudioFile(
267 PhabricatorFile $file,
268 PhabricatorObjectHandle $handle,
269 array $options) {
270 return $this->renderMediaFile('audio', $file, $handle, $options);
271 }
272
273 private function renderVideoFile(
274 PhabricatorFile $file,
275 PhabricatorObjectHandle $handle,
276 array $options) {
277 return $this->renderMediaFile('video', $file, $handle, $options);
278 }
279
280 private function renderMediaFile(
281 $tag,
282 PhabricatorFile $file,
283 PhabricatorObjectHandle $handle,
284 array $options) {
285
286 $is_video = ($tag == 'video');
287
288 if (idx($options, 'autoplay')) {
289 $preload = 'auto';
290 $autoplay = 'autoplay';
291 } else {
292 // If we don't preload video, the user can't see the first frame and
293 // has no clue what they're looking at, so always preload.
294 if ($is_video) {
295 $preload = 'auto';
296 } else {
297 $preload = 'none';
298 }
299 $autoplay = null;
300 }
301
302 // Rendering contexts like feed can disable autoplay.
303 $engine = $this->getEngine();
304 if ($engine->getConfig('autoplay.disable')) {
305 $autoplay = null;
306 }
307
308 if ($is_video) {
309 // See T13135. Chrome refuses to play videos with type "video/quicktime",
310 // even though it may actually be able to play them. The least awful fix
311 // based on available information is to simply omit the "type" attribute
312 // from `<source />` tags. This causes Chrome to try to play the video
313 // and realize it can, and does not appear to produce any bad behavior in
314 // any other browser.
315 $mime_type = null;
316 } else {
317 $mime_type = $file->getMimeType();
318 }
319
320 $thumb_class = null;
321 if (isset($options['size']) && $options['size'] == 'thumb') {
322 $thumb_class = ' video-thumb';
323 }
324
325 return $this->newTag(
326 $tag,
327 array(
328 'controls' => 'controls',
329 'preload' => $preload,
330 'autoplay' => $autoplay,
331 'loop' => idx($options, 'loop') ? 'loop' : null,
332 'alt' => $options['alt'],
333 'class' => 'phabricator-media'.$thumb_class,
334 ),
335 $this->newTag(
336 'source',
337 array(
338 'src' => $file->getBestURI(),
339 'type' => $mime_type,
340 )));
341 }
342
343 private function renderFileLink(
344 PhabricatorFile $file,
345 PhabricatorObjectHandle $handle,
346 array $options) {
347
348 return id(new PhabricatorFileLinkView())
349 ->setViewer($this->viewer)
350 ->setFilePHID($file->getPHID())
351 ->setFileName($this->assertFlatText($options['name']))
352 ->setFileDownloadURI($file->getDownloadURI())
353 ->setFileViewURI($file->getBestURI())
354 ->setFileViewable((bool)$options['viewable'])
355 ->setFileSize(phutil_format_bytes($file->getByteSize()))
356 ->setFileMonogram($file->getMonogram());
357 }
358
359 private function parseDimension($string) {
360 if ($string !== null) {
361 $string = trim($string);
362 if (preg_match('/^(?:\d*\\.)?\d+%?$/', $string)) {
363 return $string;
364 }
365 }
366
367 return null;
368 }
369
370}