Skip to content
Alan Paynter edited this page Jul 19, 2026 · 1 revision

Plugin Development

eveBB has two extension mechanisms, both living under plugins/:

System What it is How it runs
Manifest plugins / addons (the flux_hook system) Code that hooks into normal page rendering A plugin_<slug> class registered with the addon manager; fired at flux_hook() call sites
Classic admin plugins (AP_ / AMP_) Standalone admin-console pages, inherited from FluxBB A single .php file included by admin_loader.php

They are unrelated: classic admin plugins never touch flux_hook, and hook addons don't need an admin page (though a manifest plugin can ship one).

The flux_hook system

Architecture

Three pieces, all defined in core:

  • flux_hook($name) (include/functions.php) — the global fire function. Core code calls it at fixed points; it forwards to the manager if one exists, and is a safe no-op otherwise.

  • flux_addon_manager (include/addons.php) — holds a $hooks map of hook name → callbacks. Instantiated on every request in include/common.php as the global $flux_addons. Loading is lazy: nothing is scanned or included until the first hook actually fires.

  • flux_addon (include/addons.php) — the base class your plugin extends. It defines one method to override:

    class flux_addon {
        function register($manager) { }
    }

When the first hook fires, the manager loads extensions from two sources, in order:

  1. Legacy addons: every *.php file directly in addons/ is included unconditionally (no enable/disable, no manifest). addons/foo.php must define class addon_foo extends flux_addon. This is the old raw-file-drop path; the shipped addons/ directory is empty.
  2. Active manifest plugins: for each slug listed in the o_active_plugins config value (comma-separated, managed by Administration → Plugins), the manager reads plugins/<slug>/plugin.json, includes the file named by its addon key, and instantiates class plugin_<slug>.

Each loaded extension gets register($manager) called once, where it binds handlers:

$manager->bind('hook_name', array($this, 'method_name'));

bind() only accepts callables, and handlers are stored as (object, method) pairs.

Hook semantics — read this before writing a handler

  • Handlers are called with no arguments, and their return value is ignored. There is no filter chain and no way to short-circuit or cancel the surrounding action.

  • A handler runs as an ordinary method in its own object scope — it does not see the calling page's local variables. Reach shared state through globals: global $db, $pun_config, $pun_user, $lang_common; etc.

  • Handlers influence the page through side effects: echo output (at output-point hooks) and mutations of global state (at logic-point hooks).

  • All handlers bound to a hook run, in registration order (legacy addons/ first, then active plugins in their o_active_plugins order).

  • There is no hook cache — discovery happens live on the first fire of each request.

  • Every plugin file must start with the direct-access guard:

    if (!defined('PUN')) exit;

This is a deliberately simplified subset of upstream FluxBB's hook system; if you're porting a FluxBB extension, expect far fewer hooks and no parameterized/short-circuiting hooks.

Complete hook reference

These are all the hooks in the current core, named by the convention <page>_<point>. "Validation" hooks fire inside form handlers (logic points — mutate globals, don't echo); "before_header" and "before_submit" hooks fire during HTML output (echo away).

Hook Fired from Point
header_head_end header.php end of <head> on every page — inject CSS/JS/meta here
parser_smilies include/parser.php during smiley parsing — override the global smiley table
quickpost_before_submit viewtopic.php inside the quick-reply form, before the submit button
login_before_validation login.php login POST, before credentials are checked
login_after_validation login.php login POST, after successful validation
login_before_header login.php before the login page renders
login_before_submit login.php inside the login form
forget_password_before_validation login.php password-reset POST, before validation
forget_password_after_validation login.php password-reset POST, after validation
forget_password_before_header login.php before the reset page renders
forget_password_before_submit login.php inside the reset form
register_before_validation register.php registration POST, before validation
register_after_validation register.php registration POST, after validation
register_before_header register.php before the registration page renders
register_before_submit register.php inside the registration form
post_before_validation post.php new topic/reply POST, before validation
post_after_validation post.php new topic/reply POST, after validation
post_before_header post.php before the post page renders
post_before_submit post.php inside the post form
profile_after_form_handling profile.php after a profile form POST is processed
profile_admin_before_header profile.php profile Admin section, before rendering
profile_admin_after_form profile.php profile Admin section, after the form

(22 fire sites at the time of writing. When in doubt, grep -rn "flux_hook(" *.php include/ in your tree — the list above was produced exactly that way.)

Manifest plugins

The modern way to package hook code (and optional admin settings), managed from Administration → Plugins.

Layout and manifest

A plugin is a folder plugins/<slug>/ containing plugin.json:

{
  "name": "My Plugin",
  "slug": "myplugin",
  "version": "1.0",
  "author": "You",
  "description": "What it does",
  "addon": "myplugin.php",
  "admin": "admin.php"
}
  • name, slug, versionrequired. The slug must match the folder name and match ^[a-z0-9][a-z0-9_-]{0,63}$ (lowercase).
  • addon — optional; a PHP file in the plugin folder defining class plugin_<slug> extends flux_addon. Omit it for a plugin that only has an admin page.
  • admin — optional; a PHP settings page (see below).
  • addon/admin values must be plain .php filenames inside the plugin folder (no .., no leading /).

Activation state lives in the o_active_plugins config value, so it survives one-click core updates. Freshly installed plugins start inactive.

The bundled example: plugins/hello/

The reference plugin injects a <meta> marker into every page's <head> and provides a settings page for the message text.

plugins/hello/plugin.json:

{
  "name": "Hello eveBB",
  "slug": "hello",
  "version": "1.0",
  "author": "eveBB",
  "description": "The reference eveBB plugin: injects a small marker ...",
  "addon": "hello.php",
  "admin": "admin.php"
}

plugins/hello/hello.php — the addon class:

<?php
if (!defined('PUN')) exit;

class plugin_hello extends flux_addon
{
    function register($manager)
    {
        $manager->bind('header_head_end', array($this, 'inject_marker'));
    }

    function inject_marker()
    {
        global $pun_config;
        $message = isset($pun_config['o_hello_message'])
            ? $pun_config['o_hello_message']
            : 'Hello from the eveBB plugin system';
        echo '<meta name="evebb-hello" content="'.pun_htmlspecialchars($message).'" />'."\n";
    }
}

Everything important is visible here: the PUN guard, the plugin_<slug> class name, binding a method to a hook in register(), a zero-argument handler, global for shared state, and output via echo with proper escaping.

The admin settings page

If the manifest declares an admin file, the plugin gets a Settings action in Administration → Plugins and a direct "Name Settings" link in the admin menu. The file is required inside the admin chrome, already wrapped in a <form method="post"> — so your page body just emits form fields, and handles its own POST at the top. In scope you have $db, $pun_config, the loaded $lang_* arrays, the CSRF helpers, plus $plugin_slug and $plugin_manifest.

The hello plugin's admin.php shows the pattern: on POST it validates confirm_referrer('admin_plugins.php') and check_csrf($_POST['csrf_token']), saves its setting into the config table (o_hello_message), regenerates the config cache with generate_config_cache(), and redirect()s; otherwise it prints an input field.

Conventions worth copying:

  • Store settings as config rows with an o_<plugin>_... prefix; they're cached with board config, cheap to read, and survive updates.
  • Always CSRF-check state changes — eveBB enforces a valid csrf_token on every state-changing POST anyway, and confirm_referrer adds another layer.
  • Escape all output with pun_htmlspecialchars().

Packaging and installation

Ship the plugin as a zip with a single top-level folder named after the slug:

myplugin.zip
└── myplugin/
    ├── plugin.json
    ├── myplugin.php
    └── admin.php

Admins install it via Administration → Plugins → Upload a plugin. The installer requires ZipArchive, validates the manifest before and after extraction, rejects unsafe archive entries (zip-slip protection), and refuses to overwrite an existing slug. Plugins can also simply be copied into plugins/ over FTP.

Activate / Deactivate toggles the slug in o_active_plugins without touching files; Delete (inactive plugins only) removes the folder.

Security note for admins: an active plugin's code runs with full forum privileges on every request. Only install plugins you trust.

Library reference

include/plugins.php exposes the management API (useful for CLI tooling and tests): evebb_plugin_slug_is_valid(), evebb_manifest_check(), evebb_read_manifest(), evebb_installed_plugins(), evebb_active_plugins(), evebb_plugin_is_active(), evebb_activate_plugin(), evebb_deactivate_plugin(), evebb_delete_plugin(), evebb_install_plugin_zip(). The characterization test tests/characterization/PluginLibraryTest.php pins this library's behaviour (slug rules, manifest validation, zip-entry safety, o_active_plugins parsing), and tests/e2e/plugins-test.sh exercises install/activate end to end.

Classic admin plugins (AP_ / AMP_)

The legacy FluxBB mechanism for adding standalone admin-console pages. A classic plugin is a single file in plugins/:

  • AP_Name.php — accessible to administrators only.
  • AMP_Name.php — accessible to administrators and moderators.

It is opened via admin_loader.php?plugin=AP_Name.php (there is no automatic menu entry in eveBB — link to it yourself, e.g. from a manifest plugin's settings page). The loader validates the filename against ^AM?P_(\w*?)\.php$, enforces the prefix's access rule, renders the admin header, and includes the file. The file must call

define('PUN_PLUGIN_LOADED', 1);

once it has run, or the loader reports a load failure. The underscored part of the filename becomes the page title (AP_My_Plugin.php → "My Plugin").

No classic plugins ship with eveBB, and old FluxBB ones may need PHP 8 fixes — but the loader remains for compatibility. For new work, use a manifest plugin.

Writing your first plugin — checklist

  1. mkdir plugins/myplugin and write plugin.json (name, slug, version, addon).
  2. Write myplugin.php: PUN guard → class plugin_myplugin extends flux_addonregister() binding methods to hooks from the reference table.
  3. Copy the folder to a dev board, activate it in Administration → Plugins.
  4. Remember the constraints: no handler arguments, no return values, globals for state, echo only at output-point hooks.
  5. Add an admin.php if it needs settings; store them as o_myplugin_* config rows.
  6. Zip the folder for distribution.

Clone this wiki locally