Jump to content

User:Dragoniez/markblocked.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/*
You can import this gadget to other wikis by using mw.loader.load and specifying the local alias for Special:Contributions. For example:

window.markblocked_contributions = 'Special:Contributions';
mw.loader.load('//en.chped.com/w/index.php?title=MediaWiki:Gadget-markblocked.js&bcache=1&maxage=259200&action=raw&ctype=text/javascript');

Note that window.markblocked_contributions is a regex string; hence a value like 'Special:Contrib(?:ution)?s' will also work.
However, do not make any capturing group in the regex string; otherwise the gadget will throw an error.

This gadget will pull the user accounts and IPs from the history page and will strike out the users that are currently blocked.

Configuration variables:
- window.markblocked_contributions - Let wikis that are importing this gadget specify the local alias of Special:Contributions
- window.mbIndefStyle - custom CSS to override default CSS for indefinite blocks
- window.mbNoAutoStart - if set to true, doesn't mark blocked until you click "XX" in the "More" menu
- window.mbPartialStyle - custom CSS to override default CSS for partial blocks
- window.mbTempStyle - custom CSS to override default CSS for short duration blocks
- window.mbTipBox - if set to true, loads a yellow box with a pound sign next to blocked usernames. upon hovering over it, displays a tooltip.
- window.mbTipBoxStyle - custom CSS to override default CSS for the tip box (see above)
- window.mbTooltip - custom pattern to use for tooltips. default is '; blocked ($1) by $2: $3 ($4 ago)'
*/

// @ts-check
/* global mw */
//<nowiki>
( () => {
	function execute() {
		if ( [ 'edit', 'submit' ].indexOf( mw.config.get( 'wgAction' ) ) !== -1 ) {
			return;
		}

		$.when( $.ready, mw.loader.using( [ 'mediawiki.util', 'mediawiki.Title', 'mediawiki.api' ] ) ).then( () => {

			mw.util.addCSS( '\
				.markblocked-loading a.userlink {opacity:' + ( window.mbLoadingOpacity || 0.85 ) + '}\
				a.user-blocked-temp {' + ( window.mbTempStyle || 'opacity: 0.7; text-decoration: line-through' ) + '}\
				a.user-blocked-indef {' + ( window.mbIndefStyle || 'opacity: 0.4; font-style: italic; text-decoration: line-through' ) + '}\
				a.user-blocked-partial {' + ( window.mbPartialStyle || 'text-decoration: underline; text-decoration-style: dotted' ) + '}\
				.user-blocked-tipbox {' + ( window.mbTipBoxStyle || 'font-size:smaller; background:#FFFFF0; border:1px solid #FEA; padding:0 0.3em; color:#AAA' ) + '}\
			' );

			if ( window.mbNoAutoStart ) {
				const portletLink = mw.util.addPortletLink(
					document.getElementById( 'p-cactions' ) ? 'p-cactions' : 'p-personal', // minerva doesn't have p-cactions
					'',
					'XX',
					'ca-showblocks'
				);
				if ( !portletLink ) {
					mw.notify('Failed to create a portlet link for markblocked.', { type: 'error', autoHideSeconds: 'long' } );
					return;
				}
				portletLink.addEventListener( 'click', function( e ) {
					e.preventDefault();
					this.remove(); // Remove the link right away
					hook();
				});
			} else {
				hook();
			}


		} );
	}

	/**
	 * @typedef {object} LinkRegex
	 * @property {RegExp} article `/wiki/(TITLE)`
	 * @property {RegExp} script  `/w/index.php?title=(TITLE)`
	 * @property {RegExp} user For all titles that are  User:| User_talk: | Special:Contributions/ (`$2`: user name)
	 */

	function hook() {
		// Get all aliases for user: & user_talk:
		const userNS = [];
		const wgNamespaceIds = mw.config.get( 'wgNamespaceIds' );
		for ( const ns in wgNamespaceIds ) {
			switch ( wgNamespaceIds[ ns ] ) {
				// case -1:
					// TODO: We should also collect aliases for special:
					// break;
				case 2:
				case 3:
					userNS.push( mw.util.escapeRegExp( ns.replace( /_/g, ' ' ) ) + ':' );
					break;
				default:
			}
		}

		// Let wikis that are importing this gadget specify the local alias of Special:Contributions
		const contribs = typeof window.markblocked_contributions === 'string' ? window.markblocked_contributions : 'Special:Contrib(?:ution)?s' ;

		// RegExp for links
		/**
		 * @type {LinkRegex}
		 */
		const rg = {
			article: new RegExp( mw.config.get( 'wgArticlePath' ).replace( '$1', '' ) + '([^#]+)' ),
			script: new RegExp( '^' + mw.config.get( 'wgScript' ) + '\\?title=([^#&]+)' ),
			user: new RegExp( '^(' + userNS.join( '|' ) + '|' + contribs + '\\/)+([^\\/#]+)$', 'i' )
		};

		let firstTime = true;
		mw.hook( 'wikipage.content' ).add( ( $container ) => {
			// On the first call after initial page load, container is mw.util.$content

			// Limit mainspace activity to just the diff definitions
			if ( mw.config.get( 'wgAction' ) === 'view' && mw.config.get( 'wgNamespaceNumber' ) === 0 ) {
				$container = $container.find( '.diff-title' );
			}

			if ( firstTime ) {
				firstTime = false;

				// On page load, also update the namespace tab
				$container = $container.add( '#ca-nstab-user' );

			}

			markBlocked( $container, rg );
		} );
	}

	/**
	 * @param {JQuery<HTMLElement>} $container
	 * @param {LinkRegex} rg
	 * @returns
	 */
	function markBlocked( $container, rg ) {
		// Collect all user links in the page's content
		/**
		 * @type {Record<string, HTMLAnchorElement[]>}
		 */
		const userLinks = Object.create(null);
		$container.find( 'a' ).not( '.mw-changeslist-date, .ext-discussiontools-init-timestamplink, .mw-history-undo > a, .mw-rollback-link > a' ).each( function() {
			const url = $( this ).attr( 'href' ); // This mustn't be replaced with this.href nor $( this ).prop, which return a URL with the protocol and host
			if ( !url ) {
				return;
			}

			let m, pageTitle;
			if ( ( m = rg.article.exec( url ) ) ) {
				pageTitle = m[ 1 ];
			} else if ( ( m = rg.script.exec( url ) ) ) {
				pageTitle = m[ 1 ];
			} else {
				return;
			}
			pageTitle = decodeURIComponent( pageTitle ).replace( /_/g, ' ' );

			if ( !( m = rg.user.exec( pageTitle ) ) ) {
				return;
			}
			const userTitle = mw.Title.newFromText( m[ 2 ] );
			if ( !userTitle ) {
				return;
			}

			let user = userTitle.getMainText();
			if ( mw.util.isIPv6Address( user ) ) {
				user = user.toUpperCase();
			}

			this.classList.add( 'userlink' );
			if ( !userLinks[ user ] ) {
				userLinks[ user ] = [];
			}
			userLinks[ user ].push( this );
		} );

		// Convert users into array
		const users = Object.keys( userLinks );
		if ( users.length === 0 ) {
			return;
		}

		// API request
		$container.addClass( 'markblocked-loading' );
		const deferreds = [];
		while ( users.length > 0 ) {
			deferreds.push( markLinks( users.splice( 0, 50 ), userLinks ) );
		}
		$.when.apply( $, deferreds ).then( () => {
			$container.removeClass( 'markblocked-loading' );
		} );

	}

	/**
	 * @param {string[]} users
	 * @param {Record<string, HTMLAnchorElement[]>} userLinks
	 * @returns {JQueryProimse<void>}
	 */
	function markLinks ( users, userLinks ) {
		return new mw.Api().post( {
			action: 'query',
			list: 'blocks',
			bkusers: users.join( '|' ),
			bkprop: 'user|by|timestamp|expiry|reason|restrictions', // no need for 'id|flags'
			curtimestamp: true,
			formatversion: '2'
		} ).then( ( res ) => {

			const list = res && res.query && res.query.blocks;
			if ( !list ) {
				return;
			}

			const serverTime = new Date( res.curtimestamp );
			for ( let i = 0; i < list.length; i++ ) {
				const block = list[ i ];
				const partial = block.restrictions && !Array.isArray( block.restrictions ); // Partial block
				let htmlClass, blockTime;
				if ( /^in/.test( block.expiry ) ) {
					htmlClass = partial ? 'user-blocked-partial' : 'user-blocked-indef';
					blockTime = block.expiry;
				} else {
					htmlClass = partial ? 'user-blocked-partial' : 'user-blocked-temp';
					// Apparently you can subtract date objects in JavaScript. Some kind of
					// magic happens and they are automatically converted to milliseconds.
					blockTime = inHours( parseTimestamp( block.expiry ) - parseTimestamp( block.timestamp ) );
				}
				let tooltipString = window.mbTooltip || '; blocked ($1) by $2: $3 ($4 ago)';
				if ( partial ) {
					tooltipString = tooltipString.replace( 'blocked', 'partially blocked' );
				}
				tooltipString = tooltipString.replace( '$1', blockTime )
					.replace( '$2', block.by )
					.replace( '$3', block.reason )
					.replace( '$4', inHours( serverTime - parseTimestamp( block.timestamp ) ) );
				const links = userLinks[ block.user ];
				for ( let k = 0; links && k < links.length; k++ ) {
					const $link = $( links[ k ] ).addClass( htmlClass );
					if ( window.mbTipBox ) {
						$( '<span class=user-blocked-tipbox>#</span>' ).attr( 'title', tooltipString ).insertBefore( $link );
					} else {
						$link.attr( 'title', $link.attr( 'title' ) + tooltipString );
					}
				}
			}

		} ).catch( ( _, err ) => {
			console.error( err );
		} );
	}

	/**
	 * @param {string} timestamp 20081226220605 or 2008-01-26T06:34:19Z
	 * @return {Date}
	 */
	function parseTimestamp( timestamp ) {
		const matches = timestamp.replace( /\D/g, '' ).match( /(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/ );
		return new Date( Date.UTC( matches[ 1 ], matches[ 2 ] - 1, matches[ 3 ], matches[ 4 ], matches[ 5 ], matches[ 6 ] ) );
	}

	/**
	 * @param {number} milliseconds 604800000
	 * @return {string} "2:30" or "5.06d" or "21d"
	 */
	function inHours( milliseconds ) {
		let minutes = Math.floor( milliseconds / 60000 );
		if ( !minutes ) {
			return Math.floor( milliseconds / 1000 ) + 's';
		}
		let hours = Math.floor( minutes / 60 );
		minutes = minutes % 60;
		const days = Math.floor( hours / 24 );
		hours = hours % 24;
		if ( days ) {
			return days + ( days < 10 ? '.' + addLeadingZeroIfNeeded( hours ) : '' ) + 'd';
		}
		return hours + ':' + addLeadingZeroIfNeeded( minutes );
	}

	/**
	 * @param {number} v 9
	 * @return {string} 09
	 */
	function addLeadingZeroIfNeeded( v ) {
		if ( v <= 9 ) {
			v = '0' + v;
		}
		return v;
	}

	execute();
} )();
//</nowiki>