#!/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)
    '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_<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', $_[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__
