diff --git a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
index 22dbbee717..ddc5bd548d 100644
--- a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
@@ -1,78 +1,81 @@
getManualDiff();
$branch = $diff->getBranch();
$bookmark = $diff->getBookmark();
$has_branch = ($branch != '');
$has_bookmark = ($bookmark != '');
if ($has_branch && $has_bookmark) {
$branch = "{$bookmark} bookmark on {$branch} branch";
} else if ($has_bookmark) {
$branch = "{$bookmark} bookmark";
} else if (!$has_branch) {
return null;
}
return $branch;
}
public function renderValueForMail($phase) {
$status = $this->getRevision()->getStatus();
if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION &&
$status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
return null;
}
$diff = $this->getRevision()->loadActiveDiff();
if ($diff) {
$branch = $diff->getBranch();
if ($branch) {
return "BRANCH\n $branch";
}
}
}
public function didWriteRevision(DifferentialRevisionEditor $editor) {
$maniphest = 'PhabricatorApplicationManiphest';
if (!PhabricatorApplication::isClassInstalled($maniphest)) {
return;
}
$branch = $this->getDiff()->getBranch();
$match = null;
if (preg_match('/^T(\d+)/i', $branch, $match)) { // No $ to allow T123_demo.
list(, $task_id) = $match;
- $task = id(new ManiphestTask())->load($task_id);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($editor->requireActor())
+ ->withIDs(array($task_id))
+ ->executeOne();
if ($task) {
id(new PhabricatorEdgeEditor())
->setActor($this->getUser())
->addEdge(
$this->getRevision()->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK,
$task->getPHID())
->save();
}
}
}
public function getCommitMessageTips() {
return array(
'Name branch "T123" to attach the diff to a task.',
);
}
}
diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
index 032884fb3c..db29521739 100644
--- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php
@@ -1,270 +1,272 @@
ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'resolves' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'fix' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'fixes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
'wontfixes' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
'wontfixed' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
'spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'spites' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'spited' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'invalidate' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
'invaldiates' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
'invalidated' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
'close' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'closes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'closed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'ref' => null,
'refs' => null,
'references' => null,
'cf.' => null,
);
$suffixes = array(
'as resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'as fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
'as wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
'as spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'out of spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE,
'as invalid' => ManiphestTaskStatus::STATUS_CLOSED_INVALID,
'' => null,
);
$prefix_regex = array();
foreach ($prefixes as $prefix => $resolution) {
$prefix_regex[] = preg_quote($prefix, '/');
}
$prefix_regex = implode('|', $prefix_regex);
$suffix_regex = array();
foreach ($suffixes as $suffix => $resolution) {
$suffix_regex[] = preg_quote($suffix, '/');
}
$suffix_regex = implode('|', $suffix_regex);
$matches = null;
preg_match_all(
"/({$prefix_regex})\s+T(\d+)\s*({$suffix_regex})/i",
$message,
$matches,
PREG_SET_ORDER);
$tasks_statuses = array();
foreach ($matches as $set) {
$prefix = strtolower($set[1]);
$task_id = (int)$set[2];
$suffix = strtolower($set[3]);
$status = idx($suffixes, $suffix);
if (!$status) {
$status = idx($prefixes, $prefix);
}
$tasks_statuses[$task_id] = $status;
}
return $tasks_statuses;
}
private function findDependentRevisions($message) {
$dependents = array();
$matches = null;
preg_match_all(
'/\b(?i:depends\s+on):?\s+D(\d+(,\s+D\d++)*)\b/',
$message,
$matches);
foreach ($matches[1] as $revisions) {
foreach (preg_split('/,\s+D/', $revisions) as $id) {
$dependents[$id] = $id;
}
}
return $dependents;
}
public static function findRevertedCommits($message) {
$reverts = array();
$matches = null;
// NOTE: Git language is "This reverts commit X."
// NOTE: Mercurial language is "Backed out changeset Y".
$prefixes = array(
'revert' => true,
'reverts' => true,
'back\s*out' => true,
'backs\s*out' => true,
'backed\s*out' => true,
'undo' => true,
'undoes' => true,
);
$optional = array(
'commit' => true,
'changeset' => true,
'rev' => true,
'revision' => true,
'change' => true,
'diff' => true,
);
$pre_re = implode('|', array_keys($prefixes));
$opt_re = implode('|', array_keys($optional));
$matches = null;
preg_match_all(
'/\b(?i:'.$pre_re.')(?:\s+(?i:'.$opt_re.'))?([rA-Z0-9a-f,\s]+)\b/',
$message,
$matches);
$result = array();
foreach ($matches[1] as $commits) {
$commits = preg_split('/[,\s]+/', $commits);
$commits = array_filter($commits);
foreach ($commits as $commit) {
$result[$commit] = $commit;
}
}
return $result;
}
public function didWriteRevision(DifferentialRevisionEditor $editor) {
$message = $this->renderValueForCommitMessage(false);
$tasks = $this->findMentionedTasks($message);
if ($tasks) {
- $tasks = id(new ManiphestTask())
- ->loadAllWhere('id IN (%Ld)', array_keys($tasks));
+ $tasks = id(new ManiphestTaskQuery())
+ ->setViewer($editor->getActor())
+ ->withIDs(array_keys($tasks))
+ ->execute();
$this->saveFieldEdges(
$editor->getRevision(),
PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK,
mpull($tasks, 'getPHID'));
}
$dependents = $this->findDependentRevisions($message);
if ($dependents) {
$dependents = id(new DifferentialRevision())
->loadAllWhere('id IN (%Ld)', $dependents);
$this->saveFieldEdges(
$editor->getRevision(),
PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV,
mpull($dependents, 'getPHID'));
}
}
private function saveFieldEdges(
DifferentialRevision $revision,
$edge_type,
array $add_phids) {
$revision_phid = $revision->getPHID();
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$revision_phid,
$edge_type);
$add_phids = array_diff($add_phids, $old_phids);
if (!$add_phids) {
return;
}
$edge_editor = id(new PhabricatorEdgeEditor())->setActor($this->getUser());
foreach ($add_phids as $phid) {
$edge_editor->addEdge($revision_phid, $edge_type, $phid);
}
// NOTE: Deletes only through the fields.
$edge_editor->save();
}
public function didParseCommit(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit,
PhabricatorRepositoryCommitData $data) {
$message = $this->renderValueForCommitMessage($is_edit = false);
$user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$data->getCommitDetail('authorPHID'));
if (!$user) {
// TODO: Maybe after grey users, we should find a way to proceed even
// if we don't know who the author is.
return;
}
$commit_names = self::findRevertedCommits($message);
if ($commit_names) {
$reverts = id(new DiffusionCommitQuery())
->setViewer($user)
->withIdentifiers($commit_names)
->withDefaultRepository($repository)
->execute();
foreach ($reverts as $revert) {
// TODO: Do interesting things here.
}
}
$tasks_statuses = $this->findMentionedTasks($message);
if (!$tasks_statuses) {
return;
}
$tasks = id(new ManiphestTaskQuery())
->setViewer($user)
->withIDs(array_keys($tasks_statuses))
->execute();
foreach ($tasks as $task_id => $task) {
id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge(
$task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT,
$commit->getPHID())
->save();
$status = $tasks_statuses[$task_id];
if (!$status) {
// Text like "Ref T123", don't change the task status.
continue;
}
if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
// Task is already closed.
continue;
}
$commit_name = $repository->formatCommitName(
$commit->getCommitIdentifier());
$call = new ConduitCall(
'maniphest.update',
array(
'id' => $task->getID(),
'status' => $status,
'comments' => "Closed by commit {$commit_name}.",
));
$call->setUser($user);
$call->execute();
}
}
}
diff --git a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php
index 2b7be3f219..dd851590ab 100644
--- a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php
@@ -1,185 +1,186 @@
getManiphestTaskPHIDs();
}
public function renderLabelForRevisionView() {
return 'Maniphest Tasks:';
}
public function renderValueForRevisionView() {
$task_phids = $this->getManiphestTaskPHIDs();
if (!$task_phids) {
return null;
}
$links = array();
foreach ($task_phids as $task_phid) {
$links[] = $this->getHandle($task_phid)->renderLink();
}
return phutil_implode_html(phutil_tag('br'), $links);
}
private function getManiphestTaskPHIDs() {
$revision = $this->getRevision();
if (!$revision->getPHID()) {
return array();
}
return PhabricatorEdgeQuery::loadDestinationPHIDs(
$revision->getPHID(),
PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK);
}
/**
* Attach the revision to the task(s) and the task(s) to the revision.
*
* @return void
*/
public function didWriteRevision(DifferentialRevisionEditor $editor) {
$revision = $editor->getRevision();
$revision_phid = $revision->getPHID();
$edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK;
$old_phids = $this->oldManiphestTasks;
$add_phids = $this->maniphestTasks;
$rem_phids = array_diff($old_phids, $add_phids);
$edge_editor = id(new PhabricatorEdgeEditor())
->setActor($this->getUser());
foreach ($add_phids as $phid) {
$edge_editor->addEdge($revision_phid, $edge_type, $phid);
}
foreach ($rem_phids as $phid) {
$edge_editor->removeEdge($revision_phid, $edge_type, $phid);
}
$edge_editor->save();
}
protected function didSetRevision() {
$this->maniphestTasks = $this->getManiphestTaskPHIDs();
$this->oldManiphestTasks = $this->maniphestTasks;
}
public function getRequiredHandlePHIDsForCommitMessage() {
return $this->maniphestTasks;
}
public function shouldAppearOnCommitMessageTemplate() {
return false;
}
public function shouldAppearOnCommitMessage() {
return $this->shouldAppearOnRevisionView();
}
public function getCommitMessageKey() {
return 'maniphestTaskPHIDs';
}
public function setValueFromParsedCommitMessage($value) {
$this->maniphestTasks = array_unique(nonempty($value, array()));
return $this;
}
public function renderLabelForCommitMessage() {
return 'Maniphest Tasks';
}
public function getSupportedCommitMessageLabels() {
return array(
'Maniphest Task',
'Maniphest Tasks',
);
}
public function renderValueForCommitMessage($is_edit) {
if (!$this->maniphestTasks) {
return null;
}
$names = array();
foreach ($this->maniphestTasks as $phid) {
$handle = $this->getHandle($phid);
$names[] = $handle->getName();
}
return implode(', ', $names);
}
public function parseValueFromCommitMessage($value) {
$matches = null;
preg_match_all('/T(\d+)/', $value, $matches);
if (empty($matches[0])) {
return array();
}
+ // TODO: T603 Get a viewer here so we can issue the right query.
$task_ids = $matches[1];
$tasks = id(new ManiphestTask())
->loadAllWhere('id in (%Ld)', $task_ids);
$task_phids = array();
$invalid = array();
foreach ($task_ids as $task_id) {
$task = idx($tasks, $task_id);
if (empty($task)) {
$invalid[] = 'T'.$task_id;
} else {
$task_phids[] = $task->getPHID();
}
}
if ($invalid) {
$what = pht('Maniphest Task(s)', count($invalid));
$invalid = implode(', ', $invalid);
throw new DifferentialFieldParseException(
"Commit message references nonexistent {$what}: {$invalid}.");
}
return $task_phids;
}
public function renderValueForMail($phase) {
if ($phase == DifferentialMailPhase::COMMENT) {
return null;
}
if (!$this->maniphestTasks) {
return null;
}
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getUser())
->withPHIDs($this->maniphestTasks)
->execute();
$body = array();
$body[] = 'MANIPHEST TASKS';
foreach ($handles as $handle) {
$body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI());
}
return implode("\n", $body);
}
public function getCommitMessageTips() {
return array(
'Use "Fixes T123" in your summary to mark that the current '.
'revision completes a given task.'
);
}
}
diff --git a/src/applications/maniphest/conduit/ConduitAPI_maniphest_info_Method.php b/src/applications/maniphest/conduit/ConduitAPI_maniphest_info_Method.php
index 59c0ab8e2f..a6c14f48f3 100644
--- a/src/applications/maniphest/conduit/ConduitAPI_maniphest_info_Method.php
+++ b/src/applications/maniphest/conduit/ConduitAPI_maniphest_info_Method.php
@@ -1,41 +1,44 @@
'required id',
);
}
public function defineReturnType() {
return 'nonempty dict';
}
public function defineErrorTypes() {
return array(
'ERR_BAD_TASK' => 'No such maniphest task exists',
);
}
protected function execute(ConduitAPIRequest $request) {
$task_id = $request->getValue('task_id');
- $task = id(new ManiphestTask())->load($task_id);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($request->getUser())
+ ->withIDs(array($task_id))
+ ->executeOne();
if (!$task) {
throw new ConduitException('ERR_BAD_TASK');
}
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/maniphest/conduit/ConduitAPI_maniphest_update_Method.php b/src/applications/maniphest/conduit/ConduitAPI_maniphest_update_Method.php
index a076b0f5da..aeae252f55 100644
--- a/src/applications/maniphest/conduit/ConduitAPI_maniphest_update_Method.php
+++ b/src/applications/maniphest/conduit/ConduitAPI_maniphest_update_Method.php
@@ -1,63 +1,67 @@
'No such maniphest task exists.',
'ERR-INVALID-PARAMETER' => 'Missing or malformed parameter.',
'ERR-NO-EFFECT' => 'Update has no effect.',
);
}
public function defineParamTypes() {
return $this->getTaskFields($is_new = false);
}
public function defineReturnType() {
return 'nonempty dict';
}
protected function execute(ConduitAPIRequest $request) {
$id = $request->getValue('id');
$phid = $request->getValue('phid');
if (($id && $phid) || (!$id && !$phid)) {
throw new Exception("Specify exactly one of 'id' and 'phid'.");
}
if ($id) {
- $task = id(new ManiphestTask())->load($id);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($request->getUser())
+ ->withIDs(array($id))
+ ->executeOne();
} else {
- $task = id(new ManiphestTask())->loadOneWhere(
- 'phid = %s',
- $phid);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($request->getUser())
+ ->withPHIDs(array($phid))
+ ->executeOne();
}
$params = $request->getAllParameters();
unset($params['id']);
unset($params['phid']);
$params = call_user_func_array('coalesce', $params);
if (!$params) {
throw new ConduitException('ERR-NO-EFFECT');
}
if (!$task) {
throw new ConduitException('ERR-BAD-TASK');
}
$this->applyRequest($task, $request, $is_new = false);
return $this->buildTaskInfoDictionary($task);
}
}
diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php
index 1664d18956..a9dd8136f2 100644
--- a/src/applications/maniphest/controller/ManiphestBatchEditController.php
+++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php
@@ -1,332 +1,338 @@
getRequest();
$user = $request->getUser();
$task_ids = $request->getArr('batch');
- $tasks = id(new ManiphestTask())->loadAllWhere(
- 'id IN (%Ld)',
- $task_ids);
+ $tasks = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs($task_ids)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->execute();
$actions = $request->getStr('actions');
if ($actions) {
$actions = json_decode($actions, true);
}
if ($request->isFormPost() && is_array($actions)) {
foreach ($tasks as $task) {
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
$field_list->readFieldsFromStorage($task);
$xactions = $this->buildTransactions($actions, $task);
if ($xactions) {
// TODO: Set content source to "batch edit".
$editor = id(new ManiphestTransactionEditorPro())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($task, $xactions);
}
}
$task_ids = implode(',', mpull($tasks, 'getID'));
return id(new AphrontRedirectResponse())
->setURI('/maniphest/?ids='.$task_ids);
}
$handle_phids = mpull($tasks, 'getOwnerPHID');
$handles = $this->loadViewerHandles($handle_phids);
$list = new ManiphestTaskListView();
$list->setTasks($tasks);
$list->setUser($user);
$list->setHandles($handles);
$template = new AphrontTokenizerTemplateView();
$template = $template->render();
require_celerity_resource('maniphest-batch-editor');
Javelin::initBehavior(
'maniphest-batch-editor',
array(
'root' => 'maniphest-batch-edit-form',
'tokenizerTemplate' => $template,
'sources' => array(
'project' => array(
'src' => '/typeahead/common/projects/',
'placeholder' => pht('Type a project name...'),
),
'owner' => array(
'src' => '/typeahead/common/searchowner/',
'placeholder' => pht('Type a user name...'),
'limit' => 1,
),
'cc' => array(
'src' => '/typeahead/common/mailable/',
'placeholder' => pht('Type a user name...'),
)
),
'input' => 'batch-form-actions',
'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(),
'statusMap' => ManiphestTaskStatus::getTaskStatusMap(),
));
$form = new AphrontFormView();
$form->setUser($user);
$form->setID('maniphest-batch-edit-form');
foreach ($tasks as $task) {
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'batch[]',
'value' => $task->getID(),
)));
}
$form->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'actions',
'id' => 'batch-form-actions',
)));
$form->appendChild(
phutil_tag('p', array(), pht('These tasks will be edited:')));
$form->appendChild($list);
$form->appendChild(
id(new AphrontFormInsetView())
->setTitle('Actions')
->setRightButton(javelin_tag(
'a',
array(
'href' => '#',
'class' => 'button green',
'sigil' => 'add-action',
'mustcapture' => true,
),
pht('Add Another Action')))
->setContent(javelin_tag(
'table',
array(
'sigil' => 'maniphest-batch-actions',
'class' => 'maniphest-batch-actions-table',
),
'')))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Update Tasks'))
->addCancelButton('/maniphest/'));
$title = pht('Batch Editor');
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($title));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText(pht('Batch Edit Tasks'))
->setForm($form);
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
),
array(
'title' => $title,
));
}
private function buildTransactions($actions, ManiphestTask $task) {
$value_map = array();
$type_map = array(
'add_comment' => PhabricatorTransactions::TYPE_COMMENT,
'assign' => ManiphestTransaction::TYPE_OWNER,
'status' => ManiphestTransaction::TYPE_STATUS,
'priority' => ManiphestTransaction::TYPE_PRIORITY,
'add_project' => ManiphestTransaction::TYPE_PROJECTS,
'remove_project' => ManiphestTransaction::TYPE_PROJECTS,
'add_ccs' => ManiphestTransaction::TYPE_CCS,
'remove_ccs' => ManiphestTransaction::TYPE_CCS,
);
$edge_edit_types = array(
'add_project' => true,
'remove_project' => true,
'add_ccs' => true,
'remove_ccs' => true,
);
$xactions = array();
foreach ($actions as $action) {
if (empty($type_map[$action['action']])) {
throw new Exception("Unknown batch edit action '{$action}'!");
}
$type = $type_map[$action['action']];
// Figure out the current value, possibly after modifications by other
// batch actions of the same type. For example, if the user chooses to
// "Add Comment" twice, we should add both comments. More notably, if the
// user chooses "Remove Project..." and also "Add Project...", we should
// avoid restoring the removed project in the second transaction.
if (array_key_exists($type, $value_map)) {
$current = $value_map[$type];
} else {
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$current = null;
break;
case ManiphestTransaction::TYPE_OWNER:
$current = $task->getOwnerPHID();
break;
case ManiphestTransaction::TYPE_STATUS:
$current = $task->getStatus();
break;
case ManiphestTransaction::TYPE_PRIORITY:
$current = $task->getPriority();
break;
case ManiphestTransaction::TYPE_PROJECTS:
$current = $task->getProjectPHIDs();
break;
case ManiphestTransaction::TYPE_CCS:
$current = $task->getCCPHIDs();
break;
}
}
// Check if the value is meaningful / provided, and normalize it if
// necessary. This discards, e.g., empty comments and empty owner
// changes.
$value = $action['value'];
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (!strlen($value)) {
continue 2;
}
break;
case ManiphestTransaction::TYPE_OWNER:
if (empty($value)) {
continue 2;
}
$value = head($value);
if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) {
$value = null;
}
break;
case ManiphestTransaction::TYPE_PROJECTS:
if (empty($value)) {
continue 2;
}
break;
case ManiphestTransaction::TYPE_CCS:
if (empty($value)) {
continue 2;
}
break;
}
// If the edit doesn't change anything, go to the next action. This
// check is only valid for changes like "owner", "status", etc, not
// for edge edits, because we should still apply an edit like
// "Remove Projects: A, B" to a task with projects "A, B".
if (empty($edge_edit_types[$action['action']])) {
if ($value == $current) {
continue;
}
}
// Apply the value change; for most edits this is just replacement, but
// some need to merge the current and edited values (add/remove project).
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
if (strlen($current)) {
$value = $current."\n\n".$value;
}
break;
case ManiphestTransaction::TYPE_PROJECTS:
case ManiphestTransaction::TYPE_CCS:
$remove_actions = array(
'remove_project' => true,
'remove_ccs' => true,
);
$is_remove = isset($remove_actions[$action['action']]);
$current = array_fill_keys($current, true);
$value = array_fill_keys($value, true);
$new = $current;
$did_something = false;
if ($is_remove) {
foreach ($value as $phid => $ignored) {
if (isset($new[$phid])) {
unset($new[$phid]);
$did_something = true;
}
}
} else {
foreach ($value as $phid => $ignored) {
if (empty($new[$phid])) {
$new[$phid] = true;
$did_something = true;
}
}
}
if (!$did_something) {
continue 2;
}
$value = array_keys($new);
break;
}
$value_map[$type] = $value;
}
$template = new ManiphestTransaction();
foreach ($value_map as $type => $value) {
$xaction = clone $template;
$xaction->setTransactionType($type);
switch ($type) {
case PhabricatorTransactions::TYPE_COMMENT:
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($value));
break;
default:
$xaction->setNewValue($value);
break;
}
$xactions[] = $xaction;
}
return $xactions;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php
index d48189a107..67ed9fd09f 100644
--- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php
+++ b/src/applications/maniphest/controller/ManiphestSubpriorityController.php
@@ -1,62 +1,73 @@
getRequest();
$user = $request->getUser();
if (!$request->validateCSRF()) {
return new Aphront403Response();
}
- $task = id(new ManiphestTask())->load($request->getInt('task'));
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($request->getInt('task')))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
if (!$task) {
return new Aphront404Response();
}
if ($request->getInt('after')) {
- $after_task = id(new ManiphestTask())->load($request->getInt('after'));
+ $after_task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($request->getInt('after')))
+ ->executeOne();
if (!$after_task) {
return new Aphront404Response();
}
$after_pri = $after_task->getPriority();
$after_sub = $after_task->getSubpriority();
} else {
$after_pri = $request->getInt('priority');
$after_sub = null;
}
$new_sub = ManiphestTransactionEditor::getNextSubpriority(
$after_pri,
$after_sub);
$task->setSubpriority($new_sub);
if ($after_pri != $task->getPriority()) {
$xactions = array();
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_PRIORITY)
->setNewValue($after_pri);
$editor = id(new ManiphestTransactionEditorPro())
->setActor($user)
->setContinueOnMissingFields(true)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($task, $xactions);
} else {
$task->save();
}
return id(new AphrontAjaxResponse())->setContent(
array(
'tasks' => $this->renderSingleTask($task),
));
}
}
diff --git a/src/applications/maniphest/controller/ManiphestSubscribeController.php b/src/applications/maniphest/controller/ManiphestSubscribeController.php
index d05af5a16c..d9a1ad5049 100644
--- a/src/applications/maniphest/controller/ManiphestSubscribeController.php
+++ b/src/applications/maniphest/controller/ManiphestSubscribeController.php
@@ -1,48 +1,51 @@
id = $data['id'];
$this->action = $data['action'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
- $task = id(new ManiphestTask())->load($this->id);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($this->id))
+ ->executeOne();
if (!$task) {
return new Aphront404Response();
}
$ccs = $task->getCCPHIDs();
switch ($this->action) {
case 'add':
$ccs[] = $user->getPHID();
break;
case 'rem':
$ccs = array_diff($ccs, array($user->getPHID()));
break;
default:
return new Aphront400Response();
}
$xaction = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_CCS)
->setNewValue($ccs);
$editor = id(new ManiphestTransactionEditorPro())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->applyTransactions($task, array($xaction));
return id(new AphrontRedirectResponse())->setURI('/T'.$task->getID());
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
index e3909df011..4e72012bd1 100644
--- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php
@@ -1,604 +1,610 @@
id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$e_title = null;
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
- $task = id(new ManiphestTask())->load($this->id);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($this->id))
+ ->executeOne();
if (!$task) {
return new Aphront404Response();
}
$workflow = $request->getStr('workflow');
$parent_task = null;
if ($workflow && is_numeric($workflow)) {
- $parent_task = id(new ManiphestTask())->load($workflow);
+ $parent_task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($workflow))
+ ->executeOne();
}
$transactions = id(new ManiphestTransactionQuery())
->setViewer($user)
->withObjectPHIDs(array($task->getPHID()))
->needComments(true)
->execute();
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_VIEW);
foreach ($field_list->getFields() as $field) {
$field->setObject($task);
$field->setViewer($user);
}
$field_list->readFieldsFromStorage($task);
$aux_fields = $field_list->getFields();
$e_commit = PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT;
$e_dep_on = PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK;
$e_dep_by = PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK;
$e_rev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
$e_mock = PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK;
$phid = $task->getPHID();
$query = id(new PhabricatorEdgeQuery())
->withSourcePHIDs(array($phid))
->withEdgeTypes(
array(
$e_commit,
$e_dep_on,
$e_dep_by,
$e_rev,
$e_mock,
));
$edges = idx($query->execute(), $phid);
$phids = array_fill_keys($query->getDestinationPHIDs(), true);
foreach ($task->getCCPHIDs() as $phid) {
$phids[$phid] = true;
}
foreach ($task->getProjectPHIDs() as $phid) {
$phids[$phid] = true;
}
if ($task->getOwnerPHID()) {
$phids[$task->getOwnerPHID()] = true;
}
$phids[$task->getAuthorPHID()] = true;
$attached = $task->getAttached();
foreach ($attached as $type => $list) {
foreach ($list as $phid => $info) {
$phids[$phid] = true;
}
}
if ($parent_task) {
$phids[$parent_task->getPHID()] = true;
}
$phids = array_keys($phids);
$this->loadHandles($phids);
$handles = $this->getLoadedHandles();
$context_bar = null;
if ($parent_task) {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?parent='.$parent_task->getID(),
'class' => 'green button',
),
pht('Create Another Subtask')));
$context_bar->appendChild(hsprintf(
'Created a subtask of %s',
$this->getHandle($parent_task->getPHID())->renderLink()));
} else if ($workflow == 'create') {
$context_bar = new AphrontContextBarView();
$context_bar->addButton(phutil_tag('label', array(), 'Create Another'));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/?template='.$task->getID(),
'class' => 'green button',
),
pht('Similar Task')));
$context_bar->addButton(phutil_tag(
'a',
array(
'href' => '/maniphest/task/create/',
'class' => 'green button',
),
pht('Empty Task')));
$context_bar->appendChild(pht('New task created.'));
}
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
$engine->addObject($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION);
foreach ($transactions as $modern_xaction) {
if ($modern_xaction->getComment()) {
$engine->addObject(
$modern_xaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
}
$engine->process();
$resolution_types = ManiphestTaskStatus::getTaskStatusMap();
$transaction_types = array(
PhabricatorTransactions::TYPE_COMMENT => pht('Comment'),
ManiphestTransaction::TYPE_STATUS => pht('Close Task'),
ManiphestTransaction::TYPE_OWNER => pht('Reassign / Claim'),
ManiphestTransaction::TYPE_CCS => pht('Add CCs'),
ManiphestTransaction::TYPE_PRIORITY => pht('Change Priority'),
ManiphestTransaction::TYPE_ATTACH => pht('Upload File'),
ManiphestTransaction::TYPE_PROJECTS => pht('Associate Projects'),
);
if ($task->getStatus() == ManiphestTaskStatus::STATUS_OPEN) {
$resolution_types = array_select_keys(
$resolution_types,
array(
ManiphestTaskStatus::STATUS_CLOSED_RESOLVED,
ManiphestTaskStatus::STATUS_CLOSED_WONTFIX,
ManiphestTaskStatus::STATUS_CLOSED_INVALID,
ManiphestTaskStatus::STATUS_CLOSED_SPITE,
));
} else {
$resolution_types = array(
ManiphestTaskStatus::STATUS_OPEN => 'Reopened',
);
$transaction_types[ManiphestTransaction::TYPE_STATUS] =
'Reopen Task';
unset($transaction_types[ManiphestTransaction::TYPE_PRIORITY]);
unset($transaction_types[ManiphestTransaction::TYPE_OWNER]);
}
$default_claim = array(
$user->getPHID() => $user->getUsername().' ('.$user->getRealName().')',
);
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft_text = $draft->getDraft();
} else {
$draft_text = null;
}
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
if ($is_serious) {
// Prevent tasks from being closed "out of spite" in serious business
// installs.
unset($resolution_types[ManiphestTaskStatus::STATUS_CLOSED_SPITE]);
}
$comment_form = new AphrontFormView();
$comment_form
->setUser($user)
->setShaded(true)
->setAction('/maniphest/transaction/save/')
->setEncType('multipart/form-data')
->addHiddenInput('taskID', $task->getID())
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Action'))
->setName('action')
->setOptions($transaction_types)
->setID('transaction-action'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Resolution'))
->setName('resolution')
->setControlID('resolution')
->setControlStyle('display: none')
->setOptions($resolution_types))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assign To'))
->setName('assign_to')
->setControlID('assign_to')
->setControlStyle('display: none')
->setID('assign-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CCs'))
->setName('ccs')
->setControlID('ccs')
->setControlStyle('display: none')
->setID('cc-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setControlID('priority')
->setControlStyle('display: none')
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setControlID('projects')
->setControlStyle('display: none')
->setID('projects-tokenizer')
->setDisableBehavior(true))
->appendChild(
id(new AphrontFormFileControl())
->setLabel(pht('File'))
->setName('file')
->setControlID('file')
->setControlStyle('display: none'))
->appendChild(
id(new PhabricatorRemarkupControl())
->setLabel(pht('Comments'))
->setName('comments')
->setValue($draft_text)
->setID('transaction-comments')
->setUser($user))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue($is_serious ? pht('Submit') : pht('Avast!')));
$control_map = array(
ManiphestTransaction::TYPE_STATUS => 'resolution',
ManiphestTransaction::TYPE_OWNER => 'assign_to',
ManiphestTransaction::TYPE_CCS => 'ccs',
ManiphestTransaction::TYPE_PRIORITY => 'priority',
ManiphestTransaction::TYPE_PROJECTS => 'projects',
ManiphestTransaction::TYPE_ATTACH => 'file',
);
$tokenizer_map = array(
ManiphestTransaction::TYPE_PROJECTS => array(
'id' => 'projects-tokenizer',
'src' => '/typeahead/common/projects/',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => pht('Type a project name...'),
),
ManiphestTransaction::TYPE_OWNER => array(
'id' => 'assign-tokenizer',
'src' => '/typeahead/common/users/',
'value' => $default_claim,
'limit' => 1,
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => pht('Type a user name...'),
),
ManiphestTransaction::TYPE_CCS => array(
'id' => 'cc-tokenizer',
'src' => '/typeahead/common/mailable/',
'ondemand' => PhabricatorEnv::getEnvConfig('tokenizer.ondemand'),
'placeholder' => pht('Type a user or mailing list...'),
),
);
Javelin::initBehavior('maniphest-transaction-controls', array(
'select' => 'transaction-action',
'controlMap' => $control_map,
'tokenizers' => $tokenizer_map,
));
Javelin::initBehavior('maniphest-transaction-preview', array(
'uri' => '/maniphest/transaction/preview/'.$task->getID().'/',
'preview' => 'transaction-preview',
'comments' => 'transaction-comments',
'action' => 'transaction-action',
'map' => $control_map,
'tokenizers' => $tokenizer_map,
));
$comment_header = id(new PHUIHeaderView())
->setHeader($is_serious ? pht('Add Comment') : pht('Weigh In'));
$preview_panel = hsprintf(
'
',
pht('Loading preview...'));
$timeline = id(new PhabricatorApplicationTransactionView())
->setUser($user)
->setObjectPHID($task->getPHID())
->setTransactions($transactions)
->setMarkupEngine($engine);
$object_name = 'T'.$task->getID();
$actions = $this->buildActionView($task);
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($object_name)
->setHref('/'.$object_name))
->setActionList($actions);
$header = $this->buildHeaderView($task);
$properties = $this->buildPropertyView($task, $field_list, $edges, $engine);
return $this->buildApplicationPage(
array(
$crumbs,
$context_bar,
$header,
$actions,
$properties,
$timeline,
$comment_header,
$comment_form,
$preview_panel,
),
array(
'title' => 'T'.$task->getID().' '.$task->getTitle(),
'pageObjects' => array($task->getPHID()),
'device' => true,
));
}
private function buildHeaderView(ManiphestTask $task) {
$view = id(new PHUIHeaderView())
->setHeader($task->getTitle());
$status = $task->getStatus();
$status_name = ManiphestTaskStatus::renderFullDescription($status);
$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name);
return $view;
}
private function buildActionView(ManiphestTask $task) {
$viewer = $this->getRequest()->getUser();
$viewer_phid = $viewer->getPHID();
$viewer_is_cc = in_array($viewer_phid, $task->getCCPHIDs());
$id = $task->getID();
$phid = $task->getPHID();
$view = id(new PhabricatorActionListView())
->setUser($viewer)
->setObject($task)
->setObjectURI($this->getRequest()->getRequestURI())
->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Task'))
->setIcon('edit')
->setHref($this->getApplicationURI("/task/edit/{$id}/")));
if ($task->getOwnerPHID() === $viewer_phid) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Automatically Subscribed'))
->setDisabled(true)
->setIcon('enable'));
} else {
$action = $viewer_is_cc ? 'rem' : 'add';
$name = $viewer_is_cc ? pht('Unsubscribe') : pht('Subscribe');
$icon = $viewer_is_cc ? 'disable' : 'check';
$view->addAction(
id(new PhabricatorActionView())
->setName($name)
->setHref("/maniphest/subscribe/{$action}/{$id}/")
->setRenderAsForm(true)
->setUser($viewer)
->setIcon($icon));
}
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Merge Duplicates In'))
->setHref("/search/attach/{$phid}/TASK/merge/")
->setWorkflow(true)
->setIcon('merge'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Create Subtask'))
->setHref($this->getApplicationURI("/task/create/?parent={$id}"))
->setIcon('fork'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Dependencies'))
->setHref("/search/attach/{$phid}/TASK/dependencies/")
->setWorkflow(true)
->setIcon('link'));
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Differential Revisions'))
->setHref("/search/attach/{$phid}/DREV/")
->setWorkflow(true)
->setIcon('attach'));
$pholio_app =
PhabricatorApplication::getByClass('PhabricatorApplicationPholio');
if ($pholio_app->isInstalled()) {
$view->addAction(
id(new PhabricatorActionView())
->setName(pht('Edit Pholio Mocks'))
->setHref("/search/attach/{$phid}/MOCK/edge/")
->setWorkflow(true)
->setIcon('attach'));
}
return $view;
}
private function buildPropertyView(
ManiphestTask $task,
PhabricatorCustomFieldList $field_list,
array $edges,
PhabricatorMarkupEngine $engine) {
$viewer = $this->getRequest()->getUser();
$view = id(new PhabricatorPropertyListView())
->setUser($viewer)
->setObject($task);
$view->addProperty(
pht('Assigned To'),
$task->getOwnerPHID()
? $this->getHandle($task->getOwnerPHID())->renderLink()
: phutil_tag('em', array(), pht('None')));
$view->addProperty(
pht('Priority'),
ManiphestTaskPriority::getTaskPriorityName($task->getPriority()));
$view->addProperty(
pht('Subscribers'),
$task->getCCPHIDs()
? $this->renderHandlesForPHIDs($task->getCCPHIDs(), ',')
: phutil_tag('em', array(), pht('None')));
$view->addProperty(
pht('Author'),
$this->getHandle($task->getAuthorPHID())->renderLink());
$source = $task->getOriginalEmailSource();
if ($source) {
$subject = '[T'.$task->getID().'] '.$task->getTitle();
$view->addProperty(
pht('From Email'),
phutil_tag(
'a',
array(
'href' => 'mailto:'.$source.'?subject='.$subject
),
$source));
}
$view->addProperty(
pht('Projects'),
$task->getProjectPHIDs()
? $this->renderHandlesForPHIDs($task->getProjectPHIDs(), ',')
: phutil_tag('em', array(), pht('None')));
$edge_types = array(
PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK
=> pht('Dependent Tasks'),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK
=> pht('Depends On'),
PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV
=> pht('Differential Revisions'),
PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK
=> pht('Pholio Mocks'),
);
$revisions_commits = array();
$handles = $this->getLoadedHandles();
$commit_phids = array_keys(
$edges[PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT]);
if ($commit_phids) {
$commit_drev = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_DREV;
$drev_edges = id(new PhabricatorEdgeQuery())
->withSourcePHIDs($commit_phids)
->withEdgeTypes(array($commit_drev))
->execute();
foreach ($commit_phids as $phid) {
$revisions_commits[$phid] = $handles[$phid]->renderLink();
$revision_phid = key($drev_edges[$phid][$commit_drev]);
$revision_handle = idx($handles, $revision_phid);
if ($revision_handle) {
$task_drev = PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV;
unset($edges[$task_drev][$revision_phid]);
$revisions_commits[$phid] = hsprintf(
'%s / %s',
$revision_handle->renderLink($revision_handle->getName()),
$revisions_commits[$phid]);
}
}
}
foreach ($edge_types as $edge_type => $edge_name) {
if ($edges[$edge_type]) {
$view->addProperty(
$edge_name,
$this->renderHandlesForPHIDs(array_keys($edges[$edge_type])));
}
}
if ($revisions_commits) {
$view->addProperty(
pht('Commits'),
phutil_implode_html(phutil_tag('br'), $revisions_commits));
}
$attached = $task->getAttached();
if (!is_array($attached)) {
$attached = array();
}
$file_infos = idx($attached, PhabricatorFilePHIDTypeFile::TYPECONST);
if ($file_infos) {
$file_phids = array_keys($file_infos);
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
$file_view = new PhabricatorFileLinkListView();
$file_view->setFiles($files);
$view->addProperty(
pht('Files'),
$file_view->render());
}
$field_list->appendFieldsToPropertyList(
$task,
$viewer,
$view);
$view->invokeWillRenderEvent();
if (strlen($task->getDescription())) {
$view->addSectionHeader(pht('Description'));
$view->addTextContent(
phutil_tag(
'div',
array(
'class' => 'phabricator-remarkup',
),
$engine->getOutput($task, ManiphestTask::MARKUP_FIELD_DESCRIPTION)));
}
return $view;
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php
index 55ade3bf6f..16a556c376 100644
--- a/src/applications/maniphest/controller/ManiphestTaskEditController.php
+++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php
@@ -1,562 +1,576 @@
id = idx($data, 'id');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$files = array();
$parent_task = null;
$template_id = null;
if ($this->id) {
- $task = id(new ManiphestTask())->load($this->id);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->withIDs(array($this->id))
+ ->executeOne();
if (!$task) {
return new Aphront404Response();
}
} else {
$task = new ManiphestTask();
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
$task->setAuthorPHID($user->getPHID());
// These allow task creation with defaults.
if (!$request->isFormPost()) {
$task->setTitle($request->getStr('title'));
$default_projects = $request->getStr('projects');
if ($default_projects) {
$task->setProjectPHIDs(explode(';', $default_projects));
}
$task->setDescription($request->getStr('description'));
$assign = $request->getStr('assign');
if (strlen($assign)) {
$assign_user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$assign);
if ($assign_user) {
$task->setOwnerPHID($assign_user->getPHID());
}
}
}
$file_phids = $request->getArr('files', array());
if (!$file_phids) {
// Allow a single 'file' key instead, mostly since Mac OS X urlencodes
// square brackets in URLs when passed to 'open', so you can't 'open'
// a URL like '?files[]=xyz' and have PHP interpret it correctly.
$phid = $request->getStr('file');
if ($phid) {
$file_phids = array($phid);
}
}
if ($file_phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid IN (%Ls)',
$file_phids);
}
$template_id = $request->getInt('template');
// You can only have a parent task if you're creating a new task.
$parent_id = $request->getInt('parent');
if ($parent_id) {
- $parent_task = id(new ManiphestTask())->load($parent_id);
+ $parent_task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($parent_id))
+ ->executeOne();
if (!$template_id) {
$template_id = $parent_id;
}
}
}
$errors = array();
$e_title = true;
$field_list = PhabricatorCustomField::getObjectFields(
$task,
PhabricatorCustomField::ROLE_EDIT);
foreach ($field_list->getFields() as $field) {
$field->setObject($task);
$field->setViewer($user);
}
$field_list->readFieldsFromStorage($task);
$aux_fields = $field_list->getFields();
if ($request->isFormPost()) {
$changes = array();
$new_title = $request->getStr('title');
$new_desc = $request->getStr('description');
$new_status = $request->getStr('status');
if (!$task->getID()) {
$workflow = 'create';
} else {
$workflow = '';
}
$changes[ManiphestTransaction::TYPE_TITLE] = $new_title;
$changes[ManiphestTransaction::TYPE_DESCRIPTION] = $new_desc;
$changes[ManiphestTransaction::TYPE_STATUS] = $new_status;
$owner_tokenizer = $request->getArr('assigned_to');
$owner_phid = reset($owner_tokenizer);
if (!strlen($new_title)) {
$e_title = pht('Required');
$errors[] = pht('Title is required.');
}
$old_values = array();
foreach ($aux_fields as $aux_arr_key => $aux_field) {
// TODO: This should be buildFieldTransactionsFromRequest() once we
// switch to ApplicationTransactions properly.
$aux_old_value = $aux_field->getOldValueForApplicationTransactions();
$aux_field->readValueFromRequest($request);
$aux_new_value = $aux_field->getNewValueForApplicationTransactions();
// TODO: We're faking a call to the ApplicaitonTransaction validation
// logic here. We need valid objects to pass, but they aren't used
// in a meaningful way. For now, build User objects. Once the Maniphest
// objects exist, this will switch over automatically. This is a big
// hack but shouldn't be long for this world.
$placeholder_editor = new PhabricatorUserProfileEditor();
$field_errors = $aux_field->validateApplicationTransactions(
$placeholder_editor,
PhabricatorTransactions::TYPE_CUSTOMFIELD,
array(
id(new ManiphestTransaction())
->setOldValue($aux_old_value)
->setNewValue($aux_new_value),
));
foreach ($field_errors as $error) {
$errors[] = $error->getMessage();
}
$old_values[$aux_field->getFieldKey()] = $aux_old_value;
}
if ($errors) {
$task->setTitle($new_title);
$task->setDescription($new_desc);
$task->setPriority($request->getInt('priority'));
$task->setOwnerPHID($owner_phid);
$task->setCCPHIDs($request->getArr('cc'));
$task->setProjectPHIDs($request->getArr('projects'));
} else {
$changes[ManiphestTransaction::TYPE_PRIORITY] =
$request->getInt('priority');
$changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid;
$changes[ManiphestTransaction::TYPE_CCS] = $request->getArr('cc');
$changes[ManiphestTransaction::TYPE_PROJECTS] =
$request->getArr('projects');
if ($files) {
$file_map = mpull($files, 'getPHID');
$file_map = array_fill_keys($file_map, array());
$changes[ManiphestTransaction::TYPE_ATTACH] = array(
PhabricatorFilePHIDTypeFile::TYPECONST => $file_map,
);
}
$template = new ManiphestTransaction();
$transactions = array();
foreach ($changes as $type => $value) {
$transaction = clone $template;
$transaction->setTransactionType($type);
$transaction->setNewValue($value);
$transactions[] = $transaction;
}
if ($aux_fields) {
foreach ($aux_fields as $aux_field) {
$transaction = clone $template;
$transaction->setTransactionType(
PhabricatorTransactions::TYPE_CUSTOMFIELD);
$aux_key = $aux_field->getFieldKey();
$transaction->setMetadataValue('customfield:key', $aux_key);
$old = idx($old_values, $aux_key);
$new = $aux_field->getNewValueForApplicationTransactions();
$transaction->setOldValue($old);
$transaction->setNewValue($new);
$transactions[] = $transaction;
}
}
if ($transactions) {
$is_new = !$task->getID();
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditorPro())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnNoEffect(true)
->applyTransactions($task, $transactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
}
if ($parent_task) {
id(new PhabricatorEdgeEditor())
->setActor($user)
->addEdge(
$parent_task->getPHID(),
PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$task->getPHID())
->save();
$workflow = $parent_task->getID();
}
if ($request->isAjax()) {
return id(new AphrontAjaxResponse())->setContent(
array(
'tasks' => $this->renderSingleTask($task),
));
}
$redirect_uri = '/T'.$task->getID();
if ($workflow) {
$redirect_uri .= '?workflow='.$workflow;
}
return id(new AphrontRedirectResponse())
->setURI($redirect_uri);
}
} else {
if (!$task->getID()) {
$task->setCCPHIDs(array(
$user->getPHID(),
));
if ($template_id) {
- $template_task = id(new ManiphestTask())->load($template_id);
+ $template_task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($template_id))
+ ->executeOne();
if ($template_task) {
$task->setCCPHIDs($template_task->getCCPHIDs());
$task->setProjectPHIDs($template_task->getProjectPHIDs());
$task->setOwnerPHID($template_task->getOwnerPHID());
$task->setPriority($template_task->getPriority());
$template_fields = PhabricatorCustomField::getObjectFields(
$template_task,
PhabricatorCustomField::ROLE_EDIT);
$fields = $template_fields->getFields();
foreach ($fields as $key => $field) {
if (!$field->shouldCopyWhenCreatingSimilarTask()) {
unset($fields[$key]);
}
if (empty($aux_fields[$key])) {
unset($fields[$key]);
}
}
if ($fields) {
id(new PhabricatorCustomFieldList($fields))
->readFieldsFromStorage($template_task);
foreach ($fields as $key => $field) {
$aux_fields[$key]->setValueFromStorage(
$field->getValueForStorage());
}
}
}
}
}
}
$phids = array_merge(
array($task->getOwnerPHID()),
$task->getCCPHIDs(),
$task->getProjectPHIDs());
if ($parent_task) {
$phids[] = $parent_task->getPHID();
}
$phids = array_filter($phids);
$phids = array_unique($phids);
$handles = $this->loadViewerHandles($phids);
$tvalues = mpull($handles, 'getFullName', 'getPHID');
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setErrors($errors);
$error_view->setTitle(pht('Form Errors'));
}
$priority_map = ManiphestTaskPriority::getTaskPriorityMap();
if ($task->getOwnerPHID()) {
$assigned_value = array(
$task->getOwnerPHID() => $handles[$task->getOwnerPHID()]->getFullName(),
);
} else {
$assigned_value = array();
}
if ($task->getCCPHIDs()) {
$cc_value = array_select_keys($tvalues, $task->getCCPHIDs());
} else {
$cc_value = array();
}
if ($task->getProjectPHIDs()) {
$projects_value = array_select_keys($tvalues, $task->getProjectPHIDs());
} else {
$projects_value = array();
}
$cancel_id = nonempty($task->getID(), $template_id);
if ($cancel_id) {
$cancel_uri = '/T'.$cancel_id;
} else {
$cancel_uri = '/maniphest/';
}
if ($task->getID()) {
$button_name = pht('Save Task');
$header_name = pht('Edit Task');
} else if ($parent_task) {
$cancel_uri = '/T'.$parent_task->getID();
$button_name = pht('Create Task');
$header_name = pht('Create New Subtask');
} else {
$button_name = pht('Create Task');
$header_name = pht('Create New Task');
}
require_celerity_resource('maniphest-task-edit-css');
$project_tokenizer_id = celerity_generate_unique_node_id();
if ($request->isAjax()) {
$form = new PHUIFormLayoutView();
} else {
$form = new AphrontFormView();
$form
->setUser($user)
->addHiddenInput('template', $template_id);
}
if ($parent_task) {
$form
->appendChild(
id(new AphrontFormStaticControl())
->setLabel(pht('Parent Task'))
->setValue($handles[$parent_task->getPHID()]->getFullName()))
->addHiddenInput('parent', $parent_task->getID());
}
$form
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel(pht('Title'))
->setName('title')
->setError($e_title)
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
->setValue($task->getTitle()));
if ($task->getID()) {
// Only show this in "edit" mode, not "create" mode, since creating a
// non-open task is kind of silly and it would just clutter up the
// "create" interface.
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Status'))
->setName('status')
->setValue($task->getStatus())
->setOptions(ManiphestTaskStatus::getTaskStatusMap()));
}
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Assigned To'))
->setName('assigned_to')
->setValue($assigned_value)
->setUser($user)
->setDatasource('/typeahead/common/users/')
->setLimit(1))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('CC'))
->setName('cc')
->setValue($cc_value)
->setUser($user)
->setDatasource('/typeahead/common/mailable/'))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Priority'))
->setName('priority')
->setOptions($priority_map)
->setValue($task->getPriority()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setLabel(pht('Projects'))
->setName('projects')
->setValue($projects_value)
->setID($project_tokenizer_id)
->setCaption(
javelin_tag(
'a',
array(
'href' => '/project/create/',
'mustcapture' => true,
'sigil' => 'project-create',
),
pht('Create New Project')))
->setDatasource('/typeahead/common/projects/'));
foreach ($aux_fields as $aux_field) {
$aux_control = $aux_field->renderEditControl();
$form->appendChild($aux_control);
}
require_celerity_resource('aphront-error-view-css');
Javelin::initBehavior('project-create', array(
'tokenizerID' => $project_tokenizer_id,
));
if ($files) {
$file_display = mpull($files, 'getName');
$file_display = phutil_implode_html(phutil_tag('br'), $file_display);
$form->appendChild(
id(new AphrontFormMarkupControl())
->setLabel(pht('Files'))
->setValue($file_display));
foreach ($files as $ii => $file) {
$form->addHiddenInput('files['.$ii.']', $file->getPHID());
}
}
$description_control = new PhabricatorRemarkupControl();
// "Upsell" creating tasks via email in create flows if the instance is
// configured for this awesomeness.
$email_create = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.public-create-email');
if (!$task->getID() && $email_create) {
$email_hint = pht(
'You can also create tasks by sending an email to: %s',
phutil_tag('tt', array(), $email_create));
$description_control->setCaption($email_hint);
}
$description_control
->setLabel(pht('Description'))
->setName('description')
->setID('description-textarea')
->setValue($task->getDescription())
->setUser($user);
$form
->appendChild($description_control);
if ($request->isAjax()) {
$dialog = id(new AphrontDialogView())
->setUser($user)
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle($header_name)
->appendChild(
array(
$error_view,
$form,
))
->addCancelButton($cancel_uri)
->addSubmitButton($button_name);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton($cancel_uri)
->setValue($button_name));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($header_name)
->setFormError($error_view)
->setForm($form);
$preview = id(new PHUIRemarkupPreviewPanel())
->setHeader(pht('Description Preview'))
->setControlID('description-textarea')
->setPreviewURI($this->getApplicationURI('task/descriptionpreview/'));
if ($task->getID()) {
$page_objects = array( $task->getPHID() );
} else {
$page_objects = array();
}
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addCrumb(
id(new PhabricatorCrumbView())
->setName($header_name));
return $this->buildApplicationPage(
array(
$crumbs,
$form_box,
$preview,
),
array(
'title' => $header_name,
'pageObjects' => $page_objects,
'device' => true,
));
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php
index d5aad01980..eca224d115 100644
--- a/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php
+++ b/src/applications/maniphest/controller/ManiphestTransactionPreviewController.php
@@ -1,128 +1,131 @@
id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$comments = $request->getStr('comments');
- $task = id(new ManiphestTask())->load($this->id);
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($this->id))
+ ->executeOne();
if (!$task) {
return new Aphront404Response();
}
id(new PhabricatorDraft())
->setAuthorPHID($user->getPHID())
->setDraftKey($task->getPHID())
->setDraft($comments)
->replaceOrDelete();
$action = $request->getStr('action');
$transaction = new ManiphestTransaction();
$transaction->setAuthorPHID($user->getPHID());
$transaction->setTransactionType($action);
// This should really be split into a separate transaction, but it should
// all come out in the wash once we fully move to modern stuff.
$transaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($comments));
$value = $request->getStr('value');
// grab phids for handles and set transaction values based on action and
// value (empty or control-specific format) coming in from the wire
switch ($action) {
case ManiphestTransaction::TYPE_PRIORITY:
$transaction->setOldValue($task->getPriority());
$transaction->setNewValue($value);
break;
case ManiphestTransaction::TYPE_OWNER:
if ($value) {
$value = current(json_decode($value));
$phids = array($value);
} else {
$phids = array();
}
$transaction->setNewValue($value);
break;
case ManiphestTransaction::TYPE_CCS:
if ($value) {
$value = json_decode($value);
}
if (!$value) {
$value = array();
}
$phids = $value;
foreach ($task->getCCPHIDs() as $cc_phid) {
$phids[] = $cc_phid;
$value[] = $cc_phid;
}
$transaction->setOldValue($task->getCCPHIDs());
$transaction->setNewValue($value);
break;
case ManiphestTransaction::TYPE_PROJECTS:
if ($value) {
$value = json_decode($value);
}
if (!$value) {
$value = array();
}
$phids = $value;
foreach ($task->getProjectPHIDs() as $project_phid) {
$phids[] = $project_phid;
$value[] = $project_phid;
}
$transaction->setOldValue($task->getProjectPHIDs());
$transaction->setNewValue($value);
break;
default:
$phids = array();
$transaction->setNewValue($value);
break;
}
$phids[] = $user->getPHID();
$handles = $this->loadViewerHandles($phids);
$transactions = array();
$transactions[] = $transaction;
$engine = new PhabricatorMarkupEngine();
$engine->setViewer($user);
if ($transaction->hasComment()) {
$engine->addObject(
$transaction->getComment(),
PhabricatorApplicationTransactionComment::MARKUP_FIELD_COMMENT);
}
$engine->process();
$transaction->setHandles($handles);
$view = id(new PhabricatorApplicationTransactionView())
->setUser($user)
->setTransactions($transactions)
->setIsPreview(true)
->setIsDetailView(true);
return id(new AphrontAjaxResponse())
->setContent((string)phutil_implode_html('', $view->buildEvents()));
}
}
diff --git a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
index d2a52fcdf2..494bf9ac9f 100644
--- a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
+++ b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php
@@ -1,246 +1,253 @@
getRequest();
$user = $request->getUser();
- $task = id(new ManiphestTask())->load($request->getStr('taskID'));
+ // TODO: T603 This doesn't require CAN_EDIT because non-editors can still
+ // leave comments, probably? For now, this just nondisruptive. Smooth this
+ // out once policies are more clear.
+
+ $task = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withIDs(array($request->getStr('taskID')))
+ ->executeOne();
if (!$task) {
return new Aphront404Response();
}
$transactions = array();
$action = $request->getStr('action');
// If we have drag-and-dropped files, attach them first in a separate
// transaction. These can come in on any transaction type, which is why we
// handle them separately.
$files = array();
// Look for drag-and-drop uploads first.
$file_phids = $request->getArr('files');
if ($file_phids) {
$files = id(new PhabricatorFile())->loadAllWhere(
'phid in (%Ls)',
$file_phids);
}
// This means "attach a file" even though we store other types of data
// as 'attached'.
if ($action == ManiphestTransaction::TYPE_ATTACH) {
if (!empty($_FILES['file'])) {
$err = idx($_FILES['file'], 'error');
if ($err != UPLOAD_ERR_NO_FILE) {
$file = PhabricatorFile::newFromPHPUpload(
$_FILES['file'],
array(
'authorPHID' => $user->getPHID(),
));
$files[] = $file;
}
}
}
// If we had explicit or drag-and-drop files, create a transaction
// for those before we deal with whatever else might have happened.
$file_transaction = null;
if ($files) {
$files = mpull($files, 'getPHID', 'getPHID');
$new = $task->getAttached();
foreach ($files as $phid) {
if (empty($new[PhabricatorFilePHIDTypeFile::TYPECONST])) {
$new[PhabricatorFilePHIDTypeFile::TYPECONST] = array();
}
$new[PhabricatorFilePHIDTypeFile::TYPECONST][$phid] = array();
}
$transaction = new ManiphestTransaction();
$transaction
->setTransactionType(ManiphestTransaction::TYPE_ATTACH);
$transaction->setNewValue($new);
$transactions[] = $transaction;
}
// Compute new CCs added by @mentions. Several things can cause CCs to
// be added as side effects: mentions, explicit CCs, users who aren't
// CC'd interacting with the task, and ownership changes. We build up a
// list of all the CCs and then construct a transaction for them at the
// end if necessary.
$added_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions(
array(
$request->getStr('comments'),
));
$cc_transaction = new ManiphestTransaction();
$cc_transaction
->setTransactionType(ManiphestTransaction::TYPE_CCS);
$transaction = new ManiphestTransaction();
$transaction
->setTransactionType($action);
switch ($action) {
case ManiphestTransaction::TYPE_STATUS:
$transaction->setNewValue($request->getStr('resolution'));
break;
case ManiphestTransaction::TYPE_OWNER:
$assign_to = $request->getArr('assign_to');
$assign_to = reset($assign_to);
$transaction->setNewValue($assign_to);
break;
case ManiphestTransaction::TYPE_PROJECTS:
$projects = $request->getArr('projects');
$projects = array_merge($projects, $task->getProjectPHIDs());
$projects = array_filter($projects);
$projects = array_unique($projects);
$transaction->setNewValue($projects);
break;
case ManiphestTransaction::TYPE_CCS:
// Accumulate the new explicit CCs into the array that we'll add in
// the CC transaction later.
$added_ccs = array_merge($added_ccs, $request->getArr('ccs'));
// Throw away the primary transaction.
$transaction = null;
break;
case ManiphestTransaction::TYPE_PRIORITY:
$transaction->setNewValue($request->getInt('priority'));
break;
case ManiphestTransaction::TYPE_ATTACH:
// Nuke this, we created it above.
$transaction = null;
break;
case PhabricatorTransactions::TYPE_COMMENT:
// Nuke this, we're going to create it below.
$transaction = null;
break;
default:
throw new Exception('unknown action');
}
if ($transaction) {
$transactions[] = $transaction;
}
if ($request->getStr('comments')) {
$transactions[] = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent($request->getStr('comments')));
}
// When you interact with a task, we add you to the CC list so you get
// further updates, and possibly assign the task to you if you took an
// ownership action (closing it) but it's currently unowned. We also move
// previous owners to CC if ownership changes. Detect all these conditions
// and create side-effect transactions for them.
$implicitly_claimed = false;
switch ($action) {
case ManiphestTransaction::TYPE_OWNER:
if ($task->getOwnerPHID() == $transaction->getNewValue()) {
// If this is actually no-op, don't generate the side effect.
break;
}
// Otherwise, when a task is reassigned, move the previous owner to CC.
$added_ccs[] = $task->getOwnerPHID();
break;
case ManiphestTransaction::TYPE_STATUS:
if (!$task->getOwnerPHID() &&
$request->getStr('resolution') !=
ManiphestTaskStatus::STATUS_OPEN) {
// Closing an unassigned task. Assign the user as the owner of
// this task.
$assign = new ManiphestTransaction();
$assign->setTransactionType(ManiphestTransaction::TYPE_OWNER);
$assign->setNewValue($user->getPHID());
$transactions[] = $assign;
$implicitly_claimed = true;
}
break;
}
$user_owns_task = false;
if ($implicitly_claimed) {
$user_owns_task = true;
} else {
if ($action == ManiphestTransaction::TYPE_OWNER) {
if ($transaction->getNewValue() == $user->getPHID()) {
$user_owns_task = true;
}
} else if ($task->getOwnerPHID() == $user->getPHID()) {
$user_owns_task = true;
}
}
if (!$user_owns_task) {
// If we aren't making the user the new task owner and they aren't the
// existing task owner, add them to CC unless they're aleady CC'd.
if (!in_array($user->getPHID(), $task->getCCPHIDs())) {
$added_ccs[] = $user->getPHID();
}
}
// Evade no-effect detection in the new editor stuff until we can switch
// to subscriptions.
$added_ccs = array_filter(array_diff($added_ccs, $task->getCCPHIDs()));
if ($added_ccs) {
// We've added CCs, so include a CC transaction.
$all_ccs = array_merge($task->getCCPHIDs(), $added_ccs);
$cc_transaction->setNewValue($all_ccs);
$transactions[] = $cc_transaction;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'new' => false,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$transactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditorPro())
->setActor($user)
->setContentSourceFromRequest($request)
->setContinueOnMissingFields(true)
->applyTransactions($task, $transactions);
$draft = id(new PhabricatorDraft())->loadOneWhere(
'authorPHID = %s AND draftKey = %s',
$user->getPHID(),
$task->getPHID());
if ($draft) {
$draft->delete();
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => false,
'transactions' => $transactions,
));
$event->setUser($user);
$event->setAphrontRequest($request);
PhutilEventEngine::dispatchEvent($event);
return id(new AphrontRedirectResponse())
->setURI('/T'.$task->getID());
}
}
diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
index 0051aaaf68..6f69864562 100644
--- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php
@@ -1,38 +1,41 @@
setMailReceiver($task);
return $handler_object;
}
public static function getNextSubpriority($pri, $sub) {
+ // TODO: T603 Figure out what the policies here should be once this gets
+ // cleaned up.
+
if ($sub === null) {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d ORDER BY subpriority ASC LIMIT 1',
$pri);
if ($next) {
return $next->getSubpriority() - ((double)(2 << 16));
}
} else {
$next = id(new ManiphestTask())->loadOneWhere(
'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1',
$pri,
$sub);
if ($next) {
return ($sub + $next->getSubpriority()) / 2;
}
}
return (double)(2 << 32);
}
}
diff --git a/src/applications/maniphest/event/ManiphestEdgeEventListener.php b/src/applications/maniphest/event/ManiphestEdgeEventListener.php
index adf87f2e46..be9fc0115d 100644
--- a/src/applications/maniphest/event/ManiphestEdgeEventListener.php
+++ b/src/applications/maniphest/event/ManiphestEdgeEventListener.php
@@ -1,136 +1,139 @@
listen(PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES);
$this->listen(PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES);
}
public function handleEvent(PhutilEvent $event) {
switch ($event->getType()) {
case PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES:
return $this->handleWillEditEvent($event);
case PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES:
return $this->handleDidEditEvent($event);
}
}
private function handleWillEditEvent(PhutilEvent $event) {
// NOTE: Everything is namespaced by `id` so that we aren't left in an
// inconsistent state if an edit fails to complete (e.g., something throws)
// or an edit happens inside another edit.
$id = $event->getValue('id');
$edges = $this->loadAllEdges($event);
$tasks = array();
if ($edges) {
+ // TODO: T603 This should probably all get nuked. Until then, this isn't
+ // realllllly a policy issue since callers are (or should be) doing
+ // policy checks anyway.
$tasks = id(new ManiphestTask())->loadAllWhere(
'phid IN (%Ls)',
array_keys($edges));
$tasks = mpull($tasks, null, 'getPHID');
}
$this->edges[$id] = $edges;
$this->tasks[$id] = $tasks;
}
private function handleDidEditEvent(PhutilEvent $event) {
$id = $event->getValue('id');
$old_edges = $this->edges[$id];
$tasks = $this->tasks[$id];
unset($this->edges[$id]);
unset($this->tasks[$id]);
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_LEGACY,
array());
$new_edges = $this->loadAllEdges($event);
$editor = id(new ManiphestTransactionEditorPro())
->setActor($event->getUser())
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
foreach ($tasks as $phid => $task) {
$xactions = array();
$old = $old_edges[$phid];
$new = $new_edges[$phid];
$types = array_keys($old + $new);
foreach ($types as $type) {
$old_type = idx($old, $type, array());
$new_type = idx($new, $type, array());
if ($old_type === $new_type) {
continue;
}
$xactions[] = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_EDGE)
->setOldValue($old_type)
->setNewValue($new_type)
->setMetadataValue('edge:type', $type);
}
if ($xactions) {
$editor->applyTransactions($task, $xactions);
}
}
}
private function filterEdgesBySourceType(array $edges, $type) {
foreach ($edges as $key => $edge) {
if ($edge['src_type'] !== $type) {
unset($edges[$key]);
}
}
return $edges;
}
private function loadAllEdges(PhutilEvent $event) {
$add_edges = $event->getValue('add');
$rem_edges = $event->getValue('rem');
$type_task = ManiphestPHIDTypeTask::TYPECONST;
$all_edges = array_merge($add_edges, $rem_edges);
$all_edges = $this->filterEdgesBySourceType($all_edges, $type_task);
if (!$all_edges) {
return;
}
$all_tasks = array();
$all_types = array();
foreach ($all_edges as $edge) {
$all_tasks[$edge['src']] = true;
$all_types[$edge['type']] = true;
}
$all_tasks = array_keys($all_tasks);
$all_types = array_keys($all_types);
return id(new PhabricatorEdgeQuery())
->withSourcePHIDs($all_tasks)
->withEdgeTypes($all_types)
->needEdgeData(true)
->execute();
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php
index a9a93e789b..6f4d58c81e 100644
--- a/src/applications/search/controller/PhabricatorSearchAttachController.php
+++ b/src/applications/search/controller/PhabricatorSearchAttachController.php
@@ -1,336 +1,337 @@
phid = $data['phid'];
$this->type = $data['type'];
$this->action = idx($data, 'action', self::ACTION_ATTACH);
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$handle = id(New PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs(array($this->phid))
->executeOne();
$object_type = $handle->getType();
$attach_type = $this->type;
$object = id(new PhabricatorObjectQuery())
->setViewer($user)
->withPHIDs(array($this->phid))
->executeOne();
if (!$object) {
return new Aphront404Response();
}
$edge_type = null;
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_DEPENDENCIES:
case self::ACTION_ATTACH:
$edge_type = $this->getEdgeType($object_type, $attach_type);
break;
}
if ($request->isFormPost()) {
$phids = explode(';', $request->getStr('phids'));
$phids = array_filter($phids);
$phids = array_values($phids);
if ($edge_type) {
$do_txn = $object instanceof PhabricatorApplicationTransactionInterface;
$old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
$add_phids = $phids;
$rem_phids = array_diff($old_phids, $add_phids);
if ($do_txn) {
$txn_editor = $object->getApplicationTransactionEditor()
->setActor($user)
->setContentSourceFromRequest($request);
$txn_template = $object->getApplicationTransactionObject()
->setTransactionType(PhabricatorTransactions::TYPE_EDGE)
->setMetadataValue('edge:type', $edge_type)
->setNewValue(array(
'+' => array_fuse($add_phids),
'-' => array_fuse($rem_phids)));
$txn_editor->applyTransactions($object, array($txn_template));
} else {
$editor = id(new PhabricatorEdgeEditor());
$editor->setActor($user);
foreach ($add_phids as $phid) {
$editor->addEdge($this->phid, $edge_type, $phid);
}
foreach ($rem_phids as $phid) {
$editor->removeEdge($this->phid, $edge_type, $phid);
}
try {
$editor->save();
} catch (PhabricatorEdgeCycleException $ex) {
$this->raiseGraphCycleException($ex);
}
}
return id(new AphrontReloadResponse())->setURI($handle->getURI());
} else {
return $this->performMerge($object, $handle, $phids);
}
} else {
if ($edge_type) {
$phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$this->phid,
$edge_type);
} else {
// This is a merge.
$phids = array();
}
}
$strings = $this->getStrings();
$handles = $this->loadViewerHandles($phids);
$obj_dialog = new PhabricatorObjectSelectorDialog();
$obj_dialog
->setUser($user)
->setHandles($handles)
->setFilters($this->getFilters($strings))
->setSelectedFilter($strings['selected'])
->setExcluded($this->phid)
->setCancelURI($handle->getURI())
->setSearchURI('/search/select/'.$attach_type.'/')
->setTitle($strings['title'])
->setHeader($strings['header'])
->setButtonText($strings['button'])
->setInstructions($strings['instructions']);
$dialog = $obj_dialog->buildDialog();
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function performMerge(
ManiphestTask $task,
PhabricatorObjectHandle $handle,
array $phids) {
$user = $this->getRequest()->getUser();
$response = id(new AphrontReloadResponse())->setURI($handle->getURI());
$phids = array_fill_keys($phids, true);
unset($phids[$task->getPHID()]); // Prevent merging a task into itself.
if (!$phids) {
return $response;
}
- $targets = id(new ManiphestTask())->loadAllWhere(
- 'phid in (%Ls) ORDER BY id ASC',
- array_keys($phids));
+ $targets = id(new ManiphestTaskQuery())
+ ->setViewer($user)
+ ->withPHIDs(array_keys($phids))
+ ->execute();
if (empty($targets)) {
return $response;
}
$editor = id(new ManiphestTransactionEditorPro())
->setActor($user)
->setContentSourceFromRequest($this->getRequest())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$task_names = array();
$merge_into_name = 'T'.$task->getID();
$cc_vector = array();
$cc_vector[] = $task->getCCPHIDs();
foreach ($targets as $target) {
$cc_vector[] = $target->getCCPHIDs();
$cc_vector[] = array(
$target->getAuthorPHID(),
$target->getOwnerPHID());
$close_task = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_STATUS)
->setNewValue(ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE);
$merge_comment = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent("\xE2\x9C\x98 Merged into {$merge_into_name}."));
$editor->applyTransactions(
$target,
array(
$close_task,
$merge_comment,
));
$task_names[] = 'T'.$target->getID();
}
$all_ccs = array_mergev($cc_vector);
$all_ccs = array_filter($all_ccs);
$all_ccs = array_unique($all_ccs);
$task_names = implode(', ', $task_names);
$add_ccs = id(new ManiphestTransaction())
->setTransactionType(ManiphestTransaction::TYPE_CCS)
->setNewValue($all_ccs);
$merged_comment = id(new ManiphestTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new ManiphestTransactionComment())
->setContent("\xE2\x97\x80 Merged tasks: {$task_names}."));
$editor->applyTransactions($task, array($add_ccs, $merged_comment));
return $response;
}
private function getStrings() {
switch ($this->type) {
case DifferentialPHIDTypeRevision::TYPECONST:
$noun = 'Revisions';
$selected = 'created';
break;
case ManiphestPHIDTypeTask::TYPECONST:
$noun = 'Tasks';
$selected = 'assigned';
break;
case PhabricatorRepositoryPHIDTypeCommit::TYPECONST:
$noun = 'Commits';
$selected = 'created';
break;
case PholioPHIDTypeMock::TYPECONST:
$noun = 'Mocks';
$selected = 'created';
break;
}
switch ($this->action) {
case self::ACTION_EDGE:
case self::ACTION_ATTACH:
$dialog_title = "Manage Attached {$noun}";
$header_text = "Currently Attached {$noun}";
$button_text = "Save {$noun}";
$instructions = null;
break;
case self::ACTION_MERGE:
$dialog_title = "Merge Duplicate Tasks";
$header_text = "Tasks To Merge";
$button_text = "Merge {$noun}";
$instructions =
"These tasks will be merged into the current task and then closed. ".
"The current task will grow stronger.";
break;
case self::ACTION_DEPENDENCIES:
$dialog_title = "Edit Dependencies";
$header_text = "Current Dependencies";
$button_text = "Save Dependencies";
$instructions = null;
break;
}
return array(
'target_plural_noun' => $noun,
'selected' => $selected,
'title' => $dialog_title,
'header' => $header_text,
'button' => $button_text,
'instructions' => $instructions,
);
}
private function getFilters(array $strings) {
if ($this->type == PholioPHIDTypeMock::TYPECONST) {
$filters = array(
'created' => 'Created By Me',
'all' => 'All '.$strings['target_plural_noun'],
);
} else {
$filters = array(
'assigned' => 'Assigned to Me',
'created' => 'Created By Me',
'open' => 'All Open '.$strings['target_plural_noun'],
'all' => 'All '.$strings['target_plural_noun'],
);
}
return $filters;
}
private function getEdgeType($src_type, $dst_type) {
$t_cmit = PhabricatorRepositoryPHIDTypeCommit::TYPECONST;
$t_task = ManiphestPHIDTypeTask::TYPECONST;
$t_drev = DifferentialPHIDTypeRevision::TYPECONST;
$t_mock = PholioPHIDTypeMock::TYPECONST;
$map = array(
$t_cmit => array(
$t_task => PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK,
),
$t_task => array(
$t_cmit => PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT,
$t_task => PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK,
$t_drev => PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV,
$t_mock => PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK,
),
$t_drev => array(
$t_drev => PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV,
$t_task => PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK,
),
$t_mock => array(
$t_task => PhabricatorEdgeConfig::TYPE_MOCK_HAS_TASK,
),
);
if (empty($map[$src_type][$dst_type])) {
return null;
}
return $map[$src_type][$dst_type];
}
private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) {
$cycle = $ex->getCycle();
$handles = $this->loadViewerHandles($cycle);
$names = array();
foreach ($cycle as $cycle_phid) {
$names[] = $handles[$cycle_phid]->getFullName();
}
$names = implode(" \xE2\x86\x92 ", $names);
throw new Exception(
"You can not create that dependency, because it would create a ".
"circular dependency: {$names}.");
}
}
diff --git a/src/applications/search/controller/PhabricatorSearchSelectController.php b/src/applications/search/controller/PhabricatorSearchSelectController.php
index 3477614bd5..cab9a98675 100644
--- a/src/applications/search/controller/PhabricatorSearchSelectController.php
+++ b/src/applications/search/controller/PhabricatorSearchSelectController.php
@@ -1,115 +1,117 @@
type = $data['type'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$query = new PhabricatorSearchQuery();
$query_str = $request->getStr('query');
$query->setQuery($query_str);
$query->setParameter('type', $this->type);
switch ($request->getStr('filter')) {
case 'assigned':
$query->setParameter('owner', array($user->getPHID()));
$query->setParameter('open', 1);
break;
case 'created';
$query->setParameter('author', array($user->getPHID()));
// TODO - if / when we allow pholio mocks to be archived, etc
// update this
if ($this->type != PholioPHIDTypeMock::TYPECONST) {
$query->setParameter('open', 1);
}
break;
case 'open':
$query->setParameter('open', 1);
break;
}
$query->setParameter('exclude', $request->getStr('exclude'));
$query->setParameter('limit', 100);
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$results = $engine->executeSearch($query);
$phids = array_fill_keys($results, true);
$phids += $this->queryObjectNames($query_str);
$phids = array_keys($phids);
$handles = $this->loadViewerHandles($phids);
$data = array();
foreach ($handles as $handle) {
$view = new PhabricatorHandleObjectSelectorDataView($handle);
$data[] = $view->renderData();
}
return id(new AphrontAjaxResponse())->setContent($data);
}
private function queryObjectNames($query) {
$pattern = null;
switch ($this->type) {
case ManiphestPHIDTypeTask::TYPECONST:
$pattern = '/\bT(\d+)\b/i';
break;
case DifferentialPHIDTypeRevision::TYPECONST:
$pattern = '/\bD(\d+)\b/i';
break;
case PholioPHIDTypeMock::TYPECONST:
$pattern = '/\bM(\d+)\b/i';
break;
}
if (!$pattern) {
return array();
}
$matches = array();
preg_match_all($pattern, $query, $matches);
if (!$matches) {
return array();
}
$object_ids = $matches[1];
if (!$object_ids) {
return array();
}
switch ($this->type) {
case DifferentialPHIDTypeRevision::TYPECONST:
$objects = id(new DifferentialRevision())->loadAllWhere(
'id IN (%Ld)',
$object_ids);
break;
case ManiphestPHIDTypeTask::TYPECONST:
+ // TODO: (T603) Clean this up. This should probably all run through
+ // ObjectQuery?
$objects = id(new ManiphestTask())->loadAllWhere(
'id IN (%Ld)',
$object_ids);
break;
case PholioPHIDTypeMock::TYPECONST:
$objects = id(new PholioMock())->loadAllWhere(
'id IN (%Ld)',
$object_ids);
break;
}
return array_fill_keys(mpull($objects, 'getPHID'), true);
}
}
diff --git a/src/infrastructure/PhabricatorEditor.php b/src/infrastructure/PhabricatorEditor.php
index 5c0508a2e3..745ce3c156 100644
--- a/src/infrastructure/PhabricatorEditor.php
+++ b/src/infrastructure/PhabricatorEditor.php
@@ -1,34 +1,34 @@
actor = $actor;
return $this;
}
- final protected function getActor() {
+ final public function getActor() {
return $this->actor;
}
- final protected function requireActor() {
+ final public function requireActor() {
$actor = $this->getActor();
if (!$actor) {
throw new Exception('You must setActor()!');
}
return $actor;
}
final public function setExcludeMailRecipientPHIDs($phids) {
$this->excludeMailRecipientPHIDs = $phids;
return $this;
}
final protected function getExcludeMailRecipientPHIDs() {
return $this->excludeMailRecipientPHIDs;
}
}