65
67
use constant MATCH_SKIP_CONFIRM => 1;
69
use constant DEFAULT_USER => {
73
'showmybugslink' => 0,
78
use constant DB_TABLE => 'profiles';
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
84
use constant DB_COLUMNS => (
85
'profiles.userid AS id',
86
'profiles.login_name',
88
'profiles.mybugslink AS showmybugslink',
89
'profiles.disabledtext',
90
'profiles.disable_mail',
92
use constant NAME_FIELD => 'login_name';
93
use constant ID_FIELD => 'userid';
94
use constant LIST_ORDER => NAME_FIELD;
96
use constant REQUIRED_CREATE_FIELDS => qw(login_name cryptpassword);
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,
114
push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
67
118
################################################################################
69
120
################################################################################
72
123
my $invocant = shift;
77
detaint_natural($user_id)
78
|| ThrowCodeError('invalid_numeric_argument',
79
{argument => 'userID',
81
function => 'Bugzilla::User::new'});
82
return $invocant->_create("userid=?", $user_id);
85
return $invocant->_create;
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)
101
my $dbh = Bugzilla->dbh;
102
return $invocant->_create($dbh->sql_istrcmp('login_name', '?'), $login);
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
109
my $invocant = shift;
110
124
my $class = ref($invocant) || $invocant;
115
# Allow invocation with no parameters to create a blank object
120
'showmybugslink' => 0,
121
'disabledtext' => '',
124
bless ($self, $class);
125
return $self unless $cond && $val;
127
# We're checking for validity here, so any value is OK
127
my $user = DEFAULT_USER;
128
bless ($user, $class);
129
return $user unless $param;
131
return $class->SUPER::new(@_);
136
my $changes = $self->SUPER::update(@_);
130
137
my $dbh = Bugzilla->dbh;
136
$mybugslink) = $dbh->selectrow_array(qq{SELECT userid,
146
return undef unless defined $id;
149
$self->{'name'} = $name;
150
$self->{'login'} = $login;
151
$self->{'disabledtext'} = $disabledtext;
152
$self->{'showmybugslink'} = $mybugslink;
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();
146
# Logout the user if necessary.
147
Bugzilla->logout_user($self)
148
if (exists $changes->{login_name} || exists $changes->{disabledtext}
149
|| exists $changes->{cryptpassword});
151
# XXX Can update profiles_activity here as soon as it understands
152
# field names like login_name.
157
################################################################################
159
################################################################################
161
sub _check_disable_mail { return $_[1] ? 1 : 0; }
162
sub _check_disabledtext { return trim($_[1]) || ''; }
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) = @_;
169
$name || ThrowUserError('user_login_required');
170
validate_email_syntax($name)
171
|| ThrowUserError('illegal_email_address', { addr => $name });
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 });
182
sub _check_password {
183
my ($self, $pass) = @_;
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
188
return $pass if $pass eq '*';
190
validate_password($pass);
191
my $cryptpassword = bz_crypt($pass);
192
return $cryptpassword;
195
sub _check_realname { return trim($_[1]) || ''; }
197
################################################################################
199
################################################################################
201
sub set_disabledtext { $_[0]->set('disabledtext', $_[1]); }
202
sub set_disable_mail { $_[0]->set('disable_mail', $_[1]); }
205
my ($self, $login) = @_;
206
$self->set('login_name', $login);
207
delete $self->{identity};
208
delete $self->{nick};
212
my ($self, $name) = @_;
213
$self->set('realname', $name);
214
delete $self->{identity};
217
sub set_password { $_[0]->set('cryptpassword', $_[1]); }
220
################################################################################
222
################################################################################
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}); }
168
while (my $key = shift) {
169
$self->{'flags'}->{$key} = shift;
235
my ($self, $authorizer) = @_;
236
$self->{authorizer} = $authorizer;
240
if (!$self->{authorizer}) {
241
require Bugzilla::Auth;
242
$self->{authorizer} = new Bugzilla::Auth();
176
return $self->{'flags'}->{$key};
244
return $self->{authorizer};
179
247
# Generate a string to identify the user by name + login if the user
207
275
my $self = shift;
209
276
return $self->{queries} if defined $self->{queries};
210
277
return [] unless $self->id;
212
279
my $dbh = Bugzilla->dbh;
213
my $used_in_whine_ref = $dbh->selectcol_arrayref(q{
214
SELECT DISTINCT query_name
216
INNER JOIN whine_queries wq
217
ON we.id = wq.eventid
218
WHERE we.owner_userid = ?}, undef, $self->{id});
220
my $queries_ref = $dbh->selectall_arrayref(q{
221
SELECT name, query, linkinfooter, query_type
224
ORDER BY UPPER(name)},{'Slice'=>{}}, $self->{id});
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;
234
$self->{queries} = $queries_ref;
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};
287
sub queries_subscribed {
289
return $self->{queries_subscribed} if defined $self->{queries_subscribed};
290
return [] unless $self->id;
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';
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 . ")",
308
require Bugzilla::Search::Saved;
309
$self->{queries_subscribed} =
310
Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
311
return $self->{queries_subscribed};
314
sub queries_available {
316
return $self->{queries_available} if defined $self->{queries_available};
317
return [] unless $self->id;
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';
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};
277
373
# The above gives us an arrayref [name, id, name, id, ...]
278
374
# Convert that into a hashref
279
375
my %groups = @$groups;
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
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;
391
# And group membership
392
push (@{$group_membership{$member_id}}, $grantor_id);
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;
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;
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});
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;
300
423
$self->{groups} = \%groups;
459
624
sub get_selectable_products {
460
625
my $self = shift;
461
my $classification_id = shift;
463
if (defined $self->{selectable_products}) {
464
return $self->{selectable_products};
467
my $dbh = Bugzilla->dbh;
470
my $query = "SELECT id " .
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 ";
477
$query .= "AND group_control_map.membercontrol = " .
478
CONTROLMAPMANDATORY . " ";
480
$query .= "AND group_id NOT IN(" .
481
$self->groups_as_string . ") " .
482
"WHERE group_id IS NULL ";
484
if (Param('useclassification') && $classification_id) {
485
$query .= "AND classification_id = ? ";
486
detaint_natural($classification_id);
487
push(@params, $classification_id);
490
$query .= "ORDER BY name";
492
my $prod_ids = $dbh->selectcol_arrayref($query, undef, @params);
494
foreach my $prod_id (@$prod_ids) {
495
push(@products, new Bugzilla::Product($prod_id));
497
$self->{selectable_products} = \@products;
626
my $class_id = shift;
627
my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
629
if (!defined $self->{selectable_products}) {
630
my $query = "SELECT id " .
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 ";
637
$query .= " AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY;
639
$query .= " AND group_id NOT IN(" . $self->groups_as_string . ") " .
640
" WHERE group_id IS NULL " .
643
my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
644
$self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
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}}];
651
# If we come here, then we want all selectable products.
498
652
return $self->{selectable_products};
522
677
my $dbh = Bugzilla->dbh;
524
679
if (!defined($product_name)) {
680
return unless $warn == THROW_ERROR;
526
681
ThrowUserError('no_products');
528
683
trick_taint($product_name);
530
# Checks whether the user has access to the product.
531
my $has_access = $dbh->selectrow_array('SELECT CASE WHEN group_id IS NULL
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 = ? ' .
540
undef, $product_name);
544
ThrowUserError('entry_access_denied', { product => $product_name });
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
552
CASE WHEN versions.value IS NOT NULL
555
INNER JOIN components
556
ON components.product_id = products.id
558
ON versions.product_id = products.id
559
WHERE products.name = ? ' .
560
$dbh->sql_limit(1), undef, $product_name);
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;
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 });
685
grep($_->name eq $product_name, @{$self->get_enterable_products});
687
return 1 if $can_enter;
689
return 0 unless $warn == THROW_ERROR;
691
# Check why access was denied. These checks are slow,
692
# but that's fine, because they only happen if we fail.
694
my $product = new Bugzilla::Product({name => $product_name});
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});
700
# It could be closed for bug entry...
701
elsif ($product->disallow_new) {
702
ThrowUserError('product_disabled', {product => $product});
704
# It could have no components...
705
elsif (!@{$product->components}) {
706
ThrowUserError('missing_component', {product => $product});
708
# It could have no versions...
709
elsif (!@{$product->versions}) {
710
ThrowUserError ('missing_version', {product => $product});
713
die "can_enter_product reached an unreachable location.";
581
716
sub get_enterable_products {
582
717
my $self = shift;
718
my $dbh = Bugzilla->dbh;
584
720
if (defined $self->{enterable_products}) {
585
721
return $self->{enterable_products};
589
foreach my $product (Bugzilla::Product::get_all_products()) {
590
if ($self->can_enter_product($product->name)) {
591
push(@products, $product);
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') || []};
734
if (@enterable_ids) {
735
# And all of these products must have at least one component
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)) .
594
$self->{enterable_products} = \@products;
745
$self->{enterable_products} =
746
Bugzilla::Product->new_from_list(\@enterable_ids);
595
747
return $self->{enterable_products};
750
sub get_accessible_products {
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};
758
return [ values %products ];
761
sub check_can_admin_product {
762
my ($self, $product_name) = @_;
764
# First make sure the product name is valid.
765
my $product = Bugzilla::Product::check_product($product_name);
767
($self->in_group('editcomponents', $product->id)
768
&& $self->can_see_product($product->name))
769
|| ThrowUserError('product_admin_denied', {product => $product->name});
771
# Return the validated product object.
775
sub can_request_flag {
776
my ($self, $flag_type) = @_;
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;
784
my ($self, $flag_type) = @_;
786
return (!$flag_type->grant_group
787
|| $self->in_group_id($flag_type->grant_group->id)) ? 1 : 0;
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 {
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;
674
903
$group_delete->execute($id, $group, GRANT_REGEXP) if $present;
678
$dbh->do(q{UPDATE profiles
679
SET refreshed_when = ?
686
908
sub product_responsibilities {
687
909
my $self = shift;
910
my $dbh = Bugzilla->dbh;
689
912
return $self->{'product_resp'} if defined $self->{'product_resp'};
690
913
return [] unless $self->id;
692
my $h = Bugzilla->dbh->selectall_arrayref(
693
qq{SELECT products.name AS productname,
694
components.name AS componentname,
697
FROM products, components
698
WHERE products.id = components.product_id
699
AND ? IN (initialowner, initialqacontact)
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));
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'};
762
982
# ones following it will not execute.
764
984
# first try wildcards
766
985
my $wildstr = $str;
767
my $user = Bugzilla->user;
768
my $dbh = Bugzilla->dbh;
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')
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 " .
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 ";
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 " .
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})) .
1005
join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
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;
795
1011
# Execute the query, retrieve the results, and make them into
798
&::PushGlobalSQLState();
800
push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
801
&::PopGlobalSQLState();
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 }));
804
1018
else { # try an exact match
806
my $sqlstr = &::SqlQuote($str);
807
my $query = "SELECT userid, realname, login_name " .
809
"WHERE " . $dbh->sql_istrcmp('login_name', $sqlstr);
810
1019
# Exact matches don't care if a user is disabled.
1021
my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles
1022
WHERE ' . $dbh->sql_istrcmp('login_name', '?'),
812
&::PushGlobalSQLState();
814
push(@users, new Bugzilla::User(&::FetchSQLData())) if &::MoreSQLData();
815
&::PopGlobalSQLState();
1025
push(@users, new Bugzilla::User($user_id)) if $user_id;
818
1028
# then try substring search
820
1029
if ((scalar(@users) == 0)
821
&& (&::Param('usermatchmode') eq 'search')
1030
&& (Bugzilla->params->{'usermatchmode'} eq 'search')
822
1031
&& (length($str) >= 3))
825
my $sqlstr = &::SqlQuote(lc($str));
827
my $query = "SELECT DISTINCT userid, realname, login_name, " .
828
"LENGTH(login_name) AS namelength " .
830
if (&::Param('usevisibilitygroups')) {
831
$query .= ", user_group_map";
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 ";
833
1041
$query .= " WHERE (" .
834
$dbh->sql_position($sqlstr, 'LOWER(login_name)') . " > 0" .
836
$dbh->sql_position($sqlstr, 'LOWER(realname)') . " > 0)";
837
if (&::Param('usevisibilitygroups')) {
838
$query .= " AND user_group_map.user_id = userid" .
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})) . ")";
843
$query .= " AND disabledtext = ''" if $exclude_disabled;
844
$query .= " ORDER BY namelength";
845
$query .= " " . $dbh->sql_limit($limit) if $limit;
846
&::PushGlobalSQLState();
848
push(@users, new Bugzilla::User(&::FetchSQLData())) while &::MoreSQLData();
849
&::PopGlobalSQLState();
1047
join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
1049
$query .= " AND disabledtext = '' " if $exclude_disabled;
1050
$query .= " ORDER BY login_name ";
1051
$query .= $dbh->sql_limit($limit) if $limit;
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 }));
852
# order @users by alpha
854
@users = sort { uc($a->login) cmp uc($b->login) } @users;
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);
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) = @_;
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'};
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.
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;
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;
1155
1363
# If the user is in a particular role and the value of that role
1318
1540
return $self->{'userlist'};
1321
sub insert_new_user {
1322
my ($username, $realname, $password, $disabledtext) = (@_);
1544
my $invocant = shift;
1545
my $class = ref($invocant) || $invocant;
1323
1546
my $dbh = Bugzilla->dbh;
1325
$disabledtext ||= '';
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;
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);
1338
# Insert the new user record into the database.
1339
$dbh->do("INSERT INTO profiles
1340
(login_name, realname, cryptpassword, disabledtext,
1342
VALUES (?, ?, ?, ?, '1901-01-01 00:00:00')",
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');
1552
my $user = $class->SUPER::create(@_);
1346
1554
# Turn on all email for the new user
1347
my $userid = $dbh->bz_last_key('profiles', 'userid');
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));
1358
$dbh->do("INSERT INTO email_setting " .
1359
"(user_id, relationship, event) " .
1360
"VALUES ($userid, $rel, $event)");
1564
$dbh->do('INSERT INTO email_setting (user_id, relationship, event)
1565
VALUES (?, ?, ?)', undef, ($user->id, $rel, $event));
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));
1370
my $user = new Bugzilla::User($userid);
1371
1574
$user->derive_regexp_groups();
1374
# Return the password to the calling code so it can be included
1375
# in an email sent to the user.
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');
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));
1587
$dbh->bz_unlock_tables();
1589
# Return the newly created user account.
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(
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));
1404
if (my ($eventdata) = $sth->fetchrow_array()) {
1405
1622
# Allow thru owner of token
1406
1623
if($old_username && ($eventdata eq "$old_username:$username")) {
1415
1632
sub login_to_id {
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 });
1431
return exists Bugzilla->user->groups->{$_[0]} ? 1 : 0;
1650
sub user_id_to_login {
1651
my $user_id = shift;
1652
my $dbh = Bugzilla->dbh;
1654
return '' unless ($user_id && detaint_natural($user_id));
1656
my $login = $dbh->selectrow_array('SELECT login_name FROM profiles
1657
WHERE userid = ?', undef, $user_id);
1658
return $login || '';
1661
sub validate_password {
1662
my ($password, $matchpassword) = @_;
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');
1671
# Having done these checks makes us consider the password untainted.
1492
=item C<new($userid)>
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.
1497
If an id was given but there was no matching user found, undef is returned.
1501
=item C<new_from_login($login)>
1503
Creates a new C<Bugzilla::User> object given the provided login. Returns
1504
C<undef> if no matching user is found.
1506
This routine should not be required in general; most scripts should be using
1742
=head2 Saved and Shared Queries
1748
Returns an arrayref of the user's own saved queries, sorted by name. The
1749
array contains L<Bugzilla::Search::Saved> objects.
1751
=item C<queries_subscribed>
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.
1757
=item C<queries_available>
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.
1763
=item C<flush_queries_cache>
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.
1769
=item C<queryshare_groups>
1771
An arrayref of group ids. The user can share their own queries with these
1776
=head2 Other Methods
1804
=item C<insert_new_user>
1806
Creates a new user in the database.
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
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.
1819
Returns: The password for this user, in plain text, so it can be included
1820
in an e-mail sent to the user.
2062
The same as L<Bugzilla::Object/create>.
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
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.
1822
2076
=item C<is_available_username>