MediaWiki:Gadget-libGlobalReplace.js

/** // List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/ // Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting] /* eslint indent:[error,tab,{outerIIFEBody:0}] */ // Set jsHint-options. You should not set forin or undef to false if your script does not validate. /* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, undef:true, curly:false, browser:true*/ /* global jQuery:false, mediaWiki:false*/ ( function ( $, mw ) { 'use strict';
 * MediaWiki:Gadget-libGlobalReplace.js
 * Replaces a file on all wikis, including Wikimedia Commons
 * Uses either CORS under the current user account
 * or deputes the task to CommonsDelinker
 * The method used is determined by
 * -Browser capabilities (CORS required)
 * -The usage count: More than the given number
 * aren't attempted to be replaced
 * under the user account
 * It adds only one public method to the mw.libs - object:
 * @example
 * var $jQuery_Deferred_Object;
 * $jQuery_Deferred_Object = mw.libs.globalReplace(oldFile, newFile, shortReason, fullReason);
 * $jQuery_Deferred_Object.done(function { alert("Good news! " + oldFile + " has been replaced by " + newFile + "!") });
 * Internal stuff:
 * Since we don't use instances of classes, we have to pass around all the parameters
 * TODO: I18n (progress messages) when Krinkle is ready with Gadgets 2.0 :-)
 * @rev 1 (2012-11-26)
 * @rev 5 (2017-12-15)
 * @rev 6 (2019-09-21)
 * @author Rillke – 2012–2015, Perhelion 2017–2019
 * @rev 1 (2012-11-26)
 * @rev 5 (2017-12-15)
 * @rev 6 (2019-09-21)
 * @author Rillke – 2012–2015, Perhelion 2017–2019
 * @author Rillke – 2012–2015, Perhelion 2017–2019

// Config // When this number is exceeded or reached, use CommonsDelinker // This number must not be higher than 50 // (can't query more than 50 titles at once) var usageThreshold = 45, // Internal stuff CORSsupported = false;

/** if ( !Object.keys ) { Object.keys = function ( o ) { var k = [], p;		for ( p in o ) { if ( Object.prototype.hasOwnProperty.call( o, p ) ) { k.push( p ); } } return k;	}; }
 * TODO: Outsource to library as I often use them OR does jQuery provide something like that?

var _firstItem = function ( o ) { return o[ Object.keys( o )[ 0 ] ]; },	// TODO: Keep in sync with CommonsDelinker source: // https://bitbucket.org/magnusmanske/commons-delinquent/src/master/demon.php getFileRegEx = function ( title, prefix ) { prefix = prefix || '[\\n\\[\\:\\=\\>\\|]\\s*'; return new RegExp( '(' + prefix + ')[' + mw.util.escapeRegExp( title[ 0 ].toUpperCase + title[ 0 ].toLowerCase ) + ']' + mw.util.escapeRegExp( title.slice( 1 ) ).replace( / /g, '[ _]' ), 'g' ); },	queryGET = function ( params, cb, errCb ) { mw.loader.using( [ 'ext.gadget.libAPI' ], function {			params.action = params.action || 'query';			mw.libs.commons.api.query( params, { cache: true, cb: cb, errCb: errCb } );		} );	},	centralToken = {}, edittoken = mw.user.tokens.get( 'csrfToken' ), fbToken, // fallback fetchingFbToken;

function doCORSreq( params, wiki, cb, errCb, method ) { var api = new mw.ForeignApi( '//' + wiki + mw.util.wikiScript( 'api' ) ); method = method === 'POST' ? 'post' : 'get'; api[ method ]( params ).done( function ( r ) {		cb( r, wiki );	} ).fail( function ( r ) {		mw.log.warn( 'API FAIL:', JSON.stringify( arguments ), r, params );		errCb( r, wiki );	} ); }

var getFbToken = function ( cb, wiki ) { if ( fbToken ) { return cb( fbToken ); } if ( fetchingFbToken ) { return; }

var para = { meta: 'tokens' }, h = mw.hook( 'commons.libglobalreplace.fbToken.fetched' ).add( cb ), errCb = function ( /* r */ ) { centralToken.centralauthtoken = 0; fbToken = '+\\'; mw.notify( 'Error fetching csrftoken from Wikidata. ' ); };	fetchingFbToken = true;

if ( centralToken.centralauthtoken ) { para.centralauthtoken = centralToken.centralauthtoken; centralToken.centralauthtoken = 0; }	if ( !para.centralauthtoken ) { CORSsupported = false; // Test logged-in return testCORS( function ( r ) {			// If the user is suddenly reported to be logged-out try again.			if ( CORSsupported !== 'OK' ) { return getCentralAuth( cb, errCb, wiki ); }

fbToken = r.query.tokens.csrftoken; h.fire( fbToken ); }, wiki );	}

doCORSreq( para, wiki, function ( r ) {		fbToken = r.query.tokens.csrftoken;		getCentralAuth( cb, errCb, wiki ); // Need new authtoken	}, errCb ); };

function queryCentralToken( token, cb, wiki ) { if ( token.centralauthtoken ) { centralToken = token.centralauthtoken; } getFbToken( cb, wiki ); }

function getCentralAuth( cb, errCb, wiki ) { new mw.Api.get( {		action: 'centralauthtoken'	} ).done( function ( r ) {		fetchingFbToken = false;		queryCentralToken( r, cb, wiki );	} ).fail( errCb ); }

function testCORS( done, wiki ) { mw.loader.using( [ 'mediawiki.user', 'mediawiki.api', 'mediawiki.ForeignApi' ] ).done( function {		if ( CORSsupported ) { return done; }		doCORSreq( { meta: 'tokens|userinfo' }, wiki || 'www.mediawiki.org', function ( data, textStatus ) { if ( !data.query || !data.query.userinfo.id ) { CORSsupported = 'CORS supported but not logged-in'; mw.log( CORSsupported, data, textStatus ); } else { CORSsupported = 'OK'; }			done( data ); }, function ( jqXHR, textStatus, errorThrown ) { CORSsupported = 'CORS not supported: ' + textStatus + '\nError: ' + errorThrown; done; } );	} ); }

var updateReplaceStatus = function ( $prog ) { /* If we are using CommonsDelinker (CD), it will mark this progress object * as resolved as soon as the requst was placed in the queue; * Don't know whether we should stop replacement under user account * when we request CD to do our job; but see no pressing need to */ if ( !$prog.remaining && !$prog.usingCD ) { $prog.resolve( 'All usages replaced' ); // Kill the timer: Everything worked in time! if ( $prog.CDtimeout ) { clearTimeout( $prog.CDtimeout ); } }		$prog.notify( 'Replacing usage: ' +			Math.round( ( $prog.total - $prog.remaining ) * 100 / $prog.total ) +			'% (' + ( $prog.total - $prog.remaining ) + '/' + $prog.total + ')\nDo not close this window until the task is completed.' ); },	decrementAndUpdate = function ( $prog ) { $prog.remaining--; updateReplaceStatus( $prog ); },	incrementAndUpdate = function ( $prog ) { $prog.remaining++; updateReplaceStatus( $prog ); },	checkPage = function ( $prog, pg, wiki, cb ) { if ( !pg.revisions ) { $prog.notify( 'No page text for ' + pg.title + ' – ' + wiki + ' – private wiki or out of date?' ); if ( typeof cb === 'function' ) { cb; } return false; } else { return true; }	},	compareTexts = function ( $prog, oldT, newT, title, wiki ) { if ( oldT === newT ) { $prog.notify( 'No changes at ' + title + ' – ' + wiki + ' – template use?' ); decrementAndUpdate( $prog ); return false; } else { return true; }	};

function noUnlinkFromNamespace( pg, $prog ) { return ( pg.ns % 2 ) || // Skip talk pages ( pg.ns < 0 ) || // Paranoia ( $prog.notOnNs && $prog.notOnNs.indexOf( pg.ns ) >= 0 ); // Skip optional namespaces }

/** var commonsDelinker = function ( of, nf, sr, fr, $prog ) { // Don't ask CommonsDelinker multiple times to replace the same file if ( $prog.usingCD ) { return; } if ( $prog.dontUseCD ) { return $prog.reject( 'Unable replacing all usages. Usually CD would now have been instructed but you wished not to do so.' ); } // Tell other processes that we're now using the delinker // So they don't stop us by resolving the progress $prog.usingCD = true;
 * Asks CommonsDelinker to replace a file.

mw.libs.globalReplaceDelinker( of, nf, sr + ' ' + fr, function {			$prog.resolve( 'CommonsDelinker has been instructed to replace ' + of + ' with ' + nf );		}, function ( t ) {			$prog.reject( 'Error while asking CommonsDelinker to replace ' + of + ' with ' + nf + ' Reason: ' + t );		} ); },	/**	* Replace usage at Wikimedia Commons. **/	localReplace = function ( re, localUsage, of, nf, sr, fr, $prog ) {

function isBadPage( pg ) { return ( pg.ns === 6 &&			[ of, nf ].indexOf( pg.title.replace( /^File:/, '' ) ) !== -1 ) || // Self-reference ( pg.ns === 2 && /^User:\w+Bot\b/.test( pg.title ) ) || // Bot subpage on Commons ( pg.ns === 4 && /(Deletion[_ ]requests\/[^\n]*|Undeletion[_ ]requests\/[^\n]*)\b/.test( pg.title ) ); // DR and UDR on Commons }

$.each( localUsage, function ( id, pg ) {		// Check page exists			if ( !checkPage( $prog, pg, 'Commons' ) || isBadPage( pg ) || noUnlinkFromNamespace( pg, $prog ) ) {				decrementAndUpdate( $prog );				return mw.log( 'LocalReplace skipped for', pg.title );			}

var isEditable = true, summary = sr + ' →  ' + fr, edit;

$.each( pg.protection, function ( i, pr ) {				if ( pr.type === 'edit' ) {					if ( mw.config.get( 'wgUserGroups' ).indexOf( pr.level ) === -1 ) { isEditable = false; }					return false;				}			} );

if ( isEditable ) { var oldText = pg.revisions[ 0 ][ '*' ], nwe1 = mw.libs.wikiDOM.nowikiEscaper( pg.revisions[ 0 ][ '*' ] ), newText = nwe1.secureReplace( re, '$1' + nf ).getText;

if ( !compareTexts( $prog, oldText, newText, pg.title, 'Commons' ) ) { return; }

edit = { cb: function { decrementAndUpdate( $prog ); },					errCb: function { decrementAndUpdate( $prog ); $prog.notify( 'Unable to update ' + pg.title + ' \nUsing CommonsDelinker' ); commonsDelinker( of, nf, sr, fr, $prog ); },					title: pg.title, text: newText, editType: 'text', watchlist: 'nochange', minor: true, summary: summary, basetimestamp: pg.revisions[ 0 ].timestamp };			} else { // If page is protected, post a request to the talk page edit = { cb: function { decrementAndUpdate( $prog ); },					errCb: function { decrementAndUpdate( $prog ); },					title: mw.libs.commons.getTalkPageFromTitle( pg.title ), text: '== Please replace File:' + of + ' ==\n\nThis page is protected while posting this message. Please replace  with   because ' + sr + ' ' + fr + '\nThank you. Message added by global replace -- ~', editType: 'appendtext', watchlist: 'nochange', minor: true, summary: summary };			}			mw.loader.using( [ 'ext.gadget.libAPI', 'mediawiki.user' ], function {				if ( !mw.user.isAnon ) { edit.assert = 'user'; }				mw.libs.commons.api.editPage( edit );			} ); } );	},

sanitizeFileName = function ( fn ) { return fn.replace( /_/g, ' ' ).trim.replace( /^(?:File|Image):/i, '' ); },	/**	* Replace usage in other wikis. It's not uncommon that edits fail due to	* title blacklist, abuse filter, captcha, server timeouts, protected pages * etc. but in this case we kindly ask CommonsDelinker whether it will do * the remaining ones for us. *	* @param {RegExp}   re           File RegExp object * @param {Array}    globalUsage  The global usage * @param {string}   of           Old file name. The old file name will be replaced with the new file name. * @param {string}   nf           New file name. * @param {string}   sr           Short reason like "file renamed". Will be prefixed to the edit summary. * @param {string}   fr           Full reason like "file renamed because it was offending". Will be appended to the edit summary. * @param {Object}   $prog        Deferred (factory function) object reflecting the current progress. * @return {boolean} */	globalReplace = function ( re, globalUsage, of, nf, sr, fr, $prog ) { var guWiki = {}, queries = [], chunks = [], summary = '(GR) ' + sr.replace( /\[\[(.+)\]\]/, '$1' ) + ' →  ' +				fr.replace( /\[\[(.+?)\]\]/g, '$1' ), edit = { action: 'edit', summary: summary, minor: true, nocreate: true, watchlist: 'nochange' },			wdEdit = { action: 'wbsetclaimvalue', snaktype: 'value', summary: summary },			setQuery = function ( wiki ) { window.setTimeout( function {					if ( wiki && chunks.length ) {						runReplacements( wiki );					} else { checkLocalFiles; }				}, 10 ); };

function getPageContentsFailed( err, wiki, text ) { err += err ? ' \n' : ' '; $prog.notify( ( text || 'Unable to get information from ' ) + wiki + err + '\nUsing CommonsDelinker' ); decrementAndUpdate( $prog ); commonsDelinker( of, nf, sr, fr, $prog ); return false; }

// First we have to compile a list of pages per Wiki $.each( globalUsage, function ( i, gu ) {			var pg = gu.title,				wiki = gu.wiki;			// Exclude before do query			if ( noUnlinkFromNamespace( gu, $prog ) ) {				decrementAndUpdate( $prog );				return;			}			if ( wiki in guWiki ) {				// Templates first				guWiki[ wiki ][( gu.ns === '10' ? 'unshift' : 'push' )]( pg );			} else {				guWiki[ wiki ] = [ pg ];			}		} );

var gotPagesContents = function ( result, wiki ) { var pages = result.query.pages, pagelist = Object.keys( pages ), setEdit = function { window.setTimeout( function {						if ( pagelist.length ) {							performEdit( pages[ pagelist.shift ] );						} else { setQuery( wiki ); }					}, 30 ); },				_onErr = function ( r ) { setEdit; getPageContentsFailed( '', wiki, JSON.stringify( r ) + ' Unable to update page at ' ); },				editNow = function ( edit ) { if ( !mw.user.isAnon ) { edit.assert = 'user'; } doCORSreq( edit, wiki, function ( r ) {						mw.log( 'editNow', r );						if ( r.error || ( r.edit && ( r.edit.spamblacklist || r.edit.result !== 'Success' ) ) ) {						// ERROR							_onErr( r );						} else {						// SUCCESS							decrementAndUpdate( $prog );							setEdit;						}					}, _onErr, 'POST' ); };

$prog.notify( 'Got page contents for ' + wiki + '. Updating them now.' ); edittoken = result.query.tokens.csrftoken;

// TODO: Work around protection function performEdit( pg ) { if ( !checkPage( $prog, pg, wiki, function { // Perhaps it's a private wiki and CommonsDelinker has access? commonsDelinker( of, nf, sr, fr, $prog ); } ) ) {					decrementAndUpdate( $prog ); return setEdit; }

var replacementCount = 0, newText, oldText = pg.revisions[ 0 ][ '*' ];

if ( wiki === 'www.wikidata.org' && pg.contentmodel === 'wikibase-item' ) { try { newText = JSON.parse( oldText ); $.each( newText.claims, function ( propId, propClaims ) {							$.each( propClaims, function ( idx, claim ) { if ( claim.type !== 'statement' || !claim.mainsnak || !claim.mainsnak.datavalue ||									typeof claim.mainsnak.datavalue.value !== 'string' ) { return setEdit; } if ( sanitizeFileName( claim.mainsnak.datavalue.value ) === sanitizeFileName( of ) ) { replacementCount++; if ( replacementCount > 1 ) { incrementAndUpdate( $prog ); } getFbToken( function ( token ) {										$.extend( wdEdit, { claim: claim.id, baserevid: pg.lastrevid, value: JSON.stringify( nf ), token: token } );										if ( centralToken.centralauthtoken ) {											wdEdit.centralauthtoken = centralToken.centralauthtoken;											centralToken.centralauthtoken = 0;										}										editNow( wdEdit );									}, 'www.wikidata.org' ); }							} );						} );						if ( !replacementCount ) { setEdit; return getPageContentsFailed( '', wiki, 'Nothing suitable for replacement found on ' + pg.title + ' on ' ); }					} catch ( noMatterWhat ) { setEdit; return getPageContentsFailed( '', wiki, noMatterWhat + ' Issue replacing usage on entry ' + pg.title + ' on ' ); }				} else { var editNowCB = function ( token ) { if ( !token || /^\+\\+$/.test( token ) ) { setEdit; return getPageContentsFailed( '', wiki, 'No token for ' ); }						newText = mw.libs.wikiDOM.nowikiEscaper( oldText ).secureReplace( re, '$1' + nf ).getText; if ( !compareTexts( $prog, oldText, newText, pg.title, wiki ) ) { return setEdit; } $.extend( edit, {							title: pg.title,							starttimestamp: result.curtimestamp,							basetimestamp: pg.revisions[ 0 ].timestamp,							text: newText,							token: token						} ); if ( centralToken.centralauthtoken ) { edit.centralauthtoken = centralToken.centralauthtoken; centralToken.centralauthtoken = 0; }						editNow( edit ); };

if ( !edittoken || /^\+\\+$/.test( edittoken ) ) { // Try get fallback token return getFbToken( editNowCB, wiki ); }					editNowCB( edittoken ); }			}			setEdit; };

function runReplacements( wiki ) { var titles = chunks.shift; if ( !titles ) { return checkLocalFiles; } doCORSreq( {				prop: 'info|revisions',				curtimestamp: 1,				meta: 'tokens',				rvprop: 'content|timestamp',				titles: titles.join( '|' ).replace( /_/g, ' ' )			}, wiki, gotPagesContents, function ( r, wiki ) {				getPageContentsFailed( wiki, titles );				setQuery( wiki );			} );

}

function checkLocalFiles( /* wiki, titles*/ ) { var wiki = queries.shift; if ( !wiki ) { return; } // finish var titles = guWiki[ wiki ]; // Now, it's possible that the wiki has a local file with the new name, // a so-called "shadow". // In this case the replacement is most likely undesired. // Convert the edits in chunks chunks = ( function ( arr, cSize ) {				var c = [];				while ( arr.length ) { c.push( arr.splice( 0, cSize ) ); }				return c;			}( titles, usageThreshold ) ); // Test shadow copy doCORSreq( {				list: 'allimages',				aifrom: nf,				aito: nf			}, wiki, function ( r ) {				if ( r && r.query && r.query.allimages && r.query.allimages.length ) {					// Skip this wiki					$prog.notify( 'Skipping ' + wiki + ' because there is a shadow file with the same target name.' );					$prog.remaining -= titles.length;					updateReplaceStatus( $prog );					checkLocalFiles;				} else {					runReplacements( wiki );				}			}, function ( r, wiki ) {				runReplacements( wiki );			} ); }

// Then send out the queries to the Wikis // First Wikidata if ( 'www.wikidata.org' in guWiki ) { chunks.push( guWiki[ 'www.wikidata.org' ] ); runReplacements( 'www.wikidata.org' ); delete guWiki[ 'www.wikidata.org' ]; queries = Object.keys( guWiki ); } else { queries = Object.keys( guWiki ); checkLocalFiles; // async }

// $.each(guWiki, checkLocalFiles); // sync },

uGroups = mw.config.get( 'wgUserGroups' ), /**	* @param {string} of Old file name. The old file name will be replaced with the new file name. * @param {string} nf New file name. * @param {string} sr Short reason like "file renamed". Will be prefixed to the edit summary. * @param {string} fr Full reason like "file renamed because it was offending". Will be appended to the edit summary. * @param {$.Deferred} $prog Deferred object reflecting the current progress. **/	replace = function ( of, nf, sr, fr, $prog ) {

of = sanitizeFileName( of ); nf = sanitizeFileName( nf );

var pending = 0, localResult, globalUsage = [], globalResult, sysop = uGroups.indexOf( 'sysop' ) !== -1, _getGlobalQuery = function ( gucontinue ) { queryGET( {					prop: 'globalusage',					guprop: 'namespace',					gulimit: sysop ? 250 : usageThreshold,					gufilterlocal: 1,					gucontinue: gucontinue || '||',					titles: 'File:' + of				}, _queryGlobal ); },			_selectMethod = function { globalUsage = globalUsage.concat( _firstItem( globalResult.query.pages ).globalusage );

var globalUsageCount = globalUsage.length, localUsage = localResult.query ? localResult.query.pages : {}, usageCount = Object.keys( localUsage ).length + globalUsageCount;

$prog.remaining = usageCount; $prog.total = usageCount; mw.log( CORSsupported ); if ( !usageCount ) { $prog.resolve( 'File was not in use. Nothing replaced.' ); } else if ( ( usageCount >= usageThreshold || ( CORSsupported !== 'OK' && globalUsageCount ) ) && !$prog.dontUseCD ) { $prog.notify( 'Instructing CommonsDelinker to replace this file' ); commonsDelinker( of, nf, sr, fr, $prog ); } else { if ( usageCount - globalUsageCount ) { localReplace( getFileRegEx( of, '(?:[\\n\\[\\=\\>\\|]|[\\n\\[\\=\\>\\|][Ff]ile\\:)\\s*' ), localUsage, of, nf, sr, fr, $prog ); } if ( globalUsageCount ) { if ( 'continue' in globalResult ) { // eslint-disable-next-line dot-notation return _getGlobalQuery( globalResult[ 'continue' ].gucontinue ); }

globalReplace( getFileRegEx( of ), globalUsage, of, nf, sr, fr, $prog ); }					$prog.notify( 'Replacing usage immediately using your user account. Do not close this window until the process is complete.' ); }				// Finally, set a timeout that will instruct CommonsDelinker if it takes too long $prog.CDtimeout = setTimeout( function {					commonsDelinker( of, nf, sr, fr, $prog );				}, 50000 ); },			_queryLocal = function ( result ) { pending--; if ( result ) { localResult = result; } if ( pending > 0 ) { return; } _selectMethod; },			_queryGlobal = function ( result ) { pending--; if ( result ) { globalResult = result; }

if ( pending > 0 ) { return; } _selectMethod; };

$prog.notify( 'Query usage and selecting replace-method' ); pending++; queryGET( {			generator: 'imageusage',			giufilterredir: 'nonredirects',			giulimit: sysop ? 250 : usageThreshold,			prop: 'info|revisions',			inprop: 'protection',			rvprop: 'content|timestamp',			giuredirect: 1,			giutitle: 'File:' + of		}, _queryLocal ); pending++; _getGlobalQuery; pending++; testCORS( function {			pending--;			if ( pending > 0 ) { return; }			_selectMethod;		} ); };

// Expose globally /** mw.libs.globalReplace = function ( oldFile, newFile, shortReason, fullReason, dontUseDelinker, notOnNamespaces ) { var $progress = $.Deferred; $progress.pendingQueries = 0; $progress.dontUseCD = dontUseDelinker; $progress.notOnNs = Array.isArray( notOnNamespaces ) ? notOnNamespaces : false; var args = Array.prototype.slice.call( arguments, 0 ); // Delete optional dontUseDelinker and notOnNamespaces if ( args.length > 4 ) { args.splice( 4 ); } // Add progress args.push( $progress ); replace.apply( this, args ); return $progress; }; mw.libs.globalReplaceDelinker = function ( oldFile, newFile, reason, cb, errCb ) { oldFile = sanitizeFileName( oldFile ); newFile = sanitizeFileName( newFile );
 * @param {string} oldFile Old file name. The old file name will be replaced with the new file name.
 * Can be in any format (both "File:Abc def.png" and "Abc_def.png" work)
 * @param {string} newFile New file name.
 * Can be in any format (both "File:Abc def.png" and "Abc_def.png" work)
 * @param {string} shortReason Short reason like "file renamed". Will be prefixed to the edit summary.
 * @param {string} fullReason Full reason like "file renamed because it was offending". Will be appended to the edit summary.
 * @param {boolean} dontUseDelinker Prevents usage of CommonsDelinker (only provided for debugging/scripting)
 * @param {Array} notOnNamespaces Skip optional namespacenumbers
 * @return {$.Deferred} $prog jQuery deferred-object reflecting the current progress. See http://api.jquery.com/category/deferred-object/ for more info.
 * @examle See this gadget's introduction.

reason = reason.replace( /\{/g, '&#123;' ).replace( /\}/g, '&#125;' ).replace( /=/g, '&#61;' ); var edit = { cb: cb, errCb: errCb, title: 'User:CommonsDelinker/commands', text: '\n', editType: 'appendtext', watchlist: 'nochange', summary: 'universal replace: → ' };	if ( mw.config.get( 'wgUserGroups' ).indexOf( 'sysop' ) === -1 ) { edit.title = 'User:CommonsDelinker/commands/filemovers'; }

mw.loader.using( 'ext.gadget.libAPI', function {		mw.libs.commons.api.editPage( edit );	} ); };

}( jQuery, mediaWiki ) ); //