An introduction to plugin development for Gforge

In this article we will cover the basic steps needed to write a simple plugin for Gforge AS, the HelloWorld plugin. There will be no real world use for this “dummy” plugin, but it will serve to our demonstration purposes. You can find the full source in this tar file or you can checkout the code from the helloworld project

Plugins basic architecture overview

There are three types of plugins in Gforge AS:

  • site plugins (there is one plugin instance available to the whole site),
  • project plugins (multiple instances, one per project, which can be enabled/disabled in the project admin page) and
  • user plugins (multiple instances, one per user, can be activated/de-activated by regular users in the My page).

There is a set of predefined actions that a plugin will listen for. When a given action is performed, Gforge AS will notify the plugins, and each plugin will handle it. For instance, when a project is approved, the PROJECT_APPROVE notification is sent. One of the listeners, the tracker plugin, will create a set of new trackers based on the project template used when creating this new project.

Basic file layout

We need to create the plugin directory inside <gforge_path>/plugins, using the plugin name in lowercase as directory name. For the HelloWord plugin:

$ ls plugins/
build ldap scmcc snippet userwiki
docman mailman scmcvs survey wiki
forum news scmgit tracker xmlcompatibility
frs projectjabber scmperforce userblog
helloworld projectvhost scmsvn userfiles
jabber scm scmvss userjabber

Let’s take a look at the plugin directory structure

$ ls plugins/helloworld/
bin conf cronjobs languages lib wwwlib

The Gforge convention for each subdirectory:

  • bin: contains scripts executables that are not cronjobs,
  • conf: configurations files,
  • cronjobs: scripts to be executed as cronjobs.
  • languages: internationalization files,
  • lib: contains all the plugins classes and functions,
  • lib/html: contains the plugin template class,
  • wwwlib: contains all the scripts for the web interface.
  • wwwlib/admin: web scripts for the admin interface

In Gforge convention class names are CamelCased and there is one file for each class, using the class name as file name.

The GFPlugin interface

The GFPlugin interface (lib/plugin/GFPlugin.php) defines the basic methods that every plugin will need to implement:

  • getType(), return the plugin type,
  • getName(), returns the plugin name,
  • getListeners(), returns an array of action names that the plugin will answer,
  • handleAction($listenername, $params), this method, as his name implies, will handle every registered action,
  • search(GFsearch $search), takes a GFsearch object and returns some result, used for project/site searches.

The plugin’s main class will implement the GFPlugin interface. The name convention for this main class is the plugin name followed by the ‘Plugin’ string. Thus, for the HelloWorld plugin, we will call it HelloWorldPlugin, and store it in the HelloWorldPlugin.php file.

GFProjectPlugin

This interface extends the GFPlugin interface adding some methods commonly used in project plugins. We will not cover it here though, since the HelloWorld plugin is a very simple one. See lib/plugin/GFProjectPlugin.php for more information.

PluginResult

This is a convenient wrapper for any result returned in response to a given action by plugins. This class provides getters and setters for the result, error status and a method to access the plugin object returning the data. See lib/plugin/PluginResult.php for more details.

The HelloWorld plugin

Now we can start writing our HelloWorldPlugin class definition, first we need to know what classes we will use. We already explained GFPlugin and PluginResult, the ActionManager class handles the actions notifications and other interactions between the plugin and the Gforge core, GFUrl generates and parses Gforge’s urls and Language is a set of tools to handle language translations.

<?php
require_once('plugin/GFPlugin.php');
require_once('plugin/ActionManager.php');
require_once('plugin/PluginResult.php');
require_once('core/Language.php');
require_once('GFUrl.php');

The first method we implement is initialize(), this method will register the actions that this plugin will answer using the ActionManager::registerListeners() method.

class HelloworldPlugin implements GFPlugin {
+++ CONST ALLOW = 1;
+++ CONST DISABLE = 0;
+++ CONST SECTION = 'Helloworld';


+++public function initialize() {
++++++ActionManager::registerListeners($this,
++++++$this->getListeners());
++++}

getType() returns the plugin’s type, in this case a project plugin.

public function getType() {
+++return GFPlugin::PROJECT_PLUGIN;
}

getName() will return the name of the plugin, in lowercase.

public function getName() {
+++return 'helloworld';
}

getListeners() just returns an array of strings containing the action names that this plugin listens.

public function getListeners() {
+++return array(
++++++'BUILD_PROJECT_TABS',
++++++'GET_NAV_ELEMENTS',
++++++'GET_OBSERVER_ROLE_CHOICES',
++++++'GET_ROLE_CHOICES',
++++++'PROJECT_APPROVE'
+++);
}

handleActions() maps actions into methods. Don’t worry if you don’t know the meaning of every action, we will see a brief description later.

public function handleAction($listenerName, $params) {
+++switch ($listenerName) {
++++++case 'BUILD_PROJECT_TABS': {
+++++++++return
+++++++++$this->_buildMainTabsListener($params);
+++++++++break;
+++++++}
++++++case 'GET_NAV_ELEMENTS': {
+++++++++return $this->getNavElements($params);
+++++++++break;
++++++}
++++++case 'GET_ROLE_CHOICES': {
+++++++++return $this->getRoleChoices($params);
+++++++++break;
++++++}
++++++case 'GET_OBSERVER_ROLE_CHOICES': {
+++++++++return $this->getObserverRoleChoices($params);
+++++++++break;
++++++}
++++++case 'PROJECT_APPROVE': {
+++++++++return $this->approveActions($params);
+++++++++break;
++++++}
+++}
}

Since this plugin will not return any data on searches, we can write just a stub for the search() method.

public function search(GFSearch $search) {
}

The next two methods are mandatory although not specified in the GFPlugin interface but in GFProjectPlugin. To avoid unnecesary complexity and for historical reasons the HelloWorld plugin implements the GFPluing interface, but when you write a real project plugin you should always implement GFProjectPlugin.

disableFromProject() as its names implies, disables a plugin from a given project. It will be called from a Project object when disabling the plugin. We only need to unregister the actions that the plugin is listening, the caller will take care of everything else.

function disableFromProject() {
+++// unregister the actions
+++ActionManager::unregisterListeners($this);
}

enableInProject() will be called by a Project object when the plugin is enabled, it’s safe to write just a stub here.

function enableInProject(Project $project) {
+++return;
}

HelloworldTemplate

This class extends ProjectTemplate (lib/html/ProjectTemplate.php) and overrides two methods: header() and getPluginNavElements(). The first method will set the page title and it will add the breadcrumb for our plugin. The second method will add navigation elements to the plugin entry in the left side menu, as we will see later

Actions

BUILD_PROJECT_TABS

Tab image of the menu entry
The left menu entry for HelloWorld

The first action we need to handle is BUILD_PROJECT_TABS. This signal is sent by the secondTabs method inside the Layout class (lib/html/Layout.php), around line 674:

$prs=ActionManager::notify("BUILD_PROJECT_TABS",$params);

The method expects a collection of PluginResult objects to render the left menu on the project view. So, here is our method to handle this action:

private function _buildMainTabsListener($params) {
+++if(!isset($params['project']) ||
+++!is_object($params['project']) ||
+++!$params['project']->usesPlugin($this->getName())) {
++++++return;
+++}
+++$project = $params['project'];
+++$result = new PluginResult($this);
+++$value = array(
++++++'text' =>
++++++Language::getSessText('Helloworld.TabString'),
++++++'action'=>
++++++GFUrl::getUrl()->projectURL($project,'','helloworld'),
++++++'id'=> 'helloworld'
+++);
+++$result->setResult($value);
+++return $result;
}

First we check if a project object is present in the $params array, and if our plugin is active for that project. Then, we define some data expected by the caller to build the left side menu and we return it inside a PluginResult object.

Language translation

This line in the previous code snippet

Language::getSessText('Helloworld.TabString')

uses the Language class, which provides a convenient tool for language translation. This class expects languages files inside the languages subdir in the plugin’s subtree, where the general format is:

TagName<tab>Message

Base.tab should contain english messages. See the plugin’s source for more examples on other languages.

GET_NAV_ELEMENTS

This signal is sent from HelloWorldTemplate::getPluginNavElements() function which we already covered. It will insert submenu entries below the plugin entry in the left menu, as you can see in the next screenshot.
Insert image here.

view of elements in left menu
navigation elements in the left menu

GET_ROLE_CHOICES and GET_OBSERVER_ROLE_CHOICES

When this signal is sent the plugin will return a data structure contaning the available permissions that can be set for a given project. We will only use HelloWorldPlugin::ALLOW and HelloWorldPlugin::DISABLE here, but other plugins implement more complex permissions.
GET_OBSERVER_ROLE_CHOICES is similar except that the plugin must return available permissions for non-members (usually private and public). See the source code for details about the implementation.

PROJECT_APPROVE

This signal is sent when a project is approved. HelloWorldPlugin::approveActions() does nothing actually, but you can see a well commented example from the mailman plugin.

The web interface

Scripts that handle the web interface are stored in plugins/helloworld/wwwlib. The first thing to notice is a file called handler.php, which is a dispatcher. This file will map the parameter ‘action’ in the URL to a given script. The default action is handled by index.php, we use the HelloWorldTemplate object to print the page header and footer

<?php
getLanguage();
$Project = GFUrl::getUrl()->getProject();
require_once('plugins/helloworld/lib/html/HelloworldTemplate.php');
$theme = new HelloworldTemplate();
$params = array(
+++'title' => 'Index',
+++'project' => $Project,
+++'selectedbottomtab' => 'helloworld'
);
$theme->header($params);
?>
Hello world!
footer($params);
?>

Hello world main page view
Hello world main page view

Installation

The final step is to install our new plugin, first we need to copy the source files to plugins directory in the gforge installation, and add an entry into the plugin table for HelloWorld.

INSERT INTO plugin(plugin_name, plugin_desc, plugin_type, plugin_order, exclusive_type)
VALUES ('helloworld', 'A hello world plugin', 'project',
(SELECT max(plugin_order) FROM plugin WHERE plugin_type='project') + 1,null);

Finally, we must run upgrade.php to update the configuration and language cache.

#cd /opt/gforge5 && php upgrade.php

This tutorial is not a comprehensive guide, but we covered the basics to start writing your own plugin for Gforge, the best documentation you can get is in the source code of the main plugins bundled with GForge AS.