summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/RuleEngine/lib/Pages.pm')
-rw-r--r--extensions/RuleEngine/lib/Pages.pm1631
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;
+}