diff --git a/conf/default.conf.php b/conf/default.conf.php
index 5caf71509c..276e9df1b2 100644
--- a/conf/default.conf.php
+++ b/conf/default.conf.php
@@ -176,11 +176,25 @@ return array(
'amazon-ses.secret-key' => null,
+// -- Auth ------------------------------------------------------------------ //
+
+ // Can users login with a username/password, or by following the link from
+ // a password reset email? You can disable this and configure one or more
+ // OAuth providers instead.
+ 'auth.password-auth-enabled' => true,
+
// -- Facebook ------------------------------------------------------------ //
// Can users use Facebook credentials to login to Phabricator?
'facebook.auth-enabled' => false,
+ // Can users use Facebook credentials to create new Phabricator accounts?
+ 'facebook.registration-enabled' => true,
+
+ // Are Facebook accounts permanently linked to Phabricator accounts, or can
+ // the user unlink them?
+ 'facebook.auth-permanent' => false,
+
// The Facebook "Application ID" to use for Facebook API access.
'facebook.application-id' => null,
@@ -193,6 +207,13 @@ return array(
// Can users use Github credentials to login to Phabricator?
'github.auth-enabled' => false,
+ // Can users use Github credentials to create new Phabricator accounts?
+ 'github.registration-enabled' => true,
+
+ // Are Github accounts permanently linked to Phabricator accounts, or can
+ // the user unlink them?
+ 'github.auth-permanent' => false,
+
// The Github "Client ID" to use for Github API access.
'github.application-id' => null,
@@ -252,4 +273,7 @@ return array(
'aphront.default-application-configuration-class' =>
'AphrontDefaultApplicationConfiguration',
+
+ 'controller.oauth-registration' =>
+ 'PhabricatorOAuthDefaultRegistrationController',
);
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 86549d07f0..a5823a6c62 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -212,12 +212,14 @@ phutil_register_library_map(array(
'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists',
'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send',
'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
+ 'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/default',
'PhabricatorOAuthDiagnosticsController' => 'applications/auth/controller/oauthdiagnostics',
'PhabricatorOAuthFailureView' => 'applications/auth/view/oauthfailure',
'PhabricatorOAuthLoginController' => 'applications/auth/controller/oauth',
'PhabricatorOAuthProvider' => 'applications/auth/oauth/provider/base',
'PhabricatorOAuthProviderFacebook' => 'applications/auth/oauth/provider/facebook',
'PhabricatorOAuthProviderGithub' => 'applications/auth/oauth/provider/github',
+ 'PhabricatorOAuthRegistrationController' => 'applications/auth/controller/oauthregistration/base',
'PhabricatorOAuthUnlinkController' => 'applications/auth/controller/unlink',
'PhabricatorObjectHandle' => 'applications/phid/handle',
'PhabricatorObjectHandleData' => 'applications/phid/handle/data',
@@ -463,11 +465,13 @@ phutil_register_library_map(array(
'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController',
'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
+ 'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController',
'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController',
'PhabricatorOAuthFailureView' => 'AphrontView',
'PhabricatorOAuthLoginController' => 'PhabricatorAuthController',
'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider',
'PhabricatorOAuthProviderGithub' => 'PhabricatorOAuthProvider',
+ 'PhabricatorOAuthRegistrationController' => 'PhabricatorAuthController',
'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController',
'PhabricatorPHID' => 'PhabricatorPHIDDAO',
'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
diff --git a/src/aphront/console/core/DarkConsoleCore.php b/src/aphront/console/core/DarkConsoleCore.php
index d87cec6642..afb9d09350 100755
--- a/src/aphront/console/core/DarkConsoleCore.php
+++ b/src/aphront/console/core/DarkConsoleCore.php
@@ -88,6 +88,7 @@ final class DarkConsoleCore {
$visible = $user->getConsoleVisible();
if (!isset($plugins[$selected])) {
+ reset($plugins);
$selected = key($plugins);
}
diff --git a/src/aphront/controller/AphrontController.php b/src/aphront/controller/AphrontController.php
index 0ba389fd05..a70e905128 100644
--- a/src/aphront/controller/AphrontController.php
+++ b/src/aphront/controller/AphrontController.php
@@ -41,4 +41,8 @@ abstract class AphrontController {
return $this->request;
}
+ final public function delegateToController(AphrontController $controller) {
+ return $controller->processRequest();
+ }
+
}
diff --git a/src/applications/auth/controller/email/PhabricatorEmailLoginController.php b/src/applications/auth/controller/email/PhabricatorEmailLoginController.php
index 23ea196795..bb43020bfc 100644
--- a/src/applications/auth/controller/email/PhabricatorEmailLoginController.php
+++ b/src/applications/auth/controller/email/PhabricatorEmailLoginController.php
@@ -25,6 +25,10 @@ class PhabricatorEmailLoginController extends PhabricatorAuthController {
public function processRequest() {
$request = $this->getRequest();
+ if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
+ return new Aphront400Response();
+ }
+
$e_email = true;
$e_captcha = true;
$errors = array();
diff --git a/src/applications/auth/controller/email/__init__.php b/src/applications/auth/controller/email/__init__.php
index 72d4c63071..d5e2f5174e 100644
--- a/src/applications/auth/controller/email/__init__.php
+++ b/src/applications/auth/controller/email/__init__.php
@@ -6,6 +6,7 @@
+phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/metamta/storage/mail');
phutil_require_module('phabricator', 'applications/people/storage/user');
diff --git a/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php b/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php
index abfd26e89e..bc922aced2 100644
--- a/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php
+++ b/src/applications/auth/controller/emailtoken/PhabricatorEmailTokenController.php
@@ -31,6 +31,10 @@ class PhabricatorEmailTokenController extends PhabricatorAuthController {
public function processRequest() {
$request = $this->getRequest();
+ if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
+ return new Aphront400Response();
+ }
+
$token = $this->token;
$email = $request->getStr('email');
diff --git a/src/applications/auth/controller/emailtoken/__init__.php b/src/applications/auth/controller/emailtoken/__init__.php
index fd917cbcc1..cc20581f39 100644
--- a/src/applications/auth/controller/emailtoken/__init__.php
+++ b/src/applications/auth/controller/emailtoken/__init__.php
@@ -6,9 +6,11 @@
+phutil_require_module('phabricator', 'aphront/response/400');
phutil_require_module('phabricator', 'aphront/response/redirect');
phutil_require_module('phabricator', 'applications/auth/controller/base');
phutil_require_module('phabricator', 'applications/people/storage/user');
+phutil_require_module('phabricator', 'infrastructure/env');
phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/error');
diff --git a/src/applications/auth/controller/login/PhabricatorLoginController.php b/src/applications/auth/controller/login/PhabricatorLoginController.php
index e6991fae06..f55cc98662 100644
--- a/src/applications/auth/controller/login/PhabricatorLoginController.php
+++ b/src/applications/auth/controller/login/PhabricatorLoginController.php
@@ -30,69 +30,73 @@ class PhabricatorLoginController extends PhabricatorAuthController {
return id(new AphrontRedirectResponse())->setURI('/');
}
- $error = false;
- $username = $request->getCookie('phusr');
- if ($request->isFormPost()) {
- $username = $request->getStr('username');
+ $password_auth = PhabricatorEnv::getEnvConfig('auth.password-auth-enabled');
- $user = id(new PhabricatorUser())->loadOneWhere(
- 'username = %s',
- $username);
-
- $okay = false;
- if ($user) {
- if ($user->comparePassword($request->getStr('password'))) {
-
- $session_key = $user->establishSession('web');
-
- $request->setCookie('phusr', $user->getUsername());
- $request->setCookie('phsid', $session_key);
-
- return id(new AphrontRedirectResponse())
- ->setURI('/');
- }
- }
-
- if (!$okay) {
- $request->clearCookie('phusr');
- $request->clearCookie('phsid');
- }
-
- $error = true;
- }
+ $forms = array();
$error_view = null;
- if ($error) {
- $error_view = new AphrontErrorView();
- $error_view->setTitle('Bad username/password.');
+ if ($password_auth) {
+ $error = false;
+ $username = $request->getCookie('phusr');
+ if ($request->isFormPost()) {
+ $username = $request->getStr('username');
+
+ $user = id(new PhabricatorUser())->loadOneWhere(
+ 'username = %s',
+ $username);
+
+ $okay = false;
+ if ($user) {
+ if ($user->comparePassword($request->getStr('password'))) {
+
+ $session_key = $user->establishSession('web');
+
+ $request->setCookie('phusr', $user->getUsername());
+ $request->setCookie('phsid', $session_key);
+
+ return id(new AphrontRedirectResponse())
+ ->setURI('/');
+ }
+ }
+
+ if (!$okay) {
+ $request->clearCookie('phusr');
+ $request->clearCookie('phsid');
+ }
+
+ $error = true;
+ }
+
+ if ($error) {
+ $error_view = new AphrontErrorView();
+ $error_view->setTitle('Bad username/password.');
+ }
+
+ $form = new AphrontFormView();
+ $form
+ ->setUser($request->getUser())
+ ->setAction('/login/')
+ ->appendChild(
+ id(new AphrontFormTextControl())
+ ->setLabel('Username/Email')
+ ->setName('username')
+ ->setValue($username))
+ ->appendChild(
+ id(new AphrontFormPasswordControl())
+ ->setLabel('Password')
+ ->setName('password')
+ ->setCaption(
+ ''.
+ 'Forgot your password? / Email Login'))
+ ->appendChild(
+ id(new AphrontFormSubmitControl())
+ ->setValue('Login'));
+
+
+ // $panel->setCreateButton('Register New Account', '/login/register/');
+ $forms['Phabricator Login'] = $form;
}
- $form = new AphrontFormView();
- $form
- ->setUser($request->getUser())
- ->setAction('/login/')
- ->appendChild(
- id(new AphrontFormTextControl())
- ->setLabel('Username/Email')
- ->setName('username')
- ->setValue($username))
- ->appendChild(
- id(new AphrontFormPasswordControl())
- ->setLabel('Password')
- ->setName('password')
- ->setCaption(
- 'Forgot your password? / Email Login'))
- ->appendChild(
- id(new AphrontFormSubmitControl())
- ->setValue('Login'));
-
-
- $panel = new AphrontPanelView();
- $panel->setHeader('Phabricator Login');
- $panel->setWidth(AphrontPanelView::WIDTH_FORM);
-// $panel->setCreateButton('Register New Account', '/login/register/');
- $panel->appendChild($form);
-
$providers = array(
PhabricatorOAuthProvider::PROVIDER_FACEBOOK,
PhabricatorOAuthProvider::PROVIDER_GITHUB,
@@ -117,6 +121,19 @@ class PhabricatorLoginController extends PhabricatorAuthController {
// does not seem like the most severe threat in the world, and generating
// CSRF for logged-out users is vaugely tricky.
+ if ($provider->isProviderRegistrationEnabled()) {
+ $title = "Login or Register with {$provider_name}";
+ $body = "Login or register for Phabricator using your ".
+ "{$provider_name} account.";
+ $button = "Login or Register with {$provider_name}";
+ } else {
+ $title = "Login with {$provider_name}";
+ $body = "Login to your existing Phabricator account using your ".
+ "{$provider_name} account.
You can not use ".
+ "{$provider_name} to register a new account.";
+ $button = "Login with {$provider_name}";
+ }
+
$auth_form = new AphrontFormView();
$auth_form
->setAction($auth_uri)
@@ -126,16 +143,20 @@ class PhabricatorLoginController extends PhabricatorAuthController {
->setUser($request->getUser())
->setMethod('GET')
->appendChild(
- '
Login or register for '. - 'Phabricator using your '.$provider_name.' account.
') + ''.$body.'
') ->appendChild( id(new AphrontFormSubmitControl()) - ->setValue("Login with {$provider_name} \xC2\xBB")); + ->setValue("{$button} \xC2\xBB")); - $panel->appendChild( - 'The account you are logged in with is already linked to a '. + $provider_name.' account. Before you can link it to a different '. + $provider_name.' account, you must unlink the old account.
'); + $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); + return id(new AphrontDialogResponse())->setDialog($dialog); + } + if (!$request->isDialogFormPost()) { $dialog = new AphrontDialogView(); $dialog->setUser($current_user); @@ -163,17 +118,15 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { $dialog->appendChild( 'Link your '.$provider_name.' account to your Phabricator '. 'account?
'); - $dialog->addHiddenInput('token', $token); - $dialog->addHiddenInput('expires', $this->tokenExpires); + $dialog->addHiddenInput('token', $provider->getAccessToken()); + $dialog->addHiddenInput('expires', $oauth_info->getTokenExpires()); $dialog->addSubmitButton('Link Accounts'); $dialog->addCancelButton('/settings/page/'.$provider_key.'/'); return id(new AphrontDialogResponse())->setDialog($dialog); } - $oauth_info = new PhabricatorUserOAuthInfo(); $oauth_info->setUserID($current_user->getID()); - $this->configureOAuthInfo($oauth_info); $oauth_info->save(); return id(new AphrontRedirectResponse()) @@ -183,12 +136,11 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { // Login with known auth. - if ($known_oauth) { - $known_user = id(new PhabricatorUser())->load($known_oauth->getUserID()); + if ($oauth_info->getID()) { + $known_user = id(new PhabricatorUser())->load($oauth_info->getUserID()); $session_key = $known_user->establishSession('web'); - $this->configureOAuthInfo($known_oauth); - $known_oauth->save(); + $oauth_info->save(); $request->setCookie('phusr', $known_user->getUsername()); $request->setCookie('phsid', $session_key); @@ -196,10 +148,7 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { ->setURI('/'); } - // Merge accounts based on shared email. TODO: should probably get rid of - // this. - - $oauth_email = $this->retrieveUserEmail(); + $oauth_email = $provider->retrieveUserEmail(); if ($oauth_email) { $known_email = id(new PhabricatorUser()) ->loadOneWhere('email = %s', $oauth_email); @@ -218,159 +167,28 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { } } - $errors = array(); - $e_username = true; - $e_email = true; - $e_realname = true; + if (!$provider->isProviderRegistrationEnabled()) { + $dialog = new AphrontDialogView(); + $dialog->setUser($current_user); + $dialog->setTitle('No Account Registration With '.$provider_name); + $dialog->appendChild( + 'You can not register a new account using '.$provider_name.'; '. + 'you can only use your '.$provider_name.' account to log into an '. + 'existing Phabricator account which you have registered through '. + 'other means.
'); + $dialog->addCancelButton('/login/'); - $user = new PhabricatorUser(); - - $suggestion = $this->retrieveUsernameSuggestion(); - $user->setUsername($suggestion); - - $oauth_realname = $this->retreiveRealNameSuggestion(); - - if ($request->isFormPost()) { - - $user->setUsername($request->getStr('username')); - $username = $user->getUsername(); - $matches = null; - if (!strlen($user->getUsername())) { - $e_username = 'Required'; - $errors[] = 'Username is required.'; - } else if (!preg_match('/^[a-zA-Z0-9]+$/', $username, $matches)) { - $e_username = 'Invalid'; - $errors[] = 'Username may only contain letters and numbers.'; - } else { - $e_username = null; - } - - if ($oauth_email) { - $user->setEmail($oauth_email); - } else { - $user->setEmail($request->getStr('email')); - if (!strlen($user->getEmail())) { - $e_email = 'Required'; - $errors[] = 'Email is required.'; - } else { - $e_email = null; - } - } - - if ($oauth_realname) { - $user->setRealName($oauth_realname); - } else { - $user->setRealName($request->getStr('realname')); - if (!strlen($user->getStr('realname'))) { - $e_realname = 'Required'; - $errors[] = 'Real name is required.'; - } else { - $e_realname = null; - } - } - - if (!$errors) { - $image = $this->retreiveProfileImageSuggestion(); - if ($image) { - $file = PhabricatorFile::newFromFileData( - $image, - array( - 'name' => $provider->getProviderKey().'-profile.jpg' - )); - $user->setProfileImagePHID($file->getPHID()); - } - - try { - $user->save(); - - $oauth_info = new PhabricatorUserOAuthInfo(); - $oauth_info->setUserID($user->getID()); - $this->configureOAuthInfo($oauth_info); - $oauth_info->save(); - - $session_key = $user->establishSession('web'); - $request->setCookie('phusr', $user->getUsername()); - $request->setCookie('phsid', $session_key); - return id(new AphrontRedirectResponse())->setURI('/'); - } catch (AphrontQueryDuplicateKeyException $exception) { - - $same_username = id(new PhabricatorUser())->loadOneWhere( - 'userName = %s', - $user->getUserName()); - - $same_email = id(new PhabricatorUser())->loadOneWhere( - 'email = %s', - $user->getEmail()); - - if ($same_username) { - $e_username = 'Duplicate'; - $errors[] = 'That username or email is not unique.'; - } else if ($same_email) { - $e_email = 'Duplicate'; - $errors[] = 'That email is not unique.'; - } else { - throw $exception; - } - } - } + return id(new AphrontDialogResponse())->setDialog($dialog); } - $error_view = null; - if ($errors) { - $error_view = new AphrontErrorView(); - $error_view->setTitle('Registration Failed'); - $error_view->setErrors($errors); - } + $class = PhabricatorEnv::getEnvConfig('controller.oauth-registration'); + PhutilSymbolLoader::loadClass($class); + $controller = newv($class, array($this->getRequest())); - $form = new AphrontFormView(); - $form - ->addHiddenInput('token', $token) - ->addHiddenInput('expires', $this->tokenExpires) - ->setUser($request->getUser()) - ->setAction($provider->getRedirectURI()) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Username') - ->setName('username') - ->setValue($user->getUsername()) - ->setError($e_username)); + $controller->setOAuthProvider($provider); + $controller->setOAuthInfo($oauth_info); - if (!$oauth_email) { - $form->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Email') - ->setName('email') - ->setValue($request->getStr('email')) - ->setError($e_email)); - } - - if (!$oauth_realname) { - $form->appendChild( - id(new AphrontFormTextControl()) - ->setLabel('Real Name') - ->setName('realname') - ->setValue($request->getStr('realname')) - ->setError($e_realname)); - } - - $form - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue('Create Account')); - - $panel = new AphrontPanelView(); - $panel->setHeader('Create New Account'); - $panel->setWidth(AphrontPanelView::WIDTH_FORM); - $panel->appendChild($form); - - return $this->buildStandardPageResponse( - array( - $error_view, - $panel, - ), - array( - 'title' => 'Create New Account', - )); + return $this->delegateToController($controller); } private function buildErrorResponse(PhabricatorOAuthFailureView $view) { @@ -386,71 +204,90 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { )); } - private function retrieveUserID() { - return $this->userData['id']; - } + private function retrieveAccessToken(PhabricatorOAuthProvider $provider) { + $request = $this->getRequest(); - private function retrieveUserEmail() { - return $this->userData['email']; - } - - private function retrieveUsernameSuggestion() { - switch ($this->provider->getProviderKey()) { - case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: - $matches = null; - $link = $this->userData['link']; - if (preg_match('@/([a-zA-Z0-9]+)$@', $link, $matches)) { - return $matches[1]; - } - break; - case PhabricatorOAuthProvider::PROVIDER_GITHUB: - return $this->userData['login']; + $token = $request->getStr('token'); + if ($token) { + $this->tokenExpires = $request->getInt('expires'); + $this->accessToken = $token; + return null; } + + $client_id = $provider->getClientID(); + $client_secret = $provider->getClientSecret(); + $redirect_uri = $provider->getRedirectURI(); + $auth_uri = $provider->getTokenURI(); + + $code = $request->getStr('code'); + $query_data = array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'code' => $code, + ); + + $post_data = http_build_query($query_data); + $post_length = strlen($post_data); + + $stream_context = stream_context_create( + array( + 'http' => array( + 'method' => 'POST', + 'header' => + "Content-Type: application/x-www-form-urlencoded\r\n". + "Content-Length: {$post_length}\r\n", + 'content' => $post_data, + ), + )); + + $stream = fopen($auth_uri, 'r', false, $stream_context); + + $response = false; + $meta = null; + if ($stream) { + $meta = stream_get_meta_data($stream); + $response = stream_get_contents($stream); + fclose($stream); + } + + if ($response === false) { + return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); + } + + $data = array(); + parse_str($response, $data); + + $token = idx($data, 'access_token'); + if (!$token) { + return $this->buildErrorResponse(new PhabricatorOAuthFailureView()); + } + + if (idx($data, 'expires')) { + $this->tokenExpires = time() + $data['expires']; + } + + $this->accessToken = $token; + return null; } - private function retreiveProfileImageSuggestion() { - switch ($this->provider->getProviderKey()) { - case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: - $uri = 'https://graph.facebook.com/me/picture?access_token='; - return @file_get_contents($uri.$this->accessToken); - case PhabricatorOAuthProvider::PROVIDER_GITHUB: - $id = $this->userData['gravatar_id']; - if ($id) { - $uri = 'http://www.gravatar.com/avatar/'.$id.'?s=50'; - return @file_get_contents($uri); - } + private function retrieveOAuthInfo(PhabricatorOAuthProvider $provider) { + + $oauth_info = id(new PhabricatorUserOAuthInfo())->loadOneWhere( + 'oauthProvider = %s and oauthUID = %s', + $provider->getProviderKey(), + $provider->retrieveUserID()); + + if (!$oauth_info) { + $oauth_info = new PhabricatorUserOAuthInfo(); + $oauth_info->setOAuthProvider($provider->getProviderKey()); + $oauth_info->setOAuthUID($provider->retrieveUserID()); } - return null; - } - private function retrieveAccountURI() { - switch ($this->provider->getProviderKey()) { - case PhabricatorOAuthProvider::PROVIDER_FACEBOOK: - return $this->userData['link']; - case PhabricatorOAuthProvider::PROVIDER_GITHUB: - $username = $this->retrieveUsernameSuggestion(); - if ($username) { - return 'https://github.com/'.$username; - } - return null; - } - return null; - } - - private function retreiveRealNameSuggestion() { - return $this->userData['name']; - } - - private function configureOAuthInfo(PhabricatorUserOAuthInfo $oauth_info) { - $provider = $this->provider; - - $oauth_info->setOAuthProvider($provider->getProviderKey()); - $oauth_info->setOAuthUID($this->retrieveUserID()); - $oauth_info->setAccountURI($this->retrieveAccountURI()); - $oauth_info->setAccountName($this->retrieveUserNameSuggestion()); - - $oauth_info->setToken($this->accessToken); + $oauth_info->setAccountURI($provider->retrieveUserAccountURI()); + $oauth_info->setAccountName($provider->retrieveUserAccountName()); + $oauth_info->setToken($provider->getAccessToken()); $oauth_info->setTokenStatus(PhabricatorUserOAuthInfo::TOKEN_STATUS_GOOD); // If we have out-of-date expiration info, just clear it out. Then replace @@ -463,6 +300,8 @@ class PhabricatorOAuthLoginController extends PhabricatorAuthController { $expires = $this->tokenExpires; } $oauth_info->setTokenExpires($expires); + + return $oauth_info; } } diff --git a/src/applications/auth/controller/oauth/__init__.php b/src/applications/auth/controller/oauth/__init__.php index 19082b7aa2..b3ee8dda26 100644 --- a/src/applications/auth/controller/oauth/__init__.php +++ b/src/applications/auth/controller/oauth/__init__.php @@ -12,17 +12,13 @@ phutil_require_module('phabricator', 'aphront/response/redirect'); phutil_require_module('phabricator', 'applications/auth/controller/base'); phutil_require_module('phabricator', 'applications/auth/oauth/provider/base'); phutil_require_module('phabricator', 'applications/auth/view/oauthfailure'); -phutil_require_module('phabricator', 'applications/files/storage/file'); phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'applications/people/storage/useroauthinfo'); +phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'view/dialog'); -phutil_require_module('phabricator', 'view/form/base'); -phutil_require_module('phabricator', 'view/form/control/submit'); -phutil_require_module('phabricator', 'view/form/control/text'); -phutil_require_module('phabricator', 'view/form/error'); -phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phutil', 'parser/uri'); +phutil_require_module('phutil', 'symbols'); phutil_require_module('phutil', 'utils'); diff --git a/src/applications/auth/controller/oauthregistration/base/PhabricatorOAuthRegistrationController.php b/src/applications/auth/controller/oauthregistration/base/PhabricatorOAuthRegistrationController.php new file mode 100644 index 0000000000..addbe914ee --- /dev/null +++ b/src/applications/auth/controller/oauthregistration/base/PhabricatorOAuthRegistrationController.php @@ -0,0 +1,43 @@ +oauthInfo = $info; + return $this; + } + + final public function getOAuthInfo() { + return $this->oauthInfo; + } + + final public function setOAuthProvider($provider) { + $this->oauthProvider = $provider; + return $this; + } + + final public function getOAuthProvider() { + return $this->oauthProvider; + } + +} diff --git a/src/applications/auth/controller/oauthregistration/base/__init__.php b/src/applications/auth/controller/oauthregistration/base/__init__.php new file mode 100644 index 0000000000..9a240054a7 --- /dev/null +++ b/src/applications/auth/controller/oauthregistration/base/__init__.php @@ -0,0 +1,12 @@ +getOAuthProvider(); + $oauth_info = $this->getOAuthInfo(); + $request = $this->getRequest(); + + $errors = array(); + $e_username = true; + $e_email = true; + $e_realname = true; + + $user = new PhabricatorUser(); + + $user->setUsername($provider->retrieveUserAccountName()); + $user->setRealName($provider->retrieveUserRealName()); + $user->setEmail($provider->retrieveUserEmail()); + + if ($request->isFormPost()) { + + $user->setUsername($request->getStr('username')); + $username = $user->getUsername(); + $matches = null; + if (!strlen($user->getUsername())) { + $e_username = 'Required'; + $errors[] = 'Username is required.'; + } else if (!preg_match('/^[a-zA-Z0-9]+$/', $username, $matches)) { + $e_username = 'Invalid'; + $errors[] = 'Username may only contain letters and numbers.'; + } else { + $e_username = null; + } + + if ($user->getEmail() === null) { + $user->setEmail($request->getStr('email')); + if (!strlen($user->getEmail())) { + $e_email = 'Required'; + $errors[] = 'Email is required.'; + } else { + $e_email = null; + } + } + + if ($user->getRealName() === null) { + $user->setRealName($request->getStr('realname')); + if (!strlen($user->getStr('realname'))) { + $e_realname = 'Required'; + $errors[] = 'Real name is required.'; + } else { + $e_realname = null; + } + } + + if (!$errors) { + $image = $provider->retreiveUserProfileImage(); + if ($image) { + $file = PhabricatorFile::newFromFileData( + $image, + array( + 'name' => $provider->getProviderKey().'-profile.jpg' + )); + $user->setProfileImagePHID($file->getPHID()); + } + + try { + $user->save(); + + $oauth_info->setUserID($user->getID()); + $oauth_info->save(); + + $session_key = $user->establishSession('web'); + $request->setCookie('phusr', $user->getUsername()); + $request->setCookie('phsid', $session_key); + return id(new AphrontRedirectResponse())->setURI('/'); + } catch (AphrontQueryDuplicateKeyException $exception) { + + $same_username = id(new PhabricatorUser())->loadOneWhere( + 'userName = %s', + $user->getUserName()); + + $same_email = id(new PhabricatorUser())->loadOneWhere( + 'email = %s', + $user->getEmail()); + + if ($same_username) { + $e_username = 'Duplicate'; + $errors[] = 'That username or email is not unique.'; + } else if ($same_email) { + $e_email = 'Duplicate'; + $errors[] = 'That email is not unique.'; + } else { + throw $exception; + } + } + } + } + + $error_view = null; + if ($errors) { + $error_view = new AphrontErrorView(); + $error_view->setTitle('Registration Failed'); + $error_view->setErrors($errors); + } + + $form = new AphrontFormView(); + $form + ->addHiddenInput('token', $provider->getAccessToken()) + ->addHiddenInput('expires', $oauth_info->getTokenExpires()) + ->setUser($request->getUser()) + ->setAction($provider->getRedirectURI()) + ->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Username') + ->setName('username') + ->setValue($user->getUsername()) + ->setError($e_username)); + + if ($provider->retrieveUserEmail() === null) { + $form->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Email') + ->setName('email') + ->setValue($request->getStr('email')) + ->setError($e_email)); + } + + if ($provider->retrieveUserRealName () === null) { + $form->appendChild( + id(new AphrontFormTextControl()) + ->setLabel('Real Name') + ->setName('realname') + ->setValue($request->getStr('realname')) + ->setError($e_realname)); + } + + $form + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue('Create Account')); + + $panel = new AphrontPanelView(); + $panel->setHeader('Create New Account'); + $panel->setWidth(AphrontPanelView::WIDTH_FORM); + $panel->appendChild($form); + + return $this->buildStandardPageResponse( + array( + $error_view, + $panel, + ), + array( + 'title' => 'Create New Account', + )); + } + +} diff --git a/src/applications/auth/controller/oauthregistration/default/__init__.php b/src/applications/auth/controller/oauthregistration/default/__init__.php new file mode 100644 index 0000000000..ba332f4cf1 --- /dev/null +++ b/src/applications/auth/controller/oauthregistration/default/__init__.php @@ -0,0 +1,22 @@ +getUser(); $provider = $this->provider; + + if ($provider->isProviderLinkPermanent()) { + throw new Exception( + "You may not unlink accounts from this OAuth provider."); + } + $provider_name = $provider->getProviderName(); $provider_key = $provider->getProviderKey(); diff --git a/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php index dd3590a9c6..f5124eaf81 100644 --- a/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php +++ b/src/applications/auth/oauth/provider/base/PhabricatorOAuthProvider.php @@ -21,9 +21,13 @@ abstract class PhabricatorOAuthProvider { const PROVIDER_FACEBOOK = 'facebook'; const PROVIDER_GITHUB = 'github'; + private $accessToken; + abstract public function getProviderKey(); abstract public function getProviderName(); abstract public function isProviderEnabled(); + abstract public function isProviderLinkPermanent(); + abstract public function isProviderRegistrationEnabled(); abstract public function getRedirectURI(); abstract public function getClientID(); abstract public function getClientSecret(); @@ -32,10 +36,27 @@ abstract class PhabricatorOAuthProvider { abstract public function getUserInfoURI(); abstract public function getMinimumScope(); + abstract public function setUserData($data); + abstract public function retrieveUserID(); + abstract public function retrieveUserEmail(); + abstract public function retrieveUserAccountName(); + abstract public function retrieveUserProfileImage(); + abstract public function retrieveUserAccountURI(); + abstract public function retrieveUserRealName(); + public function __construct() { } + final public function setAccessToken($access_token) { + $this->accessToken = $access_token; + return $this; + } + + final public function getAccessToken() { + return $this->accessToken; + } + public static function newProvider($which) { switch ($which) { case self::PROVIDER_FACEBOOK: diff --git a/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php b/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php index 8893f9a178..ec97334a1f 100644 --- a/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php +++ b/src/applications/auth/oauth/provider/facebook/PhabricatorOAuthProviderFacebook.php @@ -18,6 +18,8 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider { + private $userData; + public function getProviderKey() { return self::PROVIDER_FACEBOOK; } @@ -30,6 +32,14 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider { return PhabricatorEnv::getEnvConfig('facebook.auth-enabled'); } + public function isProviderLinkPermanent() { + return PhabricatorEnv::getEnvConfig('facebook.auth-permanent'); + } + + public function isProviderRegistrationEnabled() { + return PhabricatorEnv::getEnvConfig('facebook.registration-enabled'); + } + public function getRedirectURI() { return PhabricatorEnv::getURI('/oauth/facebook/login/'); } @@ -58,4 +68,39 @@ class PhabricatorOAuthProviderFacebook extends PhabricatorOAuthProvider { return 'email'; } + public function setUserData($data) { + $this->userData = $data; + return $this; + } + + public function retrieveUserID() { + return $this->userData['id']; + } + + public function retrieveUserEmail() { + return $this->userData['email']; + } + + public function retrieveUserAccountName() { + $matches = null; + $link = $this->userData['link']; + if (preg_match('@/([a-zA-Z0-9]+)$@', $link, $matches)) { + return $matches[1]; + } + return null; + } + + public function retrieveUserProfileImage() { + $uri = 'https://graph.facebook.com/me/picture?access_token='; + return @file_get_contents($uri.$this->getAccessToken()); + } + + public function retrieveUserAccountURI() { + return $this->userData['link']; + } + + public function retrieveUserRealName() { + return $this->userData['name']; + } + } diff --git a/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php b/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php index 7b92956a21..f5834369ae 100644 --- a/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php +++ b/src/applications/auth/oauth/provider/github/PhabricatorOAuthProviderGithub.php @@ -18,6 +18,8 @@ class PhabricatorOAuthProviderGithub extends PhabricatorOAuthProvider { + private $userData; + public function getProviderKey() { return self::PROVIDER_GITHUB; } @@ -30,6 +32,14 @@ class PhabricatorOAuthProviderGithub extends PhabricatorOAuthProvider { return PhabricatorEnv::getEnvConfig('github.auth-enabled'); } + public function isProviderLinkPermanent() { + return PhabricatorEnv::getEnvConfig('github.auth-permanent'); + } + + public function isProviderRegistrationEnabled() { + return PhabricatorEnv::getEnvConfig('github.registration-enabled'); + } + public function getRedirectURI() { return PhabricatorEnv::getURI('/oauth/github/login/'); } @@ -58,4 +68,42 @@ class PhabricatorOAuthProviderGithub extends PhabricatorOAuthProvider { return null; } + public function setUserData($data) { + $this->userData = $data['user']; + return $this; + } + + public function retrieveUserID() { + return $this->userData['id']; + } + + public function retrieveUserEmail() { + return $this->userData['email']; + } + + public function retrieveUserAccountName() { + return $this->userData['login']; + } + + public function retrieveUserProfileImage() { + $id = $this->userData['gravatar_id']; + if ($id) { + $uri = 'http://www.gravatar.com/avatar/'.$id.'?s=50'; + return @file_get_contents($uri); + } + return null; + } + + public function retrieveUserAccountURI() { + $username = $this->retrieveUserAccountName(); + if ($username) { + return 'https://github.com/'.$username; + } + return null; + } + + public function retrieveUserRealName() { + return $this->userData['name']; + } + } diff --git a/src/applications/people/controller/settings/PhabricatorUserSettingsController.php b/src/applications/people/controller/settings/PhabricatorUserSettingsController.php index 8202d8a226..fea4fa4092 100644 --- a/src/applications/people/controller/settings/PhabricatorUserSettingsController.php +++ b/src/applications/people/controller/settings/PhabricatorUserSettingsController.php @@ -358,18 +358,20 @@ class PhabricatorUserSettingsController extends PhabricatorPeopleController { ->setLabel($provider_name.' URI') ->setValue($oauth_info->getAccountURI())); - $unlink = 'Unlink '.$provider_name.' Account'; - $unlink_form = new AphrontFormView(); - $unlink_form - ->setUser($user) - ->appendChild( - 'You may unlink this account '. - 'from your '.$provider_name.' account. This will prevent you from '. - 'logging in with your '.$provider_name.' credentials.
') - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton('/oauth/'.$provider_key.'/unlink/', $unlink)); - $forms['Unlink Account'] = $unlink_form; + if (!$provider->isProviderLinkPermanent()) { + $unlink = 'Unlink '.$provider_name.' Account'; + $unlink_form = new AphrontFormView(); + $unlink_form + ->setUser($user) + ->appendChild( + 'You may unlink this account '. + 'from your '.$provider_name.' account. This will prevent you from '. + 'logging in with your '.$provider_name.' credentials.
') + ->appendChild( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/oauth/'.$provider_key.'/unlink/', $unlink)); + $forms['Unlink Account'] = $unlink_form; + } $expires = $oauth_info->getTokenExpires(); if ($expires) {