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 | '. '
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; } }