@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 PhrictionDocumentController
4 extends PhrictionController {
5
6 private $slug;
7
8 public function shouldAllowPublic() {
9 return true;
10 }
11
12
13 public function handleRequest(AphrontRequest $request) {
14 $viewer = $request->getViewer();
15 $this->slug = $request->getURIData('slug');
16
17 $slug = PhabricatorSlug::normalize($this->slug);
18 if ($slug != $this->slug) {
19 $uri = PhrictionDocument::getSlugURI($slug);
20 // Canonicalize pages to their one true URI.
21 return id(new AphrontRedirectResponse())->setURI($uri);
22 }
23
24 $version_note = null;
25 $core_content = '';
26 $move_notice = '';
27 $properties = null;
28 $content = null;
29 $toc = null;
30
31 $is_draft = false;
32
33 $document = id(new PhrictionDocumentQuery())
34 ->setViewer($viewer)
35 ->withSlugs(array($slug))
36 ->needContent(true)
37 ->executeOne();
38 if (!$document) {
39 $document = PhrictionDocument::initializeNewDocument($viewer, $slug);
40 if ($slug == '/') {
41 $title = pht('Welcome to Phriction');
42 $subtitle = pht('Phriction is a simple and easy to use wiki for '.
43 'keeping track of documents and their changes.');
44 $page_title = pht('Welcome');
45 $create_text = pht('Edit this Document');
46 $this->setShowingWelcomeDocument(true);
47
48
49 } else {
50 $title = pht('No Document Here');
51 $subtitle = pht('There is no document here, but you may create it.');
52 $page_title = pht('Page Not Found');
53 $create_text = pht('Create this Document');
54 }
55
56 $create_uri = '/phriction/edit/?slug='.$slug;
57 $create_button = id(new PHUIButtonView())
58 ->setTag('a')
59 ->setText($create_text)
60 ->setHref($create_uri)
61 ->setColor(PHUIButtonView::GREEN);
62
63 $core_content = id(new PHUIBigInfoView())
64 ->setIcon('fa-book')
65 ->setTitle($title)
66 ->setDescription($subtitle)
67 ->addAction($create_button);
68
69 } else {
70 $max_version = (int)$document->getMaxVersion();
71
72 $version = $request->getInt('v');
73 if ($version) {
74 $content = id(new PhrictionContentQuery())
75 ->setViewer($viewer)
76 ->withDocumentPHIDs(array($document->getPHID()))
77 ->withVersions(array($version))
78 ->executeOne();
79 if (!$content) {
80 return new Aphront404Response();
81 }
82
83 // When the "v" parameter exists, the user is in history mode so we
84 // show this header even if they're looking at the current version
85 // of the document. This keeps the next/previous links working.
86
87 $view_version = (int)$content->getVersion();
88 $published_version = (int)$document->getContent()->getVersion();
89
90 if ($view_version < $published_version) {
91 $version_note = pht(
92 'You are viewing an older version of this document, as it '.
93 'appeared on %s.',
94 phabricator_datetime($content->getDateCreated(), $viewer));
95 } else if ($view_version > $published_version) {
96 $is_draft = true;
97 $version_note = pht(
98 'You are viewing an unpublished draft of this document.');
99 } else {
100 $version_note = pht(
101 'You are viewing the current published version of this document.');
102 }
103
104 $version_note = array(
105 phutil_tag(
106 'strong',
107 array(),
108 pht('Version %d of %d: ', $view_version, $max_version)),
109 ' ',
110 $version_note,
111 );
112
113 $version_note = id(new PHUIInfoView())
114 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
115 ->appendChild($version_note);
116
117 $document_uri = new PhutilURI($document->getURI());
118
119 if ($view_version > 1) {
120 $previous_uri = $document_uri->alter('v', ($view_version - 1));
121 } else {
122 $previous_uri = null;
123 }
124
125 if ($view_version !== $published_version) {
126 $current_uri = $document_uri->alter('v', $published_version);
127 } else {
128 $current_uri = null;
129 }
130
131 if ($view_version < $max_version) {
132 $next_uri = $document_uri->alter('v', ($view_version + 1));
133 } else {
134 $next_uri = null;
135 }
136
137 if ($view_version !== $max_version) {
138 $draft_uri = $document_uri->alter('v', $max_version);
139 } else {
140 $draft_uri = null;
141 }
142
143 $button_bar = id(new PHUIButtonBarView())
144 ->addButton(
145 id(new PHUIButtonView())
146 ->setTag('a')
147 ->setColor('grey')
148 ->setIcon('fa-backward')
149 ->setDisabled(!$previous_uri)
150 ->setHref($previous_uri)
151 ->setText(pht('Previous')))
152 ->addButton(
153 id(new PHUIButtonView())
154 ->setTag('a')
155 ->setColor('grey')
156 ->setIcon('fa-file-o')
157 ->setDisabled(!$current_uri)
158 ->setHref($current_uri)
159 ->setText(pht('Published')))
160 ->addButton(
161 id(new PHUIButtonView())
162 ->setTag('a')
163 ->setColor('grey')
164 ->setIcon('fa-forward', false)
165 ->setDisabled(!$next_uri)
166 ->setHref($next_uri)
167 ->setText(pht('Next')))
168 ->addButton(
169 id(new PHUIButtonView())
170 ->setTag('a')
171 ->setColor('grey')
172 ->setIcon('fa-fast-forward', false)
173 ->setDisabled(!$draft_uri)
174 ->setHref($draft_uri)
175 ->setText(pht('Draft')));
176
177 require_celerity_resource('phui-document-view-css');
178
179 $version_note = array(
180 $version_note,
181 phutil_tag(
182 'div',
183 array(
184 'class' => 'phui-document-version-navigation',
185 ),
186 $button_bar),
187 );
188 } else {
189 $content = $document->getContent();
190
191 if ($content->getVersion() < $document->getMaxVersion()) {
192 $can_edit = PhabricatorPolicyFilter::hasCapability(
193 $viewer,
194 $document,
195 PhabricatorPolicyCapability::CAN_EDIT);
196 if ($can_edit) {
197 $document_uri = new PhutilURI($document->getURI());
198 $draft_uri = $document_uri->alter('v', $document->getMaxVersion());
199
200 $draft_link = phutil_tag(
201 'a',
202 array(
203 'href' => $draft_uri,
204 ),
205 pht('View Draft Version'));
206
207 $draft_link = phutil_tag('strong', array(), $draft_link);
208
209 $version_note = id(new PHUIInfoView())
210 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
211 ->appendChild(
212 array(
213 pht('This document has unpublished draft changes.'),
214 ' ',
215 $draft_link,
216 ));
217 }
218 }
219 }
220
221 $page_title = $content->getTitle();
222 $properties = $this
223 ->buildPropertyListView($document, $content, $slug);
224
225 $doc_status = $document->getStatus();
226 $current_status = $content->getChangeType();
227 if ($current_status == PhrictionChangeType::CHANGE_EDIT ||
228 $current_status == PhrictionChangeType::CHANGE_MOVE_HERE) {
229
230 $remarkup_view = $content->newRemarkupView($viewer);
231
232 $core_content = $remarkup_view->render();
233
234 $toc = $remarkup_view->getTableOfContents();
235 $toc = $this->getToc($toc);
236
237 } else if ($current_status == PhrictionChangeType::CHANGE_DELETE) {
238 $notice = new PHUIInfoView();
239 $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
240 $notice->setTitle(pht('Document Deleted'));
241 $notice->appendChild(
242 pht('This document has been deleted. You can edit it to put new '.
243 'content here, or use history to revert to an earlier version.'));
244 $core_content = $notice->render();
245 } else if ($current_status == PhrictionChangeType::CHANGE_STUB) {
246 $notice = new PHUIInfoView();
247 $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
248 $notice->setTitle(pht('Empty Document'));
249 $notice->appendChild(
250 pht('This document is empty. You can edit it to put some proper '.
251 'content here.'));
252 $core_content = $notice->render();
253 } else if ($current_status == PhrictionChangeType::CHANGE_MOVE_AWAY) {
254 $new_doc_id = $content->getChangeRef();
255 $slug_uri = null;
256
257 // If the new document exists and the viewer can see it, provide a link
258 // to it. Otherwise, render a generic message.
259 $new_docs = id(new PhrictionDocumentQuery())
260 ->setViewer($viewer)
261 ->withIDs(array($new_doc_id))
262 ->execute();
263 if ($new_docs) {
264 $new_doc = head($new_docs);
265 $slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug());
266 }
267
268 $notice = id(new PHUIInfoView())
269 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE);
270
271 if ($slug_uri) {
272 $notice->appendChild(
273 phutil_tag(
274 'p',
275 array(),
276 pht(
277 'This document has been moved to %s. You can edit it to put '.
278 'new content here, or use history to revert to an earlier '.
279 'version.',
280 phutil_tag('a', array('href' => $slug_uri), $slug_uri))));
281 } else {
282 $notice->appendChild(
283 phutil_tag(
284 'p',
285 array(),
286 pht(
287 'This document has been moved. You can edit it to put new '.
288 'content here, or use history to revert to an earlier '.
289 'version.')));
290 }
291
292 $core_content = $notice->render();
293 } else {
294 throw new Exception(pht("Unknown document status '%s'!", $doc_status));
295 }
296 }
297
298 $children = $this->renderDocumentChildren($slug);
299
300 $curtain = null;
301 if ($document->getID()) {
302 $curtain = $this->buildCurtain($document, $content);
303 }
304
305 $crumbs = $this->buildApplicationCrumbs();
306 $crumbs->setBorder(true);
307 $crumb_views = $this->renderBreadcrumbs($slug);
308 foreach ($crumb_views as $view) {
309 $crumbs->addCrumb($view);
310 }
311
312 $header = id(new PHUIHeaderView())
313 ->setUser($viewer)
314 ->setPolicyObject($document)
315 ->setHeader($page_title);
316
317 if ($is_draft) {
318 $draft_tag = id(new PHUITagView())
319 ->setName(pht('Draft'))
320 ->setIcon('fa-spinner')
321 ->setColor('pink')
322 ->setType(PHUITagView::TYPE_SHADE);
323
324 $header->addTag($draft_tag);
325 } else if ($content) {
326 $header->setEpoch($content->getDateCreated());
327 }
328
329 $prop_list = null;
330 if ($properties) {
331 $prop_list = new PHUIPropertyGroupView();
332 $prop_list->addPropertyList($properties);
333 }
334 $prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list);
335
336 $page_content = id(new PHUIDocumentView())
337 ->setBanner($version_note)
338 ->setHeader($header)
339 ->setToc($toc)
340 ->appendChild(
341 array(
342 $move_notice,
343 $core_content,
344 ));
345
346 if ($curtain) {
347 $page_content->setCurtain($curtain);
348 }
349
350 if ($document->getPHID()) {
351 $timeline = $this->buildTransactionTimeline(
352 $document,
353 new PhrictionTransactionQuery());
354
355 $edit_engine = id(new PhrictionDocumentEditEngine())
356 ->setViewer($viewer)
357 ->setTargetObject($document);
358
359 $comment_view = $edit_engine
360 ->buildEditEngineCommentView($document);
361 } else {
362 $timeline = null;
363 $comment_view = null;
364 }
365
366 return $this->newPage()
367 ->setTitle($page_title)
368 ->setCrumbs($crumbs)
369 ->setPageObjectPHIDs(array($document->getPHID()))
370 ->appendChild(
371 array(
372 $page_content,
373 $prop_list,
374 phutil_tag(
375 'div',
376 array(
377 'class' => 'phui-document-view-pro-box',
378 ),
379 array(
380 $children,
381 $timeline,
382 $comment_view,
383 )),
384 ));
385
386 }
387
388 private function buildPropertyListView(
389 PhrictionDocument $document,
390 PhrictionContent $content,
391 $slug) {
392
393 $viewer = $this->getViewer();
394
395 $view = id(new PHUIPropertyListView())
396 ->setUser($viewer);
397
398 $view->addProperty(
399 pht('Last Author'),
400 $viewer->renderHandle($content->getAuthorPHID()));
401
402 $view->addProperty(
403 pht('Last Edited'),
404 phabricator_dual_datetime($content->getDateCreated(), $viewer));
405
406 return $view;
407 }
408
409 private function buildCurtain(
410 PhrictionDocument $document,
411 PhrictionContent $content) {
412 $viewer = $this->getViewer();
413
414 $can_edit = PhabricatorPolicyFilter::hasCapability(
415 $viewer,
416 $document,
417 PhabricatorPolicyCapability::CAN_EDIT);
418
419 $slug = PhabricatorSlug::normalize($this->slug);
420 $id = $document->getID();
421
422 $curtain = $this->newCurtainView($document);
423
424 $curtain->addAction(
425 id(new PhabricatorActionView())
426 ->setName(pht('Edit Document'))
427 ->setDisabled(!$can_edit)
428 ->setIcon('fa-pencil')
429 ->setHref('/phriction/edit/'.$document->getID().'/'));
430
431 $curtain->addAction(
432 id(new PhabricatorActionView())
433 ->setName(pht('View History'))
434 ->setIcon('fa-history')
435 ->setHref(PhrictionDocument::getSlugURI($slug, 'history')));
436
437 $is_current = false;
438 $content_id = null;
439 $is_draft = false;
440 if ($content) {
441 if ($content->getPHID() == $document->getContentPHID()) {
442 $is_current = true;
443 }
444 $content_id = $content->getID();
445
446 $current_version = $document->getContent()->getVersion();
447 $is_draft = ($content->getVersion() >= $current_version);
448 }
449 $can_publish = ($can_edit && $content && !$is_current);
450
451 if ($is_draft) {
452 $publish_name = pht('Publish Draft');
453 } else {
454 $publish_name = pht('Publish Older Version');
455 }
456
457 // If you're looking at the current version; and it's an unpublished
458 // draft; and you can publish it, add a UI hint that this might be an
459 // interesting action to take.
460 $hint_publish = false;
461 if ($is_draft) {
462 if ($can_publish) {
463 if ($document->getMaxVersion() == $content->getVersion()) {
464 $hint_publish = true;
465 }
466 }
467 }
468
469 $publish_uri = "/phriction/publish/{$id}/{$content_id}/";
470
471 $curtain->addAction(
472 id(new PhabricatorActionView())
473 ->setName($publish_name)
474 ->setIcon('fa-upload')
475 ->setSelected($hint_publish)
476 ->setDisabled(!$can_publish)
477 ->setWorkflow(true)
478 ->setHref($publish_uri));
479
480 if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) {
481 $curtain->addAction(
482 id(new PhabricatorActionView())
483 ->setName(pht('Move Document'))
484 ->setDisabled(!$can_edit)
485 ->setIcon('fa-arrows')
486 ->setHref('/phriction/move/'.$document->getID().'/')
487 ->setWorkflow(true));
488
489 $curtain->addAction(
490 id(new PhabricatorActionView())
491 ->setName(pht('Delete Document'))
492 ->setDisabled(!$can_edit)
493 ->setIcon('fa-times')
494 ->setHref('/phriction/delete/'.$document->getID().'/')
495 ->setWorkflow(true));
496 }
497
498 $print_uri = PhrictionDocument::getSlugURI($slug).'?__print__=1';
499
500 $curtain->addAction(
501 id(new PhabricatorActionView())
502 ->setName(pht('Printable Page'))
503 ->setIcon('fa-print')
504 ->setOpenInNewWindow(true)
505 ->setHref($print_uri));
506
507 return $curtain;
508 }
509
510 private function renderDocumentChildren($slug) {
511
512 $d_child = PhabricatorSlug::getDepth($slug) + 1;
513 $d_grandchild = PhabricatorSlug::getDepth($slug) + 2;
514 $limit = 250;
515
516 $query = id(new PhrictionDocumentQuery())
517 ->setViewer($this->getRequest()->getUser())
518 ->withDepths(array($d_child, $d_grandchild))
519 ->withSlugPrefix($slug == '/' ? '' : $slug)
520 ->withStatuses(array(
521 PhrictionDocumentStatus::STATUS_EXISTS,
522 PhrictionDocumentStatus::STATUS_STUB,
523 ))
524 ->setLimit($limit)
525 ->setOrder(PhrictionDocumentQuery::ORDER_HIERARCHY)
526 ->needContent(true);
527
528 $children = $query->execute();
529 if (!$children) {
530 return;
531 }
532
533 // We're going to render in one of three modes to try to accommodate
534 // different information scales:
535 //
536 // - If we found fewer than $limit rows, we know we have all the children
537 // and grandchildren and there aren't all that many. We can just render
538 // everything.
539 // - If we found $limit rows but the results included some grandchildren,
540 // we just throw them out and render only the children, as we know we
541 // have them all.
542 // - If we found $limit rows and the results have no grandchildren, we
543 // have a ton of children. Render them and then let the user know that
544 // this is not an exhaustive list.
545
546 if (count($children) == $limit) {
547 $more_children = true;
548 foreach ($children as $child) {
549 if ($child->getDepth() == $d_grandchild) {
550 $more_children = false;
551 }
552 }
553 $show_grandchildren = false;
554 } else {
555 $show_grandchildren = true;
556 $more_children = false;
557 }
558
559 $children_dicts = array();
560 $grandchildren_dicts = array();
561 foreach ($children as $key => $child) {
562 $child_dict = array(
563 'slug' => $child->getSlug(),
564 'depth' => $child->getDepth(),
565 'title' => $child->getContent()->getTitle(),
566 );
567 if ($child->getDepth() == $d_child) {
568 $children_dicts[] = $child_dict;
569 continue;
570 } else {
571 unset($children[$key]);
572 if ($show_grandchildren) {
573 $ancestors = PhabricatorSlug::getAncestry($child->getSlug());
574 $grandchildren_dicts[end($ancestors)][] = $child_dict;
575 }
576 }
577 }
578
579 // Fill in any missing children.
580 $known_slugs = mpull($children, null, 'getSlug');
581 foreach ($grandchildren_dicts as $slug => $ignored) {
582 if (empty($known_slugs[$slug])) {
583 $children_dicts[] = array(
584 'slug' => $slug,
585 'depth' => $d_child,
586 'title' => PhabricatorSlug::getDefaultTitle($slug),
587 'empty' => true,
588 );
589 }
590 }
591
592 $children_dicts = isort($children_dicts, 'title');
593
594 $list = array();
595 foreach ($children_dicts as $child) {
596 $list[] = hsprintf('<li class="remarkup-list-item">');
597 $list[] = $this->renderChildDocumentLink($child);
598 $grand = idx($grandchildren_dicts, $child['slug'], array());
599 if ($grand) {
600 $list[] = hsprintf('<ul class="remarkup-list">');
601 foreach ($grand as $grandchild) {
602 $list[] = hsprintf('<li class="remarkup-list-item">');
603 $list[] = $this->renderChildDocumentLink($grandchild);
604 $list[] = hsprintf('</li>');
605 }
606 $list[] = hsprintf('</ul>');
607 }
608 $list[] = hsprintf('</li>');
609 }
610 if ($more_children) {
611 $list[] = phutil_tag(
612 'li',
613 array(
614 'class' => 'remarkup-list-item',
615 ),
616 pht('More...'));
617 }
618
619 $header = id(new PHUIHeaderView())
620 ->setHeader(pht('Document Hierarchy'));
621
622 $box = id(new PHUIObjectBoxView())
623 ->setHeader($header)
624 ->appendChild(phutil_tag(
625 'div',
626 array(
627 'class' => 'phabricator-remarkup mlt mlb',
628 ),
629 phutil_tag(
630 'ul',
631 array(
632 'class' => 'remarkup-list',
633 ),
634 $list)));
635
636 return $box;
637 }
638
639 private function renderChildDocumentLink(array $info) {
640 $title = nonempty($info['title'], pht('(Untitled Document)'));
641 $item = phutil_tag(
642 'a',
643 array(
644 'href' => PhrictionDocument::getSlugURI($info['slug']),
645 ),
646 $title);
647
648 if (isset($info['empty'])) {
649 $item = phutil_tag('em', array(), $item);
650 }
651
652 return $item;
653 }
654
655 protected function getDocumentSlug() {
656 return $this->slug;
657 }
658
659 protected function getToc($toc) {
660
661 if ($toc) {
662 $toc = phutil_tag_div('phui-document-toc-content', array(
663 phutil_tag_div(
664 'phui-document-toc-header',
665 pht('Contents')),
666 $toc,
667 ));
668 }
669
670 return $toc;
671 }
672
673}