-
Notifications
You must be signed in to change notification settings - Fork 0
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).
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$hooksmap of hook name → callbacks. Instantiated on every request ininclude/common.phpas 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:
-
Legacy addons: every
*.phpfile directly inaddons/is included unconditionally (no enable/disable, no manifest).addons/foo.phpmust defineclass addon_foo extends flux_addon. This is the old raw-file-drop path; the shippedaddons/directory is empty. -
Active manifest plugins: for each slug listed in the
o_active_pluginsconfig value (comma-separated, managed by Administration → Plugins), the manager readsplugins/<slug>/plugin.json, includes the file named by itsaddonkey, and instantiatesclass 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.
-
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:
echooutput (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 theiro_active_pluginsorder). -
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.
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.)
The modern way to package hook code (and optional admin settings), managed from Administration → Plugins.
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,version— required. 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 definingclass 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/adminvalues must be plain.phpfilenames 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 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.
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_tokenon every state-changing POST anyway, andconfirm_referreradds another layer. - Escape all output with
pun_htmlspecialchars().
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.
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.
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.
-
mkdir plugins/mypluginand writeplugin.json(name,slug,version,addon). - Write
myplugin.php:PUNguard →class plugin_myplugin extends flux_addon→register()binding methods to hooks from the reference table. - Copy the folder to a dev board, activate it in Administration → Plugins.
- Remember the constraints: no handler arguments, no return values, globals for state, echo only at output-point hooks.
- Add an
admin.phpif it needs settings; store them aso_myplugin_*config rows. - Zip the folder for distribution.