Page MenuHomestyx hydra

No OneTemporary

diff --git a/resources/sql/autopatches/20150504.symbolsproject.1.php b/resources/sql/autopatches/20150504.symbolsproject.1.php
index 8ab77bbef4..037397998b 100644
--- a/resources/sql/autopatches/20150504.symbolsproject.1.php
+++ b/resources/sql/autopatches/20150504.symbolsproject.1.php
@@ -1,78 +1,82 @@
<?php
$table_w = new PhabricatorRepository();
$conn_w = $table_w->establishConnection('w');
// Repository and Project share a database.
$conn_r = $table_w->establishConnection('r');
$projects_table = 'repository_arcanistproject';
$raw_projects_data = queryfx_all($conn_r, 'SELECT * FROM %T', $projects_table);
$raw_projects_data = ipull($raw_projects_data, null, 'id');
$repository_ids = ipull($raw_projects_data, 'repositoryID');
+if (!$repository_ids) {
+ return;
+}
+
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withIDs($repository_ids)
->execute();
$projects_to_repo_ids_map = ipull($raw_projects_data, 'repositoryID', 'phid');
$projects_to_repos_map = array();
foreach ($projects_to_repo_ids_map as $projectPHID => $repositoryID) {
$repo = idx($repositories, $repositoryID);
if ($repo) {
$projects_to_repos_map[$projectPHID] = $repo->getPHID();
}
}
foreach ($raw_projects_data as $project_row) {
$repositoryID = idx($project_row, 'repositoryID');
$repo = idx($repositories, $repositoryID);
if (!$repo) {
continue;
}
echo pht(
"Migrating symbols configuration for '%s' project...\n",
idx($project_row, 'name', '???'));
$symbol_index_projects = $project_row['symbolIndexProjects'];
$symbol_index_projects = nonempty($symbol_index_projects, '[]');
try {
$symbol_index_projects = phutil_json_decode($symbol_index_projects);
} catch (PhutilJSONParserException $ex) {
continue;
}
$sources = $repo->getDetail('symbol-sources', array());
foreach ($symbol_index_projects as $index_project) {
$sources[] = idx($projects_to_repos_map, $index_project);
}
$sources = array_filter($sources);
$sources = array_unique($sources);
$repo->setDetail('symbol-sources', $sources);
$languages = $project_row['symbolIndexLanguages'];
$languages = nonempty($languages, '[]');
try {
$languages = phutil_json_decode($languages);
} catch (PhutilJSONParserException $ex) {
continue;
}
$languages = array_merge(
$repo->getDetail('symbol-languages', array()),
$languages);
$languages = array_unique($languages);
$repo->setDetail('symbol-languages', $languages);
queryfx(
$conn_w,
'UPDATE %T SET details = %s WHERE id = %d',
$table_w->getTableName(),
json_encode($repo->getDetails()),
$repo->getID());
}
diff --git a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php b/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
index b7fafa83ff..18f433fe52 100644
--- a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
+++ b/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php
@@ -1,221 +1,221 @@
<?php
final class ManiphestTaskTestCase extends PhabricatorTestCase {
protected function getPhabricatorTestCaseConfiguration() {
return array(
self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
);
}
public function testTaskReordering() {
$viewer = $this->generateNewTestUser();
$t1 = $this->newTask($viewer, pht('Task 1'));
$t2 = $this->newTask($viewer, pht('Task 2'));
$t3 = $this->newTask($viewer, pht('Task 3'));
$auto_base = min(mpull(array($t1, $t2, $t3), 'getID'));
// Default order should be reverse creation.
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(3, 2, 1), array_keys($tasks));
// Move T3 to the middle.
$this->moveTask($viewer, $t3, $t2, true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 3, 1), array_keys($tasks));
// Move T3 to the end.
$this->moveTask($viewer, $t3, $t1, true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 1, 3), array_keys($tasks));
// Repeat the move above, there should be no overall change in order.
$this->moveTask($viewer, $t3, $t1, true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 1, 3), array_keys($tasks));
// Move T3 to the first slot in the priority.
$this->movePriority($viewer, $t3, $t3->getPriority(), false);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(3, 2, 1), array_keys($tasks));
// Move T3 to the last slot in the priority.
$this->movePriority($viewer, $t3, $t3->getPriority(), true);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 1, 3), array_keys($tasks));
// Move T3 before T2.
$this->moveTask($viewer, $t3, $t2, false);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(3, 2, 1), array_keys($tasks));
// Move T3 before T1.
$this->moveTask($viewer, $t3, $t1, false);
$tasks = $this->loadTasks($viewer, $auto_base);
$t1 = $tasks[1];
$t2 = $tasks[2];
$t3 = $tasks[3];
$this->assertEqual(array(2, 3, 1), array_keys($tasks));
}
public function testTaskAdjacentBlocks() {
$viewer = $this->generateNewTestUser();
$t = array();
for ($ii = 1; $ii < 10; $ii++) {
$t[$ii] = $this->newTask($viewer, pht('Task Block %d', $ii));
// This makes sure this test remains meaningful if we begin assigning
// subpriorities when tasks are created.
$t[$ii]->setSubpriority(0)->save();
}
$auto_base = min(mpull($t, 'getID'));
$tasks = $this->loadTasks($viewer, $auto_base);
$this->assertEqual(
array(9, 8, 7, 6, 5, 4, 3, 2, 1),
array_keys($tasks));
$this->moveTask($viewer, $t[9], $t[8], true);
$tasks = $this->loadTasks($viewer, $auto_base);
$this->assertEqual(
array(8, 9, 7, 6, 5, 4, 3, 2, 1),
array_keys($tasks));
// When there is a large block of tasks which all have the same
// subpriority, they should be assigned distinct subpriorities as a
// side effect of having a task moved into the block.
$subpri = mpull($tasks, 'getSubpriority');
$unique_subpri = array_unique($subpri);
$this->assertEqual(
9,
count($subpri),
pht('Expected subpriorities to be distributed.'));
}
private function newTask(PhabricatorUser $viewer, $title) {
$task = ManiphestTask::initializeNewTask($viewer);
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_TITLE)
->setNewValue($title);
$this->applyTaskTransactions($viewer, $task, $xactions);
return $task;
}
private function loadTasks(PhabricatorUser $viewer, $auto_base) {
$tasks = id(new ManiphestTaskQuery())
->setViewer($viewer)
- ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
+ ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->execute();
// NOTE: AUTO_INCREMENT changes survive ROLLBACK, and we can't throw them
// away without committing the current transaction, so we adjust the
// apparent task IDs as though the first one had been ID 1. This makes the
// tests a little easier to understand.
$map = array();
foreach ($tasks as $task) {
$map[($task->getID() - $auto_base) + 1] = $task;
}
return $map;
}
private function moveTask(PhabricatorUser $viewer, $src, $dst, $is_after) {
list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority(
$dst,
$is_after);
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($pri);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue($sub);
return $this->applyTaskTransactions($viewer, $src, $xactions);
}
private function movePriority(
PhabricatorUser $viewer,
$src,
$target_priority,
$is_end) {
list($pri, $sub) = ManiphestTransactionEditor::getEdgeSubpriority(
$target_priority,
$is_end);
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($pri);
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY)
->setNewValue($sub);
return $this->applyTaskTransactions($viewer, $src, $xactions);
}
private function applyTaskTransactions(
PhabricatorUser $viewer,
ManiphestTask $task,
array $xactions) {
$content_source = PhabricatorContentSource::newConsoleSource();
$editor = id(new ManiphestTransactionEditor())
->setActor($viewer)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->applyTransactions($task, $xactions);
return $task;
}
}
diff --git a/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php
index 8a2321b2d3..ea8aeb95ed 100644
--- a/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php
+++ b/src/applications/maniphest/conduit/ManiphestQueryConduitAPIMethod.php
@@ -1,122 +1,122 @@
<?php
final class ManiphestQueryConduitAPIMethod extends ManiphestConduitAPIMethod {
public function getAPIMethodName() {
return 'maniphest.query';
}
public function getMethodDescription() {
return pht('Execute complex searches for Maniphest tasks.');
}
protected function defineParamTypes() {
$statuses = array(
ManiphestTaskQuery::STATUS_ANY,
ManiphestTaskQuery::STATUS_OPEN,
ManiphestTaskQuery::STATUS_CLOSED,
ManiphestTaskQuery::STATUS_RESOLVED,
ManiphestTaskQuery::STATUS_WONTFIX,
ManiphestTaskQuery::STATUS_INVALID,
ManiphestTaskQuery::STATUS_SPITE,
ManiphestTaskQuery::STATUS_DUPLICATE,
);
$status_const = $this->formatStringConstants($statuses);
$orders = array(
ManiphestTaskQuery::ORDER_PRIORITY,
ManiphestTaskQuery::ORDER_CREATED,
ManiphestTaskQuery::ORDER_MODIFIED,
);
$order_const = $this->formatStringConstants($orders);
return array(
'ids' => 'optional list<uint>',
'phids' => 'optional list<phid>',
'ownerPHIDs' => 'optional list<phid>',
'authorPHIDs' => 'optional list<phid>',
'projectPHIDs' => 'optional list<phid>',
'ccPHIDs' => 'optional list<phid>',
'fullText' => 'optional string',
'status' => 'optional '.$status_const,
'order' => 'optional '.$order_const,
'limit' => 'optional int',
'offset' => 'optional int',
);
}
protected function defineReturnType() {
return 'list';
}
protected function execute(ConduitAPIRequest $request) {
$query = id(new ManiphestTaskQuery())
->setViewer($request->getUser())
->needProjectPHIDs(true)
->needSubscriberPHIDs(true);
$task_ids = $request->getValue('ids');
if ($task_ids) {
$query->withIDs($task_ids);
}
$task_phids = $request->getValue('phids');
if ($task_phids) {
$query->withPHIDs($task_phids);
}
$owners = $request->getValue('ownerPHIDs');
if ($owners) {
$query->withOwners($owners);
}
$authors = $request->getValue('authorPHIDs');
if ($authors) {
$query->withAuthors($authors);
}
$projects = $request->getValue('projectPHIDs');
if ($projects) {
$query->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_AND,
$projects);
}
$ccs = $request->getValue('ccPHIDs');
if ($ccs) {
$query->withSubscribers($ccs);
}
$full_text = $request->getValue('fullText');
if ($full_text) {
$query->withFullTextSearch($full_text);
}
$status = $request->getValue('status');
if ($status) {
$query->withStatus($status);
}
$order = $request->getValue('order');
if ($order) {
- $query->setOrderBy($order);
+ $query->setOrder($order);
}
$limit = $request->getValue('limit');
if ($limit) {
$query->setLimit($limit);
}
$offset = $request->getValue('offset');
if ($offset) {
$query->setOffset($offset);
}
$results = $query->execute();
return $this->buildTaskInfoDictionaries($results);
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 76a8c7f6f1..39111644cb 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,737 +1,737 @@
<?php
final class ManiphestTransactionEditor
extends PhabricatorApplicationTransactionEditor {
public function getEditorApplicationClass() {
return 'PhabricatorManiphestApplication';
}
public function getEditorObjectsDescription() {
return pht('Maniphest Tasks');
}
public function getTransactionTypes() {
$types = parent::getTransactionTypes();
$types[] = PhabricatorTransactions::TYPE_COMMENT;
$types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = ManiphestTransaction::TYPE_PRIORITY;
$types[] = ManiphestTransaction::TYPE_STATUS;
$types[] = ManiphestTransaction::TYPE_TITLE;
$types[] = ManiphestTransaction::TYPE_DESCRIPTION;
$types[] = ManiphestTransaction::TYPE_OWNER;
$types[] = ManiphestTransaction::TYPE_SUBPRIORITY;
$types[] = ManiphestTransaction::TYPE_PROJECT_COLUMN;
$types[] = ManiphestTransaction::TYPE_MERGED_INTO;
$types[] = ManiphestTransaction::TYPE_MERGED_FROM;
$types[] = ManiphestTransaction::TYPE_UNBLOCK;
$types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
return $types;
}
protected function getCustomTransactionOldValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
if ($this->getIsNewObject()) {
return null;
}
return (int)$object->getPriority();
case ManiphestTransaction::TYPE_STATUS:
if ($this->getIsNewObject()) {
return null;
}
return $object->getStatus();
case ManiphestTransaction::TYPE_TITLE:
if ($this->getIsNewObject()) {
return null;
}
return $object->getTitle();
case ManiphestTransaction::TYPE_DESCRIPTION:
if ($this->getIsNewObject()) {
return null;
}
return $object->getDescription();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($object->getOwnerPHID(), null);
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// These are pre-populated.
return $xaction->getOldValue();
case ManiphestTransaction::TYPE_SUBPRIORITY:
return $object->getSubpriority();
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
return null;
}
}
protected function getCustomTransactionNewValue(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return (int)$xaction->getNewValue();
case ManiphestTransaction::TYPE_OWNER:
return nonempty($xaction->getNewValue(), null);
case ManiphestTransaction::TYPE_STATUS:
case ManiphestTransaction::TYPE_TITLE:
case ManiphestTransaction::TYPE_DESCRIPTION:
case ManiphestTransaction::TYPE_SUBPRIORITY:
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
case ManiphestTransaction::TYPE_MERGED_INTO:
case ManiphestTransaction::TYPE_MERGED_FROM:
case ManiphestTransaction::TYPE_UNBLOCK:
return $xaction->getNewValue();
}
}
protected function transactionHasEffect(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
$old = $xaction->getOldValue();
$new = $xaction->getNewValue();
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$new_column_phids = $new['columnPHIDs'];
$old_column_phids = $old['columnPHIDs'];
sort($new_column_phids);
sort($old_column_phids);
return ($old !== $new);
}
return parent::transactionHasEffect($object, $xaction);
}
protected function applyCustomInternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PRIORITY:
return $object->setPriority($xaction->getNewValue());
case ManiphestTransaction::TYPE_STATUS:
return $object->setStatus($xaction->getNewValue());
case ManiphestTransaction::TYPE_TITLE:
return $object->setTitle($xaction->getNewValue());
case ManiphestTransaction::TYPE_DESCRIPTION:
return $object->setDescription($xaction->getNewValue());
case ManiphestTransaction::TYPE_OWNER:
$phid = $xaction->getNewValue();
// Update the "ownerOrdering" column to contain the full name of the
// owner, if the task is assigned.
$handle = null;
if ($phid) {
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
}
if ($handle) {
$object->setOwnerOrdering($handle->getName());
} else {
$object->setOwnerOrdering(null);
}
return $object->setOwnerPHID($phid);
case ManiphestTransaction::TYPE_SUBPRIORITY:
$object->setSubpriority($xaction->getNewValue());
return;
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
// these do external (edge) updates
return;
case ManiphestTransaction::TYPE_MERGED_INTO:
$object->setStatus(ManiphestTaskStatus::getDuplicateStatus());
return;
case ManiphestTransaction::TYPE_MERGED_FROM:
return;
}
}
protected function applyCustomExternalTransaction(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_PROJECT_COLUMN:
$board_phid = idx($xaction->getNewValue(), 'projectPHID');
if (!$board_phid) {
throw new Exception(
pht(
"Expected '%s' in column transaction.",
'projectPHID'));
}
$old_phids = idx($xaction->getOldValue(), 'columnPHIDs', array());
$new_phids = idx($xaction->getNewValue(), 'columnPHIDs', array());
if (count($new_phids) !== 1) {
throw new Exception(
pht(
"Expected exactly one '%s' in column transaction.",
'columnPHIDs'));
}
$columns = id(new PhabricatorProjectColumnQuery())
->setViewer($this->requireActor())
->withPHIDs($new_phids)
->execute();
$columns = mpull($columns, null, 'getPHID');
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($this->requireActor())
->withObjectPHIDs(array($object->getPHID()))
->withBoardPHIDs(array($board_phid))
->execute();
$before_phid = idx($xaction->getNewValue(), 'beforePHID');
$after_phid = idx($xaction->getNewValue(), 'afterPHID');
if (!$before_phid && !$after_phid && ($old_phids == $new_phids)) {
// If we are not moving the object between columns and also not
// reordering the position, this is a move on some other order
// (like priority). We can leave the positions untouched and just
// bail, there's no work to be done.
return;
}
// Otherwise, we're either moving between columns or adjusting the
// object's position in the "natural" ordering, so we do need to update
// some rows.
// Remove all existing column positions on the board.
foreach ($positions as $position) {
$position->delete();
}
// Add the new column positions.
foreach ($new_phids as $phid) {
$column = idx($columns, $phid);
if (!$column) {
throw new Exception(
pht('No such column "%s" exists!', $phid));
}
// Load the other object positions in the column. Note that we must
// skip implicit column creation to avoid generating a new position
// if the target column is a backlog column.
$other_positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($this->requireActor())
->withColumns(array($column))
->withBoardPHIDs(array($board_phid))
->setSkipImplicitCreate(true)
->execute();
$other_positions = msort($other_positions, 'getOrderingKey');
// Set up the new position object. We're going to figure out the
// right sequence number and then persist this object with that
// sequence number.
$new_position = id(new PhabricatorProjectColumnPosition())
->setBoardPHID($board_phid)
->setColumnPHID($column->getPHID())
->setObjectPHID($object->getPHID());
$updates = array();
$sequence = 0;
// If we're just dropping this into the column without any specific
// position information, put it at the top.
if (!$before_phid && !$after_phid) {
$new_position->setSequence($sequence)->save();
$sequence++;
}
foreach ($other_positions as $position) {
$object_phid = $position->getObjectPHID();
// If this is the object we're moving before and we haven't
// saved yet, insert here.
if (($before_phid == $object_phid) && !$new_position->getID()) {
$new_position->setSequence($sequence)->save();
$sequence++;
}
// This object goes here in the sequence; we might need to update
// the row.
if ($sequence != $position->getSequence()) {
$updates[$position->getID()] = $sequence;
}
$sequence++;
// If this is the object we're moving after and we haven't saved
// yet, insert here.
if (($after_phid == $object_phid) && !$new_position->getID()) {
$new_position->setSequence($sequence)->save();
$sequence++;
}
}
// We should have found a place to put it.
if (!$new_position->getID()) {
throw new Exception(
pht('Unable to find a place to insert object on column!'));
}
// If we changed other objects' column positions, bulk reorder them.
if ($updates) {
$position = new PhabricatorProjectColumnPosition();
$conn_w = $position->establishConnection('w');
$pairs = array();
foreach ($updates as $id => $sequence) {
// This is ugly because MySQL gets upset with us if it is
// configured strictly and we attempt inserts which can't work.
// We'll never actually do these inserts since they'll always
// collide (triggering the ON DUPLICATE KEY logic), so we just
// provide dummy values in order to get there.
$pairs[] = qsprintf(
$conn_w,
'(%d, %d, "", "", "")',
$id,
$sequence);
}
queryfx(
$conn_w,
'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID)
VALUES %Q ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)',
$position->getTableName(),
implode(', ', $pairs));
}
}
break;
default:
break;
}
}
protected function applyFinalEffects(
PhabricatorLiskDAO $object,
array $xactions) {
// When we change the status of a task, update tasks this tasks blocks
// with a message to the effect of "alincoln resolved blocking task Txxx."
$unblock_xaction = null;
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_STATUS:
$unblock_xaction = $xaction;
break;
}
}
if ($unblock_xaction !== null) {
$blocked_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$object->getPHID(),
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST);
if ($blocked_phids) {
// In theory we could apply these through policies, but that seems a
// little bit surprising. For now, use the actor's vision.
$blocked_tasks = id(new ManiphestTaskQuery())
->setViewer($this->getActor())
->withPHIDs($blocked_phids)
->needSubscriberPHIDs(true)
->needProjectPHIDs(true)
->execute();
$old = $unblock_xaction->getOldValue();
$new = $unblock_xaction->getNewValue();
foreach ($blocked_tasks as $blocked_task) {
$unblock_xactions = array();
$unblock_xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_UNBLOCK)
->setOldValue(array($object->getPHID() => $old))
->setNewValue(array($object->getPHID() => $new));
id(new ManiphestTransactionEditor())
->setActor($this->getActor())
->setActingAsPHID($this->getActingAsPHID())
->setContentSource($this->getContentSource())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($blocked_task, $unblock_xactions);
}
}
}
return $xactions;
}
protected function shouldSendMail(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function getMailSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
protected function getMailThreadID(PhabricatorLiskDAO $object) {
return 'maniphest-task-'.$object->getPHID();
}
protected function getMailTo(PhabricatorLiskDAO $object) {
$phids = array();
if ($object->getOwnerPHID()) {
$phids[] = $object->getOwnerPHID();
}
$phids[] = $this->getActingAsPHID();
return $phids;
}
public function getMailTagsMap() {
return array(
ManiphestTransaction::MAILTAG_STATUS =>
pht("A task's status changes."),
ManiphestTransaction::MAILTAG_OWNER =>
pht("A task's owner changes."),
ManiphestTransaction::MAILTAG_PRIORITY =>
pht("A task's priority changes."),
ManiphestTransaction::MAILTAG_CC =>
pht("A task's subscribers change."),
ManiphestTransaction::MAILTAG_PROJECTS =>
pht("A task's associated projects change."),
ManiphestTransaction::MAILTAG_UNBLOCK =>
pht('One of the tasks a task is blocked by changes status.'),
ManiphestTransaction::MAILTAG_COLUMN =>
pht('A task is moved between columns on a workboard.'),
ManiphestTransaction::MAILTAG_COMMENT =>
pht('Someone comments on a task.'),
ManiphestTransaction::MAILTAG_OTHER =>
pht('Other task activity not listed above occurs.'),
);
}
protected function buildReplyHandler(PhabricatorLiskDAO $object) {
return id(new ManiphestReplyHandler())
->setMailReceiver($object);
}
protected function buildMailTemplate(PhabricatorLiskDAO $object) {
$id = $object->getID();
$title = $object->getTitle();
return id(new PhabricatorMetaMTAMail())
->setSubject("T{$id}: {$title}")
->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle());
}
protected function buildMailBody(
PhabricatorLiskDAO $object,
array $xactions) {
$body = parent::buildMailBody($object, $xactions);
if ($this->getIsNewObject()) {
$body->addTextSection(
pht('TASK DESCRIPTION'),
$object->getDescription());
}
$body->addLinkSection(
pht('TASK DETAIL'),
PhabricatorEnv::getProductionURI('/T'.$object->getID()));
$board_phids = array();
$type_column = ManiphestTransaction::TYPE_PROJECT_COLUMN;
foreach ($xactions as $xaction) {
if ($xaction->getTransactionType() == $type_column) {
$new = $xaction->getNewValue();
$project_phid = idx($new, 'projectPHID');
if ($project_phid) {
$board_phids[] = $project_phid;
}
}
}
if ($board_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->requireActor())
->withPHIDs($board_phids)
->execute();
foreach ($projects as $project) {
$body->addLinkSection(
pht('WORKBOARD'),
PhabricatorEnv::getProductionURI(
'/project/board/'.$project->getID().'/'));
}
}
return $body;
}
protected function shouldPublishFeedStory(
PhabricatorLiskDAO $object,
array $xactions) {
return $this->shouldSendMail($object, $xactions);
}
protected function supportsSearch() {
return true;
}
protected function shouldApplyHeraldRules(
PhabricatorLiskDAO $object,
array $xactions) {
return true;
}
protected function buildHeraldAdapter(
PhabricatorLiskDAO $object,
array $xactions) {
return id(new HeraldManiphestTaskAdapter())
->setTask($object);
}
protected function didApplyHeraldRules(
PhabricatorLiskDAO $object,
HeraldAdapter $adapter,
HeraldTranscript $transcript) {
$xactions = array();
$assign_phid = $adapter->getAssignPHID();
if ($assign_phid) {
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_OWNER)
->setNewValue($assign_phid);
}
return $xactions;
}
protected function requireCapabilities(
PhabricatorLiskDAO $object,
PhabricatorApplicationTransaction $xaction) {
parent::requireCapabilities($object, $xaction);
$app_capability_map = array(
ManiphestTransaction::TYPE_PRIORITY =>
ManiphestEditPriorityCapability::CAPABILITY,
ManiphestTransaction::TYPE_STATUS =>
ManiphestEditStatusCapability::CAPABILITY,
ManiphestTransaction::TYPE_OWNER =>
ManiphestEditAssignCapability::CAPABILITY,
PhabricatorTransactions::TYPE_EDIT_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
PhabricatorTransactions::TYPE_VIEW_POLICY =>
ManiphestEditPoliciesCapability::CAPABILITY,
);
$transaction_type = $xaction->getTransactionType();
$app_capability = null;
if ($transaction_type == PhabricatorTransactions::TYPE_EDGE) {
switch ($xaction->getMetadataValue('edge:type')) {
case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST:
$app_capability = ManiphestEditProjectsCapability::CAPABILITY;
break;
}
} else {
$app_capability = idx($app_capability_map, $transaction_type);
}
if ($app_capability) {
$app = id(new PhabricatorApplicationQuery())
->setViewer($this->getActor())
->withClasses(array('PhabricatorManiphestApplication'))
->executeOne();
PhabricatorPolicyFilter::requireCapability(
$this->getActor(),
$app,
$app_capability);
}
}
protected function adjustObjectForPolicyChecks(
PhabricatorLiskDAO $object,
array $xactions) {
$copy = parent::adjustObjectForPolicyChecks($object, $xactions);
foreach ($xactions as $xaction) {
switch ($xaction->getTransactionType()) {
case ManiphestTransaction::TYPE_OWNER:
$copy->setOwnerPHID($xaction->getNewValue());
break;
default:
continue;
}
}
return $copy;
}
/**
* Get priorities for moving a task to a new priority.
*/
public static function getEdgeSubpriority(
$priority,
$is_end) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($priority))
->setLimit(1);
if ($is_end) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$result = $query->executeOne();
$step = (double)(2 << 32);
if ($result) {
$base = $result->getSubpriority();
if ($is_end) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
} else {
$sub = 0;
}
return array($priority, $sub);
}
/**
* Get priorities for moving a task before or after another task.
*/
public static function getAdjacentSubpriority(
ManiphestTask $dst,
$is_after,
$allow_recursion = true) {
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
- ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
+ ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->withPriorities(array($dst->getPriority()))
->setLimit(1);
if ($is_after) {
$query->setAfterID($dst->getID());
} else {
$query->setBeforeID($dst->getID());
}
$adjacent = $query->executeOne();
$base = $dst->getSubpriority();
$step = (double)(2 << 32);
// If we find an adjacent task, we average the two subpriorities and
// return the result.
if ($adjacent) {
$epsilon = 0.01;
// If the adjacent task has a subpriority that is identical or very
// close to the task we're looking at, we're going to move it and all
// tasks with the same subpriority a little farther down the subpriority
// scale.
if ($allow_recursion &&
(abs($adjacent->getSubpriority() - $base) < $epsilon)) {
$conn_w = $adjacent->establishConnection('w');
$min = ($adjacent->getSubpriority() - ($epsilon));
$max = ($adjacent->getSubpriority() + ($epsilon));
// Get all of the tasks with the similar subpriorities to the adjacent
// task, including the adjacent task itself.
$query = id(new ManiphestTaskQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPriorities(array($adjacent->getPriority()))
->withSubpriorityBetween($min, $max);
if (!$is_after) {
$query->setOrderVector(array('-priority', '-subpriority', '-id'));
} else {
$query->setOrderVector(array('priority', 'subpriority', 'id'));
}
$shift_all = $query->execute();
$shift_last = last($shift_all);
// Select the most extreme subpriority in the result set as the
// base value.
$shift_base = head($shift_all)->getSubpriority();
// Find the subpriority before or after the task at the end of the
// block.
list($shift_pri, $shift_sub) = self::getAdjacentSubpriority(
$shift_last,
$is_after,
$allow_recursion = false);
$delta = ($shift_sub - $shift_base);
$count = count($shift_all);
$shift = array();
$cursor = 1;
foreach ($shift_all as $shift_task) {
$shift_target = $shift_base + (($cursor / $count) * $delta);
$cursor++;
queryfx(
$conn_w,
'UPDATE %T SET subpriority = %f WHERE id = %d',
$adjacent->getTableName(),
$shift_target,
$shift_task->getID());
// If we're shifting the adjacent task, update it.
if ($shift_task->getID() == $adjacent->getID()) {
$adjacent->setSubpriority($shift_target);
}
// If we're shifting the original target task, update the base
// subpriority.
if ($shift_task->getID() == $dst->getID()) {
$base = $shift_target;
}
}
}
$sub = ($adjacent->getSubpriority() + $base) / 2;
} else {
// Otherwise, we take a step away from the target's subpriority and
// use that.
if ($is_after) {
$sub = ($base - $step);
} else {
$sub = ($base + $step);
}
}
return array($dst->getPriority(), $sub);
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php
index 636fea218d..956810524d 100644
--- a/src/applications/maniphest/query/ManiphestTaskQuery.php
+++ b/src/applications/maniphest/query/ManiphestTaskQuery.php
@@ -1,862 +1,849 @@
<?php
/**
* Query tasks by specific criteria. This class uses the higher-performance
* but less-general Maniphest indexes to satisfy queries.
*/
final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
private $taskIDs = array();
private $taskPHIDs = array();
private $authorPHIDs = array();
private $ownerPHIDs = array();
private $noOwner;
private $anyOwner;
private $subscriberPHIDs = array();
private $dateCreatedAfter;
private $dateCreatedBefore;
private $dateModifiedAfter;
private $dateModifiedBefore;
private $subpriorityMin;
private $subpriorityMax;
private $fullTextSearch = '';
private $status = 'status-any';
const STATUS_ANY = 'status-any';
const STATUS_OPEN = 'status-open';
const STATUS_CLOSED = 'status-closed';
const STATUS_RESOLVED = 'status-resolved';
const STATUS_WONTFIX = 'status-wontfix';
const STATUS_INVALID = 'status-invalid';
const STATUS_SPITE = 'status-spite';
const STATUS_DUPLICATE = 'status-duplicate';
private $statuses;
private $priorities;
private $subpriorities;
private $groupBy = 'group-none';
const GROUP_NONE = 'group-none';
const GROUP_PRIORITY = 'group-priority';
const GROUP_OWNER = 'group-owner';
const GROUP_STATUS = 'group-status';
const GROUP_PROJECT = 'group-project';
- private $orderBy = 'order-modified';
const ORDER_PRIORITY = 'order-priority';
const ORDER_CREATED = 'order-created';
const ORDER_MODIFIED = 'order-modified';
const ORDER_TITLE = 'order-title';
private $needSubscriberPHIDs;
private $needProjectPHIDs;
private $blockingTasks;
private $blockedTasks;
public function withAuthors(array $authors) {
$this->authorPHIDs = $authors;
return $this;
}
public function withIDs(array $ids) {
$this->taskIDs = $ids;
return $this;
}
public function withPHIDs(array $phids) {
$this->taskPHIDs = $phids;
return $this;
}
public function withOwners(array $owners) {
$no_owner = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
$any_owner = PhabricatorPeopleAnyOwnerDatasource::FUNCTION_TOKEN;
foreach ($owners as $k => $phid) {
if ($phid === $no_owner || $phid === null) {
$this->noOwner = true;
unset($owners[$k]);
break;
}
if ($phid === $any_owner) {
$this->anyOwner = true;
unset($owners[$k]);
break;
}
}
$this->ownerPHIDs = $owners;
return $this;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withStatuses(array $statuses) {
$this->statuses = $statuses;
return $this;
}
public function withPriorities(array $priorities) {
$this->priorities = $priorities;
return $this;
}
public function withSubpriorities(array $subpriorities) {
$this->subpriorities = $subpriorities;
return $this;
}
public function withSubpriorityBetween($min, $max) {
$this->subpriorityMin = $min;
$this->subpriorityMax = $max;
return $this;
}
public function withSubscribers(array $subscribers) {
$this->subscriberPHIDs = $subscribers;
return $this;
}
public function withFullTextSearch($fulltext_search) {
$this->fullTextSearch = $fulltext_search;
return $this;
}
public function setGroupBy($group) {
$this->groupBy = $group;
- return $this;
- }
- public function setOrderBy($order) {
- $this->orderBy = $order;
+ switch ($this->groupBy) {
+ case self::GROUP_NONE:
+ $vector = array();
+ break;
+ case self::GROUP_PRIORITY:
+ $vector = array('priority');
+ break;
+ case self::GROUP_OWNER:
+ $vector = array('owner');
+ break;
+ case self::GROUP_STATUS:
+ $vector = array('status');
+ break;
+ case self::GROUP_PROJECT:
+ $vector = array('project');
+ break;
+ }
+
+ $this->setGroupVector($vector);
+
return $this;
}
/**
* True returns tasks that are blocking other tasks only.
* False returns tasks that are not blocking other tasks only.
* Null returns tasks regardless of blocking status.
*/
public function withBlockingTasks($mode) {
$this->blockingTasks = $mode;
return $this;
}
public function shouldJoinBlockingTasks() {
return $this->blockingTasks !== null;
}
/**
* True returns tasks that are blocked by other tasks only.
* False returns tasks that are not blocked by other tasks only.
* Null returns tasks regardless of blocked by status.
*/
public function withBlockedTasks($mode) {
$this->blockedTasks = $mode;
return $this;
}
public function shouldJoinBlockedTasks() {
return $this->blockedTasks !== null;
}
public function withDateCreatedBefore($date_created_before) {
$this->dateCreatedBefore = $date_created_before;
return $this;
}
public function withDateCreatedAfter($date_created_after) {
$this->dateCreatedAfter = $date_created_after;
return $this;
}
public function withDateModifiedBefore($date_modified_before) {
$this->dateModifiedBefore = $date_modified_before;
return $this;
}
public function withDateModifiedAfter($date_modified_after) {
$this->dateModifiedAfter = $date_modified_after;
return $this;
}
public function needSubscriberPHIDs($bool) {
$this->needSubscriberPHIDs = $bool;
return $this;
}
public function needProjectPHIDs($bool) {
$this->needProjectPHIDs = $bool;
return $this;
}
public function newResultObject() {
return new ManiphestTask();
}
- protected function willExecute() {
- // If we already have an order vector, use it as provided.
- // TODO: This is a messy hack to make setOrderVector() stronger than
- // setPriority().
- $vector = $this->getOrderVector();
- $keys = mpull(iterator_to_array($vector), 'getOrderKey');
- if (array_values($keys) !== array('id')) {
- return;
- }
-
- $parts = array();
- switch ($this->groupBy) {
- case self::GROUP_NONE:
- break;
- case self::GROUP_PRIORITY:
- $parts[] = array('priority');
- break;
- case self::GROUP_OWNER:
- $parts[] = array('owner');
- break;
- case self::GROUP_STATUS:
- $parts[] = array('status');
- break;
- case self::GROUP_PROJECT:
- $parts[] = array('project');
- break;
- }
-
- if ($this->applicationSearchOrders) {
- $columns = array();
- foreach ($this->applicationSearchOrders as $order) {
- $part = 'custom:'.$order['key'];
- if ($order['ascending']) {
- $part = '-'.$part;
- }
- $columns[] = $part;
- }
- $columns[] = 'id';
- $parts[] = $columns;
- } else {
- switch ($this->orderBy) {
- case self::ORDER_PRIORITY:
- $parts[] = array('priority', 'subpriority', 'id');
- break;
- case self::ORDER_CREATED:
- $parts[] = array('id');
- break;
- case self::ORDER_MODIFIED:
- $parts[] = array('updated', 'id');
- break;
- case self::ORDER_TITLE:
- $parts[] = array('title', 'id');
- break;
- }
- }
-
- $parts = array_mergev($parts);
- // We may have a duplicate column if we are both ordering and grouping
- // by priority.
- $parts = array_unique($parts);
- $this->setOrderVector($parts);
- }
-
protected function loadPage() {
$task_dao = new ManiphestTask();
$conn = $task_dao->establishConnection('r');
$where = array();
$where[] = $this->buildTaskIDsWhereClause($conn);
$where[] = $this->buildTaskPHIDsWhereClause($conn);
$where[] = $this->buildStatusWhereClause($conn);
$where[] = $this->buildStatusesWhereClause($conn);
$where[] = $this->buildDependenciesWhereClause($conn);
$where[] = $this->buildAuthorWhereClause($conn);
$where[] = $this->buildOwnerWhereClause($conn);
$where[] = $this->buildFullTextWhereClause($conn);
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn,
'task.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn,
'task.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->dateModifiedAfter) {
$where[] = qsprintf(
$conn,
'task.dateModified >= %d',
$this->dateModifiedAfter);
}
if ($this->dateModifiedBefore) {
$where[] = qsprintf(
$conn,
'task.dateModified <= %d',
$this->dateModifiedBefore);
}
if ($this->priorities) {
$where[] = qsprintf(
$conn,
'task.priority IN (%Ld)',
$this->priorities);
}
if ($this->subpriorities) {
$where[] = qsprintf(
$conn,
'task.subpriority IN (%Lf)',
$this->subpriorities);
}
if ($this->subpriorityMin) {
$where[] = qsprintf(
$conn,
'task.subpriority >= %f',
$this->subpriorityMin);
}
if ($this->subpriorityMax) {
$where[] = qsprintf(
$conn,
'task.subpriority <= %f',
$this->subpriorityMax);
}
$where[] = $this->buildWhereClauseParts($conn);
$where = $this->formatWhereClause($where);
$group_column = '';
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$group_column = qsprintf(
$conn,
', projectGroupName.indexedObjectPHID projectGroupPHID');
break;
}
$rows = queryfx_all(
$conn,
'%Q %Q FROM %T task %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$group_column,
$task_dao->getTableName(),
$this->buildJoinClause($conn),
$where,
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$data = ipull($rows, null, 'id');
break;
default:
$data = $rows;
break;
}
$tasks = $task_dao->loadAllFromArray($data);
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$results = array();
foreach ($rows as $row) {
$task = clone $tasks[$row['id']];
$task->attachGroupByProjectPHID($row['projectGroupPHID']);
$results[] = $task;
}
$tasks = $results;
break;
}
return $tasks;
}
protected function willFilterPage(array $tasks) {
if ($this->groupBy == self::GROUP_PROJECT) {
// We should only return project groups which the user can actually see.
$project_phids = mpull($tasks, 'getGroupByProjectPHID');
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($tasks as $key => $task) {
if (!$task->getGroupByProjectPHID()) {
// This task is either not in any projects, or only in projects
// which we're ignoring because they're being queried for explicitly.
continue;
}
if (empty($projects[$task->getGroupByProjectPHID()])) {
unset($tasks[$key]);
}
}
}
return $tasks;
}
protected function didFilterPage(array $tasks) {
$phids = mpull($tasks, 'getPHID');
if ($this->needProjectPHIDs) {
$edge_query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($phids)
->withEdgeTypes(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
));
$edge_query->execute();
foreach ($tasks as $task) {
$project_phids = $edge_query->getDestinationPHIDs(
array($task->getPHID()));
$task->attachProjectPHIDs($project_phids);
}
}
if ($this->needSubscriberPHIDs) {
$subscriber_sets = id(new PhabricatorSubscribersQuery())
->withObjectPHIDs($phids)
->execute();
foreach ($tasks as $task) {
$subscribers = idx($subscriber_sets, $task->getPHID(), array());
$task->attachSubscriberPHIDs($subscribers);
}
}
return $tasks;
}
private function buildTaskIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskIDs) {
return null;
}
return qsprintf(
$conn,
'task.id in (%Ld)',
$this->taskIDs);
}
private function buildTaskPHIDsWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->taskPHIDs) {
return null;
}
return qsprintf(
$conn,
'task.phid in (%Ls)',
$this->taskPHIDs);
}
private function buildStatusWhereClause(AphrontDatabaseConnection $conn) {
static $map = array(
self::STATUS_RESOLVED => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
self::STATUS_WONTFIX => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
self::STATUS_INVALID => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
self::STATUS_SPITE => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
self::STATUS_DUPLICATE => ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE,
);
switch ($this->status) {
case self::STATUS_ANY:
return null;
case self::STATUS_OPEN:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
case self::STATUS_CLOSED:
return qsprintf(
$conn,
'task.status IN (%Ls)',
ManiphestTaskStatus::getClosedStatusConstants());
default:
$constant = idx($map, $this->status);
if (!$constant) {
throw new Exception(pht("Unknown status query '%s'!", $this->status));
}
return qsprintf(
$conn,
'task.status = %s',
$constant);
}
}
private function buildStatusesWhereClause(AphrontDatabaseConnection $conn) {
if ($this->statuses) {
return qsprintf(
$conn,
'task.status IN (%Ls)',
$this->statuses);
}
return null;
}
private function buildAuthorWhereClause(AphrontDatabaseConnection $conn) {
if (!$this->authorPHIDs) {
return null;
}
return qsprintf(
$conn,
'task.authorPHID in (%Ls)',
$this->authorPHIDs);
}
private function buildOwnerWhereClause(AphrontDatabaseConnection $conn) {
$subclause = array();
if ($this->noOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NULL');
}
if ($this->anyOwner) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IS NOT NULL');
}
if ($this->ownerPHIDs) {
$subclause[] = qsprintf(
$conn,
'task.ownerPHID IN (%Ls)',
$this->ownerPHIDs);
}
if (!$subclause) {
return '';
}
return '('.implode(') OR (', $subclause).')';
}
private function buildFullTextWhereClause(AphrontDatabaseConnection $conn) {
if (!strlen($this->fullTextSearch)) {
return null;
}
// In doing a fulltext search, we first find all the PHIDs that match the
// fulltext search, and then use that to limit the rest of the search
$fulltext_query = id(new PhabricatorSavedQuery())
->setEngineClassName('PhabricatorSearchApplicationSearchEngine')
->setParameter('query', $this->fullTextSearch);
// NOTE: Setting this to something larger than 2^53 will raise errors in
// ElasticSearch, and billions of results won't fit in memory anyway.
$fulltext_query->setParameter('limit', 100000);
$fulltext_query->setParameter('types',
array(ManiphestTaskPHIDType::TYPECONST));
$engine = PhabricatorSearchEngine::loadEngine();
$fulltext_results = $engine->executeSearch($fulltext_query);
if (empty($fulltext_results)) {
$fulltext_results = array(null);
}
return qsprintf(
$conn,
'task.phid IN (%Ls)',
$fulltext_results);
}
private function buildDependenciesWhereClause(
AphrontDatabaseConnection $conn) {
if (!$this->shouldJoinBlockedTasks() &&
!$this->shouldJoinBlockingTasks()) {
return null;
}
$parts = array();
if ($this->blockingTasks === true) {
$parts[] = qsprintf(
$conn,
'blocking.dst IS NOT NULL AND blockingtask.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
} else if ($this->blockingTasks === false) {
$parts[] = qsprintf(
$conn,
'blocking.dst IS NULL OR blockingtask.status NOT IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
}
if ($this->blockedTasks === true) {
$parts[] = qsprintf(
$conn,
'blocked.dst IS NOT NULL AND blockedtask.status IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
} else if ($this->blockedTasks === false) {
$parts[] = qsprintf(
$conn,
'blocked.dst IS NULL OR blockedtask.status NOT IN (%Ls)',
ManiphestTaskStatus::getOpenStatusConstants());
}
return '('.implode(') OR (', $parts).')';
}
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$joins = array();
if ($this->shouldJoinBlockingTasks()) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T blocking ON blocking.src = task.phid '.
'AND blocking.type = %d '.
'LEFT JOIN %T blockingtask ON blocking.dst = blockingtask.phid',
$edge_table,
ManiphestTaskDependedOnByTaskEdgeType::EDGECONST,
id(new ManiphestTask())->getTableName());
}
if ($this->shouldJoinBlockedTasks()) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T blocked ON blocked.src = task.phid '.
'AND blocked.type = %d '.
'LEFT JOIN %T blockedtask ON blocked.dst = blockedtask.phid',
$edge_table,
ManiphestTaskDependsOnTaskEdgeType::EDGECONST,
id(new ManiphestTask())->getTableName());
}
if ($this->subscriberPHIDs) {
$joins[] = qsprintf(
$conn_r,
'JOIN %T e_ccs ON e_ccs.src = task.phid '.
'AND e_ccs.type = %s '.
'AND e_ccs.dst in (%Ls)',
PhabricatorEdgeConfig::TABLE_NAME_EDGE,
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
$this->subscriberPHIDs);
}
switch ($this->groupBy) {
case self::GROUP_PROJECT:
$ignore_group_phids = $this->getIgnoreGroupedProjectPHIDs();
if ($ignore_group_phids) {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d
AND projectGroup.dst NOT IN (%Ls)',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$ignore_group_phids);
} else {
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroup ON task.phid = projectGroup.src
AND projectGroup.type = %d',
$edge_table,
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST);
}
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T projectGroupName
ON projectGroup.dst = projectGroupName.indexedObjectPHID',
id(new ManiphestNameIndex())->getTableName());
break;
}
$joins[] = parent::buildJoinClauseParts($conn_r);
return $joins;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn_r) {
$joined_multiple_rows = $this->shouldJoinBlockingTasks() ||
$this->shouldJoinBlockedTasks() ||
($this->shouldGroupQueryResultRows());
$joined_project_name = ($this->groupBy == self::GROUP_PROJECT);
// If we're joining multiple rows, we need to group the results by the
// task IDs.
if ($joined_multiple_rows) {
if ($joined_project_name) {
return 'GROUP BY task.phid, projectGroup.dst';
} else {
return 'GROUP BY task.phid';
}
} else {
return '';
}
}
/**
* Return project PHIDs which we should ignore when grouping tasks by
* project. For example, if a user issues a query like:
*
* Tasks in all projects: Frontend, Bugs
*
* ...then we don't show "Frontend" or "Bugs" groups in the result set, since
* they're meaningless as all results are in both groups.
*
* Similarly, for queries like:
*
* Tasks in any projects: Public Relations
*
* ...we ignore the single project, as every result is in that project. (In
* the case that there are several "any" projects, we do not ignore them.)
*
* @return list<phid> Project PHIDs which should be ignored in query
* construction.
*/
private function getIgnoreGroupedProjectPHIDs() {
// Maybe we should also exclude the "OPERATOR_NOT" PHIDs? It won't
// impact the results, but we might end up with a better query plan.
// Investigate this on real data? This is likely very rare.
$edge_types = array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
);
$phids = array();
$phids[] = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_AND,
));
$any = $this->getEdgeLogicValues(
$edge_types,
array(
PhabricatorQueryConstraint::OPERATOR_OR,
));
if (count($any) == 1) {
$phids[] = $any;
}
return array_mergev($phids);
}
protected function getResultCursor($result) {
$id = $result->getID();
if ($this->groupBy == self::GROUP_PROJECT) {
return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.');
}
return $id;
}
+ public function getBuiltinOrders() {
+ $orders = array(
+ 'priority' => array(
+ 'vector' => array('priority', 'subpriority', 'id'),
+ 'name' => pht('Priority'),
+ 'aliases' => array(self::ORDER_PRIORITY),
+ ),
+ 'updated' => array(
+ 'vector' => array('updated', 'id'),
+ 'name' => pht('Date Updated'),
+ 'aliases' => array(self::ORDER_MODIFIED),
+ ),
+ 'title' => array(
+ 'vector' => array('title', 'id'),
+ 'name' => pht('Title'),
+ 'aliases' => array(self::ORDER_TITLE),
+ ),
+ ) + parent::getBuiltinOrders();
+
+ // Alias the "newest" builtin to the historical key for it.
+ $orders['newest']['aliases'][] = self::ORDER_CREATED;
+
+ $orders = array_select_keys(
+ $orders,
+ array(
+ 'priority',
+ 'updated',
+ 'newest',
+ 'oldest',
+ 'title',
+ )) + $orders;
+
+ return $orders;
+ }
+
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'priority' => array(
'table' => 'task',
'column' => 'priority',
'type' => 'int',
),
'owner' => array(
'table' => 'task',
'column' => 'ownerOrdering',
'null' => 'head',
'reverse' => true,
'type' => 'string',
),
'status' => array(
'table' => 'task',
'column' => 'status',
'type' => 'string',
'reverse' => true,
),
'project' => array(
'table' => 'projectGroupName',
'column' => 'indexedObjectName',
'type' => 'string',
'null' => 'head',
'reverse' => true,
),
'title' => array(
'table' => 'task',
'column' => 'title',
'type' => 'string',
'reverse' => true,
),
'subpriority' => array(
'table' => 'task',
'column' => 'subpriority',
'type' => 'float',
),
'updated' => array(
'table' => 'task',
'column' => 'dateModified',
'type' => 'int',
),
);
}
protected function getPagingValueMap($cursor, array $keys) {
$cursor_parts = explode('.', $cursor, 2);
$task_id = $cursor_parts[0];
$group_id = idx($cursor_parts, 1);
$task = $this->loadCursorObject($task_id);
$map = array(
'id' => $task->getID(),
'priority' => $task->getPriority(),
'subpriority' => $task->getSubpriority(),
'owner' => $task->getOwnerOrdering(),
'status' => $task->getStatus(),
'title' => $task->getTitle(),
'updated' => $task->getDateModified(),
);
foreach ($keys as $key) {
switch ($key) {
case 'project':
$value = null;
if ($group_id) {
$paging_projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->withPHIDs(array($group_id))
->execute();
if ($paging_projects) {
$value = head($paging_projects)->getName();
}
}
$map[$key] = $value;
break;
}
}
foreach ($keys as $key) {
if ($this->isCustomFieldOrderKey($key)) {
$map += $this->getPagingValueMapForCustomFields($task);
break;
}
}
return $map;
}
protected function getPrimaryTableAlias() {
return 'task';
}
public function getQueryApplicationClass() {
return 'PhabricatorManiphestApplication';
}
}
diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
index c587de5c10..1cdb71994d 100644
--- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
+++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php
@@ -1,521 +1,505 @@
<?php
final class ManiphestTaskSearchEngine
extends PhabricatorApplicationSearchEngine {
private $showBatchControls;
private $baseURI;
private $isBoardView;
public function setIsBoardView($is_board_view) {
$this->isBoardView = $is_board_view;
return $this;
}
public function getIsBoardView() {
return $this->isBoardView;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
return $this;
}
public function getResultTypeDescription() {
return pht('Tasks');
}
public function getApplicationClassName() {
return 'PhabricatorManiphestApplication';
}
- public function getCustomFieldObject() {
- return new ManiphestTask();
+ public function newQuery() {
+ return id(new ManiphestTaskQuery())
+ ->needProjectPHIDs(true);
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter(
'assignedPHIDs',
$this->readUsersFromRequest($request, 'assigned'));
$saved->setParameter(
'authorPHIDs',
$this->readUsersFromRequest($request, 'authors'));
$saved->setParameter(
'subscriberPHIDs',
$this->readSubscribersFromRequest($request, 'subscribers'));
$saved->setParameter(
'statuses',
$this->readListFromRequest($request, 'statuses'));
$saved->setParameter(
'priorities',
$this->readListFromRequest($request, 'priorities'));
$saved->setParameter(
'blocking',
$this->readBoolFromRequest($request, 'blocking'));
$saved->setParameter(
'blocked',
$this->readBoolFromRequest($request, 'blocked'));
$saved->setParameter('group', $request->getStr('group'));
$saved->setParameter('order', $request->getStr('order'));
$ids = $request->getStrList('ids');
foreach ($ids as $key => $id) {
$id = trim($id, ' Tt');
if (!$id || !is_numeric($id)) {
unset($ids[$key]);
} else {
$ids[$key] = $id;
}
}
$saved->setParameter('ids', $ids);
$saved->setParameter('fulltext', $request->getStr('fulltext'));
$saved->setParameter(
'projects',
$this->readProjectsFromRequest($request, 'projects'));
$saved->setParameter('createdStart', $request->getStr('createdStart'));
$saved->setParameter('createdEnd', $request->getStr('createdEnd'));
$saved->setParameter('modifiedStart', $request->getStr('modifiedStart'));
$saved->setParameter('modifiedEnd', $request->getStr('modifiedEnd'));
$limit = $request->getInt('limit');
if ($limit > 0) {
$saved->setParameter('limit', $limit);
}
$this->readCustomFieldsFromRequest($request, $saved);
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
- $query = id(new ManiphestTaskQuery())
- ->needProjectPHIDs(true);
+ $query = $this->newQuery();
$viewer = $this->requireViewer();
$datasource = id(new PhabricatorPeopleUserFunctionDatasource())
->setViewer($viewer);
$author_phids = $saved->getParameter('authorPHIDs', array());
$author_phids = $datasource->evaluateTokens($author_phids);
if ($author_phids) {
$query->withAuthors($author_phids);
}
$datasource = id(new PhabricatorMetaMTAMailableFunctionDatasource())
->setViewer($viewer);
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$subscriber_phids = $datasource->evaluateTokens($subscriber_phids);
if ($subscriber_phids) {
$query->withSubscribers($subscriber_phids);
}
$datasource = id(new PhabricatorPeopleOwnerDatasource())
->setViewer($this->requireViewer());
$assigned_phids = $this->readAssignedPHIDs($saved);
$assigned_phids = $datasource->evaluateTokens($assigned_phids);
if ($assigned_phids) {
$query->withOwners($assigned_phids);
}
$datasource = id(new ManiphestTaskStatusFunctionDatasource())
->setViewer($this->requireViewer());
$statuses = $saved->getParameter('statuses', array());
$statuses = $datasource->evaluateTokens($statuses);
if ($statuses) {
$query->withStatuses($statuses);
}
$priorities = $saved->getParameter('priorities', array());
if ($priorities) {
$query->withPriorities($priorities);
}
$query->withBlockingTasks($saved->getParameter('blocking'));
$query->withBlockedTasks($saved->getParameter('blocked'));
- $this->applyOrderByToQuery(
- $query,
- $this->getOrderValues(),
- $saved->getParameter('order'));
+ // TODO: This is glue that will be obsolete soon.
+ $order = $saved->getParameter('order');
+ $builtin = $query->getBuiltinOrderAliasMap();
+ if (strlen($order) && isset($builtin[$order])) {
+ $query->setOrder($order);
+ } else {
+ $query->setOrder(head_key($builtin));
+ }
$group = $saved->getParameter('group');
$group = idx($this->getGroupValues(), $group);
if ($group) {
$query->setGroupBy($group);
} else {
$query->setGroupBy(head($this->getGroupValues()));
}
$ids = $saved->getParameter('ids');
if ($ids) {
$query->withIDs($ids);
}
$fulltext = $saved->getParameter('fulltext');
if (strlen($fulltext)) {
$query->withFullTextSearch($fulltext);
}
$projects = $this->readProjectTokens($saved);
$adjusted = id(clone $saved)->setParameter('projects', $projects);
$this->setQueryProjects($query, $adjusted);
$start = $this->parseDateTime($saved->getParameter('createdStart'));
$end = $this->parseDateTime($saved->getParameter('createdEnd'));
if ($start) {
$query->withDateCreatedAfter($start);
}
if ($end) {
$query->withDateCreatedBefore($end);
}
$mod_start = $this->parseDateTime($saved->getParameter('modifiedStart'));
$mod_end = $this->parseDateTime($saved->getParameter('modifiedEnd'));
if ($mod_start) {
$query->withDateModifiedAfter($mod_start);
}
if ($mod_end) {
$query->withDateModifiedBefore($mod_end);
}
$this->applyCustomFieldsToQuery($query, $saved);
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$assigned_phids = $this->readAssignedPHIDs($saved);
$author_phids = $saved->getParameter('authorPHIDs', array());
$projects = $this->readProjectTokens($saved);
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$statuses = $saved->getParameter('statuses', array());
$priorities = $saved->getParameter('priorities', array());
$blocking_control = id(new AphrontFormSelectControl())
->setLabel(pht('Blocking'))
->setName('blocking')
->setValue($this->getBoolFromQuery($saved, 'blocking'))
->setOptions(array(
'' => pht('Show All Tasks'),
'true' => pht('Show Tasks Blocking Other Tasks'),
'false' => pht('Show Tasks Not Blocking Other Tasks'),
));
$blocked_control = id(new AphrontFormSelectControl())
->setLabel(pht('Blocked'))
->setName('blocked')
->setValue($this->getBoolFromQuery($saved, 'blocked'))
->setOptions(array(
'' => pht('Show All Tasks'),
'true' => pht('Show Tasks Blocked By Other Tasks'),
'false' => pht('Show Tasks Not Blocked By Other Tasks'),
));
$ids = $saved->getParameter('ids', array());
- $builtin_orders = $this->getOrderOptions();
- $custom_orders = $this->getCustomFieldOrderOptions();
- $all_orders = $builtin_orders + $custom_orders;
+ $all_orders = ipull($this->newQuery()->getBuiltinOrders(), 'name');
$form
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleOwnerDatasource())
->setName('assigned')
->setLabel(pht('Assigned To'))
->setValue($assigned_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorProjectLogicalDatasource())
->setName('projects')
->setLabel(pht('Projects'))
->setValue($projects))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorPeopleUserFunctionDatasource())
->setName('authors')
->setLabel(pht('Authors'))
->setValue($author_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new PhabricatorMetaMTAMailableFunctionDatasource())
->setName('subscribers')
->setLabel(pht('Subscribers'))
->setValue($subscriber_phids))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new ManiphestTaskStatusFunctionDatasource())
->setLabel(pht('Statuses'))
->setName('statuses')
->setValue($statuses))
->appendControl(
id(new AphrontFormTokenizerControl())
->setDatasource(new ManiphestTaskPriorityDatasource())
->setLabel(pht('Priorities'))
->setName('priorities')
->setValue($priorities))
->appendChild(
id(new AphrontFormTextControl())
->setName('fulltext')
->setLabel(pht('Contains Words'))
->setValue($saved->getParameter('fulltext')))
->appendChild($blocking_control)
->appendChild($blocked_control);
if (!$this->getIsBoardView()) {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setName('group')
->setLabel(pht('Group By'))
->setValue($saved->getParameter('group'))
->setOptions($this->getGroupOptions()))
->appendChild(
id(new AphrontFormSelectControl())
->setName('order')
->setLabel(pht('Order By'))
->setValue($saved->getParameter('order'))
->setOptions($all_orders));
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('ids')
->setLabel(pht('Task IDs'))
->setValue(implode(', ', $ids)));
$this->appendCustomFieldsToForm($form, $saved);
$this->buildDateRange(
$form,
$saved,
'createdStart',
pht('Created After'),
'createdEnd',
pht('Created Before'));
$this->buildDateRange(
$form,
$saved,
'modifiedStart',
pht('Updated After'),
'modifiedEnd',
pht('Updated Before'));
if (!$this->getIsBoardView()) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('limit')
->setLabel(pht('Page Size'))
->setValue($saved->getParameter('limit', 100)));
}
}
protected function getURI($path) {
if ($this->baseURI) {
return $this->baseURI.$path;
}
return '/maniphest/'.$path;
}
protected function getBuiltinQueryNames() {
$names = array();
if ($this->requireViewer()->isLoggedIn()) {
$names['assigned'] = pht('Assigned');
$names['authored'] = pht('Authored');
$names['subscribed'] = pht('Subscribed');
}
$names['open'] = pht('Open Tasks');
$names['all'] = pht('All Tasks');
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
$viewer_phid = $this->requireViewer()->getPHID();
switch ($query_key) {
case 'all':
return $query;
case 'assigned':
return $query
->setParameter('assignedPHIDs', array($viewer_phid))
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'subscribed':
return $query
->setParameter('subscriberPHIDs', array($viewer_phid))
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'open':
return $query
->setParameter(
'statuses',
ManiphestTaskStatus::getOpenStatusConstants());
case 'authored':
return $query
->setParameter('authorPHIDs', array($viewer_phid))
->setParameter('order', 'created')
->setParameter('group', 'none');
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
- private function getOrderOptions() {
- return array(
- 'priority' => pht('Priority'),
- 'updated' => pht('Date Updated'),
- 'created' => pht('Date Created'),
- 'title' => pht('Title'),
- );
- }
-
- private function getOrderValues() {
- return array(
- 'priority' => ManiphestTaskQuery::ORDER_PRIORITY,
- 'updated' => ManiphestTaskQuery::ORDER_MODIFIED,
- 'created' => ManiphestTaskQuery::ORDER_CREATED,
- 'title' => ManiphestTaskQuery::ORDER_TITLE,
- );
- }
-
private function getGroupOptions() {
return array(
'priority' => pht('Priority'),
'assigned' => pht('Assigned'),
'status' => pht('Status'),
'project' => pht('Project'),
'none' => pht('None'),
);
}
private function getGroupValues() {
return array(
'priority' => ManiphestTaskQuery::GROUP_PRIORITY,
'assigned' => ManiphestTaskQuery::GROUP_OWNER,
'status' => ManiphestTaskQuery::GROUP_STATUS,
'project' => ManiphestTaskQuery::GROUP_PROJECT,
'none' => ManiphestTaskQuery::GROUP_NONE,
);
}
protected function renderResultList(
array $tasks,
PhabricatorSavedQuery $saved,
array $handles) {
$viewer = $this->requireViewer();
if ($this->isPanelContext()) {
$can_edit_priority = false;
$can_bulk_edit = false;
} else {
$can_edit_priority = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getApplication(),
ManiphestEditPriorityCapability::CAPABILITY);
$can_bulk_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$this->getApplication(),
ManiphestBulkEditCapability::CAPABILITY);
}
return id(new ManiphestTaskResultListView())
->setUser($viewer)
->setTasks($tasks)
->setSavedQuery($saved)
->setCanEditPriority($can_edit_priority)
->setCanBatchEdit($can_bulk_edit)
->setShowBatchControls($this->showBatchControls);
}
private function readAssignedPHIDs(PhabricatorSavedQuery $saved) {
$assigned_phids = $saved->getParameter('assignedPHIDs', array());
// This may be present in old saved queries from before parameterized
// typeaheads, and is retained for compatibility. We could remove it by
// migrating old saved queries.
if ($saved->getParameter('withUnassigned')) {
$assigned_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN;
}
return $assigned_phids;
}
private function readProjectTokens(PhabricatorSavedQuery $saved) {
$projects = $saved->getParameter('projects', array());
$all = $saved->getParameter('allProjectPHIDs', array());
foreach ($all as $phid) {
$projects[] = $phid;
}
$any = $saved->getParameter('anyProjectPHIDs', array());
foreach ($any as $phid) {
$projects[] = 'any('.$phid.')';
}
$not = $saved->getParameter('excludeProjectPHIDs', array());
foreach ($not as $phid) {
$projects[] = 'not('.$phid.')';
}
$users = $saved->getParameter('userProjectPHIDs', array());
foreach ($users as $phid) {
$projects[] = 'projects('.$phid.')';
}
$no = $saved->getParameter('withNoProject');
if ($no) {
$projects[] = 'null()';
}
return $projects;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
index b9c366fecc..bcb2b281ad 100644
--- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php
+++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php
@@ -1,745 +1,745 @@
<?php
final class PhabricatorProjectBoardViewController
extends PhabricatorProjectBoardController {
const BATCH_EDIT_ALL = 'all';
private $id;
private $slug;
private $handles;
private $queryKey;
private $filter;
private $sortKey;
private $showHidden;
public function shouldAllowPublic() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getUser();
$id = $request->getURIData('id');
$show_hidden = $request->getBool('hidden');
$this->showHidden = $show_hidden;
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needImages(true);
$id = $request->getURIData('id');
$slug = $request->getURIData('slug');
if ($slug) {
$project->withSlugs(array($slug));
} else {
$project->withIDs(array($id));
}
$project = $project->executeOne();
if (!$project) {
return new Aphront404Response();
}
$this->setProject($project);
$this->id = $project->getID();
$sort_key = $request->getStr('order');
switch ($sort_key) {
case PhabricatorProjectColumn::ORDER_NATURAL:
case PhabricatorProjectColumn::ORDER_PRIORITY:
break;
default:
$sort_key = PhabricatorProjectColumn::DEFAULT_ORDER;
break;
}
$this->sortKey = $sort_key;
$column_query = id(new PhabricatorProjectColumnQuery())
->setViewer($viewer)
->withProjectPHIDs(array($project->getPHID()));
if (!$show_hidden) {
$column_query->withStatuses(
array(PhabricatorProjectColumn::STATUS_ACTIVE));
}
$columns = $column_query->execute();
$columns = mpull($columns, null, 'getSequence');
// TODO: Expand the checks here if we add the ability
// to hide the Backlog column
if (!$columns) {
switch ($request->getStr('initialize-type')) {
case 'backlog-only':
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$column = PhabricatorProjectColumn::initializeNewColumn($viewer)
->setSequence(0)
->setProperty('isDefault', true)
->setProjectPHID($project->getPHID())
->save();
$column->attachProject($project);
$columns[0] = $column;
unset($unguarded);
break;
case 'import':
return id(new AphrontRedirectResponse())
->setURI(
$this->getApplicationURI('board/'.$project->getID().'/import/'));
break;
default:
return $this->initializeWorkboardDialog($project);
break;
}
}
ksort($columns);
$board_uri = $this->getApplicationURI('board/'.$project->getID().'/');
$engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer)
->setBaseURI($board_uri)
->setIsBoardView(true);
if ($request->isFormPost()) {
$saved = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved);
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$engine->buildSearchForm($filter_form, $saved);
if ($engine->getErrors()) {
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setErrors($engine->getErrors())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
return id(new AphrontRedirectResponse())->setURI(
$this->getURIWithState(
$engine->getQueryResultsPageURI($saved->getQueryKey())));
}
$query_key = $request->getURIData('queryKey');
if (!$query_key) {
$query_key = 'open';
}
$this->queryKey = $query_key;
$custom_query = null;
if ($engine->isBuiltinQuery($query_key)) {
$saved = $engine->buildSavedQueryFromBuiltin($query_key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved) {
return new Aphront404Response();
}
$custom_query = $saved;
}
if ($request->getURIData('filter')) {
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$engine->buildSearchForm($filter_form, $saved);
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
$task_query = $engine->buildQueryFromSavedQuery($saved);
$tasks = $task_query
->withEdgeLogicPHIDs(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_AND,
array($project->getPHID()))
- ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
+ ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY)
->setViewer($viewer)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
if ($tasks) {
$positions = id(new PhabricatorProjectColumnPositionQuery())
->setViewer($viewer)
->withObjectPHIDs(mpull($tasks, 'getPHID'))
->withColumns($columns)
->execute();
$positions = mpull($positions, null, 'getObjectPHID');
} else {
$positions = array();
}
$task_map = array();
foreach ($tasks as $task) {
$task_phid = $task->getPHID();
if (empty($positions[$task_phid])) {
// This shouldn't normally be possible because we create positions on
// demand, but we might have raced as an object was removed from the
// board. Just drop the task if we don't have a position for it.
continue;
}
$position = $positions[$task_phid];
$task_map[$position->getColumnPHID()][] = $task_phid;
}
// If we're showing the board in "natural" order, sort columns by their
// column positions.
if ($this->sortKey == PhabricatorProjectColumn::ORDER_NATURAL) {
foreach ($task_map as $column_phid => $task_phids) {
$order = array();
foreach ($task_phids as $task_phid) {
if (isset($positions[$task_phid])) {
$order[$task_phid] = $positions[$task_phid]->getOrderingKey();
} else {
$order[$task_phid] = 0;
}
}
asort($order);
$task_map[$column_phid] = array_keys($order);
}
}
$task_can_edit_map = id(new PhabricatorPolicyFilter())
->setViewer($viewer)
->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
->apply($tasks);
// If this is a batch edit, select the editable tasks in the chosen column
// and ship the user into the batch editor.
$batch_edit = $request->getStr('batch');
if ($batch_edit) {
if ($batch_edit !== self::BATCH_EDIT_ALL) {
$column_id_map = mpull($columns, null, 'getID');
$batch_column = idx($column_id_map, $batch_edit);
if (!$batch_column) {
return new Aphront404Response();
}
$batch_task_phids = idx($task_map, $batch_column->getPHID(), array());
foreach ($batch_task_phids as $key => $batch_task_phid) {
if (empty($task_can_edit_map[$batch_task_phid])) {
unset($batch_task_phids[$key]);
}
}
$batch_tasks = array_select_keys($tasks, $batch_task_phids);
} else {
$batch_tasks = $task_can_edit_map;
}
if (!$batch_tasks) {
$cancel_uri = $this->getURIWithState($board_uri);
return $this->newDialog()
->setTitle(pht('No Editable Tasks'))
->appendParagraph(
pht(
'The selected column contains no visible tasks which you '.
'have permission to edit.'))
->addCancelButton($board_uri);
}
$batch_ids = mpull($batch_tasks, 'getID');
$batch_ids = implode(',', $batch_ids);
$batch_uri = new PhutilURI('/maniphest/batch/');
$batch_uri->setQueryParam('board', $this->id);
$batch_uri->setQueryParam('batch', $batch_ids);
return id(new AphrontRedirectResponse())
->setURI($batch_uri);
}
$board_id = celerity_generate_unique_node_id();
$board = id(new PHUIWorkboardView())
->setUser($viewer)
->setID($board_id);
$behavior_config = array(
'boardID' => $board_id,
'projectPHID' => $project->getPHID(),
'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'),
'createURI' => '/maniphest/task/create/',
'order' => $this->sortKey,
);
$this->initBehavior(
'project-boards',
$behavior_config);
$this->addExtraQuickSandConfig(array('boardConfig' => $behavior_config));
$this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks);
foreach ($columns as $column) {
$task_phids = idx($task_map, $column->getPHID(), array());
$column_tasks = array_select_keys($tasks, $task_phids);
$panel = id(new PHUIWorkpanelView())
->setHeader($column->getDisplayName())
->setSubHeader($column->getDisplayType())
->addSigil('workpanel');
$header_icon = $column->getHeaderIcon();
if ($header_icon) {
$panel->setHeaderIcon($header_icon);
}
if ($column->isHidden()) {
$panel->addClass('project-panel-hidden');
}
$column_menu = $this->buildColumnMenu($project, $column);
$panel->addHeaderAction($column_menu);
$tag_id = celerity_generate_unique_node_id();
$tag_content_id = celerity_generate_unique_node_id();
$count_tag = id(new PHUITagView())
->setType(PHUITagView::TYPE_SHADE)
->setShade(PHUITagView::COLOR_BLUE)
->setID($tag_id)
->setName(phutil_tag('span', array('id' => $tag_content_id), '-'))
->setStyle('display: none');
$panel->setHeaderTag($count_tag);
$cards = id(new PHUIObjectItemListView())
->setUser($viewer)
->setFlush(true)
->setAllowEmptyList(true)
->addSigil('project-column')
->setMetadata(
array(
'columnPHID' => $column->getPHID(),
'countTagID' => $tag_id,
'countTagContentID' => $tag_content_id,
'pointLimit' => $column->getPointLimit(),
));
foreach ($column_tasks as $task) {
$owner = null;
if ($task->getOwnerPHID()) {
$owner = $this->handles[$task->getOwnerPHID()];
}
$can_edit = idx($task_can_edit_map, $task->getPHID(), false);
$cards->addItem(id(new ProjectBoardTaskCard())
->setViewer($viewer)
->setTask($task)
->setOwner($owner)
->setCanEdit($can_edit)
->getItem());
}
$panel->setCards($cards);
$board->addPanel($panel);
}
$sort_menu = $this->buildSortMenu(
$viewer,
$sort_key);
$filter_menu = $this->buildFilterMenu(
$viewer,
$custom_query,
$engine,
$query_key);
$manage_menu = $this->buildManageMenu($project, $show_hidden);
$header_link = phutil_tag(
'a',
array(
'href' => $this->getApplicationURI('profile/'.$project->getID().'/'),
),
$project->getName());
$header = id(new PHUIHeaderView())
->setHeader(pht('%s Workboard', $header_link))
->setUser($viewer)
->setNoBackground(true)
->addActionLink($sort_menu)
->addActionLink($filter_menu)
->addActionLink($manage_menu)
->setPolicyObject($project);
$board_box = id(new PHUIBoxView())
->appendChild($board)
->addClass('project-board-wrapper');
$nav = $this->buildIconNavView($project);
$nav->appendChild($header);
$nav->appendChild($board_box);
return $this->buildApplicationPage(
$nav,
array(
'title' => pht('%s Board', $project->getName()),
'showFooter' => false,
'pageObjects' => array($project->getPHID()),
));
}
private function buildSortMenu(
PhabricatorUser $viewer,
$sort_key) {
$sort_icon = id(new PHUIIconView())
->setIconFont('fa-sort-amount-asc bluegrey');
$named = array(
PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'),
PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'),
);
$base_uri = $this->getURIWithState();
$items = array();
foreach ($named as $key => $name) {
$is_selected = ($key == $sort_key);
if ($is_selected) {
$active_order = $name;
}
$item = id(new PhabricatorActionView())
->setIcon('fa-sort-amount-asc')
->setSelected($is_selected)
->setName($name);
$uri = $base_uri->alter('order', $key);
$item->setHref($uri);
$items[] = $item;
}
$sort_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$sort_menu->addAction($item);
}
$sort_button = id(new PHUIButtonView())
->setText(pht('Sort: %s', $active_order))
->setIcon($sort_icon)
->setTag('a')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $sort_menu),
));
return $sort_button;
}
private function buildFilterMenu(
PhabricatorUser $viewer,
$custom_query,
PhabricatorApplicationSearchEngine $engine,
$query_key) {
$filter_icon = id(new PHUIIconView())
->setIconFont('fa-search-plus bluegrey');
$named = array(
'open' => pht('Open Tasks'),
'all' => pht('All Tasks'),
);
if ($viewer->isLoggedIn()) {
$named['assigned'] = pht('Assigned to Me');
}
if ($custom_query) {
$named[$custom_query->getQueryKey()] = pht('Custom Filter');
}
$items = array();
foreach ($named as $key => $name) {
$is_selected = ($key == $query_key);
if ($is_selected) {
$active_filter = $name;
}
$is_custom = false;
if ($custom_query) {
$is_custom = ($key == $custom_query->getQueryKey());
}
$item = id(new PhabricatorActionView())
->setIcon('fa-search')
->setSelected($is_selected)
->setName($name);
if ($is_custom) {
$uri = $this->getApplicationURI(
'board/'.$this->id.'/filter/query/'.$key.'/');
$item->setWorkflow(true);
} else {
$uri = $engine->getQueryResultsPageURI($key);
}
$uri = $this->getURIWithState($uri);
$item->setHref($uri);
$items[] = $item;
}
$items[] = id(new PhabricatorActionView())
->setIcon('fa-cog')
->setHref($this->getApplicationURI('board/'.$this->id.'/filter/'))
->setWorkflow(true)
->setName(pht('Advanced Filter...'));
$filter_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$filter_menu->addAction($item);
}
$filter_button = id(new PHUIButtonView())
->setText(pht('Filter: %s', $active_filter))
->setIcon($filter_icon)
->setTag('a')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $filter_menu),
));
return $filter_button;
}
private function buildManageMenu(
PhabricatorProject $project,
$show_hidden) {
$request = $this->getRequest();
$viewer = $request->getUser();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$manage_icon = id(new PHUIIconView())
->setIconFont('fa-cog bluegrey');
$manage_items = array();
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-plus')
->setName(pht('Add Column'))
->setHref($this->getApplicationURI('board/'.$this->id.'/edit/'))
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-exchange')
->setName(pht('Reorder Columns'))
->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/'))
->setDisabled(!$can_edit)
->setWorkflow(true);
if ($show_hidden) {
$hidden_uri = $this->getURIWithState()
->setQueryParam('hidden', null);
$hidden_icon = 'fa-eye-slash';
$hidden_text = pht('Hide Hidden Columns');
} else {
$hidden_uri = $this->getURIWithState()
->setQueryParam('hidden', 'true');
$hidden_icon = 'fa-eye';
$hidden_text = pht('Show Hidden Columns');
}
$manage_items[] = id(new PhabricatorActionView())
->setIcon($hidden_icon)
->setName($hidden_text)
->setHref($hidden_uri);
$batch_edit_uri = $request->getRequestURI();
$batch_edit_uri->setQueryParam('batch', self::BATCH_EDIT_ALL);
$can_batch_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
ManiphestBulkEditCapability::CAPABILITY);
$manage_items[] = id(new PhabricatorActionView())
->setIcon('fa-list-ul')
->setName(pht('Batch Edit Visible Tasks...'))
->setHref($batch_edit_uri)
->setDisabled(!$can_batch_edit);
$manage_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($manage_items as $item) {
$manage_menu->addAction($item);
}
$manage_button = id(new PHUIButtonView())
->setText(pht('Manage Board'))
->setIcon($manage_icon)
->setTag('a')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $manage_menu),
));
return $manage_button;
}
private function buildColumnMenu(
PhabricatorProject $project,
PhabricatorProjectColumn $column) {
$request = $this->getRequest();
$viewer = $request->getUser();
$can_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
$project,
PhabricatorPolicyCapability::CAN_EDIT);
$column_items = array();
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-plus')
->setName(pht('Create Task...'))
->setHref('/maniphest/task/create/')
->addSigil('column-add-task')
->setMetadata(
array(
'columnPHID' => $column->getPHID(),
))
->setDisabled(!$can_edit);
$batch_edit_uri = $request->getRequestURI();
$batch_edit_uri->setQueryParam('batch', $column->getID());
$can_batch_edit = PhabricatorPolicyFilter::hasCapability(
$viewer,
PhabricatorApplication::getByClass('PhabricatorManiphestApplication'),
ManiphestBulkEditCapability::CAPABILITY);
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-list-ul')
->setName(pht('Batch Edit Tasks...'))
->setHref($batch_edit_uri)
->setDisabled(!$can_batch_edit);
$edit_uri = $this->getApplicationURI(
'board/'.$this->id.'/column/'.$column->getID().'/');
$column_items[] = id(new PhabricatorActionView())
->setIcon('fa-pencil')
->setName(pht('Edit Column'))
->setHref($edit_uri)
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
$can_hide = ($can_edit && !$column->isDefaultColumn());
$hide_uri = 'board/'.$this->id.'/hide/'.$column->getID().'/';
$hide_uri = $this->getApplicationURI($hide_uri);
$hide_uri = $this->getURIWithState($hide_uri);
if (!$column->isHidden()) {
$column_items[] = id(new PhabricatorActionView())
->setName(pht('Hide Column'))
->setIcon('fa-eye-slash')
->setHref($hide_uri)
->setDisabled(!$can_hide)
->setWorkflow(true);
} else {
$column_items[] = id(new PhabricatorActionView())
->setName(pht('Show Column'))
->setIcon('fa-eye')
->setHref($hide_uri)
->setDisabled(!$can_hide)
->setWorkflow(true);
}
$column_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($column_items as $item) {
$column_menu->addAction($item);
}
$column_button = id(new PHUIIconView())
->setIconFont('fa-caret-down')
->setHref('#')
->addSigil('boards-dropdown-menu')
->setMetadata(
array(
'items' => hsprintf('%s', $column_menu),
));
return $column_button;
}
private function initializeWorkboardDialog(PhabricatorProject $project) {
$instructions = pht('This workboard has not been setup yet.');
$new_selector = id(new AphrontFormRadioButtonControl())
->setName('initialize-type')
->setValue('backlog-only')
->addButton(
'backlog-only',
pht('New Empty Board'),
pht('Create a new board with just a backlog column.'))
->addButton(
'import',
pht('Import Columns'),
pht('Import board columns from another project.'));
$dialog = id(new AphrontDialogView())
->setUser($this->getRequest()->getUser())
->setTitle(pht('New Workboard'))
->addSubmitButton('Continue')
->addCancelButton($this->getApplicationURI('view/'.$project->getID().'/'))
->appendParagraph($instructions)
->appendChild($new_selector);
return id(new AphrontDialogResponse())
->setDialog($dialog);
}
/**
* Add current state parameters (like order and the visibility of hidden
* columns) to a URI.
*
* This allows actions which toggle or adjust one piece of state to keep
* the rest of the board state persistent. If no URI is provided, this method
* starts with the request URI.
*
* @param string|null URI to add state parameters to.
* @return PhutilURI URI with state parameters.
*/
private function getURIWithState($base = null) {
if ($base === null) {
$base = $this->getRequest()->getRequestURI();
}
$base = new PhutilURI($base);
if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) {
$base->setQueryParam('order', $this->sortKey);
} else {
$base->setQueryParam('order', null);
}
$base->setQueryParam('hidden', $this->showHidden ? 'true' : null);
return $base;
}
}
diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
index 419447497c..6204a37c35 100644
--- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
+++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php
@@ -1,1218 +1,1158 @@
<?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 $customFields = false;
private $request;
private $context;
const CONTEXT_LIST = 'list';
const CONTEXT_PANEL = 'panel';
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 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;
}
if ($object instanceof PhabricatorSubscribableInterface) {
if (!empty($map['subscriberPHIDs'])) {
$query->withEdgeLogicPHIDs(
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
$map['subscriberPHIDs']);
}
}
if ($object instanceof PhabricatorProjectInterface) {
if (!empty($map['projectPHIDs'])) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$map['projectPHIDs']);
}
}
if ($object instanceof PhabricatorSpacesInterface) {
if (!empty($map['spacePHIDs'])) {
$query->withSpacePHIDs($map['spacePHIDs']);
}
}
if ($object instanceof PhabricatorCustomFieldInterface) {
$this->applyCustomFieldsToQuery($query, $saved);
}
$order = $saved->getParameter('order');
- $builtin = $query->getBuiltinOrders();
+ $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();
$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) {
if ($object instanceof PhabricatorSubscribableInterface) {
$fields[] = id(new PhabricatorSearchSubscribersField())
->setLabel(pht('Subscribers'))
->setKey('subscriberPHIDs')
->setAliases(array('subscriber', 'subscribers'));
}
if ($object instanceof PhabricatorProjectInterface) {
$fields[] = id(new PhabricatorSearchProjectsField())
->setKey('projectPHIDs')
->setAliases(array('project', 'projects'))
->setLabel(pht('Projects'));
}
if ($object instanceof PhabricatorSpacesInterface) {
if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
$fields[] = id(new PhabricatorSearchSpacesField())
->setKey('spacePHIDs')
->setAliases(array('space', 'spaces'))
->setLabel(pht('Spaces'));
}
}
}
foreach ($this->buildCustomFieldSearchFields() as $custom_field) {
$fields[] = $custom_field;
}
$query = $this->newQuery();
if ($query) {
$orders = $query->getBuiltinOrders();
$orders = ipull($orders, 'name');
$fields[] = id(new PhabricatorSearchOrderField())
->setLabel(pht('Order'))
->setKey('order')
->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 $this->adjustFieldsForDisplay($field_map);
}
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);
return $head + $body + $tail;
}
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();
}
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');
return $this;
}
public function loadAllNamedQueries() {
$viewer = $this->requireViewer();
$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 = $this->getBuiltinQueries($viewer);
$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');
return $named_queries + $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);
}
}
/* -( 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() {
$engines = id(new PhutilSymbolLoader())
->setAncestorClass(__CLASS__)
->loadObjects();
return $engines;
}
/**
* 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) {
return $saved->getParameter('limit', 100);
}
public function shouldUseOffsetPaging() {
return false;
}
public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
if ($this->shouldUseOffsetPaging()) {
$pager = new AphrontPagerView();
} 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();
}
protected function renderResultList(
array $objects,
PhabricatorSavedQuery $query,
array $handles) {
throw new Exception(pht('Not supported here yet!'));
}
/* -( Application Search )------------------------------------------------- */
/**
* Retrieve an object to use to define custom fields for this search.
*
* To integrate with custom fields, subclasses should override this method
* and return an instance of the application object which implements
* @{interface:PhabricatorCustomFieldInterface}.
*
* @return PhabricatorCustomFieldInterface|null Object with custom fields.
* @task appsearch
*/
public function getCustomFieldObject() {
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
return $object;
}
return null;
}
/**
* Get the custom fields for this search.
*
* @return PhabricatorCustomFieldList|null Custom fields, if this search
* supports custom fields.
* @task appsearch
*/
public function getCustomFieldList() {
if ($this->customFields === false) {
$object = $this->getCustomFieldObject();
if ($object) {
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->requireViewer());
} else {
$fields = null;
}
$this->customFields = $fields;
}
return $this->customFields;
}
/**
* Moves data from the request into a saved query.
*
* @param AphrontRequest Request to read.
* @param PhabricatorSavedQuery Query to write to.
* @return void
* @task appsearch
*/
protected function readCustomFieldsFromRequest(
AphrontRequest $request,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $field->readApplicationSearchValueFromRequest(
$this,
$request);
$saved->setParameter($key, $value);
}
}
/**
* Applies data from a saved query to an executable query.
*
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
* @param PhabricatorSavedQuery Saved query to read.
* @return void
*/
protected function applyCustomFieldsToQuery(
PhabricatorCursorPagedPolicyAwareQuery $query,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $field->applyApplicationSearchConstraintToQuery(
$this,
$query,
$saved->getParameter($key));
}
}
- protected function applyOrderByToQuery(
- PhabricatorCursorPagedPolicyAwareQuery $query,
- array $standard_values,
- $order) {
-
- if (substr($order, 0, 7) === 'custom:') {
- $list = $this->getCustomFieldList();
- if (!$list) {
- $query->setOrderBy(head($standard_values));
- return;
- }
-
- foreach ($list->getFields() as $field) {
- $key = $this->getKeyForCustomField($field);
-
- if ($key === $order) {
- $index = $field->buildOrderIndex();
-
- if ($index === null) {
- $query->setOrderBy(head($standard_values));
- return;
- }
-
- $query->withApplicationSearchOrder(
- $field,
- $index,
- false);
- break;
- }
- }
- } else {
- $order = idx($standard_values, $order);
- if ($order) {
- $query->setOrderBy($order);
- } else {
- $query->setOrderBy(head($standard_values));
- }
- }
- }
-
-
- protected function getCustomFieldOrderOptions() {
- $list = $this->getCustomFieldList();
- if (!$list) {
- return;
- }
-
- $custom_order = array();
- foreach ($list->getFields() as $field) {
- if ($field->shouldAppearInApplicationSearch()) {
- if ($field->buildOrderIndex() !== null) {
- $key = $this->getKeyForCustomField($field);
- $custom_order[$key] = $field->getFieldName();
- }
- }
- }
-
- return $custom_order;
- }
-
/**
* Get a unique key identifying a field.
*
* @param PhabricatorCustomField Field to identify.
* @return string Unique identifier, suitable for use as an input name.
*/
public function getKeyForCustomField(PhabricatorCustomField $field) {
return 'custom:'.$field->getFieldIndex();
}
private function buildCustomFieldSearchFields() {
$list = $this->getCustomFieldList();
if (!$list) {
return array();
}
$fields = array();
foreach ($list->getFields() as $field) {
$fields[] = id(new PhabricatorSearchCustomFieldProxyField())
->setSearchEngine($this)
->setCustomField($field);
}
return $fields;
}
// TODO: Remove.
protected function appendCustomFieldsToForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$list = $this->getCustomFieldList();
if (!$list) {
return;
}
foreach ($list->getFields() as $field) {
$key = $this->getKeyForCustomField($field);
$value = $saved->getParameter($key);
$field->appendToApplicationSearchForm($this, $form, $value);
}
}
}
diff --git a/src/infrastructure/query/order/PhabricatorQueryOrderVector.php b/src/infrastructure/query/order/PhabricatorQueryOrderVector.php
index 37c0fda440..d4b100437b 100644
--- a/src/infrastructure/query/order/PhabricatorQueryOrderVector.php
+++ b/src/infrastructure/query/order/PhabricatorQueryOrderVector.php
@@ -1,127 +1,143 @@
<?php
/**
* Structural class representing a column ordering for a query.
*
* Queries often order results on multiple columns. For example, projects might
* be ordered by "name, id". This class wraps a list of column orderings and
* makes them easier to manage.
*
* To construct an order vector, use @{method:newFromVector}:
*
* $vector = PhabricatorQueryOrderVector::newFromVector(array('name', 'id'));
*
* You can iterate over an order vector normally:
*
* foreach ($vector as $item) {
* // ...
* }
*
* The items are objects of class @{class:PhabricatorQueryOrderItem}.
*
* This class is primarily internal to the query infrastructure, and most
* application code should not need to interact with it directly.
*/
final class PhabricatorQueryOrderVector
extends Phobject
implements Iterator {
private $items;
private $keys;
private $cursor;
private function __construct() {
// <private>
}
public static function newFromVector($vector) {
if ($vector instanceof PhabricatorQueryOrderVector) {
return (clone $vector);
}
if (!is_array($vector)) {
throw new Exception(
pht(
'An order vector can only be constructed from a list of strings or '.
'another order vector.'));
}
if (!$vector) {
throw new Exception(
pht(
'An order vector must not be empty.'));
}
$items = array();
foreach ($vector as $key => $scalar) {
if (!is_string($scalar)) {
throw new Exception(
pht(
'Value with key "%s" in order vector is not a string (it has '.
'type "%s"). An order vector must contain only strings.',
$key,
gettype($scalar)));
}
$item = PhabricatorQueryOrderItem::newFromScalar($scalar);
// Orderings like "id, id, id" or "id, -id" are meaningless and invalid.
if (isset($items[$item->getOrderKey()])) {
throw new Exception(
pht(
'Order vector "%s" specifies order "%s" twice. Each component '.
'of an ordering must be unique.',
implode(', ', $vector),
$item->getOrderKey()));
}
$items[$item->getOrderKey()] = $item;
}
$obj = new PhabricatorQueryOrderVector();
$obj->items = $items;
$obj->keys = array_keys($items);
return $obj;
}
+ public function appendVector($vector) {
+ $vector = self::newFromVector($vector);
+
+ // When combining vectors (like "group by" and "order by" vectors), there
+ // may be redundant columns. We only want to append unique columns which
+ // aren't already present in the vector.
+ foreach ($vector->items as $key => $item) {
+ if (empty($this->items[$key])) {
+ $this->items[$key] = $item;
+ $this->keys[] = $key;
+ }
+ }
+
+ return $this;
+ }
+
public function getAsString() {
$scalars = array();
foreach ($this->items as $item) {
$scalars[] = $item->getAsScalar();
}
return implode(', ', $scalars);
}
public function containsKey($key) {
return isset($this->items[$key]);
}
/* -( Iterator Interface )------------------------------------------------- */
public function rewind() {
$this->cursor = 0;
}
public function current() {
return $this->items[$this->key()];
}
public function key() {
return $this->keys[$this->cursor];
}
public function next() {
++$this->cursor;
}
public function valid() {
return isset($this->keys[$this->cursor]);
}
}
diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
index aace1c728c..b48e1e5c46 100644
--- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
+++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php
@@ -1,1829 +1,1841 @@
<?php
/**
* A query class which uses cursor-based paging. This paging is much more
* performant than offset-based paging in the presence of policy filtering.
*
* @task clauses Building Query Clauses
* @task appsearch Integration with ApplicationSearch
* @task customfield Integration with CustomField
* @task paging Paging
* @task order Result Ordering
* @task edgelogic Working with Edge Logic
* @task spaces Working with Spaces
*/
abstract class PhabricatorCursorPagedPolicyAwareQuery
extends PhabricatorPolicyAwareQuery {
private $afterID;
private $beforeID;
private $applicationSearchConstraints = array();
- protected $applicationSearchOrders = array();
private $internalPaging;
private $orderVector;
+ private $groupVector;
private $builtinOrder;
private $edgeLogicConstraints = array();
private $edgeLogicConstraintsAreValid = false;
private $spacePHIDs;
protected function getPageCursors(array $page) {
return array(
$this->getResultCursor(head($page)),
$this->getResultCursor(last($page)),
);
}
protected function getResultCursor($object) {
if (!is_object($object)) {
throw new Exception(
pht(
'Expected object, got "%s".',
gettype($object)));
}
return $object->getID();
}
protected function nextPage(array $page) {
// See getPagingViewer() for a description of this flag.
$this->internalPaging = true;
if ($this->beforeID !== null) {
$page = array_reverse($page, $preserve_keys = true);
list($before, $after) = $this->getPageCursors($page);
$this->beforeID = $before;
} else {
list($before, $after) = $this->getPageCursors($page);
$this->afterID = $after;
}
}
final public function setAfterID($object_id) {
$this->afterID = $object_id;
return $this;
}
final protected function getAfterID() {
return $this->afterID;
}
final public function setBeforeID($object_id) {
$this->beforeID = $object_id;
return $this;
}
final protected function getBeforeID() {
return $this->beforeID;
}
protected function loadStandardPage(PhabricatorLiskDAO $table) {
$rows = $this->loadStandardPageRows($table);
return $table->loadAllFromArray($rows);
}
protected function loadStandardPageRows(PhabricatorLiskDAO $table) {
$conn = $table->establishConnection('r');
$rows = queryfx_all(
$conn,
'%Q FROM %T %Q %Q %Q %Q %Q %Q %Q',
$this->buildSelectClause($conn),
$table->getTableName(),
(string)$this->getPrimaryTableAlias(),
$this->buildJoinClause($conn),
$this->buildWhereClause($conn),
$this->buildGroupClause($conn),
$this->buildHavingClause($conn),
$this->buildOrderClause($conn),
$this->buildLimitClause($conn));
return $rows;
}
/**
* Get the viewer for making cursor paging queries.
*
* NOTE: You should ONLY use this viewer to load cursor objects while
* building paging queries.
*
* Cursor paging can happen in two ways. First, the user can request a page
* like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we
* can fall back to implicit paging if we filter some results out of a
* result list because the user can't see them and need to go fetch some more
* results to generate a large enough result list.
*
* In the first case, want to use the viewer's policies to load the object.
* This prevents an attacker from figuring out information about an object
* they can't see by executing queries like `/stuff/?after=33&order=name`,
* which would otherwise give them a hint about the name of the object.
* Generally, if a user can't see an object, they can't use it to page.
*
* In the second case, we need to load the object whether the user can see
* it or not, because we need to examine new results. For example, if a user
* loads `/stuff/` and we run a query for the first 100 items that they can
* see, but the first 100 rows in the database aren't visible, we need to
* be able to issue a query for the next 100 results. If we can't load the
* cursor object, we'll fail or issue the same query over and over again.
* So, generally, internal paging must bypass policy controls.
*
* This method returns the appropriate viewer, based on the context in which
* the paging is occuring.
*
* @return PhabricatorUser Viewer for executing paging queries.
*/
final protected function getPagingViewer() {
if ($this->internalPaging) {
return PhabricatorUser::getOmnipotentUser();
} else {
return $this->getViewer();
}
}
final protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
if ($this->getRawResultLimit()) {
return qsprintf($conn_r, 'LIMIT %d', $this->getRawResultLimit());
} else {
return '';
}
}
final protected function didLoadResults(array $results) {
if ($this->beforeID) {
$results = array_reverse($results, $preserve_keys = true);
}
return $results;
}
final public function executeWithCursorPager(AphrontCursorPagerView $pager) {
$limit = $pager->getPageSize();
$this->setLimit($limit + 1);
if ($pager->getAfterID()) {
$this->setAfterID($pager->getAfterID());
} else if ($pager->getBeforeID()) {
$this->setBeforeID($pager->getBeforeID());
}
$results = $this->execute();
$count = count($results);
$sliced_results = $pager->sliceResults($results);
if ($sliced_results) {
list($before, $after) = $this->getPageCursors($sliced_results);
if ($pager->getBeforeID() || ($count > $limit)) {
$pager->setNextPageID($after);
}
if ($pager->getAfterID() ||
($pager->getBeforeID() && ($count > $limit))) {
$pager->setPrevPageID($before);
}
}
return $sliced_results;
}
/**
* Return the alias this query uses to identify the primary table.
*
* Some automatic query constructions may need to be qualified with a table
* alias if the query performs joins which make column names ambiguous. If
* this is the case, return the alias for the primary table the query
* uses; generally the object table which has `id` and `phid` columns.
*
* @return string Alias for the primary table.
*/
protected function getPrimaryTableAlias() {
return null;
}
public function newResultObject() {
return null;
}
/* -( Building Query Clauses )--------------------------------------------- */
/**
* @task clauses
*/
protected function buildSelectClause(AphrontDatabaseConnection $conn) {
$parts = $this->buildSelectClauseParts($conn);
return $this->formatSelectClause($parts);
}
/**
* @task clauses
*/
protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) {
$select = array();
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$select[] = qsprintf($conn, '%T.*', $alias);
} else {
$select[] = '*';
}
$select[] = $this->buildEdgeLogicSelectClause($conn);
return $select;
}
/**
* @task clauses
*/
protected function buildJoinClause(AphrontDatabaseConnection $conn) {
$joins = $this->buildJoinClauseParts($conn);
return $this->formatJoinClause($joins);
}
/**
* @task clauses
*/
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = array();
$joins[] = $this->buildEdgeLogicJoinClause($conn);
$joins[] = $this->buildApplicationSearchJoinClause($conn);
return $joins;
}
/**
* @task clauses
*/
protected function buildWhereClause(AphrontDatabaseConnection $conn) {
$where = $this->buildWhereClauseParts($conn);
return $this->formatWhereClause($where);
}
/**
* @task clauses
*/
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = array();
$where[] = $this->buildPagingClause($conn);
$where[] = $this->buildEdgeLogicWhereClause($conn);
$where[] = $this->buildSpacesWhereClause($conn);
return $where;
}
/**
* @task clauses
*/
protected function buildHavingClause(AphrontDatabaseConnection $conn) {
$having = $this->buildHavingClauseParts($conn);
return $this->formatHavingClause($having);
}
/**
* @task clauses
*/
protected function buildHavingClauseParts(AphrontDatabaseConnection $conn) {
$having = array();
$having[] = $this->buildEdgeLogicHavingClause($conn);
return $having;
}
/**
* @task clauses
*/
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
if (!$this->shouldGroupQueryResultRows()) {
return '';
}
return qsprintf(
$conn,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn());
}
/**
* @task clauses
*/
protected function shouldGroupQueryResultRows() {
if ($this->shouldGroupEdgeLogicResultRows()) {
return true;
}
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return true;
}
return false;
}
/* -( Paging )------------------------------------------------------------- */
/**
* @task paging
*/
protected function buildPagingClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
if ($this->beforeID !== null) {
$cursor = $this->beforeID;
$reversed = true;
} else if ($this->afterID !== null) {
$cursor = $this->afterID;
$reversed = false;
} else {
// No paging is being applied to this query so we do not need to
// construct a paging clause.
return '';
}
$keys = array();
foreach ($vector as $order) {
$keys[] = $order->getOrderKey();
}
$value_map = $this->getPagingValueMap($cursor, $keys);
$columns = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (!array_key_exists($key, $value_map)) {
throw new Exception(
pht(
'Query "%s" failed to return a value from getPagingValueMap() '.
'for column "%s".',
get_class($this),
$key));
}
$column = $orderable[$key];
$column['value'] = $value_map[$key];
$columns[] = $column;
}
return $this->buildPagingClauseFromMultipleColumns(
$conn,
$columns,
array(
'reversed' => $reversed,
));
}
/**
* @task paging
*/
protected function getPagingValueMap($cursor, array $keys) {
return array(
'id' => $cursor,
);
}
/**
* @task paging
*/
protected function loadCursorObject($cursor) {
$query = newv(get_class($this), array())
->setViewer($this->getPagingViewer())
->withIDs(array((int)$cursor));
$this->willExecuteCursorQuery($query);
$object = $query->executeOne();
if (!$object) {
throw new Exception(
pht(
'Cursor "%s" does not identify a valid object.',
$cursor));
}
return $object;
}
/**
* @task paging
*/
protected function willExecuteCursorQuery(
PhabricatorCursorPagedPolicyAwareQuery $query) {
return;
}
/**
* Simplifies the task of constructing a paging clause across multiple
* columns. In the general case, this looks like:
*
* A > a OR (A = a AND B > b) OR (A = a AND B = b AND C > c)
*
* To build a clause, specify the name, type, and value of each column
* to include:
*
* $this->buildPagingClauseFromMultipleColumns(
* $conn_r,
* array(
* array(
* 'table' => 't',
* 'column' => 'title',
* 'type' => 'string',
* 'value' => $cursor->getTitle(),
* 'reverse' => true,
* ),
* array(
* 'table' => 't',
* 'column' => 'id',
* 'type' => 'int',
* 'value' => $cursor->getID(),
* ),
* ),
* array(
* 'reversed' => $is_reversed,
* ));
*
* This method will then return a composable clause for inclusion in WHERE.
*
* @param AphrontDatabaseConnection Connection query will execute on.
* @param list<map> Column description dictionaries.
* @param map Additional constuction options.
* @return string Query clause.
* @task paging
*/
final protected function buildPagingClauseFromMultipleColumns(
AphrontDatabaseConnection $conn,
array $columns,
array $options) {
foreach ($columns as $column) {
PhutilTypeSpec::checkMap(
$column,
array(
'table' => 'optional string|null',
'column' => 'string',
'value' => 'wild',
'type' => 'string',
'reverse' => 'optional bool',
'unique' => 'optional bool',
'null' => 'optional string|null',
));
}
PhutilTypeSpec::checkMap(
$options,
array(
'reversed' => 'optional bool',
));
$is_query_reversed = idx($options, 'reversed', false);
$clauses = array();
$accumulated = array();
$last_key = last_key($columns);
foreach ($columns as $key => $column) {
$type = $column['type'];
$null = idx($column, 'null');
if ($column['value'] === null) {
if ($null) {
$value = null;
} else {
throw new Exception(
pht(
'Column "%s" has null value, but does not specify a null '.
'behavior.',
$key));
}
} else {
switch ($type) {
case 'int':
$value = qsprintf($conn, '%d', $column['value']);
break;
case 'float':
$value = qsprintf($conn, '%f', $column['value']);
break;
case 'string':
$value = qsprintf($conn, '%s', $column['value']);
break;
default:
throw new Exception(
pht(
'Column "%s" has unknown column type "%s".',
$column['column'],
$type));
}
}
$is_column_reversed = idx($column, 'reverse', false);
$reverse = ($is_query_reversed xor $is_column_reversed);
$clause = $accumulated;
$table_name = idx($column, 'table');
$column_name = $column['column'];
if ($table_name !== null) {
$field = qsprintf($conn, '%T.%T', $table_name, $column_name);
} else {
$field = qsprintf($conn, '%T', $column_name);
}
$parts = array();
if ($null) {
$can_page_if_null = ($null === 'head');
$can_page_if_nonnull = ($null === 'tail');
if ($reverse) {
$can_page_if_null = !$can_page_if_null;
$can_page_if_nonnull = !$can_page_if_nonnull;
}
$subclause = null;
if ($can_page_if_null && $value === null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NOT NULL)',
$field);
} else if ($can_page_if_nonnull && $value !== null) {
$parts[] = qsprintf(
$conn,
'(%Q IS NULL)',
$field);
}
}
if ($value !== null) {
$parts[] = qsprintf(
$conn,
'%Q %Q %Q',
$field,
$reverse ? '>' : '<',
$value);
}
if ($parts) {
if (count($parts) > 1) {
$clause[] = '('.implode(') OR (', $parts).')';
} else {
$clause[] = head($parts);
}
}
if ($clause) {
if (count($clause) > 1) {
$clauses[] = '('.implode(') AND (', $clause).')';
} else {
$clauses[] = head($clause);
}
}
if ($value === null) {
$accumulated[] = qsprintf(
$conn,
'%Q IS NULL',
$field);
} else {
$accumulated[] = qsprintf(
$conn,
'%Q = %Q',
$field,
$value);
}
}
return '('.implode(') OR (', $clauses).')';
}
/* -( Result Ordering )---------------------------------------------------- */
/**
* Select a result ordering.
*
* This is a high-level method which selects an ordering from a predefined
* list of builtin orders, as provided by @{method:getBuiltinOrders}. These
* options are user-facing and not exhaustive, but are generally convenient
* and meaningful.
*
* You can also use @{method:setOrderVector} to specify a low-level ordering
* across individual orderable columns. This offers greater control but is
* also more involved.
*
* @param string Key of a builtin order supported by this query.
* @return this
* @task order
*/
public function setOrder($order) {
- $orders = $this->getBuiltinOrders();
+ $aliases = $this->getBuiltinOrderAliasMap();
- if (empty($orders[$order])) {
+ if (empty($aliases[$order])) {
throw new Exception(
pht(
'Query "%s" does not support a builtin order "%s". Supported orders '.
'are: %s.',
get_class($this),
$order,
- implode(', ', array_keys($orders))));
+ implode(', ', array_keys($aliases))));
}
- $this->builtinOrder = $order;
+ $this->builtinOrder = $aliases[$order];
+ $this->orderVector = null;
+
+ return $this;
+ }
+
+
+ /**
+ * Set a grouping order to apply before primary result ordering.
+ *
+ * This allows you to preface the query order vector with additional orders,
+ * so you can effect "group by" queries while still respecting "order by".
+ *
+ * This is a high-level method which works alongside @{method:setOrder}. For
+ * lower-level control over order vectors, use @{method:setOrderVector}.
+ *
+ * @param PhabricatorQueryOrderVector|list<string> List of order keys.
+ * @return this
+ * @task order
+ */
+ public function setGroupVector($vector) {
+ $this->groupVector = $vector;
$this->orderVector = null;
return $this;
}
/**
* Get builtin orders for this class.
*
* In application UIs, we want to be able to present users with a small
* selection of meaningful order options (like "Order by Title") rather than
* an exhaustive set of column ordering options.
*
* Meaningful user-facing orders are often really orders across multiple
* columns: for example, a "title" ordering is usually implemented as a
* "title, id" ordering under the hood.
*
* Builtin orders provide a mapping from convenient, understandable
* user-facing orders to implementations.
*
* A builtin order should provide these keys:
*
* - `vector` (`list<string>`): The actual order vector to use.
* - `name` (`string`): Human-readable order name.
*
* @return map<string, wild> Map from builtin order keys to specification.
* @task order
*/
public function getBuiltinOrders() {
$orders = array(
'newest' => array(
'vector' => array('id'),
'name' => pht('Creation (Newest First)'),
'aliases' => array('created'),
),
'oldest' => array(
'vector' => array('-id'),
'name' => pht('Creation (Oldest First)'),
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$key = $field->getFieldKey();
$digest = $field->getFieldIndex();
$full_key = 'custom:'.$key;
$orders[$full_key] = array(
'vector' => array($full_key, 'id'),
'name' => $field->getFieldName(),
);
}
}
return $orders;
}
+ public function getBuiltinOrderAliasMap() {
+ $orders = $this->getBuiltinOrders();
+
+ $map = array();
+ foreach ($orders as $key => $order) {
+ $keys = array();
+ $keys[] = $key;
+ foreach (idx($order, 'aliases', array()) as $alias) {
+ $keys[] = $alias;
+ }
+
+ foreach ($keys as $alias) {
+ if (isset($map[$alias])) {
+ throw new Exception(
+ pht(
+ 'Two builtin orders ("%s" and "%s") define the same key or '.
+ 'alias ("%s"). Each order alias and key must be unique and '.
+ 'identify a single order.',
+ $key,
+ $map[$alias],
+ $alias));
+ }
+ $map[$alias] = $key;
+ }
+ }
+
+ return $map;
+ }
+
/**
* Set a low-level column ordering.
*
* This is a low-level method which offers granular control over column
* ordering. In most cases, applications can more easily use
* @{method:setOrder} to choose a high-level builtin order.
*
* To set an order vector, specify a list of order keys as provided by
* @{method:getOrderableColumns}.
*
* @param PhabricatorQueryOrderVector|list<string> List of order keys.
* @return this
* @task order
*/
public function setOrderVector($vector) {
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
$orderable = $this->getOrderableColumns();
// Make sure that all the components identify valid columns.
$unique = array();
foreach ($vector as $order) {
$key = $order->getOrderKey();
if (empty($orderable[$key])) {
$valid = implode(', ', array_keys($orderable));
throw new Exception(
pht(
'This query ("%s") does not support sorting by order key "%s". '.
'Supported orders are: %s.',
get_class($this),
$key,
$valid));
}
$unique[$key] = idx($orderable[$key], 'unique', false);
}
// Make sure that the last column is unique so that this is a strong
// ordering which can be used for paging.
$last = last($unique);
if ($last !== true) {
throw new Exception(
pht(
'Order vector "%s" is invalid: the last column in an order must '.
'be a column with unique values, but "%s" is not unique.',
$vector->getAsString(),
last_key($unique)));
}
// Make sure that other columns are not unique; an ordering like "id, name"
// does not make sense because only "id" can ever have an effect.
array_pop($unique);
foreach ($unique as $key => $is_unique) {
if ($is_unique) {
throw new Exception(
pht(
'Order vector "%s" is invalid: only the last column in an order '.
'may be unique, but "%s" is a unique column and not the last '.
'column in the order.',
$vector->getAsString(),
$key));
}
}
$this->orderVector = $vector;
return $this;
}
/**
* Get the effective order vector.
*
* @return PhabricatorQueryOrderVector Effective vector.
* @task order
*/
protected function getOrderVector() {
if (!$this->orderVector) {
if ($this->builtinOrder !== null) {
$builtin_order = idx($this->getBuiltinOrders(), $this->builtinOrder);
$vector = $builtin_order['vector'];
} else {
$vector = $this->getDefaultOrderVector();
}
+
+ if ($this->groupVector) {
+ $group = PhabricatorQueryOrderVector::newFromVector($this->groupVector);
+ $group->appendVector($vector);
+ $vector = $group;
+ }
+
$vector = PhabricatorQueryOrderVector::newFromVector($vector);
// We call setOrderVector() here to apply checks to the default vector.
// This catches any errors in the implementation.
$this->setOrderVector($vector);
}
return $this->orderVector;
}
/**
* @task order
*/
protected function getDefaultOrderVector() {
return array('id');
}
/**
* @task order
*/
public function getOrderableColumns() {
$columns = array(
'id' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'id',
'reverse' => false,
'type' => 'int',
'unique' => true,
),
);
$object = $this->newResultObject();
if ($object instanceof PhabricatorCustomFieldInterface) {
$list = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
foreach ($list->getFields() as $field) {
$index = $field->buildOrderIndex();
if (!$index) {
continue;
}
$key = $field->getFieldKey();
$digest = $field->getFieldIndex();
$full_key = 'custom:'.$key;
$columns[$full_key] = array(
'table' => 'appsearch_order_'.$digest,
'column' => 'indexValue',
'type' => $index->getIndexValueType(),
'null' => 'tail',
'customfield' => true,
'customfield.index.table' => $index->getTableName(),
'customfield.index.key' => $digest,
);
}
}
return $columns;
}
/**
* @task order
*/
final protected function buildOrderClause(AphrontDatabaseConnection $conn) {
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
$parts = array();
foreach ($vector as $order) {
$part = $orderable[$order->getOrderKey()];
if ($order->getIsReversed()) {
$part['reverse'] = !idx($part, 'reverse', false);
}
$parts[] = $part;
}
return $this->formatOrderClause($conn, $parts);
}
/**
* @task order
*/
protected function formatOrderClause(
AphrontDatabaseConnection $conn,
array $parts) {
$is_query_reversed = false;
if ($this->getBeforeID()) {
$is_query_reversed = !$is_query_reversed;
}
$sql = array();
foreach ($parts as $key => $part) {
$is_column_reversed = !empty($part['reverse']);
$descending = true;
if ($is_query_reversed) {
$descending = !$descending;
}
if ($is_column_reversed) {
$descending = !$descending;
}
$table = idx($part, 'table');
$column = $part['column'];
if ($table !== null) {
$field = qsprintf($conn, '%T.%T', $table, $column);
} else {
$field = qsprintf($conn, '%T', $column);
}
$null = idx($part, 'null');
if ($null) {
switch ($null) {
case 'head':
$null_field = qsprintf($conn, '(%Q IS NULL)', $field);
break;
case 'tail':
$null_field = qsprintf($conn, '(%Q IS NOT NULL)', $field);
break;
default:
throw new Exception(
pht(
'NULL value "%s" is invalid. Valid values are "head" and '.
'"tail".',
$null));
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $null_field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $null_field);
}
}
if ($descending) {
$sql[] = qsprintf($conn, '%Q DESC', $field);
} else {
$sql[] = qsprintf($conn, '%Q ASC', $field);
}
}
return qsprintf($conn, 'ORDER BY %Q', implode(', ', $sql));
}
/* -( Application Search )------------------------------------------------- */
/**
* Constrain the query with an ApplicationSearch index, requiring field values
* contain at least one of the values in a set.
*
* This constraint can build the most common types of queries, like:
*
* - Find users with shirt sizes "X" or "XL".
* - Find shoes with size "13".
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param string|list<string> One or more values to filter by.
* @return this
* @task appsearch
*/
public function withApplicationSearchContainsConstraint(
PhabricatorCustomFieldIndexStorage $index,
$value) {
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => '=',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'value' => $value,
);
return $this;
}
/**
* Constrain the query with an ApplicationSearch index, requiring values
* exist in a given range.
*
* This constraint is useful for expressing date ranges:
*
* - Find events between July 1st and July 7th.
*
* The ends of the range are inclusive, so a `$min` of `3` and a `$max` of
* `5` will match fields with values `3`, `4`, or `5`. Providing `null` for
* either end of the range will leave that end of the constraint open.
*
* @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
* @param int|null Minimum permissible value, inclusive.
* @param int|null Maximum permissible value, inclusive.
* @return this
* @task appsearch
*/
public function withApplicationSearchRangeConstraint(
PhabricatorCustomFieldIndexStorage $index,
$min,
$max) {
$index_type = $index->getIndexValueType();
if ($index_type != 'int') {
throw new Exception(
pht(
'Attempting to apply a range constraint to a field with index type '.
'"%s", expected type "%s".',
$index_type,
'int'));
}
$this->applicationSearchConstraints[] = array(
'type' => $index->getIndexValueType(),
'cond' => 'range',
'table' => $index->getTableName(),
'index' => $index->getIndexKey(),
'value' => array($min, $max),
);
return $this;
}
- /**
- * Order the results by an ApplicationSearch index.
- *
- * @param PhabricatorCustomField Field to which the index belongs.
- * @param PhabricatorCustomFieldIndexStorage Table where the index is stored.
- * @param bool True to sort ascending.
- * @return this
- * @task appsearch
- */
- public function withApplicationSearchOrder(
- PhabricatorCustomField $field,
- PhabricatorCustomFieldIndexStorage $index,
- $ascending) {
-
- $this->applicationSearchOrders[] = array(
- 'key' => $field->getFieldKey(),
- 'type' => $index->getIndexValueType(),
- 'table' => $index->getTableName(),
- 'index' => $index->getIndexKey(),
- 'ascending' => $ascending,
- );
-
- return $this;
- }
-
-
/**
* Get the name of the query's primary object PHID column, for constructing
* JOIN clauses. Normally (and by default) this is just `"phid"`, but it may
* be something more exotic.
*
* See @{method:getPrimaryTableAlias} if the column needs to be qualified with
* a table alias.
*
* @return string Column name.
* @task appsearch
*/
protected function getApplicationSearchObjectPHIDColumn() {
if ($this->getPrimaryTableAlias()) {
$prefix = $this->getPrimaryTableAlias().'.';
} else {
$prefix = '';
}
return $prefix.'phid';
}
/**
* Determine if the JOINs built by ApplicationSearch might cause each primary
* object to return multiple result rows. Generally, this means the query
* needs an extra GROUP BY clause.
*
* @return bool True if the query may return multiple rows for each object.
* @task appsearch
*/
protected function getApplicationSearchMayJoinMultipleRows() {
foreach ($this->applicationSearchConstraints as $constraint) {
$type = $constraint['type'];
$value = $constraint['value'];
$cond = $constraint['cond'];
switch ($cond) {
case '=':
switch ($type) {
case 'string':
case 'int':
if (count((array)$value) > 1) {
return true;
}
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
break;
case 'range':
// NOTE: It's possible to write a custom field where multiple rows
// match a range constraint, but we don't currently ship any in the
// upstream and I can't immediately come up with cases where this
// would make sense.
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
return false;
}
/**
* Construct a GROUP BY clause appropriate for ApplicationSearch constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Group clause.
* @task appsearch
*/
protected function buildApplicationSearchGroupClause(
AphrontDatabaseConnection $conn_r) {
if ($this->getApplicationSearchMayJoinMultipleRows()) {
return qsprintf(
$conn_r,
'GROUP BY %Q',
$this->getApplicationSearchObjectPHIDColumn());
} else {
return '';
}
}
/**
* Construct a JOIN clause appropriate for applying ApplicationSearch
* constraints.
*
* @param AphrontDatabaseConnection Connection executing the query.
* @return string Join clause.
* @task appsearch
*/
protected function buildApplicationSearchJoinClause(
AphrontDatabaseConnection $conn_r) {
$joins = array();
foreach ($this->applicationSearchConstraints as $key => $constraint) {
$table = $constraint['table'];
$alias = 'appsearch_'.$key;
$index = $constraint['index'];
$cond = $constraint['cond'];
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
switch ($cond) {
case '=':
$type = $constraint['type'];
switch ($type) {
case 'string':
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue IN (%Ls)',
$alias,
(array)$constraint['value']);
break;
case 'int':
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue IN (%Ld)',
$alias,
(array)$constraint['value']);
break;
default:
throw new Exception(pht('Unknown index type "%s"!', $type));
}
$joins[] = qsprintf(
$conn_r,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
case 'range':
list($min, $max) = $constraint['value'];
if (($min === null) && ($max === null)) {
// If there's no actual range constraint, just move on.
break;
}
if ($min === null) {
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue <= %d',
$alias,
$max);
} else if ($max === null) {
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue >= %d',
$alias,
$min);
} else {
$constraint_clause = qsprintf(
$conn_r,
'%T.indexValue BETWEEN %d AND %d',
$alias,
$min,
$max);
}
$joins[] = qsprintf(
$conn_r,
'JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s
AND (%Q)',
$table,
$alias,
$alias,
$phid_column,
$alias,
$index,
$constraint_clause);
break;
default:
throw new Exception(pht('Unknown constraint condition "%s"!', $cond));
}
}
- // TODO: Get rid of this.
- foreach ($this->applicationSearchOrders as $key => $order) {
- $table = $order['table'];
- $index = $order['index'];
- $alias = 'appsearch_order_'.$index;
- $phid_column = $this->getApplicationSearchObjectPHIDColumn();
-
- $joins[] = qsprintf(
- $conn_r,
- 'LEFT JOIN %T %T ON %T.objectPHID = %Q
- AND %T.indexKey = %s',
- $table,
- $alias,
- $alias,
- $phid_column,
- $alias,
- $index);
- }
-
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
foreach ($vector as $order) {
$spec = $orderable[$order->getOrderKey()];
if (empty($spec['customfield'])) {
continue;
}
$table = $spec['customfield.index.table'];
$alias = $spec['table'];
$key = $spec['customfield.index.key'];
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$table,
$alias,
$alias,
$phid_column,
$alias,
$key);
}
return implode(' ', $joins);
}
/* -( Integration with CustomField )--------------------------------------- */
/**
* @task customfield
*/
protected function getPagingValueMapForCustomFields(
PhabricatorCustomFieldInterface $object) {
// We have to get the current field values on the cursor object.
$fields = PhabricatorCustomField::getObjectFields(
$object,
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
$fields->setViewer($this->getViewer());
$fields->readFieldsFromStorage($object);
$map = array();
foreach ($fields->getFields() as $field) {
$map['custom:'.$field->getFieldKey()] = $field->getValueForStorage();
}
return $map;
}
/**
* @task customfield
*/
protected function isCustomFieldOrderKey($key) {
$prefix = 'custom:';
return !strncmp($key, $prefix, strlen($prefix));
}
/* -( Edge Logic )--------------------------------------------------------- */
/**
* Convenience method for specifying edge logic constraints with a list of
* PHIDs.
*
* @param const Edge constant.
* @param const Constraint operator.
* @param list<phid> List of PHIDs.
* @return this
* @task edgelogic
*/
public function withEdgeLogicPHIDs($edge_type, $operator, array $phids) {
$constraints = array();
foreach ($phids as $phid) {
$constraints[] = new PhabricatorQueryConstraint($operator, $phid);
}
return $this->withEdgeLogicConstraints($edge_type, $constraints);
}
/**
* @task edgelogic
*/
public function withEdgeLogicConstraints($edge_type, array $constraints) {
assert_instances_of($constraints, 'PhabricatorQueryConstraint');
$constraints = mgroup($constraints, 'getOperator');
foreach ($constraints as $operator => $list) {
foreach ($list as $item) {
$value = $item->getValue();
$this->edgeLogicConstraints[$edge_type][$operator][$value] = $item;
}
}
$this->edgeLogicConstraintsAreValid = false;
return $this;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicSelectClause(AphrontDatabaseConnection $conn) {
$select = array();
$this->validateEdgeLogicConstraints();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$select[] = qsprintf(
$conn,
'COUNT(DISTINCT(%T.dst)) %T',
$alias,
$this->buildEdgeLogicTableAliasCount($alias));
}
break;
default:
break;
}
}
}
return $select;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicJoinClause(AphrontDatabaseConnection $conn) {
$edge_table = PhabricatorEdgeConfig::TABLE_NAME_EDGE;
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
$joins = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
mpull($list, 'getValue'));
break;
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
// If we're including results with no matches, we have to degrade
// this to a LEFT join. We'll use WHERE to select matching rows
// later.
if ($has_null) {
$join_type = 'LEFT';
} else {
$join_type = '';
}
$joins[] = qsprintf(
$conn,
'%Q JOIN %T %T ON %Q = %T.src AND %T.type = %d
AND %T.dst IN (%Ls)',
$join_type,
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type,
$alias,
mpull($list, 'getValue'));
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$joins[] = qsprintf(
$conn,
'LEFT JOIN %T %T ON %Q = %T.src AND %T.type = %d',
$edge_table,
$alias,
$phid_column,
$alias,
$alias,
$type);
break;
}
}
}
return $joins;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicWhereClause(AphrontDatabaseConnection $conn) {
$where = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
$full = array();
$null = array();
$op_null = PhabricatorQueryConstraint::OPERATOR_NULL;
$has_null = isset($constraints[$op_null]);
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
$full[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if ($has_null) {
$full[] = qsprintf(
$conn,
'%T.dst IS NOT NULL',
$alias);
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
$null[] = qsprintf(
$conn,
'%T.dst IS NULL',
$alias);
break;
}
}
if ($full && $null) {
$full = $this->formatWhereSubclause($full);
$null = $this->formatWhereSubclause($null);
$where[] = qsprintf($conn, '(%Q OR %Q)', $full, $null);
} else if ($full) {
foreach ($full as $condition) {
$where[] = $condition;
}
} else if ($null) {
foreach ($null as $condition) {
$where[] = $condition;
}
}
}
return $where;
}
/**
* @task edgelogic
*/
public function buildEdgeLogicHavingClause(AphrontDatabaseConnection $conn) {
$having = array();
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
$alias = $this->getEdgeLogicTableAlias($operator, $type);
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_AND:
if (count($list) > 1) {
$having[] = qsprintf(
$conn,
'%T = %d',
$this->buildEdgeLogicTableAliasCount($alias),
count($list));
}
break;
}
}
}
return $having;
}
/**
* @task edgelogic
*/
public function shouldGroupEdgeLogicResultRows() {
foreach ($this->edgeLogicConstraints as $type => $constraints) {
foreach ($constraints as $operator => $list) {
switch ($operator) {
case PhabricatorQueryConstraint::OPERATOR_NOT:
case PhabricatorQueryConstraint::OPERATOR_AND:
case PhabricatorQueryConstraint::OPERATOR_OR:
if (count($list) > 1) {
return true;
}
break;
case PhabricatorQueryConstraint::OPERATOR_NULL:
return true;
}
}
}
return false;
}
/**
* @task edgelogic
*/
private function getEdgeLogicTableAlias($operator, $type) {
return 'edgelogic_'.$operator.'_'.$type;
}
/**
* @task edgelogic
*/
private function buildEdgeLogicTableAliasCount($alias) {
return $alias.'_count';
}
/**
* Select certain edge logic constraint values.
*
* @task edgelogic
*/
protected function getEdgeLogicValues(
array $edge_types,
array $operators) {
$values = array();
$constraint_lists = $this->edgeLogicConstraints;
if ($edge_types) {
$constraint_lists = array_select_keys($constraint_lists, $edge_types);
}
foreach ($constraint_lists as $type => $constraints) {
if ($operators) {
$constraints = array_select_keys($constraints, $operators);
}
foreach ($constraints as $operator => $list) {
foreach ($list as $constraint) {
$values[] = $constraint->getValue();
}
}
}
return $values;
}
/**
* Validate edge logic constraints for the query.
*
* @return this
* @task edgelogic
*/
private function validateEdgeLogicConstraints() {
if ($this->edgeLogicConstraintsAreValid) {
return $this;
}
// This should probably be more modular, eventually, but we only do
// project-based edge logic today.
$project_phids = $this->getEdgeLogicValues(
array(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
),
array(
PhabricatorQueryConstraint::OPERATOR_AND,
PhabricatorQueryConstraint::OPERATOR_OR,
PhabricatorQueryConstraint::OPERATOR_NOT,
));
if ($project_phids) {
$projects = id(new PhabricatorProjectQuery())
->setViewer($this->getViewer())
->setParentQuery($this)
->withPHIDs($project_phids)
->execute();
$projects = mpull($projects, null, 'getPHID');
foreach ($project_phids as $phid) {
if (empty($projects[$phid])) {
throw new PhabricatorEmptyQueryException(
pht(
'This query is constrained by a project you do not have '.
'permission to see.'));
}
}
}
$this->edgeLogicConstraintsAreValid = true;
return $this;
}
/* -( Spaces )------------------------------------------------------------- */
/**
* Constrain the query to return results from only specific Spaces.
*
* Pass a list of Space PHIDs, or `null` to represent the default space. Only
* results in those Spaces will be returned.
*
* Queries are always constrained to include only results from spaces the
* viewer has access to.
*
* @param list<phid|null>
* @task spaces
*/
public function withSpacePHIDs(array $space_phids) {
$object = $this->newResultObject();
if (!$object) {
throw new Exception(
pht(
'This query (of class "%s") does not implement newResultObject(), '.
'but must implement this method to enable support for Spaces.',
get_class($this)));
}
if (!($object instanceof PhabricatorSpacesInterface)) {
throw new Exception(
pht(
'This query (of class "%s") returned an object of class "%s" from '.
'getNewResultObject(), but it does not implement the required '.
'interface ("%s"). Objects must implement this interface to enable '.
'Spaces support.',
get_class($this),
get_class($object),
'PhabricatorSpacesInterface'));
}
$this->spacePHIDs = $space_phids;
return $this;
}
/**
* Constrain the query to include only results in valid Spaces.
*
* This method builds part of a WHERE clause which considers the spaces the
* viewer has access to see with any explicit constraint on spaces added by
* @{method:withSpacePHIDs}.
*
* @param AphrontDatabaseConnection Database connection.
* @return string Part of a WHERE clause.
* @task spaces
*/
private function buildSpacesWhereClause(AphrontDatabaseConnection $conn) {
$object = $this->newResultObject();
if (!$object) {
return null;
}
if (!($object instanceof PhabricatorSpacesInterface)) {
return null;
}
$viewer = $this->getViewer();
$space_phids = array();
$include_null = false;
$all = PhabricatorSpacesNamespaceQuery::getAllSpaces();
if (!$all) {
// If there are no spaces at all, implicitly give the viewer access to
// the default space.
$include_null = true;
} else {
// Otherwise, give them access to the spaces they have permission to
// see.
$viewer_spaces = PhabricatorSpacesNamespaceQuery::getViewerSpaces(
$viewer);
foreach ($viewer_spaces as $viewer_space) {
$phid = $viewer_space->getPHID();
$space_phids[$phid] = $phid;
if ($viewer_space->getIsDefaultNamespace()) {
$include_null = true;
}
}
}
// If we have additional explicit constraints, evaluate them now.
if ($this->spacePHIDs !== null) {
$explicit = array();
$explicit_null = false;
foreach ($this->spacePHIDs as $phid) {
if ($phid === null) {
$space = PhabricatorSpacesNamespaceQuery::getDefaultSpace();
} else {
$space = idx($all, $phid);
}
if ($space) {
$phid = $space->getPHID();
$explicit[$phid] = $phid;
if ($space->getIsDefaultNamespace()) {
$explicit_null = true;
}
}
}
// If the viewer can see the default space but it isn't on the explicit
// list of spaces to query, don't match it.
if ($include_null && !$explicit_null) {
$include_null = false;
}
// Include only the spaces common to the viewer and the constraints.
$space_phids = array_intersect_key($space_phids, $explicit);
}
if (!$space_phids && !$include_null) {
if ($this->spacePHIDs === null) {
throw new PhabricatorEmptyQueryException(
pht('You do not have access to any spaces.'));
} else {
throw new PhabricatorEmptyQueryException(
pht(
'You do not have access to any of the spaces this query '.
'is constrained to.'));
}
}
$alias = $this->getPrimaryTableAlias();
if ($alias) {
$col = qsprintf($conn, '%T.spacePHID', $alias);
} else {
$col = 'spacePHID';
}
if ($space_phids && $include_null) {
return qsprintf(
$conn,
'(%Q IN (%Ls) OR %Q IS NULL)',
$col,
$space_phids,
$col);
} else if ($space_phids) {
return qsprintf(
$conn,
'%Q IN (%Ls)',
$col,
$space_phids);
} else {
return qsprintf(
$conn,
'%Q IS NULL',
$col);
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 1, 12:18 AM (21 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
430047
Default Alt Text
(192 KB)

Event Timeline