FUSIONFORGE PLUGINS HOWTO -------------------------------- Here is a short HOWTO explaining how plugins work, and how to make a new one. It was written by Roland Mas . WHAT PLUGINS ARE, AND WHY THEY ARE USEFUL ----------------------------------------- Plugins are extensions to the "core" of GForge, providing extra functionality without being tightly integrated within Sourceforge proper. They are useful because they allow for independent development of third-party functionality, and they add flexibility to Sourceforge as to what features are available on a particular installation. As an example, it's been suggested to integrate a shared calendar application in Sourceforge. It's a good idea and an interesting feature, but not one that everybody wants. Thus, including it in the GForge code would piss off someone. Additionnally, there might be several competing implementations for such a calnedar application. Choosing one among them would also piss off people. So it is made possible to have a system so that different implementations can exist and be installed separately. HOW PLUGINS WORK ---------------- It is expected that a plugin is just some new feature added to GForge, and not a change in the behaviour of existing features. A plug-in should therefore only add files, not change existing ones. Whether these files be web pages, offline scripts, static documentation or else is not relevant. Of course, *some* changes will have to be made to the "core" files, if only to add links to new web pages, for instance. These changes are acceptable, and will be discussed below. Here come the details about how the plugin system is implemented. - A plugin will be identified primarily by a string handle, which will be static across all installations of this plugin. It should be composed of lowercase letters only, because it's going to be used in table names and we don't want namespace conflicts. For instance, if the ACME company writes a time tracking tool plugin, the handle for that plugin could be "acmetimetracker". When installed, the plugin will be assigned an integer identifier. This id might vary from site to site, and should not be depended upon. We [the GForge-proper maintainers team] will maintain some sort of list of allocated plugin names so that different plugins get different allocated identifiers, see below. - Tables in the database schema: special tables have been added to the database schema to keep track of installed plugins. They are described below (simplified descriptions): ,---- | CREATE TABLE plugins (plugin_id integer, | plugin_name character(32), | plugin_desc text, | CONSTRAINT plugins_pkey PRIMARY KEY (plugin_id) | ) | CREATE TABLE group_plugin (group_plugin_id integer, | group_id integer, | plugin_id integer, | CONSTRAINT PRIMARY KEY (plugin_id), | CONSTRAINT FOREIGN KEY (group_id) REFERENCES groups(group_id) | ) | CREATE TABLE user_plugin (user_plugin_id integer, | user_id integer, | plugin_id integer, | CONSTRAINT PRIMARY KEY (plugin_id), | CONSTRAINT FOREIGN KEY (user_id) REFERENCES users(user_id) | ) `---- TODO: add plugins_persistence, group_plugin_persistence, user_plugin_persistence "plugins" lists the installed plugins, with the numeric id, the string handle (say, "acmetimetracker") and a description. "group_plugin" is a way to store the fact that a group "uses" a plugin without needing to add a "uses_acmetimetracker" to the groups table for each known plugin. "user_plugin" is the same, for users. - A plugin may create its own tables in the same database. These tables must be named plugin_foo_* if the plugin's string identifier is "foo". One suggested table is plugin_foo_meta_data, which should be used to store the plugin meta-data, such as the installed version. The plugin can then use some code like db-upgrade.pl if its database schema changes over time. [TODO: Standardise the command/script/something below] These tables may have foreign key referential integrity constraints going from them to standard tables, but not the other way round. If they have, then a command/script/something must be provided so that the main db-upgrade.pl can disable the constraints and re-enable them afterwards in case some database schema changes are needed. Similarly, a plugin may create sequences, indexes, views, etc, provided that their names are prefixed with plugin_foo_ too. A plugin should not modify the data in tables that do not belong to it. If it really needs to, then please discuss it with us first, there might be cases where it's needed/useful. Reading those data is okay, but it must be careful not to leak any info to places/users which normally wouldn't have had access to it. - Functions in Group.class.php and User.class.php: the Group and User classes now have a usesPlugin() method. It takes a single parameter, the "acmetimetracker" identifier for the module, and returns a boolean if the particular user/group has turned on the use of that module. Also provided are setPluginUsage() methods, taking a plugin name and a boolean value as arguments and returning true on success and false on failure. - A plugin should not change the existing files. Of course, it will need a way to adds links to its own web pages. This is done by a "hook" system. Each plugin can hook itself up to a number of hook points in the main code, and execute arbitrary code when that point is reached. Basically, the plugin registers itself to a global object (of the PluginManager class, if you want to know). You have to call the register_plugin() function, providing it an object of a subclass of the Plugin class that is provided by the main code. That object must provide a GetHooks() method, which returns a list of hook names. Whenever one of these hooks is encountered, the object's CallHook() method is called with the hook name and extra parameters that depend on the hook. Adding a link to your page in some place is just a matter of "subscribing" yourself to the hook that is in the appropriate place, and printing the appropriate link whenever your CallHook() method is called from that place. Registering your plugin is done by providing a /usr/share/gforge/plugins//include/-init.php. It will be parsed by the PluginManager object. That file should contain a call to register_plugin(), passing it an object of the appropriate class. See the helloworld plugin for an example. The hooks are managed centrally by the GForge code maintainers. If you need one, please ask, we'll add it. The current list of hooks is provided at the end of this document. I rely on you plugins developers to provide more ideas :-) - Plugin-specific web pages should reside either in the /plugin/*/ URL-space (that is, plugin "foo" will probably put its files in /usr/share/gforge/www/plugins/foo/) or (if the web interface is not written in PHP) in /plugin/*/cgi-bin/ URL-space (files in /usr/lib/sourceforge/cgi-bin/plugins/foo/). If possible, and as much as possible, a plugin should use the layout functions defined by GForge (Layout.class.php, HTML.class.php, or whatever they're called), to ensure a consistent look and themability. Of course, the previous point only applies to plugins written in PHP. Plugins written in other languages are not excluded by this proposal, and there is no need to restrict them. Should they appear, they might need to recode some of GForge's functions in Perl or another language. I see no need to restrict that either. Only thing: it would be better if the porting were as straightforward as possible. Do not reimplement, please. Just translate from PHP to Perl or whatever. If you do, please submit your translation to us, so that it can be provided by GForge proper and maintained in common. [TODO: Think about that, design, implement] Speaking of languages... There should be some way to have plugin-specific language files, so that the plugins can use the standard methods used elsewhere in Sourceforge. I haven't thought about that very deeply yet, but I think it will evolve into a recommendation that the "handles" in the language files are plugin_foo_page / item (as compared to the current page / item model used for "core" GForge i18n strings). - A plugin should register itself into the database using the provided register-plugin script on its installation, and unregister itself using unregister-plugin on removal. When unregistering, be careful to delete all the rows in tables that contain a reference to your plugin_id, so that the unregistration process (which deletes your row in the plugins table) does not fail due to referential integrity errors. HOW DO I MAKE A PLUGIN? ----------------------- Your best bet would be to start with the sample "helloworld" plugin and change parts of it. It shows an example of most of the things you should need to make your plugin: PHP pages, configuration files, bits of Apache configuration, cron jobs, etc. If you need something else, please ask, we'll discuss it, (hopefully) reach an agreement on how to Do It Right, and implement a sample in helloworld. HOW TO NAME MY PLUGIN --------------------- If you plan on distributing your plugin to the public, please contact the GForge maintainer with your proposed name, and we'll add it to the list below. This ensures that no other plugin will use the same name, so as to reduce the risk of name conflicts. If you only intend to keep your plugin for yourself, you might still contact us with your plugin name. If you're really nice, we might consider adding it here too, so that other people who want to distribute their plugin do not reuse the same name. For reference, this is the list of currently used plugin names: - helloworld, the plugin provided as an example. - extldapauth, a plugin allowing on-the-fly account creation from an existing LDAP directory; CURRENT LIST OF PLUGIN HOOKS ---------------------------- The following is a list of hooks available in GForge for plugins to utilise. Each hook is listed with its name, locations in the source code where the hook is called, and parameters that are passed into the hook and a brief description. There may be other hooks available, added after this section had been written. Hook Name : session_set_entry Locations : common/include/session.php Description: Called before checking if the user is logged in by reading session cookie. You can use this hook to handle session setup specific to your plugin. Hook Name : session_set_return Locations : common/include/session.php Description: Called after checking if the user is logged in by reading session cookie. You can use this hook to handle session setup specific to your plugin. Hook Name : artifact_extra_detail Parameters : artifact_id - The ID of a tracker item Locations : www/tracker/detail.php www/tracker/mod-limited.php www/tracker/mod.php Description: Use this hook to provide additional information about a tracker item based upon its ID. Hook Name : before_logout_redirect Locations : www/account/logout.php Description: Called once the GForge user has had their session logged out, and before the user is redirected to the homepage of the site. Hook Name : cssfile Locations : www/include/Layout.class.php Description: Used to include a CSS link element to include in the page layout. The hook should return a complete element. Hook Name : cssstyle Locations : www/include/Layout.class.php Description: Used to include inline CSS into the page layout. The hook should return pure CSS, without surrounding