Jump to content

User:Js/tools/combinedContribs.js

From Wikipedia, the free encyclopedia
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
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=\');" >&nbsp;</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) + '&timestamp=' + 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,'&quot;') + '">✓</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,'&quot;') + '">✓</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&rawcontinue=&' + $.param(req) + '">' 
         + (req.list || req.prop || 'request') + ' ' 
         + (req.ucuser || req.ucuserprefix || '')
         + '</a>')

 pendingRequests++
 $.getJSON( wgScriptPath+'/api.php?action=query&rawcontinue=&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
 return  document.URL.replace(/\?.*/, '') + '?' + $(frm).serialize()
 + ( mw.util.getParamValue('withjs') ? '&withjs=' + mw.util.getParamValue('withjs') : '')
 + ( mw.util.getParamValue('runjs')  ? '&runjs='  + mw.util.getParamValue('runjs')  : '')
 //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&rawcontinue=&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,'&quot;') + '"'
 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