Version 4 > Plug-ins

Plug-ins

A plug-in is an add-on code module that can perform customized tasks that are beyond the scope of the core system. Plug-ins can:

  • generate specialized content for insertion into web pages
  • serve a control panel for the back-end admin interface
  • serve a concise dashboard view
  • access the system task manager for scheduling off-line tasks
  • provide their own API interface for direct data manipulation
  • provide an interface to unrelated 3rd-party applications

Plug-in Basics

Plug-ins are Perl modules that are kept in the cgi/Modules directory of your system. Installation can be as simple as copying your module into that directory; un-installation can be as simple as removing it.

In practice, complex plug-ins can have many other files as well, including:

  • images, stylesheets, javascripts, and other web assets
  • system icons
  • special database tables and corresponding map files
  • help files and documentation
  • other data files

But the basic Perl module is the minimum requirement.

Hello World Example

The simplest illustration of how a plug-in works is to create a "Hello World" plug-in. This plug-in module does nothing except insert the text "Hello World!" into a web page. Here is a bare-bones version of this plug-in, written in Perl:

package Modules::HelloWorld;
sub read {
    my ($this,$options) = @_;
   # do nothing
}
sub write {
    my ($this,$options) = @_;
    return "Hello World!";
}
sub ioctl {
  my ($this,$options) = @_;
    # do nothing
}
1;

To install this plug-in, save this code to a file named HelloWorld.pm, and place the file in the cgi/Modules directory of your system.

What this code does:

  1. The read() method manages all input to the plug-in. It should scan the various sources of input (the query string, the post data, path info, and others) and configure the plug-in appropriately. In this case, we don't need to deal with any input, so the method is empty.
  2. The write() method manages all output from the plug-in. Based on the inputs and any other context that is important, it will assemble a block of HTML to be inserted into a web page. In this case, we are only outputting a simple string, "Hello World!", which is the return value of this method.
  3. The ioctl() method is used for all other system interactions with the plug-in. The system will query the plug-in from time to time to find out what features it supports, by passing a simple text message to ioctl(). If the plug-in responds with something appropriate, then the feature is supported, otherwise, the feature is disabled. In this case, we don't support any other features at all, so the method is empty.

Linux system programmers might recognize these methods as being similar to unix device drivers. That is because the basic idea is similar: a plug-in handles input (reads) and output (writes) to a special subsystem, which may or may not support extra functions (ioctls) above and beyond basic reads and writes. Other that that rough architectural similarity, you do not need to know anything about Linux system programming to develop your own plug-ins.

Base Classes—Generic Plug-in Modules

The system provides two generic plug-in modules that can serve as base classes for your plug-ins to inherit from.

  • cgi/Modules/BaseDCD.pm - generic plug-in module, including methods for automatically loading configuration files, reading standard input sources, generating self-links, and communicating basic module information to the base system.
  • cgi/Modules/Content.pm - generic plug-in module for a specialized Content type. Includes standardized methods for editing, configuring, copying, ordering, and deleting content, tools for managing workflow states, and administrator screens for managing schedules, translations, prices, images, and contact information.

It is very helpful to inherit from one of these generic modules when starting your own plug-in. It ensures that your plug-in is following good practices and has access to some functions that are very useful in plug-in development.

Here is our Hello World plug-in, re-coded to inherit from BaseDCD.pm:

package Modules::HelloWorld;
use strict;
use Modules::BaseDCD;
use vars qw(@ISA);
@ISA = qw(Modules::BaseDCD);
sub write { my ($this,$options) = @_; return "Hello World!"; }
1;

Now our Hello World plug-in only has to define its own specific write() method, since it will inherit anything unspecific from BaseDCD.pm.

ExSite Plug-in Boilerplate

Some plug-ins are simply glue layers to interface to an external code package that is not part of ExSite. Others are tightly integrated into ExSite, making use of the system database and ExSite's internal packages and libraries. In the latter case, there is some additional boilerplate code you will want to include to integrate your plug-in module with the surrounding system.

package Modules::HelloWorld;
use strict;
use ExSite::Config;
use ExSite::Input;
use ExSite::Misc;
use ExSite::Util;
use ExSite::ML;
use ExSite::UI;
use ExSite::FormBuilder;
use ExSite::ReportBuilder; 
use ExSite::Form; 

use Modules::BaseDCD;
use vars qw(@ISA);
@ISA = qw(Modules::BaseDCD);

sub write {
    my ($this,$options) = @_;
    #
    # get some standard objects that might be useful
    #
    my $db = $share{DB};   # current database handle, probably an ExSite::Form object
    my $page = $share{Page};    # current page being built, some variation on an ExSite::Content object
    my $ml = &get_obj("ML");   # ExSite::ML object
    #
    # build our HTML
    #
    my $out;
    $out .= $ml->h1("Hello world");
    return $out;
}
1;

This version of our Hello World plug-in includes a bunch of ExSite packages that may come in handy in the course of writing the plug-in code. Here is a summary of packages that may be useful:

Package Description
ExSite::Config covers system setup and configuration, and gives you access to global variables
ExSite::DB reads and writes to the database
ExSite::Report same as DB, but with extra tools for reporting (ie. displaying) the data you have obtained from the database
ExSite::Form same as Report, but with extra tools for generating forms and inputs for data you intend to write to the database
ExSite::ReportBuilder tools for converting datasets to visual formats including HTML tables, spreadsheets, and charts
ExSite::FormBuilder tools for constructing HTML forms, with various advanced features such as input validation, captchas, and templating
ExSite::Misc miscellaneous utilities for text processing and web data handling
ExSite::Input tools for processing web inputs such as query strings, posted form data, uploaded files, and path info
ExSite::ML markup language generation, useful for generating well-formed HTML and XML documents
ExSite::UI tools for generating complex HTML structures and Bootstrap widgets, such as boxes and panels, toolbars, blindboxes, tabs, and popups; very useful when building control panels
ExSite::Content base class for the CMS, very useful if interacting directly with the content tables
ExSite::Mail tools for sending email notifications
ExSite::Module utilities for interacting with other plug-ins
ExSite::Chart tools for generating graphs

There are many others; this is not a complete list, simply a concise overview of the more common ones from the ExSite kernel.

Plug-in Web Assets

If your plug-in requires any web assets that are not already included in the main system, it can keep those in a reserved directory, html/_Modules/PluginName, where PluginName is the same as your module name, less the .pm suffix. The system will automatically find and make use of the following files in that directory, if they are present:

File/Asset Description
icon.png or icon.gif an icon to represent your plug-in in certain contexts
help.html help documentation for admins using your plug-in
PluginName.css a stylesheet that should be loaded on your plug-in's administrator control panels
PluginName.js javascript code that should be loaded on your plug-in's administrator control panels

Any other files you need can also be included in this same directory, to keep them compartmentalized from other system components.

Plug-in Database Extensions

If your plug-in adds tables to the system database, you can place your extensions' map files (see Database Maps) into its own directory in cgi/dbmap/PluginName/. These files may include

DBmap file Description
.table tables that your plug-in is adding to the database
.datatype datatypes that your plug-in is declaring
tablename column mappings for each table that you add

Optional Plug-in Features

Administrator Control Panels

Just as the write() method generates content for insertion into web pages, you can optionally support a back-end control panel to provide an administrative interface. To find out whether your plug-in supports control panels, the system will query your ioctl() method, passing the "ControlPanel" keyword. Your ioctl() method should respond with a CODE reference to a method in your plug-in that will generate the control panel HTML.

If the HTML received back from this call is a complete HTML document (beginning with a doctype declaration or html tag), then the system will presume that your code is generating the complete control panel and will display it to the administrator unaltered.

The HTML received back is a partial HTML document, then it will be merged into the standard system control panel template, automatically incorporating any web assets you have defined (see above). 

Example:

sub ioctl {
    my $this = shift;
    $_ = shift;           # $_ is the ioctl request

    if (/ControlPanel/) {
        return \&ctrl_panel;
    }
}

sub ctrl_panel {
    my $this = shift;
    $ml = &get_obj("ML");  # markup generator
    my $out;  #  output buffer
    $out .= $ml->h1("This is your control panel");
    # ... 
    return $out;
}

Task Management

If your plug-in needs to execute off-line tasks at certain intervals, or at preset times, it can make itself available to the system's task manager. To find out whether your plug-in supports scheduled tasks, the system will query your ioctl() method, passing the "Cron" keyword. Your ioctl() method should respond with a CODE reference to a method in your plug-in that will handle task management.

All task management calls receive 3 parameters: firstly an action, which is presumed to be a command word, and a type + ID, which can (but does not necessarily have to) refer to some object in the system. For example, a task with the parameters ("publish", "page", 567) could be construed to be a command to publish page ID 567. In practice, you are free to interpret these three parameters in whatever manner suits you. They are only structured this way for convenience, and the system does not otherwise impose any semantic requirements on how they must be used.

Example:

sub ioctl {
    my $this = shift;
    $_ = shift;   # $_ is the ioctl request
    if (/Cron/) {
        return \&task_manager;
    }
}

sub task_manager {
    my ($this,$action,$type,$id) = @_;
    # ...
}

Adding Plug-ins to the System Dashboard

The system dashboard is a visual overview of the entire system. Plug-ins that have something offer for the system dashboard can make it available by providing a Dashboard hook, in a similar manner to the above.

sub ioctl {
    my $this = shift;
    $_ = shift;   # $_ is the ioctl request
    if (/Dashboard/) {
        return \&dashboard;
    }
}

sub dashboard {
    my ($this) = @_;
    # ...
}

The dashboard hook takes no parameters, and should restrict its output to a compact overview that occupies a rectangle approximately 500 pixels wide by 300 tall. Many plug-ins opt to use a chart for this purpose, but in principle you can output text, a table, or anything at all.

Recursive Views

Typically a plug-in will generate a starting view (which may depend on the options string that is passed to the write() method) and the user will that navigate around inside the plugin to complete their task. Since you may not know where exactly the plug-in has been "plugged-in" to the website, the URLs that you are linking to are unpredictable.

If you use either of the base classes mentioned above, you can easily generate recursive self-links using the built-in link() method, which generates a link back to the current URL. This method takes a hash of keys=>values that are used as query string parameters to modify the URL. If you set a key to undef, that parameter will be cleared from the URL, otherwise it will be added/changed.

For example, if your plug-in accepts two commands, stop and go, you can use the following calls to generate the appropriate links, no matter where the plug-in has been embedded on your site:

$stop_url = $this->link(cmd=>"stop");
$go_url = $this->link(cmd=>"go");
$reset_url = $this->link(cmd=>undef); # clear the command

Service Pages

Sometimes you do need strict control over the page where the plug-in does its work. If so, you can give the plug-in its own home page, where it will automatically redirect to when you interact with it. This not only gives you a predictable URL for your plug-in, but also more control over the page layout.

The plug-in page is called a service page, so-called because it services all links to the plug-in. Define your service page by adding a record to the service table. You can only have one service page per plug-in, although you are allowed to setup alternate services pages for other languages on multilingual sites.

Integration with other Plug-ins

Sometimes you want to make use of other system plug-ins, so that you can take advantage of their functionality without having to redevelop all of those features.

Transferring control to another plug-in

If you simply need to call another plug-in and allow it to assume control, you can embed the other plug-in into your write() method's output. The format is

This basically just calls the other plug-in's write() method, passing the options string as a parameter.

For example, to add an item to the shopping cart, and allow the cart to take control of the workflow so that the user can check out and pay, you can embed the Pay module into your output with the purchased item parameters, like so:

and the cart will embed itself into the page at that spot.

Background calls

To call another plug-in but not give it control over the workflow, you can call the plug-in from your Perl code. For example, you can make the same shopping cart call as above, but trap the shopping cart output so that it does not display in your page, like so:

use ExSite::Module qw(&get_module);

my $Pay = &get_module("Pay");
my $Pay_output = $Pay->write("item=Donation&amount=50.00&acctcode=6");

Deep Integrations

If you will actually be writing code that interacts with other modules' data and logic, you may need to load up the configurations and other settings so that you are both on the same page, so to speak.

The simplest way to do this is to declare a dependency; in other words, that your module depends on a separate module, and when your module gets set up, the system should also set up the separate module as well to make sure they are both operating under the same conditions.

The system checks for dependencies by calling the ioctl() method, passing the "Dependencies" parameter. Your ioctl() method should respond with an array of module names that it depends on. The system will automatically load the configuration settings and dbmaps of those modules, so that they are available in the current context.

Example

The example plug-in below illustrates some useful features and methods. You can use it as a template for writing your own plug-ins.

package Modules::Example;

use strict;
use ExSite::Config;
use ExSite::Input;
use ExSite::Misc;
use Modules::BaseDCD;

use vars qw(@ISA $ml $ui);
@ISA = qw(Modules::BaseDCD);

sub read {
    my ($this,$options) = @_;
    my $in = new ExSite::Input;
    $this->{input} = $in->combine;
    $this->{post} = $in->post;
    $this->{query} = $in->query;
    $this->{is_post} = $in->is_post; #scalar keys %{$this->{post}};
}

sub write {
    my ($this,$options) = @_;

    my $ml = &get_obj("ML");
    my $out = $ml->h1("Test Plug-in");

    # display information about the request
    
    $out .= $ml->h2("Request");
    $out .= $ml->p("URL: ".$config{server}{server}.$ENV{REQUEST_URI});
    $out .= $ml->p("Request Type: ".$ENV{REQUEST_METHOD});
    if ($share{Page}) {
	$out .= $ml->p("Page: ID=".$share{Page}->id." Name=".$share{Page}->name);
	$out .= $ml->p("Section: ID=".$share{Page}->my_section->id." Title=".$share{Page}->my_section->title);
    }

    # display input data
    
    $ml->h2("Input");
    if ($options) {
	my %opt = &DecodeString($options);
	$out .= $ml->h3("Options").&ShowHash(%opt);
    }
    if (scalar keys %{$this->{query}}) {
	$out .= $ml->h3("Query Data").&ShowHash($this->{query});
    }	
    if ($this->{is_post}) {
	$out .= $ml->h3("Post Data").&ShowHash($this->{post});
    }

    # mess around with the query data

    $out .= $ml->h2("Test Links");
    $out .= $ml->p(
	$ml->a("Here",{href=>$this->link(test=>123)}).
	    " are a few ".
	    $ml->a("test links",{href=>$this->link(test=>undef,cmd=>"go")}).
	    " you can ".
	    $ml->a("try",{href=>$this->link(id=>555,cmd=>undef)}).
	    "."
	);

    # mess around with the post data
    
    $out .= $ml->h2("Test Form");
    my $f = new ExSite::FormBuilder();
    $f->input(prompt=>"Name",name=>"name",type=>"text",size=>40,required=>1,value=>$this->{input}{name});
    $f->input(prompt=>"E-mail",name=>"email",type=>"email",size=>25,required=>1,value=>$this->{input}{email});
    $f->input(prompt=>"Age",name=>"age",type=>"number",min=>19,max=>85,value=>$this->{input}{age}||30);
    $f->buttons(submit=>1);
    $out .= $f->make();

    return $out;
}

sub ioctl {
    my $this = shift;
    $_ = shift;           # $_ is the ioctl request

    if (/isRestricted/) {
	# not restricted, any website can make use of this plug-in
	return 0;
    }
    elsif (/isService/) {
	# does not use a service page
	return 0;
    }
    elsif (/ControlPanel/) {
	return \&ctrl_panel;
    }
    elsif (/ModuleName/) {
	return "Example Plug-in";
    }
    elsif (/ModuleInfo/) {
	return "Sample code for developers to learn how plug-ins work.";
    }
# the following functions are not supported by this example plug-in
#    elsif (/Cron/) {
#	return \&task_manager;
#    }
#    elsif (/Dependencides/) {
#	return [ "Plugin1", "Plugin2" ];
#    }
}

sub ctrl_panel {
    my $this = shift;
    my $db = $share{DB};
    $ml = &get_obj("ML");
    $ui = &get_obj("UI");
    my $out;
    $out .= $ui->HelpMsg("This control panel simply reproduces the functionality from write()");
    $out .= $this->write();
    return $out;
}

1;