Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/people/storage/PhabricatorUser.php
12256 views
1
<?php
2
3
/**
4
* @task availability Availability
5
* @task image-cache Profile Image Cache
6
* @task factors Multi-Factor Authentication
7
* @task handles Managing Handles
8
* @task settings Settings
9
* @task cache User Cache
10
*/
11
final class PhabricatorUser
12
extends PhabricatorUserDAO
13
implements
14
PhutilPerson,
15
PhabricatorPolicyInterface,
16
PhabricatorCustomFieldInterface,
17
PhabricatorDestructibleInterface,
18
PhabricatorSSHPublicKeyInterface,
19
PhabricatorFlaggableInterface,
20
PhabricatorApplicationTransactionInterface,
21
PhabricatorFulltextInterface,
22
PhabricatorFerretInterface,
23
PhabricatorConduitResultInterface,
24
PhabricatorAuthPasswordHashInterface {
25
26
const SESSION_TABLE = 'phabricator_session';
27
const NAMETOKEN_TABLE = 'user_nametoken';
28
const MAXIMUM_USERNAME_LENGTH = 64;
29
30
protected $userName;
31
protected $realName;
32
protected $profileImagePHID;
33
protected $defaultProfileImagePHID;
34
protected $defaultProfileImageVersion;
35
protected $availabilityCache;
36
protected $availabilityCacheTTL;
37
38
protected $conduitCertificate;
39
40
protected $isSystemAgent = 0;
41
protected $isMailingList = 0;
42
protected $isAdmin = 0;
43
protected $isDisabled = 0;
44
protected $isEmailVerified = 0;
45
protected $isApproved = 0;
46
protected $isEnrolledInMultiFactor = 0;
47
48
protected $accountSecret;
49
50
private $profile = null;
51
private $availability = self::ATTACHABLE;
52
private $preferences = null;
53
private $omnipotent = false;
54
private $customFields = self::ATTACHABLE;
55
private $badgePHIDs = self::ATTACHABLE;
56
57
private $alternateCSRFString = self::ATTACHABLE;
58
private $session = self::ATTACHABLE;
59
private $rawCacheData = array();
60
private $usableCacheData = array();
61
62
private $handlePool;
63
private $csrfSalt;
64
65
private $settingCacheKeys = array();
66
private $settingCache = array();
67
private $allowInlineCacheGeneration;
68
private $conduitClusterToken = self::ATTACHABLE;
69
70
protected function readField($field) {
71
switch ($field) {
72
// Make sure these return booleans.
73
case 'isAdmin':
74
return (bool)$this->isAdmin;
75
case 'isDisabled':
76
return (bool)$this->isDisabled;
77
case 'isSystemAgent':
78
return (bool)$this->isSystemAgent;
79
case 'isMailingList':
80
return (bool)$this->isMailingList;
81
case 'isEmailVerified':
82
return (bool)$this->isEmailVerified;
83
case 'isApproved':
84
return (bool)$this->isApproved;
85
default:
86
return parent::readField($field);
87
}
88
}
89
90
91
/**
92
* Is this a live account which has passed required approvals? Returns true
93
* if this is an enabled, verified (if required), approved (if required)
94
* account, and false otherwise.
95
*
96
* @return bool True if this is a standard, usable account.
97
*/
98
public function isUserActivated() {
99
if (!$this->isLoggedIn()) {
100
return false;
101
}
102
103
if ($this->isOmnipotent()) {
104
return true;
105
}
106
107
if ($this->getIsDisabled()) {
108
return false;
109
}
110
111
if (!$this->getIsApproved()) {
112
return false;
113
}
114
115
if (PhabricatorUserEmail::isEmailVerificationRequired()) {
116
if (!$this->getIsEmailVerified()) {
117
return false;
118
}
119
}
120
121
return true;
122
}
123
124
125
/**
126
* Is this a user who we can reasonably expect to respond to requests?
127
*
128
* This is used to provide a grey "disabled/unresponsive" dot cue when
129
* rendering handles and tags, so it isn't a surprise if you get ignored
130
* when you ask things of users who will not receive notifications or could
131
* not respond to them (because they are disabled, unapproved, do not have
132
* verified email addresses, etc).
133
*
134
* @return bool True if this user can receive and respond to requests from
135
* other humans.
136
*/
137
public function isResponsive() {
138
if (!$this->isUserActivated()) {
139
return false;
140
}
141
142
if (!$this->getIsEmailVerified()) {
143
return false;
144
}
145
146
return true;
147
}
148
149
150
public function canEstablishWebSessions() {
151
if ($this->getIsMailingList()) {
152
return false;
153
}
154
155
if ($this->getIsSystemAgent()) {
156
return false;
157
}
158
159
return true;
160
}
161
162
public function canEstablishAPISessions() {
163
if ($this->getIsDisabled()) {
164
return false;
165
}
166
167
// Intracluster requests are permitted even if the user is logged out:
168
// in particular, public users are allowed to issue intracluster requests
169
// when browsing Diffusion.
170
if (PhabricatorEnv::isClusterRemoteAddress()) {
171
if (!$this->isLoggedIn()) {
172
return true;
173
}
174
}
175
176
if (!$this->isUserActivated()) {
177
return false;
178
}
179
180
if ($this->getIsMailingList()) {
181
return false;
182
}
183
184
return true;
185
}
186
187
public function canEstablishSSHSessions() {
188
if (!$this->isUserActivated()) {
189
return false;
190
}
191
192
if ($this->getIsMailingList()) {
193
return false;
194
}
195
196
return true;
197
}
198
199
/**
200
* Returns `true` if this is a standard user who is logged in. Returns `false`
201
* for logged out, anonymous, or external users.
202
*
203
* @return bool `true` if the user is a standard user who is logged in with
204
* a normal session.
205
*/
206
public function getIsStandardUser() {
207
$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
208
return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
209
}
210
211
protected function getConfiguration() {
212
return array(
213
self::CONFIG_AUX_PHID => true,
214
self::CONFIG_COLUMN_SCHEMA => array(
215
'userName' => 'sort64',
216
'realName' => 'text128',
217
'profileImagePHID' => 'phid?',
218
'conduitCertificate' => 'text255',
219
'isSystemAgent' => 'bool',
220
'isMailingList' => 'bool',
221
'isDisabled' => 'bool',
222
'isAdmin' => 'bool',
223
'isEmailVerified' => 'uint32',
224
'isApproved' => 'uint32',
225
'accountSecret' => 'bytes64',
226
'isEnrolledInMultiFactor' => 'bool',
227
'availabilityCache' => 'text255?',
228
'availabilityCacheTTL' => 'uint32?',
229
'defaultProfileImagePHID' => 'phid?',
230
'defaultProfileImageVersion' => 'text64?',
231
),
232
self::CONFIG_KEY_SCHEMA => array(
233
'key_phid' => null,
234
'phid' => array(
235
'columns' => array('phid'),
236
'unique' => true,
237
),
238
'userName' => array(
239
'columns' => array('userName'),
240
'unique' => true,
241
),
242
'realName' => array(
243
'columns' => array('realName'),
244
),
245
'key_approved' => array(
246
'columns' => array('isApproved'),
247
),
248
),
249
self::CONFIG_NO_MUTATE => array(
250
'availabilityCache' => true,
251
'availabilityCacheTTL' => true,
252
),
253
) + parent::getConfiguration();
254
}
255
256
public function generatePHID() {
257
return PhabricatorPHID::generateNewPHID(
258
PhabricatorPeopleUserPHIDType::TYPECONST);
259
}
260
261
public function getMonogram() {
262
return '@'.$this->getUsername();
263
}
264
265
public function isLoggedIn() {
266
return !($this->getPHID() === null);
267
}
268
269
public function saveWithoutIndex() {
270
return parent::save();
271
}
272
273
public function save() {
274
if (!$this->getConduitCertificate()) {
275
$this->setConduitCertificate($this->generateConduitCertificate());
276
}
277
278
$secret = $this->getAccountSecret();
279
if (($secret === null) || !strlen($secret)) {
280
$this->setAccountSecret(Filesystem::readRandomCharacters(64));
281
}
282
283
$result = $this->saveWithoutIndex();
284
285
if ($this->profile) {
286
$this->profile->save();
287
}
288
289
$this->updateNameTokens();
290
291
PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
292
293
return $result;
294
}
295
296
public function attachSession(PhabricatorAuthSession $session) {
297
$this->session = $session;
298
return $this;
299
}
300
301
public function getSession() {
302
return $this->assertAttached($this->session);
303
}
304
305
public function hasSession() {
306
return ($this->session !== self::ATTACHABLE);
307
}
308
309
public function hasHighSecuritySession() {
310
if (!$this->hasSession()) {
311
return false;
312
}
313
314
return $this->getSession()->isHighSecuritySession();
315
}
316
317
private function generateConduitCertificate() {
318
return Filesystem::readRandomCharacters(255);
319
}
320
321
const EMAIL_CYCLE_FREQUENCY = 86400;
322
const EMAIL_TOKEN_LENGTH = 24;
323
324
public function getUserProfile() {
325
return $this->assertAttached($this->profile);
326
}
327
328
public function attachUserProfile(PhabricatorUserProfile $profile) {
329
$this->profile = $profile;
330
return $this;
331
}
332
333
public function loadUserProfile() {
334
if ($this->profile) {
335
return $this->profile;
336
}
337
338
$profile_dao = new PhabricatorUserProfile();
339
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
340
$this->getPHID());
341
342
if (!$this->profile) {
343
$this->profile = PhabricatorUserProfile::initializeNewProfile($this);
344
}
345
346
return $this->profile;
347
}
348
349
public function loadPrimaryEmailAddress() {
350
$email = $this->loadPrimaryEmail();
351
if (!$email) {
352
throw new Exception(pht('User has no primary email address!'));
353
}
354
return $email->getAddress();
355
}
356
357
public function loadPrimaryEmail() {
358
return id(new PhabricatorUserEmail())->loadOneWhere(
359
'userPHID = %s AND isPrimary = 1',
360
$this->getPHID());
361
}
362
363
364
/* -( Settings )----------------------------------------------------------- */
365
366
367
public function getUserSetting($key) {
368
// NOTE: We store available keys and cached values separately to make it
369
// faster to check for `null` in the cache, which is common.
370
if (isset($this->settingCacheKeys[$key])) {
371
return $this->settingCache[$key];
372
}
373
374
$settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
375
if ($this->getPHID()) {
376
$settings = $this->requireCacheData($settings_key);
377
} else {
378
$settings = $this->loadGlobalSettings();
379
}
380
381
if (array_key_exists($key, $settings)) {
382
$value = $settings[$key];
383
return $this->writeUserSettingCache($key, $value);
384
}
385
386
$cache = PhabricatorCaches::getRuntimeCache();
387
$cache_key = "settings.defaults({$key})";
388
$cache_map = $cache->getKeys(array($cache_key));
389
390
if ($cache_map) {
391
$value = $cache_map[$cache_key];
392
} else {
393
$defaults = PhabricatorSetting::getAllSettings();
394
if (isset($defaults[$key])) {
395
$value = id(clone $defaults[$key])
396
->setViewer($this)
397
->getSettingDefaultValue();
398
} else {
399
$value = null;
400
}
401
402
$cache->setKey($cache_key, $value);
403
}
404
405
return $this->writeUserSettingCache($key, $value);
406
}
407
408
409
/**
410
* Test if a given setting is set to a particular value.
411
*
412
* @param const Setting key.
413
* @param wild Value to compare.
414
* @return bool True if the setting has the specified value.
415
* @task settings
416
*/
417
public function compareUserSetting($key, $value) {
418
$actual = $this->getUserSetting($key);
419
return ($actual == $value);
420
}
421
422
private function writeUserSettingCache($key, $value) {
423
$this->settingCacheKeys[$key] = true;
424
$this->settingCache[$key] = $value;
425
return $value;
426
}
427
428
public function getTranslation() {
429
return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY);
430
}
431
432
public function getTimezoneIdentifier() {
433
return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);
434
}
435
436
public static function getGlobalSettingsCacheKey() {
437
return 'user.settings.globals.v1';
438
}
439
440
private function loadGlobalSettings() {
441
$cache_key = self::getGlobalSettingsCacheKey();
442
$cache = PhabricatorCaches::getMutableStructureCache();
443
444
$settings = $cache->getKey($cache_key);
445
if (!$settings) {
446
$preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
447
$settings = $preferences->getPreferences();
448
$cache->setKey($cache_key, $settings);
449
}
450
451
return $settings;
452
}
453
454
455
/**
456
* Override the user's timezone identifier.
457
*
458
* This is primarily useful for unit tests.
459
*
460
* @param string New timezone identifier.
461
* @return this
462
* @task settings
463
*/
464
public function overrideTimezoneIdentifier($identifier) {
465
$timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;
466
$this->settingCacheKeys[$timezone_key] = true;
467
$this->settingCache[$timezone_key] = $identifier;
468
return $this;
469
}
470
471
public function getGender() {
472
return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY);
473
}
474
475
/**
476
* Populate the nametoken table, which used to fetch typeahead results. When
477
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
478
* typeahead sources. To do this, we need a separate table of name fragments.
479
*/
480
public function updateNameTokens() {
481
$table = self::NAMETOKEN_TABLE;
482
$conn_w = $this->establishConnection('w');
483
484
$tokens = PhabricatorTypeaheadDatasource::tokenizeString(
485
$this->getUserName().' '.$this->getRealName());
486
487
$sql = array();
488
foreach ($tokens as $token) {
489
$sql[] = qsprintf(
490
$conn_w,
491
'(%d, %s)',
492
$this->getID(),
493
$token);
494
}
495
496
queryfx(
497
$conn_w,
498
'DELETE FROM %T WHERE userID = %d',
499
$table,
500
$this->getID());
501
if ($sql) {
502
queryfx(
503
$conn_w,
504
'INSERT INTO %T (userID, token) VALUES %LQ',
505
$table,
506
$sql);
507
}
508
}
509
510
public static function describeValidUsername() {
511
return pht(
512
'Usernames must contain only numbers, letters, period, underscore, and '.
513
'hyphen, and can not end with a period. They must have no more than %d '.
514
'characters.',
515
new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
516
}
517
518
public static function validateUsername($username) {
519
// NOTE: If you update this, make sure to update:
520
//
521
// - Remarkup rule for @mentions.
522
// - Routing rule for "/p/username/".
523
// - Unit tests, obviously.
524
// - describeValidUsername() method, above.
525
526
if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
527
return false;
528
}
529
530
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
531
}
532
533
public static function getDefaultProfileImageURI() {
534
return celerity_get_resource_uri('/rsrc/image/avatar.png');
535
}
536
537
public function getProfileImageURI() {
538
$uri_key = PhabricatorUserProfileImageCacheType::KEY_URI;
539
return $this->requireCacheData($uri_key);
540
}
541
542
public function getUnreadNotificationCount() {
543
$notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT;
544
return $this->requireCacheData($notification_key);
545
}
546
547
public function getUnreadMessageCount() {
548
$message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT;
549
return $this->requireCacheData($message_key);
550
}
551
552
public function getRecentBadgeAwards() {
553
$badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
554
return $this->requireCacheData($badges_key);
555
}
556
557
public function getFullName() {
558
if (strlen($this->getRealName())) {
559
return $this->getUsername().' ('.$this->getRealName().')';
560
} else {
561
return $this->getUsername();
562
}
563
}
564
565
public function getTimeZone() {
566
return new DateTimeZone($this->getTimezoneIdentifier());
567
}
568
569
public function getTimeZoneOffset() {
570
$timezone = $this->getTimeZone();
571
$now = new DateTime('@'.PhabricatorTime::getNow());
572
$offset = $timezone->getOffset($now);
573
574
// Javascript offsets are in minutes and have the opposite sign.
575
$offset = -(int)($offset / 60);
576
577
return $offset;
578
}
579
580
public function getTimeZoneOffsetInHours() {
581
$offset = $this->getTimeZoneOffset();
582
$offset = (int)round($offset / 60);
583
$offset = -$offset;
584
585
return $offset;
586
}
587
588
public function formatShortDateTime($when, $now = null) {
589
if ($now === null) {
590
$now = PhabricatorTime::getNow();
591
}
592
593
try {
594
$when = new DateTime('@'.$when);
595
$now = new DateTime('@'.$now);
596
} catch (Exception $ex) {
597
return null;
598
}
599
600
$zone = $this->getTimeZone();
601
602
$when->setTimeZone($zone);
603
$now->setTimeZone($zone);
604
605
if ($when->format('Y') !== $now->format('Y')) {
606
// Different year, so show "Feb 31 2075".
607
$format = 'M j Y';
608
} else if ($when->format('Ymd') !== $now->format('Ymd')) {
609
// Same year but different month and day, so show "Feb 31".
610
$format = 'M j';
611
} else {
612
// Same year, month and day so show a time of day.
613
$pref_time = PhabricatorTimeFormatSetting::SETTINGKEY;
614
$format = $this->getUserSetting($pref_time);
615
}
616
617
return $when->format($format);
618
}
619
620
public function __toString() {
621
return $this->getUsername();
622
}
623
624
public static function loadOneWithEmailAddress($address) {
625
$email = id(new PhabricatorUserEmail())->loadOneWhere(
626
'address = %s',
627
$address);
628
if (!$email) {
629
return null;
630
}
631
return id(new PhabricatorUser())->loadOneWhere(
632
'phid = %s',
633
$email->getUserPHID());
634
}
635
636
public function getDefaultSpacePHID() {
637
// TODO: We might let the user switch which space they're "in" later on;
638
// for now just use the global space if one exists.
639
640
// If the viewer has access to the default space, use that.
641
$spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
642
foreach ($spaces as $space) {
643
if ($space->getIsDefaultNamespace()) {
644
return $space->getPHID();
645
}
646
}
647
648
// Otherwise, use the space with the lowest ID that they have access to.
649
// This just tends to keep the default stable and predictable over time,
650
// so adding a new space won't change behavior for users.
651
if ($spaces) {
652
$spaces = msort($spaces, 'getID');
653
return head($spaces)->getPHID();
654
}
655
656
return null;
657
}
658
659
660
public function hasConduitClusterToken() {
661
return ($this->conduitClusterToken !== self::ATTACHABLE);
662
}
663
664
public function attachConduitClusterToken(PhabricatorConduitToken $token) {
665
$this->conduitClusterToken = $token;
666
return $this;
667
}
668
669
public function getConduitClusterToken() {
670
return $this->assertAttached($this->conduitClusterToken);
671
}
672
673
674
/* -( Availability )------------------------------------------------------- */
675
676
677
/**
678
* @task availability
679
*/
680
public function attachAvailability(array $availability) {
681
$this->availability = $availability;
682
return $this;
683
}
684
685
686
/**
687
* Get the timestamp the user is away until, if they are currently away.
688
*
689
* @return int|null Epoch timestamp, or `null` if the user is not away.
690
* @task availability
691
*/
692
public function getAwayUntil() {
693
$availability = $this->availability;
694
695
$this->assertAttached($availability);
696
if (!$availability) {
697
return null;
698
}
699
700
return idx($availability, 'until');
701
}
702
703
704
public function getDisplayAvailability() {
705
$availability = $this->availability;
706
707
$this->assertAttached($availability);
708
if (!$availability) {
709
return null;
710
}
711
712
$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
713
714
return idx($availability, 'availability', $busy);
715
}
716
717
718
public function getAvailabilityEventPHID() {
719
$availability = $this->availability;
720
721
$this->assertAttached($availability);
722
if (!$availability) {
723
return null;
724
}
725
726
return idx($availability, 'eventPHID');
727
}
728
729
730
/**
731
* Get cached availability, if present.
732
*
733
* @return wild|null Cache data, or null if no cache is available.
734
* @task availability
735
*/
736
public function getAvailabilityCache() {
737
$now = PhabricatorTime::getNow();
738
if ($this->availabilityCacheTTL <= $now) {
739
return null;
740
}
741
742
try {
743
return phutil_json_decode($this->availabilityCache);
744
} catch (Exception $ex) {
745
return null;
746
}
747
}
748
749
750
/**
751
* Write to the availability cache.
752
*
753
* @param wild Availability cache data.
754
* @param int|null Cache TTL.
755
* @return this
756
* @task availability
757
*/
758
public function writeAvailabilityCache(array $availability, $ttl) {
759
if (PhabricatorEnv::isReadOnly()) {
760
return $this;
761
}
762
763
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
764
queryfx(
765
$this->establishConnection('w'),
766
'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
767
WHERE id = %d',
768
$this->getTableName(),
769
phutil_json_encode($availability),
770
$ttl,
771
$this->getID());
772
unset($unguarded);
773
774
return $this;
775
}
776
777
778
/* -( Multi-Factor Authentication )---------------------------------------- */
779
780
781
/**
782
* Update the flag storing this user's enrollment in multi-factor auth.
783
*
784
* With certain settings, we need to check if a user has MFA on every page,
785
* so we cache MFA enrollment on the user object for performance. Calling this
786
* method synchronizes the cache by examining enrollment records. After
787
* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
788
* the user is enrolled.
789
*
790
* This method should be called after any changes are made to a given user's
791
* multi-factor configuration.
792
*
793
* @return void
794
* @task factors
795
*/
796
public function updateMultiFactorEnrollment() {
797
$factors = id(new PhabricatorAuthFactorConfigQuery())
798
->setViewer($this)
799
->withUserPHIDs(array($this->getPHID()))
800
->withFactorProviderStatuses(
801
array(
802
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
803
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
804
))
805
->execute();
806
807
$enrolled = count($factors) ? 1 : 0;
808
if ($enrolled !== $this->isEnrolledInMultiFactor) {
809
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
810
queryfx(
811
$this->establishConnection('w'),
812
'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
813
$this->getTableName(),
814
$enrolled,
815
$this->getID());
816
unset($unguarded);
817
818
$this->isEnrolledInMultiFactor = $enrolled;
819
}
820
}
821
822
823
/**
824
* Check if the user is enrolled in multi-factor authentication.
825
*
826
* Enrolled users have one or more multi-factor authentication sources
827
* attached to their account. For performance, this value is cached. You
828
* can use @{method:updateMultiFactorEnrollment} to update the cache.
829
*
830
* @return bool True if the user is enrolled.
831
* @task factors
832
*/
833
public function getIsEnrolledInMultiFactor() {
834
return $this->isEnrolledInMultiFactor;
835
}
836
837
838
/* -( Omnipotence )-------------------------------------------------------- */
839
840
841
/**
842
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
843
* checks.
844
*
845
* @return bool True if the user bypasses policy checks.
846
*/
847
public function isOmnipotent() {
848
return $this->omnipotent;
849
}
850
851
852
/**
853
* Get an omnipotent user object for use in contexts where there is no acting
854
* user, notably daemons.
855
*
856
* @return PhabricatorUser An omnipotent user.
857
*/
858
public static function getOmnipotentUser() {
859
static $user = null;
860
if (!$user) {
861
$user = new PhabricatorUser();
862
$user->omnipotent = true;
863
$user->makeEphemeral();
864
}
865
return $user;
866
}
867
868
869
/**
870
* Get a scalar string identifying this user.
871
*
872
* This is similar to using the PHID, but distinguishes between omnipotent
873
* and public users explicitly. This allows safe construction of cache keys
874
* or cache buckets which do not conflate public and omnipotent users.
875
*
876
* @return string Scalar identifier.
877
*/
878
public function getCacheFragment() {
879
if ($this->isOmnipotent()) {
880
return 'u.omnipotent';
881
}
882
883
$phid = $this->getPHID();
884
if ($phid) {
885
return 'u.'.$phid;
886
}
887
888
return 'u.public';
889
}
890
891
892
/* -( Managing Handles )--------------------------------------------------- */
893
894
895
/**
896
* Get a @{class:PhabricatorHandleList} which benefits from this viewer's
897
* internal handle pool.
898
*
899
* @param list<phid> List of PHIDs to load.
900
* @return PhabricatorHandleList Handle list object.
901
* @task handle
902
*/
903
public function loadHandles(array $phids) {
904
if ($this->handlePool === null) {
905
$this->handlePool = id(new PhabricatorHandlePool())
906
->setViewer($this);
907
}
908
909
return $this->handlePool->newHandleList($phids);
910
}
911
912
913
/**
914
* Get a @{class:PHUIHandleView} for a single handle.
915
*
916
* This benefits from the viewer's internal handle pool.
917
*
918
* @param phid PHID to render a handle for.
919
* @return PHUIHandleView View of the handle.
920
* @task handle
921
*/
922
public function renderHandle($phid) {
923
return $this->loadHandles(array($phid))->renderHandle($phid);
924
}
925
926
927
/**
928
* Get a @{class:PHUIHandleListView} for a list of handles.
929
*
930
* This benefits from the viewer's internal handle pool.
931
*
932
* @param list<phid> List of PHIDs to render.
933
* @return PHUIHandleListView View of the handles.
934
* @task handle
935
*/
936
public function renderHandleList(array $phids) {
937
return $this->loadHandles($phids)->renderList();
938
}
939
940
public function attachBadgePHIDs(array $phids) {
941
$this->badgePHIDs = $phids;
942
return $this;
943
}
944
945
public function getBadgePHIDs() {
946
return $this->assertAttached($this->badgePHIDs);
947
}
948
949
/* -( CSRF )--------------------------------------------------------------- */
950
951
952
public function getCSRFToken() {
953
if ($this->isOmnipotent()) {
954
// We may end up here when called from the daemons. The omnipotent user
955
// has no meaningful CSRF token, so just return `null`.
956
return null;
957
}
958
959
return $this->newCSRFEngine()
960
->newToken();
961
}
962
963
public function validateCSRFToken($token) {
964
return $this->newCSRFengine()
965
->isValidToken($token);
966
}
967
968
public function getAlternateCSRFString() {
969
return $this->assertAttached($this->alternateCSRFString);
970
}
971
972
public function attachAlternateCSRFString($string) {
973
$this->alternateCSRFString = $string;
974
return $this;
975
}
976
977
private function newCSRFEngine() {
978
if ($this->getPHID()) {
979
$vec = $this->getPHID().$this->getAccountSecret();
980
} else {
981
$vec = $this->getAlternateCSRFString();
982
}
983
984
if ($this->hasSession()) {
985
$vec = $vec.$this->getSession()->getSessionKey();
986
}
987
988
$engine = new PhabricatorAuthCSRFEngine();
989
990
if ($this->csrfSalt === null) {
991
$this->csrfSalt = $engine->newSalt();
992
}
993
994
$engine
995
->setSalt($this->csrfSalt)
996
->setSecret(new PhutilOpaqueEnvelope($vec));
997
998
return $engine;
999
}
1000
1001
1002
/* -( PhabricatorPolicyInterface )----------------------------------------- */
1003
1004
1005
public function getCapabilities() {
1006
return array(
1007
PhabricatorPolicyCapability::CAN_VIEW,
1008
PhabricatorPolicyCapability::CAN_EDIT,
1009
);
1010
}
1011
1012
public function getPolicy($capability) {
1013
switch ($capability) {
1014
case PhabricatorPolicyCapability::CAN_VIEW:
1015
return PhabricatorPolicies::POLICY_PUBLIC;
1016
case PhabricatorPolicyCapability::CAN_EDIT:
1017
if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
1018
return PhabricatorPolicies::POLICY_ADMIN;
1019
} else {
1020
return PhabricatorPolicies::POLICY_NOONE;
1021
}
1022
}
1023
}
1024
1025
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
1026
return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
1027
}
1028
1029
public function describeAutomaticCapability($capability) {
1030
switch ($capability) {
1031
case PhabricatorPolicyCapability::CAN_EDIT:
1032
return pht('Only you can edit your information.');
1033
default:
1034
return null;
1035
}
1036
}
1037
1038
1039
/* -( PhabricatorCustomFieldInterface )------------------------------------ */
1040
1041
1042
public function getCustomFieldSpecificationForRole($role) {
1043
return PhabricatorEnv::getEnvConfig('user.fields');
1044
}
1045
1046
public function getCustomFieldBaseClass() {
1047
return 'PhabricatorUserCustomField';
1048
}
1049
1050
public function getCustomFields() {
1051
return $this->assertAttached($this->customFields);
1052
}
1053
1054
public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
1055
$this->customFields = $fields;
1056
return $this;
1057
}
1058
1059
1060
/* -( PhabricatorDestructibleInterface )----------------------------------- */
1061
1062
1063
public function destroyObjectPermanently(
1064
PhabricatorDestructionEngine $engine) {
1065
1066
$viewer = $engine->getViewer();
1067
1068
$this->openTransaction();
1069
$this->delete();
1070
1071
$externals = id(new PhabricatorExternalAccountQuery())
1072
->setViewer($viewer)
1073
->withUserPHIDs(array($this->getPHID()))
1074
->newIterator();
1075
foreach ($externals as $external) {
1076
$engine->destroyObject($external);
1077
}
1078
1079
$prefs = id(new PhabricatorUserPreferencesQuery())
1080
->setViewer($viewer)
1081
->withUsers(array($this))
1082
->execute();
1083
foreach ($prefs as $pref) {
1084
$engine->destroyObject($pref);
1085
}
1086
1087
$profiles = id(new PhabricatorUserProfile())->loadAllWhere(
1088
'userPHID = %s',
1089
$this->getPHID());
1090
foreach ($profiles as $profile) {
1091
$profile->delete();
1092
}
1093
1094
$keys = id(new PhabricatorAuthSSHKeyQuery())
1095
->setViewer($viewer)
1096
->withObjectPHIDs(array($this->getPHID()))
1097
->execute();
1098
foreach ($keys as $key) {
1099
$engine->destroyObject($key);
1100
}
1101
1102
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
1103
'userPHID = %s',
1104
$this->getPHID());
1105
foreach ($emails as $email) {
1106
$engine->destroyObject($email);
1107
}
1108
1109
$sessions = id(new PhabricatorAuthSession())->loadAllWhere(
1110
'userPHID = %s',
1111
$this->getPHID());
1112
foreach ($sessions as $session) {
1113
$session->delete();
1114
}
1115
1116
$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
1117
'userPHID = %s',
1118
$this->getPHID());
1119
foreach ($factors as $factor) {
1120
$factor->delete();
1121
}
1122
1123
$this->saveTransaction();
1124
}
1125
1126
1127
/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
1128
1129
1130
public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
1131
if ($viewer->getPHID() == $this->getPHID()) {
1132
// If the viewer is managing their own keys, take them to the normal
1133
// panel.
1134
return '/settings/panel/ssh/';
1135
} else {
1136
// Otherwise, take them to the administrative panel for this user.
1137
return '/settings/user/'.$this->getUsername().'/page/ssh/';
1138
}
1139
}
1140
1141
public function getSSHKeyDefaultName() {
1142
return 'id_rsa_phabricator';
1143
}
1144
1145
public function getSSHKeyNotifyPHIDs() {
1146
return array(
1147
$this->getPHID(),
1148
);
1149
}
1150
1151
1152
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
1153
1154
1155
public function getApplicationTransactionEditor() {
1156
return new PhabricatorUserTransactionEditor();
1157
}
1158
1159
public function getApplicationTransactionTemplate() {
1160
return new PhabricatorUserTransaction();
1161
}
1162
1163
1164
/* -( PhabricatorFulltextInterface )--------------------------------------- */
1165
1166
1167
public function newFulltextEngine() {
1168
return new PhabricatorUserFulltextEngine();
1169
}
1170
1171
1172
/* -( PhabricatorFerretInterface )----------------------------------------- */
1173
1174
1175
public function newFerretEngine() {
1176
return new PhabricatorUserFerretEngine();
1177
}
1178
1179
1180
/* -( PhabricatorConduitResultInterface )---------------------------------- */
1181
1182
1183
public function getFieldSpecificationsForConduit() {
1184
return array(
1185
id(new PhabricatorConduitSearchFieldSpecification())
1186
->setKey('username')
1187
->setType('string')
1188
->setDescription(pht("The user's username.")),
1189
id(new PhabricatorConduitSearchFieldSpecification())
1190
->setKey('realName')
1191
->setType('string')
1192
->setDescription(pht("The user's real name.")),
1193
id(new PhabricatorConduitSearchFieldSpecification())
1194
->setKey('roles')
1195
->setType('list<string>')
1196
->setDescription(pht('List of account roles.')),
1197
);
1198
}
1199
1200
public function getFieldValuesForConduit() {
1201
$roles = array();
1202
1203
if ($this->getIsDisabled()) {
1204
$roles[] = 'disabled';
1205
}
1206
1207
if ($this->getIsSystemAgent()) {
1208
$roles[] = 'bot';
1209
}
1210
1211
if ($this->getIsMailingList()) {
1212
$roles[] = 'list';
1213
}
1214
1215
if ($this->getIsAdmin()) {
1216
$roles[] = 'admin';
1217
}
1218
1219
if ($this->getIsEmailVerified()) {
1220
$roles[] = 'verified';
1221
}
1222
1223
if ($this->getIsApproved()) {
1224
$roles[] = 'approved';
1225
}
1226
1227
if ($this->isUserActivated()) {
1228
$roles[] = 'activated';
1229
}
1230
1231
return array(
1232
'username' => $this->getUsername(),
1233
'realName' => $this->getRealName(),
1234
'roles' => $roles,
1235
);
1236
}
1237
1238
public function getConduitSearchAttachments() {
1239
return array(
1240
id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
1241
->setAttachmentKey('availability'),
1242
);
1243
}
1244
1245
1246
/* -( User Cache )--------------------------------------------------------- */
1247
1248
1249
/**
1250
* @task cache
1251
*/
1252
public function attachRawCacheData(array $data) {
1253
$this->rawCacheData = $data + $this->rawCacheData;
1254
return $this;
1255
}
1256
1257
public function setAllowInlineCacheGeneration($allow_cache_generation) {
1258
$this->allowInlineCacheGeneration = $allow_cache_generation;
1259
return $this;
1260
}
1261
1262
/**
1263
* @task cache
1264
*/
1265
protected function requireCacheData($key) {
1266
if (isset($this->usableCacheData[$key])) {
1267
return $this->usableCacheData[$key];
1268
}
1269
1270
$type = PhabricatorUserCacheType::requireCacheTypeForKey($key);
1271
1272
if (isset($this->rawCacheData[$key])) {
1273
$raw_value = $this->rawCacheData[$key];
1274
1275
$usable_value = $type->getValueFromStorage($raw_value);
1276
$this->usableCacheData[$key] = $usable_value;
1277
1278
return $usable_value;
1279
}
1280
1281
// By default, we throw if a cache isn't available. This is consistent
1282
// with the standard `needX()` + `attachX()` + `getX()` interaction.
1283
if (!$this->allowInlineCacheGeneration) {
1284
throw new PhabricatorDataNotAttachedException($this);
1285
}
1286
1287
$user_phid = $this->getPHID();
1288
1289
// Try to read the actual cache before we generate a new value. We can
1290
// end up here via Conduit, which does not use normal sessions and can
1291
// not pick up a free cache load during session identification.
1292
if ($user_phid) {
1293
$raw_data = PhabricatorUserCache::readCaches(
1294
$type,
1295
$key,
1296
array($user_phid));
1297
if (array_key_exists($user_phid, $raw_data)) {
1298
$raw_value = $raw_data[$user_phid];
1299
$usable_value = $type->getValueFromStorage($raw_value);
1300
$this->rawCacheData[$key] = $raw_value;
1301
$this->usableCacheData[$key] = $usable_value;
1302
return $usable_value;
1303
}
1304
}
1305
1306
$usable_value = $type->getDefaultValue();
1307
1308
if ($user_phid) {
1309
$map = $type->newValueForUsers($key, array($this));
1310
if (array_key_exists($user_phid, $map)) {
1311
$raw_value = $map[$user_phid];
1312
$usable_value = $type->getValueFromStorage($raw_value);
1313
1314
$this->rawCacheData[$key] = $raw_value;
1315
PhabricatorUserCache::writeCache(
1316
$type,
1317
$key,
1318
$user_phid,
1319
$raw_value);
1320
}
1321
}
1322
1323
$this->usableCacheData[$key] = $usable_value;
1324
1325
return $usable_value;
1326
}
1327
1328
1329
/**
1330
* @task cache
1331
*/
1332
public function clearCacheData($key) {
1333
unset($this->rawCacheData[$key]);
1334
unset($this->usableCacheData[$key]);
1335
return $this;
1336
}
1337
1338
1339
public function getCSSValue($variable_key) {
1340
$preference = PhabricatorAccessibilitySetting::SETTINGKEY;
1341
$key = $this->getUserSetting($preference);
1342
1343
$postprocessor = CelerityPostprocessor::getPostprocessor($key);
1344
$variables = $postprocessor->getVariables();
1345
1346
if (!isset($variables[$variable_key])) {
1347
throw new Exception(
1348
pht(
1349
'Unknown CSS variable "%s"!',
1350
$variable_key));
1351
}
1352
1353
return $variables[$variable_key];
1354
}
1355
1356
/* -( PhabricatorAuthPasswordHashInterface )------------------------------- */
1357
1358
1359
public function newPasswordDigest(
1360
PhutilOpaqueEnvelope $envelope,
1361
PhabricatorAuthPassword $password) {
1362
1363
// Before passwords are hashed, they are digested. The goal of digestion
1364
// is twofold: to reduce the length of very long passwords to something
1365
// reasonable; and to salt the password in case the best available hasher
1366
// does not include salt automatically.
1367
1368
// Users may choose arbitrarily long passwords, and attackers may try to
1369
// attack the system by probing it with very long passwords. When large
1370
// inputs are passed to hashers -- which are intentionally slow -- it
1371
// can result in unacceptably long runtimes. The classic attack here is
1372
// to try to log in with a 64MB password and see if that locks up the
1373
// machine for the next century. By digesting passwords to a standard
1374
// length first, the length of the raw input does not impact the runtime
1375
// of the hashing algorithm.
1376
1377
// Some hashers like bcrypt are self-salting, while other hashers are not.
1378
// Applying salt while digesting passwords ensures that hashes are salted
1379
// whether we ultimately select a self-salting hasher or not.
1380
1381
// For legacy compatibility reasons, old VCS and Account password digest
1382
// algorithms are significantly more complicated than necessary to achieve
1383
// these goals. This is because they once used a different hashing and
1384
// salting process. When we upgraded to the modern modular hasher
1385
// infrastructure, we just bolted it onto the end of the existing pipelines
1386
// so that upgrading didn't break all users' credentials.
1387
1388
// New implementations can (and, generally, should) safely select the
1389
// simple HMAC SHA256 digest at the bottom of the function, which does
1390
// everything that a digest callback should without any needless legacy
1391
// baggage on top.
1392
1393
if ($password->getLegacyDigestFormat() == 'v1') {
1394
switch ($password->getPasswordType()) {
1395
case PhabricatorAuthPassword::PASSWORD_TYPE_VCS:
1396
// Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm.
1397
// They originally used this as a hasher, but it became a digest
1398
// algorithm once hashing was upgraded to include bcrypt.
1399
$digest = $envelope->openEnvelope();
1400
$salt = $this->getPHID();
1401
for ($ii = 0; $ii < 1000; $ii++) {
1402
$digest = PhabricatorHash::weakDigest($digest, $salt);
1403
}
1404
return new PhutilOpaqueEnvelope($digest);
1405
case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT:
1406
// Account passwords previously used this weird mess of salt and did
1407
// not digest the input to a standard length.
1408
1409
// Beyond this being a weird special case, there are two actual
1410
// problems with this, although neither are particularly severe:
1411
1412
// First, because we do not normalize the length of passwords, this
1413
// algorithm may make us vulnerable to DOS attacks where an attacker
1414
// attempts to use a very long input to slow down hashers.
1415
1416
// Second, because the username is part of the hash algorithm,
1417
// renaming a user breaks their password. This isn't a huge deal but
1418
// it's pretty silly. There's no security justification for this
1419
// behavior, I just didn't think about the implication when I wrote
1420
// it originally.
1421
1422
$parts = array(
1423
$this->getUsername(),
1424
$envelope->openEnvelope(),
1425
$this->getPHID(),
1426
$password->getPasswordSalt(),
1427
);
1428
1429
return new PhutilOpaqueEnvelope(implode('', $parts));
1430
}
1431
}
1432
1433
// For passwords which do not have some crazy legacy reason to use some
1434
// other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies
1435
// the digest requirements and is simple.
1436
1437
$digest = PhabricatorHash::digestHMACSHA256(
1438
$envelope->openEnvelope(),
1439
$password->getPasswordSalt());
1440
1441
return new PhutilOpaqueEnvelope($digest);
1442
}
1443
1444
public function newPasswordBlocklist(
1445
PhabricatorUser $viewer,
1446
PhabricatorAuthPasswordEngine $engine) {
1447
1448
$list = array();
1449
$list[] = $this->getUsername();
1450
$list[] = $this->getRealName();
1451
1452
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
1453
'userPHID = %s',
1454
$this->getPHID());
1455
foreach ($emails as $email) {
1456
$list[] = $email->getAddress();
1457
}
1458
1459
return $list;
1460
}
1461
1462
1463
}
1464
1465