MediaWiki:Gadget-libLua.js
Appearance
This page is loaded as a part of the libLua gadget, a hidden gadget used by libSensitiveIPs. |
/* ___________________________________________________________________________
* | |
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes on the talk page or on [[Wikipedia_talk:Gadget]] |
* | before editing. |
* |___________________________________________________________________________|
*
*
* libLua provides functions for interacting with Lua modules from JavaScript.
*
*
* === USAGE ===
*
* The library should be loaded as a MediaWiki gadget, using mw.loader.load,
* mw.loader.using, or similar. The name of the gadget is
* "ext.gadget.libLua". Once the gadget is loaded, you can access its
* functions from mw.libs.lua.<function name>. Documentation for the
* functions can be found in the JSDoc comment blocks in the library code. For
* example:
*
* // Call p.main("foo", "bar") in [[Module:Example]]
* mw.loader.using( [ 'ext.gadget.libLua' ], function () {
* mw.libs.lua.call( {
* module: 'Example',
* func: 'main',
* args: [ 'foo', 'bar' ]
* } ).then( function ( result ) {
* // Do something with the result
* } );
* } );
*
*
* === LICENCE ===
*
* Author: Mr. Stradivarius
* Licence: MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2016 Mr. Stradivarius
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
( function ( $, mw, undefined ) {
'use strict';
/**
* Encode a string for including in a Lua question.
* At the moment this is just a wrapper for JSON.stringify, as that does
* what we need. However, encoding a Lua string is conceptually different
* from encoding JSON, so we use different function names for the two tasks.
* This will also make it easier to update the code in the future, if
* necessary.
* @private
*/
function makeLuaString( s ) {
return JSON.stringify( s );
}
/**
* Make a Lua question string from a module name, a function name and an
* optional args array.
* @private
*/
function makeQuestion( module, func, args ) {
var escapedModule = makeLuaString( 'Module:' + module ),
escapedFunc = makeLuaString( func ),
json, escapedJson, argString;
if ( args ) {
json = JSON.stringify( args );
escapedJson = makeLuaString( json );
argString = 'unpack(mw.text.jsonDecode(' + escapedJson + '))';
} else {
argString = '';
}
return '=require(' + escapedModule + ')[' + escapedFunc + '](' + argString + ')';
}
/**
* Reject a deferred object with the specified error code and error message.
* If no deferred object is supplied with the third parameter, a new one is
* created. We use this particular format for the error objects as it is the
* same one used by the MediaWiki API, and so clients will only have to
* worry about errors being formatted in one way.
* @private
*/
function rejectDeferred( code, msg, deferred ) {
if ( !deferred ) {
deferred = $.Deferred();
}
return deferred.reject(
code,
{ error: {
code: code,
info: msg
} }
);
}
mw.libs.lua = {
/**
* Call a function in a Lua module. The function call is made
* asynchronously through the MediaWiki Action API, and its result is
* wrapped in a jQuery promise.
*
* @param {Object} options
*
* @param {string} options.module - The name of the module to load.
* (Don't use a "Module:" prefix.)
*
* @param {string} options.func - The name of the function to call.
* Only strings are accepted as function names.
*
* @param {*[]} [options.args] - An array of arguments to pass to the
* function. These must be serializable as JSON. The arguments will be
* unpacked when passed to the function; when calling a function "func",
* an args array of ["foo", "bar", "baz"] will be called as
* func("foo", "bar", "baz"). There are limitations in what can be
* decoded from JSON in Lua: for example, keys may be dropped from
* arrays containing null values. See
* https://www.mediawiki.org/wiki/Extension:Scribunto/Lua_reference_manual#mw.text.jsonDecode
* for more details. For this reason, calls like func('foo', nil, 'bar')
* cannot be made directly. To work around this you can define an
* intermediary function in a Lua module that calls the desired function
* indirectly, and then call that function from this library instead.
*
* @param {('string'|'json')} [options.format=string] - The expected
* return format. If this is "string" or undefined, then the return value
* will be a string. (If the Lua function call returns a non-string value
* it will be converted to a string, and if the function call returns
* multiple values then they will be converted to strings and
* concatenated with tabs as separators.) If this is "json", then the
* return string from the function call is assumed to be JSON, and is
* converted to a JavaScript object using JSON.parse. If the return
* string is not valid JSON, the promise returned from the function is
* rejected, but no error is thrown.
*
* @param {mw.Api} [options.api] - An mw.Api object to use for API
* calls. If this is not specified, a new mw.Api object using default
* values is created.
*
* @return {$.Promise}
* A jQuery Promise that is resolved with the result of the function
* call.
*
@example
// Load the gadget
mw.loader.using( 'ext.gadget.libLua', function () {
// Call p.main( "foo", "bar", "baz" ) in Module:Example.
mw.libs.lua.call( {
"module": "Example",
"func": "main",
"args": [ "foo", "bar", "baz" ]
} ).done( function( resultString ) {
doSomething( resultString );
} );
} );
*
@example
// Load the gadget
mw.loader.using( 'ext.gadget.libLua', function () {
// Call p.getJson( "foo" ) in Module:Example.
mw.libs.lua.call( {
"format": "json",
"module": "Example",
"func": "getJson",
"args": [ "foo" ]
} ).done( function( data ) {
doSomething( data.bar.baz );
} );
} );
*
*/
call: function ( options ) {
// Deal with bad arguments
if ( !( options instanceof Object ) ) {
return rejectDeferred(
'liblua-call-options-type-error',
"type error in arg #1 to 'call' (object expected)"
);
} else if ( typeof options.module !== 'string' ) {
return rejectDeferred(
'liblua-call-module-type-error',
'type error in options.module (string expected)'
);
} else if ( typeof options.func !== 'string' ) {
return rejectDeferred(
'liblua-call-func-type-error',
'type error in options.func (string expected)'
);
} else if ( options.args !== undefined && !$.isArray( options.args ) ) {
return rejectDeferred(
'liblua-call-invalid-args',
'options.args was defined but was not an array'
);
} else if ( options.format !== undefined
&& options.format !== 'json'
&& options.format !== 'string' ) {
return rejectDeferred(
'liblua-call-format-type-error',
"invalid format specified (must be 'json', 'string' or undefined)"
);
} else if ( options.api !== undefined && !( options.api instanceof mw.Api ) ) {
return rejectDeferred(
'liblua-call-invalid-api-object',
'options.api is not a valid mw.Api object.'
);
}
// Generate a new API object if we weren't passed one.
var api = options.api || new mw.Api();
// Make the API call.
// The title field in scribunto-console doesn't seem to allow us to
// use the p variable to load the module content, so set it to a
// dummy value with blank content and load the module in the
// question instead.
return api.postWithToken( 'csrf', {
action: 'scribunto-console',
format: 'json',
title: 'Example',
content: '',
question: makeQuestion( options.module, options.func, options.args ),
clear: true
} ).then( function ( obj ) {
// Wrap the API query in a new jQuery Deferred object so that
// we can reject API results that are invalid Lua but not
// treated as errors by the API.
return $.Deferred( function ( deferred ) {
// Deal with any errors from the API or from Lua.
if ( obj.type === 'error' ) {
// Lua command failed but API call succeeded
return rejectDeferred(
obj.messagename,
obj.message,
deferred
);
} else if ( obj.error ) {
// API call failed
return deferred.reject( obj.error.code, obj );
} else if ( obj.type !== 'normal' ) {
// Unknown API response
return rejectDeferred(
'liblua-call-unknown-api-response',
'Unknown API response',
deferred
);
}
var result = obj['return'];
// Try to parse JSON if options.format equals 'json'
if ( options.format == 'json' ) {
try {
result = JSON.parse( result );
} catch ( e ) {
if ( e instanceof SyntaxError ) {
return rejectDeferred(
'liblua-call-json-syntax-error',
'The Lua function call returned invalid JSON: ' + e.message,
deferred
);
} else {
return rejectDeferred(
'liblua-call-json-unexpected-error',
'An unexpected error occurred while trying to ' +
'parse the JSON returned from the Lua function call',
deferred
);
}
}
}
return deferred.resolve( result );
} ).promise();
} );
}
};
} )( jQuery, mediaWiki );