Plugin API

From Open-Xchange

The Open-Xchange plugin API is a set of JavaScript objects and classes which can be used to extend the Open-Xchange GUI. The API is very new and still growing. Suggestions about desired functionality are always welcome on the API Suggestions page. As the API evolves, certain interfaces may change or be replaced by newer ones. When this happens, the old interfaces will be marked as "deprecated", but will continue to work long enough to allow all affected code to be updated.

A reference of all currently available classes and functions of the plugin API is available at Programming interfaces (OX6) and Programming Interfaces (AppSuite). The reference for any other version since Service Pack 5 can be created by checking out the corresponding GUI source code from CVS and executing the jsdoc Ant target in the top directory of the GUI source. The documentation is then placed in the directory sdk/jsdoc by default, but the location can be changed by defining the Ant property jsdoc.dir on the command line.

This page complements the API reference as a guide to the creation of a GUI plugin. It starts with a description of a mininmal plugin, which is then expanded with examples of various API functionality.

Hello, World!

First of all, every plugin needs a unique name. This name will be used in many places to avoid conflicts with other plugins which may be installed on the same server. The simplest way to come up with a unique name is to pick any name and prefix it with a domain name you control. The individual parts of the domain should be reversed like in Java (and other languages) package names. If we pick the canonical example domain example.com and name our plugin "test", then its unique name would be com.example.test.

After choosing a name, we can start creating the actual plugin. The entire plugin must be contained in a single directory, which has the same name as the plugin. This directory will be installed on the server, and is the first example where the name must be unique to avoid overwriting other people's plugins.

The entire JavaScript source code for the plugin must be put in a file called register.js. This file is loaded and executed when the GUI is initialized after the user logs in. The primary task of this file is to register the plugin and hook it up inside the GUI.

For the first plugin, we only want to see when our code gets executed. For this, a simple alert will do:

alert("Hello, World!");

That's it! The absolutely minimal plugin is done and ready to be installed.

Installation

To install the plugin, the directory which contains the register.js file is copied to the server into the plugins subdirectory of the GUI installation. For example, if the GUI is installed in /var/www/, then the plugin code will end up in /var/www/plugins/com.example.test/register.js.

To enable the installed plugin, the GUI configuration HTTP API (Module "config") provided by the server must contain an entry for the plugin. This can be accomplished either dynamically (e. g. per-user or per-context) by an OSGi bundle, or statically in a configuration file. We will use the latter method to enable the plugin for testing. The GUI configuration entry is added through a Java properties file in the settings subdirectory of the application server's configuration directory. Usually, this will be /opt/openexchange/etc/groupware/settings/. To avoid conflicts with configuration files for other plugins, again, the unique name of the plugin should be used together with the .properties file extension, e. g. com.example.test.properties. This file can now be used to add arbitrary entries to the GUI configuration by specifying the full path (separated by slashes) as the key and the value for each leaf configuration node as the value of a property. Inner nodes of the configuration tree don't need to be specified explicitly, they have no own values and are created automatically.

The node required to enable the plugin must be a direct child of the node modules and have the same name as the plugin. The plugin is enabled if this node itself has children, or if its value is true. Since our plugin doesn't need any other settings yet, we use the latter variant:

modules/com.example.test = true

After (re)starting the server and logging in, the alert pop-up appears when the plugin is initialized. Since the initialization of all plugins happens immediately after logging in, plugins should perform only the minimum necessary initialization directly in register.js, and do the rest in some callback, e. g. when the user first interacts with the plugin.

Configuration tree

After the plugin itself works, we can add some real functionality to it. Currently, the only place where plugins are supported is the configuration area. There one can add nodes to the configuration tree on the left, and add pages which are displayed when a node is clicked. Nodes can be added directly to the root node of the tree, or to other inner nodes, which look like folders. New inner nodes can also be added. While multiple levels of inner nodes would work, is it discouraged because of too many clicks a user would have to perform to get to the actual configuration pages.

First, we add an inner node named "Example":

new ox.Configuration.InnerNode("configuration/com.example.test", _("Example"));

The above line replaces the alert statement of the "Hello, World!" plugin. After reloading the GUI and clicking on the configuration module, there should be a new entry in the configuration tree. If the alert still appears, the browser cache must be cleared before reloading the GUI, or even better, the Apache module mod_expires should be disabled at least for the directory of the plugin.

The constructor of the class ox.Configuration.InnerNode does all the work of creating the node object and adding it to the configuration tree. It accepts two parameters: the path and the name of the new node. The path of the node is similar to a file path in a directory tree. It specifies the location of the node as a list of path elements, separated by slashes. The root configuration node is called configuration. For the next level, plugins should use their unique name to avoid conflicts. If present, the last level can contain any strings, since it is usually defined by the plugin which created the second level.

The second parameter, the name of the node, is a user-visible string which must be translated to the user's current language. The translation is performed by the function called _ (that is, a single underscore). It is the same name as used by the GNU gettext tools. Most other functions like ngettext, pgettext and npgettext are also available.

Similar to inner nodes, leaf nodes are added by creating an object of the class ox.Configuration.LeafNode:

var node = new ox.Configuration.LeafNode(
    "configuration/com.example.test/account", _("Account"));

The LeafNode constructor takes the same parameters as the InnerNode constructor. This time, the created object is stored in the variable node because we still need it to add a page which is opened when the node is activated:

var page = new ox.Configuration.Page(node, _("Account settings"));

The first parameter is the previously created inner node. The second parameter is the page title which is displayed at the top of the new page:

Test page

Translation

So far, all displayed text of the plugin is always in English. The translation into every desired language must first be provided by a translator. This is done using the Portable Object (.po) file format also used by GNU gettext. First, a PO template (.pot) file needs to be created. This can be done automatically via the Ant build script provided in the sdk directory of the GUI source code. The script should be copied to the plugin directory and modified by inserting the name of the plugin in the following line near the start of the file:

<!-- Plugin name -->
<property name="name" value="com.example.test"/>

After running Ant in the plugin directory, the newly created directory named lang should contain the translation template com.example.test.pot which looks like this:

msgid ""
msgstr ""
"Project-Id-Version: NAME\n"
"POT-Creation-Date: 2009-01-01 12:34+0100\n"
"PO-Revision-Date: DATE\n"
"Last-Translator: NAME <EMAIL>\n"
"Language-Team: NAME <EMAIL>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"

msgid "Example"
msgstr ""

msgid "Account"
msgstr ""

msgid "Account settings"
msgstr ""

This file can be opened in a dedicated tool like KBabel or Poedit, or in a simple text editor as long as it supports UTF-8. The translation of each msgid text is stored as the corresponding msgstr text. A German translation could look like this:

msgid "Example"
msgstr "Beispiel"

msgid "Account"
msgstr "Benutzerkonto"

msgid "Account settings"
msgstr "Kontoeinstellungen"

The first msgid / msgstr pair is a special case. It contains an empty text as msgid and a header with meta-information as msgstr. This information is useful for maintaining multiple versions of multiple translations. The only entry which influences the actual translation is the last one, Plural-Forms. It specifies how to translate text which contains numbers. Unlike with the original GNU gettext tools, this entry is currently not optional in .po files used in the Open-Xchange GUI. The two placeholders INTEGER and EXPRESSION must be replaced with the number of plural forms and a rule for selecting the correct plural form for a number, respectively. A list of these values for most languages can be found in the GNU gettext manual. For German this line would look like this:

"Plural-Forms: nplurals=2; plural=n != 1;\n"

The translated file must be saved in the lang directory with the language ID as file name and .po as extension, e. g. de_DE.po for the German translation. The language ID usually consists of a lowercase two-letter language code and an uppercase two-letter country code, separated by an underscore.

After saving the translated file and reloading the GUI, the configuration page of our plugin should appear translated when a translation for the current language is available, and appear in English when a translation could not be found.

Whenever new user-visible strings are added to the plugin, the build script should be re-run to create an updated PO template, which can then be merged with the existing translations to include the new untranslated strings without throwing away the existing translations. The merging can be done either in one of the PO editors, or manually with the msgmerge tool:

msgmerge -U lang/de_DE.po lang/com.example.test.pot

Widgets

To do anything useful, our new configuration page needs some widgets. A widget is, in general, any visible UI element like an input field or a button. All widgets have some common methods and properties, which are defined in the abstract class ox.UI.Widget. Concrete widgets are members of direct or indirect subclasses of ox.UI.Widget. Intermediate subclasses add common functionality which is useful for many, but not all widgets. For example, ox.UI.Container adds the ability to contain other widgets. Our configuration page is a subclasses of ox.UI.Container and therefore we can add other widgets to it using the addWidget method, which it inherits from ox.UI.Container.

The code to add widgets is placed in the init method of the configuration page. This method is called before the page is displayed for the first time. This is the preferred place to add widgets and perform other initialization, instead of the body of the plugin.

page.init = function() {
    page.addWidget(new ox.UI.Input(_("Mail address")), "email");
    page.addWidget(new ox.UI.Password(_("Password")), "password");
};

ox.UI.Input is a simple text input field with a label. ox.UI.Password is a subclass of ox.UI.Input which does not display the entered password. Both constructors take the translated label as the only parameter. The created objects are passed as the first parameter to the method addWidget of the configuration page. This adds them to the page and automatically aligns the labels and the input fields:

Test page

Communication with the server

The second parameter to addWidget determines how the data of the added widget is handled by its parent container. When the user clicks on the Save button at the top of the window, the configuration page queries all its children and collects their data into a single object. In the example above, the second parameter is a string which specifies the field name in the composite object. When the user enters "user@example.com" as mail address and "secret" as password, the resulting object will be

{ email: "user@example.com", password: "secret" }

To actually do something with the created object, we override the save method of the page:

page.save = function(data, cont) {
    ox.JSON.put(AjaxRoot + "/com.example.test?action=set&session=" + session,
        data, cont, handleErrors);
}

The method gets called when the page needs to be saved (either because the user clicked on the Save button or because the user is about to leave the page). Its two parameters are data, the object which contains the page's data, and cont, a continuation function which should be called once the data is saved successfully. If the continuation function is not called, the data will still be considered as not saved, i. e. the user will be asked to save the data before leaving the page.

In the example above, the parameters are passed unmodified to the static method ox.JSON.put, which encodes the data as JSON and sends it via an HTTP PUT request to the specified URL. Static methods for GET and POST requests are also available. The URL is constructed from the global variable AjaxRoot, which specifies the path of the Open-Xchange application server (usually /ajax), the name of a hypothetical Java servlet, and URL parameters which depend on the servlet. The URL parameter session should be present in every servlet request and contain the value of the global variable of the same name. This parameter is used to protect against XSRF attacks.

The reply from the server should conform to the general rules of the HTTP API, in particular, the error handling. If there are no errors, the reply is decoded and passed as an object to the specified callback function. In our case, that callback function is cont, which ignores any parameters and merely updates some internal state. If there are errors, the callback is not called. Instead, the error is decoded and displayed by default. If, as in the above example, a second callback function is specified as the optional fourth parameter to ox.JSON.put then it is called and given a chance to handle the error before or instead of displaying it. For our example this function handles the inevitable error which will occur when we try to access the non-existent servlet at /ajax/com.example.test:

function handleErrors(result, status) {
    if (status == 404 || status == 405) {
        ox.Configuration.error(
            _("Please write and install a servlet named com.example.test"));
        return true;
    }

There are two slightly different cases in which the error callback is called. When the server returns a properly formatted JSON reply with an error field as described in HTTP_API (Error handling), the callback is called with the object which represents the entire server reply as the only parameter. When the server returns an HTTP status code other than 200, the error callback is called with the status text as the first parameter and the status code as the second parameter. In both cases, the error handler can return true to prevent the default error handling from displaying the error message. This is exactly what the handleErrors function does when the server returns an HTTP status 404 ("Not Found") or 405 ("Method Not Allowed"). As a general rule, error callbacks should only return true for specific known cases, and let the default error handling process any unknown errors.

Besides uploading of entered settings, a typical configuration page will need to display the currently active settings. These can be displayed by overriding the load method of the page:

page.load = function(cont) {
    ox.JSON.get(AjaxRoot + "/com.example.test?action=get&session=" + session,
        function(reply) { cont(reply.data); }, handleErrors);
}

Similar to the save method, the load method takes a continuation function as parameter. The difference is that, this time, the data travels in the other direction: instead of getting the data from the plugin API as a parameter, the load method must pass the data as parameter to the plugin API's continuation function. In most cases, the reply received from the server will not be structured exactly like the data object defined by addWidget calls. Therefore, the continuation function can't be passed directly to ox.JSON.get like in the save method. Instead, an anonymous function is used to extract the data object from its reply parameter and call the continuation function with the extracted data as parameter. The function does not need to be anonymous, but it must be defined inside of the load method, to have access to cont.