Page MenuHomePhabricator

D11737.id28302.diff
No OneTemporary

D11737.id28302.diff

diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php
--- a/src/applications/auth/constants/PhabricatorCookies.php
+++ b/src/applications/auth/constants/PhabricatorCookies.php
@@ -57,6 +57,12 @@
const COOKIE_HISEC = 'jump_to_hisec';
+ /**
+ * Stores an invite code.
+ */
+ const COOKIE_INVITE = 'invite';
+
+
/* -( Client ID Cookie )--------------------------------------------------- */
diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php
--- a/src/applications/auth/controller/PhabricatorAuthController.php
+++ b/src/applications/auth/controller/PhabricatorAuthController.php
@@ -108,6 +108,9 @@
// Clear the client ID / OAuth state key.
$request->clearCookie(PhabricatorCookies::COOKIE_CLIENTID);
+
+ // Clear the invite cookie.
+ $request->clearCookie(PhabricatorCookies::COOKIE_INVITE);
}
private function buildLoginValidateResponse(PhabricatorUser $user) {
@@ -246,4 +249,57 @@
return array($account, $provider, null);
}
+ protected function loadInvite() {
+ $invite_cookie = PhabricatorCookies::COOKIE_INVITE;
+ $invite_code = $this->getRequest()->getCookie($invite_cookie);
+ if (!$invite_code) {
+ return null;
+ }
+
+ $engine = id(new PhabricatorAuthInviteEngine())
+ ->setViewer($this->getViewer())
+ ->setUserHasConfirmedVerify(true);
+
+ try {
+ return $engine->processInviteCode($invite_code);
+ } catch (Exception $ex) {
+ // If this fails for any reason, just drop the invite. In normal
+ // circumstances, we gave them a detailed explanation of any error
+ // before they jumped into this workflow.
+ return null;
+ }
+ }
+
+ protected function renderInviteHeader(PhabricatorAuthInvite $invite) {
+ $viewer = $this->getViewer();
+
+ $invite_author = id(new PhabricatorPeopleQuery())
+ ->setViewer($viewer)
+ ->withPHIDs(array($invite->getAuthorPHID()))
+ ->needProfileImage(true)
+ ->executeOne();
+
+ // If we can't load the author for some reason, just drop this message.
+ // We lose the value of contextualizing things without author details.
+ if (!$invite_author) {
+ return null;
+ }
+
+ $invite_item = id(new PHUIObjectItemView())
+ ->setHeader(pht('Welcome to Phabricator!'))
+ ->setImageURI($invite_author->getProfileImageURI())
+ ->addAttribute(
+ pht(
+ '%s has invited you to join Phabricator.',
+ $invite_author->getFullName()));
+
+ $invite_list = id(new PHUIObjectItemListView())
+ ->addItem($invite_item)
+ ->setFlush(true);
+
+ return id(new PHUIBoxView())
+ ->addMargin(PHUI::MARGIN_LARGE)
+ ->appendChild($invite_list);
+ }
+
}
diff --git a/src/applications/auth/controller/PhabricatorAuthInviteController.php b/src/applications/auth/controller/PhabricatorAuthInviteController.php
--- a/src/applications/auth/controller/PhabricatorAuthInviteController.php
+++ b/src/applications/auth/controller/PhabricatorAuthInviteController.php
@@ -17,8 +17,10 @@
$engine->setUserHasConfirmedVerify(true);
}
+ $invite_code = $request->getURIData('code');
+
try {
- $invite = $engine->processInviteCode($request->getURIData('code'));
+ $invite = $engine->processInviteCode($invite_code);
} catch (PhabricatorAuthInviteDialogException $ex) {
$response = $this->newDialog()
->setTitle($ex->getTitle())
@@ -48,10 +50,13 @@
return id(new AphrontRedirectResponse())->setURI('/');
}
+ // Give the user a cookie with the invite code and send them through
+ // normal registration. We'll adjust the flow there.
+ $request->setCookie(
+ PhabricatorCookies::COOKIE_INVITE,
+ $invite_code);
- // TODO: This invite is good, but we need to drive the user through
- // registration.
- throw new Exception(pht('TODO: Build invite/registration workflow.'));
+ return id(new AphrontRedirectResponse())->setURI('/auth/start/');
}
diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
--- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php
+++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php
@@ -38,19 +38,25 @@
return $response;
}
- if (!$provider->shouldAllowRegistration()) {
+ $invite = $this->loadInvite();
- // TODO: This is a routine error if you click "Login" on an external
- // auth source which doesn't allow registration. The error should be
- // more tailored.
+ if (!$provider->shouldAllowRegistration()) {
+ if ($invite) {
+ // If the user has an invite, we allow them to register with any
+ // provider, even a login-only provider.
+ } else {
+ // TODO: This is a routine error if you click "Login" on an external
+ // auth source which doesn't allow registration. The error should be
+ // more tailored.
- return $this->renderError(
- pht(
- 'The account you are attempting to register with uses an '.
- 'authentication provider ("%s") which does not allow registration. '.
- 'An administrator may have recently disabled registration with this '.
- 'provider.',
- $provider->getProviderName()));
+ return $this->renderError(
+ pht(
+ 'The account you are attempting to register with uses an '.
+ 'authentication provider ("%s") which does not allow '.
+ 'registration. An administrator may have recently disabled '.
+ 'registration with this provider.',
+ $provider->getProviderName()));
+ }
}
$user = new PhabricatorUser();
@@ -59,9 +65,15 @@
$default_realname = $account->getRealName();
$default_email = $account->getEmail();
+
+ if ($invite) {
+ $default_email = $invite->getEmailAddress();
+ }
+
if (!PhabricatorUserEmail::isValidAddress($default_email)) {
$default_email = null;
}
+
if ($default_email !== null) {
// We should bypass policy here becase e.g. limiting an application use
// to a subset of users should not allow the others to overwrite
@@ -105,7 +117,13 @@
'address = %s',
$default_email);
if ($same_email) {
- $default_email = null;
+ if ($invite) {
+ // We're allowing this to continue. The fact that we loaded the
+ // invite means that the address is nonprimary and unverified and
+ // we're OK to steal it.
+ } else {
+ $default_email = null;
+ }
}
}
}
@@ -166,7 +184,13 @@
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
- if ($request->isFormPost() || !$can_edit_anything) {
+ $from_invite = $request->getStr('invite');
+ if ($from_invite && $can_edit_username) {
+ $value_username = $request->getStr('username');
+ $e_username = null;
+ }
+
+ if (($request->isFormPost() || !$can_edit_anything) && !$from_invite) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
if ($must_set_password) {
@@ -252,28 +276,48 @@
}
try {
+ $verify_email = false;
+
if ($force_verify) {
$verify_email = true;
- } else {
- $verify_email =
- ($account->getEmailVerified()) &&
- ($value_email === $default_email);
}
- if ($provider->shouldTrustEmails() &&
- $value_email === $default_email) {
- $verify_email = true;
+ if ($value_email === $default_email) {
+ if ($account->getEmailVerified()) {
+ $verify_email = true;
+ }
+
+ if ($provider->shouldTrustEmails()) {
+ $verify_email = true;
+ }
+
+ if ($invite) {
+ $verify_email = true;
+ }
}
- $email_obj = id(new PhabricatorUserEmail())
- ->setAddress($value_email)
- ->setIsVerified((int)$verify_email);
+ $email_obj = null;
+ if ($invite) {
+ // If we have a valid invite, this email may exist but be
+ // nonprimary and unverified, so we'll reassign it.
+ $email_obj = id(new PhabricatorUserEmail())->loadOneWhere(
+ 'address = %s',
+ $value_email);
+ }
+ if (!$email_obj) {
+ $email_obj = id(new PhabricatorUserEmail())
+ ->setAddress($value_email);
+ }
+
+ $email_obj->setIsVerified((int)$verify_email);
$user->setUsername($value_username);
$user->setRealname($value_realname);
if ($is_setup) {
$must_approve = false;
+ } else if ($invite) {
+ $must_approve = false;
} else {
$must_approve = PhabricatorEnv::getEnvConfig(
'auth.require-approval');
@@ -285,12 +329,18 @@
$user->setIsApproved(1);
}
+ if ($invite) {
+ $allow_reassign_email = true;
+ } else {
+ $allow_reassign_email = false;
+ }
+
$user->openTransaction();
$editor = id(new PhabricatorUserEditor())
->setActor($user);
- $editor->createNewUser($user, $email_obj);
+ $editor->createNewUser($user, $email_obj, $allow_reassign_email);
if ($must_set_password) {
$envelope = new PhutilOpaqueEnvelope($value_password);
$editor->changePassword($user, $envelope);
@@ -314,6 +364,10 @@
$this->sendWaitingForApprovalEmail($user);
}
+ if ($invite) {
+ $invite->setAcceptedByPHID($user->getPHID())->save();
+ }
+
return $this->loginUser($user);
} catch (AphrontDuplicateKeyQueryException $exception) {
$same_username = id(new PhabricatorUser())->loadOneWhere(
@@ -374,21 +428,30 @@
->setError($e_username));
}
+ if ($can_edit_realname) {
+ $form->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Real Name'))
+ ->setName('realName')
+ ->setValue($value_realname)
+ ->setError($e_realname));
+ }
+
if ($must_set_password) {
$form->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Password'))
->setName('password')
+ ->setError($e_password));
+ $form->appendChild(
+ id(new AphrontFormPasswordControl())
+ ->setLabel(pht('Confirm Password'))
+ ->setName('confirm')
->setError($e_password)
->setCaption(
$min_len
? pht('Minimum length of %d characters.', $min_len)
: null));
- $form->appendChild(
- id(new AphrontFormPasswordControl())
- ->setLabel(pht('Confirm Password'))
- ->setName('confirm')
- ->setError($e_password));
}
if ($can_edit_email) {
@@ -401,15 +464,6 @@
->setError($e_email));
}
- if ($can_edit_realname) {
- $form->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel(pht('Real Name'))
- ->setName('realName')
- ->setValue($value_realname)
- ->setError($e_realname));
- }
-
if ($must_set_password) {
$form->appendChild(
id(new AphrontFormRecaptchaControl())
@@ -459,10 +513,16 @@
->setForm($form)
->setFormErrors($errors);
+ $invite_header = null;
+ if ($invite) {
+ $invite_header = $this->renderInviteHeader($invite);
+ }
+
return $this->buildApplicationPage(
array(
$crumbs,
$welcome_view,
+ $invite_header,
$object_box,
),
array(
diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php
--- a/src/applications/auth/controller/PhabricatorAuthStartController.php
+++ b/src/applications/auth/controller/PhabricatorAuthStartController.php
@@ -109,14 +109,21 @@
}
}
+ $invite = $this->loadInvite();
+
$not_buttons = array();
$are_buttons = array();
$providers = msort($providers, 'getLoginOrder');
foreach ($providers as $provider) {
+ if ($invite) {
+ $form = $provider->buildInviteForm($this);
+ } else {
+ $form = $provider->buildLoginForm($this);
+ }
if ($provider->isLoginFormAButton()) {
- $are_buttons[] = $provider->buildLoginForm($this);
+ $are_buttons[] = $form;
} else {
- $not_buttons[] = $provider->buildLoginForm($this);
+ $not_buttons[] = $form;
}
}
@@ -159,6 +166,11 @@
$login_message = PhabricatorEnv::getEnvConfig('auth.login-message');
$login_message = phutil_safe_html($login_message);
+ $invite_message = null;
+ if ($invite) {
+ $invite_message = $this->renderInviteHeader($invite);
+ }
+
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Login'));
$crumbs->setBorder(true);
@@ -167,6 +179,7 @@
array(
$crumbs,
$login_message,
+ $invite_message,
$out,
),
array(
diff --git a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
--- a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
+++ b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php
@@ -250,7 +250,7 @@
}
private function getLogoutURI() {
- return '/auth/logout/';
+ return '/logout/';
}
}
diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php
--- a/src/applications/auth/provider/PhabricatorAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorAuthProvider.php
@@ -158,6 +158,10 @@
return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
}
+ public function buildInviteForm(PhabricatorAuthStartController $controller) {
+ return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
+ }
+
abstract public function processLoginRequest(
PhabricatorAuthLoginController $controller);
@@ -401,6 +405,8 @@
$button_text = pht('Link External Account');
} else if ($mode == 'refresh') {
$button_text = pht('Refresh Account Link');
+ } else if ($mode == 'invite') {
+ $button_text = pht('Register Account');
} else if ($this->shouldAllowRegistration()) {
$button_text = pht('Login or Register');
} else {
diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
--- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
+++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php
@@ -135,6 +135,29 @@
return $this->renderPasswordLoginForm($request);
}
+ public function buildInviteForm(
+ PhabricatorAuthStartController $controller) {
+ $request = $controller->getRequest();
+ $viewer = $request->getViewer();
+
+ $form = id(new AphrontFormView())
+ ->setUser($viewer)
+ ->addHiddenInput('invite', true)
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel(pht('Username'))
+ ->setName('username'));
+
+ $dialog = id(new AphrontDialogView())
+ ->setUser($viewer)
+ ->setTitle(pht('Register an Account'))
+ ->appendForm($form)
+ ->setSubmitURI('/auth/register/')
+ ->addSubmitButton(pht('Continue'));
+
+ return $dialog;
+ }
+
public function buildLinkForm(
PhabricatorAuthLinkController $controller) {
throw new Exception("Password providers can't be linked.");
diff --git a/src/applications/people/editor/PhabricatorUserEditor.php b/src/applications/people/editor/PhabricatorUserEditor.php
--- a/src/applications/people/editor/PhabricatorUserEditor.php
+++ b/src/applications/people/editor/PhabricatorUserEditor.php
@@ -23,14 +23,25 @@
*/
public function createNewUser(
PhabricatorUser $user,
- PhabricatorUserEmail $email) {
+ PhabricatorUserEmail $email,
+ $allow_reassign = false) {
if ($user->getID()) {
throw new Exception('User has already been created!');
}
+ $is_reassign = false;
if ($email->getID()) {
- throw new Exception('Email has already been created!');
+ if ($allow_reassign) {
+ if ($email->getIsPrimary()) {
+ throw new Exception(
+ pht(
+ 'Primary email addresses can not be reassigned.'));
+ }
+ $is_reassign = true;
+ } else {
+ throw new Exception('Email has already been created!');
+ }
}
if (!PhabricatorUser::validateUsername($user->getUsername())) {
@@ -71,6 +82,15 @@
$log->setNewValue($email->getAddress());
$log->save();
+ if ($is_reassign) {
+ $log = PhabricatorUserLog::initializeNewLog(
+ $this->requireActor(),
+ $user->getPHID(),
+ PhabricatorUserLog::ACTION_EMAIL_REASSIGN);
+ $log->setNewValue($email->getAddress());
+ $log->save();
+ }
+
$user->saveTransaction();
return $this;

File Metadata

Mime Type
text/plain
Expires
Wed, Nov 6, 9:33 PM (1 w, 6 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
6723126
Default Alt Text
D11737.id28302.diff (17 KB)

Event Timeline