Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
index 1866815610..29a710209e 100644
--- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
+++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
@@ -1,433 +1,445 @@
<?php
final class PhabricatorDashboardPanelRenderingEngine extends Phobject {
const HEADER_MODE_NORMAL = 'normal';
const HEADER_MODE_NONE = 'none';
const HEADER_MODE_EDIT = 'edit';
private $panel;
private $panelPHID;
private $viewer;
private $enableAsyncRendering;
private $parentPanelPHIDs;
private $headerMode = self::HEADER_MODE_NORMAL;
private $movable;
private $panelHandle;
private $editMode;
private $contextObject;
private $panelKey;
public function setContextObject($object) {
$this->contextObject = $object;
return $this;
}
public function getContextObject() {
return $this->contextObject;
}
public function setPanelKey($panel_key) {
$this->panelKey = $panel_key;
return $this;
}
public function getPanelKey() {
return $this->panelKey;
}
public function setHeaderMode($header_mode) {
$this->headerMode = $header_mode;
return $this;
}
public function getHeaderMode() {
return $this->headerMode;
}
public function setPanelHandle(PhabricatorObjectHandle $panel_handle) {
$this->panelHandle = $panel_handle;
return $this;
}
public function getPanelHandle() {
return $this->panelHandle;
}
public function isEditMode() {
return $this->editMode;
}
public function setEditMode($mode) {
$this->editMode = $mode;
return $this;
}
/**
* Allow the engine to render the panel via Ajax.
*/
public function setEnableAsyncRendering($enable) {
$this->enableAsyncRendering = $enable;
return $this;
}
public function setParentPanelPHIDs(array $parents) {
$this->parentPanelPHIDs = $parents;
return $this;
}
public function getParentPanelPHIDs() {
return $this->parentPanelPHIDs;
}
public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer;
return $this;
}
public function getViewer() {
return $this->viewer;
}
public function setPanel(PhabricatorDashboardPanel $panel) {
$this->panel = $panel;
return $this;
}
public function setMovable($movable) {
$this->movable = $movable;
return $this;
}
public function getMovable() {
return $this->movable;
}
public function getPanel() {
return $this->panel;
}
public function setPanelPHID($panel_phid) {
$this->panelPHID = $panel_phid;
return $this;
}
public function getPanelPHID() {
return $this->panelPHID;
}
public function renderPanel() {
$panel = $this->getPanel();
if (!$panel) {
$handle = $this->getPanelHandle();
if ($handle->getPolicyFiltered()) {
return $this->renderErrorPanel(
pht('Restricted Panel'),
pht(
'You do not have permission to see this panel.'));
} else {
return $this->renderErrorPanel(
pht('Invalid Panel'),
pht(
'This panel is invalid or does not exist. It may have been '.
'deleted.'));
}
}
$panel_type = $panel->getImplementation();
if (!$panel_type) {
return $this->renderErrorPanel(
$panel->getName(),
pht(
'This panel has type "%s", but that panel type is not known to '.
'Phabricator.',
$panel->getPanelType()));
}
try {
$this->detectRenderingCycle($panel);
if ($this->enableAsyncRendering) {
if ($panel_type->shouldRenderAsync()) {
return $this->renderAsyncPanel();
}
}
return $this->renderNormalPanel();
} catch (Exception $ex) {
return $this->renderErrorPanel(
$panel->getName(),
pht(
'%s: %s',
phutil_tag('strong', array(), get_class($ex)),
$ex->getMessage()));
}
}
private function renderNormalPanel() {
$panel = $this->getPanel();
$panel_type = $panel->getImplementation();
$content = $panel_type->renderPanelContent(
$this->getViewer(),
$panel,
$this);
$header = $this->renderPanelHeader();
return $this->renderPanelDiv(
$content,
$header);
}
private function renderAsyncPanel() {
$context_phid = $this->getContextPHID();
$panel = $this->getPanel();
$panel_id = celerity_generate_unique_node_id();
Javelin::initBehavior(
'dashboard-async-panel',
array(
'panelID' => $panel_id,
'parentPanelPHIDs' => $this->getParentPanelPHIDs(),
'headerMode' => $this->getHeaderMode(),
'contextPHID' => $context_phid,
'panelKey' => $this->getPanelKey(),
'movable' => $this->getMovable(),
'uri' => '/dashboard/panel/render/'.$panel->getID().'/',
));
$header = $this->renderPanelHeader();
$content = id(new PHUIPropertyListView())
->addTextContent(pht('Loading...'));
return $this->renderPanelDiv(
$content,
$header,
$panel_id);
}
private function renderErrorPanel($title, $body) {
switch ($this->getHeaderMode()) {
case self::HEADER_MODE_NONE:
$header = null;
break;
case self::HEADER_MODE_EDIT:
$header = id(new PHUIHeaderView())
->setHeader($title);
$header = $this->addPanelHeaderActions($header);
break;
case self::HEADER_MODE_NORMAL:
default:
$header = id(new PHUIHeaderView())
->setHeader($title);
break;
}
$icon = id(new PHUIIconView())
->setIcon('fa-warning red msr');
$content = id(new PHUIBoxView())
->addClass('dashboard-box')
->addMargin(PHUI::MARGIN_LARGE)
->appendChild($icon)
->appendChild($body);
return $this->renderPanelDiv(
$content,
$header);
}
private function renderPanelDiv(
$content,
$header = null,
$id = null) {
require_celerity_resource('phabricator-dashboard-css');
$panel = $this->getPanel();
if (!$id) {
$id = celerity_generate_unique_node_id();
}
$box = new PHUIObjectBoxView();
$interface = 'PhabricatorApplicationSearchResultView';
if ($content instanceof $interface) {
if ($content->getObjectList()) {
$box->setObjectList($content->getObjectList());
}
if ($content->getTable()) {
$box->setTable($content->getTable());
}
if ($content->getContent()) {
$box->appendChild($content->getContent());
}
} else {
$box->appendChild($content);
}
$box
->setHeader($header)
->setID($id)
->addClass('dashboard-box')
->addSigil('dashboard-panel');
if ($this->getMovable()) {
$box->addSigil('panel-movable');
}
if ($panel) {
$box->setMetadata(
array(
'panelKey' => $this->getPanelKey(),
));
}
return $box;
}
private function renderPanelHeader() {
$panel = $this->getPanel();
switch ($this->getHeaderMode()) {
case self::HEADER_MODE_NONE:
$header = null;
break;
case self::HEADER_MODE_EDIT:
// In edit mode, include the panel monogram to make managing boards
// a little easier.
$header_text = pht('%s %s', $panel->getMonogram(), $panel->getName());
$header = id(new PHUIHeaderView())
->setHeader($header_text);
$header = $this->addPanelHeaderActions($header);
break;
case self::HEADER_MODE_NORMAL:
default:
$header = id(new PHUIHeaderView())
->setHeader($panel->getName());
$panel_type = $panel->getImplementation();
$header = $panel_type->adjustPanelHeader(
$this->getViewer(),
$panel,
$this,
$header);
break;
}
return $header;
}
private function addPanelHeaderActions(
PHUIHeaderView $header) {
$viewer = $this->getViewer();
$panel = $this->getPanel();
$context_phid = $this->getContextPHID();
$actions = array();
if ($panel) {
+ $panel_actions = $panel->newHeaderEditActions(
+ $viewer,
+ $context_phid);
+
+ if ($panel_actions) {
+ foreach ($panel_actions as $panel_action) {
+ $actions[] = $panel_action;
+ }
+ $actions[] = id(new PhabricatorActionView())
+ ->setType(PhabricatorActionView::TYPE_DIVIDER);
+ }
+
$panel_id = $panel->getID();
$edit_uri = "/dashboard/panel/edit/{$panel_id}/";
$params = array(
'contextPHID' => $context_phid,
);
$edit_uri = new PhutilURI($edit_uri, $params);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Panel'))
->setHref($edit_uri);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-window-maximize')
->setName(pht('View Panel Details'))
->setHref($panel->getURI());
}
if ($context_phid) {
$panel_phid = $this->getPanelPHID();
$remove_uri = urisprintf('/dashboard/adjust/remove/');
$params = array(
'contextPHID' => $context_phid,
'panelKey' => $this->getPanelKey(),
);
$remove_uri = new PhutilURI($remove_uri, $params);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-times')
->setHref($remove_uri)
->setName(pht('Remove Panel'))
->setWorkflow(true);
}
$dropdown_menu = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($actions as $action) {
$dropdown_menu->addAction($action);
}
$action_menu = id(new PHUIButtonView())
->setTag('a')
->setIcon('fa-cog')
->setText(pht('Manage Panel'))
->setDropdownMenu($dropdown_menu);
$header->addActionLink($action_menu);
return $header;
}
/**
* Detect graph cycles in panels, and deeply nested panels.
*
* This method throws if the current rendering stack is too deep or contains
* a cycle. This can happen if you embed layout panels inside each other,
* build a big stack of panels, or embed a panel in remarkup inside another
* panel. Generally, all of this stuff is ridiculous and we just want to
* shut it down.
*
* @param PhabricatorDashboardPanel Panel being rendered.
* @return void
*/
private function detectRenderingCycle(PhabricatorDashboardPanel $panel) {
if ($this->parentPanelPHIDs === null) {
throw new PhutilInvalidStateException('setParentPanelPHIDs');
}
$max_depth = 4;
if (count($this->parentPanelPHIDs) >= $max_depth) {
throw new Exception(
pht(
'To render more than %s levels of panels nested inside other '.
'panels, purchase a subscription to Phabricator Gold.',
new PhutilNumber($max_depth)));
}
if (in_array($panel->getPHID(), $this->parentPanelPHIDs)) {
throw new Exception(
pht(
'You awake in a twisting maze of mirrors, all alike. '.
'You are likely to be eaten by a graph cycle. '.
'Should you escape alive, you resolve to be more careful about '.
'putting dashboard panels inside themselves.'));
}
}
private function getContextPHID() {
$context = $this->getContextObject();
if ($context) {
return $context->getPHID();
}
return null;
}
}
diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php
index 8f9134e642..d6311e7765 100644
--- a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php
+++ b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php
@@ -1,66 +1,73 @@
<?php
abstract class PhabricatorDashboardPanelType extends Phobject {
abstract public function getPanelTypeKey();
abstract public function getPanelTypeName();
abstract public function getPanelTypeDescription();
abstract public function getIcon();
abstract public function renderPanelContent(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine);
public function initializeFieldsFromRequest(
PhabricatorDashboardPanel $panel,
PhabricatorCustomFieldList $field_list,
AphrontRequest $request) {
return;
}
/**
* Should this panel pull content in over AJAX?
*
* Normally, panels use AJAX to render their content. This makes the page
* interactable sooner, allows panels to render in parallel, and prevents one
* slow panel from slowing everything down.
*
* However, some panels are very cheap to build (i.e., no expensive service
* calls or complicated rendering). In these cases overall performance can be
* improved by disabling async rendering so the panel rendering happens in the
* same process.
*
* @return bool True to enable asynchronous rendering when appropriate.
*/
public function shouldRenderAsync() {
return true;
}
public function adjustPanelHeader(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine,
PHUIHeaderView $header) {
return $header;
}
public static function getAllPanelTypes() {
return id(new PhutilClassMapQuery())
->setAncestorClass(__CLASS__)
->setUniqueMethod('getPanelTypeKey')
->execute();
}
final public function getEditEngineFields(PhabricatorDashboardPanel $panel) {
return $this->newEditEngineFields($panel);
}
abstract protected function newEditEngineFields(
PhabricatorDashboardPanel $panel);
public function getSubpanelPHIDs(PhabricatorDashboardPanel $panel) {
return array();
}
+ public function newHeaderEditActions(
+ PhabricatorDashboardPanel $panel,
+ PhabricatorUser $viewer,
+ $context_phid) {
+ return array();
+ }
+
}
diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php
index 1db662a993..9e0da25c43 100644
--- a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php
+++ b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php
@@ -1,239 +1,261 @@
<?php
final class PhabricatorDashboardQueryPanelType
extends PhabricatorDashboardPanelType {
public function getPanelTypeKey() {
return 'query';
}
public function getPanelTypeName() {
return pht('Query Panel');
}
public function getIcon() {
return 'fa-search';
}
public function getPanelTypeDescription() {
return pht(
'Show results of a search query, like the most recently filed tasks or '.
'revisions you need to review.');
}
protected function newEditEngineFields(PhabricatorDashboardPanel $panel) {
$application_field =
id(new PhabricatorDashboardQueryPanelApplicationEditField())
->setKey('class')
->setLabel(pht('Search For'))
->setTransactionType(
PhabricatorDashboardQueryPanelApplicationTransaction::TRANSACTIONTYPE)
->setValue($panel->getProperty('class', ''));
$application_id = $application_field->getControlID();
$query_field =
id(new PhabricatorDashboardQueryPanelQueryEditField())
->setKey('key')
->setLabel(pht('Query'))
->setApplicationControlID($application_id)
->setTransactionType(
PhabricatorDashboardQueryPanelQueryTransaction::TRANSACTIONTYPE)
->setValue($panel->getProperty('key', ''));
$limit_field = id(new PhabricatorIntEditField())
->setKey('limit')
->setLabel(pht('Limit'))
->setTransactionType(
PhabricatorDashboardQueryPanelLimitTransaction::TRANSACTIONTYPE)
->setValue($panel->getProperty('limit'));
return array(
$application_field,
$query_field,
$limit_field,
);
}
public function initializeFieldsFromRequest(
PhabricatorDashboardPanel $panel,
PhabricatorCustomFieldList $field_list,
AphrontRequest $request) {
$map = array();
if (strlen($request->getStr('engine'))) {
$map['class'] = $request->getStr('engine');
}
if (strlen($request->getStr('query'))) {
$map['key'] = $request->getStr('query');
}
$full_map = array();
foreach ($map as $key => $value) {
$full_map["std:dashboard:core:{$key}"] = $value;
}
foreach ($field_list->getFields() as $field) {
$field_key = $field->getFieldKey();
if (isset($full_map[$field_key])) {
$field->setValueFromStorage($full_map[$field_key]);
}
}
}
public function renderPanelContent(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine) {
$engine = $this->getSearchEngine($panel);
$engine->setViewer($viewer);
$engine->setContext(PhabricatorApplicationSearchEngine::CONTEXT_PANEL);
$key = $panel->getProperty('key');
if ($engine->isBuiltinQuery($key)) {
$saved = $engine->buildSavedQueryFromBuiltin($key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withEngineClassNames(array(get_class($engine)))
->withQueryKeys(array($key))
->executeOne();
}
if (!$saved) {
throw new Exception(
pht(
'Query "%s" is unknown to application search engine "%s"!',
$key,
get_class($engine)));
}
$query = $engine->buildQueryFromSavedQuery($saved);
$pager = $engine->newPagerForSavedQuery($saved);
if ($panel->getProperty('limit')) {
$limit = (int)$panel->getProperty('limit');
if ($pager->getPageSize() !== 0xFFFF) {
$pager->setPageSize($limit);
}
}
$query->setReturnPartialResultsOnOverheat(true);
$results = $engine->executeQuery($query, $pager);
$results_view = $engine->renderResults($results, $saved);
$is_overheated = $query->getIsOverheated();
$overheated_view = null;
if ($is_overheated) {
$content = $results_view->getContent();
$overheated_message =
PhabricatorApplicationSearchController::newOverheatedError(
(bool)$results);
$overheated_warning = id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setTitle(pht('Query Overheated'))
->setErrors(
array(
$overheated_message,
));
$overheated_box = id(new PHUIBoxView())
->addClass('mmt mmb')
->appendChild($overheated_warning);
$content = array($content, $overheated_box);
$results_view->setContent($content);
}
// TODO: A small number of queries, including "Notifications" and "Search",
// use an offset pager which has a slightly different API. Some day, we
// should unify these.
if ($pager instanceof PHUIPagerView) {
$has_more = $pager->getHasMorePages();
} else {
$has_more = $pager->getHasMoreResults();
}
if ($has_more) {
$item_list = $results_view->getObjectList();
$more_href = $engine->getQueryResultsPageURI($key);
if ($item_list) {
$item_list->newTailButton()
->setHref($more_href);
} else {
// For search engines that do not return an object list, add a fake
// one to the end so we can render a "View All Results" button that
// looks like it does in normal applications. At time of writing,
// several major applications like Maniphest (which has group headers)
// and Feed (which uses custom rendering) don't return simple lists.
$content = $results_view->getContent();
$more_list = id(new PHUIObjectItemListView())
->setAllowEmptyList(true);
$more_list->newTailButton()
->setHref($more_href);
$content = array($content, $more_list);
$results_view->setContent($content);
}
}
return $results_view;
}
public function adjustPanelHeader(
PhabricatorUser $viewer,
PhabricatorDashboardPanel $panel,
PhabricatorDashboardPanelRenderingEngine $engine,
PHUIHeaderView $header) {
$search_engine = $this->getSearchEngine($panel);
$key = $panel->getProperty('key');
$href = $search_engine->getQueryResultsPageURI($key);
$icon = id(new PHUIIconView())
->setIcon('fa-search');
$button = id(new PHUIButtonView())
->setTag('a')
->setText(pht('View All'))
->setIcon($icon)
->setHref($href)
->setColor(PHUIButtonView::GREY);
$header->addActionLink($button);
return $header;
}
private function getSearchEngine(PhabricatorDashboardPanel $panel) {
$class = $panel->getProperty('class');
$engine = PhabricatorApplicationSearchEngine::getEngineByClassName($class);
if (!$engine) {
throw new Exception(
pht(
'The application search engine "%s" is not known to Phabricator!',
$class));
}
if (!$engine->canUseInPanelContext()) {
throw new Exception(
pht(
'Application search engines of class "%s" can not be used to build '.
'dashboard panels.',
$class));
}
return $engine;
}
+ public function newHeaderEditActions(
+ PhabricatorDashboardPanel $panel,
+ PhabricatorUser $viewer,
+ $context_phid) {
+ $actions = array();
+
+ $engine = $this->getSearchEngine($panel);
+
+ $customize_uri = $engine->getCustomizeURI(
+ $panel->getProperty('key'),
+ $panel->getPHID(),
+ $context_phid);
+
+ $actions[] = id(new PhabricatorActionView())
+ ->setIcon('fa-pencil-square-o')
+ ->setName(pht('Customize Query'))
+ ->setWorkflow(true)
+ ->setHref($customize_uri);
+
+ return $actions;
+ }
+
}
diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
index a8bf58e8ab..4173b208ba 100644
--- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
+++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php
@@ -1,176 +1,185 @@
<?php
/**
* An individual dashboard panel.
*/
final class PhabricatorDashboardPanel
extends PhabricatorDashboardDAO
implements
PhabricatorApplicationTransactionInterface,
PhabricatorPolicyInterface,
PhabricatorFlaggableInterface,
PhabricatorDestructibleInterface,
PhabricatorFulltextInterface,
PhabricatorFerretInterface,
PhabricatorDashboardPanelContainerInterface {
protected $name;
protected $panelType;
protected $viewPolicy;
protected $editPolicy;
protected $authorPHID;
protected $isArchived = 0;
protected $properties = array();
public static function initializeNewPanel(PhabricatorUser $actor) {
return id(new PhabricatorDashboardPanel())
->setName('')
->setAuthorPHID($actor->getPHID())
->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
->setEditPolicy($actor->getPHID());
}
protected function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'properties' => self::SERIALIZATION_JSON,
),
self::CONFIG_COLUMN_SCHEMA => array(
'name' => 'sort255',
'panelType' => 'text64',
'authorPHID' => 'phid',
'isArchived' => 'bool',
),
) + parent::getConfiguration();
}
public function getPHIDType() {
return PhabricatorDashboardPanelPHIDType::TYPECONST;
}
public function getProperty($key, $default = null) {
return idx($this->properties, $key, $default);
}
public function setProperty($key, $value) {
$this->properties[$key] = $value;
return $this;
}
public function getMonogram() {
return 'W'.$this->getID();
}
public function getURI() {
return '/'.$this->getMonogram();
}
public function getPanelTypes() {
$panel_types = PhabricatorDashboardPanelType::getAllPanelTypes();
$panel_types = mpull($panel_types, 'getPanelTypeName', 'getPanelTypeKey');
asort($panel_types);
$panel_types = (array('' => pht('(All Types)')) + $panel_types);
return $panel_types;
}
public function getStatuses() {
$statuses =
array(
'' => pht('(All Panels)'),
'active' => pht('Active Panels'),
'archived' => pht('Archived Panels'),
);
return $statuses;
}
public function getImplementation() {
return idx(
PhabricatorDashboardPanelType::getAllPanelTypes(),
$this->getPanelType());
}
public function requireImplementation() {
$impl = $this->getImplementation();
if (!$impl) {
throw new Exception(
pht(
'Attempting to use a panel in a way that requires an '.
'implementation, but the panel implementation ("%s") is unknown to '.
'Phabricator.',
$this->getPanelType()));
}
return $impl;
}
public function getEditEngineFields() {
return $this->requireImplementation()->getEditEngineFields($this);
}
+ public function newHeaderEditActions(
+ PhabricatorUser $viewer,
+ $context_phid) {
+ return $this->requireImplementation()->newHeaderEditActions(
+ $this,
+ $viewer,
+ $context_phid);
+ }
+
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
public function getApplicationTransactionEditor() {
return new PhabricatorDashboardPanelTransactionEditor();
}
public function getApplicationTransactionTemplate() {
return new PhabricatorDashboardPanelTransaction();
}
/* -( PhabricatorPolicyInterface )----------------------------------------- */
public function getCapabilities() {
return array(
PhabricatorPolicyCapability::CAN_VIEW,
PhabricatorPolicyCapability::CAN_EDIT,
);
}
public function getPolicy($capability) {
switch ($capability) {
case PhabricatorPolicyCapability::CAN_VIEW:
return $this->getViewPolicy();
case PhabricatorPolicyCapability::CAN_EDIT:
return $this->getEditPolicy();
}
}
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
return false;
}
/* -( PhabricatorDestructibleInterface )----------------------------------- */
public function destroyObjectPermanently(
PhabricatorDestructionEngine $engine) {
$this->openTransaction();
$this->delete();
$this->saveTransaction();
}
/* -( PhabricatorDashboardPanelContainerInterface )------------------------ */
public function getDashboardPanelContainerPanelPHIDs() {
return $this->requireImplementation()->getSubpanelPHIDs($this);
}
/* -( PhabricatorFulltextInterface )--------------------------------------- */
public function newFulltextEngine() {
return new PhabricatorDashboardPanelFulltextEngine();
}
/* -( PhabricatorFerretInterface )----------------------------------------- */
public function newFerretEngine() {
return new PhabricatorDashboardPanelFerretEngine();
}
}
diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php
index 183aea50dc..81373ee190 100644
--- a/src/applications/search/controller/PhabricatorApplicationSearchController.php
+++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php
@@ -1,988 +1,1094 @@
<?php
final class PhabricatorApplicationSearchController
extends PhabricatorSearchBaseController {
private $searchEngine;
private $navigation;
private $queryKey;
private $preface;
private $activeQuery;
public function setPreface($preface) {
$this->preface = $preface;
return $this;
}
public function getPreface() {
return $this->preface;
}
public function setQueryKey($query_key) {
$this->queryKey = $query_key;
return $this;
}
protected function getQueryKey() {
return $this->queryKey;
}
public function setNavigation(AphrontSideNavFilterView $navigation) {
$this->navigation = $navigation;
return $this;
}
protected function getNavigation() {
return $this->navigation;
}
public function setSearchEngine(
PhabricatorApplicationSearchEngine $search_engine) {
$this->searchEngine = $search_engine;
return $this;
}
protected function getSearchEngine() {
return $this->searchEngine;
}
protected function getActiveQuery() {
if (!$this->activeQuery) {
throw new Exception(pht('There is no active query yet.'));
}
return $this->activeQuery;
}
protected function validateDelegatingController() {
$parent = $this->getDelegatingController();
if (!$parent) {
throw new Exception(
pht('You must delegate to this controller, not invoke it directly.'));
}
$engine = $this->getSearchEngine();
if (!$engine) {
throw new PhutilInvalidStateException('setEngine');
}
$engine->setViewer($this->getRequest()->getUser());
$parent = $this->getDelegatingController();
}
public function processRequest() {
$this->validateDelegatingController();
$query_action = $this->getRequest()->getURIData('queryAction');
if ($query_action == 'export') {
return $this->processExportRequest();
}
+ if ($query_action === 'customize') {
+ return $this->processCustomizeRequest();
+ }
+
$key = $this->getQueryKey();
if ($key == 'edit') {
return $this->processEditRequest();
} else {
return $this->processSearchRequest();
}
}
private function processSearchRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$user = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
if ($request->isFormPost()) {
$saved_query = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved_query);
return id(new AphrontRedirectResponse())->setURI(
$engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
}
$named_query = null;
$run_query = true;
$query_key = $this->queryKey;
if ($this->queryKey == 'advanced') {
$run_query = false;
$query_key = $request->getStr('query');
} else if (!strlen($this->queryKey)) {
$found_query_data = false;
if ($request->isHTTPGet() || $request->isQuicksand()) {
// If this is a GET request and it has some query data, don't
// do anything unless it's only before= or after=. We'll build and
// execute a query from it below. This allows external tools to build
// URIs like "/query/?users=a,b".
$pt_data = $request->getPassthroughRequestData();
$exempt = array(
'before' => true,
'after' => true,
'nux' => true,
'overheated' => true,
);
foreach ($pt_data as $pt_key => $pt_value) {
if (isset($exempt[$pt_key])) {
continue;
}
$found_query_data = true;
break;
}
}
if (!$found_query_data) {
// Otherwise, there's no query data so just run the user's default
// query for this application.
$query_key = $engine->getDefaultQueryKey();
}
}
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($user)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved_query) {
return new Aphront404Response();
}
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
} else {
$saved_query = $engine->buildSavedQueryFromRequest($request);
// Save the query to generate a query key, so "Save Custom Query..." and
// other features like "Bulk Edit" and "Export Data" work correctly.
$engine->saveQuery($saved_query);
}
$this->activeQuery = $saved_query;
$nav->selectFilter(
'query/'.$saved_query->getQueryKey(),
'query/advanced');
$form = id(new AphrontFormView())
->setUser($user)
->setAction($request->getPath());
$engine->buildSearchForm($form, $saved_query);
$errors = $engine->getErrors();
if ($errors) {
$run_query = false;
}
$submit = id(new AphrontFormSubmitControl())
->setValue(pht('Search'));
if ($run_query && !$named_query && $user->isLoggedIn()) {
$save_button = id(new PHUIButtonView())
->setTag('a')
->setHref('/search/edit/key/'.$saved_query->getQueryKey().'/')
->setText(pht('Save Query'))
->setIcon('fa-floppy-o');
$submit->addButton($save_button);
}
$form->appendChild($submit);
$body = array();
if ($this->getPreface()) {
$body[] = $this->getPreface();
}
if ($named_query) {
$title = $named_query->getQueryName();
} else {
$title = pht('Advanced Search');
}
$header = id(new PHUIHeaderView())
->setHeader($title)
->setProfileHeader(true);
$box = id(new PHUIObjectBoxView())
->setHeader($header)
->addClass('application-search-results');
if ($run_query || $named_query) {
$box->setShowHide(
pht('Edit Query'),
pht('Hide Query'),
$form,
$this->getApplicationURI('query/advanced/?query='.$query_key),
(!$named_query ? true : false));
} else {
$box->setForm($form);
}
$body[] = $box;
$more_crumbs = null;
if ($run_query) {
$exec_errors = array();
$box->setAnchor(
id(new PhabricatorAnchorView())
->setAnchorName('R'));
try {
$engine->setRequest($request);
$query = $engine->buildQueryFromSavedQuery($saved_query);
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->readFromRequest($request);
$query->setReturnPartialResultsOnOverheat(true);
$objects = $engine->executeQuery($query, $pager);
$force_nux = $request->getBool('nux');
if (!$objects || $force_nux) {
$nux_view = $this->renderNewUserView($engine, $force_nux);
} else {
$nux_view = null;
}
$is_overflowing =
$pager->willShowPagingControls() &&
$engine->getResultBucket($saved_query);
$force_overheated = $request->getBool('overheated');
$is_overheated = $query->getIsOverheated() || $force_overheated;
if ($nux_view) {
$box->appendChild($nux_view);
} else {
$list = $engine->renderResults($objects, $saved_query);
if (!($list instanceof PhabricatorApplicationSearchResultView)) {
throw new Exception(
pht(
'SearchEngines must render a "%s" object, but this engine '.
'(of class "%s") rendered something else ("%s").',
'PhabricatorApplicationSearchResultView',
get_class($engine),
phutil_describe_type($list)));
}
if ($list->getObjectList()) {
$box->setObjectList($list->getObjectList());
}
if ($list->getTable()) {
$box->setTable($list->getTable());
}
if ($list->getInfoView()) {
$box->setInfoView($list->getInfoView());
}
if ($is_overflowing) {
$box->appendChild($this->newOverflowingView());
}
if ($list->getContent()) {
$box->appendChild($list->getContent());
}
if ($is_overheated) {
$box->appendChild($this->newOverheatedView($objects));
}
$result_header = $list->getHeader();
if ($result_header) {
$box->setHeader($result_header);
$header = $result_header;
}
$actions = $list->getActions();
if ($actions) {
foreach ($actions as $action) {
$header->addActionLink($action);
}
}
$use_actions = $engine->newUseResultsActions($saved_query);
// TODO: Eventually, modularize all this stuff.
$builtin_use_actions = $this->newBuiltinUseActions();
if ($builtin_use_actions) {
foreach ($builtin_use_actions as $builtin_use_action) {
$use_actions[] = $builtin_use_action;
}
}
if ($use_actions) {
$use_dropdown = $this->newUseResultsDropdown(
$saved_query,
$use_actions);
$header->addActionLink($use_dropdown);
}
$more_crumbs = $list->getCrumbs();
if ($pager->willShowPagingControls()) {
$pager_box = id(new PHUIBoxView())
->setColor(PHUIBoxView::GREY)
->addClass('application-search-pager')
->appendChild($pager);
$body[] = $pager_box;
}
}
} catch (PhabricatorTypeaheadInvalidTokenException $ex) {
$exec_errors[] = pht(
'This query specifies an invalid parameter. Review the '.
'query parameters and correct errors.');
} catch (PhutilSearchQueryCompilerSyntaxException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorSearchConstraintException $ex) {
$exec_errors[] = $ex->getMessage();
} catch (PhabricatorInvalidQueryCursorException $ex) {
$exec_errors[] = $ex->getMessage();
}
// The engine may have encountered additional errors during rendering;
// merge them in and show everything.
foreach ($engine->getErrors() as $error) {
$exec_errors[] = $error;
}
$errors = $exec_errors;
}
if ($errors) {
$box->setFormErrors($errors, pht('Query Errors'));
}
$crumbs = $parent
->buildApplicationCrumbs()
->setBorder(true);
if ($more_crumbs) {
$query_uri = $engine->getQueryResultsPageURI($saved_query->getQueryKey());
$crumbs->addTextCrumb($title, $query_uri);
foreach ($more_crumbs as $crumb) {
$crumbs->addCrumb($crumb);
}
} else {
$crumbs->addTextCrumb($title);
}
require_celerity_resource('application-search-view-css');
return $this->newPage()
->setTitle(pht('Query: %s', $title))
->setCrumbs($crumbs)
->setNavigation($nav)
->addClass('application-search-view')
->appendChild($body);
}
private function processExportRequest() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$request = $this->getRequest();
if (!$this->canExport()) {
return new Aphront404Response();
}
$query_key = $this->getQueryKey();
if ($engine->isBuiltinQuery($query_key)) {
$saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
} else if ($query_key) {
$saved_query = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
} else {
$saved_query = null;
}
if (!$saved_query) {
return new Aphront404Response();
}
$cancel_uri = $engine->getQueryResultsPageURI($query_key);
$named_query = idx($engine->loadEnabledNamedQueries(), $query_key);
if ($named_query) {
$filename = $named_query->getQueryName();
$sheet_title = $named_query->getQueryName();
} else {
$filename = $engine->getResultTypeDescription();
$sheet_title = $engine->getResultTypeDescription();
}
$filename = phutil_utf8_strtolower($filename);
$filename = PhabricatorFile::normalizeFileName($filename);
$all_formats = PhabricatorExportFormat::getAllExportFormats();
$available_options = array();
$unavailable_options = array();
$formats = array();
$unavailable_formats = array();
foreach ($all_formats as $key => $format) {
if ($format->isExportFormatEnabled()) {
$available_options[$key] = $format->getExportFormatName();
$formats[$key] = $format;
} else {
$unavailable_options[$key] = pht(
'%s (Not Available)',
$format->getExportFormatName());
$unavailable_formats[$key] = $format;
}
}
$format_options = $available_options + $unavailable_options;
// Try to default to the format the user used last time. If you just
// exported to Excel, you probably want to export to Excel again.
$format_key = $this->readExportFormatPreference();
if (!isset($formats[$format_key])) {
$format_key = head_key($format_options);
}
// Check if this is a large result set or not. If we're exporting a
// large amount of data, we'll build the actual export file in the daemons.
$threshold = 1000;
$query = $engine->buildQueryFromSavedQuery($saved_query);
$pager = $engine->newPagerForSavedQuery($saved_query);
$pager->setPageSize($threshold + 1);
$objects = $engine->executeQuery($query, $pager);
$object_count = count($objects);
$is_large_export = ($object_count > $threshold);
$errors = array();
$e_format = null;
if ($request->isFormPost()) {
$format_key = $request->getStr('format');
if (isset($unavailable_formats[$format_key])) {
$unavailable = $unavailable_formats[$format_key];
$instructions = $unavailable->getInstallInstructions();
$markup = id(new PHUIRemarkupView($viewer, $instructions))
->setRemarkupOption(
PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS,
false);
return $this->newDialog()
->setTitle(pht('Export Format Not Available'))
->appendChild($markup)
->addCancelButton($cancel_uri, pht('Done'));
}
$format = idx($formats, $format_key);
if (!$format) {
$e_format = pht('Invalid');
$errors[] = pht('Choose a valid export format.');
}
if (!$errors) {
$this->writeExportFormatPreference($format_key);
$export_engine = id(new PhabricatorExportEngine())
->setViewer($viewer)
->setSearchEngine($engine)
->setSavedQuery($saved_query)
->setTitle($sheet_title)
->setFilename($filename)
->setExportFormat($format);
if ($is_large_export) {
$job = $export_engine->newBulkJob($request);
return id(new AphrontRedirectResponse())
->setURI($job->getMonitorURI());
} else {
$file = $export_engine->exportFile();
return $file->newDownloadResponse();
}
}
}
$export_form = id(new AphrontFormView())
->setViewer($viewer)
->appendControl(
id(new AphrontFormSelectControl())
->setName('format')
->setLabel(pht('Format'))
->setError($e_format)
->setValue($format_key)
->setOptions($format_options));
if ($is_large_export) {
$submit_button = pht('Continue');
} else {
$submit_button = pht('Download Data');
}
return $this->newDialog()
->setTitle(pht('Export Results'))
->setErrors($errors)
->appendForm($export_form)
->addCancelButton($cancel_uri)
->addSubmitButton($submit_button);
}
private function processEditRequest() {
$parent = $this->getDelegatingController();
$request = $this->getRequest();
$viewer = $request->getUser();
$engine = $this->getSearchEngine();
$nav = $this->getNavigation();
if (!$nav) {
$nav = $this->buildNavigation();
}
$named_queries = $engine->loadAllNamedQueries();
$can_global = $viewer->getIsAdmin();
$groups = array(
'personal' => array(
'name' => pht('Personal Saved Queries'),
'items' => array(),
'edit' => true,
),
'global' => array(
'name' => pht('Global Saved Queries'),
'items' => array(),
'edit' => $can_global,
),
);
foreach ($named_queries as $named_query) {
if ($named_query->isGlobal()) {
$group = 'global';
} else {
$group = 'personal';
}
$groups[$group]['items'][] = $named_query;
}
$default_key = $engine->getDefaultQueryKey();
$lists = array();
foreach ($groups as $group) {
$lists[] = $this->newQueryListView(
$group['name'],
$group['items'],
$default_key,
$group['edit']);
}
$crumbs = $parent
->buildApplicationCrumbs()
->addTextCrumb(pht('Saved Queries'), $engine->getQueryManagementURI())
->setBorder(true);
$nav->selectFilter('query/edit');
$header = id(new PHUIHeaderView())
->setHeader(pht('Saved Queries'))
->setProfileHeader(true);
$view = id(new PHUITwoColumnView())
->setHeader($header)
->setFooter($lists);
return $this->newPage()
->setTitle(pht('Saved Queries'))
->setCrumbs($crumbs)
->setNavigation($nav)
->appendChild($view);
}
private function newQueryListView(
$list_name,
array $named_queries,
$default_key,
$can_edit) {
$engine = $this->getSearchEngine();
$viewer = $this->getViewer();
$list = id(new PHUIObjectItemListView())
->setViewer($viewer);
if ($can_edit) {
$list_id = celerity_generate_unique_node_id();
$list->setID($list_id);
Javelin::initBehavior(
'search-reorder-queries',
array(
'listID' => $list_id,
'orderURI' => '/search/order/'.get_class($engine).'/',
));
}
foreach ($named_queries as $named_query) {
$class = get_class($engine);
$key = $named_query->getQueryKey();
$item = id(new PHUIObjectItemView())
->setHeader($named_query->getQueryName())
->setHref($engine->getQueryResultsPageURI($key));
if ($named_query->getIsDisabled()) {
if ($can_edit) {
$item->setDisabled(true);
} else {
// If an item is disabled and you don't have permission to edit it,
// just skip it.
continue;
}
}
if ($can_edit) {
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
$icon = 'fa-plus';
$disable_name = pht('Enable');
} else {
$icon = 'fa-times';
if ($named_query->getIsBuiltin()) {
$disable_name = pht('Disable');
} else {
$disable_name = pht('Delete');
}
}
if ($named_query->getID()) {
$disable_href = '/search/delete/id/'.$named_query->getID().'/';
} else {
$disable_href = '/search/delete/key/'.$key.'/'.$class.'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($icon)
->setHref($disable_href)
->setRenderNameAsTooltip(true)
->setName($disable_name)
->setWorkflow(true));
}
$default_disabled = $named_query->getIsDisabled();
$default_icon = 'fa-thumb-tack';
if ($default_key === $key) {
$default_color = 'green';
} else {
$default_color = null;
}
$item->addAction(
id(new PHUIListItemView())
->setIcon("{$default_icon} {$default_color}")
->setHref('/search/default/'.$key.'/'.$class.'/')
->setRenderNameAsTooltip(true)
->setName(pht('Make Default'))
->setWorkflow(true)
->setDisabled($default_disabled));
if ($can_edit) {
if ($named_query->getIsBuiltin()) {
$edit_icon = 'fa-lock lightgreytext';
$edit_disabled = true;
$edit_name = pht('Builtin');
$edit_href = null;
} else {
$edit_icon = 'fa-pencil';
$edit_disabled = false;
$edit_name = pht('Edit');
$edit_href = '/search/edit/id/'.$named_query->getID().'/';
}
$item->addAction(
id(new PHUIListItemView())
->setIcon($edit_icon)
->setHref($edit_href)
->setRenderNameAsTooltip(true)
->setName($edit_name)
->setDisabled($edit_disabled));
}
$item->setGrippable($can_edit);
$item->addSigil('named-query');
$item->setMetadata(
array(
'queryKey' => $named_query->getQueryKey(),
));
$list->addItem($item);
}
$list->setNoDataString(pht('No saved queries.'));
return id(new PHUIObjectBoxView())
->setHeaderText($list_name)
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
->setObjectList($list);
}
public function buildApplicationMenu() {
$menu = $this->getDelegatingController()
->buildApplicationMenu();
if ($menu instanceof PHUIApplicationMenuView) {
$menu->setSearchEngine($this->getSearchEngine());
}
return $menu;
}
private function buildNavigation() {
$viewer = $this->getViewer();
$engine = $this->getSearchEngine();
$nav = id(new AphrontSideNavFilterView())
->setUser($viewer)
->setBaseURI(new PhutilURI($this->getApplicationURI()));
$engine->addNavigationItems($nav->getMenu());
return $nav;
}
private function renderNewUserView(
PhabricatorApplicationSearchEngine $engine,
$force_nux) {
// Don't render NUX if the user has clicked away from the default page.
if (strlen($this->getQueryKey())) {
return null;
}
// Don't put NUX in panels because it would be weird.
if ($engine->isPanelContext()) {
return null;
}
// Try to render the view itself first, since this should be very cheap
// (just returning some text).
$nux_view = $engine->renderNewUserView();
if (!$nux_view) {
return null;
}
$query = $engine->newQuery();
if (!$query) {
return null;
}
// Try to load any object at all. If we can, the application has seen some
// use so we just render the normal view.
if (!$force_nux) {
$object = $query
->setViewer(PhabricatorUser::getOmnipotentUser())
->setLimit(1)
->setReturnPartialResultsOnOverheat(true)
->execute();
if ($object) {
return null;
}
}
return $nux_view;
}
private function newUseResultsDropdown(
PhabricatorSavedQuery $query,
array $dropdown_items) {
$viewer = $this->getViewer();
$action_list = id(new PhabricatorActionListView())
->setViewer($viewer);
foreach ($dropdown_items as $dropdown_item) {
$action_list->addAction($dropdown_item);
}
return id(new PHUIButtonView())
->setTag('a')
->setHref('#')
->setText(pht('Use Results'))
->setIcon('fa-bars')
->setDropdownMenu($action_list)
->addClass('dropdown');
}
private function newOverflowingView() {
$message = pht(
'The query matched more than one page of results. Results are '.
'paginated before bucketing, so later pages may contain additional '.
'results in any bucket.');
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Buckets Overflowing'))
->setErrors(
array(
$message,
));
}
public static function newOverheatedError($has_results) {
$overheated_link = phutil_tag(
'a',
array(
'href' => 'https://phurl.io/u/overheated',
'target' => '_blank',
),
pht('Learn More'));
if ($has_results) {
$message = pht(
'This query took too long, so only some results are shown. %s',
$overheated_link);
} else {
$message = pht(
'This query took too long. %s',
$overheated_link);
}
return $message;
}
private function newOverheatedView(array $results) {
$message = self::newOverheatedError((bool)$results);
return id(new PHUIInfoView())
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
->setFlush(true)
->setTitle(pht('Query Overheated'))
->setErrors(
array(
$message,
));
}
private function newBuiltinUseActions() {
$actions = array();
$request = $this->getRequest();
$viewer = $request->getUser();
$is_dev = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');
$engine = $this->getSearchEngine();
$engine_class = get_class($engine);
$query_key = $this->getActiveQuery()->getQueryKey();
$can_use = $engine->canUseInPanelContext();
$is_installed = PhabricatorApplication::isClassInstalledForViewer(
'PhabricatorDashboardApplication',
$viewer);
if ($can_use && $is_installed) {
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-dashboard')
->setName(pht('Add to Dashboard'))
->setWorkflow(true)
->setHref("/dashboard/panel/install/{$engine_class}/{$query_key}/");
}
if ($this->canExport()) {
$export_uri = $engine->getExportURI($query_key);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-download')
->setName(pht('Export Data'))
->setWorkflow(true)
->setHref($export_uri);
}
if ($is_dev) {
$engine = $this->getSearchEngine();
$nux_uri = $engine->getQueryBaseURI();
$nux_uri = id(new PhutilURI($nux_uri))
->replaceQueryParam('nux', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-user-plus')
->setName(pht('DEV: New User State'))
->setHref($nux_uri);
}
if ($is_dev) {
$overheated_uri = $this->getRequest()->getRequestURI()
->replaceQueryParam('overheated', true);
$actions[] = id(new PhabricatorActionView())
->setIcon('fa-fire')
->setName(pht('DEV: Overheated State'))
->setHref($overheated_uri);
}
return $actions;
}
private function canExport() {
$engine = $this->getSearchEngine();
if (!$engine->canExport()) {
return false;
}
// Don't allow logged-out users to perform exports. There's no technical
// or policy reason they can't, but we don't normally give them access
// to write files or jobs. For now, just err on the side of caution.
$viewer = $this->getViewer();
if (!$viewer->getPHID()) {
return false;
}
return true;
}
private function readExportFormatPreference() {
$viewer = $this->getViewer();
$export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
return $viewer->getUserSetting($export_key);
}
private function writeExportFormatPreference($value) {
$viewer = $this->getViewer();
$request = $this->getRequest();
if (!$viewer->isLoggedIn()) {
return;
}
$export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY;
$preferences = PhabricatorUserPreferences::loadUserPreferences($viewer);
$editor = id(new PhabricatorUserPreferencesEditor())
->setActor($viewer)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$xactions[] = $preferences->newTransaction($export_key, $value);
$editor->applyTransactions($preferences, $xactions);
}
+ private function processCustomizeRequest() {
+ $viewer = $this->getViewer();
+ $engine = $this->getSearchEngine();
+ $request = $this->getRequest();
+
+ $object_phid = $request->getStr('search.objectPHID');
+ $context_phid = $request->getStr('search.contextPHID');
+
+ // For now, the object can only be a dashboard panel, so just use a panel
+ // query explicitly.
+ $object = id(new PhabricatorDashboardPanelQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($object_phid))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
+ if (!$object) {
+ return new Aphront404Response();
+ }
+
+ $object_name = pht('%s %s', $object->getMonogram(), $object->getName());
+
+ // Likewise, the context object can only be a dashboard.
+ if (strlen($context_phid)) {
+ $context = id(new PhabricatorDashboardQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($context_phid))
+ ->executeOne();
+ if (!$context) {
+ return new Aphront404Response();
+ }
+ } else {
+ $context = $object;
+ }
+
+ $done_uri = $context->getURI();
+
+ if ($request->isFormPost()) {
+ $saved_query = $engine->buildSavedQueryFromRequest($request);
+ $engine->saveQuery($saved_query);
+ $query_key = $saved_query->getQueryKey();
+ } else {
+ $query_key = $this->getQueryKey();
+ if ($engine->isBuiltinQuery($query_key)) {
+ $saved_query = $engine->buildSavedQueryFromBuiltin($query_key);
+ } else if ($query_key) {
+ $saved_query = id(new PhabricatorSavedQueryQuery())
+ ->setViewer($viewer)
+ ->withQueryKeys(array($query_key))
+ ->executeOne();
+ } else {
+ $saved_query = null;
+ }
+ }
+
+ if (!$saved_query) {
+ return new Aphront404Response();
+ }
+
+ $form = id(new AphrontFormView())
+ ->setViewer($viewer)
+ ->addHiddenInput('search.objectPHID', $object_phid)
+ ->addHiddenInput('search.contextPHID', $context_phid)
+ ->setAction($request->getPath());
+
+ $engine->buildSearchForm($form, $saved_query);
+
+ $errors = $engine->getErrors();
+ if ($request->isFormPost()) {
+ if (!$errors) {
+ $xactions = array();
+
+ // Since this workflow is currently used only by dashboard panels,
+ // we can hard-code how the edit works.
+ $xactions[] = $object->getApplicationTransactionTemplate()
+ ->setTransactionType(
+ PhabricatorDashboardQueryPanelQueryTransaction::TRANSACTIONTYPE)
+ ->setNewValue($query_key);
+
+ $editor = $object->getApplicationTransactionEditor()
+ ->setActor($viewer)
+ ->setContentSourceFromRequest($request)
+ ->setContinueOnNoEffect(true)
+ ->setContinueOnMissingFields(true);
+
+ $editor->applyTransactions($object, $xactions);
+
+ return id(new AphrontRedirectResponse())->setURI($done_uri);
+ }
+ }
+
+ return $this->newDialog()
+ ->setTitle(pht('Customize Query: %s', $object_name))
+ ->setErrors($errors)
+ ->setWidth(AphrontDialogView::WIDTH_FULL)
+ ->appendForm($form)
+ ->addCancelButton($done_uri)
+ ->addSubmitButton(pht('Save Changes'));
+ }
}
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index 4f5308fa28..95f1ffafa3 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1613 +1,1626 @@
<?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';
const BUCKET_NONE = 'none';
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) {
if ($query->getID()) {
throw new Exception(
pht(
'Query (with ID "%s") has already been saved. Queries are '.
'immutable once saved.',
$query->getID()));
}
$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 PhabricatorQuery The result of the query.
*/
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $original) {
$saved = clone $original;
$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;
}
$original->attachParameterMap($map);
$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);
}
$buckets = $this->newResultBuckets();
if ($query && $buckets) {
$bucket_options = array(
self::BUCKET_NONE => pht('No Bucketing'),
) + mpull($buckets, 'getResultBucketName');
$fields[] = id(new PhabricatorSearchSelectField())
->setLabel(pht('Bucket'))
->setKey('bucket')
->setOptions($bucket_options);
}
$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;
// Force the fulltext "query" field to the top unconditionally.
$result = array_select_keys($result, array('query')) + $result;
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 infrastructure) 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/');
}
public function getQueryBaseURI() {
return $this->getURI('');
}
public function getExportURI($query_key) {
return $this->getURI('query/'.$query_key.'/export/');
}
+ public function getCustomizeURI($query_key, $object_phid, $context_phid) {
+ $params = array(
+ 'search.objectPHID' => $object_phid,
+ 'search.contextPHID' => $context_phid,
+ );
+
+ $uri = $this->getURI('query/'.$query_key.'/customize/');
+ $uri = new PhutilURI($uri, $params);
+
+ return phutil_string_cast($uri);
+ }
+
+
/**
* 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();
if ($this->namedQueries === null) {
$named_queries = id(new PhabricatorNamedQueryQuery())
->setViewer($viewer)
->withEngineClassNames(array(get_class($this)))
->withUserPHIDs(
array(
$viewer->getPHID(),
PhabricatorNamedQuery::SCOPE_GLOBAL,
))
->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 = msortv($named_queries, 'getNamedQuerySortVector');
$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;
}
public function getDefaultQueryKey() {
$viewer = $this->requireViewer();
$configs = id(new PhabricatorNamedQueryConfigQuery())
->setViewer($viewer)
->withEngineClassNames(array(get_class($this)))
->withScopePHIDs(
array(
$viewer->getPHID(),
PhabricatorNamedQueryConfig::SCOPE_GLOBAL,
))
->execute();
$configs = msortv($configs, 'getStrengthSortVector');
$key_pinned = PhabricatorNamedQueryConfig::PROPERTY_PINNED;
$map = $this->loadEnabledNamedQueries();
foreach ($configs as $config) {
$pinned = $config->getConfigProperty($key_pinned);
if (!isset($map[$pinned])) {
continue;
}
return $pinned;
}
return head_key($map);
}
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(PhabricatorNamedQuery::SCOPE_GLOBAL)
->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 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 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 )--------------------------------------- */
protected function newResultBuckets() {
return array();
}
public function getResultBucket(PhabricatorSavedQuery $saved) {
$key = $saved->getParameter('bucket');
if ($key == self::BUCKET_NONE) {
return null;
}
$buckets = $this->newResultBuckets();
return idx($buckets, $key);
}
public function getPageSize(PhabricatorSavedQuery $saved) {
$bucket = $this->getResultBucket($saved);
$limit = (int)$saved->getParameter('limit');
if ($limit > 0) {
if ($bucket) {
$bucket->setPageSize($limit);
}
return $limit;
}
if ($bucket) {
return $bucket->getPageSize();
}
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);
}
$this->didExecuteQuery($query);
return $objects;
}
protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) {
return;
}
/* -( 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;
}
// These are handled separately for Conduit, so don't show them as
// supported.
unset($fields['order']);
unset($fields['limit']);
$viewer = $this->requireViewer();
foreach ($fields as $key => $field) {
$field->setViewer($viewer);
}
return $fields;
}
public function buildConduitResponse(
ConduitAPIRequest $request,
ConduitAPIMethod $method) {
$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());
if (!is_array($constraints)) {
throw new Exception(
pht(
'Parameter "constraints" must be a map of constraints, got "%s".',
phutil_describe_type($constraints)));
}
$fields = $this->getSearchFieldsForConduit();
foreach ($fields as $key => $field) {
if (!$field->getConduitParameterType()) {
unset($fields[$key]);
}
}
$valid_constraints = array();
foreach ($fields as $field) {
foreach ($field->getValidConstraintKeys() as $key) {
$valid_constraints[$key] = true;
}
}
foreach ($constraints as $key => $constraint) {
if (empty($valid_constraints[$key])) {
throw new Exception(
pht(
'Constraint "%s" is not a valid constraint for this query.',
$key));
}
}
foreach ($fields as $field) {
if (!$field->getValueExistsInConduitRequest($constraints)) {
continue;
}
$value = $field->readValueFromConduitRequest(
$constraints,
$request->getIsStrictlyTyped());
$saved_query->setParameter($field->getKey(), $value);
}
// NOTE: Currently, when running an ad-hoc query we never persist it into
// a saved query. We might want to add an option to do this in the future
// (for example, to enable a CLI-to-Web workflow where user can view more
// details about results by following a link), but have no use cases for
// it today. If we do identify a use case, we could save the query here.
$query = $this->buildQueryFromSavedQuery($saved_query);
$pager = $this->newPagerForSavedQuery($saved_query);
$attachments = $this->getConduitSearchAttachments();
// TODO: Validate this better.
$attachment_specs = $request->getValue('attachments', array());
$attachments = array_select_keys(
$attachments,
array_keys($attachment_specs));
foreach ($attachments as $key => $attachment) {
$attachment->setViewer($viewer);
}
foreach ($attachments as $key => $attachment) {
$attachment->willLoadAttachmentData($query, $attachment_specs[$key]);
}
$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();
$extension_data = array();
foreach ($field_extensions as $key => $extension) {
$extension_data[$key] = $extension->loadExtensionConduitData($objects);
}
$attachment_data = array();
foreach ($attachments as $key => $attachment) {
$attachment_data[$key] = $attachment->loadAttachmentData(
$objects,
$attachment_specs[$key]);
}
foreach ($objects as $object) {
$field_map = $this->getObjectWireFieldsForConduit(
$object,
$field_extensions,
$extension_data);
$attachment_map = array();
foreach ($attachments as $key => $attachment) {
$attachment_map[$key] = $attachment->getAttachmentForObject(
$object,
$attachment_data[$key],
$attachment_specs[$key]);
}
// If this is empty, we still want to emit a JSON object, not a
// JSON list.
if (!$attachment_map) {
$attachment_map = (object)$attachment_map;
}
$id = (int)$object->getID();
$phid = $object->getPHID();
$data[] = array(
'id' => $id,
'type' => phid_get_type($phid),
'phid' => $phid,
'fields' => $field_map,
'attachments' => $attachment_map,
);
}
}
return array(
'data' => $data,
'maps' => $method->getQueryMaps($query),
'query' => array(
// This may be `null` if we have not saved the query.
'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();
$map = array();
foreach ($extensions as $extension) {
$specifications = $extension->getFieldSpecificationsForConduit($object);
foreach ($specifications as $specification) {
$key = $specification->getKey();
if (isset($map[$key])) {
throw new Exception(
pht(
'Two field specifications share the same key ("%s"). Each '.
'specification must have a unique key.',
$key));
}
$map[$key] = $specification;
}
}
return $map;
}
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 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 getObjectWireFieldsForConduit(
$object,
array $field_extensions,
array $extension_data) {
$fields = array();
foreach ($field_extensions as $key => $extension) {
$data = idx($extension_data, $key, array());
$fields += $extension->getFieldValuesForConduit($object, $data);
}
return $fields;
}
public function getConduitSearchAttachments() {
$extensions = $this->getEngineExtensions();
$object = $this->newResultObject();
$attachments = array();
foreach ($extensions as $extension) {
$extension_attachments = $extension->getSearchAttachments($object);
foreach ($extension_attachments as $attachment) {
$attachment_key = $attachment->getAttachmentKey();
if (isset($attachments[$attachment_key])) {
$other = $attachments[$attachment_key];
throw new Exception(
pht(
'Two search engine attachments (of classes "%s" and "%s") '.
'specify the same attachment key ("%s"); keys must be unique.',
get_class($attachment),
get_class($other),
$attachment_key));
}
$attachments[$attachment_key] = $attachment;
}
}
return $attachments;
}
final public function renderNewUserView() {
$body = $this->getNewUserBody();
if (!$body) {
return null;
}
return $body;
}
protected function getNewUserHeader() {
return null;
}
protected function getNewUserBody() {
return null;
}
public function newUseResultsActions(PhabricatorSavedQuery $saved) {
return array();
}
/* -( Export )------------------------------------------------------------- */
public function canExport() {
$fields = $this->newExportFields();
return (bool)$fields;
}
final public function newExportFieldList() {
$object = $this->newResultObject();
$builtin_fields = array(
id(new PhabricatorIDExportField())
->setKey('id')
->setLabel(pht('ID')),
);
if ($object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) {
$builtin_fields[] = id(new PhabricatorPHIDExportField())
->setKey('phid')
->setLabel(pht('PHID'));
}
$fields = mpull($builtin_fields, null, 'getKey');
$export_fields = $this->newExportFields();
foreach ($export_fields as $export_field) {
$key = $export_field->getKey();
if (isset($fields[$key])) {
throw new Exception(
pht(
'Search engine ("%s") defines an export field with a key ("%s") '.
'that collides with another field. Each field must have a '.
'unique key.',
get_class($this),
$key));
}
$fields[$key] = $export_field;
}
$extensions = $this->newExportExtensions();
foreach ($extensions as $extension) {
$extension_fields = $extension->newExportFields();
foreach ($extension_fields as $extension_field) {
$key = $extension_field->getKey();
if (isset($fields[$key])) {
throw new Exception(
pht(
'Export engine extension ("%s") defines an export field with '.
'a key ("%s") that collides with another field. Each field '.
'must have a unique key.',
get_class($extension_field),
$key));
}
$fields[$key] = $extension_field;
}
}
return $fields;
}
final public function newExport(array $objects) {
$object = $this->newResultObject();
$has_phid = $object->getConfigOption(LiskDAO::CONFIG_AUX_PHID);
$objects = array_values($objects);
$n = count($objects);
$maps = array();
foreach ($objects as $object) {
$map = array(
'id' => $object->getID(),
);
if ($has_phid) {
$map['phid'] = $object->getPHID();
}
$maps[] = $map;
}
$export_data = $this->newExportData($objects);
$export_data = array_values($export_data);
if (count($export_data) !== count($objects)) {
throw new Exception(
pht(
'Search engine ("%s") exported the wrong number of objects, '.
'expected %s but got %s.',
get_class($this),
phutil_count($objects),
phutil_count($export_data)));
}
for ($ii = 0; $ii < $n; $ii++) {
$maps[$ii] += $export_data[$ii];
}
$extensions = $this->newExportExtensions();
foreach ($extensions as $extension) {
$extension_data = $extension->newExportData($objects);
$extension_data = array_values($extension_data);
if (count($export_data) !== count($objects)) {
throw new Exception(
pht(
'Export engine extension ("%s") exported the wrong number of '.
'objects, expected %s but got %s.',
get_class($extension),
phutil_count($objects),
phutil_count($export_data)));
}
for ($ii = 0; $ii < $n; $ii++) {
$maps[$ii] += $extension_data[$ii];
}
}
return $maps;
}
protected function newExportFields() {
return array();
}
protected function newExportData(array $objects) {
throw new PhutilMethodNotImplementedException();
}
private function newExportExtensions() {
$object = $this->newResultObject();
$viewer = $this->requireViewer();
$extensions = PhabricatorExportEngineExtension::getAllExtensions();
$supported = array();
foreach ($extensions as $extension) {
$extension = clone $extension;
$extension->setViewer($viewer);
if ($extension->supportsObject($object)) {
$supported[] = $extension;
}
}
return $supported;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Jul 28, 7:32 PM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
187421
Default Alt Text
(103 KB)

Event Timeline