diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index faa878d4a8..e73cda8e36 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -1,323 +1,341 @@ <?php abstract class PhabricatorAuthProvider { private $providerConfig; public function attachProviderConfig(PhabricatorAuthProviderConfig $config) { $this->providerConfig = $config; return $this; } public function hasProviderConfig() { return (bool)$this->providerConfig; } public function getProviderConfig() { if ($this->providerConfig === null) { throw new Exception( "Call attachProviderConfig() before getProviderConfig()!"); } return $this->providerConfig; } public function getConfigurationHelp() { return null; } public function getDefaultProviderConfig() { return id(new PhabricatorAuthProviderConfig()) ->setProviderClass(get_class($this)) ->setIsEnabled(1) ->setShouldAllowLogin(1) ->setShouldAllowRegistration(1) ->setShouldAllowLink(1) ->setShouldAllowUnlink(1); } public function getNameForCreate() { return $this->getProviderName(); } public function getDescriptionForCreate() { return null; } public function getProviderKey() { return $this->getAdapter()->getAdapterKey(); } public function getProviderType() { return $this->getAdapter()->getAdapterType(); } public function getProviderDomain() { return $this->getAdapter()->getAdapterDomain(); } public static function getAllBaseProviders() { static $providers; if ($providers === null) { $objects = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); $providers = $objects; } return $providers; } public static function getAllProviders() { static $providers; if ($providers === null) { $objects = self::getAllBaseProviders(); $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->execute(); $providers = array(); foreach ($configs as $config) { if (!isset($objects[$config->getProviderClass()])) { // This configuration is for a provider which is not installed. continue; } $object = clone $objects[$config->getProviderClass()]; $object->attachProviderConfig($config); $key = $object->getProviderKey(); if (isset($providers[$key])) { throw new Exception( pht( "Two authentication providers use the same provider key ". "('%s'). Each provider must be identified by a unique ". "key.", $key)); } $providers[$key] = $object; } } return $providers; } public static function getAllEnabledProviders() { $providers = self::getAllProviders(); foreach ($providers as $key => $provider) { if (!$provider->isEnabled()) { unset($providers[$key]); } } return $providers; } public static function getEnabledProviderByKey($provider_key) { return idx(self::getAllEnabledProviders(), $provider_key); } abstract public function getProviderName(); abstract public function getAdapter(); public function isEnabled() { return $this->getProviderConfig()->getIsEnabled(); } public function shouldAllowLogin() { return $this->getProviderConfig()->getShouldAllowLogin(); } public function shouldAllowRegistration() { return $this->getProviderConfig()->getShouldAllowRegistration(); } public function shouldAllowAccountLink() { return $this->getProviderConfig()->getShouldAllowLink(); } public function shouldAllowAccountUnlink() { return $this->getProviderConfig()->getShouldAllowUnlink(); } public function buildLoginForm( PhabricatorAuthStartController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'start'); } abstract public function processLoginRequest( PhabricatorAuthLoginController $controller); public function buildLinkForm( PhabricatorAuthLinkController $controller) { return $this->renderLoginForm($controller->getRequest(), $mode = 'link'); } protected function renderLoginForm( AphrontRequest $request, $mode) { throw new Exception("Not implemented!"); } public function createProviders() { return array($this); } protected function willSaveAccount(PhabricatorExternalAccount $account) { return; } public function willRegisterAccount(PhabricatorExternalAccount $account) { return; } protected function loadOrCreateAccount($account_id) { if (!strlen($account_id)) { throw new Exception( "loadOrCreateAccount(...): empty account ID!"); } $adapter = $this->getAdapter(); $adapter_class = get_class($adapter); if (!strlen($adapter->getAdapterType())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter type."); } if (!strlen($adapter->getAdapterDomain())) { throw new Exception( "AuthAdapter (of class '{$adapter_class}') has an invalid ". "implementation: no adapter domain."); } $account = id(new PhabricatorExternalAccount())->loadOneWhere( 'accountType = %s AND accountDomain = %s AND accountID = %s', $adapter->getAdapterType(), $adapter->getAdapterDomain(), $account_id); if (!$account) { $account = id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()) ->setAccountID($account_id); } $account->setUsername($adapter->getAccountName()); $account->setRealName($adapter->getAccountRealName()); $account->setEmail($adapter->getAccountEmail()); $account->setAccountURI($adapter->getAccountURI()); $account->setProfileImagePHID(null); $image_uri = $adapter->getAccountImageURI(); if ($image_uri) { try { $name = PhabricatorSlug::normalize($this->getProviderName()); $name = $name.'-profile.jpg'; // TODO: If the image has not changed, we do not need to make a new // file entry for it, but there's no convenient way to do this with // PhabricatorFile right now. The storage will get shared, so the impact // here is negligible. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $image_file = PhabricatorFile::newFromFileDownload( $image_uri, array( 'name' => $name, )); unset($unguarded); if ($image_file) { $account->setProfileImagePHID($image_file->getPHID()); } } catch (Exception $ex) { // Log this but proceed, it's not especially important that we // be able to pull profile images. phlog($ex); } } $this->willSaveAccount($account); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); return $account; } public function getLoginURI() { $app = PhabricatorApplication::getByClass('PhabricatorApplicationAuth'); $uri = $app->getApplicationURI('/login/'.$this->getProviderKey().'/'); return PhabricatorEnv::getURI($uri); } public function getSettingsURI() { return '/settings/panel/external/'; } public function getStartURI() { $app = PhabricatorApplication::getByClass('PhabricatorApplicationAuth'); $uri = $app->getApplicationURI('/start/'); return $uri; } public function isDefaultRegistrationProvider() { return false; } public function shouldRequireRegistrationPassword() { return false; } public function getDefaultExternalAccount() { throw new Exception("Not implemented!"); } public function getLoginOrder() { return '500-'.$this->getProviderName(); } protected function getLoginIcon() { return 'Generic'; } public function isLoginFormAButton() { return false; } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { return null; } public function readFormValuesFromProvider() { return array(); } public function readFormValuesFromRequest(AphrontRequest $request) { return array(); } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { return; } + public function willRenderLinkedAccount( + PhabricatorUser $viewer, + PhabricatorObjectItemView $item, + PhabricatorExternalAccount $account) { + + $account_view = id(new PhabricatorAuthAccountView()) + ->setExternalAccount($account) + ->setAuthProvider($this); + + $item->appendChild( + phutil_tag( + 'div', + array( + 'class' => 'mmr mml mst mmb', + ), + $account_view)); + } + } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php index 16b45d53cb..97c2687edb 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php @@ -1,346 +1,376 @@ <?php abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider { protected $adapter; abstract protected function newOAuthAdapter(); public function getDescriptionForCreate() { return pht('Configure %s OAuth.', $this->getProviderName()); } public function getAdapter() { if (!$this->adapter) { $adapter = $this->newOAuthAdapter(); $this->adapter = $adapter; $this->configureAdapter($adapter); } return $this->adapter; } protected function configureAdapter(PhutilAuthAdapterOAuth $adapter) { $config = $this->getProviderConfig(); $adapter->setClientID($config->getProperty(self::PROPERTY_APP_ID)); $adapter->setClientSecret( new PhutilOpaqueEnvelope( $config->getProperty(self::PROPERTY_APP_SECRET))); $adapter->setRedirectURI($this->getLoginURI()); return $adapter; } public function isLoginFormAButton() { return true; } protected function renderLoginForm(AphrontRequest $request, $mode) { $viewer = $request->getUser(); if ($mode == 'link') { $button_text = pht('Link External Account'); } else if ($this->shouldAllowRegistration()) { $button_text = pht('Login or Register'); } else { $button_text = pht('Login'); } $icon = id(new PHUIIconView()) ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) ->setSpriteIcon($this->getLoginIcon()); $button = id(new PHUIButtonView()) ->setSize(PHUIButtonView::BIG) ->setColor(PHUIButtonView::GREY) ->setIcon($icon) ->setText($button_text) ->setSubtext($this->getProviderName()); $adapter = $this->getAdapter(); $adapter->setState(PhabricatorHash::digest($request->getCookie('phcid'))); $uri = new PhutilURI($adapter->getAuthenticateURI()); $params = $uri->getQueryParams(); $uri->setQueryParams(array()); $content = array($button); foreach ($params as $key => $value) { $content[] = phutil_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, )); } return phabricator_form( $viewer, array( 'method' => 'GET', 'action' => (string)$uri, ), $content); } public function processLoginRequest( PhabricatorAuthLoginController $controller) { $request = $controller->getRequest(); $adapter = $this->getAdapter(); $account = null; $response = null; $error = $request->getStr('error'); if ($error) { $response = $controller->buildProviderErrorResponse( $this, pht( 'The OAuth provider returned an error: %s', $error)); return array($account, $response); } $code = $request->getStr('code'); if (!strlen($code)) { $response = $controller->buildProviderErrorResponse( $this, pht( 'The OAuth provider did not return a "code" parameter in its '. 'response.')); return array($account, $response); } if ($adapter->supportsStateParameter()) { $phcid = $request->getCookie('phcid'); if (!strlen($phcid)) { $response = $controller->buildProviderErrorResponse( $this, pht( 'Your browser did not submit a "phcid" cookie with OAuth state '. 'information in the request. Check that cookies are enabled. '. 'If this problem persists, you may need to clear your cookies.')); } $state = $request->getStr('state'); $expect = PhabricatorHash::digest($phcid); if ($state !== $expect) { $response = $controller->buildProviderErrorResponse( $this, pht( 'The OAuth provider did not return the correct "state" parameter '. 'in its response. If this problem persists, you may need to clear '. 'your cookies.')); } } $adapter->setCode($code); // NOTE: As a side effect, this will cause the OAuth adapter to request // an access token. try { $account_id = $adapter->getAccountID(); } catch (Exception $ex) { // TODO: Handle this in a more user-friendly way. throw $ex; } if (!strlen($account_id)) { $response = $controller->buildProviderErrorResponse( $this, pht( 'The OAuth provider failed to retrieve an account ID.')); return array($account, $response); } return array($this->loadOrCreateAccount($account_id), $response); } const PROPERTY_APP_ID = 'oauth:app:id'; const PROPERTY_APP_SECRET = 'oauth:app:secret'; public function readFormValuesFromProvider() { $config = $this->getProviderConfig(); $id = $config->getProperty(self::PROPERTY_APP_ID); $secret = $config->getProperty(self::PROPERTY_APP_SECRET); return array( self::PROPERTY_APP_ID => $id, self::PROPERTY_APP_SECRET => $secret, ); } public function readFormValuesFromRequest(AphrontRequest $request) { return array( self::PROPERTY_APP_ID => $request->getStr(self::PROPERTY_APP_ID), self::PROPERTY_APP_SECRET => $request->getStr(self::PROPERTY_APP_SECRET), ); } public function processEditForm( AphrontRequest $request, array $values) { $errors = array(); $issues = array(); $key_id = self::PROPERTY_APP_ID; $key_secret = self::PROPERTY_APP_SECRET; if (!strlen($values[$key_id])) { $errors[] = pht('Application ID is required.'); $issues[$key_id] = pht('Required'); } if (!strlen($values[$key_secret])) { $errors[] = pht('Application secret is required.'); $issues[$key_secret] = pht('Required'); } // If the user has not changed the secret, don't update it (that is, // don't cause a bunch of "****" to be written to the database). if (preg_match('/^[*]+$/', $values[$key_secret])) { unset($values[$key_secret]); } return array($errors, $issues, $values); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { $key_id = self::PROPERTY_APP_ID; $key_secret = self::PROPERTY_APP_SECRET; $v_id = $values[$key_id]; $v_secret = $values[$key_secret]; if ($v_secret) { $v_secret = str_repeat('*', strlen($v_secret)); } $e_id = idx($issues, $key_id, $request->isFormPost() ? null : true); $e_secret = idx($issues, $key_secret, $request->isFormPost() ? null : true); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('OAuth App ID')) ->setName($key_id) ->setValue($v_id) ->setError($e_id)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('OAuth App Secret')) ->setName($key_secret) ->setValue($v_secret) ->setError($e_secret)); } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); switch ($key) { case self::PROPERTY_APP_ID: if (strlen($old)) { return pht( '%s updated the OAuth application ID for this provider from '. '"%s" to "%s".', $xaction->renderHandleLink($author_phid), $old, $new); } else { return pht( '%s set the OAuth application ID for this provider to '. '"%s".', $xaction->renderHandleLink($author_phid), $new); } case self::PROPERTY_APP_SECRET: if (strlen($old)) { return pht( '%s updated the OAuth application secret for this provider.', $xaction->renderHandleLink($author_phid)); } else { return pht( '%s set the OAuth application seceret for this provider.', $xaction->renderHandleLink($author_phid)); } } return parent::renderConfigPropertyTransactionTitle($xaction); } protected function willSaveAccount(PhabricatorExternalAccount $account) { parent::willSaveAccount($account); $this->synchronizeOAuthAccount($account); } protected function synchronizeOAuthAccount( PhabricatorExternalAccount $account) { $adapter = $this->getAdapter(); $oauth_token = $adapter->getAccessToken(); $account->setProperty('oauth.token.access', $oauth_token); if ($adapter->supportsTokenRefresh()) { $refresh_token = $adapter->getRefreshToken(); $account->setProperty('oauth.token.refresh', $refresh_token); } else { $account->setProperty('oauth.token.refresh', null); } $expires = $adapter->getAccessTokenExpires(); $account->setProperty('oauth.token.access.expires', $expires); } public function getOAuthAccessToken( PhabricatorExternalAccount $account, $force_refresh = false) { if ($account->getProviderKey() !== $this->getProviderKey()) { throw new Exception("Account does not match provider!"); } if (!$force_refresh) { $access_expires = $account->getProperty('oauth.token.access.expires'); $access_token = $account->getProperty('oauth.token.access'); // Don't return a token with fewer than this many seconds remaining until // it expires. $shortest_token = 60; if ($access_token) { if ($access_expires > (time() + $shortest_token)) { return $access_token; } } } $refresh_token = $account->getProperty('oauth.token.refresh'); if ($refresh_token) { $adapter = $this->getAdapter(); if ($adapter->supportsTokenRefresh()) { $adapter->refreshAccessToken($refresh_token); $this->synchronizeOAuthAccount($account); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $account->save(); unset($unguarded); } } return null; } + public function willRenderLinkedAccount( + PhabricatorUser $viewer, + PhabricatorObjectItemView $item, + PhabricatorExternalAccount $account) { + + // Get a valid token, possibly refreshing it. + $oauth_token = $this->getOAuthAccessToken($account); + + $item->addAttribute(pht('OAuth2 Account')); + + if ($oauth_token) { + $oauth_expires = $account->getProperty('oauth.token.access.expires'); + if ($oauth_expires) { + $item->addAttribute( + pht( + 'Active OAuth Token (Expires: %s)', + phabricator_datetime($oauth_expires, $viewer))); + } else { + $item->addAttribute( + pht( + 'Active OAuth Token')); + } + } else { + $item->addAttribute(pht('No OAuth Access Token')); + } + + parent::willRenderLinkedAccount($viewer, $item, $account); + } + + } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderPassword.php b/src/applications/auth/provider/PhabricatorAuthProviderPassword.php index 0ed88fd599..e5a40752b0 100644 --- a/src/applications/auth/provider/PhabricatorAuthProviderPassword.php +++ b/src/applications/auth/provider/PhabricatorAuthProviderPassword.php @@ -1,242 +1,249 @@ <?php final class PhabricatorAuthProviderPassword extends PhabricatorAuthProvider { private $adapter; public function getProviderName() { return pht('Username/Password'); } public function getConfigurationHelp() { return pht( 'You can select a minimum password length by setting '. '`account.minimum-password-length` in configuration.'); } public function getDescriptionForCreate() { return pht( 'Allow users to login or register using a username and password.'); } public function getAdapter() { if (!$this->adapter) { $adapter = new PhutilAuthAdapterEmpty(); $adapter->setAdapterType('password'); $adapter->setAdapterDomain('self'); $this->adapter = $adapter; } return $this->adapter; } public function getLoginOrder() { // Make sure username/password appears first if it is enabled. return '100-'.$this->getProviderName(); } public function shouldAllowAccountLink() { return false; } public function shouldAllowAccountUnlink() { return false; } public function isDefaultRegistrationProvider() { return true; } public function buildLoginForm( PhabricatorAuthStartController $controller) { $request = $controller->getRequest(); return $this->renderPasswordLoginForm($request); } public function buildLinkForm( PhabricatorAuthLinkController $controller) { throw new Exception("Password providers can't be linked."); } private function renderPasswordLoginForm( AphrontRequest $request, $require_captcha = false, $captcha_valid = false) { $viewer = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setSubmitURI($this->getLoginURI()) ->setUser($viewer) ->setTitle(pht('Login to Phabricator')) ->addSubmitButton(pht('Login')); if ($this->shouldAllowRegistration()) { $dialog->addCancelButton( '/auth/register/', pht('Register New Account')); } $dialog->addFooter( phutil_tag( 'a', array( 'href' => '/login/email/', ), pht('Forgot your password?'))); $v_user = nonempty( $request->getStr('username'), $request->getCookie('phusr')); $e_user = null; $e_pass = null; $e_captcha = null; $errors = array(); if ($require_captcha && !$captcha_valid) { if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) { $e_captcha = pht('Invalid'); $errors[] = pht('CAPTCHA was not entered correctly.'); } else { $e_captcha = pht('Required'); $errors[] = pht('Too many login failures recently. You must '. 'submit a CAPTCHA with your login request.'); } } else if ($request->isHTTPPost()) { // NOTE: This is intentionally vague so as not to disclose whether a // given username or email is registered. $e_user = pht('Invalid'); $e_pass = pht('Invalid'); $errors[] = pht('Username or password are incorrect.'); } if ($errors) { $errors = id(new AphrontErrorView())->setErrors($errors); } $form = id(new AphrontFormLayoutView()) ->setFullWidth(true) ->appendChild($errors) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Username or Email') ->setName('username') ->setValue($v_user) ->setError($e_user)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel('Password') ->setName('password') ->setError($e_pass)); if ($require_captcha) { $form->appendChild( id(new AphrontFormRecaptchaControl()) ->setError($e_captcha)); } $dialog->appendChild($form); return $dialog; } public function processLoginRequest( PhabricatorAuthLoginController $controller) { $request = $controller->getRequest(); $viewer = $request->getUser(); $require_captcha = false; $captcha_valid = false; if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( PhabricatorUserLog::ACTION_LOGIN_FAILURE, 60 * 15); if (count($failed_attempts) > 5) { $require_captcha = true; $captcha_valid = AphrontFormRecaptchaControl::processCaptcha($request); } } $response = null; $account = null; $log_user = null; if (!$require_captcha || $captcha_valid) { $username_or_email = $request->getStr('username'); if (strlen($username_or_email)) { $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $username_or_email); if (!$user) { $user = PhabricatorUser::loadOneWithEmailAddress($username_or_email); } if ($user) { $envelope = new PhutilOpaqueEnvelope($request->getStr('password')); if ($user->comparePassword($envelope)) { $account = $this->loadOrCreateAccount($user->getPHID()); $log_user = $user; } } } } if (!$account) { $log = PhabricatorUserLog::newLog( null, $log_user, PhabricatorUserLog::ACTION_LOGIN_FAILURE); $log->save(); $request->clearCookie('phusr'); $request->clearCookie('phsid'); $response = $controller->buildProviderPageResponse( $this, $this->renderPasswordLoginForm( $request, $require_captcha, $captcha_valid)); } return array($account, $response); } public function shouldRequireRegistrationPassword() { return true; } public function getDefaultExternalAccount() { $adapter = $this->getAdapter(); return id(new PhabricatorExternalAccount()) ->setAccountType($adapter->getAdapterType()) ->setAccountDomain($adapter->getAdapterDomain()); } protected function willSaveAccount(PhabricatorExternalAccount $account) { parent::willSaveAccount($account); $account->setUserPHID($account->getAccountID()); } public function willRegisterAccount(PhabricatorExternalAccount $account) { parent::willRegisterAccount($account); $account->setAccountID($account->getUserPHID()); } public static function getPasswordProvider() { $providers = self::getAllEnabledProviders(); foreach ($providers as $provider) { if ($provider instanceof PhabricatorAuthProviderPassword) { return $provider; } } return null; } + public function willRenderLinkedAccount( + PhabricatorUser $viewer, + PhabricatorObjectItemView $item, + PhabricatorExternalAccount $account) { + return; + } + } diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php b/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php index 6cf3f42da9..755fb1edd6 100644 --- a/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php +++ b/src/applications/settings/panel/PhabricatorSettingsPanelExternalAccounts.php @@ -1,140 +1,129 @@ <?php final class PhabricatorSettingsPanelExternalAccounts extends PhabricatorSettingsPanel { public function getPanelKey() { return 'external'; } public function getPanelName() { return pht('External Accounts'); } public function getPanelGroup() { return pht('Authentication'); } public function isEnabled() { return true; } public function processRequest(AphrontRequest $request) { $viewer = $request->getUser(); $providers = PhabricatorAuthProvider::getAllProviders(); $accounts = id(new PhabricatorExternalAccountQuery()) ->setViewer($viewer) ->withUserPHIDs(array($viewer->getPHID())) ->needImages(true) ->execute(); $linked_head = id(new PhabricatorHeaderView()) ->setHeader(pht('Linked Accounts and Authentication')); $linked = id(new PhabricatorObjectItemListView()) ->setUser($viewer) ->setNoDataString(pht('You have no linked accounts.')); $login_accounts = 0; foreach ($accounts as $account) { if ($account->isUsableForLogin()) { $login_accounts++; } } foreach ($accounts as $account) { $item = id(new PhabricatorObjectItemView()); $provider = idx($providers, $account->getProviderKey()); if ($provider) { $item->setHeader($provider->getProviderName()); $can_unlink = $provider->shouldAllowAccountUnlink(); if (!$can_unlink) { $item->addAttribute(pht('Permanently Linked')); } } else { $item->setHeader( pht('Unknown Account ("%s")', $account->getProviderKey())); $can_unlink = true; } $can_login = $account->isUsableForLogin(); if (!$can_login) { $item->addAttribute( pht( 'Disabled (an administrator has disabled login for this '. 'account provider).')); } $can_unlink = $can_unlink && (!$can_login || ($login_accounts > 1)); $item->addAction( id(new PHUIListItemView()) ->setIcon('delete') ->setWorkflow(true) ->setDisabled(!$can_unlink) ->setHref('/auth/unlink/'.$account->getProviderKey().'/')); - $account_view = id(new PhabricatorAuthAccountView()) - ->setExternalAccount($account); - if ($provider) { - $account_view->setAuthProvider($provider); + $provider->willRenderLinkedAccount($viewer, $item, $account); } - $item->appendChild( - phutil_tag( - 'div', - array( - 'class' => 'mmr mml mst mmb', - ), - $account_view)); - $linked->addItem($item); } $linkable_head = id(new PhabricatorHeaderView()) ->setHeader(pht('Add External Account')); $linkable = id(new PhabricatorObjectItemListView()) ->setUser($viewer) ->setNoDataString( pht('Your account is linked with all available providers.')); $accounts = mpull($accounts, null, 'getProviderKey'); $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $providers = msort($providers, 'getProviderName'); foreach ($providers as $key => $provider) { if (isset($accounts[$key])) { continue; } if (!$provider->shouldAllowAccountLink()) { continue; } $link_uri = '/auth/link/'.$provider->getProviderKey().'/'; $item = id(new PhabricatorObjectItemView()); $item->setHeader($provider->getProviderName()); $item->setHref($link_uri); $item->addAction( id(new PHUIListItemView()) ->setIcon('link') ->setHref($link_uri)); $linkable->addItem($item); } return array( $linked_head, $linked, $linkable_head, $linkable, ); } }