Jump to content

User:Dragoniez/markblocked.js

From Wikipedia, the free encyclopedia

This is an old revision of this page, as edited by Dragoniez (talk | contribs) at 02:41, 3 July 2024 (doc). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

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.

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' ] ) ).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();
			}


		} );
	}

	function hook() {
		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 );
		} );
	}

	function markBlocked( $container ) {
		// Collect all the links in the page's content
		const $contentLinks = $container.find( 'a' );

		// 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
		if ( typeof window.markblocked_contributions !== 'string' ) {
			window.markblocked_contributions = 'Special:Contrib(ution)?s';
		}

		const userLinks = {};
		getUserLinks( userLinks, $contentLinks, userNS );

		// Convert users into array
		const users = [];
		for ( const u in userLinks ) {
			users.push( u );
		}
		if ( users.length === 0 ) {
			return;
		}

		// API request
		let apiRequests = 0;
		$container.addClass( 'markblocked-loading' );
		while ( users.length > 0 ) {
			apiRequests++;
			// TODO: refactor to use mw.Api()
			$.post(
				mw.util.wikiScript( 'api' ) + '?format=json&action=query',
				{
					list: 'blocks',
					bklimit: 100,
					bkusers: users.splice( 0, 50 ).join( '|' ),
					bkprop: 'user|by|timestamp|expiry|reason|restrictions'
					// no need for 'id|flags'
				},
				markLinks
			);
		}

		/**
		 * Callback: receive data and mark links
		 *
		 * @todo un-nest this nested function
		 */
		function markLinks( resp, status, xhr ) {
			const serverTime = new Date( xhr.getResponseHeader( 'Date' ) );
			let list, block, tooltipString, links, $link;
			if ( !resp || !( list = resp.query ) || !( list = list.blocks ) ) {
				return;
			}

			for ( let i = 0; i < list.length; i++ ) {
				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 ) );
				}
				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 ) ) );
				links = userLinks[ block.user ];
				for ( let k = 0; links && k < links.length; k++ ) {
					$link = $( links[ k ] );
					$link = $link.addClass( htmlClass );
					if ( window.mbTipBox ) {
						$( '<span class=user-blocked-tipbox>#</span>' ).attr( 'title', tooltipString ).insertBefore( $link );
					} else {
						$link.attr( 'title', $link.attr( 'title' ) + tooltipString );
					}
				}
			}

			if ( --apiRequests === 0 ) { // last response
				$container.removeClass( 'markblocked-loading' );
				$( '#ca-showblocks' ).parent().remove(); // remove added portlet link
			}
		}
	}

	/**
	 * Find all "user" links and save them in userLinks : { 'users': [<link1>, <link2>, ...], 'user2': [<link3>, <link3>, ...], ... }
	 */
	function getUserLinks( userLinks, $contentLinks, userNS ) {
		// RegExp for all titles that are  User:| User_talk: | Special:Contributions/ (for userscripts)
		const userTitleRegex = new RegExp( '^(' + userNS.join( '|' ) + '|' + window.markblocked_contributions + '\\/)+([^\\/#]+)$', 'i' );

		// RegExp for links
		// articleRX also matches external links in order to support the noping template
		const articleRegex = new RegExp( mw.config.get( 'wgArticlePath' ).replace( '$1', '' ) + '([^#]+)' );
		const scriptRegex = new RegExp( '^' + mw.config.get( 'wgScript' ) + '\\?title=([^#&]+)' );

		const ipv6Regex = /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i;

		$contentLinks.each( ( i, link ) => {
			if ( $( link ).is( '.mw-changeslist-date, .ext-discussiontools-init-timestamplink, .mw-history-undo > a, .mw-rollback-link > a' ) ) {
				return;
			}

			const url = $( link ).attr( 'href' );
			if ( !url ) {
				return;
			}

			const articleMatch = articleRegex.exec( url ),
				scriptMatch = scriptRegex.exec( url );
			let pageTitle;
			if ( articleMatch ) {
				pageTitle = articleMatch[ 1 ];
			} else if ( scriptMatch ) {
				pageTitle = scriptMatch[ 1 ];
			} else {
				return;
			}
			pageTitle = decodeURIComponent( pageTitle ).replace( /_/g, ' ' );

			let user = userTitleRegex.exec( pageTitle );
			if ( !user ) {
				return;
			}

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

			user = userTitle.getMainText();
			if ( ipv6Regex.test( user ) ) {
				user = user.toUpperCase();
			}

			$( link ).addClass( 'userlink' );

			if ( !userLinks[ user ] ) {
				userLinks[ user ] = [];
			}
			userLinks[ user ].push( link );
		} );
	}

	/**
	 * @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>