package AxKit::XSP::Wiki; use strict; use Apache::AxKit::Language::XSP::TaglibHelper; use vars qw($VERSION $NS @ISA @EXPORT_TAGLIB); $VERSION = '0.06'; # The namespace associated with this taglib. $NS = 'http://axkit.org/NS/xsp/wiki/1'; # Using TaglibHelper: @ISA = qw(Apache::AxKit::Language::XSP::TaglibHelper); @EXPORT_TAGLIB = ( 'display_page($dbpath,$db,$page,$action;$id):as_xml=1', 'preview_page($dbpath,$db,$page,$text,$texttype):as_xml=1', ); use DBI; use XML::SAX::Writer; use Pod::SAX; use XML::LibXML::SAX::Parser; use Text::WikiFormat::SAX; sub _mkdb { my ($dbpath, $dbname) = @_; my $db = DBI->connect( 'DBI:SQLite:dbname='. $dbpath . '/wiki-' . $dbname . '.db', '', '', { AutoCommit => 1, RaiseError => 1 } ); eval { $db->do('select * from Page, Formatter, History where 1 = 2'); }; if ($@) { create_db($db); } return $db; } sub display_page ($$$$$) { my ($dbpath, $dbname, $page, $action, $id) = @_; my $db = _mkdb($dbpath, $dbname); if ($action eq 'edit') { return edit_page($db, $page); } elsif ($action eq 'history') { return show_history($db, $page); } elsif ($action eq 'historypage') { return show_history_page($db, $page, $id); } if ($action eq 'view') { return view_page($db, $page); } else { warn("Unrecognised action. Falling back to 'view'"); return view_page($db, $page); } } sub preview_page ($$$$$) { my ($dbpath, $dbname, $page, $text, $texttype) = @_; my $db = _mkdb($dbpath, $dbname); my $sth = $db->prepare(<<'EOT'); SELECT Formatter.module FROM Formatter WHERE Formatter.id = ? EOT $sth->execute($texttype); my $output = ''; my $handler = XML::SAX::Writer->new(Output => \$output); while ( my $row = $sth->fetch ) { # create the parser my $parser = $row->[0]->new(Handler => $handler); eval { $parser->parse_string($text); }; if ($@) { $output = ' Error parsing the page: ' . xml_escape($@) . ' '; } last; } if (!$output) { $output = <<'EOT'; Eek. EOT } $output =~ s/^<\?xml\s.*?\?>//s; # Now add edit stuff $output .= ''; $output .= xml_escape($text); $output .= ''; $sth = $db->prepare(<<'EOT'); SELECT Formatter.id, Formatter.name FROM Formatter EOT $sth->execute(); while (my $row = $sth->fetch) { $output .= '' : '">') . xml_escape($row->[1]) . ''; } $sth->finish; $output .= ''; return $output; } # preview sub view_page { my ($db, $page) = @_; my $sth = $db->prepare(<<'EOT'); SELECT Page.content, Formatter.module FROM Page, Formatter WHERE Page.formatterid = Formatter.id AND Page.name = ? EOT $sth->execute($page); my $output = ''; my $handler = XML::SAX::Writer->new(Output => \$output); while ( my $row = $sth->fetch ) { # create the parser my $parser = $row->[1]->new(Handler => $handler); eval { $parser->parse_string($row->[0]); }; if ($@) { $output = ' Error parsing the page: ' . xml_escape($@) . ' '; } last; } if (!$output) { $output = <<'EOT'; EOT } $output =~ s/^<\?xml\s.*?\?>//s; AxKit::Debug(10, "Wiki Got: $output"); return $output; } sub xml_escape { my $text = shift; $text =~ s/&/&/g; $text =~ s//]]>/g; return $text; } sub edit_page { my ($db, $page) = @_; my $sth = $db->prepare(<<'EOT'); SELECT Page.content, Page.formatterid FROM Page WHERE Page.name = ? EOT $sth->execute($page); my $output = ''; my $formatter = 1; while ( my $row = $sth->fetch ) { # create the parser $output .= xml_escape($row->[0]); $formatter = $row->[1]; last; } $sth->finish; $output .= ''; $sth = $db->prepare(<<'EOT'); SELECT Formatter.id, Formatter.name FROM Formatter EOT $sth->execute(); while (my $row = $sth->fetch) { $output .= '' : '">') . xml_escape($row->[1]) . ''; } $sth->finish; $output .= ''; return $output; } sub save_page { my ($dbpath, $dbname, $page, $contents, $texttype, $ip, $rss) = @_; $rss = [$rss, _mkrssheader($dbname)]; my $db = _mkdb($dbpath, $dbname); _save_page($db, $page, $contents, $texttype, $ip, $rss); } sub _save_page { my ($db, $page, $contents, $texttype, $ip, $rss) = @_; # NB fix hard coded formatterid my $last_modified = time; local $db->{AutoCommit} = 0; my (@row) = $db->selectrow_array("SELECT * FROM Page WHERE name = ?", {}, $page); if (@row) { # store history shift @row; # Remove id $db->do('INSERT INTO History (name, formatterid, content, modified, ip_address) VALUES (?, ?, ?, ?, ?)', {}, @row); } else { # New page if ($rss->[0]) { use Fatal qw(open close); open(RSS, ">$rss->[0]"); flock(RSS, 2); # lock ex print RSS $rss->[1]; my $sth = $db->prepare('SELECT * FROM Page ORDER BY last_modified DESC'); $sth->execute; while (my $row = $sth->fetch) { print RSS <<"EOT"; $row->[1] EOT } print RSS "\n"; flock(RSS, 8); # unlock close(RSS); } } my $sth = $db->prepare(<<'EOT'); INSERT OR REPLACE INTO Page ( name, formatterid, content, last_modified, ip_address ) VALUES ( ?, ?, ?, ?, ? ) EOT $sth->execute($page, $texttype, $contents, $last_modified, $ip); $db->commit; } sub _mkrssheader { my ($dbname) = @_; return <<"EOT"; $dbname http://take23.org/view/$dbname Take23 $dbname EOT } sub show_history { my ($db, $page) = @_; my $sth = $db->prepare('SELECT * FROM History WHERE name = ? ORDER BY modified DESC'); $sth->execute($page); my $hist = ''; while (my $row = $sth->fetch) { $hist .= ''; $hist .= '' . xml_escape($row->[0]) . ''; $hist .= '' . xml_escape(scalar gmtime($row->[4])) . ''; $hist .= '' . xml_escape($row->[5]) . ''; $hist .= '' . xml_escape(length($row->[3])) . ''; $hist .= ''; } $hist .= ''; return $hist; } sub show_history_page { my ($db, $page, $id) = @_; my $sth = $db->prepare(<<'EOT'); SELECT History.content, Formatter.module, History.ip_address, History.modified FROM History, Formatter WHERE History.formatterid = Formatter.id AND History.name = ? AND History.id = ? EOT $sth->execute($page, $id); my $output = ''; my $handler = XML::SAX::Writer->new(Output => \$output); my ($ip, $modified); while ( my $row = $sth->fetch ) { ($ip, $modified) = ($row->[2], scalar(gmtime($row->[3]))); # create the parser my $parser = $row->[1]->new(Handler => $handler); eval { $parser->parse_string($row->[0]); }; if ($@) { $output = ' Error parsing the page: ' . xml_escape($@) . ' '; } last; } if (!$output) { $output = <<'EOT'; Unable to find that history page, or unable to find formatter module EOT } $output =~ s/^<\?xml\s.*?\?>\s*//s; $output = "\n" . "\n" . $output; return $output; } sub restore_page { my ($dbpath, $dbname, $page, $ip, $id) = @_; my $db = _mkdb($dbpath, $dbname); my $sth = $db->prepare('SELECT * FROM History WHERE name = ? and id = ?'); $sth->execute($page, $id); my $row = $sth->fetch; die "No such row" unless $row; $sth->finish; my ($texttype, $contents) = ($row->[2], $row->[3]); _save_page($db, $page, $contents, $texttype, $ip); } sub create_db { my ($db) = @_; $db->do(q{ create table Page ( id INTEGER PRIMARY KEY, name NOT NULL, formatterid NOT NULL, content, last_modified, ip_address ) }); $db->do(q{ create table History ( id INTEGER PRIMARY KEY, name NOT NULL, formatterid NOT NULL, content, modified, ip_address ) }); $db->do(q{ create unique index Page_name on Page ( name ) }); $db->do(q{ create table Formatter ( id INTEGER PRIMARY KEY, module NOT NULL, name NOT NULL) }); $db->do(q{ insert into Formatter (module, name) values ('Pod::SAX', 'pod - plain old documentation') }); $db->do(q{ insert into Formatter (module, name) values ('Text::WikiFormat::SAX', 'wiki text') }); $db->do(q{ insert into Formatter (module, name) values ('XML::LibXML::SAX::Parser', 'xml (freeform)') }); $db->commit; } sub extract_page_info { my ($path_info) = @_; $path_info =~ s/^\///; my ($db, $page) = split("/", $path_info, 2); $page ||= ''; # can't have page named 0. Ah well. if (!$db) { return ('', ''); } elsif ($db !~ /^[A-Z][A-Za-z0-9:_-]+$/) { die "Invalid db name: $db"; } elsif (length($page) && $page !~ /^[A-Z][A-Za-z0-9:_-]+$/) { die "Invalid page name: $page"; } return ($db, $page); } 1; __END__ =head1 NAME AxKit::XSP::Wiki - An AxKit XSP based Wiki clone =head1 SYNOPSIS Follow the instructions in README for installation =head1 DESCRIPTION There's not much to say about Wiki's. They're kind cool, writable web sites. This module implements a wiki that uses (at the moment) POD for it's editing language. At the moment there's no version control, user management, search, recent edits, or pretty much any of the normally expected Wiki-type stuff. But it will come, eventually. =head1 AUTHOR Matt Sergeant, matt@sergeant.org =head1 LICENSE This is free software. You may use it and redistribute it under the same terms as perl itself. =cut