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.i18n.php -> Translations
~~/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 a solution to the proposed exercise challenge at the end of the session.
UserAvatar.php
<!--?php
if ( !defined( 'MEDIAWIKI' ) ) {
die( 'This file is a MediaWiki extension, it is not a valid entry point' );
}
/** REGISTRATION */
$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.
$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
$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
$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
$wgHooks['ParserFirstCallInit'][] = 'UserAvatarSetupTagExtension';
UserAvatar.php
$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
$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";
UserAvatar.i18n.php
<?php
/**
* Internationalisation file for extension UserAvatar.
*
* @file
* @ingroup Extensions
*/
$messages = array();
$messages['en'] = array(
'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!'
);
$messages['ca'] = array(
'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!'
);
UserAvatar.php
$wgExtensionCredits['parserhook'][] = array(
'path' => __FILE__,
'name' => 'UserAvatar',
'version' => '0.1',
'url' => 'https://www.mediawiki.org/wiki/Extension:UserAvatar',
'author' => array( 'Toniher' ),
'descriptionmsg' => 'useravatar_desc'
);
Check Special:Version now$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.
$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
$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
$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
$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
$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
Add a user avatar icon next to every edit in History page.
Tips:
var_dump()
, dirty but it works