File Coverage

blib/lib/XML/XForms/Validate.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package XML::XForms::Validate;
2              
3 18     18   6512816 use 5.008;
  18         227  
  18         822  
4 18     18   107 use strict;
  18         32  
  18         719  
5 18     18   93 use warnings;
  18         53  
  18         989  
6             our $VERSION = '0.9';
7              
8 18     18   12571 use XML::LibXML;
  0            
  0            
9             use XML::LibXML::XPathContext;
10             use XML::Schema::Type::Builtin;
11             use Time::Piece;
12             use Time::Seconds;
13              
14             use fields qw(base model refs instances submissions parser schemas binds);
15             use constant NSURI_XF => 'http://www.w3.org/2002/xforms';
16             use constant NSURI_XSD => 'http://www.w3.org/2001/XMLSchema-datatypes';
17             use constant NSURI_XSI => 'http://www.w3.org/2001/XMLSchema-instance';
18             use constant NSURI_XS => 'http://www.w3.org/2001/XMLSchema';
19             use constant NSURI_XI => 'http://www.w3.org/2001/XInclude';
20             use Exporter qw(import);
21             our @EXPORT_OK = qw(&validate &normalize);
22              
23             # General-purpose globals. Each object uses specialized private instances as well.
24             my $parser = new XML::LibXML;
25              
26             my $xpc = new XML::LibXML::XPathContext(new XML::LibXML::Document());
27             $xpc->registerNs('xf', NSURI_XF);
28              
29             my $cbs = new XML::LibXML::InputCallback();
30             $cbs->register_callbacks([ sub {die}, sub {die}, sub {die}, sub {die} ]);
31              
32             my $doc = new XML::LibXML::Document();
33              
34             sub normalize {
35             shift if UNIVERSAL::isa($_[0],'XML::XForms::Validate');
36             my ($dom, $noclean) = @_;
37             return $dom if !ref($dom);
38             my $parser = new XML::LibXML(ext_ent_handler => sub {die});
39             $parser->validation(0);
40             $parser->load_ext_dtd(0);
41             $parser->expand_xinclude(0);
42             $parser->expand_entities(0);
43             $parser->input_callbacks($cbs);
44             $parser->clean_namespaces(1);
45             $parser->no_network(1);
46             $dom = { '' => $dom } if ref($dom) ne 'HASH';
47             my $result = { };
48             foreach my $id (keys %$dom) {
49             my $old = $$dom{$id};
50             $old = $old->documentElement if $old->can('documentElement');
51             my $new = $parser->parse_string($old->toStringC14N(0));
52             normalizeRecursive($new->documentElement, $noclean, {});
53             $$result{$id} ||= $parser->parse_string($new->documentElement->toStringC14N(0));
54             $$result{''} = $$result{$id} if ($id ne '' && $$dom{$id} eq $$dom{''});
55             }
56              
57             return (ref($_[0]) eq 'HASH'?$result:$$result{''});
58             }
59              
60             my %allowed = map { (eval('&XML_'.$_.'_NODE') => 1) } qw(ELEMENT ATTRIBUTE TEXT);
61             $allowed{&XML_NAMESPACE_DECL} = 1;
62             sub normalizeRecursive {
63             my ($parent, $noclean, $usedns) = @_;
64              
65             my %newns;
66             $newns{($parent->prefix||'').'='.($parent->namespaceURI||'')} = 1;
67             foreach my $attr ($parent->attributes) {
68             next if !$attr || $attr->nodeType == XML_NAMESPACE_DECL;
69             $newns{($attr->prefix||'').'='.($attr->namespaceURI||'')} = 1;
70             }
71              
72             foreach my $node ($parent->childNodes) {
73             if ($allowed{$node->nodeType} && ($node->namespaceURI||'') ne NSURI_XI) {
74             normalizeRecursive($node, \%newns);
75             } else {
76             $parent->removeChild($node);
77             }
78             }
79              
80             if (!$noclean) {
81             foreach my $ns ($parent->getNamespaces) {
82             my $prefix = $ns->nodeName || '';
83             $prefix =~ s/^xmlns:?//;
84             next if delete $newns{$prefix.'='.($ns->nodeValue||'')};
85             $parent->setNamespaceDeclURI($prefix, undef);
86             }
87             %$usedns = (%$usedns, %newns);
88             }
89             }
90              
91             sub new {
92             my XML::XForms::Validate $self = shift;
93             $self = fields::new($self) unless (ref $self);
94              
95             $self->{parser} = new XML::LibXML();
96             $self->{parser}->no_network(1);
97             $self->{parser}->input_callbacks($cbs);
98             $self->{parser}->{ext_ent_handler} = sub { die };
99             $self->{parser}->clean_namespaces(1);
100             $self->{parser}->expand_xinclude(0);
101             $self->{parser}->validation(0);
102             $self->{parser}->load_ext_dtd(0);
103             $self->{parser}->no_network(1);
104              
105             my %options = @_;
106             my ($xforms, $model, $base) = @options{'xforms', 'model', 'base'};
107             $xforms = $self->getDom($xforms, $base);
108              
109             my $default;
110             my ($id, $node);
111             foreach my $m ($xpc->findnodes('//xf:model', $xforms)) {
112             $id = $m->getAttribute('id');
113             $id = '' if !defined $id;
114             $default = $id if !defined $default;
115             next if defined $model && $model ne $id;
116             $self->{model} = $m;
117             $model = $id;
118             last;
119             }
120              
121             my $anon = 0;
122             $self->processBinds($self->{model}, [], $anon);
123              
124             $self->{refs} = [];
125             $self->processRefs($xpc->findnodes('/*[1]', $xforms), [], $model, $default);
126              
127             if (!$xpc->findnodes('./xf:instance', $self->{model})) {
128             my $instelt = $xforms->createElementNS(NSURI_XF, 'xf:instance');
129             $self->{model}->addChild($instelt);
130             $node = $xforms->createElement('instanceData');
131             $instelt->addChild($node);
132              
133             foreach my $ui (@{$self->{refs}}) {
134             next unless @$ui == 1;
135             $node->addChild($xforms->createElement($$ui[0]));
136             }
137             }
138              
139             $self->{instances} = {};
140             $self->{schemas} = {};
141             foreach my $instance ($xpc->findnodes('./xf:instance', $self->{model})) {
142             $id = $instance->getAttribute('id');
143             $id = '' if !defined $id;
144             if (defined (my $link = $instance->getAttribute('src'))) {
145             $node = $self->{parser}->parse_file($link);
146             } else {
147             ($node) = $xpc->findnodes('./*', $instance);
148             $node = $self->{parser}->parse_string($node->toStringC14N());
149             }
150             $node = $node->documentElement();
151             $self->{instances}{$id} ||= $node;
152             $self->{instances}{''} ||= $node;
153              
154             my %loc;
155             my $loc;
156             $loc = $node->getAttributeNS(NSURI_XSI, 'schemaLocation');
157             %loc = split(/\s+/, $loc) if $loc;
158             $loc = $node->getAttributeNS(NSURI_XSI, 'noNamespaceSchemaLocation');
159             $loc{''} = $loc if $loc;
160             my $nsuri = $node->namespaceURI || '';
161             $self->{schemas}{$id} = new XML::LibXML::Schema(location => $loc{$nsuri})
162             if exists $loc{$nsuri};
163             }
164              
165             $self->{submissions} = {};
166             foreach my $s ($xpc->findnodes('./xf:submission', $self->{model})) {
167             $id = $s->getAttribute('id');
168             next unless defined $id && !$self->{submissions}{$id};
169             $self->{submissions}{$id} = { id => $id };
170             if (my $b = $s->getAttribute('bind')) {
171             die "Invalid XForms document: referenced bind \"$b\" not found" unless exists $self->{binds}{$b};
172             $self->{submissions}{$id}{'ref'} = [ @{$self->{binds}{$s->getAttribute('bind')}{nodeset}} ];
173             } else {
174             $self->{submissions}{$id}{'ref'} = [ $s->getAttribute('ref') || '/' ];
175             }
176             $self->{submissions}{''} = $self->{submissions}{$id} unless $self->{submissions}{''};
177             }
178             die "submission element is missing in model $model" unless %{$self->{submissions}};
179              
180             return $self;
181             }
182              
183             sub getDom {
184             my XML::XForms::Validate $self = shift;
185             my ($data, $base) = @_;
186              
187             if (@_ == 2) {
188             $self->{parser}->base_uri($base);
189             $self->{base} = $base;
190             }
191              
192             if (!ref($data)) {
193             if (@_ == 2 && !defined $base) {
194             $self->{parser}->base_uri($data);
195             $self->{base} = $data;
196             }
197             $data = $parser->parse_file($data);
198             } elsif (UNIVERSAL::isa($data, 'GLOB')) {
199             $data = $parser->parse_fh($data, $base);
200             } elsif (ref($data) eq 'SCALAR') {
201             $data = $parser->parse_string($$data, $base);
202             }
203             die "Not an XML::LibXML DOM" unless UNIVERSAL::isa($data, 'XML::LibXML::Document');
204              
205             return $data;
206             }
207              
208             my @mips = qw(type readonly required relevant constraint calculate p3ptype);
209             sub processBinds {
210             my XML::XForms::Validate $self = shift;
211             my $node = shift;
212             my $context = shift;
213             for my $anon ($_[0]) {
214             foreach my $b ($xpc->findnodes('./xf:bind', $node)) {
215             my $id = $b->getAttribute('id');
216             $id = ' '.($anon++) unless defined $id;
217             next if $self->{binds}{$id} || !$b->getAttribute('nodeset');
218             my $ctx = [ @$context, $b->getAttribute('nodeset') ];
219             $self->{binds}{$id} = { nodeset => $ctx, id => $id, node => $b };
220             foreach my $a (@mips) {
221             $self->{binds}{$id}{$a} = $b->getAttribute($a) if $b->getAttribute($a);
222             }
223             $self->processBinds($b, $ctx, $anon);
224             }
225             }
226             }
227              
228             my %formControls = map {$_=>1} qw(input secret textarea output upload range submit select select1 setvalue);
229             sub processRefs {
230             my XML::XForms::Validate $self = shift;
231             my ($node, $context, $model, $default) = @_;
232              
233             foreach my $ui ($node->childNodes) {
234             next unless $ui->isa('XML::LibXML::Element');
235             my $ctx = $context;
236             my $newdefault = $default;
237              
238             if (($ui->namespaceURI||'') eq NSURI_XF) {
239             my $id = $ui->localName eq 'model'?($ui->getAttribute('id')||''):($ui->getAttribute('model')||$default);
240             next if $id ne $model || $ui->localName eq 'instance';
241             $newdefault = $id;
242             my @expr;
243             if (my $b = $ui->getAttribute('bind')) {
244             die "Invalid XForms document: referenced bind \"$b\" not found" unless exists $self->{binds}{$b};
245             @expr = @{$self->{binds}{$b}{'nodeset'}}
246             }
247             $expr[0] ||= $ui->getAttribute('nodeset');
248             $expr[0] ||= $ui->getAttribute('ref');
249              
250             if (defined $expr[0]) {
251             $ctx = [ @$context, @expr ];
252             push @{$self->{refs}}, $ctx if exists $formControls{$ui->localName};
253             }
254             }
255             $self->processRefs($ui, $ctx, $model, $newdefault);
256             }
257             }
258              
259             use Carp qw(cluck);
260             sub findNodes {
261             my XML::XForms::Validate $self = shift;
262             my ($expr, $context, $xpath) = @_;
263             cluck("undef") if !$expr;
264             my @expr = @$expr;
265             my $last = pop @expr;
266             foreach my $e (@expr) {
267             ($context) = $xpath->findnodes($e, $context);
268             }
269             my @nodes = $xpath->findnodes($last, $context);
270             return @nodes;
271             }
272              
273             sub findBoolean {
274             my XML::XForms::Validate $self = shift;
275             my ($expr, $context, $xpath) = @_;
276             return $xpath->find("boolean($expr)", $context) && 1;
277             }
278              
279             sub getInput {
280             my XML::XForms::Validate $self = shift;
281             my ($input, $orig, $submission) = @_;
282              
283             my $dom = {};
284             foreach my $key (keys %{$orig}) {
285             $$dom{$key} ||= $self->{parser}->parse_string($$orig{$key}->toString())->documentElement();
286             $$dom{''} = $$dom{$key} if $key ne '' && $$orig{$key} eq $$orig{''};
287             }
288              
289             my $ixpc = makeXPathContext(sub {
290             my ($id) = @_;
291             my $result = XML::LibXML::NodeList->new;
292             $result->push($$dom{$id});
293             return $result;
294             });
295              
296             my ($node) = $self->findNodes($submission, $$dom{''}, $ixpc);
297              
298             if (ref($input) eq 'HASH') {
299             my @values;
300             while (my ($key, $vals) = each %$input) {
301             push @values, map { $key => $_ } (ref($vals)?@$vals:$vals);
302             }
303             $input = \@values;
304             }
305              
306             if (ref($input) eq 'ARRAY') {
307             my %pos;
308             while (@$input) {
309             my $key = shift @$input;
310             my $val;
311             if (ref($key)) { ($key, $val) = @$key; }
312             else { $val = shift @$input }
313             ($key) = $key =~ m/([a-zA-Z0-9:_-]*)/; # FIXME: QName.
314             my $pos = ++$pos{$key};
315             my ($node) = $xpc->findnodes(".//*[local-name() = '${key}' and not(*)][$pos]", $node);
316             $node->removeChildNodes();
317             $node->appendTextNode($val);
318             }
319             } else {
320             my $parent = $node->parentNode();
321             if (!$parent) {
322             $parent = $self->{parser}->parse_string($self->getDom($input)->documentElement()->toStringC14N());
323             foreach my $key (keys %$dom) {
324             $$dom{$key} = $parent->documentElement if ${$$dom{$key}->ownerDocument} eq ${$node->ownerDocument};
325             }
326             $node = $parent;
327             } else {
328             my $new = $self->{parser}->parse_balanced_chunk($self->getDom($input)->documentElement()->toStringC14N())->firstChild;
329             $parent->replaceChild($new, $node);
330             $node = $new;
331             }
332             }
333              
334             return ($dom, $node);
335             }
336              
337             sub makeNodeName {
338             my ($node) = @_;
339             my $name = $node->nodeName;
340             $name = '' if !defined $name;
341             return '' if $name =~ m/^xmlns/;
342             $name =~ s/^[^:]*://;
343             my $ns = $node->namespaceURI();
344             $ns = (defined $ns?"{$ns}":"");
345             return $ns.$name;
346             }
347              
348             sub checkTreeRecursive {
349             my XML::XForms::Validate $self = shift;
350             my ($new, $orig, $added) = @_;
351              
352             my $path = $new->nodePath;
353             my $ndoc = $new->ownerDocument;
354              
355             my $nname = makeNodeName($new);
356             my $oname = makeNodeName($orig);
357             return "Original node \"$oname\" doesn't match \"$nname\" ($path)"
358             if $nname ne $oname;
359              
360             my @added;
361              
362             my %nattr = map { (makeNodeName($_) => $_) } $new->attributes;
363             my %oattr = map { (makeNodeName($_) => $_) } $orig->attributes;
364             delete $nattr{''};
365             delete $oattr{''};
366             foreach my $attr (keys %oattr) {
367             delete $nattr{$attr}, next if exists $nattr{$attr};
368             $new->setAttributeNS($oattr{$attr}->namespaceURI, $oattr{$attr}->nodeName, $oattr{$attr}->nodeValue);
369             my $clone = $new->getAttributeNodeNS($oattr{$attr}->namespaceURI, $oattr{$attr}->nodeName);
370             push @added, $clone;
371             }
372              
373             return "Additional attributes found: ".join(", ", keys %nattr)." ($path)"
374             if %nattr;
375              
376             my @nelem = grep { $_->isa('XML::LibXML::Element') } $new->childNodes;
377             my @oelem = grep { $_->isa('XML::LibXML::Element') } $orig->childNodes;
378             my $firstmsg;
379             my $lastmsg;
380             while (@oelem) {
381             last if !@nelem;
382             my $nnext = $nelem[0];
383             my $onext = shift @oelem;
384             my $msg = $self->checkTreeRecursive($nnext, $onext, \@added);
385             $firstmsg = undef, shift @nelem, next if !$msg;
386             my $clone = $self->{parser}->parse_balanced_chunk($onext->toString())->firstChild;
387             $new->insertBefore($clone, $nelem[0]);
388             $new->insertAfter($new->ownerDocument->createTextNode(''), $clone) if $onext->nextSibling->nodeType == XML_TEXT_NODE;
389             push @added, $clone;
390             $firstmsg ||= $msg;
391             }
392              
393             push(@$added, @added), return undef if !@nelem && !@oelem;
394              
395             $_->parentNode->removeChild($_) foreach @added;
396             $firstmsg ||= "Additional child elements found: ".join(", ", map { $_->nodeName } @nelem)." ($path)" if @nelem;
397             $firstmsg ||= "Child elements missing: ".join(", ", map { $_->nodeName } @oelem)." ($path)" if @oelem;
398              
399             return $firstmsg;
400             }
401              
402             sub hasParents {
403             my ($child, $parents) = @_;
404             my $test = $child;
405             do {
406             return 1 if exists $$parents{${$test}};
407             } while ($test = $test->parentNode);
408             return 0;
409             }
410              
411             sub checkTree {
412             my XML::XForms::Validate $self = shift;
413             my ($new, $orig, $ixpc, $oxpc, $subtree) = @_;
414              
415             my $added = [];
416             foreach my $key (keys %$orig) {
417             my $result = $self->checkTreeRecursive($$new{$key}, $$orig{$key}, $added);
418             return $result if $result;
419             }
420              
421             $added = { map { ${$_} => $_ } @$added };
422             my %leftover = %$added;
423             foreach my $bind (values %{$self->{binds}}) {
424             next if !exists $$bind{relevant};
425             foreach my $node ($self->findNodes($$bind{nodeset}, $$new{''}, $ixpc)) {
426             next if $self->findBoolean($$bind{relevant}, $node, $ixpc);
427             my $deleted = delete $leftover{${$node}};
428             next if hasParents($node, $added) || !hasParents($node, $subtree);
429             return "Submission contains non-relevant node: ".$node->nodePath if !$deleted;
430             }
431             }
432              
433             foreach my $rest (values %leftover) {
434             delete $$added{${$rest}};
435             delete $leftover{${$rest}} if hasParents($rest, $added);
436             $$added{${$rest}} = $rest;
437             }
438             return "Missing relevant nodes: ".join(", ", map { $_->nodePath } values %leftover) if %leftover;
439              
440             return undef;
441             }
442              
443             sub checkReadonlyRecursive {
444             my XML::XForms::Validate $self = shift;
445             my ($new, $orig, $nrw, $orw) = @_;
446              
447             my %nattr = map { (makeNodeName($_) => $_) } $new->attributes;
448             my %oattr = map { (makeNodeName($_) => $_) } $orig->attributes;
449             delete $nattr{''};
450             delete $oattr{''};
451             foreach my $attr (keys %oattr) {
452             next if exists $$nrw{${$nattr{$attr}}} || exists $$orw{${$oattr{$attr}}};
453             $nattr{$attr}->setValue($oattr{$attr}->getValue());
454             }
455              
456             my @nelem = grep { $_->isa('XML::LibXML::Element') } $new->childNodes;
457             my @oelem = grep { $_->isa('XML::LibXML::Element') } $orig->childNodes;
458             while (@oelem) {
459             my $nnext = shift @nelem;
460             my $onext = shift @oelem;
461             if (!exists $$nrw{${$nnext}} && !exists $$orw{${$onext}}) {
462             my ($ntext) = $nnext->findnodes('./text()[1]');
463             my ($otext) = $onext->findnodes('./text()[1]');
464             $otext = ($otext?$otext->nodeValue:'');
465             if (!$ntext) {
466             $ntext = $nnext->appendText($otext);
467             } else {
468             $ntext->setData($otext);
469             }
470             }
471             my $result = $self->checkReadonlyRecursive($nnext, $onext, $nrw, $orw);
472             return $result if $result;
473             }
474              
475             @nelem = grep { $_->isa('XML::LibXML::Text') } $new->childNodes;
476             @oelem = grep { $_->isa('XML::LibXML::Text') } $orig->childNodes;
477             while (@oelem) {
478             my $nnext = shift @nelem;
479             my $onext = shift @oelem;
480             if (!$nnext) {
481             my $val = $onext->nodeValue;
482             $val =~ s/^\s*|\s*$//g;
483             return "Text node missing for ".$onext->nodePath if length($val) && !exists $$orw{${onext}};
484             $new->appendText('') if exists $$orw{${onext}};
485             next;
486             }
487             next if exists $$nrw{${$nnext}} || exists $$orw{${$onext}};
488             $nnext->setData($onext->nodeValue);
489             }
490             foreach my $node (@nelem) {
491             $new->removeChild($node) unless exists $$nrw{${$node}};
492             }
493              
494             return undef;
495             }
496              
497             sub setRW {
498             my ($rw, $n) = @_;
499             if ($n) {
500             $$rw{${$n}} = $n;
501             ($n) = $n->findnodes('./text()[1]');
502             $$rw{${$n}} = $n if $n;
503             }
504             }
505              
506             sub checkReadonly {
507             my XML::XForms::Validate $self = shift;
508             my ($new, $orig, $ixpc, $oxpc, $subtree) = @_;
509              
510             my %nrw;
511             my %orw;
512             foreach my $expr (@{$self->{refs}}) {
513             setRW(\%nrw, $self->findNodes($expr, $$new{''}, $ixpc));
514             setRW(\%orw, $self->findNodes($expr, $$orig{''}, $oxpc));
515             }
516              
517             foreach my $bind (values %{$self->{binds}}) {
518             my $ro = $$bind{readonly};
519             next if !defined $ro;
520             foreach my $node ($self->findNodes($$bind{nodeset}, $$new{''}, $ixpc)) {
521             next if defined $ro && !$self->findBoolean($ro, $node, $ixpc);
522             delete $nrw{${$_}} foreach $node->findnodes('.|.//*|.//@*|.//text()');
523             }
524             foreach my $node ($self->findNodes($$bind{nodeset}, $$orig{''}, $oxpc)) {
525             next if defined $ro && !$self->findBoolean($ro, $node, $oxpc);
526             delete $orw{${$_}} foreach $node->findnodes('.|.//*|.//@*|.//text()');
527             }
528             }
529              
530             foreach my $bind (values %{$self->{binds}}) {
531             if (defined $$bind{calculate}) {
532             setRW(\%nrw, $_) foreach ($self->findNodes($$bind{nodeset}, $$new{''}, $ixpc));
533             setRW(\%orw, $_) foreach ($self->findNodes($$bind{nodeset}, $$orig{''}, $oxpc));
534             }
535             }
536              
537             foreach my $key (keys %$orig) {
538             my $result = $self->checkReadonlyRecursive($$new{$key}, $$orig{$key}, \%nrw, \%orw);
539             return $result if $result;
540             }
541             }
542              
543             sub checkBinds {
544             my XML::XForms::Validate $self = shift;
545             my ($new, $orig, $ixpc, $oxpc, $subtree) = @_;
546              
547             foreach my $bind (values %{$self->{binds}}) {
548             my ($prefix, $type) = ($$bind{type}||'') =~ m/^(?:([^:]*):)?(.*)$/;
549             $prefix ||= '';
550             my $calc = $$bind{calculate};
551             my $constraint = $$bind{constraint};
552             my $required = $$bind{required};
553             my $relevant = $$bind{relevant};
554             next unless defined $type || defined $calc || defined $constraint || defined $required;
555              
556             foreach my $node ($self->findNodes($$bind{nodeset}, $$new{''}, $ixpc)) {
557             next if !hasParents($node, $subtree) || (defined $relevant && !$self->findBoolean($relevant, $node, $ixpc));
558              
559             my $val = ($node->isa('XML::LibXML::Element')?$node->findvalue('./text()[1]'):$node->nodeValue);
560             my $path = $node->nodePath;
561              
562             return "Value required for $path" if defined $required
563             && $self->findBoolean($required, $node, $ixpc) && !length($val);
564             return "Constraint error for $path ($constraint)" if defined $constraint
565             && !$self->findBoolean($constraint, $node, $ixpc);
566             return "Calculation mismatch for $path ($calc): expected \"".$ixpc->findvalue($calc, $node)."\", found \"$val\"" if defined $calc
567             && $ixpc->findvalue($calc, $node) ne $val;
568              
569             next unless $type;
570             # FIXME: really needs switch to libxm
571             my $nsuri = $$bind{node}->lookupNamespaceURI($prefix) || '';
572             my $class;
573             $class = XML::Schema::Type::Simple->builtin($type) if $nsuri eq NSURI_XSD || $nsuri eq NSURI_XS;
574             $class = "XML::XForms::Validate::Type::$type" if $nsuri eq NSURI_XF;
575             my $const = UNIVERSAL::can($class, 'new');
576             return "Type $prefix:$type unsupported for $path" if !defined $const;
577             my $obj = $const->($class);
578             return "Could not create object for $path ($type)" if !$obj;
579             return "Type mismatch for $path ($type)"
580             if !$obj->instance($val);
581             }
582             }
583              
584             return undef;
585             }
586              
587             sub checkSchema {
588             my XML::XForms::Validate $self = shift;
589             my ($new) = @_;
590              
591             foreach my $id (keys %{$self->{schemas}}) {
592             eval { $self->{schemas}{$id}->validate($$new{$id}->ownerDocument) };
593             return "Schema validation failed for instance \"$id\": $@" if $@;
594             }
595             return undef;
596             }
597              
598             sub validate {
599             my XML::XForms::Validate $self;
600             $self = shift if UNIVERSAL::isa($_[0], 'XML::XForms::Validate');
601             my %options = @_;
602             if (!$self) {
603             $self = new XML::XForms::Validate(xforms => $options{xforms}, model => $options{model}, base => $options{base});
604             }
605              
606             my $model = $self->{model};
607              
608             my $orig = { %{$self->{instances}} };
609              
610             if ($options{instance}) {
611             $options{instance} = { '' => $options{instance} } if ref($options{instance}) ne 'HASH';
612             foreach my $key (keys %{$options{instance}}) {
613             my $replaced = $$orig{$key};
614             $$orig{$key} = $self->getDom($options{instance}{$key});
615             $$orig{''} = $$orig{$key} if $$orig{''} eq $replaced;
616             }
617             }
618              
619             $options{submission} = '' unless defined $options{submission};
620             my $subref = $self->{submissions}{$options{submission}}{'ref'};
621              
622             my ($new, $subnode) = $self->getInput($options{input}, $orig, $subref);
623              
624             my $oxpc = makeXPathContext(sub {
625             my ($id) = @_;
626             my $result = XML::LibXML::NodeList->new;
627             $result->push($$orig{$id});
628             return $result;
629             });
630             my $ixpc = makeXPathContext(sub {
631             my ($id) = @_;
632             my $result = XML::LibXML::NodeList->new;
633             $result->push($$new{$id});
634             return $result;
635             });
636              
637             my ($newsub) = $self->findNodes($subref, $$new{''}, $ixpc);
638             return "Submission does not match subtree reference (@$subref)"
639             if !$newsub || ${$newsub} ne ${$subnode};
640              
641             if ($options{schema}) {
642             eval { XML::LibXML::Schema->new(location => $options{schema})->validate($subnode) };
643             return "Schema validation failed: $@" if $@;
644             return $new;
645             }
646              
647             my $subtree = { ${$subnode} => $subnode };
648             my $result = $self->checkTree($new, $orig, $ixpc, $oxpc, $subtree);
649             return $result if $result;
650             ($newsub) = $self->findNodes($subref, $$new{''}, $ixpc);
651             return "Submission does not match subtree reference (@$subref) after relevancy processing"
652             if !$newsub || ${$newsub} ne ${$subnode};
653              
654             $result = $self->checkReadonly($new, $orig, $ixpc, $oxpc, $subtree);
655             return $result if $result;
656              
657             $result = $self->checkBinds($new, $orig, $ixpc, $oxpc, $subtree);
658             return $result if $result;
659              
660             $result = $self->checkSchema($new);
661             return $result if $result;
662              
663             return $new;
664             }
665              
666             # XPath Extensions
667             # These use an internal XPathContext for calculations to make sure XPath
668             # semantics are obeyed, especially regarding type conversion.
669              
670             sub makeXPathContext {
671             my ($instancefunc) = @_;
672             my $xpc = new XML::LibXML::XPathContext;
673             $xpc->registerNs('xf', NSURI_XF); # FIXME: must be done on a per-evaluation basis instead
674             $xpc->registerFunction('instance', $instancefunc);
675             $xpc->registerFunction('boolean-from-string', \&XPath_booleanFromString);
676             $xpc->registerFunction('if', \&XPath_if);
677             $xpc->registerFunction('avg', \&XPath_avg);
678             $xpc->registerFunction('min', \&XPath_min);
679             $xpc->registerFunction('max', \&XPath_max);
680             $xpc->registerFunction('count-non-empty', \&XPath_countNonEmpty);
681             $xpc->registerFunction('index', \&XPath_index);
682             $xpc->registerFunction('property', \&XPath_property);
683             $xpc->registerFunction('now', \&XPath_now);
684             $xpc->registerFunction('days-from-date', \&XPath_daysFromDate);
685             $xpc->registerFunction('seconds-from-dateTime', \&XPath_secondsFromDateTime);
686             $xpc->registerFunction('seconds', \&XPath_seconds);
687             $xpc->registerFunction('months', \&XPath_months);
688             return $xpc;
689             }
690              
691             sub XPath_booleanFromString {
692             my ($str) = @_;
693             my $xpc = new XML::LibXML::XPathContext;
694             $xpc->registerVarLookupFunc(sub { return $str }, undef);
695             $str = $xpc->findvalue('string($str)', $doc);
696             return XML::LibXML::Boolean->True if (lc($str) eq 'true' || $str eq '1');
697             return XML::LibXML::Boolean->False if (lc($str) eq 'false' || $str eq '0');
698             die "Invalid boolean string value: $str";
699             }
700              
701             sub XPath_if {
702             my ($bool, $true, $false) = @_;
703             my $xpc = new XML::LibXML::XPathContext;
704             $xpc->registerVarLookupFunc(sub { return $bool }, undef);
705             $bool = $xpc->findvalue('boolean($bool)', $doc);
706             return ($bool eq 'true'?$true:$false);
707             }
708              
709             sub XPath_avg {
710             my ($nodeset) = @_;
711              
712             my $xpc = new XML::LibXML::XPathContext;
713             $xpc->registerVarLookupFunc(sub { return $nodeset }, undef);
714             return $xpc->find('sum($x) div count($x)', $doc);
715             }
716              
717             sub XPath_min {
718             my ($nodeset) = @_;
719             return new XML::LibXML::Number('NaN') if (!$nodeset->size());
720             my $min = $nodeset->shift;
721             my $cur = $min;
722              
723             my $xpc = new XML::LibXML::XPathContext;
724             $xpc->registerVarLookupFunc(sub { return $min }, undef);
725             while ($nodeset->size()) {
726             $cur = $nodeset->shift;
727             $min = $cur if $xpc->findvalue('. < $min', $cur) eq 'true';
728             }
729              
730             return $min;
731             }
732              
733             sub XPath_max {
734             my ($nodeset) = @_;
735             return new XML::LibXML::Number('NaN') if (!$nodeset->size());
736             my $max = $nodeset->shift;
737             my $cur = $max;
738              
739             my $xpc = new XML::LibXML::XPathContext;
740             $xpc->registerVarLookupFunc(sub { return $max }, undef);
741             while ($nodeset->size()) {
742             $cur = $nodeset->shift;
743             $max = $cur if $xpc->findvalue('. > $max', $cur) eq 'true';
744             }
745              
746             return $max;
747             }
748              
749             sub XPath_countNonEmpty {
750             my ($nodeset) = @_;
751             my $cur;
752             my $result = 0;
753              
754             my $xpc = new XML::LibXML::XPathContext;
755             while ($nodeset->size()) {
756             $cur = $nodeset->shift;
757             $result++ if $xpc->findvalue('string-length(.)', $cur) > 0;
758             }
759              
760             return new XML::LibXML::Number($result);
761             }
762              
763             sub XPath_index {
764             # Doesn't apply to validation.
765             die "index() not supported.";
766             }
767              
768             sub XPath_property {
769             my ($str) = @_;
770             my $xpc = new XML::LibXML::XPathContext;
771             $xpc->registerVarLookupFunc(sub { return $str }, undef);
772             $str = $xpc->findvalue('string($str)', $doc);
773             return new XML::LibXML::Literal('1.0') if ($str eq 'version');
774             return new XML::LibXML::Literal('full') if ($str eq 'conformance-level');
775             return new XML::LibXML::Literal('');
776             }
777              
778             sub XPath_now {
779             my $now = localtime;
780             my $off = $now->tzoffset;
781             my $time = $now->datetime().sprintf('%+03d:%02d', int($off->hours), abs($off->minutes)%60);
782             return new XML::LibXML::Literal($time);
783             }
784              
785             sub XPath_daysFromDate {
786             my ($day) = @_;
787             my $xpc = new XML::LibXML::XPathContext;
788             $xpc->registerVarLookupFunc(sub { return $day }, undef);
789             $day = $xpc->findvalue('string($day)', $doc);
790             $day =~ s/T.*//;
791             my $time = Time::Piece->strptime($day, "%Y-%m-%d")->epoch / ONE_DAY;
792             return new XML::LibXML::Number($time);
793             }
794              
795             sub XPath_secondsFromDateTime {
796             my ($date) = @_;
797             my $xpc = new XML::LibXML::XPathContext;
798             $xpc->registerVarLookupFunc(sub { return $date }, undef);
799             $date = $xpc->findvalue('string($date)', $doc);
800             my ($day, $sign, $h, $m) = $date =~ m/^(.*?)(?:\.[0-9]*)?(?:Z|([+-])([0-9]{2}):([0-9]{2}))?$/;
801             my $time = Time::Piece->strptime($day, "%Y-%m-%dT%H:%M:%S")->epoch;
802             $sign ||= '';
803             if ($sign eq '+') {
804             $time -= $h*60+$m;
805             } elsif ($sign eq '-') {
806             $time += $h*60+$m;
807             }
808             return new XML::LibXML::Number($time);
809             }
810              
811             sub XPath_seconds {
812             my ($duration) = @_;
813             my $xpc = new XML::LibXML::XPathContext;
814             $xpc->registerVarLookupFunc(sub { return $duration }, undef);
815             $duration = $xpc->findvalue('string($duration)', $doc);
816             my ($sign, $d, $h, $m, $s) = $duration =~ m/^([+-]?)P.*?(?:([0-9]+)D)?(?:T(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9.]+)S)?)?$/;
817             $sign .= 1;
818             return new XML::LibXML::Number(((($d*24+$h)*60+$m)*60+$s)*$sign);
819             }
820              
821             sub XPath_months {
822             my ($duration) = @_;
823             my $xpc = new XML::LibXML::XPathContext;
824             $xpc->registerVarLookupFunc(sub { return $duration }, undef);
825             $duration = $xpc->findvalue('string($duration)', $doc);
826             my ($sign, $y, $m) = $duration =~ m/^([+-]?)P(?:([0-9]+)Y)?(?:([0-9]+)M)?.*$/;
827             $sign .= 1;
828             return new XML::LibXML::Number(($y*12+$m)*$sign);
829             }
830              
831             package XML::Schema::Type::duration;
832             use base qw( XML::Schema::Type::timeDuration );
833             # FIXME: hack for XML::Schema bug
834              
835             package XML::Schema::Type::dateTime;
836             use base qw( XML::Schema::Type::recurringDuration );
837             use vars qw( $ERROR @FACETS );
838             @FACETS = (
839             period => { value => 'P10000Y', fixed => 1 },
840             duration => { value => 'P0Y', fixed => 1 },
841             );
842             # FIXME: hack for XML::Schema bug
843              
844             package XML::XForms::Validate::Type::listItem;
845             use base qw( XML::Schema::Type::string );
846             use vars qw( $ERROR @FACETS );
847              
848             @FACETS = (
849             pattern => {
850             value => '^\S+$',
851             errmsg => 'value is not a valid listItem',
852             }
853             );
854              
855             package XML::XForms::Validate::Type::listItems;
856             use base qw( XML::Schema::Type::string );
857             use vars qw( $ERROR @FACETS );
858             # FIXME: this is technically wrong, but sufficient for our purposes.
859              
860             @FACETS = (
861             pattern => {
862             value => '^((\S+\s+)*\S+)?$',
863             errmsg => 'value is not a valid listItem list',
864             }
865             );
866              
867             package XML::XForms::Validate::Type::dayTimeDuration;
868             use base qw( XML::Schema::Type::duration );
869             use vars qw( $ERROR @FACETS );
870             # FIXME: no idea if this works as intended.
871              
872             @FACETS = (
873             duration => { value => 'P0Y0M', fixed => 1 },
874             );
875              
876             package XML::XForms::Validate::Type::yearMonthDuration;
877             use base qw( XML::Schema::Type::duration );
878             use vars qw( $ERROR @FACETS );
879             # FIXME: no idea if this works as intended.
880              
881             @FACETS = (
882             duration => { value => 'P0DT0H0M0S', fixed => 1 },
883             );
884              
885              
886             1;
887             __END__