File Coverage

blib/lib/XML/RSS/Tools.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


line stmt bran cond sub pod time code
1             # --------------------------------------------------
2             #
3             # XML::RSS::Tools
4             # Version 0.34
5             # $Id: Tools.pm 101 2014-05-27 14:25:39Z adam $
6             #
7             # Copyright iredale Consulting, all rights reserved
8             # http://www.iredale.net/
9             #
10             # OSI Certified Open Source Software
11             #
12             # --------------------------------------------------
13              
14             package XML::RSS::Tools;
15              
16 13     13   572589 use 5.010; # No longer been tested on anything earlier
  13         50  
  13         565  
17 13     13   13793 use utf8;
  13         129  
  13         68  
18 13     13   419 use strict; # Naturally
  13         27  
  13         409  
19 13     13   68 use warnings; # Naturally
  13         27  
  13         430  
20 13     13   66 use warnings::register; # So users can "use warnings 'XML::RSS::Tools'"
  13         23  
  13         7021  
21 13     13   81 use Carp; # We're a nice module
  13         25  
  13         1076  
22 13     13   12159 use XML::RSS; # Handle the RSS/RDF files
  0            
  0            
23             use XML::LibXML; # Hand the XML file for XSLT
24             use XML::LibXSLT; # Hand the XSL file and do the XSLT
25             use URI; # Deal with URIs nicely
26             use FileHandle; # Allow the use of File Handle Objects
27              
28             our $VERSION = '0.34';
29              
30             #
31             # Tools Constructor
32             #
33              
34             sub new {
35             my $class = shift;
36             my %args = @_;
37              
38             my $object = bless {
39             _rss_version => 0.91, # We convert all feeds to this version
40             _xml_string => q{}, # Where we hold the input RSS/RDF
41             _xsl_string => q{}, # Where we hold the XSL Template
42             _output_string => q{}, # Where the output string goes
43             _transformed => 0, # Flag for transformation
44             _error_message => q{}, # Error message
45             _uri_url => q{}, # URI URL
46             _uri_file => q{}, # URI File
47             _uri_scheme => q{}, # URI Scheme
48             _xml_catalog => q{}, # XML Catalog file
49             _http_client => 'auto', # Which HTTP Client to use
50             _proxy_server => q{}, # A Proxy Server
51             _proxy_user => q{}, # Username on the proxy server
52             _proxy_password => q{}, # Password for user on the proxy server
53             _debug => $args{debug} || 0, # Debug flag
54             _auto_wash => $args{auto_wash} || 1, # Flag for auto_washing input RSS/RDF
55             },
56             ref $class || $class;
57              
58             if ( $args{version} ) {
59             croak "No such version of RSS $args{version}"
60             unless set_version( $object, $args{version} );
61             }
62              
63             if ( $args{http_client} ) {
64             croak "Not configured for HTTP Client $args{http_client}"
65             unless set_http_client( $object, $args{http_client} );
66             }
67              
68             if ( $args{xml_catalog} ) {
69             croak 'XML Catalog Support not enabled in your version of XML::LibXML'
70             if $XML::LibXML::VERSION < 1.53;
71             croak "Unable to read XML catalog $args{xml_catalog}"
72             unless set_xml_catalog( $object, $args{xml_catalog} );
73             }
74              
75             return $object;
76             }
77              
78             #
79             # Output what we have as a string
80             #
81             sub as_string {
82             my $self = shift;
83             my $mode = shift || q{};
84              
85             if ($mode) {
86             if ( $mode =~ /rss/mxi ) {
87             carp 'No RSS File to output'
88             if !$self->{_rss_string} && $self->{_debug};
89             return $self->{_rss_string};
90             }
91             elsif ( $mode =~ /xsl/mxi ) {
92             carp 'No XSL Template to output'
93             if !$self->{_xsl_string} && $self->{_debug};
94             return $self->{_xsl_string};
95             }
96             elsif ( $mode =~ /error/mxi ) {
97             if ( $self->{_error_message} ) {
98             my $message = $self->{_error_message};
99             $self->{_error_message} = q{};
100             return $message;
101             }
102             }
103             else {
104             croak "Unknown mode: $mode";
105             }
106             }
107             else {
108             carp 'Nothing To Output Yet'
109             if !$self->{_transformed} && $self->{_debug};
110             return $self->{_output_string};
111             }
112             return;
113             }
114              
115             #
116             # Set/Read the debug level
117             #
118             sub debug {
119             my $self = shift;
120             my $debug = shift;
121             $self->{_debug} = $debug if defined $debug;
122             return $self->{_debug};
123             }
124              
125             #
126             # Read the auto_wash level
127             #
128             sub get_auto_wash {
129             my $self = shift;
130             return $self->{_auto_wash};
131             }
132              
133             #
134             # Set the auto_wash level
135             #
136             sub set_auto_wash {
137             my $self = shift;
138             my $wash = shift;
139             $self->{_auto_wash} = $wash if defined $wash;
140             return $self->{_auto_wash};
141             }
142              
143             #
144             # Read the HTTP client mode
145             #
146             sub get_http_client {
147             my $self = shift;
148             return $self->{_http_client};
149             }
150              
151             #
152             # Set which HTTP client to use
153             #
154             sub set_http_client {
155             my $self = shift;
156             my $client = shift;
157              
158             return $self->_raise_error( 'No HTTP Client requested' )
159             unless defined $client;
160             return $self->_raise_error( "Not configured for HTTP Client $client" )
161             unless ( grep {/$client/mx} qw(auto ghttp lwp lite curl) );
162              
163             $self->{_http_client} = lc $client;
164             return $self->{_http_client};
165             }
166              
167             #
168             # Get the HTTP proxy
169             #
170             sub get_http_proxy {
171             my $self = shift;
172             my $proxy;
173              
174             if ( $self->{_proxy_server} ) {
175             $proxy = $self->{_proxy_user} . q{:} . $self->{_proxy_password} . q{@}
176             if ( $self->{_proxy_user} && $self->{_proxy_password} );
177             $proxy .= $self->{_proxy_server};
178             return $proxy;
179             }
180             }
181              
182             #
183             # Set the HTTP proxy
184             #
185             sub set_http_proxy {
186             my $self = shift;
187             my %args = @_;
188              
189             $self->{_proxy_server} = $args{proxy_server};
190             $self->{_proxy_user} = $args{proxy_user};
191             $self->{_proxy_password} = $args{proxy_pass};
192              
193             return $self;
194             }
195              
196             #
197             # Get the RSS Version
198             #
199             sub get_version {
200             my $self = shift;
201             return $self->{_rss_version};
202             }
203              
204             #
205             # Set the RSS Version
206             #
207             sub set_version {
208             my $self = shift;
209             my $version = shift;
210              
211             return $self->_raise_error( 'No RSS version supplied' )
212             unless defined $version;
213             return $self->_raise_error("No such version of RSS $version")
214             unless ( grep {/$version/mx} qw(0 0.9 0.91 0.92 0.93 0.94 1.0 2.0) );
215              
216             $self->{_rss_version} = $version;
217             if ($version) {
218             return $self->{_rss_version};
219             }
220             else {
221             return '0.0';
222             }
223             }
224              
225             #
226             # Get XML Catalog File
227             #
228             sub get_xml_catalog {
229             my $self = shift;
230             return $self->{_xml_catalog};
231             }
232              
233             #
234             # Set XML catalog file
235             #
236             sub set_xml_catalog {
237             my $self = shift;
238             my $catalog_file = shift;
239              
240             croak 'XML Catalog Support not enabled in your version of XML::LibXML'
241             if $XML::LibXML::VERSION < 1.53;
242              
243             if ( $self->_check_file( $catalog_file ) ) {
244             $self->{_xml_catalog} = $catalog_file;
245             return $self;
246             }
247             else {
248             return;
249             }
250             }
251              
252             #
253             # Load an RSS file, and call RSS conversion to standard RSS format
254             #
255             sub rss_file {
256             my $self = shift;
257             my $file_name = shift;
258              
259             if ( $self->_check_file( $file_name ) ) {
260             my $fh = FileHandle->new( $file_name, 'r' )
261             or croak "Unable to open $file_name for reading";
262             $self->{_rss_string} = $self->_load_filehandle( $fh );
263             undef $fh;
264             $self->_parse_rss_string;
265             $self->{_transformed} = 0;
266             return $self;
267             }
268             else {
269             return;
270             }
271             }
272              
273             #
274             # Load an XSL file
275             #
276             sub xsl_file {
277             my $self = shift;
278             my $file_name = shift;
279              
280             if ( $self->_check_file( $file_name ) ) {
281             my $fh = FileHandle->new( $file_name, 'r' )
282             or croak "Unable to open $file_name for reading";
283             $self->{_xsl_string} = $self->_load_filehandle( $fh );
284             undef $fh;
285             $self->{_transformed} = 0;
286             return $self;
287             }
288             else {
289             return;
290             }
291             }
292              
293             #
294             # Load an RSS file from a FH, and call RSS conversion to standard RSS format
295             #
296             sub rss_fh {
297             my $self = shift;
298             my $file_name = shift;
299              
300             if ( ref $file_name eq 'FileHandle' ) {
301             $self->{_rss_string} = $self->_load_filehandle( $file_name );
302             _parse_rss_string($self);
303             $self->{_transformed} = 0;
304             return $self;
305             }
306             else {
307             return $self->_raise_error(
308             'FileHandle error: No FileHandle Object Passed' );
309             }
310             }
311              
312             #
313             # Load an XSL file from a FH
314             #
315             sub xsl_fh {
316             my $self = shift;
317             my $file_name = shift;
318              
319             if ( ref $file_name eq 'FileHandle' ) {
320             $self->{_xsl_string} = $self->_load_filehandle( $file_name );
321             $self->{_transformed} = 0;
322             return $self;
323             }
324             else {
325             return $self->_raise_error(
326             'FileHandle error: No FileHandle Object Passed' );
327             }
328             }
329              
330             #
331             # Load an RSS file via HTTP and call RSS conversion to standard RSS format
332             #
333             sub rss_uri {
334             my $self = shift;
335             my $uri = shift;
336              
337             $uri = $self->_process_uri( $uri );
338             return unless $uri;
339              
340             return $self->rss_file( $self->{_uri_file} )
341             if ( $self->{_uri_scheme} eq 'file' );
342              
343             my $xml = $self->_http_get( $uri );
344             return unless $xml;
345             $self->{_rss_string} = $xml;
346             _parse_rss_string( $self );
347             $self->{_transformed} = 0;
348             return $self;
349             }
350              
351             #
352             # Load an XSL file via HTTP
353             #
354             sub xsl_uri {
355             my $self = shift;
356             my $uri = shift;
357              
358             $uri = $self->_process_uri( $uri );
359             return unless $uri;
360              
361             return $self->xsl_file( $self->{_uri_file} )
362             if ( $self->{_uri_scheme} eq 'file' );
363              
364             my $xml = $self->_http_get( $uri );
365             return unless $xml;
366             $self->{_xsl_string} = $xml;
367             $self->{_transformed} = 0;
368             return $self;
369             }
370              
371             #
372             # Parse a string and convert to standard RSS
373             #
374             sub rss_string {
375             my $self = shift;
376             my $xml = shift;
377              
378             return unless $xml;
379             $self->{_rss_string} = $xml;
380             _parse_rss_string($self);
381             $self->{_transformed} = 0;
382             return $self;
383             }
384              
385             #
386             # Import an XSL from string
387             #
388             sub xsl_string {
389             my $self = shift;
390             my $xml = shift;
391              
392             return unless $xml;
393             $self->{_xsl_string} = $xml;
394             $self->{_transformed} = 0;
395             return $self;
396             }
397              
398             #
399             # Do the transformation
400             #
401             sub transform {
402             my $self = shift;
403              
404             croak 'No XSLT loaded' unless $self->{_xsl_string};
405             croak 'No RSS loaded' unless $self->{_rss_string};
406             croak q{Can't transform twice without a change} if $self->{_transformed};
407              
408             my $xslt = XML::LibXSLT->new;
409             my $xml_parser = XML::LibXML->new;
410             if ( $self->{_xml_catalog} ) {
411             $xml_parser->load_catalog( $self->{_xml_catalog} ); # Load the catalogue
412             }
413             else {
414             $xml_parser->expand_entities( 0 ); # Otherwise don't touch entities
415             }
416             $xml_parser->keep_blanks( 0 );
417             $xml_parser->validation( 0 );
418             $xml_parser->complete_attributes( 0 );
419             my $source_xml = $xml_parser->parse_string( $self->{_rss_string} ); # Parse the source XML
420             my $style_xsl = $xml_parser->parse_string( $self->{_xsl_string} ); # and Template XSL files
421             my $stylesheet = $xslt->parse_stylesheet( $style_xsl ); # Load the parsed XSL into XSLT
422             my $result_xml = $stylesheet->transform( $source_xml ); # Transform the source XML
423             $self->{_output_string}
424             = $stylesheet->output_string( $result_xml ); # Store the result
425             $self->{_transformed} = 1;
426             return $self;
427             }
428              
429             # ---------------
430             # Private Methods
431             # ---------------
432              
433             #
434             # Parse the RSS string
435             #
436             sub _parse_rss_string {
437             my $self = shift;
438             my $xml = $self->{_rss_string};
439              
440             $xml = _wash_xml( $xml ) if $self->{_auto_wash};
441              
442             if ( $self->{_rss_version} ) { # Only normalise if version is true
443             my $rss = XML::RSS->new;
444             $rss->parse( $xml );
445             if ( $rss->{version} != $self->{_rss_version} ) {
446             $rss->{output} = $self->{_rss_version};
447             $xml = $rss->as_string;
448             $xml = _wash_xml( $xml ) if $self->{_auto_wash};
449             }
450             $self->{_xml_rss} = $rss;
451             }
452             $self->{_rss_string} = $xml;
453             return $self;
454             }
455              
456             #
457             # Load file from File Handle
458             #
459             sub _load_filehandle {
460             my $self = shift;
461             my $handle = shift;
462             my $content;
463              
464             while ( my $line = $handle->getline ) {
465             $content .= $line;
466             }
467             return $content;
468             }
469              
470             #
471             # Wash the XML File of known nasties
472             #
473             sub _wash_xml {
474             my $xml = shift;
475              
476             $xml = _clean_entities( $xml );
477             $xml =~ s/\s+/ /gsmx;
478             $xml =~ s/> />/gmx;
479             $xml =~ s/^.*(<\?xml)/$1/gsmx; # Remove bogus content before
480             return $xml;
481             }
482              
483             #
484             # Check that the requested file is there and readable
485             #
486             sub _check_file {
487             my $self = shift;
488             my $file_name = shift;
489              
490             return $self->_raise_error( 'File error: No file name supplied' )
491             unless $file_name;
492             return $self->_raise_error( "File error: Cannot find $file_name" )
493             unless -e $file_name;
494             return $self->_raise_error( "File error: $file_name isn't a real file" )
495             unless -f _;
496             return $self->_raise_error( "File error: Cannot read file $file_name" )
497             unless -r _;
498             return $self->_raise_error( "File error: $file_name is zero bytes long" )
499             if -z _;
500             return $self;
501             }
502              
503             #
504             # Process a URI ready for HTTP getting
505             #
506             sub _process_uri {
507             my $self = shift;
508             my $uri = shift;
509              
510             return $self->_raise_error( 'No URI provided.' ) unless $uri;
511             my $uri_object = URI->new( $uri )->canonical;
512             return $self->_raise_error( "URI provided ($uri) is not valid." )
513             unless $uri_object;
514              
515             $self->{_uri_scheme} = $uri_object->scheme;
516             return $self->_raise_error(
517             'No URI Scheme in ' . $uri_object->as_string . q{.} )
518             unless $self->{_uri_scheme};
519             return $self->_raise_error(
520             'Unsupported URI Scheme (' . $self->{_uri_scheme} . q{).} )
521             unless $self->{_uri_scheme} =~ /http|file/mx;
522              
523             $self->{_uri_file} = $uri_object->file if $self->{_uri_scheme} eq 'file';
524              
525             return $uri_object->as_string;
526             }
527              
528             #
529             # Grab something via HTTP
530             #
531             sub _http_get {
532             my $self = shift;
533             my $uri = shift;
534              
535             my $user_agent = "XML::RSS::Tools/$VERSION";
536              
537             if ( $self->{_http_client} eq 'auto' ) {
538             my @modules = qw "WWW::Curl::Easy HTTP::GHTTP HTTP::Lite LWP";
539             foreach my $module (@modules) {
540             eval { require $module; };
541             if ( ! $@ ) {
542             $self->{_http_client} = lc $module ;
543             $self->{_http_client} =~ s/.*:://mx;
544             last;
545             }
546             }
547             return $self->_raise_error(
548             'HTTP error: No HTTP client library installed')
549             if $self->{_http_client} eq 'auto';
550             }
551              
552             if ( $self->{_http_client} eq 'lite' ) {
553             require HTTP::Lite;
554             my $ua = HTTP::Lite->new;
555             $ua->add_req_header( 'User-Agent',
556             "$user_agent HTTP::Lite/$HTTP::Lite::VERSION ($^O)"
557             );
558             $ua->proxy( $self->{_proxy_server} ) if $self->{_proxy_server};
559             my $r = $ua->request($uri)
560             or return $self->_raise_error( "Unable to get document: $!" );
561             return $self->_raise_error( "HTTP error: $r, " . $ua->status_message )
562             unless $r == 200;
563             return $ua->body;
564             }
565              
566             if ( $self->{_http_client} eq 'lwp'
567             || $self->{_http_client} eq 'useragent' )
568             {
569             require LWP::UserAgent;
570             my $ua = LWP::UserAgent->new;
571             $ua->agent( $user_agent . ' ' . $ua->agent . " ($^O)" );
572             $ua->proxy( [ 'http', 'ftp' ], $self->{_proxy_server} )
573             if $self->{_proxy_server};
574             my $response = $ua->request( HTTP::Request->new( 'GET', $uri ) );
575             return $self->_raise_error( 'HTTP error: ' . $response->status_line )
576             if $response->is_error;
577             return $response->content( );
578             }
579              
580             if ( $self->{_http_client} eq 'ghttp' ) {
581             require HTTP::GHTTP;
582             my $ua = HTTP::GHTTP->new($uri);
583             $ua->set_header( 'User-Agent',
584             "$user_agent HTTP::GHTTP/$HTTP::GHTTP::VERSION ($^O)" );
585             if ( $self->{_proxy_server} ) {
586             $ua->set_proxy( $self->{_proxy_server} );
587             $ua->set_proxy_authinfo( $self->{_proxy_user},
588             $self->{_proxy_password} )
589             if ( $self->{_proxy_user} && $self->{_proxy_password} );
590             }
591             $ua->process_request;
592             my $xml = $ua->get_body;
593             if ( $xml ) {
594             my ( $status, $message ) = $ua->get_status;
595             return $self->_raise_error("HTTP error: $status, $message")
596             unless $status == 200;
597             return $xml;
598             }
599             else {
600             return $self->_raise_error(
601             "HTTP error: Unable to connect to server: $uri");
602             }
603             }
604              
605             if ($self->{_http_client} eq 'curl' ) {
606             require WWW::Curl::Easy;
607             my ($curl, $response_body, $file_b, $response_head,
608             $file_h, $response, $response_code);
609              
610             $curl = WWW::Curl::Easy->new;
611              
612             open $file_b, '>', \$response_body;
613             open $file_h, '>', \$response_head;
614              
615             $curl->setopt( WWW::Curl::Easy->CURLOPT_USERAGENT,
616             "$user_agent WWW::Curl::Easy/$WWW::Curl::Easy::VERSION ($^O)" );
617             $curl->setopt( WWW::Curl::Easy->CURLOPT_HEADER, 0 );
618             $curl->setopt( WWW::Curl::Easy->CURLOPT_NOPROGRESS, 1 );
619             $curl->setopt( WWW::Curl::Easy->CURLOPT_URL, $uri );
620             $curl->setopt( WWW::Curl::Easy->CURLOPT_WRITEDATA, $file_b );
621             $curl->setopt( WWW::Curl::Easy->CURLOPT_WRITEHEADER, $file_h );
622              
623             $response = $curl->perform;
624              
625             close $file_b;
626             close $file_h;
627              
628             if ($response == 0) {
629             $response_code = $curl->getinfo(
630             WWW::Curl::Easy->CURLINFO_HTTP_CODE );
631             return $self->_raise_error( "HTTP error: $response_code" )
632             unless $response_code == 200;
633             return $response_body
634             }
635             else {
636             return $self->_raise_error( "HTTP error : " .
637             $curl->strerror( $response ) . " ($response)" );
638             }
639              
640             }
641             }
642              
643             #
644             # Fix Entities
645             # This subroutine is a mix of Matt Sergent's rss-mirror script
646             # And chunks of the HTML::Entites module if you have Perl 5.8 or
647             # later you don't need this code.
648             #
649             sub _clean_entities {
650             my $xml = shift;
651              
652             my %entity = (
653             trade => '™',
654             euro => '€',
655             quot => q{"},
656             apos => q{'},
657             AElig => q{Æ},
658             Aacute => q{Á},
659             Acirc => q{Â},
660             Agrave => q{À},
661             Aring => q{Å},
662             Atilde => q{Ã},
663             Auml => q{Ä},
664             Ccedil => q{Ç},
665             ETH => q{Ð},
666             Eacute => q{É},
667             Ecirc => q{Ê},
668             Egrave => q{È},
669             Euml => q{Ë},
670             Iacute => q{Í},
671             Icirc => q{Î},
672             Igrave => q{Ì},
673             Iuml => q{Ï},
674             Ntilde => q{Ñ},
675             Oacute => q{Ó},
676             Ocirc => q{Ô},
677             Ograve => q{Ò},
678             Oslash => q{Ø},
679             Otilde => q{Õ},
680             Ouml => q{Ö},
681             THORN => q{Þ},
682             Uacute => q{Ú},
683             Ucirc => q{Û},
684             Ugrave => q{Ù},
685             Uuml => q{Ü},
686             Yacute => q{Ý},
687             aacute => q{á},
688             acirc => q{â},
689             aelig => q{æ},
690             agrave => q{à},
691             aring => q{å},
692             atilde => q{ã},
693             auml => q{ä},
694             ccedil => q{ç},
695             eacute => q{é},
696             ecirc => q{ê},
697             egrave => q{è},
698             eth => q{ð},
699             euml => q{ë},
700             iacute => q{í},
701             icirc => q{î},
702             igrave => q{ì},
703             iuml => q{ï},
704             ntilde => q{ñ},
705             oacute => q{ó},
706             ocirc => q{ô},
707             ograve => q{ò},
708             oslash => q{ø},
709             otilde => q{õ},
710             ouml => q{ö},
711             szlig => q{ß},
712             thorn => q{þ},
713             uacute => q{ú},
714             ucirc => q{û},
715             ugrave => q{ù},
716             uuml => q{ü},
717             yacute => q{ý},
718             yuml => q{ÿ},
719             copy => q{©},
720             reg => q{®},
721             nbsp => q{\240},
722             iexcl => q{¡},
723             cent => q{¢},
724             pound => q{£},
725             curren => q{¤},
726             yen => q{¥},
727             brvbar => q{¦},
728             sect => q{§},
729             uml => q{¨},
730             ordf => q{ª},
731             laquo => q{«},
732             'not' => q{¬},
733             shy => q{­},
734             macr => q{¯},
735             deg => q{°},
736             plusmn => q{±},
737             sup1 => q{¹},
738             sup2 => q{²},
739             sup3 => q{³},
740             acute => q{´},
741             micro => q{µ},
742             para => q{¶},
743             middot => q{·},
744             cedil => q{¸},
745             ordm => q{º},
746             raquo => q{»},
747             frac14 => q{¼},
748             frac12 => q{½},
749             frac34 => q{¾},
750             iquest => q{¿},
751             'times' => q{×},
752             divide => q{÷},
753             );
754             my $entities = join q{|}, keys %entity;
755             $xml =~ s/&(?!(#[0-9]+|#x[0-9a-fA-F]+|\w+);)/&/gm; # Matt's ampersand entity fixer
756             $xml =~ s/&($entities);/$entity{$1}/gimx; # Deal with odd entities
757             return $xml;
758             }
759              
760             #
761             # Raise error condition
762             #
763             sub _raise_error {
764             my $self = shift;
765             my $message = shift;
766              
767             $self->{_error_message} = $message;
768             carp $message if $self->{_debug};
769             return;
770             }
771              
772             1;
773              
774             __END__