Slash's Wiki Plugin
Pages: 1, 2, 3, 4
The update() Function
This function is a little brawnier. As described above, its
core purpose in life is to insert something into the
wikitext table and to update or to insert something into
the wikipage table. There's a twist, though.
Instead of forcing people to create new page revisions to
correct errors that only appear after something is published
(may this not be prophetic!), the editing page adds a preview
button that does everything except the database work. It's a
simple flag, but it does complicate things slightly.
The first thing to do is to set up some common variables.
The first few are standard for all plugin programming tasks.
This time, getCurrentUser() is used to fetch a
hash that contains information pertaining to the current user.
It'll come in handy in a moment. The other variables come from
the form parameters submitted by the user.
my $form = shift;
my $user = getCurrentUser();
my $slashdb = getCurrentDB();
my $constants = getCurrentStatic();
my ($raw, $description, $title, $wid) =
@$form{qw( raw description title wid )};
There's a little verification of the data to prevent obviously bad submissions. The Wiki page identifier and the version numbers must be completely numeric. The title must have a word character in it. Otherwise, there will be no page displayed. This will only happen if someone's been messing with forms or if the templates are completely broken. This is terribly user unfriendly, but many better examples leave error handling as an exercise for the reader. Patches welcome. This is also the place to strip carriage returns from the raw text, as they may be introduced by certain Web browsers and tend to have deleterious effects:
return if ($wid =~ /\D/ or $title !~ /\w/ or $version =~ /\D/);
$raw =~ tr/\r//d;
This is also the point to add access controls, perhaps
checking if the user has an Author flag again, or if her seclev
meets or exceeds one stored in the mythical configuration value
$constants->{wikiWriteSeclev}.
The next important job is to format the wiki text. This is
done with a call to Text::WikiFormat::format(). We
pass in several parameters. The first is the text to format.
The second is an optional hash reference of formatting
directives. As we're doing HTML for now, the defaults just
work. The final argument is a hash reference of extra options,
of which we have two. prefix sets the prefix for
all HTML links -- this is used to create base URLs linking to
other pages within the Wiki. extended allows or
disallows extended linking semantics. This, along with
everything else pulled out of $constants is a
Slash configuration variable, and can be changed through the
Web interface. By default, it's off, so if you write
[CatLover|Michelle] expecting a link, you'll be
disappointed. The formatting call is:
my $cooked = Text::WikiFormat::format($raw, {}, {
prefix => "$constants->{rootdir}/wiki.pl?page=",
extended => $constants->{wikiExtendedlinks},
});
Now that we have the raw text (in Wiki format) and the
cooked text (in HTML), we have a choice to make. If the user
hit the preview button, the $preview variable will
be defined, and we shouldn't store everything in the database.
Otherwise, we have some database action to perform.
There's yet another choice if we need to update the
database. Is this a new page or an existing page? If the page
already exists, we'll have a Wiki page identifier in
$wid and must call sqlUpdate() on the
wikipage table. Otherwise, we'll have to use
sqlInsert() on wikipage.
my ($method, $where);
if ($wid) {
$where = "wid = $wid AND title = " .
$slashdb->sqlQuote($title);
$method = 'sqlUpdate';
$version = $slashdb->sqlSelect('version',
'wikipage', $where) + 1;
} else {
$wid = $slashdb->sqlSelect('MAX(wid)+1','wikipage');
$where = '';
$method = 'sqlInsert';
}
$version ||= 1;
my $rows = $slashdb->$method('wikipage', {
title => $form->Slash's Wiki Plugin,
version => $version,
}, $where) or warn "Could not update database\n";
There are several important things to note. First, the method name is determined dynamically. This makes the code shorter, but it can throw people who aren't used to pushing the limits of method dispatch. (Not that Damian Conway would be impressed, but he's not the target reader this time.)
Next, be aware that the code goes through a couple of
contortions to make sure to get a good version number and a
good wid. Since these are form the key for the
wikitext table, they are very important. Because
the text table keeps around several versions associated with a
single wid, we can't rely on the magical
auto_increment on that field in the page table. It's still
clearer to leave it that way in the schema, though. Unfortunately,
there's a potential race condition between the two database actions.
On a busy site, this would be a candidate for some form of table locking,
or perhaps a single SQL statement that could be executed in one go.
After the wikipage table is updated, it's time
to insert a new row into the wikitext table. This
is much easier, as all of the data is already provided:
$slashdb->sqlInsert('wikitext', {
raw => $raw,
wid => $wid,
uid => $user->{uid},
-date => 'now()',
cooked => $cooked,
version => $version,
description => $description,
}) or warn "Could not update database\n";
Only two things are slightly different. The
$user->{uid} access grabs the unique identifier
associated with the current user. This is, of course, used to
join to the user tables to find out who created the current
version. The date parameter is also unique -- it's
actually a MySQL function to get the current database time. The
leading dash on the parameter name tells the
sqlInsert() function not to quote the value. It will be
treated as a function call and not a literal string.
All that's left to do is to display information. We can reuse the wikiView template, but we're missing two pieces of information: the time of the update and the nickname of the user performing the update. That's not a problem, as they're both available:
header("SlashWiki: $title");
slashDisplay('wikiView', {
raw => $raw,
wid => $wid,
date => timeCalc($slashdb->getTime()),
title => $title,
cooked => $cooked,
nickname => $user->{nickname},
description => $description,
});
footer();
The
list() Function
By far the ugliest of the SlashWiki functions (patches
welcome), list() handles two different kinds of
lists. First, it lists several of the most recently modified
Wiki pages. This is the default behavior. Given a valid
page parameter, it also can list the various revisions
of a Wiki page. Things start out as you should be
expecting:
my $form = shift;
my $slashdb = getCurrentDB();
my $constants = getCurrentStatic();
my $page= $form->{page} || '';
The next step is to decide which of the two behaviors to
perform. This is obviously done on the truth of
$page. There's no reason it couldn't also be done with a
$wid parameter, but it doesn't currently do
that.
If there's no page, we need to find the last several pages
that have been modified. That's why we keep the date in the
wikitext table. We'll need to pull in the
wikipage table to get titles, and the users
table to get user nicknames (to affix blame). For the sake of
flexibility, we'll allow the user or the administrator to
determine how many pages to display. First, we prefer a form
variable named recentLimit and then a
configuration variable named wikiRecentLimit. If
neither is a valid positive integer, we'll use 25:
my $recents = $form->{recentLimit} || $constants->{wikiRecentLimit};
$recents = 25 unless $recents and $recents !~ /\D/;
Next we display a header, reflecting what we're doing, and interpolating the number of pages. Something so small just seems so neat:
header("SlashWiki: Latest $recents Modified Pages");
The next step is to perform a gigantic SQL selection to grab all the data we need, to sort it and to limit it to the results we want. Make sure the kids are out of the room:
$slashdb->sqlSelectAllHashrefArray(
'wikipage.version, title, date, description, nickname',
'wikipage, wikitext, users',
'wikipage.wid = wikitext.wid AND users.uid = wikitext.uid ' .
'AND wikipage.version = wikitext.version',
"order by date limit $recents")
Unlike some of the other database operations, this time we
want a metric boatload of results. They'll be sent to the
template as an array of hash references. (The code itself
performs one more manipulation to coerce the dates into the
proper format with timeCalc(), but you probably
really don't want to see that. It ought to be buried at sea
near Innsmouth.) The results are assigned to
$pagelist, which is actually a reference to the
array.
The last trick is to determine which template to use. This will be important in a moment:
$template = 'wikiRecent';
We'll return to the display briefly, but must return to the
code to display a revision list. As with its cousin, the number
of revisions to display will come from the
revisionLimit form variable, the
wikiRevisionLimit, or the sane default of 25. There's
another nice interpolated header and another nasty SQL
statement, which has also had surrounding Stygian nastiness
edited for your protection:
$slashdb->sqlSelectAllHashrefArray(
'wikitext.version, title, description, date, nickname',
'wikipage, wikitext, users',
'wikipage.wid = wikitext.wid AND users.uid = wikitext.uid ' .
"AND title = " . $slashdb->sqlQuote($page),
"order by version DESC limit $revs>
)
The difference here is that we're concerned about versions
and a single title. This is the place to change things to pull
by wid, if that's important. The results are also
assigned to an anonymous array pointed to by
$pagelist, after the appropriate date manipulation. A
final assignment chooses the correct template:
$template = 'wikiRevisions';
After all that work, displaying things is easy. It doesn't
matter that $page may be empty, as you'll see when
we discuss the templates:
slashDisplay($template, {
page => $page,
pagelist =" $pagelist,
});
footer();
This function could be improved somewhat. On a site with an
excessive number of revisions or of available pages, there's a
potential denial of service attack, if someone were to request
a huge revisionLimit or recentLimit.
There are no UI widgets for this, but the potential still
exists.
Also, the SQL commands can use up a lot of memory. It may be
better to use sqlSelectMany() to prepare a
statement handle, passing that to the templates instead of
pulling all results into memory. The general Slash database
philosophy is that database access is slow, so it prefers to
trade memory for speed. This is worth considering, however.
Templates
The Slash Wiki comes with seven templates to customize page display. This makes it much easier to separate formatting from logic. As well, the template language is simple and effective, and really beats embedding HTML in applets all to pieces.
While the templates are very powerful, there are only two
tricks to using them in Slash. The first is knowing how to use
slashDisplay(). As demonstrated before, its first
argument is the name of the template to use. The second
argument is a reference to a hash of data to be passed to the
template. The third argument is extra options, and it's not
used terribly often.
The second trick is that templates can be associated with
pages. That is, there's a template named
header;wiki;default. Its name is header, it
is associated with the wiki page (or applet), and
it belongs to the default section. There's also a
header associated with the misc page
and another belonging to the light section. We
don't have to do anything specific to call the Wiki templates,
and there are no name collisions as long as no one adds another
wiki.pl applet.
The header Template
This template begins the HTML page sent to a browser. It also governs the boxes down the left side of a page. For style points, the Slash Wiki adds a SlashWiki menu box. The bulk of this template is stolen from the miscellaneous header template, but a few lines are different:
[% IF constants.run_ads && constants.wikiRunAds %]
<!-- ad code -->
[% END %]
This is a modification of the standard header, and is used
to display banner ads on pages. The wikiNoAds
variable allows ads to be suppressed. Even if you run ads, you
may make the wiki available only to administrative users, and
may not want the distraction of flashing monkeys. (You have the
option, though, if you only run ads for the Kudra world
domination fund.)
[% UNLESS title; title = "SlashWiki Menu"; END %]
[% contents = PROCESS wikiMenu %]
[% PROCESS fancybox
width = 100
%]
At heart, it simply calls the wikiMenu
template, assigning the results to the contents
variable and then calls the fancybox template,
passing a width parameter (which feels like a
kludge). fancybox expects title and
contents parameters as well, and they're visible,
with a default title if none has been provided.
This draws a nice little menu.