Introduction to

MediaWiki extensions

creation






Toni Hermoso Pulido
aka Toniher



MediaWiki installation




Instructions:

Linux

Mac

Windows

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.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.


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 a solution to the proposed exercise challenge at the end of the session.

Starting your extension

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

Coding 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.

$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

Semantic MediaWiki hooks



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


$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>";

HTML Class

Avoiding XSS holes

More details at:

https://www.mediawiki.org/wiki/Security_for_developers

MediaWiki Classes


List of MediaWiki Classes


Very important reference


For instance for User class:

https://www.mediawiki.org/wiki/User.php

Files are located in includes directory

Example 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

$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


$wgHooks['ParserFirstCallInit'][] = 'UserAvatarSetupTagExtension';

Definition of the parser function

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;
}   

Magic words

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' )
);

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

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!'
);

More localization

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
with different $wgLanguageCode (e.g. en-US, ca)

More localization

Messages API


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 bottom

http://www.mediawiki.org/wiki/Manual:Hooks/SkinAfterContent

UserAvatar.php
# 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;
    }

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
$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


UserAvatar.php
$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
$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


$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?


Create an API


Access MediaWiki database


Store values in a custom database table

http://www.mediawiki.org/wiki/Manual:Hooks/LoadExtensionSchemaUpdates
triggered by php maintenance/update.php

And much and much more!

Final exercise


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