====== 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. 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 [[https://www.phplist.org/manual/ch042_phplist-plugins.xhtml|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:[[https://mantis.phplist.com/view.php?id=16865|here]] and [[https://mantis.phplist.com/view.php?id=16923|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 [[plugin automated testing|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 [[https://github.com/phpList/phplist3|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 [[http://adodb.sourceforge.net|ADOdb database abstraction library]], which suports many databases including PostGreSQL and Oracle. The value of the variable $database_module 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() - Returns the array of list names keyed on the list ID. - See// phplist.php//, line 138 * getUserAttributeValues($email, $userid=0, $indexByID=false) - Called by specifying either user email address or ID - Returns an array of values keyed on the attribute name or 'attribute" concatenated with the attribute ID - See //userlib.php//, line 218 * existUserID($userid = 0) - Returns a Boolean false if $userid is not a valid user ID, or the ID back again if it is valid. - 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 [[http://resources.phplist.com/develop/functionndx|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 *Sql_Drop_Table($table) *Sql_create_Table ($table,$structure), and *Sql_Check_For_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 [[http://php.net/manual/en/ref.exec.php|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 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. {{ wiki:unform.png }} 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.{{ wiki:UIP1.png?600 x 392 }} 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. {{ wiki:UIP2.png }} 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 //Sql_Query()// accessed with one of the "Sql_Fetch_" functions, such as //Sql_Fetch_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 = '

' . $oldTitle . '

'; $replace = '

' . $newTitle . '

'; 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.{{ wiki:table1.png }} ==== 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('The list of buildings is empty.', ''); // } 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 = '

' . $oldTitle . '

'; $replace = '

' . $newTitle . '

'; print(str_replace($needle, $replace, $html));
With this code the third page of a building tabulation might appear as shown below. {{ wiki:table2.png }} 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.{{ wiki:info.png }} 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. {{ wiki:warn.png }} 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 [[https://en.wikipedia.org/wiki/Cross-site_request_forgery|cross-site request forgery (CSRF)]]. The token appears at the end of the URL in the form //&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: This is an important link 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&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: This is an important link ==== 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("&pi=examplePlugin", "", PageLinkButton("eventLog", "View Event Log")) ====== Using Javascript in your plugin ====== phpList loads both [[http://jquery.com|jQuery]] (version 1.71 in phpList v3.12) and [[https://jqueryui.com|jQueryUI]] (version 1.81 in phpList v3.12). Also loaded are [[http://www.gmarwaha.com/jquery/jcarousellite/|jCarouselLite]], the [[http://isocra.com/2008/02/table-drag-and-drop-jquery-plugin/|JQuery table drag and drop plugin]], as well as the [[http://jquerytools.github.io/documentation/scrollable/|jQuery scrollable tool]]. The jQueryUI javascript includes all of the [[http://api.jqueryui.com/category/widgets/|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 [[system/config/use_minified_assets]] the jQuery is loaded in the header or the footer of the page. By default [[system/config/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'] = ' 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 [[http://api.jqueryui.com/dialog/|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 ////. 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 [[http://jquery.com|jQuery]], you can use [[https://en.wikipedia.org/wiki/Ajax_(programming)|Ajax]] in a Javascript section added to a plugin web page or form. You can make the Javascript call using the [[http://api.jquery.com/category/ajax/|jQuery functions]] [[http://api.jquery.com/jQuery.ajax/|$.ajax()]] or more simply [[http://api.jquery.com/jQuery.post/|$.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 [[https://api.jquery.com/load/|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 [[https://resources.phplist.com/develop/plugins#example|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 ''; exit; You can produce 'Ajaxable' links using [[https://resources.phplist.com/develop/plugins?&#linking_inside_phplist|phpList linking functions discussed above]]. The [[https://resources.phplist.com/develop/plugins?&#pagelinkclass|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: Do something If the 'action' argument is omitted, the string becomes Do something 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 [[https://resources.phplist.com/develop/plugins?&#pagelinkbutton|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 [[http://us1.php.net/cli|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 [[http://wiki.dreamhost.com/Run_PHP_from_cron|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 [[http://www.cyberciti.biz/faq/how-to-redirect-output-and-errors-to-devnull/|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: 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["public_scheme]// is always set to 'http'. In the phpList web pages, the default behavior is to set //$GLOBALS["public_scheme]// 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. ==== Page header and footer ==== 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 = ''.$GLOBALS["strConfirmTitle"].''; $res .= $GLOBALS['pagedata']["header"]; $res .= '

'.$info.'

'; $res .= $html; $res .= "

".$GLOBALS["PoweredBy"].'

'; $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. - Register as a user of this wiki. - Register for the PHPlist Developers list at https://lists.sourceforge.net/lists/listinfo/phplist-developers/ - Submit a request for write-access to this wiki. Send your request to the PHP Developers mailing list. - After receiving write-access, create a page in the ''plugin'' namespace. Just load this URL in your browser: https://resources.phplist.com/plugin/{YOURPLUGINNAME} 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 PLUGIN_ROOTDIRS 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 PLUGIN_ROOTDIR 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.