« MediaWiki:Gadget-CatRename.js » : différence entre les versions

Une page de Wikipédia, l'encyclopédie libre.
Contenu supprimé Contenu ajouté
m petit réarrangement de l'ordre de certaines méthodes, pour clarifier un peu le code
Récupération, traitement (merci Od1n !) et validation des inputs de l'utilisateur une fois pour toute en début de process
Ligne 121 : Ligne 121 :
CatRename.prototype.getActionProcess = function ( action ) {
CatRename.prototype.getActionProcess = function ( action ) {
if ( action === 'rename' ) {
if ( action === 'rename' ) {
return new OO.ui.Process( this.checkCategory, this )
return new OO.ui.Process()
.next( this.prepare, this )
.next( this.checkCategory, this )
.next( this.getMembers, this )
.next( this.getMembers, this )
.next( this.lockMultitabs, this )
.next( this.lockMultitabs, this )
Ligne 132 : Ligne 134 :
}
}
else if ( action === 'rbot' ) {
else if ( action === 'rbot' ) {
return new OO.ui.Process( this.checkCategory, this )
return new OO.ui.Process()
.next( this.prepare, this )
.next( this.checkCategory, this )
.next( this.getMembers, this )
.next( this.getMembers, this )
.next( this.postRBot, this )
.next( this.postRBot, this )
.next( this.renameCategory, this )
.next( this.renameCategory, this )
.next( this.unlockMultitabs, this )
.next( this.success, this )
.next( this.close, this );
.next( this.close, this );
}
}
else if ( action === 'cancel' ) {
else if ( action === 'cancel' ) {
return new OO.ui.Process( this.unlockMultitabs, this )
return new OO.ui.Process()
.next( this.unlockMultitabs, this )
.next( this.close, this );
.next( this.close, this );
}
}
Ligne 158 : Ligne 165 :


/* Process step methods */
/* 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() {
this.oldTitle = mw.config.get( 'wgTitle' );
this.newTitle = this.newNameInput.getValue().trim().replace(/^([Cc]atégorie|[Cc]ategory):/, '');
this.oldPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.oldTitle;
this.newPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.newdTitle;
this.reason = this.reasonInput.getValue().trim();
if ( mw.Title.makeTitle( 14, this.newName ) === null ) {
return new OO.ui.Error( 'Le titre de la catégorie demandée est non valide, vide, ou mal formé.' );
}
if ( this.reason === '' ) {
return new OO.ui.Error( 'Veuillez indiquer une explication pour ce renommage.' );
}

return;
};


/**
/**
Ligne 182 : Ligne 212 :
return this.deferred;
return this.deferred;
}
}
}

if ( mw.Title.makeTitle( 14, this.newNameInput.getValue().trim() ) === null ) {
this.errorHandler( 'Le titre de la catégorie demandée est non valide, vide, ou mal formé.' );
return this.deferred;
}
}


Ligne 194 : Ligne 219 :
'formatversion': 2,
'formatversion': 2,
'prop': 'categoryinfo',
'prop': 'categoryinfo',
'titles': 'Category:' + this.newNameInput.getValue()
'titles': this.newPageName
} ).then( function( data ) {
} ).then( function( data ) {
console.log( data );
console.log( data );
Ligne 397 : Ligne 422 :
*/
*/
CatRename.prototype.editMembers = function() {
CatRename.prototype.editMembers = function() {
var dialog = this;
var dialog = this,
totalPages = this.members.length,
oldCatRegex = this.buildRegex( this.oldTitle ),
newCatRegex = buildRegex( this.newTitle ),
summary = 'Remplacement de la catégorie [[Catégorie:$1]] par [[Catégorie:$2]] : $3'
.replace( '$1', this.oldTitle )
.replace( '$2', this.newTitle )
.replace( '$3', this.reason ),
commonPayload = {
summary: summary,
minor: true,
tags: TAG
};
this.deferred = $.Deferred();
this.deferred = $.Deferred();
var totalPages = this.members.length;


console.log( '---> Édition des membres de la catégorie' );
console.log( '---> Édition des membres de la catégorie' );

var oldCatName = mw.config.get( 'wgTitle' ),
oldCatRegex = this.buildRegex( oldCatName ),
newCatName = this.newNameInput.getValue(),
newCatRegex = buildRegex( newCatName ),
reason = this.reasonInput.getValue(),
summary = 'Remplacement de la catégorie [[Catégorie:$1]] par [[Catégorie:$2]] : $3'
.replace( '$1', oldCatName )
.replace( '$2', newCatName )
.replace( '$3', reason ),
commonPayload = {
summary: summary,
minor: true,
tags: TAG
};


if ( this.userInGroup( 'bot' ) ) {
if ( this.userInGroup( 'bot' ) ) {
Ligne 445 : Ligne 466 :
content = content.replace(
content = content.replace(
oldCatRegex,
oldCatRegex,
'$1[[' + mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + newCatName + '$6]]'
'$1[[' + this.newPageName + '$6]]'
);
);
}
}
Ligne 459 : Ligne 480 :
setTimeout( doEdit, dialog.noSpammingDelay );
setTimeout( doEdit, dialog.noSpammingDelay );
} )
} )
.fail( function ( reason, data ) {
.fail( function ( code, data ) {
console.log( reason );
console.log( code );
console.log( data );
console.log( data );
if ( reason === 'protectedpage' ) {
if ( code === 'protectedpage' ) {
dialog.logFailedPages( member, 'La page est protégée en écriture.' );
dialog.logFailedPages( member, 'La page est protégée en écriture.' );
doEdit();
doEdit();
}
}
else {
else {
dialog.errorHandler( reason );
dialog.errorHandler( code );
}
}
} );
} );
Ligne 493 : Ligne 514 :
'format': 'json',
'format': 'json',
'from': mw.config.get( 'wgPageName' ),
'from': mw.config.get( 'wgPageName' ),
'to': 'Category:' + this.newNameInput.getValue(),
'to': this.newPageName,
'reason': this.reasonInput.getValue(),
'reason': this.reason,
'tags': TAG,
'tags': TAG,
'formatversion': '2'
'formatversion': '2'
Ligne 518 : Ligne 539 :
} ).fail( function( error ) {
} ).fail( function( error ) {
if ( error === 'articleexists' ) {
if ( error === 'articleexists' ) {
dialog.errorHandler( 'Impossible de déplacer la catégorie, la page de destination « $1 » existe déjà.'.replace( '$1', dialog.newNameInput.getValue() ) );
dialog.errorHandler( 'Impossible de déplacer la catégorie, la page de destination « $1 » existe déjà.'.replace( '$1', dialog.newPageName ) );
}
}
else {
else {
Ligne 543 : Ligne 564 :
var content = '\n{{Déplacement catégorie'
var content = '\n{{Déplacement catégorie'
+ '|ancienne=' + mw.config.get( 'wgPageName' )
+ '|ancienne=' + mw.config.get( 'wgPageName' )
+ '|nouvelle=' + 'Catégorie:' + this.newNameInput.getValue()
+ '|nouvelle=' + this.newPageName
+ '|raison=' + this.reasonInput.getValue()
+ '|raison=' + this.reason
+ '}}';
+ '}}';


Ligne 599 : Ligne 620 :


setTimeout( function() {
setTimeout( function() {
window.location = mw.util.getUrl( 'Category:' + dialog.newNameInput.getValue() );
window.location = mw.util.getUrl( dialog.newPageName );
}, 1000 );
}, 1000 );
};
};
Ligne 656 : Ligne 677 :
*/
*/
CatRename.prototype.errorHandler = function ( error, recoverable, warning ) {
CatRename.prototype.errorHandler = function ( error, recoverable, warning ) {
recoverable = recoverable || true;
var errorMessage = new OO.ui.Error( error, { recoverable: recoverable || true, warning: warning || false } );
warning = warning || false;
this.unlockMultitabs();
this.unlockMultitabs();
Ligne 663 : Ligne 683 :
this.$body.append( this.configContent.$element );
this.$body.append( this.configContent.$element );
this.deferred.reject( new OO.ui.Error( error, { recoverable: recoverable, warning: warning } ) );
this.deferred.reject( errorMessage );
};
};



Version du 22 mars 2018 à 20:59

//TODO: Use i18n messages
//TODO: document attributes
//TODO: remove useless console.log

mw.loader.using( [ 'oojs-ui', 'mediawiki.util', 'mediawiki.api', 'mediawiki.api.edit', 'mediawiki.api.category' ], function() {
	( function ( mw, $, OO ) {

		const TAG = 'RenommageCategorie';
		const DAILY_LIMIT = 250;
		const RBOT_PAGE = 'Wikipédia:Bot/Requêtes/Catégories';

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

			this.api = new mw.Api( {
				timeout: 7000
			} );
			this.configContent = new OO.ui.PanelLayout( { padded: true, expanded: false } );
			this.statusContent = new OO.ui.PanelLayout( { padded: true, expanded: false } );
		};
		
		
		
		/* Setup */
		
		OO.inheritClass( CatRename, OO.ui.ProcessDialog );



		/* Static Properties */
		
		CatRename.static.name = 'catrename';
		CatRename.static.title = 'Renommer une catégorie';
		CatRename.static.actions = [
			{ action: 'rename', label: 'Renommer', flags: [ 'primary', 'progressive' ] },
			{ action: 'cancel', label: 'Annuler', flags: [ 'safe', 'back' ] },
			{ action: 'rbot', label: '... ou faire faire la tâche à un bot', 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();
			this.reasonInput = new OO.ui.TextInputWidget( {
				maxLength: 1000
			} );
			this.optionCheckboxes = new OO.ui.CheckboxMultiselectInputWidget( {
				value: [ 'movetalk', 'leave-redirect' ],
				options: [
					{ data: 'movetalk', label: 'Renommer aussi la page de discussion associée' },
					{ data: 'leave-redirect', label: 'Laisser une redirection vers le nouveau titre', disabled: ! this.userInGroup( 'sysop' ) },
					{ data: 'watch', label: 'Suivre les catégories originale et nouvelle' },
					{ data: 'watch-members', label: 'Suivre les pages modifiées' }
				]
			} );

			this.layout = new OO.ui.Widget( {
				content: [
					new OO.ui.FieldLayout(
						this.newNameInput, {
							align: 'top',
							label: 'Nouveau titre :',
							help: 'Nom de la catégorie après renommage, sans le préfixe "Catégorie:".'
						}
					),
					new OO.ui.FieldLayout(
						this.reasonInput, {
							align: 'top',
							label: 'Motif :',
						}
					),
					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 ) {
			if ( action === 'rename' ) {
				return new OO.ui.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 )
					.next( this.unlockMultitabs, this )
					.next( this.success, this )
					.next( this.close, this );
			}
			else if ( action === 'rbot' ) {
				return new OO.ui.Process()
					.next( this.prepare, this )
					.next( this.checkCategory, this )
					.next( this.getMembers, this )
					.next( this.postRBot, this )
					.next( this.renameCategory, this )
					.next( this.unlockMultitabs, this )
					.next( this.success, this )
					.next( this.close, this );
			}
			else if ( action === 'cancel' ) {
				return new OO.ui.Process()
					.next( this.unlockMultitabs, this )
					.next( this.close, this );
			}
			return CatRename.parent.prototype.getActionProcess.call( this, action );
		};

		/**
		 * 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() {
			this.oldTitle = mw.config.get( 'wgTitle' );
			this.newTitle = this.newNameInput.getValue().trim().replace(/^([Cc]atégorie|[Cc]ategory):/, '');
			this.oldPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.oldTitle;
			this.newPageName = mw.config.get('wgFormattedNamespaces')[ 14 ] + ':' + this.newdTitle;
			this.reason = this.reasonInput.getValue().trim();
			
			if ( mw.Title.makeTitle( 14, this.newName ) === null ) {
				return new OO.ui.Error( 'Le titre de la catégorie demandée est non valide, vide, ou mal formé.' );
			}
			if ( this.reason === '' ) {
				return new OO.ui.Error( 'Veuillez indiquer une explication pour ce renommage.' );
			}

			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
		 *         successfull or stopping the process if rejected
		 */
		CatRename.prototype.checkCategory = function() {
			var dialog = this;
			this.deferred = $.Deferred();

			this.showStatus( 'Vérification de la catégorie cible' );
			console.log( '---> Vérification de la catégorie' );

			var restrictionMove = mw.config.get( 'wgRestrictionMove' );
			for ( var i=0; i < restrictionMove.length; i++ ) {
				if ( ! this.userInGroup( restrictionMove[ i ] ) ) {
					this.errorHandler( 'Cette catégorie est protégée, vous n\'êtes pas autorisé à la renommer.' );
					return this.deferred;
				}
			}

			this.api.get( {
				'action': 'query',
				'format': 'json',
				'formatversion': 2,
				'prop': 'categoryinfo',
				'titles': this.newPageName
			} ).then( function( data ) {
				console.log( data );
				if ( data.query.pages[ 0 ].missing !== true ) {
					//TODO: Allow user to move pages without renaming the cat
					dialog.errorHandler( 'Il existe déjà une catégorie avec ce nom...' );
					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
		 *         successfull or stopping the process if rejected
		 */
		CatRename.prototype.getMembers = function() {
			var dialog = this;
			this.deferred = $.Deferred();
			this.members = [];

			this.showStatus( 'Récupération des pages membres de la catégorie' );
			console.log( '---> Récupération des pages de la catégorie actuelle' );

			function doGetMembers( cmcontinue ) {
				cmcontinue = cmcontinue || '';
				dialog.api.get( {
					'action': 'query',
					'format': 'json',
					'list': 'categorymembers',
					'formatversion': '2',
					'cmtitle': mw.config.get( 'wgPageName' ),
					'cmprop': 'title',
					'cmlimit': 'max',
					'cmcontinue': cmcontinue,
				} ).then( function( data ) {
					console.log( data );

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

					if ( data.continue !== undefined ) {
						doGetMembers( data.continue.cmcontinue );
					}
					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-' + Math.random().toString(36).substring(7);
			this.nextTab = null;

			this.showStatus( 'En attente de la fin de renommage dans d\'autres onglets' );
			console.log( '---> En attente de la fin de renommage d\'autres onglets' );

			//TODO: check lock timestamp
			if ( localStorage.getItem( 'catrename-lock' ) === null ) {
				console.log( 'lock-initial' );
				console.log( this.lockID );
				localStorage.setItem( 'catrename-lock', this.lockID );
				this.deferred.resolve();
			}
			else {
				$( window ).on( 'storage.catrename.catrename-waiting', function( event ) {
					console.log( 'storage event 1' );
					console.log(event);
					if ( event.originalEvent.key === 'catrename-lock' && event.originalEvent.newValue === dialog.lockID ) {
						console.log( 'got the lock' );
						$( window ).off( 'storage.catrename-waiting' );
						dialog.deferred.resolve();
					}
				} );
				localStorage.setItem( 'catrename-addtab', this.lockID );
			}


			$( window ).on( 'storage.catrename', function( event ) {
				console.log( 'storage event 2' );
				console.log(event.originalEvent);
				// 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 ) {
					console.log( 'addtab' );
					dialog.nextTab = event.originalEvent.newValue;
					localStorage.setItem( 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 ) {
					console.log( 'removetab' );
					dialog.nextTab = localStorage.getItem( dialog.nextTab );
					if ( dialog.nextTab !== null ) {
						localStorage.setItem( dialog.lockID, dialog.nextTab );
					}
					else {
						localStorage.removeItem( 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( 'Vérification de la limite journalière' );
			console.log( '---> Checking daily limit' );
			
			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 ) {
				console.log( data );

				if ( data.query.usercontribs.length + dialog.members.length >= DAILY_LIMIT ) {
					dialog.errorHandler( '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.'.replace( '$1', 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 = buildRegex( this.newTitle ),
				summary = 'Remplacement de la catégorie [[Catégorie:$1]] par [[Catégorie:$2]] : $3'
					.replace( '$1', this.oldTitle )
					.replace( '$2', this.newTitle )
					.replace( '$3', this.reason ),
				commonPayload = {
					summary: summary,
					minor: true,
					tags: TAG
				};
			this.deferred = $.Deferred();

			console.log( '---> Édition des membres de la catégorie' );

			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 ( member === undefined ) {
					dialog.deferred.resolve();
					return;
				}

				//TODO: a progress-bar ?
				dialog.showStatus( 'Édition de la page $1 sur $2'.replace( '$1', totalPages - dialog.members.length ).replace( '$2', totalPages ) );
				
				dialog.api.edit( member, function ( revision ) {
					var content = revision.content,
							newCatInPageList = content.match( newCatRegex );

					if ( newCatInPageList !== null ) {
						dialog.logFailedPages( member, 'La page contient déjà la nouvelle catégorie.' );
					}
					else {
						content = content.replace(
							oldCatRegex,
							'$1[[' + this.newPageName + '$6]]'
						);
					}

					return $.extend( { text: content }, commonPayload );
				} )
					.then( function ( result ) {
					console.log( result );
					if ( result.nochange === true ) {
						dialog.logFailedPages( member, 'La catégorie n\'a pas été trouvée dans le code de la page, peut-être est-elle incluse via un modèle ?' );
					}

					setTimeout( doEdit, dialog.noSpammingDelay );
				} )
					.fail( function ( code, data ) {
					console.log( code );
					console.log( data );
					if ( code === 'protectedpage' ) {
						dialog.logFailedPages( member, 'La page est protégée en écriture.' );
						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( 'Renommage de la catégorie' );
			console.log( '---> Déplacement de la catégorie' );

			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( 'leave-redirect' ) === -1 ) {
				payload[ 'noredirect' ] = 1;
			}
			if ( options.indexOf( 'watch' ) > -1 ) {
				payload[ 'watchlist' ] = 'watch';
			}

			this.api.postWithToken( 'csrf', payload ).then( function( data ) {
				console.log( data );
				dialog.deferred.resolve();
			} ).then( function( data ) {
				console.log( data );
				dialog.deferred.resolve();
			} ).fail( function( error ) {
				if ( error === 'articleexists' ) {
					dialog.errorHandler( 'Impossible de déplacer la catégorie, la page de destination « $1 » existe déjà.'.replace( '$1', dialog.newPageName ) );
				}
				else {
					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( 'Dépôt de la requête aux bots' );
			console.log( '---> Dépot d\'un message sur RBOT' );

			var content = '\n{{Déplacement catégorie'
			+ '|ancienne=' + mw.config.get( 'wgPageName' )
			+ '|nouvelle=' + this.newPageName
			+ '|raison=' + this.reason
			+ '}}';

			this.api.postWithToken( 'csrf', {
				'action': 'edit',
				'format': 'json',
				'title': RBOT_PAGE,
				'summary': 'RBOT: Demande de renommage de catégorie',
				'tags': TAG,
				'nocreate': 1,
				'appendtext': content,
				'formatversion': '2'
			} ).then( function( data ) {
				console.log( 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' );

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

				// wake up the next tab, or reset if there is none
				if ( localStorage.getItem( 'catrename-lock' ) === this.lockID ) {
					if ( this.nextTab !== null ) {
						localStorage.setItem( 'catrename-lock', this.nextTab );
					}
					else {
						localStorage.removeItem( '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 );
		};

		/* Instanciate CatRename and add it to MediaWiki's UI */
		
		$( function() {
			if ( mw.config.get( 'wgNamespaceNumber' ) === 14 ) {
				var windowManager = new OO.ui.WindowManager();
				$( 'body' ).append( windowManager.$element );
	
				var catRename = new CatRename();
				windowManager.addWindows( [ catRename ] );
	
				var portlet = mw.util.addPortletLink( 'p-cactions', '#', 'CatRename' );
				$( portlet ).on( 'click', function ( e ) {
					windowManager.openWindow( catRename );
					e.preventDefault();
				} );
	
				window.catRename = catRename;
			}
		} );
		


		/* 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.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');
		}

	}( mediaWiki, jQuery, OO ) );
} );