@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
at upstream/main 673 lines 20 kB view raw
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}