MT#56239 api general caller/callee wildcard search support

various api rails will need to support ?caller= and ?callee=
url query parameters.

since this involves SQL queries against potentially large
database tables, special care is taken with wildcard search
to prevent slow queries:

- the ?wildcards=true query parameter has to be specified
  to accept search patterns that contain wildcard symbols,
  so wildcards are not accepted by default.

  WARNING: a search string with a leading wildcard will always
  force a *slow* full db table scan!

- the * symbol is used as a wildcard symbol
- \ (backslash) is used as escape character to search for
  a literal '*'

Change-Id: Ie6065b0cfa883f7963e1dc8259fffea9a1edfdfe
mr12.1
Rene Krenn 2 years ago
parent 5eff0601b5
commit 9c3a549094

@ -57,29 +57,11 @@ sub query_params {
},
{
param => 'caller',
description => 'Filter by the caller number',
query => {
first => sub {
my $q = shift;
{
'recording_metakeys.key' => 'caller',
'recording_metakeys.value' => $q,
};
}
},
description => "Filter by caller number (append &wildcards=true query parameter to allow patterns using '*' wildcards).",
},
{
param => 'callee',
description => 'Filter by the callee number',
query => {
first => sub {
my $q = shift;
{
'recording_metakeys.key' => 'callee',
'recording_metakeys.value' => $q,
};
}
},
description => "Filter by callee number (append &wildcards=true query parameter to allow patterns using '*' wildcards).",
},
{
param => 'start_time',

@ -68,12 +68,12 @@ sub query_params {
},
{
param => 'caller',
description => 'Filter for conversation events by caller (supports patterns with * wildcard).',
description => "Filter for conversation events by caller number (append &wildcards=true query parameter to allow patterns using '*' wildcards).",
},
{
param => 'callee',
description => 'Filter for conversation events by callee (supports patterns with * wildcard).',
},
description => "Filter for conversation events by callee number (append &wildcards=true query parameter to allow patterns using '*' wildcards).",
},
];
}

@ -1864,5 +1864,118 @@ sub return_requested_type {
}
}
sub apply_caller_filter {
my $self = shift;
my %params = @_;
my ($rs,$params,$conjunctions,$col) = @params{qw/rs params conjunctions col/};
if (exists $params->{caller}) {
$rs = $rs->search_rs({
_wildcard_search(
search_string => $params->{caller},
search => 1,
exact_search => _check_wildcard_search($params),
int_search => 0,
col_name => $col,
comparison_op => undef,
convert_code => undef,
conjunctions => $conjunctions,
)
});
}
return $rs;
}
sub apply_callee_filter {
my $self = shift;
my %params = @_;
my ($rs,$params,$conjunctions,$col) = @params{qw/rs params conjunctions col/};
if (exists $params->{callee}) {
$rs = $rs->search_rs({
_wildcard_search(
search_string => $params->{callee},
search => 1,
exact_search => _check_wildcard_search($params),
int_search => 0,
col_name => $col,
comparison_op => undef,
convert_code => undef,
conjunctions => $conjunctions,
)
});
}
return $rs;
}
sub _wildcard_search {
my %params = @_;
my ($search_string,
$search,
$exact_search,
$int_search,
$col_name,
$conjunctions,
$comparison_op,
$convert_code) = @params{qw/
search_string
search
exact_search
int_search
col_name
conjunctions
comparison_op
convert_code
/};
if ($search or $exact_search or $int_search) {
my $is_pattern = 0;
my ($search_value,$op);
(my $search_string_escaped, $is_pattern) = escape_search_string_pattern(
$search_string,( $exact_search || $int_search ));
if ($is_pattern) {
$op = 'like';
$search_value = $search_string_escaped;
} elsif ($exact_search) {
$op = '=';
$search_string_escaped = $search_string;
$search_string_escaped =~ s/\\\*/*/g;
$search_string_escaped =~ s/\\\\/\\/g;
$search_value = $search_string_escaped;
} elsif ($int_search) {
$op = '=';
$search_value = $search_string;
} else {
$op = 'like';
$search_value = '%' . $search_string_escaped . '%';
}
$op = $comparison_op if (defined $comparison_op);
$search_value = $convert_code->($search_string) if (ref $convert_code eq 'CODE');
my $stmt;
if (defined $search_value) {
if (not $int_search or $search_string =~ /^\d{1,10}$/) {
return ( %{$conjunctions // {}}, $col_name => { $op => $search_value } );
}
}
}
return ();
}
sub _check_wildcard_search {
my $params = shift;
my $exact = 1;
if (exists $params->{wildcards} and defined $params->{wildcards}) {
if ('1' eq $params->{wildcards}
or'true' eq lc($params->{wildcards})) {
$exact = 0;
}
}
return $exact;
}
1;
# vim: set tabstop=4 expandtab:

@ -42,6 +42,19 @@ sub _item_rs {
my $item_rs = $c->model('DB')->resultset('recording_calls')->search_rs(
undef, { prefetch => 'recording_metakeys' });
$item_rs = $self->apply_caller_filter(
rs => $item_rs,
params => $c->req->params,
conjunctions => { 'recording_metakeys.key' => 'caller', },
col => 'recording_metakeys.value'
);
$item_rs = $self->apply_callee_filter(
rs => $item_rs,
params => $c->req->params,
conjunctions => { 'recording_metakeys.key' => 'callee', },
col => 'recording_metakeys.value'
);
if($c->user->roles eq "reseller") {

@ -15,8 +15,6 @@ use HTTP::Status qw(:constants);
use NGCP::Panel::Form;
use Data::Dumper;
use NGCP::Panel::Utils::Datatables qw();
use Tie::IxHash;
#use Class::Hash;
@ -332,115 +330,6 @@ sub _apply_timestamp_from_to {
return $rs;
}
sub _apply_caller {
my $self = shift;
my %params = @_;
my ($rs,$params,$col) = @params{qw/rs params col/};
if (exists $params->{caller}) {
$rs = $rs->search_rs({
_wildcard_search(
search_string => $params->{caller},
search => 1,
exact_search => _check_wildcard_search($params),
int_search => 0,
col_name => $col,
comparison_op => undef,
convert_code => undef,
)
});
}
return $rs;
}
sub _apply_callee {
my $self = shift;
my %params = @_;
my ($rs,$params,$col) = @params{qw/rs params col/};
if (exists $params->{callee}) {
$rs = $rs->search_rs({
_wildcard_search(
search_string => $params->{callee},
search => 1,
exact_search => _check_wildcard_search($params),
int_search => 0,
col_name => $col,
comparison_op => undef,
convert_code => undef,
)
});
}
return $rs;
}
sub _wildcard_search {
my %params = @_;
my ($search_string,
$search,
$exact_search,
$int_search,
$col_name,
$comparison_op,
$convert_code) = @params{qw/
search_string
search
exact_search
int_search
col_name
comparison_op
convert_code
/};
if ($search or $exact_search or $int_search) {
my $is_pattern = 0;
my ($search_value,$op);
(my $search_string_escaped, $is_pattern) = NGCP::Panel::Utils::Datatables::escape_search_string_pattern(
$search_string,( $exact_search || $int_search ));
if ($is_pattern) {
$op = 'like';
$search_value = $search_string_escaped;
} elsif ($exact_search) {
$op = '=';
$search_string_escaped = $search_string;
$search_string_escaped =~ s/\\\*/*/g;
$search_string_escaped =~ s/\\\\/\\/g;
$search_value = $search_string_escaped;
} elsif ($int_search) {
$op = '=';
$search_value = $search_string;
} else {
$op = 'like';
$search_value = '%' . $search_string_escaped . '%';
}
$op = $comparison_op if (defined $comparison_op);
$search_value = $convert_code->($search_string) if (ref $convert_code eq 'CODE');
my $stmt;
if (defined $search_value) {
if (not $int_search or $search_string =~ /^\d{1,10}$/) {
return ( $col_name => { $op => $search_value } );
}
}
}
return ();
}
sub _check_wildcard_search {
my $params = shift;
my $exact = 1;
if (exists $params->{wildcards} and defined $params->{wildcards}) {
if ('1' eq $params->{wildcards}
or'true' eq lc($params->{wildcards})) {
$exact = 0;
}
}
return $exact;
}
sub _apply_direction {
my $self = shift;
my %params = @_;
@ -476,12 +365,12 @@ sub _get_call_rs {
params => $params,
col => 'me.start_time'
);
$rs = $self->_apply_caller(
$rs = $self->apply_caller_filter(
rs => $rs,
params => $params,
col => 'me.source_cli'
);
$rs = $self->_apply_callee(
$rs = $self->apply_callee_filter(
rs => $rs,
params => $params,
col => 'me.destination_user_in'
@ -603,7 +492,7 @@ sub _get_voicemail_rs {
params => $params,
col => 'me.origtime'
);
$rs = $self->_apply_caller(
$rs = $self->apply_caller_filter(
rs => $rs,
params => $params,
col => 'me.callerid'
@ -667,12 +556,12 @@ sub _get_sms_rs {
params => $params,
col => 'me.time'
);
$rs = $self->_apply_caller(
$rs = $self->apply_caller_filter(
rs => $rs,
params => $params,
col => 'me.caller'
);
$rs = $self->_apply_callee(
$rs = $self->apply_callee_filter(
rs => $rs,
params => $params,
col => 'me.callee'
@ -737,12 +626,12 @@ sub _get_fax_rs {
params => $params,
col => 'me.time'
);
$rs = $self->_apply_caller(
$rs = $self->apply_caller_filter(
rs => $rs,
params => $params,
col => 'me.caller'
);
$rs = $self->_apply_callee(
$rs = $self->apply_callee_filter(
rs => $rs,
params => $params,
col => 'me.callee'
@ -816,12 +705,12 @@ sub _get_xmpp_rs {
params => $params,
col => 'epoch'
);
$rs = $self->_apply_caller(
$rs = $self->apply_caller_filter(
rs => $rs,
params => $params,
col => 'user'
);
$rs = $self->_apply_callee(
$rs = $self->apply_callee_filter(
rs => $rs,
params => $params,
col => 'with'

@ -245,32 +245,6 @@ sub get_search_string_pattern {
}
sub escape_search_string_pattern {
my ($searchString,$no_pattern) = @_;
$searchString //= "";
my $is_pattern = 0;
return ($searchString,$is_pattern) if $no_pattern;
my $searchString_escaped = join('',map {
my $token = $_;
if ($token ne '\\\\') {
$token =~ s/%/\\%/g;
$token =~ s/_/\\_/g;
if ($token =~ s/(?<!\\)\*/%/g) {
$is_pattern = 1;
}
$token =~ s/\\\*/*/g;
}
$token;
} split(/(\\\\)/,$searchString,-1));
if (not $is_pattern and not $no_pattern) {
$searchString_escaped .= '%';
$is_pattern = 1;
}
return ($searchString_escaped,$is_pattern);
}
sub _apply_search_filters {
my ($c,$rs,$cols,$use_rs_cb,$extra_or,$extra_or_descr) = @_;

@ -8,9 +8,9 @@ use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
$VERSION = 1.00;
@ISA = qw(Exporter);
@EXPORT = ();
@EXPORT_OK = qw(is_int is_integer is_decimal merge compare is_false is_true get_inflated_columns_all hash2obj mime_type_to_extension extension_to_mime_type array_to_map escape_js escape_uri trim);
%EXPORT_TAGS = ( DEFAULT => [qw(&is_int &is_integer &is_decimal &merge &compare &is_false &is_true &mime_type_to_extension &extension_to_mime_type &array_to_map &escape_js &escape_uri &trim)],
all => [qw(&is_int &is_integer &is_decimal &merge &compare &is_false &is_true &get_inflated_columns_all &hash2obj &mime_type_to_extension &extension_to_mime_type &array_to_map &escape_js &escape_uri &trim)]);
@EXPORT_OK = qw(is_int is_integer is_decimal merge compare is_false is_true get_inflated_columns_all hash2obj mime_type_to_extension extension_to_mime_type array_to_map escape_js escape_uri trim escape_search_string_pattern);
%EXPORT_TAGS = ( DEFAULT => [qw(&is_int &is_integer &is_decimal &merge &compare &is_false &is_true &mime_type_to_extension &extension_to_mime_type &array_to_map &escape_js &escape_uri &trim &escape_search_string_pattern)],
all => [qw(&is_int &is_integer &is_decimal &merge &compare &is_false &is_true &get_inflated_columns_all &hash2obj &mime_type_to_extension &extension_to_mime_type &array_to_map &escape_js &escape_uri &trim &escape_search_string_pattern)]);
use Hash::Merge;
use Data::Compare qw//;
@ -231,4 +231,30 @@ sub trim {
return $value;
}
sub escape_search_string_pattern {
my ($searchString,$no_pattern) = @_;
$searchString //= "";
my $is_pattern = 0;
return ($searchString,$is_pattern) if $no_pattern;
my $searchString_escaped = join('',map {
my $token = $_;
if ($token ne '\\\\') {
$token =~ s/%/\\%/g;
$token =~ s/_/\\_/g;
if ($token =~ s/(?<!\\)\*/%/g) {
$is_pattern = 1;
}
$token =~ s/\\\*/*/g;
}
$token;
} split(/(\\\\)/,$searchString,-1));
if (not $is_pattern and not $no_pattern) {
$searchString_escaped .= '%';
$is_pattern = 1;
}
return ($searchString_escaped,$is_pattern);
}
1;

Loading…
Cancel
Save