[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @task factors Multi-Factor Authentication 5 */ 6 final class PhabricatorUser 7 extends PhabricatorUserDAO 8 implements 9 PhutilPerson, 10 PhabricatorPolicyInterface, 11 PhabricatorCustomFieldInterface, 12 PhabricatorDestructibleInterface, 13 PhabricatorSSHPublicKeyInterface { 14 15 const SESSION_TABLE = 'phabricator_session'; 16 const NAMETOKEN_TABLE = 'user_nametoken'; 17 const MAXIMUM_USERNAME_LENGTH = 64; 18 19 protected $userName; 20 protected $realName; 21 protected $sex; 22 protected $translation; 23 protected $passwordSalt; 24 protected $passwordHash; 25 protected $profileImagePHID; 26 protected $timezoneIdentifier = ''; 27 28 protected $consoleEnabled = 0; 29 protected $consoleVisible = 0; 30 protected $consoleTab = ''; 31 32 protected $conduitCertificate; 33 34 protected $isSystemAgent = 0; 35 protected $isAdmin = 0; 36 protected $isDisabled = 0; 37 protected $isEmailVerified = 0; 38 protected $isApproved = 0; 39 protected $isEnrolledInMultiFactor = 0; 40 41 protected $accountSecret; 42 43 private $profileImage = self::ATTACHABLE; 44 private $profile = null; 45 private $status = self::ATTACHABLE; 46 private $preferences = null; 47 private $omnipotent = false; 48 private $customFields = self::ATTACHABLE; 49 50 private $alternateCSRFString = self::ATTACHABLE; 51 private $session = self::ATTACHABLE; 52 53 protected function readField($field) { 54 switch ($field) { 55 case 'timezoneIdentifier': 56 // If the user hasn't set one, guess the server's time. 57 return nonempty( 58 $this->timezoneIdentifier, 59 date_default_timezone_get()); 60 // Make sure these return booleans. 61 case 'isAdmin': 62 return (bool)$this->isAdmin; 63 case 'isDisabled': 64 return (bool)$this->isDisabled; 65 case 'isSystemAgent': 66 return (bool)$this->isSystemAgent; 67 case 'isEmailVerified': 68 return (bool)$this->isEmailVerified; 69 case 'isApproved': 70 return (bool)$this->isApproved; 71 default: 72 return parent::readField($field); 73 } 74 } 75 76 77 /** 78 * Is this a live account which has passed required approvals? Returns true 79 * if this is an enabled, verified (if required), approved (if required) 80 * account, and false otherwise. 81 * 82 * @return bool True if this is a standard, usable account. 83 */ 84 public function isUserActivated() { 85 if ($this->getIsDisabled()) { 86 return false; 87 } 88 89 if (!$this->getIsApproved()) { 90 return false; 91 } 92 93 if (PhabricatorUserEmail::isEmailVerificationRequired()) { 94 if (!$this->getIsEmailVerified()) { 95 return false; 96 } 97 } 98 99 return true; 100 } 101 102 /** 103 * Returns `true` if this is a standard user who is logged in. Returns `false` 104 * for logged out, anonymous, or external users. 105 * 106 * @return bool `true` if the user is a standard user who is logged in with 107 * a normal session. 108 */ 109 public function getIsStandardUser() { 110 $type_user = PhabricatorPeopleUserPHIDType::TYPECONST; 111 return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user); 112 } 113 114 public function getConfiguration() { 115 return array( 116 self::CONFIG_AUX_PHID => true, 117 self::CONFIG_COLUMN_SCHEMA => array( 118 'userName' => 'sort64', 119 'realName' => 'text128', 120 'sex' => 'text4?', 121 'translation' => 'text64?', 122 'passwordSalt' => 'text32?', 123 'passwordHash' => 'text128?', 124 'profileImagePHID' => 'phid?', 125 'consoleEnabled' => 'bool', 126 'consoleVisible' => 'bool', 127 'consoleTab' => 'text64', 128 'conduitCertificate' => 'text255', 129 'isSystemAgent' => 'bool', 130 'isDisabled' => 'bool', 131 'isAdmin' => 'bool', 132 'timezoneIdentifier' => 'text255', 133 'isEmailVerified' => 'uint32', 134 'isApproved' => 'uint32', 135 'accountSecret' => 'bytes64', 136 'isEnrolledInMultiFactor' => 'bool', 137 ), 138 self::CONFIG_KEY_SCHEMA => array( 139 'key_phid' => null, 140 'phid' => array( 141 'columns' => array('phid'), 142 'unique' => true, 143 ), 144 'userName' => array( 145 'columns' => array('userName'), 146 'unique' => true, 147 ), 148 'realName' => array( 149 'columns' => array('realName'), 150 ), 151 'key_approved' => array( 152 'columns' => array('isApproved'), 153 ), 154 ), 155 ) + parent::getConfiguration(); 156 } 157 158 public function generatePHID() { 159 return PhabricatorPHID::generateNewPHID( 160 PhabricatorPeopleUserPHIDType::TYPECONST); 161 } 162 163 public function setPassword(PhutilOpaqueEnvelope $envelope) { 164 if (!$this->getPHID()) { 165 throw new Exception( 166 'You can not set a password for an unsaved user because their PHID '. 167 'is a salt component in the password hash.'); 168 } 169 170 if (!strlen($envelope->openEnvelope())) { 171 $this->setPasswordHash(''); 172 } else { 173 $this->setPasswordSalt(md5(Filesystem::readRandomBytes(32))); 174 $hash = $this->hashPassword($envelope); 175 $this->setPasswordHash($hash->openEnvelope()); 176 } 177 return $this; 178 } 179 180 // To satisfy PhutilPerson. 181 public function getSex() { 182 return $this->sex; 183 } 184 185 public function getMonogram() { 186 return '@'.$this->getUsername(); 187 } 188 189 public function getTranslation() { 190 try { 191 if ($this->translation && 192 class_exists($this->translation) && 193 is_subclass_of($this->translation, 'PhabricatorTranslation')) { 194 return $this->translation; 195 } 196 } catch (PhutilMissingSymbolException $ex) { 197 return null; 198 } 199 return null; 200 } 201 202 public function isLoggedIn() { 203 return !($this->getPHID() === null); 204 } 205 206 public function save() { 207 if (!$this->getConduitCertificate()) { 208 $this->setConduitCertificate($this->generateConduitCertificate()); 209 } 210 211 if (!strlen($this->getAccountSecret())) { 212 $this->setAccountSecret(Filesystem::readRandomCharacters(64)); 213 } 214 215 $result = parent::save(); 216 217 if ($this->profile) { 218 $this->profile->save(); 219 } 220 221 $this->updateNameTokens(); 222 223 id(new PhabricatorSearchIndexer()) 224 ->queueDocumentForIndexing($this->getPHID()); 225 226 return $result; 227 } 228 229 public function attachSession(PhabricatorAuthSession $session) { 230 $this->session = $session; 231 return $this; 232 } 233 234 public function getSession() { 235 return $this->assertAttached($this->session); 236 } 237 238 public function hasSession() { 239 return ($this->session !== self::ATTACHABLE); 240 } 241 242 private function generateConduitCertificate() { 243 return Filesystem::readRandomCharacters(255); 244 } 245 246 public function comparePassword(PhutilOpaqueEnvelope $envelope) { 247 if (!strlen($envelope->openEnvelope())) { 248 return false; 249 } 250 if (!strlen($this->getPasswordHash())) { 251 return false; 252 } 253 254 return PhabricatorPasswordHasher::comparePassword( 255 $this->getPasswordHashInput($envelope), 256 new PhutilOpaqueEnvelope($this->getPasswordHash())); 257 } 258 259 private function getPasswordHashInput(PhutilOpaqueEnvelope $password) { 260 $input = 261 $this->getUsername(). 262 $password->openEnvelope(). 263 $this->getPHID(). 264 $this->getPasswordSalt(); 265 266 return new PhutilOpaqueEnvelope($input); 267 } 268 269 private function hashPassword(PhutilOpaqueEnvelope $password) { 270 $hasher = PhabricatorPasswordHasher::getBestHasher(); 271 272 $input_envelope = $this->getPasswordHashInput($password); 273 return $hasher->getPasswordHashForStorage($input_envelope); 274 } 275 276 const CSRF_CYCLE_FREQUENCY = 3600; 277 const CSRF_SALT_LENGTH = 8; 278 const CSRF_TOKEN_LENGTH = 16; 279 const CSRF_BREACH_PREFIX = 'B@'; 280 281 const EMAIL_CYCLE_FREQUENCY = 86400; 282 const EMAIL_TOKEN_LENGTH = 24; 283 284 private function getRawCSRFToken($offset = 0) { 285 return $this->generateToken( 286 time() + (self::CSRF_CYCLE_FREQUENCY * $offset), 287 self::CSRF_CYCLE_FREQUENCY, 288 PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), 289 self::CSRF_TOKEN_LENGTH); 290 } 291 292 /** 293 * @phutil-external-symbol class PhabricatorStartup 294 */ 295 public function getCSRFToken() { 296 $salt = PhabricatorStartup::getGlobal('csrf.salt'); 297 if (!$salt) { 298 $salt = Filesystem::readRandomCharacters(self::CSRF_SALT_LENGTH); 299 PhabricatorStartup::setGlobal('csrf.salt', $salt); 300 } 301 302 // Generate a token hash to mitigate BREACH attacks against SSL. See 303 // discussion in T3684. 304 $token = $this->getRawCSRFToken(); 305 $hash = PhabricatorHash::digest($token, $salt); 306 return 'B@'.$salt.substr($hash, 0, self::CSRF_TOKEN_LENGTH); 307 } 308 309 public function validateCSRFToken($token) { 310 $salt = null; 311 $version = 'plain'; 312 313 // This is a BREACH-mitigating token. See T3684. 314 $breach_prefix = self::CSRF_BREACH_PREFIX; 315 $breach_prelen = strlen($breach_prefix); 316 317 if (!strncmp($token, $breach_prefix, $breach_prelen)) { 318 $version = 'breach'; 319 $salt = substr($token, $breach_prelen, self::CSRF_SALT_LENGTH); 320 $token = substr($token, $breach_prelen + self::CSRF_SALT_LENGTH); 321 } 322 323 // When the user posts a form, we check that it contains a valid CSRF token. 324 // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept 325 // either the current token, the next token (users can submit a "future" 326 // token if you have two web frontends that have some clock skew) or any of 327 // the last 6 tokens. This means that pages are valid for up to 7 hours. 328 // There is also some Javascript which periodically refreshes the CSRF 329 // tokens on each page, so theoretically pages should be valid indefinitely. 330 // However, this code may fail to run (if the user loses their internet 331 // connection, or there's a JS problem, or they don't have JS enabled). 332 // Choosing the size of the window in which we accept old CSRF tokens is 333 // an issue of balancing concerns between security and usability. We could 334 // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to 335 // attacks using captured CSRF tokens, but it's also more likely that real 336 // users will be affected by this, e.g. if they close their laptop for an 337 // hour, open it back up, and try to submit a form before the CSRF refresh 338 // can kick in. Since the user experience of submitting a form with expired 339 // CSRF is often quite bad (you basically lose data, or it's a big pain to 340 // recover at least) and I believe we gain little additional protection 341 // by keeping the window very short (the overwhelming value here is in 342 // preventing blind attacks, and most attacks which can capture CSRF tokens 343 // can also just capture authentication information [sniffing networks] 344 // or act as the user [xss]) the 7 hour default seems like a reasonable 345 // balance. Other major platforms have much longer CSRF token lifetimes, 346 // like Rails (session duration) and Django (forever), which suggests this 347 // is a reasonable analysis. 348 $csrf_window = 6; 349 350 for ($ii = -$csrf_window; $ii <= 1; $ii++) { 351 $valid = $this->getRawCSRFToken($ii); 352 switch ($version) { 353 // TODO: We can remove this after the BREACH version has been in the 354 // wild for a while. 355 case 'plain': 356 if ($token == $valid) { 357 return true; 358 } 359 break; 360 case 'breach': 361 $digest = PhabricatorHash::digest($valid, $salt); 362 if (substr($digest, 0, self::CSRF_TOKEN_LENGTH) == $token) { 363 return true; 364 } 365 break; 366 default: 367 throw new Exception('Unknown CSRF token format!'); 368 } 369 } 370 371 return false; 372 } 373 374 private function generateToken($epoch, $frequency, $key, $len) { 375 if ($this->getPHID()) { 376 $vec = $this->getPHID().$this->getAccountSecret(); 377 } else { 378 $vec = $this->getAlternateCSRFString(); 379 } 380 381 if ($this->hasSession()) { 382 $vec = $vec.$this->getSession()->getSessionKey(); 383 } 384 385 $time_block = floor($epoch / $frequency); 386 $vec = $vec.$key.$time_block; 387 388 return substr(PhabricatorHash::digest($vec), 0, $len); 389 } 390 391 public function attachUserProfile(PhabricatorUserProfile $profile) { 392 $this->profile = $profile; 393 return $this; 394 } 395 396 public function loadUserProfile() { 397 if ($this->profile) { 398 return $this->profile; 399 } 400 401 $profile_dao = new PhabricatorUserProfile(); 402 $this->profile = $profile_dao->loadOneWhere('userPHID = %s', 403 $this->getPHID()); 404 405 if (!$this->profile) { 406 $profile_dao->setUserPHID($this->getPHID()); 407 $this->profile = $profile_dao; 408 } 409 410 return $this->profile; 411 } 412 413 public function loadPrimaryEmailAddress() { 414 $email = $this->loadPrimaryEmail(); 415 if (!$email) { 416 throw new Exception('User has no primary email address!'); 417 } 418 return $email->getAddress(); 419 } 420 421 public function loadPrimaryEmail() { 422 return $this->loadOneRelative( 423 new PhabricatorUserEmail(), 424 'userPHID', 425 'getPHID', 426 '(isPrimary = 1)'); 427 } 428 429 public function loadPreferences() { 430 if ($this->preferences) { 431 return $this->preferences; 432 } 433 434 $preferences = null; 435 if ($this->getPHID()) { 436 $preferences = id(new PhabricatorUserPreferences())->loadOneWhere( 437 'userPHID = %s', 438 $this->getPHID()); 439 } 440 441 if (!$preferences) { 442 $preferences = new PhabricatorUserPreferences(); 443 $preferences->setUserPHID($this->getPHID()); 444 445 $default_dict = array( 446 PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph', 447 PhabricatorUserPreferences::PREFERENCE_EDITOR => '', 448 PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '', 449 PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0, 450 ); 451 452 $preferences->setPreferences($default_dict); 453 } 454 455 $this->preferences = $preferences; 456 return $preferences; 457 } 458 459 public function loadEditorLink($path, $line, $callsign) { 460 $editor = $this->loadPreferences()->getPreference( 461 PhabricatorUserPreferences::PREFERENCE_EDITOR); 462 463 if (is_array($path)) { 464 $multiedit = $this->loadPreferences()->getPreference( 465 PhabricatorUserPreferences::PREFERENCE_MULTIEDIT); 466 switch ($multiedit) { 467 case '': 468 $path = implode(' ', $path); 469 break; 470 case 'disable': 471 return null; 472 } 473 } 474 475 if (!strlen($editor)) { 476 return null; 477 } 478 479 $uri = strtr($editor, array( 480 '%%' => '%', 481 '%f' => phutil_escape_uri($path), 482 '%l' => phutil_escape_uri($line), 483 '%r' => phutil_escape_uri($callsign), 484 )); 485 486 // The resulting URI must have an allowed protocol. Otherwise, we'll return 487 // a link to an error page explaining the misconfiguration. 488 489 $ok = PhabricatorHelpEditorProtocolController::hasAllowedProtocol($uri); 490 if (!$ok) { 491 return '/help/editorprotocol/'; 492 } 493 494 return (string)$uri; 495 } 496 497 public function getAlternateCSRFString() { 498 return $this->assertAttached($this->alternateCSRFString); 499 } 500 501 public function attachAlternateCSRFString($string) { 502 $this->alternateCSRFString = $string; 503 return $this; 504 } 505 506 /** 507 * Populate the nametoken table, which used to fetch typeahead results. When 508 * a user types "linc", we want to match "Abraham Lincoln" from on-demand 509 * typeahead sources. To do this, we need a separate table of name fragments. 510 */ 511 public function updateNameTokens() { 512 $table = self::NAMETOKEN_TABLE; 513 $conn_w = $this->establishConnection('w'); 514 515 $tokens = PhabricatorTypeaheadDatasource::tokenizeString( 516 $this->getUserName().' '.$this->getRealName()); 517 518 $sql = array(); 519 foreach ($tokens as $token) { 520 $sql[] = qsprintf( 521 $conn_w, 522 '(%d, %s)', 523 $this->getID(), 524 $token); 525 } 526 527 queryfx( 528 $conn_w, 529 'DELETE FROM %T WHERE userID = %d', 530 $table, 531 $this->getID()); 532 if ($sql) { 533 queryfx( 534 $conn_w, 535 'INSERT INTO %T (userID, token) VALUES %Q', 536 $table, 537 implode(', ', $sql)); 538 } 539 } 540 541 public function sendWelcomeEmail(PhabricatorUser $admin) { 542 $admin_username = $admin->getUserName(); 543 $admin_realname = $admin->getRealName(); 544 $user_username = $this->getUserName(); 545 $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); 546 547 $base_uri = PhabricatorEnv::getProductionURI('/'); 548 549 $engine = new PhabricatorAuthSessionEngine(); 550 $uri = $engine->getOneTimeLoginURI( 551 $this, 552 $this->loadPrimaryEmail(), 553 PhabricatorAuthSessionEngine::ONETIME_WELCOME); 554 555 $body = <<<EOBODY 556 Welcome to Phabricator! 557 558 {$admin_username} ({$admin_realname}) has created an account for you. 559 560 Username: {$user_username} 561 562 To login to Phabricator, follow this link and set a password: 563 564 {$uri} 565 566 After you have set a password, you can login in the future by going here: 567 568 {$base_uri} 569 570 EOBODY; 571 572 if (!$is_serious) { 573 $body .= <<<EOBODY 574 575 Love, 576 Phabricator 577 578 EOBODY; 579 } 580 581 $mail = id(new PhabricatorMetaMTAMail()) 582 ->addTos(array($this->getPHID())) 583 ->setForceDelivery(true) 584 ->setSubject('[Phabricator] Welcome to Phabricator') 585 ->setBody($body) 586 ->saveAndSend(); 587 } 588 589 public function sendUsernameChangeEmail( 590 PhabricatorUser $admin, 591 $old_username) { 592 593 $admin_username = $admin->getUserName(); 594 $admin_realname = $admin->getRealName(); 595 $new_username = $this->getUserName(); 596 597 $password_instructions = null; 598 if (PhabricatorPasswordAuthProvider::getPasswordProvider()) { 599 $engine = new PhabricatorAuthSessionEngine(); 600 $uri = $engine->getOneTimeLoginURI( 601 $this, 602 null, 603 PhabricatorAuthSessionEngine::ONETIME_USERNAME); 604 $password_instructions = <<<EOTXT 605 If you use a password to login, you'll need to reset it before you can login 606 again. You can reset your password by following this link: 607 608 {$uri} 609 610 And, of course, you'll need to use your new username to login from now on. If 611 you use OAuth to login, nothing should change. 612 613 EOTXT; 614 } 615 616 $body = <<<EOBODY 617 {$admin_username} ({$admin_realname}) has changed your Phabricator username. 618 619 Old Username: {$old_username} 620 New Username: {$new_username} 621 622 {$password_instructions} 623 EOBODY; 624 625 $mail = id(new PhabricatorMetaMTAMail()) 626 ->addTos(array($this->getPHID())) 627 ->setForceDelivery(true) 628 ->setSubject('[Phabricator] Username Changed') 629 ->setBody($body) 630 ->saveAndSend(); 631 } 632 633 public static function describeValidUsername() { 634 return pht( 635 'Usernames must contain only numbers, letters, period, underscore and '. 636 'hyphen, and can not end with a period. They must have no more than %d '. 637 'characters.', 638 new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH)); 639 } 640 641 public static function validateUsername($username) { 642 // NOTE: If you update this, make sure to update: 643 // 644 // - Remarkup rule for @mentions. 645 // - Routing rule for "/p/username/". 646 // - Unit tests, obviously. 647 // - describeValidUsername() method, above. 648 649 if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) { 650 return false; 651 } 652 653 return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username); 654 } 655 656 public static function getDefaultProfileImageURI() { 657 return celerity_get_resource_uri('/rsrc/image/avatar.png'); 658 } 659 660 public function attachStatus(PhabricatorCalendarEvent $status) { 661 $this->status = $status; 662 return $this; 663 } 664 665 public function getStatus() { 666 return $this->assertAttached($this->status); 667 } 668 669 public function hasStatus() { 670 return $this->status !== self::ATTACHABLE; 671 } 672 673 public function attachProfileImageURI($uri) { 674 $this->profileImage = $uri; 675 return $this; 676 } 677 678 public function getProfileImageURI() { 679 return $this->assertAttached($this->profileImage); 680 } 681 682 public function loadProfileImageURI() { 683 if ($this->profileImage && ($this->profileImage !== self::ATTACHABLE)) { 684 return $this->profileImage; 685 } 686 687 $src_phid = $this->getProfileImagePHID(); 688 689 if ($src_phid) { 690 // TODO: (T603) Can we get rid of this entirely and move it to 691 // PeopleQuery with attach/attachable? 692 $file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid); 693 if ($file) { 694 $this->profileImage = $file->getBestURI(); 695 return $this->profileImage; 696 } 697 } 698 699 $this->profileImage = self::getDefaultProfileImageURI(); 700 return $this->profileImage; 701 } 702 703 public function getFullName() { 704 if (strlen($this->getRealName())) { 705 return $this->getUsername().' ('.$this->getRealName().')'; 706 } else { 707 return $this->getUsername(); 708 } 709 } 710 711 public function __toString() { 712 return $this->getUsername(); 713 } 714 715 public static function loadOneWithEmailAddress($address) { 716 $email = id(new PhabricatorUserEmail())->loadOneWhere( 717 'address = %s', 718 $address); 719 if (!$email) { 720 return null; 721 } 722 return id(new PhabricatorUser())->loadOneWhere( 723 'phid = %s', 724 $email->getUserPHID()); 725 } 726 727 /* -( Multi-Factor Authentication )---------------------------------------- */ 728 729 730 /** 731 * Update the flag storing this user's enrollment in multi-factor auth. 732 * 733 * With certain settings, we need to check if a user has MFA on every page, 734 * so we cache MFA enrollment on the user object for performance. Calling this 735 * method synchronizes the cache by examining enrollment records. After 736 * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if 737 * the user is enrolled. 738 * 739 * This method should be called after any changes are made to a given user's 740 * multi-factor configuration. 741 * 742 * @return void 743 * @task factors 744 */ 745 public function updateMultiFactorEnrollment() { 746 $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 747 'userPHID = %s', 748 $this->getPHID()); 749 750 $enrolled = count($factors) ? 1 : 0; 751 if ($enrolled !== $this->isEnrolledInMultiFactor) { 752 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 753 queryfx( 754 $this->establishConnection('w'), 755 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d', 756 $this->getTableName(), 757 $enrolled, 758 $this->getID()); 759 unset($unguarded); 760 761 $this->isEnrolledInMultiFactor = $enrolled; 762 } 763 } 764 765 766 /** 767 * Check if the user is enrolled in multi-factor authentication. 768 * 769 * Enrolled users have one or more multi-factor authentication sources 770 * attached to their account. For performance, this value is cached. You 771 * can use @{method:updateMultiFactorEnrollment} to update the cache. 772 * 773 * @return bool True if the user is enrolled. 774 * @task factors 775 */ 776 public function getIsEnrolledInMultiFactor() { 777 return $this->isEnrolledInMultiFactor; 778 } 779 780 781 /* -( Omnipotence )-------------------------------------------------------- */ 782 783 784 /** 785 * Returns true if this user is omnipotent. Omnipotent users bypass all policy 786 * checks. 787 * 788 * @return bool True if the user bypasses policy checks. 789 */ 790 public function isOmnipotent() { 791 return $this->omnipotent; 792 } 793 794 795 /** 796 * Get an omnipotent user object for use in contexts where there is no acting 797 * user, notably daemons. 798 * 799 * @return PhabricatorUser An omnipotent user. 800 */ 801 public static function getOmnipotentUser() { 802 static $user = null; 803 if (!$user) { 804 $user = new PhabricatorUser(); 805 $user->omnipotent = true; 806 $user->makeEphemeral(); 807 } 808 return $user; 809 } 810 811 812 /* -( PhabricatorPolicyInterface )----------------------------------------- */ 813 814 815 public function getCapabilities() { 816 return array( 817 PhabricatorPolicyCapability::CAN_VIEW, 818 PhabricatorPolicyCapability::CAN_EDIT, 819 ); 820 } 821 822 public function getPolicy($capability) { 823 switch ($capability) { 824 case PhabricatorPolicyCapability::CAN_VIEW: 825 return PhabricatorPolicies::POLICY_PUBLIC; 826 case PhabricatorPolicyCapability::CAN_EDIT: 827 if ($this->getIsSystemAgent()) { 828 return PhabricatorPolicies::POLICY_ADMIN; 829 } else { 830 return PhabricatorPolicies::POLICY_NOONE; 831 } 832 } 833 } 834 835 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 836 return $this->getPHID() && ($viewer->getPHID() === $this->getPHID()); 837 } 838 839 public function describeAutomaticCapability($capability) { 840 switch ($capability) { 841 case PhabricatorPolicyCapability::CAN_EDIT: 842 return pht('Only you can edit your information.'); 843 default: 844 return null; 845 } 846 } 847 848 849 /* -( PhabricatorCustomFieldInterface )------------------------------------ */ 850 851 852 public function getCustomFieldSpecificationForRole($role) { 853 return PhabricatorEnv::getEnvConfig('user.fields'); 854 } 855 856 public function getCustomFieldBaseClass() { 857 return 'PhabricatorUserCustomField'; 858 } 859 860 public function getCustomFields() { 861 return $this->assertAttached($this->customFields); 862 } 863 864 public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { 865 $this->customFields = $fields; 866 return $this; 867 } 868 869 870 /* -( PhabricatorDestructibleInterface )----------------------------------- */ 871 872 873 public function destroyObjectPermanently( 874 PhabricatorDestructionEngine $engine) { 875 876 $this->openTransaction(); 877 $this->delete(); 878 879 $externals = id(new PhabricatorExternalAccount())->loadAllWhere( 880 'userPHID = %s', 881 $this->getPHID()); 882 foreach ($externals as $external) { 883 $external->delete(); 884 } 885 886 $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 887 'userPHID = %s', 888 $this->getPHID()); 889 foreach ($prefs as $pref) { 890 $pref->delete(); 891 } 892 893 $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 894 'userPHID = %s', 895 $this->getPHID()); 896 foreach ($profiles as $profile) { 897 $profile->delete(); 898 } 899 900 $keys = id(new PhabricatorAuthSSHKey())->loadAllWhere( 901 'objectPHID = %s', 902 $this->getPHID()); 903 foreach ($keys as $key) { 904 $key->delete(); 905 } 906 907 $emails = id(new PhabricatorUserEmail())->loadAllWhere( 908 'userPHID = %s', 909 $this->getPHID()); 910 foreach ($emails as $email) { 911 $email->delete(); 912 } 913 914 $sessions = id(new PhabricatorAuthSession())->loadAllWhere( 915 'userPHID = %s', 916 $this->getPHID()); 917 foreach ($sessions as $session) { 918 $session->delete(); 919 } 920 921 $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere( 922 'userPHID = %s', 923 $this->getPHID()); 924 foreach ($factors as $factor) { 925 $factor->delete(); 926 } 927 928 $this->saveTransaction(); 929 } 930 931 932 /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */ 933 934 935 public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) { 936 if ($viewer->getPHID() == $this->getPHID()) { 937 // If the viewer is managing their own keys, take them to the normal 938 // panel. 939 return '/settings/panel/ssh/'; 940 } else { 941 // Otherwise, take them to the administrative panel for this user. 942 return '/settings/'.$this->getID().'/panel/ssh/'; 943 } 944 } 945 946 public function getSSHKeyDefaultName() { 947 return 'id_rsa_phabricator'; 948 } 949 950 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |