| 1 | package Wiki::Toolkit::Plugin::JSON; |
|---|
| 2 | |
|---|
| 3 | use strict; |
|---|
| 4 | |
|---|
| 5 | use vars qw( $VERSION ); |
|---|
| 6 | $VERSION = '0.09'; |
|---|
| 7 | |
|---|
| 8 | use POSIX 'strftime'; |
|---|
| 9 | use Time::Piece; |
|---|
| 10 | use URI::Escape; |
|---|
| 11 | use Carp qw( croak ); |
|---|
| 12 | |
|---|
| 13 | BEGIN { |
|---|
| 14 | require constant; |
|---|
| 15 | eval { |
|---|
| 16 | require JSON::Syck; |
|---|
| 17 | JSON::Syck->import(); |
|---|
| 18 | }; |
|---|
| 19 | |
|---|
| 20 | unless ($@) { |
|---|
| 21 | constant->import( JSON_SYCK => 1 ); |
|---|
| 22 | } |
|---|
| 23 | else { |
|---|
| 24 | eval { require JSON; JSON->import() }; |
|---|
| 25 | die "Couldn't find a JSON Package, install JSON::Syck or JSON" if $@; |
|---|
| 26 | constant->import( JSON_SYCK => 0 ); |
|---|
| 27 | } |
|---|
| 28 | } |
|---|
| 29 | |
|---|
| 30 | sub new { |
|---|
| 31 | my $class = shift; |
|---|
| 32 | my $self = {@_}; |
|---|
| 33 | bless $self, $class; |
|---|
| 34 | |
|---|
| 35 | unless ( $self->{wiki} && UNIVERSAL::isa( $self->{wiki}, 'Wiki::Toolkit' ) ) |
|---|
| 36 | { |
|---|
| 37 | croak 'No Wiki::Toolkit object supplied'; |
|---|
| 38 | } |
|---|
| 39 | |
|---|
| 40 | |
|---|
| 41 | foreach my $arg (qw/site_name site_url make_node_url recent_changes_link/) { |
|---|
| 42 | croak "No $arg supplied" unless $self->{$arg}; |
|---|
| 43 | } |
|---|
| 44 | |
|---|
| 45 | $self->{timestamp_fmt} = $Wiki::Toolkit::Store::Database::timestamp_fmt; |
|---|
| 46 | $self->{utc_offset} = strftime "%z", localtime; |
|---|
| 47 | $self->{utc_offset} =~ s/(..)(..)$/$1:$2/; |
|---|
| 48 | |
|---|
| 49 | return $self; |
|---|
| 50 | } |
|---|
| 51 | |
|---|
| 52 | sub recent_changes { |
|---|
| 53 | my ( $self, %args ) = @_; |
|---|
| 54 | my $wiki = $self->{wiki}; |
|---|
| 55 | |
|---|
| 56 | |
|---|
| 57 | |
|---|
| 58 | my %criteria = ( ignore_case => 1, ); |
|---|
| 59 | |
|---|
| 60 | if ( $args{days} ) { |
|---|
| 61 | $criteria{days} = $args{days}; |
|---|
| 62 | } |
|---|
| 63 | else { |
|---|
| 64 | $criteria{last_n_changes} = $args{items} || 15; |
|---|
| 65 | } |
|---|
| 66 | |
|---|
| 67 | if ( $args{ignore_minor_edits} ) { |
|---|
| 68 | $criteria{metadata_wasnt} = { major_change => 0 }; |
|---|
| 69 | } |
|---|
| 70 | |
|---|
| 71 | if ( $args{filter_on_metadata} ) { |
|---|
| 72 | $criteria{metadata_was} = $args{filter_on_metadata}; |
|---|
| 73 | } |
|---|
| 74 | |
|---|
| 75 | my @changes = $wiki->list_recent_changes(%criteria); |
|---|
| 76 | |
|---|
| 77 | foreach my $change (@changes) { |
|---|
| 78 | |
|---|
| 79 | $change->{timestamp} = $change->{last_modified}; |
|---|
| 80 | |
|---|
| 81 | |
|---|
| 82 | my $time = |
|---|
| 83 | Time::Piece->strptime( $change->{timestamp}, $self->{timestamp_fmt} ); |
|---|
| 84 | |
|---|
| 85 | my $utc_offset = $self->{utc_offset}; |
|---|
| 86 | |
|---|
| 87 | $change->{timestamp} = $time->strftime("%Y-%m-%dT%H:%M:%S$utc_offset"); |
|---|
| 88 | |
|---|
| 89 | $change->{author} = $change->{metadata}{username}[0] |
|---|
| 90 | || $change->{metadata}{host}[0] |
|---|
| 91 | || ''; |
|---|
| 92 | $change->{description} = $change->{metadata}{comment}[0] || ''; |
|---|
| 93 | |
|---|
| 94 | $change->{status} = ( 1 == $change->{version} ) ? 'new' : 'updated'; |
|---|
| 95 | |
|---|
| 96 | $change->{major_change} = $change->{metadata}{major_change}[0]; |
|---|
| 97 | $change->{major_change} = 1 unless defined $change->{major_change}; |
|---|
| 98 | $change->{importance} = $change->{major_change} ? 'major' : 'minor'; |
|---|
| 99 | |
|---|
| 100 | $change->{url} = |
|---|
| 101 | $self->{make_node_url}->( $change->{name}, $change->{version} ); |
|---|
| 102 | |
|---|
| 103 | if ( $self->{make_diff_url} ) { |
|---|
| 104 | $change->{diff_url} = $self->{make_diff_url}->( $change->{name} ); |
|---|
| 105 | } |
|---|
| 106 | |
|---|
| 107 | if ( $self->{make_history_url} ) { |
|---|
| 108 | $change->{history_url} = |
|---|
| 109 | $self->{make_history_url}->( $change->{name} ); |
|---|
| 110 | } |
|---|
| 111 | |
|---|
| 112 | my $change->{node_url} = $self->{make_node_url}->( $change->{name} ); |
|---|
| 113 | |
|---|
| 114 | my $rdf_url = $change->{node_url}; |
|---|
| 115 | $rdf_url =~ s/\?/\?id=/; |
|---|
| 116 | $rdf_url .= ';format=rdf'; |
|---|
| 117 | $change->{rdf_url} = $rdf_url; |
|---|
| 118 | |
|---|
| 119 | |
|---|
| 120 | my $title = $change->{name}; |
|---|
| 121 | $title =~ s/&/&/g; |
|---|
| 122 | $title =~ s/</</g; |
|---|
| 123 | $title =~ s/>/>/g; |
|---|
| 124 | $change->{title} = $title; |
|---|
| 125 | } |
|---|
| 126 | return $self->make_json( \@changes ); |
|---|
| 127 | } |
|---|
| 128 | |
|---|
| 129 | sub make_json { |
|---|
| 130 | my ( $self, $data ) = @_; |
|---|
| 131 | if (JSON_SYCK) { |
|---|
| 132 | return JSON::Syck::Dump($data); |
|---|
| 133 | } |
|---|
| 134 | else { |
|---|
| 135 | return JSON::objToJson($data); |
|---|
| 136 | } |
|---|
| 137 | } |
|---|
| 138 | |
|---|
| 139 | 1; |
|---|
| 140 | |
|---|
| 141 | __END__ |
|---|
| 142 | |
|---|
| 143 | =head1 NAME |
|---|
| 144 | |
|---|
| 145 | Wiki::Toolkit::Plugin::JSON - A Wiki::Toolkit plugin to output RecentChanges JSON. |
|---|
| 146 | |
|---|
| 147 | =head1 DESCRIPTION |
|---|
| 148 | |
|---|
| 149 | This is an alternative access to the recent changes of a Wiki::Toolkit |
|---|
| 150 | wiki. It outputs JSON as described by the ModWiki proposal at |
|---|
| 151 | L<http://www.usemod.com/cgi-bin/mb.pl?ModWiki> |
|---|
| 152 | |
|---|
| 153 | =head1 SYNOPSIS |
|---|
| 154 | |
|---|
| 155 | use Wiki::Toolkit; |
|---|
| 156 | use Wiki::Toolkit::Plugin::JSON; |
|---|
| 157 | |
|---|
| 158 | my $wiki = Wiki::Toolkit->new( ... ); # See perldoc Wiki::Toolkit |
|---|
| 159 | |
|---|
| 160 | # Set up the JSON feeder with the mandatory arguments - see |
|---|
| 161 | # C<new()> below for more, optional, arguments. |
|---|
| 162 | my $json = Wiki::Toolkit::Plugin::JSON->new( |
|---|
| 163 | wiki => $wiki, |
|---|
| 164 | site_name => 'My Wiki', |
|---|
| 165 | site_url => 'http://example.com/', |
|---|
| 166 | make_node_url => sub |
|---|
| 167 | { |
|---|
| 168 | my ($node_name, $version) = @_; |
|---|
| 169 | return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version); |
|---|
| 170 | }, |
|---|
| 171 | recent_changes_link => 'http://example.com/?RecentChanges', |
|---|
| 172 | ); |
|---|
| 173 | |
|---|
| 174 | print "Content-type: application/xml\n\n"; |
|---|
| 175 | print $json->recent_changes; |
|---|
| 176 | |
|---|
| 177 | =head1 METHODS |
|---|
| 178 | |
|---|
| 179 | =head2 C<new()> |
|---|
| 180 | |
|---|
| 181 | my $json = Wiki::Toolkit::Plugin::JSON->new( |
|---|
| 182 | # Mandatory arguments: |
|---|
| 183 | wiki => $wiki, |
|---|
| 184 | site_name => 'My Wiki', |
|---|
| 185 | site_url => 'http://example.com/', |
|---|
| 186 | make_node_url => sub |
|---|
| 187 | { |
|---|
| 188 | my ($node_name, $version) = @_; |
|---|
| 189 | return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version); |
|---|
| 190 | }, |
|---|
| 191 | recent_changes_link => 'http://example.com/?RecentChanges', |
|---|
| 192 | |
|---|
| 193 | # Optional arguments: |
|---|
| 194 | site_description => 'My wiki about my stuff', |
|---|
| 195 | interwiki_identifier => 'MyWiki', |
|---|
| 196 | make_diff_url => sub |
|---|
| 197 | { |
|---|
| 198 | my $node_name = shift; |
|---|
| 199 | return 'http://example.com/?diff=' . uri_escape($node_name) |
|---|
| 200 | }, |
|---|
| 201 | make_history_url => sub |
|---|
| 202 | { |
|---|
| 203 | my $node_name = shift; |
|---|
| 204 | return 'http://example.com/?hist=' . uri_escape($node_name) |
|---|
| 205 | }, |
|---|
| 206 | software_name => $your_software_name, # e.g. "Wiki::Toolkit" |
|---|
| 207 | software_version => $your_software_version, # e.g. "0.73" |
|---|
| 208 | software_homepage => $your_software_homepage, # e.g. "http://search.cpan.org/dist/CGI-Wiki/" |
|---|
| 209 | ); |
|---|
| 210 | |
|---|
| 211 | C<wiki> must be a L<Wiki::Toolkit> object. C<make_node_url>, and |
|---|
| 212 | C<make_diff_url> and C<make_history_url>, if supplied, must be coderefs. |
|---|
| 213 | |
|---|
| 214 | The mandatory arguments are: |
|---|
| 215 | |
|---|
| 216 | =over 4 |
|---|
| 217 | |
|---|
| 218 | =item * wiki |
|---|
| 219 | |
|---|
| 220 | =item * site_name |
|---|
| 221 | |
|---|
| 222 | =item * site_url |
|---|
| 223 | |
|---|
| 224 | =item * make_node_url |
|---|
| 225 | |
|---|
| 226 | =item * recent_changes_link |
|---|
| 227 | |
|---|
| 228 | =back |
|---|
| 229 | |
|---|
| 230 | The three optional arguments |
|---|
| 231 | |
|---|
| 232 | =over 4 |
|---|
| 233 | |
|---|
| 234 | =item * software_name |
|---|
| 235 | |
|---|
| 236 | =item * software_version |
|---|
| 237 | |
|---|
| 238 | =item * software_homepage |
|---|
| 239 | |
|---|
| 240 | =back |
|---|
| 241 | |
|---|
| 242 | are used to generate DOAP (Description Of A Project - see L<http://usefulinc.com/doap>) metadata |
|---|
| 243 | for the feed to show what generated it. |
|---|
| 244 | |
|---|
| 245 | =head2 C<recent_changes()> |
|---|
| 246 | |
|---|
| 247 | $wiki->write_node( |
|---|
| 248 | 'About This Wiki', |
|---|
| 249 | 'blah blah blah', |
|---|
| 250 | $checksum, |
|---|
| 251 | { |
|---|
| 252 | comment => 'Stub page, please update!', |
|---|
| 253 | username => 'Fred', |
|---|
| 254 | } |
|---|
| 255 | ); |
|---|
| 256 | |
|---|
| 257 | print "Content-type: application/xml\n\n"; |
|---|
| 258 | print $json->recent_changes; |
|---|
| 259 | |
|---|
| 260 | # Or get something other than the default of the latest 15 changes. |
|---|
| 261 | print $json->recent_changes( items => 50 ); |
|---|
| 262 | print $json->recent_changes( days => 7 ); |
|---|
| 263 | |
|---|
| 264 | # Or ignore minor edits. |
|---|
| 265 | print $json->recent_changes( ignore_minor_edits => 1 ); |
|---|
| 266 | |
|---|
| 267 | # Personalise your feed further - consider only changes |
|---|
| 268 | # made by Fred to pages about bookshops. |
|---|
| 269 | print $json->recent_changes( |
|---|
| 270 | filter_on_metadata => { |
|---|
| 271 | username => 'Fred', |
|---|
| 272 | category => 'Bookshops', |
|---|
| 273 | }, |
|---|
| 274 | ); |
|---|
| 275 | |
|---|
| 276 | If using C<filter_on_metadata>, note that only changes satisfying |
|---|
| 277 | I<all> criteria will be returned. |
|---|
| 278 | |
|---|
| 279 | B<Note:> Many of the fields emitted by the JSON generator are taken |
|---|
| 280 | from the node metadata. The form of this metadata is I<not> mandated |
|---|
| 281 | by L<Wiki::Toolkit>. Your wiki application should make sure to store some or |
|---|
| 282 | all of the following metadata when calling C<write_node>: |
|---|
| 283 | |
|---|
| 284 | =over 4 |
|---|
| 285 | |
|---|
| 286 | =item B<comment> - a brief comment summarising the edit that has just been made. Defaults to the empty string. |
|---|
| 287 | |
|---|
| 288 | =item B<username> - an identifier for the person who made the edit; will be used as the Dublin Core contributor for this item. Defaults to the empty string. |
|---|
| 289 | |
|---|
| 290 | =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. |
|---|
| 291 | |
|---|
| 292 | =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>). |
|---|
| 293 | |
|---|
| 294 | =back |
|---|
| 295 | |
|---|
| 296 | =head2 C<rss_timestamp()> |
|---|
| 297 | |
|---|
| 298 | print $json->rss_timestamp(); |
|---|
| 299 | |
|---|
| 300 | Returns the timestamp of the feed in POSIX::strftime style ("Tue, 29 Feb 2000 |
|---|
| 301 | 12:34:56 GMT"), which is equivalent to the timestamp of the most recent item |
|---|
| 302 | in the feed. Takes the same arguments as recent_changes(). You will most likely |
|---|
| 303 | need this to print a Last-Modified HTTP header so user-agents can determine |
|---|
| 304 | whether they need to reload the feed or not. |
|---|
| 305 | |
|---|
| 306 | =head1 SEE ALSO |
|---|
| 307 | |
|---|
| 308 | =over 4 |
|---|
| 309 | |
|---|
| 310 | =item * L<Wiki::Toolkit> |
|---|
| 311 | |
|---|
| 312 | =item * L<http://web.resource.org/rss/1.0/spec> |
|---|
| 313 | |
|---|
| 314 | =item * L<http://www.usemod.com/cgi-bin/mb.pl?ModWiki> |
|---|
| 315 | |
|---|
| 316 | =back |
|---|
| 317 | |
|---|
| 318 | =head1 MAINTAINER |
|---|
| 319 | |
|---|
| 320 | Earle Martin <EMARTIN@cpan.org>. Originally by Kake Pugh <kake@earth.li>. |
|---|
| 321 | |
|---|
| 322 | =head1 COPYRIGHT AND LICENSE |
|---|
| 323 | |
|---|
| 324 | Copyright 2003-4 Kake Pugh. Subsequent modifications copyright 2005 |
|---|
| 325 | Earle Martin. |
|---|
| 326 | |
|---|
| 327 | This module is free software; you can redistribute it and/or modify it |
|---|
| 328 | under the same terms as Perl itself. |
|---|
| 329 | |
|---|
| 330 | =head1 THANKS |
|---|
| 331 | |
|---|
| 332 | The members of the Semantic Web Interest Group channel on irc.freenode.net, |
|---|
| 333 | #swig, were very useful in the development of this module. |
|---|
| 334 | |
|---|
| 335 | =cut |
|---|