File Coverage

blib/lib/AxKit/XSP/Wiki.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package AxKit::XSP::Wiki;
2              
3 1     1   7171 use strict;
  1         4  
  1         30  
4              
5             #use Apache::AxKit::Language::XSP::TaglibHelper;
6 1     1   576 use AxKit::XSP::Wiki::Indexer;
  1         3  
  1         31  
7 1     1   1190 use Net::SMTP;
  1         54612  
  1         63  
8 1     1   1647 use Text::Diff;
  0            
  0            
9             use POSIX qw(strftime);
10             use vars qw($VERSION $NS @ISA @EXPORT_TAGLIB $EmailAlerts $EmailHost);
11              
12             $VERSION = '0.07';
13              
14             # The namespace associated with this taglib.
15             $NS = 'http://axkit.org/NS/xsp/wiki/1';
16             # Using TaglibHelper:
17             @ISA = qw(Apache::AxKit::Language::XSP::TaglibHelper);
18              
19             @EXPORT_TAGLIB = (
20             'display_page($dbpath,$db,$page,$action;$id):as_xml=1',
21             'preview_page($dbpath,$db,$page,$text,$texttype):as_xml=1',
22             'search($dbpath,$db,$query):as_xml=1',
23             );
24              
25             use DBI;
26             use XML::SAX::Writer;
27             use Pod::SAX;
28             use XML::LibXML::SAX::Parser;
29             use Text::WikiFormat::SAX;
30              
31             sub _mkdb {
32             my ($dbpath, $dbname) = @_;
33             my $db = DBI->connect(
34             'DBI:SQLite:dbname='. $dbpath . '/wiki-' . $dbname . '.db',
35             '', '', { AutoCommit => 1, RaiseError => 1 }
36             );
37            
38             eval {
39             $db->do('select * from Page, Formatter, History where 1 = 2');
40             };
41             if ($@) {
42             create_db($db);
43             }
44            
45             return $db;
46             }
47              
48             sub display_page ($$$$$) {
49             my ($dbpath, $dbname, $page, $action, $id) = @_;
50            
51             my $db = _mkdb($dbpath, $dbname);
52            
53             if ($action eq 'edit') {
54             return edit_page($db, $page);
55             }
56             elsif ($action eq 'history') {
57             return show_history($db, $page);
58             }
59             elsif ($action eq 'historypage') {
60             return show_history_page($db, $page, $id);
61             }
62             if ($action eq 'view') {
63             return view_page($db, $page);
64             }
65             else {
66             warn("Unrecognised action. Falling back to 'view'");
67             return view_page($db, $page);
68             }
69             }
70              
71             sub preview_page ($$$$$) {
72             my ($dbpath, $dbname, $page, $text, $texttype) = @_;
73             my $db = _mkdb($dbpath, $dbname);
74             my $sth = $db->prepare(<<'EOT');
75             SELECT Formatter.module
76             FROM Formatter
77             WHERE Formatter.id = ?
78             EOT
79             $sth->execute($texttype);
80            
81             my $output = '';
82             my $handler = XML::SAX::Writer->new(Output => \$output);
83             while ( my $row = $sth->fetch ) {
84             # create the parser
85             my $parser = $row->[0]->new(Handler => $handler);
86             eval {
87             $parser->parse_string($text);
88             };
89             if ($@) {
90             $output = '
91            
92             Error parsing the page: ' . xml_escape($@) . '
93            
94            
95             ';
96             }
97             last;
98             }
99             if (!$output) {
100             $output = <<'EOT';
101            
102            
103             Eek.
104            
105            
106             EOT
107             }
108              
109             $output =~ s/^<\?xml\s.*?\?>//s;
110              
111             # Now add edit stuff
112             $output .= '';
113             $output .= xml_escape($text);
114             $output .= '';
115            
116             $sth = $db->prepare(<<'EOT');
117             SELECT Formatter.id, Formatter.name
118             FROM Formatter
119             EOT
120             $sth->execute();
121             while (my $row = $sth->fetch) {
122             $output .= ' 123             ($texttype == $row->[0] ? '" selected="selected">' : '">') .
124             xml_escape($row->[1]) . '';
125             }
126             $sth->finish;
127            
128             $output .= '';
129              
130             return $output;
131             } # preview
132              
133             sub view_page {
134             my ($db, $page) = @_;
135             my $sth = $db->prepare(<<'EOT');
136             SELECT Page.content, Formatter.module
137             FROM Page, Formatter
138             WHERE Page.formatterid = Formatter.id
139             AND Page.name = ?
140             EOT
141             $sth->execute($page);
142            
143             my $output = '';
144             my $handler = XML::SAX::Writer->new(Output => \$output);
145             while ( my $row = $sth->fetch ) {
146             # create the parser
147             my $parser = $row->[1]->new(Handler => $handler);
148             eval {
149             $parser->parse_string($row->[0]);
150             };
151             if ($@) {
152             $output = '
153            
154             Error parsing the page: ' . xml_escape($@) . '
155            
156            
157             ';
158             }
159             last;
160             }
161             if (!$output) {
162             $output = <<'EOT';
163            
164             EOT
165             }
166             $output =~ s/^<\?xml\s.*?\?>//s;
167             AxKit::Debug(10, "Wiki Got: $output") if $ENV{MOD_PERL};
168             return $output;
169             }
170              
171             sub xml_escape {
172             my $text = shift;
173             $text =~ s/&/&/g;
174             $text =~ s/
175             $text =~ s/]]>/]]>/g;
176             return $text;
177             }
178              
179             sub get_default_formatter {
180             my ($db) = @_;
181             my $sth = $db->prepare("SELECT id FROM Formatter WHERE name LIKE ?");
182             $sth->execute("pod%");
183             while (my $row = $sth->fetch) {
184             return $row->[0];
185             }
186             die "No rows from Formatter table!";
187             }
188              
189             sub edit_page {
190             my ($db, $page) = @_;
191             my $sth = $db->prepare(<<'EOT');
192             SELECT Page.content, Page.formatterid
193             FROM Page
194             WHERE Page.name = ?
195             EOT
196             $sth->execute($page);
197            
198             my $output = '';
199             my $formatter = get_default_formatter($db);
200             while ( my $row = $sth->fetch ) {
201             # create the parser
202             $output .= xml_escape($row->[0]);
203             $formatter = $row->[1];
204             last;
205             }
206             $sth->finish;
207            
208             $output .= '';
209            
210             $sth = $db->prepare(<<'EOT');
211             SELECT Formatter.id, Formatter.name
212             FROM Formatter
213             EOT
214             $sth->execute();
215             while (my $row = $sth->fetch) {
216             $output .= ' 217             ($formatter == $row->[0] ? '" selected="selected">' : '">') .
218             xml_escape($row->[1]) . '';
219             }
220             $sth->finish;
221            
222             $output .= '';
223             return $output;
224             }
225              
226             sub search {
227             my ($dbpath, $dbname, $query) = @_;
228             my $db = _mkdb($dbpath, $dbname);
229             my %search = parse_search($query);
230             use Data::Dumper; warn(Dumper(\%search));
231             my $results = search_message_index( db => $db,
232             required => $search{required},
233             normal => $search{normal},
234             phrase => $search{phrase},
235             excluded => $search{excluded},
236             );
237             my $output = '';
238             if (!@{$results}) {
239             $output .= '';
240             }
241             foreach my $row (sort { $b->[1] <=> $a->[1] } @{$results}) {
242             $output .= "" . xml_escape($row->[0]) . "";
243             $output .= "" . xml_escape($row->[1]) . "";
244             }
245             $output .= "";
246             warn("Search results: $output\n");
247             return $output;
248             }
249              
250             sub search_message_index {
251             my %p = @_;
252            
253             my $db = $p{db};
254            
255             # Excluded words are excluded from all pages
256             my $exclude = '';
257             if ( @{$p{excluded}} ) {
258             $exclude .= " AND Page.name NOT IN (
259             SELECT DISTINCT Page.name
260             FROM Page, ContentIndex, Word
261             WHERE ContentIndex.page_id = Page.id
262             AND ContentIndex.word_id = Word.id
263             AND Word.word IN (" .
264             join(',', map { $db->quote($_) } @{$p{excluded}}) .
265             ")
266             )\n";
267             }
268              
269             my $sql = "
270             SELECT Page.name, SUM(ContentIndex.value) AS value
271             FROM ContentIndex, Page, Word
272             WHERE ContentIndex.page_id = Page.id
273             AND ContentIndex.word_id = Word.id
274             AND (" .
275             join(" OR ", (
276             (map { "Word.word = " . $db->quote($_) } @{$p{required}}),
277             (map { "Page.content LIKE " . $db->quote("\%$_\%") } @{$p{phrase}}),
278             )) .
279             ")
280             $exclude
281             GROUP BY ContentIndex.page_id
282             ";
283             warn("Getting required with:\n$sql\n");
284             return $db->selectall_arrayref($sql);
285             }
286              
287              
288              
289             sub parse_search {
290             my $query = shift;
291             my %search;
292             while (defined $query && $query =~ /\G(\S*)(\s*)/gc) {
293             my $term = $1;
294             my $space = $2;
295             next unless length($term);
296              
297             $term = lc($term);
298            
299             if ($term =~ s/^\+//) {
300             $search{required}{$term}++;
301             warn "Search required: $term\n";
302             }
303             elsif ($term =~ s/^\-//) {
304             $search{excluded}{$term}++;
305             warn "Search excluded: $term\n";
306             }
307             elsif ($term =~ /^(["'])/) {
308             my $quote = $1;
309             $term =~ s/^$quote//;
310             $term .= $space;
311              
312             if ($query =~ /\G(.*?)\.?$quote\s*/gc) {
313             $term .= $1;
314             $search{phrase}{$term}++;
315             warn "Search phrase: $term\n";
316             }
317             }
318             else {
319             $search{required}{$term}++;
320             warn "Search normal: $term\n";
321             }
322             }
323              
324             # turn into arrayrefs
325             foreach ( qw( normal required excluded phrase ) )
326             {
327             if ( $search{$_} )
328             {
329             $search{$_} = [ keys %{ $search{$_} } ]
330             }
331             else
332             {
333             $search{$_} = [];
334             }
335             }
336              
337             return %search;
338             }
339              
340             sub save_page {
341             my ($dbpath, $dbname, $page, $contents, $texttype, $ip, $user) = @_;
342             my $db = _mkdb($dbpath, $dbname);
343             _save_page($db, $page, $contents, $texttype, $ip, $user);
344             }
345              
346             sub _save_page {
347             my ($db, $page, $contents, $texttype, $ip, $user) = @_;
348             # NB fix hard coded formatterid
349             my $last_modified = time;
350             my @history = $db->selectrow_array('SELECT content FROM History WHERE name = ? ORDER BY modified DESC', {}, $page);
351             local $db->{AutoCommit} = 0;
352             $db->do(<<'EOT', {}, $page, $texttype, $contents, $last_modified, $ip, $user);
353             INSERT OR REPLACE INTO Page ( name, formatterid, content, last_modified, ip_address, username )
354             VALUES ( ?, ?, ?, ?, ?, ? )
355             EOT
356             $db->do(<<'EOT', {}, $page, $texttype, $contents, $last_modified, $ip, $user);
357             INSERT INTO History ( name, formatterid, content, modified, ip_address, username )
358             VALUES ( ?, ?, ?, ?, ?, ? )
359             EOT
360             $db->commit;
361             _index_page($db, $page);
362             if ($EmailAlerts) {
363             # create diff using Text::Diff
364             my $prev = @history ? $history[0] : '';
365             my $diff = diff(\$prev, \$contents, { STYLE => 'Unified' });
366            
367             my $host = $EmailHost || 'localhost';
368             my $smtp = Net::SMTP->new($host, Timeout => 10);
369             $smtp->mail('axkitwiki') || die "Wiki email alerts: MAIL FROM: failed";
370             $smtp->to($EmailAlerts) || die "Wiki email alerts: RCPT TO:<$EmailAlerts> failed";
371             $smtp->data() || die "Wiki email alerts: DATA failed";
372             my $date = strftime('%a, %d %b %Y %H:%M:%S %Z', localtime);
373            
374             my $changed_by = $user ? "$user @ $ip" : "someone at IP $ip";
375             $smtp->datasend(<<"EOT");
376             To: $EmailAlerts
377             From: "AxKit Wiki"
378             Subject: New Wiki Content at $page
379             Date: $date
380              
381             Wiki content at $page Changed by $changed_by :
382              
383             $diff
384              
385             EOT
386             $smtp->dataend();
387             $smtp->quit();
388             }
389             }
390              
391             sub _index_page {
392             my ($db, $page) = @_;
393             my $sth = $db->prepare(<<'EOT');
394             SELECT Page.id, Page.content, Formatter.module
395             FROM Page, Formatter
396             WHERE Page.formatterid = Formatter.id
397             AND Page.name = ?
398             EOT
399             $sth->execute($page);
400            
401             my $output = '';
402             while ( my $row = $sth->fetch ) {
403             my $handler = AxKit::XSP::Wiki::Indexer->new(DB => $db, PageId => $row->[0]);
404             # create the parser
405             my $parser = $row->[2]->new(Handler => $handler);
406             eval {
407             $parser->parse_string($row->[1]);
408             };
409             if ($@) {
410             warn("Indexing failed");
411             }
412             last;
413             }
414             }
415              
416             sub show_history {
417             my ($db, $page) = @_;
418             my $sth = $page ? $db->prepare('SELECT * FROM History WHERE name = ? ORDER BY modified DESC LIMIT 50') :
419             $db->prepare('SELECT * FROM History ORDER BY modified DESC LIMIT 50');
420             $sth->execute($page);
421             my $hist = '';
422             my %h;
423             my $cols = $sth->{NAME_lc};
424             while (my $row = $sth->fetch) {
425             @h{@$cols} = @$row;
426             $hist .= '';
427             $hist .= '' . xml_escape($h{name}) . '';
428             $hist .= '' . xml_escape($h{id}) . '';
429             $hist .= '' . xml_escape(scalar gmtime($h{modified})) . '';
430             $hist .= '' . xml_escape($h{ip_address}) . '';
431             $hist .= '' . xml_escape($h{username}) . '';
432             $hist .= '' . xml_escape(length($h{content})) . '';
433             $hist .= '';
434             }
435             $hist .= '';
436             return $hist;
437             }
438              
439             sub show_history_page {
440             my ($db, $page, $id) = @_;
441             my $sth = $db->prepare(<<'EOT');
442             SELECT History.content, Formatter.module,
443             History.ip_address, History.modified
444             FROM History, Formatter
445             WHERE History.formatterid = Formatter.id
446             AND History.name = ?
447             AND History.id = ?
448             EOT
449             $sth->execute($page, $id);
450            
451             my $output = '';
452             my $handler = XML::SAX::Writer->new(Output => \$output);
453             my ($ip, $modified);
454             while ( my $row = $sth->fetch ) {
455             ($ip, $modified) = ($row->[2], scalar(gmtime($row->[3])));
456             # create the parser
457             my $parser = $row->[1]->new(Handler => $handler);
458             eval {
459             $parser->parse_string($row->[0]);
460             };
461             if ($@) {
462             $output = '
463            
464             Error parsing the page: ' . xml_escape($@) . '
465            
466            
467             ';
468             }
469             last;
470             }
471             if (!$output) {
472             $output = <<'EOT';
473            
474            
475             Unable to find that history page, or unable to find formatter module
476            
477            
478             EOT
479             }
480             $output =~ s/^<\?xml\s.*?\?>\s*//s;
481             $output = "\n" .
482             "\n" .
483             $output;
484             return $output;
485             }
486              
487             sub restore_page {
488             my ($dbpath, $dbname, $page, $ip, $id, $user) = @_;
489            
490             my $db = _mkdb($dbpath, $dbname);
491             my $sth = $db->prepare('SELECT * FROM History WHERE name = ? and id = ?');
492             $sth->execute($page, $id);
493             my $row = $sth->fetch;
494             die "No such row" unless $row;
495             $sth->finish;
496             my ($texttype, $contents) = ($row->[2], $row->[3]);
497             _save_page($db, $page, $contents, $texttype, $ip, $user);
498             }
499              
500             sub create_db {
501             my ($db) = @_;
502            
503             $db->do(q{
504             create table Page (
505             id INTEGER PRIMARY KEY,
506             name NOT NULL,
507             formatterid NOT NULL,
508             content,
509             last_modified,
510             ip_address,
511             username
512             )
513             });
514             $db->do(q{
515             create unique index Page_name on Page ( name )
516             });
517             $db->do(q{
518             create table History (
519             id INTEGER PRIMARY KEY,
520             name NOT NULL,
521             formatterid NOT NULL,
522             content,
523             modified,
524             ip_address,
525             username
526             )
527             });
528             $db->do(q{
529             CREATE TABLE IgnoreWord
530             (
531             id INTEGER PRIMARY KEY,
532             word NOT NULL
533             )
534             });
535             $db->do(q{CREATE UNIQUE INDEX IgnoreWord_word on IgnoreWord (word)});
536             $db->do(q{
537             CREATE TABLE Word
538             (
539             id INTEGER PRIMARY KEY,
540             word NOT NULL
541             )
542             });
543             $db->do(q{CREATE UNIQUE INDEX Word_word on Word (word)});
544             $db->do(q{
545             CREATE TABLE ContentIndex
546             (
547             page_id INTEGER NOT NULL,
548             word_id INTEGER NOT NULL,
549             value INTEGER NOT NULL
550             )
551             });
552             $db->do(q{
553             create unique index ContentIndex_idx on ContentIndex (page_id, word_id)
554             });
555             $db->do(q{
556             create table Formatter ( id INTEGER PRIMARY KEY, module NOT NULL, name NOT NULL)
557             });
558             $db->do(q{
559             insert into Formatter (module, name) values ('Pod::SAX', 'pod - plain old documentation')
560             });
561             $db->do(q{
562             insert into Formatter (module, name) values ('Text::WikiFormat::SAX', 'wiki text')
563             });
564             $db->do(q{
565             insert into Formatter (module, name) values ('XML::LibXML::SAX::Parser', 'xml (freeform)')
566             });
567             $db->commit;
568             }
569              
570             sub extract_page_info {
571             my ($path_info) = @_;
572             $path_info =~ s/^\///;
573             my ($db, $page) = split("/", $path_info, 2);
574             $page ||= ''; # can't have page named 0. Ah well.
575              
576             if (!$db) {
577             return ('', '');
578             }
579             elsif ($db !~ /^[A-Z][A-Za-z0-9:_-]+$/) {
580             die "Invalid db name: $db";
581             }
582             elsif (length($page) && $page !~ /^[A-Z][A-Za-z0-9:_-]+$/) {
583             die "Invalid page name: $page";
584             }
585             return ($db, $page);
586             }
587              
588             1;
589              
590             __END__