#!/usr/bin/perl # Bug predicate / "checker" object # License: Dual-license GPL 3.0+ or MPL 1.1+ # Contributor(s): Vitaliy Filippov 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) 'bypass_group_id', # Group members of which may bypass this check ); 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, bypass_group_id => \&_check_bypass_group_id, user_id => \&_check_user_id, }; use constant UPDATE_COLUMNS => (grep { $_ ne 'id' && $_ ne 'user_id' } DB_COLUMNS); # 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) = @_; # This code allows to create predicates using searches shared by other users, # but the UI doesn't allow it (yet?). my $q = Bugzilla::Search::Saved->check({ id => $value }); # 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_user_id { my ($invocant, $value, $field) = @_; return Bugzilla->user->id; } sub _check_bypass_group_id { my ($invocant, $value, $field) = @_; return $value ? Bugzilla::Group->check({ id => $value })->id : undef; } 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 bypass_group_id { $_[0]->{bypass_group_id} } 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_' or 'remove_', 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', $_[1]) } sub set_user_id { $_[0]->set('user_id', $_[1]) } 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__