ONLamp.com
oreilly.comSafari Books Online.Conferences.

advertisement


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.

Pages: 1, 2, 3, 4

Next Pagearrow





Sponsored by: