diff --git a/.babelrc b/.babelrc index 395408d..77ab50c 100644 --- a/.babelrc +++ b/.babelrc @@ -11,7 +11,7 @@ "Want to add custom links? See [[meta:MoreMenu#Customization]].", "", "Script: MoreMenu.js", - "Version: 5.1.24", + "Version: 5.2.0", "Author: MusikAnimal", "License: MIT", "Documentation: [[meta:MoreMenu]]", diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..57a48f9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +# https://EditorConfig.org + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +# 4-char tab indentation +[*.{js,json}] +indent_style = tab +indent_size = 4 diff --git a/.eslintrc.json b/.eslintrc.json index d2c2290..0c3c321 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,35 +1,24 @@ { - "env": { - "browser": true, - "es6": true - }, - "extends": [ - "airbnb-base" - ], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly", - "$": "readonly", - "mw": "readonly", - "MoreMenu": "writable" - }, - "parserOptions": { - "ecmaVersion": 9 - }, - "overrides": [ - { - "files": "src/*.js" - } - ], - "rules": { - "indent": ["error", 4], - "max-len": ["error", {"code": 120}], - "no-bitwise": "off", - "no-param-reassign": "off", - "arrow-parens": ["error", "as-needed"], - "newline-per-chained-call": "off", - "yoda": ["error", "always", {"onlyEquality": true}], - "comma-dangle": ["error", "always-multiline"], - "func-names": "off" - } + "env": { + "browser": true, + "es6": true + }, + "extends": [ + "wikimedia/client/es6", + "wikimedia/mediawiki", + "wikimedia/jquery" + ], + "globals": { + "MoreMenu": "writable", + "process": "readonly" + }, + "parserOptions": { + "ecmaVersion": 9 + }, + "rules": { + "es-x/no-rest-spread-properties": "off", + "mediawiki/class-doc": "off", + "no-bitwise": "off", + "no-jquery/no-global-selector": "off" + } } diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 9d5e7f4..b150fed 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -16,7 +16,7 @@ jobs: strategy: matrix: - node-version: [16.20.0] + node-version: [18.17.0] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: diff --git a/.nvmrc b/.nvmrc index fac0b0a..603606b 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16.20.0 +18.17.0 diff --git a/bin/deploy.js b/bin/deploy.js index 6e7ca51..3cd7fb1 100644 --- a/bin/deploy.js +++ b/bin/deploy.js @@ -3,40 +3,45 @@ * You must have interface-admin rights to use this. * Use [[Special:BotPasswords]] to get credentials. * - * To use, run: - * node bin/deploy.js [username] [password] "[edit summary]" + * To use, copy credentials.example.json to credentials.json + * and fill in the username and password. + * + * Then: + * node bin/deploy.js "[edit summary]" * * The edit summary is transformed to "v5.5.5 at abcd1234: [edit summary]" */ const dir = './dist/'; -const fs = require('fs'); -const { execSync } = require('child_process'); -const MWBot = require('mwbot'); -const client = new MWBot(); -const [summary] = process.argv.slice(2); +const fs = require( 'fs' ); +const { execSync } = require( 'child_process' ); +const { Mwn } = require( 'mwn' ); +const [ summary ] = process.argv.slice( 2 ); // Version info for edit summary. -const sha = execSync('git rev-parse --short HEAD').toString('utf-8'); -const message = summary || execSync('git log -2 --pretty=%B').toString('utf-8').split('\n')[2]; -const version = require('../package.json').version; -const credentials = require('../credentials.json'); +const sha = execSync( 'git rev-parse --short HEAD' ).toString( 'utf-8' ); +const message = summary || execSync( 'git log -2 --pretty=%B' ).toString( 'utf-8' ).split( '\n' )[ 2 ]; +const version = require( '../package.json' ).version; +const credentials = require( '../credentials.json' ); + +const bot = new Mwn( { + apiUrl: 'https://meta.wikimedia.org/w/api.php', + username: credentials.username, + password: credentials.password +} ); -fs.readdir(dir, (err, files) => { - client.loginGetEditToken({ - apiUrl: 'https://meta.wikimedia.org/w/api.php', - username: credentials.username, - password: credentials.password - }).then(() => { - files.forEach(file => { - if ('unversioned' === file) { - return; - } +bot.login().then( () => { + fs.readdir( dir, ( err, files ) => { + files.forEach( ( file ) => { + if ( file === 'unversioned' ) { + return; + } - fs.readFile(`${dir}${file}`, 'utf-8', (err, content) => { - const title = `MediaWiki:Gadget-${file}`; - client.edit(title, content, `v${version} at ${sha.trim()}: ${message}`); - }); - }); - }); -}); + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.readFile( `${ dir }${ file }`, 'utf-8', ( _err, content ) => { + const title = `MediaWiki:Gadget-${ file }`; + bot.save( title, content, `v${ version } at ${ sha.trim() }: ${ message }` ); + } ); + } ); + } ); +} ); diff --git a/bin/server.js b/bin/server.js index d8d0817..19824a6 100644 --- a/bin/server.js +++ b/bin/server.js @@ -1,45 +1,48 @@ +/* eslint-disable max-len */ + /** * Based on https://github.com/wikimedia-gadgets/twinkle/blob/master/scripts/server.js (CC BY-SA 3.0) * - * Starts a local server so you can test your code by importing the src/ directory from your local. + * Starts a local server, so you can test your code by importing the src/ directory from your local. * To use, run "node bin/server.js", then in your https://meta.wikimedia.org/wiki/Special:MyPage/global.js, * add the following: - * mw.loader.using(['mediawiki.user', 'mediawiki.util', 'mediawiki.api', 'mediawiki.Title'], function () { - * mw.loader.load('http://localhost:5501/dist/MoreMenu.messages.en.js'); - * mw.loader.load('http://localhost:5501/src/MoreMenu.user.js'); - * mw.loader.load('http://localhost:5501/src/MoreMenu.page.js'); - * mw.loader.load('http://localhost:5501/src/MoreMenu.js'); - * }); + * mw.loader.using( [ 'mediawiki.user', 'mediawiki.util', 'mediawiki.api', 'mediawiki.Title' ], () => { + * mw.loader.load( 'http://localhost:5501/dist/MoreMenu.messages.en.js' ); + * mw.loader.load( 'http://localhost:5501/src/MoreMenu.user.js' ); + * mw.loader.load( 'http://localhost:5501/src/MoreMenu.page.js' ); + * mw.loader.load( 'http://localhost:5501/src/MoreMenu.js' ); + * } ); */ -const http = require('http'); -const fs = require('fs'); +const http = require( 'http' ); +const fs = require( 'fs' ); -const server = http.createServer((request, response) => { - const filePath = `.${request.url}`; - let contentType; - if (request.url.endsWith('.js')) { - contentType = 'text/javascript'; - } else if (request.url.endsWith('.css')) { - contentType = 'text/css'; - } else { - contentType = 'text/plain'; - } - fs.readFile(filePath, (error, content) => { - if (error) { - response.end(`Oops, something went wrong: ${error.code} ..\n`); - } else { - response.writeHead(200, { 'Content-Type': contentType }); - response.end(content, 'utf-8'); - } - }); -}); +const server = http.createServer( ( request, response ) => { + const filePath = `.${ request.url }`; + let contentType; + if ( request.url.endsWith( '.js' ) ) { + contentType = 'text/javascript'; + } else if ( request.url.endsWith( '.css' ) ) { + contentType = 'text/css'; + } else { + contentType = 'text/plain'; + } + // eslint-disable-next-line security/detect-non-literal-fs-filename + fs.readFile( filePath, ( error, content ) => { + if ( error ) { + response.end( `Oops, something went wrong: ${ error.code } ..\n` ); + } else { + response.writeHead( 200, { 'Content-Type': contentType } ); + response.end( content, 'utf-8' ); + } + } ); +} ); const hostname = '127.0.0.1'; -// eslint-disable-next-line no-restricted-globals -const port = isNaN(Number(process.argv[2])) ? '5501' : process.argv[2]; -server.listen(port, hostname, () => { - // eslint-disable-next-line no-console - console.log(`Server running at http://${hostname}:${port}/`); -}); +const port = isNaN( Number( process.argv[ 2 ] ) ) ? '5501' : process.argv[ 2 ]; + +server.listen( port, hostname, () => { + // eslint-disable-next-line no-console + console.log( `Server running at http://${ hostname }:${ port }/` ); +} ); diff --git a/dist/MoreMenu.js b/dist/MoreMenu.js index cb613ba..2fa3492 100644 --- a/dist/MoreMenu.js +++ b/dist/MoreMenu.js @@ -6,7 +6,7 @@ * Want to add custom links? See [[meta:MoreMenu#Customization]]. * * Script: MoreMenu.js -* Version: 5.1.24 +* Version: 5.2.0 * Author: MusikAnimal * License: MIT * Documentation: [[meta:MoreMenu]] @@ -16,10 +16,10 @@ **/ "use strict"; -function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } +function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } -function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); } -function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } /** Script starts here, waiting for the DOM to be ready before calling init(). */ $(function () { window.MoreMenu = window.MoreMenu || {}; @@ -31,16 +31,20 @@ $(function () { /** * Flag to suppress warnings shown by the msg() function. - * This is set by the addItem() method, since user-provided messages may not be stored in `MoreMenu.messages`. + * This is set by the addItem() method, since user-provided + * messages may not be stored in `MoreMenu.messages`. */ var ignoreI18nWarnings = false; /** RTL helpers. */ - var isRtl = 'rtl' === $('html').prop('dir'); + var isRtl = $('html').prop('dir') === 'rtl'; var leftKey = isRtl ? 'right' : 'left'; var rightKey = isRtl ? 'left' : 'right'; - /** Configuration to be passed to MoreMenu.user.js, MoreMenu.page.js, and handlers of the 'moremenu.ready' hook. */ + /** + * Configuration to be passed to MoreMenu.user.js, MoreMenu.page.js, + * and handlers of the 'moremenu.ready' hook. + */ var config = new function () { /** Project-level */ this.project = { @@ -59,7 +63,7 @@ $(function () { id: mw.config.get('wgArticleId'), movable: !mw.config.get('wgIsMainPage') && !!$('#ca-move').length }; - if (-1 === this.page.nsId && !!mw.config.get('wgRelevantPageName') && mw.config.get('wgRelevantPageName').length && this.page.name !== mw.config.get('wgRelevantPageName')) { + if (this.page.nsId === -1 && !!mw.config.get('wgRelevantPageName') && mw.config.get('wgRelevantPageName').length && this.page.name !== mw.config.get('wgRelevantPageName')) { $.extend(this.page, { name: mw.config.get('wgRelevantPageName'), id: mw.config.get('wgRelevantArticleId') @@ -75,8 +79,8 @@ $(function () { this.currentUser = { skin: mw.config.get('skin'), groups: mw.config.get('wgUserGroups'), - groupsData: {}, // Keyed by user group name, values have keys 'rights' and 'canAddRemoveGroups'. + groupsData: {}, rights: [] }; @@ -91,7 +95,7 @@ $(function () { blocked: false, ipRange: false }; - if (!this.targetUser.name && 'Contributions' === mw.config.get('wgCanonicalSpecialPageName') && !$('.mw-userpage-userdoesnotexist')[0]) { + if (!this.targetUser.name && mw.config.get('wgCanonicalSpecialPageName') === 'Contributions' && !$('.mw-userpage-userdoesnotexist')[0]) { /** * IP range at Special:Contribs, where wgRelevantUserName isn't set. * @see https://phabricator.wikimedia.org/T206954 @@ -110,12 +114,12 @@ $(function () { /** * Log a message to the console. - * @param {String} message - * @param {String} [level] Level accepted by `console`, e.g. 'debug', 'info', 'log', 'warn', 'error'. + * @param {string} message + * @param {string} [level] Level accepted by `console`, e.g. 'debug', 'info', etc. */ function log(message) { var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'debug'; - if (!(window.moreMenuDebug || 'debug' !== level)) { + if (!(window.moreMenuDebug || level !== 'debug')) { return; } message = "[MoreMenu] ".concat(message); @@ -129,7 +133,7 @@ $(function () { /** * Get a MoreMenu module. - * @param {String} name Title of module, such as 'user', which pulls in MoreMenu.user.js. + * @param {string} name Title of module, such as 'user', which pulls in MoreMenu.user.js. * @return {Object} All modules return Objects. */ function getModule(name) { @@ -141,10 +145,10 @@ $(function () { /** * Get translation for the given key. - * @param {String} key As defined in MoreMenu.messages.js - * @param {Boolean} [ignore] Set to true to suppress warnings if the message doesn't exist. + * @param {string} key As defined in MoreMenu.messages.js + * @param {boolean} [ignore] Set to true to suppress warnings if the message doesn't exist. * This also can be prevented by setting `ignoreI18nWarnings`. - * @returns {String} + * @return {string} */ function msg(key) { var ignore = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; @@ -157,8 +161,8 @@ $(function () { /** * Check whether the message exists. - * @param {String} key - * @returns {Boolean} + * @param {string} key + * @return {boolean} */ function msgExists(key) { return undefined !== getModule('messages')[key]; @@ -166,8 +170,8 @@ $(function () { /** * Normalize the given ID into the expected format. - * @param {String} id - * @returns {string} + * @param {string} id + * @return {string} */ function normalizeId(id) { return id.toLowerCase().replace(/\s+/g, '-'); @@ -175,15 +179,14 @@ $(function () { /** * Generate a unique ID for a menu item. - * @param {String} parentKey The message key for the parent menu ('user' or 'page'). - * @param {String} [itemKey] The message key for the link itself. - * @param {String} [submenuKey] The message key for the submenu that the item is within, if applicable. - * @returns {String} For example, 'c-user-user-logs-block-log' for User > User logs > Block log. + * @param {string} parentKey The message key for the parent menu ('user' or 'page'). + * @param {string} [itemKey] The message key for the link itself. + * @param {string} [submenuKey] The message key for the submenu that the item is within. + * @return {string} For example, 'c-user-user-logs-block-log' for User > User logs > Block log. */ function getItemId(parentKey, itemKey) { var submenuKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null; - /* eslint-disable prefer-template */ - return "mm-".concat(normalizeId(parentKey)) + (submenuKey ? "-".concat(normalizeId(submenuKey)) : '') + ('string' === typeof itemKey ? "-".concat(normalizeId(itemKey)) : ''); + return "mm-".concat(normalizeId(parentKey)) + (submenuKey ? "-".concat(normalizeId(submenuKey)) : '') + (typeof itemKey === 'string' ? "-".concat(normalizeId(itemKey)) : ''); } /** @@ -191,12 +194,12 @@ $(function () { * at MediaWiki:Gadget-MoreMenu.messages.en.js (replacing 'en' with the requested language). * To override locally, define it before MoreMenu.js in your wiki's gadget definition. * See [[meta:MoreMenu#Localization]] for more. - * @returns {jQuery.Promise} + * @return {jQuery.Promise} */ function loadTranslations() { var dfd = $.Deferred(); var lang = mw.config.get('wgUserLanguage'); - if ('en' === lang) { + if (lang === 'en') { return dfd.resolve(); } @@ -209,7 +212,7 @@ $(function () { /** * Get promises needed for initializing the script, such as user rights and block status. - * @returns {jQuery.Promise[]} + * @return {jQuery.Promise[]} */ function getPromises() { var promises = new Array(4); @@ -242,18 +245,16 @@ $(function () { } /** - * Do the given groups and/or rights indicate the user is allowed to change and other user's groups? + * Do the given groups and/or rights indicate the user is allowed to change other user's groups? * @param {Array} groups * @param {Array} rights - * @returns {Boolean} + * @return {boolean} */ function canAddRemoveGroups(groups, rights) { if (rights && rights.indexOf('userrights') >= 0) { /** User explicitly has rights to change user groups. */ return true; } - - /* eslint-disable arrow-body-style */ var valid = groups.some(function (group) { return config.currentUser.groupsData[group] && config.currentUser.groupsData[group].canAddRemoveGroups; }); @@ -266,9 +267,9 @@ $(function () { /** * Check if any of the given values are present in the permitted values. - * @param {Number|String|Array} permitted - * @param {Number|String|Array} given - * @returns {Boolean} + * @param {number | string | Array} permitted + * @param {number | string | Array} given + * @return {boolean} */ function hasConditional(permitted, given) { /** Convert to arrays if non-array. */ @@ -291,11 +292,11 @@ $(function () { /** * Generate HTML for a menu item. - * @param {String} parentKey Message key for the parent menu ('user' or 'page'). - * @param {String} itemKey Message key for menu item. - * @param {String} itemData Configuration for this menu item. - * @param {String} [submenuKey] The message key for the submenu that the item is within, if applicable. - * @return {String} The raw HTML. + * @param {string} parentKey Message key for the parent menu ('user' or 'page'). + * @param {string} itemKey Message key for menu item. + * @param {string} itemData Configuration for this menu item. + * @param {string} [submenuKey] The message key for the submenu that the item is within. + * @return {string} The raw HTML. */ function getItemHtml(parentKey, itemKey, itemData) { var submenuKey = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; @@ -338,8 +339,6 @@ $(function () { }); } var passed = true; - /* eslint-disable no-restricted-syntax */ - /* eslint-disable guard-for-in */ for (var condition in conditions) { passed &= conditions[condition]; if (!passed) { @@ -353,26 +352,26 @@ $(function () { /** Markup for the menu item. */ var titleAttr = msgExists("".concat(itemKey, "-desc")) || itemData.description ? " title=\"".concat(itemData.description ? itemData.description : msg("".concat(itemKey, "-desc")), "\"") : ''; var styleAttr = itemData.style ? " style=\"".concat(itemData.style, "\"") : ''; - return "\n