Bug 37762: REST API: Convert XML payload to JSON for swagger definition validation

Convert JSON response to XML if header content_type: application/xml

Signed-off-by: Jason Robb <jrobb@sekls.org>
Signed-off-by: Lisette Scheer <lisette@bywatersolutions.com>
Signed-off-by: Lucas Gass <lucas@bywatersolutions.com>
This commit is contained in:
Pedro Amorim 2025-09-08 08:25:58 +00:00 committed by Lucas Gass
parent 2a1fa8d091
commit 55fa4609fa
No known key found for this signature in database
GPG Key ID: 60B447398B0D36CA
1 changed files with 235 additions and 1 deletions

View File

@ -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_xml> 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<XML::LibXML::Node> 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<parse_xml> 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;