MediaWiki:Gadget-CatRename.js

Une page de Wikipédia, l'encyclopédie libre.
Note : après avoir enregistré la page, vous devrez forcer le rechargement complet du cache de votre navigateur pour voir les changements.

Mozilla / Firefox / Konqueror / Safari : maintenez la touche Majuscule (Shift) en cliquant sur le bouton Actualiser (Reload) ou pressez Maj-Ctrl-R (Cmd-R sur Apple Mac) ;

Chrome / Internet Explorer / Opera : maintenez la touche Ctrl en cliquant sur le bouton Actualiser ou pressez Ctrl-F5.
/*
* CatRename
*
* Ajoute un onglet permettant de renommer une catégorie, en déplaçant les pages
* incluses dans celle-ci. Permet de faire faire l'action à un bot en un clic.
*
* {{Projet:JavaScript/Script|CatRename}}
*/
/* <nowiki> */

/* globals mw, OO, $ */

if ( mw.config.get( 'wgNamespaceNumber' ) === 14 ) {
	mw.loader.using( 'mediawiki.util', function () {
		'use strict';

		// Site-related parameters
		const TAG = 'RenommageCategorie';
		const DAILY_LIMIT = 250;
		const RBOT_PAGE = 'Wikipédia:Bot/Requêtes/Catégories';
		const DR_TEMPLATE = '{{Suppression Immédiate|raison=Catégorie récemment renommée en [[:Catégorie:$2]] ($3)|utilisateur=$4}}\n\n';
		const RBOT_TEMPLATE = '\n{{Déplacement catégorie|ancienne=$1|nouvelle=$2|raison=$3|demandeur=$4}}';

		// Literal non-breaking space, for situations where HTML entities can't be used
		const NBSP = '\xA0';

		// Messages
		const messages = {
			'fr': {
				'catrename-title': 'Renommer une catégorie',
				'catrename-portlet-title': 'CatRename',

				'catrename-action-rename': 'Renommer',
				'catrename-action-cancel': 'Annuler',
				'catrename-action-rbot': '… ou faire faire la tâche par un bot',

				'catrename-checkbox-movetalk': 'Renommer aussi la page de discussion associée',
				'catrename-checkbox-leave-redirect': 'Laisser une redirection vers le nouveau titre',
				'catrename-checkbox-post-dr': 'Déposer une demande de suppression de l\'ancienne catégorie',
				'catrename-checkbox-watch': 'Suivre les catégories originale et nouvelle',
				'catrename-checkbox-watch-members': 'Suivre les pages modifiées',

				'catrename-field-title': 'Nouveau titre' + NBSP + ':',
				'catrename-field-reason': 'Motif' + NBSP + ':',

				'catrename-summary': 'Renommage de la catégorie [[Catégorie:$1]] en [[Catégorie:$2]]&nbsp;: $3',
				'catrename-dr-summary': 'Demande de suppression après renommage',
				'catrename-rbot-summary': 'RBOT&nbsp;: Demande de renommage de catégorie',

				'catrename-status-checkcategory': 'Vérification de la catégorie cible',
				'catrename-status-getmembers': 'Récupération des pages membres de la catégorie',
				'catrename-status-waitinglock': 'En attente de la fin de renommage dans d\'autres onglets',
				'catrename-status-checklimits': 'Vérification de la limite journalière',
				'catrename-status-editmembers': 'Modification de la page $1 sur $2',
				'catrename-status-renamecategory': 'Renommage de la catégorie',
				'catrename-status-postdr': 'Dépôt de la demande de suppression',
				'catrename-status-postrbot': 'Dépôt de la requête aux bots',

				'catrename-error-canceled': 'Le processus de renommage a été annulé.',
				'catrename-error-same': 'Le nouveau titre est identique au titre actuel.',
				'catrename-error-invalidtitle': 'Le titre de la catégorie demandée est non valide, vide, ou mal formé.',
				'catrename-error-noreason': 'Veuillez indiquer un motif pour ce renommage.',
				'catrename-error-protected': 'Cette catégorie est protégée, vous n\'êtes pas autorisé à la renommer.',
				'catrename-error-categoryexist': 'Il existe déjà une catégorie avec ce nom…',
				'catrename-error-limitreached': 'Le renommage de cette catégorie vous ferait faire plus de $1 modifications avec ce script en moins de 24h. Vous pouvez cependant faire une requête aux bots via le bouton en bas à gauche.',
				'catrename-error-categorypresent': 'La page contient déjà la nouvelle catégorie.',
				'catrename-error-notfound': 'La catégorie n\'a pas été trouvée dans le code de la page, peut-être est-elle incluse via un modèle' + NBSP + '?',
				'catrename-error-pageprotected': 'La page est protégée en écriture.',
				'catrename-error-articleexists': 'Impossible de déplacer la catégorie, la page de destination «' + NBSP + '$1' + NBSP + '» existe déjà.',
			}
		};
		mw.messages.set( messages.fr );
		var lang = mw.config.get( 'wgUserLanguage' );
		if ( lang !== 'fr' && lang in messages ) {
			mw.messages.set( messages[ lang ] );
		}


		var isBootstrapped = false;
		var instanceWindowManager;
		var instanceCatRename;

		$( function ( $ ) {
			var portlet = mw.util.addPortletLink( 'p-cactions', '#', mw.msg( 'catrename-portlet-title' ) );
			$( portlet ).on( 'click', function ( e ) {
				e.preventDefault();
				mw.loader.using( [ 'oojs-ui', 'mediawiki.storage', 'mediawiki.api' ], function () {
					bootstrapOnce();
					instanceCatRename.open();
				} );
			} );
		} );


		/* Instanciate CatRename and add it to MediaWiki's UI. */

		function bootstrapOnce() {
			if (isBootstrapped) {
				return;
			}

			isBootstrapped = true;

			/**
			 * Main class of the gadget CatRename, which is displayed as a ProcessDialog
			 *
			 * @class
			 * @extends OO.ui.ProcessDialog
			 *
			 * @constructor
			 */
			var CatRename = function () {
				// Initialize config
				var config = { size: 'medium' };

				// Parent constructor
				CatRename.parent.call( this, config );

				// Properties
				this.api = new mw.Api( { timeout: 7000 } );
				this.oldTitle;
				this.newTitle;
				this.oldPageName;
				this.newPageName;
				this.reason;
				this.deferred;
				this.members;
				this.lockID;
				this.nextTab;
				this.noSpammingDelay;

				// Graphical properties
				this.configContent = new OO.ui.PanelLayout( { padded: true, expanded: false } );
				this.statusContent = new OO.ui.PanelLayout( { padded: true, expanded: false } );
				this.newNameInput;
				this.reasonInput;
				this.optionCheckboxes;
				this.layout;
				this.$body;
				this.statusIndicator;
				this.pagesInError;
			};



			/* Setup */

			OO.inheritClass( CatRename, OO.ui.ProcessDialog );



			/* Static Properties */

			CatRename.static.name = 'catrename';
			CatRename.static.title = mw.msg( 'catrename-title' );
			CatRename.static.actions = [
				{ action: 'rename', label: mw.msg( 'catrename-action-rename' ), flags: [ 'primary', 'progressive' ] },
				{ action: 'cancel', label: mw.msg( 'catrename-action-cancel' ), flags: [ 'safe', 'back' ] },
				{ action: 'rbot', label: mw.msg( 'catrename-action-rbot' ), flags: 'other' }
			];



			/* ProcessDialog-related Methods */

			/**
			 * Build the interface displayed inside the ProcessDialog box.
			 */
			CatRename.prototype.initialize = function () {
				CatRename.parent.prototype.initialize.apply( this, arguments );

				this.newNameInput = new OO.ui.TextInputWidget( { value: mw.config.get( 'wgTitle' ) } );
				this.reasonInput = new OO.ui.TextInputWidget( {
					maxLength: 500,
					name: 'wpSummary'
				} );

				this.optionCheckboxes = new OO.ui.CheckboxMultiselectInputWidget( {
					value: [ 'movetalk', 'post-dr' ],
					options: [
						{ data: 'movetalk', label: mw.msg( 'catrename-checkbox-movetalk' ) },
						( this.userInGroup( 'sysop' ) || this.userInGroup( 'bot' ) ?
							{ data: 'leave-redirect', label: mw.msg( 'catrename-checkbox-leave-redirect' ) } :
							{ data: 'post-dr', label: mw.msg( 'catrename-checkbox-post-dr' ) }
						),
						{ data: 'watch', label: mw.msg( 'catrename-checkbox-watch' ) },
						{ data: 'watch-members', label: mw.msg( 'catrename-checkbox-watch-members' ) }
					]
				} );

				this.layout = new OO.ui.Widget( {
					content: [
						new OO.ui.FieldLayout(
							this.newNameInput, {
								align: 'top',
								label: mw.msg( 'catrename-field-title' ),
							}
						),
						new OO.ui.FieldLayout(
							this.reasonInput, {
								align: 'top',
								label: mw.msg( 'catrename-field-reason' ),
							}
						),
						new OO.ui.FieldLayout(
							this.optionCheckboxes, {}
						)
					],
				} );

				this.configContent.$element.append( this.layout.$element );

				this.$body.append( this.configContent.$element );

				this.statusIndicator = $( '<h3>' )
					.css( 'text-align', 'center' )
					.css( 'margin-top', '1em' )
					.css( 'margin-bottom', '2em' );
				this.pagesInError = $( '<ul>' );
				this.statusContent.$element.append( this.statusIndicator ).append( this.pagesInError );

				this.setSize( this.size );
				this.updateSize();
			};

			/**
			 * Get a process for taking action.
			 *
			 * This method is called within the ProcessDialog when the user clicks
			 * on an action button (the one defined in CatRename.static.actions).
			 * Here is defined in which order each method of the category moving
			 * process is called.
			 * @param {string} action Name of the action button clicked.
			 * @return {OO.ui.Process} Action process.
			 */
			CatRename.prototype.getActionProcess = function ( action ) {
				var process = new OO.ui.Process(),
					options = this.optionCheckboxes.getValue();

				if ( action === 'cancel' || action === '' ) { // empty string when closing with Escape key
					return process.next( this.unlockMultitabs, this )
						.next( this.closeDialog, this );
				}
				else if ( action === 'rename' ) {
					process.next( this.prepare, this )
						.next( this.checkCategory, this )
						.next( this.getMembers, this )
						.next( this.lockMultitabs, this )
						.next( this.checkLimits, this )
						.next( this.editMembers, this )
						.next( this.renameCategory, this );
				}
				else if ( action === 'rbot' ) {
					process.next( this.prepare, this )
						.next( this.checkCategory, this )
						.next( this.getMembers, this )
						.next( this.postRBot, this )
						.next( this.renameCategory, this );
				}

				if ( options.indexOf( 'post-dr' ) > -1 ) {
					process.next( this.postDR, this );
				}
				process.next( this.unlockMultitabs, this )
					.next( this.success, this )
					.next( this.closeDialog, this );

				return process;
			};

			/**
			 * Close the window.
			 *
			 * @return {jQuery.Promise} Promise resolved when window is closed
			 */
			CatRename.prototype.closeDialog = function () {
				var dialog = this;

				var lifecycle = dialog.close();

				return lifecycle.closed;
			};

			/**
			 * Get the height of the window body.
			 * Used by the ProcessDialog to set an accurate height to the dialog.
			 *
			 * @return {number} Height in px the dialog should be.
			 */
			CatRename.prototype.getBodyHeight = function () {
				return this.configContent.$element.outerHeight( true );
			};



			/* Process step methods */

			/**
			 * Fetch and validate user's input to make it easily accessible later.
			 *
			 * @return {undefined|OO.ui.Error} Error message for the ProcessDialog
			 *         to display, if any.
			 */
			CatRename.prototype.prepare = function () {
				var dialog = this;

				this.oldTitle = mw.config.get( 'wgTitle' );
				this.newTitle = this.newNameInput.getValue().trim().replace(/^([Cc]atégorie|[Cc]ategory):/, '');
				this.reason = this.reasonInput.getValue().trim();

				if ( mw.config.get( 'wgCaseSensitiveNamespaces' ).indexOf( 14 ) === -1 ) {
					this.newTitle = this.newTitle.charAt( 0 ).toUpperCase() + this.newTitle.slice( 1 );
				}
				if ( this.newTitle === this.oldTitle ) {
					return new OO.ui.Error( mw.msg( 'catrename-error-same' ) );
				}
				if ( mw.Title.makeTitle( 14, this.newTitle ) === null ) {
					return new OO.ui.Error( mw.msg( 'catrename-error-invalidtitle' ) );
				}
				if ( this.reason === '' ) {
					return new OO.ui.Error( mw.msg( 'catrename-error-noreason' ) );
				}

				this.oldPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.oldTitle;
				this.newPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.newTitle;

				// Disable actions button when a process is runing
				this.getActions().get( { actions: 'rename' } )[ 0 ].setDisabled( true );
				this.getActions().get( { actions: 'rbot' } )[ 0 ].setDisabled( true );
				// Except for the cancel button, which behaviour change to cancel the ongoing process
				this.getActions().get( { actions: 'cancel' } )[ 0 ].on( 'click', function () {
					dialog.errorHandler( mw.msg( 'catrename-error-canceled' ) );
				} );

				return;
			};

			/**
			 * Check if it is technically possible to move the category.
			 *
			 * Two main checks are performed:
			 * * Has the user the right to move the category according to the
			 *   protection level?
			 * * Is the target title free?
			 * @return {JQuery.Deferred} Promise telling to continue the process if
			 *         successful or stopping the process if rejected.
			 */
			CatRename.prototype.checkCategory = function () {
				var dialog = this;
				this.deferred = $.Deferred();

				this.showStatus( mw.msg( 'catrename-status-checkcategory' ) );

				var restrictionMove = mw.config.get( 'wgRestrictionMove' );
				for ( var i = 0; i < restrictionMove.length; i++ ) {
					if ( ! this.userInGroup( restrictionMove[ i ] ) ) {
						this.errorHandler( mw.msg( 'catrename-error-protected' ) );
						return this.deferred;
					}
				}

				this.api.get( {
					'action': 'query',
					'format': 'json',
					'formatversion': 2,
					'prop': 'categoryinfo',
					'titles': this.newPageName
				} ).then( function ( data ) {
					if ( data.query.pages[ 0 ].missing !== true ) {
						//TODO: Allow user to move pages without renaming the cat
						dialog.errorHandler( mw.msg( 'catrename-error-categoryexist' ) );
						return;
					}

					dialog.deferred.resolve();
				} ).fail( function ( error ) {
					dialog.errorHandler( error );
				} );

				return this.deferred;
			};

			/**
			 * Get all pages, files and sub-categories in the source category.
			 *
			 * This method populates the attribute 'members'.
			 * @return {JQuery.Deferred} Promise telling to continue the process if
			 *         successful or stopping the process if rejected.
			 */
			CatRename.prototype.getMembers = function () {
				var dialog = this;
				this.deferred = $.Deferred();
				this.members = [];

				this.showStatus( mw.msg( 'catrename-status-getmembers' ) );

				function doGetMembers( paramsContinue ) {
					var params = {
						'action': 'query',
						'format': 'json',
						'list': 'categorymembers',
						'formatversion': '2',
						'cmtitle': mw.config.get( 'wgPageName' ),
						'cmprop': 'title',
						'cmlimit': 'max',
					};
					if ( paramsContinue ) {
						Object.assign( params, paramsContinue );
					}

					dialog.api.get( params ).then( function ( data ) {

						var categoryMembers = data.query.categorymembers;
						for ( var i = 0; i < categoryMembers.length; i++ ) {
							dialog.members.push( categoryMembers[ i ].title );
						}

						if ( data[ 'continue' ] ) {
							doGetMembers( data[ 'continue' ] );
						}
						else {
							dialog.deferred.resolve();
						}
					} ).fail( function ( error ) {
						dialog.errorHandler( error );
					} );

				}
				doGetMembers();

				return this.deferred;
			};

			/**
			 * Lock the process while other instances of CatRename are running.
			 *
			 * This method acts a bit like the POSIX sem_wait.
			 * @return {JQuery.Deferred} Promise telling to continue the process
			 * when it is its turn to execute.
			 */
			CatRename.prototype.lockMultitabs = function () {
				var dialog = this;
				this.deferred = $.Deferred();

				if ( this.userInGroup( 'bot' ) ) {
					return;
				}

				this.lockID = 'catrename-' + this.randomString( 16 );
				this.nextTab = null;

				this.showStatus( mw.msg( 'catrename-status-waitinglock' ) );

				//TODO: check lock timestamp
				if ( mw.storage.get( 'catrename-lock' ) === null ) {
					mw.storage.set( 'catrename-lock', this.lockID );
					this.deferred.resolve();
				}
				else {
					$( window ).on( 'storage.catrename.catrename-waiting', function ( event ) {
						if ( event.originalEvent.key === 'catrename-lock' && event.originalEvent.newValue === dialog.lockID ) {
							$( window ).off( 'storage.catrename-waiting' );
							dialog.deferred.resolve();
						}
					} );
					mw.storage.set( 'catrename-addtab', this.lockID );
				}


				$( window ).on( 'storage.catrename', function ( event ) {
					// if this tab has no successor and a new one appears, add it as our successor
					if ( dialog.nextTab === null && event.originalEvent.key === 'catrename-addtab' && event.originalEvent.newValue !== null ) {
						dialog.nextTab = event.originalEvent.newValue;
						mw.storage.set( dialog.lockID, dialog.nextTab );
					}
					// if our successor decides to leave, remove it and take its successor
					else if ( dialog.nextTab !== null && event.originalEvent.key === 'catrename-removetab' && event.originalEvent.newValue === dialog.nextTab ) {
						dialog.nextTab = mw.storage.get( dialog.nextTab );
						if ( dialog.nextTab !== null ) {
							mw.storage.set( dialog.lockID, dialog.nextTab );
						}
						else {
							mw.storage.remove( dialog.lockID );
						}
					}
				} );

				window.addEventListener( 'unload', function (e) {
					dialog.unlockMultitabs();
				} );

				return this.deferred;
			};

			/**
			 * Check if the daily limit of edits using this script would be reached
			 * if the move is performed.
			 *
			 * In fact, we are not looking realy on a daily basis, but a 24h rolling
			 * period.
			 * @return {JQuery.Deferred} Promise telling to continue the process
			 * when it is its turn to execute.
			 */
			CatRename.prototype.checkLimits = function () {
				var dialog = this;
				this.deferred = $.Deferred();
				var yesterday = new Date();
				yesterday.setDate( yesterday.getDate() - 1 );

				if ( this.userInGroup( 'bot' ) ) {
					this.noSpammingDelay = 0;
					return;
				}

				this.noSpammingDelay = 5000;
				if ( this.members.length > 50 ) {
					this.noSpammingDelay = 20000;
				}
				else if ( this.members.length > 10 ) {
					this.noSpammingDelay = 10000;
				}

				this.showStatus( mw.msg( 'catrename-status-checklimits' ) );

				this.api.get( {
					'action': 'query',
					'format': 'json',
					'list': 'usercontribs',
					'formatversion': '2',
					'uclimit': 'max', // only query DAILY_LIMIT results ?
					'ucend': yesterday.toISOString(),
					'ucuser': mw.config.get( 'wgUserName' ),
					'ucprop': 'timestamp',
					'uctag': TAG
				} ).then( function ( data ) {

					if ( data.query.usercontribs.length + dialog.members.length >= DAILY_LIMIT ) {
						dialog.errorHandler( mw.msg( 'catrename-error-limitreached', DAILY_LIMIT ), false );
					}
					else {
						dialog.deferred.resolve();
					}
				} ).fail( function ( error ) {
					dialog.errorHandler( error );
				} );

				return this.deferred;
			};

			/**
			 * Try to move all the pages inside the 'members' attribute from the old
			 * to the new category name by fetching and editing their wikicode.
			 *
			 * @return {JQuery.Deferred} Promise telling to continue the process
			 * when it is its turn to execute.
			 */
			CatRename.prototype.editMembers = function () {
				var dialog = this,
					totalPages = this.members.length,
					oldCatRegex = this.buildRegex( this.oldTitle ),
					newCatRegex = this.buildRegex( this.newTitle ),
					summary = mw.msg( 'catrename-summary', this.oldTitle, this.newTitle, this.reason ),
					commonPayload = {
						summary: summary,
						minor: true,
						tags: TAG
					};
				this.deferred = $.Deferred();

				if ( this.userInGroup( 'bot' ) ) {
					commonPayload[ 'bot' ] = 1;
				}
				if ( this.optionCheckboxes.getValue().indexOf( 'watch-members' ) > -1 ) {
					commonPayload[ 'watchlist' ] = 'watch';
				}

				function doEdit() {
					var member = dialog.members.pop();
					if ( dialog.deferred.state() !== 'pending' ) {
						return;
					}
					if ( member === undefined ) {
						dialog.deferred.resolve();
						return;
					}

					//TODO: a progress-bar ?
					dialog.showStatus( mw.msg( 'catrename-status-editmembers', totalPages - dialog.members.length, totalPages ) );

					dialog.api.edit( member, function ( revision ) {
						var content = revision.content,
								newCatInPageList = content.match( newCatRegex );

						if ( newCatInPageList !== null ) {
							dialog.logFailedPages( member, mw.msg( 'catrename-error-categorypresent' ) );
						}
						else {
							content = content.replace(
								oldCatRegex,
								'$1[[' + dialog.newPageName + '$6]]'
							);
						}

						return Object.assign( { text: content }, commonPayload );
					} )
						.then( function ( result ) {
						if ( result.nochange === true ) {
							dialog.logFailedPages( member, mw.msg( 'catrename-error-notfound' ) );
						}

						setTimeout( doEdit, dialog.noSpammingDelay );
					} )
						.fail( function ( code, data ) {
						if ( code === 'protectedpage' ) {
							dialog.logFailedPages( member, mw.msg( 'catrename-error-pageprotected' ) );
							doEdit();
						}
						else {
							dialog.errorHandler( code );
						}
					} );
				}
				doEdit();

				return this.deferred;
			};

			/**
			 * Move the category itself.
			 *
			 * @return {JQuery.Deferred} Promise telling to continue the process
			 * when it is its turn to execute.
			 */
			CatRename.prototype.renameCategory = function () {
				var dialog = this;
				this.deferred = $.Deferred();

				this.showStatus( mw.msg( 'catrename-status-renamecategory' ) );

				var payload = {
					'action': 'move',
					'format': 'json',
					'from': mw.config.get( 'wgPageName' ),
					'to': this.newPageName,
					'reason': this.reason,
					'tags': TAG,
					'formatversion': '2'
				};

				var options = this.optionCheckboxes.getValue();
				if ( options.indexOf( 'movetalk' ) > -1 ) {
					payload[ 'movetalk' ] = 1;
				}
				if ( options.indexOf( 'watch' ) > -1 ) {
					payload[ 'watchlist' ] = 'watch';
				}
				if ( this.userInGroup( 'sysop' ) || this.userInGroup( 'bot' ) ) {
					if ( options.indexOf( 'leave-redirect' ) === -1 ) {
						payload[ 'noredirect' ] = 1;
					}
				}

				this.api.postWithToken( 'csrf',	payload ).then( function ( data ) {
					dialog.deferred.resolve();
				} ).fail( function ( error ) {
					if ( error === 'articleexists' ) {
						dialog.errorHandler( mw.msg( 'catrename-error-articleexists', dialog.newPageName ) );
					}
					else {
						dialog.errorHandler( error );
					}
				} );

				return this.deferred;
			};

			/**
			 * Post a deletion request.
			 *
			 * @return {JQuery.Deferred} Promise telling to continue the process
			 * when it is its turn to execute.
			 */
			CatRename.prototype.postDR = function () {
				var dialog = this;
				this.deferred = $.Deferred();

				this.showStatus( mw.msg( 'catrename-status-postdr' ) );

				var content = DR_TEMPLATE
					.replace( /\$1/g, this.oldTitle )
					.replace( /\$2/g, this.newTitle )
					.replace( /\$3/g, this.reason )
					.replace( /\$4/g, mw.config.get( 'wgUserName' ) );

				this.api.postWithToken( 'csrf', {
					'action': 'edit',
					'format': 'json',
					'title': this.oldPageName,
					'summary': mw.msg( 'catrename-dr-summary' ),
					'tags': TAG,
					'nocreate': 1,
					'prependtext': content,
					'formatversion': '2'
				} ).then( function ( data ) {
					dialog.deferred.resolve();
				} ).fail( function ( error ) {
					dialog.errorHandler( error );
				} );

				return this.deferred;
			};

			/**
			 * Post a move request for the bots.
			 *
			 * @return {JQuery.Deferred} Promise telling to continue the process
			 * when it is its turn to execute.
			 */
			CatRename.prototype.postRBot = function () {
				var dialog = this;
				this.deferred = $.Deferred();

				this.showStatus( mw.msg( 'catrename-status-postrbot' ) );

				var content = RBOT_TEMPLATE
					.replace( /\$1/g, this.oldTitle )
					.replace( /\$2/g, this.newTitle )
					.replace( /\$3/g, this.reason )
					.replace( /\$4/g, mw.config.get( 'wgUserName' ) );

				this.api.postWithToken( 'csrf', {
					'action': 'edit',
					'format': 'json',
					'title': RBOT_PAGE,
					'summary': mw.msg( 'catrename-rbot-summary' ),
					'tags': TAG,
					'nocreate': 1,
					'appendtext': content,
					'formatversion': '2'
				} ).then( function ( data ) {
					dialog.deferred.resolve();
				} ).fail( function ( error ) {
					dialog.errorHandler( error );
				} );

				return this.deferred;
			};

			/**
			 * Release the lock to allow other instances of CatRename to execute.
			 *
			 * This method acts a bit like the POSIX sem_post.
			 */
			CatRename.prototype.unlockMultitabs = function () {
				if ( this.lockID !== undefined ) {
					$( window ).off( 'storage.catrename' );

					mw.storage.set( 'catrename-removetab', this.lockID ); //Inform other tabs that we're closing
					mw.storage.remove( this.lockID ); //Clean up our mess from the localStorage

					// wake up the next tab, or reset if there is none
					if ( mw.storage.get( 'catrename-lock' ) === this.lockID ) {
						if ( this.nextTab !== null ) {
							mw.storage.set( 'catrename-lock', this.nextTab );
						}
						else {
							mw.storage.remove( 'catrename-lock' );
						}
					}

					delete this.lockID;
				}
			};

			/**
			 * Method called when all has gone well (yeah !).
			 */
			CatRename.prototype.success = function () {
				var dialog = this;

				setTimeout( function () {
					window.location = mw.util.getUrl( dialog.newPageName );
				}, 1000 );
			};



			/* Helper Methods */

			/**
			 * Get information about the current user's groups.
			 *
			 * @param {string} groupName Name of the group to check.
			 * @return {boolean} Whether the current user is in the given group.
			 */
			CatRename.prototype.userInGroup = function ( groupName ) {
				return ( mw.config.get( 'wgUserGroups' ).indexOf( groupName ) > -1 );
			};

			/**
			 * Display a status message inside the main content of the dialog.
			 *
			 * @return {string} Status message to display.
			 */
			CatRename.prototype.showStatus = function ( status ) {
				this.statusIndicator.text( status );
				this.$body.children().detach();
				this.$body.append( this.statusContent.$element );
			};

			/**
			 * Raise an error using OO.ui.Error, and reset all what should be.
			 *
			 * @param {string} error Error message to display to the user.
			 * @param {boolean} recoverable Is the error recoverable (default to true).
			 * @param {boolean} warning Should we raise a warning instead an error (default to false).
			 */
			CatRename.prototype.errorHandler = function ( error, recoverable, warning ) {
				var errorMessage = new OO.ui.Error( error, { recoverable: recoverable || true, warning: warning || false } );

				this.unlockMultitabs();
				this.$body.children().detach();
				this.$body.append( this.configContent.$element );

				this.getActions().get( { actions: 'rename' } )[ 0 ].setDisabled( false );
				this.getActions().get( { actions: 'rbot' } )[ 0 ].setDisabled( false );

				this.deferred.reject( errorMessage );
			};

			/**
			 * Add a page to the error log.
			 *
			 * @param {string} pageName Name (including namespace) of the page.
			 * @param {string} reason Explaination of the error.
			 */
			CatRename.prototype.logFailedPages = function ( pageName, reason ) {
				var li = $( '<li>' ).text( ' - ' + reason ),
					a = $( '<a>' ).attr( 'href', mw.util.getUrl( pageName ) ).text( pageName );
				this.pagesInError.append( li.prepend( a ) );
			};

			/**
			 * Build a regex to extract the link to a given category from wikicode.
			 *
			 * @param {string} category Name (without namespace) of the category.
			 * @return {RegExp} Regex object to extract the given category.
			 */
			CatRename.prototype.buildRegex = function ( category ) {
				var formattedNamespace = mw.config.get( 'wgFormattedNamespaces' )[ 14 ],
					isFirstLetterCaseSensitive = ( mw.config.get( 'wgCaseSensitiveNamespaces' ).indexOf( 14 ) > -1 ),
					namespace = '(?:[' + formattedNamespace.charAt( 0 ) + formattedNamespace.charAt( 0 ).toLowerCase() + ']' + formattedNamespace.slice( 1 ) + '|[Cc]ategory)';

				category = category.replace( /([\\^$*+?.|{}[\]()])/g, '\\$1' );

				if ( ! isFirstLetterCaseSensitive ) {
					var firstLetter = category.charAt(0);
					if ( firstLetter.toUpperCase() !== firstLetter.toLowerCase() ) {
						category = '[' + firstLetter.toUpperCase() + firstLetter.toLowerCase() + ']'
							+ category.slice(1);
					}
				}

				return new RegExp('(\\s*)\\[\\[( |_)*' + namespace + '( |_)*:( |_)*' + category + '( |_)*(\\|[^\\]]*)?\\]\\]', 'g');
			};

			/**
			 * Generate a random string.
			 *
			 * @param {number} length Length of the string to generate.
			 * @return {string} The generated string.
			 */
			CatRename.prototype.randomString = function ( length ) {
				var result = '';
				var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
				for ( var i = 0; i < length; ++i ) {
					result += chars.charAt( Math.floor( Math.random() * chars.length ) );
				}
				return result;
			};

			instanceWindowManager = new OO.ui.WindowManager();
			$( 'body' ).append( instanceWindowManager.$element );

			instanceCatRename = new CatRename();
			instanceWindowManager.addWindows( [ instanceCatRename ] );
		}

	} );
}

/* </nowiki> */