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