mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-20 12:30:56 +01:00
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
This commit is contained in:
parent
ae59760222
commit
6f90fbdef8
9 changed files with 210 additions and 10 deletions
|
@ -1361,6 +1361,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAuthInviteSearchEngine' => 'applications/auth/query/PhabricatorAuthInviteSearchEngine.php',
|
||||
'PhabricatorAuthInviteTestCase' => 'applications/auth/factor/__tests__/PhabricatorAuthInviteTestCase.php',
|
||||
'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php',
|
||||
'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php',
|
||||
'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php',
|
||||
'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php',
|
||||
'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php',
|
||||
|
@ -2626,6 +2627,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php',
|
||||
'PhabricatorWorkerLeaseQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php',
|
||||
'PhabricatorWorkerManagementCancelWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementCancelWorkflow.php',
|
||||
'PhabricatorWorkerManagementExecuteWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php',
|
||||
'PhabricatorWorkerManagementFloodWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFloodWorkflow.php',
|
||||
'PhabricatorWorkerManagementFreeWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementFreeWorkflow.php',
|
||||
'PhabricatorWorkerManagementRetryWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php',
|
||||
|
@ -4591,6 +4593,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorAuthInviteSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhabricatorAuthInviteTestCase' => 'PhabricatorTestCase',
|
||||
'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException',
|
||||
'PhabricatorAuthInviteWorker' => 'PhabricatorWorker',
|
||||
'PhabricatorAuthLinkController' => 'PhabricatorAuthController',
|
||||
'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController',
|
||||
'PhabricatorAuthLoginController' => 'PhabricatorAuthController',
|
||||
|
@ -5959,6 +5962,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorWorkerLeaseQuery' => 'PhabricatorQuery',
|
||||
'PhabricatorWorkerManagementCancelWorkflow' => 'PhabricatorWorkerManagementWorkflow',
|
||||
'PhabricatorWorkerManagementExecuteWorkflow' => 'PhabricatorWorkerManagementWorkflow',
|
||||
'PhabricatorWorkerManagementFloodWorkflow' => 'PhabricatorWorkerManagementWorkflow',
|
||||
'PhabricatorWorkerManagementFreeWorkflow' => 'PhabricatorWorkerManagementWorkflow',
|
||||
'PhabricatorWorkerManagementRetryWorkflow' => 'PhabricatorWorkerManagementWorkflow',
|
||||
|
|
|
@ -189,4 +189,22 @@ final class PhabricatorAuthInviteAction extends Phobject {
|
|||
return $results;
|
||||
}
|
||||
|
||||
public function sendInvite(PhabricatorUser $actor, $template) {
|
||||
if (!$this->willSend()) {
|
||||
throw new Exception(pht('Invite action is not a send action!'));
|
||||
}
|
||||
|
||||
if (!preg_match('/{\$INVITE_URI}/', $template)) {
|
||||
throw new Exception(pht('Invite template does not include invite URI!'));
|
||||
}
|
||||
|
||||
PhabricatorWorker::scheduleTask(
|
||||
'PhabricatorAuthInviteWorker',
|
||||
array(
|
||||
'address' => $this->getEmailAddress(),
|
||||
'template' => $template,
|
||||
'authorPHID' => $actor->getPHID(),
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -35,9 +35,10 @@ final class PhabricatorAuthInviteEngine extends Phobject {
|
|||
public function processInviteCode($code) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$invite = id(new PhabricatorAuthInvite())->loadOneWhere(
|
||||
'verificationHash = %s',
|
||||
PhabricatorHash::digestForIndex($code));
|
||||
$invite = id(new PhabricatorAuthInviteQuery())
|
||||
->setViewer($viewer)
|
||||
->withVerificationCodes(array($code))
|
||||
->executeOne();
|
||||
if (!$invite) {
|
||||
throw id(new PhabricatorAuthInviteInvalidException(
|
||||
pht('Bad Invite Code'),
|
||||
|
|
|
@ -88,6 +88,7 @@ final class PhabricatorAuthInviteQuery
|
|||
foreach ($this->verificationCodes as $code) {
|
||||
$hashes[] = PhabricatorHash::digestForIndex($code);
|
||||
}
|
||||
|
||||
$where[] = qsprintf(
|
||||
$conn_r,
|
||||
'verificationHash IN (%Ls)',
|
||||
|
|
|
@ -86,7 +86,21 @@ final class PhabricatorAuthInviteSearchEngine
|
|||
);
|
||||
}
|
||||
|
||||
$table = new AphrontTableView($rows);
|
||||
$table = id(new AphrontTableView($rows))
|
||||
->setHeaders(
|
||||
array(
|
||||
pht('Email Address'),
|
||||
pht('Sent By'),
|
||||
pht('Accepted By'),
|
||||
pht('Invited'),
|
||||
))
|
||||
->setColumnClasses(
|
||||
array(
|
||||
'',
|
||||
'',
|
||||
'wide',
|
||||
'right',
|
||||
));
|
||||
|
||||
return id(new PHUIObjectBoxView())
|
||||
->setHeaderText(pht('Email Invitations'))
|
||||
|
|
|
@ -38,15 +38,21 @@ final class PhabricatorAuthInvite
|
|||
PhabricatorAuthInvitePHIDType::TYPECONST);
|
||||
}
|
||||
|
||||
public function regenerateVerificationCode() {
|
||||
$this->verificationCode = Filesystem::readRandomCharacters(16);
|
||||
$this->verificationHash = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVerificationCode() {
|
||||
if (!$this->getVerificationHash()) {
|
||||
if (!$this->verificationCode) {
|
||||
if ($this->verificationHash) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Verification code can not be regenerated after an invite is '.
|
||||
'created.'));
|
||||
}
|
||||
$this->verificationCode = Filesystem::readRandomCharacters(16);
|
||||
$this->regenerateVerificationCode();
|
||||
}
|
||||
return $this->verificationCode;
|
||||
}
|
||||
|
|
60
src/applications/auth/worker/PhabricatorAuthInviteWorker.php
Normal file
60
src/applications/auth/worker/PhabricatorAuthInviteWorker.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorAuthInviteWorker
|
||||
extends PhabricatorWorker {
|
||||
|
||||
protected function doWork() {
|
||||
$data = $this->getTaskData();
|
||||
$viewer = PhabricatorUser::getOmnipotentUser();
|
||||
|
||||
$address = idx($data, 'address');
|
||||
$author_phid = idx($data, 'authorPHID');
|
||||
|
||||
$author = id(new PhabricatorPeopleQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($author_phid))
|
||||
->executeOne();
|
||||
if (!$author) {
|
||||
throw new PhabricatorWorkerPermanentFailureException(
|
||||
pht('Invite has invalid author PHID ("%s").', $author_phid));
|
||||
}
|
||||
|
||||
$invite = id(new PhabricatorAuthInviteQuery())
|
||||
->setViewer($viewer)
|
||||
->withEmailAddresses(array($address))
|
||||
->executeOne();
|
||||
if ($invite) {
|
||||
// If we're inviting a user who has already been invited, we just
|
||||
// regenerate their invite code.
|
||||
$invite->regenerateVerificationCode();
|
||||
} else {
|
||||
// Otherwise, we're creating a new invite.
|
||||
$invite = id(new PhabricatorAuthInvite())
|
||||
->setEmailAddress($address);
|
||||
}
|
||||
|
||||
// Whether this is a new invite or not, tag this most recent author as
|
||||
// the invite author.
|
||||
$invite->setAuthorPHID($author_phid);
|
||||
|
||||
$code = $invite->getVerificationCode();
|
||||
$invite_uri = '/auth/invite/'.$code.'/';
|
||||
$invite_uri = PhabricatorEnv::getProductionURI($invite_uri);
|
||||
|
||||
$template = idx($data, 'template');
|
||||
$template = str_replace('{$INVITE_URI}', $invite_uri, $template);
|
||||
|
||||
$invite->save();
|
||||
|
||||
$mail = id(new PhabricatorMetaMTAMail())
|
||||
->addRawTos(array($invite->getEmailAddress()))
|
||||
->setForceDelivery(true)
|
||||
->setSubject(
|
||||
pht(
|
||||
'[Phabricator] %s has invited you to join Phabricator',
|
||||
$author->getFullName()))
|
||||
->setBody($template)
|
||||
->saveAndSend();
|
||||
}
|
||||
|
||||
}
|
|
@ -44,9 +44,8 @@ final class PhabricatorPeopleInviteSendController
|
|||
|
||||
$any_valid = false;
|
||||
$all_valid = true;
|
||||
$action_send = PhabricatorAuthInviteAction::ACTION_SEND;
|
||||
foreach ($actions as $action) {
|
||||
if ($action->getAction() == $action_send) {
|
||||
if ($action->willSend()) {
|
||||
$any_valid = true;
|
||||
} else {
|
||||
$all_valid = false;
|
||||
|
@ -72,8 +71,44 @@ final class PhabricatorPeopleInviteSendController
|
|||
}
|
||||
|
||||
if ($any_valid && $request->getBool('confirm')) {
|
||||
throw new Exception(
|
||||
pht('TODO: This workflow is not yet fully implemented.'));
|
||||
|
||||
// TODO: The copywriting on this mail could probably be more
|
||||
// engaging and we could have a fancy HTML version.
|
||||
|
||||
$template = array();
|
||||
$template[] = pht(
|
||||
'%s has invited you to join Phabricator.',
|
||||
$viewer->getFullName());
|
||||
|
||||
if (strlen(trim($message))) {
|
||||
$template[] = $message;
|
||||
}
|
||||
|
||||
$template[] = pht(
|
||||
'To register an account and get started, follow this link:');
|
||||
|
||||
// This isn't a variable; it will be replaced later on in the
|
||||
// daemons once they generate the URI.
|
||||
$template[] = '{$INVITE_URI}';
|
||||
|
||||
$template[] = pht(
|
||||
'If you already have an account, you can follow the link to '.
|
||||
'quickly verify this email address.');
|
||||
|
||||
$template = implode("\n\n", $template);
|
||||
|
||||
foreach ($actions as $action) {
|
||||
if ($action->willSend()) {
|
||||
$action->sendInvite($viewer, $template);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This is a bit anticlimactic. We don't really have anything
|
||||
// to show the user because the action is happening in the background
|
||||
// and the invites won't exist yet. After T5166 we can show a
|
||||
// better progress bar.
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI($this->getApplicationURI());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorWorkerManagementExecuteWorkflow
|
||||
extends PhabricatorWorkerManagementWorkflow {
|
||||
|
||||
protected function didConstruct() {
|
||||
$this
|
||||
->setName('execute')
|
||||
->setExamples('**execute** --id __id__')
|
||||
->setSynopsis(
|
||||
pht(
|
||||
'Execute a task explicitly. This command ignores leases, is '.
|
||||
'dangerous, and may cause work to be performed twice.'))
|
||||
->setArguments($this->getTaskSelectionArguments());
|
||||
}
|
||||
|
||||
public function execute(PhutilArgumentParser $args) {
|
||||
$console = PhutilConsole::getConsole();
|
||||
$tasks = $this->loadTasks($args);
|
||||
|
||||
foreach ($tasks as $task) {
|
||||
$can_execute = !$task->isArchived();
|
||||
if (!$can_execute) {
|
||||
$console->writeOut(
|
||||
"**<bg:yellow> %s </bg>** %s\n",
|
||||
pht('ARCHIVED'),
|
||||
pht(
|
||||
'%s is already archived, and can not be executed.',
|
||||
$this->describeTask($task)));
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE: This ignores leases, maybe it should respect them without
|
||||
// a parameter like --force?
|
||||
|
||||
$task->setLeaseOwner(null);
|
||||
$task->setLeaseExpires(PhabricatorTime::getNow());
|
||||
$task->save();
|
||||
|
||||
$task_data = id(new PhabricatorWorkerTaskData())->loadOneWhere(
|
||||
'id = %d',
|
||||
$task->getDataID());
|
||||
$task->setData($task_data->getData());
|
||||
|
||||
$id = $task->getID();
|
||||
$class = $task->getTaskClass();
|
||||
|
||||
$console->writeOut("Executing task {$id} ({$class})...");
|
||||
|
||||
$task->executeTask();
|
||||
$ex = $task->getExecutionException();
|
||||
|
||||
if ($ex) {
|
||||
throw $ex;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue