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