Note: After saving, changes may not occur immediately. Click here to learn how to bypass your browser's cache.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (Cmd-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (Cmd-Shift-R on a Mac)
- Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Clear the cache in Tools → Preferences
For details and instructions about other browsers, see Wikipedia:Bypass your cache.
Code that you insert on this page could contain malicious content capable of compromising your account. If you are unsure whether code you are adding to this page is safe, you can ask at the central discussion page, Scriptorium. The code will be executed when previewing this page under some skins, including Monobook. You can in the interim if you wish to refresh the content sooner under another skin. |
This script seems to have a documentation page at User:Inductiveload/popups reloaded and an accompanying .css page at User:Inductiveload/popups reloaded.css. |
/**
* Re-imagining of the Navigation popups gadgets.
* Principally, a simple way to append hooks for actions and content is
* hoped for so it's easy to "plug in" extra functions.
*
* It also listens for page mutations and adds handlers to links added by
* JS, which can be helpful.
*
* There are three types of "hooks" you can register:
* * recogniser hook: work out the "canonical" form of a link
* * content hook: displays some content relating to a link
* * action hooks: adds to the actions related to a link
*/
// IE11 users have to git gud
/* eslint-disable no-var, compat/compat */
( function ( $, mw, Promise ) {
'use strict';
var Popups = {
cfg: {
recogniserHooks: [],
contentHooks: [],
actionHooks: [],
skinDenylist: [ 'minerva' ],
userContribsByDefault: true,
showTimeout: 500,
imgWidth: 400,
rcTypes: [ 'edit', 'new', 'log' ],
watchlistTypes: [ 'edit', 'new', 'log' ], // can add categorize
// how many log events (RC, watchlist, contribs..) to show
logLimit: {
default: 25
},
// how many links to show
linkLimit: {
default: 25
},
// namespaces to show shortcut links for in contribs actions
nsLinkList: {
default: [ 0, 1, 2, 3, 4, 5, 8, 10, 12, 14, 100, 102, 103, 104, 106, 114, 828 ]
},
timeOffset: 0, // TODO
icons: {
copy: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6b/Ic_content_copy_48px.svg/16px-Ic_content_copy_48px.svg.png',
edit: 'https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Ic_create_48px.svg/16px-Ic_create_48px.svg.png'
},
// Provide these special block types
blocks: [
{
asAction: true,
name: 'spam',
expiry: 'indefinite',
message: 'popups-block-spam-reason',
allowusertalk: true,
watchuser: true
}
],
// provide these special delete types
deletes: [
{
asAction: true,
name: 'spam',
message: 'popups-delete-spam-reason',
watchlist: 'watch'
}
]
},
userRights: [],
// cache of siteinfo
siteinfo: []
};
// Set default messages - users/wikis can override
mw.messages.set( {
'popups-block-confirm': 'Are you sure you want to block $1 with reason "$2"?',
'popups-user-blocked': '$1 blocked successfully',
'popups-user-block-failed': 'Failed to block $1',
'popups-block-spam-reason': 'Spamming/Promoting links to external sites',
'popups-delete-ok': '$1 deleted successfully',
'popups-delete-failed': 'Failed to delete $1',
'popups-delete-confirm': 'Are you sure you want to delete $1 with reason "$2"?',
'popups-delete-spam-reason': 'Spamming/Promoting links to external sites',
'popups-actions-delete': 'del',
'popups-action-mass-delete': 'nuke',
'popups-actions-move': 'move',
'popups-actions-info': 'info',
'popups-diff-template': '{{diff|$1}}'
} );
// Wikis/users can use this hook to set their own messages
mw.hook( 'ext.gadget.popups-reloaded.messages' ).fire();
// returns promise with boolean "confirmed"
function confirmAction( message ) {
return OO.ui.confirm( message )
.then( function ( confirmed ) {
if ( !confirmed ) {
return Promise.reject( confirmed );
}
return confirmed;
} );
}
Popups.wiki = {
server: mw.config.get( 'wgServer' ),
serverName: mw.config.get( 'wgServerName' ),
indexLink: mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' )
};
Popups.commons = {
server: '//commons.wikimedia.org',
serverName: 'commons.wikimedia.org',
indexLink: '//commons.wikimedia.org/w/index.php',
apiLink: '//commons.wikimedia.org/w/api.php'
};
Popups.wikidata = {
server: '//www.wikidata.org',
serverName: 'www.wikidata.org',
indexLink: '//www.wikidata.org/w/index.php'
};
/* these wikis can be used in ForeignApi */
Popups.foreignWikis = [
Popups.commons.serverName,
Popups.wikidata.serverName,
'wikipedia.org',
'wikisource.org',
'wikibooks.org',
'wikispecies.org',
'wikiquote.org',
'wiktionary.org',
'wikivoyage.org',
'wikiversity.org',
'wikinews.org',
'mediawiki.org',
'meta.wikimedia.org',
'foundation.wikimedia.org'
];
function haveRight( right ) {
return Popups.userRights.indexOf( right ) !== -1;
}
function recognisedWikiServer( serverName ) {
// first, check the current wiki
if ( serverName === Popups.wiki.serverName ) {
return true;
}
for ( var i = 0; i < Popups.foreignWikis.length; ++i ) {
if ( serverName.endsWith( Popups.foreignWikis[ i ] ) ) {
return true;
}
}
return false;
}
function getWikiArticleUrl( serverName, title ) {
return '//' + serverName + '/wiki/' + title;
}
function getWikiActionUrl( serverName, title, action, params ) {
var url = '//' + serverName + '/w/index.php?title=' + encodeURIComponent( title );
if ( action ) {
url += '&action=' + action;
}
if ( params ) {
Object.keys( params ).forEach( function ( key ) {
url += '&' + key + '=' + params[ key ];
} );
}
return url;
}
function getArticleUrl( article, encode ) {
var enc = encode ? encodeURIComponent : function ( x ) {
return x;
};
return mw.config.get( 'wgArticlePath' ).replace( '$1', enc( article ) );
}
function getActionUrl( pg, action, params ) {
return getWikiActionUrl( Popups.wiki.serverName, pg, action, params );
}
function getHrefLink( href, text ) {
text = text || href;
return '<a href="' + href + '">' + text + '</a>';
}
function getPageLink( serverName, title, text ) {
text = text || title;
return '<a href="' + getWikiArticleUrl( serverName, title, true ) + '">' + text + '</a>';
}
function getActionLink( serverName, pg, action, text ) {
return getHrefLink( getWikiActionUrl( serverName, pg, action ), text );
}
function getRevidLink( serverName, title, rev, text ) {
var href = getWikiActionUrl( serverName, title, null,
{ oldid: rev.revid } );
return getHrefLink( href, text );
}
function getDiffUrl( serverName, title, ida, idb ) {
return getWikiActionUrl( serverName, title, null,
{ diff: idb, oldid: ida } );
}
function getDiffLink( serverName, title, ida, idb, text ) {
return getHrefLink( getDiffUrl( serverName, title, ida, idb ), text );
}
function getUserLink( serverName, user ) {
user = user.replace( /^[uU]ser:/, '' );
return getPageLink( serverName, 'User:' + user, user );
}
function getCommonsArticleUrl( filename ) {
return Popups.commons.server + '/wiki/' + filename;
}
function getCommonsActionUrl( filename, action ) {
return Popups.commons.indexLink + '?title=' +
filename + '&action=' + action;
}
function getCommonsOrLocalIndexPhp( local ) {
return local ? Popups.wiki.indexLink : Popups.commons.indexLink;
}
function getReuploadUrl( filename, local ) {
return getCommonsOrLocalIndexPhp( local ) + '?title=Special:Upload' +
'&wpDestFile=' + filename + '&wpForReUpload=1';
}
function isNamespaceOrTalk( candidate, wanted ) {
return candidate === wanted || ( candidate === wanted + ' talk' );
}
var getSpecialDiffLink = function ( revid ) {
// eslint-disable-next-line no-useless-concat
return '[' + '[Special:Diff/' + revid.toString() + '|' + revid.toString() + ']]';
};
var getDatetimeFromTimestamp = function ( ts ) {
return ts.replace( 'T', ' ' ).replace( 'Z', '' ); // 2018-12-18T16:59:42Z"
};
function bracketedLinkList( links ) {
var o = '<span>(' + links[ 0 ];
for ( var i = 1; i < links.length; i++ ) {
o += ' | ' + links[ i ];
}
o += ')</span>';
return o;
}
function repeatString( s, mult ) {
var ret = '';
for ( var i = 0; i < mult; ++i ) {
ret += s;
}
return ret;
}
function zeroFill( s, min ) {
min = min || 2;
var t = s.toString();
return repeatString( '0', min - t.length ) + t;
}
function mapArray( f, o ) {
var ret = [];
for ( var i = 0; i < o.length; ++i ) {
ret.push( f( o[ i ] ) );
}
return ret;
}
function mapObject( f, o ) {
var ret = {};
for ( var i in o ) {
ret[ o ] = f( o[ i ] );
}
return ret;
}
function map( f, o ) {
if ( Array.isArray( o ) ) {
return mapArray( f, o );
}
return mapObject( f, o );
}
function getDateFromTimestamp( t ) {
var s = t.split( /[^0-9]/ );
switch ( s.length ) {
case 0: return null;
case 1: return new Date( s[ 0 ] );
case 2: return new Date( s[ 0 ], s[ 1 ] - 1 );
case 3: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ] );
case 4: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ] );
case 5: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ], s[ 4 ] );
case 6: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ], s[ 4 ], s[ 5 ] );
default: return new Date( s[ 0 ], s[ 1 ] - 1, s[ 2 ], s[ 3 ], s[ 4 ], s[ 5 ], s[ 6 ] );
}
}
function adjustDate( d, offset ) {
// offset is in minutes
var o = offset * 60 * 1000;
return new Date( +d + o );
}
function dayFormat( editDate, utc ) {
if ( utc ) {
return map( zeroFill, [ editDate.getUTCFullYear(), editDate.getUTCMonth() + 1, editDate.getUTCDate() ] ).join( '-' );
}
return map( zeroFill, [ editDate.getFullYear(), editDate.getMonth() + 1, editDate.getDate() ] ).join( '-' );
}
function timeFormat( editDate, utc ) {
if ( utc ) {
return map( zeroFill, [ editDate.getUTCHours(), editDate.getUTCMinutes(), editDate.getUTCSeconds() ] ).join( ':' );
}
return map( zeroFill, [ editDate.getHours(), editDate.getMinutes(), editDate.getSeconds() ] ).join( ':' );
}
function makeClassedSpan( cls, text ) {
// eslint-disable-next-line mediawiki/class-doc
return $( '<span>' )
.addClass( cls )
.append( text );
}
function makeColspanTableRow( classes, colspan, content ) {
return '<tr><td colspan=' + colspan + " class='" + classes + "'>" +
content + '<td></tr>';
}
function makeOpenTag( tag, cls ) {
if ( cls ) {
return '<' + tag + " class='" + cls + "'>";
} else {
return '<' + tag + '>';
}
}
function makeEnclosingTag( tag, text, cls ) {
var o = '<' + tag;
if ( cls ) {
o += " class='" + cls + "'";
}
o += '>' + ( text || '' ) + '</' + tag + '>';
return o;
}
function arraysIntersect( a, b ) {
return a.some( function ( v ) {
return b.indexOf( v ) !== -1;
} );
}
function Api( serverName ) {
this.serverName = serverName;
if ( serverName === mw.config.get( 'wgServerName' ) ) {
this.api = new mw.Api();
} else {
this.api = new mw.ForeignApi( '//' + serverName + '/w/api.php' );
}
}
Api.prototype.defaultFormat = function ( params ) {
if ( !params.format ) {
params.format = 'json';
params.formatversion = 2;
}
return params;
};
Api.prototype.get = function ( params, commonsFallback ) {
params = this.defaultFormat( params );
return this.api.get( params )
.then( function ( data ) {
return data;
},
function ( data ) {
if ( commonsFallback && data === 'missingtitle' ) {
return new mw.ForeignApi( Popups.commons.apiLink )
.get( params );
}
throw ( data );
} );
};
Api.prototype.post = function ( params ) {
params = this.defaultFormat( params );
return this.api.post( params );
};
Api.prototype.postWithToken = function ( token, params ) {
params = this.defaultFormat( params );
return this.api.postWithToken( token, params );
};
Api.prototype.getSiteInfo = function ( siProps ) {
const params = {
action: 'query',
meta: 'siteinfo',
siprop: siProps.join( '|' )
};
return this.api.get( params )
.then( ( data ) => data.query );
};
Api.prototype.getSections = function ( pageTitle ) {
const params = {
action: 'parse',
page: pageTitle,
prop: 'sections'
};
return this.api.get( params )
.then( ( data ) => data.parse.sections );
};
Api.prototype.getSectionWithTitle = function ( pageTitle, secTitle ) {
return this.getSections( pageTitle )
.then( ( sections ) => {
for ( const section of sections ) {
if ( section.line === secTitle || section.anchor === secTitle ) {
return section;
}
}
throw `No section ${secTitle} at ${pageTitle}`;
} );
};
Api.prototype.getPageRender = function ( title, section ) {
const params = {
action: 'parse',
page: title,
section: section
};
return this.get( params )
.then( function ( data ) {
return data.parse;
} );
};
Api.prototype.getPageImageUrl = function ( filename, page, size ) {
var params = {
action: 'query',
titles: 'File:' + filename,
prop: 'imageinfo',
iiprop: 'url'
};
if ( size && page ) {
params.iiurlparam = 'page' + page + '-' + size + 'px';
}
return this.get( params );
};
Api.prototype.getPageThumbUrlForTitle = function ( titleNoNs, size ) {
// load page image
var fn = titleNoNs.replace( /\/\d+$/, '' );
var pg = titleNoNs.replace( /^.*\/(\d+)$/, '$1' );
return this.getPageImageUrl( fn, pg, size )
.then( function ( data ) {
return data.query.pages[ 0 ].imageinfo[ 0 ].thumburl;
} );
};
Api.prototype.getPageWikitext = function ( title, oldid ) {
var slot = 'main';
var params = {
action: 'query',
prop: 'revisions',
rvslots: slot,
rvprop: 'content',
titles: title,
rvlimit: 1
};
if ( oldid ) {
params.rvstartid = oldid;
params.rvendid = oldid;
}
return this.get( params )
.then( function ( data ) {
return data.query.pages[ 0 ].revisions[ 0 ].slots[ slot ].content;
} );
};
/* resolves with true or false */
Api.prototype.ifPageExists = function ( title ) {
var params = {
action: 'query',
titles: title
};
return this.get( params )
.then( function ( data ) {
return !data.query.pages[ 0 ].missing;
} );
};
Api.prototype.getOldRevision = function ( oldid ) {
var params = {
action: 'parse',
oldid: oldid,
prop: 'text'
};
return this.get( params )
.then( function ( data ) {
var $content = $( '<div>' )
.addClass( 'popups_page_content popups_oldrev_content' );
var oldcontent = data.parse.text;
$content.append( oldcontent );
return $content;
} );
};
Api.prototype.getPageHistory = function ( title, limit, rvprops ) {
var params = {
action: 'query',
titles: title,
prop: 'revisions',
rvprop: rvprops.join( '|' ),
rvlimit: limit
};
return this.get( params )
.then( function ( data ) {
return data.query.pages[ 0 ];
} );
};
Api.prototype.getCurrentRevs = function ( titles ) {
var params = {
action: 'query',
prop: 'revisions',
titles: Array.isArray( titles ) ? titles.join( '|' ) : titles
};
return this.get( params )
.then( function ( data ) {
return data.query.pages.map( function ( p ) {
return p.revisions[ 0 ];
} );
} );
};
/*
* Get what we need to undo a single edit
*
* Returns a promise that resolves with the 'compare' data.
*/
Api.prototype.getSingleEditCompare = function ( revision, props ) {
var params = {
action: 'compare',
fromrev: revision,
torelative: 'prev',
prop: props.join( '|' )
};
return this.postWithToken( 'csrf', params )
.then( function ( data ) {
return data.compare;
} );
};
Api.prototype.rollbackEdit = function ( toId, toUser ) {
var params = {
action: 'rollback',
pageid: toId,
user: toUser
};
return this.postWithToken( 'rollback', params );
};
Api.prototype.patrolRevision = function ( revid, tags ) {
const params = {
action: 'patrol',
revid: revid
};
if ( tags ) {
params.tags = tags.join( '|' );
}
return this.postWithToken( 'patrol', params );
};
Api.prototype.thankForRevision = function ( revision ) {
var params = {
action: 'thank',
rev: revision
};
return this
.postWithToken( 'csrf', params );
};
Api.prototype.getWhatLeavesHere = function ( title, props, limit ) {
var params = {
action: 'query',
titles: title,
prop: props.join( '|' ),
tllimit: limit,
imlimit: limit,
pllimit: limit
};
return this.get( params )
.then( function ( data ) {
return data.query.pages[ 0 ];
} );
};
Api.prototype.getImageInfo = function ( image, props ) {
var params = {
action: 'parse',
page: image,
prop: props.join( '|' )
};
return this.get( params, true )
.then( function ( data ) {
return data.parse.text;
} );
};
Api.prototype.getRecentChanges = function ( title, options ) {
const params = {
action: 'query',
list: 'recentchanges',
rctitle: title,
rcprop: options.props ? options.props.join( '|' ) : undefined,
rctype: options.types ? options.types.join( '|' ) : undefined,
rcnamespace: options.ns ? options.ns.join( '|' ) : undefined,
rclimit: options.limit
};
return this.get( params )
.then( function ( data ) {
return data.query.recentchanges;
} );
};
Api.prototype.getWatchlistEntries = function ( excudeUsers, wlProps, wlTypes,
wlNamespaces, limit ) {
const params = {
action: 'query',
list: 'watchlist',
wlexcludeuser: excudeUsers,
wlprop: wlProps.join( '|' ),
wltype: wlTypes.join( '|' ),
wllimit: limit,
wlallrev: 1
};
if ( wlNamespaces && wlNamespaces.length ) {
params.wlnamespace = wlNamespaces.join( '|' );
}
return this.get( params )
.then( function ( data ) {
return data.query.watchlist;
} );
};
Api.prototype.getCategoryMembers = function ( category, limit ) {
var params = {
action: 'query',
list: 'categorymembers',
cmtitle: category,
cmlimit: limit
};
return this.get( params )
.then( function ( data ) {
return data.query.categorymembers;
} );
};
Api.prototype.getRandomPages = function ( namespaces, limit ) {
var params = {
action: 'query',
list: 'random',
rnlimit: limit
};
if ( namespaces ) {
params.rnnamespace = namespaces.join( '|' );
}
return this.get( params )
.then( function ( data ) {
return data.query.random;
} );
};
Api.prototype.getPageInfo = function ( title, inprops ) {
var params = {
action: 'query',
titles: title,
prop: 'info',
inprop: inprops.join( '|' )
};
var ok = function ( data ) {
return data.query.pages[ 0 ];
};
var fail = function () {
console.error( 'GET Failed: for pageinfo: ', title );
};
return this.get( params )
.then( ok, fail );
};
Api.prototype.getUserContributions = function ( user, ucprops, namespace, limit ) {
var params = {
action: 'query',
list: 'usercontribs',
ucprop: ucprops.join( '|' ),
ucuser: user,
uclimit: limit,
ucnamespace: namespace
};
return this.get( params )
.then( function ( data ) {
return data.query.usercontribs;
},
function () {
console.error( 'GET Failed: for usercontribs: ', user );
} );
};
Api.prototype.getUserLog = function ( user, leprops, limit ) {
var params = {
action: 'query',
list: 'logevents',
leuser: user,
leprop: leprops.join( '|' ),
lelimit: limit
};
return this.get( params )
.then( function ( data ) {
return data.query.logevents;
} );
};
// Type: all, new, overwrite, revert
Api.prototype.getUserFiles = function ( user, type, limit ) {
const params = {
action: 'query',
list: 'logevents',
leuser: user,
lelimit: limit
};
switch ( type ) {
case 'all':
params.letype = 'upload';
break;
case 'new':
params.leaction = 'upload/upload';
break;
case 'overwrite':
params.leaction = 'upload/overwrite';
break;
case 'revert':
params.leaction = 'upload/revert';
break;
}
return this.get( params )
.then( function ( data ) {
return data.query.logevents;
} );
};
Api.prototype.getUserAbuseFilterLog = function ( user, aflprops, limit ) {
var params = {
action: 'query',
list: 'abuselog',
afluser: user,
aflprop: aflprops.join( '|' ),
afllimit: limit
};
return this.get( params )
.then( function ( data ) {
return data.query.abuselog;
} );
};
Api.prototype.getAbuseFilterLogEntry = function ( aflentry ) {
const params = {
action: 'query',
list: 'abuselog',
afllogid: aflentry,
aflprop: 'ids|details|filter'
};
return this.get( params )
.then( ( data ) => {
return data.query.abuselog[ 0 ];
} );
};
Api.prototype.getWhatLinksHere = function ( title, types, limit ) {
var params = {
action: 'query',
list: types.join( '|' ),
bltitle: title,
bllimit: limit,
eititle: title,
eilimit: limit
};
return this.get( params )
.then( function ( data ) {
return data.query;
} );
};
Api.prototype.doPurgePage = function ( page ) {
var params = {
action: 'purge',
titles: page,
forcerecursivelinkupdate: 1,
redirects: 1
};
return this.post( params );
};
/* Returns a promise that resolves with image info */
Api.prototype.getFileImageinfo = function ( filename, iiprops ) {
var params = {
action: 'query',
formatversion: 2,
titles: 'File:' + filename,
prop: 'imageinfo'
};
if ( iiprops && iiprops.length ) {
params.iiprop = iiprops.join( '|' );
}
return this.get( params )
.then( function ( data ) {
return data.query.pages[ 0 ];
} );
};
Api.prototype.getFileIsLocal = function ( filename ) {
return this
.getFileImageinfo( filename, null )
.then( function ( imageinfo ) {
return imageinfo.imagerepository !== 'shared';
} );
};
Api.prototype.watchPage = function ( title, watch ) {
var params = {
action: 'watch',
titles: title
};
if ( !watch ) {
params.unwatch = 1;
}
return this.postWithToken( 'watch', params );
};
Api.prototype.blockUser = function ( user, expiry, options ) {
var params = {
action: 'block',
expiry: expiry,
user: user,
reason: options.reason,
watchuser: options.watchuser,
allowusertalk: options.allowusertalk
};
return this.postWithToken( 'csrf', params );
};
Api.prototype.delete = function ( page, reason, watchPage ) {
const params = {
action: 'delete',
title: page,
reason: reason,
watchlist: watchPage
};
return this.postWithToken( 'csrf', params );
};
/*
* Generic cache object, contains more specific cache objects
*
* The main ones are:
* * link: data relating to a raw HTML link
* * wiki: data for a link to a wiki
*
* Scoring functions can add their own data for use in the hook later on.
*/
function Cache() {
}
function oddEvenText( i ) {
return ( i % 2 ) ? 'odd' : 'even';
}
function makeTableWithCols( tableCls, colCls ) {
/* eslint-disable mediawiki/class-doc */
var $cg = $( '<colgroup>' );
var $tbody = $( '<tbody>' ).append( $cg );
var $table = $( '<table>' ).addClass( tableCls )
.append( $cg, $tbody );
for ( var i = 0; i < colCls.length; i++ ) {
var $col = $( '<col>' );
if ( colCls[ i ] ) {
$col.addClass( colCls[ i ] );
}
$cg.append( $col );
}
return $table;
/* eslint-enable mediawiki/class-doc */
}
function makeDailyEventTableRows( items, cols, timestampFxn, rowFxn ) {
var day = null;
var timeOffset = 0;
var trs = '';
for ( var i = 0; i < items.length; i++ ) {
var timestamp = timestampFxn( items[ i ] );
var editDate = adjustDate( getDateFromTimestamp( timestamp ), timeOffset );
var thisDay = dayFormat( editDate );
var thisTime = timeFormat( editDate );
if ( thisDay === day ) {
thisDay = '';
} else {
day = thisDay;
}
// day header
if ( thisDay ) {
trs += makeColspanTableRow( 'popups_date', cols, thisDay );
}
var cells = rowFxn( items[ i ], thisTime );
trs += "<tr class='popups_table_row_" + oddEvenText( i ) + "'>";
for ( var d = 0; d < cells.length; d++ ) {
trs += '<td>' + cells[ d ] + '</td>';
}
trs += '</tr>';
}
return trs;
}
/*
* Copy to clipboard
*/
function copyToClipboard( value ) {
var tempInput = document.createElement( 'input' );
tempInput.style = 'position: absolute; left: -1000px; top: -1000px';
tempInput.value = value;
document.body.appendChild( tempInput );
tempInput.select();
document.execCommand( 'copy' );
document.body.removeChild( tempInput );
}
function makeCopyIcon( toCopy ) {
var $icon = $( '<img>' )
.attr( 'src', Popups.cfg.icons.copy )
.addClass( 'popups_icon popups_icon_inline popups_icon_copy' )
.on( 'click', function () {
copyToClipboard( toCopy );
} );
return $icon;
}
function makeEditIcon( url ) {
const $link = $( '<a>' )
.attr( 'href', url );
$( '<img>' )
.attr( 'src', Popups.cfg.icons.edit )
.addClass( 'popups_icon popups_icon_inline popups_icon_edit' )
.appendTo( $link );
return $link;
}
function constructEditsTable( serverName, revs, title, isContribs ) {
var $table = makeTableWithCols( 'popups_rev_table',
[ 'rev-links', 'rev-time', 'rev-user', 'rev-comment' ] );
var $tb = $table.find( 'tbody' );
// var timeOffset = 0;
var timestampFxn = function ( rev ) {
return rev.timestamp;
};
var cellsFxn = function ( rev, time ) {
var revTitle = title || rev.title;
var cells = [];
if ( isContribs ) {
var diffLink = getDiffLink( serverName, revTitle, rev.parentid, rev.revid, 'diff' );
var histLink = getActionLink( serverName, revTitle, 'history', 'hist' );
cells.push( bracketedLinkList( [ histLink, diffLink ] ) );
} else {
var curLink = getDiffLink( serverName, revTitle, rev.revid, revs[ 0 ].revid, 'cur' );
var prevLink = getDiffLink( serverName, revTitle, rev.parentid, rev.revid, 'prev' );
cells.push( bracketedLinkList( [ curLink, prevLink ] ) );
}
cells.push( getRevidLink( serverName, revTitle, rev, time ) );
if ( isContribs ) {
cells.push( getPageLink( serverName, revTitle ) );
} else {
cells.push( getUserLink( serverName, rev.user ) );
}
var minor = rev.minor ? '<span class="popups_edit_flags">m </span>' : '';
cells.push( minor + rev.parsedcomment );
return cells;
};
$tb.append( makeDailyEventTableRows( revs, 4, timestampFxn, cellsFxn ) );
return $table;
}
/**
* Table for uploads - can't quite emulate Special:ListFiles, but good enough
*
* @param {string} serverName
* @param {Array} uploadLogEvents
* @return {jQuery}
*/
function constructUploadsTable( serverName, uploadLogEvents ) {
const $table = makeTableWithCols( 'popups_uploads_table',
[ 'upload-file', 'upload-action', 'upload-time' ] );
const timestampFxn = function ( logEvent ) {
return logEvent.timestamp;
};
const cellsFxn = function ( logEvent, time ) {
const cells = [];
cells.push( getPageLink( serverName, logEvent.title ) );
cells.push( logEvent.action );
cells.push( time );
return cells;
};
$table
.find( 'tbody' )
.append( makeDailyEventTableRows( uploadLogEvents, 2, timestampFxn, cellsFxn ) );
return $table;
}
function constructDiffView( compare ) {
var $div = $( '<div>' ).addClass( 'popups_diff_view' );
var $table = makeTableWithCols( 'popups_diff_table',
[ 'diff-marker', 'diff-content', 'diff-marker', 'diff-content' ] );
var $tbody = $table.find( 'tbody' );
$div.append( $( '<div>' ).addClass( 'popups_diff_comment' )
.append( compare.toparsedcomment ) );
var sizediff = compare.tosize - compare.fromsize;
if ( sizediff > 0 ) {
sizediff = '+' + sizediff;
}
if ( compare.fromsize > 0 ) {
sizediff += ' (' + ( 100 * ( compare.tosize - compare.fromsize ) / compare.tosize ).toFixed( 0 ) + '%)';
}
$div.append( $( '<div>' ).addClass( 'popups_diff_size' )
.append( compare.fromsize + ' → ' + compare.tosize + ': ' + sizediff )
);
$( compare.body )
.appendTo( $tbody );
return $div.append( $table );
}
function genericTimestampFunction( item ) {
return item.timestamp;
}
function constructLogEventView( serverName, events ) {
var $table = makeTableWithCols( 'popups_log_event_table',
[ 'le_type', 'le_title', 'le_timestamp', 'le_comment' ] );
var $tb = $table.children( 'tbody' );
// var timeOffset = 0;
var cellsFxn = function ( evt, time ) {
var cells = [
evt.type,
getPageLink( serverName, evt.title ),
time,
evt.parsedcomment
];
return cells;
};
$tb.append( makeDailyEventTableRows( events, 4, genericTimestampFunction, cellsFxn ) );
return $table;
}
function constructAfLogView( serverName, log ) {
var $table = makeTableWithCols( 'popups_log_event_table',
[ 'le_type', 'le_title', 'le_timestamp', 'le_comment' ] );
var $tb = $table.children( 'tbody' );
function logTimestamp( logItem ) {
return logItem.timestamp;
}
function logCells( logItem, time ) {
var actionRes = logItem.action +
( logItem.result ? ( ' (' + logItem.result + ')' ) : '' );
var cells = [ getPageLink( serverName, 'Special:AbuseLog/' + logItem.id,
actionRes ) ];
if ( logItem.revid ) {
cells.push( getDiffLink( serverName, logItem.title, logItem.revid, 'prev', time ) );
} else {
cells.push( time );
}
cells.push( getPageLink( serverName, logItem.title ) );
cells.push( getPageLink( serverName, 'Special:AbuseFilter/' + logItem.filter_id,
logItem.filter + '(' + logItem.filter_id + ')' ) );
return cells;
}
$tb.append( makeDailyEventTableRows( log, 4, logTimestamp, logCells ) );
return $table;
}
function addToDl( $dl, dt, dd ) {
$( '<dt>' ).append( dt ).appendTo( $dl );
$( '<dd>' ).append( dd ).appendTo( $dl );
}
function constructAfLogEntry( serverName, entry ) {
const $elem = $( '<div>' );
const $dl = $( '<dl>' ).appendTo( $elem );
const filterLink = getPageLink( serverName,
'Special:AbuseFilter/' + entry.filter_id,
entry.filter );
addToDl( $dl, 'Filter', filterLink );
addToDl( $dl, 'User name', getUserLink( serverName, entry.details.user_name ) );
addToDl( $dl, 'User age', entry.details.user_age );
addToDl( $dl, 'User edit count', entry.details.user_editcount );
addToDl( $dl, 'Page age', entry.details.page_age );
addToDl( $dl, 'Added lines', entry.details.added_lines );
return $elem;
}
/*
* Recent changes and watchlists
*/
function constructRcLogView( serverName, log ) {
var $table = makeTableWithCols( 'popups_rc_table',
[ 'le_type', 'le_title', 'le_timestamp', 'le_comment' ] );
var $tb = $table.children( 'tbody' );
var cellsFxn = function ( logItem, time ) {
var patrolled = '';
if ( logItem.unpatrolled ) {
patrolled = makeEnclosingTag( 'span', '!', 'popups_unpatrolled' );
}
if ( logItem.new ) {
patrolled = makeEnclosingTag( 'span', 'N', 'popups_edit_flags' );
}
if ( logItem.minor ) {
patrolled = makeEnclosingTag( 'span', 'm', 'popups_edit_flags' );
}
if ( logItem.bot ) {
patrolled = makeEnclosingTag( 'span', 'b', 'popups_edit_flags' );
}
var cells = [
logItem.type + '<br/>' + getUserLink( serverName, logItem.user ),
getPageLink( serverName, logItem.title ),
getDiffLink( serverName, logItem.title, logItem.revid, 'prev', time ),
patrolled + ' ' + logItem.parsedcomment
];
return cells;
};
$tb.append( makeDailyEventTableRows( log, 4, genericTimestampFunction, cellsFxn ) );
return $table;
}
function constructCategoryListView( serverName, cmems, listClass ) {
var lis = makeOpenTag( 'ul', listClass );
for ( var i = 0; i < cmems.length; i++ ) {
lis += '<li>' + getPageLink( serverName, cmems[ i ].title ) + '</li>';
}
lis += '</ul>';
return lis;
}
function constructBacklinksView( serverName, backlinks ) {
var content = makeOpenTag( 'div', 'popups_backlinks' );
if ( backlinks.length > 0 ) {
content += '<ul>';
for ( var i = 0; i < backlinks.length; i++ ) {
content += '<li>' + getPageLink( serverName, backlinks[ i ].title ) + '</li>';
}
content += '</ul>';
} else {
content += makeEnclosingTag( 'span', 'No pages link to this page.' );
}
content += '</div>';
return content;
}
function constructWikitextView( wikitext ) {
var content = makeOpenTag( 'div', 'popups_wikitext' );
if ( wikitext.length > 0 ) {
content += makeEnclosingTag( 'pre', wikitext );
}
content += '</div>';
return content;
}
function constructPageInfo( data ) {
var content = makeOpenTag( 'div', 'popups_pageinfo popups_info_table_horz' );
var $table = $( '<table>' );
var makeRow = function ( h, d ) {
if ( !d ) {
return null;
}
return $( '<tr>' )
.append( $( '<th>' )
.attr( 'scope', 'row' )
.append( h )
)
.append( $( '<td>' )
.append( d )
);
};
var protection = function ( parray ) {
if ( !parray || parray.length === 0 ) {
return 'none';
}
var strs = parray.map( function ( p ) {
return p.type + ': ' + p.level + ' (' + p.expiry + ')';
} );
return strs.join( '<br>' );
};
var restrictions = function ( rarray ) {
if ( !rarray || rarray.length === 0 ) {
return 'none';
}
return rarray.join( ', ' );
};
// console.log( data );
if ( data.missing ) {
$table.append( makeRow( 'Missing', 'yes' ) );
}
$table
.append( makeRow( 'Page ID', data.pageid ) )
.append( makeRow( 'Last rev', data.lastrevid ) )
.append( makeRow( 'Content model', data.contentmodel ) )
.append( makeRow( 'Length', data.length ) )
.append( makeRow( 'Protection', protection( data.protection ) ) )
.append( makeRow( 'Restrictions', restrictions( data.restrictiontypes ) ) )
.append( makeRow( 'Watchers', data.watchers ) );
if ( data.visitingwatchers ) {
$table.append( makeRow( 'Visiting watchers:', data.visitingwatchers ) );
}
content += $table.get( 0 ).outerHTML;
content += '</div>';
return Promise.resolve( content );
}
/*
* Basic "pass-though" recogniser that just copies the address
*/
Popups.cfg.recogniserHooks.push( {
score: function ( /* $l, cache */ ) {
// this is the very least we can do
return 0;
},
canonical: function ( $l /* , cache */ ) {
return Promise.resolve( {
type: 'basic',
href: $l.attr( 'href' ),
display: $l.text(),
canonical: $l.text()
} );
}
} );
function WikiCache() {
this.title = undefined;
}
WikiCache.prototype.isBasePage = function () {
return this.title === this.baseName;
};
function cacheUpdateWikiProps( $l, cache ) {
if ( cache.wiki ) {
return;
}
cache.wiki = new WikiCache();
// normalise hrefs
cache.link.href = cache.link.href.replace( /_/g, ' ' );
if ( cache.link.url.pathname.startsWith( '/wiki/' ) ) {
cache.wiki.title = decodeURIComponent( cache.link.url.pathname )
.replace( /^\/wiki\//, '' )
.replace( /_/g, ' ' );
} else if ( cache.link.getParam( 'title' ) ) {
cache.wiki.title = cache.link.getParam( 'title' );
}
cache.wiki.action = cache.link.getParam( 'action' ) || 'view';
if ( cache.wiki.title ) {
cache.wiki.baseName = cache.wiki.title.replace( /\/.*$/, '' );
cache.wiki.namespace = cache.wiki.baseName.replace( /:.*$/, '' );
cache.wiki.titleNoNs = cache.wiki.title.replace( cache.wiki.namespace + ':', '' );
}
const titleParts = cache.wiki.title.split( '/' );
if ( cache.wiki.baseName === 'Special:Contributions' ) {
cache.wiki.user = titleParts[ 1 ];
cache.wiki.isContribs = true;
} else if ( cache.wiki.baseName === 'Special:Log' &&
cache.link.getParam( 'user' ) ) {
cache.wiki.user = cache.link.getParam( 'user' );
} else if ( cache.wiki.baseName === 'Special:Block' ) {
cache.wiki.user = titleParts[ 1 ];
} else if ( isNamespaceOrTalk( cache.wiki.namespace, 'User' ) ) {
cache.wiki.user = cache.wiki.baseName.substring( cache.wiki.namespace.length + 1 );
}
if ( cache.link.url.hash ) {
cache.wiki.section = cache.link.url.hash.substring( 1 );
}
// eslint-disable-next-line no-jquery/no-class-state
cache.wiki.redlink = $l.hasClass( 'new' );
}
function cacheSiteInfo( api ) {
const serverName = api.serverName;
let siProm;
if ( !Popups.siteinfo[ serverName ] ) {
siProm = api
.getSiteInfo( [ 'namespaces' ] )
.then( ( si ) => {
Popups.siteinfo[ serverName ] = si;
return Popups.siteinfo[ serverName ];
} );
} else {
siProm = Promise.resolve( Popups.siteinfo[ serverName ] );
}
return siProm;
}
function canonicaliseNs( serverName, localNs ) {
const nses = Popups.siteinfo[ serverName ].namespaces;
for ( const ns in nses ) {
if ( nses[ ns ][ '*' ] === localNs ) {
return nses[ ns ].canonical;
}
}
return localNs;
}
/** On-wiki links */
Popups.cfg.recogniserHooks.push( {
score: function ( $l, cache ) {
// no match - not a URL or not this server or a known foreign API
if ( !cache.link.url ||
!recognisedWikiServer( cache.link.url.hostname )
) {
return;
}
cacheUpdateWikiProps( $l, cache );
return 100;
},
canonical: function ( $l, cache ) {
cacheUpdateWikiProps( $l, cache );
var display = cache.wiki.title;
var href = cache.link.url.href;
if ( cache.wiki.user ) {
display = 'User:' + cache.wiki.user;
href = getArticleUrl( display );
}
if ( cache.wiki.section ) {
display += '#' + cache.wiki.section;
}
var oldid = cache.link.getParam( 'oldid' );
if ( oldid ) {
display += ' @' + oldid;
}
cache.wiki.local = cache.link.url.hostname === mw.config.get( 'wgServerName' );
cache.wiki.serverName = cache.link.url.hostname;
cache.wiki.api = new Api( cache.wiki.serverName );
const isHash = href.endsWith( '/#' ) && cache.link.url.pathname === '/';
const recognised = {
type: isHash ? 'wiki_hash' : 'wiki_local',
href: href,
display: display,
canonical: cache.wiki.title
};
// eslint-disable-next-line no-jquery/no-class-state
if ( $l.hasClass( 'popups_wikitext' ) ) {
recognised.type = 'wikitext';
}
return cacheSiteInfo( cache.wiki.api )
.then( () => {
// canonicalise namespace
cache.wiki.namespace = canonicaliseNs( cache.wiki.serverName,
cache.wiki.namespace );
if ( cache.wiki.namespace !== 'Special' ) {
recognised.editUrl = getWikiActionUrl( cache.wiki.serverName,
cache.wiki.title, 'edit' );
}
} )
.then( () => recognised );
}
} );
function constructRawImageContent( src ) {
var $content = $( '<div>' )
.addClass( 'popups_rawimage' );
$( '<img>' )
.attr( 'src', src )
.appendTo( $content );
return $content;
}
function constructPageImageContent( serverName, titleNoNs ) {
var size = 350;
return new Api( serverName ).getPageThumbUrlForTitle( titleNoNs, size )
.then( function ( imgUrl ) {
return constructRawImageContent( imgUrl );
} );
}
function constructMissingPageError() {
return $( '<div>' )
.addClass( 'popups_missingpage' )
.append( 'Page does not exist' );
}
function constructGenericError( e ) {
return $( '<div>' )
.addClass( 'popups_error' )
.append( e );
}
/**
* @param {string} serverName
* @param {string} title
* @param {string} [section]
* @return {Promise} resolves with the content as jQuery
*/
function constructPageRender( serverName, title, section ) {
const api = new Api( serverName );
let renderProm;
if ( section ) {
renderProm = api.getSectionWithTitle( title, section )
.then( function ( matchingSection ) {
return api.getPageRender( title, matchingSection.index );
} );
} else {
renderProm = api.getPageRender( title );
}
return renderProm
.then( function ( parse ) {
var $content = $( '<div>' )
.addClass( 'popups_page_content' );
const $parsed = $( parse.text );
$content.append( $parsed );
return $content;
},
function ( data ) {
if ( data !== 'missingtitle' ) {
return constructGenericError( `GET Failed: for ${title}: ` + data );
}
return constructMissingPageError();
} );
}
function escapeHtml( html ) {
return html
.replace( /&/g, '&' )
.replace( /</g, '<' )
.replace( />/g, '>' )
.replace( /"/g, '"' )
.replace( /'/g, ''' );
}
/**
* Returns the page's Wikitext, as escaped HTML
*
* @param {string} serverName
* @param {string} title
* @param {number} oldid
* @return {Promise} the page wikitext as jQuery
*/
function constructPageWikitext( serverName, title, oldid ) {
return new Api( serverName )
.getPageWikitext( title, oldid )
.then( function ( wikitext ) {
return constructWikitextView( escapeHtml( wikitext ) );
} );
}
/**
* Basic non-wiki content
*/
Popups.cfg.contentHooks.push( {
name: 'basic non-wiki content',
onlyType: 'basic',
score: function () {
return 0;
},
content: function ( $l, cache ) {
// Note: for many (most?) external URLs, this will fail due to CORS
return fetch( cache.link.href )
.then( function ( data ) {
const imageTypes = [ 'image/jpeg', 'image/png' ];
// many images will load OK
if ( imageTypes.indexOf( data.headers.get( 'content-type' ) ) !== -1 ) {
return $( '<img>' )
.attr( 'src', data.url );
}
throw new Error( 'Unsupported external site data' );
} );
}
} );
/**
* Mediawiki images
*/
Popups.cfg.contentHooks.push( {
name: 'wikimedia image',
onlyType: 'basic',
score: function ( $l, cache ) {
if ( cache.link.url.hostname === 'upload.wikimedia.org' ) {
return 100;
}
},
content: function ( $l, cache ) {
const changeWikimediaThumbRes = function ( url, newRes ) {
return url.replace( /(?<=\/(?:page\d+-)?)\d+(?=px-)/, newRes );
};
// downres to avoid massive images
const url = changeWikimediaThumbRes( cache.link.url.href, Popups.cfg.imgWidth );
return $( '<img>' )
.addClass( 'popups_wikimedia_thumb' )
.attr( 'src', url );
}
} );
Popups.cfg.contentHooks.push( {
name: 'hathitrust',
onlyType: 'basic',
score: function ( $l, cache ) {
if ( cache.link.url.hostname === 'babel.hathitrust.org' ) {
cache.hathi = {
id: cache.link.getParam( 'id' ),
seq: cache.link.getParam( 'seq' )
};
return 100;
}
},
content: function ( $l, cache ) {
if ( cache.hathi && cache.hathi.id ) {
return $( '<code>' )
.append( cache.hathi.id );
}
}
} );
/**
* Basic wiki content
*/
Popups.cfg.contentHooks.push( {
name: 'basic wiki content',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
// special namespace pages don't have anything useful
if ( cache.wiki.namespace !== 'Special' ) {
// basic content is not very interesting, but it's the best we can
// do for many pages
return 0;
}
},
content: function ( $l, cache ) {
return constructPageRender( cache.wiki.serverName,
cache.wiki.title, cache.wiki.section );
}
} );
/*
* Wikitext
*/
Popups.cfg.contentHooks.push( {
name: 'wikitext content',
onlyType: 'wikitext',
score: function ( $l, cache ) {
// special namespace pages don't have anything useful
if ( cache.wiki.namespace !== 'Special' ) {
// matched wikitext exactly
return 2000;
}
},
content: function ( $l, cache ) {
var oldid = cache.link.getParam( 'oldid' );
return constructPageWikitext( cache.wiki.serverName, cache.wiki.title, oldid );
}
} );
Popups.cfg.contentHooks.push( {
name: 'page NS content',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.namespace === 'Page' &&
( cache.wiki.action === 'view' || cache.wiki.redlink ) ) {
return 1000;
}
},
content: function ( $l, cache ) {
return new Api( cache.wiki.serverName )
.ifPageExists( cache.wiki.title )
.then( function ( exists ) {
if ( !exists ) {
// load page image
return constructPageImageContent( cache.wiki.serverName,
cache.wiki.titleNoNs );
} else {
return constructPageRender( cache.wiki.serverName, cache.wiki.title );
}
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'old revision',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.link && cache.link.getParam( 'oldid' ) && !cache.link.getParam( 'diff' ) ) {
cache.oldRev = {
id: cache.link.getParam( 'oldid' )
};
return 2000;
}
},
content: function ( $l, cache ) {
return new Api( cache.wiki.serverName )
.getOldRevision( cache.oldRev.id )
.then( ( content ) => {
const $ret = $( '<div>' );
const diffTemplate = mw.message( 'popups-diff-template', cache.oldRev.id ).plain();
$( '<div>' )
.append( $( '<code>' )
.append( diffTemplate )
)
.append( makeCopyIcon( diffTemplate ) )
.appendTo( $ret );
$( '<div>' )
.append( content )
.appendTo( $ret );
return $ret;
},
function ( data ) {
console.error( 'GET Failed: for ', cache.wiki.title, data );
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'page history',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.action === 'history' ) {
// outscore the "Plain" content hook for that page (1000)
return 2000;
}
},
content: function ( $l, cache ) {
var ok = function ( page ) {
var revs = page.revisions;
var $table;
if ( revs ) {
$table = constructEditsTable( cache.wiki.serverName,
revs, page.title, false );
}
return $table;
};
var fail = function () {
console.error( 'GET Failed: for ', cache.wiki.title );
};
return new Api( cache.wiki.serverName )
.getPageHistory( cache.wiki.title,
Popups.cfg.logLimit.history || Popups.cfg.logLimit.default,
[ 'tags', 'timestamp', 'user', 'parsedcomment', 'flags', 'ids', 'size' ]
)
.then( ok, fail );
}
} );
/* extended page information */
Popups.cfg.contentHooks.push( {
name: 'page info',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.action === 'info' ) {
// outscore the "Plain" content hook for that page (1000)
return 2000;
}
},
content: function ( $l, cache ) {
return new Api( cache.wiki.serverName )
.getPageInfo( cache.wiki.title,
[ 'watchers', 'watched', 'visitingwatchers', 'varianttitles', 'url',
'talkid', 'subjectid', 'preload', 'protection', 'notificationtimestamp',
'linkclasses', 'displaytitle' ]
)
.then( function ( pageInfo ) {
return constructPageInfo( pageInfo );
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'user contributions',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.isContribs ) {
return 100;
}
if ( Popups.cfg.userContribsByDefault &&
( cache.wiki.namespace === 'User' && cache.wiki.isBasePage() ) ) {
return 100;
}
},
content: function ( $l, cache ) {
var user;
if ( cache.wiki.isContribs ) {
user = cache.wiki.title.split( '/' )[ 1 ];
} else {
user = cache.wiki.titleNoNs; // == wiki.baseName
}
var namespace;
if ( cache.link ) {
namespace = cache.link.getParam( 'namespace' );
}
return new Api( cache.wiki.serverName )
.getUserContributions( user,
[ 'ids', 'title', 'timestamp', 'parsedcomment', 'size', 'flags' ], namespace,
Popups.cfg.logLimit.contribs || Popups.cfg.logLimit.default
)
.then( function ( contribRevs ) {
var $table = constructEditsTable(
cache.wiki.serverName, contribRevs, null, true );
return $table;
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'user files',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName.startsWith( 'Special:ListFiles' ) ||
cache.wiki.baseName.startsWith( 'Special:AllMyUploads' )
) {
return 100;
}
},
content: function ( $l, cache ) {
if ( !cache.link ) {
// TODO: throw something standard here
return;
}
let user = cache.link.getParam( 'user' );
if ( !user ) {
user = cache.wiki.titleNoNs.split( '/' )[ 1 ];
}
if ( !user ) {
return;
}
const type = 'all';
return new Api( cache.wiki.serverName )
.getUserFiles( user, type,
Popups.cfg.logLimit.contribs || Popups.cfg.logLimit.default
)
.then( function ( userFiles ) {
var $table = constructUploadsTable( cache.wiki.serverName, userFiles );
return $table;
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'diff view',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.link.hasAllParams( [ 'oldid', 'diff' ] ) ||
cache.link.hasAllParams( [ 'undo', 'undoafter' ] ) ) {
// this should out-score the "plain" content for that page
// which is usually 1000)
return 2000;
}
if ( cache.wiki.baseName.startsWith( 'Special:Diff' ) ) {
cache.diff.diff = cache.wiki.title.replace( /^.*\//, '' );
return 2000;
}
},
content: function ( $l, cache ) {
var params = {
action: 'compare',
format: 'json',
formatversion: 2,
prop: [ 'diff', 'ids', 'title', 'parsedcomment', 'size' ].join( '|' )
};
if ( cache.link.getParam( 'undo' ) ) {
params.fromrev = cache.link.getParam( 'undo' );
params.torev = cache.link.getParam( 'undoafter' );
} else {
var oldid = cache.link.getParam( 'oldid' );
var diff = cache.link.getParam( 'diff' );
// taken from navigation popups
switch ( diff ) {
case 'cur':
switch ( oldid ) {
case null:
case '':
case 'prev':
// this can only work if we have the title
// cur -> prev
params.fromtitle = cache.wiki.title;
// params.fromrev = 'cur';
params.torelative = 'prev';
break;
default:
params.fromrev = oldid;
params.torelative = 'cur';
break;
}
break;
case 'prev':
if ( oldid ) {
params.fromrev = oldid;
} else {
// params.fromtitle;
}
params.torelative = 'prev';
break;
case 'next':
params.fromrev = oldid || 0;
params.torelative = 'next';
break;
default:
params.fromrev = oldid || 0;
params.torev = diff || 0;
break;
}
}
return new Api( cache.wiki.serverName )
.get( params )
.then( function ( data ) {
var $table = constructDiffView( data.compare );
return $table;
},
function ( data ) {
console.error( `GET Failed: for ${cache.wiki.title}`, data );
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'user log',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName === 'Special:Log' &&
cache.link.getParam( 'user' ) ) {
return 100;
}
},
content: function ( $l, cache ) {
return new Api( cache.wiki.serverName )
.getUserLog( cache.link.getParam( 'user' ),
[ 'ids', 'title', 'type', 'timestamp', 'parsedcomment' ],
Popups.cfg.logLimit.userlog || Popups.cfg.logLimit.default )
.then( function ( logEvents ) {
var $table = constructLogEventView( cache.wiki.serverName, logEvents );
return $table;
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'user abuse filter log',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName === 'Special:AbuseLog' ) {
var user = cache.link.getParam( 'wpSearchUser' );
if ( user ) {
cache.afLog = {
user: user
};
return 100;
}
}
},
content: function ( $l, cache ) {
return new Api( cache.wiki.serverName )
.getUserAbuseFilterLog(
cache.afLog.user,
[ 'ids', 'title', 'action', 'timestamp', 'revid', 'filter', 'result' ],
Popups.cfg.logLimit.userlog || Popups.cfg.logLimit.default
)
.then( function ( abuseLog ) {
var $table = constructAfLogView( cache.wiki.serverName, abuseLog );
return $table;
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'abusefilter log entry',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName === 'Special:AbuseLog' ) {
const parts = cache.wiki.title.split( '/' );
if ( parts.length === 2 ) {
cache.afLog = {
entry: parseInt( parts[ 1 ] )
};
return 100;
}
}
},
content: function ( $l, cache ) {
return new Api( cache.wiki.serverName )
.getAbuseFilterLogEntry( cache.afLog.entry )
.then( ( entry ) => {
return constructAfLogEntry( cache.wiki.serverName, entry );
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'what links here',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName === 'Special:WhatLinksHere' ) {
cache.whatLinksHere = {
title: cache.wiki.title.substr( cache.wiki.title.indexOf( '/' ) + 1 )
};
return 1000;
}
},
content: function ( $l, cache ) {
var ok = function ( query ) {
var pages = [];
if ( query.backlinks ) {
pages = pages.concat( query.backlinks );
}
if ( query.embeddedin ) {
pages = pages.concat( query.embeddedin );
}
var $table = constructBacklinksView( cache.wiki.serverName, pages );
return $table;
};
return new Api( cache.wiki.serverName )
.getWhatLinksHere( cache.whatLinksHere.title,
[ 'backlinks', 'embeddedin' ],
Popups.cfg.linkLimit.default )
.then( ok );
}
} );
Popups.cfg.contentHooks.push( {
name: 'what leaves here',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName === 'Special:WhatLeavesHere' ) {
cache.whatLeavesHere = {
title: cache.link.getParam( 'target' )
};
return 1000;
}
},
content: function ( $l, cache ) {
var ok = function ( page ) {
if ( page.missing ) {
return constructMissingPageError();
} else {
var pages = [];
if ( page.templates ) {
pages = pages.concat( page.templates );
}
if ( page.images ) {
pages = pages.concat( page.images );
}
if ( page.links ) {
pages = pages.concat( page.links );
}
var $table = constructBacklinksView( cache.wiki.serverName, pages );
return $table;
}
};
return new Api( cache.wiki.serverName )
.getWhatLeavesHere( cache.whatLeavesHere.title,
[ 'templates', 'images', 'links' ],
Popups.cfg.linkLimit.default )
.then( ok );
}
} );
Popups.cfg.contentHooks.push( {
name: 'image info',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.title && cache.wiki.namespace === 'File' &&
cache.wiki.action === 'view' ) {
return 1000;
}
},
content: function ( $l, cache ) {
var $content = $( '<div>' ).addClass( 'popups_img' );
var $info = $( '<div>' )
.addClass( 'popups_image_info' )
.appendTo( $content );
var alt = $l.attr( 'alt' ) || 'none';
$info.append( makeClassedSpan( 'popups_info_title', 'Alt text: ' ), alt );
return new Api( cache.wiki.serverName )
.getImageInfo( cache.wiki.title,
[ 'text', 'categories', 'externallinks', 'properties' ] )
.then( function ( text ) {
$content.append( $( '<div>' )
.addClass( 'popups_image_content' )
.append( text ) );
return $content;
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'recent changes',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.title ) {
cache.recentChanges = {};
const nses = cache.link.getParam( 'namespace' );
if ( nses ) {
cache.recentChanges.namespaces = nses.split( '|' );
}
if ( cache.wiki.baseName === 'Special:RecentChanges' ) {
return 1000;
}
// This does not actually work, because there is no API for this
// (requested since 2008: T17552)
if ( cache.wiki.baseName === 'Special:RecentChangesLinked' ) {
cache.recentChanges = {
title: cache.wiki.title.substr( cache.wiki.title.indexOf( '/' ) + 1 )
};
return 1000;
}
cache.recentChanges = undefined;
}
},
content: function ( $l, cache ) {
var $content = $( '<div>' ).addClass( 'popups_rc' );
var rcprops = [ 'title', 'timestamp', 'ids', 'user', 'parsedcomment', 'sizes' ];
if ( haveRight( 'patrol' ) ) {
rcprops.push( 'patrolled' );
}
return new Api( cache.wiki.serverName )
.getRecentChanges( cache.recentChanges.title,
{
props: rcprops,
types: Popups.cfg.rcTypes,
limit: Popups.cfg.logLimit.rc || Popups.cfg.logLimit.default,
ns: cache.recentChanges.namespaces
} )
.then( function ( recentChanges ) {
$content.append( constructRcLogView( cache.wiki.serverName,
recentChanges ) );
return $content;
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'watchlist',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName === 'Special:Watchlist' ) {
cache.watchlist = {};
const ns = cache.link.getParam( 'namespace' );
if ( ns !== null ) {
cache.watchlist.namespaces = ns.split( ';' );
}
return 1000;
}
},
content: function ( $l, cache ) {
var $content = $( '<div>' ).addClass( 'popups_watchlist' );
var wlProps = [ 'title', 'timestamp', 'ids', 'user', 'parsedcomment', 'flags', 'sizes' ];
if ( haveRight( 'patrol' ) ) {
wlProps.push( 'patrolled' );
}
return new Api( cache.wiki.serverName )
.getWatchlistEntries(
mw.config.get( 'wgUserName' ),
wlProps,
Popups.cfg.watchlistTypes,
cache.watchlist.namespaces,
Popups.cfg.logLimit.watchlist || Popups.cfg.logLimit.default
)
.then( function ( watchlist ) {
$content.append( constructRcLogView( cache.wiki.serverName, watchlist ) );
return $content;
} );
}
} );
Popups.cfg.contentHooks.push( {
name: 'category members',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.namespace === 'Category' ) {
return 1000;
}
},
content: function ( $l, cache ) {
var handleCatMembers = function ( categorymembers ) {
var $content = $( '<div>' ).addClass( 'popups_catmember' );
$content.append( $( '<h1>' ).append( 'Parent categories' ) );
$content.append( $( '<h1>' ).append( 'Category members' ) );
$content.append( constructCategoryListView( cache.wiki.serverName,
categorymembers, 'hlist' ) );
return $content;
};
return new Api( cache.wiki.serverName )
.getCategoryMembers( cache.wiki.title,
Popups.cfg.linkLimit.categoryMembers || Popups.cfg.linkLimit.default )
.then( handleCatMembers );
}
} );
Popups.cfg.contentHooks.push( {
name: 'raw page image',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.link && cache.link.href && cache.link.href.match( /page\d+-\d+px/ ) ) {
// outweigh "normal" raw image content, which might go to File: page
return 2000;
}
},
content: function ( $l, cache ) {
return constructRawImageContent( cache.link.href );
}
} );
Popups.cfg.contentHooks.push( {
name: 'random',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.baseName === 'Special:Random' ||
cache.wiki.baseName === 'Special:RandomRootpage' ) {
return 2000;
}
},
content: function ( $l, cache ) {
var $content = $( '<div>' )
.addClass( 'popups-random' );
var $ul = $( '<ul>' ).appendTo( $content );
/* Dirty hack: T274219 */
var filterRoots = function ( page ) {
return ( page.ns !== 0 ) || page.title.indexOf( '/' ) === -1;
};
// TODO: invalid outside enWS
var ns = [];
switch ( cache.wiki.title.substr( cache.wiki.title.lastIndexOf( '/' ) + 1 ) ) {
case 'Index':
ns.push( 106 );
break;
case 'Author':
ns.push( 102 );
break;
default:
ns.push( 0 );
}
return new Api( cache.wiki.serverName )
.getRandomPages( ns,
Popups.cfg.linkLimit.randomPages || Popups.cfg.linkLimit.default )
.then( function ( pages ) {
pages = pages.filter( filterRoots );
for ( var i = 0; i < pages.length; ++i ) {
$( '<li>' ).append( $( '<a>' )
.attr( 'href', getArticleUrl( pages[ i ].title ) )
.append( pages[ i ].title ) )
.appendTo( $ul );
}
return $content;
} );
}
} );
/*
* Convert Special:EntityPage into normal content
*/
Popups.cfg.contentHooks.push( {
name: 'wikibase entity',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.namespace === 'Special' &&
cache.wiki.titleNoNs.startsWith( 'EntityPage/' ) ) {
cache.entityPage = {
qid: cache.wiki.titleNoNs.split( '/' ).at( -1 )
};
return 2000;
}
},
content: function ( $l, cache ) {
return constructPageRender( cache.wiki.serverName, cache.entityPage.qid );
}
} );
function deleteFromPreset( api, page, delInfo ) {
let reason = '';
if ( delInfo.message ) {
/* eslint-disable-next-line mediawiki/msg-doc */
reason = mw.msg( delInfo.message );
} else if ( delInfo.reason ) {
reason = delInfo.reason;
}
let promise;
if ( !delInfo.noconfirm ) {
promise = confirmAction(
mw.msg( 'popups-delete-confirm', page, reason )
);
} else {
promise = Promise.resolve( true );
}
promise.then( function () {
api.delete( page, reason, delInfo.watchlist )
.then(
function () {
mw.notify( mw.msg( 'popups-delete-ok', page ) );
},
function () {
mw.notify( mw.msg( 'popups-delete-failed', page ) );
}
);
} );
}
function clickableLink( text, clickHandler ) {
return $( '<a>' )
.append( text )
.on( 'click', clickHandler );
}
Popups.cfg.contentHooks.push( {
name: 'delete',
score: function ( $l, cache ) {
if ( cache.recognised.type === 'wiki_local' &&
cache.wiki.action === 'delete'
) {
return 100;
}
},
content: function ( $l, cache ) {
const $ul = $( '<ul>' );
const api = new Api( cache.wiki.serverName );
for ( const delInfo of Popups.cfg.deletes ) {
$( '<li>' )
.append( clickableLink( delInfo.name,
function () {
deleteFromPreset( api, cache.wiki.title, delInfo );
} )
)
.appendTo( $ul );
}
return $ul;
}
} );
var purgePage = function ( serverName, page ) {
var notify = function ( ok ) {
if ( ok ) {
mw.notify( 'Page purged.' );
} else {
mw.notify( 'Purge failed.', { type: 'error' } );
}
};
new Api( serverName )
.doPurgePage( page )
.then( function ( data ) {
notify( data.purge[ 0 ].purged );
}, function () {
notify( false );
} );
};
function makeEditHistTuple( serverName, title, text, isLocal ) {
var actionFactory, urlFactory;
if ( isLocal ) {
urlFactory = function ( urlTitle ) {
return getWikiArticleUrl( serverName, urlTitle );
};
actionFactory = function ( urlTitle, action ) {
return getWikiActionUrl( serverName, urlTitle, action );
};
} else {
urlFactory = getCommonsArticleUrl;
actionFactory = getCommonsActionUrl;
}
return [
{
text: text || title,
href: urlFactory( title )
},
{
text: 'e',
href: actionFactory( title, 'edit' ),
nopopup: true
},
{
text: 'h',
href: actionFactory( title, 'history' )
}
];
}
function makeFileDirectTuple( file, directUrl, isLocal, opts ) {
var actionFactory, urlFactory;
if ( isLocal ) {
urlFactory = getArticleUrl;
actionFactory = getActionUrl;
} else {
urlFactory = getCommonsArticleUrl;
actionFactory = getCommonsActionUrl;
}
return [
{
text: opts.name || 'file',
href: urlFactory( 'File:' + file )
},
{
text: 'e',
href: actionFactory( file, 'edit' ),
nopopup: true
},
{
text: 'h',
href: actionFactory( file, 'history' )
},
{
text: '↓',
href: directUrl,
nopopup: true
},
{
text: 'reupload',
href: getReuploadUrl( file, isLocal )
}
];
}
Popups.cfg.actionHooks.push( {
name: 'basic actions',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
// if not a page on the current wiki, or a Js thing, basic content
// doesn't work
if ( !cache.wiki.title ||
cache.wiki.namespace === 'Special' ) {
return;
}
// the basic actions are usually fairly important
return 1000;
},
actions: function ( $l, cache ) {
var actions = [];
var altNs;
var altDisplay;
var fn = cache.wiki.titleNoNs;
const api = new Api( cache.wiki.serverName );
var isLocalPromise;
if ( cache.wiki.namespace === 'File' && cache.wiki.local ) {
// files can be non-local even if the link says they are local
isLocalPromise = api.getFileIsLocal( fn );
} else {
isLocalPromise = Promise.resolve( true );
}
return isLocalPromise.then( function ( isLocal ) {
var serverName;
if ( !isLocal ) {
serverName = Popups.commons.serverName;
} else {
serverName = cache.wiki.serverName;
}
if ( cache.wiki.namespace.endsWith( ' talk' ) ) {
altNs = cache.wiki.namespace.replace( ' talk', '' );
altDisplay = 'article';
} else {
altNs = cache.wiki.namespace + ' talk';
altDisplay = 'talk';
}
var oldid;
if ( cache.link ) {
oldid = cache.link.getParam( 'oldid' );
}
const basics = [];
if ( cache.wiki.section ) {
basics.push( {
text: 'page',
href: getWikiArticleUrl( serverName, cache.wiki.title )
} );
}
basics.push( {
text: 'edit',
href: getWikiActionUrl( serverName, cache.wiki.title, 'edit',
{ oldid: oldid } ),
nopopup: true
} );
basics.push( {
text: 'wikitext',
href: oldid ?
getWikiActionUrl( serverName, cache.wiki.title, null,
{ oldid: oldid } ) :
getWikiArticleUrl( serverName, cache.wiki.title ),
class: [ 'popups_wikitext' ]
} );
basics.push( {
text: 'history',
href: getWikiActionUrl( serverName, cache.wiki.title, 'history' )
} );
basics.push( makeEditHistTuple( serverName,
altNs + ':' + cache.wiki.titleNoNs, altDisplay, isLocal )
);
basics.push( {
text: mw.msg( 'popups-actions-info' ),
href: getWikiActionUrl( serverName, cache.wiki.title, 'info' )
} );
basics.push( {
text: mw.msg( 'popups-actions-move' ),
href: getWikiArticleUrl( serverName,
'Special:MovePage/' + cache.wiki.title )
} );
const deletes = [
// The main delete action
{
text: mw.msg( 'popups-actions-delete' ),
href: getWikiActionUrl( serverName, cache.wiki.title, 'delete' )
}
];
for ( const delInfo of Popups.cfg.deletes ) {
if ( delInfo.asAction ) {
deletes.push( {
text: delInfo.name,
click: function () {
deleteFromPreset( api, cache.wiki.title, delInfo );
}
} );
}
}
basics.push( deletes );
actions.push( basics );
var unWatchPage = function ( watch, title ) {
new Api( cache.wiki.serverName )
.watchPage( title, watch )
.then( function () {
var verb = watch ? 'added to' : 'removed from';
mw.notify( "'" + title + "' " + verb + ' watchlist.' );
},
function ( data ) {
mw.notify( 'Watchlist edit failed: ' + data, {
type: 'error'
} );
} );
};
actions.push( [ {
text: 'most recent edit',
href: getDiffUrl( serverName, cache.wiki.title, 'prev', 'cur' )
},
{
text: 'related changes',
href: getWikiArticleUrl( serverName, 'Special:RecentChangesLinked/' + cache.wiki.title )
},
[
{
text: 'un',
href: '#',
click: function () { unWatchPage( false, cache.wiki.title ); }
},
{
text: 'watch',
href: '#',
click: function () { unWatchPage( true, cache.wiki.title ); }
}
]
] );
actions.push( [ {
text: 'what links here',
href: getWikiArticleUrl( serverName, 'Special:WhatLinksHere/' + cache.wiki.title )
},
{
text: 'what leaves here',
href: getWikiArticleUrl( serverName, 'Special:WhatLeavesHere?target=' + cache.wiki.title )
},
{
text: 'purge',
click: function () {
purgePage( serverName, cache.wiki.title );
},
nopopup: true
}
] );
return actions;
} ); // end local then()
}
} );
function blockByPresetInfo( api, user, blockInfo ) {
let reason;
if ( blockInfo.message ) {
// eslint-disable-next-line mediawiki/msg-doc
reason = mw.msg( blockInfo.message );
} else {
// use the reason the user gave
reason = blockInfo.reason;
}
let promise;
if ( !blockInfo.noconfirm ) {
promise = confirmAction(
mw.msg( 'popups-block-confirm', user, reason )
);
} else {
promise = Promise.resolve( true );
}
promise.then( function ( confirmed ) {
if ( !confirmed ) {
return;
}
api
.blockUser( user, blockInfo.expiry,
{
reason: reason,
allowusertalk: blockInfo.allowusertalk
} )
.then(
function () {
mw.notify( mw.msg( 'popups-user-blocked', user ) );
},
function () {
mw.notify(
mw.msg( 'popups-user-block-failed', user ),
{ type: 'error' }
);
}
);
} );
}
Popups.cfg.actionHooks.push( {
name: 'user',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.user ) {
return 2000; // at top
}
},
actions: function ( $l, cache ) {
console.assert( cache.wiki.user );
const actions = [];
const acctActions = [];
const api = new Api( cache.wiki.serverName );
if ( !cache.wiki.isContribs ) {
actions.push( [
{
text: 'contribs',
href: getWikiArticleUrl( cache.wiki.serverName,
'Special:Contributions/' + cache.wiki.user )
},
{
text: 'com',
href: getWikiArticleUrl( Popups.commons.serverName,
'Special:Contributions/' + cache.wiki.user )
}
] );
} else if ( cache.wiki.serverName !== 'commons.wikimedia.org' ) {
actions.push( {
text: 'com',
href: getWikiArticleUrl( Popups.commons.serverName,
'Special:Contributions/' + cache.wiki.user )
} );
}
if ( cache.wiki.serverName !== 'commons.wikimedia.org' ) {
actions.push( [
{
text: '↑',
href: getWikiArticleUrl( cache.wiki.serverName,
'Special:ListFiles/' + cache.wiki.user )
},
{
text: 'com',
href: getWikiArticleUrl( Popups.commons.serverName,
'Special:ListFiles/' + cache.wiki.user )
}
] );
} else {
actions.push( {
text: '↑',
href: getWikiArticleUrl( cache.wiki.serverName,
'Special:ListFiles/' + cache.wiki.user )
} );
}
actions.push( {
text: 'user log',
href: getWikiActionUrl( cache.wiki.serverName, 'Special:Log', null,
{ user: cache.wiki.user } )
} );
actions.push( {
text: 'abuse log',
href: getWikiActionUrl( cache.wiki.serverName, 'Special:AbuseLog', null,
{ wpSearchUser: cache.wiki.user } )
} );
acctActions.push( {
text: 'Xtools',
href: 'https://xtools.wmflabs.org/ec/' + cache.wiki.serverName + '/' + cache.wiki.user
} );
acctActions.push( {
text: 'SUL',
href: getWikiArticleUrl( cache.wiki.serverName, 'Special:CentralAuth/' + cache.wiki.user )
} );
acctActions.push( {
text: 'global',
href: 'https://tools.wmflabs.org/guc/index.php?user=' + cache.wiki.user + '&blocks=true'
} );
if ( haveRight( 'block' ) ) {
const blocks = [];
blocks.push( {
text: 'un',
href: getWikiArticleUrl( cache.wiki.serverName,
'Special:Ublock/' + cache.wiki.user )
} );
blocks.push( {
text: 'block',
href: getWikiArticleUrl( cache.wiki.serverName,
'Special:Block/' + cache.wiki.user )
} );
for ( const blockInfo of Popups.cfg.blocks ) {
blocks.push( {
text: blockInfo.name,
click: function () {
blockByPresetInfo( api, cache.wiki.user, blockInfo );
}
} );
}
acctActions.push( blocks );
}
if ( haveRight( 'nuke' ) ) {
acctActions.push( {
text: mw.msg( 'popups-action-mass-delete' ),
href: getWikiActionUrl( cache.wiki.serverName,
'Special:Nuke', null,
{ target: cache.wiki.user }
)
} );
}
return [ actions, acctActions ];
}
} );
Popups.cfg.actionHooks.push( {
name: 'prefs',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.title === 'Special:Preferences' ) {
return 2000; // at top
}
},
actions: function ( $l, cache ) {
var prefLink = getWikiArticleUrl( cache.wiki.serverName, 'Special:Preferences' );
var prefSecs = [
[ 'user profile', '#mw-prefsection-personal' ],
[ 'appearance', '#mw-prefsection-rendering' ],
[ 'editing', '#mw-prefsection-editing' ],
[ 'recent changes', '#mw-prefsection-rc' ],
[ 'watchlist', '#mw-prefsection-watchlist' ],
[ 'gadgets', '#mw-prefsection-gadgets' ],
[ 'search', '#mw-prefsection-searchoptions' ],
[ 'beta features', '#mw-prefsection-betafeatures' ],
[ 'notifications', '#mw-prefsection-echo' ]
];
var actions = [];
for ( var i = 0; i < prefSecs.length; ++i ) {
actions.push( {
text: prefSecs[ i ][ 0 ],
href: prefLink + prefSecs[ i ][ 1 ],
nopopup: true
} );
}
return actions;
}
} );
Popups.cfg.actionHooks.push( {
name: 'contributions',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.isContribs ) {
return 2000; // at top
}
},
actions: function ( $l, cache ) {
var contribLink = getWikiArticleUrl( cache.wiki.serverName,
'Special:Contributions/' + cache.wiki.user );
var actions = [
[]
];
var nsList = Popups.cfg.nsLinkList.default;
for ( var i = 0; i < nsList.length; ++i ) {
var ns = nsList[ i ];
var nsName = mw.config.get( 'wgFormattedNamespaces' )[ ns ];
var url = contribLink + '?namespace=' + ns;
actions[ 0 ].push( {
text: nsName || 'Main',
href: url
} );
}
return actions;
}
} );
Popups.cfg.actionHooks.push( {
name: 'file',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.namespace === 'File' ) {
return 0; // at end
}
},
actions: function ( $l, cache ) {
var fn = cache.wiki.titleNoNs;
return new Api( cache.wiki.serverName )
.getFileImageinfo( fn, [ 'url' ] )
.then( function ( imgPage ) {
const isLocal = imgPage.imagerepository !== 'shared';
const directUrl = imgPage.imageinfo[ 0 ].url;
var actions = [
{
text: '↓',
href: directUrl,
nopopup: true
},
{
text: 'reupload',
href: getReuploadUrl( fn, isLocal )
}
];
return [ actions ];
} );
}
} );
Popups.cfg.actionHooks.push( {
name: 'watchlist',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.title === 'Special:Watchlist' ) {
return 100;
}
},
actions: function ( $l, cache ) {
const nsActions = [];
for ( const ns of Popups.cfg.nsLinkList.default ) {
nsActions.push( {
href: getWikiActionUrl( cache.wiki.serverName, 'Special:Watchlist',
null, { namespace: ns } ),
text: mw.config.get( 'wgFormattedNamespaces' )[ ns ] || 'Main'
} );
}
return [ nsActions ];
}
} );
Popups.cfg.actionHooks.push( {
name: 'recentchanges',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.title === 'Special:RecentChanges' ) {
return 100;
}
},
actions: function ( $l, cache ) {
const nsActions = [];
for ( const ns of Popups.cfg.nsLinkList.default ) {
nsActions.push( {
href: getWikiActionUrl( cache.wiki.serverName, 'Special:RecentChanges',
null, { namespace: ns } ),
text: mw.config.get( 'wgFormattedNamespaces' )[ ns ] || 'Main'
} );
}
return [ nsActions ];
}
} );
Popups.cfg.actionHooks.push( {
name: 'pp-index',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.namespace === 'Index' ) {
return 0; // at end
}
},
actions: function ( $l, cache ) {
var fn = cache.wiki.titleNoNs;
return new Api( cache.wiki.serverName )
.getFileImageinfo( fn, [ 'url' ] )
.then( function ( imgPage ) {
var isLocal = imgPage.imagerepository !== 'shared';
var actions = [];
actions.push( makeFileDirectTuple( fn, imgPage.imageinfo[ 0 ].url, isLocal,
{ name: 'file' } ) );
// saves actually looking up the content model
if ( !fn.endsWith( '.css' ) ) {
actions.push( makeEditHistTuple( cache.wiki.serverName,
'Index:' + fn + '/styles.css', 'styles.css', true ) );
}
return [ actions ];
} );
}
} );
Popups.cfg.actionHooks.push( {
name: 'pp-page',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.wiki.namespace === 'Page' ) {
return 0; // at end
}
},
actions: function ( $l, cache ) {
var preloadImageHead = function ( url ) {
$.ajax( {
type: 'HEAD',
url: url
} );
};
var fn = cache.wiki.titleNoNs.replace( /\/\d+$/, '' );
var pg = cache.wiki.titleNoNs.replace( /^.*\/(\d+)$/, '$1' );
var size = 1024;
return new Api( cache.wiki.serverName )
.getPageImageUrl( fn, pg, size )
.then( function ( data ) {
var imgActions = [];
// var num = parseInt( pg );
if ( pg > 1 ) {
imgActions.push( {
text: 'prev page',
href: getArticleUrl( 'Page:' + fn + '/' + ( parseInt( pg ) - 1 ) )
} );
}
const pageInfo = data.query.pages[ 0 ];
const imageInfo = pageInfo.imageinfo[ 0 ];
const pageImageUrl = imageInfo.thumburl;
imgActions.push( {
text: 'page image',
href: pageImageUrl
} );
preloadImageHead( pageImageUrl );
const isLocal = pageInfo.imagerepository !== 'shared';
imgActions.push( {
text: 'next page',
href: getArticleUrl( 'Page:' + fn + '/' + ( parseInt( pg ) + 1 ) )
} );
const indexActions = [
makeEditHistTuple( cache.wiki.serverName,
'Index:' + fn, 'index', true ),
makeFileDirectTuple( fn, imageInfo.url, isLocal,
{
name: 'file'
} ),
makeEditHistTuple( cache.wiki.serverName,
'Index:' + fn + '/styles.css', 'styles', true )
];
return [ imgActions, indexActions ];
} );
}
} );
Popups.cfg.actionHooks.push( {
name: 'diff',
onlyType: 'wiki_local',
score: function ( $l, cache ) {
if ( cache.link.getParam( 'oldid' ) ) {
return 2000;
}
},
actions: function ( $l, cache ) {
var revPromise;
var diffInt = parseInt( cache.link.getParam( 'diff' ) );
if ( cache.link.getParam( 'oldid' ) ) {
if ( !cache.link.getParam( 'diff' ) || isNaN( diffInt ) ) {
// easy - the rev is the oldid
revPromise = Promise.resolve( cache.link.getParam( 'oldid' ) );
}
}
const api = new Api( cache.wiki.serverName );
if ( cache.link.getParam( 'diff' ) === 'cur' ) {
// we have to get the current ID as an integer
revPromise = api
.getCurrentRevs( cache.wiki.title )
.then( function ( revs ) {
return revs[ 0 ].revid;
} );
}
if ( !revPromise && !isNaN( diffInt ) ) {
revPromise = Promise.resolve( cache.link.getParam( 'diff' ) );
}
if ( !revPromise ) {
return;
}
return revPromise.then( function ( rev ) {
var promises = [];
var getRevertAction = function ( compare ) {
var revLink = getSpecialDiffLink( compare.torevid );
var revertSummary = 'Revert to revision ' + revLink +
' dated ' + getDatetimeFromTimestamp( compare.totimestamp ) +
' by ' + getUserLink( cache.wiki.serverName, compare.touser );
var revertToUrl = getWikiActionUrl( cache.wiki.serverName,
cache.wiki.title, 'edit', {
summary: revertSummary,
oldid: compare.torevid
} );
var revertAction = {
text: 'revert to',
href: revertToUrl,
nopopup: true
};
return revertAction;
};
var getUndoAction = function ( compare ) {
var url = getWikiActionUrl( cache.wiki.serverName, cache.wiki.title, 'edit',
{ undo: compare.torevid, undoafter: compare.fromrevid } );
return {
text: 'undo',
href: url
};
};
var getRollbackAction = function ( compare ) {
return {
text: 'rollback',
click: function () {
api
.rollbackEdit( compare.toid, compare.touser )
.then( function () {
mw.notify( 'Edit rolled back.' );
},
function ( error ) {
mw.notify( 'Rollback failed: ' + error, {
type: 'error'
} );
} );
}
};
};
var getPatrolAction = function ( revToPatrol ) {
return {
text: 'patrol',
click: () => {
api
.patrolRevision( revToPatrol )
.then( () => {
mw.notify( `Revision on page ${cache.wiki.title}`,
{
type: 'success',
title: 'Patrolled'
} );
},
( err, info ) => {
mw.notify( `Revision on page ${cache.wiki.title}:` +
'\n' + info.error.info,
{
title: 'Failed to patrol',
type: 'error'
} );
} );
}
};
};
var thankAction = function ( revToThank ) {
return {
text: 'thank',
click: function () {
api
.thankForRevision( revToThank )
.then( function () {
mw.notify( 'User thanked.' );
},
function ( error ) {
mw.notify( 'Thank failed: ' + error, {
type: 'error'
} );
} );
}
};
};
var undoProm = new Api( cache.wiki.serverName )
.getSingleEditCompare( rev, [ 'ids', 'user', 'timestamp' ] )
.then( function ( compare ) {
var actions = [
getUndoAction( compare ),
getRevertAction( compare )
];
if ( haveRight( 'rollback' ) ) {
actions.push( getRollbackAction( compare ) );
}
if ( haveRight( 'patrol' ) ) {
actions.push( getPatrolAction( rev ) );
}
actions.push( thankAction( rev ) );
return actions;
},
function () {
var actions = [];
if ( haveRight( 'patrol' ) ) {
actions.push( getPatrolAction( rev ) );
}
actions.push( thankAction( rev ) );
return actions;
} );
promises.push( undoProm );
return Promise.all( promises )
.then( function ( values ) {
var actions = [];
for ( var i = 0; i < values.length; ++i ) {
[].push.apply( actions, values[ i ] );
}
return [ actions ];
} );
} );
}
} );
function sortByScore( a, b ) {
return b[ 1 ] - a[ 1 ];
}
function recognise( $elem, cache ) {
const scores = [];
const proms = [];
for ( const hook of Popups.cfg.recogniserHooks ) {
proms.push(
// it's OK if it returns a value, promisify it
Promise.resolve( hook.score( $elem, cache ) )
.then(
function ( score ) {
if ( score !== undefined ) {
scores.push( [ hook, score ] );
}
}
)
);
}
return Promise.all( proms )
.then( function () {
scores.sort( sortByScore );
if ( scores.length > 0 ) {
const topScore = scores[ 0 ][ 1 ];
return scores[ 0 ][ 0 ].canonical( $elem, cache )
.then( function ( recognised ) {
cache.recognised = recognised;
return { recognised, score: topScore };
} );
} else {
return Promise.reject();
}
} );
}
function getContent( $elem, cache ) {
const scores = [];
const proms = [];
for ( const hook of Popups.cfg.contentHooks ) {
// skip content hooks without compatible recognised types
let onlyTypes = hook.onlyType;
if ( onlyTypes ) {
if ( !Array.isArray( onlyTypes ) ) {
onlyTypes = [ onlyTypes ];
}
if ( onlyTypes && onlyTypes.indexOf( cache.recognised.type ) === -1 ) {
continue;
}
}
proms.push(
// it's OK if it returns a value, promisify it
Promise.resolve( hook.score( $elem, cache ) )
.then(
function ( score ) {
if ( score !== undefined ) {
scores.push( [ hook, score ] );
}
}
)
);
}
return Promise.all( proms )
.then( function () {
scores.sort( sortByScore );
if ( scores.length > 0 ) {
const topScore = scores[ 0 ][ 1 ];
console.log( `Content hook: ${scores[ 0 ][ 0 ].name}, score: ${topScore}` );
return scores[ 0 ][ 0 ].content( $elem, cache );
} else {
return Promise.reject( 'No scoring content found' );
}
} );
}
function getActions( $elem, cache, callback ) {
const scores = [];
const proms = [];
for ( const hook of Popups.cfg.actionHooks ) {
// skip hooks without compatible recognised types
let onlyTypes = hook.onlyType;
if ( onlyTypes ) {
if ( !Array.isArray( onlyTypes ) ) {
onlyTypes = [ onlyTypes ];
}
if ( onlyTypes && onlyTypes.indexOf( cache.recognised.type ) === -1 ) {
continue;
}
}
proms.push(
// it's OK if it returns a value, promisify it
Promise.resolve( hook.score( $elem, cache ) )
.then(
function ( score ) {
if ( score !== undefined ) {
scores.push( [ hook, score ] );
}
}
)
);
}
// sort the returns and resolve
// TODO: should we return ASAP above, and let the caller sort, or
// will that make things too jumpy?
return Promise.all( proms )
.then( function () {
scores.sort( sortByScore );
for ( var ii = 0; ii < scores.length; ii++ ) {
const actions = scores[ ii ][ 0 ].actions( $elem, cache );
Promise.resolve( actions )
.then( function ( resolvedActions ) {
if ( resolvedActions && resolvedActions.length ) {
callback( resolvedActions );
}
} );
}
} );
}
function LinkCache( href, baseHref ) {
this.href = href;
if ( href ) {
// tack on fragments
if ( href.startsWith( '#' ) ) {
href = baseHref.replace( /#.*$/, '' ) + href;
}
try {
this.url = new URL( href, baseHref );
if ( this.url.search ) {
this.params = this.url.searchParams;
}
} catch ( e ) {
// this is OK, we just won't have a URL
}
}
}
LinkCache.prototype.getParam = function ( param ) {
return this.params ? this.params.get( param ) : null;
};
LinkCache.prototype.hasAllParams = function ( params ) {
if ( !this.params ) {
return false;
}
for ( var i = 0; i < params.length; ++i ) {
if ( !this.params.get( params[ i ] ) ) {
return false;
}
}
return true;
};
function makeActionLink( action ) {
const $actLink = $( '<a>' );
if ( action.href ) {
$actLink.attr( 'href', action.href );
}
if ( action.click ) {
$actLink.on( 'click', action.click );
}
$actLink.append( action.text );
if ( action.nopopup ) {
$actLink.addClass( 'popups_nopopup' );
}
if ( action.class ) {
// eslint-disable-next-line mediawiki/class-doc
$actLink.addClass( action.class );
}
return $actLink;
}
function PopupManager() {
this.popups = [];
}
PopupManager.prototype.destroyPopups = function ( fromIndex ) {
fromIndex = fromIndex || 0;
for ( let i = this.popups.length - 1; i >= fromIndex; --i ) {
const leaf = this.popups.pop();
leaf.$element.remove();
}
};
PopupManager.prototype.addPopup = function ( popup ) {
this.popups.push( popup );
};
PopupManager.prototype.showPopup = function ( $elem /* event */ ) {
let $parent;
// if we get some kind of relative URL, we need to make it relative
// to the right thing
let baseHref;
if ( $elem.parents( '.popups_popup' ).length === 0 ) {
// if this is a new root popup, remove ALL existing ones
this.destroyPopups();
$parent = $( document.body );
baseHref = window.location.href;
} else {
// the closest popup to the hovered element
$parent = $elem.closest( '.popups_popup' );
const parentIndex = $parent.data( 'popups-popup-index' );
// delete any would-be siblings and their descendents
this.destroyPopups( parentIndex + 1 );
baseHref = $parent.data( 'popups-base-href' );
}
// cached data that hooks can read and write
var cache = new Cache();
cache.link = new LinkCache(
$elem.attr( 'href' ),
baseHref
);
// update it in case it _wasn't_ relative
baseHref = cache.link.url.href;
var $popupContent = $( '<div>' ).addClass( 'popups_main' );
var $popupTitle = $( '<div>' ).addClass( 'popups_title' );
var $contentContainer = $( '<div>' ).addClass( 'popups_content' );
var $actionsContainer = $( '<div>' ).addClass( 'popups_actions' );
var $actionsList = $( '<ul>' );
// $popupTitle.append(cache.pg_name);
$actionsContainer.append( $actionsList );
$popupContent
.append( $popupTitle )
.append( $actionsContainer )
.append( $contentContainer );
var popup = new OO.ui.PopupWidget( {
$content: $popupContent,
$floatableContainer: $elem,
padded: false,
width: null,
autoClose: true,
align: 'left',
hideWhenOutOfView: false,
classes: [ 'popups_popup' ]
} );
popup.$element.data( 'popups-base-href', baseHref );
popup.$element.data( 'popups-popup-index', this.popups.length );
$parent.append( popup.$element );
var $throbber = $( '<div>' )
.addClass( 'popups_throbber' )
.appendTo( $contentContainer );
popup.$element.on( 'keydown', function ( event ) {
console.log( event );
event.preventDefault();
} );
this.addPopup( popup );
recognise( $elem, cache ).then( function ( data ) {
// unpack
const { recognised, score } = data;
console.log( `Recognised with ${recognised.type}, score ${score}` );
var $titleLink = $( '<a>' )
.attr( 'href', recognised.href )
.append( recognised.display )
.appendTo( $popupTitle );
if ( recognised.href === cache.link.href ||
recognised.href === cache.link.href ) {
$titleLink.addClass( 'popups_nopopup' );
}
$popupTitle
.append(
makeCopyIcon( recognised.canonical )
);
if ( recognised.editUrl ) {
$popupTitle
.append(
makeEditIcon( recognised.editUrl )
);
}
getContent( $elem, cache )
.then( function ( $content, $shouldBeVisible ) {
$throbber.remove();
$contentContainer.append( $content );
popup.toggle( true );
if ( $shouldBeVisible && $shouldBeVisible.length > 0 ) {
$shouldBeVisible[ 0 ].scrollElementIntoView();
}
} );
getActions( $elem, cache, function ( actions ) {
for ( var i = 0; i < actions.length; i++ ) {
if ( actions[ i ].length === 0 ) {
continue; // skip empties
}
var $list = $( '<li>' )
.appendTo( $actionsList );
if ( Array.isArray( actions[ i ] ) ) {
for ( var j = 0; j < actions[ i ].length; ++j ) {
if ( Array.isArray( actions[ i ][ j ] ) ) {
$list.append( '(' );
for ( var k = 0; k < actions[ i ][ j ].length; ++k ) {
$list.append( makeActionLink( actions[ i ][ j ][ k ] ) );
if ( k < actions[ i ][ j ].length - 1 ) {
$list.append( ' | ' );
}
}
$list.append( ')' );
} else {
var $actLink = makeActionLink( actions[ i ][ j ] );
$list.append( $actLink );
}
if ( j < actions[ i ].length - 1 ) {
$list.append( ' · ' );
}
}
} else {
// single action
var $singleActionLink = makeActionLink( actions[ i ] );
$list.append( $singleActionLink );
}
}
popup.toggle( true );
} );
} );
};
/**
* Set up popup handlers for a given element
*
* @param {Object} elem the DOM element
*/
PopupManager.prototype.setupPopups = function ( elem ) {
var that = this;
$( elem ).find( 'a:not(.popups_nopopup)[href]' )
// .css({"color": "red"})
.on( 'mouseenter', function ( event ) {
if ( event.altKey ) {
return;
}
var $enteredElem = $( this );
// $enteredElem.css({"color": "green"});
setTimeout( function () {
// popup if still hovering after 500ms
if ( $enteredElem.is( ':hover' ) ) {
that.showPopup( $enteredElem, event );
}
}, Popups.cfg.showTimeout );
} );
};
const manager = new PopupManager();
function onMutation( mutations ) {
for ( var i = 0; i < mutations.length; i++ ) {
for ( var j = 0; j < mutations[ i ].addedNodes.length; j++ ) {
manager.setupPopups( mutations[ i ].addedNodes[ j ] );
}
}
}
function initPopupGadget() {
if ( Popups.cfg.skinDenylist.indexOf( mw.config.get( 'skin' ) ) > -1 ) {
return;
}
// cache user rights
mw.user.getRights().then( function ( r ) {
Popups.userRights = r;
} );
// eslint-disable-next-line no-jquery/no-global-selector
var body = $( 'body' )[ 0 ];
manager.setupPopups( body );
var observer = new MutationObserver( onMutation );
observer.observe( body, {
subtree: true,
childList: true
} );
}
const loadables = [ $.ready ];
mw.loader.using( [ 'mediawiki.api' ], function () {
// cache user rights
loadables.push( mw.loader.using( [ 'mediawiki.user' ],
function () {
mw.user.getRights()
.then( function ( r ) {
Popups.userRights = r;
} );
} )
);
const api = new Api( mw.config.get( 'wgServerName' ) );
loadables.push( cacheSiteInfo( api ) );
loadables.push( mw.loader.using( [
'mediawiki.util', 'mediawiki.ForeignApi',
'oojs-ui-core', 'oojs-ui-widgets'
] ) );
Promise.all( loadables ).then( () => {
mw.hook( 'ext.gadget.popups-reloaded.config' ).fire( Popups.cfg );
initPopupGadget();
} );
} );
// eslint-disable-next-line no-undef
}( jQuery, mediaWiki, Promise ) );