Page MenuHomestyx hydra

No OneTemporary

diff --git a/scripts/user/account_admin.php b/scripts/user/account_admin.php
index 7e4f587c6a..52c09f2781 100755
--- a/scripts/user/account_admin.php
+++ b/scripts/user/account_admin.php
@@ -1,174 +1,174 @@
#!/usr/bin/env php
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
echo "Enter a username to create a new account or edit an existing account.";
$username = phutil_console_prompt("Enter a username:");
if (!strlen($username)) {
echo "Cancelled.\n";
exit(1);
}
if (!PhabricatorUser::validateUsername($username)) {
- echo "The username '{$username}' is invalid. Usernames must consist of only ".
- "numbers and letters.\n";
+ $valid = PhabricatorUser::describeValidUsername();
+ echo "The username '{$username}' is invalid. {$valid}\n";
exit(1);
}
$user = id(new PhabricatorUser())->loadOneWhere(
'username = %s',
$username);
if (!$user) {
$original = new PhabricatorUser();
echo "There is no existing user account '{$username}'.\n";
$ok = phutil_console_confirm(
"Do you want to create a new '{$username}' account?",
$default_no = false);
if (!$ok) {
echo "Cancelled.\n";
exit(1);
}
$user = new PhabricatorUser();
$user->setUsername($username);
$is_new = true;
} else {
$original = clone $user;
echo "There is an existing user account '{$username}'.\n";
$ok = phutil_console_confirm(
"Do you want to edit the existing '{$username}' account?",
$default_no = false);
if (!$ok) {
echo "Cancelled.\n";
exit(1);
}
$is_new = false;
}
$user_realname = $user->getRealName();
if (strlen($user_realname)) {
$realname_prompt = ' ['.$user_realname.']';
} else {
$realname_prompt = '';
}
$realname = nonempty(
phutil_console_prompt("Enter user real name{$realname_prompt}:"),
$user_realname);
$user->setRealName($realname);
// When creating a new user we prompt for an email address; when editing an
// existing user we just skip this because it would be quite involved to provide
// a reasonable CLI interface for editing multiple addresses and managing email
// verification and primary addresses.
$new_email = null;
if ($is_new) {
do {
$email = phutil_console_prompt("Enter user email address:");
$duplicate = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$email);
if ($duplicate) {
echo "ERROR: There is already a user with that email address. ".
"Each user must have a unique email address.\n";
} else {
break;
}
} while (true);
$new_email = $email;
}
$changed_pass = false;
// This disables local echo, so the user's password is not shown as they type
// it.
phutil_passthru('stty -echo');
$password = phutil_console_prompt(
"Enter a password for this user [blank to leave unchanged]:");
phutil_passthru('stty echo');
if (strlen($password)) {
$changed_pass = $password;
}
$is_admin = $user->getIsAdmin();
$set_admin = phutil_console_confirm(
'Should this user be an administrator?',
$default_no = !$is_admin);
echo "\n\nACCOUNT SUMMARY\n\n";
$tpl = "%12s %-30s %-30s\n";
printf($tpl, null, 'OLD VALUE', 'NEW VALUE');
printf($tpl, 'Username', $original->getUsername(), $user->getUsername());
printf($tpl, 'Real Name', $original->getRealName(), $user->getRealName());
if ($new_email) {
printf($tpl, 'Email', '', $new_email);
}
printf($tpl, 'Password', null,
($changed_pass !== false)
? 'Updated'
: 'Unchanged');
printf(
$tpl,
'Admin',
$original->getIsAdmin() ? 'Y' : 'N',
$user->getIsAdmin() ? 'Y' : 'N');
echo "\n";
if (!phutil_console_confirm("Save these changes?", $default_no = false)) {
echo "Cancelled.\n";
exit(1);
}
$user->openTransaction();
$editor = new PhabricatorUserEditor();
// TODO: This is wrong, but we have a chicken-and-egg problem when you use
// this script to create the first user.
$editor->setActor($user);
if ($new_email) {
$email = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(1);
$editor->createNewUser($user, $email);
} else {
$editor->updateUser($user);
}
$editor->makeAdminUser($user, $set_admin);
if ($changed_pass !== false) {
$editor->changePassword($user, $changed_pass);
}
$user->saveTransaction();
echo "Saved changes.\n";
diff --git a/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php b/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php
index 4d6a89f4eb..9c8c157690 100644
--- a/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php
+++ b/src/applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php
@@ -1,230 +1,230 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorOAuthDefaultRegistrationController
extends PhabricatorOAuthRegistrationController {
public function processRequest() {
$provider = $this->getOAuthProvider();
$oauth_info = $this->getOAuthInfo();
$request = $this->getRequest();
$errors = array();
$e_username = true;
$e_email = true;
$e_realname = true;
$user = new PhabricatorUser();
$user->setUsername($provider->retrieveUserAccountName());
$user->setRealName($provider->retrieveUserRealName());
$new_email = $provider->retrieveUserEmail();
if ($new_email) {
// If the user's OAuth provider account has an email address but the
// email address domain is not allowed by the Phabricator configuration,
// we just pretend the provider did not supply an address.
//
// For instance, if the user uses Google OAuth and their Google address
// is "joe@personal.com" but Phabricator is configured to require users
// use "@company.com" addresses, we show a prompt below and tell the user
// to provide their "@company.com" address. They can still use the OAuth
// account to login, they just need to associate their account with an
// allowed address.
//
// If the OAuth address is fine, we just use it and don't prompt the user.
if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$new_email = null;
}
}
$show_email_input = ($new_email === null);
if ($request->isFormPost()) {
$user->setUsername($request->getStr('username'));
$username = $user->getUsername();
if (!strlen($user->getUsername())) {
$e_username = 'Required';
$errors[] = 'Username is required.';
} else if (!PhabricatorUser::validateUsername($username)) {
$e_username = 'Invalid';
- $errors[] = 'Username must consist of only numbers and letters.';
+ $errors[] = PhabricatorUser::describeValidUsername();
} else {
$e_username = null;
}
if (!$new_email) {
$new_email = trim($request->getStr('email'));
if (!$new_email) {
$e_email = 'Required';
$errors[] = 'Email is required.';
} else {
$e_email = null;
}
}
if ($new_email) {
$email_ok = PhabricatorUserEmail::isAllowedAddress($new_email);
if (!$email_ok) {
$e_email = 'Invalid';
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
}
}
if (!strlen($user->getRealName())) {
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getRealName())) {
$e_realname = 'Required';
$errors[] = 'Real name is required.';
} else {
$e_realname = null;
}
}
if (!$errors) {
$image = $provider->retrieveUserProfileImage();
if ($image) {
$file = PhabricatorFile::newFromFileData(
$image,
array(
'name' => $provider->getProviderKey().'-profile.jpg',
'authorPHID' => $user->getPHID(),
));
$user->setProfileImagePHID($file->getPHID());
}
try {
// NOTE: We don't verify OAuth email addresses by default because
// OAuth providers might associate email addresses with accounts that
// haven't actually verified they own them. We could selectively
// auto-verify some providers that we trust here, but the stakes for
// verifying an email address are high because having a corporate
// address at a company is sometimes the key to the castle.
$email_obj = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
id(new PhabricatorUserEditor())
->setActor($user)
->createNewUser($user, $email_obj);
$oauth_info->setUserID($user->getID());
$oauth_info->save();
$session_key = $user->establishSession('web');
$request->setCookie('phusr', $user->getUsername());
$request->setCookie('phsid', $session_key);
$email_obj->sendVerificationEmail($user);
return id(new AphrontRedirectResponse())->setURI('/');
} catch (AphrontQueryDuplicateKeyException $exception) {
$same_username = id(new PhabricatorUser())->loadOneWhere(
'userName = %s',
$user->getUserName());
$same_email = id(new PhabricatorUserEmail())->loadOneWhere(
'address = %s',
$new_email);
if ($same_username) {
$e_username = 'Duplicate';
$errors[] = 'That username or email is not unique.';
} else if ($same_email) {
$e_email = 'Duplicate';
$errors[] = 'That email is not unique.';
} else {
throw $exception;
}
}
}
}
$error_view = null;
if ($errors) {
$error_view = new AphrontErrorView();
$error_view->setTitle('Registration Failed');
$error_view->setErrors($errors);
}
// Strip the URI down to the path, because otherwise we'll trigger
// external CSRF protection (by having a protocol in the form "action")
// and generate a form with no CSRF token.
$action_uri = new PhutilURI($provider->getRedirectURI());
$action_path = $action_uri->getPath();
$form = new AphrontFormView();
$form
->addHiddenInput('confirm_token', $provider->getAccessToken())
->addHiddenInput('expires', $oauth_info->getTokenExpires())
->addHiddenInput('state', $this->getOAuthState())
->setUser($request->getUser())
->setAction($action_path)
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username')
->setName('username')
->setValue($user->getUsername())
->setError($e_username));
if ($show_email_input) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setValue($request->getStr('email'))
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
}
if ($provider->retrieveUserRealName() === null) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Real Name')
->setName('realname')
->setValue($request->getStr('realname'))
->setError($e_realname));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Create Account'));
$panel = new AphrontPanelView();
$panel->setHeader('Create New Account');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return $this->buildStandardPageResponse(
array(
$error_view,
$panel,
),
array(
'title' => 'Create New Account',
));
}
}
diff --git a/src/applications/people/PhabricatorUserEditor.php b/src/applications/people/PhabricatorUserEditor.php
index e92b7b5198..ea34144a10 100644
--- a/src/applications/people/PhabricatorUserEditor.php
+++ b/src/applications/people/PhabricatorUserEditor.php
@@ -1,416 +1,421 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Editor class for creating and adjusting users. This class guarantees data
* integrity and writes logs when user information changes.
*
* @task config Configuration
* @task edit Creating and Editing Users
* @task role Editing Roles
* @task email Adding, Removing and Changing Email
* @task internal Internals
*/
final class PhabricatorUserEditor {
private $actor;
private $logs = array();
/* -( Configuration )------------------------------------------------------ */
/**
* @task config
*/
public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
/* -( Creating and Editing Users )----------------------------------------- */
/**
* @task edit
*/
public function createNewUser(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
if ($user->getID()) {
throw new Exception("User has already been created!");
}
if ($email->getID()) {
throw new Exception("Email has already been created!");
}
+ if (!PhabricatorUser::validateUsername($user->getUsername())) {
+ $valid = PhabricatorUser::describeValidUsername();
+ throw new Exception("Username is invalid! {$valid}");
+ }
+
// Always set a new user's email address to primary.
$email->setIsPrimary(1);
$this->willAddEmail($email);
$user->openTransaction();
try {
$user->save();
$email->setUserPHID($user->getPHID());
$email->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// We might have written the user but failed to write the email; if
// so, erase the IDs we attached.
$user->setID(null);
$user->setPHID(null);
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::newLog(
$this->actor,
$user,
PhabricatorUserLog::ACTION_CREATE);
$log->setNewValue($email->getAddress());
$log->save();
$user->saveTransaction();
return $this;
}
/**
* @task edit
*/
public function updateUser(PhabricatorUser $user) {
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$actor = $this->requireActor();
$user->openTransaction();
$user->save();
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_EDIT);
$log->save();
$user->saveTransaction();
return $this;
}
/**
* @task edit
*/
public function changePassword(PhabricatorUser $user, $password) {
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->reload();
$user->setPassword($password);
$user->save();
$log = PhabricatorUserLog::newLog(
$this->actor,
$user,
PhabricatorUserLog::ACTION_CHANGE_PASSWORD);
$log->save();
$user->saveTransaction();
}
/* -( Editing Roles )------------------------------------------------------ */
/**
* @task role
*/
public function makeAdminUser(PhabricatorUser $user, $admin) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsAdmin() == $admin) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_ADMIN);
$log->setOldValue($user->getIsAdmin());
$log->setNewValue($admin);
$user->setIsAdmin($admin);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task role
*/
public function disableUser(PhabricatorUser $user, $disable) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
if ($user->getIsDisabled() == $disable) {
$user->endWriteLocking();
$user->killTransaction();
return $this;
}
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_DISABLE);
$log->setOldValue($user->getIsDisabled());
$log->setNewValue($disable);
$user->setIsDisabled($disable);
$user->save();
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/* -( Adding, Removing and Changing Email )-------------------------------- */
/**
* @task email
*/
public function addEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if ($email->getID()) {
throw new Exception("Email has already been created!");
}
// Use changePrimaryEmail() to change primary email.
$email->setIsPrimary(0);
$email->setUserPHID($user->getPHID());
$this->willAddEmail($email);
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
try {
$email->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
$user->endWriteLocking();
$user->killTransaction();
throw $ex;
}
$log = PhabricatorUserLog::newLog(
$this->actor,
$user,
PhabricatorUserLog::ACTION_EMAIL_ADD);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task email
*/
public function removeEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if (!$email->getID()) {
throw new Exception("Email has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getIsPrimary()) {
throw new Exception("Can't remove primary email!");
}
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception("Email not owned by user!");
}
$email->delete();
$log = PhabricatorUserLog::newLog(
$this->actor,
$user,
PhabricatorUserLog::ACTION_EMAIL_REMOVE);
$log->setOldValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
return $this;
}
/**
* @task email
*/
public function changePrimaryEmail(
PhabricatorUser $user,
PhabricatorUserEmail $email) {
$actor = $this->requireActor();
if (!$user->getID()) {
throw new Exception("User has not been created yet!");
}
if (!$email->getID()) {
throw new Exception("Email has not been created yet!");
}
$user->openTransaction();
$user->beginWriteLocking();
$user->reload();
$email->reload();
if ($email->getUserPHID() != $user->getPHID()) {
throw new Exception("User does not own email!");
}
if ($email->getIsPrimary()) {
throw new Exception("Email is already primary!");
}
if (!$email->getIsVerified()) {
throw new Exception("Email is not verified!");
}
$old_primary = $user->loadPrimaryEmail();
if ($old_primary) {
$old_primary->setIsPrimary(0);
$old_primary->save();
}
$email->setIsPrimary(1);
$email->save();
$log = PhabricatorUserLog::newLog(
$actor,
$user,
PhabricatorUserLog::ACTION_EMAIL_PRIMARY);
$log->setOldValue($old_primary ? $old_primary->getAddress() : null);
$log->setNewValue($email->getAddress());
$log->save();
$user->endWriteLocking();
$user->saveTransaction();
if ($old_primary) {
$old_primary->sendOldPrimaryEmail($user, $email);
}
$email->sendNewPrimaryEmail($user);
return $this;
}
/* -( Internals )---------------------------------------------------------- */
/**
* @task internal
*/
private function requireActor() {
if (!$this->actor) {
throw new Exception("User edit requires actor!");
}
return $this->actor;
}
/**
* @task internal
*/
private function willAddEmail(PhabricatorUserEmail $email) {
// Hard check before write to prevent creation of disallowed email
// addresses. Normally, the application does checks and raises more
// user friendly errors for us, but we omit the courtesy checks on some
// pathways like administrative scripts for simplicity.
if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) {
throw new Exception(PhabricatorUserEmail::describeAllowedAddresses());
}
}
}
diff --git a/src/applications/people/controller/PhabricatorPeopleEditController.php b/src/applications/people/controller/PhabricatorPeopleEditController.php
index c2e314adf4..3db70ed7ea 100644
--- a/src/applications/people/controller/PhabricatorPeopleEditController.php
+++ b/src/applications/people/controller/PhabricatorPeopleEditController.php
@@ -1,501 +1,501 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorPeopleEditController
extends PhabricatorPeopleController {
public function shouldRequireAdmin() {
return true;
}
private $id;
private $view;
public function willProcessRequest(array $data) {
$this->id = idx($data, 'id');
$this->view = idx($data, 'view');
}
public function processRequest() {
$request = $this->getRequest();
$admin = $request->getUser();
if ($this->id) {
$user = id(new PhabricatorUser())->load($this->id);
if (!$user) {
return new Aphront404Response();
}
} else {
$user = new PhabricatorUser();
}
$views = array(
'basic' => 'Basic Information',
'role' => 'Edit Roles',
'cert' => 'Conduit Certificate',
);
if (!$user->getID()) {
$view = 'basic';
} else if (isset($views[$this->view])) {
$view = $this->view;
} else {
$view = 'basic';
}
$content = array();
if ($request->getStr('saved')) {
$notice = new AphrontErrorView();
$notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
$notice->setTitle('Changes Saved');
$notice->appendChild('<p>Your changes were saved.</p>');
$content[] = $notice;
}
switch ($view) {
case 'basic':
$response = $this->processBasicRequest($user);
break;
case 'role':
$response = $this->processRoleRequest($user);
break;
case 'cert':
$response = $this->processCertificateRequest($user);
break;
}
if ($response instanceof AphrontResponse) {
return $response;
}
$content[] = $response;
if ($user->getID()) {
$side_nav = new AphrontSideNavView();
$side_nav->appendChild($content);
foreach ($views as $key => $name) {
$side_nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/people/edit/'.$user->getID().'/'.$key.'/',
'class' => ($key == $view)
? 'aphront-side-nav-selected'
: null,
),
phutil_escape_html($name)));
}
$content = $side_nav;
}
return $this->buildStandardPageResponse(
$content,
array(
'title' => 'Edit User',
));
}
private function processBasicRequest(PhabricatorUser $user) {
$request = $this->getRequest();
$admin = $request->getUser();
$e_username = true;
$e_realname = true;
$e_email = true;
$errors = array();
$welcome_checked = true;
$new_email = null;
$request = $this->getRequest();
if ($request->isFormPost()) {
$welcome_checked = $request->getInt('welcome');
if (!$user->getID()) {
$user->setUsername($request->getStr('username'));
$new_email = $request->getStr('email');
if (!strlen($new_email)) {
$errors[] = 'Email is required.';
$e_email = 'Required';
} else if (!PhabricatorUserEmail::isAllowedAddress($new_email)) {
$e_email = 'Invalid';
$errors[] = PhabricatorUserEmail::describeAllowedAddresses();
} else {
$e_email = null;
}
if ($request->getStr('role') == 'agent') {
$user->setIsSystemAgent(true);
}
}
$user->setRealName($request->getStr('realname'));
if (!strlen($user->getUsername())) {
$errors[] = "Username is required.";
$e_username = 'Required';
} else if (!PhabricatorUser::validateUsername($user->getUsername())) {
- $errors[] = "Username must consist of only numbers and letters.";
+ $errors[] = PhabricatorUser::describeValidUsername();
$e_username = 'Invalid';
} else {
$e_username = null;
}
if (!strlen($user->getRealName())) {
$errors[] = 'Real name is required.';
$e_realname = 'Required';
} else {
$e_realname = null;
}
if (!$errors) {
try {
$is_new = !$user->getID();
if (!$is_new) {
id(new PhabricatorUserEditor())
->setActor($admin)
->updateUser($user);
} else {
$email = id(new PhabricatorUserEmail())
->setAddress($new_email)
->setIsVerified(0);
id(new PhabricatorUserEditor())
->setActor($admin)
->createNewUser($user, $email);
if ($welcome_checked) {
$user->sendWelcomeEmail($admin);
}
}
$response = id(new AphrontRedirectResponse())
->setURI('/people/edit/'.$user->getID().'/?saved=true');
return $response;
} catch (AphrontQueryDuplicateKeyException $ex) {
$errors[] = 'Username and email must be unique.';
$same_username = id(new PhabricatorUser())
->loadOneWhere('username = %s', $user->getUsername());
$same_email = id(new PhabricatorUserEmail())
->loadOneWhere('address = %s', $new_email);
if ($same_username) {
$e_username = 'Duplicate';
}
if ($same_email) {
$e_email = 'Duplicate';
}
}
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
$form = new AphrontFormView();
$form->setUser($admin);
if ($user->getID()) {
$form->setAction('/people/edit/'.$user->getID().'/');
} else {
$form->setAction('/people/edit/');
}
if ($user->getID()) {
$is_immutable = true;
} else {
$is_immutable = false;
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username')
->setName('username')
->setValue($user->getUsername())
->setError($e_username)
->setDisabled($is_immutable)
->setCaption('Usernames are permanent and can not be changed later!'))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Real Name')
->setName('realname')
->setValue($user->getRealName())
->setError($e_realname));
if (!$user->getID()) {
$form->appendChild(
id(new AphrontFormTextControl())
->setLabel('Email')
->setName('email')
->setDisabled($is_immutable)
->setValue($new_email)
->setCaption(PhabricatorUserEmail::describeAllowedAddresses())
->setError($e_email));
} else {
$email = $user->loadPrimaryEmail();
if ($email) {
$status = $email->getIsVerified() ? 'Verified' : 'Unverified';
} else {
$status = 'No Email Address';
}
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Email')
->setValue($status));
}
$form->appendChild($this->getRoleInstructions());
if (!$user->getID()) {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Role')
->setName('role')
->setValue('user')
->setOptions(
array(
'user' => 'Normal User',
'agent' => 'System Agent',
))
->setCaption(
'You can create a "system agent" account for bots, scripts, '.
'etc.'))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'welcome',
1,
'Send "Welcome to Phabricator" email.',
$welcome_checked));
} else {
$roles = array();
if ($user->getIsSystemAgent()) {
$roles[] = 'System Agent';
}
if ($user->getIsAdmin()) {
$roles[] = 'Admin';
}
if ($user->getIsDisabled()) {
$roles[] = 'Disabled';
}
if (!$roles) {
$roles[] = 'Normal User';
}
$roles = implode(', ', $roles);
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Roles')
->setValue($roles));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Save'));
$panel = new AphrontPanelView();
if ($user->getID()) {
$panel->setHeader('Edit User');
} else {
$panel->setHeader('Create New User');
}
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return array($error_view, $panel);
}
private function processRoleRequest(PhabricatorUser $user) {
$request = $this->getRequest();
$admin = $request->getUser();
$is_self = ($user->getID() == $admin->getID());
$errors = array();
if ($request->isFormPost()) {
$log_template = PhabricatorUserLog::newLog(
$admin,
$user,
null);
$logs = array();
if ($is_self) {
$errors[] = "You can not edit your own role.";
} else {
$new_admin = (bool)$request->getBool('is_admin');
$old_admin = (bool)$user->getIsAdmin();
if ($new_admin != $old_admin) {
id(new PhabricatorUserEditor())
->setActor($admin)
->makeAdminUser($user, $new_admin);
}
$new_disabled = (bool)$request->getBool('is_disabled');
$old_disabled = (bool)$user->getIsDisabled();
if ($new_disabled != $old_disabled) {
id(new PhabricatorUserEditor())
->setActor($admin)
->disableUser($user, $new_disabled);
}
}
if (!$errors) {
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI()->alter('saved', 'true'));
}
}
$error_view = null;
if ($errors) {
$error_view = id(new AphrontErrorView())
->setTitle('Form Errors')
->setErrors($errors);
}
$form = id(new AphrontFormView())
->setUser($admin)
->setAction($request->getRequestURI()->alter('saved', null));
if ($is_self) {
$form->appendChild(
'<p class="aphront-form-instructions">NOTE: You can not edit your own '.
'role.</p>');
}
$form
->appendChild($this->getRoleInstructions())
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'is_admin',
1,
'Administrator',
$user->getIsAdmin())
->setDisabled($is_self))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'is_disabled',
1,
'Disabled',
$user->getIsDisabled())
->setDisabled($is_self))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'is_agent',
1,
'System Agent (Bot/Script User)',
$user->getIsSystemAgent())
->setDisabled(true));
if (!$is_self) {
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Edit Role'));
}
$panel = new AphrontPanelView();
$panel->setHeader('Edit Role');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array($error_view, $panel);
}
private function processCertificateRequest($user) {
$request = $this->getRequest();
$admin = $request->getUser();
$form = new AphrontFormView();
$form
->setUser($admin)
->setAction($request->getRequestURI())
->appendChild(
'<p class="aphront-form-instructions">You can use this certificate '.
'to write scripts or bots which interface with Phabricator over '.
'Conduit.</p>');
if ($user->getIsSystemAgent()) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Username')
->setValue($user->getUsername()))
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Certificate')
->setValue($user->getConduitCertificate()));
} else {
$form->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Certificate')
->setValue(
'You may only view the certificates of System Agents.'));
}
$panel = new AphrontPanelView();
$panel->setHeader('Conduit Certificate');
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return array($panel);
}
private function getRoleInstructions() {
$roles_link = phutil_render_tag(
'a',
array(
'href' => PhabricatorEnv::getDoclink(
'article/User_Guide_Account_Roles.html'),
'target' => '_blank',
),
'User Guide: Account Roles');
return
'<p class="aphront-form-instructions">'.
'For a detailed explanation of account roles, see '.
$roles_link.'.'.
'</p>';
}
}
diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php
index a6055a66a8..98f513aff9 100644
--- a/src/applications/people/storage/PhabricatorUser.php
+++ b/src/applications/people/storage/PhabricatorUser.php
@@ -1,587 +1,592 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorUser extends PhabricatorUserDAO {
const SESSION_TABLE = 'phabricator_session';
const NAMETOKEN_TABLE = 'user_nametoken';
protected $phid;
protected $userName;
protected $realName;
protected $sex;
protected $passwordSalt;
protected $passwordHash;
protected $profileImagePHID;
protected $timezoneIdentifier = '';
protected $consoleEnabled = 0;
protected $consoleVisible = 0;
protected $consoleTab = '';
protected $conduitCertificate;
protected $isSystemAgent = 0;
protected $isAdmin = 0;
protected $isDisabled = 0;
private $preferences = null;
protected function readField($field) {
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);
}
}
public function getConfiguration() {
return array(
self::CONFIG_AUX_PHID => true,
self::CONFIG_PARTIAL_OBJECTS => true,
) + parent::getConfiguration();
}
public function generatePHID() {
return PhabricatorPHID::generateNewPHID(
PhabricatorPHIDConstants::PHID_TYPE_USER);
}
public function setPassword($password) {
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.");
}
if (!strlen($password)) {
$this->setPasswordHash('');
} else {
$this->setPasswordSalt(md5(mt_rand()));
$hash = $this->hashPassword($password);
$this->setPasswordHash($hash);
}
return $this;
}
public function isLoggedIn() {
return !($this->getPHID() === null);
}
public function save() {
if (!$this->getConduitCertificate()) {
$this->setConduitCertificate($this->generateConduitCertificate());
}
$result = parent::save();
$this->updateNameTokens();
PhabricatorSearchUserIndexer::indexUser($this);
return $result;
}
private function generateConduitCertificate() {
return Filesystem::readRandomCharacters(255);
}
public function comparePassword($password) {
if (!strlen($password)) {
return false;
}
if (!strlen($this->getPasswordHash())) {
return false;
}
$password = $this->hashPassword($password);
return ($password === $this->getPasswordHash());
}
private function hashPassword($password) {
$password = $this->getUsername().
$password.
$this->getPHID().
$this->getPasswordSalt();
for ($ii = 0; $ii < 1000; $ii++) {
$password = md5($password);
}
return $password;
}
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);
}
public function validateCSRFToken($token) {
if (!$this->getPHID()) {
return true;
}
// 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++) {
$valid = $this->getCSRFToken($ii);
if ($token == $valid) {
return true;
}
}
return false;
}
private function generateToken($epoch, $frequency, $key, $len) {
$time_block = floor($epoch / $frequency);
$vec = $this->getPHID().$this->getPasswordHash().$key.$time_block;
return substr(PhabricatorHash::digest($vec), 0, $len);
}
/**
* 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.
*/
public function establishSession($session_type) {
$conn_w = $this->establishConnection('w');
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!");
}
// 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);
// Load all the currently active sessions.
$sessions = queryfx_all(
$conn_w,
'SELECT type, sessionKey, sessionStart FROM %T
WHERE userPHID = %s AND type LIKE %>',
PhabricatorUser::SESSION_TABLE,
$this->getPHID(),
$session_type.'-');
$sessions = ipull($sessions, null, 'type');
$sessions = isort($sessions, 'sessionStart');
$existing_sessions = array_keys($sessions);
// UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
$retries = 0;
while (true) {
// 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;
}
}
// 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'];
}
// 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);
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!");
}
}
$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();
return $session_key;
}
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);
}
private function generateEmailToken(
PhabricatorUserEmail $email,
$offset = 0) {
$key = implode(
'-',
array(
PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
$this->getPHID(),
$email->getVerificationCode(),
));
return $this->generateToken(
time() + ($offset * self::EMAIL_CYCLE_FREQUENCY),
self::EMAIL_CYCLE_FREQUENCY,
$key,
self::EMAIL_TOKEN_LENGTH);
}
public function validateEmailToken(
PhabricatorUserEmail $email,
$token) {
for ($ii = -1; $ii <= 1; $ii++) {
$valid = $this->generateEmailToken($email, $ii);
if ($token == $valid) {
return true;
}
}
return false;
}
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);
$uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/');
$uri = new PhutilURI($uri);
return $uri->alter('email', $email->getAddress());
}
public function loadPrimaryEmailAddress() {
$email = $this->loadPrimaryEmail();
if (!$email) {
throw new Exception("User has no primary email address!");
}
return $email->getAddress();
}
public function loadPrimaryEmail() {
return $this->loadOneRelative(
new PhabricatorUserEmail(),
'userPHID',
'getPHID',
'(isPrimary = 1)');
}
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',
PhabricatorUserPreferences::PREFERENCE_EDITOR => '',
PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '');
$preferences->setPreferences($default_dict);
}
$this->preferences = $preferences;
return $preferences;
}
public function loadEditorLink($path, $line, $callsign) {
$editor = $this->loadPreferences()->getPreference(
PhabricatorUserPreferences::PREFERENCE_EDITOR);
if ($editor) {
return strtr($editor, array(
'%%' => '%',
'%f' => phutil_escape_uri($path),
'%l' => phutil_escape_uri($line),
'%r' => phutil_escape_uri($callsign),
));
}
}
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));
}
}
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();
}
+ public static function describeValidUsername() {
+ return 'Usernames must contain only numbers, letters, period, underscore '.
+ 'and hyphen, and can not end with a period.';
+ }
+
public static function validateUsername($username) {
- return (bool)preg_match('/^[a-zA-Z0-9]+$/', $username);
+ return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]$/', $username);
}
public static function getDefaultProfileImageURI() {
return celerity_get_resource_uri('/rsrc/image/avatar.png');
}
public function loadProfileImageURI() {
$src_phid = $this->getProfileImagePHID();
$file = id(new PhabricatorFile())->loadOneWhere('phid = %s', $src_phid);
if ($file) {
return $file->getBestURI();
}
return self::getDefaultProfileImageURI();
}
public function getFullName() {
return $this->getUsername().' ('.$this->getRealName().')';
}
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());
}
}
diff --git a/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php b/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
index 28165bf3ee..09c8437fee 100644
--- a/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
+++ b/src/applications/people/storage/__tests__/PhabricatorUserTestCase.php
@@ -1,44 +1,65 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class PhabricatorUserTestCase extends PhabricatorTestCase {
public function testUsernameValidation() {
$map = array(
'alincoln' => true,
'alincoln69' => true,
'hd3' => true,
- '7' => true, // Silly, but permitted.
- '0' => true,
'Alincoln' => true,
+ 'a.lincoln' => true,
'alincoln!' => false,
- ' alincoln' => false,
- '____' => false,
'' => false,
+
+ // These are silly, but permitted.
+ '7' => true,
+ '0' => true,
+ '____' => true,
+ '-' => true,
+
+ // These are not permitted because they make capturing @mentions
+ // ambiguous.
+ 'joe.' => false,
+
+ // We can never allow these because they invalidate usernames as tokens
+ // in commit messages ("Reviewers: alincoln, usgrant"), or as parameters
+ // in URIs ("/p/alincoln/", "?user=alincoln"), or make them unsafe in
+ // HTML. Theoretically we escape all the HTML/URI stuff, but these
+ // restrictions make attacks more difficult and are generally reasonable,
+ // since usernames like "<^, ,^>" don't seem very important to support.
+ '<script>' => false,
+ 'a lincoln' => false,
+ ' alincoln' => false,
+ 'alincoln ' => false,
+ 'a,lincoln' => false,
+ 'a&lincoln' => false,
+ 'a/lincoln' => false,
);
foreach ($map as $name => $expect) {
$this->assertEqual(
$expect,
PhabricatorUser::validateUsername($name),
"Validity of '{$name}'.");
}
}
}
diff --git a/src/infrastructure/markup/remarkup/markuprule/PhabricatorRemarkupRuleMention.php b/src/infrastructure/markup/remarkup/markuprule/PhabricatorRemarkupRuleMention.php
index 1e833a16ee..ba42697c42 100644
--- a/src/infrastructure/markup/remarkup/markuprule/PhabricatorRemarkupRuleMention.php
+++ b/src/infrastructure/markup/remarkup/markuprule/PhabricatorRemarkupRuleMention.php
@@ -1,145 +1,146 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @group markup
*/
final class PhabricatorRemarkupRuleMention
extends PhutilRemarkupRule {
const KEY_RULE_MENTION = 'rule.mention';
const KEY_RULE_MENTION_ORIGINAL = 'rule.mention.original';
const KEY_MENTIONED = 'phabricator.mentioned-user-phids';
- // NOTE: Negative lookahead for period prevents us from picking up email
- // addresses, while allowing constructs like "@tomo, lol". The negative
- // lookbehind for a word character prevents us from matching "mail@lists"
- // while allowing "@tomo/@mroch". The negative lookahead prevents us from
- // matching "@joe.com" while allowing us to match "hey, @joe.".
- const REGEX = '/(?<!\w)@([a-zA-Z0-9]+)\b(?![.]\w)/';
+ // NOTE: The negative lookbehind prevents matches like "mail@lists", while
+ // allowing constructs like "@tomo/@mroch". Since we now allow periods in
+ // usernames, we can't resonably distinguish that "@company.com" isn't a
+ // username, so we'll incorrectly pick it up, but there's little to be done
+ // about that. We forbid terminal periods so that we can correctly capture
+ // "@joe" instead of "@joe." in "Hey, @joe.".
+ const REGEX = '/(?<!\w)@([a-zA-Z0-9._-]*[a-zA-Z0-9_-])/';
public function apply($text) {
return preg_replace_callback(
self::REGEX,
array($this, 'markupMention'),
$text);
}
private function markupMention($matches) {
$engine = $this->getEngine();
$token = $engine->storeText('');
// Store the original text exactly so we can preserve casing if it doesn't
// resolve into a username.
$original_key = self::KEY_RULE_MENTION_ORIGINAL;
$original = $engine->getTextMetadata($original_key, array());
$original[$token] = $matches[1];
$engine->setTextMetadata($original_key, $original);
$metadata_key = self::KEY_RULE_MENTION;
$metadata = $engine->getTextMetadata($metadata_key, array());
$username = strtolower($matches[1]);
if (empty($metadata[$username])) {
$metadata[$username] = array();
}
$metadata[$username][] = $token;
$engine->setTextMetadata($metadata_key, $metadata);
return $token;
}
public function didMarkupText() {
$engine = $this->getEngine();
$metadata_key = self::KEY_RULE_MENTION;
$metadata = $engine->getTextMetadata($metadata_key, array());
if (empty($metadata)) {
// No mentions, or we already processed them.
return;
}
$original_key = self::KEY_RULE_MENTION_ORIGINAL;
$original = $engine->getTextMetadata($original_key, array());
$usernames = array_keys($metadata);
$user_table = new PhabricatorUser();
$real_user_names = queryfx_all(
$user_table->establishConnection('r'),
'SELECT username, phid, realName, isDisabled
FROM %T
WHERE username IN (%Ls)',
$user_table->getTableName(),
$usernames);
$actual_users = array();
$mentioned_key = self::KEY_MENTIONED;
$mentioned = $engine->getTextMetadata($mentioned_key, array());
foreach ($real_user_names as $row) {
$actual_users[strtolower($row['username'])] = $row;
$mentioned[$row['phid']] = $row['phid'];
}
$engine->setTextMetadata($mentioned_key, $mentioned);
foreach ($metadata as $username => $tokens) {
$exists = isset($actual_users[$username]);
if (!$exists) {
$class = 'phabricator-remarkup-mention-unknown';
} else if ($actual_users[$username]['isDisabled']) {
$class = 'phabricator-remarkup-mention-disabled';
} else {
$class = 'phabricator-remarkup-mention-exists';
}
if ($exists) {
$tag = phutil_render_tag(
'a',
array(
'class' => $class,
'href' => '/p/'.$actual_users[$username]['username'].'/',
'target' => '_blank',
'title' => $actual_users[$username]['realName'],
),
phutil_escape_html('@'.$actual_users[$username]['username']));
foreach ($tokens as $token) {
$engine->overwriteStoredText($token, $tag);
}
} else {
// NOTE: The structure here is different from the 'exists' branch,
// because we want to preserve the original text capitalization and it
// may differ for each token.
foreach ($tokens as $token) {
$tag = phutil_render_tag(
'span',
array(
'class' => $class,
),
phutil_escape_html('@'.idx($original, $token, $username)));
$engine->overwriteStoredText($token, $tag);
}
}
}
// Don't re-process these mentions.
$engine->setTextMetadata($metadata_key, array());
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Jul 27, 10:17 PM (1 w, 14 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
186244
Default Alt Text
(64 KB)

Event Timeline