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 @@ -5237,7 +5237,11 @@ 'PhortuneAccountEmailEditor' => 'applications/phortune/editor/PhortuneAccountEmailEditor.php', 'PhortuneAccountEmailPHIDType' => 'applications/phortune/phid/PhortuneAccountEmailPHIDType.php', 'PhortuneAccountEmailQuery' => 'applications/phortune/query/PhortuneAccountEmailQuery.php', + 'PhortuneAccountEmailRotateController' => 'applications/phortune/controller/account/PhortuneAccountEmailRotateController.php', + 'PhortuneAccountEmailRotateTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php', 'PhortuneAccountEmailStatus' => 'applications/phortune/constants/PhortuneAccountEmailStatus.php', + 'PhortuneAccountEmailStatusController' => 'applications/phortune/controller/account/PhortuneAccountEmailStatusController.php', + 'PhortuneAccountEmailStatusTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php', 'PhortuneAccountEmailTransaction' => 'applications/phortune/storage/PhortuneAccountEmailTransaction.php', 'PhortuneAccountEmailTransactionQuery' => 'applications/phortune/query/PhortuneAccountEmailTransactionQuery.php', 'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php', @@ -5296,6 +5300,7 @@ 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php', 'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php', + 'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php', 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php', @@ -11815,7 +11820,11 @@ 'PhortuneAccountEmailEditor' => 'PhabricatorApplicationTransactionEditor', 'PhortuneAccountEmailPHIDType' => 'PhabricatorPHIDType', 'PhortuneAccountEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhortuneAccountEmailRotateController' => 'PhortuneAccountController', + 'PhortuneAccountEmailRotateTransaction' => 'PhortuneAccountEmailTransactionType', 'PhortuneAccountEmailStatus' => 'Phobject', + 'PhortuneAccountEmailStatusController' => 'PhortuneAccountController', + 'PhortuneAccountEmailStatusTransaction' => 'PhortuneAccountEmailTransactionType', 'PhortuneAccountEmailTransaction' => 'PhabricatorModularTransaction', 'PhortuneAccountEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType', @@ -11883,6 +11892,7 @@ 'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneExternalController' => 'PhortuneController', 'PhortuneExternalOverviewController' => 'PhortuneExternalController', + 'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController', 'PhortuneInvoiceView' => 'AphrontTagView', 'PhortuneLandingController' => 'PhortuneController', 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/phortune/application/PhabricatorPhortuneApplication.php b/src/applications/phortune/application/PhabricatorPhortuneApplication.php --- a/src/applications/phortune/application/PhabricatorPhortuneApplication.php +++ b/src/applications/phortune/application/PhabricatorPhortuneApplication.php @@ -87,7 +87,12 @@ ), 'addresses/' => array( '' => 'PhortuneAccountEmailAddressesController', - '(?P\d+)/' => 'PhortuneAccountEmailViewController', + '(?P\d+)/' => array( + '' => 'PhortuneAccountEmailViewController', + 'rotate/' => 'PhortuneAccountEmailRotateController', + '(?Pdisable|enable)/' + => 'PhortuneAccountEmailStatusController', + ), $this->getEditRoutePattern('edit/') => 'PhortuneAccountEmailEditController', ), @@ -106,6 +111,7 @@ ), 'external/(?P[^/]+)/(?P[^/]+)/' => array( '' => 'PhortuneExternalOverviewController', + 'unsubscribe/' => 'PhortuneExternalUnsubscribeController', ), 'merchant/' => array( $this->getQueryRoutePattern() diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php @@ -0,0 +1,62 @@ +getViewer(); + $account = $this->getAccount(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('addressID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $address_uri = $address->getURI(); + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $address->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailRotateTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $address->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($address_uri) + ->applyTransactions($address, $xactions); + + return id(new AphrontRedirectResponse())->setURI($address_uri); + } + + return $this->newDialog() + ->setTitle(pht('Rotate Access Key')) + ->appendParagraph( + pht( + 'Rotate the access key for email address %s?', + phutil_tag('strong', array(), $address->getAddress()))) + ->appendParagraph( + pht( + 'Existing access links which have been sent to this email address '. + 'will stop working.')) + ->addSubmitButton(pht('Rotate Access Key')) + ->addCancelButton($address_uri); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php @@ -0,0 +1,137 @@ +getViewer(); + $account = $this->getAccount(); + + $address = id(new PhortuneAccountEmailQuery()) + ->setViewer($viewer) + ->withAccountPHIDs(array($account->getPHID())) + ->withIDs(array($request->getURIData('addressID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$address) { + return new Aphront404Response(); + } + + $address_uri = $address->getURI(); + + $is_enable = false; + $is_disable = false; + + $old_status = $address->getStatus(); + switch ($request->getURIData('action')) { + case 'enable': + if ($old_status === PhortuneAccountEmailStatus::STATUS_ACTIVE) { + return $this->newDialog() + ->setTitle(pht('Already Enabled')) + ->appendParagraph( + pht( + 'You can not enable this address because it is already '. + 'active.')) + ->addCancelButton($address_uri); + } + + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'You can not enable this address because it has been '. + 'permanently unsubscribed.')) + ->addCancelButton($address_uri); + } + + $new_status = PhortuneAccountEmailStatus::STATUS_ACTIVE; + $is_enable = true; + break; + case 'disable': + if ($old_status === PhortuneAccountEmailStatus::STATUS_DISABLED) { + return $this->newDialog() + ->setTitle(pht('Already Disabled')) + ->appendParagraph( + pht( + 'You can not disabled this address because it is already '. + 'disabled.')) + ->addCancelButton($address_uri); + } + + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'You can not disable this address because it has been '. + 'permanently unsubscribed.')) + ->addCancelButton($address_uri); + } + + $new_status = PhortuneAccountEmailStatus::STATUS_DISABLED; + $is_disable = true; + break; + default: + return new Aphront404Response(); + } + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $address->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($new_status); + + $address->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($address_uri) + ->applyTransactions($address, $xactions); + + return id(new AphrontRedirectResponse())->setURI($address_uri); + } + + $dialog = $this->newDialog(); + + $body = array(); + + if ($is_disable) { + $title = pht('Disable Address'); + + $body[] = pht( + 'This address will no longer receive email, and access links will '. + 'no longer function.'); + + $submit = pht('Disable Address'); + } else { + $title = pht('Enable Address'); + + $body[] = pht( + 'This address will receive email again, and existing links '. + 'to access order history will work again.'); + + $submit = pht('Enable Address'); + } + + foreach ($body as $graph) { + $dialog->appendParagraph($graph); + } + + return $dialog + ->setTitle($title) + ->addCancelButton($address_uri) + ->addSubmitButton($submit); + } +} diff --git a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php --- a/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php +++ b/src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php @@ -14,7 +14,7 @@ $address = id(new PhortuneAccountEmailQuery()) ->setViewer($viewer) ->withAccountPHIDs(array($account->getPHID())) - ->withIDs(array($request->getURIData('id'))) + ->withIDs(array($request->getURIData('addressID'))) ->executeOne(); if (!$address) { return new Aphront404Response(); @@ -83,6 +83,56 @@ ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + switch ($address->getStatus()) { + case PhortuneAccountEmailStatus::STATUS_ACTIVE: + $disable_name = pht('Disable Address'); + $disable_icon = 'fa-times'; + $can_disable = true; + $disable_action = 'disable'; + break; + case PhortuneAccountEmailStatus::STATUS_DISABLED: + $disable_name = pht('Enable Address'); + $disable_icon = 'fa-check'; + $can_disable = true; + $disable_action = 'enable'; + break; + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: + $disable_name = pht('Disable Address'); + $disable_icon = 'fa-times'; + $can_disable = false; + $disable_action = 'disable'; + break; + } + + $disable_uri = $this->getApplicationURI( + urisprintf( + 'account/%d/addresses/%d/%s/', + $account->getID(), + $address->getID(), + $disable_action)); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setHref($disable_uri) + ->setDisabled(!$can_disable) + ->setWorkflow(true)); + + $rotate_uri = $this->getApplicationURI( + urisprintf( + 'account/%d/addresses/%d/rotate/', + $account->getID(), + $address->getID())); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Rotate Access Key')) + ->setIcon('fa-refresh') + ->setHref($rotate_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Show External View')) @@ -100,7 +150,23 @@ $view = id(new PHUIPropertyListView()) ->setUser($viewer); + $access_key = $address->getAccessKey(); + + // This is not a meaningful security barrier: the full plaintext of the + // access key is visible on the page in the link target of the "Show + // External View" action. It's just here to make it clear "Rotate Access + // Key" actually does something. + + $prefix_length = 4; + $visible_part = substr($access_key, 0, $prefix_length); + $masked_part = str_repeat( + "\xE2\x80\xA2", + strlen($access_key) - $prefix_length); + $access_display = $visible_part.$masked_part; + $access_display = phutil_tag('tt', array(), $access_display); + $view->addProperty(pht('Email Address'), $address->getAddress()); + $view->addProperty(pht('Access Key'), $access_display); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Email Address Details')) diff --git a/src/applications/phortune/controller/external/PhortuneExternalController.php b/src/applications/phortune/controller/external/PhortuneExternalController.php --- a/src/applications/phortune/controller/external/PhortuneExternalController.php +++ b/src/applications/phortune/controller/external/PhortuneExternalController.php @@ -83,7 +83,29 @@ return $dialog; } - // TODO: Test that status is good. + switch ($email->getStatus()) { + case PhortuneAccountEmailStatus::STATUS_ACTIVE: + break; + case PhortuneAccountEmailStatus::STATUS_DISABLED: + return $this->newDialog() + ->setTitle(pht('Address Disabled')) + ->appendParagraph( + pht( + 'This email address (%s) has been disabled and no longer has '. + 'access to this payment account.', + $email_display)); + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribed')) + ->appendParagraph( + pht( + 'This email address (%s) has been permanently unsubscribed '. + 'and no longer has access to this payment account.', + $email_display)); + break; + default: + return new Aphront404Response(); + } $this->email = $email; diff --git a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php --- a/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php +++ b/src/applications/phortune/controller/external/PhortuneExternalOverviewController.php @@ -12,7 +12,14 @@ ->setBorder(true); $header = id(new PHUIHeaderView()) - ->setHeader(pht('Invoices and Receipts: %s', $account->getName())); + ->setHeader(pht('Invoices and Receipts: %s', $account->getName())) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-times') + ->setText(pht('Unsubscribe')) + ->setHref($email->getUnsubscribeURI()) + ->setWorkflow(true)); $external_view = $this->newExternalView(); $invoices_view = $this->newInvoicesView(); diff --git a/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php b/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php @@ -0,0 +1,67 @@ +getExternalViewer(); + $email = $this->getAccountEmail(); + $account = $email->getAccount(); + + $email_uri = $email->getExternalURI(); + + if ($request->isFormOrHisecPost()) { + $xactions = array(); + + $xactions[] = $email->getApplicationTransactionTemplate() + ->setTransactionType( + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) + ->setNewValue(PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED); + + $email->getApplicationTransactionEditor() + ->setActor($xviewer) + ->setActingAsPHID($email->getPHID()) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setCancelURI($email_uri) + ->applyTransactions($email, $xactions); + + return id(new AphrontRedirectResponse())->setURI($email_uri); + } + + $email_display = phutil_tag( + 'strong', + array(), + $email->getAddress()); + + $account_display = phutil_tag( + 'strong', + array(), + $account->getName()); + + $submit = pht( + 'Permanently Unsubscribe (%s)', + $email->getAddress()); + + return $this->newDialog() + ->setTitle(pht('Permanently Unsubscribe')) + ->appendParagraph( + pht( + 'Permanently unsubscribe this email address (%s) from this '. + 'payment account (%s)?', + $email_display, + $account_display)) + ->appendParagraph( + pht( + 'You will no longer receive email and access links will no longer '. + 'function.')) + ->appendParagraph( + pht( + 'This action is permanent and can not be undone.')) + ->addCancelButton($email_uri) + ->addSubmitButton($submit); + + } + +} diff --git a/src/applications/phortune/storage/PhortuneAccountEmail.php b/src/applications/phortune/storage/PhortuneAccountEmail.php --- a/src/applications/phortune/storage/PhortuneAccountEmail.php +++ b/src/applications/phortune/storage/PhortuneAccountEmail.php @@ -85,6 +85,13 @@ $this->getAccessKey()); } + public function getUnsubscribeURI() { + return urisprintf( + '/phortune/external/%s/%s/unsubscribe/', + $this->getAddressKey(), + $this->getAccessKey()); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php b/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php @@ -0,0 +1,23 @@ +setAccessKey($access_key); + } + + public function getTitle() { + return pht( + '%s rotated the access key for this email address.', + $this->renderAuthor()); + } + +} diff --git a/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php b/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php @@ -0,0 +1,23 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + return pht( + '%s changed the status for this address to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } + +}