Instructions:
mw-config
directory after installation$wgSitename = "My Wiki";
require_once( "$IP/extensions/UserAvatar/UserAvatar.php" );
error_reporting( -1 );
ini_set( 'display_errors', 1 );
$wgShowSQLErrors = true;
$wgDebugDumpSql = true;
$wgShowExceptionDetails = true;
$wgDebugToolbar = true;
More details: https://www.mediawiki.org/wiki/Debugging
/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.
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)
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 page
https://www.mediawiki.org/wiki/Manual:Coding_conventions
function wfTimestampOrNull( $outputtype = TS_UNIX, $ts = null ) {
if ( is_null( $ts ) ) {
return null;
} else {
return wfTimestamp( $outputtype, $ts );
}
}
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
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';
// 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;
}
/**
* @param $input string
* @param $args array
* @param $parser Parser
* @param $frame Frame
* @return string
*/
function printTag( $input, $args, $parser, $frame ) {
return "My userprofile function!";
}
<userprofile />
<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!" );
}
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>
$data = "<div class='useravatar-output'>" .
Html::element(
'img',
array(
'src' => $input,
'alt' => 'Image test',
'width' => $width
)
) .
"</div>";
Avoiding XSS holes
More details at:
Very important reference
For instance for User class:
https://www.mediawiki.org/wiki/User.php
Files are located inincludes
directory
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 );
var_dump( $user )
to discover the object.
Does it exist in the wiki?
$user->getId()
Explore other functions, both for creating:
$user = User:newFromId( 1 );
and for getting
$user->getEffectiveGroups();
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
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:$file->getUser();
$file->getImageSize();
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!" );
}
}
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;
}
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:self::myMethod(@args)
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';
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;
}
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' )
);
/**
* @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!" );
}
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";
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 jsonlint
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';
$wgLanguageCode
(e.g. en-US, ca)
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.
# 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;
}
We get a Title instance of the current page
$title = $skin->getTitle();
Skin class doesn't have getTitle() method, 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;
}
We get a Title instance of the current page
$title = $out->getTitle();
OutPutPage class doesn't have getTitle() method, $context = new RequestContext();
$title = $context->getTitle();
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'
);
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;
}
css/ext.UserAvatar.css
.useravatar-profile img { width: 80px; }
.useravatar-lastedit img { width: 80px; }
Play with other styles…$(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…http://www.mediawiki.org/wiki/Manual:Ajax
https://www.mediawiki.org/wiki/Manual:$wgAjaxExportList
UserAvatar.php
$GLOBALS['wgAjaxExportList'][] = 'UserAvatar::getUserInfo';
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 '';
}
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);
});
});
Store values in a custom database table
http://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdatesphp maintenance/update.php
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';
}
}
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'
);
}
}
UserAvatar.php
/** Loading of classes **/
$GLOBALS['wgAutoloadClasses']['ApiUserAvatar'] = dirname( __FILE__ ). '/UserAvatar.api.php';
// API module
$GLOBALS['wgAPIModules']['useravatar'] = 'ApiUserAvatar';
Check in /w/api.php
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"]
}
}
Example: 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
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
Starting point links:
https://semantic-mediawiki.org/wiki/Programmer%27s_guide_to_SMW
https://semantic-mediawiki.org/wiki/User:Yury_Katkov/programming_examples
Example:
Add a user avatar icon next to every edit in History page.
Tips:
var_dump()
, dirty but it works