File Coverage

lib/Kwiki/Atom.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Kwiki::Atom;
2 1     1   39828 use strict;
  1         2  
  1         35  
3 1     1   5 use warnings;
  1         1  
  1         28  
4 1     1   1583 use Kwiki::Plugin '-Base';
  0            
  0            
5             use Kwiki::Display;
6             use mixin 'Kwiki::Installer';
7             our $VERSION = '0.15';
8              
9             use XML::Atom;
10             use XML::Atom::Feed;
11             use XML::Atom::Link;
12             use XML::Atom::Entry;
13             use XML::Atom::Content;
14              
15             use DateTime;
16             use Kwiki::Atom::Server;
17              
18             use constant ATOM_TYPE => "application/atom+xml";
19              
20             const class_id => 'atom';
21             const class_title => 'Atom';
22             const css_file => 'atom.css';
23             const config_file => 'atom.yaml';
24             const cgi_class => 'Kwiki::Atom::CGI';
25             const server_class => 'Kwiki::Atom::Server';
26              
27             field depth => 0;
28             field 'headers';
29             field 'server';
30              
31             sub process {
32             }
33              
34             sub register {
35             my $registry = shift;
36             $registry->add(action => 'atom_edit');
37             $registry->add(action => 'atom_feed');
38             $registry->add(action => 'atom_post');
39             $registry->add(toolbar => 'recent_changes_atom_button',
40             template => 'recent_changes_atom_button.html',
41             show_for => ['recent_changes'],
42             );
43             $registry->add(toolbar => 'edit_atom_button',
44             template => 'edit_atom_button.html',
45             show_for => ['display'],
46             params_class => $self->class_id,
47             );
48             }
49              
50             sub fill_links {
51             my $name = eval { $self->hub->cgi->page_name };
52             my $url = CGI->new->url;
53             push @{ $self->hub->{links}{all} }, ($name ? {
54             rel => 'alternate',
55             type => ATOM_TYPE,
56             href => "$url?action=atom_edit;page_name=". $self->pages->current->uri,
57             } : ()), {
58             rel => 'service.feed',
59             type => ATOM_TYPE,
60             href => "$url?action=atom_feed",
61             }, {
62             rel => 'service.post',
63             type => ATOM_TYPE,
64             href => "$url?action=atom_post",
65             };
66             return;
67             }
68              
69             sub toolbar_params {
70             # require YAML;
71             # open X, '>>/tmp/post.log';
72             # print X "POSTDATA:\n", $self->cgi->POSTDATA, "\n";
73             # print X "HEADERS:\n", YAML::Dump(\%ENV), $/;
74             # close X;
75             return () unless $ENV{CONTENT_TYPE} and
76             ($ENV{CONTENT_TYPE} eq ATOM_TYPE
77             or $ENV{CONTENT_TYPE} =~ m{^\w+/xml}); # XXX ecto XXX
78              
79             $self->atom_post;
80              
81             my %header = &Spoon::Cookie::content_type;
82             print CGI::header(%header);
83             print $self->server->print;
84             exit;
85             }
86              
87             sub fill_header {
88             $self->wrap_header if !$self->headers;
89             $self->headers( [ @{$self->headers||[]}, @_ ] );
90             }
91              
92             sub wrap_header {
93             my $server = $self->server($self->server_class->new);
94              
95             my %accept = map { $_ => 1 }
96             $server->request_header('Accept') =~ m{([^\s,]+/[^;,]+)}g;
97             my $content_type = 'text/xml'; # fallback
98             foreach my $try_type (qw(
99             application/atom+xml
100             application/x.atom+xml
101             application/xml
102             )) {
103             $accept{$try_type} or next;
104             $content_type = $try_type;
105             last;
106             }
107              
108             $content_type .= '; charset=UTF-8';
109             $server->response_content_type($content_type);
110             $server->client($self);
111              
112             $self->hub->headers->content_type($content_type);
113             }
114              
115             sub make_entry {
116             my ($page, $depth, $flavor) = @_;
117             my $url = $self->server->uri;
118              
119             my $author = XML::Atom::Person->new;
120             $author->name($page->metadata->edit_by);
121              
122             my $link_html = XML::Atom::Link->new;
123             $link_html->type('text/html');
124             $link_html->rel('alternate');
125             $link_html->href("$url?".$page->uri);
126             $link_html->title('');
127              
128             my $link_edit = XML::Atom::Link->new;
129             $link_edit->type(ATOM_TYPE);
130             $link_edit->rel('service.edit');
131             $link_edit->href("$url?action=atom_edit;page_name=".$page->uri);
132             $link_edit->title('');
133              
134             my $entry = XML::Atom::Entry->new;
135             $entry->title($page->title);
136              
137             my $content = XML::Atom::Content->new;
138             my $elem = $content->elem;
139             my $text = ($content->LIBXML) ? 'XML::LibXML::Text'
140             : 'XML::XPath::Node::Text';
141             if ($flavor and $flavor eq 'html') {
142             $content->type('text/html');
143              
144             my $data = $page->to_html;
145             my $copy = qq(
$data
);
146             my $node;
147             local $@;
148             eval {
149             if ($content->LIBXML) {
150             require XML::LibXML;
151             my $parser = XML::LibXML->new;
152             my $tree = $parser->parse_string($copy);
153             $node = $tree->getDocumentElement;
154             } else {
155             require XML::XPath;
156             my $xp = XML::XPath->new(xml => $copy);
157             $node = (($xp->find('/')->get_nodelist)[0]->getChildNodes)[0]
158             if $xp;
159             }
160             };
161             if (!$@ && $node) {
162             $elem->appendChild($node);
163             $elem->setAttribute('mode', 'xml');
164             } else {
165             $elem->appendChild($text->new($data));
166             $elem->setAttribute('mode', 'escaped');
167             }
168             }
169             else {
170             $content->type('text/plain');
171             $elem->appendChild($text->new($page->content));
172             $elem->setAttribute('mode', 'escaped');
173             }
174              
175             $entry->content($content);
176             $entry->summary('');
177             $entry->issued( DateTime->from_epoch( epoch => $page->io->ctime || time )->iso8601 . 'Z' );
178             $entry->modified( DateTime->from_epoch( epoch => $page->io->mtime || time )->iso8601 . 'Z' );
179             $entry->id("$url?".$page->uri);
180              
181             $entry->author($author);
182             $entry->add_link($link_html);
183             $entry->add_link($link_edit);
184              
185             return $entry;
186             }
187              
188             sub update_page {
189             my $page = shift;
190             my $method = $self->server->request_method;
191             my $entry = eval { $self->server->atom_body };
192              
193             if (!$entry) {
194             print "Status: 400\n\n";
195             $self->fill_header( -status => 400 );
196             return;
197             }
198              
199             if (!$page) {
200             my $title = $entry->title;
201             if ($entry->content->type =~ /\bx?html\b/i) {
202             require HTML::Entities;
203             HTML::Entities::decode_entities($title);
204             }
205             $page = $self->pages->new_page($title);
206              
207             if ($page->exists and $method eq 'POST') {
208             $self->server->response_code(409);
209             $self->server->{_error} = 'This page already exists';
210             $self->fill_header(
211             -status => 409,
212             -type => 'text/plain',
213             -warning => 'This page already exists',
214             );
215             return undef;
216             }
217             }
218              
219             $self->hub->users->current->name(
220             eval { $self->server->get_auth_info->{Username} }
221             || $self->hub->config->user_default_name
222             );
223             my $body = $entry->content->body;
224             if ($entry->content->type =~ /\bx?html\b/i) {
225             $body =~ s/<[^>]+>//g;
226             require HTML::Entities;
227             HTML::Entities::decode_entities($body);
228             }
229             $page->content($body);
230             $page->update->store;
231              
232             return $page;
233             }
234              
235             sub atom_list {
236             my $url = $self->server->uri;
237              
238             my $link_feed = XML::Atom::Link->new;
239             $link_feed->type(ATOM_TYPE);
240             $link_feed->rel('service.feed');
241             $link_feed->title($self->config->site_title);
242             $link_feed->href("$url?action=atom_feed");
243              
244             my $link_post = XML::Atom::Link->new;
245             $link_post->type(ATOM_TYPE);
246             $link_post->rel('service.post');
247             $link_post->title($self->config->site_title);
248             $link_post->href("$url?action=atom_post");
249              
250             my $feed = XML::Atom::Feed->new;
251             $feed->title($self->config->site_title);
252             $feed->info($self->config->site_title);
253             $feed->add_link($link_feed);
254             $feed->add_link($link_post);
255             $feed->modified(DateTime->now->iso8601 . 'Z');
256              
257             $self->munge($feed->as_xml);
258             }
259              
260             sub atom_post {
261             $self->fill_header;
262             return $self->atom_list if $self->server->request_method eq 'GET';
263              
264             $self->server->{request_content} = $self->cgi->POSTDATA
265             if $self->server->request_method eq 'POST';
266              
267             $self->server->run;
268             $self->server->print;
269             }
270              
271             sub atom_edit {
272             $self->fill_header;
273             $self->server->run;
274             $self->server->print;
275             }
276              
277             sub atom_feed {
278             $self->fill_header;
279              
280             my $depth = $self->cgi->depth;
281             my $flavor = $self->cgi->flavor;
282             my $pages = [
283             sort {
284             $b->modified_time <=> $a->modified_time
285             } ($depth ? $self->pages->recent_by_count($depth) : $self->pages->all)
286             ];
287              
288             my $timestamp = @$pages ? $pages->[0]->metadata->edit_unixtime : time;
289              
290             my $cache = eval { $self->hub->load_class('cache') }
291             or return $self->generate($pages, $depth, $flavor, $timestamp);
292              
293             $cache->process(
294             sub { $self->generate($pages, $depth, $flavor, $timestamp) },
295             'atom', $depth, $flavor, $timestamp, int(time / 600)
296             );
297             }
298              
299             sub generate {
300             my ($pages, $depth, $flavor, $timestamp) = @_;
301              
302             my $datetime = DateTime->from_epoch( epoch => $timestamp );
303             my $url = $self->server->uri;
304             my $link_html = XML::Atom::Link->new;
305             $link_html->type('text/html');
306             $link_html->rel('alternate');
307             $link_html->title($self->config->site_title);
308             $link_html->href($url);
309              
310             my $link_post = XML::Atom::Link->new;
311             $link_post->type('application/atom+xml');
312             $link_post->rel('service.post');
313             $link_post->title($self->config->site_title);
314             $link_post->href("$url?action=atom_post");
315              
316             my $feed = XML::Atom::Feed->new;
317             $feed->title($self->config->site_title);
318             $feed->info($self->config->site_title);
319             $feed->add_link($link_html);
320             $feed->add_link($link_post);
321             $feed->modified($datetime->iso8601 . 'Z');
322              
323             my $author = XML::Atom::Person->new;
324             $author->name($self->config->site_url);
325              
326             $self->config->script_name($url);
327              
328             for my $page (@$pages) {
329             $feed->add_entry( $self->make_entry($page, $depth, $flavor) );
330             }
331              
332             $self->munge($feed->as_xml);
333             }
334              
335             sub munge {
336             my $xml = shift;
337             $xml =~ /<\?xml/ or $xml = qq($xml);
338             $xml =~ s/version="1.0"(?![^>]*encoding=)/version="1.0" encoding="UTF-8"/;
339             $xml =~ s/(<\w+)/$1 version="0.3"/;
340             $xml =~ s{\?>}{?>};
341             return $xml;
342             }
343              
344             package Kwiki::Atom::CGI;
345             use Kwiki::CGI '-base';
346              
347             cgi 'depth';
348             cgi 'flavor';
349             cgi 'POSTDATA';
350              
351             1;
352              
353             package Kwiki::Atom;
354              
355             1;
356              
357             __DATA__