@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add credential rotation and statuses (disabled, unsubscribed) to Phortune external email

Summary: Depends on D20737. Ref T13367. Allow external addresses to have their access key rotated. Account managers can disable them, and anyone with the link can permanently unsubscribe them.

Test Plan: Enabled/disabled addresses; permanently unsubscribed addresses.

Maniphest Tasks: T13367

Differential Revision: https://secure.phabricator.com/D20738

+434 -4
+10
src/__phutil_library_map__.php
··· 5237 5237 'PhortuneAccountEmailEditor' => 'applications/phortune/editor/PhortuneAccountEmailEditor.php', 5238 5238 'PhortuneAccountEmailPHIDType' => 'applications/phortune/phid/PhortuneAccountEmailPHIDType.php', 5239 5239 'PhortuneAccountEmailQuery' => 'applications/phortune/query/PhortuneAccountEmailQuery.php', 5240 + 'PhortuneAccountEmailRotateController' => 'applications/phortune/controller/account/PhortuneAccountEmailRotateController.php', 5241 + 'PhortuneAccountEmailRotateTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php', 5240 5242 'PhortuneAccountEmailStatus' => 'applications/phortune/constants/PhortuneAccountEmailStatus.php', 5243 + 'PhortuneAccountEmailStatusController' => 'applications/phortune/controller/account/PhortuneAccountEmailStatusController.php', 5244 + 'PhortuneAccountEmailStatusTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php', 5241 5245 'PhortuneAccountEmailTransaction' => 'applications/phortune/storage/PhortuneAccountEmailTransaction.php', 5242 5246 'PhortuneAccountEmailTransactionQuery' => 'applications/phortune/query/PhortuneAccountEmailTransactionQuery.php', 5243 5247 'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php', ··· 5296 5300 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 5297 5301 'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php', 5298 5302 'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php', 5303 + 'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php', 5299 5304 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 5300 5305 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', 5301 5306 'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php', ··· 11815 11820 'PhortuneAccountEmailEditor' => 'PhabricatorApplicationTransactionEditor', 11816 11821 'PhortuneAccountEmailPHIDType' => 'PhabricatorPHIDType', 11817 11822 'PhortuneAccountEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 11823 + 'PhortuneAccountEmailRotateController' => 'PhortuneAccountController', 11824 + 'PhortuneAccountEmailRotateTransaction' => 'PhortuneAccountEmailTransactionType', 11818 11825 'PhortuneAccountEmailStatus' => 'Phobject', 11826 + 'PhortuneAccountEmailStatusController' => 'PhortuneAccountController', 11827 + 'PhortuneAccountEmailStatusTransaction' => 'PhortuneAccountEmailTransactionType', 11819 11828 'PhortuneAccountEmailTransaction' => 'PhabricatorModularTransaction', 11820 11829 'PhortuneAccountEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 11821 11830 'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType', ··· 11883 11892 'PhortuneErrCode' => 'PhortuneConstants', 11884 11893 'PhortuneExternalController' => 'PhortuneController', 11885 11894 'PhortuneExternalOverviewController' => 'PhortuneExternalController', 11895 + 'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController', 11886 11896 'PhortuneInvoiceView' => 'AphrontTagView', 11887 11897 'PhortuneLandingController' => 'PhortuneController', 11888 11898 'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
+7 -1
src/applications/phortune/application/PhabricatorPhortuneApplication.php
··· 87 87 ), 88 88 'addresses/' => array( 89 89 '' => 'PhortuneAccountEmailAddressesController', 90 - '(?P<id>\d+)/' => 'PhortuneAccountEmailViewController', 90 + '(?P<addressID>\d+)/' => array( 91 + '' => 'PhortuneAccountEmailViewController', 92 + 'rotate/' => 'PhortuneAccountEmailRotateController', 93 + '(?P<action>disable|enable)/' 94 + => 'PhortuneAccountEmailStatusController', 95 + ), 91 96 $this->getEditRoutePattern('edit/') 92 97 => 'PhortuneAccountEmailEditController', 93 98 ), ··· 106 111 ), 107 112 'external/(?P<addressKey>[^/]+)/(?P<accessKey>[^/]+)/' => array( 108 113 '' => 'PhortuneExternalOverviewController', 114 + 'unsubscribe/' => 'PhortuneExternalUnsubscribeController', 109 115 ), 110 116 'merchant/' => array( 111 117 $this->getQueryRoutePattern()
+62
src/applications/phortune/controller/account/PhortuneAccountEmailRotateController.php
··· 1 + <?php 2 + 3 + final class PhortuneAccountEmailRotateController 4 + extends PhortuneAccountController { 5 + 6 + protected function shouldRequireAccountEditCapability() { 7 + return true; 8 + } 9 + 10 + protected function handleAccountRequest(AphrontRequest $request) { 11 + $viewer = $this->getViewer(); 12 + $account = $this->getAccount(); 13 + 14 + $address = id(new PhortuneAccountEmailQuery()) 15 + ->setViewer($viewer) 16 + ->withAccountPHIDs(array($account->getPHID())) 17 + ->withIDs(array($request->getURIData('addressID'))) 18 + ->requireCapabilities( 19 + array( 20 + PhabricatorPolicyCapability::CAN_VIEW, 21 + PhabricatorPolicyCapability::CAN_EDIT, 22 + )) 23 + ->executeOne(); 24 + if (!$address) { 25 + return new Aphront404Response(); 26 + } 27 + 28 + $address_uri = $address->getURI(); 29 + 30 + if ($request->isFormOrHisecPost()) { 31 + $xactions = array(); 32 + 33 + $xactions[] = $address->getApplicationTransactionTemplate() 34 + ->setTransactionType( 35 + PhortuneAccountEmailRotateTransaction::TRANSACTIONTYPE) 36 + ->setNewValue(true); 37 + 38 + $address->getApplicationTransactionEditor() 39 + ->setActor($viewer) 40 + ->setContentSourceFromRequest($request) 41 + ->setContinueOnMissingFields(true) 42 + ->setContinueOnNoEffect(true) 43 + ->setCancelURI($address_uri) 44 + ->applyTransactions($address, $xactions); 45 + 46 + return id(new AphrontRedirectResponse())->setURI($address_uri); 47 + } 48 + 49 + return $this->newDialog() 50 + ->setTitle(pht('Rotate Access Key')) 51 + ->appendParagraph( 52 + pht( 53 + 'Rotate the access key for email address %s?', 54 + phutil_tag('strong', array(), $address->getAddress()))) 55 + ->appendParagraph( 56 + pht( 57 + 'Existing access links which have been sent to this email address '. 58 + 'will stop working.')) 59 + ->addSubmitButton(pht('Rotate Access Key')) 60 + ->addCancelButton($address_uri); 61 + } 62 + }
+137
src/applications/phortune/controller/account/PhortuneAccountEmailStatusController.php
··· 1 + <?php 2 + 3 + final class PhortuneAccountEmailStatusController 4 + extends PhortuneAccountController { 5 + 6 + protected function shouldRequireAccountEditCapability() { 7 + return true; 8 + } 9 + 10 + protected function handleAccountRequest(AphrontRequest $request) { 11 + $viewer = $this->getViewer(); 12 + $account = $this->getAccount(); 13 + 14 + $address = id(new PhortuneAccountEmailQuery()) 15 + ->setViewer($viewer) 16 + ->withAccountPHIDs(array($account->getPHID())) 17 + ->withIDs(array($request->getURIData('addressID'))) 18 + ->requireCapabilities( 19 + array( 20 + PhabricatorPolicyCapability::CAN_VIEW, 21 + PhabricatorPolicyCapability::CAN_EDIT, 22 + )) 23 + ->executeOne(); 24 + if (!$address) { 25 + return new Aphront404Response(); 26 + } 27 + 28 + $address_uri = $address->getURI(); 29 + 30 + $is_enable = false; 31 + $is_disable = false; 32 + 33 + $old_status = $address->getStatus(); 34 + switch ($request->getURIData('action')) { 35 + case 'enable': 36 + if ($old_status === PhortuneAccountEmailStatus::STATUS_ACTIVE) { 37 + return $this->newDialog() 38 + ->setTitle(pht('Already Enabled')) 39 + ->appendParagraph( 40 + pht( 41 + 'You can not enable this address because it is already '. 42 + 'active.')) 43 + ->addCancelButton($address_uri); 44 + } 45 + 46 + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { 47 + return $this->newDialog() 48 + ->setTitle(pht('Permanently Unsubscribed')) 49 + ->appendParagraph( 50 + pht( 51 + 'You can not enable this address because it has been '. 52 + 'permanently unsubscribed.')) 53 + ->addCancelButton($address_uri); 54 + } 55 + 56 + $new_status = PhortuneAccountEmailStatus::STATUS_ACTIVE; 57 + $is_enable = true; 58 + break; 59 + case 'disable': 60 + if ($old_status === PhortuneAccountEmailStatus::STATUS_DISABLED) { 61 + return $this->newDialog() 62 + ->setTitle(pht('Already Disabled')) 63 + ->appendParagraph( 64 + pht( 65 + 'You can not disabled this address because it is already '. 66 + 'disabled.')) 67 + ->addCancelButton($address_uri); 68 + } 69 + 70 + if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) { 71 + return $this->newDialog() 72 + ->setTitle(pht('Permanently Unsubscribed')) 73 + ->appendParagraph( 74 + pht( 75 + 'You can not disable this address because it has been '. 76 + 'permanently unsubscribed.')) 77 + ->addCancelButton($address_uri); 78 + } 79 + 80 + $new_status = PhortuneAccountEmailStatus::STATUS_DISABLED; 81 + $is_disable = true; 82 + break; 83 + default: 84 + return new Aphront404Response(); 85 + } 86 + 87 + if ($request->isFormOrHisecPost()) { 88 + $xactions = array(); 89 + 90 + $xactions[] = $address->getApplicationTransactionTemplate() 91 + ->setTransactionType( 92 + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) 93 + ->setNewValue($new_status); 94 + 95 + $address->getApplicationTransactionEditor() 96 + ->setActor($viewer) 97 + ->setContentSourceFromRequest($request) 98 + ->setContinueOnMissingFields(true) 99 + ->setContinueOnNoEffect(true) 100 + ->setCancelURI($address_uri) 101 + ->applyTransactions($address, $xactions); 102 + 103 + return id(new AphrontRedirectResponse())->setURI($address_uri); 104 + } 105 + 106 + $dialog = $this->newDialog(); 107 + 108 + $body = array(); 109 + 110 + if ($is_disable) { 111 + $title = pht('Disable Address'); 112 + 113 + $body[] = pht( 114 + 'This address will no longer receive email, and access links will '. 115 + 'no longer function.'); 116 + 117 + $submit = pht('Disable Address'); 118 + } else { 119 + $title = pht('Enable Address'); 120 + 121 + $body[] = pht( 122 + 'This address will receive email again, and existing links '. 123 + 'to access order history will work again.'); 124 + 125 + $submit = pht('Enable Address'); 126 + } 127 + 128 + foreach ($body as $graph) { 129 + $dialog->appendParagraph($graph); 130 + } 131 + 132 + return $dialog 133 + ->setTitle($title) 134 + ->addCancelButton($address_uri) 135 + ->addSubmitButton($submit); 136 + } 137 + }
+67 -1
src/applications/phortune/controller/account/PhortuneAccountEmailViewController.php
··· 14 14 $address = id(new PhortuneAccountEmailQuery()) 15 15 ->setViewer($viewer) 16 16 ->withAccountPHIDs(array($account->getPHID())) 17 - ->withIDs(array($request->getURIData('id'))) 17 + ->withIDs(array($request->getURIData('addressID'))) 18 18 ->executeOne(); 19 19 if (!$address) { 20 20 return new Aphront404Response(); ··· 83 83 ->setDisabled(!$can_edit) 84 84 ->setWorkflow(!$can_edit)); 85 85 86 + switch ($address->getStatus()) { 87 + case PhortuneAccountEmailStatus::STATUS_ACTIVE: 88 + $disable_name = pht('Disable Address'); 89 + $disable_icon = 'fa-times'; 90 + $can_disable = true; 91 + $disable_action = 'disable'; 92 + break; 93 + case PhortuneAccountEmailStatus::STATUS_DISABLED: 94 + $disable_name = pht('Enable Address'); 95 + $disable_icon = 'fa-check'; 96 + $can_disable = true; 97 + $disable_action = 'enable'; 98 + break; 99 + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: 100 + $disable_name = pht('Disable Address'); 101 + $disable_icon = 'fa-times'; 102 + $can_disable = false; 103 + $disable_action = 'disable'; 104 + break; 105 + } 106 + 107 + $disable_uri = $this->getApplicationURI( 108 + urisprintf( 109 + 'account/%d/addresses/%d/%s/', 110 + $account->getID(), 111 + $address->getID(), 112 + $disable_action)); 113 + 114 + $curtain->addAction( 115 + id(new PhabricatorActionView()) 116 + ->setName($disable_name) 117 + ->setIcon($disable_icon) 118 + ->setHref($disable_uri) 119 + ->setDisabled(!$can_disable) 120 + ->setWorkflow(true)); 121 + 122 + $rotate_uri = $this->getApplicationURI( 123 + urisprintf( 124 + 'account/%d/addresses/%d/rotate/', 125 + $account->getID(), 126 + $address->getID())); 127 + 128 + $curtain->addAction( 129 + id(new PhabricatorActionView()) 130 + ->setName(pht('Rotate Access Key')) 131 + ->setIcon('fa-refresh') 132 + ->setHref($rotate_uri) 133 + ->setDisabled(!$can_edit) 134 + ->setWorkflow(true)); 135 + 86 136 $curtain->addAction( 87 137 id(new PhabricatorActionView()) 88 138 ->setName(pht('Show External View')) ··· 100 150 $view = id(new PHUIPropertyListView()) 101 151 ->setUser($viewer); 102 152 153 + $access_key = $address->getAccessKey(); 154 + 155 + // This is not a meaningful security barrier: the full plaintext of the 156 + // access key is visible on the page in the link target of the "Show 157 + // External View" action. It's just here to make it clear "Rotate Access 158 + // Key" actually does something. 159 + 160 + $prefix_length = 4; 161 + $visible_part = substr($access_key, 0, $prefix_length); 162 + $masked_part = str_repeat( 163 + "\xE2\x80\xA2", 164 + strlen($access_key) - $prefix_length); 165 + $access_display = $visible_part.$masked_part; 166 + $access_display = phutil_tag('tt', array(), $access_display); 167 + 103 168 $view->addProperty(pht('Email Address'), $address->getAddress()); 169 + $view->addProperty(pht('Access Key'), $access_display); 104 170 105 171 return id(new PHUIObjectBoxView()) 106 172 ->setHeaderText(pht('Email Address Details'))
+23 -1
src/applications/phortune/controller/external/PhortuneExternalController.php
··· 83 83 return $dialog; 84 84 } 85 85 86 - // TODO: Test that status is good. 86 + switch ($email->getStatus()) { 87 + case PhortuneAccountEmailStatus::STATUS_ACTIVE: 88 + break; 89 + case PhortuneAccountEmailStatus::STATUS_DISABLED: 90 + return $this->newDialog() 91 + ->setTitle(pht('Address Disabled')) 92 + ->appendParagraph( 93 + pht( 94 + 'This email address (%s) has been disabled and no longer has '. 95 + 'access to this payment account.', 96 + $email_display)); 97 + case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED: 98 + return $this->newDialog() 99 + ->setTitle(pht('Permanently Unsubscribed')) 100 + ->appendParagraph( 101 + pht( 102 + 'This email address (%s) has been permanently unsubscribed '. 103 + 'and no longer has access to this payment account.', 104 + $email_display)); 105 + break; 106 + default: 107 + return new Aphront404Response(); 108 + } 87 109 88 110 $this->email = $email; 89 111
+8 -1
src/applications/phortune/controller/external/PhortuneExternalOverviewController.php
··· 12 12 ->setBorder(true); 13 13 14 14 $header = id(new PHUIHeaderView()) 15 - ->setHeader(pht('Invoices and Receipts: %s', $account->getName())); 15 + ->setHeader(pht('Invoices and Receipts: %s', $account->getName())) 16 + ->addActionLink( 17 + id(new PHUIButtonView()) 18 + ->setTag('a') 19 + ->setIcon('fa-times') 20 + ->setText(pht('Unsubscribe')) 21 + ->setHref($email->getUnsubscribeURI()) 22 + ->setWorkflow(true)); 16 23 17 24 $external_view = $this->newExternalView(); 18 25 $invoices_view = $this->newInvoicesView();
+67
src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php
··· 1 + <?php 2 + 3 + final class PhortuneExternalUnsubscribeController 4 + extends PhortuneExternalController { 5 + 6 + protected function handleExternalRequest(AphrontRequest $request) { 7 + $xviewer = $this->getExternalViewer(); 8 + $email = $this->getAccountEmail(); 9 + $account = $email->getAccount(); 10 + 11 + $email_uri = $email->getExternalURI(); 12 + 13 + if ($request->isFormOrHisecPost()) { 14 + $xactions = array(); 15 + 16 + $xactions[] = $email->getApplicationTransactionTemplate() 17 + ->setTransactionType( 18 + PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE) 19 + ->setNewValue(PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED); 20 + 21 + $email->getApplicationTransactionEditor() 22 + ->setActor($xviewer) 23 + ->setActingAsPHID($email->getPHID()) 24 + ->setContentSourceFromRequest($request) 25 + ->setContinueOnMissingFields(true) 26 + ->setContinueOnNoEffect(true) 27 + ->setCancelURI($email_uri) 28 + ->applyTransactions($email, $xactions); 29 + 30 + return id(new AphrontRedirectResponse())->setURI($email_uri); 31 + } 32 + 33 + $email_display = phutil_tag( 34 + 'strong', 35 + array(), 36 + $email->getAddress()); 37 + 38 + $account_display = phutil_tag( 39 + 'strong', 40 + array(), 41 + $account->getName()); 42 + 43 + $submit = pht( 44 + 'Permanently Unsubscribe (%s)', 45 + $email->getAddress()); 46 + 47 + return $this->newDialog() 48 + ->setTitle(pht('Permanently Unsubscribe')) 49 + ->appendParagraph( 50 + pht( 51 + 'Permanently unsubscribe this email address (%s) from this '. 52 + 'payment account (%s)?', 53 + $email_display, 54 + $account_display)) 55 + ->appendParagraph( 56 + pht( 57 + 'You will no longer receive email and access links will no longer '. 58 + 'function.')) 59 + ->appendParagraph( 60 + pht( 61 + 'This action is permanent and can not be undone.')) 62 + ->addCancelButton($email_uri) 63 + ->addSubmitButton($submit); 64 + 65 + } 66 + 67 + }
+7
src/applications/phortune/storage/PhortuneAccountEmail.php
··· 85 85 $this->getAccessKey()); 86 86 } 87 87 88 + public function getUnsubscribeURI() { 89 + return urisprintf( 90 + '/phortune/external/%s/%s/unsubscribe/', 91 + $this->getAddressKey(), 92 + $this->getAccessKey()); 93 + } 94 + 88 95 89 96 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 90 97
+23
src/applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php
··· 1 + <?php 2 + 3 + final class PhortuneAccountEmailRotateTransaction 4 + extends PhortuneAccountEmailTransactionType { 5 + 6 + const TRANSACTIONTYPE = 'rotate'; 7 + 8 + public function generateOldValue($object) { 9 + return false; 10 + } 11 + 12 + public function applyInternalEffects($object, $value) { 13 + $access_key = Filesystem::readRandomCharacters(16); 14 + $object->setAccessKey($access_key); 15 + } 16 + 17 + public function getTitle() { 18 + return pht( 19 + '%s rotated the access key for this email address.', 20 + $this->renderAuthor()); 21 + } 22 + 23 + }
+23
src/applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php
··· 1 + <?php 2 + 3 + final class PhortuneAccountEmailStatusTransaction 4 + extends PhortuneAccountEmailTransactionType { 5 + 6 + const TRANSACTIONTYPE = 'status'; 7 + 8 + public function generateOldValue($object) { 9 + return $object->getStatus(); 10 + } 11 + 12 + public function applyInternalEffects($object, $value) { 13 + $object->setStatus($value); 14 + } 15 + 16 + public function getTitle() { 17 + return pht( 18 + '%s changed the status for this address to %s.', 19 + $this->renderAuthor(), 20 + $this->renderNewValue()); 21 + } 22 + 23 + }