/**
* 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 ) );