// <nowiki>
// Copied and edited from Novem Linguae's user highlighter simple: ]
class UserRoleIndicator {
/**
* @param {jQuery} $ jquery
* @param {Object} mw mediawiki
* @param {Window} window
*/
constructor( $, mw, window ) {
// eslint-disable-next-line no-jquery/variable-pattern
this.$ = $;
this.mw = mw;
this.window = window;
this.linksChecked = new Set();
}
async execute() {
//console.time("uri");
const defaultRoleInfoLookup = {
wmf: ,
bot: ,
stewards: ,
arbcom: ,
bureaucrats: ,
checkUsers: ,
admins: ,
formerAdmins: ,
newPageReviewers: ,
tenThousandEdits: ,
extendedConfirmed: ,
lessThan500: ,
};
if(this.window.UserRoleIndicatorCustomLabels){
this.roleInfoLookup = { ...defaultRoleInfoLookup, ...window.UserRoleIndicatorCustomLabels };
}else{
this.roleInfoLookup = defaultRoleInfoLookup;
}
this.labelPosition = "after";
if(this.window.UserRoleIndicatorCustomPlacement){
this.labelPosition = this.window.UserRoleIndicatorCustomPlacement;
}
//console.time("get usernames")
await this.getUsernames();
//console.timeEnd("get usernames")
this.addCSS('user-role-indicator', 'font-size: smaller; display: inline; background: #b7b9ff55; padding: 0.1em; border-radius: 5px;')
this.addCSS('label-after', 'margin-left:3px;');
this.addCSS('label-before', 'margin-right:1px;margin-left:2px;');
const $links = this.$( '#article a, #bodyContent a, #mw_contentholder a' );
//console.time("linkloop")
$links.each( ( index, element ) => {
this.$link = this.$( element );
if(this.linksChecked.has(element)){
return;
}
this.linksChecked.add(element);
if ( !this.linksToAUser() ) {
return;
}
this.user = this.getUserName();
const isUserSubpage = this.user.includes( '/' );
if ( isUserSubpage ) {
return;
}
this.hasAdvancedPermissions = false;
this.addRoleInfoIfNeeded();
} );
//console.timeEnd("linkloop")
//console.timeEnd("uri");
//console.log("-------");
}
addCSS( htmlClass, cssDeclaration ) {
// .plainlinks is for Wikipedia Signpost articles
// To support additional custom signature edge cases, add to the selectors here.
this.mw.util.addCSS( `
.plainlinks .${ htmlClass }.external,
.${ htmlClass },
.${ htmlClass } b,
.${ htmlClass } big,
.${ htmlClass } font,
.${ htmlClass } kbd,
.${ htmlClass } small,
.${ htmlClass } span {
${ cssDeclaration }
}
` );
}
async getWikitextFromCache( title ) {
const api = new this.mw.ForeignApi( 'https://en.wikipedia.org/w/api.php' );
let wikitext = '';
await api.get( {
action: 'query',
prop: 'revisions',
titles: title,
rvslots: '*',
rvprop: 'content',
formatversion: '2',
uselang: 'content', // needed for caching
smaxage: '86400', // cache for 1 day
maxage: '86400' // cache for 1 day
} ).then( ( data ) => {
wikitext = data.query.pages.revisions.slots.main.content;
} );
return wikitext;
}
async getUsernames() {
if(this.wmf){
return;
}
const dataString = await this.getWikitextFromCache( 'User:NovemBot/userlist.js' );
const dataJSON = JSON.parse( dataString );
this.wmf = {
...dataJSON.founder,
...dataJSON.boardOfTrustees,
...dataJSON.staff
// WMF is hard-coded a bit further down. The script detects those strings in the username. This is safe to do because the WMF string is blacklisted from names, so has to be specially created.
// ...dataJSON,
// ...dataJSON,
// ...dataJSON,
// ...dataJSON,
// ...dataJSON,
};
this.bot = dataJSON.bot;
this.stewards = dataJSON.steward;
this.arbcom = dataJSON.arbcom;
this.bureaucrats = dataJSON.bureaucrat;
this.admins = dataJSON.sysop;
this.checkUsers = dataJSON.checkuser;
this.formerAdmins = dataJSON.formeradmin;
this.newPageReviewers = dataJSON.patroller;
this.tenThousandEdits = dataJSON;
this.extendedConfirmed = {
...dataJSON.extendedconfirmed,
...dataJSON.productiveIPs
};
}
hasHref( url ) {
return Boolean( url );
}
isAnchor( url ) {
return url.charAt( 0 ) === '#';
}
isHttpOrHttps( url ) {
return url.startsWith( 'http://', 0 ) ||
url.startsWith( 'https://', 0 ) ||
url.startsWith( '/', 0 );
}
/**
* Figure out the wikipedia article title of the link
*
* @param {string} url
* @param {mw.Uri} urlHelper
* @return {string}
*/
getTitle( url, urlHelper ) {
// for links in the format /w/index.php?title=Blah
const titleParameterOfUrl = this.mw.util.getParamValue( 'title', url );
if ( titleParameterOfUrl ) {
return titleParameterOfUrl;
}
// for links in the format https://wikines.com/en/PageName. Slice off the https://wikines.com/en/ (first 6 characters)
if ( urlHelper.path.startsWith( 'https://wikines.com/en/' ) ) {
return decodeURIComponent( urlHelper.path.slice( 6 ) );
}
return '';
}
notInUserOrUserTalkNamespace() {
const namespace = this.titleHelper.getNamespaceId();
const notInSpecialUserOrUserTalkNamespace = this.$.inArray( namespace, ) === -1;
return notInSpecialUserOrUserTalkNamespace;
}
linksToAUser() {
let url = this.$link.attr( 'href' );
if ( !this.hasHref( url ) || this.isAnchor( url ) || !this.isHttpOrHttps( url ) ) {
return false;
}
url = this.addDomainIfMissing( url );
// mw.Uri(url) throws an error if it doesn't like the URL. An example of a URL it doesn't like is https://meta.wikimedia.orghttps://wikines.com/en/Community_Wishlist_Survey_2022/Larger_suggestions#1%, which has a section link to a section titled 1% (one percent).
let urlHelper;
try {
urlHelper = new this.mw.Uri( url );
} catch {
return false;
}
// Skip links that aren't to user pages
const isUserPageLink = url.includes( '/w/index.php?title=User' ) || url.includes( 'https://wikines.com/en/User' );
if ( !isUserPageLink ) {
return false;
}
// Even if it is a link to a userpage, skip URLs that have any parameters except title=User, action=edit, and redlink=. We don't want links to diff pages, section editing pages, etc. to be highlighted.
const urlParameters = urlHelper.query;
delete urlParameters.title;
delete urlParameters.action;
delete urlParameters.redlink;
const hasNonUserpageParametersInUrl = !this.$.isEmptyObject( urlParameters );
if ( hasNonUserpageParametersInUrl ) {
return false;
}
const title = this.getTitle( url, urlHelper );
// Handle edge cases such as https://web.archive.org/web/20231105033559/https://en.wikipedia.orghttps://wikines.com/en/User:SandyGeorgia/SampleIssue, which shows up as isUserPageLink = true but isn't really a user page.
try {
this.titleHelper = new this.mw.Title( title );
} catch {
return false;
}
if ( this.notInUserOrUserTalkNamespace() ) {
return false;
}
const isDiscussionToolsSectionLink = url.includes( '#' );
if ( isDiscussionToolsSectionLink ) {
return false;
}
return true;
}
// Brandon Frohbieter, CC BY-SA 4.0, https://stackoverflow.com/a/4009771/3480193
countInstances( string, word ) {
return string.split( word ).length - 1;
}
/**
* mw.Uri(url) expects a complete URL. If we get something like https://wikines.com/en/User:Test, convert it to https://en.wikipedia.orghttps://wikines.com/en/User:Test. Without this, UserHighlighterSimple doesn't work on metawiki.
*
* @param {string} url
* @return {string} url
*/
addDomainIfMissing( url ) {
if ( url.startsWith( '/' ) ) {
url = window.location.origin + url;
}
return url;
}
/**
* @return {string}
*/
getUserName() {
const user = this.titleHelper.getMain().replace( /_/g, ' ' );
return user;
}
addRoleInfoIfAppropriate( listOfUsernames, label, descriptionForHover ) {
if ( listOfUsernames === 1 ) {
this.addRoleIcon( label, descriptionForHover );
}
}
addRoleIcon( icon, descriptionForHover ) {
const title = this.$link.attr( 'title' );
if ( !title || title.startsWith( 'User:' ) ) {
this.$link.attr( 'title', descriptionForHover );
switch(this.labelPosition){
case "before":
this.$link.prepend($("<div class='user-role-indicator label-before'>"+icon+"</div>"))
break;
default:
// Defaults to "after"
this.$link.append($("<div class='user-role-indicator label-after'>"+icon+"</div>"))
break;
}
}
this.hasAdvancedPermissions = true;
}
addRoleInfoIfNeeded() {
// highlight anybody with "WMF" in their name, case insensitive. this should not generate false positives because "WMF" is on the username blacklist. see https://meta.wikimedia.orghttps://wikines.com/en/Title_blacklist
if ( this.user.match( /^*WMF/i ) ) {
this.addRoleIcon( this.roleInfoLookup.wmf, this.roleInfoLookup.wmf );
}
// TODO: grab the order from an array, so I can keep checkForPermission and addCSS in the same order easily, lowering the risk of the HTML title="" being one thing, and the color being another
this.addRoleInfoIfAppropriate( this.wmf, this.roleInfoLookup.wmf, this.roleInfoLookup.wmf);
this.addRoleInfoIfAppropriate( this.bot, this.roleInfoLookup.bot, this.roleInfoLookup.bot);
this.addRoleInfoIfAppropriate( this.stewards, this.roleInfoLookup.stewards, this.roleInfoLookup.stewards);
this.addRoleInfoIfAppropriate( this.arbcom, this.roleInfoLookup.arbcom, this.roleInfoLookup.arbcom);
this.addRoleInfoIfAppropriate( this.bureaucrats, this.roleInfoLookup.bureaucrats, this.roleInfoLookup.bureaucrats);
this.addRoleInfoIfAppropriate( this.checkUsers, this.roleInfoLookup.checkUsers, this.roleInfoLookup.checkUsers);
this.addRoleInfoIfAppropriate( this.admins, this.roleInfoLookup.admins, this.roleInfoLookup.admins);
this.addRoleInfoIfAppropriate( this.formerAdmins, this.roleInfoLookup.formerAdmins, this.roleInfoLookup.formerAdmins);
this.addRoleInfoIfAppropriate( this.newPageReviewers, this.roleInfoLookup.newPageReviewers, this.roleInfoLookup.newPageReviewers);
this.addRoleInfoIfAppropriate( this.tenThousandEdits, this.roleInfoLookup.tenThousandEdits, this.roleInfoLookup.tenThousandEdits);
this.addRoleInfoIfAppropriate( this.extendedConfirmed, this.roleInfoLookup.extendedConfirmed, this.roleInfoLookup.extendedConfirmed);
// If they have no perms, then they are non-EC, so <500 edits
if ( !this.hasAdvancedPermissions ) {
this.addRoleIcon(this.roleInfoLookup.lessThan500, this.roleInfoLookup.lessThan500);
}
}
}
var userRoleIndicator = new UserRoleIndicator( $, mw, window )
// Fire after wiki content is added to the DOM, such as when first loading a page, or when a gadget such as the XTools gadget loads.
mw.hook( 'wikipage.content' ).add( async () => {
await mw.loader.using( , async () => {
await userRoleIndicator.execute();
} );
} );
// Fire after an edit is successfully saved via JavaScript, such as edits by the Visual Editor and HotCat.
mw.hook( 'postEdit' ).add( async () => {
await mw.loader.using( , async () => {
await userRoleIndicator.execute();
} );
} );
// </nowiki>