@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 PhabricatorFileViewController extends PhabricatorFileController {
4
5 public function shouldAllowPublic() {
6 return true;
7 }
8
9 public function handleRequest(AphrontRequest $request) {
10 $viewer = $request->getViewer();
11 $id = $request->getURIData('id');
12 $phid = $request->getURIData('phid');
13
14 if ($phid) {
15 $file = id(new PhabricatorFileQuery())
16 ->setViewer($viewer)
17 ->withPHIDs(array($phid))
18 ->withIsDeleted(false)
19 ->executeOne();
20
21 if (!$file) {
22 return new Aphront404Response();
23 }
24 return id(new AphrontRedirectResponse())->setURI($file->getInfoURI());
25 }
26
27 $file = id(new PhabricatorFileQuery())
28 ->setViewer($viewer)
29 ->withIDs(array($id))
30 ->withIsDeleted(false)
31 ->executeOne();
32 if (!$file) {
33 return new Aphront404Response();
34 }
35
36 $phid = $file->getPHID();
37
38 $header = id(new PHUIHeaderView())
39 ->setViewer($viewer)
40 ->setPolicyObject($file)
41 ->setHeader($file->getName())
42 ->setHeaderIcon('fa-file-o');
43
44 $ttl = $file->getTTL();
45 if ($ttl !== null) {
46 $ttl_tag = id(new PHUITagView())
47 ->setType(PHUITagView::TYPE_SHADE)
48 ->setColor(PHUITagView::COLOR_YELLOW)
49 ->setName(pht('Temporary'));
50 $header->addTag($ttl_tag);
51 }
52
53 $partial = $file->getIsPartial();
54 if ($partial) {
55 $partial_tag = id(new PHUITagView())
56 ->setType(PHUITagView::TYPE_SHADE)
57 ->setColor(PHUITagView::COLOR_ORANGE)
58 ->setName(pht('Partial Upload'));
59 $header->addTag($partial_tag);
60 }
61
62 $curtain = $this->buildCurtainView($file);
63 $timeline = $this->buildTransactionView($file);
64 $crumbs = $this->buildApplicationCrumbs();
65 $crumbs->addTextCrumb(
66 $file->getMonogram(),
67 $file->getInfoURI());
68 $crumbs->setBorder(true);
69
70 $object_box = id(new PHUIObjectBoxView())
71 ->setHeaderText(pht('File Metadata'))
72 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY);
73
74 $this->buildPropertyViews($object_box, $file);
75 $title = $file->getName();
76
77 $file_content = $this->newFileContent($file);
78
79 $view = id(new PHUITwoColumnView())
80 ->setHeader($header)
81 ->setCurtain($curtain)
82 ->setMainColumn(
83 array(
84 $file_content,
85 $object_box,
86 $timeline,
87 ));
88
89 return $this->newPage()
90 ->setTitle($title)
91 ->setCrumbs($crumbs)
92 ->setPageObjectPHIDs(array($file->getPHID()))
93 ->appendChild($view);
94 }
95
96 private function buildTransactionView(PhabricatorFile $file) {
97 $viewer = $this->getViewer();
98
99 $timeline = $this->buildTransactionTimeline(
100 $file,
101 new PhabricatorFileTransactionQuery());
102
103 $comment_view = id(new PhabricatorFileEditEngine())
104 ->setViewer($viewer)
105 ->buildEditEngineCommentView($file);
106
107 $monogram = $file->getMonogram();
108
109 $timeline->setQuoteRef($monogram);
110 $comment_view->setTransactionTimeline($timeline);
111
112 return array(
113 $timeline,
114 $comment_view,
115 );
116 }
117
118 private function buildCurtainView(PhabricatorFile $file) {
119 $viewer = $this->getViewer();
120
121 $id = $file->getID();
122
123 $can_edit = PhabricatorPolicyFilter::hasCapability(
124 $viewer,
125 $file,
126 PhabricatorPolicyCapability::CAN_EDIT);
127
128 $curtain = $this->newCurtainView($file);
129
130 $can_download = !$file->getIsPartial();
131
132 if ($file->isViewableInBrowser()) {
133 $curtain->addAction(
134 id(new PhabricatorActionView())
135 ->setName(pht('View File'))
136 ->setIcon('fa-file-o')
137 ->setHref($file->getViewURI())
138 ->setDisabled(!$can_download)
139 ->setWorkflow(!$can_download));
140 } else {
141 $curtain->addAction(
142 id(new PhabricatorActionView())
143 ->setViewer($viewer)
144 ->setDownload($can_download)
145 ->setName(pht('Download File'))
146 ->setIcon('fa-download')
147 ->setHref($file->getDownloadURI())
148 ->setDisabled(!$can_download)
149 ->setWorkflow(!$can_download));
150 }
151
152 $curtain->addAction(
153 id(new PhabricatorActionView())
154 ->setName(pht('Edit File'))
155 ->setIcon('fa-pencil')
156 ->setHref($this->getApplicationURI("/edit/{$id}/"))
157 ->setWorkflow(!$can_edit)
158 ->setDisabled(!$can_edit));
159
160 $curtain->addAction(
161 id(new PhabricatorActionView())
162 ->setName(pht('Delete File'))
163 ->setIcon('fa-times')
164 ->setHref($this->getApplicationURI("/delete/{$id}/"))
165 ->setWorkflow(true)
166 ->setDisabled(!$can_edit));
167
168 $curtain->addAction(
169 id(new PhabricatorActionView())
170 ->setName(pht('View Transforms'))
171 ->setIcon('fa-crop')
172 ->setHref($this->getApplicationURI("/transforms/{$id}/")));
173
174 $phids = array();
175
176 $viewer_phid = $viewer->getPHID();
177 $author_phid = $file->getAuthorPHID();
178 if ($author_phid) {
179 $phids[] = $author_phid;
180 }
181
182 $handles = $viewer->loadHandles($phids);
183
184 if ($author_phid) {
185 $author_refs = id(new PHUICurtainObjectRefListView())
186 ->setViewer($viewer);
187
188 $author_ref = $author_refs->newObjectRefView()
189 ->setHandle($handles[$author_phid])
190 ->setEpoch($file->getDateCreated())
191 ->setHighlighted($author_phid === $viewer_phid);
192
193 $curtain->newPanel()
194 ->setHeaderText(pht('Authored By'))
195 ->appendChild($author_refs);
196 }
197
198 $curtain->newPanel()
199 ->setHeaderText(pht('Size'))
200 ->appendChild(phutil_format_bytes($file->getByteSize()));
201
202 $width = $file->getImageWidth();
203 $height = $file->getImageHeight();
204
205 if ($width || $height) {
206 $curtain->newPanel()
207 ->setHeaderText(pht('Dimensions'))
208 ->appendChild(
209 pht(
210 "%spx \xC3\x97 %spx",
211 new PhutilNumber($width),
212 new PhutilNumber($height)));
213 }
214
215 return $curtain;
216 }
217
218 private function buildPropertyViews(
219 PHUIObjectBoxView $box,
220 PhabricatorFile $file) {
221 $request = $this->getRequest();
222 $viewer = $request->getUser();
223
224 $tab_group = new PHUITabGroupView();
225 $box->addTabGroup($tab_group);
226
227 $finfo = new PHUIPropertyListView();
228
229 $tab_group->addTab(
230 id(new PHUITabView())
231 ->setName(pht('Details'))
232 ->setKey('details')
233 ->appendChild($finfo));
234
235 $finfo->addProperty(
236 pht('Mime Type'),
237 $file->getMimeType());
238
239 $ttl = $file->getTtl();
240 if ($ttl) {
241 $delta = $ttl - PhabricatorTime::getNow();
242
243 $finfo->addProperty(
244 pht('Expires'),
245 pht(
246 '%s (%s)',
247 phabricator_datetime($ttl, $viewer),
248 phutil_format_relative_time_detailed($delta)));
249 }
250
251 $is_image = $file->isViewableImage();
252 if ($is_image) {
253 $image_string = pht('Yes');
254 $cache_string = $file->getCanCDN() ? pht('Yes') : pht('No');
255 } else {
256 $image_string = pht('No');
257 $cache_string = pht('Not Applicable');
258 }
259
260 $types = array();
261 if ($file->isViewableImage()) {
262 $types[] = pht('Image');
263 }
264
265 if ($file->isVideo()) {
266 $types[] = pht('Video');
267 }
268
269 if ($file->isAudio()) {
270 $types[] = pht('Audio');
271 }
272
273 if ($file->getCanCDN()) {
274 $types[] = pht('Can CDN');
275 }
276
277 $builtin = $file->getBuiltinName();
278 if ($builtin !== null) {
279 $types[] = pht('Builtin ("%s")', $builtin);
280 }
281
282 if ($file->getIsProfileImage()) {
283 $types[] = pht('Profile');
284 }
285
286 if ($types) {
287 $types = implode(', ', $types);
288 $finfo->addProperty(pht('Attributes'), $types);
289 }
290
291 $finfo->addProperty(
292 pht('Storage Engine'),
293 $file->getStorageEngine());
294
295 $engine = $this->loadStorageEngine($file);
296 if ($engine && $engine->isChunkEngine()) {
297 $format_name = pht('Chunks');
298 } else {
299 $format_key = $file->getStorageFormat();
300 $format = PhabricatorFileStorageFormat::getFormat($format_key);
301 if ($format) {
302 $format_name = $format->getStorageFormatName();
303 } else {
304 $format_name = pht('Unknown ("%s")', $format_key);
305 }
306 }
307 $finfo->addProperty(pht('Storage Format'), $format_name);
308
309 $finfo->addProperty(
310 pht('Storage Handle'),
311 $file->getStorageHandle());
312
313 $custom_alt = $file->getCustomAltText();
314 if (phutil_nonempty_string($custom_alt)) {
315 $finfo->addProperty(pht('Custom Alt Text'), $custom_alt);
316 }
317
318 $default_alt = $file->getDefaultAltText();
319 if (phutil_nonempty_string($default_alt)) {
320 $finfo->addProperty(pht('Default Alt Text'), $default_alt);
321 }
322
323 $attachments_table = $this->newAttachmentsView($file);
324
325 $tab_group->addTab(
326 id(new PHUITabView())
327 ->setName(pht('Attached'))
328 ->setKey('attached')
329 ->appendChild($attachments_table));
330
331 $engine = $this->loadStorageEngine($file);
332 if ($engine) {
333 if ($engine->isChunkEngine()) {
334 $chunkinfo = new PHUIPropertyListView();
335
336 $tab_group->addTab(
337 id(new PHUITabView())
338 ->setName(pht('Chunks'))
339 ->setKey('chunks')
340 ->appendChild($chunkinfo));
341
342 $chunks = id(new PhabricatorFileChunkQuery())
343 ->setViewer($viewer)
344 ->withChunkHandles(array($file->getStorageHandle()))
345 ->execute();
346 $chunks = msort($chunks, 'getByteStart');
347
348 $rows = array();
349 $completed = array();
350 foreach ($chunks as $chunk) {
351 $is_complete = $chunk->getDataFilePHID();
352
353 $rows[] = array(
354 $chunk->getByteStart(),
355 $chunk->getByteEnd(),
356 ($is_complete ? pht('Yes') : pht('No')),
357 );
358
359 if ($is_complete) {
360 $completed[] = $chunk;
361 }
362 }
363
364 $table = id(new AphrontTableView($rows))
365 ->setHeaders(
366 array(
367 pht('Offset'),
368 pht('End'),
369 pht('Complete'),
370 ))
371 ->setColumnClasses(
372 array(
373 '',
374 '',
375 'wide',
376 ));
377
378 $chunkinfo->addProperty(
379 pht('Total Chunks'),
380 count($chunks));
381
382 $chunkinfo->addProperty(
383 pht('Completed Chunks'),
384 count($completed));
385
386 $chunkinfo->addRawContent($table);
387 }
388 }
389
390 }
391
392 private function loadStorageEngine(PhabricatorFile $file) {
393
394 try {
395 return $file->instantiateStorageEngine();
396 } catch (Throwable $ex) {
397 // Don't bother raising this anywhere for now.
398 }
399
400 return null;
401 }
402
403 private function newFileContent(PhabricatorFile $file) {
404 $request = $this->getRequest();
405
406 $ref = id(new PhabricatorDocumentRef())
407 ->setFile($file);
408
409 $engine = id(new PhabricatorFileDocumentRenderingEngine())
410 ->setRequest($request);
411
412 return $engine->newDocumentView($ref);
413 }
414
415 private function newAttachmentsView(PhabricatorFile $file) {
416 $viewer = $this->getViewer();
417
418 $attachments = id(new PhabricatorFileAttachmentQuery())
419 ->setViewer($viewer)
420 ->withFilePHIDs(array($file->getPHID()))
421 ->execute();
422
423 $handles = $viewer->loadHandles(mpull($attachments, 'getObjectPHID'));
424
425 $rows = array();
426
427 $mode_map = PhabricatorFileAttachment::getModeNameMap();
428
429 foreach ($attachments as $attachment) {
430 $object_phid = $attachment->getObjectPHID();
431 $handle = $handles[$object_phid];
432
433 $attachment_mode = $attachment->getAttachmentMode();
434
435 $mode_name = idx($mode_map, $attachment_mode);
436 if ($mode_name === null) {
437 $mode_name = pht('Unknown ("%s")', $attachment_mode);
438 }
439
440 $detach_uri = urisprintf(
441 '/file/ui/detach/%s/%s/',
442 $object_phid,
443 $file->getPHID());
444
445 $is_disabled = !$attachment->canDetach();
446
447 $detach_button = id(new PHUIButtonView())
448 ->setHref($detach_uri)
449 ->setTag('a')
450 ->setWorkflow(true)
451 ->setDisabled($is_disabled)
452 ->setColor(PHUIButtonView::GREY)
453 ->setSize(PHUIButtonView::SMALL)
454 ->setText(pht('Detach File'));
455
456 $rows[] = array(
457 $handle->renderLink(),
458 $mode_name,
459 $detach_button,
460 );
461 }
462
463 $table = id(new AphrontTableView($rows))
464 ->setHeaders(
465 array(
466 pht('Attached To'),
467 pht('Mode'),
468 null,
469 ))
470 ->setColumnClasses(
471 array(
472 'pri wide',
473 null,
474 null,
475 ));
476
477 return $table;
478 }
479
480
481}