Skip to Content.
Sympa Menu

Template plugins

The template plugins system extends template customization capabilities, it allows you to run your own code to add content or do some processing in templates.

It is then possible to add links to a menu based on privileges from another backend, get data and render it from a webservice and much more.

A plugin consists of a single perl package located in the $MODULEDIR/Sympa/Template/Plugin directory under Sympa's root.

The plugin is called in templates with a simple [% USE myPlugin %], it is possible to pass arguments ([% USE myPlugin("foo") %]), more examples to follow.

This package must use Template::Plugin as base and provide at least 2 subroutines :

Subroutine Called when Basic mode Singleton mode
Arguments Must return Arguments Must return
load Plugin is required by perl, that is when first used Package name and context object Package name Package name and context object Blessed reference to package instance
new Each time the plugin is USEd in templates Package name, context object and given arguments Blessed reference to package instance Blessed instance returned by initial load call, context object and given arguments The blessed instance given in arguments

In the basic mode each time the plugin is USEd a new instance is created and returned, in the singleton mode a single instance is created and return each time the plugin is USEd, it can be useful when the plugin connects to a database to use only one socket ...

Plugin skeleton

#!/usr/bin/perl
 
package Sympa::Template::Plugin::myPlugin;
 
use base qw( Template::Plugin );
use Template::Plugin;
 
# Called as Sympa::Template::Plugin::Dokuwiki->load($context)
# by Template engine
sub load {
    my ($class, $context) = @_;
    return $class;
}
 
# Called as Sympa::Template::Plugin::Dokuwiki->new($context, @params)
# when using [% USE Dokuwiki(param1, param2, ...) %] in template
sub new {
    my ($class, $context, @params) = @_;
 
    my $self = {
        'context' => $context # Keep the USE context (template variables ...)
    };
 
    bless $self, $class;
 
    return $self;
}
 
# Package must return trueish value
1;

So, how can I use it ?

Once you have USEd your plugin in a template you can access its properties and call its methods :

#!/usr/bin/perl
 
package Sympa::Template::Plugin::myPlugin;
 
use base qw( Template::Plugin );
use Template::Plugin;
 
# Called as Sympa::Template::Plugin::Dokuwiki->load($context)
# by Template engine
sub load {
    my ($class, $context) = @_;
    return $class;
}
 
# Called as Sympa::Template::Plugin::Dokuwiki->new($context, @params)
# when using [% USE Dokuwiki(param1, param2, ...) %] in template
sub new {
    my ($class, $context, @params) = @_;
 
    my $self = {
        'context' => $context, # Keep the USE context (template variables ...)
        'foo' => 'Bar !',
    };
 
    bless $self, $class;
 
    return $self;
}
 
sub bar {
    my ($self, $value) = @_;
 
    return "You gave me ".$value;
}
 
# Package must return trueish value
1;
[% USE myPlugin %] or [% USE p = myPlugin %]

Foo : [% myPlugin.foo %] or [% p.foo %]

Bar : [% myPlugin.bar("foobar") %] or [% p.bar("foobar") %]

Error handling

You can use the plugin error reporting system along with the template engine's TRY/CATCH mechanism to handle errors :

#!/usr/bin/perl
 
package Sympa::Template::Plugin::myPlugin;
 
use base qw( Template::Plugin );
use Template::Plugin;
 
# Called as Sympa::Template::Plugin::Dokuwiki->load($context)
# by Template engine
sub load {
    my ($class, $context) = @_;
    return $class;
}
 
# Called as Sympa::Template::Plugin::Dokuwiki->new($context, @params)
# when using [% USE Dokuwiki(param1, param2, ...) %] in template
sub new {
    my ($class, $context, @params) = @_;
 
    return $class->error('foo is not foo') unless $params[0] eq 'foo';
 
    my $self = {
        'context' => $context, # Keep the USE context (template variables ...)
        'foo' => $param[0],
    };
 
    bless $self, $class;
 
    return $self;
}
 
sub bar {
    my ($self, $value) = @_;
 
    return $self->error('value must be scalar !') if(ref $value);
 
    return "You gave me ".$value;
}
 
# Package must return trueish value
1;
[% TRY %]
    [% USE myPlugin("bar") %]
    [% myPlugin.foo %]
[% CATCH %]
    [% error.info %] # error.info value is the string given to the error method call
[% END %]

Accessing template variables

You can access the templates variables which are in scope by accessing the context stash in your plugin's methods :

my $robot = $self->{'context'}->stash()->get('robot'); # Get same value as using [% robot %] in template
 
my $user_email = $self->{'context'}->stash()->get(['user', 0, 'email', 0]); # Get same value as using [% user.email %] in template, 0's after each token are mandatory

Getting the request URL

The request URL is availabe as the path_info template variable, retrieving it to handle custom parameters is as simple as :

my $path_info = $self->{'context'}->stash()->get('path_info'); # URL https://mylistserver.tld/sympa/foo/bar will give /foo/bar

Using forms

As Sympa filters carefully input parameters a plugin dedicated mechanism has been designed to allow plugins to handle their parameters the way the want/need it.

Your plugin's inputs in your plugin's forms must have names like plugin.myPlugin.foo, their values will be set as entries in the plugin.myPlugin template variable.

#!/usr/bin/perl
 
package Sympa::Template::Plugin::myPlugin;
 
use base qw( Template::Plugin );
use Template::Plugin;
 
# Called as Sympa::Template::Plugin::Dokuwiki->load($context)
# by Template engine
sub load {
    my ($class, $context) = @_;
    return $class;
}
 
# Called as Sympa::Template::Plugin::Dokuwiki->new($context, @params)
# when using [% USE Dokuwiki(param1, param2, ...) %] in template
sub new {
    my ($class, $context, @params) = @_;
 
    my $self = {
        'context' => $context, # Keep the USE context (template variables ...)
    };
 
    bless $self, $class;
 
    return $self;
}
 
sub processForm {
    my ($self) = @_;
 
    my $data = $self->{'context'}->stash()->get(['plugin', 0, 'myPlugin', 0]); # Retreive form data
 
    return 0 unless defined $data->{'save'}; # Was the form submitted
 
    return $self->error('Bad foo !') unless $data->{'foo'} =~/^[0-9]+$/;
 
    # Save foo somewhere
 
    return 1;
}
 
# Package must return trueish value
1;
[% USE myPlugin %]
[% TRY %]
    [% IF myPlugin.processForm() %]
        Ok !
    [% ELSE %]
        <form action="" method="post">
            <input type="text" name="plugin.myPlugin.foo" value="" />
            <input type="submit" name="plugin.myPlugin.save" value="Save" />
        </form>
    [% END %]
[% CATCH %]
    Error while processing form : [% error.info %]
[% END %]

A configuration file for your plugin

Managing configuration directly in your package file can quickly become a hassle, you can rely on Sympa's native configuration seeking and parsing capabilities to help you having nice configuration files, per virtualhost configuration, why not per list configuration with default virtualhost values ...

Parsing Sympa style configuration file

my $config_structure = {
    'foo' => {
        'format' => '[0-9]+', # parameter validation regexp
        'occurrence' => '1',  # parameter occurence rule
        'case' => 'insensitive',
    },
    'bar' => {
        'format' => '.+',
        'occurence' => '1-n',
        'case' => 'insensitive',
    },
};
 
my $config = Conf::load_generic_conf_file($config_file, $config_structure, 'abort'); # $config will be a reference to a hash of configuration parameters, undef if parsing failed
return $class->error('Config error') unless($config);

Configuration file example :

foo 3
bar abc
bar def5

The format entry of root parameters can be composite to define a paragraph :

my $config_structure = {
    'foo' => {
        'occurrence' => '0-n',
        'format' => {
            'name' => {
                'format' => '[a-z0-9]+',
                'occurrence' => '1',
                'case' => 'insensitive',
            },
            'bar' => {
                'format' => '[0-9]+',
                'occurence' => '1',
                'case' => 'insensitive',
            },
        }
    }
};

Then the configuration file will look like :

foo
    name foo1
    bar 27

foo
    name foo2
    bar 42

A configuration file aside of the plugin package file

You can put your configuration file in the same directory as your package file, that is the $MODULEDIR/Sympa/Template/Plugin directory :

my $file = $class;
$file =~ s/::/\//g;
my $config_file = Sympa::Constants::MODULEDIR.'/'.$file.'.conf';

or even simpler

my $config_file = Sympa::Constants::MODULEDIR.'/Sympa/Template/Plugin/myPlugin.conf';

A virtualhost specific config file with a default in etc/ dir

Sympa's configuration seeking mechanism work as follows :

You can make use of this mechanism in your plugin to have a global configuration which is overridden for some virtualhosts for example :

my $config_file = tools::get_filename('etc', {}, 'myPlugin.conf', $self->{'context'}->stash()->get('robot'));
return $self->error('Config file not found') unless($config_file);

Logging

If you want your plugin to log in the same files as the Sympa process, use the Sympa primitives like this:

# Use the main Sympa process logger singleton
my $logger = Sympa::Log->instance();
 
...
 
# Later in the code, use logs.
# Sympa logs are based on sprintf calls.
# $variable value will thus replace %s in the final log.
$logger->syslog('info','My log message %s',$variable);

Database access

Your plugin can manipulate the Sympa database. Even create tables if needed. Take care, though, that you can also alter the main Sympa database's tables. If you need to add a table, we strongly suggest that the table name would be called after your plugin and prefixed in a way that explicitely tells users that this is a plugin-related table.

We suggest the following naming scheme: "plugin_[plugin_name]_[function]"

This could lead to, for example, the following table name: "plugin_sponsoring_godsons". For the godsons table in the Sponsoring plugin.

# Use the main Sympa process database handler singleton
my $sdm = Sympa::DatabaseManager->instance();
 
...
 
# Table creation
 
# 1. Put datastructure description into a hash.
my $table_structure = (
  [
    {
      table => 'plugin_myplugin_table',
      field => 'plugin_myplugin_table_key',
      key => 1,
      type => 'varchar(150)',
      notnull => 1,
    },
    {
      table => 'plugin_myplugin_table',
      field => 'plugin_myplugin_table_field1',
      type => 'varchar(100)',
    },
    {
      table => 'plugin_myplugin_table',
      field => 'plugin_myplugin_table_field2',
      type => 'varchar(150)',
      notnull => 1,
    },
  ]
);
 
# 2. Add table if it does not exist
 
  my $table_exists;
  my $class = ref $self;
  my @tables;
  unless (
            $sdm
            and @tables = @{$sdm->get_tables()}
      ) {
    return $class->error('Unable to query database.');
  }
  foreach my $table (@tables) {
    if ($table eq 'plugin_myplugin_table') {
      $table_exists = 1;
      last;
    }
  }
  unless ($table_exists) {
    unless (
        $sdm
        and $sdm->add_table({table => 'plugin_myplugin_table'})
        ) {
      return $class->error('Unable to create table.');
    }
  }
 
# 3. Update fields according to table structure description
 
  my @primary_key;
  my $existing_fields = $sdm->get_fields({table => 'plugin_myplugin_table'});
  foreach my $field (@{$self->{table_structure}}) {
    if ($existing_fields->{$field->{field}}) {
      unless ($sdm->update_field($field)) {
        return $class->error(sprintf 'Unable to update field %s: %s',$field->{field},$sdm->error);
      }
    }else{
      unless ($sdm->add_field($field)) {
        return $class->error(sprintf 'Unable to add field %s: %s',$field->{field},$sdm->error);
      }
    }
    if ($field->{key}) {
      push @primary_key, $field->{field};
    }
  }
 
# 4. Delete temporary field used at table creation
 
  unless ($table_exists) {
    $sdm->delete_field({
      table => 'plugin_myplugin_table',
      field => 'temporary',
    });
  }
 
# 5. Reset primary key
 
  my $primary_keys = $sdm->get_primary_key({table => 'plugin_myplugin_table'});
  if(keys %$primary_keys) {
    unless ($sdm->unset_primary_key({table => 'plugin_myplugin_table'})) {
      return $class->error("Unable to unset primary key");
    }
  }
  unless (my $report = $sdm->set_primary_key ({
    table => 'plugin_myplugin_table',
    fields => \@primary_key,
  })) {
    return $class->error($report);
  }
 
 
# 6. INSERT statement
my ($key_value,$field1_value, $field2_value);
$sdm->do_prepared_query(
  sprintf (q{
    INSERT INTO %s (%s,%s,%s) VALUES(?,?,?)
    },
      'plugin_myplugin_table',
      'plugin_myplugin_table_key',
      'plugin_myplugin_table_field1',
      'plugin_myplugin_table_field2',),
    $key_value,
    $field1_value,
    $field2_value,
    );
 
# 7. SELECT statement
 
  my $sth = $sdm->do_prepared_query(
    sprintf (q{
      SELECT %s,%s from %s WHERE %s=?
      },
      'plugin_myplugin_table_field1',
      'plugin_myplugin_table_field2',
      'plugin_myplugin_table',
      'plugin_myplugin_table_key',
      ),
    $key_value,
    );
  while (my $entry = $sth->fetchrow_arrayref()) {
  #...
  # Do something with the entry.
  }

List of template plugins

This is the list of template plugins avaialable for Sympa.

Top of Page