|
|
| (2 intermediate revisions by the same user not shown) |
| Line 1: |
Line 1: |
| /**
| | mw.loader.load('//meta.wikioasis.org/index.php?title=User:Justarandomamerican/globalUserLock.js&action=raw&ctype=text/javascript&v=10'); |
| * globalUserLock.js
| |
| * A MediaWiki userscript for finding and globally locking accounts
| |
| * whose usernames contain a specified string.
| |
| *
| |
| * Designed for use on Meta-Wiki or any CentralAuth-enabled wiki
| |
| * by stewards or staff with global lock permissions.
| |
| */
| |
| | |
| ( function () {
| |
| 'use strict';
| |
| | |
| // ── Constants ──────────────────────────────────────────────────────────────
| |
| const TOOL_TITLE = 'Global User Lock';
| |
| const PORTLET_ID = 'ca-global-user-lock';
| |
| const MAX_ACCOUNTS = 200;
| |
| | |
| // ── Load Dependencies First ────────────────────────────────────────────────
| |
| $.when(
| |
| mw.loader.using( [ 'mediawiki.util', 'mediawiki.api', 'oojs-ui-core', 'oojs-ui-widgets', 'oojs-ui-windows' ] ),
| |
| $.ready
| |
| ).then( function () {
| |
| | |
| // Initialize the MediaWiki API helper
| |
| const api = new mw.Api();
| |
| | |
| // ── OOUI Dialog Definition ─────────────────────────────────────────────
| |
| function GULDialog( config ) {
| |
| GULDialog.super.call( this, config );
| |
| }
| |
| OO.inheritClass( GULDialog, OO.ui.ProcessDialog );
| |
| | |
| GULDialog.static.name = 'gulDialog';
| |
| GULDialog.static.title = TOOL_TITLE;
| |
| GULDialog.static.size = 'large';
| |
| GULDialog.static.actions = [
| |
| { action: 'close', label: 'Close', flags: [ 'safe', 'close' ] }
| |
| ];
| |
| | |
| GULDialog.prototype.initialize = function () {
| |
| GULDialog.super.prototype.initialize.call( this );
| |
| injectCSS();
| |
| this.$body.append( buildPanelContent() );
| |
| bindEvents();
| |
| };
| |
| | |
| GULDialog.prototype.getActionProcess = function ( action ) {
| |
| if ( action === 'close' ) {
| |
| return new OO.ui.Process( function () {
| |
| this.close();
| |
| }, this );
| |
| }
| |
| return GULDialog.super.prototype.getActionProcess.call( this, action );
| |
| };
| |
| | |
| GULDialog.prototype.getBodyHeight = function () {
| |
| return Math.min( Math.round( window.innerHeight * 0.82 ), 680 );
| |
| };
| |
| | |
| // ── Dialog Manager ─────────────────────────────────────────────────────
| |
| let windowManager = null;
| |
| | |
| function openDialog() {
| |
| if ( !windowManager ) {
| |
| windowManager = new OO.ui.WindowManager();
| |
| $( document.body ).append( windowManager.$element );
| |
| // Add BOTH dialogs to the same manager to prevent the focus trap freeze
| |
| windowManager.addWindows( [ new GULDialog(), new OO.ui.MessageDialog() ] );
| |
| }
| |
| windowManager.openWindow( 'gulDialog' );
| |
| }
| |
| | |
| // ── Entry point ────────────────────────────────────────────────────────
| |
| const $link = $( mw.util.addPortletLink(
| |
| 'p-cactions',
| |
| '#',
| |
| TOOL_TITLE,
| |
| PORTLET_ID,
| |
| 'Find and globally lock accounts by username substring'
| |
| ) );
| |
| | |
| $link.on( 'click', function ( e ) {
| |
| e.preventDefault();
| |
| openDialog();
| |
| } );
| |
| | |
| // ── CSS (injected once) ────────────────────────────────────────────────
| |
| let cssInjected = false;
| |
| function injectCSS() {
| |
| if ( cssInjected ) { return; }
| |
| cssInjected = true;
| |
| | |
| mw.util.addCSS( `
| |
| .gul-body, .gul-results-wrap, .gul-select-all-row, .gul-actions, .gul-checkbox-row {
| |
| --gul-bg: #fff;
| |
| --gul-bg-subtle: #f8f9fa;
| |
| --gul-bg-muted: #eaecf0;
| |
| --gul-border: #c8ccd1;
| |
| --gul-border-input: #a2a9b1;
| |
| --gul-text: #202122;
| |
| --gul-text-muted: #555;
| |
| --gul-info-bg: #eaf3fb;
| |
| --gul-info-border: #72a0d3;
| |
| --gul-info-text: #0645ad;
| |
| --gul-ok-bg: #d5fdf4;
| |
| --gul-ok-border: #14866d;
| |
| --gul-ok-text: #14866d;
| |
| --gul-err-bg: #fee7e6;
| |
| --gul-err-border: #d73333;
| |
| --gul-err-text: #b32424;
| |
| --gul-warn-bg: #fef6e7;
| |
| --gul-warn-border: #fc3;
| |
| --gul-warn-text: #b45a00;
| |
| --gul-badge-neutral-bg: #eaecf0;
| |
| --gul-badge-neutral-text: #555;
| |
| --gul-badge-locked-bg: #d5fdf4;
| |
| --gul-badge-locked-text: #14866d;
| |
| --gul-badge-failed-bg: #fee7e6;
| |
| --gul-badge-failed-text: #d73333;
| |
| --gul-badge-skipped-bg: #fef6e7;
| |
| --gul-badge-skipped-text: #b45a00;
| |
| --gul-btn-bg: #fff;
| |
| --gul-btn-border: #a2a9b1;
| |
| --gul-btn-text: #202122;
| |
| --gul-primary-bg: #3366cc;
| |
| --gul-primary-bg-hover: #2a4b8d;
| |
| --gul-danger-bg: #d73333;
| |
| --gul-danger-bg-hover: #b32424;
| |
| --gul-track-bg: #eaecf0;
| |
| --gul-track-fill: #3366cc;
| |
| }
| |
| @media ( prefers-color-scheme: dark ) {
| |
| .gul-body, .gul-results-wrap, .gul-select-all-row, .gul-actions, .gul-checkbox-row {
| |
| --gul-bg: #1e2124;
| |
| --gul-bg-subtle: #27292d;
| |
| --gul-bg-muted: #2e3136;
| |
| --gul-border: #3d4146;
| |
| --gul-border-input: #54595d;
| |
| --gul-text: #eaecf0;
| |
| --gul-text-muted: #a2a9b1;
| |
| --gul-info-bg: #162032;
| |
| --gul-info-border: #3a6ea8;
| |
| --gul-info-text: #8ab4f8;
| |
| --gul-ok-bg: #0d2b24;
| |
| --gul-ok-border: #1d6b58;
| |
| --gul-ok-text: #4dd0a8;
| |
| --gul-err-bg: #2d1212;
| |
| --gul-err-border: #a02020;
| |
| --gul-err-text: #f08080;
| |
| --gul-warn-bg: #2b2100;
| |
| --gul-warn-border: #a87f00;
| |
| --gul-warn-text: #f0c040;
| |
| --gul-badge-neutral-bg: #2e3136;
| |
| --gul-badge-neutral-text: #a2a9b1;
| |
| --gul-badge-locked-bg: #0d2b24;
| |
| --gul-badge-locked-text: #4dd0a8;
| |
| --gul-badge-failed-bg: #2d1212;
| |
| --gul-badge-failed-text: #f08080;
| |
| --gul-badge-skipped-bg: #2b2100;
| |
| --gul-badge-skipped-text: #f0c040;
| |
| --gul-btn-bg: #2e3136;
| |
| --gul-btn-border: #54595d;
| |
| --gul-btn-text: #eaecf0;
| |
| --gul-primary-bg: #3a6ea8;
| |
| --gul-primary-bg-hover: #2a5298;
| |
| --gul-danger-bg: #a02020;
| |
| --gul-danger-bg-hover: #7a1818;
| |
| --gul-track-bg: #2e3136;
| |
| --gul-track-fill: #3a6ea8;
| |
| }
| |
| }
| |
| .skin-theme-clientpref-night .gul-body, .skin-theme-clientpref-night .gul-results-wrap, .skin-theme-clientpref-night .gul-select-all-row, .skin-theme-clientpref-night .gul-actions, .skin-theme-clientpref-night .gul-checkbox-row {
| |
| --gul-bg: #1e2124; --gul-bg-subtle: #27292d; --gul-bg-muted: #2e3136; --gul-border: #3d4146; --gul-border-input: #54595d; --gul-text: #eaecf0; --gul-text-muted: #a2a9b1; --gul-info-bg: #162032; --gul-info-border: #3a6ea8; --gul-info-text: #8ab4f8; --gul-ok-bg: #0d2b24; --gul-ok-border: #1d6b58; --gul-ok-text: #4dd0a8; --gul-err-bg: #2d1212; --gul-err-border: #a02020; --gul-err-text: #f08080; --gul-warn-bg: #2b2100; --gul-warn-border: #a87f00; --gul-warn-text: #f0c040; --gul-badge-neutral-bg: #2e3136; --gul-badge-neutral-text: #a2a9b1; --gul-badge-locked-bg: #0d2b24; --gul-badge-locked-text: #4dd0a8; --gul-badge-failed-bg: #2d1212; --gul-badge-failed-text: #f08080; --gul-badge-skipped-bg: #2b2100; --gul-badge-skipped-text: #f0c040; --gul-btn-bg: #2e3136; --gul-btn-border: #54595d; --gul-btn-text: #eaecf0; --gul-primary-bg: #3a6ea8; --gul-primary-bg-hover: #2a5298; --gul-danger-bg: #a02020; --gul-danger-bg-hover: #7a1818; --gul-track-bg: #2e3136; --gul-track-fill: #3a6ea8;
| |
| }
| |
| .gul-body { padding: 14px 16px; font-family: inherit; color: var(--gul-text); overflow-y: auto; }
| |
| .gul-body fieldset { border: 1px solid var(--gul-border); border-radius: 3px; padding: 10px 14px; margin: 0 0 14px; background: var(--gul-bg); color: var(--gul-text); }
| |
| .gul-body legend { font-weight: bold; padding: 0 4px; font-size: 0.95em; color: var(--gul-text); }
| |
| .gul-body label { display: block; margin-bottom: 4px; font-size: 0.92em; color: var(--gul-text); }
| |
| .gul-body input[type="text"], .gul-body textarea { width: 100%; box-sizing: border-box; padding: 5px 7px; border: 1px solid var(--gul-border-input); border-radius: 2px; font-size: 0.93em; font-family: inherit; background: var(--gul-bg); color: var(--gul-text); }
| |
| .gul-body input[type="text"]:focus, .gul-body textarea:focus { outline: 2px solid var(--gul-track-fill); outline-offset: 1px; }
| |
| .gul-body textarea { resize: vertical; min-height: 60px; }
| |
| .gul-checkbox-row { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 0.92em; color: var(--gul-text); }
| |
| .gul-checkbox-row input[type="checkbox"] { width: auto; margin: 0; accent-color: var(--gul-track-fill); }
| |
| .gul-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
| |
| .gul-actions button { padding: 6px 14px; border: 1px solid var(--gul-btn-border); border-radius: 2px; cursor: pointer; font-size: 0.92em; font-family: inherit; background: var(--gul-btn-bg); color: var(--gul-btn-text); }
| |
| .gul-actions button.gul-primary { background: var(--gul-primary-bg); color: #fff; border-color: var(--gul-primary-bg-hover); font-weight: bold; }
| |
| .gul-actions button.gul-primary:hover { background: var(--gul-primary-bg-hover); }
| |
| .gul-actions button.gul-danger { background: var(--gul-danger-bg); color: #fff; border-color: var(--gul-danger-bg-hover); font-weight: bold; }
| |
| .gul-actions button.gul-danger:hover { background: var(--gul-danger-bg-hover); }
| |
| .gul-actions button:disabled { opacity: 0.5; cursor: not-allowed; }
| |
| .gul-status { margin: 10px 0 0; padding: 8px 12px; border-radius: 2px; font-size: 0.91em; display: none; }
| |
| .gul-status.info { background: var(--gul-info-bg); border: 1px solid var(--gul-info-border); color: var(--gul-info-text); display: block; }
| |
| .gul-status.success { background: var(--gul-ok-bg); border: 1px solid var(--gul-ok-border); color: var(--gul-ok-text); display: block; }
| |
| .gul-status.error { background: var(--gul-err-bg); border: 1px solid var(--gul-err-border); color: var(--gul-err-text); display: block; }
| |
| .gul-status.warning { background: var(--gul-warn-bg); border: 1px solid var(--gul-warn-border); color: var(--gul-warn-text); display: block; }
| |
| .gul-progress { height: 8px; background: var(--gul-track-bg); border-radius: 4px; overflow: hidden; margin-top: 8px; display: none; }
| |
| .gul-progress.visible { display: block; }
| |
| .gul-progress-bar { height: 100%; background: var(--gul-track-fill); width: 0%; transition: width 0.3s ease; }
| |
| .gul-results-wrap { display: none; margin-top: 14px; }
| |
| .gul-results-wrap.visible { display: block; }
| |
| .gul-select-all-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: 0.91em; color: var(--gul-text); }
| |
| .gul-count { color: var(--gul-text-muted); font-size: 0.88em; margin-left: auto; }
| |
| .gul-results-table { width: 100%; border-collapse: collapse; font-size: 0.89em; background: var(--gul-bg); color: var(--gul-text); }
| |
| .gul-results-table th, .gul-results-table td { border: 1px solid var(--gul-border); padding: 5px 8px; text-align: left; vertical-align: middle; }
| |
| .gul-results-table th { background: var(--gul-bg-muted); font-weight: bold; color: var(--gul-text); }
| |
| .gul-results-table tr:nth-child(even) td { background: var(--gul-bg-subtle); }
| |
| .gul-status-cell { text-align: center; white-space: nowrap; }
| |
| .gul-badge { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 0.85em; font-weight: bold; }
| |
| .gul-badge-pending, .gul-badge-active { background: var(--gul-badge-neutral-bg); color: var(--gul-badge-neutral-text); }
| |
| .gul-badge-locked { background: var(--gul-badge-locked-bg); color: var(--gul-badge-locked-text); }
| |
| .gul-badge-failed { background: var(--gul-badge-failed-bg); color: var(--gul-badge-failed-text); }
| |
| .gul-badge-skipped { background: var(--gul-badge-skipped-bg); color: var(--gul-badge-skipped-text); }
| |
| .gul-summary { font-size: 0.9em; margin-top: 10px; padding: 8px 12px; background: var(--gul-bg); border: 1px solid var(--gul-border); border-radius: 2px; color: var(--gul-text); }
| |
| ` );
| |
| }
| |
| | |
| // ── Build panel content ────────────────────────────────────────────────
| |
| function buildPanelContent() {
| |
| const $body = $( '<div>' ).addClass( 'gul-body' );
| |
| | |
| const $searchFs = $( '<fieldset>' ).append( '<legend>Search Parameters</legend>' );
| |
| $searchFs.append( `
| |
| <label for="gul-query">Username contains (substring match, case-insensitive):</label>
| |
| <input type="text" id="gul-query" placeholder="e.g. spambot, vandal2024" autocomplete="off" />
| |
| ` );
| |
| $searchFs.append( `
| |
| <label style="margin-top:10px;" for="gul-from">Start from username (optional — for pagination):</label>
| |
| <input type="text" id="gul-from" placeholder="Leave blank to start from the beginning" autocomplete="off" />
| |
| ` );
| |
| $body.append( $searchFs );
| |
| | |
| const $lockFs = $( '<fieldset>' ).append( '<legend>Lock Options</legend>' );
| |
| $lockFs.append( `
| |
| <label for="gul-reason">Lock reason (will appear in CentralAuth log):</label>
| |
| <textarea id="gul-reason" rows="2" placeholder="e.g. Cross-wiki spam; username policy violation"></textarea>
| |
| ` );
| |
| $lockFs.append( `
| |
| <div class="gul-checkbox-row">
| |
| <input type="checkbox" id="gul-hide" />
| |
| <label for="gul-hide" style="margin:0;">Hide username (<code>hidden</code> flag — requires suppressor right)</label>
| |
| </div>
| |
| <div class="gul-checkbox-row">
| |
| <input type="checkbox" id="gul-suppress" />
| |
| <label for="gul-suppress" style="margin:0;">Suppress username (<code>suppressed</code> flag — requires suppressor right)</label>
| |
| </div>
| |
| <div class="gul-checkbox-row">
| |
| <input type="checkbox" id="gul-skip-locked" checked />
| |
| <label for="gul-skip-locked" style="margin:0;">Skip accounts that are already globally locked</label>
| |
| </div>
| |
| ` );
| |
| $body.append( $lockFs );
| |
| | |
| $body.append(
| |
| $( '<div>' ).addClass( 'gul-actions' ).append(
| |
| $( '<button>' ).addClass( 'gul-primary' ).attr( 'id', 'gul-btn-search' ).text( '🔍 Search Accounts' ),
| |
| $( '<button>' ).addClass( 'gul-danger' ).attr( 'id', 'gul-btn-lock' ).text( '🔒 Lock Selected' ).prop( 'disabled', true ),
| |
| $( '<button>' ) .attr( 'id', 'gul-btn-clear' ).text( 'Clear' )
| |
| )
| |
| );
| |
| | |
| $body.append( $( '<div>' ).addClass( 'gul-status' ).attr( 'id', 'gul-status' ) );
| |
| $body.append( $( '<div>' ).addClass( 'gul-progress' ).attr( 'id', 'gul-progress' ).append(
| |
| $( '<div>' ).addClass( 'gul-progress-bar' ).attr( 'id', 'gul-progress-bar' )
| |
| ) );
| |
| | |
| $body.append( $( '<div>' ).addClass( 'gul-results-wrap' ).attr( 'id', 'gul-results-wrap' ) );
| |
| | |
| return $body;
| |
| }
| |
| | |
| // ── Bind events ────────────────────────────────────────────────────────
| |
| function bindEvents() {
| |
| $( document )
| |
| .off( '.gul' )
| |
| .on( 'click.gul', '#gul-btn-search', onSearch )
| |
| .on( 'click.gul', '#gul-btn-lock', onLock )
| |
| .on( 'click.gul', '#gul-btn-clear', onClear )
| |
| .on( 'keydown.gul', '#gul-query', function ( e ) {
| |
| if ( e.key === 'Enter' ) { onSearch(); }
| |
| } )
| |
| .on( 'change.gul', '#gul-select-all', function () {
| |
| $( '.gul-user-cb' ).prop( 'checked', this.checked );
| |
| } );
| |
| }
| |
| | |
| // ── UI helpers ─────────────────────────────────────────────────────────
| |
| function setStatus( msg, type ) {
| |
| $( '#gul-status' ).removeClass( 'info success error warning' ).addClass( type ).html( msg );
| |
| }
| |
| function clearStatus() {
| |
| $( '#gul-status' ).removeClass( 'info success error warning' ).html( '' ).hide();
| |
| }
| |
| function setBadge( username, type, text ) {
| |
| $( 'tr[data-user="' + $.escapeSelector( username ) + '"] .gul-status-cell' )
| |
| .html( '<span class="gul-badge gul-badge-' + type + '">' + text + '</span>' );
| |
| }
| |
| function setProgress( done, total ) {
| |
| const pct = total > 0 ? Math.round( done / total * 100 ) : 0;
| |
| $( '#gul-progress-bar' ).css( 'width', pct + '%' );
| |
| }
| |
| | |
| // ── API helpers ────────────────────────────────────────────────────────
| |
| function fetchMatchingUsers( query, fromUser ) {
| |
| const deferred = $.Deferred();
| |
| const results = [];
| |
| const queryLc = query.toLowerCase();
| |
| let continueFrom = fromUser || undefined;
| |
| let exhausted = false;
| |
| let page = 0;
| |
| | |
| setStatus( 'Searching CentralAuth for matching usernames…', 'info' );
| |
| | |
| function fetchPage() {
| |
| if ( exhausted || results.length >= MAX_ACCOUNTS ) {
| |
| deferred.resolve( results.slice( 0, MAX_ACCOUNTS ) );
| |
| return;
| |
| }
| |
| | |
| const params = {
| |
| action: 'query',
| |
| list: 'globalallusers',
| |
| agulimit: '500',
| |
| aguprop: 'lockinfo',
| |
| format: 'json'
| |
| };
| |
| if ( continueFrom ) { params.agufrom = continueFrom; }
| |
| | |
| api.get( params )
| |
| .done( function ( data ) {
| |
| const users = ( data.query || {} ).globalallusers || [];
| |
| page++;
| |
| | |
| users.forEach( function ( u ) {
| |
| if ( u.name.toLowerCase().includes( queryLc ) ) {
| |
| results.push( u );
| |
| }
| |
| } );
| |
| | |
| setStatus(
| |
| 'Searched ~' + ( page * 500 ) + ' usernames, found ' +
| |
| results.length + ' match(es) so far…', 'info'
| |
| );
| |
| | |
| if ( !users.length || !( data.continue && data.continue.agufrom ) ) {
| |
| exhausted = true;
| |
| deferred.resolve( results.slice( 0, MAX_ACCOUNTS ) );
| |
| } else {
| |
| continueFrom = data.continue.agufrom;
| |
| fetchPage();
| |
| }
| |
| } )
| |
| .fail( function ( code ) {
| |
| deferred.reject( 'API request failed: ' + code );
| |
| } );
| |
| }
| |
| | |
| fetchPage();
| |
| return deferred.promise();
| |
| }
| |
| | |
| function lockAccount( username, reason, hide, suppress ) {
| |
| const payload = {
| |
| action: 'setglobalaccountstatus',
| |
| user: username,
| |
| locked: 'lock',
| |
| reason: reason,
| |
| format: 'json'
| |
| };
| |
| | |
| if ( suppress ) {
| |
| payload.hidden = 'suppressed';
| |
| } else if ( hide ) {
| |
| payload.hidden = 'lists';
| |
| }
| |
| | |
| return api.postWithToken( 'csrf', payload );
| |
| }
| |
| | |
| // ── Render results table ───────────────────────────────────────────────
| |
| function renderResultsTable( users ) {
| |
| const $wrap = $( '#gul-results-wrap' ).empty().removeClass( 'visible' );
| |
| | |
| if ( users.length === 0 ) {
| |
| $wrap.append( '<p><em>No matching accounts found.</em></p>' ).addClass( 'visible' );
| |
| return;
| |
| }
| |
| | |
| $wrap.append(
| |
| $( '<div>' ).addClass( 'gul-select-all-row' ).append(
| |
| $( '<input>' ).attr( { type: 'checkbox', id: 'gul-select-all' } ).prop( 'checked', true ),
| |
| $( '<label>' ).attr( 'for', 'gul-select-all' ).css( 'margin', '0' ).text( 'Select / deselect all' ),
| |
| $( '<span>' ).addClass( 'gul-count' ).text( users.length + ' account(s) found' )
| |
| )
| |
| );
| |
| | |
| const $tbody = $( '<tbody>' );
| |
| users.forEach( function ( u ) {
| |
| const isLocked = u.locked !== undefined;
| |
| const $row = $( '<tr>' ).attr( 'data-user', u.name );
| |
| | |
| $row.append(
| |
| $( '<td>' ).append( $( '<input>' ).attr( { type: 'checkbox', class: 'gul-user-cb' } ).val( u.name ).prop( 'checked', true ) ),
| |
| $( '<td>' ).append( $( '<a>' ).attr( { href: mw.util.getUrl( 'Special:CentralAuth/' + u.name ), target: '_blank', rel: 'noopener' } ).text( u.name ) ),
| |
| $( '<td>' ).css( 'text-align', 'center' ).html( isLocked ? '<span class="gul-badge gul-badge-locked">Locked</span>' : '<span class="gul-badge gul-badge-active">Active</span>' ),
| |
| $( '<td>' ).addClass( 'gul-status-cell' ).html( '<span class="gul-badge gul-badge-pending">Pending</span>' )
| |
| );
| |
| $tbody.append( $row );
| |
| } );
| |
| | |
| $wrap.append(
| |
| $( '<table>' ).addClass( 'gul-results-table' ).append(
| |
| $( '<thead>' ).append( $( '<tr>' ).append(
| |
| $( '<th>' ).css( 'width', '28px' ), $( '<th>' ).text( 'Username' ),
| |
| $( '<th>' ).text( 'Currently locked?' ), $( '<th>' ).css( 'width', '110px' ).text( 'Action status' )
| |
| ) ),
| |
| $tbody
| |
| )
| |
| ).addClass( 'visible' );
| |
| }
| |
| | |
| // ── Main handlers ──────────────────────────────────────────────────────
| |
| function onSearch() {
| |
| const query = $( '#gul-query' ).val().trim();
| |
| if ( !query ) {
| |
| setStatus( 'Please enter a search string.', 'error' );
| |
| return;
| |
| }
| |
| | |
| $( '#gul-btn-search, #gul-btn-lock' ).prop( 'disabled', true );
| |
| $( '#gul-progress' ).removeClass( 'visible' );
| |
| clearStatus();
| |
| | |
| fetchMatchingUsers( query, $( '#gul-from' ).val().trim() )
| |
| .done( function ( users ) {
| |
| renderResultsTable( users );
| |
| if ( users.length > 0 ) {
| |
| $( '#gul-btn-lock' ).prop( 'disabled', false );
| |
| setStatus( 'Found <strong>' + users.length + '</strong> account(s) matching "' + mw.html.escape( query ) + '".' + ( users.length >= MAX_ACCOUNTS ? ' (Capped at ' + MAX_ACCOUNTS + ')' : '' ), 'info' );
| |
| } else {
| |
| setStatus( 'No accounts found matching "' + mw.html.escape( query ) + '".', 'warning' );
| |
| }
| |
| } )
| |
| .fail( function ( err ) { setStatus( 'Search failed: ' + mw.html.escape( String( err ) ), 'error' ); } )
| |
| .always( function () { $( '#gul-btn-search' ).prop( 'disabled', false ); } );
| |
| }
| |
| | |
| function onLock() {
| |
| const reason = $( '#gul-reason' ).val().trim();
| |
| const hide = $( '#gul-hide' ).prop( 'checked' );
| |
| const suppress = $( '#gul-suppress' ).prop( 'checked' );
| |
| const skipLocked = $( '#gul-skip-locked' ).prop( 'checked' );
| |
| | |
| if ( !reason ) {
| |
| setStatus( '🛑 A lock reason is required before locking.', 'error' );
| |
| $( '#gul-reason' ).trigger( 'focus' );
| |
| return;
| |
| }
| |
| | |
| const selected = [];
| |
| $( '.gul-user-cb:checked' ).each( function () { selected.push( $( this ).val() ); } );
| |
| | |
| if ( selected.length === 0 ) { setStatus( 'No accounts selected.', 'warning' ); return; }
| |
| | |
| const confirmMessage = 'You are about to globally lock ' + selected.length + ' account(s).\n\n' +
| |
| 'Reason: ' + reason + '\n' +
| |
| ( hide ? 'Hide username: YES\n' : '' ) +
| |
| ( suppress ? 'Suppress username: YES\n' : '' ) +
| |
| '\nThis action will be logged. Proceed?';
| |
| | |
| windowManager.openWindow( 'message', {
| |
| title: 'Confirm global lock',
| |
| message: confirmMessage,
| |
| actions: [
| |
| { action: 'accept', label: 'Lock accounts', flags: [ 'primary', 'destructive' ] },
| |
| { action: 'reject', label: 'Cancel', flags: [ 'safe' ] }
| |
| ]
| |
| } ).closed.then( function ( data ) {
| |
| if ( !data || data.action !== 'accept' ) { return; }
| |
| | |
| $( '#gul-btn-lock, #gul-btn-search' ).prop( 'disabled', true );
| |
| $( '#gul-progress' ).addClass( 'visible' );
| |
| setProgress( 0, selected.length );
| |
|
| |
| setStatus( 'Locking ' + selected.length + ' account(s)…', 'info' );
| |
| let done = 0, locked = 0, skipped = 0, failed = 0;
| |
| | |
| function processNext( i ) {
| |
| if ( i >= selected.length ) {
| |
| setProgress( done, selected.length );
| |
| setStatus( 'Done. <strong>' + locked + '</strong> locked, <strong>' + skipped + '</strong> skipped, <strong>' + failed + '</strong> failed.', failed > 0 ? 'warning' : 'success' );
| |
| $( '#gul-btn-search' ).prop( 'disabled', false );
| |
| $( '#gul-lock-summary' ).remove();
| |
| $( '#gul-results-wrap' ).append( $( '<div id="gul-lock-summary">' ).addClass( 'gul-summary' ).html( '<strong>Summary:</strong> ' + locked + ' locked · ' + skipped + ' skipped · ' + failed + ' failed' ) );
| |
| return;
| |
| }
| |
| | |
| const username = selected[ i ];
| |
| const wasLocked = $( 'tr[data-user="' + $.escapeSelector( username ) + '"] td:nth-child(3)' ).text().trim() === 'Locked';
| |
| | |
| if ( skipLocked && wasLocked ) {
| |
| skipped++; done++;
| |
| setBadge( username, 'skipped', 'Skipped' );
| |
| setProgress( done, selected.length );
| |
| processNext( i + 1 );
| |
| return;
| |
| }
| |
| | |
| setBadge( username, 'pending', 'Locking…' );
| |
| | |
| lockAccount( username, reason, hide, suppress )
| |
| .done( function ( result ) {
| |
| if ( result.error ) {
| |
| failed++;
| |
| setBadge( username, 'failed', 'Error: ' + mw.html.escape( result.error.info || result.error.code ) );
| |
| } else {
| |
| locked++;
| |
| setBadge( username, 'locked', 'Locked ✓' );
| |
| }
| |
| } )
| |
| .fail( function ( code, result ) {
| |
| failed++;
| |
| const errMsg = (result && result.error && result.error.info) ? result.error.info : code;
| |
| setBadge( username, 'failed', 'Error: ' + mw.html.escape( errMsg ) );
| |
| } )
| |
| .always( function () {
| |
| done++;
| |
| setProgress( done, selected.length );
| |
| setStatus( 'Processing ' + done + ' / ' + selected.length + '…', 'info' );
| |
| setTimeout( function () { processNext( i + 1 ); }, 300 );
| |
| } );
| |
| }
| |
| processNext( 0 );
| |
| } );
| |
| }
| |
| | |
| function onClear() {
| |
| $( '#gul-query, #gul-from, #gul-reason' ).val( '' );
| |
| $( '#gul-hide, #gul-suppress' ).prop( 'checked', false );
| |
| $( '#gul-skip-locked' ).prop( 'checked', true );
| |
| $( '#gul-results-wrap' ).removeClass( 'visible' ).empty();
| |
| $( '#gul-progress' ).removeClass( 'visible' );
| |
| $( '#gul-progress-bar' ).css( 'width', '0%' );
| |
| clearStatus();
| |
| $( '#gul-btn-lock' ).prop( 'disabled', true );
| |
| }
| |
| | |
| } ); // End of mw.loader.using().then()
| |
| | |
| }() );
| |