From b262ccdaff31fb79a11e87ff083de425d01608a9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 3 Feb 2014 12:52:47 -0800 Subject: [PATCH] Use ApplicationSearch to power primary search Summary: Ref T4365. Drive primary search through ApplicationSearch instead of through a bunch of custom nonsense. Notably, this allows you to save searches, notably. The one thing this doesn't do -- which I'd like it to -- is carry your query text across searches. When you search for "quack", I want to overwrite the query in your default filter and give you those results, so you can turn the search into an "Open Tasks" search by default by reordering the queries. I'll probably do that next. It feels a little hacky but I want to try it out. Test Plan: {F106932} Reviewers: btrahan, chad Reviewed By: btrahan CC: aran, bigo Maniphest Tasks: T4365 Differential Revision: https://secure.phabricator.com/D8123 --- src/__phutil_library_map__.php | 6 +- .../PhabricatorApplicationSearch.php | 3 +- .../config/PhabricatorSearchConfigOptions.php | 1 + .../PhabricatorSearchController.php | 344 ++++-------------- .../engine/PhabricatorSearchEngineElastic.php | 66 ++-- .../engine/PhabricatorSearchEngineMySQL.php | 88 +++-- .../PhabricatorSearchAbstractDocument.php | 14 - ...abricatorSearchApplicationSearchEngine.php | 182 ++++++++- .../query/PhabricatorSearchDocumentQuery.php | 56 ++- 9 files changed, 410 insertions(+), 350 deletions(-) diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 654ac6ee69..a1bfc5b6e2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4692,7 +4692,11 @@ phutil_register_library_map(array( 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions', - 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', + 'PhabricatorSearchController' => + array( + 0 => 'PhabricatorSearchBaseController', + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', + ), 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', diff --git a/src/applications/search/application/PhabricatorApplicationSearch.php b/src/applications/search/application/PhabricatorApplicationSearch.php index ea7da96a34..eb2199947b 100644 --- a/src/applications/search/application/PhabricatorApplicationSearch.php +++ b/src/applications/search/application/PhabricatorApplicationSearch.php @@ -29,8 +29,7 @@ final class PhabricatorApplicationSearch extends PhabricatorApplication { public function getRoutes() { return array( '/search/' => array( - '' => 'PhabricatorSearchController', - '(?P[^/]+)/' => 'PhabricatorSearchController', + '(?:query/(?P[^/]+)/)?' => 'PhabricatorSearchController', 'attach/(?P[^/]+)/(?P\w+)/(?:(?P\w+)/)?' => 'PhabricatorSearchAttachController', 'select/(?P\w+)/' diff --git a/src/applications/search/config/PhabricatorSearchConfigOptions.php b/src/applications/search/config/PhabricatorSearchConfigOptions.php index 21048bdc4a..71d3f262d6 100644 --- a/src/applications/search/config/PhabricatorSearchConfigOptions.php +++ b/src/applications/search/config/PhabricatorSearchConfigOptions.php @@ -29,6 +29,7 @@ final class PhabricatorSearchConfigOptions "your documents in some search engine which does not have ". "default support.")), $this->newOption('search.elastic.host', 'string', null) + ->setLocked(true) ->setDescription(pht("Elastic Search host.")) ->addExample('http://elastic.example.com:9200/', pht('Valid Setting')), ); diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php index e570f745bf..82be2e14b9 100644 --- a/src/applications/search/controller/PhabricatorSearchController.php +++ b/src/applications/search/controller/PhabricatorSearchController.php @@ -1,298 +1,98 @@ key = idx($data, 'key'); + $this->queryKey = idx($data, 'queryKey'); } public function processRequest() { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); - if ($this->key) { - $query = id(new PhabricatorSavedQuery())->loadOneWhere( - 'queryKey = %s', - $this->key); - if (!$query) { - return new Aphront404Response(); - } - } else { - $query = id(new PhabricatorSavedQuery()) - ->setEngineClassName('PhabricatorSearchApplicationSearchEngine'); - - if ($request->isFormPost()) { - $query_str = $request->getStr('query'); - - $pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP; - if ($request->getStr('jump') != 'no' && - $user && $user->loadPreferences()->getPreference($pref_jump, 1)) { - $response = PhabricatorJumpNavHandler::getJumpResponse( - $user, - $query_str); - } else { - $response = null; - } + if ($request->getStr('jump') != 'no') { + $pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP; + if ($viewer->loadPreferences($pref_jump, 1)) { + $response = PhabricatorJumpNavHandler::getJumpResponse( + $viewer, + $request->getStr('query')); if ($response) { return $response; - } else { - $query->setParameter('query', $query_str); - - if ($request->getStr('scope')) { - switch ($request->getStr('scope')) { - case PhabricatorSearchScope::SCOPE_OPEN_REVISIONS: - $query->setParameter('open', 1); - $query->setParameter( - 'type', - DifferentialPHIDTypeRevision::TYPECONST); - break; - case PhabricatorSearchScope::SCOPE_OPEN_TASKS: - $query->setParameter('open', 1); - $query->setParameter( - 'type', - ManiphestPHIDTypeTask::TYPECONST); - break; - case PhabricatorSearchScope::SCOPE_WIKI: - $query->setParameter( - 'type', - PhrictionPHIDTypeDocument::TYPECONST); - break; - case PhabricatorSearchScope::SCOPE_COMMITS: - $query->setParameter( - 'type', - PhabricatorRepositoryPHIDTypeCommit::TYPECONST); - break; - default: - break; - } - } else { - if (strlen($request->getStr('type'))) { - $query->setParameter('type', $request->getStr('type')); - } - - if ($request->getArr('author')) { - $query->setParameter('author', $request->getArr('author')); - } - - if ($request->getArr('owner')) { - $query->setParameter('owner', $request->getArr('owner')); - } - - if ($request->getArr('subscribers')) { - $query->setParameter('subscribers', - $request->getArr('subscribers')); - } - - if ($request->getInt('open')) { - $query->setParameter('open', $request->getInt('open')); - } - - if ($request->getArr('project')) { - $query->setParameter('project', $request->getArr('project')); - } - } - - try { - $query->save(); - } catch (AphrontQueryDuplicateKeyException $ex) { - // Someone has already executed this query. - } - return id(new AphrontRedirectResponse()) - ->setURI('/search/'.$query->getQueryKey().'/'); } } } - $options = array( - '' => 'All Documents', - ) + PhabricatorSearchAbstractDocument::getSupportedTypes(); + $controller = id(new PhabricatorApplicationSearchController($request)) + ->setQueryKey($this->queryKey) + ->setSearchEngine(new PhabricatorSearchApplicationSearchEngine()) + ->setUseOffsetPaging(true) + ->setNavigation($this->buildSideNavView()); - $status_options = array( - 0 => 'Open and Closed Documents', - 1 => 'Open Documents', - ); - - $phids = array_merge( - $query->getParameter('author', array()), - $query->getParameter('owner', array()), - $query->getParameter('subscribers', array()), - $query->getParameter('project', array())); - - $handles = $this->loadViewerHandles($phids); - - $author_value = array_select_keys( - $handles, - $query->getParameter('author', array())); - - $owner_value = array_select_keys( - $handles, - $query->getParameter('owner', array())); - - $subscribers_value = array_select_keys( - $handles, - $query->getParameter('subscribers', array())); - - $project_value = array_select_keys( - $handles, - $query->getParameter('project', array())); - - $search_form = new AphrontFormView(); - $search_form - ->setUser($user) - ->setAction('/search/') - ->appendChild( - phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => 'jump', - 'value' => 'no', - ))) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Search') - ->setName('query') - ->setValue($query->getParameter('query'))) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel('Document Type') - ->setName('type') - ->setOptions($options) - ->setValue($query->getParameter('type'))) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel('Document Status') - ->setName('open') - ->setOptions($status_options) - ->setValue($query->getParameter('open'))) - ->appendChild( - id(new AphrontFormTokenizerControl()) - ->setName('author') - ->setLabel('Author') - ->setDatasource('/typeahead/common/users/') - ->setValue($author_value)) - ->appendChild( - id(new AphrontFormTokenizerControl()) - ->setName('owner') - ->setLabel('Owner') - ->setDatasource('/typeahead/common/searchowner/') - ->setValue($owner_value) - ->setCaption( - 'Tip: search for "Up For Grabs" to find unowned documents.')) - ->appendChild( - id(new AphrontFormTokenizerControl()) - ->setName('subscribers') - ->setLabel('Subscribers') - ->setDatasource('/typeahead/common/users/') - ->setValue($subscribers_value)) - ->appendChild( - id(new AphrontFormTokenizerControl()) - ->setName('project') - ->setLabel('Project') - ->setDatasource('/typeahead/common/projects/') - ->setValue($project_value)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue('Search')); - - $search_panel = new AphrontListFilterView(); - $search_panel->appendChild($search_form); - - require_celerity_resource('phabricator-search-results-css'); - - if ($query->getID()) { - - $limit = 20; - - $pager = new AphrontPagerView(); - $pager->setURI($request->getRequestURI(), 'page'); - $pager->setPageSize($limit); - $pager->setOffset($request->getInt('page')); - - $query->setParameter('limit', $limit + 1); - $query->setParameter('offset', $pager->getOffset()); - - $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); - $results = $engine->executeSearch($query); - $results = $pager->sliceResults($results); - - // If there are any objects which match the query by name, and we're - // not paging through the results, prefix the results with the named - // objects. - if (!$request->getInt('page')) { - $named = id(new PhabricatorObjectQuery()) - ->setViewer($user) - ->withNames(array($query->getParameter('queyr'))) - ->execute(); - if ($named) { - $results = array_merge(array_keys($named), $results); - } - } - - if ($results) { - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($user) - ->withPHIDs($results) - ->execute(); - $objects = id(new PhabricatorObjectQuery()) - ->setViewer($user) - ->withPHIDs($results) - ->execute(); - $results = array(); - foreach ($handles as $phid => $handle) { - $view = id(new PhabricatorSearchResultView()) - ->setHandle($handle) - ->setQuery($query) - ->setObject(idx($objects, $phid)); - $results[] = $view->render(); - } - - $results = phutil_tag_div('phabricator-search-result-list', array( - phutil_implode_html("\n", $results), - phutil_tag_div('search-results-pager', $pager->render()), - )); - } else { - $results = phutil_tag_div( - 'phabricator-search-result-list', - phutil_tag( - 'p', - array('class' => 'phabricator-search-no-results'), - pht('No search results.'))); - } - $results = id(new PHUIBoxView()) - ->addMargin(PHUI::MARGIN_LARGE) - ->addPadding(PHUI::PADDING_LARGE) - ->setShadow(true) - ->appendChild($results) - ->addClass('phabricator-search-result-box'); - } else { - $results = null; - } - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Search')); - - return $this->buildApplicationPage( - array( - $crumbs, - $search_panel, - $results, - ), - array( - 'title' => pht('Search Results'), - 'device' => true, - )); + return $this->delegateToController($controller); } + public function buildSideNavView($for_app = false) { + $viewer = $this->getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new PhabricatorSearchApplicationSearchEngine()) + ->setViewer($viewer) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + public function renderResultsList( + array $results, + PhabricatorSavedQuery $query) { + + $viewer = $this->getRequest()->getUser(); + + if ($results) { + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(mpull($results, 'getPHID')) + ->execute(); + + $output = array(); + foreach ($results as $phid => $handle) { + $view = id(new PhabricatorSearchResultView()) + ->setHandle($handle) + ->setQuery($query) + ->setObject(idx($objects, $phid)); + $output[] = $view->render(); + } + + $results = phutil_tag_div( + 'phabricator-search-result-list', + $output); + } else { + $results = phutil_tag_div( + 'phabricator-search-result-list', + phutil_tag( + 'p', + array('class' => 'phabricator-search-no-results'), + pht('No search results.'))); + } + + return id(new PHUIBoxView()) + ->addMargin(PHUI::MARGIN_LARGE) + ->addPadding(PHUI::PADDING_LARGE) + ->setShadow(true) + ->appendChild($results) + ->addClass('phabricator-search-result-box'); + } } diff --git a/src/applications/search/engine/PhabricatorSearchEngineElastic.php b/src/applications/search/engine/PhabricatorSearchEngineElastic.php index ad4072e62b..95b973ad28 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineElastic.php +++ b/src/applications/search/engine/PhabricatorSearchEngineElastic.php @@ -95,7 +95,7 @@ final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine { $spec = array(); $filter = array(); - if ($query->getParameter('query') != '') { + if (strlen($query->getParameter('query'))) { $spec[] = array( 'field' => array( 'field.corpus' => $query->getParameter('query'), @@ -114,17 +114,41 @@ final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine { ); } - $rel_mapping = array( - 'author' => PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, - 'open' => PhabricatorSearchRelationship::RELATIONSHIP_OPEN, - 'owner' => PhabricatorSearchRelationship::RELATIONSHIP_OWNER, - 'subscribers' => PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER, - 'project' => PhabricatorSearchRelationship::RELATIONSHIP_PROJECT, - 'repository' => PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY, + $relationship_map = array( + PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR => + $query->getParameter('authorPHIDs', array()), + PhabricatorSearchRelationship::RELATIONSHIP_OWNER => + $query->getParameter('ownerPHIDs', array()), + PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER => + $query->getParameter('subscriberPHIDs', array()), + PhabricatorSearchRelationship::RELATIONSHIP_PROJECT => + $query->getParameter('projectPHIDs', array()), + PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY => + $query->getParameter('repositoryPHIDs', array()), ); - foreach ($rel_mapping as $name => $field) { - $param = $query->getParameter($name); - if (is_array($param)) { + + $statuses = $query->getParameter('statuses', array()); + $statuses = array_fuse($statuses); + + $rel_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN; + $rel_closed = PhabricatorSearchRelationship::RELATIONSHIP_CLOSED; + $rel_unowned = PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED; + + $include_open = !empty($statuses[$rel_open]); + $include_closed = !empty($statuses[$rel_closed]); + + if ($include_open && !$include_closed) { + $relationship_map[$rel_open] = true; + } else if (!$include_open && $include_closed) { + $relationship_map[$rel_closed] = true; + } + + if ($query->getParameter('withUnowned')) { + $relationship_map[$rel_unowned] = true; + } + + foreach ($relationship_map as $field => $param) { + if (is_array($param) && $param) { $should = array(); foreach ($param as $val) { $should[] = array( @@ -177,24 +201,24 @@ final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine { } public function executeSearch(PhabricatorSavedQuery $query) { - $type = $query->getParameter('type'); - if ($type) { - $uri = "/phabricator/{$type}/_search"; - } else { - // Don't use '/phabricator/_search' for the case that there is something - // else in the index (for example if 'phabricator' is only an alias to - // some bigger index). - $types = PhabricatorSearchAbstractDocument::getSupportedTypes(); - $uri = '/phabricator/' . implode(',', array_keys($types)) . '/_search'; + $types = $query->getParameter('types'); + if (!$types) { + $types = array_keys( + PhabricatorSearchApplicationSearchEngine::getIndexableDocumentTypes()); } + // Don't use '/phabricator/_search' for the case that there is something + // else in the index (for example if 'phabricator' is only an alias to + // some bigger index). + $uri = '/phabricator/'.implode(',', $types).'/_search'; + try { $response = $this->executeRequest($uri, $this->buildSpec($query)); } catch (HTTPFutureResponseStatusHTTP $ex) { // elasticsearch probably uses Lucene query syntax: // http://lucene.apache.org/core/3_6_1/queryparsersyntax.html // Try literal search if operator search fails. - if (!$query->getParameter('query')) { + if (!strlen($query->getParameter('query'))) { throw $ex; } $query = clone $query; diff --git a/src/applications/search/engine/PhabricatorSearchEngineMySQL.php b/src/applications/search/engine/PhabricatorSearchEngineMySQL.php index 80aea6b94b..91603749bb 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineMySQL.php +++ b/src/applications/search/engine/PhabricatorSearchEngineMySQL.php @@ -179,7 +179,7 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine { $q); $field = $query->getParameter('field'); - if ($field/* && $field != AdjutantQuery::FIELD_ALL*/) { + if ($field) { $where[] = qsprintf( $conn_r, 'field.field = %s', @@ -192,49 +192,74 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine { $where[] = qsprintf($conn_r, 'document.phid != %s', $exclude); } - if ($query->getParameter('type')) { + $types = $query->getParameter('types'); + if ($types) { if (strlen($q)) { - // TODO: verify that this column actually does something useful in query - // plans once we have nontrivial amounts of data. $where[] = qsprintf( $conn_r, - 'field.phidType = %s', - $query->getParameter('type')); + 'field.phidType IN (%Ls)', + $types); } $where[] = qsprintf( $conn_r, - 'document.documentType = %s', - $query->getParameter('type')); + 'document.documentType IN (%Ls)', + $types); } $join[] = $this->joinRelationship( $conn_r, $query, - 'author', + 'authorPHIDs', PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR); - $join[] = $this->joinRelationship( - $conn_r, - $query, - 'open', - PhabricatorSearchRelationship::RELATIONSHIP_OPEN); + $statuses = $query->getParameter('statuses', array()); + $statuses = array_fuse($statuses); + $open_rel = PhabricatorSearchRelationship::RELATIONSHIP_OPEN; + $closed_rel = PhabricatorSearchRelationship::RELATIONSHIP_CLOSED; + $include_open = !empty($statuses[$open_rel]); + $include_closed = !empty($statuses[$closed_rel]); + + if ($include_open && !$include_closed) { + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'statuses', + $open_rel, + true); + } else if ($include_closed && !$include_open) { + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'statuses', + $closed_rel, + true); + } + + if ($query->getParameter('withUnowned')) { + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'withUnowned', + PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED, + true); + } else { + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'ownerPHIDs', + PhabricatorSearchRelationship::RELATIONSHIP_OWNER); + } $join[] = $this->joinRelationship( $conn_r, $query, - 'owner', - PhabricatorSearchRelationship::RELATIONSHIP_OWNER); - - $join[] = $this->joinRelationship( - $conn_r, - $query, - 'subscribers', + 'subscriberPHIDs', PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER); $join[] = $this->joinRelationship( $conn_r, $query, - 'project', + 'projectPHIDs', PhabricatorSearchRelationship::RELATIONSHIP_PROJECT); $join[] = $this->joinRelationship( @@ -283,19 +308,8 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine { AphrontDatabaseConnection $conn, PhabricatorSavedQuery $query, $field, - $type) { - - $phids = $query->getParameter($field, array()); - if (!$phids) { - return null; - } - - $is_existence = false; - switch ($type) { - case PhabricatorSearchRelationship::RELATIONSHIP_OPEN: - $is_existence = true; - break; - } + $type, + $is_existence = false) { $sql = qsprintf( $conn, @@ -307,6 +321,10 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine { $type); if (!$is_existence) { + $phids = $query->getParameter($field, array()); + if (!$phids) { + return null; + } $sql .= qsprintf( $conn, ' AND %C.relatedPHID in (%Ls)', diff --git a/src/applications/search/index/PhabricatorSearchAbstractDocument.php b/src/applications/search/index/PhabricatorSearchAbstractDocument.php index 8f73be1f33..3e7626f798 100644 --- a/src/applications/search/index/PhabricatorSearchAbstractDocument.php +++ b/src/applications/search/index/PhabricatorSearchAbstractDocument.php @@ -1,8 +1,5 @@ 'Differential Revisions', - PhabricatorRepositoryPHIDTypeCommit::TYPECONST => 'Repository Commits', - ManiphestPHIDTypeTask::TYPECONST => 'Maniphest Tasks', - PhrictionPHIDTypeDocument::TYPECONST => 'Phriction Documents', - PhabricatorPeoplePHIDTypeUser::TYPECONST => 'Phabricator Users', - PonderPHIDTypeQuestion::TYPECONST => 'Ponder Questions', - ); - } - public function setPHID($phid) { $this->phid = $phid; return $this; diff --git a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php index dc08ed21ae..d9a32f7e7a 100644 --- a/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php +++ b/src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php @@ -6,19 +6,155 @@ final class PhabricatorSearchApplicationSearchEngine public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); + $saved->setParameter('query', $request->getStr('query')); + $saved->setParameter( + 'statuses', + $this->readListFromRequest($request, 'statuses')); + $saved->setParameter( + 'types', + $this->readListFromRequest($request, 'types')); + + $saved->setParameter( + 'authorPHIDs', + $this->readUsersFromRequest($request, 'authorPHIDs')); + + $saved->setParameter( + 'ownerPHIDs', + $this->readUsersFromRequest($request, 'ownerPHIDs')); + + $saved->setParameter( + 'withUnowned', + $this->readBoolFromRequest($request, 'withUnowned')); + + $saved->setParameter( + 'subscriberPHIDs', + $this->readPHIDsFromRequest($request, 'subscriberPHIDs')); + + $saved->setParameter( + 'projectPHIDs', + $this->readPHIDsFromRequest($request, 'projectPHIDs')); + return $saved; } public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorSearchDocumentQuery()); - + $query = id(new PhabricatorSearchDocumentQuery()) + ->withSavedQuery($saved); return $query; } public function buildSearchForm( AphrontFormView $form, - PhabricatorSavedQuery $saved_query) { - return; + PhabricatorSavedQuery $saved) { + + $options = array(); + $author_value = null; + $owner_value = null; + $subscribers_value = null; + $project_value = null; + + $author_phids = $saved->getParameter('authorPHIDs', array()); + $owner_phids = $saved->getParameter('ownerPHIDs', array()); + $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); + $project_phids = $saved->getParameter('projectPHIDs', array()); + + $all_phids = array_merge( + $author_phids, + $owner_phids, + $subscriber_phids, + $project_phids); + + $all_handles = id(new PhabricatorHandleQuery()) + ->setViewer($this->requireViewer()) + ->withPHIDs($all_phids) + ->execute(); + + $author_handles = array_select_keys($all_handles, $author_phids); + $owner_handles = array_select_keys($all_handles, $owner_phids); + $subscriber_handles = array_select_keys($all_handles, $subscriber_phids); + $project_handles = array_select_keys($all_handles, $project_phids); + + $with_unowned = $saved->getParameter('withUnowned', array()); + + $status_values = $saved->getParameter('statuses', array()); + $status_values = array_fuse($status_values); + + $statuses = array( + PhabricatorSearchRelationship::RELATIONSHIP_OPEN => pht('Open'), + PhabricatorSearchRelationship::RELATIONSHIP_CLOSED => pht('Closed'), + ); + $status_control = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Document Status')); + foreach ($statuses as $status => $name) { + $status_control->addCheckbox( + 'statuses[]', + $status, + $name, + isset($status_values[$status])); + } + + $type_values = $saved->getParameter('types', array()); + $type_values = array_fuse($type_values); + + $types = self::getIndexableDocumentTypes(); + + $types_control = id(new AphrontFormCheckboxControl()) + ->setLabel(pht('Document Types')); + foreach ($types as $type => $name) { + $types_control->addCheckbox( + 'types[]', + $type, + $name, + isset($type_values[$type])); + } + + $form + ->appendChild( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'jump', + 'value' => 'no', + ))) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Query') + ->setName('query') + ->setValue($saved->getParameter('query'))) + ->appendChild($status_control) + ->appendChild($types_control) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setName('authorPHIDs') + ->setLabel('Authors') + ->setDatasource('/typeahead/common/users/') + ->setValue($author_handles)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setName('ownerPHIDs') + ->setLabel('Owners') + ->setDatasource('/typeahead/common/searchowner/') + ->setValue($owner_handles)) + ->appendChild( + id(new AphrontFormCheckboxControl()) + ->addCheckbox( + 'withUnowned', + 1, + pht('Show only unowned documents.'), + $with_unowned)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setName('subscriberPHIDs') + ->setLabel('Subscribers') + ->setDatasource('/typeahead/common/users/') + ->setValue($subscriber_handles)) + ->appendChild( + id(new AphrontFormTokenizerControl()) + ->setName('projectPHIDs') + ->setLabel('In Any Project') + ->setDatasource('/typeahead/common/projects/') + ->setValue($project_handles)); } protected function getURI($path) { @@ -28,6 +164,8 @@ final class PhabricatorSearchApplicationSearchEngine public function getBuiltinQueryNames() { $names = array( 'all' => pht('All Documents'), + 'open' => pht('Open Documents'), + 'open-tasks' => pht('Open Tasks'), ); return $names; @@ -41,9 +179,45 @@ final class PhabricatorSearchApplicationSearchEngine switch ($query_key) { case 'all': return $query; + case 'open': + return $query->setParameter('statuses', array('open')); + case 'open-tasks': + return $query + ->setParameter('statuses', array('open')) + ->setParameter('types', array(ManiphestPHIDTypeTask::TYPECONST)); } return parent::buildSavedQueryFromBuiltin($query_key); } + public static function getIndexableDocumentTypes() { + // TODO: This is inelegant and not very efficient, but gets us reasonable + // results. It would be nice to do this more elegantly. + + // TODO: We should hide types associated with applications the user can + // not access. There's no reasonable way to do this right now. + + $indexers = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorSearchDocumentIndexer') + ->loadObjects(); + + $types = PhabricatorPHIDType::getAllTypes(); + + $results = array(); + foreach ($types as $type) { + $typeconst = $type->getTypeConstant(); + foreach ($indexers as $indexer) { + $fake_phid = 'PHID-'.$typeconst.'-fake'; + if ($indexer->shouldIndexDocumentByPHID($fake_phid)) { + $results[$typeconst] = $type->getTypeName(); + } + } + } + + asort($results); + + return $results; + } + + } diff --git a/src/applications/search/query/PhabricatorSearchDocumentQuery.php b/src/applications/search/query/PhabricatorSearchDocumentQuery.php index b5a6e4cb8d..92a2ec36ba 100644 --- a/src/applications/search/query/PhabricatorSearchDocumentQuery.php +++ b/src/applications/search/query/PhabricatorSearchDocumentQuery.php @@ -3,12 +3,66 @@ final class PhabricatorSearchDocumentQuery extends PhabricatorCursorPagedPolicyAwareQuery { + private $savedQuery; + + public function withSavedQuery(PhabricatorSavedQuery $query) { + $this->savedQuery = $query; + return $this; + } + protected function loadPage() { - return array(); + $phids = $this->loadDocumentPHIDsWithoutPolicyChecks(); + + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($phids) + ->execute(); + + // Retain engine order. + $handles = array_select_keys($handles, $phids); + + return $handles; + } + + protected function willFilterPage(array $handles) { + foreach ($handles as $key => $handle) { + if (!$handle->isComplete()) { + unset($handles[$key]); + continue; + } + if ($handle->getPolicyFiltered()) { + unset($handles[$key]); + continue; + } + } + + return $handles; + } + + public function loadDocumentPHIDsWithoutPolicyChecks() { + $query = id(clone($this->savedQuery)) + ->setParameter('offset', $this->getOffset()) + ->setParameter('limit', $this->getRawResultLimit()); + + $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); + + return $engine->executeSearch($query); } public function getQueryApplicationClass() { return 'PhabricatorApplicationSearch'; } + protected function getPagingValue($result) { + throw new Exception( + pht( + 'This query does not support cursor paging; it must be offset '. + 'paged.')); + } + + protected function nextPage(array $page) { + $this->setOffset($this->getOffset() + count($page)); + return $this; + } + }