summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/RuleEngine/lib/Rule.pm')
-rw-r--r--extensions/RuleEngine/lib/Rule.pm755
1 files changed, 755 insertions, 0 deletions
diff --git a/extensions/RuleEngine/lib/Rule.pm b/extensions/RuleEngine/lib/Rule.pm
new file mode 100644
index 000000000..8cc386e0b
--- /dev/null
+++ b/extensions/RuleEngine/lib/Rule.pm
@@ -0,0 +1,755 @@
+# 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::Rule;
+
+use strict;
+use warnings;
+use 5.10.1;
+
+use Scalar::Util qw(blessed looks_like_number);
+use List::MoreUtils qw(any);
+
+use Bugzilla::Constants;
+use Bugzilla::Field;
+use Bugzilla::Util;
+use Bugzilla::Error;
+
+use Bugzilla::Extension::RuleEngine::RuleDetail;
+use Bugzilla::Extension::RuleEngine::RuleGroup;
+
+use base qw(Bugzilla::Object);
+
+###############################
+#### Initialization ####
+###############################
+
+use constant DB_TABLE => 'rh_rule';
+use constant LIST_ORDER => 'sortkey, name';
+use constant EMAIL_SPLIT_RX => qr/(?:(?:,\s+)|,|\s+)/;
+
+use constant DB_COLUMNS => qw(
+ id
+ name
+ description
+ creation_ts
+ rule_group_id
+ current_detail_id
+ sortkey
+ is_periodic
+ minor_update
+);
+
+use constant UPDATE_COLUMNS => qw(
+ name
+ description
+ rule_group_id
+ current_detail_id
+ sortkey
+ is_periodic
+ minor_update
+);
+
+use constant VALIDATORS => {
+ name => \&_check_name,
+ description => \&_check_description,
+ creation_ts => \&_check_creation_ts,
+ rule_group_id => \&_check_rule_group_id,
+ current_detail_id => \&_check_current_detail_id,
+ sortkey => \&_check_sortkey,
+ is_periodic => \&_check_is_periodic,
+ minor_update => \&Bugzilla::Object::check_boolean,
+};
+
+###############################
+#### Validators ####
+###############################
+
+sub _check_creation_ts {
+ return Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+}
+
+sub _check_current_detail_id {
+ my ($invocant, $id) = @_;
+
+ $id = trim($id);
+
+ # Return blank if specified, or we are creating this rule
+ return '' if !$invocant || $id eq '';
+
+ # Check that the ruledetail belongs to this rule
+ my $object = Bugzilla::Extension::RuleEngine::RuleDetail->check({id => $id});
+ if ($object->rule_id != $invocant->id) {
+ ThrowCodeError(
+ 'rule_id_mismatch',
+ {
+ other_id => $object->id,
+ other_rule_id => $object->rule_id,
+ this_rule_id => $invocant->id
+ }
+ );
+ }
+ return $object->id;
+}
+
+sub _check_description {
+ my ($invocant, $description) = @_;
+
+ $description = trim($description || '');
+ return $description;
+}
+
+sub _check_rule_group_id {
+ my ($invocant, $value) = @_;
+ my $old_id = blessed($invocant) ? $invocant->rule_group_id : 0;
+ $value = trim($value);
+
+ my $object;
+
+ if (looks_like_number($value)) {
+ $object = Bugzilla::Extension::RuleEngine::RuleGroup->check({id => $value});
+ }
+ else {
+ $object = Bugzilla::Extension::RuleEngine::RuleGroup->check({name => $value});
+ }
+
+ if ((!$old_id || $object->id != $old_id) && !$object->is_active) {
+ ThrowUserError('value_inactive',
+ {class => ref($object), value => $object->name});
+ }
+ return $object->id;
+}
+
+sub _check_name {
+ my ($invocant, $name) = @_;
+
+ $name = trim($name);
+ $name || ThrowUserError('rule_not_specified');
+
+ if (length($name) > 64) {
+ ThrowUserError('rule_name_too_long', {'name' => $name});
+ }
+
+ my $rule = Bugzilla::Extension::RuleEngine::Rule->new({name => $name});
+ if ($rule && (!ref $invocant || $rule->id != $invocant->id)) {
+ ThrowUserError("rule_already_exists", {name => $rule->name});
+ }
+ return $name;
+}
+
+sub _check_sortkey {
+ my ($invocant, $sortkey) = @_;
+
+ $sortkey ||= 0;
+ my $stored_sortkey = $sortkey;
+ if (!detaint_natural($sortkey) || $sortkey > MAX_SMALLINT) {
+ ThrowUserError('rule_invalid_sortkey', {'sortkey' => $stored_sortkey});
+ }
+ return $sortkey;
+}
+
+sub _check_is_periodic {
+ my (undef, $number) = @_;
+
+ if ($number >= 0 && $number <= 2) {
+ return $number;
+ }
+
+ # Make the rule on-change if they pick something invalid
+ return 0;
+}
+
+###############################
+#### Methods ####
+###############################
+
+sub set_name { $_[0]->set('name', $_[1]); return; }
+sub set_description { $_[0]->set('description', $_[1]); return; }
+sub set_rule_group_id { $_[0]->set('rule_group_id', $_[1]); return; }
+sub set_current_detail_id { $_[0]->set('current_detail_id', $_[1]); return; }
+sub set_sortkey { $_[0]->set('sortkey', $_[1]); return; }
+sub set_is_periodic { $_[0]->set('is_periodic', $_[1]); return; }
+sub set_minor_update { $_[0]->set('minor_update', $_[1]); return; }
+
+sub rule_group {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{rule_group}) {
+ $self->{rule_group}
+ = Bugzilla::Extension::RuleEngine::RuleGroup->new($self->rule_group_id);
+ }
+ return $self->{rule_group};
+}
+
+sub current {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!defined $self->{current_detail}) {
+ $self->{current_detail}
+ = Bugzilla::Extension::RuleEngine::RuleDetail->new($self->current_detail_id);
+ }
+ return $self->{current_detail};
+}
+
+sub all_details {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ if (!$self->{'all_details'}) {
+ my $ids = $dbh->selectcol_arrayref(
+ q{
+ SELECT id FROM rh_rule_detail
+ WHERE rule_id = ?
+ ORDER BY creation_ts, id}, undef, $self->id
+ );
+
+ $self->{'all_details'}
+ = Bugzilla::Extension::RuleEngine::RuleDetail->new_from_list($ids);
+ }
+ return $self->{'all_details'};
+}
+
+sub set_rule_owners {
+ my $self = shift;
+ my $owners = shift; # arrayref of Bugzilla::User objects or comma seperated string.
+
+ unless (ref $owners) {
+ my $strings = _split_rule_owners($owners);
+ $owners = [map { Bugzilla::User->check({name => $_}) } @$strings];
+ }
+
+ $self->{rule_owners} = $owners;
+
+ return $self->{rule_owners};
+}
+
+###############################
+#### Accessors ####
+###############################
+
+sub description { return $_[0]->{'description'}; }
+sub creation_ts { return $_[0]->{'creation_ts'}; }
+sub rule_group_id { return $_[0]->{'rule_group_id'}; }
+sub current_detail_id { return $_[0]->{'current_detail_id'}; }
+sub sortkey { return $_[0]->{'sortkey'}; }
+sub periodic_type { return $_[0]->{is_periodic}; }
+
+sub is_periodic {
+ my $self = shift;
+
+ return $self->{'is_periodic'} == 1 || $self->{is_periodic} == 2;
+}
+
+sub minor_update {
+ my $self = shift;
+
+ return $self->{'minor_update'};
+}
+
+sub run_type {
+ my $self = shift;
+
+ my $type = 'On bug change';
+
+ $type = 'Periodically' if ($self->{is_periodic} == 1);
+ $type = 'On bug change & periodically' if ($self->{is_periodic} == 2);
+
+ return ($type);
+}
+
+sub is_bug_change {
+ my $self = shift;
+
+ return $self->{is_periodic} == 0 || $self->{is_periodic} == 2;
+}
+
+sub rule_owners {
+ my $self = shift;
+
+ return $self->{'rule_owners'} if exists $self->{rule_owners};
+
+ my $query = 'SELECT profile_id FROM rh_rule_owner WHERE rule_id = ?';
+ my $rule_owners = Bugzilla->dbh->selectcol_arrayref($query, undef, $self->id);
+
+ $self->{rule_owners} = Bugzilla::User->new_from_list($rule_owners);
+
+ return $self->{rule_owners};
+}
+
+sub last_used {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = q{
+ SELECT MAX(bug_when)
+ FROM bugs_activity
+ WHERE fieldid = (SELECT id FROM fielddefs WHERE name = 'rh_rule')
+ AND added = ? };
+
+ my $when = Bugzilla->dbh->selectrow_array($query, undef, $self->id);
+ return $when || '';
+}
+
+sub use_count {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = q{
+ SELECT COUNT(*)
+ FROM bugs_activity
+ WHERE fieldid = (SELECT id FROM fielddefs WHERE name = 'rh_rule')
+ AND added = ? };
+
+ my $count = Bugzilla->dbh->selectrow_array($query, undef, $self->id);
+ return $count || 0;
+}
+
+sub use_since_change {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = q{
+ SELECT COUNT(*)
+ FROM bugs_activity
+ WHERE fieldid = (SELECT id FROM fielddefs WHERE name = 'rh_rule')
+ AND added = ?
+ AND bug_when >= ?};
+
+ my $count = Bugzilla->dbh->selectrow_array($query, undef, $self->id,
+ $self->current->creation_ts);
+ return $count || 0;
+}
+
+###############################
+#### Helpers ####
+###############################
+
+# inactive: If false, will only show rules that are active OR have been changed in the last 7 days
+# If true, returns all rules.
+# product: The name of the product to show rules for
+
+sub match_rules {
+ my ($self, $criteria) = @_;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = q{
+ SELECT r.id
+ FROM rh_rule r JOIN rh_rule_detail d ON (r.current_detail_id = d.id)
+ WHERE 1=1
+ };
+
+ # The inactive criteria can be done in SQL
+ if (!$criteria->{inactive}) {
+
+ # Only show active rules and those changed in the last 7 days
+ my $sevendays = $dbh->sql_date_math('NOW()', '-', 7, 'DAY');
+ $query .= " AND (d.isactive = 1 OR d.creation_ts > $sevendays)";
+ }
+
+ if ($criteria->{rule_group_id}) {
+ $query .= ' AND rule_group_id = ' . $dbh->quote($criteria->{rule_group_id});
+ }
+
+ if ($criteria->{is_bug_change}) {
+ $query .= ' AND is_periodic IN (0,2) ';
+ }
+
+ if ($criteria->{isactive}) {
+
+ # Only show active rules
+ $query .= " AND (d.isactive = 1)";
+ }
+
+ if ($criteria->{is_periodic}) {
+ $query .= " AND is_periodic IN (1,2)";
+ }
+
+ # Get the rule objects.
+ my $rule_ids = $dbh->selectcol_arrayref($query);
+ my $unsorted_rules = $self->new_from_list($rule_ids);
+
+ # Do the sort. We can't do in SQL, since new_from_list ignores this
+ # After everything else, we always sort by sortkey and id
+ my $s = $criteria->{sort_order} || 'run';
+ my @rules = sort {
+ ($s eq 'alpha' && $a->name cmp $b->name)
+ || ($s eq 'create' && $a->creation_ts cmp $b->creation_ts)
+ || ($s eq 'update' && $a->current->creation_ts cmp $b->current->creation_ts)
+ || ($s eq 'last' && $a->last_used cmp $b->last_used)
+ || ($s eq 'hits' && $a->use_count <=> $b->use_count)
+ || $a->sortkey <=> $b->sortkey
+ || lc($a->name) cmp lc($b->name)
+ } @$unsorted_rules;
+
+ # Do you need to reverse the list?
+ @rules = reverse @rules if $criteria->{reverse};
+
+ # If there are no product restrictions, we can return now.
+ if (!$criteria->{product}) {
+ return \@rules;
+ }
+
+ # Get the product name and its classification
+ my $product_name = $criteria->{product};
+ my $product = Bugzilla::Product->check($product_name);
+ my $classification_name = $product->classification->name;
+
+ my @matching_rules = ();
+ foreach my $rule (@rules) {
+
+ # Get the product and/or classification restrictions on this rule
+ my $products = $rule->current->match->{product}{values} || [];
+ my $classifications = $rule->current->match->{classification}{values} || [];
+
+ # If a product restriction is specified, this product must be in the list
+ if (scalar @$products) {
+ unless (any { $_ eq $product_name } @$products) {
+ next;
+ }
+ }
+
+ # Ditto for the classification
+ if (scalar @$classifications) {
+ unless (any { $_ eq $classification_name } @$classifications) {
+ next;
+ }
+ }
+
+ # We can add this rule to the list
+ push @matching_rules, $rule;
+ }
+
+ return \@matching_rules;
+
+}
+
+sub run_after {
+ my $self = shift;
+
+ if (not exists $self->{run_after}) {
+ my $query = q{
+ SELECT name
+ FROM rh_rule
+ WHERE (sortkey < ? OR (sortkey = ? AND name < ?))
+ AND rule_group_id = ?
+ ORDER BY sortkey DESC, name DESC
+ LIMIT 1
+ };
+
+ my ($rule_name)
+ = Bugzilla->dbh->selectrow_array($query, undef, $self->sortkey,
+ $self->sortkey, $self->name, $self->rule_group_id);
+
+ $self->{run_after} = defined $rule_name ? $rule_name : '';
+ }
+
+ return $self->{run_after};
+}
+
+sub last7days {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = q{
+ SELECT COUNT(*)
+ FROM bugs_activity
+ WHERE fieldid = (SELECT id FROM fielddefs WHERE name = 'rh_rule')
+ AND added = ?
+ AND bug_when > } . $dbh->sql_date_math('NOW()', '-', 7, 'DAY');
+
+ my $count = Bugzilla->dbh->selectrow_array($query, undef, $self->id);
+ return $count || 0;
+}
+
+sub last24hours {
+ my $self = shift;
+ my $dbh = Bugzilla->dbh;
+
+ my $query = q{
+ SELECT COUNT(*)
+ FROM bugs_activity
+ WHERE fieldid = (SELECT id FROM fielddefs WHERE name = 'rh_rule')
+ AND added = ?
+ AND bug_when > } . $dbh->sql_date_math('NOW()', '-', 24, 'HOUR');
+
+ my $count = Bugzilla->dbh->selectrow_array($query, undef, $self->id);
+ return $count || 0;
+}
+
+sub set_active {
+ my $self = shift;
+ my $is_active = shift;
+
+ # 1. Create the rule detail. We always create a new row, never update
+ # an existing row
+ my $rule_detail = Bugzilla::Extension::RuleEngine::RuleDetail->create({
+ match => $self->current->match,
+ action => $self->current->action,
+ isactive => $is_active,
+ creator_id => Bugzilla->user->id,
+ rule_id => $self->id,
+ });
+
+ # 2. Update the rule's current_detail_id
+ $self->set_current_detail_id($rule_detail->id);
+ $self->update();
+
+ # No particular reason to return this
+ return $rule_detail->id;
+}
+
+# Override Bugzilla::Object::create so we can handle the rule owners.
+sub create {
+ my ($class, $params) = @_;
+
+ # Rule owners isn't stored in the rh_rule table, it's managed seperately
+ my $rule_owners = delete $params->{rule_owners};
+
+ # Construct the Rule object itself
+ my $self = $class->SUPER::create($params);
+
+ return $self unless defined $rule_owners;
+
+ # If we have any rule owners, set them now.
+ my $rule_owner_objs = $self->set_rule_owners($rule_owners);
+
+ if (defined($rule_owner_objs)) {
+ my $dbh = Bugzilla->dbh;
+ foreach my $rule_owner (@$rule_owner_objs) {
+ $dbh->do('INSERT INTO rh_rule_owner (profile_id, rule_id) VALUES (?,?)',
+ undef, $rule_owner->id, $self->id);
+ }
+ }
+
+ return $self;
+}
+
+sub update {
+ my $self = shift;
+
+ my ($changes, $old_rule) = $self->SUPER::update(@_);
+
+ # Perform the update for the rule_owners.
+
+ my @old_owners = map { $_->id } @{$old_rule->rule_owners};
+ my @new_owners = map { $_->id } @{$self->rule_owners};
+
+ my ($removed_owner, $added_owner) = diff_arrays(\@old_owners, \@new_owners);
+
+ my $dbh = Bugzilla->dbh;
+
+ if (scalar @$removed_owner) {
+ $dbh->do(
+ 'DELETE FROM rh_rule_owner WHERE rule_id = ? AND '
+ . $dbh->sql_in('profile_id', $removed_owner),
+ undef, $self->id
+ );
+ }
+
+ foreach my $added_owner_id (@$added_owner) {
+ $dbh->do('INSERT INTO rh_rule_owner (profile_id, rule_id) VALUES (?,?)',
+ undef, $added_owner_id, $self->id);
+ }
+
+ if (scalar(@$removed_owner) || scalar(@$added_owner)) {
+ $changes->{rule_owners} = [$removed_owner, $added_owner];
+ }
+
+ return ($changes, $old_rule);
+}
+
+# Note, this is a function not an object method.
+
+# Given a single string 'foo@bar.com, bob@redhat.com', split it into
+# an array of email addresses.
+#
+# Email addresses should be seperated by a space or comma.
+sub _split_rule_owners {
+ my $owners = shift;
+
+ my @strings = split(EMAIL_SPLIT_RX, $owners);
+
+ return \@strings;
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Extension::RuleEngine::Rule - Bugzilla Rule class.
+
+=head1 SYNOPSIS
+
+ use Bugzilla::Extension::RuleEngine::Rule;
+
+ my $rule = new Bugzilla::Extension::RuleEngine::Rule(1);
+ my $rule = new Bugzilla::Extension::RuleEngine::Rule({name => 'Acme'});
+
+ my $id = $rule->id;
+ my $name = $rule->name;
+ my $description = $rule->description;
+ my $creation_ts = $rule->creation_ts;
+ my $rule_group_id = $rule->rule_group_id;
+ my $current_detail_id = $rule->current_detail_id;
+ my $last_used = $rule->last_used;
+ my $use_count = $rule->use_count;
+ my $sortkey = $rule->sortkey;
+ my $is_periodic = $rule->is_periodic;
+
+=head1 DESCRIPTION
+
+Bugzilla::Extension::RuleEngine::Rule represents a rule object. It is an
+implementation of L<Bugzilla::Object>, and thus provides all methods
+that L<Bugzilla::Object> provides.
+
+The methods that are specific to L<Bugzilla::Extension::RuleEngine::Rule> are listed
+below.
+
+A Rule is a rule that is part of the Bugzilla Rules Engine. A rule is versioned
+with a specific version called a rule detail.
+
+=head1 METHODS
+
+=over
+
+=item C<current>
+
+=over
+
+=item B<Description>
+
+Returns the current version of the rule
+
+=item B<Params>
+
+none.
+
+=item B<Returns>
+
+A Bugzilla::Extension::RuleEngine::RuleDetail object.
+
+=back
+
+=item C<all_details>
+
+=over
+
+=item B<Description>
+
+Returns all versions of the rule
+
+=item B<Params>
+
+none.
+
+=item B<Returns>
+
+An arrayref of Bugzilla::Extension::RuleEngine::RuleDetail objects.
+
+=back
+
+=item C<match_rules>
+
+=over
+
+=item B<Description>
+
+Returns rules that meet a certain criteria
+
+=item B<Params>
+
+=over
+
+=item B<inactive>
+
+If set, will return all rules. Otherwise will only return active rules or those
+changed in the last 7 days.
+
+=item B<product>
+
+The name of the product. All products if not specified.
+
+=item B<sort_order>
+
+The order to sort rows by. Possible values are:
+run, alpha, create, update, last or hits
+
+=item B<reverse>
+
+Reverse the sort order above
+
+=item B<rule_group_id>
+
+The id of the rule group to limit results to.
+
+=item B<is_bug_change>
+
+only rules that fire on bug change
+
+=item B<isactive>
+
+limit to active rules
+
+=item B<is_periodic>
+
+limit to periodic rules
+
+=item B<minor_update>
+
+Should changes made by this rule be marked as minor updates?
+
+=back
+
+=item B<Returns>
+
+An arrayref of Bugzilla::Extension::RuleEngine::Rule objects.
+
+=back
+
+=item C<run_after>
+
+=over
+
+=item B<Description>
+
+Returns the name of the rule that is run before this one
+
+=item B<Params>
+
+none.
+
+=item B<Returns>
+
+The name of the rule that is run before this one. Returns
+an empty string if this is the first rule.
+
+=back
+
+=item C<set_active>
+
+=over
+
+=item B<Description>
+
+Changes the active state of the bug. This is done by creating
+a new rule detail row, and updating the current_detail_id value.
+
+=item B<Params>
+
+is_active (boolean) - Whether this rule should be active or
+inactive.
+
+=item B<Returns>
+
+The id of the newly create rule detail.
+
+=back
+
+=back
+
+=cut