This tutorial will walk you through writing your first ikiwiki plugin.
What should the plugin do? Let's make it calculate and output the Fibonacci sequence. To output the next number in the sequence, all a user has to do is write this on a wiki page:
[[!fib ]]
When the page is built, the directive will be replaced by the next number in the sequence.
Most of ikiwiki's plugins are written in Perl, and it's currently easiest to write them in Perl. So, open your favorite text editor and start editing a file named "fib.pm".
#!/usr/bin/perl
This isn't really necessary, since fib.pm will be a Perl module, but it's nice to have. Since it's a module, the next bit is this. Notice the "fib" at the end, matching the "fib" in the filename.
package IkiWiki::Plugin::fib;
Now let's import a few modules. Warnings and strict are good ideas, but the important one is the IkiWiki module.
use warnings;
use strict;
use IkiWiki 3.00;
Ok, boilerplate is out of the way. Now to add the one function that ikiwiki
expects to find in any module: import
. The import function is called when
the module is first loaded; what modules typically do with it is
register hooks that ikiwiki will call later.
sub import {
hook(type => "preprocess", id => "fib", call => \&preprocess);
}
This has hooked our plugin into the preprocess hook, which ikiwiki uses to expand preprocessor directives. Notice that "fib" has shown up again. It doesn't actually have to match the module name this time, but it generally will. This "fib" is telling ikiwiki what kind of preprocessor directive to handle, namely one that looks like this:
[[!fib ]]
Notice the \&preprocess
? This is how you pass a reference to a function,
and the preprocess
function is the one that ikiwiki will call to expand
the preprocessor directive. So, time to write that function:
sub preprocess {
my %params=@_;
return 1;
}
Whatever this function returns is what will show up on the wiki page. Since this is the Fibonacci sequence, returning 1 will be right for the first two calls anways, so our plugin isn't too buggy. Before we fix the bug, let's finish up the plugin.
1
Always put this as the last line in your Perl modules. Perl likes it.
Ok, done! If you save the plugin, you can copy it to a place your ikiwiki
looks for plugins (/usr/share/perl5/IkiWiki/Plugins/
is a good bet; see
install for the details of how to figure out where to
install it). Then configure ikiwiki to use the plugin, and you're ready to
insert at least the first two numbers of the Fibonacci sequence on web
pages. Behold, the power of ikiwiki! ...
You could stop here, if you like, and go write your own plugin that does something more useful. Rather than leave you with a broken fib plugin though, this tutorial will go ahead and complete it. Let's add a simple Fibonacci generating function to the plugin. This is right out of a textbook.
sub fib {
my $num=shift;
return 0 if $num == 0;
return 1 if $num == 1;
return fib($num - 1) + fib($num - 2);
}
And let's change the preprocess
sub to use it:
my $last=0;
sub preprocess {
my %params=@_;
my $num=++$last;
return fib($num);
}
Feel free to try it out with a simple page like this:
[[!fib ]], [[!fib ]], [[!fib ]], [[!fib ]], [[!fib ]]
Looks like it works ok, doesn't it? That creates a page that lists:
1, 1, 2, 3, 5
But what happens if there are two pages that both use fib? Try it out.
If ikiwiki builds both pages in one pass, the sequence will continue
counting up from one page to the next. But if that second page is modified
later and needs to be rebuilt, the sequence will start over from 1. This is
because $last
only remembers what was output during the current
ikiwiki run.
But that's not the end of the strange behavior. Create a page that inlines a page that uses fib. Now the inlined page will have one set of numbers, and the standalone page another. The numbers might even skip over part of the sequence in some cases.
Obviously, using a global $last
variable was a bad idea. It would
work ok in a more regular cgi-based wiki, which only outputs one page per
run. But since ikiwiki is a wiki compiler, things are a bit more
complicated. It's not very hard to fix, though, if we do want the sequence
to start from 1 in every page that uses it.
my %last;
sub preprocess {
my %params=@_;
my $page=$params{destpage};
my $num=++$last{$page};
return fib($num);
}
All this is doing is using a hash to store the last number on a per-page
basis. To get the name of the page that's being built, it looks in the
%params
hash.
Ok, one more enhancement. Just incrementing the numbers is pretty boring. It would be nice to be able to jump directly to a given point in the sequence:
[[!fib seed=20]], [[!fib ]], [[!fib ]]
Just insert these lines of code inside preprocess
, in the appropriate
spot:
if (exists $params{seed}) {
$last{$page}=$params{seed};
}
But this highlights another issue with the plugin. The fib()
function is
highly recursive and gets quite slow for large numbers. If a user enters
seed=1000, it will run for a very long time, blocking ikiwiki from
finishing. This denial of service attack also uses an ever-increasing
amount of memory due to all the recursion.
Now, we could try to fix fib()
to run in constant time for any number,
but that's not the focus of this tutorial. Instead, let's concentrate on
making the plugin use the existing function safely. A good first step would
be a guard on how high it will go.
my %last;
sub preprocess {
my %params=@_;
my $page=$params{destpage};
if (exists $params{seed}) {
$last{$page}=$params{seed}-1;
}
my $num=++$last{$page};
if ($num > 25) {
error "can only calculate the first 25 numbers in the sequence";
}
return fib($num);
}
Returning an error message like this is standard for preprocessor plugins, so that the user can look at the built page and see what went wrong.
Are we done? Nope, there's still a security hole. Consider what fib()
does for numbers less than 0. Or for any number that's not an integer. In
either case, it will run forever. Here's one way to fix that:
if (int($num) != $num || $num < 0) {
error "positive integers only, please";
}
As these security problems have demonstrated, even a simple input from the user needs to be checked thoroughly before being used by an ikiwiki plugin.