User:Dragoniez/Gadget-MarkBLocked.js

/*************************************************************************************************** * Name: Gadget-MarkBLocked (GMBL)                                                                * * Author: Dragoniez (mw:User:Dragoniez/Gadget-MarkBLocked.js)                                * * License: MIT                                                                                   * * Notes: This is a script forked from m:User:Dragoniez/Mark BLocked Global.js. This script:  * *       - Marks up locally blocked users and single IPs                                          * *       - (*) Can mark up single IPs included in locally-blocked IP ranges                       * *       - (*) Can mark up globally locked users                                                  * *       - (*) Can mark up globally blocked single IPs and IP ranges                              * *       The starred features require quite some API calls and could lead to performance issues   * *       depending on the browser and computer environments of the editor who uses the script;    * *       hence disabled by default. You can enable them via the configuration page added by the  * *       script, namely via Special:MarkBLockedPreferences (and also Special:MBLP or      * *        Special:MBP). * * Setup: Before installing the script as a gadget, configure the 'MarkBLocked' object for proper * *       localization. * //

(function(mw, $) { // Wrapper function

// *************************************** LOCALIZATION SETTINGS ***************************************

var MarkBLocked = { // Portletlink configurations portletlink: { position: 'p-tb', text: 'MarkBLocked Preferences', id: 't-gmblp', tooltip: 'Configure MarkBLocked', accesskey: null, nextnode: null },   /* -        Register all local page names for Special:Contributions and Special:CentralAuth (without the namespace prefix). 'contribs', 'contributions', 'ca', and 'centralauth' are registered by default: No need to register them. Note that the items are case-insensitive, compatible both with " " and "_" for spaces, and should NEVER be URI-encoded. If nothing needs to be registered, leave the array empty. */   contribs_CA: ['投稿記録', 'アカウント統一管理'], // Example setting for jawiki // Texts to show on Special:MarkBLockedPreferences configpage: { heading: 'MarkBLocked Preferences', check: { localips: 'Check whether single IPs are included in locally-blocked IP ranges', globalusers: 'Check whether registered users are globally locked', globalips: 'Check whether IPs are globally blocked' },       save: { button: 'Save', doing: 'Saving preferences', done: 'Saved preferences', failed: 'Failed to save preferences', lastsave: 'Last saved at' // This is FOLLOWED by a space and a timestamp }   },    // Names of the local user groups that have the 'apihighlimits' user right apihighlimits: ['bot', 'sysop'] };

// *************************************** ACROSS-THE-FUNCTION VARIABLES ***************************************

// Get all aliases for the special, and user: and user_talk: namespaces var nsObj = mw.config.get('wgNamespaceIds'); // {"special": -1, "user": 2, ...} var specialNs = [], userNs = []; Object.keys(nsObj).forEach(function(key) {   if (nsObj[key] == -1) {        specialNs.push(key);    } else if (nsObj[key] == 2 || nsObj[key] == 3) {        userNs.push(key + ':');    } }); specialNs = specialNs.join('|').replace(/[ _]/g, '[ _]'); userNs = userNs.join('|').replace(/[ _]/g, '[ _]');

// *************************************** INITIALIZATION ***************************************

var prefs; $.when(   mw.loader.using(['mediawiki.util', 'mediawiki.api', 'mediawiki.user']),    $.ready ).then(function {

// Get personal preferences for MarkBLocked prefs = mw.user.options.get('userjs-gmbl-preferences'); if (prefs) { prefs = JSON.parse(prefs); } else { prefs = { localips: false, globalusers: false, globalips: false };   }

// If the user is on Special:MarkBLockedPreferences, create a form for personal settings var prefRegex = new RegExp('^(' + specialNs + '):(markblockedpreferences|mbl?p)$', 'i'); if (mw.config.get('wgPageName').match(prefRegex)) { return createPreferencesPage; }

// Create portletlink mw.util.addPortletLink(       MarkBLocked.portletlink.position,        '/wiki/Special:MarkBLockedPreferences',        MarkBLocked.portletlink.text,        MarkBLocked.portletlink.id,        MarkBLocked.portletlink.tooltip,        MarkBLocked.portletlink.accesskey,        MarkBLocked.portletlink.nextnode    );

// Prepare for markup if not action === 'edit' (if there's a block message, run the script even if action === 'edit') if (mw.config.get('wgAction') !== 'edit' || $('.mw-logline-block').length !== 0) initializeMarkBLocked;

});

// *************************************** MAIN FUNCTIONS ***************************************

/** * Function to create the page content of Special:MarkBLockedPreferences */ function createPreferencesPage {

// Change document title document.title = 'MarkBLockedPreferences - Wikipedia';

// Append $('head').append(       ' ' +            '.gmblp-checkbox {' +                'margin-right: 0.5em;' +            '}' +        ' '    );

// Create page content $(mw.util.$content).prop('innerHTML',       ' ' +            ' ' +                ' ' + MarkBLocked.configpage.heading + ' ' +            ' ' +            ' ' +                ' ' +                '' + MarkBLocked.configpage.check.localips + ' ' +                ' ' +                '' + MarkBLocked.configpage.check.globalusers + ' ' +                ' ' +                '' + MarkBLocked.configpage.check.globalips + ' ' +                ' ' +                ' ' + ' ' + ' ' +       ' '    );

// (Un)check the checkboxes in accordance with user preferences $('#gmblp-localips').prop('checked', prefs.localips); $('#gmblp-globalusers').prop('checked', prefs.globalusers); $('#gmblp-globalips').prop('checked', prefs.globalips);

// When the 'Save' button is hit $('#gmblp-save').click(function {

// Show the 'Saving preferences' message $('#gmblp-status').css('display', 'block').prop('innerHTML',            MarkBLocked.configpage.save.doing +            ''        );

// Get the status of the checkboxes as an object var options = { localips: $('#gmblp-localips').is(':checked'), globalusers: $('#gmblp-globalusers').is(':checked'), globalips: $('#gmblp-globalips').is(':checked') };       options = JSON.stringify(options);

// API call to save the preferences new mw.Api.saveOption('userjs-gmbl-preferences', options) .then(function { // Success

$('#gmblp-status').prop('innerHTML',               ' ' +                MarkBLocked.configpage.save.done            ); $('#gmblp-lastsaved').css('display', 'block').text(MarkBLocked.configpage.save.lastsave + ' ' + new Date.toJSON.replace(/\.\d{3}Z$/, '')); mw.user.options.set('userjs-gmbl-preferences', options);

}).catch(function(code, err) { // Failure

mw.log.error(err.error.info); $('#gmblp-status').prop('innerHTML',               ' ' +                MarkBLocked.configpage.save.failed            );

}).then(function { setTimeout(function { // Hide the progress message after 5 seconds               $('#gmblp-status').css('display', 'none').empty;            }, 5000); });

}); }

/** * Function to append a CSS style tag for the script and trigger markup */ function initializeMarkBLocked {

// Append $('head').append(       ' ' +            '.gmbl-userlink {' +                'opacity: 0.85;' +            '}' +            '.gmbl-blocked-temp.gmbl-globally-locked,' +            '.gmbl-blocked-temp.gmbl-globally-blocked-indef,' +            '.gmbl-blocked-indef.gmbl-globally-locked,' +            '.gmbl-blocked-indef.gmbl-globally-blocked-indef,' +            '.gmbl-blocked-indef.gmbl-globally-blocked-temp {' +                'opacity: 0.4;' +                'text-decoration: line-through;' +                'border-bottom: dashed medium red;' +            '}' +            '.gmbl-blocked-partial.gmbl-globally-locked,' +            '.gmbl-blocked-partial.gmbl-globally-blocked-temp {' +                'opacity: 0.7;' +                'text-decoration: underline dotted;' +                'border-bottom: dashed medium red;' +            '}' +            '.gmbl-blocked-temp.gmbl-globally-blocked-temp {' + 'opacity: 0.7;' + 'text-decoration: line-through;' + 'border-bottom: dashed medium red;' + '}' +           '.gmbl-blocked-partial.gmbl-globally-blocked-indef {' + 'opacity: 0.4;' + 'text-decoration: underline dotted;' + 'border-bottom: dashed medium red;' + '}' +           '.gmbl-blocked-temp:not(.gmbl-globally-locked):not(.gmbl-globally-blocked-indef):not(.gmbl-globally-blocked-temp) {' + 'opacity: 0.7;' + 'text-decoration: line-through;' + '}' +           '.gmbl-blocked-indef:not(.gmbl-globally-locked):not(.gmbl-globally-blocked-indef):not(.gmbl-globally-blocked-temp) {' + 'opacity: 0.4;' + 'text-decoration: line-through;' + '}' +           '.gmbl-blocked-partial:not(.gmbl-globally-locked):not(.gmbl-globally-blocked-indef):not(.gmbl-globally-blocked-temp) {' + 'text-decoration: underline dotted;' + '}' +           '.gmbl-globally-locked:not(.gmbl-blocked-temp):not(.gmbl-blocked-indef):not(.gmbl-blocked-partial),' + '.gmbl-globally-blocked-indef:not(.gmbl-blocked-temp):not(.gmbl-blocked-indef):not(.gmbl-blocked-partial) {' + 'opacity: 0.4;' + 'border-bottom: dashed medium red;' + '}' +           '.gmbl-globally-blocked-temp:not(.gmbl-blocked-temp):not(.gmbl-blocked-indef):not(.gmbl-blocked-partial) {' + 'opacity: 0.7;' + 'border-bottom: dashed medium red;' + '}' +       ' '    );

// Mark up links mw.hook('wikipage.content').add(markupUserLinks); // Check links again when the DOM is updated on Special:Recentchanges and Special:Watchlist

}

/** * Function to find all user links that match the RegExps and save them into 'userLinks', and then send API requests to check (b)lock status */ var userLinks = {}; // {'username': [, , ...], 'username2': [ , , ...], ...} function markupUserLinks {

// RegExp to filter out userlinks var contribs_CA = MarkBLocked.contribs_CA.length === 0 ? '' : '|' + MarkBLocked.contribs_CA.join('|').replace(/[ _]/g, '[ _]'); var regex = { user: new RegExp('^(?:' + userNs + '|(?:' + specialNs + '):(?:contrib(?:ution)?s|ca|centralauth' + contribs_CA + ')\/)+([^\/#]+|(?:\.*\/\\d\\d)+)$', 'i'), /* Matches 'User:USERNAME', 'User talk:USERNAME', 'Special:Contributions/USERNAME', and 'Special:CentralAuth/USERNAME' Note: Exclude slashes and hashes because a USERNAME containing them is an article name (e.g. User:Dragoniez/sandbox, Wikipedia:Sandbox#section) But IP ranges always contain a slash (e.g. /64); hence exclude strings ending with slash-digit-digit                                          */ page: new RegExp(mw.config.get('wgArticlePath').replace('$1', '') + '([^#]+)'), // Matches '/wiki/PAGENAME' in URLs script: new RegExp(mw.config.get('wgScript') + '\\?title=([^#&]+)') // Matches '/w/index.php?title=PAGENAME' in URLs };

$(mw.util.$content).find('a').each(function(i, lnk) { // Loop through all links in the page content

// No need to look at certain links if ($(lnk).hasClass('mw-changeslist-date') || $(lnk).parent('span').hasClass('mw-history-undo') || $(lnk).parent('span').hasClass('mw-rollback-link')) return;

// Get the href attribute of the link var url = $(lnk).attr('href'); if (!url) return;

// Extract a page title from the href var mtch, pageTitle; if (mtch = regex.page.exec(url)) { pageTitle = mtch[1]; } else if (mtch = regex.script.exec(url)) { pageTitle = mtch[1]; } else { return; }       pageTitle = decodeURIComponent(pageTitle).replace(/_/g, ' ');

// Extract a username from the page title var username = regex.user.exec(pageTitle); if (!username) return; username = username[1]; if (mw.util.isIPv6Address(username, true)) username = username.toUpperCase; // Alphabets in IPv6s are case-insensitive $(lnk).addClass('gmbl-userlink'); if (!userLinks[username]) userLinks[username] = []; if ($.inArray(lnk, userLinks[username]) === -1) { userLinks[username].push(lnk); // {'username': [, , ...], 'username2': [ , , ...], ...} }

});   if ($.isEmptyObject(userLinks)) return;

// Get all usernames from the object 'userLinks' and sort them into registered users and IPs var regUsers = [], ips = []; for (var un in userLinks) { if (mw.util.isIPAddress(un, true)) { ips.push(un); } else { if (!un.match(/[/@#<>\[\]\|{}:]/)) regUsers.push(un); }   }    var users = [].concat(regUsers, ips);

// Check (b)lock status and mark up the links if needed markBlockedUsers(users); if (prefs.localips) markIpsInBlockedRanges(ips); if (prefs.globalusers) markLockedUsers(regUsers); if (prefs.globalips) markGloballyBlockedIps(ips);

}

/** * Function to get the local block status of registered users and single IPs (this can't check whether single IPs are in a range that's been locally blocked) * @param {Array} usersArr */ function markBlockedUsers(usersArr) {

// Create a copy of the passed array (this array will be spliced; prevent pass-by-reference just in case) var users = JSON.parse(JSON.stringify(usersArr));

// Check if the current user has the 'apihighlimits' user right var userGroups = MarkBLocked.apihighlimits.concat(['apihighlimits-requestor', 'founder', 'global-bot', 'global-sysop', 'staff', 'steward', 'sysadmin', 'wmf-researcher']); var apihighlimits = [].concat(mw.config.get('wgUserGroups'), mw.config.get('wgGlobalGroups')).some(function(curUsersGroup) {       return $.inArray(curUsersGroup, userGroups) !== -1;    }); var bklimit = apihighlimits ? 500 : 50; // Better performance for users with 'apihighlimits'

// Functon to make an API call to check whether given users are locally blocked var query = function(arr) { new mw.Api.post({ // This MUST be a POST request because the parameters can exceed the word count limit of URI           action: 'query',            list: 'blocks',            bklimit: bklimit,            bkusers: arr.join('|'),            bkprop: 'user|expiry|restrictions',            formatversion: '2'        }).then(function(res){

var resBlk, clss, links; if (!res || !res.query || !(resBlk = res.query.blocks)) return; if (resBlk.length === 0) return;

resBlk.forEach(function(obj) {               var partialBlk = obj.restrictions && !Array.isArray(obj.restrictions); // Boolean: True if partial block                if (obj.expiry.indexOf('in') !== -1) { // Has the value 'infinity' if blocked indefinitely                    clss = partialBlk ? 'gmbl-blocked-partial' : 'gmbl-blocked-indef';                } else {                    clss = partialBlk ? 'gmbl-blocked-partial' : 'gmbl-blocked-temp';                }                links = userLinks[obj.user]; // Get all links related to the user                for (var i = 0; links && i < links.length; i++) $(links[i]).addClass(clss); // Assign class            });

}).catch(function(code, err) { mw.log.error(err.error.info); });   };

// Send API calls while (users.length !== 0) { query(users.slice(0, bklimit)); users.splice(0, bklimit); }

}

/** * Function to check whether single IPs are locally blocked AND whether they are included in any locally-blocked IP range * @param {Array} ipsArr */ function markIpsInBlockedRanges(ipsArr) {

var query = function(ip) { new mw.Api.get({           action: 'query',            list: 'blocks',            bklimit: '1', // Only one IP can be checked in one API call, which means it's neccesary to send as many API requests as the            bkip: ip,     // length of the array. You can see why we need the personal preferences: This can lead to performance issues.             bkprop: 'user|expiry|restrictions',            formatversion: '2'        }).then(function(res){

var resBlk, clss, links; if (!res || !res.query || !(resBlk = res.query.blocks)) return; if (resBlk.length === 0) return;

resBlk = resBlk[0]; var partialBlk = resBlk.restrictions && !Array.isArray(resBlk.restrictions); if (resBlk.expiry.indexOf('in') !== -1) { clss = partialBlk ? 'gmbl-blocked-partial' : 'gmbl-blocked-indef'; } else { clss = partialBlk ? 'gmbl-blocked-partial' : 'gmbl-blocked-temp'; }           links = userLinks[ip]; for (var i = 0; links && i < links.length; i++) $(links[i]).addClass(clss);

}).catch(function(code, err) { mw.log.error(err.error.info); });   };

ipsArr.forEach(function(ip) {       query(ip);    });

}

/** * Function to get the global lock status of registered users * @param {Array} regUsersArr */ function markLockedUsers(regUsersArr) {

var query = function(regUser) { new mw.Api.get({           action: 'query',            list: 'globalallusers',            agulimit: '1',            agufrom: regUser,            aguto: regUser,            aguprop: 'lockinfo',            formatversion: '2'        }).then(function(res) {

var resLck, locked, links; if (!res || !res.query || !(resLck = res.query.globalallusers)) return; if (resLck.length === 0) return;

locked = resLck[0].locked === ''; if (locked) { links = userLinks[regUser]; for (var i = 0; links && i < links.length; i++) $(links[i]).addClass('gmbl-globally-locked'); }

}).catch(function(code, err) { mw.log.error(err.error.info); });   };

regUsersArr.forEach(function(regUser) {       query(regUser);    }); }

/** * Function to get the global block status of IPs; can check whether single IPs are included in globally-blocked IP ranges * @param {Array} ipsArr */ function markGloballyBlockedIps(ipsArr) {

var query = function(ip) { new mw.Api.get({           action: 'query',            list: 'globalblocks',            bgip: ip,            bglimit: '1',            bgprop: 'address|expiry',            formatversion: '2'        }).then(function(res){

var resBlk, clss, links; if (!res || !res.query || !(resBlk = res.query.globalblocks)) return; if (resBlk.length === 0) return; // resBlk is an empty array if the IP isn't globally blocked

resBlk = resBlk[0]; if (resBlk.expiry.indexOf('in') !== -1) { clss = 'gmbl-globally-blocked-indef'; } else { clss = 'gmbl-globally-blocked-temp'; }           links = userLinks[ip]; for (var i = 0; links && i < links.length; i++) $(links[i]).addClass(clss);

}).catch(function(code, err) { mw.log.error(err.error.info); });   };

ipsArr.forEach(function(ip) {       query(ip);    }); }

// *****************************************************************************************

})(mediaWiki, jQuery); //