File Coverage

blib/lib/WWW/Sitemap/XML.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             #ABSTRACT: XML Sitemap protocol
2 6     6   303323 use strict;
  6         12  
  6         169  
3 6     6   2068 use warnings;
  6         11  
  6         278  
4             package WWW::Sitemap::XML;
5             BEGIN {
6 6     6   162 $WWW::Sitemap::XML::AUTHORITY = 'cpan:AJGB';
7             }
8             $WWW::Sitemap::XML::VERSION = '2.02';
9 6     6   5066 use Moose;
  6         2921128  
  6         39  
10              
11 6     6   47548 use WWW::Sitemap::XML::URL;
  0            
  0            
12             use XML::LibXML qw(XML_ELEMENT_NODE);
13             use Scalar::Util qw( blessed );
14              
15             use WWW::Sitemap::XML::Types qw( SitemapURL );
16              
17              
18             has '_rootcontainer' => (
19             is => 'ro',
20             traits => [qw( Array )],
21             isa => 'ArrayRef',
22             default => sub { [] },
23             handles => {
24             _add_entry => 'push',
25             _count_entries => 'count',
26             _entries => 'elements',
27             }
28             );
29              
30             has '_first_loc' => (
31             is => 'rw',
32             );
33              
34             has '_check_req_interface' => (
35             is => 'ro',
36             default => sub {
37             sub {
38             die 'object does not implement WWW::Sitemap::XML::URL::Interface'
39             unless is_SitemapURL($_[0]);
40             }
41             }
42             );
43              
44             has '_entry_class' => (
45             is => 'ro',
46             default => 'WWW::Sitemap::XML::URL'
47             );
48              
49             has '_root_ns' => (
50             is => 'ro',
51             default => sub {
52             {
53             'xmlns' => "http://www.sitemaps.org/schemas/sitemap/0.9",
54             'xmlns:xsi' => "http://www.w3.org/2001/XMLSchema-instance",
55             'xsi:schemaLocation' => join(' ',
56             'http://www.sitemaps.org/schemas/sitemap/0.9',
57             'http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd'
58             ),
59             'xmlns:image' => "http://www.google.com/schemas/sitemap-image/1.1",
60             'xmlns:video' => "http://www.google.com/schemas/sitemap-video/1.1",
61             'xmlns:mobile' => "http://www.google.com/schemas/sitemap-mobile/1.0",
62             }
63             },
64             );
65              
66             has '_root_elem' => (
67             is => 'ro',
68             default => 'urlset',
69             );
70              
71             has '_entry_elem' => (
72             is => 'ro',
73             default => 'url',
74             );
75              
76             sub _pre_check_add {
77             my ($self, $entry) = @_;
78              
79             $self->_check_req_interface->($entry);
80              
81             die "Single file cannot contain more then 50 000 entries"
82             if $self->_count_entries >= 50_000;
83              
84             my $loc = $entry->loc;
85              
86             die "URL cannot be longer then 2048 characters"
87             unless length $loc < 2048;
88              
89             my($scheme, $authority) = $loc =~ m|(?:([^:/?#]+):)?(?://([^/?#]*))?|;
90             my $new = "$scheme://$authority";
91             if ( $self->_count_entries ) {
92             my $first = $self->_first_loc;
93              
94             die "All URLs in same file should use the same protocol and reside on the "
95             ."same host: $first, not $new" unless $first eq $new;
96             } else {
97             $self->_first_loc( $new );
98             }
99             }
100              
101              
102             sub add {
103             my $self = shift;
104              
105             my $class = $self->_entry_class;
106              
107             my $arg = @_ == 1 && blessed $_[0] ?
108             shift @_ : $class->new(@_);
109              
110             $self->_pre_check_add($arg);
111              
112             $self->_add_entry( $arg );
113             }
114              
115              
116             sub urls { shift->_entries }
117              
118              
119             sub load {
120             my $self = shift;
121              
122             $self->add($_) for $self->read(@_);
123             }
124              
125              
126             sub read {
127             my ($self, %args) = @_;
128              
129             my @entries;
130             my $class = $self->_entry_class;
131              
132             my $xml = XML::LibXML->load_xml( %args );
133             my $doc = $xml->getDocumentElement;
134              
135             my @ns = $doc->getNamespaces();
136             for my $ns ( @ns ) {
137             my $name = $ns->localname;
138             next unless $name;
139              
140             $self->_root_ns->{ $ns->nodeName } = $ns->href;
141             }
142              
143             for my $url ( $doc->getChildrenByLocalName( $self->_entry_elem ) ) {
144             my @childNodes = grep { $_->nodeType == XML_ELEMENT_NODE() } $url->nonBlankChildNodes;
145             my %args;
146             for my $n ( @childNodes ) {
147             my $localname = $n->localname;
148              
149             if ( $localname eq 'image' ) {
150             push @{ $args{images} }, {
151             map {
152             $_->localname => $_->textContent
153             }
154             grep { $_->nodeType == XML_ELEMENT_NODE() }
155             $n->nonBlankChildNodes
156             };
157             }
158             elsif ( $localname eq 'video' ) {
159             my $video = {};
160             my @videoChildNodes = grep { $_->nodeType == XML_ELEMENT_NODE() } $n->nonBlankChildNodes;
161              
162             for my $cn ( @videoChildNodes ) {
163             my $vname = $cn->localname;
164              
165             if ( $vname eq 'player_loc' ) {
166             $video->{player} = {
167             loc => $cn->textContent,
168             (
169             map {
170             $_ => $cn->getAttribute($_)
171             }
172             grep {
173             $cn->hasAttribute($_)
174             } qw( allow_embed autoplay )
175             )
176             };
177             }
178             else {
179             $video->{ $vname } = $cn->textContent;
180             }
181             }
182              
183             push @{ $args{videos} }, $video;
184             }
185             elsif ( $localname eq 'mobile' ) {
186             $args{mobile} = 1;
187             }
188             else {
189             $args{ $n->localname } = $n->textContent;
190             }
191             }
192             push @entries,
193             $class->new( %args );
194             }
195              
196             return @entries;
197             }
198              
199              
200             sub write {
201             my ($self, $fh, $format) = @_;
202              
203             $format ||= 0;
204              
205             my $writer = 'toFH';
206             my $xml = $self->as_xml;
207              
208             unless ( ref $fh ) {
209             $writer = 'toFile';
210             if ( $fh =~ /\.gz$/i ) {
211             $xml->setCompression(8);
212             }
213             }
214              
215             $xml->$writer( $fh, $format );
216             }
217              
218              
219             sub as_xml {
220             my $self = shift;
221              
222             my $xml = XML::LibXML->createDocument('1.0','UTF-8');
223             my $root = $xml->createElement($self->_root_elem);
224              
225             while (my ($k, $v) = each %{ $self->_root_ns() } ) {
226             $root->setAttribute($k, $v);
227             };
228              
229             $root->appendChild($_) for
230             map {
231             my $xml = $_->as_xml;
232             blessed $xml ? $xml : XML::LibXML->load_xml(string => $xml)->documentElement()
233             } $self->_entries;
234              
235             $xml->setDocumentElement($root);
236              
237             return $xml;
238             }
239              
240              
241             __PACKAGE__->meta->make_immutable;
242              
243             1;
244              
245             __END__
246              
247             =pod
248              
249             =encoding UTF-8
250              
251             =head1 NAME
252              
253             WWW::Sitemap::XML - XML Sitemap protocol
254              
255             =head1 VERSION
256              
257             version 2.02
258              
259             =head1 SYNOPSIS
260              
261             use WWW::Sitemap::XML;
262              
263             my $map = WWW::Sitemap::XML->new();
264              
265             # add new url
266             $map->add( 'http://mywebsite.com/' );
267              
268             # or
269             $map->add(
270             loc => 'http://mywebsite.com/',
271             lastmod => '2010-11-22',
272             changefreq => 'monthly',
273             priority => 1.0,
274             mobile => 1,
275             images => [
276             {
277             loc => 'http://mywebsite.com/image1.jpg',
278             caption => 'Caption 1',
279             title => 'Title 1',
280             license => 'http://www.mozilla.org/MPL/2.0/',
281             geo_location => 'Town, Region',
282             },
283             {
284             loc => 'http://mywebsite.com/image2.jpg',
285             caption => 'Caption 2',
286             title => 'Title 2',
287             license => 'http://www.mozilla.org/MPL/2.0/',
288             geo_location => 'Town, Region',
289             }
290             ],
291             videos => {
292             content_loc => 'http://mywebsite.com/video1.flv',
293             player => {
294             loc => 'http://mywebsite.com/video_player.swf?video=1',
295             allow_embed => "yes",
296             autoplay => "ap=1",
297             },
298             thumbnail_loc => 'http://mywebsite.com/thumbs/1.jpg',
299             title => 'Video Title 1',
300             description => 'Video Description 1',
301             }
302             );
303              
304             # or
305             $map->add(
306             WWW::Sitemap::XML::URL->new(
307             loc => 'http://mywebsite.com/',
308             lastmod => '2010-11-22',
309             changefreq => 'monthly',
310             priority => 1.0,
311             mobile => 1,
312             images => [
313             WWW::Sitemap::XML::Google::Image->new(
314             {
315             loc => 'http://mywebsite.com/image1.jpg',
316             caption => 'Caption 1',
317             title => 'Title 1',
318             license => 'http://www.mozilla.org/MPL/2.0/',
319             geo_location => 'Town, Region',
320             },
321             ),
322             WWW::Sitemap::XML::Google::Image->new(
323             {
324             loc => 'http://mywebsite.com/image2.jpg',
325             caption => 'Caption 2',
326             title => 'Title 2',
327             license => 'http://www.mozilla.org/MPL/2.0/',
328             geo_location => 'Town, Region',
329             }
330             ),
331             ],
332             videos => [
333             WWW::Sitemap::XML::Google::Video->new(
334             content_loc => 'http://mywebsite.com/video1.flv',
335             player => WWW::Sitemap::XML::Google::Video::Player->new(
336             {
337             loc => 'http://mywebsite.com/video_player.swf?video=1',
338             allow_embed => "yes",
339             autoplay => "ap=1",
340             }
341             ),
342             thumbnail_loc => 'http://mywebsite.com/thumbs/1.jpg',
343             title => 'Video Title 1',
344             description => 'Video Description 1',
345             ),
346             ],
347             )
348             );
349              
350             # read URLs from existing sitemap.xml file
351             my @urls = $map->read( location => 'sitemap.xml' );
352              
353             # load urls from existing sitemap.xml file
354             $map->load( location => 'sitemap.xml' );
355              
356             # get XML::LibXML object
357             my $xml = $map->as_xml;
358              
359             print $xml->toString(1);
360              
361             # write to file
362             $map->write( 'sitemap.xml', my $pretty_print = 1 );
363              
364             # write compressed
365             $map->write( 'sitemap.xml.gz' );
366              
367             =head1 DESCRIPTION
368              
369             Read and write sitemap XML files as defined at L<http://www.sitemaps.org/> and
370             with support of Google video, image and mobile extensions described at L<https://support.google.com/webmasters/answer/183668>.
371              
372             =head1 METHODS
373              
374             =head2 add($url|%attrs)
375              
376             $map->add(
377             WWW::Sitemap::XML::URL->new(
378             loc => 'http://mywebsite.com/',
379             lastmod => '2010-11-22',
380             changefreq => 'monthly',
381             priority => 1.0,
382             )
383             );
384              
385             Add the C<$url> object representing single page in the sitemap.
386              
387             Accepts blessed objects implementing L<WWW::Sitemap::XML::URL::Interface>.
388              
389             Otherwise the arguments C<%attrs> are passed as-is to create new
390             L<WWW::Sitemap::XML::URL> object.
391              
392             $map->add(
393             loc => 'http://mywebsite.com/',
394             lastmod => '2010-11-22',
395             changefreq => 'monthly',
396             priority => 1.0,
397             );
398              
399             # single url argument
400             $map->add( 'http://mywebsite.com/' );
401              
402             # is same as
403             $map->add( loc => 'http://mywebsite.com/' );
404              
405             Performs basic validation of URLs added:
406              
407             =over
408              
409             =item * maximum of 50 000 URLs in single sitemap
410              
411             =item * URL no longer then 2048 characters
412              
413             =item * all URLs should use the same protocol and reside on same host
414              
415             =back
416              
417             =head2 urls
418              
419             my @urls = $map->urls;
420              
421             Returns a list of all URL objects added to sitemap.
422              
423             =head2 load(%sitemap_location)
424              
425             $map->load( location => $sitemap_file );
426              
427             It is a shortcut for:
428              
429             $map->add($_) for $map->read( location => $sitemap_file );
430              
431             Please see L<"read"> for details.
432              
433             =head2 read(%sitemap_location)
434              
435             # file or url to sitemap
436             my @urls = $map->read( location => $file_or_url );
437              
438             # file handle
439             my @urls = $map->read( IO => $fh );
440              
441             # XML string
442             my @urls = $map->read( string => $xml );
443              
444             Read the sitemap from file, URL, open file handle or string and return the list of
445             L<WWW::Sitemap::XML::URL> objects representing C<E<lt>urlE<gt>> elements.
446              
447             =head2 write($file, $format = 0)
448              
449             # write to file
450             $map->write( 'sitemap.xml', my $pretty_print = 1);
451              
452             # or
453             my $fh = IO::File->new();
454             $fh->open('sitemap.xml', 'w');
455             $map->write( $fh, my $pretty_print = 1);
456             $cfh->close;
457              
458             # write compressed
459             $map->write( 'sitemap.xml.gz' );
460              
461             Write XML sitemap to C<$file> - a file name or L<IO::Handle> object.
462              
463             If file names ends in C<.gz> then the output file will be compressed by
464             setting compression on XML object - please note that it requires I<libxml2> to
465             be compiled with I<zlib> support.
466              
467             Optional C<$format> is passed to C<toFH> or C<toFile> methods
468             (depending on the type of C<$file>, respectively for file handle and file name)
469             as described in L<XML::LibXML>.
470              
471             =head2 as_xml
472              
473             my $xml = $map->as_xml;
474              
475             # pretty print
476             print $xml->toString(1);
477              
478             # write compressed
479             $xml->setCompression(8);
480             $xml->toFile( 'sitemap.xml.gz' );
481              
482             Returns L<XML::LibXML::Document> object representing the sitemap in XML format.
483              
484             The C<E<lt>urlE<gt>> elements are built by calling I<as_xml> on all URL objects
485             added into sitemap.
486              
487             =head1 SEE ALSO
488              
489             L<WWW::SitemapIndex::XML>
490              
491             L<http://www.sitemaps.org/>
492              
493             L<https://support.google.com/webmasters/answer/183668>
494              
495             =head1 AUTHOR
496              
497             Alex J. G. BurzyÅ„ski <ajgb@cpan.org>
498              
499             =head1 COPYRIGHT AND LICENSE
500              
501             This software is copyright (c) 2014 by Alex J. G. BurzyÅ„ski <ajgb@cpan.org>.
502              
503             This is free software; you can redistribute it and/or modify it under
504             the same terms as the Perl 5 programming language system itself.
505              
506             =cut