Move some discussion from comments page to here
[ikiwiki] / doc / plugins / write / tutorial.mdwn
1 This tutorial will walk you through [[writing|write]] your first ikiwiki
2 plugin.
3
4 What should the plugin do? Let's make it calculate and output the Fibonacci
5 sequence. To output the next number in the sequence, all a user has to do
6 is write this on a wiki page:
7
8         \[[!fib]]
9
10 When the page is built, the [[ikiwiki/directive]] will be
11 replaced by the next number in the sequence.
12
13 Most of ikiwiki's plugins are written in Perl, and it's currently easiest
14 to write them in Perl. So, open your favorite text editor and start
15 editing a file named "fib.pm".
16
17         #!/usr/bin/perl
18
19 This isn't really necessary, since fib.pm will be a Perl module, but it's
20 nice to have. Since it's a module, the next bit is this. Notice the "fib"
21 at the end, matching the "fib" in the filename.
22
23         package IkiWiki::Plugin::fib;
24
25 Now let's import a few modules. Warnings and strict are good ideas, but the
26 important one is the IkiWiki module.
27
28         use warnings;
29         use strict;
30         use IkiWiki 2.00;
31
32 Ok, boilerplate is out of the way. Now to add the one function that ikiwiki
33 expects to find in any module: `import`. The import function is called when
34 the module is first loaded; what modules typically do with it is
35 register hooks that ikiwiki will call later.
36
37         sub import {
38                 hook(type => "preprocess", id => "fib", call => \&preprocess);
39         }
40
41 This has hooked our plugin into the preprocess hook, which ikiwiki uses to
42 expand preprocessor [[directives|ikiwiki/directive]]. Notice
43 that "fib" has shown up again. It doesn't actually have to match the module
44 name this time, but it generally will. This "fib" is telling ikiwiki what
45 kind of preprocessor directive to handle, namely one that looks like this:
46
47         [[!fib ]]
48
49 Notice the `\&preprocess`? This is how you pass a reference to a function,
50 and the `preprocess` function is the one that ikiwiki will call to expand
51 the preprocessor directive. So, time to write that function:
52
53         sub preprocess {
54                 my %params=@_;
55                 return 1;
56         }
57
58 Whatever this function returns is what will show up on the wiki page.
59 Since this is the Fibonacci sequence, returning 1 will be right for the
60 first two calls anways, so our plugin isn't _too_ buggy. ;-) Before we fix
61 the bug, let's finish up the plugin.
62
63         1
64
65 Always put this as the last line in your Perl modules. Perl likes it.
66
67 Ok, done! If you save the plugin, you can copy it to a place your ikiwiki
68 looks for plugins (`/usr/share/perl5/IkiWiki/Plugins/` is a good bet; see
69 [[install]] for the details of how to figure out where to
70 install it). Then configure ikiwiki to use the plugin, and you're ready to
71 insert at least the first two numbers of the Fibonacci sequence on web
72 pages. Behold, the power of ikiwiki! ...
73
74 ----
75
76 You could stop here, if you like, and go write your own plugin that does
77 something more useful. Rather than leave you with a broken fib plugin
78 though, this tutorial will go ahead and complete it. Let's add a simple
79 Fibonacci generating function to the plugin. This is right out of a
80 textbook.
81
82         sub fib {
83                 my $num=shift;
84                 return 0 if $num == 1;
85                 return 1 if $num == 2;
86                 return fib($num - 1) + fib($num - 2);
87         }
88
89 And let's change the `preprocess` sub to use it:
90
91         my $last=0;
92
93         sub preprocess {
94                 my %params=@_;
95                 my $num=$last++;
96                 return fib($num);
97         }
98
99 Feel free to try it out with a simple page like this:
100
101         [[!fib ]], [[!fib ]], [[!fib ]], [[!fib ]], [[!fib ]]
102
103 Looks like it works ok, doesn't it? That creates a page that lists:
104
105         1, 1, 3, 5, 8
106
107 But what happens if there are two pages that both use fib? Try it out.
108 If ikiwiki builds both pages in one pass, the sequence will continue
109 counting up from one page to the next. But if that second page is modified
110 later and needs to be rebuilt, the sequence will start over from 1. This is
111 because `$last` only remembers what was output during the current
112 ikiwiki run.
113
114 But that's not the end of the strange behavior. Create a page that inlines
115 a page that uses fib. Now the inlined page will have one set of numbers,
116 and the standalone page another. The numbers might even skip over part of
117 the sequence in some cases.
118
119 Obviously, using a global `$last` variable was a bad idea. It would
120 work ok in a more regular cgi-based wiki, which only outputs one page per
121 run. But since ikiwiki is a wiki *compiler*, things are a bit more
122 complicated. It's not very hard to fix, though, if we do want the sequence
123 to start from 1 in every page that uses it.
124
125         my %last;
126
127         sub preprocess {
128                 my %params=@_;
129                 my $page=$params{destpage};
130                 my $num=$last{$page}++;
131                 return fib($num);
132         }
133
134 All this is doing is using a hash to store the last number on a per-page
135 basis. To get the name of the page that's being built, it looks in the
136 `%params` hash.
137
138 Ok, one more enhancement. Just incrementing the numbers is pretty boring.
139 It would be nice to be able to jump directly to a given point in the
140 sequence:
141
142         \[[!fib seed=20]], [[!fib ]], [[!fib ]]
143
144 Just insert these lines of code inside `preprocess`, in the appropriate
145 spot:
146
147                 if (exists $params{seed}) {
148                         $last{$page}=$params{seed}-1;
149                 }
150
151 But this highlights another issue with the plugin. The `fib()` function is
152 highly recursive and gets quite slow for large numbers. If a user enters
153 seed=1000, it will run for a very long time, blocking ikiwiki from
154 finishing. This denial of service attack also uses an ever-increasing
155 amount of memory due to all the recursion. 
156
157 Now, we could try to fix `fib()` to run in constant time for any number,
158 but that's not the focus of this tutorial. Instead, let's concentrate on
159 making the plugin use the existing function safely. A good first step would
160 be a guard on how high it will go.
161         
162         my %last;
163
164         sub preprocess {
165                 my %params=@_;
166                 my $page=$params{destpage};
167                 if (exists $params{seed}) {
168                         $last{$page}=$params{seed}-1;
169                 }
170                 my $num=$last{$page}++;
171                 if ($num > 25) {
172                         error "can only calculate the first 25 numbers in the sequence";
173                 }
174                 return fib($num);
175         }
176
177 Returning an error message like this is standard for preprocessor plugins,
178 so that the user can look at the built page and see what went wrong.
179
180 Are we done? Nope, there's still a security hole. Consider what `fib()`
181 does for numbers less than 1. Or for any number that's not an integer. In
182 either case, it will run forever. Here's one way to fix that:
183
184                 if (int($num) != $num || $num < 1) {
185                         error "positive integers only, please";
186                 }
187
188 As these security problems have demonstrated, even a simple input from the
189 user needs to be checked thoroughly before being used by an ikiwiki plugin.