diff --git a/src/applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php b/src/applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php index bd84e9f5c1..9208d68252 100644 --- a/src/applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php +++ b/src/applications/phriction/conduit/ConduitAPI_phriction_edit_Method.php @@ -1,44 +1,54 @@ 'required string', 'title' => 'optional string', 'content' => 'optional string', 'description' => 'optional string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $slug = $request->getValue('slug'); + $doc = id(new PhrictionDocumentQuery()) + ->setViewer($request->getUser()) + ->withSlugs(array(PhabricatorSlug::normalize($slug))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$doc) { + throw new Exception(pht('No such document.')); + } + $editor = id(PhrictionDocumentEditor::newForSlug($slug)) ->setActor($request->getUser()) ->setTitle($request->getValue('title')) ->setContent($request->getValue('content')) ->setDescription($request->getvalue('description')) ->save(); return $this->buildDocumentInfoDictionary($editor->getDocument()); } } diff --git a/src/applications/phriction/conduit/ConduitAPI_phriction_history_Method.php b/src/applications/phriction/conduit/ConduitAPI_phriction_history_Method.php index 5dc467a23c..e0928e5e14 100644 --- a/src/applications/phriction/conduit/ConduitAPI_phriction_history_Method.php +++ b/src/applications/phriction/conduit/ConduitAPI_phriction_history_Method.php @@ -1,52 +1,50 @@ 'required string', ); } public function defineReturnType() { return 'nonempty list'; } public function defineErrorTypes() { return array( 'ERR-BAD-DOCUMENT' => 'No such document exists.', ); } protected function execute(ConduitAPIRequest $request) { $slug = $request->getValue('slug'); - $doc = id(new PhrictionDocument())->loadOneWhere( - 'slug = %s', - PhabricatorSlug::normalize($slug)); + $doc = id(new PhrictionDocumentQuery()) + ->setViewer($request->getUser()) + ->withSlugs(array(PhabricatorSlug::normalize($slug))) + ->executeOne(); if (!$doc) { throw new ConduitException('ERR-BAD-DOCUMENT'); } $content = id(new PhrictionContent())->loadAllWhere( 'documentID = %d ORDER BY version DESC', $doc->getID()); $results = array(); foreach ($content as $version) { $results[] = $this->buildDocumentContentDictionary( $doc, $version); } return $results; } } diff --git a/src/applications/phriction/conduit/ConduitAPI_phriction_info_Method.php b/src/applications/phriction/conduit/ConduitAPI_phriction_info_Method.php index 32947283e8..838907b51e 100644 --- a/src/applications/phriction/conduit/ConduitAPI_phriction_info_Method.php +++ b/src/applications/phriction/conduit/ConduitAPI_phriction_info_Method.php @@ -1,46 +1,43 @@ 'required string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR-BAD-DOCUMENT' => 'No such document exists.', ); } protected function execute(ConduitAPIRequest $request) { $slug = $request->getValue('slug'); - $doc = id(new PhrictionDocument())->loadOneWhere( - 'slug = %s', - PhabricatorSlug::normalize($slug)); - - if (!$doc) { + $document = id(new PhrictionDocumentQuery()) + ->setViewer($request->getUser()) + ->withSlugs(array(PhabricatorSlug::normalize($slug))) + ->needContent(true) + ->executeOne(); + if (!$document) { throw new ConduitException('ERR-BAD-DOCUMENT'); } - $content = id(new PhrictionContent())->load($doc->getContentID()); - $doc->attachContent($content); - - return $this->buildDocumentInfoDictionary($doc); + return $this->buildDocumentInfoDictionary( + $document, + $document->getContent()); } } diff --git a/src/applications/phriction/controller/PhrictionController.php b/src/applications/phriction/controller/PhrictionController.php index 5a2be7d116..2c4ab17594 100644 --- a/src/applications/phriction/controller/PhrictionController.php +++ b/src/applications/phriction/controller/PhrictionController.php @@ -1,96 +1,97 @@ getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); if ($for_app) { $nav->addFilter('create', pht('New Document')); $nav->addFilter('/phriction/', pht('Index')); } id(new PhrictionSearchEngine()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); return $nav; } public function buildApplicationMenu() { return $this->buildSideNavView(true)->getMenu(); } public function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); if (get_class($this) != 'PhrictionListController') { $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Index')) ->setHref('/phriction/') ->setIcon('fa-home')); } $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('New Document')) ->setHref('/phriction/new/?slug='.$this->getDocumentSlug()) ->setWorkflow(true) ->setIcon('fa-plus-square')); return $crumbs; } public function renderBreadcrumbs($slug) { $ancestor_handles = array(); $ancestral_slugs = PhabricatorSlug::getAncestry($slug); $ancestral_slugs[] = $slug; if ($ancestral_slugs) { $empty_slugs = array_fill_keys($ancestral_slugs, null); - $ancestors = id(new PhrictionDocument())->loadAllWhere( - 'slug IN (%Ls)', - $ancestral_slugs); + $ancestors = id(new PhrictionDocumentQuery()) + ->setViewer($this->getRequest()->getUser()) + ->withSlugs($ancestral_slugs) + ->execute(); $ancestors = mpull($ancestors, null, 'getSlug'); $ancestor_phids = mpull($ancestors, 'getPHID'); $handles = array(); if ($ancestor_phids) { $handles = $this->loadViewerHandles($ancestor_phids); } $ancestor_handles = array(); foreach ($ancestral_slugs as $slug) { if (isset($ancestors[$slug])) { $ancestor_handles[] = $handles[$ancestors[$slug]->getPHID()]; } else { $handle = new PhabricatorObjectHandle(); $handle->setName(PhabricatorSlug::getDefaultTitle($slug)); $handle->setURI(PhrictionDocument::getSlugURI($slug)); $ancestor_handles[] = $handle; } } } $breadcrumbs = array(); foreach ($ancestor_handles as $ancestor_handle) { $breadcrumbs[] = id(new PhabricatorCrumbView()) ->setName($ancestor_handle->getName()) ->setHref($ancestor_handle->getUri()); } return $breadcrumbs; } protected function getDocumentSlug() { return ''; } } diff --git a/src/applications/phriction/controller/PhrictionDeleteController.php b/src/applications/phriction/controller/PhrictionDeleteController.php index 9988470f18..7a97c3abfc 100644 --- a/src/applications/phriction/controller/PhrictionDeleteController.php +++ b/src/applications/phriction/controller/PhrictionDeleteController.php @@ -1,63 +1,67 @@ id = $data['id']; } public function processRequest() { - $request = $this->getRequest(); $user = $request->getUser(); - $document = id(new PhrictionDocument())->load($this->id); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_EDIT, + PhabricatorPolicyCapability::CAN_VIEW, + )) + ->executeOne(); if (!$document) { return new Aphront404Response(); } $e_text = null; $disallowed_states = array( PhrictionDocumentStatus::STATUS_DELETED => true, // Silly PhrictionDocumentStatus::STATUS_MOVED => true, // Makes no sense PhrictionDocumentStatus::STATUS_STUB => true, // How could they? ); if (isset($disallowed_states[$document->getStatus()])) { $e_text = pht('An already moved or deleted document can not be deleted'); } $document_uri = PhrictionDocument::getSlugURI($document->getSlug()); if (!$e_text && $request->isFormPost()) { $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) ->setActor($user) ->delete(); return id(new AphrontRedirectResponse())->setURI($document_uri); } if ($e_text) { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Can not delete document!')) ->appendChild($e_text) ->addCancelButton($document_uri); } else { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Delete Document?')) ->appendChild( pht('Really delete this document? You can recover it later by '. 'reverting to a previous version.')) ->addSubmitButton(pht('Delete')) ->addCancelButton($document_uri); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/controller/PhrictionDiffController.php b/src/applications/phriction/controller/PhrictionDiffController.php index dc0051a1bb..7c836aeaf3 100644 --- a/src/applications/phriction/controller/PhrictionDiffController.php +++ b/src/applications/phriction/controller/PhrictionDiffController.php @@ -1,292 +1,295 @@ id = $data['id']; } public function processRequest() { - $request = $this->getRequest(); $user = $request->getUser(); - $document = id(new PhrictionDocument())->load($this->id); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->needContent(true) + ->executeOne(); if (!$document) { return new Aphront404Response(); } - $current = id(new PhrictionContent())->load($document->getContentID()); + $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 = 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(); $text_l = phutil_utf8_hard_wrap($text_l, 80); $text_l = implode("\n", $text_l); $text_r = phutil_utf8_hard_wrap($text_r, 80); $text_r = implode("\n", $text_r); $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent($text_l, $text_r); $changeset->setOldProperties( array( 'Title' => $content_l->getTitle(), )); $changeset->setNewProperties( array( 'Title' => $content_r->getTitle(), )); $whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; $parser = new DifferentialChangesetParser(); $parser->setChangeset($changeset); $parser->setRenderingReference("{$l},{$r}"); $parser->setWhitespaceMode($whitespace_mode); $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); $engine->process(); $parser->setMarkupEngine($engine); $spec = $request->getStr('range'); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $output = $parser->render($range_s, $range_e, $mask); if ($request->isAjax()) { return id(new PhabricatorChangesetResponse()) ->setRenderedChangeset($output); } require_celerity_resource('differential-changeset-view-css'); require_celerity_resource('syntax-highlighting-css'); require_celerity_resource('phriction-document-css'); Javelin::initBehavior('differential-show-more', array( 'uri' => '/phriction/diff/'.$document->getID().'/', 'whitespace' => $whitespace_mode, )); $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); $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', ), pht("\xC2\xAB Previous Change")); } else { $link_l = phutil_tag( 'a', array( 'href' => '#', 'class' => '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', ), pht("Next Change \xC2\xBB")); } else { $link_r = phutil_tag( 'a', array( 'href' => '#', 'class' => '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
'. '%s'. '
', $comparison_table->render(), $navigation_table, $revert_l, $revert_r, $output); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($output); return $this->buildApplicationPage( array( $crumbs, $object_box, ), array( 'title' => pht('Document History'), 'device' => true, )); } 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 grey', ), pht('Edit Current Version')); } return phutil_tag( 'a', array( 'href' => '/phriction/edit/'.$document_id.'/?revert='.$version, 'class' => 'button grey', ), pht('Revert to Version %s...', $version)); } private function renderComparisonTable(array $content) { assert_instances_of($content, 'PhrictionContent'); $user = $this->getRequest()->getUser(); $phids = mpull($content, 'getAuthorPHID'); $handles = $this->loadViewerHandles($phids); $list = new PHUIObjectItemListView(); $list->setCards(true); $list->setFlush(true); $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(), $user), phabricator_time($c->getDateCreated(), $user))); if ($c->getDescription()) { $item->addAttribute($c->getDescription()); } if ($first == true) { $item->setBarColor('green'); $first = false; } else { $item->setBarColor('red'); } $list->addItem($item); } return $list; } } diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 4e0b65ea91..9fbd16e5ca 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -1,462 +1,467 @@ slug = $data['slug']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $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($user) ->withSlugs(array($slug)) ->executeOne(); $version_note = null; $core_content = ''; $move_notice = ''; $properties = null; if (!$document) { $document = new PhrictionDocument(); if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs(array( PhrictionDocument::getProjectSlugIdentifier($slug))) ->executeOne(); if (!$project) { return new Aphront404Response(); } } $create_uri = '/phriction/edit/?slug='.$slug; $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NODATA); $notice->setTitle(pht('No content here!')); $notice->appendChild( pht( 'No document found at %s. You can '. 'create a new document here.', phutil_tag('tt', array(), $slug), $create_uri)); $core_content = $notice; $page_title = pht('Page Not Found'); } else { $version = $request->getInt('v'); if ($version) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $version); if (!$content) { return new Aphront404Response(); } if ($content->getID() != $document->getContentID()) { $vdate = phabricator_datetime($content->getDateCreated(), $user); $version_note = new AphrontErrorView(); $version_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $version_note->setTitle('Older Version'); $version_note->appendChild( pht('You are viewing an older version of this document, as it '. 'appeared on %s.', $vdate)); } } else { $content = id(new PhrictionContent())->load($document->getContentID()); } $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) { $core_content = $content->renderContent($user); } else if ($current_status == PhrictionChangeType::CHANGE_DELETE) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::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 AphrontErrorView(); $notice->setSeverity(AphrontErrorView::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(); - $new_doc = new PhrictionDocument(); - $new_doc->load($new_doc_id); + $new_doc = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withIDs(array($new_doc_id)) + ->exectueOne(); $slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug()); $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle(pht('Document Moved')); $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)))); $core_content = $notice->render(); } else { throw new Exception("Unknown document status '{$doc_status}'!"); } $move_notice = null; if ($current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { $from_doc_id = $content->getChangeRef(); - $from_doc = id(new PhrictionDocument())->load($from_doc_id); + $from_doc = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withIDs(array($from_doc_id)) + ->executeOne(); $slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug()); $move_notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) ->appendChild(pht('This document was moved from %s', phutil_tag('a', array('href' => $slug_uri), $slug_uri))) ->render(); } } if ($version_note) { $version_note = $version_note->render(); } $children = $this->renderDocumentChildren($slug); $actions = $this->buildActionView($user, $document); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setActionList($actions); $crumb_views = $this->renderBreadcrumbs($slug); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } $header = id(new PHUIHeaderView()) ->setHeader($page_title); $prop_list = null; if ($properties) { $prop_list = new PHUIPropertyGroupView(); $prop_list->addPropertyList($properties); } $page_content = id(new PHUIDocumentView()) ->setOffset(true) ->setHeader($header) ->appendChild( array( $actions, $prop_list, $move_notice, $core_content, )); $core_page = phutil_tag( 'div', array( 'class' => 'phriction-offset' ), array( $page_content, $children, )); return $this->buildApplicationPage( array( $crumbs->render(), $core_page, ), array( 'pageObjects' => array($document->getPHID()), 'title' => $page_title, 'device' => true, )); } private function buildPropertyListView( PhrictionDocument $document, PhrictionContent $content, $slug) { $viewer = $this->getRequest()->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($document); $project_phid = null; if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPhrictionSlugs(array( PhrictionDocument::getProjectSlugIdentifier($slug))) ->executeOne(); if ($project) { $project_phid = $project->getPHID(); } } $phids = array_filter( array( $content->getAuthorPHID(), $project_phid, )); $this->loadHandles($phids); $project_info = null; if ($project_phid) { $view->addProperty( pht('Project Info'), $this->getHandle($project_phid)->renderLink()); } $view->addProperty( pht('Last Author'), $this->getHandle($content->getAuthorPHID())->renderLink()); $age = time() - $content->getDateCreated(); $age = floor($age / (60 * 60 * 24)); if ($age < 1) { $when = pht('Today'); } else if ($age == 1) { $when = pht('Yesterday'); } else { $when = pht("%d Days Ago", $age); } $view->addProperty(pht('Last Updated'), $when); return $view; } private function buildActionView( PhabricatorUser $user, PhrictionDocument $document) { $can_edit = PhabricatorPolicyFilter::hasCapability( $user, $document, PhabricatorPolicyCapability::CAN_EDIT); $slug = PhabricatorSlug::normalize($this->slug); $action_view = id(new PhabricatorActionListView()) ->setUser($user) ->setObjectURI($this->getRequest()->getRequestURI()) ->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')) ->setIcon('fa-pencil') ->setHref('/phriction/edit/'.$document->getID().'/')); if ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) { $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Move Document')) ->setIcon('fa-arrows') ->setHref('/phriction/move/'.$document->getID().'/') ->setWorkflow(true)); $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete Document')) ->setIcon('fa-times') ->setHref('/phriction/delete/'.$document->getID().'/') ->setWorkflow(true)); } return $action_view->addAction( id(new PhabricatorActionView()) ->setName(pht('View History')) ->setIcon('fa-list') ->setHref(PhrictionDocument::getSlugURI($slug, 'history'))); } private function renderDocumentChildren($slug) { $document_dao = new PhrictionDocument(); $content_dao = new PhrictionContent(); $conn = $document_dao->establishConnection('r'); $limit = 250; $d_child = PhabricatorSlug::getDepth($slug) + 1; $d_grandchild = PhabricatorSlug::getDepth($slug) + 2; // Select children and grandchildren. $children = queryfx_all( $conn, 'SELECT d.slug, d.depth, c.title FROM %T d JOIN %T c ON d.contentID = c.id WHERE d.slug LIKE %> AND d.depth IN (%d, %d) AND d.status IN (%Ld) ORDER BY d.depth, c.title LIMIT %d', $document_dao->getTableName(), $content_dao->getTableName(), ($slug == '/' ? '' : $slug), $d_child, $d_grandchild, array( PhrictionDocumentStatus::STATUS_EXISTS, PhrictionDocumentStatus::STATUS_STUB, ), $limit); 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['depth'] == $d_grandchild) { $more_children = false; } } $show_grandchildren = false; } else { $show_grandchildren = true; $more_children = false; } $grandchildren = array(); foreach ($children as $key => $child) { if ($child['depth'] == $d_child) { continue; } else { unset($children[$key]); if ($show_grandchildren) { $ancestors = PhabricatorSlug::getAncestry($child['slug']); $grandchildren[end($ancestors)][] = $child; } } } // Fill in any missing children. $known_slugs = ipull($children, null, 'slug'); foreach ($grandchildren as $slug => $ignored) { if (empty($known_slugs[$slug])) { $children[] = array( 'slug' => $slug, 'depth' => $d_child, 'title' => PhabricatorSlug::getDefaultTitle($slug), 'empty' => true, ); } } $children = isort($children, 'title'); $list = array(); foreach ($children as $child) { $list[] = hsprintf('
  • '); $list[] = $this->renderChildDocumentLink($child); $grand = idx($grandchildren, $child['slug'], array()); if ($grand) { $list[] = hsprintf(''); } $list[] = hsprintf('
  • '); } if ($more_children) { $list[] = phutil_tag('li', array(), pht('More...')); } $content = array( phutil_tag( 'div', array( 'class' => 'phriction-children-header '. 'sprite-gradient gradient-lightblue-header', ), pht('Document Hierarchy')), phutil_tag( 'div', array( 'class' => 'phriction-children', ), phutil_tag('ul', array(), $list)), ); return id(new PHUIDocumentView()) ->setOffset(true) ->appendChild($content); } 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; } } diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index 7750aaab4b..c18a77d953 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -1,289 +1,296 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { - $document = id(new PhrictionDocument())->load($this->id); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); if (!$document) { return new Aphront404Response(); } $revert = $request->getInt('revert'); if ($revert) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $revert); if (!$content) { return new Aphront404Response(); } } else { $content = id(new PhrictionContent())->load($document->getContentID()); } } else { $slug = $request->getStr('slug'); $slug = PhabricatorSlug::normalize($slug); if (!$slug) { return new Aphront404Response(); } - $document = id(new PhrictionDocument())->loadOneWhere( - 'slug = %s', - $slug); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withSlugs(array($slug)) + ->needContent(true) + ->executeOne(); if ($document) { - $content = id(new PhrictionContent())->load($document->getContentID()); + $content = $document->getContent(); } else { if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs(array( PhrictionDocument::getProjectSlugIdentifier($slug))) ->executeOne(); if (!$project) { return new Aphront404Response(); } } $document = new PhrictionDocument(); $document->setSlug($slug); $content = new PhrictionContent(); $content->setSlug($slug); $default_title = PhabricatorSlug::getDefaultTitle($slug); $content->setTitle($default_title); } } 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', $user->getPHID(), $draft_key); } require_celerity_resource('phriction-document-css'); $e_title = true; $notes = null; $errors = array(); if ($request->isFormPost()) { $overwrite = $request->getBool('overwrite'); if (!$overwrite) { $edit_version = $request->getStr('contentVersion'); $current_version = $content->getVersion(); if ($edit_version != $current_version) { $dialog = $this->newDialog() ->setTitle(pht('Edit Conflict!')) ->appendParagraph( pht( 'Another user made changes to this document after you began '. 'editing it. Do you want to overwrite their changes?')) ->appendParagraph( pht( 'If you choose to overwrite their changes, you should review '. 'the document edit history to see what you overwrote, and '. 'then make another edit to merge the changes if necessary.')) ->addSubmitButton(pht('Overwrite Changes')) ->addCancelButton($request->getRequestURI()); $dialog->addHiddenInput('overwrite', 'true'); foreach ($request->getPassthroughRequestData() as $key => $value) { $dialog->addHiddenInput($key, $value); } return $dialog; } } $title = $request->getStr('title'); $notes = $request->getStr('description'); if (!strlen($title)) { $e_title = pht('Required'); $errors[] = pht('Document title is required.'); } else { $e_title = null; } if ($document->getID()) { if ($content->getTitle() == $title && $content->getContent() == $request->getStr('content')) { $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('No Edits')); $dialog->appendChild(phutil_tag('p', array(), pht( 'You did not make any changes to the document.'))); $dialog->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } else if (!strlen($request->getStr('content'))) { // We trigger this only for new pages. For existing pages, deleting // all the content counts as deleting the page. $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle(pht('Empty Page')); $dialog->appendChild(phutil_tag('p', array(), pht( 'You can not create an empty document.'))); $dialog->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } if (!count($errors)) { $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) ->setActor($user) ->setTitle($title) ->setContent($request->getStr('content')) ->setDescription($notes); $editor->save(); if ($draft) { $draft->delete(); } $uri = PhrictionDocument::getSlugURI($document->getSlug()); return id(new AphrontRedirectResponse())->setURI($uri); } } if ($document->getID()) { $panel_header = pht('Edit Phriction Document'); $submit_button = pht('Save Changes'); } else { $panel_header = pht('Create New Phriction Document'); $submit_button = pht('Create Document'); } $uri = $document->getSlug(); $uri = PhrictionDocument::getSlugURI($uri); $uri = PhabricatorEnv::getProductionURI($uri); $cancel_uri = PhrictionDocument::getSlugURI($document->getSlug()); 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 AphrontErrorView(); $draft_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $draft_note->setTitle('Recovered Draft'); $draft_note->appendChild(hsprintf( '

    Showing a saved draft of your edits, you can %s.

    ', $discard)); } else { $content_text = $content->getContent(); $draft_note = null; } $form = id(new AphrontFormView()) ->setUser($user) ->setWorkflow(true) ->setAction($request->getRequestURI()->getPath()) ->addHiddenInput('slug', $document->getSlug()) ->addHiddenInput('nodraft', $request->getBool('nodraft')) ->addHiddenInput('contentVersion', $content->getVersion()) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Title')) ->setValue($content->getTitle()) ->setError($e_title) ->setName('title')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('URI')) ->setValue($uri)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Content')) ->setValue($content_text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('content') ->setID('document-textarea') ->setUser($user)) ->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(pht('Edit Document')) ->setFormErrors($errors) ->setForm($form); $preview = id(new PHUIRemarkupPreviewPanel()) ->setHeader(pht('Document Preview')) ->setPreviewURI('/phriction/preview/') ->setControlID('document-textarea') ->setSkin('document'); $crumbs = $this->buildApplicationCrumbs(); if ($document->getID()) { $crumbs->addTextCrumb( $content->getTitle(), PhrictionDocument::getSlugURI($document->getSlug())); $crumbs->addTextCrumb(pht('Edit')); } else { $crumbs->addTextCrumb(pht('Create')); } return $this->buildApplicationPage( array( $crumbs, $draft_note, $form_box, $preview, ), array( 'title' => pht('Edit Document'), 'device' => true, )); } } diff --git a/src/applications/phriction/controller/PhrictionHistoryController.php b/src/applications/phriction/controller/PhrictionHistoryController.php index fa2154428e..622747e525 100644 --- a/src/applications/phriction/controller/PhrictionHistoryController.php +++ b/src/applications/phriction/controller/PhrictionHistoryController.php @@ -1,171 +1,169 @@ slug = $data['slug']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); - $document = id(new PhrictionDocument())->loadOneWhere( - 'slug = %s', - PhabricatorSlug::normalize($this->slug)); - + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withSlugs(array(PhabricatorSlug::normalize($this->slug))) + ->needContent(true) + ->executeOne(); if (!$document) { return new Aphront404Response(); } - $current = id(new PhrictionContent())->load($document->getContentID()); + $current = $document->getContent(); $pager = new AphrontPagerView(); $pager->setOffset($request->getInt('page')); $pager->setURI($request->getRequestURI(), 'page'); $history = id(new PhrictionContent())->loadAllWhere( 'documentID = %d ORDER BY version DESC LIMIT %d, %d', $document->getID(), $pager->getOffset(), $pager->getPageSize() + 1); $history = $pager->sliceResults($history); $author_phids = mpull($history, 'getAuthorPHID'); $handles = $this->loadViewerHandles($author_phids); $list = new PHUIObjectItemListView(); $list->setCards(true); $list->setFlush(true); foreach ($history as $content) { $author = $handles[$content->getAuthorPHID()]->renderLink(); $slug_uri = PhrictionDocument::getSlugURI($document->getSlug()); $version = $content->getVersion(); $diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/'); $vs_previous = null; if ($content->getVersion() != 1) { $vs_previous = $diff_uri ->alter('l', $content->getVersion() - 1) ->alter('r', $content->getVersion()); } $vs_head = null; if ($content->getID() != $document->getContentID()) { $vs_head = $diff_uri ->alter('l', $content->getVersion()) ->alter('r', $current->getVersion()); } $change_type = PhrictionChangeType::getChangeTypeLabel( $content->getChangeType()); switch ($content->getChangeType()) { case PhrictionChangeType::CHANGE_DELETE: $color = 'red'; break; case PhrictionChangeType::CHANGE_EDIT: $color = 'blue'; break; case PhrictionChangeType::CHANGE_MOVE_HERE: $color = 'yellow'; break; case PhrictionChangeType::CHANGE_MOVE_AWAY: $color = 'orange'; break; case PhrictionChangeType::CHANGE_STUB: $color = 'green'; break; default: throw new Exception("Unknown change type!"); break; } $item = id(new PHUIObjectItemView()) ->setHeader(pht('%s by %s', $change_type, $author)) ->setBarColor($color) ->addAttribute( phutil_tag( 'a', array( 'href' => $slug_uri.'?v='.$version, ), pht('Version %s', $version))) ->addAttribute(pht('%s %s', phabricator_date($content->getDateCreated(), $user), phabricator_time($content->getDateCreated(), $user))); if ($content->getDescription()) { $item->addAttribute($content->getDescription()); } if ($vs_previous) { $item->addIcon( 'arrow_left', pht('Show Change'), array( 'href' => $vs_previous, )); } else { $item->addIcon('arrow_left-grey', phutil_tag('em', array(), pht('No previous change'))); } if ($vs_head) { $item->addIcon( 'merge', pht('Show Later Changes'), array( 'href' => $vs_head, )); } else { $item->addIcon('merge-grey', phutil_tag('em', array(), pht('No later changes'))); } $list->addItem($item); } $crumbs = $this->buildApplicationCrumbs(); $crumb_views = $this->renderBreadcrumbs($document->getSlug()); foreach ($crumb_views as $view) { $crumbs->addCrumb($view); } $crumbs->addTextCrumb( pht('History'), PhrictionDocument::getSlugURI($document->getSlug(), 'history')); $header = new PHUIHeaderView(); $header->setHeader(pht('Document History for %s', phutil_tag( 'a', array('href' => PhrictionDocument::getSlugURI($document->getSlug())), head($history)->getTitle()))); $obj_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->appendChild($list) ->appendChild($pager); return $this->buildApplicationPage( array( $crumbs, $obj_box, ), array( 'title' => pht('Document History'), 'device' => true, )); } } diff --git a/src/applications/phriction/controller/PhrictionMoveController.php b/src/applications/phriction/controller/PhrictionMoveController.php index 4de57b568f..bf74755263 100644 --- a/src/applications/phriction/controller/PhrictionMoveController.php +++ b/src/applications/phriction/controller/PhrictionMoveController.php @@ -1,146 +1,164 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { - $document = id(new PhrictionDocument())->load($this->id); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); } else { $slug = PhabricatorSlug::normalize( $request->getStr('slug')); if (!$slug) { return new Aphront404Response(); } - $document = id(new PhrictionDocument())->loadOneWhere( - 'slug = %s', - $slug); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withSlugs(array($slug)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); } if (!$document) { return new Aphront404Response(); } if (!isset($slug)) { $slug = $document->getSlug(); } $target_slug = PhabricatorSlug::normalize( $request->getStr('new-slug', $slug)); $submit_uri = $request->getRequestURI()->getPath(); $cancel_uri = PhrictionDocument::getSlugURI($slug); $errors = array(); $error_view = null; $e_url = null; $disallowed_statuses = array( PhrictionDocumentStatus::STATUS_DELETED => true, // Silly PhrictionDocumentStatus::STATUS_MOVED => true, // Plain silly PhrictionDocumentStatus::STATUS_STUB => true, // Utterly silly ); if (isset($disallowed_statuses[$document->getStatus()])) { $error_dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle("Can not move page!") ->appendChild(pht('An already moved or deleted document '. 'can not be moved again.')) ->addCancelButton($cancel_uri); return id(new AphrontDialogResponse())->setDialog($error_dialog); } $content = id(new PhrictionContent())->load($document->getContentID()); if ($request->isFormPost() && !count($errors)) { if (!count($errors)) { // First check if the target document exists - $target_document = id(new PhrictionDocument())->loadOneWhere( - 'slug = %s', - $target_slug); + + // NOTE: We use the ominpotent user because we can't let users overwrite + // documents even if they can't see them. + $target_document = id(new PhrictionDocumentQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSlugs(array($target_slug)) + ->executeOne(); // Considering to overwrite existing docs? Nuke this! if ($target_document && $target_document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) { $errors[] = pht('Can not overwrite existing target document.'); $e_url = pht('Already exists.'); } } if (!count($errors)) { // I like to move it, move it! $from_editor = id(PhrictionDocumentEditor::newForSlug($slug)) ->setActor($user) ->setTitle($content->getTitle()) ->setContent($content->getContent()) ->setDescription($content->getDescription()); $target_editor = id(PhrictionDocumentEditor::newForSlug( $target_slug)) ->setActor($user) ->setTitle($content->getTitle()) ->setContent($content->getContent()) ->setDescription($content->getDescription()); // Move it! $target_editor->moveHere($document->getID(), $document->getPHID()); // Retrieve the target doc directly from the editor // No need to load it per Sql again $target_document = $target_editor->getDocument(); $from_editor->moveAway($target_document->getID()); $redir_uri = PhrictionDocument::getSlugURI($target_document->getSlug()); return id(new AphrontRedirectResponse())->setURI($redir_uri); } } if ($errors) { $error_view = id(new AphrontErrorView()) ->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->setUser($user) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel(pht('Title')) ->setValue($content->getTitle())) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('New URI')) ->setValue($target_slug) ->setError($e_url) ->setName('new-slug') ->setCaption(pht('The new location of the document.'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Edit Notes')) ->setValue($content->getDescription()) ->setError(null) ->setName('description')); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Move Document')) ->appendChild($form) ->setSubmitURI($submit_uri) ->addSubmitButton(pht('Move Document')) ->addCancelButton($cancel_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/controller/PhrictionNewController.php b/src/applications/phriction/controller/PhrictionNewController.php index 609907955c..fa122c489e 100644 --- a/src/applications/phriction/controller/PhrictionNewController.php +++ b/src/applications/phriction/controller/PhrictionNewController.php @@ -1,85 +1,82 @@ getRequest(); $user = $request->getUser(); $slug = PhabricatorSlug::normalize($request->getStr('slug')); if ($request->isFormPost()) { - $document = id(new PhrictionDocument())->loadOneWhere( - 'slug = %s', - $slug); + $document = id(new PhrictionDocumentQuery()) + ->setViewer($user) + ->withSlugs(array($slug)) + ->executeOne(); $prompt = $request->getStr('prompt', 'no'); $document_exists = $document && $document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS; if ($document_exists && $prompt == 'no') { $dialog = new AphrontDialogView(); $dialog->setSubmitURI('/phriction/new/') ->setTitle(pht('Edit Existing Document?')) ->setUser($user) ->appendChild(pht( 'The document %s already exists. Do you want to edit it instead?', phutil_tag('tt', array(), $slug))) ->addHiddenInput('slug', $slug) ->addHiddenInput('prompt', 'yes') ->addCancelButton('/w/') ->addSubmitButton(pht('Edit Document')); return id(new AphrontDialogResponse())->setDialog($dialog); } else if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withPhrictionSlugs(array( PhrictionDocument::getProjectSlugIdentifier($slug))) ->executeOne(); if (!$project) { $dialog = new AphrontDialogView(); $dialog->setSubmitURI('/w/') ->setTitle(pht('Oops!')) ->setUser($user) ->appendChild(pht( 'You cannot create wiki pages under "projects/", because they are reserved as project pages. Create a new project with this name first.')) ->addCancelButton('/w/', 'Okay'); return id(new AphrontDialogResponse())->setDialog($dialog); } } $uri = '/phriction/edit/?slug='.$slug; return id(new AphrontRedirectResponse()) ->setURI($uri); } if ($slug == '/') { $slug = ''; } $view = id(new PHUIFormLayoutView()) ->appendChild(id(new AphrontFormTextControl()) ->setLabel('/w/') ->setValue($slug) ->setName('slug')); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('New Document')) ->setSubmitURI('/phriction/new/') ->appendChild(phutil_tag('p', array(), pht('Create a new document at'))) ->appendChild($view) ->addSubmitButton(pht('Create')) ->addCancelButton('/w/'); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/editor/PhrictionDocumentEditor.php b/src/applications/phriction/editor/PhrictionDocumentEditor.php index d0ca728e89..b78f0ede35 100644 --- a/src/applications/phriction/editor/PhrictionDocumentEditor.php +++ b/src/applications/phriction/editor/PhrictionDocumentEditor.php @@ -1,375 +1,377 @@ } public static function newForSlug($slug) { $slug = PhabricatorSlug::normalize($slug); + + // TODO: Get rid of this. $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); $content = null; if ($document) { $content = id(new PhrictionContent())->load($document->getContentID()); } else { $document = new PhrictionDocument(); $document->setSlug($slug); } if (!$content) { $default_title = PhabricatorSlug::getDefaultTitle($slug); $content = new PhrictionContent(); $content->setSlug($slug); $content->setTitle($default_title); $content->setContent(''); } $obj = new PhrictionDocumentEditor(); $obj->document = $document; $obj->content = $content; return $obj; } public function setTitle($title) { $this->newTitle = $title; return $this; } public function setContent($content) { $this->newContent = $content; return $this; } public function setDescription($description) { $this->description = $description; return $this; } public function getDocument() { return $this->document; } public function moveAway($new_doc_id) { return $this->execute( PhrictionChangeType::CHANGE_MOVE_AWAY, true, $new_doc_id); } public function moveHere($old_doc_id, $old_doc_phid) { $this->fromDocumentPHID = $old_doc_phid; return $this->execute( PhrictionChangeType::CHANGE_MOVE_HERE, false, $old_doc_id); } private function execute( $change_type, $del_new_content = true, $doc_ref = null) { $actor = $this->requireActor(); $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); $new_content->setChangeType($change_type); if ($del_new_content) { $new_content->setContent(''); } if ($doc_ref) { $new_content->setChangeRef($doc_ref); } return $this->updateDocument($document, $content, $new_content); } public function delete() { return $this->execute(PhrictionChangeType::CHANGE_DELETE, true); } private function stub() { return $this->execute(PhrictionChangeType::CHANGE_STUB, true); } public function save() { $actor = $this->requireActor(); if ($this->newContent === '') { // If this is an edit which deletes all the content, just treat it as // a delete. NOTE: null means "don't change the content", not "delete // the page"! Thus the strict type check. return $this->delete(); } $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); return $this->updateDocument($document, $content, $new_content); } private function buildContentTemplate( PhrictionDocument $document, PhrictionContent $content) { $new_content = new PhrictionContent(); $new_content->setSlug($document->getSlug()); $new_content->setAuthorPHID($this->getActor()->getPHID()); $new_content->setChangeType(PhrictionChangeType::CHANGE_EDIT); $new_content->setTitle( coalesce( $this->newTitle, $content->getTitle())); $new_content->setContent( coalesce( $this->newContent, $content->getContent())); if (strlen($this->description)) { $new_content->setDescription($this->description); } return $new_content; } private function updateDocument($document, $content, $new_content) { $is_new = false; if (!$document->getID()) { $is_new = true; } $new_content->setVersion($content->getVersion() + 1); $change_type = $new_content->getChangeType(); switch ($change_type) { case PhrictionChangeType::CHANGE_EDIT: $doc_status = PhrictionDocumentStatus::STATUS_EXISTS; $feed_action = $is_new ? PhrictionActionConstants::ACTION_CREATE : PhrictionActionConstants::ACTION_EDIT; break; case PhrictionChangeType::CHANGE_DELETE: $doc_status = PhrictionDocumentStatus::STATUS_DELETED; $feed_action = PhrictionActionConstants::ACTION_DELETE; if ($is_new) { throw new Exception( "You can not delete a document which doesn't exist yet!"); } break; case PhrictionChangeType::CHANGE_STUB: $doc_status = PhrictionDocumentStatus::STATUS_STUB; $feed_action = null; break; case PhrictionChangeType::CHANGE_MOVE_AWAY: $doc_status = PhrictionDocumentStatus::STATUS_MOVED; $feed_action = null; break; case PhrictionChangeType::CHANGE_MOVE_HERE: $doc_status = PhrictionDocumentStatus::STATUS_EXISTS; $feed_action = PhrictionActionConstants::ACTION_MOVE_HERE; break; default: throw new Exception( "Unsupported content change type '{$change_type}'!"); } $document->setStatus($doc_status); // TODO: This should be transactional. if ($is_new) { $document->save(); } $new_content->setDocumentID($document->getID()); $new_content->save(); $document->setContentID($new_content->getID()); $document->save(); $document->attachContent($new_content); id(new PhabricatorSearchIndexer()) ->queueDocumentForIndexing($document->getPHID()); // Stub out empty parent documents if they don't exist $ancestral_slugs = PhabricatorSlug::getAncestry($document->getSlug()); if ($ancestral_slugs) { $ancestors = id(new PhrictionDocument())->loadAllWhere( 'slug IN (%Ls)', $ancestral_slugs); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestral_slugs as $slug) { // We check for change type to prevent near-infinite recursion if (!isset($ancestors[$slug]) && $new_content->getChangeType() != PhrictionChangeType::CHANGE_STUB) { id(PhrictionDocumentEditor::newForSlug($slug)) ->setActor($this->getActor()) ->setTitle(PhabricatorSlug::getDefaultTitle($slug)) ->setContent('') ->setDescription(pht('Empty Parent Document')) ->stub(); } } } $project_phid = null; $slug = $document->getSlug(); if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProjectQuery()) ->setViewer($this->requireActor()) ->withPhrictionSlugs(array( PhrictionDocument::getProjectSlugIdentifier($slug))) ->executeOne(); if ($project) { $project_phid = $project->getPHID(); } } $related_phids = array( $document->getPHID(), $this->getActor()->getPHID(), ); if ($project_phid) { $related_phids[] = $project_phid; } if ($this->fromDocumentPHID) { $related_phids[] = $this->fromDocumentPHID; } if ($feed_action) { id(new PhabricatorFeedStoryPublisher()) ->setRelatedPHIDs($related_phids) ->setStoryAuthorPHID($this->getActor()->getPHID()) ->setStoryTime(time()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PHRICTION) ->setStoryData( array( 'phid' => $document->getPHID(), 'action' => $feed_action, 'content' => phutil_utf8_shorten($new_content->getContent(), 140), 'project' => $project_phid, 'movedFromPHID' => $this->fromDocumentPHID, )) ->publish(); } // TODO: Migrate to ApplicationTransactions fast, so we get rid of this code $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $document->getPHID()); $this->sendMailToSubscribers($subscribers, $content); return $this; } private function getChangeTypeDescription($const, $title) { $map = array( PhrictionChangeType::CHANGE_EDIT => pht("Phriction Document %s was edited.", $title), PhrictionChangeType::CHANGE_DELETE => pht("Phriction Document %s was deleted.", $title), PhrictionChangeType::CHANGE_MOVE_HERE => pht("Phriction Document %s was moved here.", $title), PhrictionChangeType::CHANGE_MOVE_AWAY => pht("Phriction Document %s was moved away.", $title), PhrictionChangeType::CHANGE_STUB => pht("Phriction Document %s was created through child.", $title), ); return idx($map, $const, pht('Something magical occurred.')); } private function sendMailToSubscribers(array $subscribers, $old_content) { if (!$subscribers) { return; } $author_phid = $this->getActor()->getPHID(); $document = $this->document; $content = $document->getContent(); $slug_uri = PhrictionDocument::getSlugURI($document->getSlug()); $diff_uri = new PhutilURI('/phriction/diff/'.$document->getID().'/'); $prod_uri = PhabricatorEnv::getProductionURI(''); $vs_head = $diff_uri ->alter('l', $old_content->getVersion()) ->alter('r', $content->getVersion()); $old_title = $old_content->getTitle(); $title = $content->getTitle(); $name = $this->getChangeTypeDescription($content->getChangeType(), $title); $action = PhrictionChangeType::getChangeTypeLabel( $content->getChangeType()); $body = array($name); // Content may have changed, you never know if ($content->getChangeType() == PhrictionChangeType::CHANGE_EDIT) { if ($old_title != $title) { $body[] = pht('Title was changed from "%s" to "%s"', $old_title, $title); } $body[] = pht("Link to new version:\n%s", $prod_uri.$slug_uri.'?v='.$content->getVersion()); $body[] = pht("Link to diff:\n%s", $prod_uri.$vs_head); } else if ($content->getChangeType() == PhrictionChangeType::CHANGE_MOVE_AWAY) { $target_document = id(new PhrictionDocument()) ->load($content->getChangeRef()); $slug_uri = PhrictionDocument::getSlugURI($target_document->getSlug()); $body[] = pht("Link to destination document:\n%s", $prod_uri.$slug_uri); } $body = implode("\n\n", $body); $subject_prefix = $this->getMailSubjectPrefix(); $mail = new PhabricatorMetaMTAMail(); $mail->setSubject($name) ->setSubjectPrefix($subject_prefix) ->setVarySubjectPrefix('['.$action.']') ->addHeader('Thread-Topic', $name) ->setFrom($author_phid) ->addTos($subscribers) ->setBody($body) ->setRelatedPHID($document->getPHID()) ->setIsBulk(true); $mail->saveAndSend(); } /* --( For less copy-pasting when switching to ApplicationTransactions )--- */ protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.phriction.subject-prefix'); } } diff --git a/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php b/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php index 9202455acc..8cb4835176 100644 --- a/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php +++ b/src/applications/phriction/phid/PhrictionPHIDTypeDocument.php @@ -1,53 +1,54 @@ needContent(true) ->withPHIDs($phids); } public function loadHandles( PhabricatorHandleQuery $query, array $handles, array $objects) { foreach ($handles as $phid => $handle) { $document = $objects[$phid]; $content = $document->getContent(); $title = $content->getTitle(); $slug = $document->getSlug(); $status = $document->getStatus(); $handle->setName($title); $handle->setURI(PhrictionDocument::getSlugURI($slug)); if ($status != PhrictionDocumentStatus::STATUS_EXISTS) { $handle->setStatus(PhabricatorObjectHandleStatus::STATUS_CLOSED); } } } } diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 5f2b3b1b6f..3e0b712235 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -1,193 +1,199 @@ 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 withStatus($status) { $this->status = $status; return $this; } + public function needContent($need_content) { + $this->needContent = $need_content; + return $this; + } + public function setOrder($order) { $this->order = $order; return $this; } protected function loadPage() { $document = new PhrictionDocument(); $conn_r = $document->establishConnection('r'); $rows = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $document->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $document->loadAllFromArray($rows); } protected function willFilterPage(array $documents) { - $contents = id(new PhrictionContent())->loadAllWhere( - 'id IN (%Ld)', - mpull($documents, 'getContentID')); + if ($this->needContent) { + $contents = id(new PhrictionContent())->loadAllWhere( + 'id IN (%Ld)', + mpull($documents, 'getContentID')); - foreach ($documents as $key => $document) { - $content_id = $document->getContentID(); - if (empty($contents[$content_id])) { - unset($documents[$key]); - continue; + foreach ($documents as $key => $document) { + $content_id = $document->getContentID(); + if (empty($contents[$content_id])) { + unset($documents[$key]); + continue; + } + $document->attachContent($contents[$content_id]); } - $document->attachContent($contents[$content_id]); } foreach ($documents as $document) { $document->attachProject(null); } $project_slugs = array(); foreach ($documents as $key => $document) { $slug = $document->getSlug(); if (!PhrictionDocument::isProjectSlug($slug)) { continue; } $project_slugs[$key] = PhrictionDocument::getProjectSlugIdentifier($slug); } if ($project_slugs) { $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withPhrictionSlugs($project_slugs) ->execute(); $projects = mpull($projects, null, 'getPhrictionSlug'); foreach ($documents as $key => $document) { $slug = idx($project_slugs, $key); if ($slug) { $project = idx($projects, $slug); if (!$project) { unset($documents[$key]); continue; } $document->attachProject($project); } } } return $documents; } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->slugs) { $where[] = qsprintf( $conn, 'slug IN (%Ls)', $this->slugs); } switch ($this->status) { case self::STATUS_OPEN: $where[] = qsprintf( $conn, 'status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_DELETED, PhrictionDocumentStatus::STATUS_MOVED, PhrictionDocumentStatus::STATUS_STUB, )); break; case self::STATUS_NONSTUB: $where[] = qsprintf( $conn, 'status NOT IN (%Ld)', array( PhrictionDocumentStatus::STATUS_MOVED, PhrictionDocumentStatus::STATUS_STUB, )); break; case self::STATUS_ANY: break; default: throw new Exception("Unknown status '{$this->status}'!"); } $where[] = $this->buildPagingClause($conn); return $this->formatWhereClause($where); } protected function getPagingColumn() { switch ($this->order) { case self::ORDER_CREATED: return 'id'; case self::ORDER_UPDATED: return 'contentID'; default: throw new Exception("Unknown order '{$this->order}'!"); } } protected function getPagingValue($result) { switch ($this->order) { case self::ORDER_CREATED: return $result->getID(); case self::ORDER_UPDATED: return $result->getContentID(); default: throw new Exception("Unknown order '{$this->order}'!"); } } public function getQueryApplicationClass() { return 'PhabricatorApplicationPhriction'; } } diff --git a/src/applications/phriction/query/PhrictionSearchEngine.php b/src/applications/phriction/query/PhrictionSearchEngine.php index 04a96bbbed..2cdee9f742 100644 --- a/src/applications/phriction/query/PhrictionSearchEngine.php +++ b/src/applications/phriction/query/PhrictionSearchEngine.php @@ -1,183 +1,184 @@ setParameter('status', $request->getArr('status')); $saved->setParameter('order', $request->getArr('order')); return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $query = id(new PhrictionDocumentQuery()) + ->needContent(true) ->withStatus(PhrictionDocumentQuery::STATUS_NONSTUB); $status = $saved->getParameter('status'); $status = idx($this->getStatusValues(), $status); if ($status) { $query->withStatus($status); } $order = $saved->getParameter('order'); $order = idx($this->getOrderValues(), $order); if ($order) { $query->setOrder($order); } return $query; } public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved_query) { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Status')) ->setName('status') ->setOptions($this->getStatusOptions()) ->setValue($saved_query->getParameter('status'))) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Order')) ->setName('order') ->setOptions($this->getOrderOptions()) ->setValue($saved_query->getParameter('order'))); } protected function getURI($path) { return '/phriction/'.$path; } public function getBuiltinQueryNames() { $names = array( 'active' => pht('Active'), 'updated' => pht('Updated'), 'all' => pht('All'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { case 'active': return $query->setParameter('status', 'active'); case 'all': return $query; case 'updated': return $query->setParameter('order', 'updated'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getStatusOptions() { return array( 'active' => pht('Show Active Documents'), 'all' => pht('Show All Documents'), ); } private function getStatusValues() { return array( 'active' => PhrictionDocumentQuery::STATUS_OPEN, 'all' => PhrictionDocumentQuery::STATUS_NONSTUB, ); } private function getOrderOptions() { return array( 'created' => pht('Date Created'), 'updated' => pht('Date Updated'), ); } private function getOrderValues() { return array( 'created' => PhrictionDocumentQuery::ORDER_CREATED, 'updated' => PhrictionDocumentQuery::ORDER_UPDATED, ); } protected function getRequiredHandlePHIDsForResultList( array $documents, PhabricatorSavedQuery $query) { $phids = array(); foreach ($documents as $document) { $content = $document->getContent(); if ($document->hasProject()) { $phids[] = $document->getProject()->getPHID(); } $phids[] = $content->getAuthorPHID(); } return $phids; } protected function renderResultList( array $documents, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($documents, 'PhrictionDocument'); $viewer = $this->requireViewer(); $list = new PHUIObjectItemListView(); $list->setUser($viewer); foreach ($documents as $document) { $content = $document->getContent(); $slug = $document->getSlug(); $author_phid = $content->getAuthorPHID(); $slug_uri = PhrictionDocument::getSlugURI($slug); $byline = pht( 'Edited by %s', $handles[$author_phid]->renderLink()); $updated = phabricator_datetime( $content->getDateCreated(), $viewer); $item = id(new PHUIObjectItemView()) ->setHeader($content->getTitle()) ->setHref($slug_uri) ->addByline($byline) ->addIcon('none', $updated); if ($document->hasProject()) { $item->addAttribute( $handles[$document->getProject()->getPHID()]->renderLink()); } $item->addAttribute($slug_uri); switch ($document->getStatus()) { case PhrictionDocumentStatus::STATUS_DELETED: $item->setDisabled(true); $item->addIcon('delete', pht('Deleted')); break; case PhrictionDocumentStatus::STATUS_MOVED: $item->setDisabled(true); $item->addIcon('arrow-right', pht('Moved Away')); break; } $list->addItem($item); } return $list; } } diff --git a/src/applications/phriction/search/PhrictionSearchIndexer.php b/src/applications/phriction/search/PhrictionSearchIndexer.php index 067da92d07..996002d04e 100644 --- a/src/applications/phriction/search/PhrictionSearchIndexer.php +++ b/src/applications/phriction/search/PhrictionSearchIndexer.php @@ -1,50 +1,47 @@ loadDocumentByPHID($phid); $content = id(new PhrictionContent())->load($document->getContentID()); $document->attachContent($content); $content = $document->getContent(); $doc = new PhabricatorSearchAbstractDocument(); $doc->setPHID($document->getPHID()); $doc->setDocumentType(PhrictionPHIDTypeDocument::TYPECONST); $doc->setDocumentTitle($content->getTitle()); // TODO: This isn't precisely correct, denormalize into the Document table? $doc->setDocumentCreated($content->getDateCreated()); $doc->setDocumentModified($content->getDateModified()); $doc->addField( PhabricatorSearchField::FIELD_BODY, $content->getContent()); $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $content->getAuthorPHID(), PhabricatorPeoplePHIDTypeUser::TYPECONST, $content->getDateCreated()); $doc->addRelationship( ($document->getStatus() == PhrictionDocumentStatus::STATUS_EXISTS) ? PhabricatorSearchRelationship::RELATIONSHIP_OPEN : PhabricatorSearchRelationship::RELATIONSHIP_CLOSED, $document->getPHID(), PhrictionPHIDTypeDocument::TYPECONST, time()); return $doc; } }