:User:Andy M. Wang/recent2.js

/**

* Lupin's Anti-Vandal Tool, adapted by User:Philip Trueman into PILT.

* (User:Lupin/recent2.js)

* (User:Philip Trueman/recent2.js)

*

* Cloned and modified by en:User:Andy M. Wang in April 2016.

*

* This tool hits the RSS feed for recent changes every 10 seconds or

* so and checks for common vandalism. It does not make a separate server request

* for every edit.

*

* Cloned and modified by en:User:Andy M. Wang in April 2016.

* @author: en:User:Lupin

* @author: Helder (https://github.com/he7d3r)

* @source: en:User:Lupin/recent2.js

*

* Dual license:

* @license CC-BY 3.0

* @license GFDL 1.2 or any later version

*/

/*jshint

camelcase: false, curly: true, eqeqeq: false, immed: true, latedef: true,

newcap: true, noarg: true, noempty: true, nonew: true, quotmark: single,

trailing: true, undef: false, unused: false, bitwise: false, forin: false,

onevar: false,

boss: true, eqnull: true, evil: true, funcscope: true,

laxbreak: true, scripturl: true, shadow: true,

wsh: true, nonstandard: true

  • /

/*global mw, $, wikEdUseWikEd, WikEdUpdateFrame, setupTooltips,

grabRecentChanges, processRecentChangesSingle, processRecentChanges,

feedFailed, newOutputDiv, processRecentChangesDisplay, getFirstTagContent,

nextChangeSoon, diffCellRe, badWords, spellRe, formatTime, maybeStart,

showHideDetailRange, outputDivs, showHideDetail, loopRecentChanges,

saveBundle, vandalColour, linkmaker, spelldict, showSysopEdits, marvin,

addMarvin, AVTAutoEdit, self

  • /

//

recent2={

// Edit these to your liking.

// Make sure there's a comma at the end of each line.

badwordsUrl: 'User:Lupin/badwords',

filterPage: 'User:Lupin/Filter_recent_changes',

allRecentPage: 'User:Lupin/All_recent_changes',

recentIPPage: 'User:Lupin/Recent_IP_edits',

monitorWatchlistPage: 'User:Lupin/Monitor_my_watchlist',

spelldictUrl: 'Wikipedia:Lists_of_common_misspellings/For_machines',

spelldictPage: 'User:Lupin/Live_spellcheck',

safePages: '([Ww]ikipedia:([Ii]ntroduction|[Ss]andbox|[Tt]utorial[^/]*/sandbox)|[Tt]emplate:(X[1-9]|[Ss]andbox))',

linkify: true,

// leave this alone

dummy: null

};

recent2.download=function(bundle) {

// mandatory: bundle.url

// optional: bundle.onSuccess (xmlhttprequest, bundle)

// optional: bundle.onFailure (xmlhttprequest, bundle)

// optional: bundle.otherStuff OK too, passed to onSuccess and onFailure

var x = window.XMLHttpRequest ? new XMLHttpRequest()

: window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP")

: false;

if (x) {

x.onreadystatechange=function() {

x.readyState===4 && recent2.downloadComplete(x,bundle);

};

x.open("GET",bundle.url,true);

x.send(null);

}

return x;

}

recent2.downloadComplete=function(x,bundle) {

x.status===200 && ( bundle.onSuccess && bundle.onSuccess(x,bundle) || true )

|| ( bundle.onFailure && bundle.onFailure(x,bundle) || alert(x.statusText));

};

if (! recent2.outputPosition) { recent2.outputPosition=''; }

window.gettingBadWords=false;

window.badWords=null;

// paths

if ( typeof(mw.config.get('wgServer'))!='string' ||

typeof(mw.config.get('wgArticlePath'))!='string' ||

typeof(mw.config.get('wgScriptPath'))!='string') {

recent2.articlePath= '//' + document.location.hostname + '/wiki/';

recent2.scriptPath= '//' + document.location.hostname + '/w/';

} else {

recent2.articlePath=mw.config.get('wgServer')+mw.config.get('wgArticlePath').replace(/\$1/, '');

recent2.scriptPath=mw.config.get('wgServer')+mw.config.get('wgScriptPath')+'/';

}

var goodeditor = ''; // Dodgy coding this

var goodbundleid; // Dodgy coding this

// add to this list manually

var trustededitors = new Object();

trustededitors['ClueBot NG'] = true;

trustededitors['VoABot II'] = true;

trustededitors['SmackBot'] = true;

trustededitors['Antandrus'] = true;

trustededitors['Gilliam'] = true;

trustededitors['Zzuuzz'] = true;

trustededitors['Rjwilmsi'] = true;

trustededitors['Gogo Dodo'] = true;

trustededitors['Acalamari'] = true;

trustededitors['Nlu'] = true;

trustededitors['Materialscientist'] = true;

trustededitors['Alansohn'] = true;

trustededitors['Rtkat3'] = true;

trustededitors['Widr'] = true;

trustededitors['BethNaught'] = true;

trustededitors['Bongwarrior'] = true;

trustededitors['Kelapstick'] = true;

trustededitors['Smalljim'] = true;

trustededitors['Oshwah'] = true;

trustededitors['GeneralizationsAreBad'] = true;

trustededitors['Qpalzmmzlapq'] = true;

trustededitors['Favonian'] = true;

var newpull = 0;

var lastgot = 50;

var prevgot = 50;

var prevprevgot = 50;

// Note whether certain bots are running.

window.CBrunning=false;

window.VBrunning=false;

var rbwins = new Array();

var sigs = new Array();

var sigindex = 2;

var prerevededits;

var pulled = 0;

var duplicates = 0;

var watched = 0;

var trusted = 0;

var prereverted = 0;

var allreverts = 0;

var myreverts = 0;

var lastsuccessfulreverts = ' ';

recent2.getBadWords=function() {

window.gettingBadWords=true;

recent2.download({ url: recent2.scriptPath + 'index.php?title=' +

recent2.badwordsUrl + '&action=raw&ctype=text/css&max-age=7200', // reload every 2 h

onSuccess: recent2.processBadWords,

onFailure: function () { setTimeout(recent2.getBadWords, 15000); return true;}});

}

window.diffCellRe=/\+<\/td>\s*]*>\s*

\s*(.*?)\s*<\/div>\s*<\/td>/gi;

// processBadWords: generate the badWords RegExp from

// the downloaded data.

// d is the xmlhttprequest object from the download

recent2.processBadWords=function(d) {

var data=d.responseText.split('\n');

var phrase=[];

var string=[];

for (var i=0; i

var s=data[i];

// ignore empty lines, whitespace-only lines and lines starting with '<'

if (/^\s*$|^

// lines beginning and ending with a (back-)slash (and possibly trailing

// whitespace) are treated as regexps

if (/^([\\\/]).*\1\s*$/.test(s)) {

var isPhrase=(s.charAt(0)=='/');

// remove slashes and trailing whitespace

s=s.replace(/^([\\\/])|([\\\/]\s*$)/g, '');

// escape opening parens: ( -> (?:

s=s.replace(/\(?!\?/g, '(?:');

// check that s represents a valid regexp

try { var r=new RegExp(s); }

catch (err) {

var errDiv=newOutputDiv('recent2_error', recent2.outputPosition);

errDiv.innerHTML='Warning: ignoring odd-looking regexp on line '+i

+' of badwords:

' + s + '
';

continue;

}

if (isPhrase) phrase.push(s); else string.push(s);

} else {

// treat this line as a non-regexp and escape it.

phrase.push( mw.RegExp.escape(s) );

}

}

// 123 3 2|4 4|5 56 67 71

// ((( repeated char ) )|( ... | strings | ... )|( border )( ... | phrases | ... )( border ))

window.badWords=new RegExp('((([^\\-\\|\\{\\}\\].\\s\'=wI:*#0-9a-f])\\3{2,})|(' + string.join('|') + ')|(^|[^/\\w])(' + phrase.join('|') + ')(?![/\\w]))', 'gi');

};

window.gettingWatchlist=false;

recent2.watchlist=null;

recent2.getWatchlist=function() {

window.gettingWatchlist=true;

recent2.download({url: recent2.articlePath + 'Special:Watchlist/edit',

onSuccess: recent2.processWatchlist,

onFailure: function () { setTimeout(getWatchlist, 15000); return true; }});

};

recent2.processWatchlist=function(req, bundle) {

var watchlist={};

var lines=req.responseText.split('\n');

for (var i=0; i

if (lines[i].indexOf('

  • watchlist[article]=true;

    }

    }

    window.watchlist=watchlist;

    };

    window.gettingSpelldict=false;

    window.spelldict=null;

    recent2.getSpelldict=function() {

    window.gettingSpelldict=true;

    recent2.download({url: recent2.scriptPath + 'index.php?title=' + recent2.spelldictUrl + '&action=raw&ctype=text/css',

    onSuccess: recent2.processSpelldict,

    onFailure: function () { setTimeout(getSpelldict, 15000); return true; }});

    };

    recent2.processSpelldict=function(req, bundle) {

    var spelldict={};

    var lines=req.responseText.split('\n');

    var a=[];

    for (var i=0; i

    var split=lines[i].split('->');

    if (split.length<2) { continue; }

    split[1]=split.slice(1).join('->').split(/, */);

    split[0]=split[0].toLowerCase().replace(/^\s*/, '');

    spelldict[split[0]]=split[1];

    a.push(split[0]);

    }

    window.spelldict=spelldict;

    window.spellRe=RegExp('\\b(' + a.join('|') + ')\\b', 'i');

    };

    ilimit=50;

    window.newOutputDiv=function(klass, position, immortal) {

    var h1=document.getElementsByTagName('h1')[0];

    var ret=document.createElement('div');

    if (klass) { ret.className=klass; }

    if (!position) { position='bottom'; }

    switch(position) {

    case 'top':

    h1.parentNode.insertBefore(ret, h1.nextSibling);

    break;

    case 'bottom':

    h1.parentNode.appendChild(ret);

    break;

    default:

    if (!newOutputDiv.alerted) {

    alert('Unknown position '+position+' in recent2.js, newOutputDiv');

    window.newOutputDiv.alerted=true;

    }

    return newOutputDiv(klass, 'bottom');

    }

    if (!immortal) { ret.id=newOutputDiv.uid++; }

    window.outputDivs.push(ret);

    return ret;

    };

    window.newOutputDiv.alerted=false;

    window.newOutputDiv.uid=0;

    window.outputDivs=[];

    window.grabRecentChanges=function(feed) {

    if (! window.badWords && recent2.filter_badwords ) {

    if ( ! window.gettingBadWords ) { recent2.getBadWords(); }

    return setTimeout(function(){grabRecentChanges(feed);}, 500);

    }

    if (! window.watchlist && recent2.filter_watchlist) {

    if (! window.gettingWatchlist ) recent2.getWatchlist();

    return setTimeout(function(){grabRecentChanges(feed);}, 500);

    }

    if (! window.spelldict && recent2.filter_spelling) {

    if (! window.gettingSpelldict) recent2.getSpelldict();

    return setTimeout(function(){grabRecentChanges(feed);}, 500);

    }

    var pos=recent2.outputPosition;

    if (pos=='top') {

    var output=newOutputDiv('recent2.lines', pos);

    var status=newOutputDiv('recent2.status', pos);

    } else {

    var status=newOutputDiv('recent2.status', pos);

    var output=newOutputDiv('recent2.lines', pos);

    }

    status.style.borderStyle='solid';

    status.style.borderColor='orange';

    status.innerHTML=greyFont+'(' + recent2.count + ') updating...';

    // this abort stuff doesn't work properly for some reason...

    //recent2.lastFeedDownload && recent2.lastFeedDownload.abort(); // } catch (summatNasty) { /* do nothing */ }

    recent2.lastFeedDownload=recent2.download({url: feed,

    onSuccess: processRecentChanges,

    output: output, status: status, onFailure: feedFailed});

    };

    var greyFont='';

    window.feedFailed=function(x,bundle) {

    try { bundle.status.innerHTML+=greyFont+'failed: '+x.statusText + ''; }

    catch (err) { bundle.status.innerHTML+=greyFont+'failed badly: '+err+''; }

    return true;

    };

    recent2.newWindows=true;

    window.linkmaker=function(url, text) {

    var s='

    recent2.newWindows && (s += ' target="_blank"');

    s += '>' + text + '';

    return s;

    };

    recent2.pageblankRegex=RegExp('Blanked the page');

    recent2.pagereplaceRegex=RegExp('Replaced.*content with');

    recent2.revertedRegex=RegExp('Revert');

    recent2.awbRegex=RegExp('(AWB|[Aa]dvisor.js|Reflinks|dashes)');

    recent2.disambigRegex=RegExp('(^Disambiguat|^Categorization |^Stub-sorting |Repairing link to disambiguation page )');

    recent2.partialrollbackRegex=RegExp('evert.*by[^\\.0-9]*([0-9][\\.0-9]*)[^\\.0-9]*to.*by[^\\.0-9]*([0-9][\\.0-9]*)[^\\.0-9]');

    recent2.undoboteditRegex=RegExp('Undid revision.*([a-zA-Z]*Bot).*to');

    recent2.ipUserRegex=RegExp('(User:)?((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}' +

    '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])');

    recent2.outputSeparator='


    ';

    recent2.delayedLines={};

    recent2.delay=0;

    recent2.templateNamespace = 'Template';

    recent2.namespaces={'Media':1, "Special":1, "User":1, "User talk":1, "Wikipedia":1,

    "Wikipedia talk":1, "Image":1, "Image talk":1, "MediaWiki":1,

    "MediaWiki talk":1, "Template":1, "Template talk":1, "Help":1,

    "Help talk":1, "Category":1, "Category talk":1, "Portal":1, "Portal talk":1};

    window.processRecentChanges=function(req, bundle){

    recent2.initialId=processRecentChanges.id;

    recent2.latest=processRecentChanges.lastDate;

    var doc;

    if (doc=req.responseXML.documentElement) {

    if (recent2.items=doc.getElementsByTagName('item')) {

    if ((recent2.itemsCurrent=recent2.items.length) > 0) {

    if (sigindex === 1) { sigs.shift(); sigs[1] = new Object();}

    else if (sigindex === 0) { sigindex = 1; sigs[1] = new Object();}

    else { sigindex = 0; sigs[0] = new Object();}

    prerevededits = new Object();

    pulled = 0;

    duplicates = 0;

    watched = 0;

    prereverted = 0;

    trusted = 0;

    allreverts = 0;

    myreverts = 0;

    lastsuccessfulreverts = ' ';

    recent2.bundleRef = bundle;

    recent2.itemsTotal = recent2.itemsCurrent;

    recent2.itemsCurrent = 0;

    processRecentChangesSingle(); // start processing one diff every 50 ms

    return;

    }

    }

    }

    processRecentChangesDisplay(bundle);

    return;

    }

    recent2.safePagesRe=new RegExp('^' + recent2.safePages + '$');

    recent2.changeDelay=50; // delay between processing each diff, in ms

    window.nextChangeSoon=function(rightNow) {

    setTimeout(processRecentChangesSingle, rightNow ? 0 : recent2.changeDelay);

    };

    // process single diff items delayed by a short timespan

    window.processRecentChangesSingle=function(){

    var i = recent2.itemsCurrent;

    var items = recent2.items;

    recent2.itemsCurrent++;

    if (recent2.itemsCurrent > recent2.itemsTotal) { processRecentChangesDisplay(recent2.bundleRef); return; }

    pulled++;

    var timestamp = Date.parse(getFirstTagContent(items[i],'pubDate'));

    // if (timestamp <= processRecentChanges.lastDate) { nextChangeSoon(true); return; }

    recent2.latest = (timestamp > recent2.latest) ? timestamp : recent2.latest;

    var diffText=getFirstTagContent(items[i],'description').split('').join('\n');

    var editSummary=diffText.replace( /^

    (.*?)<\/p>[\s\S]*/, '$1');

    var editor=getFirstTagContent(items[i], 'creator') || getFirstTagContent(items[i], 'dc:creator');

    // NB article is the link attribute - a fully qualified URL

    var article=getFirstTagContent(items[i], 'link');

    if (recent2.delayedLines[article] && recent2.delayedLines[article].editor != editor) {

    delete recent2.delayedLines[article];

    }

    // articleTitle is the wgTitle thingy with spaces and all that

    var articleTitle=getFirstTagContent(items[i], 'title');

    //console.info('articleTitle=%s', articleTitle);

    // Here we completely recast the logic for eliminating edits we have already seen. Instead of relying on

    // timestamps, we keep a record of the signatures of each edit, and don't show edits whose signatures we

    // saw in the last bundle

    var sig = timestamp.toString() + editor + articleTitle;

    sigs[sigindex][sig] = true;

    if ((sigindex === 1) && (sigs[0][sig] == true)) { duplicates++; nextChangeSoon(true); return; }

    // alert(sig); // Debugging only

    if (recent2.ignore_safe_pages && recent2.safePagesRe.test(articleTitle)) {

    //console.warn('Ignoring safe page %s', article);

    nextChangeSoon(true); return;

    }

    if (recent2.hideNonArticles) {

    var namespace=articleTitle.replace(/:.*/, '');

    if (recent2.namespaces[namespace] &&

    ( ( recent2.showTemplates && namespace != recent2.templateNamespace ) ||

    ! recent2.showTemplates )) {

    nextChangeSoon(true); return;

    }

    }

    // perhaps ignore talk pages

    if (! recent2.show_talkpages && articleTitle

    && /^Talk:|^[^:]*?[_ ]talk:/.test(articleTitle)) {

    nextChangeSoon(true); return;

    }

    // perhaps restrict to watchlist articles

    if (recent2.filter_watchlist && articleTitle &&

    ! window.watchlist[articleTitle.replace(/^Talk:/, '').replace(/[ _]talk:/, ':')]) {

    nextChangeSoon(true); return;

    }

    watched++;

    // Highly experimental - skip all edits before a revert of that article

    if (prerevededits[articleTitle]!=null && timestamp < prerevededits[articleTitle]) { if (mw.config.get('wgUserName')==editor) {alert('Skipping '+articleTitle); } prereverted++; nextChangeSoon(true); return; }

    // Track reverts of watched changes

    if (recent2.revertedRegex.test(editSummary))

    {

    prerevededits[articleTitle] = timestamp;

    allreverts++;

    if (mw.config.get('wgUserName')==editor)

    {

    myreverts++;

    lastsuccessfulreverts = lastsuccessfulreverts+articleTitle+' ';

    }

    }

    // Mod here - we don't want to skip partial rollbacks by anyone

    if (!recent2.ipUserRegex.test(editor) && !recent2.partialrollbackRegex.test(editSummary))

    {

    if (recent2.filter_anonsOnly) { nextChangeSoon(true); return; }

    if (recent2.ignore_my_edits && mw.config.get('wgUserName')==editor) { nextChangeSoon(true); return; }

    // TODO: Make this a number that we can count down so that we can tell when CB stops running

    if (editor=='ClueBot') { window.CBrunning=true; }

    if (editor=='VoABot II') { window.VBrunning=true; }

    // Modification here. Skip edits by certain trusted editors. This is a crude way to do it,

    // but it seems to work. Philip Trueman 11May2007

    // Reworked to use an associate array 9Oct2010

    if ((editor != mw.config.get('wgUserName')) && (trustededitors[editor] == true)) {

    if (mw.config.get('wgUserName') == 'PhilT2') alert('Skipping edit by ' + editor);

    trusted++; nextChangeSoon(true); return;

    }

    // This one is a little riskier. If the edit summary contained the word 'Revert' (NOT 'Undid')

    // will ignore the edit. TODO: Have a better test for whether the reversion was done with

    // an advanced tool such as TWINKLE or VandalProof

    //if ((recent2.revertedRegex.test(editSummary)) && !window.vandals[editor]) { trusted++; nextChangeSoon(true); return; }

    // This one is also a little risky.

    //if ((recent2.awbRegex.test(editSummary)) && !window.vandals[editor]) { trusted++; nextChangeSoon(true); return; }

    // This one is also a little risky.

    //if ((recent2.disambigRegex.test(editSummary)) && !window.vandals[editor]) { trusted++; nextChangeSoon(true); return; }

    }

    // If the bots are running we will ignore all edits whose edit summaries contain the

    // words 'Blanked the page' or 'Replaced page with'. This is partly because they are faster

    // than most humans, and partly because these edits tend to generate large diffs which take

    // longer to scroll down through.

    if (window.CBrunning===true && recent2.pageblankRegex.test(editSummary)) { nextChangeSoon(true); return; }

    if (window.CBrunning===true && recent2.pagereplaceRegex.test(editSummary)) { nextChangeSoon(true); return; }

    if (window.VBrunning===true && recent2.pageblankRegex.test(editSummary)) { nextChangeSoon(true); return; }

    if (window.VBrunning===true && recent2.pagereplaceRegex.test(editSummary)) { nextChangeSoon(true); return; }

    // filter against badwords regexp

    if (recent2.filter_badwords) {

    var badMatch=null;

    var diffCell=null;

    var previousVandal= window.vandals[editor];

    var matchesRe='';

    var matchesPlain='';

    diffCellRe.lastIndex=0;

    // An edit summary that is the article title is often a sign of a bad edit

    var badEditSummary= (articleTitle==editSummary);

    // Highly experimental - 1

    // The idea of this one is that a bot or anti-vandalism tool that reverts an IP to a previous version by

    // a similar IP may have missed some dynamic IP vandalism

    var partRevert= recent2.partialrollbackRegex.exec(editSummary);

    if (partRevert)

    {

    var a = partRevert[1].split('.');

    var b = partRevert[2].split('.');

    if ((a[0]==b[0]) && (a[1]==b[1]))

    {

    badEditSummary=true;

    }

    }

    // Highly experimental - 2

    // The idea of this one is an Undo of an edit by a Bot is likely to be bad

    var botUndo= recent2.undoboteditRegex.test(editSummary);

    if (botUndo)

    {

    badEditSummary=true;

    }

    // Why do the test if the editor is a known vandal?

    if (!previousVandal && !badEditSummary) {

    while (diffCell=diffCellRe.exec(diffText)) {

    // get content of addition table cells, faster than direct fulltext search

    badWords.lastIndex=0;

    // .test() is meant to be faster than a full match

    if (badMatch=badWords.test(diffCell[1])) { break; }

    }

    }

    if (badMatch===true || previousVandal || badEditSummary) {

    badWords.lastIndex=0;

    var reMatch;

    while (diffCell && (reMatch=badWords.exec(diffCell[1]))) {

    var badWord=reMatch[2] || reMatch[4] || reMatch[6] || '';

    if (articleTitle.toLowerCase().indexOf(badWord.toLowerCase())<0) { // avoid legit article title occurrences

    badWord=badWord.replace(/^\s+|\s+$/g, '');

    if (badWord!='') {

    matchesPlain+=badWord+', ';

    badWord=badWord.replace(/([^\w ])/g, '\\$1');

    matchesRe+=badWord+'|';

    }

    }

    }

    matchesRe=matchesRe.replace(/\|$/, '');

    if (!previousVandal && !badEditSummary && matchesRe=='') { nextChangeSoon(); return; }

    matchesPlain=matchesPlain.replace(/, $/, '');

    // highlighting

    var highlighted=diffCell && diffCell[1];

    if (matchesRe) {

    highlighted=highlighted.replace(RegExp('('+matchesRe+')', 'g'),

    '$1');

    }

    // linkify

    highlighted=recent2.doLinkify(highlighted);

    diffText=recent2.doLinkify(diffText);

    if (previousVandal) {

    matchesPlain = '[Previously rolled back this editor] ' + matchesPlain;

    }

    if (badEditSummary) {

    matchesPlain = '[Suspicious edit summary] ' + matchesPlain;

    }

    recent2.delayedLines[article]={

    timestamp: timestamp, article:article, count:recent2.count, articleTitle:articleTitle,

    editor:editor, badWord:matchesPlain, badDiffFragment:highlighted, diff:diffText, summary:editSummary

    };

    }

    } else if (recent2.filter_spelling) {

    var splMatch=null;

    while (diffCell=diffCellRe.exec(diffText)) {

    if (splMatch=spellRe.test(diffCell[1])) { break; }

    }

    if (splMatch) {

    splMatch = diffCell[1].match(spellRe);

    var misspelling = splMatch[1]; //.replace(/^\s*/, '');

    var badWord = '

    '","'+misspelling.split("'").join("%27")+'")\'>'+ misspelling + '';

    diffText = diffText.replace(RegExp('('+misspelling+')', 'gi'),

    '$1');

    // linkify

    diffText=recent2.doLinkify(diffText);

    recent2.delayedLines[article] = {

    timestamp: timestamp, article:article, count:recent2.count, articleTitle:articleTitle,

    editor:editor, badWord:badWord, badDiffFragment:'', diff:diffText, summary: editSummary

    };

    }

    } else {

    var article=getFirstTagContent(items[i], 'link');

    var articleTitle=getFirstTagContent(items[i], 'title');

    if (recent2.CustomFilter &&

    ! recent2.CustomFilter({timestamp:timestamp, article:article, articleTitle:articleTitle,

    editor:editor, diff:diffText, summary:editSummary})) { nextChangeSoon(); return; }

    // linkify

    diffText=recent2.doLinkify(diffText);

    recent2.delayedLines[article]={

    timestamp: timestamp, article:article, count:recent2.count, articleTitle:articleTitle,

    editor:editor, diff:diffText, summary:editSummary

    };

    }

    // schedule next iteration

    nextChangeSoon();

    return;

    }

    window.processRecentChangesDisplay=function(bundle){

    var output=recent2.getDelayedLineOutput();

    //console.log(output);

    var outputString='';

    if (recent2.outputPosition=='top') {

    outputString=output.join(recent2.outputSeparator);

    }

    else {

    for (var i=output.length-1; i>=0; --i) {

    outputString+=output[i] + (i>0 ? recent2.outputSeparator : '') ;

    }

    }

    bundle.output.innerHTML+=outputString;

    if (recent2.wait_for_output) { recent2.pauseOutput(); }

    setTimeout(function() {recent2.doPopups(bundle.output)}, 300);

    // overlap better than missing some out, i think; FIXME do this properly

    processRecentChanges.lastDate=recent2.latest; // - 1;

    // Very experimental - dynamic pull sizes

    // The idea here is to estimate the current update rate, assume a Poisson distribution, and pull three standard deviations

    // more than that

    newpull = (pulled - duplicates + lastgot + prevgot + prevprevgot) / 4;

    newpull += 3 * (Math.sqrt(newpull));

    if ((newpull < 51) && (newpull > 10)) ilimit = Math.floor(newpull);

    if (newpull >= 51) ilimit = 50;

    if (newpull <= 10) ilimit = 10;

    if (duplicates === 0) ilimit = 50;

    prevprevgot = prevgot;

    prevgot = lastgot;

    lastgot = pulled - duplicates;

    var statusTail=greyFont+'done up to ' + formatTime(recent2.latest) + ' ' + pulled + '/' + duplicates + '/' + watched + '/' + prereverted + '/' + trusted + ' ' + myreverts + '/' + allreverts + ' ' + lastsuccessfulreverts + '';

    if (processRecentChanges.id > recent2.initialId) {

    statusTail+=' toggle these details |';

    if (recent2.autoexpand) {

    setTimeout( function() {

    /* document.title=initialId+' '+processRecentChanges.id; */

    showHideDetailRange(recent2.initialId, processRecentChanges.id); }, 250 );

    }

    }

    statusTail += ' remove earlier output';

    if (recent2.wait_for_output) {

    statusTail += ' | show new output';

    }

    statusTail+='
    ';

    bundle.status.innerHTML+=statusTail;

    return;

    }

    // linkify and popupsify wikilinks

    recent2.doLinkify=function(txt) {

    if (!txt || !recent2.linkify) { return txt; }

    var inheritColor='color:inherit;color:expression(parentElement.currentStyle.color)';

    var externalLinkStyle='text-decoration:none;';

    var internalLinkStyle='text-decoration:none;';

    externalLinkStyle=internalLinkStyle='text-decoration:none;border-bottom: 1px dotted;';

    txt=txt.replace(/((https?|ftp):(\/\/[^\[\]\{\}\(\)<>\s&=\?#%]+|<[^>]*>)+)/g, function (p,p1) {

    p1=p1.replace(/<[^>]*>/g, '');

    var url=encodeURI(p1);

    url=url.replace(/\"/g, '%22');

    url=url.replace(/\'/g, '%27');

    url=url.replace(/#/g, '%23');

    var ti=p1.replace(/\"/g, '"');

    return(''+p+'');

    });

    // BUG: doLinkify('123 badword blah blah')

    // gives '123 badword blah blah'

    // and the browser closes the inside the , so the badword is not red!

    txt=txt.replace(/((\[\[)([^\|\[\]\{\}\n]*)([^\]\n]*)(\]\]))/g, function (p,p1,p2,p3) {

    p3=p3.replace(/<[^>]*>/g, '');

    var url=encodeURI(p3);

    url=url.replace(/\"/g, '%22');

    url=url.replace(/\'/g, '%27');

    url=url.replace(/#/g, '%23');

    url=recent2.articlePath+url;

    var ti=p3.replace(/\"/g, '"');

    return(''+p+'');

    });

    return ('' + txt + '');

    };

    processRecentChanges.lastDate=0;

    processRecentChanges.id=0;

    recent2.getDelayedLineOutput=function() {

    var ret=[];

    var id=processRecentChanges.id;

    for (var a in recent2.delayedLines) {

    if (recent2.delayedLines[a] && typeof recent2.delayedLines[a].count == typeof 1 &&

    recent2.count - recent2.delayedLines[a].count >= recent2.delay) {

    recent2.delayedLines[a].id=id++;

    var line=(recent2.doLine(recent2.delayedLines[a]));

    if (line) { ret.push(line); }

    delete recent2.delayedLines[a];

    }

    }

    processRecentChanges.id=id;

    return ret;

    }

    window.deleteEarlierOutputDivs=function(cur) {

    for(var i=0; i

    if (!outputDivs[i] || !outputDivs[i].id) continue;

    if (outputDivs[i].id >= 0 && outputDivs[i].id < cur) {

    // FIXME BUG: if we go from the bottom up, then we'll delete one too many or too few, or something :-)

    outputDivs[i].parentNode.removeChild(outputDivs[i]);

    outputDivs[i]=null;

    }

    }

    // scroll to the top if we're appending output to the bottom, to keep the div we've clicked visible after the deletions

    if (recent2.outputPosition!='top') document.location='#';

    }

    window.showHideDetailRange=function(start,end) {

    // use the first div to see if we should show or hide

    var div=document.getElementById('diff_div_' + start);

    if (!div) {alert('no such div: diff_div_' + start); return; }

    var state=false; // hide

    if (div.style.display=='none') state=true; // show

    for (var i=start; i

    showHideDetail(i, true, state);

    }

    }

    window.toggleSysopEdits=function() {

    var divs=document.getElementsByTagName('div');

    for (var i=0; i

    if (divs[i].className=='sysop_edit_line') divs[i].style.display= ( toggleSysopEdits.hidden ? 'none' : 'inline' );

    }

    toggleSysopEdits.hidden = ! toggleSysopEdits.hidden;

    }

    window.bundles={};

    window.vandalColour = function(vandal) {

    var num=window.vandals[vandal];

    if (!num) return '';

    switch (num) {

    case 1: return '#DDFFDD';

    case 2: return '#BBFFBB';

    }

    var i= 9-(num - 3) *2;

    if (i < 0) i=0;

    return '#' + i + i + 'FF' + i + i;

    }

    window.clickDetails=function(action, max) {

    if(!action) action='show';

    if (!max) max = document.links.length;

    var count=0;

    for (var i=0; i

    if(document.links[i].innerHTML==action + ' details' && document.links[i].href.indexOf('javascript:') == 0) {

    ++count;

    eval(document.links[i].href.replace('javascript:', ''));

    }

    }

    }

    recent2.pendingLines=[];

    recent2.unpauseOutputOnce=function() {

    //console.log('unpausing once');

    if (recent2.pausedOutput) {

    recent2.togglePausedOutput();

    recent2.togglePausedOutput();

    }

    };

    recent2.pauseOutput=function() {

    //console.log('pausing');

    if (!recent2.pausedOutput) { recent2.togglePausedOutput(); }

    //console.log(recent2.pausedOutput);

    }

    recent2.unpauseOutput=function() {

    //console.log('unpausing');

    if (recent2.pausedOutput) { recent2.togglePausedOutput(); }

    //console.log(recent2.pausedOutput);

    }

    recent2.togglePausedOutput=function() {

    if (!recent2.pausedOutput) { recent2.pausedOutput = true; return true; }

    else recent2.pausedOutput=false;

    var outputBuffer='';

    while (recent2.pendingLines.length) {

    outputBuffer+=recent2.doLine(recent2.pendingLines.pop());

    if (recent2.pendingLines.length) { outputBuffer+=recent2.outputSeparator; }

    }

    var pos=recent2.outputPosition;

    var output=newOutputDiv('recent2.lines', pos);

    output.innerHTML=outputBuffer;

    setTimeout(function() {recent2.doPopups(output)}, 300);

    return false;

    }

    recent2.togglePaused=function() {

    if(!recent2.paused) { recent2.paused=true; return true; }

    recent2.paused=false;

    loopRecentChanges(loopRecentChanges.iterations);

    return false;

    }

    recent2.doLine=function(bundle) {

    if (recent2.pausedOutput) {

    recent2.pendingLines.push(bundle);

    return '';

    }

    //if (recent2.filter_spelling) { return recent2.doSpellLine(bundle); }

    var sysop = null;

    if (typeof sysops != 'undefined') sysop=sysops.test(bundle.editor);

    //alert(bundle.article);

    var art = bundle.article.split('&')

    //alert(art[0]);

    bundle.article=art[0];

    var lastDiffPage=bundle.article + '&diff=cur&oldid=prev';

    bundle.url=lastDiffPage;

    saveBundle(bundle);

    var div='';

    if (window.vandals[bundle.editor]) { if (window.vandals[bundle.editor] > 0) { div='

    '} }

    else if (sysop) {div='

    '};

    return div +

    '

  • ' +

    'view' +

    ' · rollback · ' +

    '' + formatTime(bundle.timestamp) + ' · ' +

    ' (' + linkmaker(bundle.article+'&limit=20&action=history', 'hist') + ') · ' +

    linkmaker(bundle.article, bundle.articleTitle) +

    ( bundle.badWord ? ' matched ' + bundle.badWord + '' : '') + '
    ' +

    '' +

    linkmaker(recent2.articlePath + 'User:' + encodeURIComponent(bundle.editor), bundle.editor) +

    ' (' +

    'ignore' + ' | ' +

    linkmaker(recent2.articlePath + 'User_talk:' + encodeURIComponent(bundle.editor), 'talk') + ' · ' +

    linkmaker(recent2.articlePath + 'User_talk:' + encodeURIComponent(bundle.editor)+'?limit=20&action=history', 'talkhist') + ' · ' +

    linkmaker(recent2.articlePath + 'Special:Contributions/' + encodeURIComponent(bundle.editor), 'contribs') + ' · ' +

    linkmaker(recent2.articlePath + 'Special:Blockip/' + encodeURIComponent(bundle.editor), 'block') + ') · ' +

    ( bundle.summary ? '('+bundle.summary+')' : '') +

    '

  • ' +

    ( div ? '

    ' : '') ;

    };

    recent2.correctSpelling=function (article, badword) {

    var url=recent2.articlePath + article + '?action=edit&autoclick=wpDiff&autominor=true';

    var wl=badword.toLowerCase();

    var cor=spelldict[wl];

    if (!cor|| !cor.length) { alert('Could not find an entry for ' + wl); return; }

    if (cor.length > 1) {

    var q='Which correction should I use?\nPlease either type a number or another correction.\n';

    for (var i=0; i

    var ans=prompt(q);

    if (!ans) {return;}

    var num=parseInt(ans, 10);

    if (num > -1 && num < cor.length) { cor = cor[num]; }

    else { cor = ans; }

    } else {

    cor = cor[0];

    }

    cor=cor.replace(/^ *| *$/g, '');

    url += '&avtautosummary=Correcting%20spelling:%20' + wl + '->' + cor;

    url += '&avtautoedit=';

    c0=cor.charAt(0);

    wl0 = wl.charAt(0);

    b='\\b';

    url += ['s', b + wl + b, cor, 'g;'].join('#');

    wl=wl0.toUpperCase() + wl.substring(1);

    cor=c0.toUpperCase() + cor.substring(1);

    url += ['s', b + wl + b, cor, 'g;'].join('#');

    wl=wl.toUpperCase();

    cor=cor.toUpperCase();

    url += ['s', b + wl + b, cor, 'g;'].join('#');

    window.open(url);

    };

    window.saveBundle= function(bundle) {

    var z={};

    for (var prop in bundle) { z[prop]=bundle[prop]; }

    window.bundles[bundle.id]=z;

    }

    window.vandals={};

    window.tryRollback=function(id) {

    if (recent2.non_admin_rollback) { recent2.tryNonAdminRollback(id); }

    else { recent2.tryFastAdminRollback(id); }

    // PT addition

    showHideDetail(id, true, false);

    // TODO: get the div of the next displayed diff and invoke scrollIntoView on it

    };

    recent2.getBundleVandal=function(id) {

    var b=window.bundles[id];

    if (!b) {

    alert('No bundle! Please tell Lupin how to reproduce this error - it should not really happen.');

    return null;

    }

    var vandal=b.editor;

    if (window.vandals[vandal]==null) { window.vandals[vandal]=1; }

    else { window.vandals[vandal]++; }

    return b;

    }

    recent2.tryAdminRollback=function(id){

    var b=recent2.getBundleVandal(id);

    if (!b) { return; }

    var vandal=b.editor;

    var onSuccess=function (x, bundle) {

    var rollRe=RegExp('(.*?))?');

    // match[0]: useless

    // match[1]: url (escaped)

    // match[2]: last editor (escaped)

    // match[4]: last edit summary (wikiText - FIXME strip this to plain text)

    var match=rollRe.exec(x.responseText);

    if (!match) {

    alert('No rollback link found.' +

    '\nMaybe you should try the non-admin rollback by checking the checkbox above?\n' +

    'Alternatively, this may be a bug.');

    return;

    }

    var lastEditor=match[2].split('+').join(' ');

    var lastSummary=match[4] || '';

    // var vandal=b.editor; // from the closure

    if (lastEditor != vandal) {

    var summary=lastSummary.replace(RegExp('<[^>]*?>','g'),'');

    if (!summary) summary=lastSummary;

    this.focus();

    alert( 'Could not rollback - someone else has edited since the vandal.\n\nPage: '+ b.articleTitle +

    '\nVandal: '+vandal+'\nLast editor: '+lastEditor+'\nEdit summary: '+summary);

    return;

    }

    var rollbackUrl=match[1].split('&').join('&');

    // confirm('Rollback edits by '+vandal + ' to '+b.articleTitle+'?') &&

    // window.open(rollbackUrl, '_blank');

    var newWin = window.open(rollbackUrl, '_blank');

    // Send the new window to the back

    newWin.blur();

    // Record this window

    rbwins[rbwins.length] = newWin;

    // Limit the number of open windows on a first-in first-out basis

    if (rbwins.length > 10)

    {

    if (!rbwins[0].closed)

    rbwins[0].close();

    rbwins.shift();

    }

    }

    var onFailure = function(x,bundle) {

    alert('HTTP failed when trying to get rollback link in url\n' + bundle.url +

    '\n\nHTTP status text: ' + x.statusText);

    return true;

    }

    recent2.download({ url:b.url, onSuccess: onSuccess, id: b.id, onFailure:onFailure});

    };

    recent2.tryNonAdminRollback=function(id) {

    if (typeof(avtautoEdit)=='undefined') {

    alert('You need to have AVTautoedit functionality for non-admin rollback.\n\n' +

    'This is included in Navigation popups - see WP:POP.\n\n'+

    'Alternatively, you can try adding '+

    '{'+'{subst:js|User:Lupin/autoedit.js}} ' +

    'to your user javascript file.'); return;

    }

    var b=recent2.getBundleVandal(id);

    if (!b) { return; }

    var vandal=b.editor;

    var url=recent2.scriptPath + 'api.php?action=query&format=json&titles=' + encodeURIComponent(b.articleTitle) + '&prop=revisions&rvlimit=30';

    var onSuccess=function(x,y){ recent2.processHistoryQuery(x,y,b); }

    recent2.download({ url: url, onSuccess: onSuccess, id: b.id}); // fixme: onFailure

    };

    recent2.processHistoryQuery=function(x,downloadBundle, bundle) {

    var json=x.responseText;

    try {

    eval('var o='+json);

    var edits=recent2.anyChild(o.query.pages).revisions;

    }

    catch ( someError ) { alert('JSON business failed.\n\n' + json.substring(0,200)

    + '\n\nCannot rollback.'); return; }

    var i;

    for (i=0; i

    if (edits[i]['user']!=bundle.editor)

    {

    /* The plan here is to see if the previous editor is an IP close to the vandal's IP */

    /* To start with we will just warn the user rather than roll back further */

    if ((recent2.ipUserRegex.test(edits[i]['user'])) && (recent2.ipUserRegex.test(bundle.editor)))

    {

    var a = edits[i]['user'].split('.');

    var b = bundle.editor.split('.');

    if ((a[0]==b[0]) && (a[1]==b[1]))

    {

    alert( 'Possible chameleon IP vandal! Rollback of ' + bundle.articleTitle + ' aborted!\n\n'); return;

    }

    else

    break;

    }

    else

    break;

    }

    } // end loop searching for identity of previous editor

    if (i===0) {

    alert( 'Could not rollback - someone else has edited since the vandal.\n\nPage: ' +

    bundle.articleTitle +

    '\nVandal: '+bundle.editor+'\nLast editor: '+edits[0]['user']+

    '\nEdit summary: '+edits[0]['comment']);

    return;

    }

    if (window.vandals[edits[i]['user']])

    {

    alert('Would roll back to previous vandal! Rollback of ' + bundle.articleTitle + ' aborted!\n\n'); return;

    }

    if (i==edits.length) {

    alert(bundle.editor + ' seems to be the only editor to ' + bundle.articleTitle +

    '.\n\nRollback aborted.'); return;

    }

    var prevEditor=edits[i]['user'];

    var prevRev=edits[i]['revid'];

    var edCount=i.toString() + ((i===1) ? ' edit' : ' edits');

    var summary='Reverted ' + edCount + ' by [[Special:Contributions/' + escape(bundle.editor) + '|' +

    escape(bundle.editor) + ']] to last version by ' + escape(prevEditor);

    summary=summary.split(' ').join('%20');

    var art = bundle.article.split('&')

    //alert(art[0]);

    bundle.article=art[0];

    var url=bundle.article + '&action=edit&avtautosummary=' + summary + '&oldid=' + prevRev +

    '&autoclick=wpSave&actoken=' + autoClickToken() + '&autominor=true';

    var newWin = window.open(url, '_blank');

    // Send the new window to the back

    newWin.blur();

    // Record this window

    rbwins[rbwins.length] = newWin;

    // Limit the number of open windows on a first-in first-out basis

    if (rbwins.length > 10)

    {

    if (!rbwins[0].closed)

    rbwins[0].close();

    rbwins.shift();

    }

    };

    //recent2.non_admin_rollback=true;

    recent2.tryFastAdminRollback=function(id) {

    var b=recent2.getBundleVandal(id);

    if (!b) { return; }

    var vandal=b.editor;

    var url=recent2.scriptPath + 'api.php?action=query&format=json&titles=' + encodeURIComponent(b.articleTitle) + '&prop=revisions&rvlimit=30&rvprop=user|comment&rvtoken=rollback';

    var onSuccess=function(x,y){ recent2.processHistoryQueryFR(x,y,b); }

    recent2.download({ url: url, onSuccess: onSuccess, id: b.id}); // fixme: onFailure

    };

    recent2.processHistoryQueryFR=function(x,downloadBundle, bundle) {

    var json=x.responseText;

    try {

    eval('var o='+json);

    var edits=recent2.anyChild(o.query.pages).revisions;

    }

    catch ( someError ) { alert('JSON business failed.\n\n' + json.substring(0,200)

    + '\n\nCannot rollback.'); return; }

    var i;

    for (i=0; i

    if (edits[i]['user']!=bundle.editor)

    { if (window.vandals[edits[i]['user']])

    {

    alert('Would roll back to previous vandal! Rollback of ' + bundle.articleTitle + ' aborted!\n\n'); return;

    }

    /* The plan here is to see if the previous editor is an IP close to the vandal's IP */

    /* To start with we will just warn the user rather than roll back further */

    else if ((recent2.ipUserRegex.test(edits[i]['user'])) && (recent2.ipUserRegex.test(bundle.editor)))

    {

    var a = edits[i]['user'].split('.');

    var b = bundle.editor.split('.');

    if ((a[0]==b[0]) && (a[1]==b[1]))

    {

    alert( 'Possible chameleon IP vandal! Rollback of ' + bundle.articleTitle + ' aborted!\n\n'); return;

    }

    else

    break;

    }

    else

    break;

    }

    }

    if (i===0) {

    alert( 'Could not rollback - someone else has edited since the vandal.\n\nPage: ' +

    bundle.articleTitle +

    '\nVandal: '+bundle.editor+'\nLast editor: '+edits[0]['user']+

    '\nEdit summary: '+edits[0]['comment']);

    return;

    }

    if (i==edits.length) {

    alert(bundle.editor + ' seems to be the only editor to ' + bundle.articleTitle +

    '.\n\nRollback aborted.'); return;

    }

    var rollbacktoken=edits[0]['rollbacktoken'];

    // var prevEditor=edits[i]['user'];

    // var edCount=i.toString() + ((i===1) ? ' edit' : ' edits');

    // var summary='Reverted ' + edCount + ' by [[Special:Contributions/' + escape(bundle.editor) + '|' +

    // escape(bundle.editor) + ']] to last version by ' + escape(prevEditor);

    // summary=summary.split(' ').join('%20');

    var req = new XMLHttpRequest();

    var url=mw.config.get('wgScriptPath') + "/api.php";

    var params="action=rollback&token="+encodeURIComponent(rollbacktoken)+"&title="+encodeURIComponent(bundle.articleTitle)+"&user="+encodeURIComponent(bundle.editor)+"&format=json";

    //alert(params);

    req.open("POST", url, true);

    req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");

    req.setRequestHeader("Content-length", params.length);

    req.setRequestHeader("Connection", "close");

    req.onreadystatechange = function() {

    if(req.readyState === 4 && req.status === 200) {

    response = eval('(' + req.responseText + ')');

    //alert(req.responseText);

    delete req;

    }

    }

    req.send(params);

    };

    recent2.anyChild=function(obj) {

    for (var p in obj) {

    return obj[p];

    }

    return null;

    };

    recent2.doPopups=function(div) {

    if (typeof(window.setupTooltips)!='undefined') { setupTooltips(div); }

    }

    window.formatTime=function(timestamp) {

    var date=new Date(timestamp);

    var nums=[date.getHours(), date.getMinutes(), date.getSeconds()];

    for (var i=0; i

    return nums.join(':');

    };

    window.showHideDetail = function(id, force, state) {

    var div=document.getElementById('diff_div_' + id);

    var lk=document.getElementById('showdiff_link_' + id);

    if (!div) return;

    var bundle=window.bundles[id];

    if (!div.innerHTML) div.innerHTML= ( bundle.badDiffFragment ? bundle.badDiffFragment:'') + bundle.diff;

    if ((force && state===true) || (!force && div.style.display=='none')) { div.style.display='inline'; lk.innerHTML='hide'; }

    else { div.style.display='none'; lk.innerHTML='view'; }

    };

    window.ignoreEditor = function() {

    // alert('ignoreEditor called');

    // alert(goodeditor);

    trustededitors[goodeditor] = true;

    showHideDetail(goodbundleid, true, false);

    };

    window.getFirstTagContent=function(parent, tag) {

    var e=parent.getElementsByTagName(tag);

    if (e && (e=e[0]) ) {

    var ret = e.firstChild.nodeValue || e.nodeValue;

    if (typeof ret != typeof ) return ;

    return ret;

    }

    };

    recent2.newCell=function() {

    var numCols=3;

    var c=recent2.controls;

    if (!c) { return; }

    if (!c.cellCount) {

    // start a table

    c.cellCount = 0;

    c.table=document.createElement('table');

    c.appendChild(c.table);

    c.tbody=document.createElement('tbody');

    c.table.appendChild(c.tbody);

    }

    if (!(c.cellCount % numCols)) {

    // start a row

    c.curRow=document.createElement('tr');

    c.tbody.appendChild(c.curRow);

    }

    // start a cell

    c.curCell=document.createElement('td');

    c.curRow.appendChild(c.curCell);

    ++c.cellCount;

    };

    recent2.newCheckbox=function(label, state, action, internalName, append) {

    // checkbox

    recent2.newCell();

    var ret=document.createElement('input');

    ret.type='checkbox';

    ret.checked = state;

    ret.onclick = function() { this.setVariables(); };

    ret.setVariables = action;

    recent2.controls.curCell.appendChild(ret);

    if (internalName) { recent2.controls[internalName]=ret; }

    // label

    var l=document.createElement('label');

    l.innerHTML=label;

    l.onclick=function(){ ret.click(); }

    // recent2.controls.appendChild(l);

    recent2.controls.curCell.appendChild(l);

    recent2.checkboxes.push(ret);

    return ret;

    };

    recent2.checkboxes=[];

    recent2.controlUI=function() {

    recent2.controls=newOutputDiv('recent2.controls', 'top', true);

    recent2.newCheckbox('Article namespace only', false,

    function() { recent2.hideNonArticles = this.checked; }, 'hidenonarticles');

    recent2.newCheckbox('... except Template', false,

    function() { recent2.showTemplates = this.checked; }, 'showtemplates');

    recent2.newCheckbox('Auto expand content', recent2.autoexpand,

    function() { recent2.autoexpand = this.checked; }, 'autoexpand');

    recent2.newCheckbox('Hide talk pages', !recent2.show_talkpages,

    function() { recent2.show_talkpages=!this.checked; }, 'talk');

    recent2.newCheckbox('Hide safe pages', false,

    function() { recent2.ignore_safe_pages = this.checked; }, 'ignoresafepages');

    recent2.newCheckbox('Show unchanged (delay 7)', false,

    function() { recent2.delay = (this.checked) ? 7 : 0; }, 'delayby7');

    recent2.newCheckbox('Ignore my edits', true,

    function() { recent2.ignore_my_edits = this.checked; }, 'ignoremyedits');

    recent2.newCheckbox('Non-admin rollback', false,

    function() { recent2.non_admin_rollback = !this.checked; }, 'nonadminrollback');

    var b=document.createElement('input');

    b.type='button';

    b.value='pause updates';

    b.onclick=function(){

    b.value=(recent2.paused)?'pause updates':'resume updates';

    recent2.togglePaused();

    }

    recent2.newCell();

    recent2.controls.curCell.appendChild(b);

    }

    recent2.count=0;

    window.loopRecentChanges=function(iterations) {

    if (!iterations) iterations=20;

    loopRecentChanges.iterations=iterations;

    url=recent2.feed=recent2.scriptPath + 'index.php?title=Special:Recentchanges&feed=rss&limit=' + ilimit + '&action=purge';

    grabRecentChanges(url);

    setTimeout(function () {

    if (recent2.paused) {++recent2.count; return; }

    if (++recent2.count >= iterations && ! confirm('Continue monitoring recent changes?') ) return;

    recent2.count %= iterations; loopRecentChanges(iterations);

    }, 8000);

    }

    window.marvin=function() {

    // this isn't really used (not accessible from the UI), so don't worry about it

    window.sysops=RegExp("^(\\-\\- April|23skidoo|Lupin)$");

    recent2.show_talkpages=true;

    recent2.controlUI();

    loopRecentChanges(200);

    }

    // **************************************************

    // Installation

    // **************************************************

    recent2.addlilink=function(tabs, url, name, id, title, key){

    var na = document.createElement('a');

    na.href = url;

    na.appendChild(document.createTextNode(name));

    var li = document.createElement('li');

    if(id) li.id = id;

    li.appendChild(na);

    tabs.appendChild(li);

    if(id) {

    if(key && title) ta[id] = [key, title];

    else if(key) ta[id] = [key, ''];

    else if(title) ta[id] = ['', title];

    }

    // re-render the title and accesskeys from existing code in wikibits.js

    // akeytt(); Deprecated

    return li;

    }

    recent2.addToolboxLink=function(url, name, id){

    var tb = document.getElementById('p-tb').getElementsByTagName('ul')[0];

    recent2.addlilink(tb, url, name, id);

    }

    window.addMarvin=function() {

    mw.util.addPortletLink( 'p-tb', mw.util.getUrl( recent2.filterPage ),

    'Filter recent changes', 'toolbox_filter_changes');

    mw.util.addPortletLink( 'p-tb', mw.util.getUrl( recent2.allRecentPage ),

    'All recent changes', 'toolbox_all_changes');

    mw.util.addPortletLink( 'p-tb', mw.util.getUrl( recent2.recentIPPage ),

    'Recent IP edits', 'toolbox_IP_edits');

    mw.util.addPortletLink( 'p-tb', mw.util.getUrl( recent2.monitorWatchlistPage ),

    'Monitor my watchlist', 'toolbox_watchlist_edits');

    mw.util.addPortletLink( 'p-tb', mw.util.getUrl( recent2.spelldictPage ),

    'Live spellcheck', 'toolbox_spelling');

    };

    recent2.testPage = function (str) {

    return RegExp(str.split(/[_ ]/).join('[_ ]'), 'i').test(document.location.href);

    };

    window.maybeStart=function() {

    var loc=document.location.href;

    if (recent2.testPage(recent2.filterPage)) {

    recent2.filter_badwords=true;

    } else if (recent2.testPage(recent2.allRecentPage)) {

    recent2.filter_badwords=false;

    } else if (recent2.testPage(recent2.recentIPPage)) {

    recent2.filter_anonsOnly=true;

    } else if (recent2.testPage(recent2.monitorWatchlistPage)) {

    recent2.filter_watchlist=true;

    } else if (recent2.testPage(recent2.spelldictPage)) {

    recent2.filter_spelling=true;

    } else {

    return;

    }

    setTimeout(marvin, 1000);

    }

    var AVTAutoEditLoader = function() {

    if (typeof(window.AVTAutoEdit) != 'undefined') {

    if (window.AVTAutoEdit.alreadyRan) {

    return false;

    }

    } else {

    window.AVTAutoEdit = {};

    }

    window.AVTAutoEdit.alreadyRan = true;

    var editbox, cmdString = mw.util.getParamValue('avtautoedit');

    if (cmdString) {

    try {

    editbox = document.editform.wpTextbox1;

    } catch (dang) { return; }

    var cmdList = recent2.parseCmd(cmdString);

    var input = editbox.value;

    var output = recent2.execCmds(input, cmdList);

    editbox.value = output;

    // wikEd user script compatibility

    if (typeof(wikEdUseWikEd) != 'undefined') {

    if (wikEdUseWikEd === true) {

    /*jshint newcap: false*/

    WikEdUpdateFrame();

    /*jshint newcap: true*/

    }

    }

    }

    var summary = mw.util.getParamValue('avtautosummary');

    if (summary) {

    document.editform.wpSummary.value = summary;

    }

    var minor = mw.util.getParamValue('avtautominor');

    if (minor) {

    switch (minor) {

    case '1':

    case 'yes':

    case 'true':

    document.editform.wpMinoredit.checked = true;

    break;

    case '0':

    case 'no':

    case 'false':

    document.editform.wpMinoredit.checked = false;

    }

    }

    var watch = mw.util.getParamValue('avtautowatch');

    if (watch) {

    switch (watch) {

    case '1':

    case 'yes':

    case 'true':

    document.editform.wpWatchthis.checked = true;

    break;

    case '0':

    case 'no':

    case 'false':

    document.editform.wpWatchthis.checked = false;

    }

    }

    var btn = mw.util.getParamValue('avtautoclick');

    if (btn) {

    if (document.editform && document.editform[btn]) {

    var headings = document.getElementsByTagName('h1');

    if (headings) {

    var div = document.createElement('div');

    var button = document.editform[btn];

    div.innerHTML = '' +

    mw.msg( 'avt-auto-click', button.value ) + '';

    document.title = '(' + document.title + ')';

    headings[0].parentNode.insertBefore(div, headings[0]);

    button.click();

    }

    } else {

    alert( mw.msg( 'avt-auto-click-button-missing', btn ) );

    }

    }

    };

    mw.loader.using( [ 'mediawiki.util', 'mediawiki.RegExp' ], function(){

    // onload

    $( maybeStart );

    $( addMarvin );

    $( AVTAutoEditLoader );

    } );

    //// testing code

    //recent2.filter_badwords=true;

    //recent2.filter_spelling=true;

    //setTimeout(marvin,1000);

    //