| # 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; |
| |
| use 5.10.1; |
| use strict; |
| use warnings; |
| |
| use Bugzilla::Error; |
| use Bugzilla::Util; |
| use Bugzilla::Constants; |
| use Bugzilla::Search::Recent; |
| use Bugzilla::User::Setting; |
| use Bugzilla::Product; |
| use Bugzilla::Classification; |
| use Bugzilla::Field; |
| use Bugzilla::Group; |
| use Bugzilla::BugUserLastVisit; |
| use Bugzilla::Hook; |
| |
| use DateTime::TimeZone; |
| use List::Util qw(max); |
| use List::MoreUtils qw(any); |
| use Scalar::Util qw(blessed); |
| use Storable qw(dclone); |
| use URI; |
| use URI::QueryParam; |
| |
| use Data::Password::zxcvbn qw(password_strength); # WEBKIT_CHANGES |
| |
| use parent qw(Bugzilla::Object Exporter); |
| @Bugzilla::User::EXPORT = qw(is_available_username |
| login_to_id validate_password validate_password_check |
| USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS |
| MATCH_SKIP_CONFIRM |
| ); |
| |
| ##################################################################### |
| # Constants |
| ##################################################################### |
| |
| use constant USER_MATCH_MULTIPLE => -1; |
| use constant USER_MATCH_FAILED => 0; |
| use constant USER_MATCH_SUCCESS => 1; |
| |
| use constant MATCH_SKIP_CONFIRM => 1; |
| |
| use constant DEFAULT_USER => { |
| 'userid' => 0, |
| 'realname' => '', |
| 'login_name' => '', |
| 'showmybugslink' => 0, |
| 'disabledtext' => '', |
| 'disable_mail' => 0, |
| 'is_enabled' => 1, |
| }; |
| |
| use constant DB_TABLE => 'profiles'; |
| |
| # XXX Note that Bugzilla::User->name does not return the same thing |
| # that you passed in for "name" to new(). That's because historically |
| # Bugzilla::User used "name" for the realname field. This should be |
| # fixed one day. |
| sub DB_COLUMNS { |
| my $dbh = Bugzilla->dbh; |
| return ( |
| 'profiles.userid', |
| 'profiles.login_name', |
| 'profiles.realname', |
| 'profiles.mybugslink AS showmybugslink', |
| 'profiles.disabledtext', |
| 'profiles.disable_mail', |
| 'profiles.extern_id', |
| 'profiles.is_enabled', |
| $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date', |
| ), |
| } |
| |
| use constant NAME_FIELD => 'login_name'; |
| use constant ID_FIELD => 'userid'; |
| use constant LIST_ORDER => NAME_FIELD; |
| |
| use constant VALIDATORS => { |
| cryptpassword => \&_check_password, |
| disable_mail => \&_check_disable_mail, |
| disabledtext => \&_check_disabledtext, |
| login_name => \&check_login_name, |
| realname => \&_check_realname, |
| extern_id => \&_check_extern_id, |
| is_enabled => \&_check_is_enabled, |
| }; |
| |
| sub UPDATE_COLUMNS { |
| my $self = shift; |
| my @cols = qw( |
| disable_mail |
| disabledtext |
| login_name |
| realname |
| extern_id |
| is_enabled |
| ); |
| push(@cols, 'cryptpassword') if exists $self->{cryptpassword}; |
| return @cols; |
| }; |
| |
| use constant VALIDATOR_DEPENDENCIES => { |
| is_enabled => ['disabledtext'], |
| }; |
| |
| use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); |
| |
| ################################################################################ |
| # Functions |
| ################################################################################ |
| |
| sub new { |
| my $invocant = shift; |
| my $class = ref($invocant) || $invocant; |
| my ($param) = @_; |
| |
| my $user = { %{ DEFAULT_USER() } }; |
| bless ($user, $class); |
| return $user unless $param; |
| |
| if (ref($param) eq 'HASH') { |
| if (defined $param->{extern_id}) { |
| $param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] }; |
| $_[0] = $param; |
| } |
| } |
| return $class->SUPER::new(@_); |
| } |
| |
| sub super_user { |
| my $invocant = shift; |
| my $class = ref($invocant) || $invocant; |
| my ($param) = @_; |
| |
| my $user = { %{ DEFAULT_USER() } }; |
| $user->{groups} = [Bugzilla::Group->get_all]; |
| $user->{bless_groups} = [Bugzilla::Group->get_all]; |
| bless $user, $class; |
| return $user; |
| } |
| |
| sub _update_groups { |
| my $self = shift; |
| my $group_changes = shift; |
| my $changes = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| # Update group settings. |
| my $sth_add_mapping = $dbh->prepare( |
| qq{INSERT INTO user_group_map ( |
| user_id, group_id, isbless, grant_type |
| ) VALUES ( |
| ?, ?, ?, ? |
| ) |
| }); |
| my $sth_remove_mapping = $dbh->prepare( |
| qq{DELETE FROM user_group_map |
| WHERE user_id = ? |
| AND group_id = ? |
| AND isbless = ? |
| AND grant_type = ? |
| }); |
| |
| foreach my $is_bless (keys %$group_changes) { |
| my ($removed, $added) = @{$group_changes->{$is_bless}}; |
| |
| foreach my $group (@$removed) { |
| $sth_remove_mapping->execute( |
| $self->id, $group->id, $is_bless, GRANT_DIRECT |
| ); |
| } |
| foreach my $group (@$added) { |
| $sth_add_mapping->execute( |
| $self->id, $group->id, $is_bless, GRANT_DIRECT |
| ); |
| } |
| |
| if (! $is_bless) { |
| my $query = qq{ |
| INSERT INTO profiles_activity |
| (userid, who, profiles_when, fieldid, oldvalue, newvalue) |
| VALUES ( ?, ?, now(), ?, ?, ?) |
| }; |
| |
| $dbh->do( |
| $query, undef, |
| $self->id, Bugzilla->user->id, |
| get_field_id('bug_group'), |
| join(', ', map { $_->name } @$removed), |
| join(', ', map { $_->name } @$added) |
| ); |
| } |
| else { |
| # XXX: should create profiles_activity entries for blesser changes. |
| } |
| |
| Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id }); |
| |
| my $type = $is_bless ? 'bless_groups' : 'groups'; |
| $changes->{$type} = [ |
| [ map { $_->name } @$removed ], |
| [ map { $_->name } @$added ], |
| ]; |
| } |
| } |
| |
| sub update { |
| my $self = shift; |
| my $options = shift; |
| |
| my $group_changes = delete $self->{_group_changes}; |
| |
| my $changes = $self->SUPER::update(@_); |
| my $dbh = Bugzilla->dbh; |
| $self->_update_groups($group_changes, $changes); |
| |
| if (exists $changes->{login_name}) { |
| # Delete all the tokens related to the userid |
| $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) |
| unless $options->{keep_tokens}; |
| # And rederive regex groups |
| $self->derive_regexp_groups(); |
| } |
| |
| # Logout the user if necessary. |
| Bugzilla->logout_user($self) |
| if (!$options->{keep_session} |
| && (exists $changes->{login_name} |
| || exists $changes->{disabledtext} |
| || exists $changes->{cryptpassword})); |
| |
| # XXX Can update profiles_activity here as soon as it understands |
| # field names like login_name. |
| |
| return $changes; |
| } |
| |
| ################################################################################ |
| # Validators |
| ################################################################################ |
| |
| sub _check_disable_mail { return $_[1] ? 1 : 0; } |
| sub _check_disabledtext { return trim($_[1]) || ''; } |
| |
| # Check whether the extern_id is unique. |
| sub _check_extern_id { |
| my ($invocant, $extern_id) = @_; |
| $extern_id = trim($extern_id); |
| return undef unless defined($extern_id) && $extern_id ne ""; |
| if (!ref($invocant) || $invocant->extern_id ne $extern_id) { |
| my $existing_login = $invocant->new({ extern_id => $extern_id }); |
| if ($existing_login) { |
| ThrowUserError( 'extern_id_exists', |
| { extern_id => $extern_id, |
| existing_login_name => $existing_login->login }); |
| } |
| } |
| 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); |
| |
| # 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 $name; |
| } |
| |
| sub _check_password { |
| my ($self, $pass) = @_; |
| |
| # If the password is '*', do not encrypt it or validate it further--we |
| # are creating a user who should not be able to log in using DB |
| # authentication. |
| return $pass if $pass eq '*'; |
| |
| validate_password($pass); |
| my $cryptpassword = bz_crypt($pass); |
| return $cryptpassword; |
| } |
| |
| sub _check_realname { return trim($_[1]) || ''; } |
| |
| sub _check_is_enabled { |
| my ($invocant, $is_enabled, undef, $params) = @_; |
| # is_enabled is set automatically on creation depending on whether |
| # disabledtext is empty (enabled) or not empty (disabled). |
| # When updating the user, is_enabled is set by calling set_disabledtext(). |
| # Any value passed into this validator is ignored. |
| my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext}; |
| return $disabledtext ? 0 : 1; |
| } |
| |
| ################################################################################ |
| # Mutators |
| ################################################################################ |
| |
| sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); } |
| sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); } |
| sub set_extern_id { $_[0]->set('extern_id', $_[1]); } |
| |
| sub set_login { |
| my ($self, $login) = @_; |
| $self->set('login_name', $login); |
| delete $self->{identity}; |
| delete $self->{nick}; |
| } |
| |
| sub set_name { |
| my ($self, $name) = @_; |
| $self->set('realname', $name); |
| delete $self->{identity}; |
| } |
| |
| sub set_password { $_[0]->set('cryptpassword', $_[1]); } |
| |
| sub set_disabledtext { |
| $_[0]->set('disabledtext', $_[1]); |
| $_[0]->set('is_enabled', $_[1] ? 0 : 1); |
| } |
| |
| sub set_groups { |
| my $self = shift; |
| $self->_set_groups(GROUP_MEMBERSHIP, @_); |
| } |
| |
| sub set_bless_groups { |
| my $self = shift; |
| |
| # The person making the change needs to be in the editusers group |
| Bugzilla->user->in_group('editusers') |
| || ThrowUserError("auth_failure", {group => "editusers", |
| reason => "cant_bless", |
| action => "edit", |
| object => "users"}); |
| |
| $self->_set_groups(GROUP_BLESS, @_); |
| } |
| |
| sub _set_groups { |
| my $self = shift; |
| my $is_bless = shift; |
| my $changes = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| # The person making the change is $user, $self is the person being changed |
| my $user = Bugzilla->user; |
| |
| # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array |
| # is a list of group ids and/or names. |
| |
| # First turn the arrays into group objects. |
| $changes = $self->_set_groups_to_object($changes); |
| |
| # Get a list of the groups the user currently is a member of |
| my $ids = $dbh->selectcol_arrayref( |
| q{SELECT DISTINCT group_id |
| FROM user_group_map |
| WHERE user_id = ? AND isbless = ? AND grant_type = ?}, |
| undef, $self->id, $is_bless, GRANT_DIRECT); |
| |
| my $current_groups = Bugzilla::Group->new_from_list($ids); |
| my $new_groups = dclone($current_groups); |
| |
| # Record the changes |
| if (exists $changes->{set}) { |
| $new_groups = $changes->{set}; |
| |
| # We need to check the user has bless rights on the existing groups |
| # If they don't, then we need to add them back to new_groups |
| foreach my $group (@$current_groups) { |
| if (! $user->can_bless($group->id)) { |
| push @$new_groups, $group |
| unless grep { $_->id eq $group->id } @$new_groups; |
| } |
| } |
| } |
| else { |
| foreach my $group (@{$changes->{remove} // []}) { |
| @$new_groups = grep { $_->id ne $group->id } @$new_groups; |
| } |
| foreach my $group (@{$changes->{add} // []}) { |
| push @$new_groups, $group |
| unless grep { $_->id eq $group->id } @$new_groups; |
| } |
| } |
| |
| # Stash the changes, so self->update can actually make them |
| my @diffs = diff_arrays($current_groups, $new_groups, 'id'); |
| if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) { |
| $self->{_group_changes}{$is_bless} = \@diffs; |
| } |
| } |
| |
| sub _set_groups_to_object { |
| my $self = shift; |
| my $changes = shift; |
| my $user = Bugzilla->user; |
| |
| foreach my $key (keys %$changes) { |
| # Check we were given an array |
| unless (ref($changes->{$key}) eq 'ARRAY') { |
| ThrowCodeError( |
| 'param_invalid', |
| { param => $changes->{$key}, function => $key } |
| ); |
| } |
| |
| # Go through the array, and turn items into group objects |
| my @groups = (); |
| foreach my $value (@{$changes->{$key}}) { |
| my $type = $value =~ /^\d+$/ ? 'id' : 'name'; |
| my $group = Bugzilla::Group->new({$type => $value}); |
| |
| if (! $group || ! $user->can_bless($group->id)) { |
| ThrowUserError('auth_failure', |
| { group => $value, reason => 'cant_bless', |
| action => 'edit', object => 'users' }); |
| } |
| push @groups, $group; |
| } |
| $changes->{$key} = \@groups; |
| } |
| |
| return $changes; |
| } |
| |
| sub update_last_seen_date { |
| my $self = shift; |
| return unless $self->id; |
| my $dbh = Bugzilla->dbh; |
| my $date = $dbh->selectrow_array( |
| 'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); |
| |
| if (!$self->last_seen_date or $date ne $self->last_seen_date) { |
| $self->{last_seen_date} = $date; |
| # We don't use the normal update() routine here as we only |
| # want to update the last_seen_date column, not any other |
| # pending changes |
| $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?", |
| undef, $date, $self->id); |
| Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); |
| } |
| } |
| |
| ################################################################################ |
| # Methods |
| ################################################################################ |
| |
| # Accessors for user attributes |
| sub name { $_[0]->{realname}; } |
| sub login { $_[0]->{login_name}; } |
| sub extern_id { $_[0]->{extern_id}; } |
| sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; } |
| sub disabledtext { $_[0]->{'disabledtext'}; } |
| sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; } |
| sub showmybugslink { $_[0]->{showmybugslink}; } |
| sub email_disabled { $_[0]->{disable_mail}; } |
| sub email_enabled { !($_[0]->{disable_mail}); } |
| sub last_seen_date { $_[0]->{last_seen_date}; } |
| sub cryptpassword { |
| my $self = shift; |
| # We don't store it because we never want it in the object (we |
| # don't want to accidentally dump even the hash somewhere). |
| my ($pw) = Bugzilla->dbh->selectrow_array( |
| 'SELECT cryptpassword FROM profiles WHERE userid = ?', |
| undef, $self->id); |
| return $pw; |
| } |
| |
| sub set_authorizer { |
| my ($self, $authorizer) = @_; |
| $self->{authorizer} = $authorizer; |
| } |
| sub authorizer { |
| my ($self) = @_; |
| if (!$self->{authorizer}) { |
| require Bugzilla::Auth; |
| $self->{authorizer} = new Bugzilla::Auth(); |
| } |
| return $self->{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. |
| sub identity { |
| my $self = shift; |
| |
| return "" unless $self->id; |
| |
| if (!defined $self->{identity}) { |
| $self->{identity} = |
| $self->name ? $self->name . " <" . $self->login. ">" : $self->login; |
| } |
| |
| return $self->{identity}; |
| } |
| |
| sub nick { |
| my $self = shift; |
| |
| return "" unless $self->id; |
| |
| if (!defined $self->{nick}) { |
| $self->{nick} = (split(/@/, $self->login, 2))[0]; |
| } |
| |
| return $self->{nick}; |
| } |
| |
| sub queries { |
| my $self = shift; |
| return $self->{queries} if defined $self->{queries}; |
| return [] unless $self->id; |
| |
| my $dbh = Bugzilla->dbh; |
| my $query_ids = $dbh->selectcol_arrayref( |
| 'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id); |
| require Bugzilla::Search::Saved; |
| $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids); |
| |
| # We preload link_in_footer from here as this information is always requested. |
| # This only works if the user object represents the current logged in user. |
| Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id; |
| |
| return $self->{queries}; |
| } |
| |
| sub queries_subscribed { |
| my $self = shift; |
| return $self->{queries_subscribed} if defined $self->{queries_subscribed}; |
| return [] unless $self->id; |
| |
| # Exclude the user's own queries. |
| my @my_query_ids = map($_->id, @{$self->queries}); |
| my $query_id_string = join(',', @my_query_ids) || '-1'; |
| |
| # Only show subscriptions that we can still actually see. If a |
| # user changes the shared group of a query, our subscription |
| # will remain but we won't have access to the query anymore. |
| my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref( |
| "SELECT lif.namedquery_id |
| FROM namedqueries_link_in_footer lif |
| INNER JOIN namedquery_group_map ngm |
| ON ngm.namedquery_id = lif.namedquery_id |
| WHERE lif.user_id = ? |
| AND lif.namedquery_id NOT IN ($query_id_string) |
| AND " . $self->groups_in_sql, |
| undef, $self->id); |
| require Bugzilla::Search::Saved; |
| $self->{queries_subscribed} = |
| Bugzilla::Search::Saved->new_from_list($subscribed_query_ids); |
| return $self->{queries_subscribed}; |
| } |
| |
| sub queries_available { |
| my $self = shift; |
| return $self->{queries_available} if defined $self->{queries_available}; |
| return [] unless $self->id; |
| |
| # Exclude the user's own queries. |
| my @my_query_ids = map($_->id, @{$self->queries}); |
| my $query_id_string = join(',', @my_query_ids) || '-1'; |
| |
| my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref( |
| 'SELECT namedquery_id FROM namedquery_group_map |
| WHERE ' . $self->groups_in_sql . " |
| AND namedquery_id NOT IN ($query_id_string)"); |
| require Bugzilla::Search::Saved; |
| $self->{queries_available} = |
| Bugzilla::Search::Saved->new_from_list($avail_query_ids); |
| return $self->{queries_available}; |
| } |
| |
| sub tags { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| if (!defined $self->{tags}) { |
| # We must use LEFT JOIN instead of INNER JOIN as we may be |
| # in the process of inserting a new tag to some bugs, |
| # in which case there are no bugs with this tag yet. |
| $self->{tags} = $dbh->selectall_hashref( |
| 'SELECT name, id, COUNT(bug_id) AS bug_count |
| FROM tag |
| LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id |
| WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), |
| 'name', undef, $self->id); |
| } |
| return $self->{tags}; |
| } |
| |
| sub bugs_ignored { |
| my ($self) = @_; |
| my $dbh = Bugzilla->dbh; |
| if (!defined $self->{'bugs_ignored'}) { |
| $self->{'bugs_ignored'} = $dbh->selectall_arrayref( |
| 'SELECT bugs.bug_id AS id, |
| bugs.bug_status AS status, |
| bugs.short_desc AS summary |
| FROM bugs |
| INNER JOIN email_bug_ignore |
| ON bugs.bug_id = email_bug_ignore.bug_id |
| WHERE user_id = ?', |
| { Slice => {} }, $self->id); |
| # Go ahead and load these into the visible bugs cache |
| # to speed up can_see_bug checks later |
| $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]); |
| } |
| return $self->{'bugs_ignored'}; |
| } |
| |
| sub is_bug_ignored { |
| my ($self, $bug_id) = @_; |
| return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0; |
| } |
| |
| ########################## |
| # Saved Recent Bug Lists # |
| ########################## |
| |
| sub recent_searches { |
| my $self = shift; |
| $self->{recent_searches} ||= |
| Bugzilla::Search::Recent->match({ user_id => $self->id }); |
| return $self->{recent_searches}; |
| } |
| |
| sub recent_search_containing { |
| my ($self, $bug_id) = @_; |
| my $searches = $self->recent_searches; |
| |
| foreach my $search (@$searches) { |
| return $search if grep($_ == $bug_id, @{ $search->bug_list }); |
| } |
| |
| return undef; |
| } |
| |
| sub recent_search_for { |
| my ($self, $bug) = @_; |
| my $params = Bugzilla->input_params; |
| my $cgi = Bugzilla->cgi; |
| |
| if ($self->id) { |
| # First see if there's a list_id parameter in the query string. |
| my $list_id = $params->{list_id}; |
| if (!$list_id) { |
| # If not, check for "list_id" in the query string of the referer. |
| my $referer = $cgi->referer; |
| if ($referer) { |
| my $uri = URI->new($referer); |
| if ($uri->path =~ /buglist\.cgi$/) { |
| $list_id = $uri->query_param('list_id') |
| || $uri->query_param('regetlastlist'); |
| } |
| } |
| } |
| |
| if ($list_id && $list_id ne 'cookie') { |
| # If we got a bad list_id (either some other user's or an expired |
| # one) don't crash, just don't return that list. |
| my $search = Bugzilla::Search::Recent->check_quietly( |
| { id => $list_id }); |
| return $search if $search; |
| } |
| |
| # If there's no list_id, see if the current bug's id is contained |
| # in any of the user's saved lists. |
| my $search = $self->recent_search_containing($bug->id); |
| return $search if $search; |
| } |
| |
| # Finally (or always, if we're logged out), if there's a BUGLIST cookie |
| # and the selected bug is in the list, then return the cookie as a fake |
| # Search::Recent object. |
| if (my $list = $cgi->cookie('BUGLIST')) { |
| # Also split on colons, which was used as a separator in old cookies. |
| my @bug_ids = split(/[:-]/, $list); |
| if (grep { $_ == $bug->id } @bug_ids) { |
| my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids); |
| return $search; |
| } |
| } |
| |
| return undef; |
| } |
| |
| sub save_last_search { |
| my ($self, $params) = @_; |
| my ($bug_ids, $order, $vars, $list_id) = |
| @$params{qw(bugs order vars list_id)}; |
| |
| my $cgi = Bugzilla->cgi; |
| if ($order) { |
| $cgi->send_cookie(-name => 'LASTORDER', |
| -value => $order, |
| -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); |
| } |
| |
| return if !@$bug_ids; |
| |
| my $search; |
| if ($self->id) { |
| on_main_db { |
| if ($list_id) { |
| $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id }); |
| } |
| |
| if ($search) { |
| if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) { |
| $search->set_bug_list($bug_ids); |
| } |
| if (!$search->list_order || $order ne $search->list_order) { |
| $search->set_list_order($order); |
| } |
| $search->update(); |
| } |
| else { |
| # If we already have an existing search with a totally |
| # identical bug list, then don't create a new one. This |
| # prevents people from writing over their whole |
| # recent-search list by just refreshing a saved search |
| # (which doesn't have list_id in the header) over and over. |
| my $list_string = join(',', @$bug_ids); |
| my $existing_search = Bugzilla::Search::Recent->match({ |
| user_id => $self->id, bug_list => $list_string }); |
| |
| if (!scalar(@$existing_search)) { |
| $search = Bugzilla::Search::Recent->create({ |
| user_id => $self->id, |
| bug_list => $bug_ids, |
| list_order => $order }); |
| } |
| else { |
| $search = $existing_search->[0]; |
| } |
| } |
| }; |
| delete $self->{recent_searches}; |
| } |
| # Logged-out users use a cookie to store a single last search. We don't |
| # override that cookie with the logged-in user's latest search, because |
| # if they did one search while logged out and another while logged in, |
| # they may still want to navigate through the search they made while |
| # logged out. |
| else { |
| my $bug_list = join('-', @$bug_ids); |
| if (length($bug_list) < 4000) { |
| $cgi->send_cookie(-name => 'BUGLIST', |
| -value => $bug_list, |
| -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'); |
| } |
| else { |
| $cgi->remove_cookie('BUGLIST'); |
| $vars->{'toolong'} = 1; |
| } |
| } |
| return $search; |
| } |
| |
| sub reports { |
| my $self = shift; |
| return $self->{reports} if defined $self->{reports}; |
| return [] unless $self->id; |
| |
| my $dbh = Bugzilla->dbh; |
| my $report_ids = $dbh->selectcol_arrayref( |
| 'SELECT id FROM reports WHERE user_id = ?', undef, $self->id); |
| require Bugzilla::Report; |
| $self->{reports} = Bugzilla::Report->new_from_list($report_ids); |
| return $self->{reports}; |
| } |
| |
| sub flush_reports_cache { |
| my $self = shift; |
| |
| delete $self->{reports}; |
| } |
| |
| sub settings { |
| my ($self) = @_; |
| |
| return $self->{'settings'} if (defined $self->{'settings'}); |
| |
| # IF the user is logged in |
| # THEN get the user's settings |
| # ELSE get default settings |
| if ($self->id) { |
| $self->{'settings'} = get_all_settings($self->id); |
| } else { |
| $self->{'settings'} = get_defaults(); |
| } |
| |
| return $self->{'settings'}; |
| } |
| |
| sub setting { |
| my ($self, $name) = @_; |
| return $self->settings->{$name}->{'value'}; |
| } |
| |
| sub timezone { |
| my $self = shift; |
| |
| if (!defined $self->{timezone}) { |
| my $tz = $self->setting('timezone'); |
| if ($tz eq 'local') { |
| # The user wants the local timezone of the server. |
| $self->{timezone} = Bugzilla->local_timezone; |
| } |
| else { |
| $self->{timezone} = DateTime::TimeZone->new(name => $tz); |
| } |
| } |
| return $self->{timezone}; |
| } |
| |
| sub flush_queries_cache { |
| my $self = shift; |
| |
| delete $self->{queries}; |
| delete $self->{queries_subscribed}; |
| delete $self->{queries_available}; |
| } |
| |
| sub groups { |
| my $self = shift; |
| |
| return $self->{groups} if defined $self->{groups}; |
| return [] unless $self->id; |
| |
| my $user_groups_key = "user_groups." . $self->id; |
| my $groups = Bugzilla->memcached->get_config({ |
| key => $user_groups_key |
| }); |
| |
| if (!$groups) { |
| my $dbh = Bugzilla->dbh; |
| my $groups_to_check = $dbh->selectcol_arrayref( |
| "SELECT DISTINCT group_id |
| FROM user_group_map |
| WHERE user_id = ? AND isbless = 0", undef, $self->id); |
| |
| my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP; |
| my $membership_rows = Bugzilla->memcached->get_config({ |
| key => $grant_type_key, |
| }); |
| if (!$membership_rows) { |
| $membership_rows = $dbh->selectall_arrayref( |
| "SELECT DISTINCT grantor_id, member_id |
| FROM group_group_map |
| WHERE grant_type = " . GROUP_MEMBERSHIP); |
| Bugzilla->memcached->set_config({ |
| key => $grant_type_key, |
| data => $membership_rows, |
| }); |
| } |
| |
| my %group_membership; |
| foreach my $row (@$membership_rows) { |
| my ($grantor_id, $member_id) = @$row; |
| push (@{ $group_membership{$member_id} }, $grantor_id); |
| } |
| |
| # Let's walk the groups hierarchy tree (using FIFO) |
| # On the first iteration it's pre-filled with direct groups |
| # membership. Later on, each group can add its own members into the |
| # FIFO. Circular dependencies are eliminated by checking |
| # $checked_groups{$member_id} hash values. |
| # As a result, %groups will have all the groups we are the member of. |
| my %checked_groups; |
| my %groups; |
| while (scalar(@$groups_to_check) > 0) { |
| # Pop the head group from FIFO |
| my $member_id = shift @$groups_to_check; |
| |
| # Skip the group if we have already checked it |
| if (!$checked_groups{$member_id}) { |
| # Mark group as checked |
| $checked_groups{$member_id} = 1; |
| |
| # Add all its members to the FIFO check list |
| # %group_membership contains arrays of group members |
| # for all groups. Accessible by group number. |
| my $members = $group_membership{$member_id}; |
| my @new_to_check = grep(!$checked_groups{$_}, @$members); |
| push(@$groups_to_check, @new_to_check); |
| |
| $groups{$member_id} = 1; |
| } |
| } |
| $groups = [ keys %groups ]; |
| |
| Bugzilla->memcached->set_config({ |
| key => $user_groups_key, |
| data => $groups, |
| }); |
| } |
| |
| $self->{groups} = Bugzilla::Group->new_from_list($groups); |
| return $self->{groups}; |
| } |
| |
| sub last_visited { |
| my ($self, $ids) = @_; |
| |
| return Bugzilla::BugUserLastVisit->match({ user_id => $self->id, |
| $ids ? ( bug_id => $ids ) : () }); |
| } |
| |
| sub is_involved_in_bug { |
| my ($self, $bug) = @_; |
| my $user_id = $self->id; |
| my $user_login = $self->login; |
| |
| return unless $user_id; |
| return 1 if $user_id == $bug->assigned_to->id; |
| return 1 if $user_id == $bug->reporter->id; |
| |
| if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) { |
| return 1 if $user_id == $bug->qa_contact->id; |
| } |
| |
| return any { $user_login eq $_ } @{ $bug->cc }; |
| } |
| |
| # It turns out that calling ->id on objects a few hundred thousand |
| # times is pretty slow. (It showed up as a significant time contributor |
| # when profiling xt/search.t.) So we cache the group ids separately from |
| # groups for functions that need the group ids. |
| sub _group_ids { |
| my ($self) = @_; |
| $self->{group_ids} ||= [map { $_->id } @{ $self->groups }]; |
| return $self->{group_ids}; |
| } |
| |
| sub groups_as_string { |
| my $self = shift; |
| my $ids = $self->_group_ids; |
| return scalar(@$ids) ? join(',', @$ids) : '-1'; |
| } |
| |
| sub groups_in_sql { |
| my ($self, $field) = @_; |
| $field ||= 'group_id'; |
| my $ids = $self->_group_ids; |
| $ids = [-1] if !scalar @$ids; |
| return Bugzilla->dbh->sql_in($field, $ids); |
| } |
| |
| sub bless_groups { |
| my $self = shift; |
| |
| return $self->{'bless_groups'} if defined $self->{'bless_groups'}; |
| return [] unless $self->id; |
| |
| if ($self->in_group('editusers')) { |
| # Users having editusers permissions may bless all groups. |
| $self->{'bless_groups'} = [Bugzilla::Group->get_all]; |
| return $self->{'bless_groups'}; |
| } |
| |
| if (Bugzilla->params->{usevisibilitygroups} |
| && !@{ $self->visible_groups_inherited }) { |
| return []; |
| } |
| |
| my $dbh = Bugzilla->dbh; |
| |
| # Get all groups for the user where they have direct bless privileges. |
| my $query = " |
| SELECT DISTINCT group_id |
| FROM user_group_map |
| WHERE user_id = ? |
| AND isbless = 1"; |
| if (Bugzilla->params->{usevisibilitygroups}) { |
| $query .= " AND " |
| . $dbh->sql_in('group_id', $self->visible_groups_inherited); |
| } |
| |
| # Get all groups for the user where they are a member of a group that |
| # inherits bless privs. |
| my @group_ids = map { $_->id } @{ $self->groups }; |
| if (@group_ids) { |
| $query .= " |
| UNION |
| SELECT DISTINCT grantor_id |
| FROM group_group_map |
| WHERE grant_type = " . GROUP_BLESS . " |
| AND " . $dbh->sql_in('member_id', \@group_ids); |
| if (Bugzilla->params->{usevisibilitygroups}) { |
| $query .= " AND " |
| . $dbh->sql_in('grantor_id', $self->visible_groups_inherited); |
| } |
| } |
| |
| my $ids = $dbh->selectcol_arrayref($query, undef, $self->id); |
| return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids); |
| } |
| |
| sub in_group { |
| my ($self, $group, $product_id) = @_; |
| $group = $group->name if blessed $group; |
| if (scalar grep($_->name eq $group, @{ $self->groups })) { |
| return 1; |
| } |
| elsif ($product_id && detaint_natural($product_id)) { |
| # Make sure $group exists on a per-product basis. |
| return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); |
| |
| $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"}; |
| if (!defined $self->{"product_$product_id"}->{$group}) { |
| my $dbh = Bugzilla->dbh; |
| my $in_group = $dbh->selectrow_array( |
| "SELECT 1 |
| FROM group_control_map |
| WHERE product_id = ? |
| AND $group != 0 |
| AND " . $self->groups_in_sql . ' ' . |
| $dbh->sql_limit(1), |
| undef, $product_id); |
| |
| $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; |
| } |
| return $self->{"product_$product_id"}->{$group}; |
| } |
| # If we come here, then the user is not in the requested group. |
| return 0; |
| } |
| |
| sub in_group_id { |
| my ($self, $id) = @_; |
| return grep($_->id == $id, @{ $self->groups }) ? 1 : 0; |
| } |
| |
| # This is a helper to get all groups which have an icon to be displayed |
| # besides the name of the commenter. |
| sub groups_with_icon { |
| my $self = shift; |
| |
| return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }]; |
| } |
| |
| sub get_products_by_permission { |
| my ($self, $group) = @_; |
| # Make sure $group exists on a per-product basis. |
| return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES); |
| |
| my $product_ids = Bugzilla->dbh->selectcol_arrayref( |
| "SELECT DISTINCT product_id |
| FROM group_control_map |
| WHERE $group != 0 |
| AND " . $self->groups_in_sql); |
| |
| # No need to go further if the user has no "special" privs. |
| return [] unless scalar(@$product_ids); |
| my %product_map = map { $_ => 1 } @$product_ids; |
| |
| # We will restrict the list to products the user can see. |
| my $selectable_products = $self->get_selectable_products; |
| my @products = grep { $product_map{$_->id} } @$selectable_products; |
| return \@products; |
| } |
| |
| sub can_see_user { |
| my ($self, $otherUser) = @_; |
| my $query; |
| |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| # If the user can see no groups, then no users are visible either. |
| my $visibleGroups = $self->visible_groups_as_string() || return 0; |
| $query = qq{SELECT COUNT(DISTINCT userid) |
| FROM profiles, user_group_map |
| WHERE userid = ? |
| AND user_id = userid |
| AND isbless = 0 |
| AND group_id IN ($visibleGroups) |
| }; |
| } else { |
| $query = qq{SELECT COUNT(userid) |
| FROM profiles |
| WHERE userid = ? |
| }; |
| } |
| return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); |
| } |
| |
| sub can_edit_product { |
| my ($self, $prod_id) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| if (Bugzilla->params->{'or_groups'}) { |
| my $groups = $self->groups_as_string; |
| # For or-groups, we check if there are any can_edit groups for the |
| # product, and if the user is in any of them. If there are none or |
| # the user is in at least one of them, they can edit the product |
| my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array( |
| "SELECT SUM(p.cnt_can_edit), |
| SUM(p.cnt_group_member) |
| FROM (SELECT CASE WHEN canedit = 1 THEN 1 ELSE 0 END AS cnt_can_edit, |
| CASE WHEN canedit = 1 AND group_id IN ($groups) THEN 1 ELSE 0 END AS cnt_group_member |
| FROM group_control_map |
| WHERE product_id = $prod_id) AS p"); |
| return (!$cnt_can_edit or $cnt_group_member); |
| } |
| else { |
| # For and-groups, a user needs to be in all canedit groups. Therefore |
| # if the user is not in a can_edit group for the product, they cannot |
| # edit the product. |
| my $has_external_groups = |
| $dbh->selectrow_array('SELECT 1 |
| FROM group_control_map |
| WHERE product_id = ? |
| AND canedit != 0 |
| AND group_id NOT IN(' . $self->groups_as_string . ')', |
| undef, $prod_id); |
| |
| return !$has_external_groups; |
| } |
| } |
| |
| sub can_see_bug { |
| my ($self, $bug_id) = @_; |
| return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0; |
| } |
| |
| sub visible_bugs { |
| my ($self, $bugs) = @_; |
| # Allow users to pass in Bug objects and bug ids both. |
| my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; |
| |
| # We only check the visibility of bugs that we haven't |
| # checked yet. |
| # Bugzilla::Bug->update automatically removes updated bugs |
| # from the cache to force them to be checked again. |
| my $visible_cache = $self->{_visible_bugs_cache} ||= {}; |
| my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids); |
| |
| if (@check_ids) { |
| foreach my $id (@check_ids) { |
| my $orig_id = $id; |
| detaint_natural($id) |
| || ThrowCodeError('param_must_be_numeric', { param => $orig_id, |
| function => 'Bugzilla::User->visible_bugs'}); |
| } |
| |
| Bugzilla->params->{'or_groups'} |
| ? $self->_visible_bugs_check_or(\@check_ids) |
| : $self->_visible_bugs_check_and(\@check_ids); |
| } |
| |
| return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs]; |
| } |
| |
| sub _visible_bugs_check_or { |
| my ($self, $check_ids) = @_; |
| my $visible_cache = $self->{_visible_bugs_cache}; |
| my $dbh = Bugzilla->dbh; |
| my $user_id = $self->id; |
| |
| my $sth; |
| # Speed up the can_see_bug case. |
| if (scalar(@$check_ids) == 1) { |
| $sth = $self->{_sth_one_visible_bug}; |
| } |
| my $query = qq{ |
| SELECT DISTINCT bugs.bug_id |
| FROM bugs |
| LEFT JOIN bug_group_map AS security_map ON bugs.bug_id = security_map.bug_id |
| LEFT JOIN cc AS security_cc ON bugs.bug_id = security_cc.bug_id AND security_cc.who = $user_id |
| WHERE bugs.bug_id IN (} . join(',', ('?') x @$check_ids) . qq{) |
| AND ((security_map.group_id IS NULL OR security_map.group_id IN (} . $self->groups_as_string . qq{)) |
| OR (bugs.reporter_accessible = 1 AND bugs.reporter = $user_id) |
| OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL) |
| OR bugs.assigned_to = $user_id |
| }; |
| |
| if (Bugzilla->params->{'useqacontact'}) { |
| $query .= " OR bugs.qa_contact = $user_id"; |
| } |
| $query .= ')'; |
| |
| $sth ||= $dbh->prepare($query); |
| if (scalar(@$check_ids) == 1) { |
| $self->{_sth_one_visible_bug} = $sth; |
| } |
| |
| # Set all bugs as non visible |
| foreach my $bug_id (@$check_ids) { |
| $visible_cache->{$bug_id} = 0; |
| } |
| |
| # Now get the bugs the user can see |
| my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids); |
| foreach my $bug_id (@$visible_bug_ids) { |
| $visible_cache->{$bug_id} = 1; |
| } |
| } |
| |
| sub _visible_bugs_check_and { |
| my ($self, $check_ids) = @_; |
| my $visible_cache = $self->{_visible_bugs_cache}; |
| my $dbh = Bugzilla->dbh; |
| my $user_id = $self->id; |
| |
| my $sth; |
| # Speed up the can_see_bug case. |
| # WEBKIT_CHANGES: Disable statement caching |
| #if (scalar(@$check_ids) == 1) { |
| # $sth = $self->{_sth_one_visible_bug}; |
| #} |
| $sth ||= $dbh->prepare( |
| # This checks for groups that the bug is in that the user |
| # *isn't* in. Then, in the Perl code below, we check if |
| # the user can otherwise access the bug (for example, by being |
| # the assignee or QA Contact). |
| # |
| # The DISTINCT exists because the bug could be in *several* |
| # groups that the user isn't in, but they will all return the |
| # same result for bug_group_map.bug_id (so DISTINCT filters |
| # out duplicate rows). |
| "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact, |
| reporter_accessible, cclist_accessible, cc.who, |
| bug_group_map.bug_id |
| FROM bugs |
| LEFT JOIN cc |
| ON cc.bug_id = bugs.bug_id |
| AND cc.who = $user_id |
| LEFT JOIN bug_group_map |
| ON bugs.bug_id = bug_group_map.bug_id |
| AND bug_group_map.group_id NOT IN (" |
| . $self->groups_as_string . ') |
| WHERE bugs.bug_id IN (' . join(',', ('?') x @$check_ids) . ') |
| AND creation_ts IS NOT NULL '); |
| # WEBKIT_CHANGES: Disable statement caching |
| #if (scalar(@$check_ids) == 1) { |
| # $self->{_sth_one_visible_bug} = $sth; |
| #} |
| |
| $sth->execute(@$check_ids); |
| my $use_qa_contact = Bugzilla->params->{'useqacontact'}; |
| while (my $row = $sth->fetchrow_arrayref) { |
| my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, |
| $cclist_access, $isoncclist, $missinggroup) = @$row; |
| $visible_cache->{$bug_id} ||= |
| ((($reporter == $user_id) && $reporter_access) |
| || ($use_qa_contact |
| && $qacontact && ($qacontact == $user_id)) |
| || ($owner == $user_id) |
| || ($isoncclist && $cclist_access) |
| || !$missinggroup) ? 1 : 0; |
| } |
| |
| } |
| |
| sub clear_product_cache { |
| my $self = shift; |
| delete $self->{enterable_products}; |
| delete $self->{selectable_products}; |
| delete $self->{selectable_classifications}; |
| } |
| |
| sub can_see_product { |
| my ($self, $product_name) = @_; |
| |
| return scalar(grep {$_->name eq $product_name} @{$self->get_selectable_products}); |
| } |
| |
| sub get_selectable_products { |
| my $self = shift; |
| my $class_id = shift; |
| my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; |
| |
| if (!defined $self->{selectable_products}) { |
| my $query = "SELECT id |
| FROM products |
| LEFT JOIN group_control_map |
| ON group_control_map.product_id = products.id |
| AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY; |
| |
| if (Bugzilla->params->{'or_groups'}) { |
| # Either the user is in at least one of the MANDATORY groups, or |
| # there are no such groups for the product. |
| $query .= " WHERE group_id IN (" . $self->groups_as_string . ") |
| OR group_id IS NULL"; |
| } |
| else { |
| # There must be no MANDATORY groups that the user is not in. |
| $query .= " AND group_id NOT IN (" . $self->groups_as_string . ") |
| WHERE group_id IS NULL"; |
| } |
| |
| my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query); |
| $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids); |
| } |
| |
| # Restrict the list of products to those being in the classification, if any. |
| if ($class_restricted) { |
| return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}]; |
| } |
| # If we come here, then we want all selectable products. |
| return $self->{selectable_products}; |
| } |
| |
| sub get_selectable_classifications { |
| my ($self) = @_; |
| |
| if (!defined $self->{selectable_classifications}) { |
| my $products = $self->get_selectable_products; |
| my %class_ids = map { $_->classification_id => 1 } @$products; |
| |
| $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]); |
| } |
| return $self->{selectable_classifications}; |
| } |
| |
| sub can_enter_product { |
| my ($self, $input, $warn) = @_; |
| my $dbh = Bugzilla->dbh; |
| $warn ||= 0; |
| |
| $input = trim($input) if !ref $input; |
| if (!defined $input or $input eq '') { |
| return unless $warn == THROW_ERROR; |
| ThrowUserError('object_not_specified', |
| { class => 'Bugzilla::Product' }); |
| } |
| |
| if (!scalar @{ $self->get_enterable_products }) { |
| return unless $warn == THROW_ERROR; |
| ThrowUserError('no_products'); |
| } |
| |
| my $product = blessed($input) ? $input |
| : new Bugzilla::Product({ name => $input }); |
| my $can_enter = |
| $product && grep($_->name eq $product->name, |
| @{ $self->get_enterable_products }); |
| |
| return $product if $can_enter; |
| |
| return 0 unless $warn == THROW_ERROR; |
| |
| # Check why access was denied. These checks are slow, |
| # but that's fine, because they only happen if we fail. |
| |
| # We don't just use $product->name for error messages, because if it |
| # changes case from $input, then that's a clue that the product does |
| # exist but is hidden. |
| my $name = blessed($input) ? $input->name : $input; |
| |
| # The product could not exist or you could be denied... |
| if (!$product || !$product->user_has_access($self)) { |
| ThrowUserError('entry_access_denied', { product => $name }); |
| } |
| # It could be closed for bug entry... |
| elsif (!$product->is_active) { |
| ThrowUserError('product_disabled', { product => $product }); |
| } |
| # It could have no components... |
| elsif (!@{$product->components} |
| || !grep { $_->is_active } @{$product->components}) |
| { |
| ThrowUserError('missing_component', { product => $product }); |
| } |
| # It could have no versions... |
| elsif (!@{$product->versions} |
| || !grep { $_->is_active } @{$product->versions}) |
| { |
| ThrowUserError ('missing_version', { product => $product }); |
| } |
| |
| die "can_enter_product reached an unreachable location."; |
| } |
| |
| sub get_enterable_products { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| if (defined $self->{enterable_products}) { |
| return $self->{enterable_products}; |
| } |
| |
| # All products which the user has "Entry" access to. |
| my $query = |
| 'SELECT products.id FROM products |
| LEFT JOIN group_control_map |
| ON group_control_map.product_id = products.id |
| AND group_control_map.entry != 0'; |
| |
| if (Bugzilla->params->{'or_groups'}) { |
| $query .= " WHERE (group_id IN (" . $self->groups_as_string . ")" . |
| " OR group_id IS NULL)"; |
| } else { |
| $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")" . |
| " WHERE group_id IS NULL" |
| } |
| $query .= " AND products.isactive = 1"; |
| my $enterable_ids = $dbh->selectcol_arrayref($query); |
| |
| if (scalar @$enterable_ids) { |
| # And all of these products must have at least one component |
| # and one version. |
| $enterable_ids = $dbh->selectcol_arrayref( |
| 'SELECT DISTINCT products.id FROM products |
| WHERE ' . $dbh->sql_in('products.id', $enterable_ids) . |
| ' AND products.id IN (SELECT DISTINCT components.product_id |
| FROM components |
| WHERE components.isactive = 1) |
| AND products.id IN (SELECT DISTINCT versions.product_id |
| FROM versions |
| WHERE versions.isactive = 1)'); |
| } |
| |
| $self->{enterable_products} = |
| Bugzilla::Product->new_from_list($enterable_ids); |
| return $self->{enterable_products}; |
| } |
| |
| sub can_access_product { |
| my ($self, $product) = @_; |
| my $product_name = blessed($product) ? $product->name : $product; |
| return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products}); |
| } |
| |
| sub get_accessible_products { |
| my $self = shift; |
| |
| # Map the objects into a hash using the ids as keys |
| my %products = map { $_->id => $_ } |
| @{$self->get_selectable_products}, |
| @{$self->get_enterable_products}; |
| |
| return [ sort { $a->name cmp $b->name } values %products ]; |
| } |
| |
| sub can_administer { |
| my $self = shift; |
| |
| if (not defined $self->{can_administer}) { |
| my $can_administer = 0; |
| |
| $can_administer = 1 if $self->in_group('admin') |
| || $self->in_group('tweakparams') |
| || $self->in_group('editusers') |
| || $self->can_bless |
| || (Bugzilla->params->{'useclassification'} && $self->in_group('editclassifications')) |
| || $self->in_group('editcomponents') |
| || scalar(@{$self->get_products_by_permission('editcomponents')}) |
| || $self->in_group('creategroups') |
| || $self->in_group('editkeywords') |
| || $self->in_group('bz_canusewhines'); |
| |
| Bugzilla::Hook::process('user_can_administer', { can_administer => \$can_administer }); |
| $self->{can_administer} = $can_administer; |
| } |
| |
| return $self->{can_administer}; |
| } |
| |
| sub check_can_admin_product { |
| my ($self, $product_name) = @_; |
| |
| # First make sure the product name is valid. |
| my $product = Bugzilla::Product->check($product_name); |
| |
| ($self->in_group('editcomponents', $product->id) |
| && $self->can_see_product($product->name)) |
| || ThrowUserError('product_admin_denied', {product => $product->name}); |
| |
| # Return the validated product object. |
| return $product; |
| } |
| |
| sub check_can_admin_flagtype { |
| my ($self, $flagtype_id) = @_; |
| |
| my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id }); |
| my $can_fully_edit = 1; |
| |
| if (!$self->in_group('editcomponents')) { |
| my $products = $self->get_products_by_permission('editcomponents'); |
| # You need editcomponents privs for at least one product to have |
| # a chance to edit the flagtype. |
| scalar(@$products) |
| || ThrowUserError('auth_failure', {group => 'editcomponents', |
| action => 'edit', |
| object => 'flagtypes'}); |
| my $can_admin = 0; |
| my $i = $flagtype->inclusions_as_hash; |
| my $e = $flagtype->exclusions_as_hash; |
| |
| # If there is at least one product for which the user doesn't have |
| # editcomponents privs, then don't allow them to do everything with |
| # this flagtype, independently of whether this product is in the |
| # exclusion list or not. |
| my %product_ids; |
| map { $product_ids{$_->id} = 1 } @$products; |
| $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i; |
| |
| unless ($e->{0}->{0}) { |
| foreach my $product (@$products) { |
| my $id = $product->id; |
| next if $e->{$id}->{0}; |
| # If we are here, the product has not been explicitly excluded. |
| # Check whether it's explicitly included, or at least one of |
| # its components. |
| $can_admin = ($i->{0}->{0} || $i->{$id}->{0} |
| || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}})); |
| last if $can_admin; |
| } |
| } |
| $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype }); |
| } |
| return wantarray ? ($flagtype, $can_fully_edit) : $flagtype; |
| } |
| |
| sub can_request_flag { |
| my ($self, $flag_type) = @_; |
| |
| return ($self->can_set_flag($flag_type) |
| || !$flag_type->request_group_id |
| || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0; |
| } |
| |
| sub can_set_flag { |
| my ($self, $flag_type) = @_; |
| |
| return (!$flag_type->grant_group_id |
| || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; |
| } |
| |
| # visible_groups_inherited returns a reference to a list of all the groups |
| # whose members are visible to this user. |
| sub visible_groups_inherited { |
| my $self = shift; |
| return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited}; |
| return [] unless $self->id; |
| my @visgroups = @{$self->visible_groups_direct}; |
| @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)}; |
| $self->{visible_groups_inherited} = \@visgroups; |
| return $self->{visible_groups_inherited}; |
| } |
| |
| # visible_groups_direct returns a reference to a list of all the groups that |
| # are visible to this user. |
| sub visible_groups_direct { |
| my $self = shift; |
| my @visgroups = (); |
| return $self->{visible_groups_direct} if defined $self->{visible_groups_direct}; |
| return [] unless $self->id; |
| |
| my $dbh = Bugzilla->dbh; |
| my $sth; |
| |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| $sth = $dbh->prepare("SELECT DISTINCT grantor_id |
| FROM group_group_map |
| WHERE " . $self->groups_in_sql('member_id') . " |
| AND grant_type=" . GROUP_VISIBLE); |
| } |
| else { |
| # All groups are visible if usevisibilitygroups is off. |
| $sth = $dbh->prepare('SELECT id FROM groups'); |
| } |
| $sth->execute(); |
| |
| while (my ($row) = $sth->fetchrow_array) { |
| push @visgroups,$row; |
| } |
| $self->{visible_groups_direct} = \@visgroups; |
| |
| return $self->{visible_groups_direct}; |
| } |
| |
| sub visible_groups_as_string { |
| my $self = shift; |
| return join(', ', @{$self->visible_groups_inherited()}); |
| } |
| |
| # This function defines the groups a user may share a query with. |
| # More restrictive sites may want to build this reference to a list of group IDs |
| # from bless_groups instead of mirroring visible_groups_inherited, perhaps. |
| sub queryshare_groups { |
| my $self = shift; |
| my @queryshare_groups; |
| |
| return $self->{queryshare_groups} if defined $self->{queryshare_groups}; |
| |
| if ($self->in_group(Bugzilla->params->{'querysharegroup'})) { |
| # We want to be allowed to share with groups we're in only. |
| # If usevisibilitygroups is on, then we need to restrict this to groups |
| # we may see. |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| foreach(@{$self->visible_groups_inherited()}) { |
| next unless $self->in_group_id($_); |
| push(@queryshare_groups, $_); |
| } |
| } |
| else { |
| @queryshare_groups = @{ $self->_group_ids }; |
| } |
| } |
| |
| return $self->{queryshare_groups} = \@queryshare_groups; |
| } |
| |
| sub queryshare_groups_as_string { |
| my $self = shift; |
| return join(', ', @{$self->queryshare_groups()}); |
| } |
| |
| sub derive_regexp_groups { |
| my ($self) = @_; |
| |
| my $id = $self->id; |
| return unless $id; |
| |
| my $dbh = Bugzilla->dbh; |
| |
| my $sth; |
| |
| # add derived records for any matching regexps |
| |
| $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id |
| FROM groups |
| LEFT JOIN user_group_map |
| ON groups.id = user_group_map.group_id |
| AND user_group_map.user_id = ? |
| AND user_group_map.grant_type = ?"); |
| $sth->execute($id, GRANT_REGEXP); |
| |
| my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map |
| (user_id, group_id, isbless, grant_type) |
| VALUES (?, ?, 0, ?)}); |
| my $group_delete = $dbh->prepare(q{DELETE FROM user_group_map |
| WHERE user_id = ? |
| AND group_id = ? |
| AND isbless = 0 |
| AND grant_type = ?}); |
| while (my ($group, $regexp, $present) = $sth->fetchrow_array()) { |
| if (($regexp ne '') && ($self->login =~ m/$regexp/i)) { |
| $group_insert->execute($id, $group, GRANT_REGEXP) unless $present; |
| } else { |
| $group_delete->execute($id, $group, GRANT_REGEXP) if $present; |
| } |
| } |
| |
| Bugzilla->memcached->clear_config({ key => "user_groups.$id" }); |
| } |
| |
| sub product_responsibilities { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| return $self->{'product_resp'} if defined $self->{'product_resp'}; |
| return [] unless $self->id; |
| |
| my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.id |
| FROM components |
| LEFT JOIN component_cc |
| ON components.id = component_cc.component_id |
| WHERE components.initialowner = ? |
| OR components.initialqacontact = ? |
| OR component_cc.user_id = ?', |
| {Slice => {}}, ($self->id, $self->id, $self->id)); |
| |
| unless ($list) { |
| $self->{'product_resp'} = []; |
| return $self->{'product_resp'}; |
| } |
| |
| my @prod_ids = map {$_->{'product_id'}} @$list; |
| my $products = Bugzilla::Product->new_from_list(\@prod_ids); |
| # We cannot |use| it, because Component.pm already |use|s User.pm. |
| require Bugzilla::Component; |
| my @comp_ids = map {$_->{'id'}} @$list; |
| my $components = Bugzilla::Component->new_from_list(\@comp_ids); |
| |
| my @prod_list; |
| # @$products is already sorted alphabetically. |
| foreach my $prod (@$products) { |
| # We use @components instead of $prod->components because we only want |
| # components where the user is either the default assignee or QA contact. |
| push(@prod_list, {product => $prod, |
| components => [grep {$_->product_id == $prod->id} @$components]}); |
| } |
| $self->{'product_resp'} = \@prod_list; |
| return $self->{'product_resp'}; |
| } |
| |
| sub can_bless { |
| my $self = shift; |
| |
| if (!scalar(@_)) { |
| # If we're called without an argument, just return |
| # whether or not we can bless at all. |
| return scalar(@{ $self->bless_groups }) ? 1 : 0; |
| } |
| |
| # Otherwise, we're checking a specific group |
| my $group_id = shift; |
| return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0; |
| } |
| |
| sub match { |
| # Generates a list of users whose login name (email address) 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. |
| |
| # $str contains the string to match, while $limit contains the |
| # maximum number of records to retrieve. |
| my ($str, $limit, $exclude_disabled) = @_; |
| my $user = Bugzilla->user; |
| my $dbh = Bugzilla->dbh; |
| |
| $str = trim($str); |
| |
| my @users = (); |
| return \@users if $str =~ /^\s*$/; |
| |
| # The search order is wildcards, then exact match, then substring search. |
| # Wildcard matching is skipped if there is no '*', and exact matches will |
| # not (?) have a '*' in them. If any search comes up with something, the |
| # ones following it will not execute. |
| |
| # first try wildcards |
| my $wildstr = $str; |
| |
| # Do not do wildcards if there is no '*' in the string. |
| if ($wildstr =~ s/\*/\%/g && $user->id) { |
| # Build the query. |
| trick_taint($wildstr); |
| my $query = "SELECT DISTINCT userid FROM profiles "; |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| $query .= "INNER JOIN user_group_map |
| ON user_group_map.user_id = profiles.userid "; |
| } |
| $query .= "WHERE (" |
| . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " . |
| $dbh->sql_istrcmp('realname', '?', "LIKE") . ") "; |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| $query .= "AND isbless = 0 " . |
| "AND group_id IN(" . |
| join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; |
| } |
| $query .= " AND is_enabled = 1 " if $exclude_disabled; |
| $query .= $dbh->sql_limit($limit) if $limit; |
| |
| # Execute the query, retrieve the results, and make them into |
| # User objects. |
| my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr)); |
| @users = @{Bugzilla::User->new_from_list($user_ids)}; |
| } |
| else { # try an exact match |
| # Exact matches don't care if a user is disabled. |
| trick_taint($str); |
| my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles |
| WHERE ' . $dbh->sql_istrcmp('login_name', '?'), |
| undef, $str); |
| |
| push(@users, new Bugzilla::User($user_id)) if $user_id; |
| } |
| |
| # then try substring search |
| if (!scalar(@users) && length($str) >= 3 && $user->id) { |
| trick_taint($str); |
| |
| my $query = "SELECT DISTINCT userid FROM profiles "; |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| $query .= "INNER JOIN user_group_map |
| ON user_group_map.user_id = profiles.userid "; |
| } |
| $query .= " WHERE (" . |
| $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " . |
| $dbh->sql_iposition('?', 'realname') . " > 0) "; |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| $query .= " AND isbless = 0" . |
| " AND group_id IN(" . |
| join(', ', (-1, @{$user->visible_groups_inherited})) . ") "; |
| } |
| $query .= " AND is_enabled = 1 " if $exclude_disabled; |
| $query .= $dbh->sql_limit($limit) if $limit; |
| my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str)); |
| @users = @{Bugzilla::User->new_from_list($user_ids)}; |
| } |
| return \@users; |
| } |
| |
| sub match_field { |
| my $fields = shift; # arguments as a hash |
| my $data = shift || Bugzilla->input_params; # hash to look up fields in |
| my $behavior = shift || 0; # A constant that tells us how to act |
| my $matches = {}; # the values sent to the template |
| my $matchsuccess = 1; # did the match fail? |
| my $need_confirm = 0; # whether to display confirmation screen |
| my $match_multiple = 0; # whether we ever matched more than one user |
| my @non_conclusive_fields; # fields which don't have a unique user. |
| |
| my $params = Bugzilla->params; |
| |
| # prepare default form values |
| |
| # Fields can be regular expressions matching multiple form fields |
| # (f.e. "requestee-(\d+)"), so expand each non-literal field |
| # into the list of form fields it matches. |
| my $expanded_fields = {}; |
| foreach my $field_pattern (keys %{$fields}) { |
| # Check if the field has any non-word characters. Only those fields |
| # can be regular expressions, so don't expand the field if it doesn't |
| # have any of those characters. |
| if ($field_pattern =~ /^\w+$/) { |
| $expanded_fields->{$field_pattern} = $fields->{$field_pattern}; |
| } |
| else { |
| my @field_names = grep(/$field_pattern/, keys %$data); |
| |
| foreach my $field_name (@field_names) { |
| $expanded_fields->{$field_name} = |
| { type => $fields->{$field_pattern}->{'type'} }; |
| |
| # The field is a requestee field; in order for its name |
| # to show up correctly on the confirmation page, we need |
| # to find out the name of its flag type. |
| if ($field_name =~ /^requestee(_type)?-(\d+)$/) { |
| my $flag_type; |
| if ($1) { |
| require Bugzilla::FlagType; |
| $flag_type = new Bugzilla::FlagType($2); |
| } |
| else { |
| require Bugzilla::Flag; |
| my $flag = new Bugzilla::Flag($2); |
| $flag_type = $flag->type if $flag; |
| } |
| if ($flag_type) { |
| $expanded_fields->{$field_name}->{'flag_type'} = $flag_type; |
| } |
| else { |
| # No need to look for a valid requestee if the flag(type) |
| # has been deleted (may occur in race conditions). |
| delete $expanded_fields->{$field_name}; |
| delete $data->{$field_name}; |
| } |
| } |
| } |
| } |
| } |
| $fields = $expanded_fields; |
| |
| foreach my $field (keys %{$fields}) { |
| next unless defined $data->{$field}; |
| |
| #Concatenate login names, so that we have a common way to handle them. |
| my $raw_field; |
| if (ref $data->{$field}) { |
| $raw_field = join(",", @{$data->{$field}}); |
| } |
| else { |
| $raw_field = $data->{$field}; |
| } |
| $raw_field = clean_text($raw_field || ''); |
| |
| # Now we either split $raw_field by spaces/commas and put the list |
| # into @queries, or in the case of fields which only accept single |
| # entries, we simply use the verbatim text. |
| my @queries; |
| if ($fields->{$field}->{'type'} eq 'single') { |
| @queries = ($raw_field); |
| # We will repopulate it later if a match is found, else it must |
| # be set to an empty string so that the field remains defined. |
| $data->{$field} = ''; |
| } |
| elsif ($fields->{$field}->{'type'} eq 'multi') { |
| @queries = split(/[,;]+/, $raw_field); |
| # We will repopulate it later if a match is found, else it must |
| # be undefined. |
| delete $data->{$field}; |
| } |
| else { |
| # bad argument |
| ThrowCodeError('bad_arg', |
| { argument => $fields->{$field}->{'type'}, |
| function => 'Bugzilla::User::match_field', |
| }); |
| } |
| |
| # Tolerate fields that do not exist (in case you specify |
| # e.g. the QA contact, and it's currently not in use). |
| next unless (defined $raw_field && $raw_field ne ''); |
| |
| my $limit = 0; |
| if ($params->{'maxusermatches'}) { |
| $limit = $params->{'maxusermatches'} + 1; |
| } |
| |
| my @logins; |
| for my $query (@queries) { |
| $query = trim($query); |
| next if $query eq ''; |
| |
| my $users = match( |
| $query, # match string |
| $limit, # match limit |
| 1 # exclude_disabled |
| ); |
| |
| # here is where it checks for multiple matches |
| if (scalar(@{$users}) == 1) { # exactly one match |
| push(@logins, @{$users}[0]->login); |
| |
| # skip confirmation for exact matches |
| next if (lc(@{$users}[0]->login) eq lc($query)); |
| |
| $matches->{$field}->{$query}->{'status'} = 'success'; |
| $need_confirm = 1 if $params->{'confirmuniqueusermatch'}; |
| |
| } |
| elsif ((scalar(@{$users}) > 1) |
| && ($params->{'maxusermatches'} != 1)) { |
| $need_confirm = 1; |
| $match_multiple = 1; |
| push(@non_conclusive_fields, $field); |
| |
| if (($params->{'maxusermatches'}) |
| && (scalar(@{$users}) > $params->{'maxusermatches'})) |
| { |
| $matches->{$field}->{$query}->{'status'} = 'trunc'; |
| pop @{$users}; # take the last one out |
| } |
| else { |
| $matches->{$field}->{$query}->{'status'} = 'success'; |
| } |
| |
| } |
| else { |
| # everything else fails |
| $matchsuccess = 0; # fail |
| push(@non_conclusive_fields, $field); |
| $matches->{$field}->{$query}->{'status'} = 'fail'; |
| $need_confirm = 1; # confirmation screen shows failures |
| } |
| |
| $matches->{$field}->{$query}->{'users'} = $users; |
| } |
| |
| # If no match or more than one match has been found for a field |
| # expecting only one match (type eq "single"), we set it back to '' |
| # so that the caller of this function can still check whether this |
| # field was defined or not (and it was if we came here). |
| if ($fields->{$field}->{'type'} eq 'single') { |
| $data->{$field} = $logins[0] || ''; |
| } |
| elsif (scalar @logins) { |
| $data->{$field} = \@logins; |
| } |
| } |
| |
| my $retval; |
| if (!$matchsuccess) { |
| $retval = USER_MATCH_FAILED; |
| } |
| elsif ($match_multiple) { |
| $retval = USER_MATCH_MULTIPLE; |
| } |
| else { |
| $retval = USER_MATCH_SUCCESS; |
| } |
| |
| # Skip confirmation if we were told to, or if we don't need to confirm. |
| if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) { |
| return wantarray ? ($retval, \@non_conclusive_fields) : $retval; |
| } |
| |
| my $template = Bugzilla->template; |
| my $cgi = Bugzilla->cgi; |
| my $vars = {}; |
| |
| $vars->{'script'} = $cgi->url(-relative => 1); # for self-referencing URLs |
| $vars->{'fields'} = $fields; # fields being matched |
| $vars->{'matches'} = $matches; # matches that were made |
| $vars->{'matchsuccess'} = $matchsuccess; # continue or fail |
| $vars->{'matchmultiple'} = $match_multiple; |
| |
| print $cgi->header(); |
| |
| $template->process("global/confirm-user-match.html.tmpl", $vars) |
| || ThrowTemplateError($template->error()); |
| exit; |
| |
| } |
| |
| # Changes in some fields automatically trigger events. The field names are |
| # from the fielddefs table. |
| our %names_to_events = ( |
| 'resolution' => EVT_OPENED_CLOSED, |
| 'keywords' => EVT_KEYWORD, |
| 'cc' => EVT_CC, |
| 'bug_severity' => EVT_PROJ_MANAGEMENT, |
| 'priority' => EVT_PROJ_MANAGEMENT, |
| 'bug_status' => EVT_PROJ_MANAGEMENT, |
| 'target_milestone' => EVT_PROJ_MANAGEMENT, |
| 'attachments.description' => EVT_ATTACHMENT_DATA, |
| 'attachments.mimetype' => EVT_ATTACHMENT_DATA, |
| 'attachments.ispatch' => EVT_ATTACHMENT_DATA, |
| 'dependson' => EVT_DEPEND_BLOCK, |
| 'blocked' => EVT_DEPEND_BLOCK, |
| 'product' => EVT_COMPONENT, |
| 'component' => EVT_COMPONENT); |
| |
| # Returns true if the user wants mail for a given bug change. |
| # Note: the "+" signs before the constants suppress bareword quoting. |
| sub wants_bug_mail { |
| my $self = shift; |
| my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_; |
| |
| # Make a list of the events which have happened during this bug change, |
| # from the point of view of this user. |
| my %events; |
| foreach my $change (@$fieldDiffs) { |
| my $fieldName = $change->{field_name}; |
| # A change to any of the above fields sets the corresponding event |
| if (defined($names_to_events{$fieldName})) { |
| $events{$names_to_events{$fieldName}} = 1; |
| } |
| else { |
| # Catch-all for any change not caught by a more specific event |
| $events{+EVT_OTHER} = 1; |
| } |
| |
| # If the user is in a particular role and the value of that role |
| # changed, we need the ADDED_REMOVED event. |
| if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) || |
| ($fieldName eq "qa_contact" && $relationship == REL_QA)) |
| { |
| $events{+EVT_ADDED_REMOVED} = 1; |
| } |
| |
| if ($fieldName eq "cc") { |
| my $login = $self->login; |
| my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); |
| my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/); |
| if ($inold != $innew) |
| { |
| $events{+EVT_ADDED_REMOVED} = 1; |
| } |
| } |
| } |
| |
| if (!$bug->lastdiffed) { |
| # Notify about new bugs. |
| $events{+EVT_BUG_CREATED} = 1; |
| |
| # You role is new if the bug itself is. |
| # Only makes sense for the assignee, QA contact and the CC list. |
| if ($relationship == REL_ASSIGNEE |
| || $relationship == REL_QA |
| || $relationship == REL_CC) |
| { |
| $events{+EVT_ADDED_REMOVED} = 1; |
| } |
| } |
| |
| if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { |
| $events{+EVT_ATTACHMENT} = 1; |
| } |
| elsif (defined($$comments[0])) { |
| $events{+EVT_COMMENT} = 1; |
| } |
| |
| # Dependent changed bugmails must have an event to ensure the bugmail is |
| # emailed. |
| if ($dep_mail) { |
| $events{+EVT_DEPEND_BLOCK} = 1; |
| } |
| |
| my @event_list = keys %events; |
| |
| my $wants_mail = $self->wants_mail(\@event_list, $relationship); |
| |
| # The negative events are handled separately - they can't be incorporated |
| # into the first wants_mail call, because they are of the opposite sense. |
| # |
| # We do them separately because if _any_ of them are set, we don't want |
| # the mail. |
| if ($wants_mail && $changer && ($self->id == $changer->id)) { |
| $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship); |
| } |
| |
| if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') { |
| $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship); |
| } |
| |
| return $wants_mail; |
| } |
| |
| # Returns true if the user wants mail for a given set of events. |
| sub wants_mail { |
| my $self = shift; |
| my ($events, $relationship) = @_; |
| |
| # Don't send any mail, ever, if account is disabled |
| # XXX Temporary Compatibility Change 1 of 2: |
| # This code is disabled for the moment to make the behaviour like the old |
| # system, which sent bugmail to disabled accounts. |
| # return 0 if $self->{'disabledtext'}; |
| |
| # No mail if there are no events |
| return 0 if !scalar(@$events); |
| |
| # If a relationship isn't given, default to REL_ANY. |
| if (!defined($relationship)) { |
| $relationship = REL_ANY; |
| } |
| |
| # Skip DB query if relationship is explicit |
| return 1 if $relationship == REL_GLOBAL_WATCHER; |
| |
| my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; |
| return $wants_mail ? 1 : 0; |
| } |
| |
| sub mail_settings { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| if (!defined $self->{'mail_settings'}) { |
| my $data = |
| $dbh->selectall_arrayref('SELECT relationship, event FROM email_setting |
| WHERE user_id = ?', undef, $self->id); |
| my %mail; |
| # The hash is of the form $mail{$relationship}{$event} = 1. |
| $mail{$_->[0]}{$_->[1]} = 1 foreach @$data; |
| |
| $self->{'mail_settings'} = \%mail; |
| } |
| return $self->{'mail_settings'}; |
| } |
| |
| sub has_audit_entries { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| |
| if (!exists $self->{'has_audit_entries'}) { |
| $self->{'has_audit_entries'} = |
| $dbh->selectrow_array('SELECT 1 FROM audit_log WHERE user_id = ? ' . |
| $dbh->sql_limit(1), undef, $self->id); |
| } |
| return $self->{'has_audit_entries'}; |
| } |
| |
| sub is_insider { |
| my $self = shift; |
| |
| if (!defined $self->{'is_insider'}) { |
| my $insider_group = Bugzilla->params->{'insidergroup'}; |
| $self->{'is_insider'} = |
| ($insider_group && $self->in_group($insider_group)) ? 1 : 0; |
| } |
| return $self->{'is_insider'}; |
| } |
| |
| sub is_global_watcher { |
| my $self = shift; |
| |
| if (!defined $self->{'is_global_watcher'}) { |
| my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'}); |
| $self->{'is_global_watcher'} = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0; |
| } |
| return $self->{'is_global_watcher'}; |
| } |
| |
| sub is_timetracker { |
| my $self = shift; |
| |
| if (!defined $self->{'is_timetracker'}) { |
| my $tt_group = Bugzilla->params->{'timetrackinggroup'}; |
| $self->{'is_timetracker'} = |
| ($tt_group && $self->in_group($tt_group)) ? 1 : 0; |
| } |
| return $self->{'is_timetracker'}; |
| } |
| |
| sub can_tag_comments { |
| my $self = shift; |
| |
| if (!defined $self->{'can_tag_comments'}) { |
| my $group = Bugzilla->params->{'comment_taggers_group'}; |
| $self->{'can_tag_comments'} = |
| ($group && $self->in_group($group)) ? 1 : 0; |
| } |
| return $self->{'can_tag_comments'}; |
| } |
| |
| sub get_userlist { |
| my $self = shift; |
| |
| return $self->{'userlist'} if defined $self->{'userlist'}; |
| |
| my $dbh = Bugzilla->dbh; |
| my $query = "SELECT DISTINCT login_name, realname,"; |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| $query .= " COUNT(group_id) "; |
| } else { |
| $query .= " 1 "; |
| } |
| $query .= "FROM profiles "; |
| if (Bugzilla->params->{'usevisibilitygroups'}) { |
| $query .= "LEFT JOIN user_group_map " . |
| "ON user_group_map.user_id = userid AND isbless = 0 " . |
| "AND group_id IN(" . |
| join(', ', (-1, @{$self->visible_groups_inherited})) . ")"; |
| } |
| $query .= " WHERE is_enabled = 1 "; |
| $query .= $dbh->sql_group_by('userid', 'login_name, realname'); |
| |
| my $sth = $dbh->prepare($query); |
| $sth->execute; |
| |
| my @userlist; |
| while (my($login, $name, $visible) = $sth->fetchrow_array) { |
| push @userlist, { |
| login => $login, |
| identity => $name ? "$name <$login>" : $login, |
| visible => $visible, |
| }; |
| } |
| @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist; |
| |
| $self->{'userlist'} = \@userlist; |
| return $self->{'userlist'}; |
| } |
| |
| sub create { |
| my $invocant = shift; |
| my $class = ref($invocant) || $invocant; |
| my $dbh = Bugzilla->dbh; |
| |
| $dbh->bz_start_transaction(); |
| |
| my $user = $class->SUPER::create(@_); |
| |
| # Turn on all email for the new user |
| require Bugzilla::BugMail; |
| my %relationships = Bugzilla::BugMail::relationships(); |
| foreach my $rel (keys %relationships) { |
| foreach my $event (POS_EVENTS, NEG_EVENTS) { |
| # These "exceptions" define the default email preferences. |
| # |
| # We enable mail unless the change was made by the user, or it's |
| # just a CC list addition and the user is not the reporter. |
| next if ($event == EVT_CHANGED_BY_ME); |
| next if (($event == EVT_CC) && ($rel != REL_REPORTER)); |
| |
| $dbh->do('INSERT INTO email_setting (user_id, relationship, event) |
| VALUES (?, ?, ?)', undef, ($user->id, $rel, $event)); |
| } |
| } |
| |
| foreach my $event (GLOBAL_EVENTS) { |
| $dbh->do('INSERT INTO email_setting (user_id, relationship, event) |
| VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event)); |
| } |
| |
| $user->derive_regexp_groups(); |
| |
| # Add the creation date to the profiles_activity table. |
| # $who is the user who created the new user account, i.e. either an |
| # admin or the new user himself. |
| my $who = Bugzilla->user->id || $user->id; |
| my $creation_date_fieldid = get_field_id('creation_ts'); |
| |
| $dbh->do('INSERT INTO profiles_activity |
| (userid, who, profiles_when, fieldid, newvalue) |
| VALUES (?, ?, NOW(), ?, NOW())', |
| undef, ($user->id, $who, $creation_date_fieldid)); |
| |
| $dbh->bz_commit_transaction(); |
| |
| # Return the newly created user account. |
| return $user; |
| } |
| |
| ########################### |
| # Account Lockout Methods # |
| ########################### |
| |
| sub account_is_locked_out { |
| my $self = shift; |
| my $login_failures = scalar @{ $self->account_ip_login_failures }; |
| return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0; |
| } |
| |
| sub note_login_failure { |
| my $self = shift; |
| my $ip_addr = remote_ip(); |
| trick_taint($ip_addr); |
| Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time) |
| VALUES (?, ?, LOCALTIMESTAMP(0))", |
| undef, $self->id, $ip_addr); |
| delete $self->{account_ip_login_failures}; |
| } |
| |
| sub clear_login_failures { |
| my $self = shift; |
| my $ip_addr = remote_ip(); |
| trick_taint($ip_addr); |
| Bugzilla->dbh->do( |
| 'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?', |
| undef, $self->id, $ip_addr); |
| delete $self->{account_ip_login_failures}; |
| } |
| |
| sub account_ip_login_failures { |
| my $self = shift; |
| my $dbh = Bugzilla->dbh; |
| my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', |
| LOGIN_LOCKOUT_INTERVAL, 'MINUTE'); |
| my $ip_addr = remote_ip(); |
| trick_taint($ip_addr); |
| $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref( |
| "SELECT login_time, ip_addr, user_id FROM login_failure |
| WHERE user_id = ? AND login_time > $time |
| AND ip_addr = ? |
| ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr); |
| return $self->{account_ip_login_failures}; |
| } |
| |
| ############### |
| # Subroutines # |
| ############### |
| |
| sub is_available_username { |
| my ($username, $old_username) = @_; |
| |
| if(login_to_id($username) != 0) { |
| return 0; |
| } |
| |
| 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 |
| # still in progress |
| # |
| # substring/locate stuff: bug 165221; this used to use regexes, but that |
| # was unsafe and required weird escaping; using substring to pull out |
| # the new/old email addresses and sql_position() to find the delimiter (':') |
| # is cleaner/safer |
| my ($tokentype, $eventdata) = $dbh->selectrow_array( |
| "SELECT tokentype, eventdata |
| FROM tokens |
| WHERE (tokentype = 'emailold' |
| AND SUBSTRING(eventdata, 1, (" . |
| $dbh->sql_position(q{':'}, 'eventdata') . "- 1)) = ?) |
| OR (tokentype = 'emailnew' |
| AND SUBSTRING(eventdata, (" . |
| $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)", |
| undef, ($username, $username)); |
| |
| 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"))) |
| { |
| return 1; |
| } |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| sub check_account_creation_enabled { |
| my $self = shift; |
| |
| # If we're using e.g. LDAP for login, then we can't create a new account. |
| $self->authorizer->user_can_create_account |
| || ThrowUserError('auth_cant_create_account'); |
| |
| Bugzilla->params->{'createemailregexp'} |
| || ThrowUserError('account_creation_disabled'); |
| } |
| |
| sub check_and_send_account_creation_confirmation { |
| my ($self, $login) = @_; |
| my $dbh = Bugzilla->dbh; |
| |
| $dbh->bz_start_transaction; |
| |
| $login = $self->check_login_name($login); |
| my $creation_regexp = Bugzilla->params->{'createemailregexp'}; |
| |
| if ($login !~ /$creation_regexp/i) { |
| ThrowUserError('account_creation_restricted'); |
| } |
| |
| # Allow extensions to do extra checks. |
| Bugzilla::Hook::process('user_check_account_creation', { login => $login }); |
| |
| # Create and send a token for this new account. |
| require Bugzilla::Token; |
| Bugzilla::Token::issue_new_user_account_token($login); |
| |
| $dbh->bz_commit_transaction; |
| } |
| |
| # This is used in a few performance-critical areas where we don't want to |
| # do check() and pull all the user data from the database. |
| sub login_to_id { |
| my ($login, $throw_error) = @_; |
| 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 |
| # 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. |
| 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 |
| WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login); |
| $cache->{$login} = $user_id; |
| } |
| |
| if ($user_id) { |
| return $user_id; |
| } elsif ($throw_error) { |
| ThrowUserError('invalid_username', { name => $login }); |
| } else { |
| return 0; |
| } |
| } |
| |
| sub validate_password { |
| my $check = validate_password_check(@_); |
| ThrowUserError($check) if $check; |
| return 1; |
| } |
| |
| sub validate_password_check { |
| my ($password, $matchpassword) = @_; |
| |
| if (length($password) < USER_PASSWORD_MIN_LENGTH) { |
| return 'password_too_short'; |
| } elsif ((defined $matchpassword) && ($password ne $matchpassword)) { |
| return 'passwords_dont_match'; |
| } |
| |
| my $complexity_level = Bugzilla->params->{password_complexity}; |
| if ($complexity_level eq 'letters_numbers_specialchars') { |
| return 'password_not_complex' |
| if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/); |
| } elsif ($complexity_level eq 'letters_numbers') { |
| return 'password_not_complex' |
| if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/); |
| } elsif ($complexity_level eq 'mixed_letters') { |
| return 'password_not_complex' |
| if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/); |
| # WEBKIT_CHANGES |
| } elsif ($complexity_level eq 'zxcvbn_password_checker') { |
| my %opts = (score_for_feedback => 3); |
| my $est_strength = password_strength($password, \%opts); |
| return 'Password is weak. ' . $est_strength->{feedback}->{warning} |
| if ($est_strength->{score} < 4); |
| } |
| |
| # Having done these checks makes us consider the password untainted. |
| trick_taint($_[0]); |
| return; |
| } |
| |
| |
| 1; |
| |
| __END__ |
| |
| =head1 NAME |
| |
| Bugzilla::User - Object for a Bugzilla user |
| |
| =head1 SYNOPSIS |
| |
| use Bugzilla::User; |
| |
| my $user = new Bugzilla::User($id); |
| |
| my @get_selectable_classifications = |
| $user->get_selectable_classifications; |
| |
| # Class Functions |
| $user = Bugzilla::User->create({ |
| login_name => $username, |
| realname => $realname, |
| cryptpassword => $plaintext_password, |
| disabledtext => $disabledtext, |
| disable_mail => 0}); |
| |
| =head1 DESCRIPTION |
| |
| This package handles Bugzilla users. Data obtained from here is read-only; |
| there is currently no way to modify a user from this package. |
| |
| Note that the currently logged in user (if any) is available via |
| L<Bugzilla-E<gt>user|Bugzilla/"user">. |
| |
| C<Bugzilla::User> is an implementation of L<Bugzilla::Object>, and thus |
| provides all the methods of L<Bugzilla::Object> in addition to the |
| methods listed below. |
| |
| =head1 CONSTANTS |
| |
| =over |
| |
| =item C<USER_MATCH_MULTIPLE> |
| |
| Returned by C<match_field()> when at least one field matched more than |
| one user, but no matches failed. |
| |
| =item C<USER_MATCH_FAILED> |
| |
| Returned by C<match_field()> when at least one field failed to match |
| anything. |
| |
| =item C<USER_MATCH_SUCCESS> |
| |
| Returned by C<match_field()> when all fields successfully matched only one |
| user. |
| |
| =item C<MATCH_SKIP_CONFIRM> |
| |
| Passed in to match_field to tell match_field to never display a |
| confirmation screen. |
| |
| =back |
| |
| =head1 METHODS |
| |
| =head2 Constructors |
| |
| =over |
| |
| =item C<super_user> |
| |
| Returns a user who is in all groups, but who does not really exist in the |
| database. Used for non-web scripts like L<checksetup> that need to make |
| database changes and so on. |
| |
| =back |
| |
| =head2 Saved and Shared Queries |
| |
| =over |
| |
| =item C<queries> |
| |
| Returns an arrayref of the user's own saved queries, sorted by name. The |
| array contains L<Bugzilla::Search::Saved> objects. |
| |
| =item C<queries_subscribed> |
| |
| Returns an arrayref of shared queries that the user has subscribed to. |
| That is, these are shared queries that the user sees in their footer. |
| This array contains L<Bugzilla::Search::Saved> objects. |
| |
| =item C<queries_available> |
| |
| Returns an arrayref of all queries to which the user could possibly |
| subscribe. This includes the contents of L</queries_subscribed>. |
| An array of L<Bugzilla::Search::Saved> objects. |
| |
| =item C<flush_queries_cache> |
| |
| Some code modifies the set of stored queries. Because C<Bugzilla::User> does |
| not handle these modifications, but does cache the result of calling C<queries> |
| internally, such code must call this method to flush the cached result. |
| |
| =item C<queryshare_groups> |
| |
| An arrayref of group ids. The user can share their own queries with these |
| groups. |
| |
| =item C<tags> |
| |
| Returns a hashref with tag IDs as key, and a hashref with tag 'id', |
| 'name' and 'bug_count' as value. |
| |
| =item C<bugs_ignored> |
| |
| Returns an array of hashrefs containing information about bugs currently |
| being ignored by the user. |
| |
| Each hashref contains the following information: |
| |
| =over |
| |
| =item C<id> |
| |
| C<int> The id of the bug. |
| |
| =item C<status> |
| |
| C<string> The current status of the bug. |
| |
| =item C<summary> |
| |
| C<string> The current summary of the bug. |
| |
| =back |
| |
| =item C<is_bug_ignored> |
| |
| Returns true if the user does not want email notifications for the |
| specified bug ID, else returns false. |
| |
| =back |
| |
| =head2 Saved Recent Bug Lists |
| |
| =over |
| |
| =item C<recent_searches> |
| |
| Returns an arrayref of L<Bugzilla::Search::Recent> objects |
| containing the user's recent searches. |
| |
| =item C<recent_search_containing(bug_id)> |
| |
| Returns a L<Bugzilla::Search::Recent> object that contains the most recent |
| search by the user for the specified bug id. Retuns undef if no match is found. |
| |
| =item C<recent_search_for(bug)> |
| |
| Returns a L<Bugzilla::Search::Recent> object that contains a search by the |
| user. Uses the list_id of the current loaded page, or the referrer page, and |
| the bug id if that fails. Finally it will check the BUGLIST cookie, and create |
| an object based on that, or undef if it does not exist. |
| |
| =item C<save_last_search> |
| |
| Saves the users most recent search in the database if logged in, or in the |
| BUGLIST cookie if not logged in. Parameters are bug_ids, order, vars and |
| list_id. |
| |
| =back |
| |
| =head2 Account Lockout |
| |
| =over |
| |
| =item C<account_is_locked_out> |
| |
| Returns C<1> if the account has failed to log in too many times recently, |
| and thus is locked out for a period of time. Returns C<0> otherwise. |
| |
| =item C<account_ip_login_failures> |
| |
| Returns an arrayref of hashrefs, that contains information about the recent |
| times that this account has failed to log in from the current remote IP. |
| The hashes contain C<ip_addr>, C<login_time>, and C<user_id>. |
| |
| =item C<note_login_failure> |
| |
| This notes that this account has failed to log in, and stores the fact |
| in the database. The storing happens immediately, it does not wait for |
| you to call C<update>. |
| |
| =item C<set_email_enabled> |
| |
| C<bool> - Sets C<disable_mail> to the inverse of the boolean provided. |
| |
| =back |
| |
| =head2 Other Methods |
| |
| =over |
| |
| =item C<id> |
| |
| Returns the userid for this user. |
| |
| =item C<login> |
| |
| 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. |
| |
| =item C<name> |
| |
| Returns the 'real' name for this user, if any. |
| |
| =item C<showmybugslink> |
| |
| Returns C<1> if the user has set their preference to show the 'My Bugs' link in |
| the page footer, and C<0> otherwise. |
| |
| =item C<identity> |
| |
| Returns a string for the identity of the user. This will be of the form |
| C<name E<lt>emailE<gt>> if the user has specified a name, and C<email> |
| 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. |
| |
| =item C<authorizer> |
| |
| This is the L<Bugzilla::Auth> object that the User logged in with. |
| If the user hasn't logged in yet, a new, empty Bugzilla::Auth() object is |
| returned. |
| |
| =item C<set_authorizer($authorizer)> |
| |
| Sets the L<Bugzilla::Auth> object to be returned by C<authorizer()>. |
| Should only be called by C<Bugzilla::Auth::login>, for the most part. |
| |
| =item C<disabledtext> |
| |
| Returns the disable text of the user, if any. |
| |
| =item C<reports> |
| |
| Returns an arrayref of the user's own saved reports. The array contains |
| L<Bugzilla::Reports> objects. |
| |
| =item C<flush_reports_cache> |
| |
| Some code modifies the set of stored reports. Because C<Bugzilla::User> does |
| not handle these modifications, but does cache the result of calling C<reports> |
| internally, such code must call this method to flush the cached result. |
| |
| =item C<settings> |
| |
| Returns a hash of hashes which holds the user's settings. The first key is |
| the name of the setting, as found in setting.name. The second key is one of: |
| is_enabled - true if the user is allowed to set the preference themselves; |
| false to force the site defaults |
| for themselves or must accept the global site default value |
| default_value - the global site default for this setting |
| value - the value of this setting for this user. Will be the same |
| as the default_value if the user is not logged in, or if |
| is_default is true. |
| is_default - a boolean to indicate whether the user has chosen to make |
| a preference for themself or use the site default. |
| |
| =item C<setting(name)> |
| |
| Returns the value for the specified setting. |
| |
| =item C<timezone> |
| |
| Returns the timezone used to display dates and times to the user, |
| as a DateTime::TimeZone object. |
| |
| =item C<groups> |
| |
| Returns an arrayref of L<Bugzilla::Group> objects representing |
| groups that this user is a member of. |
| |
| =item C<groups_as_string> |
| |
| Returns a string containing a comma-separated list of numeric group ids. If |
| the user is not a member of any groups, returns "-1". This is most often used |
| within an SQL IN() function. |
| |
| =item C<groups_in_sql> |
| |
| This returns an C<IN> clause for SQL, containing either all of the groups |
| the user is in, or C<-1> if the user is in no groups. This takes one |
| argument--the name of the SQL field that should be on the left-hand-side |
| of the C<IN> statement, which defaults to C<group_id> if not specified. |
| |
| =item C<in_group($group_name, $product_id)> |
| |
| Determines whether or not a user is in the given group by name. |
| If $product_id is given, it also checks for local privileges for |
| this product. |
| |
| =item C<in_group_id> |
| |
| Determines whether or not a user is in the given group by id. |
| |
| =item C<bless_groups> |
| |
| Returns an arrayref of L<Bugzilla::Group> objects. |
| |
| The arrayref consists of the groups the user can bless, taking into account |
| that having editusers permissions means that you can bless all groups, and |
| that you need to be able to see a group in order to bless it. |
| |
| =item C<get_products_by_permission($group)> |
| |
| Returns a list of product objects for which the user has $group privileges |
| and which they can access. |
| $group must be one of the groups defined in PER_PRODUCT_PRIVILEGES. |
| |
| =item C<can_see_user(user)> |
| |
| Returns 1 if the specified user account exists and is visible to the user, |
| 0 otherwise. |
| |
| =item C<can_edit_product(prod_id)> |
| |
| Determines if, given a product id, the user can edit bugs in this product |
| at all. |
| |
| =item C<visible_bugs($bugs)> |
| |
| Description: Determines if a list of bugs are visible to the user. |
| Params: C<$bugs> - An arrayref of Bugzilla::Bug objects or bug ids |
| Returns: An arrayref of the bug ids that the user can see |
| |
| =item C<can_see_bug(bug_id)> |
| |
| Determines if the user can see the specified bug. |
| |
| =item C<can_see_product(product_name)> |
| |
| Returns 1 if the user can access the specified product, and 0 if the user |
| should not be aware of the existence of the product. |
| |
| =item C<derive_regexp_groups> |
| |
| Bugzilla allows for group inheritance. When data about the user (or any of the |
| groups) changes, the database must be updated. Handling updated groups is taken |
| care of by the constructor. However, when updating the email address, the |
| user may be placed into different groups, based on a new email regexp. This |
| method should be called in such a case to force reresolution of these groups. |
| |
| =item C<clear_product_cache> |
| |
| Clears the stored values for L</get_selectable_products>, |
| L</get_enterable_products>, etc. so that their data will be read from |
| the database again. Used mostly by L<Bugzilla::Product>. |
| |
| =item C<get_selectable_products> |
| |
| Description: Returns all products the user is allowed to access. This list |
| is restricted to some given classification if $classification_id |
| is given. |
| |
| Params: $classification_id - (optional) The ID of the classification |
| the products belong to. |
| |
| Returns: An array of product objects, sorted by the product name. |
| |
| =item C<get_selectable_classifications> |
| |
| Description: Returns all classifications containing at least one product |
| the user is allowed to view. |
| |
| Params: none |
| |
| Returns: An array of Bugzilla::Classification objects, sorted by |
| the classification name. |
| |
| =item C<can_enter_product($product_name, $warn)> |
| |
| Description: Returns a product object if the user can enter bugs into the |
| specified product. |
| If the user cannot enter bugs into the product, the behavior of |
| this method depends on the value of $warn: |
| - if $warn is false (or not given), a 'false' value is returned; |
| - if $warn is true, an error is thrown. |
| |
| Params: $product_name - a product name. |
| $warn - optional parameter, indicating whether an error |
| must be thrown if the user cannot enter bugs |
| into the specified product. |
| |
| Returns: A product object if the user can enter bugs into the product, |
| 0 if the user cannot enter bugs into the product and if $warn |
| is false (an error is thrown if $warn is true). |
| |
| =item C<get_enterable_products> |
| |
| Description: Returns an array of product objects into which the user is |
| allowed to enter bugs. |
| |
| Params: none |
| |
| Returns: an array of product objects. |
| |
| =item C<can_access_product($product)> |
| |
| Returns 1 if the user can search or enter bugs into the specified product |
| (either a L<Bugzilla::Product> or a product name), and 0 if the user should |
| not be aware of the existence of the product. |
| |
| =item C<get_accessible_products> |
| |
| Description: Returns an array of product objects the user can search |
| or enter bugs against. |
| |
| Params: none |
| |
| Returns: an array of product objects. |
| |
| =item C<can_administer> |
| |
| Returns 1 if the user can see the admin menu. Otherwise, returns 0 |
| |
| =item C<check_can_admin_product($product_name)> |
| |
| Description: Checks whether the user is allowed to administrate the product. |
| |
| Params: $product_name - a product name. |
| |
| Returns: On success, a product object. On failure, an error is thrown. |
| |
| =item C<check_can_admin_flagtype($flagtype_id)> |
| |
| Description: Checks whether the user is allowed to edit properties of the flag type. |
| If the flag type is also used by some products for which the user |
| hasn't editcomponents privs, then the user is only allowed to edit |
| the inclusion and exclusion lists for products they can administrate. |
| |
| Params: $flagtype_id - a flag type ID. |
| |
| Returns: On success, a flag type object. On failure, an error is thrown. |
| In list context, a boolean indicating whether the user can edit |
| all properties of the flag type is also returned. The boolean |
| is false if the user can only edit the inclusion and exclusions |
| lists. |
| |
| =item C<can_request_flag($flag_type)> |
| |
| Description: Checks whether the user can request flags of the given type. |
| |
| Params: $flag_type - a Bugzilla::FlagType object. |
| |
| Returns: 1 if the user can request flags of the given type, |
| 0 otherwise. |
| |
| =item C<can_set_flag($flag_type)> |
| |
| Description: Checks whether the user can set flags of the given type. |
| |
| Params: $flag_type - a Bugzilla::FlagType object. |
| |
| Returns: 1 if the user can set flags of the given type, |
| 0 otherwise. |
| |
| =item C<get_userlist> |
| |
| Returns a reference to an array of users. The array is populated with hashrefs |
| containing the login, identity and visibility. Users that are not visible to this |
| user will have 'visible' set to zero. |
| |
| =item C<visible_groups_inherited> |
| |
| Returns a list of all groups whose members should be visible to this user. |
| Since this list is flattened already, there is no need for all users to |
| be have derived groups up-to-date to select the users meeting this criteria. |
| |
| =item C<visible_groups_direct> |
| |
| Returns a list of groups that the user is aware of. |
| |
| =item C<visible_groups_as_string> |
| |
| Returns the result of C<visible_groups_inherited> as a string (a comma-separated |
| list). |
| |
| =item C<product_responsibilities> |
| |
| Retrieve user's product responsibilities as a list of component objects. |
| Each object is a component the user has a responsibility for. |
| |
| =item C<can_bless> |
| |
| When called with no arguments: |
| Returns C<1> if the user can bless at least one group, returns C<0> otherwise. |
| |
| When called with one argument: |
| Returns C<1> if the user can bless the group with that id, returns |
| C<0> otherwise. |
| |
| =item C<wants_bug_mail> |
| |
| Returns true if the user wants mail for a given bug change. |
| |
| =item C<wants_mail> |
| |
| Returns true if the user wants mail for a given set of events. This method is |
| more general than C<wants_bug_mail>, allowing you to check e.g. permissions |
| for flag mail. |
| |
| =item C<is_insider> |
| |
| Returns true if the user can access private comments and attachments, |
| i.e. if the 'insidergroup' parameter is set and the user belongs to this group. |
| |
| =item C<is_global_watcher> |
| |
| Returns true if the user is a global watcher, |
| i.e. if the 'globalwatchers' parameter contains the user. |
| |
| =item C<can_tag_comments> |
| |
| Returns true if the user can attach tags to comments. |
| i.e. if the 'comment_taggers_group' parameter is set and the user belongs to |
| this group. |
| |
| =item C<last_visited> |
| |
| Returns an arrayref L<Bugzilla::BugUserLastVisit> objects. |
| |
| =item C<is_involved_in_bug($bug)> |
| |
| Returns true if any of the following conditions are met, false otherwise. |
| |
| =over |
| |
| =item * |
| |
| User is the assignee of the bug |
| |
| =item * |
| |
| User is the reporter of the bug |
| |
| =item * |
| |
| User is the QA contact of the bug (if Bugzilla is configured to use a QA |
| contact) |
| |
| =item * |
| |
| User is in the cc list for the bug. |
| |
| =back |
| |
| =item C<set_groups> |
| |
| C<hash> These specify the groups that this user is directly a member of. |
| To set these, you should pass a hash as the value. The hash may contain |
| the following fields: |
| |
| =over |
| |
| =item C<add> An array of C<int>s or C<string>s. The group ids or group names |
| that the user should be added to. |
| |
| =item C<remove> An array of C<int>s or C<string>s. The group ids or group names |
| that the user should be removed from. |
| |
| =item C<set> An array of C<int>s or C<string>s. An exact set of group ids |
| and group names that the user should be a member of. NOTE: This does not |
| remove groups from the user where the person making the change does not |
| have the bless privilege for. |
| |
| If you specify C<set>, then C<add> and C<remove> will be ignored. A group in |
| both the C<add> and C<remove> list will be added. Specifying a group that the |
| user making the change does not have bless rights will generate an error. |
| |
| =back |
| |
| =item C<set_bless_groups> |
| |
| C<hash> - This is the same as set_groups, but affects what groups a user |
| has direct membership to bless that group. It takes the same inputs as |
| set_groups. |
| |
| =back |
| |
| =head1 CLASS FUNCTIONS |
| |
| These are functions that are not called on a User object, but instead are |
| called "statically," just like a normal procedural function. |
| |
| =over 4 |
| |
| =item C<create> |
| |
| 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. |
| 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 |
| be able to log in using DB authentication. |
| disabledtext - The disable-text for the new user. If given, the user |
| will be disabled, meaning they cannot log in. Defaults to an |
| empty string. |
| disable_mail - If 1, bug-related mail will not be sent to this user; |
| if 0, mail will be sent depending on the user's email preferences. |
| |
| =item C<check> |
| |
| Takes a username as its only argument. Throws an error if there is no |
| user with that username. Returns a C<Bugzilla::User> object. |
| |
| =item C<check_account_creation_enabled> |
| |
| 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)> |
| |
| 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> |
| |
| Returns a boolean indicating whether or not the supplied username 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 |
| 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. |
| |
| 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 |
| instead of returning. |
| |
| This function can also be used when you want to just find out the userid |
| 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<validate_password($passwd1, $passwd2)> |
| |
| Returns true if a password is valid (i.e. meets Bugzilla's |
| requirements for length and content), else throws an error. |
| Untaints C<$passwd1> if successful. |
| |
| If a second password is passed in, this function also verifies that |
| the two passwords match. |
| |
| =item C<validate_password_check($passwd1, $passwd2)> |
| |
| This sub routine is similair to C<validate_password>, except that it allows |
| the calling code to handle its own errors. |
| |
| Returns undef and untaints C<$passwd1> if a password is valid (i.e. meets |
| Bugzilla's requirements for length and content), else returns the error. |
| |
| If a second password is passed in, this function also verifies that |
| the two passwords match. |
| |
| =item C<match_field($data, $fields, $behavior)> |
| |
| =over |
| |
| =item B<Description> |
| |
| Wrapper for the C<match()> function. |
| |
| =item B<Params> |
| |
| =over |
| |
| =item C<$fields> - A hashref with field names as keys and a hash as values. |
| Each hash is of the form { 'type' => 'single|multi' }, which specifies |
| whether the field can take a single login name only or several. |
| |
| =item C<$data> (optional) - A hashref with field names as keys and field values |
| as values. If undefined, C<Bugzilla-E<gt>input_params> is used. |
| |
| =item C<$behavior> (optional) - If set to C<MATCH_SKIP_CONFIRM>, no confirmation |
| screen is displayed. In that case, the fields which don't match a unique user |
| are left undefined. If not set, a confirmation screen is displayed if at |
| least one field doesn't match any login name or match more than one. |
| |
| =back |
| |
| =item B<Returns> |
| |
| If the third parameter is set to C<MATCH_SKIP_CONFIRM>, the function returns |
| either C<USER_MATCH_SUCCESS> if all fields can be set unambiguously, |
| C<USER_MATCH_FAILED> if at least one field doesn't match any user account, |
| or C<USER_MATCH_MULTIPLE> if some fields match more than one user account. |
| |
| If the third parameter is not set, then if all fields could be set |
| unambiguously, nothing is returned, else a confirmation page is displayed. |
| |
| =item B<Note> |
| |
| This function must be called early in a script, before anything external |
| is done with the data. |
| |
| =back |
| |
| =back |
| |
| =head1 SEE ALSO |
| |
| L<Bugzilla|Bugzilla> |
| |
| =head1 B<Methods in need of POD> |
| |
| =over |
| |
| =item email_enabled |
| |
| =item cryptpassword |
| |
| =item clear_login_failures |
| |
| =item set_disable_mail |
| |
| =item has_audit_entries |
| |
| =item groups_with_icon |
| |
| =item check_login_name |
| |
| =item set_extern_id |
| |
| =item mail_settings |
| |
| =item email_disabled |
| |
| =item update |
| |
| =item is_timetracker |
| |
| =item is_enabled |
| |
| =item queryshare_groups_as_string |
| |
| =item set_login |
| |
| =item set_password |
| |
| =item last_seen_date |
| |
| =item set_disabledtext |
| |
| =item update_last_seen_date |
| |
| =item set_name |
| |
| =item DB_COLUMNS |
| |
| =item extern_id |
| |
| =item UPDATE_COLUMNS |
| |
| =back |