Individual projects will often find it useful toextend the built-in wiki markup with additional capabilities, whether simple string processing, or full-blown information retrieval. Tag extensions allow users to create new custom tags that do just that. For example, one might use a tag extension to introduce a simple<donation/>
tag, which injects a donation form into the page. Extensions, includingparser functions andhooks, are the most effective way to change or enhance the functionality of MediaWiki. You should always check before you start work on an extension to make sure someone else hasn't done exactly what you are trying to do.
A simple tag extension consists of acallback function, which ishooked to the parser so that, when the parser runs, it will find and replace all instances of a specific tag, calling the corresponding callback function to render the actual HTML.
![]() | Parts of this page (those related to Section) areoutdated. It was written for an older version of MediaWiki and may not apply to the most recent version. If you have checked or updated this page and found the content to be suitable, please remove this notice. See thetalk page for a possible discussion on this. |
Inextension.json, set up thehooks:
..."Hooks":{"ParserFirstCallInit":"ExampleExtensionHooks"},"HookHandlers":{"ExampleExtensionHooks":{"class":"MediaWiki\\Extension\\ExampleExtension\\Hooks"}}...
And add the hook into a PHP file
<?phpnamespaceMediaWiki\Extension\ExampleExtension;classExampleExtensionimplementsParserFirstCallInitHook{// Register any render callbacks with the parserpublicfunctiononParserFirstCallInit($parser){// When the parser sees the <sample> tag, it executes renderTagSample (see below)$parser->setHook('sample',[$this,'renderTagSample']);}// Render <sample>publicfunctionrenderTagSample($input,array$args,Parser$parser,PPFrame$frame){// Nothing exciting here, just escape the user-provided input and throw it back out again (as example)returnhtmlspecialchars($input);}}
This example registers a callback function for the<sample>
tag. When a user adds this tag to a page like this:<samplearg1="xxx"arg2="xxx">...input...</sample>
, the parser will call therenderTagSample()
function, passing in four arguments:
<sample>
and</sample>
tags, ornull if the tag is "closed", i.e.<sample/>
For a more elaborate example, seeTag extension example
Let's look at another example:
<?php$wgHooks['ParserFirstCallInit'][]='onParserFirstCallInit';functiononParserFirstCallInit(Parser$parser){$parser->setHook('sample','wfSampleRender');}functionwfSampleRender($input,array$args,Parser$parser,PPFrame$frame){$attr=[];// This time, make a list of attributes and their values, and dump them, along with the user inputforeach($argsas$name=>$value){$attr[]='<strong>'.htmlspecialchars($name).'</strong> = '.htmlspecialchars($value);}returnimplode('<br />',$attr)."\n\n".htmlspecialchars($input);/** * The following lines can be used to get the variable values directly: * $to = $args['to'] ; * $email = $args['email'] ; */}
This example dumps the attributes passed to the tag, along with their values. It's quite evident that this allows for flexible specification of new, custom tags. You might, for example, define a tag extension that allows a user to inject a contact form on their user page, using something like<emailformto="User"email="user@foo.com"/>
.
There is a veritable plethora of tag extensions available for MediaWiki, some of which arelisted on this site; others can be found via a quick web search. While a number of these are quite specialised for their use case, there are a great deal of well-loved and well-used extensions providing varying degrees of functionality.
SeeManual:Developing extensions for the general layout and setup of an extension.
It has been suggested that this page or section be merged withManual:Developing extensions#Deploying and registering#Publishing.(Discuss) |
See alsopublishing your extension.
You'll notice above that the input in the examples above is escaped usinghtmlspecialchars()
before being returned. It is vital that all user input is treated in this manner before echoing it back to the clients, to avoid introducing vectors for arbitraryHTML injection, which can lead tocross-site scripting vulnerabilities.
The right way to add modules for your extension is to attach them to the ParserOutput rather than to $wgOut. The module list will then be automatically taken from the ParserOutput object and added to $wgOut even when the page rendering is pre-cached. If you are directly adding the modules to $wgOut they might not be cached in the parser output.
functionmyCoolHook($text,array$params,Parser$parser,PPFrame$frame){// ... do stuff ...$parser->getOutput()->addModules('ext.mycoolext');$parser->getOutput()->addModuleStyles('ext.mycoolext.styles');// ... do more stuff ...}
If you change the code for an extension, all pages that use the extension will, theoretically, immediately reflect the results of new code. Technically speaking, this means your code is executed each and every time a page containing the extension is rendered.
In practice, this is often not the case, due to page caching - either by the MediaWiki software, the browser or by an intermediary proxy or firewall.
To bypass MediaWiki's parser cache and ensure a new version of the page is generated, click on edit, replace "action=edit" in the URL shown in the address bar of your browser by "action=purge" and submit the new URL. The page and all templates it references will be regenerated, ignoring all cached data. The purge action is needed if the main page itself is not modified, but the way it must be rendered has changed (the extension was modified, or only a referenced template was modified).
If this is not sufficient to get you a fresh copy of the page, you can normally bypass intermediary caches by adding '&rand=somerandomtext' to the end of the above URL. Make sure 'somerandomtext' is different every time.
Since MediaWiki 1.5, the parser is passed as the third parameter to an extension. This parser can be used to invalidate the parser cache like this:
functionwfSampleSomeHookFunction($text,array$args,Parser$parser,PPFrame$frame){$parser->getOutput()->updateCacheExpiry(0);// ...}
Maybe you don't want to disable caching entirely, you just want the page to be regenerated whenever another page is edited, similar to the way that template transclusions are handled. This can be done using the parser object that is passed to your hook function and calling theaddTemplate
.
You can use fine grained caching for your extension by using cache keys to differentiate between different versions of your extension output.While rendering you can add cache keys for every feature by adding an addExtraKey method to your hook function, e.g.:
functionwfSampleSomeHookFunction($text,array$args,Parser$parser,PPFrame$frame){$userOptionsLookup=MediaWikiServices::getInstance()->getUserOptionsLookup();$setting1=(int)$userOptionsLookup->getOption($parser->getUserIdentity(),'setting1');$parser->getOptions()->optionUsed('setting1');$setting2=(int)$userOptionsLookup->getOption($parser->getUserIdentity(),'setting2');$parser->getOptions()->optionUsed('setting2');...}
However, modifying$parser->getOptions()
during parse means that the extra option keys aren't included when trying to get a cached page, only when rendering a page to go into cache, so you can use thePageRenderingHash hook to set extra options. PageRenderingHash is run both when putting a page into cache, and getting it out, so its important to only add new keys to the hash if they're not already there. e.g:
$wgHooks['PageRenderingHash'][]='wfMyExtOnPageRenderingHash';functionwfMyExtOnPageRenderingHash(&$confstr,$user,$optionsUsed){$userOptionsLookup=MediaWikiServices::getInstance()->getUserOptionsLookup();if(in_array('setting1',$optionsUsed)){$confstr.="!setting1=".$userOptionsLookup->getOption($user,'setting1');}if(in_array('setting2',$optionsUsed)){$confstr.="!setting2=".$userOptionsLookup->getOption($user,'setting2');}}
Some important notes on this:
$parser->getOptions()->addExtraKey()
instead of$parser->getOptions()->optionUsed()
. Be warned that addExtraKey does not tell the parser cache that the extra key is in use, and thus can easily result in breaking the cache if you are not careful.MediaWiki version: | ≥ 1.16 |
Parser hook functions are passed a reference to the parser object and a frame object; these should be used to parse wikitext.
functionwfSampleWonderfulHook($text,array$args,Parser$parser,PPFrame$frame){$output=$parser->recursiveTagParse($text,$frame);return'<div>'.$output.'</div>';}
Parser::recursiveTagParse()
has been around since version 1.8. Its advantages include simplicity (it takes just one argument and returns a string) and the fact that it parses extension tags in$text
, so you can nest extension tags.
The second parameter to recursiveTagParse,$frame
, is an optional argument introduced in MW 1.16 alpha (r55682).
$frame
is provided (using the value of$frame
passed to your extension), then any template parameters in$text
will be expanded. In other words, content such as{{{1}}}
will be recognized and converted into the appropriate value.$frame
is not provided (e.g.,$parser->recursiveTagParse($text)
), or if$frame
is set to false, then template parameters will not be expanded;{{{1}}}
will not be altered. Although this unlikely to be the desired behavior, this was the only option available before MW 1.16.However, one step of parsing that is still skipped for tags, even when using recursiveTagParse, isParser::preSaveTransform
. preSaveTransform is the first step of parsing, responsible for making permanent changes to the about-to-be saved wikitext, such as:
The original call to preSaveTransform intentionally skips such conversions within all extension tags. If you need pre save transform to be done, you should consider using aparser function instead. All tag extensions can also be called as a parser function using{{#tag:tagname|input|attribute_name=value}}
which will have pre save transform applied.
Since MediaWiki 1.5, XML-style parameters (tag attributes) are supported. The parameters are passed as the second parameter to the hook function, as an associative array. The value strings have already had HTML character entities decoded for you, so if you emit them back to HTML, don't forget to usehtmlspecialchars( $codeToEncode, ENT_QUOTES )
, to avoid the risk of HTML injection.
The return value of a tag extension is consideredalmost parsed text, which means its not treated as pure html, but still modified slightly. There are two main things that are done to the output of a tag extension (Along with a couple other minor things):
<pre>
among other things. This can sometimes be an issue in some extensions.Tag extensions also support returning an array instead of just a string (Much like parser functions) in order to change how the return value is interpreted. The 0th value of the array must be the HTML. The "markerType" key can be set tonowiki
in order to stop further parsing. Doing something likereturn[$html,'markerType'=>'nowiki'];
would ensure that the $html value is not further modified and treated as just plain html.
SeeManual:Extension registration and register your extension accordingly.
Suppose you have several tags<foo>
and<bar>
that share the same callback, and inside the callback function, you want to obtain thename of the tag that invoked the callback.
$wgHooks['ParserFirstCallInit'][]='onParserFirstCallInit';# ...publicfunctiononParserFirstCallInit(Parser$parser){$parser->setHook('foo','sharedFunctionality');$parser->setHook('bar','sharedFunctionality');}# ...publicfunctionsharedFunctionality($input,array$args,Parser$parser,PPFrame$frame){// How to distinguish between 'foo' and 'bar' calls?}
The short answer is: the tag name (foo
orbar
) is not present in any of the callback's arguments.But you can work around this by dynamically constructing a separate callback for each tag:
$wgHooks['ParserFirstCallInit'][]='onParserFirstCallInit';# ...publicfunctiononParserFirstCallInit(Parser$parser){// For each tag nameforeach(['foo','bar']as$tagName){// Dynamically create a callback function$callback=function($input,$args,$parser,$frame)use($tagName){// The callback invokes the shared function.// Notice we now pass the tag name as a parameter.returnsharedFunctionality($input,$args,$parser,$frame,$tagName);};// Assign the callback to the tag$parser->setHook($tagName,$callback);}}# ...publicfunctionsharedFunctionality($input,array$args,Parser$parser,PPFrame$frame,$tagName){// Now we can retrieve the tag name and perform custom actions for that tagswitch($tagName){//...}}
Extension:WikiEditor provides an editing toolbar, allowing users to add tags into their editor by simply clicking a button.If you want a toolbar button for your new tag, create a file named something liketoolbar-button.js
in your extension'sresources
folder. The file should look like this:
varcustomizeToolbar=function(){$('#wpTextbox1').wikiEditor('addToToolbar',{section:'main',group:'format',tools:{"ExtensionName":{// replace with the name of your extensionlabel:'TagName',// replace with the label that should appear when hoving the buttontype:'button',icon:"extensions/ExtensionName/images/button-image.svg",// path to the image that should go on the buttonaction:{type:'encapsulate',options:{pre:"<tagName>",// tags that get inserted when the button is clickedpost:"</tagName>"}}}}});};/* Check if view is in edit mode and that the required modules are available. Then, customize the toolbar … */if(['edit','submit'].indexOf(mw.config.get('wgAction'))!==-1){mw.loader.using('user.options').then(function(){// This can be the string "0" if the user disabled the preference ([[phab:T54542#555387]])if(mw.user.options.get('usebetatoolbar')==1){$.when(mw.loader.using('ext.wikiEditor'),$.ready).then(customizeToolbar);}});}
Further details about customizing this file can be found here.Once you've created the file, you need to register it withResourceLoader so it will be delivered to visitors; this is done by editing yourextension.json
:
"Hooks":{"BeforePageDisplay":"ExtensionName::onBeforePageDisplay"}"ResourceModules":{"ext.ExtensionName":{"scripts":["toolbarButton.js"]}}
Then, in your PHP file:
publicstaticfunctiononBeforePageDisplay(OutputPage$out){$out->addModules(['ext.ExtensionName']);}