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 8use strict; 9 10package Bugzilla::Group; 11 12use base qw(Bugzilla::Object); 13 14use Bugzilla::Constants; 15use Bugzilla::Util; 16use Bugzilla::Error; 17use Bugzilla::Config qw(:admin); 18 19############################### 20##### Module Initialization ### 21############################### 22 23use constant DB_COLUMNS => qw( 24 groups.id 25 groups.name 26 groups.description 27 groups.isbuggroup 28 groups.userregexp 29 groups.isactive 30 groups.icon_url 31); 32 33use constant DB_TABLE => 'groups'; 34 35use constant LIST_ORDER => 'isbuggroup, name'; 36 37use constant VALIDATORS => { 38 name => \&_check_name, 39 description => \&_check_description, 40 userregexp => \&_check_user_regexp, 41 isactive => \&_check_is_active, 42 isbuggroup => \&_check_is_bug_group, 43 icon_url => \&_check_icon_url, 44}; 45 46use constant UPDATE_COLUMNS => qw( 47 name 48 description 49 userregexp 50 isactive 51 icon_url 52); 53 54# Parameters that are lists of groups. 55use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup 56 querysharegroup debug_group); 57 58############################### 59#### Accessors ###### 60############################### 61 62sub description { return $_[0]->{'description'}; } 63sub is_bug_group { return $_[0]->{'isbuggroup'}; } 64sub user_regexp { return $_[0]->{'userregexp'}; } 65sub is_active { return $_[0]->{'isactive'}; } 66sub icon_url { return $_[0]->{'icon_url'}; } 67 68sub bugs { 69 my $self = shift; 70 return $self->{bugs} if exists $self->{bugs}; 71 my $bug_ids = Bugzilla->dbh->selectcol_arrayref( 72 'SELECT bug_id FROM bug_group_map WHERE group_id = ?', 73 undef, $self->id); 74 require Bugzilla::Bug; 75 $self->{bugs} = Bugzilla::Bug->new_from_list($bug_ids); 76 return $self->{bugs}; 77} 78 79sub members_direct { 80 my ($self) = @_; 81 $self->{members_direct} ||= $self->_get_members(GRANT_DIRECT); 82 return $self->{members_direct}; 83} 84 85sub members_non_inherited { 86 my ($self) = @_; 87 $self->{members_non_inherited} ||= $self->_get_members(); 88 return $self->{members_non_inherited}; 89} 90 91# A helper for members_direct and members_non_inherited 92sub _get_members { 93 my ($self, $grant_type) = @_; 94 my $dbh = Bugzilla->dbh; 95 my $grant_clause = $grant_type ? "AND grant_type = $grant_type" : ""; 96 my $user_ids = $dbh->selectcol_arrayref( 97 "SELECT DISTINCT user_id 98 FROM user_group_map 99 WHERE isbless = 0 $grant_clause AND group_id = ?", undef, $self->id); 100 require Bugzilla::User; 101 return Bugzilla::User->new_from_list($user_ids); 102} 103 104sub flag_types { 105 my $self = shift; 106 require Bugzilla::FlagType; 107 $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id }); 108 return $self->{flag_types}; 109} 110 111sub grant_direct { 112 my ($self, $type) = @_; 113 $self->{grant_direct} ||= {}; 114 return $self->{grant_direct}->{$type} 115 if defined $self->{grant_direct}->{$type}; 116 my $dbh = Bugzilla->dbh; 117 118 my $ids = $dbh->selectcol_arrayref( 119 "SELECT member_id FROM group_group_map 120 WHERE grantor_id = ? AND grant_type = $type", 121 undef, $self->id) || []; 122 123 $self->{grant_direct}->{$type} = $self->new_from_list($ids); 124 return $self->{grant_direct}->{$type}; 125} 126 127sub granted_by_direct { 128 my ($self, $type) = @_; 129 $self->{granted_by_direct} ||= {}; 130 return $self->{granted_by_direct}->{$type} 131 if defined $self->{granted_by_direct}->{$type}; 132 my $dbh = Bugzilla->dbh; 133 134 my $ids = $dbh->selectcol_arrayref( 135 "SELECT grantor_id FROM group_group_map 136 WHERE member_id = ? AND grant_type = $type", 137 undef, $self->id) || []; 138 139 $self->{granted_by_direct}->{$type} = $self->new_from_list($ids); 140 return $self->{granted_by_direct}->{$type}; 141} 142 143sub products { 144 my $self = shift; 145 return $self->{products} if exists $self->{products}; 146 my $product_data = Bugzilla->dbh->selectall_arrayref( 147 'SELECT product_id, entry, membercontrol, othercontrol, 148 canedit, editcomponents, editbugs, canconfirm 149 FROM group_control_map WHERE group_id = ?', {Slice=>{}}, 150 $self->id); 151 my @ids = map { $_->{product_id} } @$product_data; 152 require Bugzilla::Product; 153 my $products = Bugzilla::Product->new_from_list(\@ids); 154 my %data_map = map { $_->{product_id} => $_ } @$product_data; 155 my @retval; 156 foreach my $product (@$products) { 157 # Data doesn't need to contain product_id--we already have 158 # the product object. 159 delete $data_map{$product->id}->{product_id}; 160 push(@retval, { controls => $data_map{$product->id}, 161 product => $product }); 162 } 163 $self->{products} = \@retval; 164 return $self->{products}; 165} 166 167############################### 168#### Methods #### 169############################### 170 171sub check_members_are_visible { 172 my $self = shift; 173 my $user = Bugzilla->user; 174 return if !Bugzilla->params->{'usevisibilitygroups'}; 175 176 my $group_id = $self->id; 177 my $is_visible = grep { $_ == $group_id } @{ $user->visible_groups_inherited }; 178 if (!$is_visible) { 179 ThrowUserError('group_not_visible', { group => $self }); 180 } 181} 182 183sub set_description { $_[0]->set('description', $_[1]); } 184sub set_is_active { $_[0]->set('isactive', $_[1]); } 185sub set_name { $_[0]->set('name', $_[1]); } 186sub set_user_regexp { $_[0]->set('userregexp', $_[1]); } 187sub set_icon_url { $_[0]->set('icon_url', $_[1]); } 188 189sub update { 190 my $self = shift; 191 my $dbh = Bugzilla->dbh; 192 $dbh->bz_start_transaction(); 193 my $changes = $self->SUPER::update(@_); 194 195 if (exists $changes->{name}) { 196 my ($old_name, $new_name) = @{$changes->{name}}; 197 my $update_params; 198 foreach my $group (GROUP_PARAMS) { 199 if ($old_name eq Bugzilla->params->{$group}) { 200 SetParam($group, $new_name); 201 $update_params = 1; 202 } 203 } 204 write_params() if $update_params; 205 } 206 207 # If we've changed this group to be active, fix any Mandatory groups. 208 $self->_enforce_mandatory if (exists $changes->{isactive} 209 && $changes->{isactive}->[1]); 210 211 $self->_rederive_regexp() if exists $changes->{userregexp}; 212 213 Bugzilla::Hook::process('group_end_of_update', 214 { group => $self, changes => $changes }); 215 $dbh->bz_commit_transaction(); 216 return $changes; 217} 218 219sub check_remove { 220 my ($self, $params) = @_; 221 222 # System groups cannot be deleted! 223 if (!$self->is_bug_group) { 224 ThrowUserError("system_group_not_deletable", { name => $self->name }); 225 } 226 227 # Groups having a special role cannot be deleted. 228 my @special_groups; 229 foreach my $special_group (GROUP_PARAMS) { 230 if ($self->name eq Bugzilla->params->{$special_group}) { 231 push(@special_groups, $special_group); 232 } 233 } 234 if (scalar(@special_groups)) { 235 ThrowUserError('group_has_special_role', 236 { name => $self->name, 237 groups => \@special_groups }); 238 } 239 240 return if $params->{'test_only'}; 241 242 my $cantdelete = 0; 243 244 my $users = $self->members_non_inherited; 245 if (scalar(@$users) && !$params->{'remove_from_users'}) { 246 $cantdelete = 1; 247 } 248 249 my $bugs = $self->bugs; 250 if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) { 251 $cantdelete = 1; 252 } 253 254 my $products = $self->products; 255 if (scalar(@$products) && !$params->{'remove_from_products'}) { 256 $cantdelete = 1; 257 } 258 259 my $flag_types = $self->flag_types; 260 if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { 261 $cantdelete = 1; 262 } 263 264 ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete; 265} 266 267sub remove_from_db { 268 my $self = shift; 269 my $dbh = Bugzilla->dbh; 270 $self->check_remove(@_); 271 $dbh->bz_start_transaction(); 272 Bugzilla::Hook::process('group_before_delete', { group => $self }); 273 $dbh->do('DELETE FROM whine_schedules 274 WHERE mailto_type = ? AND mailto = ?', 275 undef, MAILTO_GROUP, $self->id); 276 # All the other tables will be handled by foreign keys when we 277 # drop the main "groups" row. 278 $self->SUPER::remove_from_db(@_); 279 $dbh->bz_commit_transaction(); 280} 281 282# Add missing entries in bug_group_map for bugs created while 283# a mandatory group was disabled and which is now enabled again. 284sub _enforce_mandatory { 285 my ($self) = @_; 286 my $dbh = Bugzilla->dbh; 287 my $gid = $self->id; 288 289 my $bug_ids = 290 $dbh->selectcol_arrayref('SELECT bugs.bug_id 291 FROM bugs 292 INNER JOIN group_control_map 293 ON group_control_map.product_id = bugs.product_id 294 LEFT JOIN bug_group_map 295 ON bug_group_map.bug_id = bugs.bug_id 296 AND bug_group_map.group_id = group_control_map.group_id 297 WHERE group_control_map.group_id = ? 298 AND group_control_map.membercontrol = ? 299 AND bug_group_map.group_id IS NULL', 300 undef, ($gid, CONTROLMAPMANDATORY)); 301 302 my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) VALUES (?, ?)'); 303 foreach my $bug_id (@$bug_ids) { 304 $sth->execute($bug_id, $gid); 305 } 306} 307 308sub is_active_bug_group { 309 my $self = shift; 310 return $self->is_active && $self->is_bug_group; 311} 312 313sub _rederive_regexp { 314 my ($self) = @_; 315 316 my $dbh = Bugzilla->dbh; 317 my $sth = $dbh->prepare("SELECT userid, login_name, group_id 318 FROM profiles 319 LEFT JOIN user_group_map 320 ON user_group_map.user_id = profiles.userid 321 AND group_id = ? 322 AND grant_type = ? 323 AND isbless = 0"); 324 my $sthadd = $dbh->prepare("INSERT INTO user_group_map 325 (user_id, group_id, grant_type, isbless) 326 VALUES (?, ?, ?, 0)"); 327 my $sthdel = $dbh->prepare("DELETE FROM user_group_map 328 WHERE user_id = ? AND group_id = ? 329 AND grant_type = ? and isbless = 0"); 330 $sth->execute($self->id, GRANT_REGEXP); 331 my $regexp = $self->user_regexp; 332 while (my ($uid, $login, $present) = $sth->fetchrow_array) { 333 if ($regexp ne '' and $login =~ /$regexp/i) { 334 $sthadd->execute($uid, $self->id, GRANT_REGEXP) unless $present; 335 } else { 336 $sthdel->execute($uid, $self->id, GRANT_REGEXP) if $present; 337 } 338 } 339} 340 341sub flatten_group_membership { 342 my ($self, @groups) = @_; 343 344 my $dbh = Bugzilla->dbh; 345 my $sth; 346 my @groupidstocheck = @groups; 347 my %groupidschecked = (); 348 $sth = $dbh->prepare("SELECT member_id FROM group_group_map 349 WHERE grantor_id = ? 350 AND grant_type = " . GROUP_MEMBERSHIP); 351 while (my $node = shift @groupidstocheck) { 352 $sth->execute($node); 353 my $member; 354 while (($member) = $sth->fetchrow_array) { 355 if (!$groupidschecked{$member}) { 356 $groupidschecked{$member} = 1; 357 push @groupidstocheck, $member; 358 push @groups, $member unless grep $_ == $member, @groups; 359 } 360 } 361 } 362 return \@groups; 363} 364 365 366 367 368################################ 369##### Module Subroutines ### 370################################ 371 372sub create { 373 my $class = shift; 374 my ($params) = @_; 375 my $dbh = Bugzilla->dbh; 376 377 my $silently = delete $params->{silently}; 378 if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE and !$silently) { 379 print get_text('install_group_create', { name => $params->{name} }), 380 "\n"; 381 } 382 383 $dbh->bz_start_transaction(); 384 385 my $group = $class->SUPER::create(@_); 386 387 # Since we created a new group, give the "admin" group all privileges 388 # initially. 389 my $admin = new Bugzilla::Group({name => 'admin'}); 390 # This function is also used to create the "admin" group itself, 391 # so there's a chance it won't exist yet. 392 if ($admin) { 393 my $sth = $dbh->prepare('INSERT INTO group_group_map 394 (member_id, grantor_id, grant_type) 395 VALUES (?, ?, ?)'); 396 $sth->execute($admin->id, $group->id, GROUP_MEMBERSHIP); 397 $sth->execute($admin->id, $group->id, GROUP_BLESS); 398 $sth->execute($admin->id, $group->id, GROUP_VISIBLE); 399 } 400 401 $group->_rederive_regexp() if $group->user_regexp; 402 403 Bugzilla::Hook::process('group_end_of_create', { group => $group }); 404 $dbh->bz_commit_transaction(); 405 return $group; 406} 407 408sub ValidateGroupName { 409 my ($name, @users) = (@_); 410 my $dbh = Bugzilla->dbh; 411 my $query = "SELECT id FROM groups " . 412 "WHERE name = ?"; 413 if (Bugzilla->params->{'usevisibilitygroups'}) { 414 my @visible = (-1); 415 foreach my $user (@users) { 416 $user && push @visible, @{$user->visible_groups_direct}; 417 } 418 my $visible = join(', ', @visible); 419 $query .= " AND id IN($visible)"; 420 } 421 my $sth = $dbh->prepare($query); 422 $sth->execute($name); 423 my ($ret) = $sth->fetchrow_array(); 424 return $ret; 425} 426 427sub check_no_disclose { 428 my ($class, $params) = @_; 429 my $action = delete $params->{action}; 430 431 $action =~ /^(?:add|remove)$/ 432 or ThrowCodeError('bad_arg', { argument => $action, 433 function => "${class}::check_no_disclose" }); 434 435 $params->{_error} = ($action eq 'add') ? 'group_restriction_not_allowed' 436 : 'group_invalid_removal'; 437 438 my $group = $class->check($params); 439 return $group; 440} 441 442############################### 443### Validators ### 444############################### 445 446sub _check_name { 447 my ($invocant, $name) = @_; 448 $name = trim($name); 449 $name || ThrowUserError("empty_group_name"); 450 # If we're creating a Group or changing the name... 451 if (!ref($invocant) || lc($invocant->name) ne lc($name)) { 452 my $exists = new Bugzilla::Group({name => $name }); 453 ThrowUserError("group_exists", { name => $name }) if $exists; 454 } 455 return $name; 456} 457 458sub _check_description { 459 my ($invocant, $desc) = @_; 460 $desc = trim($desc); 461 $desc || ThrowUserError("empty_group_description"); 462 return $desc; 463} 464 465sub _check_user_regexp { 466 my ($invocant, $regex) = @_; 467 $regex = trim($regex) || ''; 468 ThrowUserError("invalid_regexp") unless (eval {qr/$regex/}); 469 return $regex; 470} 471 472sub _check_is_active { return $_[1] ? 1 : 0; } 473sub _check_is_bug_group { 474 return $_[1] ? 1 : 0; 475} 476 477sub _check_icon_url { return $_[1] ? clean_text($_[1]) : undef; } 478 4791; 480 481__END__ 482 483=head1 NAME 484 485Bugzilla::Group - Bugzilla group class. 486 487=head1 SYNOPSIS 488 489 use Bugzilla::Group; 490 491 my $group = new Bugzilla::Group(1); 492 my $group = new Bugzilla::Group({name => 'AcmeGroup'}); 493 494 my $id = $group->id; 495 my $name = $group->name; 496 my $description = $group->description; 497 my $user_reg_exp = $group->user_reg_exp; 498 my $is_active = $group->is_active; 499 my $icon_url = $group->icon_url; 500 my $is_active_bug_group = $group->is_active_bug_group; 501 502 my $group_id = Bugzilla::Group::ValidateGroupName('admin', @users); 503 my @groups = Bugzilla::Group->get_all; 504 505=head1 DESCRIPTION 506 507Group.pm represents a Bugzilla Group object. It is an implementation 508of L<Bugzilla::Object>, and thus has all the methods that L<Bugzilla::Object> 509provides, in addition to any methods documented below. 510 511=head1 SUBROUTINES 512 513=over 514 515=item C<create> 516 517Note that in addition to what L<Bugzilla::Object/create($params)> 518normally does, this function also makes the new group be inherited 519by the C<admin> group. That is, the C<admin> group will automatically 520be a member of this group. 521 522=item C<ValidateGroupName($name, @users)> 523 524Description: ValidateGroupName checks to see if ANY of the users 525 in the provided list of user objects can see the 526 named group. 527 528Params: $name - String with the group name. 529 @users - An array with Bugzilla::User objects. 530 531Returns: It returns the group id if successful 532 and undef otherwise. 533 534=back 535 536 537=head1 METHODS 538 539=over 540 541=item C<check_no_disclose> 542 543=over 544 545=item B<Description> 546 547Throws an error if the user cannot add or remove this group to/from a given 548bug, but doesn't specify if this is because the group doesn't exist, or the 549user is not allowed to edit this group restriction. 550 551=item B<Params> 552 553This method takes a single hashref as argument, with the following keys: 554 555=over 556 557=item C<name> 558 559C<string> The name of the group to add or remove. 560 561=item C<bug_id> 562 563C<integer> The ID of the bug to which the group change applies. 564 565=item C<product> 566 567C<string> The name of the product the bug belongs to. 568 569=item C<action> 570 571C<string> Must be either C<add> or C<remove>, depending on whether the group 572must be added or removed from the bug. Any other value will generate an error. 573 574=back 575 576=item C<Returns> 577 578A C<Bugzilla::Group> object on success, else an error is thrown. 579 580=back 581 582=item C<check_members_are_visible> 583 584Throws an error if this group is not visible (according to 585visibility groups) to the currently-logged-in user. 586 587=item C<check_remove> 588 589=over 590 591=item B<Description> 592 593Determines whether it's OK to remove this group from the database, and 594throws an error if it's not OK. 595 596=item B<Params> 597 598=over 599 600=item C<test_only> 601 602C<boolean> If you want to only check if the group can be deleted I<at all>, 603under any circumstances, specify C<test_only> to just do the most basic tests 604(the other parameters will be ignored in this situation, as those tests won't 605be run). 606 607=item C<remove_from_users> 608 609C<boolean> True if it would be OK to remove all users who are in this group 610from this group. 611 612=item C<remove_from_bugs> 613 614C<boolean> True if it would be OK to remove all bugs that are in this group 615from this group. 616 617=item C<remove_from_flags> 618 619C<boolean> True if it would be OK to stop all flagtypes that reference 620this group from referencing this group (e.g., as their grantgroup or 621requestgroup). 622 623=item C<remove_from_products> 624 625C<boolean> True if it would be OK to remove this group from all group controls 626on products. 627 628=back 629 630=item B<Returns> (nothing) 631 632=back 633 634=item C<members_non_inherited> 635 636Returns an arrayref of L<Bugzilla::User> objects representing people who are 637"directly" in this group, meaning that they're in it because they match 638the group regular expression, or they have been actually added to the 639group manually. 640 641=item C<flatten_group_membership> 642 643Accepts a list of groups and returns a list of all the groups whose members 644inherit membership in any group on the list. So, we can determine if a user 645is in any of the groups input to flatten_group_membership by querying the 646user_group_map for any user with DIRECT or REGEXP membership IN() the list 647of groups returned. 648 649=back 650