How to make a simple Foswiki Plugin

Introduction

This topic is written for the absolute beginner that would like to write a simple plugin for Foswiki that implements a new Foswiki variable.

At first it seems a bit intimidating but it is actually quite easy once you are passed the first few steps.

This Foswiki topic will help you through all the steps in a real "Foswiki Plugin for dummies style".

It is a supplement to the already existing and very good topics ExtensionDeveloperGuide and SpecifyingConfigurationItemsForExtensions

What is a Foswiki Plugin?

A Foswiki Plugin is some Perl code called when you either view, edit, or save a page.

Throughout the Foswiki code a number of handlers are called. A handler is a call to a subroutine (function) in each installed plugin. If a Plugin has defined that particular handler it is run. If not, Foswiki continues to the next plugin.

The order that plugin handlers are called is:
  • First, the order defined by the configure parameter {PluginsOrder}. By default SpreadSheetPlugin is the only plugin in this list so it gets called before any other plugin.
  • Then, alphabetical order. This means that it is not un-important what you name your plugin. AaaaaBeautifulPlugin, despite looking like a personal ad in a newspaper, will be executed before anything else.
There is not at this time a very good overview of which handler gets called when. StepByStepRenderingOrder is currently the best we have but, as the author admits, it could be better. And it will be one day.

However, this topic promised to describe simple plugins. And this is where we leave the handlers because there are simpler and often better ways to make a plugin than using a handler.

The other thing plugins can do is define new Foswiki macros. If you look at very old plugins you will see that they handle the new variables in the commonTagsHandler. This is sometimes necessary still. But if your plugin simply defines a Foswiki macro and turns it into something smart then you should not use the commonTagsHandler. Instead, use the newer and much more efficient Foswiki::Func::registerTagHandler method.

The Foswiki::Func::registerTagHandler ensures that:
  • Your new Foswiki macro is treated and executed same way as a normal internal Foswiki macro. Left to right, inside out.
    Example in %FOUR{"%ONE{}%"}% %FIVE{"%TWO{}% and "%THREE{}%"}% the variables are rendered in the order of the numeric words.
  • The code executes much faster and only if the new plugin defined Foswiki Macro is used inside a given topic
This topic will explain how to write a simple plugin that uses the Foswiki::Func::registerTagHandler only.

The minimum requirements of a plugin

For a plugin to work it must:
  • Have a Perl code file in lib/Foswiki/Plugins called something that ends with Plugin.pm. Example MyownPlugin.pm
    • The plugin module file must contain an initPlugin subroutine which is successful in running and returns 1.
    • Must have a line saying package Foswiki::Plugins::MyownPlugin; using MyownPlugin as example
    • use vars qw( $VERSION ); statement and a line defining $VERSION. Declare these global variables using "our" statements
      • our $VERSION;
      • our $RELEASE;
      • our $SHORTDESCRIPTION;
      • our $NO_PREFS_IN_TOPIC;
  • Have a topic in data/System with the same name as the plugin. MyownPlugin.txt. This topic can be totally blank but it has to exist.

DumbPlugin part 1

So let us write a plugin called DumbPlugin because it is absolutely dumb and does absolutely nothing.

First we make a naked lib/Foswiki/Plugins/DumbPlugin.pm
package Foswiki::Plugins::DumbPlugin;
use strict;

our $VERSION = '0.1';
our $RELEASE = '0.1';
our $SHORTDESCRIPTION = 'Dumb plugin that does nothing at all';
our $NO_PREFS_IN_TOPIC = 0;
our $pluginName = 'DumbPlugin';

sub initPlugin {
    my( $topic, $web, $user, $installWeb ) = @_;

    return 1;
}

and a very shaved down plugin topic in: data/System/DumbPlugin.txt
---+ !Dumb Plugin

I do nothing

---++ Syntax Rules

I have no syntax

---++ Settings

   * Set SHORTDESCRIPTION = A really dumb plugin

---++ Plugin Info

|  Plugin Author: | Foswiki:Main.KennethLavrsen |
|  Copyright: | ? 2008, Kenneth Lavrsen |
|  License: | GPL ([[http://www.gnu.org/copyleft/gpl.html][GNU General Public License]]) |
|  Release: | %$RELEASE% |
|  Version: | %$VERSION% |
|  Change History: | <!-- versions below in reverse order -->&nbsp; |
|  16 Aug 2008: | Initial version |
|  Foswiki Dependency: | $Foswiki::Plugins::VERSION 1.1 |
|  CPAN Dependencies: | none |
|  Other Dependencies: | none |
|  Perl Version: | 5.006 |
|  Plugin Home: | http://foswiki.org/Extensions/%TOPIC% |
|  Feedback: | http://foswiki.org/Development/%TOPIC% |

__Related Topics:__ [[%SYSTEMWEB%.Plugins][Plugins]], %SYSTEMWEB%.DeveloperDocumentationCategory, %SYSTEMWEB%.AdminDocumentationCategory, %SYSTEMWEB%.DefaultPreferences

That's it! Now let's activate the plugin and try it out:
  1. Open the configure script in the browser. Under Plugins find your DumbPlugin and activate it.
  2. Now when you look up the InstalledPlugins topic in the System web you will see the DumbPlugin listed, hopefully without any errors...
That is your minimum plugin. That was easy, wasn't it? You just needed to know how to get started, and then it is not really difficult.

In next section we will make the dumb plugin a little less dumb.

DumbPlugin part 2 - Hello World

Let us make the plugin register a new Foswiki macro called HELLOWORLD which returns "Hello World".

We do two things:
  1. In the initPlugin we add
    Foswiki::Func::registerTagHandler( 'HELLOWORLD', \&_HELLOWORLD );
  2. We create a new subroutine called _HELLOWORLD which is the subroutine that gets called when Foswiki sees a %HELLOWORLD% tag.
We could call the subroutine anything we want but it is convention to name an internal function with an initial underscore. And why not name the routine the same as your variable? wink

So here is the hello world example:
package Foswiki::Plugins::DumbPlugin;
use strict;

our $VERSION = '0.1';
our $RELEASE = '0.1';
our $SHORTDESCRIPTION = 'Dumb plugin that does nothing at all';
our $NO_PREFS_IN_TOPIC = 0;
our $pluginName = 'DumbPlugin';

sub initPlugin {
    my( $topic, $web, $user, $installWeb ) = @_;

    Foswiki::Func::registerTagHandler( 'HELLOWORLD', \&_HELLOWORLD );

    return 1;
}

sub _HELLOWORLD {
    my($session, $params, $theTopic, $theWeb) = @_;
    
    return "Hello World";
}

Now try putting a %HELLOWORLD% in a topic. You should see the variable replaced by the text, "Hello World".

DumbPlugin part 2 - Hello Parameters

So far our dumb plugin has not been able to do more than we could have done with a Set in a topic. But we have to learn to crawl before we can walk: let's take the first real steps by adding a new Foswiki macro called HELLOSOMEONE and giving it some options.

It is a good idea not to invent new strange syntaxes for Foswiki macros. The standard syntax is %VARIABLE{"some value" parameter1="some text" parameter2="some other text"}%. By always using this syntax your users will see your new variables work like all the internal variables. The Foswiki::Func::registerTagHandler handles all this for you in a very easy way.

When the variable handler is called it is called with a parameter which is a reference to the Foswiki::Attrs object containing all the given parameters. Sounds like voodoo? Don't worry. It is to me to. But it is dead easy to use. Just follow these examples.

When you define your variable handler always use this syntax like I used in the examples already

sub _HELLOWORLD {
my($session, $params, $theTopic, $theWeb) = @_;

The $session parameter a reference to the Foswiki session object. You will often ignore this in simple plugins.

The $params parameter is the reference to a Foswiki::Attrs object containing parameters. This can be used as a simple hash that maps parameter names to values, with _DEFAULT being the name for the default parameter. Let's see how simple this is to use by examples:

Someone has written this in a topic: %HELLOSOMEONE{"Mom" someoneelse="Dad" yetanother="Sister"}%

my $hellovariable = $params->{_DEFAULT}; would set $hellovariable = Mom

my $some = $params->{someoneelse}; would set $some = Dad

my $yet = $params->{yetanother}; would set $yet = Sister

my $ehm = $params->{ehm}; would leave $ehm = undefined because it is not defined in the HELLOSOMEONE variable.

You will often find that not all variables are always used. You will end up with errors or at least a flood of warnings in the Apache log if you do not always give some good default value for when a parameter is not used.

my $ehm = $params->{ehm} || ' '; is a simple way to ensure $ehm is defined to an empty string.

The last two parameters the variable handler is called with are straight forward the current topic and web name.

OK. Let us enhance the dumb plugin so it registers a new variable HELLOSOMEONE.
package Foswiki::Plugins::DumbPlugin;
use strict;

our $VERSION = '0.1';
our $RELEASE = '0.1';
our $SHORTDESCRIPTION = 'Dumb plugin that does nothing at all';
our $NO_PREFS_IN_TOPIC = 0;
our $pluginName = 'DumbPlugin';

sub initPlugin {
    my( $topic, $web, $user, $installWeb ) = @_;

    Foswiki::Func::registerTagHandler( 'HELLOWORLD', \&_HELLOWORLD );
    Foswiki::Func::registerTagHandler( 'HELLOSOMEONE', \&_HELLOSOMEONE );

    return 1;
}

sub _HELLOWORLD {
    my($session, $params, $theTopic, $theWeb) = @_;
    
    return "Hello World";
}

sub _HELLOSOMEONE {
    my($session, $params, $theTopic, $theWeb) = @_;
    
    my $defaulttext = $params->{_DEFAULT} || '';
    my $someoneelse = $params->{someoneelse} || '';
    my $yetanother = $params->{yetanother} || '';
    
    my $text = '';
    $text .= " $defaulttext" if $defaulttext;
    $text .= " and" if ($text && $someoneelse);
    $text .= " $someoneelse" if $someoneelse;
    $text .= " and" if ($text && $yetanother );
    $text .= " $yetanother" if $yetanother;
    $text = "Hello" . $text;
    
    return $text;
}

Our dumb plugin can now say hello to up to 3 people. And you can leave out anyone and still get a sensible result.

Examples:
  • %HELLOSOMEONE{"Mom" someoneelse="Dad" yetanother="Sister"}%
  • %HELLOSOMEONE{someoneelse="Dad" yetanother="Sister"}%
It is still a very dumb plugin but I think you can build on from here making much more advanced stuff.

Plugin Preference Settings

You can define plugin settings two ways:
  1. You can define a new option in configure
  2. You can define a setting in the plugin topic
There are advantages and disadvantages of the two methods.

Options defined in configure have a huge speed advantage over options defined in the plugin topic. So if your value is a static value that only an admin would ever change and only once - define it as a configure value.

If the value is the path for an external script used by the plugin then for sure you should define the setting as a configure value for security reasons.

If the setting is one you will need to redefine on a web basis or a setting users will benefit from being able to alter maybe even at topic level then you need to use a topic defined setting.

Plugin setting defined in configure

Adding a configure setting is very simple.

Your plugin must simply include a file lib/Foswiki/Plugins/DumbPlugin/Config.spec (using DumbPlugin as example) with a definision of the setting. See section "Structure of a Config.spec file" in TWikiPlugins for a good description how to do this.

Example
#---+ Plugins
#---++ DumbPlugin
# The Dumb Plugin can just say Hello
# **STRING 30**
# Options the Hello word
$Foswiki::cfg{Plugins}{DumbPlugin}{Greeting} = 'Hello';

After having defined the value in configure the plugin can now access the value simply by

$greeting = $Foswiki::cfg{Plugins}{DumbPlugin}{Greeting} || "Hello";

See also Foswiki:Development.SpecifyingConfigurationItemsForExtensions DevelopingPlugins for a good detailed description of how to define the plugin configure files.

If you ONLY use configure defined variables then you can define

$NO_PREFS_IN_TOPIC = 1;

Instead of setting it 0 as shown in the previous examples. This makes the plugin not trying to find settings in topics. You win some performance by doing this.

Plugin settings defined in topics

You can define the setting directly in the Plugin topic:

  • Set GREETING = Hello
You can also define it in Main.SitePreferences:

  • Set DUMBPLUGIN_GREETING = Allo
The Main.SitePreferences overrides the setting in the Plugin topic.

To use this setting throughout the plugin simple add a global variable like this where we add $greeting

use vars qw( $VERSION $RELEASE $SHORTDESCRIPTION $debug $pluginName $NO_PREFS_IN_TOPIC $greeting );

and in initPlugin subroutine add

$greeting = Foswiki::Func::getPreferencesValue( 'DUMBPLUGIN_GREETING' ) || "Hello";

Use EmptyPlugin as your starting point

I have shown how to make the most basic plugin without any comments or any extras. But in the real world you should not start from scratch.

Instead start your plugin based on the distributed plugin called EmptyPlugin. This plugin is also quite dumb to start with but it contains a lot of documentation in the shape of comments that helps you getting the job done. It also contains all the handlers that exist in Foswiki except the subroutines for these have been renamed by prefexing them DISABLE_. If you need to use a handler simply rename is by removing the DISABLE_ prefix and you are set to go. Even if you only use the Foswiki::Func::registerTagHandler it is still a good idea to always start with the EmptyPlugin for later extensions.

Always use the official API

You will soon need to do stuff which is more advanced than plain Perl and you will need information from the rest of Foswiki or you will need to store data in files.

ALWAYS ALWAYS use the Foswiki::Func module as your interface to Foswiki. Foswiki::Func is the official plugin API for Foswiki. It is the only part of Foswiki you can trust not to be changed - only enhanced - from Foswiki version to Foswiki version. If you want your plugin to work after next Foswiki upgrade always use only the functions in Foswiki::Func.

If you need to use code from elsewhere in Foswiki because your needed function is not in Foswiki::Func then it is better to copy the few lines of code to your plugin than to directly call other Foswiki modules. Same with accessing data in the Foswiki Session object. Get the data through Foswiki::Func even if it is also available in the Session object because it will very well have changed in the next Foswiki version.

Executing external programs and accessing the file system

Plugins are often used to fetch data by calling external programs. This is easy to do in Perl. It is also very dangerous if you do not take care.

ALERT! Never trust any data you get from the browser. Never trust that the 3 values you put in a form are the only values your program will see. There are web development plugins for Firefox that enables even small kids to submit forms with values you never intended should be possible. Always filter the input from the browser. If you have to call different external scripts based on user inputs always make "if" statements testing for those 2-3 possible values and ignore everything else. Never let data from user input be part of a filename you open unless you have filtered off anything that is not plain alphanumerical characters.

It is not difficult to write a safe plugin that takes user data as long as you think like an attacker and avoid using user inputs directly for the names of files or external programs.

Also Foswiki::Meta and Foswiki::Sandbox are considered part of the official API. The 3 APIs are described in Foswiki::Func, Foswiki::Meta, and Foswiki::Sandbox. The PluginsApiPolicies describes the official Foswiki API policy.

Please publish your plugin

You got Foswiki for free. You have access to more than 300 plugins for free. If you write a plugin, even a simple one, please consider contributing it back to the community. The Foswiki comminity appreciates this very much!

What about those handlers?

That will be the subject for a new How To tutorial which will eventually be added to the suite of supplemental documents.

We hope this topic was useful to you. Please provide feedback on how we can improve it further and please feel free to directly edit this topic.
Topic revision: r7 - 25 Jul 2019, MichaelBuchholz
The copyright of the content on this website is held by the contributing authors, except where stated elsewhere. See Copyright Statement. Creative Commons License    Legal Imprint    Privacy Policy