Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / search / engine / PhabricatorApplicationSearchEngine.php
blob95f1ffafa3482b75e9cc58a98328c0d6c6e37d6c
1 <?php
3 /**
4 * Represents an abstract search engine for an application. It supports
5 * creating and storing saved queries.
7 * @task construct Constructing Engines
8 * @task app Applications
9 * @task builtin Builtin Queries
10 * @task uri Query URIs
11 * @task dates Date Filters
12 * @task order Result Ordering
13 * @task read Reading Utilities
14 * @task exec Paging and Executing Queries
15 * @task render Rendering Results
17 abstract class PhabricatorApplicationSearchEngine extends Phobject {
19 private $application;
20 private $viewer;
21 private $errors = array();
22 private $request;
23 private $context;
24 private $controller;
25 private $namedQueries;
26 private $navigationItems = array();
28 const CONTEXT_LIST = 'list';
29 const CONTEXT_PANEL = 'panel';
31 const BUCKET_NONE = 'none';
33 public function setController(PhabricatorController $controller) {
34 $this->controller = $controller;
35 return $this;
38 public function getController() {
39 return $this->controller;
42 public function buildResponse() {
43 $controller = $this->getController();
44 $request = $controller->getRequest();
46 $search = id(new PhabricatorApplicationSearchController())
47 ->setQueryKey($request->getURIData('queryKey'))
48 ->setSearchEngine($this);
50 return $controller->delegateToController($search);
53 public function newResultObject() {
54 // We may be able to get this automatically if newQuery() is implemented.
55 $query = $this->newQuery();
56 if ($query) {
57 $object = $query->newResultObject();
58 if ($object) {
59 return $object;
63 return null;
66 public function newQuery() {
67 return null;
70 public function setViewer(PhabricatorUser $viewer) {
71 $this->viewer = $viewer;
72 return $this;
75 protected function requireViewer() {
76 if (!$this->viewer) {
77 throw new PhutilInvalidStateException('setViewer');
79 return $this->viewer;
82 public function setContext($context) {
83 $this->context = $context;
84 return $this;
87 public function isPanelContext() {
88 return ($this->context == self::CONTEXT_PANEL);
91 public function setNavigationItems(array $navigation_items) {
92 assert_instances_of($navigation_items, 'PHUIListItemView');
93 $this->navigationItems = $navigation_items;
94 return $this;
97 public function getNavigationItems() {
98 return $this->navigationItems;
101 public function canUseInPanelContext() {
102 return true;
105 public function saveQuery(PhabricatorSavedQuery $query) {
106 if ($query->getID()) {
107 throw new Exception(
108 pht(
109 'Query (with ID "%s") has already been saved. Queries are '.
110 'immutable once saved.',
111 $query->getID()));
114 $query->setEngineClassName(get_class($this));
116 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
117 try {
118 $query->save();
119 } catch (AphrontDuplicateKeyQueryException $ex) {
120 // Ignore, this is just a repeated search.
122 unset($unguarded);
126 * Create a saved query object from the request.
128 * @param AphrontRequest The search request.
129 * @return PhabricatorSavedQuery
131 public function buildSavedQueryFromRequest(AphrontRequest $request) {
132 $fields = $this->buildSearchFields();
133 $viewer = $this->requireViewer();
135 $saved = new PhabricatorSavedQuery();
136 foreach ($fields as $field) {
137 $field->setViewer($viewer);
139 $value = $field->readValueFromRequest($request);
140 $saved->setParameter($field->getKey(), $value);
143 return $saved;
147 * Executes the saved query.
149 * @param PhabricatorSavedQuery The saved query to operate on.
150 * @return PhabricatorQuery The result of the query.
152 public function buildQueryFromSavedQuery(PhabricatorSavedQuery $original) {
153 $saved = clone $original;
154 $this->willUseSavedQuery($saved);
156 $fields = $this->buildSearchFields();
157 $viewer = $this->requireViewer();
159 $map = array();
160 foreach ($fields as $field) {
161 $field->setViewer($viewer);
162 $field->readValueFromSavedQuery($saved);
163 $value = $field->getValueForQuery($field->getValue());
164 $map[$field->getKey()] = $value;
167 $original->attachParameterMap($map);
168 $query = $this->buildQueryFromParameters($map);
170 $object = $this->newResultObject();
171 if (!$object) {
172 return $query;
175 $extensions = $this->getEngineExtensions();
176 foreach ($extensions as $extension) {
177 $extension->applyConstraintsToQuery($object, $query, $saved, $map);
180 $order = $saved->getParameter('order');
181 $builtin = $query->getBuiltinOrderAliasMap();
182 if (strlen($order) && isset($builtin[$order])) {
183 $query->setOrder($order);
184 } else {
185 // If the order is invalid or not available, we choose the first
186 // builtin order. This isn't always the default order for the query,
187 // but is the first value in the "Order" dropdown, and makes the query
188 // behavior more consistent with the UI. In queries where the two
189 // orders differ, this order is the preferred order for humans.
190 $query->setOrder(head_key($builtin));
193 return $query;
197 * Hook for subclasses to adjust saved queries prior to use.
199 * If an application changes how queries are saved, it can implement this
200 * hook to keep old queries working the way users expect, by reading,
201 * adjusting, and overwriting parameters.
203 * @param PhabricatorSavedQuery Saved query which will be executed.
204 * @return void
206 protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
207 return;
210 protected function buildQueryFromParameters(array $parameters) {
211 throw new PhutilMethodNotImplementedException();
215 * Builds the search form using the request.
217 * @param AphrontFormView Form to populate.
218 * @param PhabricatorSavedQuery The query from which to build the form.
219 * @return void
221 public function buildSearchForm(
222 AphrontFormView $form,
223 PhabricatorSavedQuery $saved) {
225 $saved = clone $saved;
226 $this->willUseSavedQuery($saved);
228 $fields = $this->buildSearchFields();
229 $fields = $this->adjustFieldsForDisplay($fields);
230 $viewer = $this->requireViewer();
232 foreach ($fields as $field) {
233 $field->setViewer($viewer);
234 $field->readValueFromSavedQuery($saved);
237 foreach ($fields as $field) {
238 foreach ($field->getErrors() as $error) {
239 $this->addError(last($error));
243 foreach ($fields as $field) {
244 $field->appendToForm($form);
248 protected function buildSearchFields() {
249 $fields = array();
251 foreach ($this->buildCustomSearchFields() as $field) {
252 $fields[] = $field;
255 $object = $this->newResultObject();
256 if ($object) {
257 $extensions = $this->getEngineExtensions();
258 foreach ($extensions as $extension) {
259 $extension_fields = $extension->getSearchFields($object);
260 foreach ($extension_fields as $extension_field) {
261 $fields[] = $extension_field;
266 $query = $this->newQuery();
267 if ($query && $this->shouldShowOrderField()) {
268 $orders = $query->getBuiltinOrders();
269 $orders = ipull($orders, 'name');
271 $fields[] = id(new PhabricatorSearchOrderField())
272 ->setLabel(pht('Order By'))
273 ->setKey('order')
274 ->setOrderAliases($query->getBuiltinOrderAliasMap())
275 ->setOptions($orders);
278 $buckets = $this->newResultBuckets();
279 if ($query && $buckets) {
280 $bucket_options = array(
281 self::BUCKET_NONE => pht('No Bucketing'),
282 ) + mpull($buckets, 'getResultBucketName');
284 $fields[] = id(new PhabricatorSearchSelectField())
285 ->setLabel(pht('Bucket'))
286 ->setKey('bucket')
287 ->setOptions($bucket_options);
290 $field_map = array();
291 foreach ($fields as $field) {
292 $key = $field->getKey();
293 if (isset($field_map[$key])) {
294 throw new Exception(
295 pht(
296 'Two fields in this SearchEngine use the same key ("%s"), but '.
297 'each field must use a unique key.',
298 $key));
300 $field_map[$key] = $field;
303 return $field_map;
306 protected function shouldShowOrderField() {
307 return true;
310 private function adjustFieldsForDisplay(array $field_map) {
311 $order = $this->getDefaultFieldOrder();
313 $head_keys = array();
314 $tail_keys = array();
315 $seen_tail = false;
316 foreach ($order as $order_key) {
317 if ($order_key === '...') {
318 $seen_tail = true;
319 continue;
322 if (!$seen_tail) {
323 $head_keys[] = $order_key;
324 } else {
325 $tail_keys[] = $order_key;
329 $head = array_select_keys($field_map, $head_keys);
330 $body = array_diff_key($field_map, array_fuse($tail_keys));
331 $tail = array_select_keys($field_map, $tail_keys);
333 $result = $head + $body + $tail;
335 // Force the fulltext "query" field to the top unconditionally.
336 $result = array_select_keys($result, array('query')) + $result;
338 foreach ($this->getHiddenFields() as $hidden_key) {
339 unset($result[$hidden_key]);
342 return $result;
345 protected function buildCustomSearchFields() {
346 throw new PhutilMethodNotImplementedException();
351 * Define the default display order for fields by returning a list of
352 * field keys.
354 * You can use the special key `...` to mean "all unspecified fields go
355 * here". This lets you easily put important fields at the top of the form,
356 * standard fields in the middle of the form, and less important fields at
357 * the bottom.
359 * For example, you might return a list like this:
361 * return array(
362 * 'authorPHIDs',
363 * 'reviewerPHIDs',
364 * '...',
365 * 'createdAfter',
366 * 'createdBefore',
367 * );
369 * Any unspecified fields (including custom fields and fields added
370 * automatically by infrastructure) will be put in the middle.
372 * @return list<string> Default ordering for field keys.
374 protected function getDefaultFieldOrder() {
375 return array();
379 * Return a list of field keys which should be hidden from the viewer.
381 * @return list<string> Fields to hide.
383 protected function getHiddenFields() {
384 return array();
387 public function getErrors() {
388 return $this->errors;
391 public function addError($error) {
392 $this->errors[] = $error;
393 return $this;
397 * Return an application URI corresponding to the results page of a query.
398 * Normally, this is something like `/application/query/QUERYKEY/`.
400 * @param string The query key to build a URI for.
401 * @return string URI where the query can be executed.
402 * @task uri
404 public function getQueryResultsPageURI($query_key) {
405 return $this->getURI('query/'.$query_key.'/');
410 * Return an application URI for query management. This is used when, e.g.,
411 * a query deletion operation is cancelled.
413 * @return string URI where queries can be managed.
414 * @task uri
416 public function getQueryManagementURI() {
417 return $this->getURI('query/edit/');
420 public function getQueryBaseURI() {
421 return $this->getURI('');
424 public function getExportURI($query_key) {
425 return $this->getURI('query/'.$query_key.'/export/');
428 public function getCustomizeURI($query_key, $object_phid, $context_phid) {
429 $params = array(
430 'search.objectPHID' => $object_phid,
431 'search.contextPHID' => $context_phid,
434 $uri = $this->getURI('query/'.$query_key.'/customize/');
435 $uri = new PhutilURI($uri, $params);
437 return phutil_string_cast($uri);
443 * Return the URI to a path within the application. Used to construct default
444 * URIs for management and results.
446 * @return string URI to path.
447 * @task uri
449 abstract protected function getURI($path);
453 * Return a human readable description of the type of objects this query
454 * searches for.
456 * For example, "Tasks" or "Commits".
458 * @return string Human-readable description of what this engine is used to
459 * find.
461 abstract public function getResultTypeDescription();
464 public function newSavedQuery() {
465 return id(new PhabricatorSavedQuery())
466 ->setEngineClassName(get_class($this));
469 public function addNavigationItems(PHUIListView $menu) {
470 $viewer = $this->requireViewer();
472 $menu->newLabel(pht('Queries'));
474 $named_queries = $this->loadEnabledNamedQueries();
476 foreach ($named_queries as $query) {
477 $key = $query->getQueryKey();
478 $uri = $this->getQueryResultsPageURI($key);
479 $menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
482 if ($viewer->isLoggedIn()) {
483 $manage_uri = $this->getQueryManagementURI();
484 $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
487 $menu->newLabel(pht('Search'));
488 $advanced_uri = $this->getQueryResultsPageURI('advanced');
489 $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
491 foreach ($this->navigationItems as $extra_item) {
492 $menu->addMenuItem($extra_item);
495 return $this;
498 public function loadAllNamedQueries() {
499 $viewer = $this->requireViewer();
500 $builtin = $this->getBuiltinQueries();
502 if ($this->namedQueries === null) {
503 $named_queries = id(new PhabricatorNamedQueryQuery())
504 ->setViewer($viewer)
505 ->withEngineClassNames(array(get_class($this)))
506 ->withUserPHIDs(
507 array(
508 $viewer->getPHID(),
509 PhabricatorNamedQuery::SCOPE_GLOBAL,
511 ->execute();
512 $named_queries = mpull($named_queries, null, 'getQueryKey');
514 $builtin = mpull($builtin, null, 'getQueryKey');
516 foreach ($named_queries as $key => $named_query) {
517 if ($named_query->getIsBuiltin()) {
518 if (isset($builtin[$key])) {
519 $named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
520 unset($builtin[$key]);
521 } else {
522 unset($named_queries[$key]);
526 unset($builtin[$key]);
529 $named_queries = msortv($named_queries, 'getNamedQuerySortVector');
530 $this->namedQueries = $named_queries;
533 return $this->namedQueries + $builtin;
536 public function loadEnabledNamedQueries() {
537 $named_queries = $this->loadAllNamedQueries();
538 foreach ($named_queries as $key => $named_query) {
539 if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
540 unset($named_queries[$key]);
543 return $named_queries;
546 public function getDefaultQueryKey() {
547 $viewer = $this->requireViewer();
549 $configs = id(new PhabricatorNamedQueryConfigQuery())
550 ->setViewer($viewer)
551 ->withEngineClassNames(array(get_class($this)))
552 ->withScopePHIDs(
553 array(
554 $viewer->getPHID(),
555 PhabricatorNamedQueryConfig::SCOPE_GLOBAL,
557 ->execute();
558 $configs = msortv($configs, 'getStrengthSortVector');
560 $key_pinned = PhabricatorNamedQueryConfig::PROPERTY_PINNED;
561 $map = $this->loadEnabledNamedQueries();
562 foreach ($configs as $config) {
563 $pinned = $config->getConfigProperty($key_pinned);
564 if (!isset($map[$pinned])) {
565 continue;
568 return $pinned;
571 return head_key($map);
574 protected function setQueryProjects(
575 PhabricatorCursorPagedPolicyAwareQuery $query,
576 PhabricatorSavedQuery $saved) {
578 $datasource = id(new PhabricatorProjectLogicalDatasource())
579 ->setViewer($this->requireViewer());
581 $projects = $saved->getParameter('projects', array());
582 $constraints = $datasource->evaluateTokens($projects);
584 if ($constraints) {
585 $query->withEdgeLogicConstraints(
586 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
587 $constraints);
590 return $this;
594 /* -( Applications )------------------------------------------------------- */
597 protected function getApplicationURI($path = '') {
598 return $this->getApplication()->getApplicationURI($path);
601 protected function getApplication() {
602 if (!$this->application) {
603 $class = $this->getApplicationClassName();
605 $this->application = id(new PhabricatorApplicationQuery())
606 ->setViewer($this->requireViewer())
607 ->withClasses(array($class))
608 ->withInstalled(true)
609 ->executeOne();
611 if (!$this->application) {
612 throw new Exception(
613 pht(
614 'Application "%s" is not installed!',
615 $class));
619 return $this->application;
622 abstract public function getApplicationClassName();
625 /* -( Constructing Engines )----------------------------------------------- */
629 * Load all available application search engines.
631 * @return list<PhabricatorApplicationSearchEngine> All available engines.
632 * @task construct
634 public static function getAllEngines() {
635 return id(new PhutilClassMapQuery())
636 ->setAncestorClass(__CLASS__)
637 ->execute();
642 * Get an engine by class name, if it exists.
644 * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
645 * not exist.
646 * @task construct
648 public static function getEngineByClassName($class_name) {
649 return idx(self::getAllEngines(), $class_name);
653 /* -( Builtin Queries )---------------------------------------------------- */
657 * @task builtin
659 public function getBuiltinQueries() {
660 $names = $this->getBuiltinQueryNames();
662 $queries = array();
663 $sequence = 0;
664 foreach ($names as $key => $name) {
665 $queries[$key] = id(new PhabricatorNamedQuery())
666 ->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL)
667 ->setEngineClassName(get_class($this))
668 ->setQueryName($name)
669 ->setQueryKey($key)
670 ->setSequence((1 << 24) + $sequence++)
671 ->setIsBuiltin(true);
674 return $queries;
679 * @task builtin
681 public function getBuiltinQuery($query_key) {
682 if (!$this->isBuiltinQuery($query_key)) {
683 throw new Exception(pht("'%s' is not a builtin!", $query_key));
685 return idx($this->getBuiltinQueries(), $query_key);
690 * @task builtin
692 protected function getBuiltinQueryNames() {
693 return array();
698 * @task builtin
700 public function isBuiltinQuery($query_key) {
701 $builtins = $this->getBuiltinQueries();
702 return isset($builtins[$query_key]);
707 * @task builtin
709 public function buildSavedQueryFromBuiltin($query_key) {
710 throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
714 /* -( Reading Utilities )--------------------------------------------------- */
718 * Read a list of user PHIDs from a request in a flexible way. This method
719 * supports either of these forms:
721 * users[]=alincoln&users[]=htaft
722 * users=alincoln,htaft
724 * Additionally, users can be specified either by PHID or by name.
726 * The main goal of this flexibility is to allow external programs to generate
727 * links to pages (like "alincoln's open revisions") without needing to make
728 * API calls.
730 * @param AphrontRequest Request to read user PHIDs from.
731 * @param string Key to read in the request.
732 * @param list<const> Other permitted PHID types.
733 * @return list<phid> List of user PHIDs and selector functions.
734 * @task read
736 protected function readUsersFromRequest(
737 AphrontRequest $request,
738 $key,
739 array $allow_types = array()) {
741 $list = $this->readListFromRequest($request, $key);
743 $phids = array();
744 $names = array();
745 $allow_types = array_fuse($allow_types);
746 $user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
747 foreach ($list as $item) {
748 $type = phid_get_type($item);
749 if ($type == $user_type) {
750 $phids[] = $item;
751 } else if (isset($allow_types[$type])) {
752 $phids[] = $item;
753 } else {
754 if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
755 // If this is a function, pass it through unchanged; we'll evaluate
756 // it later.
757 $phids[] = $item;
758 } else {
759 $names[] = $item;
764 if ($names) {
765 $users = id(new PhabricatorPeopleQuery())
766 ->setViewer($this->requireViewer())
767 ->withUsernames($names)
768 ->execute();
769 foreach ($users as $user) {
770 $phids[] = $user->getPHID();
772 $phids = array_unique($phids);
775 return $phids;
780 * Read a list of subscribers from a request in a flexible way.
782 * @param AphrontRequest Request to read PHIDs from.
783 * @param string Key to read in the request.
784 * @return list<phid> List of object PHIDs.
785 * @task read
787 protected function readSubscribersFromRequest(
788 AphrontRequest $request,
789 $key) {
790 return $this->readUsersFromRequest(
791 $request,
792 $key,
793 array(
794 PhabricatorProjectProjectPHIDType::TYPECONST,
800 * Read a list of generic PHIDs from a request in a flexible way. Like
801 * @{method:readUsersFromRequest}, this method supports either array or
802 * comma-delimited forms. Objects can be specified either by PHID or by
803 * object name.
805 * @param AphrontRequest Request to read PHIDs from.
806 * @param string Key to read in the request.
807 * @param list<const> Optional, list of permitted PHID types.
808 * @return list<phid> List of object PHIDs.
810 * @task read
812 protected function readPHIDsFromRequest(
813 AphrontRequest $request,
814 $key,
815 array $allow_types = array()) {
817 $list = $this->readListFromRequest($request, $key);
819 $objects = id(new PhabricatorObjectQuery())
820 ->setViewer($this->requireViewer())
821 ->withNames($list)
822 ->execute();
823 $list = mpull($objects, 'getPHID');
825 if (!$list) {
826 return array();
829 // If only certain PHID types are allowed, filter out all the others.
830 if ($allow_types) {
831 $allow_types = array_fuse($allow_types);
832 foreach ($list as $key => $phid) {
833 if (empty($allow_types[phid_get_type($phid)])) {
834 unset($list[$key]);
839 return $list;
844 * Read a list of items from the request, in either array format or string
845 * format:
847 * list[]=item1&list[]=item2
848 * list=item1,item2
850 * This provides flexibility when constructing URIs, especially from external
851 * sources.
853 * @param AphrontRequest Request to read strings from.
854 * @param string Key to read in the request.
855 * @return list<string> List of values.
857 protected function readListFromRequest(
858 AphrontRequest $request,
859 $key) {
860 $list = $request->getArr($key, null);
861 if ($list === null) {
862 $list = $request->getStrList($key);
865 if (!$list) {
866 return array();
869 return $list;
872 protected function readBoolFromRequest(
873 AphrontRequest $request,
874 $key) {
875 if (!strlen($request->getStr($key))) {
876 return null;
878 return $request->getBool($key);
882 protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
883 $value = $query->getParameter($key);
884 if ($value === null) {
885 return $value;
887 return $value ? 'true' : 'false';
891 /* -( Dates )-------------------------------------------------------------- */
895 * @task dates
897 protected function parseDateTime($date_time) {
898 if (!strlen($date_time)) {
899 return null;
902 return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
907 * @task dates
909 protected function buildDateRange(
910 AphrontFormView $form,
911 PhabricatorSavedQuery $saved_query,
912 $start_key,
913 $start_name,
914 $end_key,
915 $end_name) {
917 $start_str = $saved_query->getParameter($start_key);
918 $start = null;
919 if (strlen($start_str)) {
920 $start = $this->parseDateTime($start_str);
921 if (!$start) {
922 $this->addError(
923 pht(
924 '"%s" date can not be parsed.',
925 $start_name));
930 $end_str = $saved_query->getParameter($end_key);
931 $end = null;
932 if (strlen($end_str)) {
933 $end = $this->parseDateTime($end_str);
934 if (!$end) {
935 $this->addError(
936 pht(
937 '"%s" date can not be parsed.',
938 $end_name));
942 if ($start && $end && ($start >= $end)) {
943 $this->addError(
944 pht(
945 '"%s" must be a date before "%s".',
946 $start_name,
947 $end_name));
950 $form
951 ->appendChild(
952 id(new PHUIFormFreeformDateControl())
953 ->setName($start_key)
954 ->setLabel($start_name)
955 ->setValue($start_str))
956 ->appendChild(
957 id(new AphrontFormTextControl())
958 ->setName($end_key)
959 ->setLabel($end_name)
960 ->setValue($end_str));
964 /* -( Paging and Executing Queries )--------------------------------------- */
967 protected function newResultBuckets() {
968 return array();
971 public function getResultBucket(PhabricatorSavedQuery $saved) {
972 $key = $saved->getParameter('bucket');
973 if ($key == self::BUCKET_NONE) {
974 return null;
977 $buckets = $this->newResultBuckets();
978 return idx($buckets, $key);
982 public function getPageSize(PhabricatorSavedQuery $saved) {
983 $bucket = $this->getResultBucket($saved);
985 $limit = (int)$saved->getParameter('limit');
987 if ($limit > 0) {
988 if ($bucket) {
989 $bucket->setPageSize($limit);
991 return $limit;
994 if ($bucket) {
995 return $bucket->getPageSize();
998 return 100;
1002 public function shouldUseOffsetPaging() {
1003 return false;
1007 public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
1008 if ($this->shouldUseOffsetPaging()) {
1009 $pager = new PHUIPagerView();
1010 } else {
1011 $pager = new AphrontCursorPagerView();
1014 $page_size = $this->getPageSize($saved);
1015 if (is_finite($page_size)) {
1016 $pager->setPageSize($page_size);
1017 } else {
1018 // Consider an INF pagesize to mean a large finite pagesize.
1020 // TODO: It would be nice to handle this more gracefully, but math
1021 // with INF seems to vary across PHP versions, systems, and runtimes.
1022 $pager->setPageSize(0xFFFF);
1025 return $pager;
1029 public function executeQuery(
1030 PhabricatorPolicyAwareQuery $query,
1031 AphrontView $pager) {
1033 $query->setViewer($this->requireViewer());
1035 if ($this->shouldUseOffsetPaging()) {
1036 $objects = $query->executeWithOffsetPager($pager);
1037 } else {
1038 $objects = $query->executeWithCursorPager($pager);
1041 $this->didExecuteQuery($query);
1043 return $objects;
1046 protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) {
1047 return;
1051 /* -( Rendering )---------------------------------------------------------- */
1054 public function setRequest(AphrontRequest $request) {
1055 $this->request = $request;
1056 return $this;
1059 public function getRequest() {
1060 return $this->request;
1063 public function renderResults(
1064 array $objects,
1065 PhabricatorSavedQuery $query) {
1067 $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
1069 if ($phids) {
1070 $handles = id(new PhabricatorHandleQuery())
1071 ->setViewer($this->requireViewer())
1072 ->witHPHIDs($phids)
1073 ->execute();
1074 } else {
1075 $handles = array();
1078 return $this->renderResultList($objects, $query, $handles);
1081 protected function getRequiredHandlePHIDsForResultList(
1082 array $objects,
1083 PhabricatorSavedQuery $query) {
1084 return array();
1087 abstract protected function renderResultList(
1088 array $objects,
1089 PhabricatorSavedQuery $query,
1090 array $handles);
1093 /* -( Application Search )------------------------------------------------- */
1096 public function getSearchFieldsForConduit() {
1097 $standard_fields = $this->buildSearchFields();
1099 $fields = array();
1100 foreach ($standard_fields as $field_key => $field) {
1101 $conduit_key = $field->getConduitKey();
1103 if (isset($fields[$conduit_key])) {
1104 $other = $fields[$conduit_key];
1105 $other_key = $other->getKey();
1107 throw new Exception(
1108 pht(
1109 'SearchFields "%s" (of class "%s") and "%s" (of class "%s") both '.
1110 'define the same Conduit key ("%s"). Keys must be unique.',
1111 $field_key,
1112 get_class($field),
1113 $other_key,
1114 get_class($other),
1115 $conduit_key));
1118 $fields[$conduit_key] = $field;
1121 // These are handled separately for Conduit, so don't show them as
1122 // supported.
1123 unset($fields['order']);
1124 unset($fields['limit']);
1126 $viewer = $this->requireViewer();
1127 foreach ($fields as $key => $field) {
1128 $field->setViewer($viewer);
1131 return $fields;
1134 public function buildConduitResponse(
1135 ConduitAPIRequest $request,
1136 ConduitAPIMethod $method) {
1137 $viewer = $this->requireViewer();
1139 $query_key = $request->getValue('queryKey');
1140 if (!strlen($query_key)) {
1141 $saved_query = new PhabricatorSavedQuery();
1142 } else if ($this->isBuiltinQuery($query_key)) {
1143 $saved_query = $this->buildSavedQueryFromBuiltin($query_key);
1144 } else {
1145 $saved_query = id(new PhabricatorSavedQueryQuery())
1146 ->setViewer($viewer)
1147 ->withQueryKeys(array($query_key))
1148 ->executeOne();
1149 if (!$saved_query) {
1150 throw new Exception(
1151 pht(
1152 'Query key "%s" does not correspond to a valid query.',
1153 $query_key));
1157 $constraints = $request->getValue('constraints', array());
1158 if (!is_array($constraints)) {
1159 throw new Exception(
1160 pht(
1161 'Parameter "constraints" must be a map of constraints, got "%s".',
1162 phutil_describe_type($constraints)));
1165 $fields = $this->getSearchFieldsForConduit();
1167 foreach ($fields as $key => $field) {
1168 if (!$field->getConduitParameterType()) {
1169 unset($fields[$key]);
1173 $valid_constraints = array();
1174 foreach ($fields as $field) {
1175 foreach ($field->getValidConstraintKeys() as $key) {
1176 $valid_constraints[$key] = true;
1180 foreach ($constraints as $key => $constraint) {
1181 if (empty($valid_constraints[$key])) {
1182 throw new Exception(
1183 pht(
1184 'Constraint "%s" is not a valid constraint for this query.',
1185 $key));
1189 foreach ($fields as $field) {
1190 if (!$field->getValueExistsInConduitRequest($constraints)) {
1191 continue;
1194 $value = $field->readValueFromConduitRequest(
1195 $constraints,
1196 $request->getIsStrictlyTyped());
1197 $saved_query->setParameter($field->getKey(), $value);
1200 // NOTE: Currently, when running an ad-hoc query we never persist it into
1201 // a saved query. We might want to add an option to do this in the future
1202 // (for example, to enable a CLI-to-Web workflow where user can view more
1203 // details about results by following a link), but have no use cases for
1204 // it today. If we do identify a use case, we could save the query here.
1206 $query = $this->buildQueryFromSavedQuery($saved_query);
1207 $pager = $this->newPagerForSavedQuery($saved_query);
1209 $attachments = $this->getConduitSearchAttachments();
1211 // TODO: Validate this better.
1212 $attachment_specs = $request->getValue('attachments', array());
1213 $attachments = array_select_keys(
1214 $attachments,
1215 array_keys($attachment_specs));
1217 foreach ($attachments as $key => $attachment) {
1218 $attachment->setViewer($viewer);
1221 foreach ($attachments as $key => $attachment) {
1222 $attachment->willLoadAttachmentData($query, $attachment_specs[$key]);
1225 $this->setQueryOrderForConduit($query, $request);
1226 $this->setPagerLimitForConduit($pager, $request);
1227 $this->setPagerOffsetsForConduit($pager, $request);
1229 $objects = $this->executeQuery($query, $pager);
1231 $data = array();
1232 if ($objects) {
1233 $field_extensions = $this->getConduitFieldExtensions();
1235 $extension_data = array();
1236 foreach ($field_extensions as $key => $extension) {
1237 $extension_data[$key] = $extension->loadExtensionConduitData($objects);
1240 $attachment_data = array();
1241 foreach ($attachments as $key => $attachment) {
1242 $attachment_data[$key] = $attachment->loadAttachmentData(
1243 $objects,
1244 $attachment_specs[$key]);
1247 foreach ($objects as $object) {
1248 $field_map = $this->getObjectWireFieldsForConduit(
1249 $object,
1250 $field_extensions,
1251 $extension_data);
1253 $attachment_map = array();
1254 foreach ($attachments as $key => $attachment) {
1255 $attachment_map[$key] = $attachment->getAttachmentForObject(
1256 $object,
1257 $attachment_data[$key],
1258 $attachment_specs[$key]);
1261 // If this is empty, we still want to emit a JSON object, not a
1262 // JSON list.
1263 if (!$attachment_map) {
1264 $attachment_map = (object)$attachment_map;
1267 $id = (int)$object->getID();
1268 $phid = $object->getPHID();
1270 $data[] = array(
1271 'id' => $id,
1272 'type' => phid_get_type($phid),
1273 'phid' => $phid,
1274 'fields' => $field_map,
1275 'attachments' => $attachment_map,
1280 return array(
1281 'data' => $data,
1282 'maps' => $method->getQueryMaps($query),
1283 'query' => array(
1284 // This may be `null` if we have not saved the query.
1285 'queryKey' => $saved_query->getQueryKey(),
1287 'cursor' => array(
1288 'limit' => $pager->getPageSize(),
1289 'after' => $pager->getNextPageID(),
1290 'before' => $pager->getPrevPageID(),
1291 'order' => $request->getValue('order'),
1296 public function getAllConduitFieldSpecifications() {
1297 $extensions = $this->getConduitFieldExtensions();
1298 $object = $this->newQuery()->newResultObject();
1300 $map = array();
1301 foreach ($extensions as $extension) {
1302 $specifications = $extension->getFieldSpecificationsForConduit($object);
1303 foreach ($specifications as $specification) {
1304 $key = $specification->getKey();
1305 if (isset($map[$key])) {
1306 throw new Exception(
1307 pht(
1308 'Two field specifications share the same key ("%s"). Each '.
1309 'specification must have a unique key.',
1310 $key));
1312 $map[$key] = $specification;
1316 return $map;
1319 private function getEngineExtensions() {
1320 $extensions = PhabricatorSearchEngineExtension::getAllEnabledExtensions();
1322 foreach ($extensions as $key => $extension) {
1323 $extension
1324 ->setViewer($this->requireViewer())
1325 ->setSearchEngine($this);
1328 $object = $this->newResultObject();
1329 foreach ($extensions as $key => $extension) {
1330 if (!$extension->supportsObject($object)) {
1331 unset($extensions[$key]);
1335 return $extensions;
1339 private function getConduitFieldExtensions() {
1340 $extensions = $this->getEngineExtensions();
1341 $object = $this->newResultObject();
1343 foreach ($extensions as $key => $extension) {
1344 if (!$extension->getFieldSpecificationsForConduit($object)) {
1345 unset($extensions[$key]);
1349 return $extensions;
1352 private function setQueryOrderForConduit($query, ConduitAPIRequest $request) {
1353 $order = $request->getValue('order');
1354 if ($order === null) {
1355 return;
1358 if (is_scalar($order)) {
1359 $query->setOrder($order);
1360 } else {
1361 $query->setOrderVector($order);
1365 private function setPagerLimitForConduit($pager, ConduitAPIRequest $request) {
1366 $limit = $request->getValue('limit');
1368 // If there's no limit specified and the query uses a weird huge page
1369 // size, just leave it at the default gigantic page size. Otherwise,
1370 // make sure it's between 1 and 100, inclusive.
1372 if ($limit === null) {
1373 if ($pager->getPageSize() >= 0xFFFF) {
1374 return;
1375 } else {
1376 $limit = 100;
1380 if ($limit > 100) {
1381 throw new Exception(
1382 pht(
1383 'Maximum page size for Conduit API method calls is 100, but '.
1384 'this call specified %s.',
1385 $limit));
1388 if ($limit < 1) {
1389 throw new Exception(
1390 pht(
1391 'Minimum page size for API searches is 1, but this call '.
1392 'specified %s.',
1393 $limit));
1396 $pager->setPageSize($limit);
1399 private function setPagerOffsetsForConduit(
1400 $pager,
1401 ConduitAPIRequest $request) {
1402 $before_id = $request->getValue('before');
1403 if ($before_id !== null) {
1404 $pager->setBeforeID($before_id);
1407 $after_id = $request->getValue('after');
1408 if ($after_id !== null) {
1409 $pager->setAfterID($after_id);
1413 protected function getObjectWireFieldsForConduit(
1414 $object,
1415 array $field_extensions,
1416 array $extension_data) {
1418 $fields = array();
1419 foreach ($field_extensions as $key => $extension) {
1420 $data = idx($extension_data, $key, array());
1421 $fields += $extension->getFieldValuesForConduit($object, $data);
1424 return $fields;
1427 public function getConduitSearchAttachments() {
1428 $extensions = $this->getEngineExtensions();
1429 $object = $this->newResultObject();
1431 $attachments = array();
1432 foreach ($extensions as $extension) {
1433 $extension_attachments = $extension->getSearchAttachments($object);
1434 foreach ($extension_attachments as $attachment) {
1435 $attachment_key = $attachment->getAttachmentKey();
1436 if (isset($attachments[$attachment_key])) {
1437 $other = $attachments[$attachment_key];
1438 throw new Exception(
1439 pht(
1440 'Two search engine attachments (of classes "%s" and "%s") '.
1441 'specify the same attachment key ("%s"); keys must be unique.',
1442 get_class($attachment),
1443 get_class($other),
1444 $attachment_key));
1446 $attachments[$attachment_key] = $attachment;
1450 return $attachments;
1453 final public function renderNewUserView() {
1454 $body = $this->getNewUserBody();
1456 if (!$body) {
1457 return null;
1460 return $body;
1463 protected function getNewUserHeader() {
1464 return null;
1467 protected function getNewUserBody() {
1468 return null;
1471 public function newUseResultsActions(PhabricatorSavedQuery $saved) {
1472 return array();
1476 /* -( Export )------------------------------------------------------------- */
1479 public function canExport() {
1480 $fields = $this->newExportFields();
1481 return (bool)$fields;
1484 final public function newExportFieldList() {
1485 $object = $this->newResultObject();
1487 $builtin_fields = array(
1488 id(new PhabricatorIDExportField())
1489 ->setKey('id')
1490 ->setLabel(pht('ID')),
1493 if ($object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) {
1494 $builtin_fields[] = id(new PhabricatorPHIDExportField())
1495 ->setKey('phid')
1496 ->setLabel(pht('PHID'));
1499 $fields = mpull($builtin_fields, null, 'getKey');
1501 $export_fields = $this->newExportFields();
1502 foreach ($export_fields as $export_field) {
1503 $key = $export_field->getKey();
1505 if (isset($fields[$key])) {
1506 throw new Exception(
1507 pht(
1508 'Search engine ("%s") defines an export field with a key ("%s") '.
1509 'that collides with another field. Each field must have a '.
1510 'unique key.',
1511 get_class($this),
1512 $key));
1515 $fields[$key] = $export_field;
1518 $extensions = $this->newExportExtensions();
1519 foreach ($extensions as $extension) {
1520 $extension_fields = $extension->newExportFields();
1521 foreach ($extension_fields as $extension_field) {
1522 $key = $extension_field->getKey();
1524 if (isset($fields[$key])) {
1525 throw new Exception(
1526 pht(
1527 'Export engine extension ("%s") defines an export field with '.
1528 'a key ("%s") that collides with another field. Each field '.
1529 'must have a unique key.',
1530 get_class($extension_field),
1531 $key));
1534 $fields[$key] = $extension_field;
1538 return $fields;
1541 final public function newExport(array $objects) {
1542 $object = $this->newResultObject();
1543 $has_phid = $object->getConfigOption(LiskDAO::CONFIG_AUX_PHID);
1545 $objects = array_values($objects);
1546 $n = count($objects);
1548 $maps = array();
1549 foreach ($objects as $object) {
1550 $map = array(
1551 'id' => $object->getID(),
1554 if ($has_phid) {
1555 $map['phid'] = $object->getPHID();
1558 $maps[] = $map;
1561 $export_data = $this->newExportData($objects);
1562 $export_data = array_values($export_data);
1563 if (count($export_data) !== count($objects)) {
1564 throw new Exception(
1565 pht(
1566 'Search engine ("%s") exported the wrong number of objects, '.
1567 'expected %s but got %s.',
1568 get_class($this),
1569 phutil_count($objects),
1570 phutil_count($export_data)));
1573 for ($ii = 0; $ii < $n; $ii++) {
1574 $maps[$ii] += $export_data[$ii];
1577 $extensions = $this->newExportExtensions();
1578 foreach ($extensions as $extension) {
1579 $extension_data = $extension->newExportData($objects);
1580 $extension_data = array_values($extension_data);
1581 if (count($export_data) !== count($objects)) {
1582 throw new Exception(
1583 pht(
1584 'Export engine extension ("%s") exported the wrong number of '.
1585 'objects, expected %s but got %s.',
1586 get_class($extension),
1587 phutil_count($objects),
1588 phutil_count($export_data)));
1591 for ($ii = 0; $ii < $n; $ii++) {
1592 $maps[$ii] += $extension_data[$ii];
1596 return $maps;
1599 protected function newExportFields() {
1600 return array();
1603 protected function newExportData(array $objects) {
1604 throw new PhutilMethodNotImplementedException();
1607 private function newExportExtensions() {
1608 $object = $this->newResultObject();
1609 $viewer = $this->requireViewer();
1611 $extensions = PhabricatorExportEngineExtension::getAllExtensions();
1613 $supported = array();
1614 foreach ($extensions as $extension) {
1615 $extension = clone $extension;
1616 $extension->setViewer($viewer);
1618 if ($extension->supportsObject($object)) {
1619 $supported[] = $extension;
1623 return $supported;