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: