Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2893583
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Advanced/Developer...
View Handle
View Hovercard
Size
81 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
index e878365acb..a0481126c6 100644
--- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
+++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php
@@ -1,209 +1,154 @@
<?php
final class PhabricatorConduitConsoleController
extends PhabricatorConduitController {
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
$method_name = $request->getURIData('method');
$method = id(new PhabricatorConduitMethodQuery())
->setViewer($viewer)
->withMethods(array($method_name))
->executeOne();
if (!$method) {
return new Aphront404Response();
}
$method->setViewer($viewer);
$call_uri = '/api/'.$method->getAPIMethodName();
$status = $method->getMethodStatus();
$reason = $method->getMethodStatusDescription();
$errors = array();
switch ($status) {
case ConduitAPIMethod::METHOD_STATUS_DEPRECATED:
$reason = nonempty($reason, pht('This method is deprecated.'));
$errors[] = pht('Deprecated Method: %s', $reason);
break;
case ConduitAPIMethod::METHOD_STATUS_UNSTABLE:
$reason = nonempty(
$reason,
pht(
'This method is new and unstable. Its interface is subject '.
'to change.'));
$errors[] = pht('Unstable Method: %s', $reason);
break;
}
$form = id(new AphrontFormView())
->setAction($call_uri)
->setUser($request->getUser())
->appendRemarkupInstructions(
pht(
'Enter parameters using **JSON**. For instance, to enter a '.
'list, type: `%s`',
'["apple", "banana", "cherry"]'));
$params = $method->getParamTypes();
foreach ($params as $param => $desc) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel($param)
->setName("params[{$param}]")
->setCaption($desc));
}
$must_login = !$viewer->isLoggedIn() &&
$method->shouldRequireAuthentication();
if ($must_login) {
$errors[] = pht(
'Login Required: This method requires authentication. You must '.
'log in before you can make calls to it.');
} else {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Output Format'))
->setName('output')
->setOptions(
array(
'human' => pht('Human Readable'),
'json' => pht('JSON'),
)))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($this->getApplicationURI())
->setValue(pht('Call Method')));
}
$header = id(new PHUIHeaderView())
->setUser($viewer)
->setHeader($method->getAPIMethodName());
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Call Method'))
->setForm($form);
$content = array();
$properties = $this->buildMethodProperties($method);
$info_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('API Method: %s', $method->getAPIMethodName()))
->setFormErrors($errors)
->appendChild($properties);
$content[] = $info_box;
+ $content[] = $method->getMethodDocumentation();
$content[] = $form_box;
$content[] = $this->renderExampleBox($method, null);
- $query = $method->newQueryObject();
- if ($query) {
- $orders = $query->getBuiltinOrders();
-
- $rows = array();
- foreach ($orders as $key => $order) {
- $rows[] = array(
- $key,
- $order['name'],
- implode(', ', $order['vector']),
- );
- }
-
- $table = id(new AphrontTableView($rows))
- ->setHeaders(
- array(
- pht('Key'),
- pht('Description'),
- pht('Columns'),
- ))
- ->setColumnClasses(
- array(
- 'pri',
- '',
- 'wide',
- ));
- $content[] = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Builtin Orders'))
- ->setTable($table);
-
- $columns = $query->getOrderableColumns();
-
- $rows = array();
- foreach ($columns as $key => $column) {
- $rows[] = array(
- $key,
- idx($column, 'unique') ? pht('Yes') : pht('No'),
- );
- }
-
- $table = id(new AphrontTableView($rows))
- ->setHeaders(
- array(
- pht('Key'),
- pht('Unique'),
- ))
- ->setColumnClasses(
- array(
- 'pri',
- 'wide',
- ));
- $content[] = id(new PHUIObjectBoxView())
- ->setHeaderText(pht('Column Orders'))
- ->setTable($table);
- }
-
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb($method->getAPIMethodName());
return $this->buildApplicationPage(
array(
$crumbs,
$content,
),
array(
'title' => $method->getAPIMethodName(),
));
}
private function buildMethodProperties(ConduitAPIMethod $method) {
$viewer = $this->getViewer();
$view = id(new PHUIPropertyListView());
$view->addProperty(
pht('Returns'),
$method->getReturnType());
$error_types = $method->getErrorTypes();
$error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.');
$error_description = array();
foreach ($error_types as $error => $meaning) {
$error_description[] = hsprintf(
'<li><strong>%s:</strong> %s</li>',
$error,
$meaning);
}
$error_description = phutil_tag('ul', array(), $error_description);
$view->addProperty(
pht('Errors'),
$error_description);
$view->addSectionHeader(
pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
$view->addTextContent(
new PHUIRemarkupView($viewer, $method->getMethodDescription()));
return $view;
}
}
diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php
index 8471ac8919..eb972a8d90 100644
--- a/src/applications/conduit/method/ConduitAPIMethod.php
+++ b/src/applications/conduit/method/ConduitAPIMethod.php
@@ -1,392 +1,396 @@
<?php
/**
* @task info Method Information
* @task status Method Status
* @task pager Paging Results
*/
abstract class ConduitAPIMethod
extends Phobject
implements PhabricatorPolicyInterface {
private $viewer;
const METHOD_STATUS_STABLE = 'stable';
const METHOD_STATUS_UNSTABLE = 'unstable';
const METHOD_STATUS_DEPRECATED = 'deprecated';
/**
* Get a short, human-readable text summary of the method.
*
* @return string Short summary of method.
* @task info
*/
public function getMethodSummary() {
return $this->getMethodDescription();
}
/**
* Get a detailed description of the method.
*
* This method should return remarkup.
*
* @return string Detailed description of the method.
* @task info
*/
abstract public function getMethodDescription();
+ public function getMethodDocumentation() {
+ return null;
+ }
+
abstract protected function defineParamTypes();
abstract protected function defineReturnType();
protected function defineErrorTypes() {
return array();
}
abstract protected function execute(ConduitAPIRequest $request);
public function getParamTypes() {
$types = $this->defineParamTypes();
$query = $this->newQueryObject();
if ($query) {
$types['order'] = 'optional order';
$types += $this->getPagerParamTypes();
}
return $types;
}
public function getReturnType() {
return $this->defineReturnType();
}
public function getErrorTypes() {
return $this->defineErrorTypes();
}
/**
* This is mostly for compatibility with
* @{class:PhabricatorCursorPagedPolicyAwareQuery}.
*/
public function getID() {
return $this->getAPIMethodName();
}
/**
* Get the status for this method (e.g., stable, unstable or deprecated).
* Should return a METHOD_STATUS_* constant. By default, methods are
* "stable".
*
* @return const METHOD_STATUS_* constant.
* @task status
*/
public function getMethodStatus() {
return self::METHOD_STATUS_STABLE;
}
/**
* Optional description to supplement the method status. In particular, if
* a method is deprecated, you can return a string here describing the reason
* for deprecation and stable alternatives.
*
* @return string|null Description of the method status, if available.
* @task status
*/
public function getMethodStatusDescription() {
return null;
}
public function getErrorDescription($error_code) {
return idx($this->getErrorTypes(), $error_code, pht('Unknown Error'));
}
public function getRequiredScope() {
// by default, conduit methods are not accessible via OAuth
return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE;
}
public function executeMethod(ConduitAPIRequest $request) {
$this->setViewer($request->getUser());
return $this->execute($request);
}
abstract public function getAPIMethodName();
/**
* Return a key which sorts methods by application name, then method status,
* then method name.
*/
public function getSortOrder() {
$name = $this->getAPIMethodName();
$map = array(
self::METHOD_STATUS_STABLE => 0,
self::METHOD_STATUS_UNSTABLE => 1,
self::METHOD_STATUS_DEPRECATED => 2,
);
$ord = idx($map, $this->getMethodStatus(), 0);
list($head, $tail) = explode('.', $name, 2);
return "{$head}.{$ord}.{$tail}";
}
public function getApplicationName() {
return head(explode('.', $this->getAPIMethodName(), 2));
}
public static function loadAllConduitMethods() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getAPIMethodName')
->execute();
}
public static function getConduitMethod($method_name) {
$method_map = self::loadAllConduitMethods();
return idx($method_map, $method_name);
}
public function shouldRequireAuthentication() {
return true;
}
public function shouldAllowPublic() {
return false;
}
public function shouldAllowUnguardedWrites() {
return false;
}
/**
* Optionally, return a @{class:PhabricatorApplication} which this call is
* part of. The call will be disabled when the application is uninstalled.
*
* @return PhabricatorApplication|null Related application.
*/
public function getApplication() {
return null;
}
protected function formatStringConstants($constants) {
foreach ($constants as $key => $value) {
$constants[$key] = '"'.$value.'"';
}
$constants = implode(', ', $constants);
return 'string-constant<'.$constants.'>';
}
public static function getParameterMetadataKey($key) {
if (strncmp($key, 'api.', 4) === 0) {
// All keys passed beginning with "api." are always metadata keys.
return substr($key, 4);
} else {
switch ($key) {
// These are real keys which always belong to request metadata.
case 'access_token':
case 'scope':
case 'output':
// This is not a real metadata key; it is included here only to
// prevent Conduit methods from defining it.
case '__conduit__':
// This is prevented globally as a blanket defense against OAuth
// redirection attacks. It is included here to stop Conduit methods
// from defining it.
case 'code':
// This is not a real metadata key, but the presence of this
// parameter triggers an alternate request decoding pathway.
case 'params':
return $key;
}
}
return null;
}
final public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
/* -( Paging Results )----------------------------------------------------- */
/**
* @task pager
*/
protected function getPagerParamTypes() {
return array(
'before' => 'optional string',
'after' => 'optional string',
'limit' => 'optional int (default = 100)',
);
}
/**
* @task pager
*/
protected function newPager(ConduitAPIRequest $request) {
$limit = $request->getValue('limit', 100);
$limit = min(1000, $limit);
$limit = max(1, $limit);
$pager = id(new AphrontCursorPagerView())
->setPageSize($limit);
$before_id = $request->getValue('before');
if ($before_id !== null) {
$pager->setBeforeID($before_id);
}
$after_id = $request->getValue('after');
if ($after_id !== null) {
$pager->setAfterID($after_id);
}
return $pager;
}
/**
* @task pager
*/
protected function addPagerResults(
array $results,
AphrontCursorPagerView $pager) {
$results['cursor'] = array(
'limit' => $pager->getPageSize(),
'after' => $pager->getNextPageID(),
'before' => $pager->getPrevPageID(),
);
return $results;
}
/* -( Implementing Query Methods )----------------------------------------- */
public function newQueryObject() {
return null;
}
protected function newQueryForRequest(ConduitAPIRequest $request) {
$query = $this->newQueryObject();
if (!$query) {
throw new Exception(
pht(
'You can not call newQueryFromRequest() in this method ("%s") '.
'because it does not implement newQueryObject().',
get_class($this)));
}
if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) {
throw new Exception(
pht(
'Call to method newQueryObject() did not return an object of class '.
'"%s".',
'PhabricatorCursorPagedPolicyAwareQuery'));
}
$query->setViewer($request->getUser());
$order = $request->getValue('order');
if ($order !== null) {
if (is_scalar($order)) {
$query->setOrder($order);
} else {
$query->setOrderVector($order);
}
}
return $query;
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getPHID() {
return null;
}
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
);
}
public function getPolicy($capability) {
// Application methods get application visibility; other methods get open
// visibility.
$application = $this->getApplication();
if ($application) {
return $application->getPolicy($capability);
}
return PhabricatorPolicies::getMostOpenPolicy();
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
if (!$this->shouldRequireAuthentication()) {
// Make unauthenticated methods universally visible.
return true;
}
return false;
}
public function describeAutomaticCapability($capability) {
return null;
}
protected function hasApplicationCapability(
$capability,
PhabricatorUser $viewer) {
$application = $this->getApplication();
if (!$application) {
return false;
}
return PhabricatorPolicyFilter::hasCapability(
$viewer,
$application,
$capability);
}
protected function requireApplicationCapability(
$capability,
PhabricatorUser $viewer) {
$application = $this->getApplication();
if (!$application) {
return;
}
PhabricatorPolicyFilter::requireCapability(
$viewer,
$this->getApplication(),
$capability);
}
}
diff --git a/src/applications/conduit/query/ConduitResultSearchEngineExtension.php b/src/applications/conduit/query/ConduitResultSearchEngineExtension.php
index b870c2c3b1..70aa1891c7 100644
--- a/src/applications/conduit/query/ConduitResultSearchEngineExtension.php
+++ b/src/applications/conduit/query/ConduitResultSearchEngineExtension.php
@@ -1,28 +1,32 @@
<?php
final class ConduitResultSearchEngineExtension
extends PhabricatorSearchEngineExtension {
const EXTENSIONKEY = 'conduit';
public function isExtensionEnabled() {
return true;
}
+ public function getExtensionOrder() {
+ return 1000;
+ }
+
public function getExtensionName() {
return pht('Support for ConduitResultInterface');
}
public function supportsObject($object) {
return ($object instanceof PhabricatorConduitResultInterface);
}
public function getFieldSpecificationsForConduit($object) {
return $object->getFieldSpecificationsForConduit();
}
public function getFieldValuesForConduit($object) {
return $object->getFieldValuesForConduit();
}
}
diff --git a/src/applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php b/src/applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php
index a187a60fa1..2bf62b3490 100644
--- a/src/applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php
+++ b/src/applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php
@@ -1,43 +1,47 @@
<?php
final class PhabricatorPolicySearchEngineExtension
extends PhabricatorSearchEngineExtension {
const EXTENSIONKEY = 'policy';
public function isExtensionEnabled() {
return true;
}
public function getExtensionName() {
return pht('Support for Policies');
}
public function supportsObject($object) {
return ($object instanceof PhabricatorPolicyInterface);
}
+ public function getExtensionOrder() {
+ return 6000;
+ }
+
public function getFieldSpecificationsForConduit($object) {
return array(
'policy' => array(
'type' => 'map<string, wild>',
'description' => pht(
'Map of capabilities to current policies.'),
),
);
}
public function getFieldValuesForConduit($object) {
$capabilities = $object->getCapabilities();
$map = array();
foreach ($capabilities as $capability) {
$map[$capability] = $object->getPolicy($capability);
}
return array(
'policy' => $map,
);
}
}
diff --git a/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php
index 298e0d0775..df804e142d 100644
--- a/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php
+++ b/src/applications/project/engineextension/PhabricatorProjectsSearchEngineExtension.php
@@ -1,53 +1,53 @@
<?php
final class PhabricatorProjectsSearchEngineExtension
extends PhabricatorSearchEngineExtension {
const EXTENSIONKEY = 'projects';
public function isExtensionEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorProjectApplication');
}
public function getExtensionName() {
return pht('Support for Projects');
}
public function getExtensionOrder() {
- return 2000;
+ return 3000;
}
public function supportsObject($object) {
return ($object instanceof PhabricatorProjectInterface);
}
public function applyConstraintsToQuery(
$object,
$query,
PhabricatorSavedQuery $saved,
array $map) {
if (!empty($map['projectPHIDs'])) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$map['projectPHIDs']);
}
}
public function getSearchFields($object) {
$fields = array();
$fields[] = id(new PhabricatorProjectSearchField())
->setKey('projectPHIDs')
->setConduitKey('projects')
->setAliases(array('project', 'projects'))
->setLabel(pht('Projects'))
->setDescription(
pht('Search for objects associated with given projects.'));
return $fields;
}
}
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index a77b39f268..e8d115d9a2 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1313 +1,1329 @@
<?php
/**
* Represents an abstract search engine for an application. It supports
* creating and storing saved queries.
*
* @task construct Constructing Engines
* @task app Applications
* @task builtin Builtin Queries
* @task uri Query URIs
* @task dates Date Filters
* @task order Result Ordering
* @task read Reading Utilities
* @task exec Paging and Executing Queries
* @task render Rendering Results
*/
abstract class PhabricatorApplicationSearchEngine extends Phobject {
private $application;
private $viewer;
private $errors = array();
private $request;
private $context;
private $controller;
private $namedQueries;
private $navigationItems = array();
const CONTEXT_LIST = 'list';
const CONTEXT_PANEL = 'panel';
public function setController(PhabricatorController $controller) {
$this->controller = $controller;
return $this;
}
public function getController() {
return $this->controller;
}
public function buildResponse() {
$controller = $this->getController();
$request = $controller->getRequest();
$search = id(new PhabricatorApplicationSearchController())
->setQueryKey($request->getURIData('queryKey'))
->setSearchEngine($this);
return $controller->delegateToController($search);
}
public function newResultObject() {
// We may be able to get this automatically if newQuery() is implemented.
$query = $this->newQuery();
if ($query) {
$object = $query->newResultObject();
if ($object) {
return $object;
}
}
return null;
}
public function newQuery() {
return null;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
protected function requireViewer() {
if (!$this->viewer) {
throw new PhutilInvalidStateException('setViewer');
}
return $this->viewer;
}
public function setContext($context) {
$this->context = $context;
return $this;
}
public function isPanelContext() {
return ($this->context == self::CONTEXT_PANEL);
}
public function setNavigationItems(array $navigation_items) {
assert_instances_of($navigation_items, 'PHUIListItemView');
$this->navigationItems = $navigation_items;
return $this;
}
public function getNavigationItems() {
return $this->navigationItems;
}
public function canUseInPanelContext() {
return true;
}
public function saveQuery(PhabricatorSavedQuery $query) {
$query->setEngineClassName(get_class($this));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$query->save();
} catch (AphrontDuplicateKeyQueryException $ex) {
// Ignore, this is just a repeated search.
}
unset($unguarded);
}
/**
* Create a saved query object from the request.
*
* @param AphrontRequest The search request.
* @return PhabricatorSavedQuery
*/
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$fields = $this->buildSearchFields();
$viewer = $this->requireViewer();
$saved = new PhabricatorSavedQuery();
foreach ($fields as $field) {
$field->setViewer($viewer);
$value = $field->readValueFromRequest($request);
$saved->setParameter($field->getKey(), $value);
}
return $saved;
}
/**
* Executes the saved query.
*
* @param PhabricatorSavedQuery The saved query to operate on.
* @return The result of the query.
*/
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$saved = clone $saved;
$this->willUseSavedQuery($saved);
$fields = $this->buildSearchFields();
$viewer = $this->requireViewer();
$map = array();
foreach ($fields as $field) {
$field->setViewer($viewer);
$field->readValueFromSavedQuery($saved);
$value = $field->getValueForQuery($field->getValue());
$map[$field->getKey()] = $value;
}
$query = $this->buildQueryFromParameters($map);
$object = $this->newResultObject();
if (!$object) {
return $query;
}
$extensions = $this->getEngineExtensions();
foreach ($extensions as $extension) {
$extension->applyConstraintsToQuery($object, $query, $saved, $map);
}
$order = $saved->getParameter('order');
$builtin = $query->getBuiltinOrderAliasMap();
if (strlen($order) && isset($builtin[$order])) {
$query->setOrder($order);
} else {
// If the order is invalid or not available, we choose the first
// builtin order. This isn't always the default order for the query,
// but is the first value in the "Order" dropdown, and makes the query
// behavior more consistent with the UI. In queries where the two
// orders differ, this order is the preferred order for humans.
$query->setOrder(head_key($builtin));
}
return $query;
}
/**
* Hook for subclasses to adjust saved queries prior to use.
*
* If an application changes how queries are saved, it can implement this
* hook to keep old queries working the way users expect, by reading,
* adjusting, and overwriting parameters.
*
* @param PhabricatorSavedQuery Saved query which will be executed.
* @return void
*/
protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
return;
}
protected function buildQueryFromParameters(array $parameters) {
throw new PhutilMethodNotImplementedException();
}
/**
* Builds the search form using the request.
*
* @param AphrontFormView Form to populate.
* @param PhabricatorSavedQuery The query from which to build the form.
* @return void
*/
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$saved = clone $saved;
$this->willUseSavedQuery($saved);
$fields = $this->buildSearchFields();
$fields = $this->adjustFieldsForDisplay($fields);
$viewer = $this->requireViewer();
foreach ($fields as $field) {
$field->setViewer($viewer);
$field->readValueFromSavedQuery($saved);
}
foreach ($fields as $field) {
foreach ($field->getErrors() as $error) {
$this->addError(last($error));
}
}
foreach ($fields as $field) {
$field->appendToForm($form);
}
}
protected function buildSearchFields() {
$fields = array();
foreach ($this->buildCustomSearchFields() as $field) {
$fields[] = $field;
}
$object = $this->newResultObject();
if ($object) {
$extensions = $this->getEngineExtensions();
foreach ($extensions as $extension) {
$extension_fields = $extension->getSearchFields($object);
foreach ($extension_fields as $extension_field) {
$fields[] = $extension_field;
}
}
}
$query = $this->newQuery();
if ($query && $this->shouldShowOrderField()) {
$orders = $query->getBuiltinOrders();
$orders = ipull($orders, 'name');
$fields[] = id(new PhabricatorSearchOrderField())
->setLabel(pht('Order By'))
->setKey('order')
->setOrderAliases($query->getBuiltinOrderAliasMap())
->setOptions($orders);
}
$field_map = array();
foreach ($fields as $field) {
$key = $field->getKey();
if (isset($field_map[$key])) {
throw new Exception(
pht(
'Two fields in this SearchEngine use the same key ("%s"), but '.
'each field must use a unique key.',
$key));
}
$field_map[$key] = $field;
}
return $field_map;
}
protected function shouldShowOrderField() {
return true;
}
private function adjustFieldsForDisplay(array $field_map) {
$order = $this->getDefaultFieldOrder();
$head_keys = array();
$tail_keys = array();
$seen_tail = false;
foreach ($order as $order_key) {
if ($order_key === '...') {
$seen_tail = true;
continue;
}
if (!$seen_tail) {
$head_keys[] = $order_key;
} else {
$tail_keys[] = $order_key;
}
}
$head = array_select_keys($field_map, $head_keys);
$body = array_diff_key($field_map, array_fuse($tail_keys));
$tail = array_select_keys($field_map, $tail_keys);
$result = $head + $body + $tail;
foreach ($this->getHiddenFields() as $hidden_key) {
unset($result[$hidden_key]);
}
return $result;
}
protected function buildCustomSearchFields() {
throw new PhutilMethodNotImplementedException();
}
/**
* Define the default display order for fields by returning a list of
* field keys.
*
* You can use the special key `...` to mean "all unspecified fields go
* here". This lets you easily put important fields at the top of the form,
* standard fields in the middle of the form, and less important fields at
* the bottom.
*
* For example, you might return a list like this:
*
* return array(
* 'authorPHIDs',
* 'reviewerPHIDs',
* '...',
* 'createdAfter',
* 'createdBefore',
* );
*
* Any unspecified fields (including custom fields and fields added
* automatically by infrastruture) will be put in the middle.
*
* @return list<string> Default ordering for field keys.
*/
protected function getDefaultFieldOrder() {
return array();
}
/**
* Return a list of field keys which should be hidden from the viewer.
*
* @return list<string> Fields to hide.
*/
protected function getHiddenFields() {
return array();
}
public function getErrors() {
return $this->errors;
}
public function addError($error) {
$this->errors[] = $error;
return $this;
}
/**
* Return an application URI corresponding to the results page of a query.
* Normally, this is something like `/application/query/QUERYKEY/`.
*
* @param string The query key to build a URI for.
* @return string URI where the query can be executed.
* @task uri
*/
public function getQueryResultsPageURI($query_key) {
return $this->getURI('query/'.$query_key.'/');
}
/**
* Return an application URI for query management. This is used when, e.g.,
* a query deletion operation is cancelled.
*
* @return string URI where queries can be managed.
* @task uri
*/
public function getQueryManagementURI() {
return $this->getURI('query/edit/');
}
/**
* Return the URI to a path within the application. Used to construct default
* URIs for management and results.
*
* @return string URI to path.
* @task uri
*/
abstract protected function getURI($path);
/**
* Return a human readable description of the type of objects this query
* searches for.
*
* For example, "Tasks" or "Commits".
*
* @return string Human-readable description of what this engine is used to
* find.
*/
abstract public function getResultTypeDescription();
public function newSavedQuery() {
return id(new PhabricatorSavedQuery())
->setEngineClassName(get_class($this));
}
public function addNavigationItems(PHUIListView $menu) {
$viewer = $this->requireViewer();
$menu->newLabel(pht('Queries'));
$named_queries = $this->loadEnabledNamedQueries();
foreach ($named_queries as $query) {
$key = $query->getQueryKey();
$uri = $this->getQueryResultsPageURI($key);
$menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
}
if ($viewer->isLoggedIn()) {
$manage_uri = $this->getQueryManagementURI();
$menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
}
$menu->newLabel(pht('Search'));
$advanced_uri = $this->getQueryResultsPageURI('advanced');
$menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
foreach ($this->navigationItems as $extra_item) {
$menu->addMenuItem($extra_item);
}
return $this;
}
public function loadAllNamedQueries() {
$viewer = $this->requireViewer();
$builtin = $this->getBuiltinQueries($viewer);
if ($this->namedQueries === null) {
$named_queries = id(new PhabricatorNamedQueryQuery())
->setViewer($viewer)
->withUserPHIDs(array($viewer->getPHID()))
->withEngineClassNames(array(get_class($this)))
->execute();
$named_queries = mpull($named_queries, null, 'getQueryKey');
$builtin = mpull($builtin, null, 'getQueryKey');
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin()) {
if (isset($builtin[$key])) {
$named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
unset($builtin[$key]);
} else {
unset($named_queries[$key]);
}
}
unset($builtin[$key]);
}
$named_queries = msort($named_queries, 'getSortKey');
$this->namedQueries = $named_queries;
}
return $this->namedQueries + $builtin;
}
public function loadEnabledNamedQueries() {
$named_queries = $this->loadAllNamedQueries();
foreach ($named_queries as $key => $named_query) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
unset($named_queries[$key]);
}
}
return $named_queries;
}
protected function setQueryProjects(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$datasource = id(new PhabricatorProjectLogicalDatasource())
->setViewer($this->requireViewer());
$projects = $saved->getParameter('projects', array());
$constraints = $datasource->evaluateTokens($projects);
if ($constraints) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$constraints);
}
return $this;
}
/* -( Applications )------------------------------------------------------- */
protected function getApplicationURI($path = '') {
return $this->getApplication()->getApplicationURI($path);
}
protected function getApplication() {
if (!$this->application) {
$class = $this->getApplicationClassName();
$this->application = id(new PhabricatorApplicationQuery())
->setViewer($this->requireViewer())
->withClasses(array($class))
->withInstalled(true)
->executeOne();
if (!$this->application) {
throw new Exception(
pht(
'Application "%s" is not installed!',
$class));
}
}
return $this->application;
}
abstract public function getApplicationClassName();
/* -( Constructing Engines )----------------------------------------------- */
/**
* Load all available application search engines.
*
* @return list<PhabricatorApplicationSearchEngine> All available engines.
* @task construct
*/
public static function getAllEngines() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->execute();
}
/**
* Get an engine by class name, if it exists.
*
* @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
* not exist.
* @task construct
*/
public static function getEngineByClassName($class_name) {
return idx(self::getAllEngines(), $class_name);
}
/* -( Builtin Queries )---------------------------------------------------- */
/**
* @task builtin
*/
public function getBuiltinQueries() {
$names = $this->getBuiltinQueryNames();
$queries = array();
$sequence = 0;
foreach ($names as $key => $name) {
$queries[$key] = id(new PhabricatorNamedQuery())
->setUserPHID($this->requireViewer()->getPHID())
->setEngineClassName(get_class($this))
->setQueryName($name)
->setQueryKey($key)
->setSequence((1 << 24) + $sequence++)
->setIsBuiltin(true);
}
return $queries;
}
/**
* @task builtin
*/
public function getBuiltinQuery($query_key) {
if (!$this->isBuiltinQuery($query_key)) {
throw new Exception(pht("'%s' is not a builtin!", $query_key));
}
return idx($this->getBuiltinQueries(), $query_key);
}
/**
* @task builtin
*/
protected function getBuiltinQueryNames() {
return array();
}
/**
* @task builtin
*/
public function isBuiltinQuery($query_key) {
$builtins = $this->getBuiltinQueries();
return isset($builtins[$query_key]);
}
/**
* @task builtin
*/
public function buildSavedQueryFromBuiltin($query_key) {
throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
}
/* -( Reading Utilities )--------------------------------------------------- */
/**
* Read a list of user PHIDs from a request in a flexible way. This method
* supports either of these forms:
*
* users[]=alincoln&users[]=htaft
* users=alincoln,htaft
*
* Additionally, users can be specified either by PHID or by name.
*
* The main goal of this flexibility is to allow external programs to generate
* links to pages (like "alincoln's open revisions") without needing to make
* API calls.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
* @param list<const> Other permitted PHID types.
* @return list<phid> List of user PHIDs and selector functions.
* @task read
*/
protected function readUsersFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$phids = array();
$names = array();
$allow_types = array_fuse($allow_types);
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
foreach ($list as $item) {
$type = phid_get_type($item);
if ($type == $user_type) {
$phids[] = $item;
} else if (isset($allow_types[$type])) {
$phids[] = $item;
} else {
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
// If this is a function, pass it through unchanged; we'll evaluate
// it later.
$phids[] = $item;
} else {
$names[] = $item;
}
}
}
if ($names) {
$users = id(new PhabricatorPeopleQuery())
->setViewer($this->requireViewer())
->withUsernames($names)
->execute();
foreach ($users as $user) {
$phids[] = $user->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/**
* Read a list of project PHIDs from a request in a flexible way.
*
* @param AphrontRequest Request to read user PHIDs from.
* @param string Key to read in the request.
* @return list<phid> List of projet PHIDs and selector functions.
* @task read
*/
protected function readProjectsFromRequest(AphrontRequest $request, $key) {
$list = $this->readListFromRequest($request, $key);
$phids = array();
$slugs = array();
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
foreach ($list as $item) {
$type = phid_get_type($item);
if ($type == $project_type) {
$phids[] = $item;
} else {
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
// If this is a function, pass it through unchanged; we'll evaluate
// it later.
$phids[] = $item;
} else {
$slugs[] = $item;
}
}
}
if ($slugs) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireViewer())
->withSlugs($slugs)
->execute();
foreach ($projects as $project) {
$phids[] = $project->getPHID();
}
$phids = array_unique($phids);
}
return $phids;
}
/**
* Read a list of subscribers from a request in a flexible way.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @return list<phid> List of object PHIDs.
* @task read
*/
protected function readSubscribersFromRequest(
AphrontRequest $request,
$key) {
return $this->readUsersFromRequest(
$request,
$key,
array(
PhabricatorProjectProjectPHIDType::TYPECONST,
));
}
/**
* Read a list of generic PHIDs from a request in a flexible way. Like
* @{method:readUsersFromRequest}, this method supports either array or
* comma-delimited forms. Objects can be specified either by PHID or by
* object name.
*
* @param AphrontRequest Request to read PHIDs from.
* @param string Key to read in the request.
* @param list<const> Optional, list of permitted PHID types.
* @return list<phid> List of object PHIDs.
*
* @task read
*/
protected function readPHIDsFromRequest(
AphrontRequest $request,
$key,
array $allow_types = array()) {
$list = $this->readListFromRequest($request, $key);
$objects = id(new PhabricatorObjectQuery())
->setViewer($this->requireViewer())
->withNames($list)
->execute();
$list = mpull($objects, 'getPHID');
if (!$list) {
return array();
}
// If only certain PHID types are allowed, filter out all the others.
if ($allow_types) {
$allow_types = array_fuse($allow_types);
foreach ($list as $key => $phid) {
if (empty($allow_types[phid_get_type($phid)])) {
unset($list[$key]);
}
}
}
return $list;
}
/**
* Read a list of items from the request, in either array format or string
* format:
*
* list[]=item1&list[]=item2
* list=item1,item2
*
* This provides flexibility when constructing URIs, especially from external
* sources.
*
* @param AphrontRequest Request to read strings from.
* @param string Key to read in the request.
* @return list<string> List of values.
*/
protected function readListFromRequest(
AphrontRequest $request,
$key) {
$list = $request->getArr($key, null);
if ($list === null) {
$list = $request->getStrList($key);
}
if (!$list) {
return array();
}
return $list;
}
protected function readDateFromRequest(
AphrontRequest $request,
$key) {
$value = AphrontFormDateControlValue::newFromRequest($request, $key);
if ($value->isEmpty()) {
return null;
}
return $value->getDictionary();
}
protected function readBoolFromRequest(
AphrontRequest $request,
$key) {
if (!strlen($request->getStr($key))) {
return null;
}
return $request->getBool($key);
}
protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
$value = $query->getParameter($key);
if ($value === null) {
return $value;
}
return $value ? 'true' : 'false';
}
/* -( Dates )-------------------------------------------------------------- */
/**
* @task dates
*/
protected function parseDateTime($date_time) {
if (!strlen($date_time)) {
return null;
}
return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
}
/**
* @task dates
*/
protected function buildDateRange(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query,
$start_key,
$start_name,
$end_key,
$end_name) {
$start_str = $saved_query->getParameter($start_key);
$start = null;
if (strlen($start_str)) {
$start = $this->parseDateTime($start_str);
if (!$start) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$start_name));
}
}
$end_str = $saved_query->getParameter($end_key);
$end = null;
if (strlen($end_str)) {
$end = $this->parseDateTime($end_str);
if (!$end) {
$this->addError(
pht(
'"%s" date can not be parsed.',
$end_name));
}
}
if ($start && $end && ($start >= $end)) {
$this->addError(
pht(
'"%s" must be a date before "%s".',
$start_name,
$end_name));
}
$form
->appendChild(
id(new PHUIFormFreeformDateControl())
->setName($start_key)
->setLabel($start_name)
->setValue($start_str))
->appendChild(
id(new AphrontFormTextControl())
->setName($end_key)
->setLabel($end_name)
->setValue($end_str));
}
/* -( Paging and Executing Queries )--------------------------------------- */
public function getPageSize(PhabricatorSavedQuery $saved) {
$limit = (int)$saved->getParameter('limit');
if ($limit > 0) {
return $limit;
}
return 100;
}
public function shouldUseOffsetPaging() {
return false;
}
public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
if ($this->shouldUseOffsetPaging()) {
$pager = new PHUIPagerView();
} else {
$pager = new AphrontCursorPagerView();
}
$page_size = $this->getPageSize($saved);
if (is_finite($page_size)) {
$pager->setPageSize($page_size);
} else {
// Consider an INF pagesize to mean a large finite pagesize.
// TODO: It would be nice to handle this more gracefully, but math
// with INF seems to vary across PHP versions, systems, and runtimes.
$pager->setPageSize(0xFFFF);
}
return $pager;
}
public function executeQuery(
PhabricatorPolicyAwareQuery $query,
AphrontView $pager) {
$query->setViewer($this->requireViewer());
if ($this->shouldUseOffsetPaging()) {
$objects = $query->executeWithOffsetPager($pager);
} else {
$objects = $query->executeWithCursorPager($pager);
}
return $objects;
}
/* -( Rendering )---------------------------------------------------------- */
public function setRequest(AphrontRequest $request) {
$this->request = $request;
return $this;
}
public function getRequest() {
return $this->request;
}
public function renderResults(
array $objects,
PhabricatorSavedQuery $query) {
$phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
if ($phids) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->witHPHIDs($phids)
->execute();
} else {
$handles = array();
}
return $this->renderResultList($objects, $query, $handles);
}
protected function getRequiredHandlePHIDsForResultList(
array $objects,
PhabricatorSavedQuery $query) {
return array();
}
abstract protected function renderResultList(
array $objects,
PhabricatorSavedQuery $query,
array $handles);
/* -( Application Search )------------------------------------------------- */
public function getSearchFieldsForConduit() {
$standard_fields = $this->buildSearchFields();
$fields = array();
foreach ($standard_fields as $field_key => $field) {
$conduit_key = $field->getConduitKey();
if (isset($fields[$conduit_key])) {
$other = $fields[$conduit_key];
$other_key = $other->getKey();
throw new Exception(
pht(
'SearchFields "%s" (of class "%s") and "%s" (of class "%s") both '.
'define the same Conduit key ("%s"). Keys must be unique.',
$field_key,
get_class($field),
$other_key,
get_class($other),
$conduit_key));
}
$fields[$conduit_key] = $field;
}
- $viewer = $this->requireViewer();
- foreach ($fields as $key => $field) {
- $field->setViewer($viewer);
- }
-
// These are handled separately for Conduit, so don't show them as
// supported.
unset($fields['ids']);
unset($fields['phids']);
unset($fields['order']);
unset($fields['limit']);
+ // TODO: Clean these up, shortly.
+ $fields = array(
+ 'ids' => id(new PhabricatorSearchDatasourceField())
+ ->setKey('ids')
+ ->setLabel(pht('IDs'))
+ ->setDescription(
+ pht('Search for objects with specific IDs.'))
+ ->setConduitParameterType(new ConduitIntListParameterType()),
+ 'phids' => id(new PhabricatorSearchDatasourceField())
+ ->setKey('phids')
+ ->setLabel(pht('PHIDs'))
+ ->setDescription(
+ pht('Search for objects with specific PHIDs.'))
+ ->setConduitParameterType(new ConduitPHIDListParameterType()),
+ ) + $fields;
+
+ $viewer = $this->requireViewer();
+ foreach ($fields as $key => $field) {
+ $field->setViewer($viewer);
+ }
+
return $fields;
}
public function buildConduitResponse(ConduitAPIRequest $request) {
$viewer = $this->requireViewer();
$query_key = $request->getValue('queryKey');
if (!strlen($query_key)) {
$saved_query = new PhabricatorSavedQuery();
} else if ($this->isBuiltinQuery($query_key)) {
$saved_query = $this->buildSavedQueryFromBuiltin($query_key);
} else {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved_query) {
throw new Exception(
pht(
'Query key "%s" does not correspond to a valid query.',
$query_key));
}
}
$constraints = $request->getValue('constraints', array());
$fields = $this->getSearchFieldsForConduit();
foreach ($fields as $key => $field) {
if (!$field->getConduitParameterType()) {
unset($fields[$key]);
}
}
foreach ($fields as $field) {
if (!$field->getValueExistsInConduitRequest($constraints)) {
continue;
}
$value = $field->readValueFromConduitRequest($constraints);
$saved_query->setParameter($field->getKey(), $value);
}
$this->saveQuery($saved_query);
$query = $this->buildQueryFromSavedQuery($saved_query);
$pager = $this->newPagerForSavedQuery($saved_query);
$this->setAutomaticConstraintsForConduit($query, $request, $constraints);
$this->setQueryOrderForConduit($query, $request);
$this->setPagerLimitForConduit($pager, $request);
$this->setPagerOffsetsForConduit($pager, $request);
$objects = $this->executeQuery($query, $pager);
$data = array();
if ($objects) {
$field_extensions = $this->getConduitFieldExtensions();
foreach ($objects as $object) {
$data[] = $this->getObjectWireFormatForConduit(
$object,
$field_extensions);
}
}
return array(
'data' => $data,
'query' => array(
'queryKey' => $saved_query->getQueryKey(),
),
'cursor' => array(
'limit' => $pager->getPageSize(),
'after' => $pager->getNextPageID(),
'before' => $pager->getPrevPageID(),
'order' => $request->getValue('order'),
),
);
}
public function getAllConduitFieldSpecifications() {
$extensions = $this->getConduitFieldExtensions();
$object = $this->newQuery()->newResultObject();
$specifications = array();
foreach ($extensions as $extension) {
$specifications += $extension->getFieldSpecificationsForConduit($object);
}
return $specifications;
}
private function getEngineExtensions() {
$extensions = PhabricatorSearchEngineExtension::getAllEnabledExtensions();
foreach ($extensions as $key => $extension) {
$extension
->setViewer($this->requireViewer())
->setSearchEngine($this);
}
$object = $this->newResultObject();
foreach ($extensions as $key => $extension) {
if (!$extension->supportsObject($object)) {
unset($extensions[$key]);
}
}
return $extensions;
}
private function getConduitFieldExtensions() {
$extensions = $this->getEngineExtensions();
$object = $this->newResultObject();
foreach ($extensions as $key => $extension) {
if (!$extension->getFieldSpecificationsForConduit($object)) {
unset($extensions[$key]);
}
}
return $extensions;
}
private function setAutomaticConstraintsForConduit(
$query,
ConduitAPIRequest $request,
array $constraints) {
$with_ids = idx($constraints, 'ids');
if ($with_ids) {
$query->withIDs($with_ids);
}
$with_phids = idx($constraints, 'phids');
if ($with_phids) {
$query->withPHIDs($with_phids);
}
}
private function setQueryOrderForConduit($query, ConduitAPIRequest $request) {
$order = $request->getValue('order');
if ($order === null) {
return;
}
if (is_scalar($order)) {
$query->setOrder($order);
} else {
$query->setOrderVector($order);
}
}
private function setPagerLimitForConduit($pager, ConduitAPIRequest $request) {
$limit = $request->getValue('limit');
// If there's no limit specified and the query uses a weird huge page
// size, just leave it at the default gigantic page size. Otherwise,
// make sure it's between 1 and 100, inclusive.
if ($limit === null) {
if ($pager->getPageSize() >= 0xFFFF) {
return;
} else {
$limit = 100;
}
}
if ($limit > 100) {
throw new Exception(
pht(
'Maximum page size for Conduit API method calls is 100, but '.
'this call specified %s.',
$limit));
}
if ($limit < 1) {
throw new Exception(
pht(
'Minimum page size for API searches is 1, but this call '.
'specified %s.',
$limit));
}
$pager->setPageSize($limit);
}
private function setPagerOffsetsForConduit(
$pager,
ConduitAPIRequest $request) {
$before_id = $request->getValue('before');
if ($before_id !== null) {
$pager->setBeforeID($before_id);
}
$after_id = $request->getValue('after');
if ($after_id !== null) {
$pager->setAfterID($after_id);
}
}
protected function getObjectWireFormatForConduit(
$object,
array $field_extensions) {
$phid = $object->getPHID();
return array(
'id' => (int)$object->getID(),
'type' => phid_get_type($phid),
'phid' => $phid,
'fields' => $this->getObjectWireFieldsForConduit(
$object,
$field_extensions),
);
}
protected function getObjectWireFieldsForConduit(
$object,
array $field_extensions) {
$fields = array();
foreach ($field_extensions as $extension) {
$fields += $extension->getFieldValuesForConduit($object);
}
return $fields;
}
}
diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
index 21fb4b54a1..df46325079 100644
--- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
+++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php
@@ -1,398 +1,475 @@
<?php
abstract class PhabricatorSearchEngineAPIMethod
extends ConduitAPIMethod {
abstract public function newSearchEngine();
public function getApplication() {
$engine = $this->newSearchEngine();
$class = $engine->getApplicationClassName();
return PhabricatorApplication::getByClass($class);
}
public function getMethodStatus() {
return self::METHOD_STATUS_UNSTABLE;
}
public function getMethodStatusDescription() {
return pht('ApplicationSearch methods are highly unstable.');
}
final protected function defineParamTypes() {
return array(
'queryKey' => 'optional string',
'constraints' => 'optional map<string, wild>',
'order' => 'optional order',
) + $this->getPagerParamTypes();
}
final protected function defineReturnType() {
return 'map<string, wild>';
}
final protected function execute(ConduitAPIRequest $request) {
$engine = $this->newSearchEngine()
->setViewer($request->getUser());
return $engine->buildConduitResponse($request);
}
final public function getMethodDescription() {
+ return pht(
+ 'This is a standard **ApplicationSearch** method which will let you '.
+ 'list, query, or search for objects.');
+ }
+
+ final public function getMethodDocumentation() {
$viewer = $this->getViewer();
$engine = $this->newSearchEngine()
->setViewer($viewer);
$query = $engine->newQuery();
$out = array();
- $out[] = pht(<<<EOTEXT
-This is a standard **ApplicationSearch** method which will let you list, query,
-or search for objects.
+ $out[] = $this->buildQueriesBox($engine);
+ $out[] = $this->buildConstraintsBox($engine);
+ $out[] = $this->buildOrderBox($engine, $query);
+ $out[] = $this->buildFieldsBox($engine);
+ $out[] = $this->buildPagingBox($engine);
-EOTEXT
- );
+ return $out;
+ }
- $out[] = pht(<<<EOTEXT
-Prebuilt Queries
-----------------
+ private function buildQueriesBox(
+ PhabricatorApplicationSearchEngine $engine) {
+ $viewer = $this->getViewer();
-You can use a builtin or saved query as a starting point by passing it with
-`queryKey`. If you don't specify a `queryKey`, the query will start with no
-constraints.
+ $info = pht(<<<EOTEXT
+You can choose a builtin or saved query as a starting point for filtering
+results by selecting it with `queryKey`. If you don't specify a `queryKey`,
+the query will start with no constraints.
For example, many applications have builtin queries like `"active"` or
`"open"` to find only active or enabled results. To use a `queryKey`, specify
it like this:
-```lang=json
+```lang=json, name="Selecting a Builtin Query"
{
...
"queryKey": "active",
...
}
```
+The table below shows the keys to use to select builtin queries and your
+saved queries, but you can also use **any** query you run via the web UI as a
+starting point. You can find the key for a query by examining the URI after
+running a normal search.
+
You can use these keys to select builtin queries and your configured saved
queries:
EOTEXT
);
- $head_querykey = pht('Query Key');
- $head_name = pht('Name');
- $head_builtin = pht('Builtin');
-
$named_queries = $engine->loadAllNamedQueries();
- $table = array();
- $table[] = "| {$head_querykey} | {$head_name} | {$head_builtin} |";
- $table[] = '|------------------|--------------|-----------------|';
+ $rows = array();
foreach ($named_queries as $named_query) {
- $key = $named_query->getQueryKey();
- $name = $named_query->getQueryName();
$builtin = $named_query->getIsBuiltin()
? pht('Builtin')
: pht('Custom');
- $table[] = "| `{$key}` | {$name} | {$builtin} |";
+ $rows[] = array(
+ $named_query->getQueryKey(),
+ $named_query->getQueryName(),
+ $builtin,
+ );
}
- $table = implode("\n", $table);
- $out[] = $table;
- $out[] = pht(<<<EOTEXT
-You can also use **any** query you run via the web UI as a starting point. You
-can find the key for a query by examining the URI after running a normal
-search.
-EOTEXT
- );
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('Query Key'),
+ pht('Name'),
+ pht('Builtin'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'prewrap',
+ 'pri wide',
+ null,
+ ));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Builtin and Saved Queries'))
+ ->setCollapsed(true)
+ ->appendChild($this->buildRemarkup($info))
+ ->appendChild($table);
+ }
+
+ private function buildConstraintsBox(
+ PhabricatorApplicationSearchEngine $engine) {
+
+ $info = pht(<<<EOTEXT
+You can apply custom constraints by passing a dictionary in `constraints`.
+This will let you search for specific sets of results (for example, you may
+want show only results with a certain state, status, or owner).
- $out[] = pht(<<<EOTEXT
-Custom Constraints
-------------------
-You can add custom constraints to the basic query by passing `constraints`.
-This will let you filter results (for example, show only results with a
-certain state, status, or owner).
+If you specify both a `queryKey` and `constraints`, the builtin or saved query
+will be applied first as a starting point, then any additional values in
+`constraints` will be applied, overwriting the defaults from the original query.
Specify constraints like this:
-```lang=json
+```lang=json, name="Example Custom Constraints"
{
...
"constraints": {
- "authorPHIDs": ["PHID-USER-1111", "PHID-USER-2222"],
- "statuses": ["open", "closed"]
+ "authors": ["PHID-USER-1111", "PHID-USER-2222"],
+ "statuses": ["open", "closed"],
+ ...
},
...
}
```
-If you specify both a `queryKey` and `constraints`, the basic query
-configuration will be applied first as a starting point, then any additional
-values in `constraints` will be applied, overwriting the defaults from the
-original query.
-
This API endpoint supports these constraints:
EOTEXT
);
- $head_key = pht('Key');
- $head_label = pht('Label');
- $head_type = pht('Type');
- $head_desc = pht('Description');
-
- $desc_ids = pht('Search for specific objects by ID.');
- $desc_phids = pht('Search for specific objects by PHID.');
-
$fields = $engine->getSearchFieldsForConduit();
- $table = array();
- $table[] = "| {$head_key} | {$head_label} | {$head_type} | {$head_desc} |";
- $table[] = '|-------------|---------------|--------------|--------------|';
- $table[] = "| `ids` | **IDs** | `list<int>` | {$desc_ids} |";
- $table[] = "| `phids` | **PHIDs** | `list<phid>` | {$desc_phids} |";
+ $rows = array();
foreach ($fields as $field) {
$key = $field->getConduitKey();
$label = $field->getLabel();
$type_object = $field->getConduitParameterType();
if ($type_object) {
- $type = '`'.$type_object->getTypeName().'`';
+ $type = $type_object->getTypeName();
$description = $field->getDescription();
} else {
- $type = '';
- $description = '//'.pht('Not Supported').'//';
+ $type = null;
+ $description = phutil_tag('em', array(), pht('Not supported.'));
}
- $table[] = "| `{$key}` | **{$label}** | {$type} | {$description}";
+ $rows[] = array(
+ $key,
+ $label,
+ $type,
+ $description,
+ );
}
- $table = implode("\n", $table);
- $out[] = $table;
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('Key'),
+ pht('Label'),
+ pht('Type'),
+ pht('Description'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'prewrap',
+ 'pri',
+ 'prewrap',
+ 'wide',
+ ));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Custom Query Constraints'))
+ ->setCollapsed(true)
+ ->appendChild($this->buildRemarkup($info))
+ ->appendChild($table);
+ }
+
+ private function buildOrderBox(
+ PhabricatorApplicationSearchEngine $engine,
+ $query) {
- $out[] = pht(<<<EOTEXT
-Result Order
-------------
+ $orders_info = pht(<<<EOTEXT
+Use `order` to choose an ordering for the results.
-Use `order` to choose an ordering for the results. Either specify a single
-key from the builtin orders (these are a set of meaningful, high-level,
-human-readable orders) or specify a list of low-level columns.
+Either specify a single key from the builtin orders (these are a set of
+meaningful, high-level, human-readable orders) or specify a custom list of
+low-level columns.
To use a high-level order, choose a builtin order from the table below
and specify it like this:
-```lang=json
+```lang=json, name="Choosing a Result Order"
{
...
"order": "newest",
...
}
```
These builtin orders are available:
EOTEXT
);
- $head_builtin = pht('Builtin Order');
- $head_label = pht('Label');
- $head_columns = pht('Columns');
-
$orders = $query->getBuiltinOrders();
- $table = array();
- $table[] = "| {$head_builtin} | {$head_label} | {$head_columns} |";
- $table[] = '|-----------------|---------------------|-----------------|';
+ $rows = array();
foreach ($orders as $key => $order) {
- $name = $order['name'];
- $columns = implode(', ', $order['vector']);
- $table[] = "| `{$key}` | {$name} | {$columns} |";
+ $rows[] = array(
+ $key,
+ $order['name'],
+ implode(', ', $order['vector']),
+ );
}
- $table = implode("\n", $table);
- $out[] = $table;
- $out[] = pht(<<<EOTEXT
-You can choose a low-level column order instead. This is an advanced feature.
-
-In your custom order: each column may only be specified once; each column may
-be prefixed with "-" to invert the order; the last column must be unique; and
-no column other than the last may be unique.
+ $orders_table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('Key'),
+ pht('Description'),
+ pht('Columns'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'pri',
+ '',
+ 'wide',
+ ));
+
+ $columns_info = pht(<<<EOTEXT
+You can choose a low-level column order instead. To do this, provide a list
+of columns instead of a single key. This is an advanced feature.
+
+In a custom column order:
+
+ - each column may only be specified once;
+ - each column may be prefixed with `-` to invert the order;
+ - the last column must be a unique column, usually `id`; and
+ - no column other than the last may be unique.
To use a low-level order, choose a sequence of columns and specify them like
this:
-```lang=json
+```lang=json, name="Using a Custom Order"
{
...
"order": ["color", "-name", "id"],
...
}
```
These low-level columns are available:
EOTEXT
);
- $head_column = pht('Column Key');
- $head_unique = pht('Unique');
-
$columns = $query->getOrderableColumns();
-
- $table = array();
- $table[] = "| {$head_column} | {$head_unique} |";
- $table[] = '|----------------|----------------|';
+ $rows = array();
foreach ($columns as $key => $column) {
- $unique = idx($column, 'unique')
- ? pht('Yes')
- : pht('No');
-
- $table[] = "| `{$key}` | {$unique} |";
+ $rows[] = array(
+ $key,
+ idx($column, 'unique') ? pht('Yes') : pht('No'),
+ );
}
- $table = implode("\n", $table);
- $out[] = $table;
-
- $out[] = pht(<<<EOTEXT
-Result Format
--------------
-
-The result format is a dictionary with several fields:
-
- - `data`: Contains the actual results, as a list of dictionaries.
- - `query`: Details about the query which was issued.
- - `cursor`: Information about how to issue another query to get the next
- (or previous) page of results. See "Paging and Limits" below.
-
-EOTEXT
- );
+ $columns_table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('Key'),
+ pht('Unique'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'pri',
+ 'wide',
+ ));
+
+
+ return id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Result Ordering'))
+ ->setCollapsed(true)
+ ->appendChild($this->buildRemarkup($orders_info))
+ ->appendChild($orders_table)
+ ->appendChild($this->buildRemarkup($columns_info))
+ ->appendChild($columns_table);
+ }
- $out[] = pht(<<<EOTEXT
-Fields
-------
+ private function buildFieldsBox(
+ PhabricatorApplicationSearchEngine $engine) {
-The `data` field of the result contains a list of results. Each result has
-some metadata and a `fields` key, which contains the primary object fields.
+ $info = pht(<<<EOTEXT
+Objects matching your query are returned as a list of dictionaries in the
+`data` property of the results. Each dictionary has some metadata and a
+`fields` key, which contains the information abou the object that most callers
+will be interested in.
For example, the results may look something like this:
-```lang=json
+```lang=json, name="Example Results"
{
...
"data": [
{
"id": 123,
"phid": "PHID-WXYZ-1111",
"fields": {
"name": "First Example Object",
"authorPHID": "PHID-USER-2222"
}
},
{
"id": 124,
"phid": "PHID-WXYZ-3333",
"fields": {
"name": "Second Example Object",
"authorPHID": "PHID-USER-4444"
}
},
...
]
...
}
```
This result structure is standardized across all search methods, but the
available fields differ from application to application.
These are the fields available on this object type:
-
EOTEXT
);
- $head_key = pht('Key');
- $head_type = pht('Type');
- $head_description = pht('Description');
-
$specs = $engine->getAllConduitFieldSpecifications();
- $table = array();
- $table[] = "| {$head_key} | {$head_type} | {$head_description} |";
- $table[] = '|-------------|--------------|---------------------|';
+ $rows = array();
foreach ($specs as $key => $spec) {
$type = idx($spec, 'type');
$description = idx($spec, 'description');
- $table[] = "| `{$key}` | `{$type}` | {$description} |";
+ $rows[] = array(
+ $key,
+ $type,
+ $description,
+ );
}
- $table = implode("\n", $table);
- $out[] = $table;
- $out[] = pht(<<<EOTEXT
-Paging and Limits
------------------
+ $table = id(new AphrontTableView($rows))
+ ->setHeaders(
+ array(
+ pht('Key'),
+ pht('Type'),
+ pht('Description'),
+ ))
+ ->setColumnClasses(
+ array(
+ 'pri',
+ 'mono',
+ 'wide',
+ ));
+
+ return id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Object Fields'))
+ ->setCollapsed(true)
+ ->appendChild($this->buildRemarkup($info))
+ ->appendChild($table);
+ }
+
+ private function buildPagingBox(
+ PhabricatorApplicationSearchEngine $engine) {
+ $info = pht(<<<EOTEXT
Queries are limited to returning 100 results at a time. If you want fewer
results than this, you can use `limit` to specify a smaller limit.
If you want more results, you'll need to make additional queries to retrieve
more pages of results.
The result structure contains a `cursor` key with information you'll need in
-order to fetch the next page. After an initial query, it will usually look
-something like this:
+order to fetch the next page of results. After an initial query, it will
+usually look something like this:
-```lang=json
+```lang=json, name="Example Cursor Result"
{
...
"cursor": {
"limit": 100,
"after": "1234",
"before": null,
"order": null
}
...
}
```
The `limit` and `order` fields are describing the effective limit and order the
query was executed with, and are usually not of much interest. The `after` and
`before` fields give you cursors which you can pass when making another API
call in order to get the next (or previous) page of results.
To get the next page of results, repeat your API call with all the same
parameters as the original call, but pass the `after` cursor you received from
the first call in the `after` parameter when making the second call.
If you do things correctly, you should get the second page of results, and
a cursor structure like this:
-```lang=json
+```lang=json, name="Second Result Page"
{
...
"cursor": {
"limit": 5,
"after": "4567",
"before": "7890",
"order": null
}
...
}
```
You can now continue to the third page of results by passing the new `after`
cursor to the `after` parameter in your third call, or return to the previous
page of results by passing the `before` cursor to the `before` parameter. This
might be useful if you are rendering a web UI for a user and want to provide
"Next Page" and "Previous Page" links.
If `after` is `null`, there is no next page of results available. Likewise,
if `before` is `null`, there are no previous results available.
-
EOTEXT
);
- $out = implode("\n\n", $out);
- return $out;
+ return id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Paging and Limits'))
+ ->setCollapsed(true)
+ ->appendChild($this->buildRemarkup($info));
}
+ private function buildRemarkup($remarkup) {
+ $viewer = $this->getViewer();
+
+ $view = new PHUIRemarkupView($viewer, $remarkup);
+
+ return id(new PHUIBoxView())
+ ->appendChild($view)
+ ->addPadding(PHUI::PADDING_LARGE);
+ }
}
diff --git a/src/applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php b/src/applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php
index 3ce010af6b..ea1db2c453 100644
--- a/src/applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php
+++ b/src/applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php
@@ -1,50 +1,54 @@
<?php
final class PhabricatorLiskSearchEngineExtension
extends PhabricatorSearchEngineExtension {
const EXTENSIONKEY = 'lisk';
public function isExtensionEnabled() {
return true;
}
public function getExtensionName() {
return pht('Lisk Builtin Properties');
}
+ public function getExtensionOrder() {
+ return 5000;
+ }
+
public function supportsObject($object) {
if (!($object instanceof LiskDAO)) {
return false;
}
if (!$object->getConfigOption(LiskDAO::CONFIG_TIMESTAMPS)) {
return false;
}
return true;
}
public function getFieldSpecificationsForConduit($object) {
return array(
'dateCreated' => array(
'type' => 'int',
'description' => pht(
'Epoch timestamp when the object was created.'),
),
'dateModified' => array(
'type' => 'int',
'description' => pht(
'Epoch timestamp when the object was last updated.'),
),
);
}
public function getFieldValuesForConduit($object) {
return array(
'dateCreated' => (int)$object->getDateCreated(),
'dateModified' => (int)$object->getDateModified(),
);
}
}
diff --git a/src/applications/search/engineextension/PhabricatorSearchEngineExtension.php b/src/applications/search/engineextension/PhabricatorSearchEngineExtension.php
index e019a676c7..1deba6063a 100644
--- a/src/applications/search/engineextension/PhabricatorSearchEngineExtension.php
+++ b/src/applications/search/engineextension/PhabricatorSearchEngineExtension.php
@@ -1,79 +1,79 @@
<?php
abstract class PhabricatorSearchEngineExtension extends Phobject {
private $viewer;
private $searchEngine;
final public function getExtensionKey() {
return $this->getPhobjectClassConstant('EXTENSIONKEY');
}
final public function setViewer($viewer) {
$this->viewer = $viewer;
return $this;
}
final public function getViewer() {
return $this->viewer;
}
final public function setSearchEngine(
PhabricatorApplicationSearchEngine $engine) {
$this->searchEngine = $engine;
return $this;
}
final public function getSearchEngine() {
return $this->searchEngine;
}
abstract public function isExtensionEnabled();
abstract public function getExtensionName();
abstract public function supportsObject($object);
public function getExtensionOrder() {
- return 5000;
+ return 7000;
}
public function getSearchFields($object) {
return array();
}
public function applyConstraintsToQuery(
$object,
$query,
PhabricatorSavedQuery $saved,
array $map) {
return;
}
public function getFieldSpecificationsForConduit($object) {
return array();
}
public function getFieldValuesForConduit($object) {
return array();
}
final public static function getAllExtensions() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getExtensionKey')
->setSortMethod('getExtensionOrder')
->execute();
}
final public static function getAllEnabledExtensions() {
$extensions = self::getAllExtensions();
foreach ($extensions as $key => $extension) {
if (!$extension->isExtensionEnabled()) {
unset($extensions[$key]);
}
}
return $extensions;
}
}
diff --git a/src/applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php b/src/applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php
index 99d01d63c3..45199c91f0 100644
--- a/src/applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php
+++ b/src/applications/spaces/engineextension/PhabricatorSpacesSearchEngineExtension.php
@@ -1,72 +1,72 @@
<?php
final class PhabricatorSpacesSearchEngineExtension
extends PhabricatorSearchEngineExtension {
const EXTENSIONKEY = 'spaces';
public function isExtensionEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorSpacesApplication');
}
public function getExtensionName() {
return pht('Support for Spaces');
}
public function getExtensionOrder() {
- return 3000;
+ return 4000;
}
public function supportsObject($object) {
return ($object instanceof PhabricatorSpacesInterface);
}
public function getSearchFields($object) {
$fields = array();
if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
$fields[] = id(new PhabricatorSpacesSearchField())
->setKey('spacePHIDs')
->setConduitKey('spaces')
->setAliases(array('space', 'spaces'))
->setLabel(pht('Spaces'))
->setDescription(
pht('Search for objects in certain spaces.'));
}
return $fields;
}
public function applyConstraintsToQuery(
$object,
$query,
PhabricatorSavedQuery $saved,
array $map) {
if (!empty($map['spacePHIDs'])) {
$query->withSpacePHIDs($map['spacePHIDs']);
} else {
// If the user doesn't search for objects in specific spaces, we
// default to "all active spaces you have permission to view".
$query->withSpaceIsArchived(false);
}
}
public function getFieldSpecificationsForConduit($object) {
return array(
'spacePHID' => array(
'type' => 'phid?',
'description' => pht(
'PHID of the policy space this object is part of.'),
),
);
}
public function getFieldValuesForConduit($object) {
return array(
'spacePHID' => $object->getSpacePHID(),
);
}
}
diff --git a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php
index ccd2e65dff..dc5838da6d 100644
--- a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php
+++ b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsSearchEngineExtension.php
@@ -1,54 +1,54 @@
<?php
final class PhabricatorSubscriptionsSearchEngineExtension
extends PhabricatorSearchEngineExtension {
const EXTENSIONKEY = 'subscriptions';
public function isExtensionEnabled() {
return PhabricatorApplication::isClassInstalled(
'PhabricatorSubscriptionsApplication');
}
public function getExtensionName() {
return pht('Support for Subscriptions');
}
public function getExtensionOrder() {
- return 1000;
+ return 2000;
}
public function supportsObject($object) {
return ($object instanceof PhabricatorSubscribableInterface);
}
public function applyConstraintsToQuery(
$object,
$query,
PhabricatorSavedQuery $saved,
array $map) {
if (!empty($map['subscriberPHIDs'])) {
$query->withEdgeLogicPHIDs(
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
$map['subscriberPHIDs']);
}
}
public function getSearchFields($object) {
$fields = array();
$fields[] = id(new PhabricatorSearchSubscribersField())
->setLabel(pht('Subscribers'))
->setKey('subscriberPHIDs')
->setConduitKey('subscribers')
->setAliases(array('subscriber', 'subscribers'))
->setDescription(
pht('Search for objects with certain subscribers.'));
return $fields;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Jan 19, 18:41 (1 w, 4 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1127521
Default Alt Text
(81 KB)
Attached To
Mode
rP Phorge
Attached
Detach File
Event Timeline
Log In to Comment