User:Js/tools/combinedContribs.js
Appearance
< User:Js
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
![]() | Documentation for this user script can be added at User:Js/tools/combinedContribs. |
var disabledEvents
var rollbackRegex = /Help:Reverting">Reverted<\/a> edits by <a.* to last version by / //"
var scriptPage = 'User:Js'
var msg =
{legend:'Combined contributions'
}
function mm(txt){ return msg[txt] || txt }
var images =
{usercontribs: 'Δ'
,newpage:'N'
,deletedrevs: '<s>Δ</s>' // Λ
,review: '✓'
,review_unapprove: '<s>✓</s>'
,patrol:'c/c5/Blue_check_PD.svg'
,move: 'b/b9/Gtk-go-forward-ltr.svg'//' 9/94/Gnome-go-next.svg'
,upload: 'c/ca/Icon_-_upload_photo.svg'
,upload_overwrite:'c/cd/Image_upload-tango.svg'
,upload_revert:'8/83/Gnome-insert-image.svg'
,block: '5/50/NIK-interdit.svg'
// 'f/f1/Stop_hand_nuvola.svg'
// 'a/aa/Commons-emblem-hand-orange.svg'
,block_unblock: 'a/ab/Torchlight_reload.png'
//'5/5f/Process-stop_Gion.svg'
//'4/4f/Blocked_user.svg'
,'delete': 'c/c3/Chess_tile_xr.png'
,delete_restore: 'a/ad/Chess_tile_xg.png' // try File:Add.svg
,delete_revision:'8/89/Gnome-edit-cut.svg'
//,'delete': '1/16/Deletion_icon.svg' //'d/de/User-trash-empty-4.svg'
,protect: '2/2c/Padlock-dash.svg'
,protect_unprotect:'3/3d/Padlock-silver-medium-open.png'
,protect_move_prot:'1/13/Padlock-olive-arrow.svg'
,protect_create:'7/70/Padlock-pink.svg'
,rights: '4/4a/Nuvola_apps_kgpg2.svg'
,renameuser: 'e/ed/Nuvola_apps_kuser.svg'
,newusers: '6/63/Bathrobecabalicon.svg'
,abuselog: 'e/ee/Emblem-unblock-granted.svg' //'a/a4/Text_document_with_red_question_mark.svg'
,abuselog_warn: 'e/ec/Emblem-unblock-request.svg'
,abuselog_disallow: '5/59/Emblem-unblock-denied.svg'
,abusefilter: 'af'
,rollback:'←'
,userlink:'1/12/User_icon_2.svg'
}
// 'block|protect|delete|upload|move|review|stable|rights|abusefilter|renameuser',
// import, patrol, merge, suppress, gblblock, globalauth, gblrights, newusers
function image(tt, sz){
tt = images[tt] || ''
if( /\.(svg|png)$/.test(tt) ) tt = apl.icon(tt, sz || 15)
return tt
}
// !! could use API action=query&titles=File:Emblem-unblock-request.svg|File:Emblem-unblock-granted.svg&prop=imageinfo&iiprop=url&iiurlwidth=15
var inpUsers
var date1, date2
var pendingRequests
var events
var isMultiple
var allTitles
var userStats, userStatTypes
var firstRequestDone
mw.util.addCSS('\
#output {font-size:88%}\
#msg_container {position:relative}\
#msg_toggle {position:absolute; right:2px; top:0; border:1px solid gray; cursor:pointer}\
#msg_content {height:150px; overflow:auto; border:1px inset gray; background:white}\
td.user, td.details {font-size:smaller}\
tr.review td.type {opacity:0.6}\
tr.review td.details {color:gray}\
tr.approve-a td.details {font-size:smaller; opacity:0.6}\
tr.deletedrevs td.type a {color:gray}\
tr.abuselog td.details {font-size:smaller}\
span.autoapprove {color:gray; font-size:smaller}\
tr.rollback td.details {font-size:smaller}\
tr.uc-top td.type {border-right: 1px dotted #bbb}\
span.uc-minor {font-weight:bold; font-size:smaller}\
tr.logevents td.title {font-size:smaller}\
tr.unapprove td.type a {color:orange}\
tr.hidden {opacity:0.4}\
')
var blankPageCSS
if( wgPageName == scriptPage ) start()
function start(){
blankPageCSS = mw.util.addCSS('body > * {display:none}')
$('<div id=output>\
<div id=js-close style="border:2px outset gray; cursor:pointer;\
padding:3px; float:right; margin:0 1em;" title=Close>X</div>\
<fieldset id=dialog><legend>'+mm('legend')+'</legend>\
<form style="display:inline">\
<textarea rows=3 cols=30 id=users style="width:auto; float:left; margin-right:1em" />\
<div style="float:left; text-align:right">\
From: <input id=date_1 placeholder="now"><br />\
Time: <input id=date_2 placeholder="any"> \
</div>\
<span id=date_display />\
<input type=button id=go value=" ↓ " style="margin-left:1em">\
</form>\
</fieldset>\
<div id=msg_container style="clear:both">\
<div id=msg_toggle title=messages style="width:1.5em; background-image: url(\'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAQCAMAAAAlM38UAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA9QTFRFsbGxmpqa3d3deXl58/n79CzHcQAAAAV0Uk5T/////wD7tg5TAAAAMklEQVR42mJgwQoYBkqYiZEZAhiZUFRDxWGicEPA4nBRhNlAcYQokpVMDEwD6kuAAAMAyGMFQVv5ldcAAAAASUVORK5CYII=\');" > </div>\
<div id=msg_content />\
</div>\
<div id=header />\
<div id=results />\
<div id=js-footer />\
</div>')
.appendTo(document.body)
.show()
//prepare form
urlToForm('#dialog form')
$('#users').keyup(autoResizeTbox).each(autoResizeTbox)
$('#go').click(formSubmit)
//prepare other elements
$('#msg_toggle').click( function() { $('#msg_content').slideToggle() })
$('#js-close').click( function(){ $('#output').remove(); blankPageCSS.disabled = true })
// ------------
function autoResizeTbox(){
var hh = Math.min( 200, Math.max(this.scrollHeight, this.clientHeight) )
if (hh > this.clientHeight) this.style.height = hh + 'px'
}
} //start
function formSubmit(){ //analyze form data & start request
dispMsg('clear'); dispMsg('show')
inpUsers = $('#users').val().replace(/\r/g,'').replace(/ *(\n|,|;) */g,'|').split('|')
//dates
date1 = input2Date( 'date_1' )
if( date1 == 'error' ) return
date2 = input2Date( 'date_2', date1 )
if( date2 == 'error' ) return
//check that date2 < date1
if( date2 && ( ( date1 || getCurrentTime() ) - date2 < 10000 ) )
return dispMsg('Time period is too small or negative','red')
dispMsg( 'Form: <a href="' + formToUrl('#dialog form') + '">current parameters</a>')
contribsRequest()
}
function input2Date( inp, fromDate ){ //text input -> date or 'error'
var dd = fromDate ? new Date(fromDate) : getCurrentTime() // "new Date()" is to avoid modifying existing object
var str = $('#'+inp).val().replace(/\s/g,'')
if( str == $('#'+inp).attr('placeholder') ) str = ''
if( !str ){
dd = null //date is omitted
}else if( /^\d{8,14}$/.test(str) ){
while( str.length < 14 ) str += '0' //fill 0's to the full timestamp
dd = apl.parseTimestamp(str)
}else if( /^\d+d?$/.test(str) ){ //adjust days
dd.setDate( dd.getDate() - parseInt(str))
}else if( /^\d+h$/.test(str) ){ //adjust hours
dd.setHours( dd.getHours() - parseInt(str) )
}else{
dd = 'error'
dispMsg('cannot parse date «' + str + '»', 'red')
}
return dd
}
function contribsRequest(){
//initialize before API requests
var ipRX = /^((\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/
events = []
userStats = {}
pendingRequests = 0
userStatTypes = {}
$('#results').empty()
var user, curReq
var range, octet, lst //for CIDRs
isMultiple = ( inpUsers.length > 1 )
//loop through all users
for( var u=0; u<inpUsers.length; u++ ){
user = $.trim(inpUsers[u])
if( user == '' ) continue //probably an empty line
curReq = { requestid: user, list: '' }
//basic request; 'prop:flags' is here to get 'top edit' flag
addRequest('usercontribs', 'uc', {prop:'timestamp|title|parsedcomment|patrolled|ids|flags'} )
//check what kind of user
if( range = /\/(\d\d)$/.exec( user ) ){ //IP range
range = parseInt(range[1])
octet = user.replace (/\/\d\d$/, '')
if( ! ipRX.test(octet) ) return dispMsg('Cannot parse range: ' + user, 'red')
octet = /^(\d+\.)(\d+\.)(\d+\.)(\d+)$/.exec(octet)
isMultiple = true
delete curReq.ucuser //using ucuserprefix instead
switch( range ){
case 13: case 14: case 15: //make several /16 requests
lst = makeOctetsList(octet[2], 16 - range)
for( var i=0; i<lst.length; i++ ){
curReq.ucuserprefix = octet[1] + lst[i] + '.'
queryAPI( curReq, contribsReceive )
}
curReq = null
break
case 16: case 17: case 18: case 19: //request /16 and filter results later
curReq.ucuserprefix = octet[1] + octet[2]
break
case 20: case 21: case 22: case 23: //make several (16, 8,4,2) /24 requests
lst = makeOctetsList( octet[3], 24 - range)
for( var i=0; i<lst.length; i++ ){
curReq.ucuserprefix = octet[1] + octet[2] + lst[i] + '.'
queryAPI( curReq, contribsReceive )
}
curReq = null
break
case 24: case 25: case 26: //request /24 and filter results later
curReq.ucuserprefix = octet[1] + octet[2] + octet[3]
break
case 27: case 28: case 29: case 30: case 31: //request contribs for a list of IPs, up to 32 with /27
lst = makeOctetsList(octet[4], 32 - range)
for( var i=0; i<lst.length; i++ ) lst[i] = octet[1] + octet[2] + octet[3] + lst[i]
curReq.ucuser = lst.join('|')
break
default:
return dispMsg(user + ': invalid range, supported are /13 - /31', 'red')
}
}else if( /\*$/.test(user) ){ // e.g. 1.2.3.*
isMultiple = true
curReq.ucuserprefix = user.replace(/\*$/,'') // and 'ucuser' (added later) will be ignored
}else{ //just one user
addRequest('abuselog', 'afl', {prop:'timestamp|title|ids|filter|result'} )
//deletedrevs for sysops
if( /sysop-/.exec( wgUserGroups.join('-') + '-' ) )
addRequest('deletedrevs', 'dr', {prop:'parsedcomment|minor'} )
//logevents for non-IP
if( ! ipRX.test(user) )
addRequest('logevents', 'le', {prop: 'timestamp|title|parsedcomment|type|details'} )
}
//call api
if( curReq ) queryAPI( curReq, contribsReceive )
//start spinner
apl.spinner('#dialog')
}
//--------
function addRequest(name, prefix, params){
$.extend(params, {limit:500, user:user })
if( date1 ) params.start = apl.makeAPITimestamp(date1)
if( date2 ) params.end = apl.makeAPITimestamp(date2)
for (var k in params) curReq[ prefix + k ] = params[k]
curReq.list += (curReq.list ? '|' : '') + name
userStatTypes[name] = true
}
//function to convert "range" into smaller subranges
// ('0', 2) -> [0,1,2,4], ('128', 2) ->[128, 129, 130, 131], ('0',3) -> [0,1,2,4,5,6,7],
//from http://en.wikipedia.org/wiki/MediaWiki:Gadget-contribsrange.js
function makeOctetsList(octetStart, binaryDigits){
var lst = []
var count = Math.pow(2, binaryDigits)
octetStart = parseInt( octetStart.replace(/\./,'') )
octetStart = octetStart - octetStart % count
for( var i=0; i<count; i++ ) lst.push( (octetStart + i) )
return lst
}
}
function contribsReceive(resp, status, xhr){
apl.updateServerTime(xhr)
//warn if user UTC time was wrong
if( !firstRequestDone ){
var timeDiff = ( getCurrentTime() - wgServerTime ) / 1000
if( timeDiff > 60 ) dispMsg('Your time is different from server time: ' + timeDiff + ' sec', 'orange')
}
firstRequestDone = true
//prepare userStats objects, set "there was more" flags
var curUser = resp.requestid
userStats[curUser] = {}
for (var tp in resp['query-continue'] )
userStats[curUser][tp+'-more'] = true
//check for API arrors, e.g. "User X not found"
if( resp.error ) userStats[curUser].error = resp.error
if( resp.warning ) jsMsg(resp.warning.code + ':' + resp.warning.info)
//process data
if( resp.query ) contribsProcess( resp.query, curUser )
if( --pendingRequests <= 0 ) contribsShow() //all data received
}
function contribsProcess(data, curUser){ //called after each API response
//usercontribs - remove results that are outside of target IP range
if ( /\/(\d\d)$/.test(curUser) && data.usercontribs ){
var checkRange = parseRange( curUser )
var UC = []
for ( var i=0; i<data.usercontribs.length; i++ )
if ( inRange(data.usercontribs[i].user, checkRange.base, checkRange.mask) )
UC.push( data.usercontribs[i] )
//save stats
userStatTypes['extrarange'] = true
userStats[curUser]['extrarange'] = data.usercontribs.length - UC.length //how many results were thrown away
//replace data
delete data.usercontribs
data.usercontribs = UC
}
//deletedrevs (grouped by title) in API results --> append as separate events
if( data.deletedrevs ){
var DR = []
for( var i=0; i<data.deletedrevs.length; i++ )
for( var r=0; r < data.deletedrevs[i].revisions.length; r++ ) // .revisions[r] provides: parsedcomment, timestamp, minor
DR.push( $.extend( { title: data.deletedrevs[i].title }, data.deletedrevs[i].revisions[r] ) )
//replace data
delete data.deletedrevs
data.deletedrevs = DR
}
//append all events into events[]
for( var curSource in data )
for ( var i=0; i<data[curSource].length; i++ ){
var ev = data[curSource][i]
if( disabledEvents && disabledEvents.test(ev.type) ) continue
ev.source = curSource
if( !ev.type ) ev.type = curSource //fill .type with 'usercontribs' / 'deletedrevs', like in logevents
if( !ev.user ) ev.user = curUser
events.push( ev )
//keep per-user stats
if( !userStats[curUser][curSource] ) userStats[curUser][curSource] = 0
userStats[curUser][curSource]++
}
}
function contribsShow(){ //called when all responses are received
dispMsg('All data received.'); dispMsg('hide'); apl.spinner()
//first show per-user stats
var htm = '<table class=wikitable><tr><th>User</th>'
var colCnt = 0
for( var t in userStatTypes ){
colCnt++
htm += '<th>'+t+'</th>'
}
htm += '</tr>'
var cell
for( var u in userStats ){
htm += '<tr><td>' + apl.outputPage('Special:Contributions/'+u, u) + '</td>'
if( userStats[u].error ){ //check if error
htm += '<td colspan=' + colCnt + ' class=error title="Error: ' + userStats[u].error.code + '">'
+ userStats[u].error.info + '</td>'
}else{ //otherwise show data
for(var t in userStatTypes){
htm += '<td>'
cell = userStats[u][t] || ''
if( cell ) switch( t ){
case 'abuselog': cell = outputIntLink('title=Special:AbuseLog&wpSearchUser='+u, cell); break
case 'logevents': cell = outputIntLink('title=Special:Log&user='+u, cell); break
case 'deletedrevs':cell = apl.outputPage('Special:DeletedContributions/'+u, cell); break
case 'extrarange': break
}
if ( userStats[u][t+'-more'] ) cell += ' ...'
htm += cell + '</td>'
}
}
htm += '</tr>'
}
$('#results').append( htm + '</table>' )
if( events.length == 0 ) return
var i, ev, prev // 'prev' is used by findPrev() below
allTitles = {}
//sort by timestamp
events.sort(function(a, b){
if( a.timestamp < b.timestamp ) return 1
else if( a.timestamp > b.timestamp ) return -1
else if( a.type == 'review' ) return -1 //review first
else if( b.type == 'review' ) return 1
else if( /usercontribs|deletedrevs/.test(a.type) ) return 1 // logevents first
else return 0
})
//create output values for every event
for (i=0; i<events.length; i++){
ev = events[i]
//initial values
ev.classes = ev.source
if( ev.source == 'logevents' ) ev.classes += ' ' + ev.type + ' ' + (ev.action != ev.type ? ev.action : '')
ev.iconTD = image(ev.type+'_'+ev.action) || image(ev.type) || '<small>'+ev.type.substr(0,3)+'</small>'
ev.titleTD = apl.output( ev.title, 'title' )
ev.detailsTD = ev.parsedcomment || ''
switch( ev.type ){
case 'usercontribs':
if( /\[(edit|move)=/.test(ev.parsedcomment) && findPrev('protect') ){
ev.iconTD = prev.iconTD
ev.classes += ' ' + prev.classes
prev.hidden = true
}
if( rollbackRegex.test(ev.parsedcomment) ){
ev.iconTD = image('rollback')
ev.classes += ' rollback'
}else if( typeof ev.minor == 'string'){
ev.detailsTD = '<span class=uc-minor>m</span> ' + ev.detailsTD
ev.classes += ' uc-minor'
}
if( typeof ev['new'] == 'string'){
ev.iconTD = image('newpage')
ev.classes += ' uc-newpage'
}
ev.iconTD = outputIntLink('diff='+ev.revid, ev.iconTD)
if( typeof ev.top == 'string'){
//ev.titleTD += ' <span class=uc-top>(top)</span>'
//ev.iconTD += '<span class=uc-top title="top edit on this page">^</span>'
ev.classes += ' uc-top'
}
break
case 'deletedrevs':
ev.iconTD = outputIntLink('title=Special:undelete&diff=prev&target='
+ encodeURIComponent(ev.title) + '×tamp=' + ev.timestamp, ev.iconTD)
break
case 'abuselog':
ev.iconTD = image('abuselog_' + ev.result) || image ('abuselog')
ev.iconTD = outputIntLink('title=Special:AbuseLog&details='+ev.id, ev.iconTD)
ev.result = ev.result || 'done'
ev.classes += ' ' + ev.result
ev.detailsTD = ev.result + ' : '
+ apl.outputPage('Special:AbuseFilter/'+ev.filter_id, ev.filter)
break
case 'move':
ev.titleTD += '<br />' + apl.output(ev.move.new_title, 'title')
if( typeof ev.move.suppressedredirect == 'string' )
ev.detailsTD = '<span class=option>[noredir]</span> ' + ev.detailsTD
break
case 'review':
ev.iconTD = outputIntLink('diff='+ev[0]+'&oldid='+ev[1], ev.iconTD)
ev.detailsTD += ' ' //time between edit and review
+ apl.output( apl.parseTimestamp(ev.timestamp) - apl.parseTimestamp(ev[2]), 'hours' )
break
case 'block':
var u = ev.title.split(':')[1]
ev.titleTD = ' ' + apl.outputPage('Special:Contributions/'+u, u)
if (ev.block){
ev.detailsTD = '<tt>'+ev.block.duration
+ (':' + ev.block.flags.replace(/^anononly,nocreate$/,'')).replace(/^:$/,'')
+ ':</tt> ' + ev.detailsTD
}
break
case 'protect':
if( /\[create=/.test(ev[0]) ) ev.iconTD = image('protect_create')
ev.detailsTD = ev.action + ': ' + (ev[0]||'') + ' ' + (ev[1]|'') + ' ' + ev.detailsTD
case 'delete':
if( ev.action == 'revision' ){
var revs = ev[1].split(',')
ev.detailsTD = ''
for (var j=0; j<revs.length; j++)
ev.detailsTD += ' ' + outputIntLink('diff='+revs[j], revs[j])
ev.detailsTD += ' ' + (ev.parsedcomment || '') + ' ['+ev[2]+':'+ev[3]+']'
}
break
case 'abusefilter':
ev.iconTD = apl.outputPage( 'Special:AbuseFilter/history/'
+ ev[1] + '/diff/prev/' + ev[0], ev.iconTD )
ev.detailsTD = ev.action + ': ' + ev.detailsTD
break
}//switch
if( /deletedrevs|abuselog|review|protect|delete/.test(ev.type) )
allTitles[ ev.title ] = true
//join event with auto-review
if( ev.type != 'review' && findPrev('review') && /approve-i?a/.test(prev.action)){
var tip = prev.parsedcomment
if( prev.title != ev.title ) tip = prev.title + ' ' + tip
ev.titleTD += ' <span class=autoapprove title="' + tip.replace(/"/g,'"') + '">✓</span>'
prev.hidden = true
}
if( ev.type != 'patrol' && findPrev('patrol')
&& prev.patrol.auto == "1" && ev.title==prev.title){ //for enwiki
ev.titleTD += ' <span class=patrolled title="'
+ ('patrolled: ' + prev.parsedcomment).replace(/"/g,'"') + '">✓</span>'
prev.hidden = true
}
}//for
//create table
htm = '<table id=apiresults><tr>'
+ addTH('Time ago: hh:mm or dd,hh', '2/26/Clock_simple.svg')
+ ( isMultiple ? addTH('User') : '' )
+ addTH('Type')
+ addTH('Page')
+ addTH('Comment')
+ '</tr>'
//add rows
for( var j=0; j<events.length; j++ ){
ev = events[j]
//if( ev.hidden ) ev.classes += ' hidden'
if( ev.hidden ) continue
htm += '<tr class="' + ev.classes + '">'
+ apl.outputCell(ev, 'timestamp')
+ ( isMultiple
? '<td class=user><span class=sort>s</span>' //to sort IPs as text, not numbers
+ apl.outputPage('Special:Contributions/'+ev.user, ev.user) + '</td>'
: '' )
+ '<td class=type title="' + ev.classes + '">' + ev.iconTD + '</td>'
+ '<td class=title>' + ev.titleTD + '</td>'
+ '<td class=details>' + ev.detailsTD + '</td>'
+ '</tr>'
}
//append to DOM
$('#results').append( htm + '</table>' )
ts_makeSortable( $('#apiresults')[0] )
//request pages info to mark some links as red
var arr = []
for (var ttl in allTitles) arr.push(ttl)
while (arr.length > 0)
queryAPI({prop:'info', titles: arr.splice(0,50).join('|')}, pagesReceive)
return
function addTH(tip, ico){
return '<th title="' + tip + '">' + (ico ? apl.icon(ico, 15) : '') + '</th>'
}
function findPrev(wh){ //find previous (upper) event with specified type, on success put it into in var 'prev' and return true
var j = i, ms
while( --j >= 0 ){
prev = events[j]
ms = apl.parseTimestamp(prev.timestamp) - apl.parseTimestamp(ev.timestamp)
if( ms > 2000 ) break //less than 2 seconds earlier
if( prev.type == wh ) return true
}
return null
}
}
function pagesReceive(pgs){
if( ! pgs.query ) return
pgs = pgs.query.pages
//get all redlinked titles
var missingPage = {}
for (var id in pgs)
if( typeof pgs[id].missing == 'string' )
missingPage[ pgs[id].title ] = true
//loop through all title cells
var a, ttl
$('#apiresults').find('td.title').each(function(){
a = $(this).find('a')
ttl = a.attr('title') || a.text() // !!! need to process quot; in tooltip
if ( missingPage[ttl] ) a.addClass('new')
})
}
//------------AUX Functions-----------------
function dispMsg(txt, color){
var cnt = $('#msg_content')
switch(txt){
case 'clear': case null: cnt.empty(); break
case 'hide': cnt.hide(); break
case 'show': cnt.show(); break
default: cnt.append( '<span style="color:'+color+'">' + txt + '</span><br />')
}
}
function getCurrentTime(){
d = new Date()
//return new Date( d.getTime() + d.getTimezoneOffset() * 60000 )
return d
}
function outputIntLink(link, name){
return '<a href="' + wgScript + '?' + link + '">'+name+'</a>'
}
function queryAPI(req, func){
dispMsg('Req: <a href="' + wgScriptPath+'/api.php?action=query&' + $.param(req) + '">'
+ (req.list || req.prop || 'request') + ' '
+ (req.ucuser || req.ucuserprefix || '')
+ '</a>')
pendingRequests++
$.getJSON( wgScriptPath+'/api.php?action=query&format=json', req, func )
}
//IP RANGES, thx to bart: http://stackoverflow.com/questions/503052/javascript-is-ip-in-one-of-these-subnets
function parseRange(s){
s = /^(\d+\.\d+\.\d+\.\d+)\/(\d\d)$/.exec(s)
if (s) return {
base: ipNum(s[1]),
mask: -1<<(32-s[2])
}; else return null
}
function ipNum(ip) {
ip = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(ip)
if (ip) return (+ip[1]<<24) + (+ip[2]<<16) + (+ip[3]<<8) + (+ip[4])
else return null
}
function inRange(ip, base, mask){
return (ipNum(ip) & mask) == base
}
function urlToForm(frm){ //autofill form from URL
frm = $(frm)
var val
frm.find('input, textarea').add(frm.find('select'))
.each( function(t,el){
val = mw.util.getParamValue( el.name || el.id )
if( val == null ) return
switch( el.type ){
case 'radio':
frm.find('input [type=radio][value="'+val+'"]').attr('selected', true)
break
case 'checkbox':
switch( val ){
case 'on': case el.value: if( !el.checked ) el.click(); break
case 'off': case '': if( el.checked ) el.click(); break
default: el.value = val //custom value checkbox?
}
break
default: //text, select
el.value = val.replace(/\+/g,' ')
}
})
}
function formToUrl(frm){ //save form values into URL
$(frm).find('input, textarea').each( function(i, el){ if( !el.name ) el.name = el.id }) //serialize() needs 'em
var w = mw.util.getParamValue('withjs')
return document.URL.replace(/\?.*/, '') + '?' + ( w ? 'withjs=' + w + '&': '') + $(frm).serialize()
//frm.find('input:checkbox:not(:checked)').each( function(t,el){ url += '&' + el.name + '=' }) // remember unchecked checkboxes ??
}
/* small library for MW API requests */
var wgServerTime
var apl = new function(){
//initialization
mw.util.addCSS('span.sort {display:none}')
//-------------------API
var queryType =
{rc:'list=recentchanges'
,uc:'list=usercontribs'
,ap:'list=allpages'
,rv:'prop=revisions'
,le:'list=logevents'
}
// Usage: var myQ = new simpleQuery('rc', {limit:20}, myFunc)
// then myQ.call(); ... if (myQ.isMore) myQ.call(50)
this.simpleQuery = function(tp, params, cbfunc){
var shortName = tp // 'rc'
var longName = queryType[shortName].split('=')[1] // 'recentcontribs'
var url = wgScriptPath+'/api.php?action=query&format=json&' + queryType[shortName]
var request = {}, callback = cbfunc, next
for (var k in params) addParam(k, params[k])
this.isMore = false
function addParam(key, val){
if (!/titles|pageids|revids|redirects|indexpageids|requestid/.test(key))
key = shortName + key
request[key] = val
}
this.call = function(params){
if (typeof params == 'number') request[shortName+'limit'] = params
else if (typeof params == 'object') for (var k in params) addParam(k, params[k])
$.getJSON(url, $.extend({}, request, next), recv)
}
this.isMore = function() {
return next
}
var recv = function(data, textStatus, jqXHR){
updateServerTime(jqXHR)
//save continue values
if (next = data['query-continue']) next = next[longName]
if (!data.query) return dispMsg('Server returned empty data')
callback(data.query[longName], data)
}
}
this.updateServerTime = updateServerTime = function(xhr){
if (xhr) wgServerTime = new Date( xhr.getResponseHeader('Date') )
}
this.parseTimestamp = parseTS
this.getChild = function(obj, path){ //example: getSomeChild( data , 'query.pages..somekey' )
path = path.split('.')
for (var i=0; i<path.length; i++){
var key = path[i]
if (key == '') for (key in obj) break //get any child
obj = obj[key]
}
return obj
}
//---------------------------- HTML OUTPUT
this.toggleCSS = (function (){ //example: toggleCSS( 'p{display:none}', true )
var sheet = {}
return function(css, isOn){
if (!sheet[css]) sheet[css] = mw.util.addCSS(css)
sheet[css].disabled = ! isOn
}
})()
this.spinner = function (el){
if (el)
$(el).append('<img class=spinner style="margin-left:1em" src="'
+ stylepath +'/common/images/spinner.gif" alt="..." title="..." />')
else
$('img.spinner').remove()
}
this.checkbox = function (name, id, val, txt){
return '<input type=checkbox name='+name+' id='+id + ' value="'+val+'">'
+ '<label for='+id+'>' + txt + '</label>'
}
this.icon = function(src, size, attr){ //returns <img ...> from Commons
if( size ) src = 'thumb/'+src+'/'+size+'px-'+src.split('/')[2] //+'.png' //for svg: not needed?
return '<img src="/media/wikipedia/commons/'
+ src + '" ' + (attr||'')+'>'
}
this.output = function (val, type){
switch ( type ){
case 'title': return this.outputPage(val, simplifyTitle(val) )
case 'user': return this.outputPage('Special:Contributions/'+val, val)
case 'touched':
case 'timestamp': return inHours( wgServerTime - parseTS(val) )
case 'hours': return inHours(val)
//case 'size': case 'oldlen': case 'newlen':
default:
return val
}
}
this.outputPage = function (page, name){
name = name || page
if (name.length > 40) name = name.substr(0, 37) + '…'
var tip = ''
if (page != name) tip = ' title="'+page.replace(/"/g,'"') + '"'
page = encodeURI(page.replace(/ /g,'_')).replace(/\?/g,'%3F')
// could use encodeURIComponent(page).replace(/%2F/g,'/') ?
return '<a href="' + wgArticlePath.replace('$1', '') + page + '"'
+ tip + '>'+name+'</a>'
}
this.outputCell = function (val, key){
var attr = '', a1 = '', a2 = '', clss = key
if( typeof val == 'object' ){
if( key == 'title' && typeof val.redirect == 'string' ) clss += ' redirect'
val = val[key]
}
if ( key == 'timestamp'){ //handle timestamp
attr = ' title="' + val + '"'
a1 = '<span class=sort>' + val + '</span><span class=hours>'
a2 = '</span>'
}
return '<td class="' + clss + '"' + attr + '>' + a1 + this.output( val, key ) + a2
}
this.makeAPITimestamp = function(d){ //date -> 2008-01-26T06:34:19Z
return d.getUTCFullYear() + '-'
+ pad0(d.getUTCMonth()+1) + '-'
+ pad0(d.getUTCDate()) + 'T'
+ pad0(d.getUTCHours()) + ':'
+ pad0(d.getUTCMinutes()) + ':'
+ pad0(d.getUTCSeconds()) + 'Z'
}
//----------- AUX Functions
function pad0(v, len){ // 6 -> '06'
len = len || 2
v = v.toString()
while (v.length < len) v = '0'+v
return v
}
function diffSize(n){
return '<span class='
+ 'mw-plusminus-'+ (n>0 ? 'pos' : (n<0?'neg':'null'))
+ (n < - 500 ? ' style="font-weight:bold"' : '')
+ '>' + n.toString() + '</span>'
}
function inHours(ms){ //milliseconds -> "2:30" or 5,06 or 21
var mm = Math.floor(ms/60000)
if( !mm ) return '<small>'+Math.floor(ms/1000)+'s</small>'
var hh = Math.floor(mm/60); mm = mm % 60
var dd = Math.floor(hh/24); hh = hh % 24
if (dd) return dd + (dd<10?'<small>,'+pad0(hh)+'</small>':'')
else return '<small>'+hh + ':' + pad0(mm)+'</small>'
}
//20081226220605 or 2008-01-26T06:34:19Z -> date
function parseTS(ts){
var m = ts.replace(/\D/g,'').match(/(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)/)
return new Date ( Date.UTC(m[1], m[2]-1, m[3], m[4], m[5], m[6]) )
}
function simplifyTitle(ttl){
for (var tt in simplerTitles)
if ( ttl.substring (0, tt.length) == tt )
ttl = simplerTitles[ tt ] + ttl.substring(tt.length)
return ttl
}
//---------- Per Project
// enwiki
var simplerTitles =
{'Wikipedia:' :'WP:'
,'Wikipedia talk:':'WT:'
}
}//library