diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 2f1cc2f6e5..5c04eefaf2 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -1,507 +1,504 @@ 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->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(); $slug_uri = null; // If the new document exists and the viewer can see it, provide a link // to it. Otherwise, render a generic message. $new_docs = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withIDs(array($new_doc_id)) ->execute(); if ($new_docs) { $new_doc = head($new_docs); $slug_uri = PhrictionDocument::getSlugURI($new_doc->getSlug()); } $notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE); if ($slug_uri) { $notice->appendChild( phutil_tag( 'p', array(), pht( 'This document has been moved to %s. You can edit it to put '. 'new content here, or use history to revert to an earlier '. 'version.', phutil_tag('a', array('href' => $slug_uri), $slug_uri)))); } else { $notice->appendChild( phutil_tag( 'p', array(), pht( 'This document has been moved. You can edit it to put new '. 'contne here, or use history to revert to an earlier '. 'version.'))); } $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(); $slug_uri = null; // If the old document exists and is visible, provide a link to it. $from_docs = id(new PhrictionDocumentQuery()) ->setViewer($user) ->withIDs(array($from_doc_id)) ->execute(); if ($from_docs) { $from_doc = head($from_docs); $slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug()); } $move_notice = id(new AphrontErrorView()) ->setSeverity(AphrontErrorView::SEVERITY_NOTICE); if ($slug_uri) { $move_notice->appendChild( pht( 'This document was moved from %s.', phutil_tag('a', array('href' => $slug_uri), $slug_uri))); } else { // Render this for consistency, even though it's a bit silly. $move_notice->appendChild( pht('This document was moved from elsewhere.')); } } } $children = $this->renderDocumentChildren($slug); $actions = $this->buildActionView($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()) ->setUser($user) ->setPolicyObject($document) ->setHeader($page_title); $prop_list = null; if ($properties) { $prop_list = new PHUIPropertyGroupView(); $prop_list->addPropertyList($properties); } $page_content = id(new PHUIDocumentView()) ->setOffset(true) ->setFontKit(PHUIDocumentView::FONT_SOURCE_SANS) ->setHeader($header) ->appendChild( array( $actions, $prop_list, $version_note, $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, )); } 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; + $limit = 250; - // 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( + $query = id(new PhrictionDocumentQuery()) + ->setViewer($this->getRequest()->getUser()) + ->withDepths(array($d_child, $d_grandchild)) + ->withSlugPrefix($slug == '/' ? '' : $slug) + ->withStatuses(array( PhrictionDocumentStatus::STATUS_EXISTS, PhrictionDocumentStatus::STATUS_STUB, - ), - $limit); + )) + ->setLimit($limit) + ->setOrder(PhrictionDocumentQuery::ORDER_HIERARCHY) + ->needContent(true); + $children = $query->execute(); if (!$children) { return; } // We're going to render in one of three modes to try to accommodate // different information scales: // // - If we found fewer than $limit rows, we know we have all the children // and grandchildren and there aren't all that many. We can just render // everything. // - If we found $limit rows but the results included some grandchildren, // we just throw them out and render only the children, as we know we // have them all. // - If we found $limit rows and the results have no grandchildren, we // have a ton of children. Render them and then let the user know that // this is not an exhaustive list. if (count($children) == $limit) { $more_children = true; foreach ($children as $child) { - if ($child['depth'] == $d_grandchild) { + if ($child->getDepth() == $d_grandchild) { $more_children = false; } } $show_grandchildren = false; } else { $show_grandchildren = true; $more_children = false; } - $grandchildren = array(); + $children_dicts = array(); + $grandchildren_dicts = array(); foreach ($children as $key => $child) { - if ($child['depth'] == $d_child) { + $child_dict = array( + 'slug' => $child->getSlug(), + 'depth' => $child->getDepth(), + 'title' => $child->getContent()->getTitle(),); + if ($child->getDepth() == $d_child) { + $children_dicts[] = $child_dict; continue; } else { unset($children[$key]); if ($show_grandchildren) { - $ancestors = PhabricatorSlug::getAncestry($child['slug']); - $grandchildren[end($ancestors)][] = $child; + $ancestors = PhabricatorSlug::getAncestry($child->getSlug()); + $grandchildren_dicts[end($ancestors)][] = $child_dict; } } } // Fill in any missing children. - $known_slugs = ipull($children, null, 'slug'); - foreach ($grandchildren as $slug => $ignored) { + $known_slugs = mpull($children, null, 'getSlug'); + foreach ($grandchildren_dicts as $slug => $ignored) { if (empty($known_slugs[$slug])) { - $children[] = array( + $children_dicts[] = array( 'slug' => $slug, 'depth' => $d_child, 'title' => PhabricatorSlug::getDefaultTitle($slug), 'empty' => true, ); } } - $children = isort($children, 'title'); + $children_dicts = isort($children_dicts, 'title'); $list = array(); - foreach ($children as $child) { + foreach ($children_dicts as $child) { $list[] = hsprintf('
  • '); $list[] = $this->renderChildDocumentLink($child); - $grand = idx($grandchildren, $child['slug'], array()); + $grand = idx($grandchildren_dicts, $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/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 35b4ee1675..f47745d191 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -1,242 +1,321 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withSlugs(array $slugs) { $this->slugs = $slugs; return $this; } + public function withDepths(array $depths) { + $this->depths = $depths; + return $this; + } + + public function withSlugPrefix($slug_prefix) { + $this->slugPrefix = $slug_prefix; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + public function withStatus($status) { $this->status = $status; return $this; } public function needContent($need_content) { $this->needContent = $need_content; return $this; } public function setOrder($order) { $this->order = $order; return $this; } protected function loadPage() { $table = new PhrictionDocument(); $conn_r = $table->establishConnection('r'); + if ($this->order = self::ORDER_HIERARCHY) { + $order_clause = $this->buildHierarchicalOrderClause($conn_r); + } else { + $order_clause = $this->buildOrderClause($conn_r); + } + $rows = queryfx_all( $conn_r, - 'SELECT * FROM %T %Q %Q %Q', + 'SELECT * FROM %T d %Q %Q %Q %Q', $table->getTableName(), + $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), + $order_clause, $this->buildLimitClause($conn_r)); $documents = $table->loadAllFromArray($rows); if ($documents) { $ancestor_slugs = array(); foreach ($documents as $key => $document) { $document_slug = $document->getSlug(); foreach (PhabricatorSlug::getAncestry($document_slug) as $ancestor) { $ancestor_slugs[$ancestor][] = $key; } } if ($ancestor_slugs) { $ancestors = queryfx_all( $conn_r, 'SELECT * FROM %T WHERE slug IN (%Ls)', $document->getTableName(), array_keys($ancestor_slugs)); $ancestors = $table->loadAllFromArray($ancestors); $ancestors = mpull($ancestors, null, 'getSlug'); foreach ($ancestor_slugs as $ancestor_slug => $document_keys) { $ancestor = idx($ancestors, $ancestor_slug); foreach ($document_keys as $document_key) { $documents[$document_key]->attachAncestor( $ancestor_slug, $ancestor); } } } } return $documents; } protected function willFilterPage(array $documents) { // To view a Phriction document, you must also be able to view all of the // ancestor documents. Filter out documents which have ancestors that are // not visible. $document_map = array(); foreach ($documents as $document) { $document_map[$document->getSlug()] = $document; foreach ($document->getAncestors() as $key => $ancestor) { if ($ancestor) { $document_map[$key] = $ancestor; } } } $filtered_map = $this->applyPolicyFilter( $document_map, array(PhabricatorPolicyCapability::CAN_VIEW)); // Filter all of the documents where a parent is not visible. foreach ($documents as $document_key => $document) { // If the document itself is not visible, filter it. if (!isset($filtered_map[$document->getSlug()])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); continue; } // If an ancestor exists but is not visible, filter the document. foreach ($document->getAncestors() as $ancestor_key => $ancestor) { if (!$ancestor) { continue; } if (!isset($filtered_map[$ancestor_key])) { $this->didRejectResult($documents[$document_key]); unset($documents[$document_key]); break; } } } if (!$documents) { return $documents; } if ($this->needContent) { $contents = id(new PhrictionContent())->loadAllWhere( 'id IN (%Ld)', mpull($documents, 'getContentID')); foreach ($documents as $key => $document) { $content_id = $document->getContentID(); if (empty($contents[$content_id])) { unset($documents[$key]); continue; } $document->attachContent($contents[$content_id]); } } return $documents; } + private function buildJoinClause(AphrontDatabaseConnection $conn) { + $join = null; + if ($this->order = self::ORDER_HIERARCHY) { + $content_dao = new PhrictionContent(); + $join = qsprintf( + $conn, + 'JOIN %T c ON d.contentID = c.id', + $content_dao->getTableName()); + } + return $join; + } protected function buildWhereClause(AphrontDatabaseConnection $conn) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'd.id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'd.phid IN (%Ls)', $this->phids); } if ($this->slugs) { $where[] = qsprintf( $conn, - 'slug IN (%Ls)', + 'd.slug IN (%Ls)', $this->slugs); } + if ($this->statuses) { + $where[] = qsprintf( + $conn, + 'd.status IN (%Ld)', + $this->statuses); + } + + if ($this->slugPrefix) { + $where[] = qsprintf( + $conn, + 'd.slug LIKE %>', + $this->slugPrefix); + } + + if ($this->depths) { + $where[] = qsprintf( + $conn, + 'd.depth IN (%Ld)', + $this->depths); + } + switch ($this->status) { case self::STATUS_OPEN: $where[] = qsprintf( $conn, - 'status NOT IN (%Ld)', + 'd.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)', + 'd.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); } + private function buildHierarchicalOrderClause( + AphrontDatabaseConnection $conn_r) { + + if ($this->getBeforeID()) { + return qsprintf( + $conn_r, + 'ORDER BY d.depth, c.title, %Q %Q', + $this->getPagingColumn(), + $this->getReversePaging() ? 'DESC' : 'ASC'); + } else { + return qsprintf( + $conn_r, + 'ORDER BY d.depth, c.title, %Q %Q', + $this->getPagingColumn(), + $this->getReversePaging() ? 'ASC' : 'DESC'); + } + } + protected function getPagingColumn() { switch ($this->order) { case self::ORDER_CREATED: - return 'id'; + case self::ORDER_HIERARCHY: + return 'd.id'; case self::ORDER_UPDATED: - return 'contentID'; + return 'd.contentID'; default: throw new Exception("Unknown order '{$this->order}'!"); } } protected function getPagingValue($result) { switch ($this->order) { case self::ORDER_CREATED: + case self::ORDER_HIERARCHY: return $result->getID(); case self::ORDER_UPDATED: return $result->getContentID(); default: throw new Exception("Unknown order '{$this->order}'!"); } } public function getQueryApplicationClass() { return 'PhabricatorPhrictionApplication'; } }