@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
3final class PhabricatorAuthRegisterController
4 extends PhabricatorAuthController {
5
6 public function shouldRequireLogin() {
7 return false;
8 }
9
10 public function handleRequest(AphrontRequest $request) {
11 $viewer = $this->getViewer();
12 $account_key = $request->getURIData('akey');
13
14 if ($viewer->isLoggedIn()) {
15 return id(new AphrontRedirectResponse())->setURI('/');
16 }
17
18 $invite = $this->loadInvite();
19
20 $is_setup = false;
21 if (phutil_nonempty_string($account_key)) {
22 $result = $this->loadAccountForRegistrationOrLinking($account_key);
23 list($account, $provider, $response) = $result;
24 $is_default = false;
25 } else if ($this->isFirstTimeSetup()) {
26 $account = null;
27 $provider = null;
28 $response = null;
29 $is_default = true;
30 $is_setup = true;
31 } else {
32 list($account, $provider, $response) = $this->loadDefaultAccount($invite);
33 $is_default = true;
34 }
35
36 if ($response) {
37 return $response;
38 }
39
40 if (!$is_setup) {
41 if (!$provider->shouldAllowRegistration()) {
42 if ($invite) {
43 // If the user has an invite, we allow them to register with any
44 // provider, even a login-only provider.
45 } else {
46 // TODO: This is a routine error if you click "Login" on an external
47 // auth source which doesn't allow registration. The error should be
48 // more tailored.
49
50 return $this->renderError(
51 pht(
52 'The account you are attempting to register with uses an '.
53 'authentication provider ("%s") which does not allow '.
54 'registration. An administrator may have recently disabled '.
55 'registration with this provider.',
56 $provider->getProviderName()));
57 }
58 }
59 }
60
61 $errors = array();
62
63 $user = new PhabricatorUser();
64
65 if ($is_setup) {
66 $default_username = null;
67 $default_realname = null;
68 $default_email = null;
69 } else {
70 $default_username = $account->getUsername();
71 $default_realname = $account->getRealName();
72 $default_email = $account->getEmail();
73 }
74
75 $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT;
76 $content_source = PhabricatorContentSource::newFromRequest($request);
77
78 if ($invite) {
79 $default_email = $invite->getEmailAddress();
80 }
81
82 if ($default_email !== null) {
83 if (!PhabricatorUserEmail::isValidAddress($default_email)) {
84 $errors[] = pht(
85 'The email address associated with this external account ("%s") is '.
86 'not a valid email address and can not be used to register an '.
87 'account. Choose a different, valid address.',
88 phutil_tag('strong', array(), $default_email));
89 $default_email = null;
90 }
91 }
92
93 if ($default_email !== null) {
94 // We should bypass policy here because e.g. limiting an application use
95 // to a subset of users should not allow the others to overwrite
96 // configured application emails.
97 $application_email = id(new PhabricatorMetaMTAApplicationEmailQuery())
98 ->setViewer(PhabricatorUser::getOmnipotentUser())
99 ->withAddresses(array($default_email))
100 ->executeOne();
101 if ($application_email) {
102 $errors[] = pht(
103 'The email address associated with this account ("%s") is '.
104 'already in use by an application and can not be used to '.
105 'register a new account. Choose a different, valid address.',
106 phutil_tag('strong', array(), $default_email));
107 $default_email = null;
108 }
109 }
110
111 $show_existing = null;
112 if ($default_email !== null) {
113 // If the account source provided an email, but it's not allowed by
114 // the configuration, roadblock the user. Previously, we let the user
115 // pick a valid email address instead, but this does not align well with
116 // user expectation and it's not clear the cases it enables are valuable.
117 // See discussion in T3472.
118 if (!PhabricatorUserEmail::isAllowedAddress($default_email)) {
119 $debug_email = new PHUIInvisibleCharacterView($default_email);
120 return $this->renderError(
121 array(
122 pht(
123 'The account you are attempting to register with has an invalid '.
124 'email address (%s). This server only allows registration with '.
125 'specific email addresses:',
126 $debug_email),
127 phutil_tag('br'),
128 phutil_tag('br'),
129 PhabricatorUserEmail::describeAllowedAddresses(),
130 ));
131 }
132
133 // If the account source provided an email, but another account already
134 // has that email, just pretend we didn't get an email.
135 if ($default_email !== null) {
136 $same_email = id(new PhabricatorUserEmail())->loadOneWhere(
137 'address = %s',
138 $default_email);
139 if ($same_email) {
140 if ($invite) {
141 // We're allowing this to continue. The fact that we loaded the
142 // invite means that the address is nonprimary and unverified and
143 // we're OK to steal it.
144 } else {
145 $show_existing = $default_email;
146 $default_email = null;
147 }
148 }
149 }
150 }
151
152 if ($show_existing !== null) {
153 if (!$request->getInt('phase')) {
154 return $this->newDialog()
155 ->setTitle(pht('Email Address Already in Use'))
156 ->addHiddenInput('phase', 1)
157 ->appendParagraph(
158 pht(
159 'You are creating a new account linked to an existing '.
160 'external account.'))
161 ->appendParagraph(
162 pht(
163 'The email address ("%s") associated with the external account '.
164 'is already in use by an existing %s account. Multiple '.
165 '%s accounts may not have the same email address, so '.
166 'you can not use this email address to register a new account.',
167 phutil_tag('strong', array(), $show_existing),
168 PlatformSymbols::getPlatformServerName(),
169 PlatformSymbols::getPlatformServerName()))
170 ->appendParagraph(
171 pht(
172 'If you want to register a new account, continue with this '.
173 'registration workflow and choose a new, unique email address '.
174 'for the new account.'))
175 ->appendParagraph(
176 pht(
177 'If you want to link an existing %s account to this '.
178 'external account, do not continue. Instead: log in to your '.
179 'existing account, then go to "Settings" and link the account '.
180 'in the "External Accounts" panel.',
181 PlatformSymbols::getPlatformServerName()))
182 ->appendParagraph(
183 pht(
184 'If you continue, you will create a new account. You will not '.
185 'be able to link this external account to an existing account.'))
186 ->addCancelButton('/auth/login/', pht('Cancel'))
187 ->addSubmitButton(pht('Create New Account'));
188 } else {
189 $errors[] = pht(
190 'The external account you are registering with has an email address '.
191 'that is already in use ("%s") by an existing %s account. '.
192 'Choose a new, valid email address to register a new account.',
193 phutil_tag('strong', array(), $show_existing),
194 PlatformSymbols::getPlatformServerName());
195 }
196 }
197
198 $profile = id(new PhabricatorRegistrationProfile())
199 ->setDefaultUsername($default_username)
200 ->setDefaultEmail($default_email)
201 ->setDefaultRealName($default_realname)
202 ->setCanEditUsername(true)
203 ->setCanEditEmail(($default_email === null))
204 ->setCanEditRealName(true)
205 ->setShouldVerifyEmail(false);
206
207 $event_type = PhabricatorEventType::TYPE_AUTH_WILLREGISTERUSER;
208 $event_data = array(
209 'account' => $account,
210 'profile' => $profile,
211 );
212
213 $event = id(new PhabricatorEvent($event_type, $event_data))
214 ->setUser($user);
215 PhutilEventEngine::dispatchEvent($event);
216
217 $default_username = $profile->getDefaultUsername();
218 $default_email = $profile->getDefaultEmail();
219 $default_realname = $profile->getDefaultRealName();
220
221 $can_edit_username = $profile->getCanEditUsername();
222 $can_edit_email = $profile->getCanEditEmail();
223 $can_edit_realname = $profile->getCanEditRealName();
224
225 if ($is_setup) {
226 $must_set_password = false;
227 } else {
228 $must_set_password = $provider->shouldRequireRegistrationPassword();
229 }
230
231 $can_edit_anything = $profile->getCanEditAnything() || $must_set_password;
232 $force_verify = $profile->getShouldVerifyEmail();
233
234 // Automatically verify the administrator's email address during first-time
235 // setup.
236 if ($is_setup) {
237 $force_verify = true;
238 }
239
240 $value_username = $default_username;
241 $value_realname = $default_realname;
242 $value_email = $default_email;
243 $value_password = null;
244
245 $require_real_name = PhabricatorEnv::getEnvConfig('user.require-real-name');
246
247 $e_username = phutil_nonempty_string($value_username) ? null : true;
248 $e_realname = $require_real_name ? true : null;
249 $e_email = phutil_nonempty_string($value_email) ? null : true;
250 $e_password = true;
251 $e_captcha = true;
252
253 $skip_captcha = false;
254 if ($invite) {
255 // If the user is accepting an invite, assume they're trustworthy enough
256 // that we don't need to CAPTCHA them.
257 $skip_captcha = true;
258 }
259
260 $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
261 $min_len = (int)$min_len;
262
263 $from_invite = $request->getStr('invite');
264 if ($from_invite && $can_edit_username) {
265 $value_username = $request->getStr('username');
266 $e_username = null;
267 }
268
269 $try_register =
270 ($request->isFormPost() || !$can_edit_anything) &&
271 !$from_invite &&
272 ($request->getInt('phase') != 1);
273
274 if ($try_register) {
275 $errors = array();
276
277 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
278
279 if ($must_set_password && !$skip_captcha) {
280 $e_captcha = pht('Again');
281
282 $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request);
283 if (!$captcha_ok) {
284 $errors[] = pht('Captcha response is incorrect, try again.');
285 $e_captcha = pht('Invalid');
286 }
287 }
288
289 if ($can_edit_username) {
290 $value_username = $request->getStr('username');
291 if (!strlen($value_username)) {
292 $e_username = pht('Required');
293 $errors[] = pht('Username is required.');
294 } else if (!PhabricatorUser::validateUsername($value_username)) {
295 $e_username = pht('Invalid');
296 $errors[] = PhabricatorUser::describeValidUsername($value_username);
297 } else {
298 $e_username = null;
299 }
300 }
301
302 if ($must_set_password) {
303 $value_password = $request->getStr('password');
304 $value_confirm = $request->getStr('confirm');
305
306 $password_envelope = new PhutilOpaqueEnvelope($value_password);
307 $confirm_envelope = new PhutilOpaqueEnvelope($value_confirm);
308
309 $engine = id(new PhabricatorAuthPasswordEngine())
310 ->setViewer($user)
311 ->setContentSource($content_source)
312 ->setPasswordType($account_type)
313 ->setObject($user);
314
315 try {
316 $engine->checkNewPassword($password_envelope, $confirm_envelope);
317 $e_password = null;
318 } catch (PhabricatorAuthPasswordException $ex) {
319 $errors[] = $ex->getMessage();
320 $e_password = $ex->getPasswordError();
321 }
322 }
323
324 if ($can_edit_email) {
325 $value_email = $request->getStr('email');
326 if (!strlen($value_email)) {
327 $e_email = pht('Required');
328 $errors[] = pht('Email is required.');
329 } else if (!PhabricatorUserEmail::isValidAddress($value_email)) {
330 $e_email = pht('Invalid');
331 $errors[] = PhabricatorUserEmail::describeValidAddresses();
332 } else if (!PhabricatorUserEmail::isAllowedAddress($value_email)) {
333 $e_email = pht('Disallowed');
334 $errors[] = PhabricatorUserEmail::describeAllowedAddresses();
335 } else {
336 $e_email = null;
337 }
338 }
339
340 if ($can_edit_realname) {
341 $value_realname = $request->getStr('realName');
342 if (!strlen($value_realname) && $require_real_name) {
343 $e_realname = pht('Required');
344 $errors[] = pht('Real name is required.');
345 } else if ($value_realname &&
346 !PhabricatorUser::validateRealName($value_realname)) {
347 $e_realname = pht('Invalid');
348 $errors[] = PhabricatorUser::describeValidRealName();
349 } else {
350 $e_realname = null;
351 }
352 }
353
354 if (!$errors) {
355 if (!$is_setup) {
356 $image = $this->loadProfilePicture($account);
357 if ($image) {
358 $user->setProfileImagePHID($image->getPHID());
359 }
360 }
361
362 try {
363 $verify_email = false;
364
365 if ($force_verify) {
366 $verify_email = true;
367 }
368
369 if (!$is_setup) {
370 if ($value_email === $default_email) {
371 if ($account->getEmailVerified()) {
372 $verify_email = true;
373 }
374
375 if ($provider->shouldTrustEmails()) {
376 $verify_email = true;
377 }
378
379 if ($invite) {
380 $verify_email = true;
381 }
382 }
383 }
384
385 $email_obj = null;
386 if ($invite) {
387 // If we have a valid invite, this email may exist but be
388 // nonprimary and unverified, so we'll reassign it.
389 $email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
390 'address = %s',
391 $value_email);
392 }
393 if (!$email_obj) {
394 $email_obj = id(new PhabricatorUserEmail())
395 ->setAddress($value_email);
396 }
397
398 $email_obj->setIsVerified((int)$verify_email);
399
400 $user->setUsername($value_username);
401 $user->setRealname($value_realname);
402
403 if ($is_setup) {
404 $must_approve = false;
405 } else if ($invite) {
406 $must_approve = false;
407 } else {
408 $must_approve = PhabricatorEnv::getEnvConfig(
409 'auth.require-approval');
410 }
411
412 if ($must_approve) {
413 $user->setIsApproved(0);
414 } else {
415 $user->setIsApproved(1);
416 }
417
418 if ($invite) {
419 $allow_reassign_email = true;
420 } else {
421 $allow_reassign_email = false;
422 }
423
424 $user->openTransaction();
425
426 $editor = id(new PhabricatorUserEditor())
427 ->setActor($user);
428
429 $editor->createNewUser($user, $email_obj, $allow_reassign_email);
430 if ($must_set_password) {
431 $password_object = PhabricatorAuthPassword::initializeNewPassword(
432 $user,
433 $account_type);
434
435 $password_object
436 ->setPassword($password_envelope, $user)
437 ->save();
438 }
439
440 if ($is_setup) {
441 $xactions = array();
442 $xactions[] = id(new PhabricatorUserTransaction())
443 ->setTransactionType(
444 PhabricatorUserEmpowerTransaction::TRANSACTIONTYPE)
445 ->setNewValue(true);
446
447 $actor = PhabricatorUser::getOmnipotentUser();
448 $content_source = PhabricatorContentSource::newFromRequest(
449 $request);
450
451 $people_application_phid = id(new PhabricatorPeopleApplication())
452 ->getPHID();
453
454 $transaction_editor = id(new PhabricatorUserTransactionEditor())
455 ->setActor($actor)
456 ->setActingAsPHID($people_application_phid)
457 ->setContentSource($content_source)
458 ->setContinueOnMissingFields(true);
459
460 $transaction_editor->applyTransactions($user, $xactions);
461 }
462
463 if (!$is_setup) {
464 $account->setUserPHID($user->getPHID());
465 $account->save();
466 }
467
468 $user->saveTransaction();
469
470 if (!$email_obj->getIsVerified()) {
471 $email_obj->sendVerificationEmail($user);
472 }
473
474 if ($must_approve) {
475 $this->sendWaitingForApprovalEmail($user);
476 }
477
478 if ($invite) {
479 $invite->setAcceptedByPHID($user->getPHID())->save();
480 }
481
482 return $this->loginUser($user);
483 } catch (AphrontDuplicateKeyQueryException $exception) {
484 $same_username = id(new PhabricatorUser())->loadOneWhere(
485 'userName = %s',
486 $user->getUserName());
487
488 $same_email = id(new PhabricatorUserEmail())->loadOneWhere(
489 'address = %s',
490 $value_email);
491
492 if ($same_username) {
493 $e_username = pht('Duplicate');
494 $errors[] = pht('Another user already has that username.');
495 }
496
497 if ($same_email) {
498 // We do not allow two user accounts with the same email address.
499 $e_email = pht('Duplicate');
500 $errors[] = pht('Another user already has that email.');
501 }
502
503 if (!$same_username && !$same_email) {
504 throw $exception;
505 }
506 }
507 }
508
509 unset($unguarded);
510 }
511
512 $form = id(new AphrontFormView())
513 ->setUser($request->getUser())
514 ->addHiddenInput('phase', 2);
515
516 if (!$is_default) {
517 $form->appendChild(
518 id(new AphrontFormMarkupControl())
519 ->setLabel(pht('External Account'))
520 ->setValue(
521 id(new PhabricatorAuthAccountView())
522 ->setUser($request->getUser())
523 ->setExternalAccount($account)
524 ->setAuthProvider($provider)));
525 }
526
527 if ($can_edit_username) {
528 $form->appendChild(
529 id(new AphrontFormTextControl())
530 ->setLabel(pht('Username'))
531 ->setName('username')
532 ->setValue($value_username)
533 ->setError($e_username));
534 } else {
535 $form->appendChild(
536 id(new AphrontFormMarkupControl())
537 ->setLabel(pht('Username'))
538 ->setValue($value_username)
539 ->setError($e_username));
540 }
541
542 if ($can_edit_realname) {
543 $form->appendChild(
544 id(new AphrontFormTextControl())
545 ->setLabel(pht('Real Name'))
546 ->setName('realName')
547 ->setValue($value_realname)
548 ->setError($e_realname));
549 }
550
551 if ($must_set_password) {
552 $form->appendChild(
553 id(new AphrontFormPasswordControl())
554 ->setLabel(pht('Password'))
555 ->setName('password')
556 ->setError($e_password));
557 $form->appendChild(
558 id(new AphrontFormPasswordControl())
559 ->setLabel(pht('Confirm Password'))
560 ->setName('confirm')
561 ->setError($e_password)
562 ->setCaption(
563 $min_len
564 ? pht('Minimum length of %d characters.', $min_len)
565 : null));
566 }
567
568 if ($can_edit_email) {
569 $form->appendChild(
570 id(new AphrontFormTextControl())
571 ->setLabel(pht('Email'))
572 ->setName('email')
573 ->setValue($value_email)
574 ->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
575 ->setError($e_email));
576 }
577
578 if ($must_set_password && !$skip_captcha) {
579 $form->appendChild(
580 id(new AphrontFormRecaptchaControl())
581 ->setLabel(pht('Captcha'))
582 ->setError($e_captcha));
583 }
584
585 $submit = new AphrontFormSubmitControl();
586
587 if ($is_setup) {
588 $submit
589 ->setValue(pht('Create Admin Account'));
590 } else {
591 $submit
592 ->addCancelButton($this->getApplicationURI('start/'))
593 ->setValue(pht('Register Account'));
594 }
595
596
597 $form->appendChild($submit);
598
599 $crumbs = $this->buildApplicationCrumbs();
600
601 if ($is_setup) {
602 $crumbs->addTextCrumb(pht('Setup Admin Account'));
603 $title = pht(
604 'Welcome to %s',
605 PlatformSymbols::getPlatformServerName());
606 } else {
607 $crumbs->addTextCrumb(pht('Register'));
608 $crumbs->addTextCrumb($provider->getProviderName());
609 $title = pht('Create a New Account');
610 }
611 $crumbs->setBorder(true);
612
613 $welcome_view = null;
614 if ($is_setup) {
615 $welcome_view = id(new PHUIInfoView())
616 ->setSeverity(PHUIInfoView::SEVERITY_NOTICE)
617 ->setTitle(
618 pht(
619 'Welcome to %s',
620 PlatformSymbols::getPlatformServerName()))
621 ->appendChild(
622 pht(
623 'Installation is complete. Register your administrator account '.
624 'below to log in. You will be able to configure options and add '.
625 'authentication mechanisms later on.'));
626 }
627
628 $object_box = id(new PHUIObjectBoxView())
629 ->setForm($form)
630 ->setFormErrors($errors);
631
632 $invite_header = null;
633 if ($invite) {
634 $invite_header = $this->renderInviteHeader($invite);
635 }
636
637 $header = id(new PHUIHeaderView())
638 ->setHeader($title);
639
640 $view = id(new PHUITwoColumnView())
641 ->setHeader($header)
642 ->setFooter(
643 array(
644 $welcome_view,
645 $invite_header,
646 $object_box,
647 ));
648
649 return $this->newPage()
650 ->setTitle($title)
651 ->setCrumbs($crumbs)
652 ->appendChild($view);
653 }
654
655 private function loadDefaultAccount($invite) {
656 $providers = PhabricatorAuthProvider::getAllEnabledProviders();
657 $account = null;
658 $provider = null;
659 $response = null;
660
661 foreach ($providers as $key => $candidate_provider) {
662 if (!$invite) {
663 if (!$candidate_provider->shouldAllowRegistration()) {
664 unset($providers[$key]);
665 continue;
666 }
667 }
668
669 if (!$candidate_provider->isDefaultRegistrationProvider()) {
670 unset($providers[$key]);
671 }
672 }
673
674 if (!$providers) {
675 $response = $this->renderError(
676 pht(
677 'There are no configured default registration providers.'));
678 return array($account, $provider, $response);
679 } else if (count($providers) > 1) {
680 $response = $this->renderError(
681 pht('There are too many configured default registration providers.'));
682 return array($account, $provider, $response);
683 }
684
685 $provider = head($providers);
686 $account = $provider->newDefaultExternalAccount();
687
688 return array($account, $provider, $response);
689 }
690
691 private function loadProfilePicture(PhabricatorExternalAccount $account) {
692 $phid = $account->getProfileImagePHID();
693 if (!$phid) {
694 return null;
695 }
696
697 // NOTE: Use of omnipotent user is okay here because the registering user
698 // can not control the field value, and we can't use their user object to
699 // do meaningful policy checks anyway since they have not registered yet.
700 // Reaching this means the user holds the account secret key and the
701 // registration secret key, and thus has permission to view the image.
702 $file = id(new PhabricatorFileQuery())
703 ->setViewer(PhabricatorUser::getOmnipotentUser())
704 ->withPHIDs(array($phid))
705 ->executeOne();
706 if (!$file) {
707 return null;
708 }
709
710 $xform = PhabricatorFileTransform::getTransformByKey(
711 PhabricatorFileThumbnailTransform::TRANSFORM_PROFILE);
712 return $xform->getOrExecuteTransformExplicit($file);
713 }
714
715 protected function renderError($message) {
716 return $this->renderErrorPage(
717 pht('Registration Failed'),
718 array($message));
719 }
720
721 private function sendWaitingForApprovalEmail(PhabricatorUser $user) {
722 $title = pht(
723 '[%s] New User "%s" Awaiting Approval',
724 PlatformSymbols::getPlatformServerName(),
725 $user->getUsername());
726
727 $body = new PhabricatorMetaMTAMailBody();
728
729 $body->addRawSection(
730 pht(
731 'Newly registered user "%s" is awaiting account approval by an '.
732 'administrator.',
733 $user->getUsername()));
734
735 $body->addLinkSection(
736 pht('APPROVAL QUEUE'),
737 PhabricatorEnv::getProductionURI(
738 '/people/query/approval/'));
739
740 $body->addLinkSection(
741 pht('DISABLE APPROVAL QUEUE'),
742 PhabricatorEnv::getProductionURI(
743 '/config/edit/auth.require-approval/'));
744
745 $admins = id(new PhabricatorPeopleQuery())
746 ->setViewer(PhabricatorUser::getOmnipotentUser())
747 ->withIsAdmin(true)
748 ->execute();
749
750 if (!$admins) {
751 return;
752 }
753
754 $mail = id(new PhabricatorMetaMTAMail())
755 ->addTos(mpull($admins, 'getPHID'))
756 ->setSubject($title)
757 ->setBody($body->render())
758 ->saveAndSend();
759 }
760
761}