source: wiki-toolkit/trunk/lib/Wiki/Toolkit/Feed/RSS.pm @ 425

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

fix uninitialized warnings (fixes #28)

  • Property svn:executable set to *
File size: 15.9 KB
Line 
1package Wiki::Toolkit::Feed::RSS;
2
3use strict;
4
5use vars qw( @ISA $VERSION );
6$VERSION = '0.10';
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/)
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 interwiki_identifier make_diff_url make_history_url encoding
45                        software_name software_version software_homepage/)
46    {
47        $self->{$arg} = $args{$arg} || '';
48    }
49
50    # Supply some defaults, if a blank string isn't what we want
51    unless($self->{encoding}) {
52        $self->{encoding} = $self->{wiki}->store->{_charset};
53    }
54
55    $self->{timestamp_fmt} = $Wiki::Toolkit::Store::Database::timestamp_fmt;
56    $self->{utc_offset} = strftime "%z", localtime;
57    $self->{utc_offset} =~ s/(..)(..)$/$1:$2/;
58
59    $self;
60}
61
62=item <build_feed_start>
63
64Internal method, to build all the stuff that will go at the start of a feed.
65Generally will output namespaces, headers and so on.
66
67=cut
68
69sub build_feed_start {
70  my ($self,$feed_timestamp) = @_;
71
72  #"http://purl.org/rss/1.0/modules/wiki/"
73  return qq{<?xml version="1.0" encoding="}. $self->{encoding} .qq{"?>
74
75<rdf:RDF
76 xmlns         = "http://purl.org/rss/1.0/"
77 xmlns:dc      = "http://purl.org/dc/elements/1.1/"
78 xmlns:doap    = "http://usefulinc.com/ns/doap#"
79 xmlns:foaf    = "http://xmlns.com/foaf/0.1/"
80 xmlns:rdf     = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
81 xmlns:rdfs    = "http://www.w3.org/2000/01/rdf-schema#"
82 xmlns:modwiki = "http://www.usemod.com/cgi-bin/mb.pl?ModWiki"
83 xmlns:geo     = "http://www.w3.org/2003/01/geo/wgs84_pos#"
84 xmlns:space   = "http://frot.org/space/0.1/"
85>
86};
87}
88
89=item <build_feed_mid>
90
91Internal method, to build all the stuff (except items) to go inside the channel
92
93=cut
94
95sub build_feed_mid {
96    my ($self,$feed_timestamp) = @_;
97
98    my $rss .= qq{<dc:publisher>} . $self->{site_url} . qq{</dc:publisher>\n};
99
100if ($self->{software_name})
101{
102  $rss .= qq{<foaf:maker>
103  <doap:Project>
104    <doap:name>} . $self->{software_name} . qq{</doap:name>\n};
105}
106
107if ($self->{software_name} && $self->{software_homepage})
108{
109  $rss .= qq{    <doap:homepage rdf:resource="} . $self->{software_homepage} . qq{" />\n};
110}
111
112if ($self->{software_name} && $self->{software_version})
113{
114  $rss .= qq{    <doap:release>
115      <doap:Version>
116        <doap:revision>} . $self->{software_version} . qq{</doap:revision>
117      </doap:Version>
118    </doap:release>\n};
119}
120
121if ($self->{software_name})
122{
123  $rss .= qq{  </doap:Project>
124</foaf:maker>\n};
125}
126
127$feed_timestamp ||= '';
128
129$rss .= qq{<title>}   . $self->{site_name}             . qq{</title>
130<link>}               . $self->{html_equiv_link}       . qq{</link>
131<description>}        . $self->{site_description}      . qq{</description>
132<dc:date>}            . $feed_timestamp                . qq{</dc:date>
133<modwiki:interwiki>}     . $self->{interwiki_identifier} . qq{</modwiki:interwiki>};
134
135   return $rss;
136}
137
138=item <build_feed_end>
139
140Internal method, to build all the stuff that will go at the end of a feed
141
142=cut
143
144sub build_feed_end {
145    my ($self,$feed_timestamp) = @_;
146
147    return "</rdf:RDF>\n";
148}
149
150
151=item <generate_node_list_feed>
152
153Generate and return an RSS feed for a list of nodes
154
155=cut
156
157sub generate_node_list_feed {
158  my ($self,$feed_timestamp,@nodes) = @_;
159
160  # Start our feed
161  my $rss = $self->build_feed_start($feed_timestamp);
162  $rss .= qq{
163
164<channel rdf:about="">
165
166};
167  $rss .= $self->build_feed_mid($feed_timestamp);
168
169  # Generate the items list, and the individiual item entries
170  my (@urls, @items);
171  foreach my $node (@nodes)
172  {
173    my $node_name = $node->{name};
174
175    my $timestamp = $node->{last_modified};
176   
177    # Make a Time::Piece object.
178    my $time = Time::Piece->strptime($timestamp, $self->{timestamp_fmt});
179
180    my $utc_offset = $self->{utc_offset};
181   
182    $timestamp = $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
183
184    my $author      = $node->{metadata}{username}[0] || $node->{metadata}{host}[0] || '';
185    my $description = $node->{metadata}{comment}[0]  || '';
186
187    $description .= " [$author]" if $author;
188
189    my $version = $node->{version};
190    my $status  = (1 == $version) ? 'new' : 'updated';
191
192    my $major_change = $node->{metadata}{major_change}[0];
193       $major_change = 1 unless defined $major_change;
194    my $importance = $major_change ? 'major' : 'minor';
195
196    my $url = $self->{make_node_url}->($node_name, $version);
197
198    push @urls, qq{    <rdf:li rdf:resource="$url" />\n};
199
200    my $diff_url = '';
201   
202    if ($self->{make_diff_url})
203    {
204            $diff_url = $self->{make_diff_url}->($node_name);
205    }
206
207    my $history_url = '';
208   
209    if ($self->{make_history_url})
210    {
211      $history_url = $self->{make_history_url}->($node_name);
212    }
213
214    my $node_url = $self->{make_node_url}->($node_name);
215
216    my $rdf_url =  $node_url;
217       $rdf_url =~ s/\?/\?id=/;
218       $rdf_url .= ';format=rdf';
219
220    # make XML-clean
221    my $title =  $node_name;
222       $title =~ s/&/&amp;/g;
223       $title =~ s/</&lt;/g;
224       $title =~ s/>/&gt;/g;
225
226    # Pop the categories into dublin core subject elements
227    #  (http://dublincore.org/usage/terms/history/#subject-004)
228    # TODO: Decide if we should include the "all categories listing" url
229    #        as the scheme (URI) attribute?
230    my $category_rss = "";
231    if($node->{metadata}->{category}) {
232        foreach my $cat (@{ $node->{metadata}->{category} }) {
233            $category_rss .= "  <dc:subject>$cat</dc:subject>\n";
234        }
235    }
236
237    # Include geospacial data, if we have it
238    my $geo_rss = $self->format_geo($node->{metadata});
239
240    push @items, qq{
241<item rdf:about="$url">
242  <title>$title</title>
243  <link>$url</link>
244  <description>$description</description>
245  <dc:date>$timestamp</dc:date>
246  <dc:contributor>$author</dc:contributor>
247  <modwiki:status>$status</modwiki:status>
248  <modwiki:importance>$importance</modwiki:importance>
249  <modwiki:diff>$diff_url</modwiki:diff>
250  <modwiki:version>$version</modwiki:version>
251  <modwiki:history>$history_url</modwiki:history>
252  <rdfs:seeAlso rdf:resource="$rdf_url" />
253$category_rss
254$geo_rss
255</item>
256};
257  }
258 
259  # Output the items list
260  $rss .= qq{
261
262<items>
263  <rdf:Seq>
264} . join('', @urls) . qq{  </rdf:Seq>
265</items>
266
267</channel>
268};
269
270  # Output the individual item entries
271  $rss .= join('', @items) . "\n";
272
273  # Finish up
274  $rss .= $self->build_feed_end($feed_timestamp);
275 
276  return $rss;   
277}
278
279
280=item B<generate_node_name_distance_feed>
281
282Generate a very cut down rss feed, based just on the nodes, their locations
283(if given), and their distance from a reference location (if given).
284
285Typically used on search feeds.
286
287=cut
288
289sub generate_node_name_distance_feed {
290  my ($self,$feed_timestamp,@nodes) = @_;
291
292  # Start our feed
293  my $rss = $self->build_feed_start($feed_timestamp);
294  $rss .= qq{
295
296<channel rdf:about="">
297
298};
299  $rss .= $self->build_feed_mid($feed_timestamp);
300
301  # Generate the items list, and the individiual item entries
302  my (@urls, @items);
303  foreach my $node (@nodes)
304  {
305    my $node_name = $node->{name};
306
307    my $url = $self->{make_node_url}->($node_name);
308
309    push @urls, qq{    <rdf:li rdf:resource="$url" />\n};
310
311    my $rdf_url =  $url;
312       $rdf_url =~ s/\?/\?id=/;
313       $rdf_url .= ';format=rdf';
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    # What location stuff do we have?
322    my $geo_rss = $self->format_geo($node);
323
324    push @items, qq{
325<item rdf:about="$url">
326  <title>$title</title>
327  <link>$url</link>
328  <rdfs:seeAlso rdf:resource="$rdf_url" />
329$geo_rss
330</item>
331};
332  }
333 
334  # Output the items list
335  $rss .= qq{
336
337<items>
338  <rdf:Seq>
339} . join('', @urls) . qq{  </rdf:Seq>
340</items>
341
342</channel>
343};
344
345  # Output the individual item entries
346  $rss .= join('', @items) . "\n";
347
348  # Finish up
349  $rss .= $self->build_feed_end($feed_timestamp);
350 
351  return $rss;   
352}
353
354=item B<feed_timestamp>
355
356Generate the timestamp for the RSS, based on the newest node (if available).
357Will return a timestamp for now if no node dates are available
358
359=cut
360
361sub feed_timestamp {
362    my ($self, $newest_node) = @_;
363
364    my $time;
365    if ($newest_node->{last_modified})
366    {
367        $time = Time::Piece->strptime( $newest_node->{last_modified}, $self->{timestamp_fmt} );
368    } else {
369        $time = localtime;
370    }
371
372    my $utc_offset = $self->{utc_offset};
373
374    return $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
375}
376
377# Compatibility method - use feed_timestamp with a node instead
378sub rss_timestamp {
379    my ($self, %args) = @_;
380
381    warn("Old style method used - please convert to calling feed_timestamp with a node!");
382    my $feed_timestamp = $self->feed_timestamp(
383                              $self->fetch_newest_for_recently_changed(%args)
384    );
385    return $feed_timestamp;
386}
387
388=item B<parse_feed_timestamp>
389
390Take a feed_timestamp and return a Time::Piece object.
391
392=cut
393
394sub parse_feed_timestamp {
395    my ($self, $feed_timestamp) = @_;
396   
397    $feed_timestamp = substr($feed_timestamp, 0, -length( $self->{utc_offset}));
398    return Time::Piece->strptime( $feed_timestamp, '%Y-%m-%dT%H:%M:%S' );
399}
400
4011;
402
403__END__
404
405=head1 NAME
406
407  Wiki::Toolkit::Feed::RSS - Output RecentChanges RSS for Wiki::Toolkit.
408
409=head1 DESCRIPTION
410
411This is an alternative access to the recent changes of a Wiki::Toolkit
412wiki. It outputs RSS as described by the ModWiki proposal at
413L<http://www.usemod.com/cgi-bin/mb.pl?ModWiki>
414
415=head1 SYNOPSIS
416
417  use Wiki::Toolkit;
418  use Wiki::Toolkit::Feed::RSS;
419
420  my $wiki = CGI::Wiki->new( ... );  # See perldoc Wiki::Toolkit
421
422  # Set up the RSS feeder with the mandatory arguments - see
423  # C<new()> below for more, optional, arguments.
424  my $rss = Wiki::Toolkit::Feed::RSS->new(
425    wiki                => $wiki,
426    site_name           => 'My Wiki',
427    site_url            => 'http://example.com/',
428    make_node_url       => sub
429                           {
430                             my ($node_name, $version) = @_;
431                             return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
432                           },
433    html_equiv_link     => 'http://example.com/?RecentChanges',
434    encoding            => 'UTF-8'
435  );
436
437  print "Content-type: application/xml\n\n";
438  print $rss->recent_changes;
439
440=head1 METHODS
441
442=head2 C<new()>
443
444  my $rss = Wiki::Toolkit::Feed::RSS->new(
445    # Mandatory arguments:
446    wiki                 => $wiki,
447    site_name            => 'My Wiki',
448    site_url             => 'http://example.com/',
449    make_node_url        => sub
450                            {
451                              my ($node_name, $version) = @_;
452                              return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
453                            },
454    html_equiv_link  => 'http://example.com/?RecentChanges',
455
456    # Optional arguments:
457    site_description     => 'My wiki about my stuff',
458    interwiki_identifier => 'MyWiki',
459    make_diff_url        => sub
460                            {
461                              my $node_name = shift;
462                              return 'http://example.com/?diff=' . uri_escape($node_name)
463                            },
464    make_history_url     => sub
465                            {
466                              my $node_name = shift;
467                              return 'http://example.com/?hist=' . uri_escape($node_name)
468                            },
469    software_name        => $your_software_name,     # e.g. "CGI::Wiki"
470    software_version     => $your_software_version,  # e.g. "0.73"
471    software_homepage    => $your_software_homepage, # e.g. "http://search.cpan.org/dist/Wiki-Toolkit/"
472  );
473
474C<wiki> must be a L<Wiki::Toolkit> object. C<make_node_url>, and
475C<make_diff_url> and C<make_history_url>, if supplied, must be coderefs.
476
477The mandatory arguments are:
478
479=over 4
480
481=item * wiki
482
483=item * site_name
484
485=item * site_url
486
487=item * make_node_url
488
489=item * html_equiv_link or recent_changes_link
490
491=back
492
493The three optional arguments
494
495=over 4
496
497=item * software_name
498
499=item * software_version
500
501=item * software_homepage
502
503=back
504
505are used to generate DOAP (Description Of A Project - see L<http://usefulinc.com/doap>) metadata
506for the feed to show what generated it.
507
508The optional argument
509
510=over 4
511
512=item * encoding
513
514=back
515
516will be used to specify the character encoding in the feed. If not set,
517will default to the wiki store's encoding.
518
519=head2 C<recent_changes()>
520
521  $wiki->write_node(
522                     'About This Wiki',
523                     'blah blah blah',
524                                 $checksum,
525                           {
526                       comment  => 'Stub page, please update!',
527                                   username => 'Fred',
528                     }
529  );
530
531  print "Content-type: application/xml\n\n";
532  print $rss->recent_changes;
533
534  # Or get something other than the default of the latest 15 changes.
535  print $rss->recent_changes( items => 50 );
536  print $rss->recent_changes( days => 7 );
537
538  # Or ignore minor edits.
539  print $rss->recent_changes( ignore_minor_edits => 1 );
540
541  # Personalise your feed further - consider only changes
542  # made by Fred to pages about bookshops.
543  print $rss->recent_changes(
544             filter_on_metadata => {
545                         username => 'Fred',
546                         category => 'Bookshops',
547                       },
548              );
549
550If using C<filter_on_metadata>, note that only changes satisfying
551I<all> criteria will be returned.
552
553B<Note:> Many of the fields emitted by the RSS generator are taken
554from the node metadata. The form of this metadata is I<not> mandated
555by L<Wiki::Toolkit>. Your wiki application should make sure to store some or
556all of the following metadata when calling C<write_node>:
557
558=over 4
559
560=item B<comment> - a brief comment summarising the edit that has just been made; will be used in the RDF description for this item.  Defaults to the empty string.
561
562=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 the empty string.
563
564=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 Dublin Core contributor for this item.  Defaults to the empty string.
565
566=item B<major_change> - true if the edit was a major edit and false if it was a minor edit; used for the importance of the item.  Defaults to true (ie if C<major_change> was not defined or was explicitly stored as C<undef>).
567
568=back
569
570=head2 C<feed_timestamp()>
571
572  print $rss->feed_timestamp();
573
574Returns the timestamp of the feed in POSIX::strftime style ("Tue, 29 Feb 2000
57512:34:56 GMT"), which is equivalent to the timestamp of the most recent item
576in the feed. Takes the same arguments as recent_changes(). You will most likely
577need this to print a Last-Modified HTTP header so user-agents can determine
578whether they need to reload the feed or not.
579 
580=head1 SEE ALSO
581
582=over 4
583
584=item * L<Wiki::Toolkit>
585
586=item * L<http://web.resource.org/rss/1.0/spec>
587
588=item * L<http://www.usemod.com/cgi-bin/mb.pl?ModWiki>
589
590=back
591
592=head1 MAINTAINER
593
594The Wiki::Toolkit project. Originally by Kake Pugh <kake@earth.li>.
595
596=head1 COPYRIGHT AND LICENSE
597
598Copyright 2003-4 Kake Pugh.
599Copyright 2005 Earle Martin.
600Copyright 2006 the Wiki::Toolkit team
601
602This module is free software; you can redistribute it and/or modify it
603under the same terms as Perl itself.
604
605=head1 THANKS
606
607The members of the Semantic Web Interest Group channel on irc.freenode.net,
608#swig, were very useful in the development of this module.
609
610=cut
Note: See TracBrowser for help on using the repository browser.