You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
724 lines
25 KiB
724 lines
25 KiB
package NGCP::Panel::Role::API;
|
|
no Moose;
|
|
|
|
use NGCP::Panel::Utils::Generic qw(:all);
|
|
use boolean qw(true);
|
|
use Safe::Isa qw($_isa);
|
|
use Storable qw();
|
|
use JSON qw();
|
|
use JSON::Pointer;
|
|
use JSON::Pointer::Exception qw();
|
|
use HTTP::Status qw(:constants);
|
|
use Scalar::Util qw/blessed/;
|
|
use TryCatch;
|
|
use DateTime::Format::HTTP qw();
|
|
use DateTime::Format::RFC3339 qw();
|
|
use Types::Standard qw(InstanceOf);
|
|
use Regexp::Common qw(delimited); # $RE{delimited}
|
|
use HTTP::Headers::Util qw(split_header_words);
|
|
use Data::HAL qw();
|
|
use Data::HAL::Link qw();
|
|
use NGCP::Panel::Utils::ValidateJSON qw();
|
|
use NGCP::Panel::Utils::Journal qw();
|
|
use parent qw/NGCP::Panel::Role::Journal/;
|
|
|
|
#fun - there is no one usage of the last_modified through all ngcp-panel sources. even in javascript
|
|
#has('last_modified', is => 'rw', isa => InstanceOf['DateTime']);
|
|
|
|
sub get_valid_post_data {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $media_type = $params{media_type};
|
|
my $json = $self->get_valid_raw_post_data(%params);
|
|
return unless $self->require_wellformed_json($c, $media_type, $json);
|
|
return JSON::from_json($json, { utf8 => 1 });
|
|
}
|
|
|
|
sub get_valid_raw_post_data {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $media_type = $params{media_type};
|
|
|
|
return unless $self->forbid_link_header($c);
|
|
return unless $self->valid_media_type($c, $media_type);
|
|
return unless $self->require_body($c);
|
|
return $c->stash->{body};
|
|
}
|
|
|
|
sub get_valid_put_data {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $media_type = $params{media_type};
|
|
my $json = $self->get_valid_raw_put_data(%params);
|
|
return unless $json;
|
|
return unless $self->require_wellformed_json($c, $media_type, $json);
|
|
return JSON::from_json($json, { utf8 => 1 });
|
|
}
|
|
|
|
sub get_valid_raw_put_data {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $media_type = $params{media_type};
|
|
my $id = $params{id};
|
|
|
|
return unless $self->valid_id($c, $id);
|
|
return unless $self->forbid_link_header($c);
|
|
return unless $self->valid_media_type($c, $media_type);
|
|
return unless $self->require_body($c);
|
|
return $c->stash->{body};
|
|
}
|
|
|
|
sub get_valid_patch_data {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $media_type = $params{media_type};
|
|
my $id = $params{id};
|
|
my $ops = $params{ops} // [qw/replace copy/];
|
|
|
|
return unless $self->valid_id($c, $id);
|
|
return unless $self->forbid_link_header($c);
|
|
return unless $self->valid_media_type($c, $media_type);
|
|
return unless $self->require_body($c);
|
|
my $json = $c->stash->{body};
|
|
return unless $self->require_wellformed_json($c, $media_type, $json);
|
|
return unless $self->require_valid_patch($c, $json, $ops);
|
|
|
|
return $json;
|
|
}
|
|
|
|
sub check_reload {
|
|
my ($self, $c, $resource) = @_;
|
|
my ($sip, $xmpp) = (1,1);
|
|
|
|
if (delete $resource->{_skip_sip_reload} || $c->config->{features}->{debug}) {
|
|
$sip = 0;
|
|
$c->log->debug("skipping SIP reload");
|
|
}
|
|
if (delete $resource->{_skip_xmpp_reload} || $c->config->{features}->{debug}) {
|
|
$xmpp = 0;
|
|
$c->log->debug("skipping XMPP reload");
|
|
}
|
|
|
|
return ($sip, $xmpp);
|
|
}
|
|
|
|
sub validate_form {
|
|
my ($self, %params) = @_;
|
|
|
|
my $c = $params{c};
|
|
my $resource = $params{resource};
|
|
my $form = $params{form};
|
|
my $run = $params{run} // 1;
|
|
my $exceptions = $params{exceptions} // [];
|
|
my $form_params = $params{form_params} // {};
|
|
push @{ $exceptions }, "external_id";
|
|
|
|
my @normalized = ();
|
|
|
|
# move {xxx_id} into {xxx}{id} for FormHandler
|
|
foreach my $key(keys %{ $resource } ) {
|
|
my $skip_normalize = grep {/^$key$/} @{ $exceptions };
|
|
if($key =~ /^(.+)_id$/ && !$skip_normalize && !exists $resource->{$1}) {
|
|
push @normalized, $1;
|
|
$resource->{$1}{id} = delete $resource->{$key};
|
|
}
|
|
}
|
|
|
|
# remove unknown keys
|
|
my %fields = map { $_->name => $_ } $form->fields;
|
|
$self->validate_fields($c, $resource, \%fields, $run);
|
|
|
|
if($run) {
|
|
# check keys/vals
|
|
$form->process(params => $resource, posted => 1, %{$form_params} );
|
|
unless($form->validated) {
|
|
my $e = join '; ', map {
|
|
my $in = (defined $_->input && ref $_->input eq 'HASH' && exists $_->input->{id}) ? $_->input->{id} : ($_->input // '');
|
|
sprintf 'field=\'%s\', input=\'%s\', errors=\'%s\'',
|
|
($_->parent->$_isa('HTML::FormHandler::Field') ? $_->parent->name . '_' : '') . $_->name,
|
|
$in,
|
|
join('', @{ $_->errors })
|
|
} $form->error_fields;
|
|
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Validation failed. $e");
|
|
return;
|
|
}
|
|
}
|
|
|
|
# move {xxx}{id} back into {xxx_id} for DB
|
|
foreach my $key(@normalized) {
|
|
next unless(exists $resource->{$key});
|
|
$resource->{$key . '_id'} = defined($resource->{$key}{id}) ?
|
|
int($resource->{$key}{id}) :
|
|
$resource->{$key}{id};
|
|
delete $resource->{$key};
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub validate_fields {
|
|
my ($self, $c, $resource, $fields, $run) = @_;
|
|
|
|
for my $k (keys %{ $resource }) {
|
|
#if($resource->{$k}->$_isa('JSON::XS::Boolean') || $resource->{$k}->$_isa('JSON::PP::Boolean')) {
|
|
if($resource->{$k}->$_isa('JSON::PP::Boolean')) {
|
|
$resource->{$k} = $resource->{$k} ? 1 : 0;
|
|
}
|
|
unless(exists $fields->{$k}) {
|
|
delete $resource->{$k};
|
|
}
|
|
$resource->{$k} = DateTime::Format::RFC3339->format_datetime($resource->{$k})
|
|
if $resource->{$k}->$_isa('DateTime');
|
|
$resource->{$k} = $resource->{$k} + 0
|
|
if(defined $resource->{$k} && (
|
|
$fields->{$k}->$_isa('HTML::FormHandler::Field::Integer') ||
|
|
$fields->{$k}->$_isa('HTML::FormHandler::Field::Money') ||
|
|
$fields->{$k}->$_isa('HTML::FormHandler::Field::Float')) &&
|
|
(is_int($resource->{$k}) || is_decimal($resource->{$k})));
|
|
|
|
if (defined $resource->{$k} &&
|
|
$fields->{$k}->$_isa('HTML::FormHandler::Field::Repeatable') &&
|
|
"ARRAY" eq ref $resource->{$k} ) {
|
|
for my $elem (@{ $resource->{$k} }) {
|
|
my ($subfield_instance) = $fields->{$k}->fields;
|
|
my %subfields = map { $_->name => $_ } $subfield_instance->fields;
|
|
$self->validate_fields($c, $elem, \%subfields, $run);
|
|
}
|
|
}
|
|
|
|
# only do this for converting back from obj to hal
|
|
# otherwise it breaks db fields with the \0 and \1 notation
|
|
unless($run) {
|
|
$resource->{$k} = $resource->{$k} ? JSON::true : JSON::false
|
|
if(defined $resource->{$k} &&
|
|
$fields->{$k}->$_isa('HTML::FormHandler::Field::Boolean'));
|
|
}
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub error {
|
|
my ($self, $c, $code, $message) = @_;
|
|
|
|
$c->log->error("error $code - $message"); # TODO: user, trace etc
|
|
|
|
$c->response->content_type('application/json');
|
|
$c->response->status($code);
|
|
$c->response->body(JSON::to_json({ code => $code, message => $message })."\n");
|
|
$c->stash(api_error_message => $message);
|
|
return;
|
|
}
|
|
|
|
sub forbid_link_header {
|
|
my ($self, $c) = @_;
|
|
return 1 unless $c->request->header('Link');
|
|
$self->error($c, HTTP_BAD_REQUEST, "The request must not contain 'Link' headers. Instead assert relationships in the entity body.");
|
|
return;
|
|
}
|
|
|
|
sub valid_media_type {
|
|
my ($self, $c, $media_type) = @_;
|
|
|
|
my $ctype = $c->request->header('Content-Type');
|
|
$ctype =~ s/;\s+boundary.+$// if $ctype;
|
|
my $type;
|
|
if(ref $media_type eq "ARRAY") {
|
|
$type = join ' or ', @{ $media_type };
|
|
return 1 if $ctype && grep { $ctype eq $_ } @{$media_type};
|
|
} else {
|
|
$type = $media_type;
|
|
return 1 if($ctype && index($ctype, $media_type) == 0);
|
|
}
|
|
$self->error($c, HTTP_UNSUPPORTED_MEDIA_TYPE, "Unsupported media type '" . ($ctype // 'undefined') . "', accepting $type only.");
|
|
return;
|
|
}
|
|
|
|
sub require_body {
|
|
my ($self, $c) = @_;
|
|
return 1 if length $c->stash->{body};
|
|
|
|
|
|
|
|
$self->error($c, HTTP_BAD_REQUEST, "This request is missing a message body.");
|
|
return;
|
|
}
|
|
|
|
# returns Catalyst::Request::Upload
|
|
sub get_upload {
|
|
my ($self, $c, $field) = @_;
|
|
my $upload = $c->req->upload($field);
|
|
return $upload if $upload;
|
|
$self->error($c, HTTP_BAD_REQUEST, "This request is missing the upload part '$field' in body.");
|
|
return;
|
|
}
|
|
|
|
sub require_preference {
|
|
my ($self, $c) = @_;
|
|
return 'minimal' unless $c->request->header('Prefer');
|
|
my @preference = grep { 'return' eq $_->[0] } split_header_words($c->request->header('Prefer'));
|
|
return $preference[0][1]
|
|
if 1 == @preference && ('minimal' eq $preference[0][1] || 'representation' eq $preference[0][1]);
|
|
$self->error($c, HTTP_BAD_REQUEST, "Header 'Prefer' must be either 'return=minimal' or 'return=representation'.");
|
|
}
|
|
|
|
sub require_wellformed_json {
|
|
my ($self, $c, $media_type, $patch) = @_;
|
|
my $ret;
|
|
try {
|
|
NGCP::Panel::Utils::ValidateJSON->new($patch);
|
|
$ret = 1;
|
|
} catch($e) {
|
|
chomp $e;
|
|
$self->error($c, HTTP_BAD_REQUEST, "The entity is not a well-formed '$media_type' document. $e");
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
sub allowed_methods_filtered {
|
|
my ($self, $c) = @_;
|
|
if($c->user->read_only) {
|
|
my @methods = ();
|
|
foreach my $m(@{ $self->allowed_methods }) {
|
|
next unless $m =~ /^(GET|HEAD|OPTIONS)$/;
|
|
push @methods, $m;
|
|
}
|
|
return \@methods;
|
|
} else {
|
|
return $self->allowed_methods;
|
|
}
|
|
}
|
|
|
|
# sub allowed_methods {
|
|
# my ($self) = @_;
|
|
# #my $meta = $self->meta;
|
|
# #my @allow;
|
|
# #for my $method ($meta->get_method_list) {
|
|
# # push @allow, $meta->get_method($method)->name
|
|
# # if $meta->get_method($method)->can('attributes') &&
|
|
# # grep { 'Allow' eq $_ } @{ $meta->get_method($method)->attributes };
|
|
# #}
|
|
# #return [sort @allow];
|
|
# return $self->attributed_methods('Allow');
|
|
# }
|
|
|
|
# sub attributed_methods {
|
|
# my ($self,$attribute) = @_;
|
|
# my $meta = $self->meta;
|
|
# my @attributed;
|
|
# for my $method ($meta->get_method_list) {
|
|
# push @attributed, $meta->get_method($method)->name
|
|
# if $meta->get_method($method)->can('attributes') &&
|
|
# grep { $attribute eq $_ } @{ $meta->get_method($method)->attributes };
|
|
# }
|
|
# return [sort @attributed];
|
|
# }
|
|
|
|
sub valid_id {
|
|
my ($self, $c, $id) = @_;
|
|
return 1 if is_int($id);
|
|
$self->error($c, HTTP_BAD_REQUEST, "Invalid id in request URI");
|
|
return;
|
|
}
|
|
|
|
sub require_valid_patch {
|
|
my ($self, $c, $json, $ops) = @_;
|
|
|
|
my $valid_ops = {
|
|
'replace' => { 'path' => 1, 'value' => 1 },
|
|
'copy' => { 'from' => 1, 'path' => 1 },
|
|
'remove' => { 'path' => 1 },
|
|
'add' => { 'path' => 1, 'value' => 1 },
|
|
'test' => { 'path' => 1, 'value' => 1 },
|
|
'move' => { 'from' => 1, 'path' => 1 },
|
|
};
|
|
for my $o(keys %{ $valid_ops }) {
|
|
unless(grep { /^$o$/ } @{ $ops }) {
|
|
delete $valid_ops->{$o};
|
|
}
|
|
}
|
|
|
|
my $patch = JSON::from_json($json, { utf8 => 1 });
|
|
unless(ref $patch eq "ARRAY") {
|
|
$self->error($c, HTTP_BAD_REQUEST, "Body for PATCH must be an array.");
|
|
return;
|
|
}
|
|
foreach my $elem(@{ $patch }) {
|
|
unless(ref $elem eq "HASH") {
|
|
$self->error($c, HTTP_BAD_REQUEST, "Array in body of PATCH must only contain hashes.");
|
|
return;
|
|
}
|
|
unless(exists $elem->{op}) {
|
|
$self->error($c, HTTP_BAD_REQUEST, "PATCH element must have an 'op' field.");
|
|
return;
|
|
}
|
|
unless(exists $valid_ops->{$elem->{op}}) {
|
|
$self->error($c, HTTP_BAD_REQUEST, "Invalid PATCH op '$elem->{op}', must be one of " . (join(', ', map { "'".$_."'" } keys %{ $valid_ops }) ));
|
|
return;
|
|
}
|
|
my $tmpelem = Storable::dclone($elem);
|
|
my $tmpops = Storable::dclone($valid_ops);
|
|
my $op = delete $tmpelem->{op};
|
|
foreach my $k(keys %{ $tmpelem }) {
|
|
unless(exists $tmpops->{$op}->{$k}) {
|
|
$self->error($c, HTTP_BAD_REQUEST, "Invalid PATCH key '$k' for op '$op', must be one of " . (join(', ', map { "'".$_."'" } keys %{ $valid_ops->{$op} }) ));
|
|
return;
|
|
}
|
|
delete $tmpops->{$op}->{$k};
|
|
}
|
|
if(keys %{ $tmpops->{$op} }) {
|
|
$self->error($c, HTTP_BAD_REQUEST, "Missing PATCH keys ". (join(', ', map { "'".$_."'" } keys %{ $tmpops->{$op} }) ) . " for op '$op'");
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|
|
sub resource_exists {
|
|
my ($self, $c, $entity_name, $resource) = @_;
|
|
return 1 if $resource;
|
|
$self->error($c, HTTP_NOT_FOUND, "Entity '$entity_name' not found.");
|
|
return;
|
|
}
|
|
|
|
sub paginate_order_collection {
|
|
my ($self, $c, $item_rs) = @_;
|
|
|
|
my $page = $c->request->params->{page} // 1;
|
|
my $rows = $c->request->params->{rows} // 10;
|
|
my $order_by = $c->request->params->{order_by};
|
|
my $direction = $c->request->params->{order_by_direction} // "asc";
|
|
my $total_count = int($item_rs->count);
|
|
$item_rs = $item_rs->search(undef, {
|
|
page => $page,
|
|
rows => $rows,
|
|
});
|
|
if ($order_by && $item_rs->result_source->has_column($order_by)) {
|
|
my $me = $item_rs->current_source_alias;
|
|
if (lc($direction) eq 'desc') {
|
|
$item_rs = $item_rs->search(undef, {
|
|
order_by => {-desc => "$me.$order_by"},
|
|
});
|
|
$c->log->debug("ordering by $me.$order_by DESC");
|
|
} else {
|
|
$item_rs = $item_rs->search(undef, {
|
|
order_by => "$me.$order_by",
|
|
});
|
|
$c->log->debug("ordering by $me.$order_by");
|
|
}
|
|
}
|
|
return ($total_count, $item_rs);
|
|
}
|
|
|
|
sub collection_nav_links {
|
|
my ($self, $page, $rows, $total_count, $path, $params) = @_;
|
|
|
|
$params = { %{ $params } }; #copy
|
|
delete @{$params}{'page', 'rows'};
|
|
my $rest_params = join( '&', map {"$_=".$params->{$_}} keys %{$params});
|
|
$rest_params = $rest_params ? "&$rest_params" : "";
|
|
|
|
my @links = (Data::HAL::Link->new(relation => 'self', href => sprintf('/%s?page=%s&rows=%s%s', $path, $page, $rows, $rest_params)));
|
|
|
|
if(($total_count / $rows) > $page ) {
|
|
push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d%s', $path, $page + 1, $rows, $rest_params));
|
|
}
|
|
if($page > 1) {
|
|
push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d%s', $path, $page - 1, $rows, $rest_params));
|
|
}
|
|
return @links;
|
|
}
|
|
|
|
sub apply_patch {
|
|
my ($self, $c, $entity, $json, $optional_field_code_ref) = @_;
|
|
my $patch = JSON::decode_json($json);
|
|
try {
|
|
for my $op (@{ $patch }) {
|
|
my $coderef = JSON::Pointer->can($op->{op});
|
|
die "invalid op '".$op->{op}."' despite schema validation" unless $coderef;
|
|
for ($op->{op}) {
|
|
if ('add' eq $_ or 'replace' eq $_) {
|
|
try {
|
|
$entity = $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value});
|
|
} catch($pe) {
|
|
if (defined $optional_field_code_ref && ref $optional_field_code_ref eq 'CODE') {
|
|
if (blessed $pe and $pe->isa('JSON::Pointer::Exception') && $pe->code == JSON::Pointer::Exception->ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE) {
|
|
&$optional_field_code_ref(substr($op->{path},1),$entity);
|
|
$entity = $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value});
|
|
}
|
|
} else {
|
|
die($pe); #->rethrow;
|
|
}
|
|
}
|
|
} elsif ('remove' eq $_) {
|
|
try {
|
|
$entity = $coderef->('JSON::Pointer', $entity, $op->{path});
|
|
} catch($pe) {
|
|
if (defined $optional_field_code_ref && ref $optional_field_code_ref eq 'CODE') {
|
|
if (blessed $pe and $pe->isa('JSON::Pointer::Exception') && $pe->code == JSON::Pointer::Exception->ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE) {
|
|
&$optional_field_code_ref(substr($op->{path},1),$entity);
|
|
$entity = $coderef->('JSON::Pointer', $entity, $op->{path});
|
|
}
|
|
} else {
|
|
die($pe); #->rethrow;
|
|
}
|
|
}
|
|
} elsif ('move' eq $_ or 'copy' eq $_) {
|
|
try {
|
|
$entity = $coderef->('JSON::Pointer', $entity, $op->{from}, $op->{path});
|
|
} catch($pe) {
|
|
if (defined $optional_field_code_ref && ref $optional_field_code_ref eq 'CODE') {
|
|
if (blessed $pe and $pe->isa('JSON::Pointer::Exception') && $pe->code == JSON::Pointer::Exception->ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE) {
|
|
&$optional_field_code_ref(substr($op->{path},1),$entity);
|
|
$entity = $coderef->('JSON::Pointer', $entity, $op->{from}, $op->{path});
|
|
}
|
|
} else {
|
|
die($pe); #->rethrow;
|
|
}
|
|
}
|
|
} elsif ('test' eq $_) {
|
|
try {
|
|
die "test failed - path: $op->{path} value: $op->{value}\n"
|
|
unless $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value});
|
|
} catch($pe) {
|
|
if (defined $optional_field_code_ref && ref $optional_field_code_ref eq 'CODE') {
|
|
if (blessed $pe and $pe->isa('JSON::Pointer::Exception') && $pe->code == JSON::Pointer::Exception->ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE) {
|
|
&$optional_field_code_ref(substr($op->{path},1),$entity);
|
|
die "test failed - path: $op->{path} value: $op->{value}\n"
|
|
unless $coderef->('JSON::Pointer', $entity, $op->{path}, $op->{value});
|
|
}
|
|
} else {
|
|
die($pe); #->rethrow;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch($e) {
|
|
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "The entity could not be processed: $e");
|
|
return;
|
|
}
|
|
return $entity;
|
|
}
|
|
|
|
#sub apply_fake_time {
|
|
# my ($self, $c) = @_;
|
|
# if (exists $ENV{API_FAKE_CLIENT_TIME} && $ENV{API_FAKE_CLIENT_TIME}) {
|
|
# my $date = $c->request->header('Date');
|
|
# if ($date) {
|
|
# my $dt = NGCP::Panel::Utils::DateTime::from_rfc1123_string($date);
|
|
# if ($dt) {
|
|
# NGCP::Panel::Utils::DateTime::set_fake_time($dt->epoch);
|
|
# $c->stash->{is_fake_time} = 1;
|
|
# $c->log('using date header to fake system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
|
|
# return;
|
|
# }
|
|
# }
|
|
# NGCP::Panel::Utils::DateTime::set_fake_time();
|
|
# $c->stash->{is_fake_time} = 0;
|
|
# $c->log('resetting faked system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
|
|
# }
|
|
#}
|
|
|
|
#sub reset_fake_time {
|
|
# my ($self, $c) = @_;
|
|
# if (exists $ENV{API_FAKE_CLIENT_TIME} && $ENV{API_FAKE_CLIENT_TIME} && $c->stash->{fake_time}) {
|
|
# NGCP::Panel::Utils::DateTime::set_fake_time();
|
|
# $c->stash->{fake_time} = 0;
|
|
# $c->log('resetting faked system time: ' . NGCP::Panel::Utils::DateTime::to_string(NGCP::Panel::Utils::DateTime::current_local));
|
|
# }
|
|
#}
|
|
|
|
sub set_body {
|
|
my ($self, $c) = @_;
|
|
$c->stash->{body} = $c->request->body ? (do { local $/; $c->request->body->getline }) : '';
|
|
}
|
|
|
|
sub log_request {
|
|
my ($self, $c) = @_;
|
|
|
|
NGCP::Panel::Utils::Message::info(
|
|
c => $c,
|
|
type => 'api_request',
|
|
log => $c->stash->{'body'},
|
|
);
|
|
}
|
|
|
|
sub log_response {
|
|
my ($self, $c) = @_;
|
|
|
|
# TODO: should be put a UUID to stash in log_request and use it here to correlate
|
|
# req/res lines?
|
|
$c->forward(qw(Controller::Root render));
|
|
$c->response->content_type('')
|
|
if $c->response->content_type =~ qr'text/html'; # stupid RenderView getting in the way
|
|
my $rc = '';
|
|
if (@{ $c->error }) {
|
|
my $msg = join ', ', @{ $c->error };
|
|
$rc = NGCP::Panel::Utils::Message::error(
|
|
c => $c,
|
|
type => 'api_response',
|
|
log => $msg,
|
|
);
|
|
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
|
|
$c->clear_errors;
|
|
}
|
|
NGCP::Panel::Utils::Message::info(
|
|
c => $c,
|
|
type => 'api_response',
|
|
log => $c->response->body,
|
|
);
|
|
}
|
|
|
|
|
|
#sub item_rs {}
|
|
sub item_rs {
|
|
my ($self, @orig_params) = @_;
|
|
my $item_rs = $self->_item_rs(@orig_params);
|
|
return unless($item_rs);
|
|
|
|
if ($self->can('query_params')) {
|
|
return $self->apply_query_params($orig_params[0],$self->query_params,$item_rs);
|
|
}
|
|
|
|
return $item_rs;
|
|
}
|
|
|
|
sub apply_query_params {
|
|
|
|
my ($self,$c,$query_params,$item_rs) = @_;
|
|
# no query params defined in collection controller
|
|
unless(@{ $query_params }) {
|
|
return $item_rs;
|
|
}
|
|
|
|
foreach my $param(keys %{ $c->req->query_params }) {
|
|
my @p = grep { $_->{param} eq $param } @{ $query_params };
|
|
next unless($p[0]->{query} || $p[0]->{new_rs}); # skip "dummy" query parameters
|
|
my $q = $c->req->query_params->{$param}; # TODO: arrayref?
|
|
$q =~ s/\*/\%/g;
|
|
$q = undef if $q eq "NULL"; # IS NULL translation
|
|
if(@p) {
|
|
if (defined $p[0]->{new_rs}) {
|
|
#compose fresh rs based on current, to support set operations with filters:
|
|
$item_rs = $p[0]->{new_rs}($c,$q,$item_rs);
|
|
} elsif (defined $p[0]->{query}) {
|
|
#regular chaining:
|
|
my($sub_where,$sub_attributes) = $self->get_query_callbacks(\@p);
|
|
$item_rs = $item_rs->search($sub_where->($q,$c), $sub_attributes->($q,$c));
|
|
}
|
|
}
|
|
}
|
|
return $item_rs;
|
|
|
|
}
|
|
sub get_query_callbacks{
|
|
my ($self, $query_param_spec) = @_;
|
|
#while believe that there is only one parameter
|
|
my @p = @$query_param_spec;
|
|
my($sub_where,$sub_attributes);
|
|
if($p[0]->{query_type}){
|
|
if('string_like' eq $p[0]->{query_type}){
|
|
$sub_where = sub {my ($q, $c) = @_; { $p[0]->{param} => { like => $q } };};
|
|
}elsif('string_eq' eq $p[0]->{query_type}){
|
|
$sub_where = sub {my ($q, $c) = @_; { $p[0]->{param} => $q };};
|
|
}
|
|
}
|
|
if($p[0]->{query}){
|
|
$sub_where //= $p[0]->{query}->{first};
|
|
$sub_attributes = $p[0]->{query}->{second};
|
|
}
|
|
$sub_attributes //= sub {};
|
|
return ($sub_where,$sub_attributes);
|
|
}
|
|
|
|
sub delay_commit {
|
|
my ($self, $c, $guard) = @_;
|
|
my $allow_delay_commit = 0;
|
|
my $cfg = $c->config->{api_debug_opts};
|
|
$allow_delay_commit = ((defined $cfg->{allow_delay_commit}) && $cfg->{allow_delay_commit} ? 1 : 0) if defined $cfg;
|
|
if ($allow_delay_commit) {
|
|
my $delay = $c->request->header('X-Delay-Commit'); #('Expect');
|
|
if ($delay && $delay =~ /\d+/ && $delay > 0 && $delay < 500) {
|
|
$c->log->debug('using X-Delay-Commit header to delay db commit for ' . $delay . ' seconds');
|
|
sleep($delay);
|
|
}
|
|
}
|
|
$guard->commit();
|
|
}
|
|
|
|
sub hal_from_item {
|
|
my ($self, $c, $item, $form) = @_;
|
|
my %resource = $item->get_inflated_columns;
|
|
$resource{contract_id} = delete $resource{peering_contract_id};
|
|
my $hal = Data::HAL->new(
|
|
links => [
|
|
Data::HAL::Link->new(
|
|
relation => 'curies',
|
|
href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}',
|
|
name => 'ngcp',
|
|
templated => true,
|
|
),
|
|
Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/", $self->resource_name)),
|
|
Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'),
|
|
Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)),
|
|
],
|
|
relation => 'ngcp:'.$self->resource_name,
|
|
);
|
|
|
|
$form //= $self->get_form($c);
|
|
|
|
$self->validate_form(
|
|
c => $c,
|
|
resource => \%resource,
|
|
form => $form,
|
|
run => 0,
|
|
);
|
|
|
|
$resource{id} = int($item->id);
|
|
$hal->resource({%resource});
|
|
return $hal;
|
|
}
|
|
|
|
sub item_by_id {
|
|
my ($self, $c, $id) = @_;
|
|
my $item_rs = $self->item_rs($c);
|
|
return $item_rs->find($id);
|
|
}
|
|
|
|
sub update_item {
|
|
my ($self, $c, $item, $old_resource, $resource, $form) = @_;
|
|
|
|
$form //= $self->get_form($c);
|
|
return unless $self->validate_form(
|
|
c => $c,
|
|
form => $form,
|
|
resource => $resource,
|
|
);
|
|
last unless $resource;
|
|
$resource = $self->process_resource($c, $item, $old_resource, $resource, $form);
|
|
last unless $resource;
|
|
last unless $self->check_duplicate($c, $item, $old_resource, $resource, $form);
|
|
$item = $self->update($c, $item, $old_resource, $resource, $form);
|
|
return $item;
|
|
}
|
|
sub process_resource{
|
|
my($self, $c, $item, $old_resource, $resource, $form) = @_;
|
|
return $resource;
|
|
}
|
|
sub update{
|
|
my($self, $c, $item, $old_resource, $resource, $form) = @_;
|
|
$item->update($resource);
|
|
return $item;
|
|
}
|
|
|
|
1;
|
|
# vim: set tabstop=4 expandtab:
|