File Coverage

blib/lib/XML/FeedLite.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             #########
2             # Author: rmp@psyphi.net
3             # Maintainer: rmp@psyphi.net
4             # Created: 2006-06-08
5             # Last Modified: $Date: 2009/01/29 15:40:33 $
6             # Id: $Id: FeedLite.pm,v 1.9 2009/01/29 15:40:33 zerojinx Exp $
7             # Source: $Source: /cvsroot/xml-feedlite/xml-feedlite/lib/XML/FeedLite.pm,v $
8             # $HeadURL$
9             #
10             package XML::FeedLite;
11 6     6   46358 use strict;
  6         11  
  6         194  
12 6     6   32 use warnings;
  6         12  
  6         146  
13 6     6   7223 use WWW::Curl::Simple;
  0            
  0            
14             use HTTP::Request;
15             use HTTP::Headers;
16             use HTML::Entities;
17             use MIME::Base64;
18             use English qw(-no_match_vars);
19             use Carp;
20             use Readonly;
21              
22             our $VERSION = do { my @r = (q$Revision: 2.0 $ =~ /\d+/smxg); sprintf '%d.'.'%03d' x $#r, @r };
23             our $DEBUG = 0;
24              
25             Readonly::Scalar our $TIMEOUT => 30;
26             Readonly::Scalar our $MAX_REQ => 5;
27              
28             our $PATTERNS = {
29             'ENTRIES' => {
30             'atom' => qr{<entry[^>]*>(.*?)</entry>}smix,
31             'rss' => qr{<item(?:\ [^>]*|)>(.*?)</item>}smix,
32             },
33             'META' => {
34             'atom' => {
35             'title' => qr{<feed.*?<title[^>]*>(.*?)</title>}smix,
36             },
37             'rss' => {
38             'title' => qr{<channel.*?<title[^>]*>(.*?)</title.*?</channel>}smix,
39             },
40             },
41             };
42              
43              
44             sub new {
45             my ($class, $ref) = @_;
46             my $self = {
47             'url' => [],
48             'timeout' => $TIMEOUT,
49             'data' => {},
50             };
51              
52             bless $self, $class;
53              
54             if($ref && (ref $ref eq 'HASH')) {
55             for my $arg (qw(url timeout http_proxy proxy_user proxy_pass user_agent)) {
56             if(defined $ref->{$arg} && $self->can($arg)) {
57             $self->$arg($ref->{$arg});
58             }
59             }
60              
61             } elsif($ref) {
62             $self->url($ref);
63             }
64              
65             return $self;
66             }
67              
68             sub http_proxy {
69             my ($self, $proxy) = @_;
70             $proxy and $self->{http_proxy} = $proxy;
71              
72             if(!$self->{'_checked_http_proxy_env'}) {
73             $self->{http_proxy} ||= $ENV{http_proxy};
74             $self->{'_checked_http_proxy_env'} = 1;
75             }
76              
77             $self->{http_proxy} ||= q();
78              
79             if($self->{http_proxy} =~ m{^(https?://)(\S+):(.*?)\@(.*?)$}smx) {
80             #########
81             # http_proxy contains username & password - we'll set them up here:
82             #
83             $self->proxy_user($2);
84             $self->proxy_pass($3);
85              
86             $self->{http_proxy} = "$1$4";
87             }
88              
89             return $self->{http_proxy};
90             }
91              
92             sub _accessor {
93             my ($self, $field, $val) = @_;
94             $val and $self->{$field} = $val;
95             return $self->{$field};
96             }
97              
98             sub proxy_user {
99             my ($self, @args) = @_;
100             return $self->_accessor('proxy_user', @args);
101             }
102              
103             sub proxy_pass {
104             my ($self, @args) = @_;
105             return $self->_accessor('proxy_pass', @args);
106             }
107              
108             sub user_agent {
109             my ($self, @args) = @_;
110             return $self->_accessor('user_agent', @args) || "XML::FeedLite v$VERSION";
111             }
112              
113             sub timeout {
114             my ($self, @args) = @_;
115             return $self->_accessor('timeout', @args);
116             }
117              
118             sub url {
119             my ($self, $url) = @_;
120              
121             if($url) {
122             $self->reset();
123              
124             if(ref $url eq 'ARRAY') {
125             $self->{url} = $url;
126              
127             } else {
128             $self->{url} = [$url];
129             }
130             }
131              
132             return $self->{url};
133             }
134              
135             sub reset { ## no critic
136             my $self = shift;
137             delete $self->{results};
138             delete $self->{feedmeta};
139             delete $self->{data};
140             return;
141             }
142              
143             sub entries {
144             my ($self, $url, $opts) = @_;
145              
146             if(exists $self->{results}) {
147             return $self->{results};
148             }
149              
150             my $results = {};
151             my $ref = {};
152             my $ar_url = [];
153              
154             if($url && $opts) {
155             if(ref $url) {
156             $ar_url = $url;
157             } else {
158             $ar_url = [$url];
159             }
160             } else {
161             $ar_url = $self->url();
162             $opts = $url;
163             }
164             $opts ||= {};
165              
166             for my $s_url (grep { $_ } @{$ar_url}) {
167             #########
168             # loop over urls to fetch
169             #
170             $results->{$s_url} = [];
171             $self->{feedmeta}->{$s_url} = {};
172              
173             $ref->{$s_url} = sub {
174             my $blk_ref = shift;
175             my $blk = ${$blk_ref};
176             $self->{data}->{$s_url} .= $blk;
177              
178             if(!$self->{format}->{$s_url}) {
179             if($blk =~ m{xmlns\s*=\s*['"]https?://[a-z\d\.\-/]+/atom}smix) {
180             $self->{format}->{$s_url} = 'atom';
181              
182             } elsif($blk =~ m{xmlns\s*=\s*['"]https?://[a-z\d\.\-/]+/rss}smix) {
183             $self->{format}->{$s_url} = 'rss';
184              
185             } elsif($blk =~ m{rss\s+version\s*=\s*"2.0"}smix) {
186             $self->{format}->{$s_url} = 'rss';
187             }
188             }
189              
190             my $feedmeta = $self->{feedmeta}->{$s_url};
191             for my $f (keys %{$PATTERNS->{META}->{$self->{format}->{$s_url}}}) {
192             if($feedmeta->{$f}) {
193             next;
194             }
195              
196             my $pat = $PATTERNS->{META}->{$self->{format}->{$s_url}}->{$f};
197             ($feedmeta->{$f}) = $blk =~ /$pat/smx;
198             }
199              
200             my $pat = $PATTERNS->{ENTRIES}->{$self->{format}->{$s_url}};
201             if(!$pat) {
202             carp qq(No pattern defined for url=$s_url fmt=@{[$self->{format}->{$s_url}||'unknown']});
203             return;
204             }
205              
206             while($self->{data}->{$s_url} =~ s/$pat//smx) {
207             $self->_parse_entry($results->{$s_url}, $1);
208             }
209             return;
210             };
211             }
212              
213             $self->fetch($ref, $opts->{headers});
214              
215             $DEBUG and print {*STDERR} qq(Content retrieved\n);
216              
217             $self->{results} = $results;
218             return $results;
219             }
220              
221             sub _parse_entry {
222             my ($self, $results, $blk) = @_;
223             my $entry = {};
224             $blk ||= q();
225              
226             my $pat = qr{(<([a-z:]+)([^>]*)>(.*?)</\2>|<([a-z:]+)([^>]*)/>)}smix;
227             while($blk =~ s{$pat}{}smx) {
228              
229             my ($tag, $attr, $content);
230             if($4) {
231             ($tag, $attr, $content) = ($2, $3, $4);
232              
233             } else {
234             ($tag, $attr) = ($5, $6)
235             }
236              
237             my $tagdata = {};
238             $attr ||= q();
239              
240             while($attr =~ s{(\S+)\s*=\s*["']([^"']*)["']}{}smx) {
241             if($2) {
242             $tagdata->{$1} = $2;
243             }
244             }
245              
246             if($content) {
247             my $mode = $tagdata->{'mode'} || q();
248              
249             if($mode eq 'escaped') {
250             $content = decode_entities($content);
251              
252             } elsif($mode eq 'base64') {
253             $content = decode_base64($content);
254             }
255              
256             $tagdata->{'content'} = $content;
257             }
258              
259             if(scalar keys %{$tagdata}) {
260             push @{$entry->{$tag}}, $tagdata;
261             }
262             }
263              
264             push @{$results}, $entry;
265             return q();
266             }
267              
268             sub meta {
269             my ($self, $feed) = @_;
270              
271             if(!$self->{'_fetched'}) {
272             $self->entries($feed);
273             $self->{'_fetched'} = 1;
274             }
275              
276             if($feed) {
277             return $self->{feedmeta}->{$feed}||{};
278             }
279              
280             return $self->{feedmeta}||{};
281             }
282              
283             sub title {
284             my ($self, $feed) = @_;
285             return $self->meta($feed)->{title} || 'Untitled';
286             }
287              
288             sub fetch {
289             my ($self, $url_ref, $headers) = @_;
290              
291             my $ua = WWW::Curl::Simple->new;
292              
293             $self->{'statuscodes'} = {};
294             if(!$headers) {
295             $headers = {};
296             }
297              
298             if($ENV{HTTP_X_FORWARDED_FOR}) {
299             $headers->{'X-Forwarded-For'} ||= $ENV{'HTTP_X_FORWARDED_FOR'};
300             }
301              
302             for my $url (keys %{$url_ref}) {
303             if(ref $url_ref->{$url} ne 'CODE') {
304             $DEBUG and print {*STDERR} qq[handler for $url isn't CODE];
305             next;
306             }
307             $DEBUG and print {*STDERR} qq(Building HTTP::Request for $url [timeout=$self->{'timeout'}] via $url_ref->{$url}\n);
308              
309             my $http_headers = HTTP::Headers->new(%{$headers});
310             $http_headers->user_agent($self->user_agent());
311              
312             if($self->proxy_user() && $self->proxy_pass()) {
313             $headers->proxy_authorization_basic($self->proxy_user(), $self->proxy_pass());
314             }
315              
316             $ua->add_request(HTTP::Request->new('GET', $url, $http_headers));
317             }
318              
319             $DEBUG and print {*STDERR} qq(Requests submitted. Waiting for content\n);
320             my $ref = $ua->wait;
321              
322             for my $curl_req (values %{$ref}) {
323             my $content = $curl_req->body;
324             my $uri = $curl_req->request->uri;
325             $self->{statuscodes}->{$uri} = $curl_req->head =~ /HTTP\S+\s+(\d+)/smx;
326             $url_ref->{$uri}->($content);
327             }
328              
329             return;
330             }
331              
332             sub statuscodes {
333             my ($self, $url) = @_;
334             $self->{statuscodes} ||= {};
335              
336             return $url?$self->{statuscodes}->{$url}:$self->{statuscodes};
337             }
338              
339             sub max_req {
340             my ($self, @args) = @_;
341             return $self->_accessor('max_req', @args);
342             }
343              
344             1;
345             __END__
346              
347             =head1 NAME
348              
349             XML::FeedLite - Perl extension for fetching Atom and RSS feeds with minimal outlay
350              
351             =head1 VERSION
352              
353             $Revision: 1.9 $
354              
355             =head1 SYNOPSIS
356              
357             use XML::FeedLite;
358              
359             =head1 DESCRIPTION
360              
361             This module fetches and processes Atom and RSS-format XML feeds. It's
362             designed as an alternative to XML::Atom, specifically to work better
363             under mod_perl. This module requires LWP::Parallel::UserAgent.
364              
365             =head1 SUBROUTINES/METHODS
366              
367             =head2 new - Constructor
368              
369             my $xfl = XML::FeedLite->new('http://www.atomenabled.org/atom.xml');
370              
371             my $xfl = XML::FeedLite->new([qw(http://www.atomenabled.org/atom.xml
372             http://slashdot.org/slashdot.rss)]);
373              
374             my $xfl = XML::FeedLite->new({
375             timeout => 60,
376             url => 'http://www.atomenabled.org/atom.xml',
377             http_proxy => 'http://user:pass@webcache.local.com:3128/',
378             });
379              
380             Options can be: url (optional scalar or array ref, URLs of feeds)
381             timeout (optional int, HTTP fetch timeout in seconds)
382             http_proxy (optional scalar, web cache or proxy if not set in %ENV)
383             proxy_user (optional scalar, username for authenticating forward-proxy)
384             proxy_pass (optional scalar, password for authenticating forward-proxy)
385             user_agent (optional scalar, User-Agent HTTP request header value)
386              
387              
388             Very often you'll want to use XML:::FeedLite::Normalised instead of this baseclass.
389              
390             =head2 http_proxy - Get/Set http_proxy
391              
392             $xfl->http_proxy("http://user:pass@squid.myco.com:3128/");
393              
394             =head2 proxy_user - Get/Set proxy username for authenticating forward-proxies
395              
396             This is only required if the username wasn't specified when setting http_proxy
397              
398             $xfl->proxy_user('myusername');
399              
400             =head2 proxy_pass - Get/Set proxy password for authenticating forward-proxies
401              
402             This is only required if the password wasn't specified when setting http_proxy
403              
404             $xfl->proxy_pass('secretpassword');
405              
406             =head2 user_agent - Get/Set user-agent for request headers
407              
408             $xfl->user_agent('Feedtastic/1.0');
409              
410             =head2 timeout - Get/Set timeout
411              
412             $xfl->timeout(30);
413              
414             =head2 url - Get/Set DSN
415              
416             $xfl->url('http://das.ensembl.org/das/ensembl1834/'); # give url (scalar or arrayref) here if not specified in new()
417              
418             Or, if you want to add to the existing url list and you're feeling sneaky...
419              
420             push @{$xfl->url}, 'http://my.server/das/additionalsource';
421              
422             =head2 reset - Flush bufers, reset flags etc.
423              
424             $xfl->reset();
425              
426             =head2 entries - Retrieve XML::Simple data structures from feeds
427              
428             my $entry_data = $xfl->entries();
429              
430             =head2 meta - Meta data globally keyed on feed, or for a given feed
431              
432             my $hrMeta = $xfl->meta();
433             my $hrFeedMeta = $xfl->meta('http://mysite.com/feed.xml');
434              
435             =head2 title - The name/title of a given feed
436              
437             my $title = $xfl->title($feed);
438              
439             =head2 fetch - Performs the HTTP fetch and processing
440              
441             $xfl->fetch({
442             #########
443             # URLs and associated callbacks
444             #
445             'url1' => sub { ... },
446             'url2' => sub { ... },
447             },
448             {
449             #########
450             # Optional HTTP headers
451             #
452             'X-Forwarded-For' => 'a.b.c.d',
453             });
454              
455             =head2 statuscodes - Retrieve HTTP status codes for request URLs
456              
457             my $code = $xfl->statuscodes($url);
458             my $code_hashref = $xfl->statuscodes();
459              
460             =head2 max_req - set number of running concurrent requests
461              
462             $xfl->max_req(5);
463             print $xfl->max_req();
464              
465             =head1 DIAGNOSTICS
466              
467             =head1 CONFIGURATION AND ENVIRONMENT
468              
469             =head1 DEPENDENCIES
470              
471             =over
472              
473             =item strict
474              
475             =item warnings
476              
477             =item WWW::Curl::Simple
478              
479             =item HTTP::Request
480              
481             =item HTTP::Headers
482              
483             =item HTML::Entities
484              
485             =item MIME::Base64
486              
487             =item English
488              
489             =item Carp
490              
491             =item Readonly
492              
493             =back
494              
495             =head1 INCOMPATIBILITIES
496              
497             =head1 BUGS AND LIMITATIONS
498              
499             =head1 AUTHOR
500              
501             Roger Pettett, E<lt>rmp@psyphi.netE<gt>
502              
503             =head1 LICENSE AND COPYRIGHT
504              
505             Copyright (C) 2010 by Roger Pettett
506              
507             This library is free software; you can redistribute it and/or modify
508             it under the same terms as Perl itself, either Perl version 5.8.4 or,
509             at your option, any later version of Perl 5 you may have available.
510              
511             =cut