@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.

Send emails for email invites

Summary:
Ref T7152. Ref T3554.

- When an administrator clicks "send invites", queue tasks to send the invites.
- Then, actually send the invites.
- Make the links in the invites work properly.
- Also provide `bin/worker execute` to make debugging one-off workers like this easier.
- Clean up some UI, too.

Test Plan:
We now get as far as the exception which is a placeholder for a registration workflow.

{F291213}

{F291214}

{F291215}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T3554, T7152

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

+210 -10
+4
src/__phutil_library_map__.php
··· 1361 1361 'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php', 1362 1362 'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php', 1363 1363 'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php', 1364 + 'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php', 1364 1365 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', 1365 1366 'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php', 1366 1367 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', ··· 2626 2627 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', 2627 2628 'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php', 2628 2629 'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php', 2630 + 'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php', 2629 2631 'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php', 2630 2632 'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php', 2631 2633 'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php', ··· 4591 4593 'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine', 4592 4594 'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase', 4593 4595 'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException', 4596 + 'PhabricatorAuthInviteWorker' => 'PhabricatorWorker', 4594 4597 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', 4595 4598 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 4596 4599 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', ··· 5959 5962 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 5960 5963 'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery', 5961 5964 'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow', 5965 + 'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow', 5962 5966 'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow', 5963 5967 'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow', 5964 5968 'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
+18
src/applications/auth/data/PhabricatorAuthInviteAction.php
··· 189 189 return $results; 190 190 } 191 191 192 + public function sendInvite(PhabricatorUser $actor, $template) { 193 + if (!$this->willSend()) { 194 + throw new Exception(pht('Invite action is not a send action!')); 195 + } 196 + 197 + if (!preg_match('/{\$INVITE_URI}/', $template)) { 198 + throw new Exception(pht('Invite template does not include invite URI!')); 199 + } 200 + 201 + PhabricatorWorker::scheduleTask( 202 + 'PhabricatorAuthInviteWorker', 203 + array( 204 + 'address' => $this->getEmailAddress(), 205 + 'template' => $template, 206 + 'authorPHID' => $actor->getPHID(), 207 + )); 208 + } 209 + 192 210 }
+4 -3
src/applications/auth/engine/PhabricatorAuthInviteEngine.php
··· 35 35 public function processInviteCode($code) { 36 36 $viewer = $this->getViewer(); 37 37 38 - $invite = id(new PhabricatorAuthInvite())->loadOneWhere( 39 - 'verificationHash = %s', 40 - PhabricatorHash::digestForIndex($code)); 38 + $invite = id(new PhabricatorAuthInviteQuery()) 39 + ->setViewer($viewer) 40 + ->withVerificationCodes(array($code)) 41 + ->executeOne(); 41 42 if (!$invite) { 42 43 throw id(new PhabricatorAuthInviteInvalidException( 43 44 pht('Bad Invite Code'),
+1
src/applications/auth/query/PhabricatorAuthInviteQuery.php
··· 88 88 foreach ($this->verificationCodes as $code) { 89 89 $hashes[] = PhabricatorHash::digestForIndex($code); 90 90 } 91 + 91 92 $where[] = qsprintf( 92 93 $conn_r, 93 94 'verificationHash IN (%Ls)',
+15 -1
src/applications/auth/query/PhabricatorAuthInviteSearchEngine.php
··· 86 86 ); 87 87 } 88 88 89 - $table = new AphrontTableView($rows); 89 + $table = id(new AphrontTableView($rows)) 90 + ->setHeaders( 91 + array( 92 + pht('Email Address'), 93 + pht('Sent By'), 94 + pht('Accepted By'), 95 + pht('Invited'), 96 + )) 97 + ->setColumnClasses( 98 + array( 99 + '', 100 + '', 101 + 'wide', 102 + 'right', 103 + )); 90 104 91 105 return id(new PHUIObjectBoxView()) 92 106 ->setHeaderText(pht('Email Invitations'))
+8 -2
src/applications/auth/storage/PhabricatorAuthInvite.php
··· 38 38 PhabricatorAuthInvitePHIDType::TYPECONST); 39 39 } 40 40 41 + public function regenerateVerificationCode() { 42 + $this->verificationCode = Filesystem::readRandomCharacters(16); 43 + $this->verificationHash = null; 44 + return $this; 45 + } 46 + 41 47 public function getVerificationCode() { 42 - if (!$this->getVerificationHash()) { 48 + if (!$this->verificationCode) { 43 49 if ($this->verificationHash) { 44 50 throw new Exception( 45 51 pht( 46 52 'Verification code can not be regenerated after an invite is '. 47 53 'created.')); 48 54 } 49 - $this->verificationCode = Filesystem::readRandomCharacters(16); 55 + $this->regenerateVerificationCode(); 50 56 } 51 57 return $this->verificationCode; 52 58 }
+60
src/applications/auth/worker/PhabricatorAuthInviteWorker.php
··· 1 + <?php 2 + 3 + final class PhabricatorAuthInviteWorker 4 + extends PhabricatorWorker { 5 + 6 + protected function doWork() { 7 + $data = $this->getTaskData(); 8 + $viewer = PhabricatorUser::getOmnipotentUser(); 9 + 10 + $address = idx($data, 'address'); 11 + $author_phid = idx($data, 'authorPHID'); 12 + 13 + $author = id(new PhabricatorPeopleQuery()) 14 + ->setViewer($viewer) 15 + ->withPHIDs(array($author_phid)) 16 + ->executeOne(); 17 + if (!$author) { 18 + throw new PhabricatorWorkerPermanentFailureException( 19 + pht('Invite has invalid author PHID ("%s").', $author_phid)); 20 + } 21 + 22 + $invite = id(new PhabricatorAuthInviteQuery()) 23 + ->setViewer($viewer) 24 + ->withEmailAddresses(array($address)) 25 + ->executeOne(); 26 + if ($invite) { 27 + // If we're inviting a user who has already been invited, we just 28 + // regenerate their invite code. 29 + $invite->regenerateVerificationCode(); 30 + } else { 31 + // Otherwise, we're creating a new invite. 32 + $invite = id(new PhabricatorAuthInvite()) 33 + ->setEmailAddress($address); 34 + } 35 + 36 + // Whether this is a new invite or not, tag this most recent author as 37 + // the invite author. 38 + $invite->setAuthorPHID($author_phid); 39 + 40 + $code = $invite->getVerificationCode(); 41 + $invite_uri = '/auth/invite/'.$code.'/'; 42 + $invite_uri = PhabricatorEnv::getProductionURI($invite_uri); 43 + 44 + $template = idx($data, 'template'); 45 + $template = str_replace('{$INVITE_URI}', $invite_uri, $template); 46 + 47 + $invite->save(); 48 + 49 + $mail = id(new PhabricatorMetaMTAMail()) 50 + ->addRawTos(array($invite->getEmailAddress())) 51 + ->setForceDelivery(true) 52 + ->setSubject( 53 + pht( 54 + '[Phabricator] %s has invited you to join Phabricator', 55 + $author->getFullName())) 56 + ->setBody($template) 57 + ->saveAndSend(); 58 + } 59 + 60 + }
+39 -4
src/applications/people/controller/PhabricatorPeopleInviteSendController.php
··· 44 44 45 45 $any_valid = false; 46 46 $all_valid = true; 47 - $action_send = PhabricatorAuthInviteAction::ACTION_SEND; 48 47 foreach ($actions as $action) { 49 - if ($action->getAction() == $action_send) { 48 + if ($action->willSend()) { 50 49 $any_valid = true; 51 50 } else { 52 51 $all_valid = false; ··· 72 71 } 73 72 74 73 if ($any_valid && $request->getBool('confirm')) { 75 - throw new Exception( 76 - pht('TODO: This workflow is not yet fully implemented.')); 74 + 75 + // TODO: The copywriting on this mail could probably be more 76 + // engaging and we could have a fancy HTML version. 77 + 78 + $template = array(); 79 + $template[] = pht( 80 + '%s has invited you to join Phabricator.', 81 + $viewer->getFullName()); 82 + 83 + if (strlen(trim($message))) { 84 + $template[] = $message; 85 + } 86 + 87 + $template[] = pht( 88 + 'To register an account and get started, follow this link:'); 89 + 90 + // This isn't a variable; it will be replaced later on in the 91 + // daemons once they generate the URI. 92 + $template[] = '{$INVITE_URI}'; 93 + 94 + $template[] = pht( 95 + 'If you already have an account, you can follow the link to '. 96 + 'quickly verify this email address.'); 97 + 98 + $template = implode("\n\n", $template); 99 + 100 + foreach ($actions as $action) { 101 + if ($action->willSend()) { 102 + $action->sendInvite($viewer, $template); 103 + } 104 + } 105 + 106 + // TODO: This is a bit anticlimactic. We don't really have anything 107 + // to show the user because the action is happening in the background 108 + // and the invites won't exist yet. After T5166 we can show a 109 + // better progress bar. 110 + return id(new AphrontRedirectResponse()) 111 + ->setURI($this->getApplicationURI()); 77 112 } 78 113 } 79 114 }
+61
src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkerManagementExecuteWorkflow 4 + extends PhabricatorWorkerManagementWorkflow { 5 + 6 + protected function didConstruct() { 7 + $this 8 + ->setName('execute') 9 + ->setExamples('**execute** --id __id__') 10 + ->setSynopsis( 11 + pht( 12 + 'Execute a task explicitly. This command ignores leases, is '. 13 + 'dangerous, and may cause work to be performed twice.')) 14 + ->setArguments($this->getTaskSelectionArguments()); 15 + } 16 + 17 + public function execute(PhutilArgumentParser $args) { 18 + $console = PhutilConsole::getConsole(); 19 + $tasks = $this->loadTasks($args); 20 + 21 + foreach ($tasks as $task) { 22 + $can_execute = !$task->isArchived(); 23 + if (!$can_execute) { 24 + $console->writeOut( 25 + "**<bg:yellow> %s </bg>** %s\n", 26 + pht('ARCHIVED'), 27 + pht( 28 + '%s is already archived, and can not be executed.', 29 + $this->describeTask($task))); 30 + continue; 31 + } 32 + 33 + // NOTE: This ignores leases, maybe it should respect them without 34 + // a parameter like --force? 35 + 36 + $task->setLeaseOwner(null); 37 + $task->setLeaseExpires(PhabricatorTime::getNow()); 38 + $task->save(); 39 + 40 + $task_data = id(new PhabricatorWorkerTaskData())->loadOneWhere( 41 + 'id = %d', 42 + $task->getDataID()); 43 + $task->setData($task_data->getData()); 44 + 45 + $id = $task->getID(); 46 + $class = $task->getTaskClass(); 47 + 48 + $console->writeOut("Executing task {$id} ({$class})..."); 49 + 50 + $task->executeTask(); 51 + $ex = $task->getExecutionException(); 52 + 53 + if ($ex) { 54 + throw $ex; 55 + } 56 + } 57 + 58 + return 0; 59 + } 60 + 61 + }