diff options
Diffstat (limited to 'Bugzilla/User.pm')
-rw-r--r-- | Bugzilla/User.pm | 240 |
1 files changed, 165 insertions, 75 deletions
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 91defebb4..c1e3adee3 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -32,7 +32,7 @@ use URI; use URI::QueryParam; use parent qw(Bugzilla::Object Exporter); -@Bugzilla::User::EXPORT = qw(is_available_username +@Bugzilla::User::EXPORT = qw(is_available_email email_to_id login_to_id validate_password validate_password_check USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS MATCH_SKIP_CONFIRM @@ -50,12 +50,13 @@ use constant MATCH_SKIP_CONFIRM => 1; use constant DEFAULT_USER => { 'userid' => 0, - 'realname' => '', 'login_name' => '', + 'email' => '', + 'realname' => '', 'showmybugslink' => 0, 'disabledtext' => '', 'disable_mail' => 0, - 'is_enabled' => 1, + 'is_enabled' => 1, }; use constant DB_TABLE => 'profiles'; @@ -69,6 +70,7 @@ sub DB_COLUMNS { return ( 'profiles.userid', 'profiles.login_name', + 'profiles.email', 'profiles.realname', 'profiles.mybugslink AS showmybugslink', 'profiles.disabledtext', @@ -88,9 +90,10 @@ use constant VALIDATORS => { disable_mail => \&_check_disable_mail, disabledtext => \&_check_disabledtext, login_name => \&check_login_name, + email => \&check_email, realname => \&_check_realname, extern_id => \&_check_extern_id, - is_enabled => \&_check_is_enabled, + is_enabled => \&_check_is_enabled, }; sub UPDATE_COLUMNS { @@ -99,6 +102,7 @@ sub UPDATE_COLUMNS { disable_mail disabledtext login_name + email realname extern_id is_enabled @@ -108,7 +112,8 @@ sub UPDATE_COLUMNS { }; use constant VALIDATOR_DEPENDENCIES => { - is_enabled => ['disabledtext'], + is_enabled => ['disabledtext'], + login_name => ['email'], }; use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); @@ -222,7 +227,7 @@ sub update { my $dbh = Bugzilla->dbh; $self->_update_groups($group_changes, $changes); - if (exists $changes->{login_name}) { + if (exists $changes->{email}) { # Delete all the tokens related to the userid $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) unless $options->{keep_tokens}; @@ -231,15 +236,16 @@ sub update { } # Logout the user if necessary. - Bugzilla->logout_user($self) + Bugzilla->logout_user($self) if (!$options->{keep_session} && (exists $changes->{login_name} + || exists $changes->{email} || exists $changes->{disabledtext} || exists $changes->{cryptpassword})); # XXX Can update profiles_activity here as soon as it understands # field names like login_name. - + return $changes; } @@ -266,23 +272,53 @@ sub _check_extern_id { return $extern_id; } -# This is public since createaccount.cgi needs to use it before issuing -# a token for account creation. sub check_login_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError('user_login_required'); - check_email_syntax($name); + my ($invocant, $login, undef, $data) = @_; + + # No control characters + $login = clean_text($login); + $login || ThrowUserError('user_login_required'); + # No whitespace + $login !~ /\s/ || ThrowUserError('login_illegal_character'); + + # No @ sign unless login is email (VALIDATOR_DEPENDENCIES means + # this will be set already) + if ($login =~ /@/) { + my $email = ref($invocant) ? $invocant->email : $data->{email}; + # We should really use fc() instead of lc(), but this requires Perl 5.16. + ThrowUserError('login_at_sign_disallowed') unless lc($login) eq lc($email); + } + # We set the max length to 127 to ensure logins aren't truncated when + # inserted into the tokens.eventdata field. + length($login) <= 127 or ThrowUserError('login_too_long'); + + trick_taint($login); + + # Check the login name if it's a new user, or if we're changing the login name. + if (!ref($invocant) || lc($invocant->login) ne lc($login)) { + if (login_to_id($login)) { + ThrowUserError('account_exists', { login => $login }); + } + } - # Check the name if it's a new user, or if we're changing the name. - if (!ref($invocant) || lc($invocant->login) ne lc($name)) { - my @params = ($name); - push(@params, $invocant->login) if ref($invocant); - is_available_username(@params) - || ThrowUserError('account_exists', { email => $name }); + return $login; +} + +sub check_email { + my ($invocant, $email) = @_; + $email = clean_text($email); + $email || ThrowUserError('email_required'); + + check_email_syntax($email); + + # Check the email if it's a new user, or if we're changing the email. + my $old_email = ref($invocant) ? $invocant->email : undef; + if (!defined($old_email) || lc($old_email) ne lc($email)) { + is_available_email($email, $old_email) + || ThrowUserError('account_exists', { email => $email }); } - return $name; + return $email; } sub _check_password { @@ -325,6 +361,12 @@ sub set_login { delete $self->{nick}; } +sub set_email { + my ($self, $email) = @_; + $self->set('email', $email); + $self->set_login($email) if Bugzilla->params->{'use_email_as_login'}; +} + sub set_name { my ($self, $name) = @_; $self->set('realname', $name); @@ -470,7 +512,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 { $_[0]->{email}; } sub disabledtext { $_[0]->{'disabledtext'}; } sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } sub showmybugslink { $_[0]->{showmybugslink}; } @@ -502,14 +544,17 @@ sub authorizer { # Generate a string to identify the user by name + login if the user # has a name or by login only if they don't. +# +# See also get_userlist(), which constructs pseudo-Bugzilla::Users, including +# the 'identity' value. sub identity { my $self = shift; return "" unless $self->id; if (!defined $self->{identity}) { - $self->{identity} = - $self->name ? $self->name . " <" . $self->login. ">" : $self->login; + $self->{identity} = + $self->name ? $self->name . " (" . $self->login. ")" : $self->login; } return $self->{identity}; @@ -521,6 +566,7 @@ sub nick { return "" unless $self->id; if (!defined $self->{nick}) { + # This has the correct result even if the login does not contain an @. $self->{nick} = (split(/@/, $self->login, 2))[0]; } @@ -1730,8 +1776,8 @@ sub can_bless { } sub match { - # Generates a list of users whose login name (email address) or real name - # matches a substring or wildcard. + # Generates a list of users whose login name or real name matches a + # substring or wildcard. # This is also called if matches are disabled (for error checking), but # in this case only the exact match code will end up running. @@ -1780,8 +1826,10 @@ sub match { @users = @{Bugzilla::User->new_from_list($user_ids)}; } else { # try an exact match - # Exact matches don't care if a user is disabled. + # Exact matches don't care if a user is disabled, and match + # login names only. trick_taint($str); + my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles WHERE ' . $dbh->sql_istrcmp('login_name', '?'), undef, $str); @@ -2253,7 +2301,7 @@ sub get_userlist { while (my($login, $name, $visible) = $sth->fetchrow_array) { push @userlist, { login => $login, - identity => $name ? "$name <$login>" : $login, + identity => $name ? "$name ($login)" : $login, visible => $visible, }; } @@ -2375,17 +2423,15 @@ sub check_current_password { # Subroutines # ############### -sub is_available_username { - my ($username, $old_username) = @_; +sub is_available_email { + my ($email, $old_email) = @_; - if(login_to_id($username) != 0) { - return 0; - } + return 0 if email_to_id($email); my $dbh = Bugzilla->dbh; - # $username is safe because it is only used in SELECT placeholders. - trick_taint($username); - # Reject if the new login is part of an email change which is + # $email is safe because it is only used in SELECT placeholders. + trick_taint($email); + # Reject if the new email is part of an email change which is # still in progress # # substring/locate stuff: bug 165221; this used to use regexes, but that @@ -2401,13 +2447,13 @@ sub is_available_username { OR (tokentype = 'emailnew' AND SUBSTRING(eventdata, (" . $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)", - undef, ($username, $username)); + undef, ($email, $email)); if ($eventdata) { # Allow thru owner of token - if ($old_username - && (($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username") - || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username"))) + if ($old_email + && (($tokentype eq 'emailnew' && $eventdata eq "$old_email:$email") + || ($tokentype eq 'emailold' && $eventdata eq "$email:$old_email"))) { return 1; } @@ -2429,24 +2475,27 @@ sub check_account_creation_enabled { } sub check_and_send_account_creation_confirmation { - my ($self, $login) = @_; + my ($self, $login, $email) = @_; + my $class = ref($self) || $self; my $dbh = Bugzilla->dbh; $dbh->bz_start_transaction; - $login = $self->check_login_name($login); + $email = $class->check_email($email); + $login = $class->check_login_name($login, undef, { email => $email }); my $creation_regexp = Bugzilla->params->{'createemailregexp'}; - if ($login !~ /$creation_regexp/i) { + if ($email !~ /$creation_regexp/i) { ThrowUserError('account_creation_restricted'); } # Allow extensions to do extra checks. - Bugzilla::Hook::process('user_check_account_creation', { login => $login }); + Bugzilla::Hook::process('user_check_account_creation', + { 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); $dbh->bz_commit_transaction; } @@ -2458,20 +2507,17 @@ sub login_to_id { my $dbh = Bugzilla->dbh; my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {}; - # We cache lookups because this function showed up as taking up a + # We cache lookups because this function showed up as taking up a # significant amount of time in profiles of xt/search.t. However, - # for users that don't exist, we re-do the check every time, because - # otherwise we break is_available_username. + # for users that don't exist, we re-do the check every time. my $user_id; if (defined $cache->{$login}) { $user_id = $cache->{$login}; } else { - # No need to validate $login -- it will be used by the following SELECT - # statement only, so it's safe to simply trick_taint. trick_taint($login); $user_id = $dbh->selectrow_array( - "SELECT userid FROM profiles + "SELECT userid FROM profiles WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login); $cache->{$login} = $user_id; } @@ -2485,6 +2531,24 @@ sub login_to_id { } } +sub email_to_id { + my ($email, $throw_error) = @_; + my $dbh = Bugzilla->dbh; + trick_taint($email); + my $user_id = $dbh->selectrow_array("SELECT userid FROM profiles WHERE " . + $dbh->sql_istrcmp('email', '?'), + undef, $email); + if ($user_id) { + return $user_id; + } + elsif ($throw_error) { + ThrowUserError('invalid_email', { email => $email }); + } + else { + return 0; + } +} + sub validate_password { my $check = validate_password_check(@_); ThrowUserError($check) if $check; @@ -2532,14 +2596,15 @@ Bugzilla::User - Object for a Bugzilla user my $user = new Bugzilla::User($id); - my @get_selectable_classifications = + my @get_selectable_classifications = $user->get_selectable_classifications; # Class Functions - $user = Bugzilla::User->create({ - login_name => $username, - realname => $realname, - cryptpassword => $plaintext_password, + $user = Bugzilla::User->create({ + login_name => $username, + email => $email, + realname => $realname, + cryptpassword => $plaintext_password, disabledtext => $disabledtext, disable_mail => 0}); @@ -2595,6 +2660,27 @@ database changes and so on. =back +=head2 Validators + +=over + +=item C<check_email> + +Returns the sanitized email address if that email address is well formatted +and not already used by another user account. Else an error is thrown. + +=item C<check_login_name> + +Returns the sanitized login name if: + +1) it is not already used by another user account; and +2) it contains no forbidden characters, which means no whitespace characters; and +3) it contains no @ character (unless it exactly matches the email address of the account). + +Else an error is thrown. + +=back + =head2 Saved and Shared Queries =over @@ -2734,8 +2820,7 @@ Returns the login name for this user. =item C<email> -Returns the user's email address. Currently this is the same value as the -login. +Returns the user's email address. =item C<name> @@ -2754,10 +2839,10 @@ otherwise. =item C<nick> -Returns a user "nickname" -- i.e. a shorter, not-necessarily-unique name by -which to identify the user. Currently the part of the user's email address -before the at sign (@), but that could change, especially if we implement -usernames not dependent on email address. +Usually, this is an alias for C<login>. +If email addresses are used as logins, though, then this is the part of the +user's email address before the at sign (@). In this case, nicks are not +unique. =item C<authorizer> @@ -3141,7 +3226,8 @@ called "statically," just like a normal procedural function. The same as L<Bugzilla::Object/create>. Params: login_name - B<Required> The login name for the new user. - realname - The full name for the new user. + email - B<Required> The email address of the new user. + realname - The full name of the new user. cryptpassword - B<Required> The password for the new user. Even though the name says "crypt", you should just specify a plain-text password. If you specify '*', the user will not @@ -3162,30 +3248,29 @@ user with that username. Returns a C<Bugzilla::User> object. Checks that users can create new user accounts, and throws an error if user creation is disabled. -=item C<check_and_send_account_creation_confirmation($login)> +=item C<check_and_send_account_creation_confirmation($login, $email)> If the user request for a new account passes validation checks, an email is sent to this user for confirmation. Otherwise an error is thrown indicating why the request has been rejected. -=item C<is_available_username> +=item C<is_available_email> -Returns a boolean indicating whether or not the supplied username is +Returns a boolean indicating whether or not the supplied email address is already taken in Bugzilla. -Params: $username (scalar, string) - The full login name of the username - that you are checking. - $old_username (scalar, string) - If you are checking an email-change - token, insert the "old" username that the user is changing from, - here. Then, as long as it's the right user for that token, they - can change their username to $username. (That is, this function +Params: $email (scalar, string) - The email address that you are checking. + $old_email (scalar, string) - If you are checking an email-change + token, insert the "old" address that the user is changing from, + here. Then, as long as it's the right user for that token, he + can change his email to $email. (That is, this function will return a boolean true value). =item C<login_to_id($login, $throw_error)> Takes a login name of a Bugzilla user and changes that into a numeric ID for that user. This ID can then be passed to Bugzilla::User::new to -create a new user. +create a new user object. If no valid user exists with that login name, then the function returns 0. However, if $throw_error is set, the function will throw a user error @@ -3197,6 +3282,11 @@ of a user, but you don't want the full weight of Bugzilla::User. However, consider using a Bugzilla::User object instead of this function if you need more information about the user than just their ID. +=item C<email_to_id($email, $throw_error)> + +Same as C<login_to_id>, but operates on an email address instead of a login +name. + =item C<validate_password($passwd1, $passwd2)> Returns true if a password is valid (i.e. meets Bugzilla's @@ -3282,8 +3372,6 @@ L<Bugzilla|Bugzilla> =item groups_with_icon -=item check_login_name - =item set_extern_id =item mail_settings @@ -3300,6 +3388,8 @@ L<Bugzilla|Bugzilla> =item set_login +=item set_email + =item set_password =item last_seen_date |