diff options
Diffstat (limited to 'extensions/RuleEngine/lib/Pages.pm')
-rw-r--r-- | extensions/RuleEngine/lib/Pages.pm | 1631 |
1 files changed, 1631 insertions, 0 deletions
diff --git a/extensions/RuleEngine/lib/Pages.pm b/extensions/RuleEngine/lib/Pages.pm new file mode 100644 index 000000000..b67fcdd9b --- /dev/null +++ b/extensions/RuleEngine/lib/Pages.pm @@ -0,0 +1,1631 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package Bugzilla::Extension::RuleEngine::Pages; + +use strict; +use warnings; +use 5.10.1; + +use Bugzilla::Classification; +use Bugzilla::Component; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Field; +use Bugzilla::FlagType; +use Bugzilla::Group; +use Bugzilla::Keyword; +use Bugzilla::Mailer; +use Bugzilla::Milestone; +use Bugzilla::Product; +use Bugzilla::Release; +use Bugzilla::Search; +use Bugzilla::Status; +use Bugzilla::Token; +use Bugzilla::Util; + +use Bugzilla::Extension::RuleEngine::Job + qw(_get_flag_change_date _get_flag_group_match); +use Bugzilla::Extension::RuleEngine::Rule; +use Bugzilla::Extension::RuleEngine::RuleDetail; +use Bugzilla::Extension::RuleEngine::RuleGroup; + +use JSON qw(from_json); +use Email::MIME; +use List::MoreUtils qw(firstidx uniq any none apply); +use List::Compare; + +use base qw(Exporter); +@Bugzilla::Extension::RuleEngine::Pages::EXPORT + = qw(_get_matching_bugs_for_rule _page_rule_edit); + +# LHS is what we store in the DB, RHS is what we show on the web page +use constant FLAG_COND_TO_TEXT => { + '' => 'Change', + 'X' => 'If unset', + '?' => 'If ?', + '+' => 'If +', + '-' => 'If -', + 'X?' => 'If unset or ?', + '?+' => 'If ? or +', + '+-' => 'If + or -', +}; + +# The opposite of the above. +use constant FLAG_TEXT_TO_COND => {reverse %{&FLAG_COND_TO_TEXT}}; + +sub _page_rule_edit { + my ($vars) = @_; + my $cgi = Bugzilla->cgi; + + # If this is the first call, do some initialisation. + if (!$cgi->param('_init')) { + my $token = issue_session_token('rule_edit'); + $cgi->param('token', $token); + _page_rule_edit_init(); + } + else { + my $token = $cgi->param('token'); + check_token_data($token, 'rule_edit'); + } + + # Check for errors. Will change params(next_page) on error + $vars->{error} = _page_rule_edit_check(); + + # Generate the required values for the page to display + _page_rule_edit_set_vars($vars); + + return; +} + +sub _page_rule_edit_init { + my $cgi = Bugzilla->cgi; + + if ($cgi->param('rule_id')) { + my $rule = Bugzilla::Extension::RuleEngine::Rule->check( + {id => scalar $cgi->param('rule_id')}); + + my $real_user = Bugzilla->user; + + Bugzilla->set_user(Bugzilla::User->new({name => $rule->rule_group->user_name})) + if ($rule->rule_group->user_name); + + my $vars = _rule_to_cgi($rule); + while (my ($key, $value) = each %$vars) { + if (ref $value) { + $cgi->param($key, @$value); + } + else { + $cgi->param($key, $value); + } + } + + Bugzilla->set_user($real_user); + } + + # If we are not updating, check at least one active rule group exists + if ($cgi->param('action') ne 'update') { + my @rule_groups = Bugzilla::Extension::RuleEngine::RuleGroup->get_all(); + unless (any { $_->is_active } @rule_groups) { + ThrowUserError('no_active_rule_group'); + } + } + + # If we are cloning, we change the name + if ($cgi->param('action') eq 'clone') { + $cgi->param('p1_name', 'Clone of ' . $cgi->param('p1_name')); + } + + $cgi->param('_init', 1); + $cgi->param('next_page', 1); + + return; +} + +sub _page_rule_edit_check { + my $cgi = Bugzilla->cgi; + my $this_page = $cgi->param('this_page') || return; + my %error = (); + + # To prevent URL stuffing, we always check for things on previous pages + # and redirect to the first page that has an error. Clever, eh? :) + + if (length(trim($cgi->param('p1_name'))) == 0) { + $error{p1_name} = 'The rule name is mandatory'; + } + if (length(trim($cgi->param('p1_description'))) == 0) { + $error{p1_description} = 'The rule description is mandatory'; + } + if (length(trim($cgi->param('p1_rule_group'))) == 0) { + $error{p1_rule_group} = 'The rule group is mandatory'; + } + if ( $cgi->param('action') eq 'update' + && length($cgi->param('p1_run_after')) + && trim($cgi->param('p1_run_after')) eq $cgi->param('p1_name')) + { + $error{p1_run_after} = 'A rule cannot run after itself'; + } + + # Check name is unique + if (length(trim($cgi->param('p1_name')))) { + my $name = trim($cgi->param('p1_name')); + my $check = Bugzilla::Extension::RuleEngine::Rule->new({name => $name}); + if ($check) { + if ($cgi->param('action') ne 'update' or $check->id != $cgi->param('rule_id')) { + $error{p1_name} = "The rule named '$name' already exists"; + } + elsif ($cgi->param('action') eq 'update' + && $check->use_count + && $check->name ne $cgi->param('p1_name')) + { + $cgi->param('p1_name', $check->name); + $error{p1_name} = 'The rule name cannot be changed. It has been changed back'; + } + } + } + + # Validate the user list to ensure that they are real users in Bugzilla. + if (length(trim(scalar $cgi->param('p1_rule_owners')))) { + my $list = Bugzilla::Extension::RuleEngine::Rule::_split_rule_owners( + scalar $cgi->param('p1_rule_owners')); + + # Bugzilla::User->check will throw an ugly error message to the user + # if they get an email address wrong. + foreach my $user_email (@$list) { + my $bz_user = Bugzilla::User->check($user_email); + } + } + + # Check the rule group and rule exists + if (length(trim($cgi->param('p1_rule_group')))) { + my $name = trim($cgi->param('p1_rule_group')); + my $check = Bugzilla::Extension::RuleEngine::RuleGroup->new({name => $name}); + if (!$check) { + $error{p1_rule_group} = "The rule group named '$name' does not exist"; + } + } + + if (length(trim($cgi->param('p1_run_after')))) { + my $name = trim($cgi->param('p1_run_after')); + my $check = Bugzilla::Extension::RuleEngine::Rule->new({name => $name}); + if (!$check) { + $error{p1_run_after} = "The rule named '$name' does not exist"; + } + } + + # Check the classifications are valid + my @classifications = $cgi->param('p1_classification'); + foreach my $class_name (@classifications) { + if (!Bugzilla::Classification->new({name => $class_name})) { + $error{p1_classification} = "The classification '$class_name' is not valid"; + } + } + + # Check the products are valid + my @products = $cgi->param('p1_product'); + foreach my $product_name (@products) { + if (!Bugzilla::Product->new({name => $product_name})) { + $error{p1_product} = "The product '$product_name' is not valid"; + } + } + + if (!@products && !@classifications) { + $error{p1_product} = "Either the product or classification must be set"; + } + + # End of first page checking + if (scalar keys %error) { + $cgi->param('next_page', 1); + return \%error; + } + + if ($this_page < 2) { + return {}; + } + + # Check the second page is valid + # If the status is not closed, clear the resolution value + unless ( + defined $cgi->param('p2_bug_status') + && any { $cgi->param('p2_bug_status') eq $_->name } + Bugzilla::Status::closed_bug_statuses + ) + { + $cgi->param('p2_resolution', ''); + } + + # Check that if 'Any status' (value '') or 'All open' are selected that + # it is the only option + my @statuses = $cgi->param('p2_bug_status'); + if ( + scalar(@statuses) > 1 && any { $_ eq '' || $_ eq 'All open' } + @statuses + ) + { + $error{p2_bug_status} + = "'Any status' and 'All open' cannot be used with another status"; + } + + # Check that the _word options are valid + foreach my $field ( + grep { + /p2_word_(?:component|target_(?:milestone|release)|keywords|bug_group|cf_.+)$/ + } $cgi->param + ) + { + if ($cgi->param($field) && $cgi->param($field) !~ /^is (any|all|not any) of$/) { + $error{$field} = "'" . $cgi->param($field) . "' is not a valid option"; + } + } + if ($cgi->param('p2_word_component_list') + !~ /^(approved|capacity|both|neither|\-\-\-)$/) + { + $error{p2_word_component_list} + = "'" . $cgi->param('p2_word_component_list') . "' is not a valid option"; + } + + # Check that the component list values are both specified (or neither) + if ($cgi->param('p2_word_component_list') ne '---' + && !$cgi->param('p2_component_list')) + { + $error{p2_component_list} = 'You must specify a component list name'; + } + if ( $cgi->param('p2_word_component_list') eq '---' + && $cgi->param('p2_component_list')) + { + $error{p2_component_list_word} + = 'You must specify a component list condition (second column)'; + } + + # That before and after are both set, or not + my $before = $cgi->param('p2_flags_order'); + my $after = $cgi->param('p2_flags_order_after'); + if ($before xor $after) { + $error{p2_flags_order} = 'You must specify both values or neither'; + } + elsif ($before and $after) { + + # ... and the values are valid + foreach my $flag ($before, $after) { + my ($flag_name, $status) = ($flag =~ /^(.+)(.)$/); + if (none { $_ eq $status } qw(+ - ?)) { + $error{p2_flags_order} + = "'$status' is not a valid status in '$flag' (should be ?, + or -)"; + last; + } + elsif (!scalar(@{Bugzilla::FlagType::match({name => $flag_name})})) { + $error{p2_flags_order} + = "'$flag_name' is not a valid flag type name in '$flag'"; + last; + } + } + if ($before eq $after && !$error{p2_flags_order}) { + $error{p2_flags_order} = 'The flag names should be different'; + } + } + + # Check the flags options are valid + foreach my $field (grep {/p2_flag_/} $cgi->param) { + if ($cgi->param($field) =~ /[^X\?\+\-]/) { + $error{$field} = "contains an invalid option (" . $cgi->param($field) . ")"; + } + } + + foreach my $cnt ( + sort { $a <=> $b } + apply {s/^p2_custom_f(\d+)$/$1/} + grep {/^p2_custom_f(\d+)$/} $cgi->param + ) + { + my $n = $cgi->param("p2_custom_n$cnt") ? 1 : 0; + my $f = trim($cgi->param("p2_custom_f$cnt")); + my $o = trim($cgi->param("p2_custom_o$cnt")); + my $v = trim($cgi->param("p2_custom_v$cnt")); + if (length($o) && $o eq 'noop' && length($v)) { + ThrowUserError('cannot_search_noop', {field => $f}); + } + } + + # End of second page checking + if (scalar keys %error) { + $cgi->param('next_page', 2); + return \%error; + } + + # We also want to check this user wasn't silly enough to have no + # criterion specified. The easiest way to check this is to + # _rule_cgi_to_array and check match isn't empty + my $data = _rule_cgi_to_array($cgi); + unless (scalar keys %{$data->{match}}) { + ThrowUserError('rule_no_criterion'); + } + + if ($this_page < 3) { + return {}; + } + + # Check the third page is valid + # If the status is not closed, clear the resolution value + unless ( + any { $cgi->param('p3_bug_status') eq $_->name } + Bugzilla::Status::closed_bug_statuses + ) + { + $cgi->param('p3_resolution', ''); + } + + # Status and resolution must be set or unset + my $status = $cgi->param('p3_bug_status'); + my $resolution = $cgi->param('p3_resolution'); + + my $resolutions = Bugzilla::Field->new({name => 'resolution'})->legal_values; + if ($status || $resolution) { + if (!$status && $resolution) { + $error{p3_bug_status} = "You must set a status if you set a resolution"; + } + my $sobj = Bugzilla::Status->new({name => $status}); + if (!$sobj) { + $error{p3_bug_status} = "The status of '$status' is not valid"; + } + elsif (!$sobj->is_active) { + $error{p3_bug_status} = "The status of '$status' is not active"; + } + elsif (!$sobj->is_open && !$resolution) { + $error{p3_resolution} + = "You need to enter a resolution if marking a bug as closed"; + } + elsif ($sobj->is_open && $resolution) { + + # Shouldn't happen due to the change made above + $error{p3_resolution} = "You cannot have a resolution if marking a bug as open"; + } + elsif ($resolution && none { $_->name eq $resolution } @$resolutions) { + $error{p3_resolution} = "The resolution of '$resolution' is not valid"; + } + + if ( + $cgi->param('p2_bug_status') eq 'All open' && none { $status eq $_->name } + Bugzilla::Status::closed_bug_statuses + ) + { + $error{p3_bug_status} + = "If you check 'All open' statuses then you must choose a closed status for the bug if you change it"; + } + + } + + # We check that middle and right hand columns are both set or unset + my $custom_fields + = Bugzilla->fields({type => FIELD_TYPE_MULTI_SELECT, custom => 1}); + my @action_fields + = ( + qw/p3_target_release p3_keywords p3_bug_group p3_cc p3_assigned_to p3_qa_contact p3_docs_contact/ + ); + push @action_fields, map { 'p3_' . $_->name } @$custom_fields; + + foreach my $field (@action_fields) { + + my $word_field = $field; + substr($word_field, 2, 0, '_word'); + + if ( + $cgi->param($word_field) + && ($cgi->param($word_field) + !~ /^(Do not change|Set|Add|Remove|Unset|Assign|Reset to default)?$/ + || ($field eq 'p3_cc' and $cgi->param($word_field) eq 'Set')) + ) + { + $error{$word_field} + = "'" . $cgi->param($word_field) . "' is not a valid option"; + } + elsif ( + $cgi->param($field) + && (!$cgi->param($word_field) + || $cgi->param($word_field) eq '' + || $cgi->param($word_field) eq 'Do not change') + ) + { + $error{$word_field} = "You must specify what action you want"; + } + elsif ( + $cgi->param($field) && any { $cgi->param($word_field) eq $_ } + ('Unset', 'Reset to default') + ) + { + $error{$word_field} = 'You cannot specify a value for this action type'; + } + + # If we are reassigning the bug to a user, verify that the user exists. + elsif (defined($cgi->param($word_field)) + && $cgi->param($word_field) eq 'Assign' + && defined($cgi->param($field))) + { + if (length($cgi->param($field)) > 0 && $cgi->param($field) !~ m/^%/) { + Bugzilla::User->check(scalar $cgi->param($field)); + } + } + elsif (!$cgi->param($field) + && defined($cgi->param($word_field)) + && $cgi->param($word_field) ne '' + && $cgi->param($word_field) ne 'Unset' + && $cgi->param($word_field) ne 'Do not change' + && $cgi->param($word_field) ne 'Reset to default') + { + $error{$field} = "You must specify what value(s) you want to " + . lc($cgi->param($word_field)); + } + } + + # Likewise with the flags + my @flags = uniq( + apply {s/p3(?:_word)?_flag_(.+?)$/$1/} + + # We're buggered if we have a flag with requestee in it's name + grep { !/p3_flag_(.+?)_(ignore|requestee)$/ } $cgi->param + ); + foreach my $flag (@flags) { + my $value = $cgi->param("p3_flag_$flag"); + my $word_value = $cgi->param("p3_word_flag_$flag"); + if ( ($value && (!$word_value || $word_value eq 'Do not change')) + || (!$value && ($word_value && $word_value ne 'Do not change'))) + { + $error{"p3_flag_$flag"} = "Both values must be set or unset"; + } + } + + # End of third page checking + if (scalar keys %error) { + $cgi->param('next_page', 3); + return \%error; + } + + # We also want to check this user wasn't silly enough to have no + # action specified. The easiest way to check this is to use the + # _rule_cgi_to_array defined above and check action isn't empty + unless (scalar keys %{$data->{action}}) { + ThrowUserError('rule_no_action'); + } + + if ($this_page < 4) { + return {}; + } + + # This is only one thing to check on this page, but don't check it if we + # are going back to a previous page. + if (!$cgi->param('happy') && $cgi->param('next_page') !~ /^[123]$/) { + $cgi->param('next_page', 4); + return {happy => 'You did not indicate you were happy about the changes!'}; + } + + return {}; +} + +sub _page_rule_edit_set_vars { + my $vars = shift; + my $cgi = Bugzilla->cgi; + my $next_page = $cgi->param('next_page'); + + my $real_user = Bugzilla->user; + + if ($cgi->param('rule_id')) { + $vars->{rule} + = Bugzilla::Extension::RuleEngine::Rule->new(scalar $cgi->param('rule_id')); + Bugzilla->set_user(Bugzilla::User->new( + {name => $vars->{rule}->rule_group->user_name})) + if ($vars->{rule}->rule_group->user_name); + } + + if ($next_page == 1) { + my $rule_group; + + if (defined($vars->{rule})) { + $rule_group = $vars->{rule}->rule_group; + } + else { + $rule_group = Bugzilla::Extension::RuleEngine::RuleGroup->new( + $cgi->param('rule_group_id')); + $cgi->param('p1_rule_group', $rule_group->name); + } + + # Get a list of current rule group, components, and products + @{$vars->{rule_group_list}} + = Bugzilla::Extension::RuleEngine::RuleGroup->get_all; + @{$vars->{classification_list}} = @{$rule_group->classifications}; + @{$vars->{product_list}} = @{$rule_group->products}; + + # The list of rules needs to exclude self if it's an update + my @rules_list = @{Bugzilla::Extension::RuleEngine::Rule->match_rules( + {rule_group_id => $rule_group->id} + ) + }; + + my $excl_rule_id = -1; + if ($cgi->param('action') eq 'update' and $cgi->param('rule_id')) { + $excl_rule_id = $cgi->param('rule_id'); + } + @{$vars->{rules_list}} = grep { $_->id != $excl_rule_id } @rules_list; + } + elsif ($next_page == 2 || $next_page == 3) { + my $rule_group; + + if (defined($vars->{rule})) { + $rule_group = $vars->{rule}->rule_group; + } + else { + my $rg_name = $cgi->param('p1_rule_group'); + $rule_group + = Bugzilla::Extension::RuleEngine::RuleGroup->new({name => $rg_name}); + } + + # A list of bug fields, except some which cannot be used + my $fields = Bugzilla->fields({obsolete => 0}); + my %exclude_fields = ( + content => 1, + 'attachstatusdefs.name' => 1, # Obselete in 2.17 + owner_idle_time => 1, # Hard to compute + rh_rule => 1, # Not a field + tag => 1, # User specific field + view => 1, + cf_extra_version => 1, # Extension + cf_extra_component => 1, # Extension + ); + + # We need to see if the field is excluded or not shown on the selected + # classification or components. Sorting is done in the template due + # to field_descs having priority when sorting. + $vars->{bug_field_list} = []; + foreach my $field (@$fields) { + + # Is it in the excluded list? + next if $exclude_fields{$field->name}; + + # Check the fields visiblity + my $visibility_field = $field->visibility_field; + if ( + $visibility_field + && ( $visibility_field->name eq 'classification' + || $visibility_field->name eq 'product') + ) + { + # Check if the classification or product is selected + my @values = $cgi->param('p1_' . $visibility_field->name); + my $visibility_values = $field->visibility_values; + + # If one list is blank, we consider it a match + my $match = scalar(@values) && scalar(@$visibility_values) ? 0 : 1; + foreach my $value (@values) { + + # If the value appears in both lists, there is a match + if (any { $_->name eq $value } @$visibility_values) { + $match = 1; + last; + } + } + + if (!$match) { + + # This field is not visible for any of the products or + # classifications, so skip it + next; + } + } + + push @{$vars->{bug_field_list}}, $field; + } + + if ($next_page == 2) { + + # We need to turn the custom field criteria into an array + foreach my $cnt ( + sort { $a <=> $b } + apply {s/^p2_custom_f(\d+)$/$1/} + grep {/^p2_custom_f(\d+)$/} $cgi->param + ) + { + my $n = $cgi->param("p2_custom_n$cnt") ? 1 : 0; + my $f = trim($cgi->param("p2_custom_f$cnt")); + my $o = trim($cgi->param("p2_custom_o$cnt")); + my $v = trim($cgi->param("p2_custom_v$cnt")); + if ($f || $v || length($o)) { + push @{$vars->{custom_list}}, + {negate => $n, field => $f, op => $o, value => $v}; + } + } + + # As are release flags + my @flag_type_ids + = map { $_->type_id } Bugzilla::Extension::Releases::Release->get_all; + $vars->{release_flag_list} = Bugzilla::FlagType->new_from_list(\@flag_type_ids); + $vars->{flag_group_list} + = [Bugzilla::Extension::RuleEngine::FlagGroup->get_all]; + } + + # For all of these, we just want to get the name + $vars->{bug_status_list} + = _filter_list('bug_status', [Bugzilla::Status->get_all]); + $vars->{keywords_list} = _filter_list('keywords', [Bugzilla::Keyword->get_all]); + $vars->{group_list} = _filter_list('group', [Bugzilla::Group->get_all]); + @{$vars->{closed_status_list}} + = map { $_->name } Bugzilla::Status::closed_bug_statuses; + Bugzilla::active_pools_to_vars($vars); + $vars->{pool_list} = _filter_list('pool', $vars->{active_pools}); + + # Resolution is a bit harder to get + $vars->{resolution_list} = _filter_list('resolution', + Bugzilla::Field->new({name => 'resolution'})->legal_values); + + # Now we need to get a list of values common to all selected products + # First of all get a list of all selected products + my @products = (); + my @product_names = $cgi->param('p1_product'); + my @classification_names = $cgi->param('p1_classification'); + if (scalar(@product_names)) { + foreach my $product (@product_names) { + push @products, Bugzilla::Product->new({name => $product}); + } + } + elsif (scalar(@classification_names)) { + foreach my $classification (@classification_names) { + + # We need to limit products in this classification to those + # that have selected this RuleGroup. + my $class_obj = Bugzilla::Classification->new({name => $classification}); + my @tmp1 = map { $_->id } @{$rule_group->products}; + my @tmp2 = map { $_->id } @{$class_obj->products}; + my $lc = List::Compare->new(\@tmp1, \@tmp2); + my @pids = $lc->get_union; + push @products, @{Bugzilla::Product->new_from_list(\@pids)}; + } + } + else { + @products = @{$vars->{rule}->rule_group->products}; + } + + # Turn this into a list of product ids + my @product_ids = map { $_->id } @products; + + # And get a list of components, milestones and releases (If they + # have different capitalisation, they will be listed muliple times) + $vars->{component_list} = _filter_list('component', + Bugzilla::Component->match({product_id => \@product_ids})); + $vars->{target_milestone_list} = _filter_list('target_milestone', + Bugzilla::Milestone->match({product_id => \@product_ids})); + $vars->{target_release_list} = _filter_list('target_release', + Bugzilla::Release->match({product_id => \@product_ids})); + + # Flags are a bit harder, since we need to get them per product. We + # need to keep them as objects to see if they are requestable + # and requesteeble + my @flag_types = (); + foreach my $product_id (@product_ids) { + push @flag_types, + @{Bugzilla::FlagType::match({product_id => $product_id, target_type => 'bug'}) + }; + } + + # Sort them + @flag_types + = sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } @flag_types; + my %seen_ids = (); + + # Add a unique version of each type to the page + foreach my $flag_type (@flag_types) { + next if $seen_ids{$flag_type->id}++; + push @{$vars->{flag_type_list}}, $flag_type; + } + + # Check that the currently logged in user can view the flags + # used in this rule. + if (defined($vars->{rule} && defined($vars->{rule}->current))) { + _check_can_view_flags($vars->{rule}->current, \@flag_types, $real_user); + } + + } + elsif ($next_page == 4) { + + # We need to get a list of the bugs that this will affect + my $json = _rule_cgi_to_array($cgi); + $vars->{json} = $json; + $vars->{bug_ids} = _get_matching_bugs_for_rule($json->{match}, $vars->{rule}); + $vars->{bug_count} = scalar(@{$vars->{bug_ids}}); + + $vars->{flag_group_map} = {}; + foreach my $flag_group (Bugzilla::Extension::RuleEngine::FlagGroup->get_all) { + $vars->{flag_group_map}{$flag_group->id} = $flag_group->name; + } + } + elsif ($next_page == 5) { + + # This is where we actually make the change. + # Since it is complex, it has its own sub. + _page_rule_write(); + + my $token = $cgi->param('token'); + delete_token($token); + } + else { + ThrowCodeError('invalid_next_page', {next_page => $next_page}); + } + + # Build next_page link and the exclude list + detaint_natural($next_page); + $vars->{next_page} = "pages/ruleengine/edit/page$next_page.html.tmpl"; + $vars->{exclude} = qr{^p${next_page}_|^id$|^next_page$|^this_page$|^safe$}; + + # Set the title (we use rule_title, since title can get munted) + if ($cgi->param('rule_id')) { + my $rule = Bugzilla::Extension::RuleEngine::Rule->new($cgi->param('rule_id')); + $vars->{rule_title} + = ucfirst $cgi->param('action') . " rule '" . $rule->name . "'"; + + Bugzilla->set_user($real_user); + } + else { + $vars->{rule_title} + = 'Add new ' . template_var('terms')->{Bugzilla} . ' Rules Engine rule'; + } + $vars->{rule_title} .= " - step $next_page"; + + return; +} + +sub _filter_list { + my $field = shift; + my $objs = shift; # An array of objects + my $cgi = Bugzilla->cgi; + + if ($cgi->param('next_page') == 3 and $field ne 'keywords') { + + # We need to filter the list to active or currently selected things + my @current = $cgi->param("p3_$field"); + my %current = map { $_ => 1 } @current; + + $objs = [grep { $_->is_active || exists $current{$_->name} } @$objs]; + } + + my @names = uniq(grep { length($_) } map { $_->name } @$objs); + return \@names; +} + +sub _page_rule_write { + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my $data = _rule_cgi_to_array($cgi); + my $rule = undef; + + $dbh->bz_start_transaction; + + # 1. Create or update the rule (a clone is a new rule) + my $rule_id = scalar $cgi->param('rule_id'); + if (scalar($cgi->param('action')) ne 'clone' and $rule_id) { + $rule = Bugzilla::Extension::RuleEngine::Rule->new($rule_id); + $rule->set_name(scalar $cgi->param('p1_name')); + $rule->set_description(scalar $cgi->param('p1_description')); + $rule->set_rule_group_id(scalar $cgi->param('p1_rule_group')); + $rule->set_is_periodic(scalar $cgi->param('p1_is_periodic')); + $rule->set_rule_owners(scalar $cgi->param('p1_rule_owners')); + $rule->set_minor_update(scalar $cgi->param('p1_minor_update')); + } + else { + $rule = Bugzilla::Extension::RuleEngine::Rule->create({ + name => scalar $cgi->param('p1_name'), + description => scalar $cgi->param('p1_description'), + rule_group_id => scalar $cgi->param('p1_rule_group'), # the object converts it to an id + is_periodic => scalar $cgi->param('p1_is_periodic'), + rule_owners => scalar $cgi->param('p1_rule_owners'), + minor_update => scalar $cgi->param('p1_minor_update'), + }); + } + + # Get active from the cgi, and set 0 if play safe is set. + my $active = $cgi->param('p1_active') && !$cgi->param('safe') ? 1 : 0; + + # 2. Create the rule detail. We always create a new row, never update + # an existing row + my $rule_detail = Bugzilla::Extension::RuleEngine::RuleDetail->create({ + match => $data->{match}, + action => $data->{action}, + isactive => $active, + creator_id => Bugzilla->user->id, + rule_id => $rule->id, + }); + + # 3. Update the rule's current_detail_id + $rule->set_current_detail_id($rule_detail->id); + $rule->update(); + + # 4. Update sort order. Unlike other sortkey fields, only the software + # can change it. + # Get all the rules + my @all_rules = @{Bugzilla::Extension::RuleEngine::Rule->match_rules( + {rule_group_id => $rule->rule_group_id} + ) + }; + + # Remove the rule we have just added + my $cur_index = firstidx { $_->id == $rule->id } @all_rules; + my $cur_rule = splice(@all_rules, $cur_index, 1); + + # Find out where it should go (if not specified, before first one + my $new_index + = $cgi->param('p1_run_after') + ? (firstidx { $_->name eq $cgi->param('p1_run_after') } @all_rules) + : -1; + + # And add it back in + splice(@all_rules, $new_index + 1, 0, $cur_rule); + + # Now go through the list and update the sortkey if needed + my $sortkey = 10; + foreach my $arule (@all_rules) { + if ($arule->sortkey != $sortkey) { + $arule->set_sortkey($sortkey); + $arule->update(); + } + $sortkey += 10; + } + + # 5. Do you need to play it safe? + if ($cgi->param('safe')) { + my $bug_ids = _get_matching_bugs_for_rule($data->{match}, $rule); + + # Add this to the job queue + Bugzilla->job_queue->insert( + 'rule_engine', + { + action => 'dryrun', + rule_id => $rule->id, + bug_ids => $bug_ids, + do_periodic => 1, + who => Bugzilla->user->id, + } + ); + } + + # 6. Commit everything. + $dbh->bz_commit_transaction; + + # 7. Send an e-mail. + if ($rule->rule_group->maintainers) { + _send_emails($rule->id); + } + + return; +} + +# Notify the Maintainer(s) that a rule has been created or updated. +sub _send_emails { + my $rule_id = shift; + + my $template = Bugzilla->template; + my ($msg_text, $msg_html); + + # Reload the rule from the database so there is no caching issues. + my $rule = Bugzilla::Extension::RuleEngine::Rule->check({id => $rule_id}); + + my $real_user = Bugzilla->user; + + Bugzilla->set_user(Bugzilla::User->new({name => $rule->rule_group->user_name})); + + my $vars = { + rule => $rule, + rule_state => $rule->current->is_active ? 'enabled' : 'disabled', + date => format_time(scalar localtime, '%a, %d %b %Y %T %z', 'UTC'), + action => scalar Bugzilla->cgi->param('action'), + }; + + $vars->{flag_group_map} = {}; + foreach my $flag_group (Bugzilla::Extension::RuleEngine::FlagGroup->get_all) { + $vars->{flag_group_map}{$flag_group->id} = $flag_group->name; + } + + $template->process("email/rule-change.txt.tmpl", $vars, \$msg_text) + || ThrowTemplateError($template->error()); + $template->process("email/rule-change.html.tmpl", $vars, \$msg_html) + || ThrowTemplateError($template->error()); + + if (utf8::is_utf8($msg_text)) { + utf8::encode($msg_text); + } + + my $text_part = Email::MIME->create( + attributes => { + content_type => "text/plain", + encoding => 'quoted-printable', + charset => 'UTF-8', + }, + body => $msg_text, + ); + + if (utf8::is_utf8($msg_html)) { + utf8::encode($msg_html); + } + + my $html_part = Email::MIME->create( + attributes => { + content_type => "text/html", + charset => 'UTF-8', + encoding => 'quoted-printable', + }, + body => $msg_html, + ); + + my @rule_owners = map { $_->login } @{$rule->rule_owners}; + my @maintainers = map { $_->login } @{$rule->rule_group->maintainers} + if (@{$rule->rule_group->maintainers}); + + foreach my $maintainer (uniq(@maintainers, @rule_owners)) { + my $msg_header; + $vars->{to} = $maintainer; + + $template->process("email/rule-change-header.txt.tmpl", $vars, \$msg_header) + || ThrowTemplateError($template->error()); + + # TT trims the trailing newline, and threadingmarker may be ignored. + my $email = Email::MIME->new("$msg_header\n"); + $email->charset_set('UTF-8'); + + my $user = Bugzilla::User->new({name => $maintainer}); + if ($user->setting('email_format') eq 'html') { + $email->content_type_set('multipart/alternative'); + $email->parts_set([$text_part, $html_part]); + } + else { + $email->content_type_set('text/plain'); + $email->parts_set([$text_part]); + } + MessageToMTA($email); + } + + Bugzilla->set_user($real_user); + + return; +} + +sub _rule_to_cgi { + my $rule = shift; + my $rule_detail = $rule->current; + my %cgi = (); + + # Page One + $cgi{p1_name} = $rule->name; + $cgi{p1_description} = $rule->description; + $cgi{p1_active} = $rule_detail->is_active ? 'on' : ''; + $cgi{p1_minor_update} = $rule->minor_update ? 'on' : ''; + $cgi{p1_rule_group} = $rule->rule_group->name; + $cgi{p1_run_after} = $rule->run_after; + $cgi{p1_is_periodic} = $rule->periodic_type; + $cgi{p1_rule_owners} = join ', ', map { $_->login } @{$rule->rule_owners}; + + # Page two (and classification and product for page one) + while (my ($key, $value) = each %{$rule_detail->match}) { + if ($key eq 'flag_types') { + foreach my $row (@$value) { + $cgi{'p2_flag_' . $row->{name}} = $row->{status}; + } + } + elsif ($key eq 'flag_groups') { + foreach my $row (@$value) { + $cgi{'p2_flaggroup_' . $row->{id}} = $row->{status}; + $cgi{'p2_word_flaggroup_' . $row->{id}} = $row->{word}; + } + } + elsif ($key eq 'custom') { + my $x = 1; + foreach my $row (@$value) { + $cgi{"p2_custom_n$x"} = $row->{negate}; + $cgi{"p2_custom_f$x"} = $row->{field}; + $cgi{"p2_custom_o$x"} = $row->{op}; + $cgi{"p2_custom_v$x"} = $row->{value}; + ++$x; + } + } + elsif ($key eq 'flags_order') { + $cgi{p2_flags_order} = $value->{before}; + $cgi{p2_flags_order_after} = $value->{after}; + } + elsif ($key eq 'classification' or $key eq 'product') { + + # These values are on page one + $cgi{"p1_$key"} = $value->{values}; + } + else { + $cgi{"p2_$key"} = $value->{values}; + if (my $word = $value->{word}) { + $cgi{"p2_word_$key"} = $word; + } + } + } + + # Page three + foreach my $key (keys %{$rule_detail->action}) { + my $value = $rule_detail->action->{$key}; + if ($key eq 'flag_types') { + while (my ($flag, $v) = each %$value) { + my $status = $v->{status} eq 'X' ? '(unset)' : $v->{status}; + $cgi{"p3_flag_$flag"} = $status; + if (defined $v->{condition}) { + $cgi{"p3_word_flag_$flag"} = FLAG_COND_TO_TEXT->{$v->{condition}}; + } + if (defined $v->{requestee}) { + $cgi{"p3_flag_${flag}_requestee"} = $v->{requestee}; + } + if (defined $v->{ignore}) { + $cgi{"p3_flag_${flag}_ignore"} = 1; + } + } + } + elsif ($key eq 'comment') { + $cgi{'p3_comment'} = $value->{text}; + $cgi{'p3_word_comment'} = $value->{private} ? 'Private' : 'Public'; + } + elsif (ref $value eq 'HASH') { + + # Action is set, add or remove. Applies to multi value fields + $cgi{"p3_$key"} = $value->{values}; + $cgi{"p3_word_$key"} = ucfirst $value->{action}; + } + else { + # Set the field to a single value + $cgi{"p3_$key"} = $value; + } + + if ($key eq 'cc' or $key eq 'notify') { + + # We need to flatten these arrays + next unless ref $cgi{"p3_$key"}; + $cgi{"p3_$key"} = join(' ', @{$cgi{"p3_$key"}}); + } + } + + return \%cgi; +} + +sub _rule_cgi_to_array { + my $cgi = shift; + my %rule = (info => {}, match => {}, action => {}); + my @errors; + my $num_matches = 0; + my $num_messages = 0; + + $cgi->param('this_page') =~ m/(\d*)/; + my $this_page = $1; + + # Page One + my $info = $rule{info}; + $info->{name} = trim(scalar $cgi->param('p1_name')); + $info->{description} = trim(scalar $cgi->param('p1_description')); + $info->{active} = scalar $cgi->param('p1_active') ? 1 : 0; + $info->{rule_group} = trim(scalar $cgi->param('p1_rule_group')); + $info->{run_after} = trim(scalar $cgi->param('p1_run_after')); + $info->{is_periodic} = scalar $cgi->param('p1_is_periodic'); + $info->{minor_update} = scalar $cgi->param('p1_minor_update'); + $info->{rule_owners} = trim(scalar $cgi->param('p1_rule_owners')); + + my $match = $rule{match}; + + # We only add these values if specified + foreach my $field (qw(classification product)) { + my @values = $cgi->param("p1_$field"); + if (scalar @values) { + $match->{$field}{values} = \@values; + } + } + + # Page two + foreach my $field ( + apply {s/^p2_(?!flag|custom|word_)//} + grep {/^p2_(?!flag|custom|word_)/} $cgi->param + ) + { + # The skipped fields are paired up with another field + # (e.g. word_component is added by component) + + my @values = $cgi->param("p2_$field"); + next unless scalar @values; + next + if ( + scalar(@values) == 1 + && $values[0] eq '' + && ( $field eq 'bug_status' + || $field eq 'component_list' + || $field eq 'resolution') + ); + + $match->{$field}{values} = \@values; + if (my $word = $cgi->param("p2_word_$field")) { + $match->{$field}{word} = $word; + } + } + + # Page two flag order + if ($cgi->param('p2_flags_order')) { + my $before = trim($cgi->param('p2_flags_order')); + my $after = trim($cgi->param('p2_flags_order_after')); + trick_taint($before); + trick_taint($after); + $match->{flags_order} = {before => $before, after => $after}; + } + + # Page two flags + foreach my $field (sort grep {/^p2_flag_/} $cgi->param) { + my $flag = substr($field, 8); + push @{$match->{flag_types}}, {name => $flag, status => [$cgi->param($field)]}; + } + + # Page two flags groups + foreach my $field (sort grep {/^p2_(?!word_)flaggroup_/} $cgi->param) { + my $flaggroup_id = substr($field, 13); + + # Inject '_word' after 'p2_' so we get 'p2_word_foo' + my $word_field = $field; + substr($word_field, 2, 0, '_word'); + + push @{$match->{flag_groups}}, + { + id => $flaggroup_id, + word => scalar $cgi->param($word_field), + status => [$cgi->param($field)], + }; + } + + # Page two custom search. We only want to display rows that are complete + foreach my $cnt ( + sort { $a <=> $b } + apply {s/^p2_custom_f(\d+)$/$1/} + grep {/^p2_custom_f(\d+)$/} $cgi->param + ) + { + my $n = $cgi->param("p2_custom_n$cnt") ? 1 : 0; + my $f = trim($cgi->param("p2_custom_f$cnt")) || ''; + my $o = trim($cgi->param("p2_custom_o$cnt")) || ''; + my $v = trim($cgi->param("p2_custom_v$cnt")) || ''; + if ($f && $v && ($v =~ /null/ || length($o))) { + push @{$match->{custom}}, {negate => $n, field => $f, op => $o, value => $v}; + } + elsif ($f && $o && any { $o eq $_ } (qw(isempty isnotempty))) { + push @{$match->{custom}}, {field => $f, op => $o, negate => $n}; + } + } + + # Page three + my $action = $rule{action}; + foreach my $field ( + apply {s/^p3_(?!flag|comment)//} + grep {/^p3_(?!flag|comment)/} $cgi->param + ) + { + if ($field =~ /^word_(.+)$/) { + + # Special case for unset, which has no values + if ($cgi->param("p3_$field") eq 'Unset') { + $action->{$1} = {action => 'unset', values => []}; + $num_matches++; + } + + # For other values, we pick this up when doing the non _word field + next; + } + + my @values = $cgi->param("p3_$field"); + my $cond = $cgi->param("p3_word_$field"); + + if ($field eq 'cc' || $field eq 'notify') { + + # User has entered them comma or space seperated + # strip off any leading or trailing whitespace and + # convert into an array + $values[0] =~ s/^\s+//; + $values[0] =~ s/\s+$//; + + @values = split /,\s*/, $values[0]; + } + + if ($cond) { + + # This field has multiple vales (cond = set, add, remove, unset) + if ($cond ne 'Do not change' and scalar(@values)) { + $action->{$field} = {action => lc($cond), values => \@values}; + + ## REDHAT EXTENSION BEGIN 1212703 + if ($this_page >= 3) { + my $vals; + if (ref($match->{$field}{values}) eq 'ARRAY') { + $vals = join '|', @{$match->{$field}{values}}; + } + else { + foreach my $hash (@{$match->{custom}}) { + if ($hash->{field} eq $field) { + $vals = $hash->{value}; + last; + } + } + } + if (ref($match->{$field}{values}) eq 'ARRAY') { + my $match_found = 0; + foreach my $value (@{$action->{$field}{values}}) { + if ( + any { $_ eq $value } + @{$match->{$field}{values}} + ) + { + $match_found = 1; + } + } + unless ($match_found) { + push(@errors, + {'field' => $field, 'value' => join(' ', @{$action->{$field}{values}})}); + } + } + elsif ( + any { $field eq $_ } + qw(assigned_to qa_contact docs_contact) + ) + { + my $match_found = 0; + + # When it comes to the assignee type fields, the only way (currently) + # to match them is via a 'custom field' + foreach my $custom (@{$match->{custom}}) { + if ($custom->{field} eq $field) { + $match_found = 1; + last; + } + } + unless ($match_found) { + push(@errors, + {field => $field, value => join(',', @{$action->{$field}{values}})}); + } + } + elsif ( + ( + $vals eq '' + || any { $_ =~ /\Q$vals\E/ } + @{$action->{$field}{values}} + ) + && (!$field =~ /agile/ && any { $_ =~ /agile/ } keys %$match) + ) + { + push(@errors, + {'field' => $field, 'value' => join(' ', @{$action->{$field}{values}})}); + } + $num_matches++; + } + + ## REDHAT EXTENSION END 1212703 + } + } + elsif ($field eq 'notify' and scalar(@values)) { + + # For notifies, we store the list of e-mail addresses as an array + $action->{$field} = \@values; + $num_messages++ if ($this_page >= 3); + } + elsif ($values[0]) { + + # We are changing a single value (e.g. bug status, or target milestone) + $action->{$field} = $values[0]; + + ## REDHAT EXTENSION BEGIN 1212703 + if ( + $this_page >= 3 && ( + !exists($match->{$field}) + || !exists($match->{$field}{values}) + || none { $action->{$field} } + @{$match->{$field}{values}} + ) + ) + { + my $found = 0; + foreach my $custom (@{$match->{custom}}) { + if ( + $custom->{field} eq $field + && ( + ( + defined($custom->{value}) + && length($custom->{value}) + && $action->{$field} =~ /\Q$custom->{value}\E/ + ) + || (defined($custom->{op}) && $custom->{op} ne '') + ) + ) + { + $found = 1; + last; + } + } + push(@errors, {'field' => $field, 'value' => $action->{$field}}) + unless ($found); + } + if ( + $this_page >= 3 && ( + exists($match->{$field}) + && exists($match->{$field}{values}) + && any { $action->{$field} eq $_ } + @{$match->{$field}{values}} + ) + ) + { + push( + @errors, + { + 'field' => $field, + 'value' => $action->{$field}, + 'matches' => $match->{$field}{values} + } + ); + ThrowUserError('insufficient_change_detected', {errors => \@errors}); + } + + $num_matches++; + + ## REDHAT EXTENSION END 1212703 + } + } + + if (my $comment = $cgi->param("p3_comment")) { + my $private = $cgi->param('p3_word_comment') eq 'Private' ? 1 : 0; + $action->{comment} = {private => $private, text => $comment}; + $num_messages++ if ($this_page >= 3); + } + + foreach my $flag ( + apply {s/^p3_word_flag_(.+)$/$1/} + grep {/^p3_word_flag_(.+)$/} $cgi->param + ) + { + my $value = $cgi->param("p3_flag_$flag"); + my $cond = $cgi->param("p3_word_flag_$flag"); + if ($cond and $cond ne 'Do not change' and $value) { + $value = 'X' if $value eq '(unset)'; + $action->{flag_types}{$flag} + = {condition => FLAG_TEXT_TO_COND->{$cond}, status => $value}; + if ($value eq '?' and my $requestee = $cgi->param("p3_flag_${flag}_requestee")) + { + $action->{flag_types}{$flag}{requestee} = $requestee; + } + if ($cgi->param("p3_flag_${flag}_ignore")) { + $action->{flag_types}{$flag}{ignore} = 1; + } + + ## REDHAT EXTENSION BEGIN 1212703 + if ($this_page >= 3) { + my $found = 0; + foreach my $hash (@{$match->{flag_types}}) { + if ($hash->{name} eq $flag) { + if ($hash->{status} ne $action->{flag_types}{$flag}{status}) { + $found = 1; + last; + } + } + } + unless ($found) { + foreach my $hash (@{($match->{flag_groups})}) { + my $flag_group = Bugzilla::Extension::RuleEngine::FlagGroup->new($hash->{id}); + + my $match_regexp = $flag_group->current->theregexp; + my $flagtypes = $flag_group->current->flag_types; + + if (defined($flagtypes) && scalar(@$flagtypes)) { + if (any { $flag eq $_->name } @$flagtypes) { + $found++; + last; + } + } + elsif ($match_regexp && $flag =~ qr{$match_regexp}) { + $found++; + last; + } + } + push(@errors, + {'field' => "flag $flag", 'value' => $action->{flag_types}{$flag}{status}}) + unless ($found); + } + + $num_matches++; + } + + ## REDHAT EXTENSION END 1212703 + } + } + + if ($num_messages && !$num_matches) { + ThrowUserError('messages_without_action_detected', {errors => \@errors}); + } + if (@errors && scalar(@errors) == $num_matches) { + ThrowUserError('recursive_action_detected', {errors => \@errors}); + } + + return \%rule; +} + +sub _get_matching_bugs_for_rule { + my $match = shift; + my $rule = shift; + my %params = (); + my $count = 1; + + foreach my $field (keys %$match) { + + # These are handled seperately + next + if $field eq 'component_list' + || $field eq 'custom' + || $field eq 'flag_types' + || $field eq 'flag_groups' + || $field eq 'flags_order'; + + if (exists $match->{$field}{word} and $match->{$field}{word} ne 'is any of') { + _add_to_params( + \%params, \$count, $field, + $match->{$field}{word}, + $match->{$field}{values} + ); + } + elsif ($field eq 'bug_status' + && scalar(@{$match->{$field}{values}}) == 1 + && $match->{$field}{values}[0] eq 'All open') + { + $params{$field} = '__open__'; + } + else { + $params{$field} = $match->{$field}{values}; + } + } + + # Component lists are more tricky. Thankfully in bug 731285 we allowed + # this + if (exists $match->{component_list}) { + my $word = $match->{component_list}{word}; + my $value = $match->{component_list}{values}[0]; + + if ($word eq 'approved') { + _add_to_params(\%params, \$count, 'component_a', 'is any of', [$value]); + } + elsif ($word eq 'capacity') { + _add_to_params(\%params, \$count, 'component_c', 'is any of', [$value]); + } + elsif ($word ne 'both' && $word ne 'neither') { + ThrowCodeError('invalid_word', {word => $word}); + } + else { + my $custom_word = $word eq 'both' ? 'is any of' : 'is not any of'; + _add_to_params(\%params, \$count, 'component_c', $custom_word, + [$value, $value]); + + # We want to change one of the fields to the approved list + # For 'neither' this will be the first one, for both it will be the + # second one (f count-1 will be 'CP') + $params{'f' . ($count - 2)} = 'component_a'; + } + } + + foreach my $flag (@{$match->{flag_types} || []}) { + if (any { $_ eq 'X' } @{$flag->{status}}) { + + # Since we cannot search for flagX in the custom search, we want + # to search for opposite flags + my @flags = (); + foreach my $status (qw(? + -)) { + unless (any { $_ eq $status } @{$flag->{status}}) { + push @flags, $flag->{name} . $status; + } + } + _add_to_params(\%params, \$count, 'flagtypes.name', 'is not any of', \@flags); + } + else { + my @flags = map { $flag->{name} . $_ } @{$flag->{status}}; + _add_to_params(\%params, \$count, 'flagtypes.name', 'is any of', \@flags); + } + } + + foreach my $custom (@{$match->{custom} || []}) { + next if ($custom->{value} && $custom->{value} =~ m/%%([^%]+)%%/); # Skip wildcards + $params{"n$count"} = $custom->{negate}; + $params{"f$count"} = $custom->{field}; + $params{"o$count"} = $custom->{op}; + $params{"v$count"} = $custom->{value}; + ++$count; + } + + my $search = Bugzilla::Search->new('fields' => ['bug_id'], params => \%params,); + my $data = $search->data; + my @bug_ids = map { $_->[0] } @$data; + + return \@bug_ids if scalar(@bug_ids) == 0; + + my @matching_bug_ids = (); + my $bugs = Bugzilla::Bug->new_from_list(\@bug_ids); + + foreach my $bug (@$bugs) { + next + unless Bugzilla::Extension::RuleEngine::Job->_rule_is_match($bug, + $rule // undef, $match); + push @matching_bug_ids, $bug->id; + } + + return \@matching_bug_ids; +} + +sub _add_to_params { + + # $count is a reference to a scalar + my ($params, $count, $field, $word, $values) = @_; + + if ($word eq 'is any of' && scalar(@$values) > 1) { + + # For 'is any of', do AND (field = value1 OR ... OR field = valuen) + $params->{"f$$count"} = 'OP'; + $params->{"j$$count"} = 'OR'; + ++$$count; + } + + foreach my $value (@$values) { + $params->{"f$$count"} = $field; + $params->{"o$$count"} = 'equals'; + $params->{"v$$count"} = $value; + $params->{"n$$count"} = 1 if $word eq 'is not any of'; + ++$$count; + } + + if ($word eq 'is any of' && scalar(@$values) > 1) { + $params->{"f$$count"} = 'CP'; + ++$$count; + } + + ++$$count; + + return; +} + +# This function will check if the user has permissions to view all the flags +# that are present on the rule. If they do not then we die with an error. +# This prevents flags from silently getting clobbered when a user with incorrect +# permissions attempts to edit a bug. +# +# Check the rule_detail of the rule currently being edited +# to see if the rule contains flags that do not exist in +# the flag_type_list. +# +# $rule_detail - a RuleDetail object +# $flag_types - A list of FlagType objects that is visible TO THE LOGGED IN USER +# +sub _check_can_view_flags { + my ($rule_detail, $flag_types, $real_user) = @_; + + return () if (Bugzilla->user->in_group('admin')); + + my $match = from_json($rule_detail->match_json); + my $action = from_json($rule_detail->action_json); + + # Extract the flag names from the rule defintion as stored in the database. + my @flag_names; + push @flag_names, map { $_->{name} } @{$match->{flag_types}} + if defined $match->{flag_types}; + push @flag_names, keys %{$action->{flag_types}} + if defined $action->{flag_types}; + + # We look at the list of FlagType objects stored in $flag_types and compare + # it to the list of flag names in @flag_names. $flag_types only contains + # flags that are visible to the logged in user. + # Therefore if a flag is in the rule definition, but not $flag_types, the + # rule uses a flag that is not visible to the logged in user. + # + # In this case we throw an error and do not allow the user to proceed with + # editing the rule. + my @no_access; + + foreach my $flag_name (uniq @flag_names) { + push(@no_access, $flag_name) if (none { $_->name eq $flag_name } @$flag_types); + } + + if (@no_access) { + my $login = Bugzilla->user->login; + Bugzilla->set_user($real_user); + + ThrowUserError('flag_cannot_see', {user_name => $login, flags => \@no_access}); + } + + return; +} |