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.
ngcp-panel/lib/NGCP/Panel/Controller/API/Root.pm

571 lines
20 KiB

package NGCP::Panel::Controller::API::Root;
use NGCP::Panel::Utils::Generic qw(:all);
use Sipwise::Base;
use Encode qw(encode);
use Clone qw/clone/;
use HTTP::Headers qw();
use HTTP::Response qw();
use HTTP::Status qw(:constants);
use File::Find::Rule;
use JSON qw(to_json encode_json);
use YAML::XS qw/Dump/;
use Safe::Isa qw($_isa);
use NGCP::Panel::Utils::API;
use parent qw/Catalyst::Controller NGCP::Panel::Role::API/;
use NGCP::Panel::Utils::Journal qw();
#with 'NGCP::Panel::Role::API';
sub dispatch_path{return '/api/';}
sub allowed_methods{
return [qw/GET OPTIONS HEAD/];
}
__PACKAGE__->config(
action => {
map { $_ => {
ACLDetachTo => 'invalid_user',
AllowedRole => [qw/admin reseller ccareadmin ccare lintercept subscriberadmin subscriber/],
Args => 0,
Does => [qw(ACL CheckTrailingSlash RequireSSL)],
Method => $_,
Path => __PACKAGE__->dispatch_path,
} } @{ __PACKAGE__->allowed_methods },
},
);
sub gather_default_action_roles {
my ($self, %args) = @_; my @roles = ();
push @roles, 'NGCP::Panel::Role::HTTPMethods' if $args{attributes}->{Method};
return @roles;
}
sub auto :Private {
my ($self, $c) = @_;
$self->set_body($c);
$self->log_request($c);
return 1;
}
sub GET : Allow {
my ($self, $c) = @_;
my $response_type;
if ($c->req->params->{swagger}) {
$response_type = 'swagger';
} elsif ($c->req->header('Accept') && $c->req->header('Accept') eq 'application/json') {
$response_type = 'plain_json';
} elsif ($c->req->params->{oldapidoc}) {
$response_type = 'oldapidoc';
} else {
$response_type = 'swaggerui';
}
if ($response_type eq 'swaggerui') {
$c->detach('swaggerui');
}
my $blacklist = {
"DomainPreferenceDefs" => 1,
"SubscriberPreferenceDefs" => 1,
"CustomerPreferenceDefs" => 1,
"ProfilePreferenceDefs" => 1,
"PeeringServerPreferenceDefs" => 1,
"ResellerPreferenceDefs" => 1,
"PbxDevicePreferenceDefs" => 1,
"PbxDeviceProfilePreferenceDefs" => 1,
"PbxFieldDevicePreferenceDefs" => 1,
"MetaConfigDefs" => 1,
"AuthTokens" => 1,
};
my $colls = NGCP::Panel::Utils::API::get_collections_files;
my %user_roles = map {$_ => 1} $c->user->roles;
foreach my $coll(@$colls) {
my $mod = $coll;
$mod =~ s/^.+\/([a-zA-Z0-9_]+)\.pm$/$1/;
next if(exists $blacklist->{$mod});
my $rel = lc $mod;
my $full_mod = 'NGCP::Panel::Controller::API::'.$mod;
my $full_item_mod = 'NGCP::Panel::Controller::API::'.$mod.'Item';
my $role = $full_mod->config->{action}->{OPTIONS}->{AllowedRole};
if($role && ref $role eq "ARRAY") {
next unless grep { $user_roles{$_}; } @{ $role };
} elsif ($role) {
next unless $user_roles{$role};
}
my $allowed_ngcp_types = $full_mod->config->{allowed_ngcp_types} // [];
if (@{$allowed_ngcp_types}) {
next unless grep { /^\Q$c->config->{general}{ngcp_type}\E$/ }
@{$allowed_ngcp_types};
}
my $query_params = [];
if($full_mod->can('query_params')) {
$query_params = $full_mod->query_params;
}
my $actions = [];
if($c->user->read_only) {
foreach my $m(sort keys %{ $full_mod->config->{action} }) {
next unless $m =~ /^(GET|HEAD|OPTIONS)$/;
push @{ $actions }, $m;
}
} else {
$actions = [ sort keys %{ $full_mod->config->{action} } ];
}
my $uri = "/api/$rel/";
my $item_actions = [];
my $journal_resource_config = {};
if($full_item_mod->can('config')) {
if($c->user->read_only) {
foreach my $m(sort keys %{ $full_item_mod->config->{action} }) {
next unless $m =~ /^(GET|HEAD|OPTIONS)$/;
push @{ $item_actions }, $m;
}
} else {
foreach my $m(sort keys %{ $full_item_mod->config->{action} }) {
next unless $m =~ /^(GET|HEAD|OPTIONS|PUT|PATCH|DELETE)$/;
push @{ $item_actions }, $m;
}
}
if($full_item_mod->can('resource_name')) {
my @operations = ();
my $op_config = {};
my $resource_name = $full_item_mod->resource_name;
$journal_resource_config = NGCP::Panel::Utils::Journal::get_journal_resource_config($c->config,$resource_name);
if (exists $full_mod->config->{action}->{POST}) {
$op_config = NGCP::Panel::Utils::Journal::get_api_journal_op_config($c->config,$resource_name,NGCP::Panel::Utils::Journal::CREATE_JOURNAL_OP);
if ($op_config->{operation_enabled}) {
push(@operations,"create (<span>POST $uri</span>)");
}
}
my $item_action_config = $full_item_mod->config->{action};
if (exists $item_action_config->{PUT} || exists $item_action_config->{PATCH}) {
$op_config = NGCP::Panel::Utils::Journal::get_api_journal_op_config($c->config,$resource_name,NGCP::Panel::Utils::Journal::UPDATE_JOURNAL_OP);
if ($op_config->{operation_enabled}) {
if (exists $item_action_config->{PUT} && exists $item_action_config->{PATCH}) {
push(@operations,"update (<span>PUT/PATCH $uri"."id</span>)");
} elsif (exists $item_action_config->{PUT}) {
push(@operations,"update (<span>PUT $uri"."id</span>)");
} elsif (exists $item_action_config->{PATCH}) {
push(@operations,"update (<span>PATCH $uri"."id</span>)");
}
}
}
if (exists $item_action_config->{DELETE}) {
$op_config = NGCP::Panel::Utils::Journal::get_api_journal_op_config($c->config,$resource_name,NGCP::Panel::Utils::Journal::CREATE_JOURNAL_OP);
if ($op_config->{operation_enabled}) {
push(@operations,"delete (<span>DELETE $uri"."id</span>)");
}
}
$journal_resource_config->{operations} = \@operations;
$journal_resource_config->{format} = $op_config->{format};
$journal_resource_config->{uri} = 'api/' . $resource_name . '/id/' . NGCP::Panel::Utils::Journal::API_JOURNAL_RESOURCE_NAME . '/';
$journal_resource_config->{query_params} = ($full_item_mod->can('journal_query_params') ? $full_item_mod->journal_query_params : []);
$journal_resource_config->{sorting_cols} = NGCP::Panel::Utils::Journal::JOURNAL_FIELDS;
$journal_resource_config->{item_uri} = $journal_resource_config->{uri} . 'journalitemid';
if (length(NGCP::Panel::Utils::Journal::API_JOURNALITEMTOP_RESOURCE_NAME) > 0) {
$journal_resource_config->{recent_uri} = $journal_resource_config->{uri} . NGCP::Panel::Utils::Journal::API_JOURNALITEMTOP_RESOURCE_NAME;
}
}
}
my ($form) = $full_mod->get_form($c);
my $sorting_cols = [];
my ($explicit_order_cols, $explicit_order_cols_params);
if ($full_mod->can('order_by_cols')) {
($explicit_order_cols,$explicit_order_cols_params) = $full_mod->order_by_cols($c);
$sorting_cols = [ sort keys %$explicit_order_cols ];
$explicit_order_cols_params //= {};
}
if (!$explicit_order_cols || $explicit_order_cols_params->{columns_are_additional}) {
my $item_rs;
try {
$item_rs = $full_mod->item_rs($c, "");
}
if ($item_rs) {
if(ref $item_rs eq "ARRAY") {
$sorting_cols = [sort (@$sorting_cols, map { $_->{name} } @{ $item_rs })];
} else {
$sorting_cols = [sort (@$sorting_cols, $item_rs->result_source->columns)];
}
}
}
my ($form_fields,$form_fields_upload) = $form ? $self->get_collection_properties($form) : ([],[]);
my $documentation_sample = {} ;
my $documentation_sample_process = sub{
my $s = shift;
$s = to_json($s, {pretty => 1}) =~ s/(^\s*{\s*)|(\s*}\s*$)//rg =~ s/\n /\n/rg;
return $s;
};
if ($full_mod->can('documentation_sample')) {
$documentation_sample->{sample_orig}->{default} = $full_mod->documentation_sample;
$documentation_sample->{sample}->{default} = $documentation_sample_process->($documentation_sample->{sample_orig}->{default});
}
foreach my $action (qw/create update/){
my $method = 'documentation_sample_'.$action;
if ($full_mod->can($method)) {
$documentation_sample->{sample_orig}->{$action} = $full_mod->$method;
$documentation_sample->{sample}->{$action} = $documentation_sample_process->($documentation_sample->{sample_orig}->{$action});
}
}
my $entity_name = $mod;
if($entity_name eq 'Faxes') {
$entity_name = 'Fax';
} else {
$entity_name =~ s/ies$/y/;
$entity_name =~ s/s$//;
}
$c->stash->{collections}->{$rel} = {
name => $mod,
entity_name => $entity_name,
description => $full_mod->api_description,
fields => $form_fields,
uploads => $form_fields_upload,
config => $full_mod->config ,
( $full_item_mod && $full_item_mod->can('config') ) ? (item_config => $full_item_mod->config) : () ,
query_params => $query_params,
actions => $actions,
item_actions => $item_actions,
sorting_cols => $sorting_cols,
uri => $uri,
properties => ( $full_mod->can('properties') ? $full_mod->properties : {} ),#
%$documentation_sample,
journal_resource_config => $journal_resource_config,
};
}
if ($user_roles{subscriber} || $user_roles{subscriberadmin}) {
$c->stash(is_subscriber_api => 1);
} else {
$c->stash(is_admin_api => 1);
}
if ($response_type eq 'plain_json') {
my $body = {};
foreach my $rel(sort keys %{ $c->stash->{collections} }) {
my $r = $c->stash->{collections}->{$rel};
foreach my $k(qw/
actions item_actions fields sorting_cols
/) {
$body->{$rel}->{$k} = $r->{$k};
}
$body->{$rel}->{query_params} = [
map { $_->{param} } @{ $r->{query_params} }
];
}
$c->response->body(JSON::to_json($body, { pretty => 1 }));
$c->response->headers(HTTP::Headers->new(
Content_Language => 'en',
Content_Type => 'application/json',
));
} elsif ($response_type eq 'swagger') {
$c->detach('swagger');
} elsif ($response_type eq 'oldapidoc') {
$c->stash(template => 'api/root.tt');
$c->forward($c->view);
$c->response->headers(HTTP::Headers->new(
Content_Language => 'en',
Content_Type => 'application/xhtml+xml',
#$self->collections_link_headers,
));
} else {
die 'This should never happen.';
}
return;
}
sub swagger :Private {
my ($self, $c) = @_;
my $collections = $c->stash->{collections};
my $user_role = $c->user->roles;
my $result = NGCP::Panel::Utils::API::generate_swagger_datastructure(
$collections,
$user_role,
);
my $headers = HTTP::Headers->new(Content_Language => 'en');
my $response;
if ($c->req->params->{swagger} eq 'yml') {
$headers->header(Content_Type => 'application/x-yaml');
$response = Dump($result);
}
else {
$headers->header(Content_Type => 'application/json');
$response = encode_json($result);
}
$c->response->headers($headers);
$c->response->body($response);
$c->response->code(200);
return;
}
sub swaggerui :Private {
my ($self, $c) = @_;
$c->stash(template => 'api/swaggerui.tt');
$c->forward($c->view);
# $c->response->headers(HTTP::Headers->new(
# Content_Language => 'en',
# Content_Type => 'application/xhtml+xml',
# #$self->collections_link_headers,
# ));
}
sub platforminfo :Path('/api/platforminfo') :CaptureArgs(0) {
my ($self, $c) = @_;
$c->response->content_type('application/json');
unless (uc $c->request->method eq 'GET') {
$c->response->status(HTTP_METHOD_NOT_ALLOWED);
$c->response->body(q());
return;
}
$c->stash->{ngcp_api_realm} = $c->request->env->{NGCP_API_REALM} // "";
$c->stash(template => 'api/platforminfo.tt');
$c->forward($c->view());
}
sub HEAD : Allow {
my ($self, $c) = @_;
$c->forward(qw(GET));
$c->response->body(q());
return;
}
sub OPTIONS : Allow {
my ($self, $c) = @_;
my $allowed_methods = $self->allowed_methods_filtered($c);
$c->response->headers(HTTP::Headers->new(
Allow => join(', ', @{ $allowed_methods }),
$self->collections_link_headers,
));
$c->response->content_type('application/json');
$c->response->body(JSON::to_json({ methods => $allowed_methods })."\n");
return;
}
sub collections_link_headers : Private {
my ($self) = @_;
my $colls = NGCP::Panel::Utils::API::get_collections_files;
# create Link header for each of the collections
my @links = ();
foreach my $mod(@$colls) {
# extract file base from path (e.g. Foo from lib/something/Foo.pm)
$mod =~ s/^.+\/([a-zA-Z0-9_]+)\.pm$/$1/;
my $rel = lc $mod;
$mod = 'NGCP::Panel::Controller::API::'.$mod;
my $dp = $mod->dispatch_path;
push @links, Link => '<'.$dp.'>; rel="collection http://purl.org/sipwise/ngcp-api/#rel-'.$rel.'"';
}
return @links;
}
sub invalid_user : Private {
my ($self, $c, $ssl_client_m_serial) = @_;
#$self->error($c, HTTP_FORBIDDEN, "Invalid certificate serial number '$ssl_client_m_serial'.");
$self->error($c, HTTP_FORBIDDEN, "Invalid user");
return;
}
sub field_to_json : Private {
my ($self, $field) = @_;
if ($field->$_isa('HTML::FormHandler::Field::Select')) {
return $self->field_to_select_options($field);
} # elsif { ... }
SWITCH: for ($field->type) {
/Float|Integer|Money|PosInteger|Minute|Hour|MonthDay|Year/ &&
return "Number";
/Boolean/ &&
return "Boolean";
/Repeatable/ &&
return "Array";
/\+NGCP::Panel::Field::Select/ &&
return $self->field_to_select_options($field);
/\+NGCP::Panel::Field::Regex/ &&
return "String";
/\+NGCP::Panel::Field::DateTime/ &&
return "String";
/\+NGCP::Panel::Field::Country/ &&
return "String";
/\+NGCP::Panel::Field::EmailList/ &&
return "String";
/\+NGCP::Panel::Field::Identifier/ &&
return "String";
/\+NGCP::Panel::Field::URI/ &&
return "String";
/\+NGCP::Panel::Field::IPAddress/ &&
return "String";
/\+NGCP::Panel::Field::E164/ &&
return "Object";
/Compound/ &&
return "Object";
/\+NGCP::Panel::Field::AliasNumber/ &&
return "Array";
/\+NGCP::Panel::Field::PbxGroupAPI/ &&
return "Array";
/\+NGCP::Panel::Field::PbxGroupMemberAPI/ &&
return "Array";
/\+NGCP::Panel::Field::Interval/ &&
return "Object";
/\+NGCP::Panel::Field::DatePicker/ &&
return "String";
/\+NGCP::Panel::Field::NumRangeAPI/ &&
return "String";
# usually {xxx}{id}
/\+NGCP::Panel::Field::/ &&
return "Number";
# default
return "String";
} # SWITCH
}
sub field_to_select_options : Private {
my ($self, $field) = @_;
return join('|',map {
my $value = $_->{value};
my $label = $_->{label};
my $s = defined $value ? "'".$value."'" : 'null';
if (defined $label && length($label)) {
if (!defined $value || (lc($value) ne lc($label))) {
$s.=' ('.$label.')';
}
}
$s;
} @{$field->options});
}
sub get_field_poperties :Private{
my ($self, $field) = @_;
my $name = $field->name;
return () if (
$field->type eq "Hidden" ||
$field->type eq "Button" ||
$field->type eq "Submit" ||
$field->type eq "AddElement" ||
$field->type eq "RmElement" ||
0);
my @types = ();
push @types, 'null' unless ($field->required || $field->validate_when_empty);
my $type;
if($field->type =~ /^\+NGCP::Panel::Field::/) {
if($field->type =~ /E164$/) {
$name = 'primary_number';
} elsif($field->type =~ /AliasNumber/) {
$name = 'alias_numbers';
} elsif($field->type =~ /PbxGroupAPI/) {
$name = 'pbx_group_ids';
} elsif($field->type =~ /Country$/) {
$name = 'country';
} elsif($field->type =~ /LnpCarrier$/) {
$name = 'carrier_id';
} elsif($field->type !~ /Regex|EmailList|Identifier|PosInteger|Interval|Select|DateTime|URI|IPAddress|DatePicker|ProfileNetwork|CFSimpleAPICompound|NumRangeAPI|IntegerList/) { # ...?
$name .= '_id';
}
}
my $enum;
push(@types, $self->field_to_json($field));
if ($field->$_isa('HTML::FormHandler::Field::Select')) {
$enum = $field->options;
}
my $desc = undef;
if($field->element_attr) {
$desc = $field->element_attr->{title}->[0];
}
unless (defined $desc && length($desc) > 0) {
$desc = $field->label;
}
unless (defined $desc && length($desc) > 0) {
$desc = 'to be described ...';
}
my $subfields;
if ($field->has_fields && scalar ($field->fields)) {
my ($firstsub) = $field->fields;
if ($field->isa('HTML::FormHandler::Field::Repeatable') && $firstsub) {
($subfields) = $self->get_collection_properties($firstsub, 1);
} elsif ($firstsub->type eq '+NGCP::Panel::Field::DataTable' && $name =~ /_id$/) {
# don't render subfields (only DataTable Field with Button)
} elsif ($firstsub->type eq '+NGCP::Panel::Field::DataTable' && $name =~ /^(country|timezone)$/) {
# also don't render subfields of country and timezone (they have no _id ending)
} elsif ($field->type eq 'String' && $name =~ /^(domain)$/) {
# another special case, special syntax of domain in subscribers
} else {
($subfields) = $self->get_collection_properties($field, 1);
}
}
return { name => $name, description => $desc, types => \@types, type_original => $field->type,
readonly => $field->readonly,
($enum ? (enum => $enum) : ()),
($subfields ? (subfields => $subfields) : ()),
};
}
sub get_collection_properties {
my ($self, $form, $is_nested) = @_;
my $renderlist = $form->form && !$is_nested
? $form->form->blocks->{fields}->{render_list}
: undef;
my %renderlist = defined $renderlist ? map { $_ => 1 } @{$renderlist} : ();
my @props = ();
my @uploads = ();
foreach my $f($form->fields) {
my $name = $f->name;
next if (defined $renderlist && !exists $renderlist{$name});
my $field_spec = $self->get_field_poperties($f);
next if !$field_spec;
push @props, $field_spec;
push @uploads, $field_spec if $f->type =~/Upload/;
if (my $spec = $f->element_attr->{implicit_parameter}) {
my $f_implicit = clone($f);
foreach my $field_attribute (keys %{$spec}){
$f_implicit->$field_attribute($spec->{$field_attribute});
}
push @props, $self->get_field_poperties($f_implicit);
}
}
@props = sort{$a->{name} cmp $b->{name}} @props;
return (\@props,\@uploads);
}
sub end : Private {
my ($self, $c) = @_;
#$self->log_response($c);
return 1;
}
1;
# vim: set tabstop=4 expandtab: