1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4# 5# This Source Code Form is "Incompatible With Secondary Licenses", as 6# defined by the Mozilla Public License, v. 2.0. 7 8package Bugzilla::Product; 9use strict; 10use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object); 11 12use Bugzilla::Constants; 13use Bugzilla::Util; 14use Bugzilla::Error; 15use Bugzilla::Group; 16use Bugzilla::Version; 17use Bugzilla::Milestone; 18use Bugzilla::Field; 19use Bugzilla::Status; 20use Bugzilla::Install::Requirements; 21use Bugzilla::Mailer; 22use Bugzilla::Series; 23use Bugzilla::Hook; 24use Bugzilla::FlagType; 25 26use Scalar::Util qw(blessed); 27 28use constant DEFAULT_CLASSIFICATION_ID => 1; 29 30############################### 31#### Initialization #### 32############################### 33 34use constant DB_TABLE => 'products'; 35 36use constant DB_COLUMNS => qw( 37 id 38 name 39 classification_id 40 description 41 isactive 42 defaultmilestone 43 allows_unconfirmed 44); 45 46use constant UPDATE_COLUMNS => qw( 47 name 48 description 49 defaultmilestone 50 isactive 51 allows_unconfirmed 52); 53 54use constant VALIDATORS => { 55 allows_unconfirmed => \&Bugzilla::Object::check_boolean, 56 classification => \&_check_classification, 57 name => \&_check_name, 58 description => \&_check_description, 59 version => \&_check_version, 60 defaultmilestone => \&_check_default_milestone, 61 isactive => \&Bugzilla::Object::check_boolean, 62 create_series => \&Bugzilla::Object::check_boolean 63}; 64 65############################### 66#### Constructors ##### 67############################### 68 69sub create { 70 my $class = shift; 71 my $dbh = Bugzilla->dbh; 72 73 $dbh->bz_start_transaction(); 74 75 $class->check_required_create_fields(@_); 76 77 my $params = $class->run_create_validators(@_); 78 # Some fields do not exist in the DB as is. 79 if (defined $params->{classification}) { 80 $params->{classification_id} = delete $params->{classification}; 81 } 82 my $version = delete $params->{version}; 83 my $create_series = delete $params->{create_series}; 84 85 my $product = $class->insert_create_data($params); 86 Bugzilla->user->clear_product_cache(); 87 88 # Add the new version and milestone into the DB as valid values. 89 Bugzilla::Version->create({ value => $version, product => $product }); 90 Bugzilla::Milestone->create({ value => $product->default_milestone, 91 product => $product }); 92 93 # Create groups and series for the new product, if requested. 94 $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'}; 95 $product->_create_series() if $create_series; 96 97 Bugzilla::Hook::process('product_end_of_create', { product => $product }); 98 99 $dbh->bz_commit_transaction(); 100 return $product; 101} 102 103# This is considerably faster than calling new_from_list three times 104# for each product in the list, particularly with hundreds or thousands 105# of products. 106sub preload { 107 my ($products, $preload_flagtypes) = @_; 108 my %prods = map { $_->id => $_ } @$products; 109 my @prod_ids = keys %prods; 110 return unless @prod_ids; 111 112 # We cannot |use| it due to a dependency loop with Bugzilla::User. 113 require Bugzilla::Component; 114 foreach my $field (qw(component version milestone)) { 115 my $classname = "Bugzilla::" . ucfirst($field); 116 my $objects = $classname->match({ product_id => \@prod_ids }); 117 118 # Now populate the products with this set of objects. 119 foreach my $obj (@$objects) { 120 my $product_id = $obj->product_id; 121 $prods{$product_id}->{"${field}s"} ||= []; 122 push(@{$prods{$product_id}->{"${field}s"}}, $obj); 123 } 124 } 125 if ($preload_flagtypes) { 126 $_->flag_types foreach @$products; 127 } 128} 129 130sub update { 131 my $self = shift; 132 my $dbh = Bugzilla->dbh; 133 134 # Don't update the DB if something goes wrong below -> transaction. 135 $dbh->bz_start_transaction(); 136 my ($changes, $old_self) = $self->SUPER::update(@_); 137 138 # Also update group settings. 139 if ($self->{check_group_controls}) { 140 require Bugzilla::Bug; 141 import Bugzilla::Bug qw(LogActivityEntry); 142 143 my $old_settings = $old_self->group_controls; 144 my $new_settings = $self->group_controls; 145 my $timestamp = $dbh->selectrow_array('SELECT NOW()'); 146 147 foreach my $gid (keys %$new_settings) { 148 my $old_setting = $old_settings->{$gid} || {}; 149 my $new_setting = $new_settings->{$gid}; 150 # If all new settings are 0 for a given group, we delete the entry 151 # from group_control_map, so we have to track it here. 152 my $all_zero = 1; 153 my @fields; 154 my @values; 155 156 foreach my $field ('entry', 'membercontrol', 'othercontrol', 'canedit', 157 'editcomponents', 'editbugs', 'canconfirm') 158 { 159 my $old_value = $old_setting->{$field}; 160 my $new_value = $new_setting->{$field}; 161 $all_zero = 0 if $new_value; 162 next if (defined $old_value && $old_value == $new_value); 163 push(@fields, $field); 164 # The value has already been validated. 165 detaint_natural($new_value); 166 push(@values, $new_value); 167 } 168 # Is there anything to update? 169 next unless scalar @fields; 170 171 if ($all_zero) { 172 $dbh->do('DELETE FROM group_control_map 173 WHERE product_id = ? AND group_id = ?', 174 undef, $self->id, $gid); 175 } 176 else { 177 if (exists $old_setting->{group}) { 178 # There is already an entry in the DB. 179 my $set_fields = join(', ', map {"$_ = ?"} @fields); 180 $dbh->do("UPDATE group_control_map SET $set_fields 181 WHERE product_id = ? AND group_id = ?", 182 undef, (@values, $self->id, $gid)); 183 } 184 else { 185 # No entry yet. 186 my $fields = join(', ', @fields); 187 # +2 because of the product and group IDs. 188 my $qmarks = join(',', ('?') x (scalar @fields + 2)); 189 $dbh->do("INSERT INTO group_control_map (product_id, group_id, $fields) 190 VALUES ($qmarks)", undef, ($self->id, $gid, @values)); 191 } 192 } 193 194 # If the group is mandatory, restrict all bugs to it. 195 if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) { 196 my $bug_ids = 197 $dbh->selectcol_arrayref('SELECT bugs.bug_id 198 FROM bugs 199 LEFT JOIN bug_group_map 200 ON bug_group_map.bug_id = bugs.bug_id 201 AND group_id = ? 202 WHERE product_id = ? 203 AND bug_group_map.bug_id IS NULL', 204 undef, $gid, $self->id); 205 206 if (scalar @$bug_ids) { 207 my $sth = $dbh->prepare('INSERT INTO bug_group_map (bug_id, group_id) 208 VALUES (?, ?)'); 209 210 foreach my $bug_id (@$bug_ids) { 211 $sth->execute($bug_id, $gid); 212 # Add this change to the bug history. 213 LogActivityEntry($bug_id, 'bug_group', '', 214 $new_setting->{group}->name, 215 Bugzilla->user->id, $timestamp); 216 } 217 push(@{$changes->{'_group_controls'}->{'now_mandatory'}}, 218 {name => $new_setting->{group}->name, 219 bug_count => scalar @$bug_ids}); 220 } 221 } 222 # If the group can no longer be used to restrict bugs, remove them. 223 elsif ($new_setting->{membercontrol} == CONTROLMAPNA) { 224 my $bug_ids = 225 $dbh->selectcol_arrayref('SELECT bugs.bug_id 226 FROM bugs 227 INNER JOIN bug_group_map 228 ON bug_group_map.bug_id = bugs.bug_id 229 WHERE product_id = ? AND group_id = ?', 230 undef, $self->id, $gid); 231 232 if (scalar @$bug_ids) { 233 $dbh->do('DELETE FROM bug_group_map WHERE group_id = ? AND ' . 234 $dbh->sql_in('bug_id', $bug_ids), undef, $gid); 235 236 # Add this change to the bug history. 237 foreach my $bug_id (@$bug_ids) { 238 LogActivityEntry($bug_id, 'bug_group', 239 $old_setting->{group}->name, '', 240 Bugzilla->user->id, $timestamp); 241 } 242 push(@{$changes->{'_group_controls'}->{'now_na'}}, 243 {name => $old_setting->{group}->name, 244 bug_count => scalar @$bug_ids}); 245 } 246 } 247 } 248 249 delete $self->{groups_available}; 250 delete $self->{groups_mandatory}; 251 } 252 $dbh->bz_commit_transaction(); 253 # Changes have been committed. 254 delete $self->{check_group_controls}; 255 Bugzilla->user->clear_product_cache(); 256 257 return $changes; 258} 259 260sub remove_from_db { 261 my ($self, $params) = @_; 262 my $user = Bugzilla->user; 263 my $dbh = Bugzilla->dbh; 264 265 $dbh->bz_start_transaction(); 266 267 $self->_check_if_controller(); 268 269 if ($self->bug_count) { 270 if (Bugzilla->params->{'allowbugdeletion'}) { 271 require Bugzilla::Bug; 272 foreach my $bug_id (@{$self->bug_ids}) { 273 # Note that we allow the user to delete bugs he can't see, 274 # which is okay, because he's deleting the whole Product. 275 my $bug = new Bugzilla::Bug($bug_id); 276 $bug->remove_from_db(); 277 } 278 } 279 else { 280 ThrowUserError('product_has_bugs', { nb => $self->bug_count }); 281 } 282 } 283 284 if ($params->{delete_series}) { 285 my $series_ids = 286 $dbh->selectcol_arrayref('SELECT series_id 287 FROM series 288 INNER JOIN series_categories 289 ON series_categories.id = series.category 290 WHERE series_categories.name = ?', 291 undef, $self->name); 292 293 if (scalar @$series_ids) { 294 $dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids)); 295 } 296 297 # If no subcategory uses this product name, completely purge it. 298 my $in_use = 299 $dbh->selectrow_array('SELECT 1 300 FROM series 301 INNER JOIN series_categories 302 ON series_categories.id = series.subcategory 303 WHERE series_categories.name = ? ' . 304 $dbh->sql_limit(1), 305 undef, $self->name); 306 if (!$in_use) { 307 $dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name); 308 } 309 } 310 311 $self->SUPER::remove_from_db(); 312 313 $dbh->bz_commit_transaction(); 314 315 # We have to delete these internal variables, else we get 316 # the old lists of products and classifications again. 317 delete $user->{selectable_products}; 318 delete $user->{selectable_classifications}; 319 320} 321 322############################### 323#### Validators #### 324############################### 325 326sub _check_classification { 327 my ($invocant, $classification_name) = @_; 328 329 my $classification_id = 1; 330 if (Bugzilla->params->{'useclassification'}) { 331 my $classification = Bugzilla::Classification->check($classification_name); 332 $classification_id = $classification->id; 333 } 334 return $classification_id; 335} 336 337sub _check_name { 338 my ($invocant, $name) = @_; 339 340 $name = trim($name); 341 $name || ThrowUserError('product_blank_name'); 342 343 if (length($name) > MAX_PRODUCT_SIZE) { 344 ThrowUserError('product_name_too_long', {'name' => $name}); 345 } 346 347 my $product = new Bugzilla::Product({name => $name}); 348 if ($product && (!ref $invocant || $product->id != $invocant->id)) { 349 # Check for exact case sensitive match: 350 if ($product->name eq $name) { 351 ThrowUserError('product_name_already_in_use', {'product' => $product->name}); 352 } 353 else { 354 ThrowUserError('product_name_diff_in_case', {'product' => $name, 355 'existing_product' => $product->name}); 356 } 357 } 358 return $name; 359} 360 361sub _check_description { 362 my ($invocant, $description) = @_; 363 364 $description = trim($description); 365 $description || ThrowUserError('product_must_have_description'); 366 return $description; 367} 368 369sub _check_version { 370 my ($invocant, $version) = @_; 371 372 $version = trim($version); 373 $version || ThrowUserError('product_must_have_version'); 374 # We will check the version length when Bugzilla::Version->create will do it. 375 return $version; 376} 377 378sub _check_default_milestone { 379 my ($invocant, $milestone) = @_; 380 381 # Do nothing if target milestones are not in use. 382 unless (Bugzilla->params->{'usetargetmilestone'}) { 383 return (ref $invocant) ? $invocant->default_milestone : '---'; 384 } 385 386 $milestone = trim($milestone); 387 388 if (ref $invocant) { 389 # The default milestone must be one of the existing milestones. 390 my $mil_obj = new Bugzilla::Milestone({name => $milestone, product => $invocant}); 391 392 $mil_obj || ThrowUserError('product_must_define_defaultmilestone', 393 {product => $invocant->name, 394 milestone => $milestone}); 395 } 396 else { 397 $milestone ||= '---'; 398 } 399 return $milestone; 400} 401 402sub _check_milestone_url { 403 my ($invocant, $url) = @_; 404 405 # Do nothing if target milestones are not in use. 406 unless (Bugzilla->params->{'usetargetmilestone'}) { 407 return (ref $invocant) ? $invocant->milestone_url : ''; 408 } 409 410 $url = trim($url || ''); 411 return $url; 412} 413 414##################################### 415# Implement Bugzilla::Field::Choice # 416##################################### 417 418use constant FIELD_NAME => 'product'; 419use constant is_default => 0; 420 421############################### 422#### Methods #### 423############################### 424 425sub _create_bug_group { 426 my $self = shift; 427 my $dbh = Bugzilla->dbh; 428 429 my $group_name = $self->name; 430 while (new Bugzilla::Group({name => $group_name})) { 431 $group_name .= '_'; 432 } 433 my $group_description = get_text('bug_group_description', {product => $self}); 434 435 my $group = Bugzilla::Group->create({name => $group_name, 436 description => $group_description, 437 isbuggroup => 1}); 438 439 # Associate the new group and new product. 440 $dbh->do('INSERT INTO group_control_map 441 (group_id, product_id, membercontrol, othercontrol) 442 VALUES (?, ?, ?, ?)', 443 undef, ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA)); 444} 445 446sub _create_series { 447 my $self = shift; 448 449 my @series; 450 # We do every status, every resolution, and an "opened" one as well. 451 foreach my $bug_status (@{get_legal_field_values('bug_status')}) { 452 push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]); 453 } 454 455 foreach my $resolution (@{get_legal_field_values('resolution')}) { 456 next if !$resolution; 457 push(@series, [$resolution, "resolution=" . url_quote($resolution)]); 458 } 459 460 my @openedstatuses = BUG_STATE_OPEN; 461 my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses); 462 push(@series, [get_text('series_all_open'), $query]); 463 464 foreach my $sdata (@series) { 465 my $series = new Bugzilla::Series(undef, $self->name, 466 get_text('series_subcategory'), 467 $sdata->[0], Bugzilla->user->id, 1, 468 $sdata->[1] . "&product=" . url_quote($self->name), 1); 469 $series->writeToDatabase(); 470 } 471} 472 473sub set_name { $_[0]->set('name', $_[1]); } 474sub set_description { $_[0]->set('description', $_[1]); } 475sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); } 476sub set_is_active { $_[0]->set('isactive', $_[1]); } 477sub set_allows_unconfirmed { $_[0]->set('allows_unconfirmed', $_[1]); } 478 479sub set_group_controls { 480 my ($self, $group, $settings) = @_; 481 482 $group->is_active_bug_group 483 || ThrowUserError('product_illegal_group', {group => $group}); 484 485 scalar(keys %$settings) 486 || ThrowCodeError('product_empty_group_controls', {group => $group}); 487 488 # We store current settings for this group. 489 my $gs = $self->group_controls->{$group->id}; 490 # If there is no entry for this group yet, create a default hash. 491 unless (defined $gs) { 492 $gs = { entry => 0, 493 membercontrol => CONTROLMAPNA, 494 othercontrol => CONTROLMAPNA, 495 canedit => 0, 496 editcomponents => 0, 497 editbugs => 0, 498 canconfirm => 0, 499 group => $group }; 500 } 501 502 # Both settings must be defined, or none of them can be updated. 503 if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) { 504 # Legality of control combination is a function of 505 # membercontrol\othercontrol 506 # NA SH DE MA 507 # NA + - - - 508 # SH + + + + 509 # DE + - + + 510 # MA - - - + 511 foreach my $field ('membercontrol', 'othercontrol') { 512 my ($is_legal) = grep { $settings->{$field} == $_ } 513 (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY); 514 defined $is_legal || ThrowCodeError('product_illegal_group_control', 515 { field => $field, value => $settings->{$field} }); 516 } 517 unless ($settings->{membercontrol} == $settings->{othercontrol} 518 || $settings->{membercontrol} == CONTROLMAPSHOWN 519 || ($settings->{membercontrol} == CONTROLMAPDEFAULT 520 && $settings->{othercontrol} != CONTROLMAPSHOWN)) 521 { 522 ThrowUserError('illegal_group_control_combination', {groupname => $group->name}); 523 } 524 $gs->{membercontrol} = $settings->{membercontrol}; 525 $gs->{othercontrol} = $settings->{othercontrol}; 526 } 527 528 foreach my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') { 529 next unless defined $settings->{$field}; 530 $gs->{$field} = $settings->{$field} ? 1 : 0; 531 } 532 $self->{group_controls}->{$group->id} = $gs; 533 $self->{check_group_controls} = 1; 534} 535 536sub components { 537 my $self = shift; 538 my $dbh = Bugzilla->dbh; 539 540 if (!defined $self->{components}) { 541 my $ids = $dbh->selectcol_arrayref(q{ 542 SELECT id FROM components 543 WHERE product_id = ? 544 ORDER BY name}, undef, $self->id); 545 546 require Bugzilla::Component; 547 $self->{components} = Bugzilla::Component->new_from_list($ids); 548 } 549 return $self->{components}; 550} 551 552sub group_controls { 553 my ($self, $full_data) = @_; 554 my $dbh = Bugzilla->dbh; 555 556 # By default, we don't return groups which are not listed in 557 # group_control_map. If $full_data is true, then we also 558 # return groups whose settings could be set for the product. 559 my $where_or_and = 'WHERE'; 560 my $and_or_where = 'AND'; 561 if ($full_data) { 562 $where_or_and = 'AND'; 563 $and_or_where = 'WHERE'; 564 } 565 566 # If $full_data is true, we collect all the data in all cases, 567 # even if the cache is already populated. 568 # $full_data is never used except in the very special case where 569 # all configurable bug groups are displayed to administrators, 570 # so we don't care about collecting all the data again in this case. 571 if (!defined $self->{group_controls} || $full_data) { 572 # Include name to the list, to allow us sorting data more easily. 573 my $query = qq{SELECT id, name, entry, membercontrol, othercontrol, 574 canedit, editcomponents, editbugs, canconfirm 575 FROM groups 576 LEFT JOIN group_control_map 577 ON id = group_id 578 $where_or_and product_id = ? 579 $and_or_where isbuggroup = 1}; 580 $self->{group_controls} = 581 $dbh->selectall_hashref($query, 'id', undef, $self->id); 582 583 # For each group ID listed above, create and store its group object. 584 my @gids = keys %{$self->{group_controls}}; 585 my $groups = Bugzilla::Group->new_from_list(\@gids); 586 $self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups; 587 } 588 589 # We never cache bug counts, for the same reason as above. 590 if ($full_data) { 591 my $counts = 592 $dbh->selectall_arrayref('SELECT group_id, COUNT(bugs.bug_id) AS bug_count 593 FROM bug_group_map 594 INNER JOIN bugs 595 ON bugs.bug_id = bug_group_map.bug_id 596 WHERE bugs.product_id = ? ' . 597 $dbh->sql_group_by('group_id'), 598 {'Slice' => {}}, $self->id); 599 foreach my $data (@$counts) { 600 $self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count}; 601 } 602 } 603 return $self->{group_controls}; 604} 605 606sub groups_available { 607 my ($self) = @_; 608 return $self->{groups_available} if defined $self->{groups_available}; 609 my $dbh = Bugzilla->dbh; 610 my $shown = CONTROLMAPSHOWN; 611 my $default = CONTROLMAPDEFAULT; 612 my %member_groups = @{ $dbh->selectcol_arrayref( 613 "SELECT group_id, membercontrol 614 FROM group_control_map 615 INNER JOIN groups ON group_control_map.group_id = groups.id 616 WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? 617 AND (membercontrol = $shown OR membercontrol = $default) 618 AND " . Bugzilla->user->groups_in_sql(), 619 {Columns=>[1,2]}, $self->id) }; 620 # We don't need to check the group membership here, because we only 621 # add these groups to the list below if the group isn't already listed 622 # for membercontrol. 623 my %other_groups = @{ $dbh->selectcol_arrayref( 624 "SELECT group_id, othercontrol 625 FROM group_control_map 626 INNER JOIN groups ON group_control_map.group_id = groups.id 627 WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? 628 AND (othercontrol = $shown OR othercontrol = $default)", 629 {Columns=>[1,2]}, $self->id) }; 630 631 # If the user is a member, then we use the membercontrol value. 632 # Otherwise, we use the othercontrol value. 633 my %all_groups = %member_groups; 634 foreach my $id (keys %other_groups) { 635 if (!defined $all_groups{$id}) { 636 $all_groups{$id} = $other_groups{$id}; 637 } 638 } 639 640 my $available = Bugzilla::Group->new_from_list([keys %all_groups]); 641 foreach my $group (@$available) { 642 $group->{is_default} = 1 if $all_groups{$group->id} == $default; 643 } 644 645 $self->{groups_available} = $available; 646 return $self->{groups_available}; 647} 648 649sub groups_mandatory { 650 my ($self) = @_; 651 return $self->{groups_mandatory} if $self->{groups_mandatory}; 652 my $groups = Bugzilla->user->groups_as_string; 653 my $mandatory = CONTROLMAPMANDATORY; 654 # For membercontrol we don't check group_id IN, because if membercontrol 655 # is Mandatory, the group is Mandatory for everybody, regardless of their 656 # group membership. 657 my $ids = Bugzilla->dbh->selectcol_arrayref( 658 "SELECT group_id 659 FROM group_control_map 660 INNER JOIN groups ON group_control_map.group_id = groups.id 661 WHERE product_id = ? AND isactive = 1 662 AND (membercontrol = $mandatory 663 OR (othercontrol = $mandatory 664 AND group_id NOT IN ($groups)))", 665 undef, $self->id); 666 $self->{groups_mandatory} = Bugzilla::Group->new_from_list($ids); 667 return $self->{groups_mandatory}; 668} 669 670# We don't just check groups_valid, because we want to know specifically 671# if this group can be validly set by the currently-logged-in user. 672sub group_is_settable { 673 my ($self, $group) = @_; 674 675 return 0 unless ($group->is_active && $group->is_bug_group); 676 677 my $is_mandatory = grep { $group->id == $_->id } 678 @{ $self->groups_mandatory }; 679 my $is_available = grep { $group->id == $_->id } 680 @{ $self->groups_available }; 681 return ($is_mandatory or $is_available) ? 1 : 0; 682} 683 684sub group_is_valid { 685 my ($self, $group) = @_; 686 return grep($_->id == $group->id, @{ $self->groups_valid }) ? 1 : 0; 687} 688 689sub groups_valid { 690 my ($self) = @_; 691 return $self->{groups_valid} if defined $self->{groups_valid}; 692 693 # Note that we don't check OtherControl below, because there is no 694 # valid NA/* combination. 695 my $ids = Bugzilla->dbh->selectcol_arrayref( 696 "SELECT DISTINCT group_id 697 FROM group_control_map AS gcm 698 INNER JOIN groups ON gcm.group_id = groups.id 699 WHERE product_id = ? AND isbuggroup = 1 700 AND membercontrol != " . CONTROLMAPNA, undef, $self->id); 701 $self->{groups_valid} = Bugzilla::Group->new_from_list($ids); 702 return $self->{groups_valid}; 703} 704 705sub versions { 706 my $self = shift; 707 my $dbh = Bugzilla->dbh; 708 709 if (!defined $self->{versions}) { 710 my $ids = $dbh->selectcol_arrayref(q{ 711 SELECT id FROM versions 712 WHERE product_id = ?}, undef, $self->id); 713 714 $self->{versions} = Bugzilla::Version->new_from_list($ids); 715 } 716 return $self->{versions}; 717} 718 719sub milestones { 720 my $self = shift; 721 my $dbh = Bugzilla->dbh; 722 723 if (!defined $self->{milestones}) { 724 my $ids = $dbh->selectcol_arrayref(q{ 725 SELECT id FROM milestones 726 WHERE product_id = ?}, undef, $self->id); 727 728 $self->{milestones} = Bugzilla::Milestone->new_from_list($ids); 729 } 730 return $self->{milestones}; 731} 732 733sub bug_count { 734 my $self = shift; 735 my $dbh = Bugzilla->dbh; 736 737 if (!defined $self->{'bug_count'}) { 738 $self->{'bug_count'} = $dbh->selectrow_array(qq{ 739 SELECT COUNT(bug_id) FROM bugs 740 WHERE product_id = ?}, undef, $self->id); 741 742 } 743 return $self->{'bug_count'}; 744} 745 746sub bug_ids { 747 my $self = shift; 748 my $dbh = Bugzilla->dbh; 749 750 if (!defined $self->{'bug_ids'}) { 751 $self->{'bug_ids'} = 752 $dbh->selectcol_arrayref(q{SELECT bug_id FROM bugs 753 WHERE product_id = ?}, 754 undef, $self->id); 755 } 756 return $self->{'bug_ids'}; 757} 758 759sub user_has_access { 760 my ($self, $user) = @_; 761 762 return Bugzilla->dbh->selectrow_array( 763 'SELECT CASE WHEN group_id IS NULL THEN 1 ELSE 0 END 764 FROM products LEFT JOIN group_control_map 765 ON group_control_map.product_id = products.id 766 AND group_control_map.entry != 0 767 AND group_id NOT IN (' . $user->groups_as_string . ') 768 WHERE products.id = ? ' . Bugzilla->dbh->sql_limit(1), 769 undef, $self->id); 770} 771 772sub flag_types { 773 my $self = shift; 774 775 return $self->{'flag_types'} if defined $self->{'flag_types'}; 776 777 # We cache flag types to avoid useless calls to get_clusions(). 778 my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; 779 $self->{flag_types} = {}; 780 my $prod_id = $self->id; 781 my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id }); 782 783 foreach my $type ('bug', 'attachment') { 784 my @flags = grep { $_->target_type eq $type } @$flagtypes; 785 $self->{flag_types}->{$type} = \@flags; 786 787 # Also populate component flag types, while we are here. 788 foreach my $comp (@{$self->components}) { 789 $comp->{flag_types} ||= {}; 790 my $comp_id = $comp->id; 791 792 foreach my $flag (@flags) { 793 my $flag_id = $flag->id; 794 $cache->{$flag_id} ||= $flag; 795 my $i = $cache->{$flag_id}->inclusions_as_hash; 796 my $e = $cache->{$flag_id}->exclusions_as_hash; 797 my $included = $i->{0}->{0} || $i->{0}->{$comp_id} 798 || $i->{$prod_id}->{0} || $i->{$prod_id}->{$comp_id}; 799 my $excluded = $e->{0}->{0} || $e->{0}->{$comp_id} 800 || $e->{$prod_id}->{0} || $e->{$prod_id}->{$comp_id}; 801 push(@{$comp->{flag_types}->{$type}}, $flag) if ($included && !$excluded); 802 } 803 } 804 } 805 return $self->{'flag_types'}; 806} 807 808sub classification { 809 my $self = shift; 810 $self->{'classification'} ||= 811 new Bugzilla::Classification($self->classification_id); 812 return $self->{'classification'}; 813} 814 815############################### 816#### Accessors ###### 817############################### 818 819sub allows_unconfirmed { return $_[0]->{'allows_unconfirmed'}; } 820sub description { return $_[0]->{'description'}; } 821sub is_active { return $_[0]->{'isactive'}; } 822sub default_milestone { return $_[0]->{'defaultmilestone'}; } 823sub classification_id { return $_[0]->{'classification_id'}; } 824 825############################### 826#### Subroutines ###### 827############################### 828 829sub check { 830 my ($class, $params) = @_; 831 $params = { name => $params } if !ref $params; 832 if (!$params->{allow_inaccessible}) { 833 $params->{_error} = 'product_access_denied'; 834 } 835 my $product = $class->SUPER::check($params); 836 837 if (!$params->{allow_inaccessible} 838 && !Bugzilla->user->can_access_product($product)) 839 { 840 ThrowUserError('product_access_denied', $params); 841 } 842 return $product; 843} 844 8451; 846 847__END__ 848 849=head1 NAME 850 851Bugzilla::Product - Bugzilla product class. 852 853=head1 SYNOPSIS 854 855 use Bugzilla::Product; 856 857 my $product = new Bugzilla::Product(1); 858 my $product = new Bugzilla::Product({ name => 'AcmeProduct' }); 859 860 my @components = $product->components(); 861 my $groups_controls = $product->group_controls(); 862 my @milestones = $product->milestones(); 863 my @versions = $product->versions(); 864 my $bugcount = $product->bug_count(); 865 my $bug_ids = $product->bug_ids(); 866 my $has_access = $product->user_has_access($user); 867 my $flag_types = $product->flag_types(); 868 my $classification = $product->classification(); 869 870 my $id = $product->id; 871 my $name = $product->name; 872 my $description = $product->description; 873 my isactive = $product->is_active; 874 my $defaultmilestone = $product->default_milestone; 875 my $classificationid = $product->classification_id; 876 my $allows_unconfirmed = $product->allows_unconfirmed; 877 878=head1 DESCRIPTION 879 880Product.pm represents a product object. It is an implementation 881of L<Bugzilla::Object>, and thus provides all methods that 882L<Bugzilla::Object> provides. 883 884The methods that are specific to C<Bugzilla::Product> are listed 885below. 886 887=head1 METHODS 888 889=over 890 891=item C<components> 892 893 Description: Returns an array of component objects belonging to 894 the product. 895 896 Params: none. 897 898 Returns: An array of Bugzilla::Component object. 899 900=item C<group_controls()> 901 902 Description: Returns a hash (group id as key) with all product 903 group controls. 904 905 Params: $full_data (optional, false by default) - when true, 906 the number of bugs per group applicable to the product 907 is also returned. Moreover, bug groups which have no 908 special settings for the product are also returned. 909 910 Returns: A hash with group id as key and hash containing 911 a Bugzilla::Group object and the properties of group 912 relative to the product. 913 914=item C<groups_available> 915 916Tells you what groups are set to Default or Shown for the 917currently-logged-in user (taking into account both OtherControl and 918MemberControl). Returns an arrayref of L<Bugzilla::Group> objects with 919an extra hash keys set, C<is_default>, which is true if the group 920is set to Default for the currently-logged-in user. 921 922=item C<groups_mandatory> 923 924Tells you what groups are mandatory for bugs in this product, for the 925currently-logged-in user. Returns an arrayref of C<Bugzilla::Group> objects. 926 927=item C<group_is_settable> 928 929=over 930 931=item B<Description> 932 933Tells you whether or not the currently-logged-in user can set a group 934on a bug (whether or not they match the MemberControl/OtherControl 935settings for a group in this product). Groups that are C<Mandatory> for 936the currently-loggeed-in user are also acceptable since from Bugzilla's 937perspective, there's no problem with "setting" a Mandatory group on 938a bug. (In fact, the user I<must> set the Mandatory group on the bug.) 939 940=item B<Params> 941 942=over 943 944=item C<$group> - A L<Bugzilla::Group> object. 945 946=back 947 948=item B<Returns> 949 950C<1> if the group is valid in this product, C<0> otherwise. 951 952=back 953 954 955=item C<groups_valid> 956 957=over 958 959=item B<Description> 960 961Returns an arrayref of L<Bugzilla::Group> objects, representing groups 962that bugs could validly be restricted to within this product. Used mostly 963when you need the list of all possible groups that could be set in a product 964by anybody, disregarding whether or not the groups are active or who the 965currently logged-in user is. 966 967B<Note>: This doesn't check whether or not the current user can add/remove 968bugs to/from these groups. It just tells you that bugs I<could be in> these 969groups, in this product. 970 971=item B<Params> (none) 972 973=item B<Returns> An arrayref of L<Bugzilla::Group> objects. 974 975=back 976 977=item C<group_is_valid> 978 979Returns C<1> if the passed-in L<Bugzilla::Group> or group id could be set 980on a bug by I<anybody>, in this product. Even inactive groups are considered 981valid. (This is a shortcut for searching L</groups_valid> to find out if 982a group is valid in a particular product.) 983 984=item C<versions> 985 986 Description: Returns all valid versions for that product. 987 988 Params: none. 989 990 Returns: An array of Bugzilla::Version objects. 991 992=item C<milestones> 993 994 Description: Returns all valid milestones for that product. 995 996 Params: none. 997 998 Returns: An array of Bugzilla::Milestone objects. 999 1000=item C<bug_count()> 1001 1002 Description: Returns the total of bugs that belong to the product. 1003 1004 Params: none. 1005 1006 Returns: Integer with the number of bugs. 1007 1008=item C<bug_ids()> 1009 1010 Description: Returns the IDs of bugs that belong to the product. 1011 1012 Params: none. 1013 1014 Returns: An array of integer. 1015 1016=item C<user_has_access()> 1017 1018 Description: Tells you whether or not the user is allowed to enter 1019 bugs into this product, based on the C<entry> group 1020 control. To see whether or not a user can actually 1021 enter a bug into a product, use C<$user->can_enter_product>. 1022 1023 Params: C<$user> - A Bugzilla::User object. 1024 1025 Returns C<1> If this user's groups allow him C<entry> access to 1026 this Product, C<0> otherwise. 1027 1028=item C<flag_types()> 1029 1030 Description: Returns flag types available for at least one of 1031 its components. 1032 1033 Params: none. 1034 1035 Returns: Two references to an array of flagtype objects. 1036 1037=item C<classification()> 1038 1039 Description: Returns the classification the product belongs to. 1040 1041 Params: none. 1042 1043 Returns: A Bugzilla::Classification object. 1044 1045=back 1046 1047=head1 SUBROUTINES 1048 1049=over 1050 1051=item C<preload> 1052 1053When passed an arrayref of C<Bugzilla::Product> objects, preloads their 1054L</milestones>, L</components>, and L</versions>, which is much faster 1055than calling those accessors on every item in the array individually. 1056 1057If the 2nd argument passed to C<preload> is true, flag types for these 1058products and their components are also preloaded. 1059 1060This function is not exported, so must be called like 1061C<Bugzilla::Product::preload($products)>. 1062 1063=back 1064 1065=head1 SEE ALSO 1066 1067L<Bugzilla::Object> 1068 1069=cut 1070