Mastering MediaWiki Hooks: Customizing Core Behavior
Why Hooks Matter in MediaWiki
Ever stumbled on a wiki that just does what you need, without you having to poke around the core files? That’s not magic – it’s hooks at work. In MediaWiki, hooks are the quiet backstage crew that let you slip in custom logic right where the core “thinks” something important is happening. Saving a page, logging a user in, rendering a parser output – each of those moments fires a hook, and if you’ve got a function waiting on that hook, it gets called. The beauty is you can shape the wiki’s behavior without forking the whole code base.
Getting Your Hands on Hook
First things first: you need to know where to register a hook. There are two common ways.
- Legacy
$wgHooksarray – still works in most wikis, especially if you’re writing a quick‑and‑dirty extension. - Extension.json – the modern, declarative approach. It’s cleaner, and it plays nicer with Composer.
Both achieve the same end: associate a hook name (like ArticleSave) with a callback. The callback can be a plain function, a static method, or an object method if you’re feeling object‑orientated.
Simple registration with $wgHooks
// LocalSettings.php or your extension file
$wgHooks['ArticleSave'][] = 'onArticleSave';
function onArticleSave( WikiPage $page, User $user, Content $content, $summary, $isMinor, $isWatch, $section, $flags, $status ) {
// Example: prepend a tiny banner to every new article
if ( $page->isNewPage() ) {
$text = $content->getText();
$content->setText( "[[Category:New articles]]\n\n$text" );
}
// Returning true lets the save continue
return true;
}
Notice the return true at the end? If a hook returns false, MediaWiki stops the chain – a handy way to abort an operation.
Using extension.json
{
"name": "BannerNewPages",
"author": "Your Name",
"version": "1.0.0",
"hooks": {
"ArticleSave": "BannerNewPages::onArticleSave"
},
"AutoloadClasses": {
"BannerNewPages": "includes/BannerNewPages.php"
}
}
Then in includes/BannerNewPages.php:
class BannerNewPages {
public static function onArticleSave( WikiPage $page, User $user, Content $content, $summary, $isMinor, $isWatch, $section, $flags, $status ) {
if ( $page->isNewPage() ) {
$text = $content->getText();
$content->setText( "[[Category:New pages]]\n\n$text" );
}
return true;
}
}
That’s the “declarative” side of things – you list the hook in JSON, MediaWiki autoloads your class, and the method gets called. It reads cleaner, especially when you have dozens of hooks.
Hook Anatomy – What You Get, What You Can Do
Every hook comes with a signature – a list of parameters the core passes. Some of those parameters are read‑only, others are mutable. The MediaWiki documentation (see the Manual:Hooks page) spells out each hook, but the pattern is predictable:
- Reference parameters – you can modify them (e.g., the
Contentobject above). - Return value – most hooks expect
trueto keep going,falseto abort. - Hook name convention – CamelCase, often ending in “Complete” or “Before”.
Some hooks fire early (ParserBeforeParse), giving you the chance add custom tags. Others fire late (OutputPageParserOutput), letting you tweak the final HTML. Knowing when a hook runs is half the battle; the other half is figuring out what you can safely change without breaking other extensions.
Practical Examples
1. Adding a custom parser tag
If you need a tag like <spoiler> that hides text until clicked, you can hook into ParserFirstCallInit:
< class="language-php">$wgHooks['ParserFirstCallInit'][] = 'MySpoiler::onParserFirstCallInit'; class MySpoiler { public static function onParserFirstCallInit( Parser $parser ) { $parser->setHook( 'spoiler', [ self::class, 'renderSpoiler' ] ); return true; } public static function renderSpoiler( $input, array $args, Parser $parser, PPFrame $frame ) { // Very lightweight HTML, no external JS for now $id = 'spoiler-' . md5( $input . microtime() ); $html = "<div class=\"spoiler\" id=\"$id\" style=\"display:none;\">$input</div>"; $html .= "<button onclick=\"document.getElementById('$id').style.display='block';this.style.display='none';\">Show spoiler</button>"; return $html; } }
That snippet is enough to drop a spoiler box on any page. It’s not a perfect UI, but you can upgrade it later – that’s the flexibility you get from a hook.
2. Preventing usernames that look like IPs
Suppose you want to block registrations where the username resembles an IPv4 address. The UserCreateForm::UserCreate hook is perfect:
$wgHooks['UserCreateForm::UserCreate'][] =IpLikeUsernames::check';
class BlockIpLikeUsernames {
public static function check( User $user, $autocreate, $isTemp ) {
$name = $user->getName();
if ( preg_match( '/^\d{1,3}(?:\.\d{1,3}){3}$/', $name ) ) {
// Add a friendly error message
$user->addError( 'Your username looks like an IP address – please choose something else.' );
// Returning false aborts creation
return false;
}
return true;
}
}
Notice the use of addError – MediaWiki will surface that message on the registration form. It’s a quick way to enforce a policy that otherwise would need core changes.
3. Logging API calls for audit
Admins sometimes need to know which bots are hammering the API. Hook into ApiMain::moduleManager (or the newer ApiBeforeMain):
$wgHooks['ApiBeforeMain'][] = 'ApiAudit::log';
class ApiAudit {
public static function log( ApiBase $api ) {
$user = RequestContext::getMain()->getUser();
$module = $api->getModuleName();
wfDebugLog( 'api-audit', sprintf(
"[%s] %s called %s",
wfTimestampNow(),
$user->getName(),
$module
) );
return true;
}
}
Now each API call writes a line to logs/api-a.log. You can rotate the file with your usual logrotate setup. This is the sort of thing you’d add after a security review – no need to touch core.
When Hooks Collide – Conflict Management
Hooks are great, but remember they’re shared real‑estate. Two extensions registering the same hook can occasionally step on each other's toes. MediaWiki resolves this by calling handlers in the order they were registered. If one returns false, the rest of the chain stops. That’s both a blessing (you can purposely block later handlers) and a curse (you might unintentionally block a crucial feature).
Here are a few practical tips to keep the peace:
- Prefer
extension.json– it makes the registration order explicit (alphabetical by extension name, unless overridden). - Return
trueunless you really mean to abort – most hooks don’t need to cancel further processing. - Document your hook’s side‑effects – a simple comment in the JSON or a README helps future maintainers.
- Use
HookContainer::registerat runtime only when necessary – dynamic registration can be harder to trace.
In a pinch, you can inspect the hook stack with Hooks::getHookNames() or by grepping the HookContainer cache file. It’s a little fiddly, but it saves you from nasty surprises when you upgrade the wiki.
Creating Your Own Hook – Extending the Core
Sometimes the built‑in hooks just don’t cover a niche requirement. MediaWiki lets you define custom hooks. The pattern is straightforward:
- Pick a descriptive name, e.g.,
MyExtensionBeforeRender. - Emit the hook at the appropriate point with
Hooks::run(or the olderwfRunHooks). - Document the parameters so others can hook into it later.
class MyRenderExtension {
public static function onBeforePageDisplay( OutputPage $out, Skin $skin ) {
// Let other extensions add something to the head
$hookResult = Hooks::run( 'MyExtensionBeforeRender', [ $out, $skin ] );
// $hookResult is true if no handler aborted
return $hookResult;
}
}
// Register the core hook that triggers your custom one
$wgHooks['BeforePageDisplay'][] = 'MyRenderExtension::onBeforePageDisplay';
Now any extension can listen for MyExtensionBeforeRender and inject CSS, analytics, or even rewrite parts of the HTML. The only responsibility you bear is to keep the parameter list stable – otherwise you’ll break downstream extensions.
JavaScript Hooks – Front‑end Extensibility
MediaWiki isn’t just PHP. The front‑end offers a mw.hook system that mirrors the back‑end model. In gadgets or ResourceLoader modules you can do:
mw.hook('ve.saveDialog').add( function ( dialog ) {
// Append a custom notice near the save button
$( dialog.$element ).find( '.mw-savebutton' ).after(
'<div class="my-notice">Remember to double‑check references!</div>'
);
} );
If you’re already comfortable with PHP hooks, the JavaScript analogue feels familiar. The main difference is that JS hooks are asynchronous and can be added at any time after the module loads.
Testing Hooks – A Quick Checklist
Before you ship a new hook or a modification, run through these sanity checks:
- Unit test the callback – mock the parameters if needed, assert that you return the correct boolean.
- Integration test with real pages – create a sandbox page, trigger the hook, verify output.
- Check other extensions – run
php maintenance/checkHooks.php(a community script) to see if any other extension already uses the same hook name. - Validate performance – hooks run on every request that matches, so keep them lightweight. Use
wfProfileIn/wfProfileOutif you want to measure impact.
Getting a hook wrong can lead to silent failures (returning false unintentionally) or even fatal errors if you misuse a reference parameter. A quick test suite catches most of those bugs.
Wrapping Up – The Hook Mindset
Mastering MediaWiki hooks isn’t about memorizing a laundry list of function names; it’s about embracing a mindset: “When something important happens, I can interject my own logic without touching the core.” That subtle philosophy is what lets the biggest wikis on the planet stay flexible after years of growth.
So, whether you’re tweaking parser behavior, enforcing policy at login, or sprinkling analytics into the API, hooks are ticket. Remember the three pillars: register cleanly (prefer extension.json), respect the return contract (don’t abort unless you mean to), and document the contract for future developers.
Happy hooking – and may your extensions play nicely together.