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