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

$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

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


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

    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

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

    More 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

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


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


    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!

    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

    http://code.tutsplus.com/tutorials/setting-up-a-local-mirror-for-composer-packages-with-satis--net-36726

    https://getcomposer.org/doc/articles/handling-private-packages-with-satis.md

    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

    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