diff options
Diffstat (limited to 'extensions/RuleEngine/lib/Rule.pm')
-rw-r--r-- | extensions/RuleEngine/lib/Rule.pm | 755 |
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 |