2011-01-23 18:09:16 -08:00
|
|
|
<?php
|
|
|
|
|
2012-06-14 18:08:06 -07:00
|
|
|
final class PhabricatorUser extends PhabricatorUserDAO implements PhutilPerson {
|
2011-01-23 18:09:16 -08:00
|
|
|
|
2011-02-05 22:36:21 -08:00
|
|
|
const SESSION_TABLE = 'phabricator_session';
|
2011-10-23 13:25:52 -07:00
|
|
|
const NAMETOKEN_TABLE = 'user_nametoken';
|
2011-02-05 22:36:21 -08:00
|
|
|
|
2011-01-23 18:09:16 -08:00
|
|
|
protected $phid;
|
|
|
|
protected $userName;
|
|
|
|
protected $realName;
|
2012-04-19 15:36:09 -07:00
|
|
|
protected $sex;
|
2012-06-14 18:08:06 -07:00
|
|
|
protected $translation;
|
2011-01-26 13:21:12 -08:00
|
|
|
protected $passwordSalt;
|
|
|
|
protected $passwordHash;
|
2011-01-30 21:28:45 -08:00
|
|
|
protected $profileImagePHID;
|
2011-06-20 13:00:31 -07:00
|
|
|
protected $timezoneIdentifier = '';
|
2011-01-23 18:09:16 -08:00
|
|
|
|
2011-02-02 22:38:42 -08:00
|
|
|
protected $consoleEnabled = 0;
|
|
|
|
protected $consoleVisible = 0;
|
|
|
|
protected $consoleTab = '';
|
2011-02-02 13:59:52 -08:00
|
|
|
|
2011-02-05 22:36:21 -08:00
|
|
|
protected $conduitCertificate;
|
|
|
|
|
2011-04-12 19:00:54 -07:00
|
|
|
protected $isSystemAgent = 0;
|
2011-05-12 10:06:54 -07:00
|
|
|
protected $isAdmin = 0;
|
|
|
|
protected $isDisabled = 0;
|
2011-04-12 18:19:09 -07:00
|
|
|
|
2013-03-24 06:42:31 -07:00
|
|
|
private $profileImage = null;
|
|
|
|
private $profile = null;
|
|
|
|
private $status = null;
|
2011-03-31 18:46:53 -07:00
|
|
|
private $preferences = null;
|
2013-02-28 11:01:40 -08:00
|
|
|
private $omnipotent = false;
|
2011-03-30 19:21:09 -07:00
|
|
|
|
2011-10-07 15:03:00 -07:00
|
|
|
protected function readField($field) {
|
2011-11-16 09:49:18 -08:00
|
|
|
switch ($field) {
|
|
|
|
case 'timezoneIdentifier':
|
|
|
|
// If the user hasn't set one, guess the server's time.
|
|
|
|
return nonempty(
|
|
|
|
$this->timezoneIdentifier,
|
|
|
|
date_default_timezone_get());
|
|
|
|
// Make sure these return booleans.
|
|
|
|
case 'isAdmin':
|
|
|
|
return (bool)$this->isAdmin;
|
|
|
|
case 'isDisabled':
|
|
|
|
return (bool)$this->isDisabled;
|
|
|
|
case 'isSystemAgent':
|
|
|
|
return (bool)$this->isSystemAgent;
|
|
|
|
default:
|
|
|
|
return parent::readField($field);
|
2011-10-07 15:03:00 -07:00
|
|
|
}
|
2011-01-31 16:00:42 -08:00
|
|
|
}
|
2011-01-30 18:52:29 -08:00
|
|
|
|
2011-01-23 18:09:16 -08:00
|
|
|
public function getConfiguration() {
|
|
|
|
return array(
|
|
|
|
self::CONFIG_AUX_PHID => true,
|
2011-10-07 15:03:00 -07:00
|
|
|
self::CONFIG_PARTIAL_OBJECTS => true,
|
2011-01-23 18:09:16 -08:00
|
|
|
) + parent::getConfiguration();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function generatePHID() {
|
2011-03-02 18:58:21 -08:00
|
|
|
return PhabricatorPHID::generateNewPHID(
|
|
|
|
PhabricatorPHIDConstants::PHID_TYPE_USER);
|
2011-01-23 18:09:16 -08:00
|
|
|
}
|
|
|
|
|
2012-07-17 12:06:33 -07:00
|
|
|
public function setPassword(PhutilOpaqueEnvelope $envelope) {
|
Mask typed passwords as they are entered into 'accountadmin'
Summary:
Currently, we echo the password as the user types it. This turns out to be a bit
of an issue in over-the-shoulder installs. Instead, disable tty echo while the
user is typing their password so nothing is shown (like how 'sudo' works).
Also show a better error message if the user chooses a duplicate email; without
testing for this we just throw a duplicate key exception when saving, which
isn't easy to understand. The other duplicate key exception is duplicate
username, which is impossible (the script updates rather than creating in this
case).
There's currently a bug where creating a user and setting their password at the
same time doesn't work. This is because we hash the PHID into the password hash,
but it's empty if the user hasn't been persisted yet. Make sure the user is
persisted before setting their password.
Finally, fix an issue where $original would have the new username set, creating
a somewhat confusing summary at the end.
I'm also going to improve the password behavior/explanation here once I add
welcome emails ("Hi Joe, epriestley created an account for you on Phabricator,
click here to login...").
Test Plan:
- Typed a password and didn't have it echoed. I also tested this on Ubuntu
without encountering problems.
- Chose a duplicate email, got a useful error message instead of the exception
I'd encountered earlier.
- Created a new user with a password in one pass and logged in as that user,
this worked properly.
- Verified summary table does not contain username for new users.
Reviewed By: jungejason
Reviewers: jungejason, tuomaspelkonen, aran
CC: moskov, jr, aran, jungejason
Differential Revision: 358
2011-05-28 07:17:42 -07:00
|
|
|
if (!$this->getPHID()) {
|
|
|
|
throw new Exception(
|
|
|
|
"You can not set a password for an unsaved user because their PHID ".
|
|
|
|
"is a salt component in the password hash.");
|
|
|
|
}
|
|
|
|
|
2012-07-17 12:06:33 -07:00
|
|
|
if (!strlen($envelope->openEnvelope())) {
|
2011-05-12 10:06:54 -07:00
|
|
|
$this->setPasswordHash('');
|
|
|
|
} else {
|
|
|
|
$this->setPasswordSalt(md5(mt_rand()));
|
2012-07-17 12:06:33 -07:00
|
|
|
$hash = $this->hashPassword($envelope);
|
2011-05-12 10:06:54 -07:00
|
|
|
$this->setPasswordHash($hash);
|
|
|
|
}
|
2011-01-26 13:21:12 -08:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-06-14 18:08:06 -07:00
|
|
|
// To satisfy PhutilPerson.
|
|
|
|
public function getSex() {
|
|
|
|
return $this->sex;
|
|
|
|
}
|
|
|
|
|
2012-06-15 23:21:25 -07:00
|
|
|
public function getTranslation() {
|
|
|
|
try {
|
|
|
|
if ($this->translation &&
|
|
|
|
class_exists($this->translation) &&
|
|
|
|
is_subclass_of($this->translation, 'PhabricatorTranslation')) {
|
|
|
|
return $this->translation;
|
|
|
|
}
|
|
|
|
} catch (PhutilMissingSymbolException $ex) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2011-10-24 12:27:16 -07:00
|
|
|
public function isLoggedIn() {
|
|
|
|
return !($this->getPHID() === null);
|
|
|
|
}
|
|
|
|
|
2011-02-05 22:36:21 -08:00
|
|
|
public function save() {
|
2011-10-07 15:03:00 -07:00
|
|
|
if (!$this->getConduitCertificate()) {
|
|
|
|
$this->setConduitCertificate($this->generateConduitCertificate());
|
2011-02-05 22:36:21 -08:00
|
|
|
}
|
2011-06-23 13:31:20 -07:00
|
|
|
$result = parent::save();
|
|
|
|
|
2011-10-23 13:25:52 -07:00
|
|
|
$this->updateNameTokens();
|
Improve Search architecture
Summary:
The search indexing API has several problems right now:
- Always runs in-process.
- It would be nice to push this into the task queue for performance. However, the API currently passses an object all the way through (and some indexers depend on preloaded object attributes), so it can't be dumped into the task queue at any stage since we can't serialize it.
- Being able to use the task queue will also make rebuilding indexes faster.
- Instead, make the API phid-oriented.
- No uniform indexing API.
- Each "Editor" currently calls SomeCustomIndexer::indexThing(). This won't work with AbstractTransactions. The API is also just weird.
- Instead, provide a uniform API.
- No uniform CLI.
- We have `scripts/search/reindex_everything.php`, but it doesn't actually index everything. Each new document type needs to be separately added to it, leading to stuff like D3839. Third-party applications can't provide indexers.
- Instead, let indexers expose documents for indexing.
- Not application-oriented.
- All the indexers live in search/ right now, which isn't the right organization in an application-orietned view of the world.
- Instead, move indexers to applications and load them with SymbolLoader.
Test Plan:
- `bin/search index`
- Indexed one revision, one task.
- Indexed `--type TASK`, `--type DREV`, etc., for all types.
- Indexed `--all`.
- Added the word "saboteur" to a revision, task, wiki page, and question and then searched for it.
- Creating users is a pain; searched for a user after indexing.
- Creating commits is a pain; searched for a commit after indexing.
- Mocks aren't currently loadable in the result view, so their indexing is moot.
Reviewers: btrahan, vrana
Reviewed By: btrahan
CC: 20after4, aran
Maniphest Tasks: T1991, T2104
Differential Revision: https://secure.phabricator.com/D4261
2012-12-21 14:21:31 -08:00
|
|
|
|
|
|
|
id(new PhabricatorSearchIndexer())
|
|
|
|
->indexDocumentByPHID($this->getPHID());
|
2011-06-23 13:31:20 -07:00
|
|
|
|
|
|
|
return $result;
|
2011-02-05 22:36:21 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
private function generateConduitCertificate() {
|
Replace callsites to sha1() that use it to asciify entropy with
Filesystem::readRandomCharacters()
Summary: See T547. To improve auditability of use of crypto-sensitive hash
functions, use Filesystem::readRandomCharacters() in place of
sha1(Filesystem::readRandomBytes()) when we're just generating random ASCII
strings.
Test Plan:
- Generated a new PHID.
- Logged out and logged back in (to test sessions).
- Regenerated Conduit certificate.
- Created a new task, verified mail key generated sensibly.
- Created a new revision, verified mail key generated sensibly.
- Ran "arc list", got blocked, installed new certificate, ran "arc list"
again.
Reviewers: jungejason, nh, tuomaspelkonen, aran, benmathews
Reviewed By: jungejason
CC: aran, epriestley, jungejason
Differential Revision: 1000
2011-10-10 19:22:30 -07:00
|
|
|
return Filesystem::readRandomCharacters(255);
|
2011-02-05 22:36:21 -08:00
|
|
|
}
|
|
|
|
|
2012-07-17 12:06:33 -07:00
|
|
|
public function comparePassword(PhutilOpaqueEnvelope $envelope) {
|
|
|
|
if (!strlen($envelope->openEnvelope())) {
|
2011-05-12 10:06:54 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!strlen($this->getPasswordHash())) {
|
|
|
|
return false;
|
|
|
|
}
|
2012-07-17 12:06:33 -07:00
|
|
|
$password_hash = $this->hashPassword($envelope);
|
|
|
|
return ($password_hash === $this->getPasswordHash());
|
2011-01-26 13:21:12 -08:00
|
|
|
}
|
|
|
|
|
2012-07-17 12:06:33 -07:00
|
|
|
private function hashPassword(PhutilOpaqueEnvelope $envelope) {
|
|
|
|
$hash = $this->getUsername().
|
|
|
|
$envelope->openEnvelope().
|
|
|
|
$this->getPHID().
|
|
|
|
$this->getPasswordSalt();
|
2011-01-26 13:21:12 -08:00
|
|
|
for ($ii = 0; $ii < 1000; $ii++) {
|
2012-07-17 12:06:33 -07:00
|
|
|
$hash = md5($hash);
|
2011-01-26 13:21:12 -08:00
|
|
|
}
|
2012-07-17 12:06:33 -07:00
|
|
|
return $hash;
|
2011-01-26 13:21:12 -08:00
|
|
|
}
|
|
|
|
|
2011-01-31 11:55:26 -08:00
|
|
|
const CSRF_CYCLE_FREQUENCY = 3600;
|
|
|
|
const CSRF_TOKEN_LENGTH = 16;
|
|
|
|
|
|
|
|
const EMAIL_CYCLE_FREQUENCY = 86400;
|
|
|
|
const EMAIL_TOKEN_LENGTH = 24;
|
|
|
|
|
|
|
|
public function getCSRFToken($offset = 0) {
|
|
|
|
return $this->generateToken(
|
|
|
|
time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
|
|
|
|
self::CSRF_CYCLE_FREQUENCY,
|
|
|
|
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
|
|
|
|
self::CSRF_TOKEN_LENGTH);
|
2011-01-30 18:52:29 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
public function validateCSRFToken($token) {
|
Fix conservative CSRF token cycling limit
Summary:
We currently cycle CSRF tokens every hour and check for the last two valid ones.
This means that a form could go stale in as little as an hour, and is certainly
stale after two.
When a stale form is submitted, you basically get a terrible heisen-state where
some of your data might persist if you're lucky but more likely it all just
vanishes. The .js file below outlines some more details.
This is a pretty terrible UX and we don't need to be as conservative about CSRF
validation as we're being. Remedy this problem by:
- Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are
valid for at least 6 hours, and for as long as 7).
- Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected
to the internet are valid indefinitely).
- Showing the user an explicit message about what went wrong when CSRF
validation fails so the experience is less bewildering.
They should now only be able to submit with a bad CSRF token if:
- They load a page, disconnect from the internet for 7 hours, reconnect, and
submit the form within 55 minutes; or
- They are actually the victim of a CSRF attack.
We could eventually fix the first one by tracking reconnects, which might be
"free" once the notification server gets built. It will probably never be an
issue in practice.
Test Plan:
- Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15
seconds, got the CSRF exception.
- Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15
seconds, got a clean form post.
- Added debugging code the the csrf refresh to make sure it was doing sensible
things (pulling different tokens, finding all the inputs).
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: aran, epriestley
Differential Revision: 660
2011-07-13 14:05:18 -07:00
|
|
|
|
Validate logins, and simplify email password resets
Summary:
- There are some recent reports of login issues, see T755 and T754. I'm not
really sure what's going on, but this is an attempt at getting some more
information.
- When we login a user by setting 'phusr' and 'phsid', send them to
/login/validate/ to validate that the cookies actually got set.
- Do email password resets in two steps: first, log the user in. Redirect them
through validate, then give them the option to reset their password.
- Don't CSRF logged-out users. It technically sort of works most of the time
right now, but is silly. If we need logged-out CSRF we should generate it in
some more reliable way.
Test Plan:
- Logged in with username/password.
- Logged in with OAuth.
- Logged in with email password reset.
- Sent bad values to /login/validate/, got appropriate errors.
- Reset password.
- Verified next_uri still works.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan, j3kuntz
Maniphest Tasks: T754, T755
Differential Revision: https://secure.phabricator.com/D1353
2012-01-10 14:42:07 -08:00
|
|
|
if (!$this->getPHID()) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
Fix conservative CSRF token cycling limit
Summary:
We currently cycle CSRF tokens every hour and check for the last two valid ones.
This means that a form could go stale in as little as an hour, and is certainly
stale after two.
When a stale form is submitted, you basically get a terrible heisen-state where
some of your data might persist if you're lucky but more likely it all just
vanishes. The .js file below outlines some more details.
This is a pretty terrible UX and we don't need to be as conservative about CSRF
validation as we're being. Remedy this problem by:
- Accepting the last 6 CSRF tokens instead of the last 1 (i.e., pages are
valid for at least 6 hours, and for as long as 7).
- Using JS to refresh the CSRF token every 55 minutes (i.e., pages connected
to the internet are valid indefinitely).
- Showing the user an explicit message about what went wrong when CSRF
validation fails so the experience is less bewildering.
They should now only be able to submit with a bad CSRF token if:
- They load a page, disconnect from the internet for 7 hours, reconnect, and
submit the form within 55 minutes; or
- They are actually the victim of a CSRF attack.
We could eventually fix the first one by tracking reconnects, which might be
"free" once the notification server gets built. It will probably never be an
issue in practice.
Test Plan:
- Reduced CSRF cycle frequency to 2 seconds, submitted a form after 15
seconds, got the CSRF exception.
- Reduced csrf-refresh cycle frequency to 3 seconds, submitted a form after 15
seconds, got a clean form post.
- Added debugging code the the csrf refresh to make sure it was doing sensible
things (pulling different tokens, finding all the inputs).
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: aran, epriestley
Differential Revision: 660
2011-07-13 14:05:18 -07:00
|
|
|
// When the user posts a form, we check that it contains a valid CSRF token.
|
|
|
|
// Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
|
|
|
|
// either the current token, the next token (users can submit a "future"
|
|
|
|
// token if you have two web frontends that have some clock skew) or any of
|
|
|
|
// the last 6 tokens. This means that pages are valid for up to 7 hours.
|
|
|
|
// There is also some Javascript which periodically refreshes the CSRF
|
|
|
|
// tokens on each page, so theoretically pages should be valid indefinitely.
|
|
|
|
// However, this code may fail to run (if the user loses their internet
|
|
|
|
// connection, or there's a JS problem, or they don't have JS enabled).
|
|
|
|
// Choosing the size of the window in which we accept old CSRF tokens is
|
|
|
|
// an issue of balancing concerns between security and usability. We could
|
|
|
|
// choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
|
|
|
|
// attacks using captured CSRF tokens, but it's also more likely that real
|
|
|
|
// users will be affected by this, e.g. if they close their laptop for an
|
|
|
|
// hour, open it back up, and try to submit a form before the CSRF refresh
|
|
|
|
// can kick in. Since the user experience of submitting a form with expired
|
|
|
|
// CSRF is often quite bad (you basically lose data, or it's a big pain to
|
|
|
|
// recover at least) and I believe we gain little additional protection
|
|
|
|
// by keeping the window very short (the overwhelming value here is in
|
|
|
|
// preventing blind attacks, and most attacks which can capture CSRF tokens
|
|
|
|
// can also just capture authentication information [sniffing networks]
|
|
|
|
// or act as the user [xss]) the 7 hour default seems like a reasonable
|
|
|
|
// balance. Other major platforms have much longer CSRF token lifetimes,
|
|
|
|
// like Rails (session duration) and Django (forever), which suggests this
|
|
|
|
// is a reasonable analysis.
|
|
|
|
$csrf_window = 6;
|
|
|
|
|
|
|
|
for ($ii = -$csrf_window; $ii <= 1; $ii++) {
|
2011-01-31 11:55:26 -08:00
|
|
|
$valid = $this->getCSRFToken($ii);
|
2011-01-30 18:52:29 -08:00
|
|
|
if ($token == $valid) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
Validate logins, and simplify email password resets
Summary:
- There are some recent reports of login issues, see T755 and T754. I'm not
really sure what's going on, but this is an attempt at getting some more
information.
- When we login a user by setting 'phusr' and 'phsid', send them to
/login/validate/ to validate that the cookies actually got set.
- Do email password resets in two steps: first, log the user in. Redirect them
through validate, then give them the option to reset their password.
- Don't CSRF logged-out users. It technically sort of works most of the time
right now, but is silly. If we need logged-out CSRF we should generate it in
some more reliable way.
Test Plan:
- Logged in with username/password.
- Logged in with OAuth.
- Logged in with email password reset.
- Sent bad values to /login/validate/, got appropriate errors.
- Reset password.
- Verified next_uri still works.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan, j3kuntz
Maniphest Tasks: T754, T755
Differential Revision: https://secure.phabricator.com/D1353
2012-01-10 14:42:07 -08:00
|
|
|
|
2011-01-30 18:52:29 -08:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2011-01-31 11:55:26 -08:00
|
|
|
private function generateToken($epoch, $frequency, $key, $len) {
|
|
|
|
$time_block = floor($epoch / $frequency);
|
2011-10-07 15:03:00 -07:00
|
|
|
$vec = $this->getPHID().$this->getPasswordHash().$key.$time_block;
|
2011-12-18 11:00:39 -08:00
|
|
|
return substr(PhabricatorHash::digest($vec), 0, $len);
|
2011-01-30 18:52:29 -08:00
|
|
|
}
|
|
|
|
|
Enable multiple web sessions
Summary:
Conduit already has multiple-session code, just move it to the main
establishSession() method and set a web session limit larger than 1.
NOTE: This will log everyone out since we no longer look for the "web" session,
only for "web-1", "web-2", ..., etc. Presumably this doesn't matter.
Test Plan:
Applied patch, was logged out. Logged in in Safari. Verified I was issued
"web-1". Logged in in Firefox. Verified I was issued "web-2".
Kept logging in and out until I got issued "web-5", then did it again and was
issued "web-1" with a new key.
Ran conduit methods and verified they work and correctly cycled session keys.
Reviewed By: tuomaspelkonen
Reviewers: tuomaspelkonen, jungejason, aran
Commenters: jungejason
CC: rm, fzamore, ola, aran, epriestley, jungejason, tuomaspelkonen
Differential Revision: 264
2011-05-11 04:52:32 -07:00
|
|
|
/**
|
|
|
|
* Issue a new session key to this user. Phabricator supports different
|
|
|
|
* types of sessions (like "web" and "conduit") and each session type may
|
|
|
|
* have multiple concurrent sessions (this allows a user to be logged in on
|
|
|
|
* multiple browsers at the same time, for instance).
|
|
|
|
*
|
|
|
|
* Note that this method is transport-agnostic and does not set cookies or
|
|
|
|
* issue other types of tokens, it ONLY generates a new session key.
|
|
|
|
*
|
|
|
|
* You can configure the maximum number of concurrent sessions for various
|
|
|
|
* session types in the Phabricator configuration.
|
|
|
|
*
|
|
|
|
* @param string Session type, like "web".
|
|
|
|
* @return string Newly generated session key.
|
|
|
|
*/
|
2011-01-30 21:28:45 -08:00
|
|
|
public function establishSession($session_type) {
|
|
|
|
$conn_w = $this->establishConnection('w');
|
|
|
|
|
Enable multiple web sessions
Summary:
Conduit already has multiple-session code, just move it to the main
establishSession() method and set a web session limit larger than 1.
NOTE: This will log everyone out since we no longer look for the "web" session,
only for "web-1", "web-2", ..., etc. Presumably this doesn't matter.
Test Plan:
Applied patch, was logged out. Logged in in Safari. Verified I was issued
"web-1". Logged in in Firefox. Verified I was issued "web-2".
Kept logging in and out until I got issued "web-5", then did it again and was
issued "web-1" with a new key.
Ran conduit methods and verified they work and correctly cycled session keys.
Reviewed By: tuomaspelkonen
Reviewers: tuomaspelkonen, jungejason, aran
Commenters: jungejason
CC: rm, fzamore, ola, aran, epriestley, jungejason, tuomaspelkonen
Differential Revision: 264
2011-05-11 04:52:32 -07:00
|
|
|
if (strpos($session_type, '-') !== false) {
|
|
|
|
throw new Exception("Session type must not contain hyphen ('-')!");
|
|
|
|
}
|
|
|
|
|
|
|
|
// We allow multiple sessions of the same type, so when a caller requests
|
|
|
|
// a new session of type "web", we give them the first available session in
|
|
|
|
// "web-1", "web-2", ..., "web-N", up to some configurable limit. If none
|
|
|
|
// of these sessions is available, we overwrite the oldest session and
|
|
|
|
// reissue a new one in its place.
|
|
|
|
|
|
|
|
$session_limit = 1;
|
|
|
|
switch ($session_type) {
|
|
|
|
case 'web':
|
|
|
|
$session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web');
|
|
|
|
break;
|
|
|
|
case 'conduit':
|
|
|
|
$session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Exception("Unknown session type '{$session_type}'!");
|
|
|
|
}
|
|
|
|
|
|
|
|
$session_limit = (int)$session_limit;
|
|
|
|
if ($session_limit <= 0) {
|
|
|
|
throw new Exception(
|
|
|
|
"Session limit for '{$session_type}' must be at least 1!");
|
|
|
|
}
|
|
|
|
|
Improve a race condition in session establishment code
Summary:
If you try to establish several sessions quickly (e.g., by running several
copies of "arc" at once, as in "arc x | arc y"), the current logic has a high
chance of making them all pick the same conduit session to refresh (since it's
the oldest one when each process selects the current sessions). This means they
all issue updates against "conduit-3" (or whatever) and one ends up with a bogus
session.
Instead, do an update against the table with the session key we read, so only
one process wins the race. If we don't win the race, try again until we do or
have tried every session slot.
Test Plan:
- Wiped conduit sessions, ran arc commands to verify the fresh session case.
- Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list |
...". It succeeds up to the session limit, and above that gets failures as
expected.
- Manually checked the session table to make sure things seemed reasonable
there.
- Generally ran a bunch of arc commands.
- Logged out and logged in on the web interface.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan
Maniphest Tasks: T687
Differential Revision: https://secure.phabricator.com/D1329
2012-01-05 17:55:21 -08:00
|
|
|
// NOTE: Session establishment is sensitive to race conditions, as when
|
|
|
|
// piping `arc` to `arc`:
|
|
|
|
//
|
|
|
|
// arc export ... | arc paste ...
|
|
|
|
//
|
|
|
|
// To avoid this, we overwrite an old session only if it hasn't been
|
|
|
|
// re-established since we read it.
|
|
|
|
|
|
|
|
// Consume entropy to generate a new session key, forestalling the eventual
|
|
|
|
// heat death of the universe.
|
|
|
|
$session_key = Filesystem::readRandomCharacters(40);
|
|
|
|
|
Enable multiple web sessions
Summary:
Conduit already has multiple-session code, just move it to the main
establishSession() method and set a web session limit larger than 1.
NOTE: This will log everyone out since we no longer look for the "web" session,
only for "web-1", "web-2", ..., etc. Presumably this doesn't matter.
Test Plan:
Applied patch, was logged out. Logged in in Safari. Verified I was issued
"web-1". Logged in in Firefox. Verified I was issued "web-2".
Kept logging in and out until I got issued "web-5", then did it again and was
issued "web-1" with a new key.
Ran conduit methods and verified they work and correctly cycled session keys.
Reviewed By: tuomaspelkonen
Reviewers: tuomaspelkonen, jungejason, aran
Commenters: jungejason
CC: rm, fzamore, ola, aran, epriestley, jungejason, tuomaspelkonen
Differential Revision: 264
2011-05-11 04:52:32 -07:00
|
|
|
// Load all the currently active sessions.
|
|
|
|
$sessions = queryfx_all(
|
|
|
|
$conn_w,
|
Improve a race condition in session establishment code
Summary:
If you try to establish several sessions quickly (e.g., by running several
copies of "arc" at once, as in "arc x | arc y"), the current logic has a high
chance of making them all pick the same conduit session to refresh (since it's
the oldest one when each process selects the current sessions). This means they
all issue updates against "conduit-3" (or whatever) and one ends up with a bogus
session.
Instead, do an update against the table with the session key we read, so only
one process wins the race. If we don't win the race, try again until we do or
have tried every session slot.
Test Plan:
- Wiped conduit sessions, ran arc commands to verify the fresh session case.
- Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list |
...". It succeeds up to the session limit, and above that gets failures as
expected.
- Manually checked the session table to make sure things seemed reasonable
there.
- Generally ran a bunch of arc commands.
- Logged out and logged in on the web interface.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan
Maniphest Tasks: T687
Differential Revision: https://secure.phabricator.com/D1329
2012-01-05 17:55:21 -08:00
|
|
|
'SELECT type, sessionKey, sessionStart FROM %T
|
|
|
|
WHERE userPHID = %s AND type LIKE %>',
|
Enable multiple web sessions
Summary:
Conduit already has multiple-session code, just move it to the main
establishSession() method and set a web session limit larger than 1.
NOTE: This will log everyone out since we no longer look for the "web" session,
only for "web-1", "web-2", ..., etc. Presumably this doesn't matter.
Test Plan:
Applied patch, was logged out. Logged in in Safari. Verified I was issued
"web-1". Logged in in Firefox. Verified I was issued "web-2".
Kept logging in and out until I got issued "web-5", then did it again and was
issued "web-1" with a new key.
Ran conduit methods and verified they work and correctly cycled session keys.
Reviewed By: tuomaspelkonen
Reviewers: tuomaspelkonen, jungejason, aran
Commenters: jungejason
CC: rm, fzamore, ola, aran, epriestley, jungejason, tuomaspelkonen
Differential Revision: 264
2011-05-11 04:52:32 -07:00
|
|
|
PhabricatorUser::SESSION_TABLE,
|
|
|
|
$this->getPHID(),
|
|
|
|
$session_type.'-');
|
|
|
|
$sessions = ipull($sessions, null, 'type');
|
Improve a race condition in session establishment code
Summary:
If you try to establish several sessions quickly (e.g., by running several
copies of "arc" at once, as in "arc x | arc y"), the current logic has a high
chance of making them all pick the same conduit session to refresh (since it's
the oldest one when each process selects the current sessions). This means they
all issue updates against "conduit-3" (or whatever) and one ends up with a bogus
session.
Instead, do an update against the table with the session key we read, so only
one process wins the race. If we don't win the race, try again until we do or
have tried every session slot.
Test Plan:
- Wiped conduit sessions, ran arc commands to verify the fresh session case.
- Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list |
...". It succeeds up to the session limit, and above that gets failures as
expected.
- Manually checked the session table to make sure things seemed reasonable
there.
- Generally ran a bunch of arc commands.
- Logged out and logged in on the web interface.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan
Maniphest Tasks: T687
Differential Revision: https://secure.phabricator.com/D1329
2012-01-05 17:55:21 -08:00
|
|
|
$sessions = isort($sessions, 'sessionStart');
|
|
|
|
|
|
|
|
$existing_sessions = array_keys($sessions);
|
|
|
|
|
2012-01-09 13:47:33 -08:00
|
|
|
// UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
|
Improve a race condition in session establishment code
Summary:
If you try to establish several sessions quickly (e.g., by running several
copies of "arc" at once, as in "arc x | arc y"), the current logic has a high
chance of making them all pick the same conduit session to refresh (since it's
the oldest one when each process selects the current sessions). This means they
all issue updates against "conduit-3" (or whatever) and one ends up with a bogus
session.
Instead, do an update against the table with the session key we read, so only
one process wins the race. If we don't win the race, try again until we do or
have tried every session slot.
Test Plan:
- Wiped conduit sessions, ran arc commands to verify the fresh session case.
- Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list |
...". It succeeds up to the session limit, and above that gets failures as
expected.
- Manually checked the session table to make sure things seemed reasonable
there.
- Generally ran a bunch of arc commands.
- Logged out and logged in on the web interface.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan
Maniphest Tasks: T687
Differential Revision: https://secure.phabricator.com/D1329
2012-01-05 17:55:21 -08:00
|
|
|
$retries = 0;
|
|
|
|
while (true) {
|
|
|
|
|
2012-01-09 13:47:33 -08:00
|
|
|
|
Improve a race condition in session establishment code
Summary:
If you try to establish several sessions quickly (e.g., by running several
copies of "arc" at once, as in "arc x | arc y"), the current logic has a high
chance of making them all pick the same conduit session to refresh (since it's
the oldest one when each process selects the current sessions). This means they
all issue updates against "conduit-3" (or whatever) and one ends up with a bogus
session.
Instead, do an update against the table with the session key we read, so only
one process wins the race. If we don't win the race, try again until we do or
have tried every session slot.
Test Plan:
- Wiped conduit sessions, ran arc commands to verify the fresh session case.
- Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list |
...". It succeeds up to the session limit, and above that gets failures as
expected.
- Manually checked the session table to make sure things seemed reasonable
there.
- Generally ran a bunch of arc commands.
- Logged out and logged in on the web interface.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan
Maniphest Tasks: T687
Differential Revision: https://secure.phabricator.com/D1329
2012-01-05 17:55:21 -08:00
|
|
|
// Choose which 'type' we'll actually establish, i.e. what number we're
|
|
|
|
// going to append to the basic session type. To do this, just check all
|
|
|
|
// the numbers sequentially until we find an available session.
|
|
|
|
$establish_type = null;
|
|
|
|
for ($ii = 1; $ii <= $session_limit; $ii++) {
|
|
|
|
$try_type = $session_type.'-'.$ii;
|
|
|
|
if (!in_array($try_type, $existing_sessions)) {
|
|
|
|
$establish_type = $try_type;
|
|
|
|
$expect_key = $session_key;
|
|
|
|
$existing_sessions[] = $try_type;
|
|
|
|
|
|
|
|
// Ensure the row exists so we can issue an update below. We don't
|
|
|
|
// care if we race here or not.
|
|
|
|
queryfx(
|
|
|
|
$conn_w,
|
|
|
|
'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart)
|
|
|
|
VALUES (%s, %s, %s, 0)',
|
|
|
|
self::SESSION_TABLE,
|
|
|
|
$this->getPHID(),
|
|
|
|
$establish_type,
|
|
|
|
$session_key);
|
|
|
|
break;
|
|
|
|
}
|
Enable multiple web sessions
Summary:
Conduit already has multiple-session code, just move it to the main
establishSession() method and set a web session limit larger than 1.
NOTE: This will log everyone out since we no longer look for the "web" session,
only for "web-1", "web-2", ..., etc. Presumably this doesn't matter.
Test Plan:
Applied patch, was logged out. Logged in in Safari. Verified I was issued
"web-1". Logged in in Firefox. Verified I was issued "web-2".
Kept logging in and out until I got issued "web-5", then did it again and was
issued "web-1" with a new key.
Ran conduit methods and verified they work and correctly cycled session keys.
Reviewed By: tuomaspelkonen
Reviewers: tuomaspelkonen, jungejason, aran
Commenters: jungejason
CC: rm, fzamore, ola, aran, epriestley, jungejason, tuomaspelkonen
Differential Revision: 264
2011-05-11 04:52:32 -07:00
|
|
|
}
|
|
|
|
|
Improve a race condition in session establishment code
Summary:
If you try to establish several sessions quickly (e.g., by running several
copies of "arc" at once, as in "arc x | arc y"), the current logic has a high
chance of making them all pick the same conduit session to refresh (since it's
the oldest one when each process selects the current sessions). This means they
all issue updates against "conduit-3" (or whatever) and one ends up with a bogus
session.
Instead, do an update against the table with the session key we read, so only
one process wins the race. If we don't win the race, try again until we do or
have tried every session slot.
Test Plan:
- Wiped conduit sessions, ran arc commands to verify the fresh session case.
- Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list |
...". It succeeds up to the session limit, and above that gets failures as
expected.
- Manually checked the session table to make sure things seemed reasonable
there.
- Generally ran a bunch of arc commands.
- Logged out and logged in on the web interface.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan
Maniphest Tasks: T687
Differential Revision: https://secure.phabricator.com/D1329
2012-01-05 17:55:21 -08:00
|
|
|
// If we didn't find an available session, choose the oldest session and
|
|
|
|
// overwrite it.
|
|
|
|
if (!$establish_type) {
|
|
|
|
$oldest = reset($sessions);
|
|
|
|
$establish_type = $oldest['type'];
|
|
|
|
$expect_key = $oldest['sessionKey'];
|
|
|
|
}
|
2011-01-30 21:28:45 -08:00
|
|
|
|
2012-01-09 13:47:33 -08:00
|
|
|
// This is so that we'll only overwrite the session if it hasn't been
|
|
|
|
// refreshed since we read it. If it has, the session key will be
|
|
|
|
// different and we know we're racing other processes. Whichever one
|
|
|
|
// won gets the session, we go back and try again.
|
|
|
|
|
|
|
|
queryfx(
|
|
|
|
$conn_w,
|
|
|
|
'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP()
|
|
|
|
WHERE userPHID = %s AND type = %s AND sessionKey = %s',
|
|
|
|
self::SESSION_TABLE,
|
|
|
|
$session_key,
|
|
|
|
$this->getPHID(),
|
|
|
|
$establish_type,
|
|
|
|
$expect_key);
|
Improve a race condition in session establishment code
Summary:
If you try to establish several sessions quickly (e.g., by running several
copies of "arc" at once, as in "arc x | arc y"), the current logic has a high
chance of making them all pick the same conduit session to refresh (since it's
the oldest one when each process selects the current sessions). This means they
all issue updates against "conduit-3" (or whatever) and one ends up with a bogus
session.
Instead, do an update against the table with the session key we read, so only
one process wins the race. If we don't win the race, try again until we do or
have tried every session slot.
Test Plan:
- Wiped conduit sessions, ran arc commands to verify the fresh session case.
- Ran a bunch of arc piped to itself, e.g. "arc list | arc list | arc list |
...". It succeeds up to the session limit, and above that gets failures as
expected.
- Manually checked the session table to make sure things seemed reasonable
there.
- Generally ran a bunch of arc commands.
- Logged out and logged in on the web interface.
Reviewers: btrahan, jungejason
Reviewed By: btrahan
CC: aran, btrahan
Maniphest Tasks: T687
Differential Revision: https://secure.phabricator.com/D1329
2012-01-05 17:55:21 -08:00
|
|
|
|
|
|
|
if ($conn_w->getAffectedRows()) {
|
|
|
|
// The update worked, so the session is valid.
|
|
|
|
break;
|
|
|
|
} else {
|
|
|
|
// We know this just got grabbed, so don't try it again.
|
|
|
|
unset($sessions[$establish_type]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (++$retries > $session_limit) {
|
|
|
|
throw new Exception("Failed to establish a session!");
|
|
|
|
}
|
|
|
|
}
|
2011-01-30 21:28:45 -08:00
|
|
|
|
Provide an activity log for login and administrative actions
Summary: This isn't complete, but I figured I'd ship it for review while it's still smallish.
Provide an activity log for high-level system actions (logins, admin actions). This basically allows two things to happen:
- The log itself is useful if there are shenanigans.
- Password login can check it and start CAPTCHA'ing users after a few failed attempts.
I'm going to change how the admin stuff works a little bit too, since right now you can make someone an agent, grab their certificate, revert them back to a normal user, and then act on their behalf over Conduit. This is a little silly, I'm going to move "agent" to the create workflow instead. I'll also add a confirm/email step to the administrative password reset flow.
Test Plan: Took various administrative and non-administrative actions, they appeared in the logs. Filtered the logs in a bunch of different ways.
Reviewers: jungejason, tuomaspelkonen, aran
CC:
Differential Revision: 302
2011-05-17 18:42:21 -07:00
|
|
|
$log = PhabricatorUserLog::newLog(
|
|
|
|
$this,
|
|
|
|
$this,
|
|
|
|
PhabricatorUserLog::ACTION_LOGIN);
|
|
|
|
$log->setDetails(
|
|
|
|
array(
|
|
|
|
'session_type' => $session_type,
|
|
|
|
'session_issued' => $establish_type,
|
|
|
|
));
|
|
|
|
$log->setSession($session_key);
|
|
|
|
$log->save();
|
|
|
|
|
2011-01-30 21:28:45 -08:00
|
|
|
return $session_key;
|
|
|
|
}
|
|
|
|
|
2011-09-08 14:16:59 -07:00
|
|
|
public function destroySession($session_key) {
|
|
|
|
$conn_w = $this->establishConnection('w');
|
|
|
|
queryfx(
|
|
|
|
$conn_w,
|
|
|
|
'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s',
|
|
|
|
self::SESSION_TABLE,
|
|
|
|
$this->getPHID(),
|
|
|
|
$session_key);
|
|
|
|
}
|
|
|
|
|
2012-05-07 10:29:33 -07:00
|
|
|
private function generateEmailToken(
|
|
|
|
PhabricatorUserEmail $email,
|
|
|
|
$offset = 0) {
|
|
|
|
|
|
|
|
$key = implode(
|
|
|
|
'-',
|
|
|
|
array(
|
|
|
|
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
|
|
|
|
$this->getPHID(),
|
|
|
|
$email->getVerificationCode(),
|
|
|
|
));
|
|
|
|
|
2011-01-31 11:55:26 -08:00
|
|
|
return $this->generateToken(
|
|
|
|
time() + ($offset * self::EMAIL_CYCLE_FREQUENCY),
|
|
|
|
self::EMAIL_CYCLE_FREQUENCY,
|
2012-05-07 10:29:33 -07:00
|
|
|
$key,
|
2011-01-31 11:55:26 -08:00
|
|
|
self::EMAIL_TOKEN_LENGTH);
|
|
|
|
}
|
|
|
|
|
2012-05-07 10:29:33 -07:00
|
|
|
public function validateEmailToken(
|
|
|
|
PhabricatorUserEmail $email,
|
|
|
|
$token) {
|
2011-01-31 11:55:26 -08:00
|
|
|
for ($ii = -1; $ii <= 1; $ii++) {
|
2012-05-07 10:29:33 -07:00
|
|
|
$valid = $this->generateEmailToken($email, $ii);
|
2011-01-31 11:55:26 -08:00
|
|
|
if ($token == $valid) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2012-05-07 10:29:33 -07:00
|
|
|
public function getEmailLoginURI(PhabricatorUserEmail $email = null) {
|
|
|
|
if (!$email) {
|
|
|
|
$email = $this->loadPrimaryEmail();
|
|
|
|
if (!$email) {
|
|
|
|
throw new Exception("User has no primary email!");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$token = $this->generateEmailToken($email);
|
Revise administrative workflow for user creation
Summary:
- When an administrator creates a user, provide an option to send a welcome
email. Right now this workflow kind of dead-ends.
- Prevent administrators from changing the "System Agent" flag. If they can
change it, they can grab another user's certificate and then act as them. This
is a vaguely weaker security policy than is exhibited elsewhere in the
application. Instead, make user accounts immutably normal users or system agents
at creation time.
- Prevent administrators from changing email addresses after account creation.
Same deal as conduit certs. The 'bin/accountadmin' script can still do this if a
user has a real problem.
- Prevent administrators from resetting passwords. There's no need for this
anymore with welcome emails plus email login and it raises the same issues.
Test Plan:
- Created a new account, selected "send welcome email", got a welcome email,
logged in with the link inside it.
- Created a new system agent.
- Reset an account's password.
Reviewed By: aran
Reviewers: tuomaspelkonen, jungejason, aran
CC: anjali, aran, epriestley
Differential Revision: 379
2011-05-30 14:59:17 -07:00
|
|
|
$uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/');
|
|
|
|
$uri = new PhutilURI($uri);
|
2012-05-07 10:29:33 -07:00
|
|
|
return $uri->alter('email', $email->getAddress());
|
|
|
|
}
|
|
|
|
|
2013-03-24 06:42:31 -07:00
|
|
|
public function attachUserProfile(PhabricatorUserProfile $profile) {
|
|
|
|
$this->profile = $profile;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loadUserProfile() {
|
|
|
|
if ($this->profile) {
|
|
|
|
return $this->profile;
|
|
|
|
}
|
|
|
|
|
|
|
|
$profile_dao = new PhabricatorUserProfile();
|
|
|
|
$this->profile = $profile_dao->loadOneWhere('userPHID = %s',
|
|
|
|
$this->getPHID());
|
|
|
|
|
|
|
|
if (!$this->profile) {
|
|
|
|
$profile_dao->setUserPHID($this->getPHID());
|
|
|
|
$this->profile = $profile_dao;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->profile;
|
|
|
|
}
|
|
|
|
|
2012-05-07 10:29:33 -07:00
|
|
|
public function loadPrimaryEmailAddress() {
|
|
|
|
$email = $this->loadPrimaryEmail();
|
|
|
|
if (!$email) {
|
|
|
|
throw new Exception("User has no primary email address!");
|
|
|
|
}
|
|
|
|
return $email->getAddress();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function loadPrimaryEmail() {
|
2012-05-25 13:11:33 -07:00
|
|
|
return $this->loadOneRelative(
|
|
|
|
new PhabricatorUserEmail(),
|
|
|
|
'userPHID',
|
|
|
|
'getPHID',
|
|
|
|
'(isPrimary = 1)');
|
Allow installs to require email verification
Summary:
Allow installs to require users to verify email addresses before they can use Phabricator. If a user logs in without a verified email address, they're given instructions to verify their address.
This isn't too useful on its own since we don't actually have arbitrary email registration, but the next step is to allow installs to restrict email to only some domains (e.g., @mycompany.com).
Test Plan:
- Verification
- Set verification requirement to `true`.
- Tried to use Phabricator with an unverified account, was told to verify.
- Tried to use Conduit, was given a verification error.
- Verified account, used Phabricator.
- Unverified account, reset password, verified implicit verification, used Phabricator.
- People Admin Interface
- Viewed as admin. Clicked "Administrate User".
- Viewed as non-admin
- Sanity Checks
- Used Conduit normally from web/CLI with a verified account.
- Logged in/out.
- Sent password reset email.
- Created a new user.
- Logged in with an unverified user but with the configuration set to off.
Reviewers: btrahan, vrana, jungejason
Reviewed By: btrahan
CC: aran, csilvers
Maniphest Tasks: T1184
Differential Revision: https://secure.phabricator.com/D2520
2012-05-21 12:47:38 -07:00
|
|
|
}
|
|
|
|
|
2011-03-30 19:21:09 -07:00
|
|
|
public function loadPreferences() {
|
|
|
|
if ($this->preferences) {
|
|
|
|
return $this->preferences;
|
|
|
|
}
|
|
|
|
|
|
|
|
$preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
|
|
|
|
'userPHID = %s',
|
|
|
|
$this->getPHID());
|
|
|
|
|
|
|
|
if (!$preferences) {
|
|
|
|
$preferences = new PhabricatorUserPreferences();
|
|
|
|
$preferences->setUserPHID($this->getPHID());
|
|
|
|
|
|
|
|
$default_dict = array(
|
|
|
|
PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
|
2012-01-16 11:08:54 -08:00
|
|
|
PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
|
2013-01-19 17:40:48 -08:00
|
|
|
PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '',
|
|
|
|
PhabricatorUserPreferences::PREFERENCE_DARK_CONSOLE => 0);
|
2011-03-30 19:21:09 -07:00
|
|
|
|
|
|
|
$preferences->setPreferences($default_dict);
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->preferences = $preferences;
|
|
|
|
return $preferences;
|
|
|
|
}
|
|
|
|
|
2012-04-04 17:53:16 -07:00
|
|
|
public function loadEditorLink($path, $line, $callsign) {
|
2012-01-16 11:08:54 -08:00
|
|
|
$editor = $this->loadPreferences()->getPreference(
|
|
|
|
PhabricatorUserPreferences::PREFERENCE_EDITOR);
|
2012-12-03 15:51:18 -08:00
|
|
|
|
|
|
|
if (is_array($path)) {
|
|
|
|
$multiedit = $this->loadPreferences()->getPreference(
|
|
|
|
PhabricatorUserPreferences::PREFERENCE_MULTIEDIT);
|
|
|
|
switch ($multiedit) {
|
|
|
|
case '':
|
|
|
|
$path = implode(' ', $path);
|
|
|
|
break;
|
|
|
|
case 'disable':
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-16 11:08:54 -08:00
|
|
|
if ($editor) {
|
|
|
|
return strtr($editor, array(
|
2012-04-10 00:55:31 -07:00
|
|
|
'%%' => '%',
|
2012-01-16 11:08:54 -08:00
|
|
|
'%f' => phutil_escape_uri($path),
|
|
|
|
'%l' => phutil_escape_uri($line),
|
2012-04-04 17:53:16 -07:00
|
|
|
'%r' => phutil_escape_uri($callsign),
|
2012-01-16 11:08:54 -08:00
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-10-23 13:25:52 -07:00
|
|
|
private static function tokenizeName($name) {
|
|
|
|
if (function_exists('mb_strtolower')) {
|
|
|
|
$name = mb_strtolower($name, 'UTF-8');
|
|
|
|
} else {
|
|
|
|
$name = strtolower($name);
|
|
|
|
}
|
|
|
|
$name = trim($name);
|
|
|
|
if (!strlen($name)) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
return preg_split('/\s+/', $name);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Populate the nametoken table, which used to fetch typeahead results. When
|
|
|
|
* a user types "linc", we want to match "Abraham Lincoln" from on-demand
|
|
|
|
* typeahead sources. To do this, we need a separate table of name fragments.
|
|
|
|
*/
|
|
|
|
public function updateNameTokens() {
|
|
|
|
$tokens = array_merge(
|
|
|
|
self::tokenizeName($this->getRealName()),
|
|
|
|
self::tokenizeName($this->getUserName()));
|
|
|
|
$tokens = array_unique($tokens);
|
|
|
|
$table = self::NAMETOKEN_TABLE;
|
|
|
|
$conn_w = $this->establishConnection('w');
|
|
|
|
|
|
|
|
$sql = array();
|
|
|
|
foreach ($tokens as $token) {
|
|
|
|
$sql[] = qsprintf(
|
|
|
|
$conn_w,
|
|
|
|
'(%d, %s)',
|
|
|
|
$this->getID(),
|
|
|
|
$token);
|
|
|
|
}
|
|
|
|
|
|
|
|
queryfx(
|
|
|
|
$conn_w,
|
|
|
|
'DELETE FROM %T WHERE userID = %d',
|
|
|
|
$table,
|
|
|
|
$this->getID());
|
|
|
|
if ($sql) {
|
|
|
|
queryfx(
|
|
|
|
$conn_w,
|
|
|
|
'INSERT INTO %T (userID, token) VALUES %Q',
|
|
|
|
$table,
|
|
|
|
implode(', ', $sql));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-01-06 10:44:12 -08:00
|
|
|
public function sendWelcomeEmail(PhabricatorUser $admin) {
|
|
|
|
$admin_username = $admin->getUserName();
|
|
|
|
$admin_realname = $admin->getRealName();
|
|
|
|
$user_username = $this->getUserName();
|
|
|
|
$is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
|
|
|
|
|
|
|
|
$base_uri = PhabricatorEnv::getProductionURI('/');
|
|
|
|
|
|
|
|
$uri = $this->getEmailLoginURI();
|
|
|
|
$body = <<<EOBODY
|
|
|
|
Welcome to Phabricator!
|
|
|
|
|
|
|
|
{$admin_username} ({$admin_realname}) has created an account for you.
|
|
|
|
|
|
|
|
Username: {$user_username}
|
|
|
|
|
|
|
|
To login to Phabricator, follow this link and set a password:
|
|
|
|
|
|
|
|
{$uri}
|
|
|
|
|
|
|
|
After you have set a password, you can login in the future by going here:
|
|
|
|
|
|
|
|
{$base_uri}
|
|
|
|
|
|
|
|
EOBODY;
|
|
|
|
|
|
|
|
if (!$is_serious) {
|
|
|
|
$body .= <<<EOBODY
|
|
|
|
|
|
|
|
Love,
|
|
|
|
Phabricator
|
|
|
|
|
|
|
|
EOBODY;
|
|
|
|
}
|
|
|
|
|
|
|
|
$mail = id(new PhabricatorMetaMTAMail())
|
|
|
|
->addTos(array($this->getPHID()))
|
|
|
|
->setSubject('[Phabricator] Welcome to Phabricator')
|
|
|
|
->setBody($body)
|
|
|
|
->setFrom($admin->getPHID())
|
|
|
|
->saveAndSend();
|
|
|
|
}
|
|
|
|
|
2012-06-06 07:09:56 -07:00
|
|
|
public function sendUsernameChangeEmail(
|
|
|
|
PhabricatorUser $admin,
|
|
|
|
$old_username) {
|
|
|
|
|
|
|
|
$admin_username = $admin->getUserName();
|
|
|
|
$admin_realname = $admin->getRealName();
|
|
|
|
$new_username = $this->getUserName();
|
|
|
|
|
|
|
|
$password_instructions = null;
|
|
|
|
if (PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
|
|
|
|
$uri = $this->getEmailLoginURI();
|
|
|
|
$password_instructions = <<<EOTXT
|
|
|
|
If you use a password to login, you'll need to reset it before you can login
|
|
|
|
again. You can reset your password by following this link:
|
|
|
|
|
|
|
|
{$uri}
|
|
|
|
|
|
|
|
And, of course, you'll need to use your new username to login from now on. If
|
|
|
|
you use OAuth to login, nothing should change.
|
|
|
|
|
|
|
|
EOTXT;
|
|
|
|
}
|
|
|
|
|
|
|
|
$body = <<<EOBODY
|
|
|
|
{$admin_username} ({$admin_realname}) has changed your Phabricator username.
|
|
|
|
|
|
|
|
Old Username: {$old_username}
|
|
|
|
New Username: {$new_username}
|
|
|
|
|
|
|
|
{$password_instructions}
|
|
|
|
EOBODY;
|
|
|
|
|
|
|
|
$mail = id(new PhabricatorMetaMTAMail())
|
|
|
|
->addTos(array($this->getPHID()))
|
|
|
|
->setSubject('[Phabricator] Username Changed')
|
|
|
|
->setBody($body)
|
|
|
|
->setFrom($admin->getPHID())
|
|
|
|
->saveAndSend();
|
|
|
|
}
|
|
|
|
|
2012-06-06 07:09:05 -07:00
|
|
|
public static function describeValidUsername() {
|
|
|
|
return 'Usernames must contain only numbers, letters, period, underscore '.
|
|
|
|
'and hyphen, and can not end with a period.';
|
|
|
|
}
|
|
|
|
|
2012-01-16 07:30:28 -08:00
|
|
|
public static function validateUsername($username) {
|
2012-06-13 08:39:02 -07:00
|
|
|
// NOTE: If you update this, make sure to update:
|
|
|
|
//
|
|
|
|
// - Remarkup rule for @mentions.
|
|
|
|
// - Routing rule for "/p/username/".
|
|
|
|
// - Unit tests, obviously.
|
|
|
|
// - describeValidUsername() method, above.
|
|
|
|
|
2012-06-06 07:09:05 -07:00
|
|
|
return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]$/', $username);
|
2012-01-16 07:30:28 -08:00
|
|
|
}
|
|
|
|
|
2012-04-27 17:44:10 -07:00
|
|
|
public static function getDefaultProfileImageURI() {
|
|
|
|
return celerity_get_resource_uri('/rsrc/image/avatar.png');
|
|
|
|
}
|
|
|
|
|
2013-03-24 06:42:31 -07:00
|
|
|
public function attachProfileImageURI($uri) {
|
|
|
|
$this->profileImage = $uri;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2012-04-27 17:44:10 -07:00
|
|
|
public function loadProfileImageURI() {
|
2013-03-24 06:42:31 -07:00
|
|
|
if ($this->profileImage) {
|
|
|
|
return $this->profileImage;
|
|
|
|
}
|
|
|
|
|
2012-04-27 17:44:10 -07:00
|
|
|
$src_phid = $this->getProfileImagePHID();
|
|
|
|
|
2013-01-02 14:03:36 -08:00
|
|
|
if ($src_phid) {
|
|
|
|
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid);
|
|
|
|
if ($file) {
|
2013-03-24 06:42:31 -07:00
|
|
|
$this->profileImage = $file->getBestURI();
|
2013-01-02 14:03:36 -08:00
|
|
|
}
|
2012-04-27 17:44:10 -07:00
|
|
|
}
|
|
|
|
|
2013-03-24 06:42:31 -07:00
|
|
|
if (!$this->profileImage) {
|
|
|
|
$this->profileImage = self::getDefaultProfileImageURI();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->profileImage;
|
2012-04-27 17:44:10 -07:00
|
|
|
}
|
|
|
|
|
2012-05-17 21:46:45 -07:00
|
|
|
public function getFullName() {
|
|
|
|
return $this->getUsername().' ('.$this->getRealName().')';
|
|
|
|
}
|
|
|
|
|
2012-06-14 18:08:06 -07:00
|
|
|
public function __toString() {
|
|
|
|
return $this->getUsername();
|
|
|
|
}
|
|
|
|
|
2012-05-07 10:29:33 -07:00
|
|
|
public static function loadOneWithEmailAddress($address) {
|
|
|
|
$email = id(new PhabricatorUserEmail())->loadOneWhere(
|
|
|
|
'address = %s',
|
|
|
|
$address);
|
|
|
|
if (!$email) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return id(new PhabricatorUser())->loadOneWhere(
|
|
|
|
'phid = %s',
|
|
|
|
$email->getUserPHID());
|
|
|
|
}
|
|
|
|
|
2013-02-28 11:01:40 -08:00
|
|
|
|
|
|
|
/* -( Omnipotence )-------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if this user is omnipotent. Omnipotent users bypass all policy
|
|
|
|
* checks.
|
|
|
|
*
|
|
|
|
* @return bool True if the user bypasses policy checks.
|
|
|
|
*/
|
|
|
|
public function isOmnipotent() {
|
|
|
|
return $this->omnipotent;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an omnipotent user object for use in contexts where there is no acting
|
|
|
|
* user, notably daemons.
|
|
|
|
*
|
|
|
|
* @return PhabricatorUser An omnipotent user.
|
|
|
|
*/
|
|
|
|
public static function getOmnipotentUser() {
|
|
|
|
static $user = null;
|
|
|
|
if (!$user) {
|
|
|
|
$user = new PhabricatorUser();
|
|
|
|
$user->omnipotent = true;
|
|
|
|
$user->makeEphemeral();
|
|
|
|
}
|
|
|
|
return $user;
|
|
|
|
}
|
|
|
|
|
2011-01-23 18:09:16 -08:00
|
|
|
}
|