1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-30 01:10:58 +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:
epriestley 2015-02-11 06:06:09 -08:00
parent ae59760222
commit 6f90fbdef8
9 changed files with 210 additions and 10 deletions

View file

@ -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',

View file

@ -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(),
));
}
}

View file

@ -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'),

View file

@ -88,6 +88,7 @@ final class PhabricatorAuthInviteQuery
foreach ($this->verificationCodes as $code) {
$hashes[] = PhabricatorHash::digestForIndex($code);
}
$where[] = qsprintf(
$conn_r,
'verificationHash IN (%Ls)',

View file

@ -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'))

View file

@ -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;
}

View 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();
}
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}