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