Jump to content

User:Js/tools/combinedContribs.js

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Js (talk | contribs) at 03:56, 13 September 2011 (new script to display IP ranges contribs). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
(diff) ← Previous revision | Latest revision (diff) | Newer revision → (diff)
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&' + $.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,'&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