MediaWiki:Gadget-ReplaceImgs.js

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

Увага: Після публікування слід очистити кеш браузера, щоб побачити зміни.

  • Firefox / Safari: тримайте Shift, коли натискаєте Оновити, або натисніть Ctrl-F5 чи Ctrl-Shift-R (⌘-R на Apple Mac)
  • Google Chrome: натисніть Ctrl-Shift-R (⌘-Shift-R на Apple Mac)
  • Internet Explorer / Edge: тримайте Ctrl, коли натискаєте Оновити, або натисніть Ctrl-F5
  • Opera: натисніть Ctrl-F5
/**
* [[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
* <nowiki>
*/
// 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';

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

/**
* TODO: Outsource to library as I often use them OR does jQuery provide something like that?
**/
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;
	};
}

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 = '+\\';
			throw new Error( '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
}

/**
*  Asks CommonsDelinker to replace a file.
**/
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;

		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 + ' [[File:' + of + ']] → [[File:' + nf + ']] ' + 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{{edit request}}\nThis page is protected while posting this message. Please replace <code>[[:File:' + of + ']]</code> with <code>[[:File:' + nf + ']]</code> because ' + sr + ' ' + fr + '\nThank you. <small>Message added by [[:c:GR|global replace]]</small> -- ~~~~',
					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 = '([[c:GR|GR]]) ' + sr.replace( /\[\[(.+)\]\]/, '[[c:$1]]' ) +
				' [[File:' + of + ']] → [[File:' + nf + ']] ' +
				fr.replace( /\[\[(.+?)\]\]/g, '[[c:$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
/**
* @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.
**/
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 );

	reason = reason.replace( /\{/g, '&#123;' ).replace( /\}/g, '&#125;' ).replace( /=/g, '&#61;' );
	var edit = {
		cb: cb,
		errCb: errCb,
		title: 'User:RLuts/scripttest/renamereplace',
		text: '\n{{universal replace|' + oldFile + '|' + newFile + '|reason=' + reason + '}}',
		editType: 'appendtext',
		watchlist: 'nochange',
		summary: 'universal replace: [[File:' + oldFile + ']] → [[File:' + newFile + ']]'
	};
	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 ) );
// </nowiki>