Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F14019100
D11737.id28302.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Referenced Files
None
Subscribers
None
D11737.id28302.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D11737: Support invites in the registration and login flow
Attached
Detach File
Event Timeline
Log In to Comment