# # Copyright (c) 2004-2007 - Consultas, PKG.fr # # This file is part of A2P. # # A2P is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # A2P is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with A2P; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # # $Id: XML.pm 3 2007-10-18 16:20:19Z guillaume $ # # Class to implement some XML support # package A2P::XML; use strict; use Fcntl ':flock'; use File::stat ; use XML::LibXML ; use Time::HiRes qw( time ); use MIME::Base64 ; use A2P::Globals ; use A2P::Syslog ; use A2P::Tools qw( ShortID ); BEGIN { our $VERSION = sprintf "%s", q$Rev: 415 $ =~ /(\d[0-9.]+)\s+/ ; } our $VERSION ; # Private variables my %DTD = {} ; my %IDs = {} ; sub id { my $time = time ; # time is here the Time::HiRes 'time' version my @Date = localtime($time); my $DayTick = ($time - int($time - ( $Date[0] + $Date[1]*60 + $Date[2]*3600 ))) * 193 ; my $id ; while ( ! defined($id) or defined($IDs{$id}) ) { $id = sprintf( "XML-%c%03D-%06X-%s", $Date[5]-40 , $Date[7] , $DayTick, &ShortID ) ; } $IDs{$id} = 1 ; return $id ; } sub new { &Debug("new " . __PACKAGE__ . " v$VERSION"); my $class = shift ; my $self = { XMLREF => @_ ? shift : undef , ID => &id(), ERROR => [], NODES => {}, LASTPARSEDFILE => "", XML => "", NODEID => "", DTD => "", DTDNAME => "" }; &Debug("New XML object is identified with $self->{ID} tag"); return bless $self , $class ; } ################################################################################ # # Common APIs # ################################################################################ sub getParser { my $self = shift ; return $self->{PARSER} if (defined($self->{PARSER})); # Read Destination configuration file to set needed options &Debug("Creating new XML parser for $self->{ID}"); my $parser = XML::LibXML->new(); return $self->error(401,"Unable to get an XML parser: $!") unless (defined($parser)); return $self->{PARSER} = $parser ; } sub clearParsedNodes { my $self = shift ; undef $self->{TREE} ; $self->{NODES} = {} ; $self->{NODEID} = "" ; } sub parse_file { my $self = shift ; my $file = shift ; my $parser ; # Check file is existing return $self->error(402, defined($file) ? "'$file' XML file doesn't exist" : "No file to parse provided") unless ( defined($file) and $file and -e $file ); # Get file time creation to check if we need to reload my $stat = stat($file); my $mtime = $stat->mtime(); # Check if we still have parsed this file and it has not changed if ( $self->{LASTPARSEDFILE} eq $file ) { # The mandatory point to parse again the file is to found a # different mtime on file, this should garanty to reload the # file at last in the second if ( $mtime == $self->{MTIMEPARSEDFILE} ) { &Debug("File to parse still known, keeping cached parsing"); return $file ; } else { &Debug("XML file has been modified"); # When we need to reload conf, we should free some structures $self->clearParsedNodes(); } } # Check with can parse file return $self->error(403, "No parser available to read XML file") unless ( $parser = $self->getParser() ); &Debug("Parsing '$file' XML file"); $self->{XML} = $parser->parse_file( $file ); if (defined($self->{XML})) { $self->{MTIMEPARSEDFILE} = $mtime ; if ($ADVANCED_DEBUGGING) { my $confdump = $self->{XML}->toString(2) ; &Debug( "XML conf: '" . ( length($confdump) > 160 ? substr($confdump,0,160) . "..." : $confdump )."'" ); } } else { $self->error(404,"Got XML Parsing error with '$file': $!"); $file = "" ; } if ($ENABLE_DTD_VALIDATION and $file and $self->{DTD}) { my $valid = eval { $self->{XML}->is_valid($self->{DTD}) } ; if ( ! $valid ) { my $error = $parser->get_last_error() ; $error = defined($error) ? ", $error" : "" ; chomp $error ; $self->error(405,"'$file' XML document is not valid toward '$self->{DTDNAME}'$error"); $file = "" ; } else { &Debug("'$file' is conform toward '$self->{DTDNAME}' DTD"); } } return $self->{LASTPARSEDFILE} = $file ; } sub parseNodesById { my $self = shift ; # Really parse only if it has not been done before unless ( keys(%{$self->{NODES}}) ) { &Debug("Parsing nodes tree by Id"); foreach my $node ( @{$self->{TREE}} ) { my $nodeid = $node->getAttribute('Id'); $self->{NODES}->{$nodeid} = $node ; } } } sub get { my $self = shift ; my $what = shift ; my $key = '__' . $what ; return $self->error(406,"Nothing to seach provided") unless (defined($what) and $what ); my $nodeid = $self->{NODEID} ; return $self->error(407,"Can't seach as no node as been selected") unless ($nodeid); return defined($self->{$key}->{$nodeid}) ? $self->{$key}->{$nodeid} : $self->{$key}->{$nodeid} = $self->{NODES}->{$nodeid}->findvalue($what) ; } sub getAttribute { my $self = shift ; my $what = shift ; my $key = '__' . $what ; return $self->error(411,"Nothing to seach provided") unless (defined($what) and $what ); my $nodeid = $self->{NODEID} ; return $self->error(412,"Can't seach as no node as been selected") unless ($nodeid); return defined($self->{$key}->{$nodeid}) ? $self->{$key}->{$nodeid} : $self->{$key}->{$nodeid} = $self->{NODES}->{$nodeid}->getAttribute($what) ; } sub concatenate { my $self = shift ; return &Error("[DEV] Can't concatenate without scalar ref as XMLREF") unless (ref($self->{XMLREF}) =~ /^SCALAR/); my $line = shift || "" ; return ${$self->{XMLREF}} .= $line ; } sub parse_string { my $self = shift ; my $parser ; # Check with can parse a string return $self->error(408, "No parser available to analyse string") unless ( $parser = $self->getParser() ); return $self->error(409, "No string to analyse") unless ( ref($self->{XMLREF}) =~ /^SCALAR$/ ); $self->{XML} = $parser->parse_string( ${$self->{XMLREF}} ); unless (defined($self->{XML})) { $self->error(410,"Got XML Parsing error on '${$self->{XMLREF}}': $!"); $self->{XML} = 0 ; } if ($ENABLE_DTD_VALIDATION and $self->{XML} and $self->{DTD} ) { my $valid = eval { $self->{XML}->is_valid($self->{DTD}) } ; if ( ! $valid ) { my $error = $parser->get_last_error() ; $error = defined($error) ? ", $error" : "" ; my $xml = ${$self->{XMLREF}} ; chomp $error ; chomp $xml ; $self->error(411,"'$xml' XML string is not valid toward '$self->{DTDNAME}'$error"); $self->{XML} = 0 ; } else { &Debug("XML string is conform toward '$self->{DTDNAME}' DTD"); } } return $self->{XML} ; } sub getid { return $_[0]->{ID} ; } sub toString { my $self = shift ; my $format = shift || 0; my $doc = $self->{XML} ; unless (defined($doc) and ref($doc) =~ /^XML/) { $self->error(412,"No XML to return"); return "" ; } # Return the XML as original format string by default my $string = $doc->toString($format) ; &Debug( "Returning XML string: '" . ( length($string) > 160 ? substr($string,0,160) . "..." : $string ) . "'" ) if ($ADVANCED_DEBUGGING); return $string; } sub setDTD { my $self = shift ; my $name = shift || "" ; return &Error("No DTD name provided") unless $name ; return &Error("No such '$name' DTD available") unless (defined($DTD{$name})); &Debug("Selecting '$name' DTD"); $self->{DTD} = $DTD{$self->{DTDNAME} = $name} ; } ################################################################################ # # ControlRecord dedicated API # ################################################################################ # Provides the selection of the right NODE in the XML # Then we can use 'get' API to retreive destid values sub selectDestId { my $self = shift ; my $destid = shift ; return $self->error(450,"No destid provided") unless (defined($destid) and $destid ); &Debug("Getting Destinations node in XML"); my $TREE = $self->getDestinations(); return $self->error(451,"No destination tree found".($!?" $!":"")) unless @{$TREE} ; $self->parseNodesById(); unless (defined($self->{NODES}->{$destid})) { $self->error(452,"No configuration found for DestID '$destid'"); return "" ; } return $self->{NODEID} = $destid ; } sub selectDocument { my $self = shift ; my $doc = shift ; return $self->error(453,"No document name provided") unless (defined($doc) and $doc ); &Debug("Getting Documents node in XML"); my $TREE = $self->getDocuments(); return $self->error(454,"No Document tree found".($!?" $!":"")) unless @{$TREE} ; $self->parseNodesById(); unless (defined($self->{NODES}->{$doc})) { &Debug("No configuration found for Document '$doc'"); return "" ; } return $self->{NODEID} = $doc ; } sub getDestinations { my $self = shift ; # Return cached TREE return $self->{TREE} if (defined($self->{TREE})); &Debug("Searching Destination nodes in XML"); my @TREE = $self->{XML}->getElementsByTagName("Destination"); return $self->{TREE} = \@TREE ; } sub getDocuments { my $self = shift ; # Return cached TREE return $self->{TREE} if (defined($self->{TREE})); &Debug("Searching Document nodes in XML"); my @TREE = $self->{XML}->getElementsByTagName("Document"); return $self->{TREE} = \@TREE ; } ################################################################################ # # E-Service dedicated API # ################################################################################ sub getbin { my $self = shift ; my $name = shift ; my $exec ; $self->error(0); $! = 0 ; return $self->error(480,"No service name provided") unless (defined($name) and $name ); &Debug("Getting E-Services node in XML"); my $TREE = $self->getEServices(); return $self->error(481,"No E-Service tree found".($!?" ($!)":"")) unless @{$TREE} ; # Really parse only if it has not been done before unless ( keys(%{$self->{NODES}}) ) { &Debug("Parsing nodes tree by name"); foreach my $node ( @{$self->{TREE}} ) { my $service = $node->getAttribute('name'); unless (defined($name) and $name) { &Warn("Skipping bad service node without name attribut '" . $node->toString() . "'"); next ; } $exec = $node->getAttribute('exec'); unless (defined($exec) and $exec) { &Warn("Skipping bad service node without exec attribut '" . $node->toString() . "'"); next ; } &Warn("'$service' E-Service definition seems to use and non existing '" . $exec . "' program") unless (-e $exec); &Warn("'$service' E-Service definition seems to use and non executable '" . $exec . "' program") unless (-x $exec); my $mode = $node->getAttribute('mode'); unless (defined($exec) and $exec) { &Debug("No service node mode attribut found in '" . $node->toString() . "', taking default"); $mode = 0 ; } &Debug("Registring node '$service' with exec '$exec' and mode '$mode'") if ($ADVANCED_DEBUGGING); $self->{NODES}->{$service} = [ $exec , $mode ] ; } } unless (defined($self->{NODES}->{$name})) { $self->error(482,"No configuration found for E-Service '$name'"); return "" ; } $exec = $self->{NODES}->{$name}->[0] || "" ; return $self->error(483,"Requested E-Service '$name' uses not existing '$exec' program") unless ( -e $exec ); # Check if its a link if not executable my $maxlink = 10 ; # Even if link loop should not pass the 483 error, its better # to keep as reasonnable loop exit condition my $link = $exec ; while ( ! -x _ and $maxlink -- ) { if ( -l $link ) { $link = readlink $link ; return $self->error(483,"Requested E-Service '$name' uses a '$exec' link to a not existing '$link' program") unless ( -e $link ); } else { last ; } } return $self->error(484,"Too much link found behind '$exec'") if ( $maxlink < 0 ); return $self->error(485,"Requested E-Service '$name' uses not executable '$link' program" . ($link ne $exec?" as '$exec' link":"")) unless ( -x _ ); return @{$self->{NODES}->{$name}} ; } sub getEServices { my $self = shift ; # Return cached TREE return $self->{TREE} if (defined($self->{TREE})); &Debug("Searching any service node in XML") if ($ADVANCED_DEBUGGING); my @TREE = $self->{XML}->getElementsByTagName("service"); &Debug("Tree nodes: @TREE") if ($ADVANCED_DEBUGGING); return $self->{TREE} = \@TREE ; } ################################################################################ # # External requests dedicated API # ################################################################################ # Provides access to the NODE xml validation sub isA2P_valid { my $self = shift ; my $strict = shift || 0 ; # 1. Parse buffer as string my $doc = $self->parse_string ; return $strict ? $self->error(460,"Not an XML document") : 0 unless ( ref($doc) =~ /^XML::LibXML::Document$/ ); # 2. Check doc is valid # TODO We need to validate toward a DTD #my $dtd = $self->getDTD('a2p'); #return $strict ? $self->error(461,"Not a valid XML A2P object") : 0 # unless ($doc->is_valid($dtd)); return $doc ; } # Provides access to the NODE to handle conversion request sub isA2P { my $self = shift ; # 1. Parse and validate buffer $self->setDTD('a2p-listener.dtd'); my $doc = $self->isA2P_valid('strict') ; return 0 unless ( $doc and ref($doc) =~ /^XML::LibXML::Document$/ ); # 2. Check document format: we must have an a2p node as root return $self->error(462,"Document has no 'a2p' node") unless ( $doc->hasChildNodes and $doc->getChildNodes->size == 1 and $doc->lastChild->nodeName eq 'a2p'); my $a2p = $doc->lastChild ; my @nodes = map { $_->nodeName } $a2p->getChildNodes ; &Debug("Available nodes: @nodes"); # 3. We need at least a path or a content node return 0 unless grep { /^(path|content)$/i } @nodes ; # 4. Still select A2P comportement my $key = 'CONTENT' ; $key = 'PATH' if (grep { /^path$/i } @nodes); # Initializes few things $self->{MESSAGE} = [ "A2P-XML-Object = ".$self->{ID} ] ; $self->{PDFS} = [] ; $self->{STATE} = 0 ; foreach my $node ( $a2p->getChildNodes ) { next unless ( $node->nodeName =~ /^$key$/i ); # Check eventually type attribut my $type = $node->getAttribute("type"); return $self->error(464,"Type '$type' conversion not supported") if (defined($type) and $type =~ /^afp$/i ); $self->{$key} = $node->textContent() ; $self->{$key} = "" unless (defined($self->{$key})); } return defined($self->{PATH}) and $self->{PATH} ? 1 : 0 if ( $key eq 'PATH' ); # key = CONTENT as AFP EBCDIDC base64 encoded flux &Debug("Got base64 content to convert"); $self->{CONTENT} = &decode_base64( $self->{CONTENT} ) ; $self->{PATH} = $SHMDIR . '/AFP.' . $self->{ID} ; open AFP, ">" .$self->{PATH} or return $self->error(463,"Can't open temporary file to save AFP content: $!"); # Lock the file just in case my $locked = flock(AFP, LOCK_EX | LOCK_NB); return $self->error(465,"Can't lock file for AFP content: $!") unless $locked ; my $print = print AFP $self->{CONTENT} or $self->error(466,"Can't write file with AFP content: $!"); flock(AFP, LOCK_UN); close(AFP); return $print ; } # Provides access to the NODE as E-Service request sub isA2P_eService { my $self = shift ; # 1. Parse and validate buffer or return without error $self->setDTD('a2p-eservice.dtd'); my $doc = $self->isA2P_valid('') ; return 0 unless ( $doc and ref($doc) =~ /^XML::LibXML::Document$/ ); # 2. Check document format: we must have an a2p node as root return $self->error(472,"Document has no 'a2p' node") unless ( $doc->hasChildNodes and $doc->getChildNodes->size == 1 and $doc->lastChild->nodeName eq 'a2p'); my $a2p = $self->{A2P} = $doc->lastChild ; my @nodes = () ; # 3. Keep ref to service node and check it exists foreach my $node ( $a2p->getChildNodes ) { my $nodename = $node->nodeName ; push @nodes , $nodename ; next unless ( $nodename =~ /^service$/i ); $self->{SERVICE} = $node ; } unless (grep { /^service$/i } @nodes and defined($self->{SERVICE})) { $self->error(473,"XML request must content a 'service' node"); &Info("Available nodes: @nodes"); return 0 ; } &Debug("Available nodes: @nodes"); # 4. Check name attribut is defined my $name = $self->{SERVICE}->getAttribute("name"); return $self->error(474,"No service name found to handle request") unless (defined($name) and $name ); return $self->error(475,"'$name' found service name is prohibited") if ( $name eq '200' ); &Debug("Claimed E-Service is '$name'"); $self->{'E-SERVICE'} = $name ; # 5. Check pdf and file attribut for just overriding warns my $pdf = $self->{SERVICE}->getAttribute("pdf"); &Warn("service node 'pdf' attribut with value '$pdf' will be lost") if (defined($pdf) and $pdf ); my $file = $self->{SERVICE}->getAttribute("file"); &Warn("service node 'file' attribut with value '$file' will be lost") if (defined($file) and $file ); # 6. Returns E-Service name as true value return $name ; } sub set_a2p_attribut { my $self = shift ; my $name = shift || "" ; my $value = shift || "" ; return 0 unless (defined($self->{A2P}) and $name and $value); $self->{A2P}->setAttribute( $name , $value ); return $self->{A2P}->hasAttribute( $name ); } sub A2Panswer { my $self = shift ; return join("\n",( @{$self->{MESSAGE}}, $self->error )) unless ( ref($self->{XML}) =~ /^XML::LibXML::Document$/ ); # We need to update XML and return its text version my $a2p = $self->{XML}->lastChild ; my $lastnode = 0 ; my $key = 'PATH' ; $self->{PATH} = (defined($self->{PDFS}) and @{$self->{PDFS}})? shift @{$self->{PDFS}} : "" ; # Update path or content if (defined($self->{CONTENT})) { $key = 'CONTENT' ; open PDF, $self->{PATH} or return $self->error(467,"Can't open pdf file for reading: $!"); $self->{CONTENT} = &encode_base64(join('',)); close(PDF); # Delete PDF as it is requested unlink $self->{PATH} ; } if (defined($self->{PATH}) and $self->{PATH}) { foreach my $node ( $a2p->getChildNodes ) { next unless ( $node->nodeName =~ /^$key$/i ); $lastnode = $node->cloneNode ; $lastnode->appendText( $self->{$key} ); $lastnode->setAttribute("type","pdf"); $node->replaceNode( $lastnode ); # We must have only one node to update last ; } } else { $self->A2Pabterm ; $self->error(470,"No pdf generated: '$_'"); $self->A2Pmessage("A2P-Error-XML-no-pdf-available"); } # Check we don't received another PDF or we didn't update the node if ( defined($self->{PDFS}) and @{$self->{PDFS}} ) { map { $self->error(468,"Too much pdf generated: '$_'"); $self->A2Pmessage("A2P-Error-Other-PDF = $_"); $self->A2Pabterm ; } @{$self->{PDFS}} ; } elsif ( defined($self->{PDFS}) and ! $lastnode ) { $self->A2Pabterm ; $self->error(469,"Too much pdf generated: '$_'"); $self->A2Pmessage("A2P-Error-XML-not-updated"); } # Set return code my $RC = sprintf("%02d",$self->{STATE}) ; my ( $returnCode ) = grep { $_->nodeName =~ /^ReturnCode$/i } $a2p->getChildNodes ; if (defined($returnCode)) { $lastnode = $returnCode->cloneNode ; $lastnode->appendText( $RC ); $returnCode->replaceNode( $lastnode ); } else { $a2p->appendTextChild( "returnCode", $RC ); } # Set messages on bad status $a2p->appendTextChild( "message", join("\n",@{$self->{MESSAGE}}) ) if ($self->{STATE}); my $answer = $self->{XML}->toString ; &Debug("XML answer is '$answer'"); return $answer ; } sub A2Pmessage { my $self = shift ; &Debug("Got XML message as '@_'"); push @{$self->{MESSAGE}}, @_ ; } sub A2Ppdf { my $self = shift ; push @{$self->{PDFS}}, @_ ; } sub getPATH { my $self = shift ; return defined($self->{PATH}) ? $self->{PATH} : "" ; } sub A2Pabterm { $_[0]->{STATE} ++ ; } sub setstate { my $self = shift ; my $state = shift ; my $path = $DONESPOOL ; # Check state unless ($state =~ /^OK$/i) { $path = $ERRORSPOOL ; $self->{STATE} ++ ; } # Update PDFs path @{$self->{PDFS}} = map { $path . '/' . $_ } @{$self->{PDFS}} ; } ################################################################################ # # General common APIs # ################################################################################ sub error { my $self = shift ; if ( @_ ) { my ( $errno , $msg ) = @_ ; # Reset ERROR when requested return @{$self->{ERROR}} = ( 0 ) unless (defined($errno) and $errno ); return 0 unless (defined($msg)); push @{$self->{ERROR}} , $msg ; &Error( "Error $errno: $msg on " . $self->{ID} ); return 0 ; } else { return wantarray ? grep { ! /^\d+$/ } @{$self->{ERROR}} : $self->{ERROR}->[0] ; } } sub DESTROY { my $self = shift; my @err = @{$self->{ERROR}} ; if (@err) { &UPSTAT('GET-XML-ERROR'); &UPSTAT('GET-XML-ERROR-' . $err[0] ); &Debug($self->{ID} . " XML object destroyed with error #" . $err[0]); } else { &UPSTAT('GOT-GOOD-XML'); &Debug($self->{ID} . " XML object destroyed"); } delete $IDs{$self->{ID}} ; } ################################################################################ ### Load default inline DTDs ################################################### ################################################################################ sub SaveDTD ( $ \$ ) { my $name = shift || "" ; my $buffer = shift ; return &Error("No name provided to keep DTD in memory") unless $name ; unless ($$buffer) { &Debug("Nothing to save for '$name' DTD"); return 1 ; } my $string = $$buffer ; # Empty the buffer $$buffer = "" ; &Debug("Saving '$name' DTD in memory..."); $DTD{$name} = XML::LibXML::Dtd->parse_string($string); } { my ( $name , $buffer , $count ) = ( "" , "" , 0 ); $! = 0 ; &Debug("Loading default DTD definitions..."); while () { next if /^\s*$/ ; # Skip empty lines next if /^####/ ; # Skip big comment lines chomp ; # Start a new DTD if (/^### DTD\s+'([^']*)'/) { die "Can't initialize '$name' DTD: $!\n" unless (!$name or ($name and $buffer and &SaveDTD($name,\$buffer) and ++ $count)); $name = $1 ; &Debug("Reading '$name' DTD..."); next ; } $buffer .= $_ . "\n" ; } # Save last DTD die "Can't initialize '$name' DTD: $!\n" unless ($name and $buffer and &SaveDTD($name,\$buffer)); close(DATA); &Debug("$count DTD loaded"); } &Debug("Module " . __PACKAGE__ . " v$VERSION loaded"); 1; ################################################################################ ### Each DTD is defined by an id and a name from the following format line ### ### After the __DATA__ inline tag ### ### DTD 'id' 'name' any string as comment for example ### ################################################################################ __DATA__ ################################################################################ ### DTD 'a2p-listener.dtd' A2P Listener DTD ### ################################################################################ ################################################################################ ### DTD 'a2p-eservice.dtd' A2P E-Service DTD ### ################################################################################ ################################################################################ ### DTD 'e-service.conf.dtd' Configuration E-Service DTD ### ################################################################################