[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/people/storage/ -> PhabricatorUser.php (source)

   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  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1