@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 administrators to change usernames

Summary:
Give them a big essay about how it's dangerous, but allow them to do it formally.

Because the username is part of the password salt, users must change their passwords after a username change.

Make password reset links work for already-logged-in-users since there's no reason not to (if you have a reset link, you can log out and use it) and it's much less confusing if you get this email and are already logged in.

Depends on: D2651

Test Plan: Changed a user's username to all kinds of crazy things. Clicked reset links in email. Tried to make invalid/nonsense name changes.

Reviewers: btrahan, vrana

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T1303

Differential Revision: https://secure.phabricator.com/D2657

+201 -45
+5 -19
src/applications/auth/controller/PhabricatorEmailTokenController.php
··· 36 36 return new Aphront400Response(); 37 37 } 38 38 39 - if ($request->getUser()->getPHID()) { 40 - $view = new AphrontRequestFailureView(); 41 - $view->setHeader('Already Logged In'); 42 - $view->appendChild( 43 - '<p>You are already logged in.</p>'); 44 - $view->appendChild( 45 - '<div class="aphront-failure-continue">'. 46 - '<a class="button" href="/">Return Home</a>'. 47 - '</div>'); 48 - return $this->buildStandardPageResponse( 49 - $view, 50 - array( 51 - 'title' => 'Already Logged In', 52 - )); 53 - } 54 - 55 39 $token = $this->token; 56 40 $email = $request->getStr('email'); 57 41 ··· 103 87 // enough, without requiring users to go through a second round of email 104 88 // verification. 105 89 106 - $target_email->setIsVerified(1); 107 - $target_email->save(); 90 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 91 + $target_email->setIsVerified(1); 92 + $target_email->save(); 93 + $session_key = $target_user->establishSession('web'); 94 + unset($unguarded); 108 95 109 - $session_key = $target_user->establishSession('web'); 110 96 $request->setCookie('phusr', $target_user->getUsername()); 111 97 $request->setCookie('phsid', $session_key); 112 98
+43
src/applications/people/PhabricatorUserEditor.php
··· 148 148 } 149 149 150 150 151 + /** 152 + * @task edit 153 + */ 154 + public function changeUsername(PhabricatorUser $user, $username) { 155 + $actor = $this->requireActor(); 156 + 157 + if (!$user->getID()) { 158 + throw new Exception("User has not been created yet!"); 159 + } 160 + 161 + if (!PhabricatorUser::validateUsername($username)) { 162 + $valid = PhabricatorUser::describeValidUsername(); 163 + throw new Exception("Username is invalid! {$valid}"); 164 + } 165 + 166 + $old_username = $user->getUsername(); 167 + 168 + $user->openTransaction(); 169 + $user->reload(); 170 + $user->setUsername($username); 171 + 172 + try { 173 + $user->save(); 174 + } catch (AphrontQueryDuplicateKeyException $ex) { 175 + $user->setUsername($old_username); 176 + $user->killTransaction(); 177 + throw $ex; 178 + } 179 + 180 + $log = PhabricatorUserLog::newLog( 181 + $this->actor, 182 + $user, 183 + PhabricatorUserLog::ACTION_CHANGE_USERNAME); 184 + $log->setOldValue($old_username); 185 + $log->setNewValue($username); 186 + $log->save(); 187 + 188 + $user->saveTransaction(); 189 + 190 + $user->sendUsernameChangeEmail($actor, $old_username); 191 + } 192 + 193 + 151 194 /* -( Editing Roles )------------------------------------------------------ */ 152 195 153 196
+112 -26
src/applications/people/controller/PhabricatorPeopleEditController.php
··· 41 41 if (!$user) { 42 42 return new Aphront404Response(); 43 43 } 44 + $base_uri = '/people/edit/'.$user->getID().'/'; 44 45 } else { 45 46 $user = new PhabricatorUser(); 47 + $base_uri = '/people/edit/'; 46 48 } 47 49 48 - $views = array( 49 - 'basic' => 'Basic Information', 50 - 'role' => 'Edit Roles', 51 - 'cert' => 'Conduit Certificate', 52 - ); 50 + $nav = new AphrontSideNavFilterView(); 51 + $nav->setBaseURI(new PhutilURI($base_uri)); 52 + $nav->addFilter('basic', 'Basic Information'); 53 + $nav->addFilter('role', 'Edit Roles'); 54 + $nav->addFilter('cert', 'Conduit Certificate'); 55 + $nav->addSpacer(); 56 + $nav->addFilter('rename', 'Change Username'); 53 57 54 58 if (!$user->getID()) { 55 - $view = 'basic'; 56 - } else if (isset($views[$this->view])) { 57 - $view = $this->view; 58 - } else { 59 - $view = 'basic'; 59 + $this->view = 'basic'; 60 60 } 61 + $view = $nav->selectFilter($this->view, 'basic'); 61 62 62 63 $content = array(); 63 64 ··· 79 80 case 'cert': 80 81 $response = $this->processCertificateRequest($user); 81 82 break; 83 + case 'rename': 84 + $response = $this->processRenameRequest($user); 85 + break; 86 + default: 87 + return new Aphront404Response(); 82 88 } 83 89 84 90 if ($response instanceof AphrontResponse) { ··· 88 94 $content[] = $response; 89 95 90 96 if ($user->getID()) { 91 - $side_nav = new AphrontSideNavView(); 92 - $side_nav->appendChild($content); 93 - foreach ($views as $key => $name) { 94 - $side_nav->addNavItem( 95 - phutil_render_tag( 96 - 'a', 97 - array( 98 - 'href' => '/people/edit/'.$user->getID().'/'.$key.'/', 99 - 'class' => ($key == $view) 100 - ? 'aphront-side-nav-selected' 101 - : null, 102 - ), 103 - phutil_escape_html($name))); 104 - } 105 - $content = $side_nav; 97 + $nav->appendChild($content); 98 + $content = $nav; 106 99 } 107 100 108 101 return $this->buildStandardPageResponse( ··· 444 437 $request = $this->getRequest(); 445 438 $admin = $request->getUser(); 446 439 447 - 448 440 $form = new AphrontFormView(); 449 441 $form 450 442 ->setUser($admin) ··· 480 472 481 473 return array($panel); 482 474 } 475 + 476 + private function processRenameRequest(PhabricatorUser $user) { 477 + $request = $this->getRequest(); 478 + $admin = $request->getUser(); 479 + 480 + $e_username = true; 481 + $username = $user->getUsername(); 482 + 483 + $errors = array(); 484 + if ($request->isFormPost()) { 485 + 486 + $username = $request->getStr('username'); 487 + if (!strlen($username)) { 488 + $e_username = 'Required'; 489 + $errors[] = 'New username is required.'; 490 + } else if ($username == $user->getUsername()) { 491 + $e_username = 'Invalid'; 492 + $errors[] = 'New username must be different from old username.'; 493 + } else if (!PhabricatorUser::validateUsername($username)) { 494 + $e_username = 'Invalid'; 495 + $errors[] = PhabricatorUser::describeValidUsername(); 496 + } 497 + 498 + if (!$errors) { 499 + try { 500 + 501 + id(new PhabricatorUserEditor()) 502 + ->setActor($admin) 503 + ->changeUsername($user, $username); 504 + 505 + return id(new AphrontRedirectResponse()) 506 + ->setURI($request->getRequestURI()->alter('saved', true)); 507 + } catch (AphrontQueryDuplicateKeyException $ex) { 508 + $e_username = 'Not Unique'; 509 + $errors[] = 'Another user already has that username.'; 510 + } 511 + } 512 + } 513 + 514 + if ($errors) { 515 + $errors = id(new AphrontErrorView()) 516 + ->setTitle('Form Errors') 517 + ->setErrors($errors); 518 + } else { 519 + $errors = null; 520 + } 521 + 522 + $form = new AphrontFormView(); 523 + $form 524 + ->setUser($admin) 525 + ->setAction($request->getRequestURI()) 526 + ->appendChild( 527 + '<p class="aphront-form-instructions">'. 528 + '<strong>Be careful when renaming users!</strong> '. 529 + 'The old username will no longer be tied to the user, so anything '. 530 + 'which uses it (like old commit messages) will no longer associate '. 531 + 'correctly. And if you give a user a username which some other user '. 532 + 'used to have, username lookups will begin returning the wrong '. 533 + 'user.'. 534 + '</p>'. 535 + '<p class="aphront-form-instructions">'. 536 + 'It is generally safe to rename newly created users (and test users '. 537 + 'and so on), but less safe to rename established users and unsafe '. 538 + 'to reissue a username.'. 539 + '</p>'. 540 + '<p class="aphront-form-instructions">'. 541 + 'Users who rely on password auth will need to reset their password '. 542 + 'after their username is changed (their username is part of the '. 543 + 'salt in the password hash). They will receive an email with '. 544 + 'instructions on how to do this.'. 545 + '</p>') 546 + ->appendChild( 547 + id(new AphrontFormStaticControl()) 548 + ->setLabel('Old Username') 549 + ->setValue($user->getUsername())) 550 + ->appendChild( 551 + id(new AphrontFormTextControl()) 552 + ->setLabel('New Username') 553 + ->setValue($username) 554 + ->setName('username') 555 + ->setError($e_username)) 556 + ->appendChild( 557 + id(new AphrontFormSubmitControl()) 558 + ->setValue('Change Username')); 559 + 560 + $panel = new AphrontPanelView(); 561 + $panel->setHeader('Change Username'); 562 + $panel->setWidth(AphrontPanelView::WIDTH_FORM); 563 + $panel->appendChild($form); 564 + 565 + return array($errors, $panel); 566 + } 567 + 568 + 483 569 484 570 private function getRoleInstructions() { 485 571 $roles_link = phutil_render_tag(
+40
src/applications/people/storage/PhabricatorUser.php
··· 549 549 ->saveAndSend(); 550 550 } 551 551 552 + public function sendUsernameChangeEmail( 553 + PhabricatorUser $admin, 554 + $old_username) { 555 + 556 + $admin_username = $admin->getUserName(); 557 + $admin_realname = $admin->getRealName(); 558 + $new_username = $this->getUserName(); 559 + 560 + $password_instructions = null; 561 + if (PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) { 562 + $uri = $this->getEmailLoginURI(); 563 + $password_instructions = <<<EOTXT 564 + If you use a password to login, you'll need to reset it before you can login 565 + again. You can reset your password by following this link: 566 + 567 + {$uri} 568 + 569 + And, of course, you'll need to use your new username to login from now on. If 570 + you use OAuth to login, nothing should change. 571 + 572 + EOTXT; 573 + } 574 + 575 + $body = <<<EOBODY 576 + {$admin_username} ({$admin_realname}) has changed your Phabricator username. 577 + 578 + Old Username: {$old_username} 579 + New Username: {$new_username} 580 + 581 + {$password_instructions} 582 + EOBODY; 583 + 584 + $mail = id(new PhabricatorMetaMTAMail()) 585 + ->addTos(array($this->getPHID())) 586 + ->setSubject('[Phabricator] Username Changed') 587 + ->setBody($body) 588 + ->setFrom($admin->getPHID()) 589 + ->saveAndSend(); 590 + } 591 + 552 592 public static function describeValidUsername() { 553 593 return 'Usernames must contain only numbers, letters, period, underscore '. 554 594 'and hyphen, and can not end with a period.';
+1
src/applications/people/storage/PhabricatorUserLog.php
··· 37 37 const ACTION_EMAIL_ADD = 'email-add'; 38 38 39 39 const ACTION_CHANGE_PASSWORD = 'change-password'; 40 + const ACTION_CHANGE_USERNAME = 'change-username'; 40 41 41 42 protected $actorPHID; 42 43 protected $userPHID;