مدیاویکی:Gadget-libGlobalReplace.js

از ایران پدیا
پرش به ناوبری پرش به جستجو

نکته: پس از انتشار ممکن است برای دیدن تغییرات نیاز باشد که حافظهٔ نهانی مرورگر خود را پاک کنید.

  • فایرفاکس / سافاری: کلید Shift را نگه دارید و روی دکمهٔ Reload کلیک کنید، یا کلید‌های Ctrl-F5 یا Ctrl-R را با هم فشار دهید (در رایانه‌های اپل مکینتاش کلید‌های ⌘-R)
  • گوگل کروم: کلیدهای Ctrl+Shift+R را با هم فشار دهید (در رایانه‌های اپل مکینتاش کلید‌های ⌘-Shift-R)
  • اینترنت اکسپلورر/ Edge: کلید Ctrl را نگه‌دارید و روی دکمهٔ Refresh کلیک کنید، یا کلید‌های Ctrl-F5 را با هم فشار دهید
  • اپرا: Ctrl-F5 را بفشارید.
/**
 * [[MediaWiki:Gadget-GlobalReplace.js]]
 * Replaces a file on all wikis, including Wikimedia Commons
 * Uses either CORS under the current user account
 * or deputes the task to Commons Delinker
 *
 * 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)
 * @author Rillke, 2012
 * <nowiki>
 */
// List the global variables for jsHint-Validation. Please make sure that it passes http://jshint.com/
// Scheme: globalVariable:allowOverwriting[, globalVariable:allowOverwriting][, globalVariable:allowOverwriting]
/*global jQuery:false, mediaWiki:false*/

// 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, strict:true, undef:true, curly:false, browser:true*/

(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 = 200;

	// Internal stuff
	var CORSsupported = false;

	/**
	 * TODO: Outsource to library as I often use them OR does jQuery provide something like that?
	 **/
	var getObjLen = function (obj) {
		var x, i = 0;
		for (x in obj) {
			if (obj.hasOwnProperty(x)) {
				i++;
			}
		}
		return i;
	};
	var firstItem = function (o) {
		for (var i in o) {
			if (o.hasOwnProperty(i)) {
				return o[i];
			}
		}
	};

	// TODO: Keep in sync with CommonsDelinker source
	// http://svn.wikimedia.org/viewvc/pywikipedia/trunk/pywikipedia/commonsdelinker/delinker.py?revision=9053&view=markup#l172
	var getFileRegEx = function (title) {
		return new RegExp('([\\n\\[\\:\\=\\>\\|]\\s*)[' + mw.RegExp.escape(title.charAt(0).toUpperCase()) + mw.RegExp.escape(title.charAt(0).toLowerCase()) + ']' + mw.RegExp.escape(
			title.slice(1)).replace(/ /g, '[ _]'), 'g');
	};

	var queryGET = function (params, cb, errCb) {
		mw.loader.using(['ext.gadget.libAPI', 'ext.gadget.libWikiDOM'], function () {
			mw.libs.commons.api.query(params, {
				method: 'GET',
				cache: true,
				cb: cb,
				errCb: errCb
			});
		});
	};

	var testCORS = function (done) {
		if (CORSsupported) return done();
		doCORSReq({
			action: 'query',
			meta: 'userinfo'
		}, 'www.mediawiki.org', function (data, textStatus, jqXHR) {
			if (!data.query.userinfo.id) {
				CORSsupported = 'CORS supported but not logged-in';
			} else {
				CORSsupported = 'OK';
			}
			done();
		}, function (jqXHR, textStatus, errorThrown) {
			CORSsupported = 'CORS not supported: ' + textStatus + ' \nError: ' + errorThrown;
			done();
		});
	};

	var doCORSReq = function (params, wiki, cb, errCb, method) {
		$.support.cors = true;
		// Format parameter first!
		var newParams = {
			format: 'json',
			origin: document.location.protocol + '//' + document.location.hostname
		};
		params = $.extend(newParams, params);
		$.ajax({
			'url': '//' + wiki + '/w/api.php',
			'data': params,
			'xhrFields': {
				'withCredentials': true
			},
			'type': method || 'GET',
			'success': function (r) {
				cb(r, wiki);
			},
			'error': errCb,
			'dataType': 'json'
		});
	};

	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 (0 === $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) +
			"% \nDo not close this window until the task is completed.");
	};
	var decrementAndUpdate = function ($prog) {
		$prog.remaining--;
		updateReplaceStatus($prog);
	};
	var 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 (cb && $.isFunction(cb)) cb();
			decrementAndUpdate($prog);
			return false;
		} else {
			return true;
		}
	};
	var 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;
		}
	};

	/**
	 *  Replace usage at Wikimedia Commons.
	 **/
	var localReplace = function (re, localUsage, of, nf, sr, fr, $prog) {
		$.each(localUsage, function (id, pg) {
			if (!checkPage($prog, pg, 'Commons')) return;

			var isEditable = true,
				summary = sr + ' [[File:' + of + ']] → [[File:' + nf + ']] ' + fr,
				edit;

			$.each(pg.protection, function (i, pr) {
				if ('edit' === pr.type) {
					if ($.inArray(pr.level, mw.config.get('wgUserGroups')) === -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);
						//$prog.notify("Using 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 by [[MediaWiki:Gadget-GlobalReplace.js]]</small> -- ~~~~",
					editType: 'appendtext',
					watchlist: 'nochange',
					minor: true,
					summary: summary
				};
			}
			mw.loader.using(['ext.gadget.libAPI'], function () {
				mw.libs.commons.api.editPage(edit);
			});
		});
	};

	var sanitizeFileName = function (fn) {
		return $.trim(fn.replace(/_/g, ' ')).replace(/^(?:File|Image)\:/, '');
	};

	/**
	 * @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.
	 **/
	var replace = function (of, nf, sr, fr, $prog) {
		var pending = 0,
			localResult,
			globalResult;

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

		var _queryLocal = function (result) {
			pending--;
			if (result) localResult = result;
			if (pending > 0) return;
			_selectMethod();
		};
		var _queryGlobal = function (result) {
			pending--;
			if (result) globalResult = result;
			if (pending > 0) return;
			_selectMethod();
		};
		var _selectMethod = function () {
			var globalUsage = firstItem(globalResult.query.pages).globalusage,
				globalUsageCount = globalUsage.length,
				localUsage = localResult.query ? localResult.query.pages : {},
				usageCount = getObjLen(localUsage) + globalUsageCount;

			$prog.remaining = usageCount;
			$prog.total = usageCount;
			if (0 === usageCount) {
				$prog.resolve("File was not in use. Nothing replaced.");
			} else if ((usageCount >= usageThreshold || (CORSsupported !== 'OK' && globalUsageCount)) && !$prog.dontUseCD) {
				//commonsDelinker(of, nf, sr, fr, $prog);
				//$prog.notify("Instructing CommonsDelinker to replace this file");
			} else {
				var re = getFileRegEx(of);
				localReplace(re, localUsage, of, nf, sr, fr, $prog);
				// globalReplace(re, globalUsage, of, nf, sr, fr, $prog);
				$prog.notify("Replacing usage immediately using your user account. Do not close this window until the process completed.");
			}
			// Finally, set a timeout that will instruct CommonsDelinker if it takes too long
			$prog.CDtimeout = setTimeout(function () {
				//commonsDelinker(of, nf, sr, fr, $prog);
			}, 60000);
		};

		$prog.notify("Query usage and selecting replace-method");
		pending++;
		queryGET({
			action: 'query',
			generator: 'imageusage',
			giufilterredir: 'nonredirects',
			giulimit: usageThreshold,
			prop: 'info|revisions',
			inprop: 'protection',
			rvprop: 'content|timestamp',
			giutitle: 'File:' + of
		}, _queryLocal);
		pending++;
		queryGET({
			action: 'query',
			prop: 'globalusage',
			guprop: '',
			gulimit: usageThreshold,
			gufilterlocal: 1,
			titles: 'File:' + of
		}, _queryGlobal);

		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)
	 * @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) {
		var $progress = $.Deferred();
		$progress.pendingQueries = 0;
		$progress.dontUseCD = dontUseDelinker;
		var args = Array.prototype.slice.call(arguments, 0);
		// Delete "dontUseDelinker"
		if (args.length > 4) args.pop();
		// Add progress
		args.push($progress);
		replace.apply(this, args);
		return $progress;
	};

}(jQuery, mediaWiki));
//</nowiki>