diff --git a/Koha/REST/V1.pm b/Koha/REST/V1.pm index 5a3be7a578..22038c6cb4 100644 --- a/Koha/REST/V1.pm +++ b/Koha/REST/V1.pm @@ -27,7 +27,8 @@ use Mojolicious::Plugin::OAuth2; use JSON::Validator::Schema::OpenAPIv2; use Try::Tiny qw( catch try ); -use JSON qw( decode_json ); +use JSON qw( encode_json decode_json ); +use XML::LibXML; =head1 NAME @@ -70,12 +71,55 @@ sub startup { Koha::Caches->flush_L1_caches(); Koha::Cache::Memory::Lite->flush(); + # Convert XML payload to JSON and validate against schema + if ( $c->req->headers->content_type && $c->req->headers->content_type =~ /application\/xml/ ) { + try { + my $xml = $c->req->body; + + my $parser = XML::LibXML->new(); + my $doc = $parser->parse_string($xml); + my $root = $doc->documentElement(); + my $json = $self->parse_xml($root); + + $c->req->body( JSON::encode_json($json) ); + + unless ( $self->validate_json_payload( $c, $json ) ) { + return $c->render( status => 400, json => { error => 'Invalid JSON payload' } ); + } + } catch { + + # If the request body is already JSON, don't try to parse it as XML + my $json = $c->req->body; + unless ( $self->validate_json_payload( $c, $json ) ) { + return $c->render( status => 400, json => { error => 'Invalid JSON payload' } ); + } + } + } + return $next->(); } ); + $self->hook( + after_dispatch => sub { + my ($c) = @_; + + # Check if the response header content_type is application/xml + if ( $c->res->headers->content_type && $c->res->headers->content_type eq 'application/xml' ) { + + # Convert JSON to XML + my $xml = to_xml( decode_json( $c->res->body ) ); + + $c->res->body($xml); + } + + return; + } + ); + # Force charset=utf8 in Content-Type header for JSON responses $self->types->type( json => 'application/json; charset=utf8' ); + $self->types->type( xml => 'application/xml' ); # MARC-related types $self->types->type( marcxml => 'application/marcxml+xml' ); @@ -181,4 +225,194 @@ sub startup { $self->plugin( 'Mojolicious::Plugin::OAuth2' => $oauth_configuration ); } +=head3 to_xml + + my $xml_string = to_xml( $json_hashref ); + +Converts a JSON-like hashref into an XML string. + +=cut + +sub to_xml { + my $json = shift; + my $xml = XML::LibXML::Document->new( '1.0', 'UTF-8' ); + + my $root_key = ( keys %$json )[0]; + my $root = $xml->createElement($root_key); + $xml->setDocumentElement($root); # Set the root element + + _json_to_xml( $json->{$root_key}, $root, $xml ); + + return $xml->toString; +} + +=head3 _json_to_xml + + _json_to_xml( $data, $parent_node, $xml_doc ); + +Internal recursive helper for C to build XML elements from nested Perl structures. + +=cut + +sub _json_to_xml { + my ( $json, $parent, $doc ) = @_; + + if ( ref $json eq 'HASH' ) { + foreach my $key ( keys %$json ) { + my $value = $json->{$key}; + if ( ref $value eq 'HASH' ) { + my $elem = $doc->createElement($key); + $parent->appendChild($elem); + _json_to_xml( $value, $elem, $doc ); + } elsif ( ref $value eq 'ARRAY' ) { + foreach my $item (@$value) { + my $elem = $doc->createElement($key); + $parent->appendChild($elem); + _json_to_xml( $item, $elem, $doc ); + } + } else { + my $elem = $doc->createElement($key); + $elem->appendText($value); + $parent->appendChild($elem); + } + } + } elsif ( ref $json eq 'ARRAY' ) { + foreach my $item (@$json) { + my $elem = $doc->createElement('item'); + $parent->appendChild($elem); + _json_to_xml( $item, $elem, $doc ); + } + } else { + my $elem = $doc->createElement('value'); + $elem->appendText($json); + $parent->appendChild($elem); + } +} + +=head3 validate_json_payload + + my $is_valid = $self->validate_json_payload( $c, $payload ); + +Validates a JSON payload against the REST API OpenAPI/Swagger schema. +Returns C<1> if valid, C<0> if there are errors. + +=cut + +sub validate_json_payload { + my ( $self, $c, $payload ) = @_; + + # Load the OpenAPI schema + my $spec_file = $self->home->rel_file("api/v1/swagger/swagger_bundle.json"); + if ( !-f $spec_file ) { + $spec_file = $self->home->rel_file("api/v1/swagger/swagger.yaml"); + } + + my $schema = JSON::Validator::Schema::OpenAPIv2->new; + $schema->resolve($spec_file); + + my $spec = $schema->bundle->data; + + # Validate the JSON payload against the schema + my $validator = JSON::Validator->new; + $validator->schema($spec); + + my $errors = $validator->validate($payload); + + # Print the validation errors + if ($errors) { + + #TODO: Do something here? + } + + # Return 1 if the payload is valid, 0 otherwise + return $errors ? 0 : 1; +} + +=head3 parse_xml + + my $hashref = $self->parse_xml( $xml_node ); + +Converts an C into a Perl hashref, using the OpenAPI schema definitions to correctly identify and cast array properties. + +=cut + +sub parse_xml { + my ( $self, $node ) = @_; + my $hash = {}; + + my $spec_file = $self->home->rel_file("api/v1/swagger/swagger_bundle.json"); + if ( !-f $spec_file ) { + $spec_file = $self->home->rel_file("api/v1/swagger/swagger.yaml"); + } + + my $schema = JSON::Validator::Schema::OpenAPIv2->new($spec_file); + + $self->_parse_node( $node, $hash, 0, $schema->data->{definitions} ); + + return $hash; +} + +=head3 _parse_node + + $self->_parse_node( $node, $hash, $is_array, $schema_definitions ); + +Internal recursive helper for C to populate a Perl hashref from XML elements. + +=cut + +sub _parse_node { + my ( $self, $node, $hash, $is_array, $schema_definitions ) = @_; + + my $name = $node->localName(); + my $properties = $schema_definitions->{$name}->{properties}; + + my @array_properties; + foreach my $key ( keys %$properties ) { + my $property = $properties->{$key}; + push @array_properties, $key if $property->{type} && $property->{type} eq 'array'; + } + + if ( $node->hasChildNodes() ) { + my $child_hash = {}; + foreach my $child ( $node->childNodes() ) { + if ( $child->nodeType() == 1 ) { # 1 is the node type for elements + if ( grep { $_ eq $child->localName() } @array_properties ) { + $self->_parse_node( $child, $child_hash, 1, $schema_definitions ); + } else { + $self->_parse_node( $child, $child_hash, 0, $schema_definitions ); + } + } + } + if (%$child_hash) { + if ( $is_array || exists $hash->{$name} ) { + if ( ref( $hash->{$name} ) eq 'ARRAY' ) { + push @{ $hash->{$name} }, $child_hash; + } else { + $hash->{$name} = [$child_hash]; + } + } else { + $hash->{$name} = $child_hash; + } + } else { + my $text = $node->textContent(); + $text =~ s/^\s+//; # remove leading whitespace + $text =~ s/\s+$//; # remove trailing whitespace + if ( $is_array || exists $hash->{$name} ) { + if ( ref( $hash->{$name} ) eq 'ARRAY' ) { + push @{ $hash->{$name} }, $text; + } else { + $hash->{$name} = [$text]; + } + } else { + $hash->{$name} = $text; + } + } + } else { + my $text = $node->textContent(); + $text =~ s/^\s+//; # remove leading whitespace + $text =~ s/\s+$//; # remove trailing whitespace + $hash->{$name} = $text; + } +} + 1;