@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 761 lines 25 kB view raw
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}