diff options
Diffstat (limited to 'Bugzilla/User.pm')
-rw-r--r-- | Bugzilla/User.pm | 3619 |
1 files changed, 1864 insertions, 1755 deletions
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm index 77e6cebb0..420aaf0fa 100644 --- a/Bugzilla/User.pm +++ b/Bugzilla/User.pm @@ -33,9 +33,9 @@ use URI::QueryParam; 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 + login_to_id validate_password validate_password_check + USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS + MATCH_SKIP_CONFIRM ); ##################################################################### @@ -46,16 +46,16 @@ 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 MATCH_SKIP_CONFIRM => 1; use constant DEFAULT_USER => { - 'userid' => 0, - 'realname' => '', - 'login_name' => '', - 'showmybugslink' => 0, - 'disabledtext' => '', - 'disable_mail' => 0, - 'is_enabled' => 1, + 'userid' => 0, + 'realname' => '', + 'login_name' => '', + 'showmybugslink' => 0, + 'disabledtext' => '', + 'disable_mail' => 0, + 'is_enabled' => 1, }; use constant DB_TABLE => 'profiles'; @@ -65,18 +65,19 @@ use constant DB_TABLE => 'profiles'; # 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', + 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'; @@ -84,32 +85,30 @@ 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, + 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; -}; + 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 VALIDATOR_DEPENDENCIES => {is_enabled => ['disabledtext'],}; use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); @@ -118,129 +117,125 @@ use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled); ################################################################################ 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; - } + 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(@_); + } + return $class->SUPER::new(@_); } sub super_user { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; + 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; + 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 ( + 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 + } + ); + 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 $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 - ); - } + 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{ + 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. - } + $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 }); + Bugzilla->memcached->clear_config({key => 'user_groups.' . $self->id}); - my $type = $is_bless ? 'bless_groups' : 'groups'; - $changes->{$type} = [ - [ map { $_->name } @$removed ], - [ map { $_->name } @$added ], - ]; - } + my $type = $is_bless ? 'bless_groups' : 'groups'; + $changes->{$type} = [[map { $_->name } @$removed], [map { $_->name } @$added],]; + } } sub update { - my $self = shift; - my $options = shift; + my $self = shift; + my $options = shift; - my $group_changes = delete $self->{_group_changes}; + my $group_changes = delete $self->{_group_changes}; - my $changes = $self->SUPER::update(@_); - my $dbh = Bugzilla->dbh; - $self->_update_groups($group_changes, $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(); - } + if (exists $changes->{login_name}) { - # Logout the user if necessary. - Bugzilla->logout_user($self) - if (!$options->{keep_session} - && (exists $changes->{login_name} - || exists $changes->{disabledtext} - || exists $changes->{cryptpassword})); + # Delete all the tokens related to the userid + $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id) + unless $options->{keep_tokens}; - # XXX Can update profiles_activity here as soon as it understands - # field names like login_name. - - return $changes; + # 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; } ################################################################################ @@ -252,62 +247,63 @@ 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 }); - } + 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; + } + 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 }); - } + 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; + return $name; } sub _check_password { - my ($self, $pass) = @_; + 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 '*'; + # 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; + 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; + 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; } ################################################################################ @@ -316,150 +312,151 @@ sub _check_is_enabled { 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_extern_id { $_[0]->set('extern_id', $_[1]); } sub set_login { - my ($self, $login) = @_; - $self->set('login_name', $login); - delete $self->{identity}; - delete $self->{nick}; + 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}; + 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); + $_[0]->set('disabledtext', $_[1]); + $_[0]->set('is_enabled', $_[1] ? 0 : 1); } sub set_groups { - my $self = shift; - $self->_set_groups(GROUP_MEMBERSHIP, @_); + my $self = shift; + $self->_set_groups(GROUP_MEMBERSHIP, @_); } sub set_bless_groups { - my $self = shift; + 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"}); + # 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, @_); + $self->_set_groups(GROUP_BLESS, @_); } sub _set_groups { - my $self = shift; - my $is_bless = shift; - my $changes = shift; - my $dbh = Bugzilla->dbh; + 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; + # 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. + # 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); + # 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 + # 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; - } - } + 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; - } + } + else { + foreach my $group (@{$changes->{remove} // []}) { + @$new_groups = grep { $_->id ne $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; + 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 } - ); - } + my $self = shift; + my $changes = shift; + my $user = Bugzilla->user; - # 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; + 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; + 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 }); - } + 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}); + } } ################################################################################ @@ -467,171 +464,181 @@ sub update_last_seen_date { ################################################################################ # 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 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 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; + 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; + my ($self, $authorizer) = @_; + $self->{authorizer} = $authorizer; } + sub authorizer { - my ($self) = @_; - if (!$self->{authorizer}) { - require Bugzilla::Auth; - $self->{authorizer} = new Bugzilla::Auth(); - } - return $self->{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; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{identity}) { - $self->{identity} = - $self->name ? $self->name . " <" . $self->login. ">" : $self->login; - } + if (!defined $self->{identity}) { + $self->{identity} + = $self->name ? $self->name . " <" . $self->login . ">" : $self->login; + } - return $self->{identity}; + return $self->{identity}; } sub nick { - my $self = shift; + my $self = shift; - return "" unless $self->id; + return "" unless $self->id; - if (!defined $self->{nick}) { - $self->{nick} = (split(/@/, $self->login, 2))[0]; - } + if (!defined $self->{nick}) { + $self->{nick} = (split(/@/, $self->login, 2))[0]; + } - return $self->{nick}; + return $self->{nick}; } sub queries { - my $self = shift; - return $self->{queries} if defined $self->{queries}; - return [] unless $self->id; + 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); + 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; + # 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}; + 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 + 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}; + 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; + 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'; + # 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}; + 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 + 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}; + 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, + 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'}; + 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; + my ($self, $bug_id) = @_; + return (grep { $_->{'id'} == $bug_id } @{$self->bugs_ignored}) ? 1 : 0; } ########################## @@ -639,309 +646,312 @@ sub is_bug_ignored { ########################## sub recent_searches { - my $self = shift; - $self->{recent_searches} ||= - Bugzilla::Search::Recent->match({ user_id => $self->id }); - return $self->{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; + my ($self, $bug_id) = @_; + my $searches = $self->recent_searches; - foreach my $search (@$searches) { - return $search if grep($_ == $bug_id, @{ $search->bug_list }); - } + foreach my $search (@$searches) { + return $search if grep($_ == $bug_id, @{$search->bug_list}); + } - return undef; + 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'); - } - } + 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 ($list_id && $list_id ne 'cookie') { - # 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; + # 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; } - # 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; - } + # 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; + 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'); - } + 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'); + 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 { - $cgi->remove_cookie('BUGLIST'); - $vars->{'toolong'} = 1; + $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' + ); } - return $search; + 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 $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}; + 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; + my $self = shift; - delete $self->{reports}; + delete $self->{reports}; } sub settings { - my ($self) = @_; + my ($self) = @_; - return $self->{'settings'} if (defined $self->{'settings'}); + 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(); - } + # 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'}; + return $self->{'settings'}; } sub setting { - my ($self, $name) = @_; - return $self->settings->{$name}->{'value'}; + my ($self, $name) = @_; + return $self->settings->{$name}->{'value'}; } sub timezone { - my $self = shift; + 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); - } + 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; } - return $self->{timezone}; + else { + $self->{timezone} = DateTime::TimeZone->new(name => $tz); + } + } + return $self->{timezone}; } sub flush_queries_cache { - my $self = shift; + my $self = shift; - delete $self->{queries}; - delete $self->{queries_subscribed}; - delete $self->{queries_available}; + delete $self->{queries}; + delete $self->{queries_subscribed}; + delete $self->{queries_available}; } sub groups { - my $self = shift; + my $self = shift; - return $self->{groups} if defined $self->{groups}; - return [] unless $self->id; + 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 - }); + 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 + 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 + 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, - }); - } + 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); - } + 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 ]; + # 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) { - Bugzilla->memcached->set_config({ - key => $user_groups_key, - data => $groups, - }); + # 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}; + $self->{groups} = Bugzilla::Group->new_from_list($groups); + return $self->{groups}; } sub last_visited { - my ($self, $ids) = @_; + my ($self, $ids) = @_; - return Bugzilla::BugUserLastVisit->match({ user_id => $self->id, - $ids ? ( bug_id => $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; + 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; + 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; - } + if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) { + return 1 if $user_id == $bug->qa_contact->id; + } - return any { $user_login eq $_ } @{ $bug->cc }; + return any { $user_login eq $_ } @{$bug->cc}; } # It turns out that calling ->id on objects a few hundred thousand @@ -949,296 +959,311 @@ sub is_involved_in_bug { # 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}; + 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'; + 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); + 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; + my $self = shift; - return $self->{'bless_groups'} if defined $self->{'bless_groups'}; - return [] unless $self->id; + 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 ($self->in_group('editusers')) { - if (Bugzilla->params->{usevisibilitygroups} - && !@{ $self->visible_groups_inherited }) { - return []; - } + # Users having editusers permissions may bless all groups. + $self->{'bless_groups'} = [Bugzilla::Group->get_all]; + return $self->{'bless_groups'}; + } - my $dbh = Bugzilla->dbh; + if (Bugzilla->params->{usevisibilitygroups} + && !@{$self->visible_groups_inherited}) + { + return []; + } - # Get all groups for the user where they have direct bless privileges. - my $query = " + 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 .= " + 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); - } + 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); + 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 + 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); + 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}; + $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0; } - # If we come here, then the user is not in the requested group. - return 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; + 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; + my $self = shift; - return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }]; + 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 ($self, $group) = @_; - my $product_ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT DISTINCT product_id + # 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); + 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; + # 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; + # 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; + 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) + 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) + } + else { + $query = qq{SELECT COUNT(userid) FROM profiles WHERE userid = ? }; - } - return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); + } + return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id); } sub can_edit_product { - my ($self, $prod_id) = @_; - my $dbh = Bugzilla->dbh; + 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), + 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 + 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); + AND group_id NOT IN(' + . $self->groups_as_string . ')', undef, $prod_id + ); - return !$has_external_groups; - } + return !$has_external_groups; + } } sub can_see_bug { - my ($self, $bug_id) = @_; - return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0; + 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'}); - } + my ($self, $bugs) = @_; + + # Allow users to pass in Bug objects and bug ids both. + my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs; - Bugzilla->params->{'or_groups'} - ? $self->_visible_bugs_check_or(\@check_ids) - : $self->_visible_bugs_check_and(\@check_ids); + # 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'}); } - return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$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{ + 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{)) + 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 .= ')'; + 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; - } + $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; - } + # 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; - } + # 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. - 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, + 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}; + } + $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 @@ -1248,1056 +1273,1122 @@ sub _visible_bugs_check_and { 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 . ') + . $self->groups_as_string . ') WHERE bugs.bug_id IN (' . join(',', ('?') x @$check_ids) . ') - AND creation_ts IS NOT NULL '); - 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; - } + AND creation_ts IS NOT NULL ' + ); + 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}; + my $self = shift; + delete $self->{enterable_products}; + delete $self->{selectable_products}; + delete $self->{selectable_classifications}; } sub can_see_product { - my ($self, $product_name) = @_; + my ($self, $product_name) = @_; - return scalar(grep {$_->name eq $product_name} @{$self->get_selectable_products}); + 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; + my $self = shift; + my $class_id = shift; + my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id; - if (!defined $self->{selectable_products}) { - my $query = "SELECT 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 . ") + 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 . ") + } + 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}; + 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) = @_; + my ($self) = @_; - if (!defined $self->{selectable_classifications}) { - my $products = $self->get_selectable_products; - my %class_ids = map { $_->classification_id => 1 } @$products; + 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}; + $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' }); - } + my ($self, $input, $warn) = @_; + my $dbh = Bugzilla->dbh; + $warn ||= 0; - if (!scalar @{ $self->get_enterable_products }) { - return unless $warn == THROW_ERROR; - ThrowUserError('no_products'); - } + $input = trim($input) if !ref $input; + if (!defined $input or $input eq '') { + return unless $warn == THROW_ERROR; + ThrowUserError('object_not_specified', {class => 'Bugzilla::Product'}); + } - my $product = blessed($input) ? $input - : new Bugzilla::Product({ name => $input }); - my $can_enter = - $product && grep($_->name eq $product->name, - @{ $self->get_enterable_products }); + if (!scalar @{$self->get_enterable_products}) { + return unless $warn == THROW_ERROR; + ThrowUserError('no_products'); + } - return $product if $can_enter; + my $product + = blessed($input) ? $input : new Bugzilla::Product({name => $input}); + my $can_enter = $product + && grep($_->name eq $product->name, @{$self->get_enterable_products}); - return 0 unless $warn == THROW_ERROR; + return $product if $can_enter; - # Check why access was denied. These checks are slow, - # but that's fine, because they only happen if we fail. + return 0 unless $warn == THROW_ERROR; - # 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; + # Check why access was denied. These checks are slow, + # but that's fine, because they only happen if we fail. - # 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 }); - } + # 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}); + } - die "can_enter_product reached an unreachable location."; + # 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; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (defined $self->{enterable_products}) { - return $self->{enterable_products}; - } + 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 + # 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 + 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)'); - } + WHERE versions.isactive = 1)' + ); + } - $self->{enterable_products} = - Bugzilla::Product->new_from_list($enterable_ids); - return $self->{enterable_products}; + $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}); + 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 ]; + 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; - } + 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'); - return $self->{can_administer}; + 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) = @_; + my ($self, $product_name) = @_; - # First make sure the product name is valid. - my $product = Bugzilla::Product->check($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}); + ( $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; + # 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; + 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) = @_; + 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; + 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) = @_; + my ($self, $flag_type) = @_; - return (!$flag_type->grant_group_id - || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0; + 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}; + 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 + 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(); + 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; + while (my ($row) = $sth->fetchrow_array) { + push @visgroups, $row; + } + $self->{visible_groups_direct} = \@visgroups; - return $self->{visible_groups_direct}; + return $self->{visible_groups_direct}; } sub visible_groups_as_string { - my $self = shift; - return join(', ', @{$self->visible_groups_inherited()}); + 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 }; - } + 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; + return $self->{queryshare_groups} = \@queryshare_groups; } sub queryshare_groups_as_string { - my $self = shift; - return join(', ', @{$self->queryshare_groups()}); + my $self = shift; + return join(', ', @{$self->queryshare_groups()}); } sub derive_regexp_groups { - my ($self) = @_; + my ($self) = @_; - my $id = $self->id; - return unless $id; + my $id = $self->id; + return unless $id; - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $sth; + my $sth; - # add derived records for any matching regexps + # add derived records for any matching regexps - $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id + $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); + AND user_group_map.grant_type = ?" + ); + $sth->execute($id, GRANT_REGEXP); - my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map + 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 + 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; - } + 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" }); + Bugzilla->memcached->clear_config({key => "user_groups.$id"}); } sub product_responsibilities { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - return $self->{'product_resp'} if defined $self->{'product_resp'}; - return [] unless $self->id; + return $self->{'product_resp'} if defined $self->{'product_resp'}; + return [] unless $self->id; - my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.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'}; - } + {Slice => {}}, ($self->id, $self->id, $self->id) + ); - 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; + 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; + 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; - } + if (!scalar(@_)) { - # Otherwise, we're checking a specific group - my $group_id = shift; - return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0; + # 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; + # 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 + ); - # then try substring search - if (!scalar(@users) && length($str) >= 3 && $user->id) { - trick_taint($str); + push(@users, new Bugzilla::User($user_id)) if $user_id; + } - my $query = "SELECT DISTINCT userid FROM profiles "; - if (Bugzilla->params->{'usevisibilitygroups'}) { - $query .= "INNER JOIN user_group_map + # 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; + $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}; - } - } - } + 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; + } + $fields = $expanded_fields; - foreach my $field (keys %{$fields}) { - next unless defined $data->{$field}; + 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', - }); - } + #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 || ''); - # 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 ''); + # 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); - my $limit = 0; - if ($params->{'maxusermatches'}) { - $limit = $params->{'maxusermatches'} + 1; - } + # 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); - 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; + # 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'}; - # 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(@{$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 } - elsif (scalar @logins) { - $data->{$field} = \@logins; + 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; } - my $retval; - if (!$matchsuccess) { - $retval = USER_MATCH_FAILED; + # 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 ($match_multiple) { - $retval = USER_MATCH_MULTIPLE; - } - else { - $retval = USER_MATCH_SUCCESS; + elsif (scalar @logins) { + $data->{$field} = \@logins; } + } - # 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 $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 = {}; + 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; + $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(); + print $cgi->header(); - $template->process("global/confirm-user-match.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $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); + '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; - } + my $self = shift; + my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_; - # 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; - } - } - } + # 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}; - 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; - } + # A change to any of the above fields sets the corresponding event + if (defined($names_to_events{$fieldName})) { + $events{$names_to_events{$fieldName}} = 1; } - - if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { - $events{+EVT_ATTACHMENT} = 1; + else { + # Catch-all for any change not caught by a more specific event + $events{+EVT_OTHER} = 1; } - elsif (defined($$comments[0])) { - $events{+EVT_COMMENT} = 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; } - - # Dependent changed bugmails must have an event to ensure the bugmail is - # emailed. - if ($dep_mail) { - $events{+EVT_DEPEND_BLOCK} = 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; + } } + } - 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); + 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; } - - return $wants_mail; + } + + 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; - } + 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); - # Skip DB query if relationship is explicit - return 1 if $relationship == REL_GLOBAL_WATCHER; + # If a relationship isn't given, default to REL_ANY. + if (!defined($relationship)) { + $relationship = REL_ANY; + } - my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events; - return $wants_mail ? 1 : 0; + # 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'}; + 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; + 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'}; + 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; + 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'}; + 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; + 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'}; + 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; + 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'}; + 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; + 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'}; + 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'}; + 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)); - } + 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(); + $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'); + # 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 + $dbh->do( + 'INSERT INTO profiles_activity (userid, who, profiles_when, fieldid, newvalue) - VALUES (?, ?, NOW(), ?, NOW())', - undef, ($user->id, $who, $creation_date_fieldid)); + VALUES (?, ?, NOW(), ?, NOW())', undef, + ($user->id, $who, $creation_date_fieldid) + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Return the newly created user account. - return $user; + # Return the newly created user account. + return $user; } ########################### @@ -2305,44 +2396,45 @@ sub create { ########################### 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; + 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}; + 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}; + 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 + 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}; + ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr + ); + return $self->{account_ip_login_failures}; } ############### @@ -2350,145 +2442,162 @@ sub account_ip_login_failures { ############### sub is_available_username { - my ($username, $old_username) = @_; - - if(login_to_id($username) != 0) { - return 0; - } + my ($username, $old_username) = @_; - 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 + 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)) = ?) + 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; + 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; + return 1; } sub check_account_creation_enabled { - my $self = shift; + 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'); + # 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'); + Bugzilla->params->{'createemailregexp'} + || ThrowUserError('account_creation_disabled'); } sub check_and_send_account_creation_confirmation { - my ($self, $login) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $login) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction; + $dbh->bz_start_transaction; - $login = $self->check_login_name($login); - my $creation_regexp = Bugzilla->params->{'createemailregexp'}; + $login = $self->check_login_name($login); + my $creation_regexp = Bugzilla->params->{'createemailregexp'}; - if ($login !~ /$creation_regexp/i) { - ThrowUserError('account_creation_restricted'); - } + if ($login !~ /$creation_regexp/i) { + ThrowUserError('account_creation_restricted'); + } - # Allow extensions to do extra checks. - Bugzilla::Hook::process('user_check_account_creation', { login => $login }); + # 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); + # Create and send a token for this new account. + require Bugzilla::Token; + Bugzilla::Token::issue_new_user_account_token($login); - $dbh->bz_commit_transaction; + $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; - } + 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; + 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:]]/); - } - - # Having done these checks makes us consider the password untainted. - trick_taint($_[0]); - return; + 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:]]/); + } + + # Having done these checks makes us consider the password untainted. + trick_taint($_[0]); + return; } |