240 lines
6.7 KiB
Perl
240 lines
6.7 KiB
Perl
#!/usr/bin/perl
|
|
# Bug predicate / "checker" object
|
|
# License: Dual-license GPL 3.0+ or MPL 1.1+
|
|
# Contributor(s): Vitaliy Filippov <vitalif@mail.ru>
|
|
|
|
package Bugzilla::Checker;
|
|
|
|
use strict;
|
|
use base qw(Bugzilla::Object Exporter);
|
|
|
|
use JSON;
|
|
use Bugzilla::Search;
|
|
use Bugzilla::Search::Saved;
|
|
use Bugzilla::Error;
|
|
use Bugzilla::Util;
|
|
|
|
use constant DB_TABLE => 'checkers';
|
|
|
|
use constant {
|
|
# Yes => check old state of the bug ("freezer")
|
|
# No => check new state of the bug ("checker")
|
|
CF_FREEZE => 0x01,
|
|
# Yes => throw an error, no => give a warning
|
|
CF_FATAL => 0x02,
|
|
# Yes <=> check new bugs
|
|
CF_CREATE => 0x04,
|
|
# Yes <=> check updates
|
|
CF_UPDATE => 0x08,
|
|
# Yes => forbid to change everything except except_fields
|
|
# No => allow to change everything except except_fields
|
|
# except_fields are empty => CF_DENY added automatically
|
|
CF_DENY => 0x10,
|
|
};
|
|
|
|
our @EXPORT = qw(CF_FREEZE CF_FATAL CF_CREATE CF_UPDATE CF_DENY);
|
|
|
|
use constant DB_COLUMNS => (
|
|
'id',
|
|
'query_id', # "Bad state" is described by this search query
|
|
'user_id', # Creator
|
|
'flags', # Bit field of CF_* flags
|
|
'message', # Error message text
|
|
'sql_code', # SQL code for query is cached here
|
|
'except_fields', # "Exception" fields - see CF_DENY above.
|
|
'triggers', # Triggers (bug changes) (requires CF_FREEZE & !CF_FATAL)
|
|
);
|
|
use constant NAME_FIELD => 'message';
|
|
use constant ID_FIELD => 'id';
|
|
use constant LIST_ORDER => 'id';
|
|
|
|
use constant REQUIRED_CREATE_FIELDS => qw(query_id message);
|
|
|
|
use constant VALIDATORS => {
|
|
query_id => \&_check_query_id,
|
|
flags => \&_check_flags,
|
|
};
|
|
|
|
use constant UPDATE_COLUMNS => (
|
|
'query_id',
|
|
'flags',
|
|
'message',
|
|
'sql_code',
|
|
'except_fields',
|
|
'triggers',
|
|
);
|
|
|
|
# The check works by executing this SQL query with added bugs.bug_id=? condition.
|
|
# Rebuild and save SQL code in the DB, from under the superuser
|
|
# (without permission checks). ORDER BY and SELECT ... FROM are removed
|
|
# and then added for more security.
|
|
sub refresh_sql
|
|
{
|
|
my $self = shift;
|
|
my ($query) = @_;
|
|
if (!$query || $query->id != $self->query_id)
|
|
{
|
|
$query = $self->query;
|
|
}
|
|
my $search = new Bugzilla::Search(
|
|
params => http_decode_query($query->query),
|
|
fields => [ 'bug_id' ],
|
|
user => $query->user,
|
|
ignore_permissions => 1,
|
|
);
|
|
my $terms = Bugzilla::Search::simplify_expression([
|
|
'AND_MANY', { term => 'bugs.bug_id=?' },
|
|
$search->{terms_without_security}
|
|
]);
|
|
my $sql = $search->get_expression_sql($terms, 'force joins');
|
|
$sql =~ s/^\s*SELECT.*?FROM/SELECT DISTINCT $self->{id} FROM/;
|
|
$self->set_sql_code($sql);
|
|
}
|
|
|
|
# Create a predicate, generating SQL code for it
|
|
sub create
|
|
{
|
|
my ($class, $params) = @_;
|
|
if ($params->{except_fields})
|
|
{
|
|
$params->{except_fields} = encode_json($params->{except_fields});
|
|
}
|
|
if ($params->{triggers})
|
|
{
|
|
$params->{triggers} = encode_json($params->{triggers});
|
|
}
|
|
my $self = Bugzilla::Object::create($class, $params);
|
|
$self->update;
|
|
$self->query->set_shared_with_group(Bugzilla::Group->check({ name => 'bz_editcheckers' }));
|
|
return $self;
|
|
}
|
|
|
|
# Update a predicate, regenerating SQL code for it
|
|
sub update
|
|
{
|
|
my $self = shift;
|
|
$self->refresh_sql;
|
|
$self->query->set_shared_with_group(Bugzilla::Group->check({ name => 'bz_editcheckers' }));
|
|
if ($self->triggers)
|
|
{
|
|
$self->{flags} |= CF_FREEZE;
|
|
}
|
|
$self->SUPER::update(@_);
|
|
}
|
|
|
|
# Check this named query exists and is accessible to the user
|
|
sub _check_query_id
|
|
{
|
|
my ($invocant, $value, $field) = @_;
|
|
my $q = Bugzilla::Search::Saved->check({ id => $value });
|
|
# This code allows to create predicates using searches shared by other users,
|
|
# but the UI doesn't allow it (yet?).
|
|
if ($q->user->id != Bugzilla->user->id &&
|
|
(!$q->shared_with_group || !Bugzilla->user->in_group($q->shared_with_group)))
|
|
{
|
|
ThrowUserError('query_access_denied', { query => $q });
|
|
}
|
|
# Check if a named query is not a search query, but just an HTTP url
|
|
if ($q->query =~ /^[a-z][a-z0-9]*:/iso)
|
|
{
|
|
ThrowUserError('query_not_savedsearch', { query => $q });
|
|
}
|
|
return $q->id;
|
|
}
|
|
|
|
sub _check_flags
|
|
{
|
|
my ($invocant, $value, $field) = @_;
|
|
$value = int($value);
|
|
return $value;
|
|
}
|
|
|
|
sub id { $_[0]->{id} }
|
|
sub query_id { $_[0]->{query_id} }
|
|
sub user_id { $_[0]->{user_id} }
|
|
sub message { $_[0]->{message} }
|
|
sub sql_code { $_[0]->{sql_code} }
|
|
sub flags { $_[0]->{flags} }
|
|
|
|
# Specific flags from the bitfield
|
|
sub is_freeze { $_[0]->{flags} & CF_FREEZE }
|
|
sub is_fatal { ($_[0]->{flags} & CF_FATAL) && !$_[0]->triggers }
|
|
sub on_create { $_[0]->{flags} & CF_CREATE }
|
|
sub on_update { $_[0]->{flags} & CF_UPDATE }
|
|
sub deny_all { $_[0]->{flags} & CF_DENY }
|
|
|
|
# { field_name => value }
|
|
# Make an exception for change of field_name to 'value', or to any value if value is undef
|
|
sub except_fields
|
|
{
|
|
my $self = shift;
|
|
if (!exists $self->{except_fields_obj})
|
|
{
|
|
$self->{except_fields_obj} = $self->{except_fields} ? decode_json($self->{except_fields}) : undef;
|
|
}
|
|
return $self->{except_fields_obj};
|
|
}
|
|
|
|
# { field_name => value }
|
|
# Change field 'field_name' to 'value'. For multivalued fields field_name may also
|
|
# by 'add_<field_name>' or 'remove_<field_name>', which means add or remove something.
|
|
# FIXME Now the only functions supported are 'add_cc' and 'clear_flags'
|
|
sub triggers
|
|
{
|
|
my $self = shift;
|
|
if (!exists $self->{triggers_obj})
|
|
{
|
|
$self->{triggers_obj} = $self->{triggers} ? decode_json($self->{triggers}) : undef;
|
|
}
|
|
return $self->{triggers_obj};
|
|
}
|
|
|
|
sub name
|
|
{
|
|
my $self = shift;
|
|
return $self->query->name;
|
|
}
|
|
|
|
sub query
|
|
{
|
|
my $self = shift;
|
|
if (!$self->{query})
|
|
{
|
|
$self->{query} = Bugzilla::Search::Saved->new({ id => $self->query_id });
|
|
}
|
|
return $self->{query};
|
|
}
|
|
|
|
sub user
|
|
{
|
|
my $self = shift;
|
|
if (!$self->{user})
|
|
{
|
|
$self->{user} = Bugzilla::User->new({ id => $self->user_id });
|
|
}
|
|
return $self->{user};
|
|
}
|
|
|
|
sub set_query_id { $_[0]->set('query_id', Bugzilla::Search::Saved->check({ id => $_[1] })->id) }
|
|
sub set_user_id { $_[0]->set('user_id', Bugzilla::User->check({ userid => $_[1] })->id) }
|
|
sub set_flags { $_[0]->set('flags', $_[1]) }
|
|
sub set_message { $_[0]->set('message', $_[1]) }
|
|
sub set_sql_code { $_[0]->set('sql_code', $_[1]) }
|
|
|
|
sub set_except_fields
|
|
{
|
|
my ($self, $value) = @_;
|
|
$self->set('except_fields', $value ? encode_json($value) : undef);
|
|
delete $self->{except_fields_obj};
|
|
}
|
|
|
|
sub set_triggers
|
|
{
|
|
my ($self, $value) = @_;
|
|
$self->set('triggers', $value ? encode_json($value) : undef);
|
|
delete $self->{triggers_obj};
|
|
}
|
|
|
|
1;
|
|
__END__
|