root/wiki-toolkit/trunk/lib/Wiki/Toolkit/Feed/Atom.pm @ 529

Revision 529, 13.1 KB (checked in by kake, 13 months ago)

Fixed deprecated use of foreach ... qw (fixes #53 and #54).

  • Property svn:executable set to *
Line 
1package Wiki::Toolkit::Feed::Atom;
2
3use strict;
4
5use vars qw( @ISA $VERSION );
6$VERSION = '0.02';
7
8use POSIX 'strftime';
9use Time::Piece;
10use URI::Escape;
11use Carp qw( croak );
12
13use Wiki::Toolkit::Feed::Listing;
14@ISA = qw( Wiki::Toolkit::Feed::Listing );
15
16=head1 NAME
17
18  Wiki::Toolkit::Feed::Atom - A Wiki::Toolkit plugin to output RecentChanges Atom.
19
20=head1 DESCRIPTION
21
22This is an alternative access to the recent changes of a Wiki::Toolkit
23wiki. It outputs the Atom Syndication Format as described at
24L<http://www.atomenabled.org/developers/syndication/>.
25
26This module is a straight port of L<Wiki::Toolkit::Feed::RSS>.
27
28=head1 SYNOPSIS
29
30  use Wiki::Toolkit;
31  use Wiki::Toolkit::Feed::Atom;
32
33  my $wiki = Wiki::Toolkit->new( ... );  # See perldoc Wiki::Toolkit
34
35  # Set up the RSS feeder with the mandatory arguments - see
36  # C<new()> below for more, optional, arguments.
37  my $atom = Wiki::Toolkit::Feed::Atom->new(
38    wiki                => $wiki,
39    site_name           => 'My Wiki',
40    site_url            => 'http://example.com/',
41    make_node_url       => sub
42                           {
43                             my ($node_name, $version) = @_;
44                             return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
45                           },
46    html_equiv_link => 'http://example.com/?RecentChanges',
47    atom_link => 'http://example.com/?action=rc;format=atom',
48  );
49
50  print "Content-type: application/atom+xml\n\n";
51  print $atom->recent_changes;
52
53=head1 METHODS
54
55=head2 C<new()>
56
57  my $atom = Wiki::Toolkit::Feed::Atom->new(
58    # Mandatory arguments:
59    wiki                 => $wiki,
60    site_name            => 'My Wiki',
61    site_url             => 'http://example.com/',
62    make_node_url        => sub
63                            {
64                              my ($node_name, $version) = @_;
65                              return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
66                            },
67    html_equiv_link  => 'http://example.com/?RecentChanges',,
68    atom_link => 'http://example.com/?action=rc;format=atom',
69
70    # Optional arguments:
71    site_description     => 'My wiki about my stuff',
72    software_name        => $your_software_name,     # e.g. "Wiki::Toolkit"
73    software_version     => $your_software_version,  # e.g. "0.73"
74    software_homepage    => $your_software_homepage, # e.g. "http://search.cpan.org/dist/CGI-Wiki/"
75    encoding             => 'UTF-8'
76  );
77
78C<wiki> must be a L<Wiki::Toolkit> object. C<make_node_url>, if supplied, must
79be a coderef.
80
81The mandatory arguments are:
82
83=over 4
84
85=item * wiki
86
87=item * site_name
88
89=item * site_url
90
91=item * make_node_url
92
93=item * html_equiv_link or recent_changes_link
94
95=item * atom_link
96
97=back
98
99The three optional arguments
100
101=over 4
102
103=item * software_name
104
105=item * software_version
106
107=item * software_homepage
108
109=back
110
111are used to generate the C<generator> part of the feed.
112
113The optional argument
114
115=over 4
116
117=item * encoding
118
119=back
120
121will be used to specify the character encoding in the feed. If not set,
122will default to the wiki store's encoding.
123
124=head2 C<recent_changes()>
125
126  $wiki->write_node(
127                     'About This Wiki',
128                     'blah blah blah',
129                         $checksum,
130                         {
131                           comment  => 'Stub page, please update!',
132                           username => 'Fred',
133                         }
134  );
135
136  print "Content-type: application/atom+xml\n\n";
137  print $atom->recent_changes;
138
139  # Or get something other than the default of the latest 15 changes.
140  print $atom->recent_changes( items => 50 );
141  print $atom->recent_changes( days => 7 );
142
143  # Or ignore minor edits.
144  print $atom->recent_changes( ignore_minor_edits => 1 );
145
146  # Personalise your feed further - consider only changes
147  # made by Fred to pages about bookshops.
148  print $atom->recent_changes(
149             filter_on_metadata => {
150                         username => 'Fred',
151                         category => 'Bookshops',
152                       },
153              );
154
155If using C<filter_on_metadata>, note that only changes satisfying
156I<all> criteria will be returned.
157
158B<Note:> Many of the fields emitted by the Atom generator are taken
159from the node metadata. The form of this metadata is I<not> mandated
160by L<Wiki::Toolkit>. Your wiki application should make sure to store some or
161all of the following metadata when calling C<write_node>:
162
163=over 4
164
165=item B<comment> - a brief comment summarising the edit that has just been made; will be used in the summary for this item.  Defaults to the empty string.
166
167=item B<username> - an identifier for the person who made the edit; will be used as the Dublin Core contributor for this item, and also in the RDF description.  Defaults to 'No description given for change'.
168
169=item B<host> - the hostname or IP address of the computer used to make the edit; if no username is supplied then this will be used as the author for this item.  Defaults to 'Anonymous'.
170
171=back
172
173=cut
174
175sub new {
176    my $class = shift;
177    my $self  = {};
178    bless $self, $class;
179
180    my %args = @_;
181    my $wiki = $args{wiki};
182
183    unless ($wiki && UNIVERSAL::isa($wiki, 'Wiki::Toolkit')) {
184        croak 'No Wiki::Toolkit object supplied';
185    }
186 
187    $self->{wiki} = $wiki;
188 
189    # Mandatory arguments.
190    foreach my $arg (qw/site_name site_url make_node_url atom_link/) {
191        croak "No $arg supplied" unless $args{$arg};
192        $self->{$arg} = $args{$arg};
193    }
194
195    # Must-supply-one-of arguments
196    my %mustoneof = ( 'html_equiv_link' => ['html_equiv_link','recent_changes_link'] );
197    $self->handle_supply_one_of(\%mustoneof,\%args);
198 
199    # Optional arguments.
200    foreach my $arg (qw/site_description software_name software_version software_homepage encoding/) {
201        $self->{$arg} = $args{$arg} || '';
202    }
203
204    # Supply some defaults, if a blank string isn't what we want
205    unless($self->{encoding}) {
206        $self->{encoding} = $self->{wiki}->store->{_charset};
207    }
208
209    $self->{timestamp_fmt} = $Wiki::Toolkit::Store::Database::timestamp_fmt;
210    $self->{utc_offset} = strftime "%z", localtime;
211    $self->{utc_offset} =~ s/(..)(..)$/$1:$2/;
212 
213    # Escape any &'s in the urls
214    foreach my $key (qw(site_url atom_link)) {
215        my @ands = ($self->{$key} =~ /(\&.{1,6})/g);
216        foreach my $and (@ands) {
217            if($and ne "&amp;") {
218                my $new_and = $and;
219                $new_and =~ s/\&/\&amp;/;
220                $self->{$key} =~ s/$and/$new_and/;
221            }
222        }
223    }
224
225    $self;
226}
227
228# Internal method, to build all the stuff that will go at the start of a feed.
229# Outputs the feed header, and initial feed info.
230
231sub build_feed_start {
232    my ($self,$atom_timestamp) = @_;
233
234    my $generator = '';
235 
236    if ($self->{software_name}) {
237        $generator  = '  <generator';
238        $generator .= ' uri="' . $self->{software_homepage} . '"'   if $self->{software_homepage};
239        $generator .= ' version=' . $self->{software_version} . '"' if $self->{software_version};
240        $generator .= ">\n";
241        $generator .= $self->{software_name} . "</generator>\n";
242    }                         
243
244    my $subtitle = $self->{site_description}
245                 ? '<subtitle>' . $self->{site_description} . "</subtitle>\n"
246                 : '';
247
248    $atom_timestamp ||= '';
249
250    my $atom = qq{<?xml version="1.0" encoding="} . $self->{encoding} . qq{"?>
251
252<feed
253 xmlns         = "http://www.w3.org/2005/Atom"
254 xmlns:geo     = "http://www.w3.org/2003/01/geo/wgs84_pos#"
255 xmlns:space   = "http://frot.org/space/0.1/"
256>
257
258  <link href="}            . $self->{site_url}     . qq{" />
259  <title>}                 . $self->{site_name}    . qq{</title>
260  <link rel="self" href="} . $self->{atom_link}    . qq{" />
261  <updated>}               . $atom_timestamp       . qq{</updated>
262  <id>}                    . $self->{site_url}     . qq{</id>
263  $subtitle};
264 
265    return $atom;
266}
267
268# Internal method, to build all the stuff that will go at the end of a feed.
269
270sub build_feed_end {
271    my ($self,$feed_timestamp) = @_;
272
273    return "</feed>\n";
274}
275
276=head2 C<generate_node_list_feed>
277 
278Generate and return an Atom feed for a list of nodes
279 
280=cut
281
282sub generate_node_list_feed {
283    my ($self,$atom_timestamp,@nodes) = @_;
284
285    my $atom = $self->build_feed_start($atom_timestamp);
286
287    my (@urls, @items);
288
289    foreach my $node (@nodes) {
290        my $node_name = $node->{name};
291
292        my $item_timestamp = $node->{last_modified};
293   
294        # Make a Time::Piece object.
295        my $time = Time::Piece->strptime($item_timestamp, $self->{timestamp_fmt});
296
297        my $utc_offset = $self->{utc_offset};
298   
299        $item_timestamp = $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
300
301        my $author      = $node->{metadata}{username}[0] || $node->{metadata}{host}[0] || 'Anonymous';
302        my $description = $node->{metadata}{comment}[0]  || 'No description given for node';
303
304        $description .= " [$author]" if $author;
305
306        my $version = $node->{version};
307        my $status  = (1 == $version) ? 'new' : 'updated';
308
309        my $major_change = $node->{metadata}{major_change}[0];
310        $major_change = 1 unless defined $major_change;
311        my $importance = $major_change ? 'major' : 'minor';
312
313        my $url = $self->{make_node_url}->($node_name, $version);
314
315        # make XML-clean
316        my $title =  $node_name;
317        $title =~ s/&/&amp;/g;
318        $title =~ s/</&lt;/g;
319        $title =~ s/>/&gt;/g;
320
321        # Pop the categories into atom:category elements (4.2.2)
322        # We can do this because the spec says:
323        #   "This specification assigns no meaning to the content (if any)
324        #    of this element."
325        # TODO: Decide if we should include the "all categories listing" url
326        #        as the scheme (URI) attribute?
327        my $category_atom = "";
328        if ($node->{metadata}->{category}) {
329            foreach my $cat (@{ $node->{metadata}->{category} }) {
330                $category_atom .= "    <category term=\"$cat\" />\n";
331            }
332        }
333
334        # Include geospacial data, if we have it
335        my $geo_atom = $self->format_geo($node->{metadata});
336
337        # TODO: Find an Atom equivalent of ModWiki, so we can include more info
338
339   
340        push @items, qq{
341  <entry>
342    <title>$title</title>
343    <link href="$url" />
344    <id>$url</id>
345    <summary>$description</summary>
346    <updated>$item_timestamp</updated>
347    <author><name>$author</name></author>
348$category_atom
349$geo_atom
350  </entry>
351};
352
353    }
354 
355    $atom .= join('', @items) . "\n";
356    $atom .= $self->build_feed_end($atom_timestamp);
357
358    return $atom;   
359}
360
361=head2 C<generate_node_name_distance_feed>
362 
363Generate a very cut down atom feed, based just on the nodes, their locations
364(if given), and their distance from a reference location (if given).
365
366Typically used on search feeds.
367 
368=cut
369
370sub generate_node_name_distance_feed {
371    my ($self,$atom_timestamp,@nodes) = @_;
372
373    my $atom = $self->build_feed_start($atom_timestamp);
374
375    my (@urls, @items);
376
377    foreach my $node (@nodes) {
378        my $node_name = $node->{name};
379
380        my $url = $self->{make_node_url}->($node_name);
381
382        # make XML-clean
383        my $title =  $node_name;
384        $title =~ s/&/&amp;/g;
385        $title =~ s/</&lt;/g;
386        $title =~ s/>/&gt;/g;
387
388        # What location stuff do we have?
389        my $geo_atom = $self->format_geo($node);
390
391        push @items, qq{
392  <entry>
393    <title>$title</title>
394    <link href="$url" />
395    <id>$url</id>
396$geo_atom
397  </entry>
398};
399
400    }
401 
402    $atom .= join('', @items) . "\n";
403    $atom .= $self->build_feed_end($atom_timestamp);
404
405    return $atom;   
406}
407
408=head2 C<feed_timestamp()>
409
410  print $atom->feed_timestamp();
411
412Returns the timestamp of the feed in POSIX::strftime style ("Tue, 29 Feb 2000
41312:34:56 GMT"), which is equivalent to the timestamp of the most recent item
414in the feed. Takes the same arguments as recent_changes(). You will most likely
415need this to print a Last-Modified HTTP header so user-agents can determine
416whether they need to reload the feed or not.
417
418=cut
419
420sub feed_timestamp {
421    my ($self, $newest_node) = @_;
422 
423    my $time;
424    if ($newest_node->{last_modified}) {
425        $time = Time::Piece->strptime( $newest_node->{last_modified}, $self->{timestamp_fmt} );
426    } else {
427        $time = localtime;
428    }
429
430    my $utc_offset = $self->{utc_offset};
431   
432    return $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
433}
434
435
436=head2 C<parse_feed_timestamp>
437
438Take a feed_timestamp and return a Time::Piece object.
439
440=cut
441
442sub parse_feed_timestamp {
443    my ($self, $feed_timestamp) = @_;
444   
445    $feed_timestamp = substr($feed_timestamp, 0, -length( $self->{utc_offset}));
446    return Time::Piece->strptime( $feed_timestamp, '%Y-%m-%dT%H:%M:%S' );
447}
4481;
449
450__END__
451
452
453=head1 SEE ALSO
454
455=over 4
456
457=item * L<Wiki::Toolkit>
458
459=item * L<http://www.atomenabled.org/developers/syndication/>
460
461=back
462
463=head1 MAINTAINER
464
465The Wiki::Toolkit team, http://www.wiki-toolkit.org/.
466
467=head1 COPYRIGHT AND LICENSE
468
469Copyright 2006-2009 Earle Martin and the Wiki::Toolkit team.
470
471This module is free software; you can redistribute it and/or modify it
472under the same terms as Perl itself.
473
474=head1 THANKS
475
476Kake Pugh for originally writing Wiki::Toolkit::Feed::RSS and indeed
477Wiki::Toolkit itself.
478
479=cut
Note: See TracBrowser for help on using the browser.