package Wiki::Toolkit::Feed::Atom;
use strict;
use vars qw( @ISA $VERSION );
$VERSION = '0.03';
use POSIX 'strftime';
use Time::Piece;
use URI::Escape;
use Carp qw( croak );
use Wiki::Toolkit::Feed::Listing;
@ISA = qw( Wiki::Toolkit::Feed::Listing );
=head1 NAME
Wiki::Toolkit::Feed::Atom - A Wiki::Toolkit plugin to output RecentChanges Atom.
=head1 DESCRIPTION
This is an alternative access to the recent changes of a Wiki::Toolkit
wiki. It outputs the Atom Syndication Format as described at
L<http://www.atomenabled.org/developers/syndication/>.
This module is a straight port of L<Wiki::Toolkit::Feed::RSS>.
=head1 SYNOPSIS
use Wiki::Toolkit;
use Wiki::Toolkit::Feed::Atom;
my $wiki = Wiki::Toolkit->new( ... ); # See perldoc Wiki::Toolkit
# Set up the RSS feeder with the mandatory arguments - see
# C<new()> below for more, optional, arguments.
my $atom = Wiki::Toolkit::Feed::Atom->new(
wiki => $wiki,
site_name => 'My Wiki',
site_url => 'http://example.com/',
make_node_url => sub
{
my ($node_name, $version) = @_;
return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
},
html_equiv_link => 'http://example.com/?RecentChanges',
atom_link => 'http://example.com/?action=rc;format=atom',
);
print "Content-type: application/atom+xml\n\n";
print $atom->recent_changes;
=head1 METHODS
=head2 C<new()>
my $atom = Wiki::Toolkit::Feed::Atom->new(
# Mandatory arguments:
wiki => $wiki,
site_name => 'My Wiki',
site_url => 'http://example.com/',
make_node_url => sub
{
my ($node_name, $version) = @_;
return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
},
html_equiv_link => 'http://example.com/?RecentChanges',,
atom_link => 'http://example.com/?action=rc;format=atom',
# Optional arguments:
site_description => 'My wiki about my stuff',
software_name => $your_software_name, # e.g. "Wiki::Toolkit"
software_version => $your_software_version, # e.g. "0.73"
software_homepage => $your_software_homepage, # e.g. "http://search.cpan.org/dist/CGI-Wiki/"
encoding => 'UTF-8'
);
C<wiki> must be a L<Wiki::Toolkit> object. C<make_node_url>, if supplied, must
be a coderef.
The mandatory arguments are:
=over 4
=item * wiki
=item * site_name
=item * site_url
=item * make_node_url
=item * html_equiv_link or recent_changes_link
=item * atom_link
=back
The three optional arguments
=over 4
=item * software_name
=item * software_version
=item * software_homepage
=back
are used to generate the C<generator> part of the feed.
The optional argument
=over 4
=item * encoding
=back
will be used to specify the character encoding in the feed. If not set,
will default to the wiki store's encoding.
=head2 C<recent_changes()>
$wiki->write_node(
'About This Wiki',
'blah blah blah',
$checksum,
{
comment => 'Stub page, please update!',
username => 'Fred',
}
);
print "Content-type: application/atom+xml\n\n";
print $atom->recent_changes;
# Or get something other than the default of the latest 15 changes.
print $atom->recent_changes( items => 50 );
print $atom->recent_changes( days => 7 );
# Or ignore minor edits.
print $atom->recent_changes( ignore_minor_edits => 1 );
# Personalise your feed further - consider only changes
# made by Fred to pages about bookshops.
print $atom->recent_changes(
filter_on_metadata => {
username => 'Fred',
category => 'Bookshops',
},
);
If using C<filter_on_metadata>, note that only changes satisfying
I<all> criteria will be returned.
B<Note:> Many of the fields emitted by the Atom generator are taken
from the node metadata. The form of this metadata is I<not> mandated
by L<Wiki::Toolkit>. Your wiki application should make sure to store some or
all of the following metadata when calling C<write_node>:
=over 4
=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.
=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'.
=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'.
=back
=cut
sub new {
my $class = shift;
my $self = {};
bless $self, $class;
my %args = @_;
my $wiki = $args{wiki};
unless ($wiki && UNIVERSAL::isa($wiki, 'Wiki::Toolkit')) {
croak 'No Wiki::Toolkit object supplied';
}
$self->{wiki} = $wiki;
# Mandatory arguments.
foreach my $arg (qw/site_name site_url make_node_url atom_link/) {
croak "No $arg supplied" unless $args{$arg};
$self->{$arg} = $args{$arg};
}
# Must-supply-one-of arguments
my %mustoneof = ( 'html_equiv_link' => ['html_equiv_link','recent_changes_link'] );
$self->handle_supply_one_of(\%mustoneof,\%args);
# Optional arguments.
foreach my $arg (qw/site_description software_name software_version software_homepage encoding/) {
$self->{$arg} = $args{$arg} || '';
}
# Supply some defaults, if a blank string isn't what we want
unless($self->{encoding}) {
$self->{encoding} = $self->{wiki}->store->{_charset};
}
$self->{timestamp_fmt} = $Wiki::Toolkit::Store::Database::timestamp_fmt;
$self->{utc_offset} = strftime "%z", localtime;
$self->{utc_offset} =~ s/(..)(..)$/$1:$2/;
# Escape any &'s in the urls
foreach my $key (qw(site_url atom_link)) {
my @ands = ($self->{$key} =~ /(\&.{1,6})/g);
foreach my $and (@ands) {
if($and ne "&") {
my $new_and = $and;
$new_and =~ s/\&/\&/;
$self->{$key} =~ s/$and/$new_and/;
}
}
}
$self;
}
# Internal method, to build all the stuff that will go at the start of a feed.
# Outputs the feed header, and initial feed info.
sub build_feed_start {
my ($self,$atom_timestamp) = @_;
my $generator = '';
if ($self->{software_name}) {
$generator = ' <generator';
$generator .= ' uri="' . $self->{software_homepage} . '"' if $self->{software_homepage};
$generator .= ' version=' . $self->{software_version} . '"' if $self->{software_version};
$generator .= ">\n";
$generator .= $self->{software_name} . "</generator>\n";
}
my $subtitle = $self->{site_description}
? '<subtitle>' . $self->{site_description} . "</subtitle>\n"
: '';
$atom_timestamp ||= '';
my $atom = qq{<?xml version="1.0" encoding="} . $self->{encoding} . qq{"?>
<feed
xmlns = "http://www.w3.org/2005/Atom"
xmlns:geo = "http://www.w3.org/2003/01/geo/wgs84_pos#"
xmlns:space = "http://frot.org/space/0.1/"
>
<link href="} . $self->{site_url} . qq{" />
<title>} . $self->{site_name} . qq{</title>
<link rel="self" href="} . $self->{atom_link} . qq{" />
<updated>} . $atom_timestamp . qq{</updated>
<id>} . $self->{site_url} . qq{</id>
$subtitle};
return $atom;
}
# Internal method, to build all the stuff that will go at the end of a feed.
sub build_feed_end {
my ($self,$feed_timestamp) = @_;
return "</feed>\n";
}
=head2 C<generate_node_list_feed>
Generate and return an Atom feed for a list of nodes
=cut
sub generate_node_list_feed {
my ($self,$atom_timestamp,@nodes) = @_;
my $atom = $self->build_feed_start($atom_timestamp);
my (@urls, @items);
foreach my $node (@nodes) {
my $node_name = $node->{name};
my $item_timestamp = $node->{last_modified};
# Make a Time::Piece object.
my $time = Time::Piece->strptime($item_timestamp, $self->{timestamp_fmt});
my $utc_offset = $self->{utc_offset};
$item_timestamp = $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
my $author = $node->{metadata}{username}[0] || $node->{metadata}{host}[0] || 'Anonymous';
my $description = $node->{metadata}{comment}[0] || 'No description given for node';
$description .= " [$author]" if $author;
my $version = $node->{version};
my $status = (1 == $version) ? 'new' : 'updated';
my $major_change = $node->{metadata}{major_change}[0];
$major_change = 1 unless defined $major_change;
my $importance = $major_change ? 'major' : 'minor';
my $url = $self->{make_node_url}->($node_name, $version);
# make XML-clean
my $title = $node_name;
$title =~ s/&/&/g;
$title =~ s/</</g;
$title =~ s/>/>/g;
# Pop the categories into atom:category elements (4.2.2)
# We can do this because the spec says:
# "This specification assigns no meaning to the content (if any)
# of this element."
# TODO: Decide if we should include the "all categories listing" url
# as the scheme (URI) attribute?
my $category_atom = "";
if ($node->{metadata}->{category}) {
foreach my $cat (@{ $node->{metadata}->{category} }) {
$category_atom .= " <category term=\"$cat\" />\n";
}
}
# Include geospacial data, if we have it
my $geo_atom = $self->format_geo($node->{metadata});
# TODO: Find an Atom equivalent of ModWiki, so we can include more info
push @items, qq{
<entry>
<title>$title</title>
<link href="$url" />
<id>$url</id>
<summary>$description</summary>
<updated>$item_timestamp</updated>
<author><name>$author</name></author>
$category_atom
$geo_atom
</entry>
};
}
$atom .= join('', @items) . "\n";
$atom .= $self->build_feed_end($atom_timestamp);
return $atom;
}
=head2 C<generate_node_name_distance_feed>
Generate a very cut down atom feed, based just on the nodes, their locations
(if given), and their distance from a reference location (if given).
Typically used on search feeds.
=cut
sub generate_node_name_distance_feed {
my ($self,$atom_timestamp,@nodes) = @_;
my $atom = $self->build_feed_start($atom_timestamp);
my (@urls, @items);
foreach my $node (@nodes) {
my $node_name = $node->{name};
my $url = $self->{make_node_url}->($node_name);
# make XML-clean
my $title = $node_name;
$title =~ s/&/&/g;
$title =~ s/</</g;
$title =~ s/>/>/g;
# What location stuff do we have?
my $geo_atom = $self->format_geo($node);
push @items, qq{
<entry>
<title>$title</title>
<link href="$url" />
<id>$url</id>
$geo_atom
</entry>
};
}
$atom .= join('', @items) . "\n";
$atom .= $self->build_feed_end($atom_timestamp);
return $atom;
}
=head2 C<feed_timestamp()>
print $atom->feed_timestamp();
Returns the timestamp of the feed in POSIX::strftime style ("Tue, 29 Feb 2000
12:34:56 GMT"), which is equivalent to the timestamp of the most recent item
in the feed. Takes the same arguments as recent_changes(). You will most likely
need this to print a Last-Modified HTTP header so user-agents can determine
whether they need to reload the feed or not.
=cut
sub feed_timestamp {
my ($self, $newest_node) = @_;
my $time;
if ($newest_node->{last_modified}) {
$time = Time::Piece->strptime( $newest_node->{last_modified}, $self->{timestamp_fmt} );
} else {
$time = localtime;
}
my $utc_offset = $self->{utc_offset};
return $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
}
=head2 C<parse_feed_timestamp>
Take a feed_timestamp and return a Time::Piece object.
=cut
sub parse_feed_timestamp {
my ($self, $feed_timestamp) = @_;
$feed_timestamp = substr($feed_timestamp, 0, -length( $self->{utc_offset}));
return Time::Piece->strptime( $feed_timestamp, '%Y-%m-%dT%H:%M:%S' );
}
1;
__END__
=head1 SEE ALSO
=over 4
=item * L<Wiki::Toolkit>
=item * L<http://www.atomenabled.org/developers/syndication/>
=back
=head1 MAINTAINER
The Wiki::Toolkit team, http://www.wiki-toolkit.org/.
=head1 COPYRIGHT AND LICENSE
Copyright 2006-2009 Earle Martin and the Wiki::Toolkit team.
Copyright 2012 the Wiki::Toolkit team.
This module is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.
=head1 THANKS
Kake Pugh for originally writing Wiki::Toolkit::Feed::RSS and indeed
Wiki::Toolkit itself.
=cut