1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-25 16:22:43 +01:00

Allow users to have multiple email addresses, and verify emails

Summary:
  - Move email to a separate table.
  - Migrate existing email to new storage.
  - Allow users to add and remove email addresses.
  - Allow users to verify email addresses.
  - Allow users to change their primary email address.
  - Convert all the registration/reset/login code to understand these changes.
  - There are a few security considerations here but I think I've addressed them. Principally, it is important to never let a user acquire a verified email address they don't actually own. We ensure this by tightening the scoping of token generation rules to be (user, email) specific.
  - This should have essentially zero impact on Facebook, but may require some minor changes in the registration code -- I don't exactly remember how it is set up.

Not included here (next steps):

  - Allow configuration to restrict email to certain domains.
  - Allow configuration to require validated email.

Test Plan:
This is a fairly extensive, difficult-to-test change.

  - From "Email Addresses" interface:
    - Added new email (verified email verifications sent).
    - Changed primary email (verified old/new notificactions sent).
    - Resent verification emails (verified they sent).
    - Removed email.
    - Tried to add already-owned email.
  - Created new users with "accountadmin". Edited existing users with "accountadmin".
  - Created new users with "add_user.php".
  - Created new users with web interface.
  - Clicked welcome email link, verified it verified email.
  - Reset password.
  - Linked/unlinked oauth accounts.
  - Logged in with oauth account.
  - Logged in with email.
  - Registered with Oauth account.
  - Tried to register with OAuth account with duplicate email.
  - Verified errors for email verification with bad tokens, etc.

Reviewers: btrahan, vrana, jungejason

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1184

Differential Revision: https://secure.phabricator.com/D2393
This commit is contained in:
epriestley 2012-05-07 10:29:33 -07:00
parent 803dea1517
commit 87207b2f4e
38 changed files with 900 additions and 140 deletions

View file

@ -0,0 +1,12 @@
CREATE TABLE {$NAMESPACE}_user.user_email (
`id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY,
userPHID varchar(64) collate utf8_bin NOT NULL,
address varchar(128) collate utf8_general_ci NOT NULL,
isVerified bool not null default 0,
isPrimary bool not null default 0,
verificationCode varchar(64) collate utf8_bin,
dateCreated int unsigned not null,
dateModified int unsigned not null,
KEY (userPHID, isPrimary),
UNIQUE KEY (address)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View file

@ -0,0 +1,49 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
echo "Migrating user emails...\n";
$table = new PhabricatorUser();
$conn = $table->establishConnection('r');
$emails = queryfx_all(
$conn,
'SELECT phid, email FROM %T',
$table->getTableName());
$emails = ipull($emails, 'email', 'phid');
$etable = new PhabricatorUserEmail();
$econn = $etable->establishConnection('w');
foreach ($emails as $phid => $email) {
// NOTE: Grandfather all existing email in as primary / verified. We generate
// verification codes because they are used for password resets, etc.
echo "Migrating '{$phid}'...\n";
queryfx(
$econn,
'INSERT INTO %T (userPHID, address, verificationCode, isVerified, isPrimary)
VALUES (%s, %s, %s, 1, 1)',
$etable->getTableName(),
$phid,
$email,
PhabricatorFile::readRandomCharacters(24));
}
echo "Done.\n";

View file

@ -0,0 +1 @@
ALTER TABLE {$NAMESPACE}_user.user DROP email;

View file

@ -55,6 +55,8 @@ if (!$user) {
}
$user = new PhabricatorUser();
$user->setUsername($username);
$is_new = true;
} else {
$original = clone $user;
@ -66,6 +68,8 @@ if (!$user) {
echo "Cancelled.\n";
exit(1);
}
$is_new = false;
}
$user_realname = $user->getRealName();
@ -79,30 +83,28 @@ $realname = nonempty(
$user_realname);
$user->setRealName($realname);
$user_email = $user->getEmail();
if (strlen($user_email)) {
$email_prompt = ' ['.$user_email.']';
} else {
$email_prompt = '';
}
// When creating a new user we prompt for an email address; when editing an
// existing user we just skip this because it would be quite involved to provide
// a reasonable CLI interface for editing multiple addresses and managing email
// verification and primary addresses.
do {
$email = nonempty(
phutil_console_prompt("Enter user email address{$email_prompt}:"),
$user_email);
$duplicate = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$email);
if ($duplicate && $duplicate->getID() != $user->getID()) {
$duplicate_username = $duplicate->getUsername();
echo "ERROR: There is already a user with that email address ".
"({$duplicate_username}). Each user must have a unique email ".
"address.\n";
} else {
break;
}
} while (true);
$user->setEmail($email);
$new_email = null;
if ($is_new) {
do {
$email = phutil_console_prompt("Enter user email address:");
$duplicate = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($duplicate) {
echo "ERROR: There is already a user with that email address. ".
"Each user must have a unique email address.\n";
} else {
break;
}
} while (true);
$new_email = $email;
}
$changed_pass = false;
// This disables local echo, so the user's password is not shown as they type
@ -126,7 +128,9 @@ $tpl = "%12s %-30s %-30s\n";
printf($tpl, null, 'OLD VALUE', 'NEW VALUE');
printf($tpl, 'Username', $original->getUsername(), $user->getUsername());
printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName());
printf($tpl, 'Email', $original->getEmail(), $user->getEmail());
if ($new_email) {
printf($tpl, 'Email', '', $new_email);
}
printf($tpl, 'Password', null,
($changed_pass !== false)
? 'Updated'
@ -153,4 +157,13 @@ if ($changed_pass !== false) {
$user->save();
}
if ($new_email) {
id(new PhabricatorUserEmail())
->setUserPHID($user->getPHID())
->setAddress($new_email)
->setIsVerified(1)
->setIsPrimary(1)
->save();
}
echo "Saved changes.\n";

View file

@ -50,20 +50,26 @@ if ($existing_user) {
"There is already a user with the username '{$username}'!");
}
$existing_user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$existing_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($existing_user) {
if ($existing_email) {
throw new Exception(
"There is already a user with the email '{$email}'!");
}
$user = new PhabricatorUser();
$user->setUsername($username);
$user->setEmail($email);
$user->setRealname($realname);
$user->save();
$email_object = id(new PhabricatorUserEmail())
->setUserPHID($user->getPHID())
->setAddress($email)
->setIsVerified(1)
->setIsPrimary(1)
->save();
$user->sendWelcomeEmail($admin);
echo "Created user '{$username}' (realname='{$realname}', email='{$email}').\n";

View file

@ -595,6 +595,7 @@ phutil_register_library_map(array(
'PhabricatorEdgeQuery' => 'infrastructure/edges/query/edge',
'PhabricatorEmailLoginController' => 'applications/auth/controller/email',
'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken',
'PhabricatorEmailVerificationController' => 'applications/people/controller/emailverification',
'PhabricatorEnv' => 'infrastructure/env',
'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__',
'PhabricatorErrorExample' => 'applications/uiexample/examples/error',
@ -946,6 +947,7 @@ phutil_register_library_map(array(
'PhabricatorUserAccountSettingsPanelController' => 'applications/people/controller/settings/panels/account',
'PhabricatorUserConduitSettingsPanelController' => 'applications/people/controller/settings/panels/conduit',
'PhabricatorUserDAO' => 'applications/people/storage/base',
'PhabricatorUserEmail' => 'applications/people/storage/email',
'PhabricatorUserEmailPreferenceSettingsPanelController' => 'applications/people/controller/settings/panels/emailpref',
'PhabricatorUserEmailSettingsPanelController' => 'applications/people/controller/settings/panels/email',
'PhabricatorUserLog' => 'applications/people/storage/log',
@ -1534,6 +1536,7 @@ phutil_register_library_map(array(
'PhabricatorEdgeQuery' => 'PhabricatorQuery',
'PhabricatorEmailLoginController' => 'PhabricatorAuthController',
'PhabricatorEmailTokenController' => 'PhabricatorAuthController',
'PhabricatorEmailVerificationController' => 'PhabricatorPeopleController',
'PhabricatorEnvTestCase' => 'PhabricatorTestCase',
'PhabricatorErrorExample' => 'PhabricatorUIExample',
'PhabricatorEvent' => 'PhutilEvent',
@ -1819,6 +1822,7 @@ phutil_register_library_map(array(
'PhabricatorUserAccountSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserConduitSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserEmail' => 'PhabricatorUserDAO',
'PhabricatorUserEmailPreferenceSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserEmailSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserLog' => 'PhabricatorUserDAO',

View file

@ -433,6 +433,9 @@ class AphrontDefaultApplicationConfiguration
'testpaymentform/' => 'PhortuneStripeTestPaymentFormController',
),
),
'/emailverify/(?P<code>[^/]+)/' =>
'PhabricatorEmailVerificationController',
);
}

View file

@ -31,7 +31,7 @@ class AphrontRedirectResponse extends AphrontResponse {
}
public function getURI() {
return $this->uri;
return (string)$this->uri;
}
public function getHeaders() {

View file

@ -57,17 +57,24 @@ final class PhabricatorEmailLoginController
// it expensive to fish for valid email addresses while giving the user
// a better error if they goof their email.
$target_user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_user) {
$errors[] = "There is no account associated with that email address.";
$e_email = "Invalid";
}
if (!$errors) {
$uri = $target_user->getEmailLoginURI();
$uri = $target_user->getEmailLoginURI($target_email);
if ($is_serious) {
$body = <<<EOBODY
You can use this link to reset your Phabricator password:

View file

@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/metamta/storage/mail');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/form/base');

View file

@ -55,11 +55,31 @@ final class PhabricatorEmailTokenController
$token = $this->token;
$email = $request->getStr('email');
$target_user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
// NOTE: We need to bind verification to **addresses**, not **users**,
// because we verify addresses when they're used to login this way, and if
// we have a user-based verification you can:
//
// - Add some address you do not own;
// - request a password reset;
// - change the URI in the email to the address you don't own;
// - login via the email link; and
// - get a "verified" address you don't control.
$target_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if (!$target_user || !$target_user->validateEmailToken($token)) {
$target_user = null;
if ($target_email) {
$target_user = id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$target_email->getUserPHID());
}
if (!$target_email ||
!$target_user ||
!$target_user->validateEmailToken($target_email, $token)) {
$view = new AphrontRequestFailureView();
$view->setHeader('Unable to Login');
$view->appendChild(
@ -71,19 +91,32 @@ final class PhabricatorEmailTokenController
'<div class="aphront-failure-continue">'.
'<a class="button" href="/login/email/">Send Another Email</a>'.
'</div>');
return $this->buildStandardPageResponse(
$view,
array(
'title' => 'Email Sent',
'title' => 'Login Failure',
));
}
// Verify email so that clicking the link in the "Welcome" email is good
// enough, without requiring users to go through a second round of email
// verification.
$target_email->setIsVerified(1);
$target_email->save();
$session_key = $target_user->establishSession('web');
$request->setCookie('phusr', $target_user->getUsername());
$request->setCookie('phsid', $session_key);
if (PhabricatorEnv::getEnvConfig('account.editable')) {
$next = '/settings/page/password/?token='.$token;
$next = (string)id(new PhutilURI('/settings/page/password/'))
->setQueryParams(
array(
'token' => $token,
'email' => $email,
));
} else {
$next = '/';
}

View file

@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/page/failure');

View file

@ -113,9 +113,7 @@ final class PhabricatorLoginController
$username_or_email);
if (!$user) {
$user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$username_or_email);
$user = PhabricatorUser::loadOneWithEmailAddress($username_or_email);
}
if (!$errors) {

View file

@ -176,8 +176,8 @@ final class PhabricatorOAuthLoginController
$oauth_email = $provider->retrieveUserEmail();
if ($oauth_email) {
$known_email = id(new PhabricatorUser())
->loadOneWhere('email = %s', $oauth_email);
$known_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $oauth_email);
if ($known_email) {
$dialog = new AphrontDialogView();
$dialog->setUser($current_user);

View file

@ -13,6 +13,7 @@ phutil_require_module('phabricator', 'aphront/writeguard');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/auth/oauth/provider/base');
phutil_require_module('phabricator', 'applications/auth/view/oauthfailure');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo');
phutil_require_module('phabricator', 'infrastructure/env');

View file

@ -33,7 +33,8 @@ final class PhabricatorOAuthDefaultRegistrationController
$user->setUsername($provider->retrieveUserAccountName());
$user->setRealName($provider->retrieveUserRealName());
$user->setEmail($provider->retrieveUserEmail());
$new_email = $provider->retrieveUserEmail();
if ($request->isFormPost()) {
@ -49,9 +50,9 @@ final class PhabricatorOAuthDefaultRegistrationController
$e_username = null;
}
if ($user->getEmail() === null) {
$user->setEmail($request->getStr('email'));
if (!strlen($user->getEmail())) {
if (!$new_email) {
$new_email = trim($request->getStr('email'));
if (!$new_email) {
$e_email = 'Required';
$errors[] = 'Email is required.';
} else {
@ -84,12 +85,29 @@ final class PhabricatorOAuthDefaultRegistrationController
try {
$user->save();
// NOTE: We don't verify OAuth email addresses by default because
// OAuth providers might associate email addresses with accounts that
// haven't actually verified they own them. We could selectively
// auto-verify some providers that we trust here, but the stakes for
// verifying an email address are high because having a corporate
// address at a company is sometimes the key to the castle.
$new_email = id(new PhabricatorUserEmail())
->setUserPHID($user->getPHID())
->setAddress($new_email)
->setIsPrimary(1)
->setIsVerified(0)
->save();
$oauth_info->setUserID($user->getID());
$oauth_info->save();
$session_key = $user->establishSession('web');
$request->setCookie('phusr', $user->getUsername());
$request->setCookie('phsid', $session_key);
$new_email->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI('/');
} catch (AphrontQueryDuplicateKeyException $exception) {
@ -97,9 +115,9 @@ final class PhabricatorOAuthDefaultRegistrationController
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$user->getEmail());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$new_email);
if ($same_username) {
$e_username = 'Duplicate';

View file

@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/auth/controller/oauthregistration/base');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');

View file

@ -26,7 +26,6 @@ abstract class ConduitAPI_user_Method extends ConduitAPIMethod {
'phid' => $user->getPHID(),
'userName' => $user->getUserName(),
'realName' => $user->getRealName(),
'email' => $user->getEmail(),
'image' => $user->loadProfileImageURI(),
'uri' => PhabricatorEnv::getURI('/p/'.$user->getUsername().'/'),
);

View file

@ -672,8 +672,7 @@ abstract class DifferentialFieldSpecification {
$object_map = array();
$users = id(new PhabricatorUser())->loadAllWhere(
'(username IN (%Ls)) OR (email IN (%Ls))',
$value,
'(username IN (%Ls))',
$value);
$user_map = mpull($users, 'getPHID', 'getUsername');

View file

@ -108,6 +108,11 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
return $this;
}
public function addRawTos(array $raw_email) {
$this->setParam('raw-to', $raw_email);
return $this;
}
public function addCCs(array $phids) {
$phids = array_unique($phids);
$this->setParam('cc', $phids);
@ -367,9 +372,12 @@ final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
$handles,
$exclude);
if ($emails) {
$add_to = $emails;
$add_to = array_merge($add_to, $emails);
}
break;
case 'raw-to':
$add_to = array_merge($add_to, $value);
break;
case 'cc':
$emails = $this->getDeliverableEmailsFromHandles(
$value,

View file

@ -274,9 +274,7 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
$from = idx($this->headers, 'from');
$from = $this->getRawEmailAddress($from);
$user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$from);
$user = PhabricatorUser::loadOneWithEmailAddress($from);
// If Phabricator is configured to allow "Reply-To" authentication, try
// the "Reply-To" address if we failed to match the "From" address.
@ -287,9 +285,7 @@ final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
$reply_to = idx($this->headers, 'reply-to');
$reply_to = $this->getRawEmailAddress($reply_to);
if ($reply_to) {
$user = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$reply_to);
$user = PhabricatorUser::loadOneWithEmailAddress($reply_to);
}
}

View file

@ -123,13 +123,20 @@ final class PhabricatorPeopleEditController
$welcome_checked = true;
$new_email = null;
$request = $this->getRequest();
if ($request->isFormPost()) {
$welcome_checked = $request->getInt('welcome');
if (!$user->getID()) {
$user->setUsername($request->getStr('username'));
$user->setEmail($request->getStr('email'));
$new_email = $request->getStr('email');
if (!strlen($new_email)) {
$errors[] = 'Email is required.';
$e_email = 'Required';
}
if ($request->getStr('role') == 'agent') {
$user->setIsSystemAgent(true);
@ -154,13 +161,6 @@ final class PhabricatorPeopleEditController
$e_realname = null;
}
if (!strlen($user->getEmail())) {
$errors[] = 'Email is required.';
$e_email = 'Required';
} else {
$e_email = null;
}
if (!$errors) {
try {
$is_new = !$user->getID();
@ -168,6 +168,14 @@ final class PhabricatorPeopleEditController
$user->save();
if ($is_new) {
$email = id(new PhabricatorUserEmail())
->setUserPHID($user->getPHID())
->setAddress($new_email)
->setIsPrimary(1)
->setIsVerified(0)
->save();
$log = PhabricatorUserLog::newLog(
$admin,
$user,
@ -187,8 +195,8 @@ final class PhabricatorPeopleEditController
$same_username = id(new PhabricatorUser())
->loadOneWhere('username = %s', $user->getUsername());
$same_email = id(new PhabricatorUser())
->loadOneWhere('email = %s', $user->getEmail());
$same_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $new_email);
if ($same_username) {
$e_username = 'Duplicate';
@ -236,15 +244,19 @@ final class PhabricatorPeopleEditController
->setLabel('Real Name')
->setName('realname')
->setValue($user->getRealName())
->setError($e_realname))
->appendChild(
->setError($e_realname));
if (!$user->getID()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setDisabled($is_immutable)
->setValue($user->getEmail())
->setError($e_email))
->appendChild($this->getRoleInstructions());
->setValue($new_email)
->setError($e_email));
}
$form->appendChild($this->getRoleInstructions());
if (!$user->getID()) {
$form

View file

@ -9,6 +9,7 @@
phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/people/controller/base');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/log');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'infrastructure/env');

View file

@ -0,0 +1,81 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorEmailVerificationController
extends PhabricatorPeopleController {
private $code;
public function willProcessRequest(array $data) {
$this->code = $data['code'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND verificationCode = %s',
$user->getPHID(),
$this->code);
$settings_link = phutil_render_tag(
'a',
array(
'href' => '/settings/page/email/',
),
'Return to Email Settings');
$settings_link = '<br /><p><strong>'.$settings_link.'</strong></p>';
if (!$email) {
$content = id(new AphrontErrorView())
->setTitle('Unable To Verify')
->appendChild(
'<p>The verification code is incorrect, the email address has '.
'been removed, or the email address is owned by another user. Make '.
'sure you followed the link in the email correctly.</p>');
} else if ($email->getIsVerified()) {
$content = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle('Address Already Verified')
->appendChild(
'<p>This email address has already been verified.</p>'.
$settings_link);
} else {
$guard = AphrontWriteGuard::beginScopedUnguardedWrites();
$email->setIsVerified(1);
$email->save();
unset($guard);
$content = id(new AphrontErrorView())
->setSeverity(AphrontErrorView::SEVERITY_NOTICE)
->setTitle('Address Verified')
->appendChild(
'<p>This email address has now been verified. Thanks!</p>'.
$settings_link);
}
return $this->buildStandardPageResponse(
$content,
array(
'title' => 'Verify Email',
));
}
}

View file

@ -0,0 +1,18 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'aphront/writeguard');
phutil_require_module('phabricator', 'applications/people/controller/base');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorEmailVerificationController.php');

View file

@ -94,7 +94,7 @@ final class PhabricatorUserSettingsController
->addFilter('profile', 'Profile')
->addSpacer()
->addLabel('Email')
->addFilter('email', 'Email Address')
->addFilter('email', 'Email Addresses')
->addFilter('emailpref', 'Email Preferences')
->addSpacer()
->addLabel('Authentication');

View file

@ -25,72 +25,317 @@ final class PhabricatorUserEmailSettingsPanelController
$user = $request->getUser();
$editable = $this->getAccountEditable();
$e_email = true;
$errors = array();
if ($request->isFormPost()) {
if (!$editable) {
return new Aphront400Response();
$uri = $request->getRequestURI();
$uri->setQueryParams(array());
if ($editable) {
$new = $request->getStr('new');
if ($new) {
return $this->returnNewAddressResponse($uri, $new);
}
$user->setEmail($request->getStr('email'));
$delete = $request->getInt('delete');
if ($delete) {
return $this->returnDeleteAddressResponse($uri, $delete);
}
}
if (!strlen($user->getEmail())) {
$errors[] = 'You must enter an e-mail address.';
$verify = $request->getInt('verify');
if ($verify) {
return $this->returnVerifyAddressResponse($uri, $verify);
}
$primary = $request->getInt('primary');
if ($primary) {
return $this->returnPrimaryAddressResponse($uri, $primary);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID = %s',
$user->getPHID());
$rowc = array();
$rows = array();
foreach ($emails as $email) {
if ($email->getIsPrimary()) {
$action = phutil_render_tag(
'a',
array(
'class' => 'button small disabled',
),
'Primary');
$remove = $action;
$rowc[] = 'highlighted';
} else {
if ($email->getIsVerified()) {
$action = javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('primary', $email->getID()),
'sigil' => 'workflow',
),
'Make Primary');
} else {
$action = javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('verify', $email->getID()),
'sigil' => 'workflow',
),
'Verify');
}
$remove = javelin_render_tag(
'a',
array(
'class' => 'button small grey',
'href' => $uri->alter('delete', $email->getID()),
'sigil' => 'workflow'
),
'Remove');
$rowc[] = null;
}
$rows[] = array(
phutil_escape_html($email->getAddress()),
$action,
$remove,
);
}
$table = new AphrontTableView($rows);
$table->setHeaders(
array(
'Email',
'Status',
'Remove',
));
$table->setColumnClasses(
array(
'wide',
'action',
'action',
));
$table->setRowClasses($rowc);
$table->setColumnVisibility(
array(
true,
true,
$editable,
));
$view = new AphrontPanelView();
if ($editable) {
$view->addButton(
javelin_render_tag(
'a',
array(
'href' => $uri->alter('new', 'true'),
'class' => 'green button',
'sigil' => 'workflow',
),
'Add New Address'));
}
$view->setHeader('Email Addresses');
$view->appendChild($table);
return $view;
}
private function returnNewAddressResponse(PhutilURI $uri, $new) {
$request = $this->getRequest();
$user = $request->getUser();
$e_email = true;
$email = trim($request->getStr('email'));
$errors = array();
if ($request->isDialogFormPost()) {
if ($new == 'verify') {
// The user clicked "Done" from the "an email has been sent" dialog.
return id(new AphrontReloadResponse())->setURI($uri);
}
if (!strlen($email)) {
$e_email = 'Required';
$errors[] = 'Email is required.';
}
if (!$errors) {
$user->save();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/email/?saved=true');
$object = id(new PhabricatorUserEmail())
->setUserPHID($user->getPHID())
->setAddress($email)
->setIsVerified(0)
->setIsPrimary(0);
try {
$object->save();
$object->sendVerificationEmail($user);
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('new', 'verify')
->setTitle('Verification Email Sent')
->appendChild(
'<p>A verification email has been sent. Click the link in the '.
'email to verify your address.</p>')
->setSubmitURI($uri)
->addSubmitButton('Done');
return id(new AphrontDialogResponse())->setDialog($dialog);
} catch (AphrontQueryDuplicateKeyException $ex) {
$email = 'Duplicate';
$errors[] = 'Another user already has this email.';
}
}
}
$notice = null;
if (!$errors) {
if ($request->getStr('saved')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Changes Saved');
$notice->appendChild('<p>Your changes have been saved.</p>');
}
} else {
$notice = new AphrontErrorView();
$notice->setTitle('Form Errors');
$notice->setErrors($errors);
if ($errors) {
$errors = id(new AphrontErrorView())
->setWidth(AphrontErrorView::WIDTH_DIALOG)
->setErrors($errors);
}
$form = new AphrontFormView();
$form
->setUser($user)
$form = id(new AphrontFormLayoutView())
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setDisabled(!$editable)
->setCaption(
'Note: there is no email validation yet; double-check your '.
'typing.')
->setValue($user->getEmail())
->setValue($request->getStr('email'))
->setError($e_email));
if ($editable) {
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('new', 'true')
->setTitle('New Address')
->appendChild($errors)
->appendChild($form)
->addSubmitButton('Save')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(PhutilURI $uri, $email_id) {
$request = $this->getRequest();
$user = $request->getUser();
// NOTE: You can only delete your own email addresses, and you can not
// delete your primary address.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
$panel = new AphrontPanelView();
$panel->setHeader('Email Settings');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
if ($request->isFormPost()) {
$email->delete();
return id(new AphrontRedirectResponse())->setURI($uri);
}
return id(new AphrontNullView())
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('delete', $email_id)
->setTitle("Really delete address '{$address}'?")
->appendChild(
array(
$notice,
$panel,
));
'<p>Are you sure you want to delete this address? You will no '.
'longer be able to use it to login.</p>')
->addSubmitButton('Delete')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnVerifyAddressResponse(PhutilURI $uri, $email_id) {
$request = $this->getRequest();
$user = $request->getUser();
// NOTE: You can only send more email for your unverified addresses.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
$email->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('verify', $email_id)
->setTitle("Send Another Verification Email?")
->appendChild(
'<p>Send another copy of the verification email to '.
phutil_escape_html($address).'?</p>')
->addSubmitButton('Send Email')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnPrimaryAddressResponse(PhutilURI $uri, $email_id) {
$request = $this->getRequest();
$user = $request->getUser();
// NOTE: You can only make your own verified addresses primary.
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0',
$email_id,
$user->getPHID());
if (!$email) {
return new Aphront404Response();
}
if ($request->isFormPost()) {
// TODO: Transactions!
$email->setIsPrimary(1);
$old_primary = $user->loadPrimaryEmail();
if ($old_primary) {
$old_primary->setIsPrimary(0);
$old_primary->save();
}
$email->save();
if ($old_primary) {
$old_primary->sendOldPrimaryEmail($user, $email);
}
$email->sendNewPrimaryEmail($user);
return id(new AphrontRedirectResponse())->setURI($uri);
}
$address = $email->getAddress();
$dialog = id(new AphrontDialogView())
->setUser($user)
->addHiddenInput('primary', $email_id)
->setTitle("Change primary email address?")
->appendChild(
'<p>If you change your primary address, Phabricator will send all '.
'email to '.phutil_escape_html($address).'.</p>')
->addSubmitButton('Change Primary Address')
->addCancelButton($uri);
return id(new AphrontDialogResponse())->setDialog($dialog);
}
}

View file

@ -6,16 +6,21 @@
phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/404');
phutil_require_module('phabricator', 'aphront/response/dialog');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'aphront/response/reload');
phutil_require_module('phabricator', 'applications/people/controller/settings/panels/base');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'infrastructure/javelin/markup');
phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/dialog');
phutil_require_module('phabricator', 'view/form/control/text');
phutil_require_module('phabricator', 'view/form/error');
phutil_require_module('phabricator', 'view/form/layout');
phutil_require_module('phabricator', 'view/layout/panel');
phutil_require_module('phabricator', 'view/null');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');

View file

@ -40,10 +40,16 @@ final class PhabricatorUserPasswordSettingsPanelController
// the workflow from a password reset email.
$token = $request->getStr('token');
$valid_token = false;
if ($token) {
$valid_token = $user->validateEmailToken($token);
} else {
$valid_token = false;
$email_address = $request->getStr('email');
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email_address);
if ($email) {
$valid_token = $user->validateEmailToken($email, $token);
}
}
$e_old = true;

View file

@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'aphront/writeguard');
phutil_require_module('phabricator', 'applications/people/controller/settings/panels/base');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/password');

View file

@ -0,0 +1,160 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @task email Email About Email
*/
final class PhabricatorUserEmail extends PhabricatorUserDAO {
protected $userPHID;
protected $address;
protected $isVerified;
protected $isPrimary;
protected $verificationCode;
public function getVerificationURI() {
return '/emailverify/'.$this->getVerificationCode().'/';
}
public function save() {
if (!$this->verificationCode) {
$this->setVerificationCode(Filesystem::readRandomCharacters(24));
}
return parent::save();
}
/* -( Email About Email )-------------------------------------------------- */
/**
* Send a verification email from $user to this address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendVerificationEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$address = $this->getAddress();
$link = PhabricatorEnv::getProductionURI($this->getVerificationURI());
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
$signature = null;
if (!$is_serious) {
$signature = <<<EOSIGNATURE
Get Well Soon,
Phabricator
EOSIGNATURE;
}
$body = <<<EOBODY
Hi {$username},
Please verify that you own this email address ({$address}) by clicking this
link:
{$link}
{$signature}
EOBODY;
id(new PhabricatorMetaMTAMail())
->addRawTos(array($address))
->setSubject('[Phabricator] Email Verification')
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is no longer their account's primary address.
*
* @param PhabricatorUser The user sending the notification.
* @param PhabricatorUserEmail New primary email address.
* @return this
* @task email
*/
public function sendOldPrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $new) {
$username = $user->getUsername();
$old_address = $this->getAddress();
$new_address = $new->getAddress();
$body = <<<EOBODY
Hi {$username},
This email address ({$old_address}) is no longer your primary email address.
Going forward, Phabricator will send all email to your new primary email
address ({$new_address}).
EOBODY;
id(new PhabricatorMetaMTAMail())
->addRawTos(array($old_address))
->setSubject('[Phabricator] Primary Address Changed')
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
}
/**
* Send a notification email from $user to this address, informing the
* recipient that this is now their account's new primary email address.
*
* @param PhabricatorUser The user sending the verification.
* @return this
* @task email
*/
public function sendNewPrimaryEmail(PhabricatorUser $user) {
$username = $user->getUsername();
$new_address = $this->getAddress();
$body = <<<EOBODY
Hi {$username},
This is now your primary email address ({$new_address}). Going forward,
Phabricator will send all email here.
EOBODY;
id(new PhabricatorMetaMTAMail())
->addRawTos(array($new_address))
->setSubject('[Phabricator] Primary Address Changed')
->setBody($body)
->setFrom($user->getPHID())
->setRelatedPHID($user->getPHID())
->saveAndSend();
return $this;
}
}

View file

@ -0,0 +1,17 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('phabricator', 'applications/metamta/storage/mail');
phutil_require_module('phabricator', 'applications/people/storage/base');
phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phutil', 'filesystem');
phutil_require_module('phutil', 'utils');
phutil_require_source('PhabricatorUserEmail.php');

View file

@ -24,7 +24,6 @@ final class PhabricatorUser extends PhabricatorUserDAO {
protected $phid;
protected $userName;
protected $realName;
protected $email;
protected $sex;
protected $passwordSalt;
protected $passwordHash;
@ -360,17 +359,30 @@ final class PhabricatorUser extends PhabricatorUserDAO {
$session_key);
}
private function generateEmailToken($offset = 0) {
private function generateEmailToken(
PhabricatorUserEmail $email,
$offset = 0) {
$key = implode(
'-',
array(
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
$this->getPHID(),
$email->getVerificationCode(),
));
return $this->generateToken(
time() + ($offset * self::EMAIL_CYCLE_FREQUENCY),
self::EMAIL_CYCLE_FREQUENCY,
PhabricatorEnv::getEnvConfig('phabricator.csrf-key').$this->getEmail(),
$key,
self::EMAIL_TOKEN_LENGTH);
}
public function validateEmailToken($token) {
public function validateEmailToken(
PhabricatorUserEmail $email,
$token) {
for ($ii = -1; $ii <= 1; $ii++) {
$valid = $this->generateEmailToken($ii);
$valid = $this->generateEmailToken($email, $ii);
if ($token == $valid) {
return true;
}
@ -378,11 +390,32 @@ final class PhabricatorUser extends PhabricatorUserDAO {
return false;
}
public function getEmailLoginURI() {
$token = $this->generateEmailToken();
public function getEmailLoginURI(PhabricatorUserEmail $email = null) {
if (!$email) {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception("User has no primary email!");
}
}
$token = $this->generateEmailToken($email);
$uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/');
$uri = new PhutilURI($uri);
return $uri->alter('email', $this->getEmail());
return $uri->alter('email', $email->getAddress());
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception("User has no primary email address!");
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return id(new PhabricatorUserEmail())->loadOneWhere(
'userPHID = %s AND isPrimary = %d',
$this->getPHID(),
1);
}
public function loadPreferences() {
@ -534,4 +567,16 @@ EOBODY;
return self::getDefaultProfileImageURI();
}
public static function loadOneWithEmailAddress($address) {
$email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$address);
if (!$email) {
return null;
}
return id(new PhabricatorUser())->loadOneWhere(
'phid = %s',
$email->getUserPHID());
}
}

View file

@ -10,6 +10,7 @@ phutil_require_module('phabricator', 'aphront/writeguard');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/metamta/storage/mail');
phutil_require_module('phabricator', 'applications/people/storage/base');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/log');
phutil_require_module('phabricator', 'applications/people/storage/preferences');
phutil_require_module('phabricator', 'applications/phid/constants');

View file

@ -149,6 +149,13 @@ final class PhabricatorObjectHandleData {
$images = mpull($images, 'getBestURI', 'getPHID');
}
// TODO: This probably should not be part of Handles anymore, only
// MetaMTA actually uses it.
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
'userPHID IN (%Ls) AND isPrimary = 1',
$phids);
$emails = mpull($emails, 'getAddress', 'getUserPHID');
foreach ($phids as $phid) {
$handle = new PhabricatorObjectHandle();
$handle->setPHID($phid);
@ -159,7 +166,7 @@ final class PhabricatorObjectHandleData {
$user = $users[$phid];
$handle->setName($user->getUsername());
$handle->setURI('/p/'.$user->getUsername().'/');
$handle->setEmail($user->getEmail());
$handle->setEmail(idx($emails, $phid));
$handle->setFullName(
$user->getUsername().' ('.$user->getRealName().')');
$handle->setAlternateID($user->getID());

View file

@ -11,6 +11,7 @@ phutil_require_module('arcanist', 'differential/constants/revisionstatus');
phutil_require_module('phabricator', 'applications/files/storage/file');
phutil_require_module('phabricator', 'applications/maniphest/constants/owner');
phutil_require_module('phabricator', 'applications/maniphest/constants/status');
phutil_require_module('phabricator', 'applications/people/storage/email');
phutil_require_module('phabricator', 'applications/people/storage/user');
phutil_require_module('phabricator', 'applications/phid/constants');
phutil_require_module('phabricator', 'applications/phid/handle');

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -103,9 +103,7 @@ abstract class PhabricatorRepositoryCommitMessageDetailParser {
}
private function findUserByEmailAddress($email_address) {
$by_email = id(new PhabricatorUser())->loadOneWhere(
'email = %s',
$email_address);
$by_email = PhabricatorUser::loadOneWithEmailAddress($email_address);
if ($by_email) {
return $by_email->getPHID();
}

View file

@ -859,6 +859,18 @@ final class PhabricatorBuiltinPatchList extends PhabricatorSQLPatchList {
'type' => 'sql',
'name' => $this->getPatchPath('userstatus.sql'),
),
'emailtable.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('emailtable.sql'),
),
'emailtableport.sql' => array(
'type' => 'php',
'name' => $this->getPatchPath('emailtableport.php'),
),
'emailtableremove.sql' => array(
'type' => 'sql',
'name' => $this->getPatchPath('emailtableremove.sql'),
),
);
}