User Tools

Site Tools

Translations of this page:

develop:plugins

How to Write a phpList Plugin

A plugin can add extra functionality to phplist in either, or both, of two ways:

  • By providing extra pages. The page can do almost anything, such as providing a report, or some extra processing.
  • By hooking into core phplist at certain points in its processing, such as when sending a campaign.

A plugin is implemented as a class that extends the core phplistPlugin class. Plugin classes reside in the directory set by the constant PLUGIN_ROOTDIR in the config file.

The default location for plugins is the “plugins” directory within the admin directory, but for security purposes, and also ease of upgrading phplist, it is better to place the directory outside of the web root. However, this may have consequences if your plugin uses assets that need to be publicly available, and solutions for this will need to be found.

If your plugin provides extra pages, then the php files for those pages should be in a subdirectory named after the plugin.

All phpList pages are loaded through index.php in the “admin” directory of the phpList installation. The page to be loaded is specified as a parameter.

So, imagine we are running with a secure connection for the site “www.example.com”. Then selecting “Configure phpList” in the “Configuration functions” submenu on the dashboard would open the URL: “https://www.example.com/path_to_phpList_root_page/admin/?page=configure”. Here index.php does not need to be explicitly included, since it is set to be the default directory index file for the phpList root directory and its subdirectories.

You can then access a plugin page similarly, setting the “page” parameter to be the page name, say “myPage”. But you must also give the name of the plugin in the “pi” parameter, say “myPlugin”. So, instead of just ?page=mypage the parameters of a URL specifying the example page would be ?page=mypage&pi=myplugin.

Example

If your plugin is called helloworld then a file helloworld.php is placed in the PLUGIN_ROOTDIR. The file contains the class definition for the plugin.

Further files associated with the plugin must go into the “helloworld” subdirectory. The public property $coderoot must be set to the subdirectory to tell phplist where the pages reside.

The plugin constructor of the plugin class must call the parent constructor to ensure that the plugin is initialised correctly.

<?php
class helloworld extends phplistPlugin
{
    public $coderoot = PLUGIN_ROOTDIR . '/helloworld/';

    function __construct()
    {
        parent::__construct();
    }
...
}
?>

Alternatively you can set up the $coderoot property dynamically:

function __construct()
{
    $this->coderoot = dirname(__FILE__) . 'helloworld/';
    parent::__construct();
}

Be aware that in phpList, even disabled plugins are instantiated, even though they are not used. Consequently you should be careful about what you put the constructor of your plugin.

It is certainly OK to initialize properties of the plugin and to do some preprocessing of the settings, but you should do nothing that affects anything outside of your plugin such as the phpList session variables or database tables, including those of the plugin.

Any such work necessary to the operation of the plugin belongs in the activate() method. The activate method is invoked only for plugins that are enabled, and it is called soon after the plugin is instantiated.

Installation

To install a plugin, place the plugin.php class file and the code files directory in the PLUGIN_ROOTDIR.

Special plugins

There are currently three “types” of plugins that are special.

  • editor plugin
  • authentication plugin
  • email sender plugin

Only have one plugin of each type can be enabled.

The editor plugin implements the editor when editing a campaign or a template.
The authentication plugin implements the authentication of administrators to the phpList system.
The email sender plugin performs the actual sending of emails, instead of using SMTP or the php mail() function.

phpList plugins and GitHub

The plugins are intended to live on GitHub and some of the integration is GitHub specific.

If you want to develop a plugin, first of all give your project a name, and it may be best to add “phpList” in the project name, eg phplistPluginForSomething

Then, in your project, add a directory called plugins in which you place your plugin files, as described above. This allows you to have multiple plugins per GitHub project.

The “plugins” directory is used to detect the plugin code, when installing with the plugin installer

The plugin installer will take the URL of the ZIPfile at GitHub and pull the file down, and install it.

But WATCH OUT!! In phpList 3.05 (and presumably in earlier 3.x versions) the plugin installer may not install any directory needed by your plugin! It may only install the single plugin class definition file itself. In that case, you can upload the directory and all the files inside it manually yourself.

This is a well-known bug discussed:here and here

Automated Testing

You can set up automated testing on Travis-CI of your plugin. To view how this has been done with the Rest-Api plugin read the page about it.

Application hooks

The file

defaultplugin.php

in the admin directory has all the class methods that can be extended by your plugin. Most of them are commented to identify what they are for.

If you encounter some of these hooks to not work, open a GitHub issue to get it fixed.

You may be disappointed to find that none of the methods provided in defaultplugin.php address the issue that you want your plugin to handle. That would not be at all surprising – a program author can hardly imagine everything that people might want to do with his software. But do not despair!

Think of the methods of defaultplugin.php as portals through which you can access data as it is being processed by phpList and modify it if you need to.

An example of this technique is a plugin prefixing the subject line of messages with the name of the list to which the message is being sent. (See http://resources.phplist.com/plugin/listnameprefix). The problem with writing this kind of plugin is that defaultplugin.php provides no method for revising the subject line of messages. Nor is it obvious how to determine what to what list the message is being sent.

Now looking at the “Process Queue” section of defaultplugin.php, you will find several methods that are passed the $messagedata array. The data is loaded into this array by the function loadMessageData($msgid) defined beginning at line 82 of the file lib.php. Looking through this function shows you what the elements of this array are. You will find that an array of the numerical IDs of the lists to which the message is being sent, is located in the 'targetlist' element of the $messagedata array. Using the list ID, it is easy to get the name of the list.

You can use one of these methods to get the data needed. It is not necessary to alter the return value from the default value.

Looking further through the phpList code, you will find that the last method of defaultplugin.php called before a message is sent to a particular subscriber is messageHeaders($mail). The argument passed to this function is an object containing all of the data for the current message about to be sent out to a subscriber. Fortunately objects in PHP are always passed to functions by reference. So what is passed here is a reference to the $mail object, meaning that you can modify the parts of the message before it is sent, by overriding this class method of defaultplugin.php.

So if you want to prefix the subject line of the message with the list name, messageHeader($mail) is where you can actually do it, by altering the 'Subject' property of $mail: $mail→Subject. Again it is not necessary to alter the default return value of this method; our work is being done as a side effect.

You will find another example of this kind of coding if you examine the Subject Line Placeholder plugin at http://resources.phplist.com/plugins/subjectlineplaceholder.

Unfortunately few of the class methods of defaultplugin.php have objects as arguments. For example, the campaignStarted() method is passed the $messageData array. As this method is defined in defaultplug.php, it is called by value. But a plugin can override this method and require the method to be called by reference instead, if phpList is running on PHP version 5.3 or later.

All you have to do is to prefix the argument with an ampersand in your method definition:

function campaignStarted(&$messagedata) {
  mycode ...
 }

This change will will allow you to modify the $messageData array, and that modification will persist after your method returns. The code of the caller is not affected by this change, it continues to call your plugin this way:

yourplugin->campaignStarted($messagedata)

But the PHP engine knows from your method definition that it is not the $messagedata array that is to be passed to your method, but rather a reference to it.

You should be very careful with this last coding technique. You might really screw up the operation of the software if you don't know what you are doing!

Adding settings

If you have things you want the administrator to configure in order for your plugin to work, you can add settings, which will show up on the “Settings page”.

To do this define a public variable in your plugin called settings

  public $settings = array(
    "myplugin_setting1" => array (
      'value' => "some default",
      'description' => 'Description of this setting',
      'type' => "text",
      'allowempty' => 0,
      "max" => 1000,
      "min" => 0,
      'category'=> 'general',
    ),
  );

Explanation:

myplugin_setting1 → name of the setting. Use only characters and no spaces. You can then retrieve the current value of this setting in your code with getConfig('myplugin_setting1')

value → this is the default for this value, if none are given by the administrator

description → What is it all about

type → this can be text. textarea. boolean, url, integer,

allowempty → 0/1 can this setting be left empty?

max and min → used for “integer” only.

category → can be anything, but it will be useful to try to use one of the existing ones. “general”, “security”, “subscription”, “segmentation”, “campaign”, “reporting”

Note: if want to allow a zero to be entered for an integer setting, you must set the allowedempty value to 1 for that setting, because Phplist considers a zero value to be an 'empty' setting.

Sql queries

Always use the abstraction of the Sql Queries. You can consider the database to be connected and there's no need to connect again.

The abstraction is in mysql.inc and the main calls are basically the “mysql_” functions in PHP, but then without the “my”.

  • Sql_Query
  • Sql_Fetch_Row
  • Sql_Fetch_Array
  • Sql_Fetch_Assoc
  • Sql_Escape
  • etc

To find out about the database structure, you can read the structure.php file.

NOTE: although the database access functions explained above have names derived from the PHP mysql_ functions, phpList is not restricted to using the MySql database, nor the deprecated PHP “mysql_” functions.

Database access is supported by a “.inc” file defining the functions discussed above. A choice of three different files is available
*
mysql.inc - mentioned above, supports the deprecated PHP mysql_ functions
*
mysqli.inc - supports the current PHP mysqli_ functions.
*
adodb.inc - supports ADOdb database abstraction library, which suports many databases including PostGreSQL and Oracle.

The value of the variable $databasemodule set in the configuration file config_extended.php selects which of three “.inc” files is loaded. In phpList v3.012 that variable is found in line 704 of the file.

But whatever database module is loaded the phpList Sql
functions will work the same way.

You should note, however, that PHPlist provides a variety of specialized functions for getting data from and storing data in the database, so that you may not need to access the database directly. Here are a few of them, provided as examples:

  • listname($listid)
    • Provides the name of the list given the numerical list ID.
    • See phplist.php, line 42
      • getListsAsArray()
      1. Returns the array of list names keyed on the list ID.
      2. See phplist.php, line 138
      • getUserAttributeValues($email, $userid=0, $indexByID=false)
      1. Called by specifying either user email address or ID
      2. Returns an array of values keyed on the attribute name or 'attribute“ concatenated with the attribute ID
      3. See userlib.php, line 218
      • existUserID($userid = 0)
      1. Returns a Boolean false if $userid is not a valid user ID, or the ID back again if it is valid.
      2. See userlib.php, line 202

      So it is a good idea, before writing a query directly to the database, to look through the PHPlist codebase, to see whether a function already exists that provides the database access that you require. You will find an index of phplist functions and class methods, starting here. These functions and methods are not yet very well documented in this index yet. Often the name of the function will give you an idea of what it does. But the index does give you the location of the function or method definition in the codebase, so that you can reference the source code to figure out what the function does and how to use it.

If you must access the database directly note that tables are referenced with the Global tables: $GLOBALS['tables']['tablename']. Note, however, that functions exist in the code base for dropping tables, creating tables, and testing for the existence of tables. In the file mysql.inc, mysqli.inc or adodb.inc found in the admin/ directory of phpList, look for the functions

  • SqlDropTable($table)
  • SqlcreateTable ($table,$structure), and
  • SqlCheckFor_Table($table)

If you must query the database directly, you need to sanitise your queries to avoid Sql Injection. Use sprintf for this. Numbers are sanitised with %d and strings with Sql_Escape

Also, it is best practice to avoid doing a select * on tables, just to be sure.

So the result would look something like

$city = 'Amsterdam'; // let's assume this came from external data
$attribute = 10;

$request = Sql_Query(sprintf('select user.id from %s user, %s user_attribute 
    where user.id = user_attribute.userid 
    and user_attribute.value = "%s" 
    and user_attribute.attributeid = %d',
    $GLOBALS['tables']['user'],
    $GLOBALS['tables']['user_attribute'],
    Sql_Escape($city),
    $attribute));

The above Query would give you all the subscribers who have attribute 10 to be Amsterdam. Presumably that means attribute 10 is “City”.

Using PHP program execution functions

The short answer here is: DON'T!

The PHP functions that we are talking about are those listed here. These functions allow you to call the shell to run a command or script, whose output will be returned to the calling program. This is very convenient. It allows you to use a system command as a function in your program. Using one of these functions, say exec(), you could incorporate a shell script as a function in your page or a PHP script, or a script written in another language entirely, like Perl or Python.

The problem here is that some system administrators shut off these functions for reasons of security. So if you are writing a plugin for general use, you cannot be sure that your page will function correctly for everyone. Furthermore, such system calls are operating system specific. So what works on Linux won't work on Windows and vice versa.

Consequently you should avoid making such system calls, unless you are writing a plugin for a very specific use on an system where you are sure that the necessary PHP functions will be available and that no security issues will arise.

Translation

To make things translatable use the s function. This used to be $GLOBALS['I18N']→get('text') and a lot of code still uses that, but the s function make this call and makes it much easier to code.

print s('This is some text in the code');

You can also use the sprintf format, which will make it easier to translate, as in some languages the structure of the sentence may be different.

print s('%d out of %d messages were sent',$count,$total)

Building pages

Any page in phpList handles it's own forms and data. Basically the structure of a page is:

if (!defined('PHPLISTINIT')) die(); ## avoid pages being loaded directly

if (isset($_POST['myVariable'])) {

   /* check the XSRF token */
   if (!verifyToken()) {
     print Error(s('Invalid security token, please reload the page and try again'));
     return;
   }
   
   ... sanitise the input variables

   ... handle the data in the form

   return;
}

print (formStart('some additional form elements'));


  ... show my form

</form>

The formStart() function specifies the “method” attribute of the form tag as “post”. It sets the action attribute to an empty string, so that on submission the page is reloaded, allowing the page itself to process the data that was “posted” to the server.

The formStart() accepts a single string as an argument. This string should consist of additional attributes to be added to the “form” tag, exactly as they should appear in the tag. For example, to add an id and a class to the “form” tag, you might print the start of the form in the following way:

print (formStart('id="myID" class="myClass"'));

Making pages look like phpList

Like WordPress, phpList will turn your valid HTML into a nicely formatted web page appropriate to the overall look of the site, the phpList administrative area. An example is show below. This will work even with a form. Of course, phpList will not insert the necessary HTML tags. You will, for example, need to surround your paragraphs with paragraph tags. And you might want to adjust font-size or text-color in one place or another with an inline style, but phpList will provide for the overall page appearance.

Using the UIPanel() class to frame text and forms

phpList usually presents its information or a form inside a nice formed box with a gray border and a title at the top as in the image below.

In this example the HTMLis put into a variable $dummyText. Then the box is produced by the following lines of code.

$panel = new UIPanel("An Example of UIPanel()", $dummyText);
print ($panel->display()); 

That is all that there is to it. You can even put a box inside a box as in the example shown in the next image.
In this example we load the HTML to be in the first box into the variable $dummyText. The HTML to go into the second box goes into $dummyText2. Then the following code creates the nested boxes.

$panel2 = new UIPanel ("More Dummy Text", $dummyText2);
$dummyText .= $panel2->display(); // Note the combined operator '.='
$panel = new UIPanel("An Example of UIPanel()", $dummyText);
print($panel->display());

Nothing more is required.

To learn more about this class, consult the class definition in commonlib/lib/interface.php under the phpList admin directory

Using the WebblerListing class to create tables

Pages commonly to show a tabulation of data contained in an array or data from an SqlQuery() accessed with one of the “SqlFetch” functions, such as SqlFetch_Row(). The WebblerListing class provides several methods that can be used to display such data in nicely formatted tables consistent with the overall look of phpList.

You can imagine the operation of this class as creating an array of table rows, indexed by the value to be entered in the first column of your table. Each row is an array of cells indexed by column names.

After all the data has been entered, the display() method produces the HTML which can be printed to put the tabulation onto a page.

You must begin by creating an object to represent your tabulation. You should create this object, passing the passing the name of the leftmost column of your table to the WebblerListing constructor. So you would begin with this code.

$myTable = new WebblerListing("firstCoumnName");

The addElement() method

The addElement($value, $url = ”“, $colsize=”“) method adds rows to the tabulation; $value is the value (number or text) to be entered into a cell in the first column. If the optional parameter $url is given a URL value, a $value is linked to that URL in the tabulation. Usually you can omit the $colsize argument.

Let's say that you have values and associated URLs in an array $myArray. Then you might create a single column tabulation with the $myTable object as in the following code.

foreach ($myArray as $anArray) {
    $val = $anArray[0];
    $url = $anArray[1];
    $myTable->addElement($val, $url);
}

if you look at the definition of this method in admin/connect.php you will find that the function definition reads

function addElement($name,$url = "",$colsize="")

The first argument is called $name instead of $value. Originally this method was imagined as creating row headings rather entering values into the first cell of a row. But this makes no difference. This argument may contain a numerical value, a string value, or a string serving as a heading for the row.

The one necessary requirement is that values entered into a tabulation using the addElement() method must be unique. These values are used to index the cells of the additional columns that you add to the table.

The addColumn() Method

Columns are added to a tabulation with the addColumn($firstColumnEntry,$column_name,$value,$url=”“,$align=”“) method.

The $firstColumnEntry argument determines in which row $value appears. The $column_name argument determines in which column $value appears. New columns are created when this function is first called with a column name that has not appeared before. Of course, the column names must be unique.

As with addElement(), $url is an optional argument that specifies a link to the value entered into the cell. Usually nothing needs to be entered for $align.

The addColumn() method calls can be made in any order, since the arguments to each call specify completely where the value entered is to be located.

So now let's say that we need to make a tabulation with three columns. The first column is a numerical id, the second column is a name of some kind, perhaps a building name. Let's suppose the third column is the building height. We also have a url for each id that will be used to hot link the id and the name. Let's say also that we have the data in an array $myArray keyed by the id.

$myTable = new WebblerListing("ID");
foreach($myArray as $key => $anArray) {
    $url = $anArray[0];
    $name = $anArray[1];
    $height = $anArray[2];
    $myTable->addElement($key, $url);
    $myTable->addColumn($key, "Building", $name, $url);
    $myTable->addColumn($key, "Height in Feet", $height); 
}

Now you could go ahead an print the table created by this loop:

print($myTable->display())

This would create a nicely formatted table inside the same kind of box that the “UIPanel” class produces. The worm in the apple here is that this table will be given the same title that appears in the first column. There seems to be no facility for creating a table title separate from the heading of the first column in the table. But you can easily change the table title with the PHP function str_replace()

$html = $myTable->display();
$oldTitle = 'ID';
$newTitle = 'The Heights of Various Buildings';
$needle = '<div class="panel"><div class="header"><h2>' . $oldTitle . '</h2></div>';
$replace = '<div class="panel"><div class="header"><h2>' . $newTitle . '</h2></div>';
print(str_replace($needle, $replace, $html));

Notice that we are including the entire “header” div in the search string, as well as the opening div of the “panel” class, because we do not want to accidentally replace anything in the table other than the title.

This code produces the table you see below.

How to page a listing

Often you will have a tabulation that is just too long to fit on a single web page. The function simplePaging($baseurl,$start,$total,$numberPerPage,$itemName = ”“) can be used with the WebblerListing class to break the tabulation up into separate pages connected by convenient links.

The argument $baseurl can be taken to be the name of the page on which the listing occurs, that is, the file name without the '.php' extension; $start is the zero-based count at which the current page of the tabulation starts.

Thus the $start variable keeps track of which page is being presented. The paging links, next page, previous page, etc., put the appropriate value as a parameter in the URL to be loaded, for example, &start=30.

The argument $total is the number of items, that is, the number of rows in the tabulation. The argument $numberPerPage is, of course, the number of items to be listed on each page; $itemName is the name of the kind of items being tabulated, in the examples here, $itemName would be “buildings”.

Now in the array $myArray, let's not key on the building id, but rather create a simple array without a key, in which each subarray contains the building id as the first item. Then we might create a paged listing with the following code in a file, say myPage.php

$myTable = new WebblerListing("ID");
$numberPerPage = 10;
$total = count($myArray);
if (isset($_GET("start")) {
    $start = $_GET("start");  // The index of the current page is passed as the "start" parameter in the URL
} else {
    $start = 0;
}
if (!$total) {
	$myTable->addElement('<strong>The list of buildings is empty.</strong>', ''); //
} else {
    for ($i = 0; $i < $numberPerPage; $i++) { // Create a table of proper length for the page.
        $anArray = $myArray[$start + $i];
        $id = $anArray[0]
        $url = $anArray[1];
        $name = $anArray[2];
        $height = $anArray[3];
        $myTable->addElement($id, $url);
        $myTable->addColumn($id, "Building", $name, $url);
        $myTable->addColumn($id, "Height in Feet", $height);
    }
    $paging=simplePaging("myPage", $start, $total, $numberPerPage,'buildings');
    $myTable->usePanel($paging);  // Pass the paging to the $myTable object
}

// Now put the correct title on the table and print it
$html = $myTable->display();
$oldTitle = 'ID';
$newTitle = 'The Heights of Various Buildings';
$needle = '<div class="panel"><div class="header"><h2>' . $oldTitle . '</h2></div>';
$replace = '<div class="panel"><div class="header"><h2>' . $newTitle . '</h2></div>';
print(str_replace($needle, $replace, $html));

With this code the third page of a building tabulation might appear as shown below.

If needed, you can add a class to the opening table tag by calling the display() the following way. Let's say that the class you want to add is “myClass.” Then you would invoke the display method this way

$html = $myTable->display(0, "myClass");

For more information on the methods of the WebblerListing class, consult the class definition in commonlib/lib/interfacelib.php under the admin directory of phpList.

Informing or warning your users

To include a notifice on a plugin page you can use the function Info($message). This function returns a string, which can be printed. The result is a yellow box containing your message with a darker yellow border, as shown below.
The message can be HTML, as in the example, or just plain text. Clicking the close button will cause the box to disappear. The box will not be shown again for the rest of the user's session.

If you do not want the user to be able to close the box, print Info() with true as a second parameter.

print(Info($message, true));

You can similarly include a warning on a plugin page, using the function Warn($message). The warning appears with red colored text in a red bordered box as seen in the image below. Again the message may be HTML or the plain text seen in the this example. Notice that the text is always centered in the box and that there is no close button.

The Warn() function does not create a string. The function prints the HTML directly on the page.

Adding pages to the menus

To make your plugin pages show up in the menu system, you need to set a few variables in your plugin.

  public $pageTitles = array( // Entries in the plugin menu of the dashboard
     'pluginpage' => 'Description of this page in my plugin',
  );
  
  public topMenuLinks = array( // Entries in the top menu at the top of each page
    'pluginpage' => array('category' => '//[category]//'),
  );

The pluginpage is a file in your plugin called “pluginpage.php”. It lives in the directory you indicated with “coderoot”.

The description is used to build the link in the menu.

The category needs to be one of the following:

 * subscribers
 * campaigns
 * statistics
 * system
 * config
 * develop (will only show up in "dev" mode)
 * info

To ensure that your pages appear in the plugin menu of the dashboard, you should override the adminMenu() method of the defaultplugin.php with the following code.

function adminMenu() {
    return $this->pageTitles;
}

If your plugin has no web pages you should simply return an empty array here. Note: If you do not override this method, the defaultplugin object will add pages to the menu on its own, including a fictitious “Hello World” page.

Linking inside phpList

It is not necessary to print raw HTML in order to create links between pages of your plugin or links to other pages in phpList; phpList provides the following functions to do this. For the sake of security, you should use the phpList functions rather than attempting to create a link using hand coded HTML.

The function for definition for each of phpList link-generating functions is found in the file connect.php. Each function returns a string. That string must be printed in order to appear in your page.

Note that the links produced by these functions usually include a token to avoid cross-site request forgery (CSRF). The token appears at the end of the URL in the form &amp;tk=hexno where “hexno” is a hexadecimal number with a random length up to 32 hex digits.

PageLink2()

This function has several arguments, for many of which default values are provided, as seen in the following declaration:

function PageLink2($page,$linkText="",$more_data="",$no_plugin = false,$title = '')

(I have changed the name of some of the arguments to indicate their purpose more clearly.)
If you are using this to link to a page, say examplePage.php, of your plugin, say “examplePlugin,” you might call this function as follows

PageLink2("examplePage", "This is an important link")

This would generally return a string with random hexadecimal number at the end, looking something like the following:

<a href="./?page=examplePage&amp;pi=examplePlugin&amp;tk=500af9b321cb9af" title="">This is an important link</a>

If you want to pass a piece of data to your page in the query part of the url, you can do that using the third argument of the function.

PageLink2("examplePage", "This is an important link", "mydata")

if you want to pass the value of a quantity 'param' as 'mydata', you would do it as follows:

PageLink2("examplePage", "This is an important link", "param=mydata")

You could similarly pass values for two quantities:

PageLink2("examplePage", "This is an important link", "param1=data1&amp;param2=data2")

if you want to link to a phpList page outside your plugin,say setup.php, you must set $no_plugin to true, as in the example below.

PageLink2("setup", "This is an important link", "", true)

PageLinkClass()

The arguments of this function are very similar to those of PageLink2():

function PageLinkClass($page,$linkText="",$more_data="", $class="",$title = '')

The function returns a string quite similar what is returned by PageLink2(). Notice though that the argument $no_plugin is replaced by $class.

This function can be used to produce a link only to pages in your plugin, but not outside your plugin. However, you do have the option of defining a class for the link. So the following call

PageLinkClass("examplePage", "This is an important link, "", "myclass")

produces a string like the following:

<a href="./?page=examplePage&amp;pi=examplePlugin&amp;tk=500af9b321cb9af" class="myclass" title="">This is an important link</a>

PageLinkButton()

The arguments for this function are exactly the same as the arguments for PageLinkClass().

function PageLinkButton($page,$linkText="",$more_data="", $class="",$title = '')

This function returns a string that will produce a clickable HTML button when printed. The button links to the specified page of your plugin and is labeled with the link text.

A disadvantage to this function is that it cannot be used to produce a button linking to a page outside your plugin.You can most easily create such a button using the PHP str_replace() function.

Let's say once more that your plugin is the “examplePlugin”. Then if we wanted to make a button linking to the phpList eventLog page with the label “View Event Log”, the following code would produce a string creating the desired button when printed:

str_replace("&amp;pi=examplePlugin", "", PageLinkButton("eventLog", "View Event Log"))

Using Javascript in your plugin

phpList loads both jQuery (version 1.71 in phpList v3.12) and jQueryUI (version 1.81 in phpList v3.12). Also loaded are jCarouselLite, the JQuery table drag and drop plugin, as well as the jQuery scrollable tool. The jQueryUI javascript includes all of the jQueryUI widgets, at least all of the widgets available at the time of the release of the version of jQueryUi that is loaded.

Depending on the value of use_minified_assets the jQuery is loaded in the header or the footer of the page. By default use_minified_assets is true, which means it is loaded in the footer.

To ensure that your Javascript works, particularly when it depends on jQuery, you need to add it to a global variable, pagefooter.

 $pagefooter['unique_value'] = '<script type="text/javascript"> 

   // put your JS here

  </script>

Make sure to use a proper unique value for unique_value, for example, based on the name of your plugin, to avoid it clashing with other plugins. A way to create a good unique value for the index is to simply append the name of your page (without ”.php“) to the name of your plugin. For example if your plugin is MyCoolPlugin and you are writing code for myCoolPage.php, you might take “MyCoolPlugin_myCoolPage” as your index for the $pagefooter array.

Javascript modal dialogs

As described in the previous topic, phpList automatically loads jQuery and jQueryUI.

You are free to use any of the tools provided by these Javascript libraries. However, you should note that jQueryUI requires a stylesheet to provide its functionality. This stylesheet is loaded with Phplist. However, Phplist loads a stylesheet, style.css, after the jQueryUI stylesheet. Thus, this Phplist stylesheet can, and in fact does, override some of the styles that jQuery requires in its widgets. This can result in a widget failing to produce the behavior described for it in the jQueryUI documentation.

This is certainly the case with jQueryUI modal dialogs. Vertical positioning does not work. This is due to a style loaded with style.css. A modal dialog is centered horizontally by default, as it is supposed to be, but the dialog appears at the top of the page, despite all reasonable efforts to restore the default vertical centering.

It is good practice to put scripts at the bottom of a webpage, after your HTML. The default vertical centering of modal dialogs can then be restored by adding the following style after </script>.

<style>
.ui-dialog{top:30% !important}
</style>

If you should have trouble with a jQueryUI widget, You should check out the styles that the widget needs to function, and then check style.css to see if those have been overridden. (Mariela Zarate must be credited with developing the fix for the vertical positioning of modal dialogs.)

Building an Ajax server page

Since phpList does load jQuery, you can use Ajax in a Javascript section added to a plugin web page or form. You can make the Javascript call using the jQuery functions $.ajax() or more simply $.post(). The URL for such an Ajax call would be the following

index.php?page=exampleAjax&pi=examplePlugin&ajaxed=1

or more concisely

?page=exampleAjax&pi=examplePlugin&ajaxed=1

or alternatively

./?page=exampleAjax&pi=examplePlugin&ajaxed=1

where the plugin name is examplePlugin and the Ajax server page is exampleAjax.php.

Including ”ajaxed=1“ in the URL is not absolutely necessary, but it will suppress some unwanted output from phpList and possibly speed up the processing of the Ajax call.

An Ajax call make take some time to complete. A convenient spinner to display on the calling page while waiting is found at the relative URL images/busy.gif.

A page to be called through Ajax is not really a command line page, although it does share some features with a command line page. First, you should begin your page with the following code:

if (!defined('PHPLISTINIT')) die(); ## avoid pages being loaded directly

Second, just as with the command line page, you must have the following statement before you produce any output, in order to prevent unwanted output from index.php being sent to the caller.

ob_end_clean();

The output of your page must be a string, which you can return to the caller with an echo or print() statement.

After completing its output, the page should either exit; or die();. You should NOT conclude the output with a return; since you will be returning to index.php which may produce output unexpected by the Ajax caller.

Although an Ajax server page is like a command line page, phpList does not consider it to be a command line page and the page does not need to be entered into the $commandlinePluginPages array.

There is another way to do Ajax. You might do it the way phpList does.

How phpList does Ajax

Instead of using the jQuery functions $.ajax() or $.post(), phpList uses the load() method of a jQuery object to load the body of phpList page into the parent DOM element containing an “ajaxable” link. The page is a very simple page containing just the result of whatever operation is specified by the link.

This operation is set up by phpList when index.php loads.Among other things the $(document).ready() method attaches a click handler to each link belonging to the CSS class “ajaxable”. This handler changes the 'href' of the link, so that it points to the pageaction page. The pageaction page prints a complete, but very simple web page. Then the .load() method of the parent element of the link is used to replace all the HTML contained in that element with the page body, including the link.

The file pageaction.php does not do much by itself. It produces simple versions of the required HTML headers and a body tag. The work is done by the file included from the admin/actions/ directory. The included page is specified by the 'action' argument of the URL passed to the load() method. After the page is included, pageaction.php prints a closing body tag, so that a complete proper web page is produced.

if the action argument of the URL is '&action=actionpage', then actionpage.php will be included from the admin/actions/ directory. If no action argument is included in the original 'href' of the link, the name of the action page is taken to be the same as that of the page in which the link is found.

This method of doing Ajax has the advantage that no additional Javascript is required for a link to create an Ajax call. All that is required is to create an appropriate action page, to place it in the actions subdirectory, and to add an 'action' argument to the URL referenced by the link.

The disadvantage is that doing Ajax this way very much limits what can be done. All that you can do is to fill a DOM element with the HTML loaded from the action page. There is no way to do any post-processing of the data returned from the call.

Doing Ajax the phpList way

A plugin can have “ajaxable” links too. The Javascript click handler will send a call to your plugin. To process this call, the plugin must have a file pageaction.php in the coderoot directory of the plugin.

The action pages for the plugin can be located in any convenient location inside the coderoot directory of the plugin. The simplest procedure might be to locate these pages inside an 'actions' subdirectory inside coderoot.

Your file pageaction.php should contain the following code.

@ob_end_clean();
@ob_start();

verifyCsrfGetToken(); // Prevent cross site request forgery

if (is_file(dirname(__FILE__).'/ui/'.$GLOBALS['ui'].'/pagetop_minimal.php')) {
	include_once dirname(__FILE__).'/ui/'.$GLOBALS['ui'].'/pagetop_minimal.php';
}

$status =  s('Failed');

... Include the proper page here, determined by the action argument.
... This page should produce a string value for the $status variable.

print $status;
print '</body></html>';
exit;  

You can produce 'Ajaxable' links using phpList linking functions discussed above. The pageLinkClass() function should be called with the $class argument set to be “ajaxable”. If you are, for example, creating a link on a page, examplePage of your plugin, you might then call the function pageLinkClass() as follows:

PageLinkClass("examplePage", "Do something", "action=myAction", "ajaxable")

This would produce the following string to be printed on your page:

<a href="./?page=examplePage&amp;action=myAction&amp;pi=examplePlugin&amp;tk=500af9b321cb9af" class="ajaxable" title="">Do something</a>

If the 'action' argument is omitted, the string becomes

<a href="./?page=examplePage&amp;pi=examplePlugin&amp;tk=500af9b321cb9af" class="ajaxable" title="">Do something</a>

In this case the click handler will be set the 'action' argument to the original value of the 'page' argument. Your pageaction should then look for an action having the same name as the page in which the link occurs, here “examplePage”.

An 'Ajaxable' button can be created in a similar fashion with the pageLinkButton() function by setting the $class argument to “ajaxable” and by setting (optionally) the $more_data argument, for example, to “action=myAction”.

NOTE: To insure that Ajax works properly, you should always use the name of the page on which the “ajaxable” link occurs as the first parameter of these linking functions.

Also note, that these links can be made to work, but without the Ajax interactivity, if the user has Javascript turned off in the browser. How to set that up is left as an exercise for the reader.

Writing plugin pages for the command line

Many Phplist pages can be run from the command line. Similarly it is also possible to set up some pages of a plugin to be run from the command line.

Let's assume that you have written a plugin named Xplugin. Remember that Xplugin will have a class definition extending the phplistPlugin class. It will be a subclass of that parent class.

Suppose you want to create pages XApage.php and XBpage.php to be run from the command line. These pages must go directly into the directory pointed to by $coderoot property of your plugin subclass. These pages cannot go into a subdirectory of that directory. They must be directly under the directory pointed to by the $coderoot property.

Further you must have a property $commandlinePages in the definition of the Xplugin subclass. This property will be an array of pages that can be run from the command line. But the pages are entered into the array without their .php extension, as follows:

public $commandlinePages = array ('XApage', 'XBpage');

It is not necessary to specify the extension here, because only plugin pages with a .php extension can be run from the command line.

Note that consulting defaultplugin.php on this point can be very misleading. In that file you will find the following lines:

  # These files can be called from the commandline
  # This should hold an array per file: filename (without .php) => path relative to admin/
  public $commandlinePluginPages = array();
 

The second comment here is dead wrong. The path is not relative to admin/. It is relative to the plugin root directory specified in the $coderoot property. Furthermore, the path cannot include any directories under the plugin root directory. Also the array does not need to be an associative array; a simple array of filenames (without .php) works just fine.

Running Your Pages From the Command Line

It is possible to run certain Phplist and plugin pages using the command line version of PHP. All pages must be invoked through the Phplist index file index.php. Following index.php are the arguments that are passed:

  • -p: the file name (without .php) to be run
  • -m: the name of the plugin to which the page belongs. This argument is not needed for Phplist pages, such as processqueue.
  • -c: the path to the Phplist config file.

You should have no space between the letter representing the argument, for example -p, and the value of the argument.

Suppose that the path to the phplist directory is /myhome/public_html/phplist/. Then you could process the Phplist queue with the following invocation of processqueue.php

php /myhome/public_html/phplist/admin/index.php -c/myhome/public_html/phplist/config.config.php -pprocessque

To run XApage.php of your plugin Xplugin, the invocation would be

php /myhome/public_html/phplist/admin/index.php -c/myhome/public_html/phplist/config/config.php -pXApage -mXplugin

You cannot run any page of your plugin without passing in the name of the plugin as the -m argument to index.php.

If you want to run your page as a cron job, you must specify the complete path to the relevant php interpreter for your installation. So php in the invocation above must be replaced by:

/path_to_PHP/php

See the explanation for this requirement published by the people at Dreamhost. if you want to suppress all output, as you probably will if you run the page with cron, you should redirect the output to /dev/null.

Composing a Page

As shown in the Building Pages section above, you should not allow your page to be run except through Phplist itself. So at the top of your code you should have:

if (!defined('PHPLISTINIT')) die(); ## avoid pages being loaded directly

If you are planning to print to the command line, you should next have the following:

ob_end_clean();

The Phplist index file writes a lot of HTML to an output buffer, even though you are calling the file from the command line. The function above will throw away the buffered material, so that the only output is what your page writes (and of course whatever error messages result from your code.)

If you want your page to serve as a web page as well, you can use the $GLOBALS['commandline'], which Phplist sets to 1 when called from the command line and 0 otherwise. Here is an example:

<?php>
if (!defined('PHPLISTINIT')) die(); ## avoid pages being loaded directly
if ($GLOBALS["commandline"]) {
  ob_end_clean();

  Your command line stuff
  
  ...
  
 } else {

  Your web stuff
  
  ...
  
 }

...

  Function definitions used in either mode
  
...

?>

Command Line Arguments

Besides the -p argument identifying your plugin page and the -m argument identifying the name of your plugin, you might want to pass other other arguments to the page. For example, you might want to pass two arguments, say arg1 and arg2. You might pass these, say, as the -x and -y options. What letter you use for each is up to you. You just have to use a single letter, other than p or m.

Phplist is very kind to you; you don't have to worry about picking the values of these options off the command line yourself. You will find those values saved for you in the $cline array, keyed by the single that you have chosen to identify each option. Thus to use the value of argi, you would refer to $cline['x'], and $cline['y'] would be the value of arg2.

So if, for example, you need to get the particular plugin page a method is running on, you could find that as $cline['p]. Remember that to use the $cline array inside a inside a plugin method, you need to declare it as global there.

global $cline;

Alternatively you can use the $GLOBALS array, which does not require such a declaration. So you could, for example, get the value of arg2 as $GLOBALS['cline']['y'].

The name of the page running is also available in the superglobal $GET. Thus the name of the page can be retrieved as $GET['page'].

Public pages

A plugin can provide a public page, similar to the phplist subscribe or preferences pages. You can create a URL pointing to your page using the following code:

$theURL = $GLOBALS["public_scheme] . "://" . getConfig("website") . $GLOBALS["pageroot"];
$theURL .= "?pi=myplugin&p=mypage"; 

This code would produce a URL similar to the following:

http://www.example.com/lists/?pi=myplugin&p=mypage

NOTE: if you are generating a link to a public page from a command line script, the item $GLOBALS[“publicscheme] is always set to 'http'. In the phpList web pages, the default behavior is to set $GLOBALS[“publicscheme] to whatever protocol the administrative pages are using. This default behavior does not occur in command line scripts.

If you need the 'https' protocol to be set in links created by a command line script, you should either use 'https' explicitly in creating the link or alternatively have your users enter the following line into
config.php:

define('PUBLIC_PROTOCOL','https');

Alternatively you can uncomment the corresponding line in config_extended.php. In phpList v3.012 it's line 806.

Configuring your plugin to use public pages

To provide a public page, a plugin has to override the $publicPages variable defined in admin/defaultplugin.php. Include this line in the class definition of your plugin:

public $publicPages = array('mypage');

As for admin pages, each page must have a corresponding file in the $coderoot directory, e.g. mypage.php.

A page can then generate its content in any way it wants. It has access to any URL query parameters through $_GET and to the phplist database, as well as constants defined in the phplist configuration files and the functions defined in admin/connect.php and admin/lib.php, as well as in the files included by these libraries. In particular, your plugin will be instantiated so that you can use the methods defined in your plugin class, e.g., accessing such a method as in the following example:

$GLOBALS['plugins']['myplugin']->mymethod(arg1, arg2);

where of course the number of arguments depends on your definition of the method.

The output does not automatically use the “Header of public pages” and “Footer of public pages” defined on the Settings page.
If you want the page to have the same theme as other phplist pages then the plugin must add those to the output.
See the function confirmPage() in the file index.php for an example of how to use the header and footer, and also to set the page title

  $res = '<title>'.$GLOBALS["strConfirmTitle"].'</title>';
  $res .= $GLOBALS['pagedata']["header"];
  $res .= '<h3>'.$info.'</h3>';
  $res .= $html;
  $res .= "<p>".$GLOBALS["PoweredBy"].'</p>';
  $res .= $GLOBALS['pagedata']["footer"];  

Authentication

By default a page is totally public, visible to anyone, similar to the subscribe page. If you want a page to be available only to subscribers, similar to the preferences page, then the plugin will need to implement authentication using the subscriber UID e.g.

http://www.example.com/base_Phplist_directory/?pi=myplugin&p=mypage&uid=xxx

The plugin should then validate that the UID is for an existing phplist subscriber.

Packaging and publishing

When you are ready to publish your plugin, please make sure that you have a composer.json file in the root of your plugin repository, which contains at least the “name” and “type” attributes.

The type should always be “phplist-plugin”. The name should be a unique name for your plugin, probably best is to make it the name of your repository.

An example is

 {
   "name": "phplist/phplist-plugin-YOURPLUGINNAME",
   "type": "phplist-plugin",
   "description": "Some plugin for phpList that does something",
   "keywords": [
     "phplist",
     "email",
     "newsletter",
     "manager",
     "permission marketing"
   ],
   "homepage": "https://www.phpList.org/",
   "license": "GNU Affero General Public License version 3.0 or later (AGPLv3+)",
   "support": {
     "issues": "https://github.com/phpList/phpList3/issues",
     "forum": "https://discuss.phplist.org/",
     "wiki": "http://resources.phplist.com/",
     "source": "https://github.com/phpList/phpList3"
   },
   "require": {
     "php": ">=7.0"
   }
 }

From version 2.11.8 onwards, phpList has an auto-installer for plugins. This currently works with GitHub only. The plugins directory that PLUGIN_ROOTDIR points to needs to be webserver-writable for this to work.

In order to publish your plugin, you can create a GitHub project called phplist-plugin-YOURPLUGIN.

Your plugin needs to have a directory called plugins in which you place the helloworld.php and the helloworld directory with files.

You can create a project that contains more than one plugin, by adding them in the plugins directory.

Once you have created the project, you can point to the auto-zip link in GitHub to allow your plugin to be downloaded and installed

https://github.com/{YOUR GITHUB NAME}/phplist-plugin-{YOURPLUGIN}/archive/master.zip

Once you've done this, add your plugin to this wiki. This will involve several steps.

  1. Register as a user of this wiki.
  2. Register for the PHPlist Developers list at https://lists.sourceforge.net/lists/listinfo/phplist-developers/
  3. Submit a request for write-access to this wiki. Send your request to the PHP Developers mailing list.
  4. After receiving write-access, create a page in the plugin namespace. Just load this URL in your browser:
     <code>https://resources.phplist.com/plugin/{YOURPLUGINNAME}</code>

Write your page, using some of the source of the other plugin pages as a model.

Multiple plugin locations

To make it easier to develop and test plugins with phplist running locally you can use the PLUGINROOTDIRS constant (note the plural S). This takes a semi-colon separated list of additional directories each of which can contain plugins. This way you can have phpList use a plugin directly in its git repository, instead of having to copy the plugin files to the PLUGINROOTDIR directory.

For example, if you have two plugins under development each with its own git repository then the directory/file structure will be simlar to this:

/home/me/git/phplist-plugin-myplugin1/plugins/myplugin1.php
/home/me/git/phplist-plugin-myplugin1/plugins/myplugin1/
/home/me/git/phplist-plugin-myplugin2/plugins/myplugin2.php
/home/me/git/phplist-plugin-myplugin2/plugins/myplugin2/

In this case add a definition for PLUGIN_ROOTDIRS that identifies the “plugins” directory in each repository:

define("PLUGIN_ROOTDIRS","/home/me/git/phplist-plugin-myplugin1/plugins;/home/me/git/phplist-plugin-myplugin2/plugins");

phplist will then use these directories in addition to that identified by PLUGIN_ROOTDIR when finding and loading plugins. Now any changes to the files in the git repositories will be used immediately by phplist.

develop/plugins.txt · Last modified: 2023/10/27 20:17 by duncanc

Resources
Contact Us

e: info@phplist.com

Boring Bit

phpList is a registered limited company

UK Company No. 07582966

VAT Registration 122 0788 37

© phpList 2016