@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 616 lines 18 kB view raw
1<?php 2 3abstract class PhabricatorAuthProvider extends Phobject { 4 5 private $providerConfig; 6 7 public function attachProviderConfig(PhabricatorAuthProviderConfig $config) { 8 $this->providerConfig = $config; 9 return $this; 10 } 11 12 public function hasProviderConfig() { 13 return (bool)$this->providerConfig; 14 } 15 16 public function getProviderConfig() { 17 if ($this->providerConfig === null) { 18 throw new PhutilInvalidStateException('attachProviderConfig'); 19 } 20 return $this->providerConfig; 21 } 22 23 public function getProviderConfigPHID() { 24 return $this->getProviderConfig()->getPHID(); 25 } 26 27 public function getConfigurationHelp() { 28 return null; 29 } 30 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); 39 } 40 41 public function getNameForCreate() { 42 return $this->getProviderName(); 43 } 44 45 public function getDescriptionForCreate() { 46 return null; 47 } 48 49 public function getProviderKey() { 50 return $this->getAdapter()->getAdapterKey(); 51 } 52 53 public function getProviderType() { 54 return $this->getAdapter()->getAdapterType(); 55 } 56 57 public function getProviderDomain() { 58 return $this->getAdapter()->getAdapterDomain(); 59 } 60 61 public static function getAllBaseProviders() { 62 return id(new PhutilClassMapQuery()) 63 ->setAncestorClass(self::class) 64 ->execute(); 65 } 66 67 /** 68 * @return array<static> 69 */ 70 public static function getAllProviders() { 71 static $providers; 72 73 if ($providers === null) { 74 $objects = self::getAllBaseProviders(); 75 76 $configs = id(new PhabricatorAuthProviderConfigQuery()) 77 ->setViewer(PhabricatorUser::getOmnipotentUser()) 78 ->execute(); 79 80 $providers = array(); 81 foreach ($configs as $config) { 82 if (!isset($objects[$config->getProviderClass()])) { 83 // This configuration is for a provider which is not installed. 84 continue; 85 } 86 87 $object = clone $objects[$config->getProviderClass()]; 88 $object->attachProviderConfig($config); 89 90 $key = $object->getProviderKey(); 91 if (isset($providers[$key])) { 92 throw new Exception( 93 pht( 94 "Two authentication providers use the same provider key ". 95 "('%s'). Each provider must be identified by a unique key.", 96 $key)); 97 } 98 $providers[$key] = $object; 99 } 100 } 101 102 return $providers; 103 } 104 105 /** 106 * @return array<static> 107 */ 108 public static function getAllEnabledProviders() { 109 $providers = self::getAllProviders(); 110 foreach ($providers as $key => $provider) { 111 if (!$provider->isEnabled()) { 112 unset($providers[$key]); 113 } 114 } 115 return $providers; 116 } 117 118 public static function getEnabledProviderByKey($provider_key) { 119 return idx(self::getAllEnabledProviders(), $provider_key); 120 } 121 122 abstract public function getProviderName(); 123 abstract public function getAdapter(); 124 125 public function isEnabled() { 126 return $this->getProviderConfig()->getIsEnabled(); 127 } 128 129 public function shouldAllowLogin() { 130 return $this->getProviderConfig()->getShouldAllowLogin(); 131 } 132 133 public function shouldAllowRegistration() { 134 if (!$this->shouldAllowLogin()) { 135 return false; 136 } 137 138 return $this->getProviderConfig()->getShouldAllowRegistration(); 139 } 140 141 public function shouldAllowAccountLink() { 142 return $this->getProviderConfig()->getShouldAllowLink(); 143 } 144 145 public function shouldAllowAccountUnlink() { 146 return $this->getProviderConfig()->getShouldAllowUnlink(); 147 } 148 149 public function shouldTrustEmails() { 150 return $this->shouldAllowEmailTrustConfiguration() && 151 $this->getProviderConfig()->getShouldTrustEmails(); 152 } 153 154 /** 155 * Should we allow the adapter to be marked as "trusted". This is true for 156 * all adapters except those that allow the user to type in emails (see 157 * @{class:PhabricatorPasswordAuthProvider}). 158 */ 159 public function shouldAllowEmailTrustConfiguration() { 160 return true; 161 } 162 163 public function buildLoginForm(PhabricatorAuthStartController $controller) { 164 return $this->renderLoginForm($controller->getRequest(), $mode = 'start'); 165 } 166 167 public function buildInviteForm(PhabricatorAuthStartController $controller) { 168 return $this->renderLoginForm($controller->getRequest(), $mode = 'invite'); 169 } 170 171 abstract public function processLoginRequest( 172 PhabricatorAuthLoginController $controller); 173 174 public function buildLinkForm($controller) { 175 return $this->renderLoginForm($controller->getRequest(), $mode = 'link'); 176 } 177 178 public function shouldAllowAccountRefresh() { 179 return true; 180 } 181 182 public function buildRefreshForm( 183 PhabricatorAuthLinkController $controller) { 184 return $this->renderLoginForm($controller->getRequest(), $mode = 'refresh'); 185 } 186 187 protected function renderLoginForm(AphrontRequest $request, $mode) { 188 throw new PhutilMethodNotImplementedException(); 189 } 190 191 public function createProviders() { 192 return array($this); 193 } 194 195 protected function willSaveAccount(PhabricatorExternalAccount $account) { 196 return; 197 } 198 199 /** 200 * @param array<PhabricatorExternalAccountIdentifier> $identifiers 201 */ 202 final protected function newExternalAccountForIdentifiers( 203 array $identifiers) { 204 205 assert_instances_of($identifiers, 206 PhabricatorExternalAccountIdentifier::class); 207 208 if (!$identifiers) { 209 throw new Exception( 210 pht( 211 'Authentication provider (of class "%s") is attempting to '. 212 'load or create an external account, but provided no account '. 213 'identifiers.', 214 get_class($this))); 215 } 216 217 $config = $this->getProviderConfig(); 218 $viewer = PhabricatorUser::getOmnipotentUser(); 219 220 $raw_identifiers = mpull($identifiers, 'getIdentifierRaw'); 221 222 $accounts = id(new PhabricatorExternalAccountQuery()) 223 ->setViewer($viewer) 224 ->withProviderConfigPHIDs(array($config->getPHID())) 225 ->withRawAccountIdentifiers($raw_identifiers) 226 ->needAccountIdentifiers(true) 227 ->execute(); 228 if (!$accounts) { 229 $account = $this->newExternalAccount(); 230 } else if (count($accounts) === 1) { 231 $account = head($accounts); 232 } else { 233 throw new Exception( 234 pht( 235 'Authentication provider (of class "%s") is attempting to load '. 236 'or create an external account, but provided a list of '. 237 'account identifiers which map to more than one account: %s.', 238 get_class($this), 239 implode(', ', $raw_identifiers))); 240 } 241 242 // See T13493. Add all the identifiers to the account. In the case where 243 // an account initially has a lower-quality identifier (like an email 244 // address) and later adds a higher-quality identifier (like a GUID), this 245 // allows us to automatically upgrade toward the higher-quality identifier 246 // and survive API changes which remove the lower-quality identifier more 247 // gracefully. 248 249 foreach ($identifiers as $identifier) { 250 $account->appendIdentifier($identifier); 251 } 252 253 return $this->didUpdateAccount($account); 254 } 255 256 final protected function newExternalAccountForUser(PhabricatorUser $user) { 257 $config = $this->getProviderConfig(); 258 259 // When a user logs in with a provider like username/password, they 260 // always already have a Phabricator account (since there's no way they 261 // could have a username otherwise). 262 263 // These users should never go to registration, so we're building a 264 // dummy "external account" which just links directly back to their 265 // internal account. 266 267 $account = id(new PhabricatorExternalAccountQuery()) 268 ->setViewer($user) 269 ->withProviderConfigPHIDs(array($config->getPHID())) 270 ->withUserPHIDs(array($user->getPHID())) 271 ->executeOne(); 272 if (!$account) { 273 $account = $this->newExternalAccount() 274 ->setUserPHID($user->getPHID()); 275 } 276 277 return $this->didUpdateAccount($account); 278 } 279 280 /** 281 * @return PhabricatorExternalAccount 282 */ 283 private function didUpdateAccount(PhabricatorExternalAccount $account) { 284 $adapter = $this->getAdapter(); 285 286 $account->setUsername($adapter->getAccountName()); 287 $account->setRealName($adapter->getAccountRealName()); 288 $account->setEmail($adapter->getAccountEmail()); 289 $account->setAccountURI($adapter->getAccountURI()); 290 291 $account->setProfileImagePHID(null); 292 $image_uri = $adapter->getAccountImageURI(); 293 if ($image_uri) { 294 try { 295 $name = PhabricatorSlug::normalize($this->getProviderName()); 296 $name = $name.'-profile.jpg'; 297 298 // TODO: If the image has not changed, we do not need to make a new 299 // file entry for it, but there's no convenient way to do this with 300 // PhabricatorFile right now. The storage will get shared, so the impact 301 // here is negligible. 302 303 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 304 $image_file = PhabricatorFile::newFromFileDownload( 305 $image_uri, 306 array( 307 'name' => $name, 308 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, 309 )); 310 if ($image_file->isViewableImage()) { 311 $image_file 312 ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) 313 ->setCanCDN(true) 314 ->save(); 315 $account->setProfileImagePHID($image_file->getPHID()); 316 } else { 317 $image_file->delete(); 318 } 319 unset($unguarded); 320 321 } catch (Exception $ex) { 322 // Log this but proceed, it's not especially important that we 323 // be able to pull profile images. 324 phlog($ex); 325 } 326 } 327 328 $this->willSaveAccount($account); 329 330 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 331 $account->save(); 332 unset($unguarded); 333 334 return $account; 335 } 336 337 public function getLoginURI() { 338 $app = PhabricatorApplication::getByClass( 339 PhabricatorAuthApplication::class); 340 return $app->getApplicationURI('/login/'.$this->getProviderKey().'/'); 341 } 342 343 public function getSettingsURI() { 344 return '/settings/panel/external/'; 345 } 346 347 public function getStartURI() { 348 $app = PhabricatorApplication::getByClass( 349 PhabricatorAuthApplication::class); 350 $uri = $app->getApplicationURI('/start/'); 351 return $uri; 352 } 353 354 public function isDefaultRegistrationProvider() { 355 return false; 356 } 357 358 public function shouldRequireRegistrationPassword() { 359 return false; 360 } 361 362 public function newDefaultExternalAccount() { 363 return $this->newExternalAccount(); 364 } 365 366 protected function newExternalAccount() { 367 $config = $this->getProviderConfig(); 368 $adapter = $this->getAdapter(); 369 370 $account = id(new PhabricatorExternalAccount()) 371 ->setProviderConfigPHID($config->getPHID()) 372 ->attachAccountIdentifiers(array()); 373 374 // TODO: Remove this when these columns are removed. They no longer have 375 // readers or writers (other than this callsite). 376 377 $account 378 ->setAccountType($adapter->getAdapterType()) 379 ->setAccountDomain($adapter->getAdapterDomain()); 380 381 // TODO: Remove this when "accountID" is removed; the column is not 382 // nullable. 383 384 $account->setAccountID(''); 385 386 return $account; 387 } 388 389 public function getLoginOrder() { 390 return '500-'.$this->getProviderName(); 391 } 392 393 /** 394 * @return string Name of the icon of the auth provider 395 */ 396 protected function getLoginIcon() { 397 return 'Generic'; 398 } 399 400 /** 401 * @return PHUIIconView Icon of the auth provider 402 */ 403 public function newIconView() { 404 return id(new PHUIIconView()) 405 ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) 406 ->setSpriteIcon($this->getLoginIcon()); 407 } 408 409 public function isLoginFormAButton() { 410 return false; 411 } 412 413 public function renderConfigPropertyTransactionTitle( 414 PhabricatorAuthProviderConfigTransaction $xaction) { 415 416 return null; 417 } 418 419 public function readFormValuesFromProvider() { 420 return array(); 421 } 422 423 public function readFormValuesFromRequest(AphrontRequest $request) { 424 return array(); 425 } 426 427 public function processEditForm( 428 AphrontRequest $request, 429 array $values) { 430 431 $errors = array(); 432 $issues = array(); 433 434 return array($errors, $issues, $values); 435 } 436 437 public function extendEditForm( 438 AphrontRequest $request, 439 AphrontFormView $form, 440 array $values, 441 array $issues) { 442 443 return; 444 } 445 446 public function willRenderLinkedAccount( 447 PhabricatorUser $viewer, 448 PHUIObjectItemView $item, 449 PhabricatorExternalAccount $account) { 450 451 $account_view = id(new PhabricatorAuthAccountView()) 452 ->setExternalAccount($account) 453 ->setAuthProvider($this); 454 455 $item->appendChild( 456 phutil_tag( 457 'div', 458 array( 459 'class' => 'mmr mml mst mmb', 460 ), 461 $account_view)); 462 } 463 464 /** 465 * Return true to use a two-step configuration (setup, configure) instead of 466 * the default single-step configuration. In practice, this means that 467 * creating a new provider instance will redirect back to the edit page 468 * instead of the provider list. 469 * 470 * @return bool True if this provider uses two-step configuration. 471 */ 472 public function hasSetupStep() { 473 return false; 474 } 475 476 /** 477 * Render a standard login/register button element. 478 * 479 * The `$attributes` parameter takes these keys: 480 * 481 * - `uri`: URI the button should take the user to when clicked. 482 * - `method`: Optional HTTP method the button should use, defaults to GET. 483 * 484 * @param AphrontRequest $request HTTP request. 485 * @param string $mode Request mode string. 486 * @param map $attributes (optional) Additional parameters, see 487 * above. 488 * @return PhutilSafeHTML Log in button. 489 */ 490 protected function renderStandardLoginButton( 491 AphrontRequest $request, 492 $mode, 493 array $attributes = array()) { 494 495 PhutilTypeSpec::checkMap( 496 $attributes, 497 array( 498 'method' => 'optional string', 499 'uri' => 'string', 500 'sigil' => 'optional string', 501 )); 502 503 $viewer = $request->getUser(); 504 $adapter = $this->getAdapter(); 505 506 if ($mode == 'link') { 507 $button_text = pht('Link External Account'); 508 } else if ($mode == 'refresh') { 509 $button_text = pht('Refresh Account Link'); 510 } else if ($mode == 'invite') { 511 $button_text = pht('Register Account'); 512 } else if ($this->shouldAllowRegistration()) { 513 $button_text = pht('Log In or Register'); 514 } else { 515 $button_text = pht('Log In'); 516 } 517 518 $icon = id(new PHUIIconView()) 519 ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) 520 ->setSpriteIcon($this->getLoginIcon()); 521 522 $button = id(new PHUIButtonView()) 523 ->setSize(PHUIButtonView::BIG) 524 ->setColor(PHUIButtonView::GREY) 525 ->setIcon($icon) 526 ->setText($button_text) 527 ->setSubtext($this->getProviderName()); 528 529 $uri = $attributes['uri']; 530 $uri = new PhutilURI($uri); 531 $params = $uri->getQueryParamsAsPairList(); 532 $uri->removeAllQueryParams(); 533 534 $content = array($button); 535 536 foreach ($params as $pair) { 537 list($key, $value) = $pair; 538 $content[] = phutil_tag( 539 'input', 540 array( 541 'type' => 'hidden', 542 'name' => $key, 543 'value' => $value, 544 )); 545 } 546 547 $static_response = CelerityAPI::getStaticResourceResponse(); 548 $static_response->addContentSecurityPolicyURI('form-action', (string)$uri); 549 550 foreach ($this->getContentSecurityPolicyFormActions() as $csp_uri) { 551 $static_response->addContentSecurityPolicyURI('form-action', $csp_uri); 552 } 553 554 return phabricator_form( 555 $viewer, 556 array( 557 'method' => idx($attributes, 'method', 'GET'), 558 'action' => (string)$uri, 559 'sigil' => idx($attributes, 'sigil'), 560 ), 561 $content); 562 } 563 564 public function renderConfigurationFooter() { 565 return null; 566 } 567 568 public function getAuthCSRFCode(AphrontRequest $request) { 569 $phcid = $request->getCookie(PhabricatorCookies::COOKIE_CLIENTID); 570 if (!phutil_nonempty_string($phcid)) { 571 throw new AphrontMalformedRequestException( 572 pht('Missing Client ID Cookie'), 573 pht( 574 'Your browser did not submit a "%s" cookie with client state '. 575 'information in the request. Check that cookies are enabled. '. 576 'If this problem persists, you may need to clear your cookies.', 577 PhabricatorCookies::COOKIE_CLIENTID), 578 true); 579 } 580 581 return PhabricatorHash::weakDigest($phcid); 582 } 583 584 protected function verifyAuthCSRFCode(AphrontRequest $request, $actual) { 585 $expect = $this->getAuthCSRFCode($request); 586 587 if (!phutil_nonempty_string($actual)) { 588 throw new Exception( 589 pht( 590 'The authentication provider did not return a client state '. 591 'parameter in its response, but one was expected. If this '. 592 'problem persists, you may need to clear your cookies.')); 593 } 594 595 if (!phutil_hashes_are_identical($actual, $expect)) { 596 throw new Exception( 597 pht( 598 'The authentication provider did not return the correct client '. 599 'state parameter in its response. If this problem persists, you may '. 600 'need to clear your cookies.')); 601 } 602 } 603 604 public function supportsAutoLogin() { 605 return false; 606 } 607 608 public function getAutoLoginURI(AphrontRequest $request) { 609 throw new PhutilMethodNotImplementedException(); 610 } 611 612 protected function getContentSecurityPolicyFormActions() { 613 return array(); 614 } 615 616}