FANDOM


m (Per Thread:669)
Line 1: Line 1:
 
/**
 
/**
* <nowiki>
+
* MassEdit/code.js
* MassEdit.js
 
 
* @file Adds/deletes/replaces content from pages/categories/namespaces
 
* @file Adds/deletes/replaces content from pages/categories/namespaces
 
* @author Eizen <dev.wikia.com/wiki/User_talk:Eizen>
 
* @author Eizen <dev.wikia.com/wiki/User_talk:Eizen>
* @license CC-BY-SA 3.0
 
 
* @external "mediawiki.util"
 
* @external "mediawiki.util"
* @external "mediawiki.user"
+
* @external "jQuery"
* @external "ext.wikia.LinkSuggest"
+
* @external "wikia.ui.factory"
* @external "jquery"
+
* @external "wikia.window"
  +
* @external "I18n-js"
 
* @external "mw"
 
* @external "mw"
* @external "wikia.window"
 
 
*/
 
*/
   
 
/*jslint browser, this:true */
 
/*jslint browser, this:true */
/*global require */
+
/*global mw, jQuery, window, require, wk, ui */
/*eslint-env es6 */
 
/*eslint-disable */
 
   
require(["jquery", "mw", "wikia.window"], function ($, mw, wk) {
+
require(["jquery", "mw", "wikia.window", "wikia.ui.factory"],
"use strict";
+
function (jQuery, mw, wk, ui) {
  +
"use strict";
   
// Define extant global object config if needed
+
if (jQuery("#massEdit-li").exists() || window.isMassEditLoaded) {
wk.dev = wk.dev || {};
+
return;
wk.dev.massEdit = wk.dev.massEdit || {};
 
 
// Prevent double loads and respect prior double load check formatting
 
if (wk.dev.massEdit.isLoaded || wk.isMassEditLoaded) {
 
return;
 
}
 
wk.dev.massEdit.isLoaded = true;
 
 
/**
 
* @description The <code>main</code> namespace object is used as a class
 
* prototype for the MassEdit class instance. It contains methods and
 
* properties related to the actual MassEdit functionality and application
 
* logic, keeping in a separate object all the methods used to initialize the
 
* script itself.
 
*
 
* @const
 
*/
 
const main = {};
 
 
/**
 
* @description The <code>init</code> namespace object contains methods and
 
* properties related to the initialization of the MassEdit script. The
 
* methods in this namespace object are responsible for loading external
 
* dependencies, validating user input, setting config, and creating a new
 
* MassEdit instance once setup is complete.
 
*
 
* @const
 
*/
 
const init = {};
 
 
/**
 
* @description This simple <code>boolean</code> flag is used to log messages
 
* in the console at sensitive application logic problem areas where issues
 
* are known to arise. Originally, DEBUG was part of an enum alongside a unit
 
* testing <code>boolean</code>; however, the removal of unit tests at the end
 
* of the testing period returned DEBUG to the script-global scope.
 
*
 
* @const
 
*/
 
const DEBUG = false;
 
 
/****************************************************************************/
 
/* Prototype pseudo-enums */
 
/****************************************************************************/
 
 
// Protected pseudo-enums of prototype
 
Object.defineProperties(main, {
 
 
/**
 
* @description This pseudo-enum of the <code>main</code> namespace object
 
* is used to store all CSS selectors in a single place in the event that
 
* one or more need to be changed. The formatting of the object literal key
 
* naming is type (id or class), location (placement, modal, content), and
 
* either the name for ids or the type of element (div, span, etc.).
 
* Originally, these were all divided into nested object literals as seen in
 
* Message.js. However, this system became too unreadable in the body of the
 
* script, necessitating a simpler system.
 
*
 
* @readonly
 
* @enum {string}
 
*/
 
Selectors: {
 
enumerable: true,
 
writable: false,
 
configurable: false,
 
value: Object.freeze({
 
 
// Toolbar placement ids
 
ID_PLACEMENT_LIST: "massedit-placement-list",
 
ID_PLACEMENT_LINK: "massedit-placement-link",
 
 
// Modal footer ids
 
ID_MODAL_CONTAINER: "massedit-modal-container",
 
ID_MODAL_SUBMIT: "massedit-modal-submit",
 
ID_MODAL_TOGGLE: "massedit-modal-toggle",
 
ID_MODAL_CANCEL: "massedit-modal-cancel",
 
ID_MODAL_CLEAR: "massedit-modal-clear",
 
ID_MODAL_CLOSE: "massedit-modal-close",
 
 
// Modal body ids
 
ID_CONTENT_CONTAINER: "massedit-content-container",
 
ID_CONTENT_FORM: "massedit-content-form",
 
ID_CONTENT_FIELDSET: "massedit-content-fieldset",
 
ID_CONTENT_CONTENT: "massedit-content-content",
 
ID_CONTENT_REPLACE: "massedit-content-replace",
 
ID_CONTENT_INDICES: "massedit-content-indices",
 
ID_CONTENT_PAGES: "massedit-content-pages",
 
ID_CONTENT_SUMMARY: "massedit-content-summary",
 
ID_CONTENT_ACTION: "massedit-content-action",
 
ID_CONTENT_TYPE: "massedit-content-type",
 
ID_CONTENT_CASE: "massedit-content-case",
 
ID_CONTENT_LOG: "massedit-content-log",
 
 
// Toolbar placement classes
 
CLASS_PLACEMENT_OVERFLOW: "overflow",
 
 
// Modal footer classes
 
CLASS_MODAL_CONTAINER: "massedit-modal-container",
 
CLASS_MODAL_BUTTON: "massedit-modal-button",
 
CLASS_MODAL_LEFT: "massedit-modal-left",
 
CLASS_MODAL_OPTION: "massedit-modal-option",
 
CLASS_MODAL_TIMER: "massedit-modal-timer",
 
 
// Modal body classes
 
CLASS_CONTENT_CONTAINER: "massedit-content-container",
 
CLASS_CONTENT_FORM: "massedit-content-form",
 
CLASS_CONTENT_FIELDSET: "massedit-content-fieldset",
 
CLASS_CONTENT_TEXTAREA: "massedit-content-textarea",
 
CLASS_CONTENT_INPUT: "massedit-content-input",
 
CLASS_CONTENT_DIV: "massedit-content-div",
 
CLASS_CONTENT_SPAN: "massedit-content-span",
 
CLASS_CONTENT_SELECT: "massedit-content-select",
 
}),
 
},
 
 
/**
 
* @description This pseudo-enum of the <code>main</code> namespace object
 
* is used to store an array denoting which user groups are permitted to
 
* make use of the script. For the purposes of forstalling the use of the
 
* script for vandalism or spam, its use is limited to certain members of
 
* local staff, various global groups, and Fandom Staff. The major group
 
* prevented from using the script is the local <code>threadmoderator</code>
 
* group, as these can be viewed as standard users with /d and thread-
 
* specific abilities unaffected by MassEdit.
 
*
 
* @readonly
 
* @enum {Array<string>}
 
*/
 
UserGroups: {
 
enumerable: true,
 
writable: false,
 
configurable: false,
 
value: Object.freeze({
 
CAN_EDIT: Object.freeze([
 
"sysop",
 
"content-moderator",
 
"bot",
 
"bot-global",
 
"staff",
 
"vstf",
 
"helper",
 
"vanguard",
 
"wiki-manager",
 
"content-team-member",
 
"content-volunteer",
 
]),
 
}),
 
},
 
 
/**
 
* @description The <code>Utility</code> pseudo-enum of the
 
* <code>main</code> namespace object is used to store several constants of
 
* the <code>number</code> data type related to standard edit interval rates
 
* and edit delays in cases of rate limiting. Originally, these were housed
 
* in a <code>const</code> object in the script-global namespace, though
 
* their exclusive use by the MassEdit class instance made their inclusion
 
* into <code>main</code> seem like a more sensible placement decision.
 
*
 
* @readonly
 
* @enum {number}
 
*/
 
Utility: {
 
enumerable: true,
 
writable: false,
 
configurable: false,
 
value: Object.freeze({
 
MAX_SUMMARY_CHARS: 800,
 
FADE_INTERVAL: 1000,
 
DELAY: 35000,
 
}),
 
 
}
 
}
});
+
window.isMassEditLoaded = true;
   
/****************************************************************************/
+
if (!window.dev || !window.dev.i18n) {
/* Setup pseudo-enums */
+
wk.importArticle({
/****************************************************************************/
+
type: "script",
+
article: "u:dev:MediaWiki:I18n-js/code.js"
// Protected pseudo-enums of script setup object
+
});
Object.defineProperties(init, {
 
 
/**
 
* @description This pseudo-enum of the <code>init</code> namespace object
 
* used to initialize the script stores data related to the external
 
* dependencies and core modules required by the script. It consists of two
 
* properties. The former, a constant <code>object</code> called "SCRIPTS,"
 
* contains key/value pairs wherein the key is the specific name of the
 
* <code>mw.hook</code> and the value is the script's location for use by
 
* <code>importArticles.articles</code>. The latter, a constant array named
 
* <code>MODULES</code>, contains a listing of the core modules required for
 
* use by <code>mw.loader.using</code>.
 
*
 
* @readonly
 
* @enum {object}
 
*/
 
Dependencies: {
 
enumerable: true,
 
writable: false,
 
configurable: false,
 
value: Object.freeze({
 
SCRIPTS: Object.freeze({
 
// Keys should NOT be altered unless hook names change
 
"dev.i18n": "u:dev:MediaWiki:I18n-js/code.js",
 
"dev.placement": "u:dev:MediaWiki:Placement.js",
 
"dev.modal": "u:dev:MediaWiki:Modal.js",
 
}),
 
MODULES: Object.freeze([
 
"mediawiki.util",
 
"mediawiki.user",
 
"ext.wikia.LinkSuggest",
 
]),
 
}),
 
},
 
 
/**
 
* @description This pseudo-enum of the <code>init</code> namespace object
 
* is used to store default data pertaining to the Placement.js external
 
* dependency. It includes an <code>object</code> denoting the default
 
* placement location for the script in the event of the user not including
 
* any user config and an array containing the two valid placement types. By
 
* default, the script tool element as built in <code>main.init</code> is
 
* appended to the user toolbar.
 
*
 
* @readonly
 
* @enum {object}
 
*/
 
Placement: {
 
enumerable: true,
 
writable: false,
 
configurable: false,
 
value: Object.freeze({
 
DEFAULTS: Object.freeze({
 
ELEMENT: "toolbar",
 
TYPE: "append",
 
}),
 
VALID_TYPES: Object.freeze([
 
"append",
 
"prepend",
 
]),
 
}),
 
},
 
 
/**
 
* @description This catchall pseudo-enum of the <code>init</code< namespace
 
* object is used to house assorted values of various data types that don't
 
* fit well into other pseudo-enums. It contains the interval rates
 
* calculated from the edit restrictions imposed upon normal users and bots.
 
* additionally, it contains a <code>string</code> constant denoting the
 
* name of the script.
 
*
 
* @see <a href="https://git.io/fA4Jk">SUS-4775</a>
 
* @see <a href="https://git.io/fA4eQ">VariablesBase.php</a>
 
* @readonly
 
* @enum {string|number}
 
*/
 
Utility: {
 
enumerable: true,
 
writable: false,
 
configurable: false,
 
value: Object.freeze({
 
SCRIPT: "MassEdit",
 
STD_INTERVAL: 1500,
 
BOT_INTERVAL: 750,
 
}),
 
 
}
 
}
});
 
   
/****************************************************************************/
+
// Constant for future testing
/* Prototype utility methods */
+
const DEBUG = false;
/****************************************************************************/
 
   
/**
+
//I18n-js
* @description As the name implies, this helper function capitalizes the
+
var $i18n;
* first character of the input string and returns the altered, adjusted
 
* string. it is generally used in the dynamic construction of i18n messages
 
* in various assembly methods.
 
*
 
* @param {string} paramTarget - <code>string</code> to be capitalized
 
* @returns {string} - Capitalized <code>string</code>
 
*/
 
main.capitalize = function (paramTarget) {
 
return paramTarget.charAt(0).toUpperCase() + paramTarget.slice(1);
 
};
 
 
/**
 
* @description This helper method is used to check whether the target object
 
* is one of several types of <code>object</code>. It is most often used to
 
* determine if the target is an <code>array</code> or a straight-up
 
* <code>object</code>.
 
*
 
* @param {string} paramType - Either "Object" or "Array"
 
* @param {string} paramTarget - Target to check
 
* @returns {boolean} - Flag denoting the nature of the target
 
*/
 
main.isThisAn = function (paramType, paramTarget) {
 
return Object.prototype.toString.call(paramTarget) === "[object " +
 
this.capitalize.call(this, paramType.toLowerCase()) + "]";
 
};
 
 
/**
 
* @description This function is used to determine whether or not the input
 
* <code>string</code> contains restricted characters as denoted by Wikia's
 
* <code>wgLegalTitleChars</code>. Legal characters are defined as follows:
 
* <code> %!"$&'()*,\-./0-9:;=?@A-Z\\\^_`a-z~+\u0080-\uFFFF</code>
 
*
 
* @param {string} paramString Content string to be checked
 
* @return {boolean} - Flag denoting the nature of the paramString
 
*/
 
main.isLegalInput = function (paramString) {
 
return new RegExp("^[" + wk.wgLegalTitleChars + "]*$").test(paramString);
 
};
 
 
/**
 
* @description This helper function uses simple regex to determine whether
 
* the parameter <code>string</code> or <code>number</code> is an integer
 
* value. It is primarily used to determine if the user has inputted a proper
 
* namespace number if mass editing by namespace.
 
*
 
* @param {string|number} paramEntry
 
* @returns {boolean} - Flag denoting the nature of the paramEntry
 
*/
 
main.isInteger = function (paramEntry) {
 
return new RegExp(/^[0-9]+$/).test(paramEntry.toString());
 
};
 
 
/**
 
* @description This function serves as an Internet Explorer-friendly
 
* implementation of <code>String.prototype.startsWith</code>, a method
 
* introduced in ES2015 and unavailable to IE 11 and earlier. It is based off
 
* the polyfill available on the method's Mozilla.org documentation page.
 
*
 
* @param {string} paramTarget - <code>string</code> to be checked
 
* @param {string} paramSearch - <code>string</code> target
 
* @returns {boolean} - Flag denoting a match
 
*/
 
main.startsWith = function (paramTarget, paramSearch) {
 
return paramTarget.substring(0, 0 + paramSearch.length) === paramSearch;
 
};
 
 
/**
 
* @description This utility method is used to check whether the user
 
* attempting to use the script is in the proper usergroup. Only certain local
 
* staff and members of select global groups are permitted the use of this
 
* script so as to prevent potential vandalism.
 
*
 
* @return {boolean} - Flag denoting user's ability to use the script
 
*/
 
main.hasRights = function () {
 
return new RegExp(["(" + this.UserGroups.CAN_EDIT.join("|") + ")"].join("")
 
).test(wk.wgUserGroups.join(" ")) && !mw.user.anonymous();
 
};
 
 
/**
 
* @description This helper function is used as the primary mechanism by which
 
* the find-and-replace operation is undertaken. It can either replace all
 
* instances of a substring from an input parameter or only certain instances
 
* as denoted by an optional parameter array of <code>number</code> indices.
 
* Assuming such a parameter array is passed, a callback function is used with
 
* <code>String.prototype.replace</code> in order to sort through the
 
* appearances of the target in the content and adjust only those at one of
 
* the desired indices. In either case, the ammended string is adjusted and
 
* returned for posting by means of the API as the pages's new adjusted
 
* content.
 
*
 
* @param {string} paramString - Original string to be adjusted
 
* @param {boolean} paramIsCaseSensitive - If case sensitivity is desired
 
* @param {string} paramTarget - Text to be replaced
 
* @param {string} paramReplacement - Text to be inserted
 
* @param {Array<number>} paramInstances - Indices at which to replace
 
* @returns {string} - An ammended <code>string</code>
 
*/
 
main.replaceOccurrences = function (paramString, paramIsCaseSensitive,
 
paramTarget, paramReplacement, paramInstances) {
 
 
// Declarations
 
var counter, regex;
 
 
// Definitions/sanitize params
 
paramInstances = (paramInstances != null) ? paramInstances : [];
 
regex = new RegExp(paramTarget, (paramIsCaseSensitive) ? "g" : "gi");
 
counter = 0;
 
 
// Replace all instances if no specific indices specified
 
return paramString.replace(regex, (!paramInstances.length)
 
? paramReplacement
 
: function (paramMatch) {
 
return ($.inArray(++counter, paramInstances) !== -1)
 
? paramReplacement
 
: paramMatch;
 
}
 
);
 
};
 
 
/****************************************************************************/
 
/* Prototype Dynamic Timer */
 
/****************************************************************************/
 
 
/**
 
* @description This function serves as a pseudo-constructor for the pausable
 
* <code>setDynamicTimeout</code> iterator. It accepts a function as a
 
* callback and an edit interval, setting these as publically accessible
 
* function properties alongside other default flow control
 
* <code>boolean</code>s. The latter are used elsewhere in the program to
 
* determine whether or not event listener handlers can be run, as certain
 
* handlers should not be accessible if an editing operation is in progress.
 
*
 
* @param {function} paramCallback - Function to run after interval complete
 
* @param {number} paramInterval - Rate at which timeout is handled
 
* @returns {object} self - Inner object return for external assignment
 
*/
 
main.setDynamicTimeout = function self (paramCallback, paramInterval) {
 
 
// Define pseudo-instance properties from args
 
self.callback = paramCallback;
 
self.interval = paramInterval;
 
 
// Set flow control booleans
 
self.isPaused = false;
 
self.isComplete = false;
 
 
// Set default value for id
 
self.identify = -1;
 
 
// Begin first iterate and define id
 
self.iterate();
 
 
// Return for definition to local variable
 
return self;
 
};
 
 
/**
 
* @description This internal method of the <code>setDynamicTimeout</code>
 
* function is used to cancel any ongoing editing operation by clearing the
 
* current timeout and setting the <code>isComplete</code> flow control
 
* <code>boolean</code> to true. This lets external handlers know that the
 
* editing operation is complete, enabling or disabling them in turn.
 
*
 
* @returns {void}
 
*/
 
main.setDynamicTimeout.cancel = function () {
 
this.isComplete = true;
 
wk.clearTimeout(this.identify);
 
};
 
 
/**
 
* @description This internal method of the <code>setDynamicTimeout</code>
 
* function is used to pause any ongoing editing operation by setting the
 
* <code>isPaused</code> flow control <code>boolean</code> and clearing the
 
* current <code>setTimeouT</code> identified <code>number</code>. This is
 
* called whenever the user presses the <code>Pause</code> modal button.
 
*
 
* @returns {void}
 
*/
 
main.setDynamicTimeout.pause = function () {
 
if (this.isPaused || this.isComplete) {
 
return;
 
}
 
 
this.isPaused = true;
 
wk.clearTimeout(this.identify);
 
};
 
 
/**
 
* @description This internal method of the <code>setDynamicTimeout</code>
 
* function is used to resume any ongoing and paused editing operation by
 
* setting the <code>isPaused</code> flow control <code>boolean</code> to
 
* <code>false</code> and calling the <code>iterate</code> method to proceed
 
* to the next iteration. It is called when the user presses "Resume."
 
*
 
* @returns {void}
 
*/
 
main.setDynamicTimeout.resume = function () {
 
if (!this.isPaused || this.isComplete) {
 
return;
 
}
 
 
this.isPaused = false;
 
this.iterate();
 
};
 
 
/**
 
* @description This internal method of the <code>setDynamicTimeout</code>
 
* function is used to proceed on to the next iteration by resetting the
 
* <code>identify</code> function property to the value returned by a new
 
* <code>setTimeout</code> invocation. The function accepts as an optional
 
* parameter an interval rate greater than that defined as in the function
 
* instance property <code>interval</code> for cases of ratelimiting. In such
 
* a case, the rate is extended to 35 seconds before the callback is called.
 
*
 
* @param {number} paramInterval - Optional interval rate parameter
 
* @returns {void}
 
*/
 
main.setDynamicTimeout.iterate = function (paramInterval) {
 
if (this.isPaused || this.isComplete) {
 
return;
 
}
 
 
// Interval should only be greater than instance interval
 
paramInterval = (paramInterval < this.interval || paramInterval == null)
 
? this.interval
 
: paramInterval;
 
 
// Define the identifier
 
this.identify = wk.setTimeout(this.callback, paramInterval);
 
};
 
 
/****************************************************************************/
 
/* Prototype API methods */
 
/****************************************************************************/
 
 
/**
 
* @description This function queries the API for member pages of a specific
 
* namespace, the id of which is included as a property of the parameter
 
* <code>object</code>. This argument is merged with the default
 
* <code>$.ajax</code> parameter object and can sometimes include properties
 
* related to <code>query-continue</code> requests for additional members
 
* beyond the default 5000 max. The method returns a resolved
 
* <code>$.Deferred</code> promise for use in attaching related callbacks to
 
* handle the member pages.
 
*
 
* @param {object} paramConfig - <code>object</code> with varying properties
 
* @returns {object} - <code>$.Deferred</code> resolved promise
 
*/
 
main.getNamespaceMembers = function (paramConfig) {
 
return $.ajax({
 
type: "GET",
 
url: mw.util.wikiScript("api"),
 
data: $.extend(false, {
 
token: mw.user.tokens.get("editToken"),
 
action: "query",
 
list: "allpages",
 
aplimit: "max",
 
format: "json",
 
}, paramConfig)
 
});
 
};
 
 
/**
 
* @description This function queries the API for member pages of a specific
 
* category, the id of which is included as a property of the parameter
 
* <code>object</code>. This argument is merged with the default
 
* <code>$.ajax</code> parameter object and can sometimes include properties
 
* related to <code>query-continue</code> requests for additional members
 
* beyond the default 5000 max. The method returns a resolved
 
* <code>$.Deferred</code> promise for use in attaching related callbacks to
 
* handle the member pages.
 
*
 
* @param {object} paramConfig - <code>object</code> with varying properties
 
* @returns {object} - <code>$.Deferred</code> resolved promise
 
*/
 
main.getCategoryMembers = function (paramConfig) {
 
return $.ajax({
 
type: "GET",
 
url: mw.util.wikiScript("api"),
 
data: $.extend(false, {
 
token: mw.user.tokens.get("editToken"),
 
action: "query",
 
list: "categorymembers",
 
cmprop: "title",
 
cmdir: "desc",
 
cmlimit: "max",
 
format: "json",
 
}, paramConfig)
 
});
 
};
 
 
/**
 
* @description This function is used in cases of content find-and-replace to
 
* acquire the parameter page's text content. As with all <code>$.ajax</code>
 
* invocations, it returns a resolved <code>$.Deferred</code> promise for use
 
* in attaching handlers tasked with combing through the page's content once
 
* returned.
 
*
 
* @param {string} paramPage - <code>string</code> title of the page
 
* @returns {object} - <code>$.Deferred</code> resolved promise
 
*/
 
main.getPageContent = function (paramPage) {
 
return $.ajax({
 
type: "GET",
 
url: mw.util.wikiScript("api"),
 
data: {
 
action: "query",
 
prop: "info|revisions",
 
intoken: "edit",
 
titles: paramPage,
 
rvprop: "content|timestamp",
 
rvlimit: "1",
 
indexpageids: "true",
 
format: "json"
 
}
 
});
 
};
 
 
/**
 
* @description This function is the primary means by which all edits are
 
* committed to the database for display on the page. As with several of the
 
* other API methods, this function is passed a config <code>object</code> for
 
* merging with the default API parameter object, with parameter properties
 
* differing depending on the operation being undertaken. Though it makes no
 
* difference for the average editor, the <code>bot</code> property is set to
 
* <code>true</code>. The function returns a resolved <code>$.Deferred</code>
 
* promise for use in attaching handlers post-edit.
 
*
 
* @param {object} paramConfig - <code>object</code> with varying properties
 
* @returns {object} - <code>$.Deferred</code> resolved promise
 
*/
 
main.postPageContent = function (paramConfig) {
 
return $.ajax({
 
type: "POST",
 
url: mw.util.wikiScript("api"),
 
data: $.extend(false, {
 
token: mw.user.tokens.get("editToken"),
 
action: "edit",
 
minor: true,
 
bot: true,
 
format: "json",
 
}, paramConfig)
 
});
 
};
 
 
/**
 
* @description Originally a part of the <code>getMemberPages</code> function,
 
* this method is used to return a <code>$>Deferred</code> object that passes
 
* back either an error message for display in the modal status log or an
 
* array containing wellformed, formatted titles of pages, categories, or
 
* namespaces. If the type of entries contained with the parameter array is
 
* either loose pages or categories, the function checks that their titles are
 
* comprised of legal characters. If the type is namespace, it checks that the
 
* number passed is a legitimate integer. It also prepends the "Category:"
 
* namespace prefix in cases of categories.
 
* <br />
 
* <br />
 
* The function returns a <code>$.Deferred</code> promise instead of an array
 
* due to the function's use in conjunction with <code>getMemberPages</code>
 
* in the body of <code>handleSubmit</code> and due to the desire to only
 
* permit handlers to add log entries and adjust the view. Originally, this
 
* function itself added log entries, which the author felt should be the sole
 
* responsibility of the handlers attached to user-facing modal buttons rather
 
* than helper functions like this and <code>getMemberPages</code>.
 
*
 
* @param {Array<string>} paramEntries - Array of pages/cats/ns
 
* @param {object} $paramType - $ object; loose page, category, or namespace
 
* @returns {object} $deferred - Promise returned for use w/ <code>then</code>
 
*/
 
main.getValidatedEntries = function (paramEntries, $paramType) {
 
 
// Declarations
 
var i, n, entry, results, $deferred, isLoosePages, isCategories,
 
isNamespaces, categoryPrefix;
 
 
// Cache booleans
 
isLoosePages =
 
($paramType.value === "pages") && ($paramType.selectedIndex === 1);
 
isCategories =
 
($paramType.value === "categories") && ($paramType.selectedIndex === 2);
 
isNamespaces =
 
($paramType.value === "namespaces") && ($paramType.selectedIndex === 3);
 
 
// Returnable array of valid pages
 
results = [];
 
 
// Returned $.Deferred
 
$deferred = new $.Deferred();
 
 
// "Category:"
 
categoryPrefix = wk.wgFormattedNamespaces[14] + ":";
 
 
for (i = 0, n = paramEntries.length; i < n; i++) {
 
 
// Cache value to prevent multiple map lookups
 
entry = this.capitalize(paramEntries[i].trim());
 
 
// If category-based and entry name doesn't begin with "Category"
 
if (isCategories && !this.startsWith(entry, categoryPrefix)) {
 
entry = categoryPrefix + entry;
 
}
 
 
// If legal page/category name, push into names array
 
if (
 
((isCategories || isLoosePages) && this.isLegalInput(entry)) ||
 
(isNamespaces && this.isInteger(entry))
 
) {
 
results.push(entry);
 
} else {
 
// Error: Use of some characters is prohibited for security reasons.
 
return $deferred.reject("modalSecurity");
 
}
 
}
 
 
if (!results.length) {
 
// Error: No wellformed pages found
 
$deferred.reject("noMembers");
 
} else {
 
$deferred.resolve(results);
 
}
 
 
return $deferred.promise();
 
};
 
 
/**
 
* @description This function is used to return a jQuery <code>Deferred</code>
 
* object providing a <code>then</code> or <code>always</code> invocation with
 
* an array of wellformed pages for editing. It accepts as input an array
 
* containing titles of either categories or namespaces from which to
 
* acquire member pages. In such cases, a number of API calls are made
 
* requesting the relevant members pages contained in the input categories or
 
* namespaces. These are checked and pushed into an entries array. Once
 
* complete, the entries array is returned by means of a resolved
 
* <code>Deferred.prototype.promise</code>.
 
* <br />
 
* <br />
 
* Originally, this function also served to validate loose pages passed in the
 
* parameter array, running them against the legl characters and returning the
 
* <code>entries</code> array for use. However, per the single responsibility
 
* principle, this functionality was eventually removed into a separate method
 
* called <code>getValidatedEntries</code> that is called by this method to
 
* ensure that the category/namespace titles are wellformed prior to making
 
* API queries.
 
*
 
* @param {Array<string>} paramEntries - Array of user input pages
 
* @param {object} $paramType - <code>jQuery</code> object, cat or ns
 
* @returns {object} $returnPages - $.Deferred promise object
 
*/
 
main.getMemberPages = function (paramEntries, $paramType) {
 
 
// Declarations
 
var i, n, names, data, entries, parameters, isCategories, isNamespaces,
 
counter, config, $getPages, $addPages, $getEntries, $returnPages;
 
 
// New pending Deferred objects
 
$returnPages = new $.Deferred();
 
$addPages = new $.Deferred();
 
 
// Iterator index for setTimeout
 
counter = 0;
 
 
// getCategoryMembers or getNamespaceMembers param object
 
parameters = {};
 
 
// Arrays
 
names = []; // Store names of user entries
 
entries = []; // New entries to be returned
 
 
// Cached booleans
 
isCategories =
 
($paramType.value === "categories") && ($paramType.selectedIndex === 2);
 
isNamespaces =
 
($paramType.value === "namespaces") && ($paramType.selectedIndex === 3);
 
 
config = {
 
2: {
 
query: "categorymembers",
 
handler: "getCategoryMembers",
 
continuer: "cmcontinue",
 
target: "cmtitle",
 
},
 
3: {
 
query: "allpages",
 
handler: "getNamespaceMembers",
 
continuer: "apfrom",
 
target: "apnamespace",
 
}
 
}[$paramType.selectedIndex];
 
 
// Get wellformed, formatted namespace numbers or category names
 
$getEntries = this.getValidatedEntries(paramEntries, $paramType);
 
 
// Once acquired, apply to names array or pass along rejection message
 
$getEntries.then(function (paramResults) {
 
names = paramResults;
 
}, $returnPages.reject.bind($));
 
 
// Iterate over user input entries
 
this.timer = this.setDynamicTimeout(function () {
 
if (counter === names.length) {
 
$addPages.resolve();
 
 
if (entries.length) {
 
return $returnPages.resolve(entries).promise();
 
} else {
 
// Error: No wellformed pages found
 
return $returnPages.reject("noMembers").promise();
 
}
 
}
 
 
// Set parameter target page
 
parameters[config.target] = names[counter];
 
 
// Fetching member pages of $1
 
$returnPages.notify("fetchingMembers", names[counter]);
 
 
// Acquire member pages of cat or ns
 
$getPages = $.when(this[config.handler](parameters));
 
 
// Once acquired, add pages to array
 
$getPages.always($addPages.notify);
 
 
}.bind(this), this.interval);
 
   
 
/**
 
/**
* @description Once the member pages from the specific category or
+
* @class MassEdit
* namespace have been returned following a successful API query, the
+
* @classdesc The central MassEdit class
* $addPages <code>$.Deferred</code> is notified, allowing for this callback
 
* function to sanitize the returned data and push the wellformed member
 
* page titles into the <code>entries</code> array. If there are still
 
* remaining pages as indicated by a "query-continue" property, the counter
 
* is left unincremented and the relevant continuer parameter added to the
 
* <code>parameters</code> object. In any case, the function ends with a
 
* call to iterate the timer.
 
 
*/
 
*/
$addPages.progress(function (paramResults, paramStatus, paramXHR) {
+
var MassEdit = {
if (DEBUG) {
+
meta: {
console.log(paramResults, paramStatus, paramXHR);
+
author: "User:Eizen",
}
+
created: "05/02/17",
  +
lastEdit: "03/09/18",
  +
version: "2.4.4"
  +
},
  +
hasRights: new RegExp(["(sysop|content-moderator|bot|bot-global" +
  +
"|staff|vstf|helper|global-discussions-moderator|content-team-member" +
  +
"|content-volunteer|wiki-manager)"].join("")).test(wk.wgUserGroups.join(" ")),
  +
legalChars: new RegExp("^[" + wk.wgLegalTitleChars + "]*$"),
   
if (paramStatus !== "success" || paramXHR.status !== 200) {
+
/**
// Error: Unable to acquire pages of $1
+
* @method addLogEntry
$returnPages.notify("failedRequest", names[counter++]);
+
* @description Method allows for quick adding of a MassEdit log entry
return this.timer.iterate();
+
* to the appropriate text field.
}
+
* @param {String} $key - The name of the JSON field
  +
* @returns {void}
  +
*/
  +
addLogEntry: function ($key) {
  +
jQuery("#massEdit-log").prepend($i18n.msg($key).escape() + "<br/>");
  +
},
   
// Define data
+
/**
data = paramResults.query[config.query];
+
* @method addLogEntry
  +
* @description "Overloaded" two-parameter variation of the method above
  +
* that accepts a JSON key and an entry substitute for $1
  +
* or $2.
  +
* @param {String} $key - The name of the JSON field
  +
* @param {String} $entry - Name of the page to substitute
  +
* @returns {void}
  +
*/
  +
addLogEntry: function ($key, $entry) {
  +
jQuery("#massEdit-log").prepend(
  +
$i18n.msg($key, $entry).escape() + "<br />");
  +
},
   
// If page doesn't exist, add log entry and continue to next iteration
+
/**
if (data == null || data.length === 0) {
+
* @method constructItem
// Error: $1 does not exist.
+
* @description Method returns a completed <code>String</code>
$returnPages.notify("noSuchPage", names[counter++]);
+
* representing the menu link item. Is comprised of a
return this.timer.iterate();
+
* link inside a list item.
}
+
* @param {String} $text - Text to be displayed in the item and title
  +
* @returns {String}
  +
*/
  +
constructItem: function ($text) {
  +
return mw.html.element("li", {
  +
"class": "overflow",
  +
"id": "massEdit-li"
  +
}, new mw.html.Raw(
  +
mw.html.element("a", {
  +
"id": "massEdit-a",
  +
"href": "#",
  +
"title": $text
  +
}, $text)
  +
));
  +
},
   
// Add extant page titles to the appropriate submission property
+
/**
for (i = 0, n = data.length; i < n; i++) {
+
* @method isLegalPage
entries.push(data[i].title);
+
* @description Utility function used to test if inputted page name
}
+
* matches the legal characters regex. Returns a boolean
  +
* flag depending on result.
  +
* @param {String} $page - Inputted page name
  +
* @returns {boolean}
  +
*/
  +
isLegalPage: function ($page) {
  +
if (!this.legalChars.test($page)) {
  +
jQuery("#massEdit-modal-form")[0].reset();
  +
this.addLogEntry("modalSecurity");
  +
return false;
  +
} else {
  +
return true;
  +
}
  +
},
   
// Only iterate counter if current query has no more extant pages
+
/**
if (
+
* @method displayModal
paramResults["query-continue"] ||
+
* @description Method constructs and displays the main user interface.
paramResults.hasOwnProperty("query-continue")
+
* Injects custom CSS prior to construction and handles all
) {
+
* button click events.
parameters[config.continuer] =
+
* @param {String} $modalHTML - The modal HTML layout
paramResults["query-continue"][config.query][config.continuer];
+
* @returns {void}
} else {
+
*/
parameters = {};
+
displayModal: function ($modalHTML) {
counter++;
+
var that = this;
}
 
   
// On to the next iteration
+
mw.util.addCSS(
return this.timer.iterate();
+
".massEdit-menu," +
}.bind(this));
+
".massEdit-textbox {" +
  +
"width: 100%;" +
  +
"padding: 0;" +
  +
"}" +
  +
".massEdit-textarea {" +
  +
"height: 50px;" +
  +
"width: 100%;" +
  +
"padding: 0;" +
  +
"overflow: auto;" +
  +
"}" +
  +
"#massEdit-log {" +
  +
"height: 55px;" +
  +
"width: 100%;" +
  +
"border: 1px solid;" +
  +
"font-family: monospace;" +
  +
"background: #fff;" +
  +
"color: #aeaeae;" +
  +
"overflow: auto;" +
  +
"padding:0;" +
  +
"}"
  +
);
   
return $returnPages.promise();
+
ui.init(["modal"]).then(function (modal) {
};
+
var config = {
  +
vars: {
  +
id: "massEdit-modal",
  +
size: "medium",
  +
title: $i18n.msg("itemTitle").escape(),
  +
content: $modalHTML,
  +
buttons: [{
  +
vars: {
  +
value: $i18n.msg("buttonCancel").escape(),
  +
classes: ["normal", "primary"],
  +
data: [{
  +
key: "event",
  +
value: "cancel"
  +
}]
  +
}
  +
}, {
  +
vars: {
  +
value: $i18n.msg("buttonClear").escape(),
  +
classes: ["normal", "primary"],
  +
data: [{
  +
key: "event",
  +
value: "clear"
  +
}]
  +
}
  +
}, {
  +
vars: {
  +
value: $i18n.msg("buttonSubmit").escape(),
  +
classes: ["normal", "primary"],
  +
data: [{
  +
key: "event",
  +
value: "submit"
  +
}]
  +
}
  +
}]
  +
}
  +
};
   
/****************************************************************************/
+
modal.createComponent(config, function (massEditModal) {
/* Prototype assemblers */
+
massEditModal.bind("cancel", function () {
/****************************************************************************/
+
massEditModal.trigger("close");
  +
});
   
/**
+
massEditModal.bind("clear", function () {
* @description This function is a simple recursive <code>string</code> HTML
+
jQuery("#massEdit-modal-form")[0].reset();
* generator that makes use of <code>mw.html</code>'s assembly methods to
+
});
* construct wellformed HTML strings from a set of nested input arrays. This
 
* allows for a more readable means of producing proper HTML than the default
 
* <code>jQuery</code> approach or the hardcoded HTML <code>string</code>
 
* approach employed in earlier iterations of this script. Through the use of
 
* nested arrays, this function permits the laying out of parent/child DOM
 
* nodes in array form in a fashion similar to actual HTML, enhancing both
 
* readability and usability.
 
* <br />
 
* <br />
 
* Furthermore, as the <code>assembleElement</code> function returns a
 
* <code>string</code>, nested invocations of the method within parameter
 
* arrays is permitted, as evidenced in certain, more specialized assembly
 
* methods elsewhere in the script.
 
* <br />
 
* <br />
 
* An example of wellformed input is shown below:
 
* <br />
 
* <pre>
 
* this.assembleElement(
 
* ["div", {id: "foo-id", class: "foo-class"},
 
* ["button", {id: "bar-id", class: "bar-class"},
 
* "Button text",
 
* ],
 
* ["li", {class: "overflow"},
 
* ["a", {href: "#"},
 
* "Link text",
 
* ],
 
* ],
 
* ],
 
* );
 
* </pre>
 
*
 
* @param {Array<string>} paramArray - Wellformed array representing DOM nodes
 
* @returns {string} - Assembled <code>string</code> HTML
 
*/
 
main.assembleElement = function (paramArray) {
 
   
// Declarations
+
massEditModal.bind("submit", function () {
var type, attributes, counter, content;
+
that.main();
  +
});
   
// Make sure input argument is a well-formatted array
+
massEditModal.show();
if (!this.isThisAn("Array", paramArray)) {
+
});
return this.assembleElement.call(this,
+
});
Array.prototype.slice.call(arguments));
 
}
 
   
// Definitions
+
// Disable replacements menu depending on current selected option
counter = 0;
+
jQuery(document).on("change", "#massEdit-actionType", function () {
content = "";
+
if (jQuery("#massEdit-actionType").val() === "replace") {
type = paramArray[counter++];
+
jQuery("#massEdit-replaceThis-value")
+
.prop("disabled", false);
// mw.html.element requires an object for the second param
+
} else {
attributes = (this.isThisAn("Object", paramArray[counter]))
+
jQuery("#massEdit-replaceThis-value")
? paramArray[counter++]
+
.prop("disabled", true);
: {};
+
}
+
});
while (counter < paramArray.length) {
+
},
 
// Check if recursive assembly is required for another inner DOM element
 
content += (this.isThisAn("Array", paramArray[counter]))
 
? this.assembleElement(paramArray[counter++])
 
: paramArray[counter++];
 
}
 
   
return mw.html.element(type, attributes, new mw.html.Raw(content));
+
/**
};
+
* @method getContent
  +
* @description This method retrieves the content of the inputted page,
  +
* including information about its time of creation and
  +
* relevant timestamp info. Used exclusively by the find
  +
* and dropdown option.
  +
* @param {String} $action - Editing action (prepend, append, replace)
  +
* @param {String} $editSummary - Edit summary
  +
* @param {String} $page - The page in question
  +
* @param {String} $newContent - New text to replace the target
  +
* @param {String} $replace - The target to be replaced in the callback
  +
* @param {function} callback - The callback handler
  +
*/
  +
getContent: function ($action, $editSummary, $page, $newContent,
  +
$replace, callback) {
   
/**
+
var that = this;
* @description This specialized assembly function is used to create a tool
+
this.api.get({
* link to inclusion at the location specified via the <code>placement</code>
+
action: "query",
* instance property. Like the <code>overflow</code> toolbar button on which
+
prop: "info|revisions",
* it is based, the element (in <code>string</code> form) returned from this
+
intoken: "edit",
* function constitutes a link element enclosed within a list element.
+
titles: $page,
*
+
rvprop: "content|timestamp",
* @param {string} paramText - Title/item text <code>string</code>
+
rvlimit: "1",
* @returns {string} - Assembled <code>string</code> HTML
+
indexpageids: "true",
*/
+
format : "json"
main.assembleOverflowElement = function (paramText) {
+
}).done(function ($data) {
return this.assembleElement(
+
if (!$data.error) {
["li", {
+
callback( // handleContent()
"class": this.Selectors.CLASS_PLACEMENT_OVERFLOW,
+
that, // this
"id": this.Selectors.ID_PLACEMENT_LIST,
+
$action, // "replace"
},
+
$editSummary, // "#massEdit-summary-value"
["a", {
+
$data, // $data
"id": this.Selectors.ID_PLACEMENT_LINK,
+
$page, // $pagesArray[$counter]
"href": "#",
+
$newContent, // "#massEdit-content-value"
"title": paramText,
+
$replace // "#massEdit-replaceThis-value"
  +
);
  +
}
  +
});
 
},
 
},
paramText,
 
],
 
]
 
);
 
};
 
   
/**
+
/**
* @description This function is one of two similar specialized assembly
+
* @method handleContent
* functions used to automate the construction of several reoccuring
+
* @description Callback function for <code>getContent</code>. Sifts
* components in the modal content body. This function builds two types of
+
* through included data and passes relevant bits to the
* textfield, namely <code>input</code>s and <code>textarea</code>s. The
+
* <code>editPage</code> method. Used exclusively by the
* components may be disabled at creation via parameter <code>boolean</code>.
+
* find and replace dropdown option.
*
+
* @param {this} that - Scope variable
* @param {string} paramName - Name for message, id/classname generation
+
* @param {String} $action - Editing action (prepend, append, replace)
* @param {string} paramType - <code>input</code> or <code>textarea</code>
+
* @param {String} $editSummary - Edit summary
* @param {boolean} paramIsDisabled - Whether to disable the node on creation
+
* @param {JSON} $data - Passed data from <code>getContent</code>
* @returns {string} - Assembled <code>string</code> HTML
+
* @param {String} $page - Specific page in question
*/
+
* @param {String} $newContent - New text to replace the target
main.assembleTextfield = function (paramName, paramType, paramIsDisabled) {
+
* @param {String} $replaceThis - Text to be replaced by $newContent
  +
* @returns {void}
  +
*/
  +
handleContent: function (that, $action, $editSummary, $data, $page,
  +
$newContent, $replaceThis) {
   
// Declarations
+
// Check if page actually exists
var elementId, elementClass, message, attributes;
+
if (Object.keys($data.query.pages)[0] === "-1") {
  +
jQuery("#massEdit-modal-form")[0].reset();
  +
that.addLogEntry("noSuchPage", $page);
  +
return;
  +
}
   
// Sanitize parameters
+
var $newText;
paramName = paramName.toLowerCase();
+
var $result = $data.query.pages[Object.keys($data.query.pages)[0]];
paramType = paramType.toLowerCase();
+
var $text = $result.revisions[0]["*"];
  +
var $timestamp = $result.revisions[0].timestamp;
  +
var $starttimestamp = $result.starttimestamp;
  +
var $token = $result.edittoken;
   
// Definitions
+
// Replace all instances of chosen text with inputted new text
elementId = "ID_CONTENT_" + paramName.toUpperCase();
+
$newText = $text.split($replaceThis).join($newContent);
elementClass = "CLASS_CONTENT_" + paramType.toUpperCase();
 
message = "modal" + this.capitalize(paramName);
 
   
attributes = {
+
// Check if old & new revisions are identical in content
id: this.Selectors[elementId],
+
if ($newText === $text) {
class: this.Selectors[elementClass],
+
jQuery("#massEdit-log").prepend(
placeholder: this.i18n.msg(message + "Placeholder").plain(),
+
$i18n.msg("noMatch", $replaceThis, $page).escape() +
disabled: paramIsDisabled || false,
+
"<br />"
};
+
);
+
} else {
if (paramType === "input") {
+
that.editPage(that, $page, $newText, $action, $editSummary,
attributes.type = "textbox";
+
$timestamp, $starttimestamp, $token);
}
+
}
 
return this.assembleElement(
 
["div", {class: this.Selectors.CLASS_CONTENT_DIV},
 
["span", {class: this.Selectors.CLASS_CONTENT_SPAN},
 
this.i18n.msg(message + "Title").escape(),
 
],
 
[paramType, attributes],
 
]
 
);
 
};
 
 
/**
 
* @description This function is one of two similar specialized assembly
 
* functions used to automate the construction of several reoccuring
 
* components in the modal content body. This function is used to build
 
* dropdown menus from a default value and an array of required
 
* <code>option</code>s.
 
*
 
* @param {string} paramName - <code>string</code> name of the dropdown
 
* @param {string} paramDefault - First dropdown option (default)
 
* @param {Array<string>} paramValues - Array of dropdown options
 
* @param {boolean} paramIsDisabled - Optional param denoting disabled status
 
* @returns {string} - Assembled <code>string</code> HTML
 
*/
 
main.assembleDropdown = function (paramName, paramDefault, paramValues,
 
paramIsDisabled) {
 
 
// Declarations
 
var i, n, titleMsg, options, value, selectId;
 
 
// Sanitize input
 
paramName = paramName.toLowerCase();
 
 
// Define select element ID
 
selectId = "ID_CONTENT_" + paramName.toUpperCase();
 
 
// Listing of selectable dropdown options
 
options = "";
 
 
// Message used in title and default dropdown option
 
titleMsg = this.i18n.msg(paramDefault).escape();
 
 
// Assemble array of HTML option strings
 
for (i = 0, n = paramValues.length; i < n; i++) {
 
 
// Sanitize parameter
 
value = paramValues[i].toLowerCase();
 
 
options += this.assembleElement(
 
["option", {value: value},
 
this.i18n.msg("dropdown" + this.capitalize(value)).escape(),
 
]
 
);
 
}
 
 
return this.assembleElement(
 
["div", {class: this.Selectors.CLASS_CONTENT_DIV},
 
["span", {class: this.Selectors.CLASS_CONTENT_SPAN},
 
titleMsg,
 
],
 
["select", {
 
size: "1",
 
name: paramName,
 
id: this.Selectors[selectId],
 
class: this.Selectors.CLASS_CONTENT_SELECT,
 
disabled: paramIsDisabled || false,
 
 
},
 
},
["option", {selected: ""},
 
titleMsg,
 
],
 
options,
 
],
 
]
 
);
 
};
 
   
/****************************************************************************/
+
/**
/* Prototype modal methods */
+
* @method editPage
/****************************************************************************/
+
* @description The one-size-fits-all editing handler for use by all
  +
* three main MassEdit functions. Takes several different
  +
* numbers of input parameters depending on the action to
  +
* be taken by the handler.
  +
* @param {this} that - Scope variable
  +
* @param {String} $page - The page to be edited
  +
* @param {String} $content - The content to be added to the page
  +
* @param {String} $action - Editing action (prepend, append, replace)
  +
* @param {String} $editSummary - Edit summary
  +
* @param {String} $timestamp - Optional, for replace option only
  +
* @param {String} $starttimestamp - Optional, for replace option only
  +
* @param {String} $token - Optional, for replace option only
  +
* @returns {void}
  +
*/
  +
editPage: function (that, $page, $content, $action, $editSummary,
  +
$timestamp, $starttimestamp, $token) {
   
/**
+
// Edit without editing; for testing purposes
* @description This one-size-fits-all helper function is used to log entries
+
if (DEBUG) {
* in the status log on the completion of some operation or other. Originally,
+
that.addLogEntry("editSuccess", $page);
* three separate loggers were used following a Java-esque method overloading
+
return;
* approach. However, this was eventually abandoned in favor of a single
+
}
* method that takes an indeterminate number of arguments at any time.
 
*
 
* @returns {void}
 
*/
 
main.addModalLogEntry = function () {
 
$("#" + this.Selectors.ID_CONTENT_LOG).prepend(
 
this.i18n.msg.apply(this,
 
(arguments.length === 1 && arguments[0] instanceof Array)
 
? arguments[0]
 
: Array.prototype.slice.call(arguments)
 
).escape() + "<br />");
 
};
 
   
/**
+
// Default base properties
* @description This helper function is a composite of several previously
+
var $params = {
* extant shorter utility functions used to reset the form element,
+
action: "edit",
* enable/disable various modal buttons, and log messages. It is called in a
+
minor: true,
* variety of contexts at the close of editing operations,
+
bot: true,
* failed API requests, and the like. Though it does not accept any formal
+
title: $page,
* parameters, it does permit an indeterminate number of arguments to be
+
summary: $editSummary
* passed if the invoking function wishes to log a status message. In such
+
};
* cases, the collated arguments are bound to a shallow array and passed to
 
* <code>addModalLogEntry</code> for logging.
 
*
 
* @returns {void}
 
*/
 
main.resetModal = function () {
 
   
// Cancel the extant timer if applicable
+
// Set additional Object properties depending on action to be taken
if (this.timer && !this.timer.isComplete) {
+
switch ($action) {
this.timer.cancel();
+
case "prepend":
}
+
$params.prependtext = $content;
  +
$params.token = mw.user.tokens.get("editToken");
  +
break;
  +
case "append":
  +
$params.appendtext = $content;
  +
$params.token = mw.user.tokens.get("editToken");
  +
break;
  +
case "replace":
  +
$params.text = $content;
  +
$params.basetimestamp = $timestamp;
  +
$params.startimestamp = $starttimestamp;
  +
$params.token = $token;
  +
break;
  +
}
   
// Add log message if i18n parameters passed
+
that.api.post($params).done(function ($data) {
if (arguments.length) {
+
jQuery("#massEdit-modal-form")[0].reset();
this.addModalLogEntry(Array.prototype.slice.call(arguments));
+
if (!$data.error) {
}
+
that.addLogEntry("editSuccess", $page);
+
} else {
// Reset the form
+
that.addLogEntry("editFailure", $page);
$("#" + this.Selectors.ID_CONTENT_FORM)[0].reset();
+
}
+
});
// Re-enable modal buttons and fieldset
 
this.toggleModalComponentsDisable(false, "modal");
 
};
 
 
/**
 
* @description This helper function is used to disable certain elements and
 
* enable others depending on the operation being performed. It is used
 
* primarily during editing to disable one of several element groups related
 
* to either replace fields or the fieldset/modal buttons in order to prevent
 
* illegitimate mid-edit changes to input. If the fieldset, etc. is disabled,
 
* the method enables the buttons related to pausing and canceling the editing
 
* operation, and vice versa.
 
*
 
* @param {boolean} paramValue - Whether or not the form/fieldset is disabled
 
* @param {string} paramTargetGroup - Group to toggle ("modal" or "replace")
 
* @returns {void}
 
*/
 
main.toggleModalComponentsDisable = function (paramValue, paramTargetGroup) {
 
 
// Declarations
 
var i, n, groupSet, current;
 
 
// Sanitize input
 
paramTargetGroup = paramTargetGroup.toLowerCase();
 
 
// Elements to disable/enable
 
groupSet = {
 
modal :[
 
{
 
target: "#" + this.Selectors.ID_CONTENT_FIELDSET,
 
value: paramValue,
 
 
},
 
},
{
 
target: "." + this.Selectors.CLASS_MODAL_OPTION,
 
value: paramValue,
 
},
 
{
 
target: "." + this.Selectors.CLASS_MODAL_TIMER,
 
value: !paramValue,
 
},
 
],
 
replace: [
 
{
 
target: "#" + this.Selectors.ID_CONTENT_CASE,
 
value: paramValue,
 
},
 
{
 
target: "#" + this.Selectors.ID_CONTENT_REPLACE,
 
value: paramValue,
 
},
 
{
 
target: "#" + this.Selectors.ID_CONTENT_INDICES,
 
value: paramValue,
 
}
 
]
 
}[paramTargetGroup];
 
   
for (i = 0, n = groupSet.length; i < n; i++) {
+
/**
current = groupSet[i];
+
* @method getNamespaceMembers
+
* @description As the name implies, this method returns the results of
$(current.target).prop("disabled", current.value);
+
* a request for an inputted namespace's associated page
}
+
* contents.
};
+
* @param {String} $namespace - Number of the namespace in question
+
* @return {JSON}
/**
+
*/
* @description This method calls <code>assembleElement</code> and its various
+
getNamespaceMembers: function ($namespace) {
* specialized cousins to assemble the complete modal content body HTML in its
+
return jQuery.ajax({
* <code>string</code> form. In previous incarnations of this script, this
+
type: "GET",
* method would have simply returned a large <code>string</code> of
+
url: mw.util.wikiScript("api"),
* preassembled HTML, this approach allows for a more readable design that
+
data: {
* can be more easily extended or expanded in future without the need to mess
+
action: "query",
* around with hardcoded HTML <code>string</code>s. The contents of this
+
list: "allpages",
* method are applied in the construction of the new <code>Modal</code> class
+
aplimit: "max",
* instance in the body of <code>buildModal</code>.
+
apnamespace: $namespace,
*
+
format: "json"
* @returns {string} - Assembled <code>string</code> HTML
+
}
*/
+
});
main.buildModalContent = function () {
 
 
// Declarations
 
var i, j, m, n, object, arrays, data, elements;
 
 
// Dataset holding assembler name and set of arguments in array form
 
data = [
 
{ // Assembles three dropdown menus
 
handler: "assembleDropdown",
 
parameterArrays: [
 
["action", "modalSelect", ["prepend", "append", "replace"]],
 
["type", "modalContentType", ["pages", "categories", "namespaces"]],
 
["case", "modalCaseSensitivity", ["sensitive", "insensitive"], true],
 
]
 
},
 
{ // Assembles five textfields (3 textareas, 2 inputs)
 
handler: "assembleTextfield",
 
parameterArrays: [
 
["replace", "textarea", true],
 
["indices", "input", true],
 
["content", "textarea"],
 
["pages", "textarea"],
 
["summary", "input"],
 
]
 
}
 
];
 
 
// Using data array, create 5 textarea/inputs and 2 dropdowns as a string
 
elements = "";
 
for (i = 0, m = data.length; i < m; i++) {
 
object = data[i];
 
arrays = object.parameterArrays;
 
for (j = 0, n = arrays.length; j < n; j++) {
 
elements += this[object.handler].apply(this, arrays[j]);
 
}
 
}
 
 
return this.assembleElement(
 
["section", {
 
id: this.Selectors.ID_CONTENT_CONTAINER,
 
class: this.Selectors.CLASS_CONTENT_CONTAINER,
 
},
 
["form", {
 
id: this.Selectors.ID_CONTENT_FORM,
 
class: this.Selectors.CLASS_CONTENT_FORM,
 
 
},
 
},
["fieldset", {id: this.Selectors.ID_CONTENT_FIELDSET},
 
elements,
 
],
 
["hr"],
 
],
 
["div", {class: this.Selectors.CLASS_CONTENT_DIV},
 
["span", {class: this.Selectors.CLASS_CONTENT_SPAN},
 
this.i18n.msg("modalLog").escape(),
 
],
 
["div", {
 
id: this.Selectors.ID_CONTENT_LOG,
 
class: this.Selectors.CLASS_CONTENT_DIV,
 
}],
 
],
 
]
 
);
 
};
 
   
/**
+
/**
* @description This method is used both to inject the requisite CSS styling
+
* @method getCategoryMembers
* governing the appearance of the modal and to build a new <code>Modal</code>
+
* @description As the name implies, this method returns the results of
* class instance itself. While the styles could be stored in a dedicated
+
* a request for an inputted category's associated page
* <code>.css</code> file on Dev, keeping them here would more easily handle
+
* contents.
* selector name changes due to the use of a <code>selectors</code> object
+
* @param {String} $category - Name of the category in question
* collating all ids and classes evidenced in the modal in a single place.
+
* @return {JSON}
*
+
*/
* @returns {object} - A new <code>Modal</code> instance
+
getCategoryMembers: function ($category) {
*/
+
return jQuery.ajax({
main.buildModal = function () {
+
type: "GET",
mw.util.addCSS(
+
url: mw.util.wikiScript("api"),
"." + this.Selectors.CLASS_CONTENT_CONTAINER + " {" +
+
data: {
"margin: auto;" +
+
action: "query",
"position: relative;" +
+
list: "categorymembers",
"width: 96%;" +
+
cmtitle: $category,
"}" +
+
cmprop: "title",
"." + this.Selectors.CLASS_CONTENT_SELECT + "," +
+
cmdir: "desc",
"." + this.Selectors.CLASS_CONTENT_TEXTAREA + "," +
+
cmlimit: "max",
"." + this.Selectors.CLASS_CONTENT_INPUT + " {" +
+
format: "json"
"width: 99.6%;" +
+
}
"padding: 0;" +
+
});
"resize: none;" +
 
"}" +
 
"." + this.Selectors.CLASS_CONTENT_TEXTAREA + " {" +
 
"height: 45px;" +
 
"}" +
 
"#" + this.Selectors.ID_CONTENT_LOG + " {" +
 
"height: 45px;" +
 
"width: 99.6%;" +
 
"border: 1px solid;" +
 
"font-family: monospace;" +
 
"background: #FFFFFF;" +
 
"color: #AEAEAE;" +
 
"overflow: auto;" +
 
"padding: 0;" +
 
"}" +
 
"." + this.Selectors.CLASS_MODAL_BUTTON + "{" +
 
"margin-left: 5px !important;" +
 
"font-size: 8pt;" +
 
"}" +
 
"." + this.Selectors.CLASS_MODAL_LEFT + "{" +
 
"float: left !important;" +
 
"margin-left: 0px !important;" +
 
"margin-right: 5px;" +
 
"}"
 
);
 
 
return new wk.dev.modal.Modal({
 
content: this.buildModalContent(),
 
id: this.Selectors.ID_MODAL_CONTAINER,
 
size: "medium",
 
title: this.i18n.msg("itemTitle").escape(),
 
events: {
 
submit: this.handleSubmit.bind(this),
 
toggle: this.handleToggle.bind(this),
 
clear: this.handleClear.bind(this),
 
cancel: this.handleCancel.bind(this),
 
},
 
buttons: [
 
{
 
text: this.i18n.msg("buttonSubmit").escape(), // Submit
 
event: "submit",
 
primary: true,
 
id: this.Selectors.ID_MODAL_SUBMIT,
 
classes: [
 
this.Selectors.CLASS_MODAL_BUTTON,
 
this.Selectors.CLASS_MODAL_OPTION,
 
],
 
 
},
 
},
{
 
text: this.i18n.msg("buttonPause").escape(), // Pause
 
event: "toggle",
 
disabled: true,
 
primary: true,
 
id: this.Selectors.ID_MODAL_TOGGLE,
 
classes: [
 
this.Selectors.CLASS_MODAL_BUTTON,
 
this.Selectors.CLASS_MODAL_TIMER,
 
],
 
},
 
{
 
text: this.i18n.msg("buttonCancel").escape(), // Cancel
 
event: "cancel",
 
disabled: true,
 
primary: true,
 
id: this.Selectors.ID_MODAL_CANCEL,
 
classes: [
 
this.Selectors.CLASS_MODAL_BUTTON,
 
this.Selectors.CLASS_MODAL_TIMER,
 
],
 
},
 
{
 
text: this.i18n.msg("buttonClose").escape(), // Close
 
event: "close",
 
id: this.Selectors.ID_MODAL_CLOSE,
 
classes: [
 
this.Selectors.CLASS_MODAL_BUTTON,
 
this.Selectors.CLASS_MODAL_LEFT,
 
this.Selectors.CLASS_MODAL_OPTION,
 
],
 
},
 
{
 
text: this.i18n.msg("buttonClear").escape(), // Clear
 
event: "clear",
 
id: this.Selectors.ID_MODAL_CLEAR,
 
classes: [
 
this.Selectors.CLASS_MODAL_BUTTON,
 
this.Selectors.CLASS_MODAL_LEFT,
 
this.Selectors.CLASS_MODAL_OPTION,
 
],
 
},
 
],
 
});
 
};
 
   
/**
+
/**
* @description This method is the primary mechanism by which the modal is
+
* @method membersHandler
* displayed to the user. If the modal has not been previously assembled, the
+
* @description This method, inspired by Java-style reflection, is a
* function constructs a new <code>Modal</code> instance via an invocation of
+
* general purpose handler for use by both namespace and
* <code>buildModal</code>, creates the modal, and attaches all the requisite
+
* category bulk editing. The method accepts either the
* event listeners related to enabling <code>linksuggest</code> and find-and-
+
* <tt>getCategoryMembers</tt> or
* replace-specific modal elements (linksuggest is enabled for the content
+
* <tt>getNamespaceMembers<tt> methods as the requests of
* <code>textarea</code> and the edit summary <code>input</code>).
+
* choice, running basically the same process with the
* <br />
+
* results of each once all calls have been completed.
* <br />
+
* <br />
* Once all listeners have been attached, the new modal is displayed to the
+
* <br />
* user. If the modal has been assembled prior to method invocation, the
+
* Basically, once the calls have been made, the method
* instance is displayed to the user and the method exited.
+
* assembles all the pages from the request results and
*
+
* pushes them into an array for processing by the
* @returns {void}
+
* <tt>actionHandler</tt> method.
*/
+
* @param {String[]} $inputArray
main.displayModal = function () {
+
* @param {String} $newContent
if (this.modal != null) {
+
* @param {String} $toReplace
this.modal.show();
+
* @param {int} $actionIndex
return;
+
* @param {String} $action
}
+
* @param {String} $editSummary
  +
* @param {function} getMembers
  +
* @param {String} $queryProperty
  +
* @returns {void}
  +
*/
  +
membersHandler: function ($inputArray, $newContent, $toReplace,
  +
$actionIndex, $action, $editSummary, getMembers,
  +
$queryProperty) {
   
// Declarations
+
var that = this;
var i, n, current, elementsForLinksuggest, actionId, isReplace;
+
var $members;
  +
var $requests;
  +
var $arguments;
  +
var $defer;
  +
var $data;
   
elementsForLinksuggest = [
+
$members = [];
this.Selectors.ID_CONTENT_CONTENT, // New content to be added
+
$requests = [];
this.Selectors.ID_CONTENT_SUMMARY, // Edit summary
+
$arguments = [];
];
 
   
// Temp alias
+
$inputArray.forEach(function ($member) {
actionId = "#" + this.Selectors.ID_CONTENT_ACTION;
+
if (
  +
$queryProperty === "categorymembers" &&
  +
!$member.startsWith("Category:")
  +
) {
  +
$member = "Category:" + $member;
  +
}
   
// Construct new Modal instance
+
if (
this.modal = this.buildModal();
+
( // If is category-based and member is legal
  +
$queryProperty === "categorymembers" &&
  +
that.isLegalPage($member)
  +
) ||
  +
( // If is namespace-based and member is integer
  +
$queryProperty === "allpages" &&
  +
$member.match(/^[0-9]+$/) !== null
  +
)
  +
) {
  +
$members.push($member);
  +
$requests.push(getMembers($member));
  +
}
  +
});
   
// Create, then apply all relevant listeners
+
if (
this.modal.create().then(function () {
+
!$members.length ||
  +
!$requests.length ||
  +
$members.length !== $requests.length
  +
) {
  +
return;
  +
}
   
// Apply linksuggest to each element on focus event
+
$inputArray = [];
for (i = 0, n = elementsForLinksuggest.length; i < n; i++) {
+
$defer = jQuery.when.apply(jQuery, $requests);
current = "#" + elementsForLinksuggest[i];
 
   
$(document).on("focus", current,
+
$defer.done(function () {
$.prototype.linksuggest.bind($(current)));
+
if ($requests.length === 1) {
}
+
$arguments.push(arguments);
  +
} else {
  +
$arguments = Array.prototype.slice.call(arguments);
  +
}
   
// Enable replace textarea + input if replace option selected in dropdown
+
jQuery.each($arguments, function ($index, $results) {
$(document).on("change", actionId, function () {
+
if ($results[1] === "success") {
isReplace = ($(actionId).val() === "replace");
+
$data = $results[0].query[$queryProperty];
   
if (DEBUG) {
+
if ($data === undefined || $data.length === 0) {
console.log(isReplace);
+
that.addLogEntry("noSuchPage", $members[$index]);
}
+
return;
  +
}
   
this.toggleModalComponentsDisable(!isReplace, "replace");
+
$data.forEach(function ($page) {
}.bind(this));
+
$inputArray.push($page.title);
  +
});
  +
}
  +
});
   
// Once events are set, display the modal
+
if ($inputArray.length) {
this.modal.show();
+
that.actionHandler($inputArray, $newContent, $toReplace,
}.bind(this));
+
$actionIndex, $action, $editSummary);
  +
}
  +
});
  +
},
   
if (DEBUG) {
+
/**
console.dir(this.modal);
+
* @method actionHandler
}
+
* @description This function invokes methods based on user's desired
};
+
* action. If the user is not in the proper user rights
  +
* group, access is denied. If no action is selected, the
  +
* user is prompted to select an action.
  +
* <tt>setInterval</tt> is employed to ensure that the
  +
* script does not make too many consecutive content GETs
  +
* or edit POSTs; replaces <tt>forEach</tt>
  +
* implementation.
  +
* <br />
  +
* <br />
  +
* Addition of global pages-based find-and-replace option
  +
* replaces old find-and-delete option, and now checks for
  +
* cases of empty pages or empty target text.
  +
* @param {String[]} $inputArray
  +
* @param {String} $newContent
  +
* @param {String} $toReplace
  +
* @param {int} $actionIndex
  +
* @param {String} $action
  +
* @param {String} $editSummary
  +
* @returns {void}
  +
*/
  +
actionHandler: function ($inputArray, $newContent, $toReplace,
  +
$actionIndex, $action, $editSummary) {
   
/****************************************************************************/
+
var that = this;
/* Prototype event handlers */
+
var $counter = 0;
/****************************************************************************/
+
var $editInterval;
   
/**
+
switch ($actionIndex) {
* @description Arguably the most important method of the program, this
+
case 0: // No action selected
* function coordinates the entire mass editing process from the initial press
+
this.addLogEntry("noOptionSelected");
* of the "Submit" button to the conclusion of the editing operation. The
+
break;
* entire workings of the process were contained within a single method to
+
case 1:
* assist in maintaining readability when it comes time to invariably repair
+
case 2: // Edit methods (prepend and append)
* bugs and broken functionality. The other two major methods used by this
+
this.addLogEntry("loading");
* function are <code>getMemberPages</code> and
 
* <code>getValidatedEntries</code>, both of which are used to sanitize input
 
* and return wellformed loose member pages if applicable.
 
* <br />
 
* <br />
 
* The function collates all extant user input added via <code>textarea</code>
 
* and <code>input</code> fields before running through a set of conditional
 
* checks to determine if the user can continue with the requested editing
 
* operation. If the user may proceed, the function makes use of a number of
 
* <code>$.Deferred</code> promises to coordinate the necessary acquisition of
 
* wellformed pages for editing. In cases of categories/namespaces, member
 
* pages are retrieved and added to the editing queue for processing.
 
*
 
* @returns {void}
 
*/
 
main.handleSubmit = function () {
 
if (this.timer && !this.timer.isComplete) {
 
if (DEBUG) {
 
console.dir(this.timer);
 
}
 
return;
 
}
 
   
// Declarations
+
$editInterval = setInterval(function () {
var $action, $type, $case, $content, $replace, $indices, indices, $pages,
+
if (that.isLegalPage($inputArray[$counter])) {
pages, $summary, counter, parameters, data, pageIndex, newText, $getPages,
+
that.editPage(that, $inputArray[$counter], $newContent,
$postPages, $getNextPage, $getPageContent, $postPageContent, error,
+
$action, $editSummary);
isCaseSensitive, isFindAndReplace;
+
}
  +
$counter++;
  +
if ($counter === $inputArray.length) {
  +
clearInterval($editInterval);
  +
}
  +
}, that.config.editInterval);
  +
break;
  +
case 3: // Find and replace
  +
this.addLogEntry("loading");
   
// Grab user input
+
$editInterval = setInterval(function () {
$action = $("#" + this.Selectors.ID_CONTENT_ACTION)[0];
+
if (that.isLegalPage($inputArray[$counter])) {
$type = $("#" + this.Selectors.ID_CONTENT_TYPE)[0];
+
that.getContent($action, $editSummary,
$case = $("#" + this.Selectors.ID_CONTENT_CASE)[0];
+
$inputArray[$counter], $newContent, $toReplace,
$replace = $("#" + this.Selectors.ID_CONTENT_REPLACE).val();
+
that.handleContent);
$indices = $("#" + this.Selectors.ID_CONTENT_INDICES).val();
+
}
$content = $("#" + this.Selectors.ID_CONTENT_CONTENT).val();
 
$pages = $("#" + this.Selectors.ID_CONTENT_PAGES).val();
 
$summary = $("#" + this.Selectors.ID_CONTENT_SUMMARY).val();
 
   
// Cache frequently used boolean flag
+
$counter++;
isFindAndReplace = ($action.value === "replace" &&
+
if ($counter === $inputArray.length) {
$action.selectedIndex === 3);
+
clearInterval($editInterval);
  +
}
  +
}, that.config.editInterval);
  +
break;
  +
}
  +
},
   
// Is not in the proper rights group
+
/**
if (!this.hasRights()) {
+
* @method main
this.resetModal();
+
* @description The main method handles the collection of user input
  +
* from the GUI modal and tests against a series of base
  +
* cases to ensure ill-formed or illegitimate input is not
  +
* included in the program's operations. Furthermore, the
  +
* new category-based function necessitated the removal of
  +
* some old code used to execute different actions (i.e.
  +
* prepend, append, etc.) to a different method, namely
  +
* <tt>actionHandler</tt>. The main method now handles the
  +
* acquisition of category members from user input cats,
  +
* passing the array of pages to the handler above.
  +
* <br />
  +
* <br />
  +
* Addition of global pages-based find-and-replace option
  +
* replaces old find-and-delete option, and now checks for
  +
* cases of empty pages or empty target text.
  +
* @returns {void}
  +
*/
  +
main: function () {
  +
var that = this;
   
// Error: Incorrect user rights group.
+
// Values of textareas
this.addModalLogEntry("modalUserRights");
+
var $newContent = jQuery("#massEdit-content-value")[0].value;
return;
+
var $toReplace = jQuery("#massEdit-replaceThis-value")[0].value;
  +
var $pagesInput = jQuery("#massEdit-pages-value")[0].value;
  +
var $editSummary = jQuery("#massEdit-summary-value")[0].value;
  +
var $pagesArray = $pagesInput.split(/[\n]+/);
   
// No pages included
+
// Dropdown menu
} else if (!$pages) {
+
var $actionIndex = jQuery("#massEdit-actionType")[0].selectedIndex;
  +
var $action = jQuery("#massEdit-actionType").val();
  +
var $typeIndex = jQuery("#massEdit-contentType")[0].selectedIndex;
   
// Error: No pages content entered.
+
// Is not in the proper rights group
this.addModalLogEntry("noPages");
+
if (!this.hasRights) {
return;
+
jQuery("#massEdit-modal-form")[0].reset();
  +
this.addLogEntry("modalUserRights");
  +
return;
   
// Is either append/prepend with no content input included
+
// No pages included
} else if (!isFindAndReplace && !$content) {
+
} else if (!$pagesInput) {
  +
this.addLogEntry("noPages");
  +
return;
   
// Error: No content entered.
+
// Is either append/prepend with no content input included
this.addModalLogEntry("noContent");
+
} else if ($action !== "replace" && !$newContent) {
return;
+
this.addLogEntry("noContent");
  +
return;
   
// Is find-and-replace with no target content included
+
// Is find-and-replace with no target content included
} else if (isFindAndReplace && !$replace) {
+
} else if ($action === "replace" && !$toReplace) {
  +
this.addLogEntry("noTarget");
  +
return;
   
// Error: No target content entered.
+
// If user forgot to select dropdown options (no reset b/c annoying)
this.addModalLogEntry("noTarget");
+
} else if ($actionIndex === 0 || $typeIndex === 0) {
return;
+
this.addLogEntry("noOptionSelected");
  +
return;
   
// If edit summary is greater than permitted max of 800 characters
+
// If edit summary is greater than permitted max of 800 characters
} else if ($summary.length > this.Utility.MAX_SUMMARY_CHARS) {
+
} else if ($editSummary.length > 800) {
  +
this.addLogEntry("overlongSummary");
  +
return;
  +
}
   
// Error: Edit summary exceeds maximum character limit.
+
switch ($typeIndex) {
this.addModalLogEntry("overlongSummary");
+
case 1: // Loose pages
return;
+
that.actionHandler($pagesArray, $newContent, $toReplace,
  +
$actionIndex, $action, $editSummary);
  +
break;
  +
case 2: // Categories
  +
that.membersHandler($pagesArray, $newContent, $toReplace,
  +
$actionIndex, $action, $editSummary,
  +
that.getCategoryMembers, "categorymembers");
  +
break;
  +
case 3: // Namespaces
  +
that.membersHandler($pagesArray, $newContent, $toReplace,
  +
$actionIndex, $action, $editSummary,
  +
that.getNamespaceMembers, "allpages");
  +
break;
  +
}
  +
},
   
// If user forgot to select dropdown options (no reset b/c annoying)
+
/**
} else if ($action.selectedIndex === 0 || $type.selectedIndex === 0 ||
+
* @method init
(isFindAndReplace && $case.selectedIndex === 0)) {
+
* @description Method initializes the program, assembling the toolbar
  +
* link and handling click events. Config options are set
  +
* per user input or the defaults.
  +
* @param {JSON} $lang - I18n-js content
  +
* @returns {void}
  +
*/
  +
init: function ($lang) {
  +
var that = this;
   
// Error: Please select an action to perform before submitting.
+
$i18n = $lang;
this.addModalLogEntry("noOptionSelected");
+
$i18n.useContentLang();
return;
 
}
 
   
// Editing...
+
this.api = new mw.Api();
this.addModalLogEntry("loading");
+
this.config = jQuery.extend({
this.toggleModalComponentsDisable(true, "modal");
+
editInterval: 1500
  +
}, window.massEditConfig);
   
// Find-and-replace specific variable definitions
+
$i18n.useUserLang();
if (isFindAndReplace) {
 
   
// Only wellformed integers should be included as f-n-r indices
+
/**
indices = $indices.split(",").map(function (paramEntry) {
+
* Handle removal of <code>noratelimit</code> right for bots/users
if (this.isInteger(paramEntry.trim())) {
+
* Current rate limit for default user/sysop/etc. is 40 edits/minute
return wk.parseInt(paramEntry, 10);
+
* Current rate limit for bot (not bot-global) is 80 edits/minute
}
+
* @see <a href="https://git.io/fA4Jk">SUS-4775</a>
}.bind(this)).filter(function (paramEntry) {
+
* @see <a href="https://git.io/fA4eQ">VariablesBase.php</a>
return paramEntry != null; // Avoid cases of [undefined]
+
*/
}.bind(this));
+
if (
  +
jQuery.inArray("bot", wk.wgUserGroups) !== -1 &&
  +
this.config.editInterval < 750
  +
) {
  +
this.config.editInterval = 750; // Reset to max 80 edits/minute
  +
} else if (
  +
jQuery.inArray("user", wk.wgUserGroups) !== -1 &&
  +
this.config.editInterval < 1500
  +
) {
  +
this.config.editInterval = 1500; // Reset to max 40 edits/minute
  +
}
   
// Whether not search and replace is case sensitive
+
var $modalHTML =
isCaseSensitive = ($case.selectedIndex === 1 &&
+
"<form id='massEdit-modal-form' class='WikiaForm '>" +
$case.value === "sensitive");
+
"<fieldset>" +
}
+
"<p>" + $i18n.msg("modalSelect").escape() +
  +
"<br />" +
  +
"<select size='1' id='massEdit-actionType'" +
  +
"class='massEdit-menu' name='action'>" +
  +
"<option selected=''>" +
  +
$i18n.msg("modalSelect").escape() +
  +
"</option>" +
  +
"<option value='prepend'>" +
  +
$i18n.msg("dropdownPrepend").escape() +
  +
"</option>" +
  +
"<option value='append'>" +
  +
$i18n.msg("dropdownAppend").escape() +
  +
"</option>" +
  +
"<option value='replace'>" +
  +
$i18n.msg("dropdownReplace").escape() +
  +
"</option>" +
  +
"</select>" +
  +
"<br />" +
  +
"</p>" +
  +
"<br />" +
  +
"<p>" + $i18n.msg("modalContentType").escape() +
  +
"<br />" +
  +
"<select size='1' id='massEdit-contentType'" +
  +
"class='massEdit-menu' name='action'>" +
  +
"<option selected=''>" +
  +
$i18n.msg("modalContentType").escape() +
  +
"</option>" +
  +
"<option value='pages'>" +
  +
$i18n.msg("dropdownPages").escape() +
  +
"</option>" +
  +
"<option value='categories'>" +
  +
$i18n.msg("dropdownCategories").escape() +
  +
"</option>" +
  +
"<option value='namespaces'>" +
  +
$i18n.msg("dropdownNamespaces").escape() +
  +
"</option>" +
  +
"</select>" +
  +
"<br />" +
  +
"</p>" +
  +
"<br />" +
  +
"<p>" + $i18n.msg("modalContentTitle").escape() +
  +
"<br />" +
  +
"<textarea id='massEdit-content-value' " +
  +
"class='massEdit-textarea' placeholder='" +
  +
$i18n.msg("modalContentPlaceholder").escape() +
  +
"'/>" +
  +
"<br />" +
  +
"</p>" +
  +
"<br />" +
  +
"<p>" + $i18n.msg("modalReplaceTitle").escape() +
  +
"<br />" +
  +
"<textarea id='massEdit-replaceThis-value' " +
  +
"class='massEdit-textarea' placeholder='" +
  +
$i18n.msg("modalReplacePlaceholder").escape() +
  +
"' disabled/>" +
  +
"<br />" +
  +
"</p>" +
  +
"<br />" +
  +
"<p>" + $i18n.msg("modalPagesTitle").escape() +
  +
"<br />" +
  +
"<textarea id='massEdit-pages-value' " +
  +
"class='massEdit-textarea' placeholder='" +
  +
$i18n.msg("modalPagesPlaceholder").escape() +
  +
"'/>" +
  +
"<br />" +
  +
"</p>" +
  +
"<br />" +
  +
"<p>" + $i18n.msg("modalSummaryTitle").escape() +
  +
"<br />" +
  +
"<input type='textbox' id='massEdit-summary-value'" +
  +
"class='massEdit-textbox' />" +
  +
"<br />" +
  +
"</p>" +
  +
"</fieldset>" +
  +
"<br />" +
  +
"<hr>" +
  +
"</form>" +
  +
"<p>" + $i18n.msg("modalLog").escape() + "</p>" +
  +
"<div id='massEdit-log'></div>" +
  +
"<br />";
   
// Array of pages/categories/namespaces
+
var $tbElement = this.constructItem($i18n.msg("itemTitle").plain());
pages = $pages.split(/[\n]+/);
+
jQuery($tbElement).prependTo("#my-tools-menu").click(function () {
+
that.displayModal($modalHTML);
// Page counter for setInterval
+
});
counter = 0;
 
 
// Default page editing parameters
 
parameters = {
 
summary: $summary,
 
};
 
 
// New pending status Deferreds
 
$postPages = new $.Deferred();
 
$getNextPage = new $.Deferred();
 
 
// Get a listing of wellformed pages
 
$getPages = this[($type.selectedIndex === 1)
 
? "getValidatedEntries"
 
: "getMemberPages"
 
](pages, $type);
 
 
/**
 
* @description The resolved <code>$getPages</code> returns an array of
 
* loose pages from a namespace or category, or returns an array of checked
 
* loose pages if the individual pages option is selected. Once resolved,
 
* <code>$getPages</code> uses a <code>setDynamicTimeout</code> to iterate
 
* over the pages, optionally acquiring page content for find-and-replace.
 
* Once done, an invocation of <code>notify</code> calls
 
* <code>$postPages.progress</code> to assemble the parameters needed to
 
* edit the page in question. Once all pages have been edited, the pending
 
* <code>$.Deferred</code>s are resolved and the timer exited.
 
*/
 
$getPages.done(function (paramResults) {
 
pages = paramResults;
 
 
// Iterate over pages
 
this.timer = this.setDynamicTimeout(function () {
 
parameters.title = pages[counter];
 
 
if (counter === pages.length) {
 
$getNextPage.resolve();
 
$postPages.resolve("editSuccessComplete");
 
} else {
 
$getPageContent = ($action.selectedIndex < 3)
 
? new $.Deferred().resolve({}).promise()
 
: this.getPageContent(pages[counter]);
 
 
// Grab data, extend parameters, then edit the page
 
$getPageContent.always($postPages.notify);
 
 
}
 
}
}.bind(this), this.interval);
 
}.bind(this));
 
 
/**
 
* @description In the cases of failed loose page acquisitions, either from
 
* a failed API GET request or from a lack of wellformed input loose pages,
 
* the relevant log entry returned from the getter function's
 
* <code>$.Deferred</code> is logged, the timer canceled, and the modal form
 
* re-enabled by means of <code>resetModal</code>.
 
*/
 
$getPages.fail(this.resetModal.bind(this));
 
 
/**
 
* @description Whenever the getter function (<code>getMemberPages</code> or
 
* <code>getValidatedEntries</code>) needs to notify its invoking function
 
* of a new ongoing category/namespace member acquisition operation, the
 
* returned status message is acquired and added to the modal log.
 
*/
 
$getPages.progress(this.addModalLogEntry.bind(this));
 
 
/**
 
* @description Once the <code>$postPages</code> <code>Deferred</code> is
 
* resolved, indicating the completion of the requested mass edits, a final
 
* status message is logged, the form reenabled and reset for a new
 
* round, and the <code>setDynamicTimeout</code> timer canceled by means of
 
* <code>resetModal</code>.
 
*/
 
$postPages.always(this.resetModal.bind(this));
 
 
/**
 
* @description The <code>progress</code> handler is used to extend the
 
* <code>parameters</code> object with properties relevant to the action
 
* being performed (i.e. append/prepend or find-and-replace). Once complete,
 
* the modified page content is committed and the edit made by means of
 
* <code>postPageContent</code>. Once the edit is complete and a resolved
 
* promise returned, <code>$postPageContent</code> pings the pending
 
* <code>$getNextPage</code> <code>$.Deferred</code> to log the relevant
 
* messages and iterate on to the next page to be edited.
 
*/
 
$postPages.progress(function (paramResults) {
 
if (DEBUG) {
 
console.log(paramResults);
 
}
 
 
if ($action.selectedIndex < 3) {
 
// "appendtext" or "prependtext"
 
parameters[$action.value.toLowerCase() + "text"] = $content;
 
parameters.token = mw.user.tokens.get("editToken");
 
} else {
 
pageIndex = Object.keys(paramResults.query.pages)[0];
 
data = paramResults.query.pages[pageIndex];
 
 
// Return if page doesn't exist to the server
 
if (pageIndex === "-1") {
 
// Error: $1 does not exist.
 
this.addModalLogEntry("noSuchPage", pages[counter++]);
 
return this.timer.iterate();
 
}
 
 
// Set replace-specific properties
 
parameters.text = data.revisions[0]["*"];
 
parameters.basetimestamp = data.revisions[0].timestamp;
 
parameters.startimestamp = data.starttimestamp;
 
parameters.token = data.edittoken;
 
 
if (DEBUG) {
 
console.log(parameters.text, isCaseSensitive, $replace, $content,
 
indices);
 
}
 
 
// Replace instances of chosen text with inputted new text
 
newText = this.replaceOccurrences(parameters.text, isCaseSensitive,
 
$replace, $content, indices);
 
 
// Return if old & new revisions are identical in content
 
if (newText === parameters.text) {
 
// Error: No instances of $1 found in $2.
 
this.addModalLogEntry("noMatch", $replace, pages[counter++]);
 
return this.timer.iterate();
 
} else {
 
parameters.text = newText;
 
}
 
}
 
 
// Deferred attached to posting of data
 
$postPageContent = this.postPageContent(parameters);
 
$postPageContent.always($getNextPage.notify);
 
 
}.bind(this));
 
 
/**
 
* @description The pending state <code>$getNextPage</code>
 
* <code>$.Deferred</code> is pinged by <code>$postPageContent</code> once
 
* an POST request is made and a resolved status <code>$.Deferred</code>
 
* returned. The <code>progress</code> callback takes the resultant success/
 
* failure data and logs the relevant messages before moving the operation
 
* on to the iteration of the <code>setDynamicTimeout</code> timer. If the
 
* user has somehow been ratelimited, the function introduces a 35 second
 
* cooldown period before undertaking the next edit and pushes the unedited
 
* page back onto the <code>pages</code> stack.
 
*/
 
$getNextPage.progress(function (paramData) {
 
if (DEBUG) {
 
console.log(paramData);
 
}
 
 
error = (paramData.error && paramData.error.code)
 
? paramData.error.code
 
: "unknownerror";
 
 
if (paramData.edit && paramData.edit.result === "Success") {
 
// Success: $1 successfully edited!
 
this.addModalLogEntry("editSuccess", pages[counter++]);
 
} else if (error === "ratelimited") {
 
// Error: Ratelimited. Editing delayed $1 seconds.
 
this.addModalLogEntry("editFailureRateLimited",
 
(this.Utility.DELAY / 1000).toString());
 
 
// Push the unedited page back on the stack
 
pages.push(pages[counter++]);
 
} else {
 
// Error: $1 not edited. Please try again.
 
this.addModalLogEntry("editFailure", pages[counter++]);
 
}
 
 
// On to the next iteration
 
this.timer.iterate((error === "ratelimited") ? this.Utility.DELAY : null);
 
}.bind(this));
 
};
 
 
/**
 
* @description The <code>handleToggle</code> is the primary click handler for
 
* the "Pause/Resume" button used to toggle the iteration timer. Depending on
 
* whether or not the timer is in use in iterating through collated pages
 
* requiring editing, the text of the button will change accordingly. Once
 
* invoked, the method will either restart the timer during an iteration or
 
* pause it indefinitely. If the timer is not running, the method will exit.
 
*
 
* @returns {void}
 
*/
 
main.handleToggle = function () {
 
if (!this.timer || (this.timer && this.timer.isComplete)) {
 
if (DEBUG) {
 
console.dir(this.timer);
 
}
 
return;
 
}
 
 
// Declarations
 
var $toggle, config;
 
 
// Definitions
 
$toggle = $("#" + this.Selectors.ID_MODAL_TOGGLE);
 
config = [
 
{
 
message: "timerPaused", // Editing paused
 
text: "buttonResume",
 
method: "pause",
 
},
 
{
 
message: "timerResume", // Editing resumed
 
text: "buttonPause",
 
method: "resume",
 
}
 
][+this.timer.isPaused];
 
 
// Add status log entry
 
this.addModalLogEntry(config.message);
 
 
// Change the text of the button
 
$toggle.text(this.i18n.msg(config.text).escape());
 
 
// Either resume or pause the setDynamicTimeout
 
this.timer[config.method]();
 
};
 
 
/**
 
* @description Similar to <code>handleToggle</code>, this function is used to
 
* cancel the timer used to iterate through pages requiring editing. As such,
 
* it cancels the timer, adds a relevant status log entry, and re-enables the
 
* standard editing buttons in the modal <code>footer</code>. If the timer is
 
* presently not running, the method simply returns and exits. The timer is
 
* logged in the console if <code>DEBUG</code> is set to <code>true</code>.
 
*
 
* @returns {void}
 
*/
 
main.handleCancel = function () {
 
if (!this.timer || (this.timer && this.timer.isComplete)) {
 
if (DEBUG) {
 
console.dir(this.timer);
 
}
 
return;
 
} else {
 
this.resetModal("timerCancel");
 
}
 
};
 
 
/**
 
* @description As the name implies, the <code>handleClear</code> listener is
 
* used to clear the modal contents and reset the <code>form</code> HTML
 
* element. Rather than simply invoke the helper function
 
* <code>resetModal</code>, however, this function adds some animation by
 
* disabling the button set and fading in and out of the modal body during the
 
* clearing operation, displaying a status message in the log upon completion.
 
*
 
* @returns {void}
 
*/
 
main.handleClear = function () {
 
if (this.timer && !this.timer.isComplete) {
 
if (DEBUG) {
 
console.dir(this.timer);
 
}
 
return;
 
}
 
 
// Declarations
 
var visible, hidden;
 
 
// $.prototype.animate objects
 
visible = {opacity: 1};
 
hidden = {opacity: 0};
 
 
// Disable all modal buttons for duration of fade and reset
 
$("." + this.Selectors.CLASS_MODAL_BUTTON).prop("disabled", true);
 
 
// Fade out on modal and reset content before fade-in
 
$("#" + this.Selectors.ID_MODAL_CONTAINER + " > section")
 
.animate(hidden, this.Utility.FADE_INTERVAL,
 
this.resetModal.bind(this))
 
.animate(visible, this.Utility.FADE_INTERVAL,
 
// Success: All fields reset
 
this.addModalLogEntry.bind(this, "resetForm"));
 
};
 
 
/****************************************************************************/
 
/* Prototype Pseudo-constructor */
 
/****************************************************************************/
 
 
/**
 
* @description The confusingly named <code>main.init</code> function serves
 
* as a pseudo-constructor of the MassEdit class instance .Through the
 
* <code>descriptor</code> passed to <code>init.main</code>'s invocation of
 
* <code>Object.create</code> sets the <code>i18n</code>,
 
* <code>interval</code>, and <code>placement</code> instance properties, this
 
* function sets default values for <code>modal</code> and <code>timer</code>
 
* and defines the toolbar element and its associated event listener, namely
 
* <code>displayModal</code>.
 
* <br />
 
* <br />
 
* Following this function's invocation, the MassEdit class instance will have
 
* a total of five instance variables, namely, <code>i18n</code>,
 
* <code>placement</code>, <code>interval</code>, <code>modal</code>, and
 
* <code>timer</code>. All other functionality related to the script is stored
 
* in the class instance prototype, the <code>main</code> namespace object,
 
* for convenience.
 
*
 
* @returns {void}
 
*/
 
main.init = function () {
 
 
// Declarations
 
var $toolItem, toolText;
 
 
// I18n config for wiki's content language
 
this.i18n.useContentLang();
 
 
// Initialize new modal property
 
this.modal = null;
 
 
// Initialize a new dynamic timer object
 
this.timer = null;
 
 
// View instance props and prototype
 
if (DEBUG) {
 
console.dir(this);
 
}
 
 
// Text to display in the tool element
 
toolText = this.i18n.msg("itemTitle").plain(); // MassEdit
 
 
// Build tool item (nested link inside list element)
 
$toolItem = $(this.assembleOverflowElement(toolText));
 
 
// Display the modal on click
 
$toolItem.on("click", this.displayModal.bind(this));
 
 
// Either append or prepend the tool to the target
 
$(this.placement.element)[this.placement.type]($toolItem);
 
};
 
 
/****************************************************************************/
 
/* Init helper functions */
 
/****************************************************************************/
 
 
/**
 
* @description The first of two user input validators, this function is used
 
* to ensure that the user's included config details related to Placement.js
 
* are wellformed and legitimate. MassEdit.js offers support for all of
 
* Placement.js's default element locations, though as a nod to the previous
 
* incarnation of the script, the default placement element is the toolbar and
 
* the default type is "append." In the event of an error being caught due to
 
* a malformed element location or a missing type, the default config options
 
* housed in <code>Message.utility.defaultConfig</code> are used instead to
 
* ensure that user input mistakes are handled somewhat gracefully.
 
*
 
* @param {object} paramConfig - Placement.js-specific config
 
* @returns {object} config - Adjusted Placement.js config
 
*/
 
init.definePlacement = function (paramConfig) {
 
 
// Declarations
 
var config, loader;
 
 
// Definitions
 
config = {};
 
loader = wk.dev.placement.loader;
 
 
try {
 
config.element = loader.element(paramConfig.element);
 
} catch (e) {
 
config.element = loader.element(this.Placement.DEFAULTS.ELEMENT);
 
}
 
 
try {
 
config.type = loader.type(
 
(this.Placement.VALID_TYPES.indexOf(paramConfig.type) !== -1)
 
? paramConfig.type
 
: this.Placement.DEFAULTS.TYPE
 
);
 
} catch (e) {
 
config.type = loader.type(this.Placement.DEFAULTS.TYPE);
 
}
 
 
// Set script name
 
loader.script(this.Utility.SCRIPT);
 
 
return config;
 
};
 
 
/**
 
* @description The second of the two validator functions used to check that
 
* user input is wellformed and legitimate, this function checks the user's
 
* edit interval value against the permissible values for standard users and
 
* flagged bot accounts. In order to ensure that the operations are carried
 
* out smoothly, the user's rate is adjusted if it exceeds the edit
 
* restrictions placed upon accounts of different user rights levels. The
 
* original incarnation of this method came from a previous version of
 
* MassEdit which made use of a similar, jankier system to ensure the smooth
 
* progression through all included pages without loss of required edits.
 
*
 
* @see <a href="https://git.io/fA4Jk">SUS-4775</a>
 
* @see <a href="https://git.io/fA4eQ">VariablesBase.php</a>
 
* @param {number} paramInterval - User's input interval value
 
* @return {number} - Adjusted interval
 
*/
 
init.defineInterval = function (paramInterval) {
 
if (
 
wk.wgUserGroups.indexOf("bot") !== -1 &&
 
(paramInterval < this.Utility.BOT_INTERVAL || wk.isNaN(paramInterval))
 
) {
 
return this.Utility.BOT_INTERVAL; // Reset to max 80 edits/minute
 
} else if (
 
wk.wgUserGroups.indexOf("user") !== -1 &&
 
(paramInterval < this.Utility.STD_INTERVAL || wk.isNaN(paramInterval))
 
) {
 
return this.Utility.STD_INTERVAL; // Reset to max 40 edits/minute
 
} else {
 
return wk.parseInt(paramInterval, 10);
 
}
 
};
 
 
/****************************************************************************/
 
/* Init main functions */
 
/****************************************************************************/
 
 
/**
 
* @description The confusingly named <code>init.main</code> function is used
 
* to coordinate the script setup madness in a single method, validating all
 
* user input by means of helper method invocation and setting all instance
 
* properties of the MassEdit class instance. Once the <code>descriptor</code>
 
* <code>object</code> has been assembled containing the relevant instance
 
* variables for placement, edit interval, and i18n messages, the method calls
 
* <code>Object.create</code> to construct a new MassEdit class instance,
 
* passing the <code>descriptor</code> and the <code>main</code> namespace
 
* <code>object</code> as the instance's prototype.
 
* <br />
 
* <br />
 
* The separation of setup code and MassEdit functionality code into distinct
 
* namespace <code>object</code>s helped to ensure that code was logically
 
* organized per the single responsibility principle and more readable by
 
* virtue of the fact that each namespace handles distinctly different tasks.
 
* This will assist in debugging should an issue arise with either the setup
 
* or the script's functionality itself.
 
*
 
* @param {object} paramLang - i18n <code>object</code> returned from hook
 
* @returns {void}
 
*/
 
init.main = function (paramLang) {
 
 
// Declarations
 
var i, n, array, descriptor, parameter, lowercase, method, property,
 
descriptorProperties;
 
 
array = ["Interval", "Placement"];
 
 
// New Object.create descriptor object
 
descriptor = {};
 
 
descriptorProperties = {
 
enumerable: true,
 
configurable: false,
 
writable: false,
 
 
};
 
};
   
// Set I18n object as instance property
+
mw.hook("dev.i18n").add(function ($i18n) {
descriptor.i18n = $.extend(true, {}, descriptorProperties);
+
jQuery.when(
descriptor.i18n.value = paramLang;
+
$i18n.loadMessages("MassEdit"),
+
mw.loader.using(["mediawiki.util", "mediawiki.api"])
// Reduce copy pasta
+
).done(jQuery.proxy(MassEdit.init, MassEdit));
for (i = 0, n = array.length; i < n; i++) {
+
});
 
// Definitions
 
property = array[i];
 
method = "define" + property;
 
lowercase = property.toLowerCase();
 
parameter = (wk.MassEditConfig && wk.MassEditConfig[lowercase])
 
? wk.MassEditConfig[lowercase]
 
: null;
 
 
// New descriptor entry
 
descriptor[lowercase] = $.extend(true, {}, descriptorProperties);
 
 
// Define descriptor entry value
 
descriptor[lowercase].value = this[method](parameter);
 
}
 
 
if (DEBUG) {
 
console.dir(init);
 
}
 
 
// Create new MassEdit instance
 
Object.create(main, descriptor).init();
 
};
 
 
/**
 
* @description This function is invoked as many times as there are external
 
* dependencies, serving as the primary hook handler for each of the required
 
* events denoted in <code>init.Dependencies.SCRIPTS</code>. Once all
 
* dependencies have been successfully loaded and the hooks fired, the
 
* function loads I18n-js messages and invokes <code>init.main</code> as
 
* the callback function.
 
*
 
* @returns {void}
 
*/
 
init.load = function () {
 
if (++this.loaded === Object.keys(this.Dependencies.SCRIPTS).length) {
 
wk.dev.i18n.loadMessages(this.Utility.SCRIPT).then(init.main.bind(this));
 
}
 
};
 
 
/**
 
* @description This function is only invoked once the ResourceLoader has
 
* successfully loaded the various required <code>mw</code> core modules,
 
* executing this callback on completion. This function is responsible for
 
* assembling the relevant hook event aliases from the listing of hook names
 
* included in <code>init.Dependencies.SCRIPTS</code> that denote the
 
* required external dependencies and libraries required by the script.
 
*
 
* @returns {void}
 
*/
 
init.preload = function () {
 
 
// Declarations
 
var i, n, hooks;
 
 
// Definitions
 
this.loaded = 0;
 
hooks = Object.keys(this.Dependencies.SCRIPTS);
 
 
for (i = 0, n = hooks.length; i < n; i++) {
 
mw.hook(hooks[i]).add(init.load.bind(this));
 
}
 
};
 
 
// Begin loading of external dependencies
 
mw.loader.using(init.Dependencies.MODULES).then(init.preload.bind(init));
 
wk.importArticles({
 
type: "script",
 
articles: Object.values(init.Dependencies.SCRIPTS),
 
});
 
 
});
 
});

Revision as of 12:16, October 2, 2019

/**
 * MassEdit/code.js
 * @file Adds/deletes/replaces content from pages/categories/namespaces
 * @author Eizen <dev.wikia.com/wiki/User_talk:Eizen>
 * @external "mediawiki.util"
 * @external "jQuery"
 * @external "wikia.ui.factory"
 * @external "wikia.window"
 * @external "I18n-js"
 * @external "mw"
 */
 
/*jslint browser, this:true */
/*global mw, jQuery, window, require, wk, ui */
 
require(["jquery", "mw", "wikia.window", "wikia.ui.factory"],
        function (jQuery, mw, wk, ui) {
    "use strict";
 
    if (jQuery("#massEdit-li").exists() || window.isMassEditLoaded) {
        return;
    }
    window.isMassEditLoaded = true;
 
    if (!window.dev || !window.dev.i18n) {
        wk.importArticle({
            type: "script",
            article: "u:dev:MediaWiki:I18n-js/code.js"
        });
    }
 
    // Constant for future testing
    const DEBUG = false;
 
    //I18n-js
    var $i18n;
 
    /**
     * @class MassEdit
     * @classdesc The central MassEdit class
     */
    var MassEdit = {
        meta: {
            author: "User:Eizen",
            created: "05/02/17",
            lastEdit: "03/09/18",
            version: "2.4.4"
        },
        hasRights: new RegExp(["(sysop|content-moderator|bot|bot-global" +
            "|staff|vstf|helper|global-discussions-moderator|content-team-member" +
            "|content-volunteer|wiki-manager)"].join("")).test(wk.wgUserGroups.join(" ")),
        legalChars: new RegExp("^[" + wk.wgLegalTitleChars + "]*$"),
 
        /**
         * @method addLogEntry
         * @description Method allows for quick adding of a MassEdit log entry
         *              to the appropriate text field.
         * @param {String} $key - The name of the JSON field
         * @returns {void}
         */
        addLogEntry: function ($key) {
            jQuery("#massEdit-log").prepend($i18n.msg($key).escape() + "<br/>");
        },
 
        /**
         * @method addLogEntry
         * @description "Overloaded" two-parameter variation of the method above
         *              that accepts a JSON key and an entry substitute for $1
         *              or $2.
         * @param {String} $key - The name of the JSON field
         * @param {String} $entry - Name of the page to substitute
         * @returns {void}
         */
        addLogEntry: function ($key, $entry) {
            jQuery("#massEdit-log").prepend(
                $i18n.msg($key, $entry).escape() + "<br />");
        },
 
        /**
         * @method constructItem
         * @description Method returns a completed <code>String</code>
         *              representing the menu link item. Is comprised of a
         *              link inside a list item.
         * @param {String} $text - Text to be displayed in the item and title
         * @returns {String}
         */
        constructItem: function ($text) {
            return mw.html.element("li", {
                "class": "overflow",
                "id": "massEdit-li"
            }, new mw.html.Raw(
                mw.html.element("a", {
                    "id": "massEdit-a",
                    "href": "#",
                    "title": $text
                }, $text)
            ));
        },
 
        /**
         * @method isLegalPage
         * @description Utility function used to test if inputted page name
         *              matches the legal characters regex. Returns a boolean
         *              flag depending on result.
         * @param {String} $page - Inputted page name
         * @returns {boolean}
         */
        isLegalPage: function ($page) {
            if (!this.legalChars.test($page)) {
                jQuery("#massEdit-modal-form")[0].reset();
                this.addLogEntry("modalSecurity");
                return false;
            } else {
                return true;
            }
        },
 
        /**
         * @method displayModal
         * @description Method constructs and displays the main user interface.
         *              Injects custom CSS prior to construction and handles all
         *              button click events.
         * @param {String} $modalHTML - The modal HTML layout
         * @returns {void}
         */
        displayModal: function ($modalHTML) {
            var that = this;
 
            mw.util.addCSS(
                ".massEdit-menu," +
                ".massEdit-textbox {" +
                    "width: 100%;" +
                    "padding: 0;" +
                "}" +
                ".massEdit-textarea {" +
                    "height: 50px;" +
                    "width: 100%;" +
                    "padding: 0;" +
                    "overflow: auto;" +
                "}" +
                "#massEdit-log {" +
                    "height: 55px;" +
                    "width: 100%;" +
                    "border: 1px solid;" +
                    "font-family: monospace;" +
                    "background: #fff;" +
                    "color: #aeaeae;" +
                    "overflow: auto;" +
                    "padding:0;" +
                "}"
            );
 
            ui.init(["modal"]).then(function (modal) {
                var config = {
                    vars: {
                        id: "massEdit-modal",
                        size: "medium",
                        title: $i18n.msg("itemTitle").escape(),
                        content: $modalHTML,
                        buttons: [{
                            vars: {
                                value: $i18n.msg("buttonCancel").escape(),
                                classes: ["normal", "primary"],
                                data: [{
                                    key: "event",
                                    value: "cancel"
                                }]
                            }
                        }, {
                            vars: {
                                value: $i18n.msg("buttonClear").escape(),
                                classes: ["normal", "primary"],
                                data: [{
                                    key: "event",
                                    value: "clear"
                                }]
                            }
                        }, {
                            vars: {
                                value: $i18n.msg("buttonSubmit").escape(),
                                classes: ["normal", "primary"],
                                data: [{
                                    key: "event",
                                    value: "submit"
                                }]
                            }
                        }]
                    }
                };
 
                modal.createComponent(config, function (massEditModal) {
                    massEditModal.bind("cancel", function () {
                        massEditModal.trigger("close");
                    });
 
                    massEditModal.bind("clear", function () {
                        jQuery("#massEdit-modal-form")[0].reset();
                    });
 
                     massEditModal.bind("submit", function () {
                        that.main();
                    });
 
                    massEditModal.show();
                });
            });
 
            // Disable replacements menu depending on current selected option
            jQuery(document).on("change", "#massEdit-actionType", function () {
                if (jQuery("#massEdit-actionType").val() === "replace") {
                    jQuery("#massEdit-replaceThis-value")
                        .prop("disabled", false);
                } else {
                    jQuery("#massEdit-replaceThis-value")
                        .prop("disabled", true);
                }
            });
        },
 
        /**
         * @method getContent
         * @description This method retrieves the content of the inputted page,
         *              including information about its time of creation and
         *              relevant timestamp info. Used exclusively by the find
         *              and dropdown option.
         * @param {String} $action - Editing action (prepend, append, replace)
         * @param {String} $editSummary - Edit summary
         * @param {String} $page - The page in question
         * @param {String} $newContent - New text to replace the target
         * @param {String} $replace - The target to be replaced in the callback
         * @param {function} callback - The callback handler
         */
        getContent: function ($action, $editSummary, $page, $newContent,
                $replace, callback) {
 
            var that = this;
            this.api.get({
                action: "query",
                prop: "info|revisions",
                intoken: "edit",
                titles: $page,
                rvprop: "content|timestamp",
                rvlimit: "1",
                indexpageids: "true",
                format : "json"
            }).done(function ($data) {
                if (!$data.error) {
                    callback(           // handleContent()
                        that,           // this
                        $action,        // "replace"
                        $editSummary,   // "#massEdit-summary-value"
                        $data,          // $data
                        $page,          // $pagesArray[$counter]
                        $newContent,    // "#massEdit-content-value"
                        $replace        // "#massEdit-replaceThis-value"
                    );
                }
            });
        },
 
        /**
         * @method handleContent
         * @description Callback function for <code>getContent</code>. Sifts
         *              through included data and passes relevant bits to the
         *              <code>editPage</code> method. Used exclusively by the
         *              find and replace dropdown option.
         * @param {this} that - Scope variable
         * @param {String} $action - Editing action (prepend, append, replace)
         * @param {String} $editSummary - Edit summary
         * @param {JSON} $data - Passed data from <code>getContent</code>
         * @param {String} $page - Specific page in question
         * @param {String} $newContent - New text to replace the target
         * @param {String} $replaceThis - Text to be replaced by $newContent
         * @returns {void}
         */
        handleContent: function (that, $action, $editSummary, $data, $page,
                $newContent, $replaceThis) {
 
            // Check if page actually exists
            if (Object.keys($data.query.pages)[0] === "-1") {
                jQuery("#massEdit-modal-form")[0].reset();
                that.addLogEntry("noSuchPage", $page);
                return;
            }
 
            var $newText;
            var $result = $data.query.pages[Object.keys($data.query.pages)[0]];
            var $text = $result.revisions[0]["*"];
            var $timestamp = $result.revisions[0].timestamp;
            var $starttimestamp = $result.starttimestamp;
            var $token = $result.edittoken;
 
            // Replace all instances of chosen text with inputted new text
            $newText = $text.split($replaceThis).join($newContent);
 
            // Check if old & new revisions are identical in content
            if ($newText === $text) {
                jQuery("#massEdit-log").prepend(
                    $i18n.msg("noMatch", $replaceThis, $page).escape() +
                    "<br />"
                );
            } else {
                that.editPage(that, $page, $newText, $action, $editSummary,
                    $timestamp, $starttimestamp, $token);
            }
        },
 
        /**
         * @method editPage
         * @description The one-size-fits-all editing handler for use by all
         *              three main MassEdit functions. Takes several different
         *              numbers of input parameters depending on the action to
         *              be taken by the handler.
         * @param {this} that - Scope variable
         * @param {String} $page - The page to be edited
         * @param {String} $content - The content to be added to the page
         * @param {String} $action - Editing action (prepend, append, replace)
         * @param {String} $editSummary - Edit summary
         * @param {String} $timestamp - Optional, for replace option only
         * @param {String} $starttimestamp - Optional, for replace option only
         * @param {String} $token - Optional, for replace option only
         * @returns {void}
         */
        editPage: function (that, $page, $content, $action, $editSummary,
                $timestamp, $starttimestamp, $token) {
 
            // Edit without editing; for testing purposes
            if (DEBUG) {
                that.addLogEntry("editSuccess", $page);
                return;
            }
 
            // Default base properties
            var $params = {
                action: "edit",
                minor: true,
                bot: true,
                title: $page,
                summary: $editSummary
            };
 
            // Set additional Object properties depending on action to be taken
            switch ($action) {
            case "prepend":
                $params.prependtext = $content;
                $params.token = mw.user.tokens.get("editToken");
                break;
            case "append":
                $params.appendtext = $content;
                $params.token = mw.user.tokens.get("editToken");
                break;
            case "replace":
                $params.text = $content;
                $params.basetimestamp = $timestamp;
                $params.startimestamp = $starttimestamp;
                $params.token = $token;
                break;
            }
 
            that.api.post($params).done(function ($data) {
                jQuery("#massEdit-modal-form")[0].reset();
                if (!$data.error) {
                    that.addLogEntry("editSuccess", $page);
                } else {
                    that.addLogEntry("editFailure", $page);
                }
            });
        },
 
        /**
         * @method getNamespaceMembers
         * @description As the name implies, this method returns the results of
         *              a request for an inputted namespace's associated page
         *              contents.
         * @param {String} $namespace - Number of the namespace in question
         * @return {JSON}
         */
        getNamespaceMembers: function ($namespace) {
            return jQuery.ajax({
                type: "GET",
                url: mw.util.wikiScript("api"),
                data: {
                    action: "query",
                    list: "allpages",
                    aplimit: "max",
                    apnamespace: $namespace,
                    format: "json"
                }
            });
        },
 
        /**
         * @method getCategoryMembers
         * @description As the name implies, this method returns the results of
         *              a request for an inputted category's associated page
         *              contents.
         * @param {String} $category - Name of the category in question
         * @return {JSON}
         */
        getCategoryMembers: function ($category) {
            return jQuery.ajax({
                type: "GET",
                url: mw.util.wikiScript("api"),
                data: {
                    action: "query",
                    list: "categorymembers",
                    cmtitle: $category,
                    cmprop: "title",
                    cmdir: "desc",
                    cmlimit: "max",
                    format: "json"
                }
            });
        },
 
        /**
         * @method membersHandler
         * @description This method, inspired by Java-style reflection, is a
         *              general purpose handler for use by both namespace and
         *              category bulk editing. The method accepts either the
         *              <tt>getCategoryMembers</tt> or
         *              <tt>getNamespaceMembers<tt> methods as the requests of
         *              choice, running basically the same process with the
         *              results of each once all calls have been completed.
         *              <br />
         *              <br />
         *              Basically, once the calls have been made, the method
         *              assembles all the pages from the request results and
         *              pushes them into an array for processing by the
         *              <tt>actionHandler</tt> method.
         * @param {String[]} $inputArray
         * @param {String} $newContent
         * @param {String} $toReplace
         * @param {int} $actionIndex
         * @param {String} $action
         * @param {String} $editSummary
         * @param {function} getMembers
         * @param {String} $queryProperty
         * @returns {void}
         */
        membersHandler: function ($inputArray, $newContent, $toReplace,
                $actionIndex, $action, $editSummary, getMembers,
                $queryProperty) {
 
            var that = this;
            var $members;
            var $requests;
            var $arguments;
            var $defer;
            var $data;
 
            $members = [];
            $requests = [];
            $arguments = [];
 
            $inputArray.forEach(function ($member) {
                if (
                    $queryProperty === "categorymembers" &&
                    !$member.startsWith("Category:")
                ) {
                    $member = "Category:" + $member;
                }
 
                if (
                    ( // If is category-based and member is legal
                        $queryProperty === "categorymembers" &&
                        that.isLegalPage($member)
                    ) ||
                    ( // If is namespace-based and member is integer
                        $queryProperty === "allpages" &&
                        $member.match(/^[0-9]+$/) !== null
                    )
                ) {
                    $members.push($member);
                    $requests.push(getMembers($member));
                }
            });
 
            if (
                !$members.length ||
                !$requests.length ||
                $members.length !== $requests.length
            ) {
                return;
            }
 
            $inputArray = [];
            $defer = jQuery.when.apply(jQuery, $requests);
 
            $defer.done(function () {
                if ($requests.length === 1) {
                    $arguments.push(arguments);
                } else {
                    $arguments = Array.prototype.slice.call(arguments);
                }
 
                jQuery.each($arguments, function ($index, $results) {
                    if ($results[1] === "success") {
                        $data = $results[0].query[$queryProperty];
 
                        if ($data === undefined || $data.length === 0) {
                            that.addLogEntry("noSuchPage", $members[$index]);
                            return;
                        }
 
                        $data.forEach(function ($page) {
                            $inputArray.push($page.title);
                        });
                    }
                });
 
                if ($inputArray.length) {
                    that.actionHandler($inputArray, $newContent, $toReplace,
                        $actionIndex, $action, $editSummary);
                }
            });
        },
 
        /**
         * @method actionHandler
         * @description This function invokes methods based on user's desired
         *              action. If the user is not in the proper user rights
         *              group, access is denied. If no action is selected, the
         *              user is prompted to select an action.
         *              <tt>setInterval</tt> is employed to ensure that the
         *              script does not make too many consecutive content GETs
         *              or edit POSTs; replaces <tt>forEach</tt>
         *              implementation.
         *              <br />
         *              <br />
         *              Addition of global pages-based find-and-replace option
         *              replaces old find-and-delete option, and now checks for
         *              cases of empty pages or empty target text.
         * @param {String[]} $inputArray
         * @param {String} $newContent
         * @param {String} $toReplace
         * @param {int} $actionIndex
         * @param {String} $action
         * @param {String} $editSummary
         * @returns {void}
         */
        actionHandler: function ($inputArray, $newContent, $toReplace,
                $actionIndex, $action, $editSummary) {
 
            var that = this;
            var $counter = 0;
            var $editInterval;
 
            switch ($actionIndex) {
            case 0: // No action selected
                this.addLogEntry("noOptionSelected");
                break;
            case 1:
            case 2: // Edit methods (prepend and append)
                this.addLogEntry("loading");
 
                $editInterval = setInterval(function () {
                    if (that.isLegalPage($inputArray[$counter])) {
                        that.editPage(that, $inputArray[$counter], $newContent,
                            $action, $editSummary);
                    }
                    $counter++;
                    if ($counter === $inputArray.length) {
                        clearInterval($editInterval);
                    }
                }, that.config.editInterval);
                break;
            case 3: // Find and replace
                this.addLogEntry("loading");
 
                $editInterval = setInterval(function () {
                    if (that.isLegalPage($inputArray[$counter])) {
                        that.getContent($action, $editSummary,
                            $inputArray[$counter], $newContent, $toReplace,
                            that.handleContent);
                    }
 
                    $counter++;
                    if ($counter === $inputArray.length) {
                        clearInterval($editInterval);
                    }
                }, that.config.editInterval);
                break;
            }
        },
 
        /**
         * @method main
         * @description The main method handles the collection of user input
         *              from the GUI modal and tests against a series of base
         *              cases to ensure ill-formed or illegitimate input is not
         *              included in the program's operations. Furthermore, the
         *              new category-based function necessitated the removal of
         *              some old code used to execute different actions (i.e.
         *              prepend, append, etc.) to a different method, namely
         *              <tt>actionHandler</tt>. The main method now handles the
         *              acquisition of category members from user input cats,
         *              passing the array of pages to the handler above.
         *              <br />
         *              <br />
         *              Addition of global pages-based find-and-replace option
         *              replaces old find-and-delete option, and now checks for
         *              cases of empty pages or empty target text.
         * @returns {void}
         */
        main: function () {
            var that = this;
 
            // Values of textareas
            var $newContent = jQuery("#massEdit-content-value")[0].value;
            var $toReplace = jQuery("#massEdit-replaceThis-value")[0].value;
            var $pagesInput = jQuery("#massEdit-pages-value")[0].value;
            var $editSummary = jQuery("#massEdit-summary-value")[0].value;
            var $pagesArray = $pagesInput.split(/[\n]+/);
 
            // Dropdown menu
            var $actionIndex = jQuery("#massEdit-actionType")[0].selectedIndex;
            var $action = jQuery("#massEdit-actionType").val();
            var $typeIndex = jQuery("#massEdit-contentType")[0].selectedIndex;
 
            // Is not in the proper rights group
            if (!this.hasRights) {
                jQuery("#massEdit-modal-form")[0].reset();
                this.addLogEntry("modalUserRights");
                return;
 
            // No pages included
            } else if (!$pagesInput) {
                this.addLogEntry("noPages");
                return;
 
            // Is either append/prepend with no content input included
            } else if ($action !== "replace" && !$newContent) {
                this.addLogEntry("noContent");
                return;
 
            // Is find-and-replace with no target content included
            } else if ($action === "replace" && !$toReplace) {
                this.addLogEntry("noTarget");
                return;
 
            // If user forgot to select dropdown options (no reset b/c annoying)
            } else if ($actionIndex === 0 || $typeIndex === 0) {
                this.addLogEntry("noOptionSelected");
                return;
 
            // If edit summary is greater than permitted max of 800 characters
            } else if ($editSummary.length > 800) {
                this.addLogEntry("overlongSummary");
                return;
            }
 
            switch ($typeIndex) {
            case 1: // Loose pages
                that.actionHandler($pagesArray, $newContent, $toReplace,
                    $actionIndex, $action, $editSummary);
                break;
            case 2: // Categories
                that.membersHandler($pagesArray, $newContent, $toReplace,
                    $actionIndex, $action, $editSummary,
                    that.getCategoryMembers, "categorymembers");
                break;
            case 3: // Namespaces
                that.membersHandler($pagesArray, $newContent, $toReplace,
                    $actionIndex, $action, $editSummary,
                    that.getNamespaceMembers, "allpages");
                break;
            }
        },
 
        /**
         * @method init
         * @description Method initializes the program, assembling the toolbar
         *              link and handling click events. Config options are set
         *              per user input or the defaults.
         * @param {JSON} $lang - I18n-js content
         * @returns {void}
         */
        init: function ($lang) {
            var that = this;
 
            $i18n = $lang;
            $i18n.useContentLang();
 
            this.api = new mw.Api();
            this.config = jQuery.extend({
                editInterval: 1500
            }, window.massEditConfig);
 
            $i18n.useUserLang();
 
            /**
             * Handle removal of <code>noratelimit</code> right for bots/users
             * Current rate limit for default user/sysop/etc. is 40 edits/minute
             * Current rate limit for bot (not bot-global) is 80 edits/minute
             * @see <a href="https://git.io/fA4Jk">SUS-4775</a>
             * @see <a href="https://git.io/fA4eQ">VariablesBase.php</a>
             */
            if (
                jQuery.inArray("bot", wk.wgUserGroups) !== -1 &&
                this.config.editInterval < 750
            ) {
                this.config.editInterval = 750; // Reset to max 80 edits/minute
            } else if (
                jQuery.inArray("user", wk.wgUserGroups) !== -1 &&
                this.config.editInterval < 1500
            ) {
                this.config.editInterval = 1500; // Reset to max 40 edits/minute
            }
 
            var $modalHTML =
            "<form id='massEdit-modal-form' class='WikiaForm '>" +
                "<fieldset>" +
                    "<p>" + $i18n.msg("modalSelect").escape() +
                        "<br />" +
                        "<select size='1' id='massEdit-actionType'" +
                                "class='massEdit-menu' name='action'>" +
                            "<option selected=''>" +
                                $i18n.msg("modalSelect").escape() +
                            "</option>" +
                            "<option value='prepend'>" +
                                $i18n.msg("dropdownPrepend").escape() +
                            "</option>" +
                            "<option value='append'>" +
                                $i18n.msg("dropdownAppend").escape() +
                            "</option>" +
                            "<option value='replace'>" +
                                $i18n.msg("dropdownReplace").escape() +
                            "</option>" +
                        "</select>" +
                        "<br />" +
                    "</p>" +
                    "<br />" +
                    "<p>" + $i18n.msg("modalContentType").escape() +
                        "<br />" +
                        "<select size='1' id='massEdit-contentType'" +
                                "class='massEdit-menu' name='action'>" +
                            "<option selected=''>" +
                                $i18n.msg("modalContentType").escape() +
                            "</option>" +
                            "<option value='pages'>" +
                                $i18n.msg("dropdownPages").escape() +
                            "</option>" +
                            "<option value='categories'>" +
                                $i18n.msg("dropdownCategories").escape() +
                            "</option>" +
                            "<option value='namespaces'>" +
                                $i18n.msg("dropdownNamespaces").escape() +
                            "</option>" +
                        "</select>" +
                        "<br />" +
                    "</p>" +
                    "<br />" +
                    "<p>" + $i18n.msg("modalContentTitle").escape() +
                        "<br />" +
                        "<textarea id='massEdit-content-value' " +
                            "class='massEdit-textarea' placeholder='" +
                            $i18n.msg("modalContentPlaceholder").escape() +
                        "'/>" +
                        "<br />" +
                    "</p>" +
                    "<br />" +
                    "<p>" + $i18n.msg("modalReplaceTitle").escape() +
                        "<br />" +
                        "<textarea id='massEdit-replaceThis-value' " +
                            "class='massEdit-textarea' placeholder='" +
                            $i18n.msg("modalReplacePlaceholder").escape() +
                        "' disabled/>" +
                        "<br />" +
                    "</p>" +
                    "<br />" +
                    "<p>" + $i18n.msg("modalPagesTitle").escape() +
                        "<br />" +
                        "<textarea id='massEdit-pages-value' " +
                            "class='massEdit-textarea' placeholder='" +
                            $i18n.msg("modalPagesPlaceholder").escape() +
                        "'/>" +
                        "<br />" +
                    "</p>" +
                    "<br />" +
                    "<p>" + $i18n.msg("modalSummaryTitle").escape() +
                        "<br />" +
                        "<input type='textbox' id='massEdit-summary-value'" +
                            "class='massEdit-textbox' />" +
                        "<br />" +
                    "</p>" +
                "</fieldset>" +
                "<br />" +
                "<hr>" +
            "</form>" +
            "<p>" + $i18n.msg("modalLog").escape() + "</p>" +
            "<div id='massEdit-log'></div>" +
            "<br />";
 
            var $tbElement = this.constructItem($i18n.msg("itemTitle").plain());
            jQuery($tbElement).prependTo("#my-tools-menu").click(function () {
                that.displayModal($modalHTML);
            });
        }
    };
 
    mw.hook("dev.i18n").add(function ($i18n) {
        jQuery.when(
            $i18n.loadMessages("MassEdit"),
            mw.loader.using(["mediawiki.util", "mediawiki.api"])
        ).done(jQuery.proxy(MassEdit.init, MassEdit));
    });
});
Community content is available under CC-BY-SA unless otherwise noted.