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

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

+900 -140
+12
resources/sql/patches/emailtable.sql
··· 1 + CREATE TABLE {$NAMESPACE}_user.user_email ( 2 + `id` int unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + userPHID varchar(64) collate utf8_bin NOT NULL, 4 + address varchar(128) collate utf8_general_ci NOT NULL, 5 + isVerified bool not null default 0, 6 + isPrimary bool not null default 0, 7 + verificationCode varchar(64) collate utf8_bin, 8 + dateCreated int unsigned not null, 9 + dateModified int unsigned not null, 10 + KEY (userPHID, isPrimary), 11 + UNIQUE KEY (address) 12 + ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+49
resources/sql/patches/emailtableport.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + echo "Migrating user emails...\n"; 20 + 21 + $table = new PhabricatorUser(); 22 + $conn = $table->establishConnection('r'); 23 + 24 + $emails = queryfx_all( 25 + $conn, 26 + 'SELECT phid, email FROM %T', 27 + $table->getTableName()); 28 + $emails = ipull($emails, 'email', 'phid'); 29 + 30 + $etable = new PhabricatorUserEmail(); 31 + $econn = $etable->establishConnection('w'); 32 + 33 + foreach ($emails as $phid => $email) { 34 + 35 + // NOTE: Grandfather all existing email in as primary / verified. We generate 36 + // verification codes because they are used for password resets, etc. 37 + 38 + echo "Migrating '{$phid}'...\n"; 39 + queryfx( 40 + $econn, 41 + 'INSERT INTO %T (userPHID, address, verificationCode, isVerified, isPrimary) 42 + VALUES (%s, %s, %s, 1, 1)', 43 + $etable->getTableName(), 44 + $phid, 45 + $email, 46 + PhabricatorFile::readRandomCharacters(24)); 47 + } 48 + 49 + echo "Done.\n";
+1
resources/sql/patches/emailtableremove.sql
··· 1 + ALTER TABLE {$NAMESPACE}_user.user DROP email;
+37 -24
scripts/user/account_admin.php
··· 55 55 } 56 56 $user = new PhabricatorUser(); 57 57 $user->setUsername($username); 58 + 59 + $is_new = true; 58 60 } else { 59 61 $original = clone $user; 60 62 ··· 66 68 echo "Cancelled.\n"; 67 69 exit(1); 68 70 } 71 + 72 + $is_new = false; 69 73 } 70 74 71 75 $user_realname = $user->getRealName(); ··· 79 83 $user_realname); 80 84 $user->setRealName($realname); 81 85 82 - $user_email = $user->getEmail(); 83 - if (strlen($user_email)) { 84 - $email_prompt = ' ['.$user_email.']'; 85 - } else { 86 - $email_prompt = ''; 86 + // When creating a new user we prompt for an email address; when editing an 87 + // existing user we just skip this because it would be quite involved to provide 88 + // a reasonable CLI interface for editing multiple addresses and managing email 89 + // verification and primary addresses. 90 + 91 + $new_email = null; 92 + if ($is_new) { 93 + do { 94 + $email = phutil_console_prompt("Enter user email address:"); 95 + $duplicate = id(new PhabricatorUserEmail())->loadOneWhere( 96 + 'address = %s', 97 + $email); 98 + if ($duplicate) { 99 + echo "ERROR: There is already a user with that email address. ". 100 + "Each user must have a unique email address.\n"; 101 + } else { 102 + break; 103 + } 104 + } while (true); 105 + 106 + $new_email = $email; 87 107 } 88 108 89 - do { 90 - $email = nonempty( 91 - phutil_console_prompt("Enter user email address{$email_prompt}:"), 92 - $user_email); 93 - $duplicate = id(new PhabricatorUser())->loadOneWhere( 94 - 'email = %s', 95 - $email); 96 - if ($duplicate && $duplicate->getID() != $user->getID()) { 97 - $duplicate_username = $duplicate->getUsername(); 98 - echo "ERROR: There is already a user with that email address ". 99 - "({$duplicate_username}). Each user must have a unique email ". 100 - "address.\n"; 101 - } else { 102 - break; 103 - } 104 - } while (true); 105 - $user->setEmail($email); 106 - 107 109 $changed_pass = false; 108 110 // This disables local echo, so the user's password is not shown as they type 109 111 // it. ··· 126 128 printf($tpl, null, 'OLD VALUE', 'NEW VALUE'); 127 129 printf($tpl, 'Username', $original->getUsername(), $user->getUsername()); 128 130 printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName()); 129 - printf($tpl, 'Email', $original->getEmail(), $user->getEmail()); 131 + if ($new_email) { 132 + printf($tpl, 'Email', '', $new_email); 133 + } 130 134 printf($tpl, 'Password', null, 131 135 ($changed_pass !== false) 132 136 ? 'Updated' ··· 151 155 // component of the password hash. 152 156 $user->setPassword($changed_pass); 153 157 $user->save(); 158 + } 159 + 160 + if ($new_email) { 161 + id(new PhabricatorUserEmail()) 162 + ->setUserPHID($user->getPHID()) 163 + ->setAddress($new_email) 164 + ->setIsVerified(1) 165 + ->setIsPrimary(1) 166 + ->save(); 154 167 } 155 168 156 169 echo "Saved changes.\n";
+10 -4
scripts/user/add_user.php
··· 50 50 "There is already a user with the username '{$username}'!"); 51 51 } 52 52 53 - $existing_user = id(new PhabricatorUser())->loadOneWhere( 54 - 'email = %s', 53 + $existing_email = id(new PhabricatorUserEmail())->loadOneWhere( 54 + 'address = %s', 55 55 $email); 56 - if ($existing_user) { 56 + if ($existing_email) { 57 57 throw new Exception( 58 58 "There is already a user with the email '{$email}'!"); 59 59 } 60 60 61 61 $user = new PhabricatorUser(); 62 62 $user->setUsername($username); 63 - $user->setEmail($email); 64 63 $user->setRealname($realname); 65 64 $user->save(); 65 + 66 + $email_object = id(new PhabricatorUserEmail()) 67 + ->setUserPHID($user->getPHID()) 68 + ->setAddress($email) 69 + ->setIsVerified(1) 70 + ->setIsPrimary(1) 71 + ->save(); 66 72 67 73 $user->sendWelcomeEmail($admin); 68 74
+4
src/__phutil_library_map__.php
··· 595 595 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/edge', 596 596 'PhabricatorEmailLoginController' => 'applications/auth/controller/email', 597 597 'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken', 598 + 'PhabricatorEmailVerificationController' => 'applications/people/controller/emailverification', 598 599 'PhabricatorEnv' => 'infrastructure/env', 599 600 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__', 600 601 'PhabricatorErrorExample' => 'applications/uiexample/examples/error', ··· 946 947 'PhabricatorUserAccountSettingsPanelController' => 'applications/people/controller/settings/panels/account', 947 948 'PhabricatorUserConduitSettingsPanelController' => 'applications/people/controller/settings/panels/conduit', 948 949 'PhabricatorUserDAO' => 'applications/people/storage/base', 950 + 'PhabricatorUserEmail' => 'applications/people/storage/email', 949 951 'PhabricatorUserEmailPreferenceSettingsPanelController' => 'applications/people/controller/settings/panels/emailpref', 950 952 'PhabricatorUserEmailSettingsPanelController' => 'applications/people/controller/settings/panels/email', 951 953 'PhabricatorUserLog' => 'applications/people/storage/log', ··· 1534 1536 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 1535 1537 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 1536 1538 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', 1539 + 'PhabricatorEmailVerificationController' => 'PhabricatorPeopleController', 1537 1540 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', 1538 1541 'PhabricatorErrorExample' => 'PhabricatorUIExample', 1539 1542 'PhabricatorEvent' => 'PhutilEvent', ··· 1819 1822 'PhabricatorUserAccountSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 1820 1823 'PhabricatorUserConduitSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 1821 1824 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 1825 + 'PhabricatorUserEmail' => 'PhabricatorUserDAO', 1822 1826 'PhabricatorUserEmailPreferenceSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 1823 1827 'PhabricatorUserEmailSettingsPanelController' => 'PhabricatorUserSettingsPanelController', 1824 1828 'PhabricatorUserLog' => 'PhabricatorUserDAO',
+3
src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
··· 433 433 'testpaymentform/' => 'PhortuneStripeTestPaymentFormController', 434 434 ), 435 435 ), 436 + 437 + '/emailverify/(?P<code>[^/]+)/' => 438 + 'PhabricatorEmailVerificationController', 436 439 ); 437 440 } 438 441
+1 -1
src/aphront/response/redirect/AphrontRedirectResponse.php
··· 31 31 } 32 32 33 33 public function getURI() { 34 - return $this->uri; 34 + return (string)$this->uri; 35 35 } 36 36 37 37 public function getHeaders() {
+10 -3
src/applications/auth/controller/email/PhabricatorEmailLoginController.php
··· 57 57 // it expensive to fish for valid email addresses while giving the user 58 58 // a better error if they goof their email. 59 59 60 - $target_user = id(new PhabricatorUser())->loadOneWhere( 61 - 'email = %s', 60 + $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 61 + 'address = %s', 62 62 $email); 63 63 64 + $target_user = null; 65 + if ($target_email) { 66 + $target_user = id(new PhabricatorUser())->loadOneWhere( 67 + 'phid = %s', 68 + $target_email->getUserPHID()); 69 + } 70 + 64 71 if (!$target_user) { 65 72 $errors[] = "There is no account associated with that email address."; 66 73 $e_email = "Invalid"; 67 74 } 68 75 69 76 if (!$errors) { 70 - $uri = $target_user->getEmailLoginURI(); 77 + $uri = $target_user->getEmailLoginURI($target_email); 71 78 if ($is_serious) { 72 79 $body = <<<EOBODY 73 80 You can use this link to reset your Phabricator password:
+1
src/applications/auth/controller/email/__init__.php
··· 9 9 phutil_require_module('phabricator', 'aphront/response/400'); 10 10 phutil_require_module('phabricator', 'applications/auth/controller/base'); 11 11 phutil_require_module('phabricator', 'applications/metamta/storage/mail'); 12 + phutil_require_module('phabricator', 'applications/people/storage/email'); 12 13 phutil_require_module('phabricator', 'applications/people/storage/user'); 13 14 phutil_require_module('phabricator', 'infrastructure/env'); 14 15 phutil_require_module('phabricator', 'view/form/base');
+38 -5
src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php
··· 55 55 $token = $this->token; 56 56 $email = $request->getStr('email'); 57 57 58 - $target_user = id(new PhabricatorUser())->loadOneWhere( 59 - 'email = %s', 58 + // NOTE: We need to bind verification to **addresses**, not **users**, 59 + // because we verify addresses when they're used to login this way, and if 60 + // we have a user-based verification you can: 61 + // 62 + // - Add some address you do not own; 63 + // - request a password reset; 64 + // - change the URI in the email to the address you don't own; 65 + // - login via the email link; and 66 + // - get a "verified" address you don't control. 67 + 68 + $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 69 + 'address = %s', 60 70 $email); 61 71 62 - if (!$target_user || !$target_user->validateEmailToken($token)) { 72 + $target_user = null; 73 + if ($target_email) { 74 + $target_user = id(new PhabricatorUser())->loadOneWhere( 75 + 'phid = %s', 76 + $target_email->getUserPHID()); 77 + } 78 + 79 + if (!$target_email || 80 + !$target_user || 81 + !$target_user->validateEmailToken($target_email, $token)) { 82 + 63 83 $view = new AphrontRequestFailureView(); 64 84 $view->setHeader('Unable to Login'); 65 85 $view->appendChild( ··· 71 91 '<div class="aphront-failure-continue">'. 72 92 '<a class="button" href="/login/email/">Send Another Email</a>'. 73 93 '</div>'); 94 + 74 95 return $this->buildStandardPageResponse( 75 96 $view, 76 97 array( 77 - 'title' => 'Email Sent', 98 + 'title' => 'Login Failure', 78 99 )); 79 100 } 80 101 102 + // Verify email so that clicking the link in the "Welcome" email is good 103 + // enough, without requiring users to go through a second round of email 104 + // verification. 105 + 106 + $target_email->setIsVerified(1); 107 + $target_email->save(); 108 + 81 109 $session_key = $target_user->establishSession('web'); 82 110 $request->setCookie('phusr', $target_user->getUsername()); 83 111 $request->setCookie('phsid', $session_key); 84 112 85 113 if (PhabricatorEnv::getEnvConfig('account.editable')) { 86 - $next = '/settings/page/password/?token='.$token; 114 + $next = (string)id(new PhutilURI('/settings/page/password/')) 115 + ->setQueryParams( 116 + array( 117 + 'token' => $token, 118 + 'email' => $email, 119 + )); 87 120 } else { 88 121 $next = '/'; 89 122 }
+1
src/applications/auth/controller/emailtoken/__init__.php
··· 9 9 phutil_require_module('phabricator', 'aphront/response/400'); 10 10 phutil_require_module('phabricator', 'aphront/response/redirect'); 11 11 phutil_require_module('phabricator', 'applications/auth/controller/base'); 12 + phutil_require_module('phabricator', 'applications/people/storage/email'); 12 13 phutil_require_module('phabricator', 'applications/people/storage/user'); 13 14 phutil_require_module('phabricator', 'infrastructure/env'); 14 15 phutil_require_module('phabricator', 'view/page/failure');
+1 -3
src/applications/auth/controller/login/PhabricatorLoginController.php
··· 113 113 $username_or_email); 114 114 115 115 if (!$user) { 116 - $user = id(new PhabricatorUser())->loadOneWhere( 117 - 'email = %s', 118 - $username_or_email); 116 + $user = PhabricatorUser::loadOneWithEmailAddress($username_or_email); 119 117 } 120 118 121 119 if (!$errors) {
+2 -2
src/applications/auth/controller/oauth/PhabricatorOAuthLoginController.php
··· 176 176 177 177 $oauth_email = $provider->retrieveUserEmail(); 178 178 if ($oauth_email) { 179 - $known_email = id(new PhabricatorUser()) 180 - ->loadOneWhere('email = %s', $oauth_email); 179 + $known_email = id(new PhabricatorUserEmail()) 180 + ->loadOneWhere('address = %s', $oauth_email); 181 181 if ($known_email) { 182 182 $dialog = new AphrontDialogView(); 183 183 $dialog->setUser($current_user);
+1
src/applications/auth/controller/oauth/__init__.php
··· 13 13 phutil_require_module('phabricator', 'applications/auth/controller/base'); 14 14 phutil_require_module('phabricator', 'applications/auth/oauth/provider/base'); 15 15 phutil_require_module('phabricator', 'applications/auth/view/oauthfailure'); 16 + phutil_require_module('phabricator', 'applications/people/storage/email'); 16 17 phutil_require_module('phabricator', 'applications/people/storage/user'); 17 18 phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo'); 18 19 phutil_require_module('phabricator', 'infrastructure/env');
+25 -7
src/applications/auth/controller/oauthregistration/default/PhabricatorOAuthDefaultRegistrationController.php
··· 33 33 34 34 $user->setUsername($provider->retrieveUserAccountName()); 35 35 $user->setRealName($provider->retrieveUserRealName()); 36 - $user->setEmail($provider->retrieveUserEmail()); 36 + 37 + $new_email = $provider->retrieveUserEmail(); 37 38 38 39 if ($request->isFormPost()) { 39 40 ··· 49 50 $e_username = null; 50 51 } 51 52 52 - if ($user->getEmail() === null) { 53 - $user->setEmail($request->getStr('email')); 54 - if (!strlen($user->getEmail())) { 53 + if (!$new_email) { 54 + $new_email = trim($request->getStr('email')); 55 + if (!$new_email) { 55 56 $e_email = 'Required'; 56 57 $errors[] = 'Email is required.'; 57 58 } else { ··· 84 85 try { 85 86 $user->save(); 86 87 88 + // NOTE: We don't verify OAuth email addresses by default because 89 + // OAuth providers might associate email addresses with accounts that 90 + // haven't actually verified they own them. We could selectively 91 + // auto-verify some providers that we trust here, but the stakes for 92 + // verifying an email address are high because having a corporate 93 + // address at a company is sometimes the key to the castle. 94 + 95 + $new_email = id(new PhabricatorUserEmail()) 96 + ->setUserPHID($user->getPHID()) 97 + ->setAddress($new_email) 98 + ->setIsPrimary(1) 99 + ->setIsVerified(0) 100 + ->save(); 101 + 87 102 $oauth_info->setUserID($user->getID()); 88 103 $oauth_info->save(); 89 104 90 105 $session_key = $user->establishSession('web'); 91 106 $request->setCookie('phusr', $user->getUsername()); 92 107 $request->setCookie('phsid', $session_key); 108 + 109 + $new_email->sendVerificationEmail($user); 110 + 93 111 return id(new AphrontRedirectResponse())->setURI('/'); 94 112 } catch (AphrontQueryDuplicateKeyException $exception) { 95 113 ··· 97 115 'userName = %s', 98 116 $user->getUserName()); 99 117 100 - $same_email = id(new PhabricatorUser())->loadOneWhere( 101 - 'email = %s', 102 - $user->getEmail()); 118 + $same_email = id(new PhabricatorUserEmail())->loadOneWhere( 119 + 'address = %s', 120 + $new_email); 103 121 104 122 if ($same_username) { 105 123 $e_username = 'Duplicate';
+1
src/applications/auth/controller/oauthregistration/default/__init__.php
··· 9 9 phutil_require_module('phabricator', 'aphront/response/redirect'); 10 10 phutil_require_module('phabricator', 'applications/auth/controller/oauthregistration/base'); 11 11 phutil_require_module('phabricator', 'applications/files/storage/file'); 12 + phutil_require_module('phabricator', 'applications/people/storage/email'); 12 13 phutil_require_module('phabricator', 'applications/people/storage/user'); 13 14 phutil_require_module('phabricator', 'view/form/base'); 14 15 phutil_require_module('phabricator', 'view/form/control/submit');
-1
src/applications/conduit/method/user/base/ConduitAPI_user_Method.php
··· 26 26 'phid' => $user->getPHID(), 27 27 'userName' => $user->getUserName(), 28 28 'realName' => $user->getRealName(), 29 - 'email' => $user->getEmail(), 30 29 'image' => $user->loadProfileImageURI(), 31 30 'uri' => PhabricatorEnv::getURI('/p/'.$user->getUsername().'/'), 32 31 );
+1 -2
src/applications/differential/field/specification/base/DifferentialFieldSpecification.php
··· 672 672 $object_map = array(); 673 673 674 674 $users = id(new PhabricatorUser())->loadAllWhere( 675 - '(username IN (%Ls)) OR (email IN (%Ls))', 676 - $value, 675 + '(username IN (%Ls))', 677 676 $value); 678 677 679 678 $user_map = mpull($users, 'getPHID', 'getUsername');
+9 -1
src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
··· 108 108 return $this; 109 109 } 110 110 111 + public function addRawTos(array $raw_email) { 112 + $this->setParam('raw-to', $raw_email); 113 + return $this; 114 + } 115 + 111 116 public function addCCs(array $phids) { 112 117 $phids = array_unique($phids); 113 118 $this->setParam('cc', $phids); ··· 367 372 $handles, 368 373 $exclude); 369 374 if ($emails) { 370 - $add_to = $emails; 375 + $add_to = array_merge($add_to, $emails); 371 376 } 377 + break; 378 + case 'raw-to': 379 + $add_to = array_merge($add_to, $value); 372 380 break; 373 381 case 'cc': 374 382 $emails = $this->getDeliverableEmailsFromHandles(
+2 -6
src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
··· 274 274 $from = idx($this->headers, 'from'); 275 275 $from = $this->getRawEmailAddress($from); 276 276 277 - $user = id(new PhabricatorUser())->loadOneWhere( 278 - 'email = %s', 279 - $from); 277 + $user = PhabricatorUser::loadOneWithEmailAddress($from); 280 278 281 279 // If Phabricator is configured to allow "Reply-To" authentication, try 282 280 // the "Reply-To" address if we failed to match the "From" address. ··· 287 285 $reply_to = idx($this->headers, 'reply-to'); 288 286 $reply_to = $this->getRawEmailAddress($reply_to); 289 287 if ($reply_to) { 290 - $user = id(new PhabricatorUser())->loadOneWhere( 291 - 'email = %s', 292 - $reply_to); 288 + $user = PhabricatorUser::loadOneWithEmailAddress($reply_to); 293 289 } 294 290 } 295 291
+27 -15
src/applications/people/controller/edit/PhabricatorPeopleEditController.php
··· 123 123 124 124 $welcome_checked = true; 125 125 126 + $new_email = null; 127 + 126 128 $request = $this->getRequest(); 127 129 if ($request->isFormPost()) { 128 130 $welcome_checked = $request->getInt('welcome'); 129 131 130 132 if (!$user->getID()) { 131 133 $user->setUsername($request->getStr('username')); 132 - $user->setEmail($request->getStr('email')); 134 + 135 + $new_email = $request->getStr('email'); 136 + if (!strlen($new_email)) { 137 + $errors[] = 'Email is required.'; 138 + $e_email = 'Required'; 139 + } 133 140 134 141 if ($request->getStr('role') == 'agent') { 135 142 $user->setIsSystemAgent(true); ··· 154 161 $e_realname = null; 155 162 } 156 163 157 - if (!strlen($user->getEmail())) { 158 - $errors[] = 'Email is required.'; 159 - $e_email = 'Required'; 160 - } else { 161 - $e_email = null; 162 - } 163 - 164 164 if (!$errors) { 165 165 try { 166 166 $is_new = !$user->getID(); ··· 168 168 $user->save(); 169 169 170 170 if ($is_new) { 171 + 172 + $email = id(new PhabricatorUserEmail()) 173 + ->setUserPHID($user->getPHID()) 174 + ->setAddress($new_email) 175 + ->setIsPrimary(1) 176 + ->setIsVerified(0) 177 + ->save(); 178 + 171 179 $log = PhabricatorUserLog::newLog( 172 180 $admin, 173 181 $user, ··· 187 195 188 196 $same_username = id(new PhabricatorUser()) 189 197 ->loadOneWhere('username = %s', $user->getUsername()); 190 - $same_email = id(new PhabricatorUser()) 191 - ->loadOneWhere('email = %s', $user->getEmail()); 198 + $same_email = id(new PhabricatorUserEmail()) 199 + ->loadOneWhere('address = %s', $new_email); 192 200 193 201 if ($same_username) { 194 202 $e_username = 'Duplicate'; ··· 236 244 ->setLabel('Real Name') 237 245 ->setName('realname') 238 246 ->setValue($user->getRealName()) 239 - ->setError($e_realname)) 240 - ->appendChild( 247 + ->setError($e_realname)); 248 + 249 + if (!$user->getID()) { 250 + $form->appendChild( 241 251 id(new AphrontFormTextControl()) 242 252 ->setLabel('Email') 243 253 ->setName('email') 244 254 ->setDisabled($is_immutable) 245 - ->setValue($user->getEmail()) 246 - ->setError($e_email)) 247 - ->appendChild($this->getRoleInstructions()); 255 + ->setValue($new_email) 256 + ->setError($e_email)); 257 + } 258 + 259 + $form->appendChild($this->getRoleInstructions()); 248 260 249 261 if (!$user->getID()) { 250 262 $form
+1
src/applications/people/controller/edit/__init__.php
··· 9 9 phutil_require_module('phabricator', 'aphront/response/404'); 10 10 phutil_require_module('phabricator', 'aphront/response/redirect'); 11 11 phutil_require_module('phabricator', 'applications/people/controller/base'); 12 + phutil_require_module('phabricator', 'applications/people/storage/email'); 12 13 phutil_require_module('phabricator', 'applications/people/storage/log'); 13 14 phutil_require_module('phabricator', 'applications/people/storage/user'); 14 15 phutil_require_module('phabricator', 'infrastructure/env');
+81
src/applications/people/controller/emailverification/PhabricatorEmailVerificationController.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + final class PhabricatorEmailVerificationController 20 + extends PhabricatorPeopleController { 21 + 22 + private $code; 23 + 24 + public function willProcessRequest(array $data) { 25 + $this->code = $data['code']; 26 + } 27 + 28 + public function processRequest() { 29 + $request = $this->getRequest(); 30 + $user = $request->getUser(); 31 + 32 + $email = id(new PhabricatorUserEmail())->loadOneWhere( 33 + 'userPHID = %s AND verificationCode = %s', 34 + $user->getPHID(), 35 + $this->code); 36 + 37 + $settings_link = phutil_render_tag( 38 + 'a', 39 + array( 40 + 'href' => '/settings/page/email/', 41 + ), 42 + 'Return to Email Settings'); 43 + $settings_link = '<br /><p><strong>'.$settings_link.'</strong></p>'; 44 + 45 + if (!$email) { 46 + $content = id(new AphrontErrorView()) 47 + ->setTitle('Unable To Verify') 48 + ->appendChild( 49 + '<p>The verification code is incorrect, the email address has '. 50 + 'been removed, or the email address is owned by another user. Make '. 51 + 'sure you followed the link in the email correctly.</p>'); 52 + } else if ($email->getIsVerified()) { 53 + $content = id(new AphrontErrorView()) 54 + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 55 + ->setTitle('Address Already Verified') 56 + ->appendChild( 57 + '<p>This email address has already been verified.</p>'. 58 + $settings_link); 59 + } else { 60 + 61 + $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); 62 + $email->setIsVerified(1); 63 + $email->save(); 64 + unset($guard); 65 + 66 + $content = id(new AphrontErrorView()) 67 + ->setSeverity(AphrontErrorView::SEVERITY_NOTICE) 68 + ->setTitle('Address Verified') 69 + ->appendChild( 70 + '<p>This email address has now been verified. Thanks!</p>'. 71 + $settings_link); 72 + } 73 + 74 + return $this->buildStandardPageResponse( 75 + $content, 76 + array( 77 + 'title' => 'Verify Email', 78 + )); 79 + } 80 + 81 + }
+18
src/applications/people/controller/emailverification/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'aphront/writeguard'); 10 + phutil_require_module('phabricator', 'applications/people/controller/base'); 11 + phutil_require_module('phabricator', 'applications/people/storage/email'); 12 + phutil_require_module('phabricator', 'view/form/error'); 13 + 14 + phutil_require_module('phutil', 'markup'); 15 + phutil_require_module('phutil', 'utils'); 16 + 17 + 18 + phutil_require_source('PhabricatorEmailVerificationController.php');
+1 -1
src/applications/people/controller/settings/PhabricatorUserSettingsController.php
··· 94 94 ->addFilter('profile', 'Profile') 95 95 ->addSpacer() 96 96 ->addLabel('Email') 97 - ->addFilter('email', 'Email Address') 97 + ->addFilter('email', 'Email Addresses') 98 98 ->addFilter('emailpref', 'Email Preferences') 99 99 ->addSpacer() 100 100 ->addLabel('Authentication');
+290 -45
src/applications/people/controller/settings/panels/email/PhabricatorUserEmailSettingsPanelController.php
··· 25 25 $user = $request->getUser(); 26 26 $editable = $this->getAccountEditable(); 27 27 28 - $e_email = true; 29 - $errors = array(); 30 - if ($request->isFormPost()) { 31 - if (!$editable) { 32 - return new Aphront400Response(); 28 + $uri = $request->getRequestURI(); 29 + $uri->setQueryParams(array()); 30 + 31 + if ($editable) { 32 + $new = $request->getStr('new'); 33 + if ($new) { 34 + return $this->returnNewAddressResponse($uri, $new); 33 35 } 34 36 35 - $user->setEmail($request->getStr('email')); 37 + $delete = $request->getInt('delete'); 38 + if ($delete) { 39 + return $this->returnDeleteAddressResponse($uri, $delete); 40 + } 41 + } 36 42 37 - if (!strlen($user->getEmail())) { 38 - $errors[] = 'You must enter an e-mail address.'; 43 + $verify = $request->getInt('verify'); 44 + if ($verify) { 45 + return $this->returnVerifyAddressResponse($uri, $verify); 46 + } 47 + 48 + $primary = $request->getInt('primary'); 49 + if ($primary) { 50 + return $this->returnPrimaryAddressResponse($uri, $primary); 51 + } 52 + 53 + $emails = id(new PhabricatorUserEmail())->loadAllWhere( 54 + 'userPHID = %s', 55 + $user->getPHID()); 56 + 57 + $rowc = array(); 58 + $rows = array(); 59 + foreach ($emails as $email) { 60 + 61 + if ($email->getIsPrimary()) { 62 + $action = phutil_render_tag( 63 + 'a', 64 + array( 65 + 'class' => 'button small disabled', 66 + ), 67 + 'Primary'); 68 + $remove = $action; 69 + $rowc[] = 'highlighted'; 70 + } else { 71 + if ($email->getIsVerified()) { 72 + $action = javelin_render_tag( 73 + 'a', 74 + array( 75 + 'class' => 'button small grey', 76 + 'href' => $uri->alter('primary', $email->getID()), 77 + 'sigil' => 'workflow', 78 + ), 79 + 'Make Primary'); 80 + } else { 81 + $action = javelin_render_tag( 82 + 'a', 83 + array( 84 + 'class' => 'button small grey', 85 + 'href' => $uri->alter('verify', $email->getID()), 86 + 'sigil' => 'workflow', 87 + ), 88 + 'Verify'); 89 + } 90 + $remove = javelin_render_tag( 91 + 'a', 92 + array( 93 + 'class' => 'button small grey', 94 + 'href' => $uri->alter('delete', $email->getID()), 95 + 'sigil' => 'workflow' 96 + ), 97 + 'Remove'); 98 + $rowc[] = null; 99 + } 100 + 101 + $rows[] = array( 102 + phutil_escape_html($email->getAddress()), 103 + $action, 104 + $remove, 105 + ); 106 + } 107 + 108 + $table = new AphrontTableView($rows); 109 + $table->setHeaders( 110 + array( 111 + 'Email', 112 + 'Status', 113 + 'Remove', 114 + )); 115 + $table->setColumnClasses( 116 + array( 117 + 'wide', 118 + 'action', 119 + 'action', 120 + )); 121 + $table->setRowClasses($rowc); 122 + $table->setColumnVisibility( 123 + array( 124 + true, 125 + true, 126 + $editable, 127 + )); 128 + 129 + $view = new AphrontPanelView(); 130 + if ($editable) { 131 + $view->addButton( 132 + javelin_render_tag( 133 + 'a', 134 + array( 135 + 'href' => $uri->alter('new', 'true'), 136 + 'class' => 'green button', 137 + 'sigil' => 'workflow', 138 + ), 139 + 'Add New Address')); 140 + } 141 + $view->setHeader('Email Addresses'); 142 + $view->appendChild($table); 143 + 144 + return $view; 145 + } 146 + 147 + private function returnNewAddressResponse(PhutilURI $uri, $new) { 148 + $request = $this->getRequest(); 149 + $user = $request->getUser(); 150 + 151 + $e_email = true; 152 + $email = trim($request->getStr('email')); 153 + $errors = array(); 154 + if ($request->isDialogFormPost()) { 155 + 156 + if ($new == 'verify') { 157 + // The user clicked "Done" from the "an email has been sent" dialog. 158 + return id(new AphrontReloadResponse())->setURI($uri); 159 + } 160 + 161 + if (!strlen($email)) { 39 162 $e_email = 'Required'; 163 + $errors[] = 'Email is required.'; 40 164 } 41 165 42 166 if (!$errors) { 43 - $user->save(); 44 - return id(new AphrontRedirectResponse()) 45 - ->setURI('/settings/page/email/?saved=true'); 167 + $object = id(new PhabricatorUserEmail()) 168 + ->setUserPHID($user->getPHID()) 169 + ->setAddress($email) 170 + ->setIsVerified(0) 171 + ->setIsPrimary(0); 172 + 173 + try { 174 + $object->save(); 175 + 176 + $object->sendVerificationEmail($user); 177 + 178 + $dialog = id(new AphrontDialogView()) 179 + ->setUser($user) 180 + ->addHiddenInput('new', 'verify') 181 + ->setTitle('Verification Email Sent') 182 + ->appendChild( 183 + '<p>A verification email has been sent. Click the link in the '. 184 + 'email to verify your address.</p>') 185 + ->setSubmitURI($uri) 186 + ->addSubmitButton('Done'); 187 + 188 + return id(new AphrontDialogResponse())->setDialog($dialog); 189 + } catch (AphrontQueryDuplicateKeyException $ex) { 190 + $email = 'Duplicate'; 191 + $errors[] = 'Another user already has this email.'; 192 + } 46 193 } 47 194 } 48 195 49 - $notice = null; 50 - if (!$errors) { 51 - if ($request->getStr('saved')) { 52 - $notice = new AphrontErrorView(); 53 - $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); 54 - $notice->setTitle('Changes Saved'); 55 - $notice->appendChild('<p>Your changes have been saved.</p>'); 56 - } 57 - } else { 58 - $notice = new AphrontErrorView(); 59 - $notice->setTitle('Form Errors'); 60 - $notice->setErrors($errors); 196 + if ($errors) { 197 + $errors = id(new AphrontErrorView()) 198 + ->setWidth(AphrontErrorView::WIDTH_DIALOG) 199 + ->setErrors($errors); 61 200 } 62 201 63 - $form = new AphrontFormView(); 64 - $form 65 - ->setUser($user) 202 + $form = id(new AphrontFormLayoutView()) 66 203 ->appendChild( 67 204 id(new AphrontFormTextControl()) 68 205 ->setLabel('Email') 69 206 ->setName('email') 70 - ->setDisabled(!$editable) 71 - ->setCaption( 72 - 'Note: there is no email validation yet; double-check your '. 73 - 'typing.') 74 - ->setValue($user->getEmail()) 207 + ->setValue($request->getStr('email')) 75 208 ->setError($e_email)); 76 209 77 - if ($editable) { 78 - $form 79 - ->appendChild( 80 - id(new AphrontFormSubmitControl()) 81 - ->setValue('Save')); 210 + $dialog = id(new AphrontDialogView()) 211 + ->setUser($user) 212 + ->addHiddenInput('new', 'true') 213 + ->setTitle('New Address') 214 + ->appendChild($errors) 215 + ->appendChild($form) 216 + ->addSubmitButton('Save') 217 + ->addCancelButton($uri); 218 + 219 + return id(new AphrontDialogResponse())->setDialog($dialog); 220 + } 221 + 222 + private function returnDeleteAddressResponse(PhutilURI $uri, $email_id) { 223 + $request = $this->getRequest(); 224 + $user = $request->getUser(); 225 + 226 + // NOTE: You can only delete your own email addresses, and you can not 227 + // delete your primary address. 228 + $email = id(new PhabricatorUserEmail())->loadOneWhere( 229 + 'id = %d AND userPHID = %s AND isPrimary = 0', 230 + $email_id, 231 + $user->getPHID()); 232 + 233 + if (!$email) { 234 + return new Aphront404Response(); 235 + } 236 + 237 + if ($request->isFormPost()) { 238 + $email->delete(); 239 + return id(new AphrontRedirectResponse())->setURI($uri); 240 + } 241 + 242 + $address = $email->getAddress(); 243 + 244 + $dialog = id(new AphrontDialogView()) 245 + ->setUser($user) 246 + ->addHiddenInput('delete', $email_id) 247 + ->setTitle("Really delete address '{$address}'?") 248 + ->appendChild( 249 + '<p>Are you sure you want to delete this address? You will no '. 250 + 'longer be able to use it to login.</p>') 251 + ->addSubmitButton('Delete') 252 + ->addCancelButton($uri); 253 + 254 + return id(new AphrontDialogResponse())->setDialog($dialog); 255 + } 256 + 257 + private function returnVerifyAddressResponse(PhutilURI $uri, $email_id) { 258 + $request = $this->getRequest(); 259 + $user = $request->getUser(); 260 + 261 + // NOTE: You can only send more email for your unverified addresses. 262 + $email = id(new PhabricatorUserEmail())->loadOneWhere( 263 + 'id = %d AND userPHID = %s AND isVerified = 0', 264 + $email_id, 265 + $user->getPHID()); 266 + 267 + if (!$email) { 268 + return new Aphront404Response(); 269 + } 270 + 271 + if ($request->isFormPost()) { 272 + $email->sendVerificationEmail($user); 273 + return id(new AphrontRedirectResponse())->setURI($uri); 274 + } 275 + 276 + $address = $email->getAddress(); 277 + 278 + $dialog = id(new AphrontDialogView()) 279 + ->setUser($user) 280 + ->addHiddenInput('verify', $email_id) 281 + ->setTitle("Send Another Verification Email?") 282 + ->appendChild( 283 + '<p>Send another copy of the verification email to '. 284 + phutil_escape_html($address).'?</p>') 285 + ->addSubmitButton('Send Email') 286 + ->addCancelButton($uri); 287 + 288 + return id(new AphrontDialogResponse())->setDialog($dialog); 289 + } 290 + 291 + private function returnPrimaryAddressResponse(PhutilURI $uri, $email_id) { 292 + $request = $this->getRequest(); 293 + $user = $request->getUser(); 294 + 295 + // NOTE: You can only make your own verified addresses primary. 296 + $email = id(new PhabricatorUserEmail())->loadOneWhere( 297 + 'id = %d AND userPHID = %s AND isVerified = 1 AND isPrimary = 0', 298 + $email_id, 299 + $user->getPHID()); 300 + 301 + if (!$email) { 302 + return new Aphront404Response(); 303 + } 304 + 305 + if ($request->isFormPost()) { 306 + 307 + // TODO: Transactions! 308 + 309 + $email->setIsPrimary(1); 310 + 311 + $old_primary = $user->loadPrimaryEmail(); 312 + if ($old_primary) { 313 + $old_primary->setIsPrimary(0); 314 + $old_primary->save(); 315 + } 316 + $email->save(); 317 + 318 + if ($old_primary) { 319 + $old_primary->sendOldPrimaryEmail($user, $email); 320 + } 321 + $email->sendNewPrimaryEmail($user); 322 + 323 + return id(new AphrontRedirectResponse())->setURI($uri); 82 324 } 83 325 84 - $panel = new AphrontPanelView(); 85 - $panel->setHeader('Email Settings'); 86 - $panel->setWidth(AphrontPanelView::WIDTH_FORM); 87 - $panel->appendChild($form); 326 + $address = $email->getAddress(); 88 327 89 - return id(new AphrontNullView()) 328 + $dialog = id(new AphrontDialogView()) 329 + ->setUser($user) 330 + ->addHiddenInput('primary', $email_id) 331 + ->setTitle("Change primary email address?") 90 332 ->appendChild( 91 - array( 92 - $notice, 93 - $panel, 94 - )); 333 + '<p>If you change your primary address, Phabricator will send all '. 334 + 'email to '.phutil_escape_html($address).'.</p>') 335 + ->addSubmitButton('Change Primary Address') 336 + ->addCancelButton($uri); 337 + 338 + return id(new AphrontDialogResponse())->setDialog($dialog); 95 339 } 340 + 96 341 }
+9 -4
src/applications/people/controller/settings/panels/email/__init__.php
··· 6 6 7 7 8 8 9 - phutil_require_module('phabricator', 'aphront/response/400'); 9 + phutil_require_module('phabricator', 'aphront/response/404'); 10 + phutil_require_module('phabricator', 'aphront/response/dialog'); 10 11 phutil_require_module('phabricator', 'aphront/response/redirect'); 12 + phutil_require_module('phabricator', 'aphront/response/reload'); 11 13 phutil_require_module('phabricator', 'applications/people/controller/settings/panels/base'); 12 - phutil_require_module('phabricator', 'view/form/base'); 13 - phutil_require_module('phabricator', 'view/form/control/submit'); 14 + phutil_require_module('phabricator', 'applications/people/storage/email'); 15 + phutil_require_module('phabricator', 'infrastructure/javelin/markup'); 16 + phutil_require_module('phabricator', 'view/control/table'); 17 + phutil_require_module('phabricator', 'view/dialog'); 14 18 phutil_require_module('phabricator', 'view/form/control/text'); 15 19 phutil_require_module('phabricator', 'view/form/error'); 20 + phutil_require_module('phabricator', 'view/form/layout'); 16 21 phutil_require_module('phabricator', 'view/layout/panel'); 17 - phutil_require_module('phabricator', 'view/null'); 18 22 23 + phutil_require_module('phutil', 'markup'); 19 24 phutil_require_module('phutil', 'utils'); 20 25 21 26
+9 -3
src/applications/people/controller/settings/panels/password/PhabricatorUserPasswordSettingsPanelController.php
··· 40 40 // the workflow from a password reset email. 41 41 42 42 $token = $request->getStr('token'); 43 + 44 + $valid_token = false; 43 45 if ($token) { 44 - $valid_token = $user->validateEmailToken($token); 45 - } else { 46 - $valid_token = false; 46 + $email_address = $request->getStr('email'); 47 + $email = id(new PhabricatorUserEmail())->loadOneWhere( 48 + 'address = %s', 49 + $email_address); 50 + if ($email) { 51 + $valid_token = $user->validateEmailToken($email, $token); 52 + } 47 53 } 48 54 49 55 $e_old = true;
+1
src/applications/people/controller/settings/panels/password/__init__.php
··· 10 10 phutil_require_module('phabricator', 'aphront/response/redirect'); 11 11 phutil_require_module('phabricator', 'aphront/writeguard'); 12 12 phutil_require_module('phabricator', 'applications/people/controller/settings/panels/base'); 13 + phutil_require_module('phabricator', 'applications/people/storage/email'); 13 14 phutil_require_module('phabricator', 'infrastructure/env'); 14 15 phutil_require_module('phabricator', 'view/form/base'); 15 16 phutil_require_module('phabricator', 'view/form/control/password');
+160
src/applications/people/storage/email/PhabricatorUserEmail.php
··· 1 + <?php 2 + 3 + /* 4 + * Copyright 2012 Facebook, Inc. 5 + * 6 + * Licensed under the Apache License, Version 2.0 (the "License"); 7 + * you may not use this file except in compliance with the License. 8 + * You may obtain a copy of the License at 9 + * 10 + * http://www.apache.org/licenses/LICENSE-2.0 11 + * 12 + * Unless required by applicable law or agreed to in writing, software 13 + * distributed under the License is distributed on an "AS IS" BASIS, 14 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 + * See the License for the specific language governing permissions and 16 + * limitations under the License. 17 + */ 18 + 19 + /** 20 + * @task email Email About Email 21 + */ 22 + final class PhabricatorUserEmail extends PhabricatorUserDAO { 23 + 24 + protected $userPHID; 25 + protected $address; 26 + protected $isVerified; 27 + protected $isPrimary; 28 + protected $verificationCode; 29 + 30 + public function getVerificationURI() { 31 + return '/emailverify/'.$this->getVerificationCode().'/'; 32 + } 33 + 34 + public function save() { 35 + if (!$this->verificationCode) { 36 + $this->setVerificationCode(Filesystem::readRandomCharacters(24)); 37 + } 38 + return parent::save(); 39 + } 40 + 41 + 42 + /* -( Email About Email )-------------------------------------------------- */ 43 + 44 + 45 + /** 46 + * Send a verification email from $user to this address. 47 + * 48 + * @param PhabricatorUser The user sending the verification. 49 + * @return this 50 + * @task email 51 + */ 52 + public function sendVerificationEmail(PhabricatorUser $user) { 53 + $username = $user->getUsername(); 54 + 55 + $address = $this->getAddress(); 56 + $link = PhabricatorEnv::getProductionURI($this->getVerificationURI()); 57 + 58 + 59 + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); 60 + 61 + $signature = null; 62 + if (!$is_serious) { 63 + $signature = <<<EOSIGNATURE 64 + Get Well Soon, 65 + Phabricator 66 + EOSIGNATURE; 67 + } 68 + 69 + $body = <<<EOBODY 70 + Hi {$username}, 71 + 72 + Please verify that you own this email address ({$address}) by clicking this 73 + link: 74 + 75 + {$link} 76 + 77 + {$signature} 78 + EOBODY; 79 + 80 + id(new PhabricatorMetaMTAMail()) 81 + ->addRawTos(array($address)) 82 + ->setSubject('[Phabricator] Email Verification') 83 + ->setBody($body) 84 + ->setFrom($user->getPHID()) 85 + ->setRelatedPHID($user->getPHID()) 86 + ->saveAndSend(); 87 + 88 + return $this; 89 + } 90 + 91 + 92 + /** 93 + * Send a notification email from $user to this address, informing the 94 + * recipient that this is no longer their account's primary address. 95 + * 96 + * @param PhabricatorUser The user sending the notification. 97 + * @param PhabricatorUserEmail New primary email address. 98 + * @return this 99 + * @task email 100 + */ 101 + public function sendOldPrimaryEmail( 102 + PhabricatorUser $user, 103 + PhabricatorUserEmail $new) { 104 + $username = $user->getUsername(); 105 + 106 + $old_address = $this->getAddress(); 107 + $new_address = $new->getAddress(); 108 + 109 + $body = <<<EOBODY 110 + Hi {$username}, 111 + 112 + This email address ({$old_address}) is no longer your primary email address. 113 + Going forward, Phabricator will send all email to your new primary email 114 + address ({$new_address}). 115 + 116 + EOBODY; 117 + 118 + id(new PhabricatorMetaMTAMail()) 119 + ->addRawTos(array($old_address)) 120 + ->setSubject('[Phabricator] Primary Address Changed') 121 + ->setBody($body) 122 + ->setFrom($user->getPHID()) 123 + ->setRelatedPHID($user->getPHID()) 124 + ->saveAndSend(); 125 + } 126 + 127 + 128 + /** 129 + * Send a notification email from $user to this address, informing the 130 + * recipient that this is now their account's new primary email address. 131 + * 132 + * @param PhabricatorUser The user sending the verification. 133 + * @return this 134 + * @task email 135 + */ 136 + public function sendNewPrimaryEmail(PhabricatorUser $user) { 137 + $username = $user->getUsername(); 138 + 139 + $new_address = $this->getAddress(); 140 + 141 + $body = <<<EOBODY 142 + Hi {$username}, 143 + 144 + This is now your primary email address ({$new_address}). Going forward, 145 + Phabricator will send all email here. 146 + 147 + EOBODY; 148 + 149 + id(new PhabricatorMetaMTAMail()) 150 + ->addRawTos(array($new_address)) 151 + ->setSubject('[Phabricator] Primary Address Changed') 152 + ->setBody($body) 153 + ->setFrom($user->getPHID()) 154 + ->setRelatedPHID($user->getPHID()) 155 + ->saveAndSend(); 156 + 157 + return $this; 158 + } 159 + 160 + }
+17
src/applications/people/storage/email/__init__.php
··· 1 + <?php 2 + /** 3 + * This file is automatically generated. Lint this module to rebuild it. 4 + * @generated 5 + */ 6 + 7 + 8 + 9 + phutil_require_module('phabricator', 'applications/metamta/storage/mail'); 10 + phutil_require_module('phabricator', 'applications/people/storage/base'); 11 + phutil_require_module('phabricator', 'infrastructure/env'); 12 + 13 + phutil_require_module('phutil', 'filesystem'); 14 + phutil_require_module('phutil', 'utils'); 15 + 16 + 17 + phutil_require_source('PhabricatorUserEmail.php');
+53 -8
src/applications/people/storage/user/PhabricatorUser.php
··· 24 24 protected $phid; 25 25 protected $userName; 26 26 protected $realName; 27 - protected $email; 28 27 protected $sex; 29 28 protected $passwordSalt; 30 29 protected $passwordHash; ··· 360 359 $session_key); 361 360 } 362 361 363 - private function generateEmailToken($offset = 0) { 362 + private function generateEmailToken( 363 + PhabricatorUserEmail $email, 364 + $offset = 0) { 365 + 366 + $key = implode( 367 + '-', 368 + array( 369 + PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), 370 + $this->getPHID(), 371 + $email->getVerificationCode(), 372 + )); 373 + 364 374 return $this->generateToken( 365 375 time() + ($offset * self::EMAIL_CYCLE_FREQUENCY), 366 376 self::EMAIL_CYCLE_FREQUENCY, 367 - PhabricatorEnv::getEnvConfig('phabricator.csrf-key').$this->getEmail(), 377 + $key, 368 378 self::EMAIL_TOKEN_LENGTH); 369 379 } 370 380 371 - public function validateEmailToken($token) { 381 + public function validateEmailToken( 382 + PhabricatorUserEmail $email, 383 + $token) { 372 384 for ($ii = -1; $ii <= 1; $ii++) { 373 - $valid = $this->generateEmailToken($ii); 385 + $valid = $this->generateEmailToken($email, $ii); 374 386 if ($token == $valid) { 375 387 return true; 376 388 } ··· 378 390 return false; 379 391 } 380 392 381 - public function getEmailLoginURI() { 382 - $token = $this->generateEmailToken(); 393 + public function getEmailLoginURI(PhabricatorUserEmail $email = null) { 394 + if (!$email) { 395 + $email = $this->loadPrimaryEmail(); 396 + if (!$email) { 397 + throw new Exception("User has no primary email!"); 398 + } 399 + } 400 + $token = $this->generateEmailToken($email); 383 401 $uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/'); 384 402 $uri = new PhutilURI($uri); 385 - return $uri->alter('email', $this->getEmail()); 403 + return $uri->alter('email', $email->getAddress()); 404 + } 405 + 406 + public function loadPrimaryEmailAddress() { 407 + $email = $this->loadPrimaryEmail(); 408 + if (!$email) { 409 + throw new Exception("User has no primary email address!"); 410 + } 411 + return $email->getAddress(); 412 + } 413 + 414 + public function loadPrimaryEmail() { 415 + return id(new PhabricatorUserEmail())->loadOneWhere( 416 + 'userPHID = %s AND isPrimary = %d', 417 + $this->getPHID(), 418 + 1); 386 419 } 387 420 388 421 public function loadPreferences() { ··· 532 565 } 533 566 534 567 return self::getDefaultProfileImageURI(); 568 + } 569 + 570 + public static function loadOneWithEmailAddress($address) { 571 + $email = id(new PhabricatorUserEmail())->loadOneWhere( 572 + 'address = %s', 573 + $address); 574 + if (!$email) { 575 + return null; 576 + } 577 + return id(new PhabricatorUser())->loadOneWhere( 578 + 'phid = %s', 579 + $email->getUserPHID()); 535 580 } 536 581 537 582 }
+1
src/applications/people/storage/user/__init__.php
··· 10 10 phutil_require_module('phabricator', 'applications/files/storage/file'); 11 11 phutil_require_module('phabricator', 'applications/metamta/storage/mail'); 12 12 phutil_require_module('phabricator', 'applications/people/storage/base'); 13 + phutil_require_module('phabricator', 'applications/people/storage/email'); 13 14 phutil_require_module('phabricator', 'applications/people/storage/log'); 14 15 phutil_require_module('phabricator', 'applications/people/storage/preferences'); 15 16 phutil_require_module('phabricator', 'applications/phid/constants');
+8 -1
src/applications/phid/handle/data/PhabricatorObjectHandleData.php
··· 149 149 $images = mpull($images, 'getBestURI', 'getPHID'); 150 150 } 151 151 152 + // TODO: This probably should not be part of Handles anymore, only 153 + // MetaMTA actually uses it. 154 + $emails = id(new PhabricatorUserEmail())->loadAllWhere( 155 + 'userPHID IN (%Ls) AND isPrimary = 1', 156 + $phids); 157 + $emails = mpull($emails, 'getAddress', 'getUserPHID'); 158 + 152 159 foreach ($phids as $phid) { 153 160 $handle = new PhabricatorObjectHandle(); 154 161 $handle->setPHID($phid); ··· 159 166 $user = $users[$phid]; 160 167 $handle->setName($user->getUsername()); 161 168 $handle->setURI('/p/'.$user->getUsername().'/'); 162 - $handle->setEmail($user->getEmail()); 169 + $handle->setEmail(idx($emails, $phid)); 163 170 $handle->setFullName( 164 171 $user->getUsername().' ('.$user->getRealName().')'); 165 172 $handle->setAlternateID($user->getID());
+1
src/applications/phid/handle/data/__init__.php
··· 11 11 phutil_require_module('phabricator', 'applications/files/storage/file'); 12 12 phutil_require_module('phabricator', 'applications/maniphest/constants/owner'); 13 13 phutil_require_module('phabricator', 'applications/maniphest/constants/status'); 14 + phutil_require_module('phabricator', 'applications/people/storage/email'); 14 15 phutil_require_module('phabricator', 'applications/people/storage/user'); 15 16 phutil_require_module('phabricator', 'applications/phid/constants'); 16 17 phutil_require_module('phabricator', 'applications/phid/handle');
+2 -4
src/applications/repository/parser/base/PhabricatorRepositoryCommitMessageDetailParser.php
··· 1 1 <?php 2 2 3 3 /* 4 - * Copyright 2011 Facebook, Inc. 4 + * Copyright 2012 Facebook, Inc. 5 5 * 6 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 7 * you may not use this file except in compliance with the License. ··· 103 103 } 104 104 105 105 private function findUserByEmailAddress($email_address) { 106 - $by_email = id(new PhabricatorUser())->loadOneWhere( 107 - 'email = %s', 108 - $email_address); 106 + $by_email = PhabricatorUser::loadOneWithEmailAddress($email_address); 109 107 if ($by_email) { 110 108 return $by_email->getPHID(); 111 109 }
+12
src/infrastructure/setup/sql/phabricator/PhabricatorBuiltinPatchList.php
··· 859 859 'type' => 'sql', 860 860 'name' => $this->getPatchPath('userstatus.sql'), 861 861 ), 862 + 'emailtable.sql' => array( 863 + 'type' => 'sql', 864 + 'name' => $this->getPatchPath('emailtable.sql'), 865 + ), 866 + 'emailtableport.sql' => array( 867 + 'type' => 'php', 868 + 'name' => $this->getPatchPath('emailtableport.php'), 869 + ), 870 + 'emailtableremove.sql' => array( 871 + 'type' => 'sql', 872 + 'name' => $this->getPatchPath('emailtableremove.sql'), 873 + ), 862 874 ); 863 875 } 864 876