@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
at upstream/main 259 lines 9.5 kB view raw
1<?php 2 3 4/** 5 * This class does an unusual amount of flow control via exceptions. The intent 6 * is to make the workflows highly testable, because this code is high-stakes 7 * and difficult to test. 8 */ 9final class PhabricatorAuthInviteEngine extends Phobject { 10 11 private $viewer; 12 private $userHasConfirmedVerify; 13 14 public function setViewer(PhabricatorUser $viewer) { 15 $this->viewer = $viewer; 16 return $this; 17 } 18 19 public function getViewer() { 20 if (!$this->viewer) { 21 throw new PhutilInvalidStateException('setViewer'); 22 } 23 return $this->viewer; 24 } 25 26 public function setUserHasConfirmedVerify($confirmed) { 27 $this->userHasConfirmedVerify = $confirmed; 28 return $this; 29 } 30 31 private function shouldVerify() { 32 return $this->userHasConfirmedVerify; 33 } 34 35 /** 36 * @return PhabricatorAuthInvite 37 */ 38 public function processInviteCode($code) { 39 $viewer = $this->getViewer(); 40 41 $invite = id(new PhabricatorAuthInviteQuery()) 42 ->setViewer($viewer) 43 ->withVerificationCodes(array($code)) 44 ->executeOne(); 45 if (!$invite) { 46 throw id(new PhabricatorAuthInviteInvalidException( 47 pht('Bad Invite Code'), 48 pht( 49 'The invite code in the link you clicked is invalid. Check that '. 50 'you followed the link correctly.'))) 51 ->setCancelButtonURI('/') 52 ->setCancelButtonText(pht('Curses!')); 53 } 54 55 $accepted_phid = $invite->getAcceptedByPHID(); 56 if ($accepted_phid) { 57 if ($accepted_phid == $viewer->getPHID()) { 58 throw id(new PhabricatorAuthInviteInvalidException( 59 pht('Already Accepted'), 60 pht( 61 'You have already accepted this invitation.'))) 62 ->setCancelButtonURI('/') 63 ->setCancelButtonText(pht('Awesome')); 64 } else { 65 throw id(new PhabricatorAuthInviteInvalidException( 66 pht('Already Accepted'), 67 pht( 68 'The invite code in the link you clicked has already '. 69 'been accepted.'))) 70 ->setCancelButtonURI('/') 71 ->setCancelButtonText(pht('Continue')); 72 } 73 } 74 75 $email = id(new PhabricatorUserEmail())->loadOneWhere( 76 'address = %s', 77 $invite->getEmailAddress()); 78 79 if ($viewer->isLoggedIn()) { 80 $this->handleLoggedInInvite($invite, $viewer, $email); 81 } 82 83 if ($email) { 84 $other_user = $this->loadUserForEmail($email); 85 86 if ($email->getIsVerified()) { 87 throw id(new PhabricatorAuthInviteLoginException( 88 pht('Already Registered'), 89 pht( 90 'The email address you just clicked a link from is already '. 91 'verified and associated with a registered account (%s). Log '. 92 'in to continue.', 93 phutil_tag('strong', array(), $other_user->getName())))) 94 ->setCancelButtonText(pht('Log In')) 95 ->setCancelButtonURI($this->getLoginURI()); 96 } else if ($email->getIsPrimary()) { 97 throw id(new PhabricatorAuthInviteLoginException( 98 pht('Already Registered'), 99 pht( 100 'The email address you just clicked a link from is already '. 101 'the primary email address for a registered account (%s). Log '. 102 'in to continue.', 103 phutil_tag('strong', array(), $other_user->getName())))) 104 ->setCancelButtonText(pht('Log In')) 105 ->setCancelButtonURI($this->getLoginURI()); 106 } else if (!$this->shouldVerify()) { 107 throw id(new PhabricatorAuthInviteVerifyException( 108 pht('Already Associated'), 109 pht( 110 'The email address you just clicked a link from is already '. 111 'associated with a registered account (%s), but is not '. 112 'verified. Log in to that account to continue. If you can not '. 113 'log in, you can register a new account.', 114 phutil_tag('strong', array(), $other_user->getName())))) 115 ->setCancelButtonText(pht('Log In')) 116 ->setCancelButtonURI($this->getLoginURI()) 117 ->setSubmitButtonText(pht('Register New Account')); 118 } else { 119 // NOTE: The address is not verified and not a primary address, so 120 // we will eventually steal it if the user completes registration. 121 } 122 } 123 124 // The invite and email address are OK, but the user needs to register. 125 return $invite; 126 } 127 128 private function handleLoggedInInvite( 129 PhabricatorAuthInvite $invite, 130 PhabricatorUser $viewer, 131 ?PhabricatorUserEmail $email = null) { 132 133 if ($email && ($email->getUserPHID() !== $viewer->getPHID())) { 134 $other_user = $this->loadUserForEmail($email); 135 if ($email->getIsVerified()) { 136 throw id(new PhabricatorAuthInviteAccountException( 137 pht('Wrong Account'), 138 pht( 139 'You are logged in as %s, but the email address you just '. 140 'clicked a link from is already verified and associated '. 141 'with another account (%s). Switch accounts, then try again.', 142 phutil_tag('strong', array(), $viewer->getUsername()), 143 phutil_tag('strong', array(), $other_user->getName())))) 144 ->setSubmitButtonText(pht('Log Out')) 145 ->setSubmitButtonURI($this->getLogoutURI()) 146 ->setCancelButtonURI('/'); 147 } else if ($email->getIsPrimary()) { 148 // NOTE: We never steal primary addresses from other accounts, even 149 // if they are unverified. This would leave the other account with 150 // no address. Users can use password recovery to access the other 151 // account if they really control the address. 152 throw id(new PhabricatorAuthInviteAccountException( 153 pht('Wrong Account'), 154 pht( 155 'You are logged in as %s, but the email address you just '. 156 'clicked a link from is already the primary email address '. 157 'for another account (%s). Switch accounts, then try again.', 158 phutil_tag('strong', array(), $viewer->getUsername()), 159 phutil_tag('strong', array(), $other_user->getName())))) 160 ->setSubmitButtonText(pht('Log Out')) 161 ->setSubmitButtonURI($this->getLogoutURI()) 162 ->setCancelButtonURI('/'); 163 } else if (!$this->shouldVerify()) { 164 throw id(new PhabricatorAuthInviteVerifyException( 165 pht('Verify Email'), 166 pht( 167 'You are logged in as %s, but the email address (%s) you just '. 168 'clicked a link from is already associated with another '. 169 'account (%s). You can log out to switch accounts, or verify '. 170 'the address and attach it to your current account. Attach '. 171 'email address %s to user account %s?', 172 phutil_tag('strong', array(), $viewer->getUsername()), 173 phutil_tag('strong', array(), $invite->getEmailAddress()), 174 phutil_tag('strong', array(), $other_user->getName()), 175 phutil_tag('strong', array(), $invite->getEmailAddress()), 176 phutil_tag('strong', array(), $viewer->getUsername())))) 177 ->setSubmitButtonText( 178 pht( 179 'Verify %s', 180 $invite->getEmailAddress())) 181 ->setCancelButtonText(pht('Log Out')) 182 ->setCancelButtonURI($this->getLogoutURI()); 183 } 184 } 185 186 if (!$email) { 187 $email = id(new PhabricatorUserEmail()) 188 ->setAddress($invite->getEmailAddress()) 189 ->setIsVerified(0) 190 ->setIsPrimary(0); 191 } 192 193 if (!$email->getIsVerified()) { 194 // We're doing this check here so that we can verify the address if 195 // it's already attached to the viewer's account, just not verified. 196 if (!$this->shouldVerify()) { 197 throw id(new PhabricatorAuthInviteVerifyException( 198 pht('Verify Email'), 199 pht( 200 'Verify this email address (%s) and attach it to your '. 201 'account (%s)?', 202 phutil_tag('strong', array(), $invite->getEmailAddress()), 203 phutil_tag('strong', array(), $viewer->getUsername())))) 204 ->setSubmitButtonText( 205 pht( 206 'Verify %s', 207 $invite->getEmailAddress())) 208 ->setCancelButtonURI('/'); 209 } 210 211 $editor = id(new PhabricatorUserEditor()) 212 ->setActor($viewer); 213 214 // If this is a new email, add it to the user's account. 215 if (!$email->getUserPHID()) { 216 $editor->addEmail($viewer, $email); 217 } 218 219 // If another user added this email (but has not verified it), 220 // take it from them. 221 $editor->reassignEmail($viewer, $email); 222 223 $editor->verifyEmail($viewer, $email); 224 } 225 226 $invite->setAcceptedByPHID($viewer->getPHID()); 227 $invite->save(); 228 229 // If we make it here, the user was already logged in with the email 230 // address attached to their account and verified, or we attached it to 231 // their account (if it was not already attached) and verified it. 232 throw new PhabricatorAuthInviteRegisteredException(); 233 } 234 235 private function loadUserForEmail(PhabricatorUserEmail $email) { 236 $user = id(new PhabricatorHandleQuery()) 237 ->setViewer(PhabricatorUser::getOmnipotentUser()) 238 ->withPHIDs(array($email->getUserPHID())) 239 ->executeOne(); 240 if (!$user) { 241 throw new Exception( 242 pht( 243 'Email record ("%s") has bad associated user PHID ("%s").', 244 $email->getAddress(), 245 $email->getUserPHID())); 246 } 247 248 return $user; 249 } 250 251 private function getLoginURI() { 252 return '/auth/start/'; 253 } 254 255 private function getLogoutURI() { 256 return '/logout/'; 257 } 258 259}