add JS api, feedback libs from UW into core.

This commit is contained in:
Neil Kandalgaonkar 2011-12-09 04:48:39 +00:00
parent ddbf20c0ba
commit cb0cf72eba
7 changed files with 681 additions and 0 deletions

View file

@ -497,10 +497,45 @@ return array(
'debugScripts' => 'resources/mediawiki/mediawiki.log.js',
'debugRaw' => false,
),
'mediawiki.api' => array(
'scripts' => 'resources/mediawiki/mediawiki.api.js',
),
'mediawiki.api.category' => array(
'scripts' => 'resources/mediawiki/mediawiki.api.category.js',
'dependencies' => array(
'mediawiki.api',
'mediawiki.Title'
),
),
'mediawiki.api.edit' => array(
'scripts' => 'resources/mediawiki/mediawiki.api.edit.js',
'dependencies' => array(
'mediawiki.api',
'mediawiki.Title'
),
),
'mediawiki.api.parse' => array(
'scripts' => 'resources/mediawiki/mediawiki.api.parse.js',
'dependencies' => 'mediawiki.api',
),
'mediawiki.api.titleblacklist' => array(
'scripts' => 'resources/mediawiki/mediawiki.api.titleblacklist.js',
'dependencies' => array(
'mediawiki.api',
'mediawiki.Title'
),
),
'mediawiki.debug' => array(
'scripts' => 'resources/mediawiki/mediawiki.debug.js',
'styles' => 'resources/mediawiki/mediawiki.debug.css',
),
'mediawiki.feedback' => array(
'scripts' => 'resources/mediawiki/mediawiki.feedback.js',
'dependencies' => array(
'mediawiki.api.edit',
'mediawiki.Title'
),
),
'mediawiki.htmlform' => array(
'scripts' => 'resources/mediawiki/mediawiki.htmlform.js',
),

View file

@ -0,0 +1,106 @@
// library to assist with API calls on categories
( function( mw, $ ) {
$.extend( mw.Api.prototype, {
/**
* Determine if a category exists
* @param {mw.Title}
* @param {Function} callback to pass boolean of category's existence
* @param {Function} optional callback to run if api error
* @return ajax call object
*/
isCategory: function( title, callback, err ) {
var params = {
'prop': 'categoryinfo',
'titles': title.toString()
};
var ok = function( data ) {
var exists = false;
if ( data.query && data.query.pages ) {
$.each( data.query.pages, function( id, page ) {
if ( page.categoryinfo ) {
exists = true;
}
} );
}
callback( exists );
};
return this.get( params, { ok: ok, err: err } );
},
/**
* Get a list of categories that match a certain prefix.
* e.g. given "Foo", return "Food", "Foolish people", "Foosball tables" ...
* @param {String} prefix to match
* @param {Function} callback to pass matched categories to
* @param {Function} optional callback to run if api error
* @return ajax call object
*/
getCategoriesByPrefix: function( prefix, callback, err ) {
// fetch with allpages to only get categories that have a corresponding description page.
var params = {
'list': 'allpages',
'apprefix': prefix,
'apnamespace': mw.config.get('wgNamespaceIds').category
};
var ok = function( data ) {
var texts = [];
if ( data.query && data.query.allpages ) {
$.each( data.query.allpages, function( i, category ) {
texts.push( new mw.Title( category.title ).getNameText() );
} );
}
callback( texts );
};
return this.get( params, { ok: ok, err: err } );
},
/**
* Get the categories that a particular page on the wiki belongs to
* @param {mw.Title}
* @param {Function} callback to pass categories to (or false, if title not found)
* @param {Function} optional callback to run if api error
* @param {Boolean} optional asynchronousness (default = true = async)
* @return ajax call object
*/
getCategories: function( title, callback, err, async ) {
var params = {
prop: 'categories',
titles: title.toString()
};
if ( async === undefined ) {
async = true;
}
var ok = function( data ) {
var ret = false;
if ( data.query && data.query.pages ) {
$.each( data.query.pages, function( id, page ) {
if ( page.categories ) {
if ( typeof ret !== 'object' ) {
ret = [];
}
$.each( page.categories, function( i, cat ) {
ret.push( new mw.Title( cat.title ) );
} );
}
} );
}
callback( ret );
};
return this.get( params, { ok: ok, err: err, async: async } );
}
} );
} )( window.mediaWiki, jQuery );

View file

@ -0,0 +1,117 @@
// library to assist with edits
( function( mw, $, undefined ) {
// cached token so we don't have to keep fetching new ones for every single post
var cachedToken = null;
$.extend( mw.Api.prototype, {
/* Post to API with edit token. If we have no token, get one and try to post.
* If we have a cached token try using that, and if it fails, blank out the
* cached token and start over.
*
* @param params API parameters
* @param ok callback for success
* @param err (optional) error callback
*/
postWithEditToken: function( params, ok, err ) {
var api = this;
if ( cachedToken === null ) {
// We don't have a valid cached token, so get a fresh one and try posting.
// We do not trap any 'badtoken' or 'notoken' errors, because we don't want
// an infinite loop. If this fresh token is bad, something else is very wrong.
var useTokenToPost = function( token ) {
params.token = token;
api.post( params, ok, err );
};
api.getEditToken( useTokenToPost, err );
} else {
// We do have a token, but it might be expired. So if it is 'bad' then
// start over with a new token.
params.token = cachedToken;
var getTokenIfBad = function( code, result ) {
if ( code === 'badtoken' ) {
cachedToken = null; // force a new token
api.postWithEditToken( params, ok, err );
} else {
err( code, result );
}
};
api.post( params, { 'ok' : ok, 'err' : getTokenIfBad });
}
},
/**
* Api helper to grab an edit token
*
* token callback has signature ( String token )
* error callback has signature ( String code, Object results, XmlHttpRequest xhr, Exception exception )
* Note that xhr and exception are only available for 'http_*' errors
* code may be any http_* error code (see mw.Api), or 'token_missing'
*
* @param {Function} received token callback
* @param {Function} error callback
*/
getEditToken: function( tokenCallback, err ) {
var api = this;
var parameters = {
'prop': 'info',
'intoken': 'edit',
/* we need some kind of dummy page to get a token from. This will return a response
complaining that the page is missing, but we should also get an edit token */
'titles': 'DummyPageForEditToken'
};
var ok = function( data ) {
var token;
$.each( data.query.pages, function( i, page ) {
if ( page['edittoken'] ) {
token = page['edittoken'];
return false;
}
} );
if ( token !== undefined ) {
cachedToken = token;
tokenCallback( token );
} else {
err( 'token-missing', data );
}
};
var ajaxOptions = {
'ok': ok,
'err': err,
// Due to the API assuming we're logged out if we pass the callback-parameter,
// we have to disable jQuery's callback system, and instead parse JSON string,
// by setting 'jsonp' to false.
'jsonp': false
};
api.get( parameters, ajaxOptions );
},
/**
* Create a new section of the page.
* @param {mw.Title|String} target page
* @param {String} header
* @param {String} wikitext message
* @param {Function} success handler
* @param {Function} error handler
*/
newSection: function( title, header, message, ok, err ) {
var params = {
action: 'edit',
section: 'new',
format: 'json',
title: title.toString(),
summary: header,
text: message
};
this.postWithEditToken( params, ok, err );
}
} ); // end extend
} )( window.mediaWiki, jQuery );

View file

@ -0,0 +1,208 @@
/* mw.Api objects represent the API of a particular MediaWiki server. */
( function( mw, $j, undefined ) {
/**
* Represents the API of a particular MediaWiki server.
*
* Required options:
* url - complete URL to API endpoint. Usually equivalent to wgServer + wgScriptPath + '/api.php'
*
* Other options:
* can override the parameter defaults and ajax default options.
* XXX document!
*
* ajax options can also be overriden on every get() or post()
*
* @param options {Mixed} can take many options, but must include at minimum the API url.
*/
mw.Api = function( options ) {
// make sure we at least have a URL endpoint for the API
if ( options.url === undefined ) {
throw new Error( 'Configuration error - needs url property' );
}
this.url = options.url;
/* We allow people to omit these default parameters from API requests */
// there is very customizable error handling here, on a per-call basis
// wondering, would it be simpler to make it easy to clone the api object, change error handling, and use that instead?
this.defaults = {
parameters: {
action: 'query',
format: 'json'
},
ajax: {
// force toString if we got a mw.Uri object
url: new String( this.url ),
/* default function for success and no API error */
ok: function() {},
// caller can supply handlers for http transport error or api errors
err: function( code, result ) {
mw.log( "mw.Api error: " + code, 'debug' );
},
timeout: 30000, /* 30 seconds */
dataType: 'json'
}
};
if ( options.parameters ) {
$j.extend( this.defaults.parameters, options.parameters );
}
if ( options.ajax ) {
$j.extend( this.defaults.ajax, options.ajax );
}
};
mw.Api.prototype = {
/**
* For api queries, in simple cases the caller just passes a success callback.
* In complex cases they pass an object with a success property as callback and probably other options.
* Normalize the argument so that it's always the latter case.
*
* @param {Object|Function} ajax properties, or just a success function
* @return Function
*/
normalizeAjaxOptions: function( arg ) {
if ( typeof arg === 'function' ) {
var ok = arg;
arg = { 'ok': ok };
}
if (! arg.ok ) {
throw Error( "ajax options must include ok callback" );
}
return arg;
},
/**
* Perform API get request
*
* @param {Object} request parameters
* @param {Object|Function} ajax properties, or just a success function
*/
get: function( parameters, ajaxOptions ) {
ajaxOptions = this.normalizeAjaxOptions( ajaxOptions );
ajaxOptions.type = 'GET';
this.ajax( parameters, ajaxOptions );
},
/**
* Perform API post request
* TODO post actions for nonlocal will need proxy
*
* @param {Object} request parameters
* @param {Object|Function} ajax properties, or just a success function
*/
post: function( parameters, ajaxOptions ) {
ajaxOptions = this.normalizeAjaxOptions( ajaxOptions );
ajaxOptions.type = 'POST';
this.ajax( parameters, ajaxOptions );
},
/**
* Perform the API call.
*
* @param {Object} request parameters
* @param {Object} ajax properties
*/
ajax: function( parameters, ajaxOptions ) {
parameters = $j.extend( {}, this.defaults.parameters, parameters );
ajaxOptions = $j.extend( {}, this.defaults.ajax, ajaxOptions );
// Some deployed MediaWiki >= 1.17 forbid periods in URLs, due to an IE XSS bug
// So let's escape them here. See bug #28235
// This works because jQuery accepts data as a query string or as an Object
ajaxOptions.data = $j.param( parameters ).replace( /\./g, '%2E' );
ajaxOptions.error = function( xhr, textStatus, exception ) {
ajaxOptions.err( 'http', { xhr: xhr, textStatus: textStatus, exception: exception } );
};
/* success just means 200 OK; also check for output and API errors */
ajaxOptions.success = function( result ) {
if ( result === undefined || result === null || result === '' ) {
ajaxOptions.err( "ok-but-empty", "OK response but empty result (check HTTP headers?)" );
} else if ( result.error ) {
var code = result.error.code === undefined ? 'unknown' : result.error.code;
ajaxOptions.err( code, result );
} else {
ajaxOptions.ok( result );
}
};
$j.ajax( ajaxOptions );
}
};
/**
* This is a list of errors we might receive from the API.
* For now, this just documents our expectation that there should be similar messages
* available.
*/
mw.Api.errors = [
/* occurs when POST aborted - jQuery 1.4 can't distinguish abort or lost connection from 200 OK + empty result */
'ok-but-empty',
// timeout
'timeout',
/* really a warning, but we treat it like an error */
'duplicate',
'duplicate-archive',
/* upload succeeded, but no image info.
this is probably impossible, but might as well check for it */
'noimageinfo',
/* remote errors, defined in API */
'uploaddisabled',
'nomodule',
'mustbeposted',
'badaccess-groups',
'stashfailed',
'missingresult',
'missingparam',
'invalid-file-key',
'copyuploaddisabled',
'mustbeloggedin',
'empty-file',
'file-too-large',
'filetype-missing',
'filetype-banned',
'filename-tooshort',
'illegal-filename',
'verification-error',
'hookaborted',
'unknown-error',
'internal-error',
'overwrite',
'badtoken',
'fetchfileerror',
'fileexists-shared-forbidden'
];
/**
* This is a list of warnings we might receive from the API.
* For now, this just documents our expectation that there should be similar messages
* available.
*/
mw.Api.warnings = [
'duplicate',
'exists'
];
}) ( window.mediaWiki, jQuery );

View file

@ -0,0 +1,29 @@
// library to assist with action=parse, that is, get rendered HTML of wikitext
( function( mw, $ ) {
$.extend( mw.Api.prototype, {
/**
* Parse wikitext into HTML
* @param {String} wikitext
* @param {Function} callback to which to pass success HTML
* @param {Function} callback if error (optional)
*/
parse: function( wikiText, useHtml, error ) {
var params = {
text: wikiText,
action: 'parse'
};
var ok = function( data ) {
if ( data && data.parse && data.parse.text && data.parse.text['*'] ) {
useHtml( data.parse.text['*'] );
}
};
this.get( params, ok, error );
}
} ); // end extend
} )( window.mediaWiki, jQuery );

View file

@ -0,0 +1,48 @@
// library to assist with API calls on titleblacklist
( function( mw, $ ) {
// cached token so we don't have to keep fetching new ones for every single post
var cachedToken = null;
$.extend( mw.Api.prototype, {
/**
* @param {mw.Title}
* @param {Function} callback to pass false on Title not blacklisted, or error text when blacklisted
* @param {Function} optional callback to run if api error
* @return ajax call object
*/
isBlacklisted: function( title, callback, err ) {
var params = {
'action': 'titleblacklist',
'tbaction': 'create',
'tbtitle': title.toString()
};
var ok = function( data ) {
// this fails open (if nothing valid is returned by the api, allows the title)
// also fails open when the API is not present, which will be most of the time.
if ( data.titleblacklist && data.titleblacklist.result && data.titleblacklist.result == 'blacklisted') {
var result;
if ( data.titleblacklist.reason ) {
result = {
reason: data.titleblacklist.reason,
line: data.titleblacklist.line,
message: data.titleblacklist.message
};
} else {
mw.log("mw.Api.titleblacklist::isBlacklisted> no reason data for blacklisted title", 'debug');
result = { reason: "Blacklisted, but no reason supplied", line: "Unknown" };
}
callback( result );
} else {
callback ( false );
}
};
return this.get( params, ok, err );
}
} );
} )( window.mediaWiki, jQuery );

View file

@ -0,0 +1,138 @@
( function( mw, $, undefined ) {
/**
* Thingy for collecting user feedback on a wiki page
* @param {mw.Api} api properly configured to talk to this wiki
* @param {mw.Title} the title of the page where you collect feedback
* @param {id} a string identifying this feedback form to separate it from others on the same page
*/
mw.Feedback = function( api, feedbackTitle ) {
var _this = this;
this.api = api;
this.feedbackTitle = feedbackTitle;
this.setup();
};
mw.Feedback.prototype = {
setup: function() {
var _this = this;
// Set up buttons for dialog box. We have to do it the hard way since the json keys are localized
_this.buttons = {};
_this.buttons[ gM( 'mwe-upwiz-feedback-cancel' ) ] = function() { _this.cancel(); };
_this.buttons[ gM( 'mwe-upwiz-feedback-submit' ) ] = function() { _this.submit(); };
var $feedbackPageLink = $j( '<a></a>' ).attr( { 'href': _this.feedbackTitle.getUrl(), 'target': '_blank' } );
this.$dialog =
$( '<div style="position:relative;"></div>' ).append(
$( '<div class="mwe-upwiz-feedback-mode mwe-upwiz-feedback-form"></div>' ).append(
$( '<div style="margin-top:0.4em;"></div>' ).append(
$( '<small></small>' ).msg( 'mwe-upwiz-feedback-note',
_this.feedbackTitle.getNameText(),
$feedbackPageLink )
),
$( '<div style="margin-top:1em;"></div>' ).append(
gM( 'mwe-upwiz-feedback-subject' ),
$( '<br/>' ),
$( '<input type="text" class="mwe-upwiz-feedback-subject" name="subject" maxlength="60" style="width:99%;"/>' )
),
$( '<div style="margin-top:0.4em;"></div>' ).append(
gM( 'mwe-upwiz-feedback-message' ),
$( '<br/>' ),
$( '<textarea name="message" class="mwe-upwiz-feedback-message" style="width:99%;" rows="5" cols="60"></textarea>' )
)
),
$( '<div class="mwe-upwiz-feedback-mode mwe-upwiz-feedback-submitting" style="text-align:center;margin:3em 0;"></div>' ).append(
gM( 'mwe-upwiz-feedback-adding' ),
$( '<br/>' ),
$( '<img src="http://upload.wikimedia.org/wikipedia/commons/4/42/Loading.gif" />' )
),
$( '<div class="mwe-upwiz-feedback-mode mwe-upwiz-feedback-error" style="position:relative;"></div>' ).append(
$( '<div class="mwe-upwiz-feedback-error-msg style="color:#990000;margin-top:0.4em;"></div>' )
)
).dialog({
width: 500,
autoOpen: false,
title: gM( 'mwe-upwiz-feedback-title' ),
modal: true,
buttons: _this.buttons
});
this.subjectInput = this.$dialog.find( 'input.mwe-upwiz-feedback-subject' ).get(0);
this.messageInput = this.$dialog.find( 'textarea.mwe-upwiz-feedback-message' ).get(0);
this.displayForm();
},
display: function( s ) {
this.$dialog.dialog( { buttons:{} } ); // hide the buttons
this.$dialog.find( '.mwe-upwiz-feedback-mode' ).hide(); // hide everything
this.$dialog.find( '.mwe-upwiz-feedback-' + s ).show(); // show the desired div
},
displaySubmitting: function() {
this.display( 'submitting' );
},
displayForm: function( contents ) {
this.subjectInput.value = (contents && contents.subject) ? contents.subject : '';
this.messageInput.value = (contents && contents.message) ? contents.message : '';
this.display( 'form' );
this.$dialog.dialog( { buttons: this.buttons } ); // put the buttons back
},
displayError: function( message ) {
this.display( 'error' );
this.$dialog.find( '.mwe-upwiz-feedback-error-msg' ).msg( message );
},
cancel: function() {
this.$dialog.dialog( 'close' );
},
submit: function() {
var _this = this;
// get the values to submit
var subject = this.subjectInput.value;
var message = "<small>User agent: " + navigator.userAgent + "</small>\n\n"
+ this.messageInput.value;
if ( message.indexOf( '~~~' ) == -1 ) {
message += " ~~~~";
}
this.displaySubmitting();
var ok = function( result ) {
if ( result.edit !== undefined ) {
if ( result.edit.result === 'Success' ) {
_this.$dialog.dialog( 'close' ); // edit complete, close dialog box
} else {
_this.displayError( 'mwe-upwiz-feedback-error1' ); // unknown API result
}
} else {
displayError( 'mwe-upwiz-feedback-error2' ); // edit failed
}
};
var err = function( code, info ) {
displayError( 'mwe-upwiz-feedback-error3' ); // ajax request failed
};
this.api.newSection( this.feedbackTitle, subject, message, ok, err );
}, // close submit button function
launch: function( contents ) {
this.displayForm( contents );
this.$dialog.dialog( 'open' );
this.subjectInput.focus();
}
};
} )( window.mediaWiki, jQuery );