diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php index b66a3b3094..f443ad657b 100644 --- a/src/applications/phriction/controller/PhrictionDiffController.php +++ b/src/applications/phriction/controller/PhrictionDiffController.php @@ -1,257 +1,258 @@ getViewer(); $id = $request->getURIData('id'); $document = id(new PhrictionDocumentQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needContent(true) ->executeOne(); if (!$document) { return new Aphront404Response(); } $current = $document->getContent(); $l = $request->getInt('l'); $r = $request->getInt('r'); $ref = $request->getStr('ref'); if ($ref) { list($l, $r) = explode(',', $ref); } - $content = id(new PhrictionContent())->loadAllWhere( - 'documentID = %d AND version IN (%Ld)', - $document->getID(), - array($l, $r)); + $content = id(new PhrictionContentQuery()) + ->setViewer($viewer) + ->withDocumentPHIDs(array($document->getPHID())) + ->withVersions(array($l, $r)) + ->execute(); $content = mpull($content, null, 'getVersion'); $content_l = idx($content, $l, null); $content_r = idx($content, $r, null); if (!$content_l || !$content_r) { return new Aphront404Response(); } $text_l = $content_l->getContent(); $text_r = $content_r->getContent(); $diff_view = id(new PhabricatorApplicationTransactionTextDiffDetailView()) ->setOldText($text_l) ->setNewText($text_r); $changes = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Content Changes')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild( phutil_tag( 'div', array( 'class' => 'prose-diff-frame', ), $diff_view)); require_celerity_resource('phriction-document-css'); $slug = $document->getSlug(); $revert_l = $this->renderRevertButton($content_l, $current); $revert_r = $this->renderRevertButton($content_r, $current); $crumbs = $this->buildApplicationCrumbs(); $crumb_views = $this->renderBreadcrumbs($slug); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } $crumbs->addTextCrumb( pht('History'), PhrictionDocument::getSlugURI($slug, 'history')); $title = pht('Version %s vs %s', $l, $r); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setHeaderIcon('fa-history'); $crumbs->addTextCrumb($title, $request->getRequestURI()); $comparison_table = $this->renderComparisonTable( array( $content_r, $content_l, )); $navigation_table = null; if ($l + 1 == $r) { $nav_l = ($l > 1); $nav_r = ($r != $current->getVersion()); $uri = $request->getRequestURI(); if ($nav_l) { $link_l = phutil_tag( 'a', array( 'href' => $uri->alter('l', $l - 1)->alter('r', $r - 1), 'class' => 'button button-grey', ), pht("\xC2\xAB Previous Change")); } else { $link_l = phutil_tag( 'a', array( 'href' => '#', 'class' => 'button button-grey disabled', ), pht('Original Change')); } $link_r = null; if ($nav_r) { $link_r = phutil_tag( 'a', array( 'href' => $uri->alter('l', $l + 1)->alter('r', $r + 1), 'class' => 'button button-grey', ), pht("Next Change \xC2\xBB")); } else { $link_r = phutil_tag( 'a', array( 'href' => '#', 'class' => 'button button-grey disabled', ), pht('Most Recent Change')); } $navigation_table = phutil_tag( 'table', array('class' => 'phriction-history-nav-table'), phutil_tag('tr', array(), array( phutil_tag('td', array('class' => 'nav-prev'), $link_l), phutil_tag('td', array('class' => 'nav-next'), $link_r), ))); } $output = hsprintf( '
'. '%s%s'. ''. ''. '
%s%s
'. '
', $comparison_table->render(), $navigation_table, $revert_l, $revert_r); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Edits')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($output); $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $object_box, $changes, )); return $this->newPage() ->setTitle(pht('Document History')) ->setCrumbs($crumbs) ->appendChild($view); } private function renderRevertButton( PhrictionContent $content, PhrictionContent $current) { $document_id = $content->getDocumentID(); $version = $content->getVersion(); $hidden_statuses = array( PhrictionChangeType::CHANGE_DELETE => true, // Silly PhrictionChangeType::CHANGE_MOVE_AWAY => true, // Plain silly PhrictionChangeType::CHANGE_STUB => true, // Utterly silly ); if (isset($hidden_statuses[$content->getChangeType()])) { // Don't show an edit/revert button for changes which deleted, moved or // stubbed the content since it's silly. return null; } if ($content->getID() == $current->getID()) { return phutil_tag( 'a', array( 'href' => '/phriction/edit/'.$document_id.'/', 'class' => 'button button-grey', ), pht('Edit Current Version')); } return phutil_tag( 'a', array( 'href' => '/phriction/edit/'.$document_id.'/?revert='.$version, 'class' => 'button button-grey', ), pht('Revert to Version %s...', $version)); } private function renderComparisonTable(array $content) { assert_instances_of($content, 'PhrictionContent'); $viewer = $this->getViewer(); $phids = mpull($content, 'getAuthorPHID'); $handles = $this->loadViewerHandles($phids); $list = new PHUIObjectItemListView(); $first = true; foreach ($content as $c) { $author = $handles[$c->getAuthorPHID()]->renderLink(); $item = id(new PHUIObjectItemView()) ->setHeader(pht('%s by %s, %s', PhrictionChangeType::getChangeTypeLabel($c->getChangeType()), $author, pht('Version %s', $c->getVersion()))) ->addAttribute(pht('%s %s', phabricator_date($c->getDateCreated(), $viewer), phabricator_time($c->getDateCreated(), $viewer))); if ($c->getDescription()) { $item->addAttribute($c->getDescription()); } if ($first == true) { $item->setStatusIcon('fa-file green'); $first = false; } else { $item->setStatusIcon('fa-file red'); } $list->addItem($item); } return $list; } } diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index a5189cc72c..8377cf7d29 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -1,495 +1,498 @@ getViewer(); $this->slug = $request->getURIData('slug'); $slug = PhabricatorSlug::normalize($this->slug); if ($slug != $this->slug) { $uri = PhrictionDocument::getSlugURI($slug); // Canonicalize pages to their one true URI. return id(new AphrontRedirectResponse())->setURI($uri); } require_celerity_resource('phriction-document-css'); - $document = id(new PhrictionDocumentQuery()) - ->setViewer($viewer) - ->withSlugs(array($slug)) - ->executeOne(); - $version_note = null; $core_content = ''; $move_notice = ''; $properties = null; $content = null; $toc = null; + $document = id(new PhrictionDocumentQuery()) + ->setViewer($viewer) + ->withSlugs(array($slug)) + ->needContent(true) + ->executeOne(); if (!$document) { $document = PhrictionDocument::initializeNewDocument($viewer, $slug); if ($slug == '/') { $title = pht('Welcome to Phriction'); $subtitle = pht('Phriction is a simple and easy to use wiki for '. 'keeping track of documents and their changes.'); $page_title = pht('Welcome'); $create_text = pht('Edit this Document'); } else { $title = pht('No Document Here'); $subtitle = pht('There is no document here, but you may create it.'); $page_title = pht('Page Not Found'); $create_text = pht('Create this Document'); } $create_uri = '/phriction/edit/?slug='.$slug; $create_button = id(new PHUIButtonView()) ->setTag('a') ->setText($create_text) ->setHref($create_uri) ->setColor(PHUIButtonView::GREEN); $core_content = id(new PHUIBigInfoView()) ->setIcon('fa-book') ->setTitle($title) ->setDescription($subtitle) ->addAction($create_button); } else { $version = $request->getInt('v'); if ($version) { - $content = id(new PhrictionContent())->loadOneWhere( - 'documentID = %d AND version = %d', - $document->getID(), - $version); + $content = id(new PhrictionContentQuery()) + ->setViewer($viewer) + ->withDocumentPHIDs(array($document->getPHID())) + ->withVersions(array($version)) + ->executeOne(); if (!$content) { return new Aphront404Response(); } if ($content->getID() != $document->getContentID()) { - $vdate = phabricator_datetime($content->getDateCreated(), $viewer); - $version_note = new PHUIInfoView(); - $version_note->setSeverity(PHUIInfoView::SEVERITY_NOTICE); - $version_note->appendChild( - pht('You are viewing an older version of this document, as it '. - 'appeared on %s.', $vdate)); + $version_note = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NOTICE) + ->appendChild( + pht( + 'You are viewing an older version of this document, as it '. + 'appeared on %s.', + phabricator_datetime($content->getDateCreated(), $viewer))); } } else { - $content = id(new PhrictionContent())->load($document->getContentID()); + $content = $document->getContent(); } + $page_title = $content->getTitle(); $properties = $this ->buildPropertyListView($document, $content, $slug); $doc_status = $document->getStatus(); $current_status = $content->getChangeType(); if ($current_status == PhrictionChangeType::CHANGE_EDIT || $current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { $remarkup_view = $content->newRemarkupView($viewer); $core_content = $remarkup_view->render(); $toc = $remarkup_view->getTableOfContents(); $toc = $this->getToc($toc); } else if ($current_status == PhrictionChangeType::CHANGE_DELETE) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $notice->setTitle(pht('Document Deleted')); $notice->appendChild( pht('This document has been deleted. You can edit it to put new '. 'content here, or use history to revert to an earlier version.')); $core_content = $notice->render(); } else if ($current_status == PhrictionChangeType::CHANGE_STUB) { $notice = new PHUIInfoView(); $notice->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $notice->setTitle(pht('Empty Document')); $notice->appendChild( pht('This document is empty. You can edit it to put some proper '. 'content here.')); $core_content = $notice->render(); } else if ($current_status == PhrictionChangeType::CHANGE_MOVE_AWAY) { $new_doc_id = $content->getChangeRef(); $slug_uri = null; // If the new document exists and the viewer can see it, provide a link // to it. Otherwise, render a generic message. $new_docs = id(new PhrictionDocumentQuery()) ->setViewer($viewer) ->withIDs(array($new_doc_id)) ->execute(); if ($new_docs) { $new_doc = head($new_docs); $slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug()); } $notice = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); if ($slug_uri) { $notice->appendChild( phutil_tag( 'p', array(), pht( 'This document has been moved to %s. You can edit it to put '. 'new content here, or use history to revert to an earlier '. 'version.', phutil_tag('a', array('href' => $slug_uri), $slug_uri)))); } else { $notice->appendChild( phutil_tag( 'p', array(), pht( 'This document has been moved. You can edit it to put new '. 'content here, or use history to revert to an earlier '. 'version.'))); } $core_content = $notice->render(); } else { throw new Exception(pht("Unknown document status '%s'!", $doc_status)); } $move_notice = null; if ($current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { $from_doc_id = $content->getChangeRef(); $slug_uri = null; // If the old document exists and is visible, provide a link to it. $from_docs = id(new PhrictionDocumentQuery()) ->setViewer($viewer) ->withIDs(array($from_doc_id)) ->execute(); if ($from_docs) { $from_doc = head($from_docs); $slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug()); } $move_notice = id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); if ($slug_uri) { $move_notice->appendChild( pht( 'This document was moved from %s.', phutil_tag('a', array('href' => $slug_uri), $slug_uri))); } else { // Render this for consistency, even though it's a bit silly. $move_notice->appendChild( pht('This document was moved from elsewhere.')); } } } $children = $this->renderDocumentChildren($slug); $actions = $this->buildActionView($viewer, $document); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); $crumb_views = $this->renderBreadcrumbs($slug); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setPolicyObject($document) ->setHeader($page_title) ->setActionList($actions); if ($content) { $header->setEpoch($content->getDateCreated()); } $prop_list = null; if ($properties) { $prop_list = new PHUIPropertyGroupView(); $prop_list->addPropertyList($properties); } $prop_list = phutil_tag_div('phui-document-view-pro-box', $prop_list); $page_content = id(new PHUIDocumentViewPro()) ->setHeader($header) ->setToc($toc) ->appendChild( array( $version_note, $move_notice, $core_content, )); return $this->newPage() ->setTitle($page_title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($document->getPHID())) ->appendChild(array( $page_content, $prop_list, $children, )); } private function buildPropertyListView( PhrictionDocument $document, PhrictionContent $content, $slug) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($document); $view->addProperty( pht('Last Author'), $viewer->renderHandle($content->getAuthorPHID())); return $view; } private function buildActionView( PhabricatorUser $viewer, PhrictionDocument $document) { $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $document, PhabricatorPolicyCapability::CAN_EDIT); $slug = PhabricatorSlug::normalize($this->slug); $action_view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($document); if (!$document->getID()) { return $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Create This Document')) ->setIcon('fa-plus-square') ->setHref('/phriction/edit/?slug='.$slug)); } $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Document')) ->setDisabled(!$can_edit) ->setIcon('fa-pencil') ->setHref('/phriction/edit/'.$document->getID().'/')); if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) { $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Move Document')) ->setDisabled(!$can_edit) ->setIcon('fa-arrows') ->setHref('/phriction/move/'.$document->getID().'/') ->setWorkflow(true)); $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Document')) ->setDisabled(!$can_edit) ->setIcon('fa-times') ->setHref('/phriction/delete/'.$document->getID().'/') ->setWorkflow(true)); } $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setIcon('fa-list') ->setHref(PhrictionDocument::getSlugURI($slug, 'history'))); $print_uri = PhrictionDocument::getSlugURI($slug).'?__print__=1'; $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Printable Page')) ->setIcon('fa-print') ->setOpenInNewWindow(true) ->setHref($print_uri)); return $action_view; } private function renderDocumentChildren($slug) { $d_child = PhabricatorSlug::getDepth($slug) + 1; $d_grandchild = PhabricatorSlug::getDepth($slug) + 2; $limit = 250; $query = id(new PhrictionDocumentQuery()) ->setViewer($this->getRequest()->getUser()) ->withDepths(array($d_child, $d_grandchild)) ->withSlugPrefix($slug == '/' ? '' : $slug) ->withStatuses(array( PhrictionDocumentStatus::STATUS_EXISTS, PhrictionDocumentStatus::STATUS_STUB, )) ->setLimit($limit) ->setOrder(PhrictionDocumentQuery::ORDER_HIERARCHY) ->needContent(true); $children = $query->execute(); if (!$children) { return; } // We're going to render in one of three modes to try to accommodate // different information scales: // // - If we found fewer than $limit rows, we know we have all the children // and grandchildren and there aren't all that many. We can just render // everything. // - If we found $limit rows but the results included some grandchildren, // we just throw them out and render only the children, as we know we // have them all. // - If we found $limit rows and the results have no grandchildren, we // have a ton of children. Render them and then let the user know that // this is not an exhaustive list. if (count($children) == $limit) { $more_children = true; foreach ($children as $child) { if ($child->getDepth() == $d_grandchild) { $more_children = false; } } $show_grandchildren = false; } else { $show_grandchildren = true; $more_children = false; } $children_dicts = array(); $grandchildren_dicts = array(); foreach ($children as $key => $child) { $child_dict = array( 'slug' => $child->getSlug(), 'depth' => $child->getDepth(), 'title' => $child->getContent()->getTitle(), ); if ($child->getDepth() == $d_child) { $children_dicts[] = $child_dict; continue; } else { unset($children[$key]); if ($show_grandchildren) { $ancestors = PhabricatorSlug::getAncestry($child->getSlug()); $grandchildren_dicts[end($ancestors)][] = $child_dict; } } } // Fill in any missing children. $known_slugs = mpull($children, null, 'getSlug'); foreach ($grandchildren_dicts as $slug => $ignored) { if (empty($known_slugs[$slug])) { $children_dicts[] = array( 'slug' => $slug, 'depth' => $d_child, 'title' => PhabricatorSlug::getDefaultTitle($slug), 'empty' => true, ); } } $children_dicts = isort($children_dicts, 'title'); $list = array(); foreach ($children_dicts as $child) { $list[] = hsprintf('
  • '); $list[] = $this->renderChildDocumentLink($child); $grand = idx($grandchildren_dicts, $child['slug'], array()); if ($grand) { $list[] = hsprintf(''); } $list[] = hsprintf('
  • '); } if ($more_children) { $list[] = phutil_tag( 'li', array( 'class' => 'remarkup-list-item', ), pht('More...')); } $header = id(new PHUIHeaderView()) ->setHeader(pht('Document Hierarchy')); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild(phutil_tag( 'div', array( 'class' => 'phabricator-remarkup mlt mlb', ), phutil_tag( 'ul', array( 'class' => 'remarkup-list', ), $list))); return phutil_tag_div('phui-document-view-pro-box', $box); } private function renderChildDocumentLink(array $info) { $title = nonempty($info['title'], pht('(Untitled Document)')); $item = phutil_tag( 'a', array( 'href' => PhrictionDocument::getSlugURI($info['slug']), ), $title); if (isset($info['empty'])) { $item = phutil_tag('em', array(), $item); } return $item; } protected function getDocumentSlug() { return $this->slug; } protected function getToc($toc) { if ($toc) { $toc = phutil_tag_div('phui-document-toc-content', array( phutil_tag_div( 'phui-document-toc-header', pht('Contents')), $toc, )); } return $toc; } } diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index 0ac3fc35bb..e7e2b40d87 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -1,324 +1,325 @@ getViewer(); $id = $request->getURIData('id'); $current_version = null; if ($id) { $is_new = false; $document = id(new PhrictionDocumentQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needContent(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$document) { return new Aphront404Response(); } $current_version = $document->getContent()->getVersion(); $revert = $request->getInt('revert'); if ($revert) { - $content = id(new PhrictionContent())->loadOneWhere( - 'documentID = %d AND version = %d', - $document->getID(), - $revert); + $content = id(new PhrictionContentQuery()) + ->setViewer($viewer) + ->withDocumentPHIDs(array($document->getPHID())) + ->withVersions(array($revert)) + ->executeOne(); if (!$content) { return new Aphront404Response(); } } else { $content = $document->getContent(); } } else { $slug = $request->getStr('slug'); $slug = PhabricatorSlug::normalize($slug); if (!$slug) { return new Aphront404Response(); } $document = id(new PhrictionDocumentQuery()) ->setViewer($viewer) ->withSlugs(array($slug)) ->needContent(true) ->executeOne(); if ($document) { $content = $document->getContent(); $current_version = $content->getVersion(); $is_new = false; } else { $document = PhrictionDocument::initializeNewDocument($viewer, $slug); $content = $document->getContent(); $is_new = true; } } if ($request->getBool('nodraft')) { $draft = null; $draft_key = null; } else { if ($document->getPHID()) { $draft_key = $document->getPHID().':'.$content->getVersion(); } else { $draft_key = 'phriction:'.$content->getSlug(); } $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $viewer->getPHID(), $draft_key); } if ($draft && strlen($draft->getDraft()) && ($draft->getDraft() != $content->getContent())) { $content_text = $draft->getDraft(); $discard = phutil_tag( 'a', array( 'href' => $request->getRequestURI()->alter('nodraft', true), ), pht('discard this draft')); $draft_note = new PHUIInfoView(); $draft_note->setSeverity(PHUIInfoView::SEVERITY_NOTICE); $draft_note->setTitle(pht('Recovered Draft')); $draft_note->appendChild( pht('Showing a saved draft of your edits, you can %s.', $discard)); } else { $content_text = $content->getContent(); $draft_note = null; } require_celerity_resource('phriction-document-css'); $e_title = true; $e_content = true; $validation_exception = null; $notes = null; $title = $content->getTitle(); $overwrite = false; $v_cc = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); if ($is_new) { $v_projects = array(); } else { $v_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( $document->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); $v_projects = array_reverse($v_projects); } if ($request->isFormPost()) { $title = $request->getStr('title'); $content_text = $request->getStr('content'); $notes = $request->getStr('description'); $current_version = $request->getInt('contentVersion'); $v_view = $request->getStr('viewPolicy'); $v_edit = $request->getStr('editPolicy'); $v_cc = $request->getArr('cc'); $v_projects = $request->getArr('projects'); $xactions = array(); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionDocumentTitleTransaction::TRANSACTIONTYPE) ->setNewValue($title); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType( PhrictionDocumentContentTransaction::TRANSACTIONTYPE) ->setNewValue($content_text); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) ->setNewValue($v_view); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($v_edit); $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('=' => $v_cc)); $proj_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $proj_edge_type) ->setNewValue(array('=' => array_fuse($v_projects))); $editor = id(new PhrictionTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setDescription($notes) ->setProcessContentVersionError(!$request->getBool('overwrite')) ->setContentVersion($current_version); try { $editor->applyTransactions($document, $xactions); if ($draft) { $draft->delete(); } $uri = PhrictionDocument::getSlugURI($document->getSlug()); return id(new AphrontRedirectResponse())->setURI($uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; $e_title = nonempty( $ex->getShortMessage( PhrictionDocumentTitleTransaction::TRANSACTIONTYPE), true); $e_content = nonempty( $ex->getShortMessage( PhrictionDocumentContentTransaction::TRANSACTIONTYPE), true); // if we're not supposed to process the content version error, then // overwrite that content...! if (!$editor->getProcessContentVersionError()) { $overwrite = true; } $document->setViewPolicy($v_view); $document->setEditPolicy($v_edit); } } if ($document->getID()) { $page_title = pht('Edit Document: %s', $content->getTitle()); if ($overwrite) { $submit_button = pht('Overwrite Changes'); } else { $submit_button = pht('Save Changes'); } } else { $submit_button = pht('Create Document'); $page_title = pht('Create Document'); } $uri = $document->getSlug(); $uri = PhrictionDocument::getSlugURI($uri); $uri = PhabricatorEnv::getProductionURI($uri); $cancel_uri = PhrictionDocument::getSlugURI($document->getSlug()); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($document) ->execute(); $view_capability = PhabricatorPolicyCapability::CAN_VIEW; $edit_capability = PhabricatorPolicyCapability::CAN_EDIT; $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('slug', $document->getSlug()) ->addHiddenInput('nodraft', $request->getBool('nodraft')) ->addHiddenInput('contentVersion', $current_version) ->addHiddenInput('overwrite', $overwrite) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setValue($title) ->setError($e_title) ->setName('title')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('URI')) ->setValue($uri)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Content')) ->setValue($content_text) ->setError($e_content) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('content') ->setID('document-textarea') ->setUser($viewer)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Tags')) ->setName('projects') ->setValue($v_projects) ->setDatasource(new PhabricatorProjectDatasource())) ->appendControl( id(new AphrontFormTokenizerControl()) ->setLabel(pht('Subscribers')) ->setName('cc') ->setValue($v_cc) ->setUser($viewer) ->setDatasource(new PhabricatorMetaMTAMailableDatasource())) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($document) ->setCapability($view_capability) ->setPolicies($policies) ->setCaption( $document->describeAutomaticCapability($view_capability))) ->appendChild( id(new AphrontFormPolicyControl()) ->setName('editPolicy') ->setPolicyObject($document) ->setCapability($edit_capability) ->setPolicies($policies) ->setCaption( $document->describeAutomaticCapability($edit_capability))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Edit Notes')) ->setValue($notes) ->setError(null) ->setName('description')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($page_title) ->setValidationException($validation_exception) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader($content->getTitle()) ->setPreviewURI('/phriction/preview/'.$document->getSlug()) ->setControlID('document-textarea') ->setPreviewType(PHUIRemarkupPreviewPanel::DOCUMENT); $crumbs = $this->buildApplicationCrumbs(); if ($document->getID()) { $crumbs->addTextCrumb( $content->getTitle(), PhrictionDocument::getSlugURI($document->getSlug())); $crumbs->addTextCrumb(pht('Edit')); } else { $crumbs->addTextCrumb(pht('Create')); } $crumbs->setBorder(true); $view = id(new PHUITwoColumnView()) ->setFooter(array( $draft_note, $form_box, $preview, )); return $this->newPage() ->setTitle($page_title) ->setCrumbs($crumbs) ->appendChild($view); } } diff --git a/src/applications/phriction/query/PhrictionContentQuery.php b/src/applications/phriction/query/PhrictionContentQuery.php index 0ab132abdd..053f40cf9c 100644 --- a/src/applications/phriction/query/PhrictionContentQuery.php +++ b/src/applications/phriction/query/PhrictionContentQuery.php @@ -1,115 +1,128 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withDocumentPHIDs(array $phids) { $this->documentPHIDs = $phids; return $this; } + public function withVersions(array $versions) { + $this->versions = $versions; + return $this; + } + public function newResultObject() { return new PhrictionContent(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'c.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'c.phid IN (%Ls)', $this->phids); } + if ($this->versions !== null) { + $where[] = qsprintf( + $conn, + 'version IN (%Ld)', + $this->versions); + } + if ($this->documentPHIDs !== null) { $where[] = qsprintf( $conn, 'd.phid IN (%Ls)', $this->documentPHIDs); } return $where; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->shouldJoinDocumentTable()) { $joins[] = qsprintf( $conn, 'JOIN %T d ON d.id = c.documentID', id(new PhrictionDocument())->getTableName()); } return $joins; } protected function willFilterPage(array $contents) { $document_ids = mpull($contents, 'getDocumentID'); $documents = id(new PhrictionDocumentQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withIDs($document_ids) ->execute(); $documents = mpull($documents, null, 'getID'); foreach ($contents as $key => $content) { $document_id = $content->getDocumentID(); $document = idx($documents, $document_id); if (!$document) { unset($contents[$key]); $this->didRejectResult($content); continue; } $content->attachDocument($document); } return $contents; } private function shouldJoinDocumentTable() { if ($this->documentPHIDs !== null) { return true; } return false; } protected function getPrimaryTableAlias() { return 'c'; } public function getQueryApplicationClass() { return 'PhabricatorPhrictionApplication'; } } diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 2736d825b5..b5118146d3 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -1,322 +1,325 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } public function withDepths(array $depths) { $this->depths = $depths; return $this; } public function withSlugPrefix($slug_prefix) { $this->slugPrefix = $slug_prefix; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withStatus($status) { $this->status = $status; return $this; } public function needContent($need_content) { $this->needContent = $need_content; return $this; } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } public function newResultObject() { return new PhrictionDocument(); } protected function willFilterPage(array $documents) { if ($documents) { $ancestor_slugs = array(); foreach ($documents as $key => $document) { $document_slug = $document->getSlug(); foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) { $ancestor_slugs[$ancestor][] = $key; } } if ($ancestor_slugs) { $table = new PhrictionDocument(); $conn_r = $table->establishConnection('r'); $ancestors = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE slug IN (%Ls)', $document->getTableName(), array_keys($ancestor_slugs)); $ancestors = $table->loadAllFromArray($ancestors); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestor_slugs as $ancestor_slug => $document_keys) { $ancestor = idx($ancestors, $ancestor_slug); foreach ($document_keys as $document_key) { $documents[$document_key]->attachAncestor( $ancestor_slug, $ancestor); } } } } // To view a Phriction document, you must also be able to view all of the // ancestor documents. Filter out documents which have ancestors that are // not visible. $document_map = array(); foreach ($documents as $document) { $document_map[$document->getSlug()] = $document; foreach ($document->getAncestors() as $key => $ancestor) { if ($ancestor) { $document_map[$key] = $ancestor; } } } $filtered_map = $this->applyPolicyFilter( $document_map, array(PhabricatorPolicyCapability::CAN_VIEW)); // Filter all of the documents where a parent is not visible. foreach ($documents as $document_key => $document) { // If the document itself is not visible, filter it. if (!isset($filtered_map[$document->getSlug()])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); continue; } // If an ancestor exists but is not visible, filter the document. foreach ($document->getAncestors() as $ancestor_key => $ancestor) { if (!$ancestor) { continue; } if (!isset($filtered_map[$ancestor_key])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); break; } } } if (!$documents) { return $documents; } if ($this->needContent) { - $contents = id(new PhrictionContent())->loadAllWhere( - 'id IN (%Ld)', - mpull($documents, 'getContentID')); + $contents = id(new PhrictionContentQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withIDs(mpull($documents, 'getContentID')) + ->execute(); + $contents = mpull($contents, null, 'getID'); foreach ($documents as $key => $document) { $content_id = $document->getContentID(); if (empty($contents[$content_id])) { unset($documents[$key]); continue; } $document->attachContent($contents[$content_id]); } } return $documents; } protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); if ($this->getOrderVector()->containsKey('updated')) { $content_dao = new PhrictionContent(); $joins[] = qsprintf( $conn, 'JOIN %T c ON d.contentID = c.id', $content_dao->getTableName()); } return $joins; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'd.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'd.phid IN (%Ls)', $this->phids); } if ($this->slugs !== null) { $where[] = qsprintf( $conn, 'd.slug IN (%Ls)', $this->slugs); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'd.status IN (%Ld)', $this->statuses); } if ($this->slugPrefix !== null) { $where[] = qsprintf( $conn, 'd.slug LIKE %>', $this->slugPrefix); } if ($this->depths !== null) { $where[] = qsprintf( $conn, 'd.depth IN (%Ld)', $this->depths); } switch ($this->status) { case self::STATUS_OPEN: $where[] = qsprintf( $conn, 'd.status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_DELETED, PhrictionDocumentStatus::STATUS_MOVED, PhrictionDocumentStatus::STATUS_STUB, )); break; case self::STATUS_NONSTUB: $where[] = qsprintf( $conn, 'd.status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_MOVED, PhrictionDocumentStatus::STATUS_STUB, )); break; case self::STATUS_ANY: break; default: throw new Exception(pht("Unknown status '%s'!", $this->status)); } return $where; } public function getBuiltinOrders() { return array( self::ORDER_HIERARCHY => array( 'vector' => array('depth', 'title', 'updated'), 'name' => pht('Hierarchy'), ), ) + parent::getBuiltinOrders(); } public function getOrderableColumns() { return parent::getOrderableColumns() + array( 'depth' => array( 'table' => 'd', 'column' => 'depth', 'reverse' => true, 'type' => 'int', ), 'title' => array( 'table' => 'c', 'column' => 'title', 'reverse' => true, 'type' => 'string', ), 'updated' => array( 'table' => 'd', 'column' => 'contentID', 'type' => 'int', 'unique' => true, ), ); } protected function getPagingValueMap($cursor, array $keys) { $document = $this->loadCursorObject($cursor); $map = array( 'id' => $document->getID(), 'depth' => $document->getDepth(), 'updated' => $document->getContentID(), ); foreach ($keys as $key) { switch ($key) { case 'title': $map[$key] = $document->getContent()->getTitle(); break; } } return $map; } protected function willExecuteCursorQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { $vector = $this->getOrderVector(); if ($vector->containsKey('title')) { $query->needContent(true); } } protected function getPrimaryTableAlias() { return 'd'; } public function getQueryApplicationClass() { return 'PhabricatorPhrictionApplication'; } } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index 01e85d4954..0a9f09afdd 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -1,268 +1,268 @@ true, self::CONFIG_TIMESTAMPS => false, self::CONFIG_COLUMN_SCHEMA => array( 'slug' => 'sort128', 'depth' => 'uint32', 'contentID' => 'id?', 'status' => 'uint32', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, 'phid' => array( 'columns' => array('phid'), 'unique' => true, ), 'slug' => array( 'columns' => array('slug'), 'unique' => true, ), 'depth' => array( 'columns' => array('depth', 'slug'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhrictionDocumentPHIDType::TYPECONST); } public static function initializeNewDocument(PhabricatorUser $actor, $slug) { $document = new PhrictionDocument(); $document->setSlug($slug); - $content = new PhrictionContent(); + $content = new PhrictionContent(); $content->setSlug($slug); $default_title = PhabricatorSlug::getDefaultTitle($slug); $content->setTitle($default_title); $document->attachContent($content); $parent_doc = null; $ancestral_slugs = PhabricatorSlug::getAncestry($slug); if ($ancestral_slugs) { $parent = end($ancestral_slugs); $parent_doc = id(new PhrictionDocumentQuery()) ->setViewer($actor) ->withSlugs(array($parent)) ->executeOne(); } if ($parent_doc) { $document->setViewPolicy($parent_doc->getViewPolicy()); $document->setEditPolicy($parent_doc->getEditPolicy()); } else { $default_view_policy = PhabricatorPolicies::getMostOpenPolicy(); $document->setViewPolicy($default_view_policy); $document->setEditPolicy(PhabricatorPolicies::POLICY_USER); } return $document; } public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); } return parent::save(); } public static function getSlugURI($slug, $type = 'document') { static $types = array( 'document' => '/w/', 'history' => '/phriction/history/', ); if (empty($types[$type])) { throw new Exception(pht("Unknown URI type '%s'!", $type)); } $prefix = $types[$type]; if ($slug == '/') { return $prefix; } else { // NOTE: The effect here is to escape non-latin characters, since modern // browsers deal with escaped UTF8 characters in a reasonable way (showing // the user a readable URI) but older programs may not. $slug = phutil_escape_uri($slug); return $prefix.$slug; } } public function setSlug($slug) { $this->slug = PhabricatorSlug::normalize($slug); $this->depth = PhabricatorSlug::getDepth($slug); return $this; } public function attachContent(PhrictionContent $content) { $this->contentObject = $content; return $this; } public function getContent() { return $this->assertAttached($this->contentObject); } public function getAncestors() { return $this->ancestors; } public function getAncestor($slug) { return $this->assertAttachedKey($this->ancestors, $slug); } public function attachAncestor($slug, $ancestor) { $this->ancestors[$slug] = $ancestor; return $this; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $user) { return false; } public function describeAutomaticCapability($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return pht( 'To view a wiki document, you must also be able to view all '. 'of its parents.'); case PhabricatorPolicyCapability::CAN_EDIT: return pht( 'To edit a wiki document, you must also be able to view all '. 'of its parents.'); } return null; } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhrictionTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhrictionTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ public function getUsersToNotifyOfTokenGiven() { return PhabricatorSubscribersQuery::loadSubscribersForPHID($this->phid); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $contents = id(new PhrictionContentQuery()) ->setViewer($engine->getViewer()) ->withDocumentPHIDs(array($this->getPHID())) ->execute(); foreach ($contents as $content) { $engine->destroyObject($content); } $this->delete(); $this->saveTransaction(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new PhrictionDocumentFulltextEngine(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new PhrictionDocumentFerretEngine(); } }