مدیاویکی: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>