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

Revision 424, 15.9 KB checked in by dom, 6 years ago (diff)

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