Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / auth / provider / PhabricatorAuthProvider.php
blob735dd42b4bc44b6c514a20982090e869101aba18
1 <?php
3 abstract class PhabricatorAuthProvider extends Phobject {
5 private $providerConfig;
7 public function attachProviderConfig(PhabricatorAuthProviderConfig $config) {
8 $this->providerConfig = $config;
9 return $this;
12 public function hasProviderConfig() {
13 return (bool)$this->providerConfig;
16 public function getProviderConfig() {
17 if ($this->providerConfig === null) {
18 throw new PhutilInvalidStateException('attachProviderConfig');
20 return $this->providerConfig;
23 public function getProviderConfigPHID() {
24 return $this->getProviderConfig()->getPHID();
27 public function getConfigurationHelp() {
28 return null;
31 public function getDefaultProviderConfig() {
32 return id(new PhabricatorAuthProviderConfig())
33 ->setProviderClass(get_class($this))
34 ->setIsEnabled(1)
35 ->setShouldAllowLogin(1)
36 ->setShouldAllowRegistration(1)
37 ->setShouldAllowLink(1)
38 ->setShouldAllowUnlink(1);
41 public function getNameForCreate() {
42 return $this->getProviderName();
45 public function getDescriptionForCreate() {
46 return null;
49 public function getProviderKey() {
50 return $this->getAdapter()->getAdapterKey();
53 public function getProviderType() {
54 return $this->getAdapter()->getAdapterType();
57 public function getProviderDomain() {
58 return $this->getAdapter()->getAdapterDomain();
61 public static function getAllBaseProviders() {
62 return id(new PhutilClassMapQuery())
63 ->setAncestorClass(__CLASS__)
64 ->execute();
67 public static function getAllProviders() {
68 static $providers;
70 if ($providers === null) {
71 $objects = self::getAllBaseProviders();
73 $configs = id(new PhabricatorAuthProviderConfigQuery())
74 ->setViewer(PhabricatorUser::getOmnipotentUser())
75 ->execute();
77 $providers = array();
78 foreach ($configs as $config) {
79 if (!isset($objects[$config->getProviderClass()])) {
80 // This configuration is for a provider which is not installed.
81 continue;
84 $object = clone $objects[$config->getProviderClass()];
85 $object->attachProviderConfig($config);
87 $key = $object->getProviderKey();
88 if (isset($providers[$key])) {
89 throw new Exception(
90 pht(
91 "Two authentication providers use the same provider key ".
92 "('%s'). Each provider must be identified by a unique key.",
93 $key));
95 $providers[$key] = $object;
99 return $providers;
102 public static function getAllEnabledProviders() {
103 $providers = self::getAllProviders();
104 foreach ($providers as $key => $provider) {
105 if (!$provider->isEnabled()) {
106 unset($providers[$key]);
109 return $providers;
112 public static function getEnabledProviderByKey($provider_key) {
113 return idx(self::getAllEnabledProviders(), $provider_key);
116 abstract public function getProviderName();
117 abstract public function getAdapter();
119 public function isEnabled() {
120 return $this->getProviderConfig()->getIsEnabled();
123 public function shouldAllowLogin() {
124 return $this->getProviderConfig()->getShouldAllowLogin();
127 public function shouldAllowRegistration() {
128 if (!$this->shouldAllowLogin()) {
129 return false;
132 return $this->getProviderConfig()->getShouldAllowRegistration();
135 public function shouldAllowAccountLink() {
136 return $this->getProviderConfig()->getShouldAllowLink();
139 public function shouldAllowAccountUnlink() {
140 return $this->getProviderConfig()->getShouldAllowUnlink();
143 public function shouldTrustEmails() {
144 return $this->shouldAllowEmailTrustConfiguration() &&
145 $this->getProviderConfig()->getShouldTrustEmails();
149 * Should we allow the adapter to be marked as "trusted". This is true for
150 * all adapters except those that allow the user to type in emails (see
151 * @{class:PhabricatorPasswordAuthProvider}).
153 public function shouldAllowEmailTrustConfiguration() {
154 return true;
157 public function buildLoginForm(PhabricatorAuthStartController $controller) {
158 return $this->renderLoginForm($controller->getRequest(), $mode = 'start');
161 public function buildInviteForm(PhabricatorAuthStartController $controller) {
162 return $this->renderLoginForm($controller->getRequest(), $mode = 'invite');
165 abstract public function processLoginRequest(
166 PhabricatorAuthLoginController $controller);
168 public function buildLinkForm($controller) {
169 return $this->renderLoginForm($controller->getRequest(), $mode = 'link');
172 public function shouldAllowAccountRefresh() {
173 return true;
176 public function buildRefreshForm(
177 PhabricatorAuthLinkController $controller) {
178 return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh');
181 protected function renderLoginForm(AphrontRequest $request, $mode) {
182 throw new PhutilMethodNotImplementedException();
185 public function createProviders() {
186 return array($this);
189 protected function willSaveAccount(PhabricatorExternalAccount $account) {
190 return;
193 final protected function newExternalAccountForIdentifiers(
194 array $identifiers) {
196 assert_instances_of($identifiers, 'PhabricatorExternalAccountIdentifier');
198 if (!$identifiers) {
199 throw new Exception(
200 pht(
201 'Authentication provider (of class "%s") is attempting to '.
202 'load or create an external account, but provided no account '.
203 'identifiers.',
204 get_class($this)));
207 $config = $this->getProviderConfig();
208 $viewer = PhabricatorUser::getOmnipotentUser();
210 $raw_identifiers = mpull($identifiers, 'getIdentifierRaw');
212 $accounts = id(new PhabricatorExternalAccountQuery())
213 ->setViewer($viewer)
214 ->withProviderConfigPHIDs(array($config->getPHID()))
215 ->withRawAccountIdentifiers($raw_identifiers)
216 ->needAccountIdentifiers(true)
217 ->execute();
218 if (!$accounts) {
219 $account = $this->newExternalAccount();
220 } else if (count($accounts) === 1) {
221 $account = head($accounts);
222 } else {
223 throw new Exception(
224 pht(
225 'Authentication provider (of class "%s") is attempting to load '.
226 'or create an external account, but provided a list of '.
227 'account identifiers which map to more than one account: %s.',
228 get_class($this),
229 implode(', ', $raw_identifiers)));
232 // See T13493. Add all the identifiers to the account. In the case where
233 // an account initially has a lower-quality identifier (like an email
234 // address) and later adds a higher-quality identifier (like a GUID), this
235 // allows us to automatically upgrade toward the higher-quality identifier
236 // and survive API changes which remove the lower-quality identifier more
237 // gracefully.
239 foreach ($identifiers as $identifier) {
240 $account->appendIdentifier($identifier);
243 return $this->didUpdateAccount($account);
246 final protected function newExternalAccountForUser(PhabricatorUser $user) {
247 $config = $this->getProviderConfig();
249 // When a user logs in with a provider like username/password, they
250 // always already have a Phabricator account (since there's no way they
251 // could have a username otherwise).
253 // These users should never go to registration, so we're building a
254 // dummy "external account" which just links directly back to their
255 // internal account.
257 $account = id(new PhabricatorExternalAccountQuery())
258 ->setViewer($user)
259 ->withProviderConfigPHIDs(array($config->getPHID()))
260 ->withUserPHIDs(array($user->getPHID()))
261 ->executeOne();
262 if (!$account) {
263 $account = $this->newExternalAccount()
264 ->setUserPHID($user->getPHID());
267 return $this->didUpdateAccount($account);
270 private function didUpdateAccount(PhabricatorExternalAccount $account) {
271 $adapter = $this->getAdapter();
273 $account->setUsername($adapter->getAccountName());
274 $account->setRealName($adapter->getAccountRealName());
275 $account->setEmail($adapter->getAccountEmail());
276 $account->setAccountURI($adapter->getAccountURI());
278 $account->setProfileImagePHID(null);
279 $image_uri = $adapter->getAccountImageURI();
280 if ($image_uri) {
281 try {
282 $name = PhabricatorSlug::normalize($this->getProviderName());
283 $name = $name.'-profile.jpg';
285 // TODO: If the image has not changed, we do not need to make a new
286 // file entry for it, but there's no convenient way to do this with
287 // PhabricatorFile right now. The storage will get shared, so the impact
288 // here is negligible.
290 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
291 $image_file = PhabricatorFile::newFromFileDownload(
292 $image_uri,
293 array(
294 'name' => $name,
295 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE,
297 if ($image_file->isViewableImage()) {
298 $image_file
299 ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy())
300 ->setCanCDN(true)
301 ->save();
302 $account->setProfileImagePHID($image_file->getPHID());
303 } else {
304 $image_file->delete();
306 unset($unguarded);
308 } catch (Exception $ex) {
309 // Log this but proceed, it's not especially important that we
310 // be able to pull profile images.
311 phlog($ex);
315 $this->willSaveAccount($account);
317 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
318 $account->save();
319 unset($unguarded);
321 return $account;
324 public function getLoginURI() {
325 $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
326 return $app->getApplicationURI('/login/'.$this->getProviderKey().'/');
329 public function getSettingsURI() {
330 return '/settings/panel/external/';
333 public function getStartURI() {
334 $app = PhabricatorApplication::getByClass('PhabricatorAuthApplication');
335 $uri = $app->getApplicationURI('/start/');
336 return $uri;
339 public function isDefaultRegistrationProvider() {
340 return false;
343 public function shouldRequireRegistrationPassword() {
344 return false;
347 public function newDefaultExternalAccount() {
348 return $this->newExternalAccount();
351 protected function newExternalAccount() {
352 $config = $this->getProviderConfig();
353 $adapter = $this->getAdapter();
355 $account = id(new PhabricatorExternalAccount())
356 ->setProviderConfigPHID($config->getPHID())
357 ->attachAccountIdentifiers(array());
359 // TODO: Remove this when these columns are removed. They no longer have
360 // readers or writers (other than this callsite).
362 $account
363 ->setAccountType($adapter->getAdapterType())
364 ->setAccountDomain($adapter->getAdapterDomain());
366 // TODO: Remove this when "accountID" is removed; the column is not
367 // nullable.
369 $account->setAccountID('');
371 return $account;
374 public function getLoginOrder() {
375 return '500-'.$this->getProviderName();
378 protected function getLoginIcon() {
379 return 'Generic';
382 public function newIconView() {
383 return id(new PHUIIconView())
384 ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
385 ->setSpriteIcon($this->getLoginIcon());
388 public function isLoginFormAButton() {
389 return false;
392 public function renderConfigPropertyTransactionTitle(
393 PhabricatorAuthProviderConfigTransaction $xaction) {
395 return null;
398 public function readFormValuesFromProvider() {
399 return array();
402 public function readFormValuesFromRequest(AphrontRequest $request) {
403 return array();
406 public function processEditForm(
407 AphrontRequest $request,
408 array $values) {
410 $errors = array();
411 $issues = array();
413 return array($errors, $issues, $values);
416 public function extendEditForm(
417 AphrontRequest $request,
418 AphrontFormView $form,
419 array $values,
420 array $issues) {
422 return;
425 public function willRenderLinkedAccount(
426 PhabricatorUser $viewer,
427 PHUIObjectItemView $item,
428 PhabricatorExternalAccount $account) {
430 $account_view = id(new PhabricatorAuthAccountView())
431 ->setExternalAccount($account)
432 ->setAuthProvider($this);
434 $item->appendChild(
435 phutil_tag(
436 'div',
437 array(
438 'class' => 'mmr mml mst mmb',
440 $account_view));
444 * Return true to use a two-step configuration (setup, configure) instead of
445 * the default single-step configuration. In practice, this means that
446 * creating a new provider instance will redirect back to the edit page
447 * instead of the provider list.
449 * @return bool True if this provider uses two-step configuration.
451 public function hasSetupStep() {
452 return false;
456 * Render a standard login/register button element.
458 * The `$attributes` parameter takes these keys:
460 * - `uri`: URI the button should take the user to when clicked.
461 * - `method`: Optional HTTP method the button should use, defaults to GET.
463 * @param AphrontRequest HTTP request.
464 * @param string Request mode string.
465 * @param map Additional parameters, see above.
466 * @return wild Log in button.
468 protected function renderStandardLoginButton(
469 AphrontRequest $request,
470 $mode,
471 array $attributes = array()) {
473 PhutilTypeSpec::checkMap(
474 $attributes,
475 array(
476 'method' => 'optional string',
477 'uri' => 'string',
478 'sigil' => 'optional string',
481 $viewer = $request->getUser();
482 $adapter = $this->getAdapter();
484 if ($mode == 'link') {
485 $button_text = pht('Link External Account');
486 } else if ($mode == 'refresh') {
487 $button_text = pht('Refresh Account Link');
488 } else if ($mode == 'invite') {
489 $button_text = pht('Register Account');
490 } else if ($this->shouldAllowRegistration()) {
491 $button_text = pht('Log In or Register');
492 } else {
493 $button_text = pht('Log In');
496 $icon = id(new PHUIIconView())
497 ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
498 ->setSpriteIcon($this->getLoginIcon());
500 $button = id(new PHUIButtonView())
501 ->setSize(PHUIButtonView::BIG)
502 ->setColor(PHUIButtonView::GREY)
503 ->setIcon($icon)
504 ->setText($button_text)
505 ->setSubtext($this->getProviderName());
507 $uri = $attributes['uri'];
508 $uri = new PhutilURI($uri);
509 $params = $uri->getQueryParamsAsPairList();
510 $uri->removeAllQueryParams();
512 $content = array($button);
514 foreach ($params as $pair) {
515 list($key, $value) = $pair;
516 $content[] = phutil_tag(
517 'input',
518 array(
519 'type' => 'hidden',
520 'name' => $key,
521 'value' => $value,
525 $static_response = CelerityAPI::getStaticResourceResponse();
526 $static_response->addContentSecurityPolicyURI('form-action', (string)$uri);
528 foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) {
529 $static_response->addContentSecurityPolicyURI('form-action', $csp_uri);
532 return phabricator_form(
533 $viewer,
534 array(
535 'method' => idx($attributes, 'method', 'GET'),
536 'action' => (string)$uri,
537 'sigil' => idx($attributes, 'sigil'),
539 $content);
542 public function renderConfigurationFooter() {
543 return null;
546 public function getAuthCSRFCode(AphrontRequest $request) {
547 $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID);
548 if (!strlen($phcid)) {
549 throw new AphrontMalformedRequestException(
550 pht('Missing Client ID Cookie'),
551 pht(
552 'Your browser did not submit a "%s" cookie with client state '.
553 'information in the request. Check that cookies are enabled. '.
554 'If this problem persists, you may need to clear your cookies.',
555 PhabricatorCookies::COOKIE_CLIENTID),
556 true);
559 return PhabricatorHash::weakDigest($phcid);
562 protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) {
563 $expect = $this->getAuthCSRFCode($request);
565 if (!strlen($actual)) {
566 throw new Exception(
567 pht(
568 'The authentication provider did not return a client state '.
569 'parameter in its response, but one was expected. If this '.
570 'problem persists, you may need to clear your cookies.'));
573 if (!phutil_hashes_are_identical($actual, $expect)) {
574 throw new Exception(
575 pht(
576 'The authentication provider did not return the correct client '.
577 'state parameter in its response. If this problem persists, you may '.
578 'need to clear your cookies.'));
582 public function supportsAutoLogin() {
583 return false;
586 public function getAutoLoginURI(AphrontRequest $request) {
587 throw new PhutilMethodNotImplementedException();
590 protected function getContentSecurityPolicyFormActions() {
591 return array();