Mediawiki extension development
Skill santhoshtr/wiki-skills/skills/mediawiki-extension-development
A collection of skills for AI coding agents focused on Wikimedia projects. Mirror of https://gitlab.wikimedia.org/santhosh/wiki-skills
npx -y skills add santhoshtr/wiki-skills --skill mediawiki-extension-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Guide developers through creating MediaWiki extensions from setup to publication. Covers extension architecture, implementing hooks/special pages/APIs, database integration, localization, testing, and Wikimedia deployment. Use when developers want to create new extensions, add functionality to MediaWiki, or prepare extensions for production deployment.
SKILL.md
47.6 KB, as published. Nobody here has run it
MediaWiki Extension Development
Overview
MediaWiki extensions add custom functionality, modify behavior, and integrate new features into MediaWiki wikis. This skill provides comprehensive guidance for developing extensions following MediaWiki best practices, from initial setup through production deployment.
Extensions can add special pages, modify wiki behavior through hooks, provide APIs, extend wiki markup, manage content, and integrate with external services. The development process involves setup, implementation, localization, testing, and publication.
When to Use This Skill
Use this skill when developers request help with:
- Creating a new MediaWiki extension from scratch
- Setting up extension structure and registration
- Implementing hooks to modify MediaWiki behavior
- Adding special pages for custom functionality
- Creating API modules (Action API or REST API)
- Extending wiki markup with parser functions or tags
- Adding database tables for extension data
- Implementing localization (i18n) for messages
- Preparing extensions for Wikimedia deployment
- Understanding MediaWiki extension architecture
- Debugging extension issues
- Following MediaWiki coding conventions
Quick Start
Minimal Extension Setup
Create a functional extension in 5 minutes:
- Create directory structure:
cd extensions/
mkdir -p MyExtension/includes
cd MyExtension
- Create
extension.json:
{
"name": "MyExtension",
"author": "Your Name",
"url": "https://www.mediawiki.org/wiki/Extension:MyExtension",
"description": "Brief description of what your extension does",
"version": "1.0.0",
"license-name": "GPL-2.0-or-later",
"type": "other",
"manifest_version": 2,
"AutoloadNamespaces": {
"MediaWiki\\Extension\\MyExtension\\": "includes/"
}
}
- Enable in
LocalSettings.php:
wfLoadExtension( 'MyExtension' );
- Verify: Visit
Special:Versionto see your extension listed.
This creates a valid, registered extension. Now add functionality through extension points.
Core Workflows
Workflow 1: Creating a New Extension
Follow this complete workflow for new extensions:
Step 1: Set Up Development Environment
Ensure a working MediaWiki installation:
# Clone MediaWiki if needed
git clone https://gerrit.wikimedia.org/r/mediawiki/core.git
cd core
composer install
php maintenance/install.php --dbtype=sqlite --dbpath=$(pwd)/data \
--pass=AdminPassword "My Wiki" "Admin"
Development settings - Add to LocalSettings.php:
// Disable caching during development
$wgMainCacheType = CACHE_NONE;
$wgCacheDirectory = false;
// Enable error reporting
error_reporting( -1 );
ini_set( 'display_errors', 1 );
$wgShowExceptionDetails = true;
$wgShowDBErrorBacktrace = true;
$wgShowSQLErrors = true;
// Enable debugging
$wgDebugToolbar = true;
$wgShowDebug = true;
Step 2: Use BoilerPlate as Starting Point
MediaWiki provides the BoilerPlate extension as a template:
cd extensions/
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/BoilerPlate.git MyExtension
cd MyExtension
rm -rf .git
Customize the extension:
- Rename namespace in files from
MediaWiki\Extension\BoilerPlatetoMediaWiki\Extension\MyExtension - Update
extension.jsonwith your details - Update
i18n/*.jsonfiles with your messages - Modify or remove example code
Step 3: Define Extension Metadata
Complete the extension.json file with all relevant fields:
Essential fields:
{
"name": "MyExtension",
"author": ["Your Name", "Contributor Name"],
"url": "https://www.mediawiki.org/wiki/Extension:MyExtension",
"description": "One-sentence description",
"version": "1.0.0",
"license-name": "GPL-2.0-or-later",
"type": "other",
"manifest_version": 2
}
Type field - Choose appropriate type:
specialpage- Adds special pagesparserhook- Extends wiki markupmedia- Media handlerssemantic- Semantic extensionsskin- Skins (for skin development)api- API extensionsother- General extensions
Additional useful fields:
{
"requires": {
"MediaWiki": ">= 1.39.0"
},
"AutoloadNamespaces": {
"MediaWiki\\Extension\\MyExtension\\": "includes/"
},
"config": {
"MyExtensionSomeSetting": {
"value": true,
"description": "Description of this configuration option"
}
},
"MessagesDirs": {
"MyExtension": ["i18n"]
}
}
See assets/templates/extension.json for a complete template.
Step 4: Implement Core Functionality
Choose appropriate extension points based on your needs. See Workflow 2-6 for specific implementations.
Step 5: Add Localization
Implement internationalization for all user-facing text:
- Create
i18n/en.json:
{
"myextension-desc": "Extension description for Special:Version",
"myextension-hello": "Hello, world!",
"myextension-greeting": "Hello, $1! Welcome to $2."
}
- Create
i18n/qqq.jsonfor documentation:
{
"myextension-desc": "{{desc|name=MyExtension|url=https://...}}",
"myextension-hello": "Simple greeting message shown on example page",
"myextension-greeting": "Personalized greeting. Parameters:\n* $1 - User name\n* $2 - Wiki name"
}
- Register in
extension.json:
{
"MessagesDirs": {
"MyExtension": ["i18n"]
}
}
For API-specific messages, use separate directory:
{
"MessagesDirs": {
"MyExtension": ["i18n", "i18n/api"]
}
}
Create i18n/api/en.json for API help messages with apihelp- prefix:
{
"apihelp-myaction-summary": "Performs a custom action",
"apihelp-myaction-param-text": "Text to process",
"apihelp-myaction-example-1": "Process the text 'hello'"
}
For special page aliases, use ExtensionMessagesFiles:
{
"ExtensionMessagesFiles": {
"MyExtensionAlias": "MyExtension.i18n.alias.php"
}
}
Create MyExtension.i18n.alias.php:
<?php
$specialPageAliases = [];
$specialPageAliases['en'] = [
'MyPage' => [ 'MyPage', 'My Page' ],
];
$specialPageAliases['nl'] = [
'MyPage' => [ 'MijnPagina' ],
];
See assets/templates/MyExtension.i18n.alias.php for complete template.
- Use in code:
// In special pages or contexts with IContextSource
$output->addHTML( $this->msg( 'myextension-hello' )->escaped() );
// With parameters
$greeting = $this->msg( 'myextension-greeting', $userName, $wikiName )->text();
// Standalone (not context-aware)
$message = wfMessage( 'myextension-hello' )->inLanguage( 'de' )->text();
See references/localization-guide.md for complete i18n patterns.
Step 6: Test and Debug
Testing approaches:
- Manual testing: Enable extension, test features in browser
- PHPUnit tests: Create tests in
tests/phpunit/ - Integration tests: Test with other extensions enabled
- Parser tests: For parser hooks, create
.txtfiles intests/parser/
Debugging tools:
// Use MediaWiki's debug log
wfDebugLog( 'MyExtension', 'Debug message here' );
// Check variables
wfDebug( print_r( $variable, true ) );
// Use MediaWiki's logger
LoggerFactory::getInstance( 'MyExtension' )->info( 'Log message' );
Common issues:
- Extension not appearing: Check
wfLoadExtension()in LocalSettings.php - Hooks not firing: Verify hook name and signature
- Namespace errors: Check autoloading configuration
- Messages not showing: Verify MessagesDirs and file location
Workflow 2: Implementing Hooks
Hooks allow extensions to modify MediaWiki behavior at specific points.
Understanding Hooks
MediaWiki core and extensions emit hooks at key execution points. Extensions register handler functions to run when hooks fire.
Hook types:
- Run hooks: Execute handler, continue regardless of return value
- Abort hooks: Handler can stop execution by returning false
- Processing hooks: Handler modifies data passed by reference
Finding hooks: See references/hooks-reference.md for common hooks or search MediaWiki documentation.
Registering Hooks
Method 1: Function-based (simple):
In extension.json:
{
"Hooks": {
"BeforePageDisplay": "MyExtensionHooks::onBeforePageDisplay"
}
}
Create handler class:
<?php
namespace MediaWiki\Extension\MyExtension;
class MyExtensionHooks {
/**
* Handler for BeforePageDisplay hook
* @param \OutputPage $out
* @param \Skin $skin
*/
public static function onBeforePageDisplay( $out, $skin ) {
// Add custom CSS
$out->addModuleStyles( 'ext.myextension.styles' );
}
}
Method 2: HookHandler (recommended for MW 1.35+):
In extension.json:
{
"HookHandlers": {
"main": {
"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\MainHookHandler"
}
},
"Hooks": {
"BeforePageDisplay": "main",
"ParserFirstCallInit": "main"
}
}
Create handler:
<?php
namespace MediaWiki\Extension\MyExtension\Hooks;
use MediaWiki\Hook\BeforePageDisplayHook;
use MediaWiki\Hook\ParserFirstCallInitHook;
class MainHookHandler implements BeforePageDisplayHook, ParserFirstCallInitHook {
public function onBeforePageDisplay( $out, $skin ): void {
$out->addModuleStyles( 'ext.myextension.styles' );
}
public function onParserFirstCallInit( $parser ) {
$parser->setHook( 'myextension', [ $this, 'renderTag' ] );
}
public function renderTag( $input, array $args, $parser, $frame ) {
return '<div class="myextension">' . htmlspecialchars( $input ) . '</div>';
}
}
Hook handler advantages:
- Type safety through interfaces
- Better IDE support and autocomplete
- Dependency injection support
- Cleaner separation of concerns
Organizing Multiple Hook Handlers
For larger extensions, organize hooks into multiple handler classes by domain or purpose:
extension.json:
{
"HookHandlers": {
"ui": {
"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\UIHooks",
"services": [ "PermissionManager" ]
},
"parser": {
"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\ParserHooks"
},
"database": {
"class": "MediaWiki\\Extension\\MyExtension\\Hooks\\DatabaseHooks"
}
},
"Hooks": {
"BeforePageDisplay": "ui",
"SkinTemplateNavigation::Universal": "ui",
"ParserFirstCallInit": "parser",
"ParserGetVariableValueSwitch": "parser",
"LoadExtensionSchemaUpdates": "database"
}
}
Handle deprecated hooks:
{
"Hooks": {
"SomeOldHook": {
"handler": "main",
"deprecated": true
},
"NewReplacementHook": "main"
}
}
Marking hooks as deprecated prevents deprecation warnings when MediaWiki phases out old hooks.
Common Hook Patterns
Modify page content before display:
// Hook: BeforePageDisplay
public function onBeforePageDisplay( $out, $skin ): void {
$out->addJsConfigVars( 'myExtensionConfig', [
'setting' => true
] );
$out->addModules( 'ext.myextension.init' );
}
Add custom user rights:
// Hook: UserGetRights
public function onUserGetRights( $user, &$rights ) {
if ( $user->isRegistered() ) {
$rights[] = 'myextension-use-feature';
}
}
Validate page saves:
// Hook: EditFilterMergedContent
public function onEditFilterMergedContent( $context, $content, $status, $summary, $user, $minoredit ) {
if ( strpos( $content->getText(), 'forbidden-word' ) !== false ) {
$status->fatal( 'myextension-forbidden-word-error' );
return false; // Abort save
}
return true;
}
Track custom actions:
// Hook: PageSaveComplete
public function onPageSaveComplete( $wikiPage, $user, $summary, $flags, $revisionRecord, $editResult ) {
// Log to custom table or external service
$dbw = wfGetDB( DB_PRIMARY );
$dbw->insert(
'myextension_edits',
[
'page_id' => $wikiPage->getId(),
'user_id' => $user->getId(),
'timestamp' => $dbw->timestamp()
],
__METHOD__
);
}
See references/hooks-reference.md for 50+ common hooks with examples and assets/templates/HookHandlerExample.php for complete implementation.
Workflow 3: Creating Special Pages
Special pages provide custom functionality accessible via Special:PageName.
Basic Special Page
- Create special page class:
Create includes/Specials/SpecialMyPage.php:
<?php
namespace MediaWiki\Extension\MyExtension\Specials;
use SpecialPage;
class SpecialMyPage extends SpecialPage {
public function __construct() {
parent::__construct( 'MyPage' );
}
public function execute( $subPage ) {
$this->setHeaders();
$this->outputHeader();
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'myextension-mypage-title' ) );
$out->addWikiMsg( 'myextension-mypage-intro' );
$out->addHTML( '<p>Hello from MyPage!</p>' );
}
protected function getGroupName() {
return 'other';
}
}
- Register in
extension.json:
{
"SpecialPages": {
"MyPage": "MediaWiki\\Extension\\MyExtension\\Specials\\SpecialMyPage"
},
"MessagesDirs": {
"MyExtension": ["i18n"]
}
}
- Add messages to
i18n/en.json:
{
"mypage": "My Page",
"myextension-mypage-title": "My Custom Page",
"myextension-mypage-intro": "This is an example special page."
}
- Access: Navigate to
Special:MyPageon your wiki.
Special Page with Form
For pages accepting user input:
<?php
namespace MediaWiki\Extension\MyExtension\Specials;
use HTMLForm;
use SpecialPage;
class SpecialMyForm extends SpecialPage {
public function __construct() {
parent::__construct( 'MyForm' );
}
public function execute( $subPage ) {
$this->setHeaders();
$this->checkPermissions();
$formDescriptor = [
'username' => [
'type' => 'text',
'label-message' => 'myextension-form-username',
'required' => true,
],
'message' => [
'type' => 'textarea',
'label-message' => 'myextension-form-message',
'rows' => 5,
],
'sendcopy' => [
'type' => 'check',
'label-message' => 'myextension-form-sendcopy',
],
];
$htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
$htmlForm
->setSubmitTextMsg( 'myextension-form-submit' )
->setSubmitCallback( [ $this, 'onSubmit' ] )
->show();
}
public function onSubmit( array $data ) {
// Process form submission
$username = $data['username'];
$message = $data['message'];
// Do something with the data
$this->getOutput()->addWikiMsg( 'myextension-form-success', $username );
return true; // Or return Status object
}
protected function getGroupName() {
return 'users';
}
}
Restricted Special Page
Require specific permissions:
public function __construct() {
parent::__construct(
'MyAdminPage',
'myextension-admin' // Required user right
);
}
public function execute( $subPage ) {
$this->setHeaders();
$this->checkPermissions(); // Verifies user has required right
// Page content for authorized users only
}
Define the user right in extension.json:
{
"AvailableRights": [
"myextension-admin"
],
"GroupPermissions": {
"sysop": {
"myextension-admin": true
}
}
}
See assets/templates/SpecialPageExample.php for complete implementations.
Includable Special Pages
Includable special pages can be transcluded into wiki pages using {{Special:PageName}} syntax:
Create includable special page:
<?php
namespace MediaWiki\Extension\MyExtension\Specials;
use IncludableSpecialPage;
class SpecialMyIncludable extends IncludableSpecialPage {
public function __construct() {
parent::__construct( 'MyIncludable' );
}
public function execute( $par = null ) {
if ( $this->including() ) {
// Content when transcluded: {{Special:MyIncludable}}
$this->getOutput()->addWikiTextAsInterface(
$this->msg( 'myextension-transcluded', $par )->text()
);
} else {
// Content when viewed directly at Special:MyIncludable
$this->setHeaders();
$this->getOutput()->addWikiTextAsInterface(
$this->msg( 'myextension-direct-view' )->text()
);
}
}
}
Register in extension.json:
{
"SpecialPages": {
"MyIncludable": "MediaWiki\\Extension\\MyExtension\\Specials\\SpecialMyIncludable"
}
}
Usage in wiki pages:
- Direct link:
[[Special:MyIncludable]] - Transclusion:
{{Special:MyIncludable}} - With parameter:
{{Special:MyIncludable/Item123}}
See assets/templates/SpecialIncludableExample.php for complete implementation.
Workflow 4: Creating API Modules
Extensions can provide API endpoints for programmatic access.
Action API Module
The Action API follows MediaWiki's api.php pattern.
- Create API module:
Create includes/Api/ApiMyAction.php:
<?php
namespace MediaWiki\Extension\MyExtension\Api;
use ApiBase;
class ApiMyAction extends ApiBase {
public function execute() {
$params = $this->extractRequestParams();
$result = [
'success' => true,
'input' => $params['text'],
'output' => strtoupper( $params['text'] )
];
$this->getResult()->addValue( null, $this->getModuleName(), $result );
}
public function getAllowedParams() {
return [
'text' => [
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true,
],
];
}
public function getExamplesMessages() {
return [
'action=myaction&text=hello'
=> 'apihelp-myaction-example-1',
];
}
}
- Register in
extension.json:
{
"APIModules": {
"myaction": "MediaWiki\\Extension\\MyExtension\\Api\\ApiMyAction"
}
}
- Use the API:
GET /api.php?action=myaction&text=hello&format=json
Response:
{
"myaction": {
"success": true,
"input": "hello",
"output": "HELLO"
}
}
API Query List Modules
Query list modules return lists of items and are accessed via action=query&list=modulename:
Create list module:
<?php
namespace MediaWiki\Extension\MyExtension\Api;
use ApiQueryBase;
use ApiBase;
class ApiQueryMyList extends ApiQueryBase {
public function __construct( $query, $moduleName ) {
// Third parameter is prefix for parameters (e.g., 'ml' for 'mllimit')
parent::__construct( $query, $moduleName, 'ml' );
}
public function execute() {
$params = $this->extractRequestParams();
$db = $this->getDB();
$res = $db->select(
'myextension_items',
[ 'item_id', 'item_name' ],
[],
__METHOD__,
[ 'LIMIT' => $params['limit'] ]
);
$items = [];
foreach ( $res as $row ) {
$items[] = [
'id' => (int)$row->item_id,
'name' => $row->item_name
];
}
$this->getResult()->addValue( [ 'query', $this->getModuleName() ], 'items', $items );
}
public function getAllowedParams() {
return [
'limit' => [
ApiBase::PARAM_TYPE => 'integer',
ApiBase::PARAM_DFLT => 10,
ApiBase::PARAM_MIN => 1,
ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
],
];
}
protected function getExamplesMessages() {
return [
'action=query&list=mylist' => 'apihelp-query+mylist-example-1',
];
}
}
Register in extension.json:
{
"APIListModules": {
"mylist": "MediaWiki\\Extension\\MyExtension\\Api\\ApiQueryMyList"
}
}
API module types:
APIModules- Base action modules (e.g.,action=myaction)APIListModules- Query list modules (e.g.,action=query&list=mylist)APIMetaModules- Query meta modules (e.g.,action=query&meta=myinfo)APIQueryModules- Query property modules (e.g.,action=query&prop=myprop)
See assets/templates/ApiQueryListExample.php for complete implementation.
REST API Module
Modern REST API (MediaWiki 1.35+):
- Create handler:
Create includes/Rest/HelloHandler.php:
<?php
namespace MediaWiki\Extension\MyExtension\Rest;
use MediaWiki\Rest\SimpleHandler;
use Wikimedia\ParamValidator\ParamValidator;
class HelloHandler extends SimpleHandler {
public function run( $name ) {
return [
'message' => "Hello, $name!",
'timestamp' => wfTimestampNow()
];
}
public function getParamSettings() {
return [
'name' => [
self::PARAM_SOURCE => 'path',
ParamValidator::PARAM_TYPE => 'string',
ParamValidator::PARAM_REQUIRED => true,
],
];
}
}
- Register in
extension.json:
{
"RestRoutes": [
{
"path": "/myextension/hello/{name}",
"method": "GET",
"class": "MediaWiki\\Extension\\MyExtension\\Rest\\HelloHandler"
}
]
}
- Use the REST API:
GET /rest.php/myextension/hello/World
Response:
{
"message": "Hello, World!",
"timestamp": "20240209123456"
}
API Security Best Practices
- Validate all inputs: Use
ParamValidatorand type checking - Require tokens for writes: Use
needsToken()for modifications - Check permissions: Use
mustBePosted()and verify user rights - Rate limiting: Consider using MediaWiki's rate limiting hooks
- Sanitize output: Escape user-provided content
Custom Page Actions
Actions add custom tabs and functionality to wiki pages (similar to "edit", "history"):
Create action class:
<?php
namespace MediaWiki\Extension\MyExtension\Actions;
use FormlessAction;
class MyCustomAction extends FormlessAction {
public function getName() {
return 'myaction';
}
protected function getDescription() {
return ''; // Empty string to disable subtitle
}
public function onView() {
return null;
}
public function show() {
parent::show();
$this->getOutput()->addWikiTextAsInterface(
$this->msg( 'myextension-action-text', $this->getTitle()->getPrefixedText() )->text()
);
}
}
Register in extension.json:
{
"Actions": {
"myaction": "MediaWiki\\Extension\\MyExtension\\Actions\\MyCustomAction"
}
}
Add action tab to navigation (in hook handler):
public function onSkinTemplateNavigation__Universal( $skin, &$content_navigation ): void {
$title = $skin->getTitle();
if ( $title && !$title->isSpecialPage() ) {
$content_navigation['actions']['myaction'] = [
'class' => $skin->getRequest()->getText( 'action' ) === 'myaction' ? 'selected' : false,
'text' => $skin->msg( 'myextension-action-tab' )->text(),
'href' => $title->getLocalURL( 'action=myaction' ),
];
}
}
Access: Navigate to any page and add ?action=myaction to the URL, or click the tab.
See assets/templates/ActionExample.php for complete implementation.
Workflow 5: Extending Wiki Markup
Add custom tags, parser functions, and variables to wiki content.
Custom Tags
Register custom XML-style tags like <myextension>content</myextension>:
// In hook handler
public function onParserFirstCallInit( $parser ) {
$parser->setHook( 'myextension', [ $this, 'renderMyTag' ] );
}
public function renderMyTag( $input, array $args, $parser, $frame ) {
// $input: Content between tags
// $args: Tag attributes (e.g., <myextension foo="bar">)
$output = '<div class="myextension-output">';
$output .= htmlspecialchars( $input );
$output .= '</div>';
// Parse wikitext in input
// $parsed = $parser->recursiveTagParse( $input, $frame );
return $output;
}
Usage in wiki:
<myextension>This content will be processed</myextension>
Parser Functions
Register template-style functions like {{#myfunction:arg1|arg2}}:
public function onParserFirstCallInit( $parser ) {
$parser->setFunctionHook( 'myfunction', [ $this, 'renderFunction' ] );
}
public function renderFunction( $parser, $arg1 = '', $arg2 = '' ) {
// Process arguments
$result = strtoupper( $arg1 ) . ' - ' . strtolower( $arg2 );
// Return array with output and options
return [ $result, 'noparse' => true, 'isHTML' => false ];
}
Usage in wiki:
{{#myfunction:Hello|WORLD}}
Output: HELLO - world
Magic Words / Variables
Add variables like {{CURRENTYEAR}}:
public function onMagicWordwgVariableIDs( &$variableIDs ) {
$variableIDs[] = 'mycustomvar';
}
public function onParserGetVariableValueSwitch( $parser, &$cache, $magicWordId, &$ret ) {
if ( $magicWordId === 'mycustomvar' ) {
$ret = 'Custom Value';
}
}
Register in extension.json:
{
"Hooks": {
"MagicWordwgVariableIDs": "main",
"ParserGetVariableValueSwitch": "main"
}
}
Usage in wiki:
{{MYCUSTOMVAR}}
Localizing Parser Extensions
For parser functions and magic words, use ExtensionMessagesFiles to provide translations:
Create MyExtension.i18n.magic.php:
<?php
$magicWords = [];
/** English */
$magicWords['en'] = [
'myfunction' => [ 0, 'myfunction' ],
'mycustomvar' => [ 0, 'MYCUSTOMVAR' ],
];
/** Dutch */
$magicWords['nl'] = [
'myfunction' => [ 0, 'mijnfunctie' ],
'mycustomvar' => [ 0, 'MIJNCUSTOMVAR' ],
];
Register in extension.json:
{
"ExtensionMessagesFiles": {
"MyExtensionMagic": "MyExtension.i18n.magic.php"
}
}
This allows users to use localized magic word names in their language.
See assets/templates/MyExtension.i18n.magic.php for complete template and MediaWiki documentation for parser extension patterns.
Workflow 6: Adding Database Tables
Extensions should use custom tables, never modify core tables.
Schema Definition
- Create SQL schema in
sql/tables.json(preferred, MW 1.39+):
[
{
"name": "myextension_items",
"columns": [
{
"name": "item_id",
"type": "integer",
"options": { "autoincrement": true, "notnull": true, "unsigned": true }
},
{
"name": "item_name",
"type": "string",
"options": { "length": 255, "notnull": true }
},
{
"name": "item_created",
"type": "mwtimestamp",
"options": { "notnull": true }
}
],
"indexes": [
{
"name": "item_name",
"columns": ["item_name"],
"unique": false
}
],
"pk": ["item_id"]
}
]
Or create SQL files for different databases:
sql/mysql/tables.sqlsql/postgres/tables.sqlsql/sqlite/tables.sql
Example sql/mysql/tables.sql:
CREATE TABLE /*_*/myextension_items (
item_id INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
item_name VARCHAR(255) NOT NULL,
item_created BINARY(14) NOT NULL,
INDEX item_name (item_name)
) /*$wgDBTableOptions*/;
- Register in hook handler:
public function onLoadExtensionSchemaUpdates( $updater ) {
$dir = dirname( __DIR__, 2 );
// For abstract schema (MW 1.39+)
$updater->addExtensionTable(
'myextension_items',
"$dir/sql/tables.json"
);
// Or for SQL files
// $updater->addExtensionTable(
// 'myextension_items',
// "$dir/sql/{$updater->getDB()->getType()}/tables.sql"
// );
}
- Run database updates:
php maintenance/update.php
Multi-Database Support
Support MySQL, PostgreSQL, and SQLite using abstract schema (recommended):
Directory structure:
sql/
├── tables.json # Abstract schema
├── tables-generated.sql # Generated MySQL SQL
├── postgres/
│ └── tables-generated.sql
└── sqlite/
└── tables-generated.sql
Generate SQL from abstract schema:
php maintenance/generateSchemaSql.php --json sql/tables.json --sql sql/tables-generated.sql
php maintenance/generateSchemaSql.php --json sql/tables.json --sql sql/postgres/tables-generated.sql --type postgres
php maintenance/generateSchemaSql.php --json sql/tables.json --sql sql/sqlite/tables-generated.sql --type sqlite
Hook handler with database detection:
public function onLoadExtensionSchemaUpdates( $updater ) {
$dbType = $updater->getDB()->getType();
$dir = __DIR__ . '/../sql';
if ( $dbType !== 'mysql' ) {
$dir .= "/$dbType";
}
$updater->addExtensionTable( 'myextension_items', "$dir/tables-generated.sql" );
}
This pattern ensures your extension works across different database systems used by MediaWiki installations.
Using the Database
Insert data:
$dbw = wfGetDB( DB_PRIMARY );
$dbw->insert(
'myextension_items',
[
'item_name' => 'Example',
'item_created' => $dbw->timestamp()
],
__METHOD__
);
$itemId = $dbw->insertId();
Query data:
$dbr = wfGetDB( DB_REPLICA );
$result = $dbr->select(
'myextension_items',
[ 'item_id', 'item_name', 'item_created' ],
[ 'item_name' => 'Example' ],
__METHOD__,
[ 'ORDER BY' => 'item_created DESC', 'LIMIT' => 10 ]
);
foreach ( $result as $row ) {
echo $row->item_name;
}
Update data:
$dbw = wfGetDB( DB_PRIMARY );
$dbw->update(
'myextension_items',
[ 'item_name' => 'Updated Name' ],
[ 'item_id' => 123 ],
__METHOD__
);
Delete data:
$dbw = wfGetDB( DB_PRIMARY );
$dbw->delete(
'myextension_items',
[ 'item_id' => 123 ],
__METHOD__
);
Database Best Practices
- Always use DB_REPLICA for reads: Only use DB_PRIMARY for writes
- Use parameter binding: Never concatenate user input into queries
- Prefix table names: Use
myextension_prefix for all tables - Use /_/ in SQL: Allows wiki-specific table prefixes
- Add indexes: Index frequently queried columns
- Use transactions: For multi-step operations
- Check existence: Use
tableExists()before querying
See references/database-schema.md for detailed patterns and migration examples.
Extension Points Reference
MediaWiki provides numerous extension points. Brief overview:
General Extension Points
- Hooks: Inject code at 500+ points in MediaWiki execution
- Domain Events: React to state changes (MW 1.44+)
- Jobs: Queue asynchronous background tasks
- Content Handlers: Support custom content models
Page Extension Points
- Special Pages: Custom pages like
Special:MyPage - Actions: Custom page actions (
?action=myaction) - Tracking Categories: Automatically categorize pages
API Extension Points
- Action API Modules: Extend
api.php - REST API Handlers: Modern REST endpoints
- API Metadata: Provide API documentation
Content Extension Points
- Parser Hooks: Tags, functions, variables
- Content Models: Support non-wikitext content
- Media Handlers: Support custom media types
Moderation Extension Points
- Log Types: Custom log entries
- Recent Changes Flags: Custom RC indicators
- Revision Tags: Annotate revisions
Authentication Extension Points
- Auth Providers: Custom login mechanisms
- Session Providers: Custom session handling
See references/extension-points.md for complete list with examples.
Best Practices
Code Organization
Directory structure:
MyExtension/
├── extension.json # Registration and config
├── includes/ # PHP code
│ ├── Api/ # API modules
│ ├── Hooks/ # Hook handlers
│ ├── Specials/ # Special pages
│ └── ...
├── resources/ # Frontend assets
│ ├── js/ # JavaScript
│ ├── css/ # Styles
│ └── ...
├── i18n/ # Localization
│ ├── en.json
│ └── qqq.json
├── sql/ # Database schemas
├── tests/ # Tests
│ ├── phpunit/
│ └── parser/
└── README.md # Documentation
Naming conventions:
- Namespaces:
MediaWiki\Extension\MyExtension\ - Classes:
UpperCamelCase - Methods:
camelCase - Constants:
UPPER_SNAKE_CASE - Config:
$wgMyExtensionSettingName - Messages:
myextension-message-key
Security
Always sanitize output:
// For HTML output
$output->addHTML( Html::element( 'div', [], $userInput ) );
// For plain text
$output->addWikiTextAsInterface( $userInput );
// For messages
$this->msg( 'key', $userInput )->escaped();
Validate input:
// Check user permissions
if ( !$this->getUser()->isAllowed( 'myextension-action' ) ) {
throw new PermissionsError( 'myextension-action' );
}
// Validate tokens for state-changing operations
$request = $this->getRequest();
if ( !$this->getUser()->matchEditToken( $request->getVal( 'token' ) ) ) {
// Invalid token
}
Database security:
// Use parameterized queries
$dbr->select(
'myextension_items',
'*',
[ 'item_name' => $userInput ], // Automatically escaped
__METHOD__
);
// NEVER do this
// $sql = "SELECT * FROM myextension_items WHERE item_name = '$userInput'";
Performance
Caching:
$cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
$key = $cache->makeKey( 'myextension', 'data', $itemId );
$data = $cache->getWithSetCallback(
$key,
$cache::TTL_HOUR,
function () use ( $itemId ) {
// Expensive operation
return $this->fetchData( $itemId );
}
);
Lazy loading:
// Don't load heavy resources on every page
public function onBeforePageDisplay( $out, $skin ): void {
// Only load on specific pages
if ( $out->getTitle()->inNamespace( NS_MAIN ) ) {
$out->addModules( 'ext.myextension.init' );
}
}
Database optimization:
- Use
DB_REPLICAfor reads - Add indexes for frequently queried columns
- Use
select()with specific fields, not* - Consider batching operations
MediaWiki Coding Conventions
Follow MediaWiki coding conventions:
- Indentation: Tabs, not spaces
- Line length: Soft limit 100 characters
- Bracing: Opening brace on same line
- Spacing: Space after keywords, around operators
- Documentation: PHPDoc for all public methods
Example:
<?php
namespace MediaWiki\Extension\MyExtension;
/**
* Example class demonstrating coding conventions
*/
class ExampleClass {
/**
* Process some data
*
* @param string $input User input
* @param array $options Processing options
* @return string Processed output
*/
public function process( $input, array $options = [] ) {
if ( empty( $input ) ) {
return '';
}
$result = strtoupper( $input );
if ( isset( $options['reverse'] ) && $options['reverse'] ) {
$result = strrev( $result );
}
return $result;
}
}
Common Patterns
Pattern 1: User Preference
Add custom user preferences:
// Hook: GetPreferences
public function onGetPreferences( $user, &$preferences ) {
$preferences['myextension-feature-enabled'] = [
'type' => 'toggle',
'label-message' => 'myextension-pref-enable',
'section' => 'personal/myextension',
];
}
// Check preference in code
$user = $this->getUser();
$enabled = $user->getOption( 'myextension-feature-enabled', false );
Pattern 2: Page Property
Store custom metadata on pages:
// Set page property
$wikiPage->setProperty( 'myextension_validated', '1' );
// Query pages with property
$dbr = wfGetDB( DB_REPLICA );
$result = $dbr->select(
[ 'page', 'page_props' ],
'page_title',
[
'pp_propname' => 'myextension_validated',
'pp_value' => '1'
],
__METHOD__,
[],
[
'page_props' => [ 'JOIN', 'page_id=pp_page' ]
]
);
Pattern 3: ResourceLoader Module
Load JavaScript and CSS:
In extension.json:
{
"ResourceModules": {
"ext.myextension.init": {
"scripts": "resources/js/init.js",
"styles": "resources/css/styles.css",
"dependencies": ["mediawiki.api"],
"messages": ["myextension-js-message"]
},
"ext.myextension.module": {
"packageFiles": [
"resources/js/module.js",
"resources/js/utils.js"
],
"dependencies": ["mediawiki.util"],
"messages": ["myextension-module-message"]
}
}
}
packageFiles vs scripts:
scripts: Traditional concatenated JavaScript filespackageFiles: Modern ES6 modules withrequire()andmodule.exports
With packageFiles, use modular JavaScript:
// resources/js/utils.js
module.exports = {
formatData: function(data) {
return data.toUpperCase();
}
};
// resources/js/module.js
var utils = require('./utils.js');
module.exports = {
init: function() {
var formatted = utils.formatData('hello');
mw.log(formatted); // "HELLO"
}
};
In hook:
public function onBeforePageDisplay( $out, $skin ): void {
$out->addModules( 'ext.myextension.init' );
}
In JavaScript:
mw.loader.using('ext.myextension.init', function() {
// Access messages
var message = mw.message('myextension-js-message').text();
// Use MW API
new mw.Api().get({
action: 'myaction',
text: 'hello'
}).done(function(data) {
console.log(data);
});
});
Pattern 4: Configuration Variables
Define user-configurable settings:
In extension.json:
{
"config": {
"MyExtensionMaxItems": {
"value": 100,
"description": "Maximum number of items to display"
},
"MyExtensionEnableFeature": {
"value": true,
"description": "Enable experimental feature"
}
}
}
In code:
$config = MediaWikiServices::getInstance()->getMainConfig();
$maxItems = $config->get( 'MyExtensionMaxItems' );
$featureEnabled = $config->get( 'MyExtensionEnableFeature' );
User can override in LocalSettings.php:
$wgMyExtensionMaxItems = 200;
$wgMyExtensionEnableFeature = false;
Pattern 5: Dependency Injection
Use MediaWiki services (MW 1.35+):
Create service in includes/Services/MyService.php:
<?php
namespace MediaWiki\Extension\MyExtension\Services;
class MyService {
private $cache;
public function __construct( $cache ) {
$this->cache = $cache;
}
public function doSomething() {
// Use injected dependencies
}
}
Register in extension.json:
{
"ServiceWiringFiles": [
"includes/ServiceWiring.php"
]
}
Create includes/ServiceWiring.php:
<?php
use MediaWiki\Extension\MyExtension\Services\MyService;
use MediaWiki\MediaWikiServices;
return [
'MyExtension.MyService' => function ( MediaWikiServices $services ) {
return new MyService(
$services->getMainWANObjectCache()
);
},
];
Use in code:
$service = MediaWikiServices::getInstance()->getService( 'MyExtension.MyService' );
$service->doSomething();
Testing and Debugging
PHPUnit Tests
Create tests in tests/phpunit/:
<?php
namespace MediaWiki\Extension\MyExtension\Tests;
use MediaWikiIntegrationTestCase;
class MyExtensionTest extends MediaWikiIntegrationTestCase {
public function testBasicFunction() {
$result = strtoupper( 'hello' );
$this->assertSame( 'HELLO', $result );
}
public function testWithDatabase() {
$this->tablesUsed[] = 'myextension_items';
$dbw = $this->db;
$dbw->insert(
'myextension_items',
[ 'item_name' => 'Test' ],
__METHOD__
);
$count = $dbw->selectField(
'myextension_items',
'COUNT(*)',
[],
__METHOD__
);
$this->assertGreaterThan( 0, $count );
}
}
Run tests:
php tests/phpunit/phpunit.php extensions/MyExtension/tests/phpunit/
Parser Tests
Create tests/parser/myExtensionTests.txt:
!! test
MyExtension tag basic
!! wikitext
<myextension>test content</myextension>
!! html
<div class="myextension-output">test content</div>
!! end
Run parser tests:
php tests/parser/parserTests.php --file=extensions/MyExtension/tests/parser/myExtensionTests.txt
QUnit JavaScript Tests
Test JavaScript modules using MediaWiki's QUnit integration:
Register tests in extension.json:
{
"QUnitTestModule": {
"localBasePath": "tests/qunit/",
"remoteExtPath": "MyExtension/tests/qunit/",
"scripts": [
"ext.myextension.test.js"
],
"dependencies": [
"ext.myextension.module"
]
}
}
Create test file tests/qunit/ext.myextension.test.js:
QUnit.module( 'ext.myextension', QUnit.newMwEnvironment(), function ( hooks ) {
hooks.beforeEach( function () {
mw.config.set( {
wgMyExtensionSetting: 'test-value'
} );
} );
QUnit.test( 'Module exports expected interface', function ( assert ) {
var myModule = require( 'ext.myextension.module' );
assert.strictEqual( typeof myModule.init, 'function', 'init method exists' );
} );
QUnit.test( 'Function produces correct output', function ( assert ) {
var myModule = require( 'ext.myextension.module' );
var result = myModule.formatData( 'hello' );
assert.strictEqual( result, 'HELLO', 'Formats data correctly' );
} );
} );
Run tests: Navigate to Special:JavaScriptTest/qunit in your browser.
See assets/templates/qunit-test-example.js for complete patterns.
Common Debugging Techniques
Enable debug mode:
$wgDebugToolbar = true;
$wgShowDebug = true;
error_reporting( -1 );
ini_set( 'display_errors', 1 );
Log debugging information:
wfDebugLog( 'MyExtension', 'Debug message: ' . print_r( $data, true ) );
Use MediaWiki logger:
$logger = LoggerFactory::getInstance( 'MyExtension' );
$logger->debug( 'Processing item', [ 'item_id' => $id ] );
$logger->error( 'Failed to process', [ 'error' => $e->getMessage() ] );
Check errors in console:
- Browser JavaScript console for frontend errors
- PHP error log for backend errors
- MediaWiki debug toolbar for request details
Publishing and Deployment
Documentation
Create documentation page on mediawiki.org:
- Create page:
https://www.mediawiki.org/wiki/Extension:MyExtension - Use the {{Extension}} template:
{{Extension
|name = MyExtension
|status = stable
|type = parserhook
|author = ([[User:YourName|Your Name]])
|version = 1.0.0
|license = GPL-2.0-or-later
|description = Brief description of what it does
|download = {{GerritDownload|MyExtension}}
}}
== Installation ==
{{ExtensionInstall
|download-link=https://gerrit.wikimedia.org/r/mediawiki/extensions/MyExtension
|custom-steps=
# Configure in LocalSettings.php
# Run update.php
}}
== Configuration ==
; <code>$wgMyExtensionSetting</code>
: Description of setting
== Usage ==
How to use the extension...
Version Management
For master compatibility:
- Maintain backward compatibility in master branch
- Support multiple MediaWiki versions
- Use feature detection instead of version checks
For release branches:
- Backport critical fixes to
REL1_XXbranches - Keep version number consistent per branch
- Document supported versions
Declare compatibility in extension.json:
{
"compatibility_policy": "master",
"requires": {
"MediaWiki": ">= 1.39.0"
}
}
Or for release branches:
{
"compatibility_policy": "rel",
"requires": {
"MediaWiki": ">= 1.39.0 < 1.42.0"
}
}
Code Review
For extensions in Gerrit:
- Create account: https://gerrit.wikimedia.org/
- Clone repository:
git clone https://gerrit.wikimedia.org/r/mediawiki/extensions/MyExtension
- Set up git-review:
cd MyExtension
git review -s
- Submit changes:
git add .
git commit -m "Add new feature"
git review
- Address feedback: Update commit and push again with
git review
Wikimedia Deployment
For deployment on Wikimedia wikis (Wikipedia, etc.):
Requirements:
- High code quality and security standards
- Performance optimization
- Comprehensive testing
- Documentation
- Community support
Process:
- Create Phabricator task: https://phabricator.wikimedia.org/
- Code review by WMF engineers
- Security review for sensitive operations
- Performance testing
- Gradual rollout (test wikis → production)
Timeline: Can take months to years depending on complexity and review capacity.
See references/deployment-checklist.md for complete Wikimedia deployment requirements.
Support and Collaboration
Create Phabricator project:
- Request project creation for your extension
- Track bugs and feature requests
- Coordinate with contributors
Maintain the extension:
- Respond to bug reports
- Review and merge contributions
- Update for new MediaWiki versions
- Keep documentation current
Troubleshooting
Extension Not Appearing
Symptoms: Extension doesn't show on Special:Version
Solutions:
- Verify
wfLoadExtension( 'MyExtension' )inLocalSettings.php - Check
extension.jsonsyntax (use JSON validator) - Verify file permissions (extension directory readable by web server)
- Check
manifest_versionis 2 - Look for PHP errors in error log
Hooks Not Firing
Symptoms: Hook handler not executing
Solutions:
- Verify hook name matches exactly (case-sensitive)
- Check hook handler signature matches documentation
- Ensure hook handler class is autoloaded
- Use
wfDebugLog()to verify registration - Check if hook was deprecated/removed in your MW version
Database Errors
Symptoms: SQL errors when accessing custom tables
Solutions:
- Run
php maintenance/update.phpto create tables - Check table name prefix in queries
- Use
DB_REPLICAfor reads,DB_PRIMARYfor writes - Verify table exists:
$db->tableExists( 'myextension_items' ) - Check SQL syntax for your database type
Localization Not Working
Symptoms: Message keys displayed instead of translated text
Solutions:
- Verify
MessagesDirsinextension.json - Check JSON syntax in
i18n/*.jsonfiles - Clear caches:
$wgMainCacheType = CACHE_NONE - Ensure message key is lowercase
- Use
->escaped()or->text()on message objects
Namespace/Autoloading Errors
Symptoms: Class not found errors
Solutions:
- Verify
AutoloadNamespacesinextension.json - Check namespace matches directory structure
- Ensure class name matches filename
- Use correct namespace in PHP files
- Run
composer dump-autoloadif using Composer
Permission Denied
Symptoms: Users can't access special pages or features
Solutions:
- Check permission requirements in special page constructor
- Verify user rights granted in
extension.json - Use
$user->isAllowed( 'right' )to check - Add required rights to appropriate user groups
- Test with admin account first
Reference Files
Detailed information in reference documents:
references/extension-points.md- Complete list of all extension points with examplesreferences/hooks-reference.md- 50+ common hooks with usage patterns and examplesreferences/database-schema.md- Database schema patterns, migrations, best practicesreferences/localization-guide.md- Complete i18n implementation including plurals, GENDER, etc.references/deployment-checklist.md- Requirements for Wikimedia production deployment
Load these files when detailed information is needed.
Template Files
Working code examples in templates:
assets/templates/extension.json- Complete extension.json with all common fieldsassets/templates/SpecialPageExample.php- Special page with form and validationassets/templates/HookHandlerExample.php- Hook handler implementing multiple hooksassets/templates/en.json- Localization file with various message typesassets/templates/qqq.json- Message documentation template
Use templates as starting points for implementation.
Key Concepts
Extension registration: Modern extensions use extension.json and load via wfLoadExtension()
Extension points: MediaWiki provides hooks, special pages, APIs, and other integration points
Localization: All user-facing text must use i18n system for translation
Security: Always validate input, sanitize output, check permissions
Performance: Use caching, lazy loading, database optimization
Compatibility: Support multiple MediaWiki versions or maintain release branches
Community: Open source development with code review and collaboration
Resources
- MediaWiki Developer Documentation: https://www.mediawiki.org/wiki/Manual:Developing_extensions
- API Documentation: https://doc.wikimedia.org/
- Extension Examples: https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/Examples
- BoilerPlate Extension: https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/extensions/BoilerPlate
- Coding Conventions: https://www.mediawiki.org/wiki/Manual:Coding_conventions
- Hooks: https://www.mediawiki.org/wiki/Manual:Hooks
- Gerrit: https://gerrit.wikimedia.org/
- Phabricator: https://phabricator.wikimedia.org/
Note: This skill covers MediaWiki 1.39+. Some features may differ in older versions. Always consult official MediaWiki documentation for version-specific details.