| | *Maintain 3* comes with a sophisticated [module system|Wordpress Style Modules], allowing developers to flexibly extend its functionality and admins to just load the modules they need for their modules without carrying around unnecessary overhead. |
| | |
| | When you want to contribute new functionality to Maintain, you therefore almost always want to write a module instead of changing the core codebase of Maintain. This document shows you, how. |
| | |
| | If you feel your idea has to be implemented in the core codebase rather than a module, please [contact us|http://maintainproject.osuosl.org/contacts] and let us know about your considerations. |
| | |
| | h2. Starting off |
| | |
| | To find out how about the code contribution process, please read the [Maintain Code Contribution Guide] before you start writing code. That guide also tells you about how to get a copy of the Maintain code base from SVN. |
| | |
| | To contribute modules, you will also need a copy of the Maintain module repository, which is separate from the core codebase. You can receive it through the following command, executed from somewhere _outside_ your maintain working copy. |
| | {noformat} |
| | $ svn co https://svn.osuosl.org/public/maintain-modules/trunk maintain-modules |
| | {noformat} |
| | |
| | h2. Guide: Installing and enabling a module |
| | |
| | To make you comfortable about linking modules into your instance of Maintain, we will walk you through installing and setting up an example module called _Message of the Day_ (motd). |
| | |
| | *Putting it into place:* All modules you want to install have to be in separate directories underneath the {{$maintain/modules}} directory. What you want to do when making a new module is symlinking it into the modules directory of your running Maintain development instance. If you ever have to remove it from there, it will be quite easy. |
| | {noformat} |
| | $ cd $maintain/modules |
| | $ ln -s $maintain-modules-workingcopy/motd . |
| | {noformat} |
| | *Configuring the module:* A lot of modules need some sort of configuration. Module config files are called {{modulename.cfg}}. Every module requiring such a file should ship with a {{modulename.cfg.dist}} file that contains example config values and ideally sufficient documentation on what config values are valid. Good distribution config files also already have meaningful default values in them, so that you can at least _run_ Maintain without problems after you installed the module. |
| | |
| | Config files are in [PHP's .ini file format|http://php.net/parse-ini-file]. |
| | |
| | So in our example, make your own config file by copying the distributed one: |
| | {noformat} |
| | $ cp motd.cfg.dist motd.cfg |
| | {noformat} |
| | and editing it as desired with your favorite UNIX text editor (or not at all, if you are fine with the default settings therein). |
| | |
| | Afterwards, switch to the directory {{$maintain/bin}} and execute the following two commands: |
| | {noformat} |
| | $ ./modules.php -i motd |
| | motd successfully installed |
| | $ ./modules.php -e motd |
| | motd successfully enabled |
| | {noformat} |
| | Now your module should be up and running\! In this example, go to the login page and note that there is a message being shown underneath the login form. |
| | |
| | Now you know how to install a module. |
| | |
| | h2. Writing your own module: Case Study |
| | |
| | The easiest way to start writing your own module is duplicating another one and replacing the interior parts of it. |
| | |
| | How modules work: |
| | * All Modules in Maintain inherit from the *Module class*, which resides in {{$maintain/modules/module.php}}. To know which functions you can (or even have to) override in your module, this is the place you have to look at. |
| | * There are some, so-called *core modules* that inherit from Core_Module, which is just an empty, abstract subclass of Module. The difference between a _normal_ module and a _core_ module is that core modules provide functionality that is vital for Maintain to run. Therefore they can not easily be disabled or even uninstalled. You should only make a module "core" if there really is no way to run Maintain without it. Most additional functionality will therefore not be flagged core. |
| | {noformat} |
| |  | Module |
| | |
| | |
| | |
| | |
| | | |
| | |
| | |
| | |
| | |
| | /|\ |
| | (vital functionality modules) |
 | | |
| | {noformat} |
| | |
| | Let's take a look at the (simplified) source code of motd.php: |
| | {code:none} |
| | <?php |
| | require_once(MODDIR.'/module.php'); // (1) |
| | |
| | /** |
| | * Displays a message of the day on Maintain's login page |
| | */ |
| | class Motd extends Module // (2) |
| | { |
| | |
| | function needsConfigFile() { // (3) |
| | return true; |
| | } |
| | function _install() // (4) |
| | { |
| | return true; |
| | } |
| | function _uninstall() |
| | { |
| | return true; |
| | } |
| | function initialize() {} // (5) |
| | |
| | function register_hooks() // (6) |
| | { |
| | Module::add_filter('login_motd', '_motd', null, NO_ACCESS); |
| | } |
| | |
| | function _motd($container) // (7) |
| | { |
| | $thetext = "Welcome to Maintain"; |
| | |
| | $container->add(html_br()); |
| | |
| | $container->add($thetext); |
| | return $container; |
| | } |
| | |
| | } |
| | {code} |
| | I numbered the important parts of the file for you: |
| | |
| | *1:* |
| | On top of your file, you should / have to {{[require_once()|http://php.net/require_once]}} the {{module.php}} file so that the Module class is known to the PHP interpreter. Make sure not to use {{\[require()\|http://php.net/require}} without {{\_once}}, because otherwise you will get an error for redefining an existing class. |
| | |
| | *2:* |
| | As mentioned before, our module extends the Module class, as (almost) every other module. |
| | |
| | *3:* |
| | {{needsConfigFile()}} is a special function that is supposed to return true if (surprise\!) your module needs a config file. If not, or if your config file is optional, you can either return {{false}} here or just not override this function, since {{false}} is the default. |
| | |
| | *4:* |
| | The functions {{\_install()}} and {{\_uninstall()}} are special functions that get called when installing/uninstalling your module. They can contain things like creating database tables, making files and folders or checking preconditions for your module to work (such as the presence of a specific command on the system Maintain is running on). |
| | |
| | These functions have to return {{true}} if their install operations were successful, {{false}} otherwise. Make sure to return {{true}} here even if you don't have anything to do. They default to {{false}} which will cause there installation/uninstallation to fail. |
| | |
| | *5:* |
| | {{initialize()}} is a *constructor*. Well, not really. But it is being called by the constructor as the first function ever executed on a new object. Advantages of this method over overriding the actual constructor: You don't have to worry about calling the parent constructor (which is vital because it does important things). |
| | |
| | *6:* |
| | {{register_hooks()}} is a central function in the Maintain module system. Here your module has to register itself to so-called module hooks. If and only if it connected itself here to some hook, it will be called later, when module hooks are executed. Note that you can hook your module up to any named hooks, even if they don't exist yet. This allows you to introduce your own module hooks in your modules and to respond to them with another function of the same module. To learn more about the {{register_hooks}} syntax as well as how to introduce your own hooks, look at the [Module System documentation|Wordpress Style Modules]. |
| | |
| | *7:* |
| | This is where the magic happens: Your own, private module function. Or one of them. Give them any name you want, and do whatever you need inside of them. Note that the second parameter used in the {{register_hooks}} call in step 6 is '_motd' which is the name of this very function. Whenever the {{login_motd}} hook is executed, it will instanciate your module and call the {{\_motd()}} function for you. |
| | |
| | h2. Naming Conventions |
| | |
| | These are easy: Modules have to be in a directory underneath the {{modules/}} folder. If your module is named {{myModule}}, you have to name your folder {{myModule}}, your actual module file {{myModule.php}} and your config file {{myModule.cfg}}. That's easy, isn't it? Note that module names are case sensitive. |
| | |
| | h2. Actions and Filters |
| | |
| | There are two types of *module hooks*: *Actions* and *filters*. There is good documentation on how they work on the [Module System documentation|Wordpress Style Modules] wiki page. Read it. The sections about {{apply_filter}} and {{do_action}} in that document will shortly explain to you what actions and filters do, and how. |
| | |
| | In our example up here, in _step 7_, the {{_motd_}} _function is called by a \_filter hook_ called 'login_motd'. It gets a (not necessarily empty) [PhpHTMLLib|http://phphtmllib.newsblob.com/] [container|http://phphtmllib.newsblob.com/doc/phpHtmlLib/Container.html] which it can modify arbitrarily. Afterwards, it returns the modifed container to the caller. It will then be taken and either passed on to the next callee (if there is one) or eventually it will just be displayed. |
| | |
| | For more information on what action and filter hooks are currently available in Maintain, see the [module hook list|Maintain Module Hooks]. This list will grow every time a new hook is added to the Maintain core. |
| | |
| | h2. Important Globals |
| | |
| | There are two types of variables available to a method being called by the module system. One of them are, obviously, the *parameters* passed to the function itself. For filter hooks, this is mainly the object being "filtered" (often a PhpHTMLLib container or a string), but also other parameters are possible, depending on the hook (see the hook list). |
| | |
| | The second part is global and superglobal variables. There are two most important global variables Maintain offers for every part of the application: |
| | * *$user* is the user object of the currently logged in user. To find out what the object can do, inspect the file {{$maintain/class/dao/extended/user.php}}. |
| | * *$conf* is the global config file array. It represents the settings in your {{maintain.cfg}} file with the format: {{$conf\['section'\]\['item'\]}}. |
| | |
| | If your module has a config file, there is a similar array available for you to use. See the *config files* section below. |
| | |
| | As for superglobal variables, all of PHP's superglobals are available. Noteworthy: {{$_REQUEST\[\]}} contains all POST and GET variables which are important for module webpages. See the section on *modpages* below. |
| | |
| | h2. {anchor:configs}Config files |
| | |
| | As mentioned above, you can have a config file, optionally or mandatory, for your module. It has to be called {{modulename.cfg}}. The format is pretty easy: It's [PHP's .ini file format|http://php.net/parse-ini-file] just like the main Maintain config file. |
| | |
| | h2. {anchor:module-security}Module Security |
| | |
| | Modules have an optional parameter to allow disabling of the module without running any of the module code. This parameter is a constant; if the user access level is lower than the constant, that module will not be executed in the maintain module engine. For example, you have a module that hooks into 'action_module': |
| | {noformat} |
| | function register_hooks() |
| | { |
| | Module::add_filter('action_module', 'doAdminOnlyAction', null, ADMIN); |
| | Module::add_filter('action_module', 'doUserAction', null, ZONE_USER); |
| | } |
| | {noformat} |
| | The function {{doAdminOnlyAction($args)}} would only be executed if the user were an admin, whereas the function {{doUserAction($args)}} would only be run by someone who was a zone user, zone admin, or admin. Other security checks are the responsibility of the module writer. |
| | |
| | h2. Module Dependencies |
| | |
| | (NOTE: This functionality was implemented POST-3.0.0, meaning this will only be available in the SVN snapshots until another Maintain release) |
| | |
| | Maintain now supports module dependencies. Users may specify by name modules that they want their module to be dependent on, and Maintain will ensure that the required modules are installed and enabled before the users module may be installed and enabled. Also, once all the modules are up and running, Maintain ensures that modules that are required by other modules are not disabled/uninstalled out from under the dependent module. |
| | |
| | Taking advantage of this feature requires that you only know the names of the modules that you want your module to be dependent on. Then, overload the \_depends() function such that it returns the name/s of the modules your module requires. |
| | {noformat} |
| | function _depends() |
| | { |
| | //returns a single string, the name of the required module. (CASE SENSITIVE!) |
| | return "some_module"; |
| | } |
| | {noformat} |
| | Furthermore, you may return an array of strings if your module has multiple dependencies. |
| | {noformat} |
| | function _depends() |
| | { |
| | //an array of strings, each a name of a required module. (CASE SENSITIVE!) |
| | $ary = array("some_module", "another_module"); |
| | |
| | //simply return the array |
| | return $ary; |
| | } |
| | {noformat} |
| | After you specify which module/s your module requires, you can rest easy because there is no way that your module will install without it's required modules being installed. Also, your module will not enable unless all its required modules are enabled. |
| | |
| | Once your module is installed and enabled, Maintain will not allow the modules that your module requres to be disabled/uninstalled out from underneath your module, essentially providing a safer environment for making specialized modules that require the functionality of others, as well as many other helpful uses. |
| | |
| | h2. Documenting Modules |
| | |
| | Please take the time to document your modules correctly. There are three useful things you should think about providing: |
| | * a *README* file. Tell people what your module does (maybe even _how_), who you are, and how they can contact you. This makes them comfortable about using your module and chances are, if there is a tiny little bug buried somewhere in the module, somebody will help you fix it. |
| | * Document your *classes and functions*. Maintain uses the [phpDoc|http://www.phpdoc.org/] standard throughout its code so that developers can immediately see what a method is supposed to do, which parameters it takes and what it will return. To make others understand your code, document as you go. If you don't document your code, people might not use your module, and chances are it won't be included in Maintain's module list because undocumented modules are hardly maintainable. |
| | * Comment your *distributed config file* ({{modulename.cfg.dist}}). If you tell people what the allowed values are for every single config option, you make it as easy as the ABC for them to use your module. If they have to read through your whole module code first to guess what else they could put in as a config option, you make it unnecessarily hard for them to use your module. |
| | |
| | As always: If you have questions about correctly documenting your source code, feel free to ask in the [forums|http://maintainproject.osuosl.org/forum] or similar. |
| | |
| | h2. Submitting New Modules to the Maintain Project |
| | |
| | As described in the [Maintain Code Contribution Guide], for every module there should be a task on the [Maintain bug tracking system|http://bugs.osuosl.org/secure/BrowseProject.jspa?id=10000] (JIRA). If there is none by now, read the contribution guide (!) and make one. |
| | |
| | You want to put your module directory and all files therein into a .tar.gz file and attach it to the task in the bug tracking system: |
| | {code:none} |
| | $ cd $maintain-modules-workingcopy |
| | $ tar czf mymodule.tar.gz mymodule/ |
| | {code} |
| | Then go to the task in JIRA and click _attach file_ in the menu on the left hand side. |
| | |
| | The module will be inspected and discussed about by the community in the task's comments section. |
| | |
| | h2. Annex: Special Modules and Functionality |
| | |
| | |
| | h3. Authentication Modules |
| | |
| | Also Maintain's authentication system is modular. This means, you can add arbitrary modules to check user credentials for correctness. Maintain comes with three auth modules by default: |
| | * *auth_maintain:* The default auth module authenticates against the user table in Maintain's database. |
| | * *auth_legacy:* A derivative of auth_maintain authenticates against an upgraded Maintain 2.4 library and updates the DB to the new password hash on login. |
| | * *auth_ldap:* This module allows to authenticate against an LDAP server. |
| | |
| | The inheritance works as follows: |
| | {noformat} |
| | Module |
| | | |
| | Auth_Module |
| | | |
| | (your auth module) |
| | {noformat} |
| | As usual, the easiest way of figuring out how Maintain authentication works is looking at the Auth modules. Some override parts of [PEAR::Auth|http://pear.php.net/package/Auth], for example. |
| | |
| | h3. Search Modules |
| | To integrate your module with the main Maintain search function, you have to override a few more functions from the {{Module}} base class. To see which ones and what they are supposed to do, take a look at the [module search documentation|Maintain Module Search Functions]. |
| | |
| | h3. Module Webpages ("Modpages") |
| | Modules can display their own frontend web pages. To accomplish that, you have to make use of a [module hook|Maintain Module Hooks] named {{mod_page}}. When {{web_root()/mod_page.php?mod=modname}} is called from the web page (preferably through somebody clicking a link), the filter method is going to be executed on this particular module (provided it registered to this hook). An empty phphtmllib container is passed to that method. The method can fill up the container arbitrarily and return it afterwards. |
| | |
| | Multiple pages can be made by adding another GET variable to the URI (e.g. "page") and {{switch()}} ing over {{$_REQUEST\['page'\]}} in the method registered to this hook. |
| | |
| | h4. Modpage Example |
| | There is a module with the following methods (among others): |
| | {code} |
| | class Display_Weather extends Module { |
| | function register_hooks() |
| | { |
| | Module::add_action('module_dashboard_main', "module_link"); |
| | Module::add_filter('mod_page', "page_load"); |
| | } |
| | |
| | function module_links() |
| | { |
| | $link = html_a(web_root()."mod_page.php?mod=display_weather&page=forecast&zip=97330", "Corvallis, OR"); |
| | $cont = container("Useless weather forecast: ", $link); |
| | return $cont; |
| | } |
| | |
| | function page_load($cont) |
| | { |
| | switch(strtolower($_REQUEST['page'])) { |
| | case 'forecast': |
| | if ($_REQUEST['zip'] == '97330') { |
| | $cont->add("This is Oregon, so chances are it'll be rainy"); |
| | } else { |
| | $cont->add("I don't know, sorry."); |
| | } |
| | break; |
| | default: |
| | header('Location: '.web_root()."index.php"); |
| | break; |
| | } |
| | } |
| | } |
| | {code} |
| | The module is registered to two hooks. The first one, {{module_dashboard_main}}, adds a link to the module widget on the main page. As you can see, the link points to {{modpage.php?mod=display_weather&page=forecast&zip=97330}}. |
| | |
| | The second hook the module is registered to is the aforementioned filter hook named {{mod_page}} which implements a module page. |
| | |
| | The user clicks the link. |
| | |
| | Now the _modpage handler_ (modpage.php) reads the request (GET and POST) variables and determines that you asked for the display_weather module. It instanciates the module and calls the method you registered to the mod_page hook. Here, the method is called {{page_load(.)}} and it takes one argument, {{$cont}}. |
| | |
| | {{$cont}} is an empty phphtmllib container which can be filled with the content that is supposed to be shown on the web page. Here, we just add a single sentence. |
| | |
| | You also see here how you can implement several pages: we {{switch()}} for the GET variable named "page" and behave differently for every (legal) page. Obviously you want to link to another, private, method for actual pages, so that you don't clutter the switch statement. |
| | |
| | Also make sure you have a {{default}} statement to account for illegal page choices. |
| | |