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