1# Copyrights 2007-2021 by [Mark Overmeer <markov@cpan.org>].
2#  For other contributors see ChangeLog.
3# See the manual pages for details on the licensing terms.
4# Pod stripped from pm file by OODoc 2.02.
5# This code is part of distribution XML-Compile-SOAP.  Meta-POD processed
6# with OODoc into POD and HTML manual-pages.  See README.md
7# Copyright Mark Overmeer.  Licensed under the same terms as Perl itself.
8
9package XML::Compile::SOAP;
10use vars '$VERSION';
11$VERSION = '3.27';
12
13
14use warnings;
15use strict;
16
17use Log::Report          'xml-compile-soap';
18
19use XML::Compile         ();
20use XML::Compile::Util   qw(SCHEMA2001 SCHEMA2001i pack_type
21   unpack_type type_of_node);
22use XML::Compile::Cache  ();
23use XML::Compile::SOAP::Util qw/:xop10 SOAP11ENC/;
24
25use Time::HiRes          qw/time/;
26use MIME::Base64         qw/decode_base64/;
27
28# XML::Compile::WSA::Util often not installed
29use constant WSA10 => 'http://www.w3.org/2005/08/addressing';
30
31sub _xop_enabled() { exists $INC{'XML/Compile/XOP.pm'} }
32
33
34sub new($@)
35{   my $class = shift;
36
37    error __x"you can only instantiate sub-classes of {class}", class => $class
38        if $class eq __PACKAGE__;
39
40    (bless {}, $class)->init( {@_} );
41}
42
43sub init($)
44{   my ($self, $args) = @_;
45    $self->{XCS_mime}   = $args->{media_type} || 'application/soap+xml';
46
47    my $schemas = $self->{XCS_schemas} = $args->{schemas}
48     || XML::Compile::Cache->new(allow_undeclared => 1
49          , any_element => 'ATTEMPT', any_attribute => 'ATTEMPT');
50
51    UNIVERSAL::isa($schemas, 'XML::Compile::Cache')
52        or panic "schemas must be a Cache object";
53
54    $self;
55}
56
57sub _initSOAP($)
58{   my ($thing, $schemas) = @_;
59    return $thing
60        if $schemas->{did_init_SOAP}++;   # ugly
61
62    $schemas->addPrefixes(xsd => SCHEMA2001, xsi => SCHEMA2001i);
63
64    $thing;
65}
66
67
68{   my (%registered, %envelope);
69    sub register($)
70    { my ($class, $uri, $env, $opclass) = @_;
71      $registered{$uri} = $class;
72      $envelope{$env}   = $opclass if $env;
73    }
74    sub plugin($)       { $registered{$_[1]} }
75    sub fromEnvelope($) { $envelope{$_[1]} }
76    sub registered($)   { values %registered }
77}
78
79#--------------------
80
81sub version()   {panic "not implemented"}
82sub mediaType() {shift->{XCS_mime}}
83
84
85sub schemas() {
86use Carp 'cluck';
87ref $_[0] or cluck;
88shift->{XCS_schemas}}
89
90#--------------------
91
92sub compileMessage($@)
93{   my ($self, $direction, %args) = @_;
94    $args{style} ||= 'document';
95
96      $direction eq 'SENDER'   ? $self->_sender(%args)
97    : $direction eq 'RECEIVER' ? $self->_receiver(%args)
98    : error __x"message direction is 'SENDER' or 'RECEIVER', not `{dir}'"
99         , dir => $direction;
100}
101
102
103sub messageStructure($)
104{   my ($thing, $xml) = @_;
105    my $env = $xml->isa('XML::LibXML::Document') ? $xml->documentElement :$xml;
106
107    my (@header, @body, $wsa_action);
108    if(my ($header) = $env->getChildrenByLocalName('Header'))
109    {   @header = map { $_->isa('XML::LibXML::Element') ? type_of_node($_) : ()}
110           $header->childNodes;
111
112        if(my $wsa = ($header->getChildrenByTagNameNS(WSA10, 'Action'))[0])
113        {   $wsa_action = $wsa->textContent;
114            for($wsa_action) { s/^\s+//; s/\s+$//; s/\s{2,}/ /g }
115        }
116    }
117
118    if(my ($body) = $env->getChildrenByLocalName('Body'))
119    {   @body = map { $_->isa('XML::LibXML::Element') ? type_of_node($_) : () }
120           $body->childNodes;
121    }
122
123    +{ header     => \@header
124     , body       => \@body
125     , wsa_action => $wsa_action
126     };
127}
128
129#------------------------------------------------
130# Sender
131
132sub _sender(@)
133{   my ($self, %args) = @_;
134
135    error __"option 'role' only for readers"  if $args{role};
136    error __"option 'roles' only for readers" if $args{roles};
137
138    my $hooks = $args{hooks}   # make copy of calling hook-list
139      = $args{hooks} ? [ @{$args{hooks}} ] : [];
140
141    my @mtom;
142    push @$hooks, $self->_writer_xop_hook(\@mtom)
143		if _xop_enabled;
144
145    my ($body,  $blabels) = $args{create_body}
146       ? $args{create_body}->($self, %args)
147       : $self->_writer_body(\%args);
148    my ($faults, $flabels) = $self->_writer_faults(\%args, $args{faults});
149
150    my ($header, $hlabels) = $self->_writer_header(\%args);
151    push @$hooks, $self->_writer_hook($self->envType('Header'), @$header);
152
153    my $style = $args{style} || 'none';
154    if($style eq 'document')
155    {   push @$hooks, $self->_writer_hook($self->envType('Body')
156          , @$body, @$faults);
157    }
158    elsif($style eq 'rpc')
159    {   my $procedure = $args{procedure} || $args{body}{procedure}
160            or error __x"sending operation requires procedure name with RPC";
161
162        my $use = $args{use} || $args{body}{use} || 'literal';
163        my $bt  = $self->envType('Body');
164        push @$hooks, $use eq 'literal'
165           ? $self->_writer_body_rpclit_hook($bt, $procedure, $body, $faults)
166           : $self->_writer_body_rpcenc_hook($bt, $procedure, $body, $faults);
167    }
168    else
169    {   error __x"unknown style `{style}'", style => $style;
170    }
171
172    #
173    # Pack everything together in one procedure
174    #
175
176    my $envelope = $self->_writer($self->envType('Envelope'), %args);
177
178    sub
179    {   my ($values, $charset) = ref $_[0] eq 'HASH' ? @_ : ( {@_}, undef);
180        my %copy  = %$values;  # do not destroy the calling hash
181        my $doc   = delete $copy{_doc}
182          || XML::LibXML::Document->new('1.0', $charset || 'UTF-8');
183
184        my %data = (
185            Body   => delete $copy{Body} || {},
186            Header => delete $copy{Header},
187        );
188
189        foreach my $label (@$hlabels)
190        {   exists $copy{$label} or next;
191            $data{Header}{$label} ||= delete $copy{$label};
192        }
193
194        foreach my $label (@$blabels, @$flabels)
195        {   exists $copy{$label} or next;
196            $data{Body}{$label} ||= delete $copy{$label};
197        }
198
199        if(@$blabels==2 && !keys %{$data{Body}} ) # ignore 'Fault'
200        {  # even when no params, we fill at least one body element
201            $data{Body}{$blabels->[0]} = \%copy;
202        }
203        elsif(keys %copy)
204        {   trace __x"available blocks: {blocks}",
205                 blocks => [ sort @$hlabels, @$blabels, @$flabels ];
206            error __x"call data not used: {blocks}", blocks => [keys %copy];
207        }
208
209        @mtom = ();   # filled via hook
210
211#use Data::Dumper;
212#warn "REPROCESSED: ", Dumper \%data;
213        my $root = $envelope->($doc, \%data)
214            or return;
215
216        $doc->setDocumentElement($root);
217
218        return ($doc, \@mtom)
219            if wantarray;
220
221        @mtom == 0
222            or error __x"{nr} XOP objects lost in sender"
223                 , nr => scalar @mtom;
224        $doc;
225    };
226}
227
228sub _writer_hook($$@)
229{   my ($self, $type, @do) = @_;
230
231    my $code = sub
232     {  my ($doc, $data, $path, $tag) = @_;
233        UNIVERSAL::isa($data, 'XML::LibXML::Element')
234            and return $data;
235
236        my %data = %$data;
237        my @h = @do;
238        my @childs;
239        while(@h)
240        {   my ($k, $c) = (shift @h, shift @h);
241            if(my $v = delete $data{$k})
242            {   push @childs, $c->($doc, $v);
243            }
244        }
245
246        if(keys %data)
247        {   warning __x"unused values {names}", names => [keys %data];
248            my @h = @do; my @keys;
249            while(@h) { push @keys, shift @h; shift @h}
250            trace "expected: ". join ' ', @keys;
251		}
252
253        my $node = $doc->createElement($tag);
254        $node->appendChild($_) for @childs;
255        $node;
256      };
257
258   +{ type => $type, replace => $code };
259}
260
261sub _writer_body_rpclit_hook($$$$$)
262{   my ($self, $type, $procedure, $params, $faults) = @_;
263    my @params   = @$params;
264    my @faults   = @$faults;
265    my $schemas  = $self->schemas;
266
267    my $proc     = $schemas->prefixed($procedure);
268    my ($prefix) = split /\:/, $proc;
269    my $prefdef  = $schemas->prefix($prefix);
270    my $proc_ns  = $prefdef->{uri};
271    $prefdef->{used} = 0;
272
273    my $code   = sub
274     {  my ($doc, $data, $path, $tag) = @_;
275        UNIVERSAL::isa($data, 'XML::LibXML::Element')
276            and return $data;
277
278        my %data = %$data;
279        my @f = @faults;
280        my (@fchilds, @pchilds);
281        while(@f)
282        {   my ($k, $c) = (shift @f, shift @f);
283            my $v = delete $data{$k};
284            push @fchilds, $c->($doc, $v) if defined $v;
285        }
286        my @p = @params;
287        while(@p)
288        {   my ($k, $c) = (shift @p, shift @p);
289            my $v = delete $data{$k};
290            push @pchilds, $c->($doc, $v) if defined $v;
291        }
292        warning __x"unused values {names}", names => [keys %data]
293            if keys %data;
294
295        my $proc = $doc->createElement($proc);
296        $proc->setNamespace($proc_ns, $prefix, 0);
297        $proc->setAttribute("SOAP-ENV:encodingStyle", SOAP11ENC);
298
299        $proc->appendChild($_) for @pchilds;
300
301        my $node = $doc->createElement($tag);
302        $node->appendChild($proc);
303        $node->appendChild($_) for @fchilds;
304        $node;
305     };
306
307   +{ type => $type, replace => $code };
308}
309
310*_writer_body_rpcenc_hook = \&_writer_body_rpclit_hook;
311
312sub _writer_header($)
313{   my ($self, $args) = @_;
314    my (@rules, @hlabels);
315
316    my $header  = $args->{header} || [];
317    my $soapenv = $self->envelopeNS;
318
319    foreach my $h (ref $header eq 'ARRAY' ? @$header : $header)
320    {   my $part    = $h->{parts}[0];
321        my $label   = $part->{name};
322        my $code    = $part->{writer};
323        if($part->{element})
324        {   $code ||= $self->_writer_part_element($args, $part);
325        }
326        elsif(my $type = $part->{type})
327        {   $code ||= $self->_writer_part_type($args, $part);
328			$label = (unpack_type $part->{name})[1];
329        }
330        else
331        {   error __x"header part {name} has neither `element' nor `type'"
332              , name => $label;
333        }
334
335        push @rules, $label => $code;
336        push @hlabels, $label;
337    }
338
339    (\@rules, \@hlabels);
340}
341
342sub _writer_body($)
343{   my ($self, $args) = @_;
344    my (@rules, @blabels);
345
346    my $body  = $args->{body} || $args->{fault};
347    my $use   = $body->{use}  || 'literal';
348#   $use eq 'literal'
349#       or error __x"RPC encoded not supported by this version";
350
351    my $parts = $body->{parts} || [];
352    my $style = $args->{style};
353    local $args->{is_rpc_enc} = $style eq 'rpc' && $use eq 'encoded';
354
355    foreach my $part (@$parts)
356    {   my $label  = $part->{name};
357        my $code;
358        if($part->{element})
359        {   $code  = $self->_writer_part_element($args, $part);
360        }
361        elsif(my $type = $part->{type})
362        {   $code  = $self->_writer_part_type($args, $part);
363			$label = (unpack_type $part->{name})[1];
364        }
365        else
366        {   error __x"body part {name} has neither `element' nor `type'"
367              , name => $label;
368        }
369
370        push @rules, $label => $code;
371        push @blabels, $label;
372    }
373
374    (\@rules, \@blabels);
375}
376
377sub _writer_part_element($$)
378{   my ($self, $args, $part) = @_;
379    my $element = $part->{element};
380    my $soapenv = $self->envelopeNS;
381
382    $part->{writer} ||= $self->_writer
383      ( $element, %$args
384      , include_namespaces  => sub {$_[0] ne $soapenv && $_[2]}
385      , xsi_type_everywhere => $args->{is_rpc_enc}
386      );
387}
388
389sub _writer_part_type($$)
390{   my ($self, $args, $part) = @_;
391
392    $args->{style} eq 'rpc'
393        or error __x"part {name} uses `type', only for rpc not {style}"
394             , name => $part->{name}, style => $args->{style};
395
396    return $part->{writer}
397        if $part->{writer};
398
399    my $soapenv = $self->envelopeNS;
400
401    $part->{writer} = $self->schemas->compileType
402      ( WRITER  => $part->{type}, %$args, element => $part->{name}
403      , include_namespaces => sub {$_[0] ne $soapenv && $_[2]}
404      , xsi_type_everywhere => $args->{is_rpc_enc}
405      );
406}
407
408sub _writer_faults($) { ([], []) }
409
410sub _writer_xop_hook($)
411{   my ($self, $xop_objects) = @_;
412
413    my $collect_objects = sub {
414        my ($doc, $val, $path, $tag, $r) = @_;
415        return $r->($doc, $val)
416            unless UNIVERSAL::isa($val, 'XML::Compile::XOP::Include');
417
418        my $node = $val->xmlNode($doc, $path, $tag);
419        push @$xop_objects, $val;
420        $node;
421      };
422
423   +{ extends => 'xsd:base64Binary', replace => $collect_objects };
424}
425
426#------------------------------------------------
427# Receiver
428
429sub _receiver(@)
430{   my ($self, %args) = @_;
431
432    error __"option 'destination' only for writers"
433        if $args{destination};
434
435    error __"option 'mustUnderstand' only for writers"
436        if $args{understand};
437
438# roles are not checked (yet)
439#   my $roles  = $args{roles} || $args{role} || 'ULTIMATE';
440#   my @roles  = ref $roles eq 'ARRAY' ? @$roles : $roles;
441
442    my $header = $self->_reader_header(\%args);
443
444    my $xops;  # forward backwards pass-on
445    my $body   = $self->_reader_body(\%args, \$xops);
446
447    my $style  = $args{style} || 'document';
448    my $kind   = $args{kind}  || 'request-response';
449    if($style eq 'rpc')
450    {   my $procedure = $args{procedure} || $args{body}{procedure};
451        keys %{$args{body}}==0 || $procedure
452            or error __x"receiving operation requires procedure name with RPC";
453
454        my $use = $args{use} || $args{body}{use} || 'literal';
455        $body = $use eq 'literal'
456           ? $self->_reader_body_rpclit_wrapper($procedure, $body)
457           : $self->_reader_body_rpcenc_wrapper($procedure, $body);
458    }
459    elsif($style ne 'document')
460    {   error __x"unknown style `{style}'", style => $style;
461    }
462
463    # faults are always possible
464    push @$body, $self->_reader_fault_reader;
465
466    my @hooks  = @{$self->{hooks} || []};
467    push @hooks
468      , $self->_reader_hook($self->envType('Header'), $header)
469      , $self->_reader_hook($self->envType('Body'),   $body  );
470
471    #
472    # Pack everything together in one procedure
473    #
474
475    my $envelope = $self->_reader($self->envType('Envelope')
476      , %args, hooks => \@hooks);
477
478    # add simplified fault information
479    my $faultdec = $self->_reader_faults(\%args, $args{faults});
480
481    sub
482    {   (my $xml, $xops) = @_;
483        my $data  = $envelope->($xml);
484        my @pairs = ( %{delete $data->{Header} || {}}
485                    , %{delete $data->{Body}   || {}});
486        while(@pairs)
487        {  my $k       = shift @pairs;
488           $data->{$k} = shift @pairs;
489        }
490
491        $faultdec->($data);
492        $data;
493    };
494}
495
496sub _reader_hook($$)
497{   my ($self, $type, $do) = @_;
498    my %trans = map +($_->[1] => [ $_->[0], $_->[2] ]), @$do; # we need copies
499    my $envns = $self->envelopeNS;
500
501    my $code  = sub
502     {  my ($xml, $trans, $path, $label) = @_;
503        my %h;
504        foreach my $child ($xml->childNodes)
505        {   next unless $child->isa('XML::LibXML::Element');
506            my $type = type_of_node $child;
507            if(my $t = $trans{$type})
508            {   my ($label, $code) = @$t;
509                my $v = $code->($child) or next;
510                   if(!defined $v)        { }
511                elsif(!exists $h{$label}) { $h{$label} = $v }
512                elsif(ref $h{$label} eq 'ARRAY') { push @{$h{$label}}, $v }
513                else { $h{$label} = [ $h{$label}, $v ] }
514                next;
515            }
516            else
517            {   $h{$type} = $child;
518                trace __x"node {type} not understood, expected are {has}",
519                    type => $type, has => [sort keys %trans];
520            }
521
522            return ($label => $self->replyMustUnderstandFault($type))
523                if $child->getAttributeNS($envns, 'mustUnderstand') || 0;
524        }
525        ($label => \%h);
526     };
527
528   +{ type    => $type
529    , replace => $code
530    };
531
532}
533
534sub _reader_body_rpclit_wrapper($$)
535{   my ($self, $procedure, $body) = @_;
536    my %trans = map +($_->[1] => [ $_->[0], $_->[2] ]), @$body;
537
538    # this should use key_rewrite, but there is no $wsdl here
539    # my $label = $wsdl->prefixed($procedure);
540    my $label = (unpack_type $procedure)[1];
541
542    my $code = sub
543      { my $xml = shift or return {};
544        my %h;
545        foreach my $child ($xml->childNodes)
546        {   $child->isa('XML::LibXML::Element') or next;
547            my $type = type_of_node $child;
548            if(my $t = $trans{$type})
549                 { $h{$t->[0]} = $t->[1]->($child) }
550            else { $h{$type} = $child }
551        }
552        \%h;
553      };
554
555    [ [ $label => $procedure => $code ] ];
556}
557
558sub _reader_header($)
559{   my ($self, $args) = @_;
560    my $header = $args->{header} || [];
561    my @rules;
562
563    foreach my $h (@$header)
564    {   my $part    = $h->{parts}[0];
565        my $label   = $part->{name};
566        my $element = $part->{element};
567        my $code    = $part->{reader} ||= $self->_reader($element, %$args);
568        push @rules, [$label, $element, $code];
569    }
570
571    \@rules;
572}
573
574sub _reader_body($$)
575{   my ($self, $args, $refxops) = @_;
576    my $body  = $args->{body};
577    my $parts = $body->{parts} || [];
578    my @hooks = @{$args->{hooks} || []};
579    push @hooks, $self->_reader_xop_hook($refxops)
580		if _xop_enabled;
581
582    local $args->{hooks} = \@hooks;
583
584    my @rules;
585    foreach my $part (@$parts)
586    {   my $label = $part->{name};
587
588        my ($t, $code);
589        if($part->{element})
590        {   ($t, $code) = $self->_reader_body_element($args, $part) }
591        elsif($part->{type})
592        {   ($t, $code) = $self->_reader_body_type($args, $part) }
593        else
594        {   error __x"part {name} has neither element nor type specified"
595              , name => $label;
596        }
597        push @rules, [ $label, $t, $code ];
598    }
599
600#use Data::Dumper;
601#warn "RULES=", Dumper \@rules, $parts;
602    \@rules;
603}
604
605sub _reader_body_element($$)
606{   my ($self, $args, $part) = @_;
607
608    my $element = $part->{element};
609    my $code    = $part->{reader} || $self->_reader($element, %$args);
610
611    ($element, $code);
612}
613
614sub _reader_body_type($$)
615{   my ($self, $args, $part) = @_;
616    my $name = $part->{name};
617
618    $args->{style} eq 'rpc'
619        or error __x"only rpc style messages can use 'type' as used by {part}"
620              , part => $name;
621
622    return $part->{reader}
623        if $part->{reader};
624
625    my $type = $part->{type};
626    my ($ns, $local) = unpack_type $type;
627
628    my $r = $part->{reader} =
629        $self->schemas->compileType
630          ( READER => $type, %$args
631          , element => $name # $args->{body}{procedure}
632          );
633
634    ($name, $r);
635}
636
637sub _reader_faults($)
638{   my ($self, $args) = @_;
639    sub { shift };
640}
641
642sub _reader_xop_hook($)
643{   my ($self, $refxops) = @_;
644
645    my $xop_merge = sub
646      { my ($xml, $args, $path, $type, $r) = @_;
647        if(my $incls = $xml->getElementsByTagNameNS(XOP10, 'Include'))
648        {   my $href = $incls->shift->getAttribute('href') || ''
649                or return ($type => $xml);
650
651            $href =~ s/^cid://;
652            my $xop  = $$refxops->{$href}
653                or return ($type => $xml);
654
655            return ($type => $xop);
656        }
657
658        ($type => decode_base64 $xml->textContent);
659      };
660
661   +{ type => 'xsd:base64Binary', replace => $xop_merge };
662}
663
664sub _reader(@) { shift->schemas->reader(@_) }
665sub _writer(@) { shift->schemas->writer(@_) }
666
667#------------------------------------------------
668
669
670sub roleURI($) { panic "not implemented" }
671
672
673sub roleAbbreviation($) { panic "not implemented" }
674
675
676sub replyMustUnderstandFault($) { panic "not implemented" }
677
678#----------------------
679
680
6811;
682