source: wiki-toolkit/trunk/lib/Wiki/Toolkit/Feed/Atom.pm @ 424

Last change on this file since 424 was 424, checked in by Dominic Hargreaves, 14 years ago

whitespace-only change to fix some POD bugs and generally make things read
more nicely. Tab-damage fixing still todo...

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