Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 721 lines (575 sloc) 17 KB
#!/usr/bin/perl
=head1 NAME
mojowka
=head1 DESCRIPTION
mojowka - is a L<Mojolicious::Lite> based lightweight wiki.
=head1 AUTHOR
Alexander Sapozhnikov
L<http://shoorick.ru/>
L<< E<lt>shoorick@cpan.orgE<gt> >>
2000-2010
=head1 LICENSE
This program is free software, you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
use utf8;
use Mojolicious::Lite;
use Mojo::ByteStream 'b';
use Text::Textile;
use DBI;
our $VERSION=0.01;
my $dbfile
= $ENV{'MOJOWKA_DATAFILE'}
|| 'data.sqlite';
our $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile", '', '');
our $txt = Text::Textile->new;
$txt->charset('utf-8');
# Routes
# any '/' => sub { shift->redirect_to( '/index' ) };
# For security reasons User can login only via POST
get '/login' => 'login';
post '/login' => sub {
my $self = shift;
my $login = $self->param('login') || '';
my $password = $self->param('password') || '';
$password = b($password)->md5_sum;
my $redirect
= $self->param('redirect')
# || $self->req->env('HTTP_REFERER')
|| '/';
if ( $login ) {
my $row = $dbh->selectrow_hashref(
q{SELECT id FROM user WHERE login=? AND password=?},
{},
$login, $password
);
if ( $row ) {
$self->session( 'login' => $login );
$self->flash( 'message' => 'Thanks for logging in' );
}
else {
$self->flash( 'message' => 'Wrong username or password' );
$redirect = '/login';
}
}
else {
$redirect = '/login';
} # else
$self->redirect_to( $redirect );
};
get '/logout' => sub {
my $self = shift;
$self->session( 'expires' => 1 );
my $redirect = $self->param('redirect') || '/';
$self->redirect_to( $redirect );
}; # => 'logout';
post '/preview' => sub {
my $self = shift;
my $text = $self->param('article') || return;
$self->render( 'text' => &markup( $text ) );
};
get '/sitemap' => sub {
my $self = shift;
my $rows = $dbh->selectcol_arrayref(
q{SELECT title FROM page ORDER BY LOWER(title)},
);
return undef unless $rows;
foreach my $row ( @$rows ) {
$row = b( $row )->decode('UTF-8')->to_string;
}
return $self->render(
'template' => 'sitemap',
'rows' => $rows,
);
};
get '/search/:query' => \&search;
get '/search' => \&search;
# Authentication
ladder sub { #same as under
my $self = shift;
# Authenticated
return 1
if $self->session( 'login' );
# Not authenticated
$self->flash( 'message' => 'You are not logged in' );
$self->redirect_to( '/login' );
return;
};
any '/edit/:id' => [ id => qr/\d+/ ] => \&edit_article;
any '/edit/:id' => [ id => 'new' ] => \&edit_article;
any '/create/(*title)' => \&edit_article;
any '/delete/:id' => [ id => qr/\d+/ ] => \&delete_article;
get '/userlist' => \&get_userlist;
# Other requests
ladder sub { 1 };
get '/profile/:login' => \&get_user;
get '/(*title)' => \&get_article;
get '/' => \&get_article;
app->types->type( 'html' => 'text/html; charset=utf-8' );
app->secret( 'mojowka' );
app->start;
END { my $rc = $dbh->disconnect }
=head1 FUNCTIONS
=head2 markup
Convert lightweight markup into HTML
=cut
sub markup {
local $_ = shift || return;
# [[internal links]]
s{
\[\[
(([^\]\|]+)\|)?
([^\]\|]+)
\]\]
}{
my $link = $2 || $3;
$link =~ tr/ /_/;
my $text = $3;
sprintf q{"%s":/%s}, $text, $link;
}gex;
return $txt->process( $_ );
} # sub markup
=head2 get_article
Get article by title
=cut
sub get_article {
my $self = shift;
my $title = $self->param('title') || '';
$title =~ s/\.html$//;
$title =~ tr/_/ /;
my $row = $dbh->selectrow_hashref(
q{SELECT * FROM page WHERE LOWER(title)=LOWER(?)},
{},
$title,
);
$self->render_text(
markup( b( $row->{'text'} )->decode('UTF-8')->to_string ),
'title' => b( $row->{'title'} )->decode('UTF-8')->to_string,
'layout' => 'default',
'id' => $row->{'id'},
) if $row;
} # sub get_article
=head2 edit_article
Draw editing form for article found by its ID
and process this form.
=cut
sub edit_article {
my $self = shift;
my $id = $self->param('id') || 0; # zero, natural number or word 'new'. See above
$id = 0 if $id eq 'new'; # digits only
my $article_title = $self->param('title') || '';
$article_title =~ s/\.html$//;
$article_title =~ tr/_/ /;
my $article_content = $self->param('article');
if ( $self->req->method eq 'POST' ) {
# Validate
my @errors = ();
TRY: {
# is title non-unique?
if (
$dbh->selectrow_hashref(
q{SELECT id FROM page WHERE LOWER(title)=LOWER(?) AND id!=?},
{},
$article_title, $id,
)
) {
push ( @errors, 'Title must be unique' );
last TRY;
} # if
# is content empty?
unless ( $article_content ) {
push ( @errors, 'Page content can not be empty' );
last TRY;
}
} # TRY
if ( @errors ) {
return $self->render(
'errors' => [ @errors ],
'template' => 'edit_article',
'title' => 'Errors occured',
'id' => $id,
'article_content' => $article_content,
'article_title' => $article_title,
);
} # if errors
# else OK
my $rows_affected = 0;
if ( $id ) { # edit
$rows_affected = $dbh->do(
q{UPDATE page SET title=?, text=? WHERE id=?},
{},
$article_title, $article_content, $id
)
} # if exists
else { # new page
$rows_affected = $dbh->do(
q{INSERT INTO page VALUES(NULL, ?, ?)},
{},
$article_title, $article_content,
)
} # else new
if ( $rows_affected ) {
$article_title =~ tr/ /_/;
$self->flash( 'message' => 'Changes was saved' );
$self->redirect_to( "/$article_title" );
}
# else NOT saved
push ( @errors, 'Can not save' );
return $self->render(
'errors' => [ @errors ],
'template' => 'edit_article',
'title' => 'Errors occured',
'id' => $id,
'article_content' => $article_content,
'article_title' => $article_title,
);
} # if POST
if ( $id ) { # existing record
my $row = $dbh->selectrow_hashref(q{SELECT * FROM page WHERE id=?}, {}, $id);
if ( $row ) {
my $article_title = b($row->{'title'})->decode('UTF-8');
return $self->render(
'template' => 'edit_article',
'title' => 'Editing',
'id' => $id,
'article_content' => b($row->{'text'} )->decode('UTF-8'),
'article_title' => $article_title,
'old_title' => $article_title,
);
} # if $row
} # if $id
else { # new record
$article_title =~ tr/_/ /;
return $self->render(
'template' => 'edit_article',
'title' => 'New page',
'id' => 0,
'article_content' => '',
'article_title' => $article_title,
);
} # else
} # sub edit_article
=head2 delete_article
Delete article specified by ID
=cut
sub delete_article {
my $self = shift;
my $id = $self->param('id') || return; # natural number only
my $rows_affected = $dbh->do(
q{DELETE FROM page WHERE id=?},
{},
$id,
);
if ( $rows_affected == 1 ) { # deleted without errors
$self->flash( 'message' => 'Page was deleted' );
$self->redirect_to( '/' );
} # if deleted
} # sub delete_article
=head2 get_user
Get user's profile
=cut
sub get_user {
my $self = shift;
my $login = $self->param('login') || return;
my $row = $dbh->selectrow_hashref(
q{SELECT text FROM user WHERE login=?},
{},
$login,
);
return $self->render_text(
markup( b( $row->{'text'} )->decode('UTF-8')->to_string ),
'title' => $login,
'layout' => 'default',
) if $row;
} # sub get_user
sub get_userlist {
my $self = shift;
my $login = $self->session( 'login' );
my $row = $dbh->selectrow_hashref(
q{SELECT 1 FROM user WHERE login=? AND is_admin=1},
{},
$login,
);
if ($row) {
my $rows = $dbh->selectall_arrayref(
q{SELECT login FROM user},
{ Slice => {} },
);
return $self->render(
'rows' => $rows,
'layout' => 'default',
'template' => 'userlist',
)
} else {
return $self->render_text(
markup( 'No rights for this' ),
'title' => 'error',
'layout' => 'default',
);
}
}
=head2 search
Search pages by title and content
=cut
sub search {
my $self = shift;
my $title = 'Search';
my $query = $self->param('query') || '';
my $rows = [];
if ( $query ) {
my $row = $dbh->selectcol_arrayref(
q{SELECT title FROM page WHERE LOWER(title)=LOWER(?)},
{},
$query,
);
if ( @$row ) {
$self->flash( 'message' => 'Exact matching found' );
$query = b( shift @$row )->decode('UTF-8')->to_string;
$query =~ tr/ /_/;
$self->redirect_to( "/$query" );
return;
} # if row
$rows = $dbh->selectcol_arrayref(
q{SELECT title FROM page WHERE LOWER(title) LIKE LOWER(?) OR LOWER(text) LIKE LOWER(?) ORDER BY LOWER(title)},
{},
"%$query%", "%$query%",
);
# TODO Change to gettext based proper singular/plural form
$title = 'Found ' . scalar @$rows . ' results';
foreach my $row ( @$rows ) {
$row = b( $row )->decode('UTF-8')->to_string;
}
} # if query
return $self->render(
'template' => 'search',
'query' => $query,
'rows' => $rows,
'title' => $title,
);
} # sub search
__DATA__
@@ userlist.html.ep
% layout 'default', 'title' => 'User list';
% foreach my $row (@$rows) {
<p><%= $row->{'login'} %></p>
% }
@@ login.html.ep
% layout 'default';
%# if ( my $login = session 'login' ) {
%# stash 'title' => 'You are already logged in';
%# }
%# else {
% stash 'title' => 'Login';
<form method="post">
<dl>
<dt>Username</dt><dd><input type="text" name="login"></dd>
<dt>Password</dt><dd><input type="password" name="password"></dd>
</dl>
<input type="submit" value="Login">
</form>
%# } # else
@@ sitemap.html.ep
% layout 'default', 'title' => 'Sitemap';
<ul>
% for my $row ( @$rows ) {
% my $link = $row;
% $link =~ tr/ /_/;
% $row ||= 'Main page';
<li><a href="/<%= $link %>"><%= $row %></a></li>
% }
</ul>
<h2>Special pages</h2>
<ul>
<li><a href="/search">Search</a></li>
<li><a href="/sitemap">Sitemap</a></li>
<li><a href="/login">Login</a></li>
</ul>
@@ search.html.ep
% layout 'default';
<form action="/search">
<input type="text" name="query" value="<%= $query %>">
<input type="submit" value="Find">
</dl>
</form>
<ol>
% for my $row ( @$rows ) {
% my $link = $row;
% $link =~ tr/ /_/;
% $row ||= 'Main page';
<li><a href="/<%= $link %>"><%= $row %></a></li>
% }
</ol>
@@ not_found.html.ep
% layout 'error';
% stash 'title' => 'Not Found - ERROR 404';
<h1>Not found</h1><p>Document you requested was not found.</p>
@@ edit_article.html.ep
% layout 'default';
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
// Tie onclick action to all .ajax links
$('a#preview_link').click(function() {
$.ajax({
url: '/preview',
type: 'post',
data: { article: $('#article').val() },
dataType: 'html',
success: function(data, textStatus) {
$('div#preview_area').html(data);
$('div#preview_area').show();
},
error: function() {
alert('Can not preview');
}
});
return false;
});
});
</script>
% if ( my $errors = stash 'errors' ) {
<ul id="errors">
% for my $error ( @$errors ) {
<li><%= $error %></li>
% }
</ul>
% } # if
<form method="post" action="/edit/<%= $id %>">
<dl>
<dt>Title</dt>
<dd><input type="text" name="title" value="<%= $article_title %>"></dd>
<dt>Content</dt>
<dd><textarea name="article" id="article"><%= $article_content %></textarea>
<br><a href="http://search.cpan.org/perldoc?Text::Textile#SYNTAX">textile syntax</a>
</dd>
</dl>
<input type="submit" value="Save">
<a href="#" id="preview_link">Preview</a>
% if ( my $old_title = stash 'old_title' ) {
<a href="/<%= $old_title %>">Cancel</a>
% }
% if ( $id ) {
<a href="/delete/<%= $id %>" id="delete_link"
onclick="return confirm('Do you really want to delete this page?')">Delete page</a>
% }
</form>
<div id="preview_area"></div>
@@ layouts/error.html.ep
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title><%= $title || 'ERROR' %></title>
<style type="text/css">body{background:#fec;color:#000}h1{color:#900}</style>
</head>
<body>
<%= content %>
<ul>
% my $article_title = param 'title';
<li><a href="/search/<%= $article_title %>">Search for “<%= $article_title %>”</a></li>
<li><a href="/sitemap">Sitemap</a></li>
% if ( session 'login' ) {
<li><a href="/create/<%= $article_title %>">Create page “<%= $article_title %>”</a></li>
% }
<li><a href="/">Main page</a></li>
</ul>
</body>
</html>
@@ layouts/default.html.ep
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head><title><%= $title || 'Welcome' %></title>
<link rel="shortcut icon" href="/favicon.ico">
<style type="text/css">
body {
background: #fff;
color: #000;
}
h1, h2, h3, h4, h5, h6 {
font-family: sans-serif;
}
@media screen {
body {
margin: 0;
padding: 1em;
}
h1 {
color: #223;
font-size: 140%;
}
h2 {
color: #335;
font-size: 120%;
}
h3 {
color: #446;
font-size: 100%;
}
#message {
border: 1px solid #fc0;
background: #fec;
padding: 1em;
}
#logged {
float: right;
display: inline;
margin: 0;
}
}
#logged li {
display: inline;
background: #eee;
border: 1px solid #ccc;
padding: 0.1em 0.5em;
}
#errors {
background: #fdc;
border: 1px solid #c00;
}
dd textarea {
width: 100%;
height: 7em;
}
#preview_area {
border: 1px solid #900;
margin-top: 1em;
padding: 1em;
background: #eee;
display: none;
}
#preview_link {
color: #656;
text-decoration: none;
border-bottom: 1px dashed #878;
}
#delete_link {
color: #900;
float: right;
}
@media print {
body {
font-family: serif;
}
#logged, #message {
display: none;
}
}
</style>
</head>
<body>
% if ( my $message = flash 'message' ) {
<div id="message"><%= $message %></div>
% }
% if ( my $login = session 'login' ) {
<ul id="logged">
<li>Logged as <a href="/profile/<%= $login %>"><%= $login %></a></li>
<li><a href="/logout">Logout</a></li>
% if ( my $id = stash 'id' ) {
<li><a href="/edit/<%= $id %>">Edit this page</a></li>
% }
</ul>
% }
<h1><%= $title %></h1>
<%= content %>
</body>
</html>