diff --git a/resources/sql/autopatches/20150611.spaces.1.mailxaction.sql b/resources/sql/autopatches/20150611.spaces.1.mailxaction.sql new file mode 100644 --- /dev/null +++ b/resources/sql/autopatches/20150611.spaces.1.mailxaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_metamta.metamta_applicationemailtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 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 @@ -2051,9 +2051,12 @@ 'PhabricatorMetaMTAApplication' => 'applications/metamta/application/PhabricatorMetaMTAApplication.php', 'PhabricatorMetaMTAApplicationEmail' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php', 'PhabricatorMetaMTAApplicationEmailDatasource' => 'applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php', + 'PhabricatorMetaMTAApplicationEmailEditor' => 'applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php', 'PhabricatorMetaMTAApplicationEmailPHIDType' => 'applications/phid/PhabricatorMetaMTAApplicationEmailPHIDType.php', 'PhabricatorMetaMTAApplicationEmailPanel' => 'applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php', 'PhabricatorMetaMTAApplicationEmailQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailQuery.php', + 'PhabricatorMetaMTAApplicationEmailTransaction' => 'applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php', + 'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php', 'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php', 'PhabricatorMetaMTAConfigOptions' => 'applications/config/option/PhabricatorMetaMTAConfigOptions.php', 'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php', @@ -5493,11 +5496,16 @@ 'PhabricatorMetaMTAApplicationEmail' => array( 'PhabricatorMetaMTADAO', 'PhabricatorPolicyInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorDestructibleInterface', ), 'PhabricatorMetaMTAApplicationEmailDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorMetaMTAApplicationEmailEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorMetaMTAApplicationEmailPHIDType' => 'PhabricatorPHIDType', 'PhabricatorMetaMTAApplicationEmailPanel' => 'PhabricatorApplicationConfigurationPanel', 'PhabricatorMetaMTAApplicationEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorMetaMTAApplicationEmailTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorMetaMTAApplicationEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorMetaMTAConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php --- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php +++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php @@ -190,30 +190,6 @@ )); } - private function validateApplicationEmail($email) { - $errors = array(); - $e_email = true; - - if (!strlen($email)) { - $e_email = pht('Required'); - $errors[] = pht('Email is required.'); - } else if (!PhabricatorUserEmail::isValidAddress($email)) { - $e_email = pht('Invalid'); - $errors[] = PhabricatorUserEmail::describeValidAddresses(); - } else if (!PhabricatorUserEmail::isAllowedAddress($email)) { - $e_email = pht('Disallowed'); - $errors[] = PhabricatorUserEmail::describeAllowedAddresses(); - } - $user_emails = id(new PhabricatorUserEmail()) - ->loadAllWhere('address = %s', $email); - if ($user_emails) { - $e_email = pht('Duplicate'); - $errors[] = pht('A user already has this email.'); - } - - return array($e_email, $errors); - } - private function returnNewAddressResponse( AphrontRequest $request, PhutilURI $uri, @@ -265,45 +241,59 @@ $viewer = $request->getUser(); - $e_email = true; - $email = null; - $errors = array(); - $default_user_key = + $config_default = PhabricatorMetaMTAApplicationEmail::CONFIG_DEFAULT_AUTHOR; + + $e_email = true; + $v_email = $email_object->getAddress(); + $v_default = $email_object->getConfigValue($config_default); + + $validation_exception = null; if ($request->isDialogFormPost()) { - $email = trim($request->getStr('email')); - list($e_email, $errors) = $this->validateApplicationEmail($email); - $email_object->setAddress($email); - $default_user = $request->getArr($default_user_key); - $default_user = reset($default_user); - if ($default_user) { - $email_object->setConfigValue($default_user_key, $default_user); - } + $e_email = null; - if (!$errors) { - try { - $email_object->save(); - return id(new AphrontRedirectResponse())->setURI( - $uri->alter('highlight', $email_object->getID())); - } catch (AphrontDuplicateKeyQueryException $ex) { - $e_email = pht('Duplicate'); - $errors[] = pht( - 'Another application is already configured to use this email '. - 'address.'); - } - } - } + $v_email = trim($request->getStr('email')); + $v_default = $request->getArr($config_default); + $v_default = nonempty(head($v_default), null); - if ($errors) { - $errors = id(new PHUIInfoView()) - ->setErrors($errors); + $type_address = + PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS; + $type_config = + PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG; + + $key_config = PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG; + + $xactions = array(); + + $xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction()) + ->setTransactionType($type_address) + ->setNewValue($v_email); + + $xactions[] = id(new PhabricatorMetaMTAApplicationEmailTransaction()) + ->setTransactionType($type_config) + ->setMetadataValue($key_config, $config_default) + ->setNewValue($v_default); + + $editor = id(new PhabricatorMetaMTAApplicationEmailEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + try { + $editor->applyTransactions($email_object, $xactions); + + return id(new AphrontRedirectResponse())->setURI( + $uri->alter('highlight', $email_object->getID())); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + $e_email = $ex->getShortMessage($type_address); + } } - $default_user = $email_object->getConfigValue($default_user_key); - if ($default_user) { - $default_user_value = array($default_user); + if ($v_default) { + $v_default = array($v_default); } else { - $default_user_value = array(); + $v_default = array(); } $form = id(new AphrontFormView()) @@ -312,28 +302,29 @@ id(new AphrontFormTextControl()) ->setLabel(pht('Email')) ->setName('email') - ->setValue($email_object->getAddress()) - ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) + ->setValue($v_email) ->setError($e_email)) ->appendControl( id(new AphrontFormTokenizerControl()) ->setDatasource(new PhabricatorPeopleDatasource()) ->setLabel(pht('Default Author')) - ->setName($default_user_key) + ->setName($config_default) ->setLimit(1) - ->setValue($default_user_value) + ->setValue($v_default) ->setCaption(pht( 'Used if the "From:" address does not map to a known account.'))); + if ($is_new) { $title = pht('New Address'); } else { $title = pht('Edit Address'); } + $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle($title) - ->appendChild($errors) + ->setValidationException($validation_exception) ->appendForm($form) ->addSubmitButton(pht('Save')) ->addCancelButton($uri); @@ -350,7 +341,8 @@ PhutilURI $uri, $email_object_id) { - $viewer = $request->getUser(); + $viewer = $this->getViewer(); + $email_object = id(new PhabricatorMetaMTAApplicationEmailQuery()) ->setViewer($viewer) ->withIDs(array($email_object_id)) @@ -365,7 +357,8 @@ } if ($request->isDialogFormPost()) { - $email_object->delete(); + $engine = new PhabricatorDestructionEngine(); + $engine->destroyObject($email_object); return id(new AphrontRedirectResponse())->setURI($uri); } diff --git a/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/metamta/editor/PhabricatorMetaMTAApplicationEmailEditor.php @@ -0,0 +1,145 @@ +<?php + +final class PhabricatorMetaMTAApplicationEmailEditor + extends PhabricatorApplicationTransactionEditor { + + public function getEditorApplicationClass() { + return pht('PhabricatorMetaMTAApplication'); + } + + public function getEditorObjectsDescription() { + return pht('Application Emails'); + } + + public function getTransactionTypes() { + $types = parent::getTransactionTypes(); + + $types[] = PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS; + $types[] = PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG; + + return $types; + } + + protected function getCustomTransactionOldValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS: + return $object->getAddress(); + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG: + $key = $xaction->getMetadataValue( + PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG); + return $object->getConfigValue($key); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS: + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG: + return $xaction->getNewValue(); + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + $new = $xaction->getNewValue(); + + switch ($xaction->getTransactionType()) { + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS: + $object->setAddress($new); + return; + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG: + $key = $xaction->getMetadataValue( + PhabricatorMetaMTAApplicationEmailTransaction::KEY_CONFIG); + $object->setConfigValue($key, $new); + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS: + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_CONFIG: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + switch ($type) { + case PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS: + foreach ($xactions as $xaction) { + $email = $xaction->getNewValue(); + if (!strlen($email)) { + // We'll deal with this below. + continue; + } + + if (!PhabricatorUserEmail::isValidAddress($email)) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('Email address is not formatted properly.')); + } + } + + $missing = $this->validateIsEmptyTextField( + $object->getAddress(), + $xactions); + + if ($missing) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Required'), + pht('You must provide an email address.'), + nonempty(last($xactions), null)); + + $error->setIsMissingFieldError(true); + $errors[] = $error; + } + break; + } + + return $errors; + } + + protected function didCatchDuplicateKeyException( + PhabricatorLiskDAO $object, + array $xactions, + Exception $ex) { + + $errors = array(); + $errors[] = new PhabricatorApplicationTransactionValidationError( + PhabricatorMetaMTAApplicationEmailTransaction::TYPE_ADDRESS, + pht('Duplicate'), + pht('This email address is already in use.'), + null); + + throw new PhabricatorApplicationTransactionValidationException($errors); + } + + +} diff --git a/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php new file mode 100644 --- /dev/null +++ b/src/applications/metamta/query/PhabricatorMetaMTAApplicationEmailTransactionQuery.php @@ -0,0 +1,10 @@ +<?php + +final class PhabricatorMetaMTAApplicationEmailTransactionQuery + extends PhabricatorApplicationTransactionQuery { + + public function getTemplateApplicationTransaction() { + return new PhabricatorMetaMTAApplicationEmailTransaction(); + } + +} diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php --- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmail.php @@ -2,7 +2,10 @@ final class PhabricatorMetaMTAApplicationEmail extends PhabricatorMetaMTADAO - implements PhabricatorPolicyInterface { + implements + PhabricatorPolicyInterface, + PhabricatorApplicationTransactionInterface, + PhabricatorDestructibleInterface { protected $applicationPHID; protected $address; @@ -109,4 +112,35 @@ return $this->getApplication()->describeAutomaticCapability($capability); } + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorMetaMTAApplicationEmailEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorMetaMTAApplicationEmailTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + return $timeline; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php @@ -0,0 +1,23 @@ +<?php + +final class PhabricatorMetaMTAApplicationEmailTransaction + extends PhabricatorApplicationTransaction { + + const KEY_CONFIG = 'appemail.config.key'; + + const TYPE_ADDRESS = 'appemail.address'; + const TYPE_CONFIG = 'appemail.config'; + + public function getApplicationName() { + return 'metamta'; + } + + public function getApplicationTransactionType() { + return PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST; + } + + public function getApplicationTransactionCommentObject() { + return null; + } + +} diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -844,6 +844,11 @@ $object->save(); } catch (AphrontDuplicateKeyQueryException $ex) { $object->killTransaction(); + + // This callback has an opportunity to throw a better exception, + // so execution may end here. + $this->didCatchDuplicateKeyException($object, $xactions, $ex); + throw $ex; } @@ -1021,6 +1026,13 @@ return $xactions; } + protected function didCatchDuplicateKeyException( + PhabricatorLiskDAO $object, + array $xactions, + Exception $ex) { + return; + } + public function publishTransactions( PhabricatorLiskDAO $object, array $xactions) {