diff --git a/.github/checksetup_answers.txt b/.github/checksetup_answers.txt
index 272c436c04..ae9d6810c0 100644
--- a/.github/checksetup_answers.txt
+++ b/.github/checksetup_answers.txt
@@ -1,3 +1,4 @@
+$answer{'ADMIN_LOGIN_NAME'} = 'admin_mozilla_bugs';
$answer{'ADMIN_EMAIL'} = 'admin@mozilla.bugs';
$answer{'ADMIN_OK'} = 'Y';
$answer{'ADMIN_PASSWORD'} = 'Te6Oovohch';
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index e76f59843a..d2bbac3cdc 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -182,6 +182,10 @@ sub extern_id_used {
|| $self->{_verifier}->extern_id_used;
}
+sub can_change_login {
+ return $_[0]->user_can_create_account;
+}
+
sub can_change_email {
return $_[0]->user_can_create_account;
}
@@ -474,6 +478,14 @@ Returns: C if users are allowed to create new Bugzilla accounts,
Description: Whether or not current login system uses extern_id.
+=item C
+
+Description: Whether or not the current login system allows users to
+ change their own login.
+Params: None
+Returns: C if users can change their own login,
+ C otherwise.
+
=item C
Description: Whether or not the current login system allows users to
diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm
index 1436b37d2b..c305dd8738 100644
--- a/Bugzilla/Auth/Verify.pm
+++ b/Bugzilla/Auth/Verify.pm
@@ -36,7 +36,8 @@ sub create_or_update_user {
my $dbh = Bugzilla->dbh;
my $extern_id = $params->{extern_id};
- my $username = $params->{bz_username} || $params->{username};
+ my $login = $params->{bz_username} || $params->{username};
+ my $email = $params->{email};
my $password = $params->{password} || '*';
my $real_name = $params->{realname} || '';
my $user_id = $params->{user_id};
@@ -44,7 +45,7 @@ sub create_or_update_user {
# A passed-in user_id always overrides anything else, for determining
# what account we should return.
if (!$user_id) {
- my $username_user_id = login_to_id($username || '');
+ my $login_user_id = login_to_id($login || '');
my $extern_user_id;
if ($extern_id) {
$extern_user_id = $dbh->selectrow_array(
@@ -55,27 +56,27 @@ sub create_or_update_user {
# If we have both a valid extern_id and a valid username, and they are
# not the same id, then we have a conflict.
- if ( $username_user_id
+ if ( $login_user_id
&& $extern_user_id
- && $username_user_id ne $extern_user_id)
+ && $login_user_id ne $extern_user_id)
{
my $extern_name = Bugzilla::User->new($extern_user_id)->login;
return {
failure => AUTH_ERROR,
error => "extern_id_conflict",
details =>
- {extern_id => $extern_id, extern_user => $extern_name, username => $username}
+ {extern_id => $extern_id, extern_user => $extern_name, username => $login}
};
}
# If we have a valid username, but no valid id,
# then we have to create the user. This happens when we're
# passed only a username, and that username doesn't exist already.
- if ($username && !$username_user_id && !$extern_user_id) {
- validate_email_syntax($username) || return {
+ if ($login && !$login_user_id && !$extern_user_id) {
+ validate_email_syntax($email) || return {
failure => AUTH_ERROR,
error => 'auth_invalid_email',
- details => {addr => $username}
+ details => {addr => $login}
};
# external authentication
@@ -83,23 +84,24 @@ sub create_or_update_user {
# XXX Theoretically this could fail with an error, but the fix for
# that is too involved to be done right now.
- my $user
- = Bugzilla::User->create({
- login_name => $username, cryptpassword => $password, realname => $real_name
- });
- $username_user_id = $user->id;
+ my $user = Bugzilla::User->create({
+ login_name => $login,
+ email => $email,
+ cryptpassword => $password,
+ realname => $real_name});
+ $login_user_id = $user->id;
}
# If we have a valid username id and an extern_id, but no valid
# extern_user_id, then we have to set the user's extern_id.
- if ($extern_id && $username_user_id && !$extern_user_id) {
+ if ($extern_id && $login_user_id && !$extern_user_id) {
$dbh->do('UPDATE profiles SET extern_id = ? WHERE userid = ?',
- undef, $extern_id, $username_user_id);
- Bugzilla->memcached->clear({table => 'profiles', id => $username_user_id});
+ undef, $extern_id, $login_user_id);
+ Bugzilla->memcached->clear({table => 'profiles', id => $login_user_id});
}
# Finally, at this point, one of these will give us a valid user id.
- $user_id = $extern_user_id || $username_user_id;
+ $user_id = $extern_user_id || $login_user_id;
}
# If we still don't have a valid user_id, then we weren't passed
@@ -117,13 +119,13 @@ sub create_or_update_user {
# Now that we have a valid User, we need to see if any data has to be
# updated.
my $user_updated = 0;
- if ($username && lc($user->login) ne lc($username)) {
- validate_email_syntax($username) || return {
+ if ($email && lc($user->email) ne lc($email)) {
+ validate_email_syntax($email) || return {
failure => AUTH_ERROR,
error => 'auth_invalid_email',
- details => {addr => $username}
+ details => {addr => $email}
};
- $user->set_login($username);
+ $user->set_email($email);
$user_updated = 1;
}
if ($real_name && $user->name ne $real_name) {
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index 14d72115a5..f1388a240f 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -79,7 +79,7 @@ sub check_regexp {
sub check_email {
my ($value) = @_;
my ($address) = Email::Address::XS->parse($value);
- if (!$address->is_valid) {
+ if (defined $address && !$address->is_valid) {
return "must be a valid email address.";
}
return "";
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 46512dec9a..a69e792e28 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -166,6 +166,7 @@ use Memoize;
MAX_SUDO_TOKEN_AGE
MAX_LOGIN_ATTEMPTS
LOGIN_LOCKOUT_INTERVAL
+ ACCOUNT_CHANGE_INTERVAL
MAX_STS_AGE
SAFE_PROTOCOLS
@@ -473,6 +474,10 @@ use constant MAX_LOGIN_ATTEMPTS => 5;
# account is locked.
use constant LOGIN_LOCKOUT_INTERVAL => 30;
+# The time in minutes a user must wait before they can request another email to
+# create a new account or change their password.
+use constant ACCOUNT_CHANGE_INTERVAL => 10;
+
# The maximum number of seconds the Strict-Transport-Security header
# will remain valid. BMO uses one year.
use constant MAX_STS_AGE => 31536000;
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index a11b9639ac..56c1639df3 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -1016,6 +1016,24 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ profiles_emails => {
+ FIELDS => [
+ id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
+ user_id => {
+ TYPE => 'INT3',
+ NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}
+ },
+ email => {TYPE => 'varchar(255)', NOTNULL => 1},
+ is_primary_email => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'},
+ display_order => {TYPE => 'INT1', NOTNULL => 1, DEFAULT => 1},
+ ],
+ INDEXES => [
+ profiles_emails_userid_idx => ['user_id'],
+ profiles_emails_email_idx => {FIELDS => ['email'], TYPE => 'UNIQUE'},
+ ],
+ },
+
profile_mfa => {
FIELDS => [
id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1},
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index c982c072ed..b76258c621 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -26,10 +26,12 @@ use Bugzilla::Error;
use Bugzilla::Group;
use Bugzilla::Product;
use Bugzilla::User;
+use Bugzilla::User::Email;
use Bugzilla::User::Setting;
use Bugzilla::Util qw(get_text);
use Bugzilla::Version;
+
use constant STATUS_WORKFLOW => (
[undef, 'UNCONFIRMED'],
[undef, 'CONFIRMED'],
@@ -420,16 +422,17 @@ sub create_admin {
return if $admin_count;
my %answer = %{Bugzilla->installation_answers};
- my $login = $answer{'ADMIN_EMAIL'};
+ my $login = $answer{'ADMIN_LOGIN_NAME'};
+ my $email = $answer{'ADMIN_EMAIL'};
my $password = $answer{'ADMIN_PASSWORD'};
my $full_name = $answer{'ADMIN_REALNAME'};
- if (!$login || !$password || !$full_name) {
+ if (!$login || !$email || !$password || !$full_name) {
print "\n" . get_text('install_admin_setup') . "\n\n";
}
while (!$login) {
- print get_text('install_admin_get_email') . ' ';
+ print get_text('install_admin_get_login_name') . ' ';
$login = ;
chomp $login;
eval { Bugzilla::User->check_login_name_for_creation($login); };
@@ -439,6 +442,17 @@ sub create_admin {
}
}
+ while (!$email) {
+ print get_text('install_admin_get_email') . ' ';
+ $email = ;
+ chomp $email;
+ eval { Bugzilla::User::Email->check_email_for_creation($email); };
+ if ($@) {
+ print $@ . "\n";
+ undef $email;
+ }
+ }
+
while (!defined $full_name) {
print get_text('install_admin_get_name') . ' ';
$full_name = ;
@@ -451,8 +465,9 @@ sub create_admin {
my $admin
= Bugzilla::User->create({
- login_name => $login, realname => $full_name, cryptpassword => $password
+ login_name => $login, email => $email, realname => $full_name, cryptpassword => $password
});
+
make_admin($admin);
}
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 0265b33507..7254407221 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -817,7 +817,8 @@ sub update_table_definitions {
$dbh->bz_alter_column('user_request_log', 'attach_id', {TYPE => 'INT5', NOTNULL => 0});
_populate_attachment_storage_class();
-
+ # Bug 1963773 - topunixguy@gmail.com
+ _copy_valid_emails_to_profiles_emails();
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
@@ -4393,6 +4394,64 @@ sub _populate_attachment_storage_class {
}
}
+sub _copy_valid_emails_to_profiles_emails {
+ my $dbh = Bugzilla->dbh;
+
+ my ($total) = $dbh->selectrow_array("SELECT COUNT(*) FROM profiles");
+ unless ($total) {
+ print "Skipping profiles_emails population: no profiles to process.\n";
+ return;
+ }
+
+ # Check if 'email' column exists in 'profiles'
+ my $columns = $dbh->selectcol_arrayref(
+ "SHOW COLUMNS FROM profiles LIKE 'email'"
+ );
+ my $has_email_column = scalar(@$columns) > 0;
+
+ # Build SELECT statement dynamically
+ my $select_sql = 'SELECT userid, login_name';
+ $select_sql .= ', email' if $has_email_column;
+ $select_sql .= ' FROM profiles';
+
+ my $select_sth = $dbh->prepare($select_sql);
+ my $check_sth = $dbh->prepare('SELECT 1 FROM profiles_emails WHERE user_id = ?');
+ my $insert_sth = $dbh->prepare('
+ INSERT INTO profiles_emails (user_id, email, is_primary_email, display_order)
+ VALUES (?, ?, 1, 1)
+ ');
+
+ $select_sth->execute();
+
+ while (my $row = $select_sth->fetchrow_hashref) {
+ my $user_id = $row->{userid};
+
+ # Skip if the user already has an entry
+ $check_sth->execute($user_id);
+ next if $check_sth->fetchrow_array;
+
+ my $email = $has_email_column ? $row->{email} : undef;
+ my $login = $row->{login_name};
+
+ my $valid_email;
+ if (defined $email && $email ne '' && validate_email_syntax($email)) {
+ $valid_email = $email;
+ } elsif (defined $login && $login ne '' && validate_email_syntax($login)) {
+ $valid_email = $login;
+ }
+
+ next unless defined $valid_email;
+
+ eval {
+ $insert_sth->execute($user_id, $valid_email);
+ };
+ warn "Failed to insert email for user $user_id: $@" if $@;
+ }
+
+ $select_sth->finish;
+ $check_sth->finish;
+ $insert_sth->finish;
+}
1;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 96de234de1..1632362a3a 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -96,44 +96,44 @@ sub check_auth_delegation_token {
# Creates and sends a token to create a new user account.
# It assumes that the login has the correct format and is not already in use.
sub issue_new_user_account_token {
- my $login_name = shift;
- my $dbh = Bugzilla->dbh;
- my $template = Bugzilla->template;
- my $vars = {};
-
- # Is there already a pending request for this login name? If yes, do not throw
- # an error because the user may have lost their email with the token inside.
- # But to prevent using this way to mailbomb an email address, make sure
- # the last request is at least 10 minutes old before sending a new email.
-
- my $pending_requests = $dbh->selectrow_array(
- 'SELECT COUNT(*)
+ my ($login, $email) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $template = Bugzilla->template;
+ my $vars = {};
+
+ # Is there already a pending request for this email? If yes, do not throw
+ # an error because the user may have lost their email with the token inside.
+ # But to prevent using this way to mailbomb an email address, make sure
+ # the last request is old enough before sending a new email (default: 10 minutes).
+
+ my $regexp = "^$email:";
+ my $pending_requests = $dbh->selectrow_array(
+ 'SELECT COUNT(*)
FROM tokens
WHERE tokentype = ?
- AND ' . $dbh->sql_istrcmp('eventdata', '?') . '
+ AND ' . $dbh->sql_regexp('eventdata', $dbh->quote($regexp)) . '
AND issuedate > '
- . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'), undef,
- ('account', $login_name)
- );
+ . $dbh->sql_date_math('NOW()', '-', ACCOUNT_CHANGE_INTERVAL, 'MINUTE'),
+ undef, 'account');
- ThrowUserError('too_soon_for_new_token', {'type' => 'account'})
- if $pending_requests;
+ ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests;
- my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
+ my ($token, $token_ts) = _create_token(undef, 'account', "$email:$login");
- $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
- $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
- $vars->{'token'} = $token;
+ $vars->{'login'} = $login;
+ $vars->{'email'} = $email;
+ $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+ $vars->{'token'} = $token;
- my $message;
- $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
- || ThrowTemplateError($template->error());
+ my $message;
+ $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
+ || ThrowTemplateError($template->error());
- # In 99% of cases, the user getting the confirmation email is the same one
- # who made the request, and so it is reasonable to send the email in the same
- # language used to view the "Create a New Account" page (we cannot use their
- # user prefs as the user has no account yet!).
- MessageToMTA($message);
+ # In 99% of cases, the user getting the confirmation email is the same one
+ # who made the request, and so it is reasonable to send the email in the same
+ # language used to view the "Create a New Account" page (we cannot use their
+ # user prefs as the user has no account yet!).
+ MessageToMTA($message);
}
sub IssueEmailChangeToken {
@@ -388,7 +388,18 @@ sub Cancel {
# is no entry in the 'profiles' table.
my $user = new Bugzilla::User($userid);
- $vars->{'emailaddress'} = $userid ? $user->email : $eventdata;
+
+ if ($userid) {
+ $vars->{'emailaddress'} = $user->email;
+ $vars->{'login'} = $user->login;
+ }
+ else {
+ # Be careful! Some logins may contain ":" in them.
+ my ($email, $login) = split(':', $eventdata, 2);
+ $vars->{'emailaddress'} = $email;
+ $vars->{'login'} = $login;
+ }
+
$vars->{'remoteaddress'} = remote_ip();
$vars->{'token'} = $token;
$vars->{'tokentype'} = $tokentype;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index 7ae4f68978..d817dacaf2 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -16,6 +16,7 @@ use Bugzilla::Util;
use Bugzilla::Constants;
use Bugzilla::Search::Recent;
use Bugzilla::User::Setting;
+use Bugzilla::User::Email;
use Bugzilla::Product;
use Bugzilla::Classification;
use Bugzilla::Field;
@@ -23,6 +24,7 @@ use Bugzilla::Group;
use Bugzilla::Hook;
use Bugzilla::BugUserLastVisit;
+
use DateTime::TimeZone;
use List::Util qw(max);
use List::MoreUtils qw(any);
@@ -154,6 +156,15 @@ sub new {
$param = {condition => 'extern_id = ?', values => [$param->{extern_id}]};
$_[0] = $param;
}
+ elsif (exists $param->{name}) {
+ my $email = $param->{name};
+
+ my $user_id = Bugzilla::User::Email->get_user_by_email($email);
+ if ($user_id) {
+ $param->{id} = $user_id;
+ delete $param->{name};
+ }
+ }
}
$user = $class->SUPER::new(@_);
@@ -326,18 +337,24 @@ sub check_login_name_for_creation {
my ($invocant, $name) = @_;
$name = trim($name);
$name || ThrowUserError('user_login_required');
- validate_email_syntax($name)
- || ThrowUserError('illegal_email_address', {addr => $name});
+
+ # No whitespace
+ $name !~ /\s/ || ThrowUserError('login_illegal_character');
+
+ # We set the max length to 127 to ensure logins aren't truncated when
+ # inserted into the tokens.eventdata field.
+ length($name) <= 127 or ThrowUserError('login_too_long');
# Check the name if it's a new user, or if we're changing the name.
if (!ref($invocant) || $invocant->login ne $name) {
is_available_username($name)
- || ThrowUserError('account_exists', {email => $name});
+ || ThrowUserError('account_exists', {login => $name});
}
return $name;
}
+
sub _check_password {
my ($self, $pass) = @_;
@@ -434,6 +451,19 @@ sub _generate_nickname {
return $nick;
}
+sub set_email {
+ my ($self, $email) = @_;
+ # Create a user email account
+ my $email_data = {
+ user_id => $self->id,
+ email => $email,
+ is_primary_email => 1
+ };
+
+ my $user_email = Bugzilla::User::Email->create($email_data);
+}
+
+
sub set_name {
my ($self, $name) = @_;
$self->set('realname', $name);
@@ -612,7 +642,7 @@ sub update_last_seen_date {
sub name { $_[0]->{realname}; }
sub login { $_[0]->{login_name}; }
sub extern_id { $_[0]->{extern_id}; }
-sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
+sub email { Bugzilla::User::Email->get_primary_email_of_user($_[0]->{userid}) || $_[0]->login;}
sub disabledtext { $_[0]->{'disabledtext'}; }
sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
sub showmybugslink { $_[0]->{showmybugslink}; }
@@ -2580,8 +2610,11 @@ sub create {
$dbh->bz_start_transaction();
$params->{nickname}
= _generate_nickname($params->{realname}, $params->{login_name}, 0);
+ my $email = exists $params->{email} ? delete $params->{email} : $params->{login_name};
my $user = $class->SUPER::create($params);
+ $user->set_email($email);
+
# Turn on all email for the new user
require Bugzilla::BugMail;
my %relationships = Bugzilla::BugMail::relationships();
@@ -2769,21 +2802,22 @@ sub check_account_creation_enabled {
}
sub check_and_send_account_creation_confirmation {
- my ($self, $login) = @_;
+ my ($self, $login, $email) = @_;
$login = $self->check_login_name_for_creation($login);
+ $email = Bugzilla::User::Email->check_email_for_creation($email);
my $creation_regexp = Bugzilla->params->{'createemailregexp'};
- if ($login !~ /$creation_regexp/i) {
+ if ($email !~ /$creation_regexp/i) {
ThrowUserError('account_creation_restricted');
}
# BMO - add a hook to allow extra validation prior to account creation.
- Bugzilla::Hook::process("user_verify_login", {login => $login});
+ Bugzilla::Hook::process("user_verify_login", {login => $login, email => $email});
# Create and send a token for this new account.
require Bugzilla::Token;
- Bugzilla::Token::issue_new_user_account_token($login);
+ Bugzilla::Token::issue_new_user_account_token($login, $email);
}
# This is used in a few performance-critical areas where we don't want to
diff --git a/Bugzilla/User/Email.pm b/Bugzilla/User/Email.pm
new file mode 100644
index 0000000000..2f1c336e11
--- /dev/null
+++ b/Bugzilla/User/Email.pm
@@ -0,0 +1,166 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::User::Email;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Object);
+
+use Bugzilla::Constants;
+use Bugzilla::Util;
+use Bugzilla::Error;
+use Scalar::Util qw(looks_like_number);
+
+#############
+# Constants #
+#############
+
+use constant DB_TABLE => 'profiles_emails';
+use constant NAME_FIELD => 'email';
+
+use constant DB_COLUMNS => qw(
+ id
+ user_id
+ email
+ is_primary_email
+ display_order
+);
+
+use constant VALIDATORS => {
+ user_id => \&_check_user_id,
+ email => \&check_email_for_creation,
+ is_primary_email => \&Bugzilla::Object::check_boolean,
+ display_order => \&_check_display_order,
+};
+
+use constant UPDATE_COLUMNS => qw(email is_primary_email display_order);
+
+# There's no gain to caching these objects
+use constant USE_MEMCACHED => 0;
+
+###############################
+#### Accessors ######
+###############################
+
+sub email { return $_[0]->{email}; }
+sub user_id { return $_[0]->{'user_id'}; }
+sub is_primary_email { return $_[0]->{'is_primary_email'}; }
+
+############
+# Mutators #
+############
+
+sub set_email { $_[0]->set('email', $_[1]); }
+sub set_primary_email { $_[0]->set('is_primary_email', $_[1]); }
+sub set_display_order { $_[0]->set('display_order', $_[1]); }
+
+###############################
+#### Constructors #####
+###############################
+
+sub new {
+ my ($class, $params) = @_;
+ my $user_email = $class->SUPER::new($params);
+ return $user_email;
+}
+
+sub create {
+ my ($class, $params) = @_;
+ my $user_email = $class->SUPER::create($params);
+
+ # Return the newly created user email account.
+ return $user_email;
+}
+
+sub update {
+ my ($class, $params) = @_;
+ my $updated_email = $class->SUPER::update();
+
+ # Return the updated user email account.
+ return $updated_email;
+}
+
+sub get_emails_by_user {
+ my ($class, $user_id) = @_;
+ my $dbh = Bugzilla->dbh;
+ my $emails_ref = $dbh->selectall_arrayref("SELECT email, is_primary_email, display_order FROM profiles_emails WHERE user_id = ?",
+ undef, $user_id);
+ return $emails_ref || [];
+}
+
+sub get_user_by_email {
+ my ($class, $email) = @_;
+ my $dbh = Bugzilla->dbh;
+ my ($user_id) = $dbh->selectrow_array("SELECT user_id FROM profiles_emails WHERE email = ? LIMIT 1", undef, $email);
+ # We use the defined-or operator because a user_id might be 0
+ return $user_id // 0;
+}
+
+sub get_primary_email_of_user {
+ my ($class, $user_id) = @_;
+ my $emails_ref = $class->get_emails_by_user($user_id);
+
+ for my $email_info (@$emails_ref) {
+ my ($email, $is_primary) = @$email_info;
+ if ($is_primary == 1) {
+ return $email;
+ }
+ }
+
+ return undef; # No primary email found
+}
+
+sub remove_from_db {
+ my $self = shift;
+ return $self->is_primary_email ? 0 : $self->SUPER::remove_from_db();
+}
+
+
+
+###############################
+### Validators ###
+###############################
+
+sub _check_user_id {
+ my ($invocant, $id) = @_;
+ require Bugzilla::User;
+ return Bugzilla::User->check({id => $id})->id;
+}
+
+sub check_email_for_creation {
+ my ($invocant, $email) = @_;
+
+ validate_email_syntax($email)
+ || ThrowUserError('illegal_email_address', {addr => $email});
+
+ if ($invocant->get_user_by_email($email)) {
+ ThrowUserError("email_exists", {'email' => $email})
+ }
+
+ return $email;
+}
+
+sub _check_display_order {
+ my ($invocant, $value) = @_;
+
+ unless (defined $value && looks_like_number($value)) {
+ return 0;
+ }
+ if ($value == int($value) && $value >= 1) {
+ return $value;
+ } else {
+ return 0;
+ }
+}
+
+
+1;
+
+__END__
diff --git a/conf/checksetup_answers.txt b/conf/checksetup_answers.txt
index 2c8e7e6b7c..e2bbc716d7 100644
--- a/conf/checksetup_answers.txt
+++ b/conf/checksetup_answers.txt
@@ -1,3 +1,4 @@
+$answer{'ADMIN_LOGIN_NAME'} = 'admin_bmo_test';
$answer{'ADMIN_EMAIL'} = 'admin@bmo.test';
$answer{'ADMIN_OK'} = 'Y';
$answer{'ADMIN_PASSWORD'} = 'password01!';
diff --git a/createaccount.cgi b/createaccount.cgi
index f0fd407305..2aa656e565 100644
--- a/createaccount.cgi
+++ b/createaccount.cgi
@@ -36,9 +36,11 @@ if (defined($login)) {
# the create account form.
my $token = $cgi->param('token');
check_hash_token($token, ['create_account']);
-
- $user->check_and_send_account_creation_confirmation($login);
+
+ my $email = $cgi->param('email');
+ $user->check_and_send_account_creation_confirmation($login, $email);
$vars->{'login'} = $login;
+ $vars->{'email'} = $email;
$template->process("account/created.html.tmpl", $vars)
|| ThrowTemplateError($template->error());
diff --git a/docs/en/rst/administering/users.rst b/docs/en/rst/administering/users.rst
index 064354bba5..4eface54c8 100644
--- a/docs/en/rst/administering/users.rst
+++ b/docs/en/rst/administering/users.rst
@@ -27,8 +27,7 @@ will appear in the Administration page.
The first screen is a search form to search for existing user
accounts. You can run searches based either on the user ID, real
-name or login name (i.e. the email address, or just the first part
-of the email address if the :param:`emailsuffix` parameter is set).
+name, login name or email address.
The search can be conducted
in different ways using the listbox to the right of the text entry
box. You can match by case-insensitive substring (the default),
@@ -56,11 +55,13 @@ Once you have found your user, you can change the following
fields:
- *Login Name*:
- This is generally the user's full email address. However, if you
- have are using the :param:`emailsuffix` parameter, this may
- just be the user's login name. Unless you turn off the
+ This is the user's login name, which we encourage to be different
+ from the user's email address and real name.
+
+- *Email Address*:
+ This is the user's full email address. Unless you turn off the
:param:`allowemailchange` parameter, users can change their
- login names themselves (to any valid email address).
+ email address themselves (to any valid email address).
- *Real Name*: The user's real name. Note that
Bugzilla does not require this to create an account.
diff --git a/editusers.cgi b/editusers.cgi
index 1a5b24979c..b2e89f3b17 100755
--- a/editusers.cgi
+++ b/editusers.cgi
@@ -77,15 +77,28 @@ elsif ($action eq 'list') {
my $matchstr = trim($cgi->param('matchstr'));
my $matchtype = $cgi->param('matchtype');
my $grouprestrict = $cgi->param('grouprestrict') || '0';
- my $query
- = 'SELECT DISTINCT userid, login_name, realname, is_enabled, '
- . $dbh->sql_date_format('last_seen_date', '%Y-%m-%d')
- . ' AS last_seen_date '
- . 'FROM profiles';
+
my @bindValues;
my $nextCondition;
my $visibleGroups;
+ my $select_fields = 'profiles.userid, profiles.login_name, profiles.realname, profiles.is_enabled, '
+ . $dbh->sql_date_format('profiles.last_seen_date', '%Y-%m-%d') . ' AS last_seen_date';
+
+ # Add email as a column from profiles_emails table
+ $select_fields .= ', profiles_emails.email AS email';
+
+ my $query = 'SELECT DISTINCT ' . $select_fields . ' FROM profiles';
+
+ # Join the two tables by userid
+ $query .= ' INNER JOIN profiles_emails ON profiles.userid = profiles_emails.user_id';
+
+ my $expr;
+ if ($matchvalue eq 'email') {
+ $expr = 'profiles_emails.email';
+ $nextCondition = 'WHERE';
+ }
+
# If a group ID is given, make sure it is a valid one.
my $group;
if ($grouprestrict) {
@@ -128,20 +141,21 @@ elsif ($action eq 'list') {
# Handle selection by login name, real name, or userid.
if (defined($matchtype)) {
$query .= " $nextCondition ";
- my $expr = "";
- if ($matchvalue eq 'userid') {
- if ($matchstr) {
- my $stored_matchstr = $matchstr;
- detaint_natural($matchstr)
- || ThrowUserError('illegal_user_id', {userid => $stored_matchstr});
+ if (!$expr) {
+ if ($matchvalue eq 'userid') {
+ if ($matchstr) {
+ my $stored_matchstr = $matchstr;
+ detaint_natural($matchstr)
+ || ThrowUserError('illegal_user_id', {userid => $stored_matchstr});
+ }
+ $expr = "profiles.userid";
+ }
+ elsif ($matchvalue eq 'realname') {
+ $expr = "profiles.realname";
+ }
+ else {
+ $expr = "profiles.login_name";
}
- $expr = "profiles.userid";
- }
- elsif ($matchvalue eq 'realname') {
- $expr = "profiles.realname";
- }
- else {
- $expr = "profiles.login_name";
}
if ($matchtype =~ /^(regexp|notregexp|exact)$/) {
@@ -176,9 +190,9 @@ elsif ($action eq 'list') {
}
$query .= ' ORDER BY profiles.login_name';
+
$vars->{'users'}
= $dbh->selectall_arrayref($query, {'Slice' => {}}, @bindValues);
-
}
if ($matchtype && $matchtype eq 'exact' && scalar(@{$vars->{'users'}}) == 1) {
diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm
index ff587d4785..2da60905dd 100644
--- a/extensions/BMO/Extension.pm
+++ b/extensions/BMO/Extension.pm
@@ -967,7 +967,7 @@ sub object_end_of_create {
my $user = $args->{object};
# Log real IP addresses for auditing
- Bugzilla->audit(sprintf('<%s> created user %s', remote_ip(), $user->login));
+ Bugzilla->audit(sprintf('<%s> created user %s', remote_ip() || 'unknown', $user->login));
# Add default searches to new user's footer
my $dbh = Bugzilla->dbh;
diff --git a/qa/config/checksetup_answers.txt b/qa/config/checksetup_answers.txt
index 79d9cb90c3..c8a9f73db6 100644
--- a/qa/config/checksetup_answers.txt
+++ b/qa/config/checksetup_answers.txt
@@ -1,3 +1,4 @@
+$answer{'ADMIN_LOGIN_NAME'} = 'admin_mozilla_bugs';
$answer{'ADMIN_EMAIL'} = 'admin@mozilla.bugs';
$answer{'ADMIN_OK'} = 'Y';
$answer{'ADMIN_PASSWORD'} = 'password';
diff --git a/scripts/entrypoint.pl b/scripts/entrypoint.pl
index dc5facf6a2..a61611c148 100755
--- a/scripts/entrypoint.pl
+++ b/scripts/entrypoint.pl
@@ -123,14 +123,14 @@ sub cmd_dev_httpd {
run(
'perl', 'scripts/generate_bmo_data.pl',
'--param' => 'use_mailer_queue=0',
- 'admin@bmo.test'
+ 'admin_bmo_test'
);
}
require Bugzilla;
my $answers = Bugzilla->installation_answers($ENV{BZ_ANSWERS_FILE});
my $BZ_URLBASE = $::ENV{'BMO_urlbase'};
- my $LOGIN_USER = "Admin user: $answers->{'ADMIN_EMAIL'}";
+ my $LOGIN_USER = "Admin user: $answers->{'ADMIN_LOGIN_NAME'}";
my $LOGIN_PASS = "Admin password: $answers->{'ADMIN_PASSWORD'}";
print <
+
- I need an email address and password to continue.
+ I need an email/login and password to continue.