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( '
%s
', 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; } }