From cb0cf72eba9ff456b4c4e0b5ca54f498265cf82f Mon Sep 17 00:00:00 2001 From: Neil Kandalgaonkar Date: Fri, 9 Dec 2011 04:48:39 +0000 Subject: [PATCH] add JS api, feedback libs from UW into core. --- resources/Resources.php | 35 +++ resources/mediawiki/mediawiki.api.category.js | 106 +++++++++ resources/mediawiki/mediawiki.api.edit.js | 117 ++++++++++ resources/mediawiki/mediawiki.api.js | 208 ++++++++++++++++++ resources/mediawiki/mediawiki.api.parse.js | 29 +++ .../mediawiki/mediawiki.api.titleblacklist.js | 48 ++++ resources/mediawiki/mediawiki.feedback.js | 138 ++++++++++++ 7 files changed, 681 insertions(+) create mode 100644 resources/mediawiki/mediawiki.api.category.js create mode 100644 resources/mediawiki/mediawiki.api.edit.js create mode 100644 resources/mediawiki/mediawiki.api.js create mode 100644 resources/mediawiki/mediawiki.api.parse.js create mode 100644 resources/mediawiki/mediawiki.api.titleblacklist.js create mode 100644 resources/mediawiki/mediawiki.feedback.js diff --git a/resources/Resources.php b/resources/Resources.php index 5f10a715dd..d6d131091e 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -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', ), diff --git a/resources/mediawiki/mediawiki.api.category.js b/resources/mediawiki/mediawiki.api.category.js new file mode 100644 index 0000000000..269d852f1a --- /dev/null +++ b/resources/mediawiki/mediawiki.api.category.js @@ -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 ); diff --git a/resources/mediawiki/mediawiki.api.edit.js b/resources/mediawiki/mediawiki.api.edit.js new file mode 100644 index 0000000000..db8a6a38db --- /dev/null +++ b/resources/mediawiki/mediawiki.api.edit.js @@ -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 ); diff --git a/resources/mediawiki/mediawiki.api.js b/resources/mediawiki/mediawiki.api.js new file mode 100644 index 0000000000..e093b59aa8 --- /dev/null +++ b/resources/mediawiki/mediawiki.api.js @@ -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 ); diff --git a/resources/mediawiki/mediawiki.api.parse.js b/resources/mediawiki/mediawiki.api.parse.js new file mode 100644 index 0000000000..7bcc4bb376 --- /dev/null +++ b/resources/mediawiki/mediawiki.api.parse.js @@ -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 ); + + diff --git a/resources/mediawiki/mediawiki.api.titleblacklist.js b/resources/mediawiki/mediawiki.api.titleblacklist.js new file mode 100644 index 0000000000..95bbc4f0d5 --- /dev/null +++ b/resources/mediawiki/mediawiki.api.titleblacklist.js @@ -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 ); diff --git a/resources/mediawiki/mediawiki.feedback.js b/resources/mediawiki/mediawiki.feedback.js new file mode 100644 index 0000000000..6c7a997726 --- /dev/null +++ b/resources/mediawiki/mediawiki.feedback.js @@ -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( '' ).attr( { 'href': _this.feedbackTitle.getUrl(), 'target': '_blank' } ); + this.$dialog = + $( '
' ).append( + $( '
' ).append( + $( '
' ).append( + $( '' ).msg( 'mwe-upwiz-feedback-note', + _this.feedbackTitle.getNameText(), + $feedbackPageLink ) + ), + $( '
' ).append( + gM( 'mwe-upwiz-feedback-subject' ), + $( '
' ), + $( '' ) + ), + $( '
' ).append( + gM( 'mwe-upwiz-feedback-message' ), + $( '
' ), + $( '' ) + ) + ), + $( '
' ).append( + gM( 'mwe-upwiz-feedback-adding' ), + $( '
' ), + $( '' ) + ), + $( '
' ).append( + $( '
' ) + + ) + ).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 = "User agent: " + navigator.userAgent + "\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 ); -- 2.20.1