User:Rillke/MwJSBot.js
Jump to navigation
Jump to search
Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
Documentation for this user script can be added at User:Rillke/MwJSBot. |
- Report page listing warnings and errors.
// Usage:
// mw.loader.implement('mediawiki.commons.MwJSBot', ["//commons.wikimedia.org/w/index.php?action=raw&ctype=text/javascript&title=User:Rillke/MwJSBot.js"], {/*no styles*/}, {/*no messages*/});
// mw.loader.load('mediawiki.commons.MwJSBot');
/* global jQuery:false, mediaWiki:false, unescape:false, console:false, File, Blob, MwJSBot */
/* eslint indent:[error,tab,{outerIIFEBody:0}] */
/* jshint curly:false, bitwise:false, unused:false */
( function ( $, mw ) {
'use strict';
var myModuleName = 'mediawiki.commons.MwJSBot',
isCommonsWiki = mw.config.get( 'wgDBname' ) === 'commonswiki';
function ContinuousAverage() {
this.n = 0;
this.avg = null;
this.lastDateTime = Date.now();
}
ContinuousAverage.fn = ContinuousAverage.prototype;
$.extend( true, ContinuousAverage.fn, {
push: function ( val ) {
if ( this.avg === null ) {
this.avg = val;
this.n = 1;
} else {
this.avg = ( this.avg * this.n + val ) / ( ++this.n );
}
},
pushTimeDiff: function ( now ) {
now = now || Date.now();
this.push( now - this.lastDateTime );
this.lastDateTime = now;
},
getN: function () {
return this.n;
},
getAvg: function () {
return this.avg;
}
} );
function firstItem( o ) {
for ( var i in o ) {
if ( o.hasOwnProperty( i ) ) {
return o[ i ];
}
}
}
function encode_utf8( s ) {
return unescape( encodeURIComponent( s ) );
}
var jsb = function () {},
clnt = $.client.profile(),
APIURL = mw.util.wikiScript( 'api' ),
MPB = '----------' + myModuleName + Math.random();
jsb.fn = jsb.prototype;
$.extend( true, jsb.fn, {
$downloadRawFile: function ( url ) {
return $.ajax( {
url: url,
beforeSend: function ( xhr ) {
xhr.overrideMimeType( 'text/plain; charset=x-user-defined' );
},
dataFilter: function ( d/* , dataType*/ ) {
// Some more sophisticated stuff avoiding killing performance with memory allocations and thousands of function invocations
// https://developer.mozilla.org/en-US/docs/JavaScript/Typed_arrays would be perhaps also valuable
var f = '',
len = d.length,
buff = 1018, // You can't apply huge arrays to functions!
arrCC = new Array( Math.min( buff, len ) ),
arrF = new Array( Math.ceil( len / buff ) );
// Remove junk-high-order-bytes
for ( var i = 0, j = 0, z = 0; i < len; i++ ) {
arrCC[ j ] = ( d.charCodeAt( i ) & 0xff );
j++;
if ( ( j % buff ) === 0 ) {
// Convert char codes to chars
arrF[ z ] = String.fromCharCode.apply( null, arrCC );
// Empty the char code array
arrCC = new Array( Math.min( buff, len - i - 1 ) );
z++;
j = 0;
}
}
if ( j !== 0 ) { arrF[ z + 1 ] = String.fromCharCode.apply( null, arrCC ); }
f = arrF.join( '' );
return f;
}
} );
},
$downloadXMLFile: function ( url ) {
return $.get( url, 'xml' );
},
// NEVER use this twice to send something. Instead, create a new message!
// TODO allow/review re-sending same message again
multipartMessageForBinaryFiles: function () {
// Body parts that must be considered to end with line breaks, therefore, should have two CRLFs preceding the encapsulation line,
// the first of which is part of the preceding body part, and the second of which is part of the encapsulation boundary
var pendingParts = 0,
tokenPart = '',
useStuckWatcher = true,
msgParts,
createPart,
appendPart,
send,
formData;
if ( clnt.name === 'firefox' && clnt.versionNumber < 22 ) {
msgParts = [];
createPart = function ( param, value, ContentType, ContentTransferEncoding, ContentDisposition ) {
var p = '--' + MPB + '\r\n';
if ( !ContentDisposition ) {
p += 'Content-Disposition: form-data; name=\"' + param + '\"\r\n';
} else {
p += 'Content-Disposition: ' + ContentDisposition + '\r\n';
}
p += 'Content-Type: ' + ContentType + '\r\n';
p += 'Content-Transfer-Encoding: ' + ContentTransferEncoding + '\r\n';
return [ p, '\r\n', value, '\r\n' ].join( '' );
};
appendPart = function ( param, value, filename, MIME ) {
var ContentType, ContentTransferEncoding, ContentDisposition, doPush = function ( value ) {
msgParts.push( createPart( param, value, ContentType, ContentTransferEncoding, ContentDisposition ) );
};
if ( filename ) {
ContentType = MIME || 'application/octet-stream';
ContentTransferEncoding = 'binary';
ContentDisposition = 'attachment; name=\"' + param + '\"; filename=\"' + encode_utf8( filename.replace( /\"/g, '-' ) ) + '\"';
if ( value instanceof Blob || value instanceof File ) {
pendingParts++;
var reader = new FileReader();
reader.onload = function () {
value = reader.result;
doPush( value );
if ( tokenPart ) { msgParts.push( tokenPart ); }
pendingParts--;
if ( pendingParts === 0 && typeof send === 'function' ) { send(); }
};
reader.readAsBinaryString( value );
return;
}
} else {
value = encode_utf8( value );
ContentType = 'text/plain; charset=UTF-8';
ContentTransferEncoding = '8bit';
if ( param === 'token' && pendingParts ) {
// Ensure token last (This is done to catch connection errors. This way we can circumvent calculating MD5)
tokenPart = createPart( param, value, ContentType, ContentTransferEncoding, ContentDisposition );
return;
}
}
doPush( value );
};
} else {
formData = new FormData();
}
var msg = {
appendPart: function ( param, value, filename, MIME ) {
if ( msgParts ) {
appendPart.apply( window, Array.prototype.slice.call( arguments, 0 ) );
} else {
if ( filename ) {
var bl;
if ( value instanceof Blob || value instanceof File ) {
bl = value;
} else {
bl = new Blob( [ value ], { type: MIME || 'application/octet-stream' } );
}
formData.append( param, bl, filename );
} else {
formData.append( param, value );
}
}
// allow something like new MwJSBot().multipartMessage().appendPart("param1", "value").appendPart("param2", "value").$send();
return msg;
},
noStuckWatcher: function () {
useStuckWatcher = false;
return msg;
},
$send: function ( url, responseType ) {
var $def = $.Deferred();
send = function () {
var req = new XMLHttpRequest(),
ca = new ContinuousAverage(),
lastProgressEvt, intervalId, killProgressWatchers, progressWatcher;
// Browsers sometimes have the attitude not to continue uploading
// upon network interruptions but they send new requests correctly
killProgressWatchers = function () {
clearInterval( intervalId );
};
progressWatcher = function () {
var now = Date.now(),
diff = now - lastProgressEvt;
if ( ca.getN() > 10 && diff > ca.getAvg() * 5 && diff > 7000 ) {
$def.notify( 'stuck', req );
}
};
req.onreadystatechange = function () {
if ( req.readyState !== 4 ) { return; }
if ( req.status === 200 ) {
// TODO: Pass more args
$def.resolve( req.statusText, req.response );
killProgressWatchers();
} else {
$def.reject( req.statusText, req.response, req );
killProgressWatchers();
}
};
req.onerror = function () {
setTimeout( function () {
$def.reject( req.statusText, req.response, req );
killProgressWatchers();
}, 100 );
};
req.onabort = function () {
setTimeout( function () {
$def.reject( req.statusText, req.response, req );
killProgressWatchers();
}, 100 );
};
// Can we monitor upload status?
if ( req.upload ) {
req.upload.onprogress = function ( e ) {
// Ensure compatible event
if ( !e.loaded || !e.total ) { return; }
$def.notify( 'uploadstatus', e );
lastProgressEvt = Date.now();
ca.pushTimeDiff( lastProgressEvt );
};
}
req.open( 'POST', url || APIURL );
if ( responseType ) {
req.responseType = responseType;
}
if ( useStuckWatcher ) {
intervalId = setInterval( progressWatcher, 5000 );
}
if ( msgParts ) {
req.setRequestHeader( 'Content-Type', 'multipart/form-data; charset=UTF-8; boundary=' + MPB );
msgParts.push( '--', MPB, '--', '\r\n' );
req.sendAsBinary( msgParts.join( '' ) );
} else {
req.send( formData );
}
};
if ( pendingParts === 0 ) {
send();
}
return $def;
}
};
return msg;
},
multipartMessageForUTF8Files: function () {
var msgParts,
createPart,
appendPart;
msgParts = [];
createPart = function ( param, value, ContentType, ContentTransferEncoding, ContentDisposition ) {
var p = '--' + MPB + '\n';
if ( !ContentDisposition ) {
p += 'Content-Disposition: form-data; name=\"' + param + '\"\n';
} else {
p += 'Content-Disposition: ' + ContentDisposition + '\n';
}
p += 'Content-Type: ' + ContentType + '\n';
p += 'Content-Transfer-Encoding: ' + ContentTransferEncoding + '\n';
return [ p, '\n', value, '\n' ].join( '' );
};
appendPart = function ( param, value, filename ) {
var ContentType, ContentTransferEncoding, ContentDisposition;
if ( filename ) {
ContentType = 'application/octet-stream';
ContentTransferEncoding = '8bit';
ContentDisposition = 'attachment; name=\"' + param + '\"; filename=\"' + filename.replace( /\"/g, '-' ) + '\"';
} else {
ContentType = 'text/plain; charset=UTF-8';
ContentTransferEncoding = '8bit';
}
msgParts.push( createPart( param, value, ContentType, ContentTransferEncoding, ContentDisposition ) );
};
var msg = {
appendPart: function ( /* param, value, filename*/ ) {
appendPart.apply( window, Array.prototype.slice.call( arguments, 0 ) );
// allow something like new MwJSBot().multipartMessage().appendPart("param1", "value").appendPart("param2", "value").$send();
return msg;
},
$send: function ( url, responseType ) {
var $def = $.Deferred(),
req = new XMLHttpRequest();
req.onreadystatechange = function () {
if ( req.readyState !== 4 ) { return; }
if ( req.status === 200 ) {
// TODO: Pass more args
$def.resolve( req.statusText, req.response );
} else {
$def.reject( req.response );
}
};
req.open( 'POST', url || APIURL );
if ( responseType ) {
req.responseType = responseType;
}
req.setRequestHeader( 'Content-Type', 'multipart/form-data; charset=UTF-8; boundary=' + MPB );
msgParts.push( '--', MPB, '--', '\n' );
req.send( msgParts.join( '' ) );
return $def;
}
};
return msg;
},
refreshToken: function ( type, cb ) {
var j = this;
mw.loader.using( [ 'ext.gadget.libAPI', 'mediawiki.user' ], function () {
/* FIXME: This is causing an error: Error: api.query is for queries only. For editing use the stable Commons edit-api. */
return;
mw.libs.commons.api.query( {
action: 'tokens',
type: type
}, {
method: 'POST',
cache: false,
cb: function ( r ) {
$.each( r.tokens, function ( type, v ) {
type = type.replace( 'token', 'Token' );
mw.user.tokens.set( type, v );
} );
cb();
},
// r-result, query, text
errCb: function ( /* t, r, q */ ) {
j.fail( 'Failed to refresh token.' );
}
} );
} );
},
chunkedUploadDefault: {
maxChunkSize: 0.5 * 1024 * 1024, // 2MB
retry: {
emptyResponse: 9,
serverError: 9,
apiErrors: 6,
offsetError: 2
},
file: null,
title: '',
summary: '',
useStash: true,
async: true,
callbacks: {
nameNeedsChange: function () {},
ignorewarningsRequired: function () {},
loginRequired: function () {}
},
passToAPI: {
upload: {
// e.g. ignorewarnings: 1
tags: isCommonsWiki ? 'rillke-mw-js-bot' : ''
},
finish: {
tags: isCommonsWiki ? 'rillke-mw-js-bot' : ''
}
}
},
uploadWarnings: {
filename: [ 'exists', 'page-exists', 'was-deleted', 'exists-normalized', 'thumb', 'thumb-name', 'bad-prefix', 'badfilename' ],
other: [ 'duplicate-archive', 'duplicate', 'large-file', 'emptyfile', 'filetype-unwanted-type' ]
},
chunkedUpload: function ( p, file ) {
var j = this,
$def = $.Deferred(),
filekey = '',
size = 0,
remaining = 0,
chunkSize = 0,
offset = 0,
offsetid = 0,
addToNextChunk = 0,
stuckCounter = 0,
chunkinfo = [],
waitingFinish,
startTime,
waitTime,
$stuckXhr;
p = $.extend( true, {}, j.chunkedUploadDefault, p );
if ( !file || !p.title ) {
return j.fail( 'chunked upload> Either no file or no title specified.' );
}
if ( !( window.File && window.File.prototype.slice && window.FileReader && window.Blob ) ) {
return j.fail( 'chunked upload> Your browser does not support the full File-API, FileReader and Blob.' );
}
var internal = {
status: 'uploading',
uploadAPI: function ( params ) {
params = $.extend( {
format: 'json',
action: 'upload',
filekey: filekey,
token: mw.user.tokens.get( 'csrfToken' )
}, params );
return $.post( APIURL, params );
},
nextChunk: function () {
chunkSize = Math.min( remaining, p.maxChunkSize + addToNextChunk );
var blob = file.slice( offset, offset + chunkSize ),
mpm = j.multipartMessageForBinaryFiles();
addToNextChunk = 0;
// Notify everyone who likes to know how the situation is progressing
chunkinfo.currentchunk = chunkinfo[ offsetid ];
$def.notify( 'prog', chunkinfo );
mpm.appendPart( 'format', 'json' );
mpm.appendPart( 'action', 'upload' );
mpm.appendPart( 'filename', p.title );
if ( filekey ) { mpm.appendPart( 'filekey', filekey ); }
if ( p.useStash ) { mpm.appendPart( 'stash', 1 ); }
mpm.appendPart( 'filesize', size );
mpm.appendPart( 'offset', offset );
if ( p.async ) { mpm.appendPart( 'async', 1 ); }
mpm.appendPart( 'chunk', blob, p.title, file.type );
mpm.appendPart( 'token', mw.user.tokens.get( 'csrfToken' ) );
$.each( p.passToAPI.upload, function ( k, v ) {
mpm.appendPart( k, v );
} );
if ( !p.async ) { mpm.noStuckWatcher(); }
startTime = Date.now();
waitTime = 4000;
mpm.$send().done( internal.chunkUploaded ).fail( internal.chunkFailed ).progress( internal.chunkUploadProgess );
},
chunkStuck: function ( xhr ) {
if ( $stuckXhr ) {
$stuckXhr.abort();
}
$stuckXhr = $.getJSON( APIURL, {
format: 'json',
action: 'tokens'
} ).done( function () {
// Connection Ok ... we can try to fix this
if ( ++stuckCounter > 10 ) {
stuckCounter = 0;
chunkinfo.currentchunk.progressText = 'Connection seems to be okay. Re-sending this request.';
p.retry.serverError += 0.8;
xhr.abort();
} else {
chunkinfo.currentchunk.progressText = 'Connection seems to be okay. Waiting one more time...';
}
$def.notify( 'stuckok', chunkinfo );
} ).fail( function ( jqXHR, textStatus, errorThrown ) {
// Connection broken ... user or server admins have to fix this
chunkinfo.currentchunk.progressText = 'Please check your connection! Error: ' + textStatus + ' | ' + errorThrown;
$def.notify( 'stuckbroken', chunkinfo );
} );
},
chunkUploadProgess: function ( type, e ) {
switch ( type ) {
case 'uploadstatus':
stuckCounter = 0;
chunkinfo.currentchunk.progress = 100 * e.loaded / e.total;
chunkinfo.currentchunk.progressText = 'upload in progress';
$def.notify( type, chunkinfo );
break;
case 'stuck':
chunkinfo.currentchunk.progressText = 'upload is stuck';
$def.notify( type, chunkinfo );
internal.chunkStuck( e );
break;
}
},
chunkUploaded: function ( status, r ) {
stuckCounter = 0;
r = JSON.parse( r );
var txt, args, defaultError;
defaultError = function ( args ) {
args = Array.prototype.slice.call( args, 0 );
args.unshift( r.error.code + ': ' + r.error.info );
return internal.fail.apply( internal, args );
};
if ( r && r.error ) {
switch ( r.error.code ) {
// r.error.info
case 'badtoken':
j.refreshToken( 'edit', $.proxy( internal.nextChunk, internal ) );
break;
case 'stasherror':
if ( r.error.info.indexOf( 'UploadStashNotLoggedInException' ) === -1 ) {
return defaultError( arguments );
}
/* falls through */
case 'readapidenied':
case 'writeapidenied':
case 'invalid-file-key':
case 'mustbeloggedin':
case 'permissiondenied':
case 'internal_api_error_UploadStashNotLoggedInException':
case 'stashnotloggedin':
p.callbacks.loginRequired( r.error.code + ': ' + r.error.info, function () {
j.refreshToken( 'edit', $.proxy( internal.nextChunk, internal ) );
} );
break;
case 'stashfailed':
case 'offseterror':
case 'offsetmismatch':
if ( --p.retry.apiErrors < 0 ) {
return defaultError( arguments );
}
if ( r.error.offset && Number( r.error.offset ) !== offset ) {
return internal.offsetmismatch( arguments, Number( r.error.offset ) );
}
internal.nextChunk();
break;
default:
return defaultError( arguments );
}
return;
}
if ( !r || !r.upload ) {
// Simply retry when getting an empty response
txt = 'Empty response';
if ( --p.retry.emptyResponse < 0 ) {
args = Array.prototype.slice.call( arguments, 0 );
args.unshift( txt );
return internal.fail.apply( internal, args );
}
$def.notify( 'err', chunkinfo, txt );
internal.nextChunk();
}
if ( r.upload.filekey ) { filekey = r.upload.filekey; }
var _successfullytransmitted = function () {
chunkinfo.currentchunk.progress = 100;
chunkinfo.currentchunk.progressText = 'Chunk uploaded';
$def.notify( 'prog', chunkinfo );
offset += chunkSize;
remaining -= chunkSize;
offsetid++;
};
if ( r.upload.result === 'Warning' ) {
var fileNameRelated = true,
warnings = [],
__insertNewParams = function ( newparams ) {
p = $.extend( p, newparams );
if ( waitingFinish ) {
internal.finish();
}
};
$.each( r.upload.warnings, function ( k, v ) {
warnings.push( k + ': \"' + v + '\"' );
fileNameRelated = fileNameRelated && $.inArray( k, j.uploadWarnings.filename ) > -1;
return fileNameRelated;
} );
warnings = warnings.join( ', ' );
if ( fileNameRelated ) {
p.callbacks.nameNeedsChange( warnings, __insertNewParams );
} else {
p.callbacks.ignorewarningsRequired( warnings, __insertNewParams );
}
if ( remaining === 0 ) {
waitingFinish = true;
} else {
// Simply continue with ignorewarnings flag
p.passToAPI.upload.ignorewarnings = 1;
internal.nextChunk();
}
return;
}
if ( r.upload.result === 'Continue' && remaining !== 0 ) {
var offsetRequested = Number( r.upload.offset ),
offsetCalculated = offset + chunkSize,
diff = offsetCalculated - offsetRequested;
if ( offsetRequested === offsetCalculated ) {
_successfullytransmitted();
internal.nextChunk();
} else if ( offsetRequested < offsetCalculated ) {
$def.notify( 'warn', chunkinfo, 'Offset requested by API is lower than offset calculated. \r\n issue?' );
// Correct current chunk size
chunkSize -= diff;
addToNextChunk = diff;
_successfullytransmitted();
internal.nextChunk();
} else {
// Error handling: Simply upload last chunk again
txt = 'Offset error: API wants to continue at ' + r.upload.offset + ' but calculated offset is ' + ( offset + chunkSize );
if ( --p.retry.offsetError < 0 ) {
args = Array.prototype.slice.call( arguments, 0 );
args.unshift( txt );
return internal.fail.apply( internal, args );
}
$def.notify( 'err', chunkinfo, txt );
internal.nextChunk();
}
return;
}
_successfullytransmitted();
if ( r.upload.result === 'Success' && remaining === 0 ) {
chunkinfo.currentchunk = chunkinfo.finalize;
internal.finish();
return;
}
if ( r.upload.result === 'Poll' ) {
if ( remaining === 0 ) {
chunkinfo.currentchunk = chunkinfo.finalize;
internal.status = 'rebuilding';
chunkinfo.finalize.progress = 1;
chunkinfo.finalize.progressText = 'Assembling chunks';
$def.notify( 'prog', chunkinfo, txt );
}
setTimeout( internal.poll, 2000 );
return;
}
},
offsetmismatch: function ( args, offsetExpectedByServer ) {
var txt,
_successfullytransmitted = function ( newOffset, newRemaining, newOffsetid ) {
chunkinfo.currentchunk.progress = 100;
chunkinfo.currentchunk.progressText = 'Chunk uploaded';
$def.notify( 'prog', chunkinfo );
offset = newOffset;
remaining = newRemaining;
offsetid = newOffsetid;
};
$def.notify( 'warn', chunkinfo, 'Offset issue by Server detected. Attempting to fix automatically.' );
if ( offsetExpectedByServer === offset + chunkSize ) {
txt = "Looks like this chunk was successfully transmitted but didn't receive a success message for it." +
'Please have a look at the file after uploading.';
$def.notify( 'warn', chunkinfo, txt );
_successfullytransmitted(
offsetExpectedByServer,
remaining - chunkSize,
offsetid + 1
);
p.retry.offsetError += 0.5;
} else if ( Math.abs( offsetExpectedByServer - offset ) <= chunkSize ) {
txt = 'The offset requested by the server differs by one or less than one chunk size from client side calculations.\r\n' +
'Going to return what is requested but please have a close look at the file after uploading.\r\n' +
'Offset expected by server: ' + offsetExpectedByServer + '. Offset calculated by client: ' + offset;
$def.notify( 'warn', chunkinfo, txt );
_successfullytransmitted(
offsetExpectedByServer,
size - offsetExpectedByServer,
offsetExpectedByServer > offset ? offsetid + 1 : offsetid
);
} else {
txt = 'The server expects continuation at byte ' + offsetExpectedByServer + '.\r\n' +
'However, to the client side calculated offset is ' + offset + '.\r\n';
$def.notify( 'warn', chunkinfo, txt );
}
if ( --p.retry.offsetError < 0 ) {
args = Array.prototype.slice.call( args, 0 );
args.unshift( txt );
return internal.fail.apply( internal, args );
}
internal.nextChunk();
},
/**
* status checker
**/
poll: function () {
var txt,
args,
cb,
word;
switch ( internal.status ) {
case 'uploading':
word = 'process chunk';
cb = function ( r ) {
internal.chunkUploaded( 'OK', r );
return true;
};
break;
case 'rebuilding':
word = 'rebuild uploaded file';
cb = function ( r ) {
return ( r.upload.stage === 'queued' ) ? false : ( internal.finish() || true );
};
break;
case 'publishing':
word = 'publish uploaded file';
cb = function ( r ) {
internal.finished( r );
return true;
};
break;
}
internal.uploadAPI( {
checkstatus: 1
} ).done( function ( r ) {
if ( r && r.error ) {
switch ( r.error.code ) {
// r.error.info
case 'badtoken':
j.refreshToken( 'edit', $.proxy( internal.poll, internal ) );
break;
case 'readapidenied':
case 'writeapidenied':
case 'invalid-file-key':
case 'mustbeloggedin':
case 'permissiondenied':
case 'internal_api_error_UploadStashNotLoggedInException':
p.callbacks.loginRequired( r.error.code + ': ' + r.error.info, function () {
j.refreshToken( 'edit', $.proxy( internal.poll, internal ) );
} );
break;
default:
args = Array.prototype.slice.call( arguments, 0 );
args.unshift( r.error.code + ': ' + r.error.info );
return internal.fail.apply( internal, args );
}
return;
}
if ( !r || !r.upload ) {
txt = 'Empty response: Still waiting for server to ' + word;
if ( --p.retry.emptyResponse < 0 ) {
args = Array.prototype.slice.call( arguments, 0 );
args.unshift( txt );
return internal.fail.apply( internal, args );
}
setTimeout( internal.poll, 7000 );
$def.notify( 'err', chunkinfo, txt );
return j.log( txt, r );
}
if ( r.upload.filekey ) { filekey = r.upload.filekey; }
// If the operation succeeded, the callback is called (which returns true for most cases)
// otherwise the callback is not called in JavaScript because the expression would evaluate to
// false anyway
if ( !( ( r.upload.result === 'Success' || r.upload.result === 'Continue' ) && cb( r ) ) ) {
txt = 'Still waiting for server to ' + word;
$def.notify( 'prog', chunkinfo, txt );
j.log( txt, r );
setTimeout( internal.poll, 4500 );
}
} ).fail( function ( jqXHR, textStatus, errorThrown ) {
// TODO: Server status etc.
// Simply re-query
txt = 'Sever-Error ' + jqXHR.status + '. Reason: ' + textStatus + ' ' + errorThrown + ' ... Still waiting for server to ' + word;
if ( --p.retry.serverError < 0 ) {
args = Array.prototype.slice.call( arguments, 0 );
args.unshift( txt );
return internal.fail.apply( internal, args );
}
setTimeout( internal.poll, 7000 );
$def.notify( 'err', chunkinfo, txt );
j.log( txt );
} );
},
finish: function () {
internal.status = 'publishing';
chunkinfo.finalize.progress = 10;
chunkinfo.finalize.progressText = 'Finishing';
$def.notify( 'prog', chunkinfo );
j.log( 'chunked upload> Finishing.' );
var params = {
filename: p.title,
filesize: size,
comment: p.summary,
text: p.text
};
if ( p.async ) { params.async = 1; }
$.each( p.passToAPI.finish, function ( k, v ) {
params[ k ] = v;
} );
internal.uploadAPI( params ).done( internal.possiblyFinished ).fail( internal.finishFailed );
},
possiblyFinished: function ( r ) {
if ( r && r.error ) {
switch ( r.error.code ) {
// r.error.info
case 'badtoken':
j.refreshToken( 'edit', $.proxy( internal.finish, internal ) );
break;
case 'readapidenied':
case 'writeapidenied':
case 'invalid-file-key':
case 'mustbeloggedin':
case 'permissiondenied':
case 'internal_api_error_UploadStashNotLoggedInException':
p.callbacks.loginRequired( r.error.code + ': ' + r.error.info, function () {
j.refreshToken( 'edit', $.proxy( internal.finish, internal ) );
} );
break;
default:
var args = Array.prototype.slice.call( arguments, 0 );
args.unshift( r.error.code + ': ' + r.error.info );
return internal.fail.apply( internal, args );
}
return;
}
if ( !r || !r.upload ) { return internal.finishFailed( 'empty response received' ); }
if ( r.upload.filekey ) { filekey = r.upload.filekey; }
switch ( r.upload.result ) {
case 'Poll':
j.log( 'Waiting for upload to be published' );
chunkinfo.finalize.progress = 50;
chunkinfo.finalize.progressText = 'Waiting for upload to be published';
$def.notify( 'prog', chunkinfo );
setTimeout( internal.poll, 2000 );
break;
case 'Success':
internal.finished( r );
break;
}
},
finished: function ( r ) {
j.log( 'Uploaded successfully' );
chunkinfo.finalize.progress = 100;
chunkinfo.finalize.progressText = 'Uploaded successfully';
$def.notify( 'prog', chunkinfo );
$def.resolve( chunkinfo, r );
},
chunkFailed: function ( statusText, response, xhr ) {
stuckCounter = 0;
var txt = 'Server error ' + xhr.status + ' after uploading chunk: ' + statusText + '\nResponse: ' + response.slice( 0, 500 ).replace( /\n\n/g, '\n' );
if ( --p.retry.serverError < 0 ) {
var args = Array.prototype.slice.call( arguments, 0 );
args.shift( txt );
return internal.fail.apply( internal, args );
}
$def.notify( 'err', chunkinfo, txt );
if ( startTime + 750 > Date.now() ) {
setTimeout( function () {
internal.nextChunk();
}, waitTime *= 2 );
} else {
internal.nextChunk();
}
},
finishFailed: function ( reasonOrXHR, textStatus, errorThrown ) {
if ( typeof reasonOrXHR === 'object' ) { reasonOrXHR = textStatus + ' ' + errorThrown; }
var txt = 'Server error while publishing upload. Reason: ' + reasonOrXHR;
if ( --p.retry.serverError < 0 ) {
var args = Array.prototype.slice.call( arguments, 0 );
args.unshift( txt );
return internal.fail.apply( internal, args );
}
$def.notify( 'err', chunkinfo, txt );
internal.finish();
},
fail: function () {
var args = Array.prototype.slice.call( arguments, 0 );
j.fail.apply( j, args );
$def.reject.apply( $def, args );
}
};
// Get some statistics about the file
size = remaining = file.size;
var remains4loop = size,
chunksize4loop,
i = 0;
while ( remains4loop > 0 ) {
chunksize4loop = Math.min( remaining, p.maxChunkSize );
chunkinfo[ i ] = {
chunksize: chunksize4loop,
remaining: remains4loop,
progress: 0,
progressText: 'in progress',
id: i
};
remains4loop -= chunksize4loop;
i++;
}
chunkinfo.finalize = {
progress: 0,
progressText: '',
id: 'finalize'
};
internal.nextChunk();
$def.chunkinfo = chunkinfo;
return $def;
},
createSampleUploadButton: function () {
var j = this;
$( '<input type="file" id="files" name="file">' ).prependTo( mw.util.$content ).on( 'change', function ( /* e */ ) {
j.chunkedUpload( {
title: 'A random title.png'
}, this.files[ 0 ] ).progress( function () {
console.log( 'prog', arguments );
} ).done( function () {
console.log( 'done', arguments );
} );
} );
},
$loadContinue: function ( title ) {
var $def = $.Deferred(),
j = this;
mw.loader.using( [ 'ext.gadget.libAPI' ], function () {
mw.libs.commons.api.query( {
action: 'query',
prop: 'revisions',
rvprop: 'content',
rvlimit: 1,
titles: title
}, {
method: 'POST',
cache: false,
cb: function ( r ) {
try {
$def.resolve( firstItem( r.query.pages ).revisions[ 0 ][ '*' ] );
} catch ( ex ) {
$def.reject( ex );
}
},
// r-result, query, text
errCb: function ( t, r/* , q*/ ) {
j.fail( 'Failed to retrieve continue param.' );
$def.reject( r );
}
} );
} );
return $def;
},
$continueQuery: function ( query, result ) {
var $def = $.Deferred(),
qc = result[ 'query-continue' ],
j = this,
oldProp = query.prop,
oldList = query.list;
if ( qc ) {
var props = [],
lists = [];
$.each( qc, function ( k, v ) {
if ( oldProp && oldProp.indexOf( k ) > -1 ) {
props.push( k );
}
if ( oldList && oldList.indexOf( k ) > -1 ) {
lists.push( k );
}
$.extend( query, v );
} );
if ( props.length ) {
query.prop = props.join( '|' );
} else {
delete query.prop;
}
if ( lists.length ) {
query.list = lists.join( '|' );
} else {
delete query.list;
}
} else if ( result.continue ) {
// new style continuation
$.extend( query, result.continue );
} else {
throw new Error( 'MW-JS-BOT: Nothing to continue.' );
}
mw.loader.using( [ 'ext.gadget.libAPI' ], function () {
mw.libs.commons.api.query( query, {
method: 'POST',
cache: false,
cb: function ( r ) {
$def.resolve( r );
},
// r-result, query, text
errCb: function ( t, r/* , q*/ ) {
j.fail( 'Failed to continue query.' );
$def.reject( r );
}
} );
} );
return $def;
},
$saveContinue: function ( title, value, summary ) {
var $def = $.Deferred(),
j = this;
mw.loader.using( [ 'ext.gadget.libAPI' ], function () {
mw.libs.commons.api.editPage( {
editType: 'text',
text: value,
title: title,
summary: 'MW-JS-BOT: ' + ( summary || ' updating continue-param' ),
cb: function ( r ) {
$def.resolve( r );
},
// r-result, query, text
errCb: function ( t, r/* , q*/ ) {
j.fail( 'Failed to save continue param.' );
$def.reject( r );
}
} );
} );
return $def;
},
$addLogline: function ( title, value, summary ) {
var $def = $.Deferred(),
j = this;
mw.loader.using( [ 'ext.gadget.libAPI' ], function () {
mw.libs.commons.api.editPage( {
editType: 'appendtext',
text: '\n# ' + value,
title: title,
summary: 'MW-JS-BOT: ' + ( summary || ' logging' ),
cb: function ( r ) {
$def.resolve( r );
},
// r-result, query, text
errCb: function ( t, r/* , q*/ ) {
j.fail( 'Failed to log.' );
$def.reject( r );
}
} );
} );
return $def;
},
$xmlFromString: function ( xmlString, title ) {
var xml = $.parseXML( xmlString ),
isXmlDoc = $.isXMLDoc( xml ),
$xmlDoc = $( xml ),
j = this;
if ( !isXmlDoc || !$xmlDoc || !$xmlDoc.length ) {
j.warn( title + ' is not an XML Document.' );
}
return $xmlDoc;
},
stringFrom$xml: function ( $xml ) {
if ( !window.XMLSerializer ) {
window.XMLSerializer = function () {};
window.XMLSerializer.prototype.serializeToString = function ( XMLObject ) {
return XMLObject.xml || '';
};
}
var oSerializer = new XMLSerializer(),
xmlStringOut = oSerializer.serializeToString( $xml[ 0 ] );
return xmlStringOut;
},
$getWindowConsole: function () {
if ( this.$windowConsole ) {
return this.$windowConsole;
}
var $console = this.$windowConsole = $( '<div>' ).css( {
'font-family': '"Lucida Console", "Courier New", Monospace'
} ).appendTo( mw.util.$content );
$console.$consoleTop = $( '<div>' ).text( 'Window Console by MW-JS-BOT' ).appendTo( $console );
$console.$droppedEntryNote = $( '<span>' ).appendTo( $console.$consoleTop );
$console.droppedEntries = 0;
$console.visibleEntries = 0;
$console.dropFirstEntry = function () {
$console.$consoleTop.next().remove();
$console.droppedEntries++;
$console.visibleEntries--;
$console.$droppedEntryNote.text( $console.droppedEntries + ' entries were dropped from the window console.' );
};
$console.log = function () {
if ( $console.totalEntries > 400 ) {
$console.dropFirstEntry();
}
$console.visibleEntries++;
var $entry = $( '<div>' ).attr( {
class: 'windowconsole-entry'
} ),
argslen = arguments.length;
for ( var i = 0; i < argslen; i++ ) {
try {
var $subentry = $( '<p>' ).text( arguments[ i ] );
$subentry.appendTo( $entry );
} catch ( ex ) {}
}
$entry.appendTo( $console );
};
return $console;
},
log: function ( /* unlimitedArgs*/ ) {
if ( window.console ) {
var arrArgs = Array.prototype.slice.call( arguments, 0 );
arrArgs.unshift( 'mwbot>' );
if ( typeof console.log === 'function' ) {
console.log.apply( console, arrArgs );
} else {
this.$getWindowConsole().apply( this, arrArgs );
}
}
},
warn: function ( /* unlimitedArgs*/ ) {
var j = this;
if ( window.console ) {
var arrArgs = Array.prototype.slice.call( arguments, 0 );
if ( typeof console.warn === 'function' ) {
arrArgs.unshift( 'mwbot>' );
console.warn.apply( console, arrArgs );
} else {
arrArgs.unshift( 'WARNING>' );
j.log.apply( j, arrArgs );
}
}
},
fail: function ( /* unlimitedArgs*/ ) {
var j = this;
if ( window.console ) {
var arrArgs = Array.prototype.slice.call( arguments, 0 );
if ( typeof console.error === 'function' ) {
arrArgs.unshift( 'mwbot>' );
console.error.apply( console, arrArgs );
} else {
arrArgs.unshift( 'ERR>' );
j.log.apply( j, arrArgs );
}
}
}
} );
window.MwJSBot = jsb;
var h = {};
h[ myModuleName ] = 'ready';
mw.loader.state( h );
new MwJSBot().log( 'Hello. I am your MwJSBot framework.' );
}( jQuery, mediaWiki ) );