diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1530,6 +1530,7 @@ 'PasteCreateMailReceiver' => 'applications/paste/mail/PasteCreateMailReceiver.php', 'PasteDefaultEditCapability' => 'applications/paste/capability/PasteDefaultEditCapability.php', 'PasteDefaultViewCapability' => 'applications/paste/capability/PasteDefaultViewCapability.php', + 'PasteEditConduitAPIMethod' => 'applications/paste/conduit/PasteEditConduitAPIMethod.php', 'PasteEmbedView' => 'applications/paste/view/PasteEmbedView.php', 'PasteInfoConduitAPIMethod' => 'applications/paste/conduit/PasteInfoConduitAPIMethod.php', 'PasteMailReceiver' => 'applications/paste/mail/PasteMailReceiver.php', @@ -1571,6 +1572,7 @@ 'PhabricatorApplicationDetailViewController' => 'applications/meta/controller/PhabricatorApplicationDetailViewController.php', 'PhabricatorApplicationEditController' => 'applications/meta/controller/PhabricatorApplicationEditController.php', 'PhabricatorApplicationEditEngine' => 'applications/transactions/editengine/PhabricatorApplicationEditEngine.php', + 'PhabricatorApplicationEditEngineAPIMethod' => 'applications/transactions/editengine/PhabricatorApplicationEditEngineAPIMethod.php', 'PhabricatorApplicationEditHTTPParameterHelpView' => 'applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php', 'PhabricatorApplicationEmailCommandsController' => 'applications/meta/controller/PhabricatorApplicationEmailCommandsController.php', 'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php', @@ -2094,6 +2096,7 @@ 'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php', 'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php', 'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php', + 'PhabricatorEdgeEditType' => 'applications/transactions/edittype/PhabricatorEdgeEditType.php', 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php', 'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php', 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php', @@ -2101,6 +2104,7 @@ 'PhabricatorEdgeType' => 'infrastructure/edges/type/PhabricatorEdgeType.php', 'PhabricatorEdgeTypeTestCase' => 'infrastructure/edges/type/__tests__/PhabricatorEdgeTypeTestCase.php', 'PhabricatorEditField' => 'applications/transactions/editfield/PhabricatorEditField.php', + 'PhabricatorEditType' => 'applications/transactions/edittype/PhabricatorEditType.php', 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', 'PhabricatorElasticSearchEngine' => 'applications/search/engine/PhabricatorElasticSearchEngine.php', 'PhabricatorElasticSearchSetupCheck' => 'applications/config/check/PhabricatorElasticSearchSetupCheck.php', @@ -2936,6 +2940,7 @@ 'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php', 'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php', 'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php', + 'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php', 'PhabricatorSite' => 'aphront/site/PhabricatorSite.php', 'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php', 'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php', @@ -5457,6 +5462,7 @@ 'PasteCreateMailReceiver' => 'PhabricatorMailReceiver', 'PasteDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PasteDefaultViewCapability' => 'PhabricatorPolicyCapability', + 'PasteEditConduitAPIMethod' => 'PhabricatorApplicationEditEngineAPIMethod', 'PasteEmbedView' => 'AphrontView', 'PasteInfoConduitAPIMethod' => 'PasteConduitAPIMethod', 'PasteMailReceiver' => 'PhabricatorObjectMailReceiver', @@ -5501,6 +5507,7 @@ 'PhabricatorApplicationDetailViewController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationEditController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationEditEngine' => 'Phobject', + 'PhabricatorApplicationEditEngineAPIMethod' => 'ConduitAPIMethod', 'PhabricatorApplicationEditHTTPParameterHelpView' => 'AphrontView', 'PhabricatorApplicationEmailCommandsController' => 'PhabricatorApplicationsController', 'PhabricatorApplicationLaunchView' => 'AphrontTagView', @@ -6121,6 +6128,7 @@ 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', 'PhabricatorEdgeConstants' => 'Phobject', 'PhabricatorEdgeCycleException' => 'Exception', + 'PhabricatorEdgeEditType' => 'PhabricatorEditType', 'PhabricatorEdgeEditor' => 'Phobject', 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', 'PhabricatorEdgeQuery' => 'PhabricatorQuery', @@ -6128,6 +6136,7 @@ 'PhabricatorEdgeType' => 'Phobject', 'PhabricatorEdgeTypeTestCase' => 'PhabricatorTestCase', 'PhabricatorEditField' => 'Phobject', + 'PhabricatorEditType' => 'Phobject', 'PhabricatorEditor' => 'Phobject', 'PhabricatorElasticSearchEngine' => 'PhabricatorSearchEngine', 'PhabricatorElasticSearchSetupCheck' => 'PhabricatorSetupCheck', @@ -7120,6 +7129,7 @@ 'PhabricatorSetupIssue' => 'Phobject', 'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', 'PhabricatorSetupIssueView' => 'AphrontView', + 'PhabricatorSimpleEditType' => 'PhabricatorEditType', 'PhabricatorSite' => 'AphrontSite', 'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', 'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO', diff --git a/src/applications/paste/conduit/PasteEditConduitAPIMethod.php b/src/applications/paste/conduit/PasteEditConduitAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/paste/conduit/PasteEditConduitAPIMethod.php @@ -0,0 +1,19 @@ +fileName = $name; } + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + switch ($type) { + case PhabricatorPasteTransaction::TYPE_CONTENT: + if (!$object->getFilePHID() && !$xactions) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('You must provide content to create a paste.'), + null); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + protected function getCustomTransactionOldValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -41,6 +41,7 @@ return id(new PhabricatorPaste()) ->setTitle('') + ->setLanguage('') ->setStatus(self::STATUS_ACTIVE) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($view_policy) diff --git a/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php b/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php --- a/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorApplicationEditEngine.php @@ -1,5 +1,10 @@ PhabricatorPolicyCapability::CAN_VIEW, 'label' => pht('View Policy'), 'description' => pht('Controls who can view the object.'), + 'edit' => 'view', ), PhabricatorTransactions::TYPE_EDIT_POLICY => array( 'key' => 'policy.edit', @@ -54,6 +60,7 @@ 'capability' => PhabricatorPolicyCapability::CAN_EDIT, 'label' => pht('Edit Policy'), 'description' => pht('Controls who can edit the object.'), + 'edit' => 'edit', ), PhabricatorTransactions::TYPE_JOIN_POLICY => array( 'key' => 'policy.join', @@ -61,6 +68,7 @@ 'capability' => PhabricatorPolicyCapability::CAN_JOIN, 'label' => pht('Join Policy'), 'description' => pht('Controls who can join the object.'), + 'edit' => 'join', ), ); @@ -74,6 +82,7 @@ $aliases = $spec['aliases']; $label = $spec['label']; $description = $spec['description']; + $edit = $spec['edit']; $policy_field = id(new PhabricatorPolicyEditField()) ->setKey($key) @@ -83,6 +92,7 @@ ->setCapability($capability) ->setPolicies($policies) ->setTransactionType($type) + ->setEditTypeKey($edit) ->setValue($object->getPolicy($capability)); $fields[] = $policy_field; @@ -93,6 +103,7 @@ $space_field = id(new PhabricatorSpaceEditField()) ->setKey('spacePHID') ->setLabel(pht('Space')) + ->setEditTypeKey('space') ->setDescription( pht('Shifts the object in the Spaces application.')) ->setAliases(array('space', 'policy.space')) @@ -126,6 +137,7 @@ $edge_field = id(new PhabricatorDatasourceEditField()) ->setKey('projectPHIDs') ->setLabel(pht('Projects')) + ->setEditTypeKey('projects') ->setDescription( pht( 'Add or remove associated projects.')) @@ -154,6 +166,7 @@ $subscribers_field = id(new PhabricatorDatasourceEditField()) ->setKey('subscriberPHIDs') ->setLabel(pht('Subscribers')) + ->setEditTypeKey('subscribers') ->setDescription(pht('Manage subscribers.')) ->setDatasource(new PhabricatorMetaMTAMailableDatasource()) ->setAliases(array('subscriber', 'subscribers')) @@ -240,11 +253,9 @@ if (!$object) { return new Aphront404Response(); } - $this->setIsCreate(false); } else { $object = $this->newEditableObject(); - $this->setIsCreate(true); } @@ -443,4 +454,171 @@ return $actions; } + +/* -( Conduit )------------------------------------------------------------ */ + + + /** + * Respond to a Conduit edit request. + * + * This method accepts a list of transactions to apply to an object, and + * either edits an existing object or creates a new one. + * + * @task conduit + */ + final public function buildConduitResponse(ConduitAPIRequest $request) { + $viewer = $this->getViewer(); + + $phid = $request->getValue('objectPHID'); + if ($phid) { + $object = $this->newObjectQuery() + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + throw new Exception(pht('No such object with PHID "%s".', $phid)); + } + $this->setIsCreate(false); + } else { + $object = $this->newEditableObject(); + $this->setIsCreate(true); + } + + $fields = $this->buildEditFields($object); + + foreach ($fields as $field) { + $field + ->setViewer($viewer) + ->setObject($object); + } + + $types = $this->getAllEditTypesFromFields($fields); + $template = $object->getApplicationTransactionTemplate(); + + $xactions = $this->getConduitTransactions($request, $types, $template); + + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromConduitRequest($request) + ->setContinueOnNoEffect(true); + + $xactions = $editor->applyTransactions($object, $xactions); + + $xactions_struct = array(); + foreach ($xactions as $xaction) { + $xactions_struct[] = array( + 'phid' => $xaction->getPHID(), + ); + } + + return array( + 'object' => array( + 'id' => $object->getID(), + 'phid' => $object->getPHID(), + ), + 'transactions' => $xactions_struct, + ); + } + + + /** + * Generate transactions which can be applied from edit actions in a Conduit + * request. + * + * @param ConduitAPIRequest The request. + * @param list Supported edit types. + * @param PhabricatorApplicationTransaction Template transaction. + * @return list Generated transactions. + * @task conduit + */ + private function getConduitTransactions( + ConduitAPIRequest $request, + array $types, + PhabricatorApplicationTransaction $template) { + + $transactions_key = 'transactions'; + + $xactions = $request->getValue($transactions_key); + if (!is_array($xactions)) { + throw new Exception( + pht( + 'Parameter "%s" is not a list of transactions.', + $transactions_key)); + } + + foreach ($xactions as $key => $xaction) { + if (!is_array($xaction)) { + throw new Exception( + pht( + 'Parameter "%s" must contain a list of transaction descriptions, '. + 'but item with key "%s" is not a dictionary.', + $transactions_key, + $key)); + } + + if (!array_key_exists('type', $xaction)) { + throw new Exception( + pht( + 'Parameter "%s" must contain a list of transaction descriptions, '. + 'but item with key "%s" is missing a "type" field. Each '. + 'transaction must have a type field.', + $transactions_key, + $key)); + } + + $type = $xaction['type']; + if (empty($types[$type])) { + throw new Exception( + pht( + 'Transaction with key "%s" has invalid type "%s". This type is '. + 'not recognized. Valid types are: %s.', + $key, + $type, + implode(', ', array_keys($types)))); + } + } + + $results = array(); + foreach ($xactions as $xaction) { + $type = $types[$xaction['type']]; + + $results[] = $type->generateTransaction( + clone $template, + $xaction); + } + + return $results; + } + + + /** + * @return map + * @task conduit + */ + private function getAllEditTypesFromFields(array $fields) { + $types = array(); + foreach ($fields as $field) { + $field_types = $field->getEditTransactionTypes(); + foreach ($field_types as $field_type) { + $field_type->setField($field); + $types[$field_type->getEditType()] = $field_type; + } + } + return $types; + } + + public function getAllEditTypes() { + $object = $this->newEditableObject(); + $fields = $this->buildEditFields($object); + return $this->getAllEditTypesFromFields($fields); + } + + + + } diff --git a/src/applications/transactions/editengine/PhabricatorApplicationEditEngineAPIMethod.php b/src/applications/transactions/editengine/PhabricatorApplicationEditEngineAPIMethod.php new file mode 100644 --- /dev/null +++ b/src/applications/transactions/editengine/PhabricatorApplicationEditEngineAPIMethod.php @@ -0,0 +1,188 @@ + 'list>', + 'objectPHID' => 'optional phid', + ); + } + + final protected function defineReturnType() { + return 'map'; + } + + final protected function execute(ConduitAPIRequest $request) { + $engine = $this->newEditEngine() + ->setViewer($request->getUser()); + + return $engine->buildConduitResponse($request); + } + + final public function getMethodDescription() { + // TODO: We don't currently have a real viewer in this method. + $viewer = new PhabricatorUser(); + + $engine = $this->newEditEngine() + ->setViewer($viewer); + + $types = $engine->getAllEditTypes(); + + $out = array(); + + $out[] = pht(<<getEditType(); + $edit_summary = $type->getSummary(); + $table[] = "| `{$edit_type}` | {$edit_summary} |"; + } + + $out[] = implode("\n", $table); + + foreach ($types as $type) { + $section = array(); + $section[] = pht('Edit Type: %s', $type->getEditType()); + $section[] = '---------'; + $section[] = null; + $section[] = $type->getDescription(); + $section[] = null; + $section[] = pht( + 'This edit generates transactions of type `%s` internally.', + $type->getTransactionType()); + $section[] = null; + + $type_description = pht( + 'Use `%s` to select this edit type.', + $type->getEditType()); + + $value_type = $type->getValueType(); + $value_description = $type->getValueDescription(); + + $table = array(); + $table[] = "| {$key} | {$head_type} | {$description} |"; + $table[] = '|--------|--------------|----------------|'; + $table[] = "| `type` | `const` | {$type_description} |"; + $table[] = "| `value` | `{$value_type}` | {$value_description} |"; + $section[] = implode("\n", $table); + + $out[] = implode("\n", $section); + } + + $out = implode("\n\n", $out); + return $out; + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php --- a/src/applications/transactions/editfield/PhabricatorEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEditField.php @@ -12,6 +12,8 @@ private $transactionType; private $metadata = array(); private $description; + private $editTypeKey; + public function setKey($key) { $this->key = $key; @@ -223,4 +225,75 @@ return 'string'; } + public function setEditTypeKey($edit_type_key) { + $this->editTypeKey = $edit_type_key; + return $this; + } + + public function getEditTypeKey() { + if ($this->editTypeKey === null) { + return $this->getKey(); + } + return $this->editTypeKey; + } + + public function getEditTransactionTypes() { + $transaction_type = $this->getTransactionType(); + $type_key = $this->getEditTypeKey(); + + // TODO: This is a pretty big pile of hard-coded hacks for now. + + $edge_types = array( + PhabricatorTransactions::TYPE_EDGE => array( + '+' => pht('Add projects.'), + '-' => pht('Remove projects.'), + '=' => pht('Set associated projects, overwriting current value.'), + ), + PhabricatorTransactions::TYPE_SUBSCRIBERS => array( + '+' => pht('Add subscribers.'), + '-' => pht('Remove subscribers.'), + '=' => pht('Set subscribers, overwriting current value.'), + ), + ); + + if (isset($edge_types[$transaction_type])) { + $base = id(new PhabricatorEdgeEditType()) + ->setTransactionType($transaction_type) + ->setMetadata($this->metadata); + + $strings = $edge_types[$transaction_type]; + + $add = id(clone $base) + ->setEditType($type_key.'.add') + ->setEdgeOperation('+') + ->setDescription($strings['+']) + ->setValueDescription(pht('List of PHIDs to add.')); + $rem = id(clone $base) + ->setEditType($type_key.'.remove') + ->setEdgeOperation('-') + ->setDescription($strings['-']) + ->setValueDescription(pht('List of PHIDs to remove.')); + $set = id(clone $base) + ->setEditType($type_key.'.set') + ->setEdgeOperation('=') + ->setDescription($strings['=']) + ->setValueDescription(pht('List of PHIDs to set.')); + + return array( + $add, + $rem, + $set, + ); + } + + return array( + id(new PhabricatorSimpleEditType()) + ->setEditType($type_key) + ->setTransactionType($transaction_type) + ->setValueType($this->getHTTPParameterType()) + ->setDescription($this->getDescription()) + ->setMetadata($this->metadata), + ); + } + } diff --git a/src/applications/transactions/edittype/PhabricatorEdgeEditType.php b/src/applications/transactions/edittype/PhabricatorEdgeEditType.php new file mode 100644 --- /dev/null +++ b/src/applications/transactions/edittype/PhabricatorEdgeEditType.php @@ -0,0 +1,51 @@ +edgeOperation = $edge_operation; + return $this; + } + + public function getEdgeOperation() { + return $this->edgeOperation; + } + + public function getValueType() { + return 'list'; + } + + public function generateTransaction( + PhabricatorApplicationTransaction $template, + array $spec) { + + $value = idx($spec, 'value'); + $value = array_fuse($value); + $value = array( + $this->getEdgeOperation() => $value, + ); + + $template + ->setTransactionType($this->getTransactionType()) + ->setNewValue($value); + + foreach ($this->getMetadata() as $key => $value) { + $template->setMetadataValue($key, $value); + } + + return $template; + } + + public function setValueDescription($value_description) { + $this->valueDescription = $value_description; + return $this; + } + + public function getValueDescription() { + return $this->valueDescription; + } + +} diff --git a/src/applications/transactions/edittype/PhabricatorEditType.php b/src/applications/transactions/edittype/PhabricatorEditType.php new file mode 100644 --- /dev/null +++ b/src/applications/transactions/edittype/PhabricatorEditType.php @@ -0,0 +1,76 @@ +description = $description; + return $this; + } + + public function getDescription() { + return $this->description; + } + + public function setSummary($summary) { + $this->summary = $summary; + return $this; + } + + public function getSummary() { + if ($this->summary === null) { + return $this->getDescription(); + } + return $this->summary; + } + + public function setField(PhabricatorEditField $field) { + $this->field = $field; + return $this; + } + + public function getField() { + return $this->field; + } + + public function setEditType($edit_type) { + $this->editType = $edit_type; + return $this; + } + + public function getEditType() { + return $this->editType; + } + + public function setMetadata($metadata) { + $this->metadata = $metadata; + return $this; + } + + public function getMetadata() { + return $this->metadata; + } + + public function setTransactionType($transaction_type) { + $this->transactionType = $transaction_type; + return $this; + } + + public function getTransactionType() { + return $this->transactionType; + } + + abstract public function generateTransaction( + PhabricatorApplicationTransaction $template, + array $spec); + + abstract public function getValueType(); + abstract public function getValueDescription(); + +} diff --git a/src/applications/transactions/edittype/PhabricatorSimpleEditType.php b/src/applications/transactions/edittype/PhabricatorSimpleEditType.php new file mode 100644 --- /dev/null +++ b/src/applications/transactions/edittype/PhabricatorSimpleEditType.php @@ -0,0 +1,41 @@ +valueType = $value_type; + return $this; + } + + public function getValueType() { + return $this->valueType; + } + + public function generateTransaction( + PhabricatorApplicationTransaction $template, + array $spec) { + + $template + ->setTransactionType($this->getTransactionType()) + ->setNewValue(idx($spec, 'value')); + + foreach ($this->getMetadata() as $key => $value) { + $template->setMetadataValue($key, $value); + } + + return $template; + } + + public function setValueDescription($value_description) { + $this->valueDescription = $value_description; + return $this; + } + + public function getValueDescription() { + return $this->valueDescription; + } + +}