diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0dadd63d7448cd4dbba193405741e0e816813966..6436dbaf1ef78f8b12633860b1a1c742680850f7 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -45,10 +45,13 @@ phutil_register_library_map(array( 'SprintHistoryController' => 'controller/SprintHistoryController.php', 'SprintHistoryDataProvider' => 'storage/SprintHistoryDataProvider.php', 'SprintHistoryTableView' => 'view/reports/SprintHistoryTableView.php', + 'SprintInfoConduitAPIMethod' => 'conduit/SprintInfoConduitAPIMethod.php', 'SprintIsSprintField' => 'customfield/SprintIsSprintField.php', 'SprintListController' => 'controller/SprintListController.php', 'SprintListDataProvider' => 'storage/SprintListDataProvider.php', 'SprintListTableView' => 'view/SprintListTableView.php', + 'SprintManiphestConduitAPIMethod' => 'conduit/SprintManiphestConduitAPIMethod.php', + 'SprintManiphestQueryConduitAPIMethod' => 'conduit/SprintManiphestQueryConduitAPIMethod.php', 'SprintPoints' => 'util/SprintPoints.php', 'SprintProjectController' => 'controller/SprintProjectController.php', 'SprintProjectCustomField' => 'customfield/SprintProjectCustomField.php', @@ -107,9 +110,12 @@ phutil_register_library_map(array( 'SprintHandleIconView' => 'AphrontTagView', 'SprintHistoryController' => 'SprintController', 'SprintHistoryTableView' => 'SprintView', + 'SprintInfoConduitAPIMethod' => 'SprintManiphestConduitAPIMethod', 'SprintIsSprintField' => 'SprintProjectCustomField', 'SprintListController' => 'SprintController', 'SprintListTableView' => 'Phobject', + 'SprintManiphestConduitAPIMethod' => 'ConduitAPIMethod', + 'SprintManiphestQueryConduitAPIMethod' => 'SprintManiphestConduitAPIMethod', 'SprintPoints' => 'Phobject', 'SprintProjectController' => 'SprintController', 'SprintProjectCustomField' => array( diff --git a/src/conduit/SprintInfoConduitAPIMethod.php b/src/conduit/SprintInfoConduitAPIMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..3bc7cbffdd28887722f8c12530657e91f96b64c8 --- /dev/null +++ b/src/conduit/SprintInfoConduitAPIMethod.php @@ -0,0 +1,45 @@ +<?php + +final class SprintInfoConduitAPIMethod extends SprintManiphestConduitAPIMethod { + + public function getAPIMethodName() { + return 'sprint.info'; + } + + public function getMethodDescription() { + return pht('Retrieve points and info for a Maniphest task, given its ID.'); + } + + protected function defineParamTypes() { + return array( + 'task_id' => 'required id', + ); + } + + protected function defineReturnType() { + return 'nonempty dict'; + } + + protected function defineErrorTypes() { + return array( + 'ERR_BAD_TASK' => pht('No such Maniphest task exists.'), + ); + } + + protected function execute(ConduitAPIRequest $request) { + $task_id = $request->getValue('task_id'); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($request->getUser()) + ->withIDs(array($task_id)) + ->needSubscriberPHIDs(true) + ->needProjectPHIDs(true) + ->executeOne(); + if (!$task) { + throw new ConduitException('ERR_BAD_TASK'); + } + + return $this->buildTaskInfoDictionary($task); + } + +} diff --git a/src/conduit/SprintManiphestConduitAPIMethod.php b/src/conduit/SprintManiphestConduitAPIMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..cbde1d8c926d7aed072f2aafc1147c15109eef56 --- /dev/null +++ b/src/conduit/SprintManiphestConduitAPIMethod.php @@ -0,0 +1,302 @@ +<?php + +abstract class SprintManiphestConduitAPIMethod extends ConduitAPIMethod { + + final public function getApplication() { + return PhabricatorApplication::getByClass( + 'PhabricatorManiphestApplication'); + } + + protected function defineErrorTypes() { + return array( + 'ERR-INVALID-PARAMETER' => pht('Missing or malformed parameter.'), + ); + } + + protected function buildTaskInfoDictionary(ManiphestTask $task) { + $results = $this->buildTaskInfoDictionaries(array($task)); + return idx($results, $task->getPHID()); + } + + protected function getTaskFields($is_new) { + $fields = array(); + + if (!$is_new) { + $fields += array( + 'id' => 'optional int', + 'phid' => 'optional int', + ); + } + + $fields += array( + 'title' => $is_new ? 'required string' : 'optional string', + 'description' => 'optional string', + 'ownerPHID' => 'optional phid', + 'viewPolicy' => 'optional phid or policy string', + 'editPolicy' => 'optional phid or policy string', + 'ccPHIDs' => 'optional list<phid>', + 'priority' => 'optional int', + 'projectPHIDs' => 'optional list<phid>', + 'auxiliary' => 'optional dict', + 'points' => 'optional int' + ); + + if (!$is_new) { + $fields += array( + 'status' => 'optional string', + 'comments' => 'optional string', + ); + } + + return $fields; + } + + protected function applyRequest( + ManiphestTask $task, + ConduitAPIRequest $request, + $is_new) { + + $changes = array(); + + if ($is_new) { + $task->setTitle((string)$request->getValue('title')); + $task->setDescription((string)$request->getValue('description')); + $changes[ManiphestTransaction::TYPE_STATUS] = + ManiphestTaskStatus::getDefaultStatus(); + $changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] = + array('+' => array($request->getUser()->getPHID())); + } else { + + $comments = $request->getValue('comments'); + if (!$is_new && $comments !== null) { + $changes[PhabricatorTransactions::TYPE_COMMENT] = null; + } + + $title = $request->getValue('title'); + if ($title !== null) { + $changes[ManiphestTransaction::TYPE_TITLE] = $title; + } + + $desc = $request->getValue('description'); + if ($desc !== null) { + $changes[ManiphestTransaction::TYPE_DESCRIPTION] = $desc; + } + + $status = $request->getValue('status'); + if ($status !== null) { + $valid_statuses = ManiphestTaskStatus::getTaskStatusMap(); + if (!isset($valid_statuses[$status])) { + throw id(new ConduitException('ERR-INVALID-PARAMETER')) + ->setErrorDescription(pht('Status set to invalid value.')); + } + $changes[ManiphestTransaction::TYPE_STATUS] = $status; + } + } + + $priority = $request->getValue('priority'); + if ($priority !== null) { + $valid_priorities = ManiphestTaskPriority::getTaskPriorityMap(); + if (!isset($valid_priorities[$priority])) { + throw id(new ConduitException('ERR-INVALID-PARAMETER')) + ->setErrorDescription(pht('Priority set to invalid value.')); + } + $changes[ManiphestTransaction::TYPE_PRIORITY] = $priority; + } + + $owner_phid = $request->getValue('ownerPHID'); + if ($owner_phid !== null) { + $this->validatePHIDList( + array($owner_phid), + PhabricatorPeopleUserPHIDType::TYPECONST, + 'ownerPHID'); + $changes[ManiphestTransaction::TYPE_OWNER] = $owner_phid; + } + + $ccs = $request->getValue('ccPHIDs'); + if ($ccs !== null) { + $changes[PhabricatorTransactions::TYPE_SUBSCRIBERS] = + array('=' => array_fuse($ccs)); + } + + $transactions = array(); + + $view_policy = $request->getValue('viewPolicy'); + if ($view_policy !== null) { + $transactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) + ->setNewValue($view_policy); + } + + $edit_policy = $request->getValue('editPolicy'); + if ($edit_policy !== null) { + $transactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($edit_policy); + } + + $project_phids = $request->getValue('projectPHIDs'); + if ($project_phids !== null) { + $this->validatePHIDList( + $project_phids, + PhabricatorProjectProjectPHIDType::TYPECONST, + 'projectPHIDS'); + + $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; + $transactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $project_type) + ->setNewValue( + array( + '=' => array_fuse($project_phids), + )); + } + + $template = new ManiphestTransaction(); + + foreach ($changes as $type => $value) { + $transaction = clone $template; + $transaction->setTransactionType($type); + if ($type == PhabricatorTransactions::TYPE_COMMENT) { + $transaction->attachComment( + id(new ManiphestTransactionComment()) + ->setContent($comments)); + } else { + $transaction->setNewValue($value); + } + + $transactions[] = $transaction; + } + + $field_list = PhabricatorCustomField::getObjectFields( + $task, + PhabricatorCustomField::ROLE_EDIT); + $field_list->readFieldsFromStorage($task); + + $auxiliary = $request->getValue('auxiliary'); + if ($auxiliary) { + foreach ($field_list->getFields() as $key => $field) { + if (!array_key_exists($key, $auxiliary)) { + continue; + } + $transaction = clone $template; + $transaction->setTransactionType( + PhabricatorTransactions::TYPE_CUSTOMFIELD); + $transaction->setMetadataValue('customfield:key', $key); + $transaction->setOldValue( + $field->getOldValueForApplicationTransactions()); + $transaction->setNewValue($auxiliary[$key]); + $transactions[] = $transaction; + } + } + + if (!$transactions) { + return; + } + + $content_source = PhabricatorContentSource::newForSource( + PhabricatorContentSource::SOURCE_CONDUIT, + array()); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($request->getUser()) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true); + + if (!$is_new) { + $editor->setContinueOnMissingFields(true); + } + + $editor->applyTransactions($task, $transactions); + + // reload the task now that we've done all the fun stuff + return id(new ManiphestTaskQuery()) + ->setViewer($request->getUser()) + ->withPHIDs(array($task->getPHID())) + ->needSubscriberPHIDs(true) + ->needProjectPHIDs(true) + ->executeOne(); + } + + protected function buildTaskInfoDictionaries(array $tasks) { + assert_instances_of($tasks, 'ManiphestTask'); + if (!$tasks) { + return array(); + } + + $task_phids = mpull($tasks, 'getPHID'); + + $all_deps = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($task_phids) + ->withEdgeTypes(array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST)); + $all_deps->execute(); + + $result = array(); + foreach ($tasks as $task) { + // TODO: Batch this get as CustomField gets cleaned up. + $field_list = PhabricatorCustomField::getObjectFields( + $task, + PhabricatorCustomField::ROLE_EDIT); + $field_list->readFieldsFromStorage($task); + + $auxiliary = mpull( + $field_list->getFields(), + 'getValueForStorage', + 'getFieldKey'); + + $task_deps = $all_deps->getDestinationPHIDs( + array($task->getPHID()), + array(ManiphestTaskDependsOnTaskEdgeType::EDGECONST)); + + $result[$task->getPHID()] = array( + 'id' => $task->getID(), + 'phid' => $task->getPHID(), + 'points' => $task->getPoints(), + 'authorPHID' => $task->getAuthorPHID(), + 'ownerPHID' => $task->getOwnerPHID(), + 'ccPHIDs' => $task->getSubscriberPHIDs(), + 'status' => $task->getStatus(), + 'statusName' => ManiphestTaskStatus::getTaskStatusName( + $task->getStatus()), + 'isClosed' => $task->isClosed(), + 'priority' => ManiphestTaskPriority::getTaskPriorityName( + $task->getPriority()), + 'priorityColor' => ManiphestTaskPriority::getTaskPriorityColor( + $task->getPriority()), + 'title' => $task->getTitle(), + 'description' => $task->getDescription(), + 'projectPHIDs' => $task->getProjectPHIDs(), + 'uri' => PhabricatorEnv::getProductionURI('/T'.$task->getID()), + 'auxiliary' => $auxiliary, + 'objectName' => 'T'.$task->getID(), + 'dateCreated' => $task->getDateCreated(), + 'dateModified' => $task->getDateModified(), + 'dependsOnTaskPHIDs' => $task_deps, + ); + } + + return $result; + } + + /** + * NOTE: This is a temporary stop gap since its easy to make malformed tasks. + * Long-term, the values set in @{method:defineParamTypes} will be used to + * validate data implicitly within the larger Conduit application. + * + * TODO: Remove this in favor of generalized Conduit hotness. + */ + private function validatePHIDList(array $phid_list, $phid_type, $field) { + $phid_groups = phid_group_by_type($phid_list); + unset($phid_groups[$phid_type]); + if (!empty($phid_groups)) { + throw id(new ConduitException('ERR-INVALID-PARAMETER')) + ->setErrorDescription( + pht( + 'One or more PHIDs were invalid for %s.', + $field)); + } + + return true; + } + +} + diff --git a/src/conduit/SprintManiphestQueryConduitAPIMethod.php b/src/conduit/SprintManiphestQueryConduitAPIMethod.php new file mode 100644 index 0000000000000000000000000000000000000000..c5c737dd2551b286be4311a00e40782f3370f29f --- /dev/null +++ b/src/conduit/SprintManiphestQueryConduitAPIMethod.php @@ -0,0 +1,122 @@ +<?php + +final class SprintManiphestQueryConduitAPIMethod extends SprintManiphestConduitAPIMethod { + + public function getAPIMethodName() { + return 'sprint.query'; + } + + public function getMethodDescription() { + return pht('Execute complex searches for Maniphest tasks.'); + } + + protected function defineParamTypes() { + $statuses = array( + ManiphestTaskQuery::STATUS_ANY, + ManiphestTaskQuery::STATUS_OPEN, + ManiphestTaskQuery::STATUS_CLOSED, + ManiphestTaskQuery::STATUS_RESOLVED, + ManiphestTaskQuery::STATUS_WONTFIX, + ManiphestTaskQuery::STATUS_INVALID, + ManiphestTaskQuery::STATUS_SPITE, + ManiphestTaskQuery::STATUS_DUPLICATE, + ); + $status_const = $this->formatStringConstants($statuses); + + $orders = array( + ManiphestTaskQuery::ORDER_PRIORITY, + ManiphestTaskQuery::ORDER_CREATED, + ManiphestTaskQuery::ORDER_MODIFIED, + ); + $order_const = $this->formatStringConstants($orders); + + return array( + 'ids' => 'optional list<uint>', + 'phids' => 'optional list<phid>', + 'ownerPHIDs' => 'optional list<phid>', + 'authorPHIDs' => 'optional list<phid>', + 'projectPHIDs' => 'optional list<phid>', + 'ccPHIDs' => 'optional list<phid>', + 'fullText' => 'optional string', + + 'status' => 'optional '.$status_const, + 'order' => 'optional '.$order_const, + + 'limit' => 'optional int', + 'offset' => 'optional int', + ); + } + + protected function defineReturnType() { + return 'list'; + } + + protected function execute(ConduitAPIRequest $request) { + $query = id(new ManiphestTaskQuery()) + ->setViewer($request->getUser()) + ->needProjectPHIDs(true) + ->needSubscriberPHIDs(true); + + $task_ids = $request->getValue('ids'); + if ($task_ids) { + $query->withIDs($task_ids); + } + + $task_phids = $request->getValue('phids'); + if ($task_phids) { + $query->withPHIDs($task_phids); + } + + $owners = $request->getValue('ownerPHIDs'); + if ($owners) { + $query->withOwners($owners); + } + + $authors = $request->getValue('authorPHIDs'); + if ($authors) { + $query->withAuthors($authors); + } + + $projects = $request->getValue('projectPHIDs'); + if ($projects) { + $query->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_AND, + $projects); + } + + $ccs = $request->getValue('ccPHIDs'); + if ($ccs) { + $query->withSubscribers($ccs); + } + + $full_text = $request->getValue('fullText'); + if ($full_text) { + $query->withFullTextSearch($full_text); + } + + $status = $request->getValue('status'); + if ($status) { + $query->withStatus($status); + } + + $order = $request->getValue('order'); + if ($order) { + $query->setOrder($order); + } + + $limit = $request->getValue('limit'); + if ($limit) { + $query->setLimit($limit); + } + + $offset = $request->getValue('offset'); + if ($offset) { + $query->setOffset($offset); + } + + $results = $query->execute(); + return $this->buildTaskInfoDictionaries($results); + } + +}