diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index e7ae1d0f8d..e015f3a67f 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -1,242 +1,246 @@ shouldAllowPublic()) { return false; } return true; } public function shouldRequireAdmin() { return false; } public function shouldRequireEnabledUser() { return true; } public function shouldAllowPublic() { return false; } public function shouldRequireEmailVerification() { $need_verify = PhabricatorUserEmail::isEmailVerificationRequired(); $need_login = $this->shouldRequireLogin(); return ($need_login && $need_verify); } final public function willBeginExecution() { $request = $this->getRequest(); $user = new PhabricatorUser(); $phusr = $request->getCookie('phusr'); $phsid = $request->getCookie('phsid'); if (strlen($phusr) && $phsid) { $info = queryfx_one( $user->establishConnection('r'), 'SELECT u.* FROM %T u JOIN %T s ON u.phid = s.userPHID AND s.type LIKE %> AND s.sessionKey = %s', $user->getTableName(), 'phabricator_session', 'web-', $phsid); if ($info) { $user->loadFromArray($info); } } $translation = $user->getTranslation(); if ($translation && $translation != PhabricatorEnv::getEnvConfig('translation.provider')) { $translation = newv($translation, array()); PhutilTranslator::getInstance() ->setLanguage($translation->getLanguage()) ->addTranslations($translation->getTranslations()); } $request->setUser($user); if ($user->getIsDisabled() && $this->shouldRequireEnabledUser()) { $disabled_user_controller = new PhabricatorDisabledUserController( $request); return $this->delegateToController($disabled_user_controller); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_CONTROLLER_CHECKREQUEST, array( 'request' => $request, 'controller' => get_class($this), )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $checker_controller = $event->getValue('controller'); if ($checker_controller != get_class($this)) { return $this->delegateToController($checker_controller); } if (PhabricatorEnv::getEnvConfig('darkconsole.enabled')) { if ($user->getConsoleEnabled() || PhabricatorEnv::getEnvConfig('darkconsole.always-on')) { $console = new DarkConsoleCore(); $request->getApplicationConfiguration()->setConsole($console); } } if ($this->shouldRequireLogin() && !$user->getPHID()) { $login_controller = new PhabricatorLoginController($request); return $this->delegateToController($login_controller); } if ($this->shouldRequireEmailVerification()) { $email = $user->loadPrimaryEmail(); if (!$email) { throw new Exception( "No primary email address associated with this account!"); } if (!$email->getIsVerified()) { $verify_controller = new PhabricatorMustVerifyEmailController($request); return $this->delegateToController($verify_controller); } } if ($this->shouldRequireAdmin() && !$user->getIsAdmin()) { return new Aphront403Response(); } } public function buildStandardPageView() { $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->setController($this); return $view; } public function buildStandardPageResponse($view, array $data) { $page = $this->buildStandardPageView(); $page->appendChild($view); $response = new AphrontWebpageResponse(); $response->setContent($page->render()); return $response; } public function getApplicationURI($path = '') { if (!$this->getCurrentApplication()) { throw new Exception("No application!"); } return $this->getCurrentApplication()->getBaseURI().ltrim($path, '/'); } public function buildApplicationPage($view, array $options) { $page = $this->buildStandardPageView(); $application = $this->getCurrentApplication(); if ($application) { $page->setApplicationName($application->getName()); $page->setTitle(idx($options, 'title')); if ($application->getTitleGlyph()) { $page->setGlyph($application->getTitleGlyph()); } } if (!($view instanceof AphrontSideNavFilterView)) { $nav = new AphrontSideNavFilterView(); $nav->appendChild($view); $view = $nav; } if ($application) { $view->setCurrentApplication($application); } $view->setUser($this->getRequest()->getUser()); $view->setFlexNav(true); $view->setShowApplicationMenu(true); $page->appendChild($view); if (idx($options, 'device')) { $page->setDeviceReady(true); $view->appendChild($page->renderFooter()); } $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } public function didProcessRequest($response) { $request = $this->getRequest(); $response->setRequest($request); if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $view = new PhabricatorStandardPageView(); $view->setRequest($request); $view->setController($this); $view->appendChild( '
'. $response->buildResponseString(). '
'); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } else { return id(new AphrontAjaxResponse()) ->setContent(array( 'dialog' => $response->buildResponseString(), )); } } else if ($response instanceof AphrontRedirectResponse) { if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'redirect' => $response->getURI(), )); } } return $response; } protected function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( "Attempting to access handle which wasn't loaded: {$phid}"); } return $this->handles[$phid]; } protected function loadHandles(array $phids) { $phids = array_filter($phids); $this->handles = $this->loadViewerHandles($phids); return $this; } + protected function getLoadedHandles() { + return $this->handles; + } + protected function loadViewerHandles(array $phids) { return id(new PhabricatorObjectHandleData($phids)) ->setViewer($this->getRequest()->getUser()) ->loadHandles(); } protected function renderHandlesForPHIDs(array $phids) { $items = array(); foreach ($phids as $phid) { $items[] = $this->getHandle($phid)->renderLink(); } return implode('
', $items); } } diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 47a0d08599..4a34a328f0 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -1,132 +1,132 @@ questionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $question = PonderQuestionQuery::loadSingle($user, $this->questionID); if (!$question) { return new Aphront404Response(); } $question->attachRelated(); $question->attachVotes($user->getPHID()); $object_phids = array($user->getPHID(), $question->getAuthorPHID()); $answers = $question->getAnswers(); $comments = $question->getComments(); foreach ($comments as $comment) { $object_phids[] = $comment->getAuthorPHID(); } foreach ($answers as $answer) { $object_phids[] = $answer->getAuthorPHID(); $comments = $answer->getComments(); foreach ($comments as $comment) { $object_phids[] = $comment->getAuthorPHID(); } } $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $question->getPHID()); $object_phids = array_merge($object_phids, $subscribers); - $handles = $this->loadViewerHandles($object_phids); $this->loadHandles($object_phids); + $handles = $this->getLoadedHandles(); $detail_panel = new PonderQuestionDetailView(); $detail_panel ->setQuestion($question) ->setUser($user) ->setHandles($handles); $responses_panel = new PonderAnswerListView(); $responses_panel ->setQuestion($question) ->setHandles($handles) ->setUser($user) ->setAnswers($answers); $answer_add_panel = new PonderAddAnswerView(); $answer_add_panel ->setQuestion($question) ->setUser($user) ->setActionURI("/ponder/answer/add/"); $header = id(new PhabricatorHeaderView()) ->setObjectName('Q'.$question->getID()) ->setHeader($question->getTitle()); $actions = $this->buildActionListView($question); $properties = $this->buildPropertyListView($question, $subscribers); $nav = $this->buildSideNavView($question); $nav->appendChild( array( $header, $actions, $properties, $detail_panel, $responses_panel, $answer_add_panel )); $nav->selectFilter(null); return $this->buildApplicationPage( $nav, array( 'device' => true, 'title' => 'Q'.$question->getID().' '.$question->getTitle() )); } private function buildActionListView(PonderQuestion $question) { $viewer = $this->getRequest()->getUser(); $view = new PhabricatorActionListView(); $view->setUser($viewer); $view->setObject($question); return $view; } private function buildPropertyListView( PonderQuestion $question, array $subscribers) { $viewer = $this->getRequest()->getUser(); $view = new PhabricatorPropertyListView(); $view->addProperty( pht('Author'), $this->getHandle($question->getAuthorPHID())->renderLink()); $view->addProperty( pht('Created'), phabricator_datetime($question->getDateCreated(), $viewer)); if ($subscribers) { foreach ($subscribers as $key => $subscriber) { $subscribers[$key] = $this->getHandle($subscriber)->renderLink(); } $subscribers = implode(', ', $subscribers); } $view->addProperty( pht('Subscribers'), nonempty($subscribers, ''.pht('None').'')); return $view; } } diff --git a/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php b/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php index d2dcdefbc0..755b4f4b35 100644 --- a/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php +++ b/src/applications/ponder/search/PhabricatorSearchPonderIndexer.php @@ -1,53 +1,66 @@ setPHID($question->getPHID()); $doc->setDocumentType(PhabricatorPHIDConstants::PHID_TYPE_QUES); $doc->setDocumentTitle($question->getTitle()); $doc->setDocumentCreated($question->getDateCreated()); $doc->setDocumentModified($question->getDateModified()); $doc->addField( PhabricatorSearchField::FIELD_BODY, $question->getContent()); $doc->addRelationship( PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, $question->getAuthorPHID(), PhabricatorPHIDConstants::PHID_TYPE_USER, $question->getDateCreated()); $comments = $question->getComments(); foreach ($comments as $curcomment) { $doc->addField( PhabricatorSearchField::FIELD_COMMENT, $curcomment->getContent() ); } $answers = $question->getAnswers(); foreach ($answers as $curanswer) { if (strlen($curanswer->getContent())) { $doc->addField( PhabricatorSearchField::FIELD_COMMENT, $curanswer->getContent()); } $answer_comments = $curanswer->getComments(); foreach ($answer_comments as $curcomment) { $doc->addField( PhabricatorSearchField::FIELD_COMMENT, $curcomment->getContent() ); } } + $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $question->getPHID()); + $handles = id(new PhabricatorObjectHandleData($subscribers)) + ->loadHandles(); + + foreach ($handles as $phid => $handle) { + $doc->addRelationship( + PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER, + $phid, + $handle->getType(), + $question->getDateModified()); // Bogus timestamp. + } + self::reindexAbstractDocument($doc); } } diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php index e0e61cafaa..4ffe4798aa 100644 --- a/src/applications/search/controller/PhabricatorSearchController.php +++ b/src/applications/search/controller/PhabricatorSearchController.php @@ -1,255 +1,273 @@ key = idx($data, 'key'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->key) { $query = id(new PhabricatorSearchQuery())->loadOneWhere( 'queryKey = %s', $this->key); if (!$query) { return new Aphront404Response(); } } else { $query = new PhabricatorSearchQuery(); 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::jumpPostResponse($query_str); } else { $response = null; } if ($response) { return $response; } else { $query->setQuery($query_str); if ($request->getStr('scope')) { switch ($request->getStr('scope')) { case PhabricatorSearchScope::SCOPE_OPEN_REVISIONS: $query->setParameter('open', 1); $query->setParameter( 'type', PhabricatorPHIDConstants::PHID_TYPE_DREV); break; case PhabricatorSearchScope::SCOPE_OPEN_TASKS: $query->setParameter('open', 1); $query->setParameter( 'type', PhabricatorPHIDConstants::PHID_TYPE_TASK); break; case PhabricatorSearchScope::SCOPE_WIKI: $query->setParameter( 'type', PhabricatorPHIDConstants::PHID_TYPE_WIKI); break; case PhabricatorSearchScope::SCOPE_COMMITS: $query->setParameter( 'type', PhabricatorPHIDConstants::PHID_TYPE_CMIT); 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')); } } $query->save(); return id(new AphrontRedirectResponse()) ->setURI('/search/'.$query->getQueryKey().'/'); } } } $options = array( '' => 'All Documents', ) + PhabricatorSearchAbstractDocument::getSupportedTypes(); $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())); $author_value = mpull($author_value, 'getFullName', 'getPHID'); $owner_value = array_select_keys( $handles, $query->getParameter('owner', array())); $owner_value = mpull($owner_value, 'getFullName', 'getPHID'); + $subscribers_value = array_select_keys( + $handles, + $query->getParameter('subscribers', array())); + $subscribers_value = mpull($subscribers_value, 'getFullName', 'getPHID'); + $project_value = array_select_keys( $handles, $query->getParameter('project', array())); $project_value = mpull($project_value, 'getFullName', 'getPHID'); $search_form = new AphrontFormView(); $search_form ->setUser($user) ->setAction('/search/') ->appendChild( phutil_render_tag( 'input', array( 'type' => 'hidden', 'name' => 'jump', 'value' => 'no', ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Search') ->setName('query') ->setValue($query->getQuery())) ->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 AphrontPanelView(); $search_panel->setHeader('Search Phabricator'); $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 (!$request->getInt('page')) { $jump = PhabricatorPHID::fromObjectName($query->getQuery()); if ($jump) { array_unshift($results, $jump); } } if ($results) { - $loader = new PhabricatorObjectHandleData($results); + $loader = id(new PhabricatorObjectHandleData($results)) + ->setViewer($user); $handles = $loader->loadHandles(); $objects = $loader->loadObjects(); $results = array(); foreach ($handles as $phid => $handle) { - $view = new PhabricatorSearchResultView(); - $view->setHandle($handle); - $view->setQuery($query); - $view->setObject(idx($objects, $phid)); + $view = id(new PhabricatorSearchResultView()) + ->setHandle($handle) + ->setQuery($query) + ->setObject(idx($objects, $phid)); $results[] = $view->render(); } $results = '
'. implode("\n", $results). '
'. $pager->render(). '
'. '
'; } else { $results = '
'. '

No search results.

'. '
'; } } else { $results = null; } return $this->buildStandardPageResponse( array( $search_panel, $results, ), array( 'title' => 'Search Results', )); } } diff --git a/src/applications/search/controller/PhabricatorSearchIndexController.php b/src/applications/search/controller/PhabricatorSearchIndexController.php deleted file mode 100644 index c73928cb65..0000000000 --- a/src/applications/search/controller/PhabricatorSearchIndexController.php +++ /dev/null @@ -1,129 +0,0 @@ -phid = $data['phid']; - } - - public function processRequest() { - - $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); - $document = $engine->reconstructDocument($this->phid); - if (!$document) { - return new Aphront404Response(); - } - - $panels = array(); - - $panel = new AphrontPanelView(); - $panel->setHeader('Abstract Document Index'); - - $props = array( - 'PHID' => phutil_escape_html($document->getPHID()), - 'Title' => phutil_escape_html($document->getDocumentTitle()), - 'Type' => phutil_escape_html($document->getDocumentType()), - ); - $rows = array(); - foreach ($props as $name => $value) { - $rows[] = array($name, $value); - } - $table = new AphrontTableView($rows); - $table->setColumnClasses( - array( - 'header', - '', - )); - $panel->appendChild($table); - $panels[] = $panel; - - - $panel = new AphrontPanelView(); - $panel->setHeader('Document Fields'); - - $fields = $document->getFieldData(); - $rows = array(); - foreach ($fields as $field) { - list($name, $corpus, $aux_phid) = $field; - $rows[] = array( - phutil_escape_html($name), - phutil_escape_html(nonempty($aux_phid, null)), - str_replace("\n", '
', phutil_escape_html($corpus)), - ); - } - - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - 'Field', - 'Aux PHID', - 'Corpus', - )); - $table->setColumnClasses( - array( - '', - '', - 'wide', - )); - $panel->appendChild($table); - $panels[] = $panel; - - - $panel = new AphrontPanelView(); - $panel->setHeader('Document Relationships'); - - $relationships = $document->getRelationshipData(); - - $phids = ipull($relationships, 1); - $handles = $this->loadViewerHandles($phids); - - $rows = array(); - foreach ($relationships as $relationship) { - list($type, $phid, $rtype, $time) = $relationship; - $rows[] = array( - phutil_escape_html($type), - phutil_escape_html($phid), - phutil_escape_html($rtype), - $handles[$phid]->renderLink(), - ); - } - - $table = new AphrontTableView($rows); - $table->setHeaders( - array( - 'Relationship', - 'Related PHID', - 'Related Type', - 'Related Handle', - )); - $table->setColumnClasses( - array( - '', - '', - '', - 'wide', - )); - $panel->appendChild($table); - $panels[] = $panel; - - - return $this->buildStandardPageResponse( - $panels, - array( - 'title' => 'Raw Index', - )); - } - -} diff --git a/src/applications/search/engine/PhabricatorSearchEngineElastic.php b/src/applications/search/engine/PhabricatorSearchEngineElastic.php index ae3ae91515..aa5273312d 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineElastic.php +++ b/src/applications/search/engine/PhabricatorSearchEngineElastic.php @@ -1,236 +1,237 @@ uri = $uri; } public function setTimeout($timeout) { $this->timeout = $timeout; return $this; } public function getTimeout() { return $this->timeout; } public function reindexAbstractDocument( PhabricatorSearchAbstractDocument $doc) { $type = $doc->getDocumentType(); $phid = $doc->getPHID(); $handle = PhabricatorObjectHandleData::loadOneHandle($phid); // URL is not used internally but it can be useful externally. $spec = array( 'title' => $doc->getDocumentTitle(), 'url' => PhabricatorEnv::getProductionURI($handle->getURI()), 'dateCreated' => $doc->getDocumentCreated(), '_timestamp' => $doc->getDocumentModified(), 'field' => array(), 'relationship' => array(), ); foreach ($doc->getFieldData() as $field) { $spec['field'][] = array_combine(array('type', 'corpus', 'aux'), $field); } foreach ($doc->getRelationshipData() as $relationship) { list($rtype, $to_phid, $to_type, $time) = $relationship; $spec['relationship'][$rtype][] = array( 'phid' => $to_phid, 'phidType' => $to_type, 'when' => $time, ); } $this->executeRequest( "/phabricator/{$type}/{$phid}/", $spec, $is_write = true); } public function reconstructDocument($phid) { $type = phid_get_type($phid); $response = $this->executeRequest("/phabricator/{$type}/{$phid}", array()); if (empty($response['exists'])) { return null; } $hit = $response['_source']; $doc = new PhabricatorSearchAbstractDocument(); $doc->setPHID($phid); $doc->setDocumentType($response['_type']); $doc->setDocumentTitle($hit['title']); $doc->setDocumentCreated($hit['dateCreated']); $doc->setDocumentModified($hit['_timestamp']); foreach ($hit['field'] as $fdef) { $doc->addField($fdef['type'], $fdef['corpus'], $fdef['aux']); } foreach ($hit['relationship'] as $rtype => $rships) { foreach ($rships as $rship) { $doc->addRelationship( $rtype, $rship['phid'], $rship['phidType'], $rship['when']); } } return $doc; } private function buildSpec(PhabricatorSearchQuery $query) { $spec = array(); $filter = array(); if ($query->getQuery()) { $spec[] = array( 'field' => array( 'field.corpus' => $query->getQuery(), ), ); } $exclude = $query->getParameter('exclude'); if ($exclude) { $filter[] = array( 'not' => array( 'ids' => array( 'values' => array($exclude), ), ), ); } $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, ); foreach ($rel_mapping as $name => $field) { $param = $query->getParameter($name); if (is_array($param)) { $should = array(); foreach ($param as $val) { $should[] = array( 'text' => array( "relationship.{$field}.phid" => array( 'query' => $val, 'type' => 'phrase', ), ), ); } // We couldn't solve it by minimum_number_should_match because it can // match multiple owners without matching author. $spec[] = array('bool' => array('should' => $should)); } else if ($param) { $filter[] = array( 'exists' => array( 'field' => "relationship.{$field}.phid", ), ); } } if ($spec) { $spec = array('query' => array('bool' => array('must' => $spec))); } if ($filter) { $filter = array('filter' => array('and' => $filter)); if ($spec) { $spec = array( 'query' => array( 'filtered' => $spec + $filter, ), ); } else { $spec = $filter; } } if (!$query->getQuery()) { $spec['sort'] = array( array('dateCreated' => 'desc'), ); } $spec['from'] = (int)$query->getParameter('offset', 0); $spec['size'] = (int)$query->getParameter('limit', 25); return $spec; } public function executeSearch(PhabricatorSearchQuery $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'; } 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->getQuery()) { throw $ex; } $query = clone $query; $query->setQuery(addcslashes($query->getQuery(), '+-&|!(){}[]^"~*?:\\')); $response = $this->executeRequest($uri, $this->buildSpec($query)); } $phids = ipull($response['hits']['hits'], '_id'); return $phids; } private function executeRequest($path, array $data, $is_write = false) { $uri = new PhutilURI($this->uri); $data = json_encode($data); $uri->setPath($path); $future = new HTTPSFuture($uri, $data); if ($is_write) { $future->setMethod('PUT'); } if ($this->getTimeout()) { $future->setTimeout($this->getTimeout()); } list($body) = $future->resolvex(); if ($is_write) { return null; } $body = json_decode($body, true); if (!is_array($body)) { throw new Exception("elasticsearch server returned invalid JSON!"); } return $body; } } diff --git a/src/applications/search/engine/PhabricatorSearchEngineMySQL.php b/src/applications/search/engine/PhabricatorSearchEngineMySQL.php index 28e90ab1d9..cdc95be54e 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineMySQL.php +++ b/src/applications/search/engine/PhabricatorSearchEngineMySQL.php @@ -1,314 +1,320 @@ getPHID(); if (!$phid) { throw new Exception("Document has no PHID!"); } $store = new PhabricatorSearchDocument(); $store->setPHID($doc->getPHID()); $store->setDocumentType($doc->getDocumentType()); $store->setDocumentTitle($doc->getDocumentTitle()); $store->setDocumentCreated($doc->getDocumentCreated()); $store->setDocumentModified($doc->getDocumentModified()); $store->replace(); $conn_w = $store->establishConnection('w'); $field_dao = new PhabricatorSearchDocumentField(); queryfx( $conn_w, 'DELETE FROM %T WHERE phid = %s', $field_dao->getTableName(), $phid); foreach ($doc->getFieldData() as $field) { list($ftype, $corpus, $aux_phid) = $field; queryfx( $conn_w, 'INSERT INTO %T (phid, phidType, field, auxPHID, corpus) '. ' VALUES (%s, %s, %s, %ns, %s)', $field_dao->getTableName(), $phid, $doc->getDocumentType(), $ftype, $aux_phid, $corpus); } $sql = array(); foreach ($doc->getRelationshipData() as $relationship) { list($rtype, $to_phid, $to_type, $time) = $relationship; $sql[] = qsprintf( $conn_w, '(%s, %s, %s, %s, %d)', $phid, $to_phid, $rtype, $to_type, $time); } $rship_dao = new PhabricatorSearchDocumentRelationship(); queryfx( $conn_w, 'DELETE FROM %T WHERE phid = %s', $rship_dao->getTableName(), $phid); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T'. ' (phid, relatedPHID, relation, relatedType, relatedTime) '. ' VALUES %Q', $rship_dao->getTableName(), implode(', ', $sql)); } } /** * Rebuild the PhabricatorSearchAbstractDocument that was used to index * an object out of the index itself. This is primarily useful for debugging, * as it allows you to inspect the search index representation of a * document. * * @param phid PHID of a document which exists in the search index. * @return null|PhabricatorSearchAbstractDocument Abstract document object * which corresponds to the original abstract document used to * build the document index. */ public function reconstructDocument($phid) { $dao_doc = new PhabricatorSearchDocument(); $dao_field = new PhabricatorSearchDocumentField(); $dao_relationship = new PhabricatorSearchDocumentRelationship(); $t_doc = $dao_doc->getTableName(); $t_field = $dao_field->getTableName(); $t_relationship = $dao_relationship->getTableName(); $doc = queryfx_one( $dao_doc->establishConnection('r'), 'SELECT * FROM %T WHERE phid = %s', $t_doc, $phid); if (!$doc) { return null; } $fields = queryfx_all( $dao_field->establishConnection('r'), 'SELECT * FROM %T WHERE phid = %s', $t_field, $phid); $relationships = queryfx_all( $dao_relationship->establishConnection('r'), 'SELECT * FROM %T WHERE phid = %s', $t_relationship, $phid); $adoc = id(new PhabricatorSearchAbstractDocument()) ->setPHID($phid) ->setDocumentType($doc['documentType']) ->setDocumentTitle($doc['documentTitle']) ->setDocumentCreated($doc['documentCreated']) ->setDocumentModified($doc['documentModified']); foreach ($fields as $field) { $adoc->addField( $field['field'], $field['corpus'], $field['auxPHID']); } foreach ($relationships as $relationship) { $adoc->addRelationship( $relationship['relation'], $relationship['relatedPHID'], $relationship['relatedType'], $relationship['relatedTime']); } return $adoc; } public function executeSearch(PhabricatorSearchQuery $query) { $where = array(); $join = array(); $order = 'ORDER BY documentCreated DESC'; $dao_doc = new PhabricatorSearchDocument(); $dao_field = new PhabricatorSearchDocumentField(); $t_doc = $dao_doc->getTableName(); $t_field = $dao_field->getTableName(); $conn_r = $dao_doc->establishConnection('r'); $q = $query->getQuery(); if (strlen($q)) { $join[] = qsprintf( $conn_r, "{$t_field} field ON field.phid = document.phid"); $where[] = qsprintf( $conn_r, 'MATCH(corpus) AGAINST (%s IN BOOLEAN MODE)', $q); // When searching for a string, promote user listings above other // listings. $order = qsprintf( $conn_r, 'ORDER BY IF(documentType = %s, 0, 1) ASC, MAX(MATCH(corpus) AGAINST (%s)) DESC', 'USER', $q); $field = $query->getParameter('field'); if ($field/* && $field != AdjutantQuery::FIELD_ALL*/) { $where[] = qsprintf( $conn_r, 'field.field = %s', $field); } } $exclude = $query->getParameter('exclude'); if ($exclude) { $where[] = qsprintf($conn_r, 'document.phid != %s', $exclude); } if ($query->getParameter('type')) { 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')); } $where[] = qsprintf( $conn_r, 'document.documentType = %s', $query->getParameter('type')); } $join[] = $this->joinRelationship( $conn_r, $query, 'author', PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR); $join[] = $this->joinRelationship( $conn_r, $query, 'open', PhabricatorSearchRelationship::RELATIONSHIP_OPEN); $join[] = $this->joinRelationship( $conn_r, $query, 'owner', PhabricatorSearchRelationship::RELATIONSHIP_OWNER); + $join[] = $this->joinRelationship( + $conn_r, + $query, + 'subscribers', + PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER); + $join[] = $this->joinRelationship( $conn_r, $query, 'project', PhabricatorSearchRelationship::RELATIONSHIP_PROJECT); $join[] = $this->joinRelationship( $conn_r, $query, 'repository', PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY); $join = array_filter($join); foreach ($join as $key => $clause) { $join[$key] = ' JOIN '.$clause; } $join = implode(' ', $join); if ($where) { $where = 'WHERE '.implode(' AND ', $where); } else { $where = ''; } $offset = (int)$query->getParameter('offset', 0); $limit = (int)$query->getParameter('limit', 25); $hits = queryfx_all( $conn_r, 'SELECT document.phid FROM %T document %Q %Q GROUP BY document.phid %Q LIMIT %d, %d', $t_doc, $join, $where, $order, $offset, $limit); return ipull($hits, 'phid'); } protected function joinRelationship( AphrontDatabaseConnection $conn, PhabricatorSearchQuery $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; } $sql = qsprintf( $conn, '%T AS %C ON %C.phid = document.phid AND %C.relation = %s', id(new PhabricatorSearchDocumentRelationship())->getTableName(), $field, $field, $field, $type); if (!$is_existence) { $sql .= qsprintf( $conn, ' AND %C.relatedPHID in (%Ls)', $field, $phids); } return $sql; } } diff --git a/src/applications/search/view/PhabricatorSearchResultView.php b/src/applications/search/view/PhabricatorSearchResultView.php index c553e41cee..555f602d40 100644 --- a/src/applications/search/view/PhabricatorSearchResultView.php +++ b/src/applications/search/view/PhabricatorSearchResultView.php @@ -1,124 +1,118 @@ handle = $handle; return $this; } public function setQuery(PhabricatorSearchQuery $query) { $this->query = $query; return $this; } public function setObject($object) { $this->object = $object; return $this; } public function render() { $handle = $this->handle; + if (!$handle->isComplete()) { + return; + } $type_name = nonempty($handle->getTypeName(), 'Document'); require_celerity_resource('phabricator-search-results-css'); $link = phutil_render_tag( 'a', array( 'href' => $handle->getURI(), ), PhabricatorEnv::getProductionURI($handle->getURI())); $img = $handle->getImageURI(); if ($img) { $img = phutil_render_tag( 'div', array( 'class' => 'result-image', 'style' => "background-image: url('{$img}');", ), ''); } switch ($handle->getType()) { case PhabricatorPHIDConstants::PHID_TYPE_CMIT: $object_name = $handle->getName(); if ($this->object) { $data = $this->object->getCommitData(); $summary = $data->getSummary(); if (strlen($summary)) { $object_name = $handle->getName().': '.$data->getSummary(); } } break; default: $object_name = $handle->getFullName(); break; } - $index_link = phutil_render_tag( - 'a', - array( - 'href' => '/search/index/'.$handle->getPHID().'/', - 'style' => 'float: right', - ), - 'Examine Index'); - return '
'. $img. '
'. - $index_link. phutil_render_tag( 'a', array( 'class' => 'result-name', 'href' => $handle->getURI(), ), $this->emboldenQuery($object_name)). '
'.$type_name.' · '.$link.'
'. '
'. '
'. '
'; } private function emboldenQuery($str) { if (!$this->query) { return phutil_escape_html($str); } $query = $this->query->getQuery(); $quoted_regexp = '/"([^"]*)"/'; $matches = array(1 => array()); preg_match_all($quoted_regexp, $query, $matches); $quoted_queries = $matches[1]; $query = preg_replace($quoted_regexp, '', $query); $query = preg_split('/\s+[+|]?/', $query); $query = array_filter($query); $query = array_merge($query, $quoted_queries); $str = phutil_escape_html($str); foreach ($query as $word) { $word = phutil_escape_html($word); $word = preg_quote($word, '/'); $word = preg_replace('/\\\\\*$/', '\w*', $word); $str = preg_replace( '/(?:^|\b)('.$word.')(?:\b|$)/i', '\1', $str); } return $str; } }