Introduction to
MediaWiki extensions
creation
MediaWiki installation
Instructions:
LocalSettings.php
- Main configuration file for a wiki installation
- MediaWiki installation assistant generates one for you. No overwriting happens
- You might want to remove
mw-config
directory after installation
-
Global parameters used by wiki and extensions are defined here
$wgSitename = "My Wiki";
- Extensions are enabled here as well
require_once( "$IP/extensions/UserAvatar/UserAvatar.php" );
Enabling debugging
error_reporting( -1 );
ini_set( 'display_errors', 1 );
$wgShowSQLErrors = true;
$wgDebugDumpSql = true;
$wgShowExceptionDetails = true;
$wgDebugToolbar = true;
More details: https://www.mediawiki.org/wiki/Debugging
If a page would not look updated, try purging it:
http://www.mediawiki.org/wiki/Manual:Purge
Extension file structure
/w/ -> Root of your wiki -> Renamed from mediawiki-1.xx.y
/w/extensions/ -> Extensions directory
/w/extensions/UserAvatar/ -> Your extension
~~/UserAvatar.php -> Main file
~~/UserAvatar.classes.php -> Extension classes
~~/UserAvatar.api.php -> API handling point
~~/UserAvatar.i18n.php -> Translations handling point
~~/UserAvatar.i18n.magic.php -> Translations of parser functions, magic words, etc.
There are alternatives and, as more complex is the extension the more directories and files are normally involved.
Used code
All the code in the following slides can be found at:
https://github.com/toniher/UserAvatar
Files with .extra in their file names are the same as the ones without but including extra content (to be presented if enough time is available)
Starting your extension
UserAvatar.php
<!--?php
if ( !defined( 'MEDIAWIKI' ) ) {
die( 'This file is a MediaWiki extension, it is not a valid entry point' );
}
/** REGISTRATION */
$GLOBALS['wgExtensionCredits']['parserhook'][] = array(
'path' =--> __FILE__,
'name' => 'UserAvatar',
'version' => '0.1',
'url' => 'https://www.mediawiki.org/wiki/Extension:UserAvatar',
'author' => array( 'Toniher' ),
'descriptionmsg' => 'Extension for rendering user avatars'
);
Check in Special:Version pageCoding style
https://www.mediawiki.org/wiki/Manual:Coding_conventions
- UTF-8 without Byte Order Mark.
- New line at the end of the file.
- Use tabs for indentation.
- Braces, conditional, parentheses, and indentation.
function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
if ( is_null( $ts ) ) {
return null;
} else {
return wfTimestamp( $outputtype, $ts );
}
}
-
Explore your editor. Example: gedit.
Hooks
https://www.mediawiki.org/wiki/Hooks
Hooks are the entry points for your extension.
$GLOBALS['wgHooks']['SkinAfterContent'][] = 'UserAvatar::onSkinAfterContent';
Functions (event handlers) act on certain predefined events.
In Skin.php class — this piece of code allows external functions to act
wfRunHooks( 'SkinAfterContent', array( &$data, $this );
You can also create your own for your extension!
MediaWiki extensions hook registry
Tag extensions
https://www.mediawiki.org/wiki/Manual:Tag_extensions
Allows replacement of custom tags (e.g. <userprofile>
for a more or less complex HTML result output.
Hook on Parser (responsible of converting wikitext to HTML)
http://www.mediawiki.org/wiki/Manual:Hooks/ParserFirstCallInit
$GLOBALS['wgHooks']['ParserFirstCallInit'][] = 'UserAvatarSetupTagExtension';
Definition of the tag
// Hook our callback function into the parser
function UserAvatarSetupTagExtension( $parser ) {
// When the parser sees the <userprofile> tag, it executes
// the printTag function
$parser->setHook( 'userprofile', 'printTag' );
// Always return true from this function. The return value does not denote
// success or otherwise have meaning - it just must always be true.
return true;
}
Defining processing function
/**
* @param $input string
* @param $args array
* @param $parser Parser
* @param $frame Frame
* @return string
*/
function printTag( $input, $args, $parser, $frame ) {
return "My userprofile function!";
}
- Notice comments on function params and return
- Let's try live!
<userprofile />
Handling parameters in function
<userprofile width=100>http://test.de/image.jog</userprofile>
/**
* @param $input string
* @param $args array
* @param $parser Parser
* @param $frame Frame
* @return string
*/
function printTag( $input, $args, $parser, $frame ) {
$width = "50px";
if ( isset( $args['width'] ) && is_numeric( $args['width'] ) ) {
$width = $args['width']."px";
}
if ( !empty( $input ) ) {
$data = '<div class="useravatar-output"><img src="'.$input.'" alt="Image Test" width='.$width.'></div>';
return $data;
} else {
return "No file associated to the user!";
}
return ( "No existing user associated!" );
}
Parsing wikitext from parameters
In some cases, inputs or parameters can be complex wikitext (such as template parameters or parameters themselves).
$args['width'] = $parser->recursiveTagParse( $args['width'], $frame );
Please try:
<userprofile width={{Width}}>myimage.png</userprofile>
Safe HTML
$data = "<div class='useravatar-output'>" .
Html::element(
'img',
array(
'src' => $input,
'alt' => 'Image test',
'width' => $width
)
) .
"</div>";
Avoiding XSS holes
More details at:
MediaWiki Classes
Very important reference
For instance for User class:
https://www.mediawiki.org/wiki/User.php
Files are located inincludes
directoryExample of User class
Let's create User object from a username:
You can create different users in your wiki.
Recommended extension: UserAdmin
$username = $parser->recursiveTagParse( trim( $input ) );
$user = User::newFromName( $username );
You can use
var_dump( $user )
to discover the object.
Example of User Class
Does it exist in the wiki?
$user->getId()
- 0 - Anonymous -> No user -> it does not exist
- 1 - Normally WikiSysop
Explore other functions, both for creating:
$user = User:newFromId( 1 );
and for getting
$user->getEffectiveGroups();
Example of File class
Let's try to find if there is any file with the filename pattern:
$filename = User-username.jpg
$file = wfFindFile( $filename );
wfFindFile
is a global function. Actually a shortcut.
Details at:
https://github.com/wikimedia/mediawiki-core/blob/master/includes/GlobalFunctions.php
Example of File Class
if ( $file && $file->exists() ) {
$data = "<div class='useravatar-output'>" .
Html::element(
'img',
array(
'src' => $file->getUrl(),
'alt' => $user->getName(),
'width' => $width
)
) .
"</div>";
}
Explore other possibilities:User uploaded:
$file->getUser();
Size of the file:
$file->getImageSize();
Let's put everything in a Class
UserAvatar.php
$GLOBALS['wgAutoloadClasses']['UserAvatar'] = __DIR__ . '/UserAvatar.classes.php';
UserAvatar.class.php
class UserAvatar {
/**
* @param $input string
* @param $args array
* @param $parser Parser
* @param $frame Frame
* @return string
*/
public static function printTag( $input, $args, $parser, $frame ) {
$width = "50px";
if ( isset( $args['width'] ) ) {
// Let's allow parsing -> Templates, etc.
$args['width'] = $parser->recursiveTagParse( $args['width'], $frame );
if ( is_numeric( $args['width'] ) ) {
$width = $args['width']."px";
}
}
if ( !empty( $input ) ) {
$username = $parser->recursiveTagParse( trim( $input ) );
$user = User::newFromName( $username );
// If larger than 0, user exists
if ( $user && $user->getId() > 0 ) {
$file = self::getFilefromUser( $user );
if ( $file && $file->exists() ) {
$data = "<div class='useravatar-output'>" .
Html::element(
'img',
array(
'src' => $file->getUrl(),
'alt' => $user->getName(),
'width' => $width
)
) .
"</div>";
return $data;
} else {
return "No file associated to the user ".$user->getName();
}
}
}
return ( "No existing user ".$input." associated!" );
}
}
Reuse the code in the Class
UserAvatar.class.php
/**
* @param $user User
* @return File
*/
private static function getFilefromUser( $user ) {
// Let's retrieve username from user object
$username = $user->getName();
if ( empty( $username ) ) {
return "";
}
// We assume all files are User-username.jpg
$filename = "User-".$username.".jpg";
// Returns a file object
$file = wfFindFile( $filename );
return $file;
}
Calling a static function in a Class
UserAvatar.php
// Hook our callback function into the parser
function UserAvatarSetupTagExtension( $parser ) {
// When the parser sees the tag, it executes
// the printTag function (see below)
$parser->setHook( 'userprofile', 'UserAvatar::printTag' );
// Always return true from this function. The return value does not denote
// success or otherwise have meaning - it just must always be true.
return true;
}
Note:within the class, you can also use:
self::myMethod(@args)
Parser functions
http://www.mediawiki.org/wiki/Manual:Parser_functions
Similar to tags extensions. Expected to deal more with wikitext.{{#userprofile: param1 | param2 }}
Creating a parser function is slightly more complicated than creating a new tag because the function name must be a magic word, a keyword that supports aliases and localization.
Hook on Parser (responsible of converting wikitext to HTML)
http://www.mediawiki.org/wiki/Manual:Hooks/ParserFirstCallInit
$GLOBALS['wgHooks']['ParserFirstCallInit'][] = 'UserAvatarSetupParserFunction';
Definition of the parser function
UserAvatar.php
$GLOBALS['wgHooks']['ParserFirstCallInit'][] = 'UserAvatarSetupParserFunction';
// Hook our callback function into the parser
function UserAvatarSetupParserFunction( $parser ) {
// When the parser sees the {{#userprofile:}} function, it executes
// the printFunction function (see below)
$parser->setFunctionHook( 'userprofile', 'UserAvatar::printFunction', SFH_OBJECT_ARGS );
// Always return true from this function. The return value does not denote
// success or otherwise have meaning - it just must always be true.
return true;
}
Magic words
UserAvatar.php
$GLOBALS['wgExtensionMessagesFiles']['UserAvatarMagic'] = __DIR__ . '/UserAvatar.i18n.magic.php';
UserAvatar.i18n.magic.php
<?php
/**
* Internationalization file.
*/
$magicWords = array();
$magicWords['en'] = array(
'userprofile' => array( 0, 'userprofile' )
);
$magicWords['ca'] = array(
'userprofile' => array( 0, 'perfilusuari', 'imatgeperfil' )
);
Processing function
UserAvatar.classes.php
/**
* @param $parser Parser
* @param $frame PPFrame
* @param $args array
* @return string
*/
public static function printFunction( $parser, $frame, $args ) {
if ( isset( $args['0']) && !empty( $args[0] ) ) {
$width = "50px";
if ( isset( $args['1'] ) && is_numeric( $args[1] ) ) {
$width = $args[1]."px";
}
$username = trim( $args[0] );
$user = User::newFromName( $username );
if ( $user && $user->getId() > 0 ) {
$file = self::getFilefromUser( $user );
if ( $file && $file->exists() ) {
$data = "<div class='useravatar-output'>[[File:".$file->getName()."|".$width."px|link=User:".$user->getName()."]]</div>";
return $data;
} else {
return "User has no avatar file!";
}
}
}
return ( "No existing user associated!" );
}
Testing magic words
Change LocalSettings.php
# Site language code, should be one of the list in ./languages/Names.php
$wgLanguageCode = "ca";
Try
{{#userprofile:Username}}
and
{{#perfilusuari:Username}}
and go back to: $wgLanguageCode = "en";
More localization
https://www.mediawiki.org/wiki/Localisation_file_format
i18n/en.json{
"@metadata": { "authors": [ "Toniher" ] },
"useravatar_desc": "Extension for rendering user avatars",
"useravatar-lastedition": "Last Edition by:",
"useravatar-noexistinguser-plain": "No existing user associated",
"useravatar-noexistinguser": "No existing user $1 associated",
"useravatar-nofiletouser": "User has no avatar file!"
}
i18n/ca.json
{
"@metadata": { "authors": [ "Toniher" ] },
"useravatar_desc": "Extensió per a dibuixar avatars d'usuari",
"useravatar-lastedition": "Darrera edició per:",
"useravatar-noexistinguser-plain": "No hi ha cap usuari associat",
"useravatar-noexistinguser": "No hi ha cap usuari $1 associat",
"useravatar-nofiletouser": "L'usuari no té fitxer d'avatar!"
}
Tip: Check JSON with jsonlintMore localization
UserAvatar.php
$GLOBALS['wgExtensionCredits']['parserhook'][] = array(
'path' => __FILE__,
'name' => 'UserAvatar',
'version' => '0.1',
'url' => 'https://www.mediawiki.org/wiki/Extension:UserAvatar',
'author' => array( 'Toniher' ),
'descriptionmsg' => 'useravatar_desc'
);
/** STRINGS AND THEIR TRANSLATIONS **/
$GLOBALS['wgMessagesDirs']['UserAvatar'] = __DIR__ . '/i18n';
/** BACK-COMPATIBILITY FILE BELOW **/
$GLOBALS['wgExtensionMessagesFiles']['UserAvatar'] = __DIR__ . '/UserAvatar.i18n.php';
Check Special:Version now
with different
$wgLanguageCode
(e.g. en-US, ca)
More localization
Let's replace in UserAvatar.classes.php
No parameters
#BEFORE: return ( "No existing user associated!" );
return wfMessage( "useravatar-noexistinguser-plain" )->text();
Parameters
#BEFORE: return "No file associated to the user ".$user->getName();
return wfMessage( "useravatar-nofiletouser", $user->getName() )->parse();
More complexity possible: plurals, genders, etc.
More hooks
Let's print avatar of last user who modified a page at its bottomhttp://www.mediawiki.org/wiki/Manual:Hooks/SkinAfterContent
UserAvatar.php
# We put avatar at the end of articles created by one person.
$GLOBALS['wgHooks']['SkinAfterContent'][] = 'UserAvatar::onSkinAfterContent';
UserAvatar.classes.php
/**
* @param $data string
* @param $skin Skin
* @return bool
*/
public static function onSkinAfterContent( &$data, $skin ) {
...
return true;
}
Getting context
We get a Title instance of the current page
$title = $skin->getTitle();
Skin class doesn't have getTitle() method, but since Skin class extends ContextSource class,
which DOES have this method,
you can use it.
Let's review onSkinAfterContent ...
More hooks
Let's print avatar in user page
http://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput
UserAvatar.php
# We put avatar only on User Page
$GLOBALS['wgHooks']['OutputPageParserOutput'][] = 'UserAvatar::onOutputPageParserOutput';
UserAvatar.classes.php
/**
* @param $out OutputPage
* @param $parserOutput ParserOutput
* @return bool
*/
public static function onOutputPageParserOutput( $out, $parserOutput ) {
...
return true;
}
Getting context - other ways
We get a Title instance of the current page
$title = $out->getTitle();
OutPutPage class doesn't have getTitle() method, but it extends ContextSource class,
which DOES have this method,
you can use it.
Alternative: http://www.mediawiki.org/wiki/Request_Context
$context = new RequestContext();
$title = $context->getTitle();
Let's review onOutputPageParserOutput...
ResourceLoader
Mechanism for delivering JavaScript, CSS, etc. in MediaWiki
http://www.mediawiki.org/wiki/ResourceLoader
http://www.mediawiki.org/wiki/ResourceLoader/Developing_with_ResourceLoader
$GLOBALS['wgResourceModules']['ext.UserAvatar'] = array(
'localBasePath' => __DIR__,
'scripts' => array( 'js/ext.UserAvatar.js' ),
'styles' => array( 'css/ext.UserAvatar.css' ),
'remoteExtPath' => 'UserAvatar'
);
Load CSS and JS in pages
http://www.mediawiki.org/wiki/Manual:Hooks/OutputPageBeforeHTML
UserAvatar.php
# We add this for loading CSS and JSS in every page by default
$GLOBALS['wgHooks']['OutputPageBeforeHTML'][] = 'UserAvatar::onOutputPageBeforeHTML';
UserAvatar.classes.php
/**
* @param $out OutputPage
* @param $text string
* @return $out OutputPage
*/
public static function onOutputPageBeforeHTML( &$out, &$text ) {
// We add Modules
$out->addModules( 'ext.UserAvatar' );
return $out;
}
Actual CSS
css/ext.UserAvatar.css
.useravatar-profile img { width: 80px; }
.useravatar-lastedit img { width: 80px; }
Play with other styles…js/ext.UserAvatar.js
JQuery used by default.
$(document).ready(function() {
// Way to get jQuery version
console.log($().jquery);
// L10n possible here as well!
console.log("UserAvatar extension is loaded!");
});
Check how to localize messages…Ajax Functions
http://www.mediawiki.org/wiki/Manual:Ajax
https://www.mediawiki.org/wiki/Manual:$wgAjaxExportList
UserAvatar.php
$GLOBALS['wgAjaxExportList'][] = 'UserAvatar::getUserInfo';
Ajax Functions
In server part - UserAvatar.classes.php
Method must be public
/**
* @param $username string
* @return string
**/
public static function getUserInfo( $username ) {
// Create user
$user = User::newFromName( $username );
if ( $user && $user->getId() > 0 ) {
$timestamp = $user->getRegistration();
// We could format timestamp as well
return $timestamp;
}
return '';
}
Ajax Functions
In client part - js/ext.UserAvatar.js
// On click on Avatar profile
$(document).on("click", ".useravatar-profile > img", function() {
console.log("Clicked!");
var username = $(this).attr('data-username');
$.get( mw.util.wikiScript(), {
format: 'json',
action: 'ajax',
rs: 'UserAvatar::getUserInfo',
rsargs: [username] // becomes &rsargs[]=arg1&rsargs[]=arg2...
}, function(data) {
alert(data);
});
});
What else can you do with your extension?
Store values in a custom database table
http://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdatestriggered by
php maintenance/update.php
And much and much more!
API
Definition at - UserAvatar.api.php
class ApiUserAvatar extends ApiBase {
public function execute() {
$params = $this->extractRequestParams();
…
return true;
}
public function getDescription() {
return array(
'API for checking whether a user has an associated Avatar'
);
}
public function getVersion() {
return __CLASS__ . ': 1.1';
}
}
API (parameters)
Definition at - UserAvatar.api.php
class ApiUserAvatar extends ApiBase {
public function getAllowedParams() {
return array(
'username' => array(
ApiBase::PARAM_TYPE => 'string',
ApiBase::PARAM_REQUIRED => true
));
}
public function getParamDescription() {
return array(
'username' => 'Username to be queried'
);
}
}
API (initialization)
UserAvatar.php
/** Loading of classes **/
$GLOBALS['wgAutoloadClasses']['ApiUserAvatar'] = dirname( __FILE__ ). '/UserAvatar.api.php';
// API module
$GLOBALS['wgAPIModules']['useravatar'] = 'ApiUserAvatar';
Check in /w/api.php
useravatar becomes a new function in the API
Composer
https://www.mediawiki.org/wiki/Composer
New way to mantain extensions up-to-date
composer.json
{
"name": "mediawiki/user-avatar",
"type": "mediawiki-extension",
"description": "Demo extension for handling user avatars",
"keywords": [ "MediaWiki","User" ],
"homepage": "https://www.mediawiki.org/wiki/User:Toniher",
"license": "GPL-3.0+",
"authors": [{
"name": "Toni Hermoso Pulido",
"role": "Developer"
}],
"require": {
"php": ">=5.3.0",
"composer/installers": "1.*,>=1.0.1"
},
"autoload": {
"files": ["UserAvatar.php"]
}
}
Composer local repository
Satis
https://getcomposer.org/doc/articles/handling-private-packages-with-satis.mdExample: satis.json
{
"name": "Similis repo",
"homepage": "http://localhost:4680",
"repositories": [
{ "type": "vcs", "url": "https://github.com/toniher/UserAvatar" }
],
"require-all": true
}
Build repository site:
php satis/bin/satis build satis.json web
Execute repository site:
php -S localhost:4680 -t web
Composer local repository
Satis
add in /w/composer.json
"repositories": [
{
"type": "composer",
"url": "http://localhost:4680"
}
],
https://packagist.org/ is enabled by default
Disable extension if enabled, move extension directory
and then install extension:
composer.phar require mediawiki/user-avatar dev-master
Semantic MediaWiki
programming
Starting point links:
https://semantic-mediawiki.org/wiki/Programmer%27s_guide_to_SMW
https://semantic-mediawiki.org/wiki/User:Yury_Katkov/programming_examples
Exercises
Example:
Add a user avatar icon next to every edit in History page.
Tips:
- Look for a suitable Hook
- Check for already existing extensions in mediawiki.org
- Inspect parameters using
var_dump()
, dirty but it works
- Reuse code we already have
MediaWiki Extension Creation
By Similis.cc
MediaWiki Extension Creation
Slides about MediaWiki Extension Creation
- 10,322