~ubuntu-branches/debian/sid/bugzilla/sid

« back to all changes in this revision

Viewing changes to Bugzilla/User.pm

  • Committer: Bazaar Package Importer
  • Author(s): Jonathan Patrick Davies
  • Date: 2008-05-29 17:20:32 UTC
  • mfrom: (1.1.6 upstream)
  • Revision ID: james.westby@ubuntu.com-20080529172032-tddh964ztksxayjm
Tags: 3.0.4-0ubuntu1
* New upstream release (LP: #138886, #235701).
* Removed "CVS" directories and ".cvsignore" files from upstream tarball.
* Added patches/ubuntu_01_bugzilla_libpath.dpatch - newly updated as necessary
  version of old 01_libpath.dpatch patch.
* Added patches/01_debian_package_version.dpatch - replaces old patch
  01_VERSION.dpatch, simply changes the version of Bugzilla to show the
  Debian packaging's versioning.
* Added patches/ubuntu_05_makefile_install.dpatch - Use a Makefile to
  install Bugzilla to the correct locations. Based on Makefile in old
  package but in patch form.
* Removed 02_checksetup.dpatch - fixed upstream.
* Removed 101_Config.diff - upstream has changed codebase.
* Renamed 06_contrib.dpatch to ubuntu_02_contrib_shebang_fixes.dpatch -
  corrects 'shebangs' which point to /usr/local/bin/ to /usr/bin/.
* Renamed 08_showdependencygraph.dpatch to
  ubuntu_03_showdependencygraph_url_fixes.dpatch and updated code as
  necessary - fixes graph URL to make the webdot generation possible.
* Removed CVE-2007-0791.dpatch - applied to upstream code.
* Removed CVE-2007-4543.dpatch - applied to upstream code.
* Renamed 09_homelink.dpatch to ubuntu_04_fixed_homepage_linked.dpatch -
  upstream now has links in
  'template/en/default/global/common-links.html.tmpl' instead of
  'useful-links.html.tmpl'.
* Removed 03_webpath.dpatch - upstream has changed stylesheet layout.
* Updated 10_perl_scripts_shebang.dpatch and removed part on "globals.pl" -
  no longer in source.
* Removed Debian vhost support patches (see docs/html/multiple-bz-dbs.html
  for how to run multiple Bugzilla instances):
  - Removed 04_Config.pm.dpatch - duplicate patch and unable to adapt it to
    new upstream code.
  - Removed 07_virtualhosting.dpatch - duplicate patch of
    04_Config.pm.dpatch.
  - Removed 'debian/examples' - contained Apache VHost example setup files
    for Bugzilla.
  - Removed section about vhosts from README.Debian.
* debian/rules:
  - Removed rules for "vhost conf dir", "examples" and "101_Config.diff"
    installation rules.
  - Removed part about bugzilla-fr package.
  - Remved part about "whine.pl" - now in Makefile.
  - Added rules to check the setup with upstream's "checksetup.pl" script.
* debian/control:
  - Updated Standards-Version to 3.7.3.
  - Updated compatibity level and debhelper build dependency version to 6.
  - Added Homepage field to source package stanza.
  - Added part about seeing 'bugzilla' package for more info to
    'bugzilla-docs'.
  - Added libapache2-mod-perl2, libtemplate-perl, libmime-perl,
    libappconfig-perl, libdbd-mysql-perl, libtimedate-perl, libgd-gd2-perl,
    libgd-text-perl, libxml-twig-perl, perlmagick, libemail-send-perl,
    libemail-mime-modifier-perl, libchart-perl, libgd-graph-perl,
    libhtml-scrubber-perl, libdbi-perl, libfile-spec-perl, libgd-graph-perl,
    libgd-text-perl, libnet-ldap-perl, libxml-parser-perl: to build
    dependencies with the necessary versions as stated by upstream in
    docs/html/installation.html - in order to check packaging correctly with
    'checksetup.pl' in rules. Also updated the 'bugzilla' dependencies with
    the above (LP: #235461).
  - Removed dependencies on old "apache" packages as they are no longer in
    the archives.
  - Moved mail transport agents on 'bugzilla' from Depends to
    Suggests (LP: #156405).
* debian/copyright: Updated the downloaded from link.
* debian/bugzilla.docs: Added "QUICKSTART", "rel_notes.txt" and "UPGRADING"
  documentation from source tarball for inclusion in package.
* debian/bugzilla-doc.doc-base: Corrected some spelling mistakes.
* debian/bugzilla.postinst: Removed sections about 101_Config.diff.
* Changed 'X_BUGZILLA_SITE' in bugzilla.cron.daily and bugzilla.postinst to
  'PROJECT'.

Show diffs side-by-side

added added

removed removed

Lines of Context:
27
27
#                 Gervase Markham <gerv@gerv.net>
28
28
#                 Lance Larsh <lance.larsh@oracle.com>
29
29
#                 Justin C. De Vries <judevries@novell.com>
 
30
#                 Dennis Melentyev <dennis.melentyev@infopulse.com.ua>
 
31
#                 Frédéric Buclin <LpSolit@gmail.com>
 
32
#                 Mads Bondo Dydensborg <mbd@dbc.dk>
30
33
 
31
34
################################################################################
32
35
# Module Initialization
38
41
# This module implements utilities for dealing with Bugzilla users.
39
42
package Bugzilla::User;
40
43
 
41
 
use Bugzilla::Config;
42
44
use Bugzilla::Error;
43
45
use Bugzilla::Util;
44
46
use Bugzilla::Constants;
45
47
use Bugzilla::User::Setting;
46
48
use Bugzilla::Product;
47
49
use Bugzilla::Classification;
 
50
use Bugzilla::Field;
48
51
 
49
 
use base qw(Exporter);
50
 
@Bugzilla::User::EXPORT = qw(insert_new_user is_available_username
51
 
    login_to_id
52
 
    UserInGroup
 
52
use base qw(Bugzilla::Object Exporter);
 
53
@Bugzilla::User::EXPORT = qw(is_available_username
 
54
    login_to_id user_id_to_login validate_password
53
55
    USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
54
56
    MATCH_SKIP_CONFIRM
55
57
);
64
66
 
65
67
use constant MATCH_SKIP_CONFIRM  => 1;
66
68
 
 
69
use constant DEFAULT_USER => {
 
70
    'id'             => 0,
 
71
    'realname'       => '',
 
72
    'login_name'     => '',
 
73
    'showmybugslink' => 0,
 
74
    'disabledtext'   => '',
 
75
    'disable_mail'   => 0,
 
76
};
 
77
 
 
78
use constant DB_TABLE => 'profiles';
 
79
 
 
80
# XXX Note that Bugzilla::User->name does not return the same thing
 
81
# that you passed in for "name" to new(). That's because historically
 
82
# Bugzilla::User used "name" for the realname field. This should be
 
83
# fixed one day.
 
84
use constant DB_COLUMNS => (
 
85
    'profiles.userid     AS id',
 
86
    'profiles.login_name',
 
87
    'profiles.realname',
 
88
    'profiles.mybugslink AS showmybugslink',
 
89
    'profiles.disabledtext',
 
90
    'profiles.disable_mail',
 
91
);
 
92
use constant NAME_FIELD => 'login_name';
 
93
use constant ID_FIELD   => 'userid';
 
94
use constant LIST_ORDER => NAME_FIELD;
 
95
 
 
96
use constant REQUIRED_CREATE_FIELDS => qw(login_name cryptpassword);
 
97
 
 
98
use constant VALIDATORS => {
 
99
    cryptpassword => \&_check_password,
 
100
    disable_mail  => \&_check_disable_mail,
 
101
    disabledtext  => \&_check_disabledtext,
 
102
    login_name    => \&check_login_name_for_creation,
 
103
    realname      => \&_check_realname,
 
104
};
 
105
 
 
106
sub UPDATE_COLUMNS {
 
107
    my $self = shift;
 
108
    my @cols = qw(
 
109
        disable_mail
 
110
        disabledtext
 
111
        login_name
 
112
        realname
 
113
    );
 
114
    push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
 
115
    return @cols;
 
116
};
 
117
 
67
118
################################################################################
68
119
# Functions
69
120
################################################################################
70
121
 
71
122
sub new {
72
123
    my $invocant = shift;
73
 
    my $user_id = shift;
74
 
 
75
 
    if ($user_id) {
76
 
        my $uid = $user_id;
77
 
        detaint_natural($user_id)
78
 
          || ThrowCodeError('invalid_numeric_argument',
79
 
                            {argument => 'userID',
80
 
                             value    => $uid,
81
 
                             function => 'Bugzilla::User::new'});
82
 
        return $invocant->_create("userid=?", $user_id);
83
 
    }
84
 
    else {
85
 
        return $invocant->_create;
86
 
    }
87
 
}
88
 
 
89
 
# This routine is sort of evil. Nothing except the login stuff should
90
 
# be dealing with addresses as an input, and they can get the id as a
91
 
# side effect of the other sql they have to do anyway.
92
 
# Bugzilla::BugMail still does this, probably as a left over from the
93
 
# pre-id days. Provide this as a helper, but don't document it, and hope
94
 
# that it can go away.
95
 
# The request flag stuff also does this, but it really should be passing
96
 
# in the id its already had to validate (or the User.pm object, of course)
97
 
sub new_from_login {
98
 
    my $invocant = shift;
99
 
    my $login = shift;
100
 
 
101
 
    my $dbh = Bugzilla->dbh;
102
 
    return $invocant->_create($dbh->sql_istrcmp('login_name', '?'), $login);
103
 
}
104
 
 
105
 
# Internal helper for the above |new| methods
106
 
# $cond is a string (including a placeholder ?) for the search
107
 
# requirement for the profiles table
108
 
sub _create {
109
 
    my $invocant = shift;
110
124
    my $class = ref($invocant) || $invocant;
111
 
 
112
 
    my $cond = shift;
113
 
    my $val = shift;
114
 
 
115
 
    # Allow invocation with no parameters to create a blank object
116
 
    my $self = {
117
 
        'id'             => 0,
118
 
        'name'           => '',
119
 
        'login'          => '',
120
 
        'showmybugslink' => 0,
121
 
        'disabledtext'   => '',
122
 
        'flags'          => {},
123
 
    };
124
 
    bless ($self, $class);
125
 
    return $self unless $cond && $val;
126
 
 
127
 
    # We're checking for validity here, so any value is OK
128
 
    trick_taint($val);
129
 
 
 
125
    my ($param) = @_;
 
126
 
 
127
    my $user = DEFAULT_USER;
 
128
    bless ($user, $class);
 
129
    return $user unless $param;
 
130
 
 
131
    return $class->SUPER::new(@_);
 
132
}
 
133
 
 
134
sub update {
 
135
    my $self = shift;
 
136
    my $changes = $self->SUPER::update(@_);
130
137
    my $dbh = Bugzilla->dbh;
131
138
 
132
 
    my ($id,
133
 
        $login,
134
 
        $name,
135
 
        $disabledtext,
136
 
        $mybugslink) = $dbh->selectrow_array(qq{SELECT userid,
137
 
                                                       login_name,
138
 
                                                       realname,
139
 
                                                       disabledtext,
140
 
                                                       mybugslink
141
 
                                                 FROM profiles
142
 
                                                 WHERE $cond},
143
 
                                             undef,
144
 
                                             $val);
145
 
 
146
 
    return undef unless defined $id;
147
 
 
148
 
    $self->{'id'}             = $id;
149
 
    $self->{'name'}           = $name;
150
 
    $self->{'login'}          = $login;
151
 
    $self->{'disabledtext'}   = $disabledtext;
152
 
    $self->{'showmybugslink'} = $mybugslink;
153
 
 
154
 
    return $self;
155
 
}
 
139
    if (exists $changes->{login_name}) {
 
140
        # If we changed the login, silently delete any tokens.
 
141
        $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id);
 
142
        # And rederive regex groups
 
143
        $self->derive_regexp_groups();
 
144
    }
 
145
 
 
146
    # Logout the user if necessary.
 
147
    Bugzilla->logout_user($self) 
 
148
        if (exists $changes->{login_name} || exists $changes->{disabledtext}
 
149
            || exists $changes->{cryptpassword});
 
150
 
 
151
    # XXX Can update profiles_activity here as soon as it understands
 
152
    #     field names like login_name.
 
153
 
 
154
    return $changes;
 
155
}
 
156
 
 
157
################################################################################
 
158
# Validators
 
159
################################################################################
 
160
 
 
161
sub _check_disable_mail { return $_[1] ? 1 : 0; }
 
162
sub _check_disabledtext { return trim($_[1]) || ''; }
 
163
 
 
164
# This is public since createaccount.cgi needs to use it before issuing
 
165
# a token for account creation.
 
166
sub check_login_name_for_creation {
 
167
    my ($invocant, $name) = @_;
 
168
    $name = trim($name);
 
169
    $name || ThrowUserError('user_login_required');
 
170
    validate_email_syntax($name)
 
171
        || ThrowUserError('illegal_email_address', { addr => $name });
 
172
 
 
173
    # Check the name if it's a new user, or if we're changing the name.
 
174
    if (!ref($invocant) || $invocant->login ne $name) {
 
175
        is_available_username($name) 
 
176
            || ThrowUserError('account_exists', { email => $name });
 
177
    }
 
178
 
 
179
    return $name;
 
180
}
 
181
 
 
182
sub _check_password {
 
183
    my ($self, $pass) = @_;
 
184
 
 
185
    # If the password is '*', do not encrypt it or validate it further--we 
 
186
    # are creating a user who should not be able to log in using DB 
 
187
    # authentication.
 
188
    return $pass if $pass eq '*';
 
189
 
 
190
    validate_password($pass);
 
191
    my $cryptpassword = bz_crypt($pass);
 
192
    return $cryptpassword;
 
193
}
 
194
 
 
195
sub _check_realname { return trim($_[1]) || ''; }
 
196
 
 
197
################################################################################
 
198
# Mutators
 
199
################################################################################
 
200
 
 
201
sub set_disabledtext { $_[0]->set('disabledtext', $_[1]); }
 
202
sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); }
 
203
 
 
204
sub set_login {
 
205
    my ($self, $login) = @_;
 
206
    $self->set('login_name', $login);
 
207
    delete $self->{identity};
 
208
    delete $self->{nick};
 
209
}
 
210
 
 
211
sub set_name {
 
212
    my ($self, $name) = @_;
 
213
    $self->set('realname', $name);
 
214
    delete $self->{identity};
 
215
}
 
216
 
 
217
sub set_password { $_[0]->set('cryptpassword', $_[1]); }
 
218
 
 
219
 
 
220
################################################################################
 
221
# Methods
 
222
################################################################################
156
223
 
157
224
# Accessors for user attributes
158
 
sub id { $_[0]->{id}; }
159
 
sub login { $_[0]->{login}; }
160
 
sub email { $_[0]->{login} . Param('emailsuffix'); }
161
 
sub name { $_[0]->{name}; }
 
225
sub name  { $_[0]->{realname};   }
 
226
sub login { $_[0]->{login_name}; }
 
227
sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
162
228
sub disabledtext { $_[0]->{'disabledtext'}; }
163
229
sub is_disabled { $_[0]->disabledtext ? 1 : 0; }
164
230
sub showmybugslink { $_[0]->{showmybugslink}; }
 
231
sub email_disabled { $_[0]->{disable_mail}; }
 
232
sub email_enabled { !($_[0]->{disable_mail}); }
165
233
 
166
 
sub set_flags {
167
 
    my $self = shift;
168
 
    while (my $key = shift) {
169
 
        $self->{'flags'}->{$key} = shift;
 
234
sub set_authorizer {
 
235
    my ($self, $authorizer) = @_;
 
236
    $self->{authorizer} = $authorizer;
 
237
}
 
238
sub authorizer {
 
239
    my ($self) = @_;
 
240
    if (!$self->{authorizer}) {
 
241
        require Bugzilla::Auth;
 
242
        $self->{authorizer} = new Bugzilla::Auth();
170
243
    }
171
 
}
172
 
 
173
 
sub get_flag {
174
 
    my $self = shift;
175
 
    my $key = shift;
176
 
    return $self->{'flags'}->{$key};
 
244
    return $self->{authorizer};
177
245
}
178
246
 
179
247
# Generate a string to identify the user by name + login if the user
185
253
 
186
254
    if (!defined $self->{identity}) {
187
255
        $self->{identity} = 
188
 
          $self->{name} ? "$self->{name} <$self->{login}>" : $self->{login};
 
256
          $self->name ? $self->name . " <" . $self->login. ">" : $self->login;
189
257
    }
190
258
 
191
259
    return $self->{identity};
197
265
    return "" unless $self->id;
198
266
 
199
267
    if (!defined $self->{nick}) {
200
 
        $self->{nick} = (split(/@/, $self->{login}, 2))[0];
 
268
        $self->{nick} = (split(/@/, $self->login, 2))[0];
201
269
    }
202
270
 
203
271
    return $self->{nick};
205
273
 
206
274
sub queries {
207
275
    my $self = shift;
208
 
 
209
276
    return $self->{queries} if defined $self->{queries};
210
277
    return [] unless $self->id;
211
278
 
212
279
    my $dbh = Bugzilla->dbh;
213
 
    my $used_in_whine_ref = $dbh->selectcol_arrayref(q{
214
 
                    SELECT DISTINCT query_name
215
 
                      FROM whine_events we
216
 
                INNER JOIN whine_queries wq
217
 
                        ON we.id = wq.eventid
218
 
                     WHERE we.owner_userid = ?}, undef, $self->{id});
219
 
 
220
 
    my $queries_ref = $dbh->selectall_arrayref(q{
221
 
                    SELECT name, query, linkinfooter, query_type
222
 
                      FROM namedqueries 
223
 
                     WHERE userid = ?
224
 
                  ORDER BY UPPER(name)},{'Slice'=>{}}, $self->{id});
225
 
 
226
 
    foreach my $name (@$used_in_whine_ref) { 
227
 
        foreach my $queries_hash (@$queries_ref) {
228
 
            if ($queries_hash->{name} eq $name) {
229
 
                $queries_hash->{usedinwhine} = 1;
230
 
                last;
231
 
            }
232
 
        }
233
 
    }
234
 
    $self->{queries} = $queries_ref;
235
 
 
 
280
    my $query_ids = $dbh->selectcol_arrayref(
 
281
        'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id);
 
282
    require Bugzilla::Search::Saved;
 
283
    $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids);
236
284
    return $self->{queries};
237
285
}
238
286
 
 
287
sub queries_subscribed {
 
288
    my $self = shift;
 
289
    return $self->{queries_subscribed} if defined $self->{queries_subscribed};
 
290
    return [] unless $self->id;
 
291
 
 
292
    # Exclude the user's own queries.
 
293
    my @my_query_ids = map($_->id, @{$self->queries});
 
294
    my $query_id_string = join(',', @my_query_ids) || '-1';
 
295
 
 
296
    # Only show subscriptions that we can still actually see. If a
 
297
    # user changes the shared group of a query, our subscription
 
298
    # will remain but we won't have access to the query anymore.
 
299
    my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref(
 
300
        "SELECT lif.namedquery_id
 
301
           FROM namedqueries_link_in_footer lif
 
302
                INNER JOIN namedquery_group_map ngm
 
303
                ON ngm.namedquery_id = lif.namedquery_id
 
304
          WHERE lif.user_id = ? 
 
305
                AND lif.namedquery_id NOT IN ($query_id_string)
 
306
                AND ngm.group_id IN (" . $self->groups_as_string . ")",
 
307
          undef, $self->id);
 
308
    require Bugzilla::Search::Saved;
 
309
    $self->{queries_subscribed} =
 
310
        Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
 
311
    return $self->{queries_subscribed};
 
312
}
 
313
 
 
314
sub queries_available {
 
315
    my $self = shift;
 
316
    return $self->{queries_available} if defined $self->{queries_available};
 
317
    return [] unless $self->id;
 
318
 
 
319
    # Exclude the user's own queries.
 
320
    my @my_query_ids = map($_->id, @{$self->queries});
 
321
    my $query_id_string = join(',', @my_query_ids) || '-1';
 
322
 
 
323
    my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
 
324
        'SELECT namedquery_id FROM namedquery_group_map
 
325
          WHERE group_id IN (' . $self->groups_as_string . ")
 
326
                AND namedquery_id NOT IN ($query_id_string)");
 
327
    require Bugzilla::Search::Saved;
 
328
    $self->{queries_available} =
 
329
        Bugzilla::Search::Saved->new_from_list($avail_query_ids);
 
330
    return $self->{queries_available};
 
331
}
 
332
 
239
333
sub settings {
240
334
    my ($self) = @_;
241
335
 
257
351
    my $self = shift;
258
352
 
259
353
    delete $self->{queries};
 
354
    delete $self->{queries_subscribed};
 
355
    delete $self->{queries_available};
260
356
}
261
357
 
262
358
sub groups {
277
373
    # The above gives us an arrayref [name, id, name, id, ...]
278
374
    # Convert that into a hashref
279
375
    my %groups = @$groups;
280
 
    my $sth;
281
376
    my @groupidstocheck = values(%groups);
282
377
    my %groupidschecked = ();
283
 
    $sth = $dbh->prepare("SELECT groups.name, groups.id
 
378
    my $rows = $dbh->selectall_arrayref(
 
379
                "SELECT DISTINCT groups.name, groups.id, member_id
284
380
                            FROM group_group_map
285
381
                      INNER JOIN groups
286
382
                              ON groups.id = grantor_id
287
 
                           WHERE member_id = ? 
288
 
                             AND grant_type = " . GROUP_MEMBERSHIP);
289
 
    while (my $node = shift @groupidstocheck) {
290
 
        $sth->execute($node);
291
 
        my ($member_name, $member_id);
292
 
        while (($member_name, $member_id) = $sth->fetchrow_array) {
293
 
            if (!$groupidschecked{$member_id}) {
294
 
                $groupidschecked{$member_id} = 1;
295
 
                push @groupidstocheck, $member_id;
296
 
                $groups{$member_name} = $member_id;
 
383
                           WHERE grant_type = " . GROUP_MEMBERSHIP);
 
384
    my %group_names = ();
 
385
    my %group_membership = ();
 
386
    foreach my $row (@$rows) {
 
387
        my ($member_name, $grantor_id, $member_id) = @$row; 
 
388
        # Just save the group names
 
389
        $group_names{$grantor_id} = $member_name;
 
390
        
 
391
        # And group membership
 
392
        push (@{$group_membership{$member_id}}, $grantor_id);
 
393
    }
 
394
    
 
395
    # Let's walk the groups hierarchy tree (using FIFO)
 
396
    # On the first iteration it's pre-filled with direct groups 
 
397
    # membership. Later on, each group can add its own members into the
 
398
    # FIFO. Circular dependencies are eliminated by checking
 
399
    # $groupidschecked{$member_id} hash values.
 
400
    # As a result, %groups will have all the groups we are the member of.
 
401
    while ($#groupidstocheck >= 0) {
 
402
        # Pop the head group from FIFO
 
403
        my $member_id = shift @groupidstocheck;
 
404
        
 
405
        # Skip the group if we have already checked it
 
406
        if (!$groupidschecked{$member_id}) {
 
407
            # Mark group as checked
 
408
            $groupidschecked{$member_id} = 1;
 
409
            
 
410
            # Add all its members to the FIFO check list
 
411
            # %group_membership contains arrays of group members 
 
412
            # for all groups. Accessible by group number.
 
413
            foreach my $newgroupid (@{$group_membership{$member_id}}) {
 
414
                push @groupidstocheck, $newgroupid 
 
415
                    if (!$groupidschecked{$newgroupid});
297
416
            }
 
417
            # Note on if clause: we could have group in %groups from 1st
 
418
            # query and do not have it in second one
 
419
            $groups{$group_names{$member_id}} = $member_id 
 
420
                if $group_names{$member_id} && $member_id;
298
421
        }
299
422
    }
300
423
    $self->{groups} = \%groups;
343
466
    }
344
467
 
345
468
    # If visibilitygroups are used, restrict the set of groups.
346
 
    if ((!$self->in_group('editusers')) && Param('usevisibilitygroups')) {
 
469
    if (!$self->in_group('editusers')
 
470
        && Bugzilla->params->{'usevisibilitygroups'}) 
 
471
    {
347
472
        # Users need to see a group in order to bless it.
348
473
        my $visibleGroups = join(', ', @{$self->visible_groups_direct()})
349
474
            || return $self->{'bless_groups'} = [];
357
482
}
358
483
 
359
484
sub in_group {
360
 
    my ($self, $group) = @_;
361
 
    return exists $self->groups->{$group} ? 1 : 0;
 
485
    my ($self, $group, $product_id) = @_;
 
486
    if (exists $self->groups->{$group}) {
 
487
        return 1;
 
488
    }
 
489
    elsif ($product_id && detaint_natural($product_id)) {
 
490
        # Make sure $group exists on a per-product basis.
 
491
        return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
 
492
 
 
493
        $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"};
 
494
        if (!defined $self->{"product_$product_id"}->{$group}) {
 
495
            my $dbh = Bugzilla->dbh;
 
496
            my $in_group = $dbh->selectrow_array(
 
497
                           "SELECT 1
 
498
                              FROM group_control_map
 
499
                             WHERE product_id = ?
 
500
                                   AND $group != 0
 
501
                                   AND group_id IN (" . $self->groups_as_string . ") " .
 
502
                              $dbh->sql_limit(1),
 
503
                             undef, $product_id);
 
504
 
 
505
            $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0;
 
506
        }
 
507
        return $self->{"product_$product_id"}->{$group};
 
508
    }
 
509
    # If we come here, then the user is not in the requested group.
 
510
    return 0;
362
511
}
363
512
 
364
513
sub in_group_id {
367
516
    return exists $j{$id} ? 1 : 0;
368
517
}
369
518
 
 
519
sub get_products_by_permission {
 
520
    my ($self, $group) = @_;
 
521
    # Make sure $group exists on a per-product basis.
 
522
    return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
 
523
 
 
524
    my $product_ids = Bugzilla->dbh->selectcol_arrayref(
 
525
                          "SELECT DISTINCT product_id
 
526
                             FROM group_control_map
 
527
                            WHERE $group != 0
 
528
                              AND group_id IN(" . $self->groups_as_string . ")");
 
529
 
 
530
    # No need to go further if the user has no "special" privs.
 
531
    return [] unless scalar(@$product_ids);
 
532
 
 
533
    # We will restrict the list to products the user can see.
 
534
    my $selectable_products = $self->get_selectable_products;
 
535
    my @products = grep {lsearch($product_ids, $_->id) > -1} @$selectable_products;
 
536
    return \@products;
 
537
}
 
538
 
370
539
sub can_see_user {
371
540
    my ($self, $otherUser) = @_;
372
541
    my $query;
373
542
 
374
 
    if (Param('usevisibilitygroups')) {
 
543
    if (Bugzilla->params->{'usevisibilitygroups'}) {
375
544
        # If the user can see no groups, then no users are visible either.
376
545
        my $visibleGroups = $self->visible_groups_as_string() || return 0;
377
546
        $query = qq{SELECT COUNT(DISTINCT userid)
393
562
sub can_edit_product {
394
563
    my ($self, $prod_id) = @_;
395
564
    my $dbh = Bugzilla->dbh;
396
 
    my $sth = $self->{sthCanEditProductId};
397
 
    my $userid = $self->{id};
398
 
    my $query = q{SELECT group_id FROM group_control_map 
399
 
                   WHERE product_id =? 
400
 
                     AND canedit != 0 };
401
 
    if (%{$self->groups}) {
402
 
        my $groups = join(',', values(%{$self->groups}));
403
 
        $query .= qq{AND group_id NOT IN($groups)};
404
 
    }
405
 
    unless ($sth) { $sth = $dbh->prepare($query); }
406
 
    $sth->execute($prod_id);
407
 
    $self->{sthCanEditProductId} = $sth;
408
 
    my $result = $sth->fetchrow_array();
409
 
    
410
 
    return (!defined($result));
 
565
 
 
566
    my $has_external_groups =
 
567
      $dbh->selectrow_array('SELECT 1
 
568
                               FROM group_control_map
 
569
                              WHERE product_id = ?
 
570
                                AND canedit != 0
 
571
                                AND group_id NOT IN(' . $self->groups_as_string . ')',
 
572
                             undef, $prod_id);
 
573
 
 
574
    return !$has_external_groups;
411
575
}
412
576
 
413
577
sub can_see_bug {
444
608
    $self->{sthCanSeeBug} = $sth;
445
609
    return ($ready
446
610
            && ((($reporter == $userid) && $reporter_access)
447
 
                || (Param('useqacontact') && $qacontact && ($qacontact == $userid))
 
611
                || (Bugzilla->params->{'useqacontact'} 
 
612
                    && $qacontact && ($qacontact == $userid))
448
613
                || ($owner == $userid)
449
614
                || ($isoncclist && $cclist_access)
450
615
                || (!$missinggroup)));
458
623
 
459
624
sub get_selectable_products {
460
625
    my $self = shift;
461
 
    my $classification_id = shift;
462
 
 
463
 
    if (defined $self->{selectable_products}) {
464
 
        return $self->{selectable_products};
465
 
    }
466
 
 
467
 
    my $dbh = Bugzilla->dbh;
468
 
    my @params = ();
469
 
 
470
 
    my $query = "SELECT id " .
471
 
                "FROM products " .
472
 
                "LEFT JOIN group_control_map " .
473
 
                "ON group_control_map.product_id = products.id ";
474
 
    if (Param('useentrygroupdefault')) {
475
 
        $query .= "AND group_control_map.entry != 0 ";
476
 
    } else {
477
 
        $query .= "AND group_control_map.membercontrol = " .
478
 
                  CONTROLMAPMANDATORY . " ";
479
 
    }
480
 
    $query .= "AND group_id NOT IN(" . 
481
 
               $self->groups_as_string . ") " .
482
 
              "WHERE group_id IS NULL ";
483
 
 
484
 
    if (Param('useclassification') && $classification_id) {
485
 
        $query .= "AND classification_id = ? ";
486
 
        detaint_natural($classification_id);
487
 
        push(@params, $classification_id);
488
 
    }
489
 
 
490
 
    $query .= "ORDER BY name";
491
 
 
492
 
    my $prod_ids = $dbh->selectcol_arrayref($query, undef, @params);
493
 
    my @products;
494
 
    foreach my $prod_id (@$prod_ids) {
495
 
        push(@products, new Bugzilla::Product($prod_id));
496
 
    }
497
 
    $self->{selectable_products} = \@products;
 
626
    my $class_id = shift;
 
627
    my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
 
628
 
 
629
    if (!defined $self->{selectable_products}) {
 
630
        my $query = "SELECT id " .
 
631
                    "  FROM products " .
 
632
                 "LEFT JOIN group_control_map " .
 
633
                    "    ON group_control_map.product_id = products.id ";
 
634
        if (Bugzilla->params->{'useentrygroupdefault'}) {
 
635
            $query .= " AND group_control_map.entry != 0 ";
 
636
        } else {
 
637
            $query .= " AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY;
 
638
        }
 
639
        $query .= "     AND group_id NOT IN(" . $self->groups_as_string . ") " .
 
640
                  "   WHERE group_id IS NULL " .
 
641
                  "ORDER BY name";
 
642
 
 
643
        my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
 
644
        $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
 
645
    }
 
646
 
 
647
    # Restrict the list of products to those being in the classification, if any.
 
648
    if ($class_restricted) {
 
649
        return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}];
 
650
    }
 
651
    # If we come here, then we want all selectable products.
498
652
    return $self->{selectable_products};
499
653
}
500
654
 
512
666
        $class->{$product->classification_id} ||= 
513
667
            new Bugzilla::Classification($product->classification_id);
514
668
    }
515
 
    my @sorted_class = sort {lc($a->name) cmp lc($b->name)} (values %$class);
 
669
    my @sorted_class = sort {$a->sortkey <=> $b->sortkey 
 
670
                             || lc($a->name) cmp lc($b->name)} (values %$class);
516
671
    $self->{selectable_classifications} = \@sorted_class;
517
672
    return $self->{selectable_classifications};
518
673
}
522
677
    my $dbh = Bugzilla->dbh;
523
678
 
524
679
    if (!defined($product_name)) {
525
 
        return unless $warn;
 
680
        return unless $warn == THROW_ERROR;
526
681
        ThrowUserError('no_products');
527
682
    }
528
683
    trick_taint($product_name);
529
 
 
530
 
    # Checks whether the user has access to the product.
531
 
    my $has_access = $dbh->selectrow_array('SELECT CASE WHEN group_id IS NULL
532
 
                                                        THEN 1 ELSE 0 END
533
 
                                              FROM products
534
 
                                         LEFT JOIN group_control_map
535
 
                                                ON group_control_map.product_id = products.id
536
 
                                               AND group_control_map.entry != 0
537
 
                                               AND group_id NOT IN (' . $self->groups_as_string . ')
538
 
                                             WHERE products.name = ? ' .
539
 
                                             $dbh->sql_limit(1),
540
 
                                            undef, $product_name);
541
 
 
542
 
    if (!$has_access) {
543
 
        return unless $warn;
544
 
        ThrowUserError('entry_access_denied', { product => $product_name });
545
 
    }
546
 
 
547
 
    # Checks whether the product is open for new bugs and
548
 
    # has at least one component and one version.
549
 
    my ($is_open, $has_version) = 
550
 
        $dbh->selectrow_array('SELECT CASE WHEN disallownew = 0
551
 
                                           THEN 1 ELSE 0 END,
552
 
                                      CASE WHEN versions.value IS NOT NULL
553
 
                                           THEN 1 ELSE 0 END
554
 
                                 FROM products
555
 
                           INNER JOIN components
556
 
                                   ON components.product_id = products.id
557
 
                            LEFT JOIN versions
558
 
                                   ON versions.product_id = products.id
559
 
                                WHERE products.name = ? ' .
560
 
                               $dbh->sql_limit(1), undef, $product_name);
561
 
 
562
 
    # Returns undef if the product has no components
563
 
    # Returns 0 if the product has no versions, or is closed for bug entry
564
 
    # Returns 1 if the user can enter bugs into the product
565
 
    return ($is_open && $has_version) unless $warn;
566
 
 
567
 
    # (undef, undef): the product has no components,
568
 
    # (0,     ?)    : the product is closed for new bug entry,
569
 
    # (?,     0)    : the product has no versions,
570
 
    # (1,     1)    : the user can enter bugs into the product,
571
 
    if (!defined $is_open) {
572
 
        ThrowUserError('missing_component', { product => $product_name });
573
 
    } elsif (!$is_open) {
574
 
        ThrowUserError('product_disabled', { product => $product_name });
575
 
    } elsif (!$has_version) {
576
 
        ThrowUserError('missing_version', { product => $product_name });
577
 
    }
578
 
    return 1;
 
684
    my $can_enter =
 
685
        grep($_->name eq $product_name, @{$self->get_enterable_products});
 
686
 
 
687
    return 1 if $can_enter;
 
688
 
 
689
    return 0 unless $warn == THROW_ERROR;
 
690
 
 
691
    # Check why access was denied. These checks are slow,
 
692
    # but that's fine, because they only happen if we fail.
 
693
 
 
694
    my $product = new Bugzilla::Product({name => $product_name});
 
695
 
 
696
    # The product could not exist or you could be denied...
 
697
    if (!$product || !$product->user_has_access($self)) {
 
698
        ThrowUserError('entry_access_denied', {product => $product_name});
 
699
    }
 
700
    # It could be closed for bug entry...
 
701
    elsif ($product->disallow_new) {
 
702
        ThrowUserError('product_disabled', {product => $product});
 
703
    }
 
704
    # It could have no components...
 
705
    elsif (!@{$product->components}) {
 
706
        ThrowUserError('missing_component', {product => $product});
 
707
    }
 
708
    # It could have no versions...
 
709
    elsif (!@{$product->versions}) {
 
710
        ThrowUserError ('missing_version', {product => $product});
 
711
    }
 
712
 
 
713
    die "can_enter_product reached an unreachable location.";
579
714
}
580
715
 
581
716
sub get_enterable_products {
582
717
    my $self = shift;
 
718
    my $dbh = Bugzilla->dbh;
583
719
 
584
720
    if (defined $self->{enterable_products}) {
585
721
        return $self->{enterable_products};
586
722
    }
587
723
 
588
 
    my @products;
589
 
    foreach my $product (Bugzilla::Product::get_all_products()) {
590
 
        if ($self->can_enter_product($product->name)) {
591
 
            push(@products, $product);
592
 
        }
 
724
     # All products which the user has "Entry" access to.
 
725
     my @enterable_ids =@{$dbh->selectcol_arrayref(
 
726
           'SELECT products.id FROM products
 
727
         LEFT JOIN group_control_map
 
728
                   ON group_control_map.product_id = products.id
 
729
                      AND group_control_map.entry != 0
 
730
                      AND group_id NOT IN (' . $self->groups_as_string . ')
 
731
            WHERE group_id IS NULL
 
732
                  AND products.disallownew = 0') || []};
 
733
 
 
734
    if (@enterable_ids) {
 
735
        # And all of these products must have at least one component
 
736
        # and one version.
 
737
        @enterable_ids = @{$dbh->selectcol_arrayref(
 
738
               'SELECT DISTINCT products.id FROM products
 
739
            INNER JOIN components ON components.product_id = products.id
 
740
            INNER JOIN versions ON versions.product_id = products.id
 
741
                 WHERE products.id IN (' . (join(',', @enterable_ids)) .
 
742
            ')') || []};
593
743
    }
594
 
    $self->{enterable_products} = \@products;
 
744
 
 
745
    $self->{enterable_products} =
 
746
         Bugzilla::Product->new_from_list(\@enterable_ids);
595
747
    return $self->{enterable_products};
596
748
}
597
749
 
 
750
sub get_accessible_products {
 
751
    my $self = shift;
 
752
    
 
753
    # Map the objects into a hash using the ids as keys
 
754
    my %products = map { $_->id => $_ }
 
755
                       @{$self->get_selectable_products},
 
756
                       @{$self->get_enterable_products};
 
757
    
 
758
    return [ values %products ];
 
759
}
 
760
 
 
761
sub check_can_admin_product {
 
762
    my ($self, $product_name) = @_;
 
763
 
 
764
    # First make sure the product name is valid.
 
765
    my $product = Bugzilla::Product::check_product($product_name);
 
766
 
 
767
    ($self->in_group('editcomponents', $product->id)
 
768
       && $self->can_see_product($product->name))
 
769
         || ThrowUserError('product_admin_denied', {product => $product->name});
 
770
 
 
771
    # Return the validated product object.
 
772
    return $product;
 
773
}
 
774
 
 
775
sub can_request_flag {
 
776
    my ($self, $flag_type) = @_;
 
777
 
 
778
    return ($self->can_set_flag($flag_type)
 
779
            || !$flag_type->request_group
 
780
            || $self->in_group_id($flag_type->request_group->id)) ? 1 : 0;
 
781
}
 
782
 
 
783
sub can_set_flag {
 
784
    my ($self, $flag_type) = @_;
 
785
 
 
786
    return (!$flag_type->grant_group
 
787
            || $self->in_group_id($flag_type->grant_group->id)) ? 1 : 0;
 
788
}
 
789
 
598
790
# visible_groups_inherited returns a reference to a list of all the groups
599
791
# whose members are visible to this user.
600
792
sub visible_groups_inherited {
616
808
    return [] unless $self->id;
617
809
 
618
810
    my $dbh = Bugzilla->dbh;
619
 
    my $glist = join(',',(-1,values(%{$self->groups})));
620
 
    my $sth = $dbh->prepare("SELECT DISTINCT grantor_id
621
 
                                FROM group_group_map
622
 
                               WHERE member_id IN($glist)
623
 
                                 AND grant_type=" . GROUP_VISIBLE);
 
811
    my $sth;
 
812
   
 
813
    if (Bugzilla->params->{'usevisibilitygroups'}) {
 
814
        my $glist = join(',',(-1,values(%{$self->groups})));
 
815
        $sth = $dbh->prepare("SELECT DISTINCT grantor_id
 
816
                                 FROM group_group_map
 
817
                                WHERE member_id IN($glist)
 
818
                                  AND grant_type=" . GROUP_VISIBLE);
 
819
    }
 
820
    else {
 
821
        # All groups are visible if usevisibilitygroups is off.
 
822
        $sth = $dbh->prepare('SELECT id FROM groups');
 
823
    }
624
824
    $sth->execute();
625
825
 
626
826
    while (my ($row) = $sth->fetchrow_array) {
636
836
    return join(', ', @{$self->visible_groups_inherited()});
637
837
}
638
838
 
 
839
# This function defines the groups a user may share a query with.
 
840
# More restrictive sites may want to build this reference to a list of group IDs
 
841
# from bless_groups instead of mirroring visible_groups_inherited, perhaps.
 
842
sub queryshare_groups {
 
843
    my $self = shift;
 
844
    my @queryshare_groups;
 
845
 
 
846
    return $self->{queryshare_groups} if defined $self->{queryshare_groups};
 
847
 
 
848
    if ($self->in_group(Bugzilla->params->{'querysharegroup'})) {
 
849
        # We want to be allowed to share with groups we're in only.
 
850
        # If usevisibilitygroups is on, then we need to restrict this to groups
 
851
        # we may see.
 
852
        if (Bugzilla->params->{'usevisibilitygroups'}) {
 
853
            foreach(@{$self->visible_groups_inherited()}) {
 
854
                next unless $self->in_group_id($_);
 
855
                push(@queryshare_groups, $_);
 
856
            }
 
857
        }
 
858
        else {
 
859
            @queryshare_groups = values(%{$self->groups});
 
860
        }
 
861
    }
 
862
 
 
863
    return $self->{queryshare_groups} = \@queryshare_groups;
 
864
}
 
865
 
 
866
sub queryshare_groups_as_string {
 
867
    my $self = shift;
 
868
    return join(', ', @{$self->queryshare_groups()});
 
869
}
 
870
 
639
871
sub derive_regexp_groups {
640
872
    my ($self) = @_;
641
873
 
646
878
 
647
879
    my $sth;
648
880
 
649
 
    # avoid races, we are only up to date as of the BEGINNING of this process
650
 
    my $time = $dbh->selectrow_array("SELECT NOW()");
651
 
 
652
881
    # add derived records for any matching regexps
653
882
 
654
883
    $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id
668
897
                                         AND isbless = 0
669
898
                                         AND grant_type = ?});
670
899
    while (my ($group, $regexp, $present) = $sth->fetchrow_array()) {
671
 
        if (($regexp ne '') && ($self->{login} =~ m/$regexp/i)) {
 
900
        if (($regexp ne '') && ($self->login =~ m/$regexp/i)) {
672
901
            $group_insert->execute($id, $group, GRANT_REGEXP) unless $present;
673
902
        } else {
674
903
            $group_delete->execute($id, $group, GRANT_REGEXP) if $present;
675
904
        }
676
905
    }
677
 
 
678
 
    $dbh->do(q{UPDATE profiles
679
 
                  SET refreshed_when = ?
680
 
                WHERE userid=?},
681
 
             undef,
682
 
             $time,
683
 
             $id);
684
906
}
685
907
 
686
908
sub product_responsibilities {
687
909
    my $self = shift;
 
910
    my $dbh = Bugzilla->dbh;
688
911
 
689
912
    return $self->{'product_resp'} if defined $self->{'product_resp'};
690
913
    return [] unless $self->id;
691
914
 
692
 
    my $h = Bugzilla->dbh->selectall_arrayref(
693
 
        qq{SELECT products.name AS productname,
694
 
                  components.name AS componentname,
695
 
                  initialowner,
696
 
                  initialqacontact
697
 
           FROM products, components
698
 
           WHERE products.id = components.product_id
699
 
             AND ? IN (initialowner, initialqacontact)
700
 
          },
701
 
        {'Slice' => {}}, $self->id);
702
 
    $self->{'product_resp'} = $h;
 
915
    my $comp_ids = $dbh->selectcol_arrayref('SELECT id FROM components
 
916
                                              WHERE initialowner = ?
 
917
                                                 OR initialqacontact = ?',
 
918
                                              undef, ($self->id, $self->id));
703
919
 
704
 
    return $h;
 
920
    # We cannot |use| it, because Component.pm already |use|s User.pm.
 
921
    require Bugzilla::Component;
 
922
    $self->{'product_resp'} = Bugzilla::Component->new_from_list($comp_ids);
 
923
    return $self->{'product_resp'};
705
924
}
706
925
 
707
926
sub can_bless {
714
933
    }
715
934
 
716
935
    # Otherwise, we're checking a specific group
717
 
    my $group_name = shift;
718
 
    return (grep {$$_{'name'} eq $group_name} (@{$self->bless_groups})) ? 1 : 0;
 
936
    my $group_id = shift;
 
937
    return (grep {$$_{'id'} eq $group_id} (@{$self->bless_groups})) ? 1 : 0;
719
938
}
720
939
 
721
940
sub flatten_group_membership {
751
970
    # $str contains the string to match, while $limit contains the
752
971
    # maximum number of records to retrieve.
753
972
    my ($str, $limit, $exclude_disabled) = @_;
754
 
    
 
973
    my $user = Bugzilla->user;
 
974
    my $dbh = Bugzilla->dbh;
 
975
 
755
976
    my @users = ();
756
 
 
757
977
    return \@users if $str =~ /^\s*$/;
758
978
 
759
979
    # The search order is wildcards, then exact match, then substring search.
762
982
    # ones following it will not execute.
763
983
 
764
984
    # first try wildcards
765
 
 
766
985
    my $wildstr = $str;
767
 
    my $user = Bugzilla->user;
768
 
    my $dbh = Bugzilla->dbh;
769
986
 
770
 
    if ($wildstr =~ s/\*/\%/g && # don't do wildcards if no '*' in the string
771
 
        Param('usermatchmode') ne 'off') { # or if we only want exact matches
 
987
    if ($wildstr =~ s/\*/\%/g # don't do wildcards if no '*' in the string
 
988
        # or if we only want exact matches
 
989
        && Bugzilla->params->{'usermatchmode'} ne 'off') 
 
990
    {
772
991
 
773
992
        # Build the query.
774
 
        my $sqlstr = &::SqlQuote($wildstr);
775
 
        my $query  = "SELECT DISTINCT userid, realname, login_name, " .
776
 
                     "LENGTH(login_name) AS namelength " .
777
 
                     "FROM profiles ";
778
 
        if (&::Param('usevisibilitygroups')) {
779
 
            $query .= ", user_group_map ";
 
993
        trick_taint($wildstr);
 
994
        my $query  = "SELECT DISTINCT login_name FROM profiles ";
 
995
        if (Bugzilla->params->{'usevisibilitygroups'}) {
 
996
            $query .= "INNER JOIN user_group_map
 
997
                               ON user_group_map.user_id = profiles.userid ";
780
998
        }
781
 
        $query .= "WHERE ("  
782
 
            . $dbh->sql_istrcmp('login_name', $sqlstr, "LIKE") . " OR " .
783
 
              $dbh->sql_istrcmp('realname', $sqlstr, "LIKE") . ") ";
784
 
        if (&::Param('usevisibilitygroups')) {
785
 
            $query .= "AND user_group_map.user_id = userid " .
786
 
                      "AND isbless = 0 " .
 
999
        $query .= "WHERE ("
 
1000
            . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " .
 
1001
              $dbh->sql_istrcmp('realname', '?', "LIKE") . ") ";
 
1002
        if (Bugzilla->params->{'usevisibilitygroups'}) {
 
1003
            $query .= "AND isbless = 0 " .
787
1004
                      "AND group_id IN(" .
788
 
                      join(', ', (-1, @{$user->visible_groups_inherited})) . 
789
 
                      ")";
 
1005
                      join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
790
1006
        }
791
1007
        $query    .= " AND disabledtext = '' " if $exclude_disabled;
792
 
        $query    .= "ORDER BY namelength ";
 
1008
        $query    .= " ORDER BY login_name ";
793
1009
        $query    .= $dbh->sql_limit($limit) if $limit;
794
1010
 
795
1011
        # Execute the query, retrieve the results, and make them into
796
1012
        # User objects.
797
 
 
798
 
        &::PushGlobalSQLState();
799
 
        &::SendSQL($query);
800
 
        push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
801
 
        &::PopGlobalSQLState();
802
 
 
 
1013
        my $user_logins = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
 
1014
        foreach my $login_name (@$user_logins) {
 
1015
            push(@users, new Bugzilla::User({ name => $login_name }));
 
1016
        }
803
1017
    }
804
1018
    else {    # try an exact match
805
 
 
806
 
        my $sqlstr = &::SqlQuote($str);
807
 
        my $query  = "SELECT userid, realname, login_name " .
808
 
                     "FROM profiles " .
809
 
                     "WHERE " . $dbh->sql_istrcmp('login_name', $sqlstr);
810
1019
        # Exact matches don't care if a user is disabled.
 
1020
        trick_taint($str);
 
1021
        my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles
 
1022
                                             WHERE ' . $dbh->sql_istrcmp('login_name', '?'),
 
1023
                                             undef, $str);
811
1024
 
812
 
        &::PushGlobalSQLState();
813
 
        &::SendSQL($query);
814
 
        push(@users, new Bugzilla::User(&::FetchSQLData())) if &::MoreSQLData();
815
 
        &::PopGlobalSQLState();
 
1025
        push(@users, new Bugzilla::User($user_id)) if $user_id;
816
1026
    }
817
1027
 
818
1028
    # then try substring search
819
 
 
820
1029
    if ((scalar(@users) == 0)
821
 
        && (&::Param('usermatchmode') eq 'search')
 
1030
        && (Bugzilla->params->{'usermatchmode'} eq 'search')
822
1031
        && (length($str) >= 3))
823
1032
    {
824
 
 
825
 
        my $sqlstr = &::SqlQuote(lc($str));
826
 
 
827
 
        my $query   = "SELECT DISTINCT userid, realname, login_name, " .
828
 
                      "LENGTH(login_name) AS namelength " .
829
 
                      "FROM  profiles";
830
 
        if (&::Param('usevisibilitygroups')) {
831
 
            $query .= ", user_group_map";
 
1033
        $str = lc($str);
 
1034
        trick_taint($str);
 
1035
 
 
1036
        my $query   = "SELECT DISTINCT login_name FROM profiles ";
 
1037
        if (Bugzilla->params->{'usevisibilitygroups'}) {
 
1038
            $query .= "INNER JOIN user_group_map
 
1039
                               ON user_group_map.user_id = profiles.userid ";
832
1040
        }
833
1041
        $query     .= " WHERE (" .
834
 
                $dbh->sql_position($sqlstr, 'LOWER(login_name)') . " > 0" .
835
 
                      " OR " .
836
 
                $dbh->sql_position($sqlstr, 'LOWER(realname)') . " > 0)";
837
 
        if (&::Param('usevisibilitygroups')) {
838
 
            $query .= " AND user_group_map.user_id = userid" .
839
 
                      " AND isbless = 0" .
 
1042
                $dbh->sql_position('?', 'LOWER(login_name)') . " > 0" . " OR " .
 
1043
                $dbh->sql_position('?', 'LOWER(realname)') . " > 0) ";
 
1044
        if (Bugzilla->params->{'usevisibilitygroups'}) {
 
1045
            $query .= " AND isbless = 0" .
840
1046
                      " AND group_id IN(" .
841
 
                join(', ', (-1, @{$user->visible_groups_inherited})) . ")";
842
 
        }
843
 
        $query     .= " AND disabledtext = ''" if $exclude_disabled;
844
 
        $query     .= " ORDER BY namelength";
845
 
        $query     .= " " . $dbh->sql_limit($limit) if $limit;
846
 
        &::PushGlobalSQLState();
847
 
        &::SendSQL($query);
848
 
        push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
849
 
        &::PopGlobalSQLState();
 
1047
                join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
 
1048
        }
 
1049
        $query     .= " AND disabledtext = '' " if $exclude_disabled;
 
1050
        $query    .= " ORDER BY login_name ";
 
1051
        $query     .= $dbh->sql_limit($limit) if $limit;
 
1052
 
 
1053
        my $user_logins = $dbh->selectcol_arrayref($query, undef, ($str, $str));
 
1054
        foreach my $login_name (@$user_logins) {
 
1055
            push(@users, new Bugzilla::User({ name => $login_name }));
 
1056
        }
850
1057
    }
851
 
 
852
 
    # order @users by alpha
853
 
 
854
 
    @users = sort { uc($a->login) cmp uc($b->login) } @users;
855
 
 
856
1058
    return \@users;
857
1059
}
858
1060
 
908
1110
    my $need_confirm = 0;       # whether to display confirmation screen
909
1111
    my $match_multiple = 0;     # whether we ever matched more than one user
910
1112
 
 
1113
    my $params = Bugzilla->params;
 
1114
 
911
1115
    # prepare default form values
912
1116
 
913
1117
    # What does a "--do_not_change--" field look like (if any)?
933
1137
                # The field is a requestee field; in order for its name 
934
1138
                # to show up correctly on the confirmation page, we need 
935
1139
                # to find out the name of its flag type.
936
 
                if ($field_name =~ /^requestee-(\d+)$/) {
937
 
                    my $flag = Bugzilla::Flag::get($1);
938
 
                    $expanded_fields->{$field_name}->{'flag_type'} = 
939
 
                      $flag->{'type'};
940
 
                }
941
 
                elsif ($field_name =~ /^requestee_type-(\d+)$/) {
942
 
                    $expanded_fields->{$field_name}->{'flag_type'} = 
943
 
                      Bugzilla::FlagType::get($1);
 
1140
                if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
 
1141
                    my $flag_type;
 
1142
                    if ($1) {
 
1143
                        require Bugzilla::FlagType;
 
1144
                        $flag_type = new Bugzilla::FlagType($2);
 
1145
                    }
 
1146
                    else {
 
1147
                        require Bugzilla::Flag;
 
1148
                        my $flag = new Bugzilla::Flag($2);
 
1149
                        $flag_type = $flag->type if $flag;
 
1150
                    }
 
1151
                    if ($flag_type) {
 
1152
                        $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
 
1153
                    }
 
1154
                    else {
 
1155
                        # No need to look for a valid requestee if the flag(type)
 
1156
                        # has been deleted (may occur in race conditions).
 
1157
                        delete $expanded_fields->{$field_name};
 
1158
                        $cgi->delete($field_name);
 
1159
                    }
944
1160
                }
945
1161
            }
946
1162
        }
1008
1224
        }
1009
1225
 
1010
1226
        my $limit = 0;
1011
 
        if (&::Param('maxusermatches')) {
1012
 
            $limit = &::Param('maxusermatches') + 1;
 
1227
        if ($params->{'maxusermatches'}) {
 
1228
            $limit = $params->{'maxusermatches'} + 1;
1013
1229
        }
1014
1230
 
1015
1231
        for my $query (@queries) {
1022
1238
 
1023
1239
            # skip confirmation for exact matches
1024
1240
            if ((scalar(@{$users}) == 1)
1025
 
                && (@{$users}[0]->{'login'} eq $query))
 
1241
                && (lc(@{$users}[0]->login) eq lc($query)))
 
1242
 
1026
1243
            {
1027
1244
                $cgi->append(-name=>$field,
1028
 
                             -values=>[@{$users}[0]->{'login'}]);
 
1245
                             -values=>[@{$users}[0]->login]);
1029
1246
 
1030
1247
                next;
1031
1248
            }
1038
1255
            if (scalar(@{$users}) == 1) { # exactly one match
1039
1256
 
1040
1257
                $cgi->append(-name=>$field,
1041
 
                             -values=>[@{$users}[0]->{'login'}]);
 
1258
                             -values=>[@{$users}[0]->login]);
1042
1259
 
1043
 
                $need_confirm = 1 if &::Param('confirmuniqueusermatch');
 
1260
                $need_confirm = 1 if $params->{'confirmuniqueusermatch'};
1044
1261
 
1045
1262
            }
1046
1263
            elsif ((scalar(@{$users}) > 1)
1047
 
                    && (&::Param('maxusermatches') != 1)) {
 
1264
                    && ($params->{'maxusermatches'} != 1)) {
1048
1265
                $need_confirm = 1;
1049
1266
                $match_multiple = 1;
1050
1267
 
1051
 
                if ((&::Param('maxusermatches'))
1052
 
                   && (scalar(@{$users}) > &::Param('maxusermatches')))
 
1268
                if (($params->{'maxusermatches'})
 
1269
                   && (scalar(@{$users}) > $params->{'maxusermatches'}))
1053
1270
                {
1054
1271
                    $matches->{$field}->{$query}->{'status'} = 'trunc';
1055
1272
                    pop @{$users};  # take the last one out
1119
1336
    'Attachment description' => EVT_ATTACHMENT_DATA,
1120
1337
    'Attachment mime type'   => EVT_ATTACHMENT_DATA,
1121
1338
    'Attachment is patch'    => EVT_ATTACHMENT_DATA,
1122
 
    'BugsThisDependsOn'      => EVT_DEPEND_BLOCK,
1123
 
    'OtherBugsDependingOnThis' => EVT_DEPEND_BLOCK);
 
1339
    'Depends on'             => EVT_DEPEND_BLOCK,
 
1340
    'Blocks'                 => EVT_DEPEND_BLOCK);
1124
1341
 
1125
1342
# Returns true if the user wants mail for a given bug change.
1126
1343
# Note: the "+" signs before the constants suppress bareword quoting.
1127
1344
sub wants_bug_mail {
1128
1345
    my $self = shift;
1129
 
    my ($bug_id, $relationship, $fieldDiffs, $commentField, $changer, $bug_is_new) = @_;
 
1346
    my ($bug_id, $relationship, $fieldDiffs, $commentField, $dependencyText,
 
1347
        $changer, $bug_is_new) = @_;
1130
1348
 
1131
 
    # Don't send any mail, ever, if account is disabled 
1132
 
    # XXX Temporary Compatibility Change 1 of 2:
1133
 
    # This code is disabled for the moment to make the behaviour like the old
1134
 
    # system, which sent bugmail to disabled accounts.
1135
 
    # return 0 if $self->{'disabledtext'};
1136
 
    
1137
1349
    # Make a list of the events which have happened during this bug change,
1138
1350
    # from the point of view of this user.    
1139
1351
    my %events;    
1140
1352
    foreach my $ref (@$fieldDiffs) {
1141
 
        my ($who, $fieldName, $when, $old, $new) = @$ref;
 
1353
        my ($who, $whoname, $fieldName, $when, $old, $new) = @$ref;
1142
1354
        # A change to any of the above fields sets the corresponding event
1143
1355
        if (defined($names_to_events{$fieldName})) {
1144
1356
            $events{$names_to_events{$fieldName}} = 1;
1145
1357
        }
1146
1358
        else {
1147
1359
            # Catch-all for any change not caught by a more specific event
1148
 
            # XXX: Temporary Compatibility Change 2 of 2:
1149
 
            # This code is disabled, and replaced with the code a few lines
1150
 
            # below, in order to make the behaviour more like the original, 
1151
 
            # which only added this event if _all_ changes were of "other" type.
1152
 
            # $events{+EVT_OTHER} = 1;            
 
1360
            $events{+EVT_OTHER} = 1;            
1153
1361
        }
1154
1362
 
1155
1363
        # If the user is in a particular role and the value of that role
1162
1370
        
1163
1371
        if ($fieldName eq "CC") {
1164
1372
            my $login = $self->login;
1165
 
            my $inold = ($old =~ /^(.*,)?\Q$login\E(,.*)?$/);
1166
 
            my $innew = ($new =~ /^(.*,)?\Q$login\E(,.*)?$/);
 
1373
            my $inold = ($old =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
 
1374
            my $innew = ($new =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
1167
1375
            if ($inold != $innew)
1168
1376
            {
1169
1377
                $events{+EVT_ADDED_REMOVED} = 1;
1188
1396
        $events{+EVT_COMMENT} = 1;
1189
1397
    }
1190
1398
    
 
1399
    # Dependent changed bugmails must have an event to ensure the bugmail is
 
1400
    # emailed.
 
1401
    if ($dependencyText ne '') {
 
1402
        $events{+EVT_DEPEND_BLOCK} = 1;
 
1403
    }
 
1404
 
1191
1405
    my @event_list = keys %events;
1192
1406
    
1193
 
    # XXX Temporary Compatibility Change 2 of 2:
1194
 
    # See above comment.
1195
 
    if (!scalar(@event_list)) {
1196
 
      @event_list = (EVT_OTHER);
1197
 
    }
1198
 
    
1199
1407
    my $wants_mail = $self->wants_mail(\@event_list, $relationship);
1200
1408
 
1201
1409
    # The negative events are handled separately - they can't be incorporated
1203
1411
    # 
1204
1412
    # We do them separately because if _any_ of them are set, we don't want
1205
1413
    # the mail.
1206
 
    if ($wants_mail && $changer && ($self->{'login'} eq $changer)) {
 
1414
    if ($wants_mail && $changer && ($self->login eq $changer)) {
1207
1415
        $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship);
1208
1416
    }    
1209
1417
    
1213
1421
        # need one piece of information, and doing so (as of 2004-11-23) slows
1214
1422
        # down bugmail sending by a factor of 2. If Bug creation was more
1215
1423
        # lazy, this might not be so bad.
1216
 
        my $bug_status = $dbh->selectrow_array("SELECT bug_status 
1217
 
                                                FROM bugs 
1218
 
                                                WHERE bug_id = $bug_id"); 
1219
 
         
 
1424
        my $bug_status = $dbh->selectrow_array('SELECT bug_status
 
1425
                                                FROM bugs WHERE bug_id = ?',
 
1426
                                                undef, $bug_id);
 
1427
 
1220
1428
        if ($bug_status eq "UNCONFIRMED") {
1221
1429
            $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
1222
1430
        }
1238
1446
    
1239
1447
    # No mail if there are no events
1240
1448
    return 0 if !scalar(@$events);
1241
 
    
1242
 
    my $dbh = Bugzilla->dbh;
1243
 
    
 
1449
 
1244
1450
    # If a relationship isn't given, default to REL_ANY.
1245
1451
    if (!defined($relationship)) {
1246
1452
        $relationship = REL_ANY;
1247
1453
    }
1248
 
    
 
1454
 
 
1455
    # Skip DB query if relationship is explicit
 
1456
    return 1 if $relationship == REL_GLOBAL_WATCHER;
 
1457
 
 
1458
    my $dbh = Bugzilla->dbh;
 
1459
 
1249
1460
    my $wants_mail = 
1250
 
        $dbh->selectrow_array("SELECT 1 
1251
 
                              FROM email_setting
1252
 
                              WHERE user_id = $self->{'id'}
1253
 
                              AND relationship = $relationship 
1254
 
                              AND event IN (" . join(",", @$events) . ") 
1255
 
                              LIMIT 1");
1256
 
                              
 
1461
        $dbh->selectrow_array('SELECT 1
 
1462
                                 FROM email_setting
 
1463
                                WHERE user_id = ?
 
1464
                                  AND relationship = ?
 
1465
                                  AND event IN (' . join(',', @$events) . ') ' .
 
1466
                                      $dbh->sql_limit(1),
 
1467
                              undef, ($self->{'id'}, $relationship));
 
1468
 
1257
1469
    return defined($wants_mail) ? 1 : 0;
1258
1470
}
1259
1471
 
1261
1473
    my $self = shift;
1262
1474
 
1263
1475
    if (!defined $self->{'is_mover'}) {
1264
 
        my @movers = map { trim($_) } split(',', Param('movers'));
 
1476
        my @movers = map { trim($_) } split(',', Bugzilla->params->{'movers'});
1265
1477
        $self->{'is_mover'} = ($self->id
1266
1478
                               && lsearch(\@movers, $self->login) != -1);
1267
1479
    }
1272
1484
    my $self = shift;
1273
1485
 
1274
1486
    if (!defined $self->{'is_insider'}) {
1275
 
        my $insider_group = Param('insidergroup');
 
1487
        my $insider_group = Bugzilla->params->{'insidergroup'};
1276
1488
        $self->{'is_insider'} =
1277
1489
            ($insider_group && $self->in_group($insider_group)) ? 1 : 0;
1278
1490
    }
1279
1491
    return $self->{'is_insider'};
1280
1492
}
1281
1493
 
 
1494
sub is_global_watcher {
 
1495
    my $self = shift;
 
1496
 
 
1497
    if (!defined $self->{'is_global_watcher'}) {
 
1498
        my @watchers = split(/[,\s]+/, Bugzilla->params->{'globalwatchers'});
 
1499
        $self->{'is_global_watcher'} = scalar(grep { $_ eq $self->login } @watchers) ? 1 : 0;
 
1500
    }
 
1501
    return  $self->{'is_global_watcher'};
 
1502
}
 
1503
 
1282
1504
sub get_userlist {
1283
1505
    my $self = shift;
1284
1506
 
1286
1508
 
1287
1509
    my $dbh = Bugzilla->dbh;
1288
1510
    my $query  = "SELECT DISTINCT login_name, realname,";
1289
 
    if (&::Param('usevisibilitygroups')) {
 
1511
    if (Bugzilla->params->{'usevisibilitygroups'}) {
1290
1512
        $query .= " COUNT(group_id) ";
1291
1513
    } else {
1292
1514
        $query .= " 1 ";
1293
1515
    }
1294
1516
    $query     .= "FROM profiles ";
1295
 
    if (&::Param('usevisibilitygroups')) {
 
1517
    if (Bugzilla->params->{'usevisibilitygroups'}) {
1296
1518
        $query .= "LEFT JOIN user_group_map " .
1297
1519
                  "ON user_group_map.user_id = userid AND isbless = 0 " .
1298
1520
                  "AND group_id IN(" .
1318
1540
    return $self->{'userlist'};
1319
1541
}
1320
1542
 
1321
 
sub insert_new_user {
1322
 
    my ($username, $realname, $password, $disabledtext) = (@_);
 
1543
sub create {
 
1544
    my $invocant = shift;
 
1545
    my $class = ref($invocant) || $invocant;
1323
1546
    my $dbh = Bugzilla->dbh;
1324
1547
 
1325
 
    $disabledtext ||= '';
1326
 
 
1327
 
    # If not specified, generate a new random password for the user.
1328
 
    # If the password is '*', do not encrypt it; we are creating a user
1329
 
    # based on the ENV auth method.
1330
 
    $password ||= generate_random_password();
1331
 
    my $cryptpassword = ($password ne '*') ? bz_crypt($password) : $password;
1332
 
 
1333
 
    # XXX - These should be moved into is_available_username or validate_email_syntax
1334
 
    #       At the least, they shouldn't be here. They're safe for now, though.
1335
 
    trick_taint($username);
1336
 
    trick_taint($realname);
1337
 
 
1338
 
    # Insert the new user record into the database.
1339
 
    $dbh->do("INSERT INTO profiles 
1340
 
                          (login_name, realname, cryptpassword, disabledtext,
1341
 
                           refreshed_when) 
1342
 
                   VALUES (?, ?, ?, ?, '1901-01-01 00:00:00')",
1343
 
             undef, 
1344
 
             ($username, $realname, $cryptpassword, $disabledtext));
 
1548
    $dbh->bz_lock_tables('profiles WRITE', 'profiles_activity WRITE',
 
1549
        'user_group_map WRITE', 'email_setting WRITE', 'groups READ', 
 
1550
        'tokens READ', 'fielddefs READ');
 
1551
 
 
1552
    my $user = $class->SUPER::create(@_);
1345
1553
 
1346
1554
    # Turn on all email for the new user
1347
 
    my $userid = $dbh->bz_last_key('profiles', 'userid');
1348
 
 
1349
1555
    foreach my $rel (RELATIONSHIPS) {
1350
1556
        foreach my $event (POS_EVENTS, NEG_EVENTS) {
1351
1557
            # These "exceptions" define the default email preferences.
1355
1561
            next if ($event == EVT_CHANGED_BY_ME);
1356
1562
            next if (($event == EVT_CC) && ($rel != REL_REPORTER));
1357
1563
 
1358
 
            $dbh->do("INSERT INTO email_setting " . 
1359
 
                     "(user_id, relationship, event) " . 
1360
 
                     "VALUES ($userid, $rel, $event)");
1361
 
        }        
 
1564
            $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
 
1565
                      VALUES (?, ?, ?)', undef, ($user->id, $rel, $event));
 
1566
        }
1362
1567
    }
1363
1568
 
1364
1569
    foreach my $event (GLOBAL_EVENTS) {
1365
 
        $dbh->do("INSERT INTO email_setting " . 
1366
 
                 "(user_id, relationship, event) " . 
1367
 
                 "VALUES ($userid, " . REL_ANY . ", $event)");
 
1570
        $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
 
1571
                  VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event));
1368
1572
    }
1369
1573
 
1370
 
    my $user = new Bugzilla::User($userid);
1371
1574
    $user->derive_regexp_groups();
1372
1575
 
1373
 
    
1374
 
    # Return the password to the calling code so it can be included
1375
 
    # in an email sent to the user.
1376
 
    return $password;
 
1576
    # Add the creation date to the profiles_activity table.
 
1577
    # $who is the user who created the new user account, i.e. either an
 
1578
    # admin or the new user himself.
 
1579
    my $who = Bugzilla->user->id || $user->id;
 
1580
    my $creation_date_fieldid = get_field_id('creation_ts');
 
1581
 
 
1582
    $dbh->do('INSERT INTO profiles_activity
 
1583
                          (userid, who, profiles_when, fieldid, newvalue)
 
1584
                   VALUES (?, ?, NOW(), ?, NOW())',
 
1585
                   undef, ($user->id, $who, $creation_date_fieldid));
 
1586
 
 
1587
    $dbh->bz_unlock_tables();
 
1588
 
 
1589
    # Return the newly created user account.
 
1590
    return $user;
1377
1591
}
1378
1592
 
1379
1593
sub is_available_username {
1393
1607
    # was unsafe and required weird escaping; using substring to pull out
1394
1608
    # the new/old email addresses and sql_position() to find the delimiter (':')
1395
1609
    # is cleaner/safer
1396
 
    my $sth = $dbh->prepare(
1397
 
        "SELECT eventdata FROM tokens WHERE tokentype = 'emailold'
1398
 
        AND SUBSTRING(eventdata, 1, (" 
1399
 
        . $dbh->sql_position(q{':'}, 'eventdata') . "-  1)) = ?
1400
 
        OR SUBSTRING(eventdata, (" 
1401
 
        . $dbh->sql_position(q{':'}, 'eventdata') . "+ 1)) = ?");
1402
 
    $sth->execute($username, $username);
 
1610
    my $eventdata = $dbh->selectrow_array(
 
1611
        "SELECT eventdata
 
1612
           FROM tokens
 
1613
          WHERE (tokentype = 'emailold'
 
1614
                AND SUBSTRING(eventdata, 1, (" .
 
1615
                    $dbh->sql_position(q{':'}, 'eventdata') . "-  1)) = ?)
 
1616
             OR (tokentype = 'emailnew'
 
1617
                AND SUBSTRING(eventdata, (" .
 
1618
                    $dbh->sql_position(q{':'}, 'eventdata') . "+ 1)) = ?)",
 
1619
         undef, ($username, $username));
1403
1620
 
1404
 
    if (my ($eventdata) = $sth->fetchrow_array()) {
 
1621
    if ($eventdata) {
1405
1622
        # Allow thru owner of token
1406
1623
        if($old_username && ($eventdata eq "$old_username:$username")) {
1407
1624
            return 1;
1413
1630
}
1414
1631
 
1415
1632
sub login_to_id {
1416
 
    my ($login) = (@_);
 
1633
    my ($login, $throw_error) = @_;
1417
1634
    my $dbh = Bugzilla->dbh;
1418
 
    # $login will only be used by the following SELECT statement, so it's safe.
 
1635
    # No need to validate $login -- it will be used by the following SELECT
 
1636
    # statement only, so it's safe to simply trick_taint.
1419
1637
    trick_taint($login);
1420
1638
    my $user_id = $dbh->selectrow_array("SELECT userid FROM profiles WHERE " .
1421
1639
                                        $dbh->sql_istrcmp('login_name', '?'),
1422
1640
                                        undef, $login);
1423
1641
    if ($user_id) {
1424
1642
        return $user_id;
 
1643
    } elsif ($throw_error) {
 
1644
        ThrowUserError('invalid_username', { name => $login });
1425
1645
    } else {
1426
1646
        return 0;
1427
1647
    }
1428
1648
}
1429
1649
 
1430
 
sub UserInGroup {
1431
 
    return exists Bugzilla->user->groups->{$_[0]} ? 1 : 0;
1432
 
}
 
1650
sub user_id_to_login {
 
1651
    my $user_id = shift;
 
1652
    my $dbh = Bugzilla->dbh;
 
1653
 
 
1654
    return '' unless ($user_id && detaint_natural($user_id));
 
1655
 
 
1656
    my $login = $dbh->selectrow_array('SELECT login_name FROM profiles
 
1657
                                       WHERE userid = ?', undef, $user_id);
 
1658
    return $login || '';
 
1659
}
 
1660
 
 
1661
sub validate_password {
 
1662
    my ($password, $matchpassword) = @_;
 
1663
 
 
1664
    if (length($password) < USER_PASSWORD_MIN_LENGTH) {
 
1665
        ThrowUserError('password_too_short');
 
1666
    } elsif (length($password) > USER_PASSWORD_MAX_LENGTH) {
 
1667
        ThrowUserError('password_too_long');
 
1668
    } elsif ((defined $matchpassword) && ($password ne $matchpassword)) {
 
1669
        ThrowUserError('passwords_dont_match');
 
1670
    }
 
1671
    # Having done these checks makes us consider the password untainted.
 
1672
    trick_taint($_[0]);
 
1673
    return 1;
 
1674
}
 
1675
 
1433
1676
 
1434
1677
1;
1435
1678
 
1449
1692
      $user->get_selectable_classifications;
1450
1693
 
1451
1694
  # Class Functions
1452
 
  $password = insert_new_user($username, $realname, $password, $disabledtext);
 
1695
  $user = Bugzilla::User->create({ 
 
1696
      login_name    => $username, 
 
1697
      realname      => $realname, 
 
1698
      cryptpassword => $plaintext_password, 
 
1699
      disabledtext  => $disabledtext,
 
1700
      disable_mail  => 0});
1453
1701
 
1454
1702
=head1 DESCRIPTION
1455
1703
 
1459
1707
Note that the currently logged in user (if any) is available via
1460
1708
L<Bugzilla-E<gt>user|Bugzilla/"user">.
1461
1709
 
 
1710
C<Bugzilla::User> is an implementation of L<Bugzilla::Object>, and thus
 
1711
provides all the methods of L<Bugzilla::Object> in addition to the
 
1712
methods listed below.
 
1713
 
1462
1714
=head1 CONSTANTS
1463
1715
 
1464
1716
=over
1487
1739
 
1488
1740
=head1 METHODS
1489
1741
 
1490
 
=over 4
1491
 
 
1492
 
=item C<new($userid)>
1493
 
 
1494
 
Creates a new C<Bugzilla::User> object for the given user id.  If no user
1495
 
id was given, a blank object is created with no user attributes.
1496
 
 
1497
 
If an id was given but there was no matching user found, undef is returned.
1498
 
 
1499
 
=begin undocumented
1500
 
 
1501
 
=item C<new_from_login($login)>
1502
 
 
1503
 
Creates a new C<Bugzilla::User> object given the provided login. Returns
1504
 
C<undef> if no matching user is found.
1505
 
 
1506
 
This routine should not be required in general; most scripts should be using
1507
 
userids instead.
1508
 
 
1509
 
=end undocumented
 
1742
=head2 Saved and Shared Queries
 
1743
 
 
1744
=over
 
1745
 
 
1746
=item C<queries>
 
1747
 
 
1748
Returns an arrayref of the user's own saved queries, sorted by name. The 
 
1749
array contains L<Bugzilla::Search::Saved> objects.
 
1750
 
 
1751
=item C<queries_subscribed>
 
1752
 
 
1753
Returns an arrayref of shared queries that the user has subscribed to.
 
1754
That is, these are shared queries that the user sees in their footer.
 
1755
This array contains L<Bugzilla::Search::Saved> objects.
 
1756
 
 
1757
=item C<queries_available>
 
1758
 
 
1759
Returns an arrayref of all queries to which the user could possibly
 
1760
subscribe. This includes the contents of L</queries_subscribed>.
 
1761
An array of L<Bugzilla::Search::Saved> objects.
 
1762
 
 
1763
=item C<flush_queries_cache>
 
1764
 
 
1765
Some code modifies the set of stored queries. Because C<Bugzilla::User> does
 
1766
not handle these modifications, but does cache the result of calling C<queries>
 
1767
internally, such code must call this method to flush the cached result.
 
1768
 
 
1769
=item C<queryshare_groups>
 
1770
 
 
1771
An arrayref of group ids. The user can share their own queries with these
 
1772
groups.
 
1773
 
 
1774
=back
 
1775
 
 
1776
=head2 Other Methods
 
1777
 
 
1778
=over
1510
1779
 
1511
1780
=item C<id>
1512
1781
 
1543
1812
before the at sign (@), but that could change, especially if we implement
1544
1813
usernames not dependent on email address.
1545
1814
 
1546
 
=item C<queries>
1547
 
 
1548
 
Returns an array of the user's named queries, sorted in a case-insensitive
1549
 
order by name. Each entry is a hash with three keys:
1550
 
 
1551
 
=over
1552
 
 
1553
 
=item *
1554
 
 
1555
 
name - The name of the query
1556
 
 
1557
 
=item *
1558
 
 
1559
 
query - The text for the query
1560
 
 
1561
 
=item *
1562
 
 
1563
 
linkinfooter - Whether or not the query should be displayed in the footer.
1564
 
 
1565
 
=back
 
1815
=item C<authorizer>
 
1816
 
 
1817
This is the L<Bugzilla::Auth> object that the User logged in with.
 
1818
If the user hasn't logged in yet, a new, empty Bugzilla::Auth() object is
 
1819
returned.
 
1820
 
 
1821
=item C<set_authorizer($authorizer)>
 
1822
 
 
1823
Sets the L<Bugzilla::Auth> object to be returned by C<authorizer()>.
 
1824
Should only be called by C<Bugzilla::Auth::login>, for the most part.
1566
1825
 
1567
1826
=item C<disabledtext>
1568
1827
 
1582
1841
is_default     - a boolean to indicate whether the user has chosen to make
1583
1842
                 a preference for themself or use the site default.
1584
1843
 
1585
 
=item C<flush_queries_cache>
1586
 
 
1587
 
Some code modifies the set of stored queries. Because C<Bugzilla::User> does
1588
 
not handle these modifications, but does cache the result of calling C<queries>
1589
 
internally, such code must call this method to flush the cached result.
1590
 
 
1591
1844
=item C<groups>
1592
1845
 
1593
1846
Returns a hashref of group names for groups the user is a member of. The keys
1597
1850
 
1598
1851
=item C<groups_as_string>
1599
1852
 
1600
 
Returns a string containing a comma-seperated list of numeric group ids.  If
 
1853
Returns a string containing a comma-separated list of numeric group ids.  If
1601
1854
the user is not a member of any groups, returns "-1". This is most often used
1602
1855
within an SQL IN() function.
1603
1856
 
1604
 
=item C<in_group>
 
1857
=item C<in_group($group_name, $product_id)>
1605
1858
 
1606
 
Determines whether or not a user is in the given group by name. 
 
1859
Determines whether or not a user is in the given group by name.
 
1860
If $product_id is given, it also checks for local privileges for
 
1861
this product.
1607
1862
 
1608
1863
=item C<in_group_id>
1609
1864
 
1618
1873
that having editusers permissions means that you can bless all groups, and
1619
1874
that you need to be aware of a group in order to bless a group.
1620
1875
 
 
1876
=item C<get_products_by_permission($group)>
 
1877
 
 
1878
Returns a list of product objects for which the user has $group privileges
 
1879
and which he can access.
 
1880
$group must be one of the groups defined in PER_PRODUCT_PRIVILEGES.
 
1881
 
1621
1882
=item C<can_see_user(user)>
1622
1883
 
1623
1884
Returns 1 if the specified user account exists and is visible to the user,
1692
1953
 
1693
1954
 Returns:     an array of product objects.
1694
1955
 
 
1956
=item C<check_can_admin_product($product_name)>
 
1957
 
 
1958
 Description: Checks whether the user is allowed to administrate the product.
 
1959
 
 
1960
 Params:      $product_name - a product name.
 
1961
 
 
1962
 Returns:     On success, a product object. On failure, an error is thrown.
 
1963
 
 
1964
=item C<can_request_flag($flag_type)>
 
1965
 
 
1966
 Description: Checks whether the user can request flags of the given type.
 
1967
 
 
1968
 Params:      $flag_type - a Bugzilla::FlagType object.
 
1969
 
 
1970
 Returns:     1 if the user can request flags of the given type,
 
1971
              0 otherwise.
 
1972
 
 
1973
=item C<can_set_flag($flag_type)>
 
1974
 
 
1975
 Description: Checks whether the user can set flags of the given type.
 
1976
 
 
1977
 Params:      $flag_type - a Bugzilla::FlagType object.
 
1978
 
 
1979
 Returns:     1 if the user can set flags of the given type,
 
1980
              0 otherwise.
 
1981
 
1695
1982
=item C<get_userlist>
1696
1983
 
1697
1984
Returns a reference to an array of users.  The array is populated with hashrefs
1723
2010
 
1724
2011
=item C<product_responsibilities>
1725
2012
 
1726
 
Retrieve user's product responsibilities as a list of hashes.
1727
 
One hash per Bugzilla component the user has a responsibility for.
1728
 
These are the hash keys:
1729
 
 
1730
 
=over
1731
 
 
1732
 
=item productname
1733
 
 
1734
 
Name of the product.
1735
 
 
1736
 
=item componentname
1737
 
 
1738
 
Name of the component.
1739
 
 
1740
 
=item initialowner
1741
 
 
1742
 
User ID of default assignee.
1743
 
 
1744
 
=item initialqacontact
1745
 
 
1746
 
User ID of default QA contact.
1747
 
 
1748
 
=back
 
2013
Retrieve user's product responsibilities as a list of component objects.
 
2014
Each object is a component the user has a responsibility for.
1749
2015
 
1750
2016
=item C<can_bless>
1751
2017
 
1753
2019
Returns C<1> if the user can bless at least one group, returns C<0> otherwise.
1754
2020
 
1755
2021
When called with one argument:
1756
 
Returns C<1> if the user can bless the group with that name, returns
 
2022
Returns C<1> if the user can bless the group with that id, returns
1757
2023
C<0> otherwise.
1758
2024
 
1759
 
=item C<set_flags>
1760
 
=item C<get_flag>
1761
 
 
1762
 
User flags are template-accessible user status information, stored in the form
1763
 
of a hash.  For an example of use, when the current user is authenticated in
1764
 
such a way that they are allowed to log out, the 'can_logout' flag is set to
1765
 
true (1).  The template then checks this flag before displaying the "Log Out"
1766
 
link.
1767
 
 
1768
 
C<set_flags> is called with any number of key,value pairs.  Flags for each key
1769
 
will be set to the specified value.
1770
 
 
1771
 
C<get_flag> is called with a single key name, which returns the associated
1772
 
value.
1773
 
 
1774
2025
=item C<wants_bug_mail>
1775
2026
 
1776
2027
Returns true if the user wants mail for a given bug change.
1792
2043
Returns true if the user can access private comments and attachments,
1793
2044
i.e. if the 'insidergroup' parameter is set and the user belongs to this group.
1794
2045
 
 
2046
=item C<is_global_watcher>
 
2047
 
 
2048
Returns true if the user is a global watcher,
 
2049
i.e. if the 'globalwatchers' parameter contains the user.
 
2050
 
1795
2051
=back
1796
2052
 
1797
2053
=head1 CLASS FUNCTIONS
1801
2057
 
1802
2058
=over 4
1803
2059
 
1804
 
=item C<insert_new_user>
1805
 
 
1806
 
Creates a new user in the database.
1807
 
 
1808
 
Params: $username (scalar, string) - The login name for the new user.
1809
 
        $realname (scalar, string) - The full name for the new user.
1810
 
        $password (scalar, string) - Optional. The password for the new user;
1811
 
                                     if not given, a random password will be
1812
 
                                     generated.
1813
 
        $disabledtext (scalar, string) - Optional. The disable text for the new
1814
 
                                         user; if not given, it will be empty.
1815
 
                                         If given, the user will be disabled,
1816
 
                                         meaning the account will be
1817
 
                                         unavailable for login.
1818
 
 
1819
 
Returns: The password for this user, in plain text, so it can be included
1820
 
         in an e-mail sent to the user.
 
2060
=item C<create>
 
2061
 
 
2062
The same as L<Bugzilla::Object/create>.
 
2063
 
 
2064
Params: login_name - B<Required> The login name for the new user.
 
2065
        realname - The full name for the new user.
 
2066
        cryptpassword  - B<Required> The password for the new user.
 
2067
            Even though the name says "crypt", you should just specify
 
2068
            a plain-text password. If you specify '*', the user will not
 
2069
            be able to log in using DB authentication.
 
2070
        disabledtext - The disable-text for the new user. If given, the user 
 
2071
            will be disabled, meaning he cannot log in. Defaults to an
 
2072
            empty string.
 
2073
        disable_mail - If 1, bug-related mail will not be  sent to this user; 
 
2074
            if 0, mail will be sent depending on the user's  email preferences.
1821
2075
 
1822
2076
=item C<is_available_username>
1823
2077
 
1832
2086
            can change his username to $username. (That is, this function
1833
2087
            will return a boolean true value).
1834
2088
 
1835
 
=item C<login_to_id($login)>
 
2089
=item C<login_to_id($login, $throw_error)>
1836
2090
 
1837
2091
Takes a login name of a Bugzilla user and changes that into a numeric
1838
2092
ID for that user. This ID can then be passed to Bugzilla::User::new to
1839
2093
create a new user.
1840
2094
 
1841
 
If no valid user exists with that login name, then the function will return 0.
 
2095
If no valid user exists with that login name, then the function returns 0.
 
2096
However, if $throw_error is set, the function will throw a user error
 
2097
instead of returning.
1842
2098
 
1843
2099
This function can also be used when you want to just find out the userid
1844
2100
of a user, but you don't want the full weight of Bugzilla::User.
1846
2102
However, consider using a Bugzilla::User object instead of this function
1847
2103
if you need more information about the user than just their ID.
1848
2104
 
1849
 
=item C<UserInGroup($groupname)>
1850
 
 
1851
 
Takes a name of a group, and returns 1 if a user is in the group, 0 otherwise.
 
2105
=item C<user_id_to_login($user_id)>
 
2106
 
 
2107
Returns the login name of the user account for the given user ID. If no
 
2108
valid user ID is given or the user has no entry in the profiles table,
 
2109
we return an empty string.
 
2110
 
 
2111
=item C<validate_password($passwd1, $passwd2)>
 
2112
 
 
2113
Returns true if a password is valid (i.e. meets Bugzilla's
 
2114
requirements for length and content), else returns false.
 
2115
Untaints C<$passwd1> if successful.
 
2116
 
 
2117
If a second password is passed in, this function also verifies that
 
2118
the two passwords match.
1852
2119
 
1853
2120
=back
1854
2121