1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4#
5# This Source Code Form is "Incompatible With Secondary Licenses", as
6# defined by the Mozilla Public License, v. 2.0.
7
8package Bugzilla::User;
9
10use 5.10.1;
11use strict;
12use warnings;
13
14use Bugzilla::Error;
15use Bugzilla::Util;
16use Bugzilla::Constants;
17use Bugzilla::Search::Recent;
18use Bugzilla::User::Setting;
19use Bugzilla::Product;
20use Bugzilla::Classification;
21use Bugzilla::Field;
22use Bugzilla::Group;
23use Bugzilla::BugUserLastVisit;
24use Bugzilla::Hook;
25
26use DateTime::TimeZone;
27use List::Util qw(max);
28use List::MoreUtils qw(any);
29use Scalar::Util qw(blessed);
30use Storable qw(dclone);
31use URI;
32use URI::QueryParam;
33
34use parent qw(Bugzilla::Object Exporter);
35@Bugzilla::User::EXPORT = qw(is_available_username
36    login_to_id validate_password validate_password_check
37    USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
38    MATCH_SKIP_CONFIRM
39);
40
41#####################################################################
42# Constants
43#####################################################################
44
45use constant USER_MATCH_MULTIPLE => -1;
46use constant USER_MATCH_FAILED   => 0;
47use constant USER_MATCH_SUCCESS  => 1;
48
49use constant MATCH_SKIP_CONFIRM  => 1;
50
51use constant DEFAULT_USER => {
52    'userid'         => 0,
53    'realname'       => '',
54    'login_name'     => '',
55    'showmybugslink' => 0,
56    'disabledtext'   => '',
57    'disable_mail'   => 0,
58    'is_enabled'     => 1,
59};
60
61use constant DB_TABLE => 'profiles';
62
63# XXX Note that Bugzilla::User->name does not return the same thing
64# that you passed in for "name" to new(). That's because historically
65# Bugzilla::User used "name" for the realname field. This should be
66# fixed one day.
67sub DB_COLUMNS {
68    my $dbh = Bugzilla->dbh;
69    return (
70        'profiles.userid',
71        'profiles.login_name',
72        'profiles.realname',
73        'profiles.mybugslink AS showmybugslink',
74        'profiles.disabledtext',
75        'profiles.disable_mail',
76        'profiles.extern_id',
77        'profiles.is_enabled',
78        $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
79    ),
80}
81
82use constant NAME_FIELD => 'login_name';
83use constant ID_FIELD   => 'userid';
84use constant LIST_ORDER => NAME_FIELD;
85
86use constant VALIDATORS => {
87    cryptpassword => \&_check_password,
88    disable_mail  => \&_check_disable_mail,
89    disabledtext  => \&_check_disabledtext,
90    login_name    => \&check_login_name,
91    realname      => \&_check_realname,
92    extern_id     => \&_check_extern_id,
93    is_enabled    => \&_check_is_enabled,
94};
95
96sub UPDATE_COLUMNS {
97    my $self = shift;
98    my @cols = qw(
99        disable_mail
100        disabledtext
101        login_name
102        realname
103        extern_id
104        is_enabled
105    );
106    push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
107    return @cols;
108};
109
110use constant VALIDATOR_DEPENDENCIES => {
111    is_enabled => ['disabledtext'],
112};
113
114use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
115
116################################################################################
117# Functions
118################################################################################
119
120sub new {
121    my $invocant = shift;
122    my $class = ref($invocant) || $invocant;
123    my ($param) = @_;
124
125    my $user = { %{ DEFAULT_USER() } };
126    bless ($user, $class);
127    return $user unless $param;
128
129    if (ref($param) eq 'HASH') {
130        if (defined $param->{extern_id}) {
131            $param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] };
132            $_[0] = $param;
133        }
134    }
135    return $class->SUPER::new(@_);
136}
137
138sub super_user {
139    my $invocant = shift;
140    my $class = ref($invocant) || $invocant;
141    my ($param) = @_;
142
143    my $user = { %{ DEFAULT_USER() } };
144    $user->{groups} = [Bugzilla::Group->get_all];
145    $user->{bless_groups} = [Bugzilla::Group->get_all];
146    bless $user, $class;
147    return $user;
148}
149
150sub _update_groups {
151    my $self = shift;
152    my $group_changes = shift;
153    my $changes = shift;
154    my $dbh = Bugzilla->dbh;
155
156    # Update group settings.
157    my $sth_add_mapping = $dbh->prepare(
158        qq{INSERT INTO user_group_map (
159                  user_id, group_id, isbless, grant_type
160                 ) VALUES (
161                  ?, ?, ?, ?
162                 )
163          });
164    my $sth_remove_mapping = $dbh->prepare(
165        qq{DELETE FROM user_group_map
166            WHERE user_id = ?
167              AND group_id = ?
168              AND isbless = ?
169              AND grant_type = ?
170          });
171
172    foreach my $is_bless (keys %$group_changes) {
173        my ($removed, $added) = @{$group_changes->{$is_bless}};
174
175        foreach my $group (@$removed) {
176            $sth_remove_mapping->execute(
177                $self->id, $group->id, $is_bless, GRANT_DIRECT
178             );
179        }
180        foreach my $group (@$added) {
181            $sth_add_mapping->execute(
182                $self->id, $group->id, $is_bless, GRANT_DIRECT
183             );
184        }
185
186        if (! $is_bless) {
187            my $query = qq{
188                INSERT INTO profiles_activity
189                    (userid, who, profiles_when, fieldid, oldvalue, newvalue)
190                VALUES ( ?, ?, now(), ?, ?, ?)
191            };
192
193            $dbh->do(
194                $query, undef,
195                $self->id, Bugzilla->user->id,
196                get_field_id('bug_group'),
197                join(', ', map { $_->name } @$removed),
198                join(', ', map { $_->name } @$added)
199            );
200        }
201        else {
202            # XXX: should create profiles_activity entries for blesser changes.
203        }
204
205        Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id });
206
207        my $type = $is_bless ? 'bless_groups' : 'groups';
208        $changes->{$type} = [
209            [ map { $_->name } @$removed ],
210            [ map { $_->name } @$added ],
211        ];
212    }
213}
214
215sub update {
216    my $self = shift;
217    my $options = shift;
218
219    my $group_changes = delete $self->{_group_changes};
220
221    my $changes = $self->SUPER::update(@_);
222    my $dbh = Bugzilla->dbh;
223    $self->_update_groups($group_changes, $changes);
224
225    if (exists $changes->{login_name}) {
226        # Delete all the tokens related to the userid
227        $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id)
228          unless $options->{keep_tokens};
229        # And rederive regex groups
230        $self->derive_regexp_groups();
231    }
232
233    # Logout the user if necessary.
234    Bugzilla->logout_user($self)
235        if (!$options->{keep_session}
236            && (exists $changes->{login_name}
237                || exists $changes->{disabledtext}
238                || exists $changes->{cryptpassword}));
239
240    # XXX Can update profiles_activity here as soon as it understands
241    #     field names like login_name.
242
243    return $changes;
244}
245
246################################################################################
247# Validators
248################################################################################
249
250sub _check_disable_mail { return $_[1] ? 1 : 0; }
251sub _check_disabledtext { return trim($_[1]) || ''; }
252
253# Check whether the extern_id is unique.
254sub _check_extern_id {
255    my ($invocant, $extern_id) = @_;
256    $extern_id = trim($extern_id);
257    return undef unless defined($extern_id) && $extern_id ne "";
258    if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
259        my $existing_login = $invocant->new({ extern_id => $extern_id });
260        if ($existing_login) {
261            ThrowUserError( 'extern_id_exists',
262                            { extern_id => $extern_id,
263                              existing_login_name => $existing_login->login });
264        }
265    }
266    return $extern_id;
267}
268
269# This is public since createaccount.cgi needs to use it before issuing
270# a token for account creation.
271sub check_login_name {
272    my ($invocant, $name) = @_;
273    $name = trim($name);
274    $name || ThrowUserError('user_login_required');
275    check_email_syntax($name);
276
277    # Check the name if it's a new user, or if we're changing the name.
278    if (!ref($invocant) || lc($invocant->login) ne lc($name)) {
279        my @params = ($name);
280        push(@params, $invocant->login) if ref($invocant);
281        is_available_username(@params)
282            || ThrowUserError('account_exists', { email => $name });
283    }
284
285    return $name;
286}
287
288sub _check_password {
289    my ($self, $pass) = @_;
290
291    # If the password is '*', do not encrypt it or validate it further--we
292    # are creating a user who should not be able to log in using DB
293    # authentication.
294    return $pass if $pass eq '*';
295
296    validate_password($pass);
297    my $cryptpassword = bz_crypt($pass);
298    return $cryptpassword;
299}
300
301sub _check_realname { return trim($_[1]) || ''; }
302
303sub _check_is_enabled {
304    my ($invocant, $is_enabled, undef, $params) = @_;
305    # is_enabled is set automatically on creation depending on whether
306    # disabledtext is empty (enabled) or not empty (disabled).
307    # When updating the user, is_enabled is set by calling set_disabledtext().
308    # Any value passed into this validator is ignored.
309    my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext};
310    return $disabledtext ? 0 : 1;
311}
312
313################################################################################
314# Mutators
315################################################################################
316
317sub set_disable_mail  { $_[0]->set('disable_mail', $_[1]); }
318sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); }
319sub set_extern_id     { $_[0]->set('extern_id', $_[1]); }
320
321sub set_login {
322    my ($self, $login) = @_;
323    $self->set('login_name', $login);
324    delete $self->{identity};
325    delete $self->{nick};
326}
327
328sub set_name {
329    my ($self, $name) = @_;
330    $self->set('realname', $name);
331    delete $self->{identity};
332}
333
334sub set_password { $_[0]->set('cryptpassword', $_[1]); }
335
336sub set_disabledtext {
337    $_[0]->set('disabledtext', $_[1]);
338    $_[0]->set('is_enabled', $_[1] ? 0 : 1);
339}
340
341sub set_groups {
342    my $self = shift;
343    $self->_set_groups(GROUP_MEMBERSHIP, @_);
344}
345
346sub set_bless_groups {
347    my $self = shift;
348
349    # The person making the change needs to be in the editusers group
350    Bugzilla->user->in_group('editusers')
351        || ThrowUserError("auth_failure", {group  => "editusers",
352                                           reason => "cant_bless",
353                                           action => "edit",
354                                           object => "users"});
355
356    $self->_set_groups(GROUP_BLESS, @_);
357}
358
359sub _set_groups {
360    my $self     = shift;
361    my $is_bless = shift;
362    my $changes  = shift;
363    my $dbh = Bugzilla->dbh;
364
365    # The person making the change is $user, $self is the person being changed
366    my $user = Bugzilla->user;
367
368    # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array
369    # is a list of group ids and/or names.
370
371    # First turn the arrays into group objects.
372    $changes = $self->_set_groups_to_object($changes);
373
374    # Get a list of the groups the user currently is a member of
375    my $ids = $dbh->selectcol_arrayref(
376        q{SELECT DISTINCT group_id
377            FROM user_group_map
378           WHERE user_id = ? AND isbless = ? AND grant_type = ?},
379        undef, $self->id, $is_bless, GRANT_DIRECT);
380
381    my $current_groups = Bugzilla::Group->new_from_list($ids);
382    my $new_groups = dclone($current_groups);
383
384    # Record the changes
385    if (exists $changes->{set}) {
386        $new_groups = $changes->{set};
387
388        # We need to check the user has bless rights on the existing groups
389        # If they don't, then we need to add them back to new_groups
390        foreach my $group (@$current_groups) {
391            if (! $user->can_bless($group->id)) {
392                push @$new_groups, $group
393                    unless grep { $_->id eq $group->id } @$new_groups;
394            }
395        }
396    }
397    else {
398        foreach my $group (@{$changes->{remove} // []}) {
399            @$new_groups = grep { $_->id ne $group->id } @$new_groups;
400        }
401        foreach my $group (@{$changes->{add} // []}) {
402            push @$new_groups, $group
403                unless grep { $_->id eq $group->id } @$new_groups;
404        }
405    }
406
407    # Stash the changes, so self->update can actually make them
408    my @diffs = diff_arrays($current_groups, $new_groups, 'id');
409    if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) {
410        $self->{_group_changes}{$is_bless} = \@diffs;
411    }
412}
413
414sub _set_groups_to_object {
415    my $self = shift;
416    my $changes = shift;
417    my $user = Bugzilla->user;
418
419    foreach my $key (keys %$changes) {
420        # Check we were given an array
421        unless (ref($changes->{$key}) eq 'ARRAY') {
422            ThrowCodeError(
423                'param_invalid',
424                { param => $changes->{$key}, function => $key }
425            );
426        }
427
428        # Go through the array, and turn items into group objects
429        my @groups = ();
430        foreach my $value (@{$changes->{$key}}) {
431            my $type = $value =~ /^\d+$/ ? 'id' : 'name';
432            my $group = Bugzilla::Group->new({$type => $value});
433
434            if (! $group || ! $user->can_bless($group->id)) {
435                ThrowUserError('auth_failure',
436                    { group  => $value, reason => 'cant_bless',
437                      action => 'edit', object => 'users' });
438            }
439            push @groups, $group;
440        }
441        $changes->{$key} = \@groups;
442    }
443
444    return $changes;
445}
446
447sub update_last_seen_date {
448    my $self = shift;
449    return unless $self->id;
450    my $dbh = Bugzilla->dbh;
451    my $date = $dbh->selectrow_array(
452        'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
453
454    if (!$self->last_seen_date or $date ne $self->last_seen_date) {
455        $self->{last_seen_date} = $date;
456        # We don't use the normal update() routine here as we only
457        # want to update the last_seen_date column, not any other
458        # pending changes
459        $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?",
460                 undef, $date, $self->id);
461        Bugzilla->memcached->clear({ table => 'profiles', id => $self->id });
462    }
463}
464
465################################################################################
466# Methods
467################################################################################
468
469# Accessors for user attributes
470sub name  { $_[0]->{realname};   }
471sub login { $_[0]->{login_name}; }
472sub extern_id { $_[0]->{extern_id}; }
473sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
474sub disabledtext { $_[0]->{'disabledtext'}; }
475sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
476sub showmybugslink { $_[0]->{showmybugslink}; }
477sub email_disabled { $_[0]->{disable_mail}; }
478sub email_enabled { !($_[0]->{disable_mail}); }
479sub last_seen_date { $_[0]->{last_seen_date}; }
480sub cryptpassword {
481    my $self = shift;
482    # We don't store it because we never want it in the object (we
483    # don't want to accidentally dump even the hash somewhere).
484    my ($pw) = Bugzilla->dbh->selectrow_array(
485        'SELECT cryptpassword FROM profiles WHERE userid = ?',
486        undef, $self->id);
487    return $pw;
488}
489
490sub set_authorizer {
491    my ($self, $authorizer) = @_;
492    $self->{authorizer} = $authorizer;
493}
494sub authorizer {
495    my ($self) = @_;
496    if (!$self->{authorizer}) {
497        require Bugzilla::Auth;
498        $self->{authorizer} = new Bugzilla::Auth();
499    }
500    return $self->{authorizer};
501}
502
503# Generate a string to identify the user by name + login if the user
504# has a name or by login only if they don't.
505sub identity {
506    my $self = shift;
507
508    return "" unless $self->id;
509
510    if (!defined $self->{identity}) {
511        $self->{identity} =
512          $self->name ? $self->name . " <" . $self->login. ">" : $self->login;
513    }
514
515    return $self->{identity};
516}
517
518sub nick {
519    my $self = shift;
520
521    return "" unless $self->id;
522
523    if (!defined $self->{nick}) {
524        $self->{nick} = (split(/@/, $self->login, 2))[0];
525    }
526
527    return $self->{nick};
528}
529
530sub queries {
531    my $self = shift;
532    return $self->{queries} if defined $self->{queries};
533    return [] unless $self->id;
534
535    my $dbh = Bugzilla->dbh;
536    my $query_ids = $dbh->selectcol_arrayref(
537        'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id);
538    require Bugzilla::Search::Saved;
539    $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids);
540
541    # We preload link_in_footer from here as this information is always requested.
542    # This only works if the user object represents the current logged in user.
543    Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id;
544
545    return $self->{queries};
546}
547
548sub queries_subscribed {
549    my $self = shift;
550    return $self->{queries_subscribed} if defined $self->{queries_subscribed};
551    return [] unless $self->id;
552
553    # Exclude the user's own queries.
554    my @my_query_ids = map($_->id, @{$self->queries});
555    my $query_id_string = join(',', @my_query_ids) || '-1';
556
557    # Only show subscriptions that we can still actually see. If a
558    # user changes the shared group of a query, our subscription
559    # will remain but we won't have access to the query anymore.
560    my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref(
561        "SELECT lif.namedquery_id
562           FROM namedqueries_link_in_footer lif
563                INNER JOIN namedquery_group_map ngm
564                ON ngm.namedquery_id = lif.namedquery_id
565          WHERE lif.user_id = ?
566                AND lif.namedquery_id NOT IN ($query_id_string)
567                AND " . $self->groups_in_sql,
568          undef, $self->id);
569    require Bugzilla::Search::Saved;
570    $self->{queries_subscribed} =
571        Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
572    return $self->{queries_subscribed};
573}
574
575sub queries_available {
576    my $self = shift;
577    return $self->{queries_available} if defined $self->{queries_available};
578    return [] unless $self->id;
579
580    # Exclude the user's own queries.
581    my @my_query_ids = map($_->id, @{$self->queries});
582    my $query_id_string = join(',', @my_query_ids) || '-1';
583
584    my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
585        'SELECT namedquery_id FROM namedquery_group_map
586          WHERE '  . $self->groups_in_sql . "
587                AND namedquery_id NOT IN ($query_id_string)");
588    require Bugzilla::Search::Saved;
589    $self->{queries_available} =
590        Bugzilla::Search::Saved->new_from_list($avail_query_ids);
591    return $self->{queries_available};
592}
593
594sub tags {
595    my $self = shift;
596    my $dbh = Bugzilla->dbh;
597
598    if (!defined $self->{tags}) {
599        # We must use LEFT JOIN instead of INNER JOIN as we may be
600        # in the process of inserting a new tag to some bugs,
601        # in which case there are no bugs with this tag yet.
602        $self->{tags} = $dbh->selectall_hashref(
603            'SELECT name, id, COUNT(bug_id) AS bug_count
604               FROM tag
605          LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id
606              WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'),
607            'name', undef, $self->id);
608    }
609    return $self->{tags};
610}
611
612sub bugs_ignored {
613    my ($self) = @_;
614    my $dbh = Bugzilla->dbh;
615    if (!defined $self->{'bugs_ignored'}) {
616        $self->{'bugs_ignored'} = $dbh->selectall_arrayref(
617            'SELECT bugs.bug_id AS id,
618                    bugs.bug_status AS status,
619                    bugs.short_desc AS summary
620               FROM bugs
621                    INNER JOIN email_bug_ignore
622                    ON bugs.bug_id = email_bug_ignore.bug_id
623              WHERE user_id = ?',
624            { Slice => {} }, $self->id);
625        # Go ahead and load these into the visible bugs cache
626        # to speed up can_see_bug checks later
627        $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]);
628    }
629    return $self->{'bugs_ignored'};
630}
631
632sub is_bug_ignored {
633    my ($self, $bug_id) = @_;
634    return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0;
635}
636
637##########################
638# Saved Recent Bug Lists #
639##########################
640
641sub recent_searches {
642    my $self = shift;
643    $self->{recent_searches} ||=
644        Bugzilla::Search::Recent->match({ user_id => $self->id });
645    return $self->{recent_searches};
646}
647
648sub recent_search_containing {
649    my ($self, $bug_id) = @_;
650    my $searches = $self->recent_searches;
651
652    foreach my $search (@$searches) {
653        return $search if grep($_ == $bug_id, @{ $search->bug_list });
654    }
655
656    return undef;
657}
658
659sub recent_search_for {
660    my ($self, $bug) = @_;
661    my $params = Bugzilla->input_params;
662    my $cgi = Bugzilla->cgi;
663
664    if ($self->id) {
665        # First see if there's a list_id parameter in the query string.
666        my $list_id = $params->{list_id};
667        if (!$list_id) {
668            # If not, check for "list_id" in the query string of the referer.
669            my $referer = $cgi->referer;
670            if ($referer) {
671                my $uri = URI->new($referer);
672                if ($uri->path =~ /buglist\.cgi$/) {
673                    $list_id = $uri->query_param('list_id')
674                               || $uri->query_param('regetlastlist');
675                }
676            }
677        }
678
679        if ($list_id && $list_id ne 'cookie') {
680            # If we got a bad list_id (either some other user's or an expired
681            # one) don't crash, just don't return that list.
682            my $search = Bugzilla::Search::Recent->check_quietly(
683                { id => $list_id });
684            return $search if $search;
685        }
686
687        # If there's no list_id, see if the current bug's id is contained
688        # in any of the user's saved lists.
689        my $search = $self->recent_search_containing($bug->id);
690        return $search if $search;
691    }
692
693    # Finally (or always, if we're logged out), if there's a BUGLIST cookie
694    # and the selected bug is in the list, then return the cookie as a fake
695    # Search::Recent object.
696    if (my $list = $cgi->cookie('BUGLIST')) {
697        # Also split on colons, which was used as a separator in old cookies.
698        my @bug_ids = split(/[:-]/, $list);
699        if (grep { $_ == $bug->id } @bug_ids) {
700            my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
701            return $search;
702        }
703    }
704
705    return undef;
706}
707
708sub save_last_search {
709    my ($self, $params) = @_;
710    my ($bug_ids, $order, $vars, $list_id) =
711        @$params{qw(bugs order vars list_id)};
712
713    my $cgi = Bugzilla->cgi;
714    if ($order) {
715        $cgi->send_cookie(-name => 'LASTORDER',
716                          -value => $order,
717                          -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
718    }
719
720    return if !@$bug_ids;
721
722    my $search;
723    if ($self->id) {
724        on_main_db {
725            if ($list_id) {
726                $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id });
727            }
728
729            if ($search) {
730                if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
731                    $search->set_bug_list($bug_ids);
732                }
733                if (!$search->list_order || $order ne $search->list_order) {
734                    $search->set_list_order($order);
735                }
736                $search->update();
737            }
738            else {
739                # If we already have an existing search with a totally
740                # identical bug list, then don't create a new one. This
741                # prevents people from writing over their whole
742                # recent-search list by just refreshing a saved search
743                # (which doesn't have list_id in the header) over and over.
744                my $list_string = join(',', @$bug_ids);
745                my $existing_search = Bugzilla::Search::Recent->match({
746                    user_id => $self->id, bug_list => $list_string });
747
748                if (!scalar(@$existing_search)) {
749                    $search = Bugzilla::Search::Recent->create({
750                        user_id    => $self->id,
751                        bug_list   => $bug_ids,
752                        list_order => $order });
753                }
754                else {
755                    $search = $existing_search->[0];
756                }
757            }
758        };
759        delete $self->{recent_searches};
760    }
761    # Logged-out users use a cookie to store a single last search. We don't
762    # override that cookie with the logged-in user's latest search, because
763    # if they did one search while logged out and another while logged in,
764    # they may still want to navigate through the search they made while
765    # logged out.
766    else {
767        my $bug_list = join('-', @$bug_ids);
768        if (length($bug_list) < 4000) {
769            $cgi->send_cookie(-name => 'BUGLIST',
770                              -value => $bug_list,
771                              -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
772        }
773        else {
774            $cgi->remove_cookie('BUGLIST');
775            $vars->{'toolong'} = 1;
776        }
777    }
778    return $search;
779}
780
781sub reports {
782    my $self = shift;
783    return $self->{reports} if defined $self->{reports};
784    return [] unless $self->id;
785
786    my $dbh = Bugzilla->dbh;
787    my $report_ids = $dbh->selectcol_arrayref(
788        'SELECT id FROM reports WHERE user_id = ?', undef, $self->id);
789    require Bugzilla::Report;
790    $self->{reports} = Bugzilla::Report->new_from_list($report_ids);
791    return $self->{reports};
792}
793
794sub flush_reports_cache {
795    my $self = shift;
796
797    delete $self->{reports};
798}
799
800sub settings {
801    my ($self) = @_;
802
803    return $self->{'settings'} if (defined $self->{'settings'});
804
805    # IF the user is logged in
806    # THEN get the user's settings
807    # ELSE get default settings
808    if ($self->id) {
809        $self->{'settings'} = get_all_settings($self->id);
810    } else {
811        $self->{'settings'} = get_defaults();
812    }
813
814    return $self->{'settings'};
815}
816
817sub setting {
818    my ($self, $name) = @_;
819    return $self->settings->{$name}->{'value'};
820}
821
822sub timezone {
823    my $self = shift;
824
825    if (!defined $self->{timezone}) {
826        my $tz = $self->setting('timezone');
827        if ($tz eq 'local') {
828            # The user wants the local timezone of the server.
829            $self->{timezone} = Bugzilla->local_timezone;
830        }
831        else {
832            $self->{timezone} = DateTime::TimeZone->new(name => $tz);
833        }
834    }
835    return $self->{timezone};
836}
837
838sub flush_queries_cache {
839    my $self = shift;
840
841    delete $self->{queries};
842    delete $self->{queries_subscribed};
843    delete $self->{queries_available};
844}
845
846sub groups {
847    my $self = shift;
848
849    return $self->{groups} if defined $self->{groups};
850    return [] unless $self->id;
851
852    my $user_groups_key = "user_groups." . $self->id;
853    my $groups = Bugzilla->memcached->get_config({
854        key => $user_groups_key
855    });
856
857    if (!$groups) {
858        my $dbh = Bugzilla->dbh;
859        my $groups_to_check = $dbh->selectcol_arrayref(
860            "SELECT DISTINCT group_id
861               FROM user_group_map
862              WHERE user_id = ? AND isbless = 0", undef, $self->id);
863
864        my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP;
865        my $membership_rows = Bugzilla->memcached->get_config({
866            key => $grant_type_key,
867        });
868        if (!$membership_rows) {
869            $membership_rows = $dbh->selectall_arrayref(
870                "SELECT DISTINCT grantor_id, member_id
871                FROM group_group_map
872                WHERE grant_type = " . GROUP_MEMBERSHIP);
873            Bugzilla->memcached->set_config({
874                key  => $grant_type_key,
875                data => $membership_rows,
876            });
877        }
878
879        my %group_membership;
880        foreach my $row (@$membership_rows) {
881            my ($grantor_id, $member_id) = @$row;
882            push (@{ $group_membership{$member_id} }, $grantor_id);
883        }
884
885        # Let's walk the groups hierarchy tree (using FIFO)
886        # On the first iteration it's pre-filled with direct groups
887        # membership. Later on, each group can add its own members into the
888        # FIFO. Circular dependencies are eliminated by checking
889        # $checked_groups{$member_id} hash values.
890        # As a result, %groups will have all the groups we are the member of.
891        my %checked_groups;
892        my %groups;
893        while (scalar(@$groups_to_check) > 0) {
894            # Pop the head group from FIFO
895            my $member_id = shift @$groups_to_check;
896
897            # Skip the group if we have already checked it
898            if (!$checked_groups{$member_id}) {
899                # Mark group as checked
900                $checked_groups{$member_id} = 1;
901
902                # Add all its members to the FIFO check list
903                # %group_membership contains arrays of group members
904                # for all groups. Accessible by group number.
905                my $members = $group_membership{$member_id};
906                my @new_to_check = grep(!$checked_groups{$_}, @$members);
907                push(@$groups_to_check, @new_to_check);
908
909                $groups{$member_id} = 1;
910            }
911        }
912        $groups = [ keys %groups ];
913
914        Bugzilla->memcached->set_config({
915            key  => $user_groups_key,
916            data => $groups,
917        });
918    }
919
920    $self->{groups} = Bugzilla::Group->new_from_list($groups);
921    return $self->{groups};
922}
923
924sub last_visited {
925    my ($self, $ids) = @_;
926
927    return Bugzilla::BugUserLastVisit->match({ user_id => $self->id,
928                                               $ids ? ( bug_id => $ids ) : () });
929}
930
931sub is_involved_in_bug {
932    my ($self, $bug) = @_;
933    my $user_id    = $self->id;
934    my $user_login = $self->login;
935
936    return unless $user_id;
937    return 1 if $user_id == $bug->assigned_to->id;
938    return 1 if $user_id == $bug->reporter->id;
939
940    if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) {
941        return 1 if $user_id == $bug->qa_contact->id;
942    }
943
944    return any { $user_login eq $_ } @{ $bug->cc };
945}
946
947# It turns out that calling ->id on objects a few hundred thousand
948# times is pretty slow. (It showed up as a significant time contributor
949# when profiling xt/search.t.) So we cache the group ids separately from
950# groups for functions that need the group ids.
951sub _group_ids {
952    my ($self) = @_;
953    $self->{group_ids} ||= [map { $_->id } @{ $self->groups }];
954    return $self->{group_ids};
955}
956
957sub groups_as_string {
958    my $self = shift;
959    my $ids = $self->_group_ids;
960    return scalar(@$ids) ? join(',', @$ids) : '-1';
961}
962
963sub groups_in_sql {
964    my ($self, $field) = @_;
965    $field ||= 'group_id';
966    my $ids = $self->_group_ids;
967    $ids = [-1] if !scalar @$ids;
968    return Bugzilla->dbh->sql_in($field, $ids);
969}
970
971sub bless_groups {
972    my $self = shift;
973
974    return $self->{'bless_groups'} if defined $self->{'bless_groups'};
975    return [] unless $self->id;
976
977    if ($self->in_group('editusers')) {
978        # Users having editusers permissions may bless all groups.
979        $self->{'bless_groups'} = [Bugzilla::Group->get_all];
980        return $self->{'bless_groups'};
981    }
982
983    if (Bugzilla->params->{usevisibilitygroups}
984        && !@{ $self->visible_groups_inherited }) {
985        return [];
986    }
987
988    my $dbh = Bugzilla->dbh;
989
990    # Get all groups for the user where they have direct bless privileges.
991    my $query = "
992        SELECT DISTINCT group_id
993          FROM user_group_map
994         WHERE user_id = ?
995               AND isbless = 1";
996    if (Bugzilla->params->{usevisibilitygroups}) {
997        $query .= " AND "
998            . $dbh->sql_in('group_id', $self->visible_groups_inherited);
999    }
1000
1001    # Get all groups for the user where they are a member of a group that
1002    # inherits bless privs.
1003    my @group_ids = map { $_->id } @{ $self->groups };
1004    if (@group_ids) {
1005        $query .= "
1006            UNION
1007            SELECT DISTINCT grantor_id
1008            FROM group_group_map
1009            WHERE grant_type = " . GROUP_BLESS . "
1010                AND " . $dbh->sql_in('member_id', \@group_ids);
1011        if (Bugzilla->params->{usevisibilitygroups}) {
1012            $query .= " AND "
1013                . $dbh->sql_in('grantor_id', $self->visible_groups_inherited);
1014        }
1015    }
1016
1017    my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
1018    return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids);
1019}
1020
1021sub in_group {
1022    my ($self, $group, $product_id) = @_;
1023    $group = $group->name if blessed $group;
1024    if (scalar grep($_->name eq $group, @{ $self->groups })) {
1025        return 1;
1026    }
1027    elsif ($product_id && detaint_natural($product_id)) {
1028        # Make sure $group exists on a per-product basis.
1029        return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
1030
1031        $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"};
1032        if (!defined $self->{"product_$product_id"}->{$group}) {
1033            my $dbh = Bugzilla->dbh;
1034            my $in_group = $dbh->selectrow_array(
1035                           "SELECT 1
1036                              FROM group_control_map
1037                             WHERE product_id = ?
1038                                   AND $group != 0
1039                                   AND " . $self->groups_in_sql . ' ' .
1040                              $dbh->sql_limit(1),
1041                             undef, $product_id);
1042
1043            $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0;
1044        }
1045        return $self->{"product_$product_id"}->{$group};
1046    }
1047    # If we come here, then the user is not in the requested group.
1048    return 0;
1049}
1050
1051sub in_group_id {
1052    my ($self, $id) = @_;
1053    return grep($_->id == $id, @{ $self->groups }) ? 1 : 0;
1054}
1055
1056# This is a helper to get all groups which have an icon to be displayed
1057# besides the name of the commenter.
1058sub groups_with_icon {
1059    my $self = shift;
1060
1061    return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }];
1062}
1063
1064sub get_products_by_permission {
1065    my ($self, $group) = @_;
1066    # Make sure $group exists on a per-product basis.
1067    return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
1068
1069    my $product_ids = Bugzilla->dbh->selectcol_arrayref(
1070                          "SELECT DISTINCT product_id
1071                             FROM group_control_map
1072                            WHERE $group != 0
1073                              AND " . $self->groups_in_sql);
1074
1075    # No need to go further if the user has no "special" privs.
1076    return [] unless scalar(@$product_ids);
1077    my %product_map = map { $_ => 1 } @$product_ids;
1078
1079    # We will restrict the list to products the user can see.
1080    my $selectable_products = $self->get_selectable_products;
1081    my @products = grep { $product_map{$_->id} } @$selectable_products;
1082    return \@products;
1083}
1084
1085sub can_see_user {
1086    my ($self, $otherUser) = @_;
1087    my $query;
1088
1089    if (Bugzilla->params->{'usevisibilitygroups'}) {
1090        # If the user can see no groups, then no users are visible either.
1091        my $visibleGroups = $self->visible_groups_as_string() || return 0;
1092        $query = qq{SELECT COUNT(DISTINCT userid)
1093                    FROM profiles, user_group_map
1094                    WHERE userid = ?
1095                    AND user_id = userid
1096                    AND isbless = 0
1097                    AND group_id IN ($visibleGroups)
1098                   };
1099    } else {
1100        $query = qq{SELECT COUNT(userid)
1101                    FROM profiles
1102                    WHERE userid = ?
1103                   };
1104    }
1105    return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id);
1106}
1107
1108sub can_edit_product {
1109    my ($self, $prod_id) = @_;
1110    my $dbh = Bugzilla->dbh;
1111
1112    if (Bugzilla->params->{'or_groups'}) {
1113        my $groups = $self->groups_as_string;
1114        # For or-groups, we check if there are any can_edit groups for the
1115        # product, and if the user is in any of them. If there are none or
1116        # the user is in at least one of them, they can edit the product
1117        my ($cnt_can_edit, $cnt_group_member) = $dbh->selectrow_array(
1118           "SELECT SUM(p.cnt_can_edit),
1119                   SUM(p.cnt_group_member)
1120              FROM (SELECT CASE WHEN canedit = 1 THEN 1 ELSE 0 END AS cnt_can_edit,
1121                           CASE WHEN canedit = 1 AND group_id IN ($groups) THEN 1 ELSE 0 END AS cnt_group_member
1122                    FROM group_control_map
1123                    WHERE product_id = $prod_id) AS p");
1124        return (!$cnt_can_edit or $cnt_group_member);
1125    }
1126    else {
1127        # For and-groups, a user needs to be in all canedit groups. Therefore
1128        # if the user is not in a can_edit group for the product, they cannot
1129        # edit the product.
1130        my $has_external_groups =
1131          $dbh->selectrow_array('SELECT 1
1132                                   FROM group_control_map
1133                                  WHERE product_id = ?
1134                                    AND canedit != 0
1135                                    AND group_id NOT IN(' . $self->groups_as_string . ')',
1136                                 undef, $prod_id);
1137
1138        return !$has_external_groups;
1139    }
1140}
1141
1142sub can_see_bug {
1143    my ($self, $bug_id) = @_;
1144    return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0;
1145}
1146
1147sub visible_bugs {
1148    my ($self, $bugs) = @_;
1149    # Allow users to pass in Bug objects and bug ids both.
1150    my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs;
1151
1152    # We only check the visibility of bugs that we haven't
1153    # checked yet.
1154    # Bugzilla::Bug->update automatically removes updated bugs
1155    # from the cache to force them to be checked again.
1156    my $visible_cache = $self->{_visible_bugs_cache} ||= {};
1157    my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids);
1158
1159    if (@check_ids) {
1160        foreach my $id (@check_ids) {
1161            my $orig_id = $id;
1162            detaint_natural($id)
1163              || ThrowCodeError('param_must_be_numeric', { param    => $orig_id,
1164                                                           function => 'Bugzilla::User->visible_bugs'});
1165        }
1166
1167        Bugzilla->params->{'or_groups'}
1168            ? $self->_visible_bugs_check_or(\@check_ids)
1169            : $self->_visible_bugs_check_and(\@check_ids);
1170    }
1171
1172    return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
1173}
1174
1175sub _visible_bugs_check_or {
1176    my ($self, $check_ids) = @_;
1177    my $visible_cache = $self->{_visible_bugs_cache};
1178    my $dbh = Bugzilla->dbh;
1179    my $user_id = $self->id;
1180
1181    my $sth;
1182    # Speed up the can_see_bug case.
1183    if (scalar(@$check_ids) == 1) {
1184        $sth = $self->{_sth_one_visible_bug};
1185    }
1186    my $query = qq{
1187        SELECT DISTINCT bugs.bug_id
1188        FROM bugs
1189            LEFT JOIN bug_group_map AS security_map ON bugs.bug_id = security_map.bug_id
1190            LEFT JOIN cc AS security_cc ON bugs.bug_id = security_cc.bug_id AND security_cc.who = $user_id
1191        WHERE bugs.bug_id IN (} . join(',', ('?') x @$check_ids) . qq{)
1192          AND ((security_map.group_id IS NULL OR security_map.group_id IN (} . $self->groups_as_string . qq{))
1193            OR (bugs.reporter_accessible = 1 AND bugs.reporter = $user_id)
1194            OR (bugs.cclist_accessible = 1 AND security_cc.who IS NOT NULL)
1195            OR bugs.assigned_to = $user_id
1196    };
1197
1198    if (Bugzilla->params->{'useqacontact'}) {
1199        $query .= " OR bugs.qa_contact = $user_id";
1200    }
1201    $query .= ')';
1202
1203    $sth ||= $dbh->prepare($query);
1204    if (scalar(@$check_ids) == 1) {
1205        $self->{_sth_one_visible_bug} = $sth;
1206    }
1207
1208    # Set all bugs as non visible
1209    foreach my $bug_id (@$check_ids) {
1210        $visible_cache->{$bug_id} = 0;
1211    }
1212
1213    # Now get the bugs the user can see
1214    my $visible_bug_ids = $dbh->selectcol_arrayref($sth, undef, @$check_ids);
1215    foreach my $bug_id (@$visible_bug_ids) {
1216        $visible_cache->{$bug_id} = 1;
1217    }
1218}
1219
1220sub _visible_bugs_check_and {
1221    my ($self, $check_ids) = @_;
1222    my $visible_cache = $self->{_visible_bugs_cache};
1223    my $dbh = Bugzilla->dbh;
1224    my $user_id = $self->id;
1225
1226    my $sth;
1227    # Speed up the can_see_bug case.
1228    if (scalar(@$check_ids) == 1) {
1229        $sth = $self->{_sth_one_visible_bug};
1230    }
1231    $sth ||= $dbh->prepare(
1232        # This checks for groups that the bug is in that the user
1233        # *isn't* in. Then, in the Perl code below, we check if
1234        # the user can otherwise access the bug (for example, by being
1235        # the assignee or QA Contact).
1236        #
1237        # The DISTINCT exists because the bug could be in *several*
1238        # groups that the user isn't in, but they will all return the
1239        # same result for bug_group_map.bug_id (so DISTINCT filters
1240        # out duplicate rows).
1241        "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact,
1242                reporter_accessible, cclist_accessible, cc.who,
1243                bug_group_map.bug_id
1244           FROM bugs
1245                LEFT JOIN cc
1246                          ON cc.bug_id = bugs.bug_id
1247                             AND cc.who = $user_id
1248                LEFT JOIN bug_group_map
1249                          ON bugs.bug_id = bug_group_map.bug_id
1250                             AND bug_group_map.group_id NOT IN ("
1251                                 . $self->groups_as_string . ')
1252          WHERE bugs.bug_id IN (' . join(',', ('?') x @$check_ids) . ')
1253                AND creation_ts IS NOT NULL ');
1254    if (scalar(@$check_ids) == 1) {
1255        $self->{_sth_one_visible_bug} = $sth;
1256    }
1257
1258    $sth->execute(@$check_ids);
1259    my $use_qa_contact = Bugzilla->params->{'useqacontact'};
1260    while (my $row = $sth->fetchrow_arrayref) {
1261        my ($bug_id, $reporter, $owner, $qacontact, $reporter_access,
1262            $cclist_access, $isoncclist, $missinggroup) = @$row;
1263        $visible_cache->{$bug_id} ||=
1264            ((($reporter == $user_id) && $reporter_access)
1265             || ($use_qa_contact
1266                 && $qacontact && ($qacontact == $user_id))
1267             || ($owner == $user_id)
1268             || ($isoncclist && $cclist_access)
1269             || !$missinggroup) ? 1 : 0;
1270    }
1271
1272}
1273
1274sub clear_product_cache {
1275    my $self = shift;
1276    delete $self->{enterable_products};
1277    delete $self->{selectable_products};
1278    delete $self->{selectable_classifications};
1279}
1280
1281sub can_see_product {
1282    my ($self, $product_name) = @_;
1283
1284    return scalar(grep {$_->name eq $product_name} @{$self->get_selectable_products});
1285}
1286
1287sub get_selectable_products {
1288    my $self = shift;
1289    my $class_id = shift;
1290    my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
1291
1292    if (!defined $self->{selectable_products}) {
1293        my $query = "SELECT id
1294                     FROM products
1295                         LEFT JOIN group_control_map
1296                             ON group_control_map.product_id = products.id
1297                             AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY;
1298
1299        if (Bugzilla->params->{'or_groups'}) {
1300            # Either the user is in at least one of the MANDATORY groups, or
1301            # there are no such groups for the product.
1302            $query .= " WHERE group_id IN (" . $self->groups_as_string . ")
1303                        OR group_id IS NULL";
1304        }
1305        else {
1306            # There must be no MANDATORY groups that the user is not in.
1307            $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")
1308                        WHERE group_id IS NULL";
1309        }
1310
1311        my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
1312        $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
1313    }
1314
1315    # Restrict the list of products to those being in the classification, if any.
1316    if ($class_restricted) {
1317        return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}];
1318    }
1319    # If we come here, then we want all selectable products.
1320    return $self->{selectable_products};
1321}
1322
1323sub get_selectable_classifications {
1324    my ($self) = @_;
1325
1326    if (!defined $self->{selectable_classifications}) {
1327        my $products = $self->get_selectable_products;
1328        my %class_ids = map { $_->classification_id => 1 } @$products;
1329
1330        $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]);
1331    }
1332    return $self->{selectable_classifications};
1333}
1334
1335sub can_enter_product {
1336    my ($self, $input, $warn) = @_;
1337    my $dbh = Bugzilla->dbh;
1338    $warn ||= 0;
1339
1340    $input = trim($input) if !ref $input;
1341    if (!defined $input or $input eq '') {
1342        return unless $warn == THROW_ERROR;
1343        ThrowUserError('object_not_specified',
1344                       { class => 'Bugzilla::Product' });
1345    }
1346
1347    if (!scalar @{ $self->get_enterable_products }) {
1348        return unless $warn == THROW_ERROR;
1349        ThrowUserError('no_products');
1350    }
1351
1352    my $product = blessed($input) ? $input
1353                                  : new Bugzilla::Product({ name => $input });
1354    my $can_enter =
1355      $product && grep($_->name eq $product->name,
1356                       @{ $self->get_enterable_products });
1357
1358    return $product if $can_enter;
1359
1360    return 0 unless $warn == THROW_ERROR;
1361
1362    # Check why access was denied. These checks are slow,
1363    # but that's fine, because they only happen if we fail.
1364
1365    # We don't just use $product->name for error messages, because if it
1366    # changes case from $input, then that's a clue that the product does
1367    # exist but is hidden.
1368    my $name = blessed($input) ? $input->name : $input;
1369
1370    # The product could not exist or you could be denied...
1371    if (!$product || !$product->user_has_access($self)) {
1372        ThrowUserError('entry_access_denied', { product => $name });
1373    }
1374    # It could be closed for bug entry...
1375    elsif (!$product->is_active) {
1376        ThrowUserError('product_disabled', { product => $product });
1377    }
1378    # It could have no components...
1379    elsif (!@{$product->components}
1380           || !grep { $_->is_active } @{$product->components})
1381    {
1382        ThrowUserError('missing_component', { product => $product });
1383    }
1384    # It could have no versions...
1385    elsif (!@{$product->versions}
1386           || !grep { $_->is_active } @{$product->versions})
1387    {
1388        ThrowUserError ('missing_version', { product => $product });
1389    }
1390
1391    die "can_enter_product reached an unreachable location.";
1392}
1393
1394sub get_enterable_products {
1395    my $self = shift;
1396    my $dbh = Bugzilla->dbh;
1397
1398    if (defined $self->{enterable_products}) {
1399        return $self->{enterable_products};
1400    }
1401
1402     # All products which the user has "Entry" access to.
1403     my $query =
1404           'SELECT products.id FROM products
1405            LEFT JOIN group_control_map
1406                ON group_control_map.product_id = products.id
1407                    AND group_control_map.entry != 0';
1408
1409    if (Bugzilla->params->{'or_groups'}) {
1410        $query .= " WHERE (group_id IN (" . $self->groups_as_string . ")" .
1411                  "    OR group_id IS NULL)";
1412    } else {
1413        $query .= " AND group_id NOT IN (" . $self->groups_as_string . ")" .
1414                  " WHERE group_id IS NULL"
1415    }
1416    $query .= " AND products.isactive = 1";
1417    my $enterable_ids = $dbh->selectcol_arrayref($query);
1418
1419    if (scalar @$enterable_ids) {
1420        # And all of these products must have at least one component
1421        # and one version.
1422        $enterable_ids = $dbh->selectcol_arrayref(
1423            'SELECT DISTINCT products.id FROM products
1424              WHERE ' . $dbh->sql_in('products.id', $enterable_ids) .
1425              ' AND products.id IN (SELECT DISTINCT components.product_id
1426                                      FROM components
1427                                     WHERE components.isactive = 1)
1428                AND products.id IN (SELECT DISTINCT versions.product_id
1429                                      FROM versions
1430                                     WHERE versions.isactive = 1)');
1431    }
1432
1433    $self->{enterable_products} =
1434         Bugzilla::Product->new_from_list($enterable_ids);
1435    return $self->{enterable_products};
1436}
1437
1438sub can_access_product {
1439    my ($self, $product) = @_;
1440    my $product_name = blessed($product) ? $product->name : $product;
1441    return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products});
1442}
1443
1444sub get_accessible_products {
1445    my $self = shift;
1446
1447    # Map the objects into a hash using the ids as keys
1448    my %products = map { $_->id => $_ }
1449                       @{$self->get_selectable_products},
1450                       @{$self->get_enterable_products};
1451
1452    return [ sort { $a->name cmp $b->name } values %products ];
1453}
1454
1455sub can_administer {
1456    my $self = shift;
1457
1458    if (not defined $self->{can_administer}) {
1459        my $can_administer = 0;
1460
1461        $can_administer = 1 if $self->in_group('admin')
1462            || $self->in_group('tweakparams')
1463            || $self->in_group('editusers')
1464            || $self->can_bless
1465            || (Bugzilla->params->{'useclassification'} && $self->in_group('editclassifications'))
1466            || $self->in_group('editcomponents')
1467            || scalar(@{$self->get_products_by_permission('editcomponents')})
1468            || $self->in_group('creategroups')
1469            || $self->in_group('editkeywords')
1470            || $self->in_group('bz_canusewhines');
1471
1472        Bugzilla::Hook::process('user_can_administer', { can_administer => \$can_administer });
1473        $self->{can_administer} = $can_administer;
1474    }
1475
1476    return $self->{can_administer};
1477}
1478
1479sub check_can_admin_product {
1480    my ($self, $product_name) = @_;
1481
1482    # First make sure the product name is valid.
1483    my $product = Bugzilla::Product->check($product_name);
1484
1485    ($self->in_group('editcomponents', $product->id)
1486       && $self->can_see_product($product->name))
1487         || ThrowUserError('product_admin_denied', {product => $product->name});
1488
1489    # Return the validated product object.
1490    return $product;
1491}
1492
1493sub check_can_admin_flagtype {
1494    my ($self, $flagtype_id) = @_;
1495
1496    my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id });
1497    my $can_fully_edit = 1;
1498
1499    if (!$self->in_group('editcomponents')) {
1500        my $products = $self->get_products_by_permission('editcomponents');
1501        # You need editcomponents privs for at least one product to have
1502        # a chance to edit the flagtype.
1503        scalar(@$products)
1504          || ThrowUserError('auth_failure', {group  => 'editcomponents',
1505                                             action => 'edit',
1506                                             object => 'flagtypes'});
1507        my $can_admin = 0;
1508        my $i = $flagtype->inclusions_as_hash;
1509        my $e = $flagtype->exclusions_as_hash;
1510
1511        # If there is at least one product for which the user doesn't have
1512        # editcomponents privs, then don't allow them to do everything with
1513        # this flagtype, independently of whether this product is in the
1514        # exclusion list or not.
1515        my %product_ids;
1516        map { $product_ids{$_->id} = 1 } @$products;
1517        $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i;
1518
1519        unless ($e->{0}->{0}) {
1520            foreach my $product (@$products) {
1521                my $id = $product->id;
1522                next if $e->{$id}->{0};
1523                # If we are here, the product has not been explicitly excluded.
1524                # Check whether it's explicitly included, or at least one of
1525                # its components.
1526                $can_admin = ($i->{0}->{0} || $i->{$id}->{0}
1527                              || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}}));
1528                last if $can_admin;
1529            }
1530        }
1531        $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
1532    }
1533    return wantarray ? ($flagtype, $can_fully_edit) : $flagtype;
1534}
1535
1536sub can_request_flag {
1537    my ($self, $flag_type) = @_;
1538
1539    return ($self->can_set_flag($flag_type)
1540            || !$flag_type->request_group_id
1541            || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0;
1542}
1543
1544sub can_set_flag {
1545    my ($self, $flag_type) = @_;
1546
1547    return (!$flag_type->grant_group_id
1548            || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0;
1549}
1550
1551# visible_groups_inherited returns a reference to a list of all the groups
1552# whose members are visible to this user.
1553sub visible_groups_inherited {
1554    my $self = shift;
1555    return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited};
1556    return [] unless $self->id;
1557    my @visgroups = @{$self->visible_groups_direct};
1558    @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)};
1559    $self->{visible_groups_inherited} = \@visgroups;
1560    return $self->{visible_groups_inherited};
1561}
1562
1563# visible_groups_direct returns a reference to a list of all the groups that
1564# are visible to this user.
1565sub visible_groups_direct {
1566    my $self = shift;
1567    my @visgroups = ();
1568    return $self->{visible_groups_direct} if defined $self->{visible_groups_direct};
1569    return [] unless $self->id;
1570
1571    my $dbh = Bugzilla->dbh;
1572    my $sth;
1573
1574    if (Bugzilla->params->{'usevisibilitygroups'}) {
1575        $sth = $dbh->prepare("SELECT DISTINCT grantor_id
1576                                 FROM group_group_map
1577                                WHERE " . $self->groups_in_sql('member_id') . "
1578                                  AND grant_type=" . GROUP_VISIBLE);
1579    }
1580    else {
1581        # All groups are visible if usevisibilitygroups is off.
1582        $sth = $dbh->prepare('SELECT id FROM groups');
1583    }
1584    $sth->execute();
1585
1586    while (my ($row) = $sth->fetchrow_array) {
1587        push @visgroups,$row;
1588    }
1589    $self->{visible_groups_direct} = \@visgroups;
1590
1591    return $self->{visible_groups_direct};
1592}
1593
1594sub visible_groups_as_string {
1595    my $self = shift;
1596    return join(', ', @{$self->visible_groups_inherited()});
1597}
1598
1599# This function defines the groups a user may share a query with.
1600# More restrictive sites may want to build this reference to a list of group IDs
1601# from bless_groups instead of mirroring visible_groups_inherited, perhaps.
1602sub queryshare_groups {
1603    my $self = shift;
1604    my @queryshare_groups;
1605
1606    return $self->{queryshare_groups} if defined $self->{queryshare_groups};
1607
1608    if ($self->in_group(Bugzilla->params->{'querysharegroup'})) {
1609        # We want to be allowed to share with groups we're in only.
1610        # If usevisibilitygroups is on, then we need to restrict this to groups
1611        # we may see.
1612        if (Bugzilla->params->{'usevisibilitygroups'}) {
1613            foreach(@{$self->visible_groups_inherited()}) {
1614                next unless $self->in_group_id($_);
1615                push(@queryshare_groups, $_);
1616            }
1617        }
1618        else {
1619            @queryshare_groups = @{ $self->_group_ids };
1620        }
1621    }
1622
1623    return $self->{queryshare_groups} = \@queryshare_groups;
1624}
1625
1626sub queryshare_groups_as_string {
1627    my $self = shift;
1628    return join(', ', @{$self->queryshare_groups()});
1629}
1630
1631sub derive_regexp_groups {
1632    my ($self) = @_;
1633
1634    my $id = $self->id;
1635    return unless $id;
1636
1637    my $dbh = Bugzilla->dbh;
1638
1639    my $sth;
1640
1641    # add derived records for any matching regexps
1642
1643    $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id
1644                            FROM groups
1645                       LEFT JOIN user_group_map
1646                              ON groups.id = user_group_map.group_id
1647                             AND user_group_map.user_id = ?
1648                             AND user_group_map.grant_type = ?");
1649    $sth->execute($id, GRANT_REGEXP);
1650
1651    my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map
1652                                       (user_id, group_id, isbless, grant_type)
1653                                       VALUES (?, ?, 0, ?)});
1654    my $group_delete = $dbh->prepare(q{DELETE FROM user_group_map
1655                                       WHERE user_id = ?
1656                                         AND group_id = ?
1657                                         AND isbless = 0
1658                                         AND grant_type = ?});
1659    while (my ($group, $regexp, $present) = $sth->fetchrow_array()) {
1660        if (($regexp ne '') && ($self->login =~ m/$regexp/i)) {
1661            $group_insert->execute($id, $group, GRANT_REGEXP) unless $present;
1662        } else {
1663            $group_delete->execute($id, $group, GRANT_REGEXP) if $present;
1664        }
1665    }
1666
1667    Bugzilla->memcached->clear_config({ key => "user_groups.$id" });
1668}
1669
1670sub product_responsibilities {
1671    my $self = shift;
1672    my $dbh = Bugzilla->dbh;
1673
1674    return $self->{'product_resp'} if defined $self->{'product_resp'};
1675    return [] unless $self->id;
1676
1677    my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.id
1678                                           FROM components
1679                                           LEFT JOIN component_cc
1680                                           ON components.id = component_cc.component_id
1681                                          WHERE components.initialowner = ?
1682                                             OR components.initialqacontact = ?
1683                                             OR component_cc.user_id = ?',
1684                                  {Slice => {}}, ($self->id, $self->id, $self->id));
1685
1686    unless ($list) {
1687        $self->{'product_resp'} = [];
1688        return $self->{'product_resp'};
1689    }
1690
1691    my @prod_ids = map {$_->{'product_id'}} @$list;
1692    my $products = Bugzilla::Product->new_from_list(\@prod_ids);
1693    # We cannot |use| it, because Component.pm already |use|s User.pm.
1694    require Bugzilla::Component;
1695    my @comp_ids = map {$_->{'id'}} @$list;
1696    my $components = Bugzilla::Component->new_from_list(\@comp_ids);
1697
1698    my @prod_list;
1699    # @$products is already sorted alphabetically.
1700    foreach my $prod (@$products) {
1701        # We use @components instead of $prod->components because we only want
1702        # components where the user is either the default assignee or QA contact.
1703        push(@prod_list, {product    => $prod,
1704                          components => [grep {$_->product_id == $prod->id} @$components]});
1705    }
1706    $self->{'product_resp'} = \@prod_list;
1707    return $self->{'product_resp'};
1708}
1709
1710sub can_bless {
1711    my $self = shift;
1712
1713    if (!scalar(@_)) {
1714        # If we're called without an argument, just return
1715        # whether or not we can bless at all.
1716        return scalar(@{ $self->bless_groups }) ? 1 : 0;
1717    }
1718
1719    # Otherwise, we're checking a specific group
1720    my $group_id = shift;
1721    return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0;
1722}
1723
1724sub match {
1725    # Generates a list of users whose login name (email address) or real name
1726    # matches a substring or wildcard.
1727    # This is also called if matches are disabled (for error checking), but
1728    # in this case only the exact match code will end up running.
1729
1730    # $str contains the string to match, while $limit contains the
1731    # maximum number of records to retrieve.
1732    my ($str, $limit, $exclude_disabled) = @_;
1733    my $user = Bugzilla->user;
1734    my $dbh = Bugzilla->dbh;
1735
1736    $str = trim($str);
1737
1738    my @users = ();
1739    return \@users if $str =~ /^\s*$/;
1740
1741    # The search order is wildcards, then exact match, then substring search.
1742    # Wildcard matching is skipped if there is no '*', and exact matches will
1743    # not (?) have a '*' in them.  If any search comes up with something, the
1744    # ones following it will not execute.
1745
1746    # first try wildcards
1747    my $wildstr = $str;
1748
1749    # Do not do wildcards if there is no '*' in the string.
1750    if ($wildstr =~ s/\*/\%/g && $user->id) {
1751        # Build the query.
1752        trick_taint($wildstr);
1753        my $query  = "SELECT DISTINCT userid FROM profiles ";
1754        if (Bugzilla->params->{'usevisibilitygroups'}) {
1755            $query .= "INNER JOIN user_group_map
1756                               ON user_group_map.user_id = profiles.userid ";
1757        }
1758        $query .= "WHERE ("
1759            . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " .
1760              $dbh->sql_istrcmp('realname', '?', "LIKE") . ") ";
1761        if (Bugzilla->params->{'usevisibilitygroups'}) {
1762            $query .= "AND isbless = 0 " .
1763                      "AND group_id IN(" .
1764                      join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
1765        }
1766        $query    .= " AND is_enabled = 1 " if $exclude_disabled;
1767        $query    .= $dbh->sql_limit($limit) if $limit;
1768
1769        # Execute the query, retrieve the results, and make them into
1770        # User objects.
1771        my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
1772        @users = @{Bugzilla::User->new_from_list($user_ids)};
1773    }
1774    else {    # try an exact match
1775        # Exact matches don't care if a user is disabled.
1776        trick_taint($str);
1777        my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles
1778                                             WHERE ' . $dbh->sql_istrcmp('login_name', '?'),
1779                                             undef, $str);
1780
1781        push(@users, new Bugzilla::User($user_id)) if $user_id;
1782    }
1783
1784    # then try substring search
1785    if (!scalar(@users) && length($str) >= 3 && $user->id) {
1786        trick_taint($str);
1787
1788        my $query   = "SELECT DISTINCT userid FROM profiles ";
1789        if (Bugzilla->params->{'usevisibilitygroups'}) {
1790            $query .= "INNER JOIN user_group_map
1791                               ON user_group_map.user_id = profiles.userid ";
1792        }
1793        $query     .= " WHERE (" .
1794                $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " .
1795                $dbh->sql_iposition('?', 'realname') . " > 0) ";
1796        if (Bugzilla->params->{'usevisibilitygroups'}) {
1797            $query .= " AND isbless = 0" .
1798                      " AND group_id IN(" .
1799                join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
1800        }
1801        $query     .= " AND is_enabled = 1 " if $exclude_disabled;
1802        $query     .= $dbh->sql_limit($limit) if $limit;
1803        my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
1804        @users = @{Bugzilla::User->new_from_list($user_ids)};
1805    }
1806    return \@users;
1807}
1808
1809sub match_field {
1810    my $fields       = shift;   # arguments as a hash
1811    my $data         = shift || Bugzilla->input_params; # hash to look up fields in
1812    my $behavior     = shift || 0; # A constant that tells us how to act
1813    my $matches      = {};      # the values sent to the template
1814    my $matchsuccess = 1;       # did the match fail?
1815    my $need_confirm = 0;       # whether to display confirmation screen
1816    my $match_multiple = 0;     # whether we ever matched more than one user
1817    my @non_conclusive_fields;  # fields which don't have a unique user.
1818
1819    my $params = Bugzilla->params;
1820
1821    # prepare default form values
1822
1823    # Fields can be regular expressions matching multiple form fields
1824    # (f.e. "requestee-(\d+)"), so expand each non-literal field
1825    # into the list of form fields it matches.
1826    my $expanded_fields = {};
1827    foreach my $field_pattern (keys %{$fields}) {
1828        # Check if the field has any non-word characters.  Only those fields
1829        # can be regular expressions, so don't expand the field if it doesn't
1830        # have any of those characters.
1831        if ($field_pattern =~ /^\w+$/) {
1832            $expanded_fields->{$field_pattern} = $fields->{$field_pattern};
1833        }
1834        else {
1835            my @field_names = grep(/$field_pattern/, keys %$data);
1836
1837            foreach my $field_name (@field_names) {
1838                $expanded_fields->{$field_name} =
1839                  { type => $fields->{$field_pattern}->{'type'} };
1840
1841                # The field is a requestee field; in order for its name
1842                # to show up correctly on the confirmation page, we need
1843                # to find out the name of its flag type.
1844                if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
1845                    my $flag_type;
1846                    if ($1) {
1847                        require Bugzilla::FlagType;
1848                        $flag_type = new Bugzilla::FlagType($2);
1849                    }
1850                    else {
1851                        require Bugzilla::Flag;
1852                        my $flag = new Bugzilla::Flag($2);
1853                        $flag_type = $flag->type if $flag;
1854                    }
1855                    if ($flag_type) {
1856                        $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
1857                    }
1858                    else {
1859                        # No need to look for a valid requestee if the flag(type)
1860                        # has been deleted (may occur in race conditions).
1861                        delete $expanded_fields->{$field_name};
1862                        delete $data->{$field_name};
1863                    }
1864                }
1865            }
1866        }
1867    }
1868    $fields = $expanded_fields;
1869
1870    foreach my $field (keys %{$fields}) {
1871        next unless defined $data->{$field};
1872
1873        #Concatenate login names, so that we have a common way to handle them.
1874        my $raw_field;
1875        if (ref $data->{$field}) {
1876            $raw_field = join(",", @{$data->{$field}});
1877        }
1878        else {
1879            $raw_field = $data->{$field};
1880        }
1881        $raw_field = clean_text($raw_field || '');
1882
1883        # Now we either split $raw_field by spaces/commas and put the list
1884        # into @queries, or in the case of fields which only accept single
1885        # entries, we simply use the verbatim text.
1886        my @queries;
1887        if ($fields->{$field}->{'type'} eq 'single') {
1888            @queries = ($raw_field);
1889            # We will repopulate it later if a match is found, else it must
1890            # be set to an empty string so that the field remains defined.
1891            $data->{$field} = '';
1892        }
1893        elsif ($fields->{$field}->{'type'} eq 'multi') {
1894            @queries =  split(/[,;]+/, $raw_field);
1895            # We will repopulate it later if a match is found, else it must
1896            # be undefined.
1897            delete $data->{$field};
1898        }
1899        else {
1900            # bad argument
1901            ThrowCodeError('bad_arg',
1902                           { argument => $fields->{$field}->{'type'},
1903                             function =>  'Bugzilla::User::match_field',
1904                           });
1905        }
1906
1907        # Tolerate fields that do not exist (in case you specify
1908        # e.g. the QA contact, and it's currently not in use).
1909        next unless (defined $raw_field && $raw_field ne '');
1910
1911        my $limit = 0;
1912        if ($params->{'maxusermatches'}) {
1913            $limit = $params->{'maxusermatches'} + 1;
1914        }
1915
1916        my @logins;
1917        for my $query (@queries) {
1918            $query = trim($query);
1919            next if $query eq '';
1920
1921            my $users = match(
1922                $query,   # match string
1923                $limit,   # match limit
1924                1         # exclude_disabled
1925            );
1926
1927            # here is where it checks for multiple matches
1928            if (scalar(@{$users}) == 1) { # exactly one match
1929                push(@logins, @{$users}[0]->login);
1930
1931                # skip confirmation for exact matches
1932                next if (lc(@{$users}[0]->login) eq lc($query));
1933
1934                $matches->{$field}->{$query}->{'status'} = 'success';
1935                $need_confirm = 1 if $params->{'confirmuniqueusermatch'};
1936
1937            }
1938            elsif ((scalar(@{$users}) > 1)
1939                    && ($params->{'maxusermatches'} != 1)) {
1940                $need_confirm = 1;
1941                $match_multiple = 1;
1942                push(@non_conclusive_fields, $field);
1943
1944                if (($params->{'maxusermatches'})
1945                   && (scalar(@{$users}) > $params->{'maxusermatches'}))
1946                {
1947                    $matches->{$field}->{$query}->{'status'} = 'trunc';
1948                    pop @{$users};  # take the last one out
1949                }
1950                else {
1951                    $matches->{$field}->{$query}->{'status'} = 'success';
1952                }
1953
1954            }
1955            else {
1956                # everything else fails
1957                $matchsuccess = 0; # fail
1958                push(@non_conclusive_fields, $field);
1959                $matches->{$field}->{$query}->{'status'} = 'fail';
1960                $need_confirm = 1;  # confirmation screen shows failures
1961            }
1962
1963            $matches->{$field}->{$query}->{'users'}  = $users;
1964        }
1965
1966        # If no match or more than one match has been found for a field
1967        # expecting only one match (type eq "single"), we set it back to ''
1968        # so that the caller of this function can still check whether this
1969        # field was defined or not (and it was if we came here).
1970        if ($fields->{$field}->{'type'} eq 'single') {
1971            $data->{$field} = $logins[0] || '';
1972        }
1973        elsif (scalar @logins) {
1974            $data->{$field} = \@logins;
1975        }
1976    }
1977
1978    my $retval;
1979    if (!$matchsuccess) {
1980        $retval = USER_MATCH_FAILED;
1981    }
1982    elsif ($match_multiple) {
1983        $retval = USER_MATCH_MULTIPLE;
1984    }
1985    else {
1986        $retval = USER_MATCH_SUCCESS;
1987    }
1988
1989    # Skip confirmation if we were told to, or if we don't need to confirm.
1990    if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
1991        return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
1992    }
1993
1994    my $template = Bugzilla->template;
1995    my $cgi = Bugzilla->cgi;
1996    my $vars = {};
1997
1998    $vars->{'script'}        = $cgi->url(-relative => 1); # for self-referencing URLs
1999    $vars->{'fields'}        = $fields; # fields being matched
2000    $vars->{'matches'}       = $matches; # matches that were made
2001    $vars->{'matchsuccess'}  = $matchsuccess; # continue or fail
2002    $vars->{'matchmultiple'} = $match_multiple;
2003
2004    print $cgi->header();
2005
2006    $template->process("global/confirm-user-match.html.tmpl", $vars)
2007      || ThrowTemplateError($template->error());
2008    exit;
2009
2010}
2011
2012# Changes in some fields automatically trigger events. The field names are
2013# from the fielddefs table.
2014our %names_to_events = (
2015    'resolution'              => EVT_OPENED_CLOSED,
2016    'keywords'                => EVT_KEYWORD,
2017    'cc'                      => EVT_CC,
2018    'bug_severity'            => EVT_PROJ_MANAGEMENT,
2019    'priority'                => EVT_PROJ_MANAGEMENT,
2020    'bug_status'              => EVT_PROJ_MANAGEMENT,
2021    'target_milestone'        => EVT_PROJ_MANAGEMENT,
2022    'attachments.description' => EVT_ATTACHMENT_DATA,
2023    'attachments.mimetype'    => EVT_ATTACHMENT_DATA,
2024    'attachments.ispatch'     => EVT_ATTACHMENT_DATA,
2025    'dependson'               => EVT_DEPEND_BLOCK,
2026    'blocked'                 => EVT_DEPEND_BLOCK,
2027    'product'                 => EVT_COMPONENT,
2028    'component'               => EVT_COMPONENT);
2029
2030# Returns true if the user wants mail for a given bug change.
2031# Note: the "+" signs before the constants suppress bareword quoting.
2032sub wants_bug_mail {
2033    my $self = shift;
2034    my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_;
2035
2036    # Make a list of the events which have happened during this bug change,
2037    # from the point of view of this user.
2038    my %events;
2039    foreach my $change (@$fieldDiffs) {
2040        my $fieldName = $change->{field_name};
2041        # A change to any of the above fields sets the corresponding event
2042        if (defined($names_to_events{$fieldName})) {
2043            $events{$names_to_events{$fieldName}} = 1;
2044        }
2045        else {
2046            # Catch-all for any change not caught by a more specific event
2047            $events{+EVT_OTHER} = 1;
2048        }
2049
2050        # If the user is in a particular role and the value of that role
2051        # changed, we need the ADDED_REMOVED event.
2052        if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) ||
2053            ($fieldName eq "qa_contact" && $relationship == REL_QA))
2054        {
2055            $events{+EVT_ADDED_REMOVED} = 1;
2056        }
2057
2058        if ($fieldName eq "cc") {
2059            my $login = $self->login;
2060            my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
2061            my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
2062            if ($inold != $innew)
2063            {
2064                $events{+EVT_ADDED_REMOVED} = 1;
2065            }
2066        }
2067    }
2068
2069    if (!$bug->lastdiffed) {
2070        # Notify about new bugs.
2071        $events{+EVT_BUG_CREATED} = 1;
2072
2073        # You role is new if the bug itself is.
2074        # Only makes sense for the assignee, QA contact and the CC list.
2075        if ($relationship == REL_ASSIGNEE
2076            || $relationship == REL_QA
2077            || $relationship == REL_CC)
2078        {
2079            $events{+EVT_ADDED_REMOVED} = 1;
2080        }
2081    }
2082
2083    if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
2084        $events{+EVT_ATTACHMENT} = 1;
2085    }
2086    elsif (defined($$comments[0])) {
2087        $events{+EVT_COMMENT} = 1;
2088    }
2089
2090    # Dependent changed bugmails must have an event to ensure the bugmail is
2091    # emailed.
2092    if ($dep_mail) {
2093        $events{+EVT_DEPEND_BLOCK} = 1;
2094    }
2095
2096    my @event_list = keys %events;
2097
2098    my $wants_mail = $self->wants_mail(\@event_list, $relationship);
2099
2100    # The negative events are handled separately - they can't be incorporated
2101    # into the first wants_mail call, because they are of the opposite sense.
2102    #
2103    # We do them separately because if _any_ of them are set, we don't want
2104    # the mail.
2105    if ($wants_mail && $changer && ($self->id == $changer->id)) {
2106        $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship);
2107    }
2108
2109    if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') {
2110        $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
2111    }
2112
2113    return $wants_mail;
2114}
2115
2116# Returns true if the user wants mail for a given set of events.
2117sub wants_mail {
2118    my $self = shift;
2119    my ($events, $relationship) = @_;
2120
2121    # Don't send any mail, ever, if account is disabled
2122    # XXX Temporary Compatibility Change 1 of 2:
2123    # This code is disabled for the moment to make the behaviour like the old
2124    # system, which sent bugmail to disabled accounts.
2125    # return 0 if $self->{'disabledtext'};
2126
2127    # No mail if there are no events
2128    return 0 if !scalar(@$events);
2129
2130    # If a relationship isn't given, default to REL_ANY.
2131    if (!defined($relationship)) {
2132        $relationship = REL_ANY;
2133    }
2134
2135    # Skip DB query if relationship is explicit
2136    return 1 if $relationship == REL_GLOBAL_WATCHER;
2137
2138    my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events;
2139    return $wants_mail ? 1 : 0;
2140}
2141
2142sub mail_settings {
2143    my $self = shift;
2144    my $dbh = Bugzilla->dbh;
2145
2146    if (!defined $self->{'mail_settings'}) {
2147        my $data =
2148          $dbh->selectall_arrayref('SELECT relationship, event FROM email_setting
2149                                    WHERE user_id = ?', undef, $self->id);
2150        my %mail;
2151        # The hash is of the form $mail{$relationship}{$event} = 1.
2152        $mail{$_->[0]}{$_->[1]} = 1 foreach @$data;
2153
2154        $self->{'mail_settings'} = \%mail;
2155    }
2156    return $self->{'mail_settings'};
2157}
2158
2159sub has_audit_entries {
2160    my $self = shift;
2161    my $dbh = Bugzilla->dbh;
2162
2163    if (!exists $self->{'has_audit_entries'}) {
2164        $self->{'has_audit_entries'} =
2165            $dbh->selectrow_array('SELECT 1 FROM audit_log WHERE user_id = ? ' .
2166                                   $dbh->sql_limit(1), undef, $self->id);
2167    }
2168    return $self->{'has_audit_entries'};
2169}
2170
2171sub is_insider {
2172    my $self = shift;
2173
2174    if (!defined $self->{'is_insider'}) {
2175        my $insider_group = Bugzilla->params->{'insidergroup'};
2176        $self->{'is_insider'} =
2177            ($insider_group && $self->in_group($insider_group)) ? 1 : 0;
2178    }
2179    return $self->{'is_insider'};
2180}
2181
2182sub is_global_watcher {
2183    my $self = shift;
2184
2185    if (!defined $self->{'is_global_watcher'}) {
2186        my @watchers = split(/[,;]+/, Bugzilla->params->{'globalwatchers'});
2187        $self->{'is_global_watcher'} = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0;
2188    }
2189    return  $self->{'is_global_watcher'};
2190}
2191
2192sub is_timetracker {
2193    my $self = shift;
2194
2195    if (!defined $self->{'is_timetracker'}) {
2196        my $tt_group = Bugzilla->params->{'timetrackinggroup'};
2197        $self->{'is_timetracker'} =
2198            ($tt_group && $self->in_group($tt_group)) ? 1 : 0;
2199    }
2200    return $self->{'is_timetracker'};
2201}
2202
2203sub can_tag_comments {
2204    my $self = shift;
2205
2206    if (!defined $self->{'can_tag_comments'}) {
2207        my $group = Bugzilla->params->{'comment_taggers_group'};
2208        $self->{'can_tag_comments'} =
2209            ($group && $self->in_group($group)) ? 1 : 0;
2210    }
2211    return $self->{'can_tag_comments'};
2212}
2213
2214sub get_userlist {
2215    my $self = shift;
2216
2217    return $self->{'userlist'} if defined $self->{'userlist'};
2218
2219    my $dbh = Bugzilla->dbh;
2220    my $query  = "SELECT DISTINCT login_name, realname,";
2221    if (Bugzilla->params->{'usevisibilitygroups'}) {
2222        $query .= " COUNT(group_id) ";
2223    } else {
2224        $query .= " 1 ";
2225    }
2226    $query     .= "FROM profiles ";
2227    if (Bugzilla->params->{'usevisibilitygroups'}) {
2228        $query .= "LEFT JOIN user_group_map " .
2229                  "ON user_group_map.user_id = userid AND isbless = 0 " .
2230                  "AND group_id IN(" .
2231                  join(', ', (-1, @{$self->visible_groups_inherited})) . ")";
2232    }
2233    $query    .= " WHERE is_enabled = 1 ";
2234    $query    .= $dbh->sql_group_by('userid', 'login_name, realname');
2235
2236    my $sth = $dbh->prepare($query);
2237    $sth->execute;
2238
2239    my @userlist;
2240    while (my($login, $name, $visible) = $sth->fetchrow_array) {
2241        push @userlist, {
2242            login => $login,
2243            identity => $name ? "$name <$login>" : $login,
2244            visible => $visible,
2245        };
2246    }
2247    @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist;
2248
2249    $self->{'userlist'} = \@userlist;
2250    return $self->{'userlist'};
2251}
2252
2253sub create {
2254    my $invocant = shift;
2255    my $class = ref($invocant) || $invocant;
2256    my $dbh = Bugzilla->dbh;
2257
2258    $dbh->bz_start_transaction();
2259
2260    my $user = $class->SUPER::create(@_);
2261
2262    # Turn on all email for the new user
2263    require Bugzilla::BugMail;
2264    my %relationships = Bugzilla::BugMail::relationships();
2265    foreach my $rel (keys %relationships) {
2266        foreach my $event (POS_EVENTS, NEG_EVENTS) {
2267            # These "exceptions" define the default email preferences.
2268            #
2269            # We enable mail unless the change was made by the user, or it's
2270            # just a CC list addition and the user is not the reporter.
2271            next if ($event == EVT_CHANGED_BY_ME);
2272            next if (($event == EVT_CC) && ($rel != REL_REPORTER));
2273
2274            $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
2275                      VALUES (?, ?, ?)', undef, ($user->id, $rel, $event));
2276        }
2277    }
2278
2279    foreach my $event (GLOBAL_EVENTS) {
2280        $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
2281                  VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event));
2282    }
2283
2284    $user->derive_regexp_groups();
2285
2286    # Add the creation date to the profiles_activity table.
2287    # $who is the user who created the new user account, i.e. either an
2288    # admin or the new user himself.
2289    my $who = Bugzilla->user->id || $user->id;
2290    my $creation_date_fieldid = get_field_id('creation_ts');
2291
2292    $dbh->do('INSERT INTO profiles_activity
2293                          (userid, who, profiles_when, fieldid, newvalue)
2294                   VALUES (?, ?, NOW(), ?, NOW())',
2295                   undef, ($user->id, $who, $creation_date_fieldid));
2296
2297    $dbh->bz_commit_transaction();
2298
2299    # Return the newly created user account.
2300    return $user;
2301}
2302
2303###########################
2304# Account Lockout Methods #
2305###########################
2306
2307sub account_is_locked_out {
2308    my $self = shift;
2309    my $login_failures = scalar @{ $self->account_ip_login_failures };
2310    return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0;
2311}
2312
2313sub note_login_failure {
2314    my $self = shift;
2315    my $ip_addr = remote_ip();
2316    trick_taint($ip_addr);
2317    Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time)
2318                       VALUES (?, ?, LOCALTIMESTAMP(0))",
2319                      undef, $self->id, $ip_addr);
2320    delete $self->{account_ip_login_failures};
2321}
2322
2323sub clear_login_failures {
2324    my $self = shift;
2325    my $ip_addr = remote_ip();
2326    trick_taint($ip_addr);
2327    Bugzilla->dbh->do(
2328        'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?',
2329        undef, $self->id, $ip_addr);
2330    delete $self->{account_ip_login_failures};
2331}
2332
2333sub account_ip_login_failures {
2334    my $self = shift;
2335    my $dbh = Bugzilla->dbh;
2336    my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-',
2337                                   LOGIN_LOCKOUT_INTERVAL, 'MINUTE');
2338    my $ip_addr = remote_ip();
2339    trick_taint($ip_addr);
2340    $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref(
2341        "SELECT login_time, ip_addr, user_id FROM login_failure
2342          WHERE user_id = ? AND login_time > $time
2343                AND ip_addr = ?
2344       ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr);
2345    return $self->{account_ip_login_failures};
2346}
2347
2348###############
2349# Subroutines #
2350###############
2351
2352sub is_available_username {
2353    my ($username, $old_username) = @_;
2354
2355    if(login_to_id($username) != 0) {
2356        return 0;
2357    }
2358
2359    my $dbh = Bugzilla->dbh;
2360    # $username is safe because it is only used in SELECT placeholders.
2361    trick_taint($username);
2362    # Reject if the new login is part of an email change which is
2363    # still in progress
2364    #
2365    # substring/locate stuff: bug 165221; this used to use regexes, but that
2366    # was unsafe and required weird escaping; using substring to pull out
2367    # the new/old email addresses and sql_position() to find the delimiter (':')
2368    # is cleaner/safer
2369    my ($tokentype, $eventdata) = $dbh->selectrow_array(
2370        "SELECT tokentype, eventdata
2371           FROM tokens
2372          WHERE (tokentype = 'emailold'
2373                AND SUBSTRING(eventdata, 1, (" .
2374                    $dbh->sql_position(q{':'}, 'eventdata') . "-  1)) = ?)
2375             OR (tokentype = 'emailnew'
2376                AND SUBSTRING(eventdata, (" .
2377                    $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)",
2378         undef, ($username, $username));
2379
2380    if ($eventdata) {
2381        # Allow thru owner of token
2382        if ($old_username
2383            && (($tokentype eq 'emailnew' && $eventdata eq "$old_username:$username")
2384                || ($tokentype eq 'emailold' && $eventdata eq "$username:$old_username")))
2385        {
2386            return 1;
2387        }
2388        return 0;
2389    }
2390
2391    return 1;
2392}
2393
2394sub check_account_creation_enabled {
2395    my $self = shift;
2396
2397    # If we're using e.g. LDAP for login, then we can't create a new account.
2398    $self->authorizer->user_can_create_account
2399      || ThrowUserError('auth_cant_create_account');
2400
2401    Bugzilla->params->{'createemailregexp'}
2402      || ThrowUserError('account_creation_disabled');
2403}
2404
2405sub check_and_send_account_creation_confirmation {
2406    my ($self, $login) = @_;
2407    my $dbh = Bugzilla->dbh;
2408
2409    $dbh->bz_start_transaction;
2410
2411    $login = $self->check_login_name($login);
2412    my $creation_regexp = Bugzilla->params->{'createemailregexp'};
2413
2414    if ($login !~ /$creation_regexp/i) {
2415        ThrowUserError('account_creation_restricted');
2416    }
2417
2418    # Allow extensions to do extra checks.
2419    Bugzilla::Hook::process('user_check_account_creation', { login => $login });
2420
2421    # Create and send a token for this new account.
2422    require Bugzilla::Token;
2423    Bugzilla::Token::issue_new_user_account_token($login);
2424
2425    $dbh->bz_commit_transaction;
2426}
2427
2428# This is used in a few performance-critical areas where we don't want to
2429# do check() and pull all the user data from the database.
2430sub login_to_id {
2431    my ($login, $throw_error) = @_;
2432    my $dbh = Bugzilla->dbh;
2433    my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
2434
2435    # We cache lookups because this function showed up as taking up a
2436    # significant amount of time in profiles of xt/search.t. However,
2437    # for users that don't exist, we re-do the check every time, because
2438    # otherwise we break is_available_username.
2439    my $user_id;
2440    if (defined $cache->{$login}) {
2441        $user_id = $cache->{$login};
2442    }
2443    else {
2444        # No need to validate $login -- it will be used by the following SELECT
2445        # statement only, so it's safe to simply trick_taint.
2446        trick_taint($login);
2447        $user_id = $dbh->selectrow_array(
2448            "SELECT userid FROM profiles
2449              WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login);
2450        $cache->{$login} = $user_id;
2451    }
2452
2453    if ($user_id) {
2454        return $user_id;
2455    } elsif ($throw_error) {
2456        ThrowUserError('invalid_username', { name => $login });
2457    } else {
2458        return 0;
2459    }
2460}
2461
2462sub validate_password {
2463    my $check = validate_password_check(@_);
2464    ThrowUserError($check) if $check;
2465    return 1;
2466}
2467
2468sub validate_password_check {
2469    my ($password, $matchpassword) = @_;
2470
2471    if (length($password) < USER_PASSWORD_MIN_LENGTH) {
2472        return 'password_too_short';
2473    } elsif ((defined $matchpassword) && ($password ne $matchpassword)) {
2474        return 'passwords_dont_match';
2475    }
2476
2477    my $complexity_level = Bugzilla->params->{password_complexity};
2478    if ($complexity_level eq 'letters_numbers_specialchars') {
2479        return 'password_not_complex'
2480          if ($password !~ /[[:alpha:]]/ || $password !~ /\d/ || $password !~ /[[:punct:]]/);
2481    } elsif ($complexity_level eq 'letters_numbers') {
2482        return 'password_not_complex'
2483          if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/ || $password !~ /\d/);
2484    } elsif ($complexity_level eq 'mixed_letters') {
2485        return 'password_not_complex'
2486          if ($password !~ /[[:lower:]]/ || $password !~ /[[:upper:]]/);
2487    }
2488
2489    # Having done these checks makes us consider the password untainted.
2490    trick_taint($_[0]);
2491    return;
2492}
2493
2494
24951;
2496
2497__END__
2498
2499=head1 NAME
2500
2501Bugzilla::User - Object for a Bugzilla user
2502
2503=head1 SYNOPSIS
2504
2505  use Bugzilla::User;
2506
2507  my $user = new Bugzilla::User($id);
2508
2509  my @get_selectable_classifications =
2510      $user->get_selectable_classifications;
2511
2512  # Class Functions
2513  $user = Bugzilla::User->create({
2514      login_name    => $username,
2515      realname      => $realname,
2516      cryptpassword => $plaintext_password,
2517      disabledtext  => $disabledtext,
2518      disable_mail  => 0});
2519
2520=head1 DESCRIPTION
2521
2522This package handles Bugzilla users. Data obtained from here is read-only;
2523there is currently no way to modify a user from this package.
2524
2525Note that the currently logged in user (if any) is available via
2526L<Bugzilla-E<gt>user|Bugzilla/"user">.
2527
2528C<Bugzilla::User> is an implementation of L<Bugzilla::Object>, and thus
2529provides all the methods of L<Bugzilla::Object> in addition to the
2530methods listed below.
2531
2532=head1 CONSTANTS
2533
2534=over
2535
2536=item C<USER_MATCH_MULTIPLE>
2537
2538Returned by C<match_field()> when at least one field matched more than
2539one user, but no matches failed.
2540
2541=item C<USER_MATCH_FAILED>
2542
2543Returned by C<match_field()> when at least one field failed to match
2544anything.
2545
2546=item C<USER_MATCH_SUCCESS>
2547
2548Returned by C<match_field()> when all fields successfully matched only one
2549user.
2550
2551=item C<MATCH_SKIP_CONFIRM>
2552
2553Passed in to match_field to tell match_field to never display a
2554confirmation screen.
2555
2556=back
2557
2558=head1 METHODS
2559
2560=head2 Constructors
2561
2562=over
2563
2564=item C<super_user>
2565
2566Returns a user who is in all groups, but who does not really exist in the
2567database. Used for non-web scripts like L<checksetup> that need to make
2568database changes and so on.
2569
2570=back
2571
2572=head2 Saved and Shared Queries
2573
2574=over
2575
2576=item C<queries>
2577
2578Returns an arrayref of the user's own saved queries, sorted by name. The
2579array contains L<Bugzilla::Search::Saved> objects.
2580
2581=item C<queries_subscribed>
2582
2583Returns an arrayref of shared queries that the user has subscribed to.
2584That is, these are shared queries that the user sees in their footer.
2585This array contains L<Bugzilla::Search::Saved> objects.
2586
2587=item C<queries_available>
2588
2589Returns an arrayref of all queries to which the user could possibly
2590subscribe. This includes the contents of L</queries_subscribed>.
2591An array of L<Bugzilla::Search::Saved> objects.
2592
2593=item C<flush_queries_cache>
2594
2595Some code modifies the set of stored queries. Because C<Bugzilla::User> does
2596not handle these modifications, but does cache the result of calling C<queries>
2597internally, such code must call this method to flush the cached result.
2598
2599=item C<queryshare_groups>
2600
2601An arrayref of group ids. The user can share their own queries with these
2602groups.
2603
2604=item C<tags>
2605
2606Returns a hashref with tag IDs as key, and a hashref with tag 'id',
2607'name' and 'bug_count' as value.
2608
2609=item C<bugs_ignored>
2610
2611Returns an array of hashrefs containing information about bugs currently
2612being ignored by the user.
2613
2614Each hashref contains the following information:
2615
2616=over
2617
2618=item C<id>
2619
2620C<int> The id of the bug.
2621
2622=item C<status>
2623
2624C<string> The current status of the bug.
2625
2626=item C<summary>
2627
2628C<string> The current summary of the bug.
2629
2630=back
2631
2632=item C<is_bug_ignored>
2633
2634Returns true if the user does not want email notifications for the
2635specified bug ID, else returns false.
2636
2637=back
2638
2639=head2 Saved Recent Bug Lists
2640
2641=over
2642
2643=item C<recent_searches>
2644
2645Returns an arrayref of L<Bugzilla::Search::Recent> objects
2646containing the user's recent searches.
2647
2648=item C<recent_search_containing(bug_id)>
2649
2650Returns a L<Bugzilla::Search::Recent> object that contains the most recent
2651search by the user for the specified bug id. Retuns undef if no match is found.
2652
2653=item C<recent_search_for(bug)>
2654
2655Returns a L<Bugzilla::Search::Recent> object that contains a search by the
2656user. Uses the list_id of the current loaded page, or the referrer page, and
2657the bug id if that fails. Finally it will check the BUGLIST cookie, and create
2658an object based on that, or undef if it does not exist.
2659
2660=item C<save_last_search>
2661
2662Saves the users most recent search in the database if logged in, or in the
2663BUGLIST cookie if not logged in. Parameters are bug_ids, order, vars and
2664list_id.
2665
2666=back
2667
2668=head2 Account Lockout
2669
2670=over
2671
2672=item C<account_is_locked_out>
2673
2674Returns C<1> if the account has failed to log in too many times recently,
2675and thus is locked out for a period of time. Returns C<0> otherwise.
2676
2677=item C<account_ip_login_failures>
2678
2679Returns an arrayref of hashrefs, that contains information about the recent
2680times that this account has failed to log in from the current remote IP.
2681The hashes contain C<ip_addr>, C<login_time>, and C<user_id>.
2682
2683=item C<note_login_failure>
2684
2685This notes that this account has failed to log in, and stores the fact
2686in the database. The storing happens immediately, it does not wait for
2687you to call C<update>.
2688
2689=item C<set_email_enabled>
2690
2691C<bool> - Sets C<disable_mail> to the inverse of the boolean provided.
2692
2693=back
2694
2695=head2 Other Methods
2696
2697=over
2698
2699=item C<id>
2700
2701Returns the userid for this user.
2702
2703=item C<login>
2704
2705Returns the login name for this user.
2706
2707=item C<email>
2708
2709Returns the user's email address. Currently this is the same value as the
2710login.
2711
2712=item C<name>
2713
2714Returns the 'real' name for this user, if any.
2715
2716=item C<showmybugslink>
2717
2718Returns C<1> if the user has set their preference to show the 'My Bugs' link in
2719the page footer, and C<0> otherwise.
2720
2721=item C<identity>
2722
2723Returns a string for the identity of the user. This will be of the form
2724C<name E<lt>emailE<gt>> if the user has specified a name, and C<email>
2725otherwise.
2726
2727=item C<nick>
2728
2729Returns a user "nickname" -- i.e. a shorter, not-necessarily-unique name by
2730which to identify the user. Currently the part of the user's email address
2731before the at sign (@), but that could change, especially if we implement
2732usernames not dependent on email address.
2733
2734=item C<authorizer>
2735
2736This is the L<Bugzilla::Auth> object that the User logged in with.
2737If the user hasn't logged in yet, a new, empty Bugzilla::Auth() object is
2738returned.
2739
2740=item C<set_authorizer($authorizer)>
2741
2742Sets the L<Bugzilla::Auth> object to be returned by C<authorizer()>.
2743Should only be called by C<Bugzilla::Auth::login>, for the most part.
2744
2745=item C<disabledtext>
2746
2747Returns the disable text of the user, if any.
2748
2749=item C<reports>
2750
2751Returns an arrayref of the user's own saved reports. The array contains
2752L<Bugzilla::Reports> objects.
2753
2754=item C<flush_reports_cache>
2755
2756Some code modifies the set of stored reports. Because C<Bugzilla::User> does
2757not handle these modifications, but does cache the result of calling C<reports>
2758internally, such code must call this method to flush the cached result.
2759
2760=item C<settings>
2761
2762Returns a hash of hashes which holds the user's settings. The first key is
2763the name of the setting, as found in setting.name. The second key is one of:
2764is_enabled     - true if the user is allowed to set the preference themselves;
2765                 false to force the site defaults
2766                 for themselves or must accept the global site default value
2767default_value  - the global site default for this setting
2768value          - the value of this setting for this user. Will be the same
2769                 as the default_value if the user is not logged in, or if
2770                 is_default is true.
2771is_default     - a boolean to indicate whether the user has chosen to make
2772                 a preference for themself or use the site default.
2773
2774=item C<setting(name)>
2775
2776Returns the value for the specified setting.
2777
2778=item C<timezone>
2779
2780Returns the timezone used to display dates and times to the user,
2781as a DateTime::TimeZone object.
2782
2783=item C<groups>
2784
2785Returns an arrayref of L<Bugzilla::Group> objects representing
2786groups that this user is a member of.
2787
2788=item C<groups_as_string>
2789
2790Returns a string containing a comma-separated list of numeric group ids.  If
2791the user is not a member of any groups, returns "-1". This is most often used
2792within an SQL IN() function.
2793
2794=item C<groups_in_sql>
2795
2796This returns an C<IN> clause for SQL, containing either all of the groups
2797the user is in, or C<-1> if the user is in no groups. This takes one
2798argument--the name of the SQL field that should be on the left-hand-side
2799of the C<IN> statement, which defaults to C<group_id> if not specified.
2800
2801=item C<in_group($group_name, $product_id)>
2802
2803Determines whether or not a user is in the given group by name.
2804If $product_id is given, it also checks for local privileges for
2805this product.
2806
2807=item C<in_group_id>
2808
2809Determines whether or not a user is in the given group by id.
2810
2811=item C<bless_groups>
2812
2813Returns an arrayref of L<Bugzilla::Group> objects.
2814
2815The arrayref consists of the groups the user can bless, taking into account
2816that having editusers permissions means that you can bless all groups, and
2817that you need to be able to see a group in order to bless it.
2818
2819=item C<get_products_by_permission($group)>
2820
2821Returns a list of product objects for which the user has $group privileges
2822and which they can access.
2823$group must be one of the groups defined in PER_PRODUCT_PRIVILEGES.
2824
2825=item C<can_see_user(user)>
2826
2827Returns 1 if the specified user account exists and is visible to the user,
28280 otherwise.
2829
2830=item C<can_edit_product(prod_id)>
2831
2832Determines if, given a product id, the user can edit bugs in this product
2833at all.
2834
2835=item C<visible_bugs($bugs)>
2836
2837Description: Determines if a list of bugs are visible to the user.
2838Params:      C<$bugs> - An arrayref of Bugzilla::Bug objects or bug ids
2839Returns:     An arrayref of the bug ids that the user can see
2840
2841=item C<can_see_bug(bug_id)>
2842
2843Determines if the user can see the specified bug.
2844
2845=item C<can_see_product(product_name)>
2846
2847Returns 1 if the user can access the specified product, and 0 if the user
2848should not be aware of the existence of the product.
2849
2850=item C<derive_regexp_groups>
2851
2852Bugzilla allows for group inheritance. When data about the user (or any of the
2853groups) changes, the database must be updated. Handling updated groups is taken
2854care of by the constructor. However, when updating the email address, the
2855user may be placed into different groups, based on a new email regexp. This
2856method should be called in such a case to force reresolution of these groups.
2857
2858=item C<clear_product_cache>
2859
2860Clears the stored values for L</get_selectable_products>,
2861L</get_enterable_products>, etc. so that their data will be read from
2862the database again. Used mostly by L<Bugzilla::Product>.
2863
2864=item C<get_selectable_products>
2865
2866 Description: Returns all products the user is allowed to access. This list
2867              is restricted to some given classification if $classification_id
2868              is given.
2869
2870 Params:      $classification_id - (optional) The ID of the classification
2871                                   the products belong to.
2872
2873 Returns:     An array of product objects, sorted by the product name.
2874
2875=item C<get_selectable_classifications>
2876
2877 Description: Returns all classifications containing at least one product
2878              the user is allowed to view.
2879
2880 Params:      none
2881
2882 Returns:     An array of Bugzilla::Classification objects, sorted by
2883              the classification name.
2884
2885=item C<can_enter_product($product_name, $warn)>
2886
2887 Description: Returns a product object if the user can enter bugs into the
2888              specified product.
2889              If the user cannot enter bugs into the product, the behavior of
2890              this method depends on the value of $warn:
2891              - if $warn is false (or not given), a 'false' value is returned;
2892              - if $warn is true, an error is thrown.
2893
2894 Params:      $product_name - a product name.
2895              $warn         - optional parameter, indicating whether an error
2896                              must be thrown if the user cannot enter bugs
2897                              into the specified product.
2898
2899 Returns:     A product object if the user can enter bugs into the product,
2900              0 if the user cannot enter bugs into the product and if $warn
2901              is false (an error is thrown if $warn is true).
2902
2903=item C<get_enterable_products>
2904
2905 Description: Returns an array of product objects into which the user is
2906              allowed to enter bugs.
2907
2908 Params:      none
2909
2910 Returns:     an array of product objects.
2911
2912=item C<can_access_product($product)>
2913
2914Returns 1 if the user can search or enter bugs into the specified product
2915(either a L<Bugzilla::Product> or a product name), and 0 if the user should
2916not be aware of the existence of the product.
2917
2918=item C<get_accessible_products>
2919
2920 Description: Returns an array of product objects the user can search
2921              or enter bugs against.
2922
2923 Params:      none
2924
2925 Returns:     an array of product objects.
2926
2927=item C<can_administer>
2928
2929Returns 1 if the user can see the admin menu. Otherwise, returns 0
2930
2931=item C<check_can_admin_product($product_name)>
2932
2933 Description: Checks whether the user is allowed to administrate the product.
2934
2935 Params:      $product_name - a product name.
2936
2937 Returns:     On success, a product object. On failure, an error is thrown.
2938
2939=item C<check_can_admin_flagtype($flagtype_id)>
2940
2941 Description: Checks whether the user is allowed to edit properties of the flag type.
2942              If the flag type is also used by some products for which the user
2943              hasn't editcomponents privs, then the user is only allowed to edit
2944              the inclusion and exclusion lists for products they can administrate.
2945
2946 Params:      $flagtype_id - a flag type ID.
2947
2948 Returns:     On success, a flag type object. On failure, an error is thrown.
2949              In list context, a boolean indicating whether the user can edit
2950              all properties of the flag type is also returned. The boolean
2951              is false if the user can only edit the inclusion and exclusions
2952              lists.
2953
2954=item C<can_request_flag($flag_type)>
2955
2956 Description: Checks whether the user can request flags of the given type.
2957
2958 Params:      $flag_type - a Bugzilla::FlagType object.
2959
2960 Returns:     1 if the user can request flags of the given type,
2961              0 otherwise.
2962
2963=item C<can_set_flag($flag_type)>
2964
2965 Description: Checks whether the user can set flags of the given type.
2966
2967 Params:      $flag_type - a Bugzilla::FlagType object.
2968
2969 Returns:     1 if the user can set flags of the given type,
2970              0 otherwise.
2971
2972=item C<get_userlist>
2973
2974Returns a reference to an array of users.  The array is populated with hashrefs
2975containing the login, identity and visibility.  Users that are not visible to this
2976user will have 'visible' set to zero.
2977
2978=item C<visible_groups_inherited>
2979
2980Returns a list of all groups whose members should be visible to this user.
2981Since this list is flattened already, there is no need for all users to
2982be have derived groups up-to-date to select the users meeting this criteria.
2983
2984=item C<visible_groups_direct>
2985
2986Returns a list of groups that the user is aware of.
2987
2988=item C<visible_groups_as_string>
2989
2990Returns the result of C<visible_groups_inherited> as a string (a comma-separated
2991list).
2992
2993=item C<product_responsibilities>
2994
2995Retrieve user's product responsibilities as a list of component objects.
2996Each object is a component the user has a responsibility for.
2997
2998=item C<can_bless>
2999
3000When called with no arguments:
3001Returns C<1> if the user can bless at least one group, returns C<0> otherwise.
3002
3003When called with one argument:
3004Returns C<1> if the user can bless the group with that id, returns
3005C<0> otherwise.
3006
3007=item C<wants_bug_mail>
3008
3009Returns true if the user wants mail for a given bug change.
3010
3011=item C<wants_mail>
3012
3013Returns true if the user wants mail for a given set of events. This method is
3014more general than C<wants_bug_mail>, allowing you to check e.g. permissions
3015for flag mail.
3016
3017=item C<is_insider>
3018
3019Returns true if the user can access private comments and attachments,
3020i.e. if the 'insidergroup' parameter is set and the user belongs to this group.
3021
3022=item C<is_global_watcher>
3023
3024Returns true if the user is a global watcher,
3025i.e. if the 'globalwatchers' parameter contains the user.
3026
3027=item C<can_tag_comments>
3028
3029Returns true if the user can attach tags to comments.
3030i.e. if the 'comment_taggers_group' parameter is set and the user belongs to
3031this group.
3032
3033=item C<last_visited>
3034
3035Returns an arrayref L<Bugzilla::BugUserLastVisit> objects.
3036
3037=item C<is_involved_in_bug($bug)>
3038
3039Returns true if any of the following conditions are met, false otherwise.
3040
3041=over
3042
3043=item *
3044
3045User is the assignee of the bug
3046
3047=item *
3048
3049User is the reporter of the bug
3050
3051=item *
3052
3053User is the QA contact of the bug (if Bugzilla is configured to use a QA
3054contact)
3055
3056=item *
3057
3058User is in the cc list for the bug.
3059
3060=back
3061
3062=item C<set_groups>
3063
3064C<hash> These specify the groups that this user is directly a member of.
3065To set these, you should pass a hash as the value. The hash may contain
3066the following fields:
3067
3068=over
3069
3070=item C<add> An array of C<int>s or C<string>s. The group ids or group names
3071that the user should be added to.
3072
3073=item C<remove> An array of C<int>s or C<string>s. The group ids or group names
3074that the user should be removed from.
3075
3076=item C<set> An array of C<int>s or C<string>s. An exact set of group ids
3077and group names that the user should be a member of. NOTE: This does not
3078remove groups from the user where the person making the change does not
3079have the bless privilege for.
3080
3081If you specify C<set>, then C<add> and C<remove> will be ignored. A group in
3082both the C<add> and C<remove> list will be added. Specifying a group that the
3083user making the change does not have bless rights will generate an error.
3084
3085=back
3086
3087=item C<set_bless_groups>
3088
3089C<hash> - This is the same as set_groups, but affects what groups a user
3090has direct membership to bless that group. It takes the same inputs as
3091set_groups.
3092
3093=back
3094
3095=head1 CLASS FUNCTIONS
3096
3097These are functions that are not called on a User object, but instead are
3098called "statically," just like a normal procedural function.
3099
3100=over 4
3101
3102=item C<create>
3103
3104The same as L<Bugzilla::Object/create>.
3105
3106Params: login_name - B<Required> The login name for the new user.
3107        realname - The full name for the new user.
3108        cryptpassword  - B<Required> The password for the new user.
3109            Even though the name says "crypt", you should just specify
3110            a plain-text password. If you specify '*', the user will not
3111            be able to log in using DB authentication.
3112        disabledtext - The disable-text for the new user. If given, the user
3113            will be disabled, meaning they cannot log in. Defaults to an
3114            empty string.
3115        disable_mail - If 1, bug-related mail will not be  sent to this user;
3116            if 0, mail will be sent depending on the user's  email preferences.
3117
3118=item C<check>
3119
3120Takes a username as its only argument. Throws an error if there is no
3121user with that username. Returns a C<Bugzilla::User> object.
3122
3123=item C<check_account_creation_enabled>
3124
3125Checks that users can create new user accounts, and throws an error
3126if user creation is disabled.
3127
3128=item C<check_and_send_account_creation_confirmation($login)>
3129
3130If the user request for a new account passes validation checks, an email
3131is sent to this user for confirmation. Otherwise an error is thrown
3132indicating why the request has been rejected.
3133
3134=item C<is_available_username>
3135
3136Returns a boolean indicating whether or not the supplied username is
3137already taken in Bugzilla.
3138
3139Params: $username (scalar, string) - The full login name of the username
3140            that you are checking.
3141        $old_username (scalar, string) - If you are checking an email-change
3142            token, insert the "old" username that the user is changing from,
3143            here. Then, as long as it's the right user for that token, they
3144            can change their username to $username. (That is, this function
3145            will return a boolean true value).
3146
3147=item C<login_to_id($login, $throw_error)>
3148
3149Takes a login name of a Bugzilla user and changes that into a numeric
3150ID for that user. This ID can then be passed to Bugzilla::User::new to
3151create a new user.
3152
3153If no valid user exists with that login name, then the function returns 0.
3154However, if $throw_error is set, the function will throw a user error
3155instead of returning.
3156
3157This function can also be used when you want to just find out the userid
3158of a user, but you don't want the full weight of Bugzilla::User.
3159
3160However, consider using a Bugzilla::User object instead of this function
3161if you need more information about the user than just their ID.
3162
3163=item C<validate_password($passwd1, $passwd2)>
3164
3165Returns true if a password is valid (i.e. meets Bugzilla's
3166requirements for length and content), else throws an error.
3167Untaints C<$passwd1> if successful.
3168
3169If a second password is passed in, this function also verifies that
3170the two passwords match.
3171
3172=item C<validate_password_check($passwd1, $passwd2)>
3173
3174This sub routine is similair to C<validate_password>, except that it allows
3175the calling code to handle its own errors.
3176
3177Returns undef and untaints C<$passwd1> if a password is valid (i.e. meets
3178Bugzilla's requirements for length and content), else returns the error.
3179
3180If a second password is passed in, this function also verifies that
3181the two passwords match.
3182
3183=item C<match_field($data, $fields, $behavior)>
3184
3185=over
3186
3187=item B<Description>
3188
3189Wrapper for the C<match()> function.
3190
3191=item B<Params>
3192
3193=over
3194
3195=item C<$fields> - A hashref with field names as keys and a hash as values.
3196Each hash is of the form { 'type' => 'single|multi' }, which specifies
3197whether the field can take a single login name only or several.
3198
3199=item C<$data> (optional) - A hashref with field names as keys and field values
3200as values. If undefined, C<Bugzilla-E<gt>input_params> is used.
3201
3202=item C<$behavior> (optional) - If set to C<MATCH_SKIP_CONFIRM>, no confirmation
3203screen is displayed. In that case, the fields which don't match a unique user
3204are left undefined. If not set, a confirmation screen is displayed if at
3205least one field doesn't match any login name or match more than one.
3206
3207=back
3208
3209=item B<Returns>
3210
3211If the third parameter is set to C<MATCH_SKIP_CONFIRM>, the function returns
3212either C<USER_MATCH_SUCCESS> if all fields can be set unambiguously,
3213C<USER_MATCH_FAILED> if at least one field doesn't match any user account,
3214or C<USER_MATCH_MULTIPLE> if some fields match more than one user account.
3215
3216If the third parameter is not set, then if all fields could be set
3217unambiguously, nothing is returned, else a confirmation page is displayed.
3218
3219=item B<Note>
3220
3221This function must be called early in a script, before anything external
3222is done with the data.
3223
3224=back
3225
3226=back
3227
3228=head1 SEE ALSO
3229
3230L<Bugzilla|Bugzilla>
3231
3232=head1 B<Methods in need of POD>
3233
3234=over
3235
3236=item email_enabled
3237
3238=item cryptpassword
3239
3240=item clear_login_failures
3241
3242=item set_disable_mail
3243
3244=item has_audit_entries
3245
3246=item groups_with_icon
3247
3248=item check_login_name
3249
3250=item set_extern_id
3251
3252=item mail_settings
3253
3254=item email_disabled
3255
3256=item update
3257
3258=item is_timetracker
3259
3260=item is_enabled
3261
3262=item queryshare_groups_as_string
3263
3264=item set_login
3265
3266=item set_password
3267
3268=item last_seen_date
3269
3270=item set_disabledtext
3271
3272=item update_last_seen_date
3273
3274=item set_name
3275
3276=item DB_COLUMNS
3277
3278=item extern_id
3279
3280=item UPDATE_COLUMNS
3281
3282=back
3283