/**
* JS-XMLRPC: Yet Another JSONRPC Library, in Javascript!
*
* ...as if the world needed it...
*
* FOR COMPLETE API DOCS, READ PHP-XMLRPC API DOCS. THE SAME API (almost) IS IMPLEMENTED HERE!
*
* Many thanks to Jan-Klaas Kollhof for JSOLAIT, and to the Yahoo YUI team, for providing the starting point for all of this
*
* @author G. Giunta
* @copyright (c) 2006-2022 G. Giunta
* @license code licensed under the BSD License: see LICENSE file
*
* KNOWN DIFFERENCES FROM PHP-XMLRPC:
* + jsonrpc_parse_resp() defaults to native parsing
*
* @todo json parsing code in json_parse() - do we need it al all in this time and age?
*/
// Requires: xmlrpc_lib.js
import {
base64_decode,
base64_encode,
htmlentities,
var_export,
xh,
xmlrpc_client,
xmlrpc_debug_log,
xmlrpcerr,
xmlrpcmsg,
xmlrpcresp,
xmlrpcstr,
xmlrpcval
} from './xmlrpc_lib.js'
/**
* @private
* @todo add support for charset transcoding
*/
function json_encode_entities(data, src_encoding, dest_encoding)
{
if (data == undefined) // catches case of data === null as well
{
return '';
}
return data.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\u002F/g, '\\/').replace(/\t/g, '\\t').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\u0008/g, '\\b').replace(/\v/g, '\\v').replace(/\f/g, '\\f');
}
/**
* @private
*/
function json_parse(data, return_jsvals = false, src_encoding = 'UTF-8', dest_encoding = 'ISO-8859-1')
{
xh.isf_reason = 'non-native JSON parsing not yet implemented.';
return false;
}
/**
* @private
*/
function json_parse_native(data)
{
try
{
var out = JSON.parse(data);
xh.value = out;
return true;
}
catch (e)
{
xh.isf_reason = 'JSON parsing failed';
return false;
}
}
/**
* @private
*/
function jsonrpc_parse_resp(data, return_jsvals = false, use_native_parsing = true)
{
xh.isf = 0;
xh.isf_reason = '';
if (use_native_parsing)
{
var ok = json_parse_native(data);
// we encode js vals to jsonrpcvals later, if needed
//if (!return_jsvals)
//{
// xh.value = jsonrpc_encode(xh.value);
//}
}
else
{
var ok = json_parse(data, return_jsvals);
}
if (ok)
{
//if (!return_jsvals)
//{
// xh.value = xh.value.me;
//}
if (typeof(xh.value) !== 'object' || xh.value['result'] === undefined
|| xh.value['error'] === undefined || xh.value['id'] === undefined)
{
//xh.isf = 2;
xh.isf_reason = 'JSON parsing did not return correct jsonrpc response object';
return false;
}
//if (!return_jsvals)
//{
// var d_error = jsonrpc_decode(xh.value['error']);
// xh.value['id'] = php_jsonrpc_decode(xh.value['id']);
//}
//else
//{
var d_error = xh.value['error'];
//}
xh.id = xh.value['id'];
if (d_error != null)
{
xh.isf = 1;
//xh.value = $d_error;
if (typeof(d_error) === 'object' && d_error['faultCode'] !== undefined
&& d_error['faultString'] !== undefined)
{
if (d_error['faultCode'] == 0)
{
// FAULT returned, errno needs to reflect that
d_error['faultCode'] = -1;
}
xh.value = d_error;
}
// NB: what about jsonrpc servers that do NOT respect
// the faultCode/faultString convention???
// we force the error into a string. regardless of type...
else //if (is_string(xh.value))
{
if (return_jsvals)
{
xh.value = {'faultCode': -1, 'faultString': var_export(xh.value['error'])};
}
else
{
xh.value = {'faultCode': -1, 'faultString': serialize_jsonrpcval(jsonrpc_encode(xh.value['error']))};
}
}
}
else
{
if (!return_jsvals)
xh.value = jsonrpc_encode(xh.value['result']);
else
xh.value = xh.value['result'];
}
return true;
}
else
{
return false;
}
}
// *****************************************************************************
/**
* @constructor
**/
export function jsonrpc_client(path, server, port, method)
{
this.no_multicall = true; // by default, multicall is not supported in jsonrpc
this.return_type = 'jsonrpcvals';
this.init(path, server, port, method);
}
jsonrpc_client.prototype = new xmlrpc_client();
// *****************************************************************************
/**
* @param {string} meth Name of the method to be invoked
* @param {array} pars list of parameters for method call (jsonrpcval objects)
* @param {any} id of method call. Either a string, number or boolean or null. NULL has a special meaning for json-rpc
* @constructor
*/
export function jsonrpcmsg(meth, pars, id)
{
this.id = null;
/** @private **/
this.params = []; // somehow needed for making this weird subclassing work
/** @private **/
this.content_type = 'application/json';
if (id !== undefined)
{
this.id = id;
}
this.init(meth, pars);
}
// let jsonrpcresp inherit methods from xmlrpcresp
jsonrpcmsg.prototype = new xmlrpcmsg();
/**
* @private
*/
jsonrpcmsg.prototype.parseResponse = function(data, headers_processed = false, return_type = 'jsonrpcvals')
{
var headers = '';
if (typeof(headers_processed) === 'string')
{
headers = headers_processed;
headers_processed = true;
}
if (this.debug)
{
xmlrpc_debug_log('<PRE>---GOT---\n' + htmlentities(data) + '\n---END---\n</PRE>');
}
if (data == '')
{
xmlrpc_error_log('XML-RPC: jsonrpcmsg::parseResponse: no response received from server.');
var r = new jsonrpcresp(0, xmlrpcerr['no_data'], xmlrpcstr['no_data']);
return r;
}
xh.reset = {headers: [], cookies: []};
var raw_data = data;
// examining http headers: check first if given as second param to function
if (headers != '')
{
var r = this.parseResponseHeaders(headers, true);
}
// else check if http headers given as part of complete html response
else if (data.slice(0, 4) == 'HTTP')
{
// if it was so, remove them (or return an error response, if parsing fails)
var r = this.ParseResponseHeaders(data, headers_processed);
if (typeof(r) !== 'string')
{
r.raw_data = data;
return r;
}
else
{
data = r;
}
}
if (this.debug)
{
var start = data.indexOf('/* SERVER DEBUG INFO (BASE64 ENCODED):');
if (start != -1)
{
start += 39; //new String('<!-- SERVER DEBUG INFO (BASE64 ENCODED):').length();
var end = data.indexOf('*/', start);
var comments = data.slice(start, end-1);
xmlrpc_debug_log('<PRE>---SERVER DEBUG INFO (DECODED)---\n\t'+htmlentities(base64_decode(comments).replace(/\n/g, '\n\t'))+'\n---END---\n</PRE>');
}
}
// be tolerant of extra whitespace in response body
data = data.replace(/^\s/, '').replace(/\s$/, '');
// be tolerant of junk after methodResponse (e.g. javascript ads automatically inserted by free hosts)
var pos = data.lastIndexOf('}');
if (pos >= 0)
{
data = data.slice(0, pos+17);
}
// if user wants back raw json, give it to him
if (return_type == 'json')
{
var r = new jsonrpcresp(data, 0, '', 'json');
r.hdrs = xh.headers;
r._cookies = xh.cookies;
r.raw_data = raw_data;
return r;
}
// @todo shall we try to check for non-unicode json received ???
if (!jsonrpc_parse_resp(data, return_type=='jsvals'))
{
if (this.debug)
{
/// @todo echo something for user?
}
var r = new jsonrpcresp(0, xmlrpcerr['invalid_return'],
xmlrpcstr['invalid_return'] + ' ' + xh.isf_reason);
}
//elseif ($return_type == 'jsonrpcvals' && !is_object($GLOBALS['_xh']['value']))
//{
// // then something odd has happened
// // and it's time to generate a client side error
// // indicating something odd went on
// $r = & new jsonrpcresp(0, $GLOBALS['xmlrpcerr']['invalid_return'],
// $GLOBALS['xmlrpcstr']['invalid_return']);
//}
else
{
var v = xh.value;
if (this.debug)
{
xmlrpc_debug_log("<PRE>---PARSED---\n");
xmlrpc_debug_log(var_export(v));
xmlrpc_debug_log("\n---END---</PRE>");
}
if (xh.isf)
{
var r = new jsonrpcresp(0, v['faultCode'], v['faultString']);
}
else
{
var r = new jsonrpcresp(v, 0, '', return_type);
}
r.id = xh.id;
}
r.hdrs = xh.headers;
r._cookies = xh.cookies;
r.raw_data = raw_data;
return r;
}
/**
* @private
*/
jsonrpcmsg.prototype.createPayload = function(charset_encoding)
{
/// @ todo: verify if all chars are allowed for method names or can we just skip the js encoding on it?
this.payload = '{\n"method": "' + json_encode_entities(this.methodname, '', charset_encoding) + '",\n"params": [ ';
for(var i = 0; i < this.params.length; ++i)
{
// NB: we try to force serialization as json even though the object
// param might be a plain xmlrpcval object.
// This way we do not need to override addParam, aren't we lazy?
this.payload += '\n ' + serialize_jsonrpcval(this.params[i], charset_encoding) + ',';
}
this.payload = this.payload.slice(0, -1) + '\n],\n"id": ' + (this.id == null ? 'null' : this.id) + '\n}\n';
}
// *****************************************************************************
/**
* @constructor
*/
export function jsonrpcresp(val, fcode, fstr, valtyp)
{
this.id = null;
this.init(val, fcode, fstr, valtyp);
}
// let jsonrpcresp inherit methods, default values, from xmlrpcresp
jsonrpcresp.prototype = new xmlrpcresp();
/**
* @private
*/
jsonrpcresp.prototype.serialize = function(charset_encoding)
{
this.payload = serialize_jsonrpcresp(this, this.id, charset_encoding);
return this.payload;
}
// *****************************************************************************
/**
* Create a jsonrpcval object out of a plain javascript value
* @param {any} val
* @param {string} type Any valid json type name (lowercase). If null, 'string' is assumed
* @constructor
*/
export function jsonrpcval(val, type)
{
this.init(val, type);
}
// let jsonrpcval inherit from xmlrpcval
jsonrpcval.prototype = new xmlrpcval();
/**
* @private
*/
jsonrpcval.prototype.serialize = function(charset_encoding)
{
return serialize_jsonrpcval(this, charset_encoding);
}
// *****************************************************************************
/**
* Takes a json value in jsonrpcval object format
* and translates it into native javascript types.
* @public
**/
export function jsonrpc_decode(jsonrpc_val, options = [])
{
switch(jsonrpc_val.kindOf())
{
case 'scalar':
return jsonrpc_val.scalarVal();
case 'array':
var size = jsonrpc_val.arraySize();
var arr = [];
for(var i = 0; i < size; ++i)
{
arr[arr.length] = jsonrpc_decode(jsonrpc_val.arrayMem(i), options);
}
return arr;
case 'struct':
// If user said so, try to rebuild js objects for specific struct vals.
/// @todo should we raise a warning for class not found?
// shall we check for proper subclass of xmlrpcval instead of
// presence of _php_class to detect what we can do?
if (options['decode_js_objs'] && jsonrpc_val._js_class != '')
//&& class_exists($xmlrpc_val->_php_class)) /// @todo check if a class exists with given name
{
var obj = new jsonrpc_val._js_class;
}
else
{
var obj = {};
}
for(var key in jsonrpc_val.me)
{
obj[key] = jsonrpc_decode(jsonrpc_val.me[key], options);
}
return obj;
case 'msg':
var paramcount = jsonrpc_val.getNumParams();
var arr = [];
for(var i = 0; i < paramcount; ++i)
{
arr[arr.length] = jsonrpc_val(jsonrpc_val.getParam(i));
}
return arr;
}
}
/**
* Takes native javascript types and encodes them into jsonrpc object format.
* It will not re-encode jsonrpcval objects.
* @public
**/
export function jsonrpc_encode(js_val, options)
{
var type = typeof js_val;
switch(type)
{
case 'string':
//if ((options != undefined && options['auto_dates']) && js_val.search(/^[0-9]{8}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/) != -1)
// var xmlrpc_val = new xmlrpcval(js_val, 'dateTime.iso8601');
//else
var jsonrpc_val = new jsonrpcval(js_val, 'string');
break;
case 'number':
/// @todo...
var num = new Number(js_val);
if (num == parseInt(num))
{
var jsonrpc_val = new jsonrpcval(js_val, 'int');
}
else //if (num == parseFloat(num))
{
var jsonrpc_val = new jsonrpcval(js_val, 'double');
}
//else
//{
// // ??? only NaN and Infinity can get here. Encode them as zero (double)...
// var xmlrpc_val = new xmlrpcval(0, 'double');
//}
break;
case 'boolean':
var jsonrpc_val = new jsonrpcval(js_val, 'boolean');
break;
case 'object':
// we should be able to use js_val instanceof Null, but FF refuses it...
// nb: check nulls first, since they have no attributes
if (js_val === null)
{
//if (options != undefined && options['null_extension'])
//{
var jsonrpc_val = new jsonrpcval(null, 'null');
//}
//else
//{
// var xmlrpc_val = new xmlrpcval();
//}
}
else
if (js_val.toJsonRpcVal)
{
var jsonrpc_val = js_val.toJsonRpcVal();
}
else
if (js_val instanceof Array)
{
var arr = [];
for(var i = 0; i < js_val.length; ++i)
{
arr[arr.length] = jsonrpc_encode(js_val[i], options);
}
var jsonrpc_val = new jsonrpcval(arr, 'array');
}
else
// xmlrpcval acquired capability to do this on its own, declaring toXmlRpcVal()
//if (js_val instanceof xmlrpcval)
//{
// var xmlrpc_val = js_val;
//}
//else
{
// generic js object. encode all members except functions
var arr = {};
for(var attr in js_val)
{
if (typeof js_val[attr] !== 'function')
{
arr[attr] = jsonrpc_encode(js_val[attr], options);
}
}
var jsonrpc_val = new jsonrpcval(arr, 'struct');
/*if (in_array('encode_php_objs', options))
{
// let's save original class name into xmlrpcval:
// might be useful later on...
$xmlrpc_val._php_class = get_class($php_val);
}*/
}
break;
// match 'function', 'undefined', ...
default:
// it has to return an empty object in case
var jsonrpc_val = new jsonrpcval();
break;
}
return jsonrpc_val;
}
/**
* Convert the json representation of a jsonrpc method call, jsonrpc method response
* or single json value into the appropriate object (deserialize)
* @param {string} json_val
* @param {object} options not used (yet)
* @type false | jsonrpcresp | jsonrpcmsg | jsonrpcval
* @public
*
* @bug cannot tell a jsonrpc object from a reponse/request, if the object contains
* the same members
**/
export function jsonrpc_decode_json(json_val, options)
{
//if (typeof(options) === 'object')
//{
// src_encoding = options['src_encoding'] != undefined ? options['src_encoding'] : xmlrpc_defencoding;
// dest_encoding = options['dest_encoding'] != undefined ? options['dest_encoding'] : xmlrpc_internalencoding;
//}
xh.reset = {};
//xh.isf = 0;
//if (!json_parse(json_val, false, src_encoding, dest_encoding))
if (!json_parse_native(json_val))
{
xmlrpc_error_log(xh.isf_reason);
return false;
}
else
{
if (typeof(xh.value) === 'object' )
{
if (/*xh.value.length == 3 &&*/ xh.value['result'] !== undefined
&& xh.value['error'] !== undefined && xh.value['id'] !== undefined)
{
// decoding a jsonrpc reponse. Check first for error case
if (xh.value['error'] != null)
{
if (typeof(xh.value['error']) === 'object' && xh.value['error']['faultCode'] !== undefined
&& xh.value['error']['faultString'] !== undefined)
{
if (xh.value['error']['faultCode'] == 0)
{
// FAULT returned, errno needs to reflect that
xh.value['error']['faultCode'] = -1;
}
}
// NB: what about jsonrpc servers that do NOT respect
// the faultCode/faultString convention???
// we force the error into a string. regardless of type...
else //if (is_string(xh.value))
{
xh.value = {'faultCode': -1, 'faultString': var_export(xh.value['error'])};
}
var r = new jsonrpcresp(0, xh.value['faultCode'], xh.value['faultString']);
}
else
{
var r = new jsonrpcresp(jsonrpc_encode(xh.value['result']));
}
r.id = xh.value['id'];
return r;
}
else if (/*xh.value.length == 3 &&*/ xh.value['method'] !== undefined
&& xh.value['params'] !== undefined && xh.value['id'] !== undefined)
{
var r = new jsonrpcmsg(xh.value['method'], null, xh.value['id']);
for (var i = 0; i < xh.value['params'].length; i++)
r.addParam(jsonrpc_encode(xh.value['params'][i]));
return r;
}
//else
// return new jsonrpcval(xh.value, 'struct');
}
//else
// parsing ok, but not a response / request: it must be a plain jsonrpcval
return jsonrpc_encode(xh.value);
}
}
/**
* Serialize a jsonrpcresp (or xmlrpcresp) as json.
* @private
*/
function serialize_jsonrpcresp(resp, id, charset_encoding)
{
var result = '{\n"id": ' + (id == undefined ? 'null' : id) + ', ';
if (resp.errno)
{
// let non-ASCII response messages be tolerated by clients
// by encoding non ascii chars
result += '"error": { "faultCode": ' + resp.errno + ', "faultString": "' + json_encode_entities(resp.errstr, null, charset_encoding) + '" }, "result": null';
}
else
{
if (typeof resp.val !== 'object' || !(resp.val instanceof xmlrpcval))
{
if (typeof resp.val === 'string' && resp.valtyp == 'json')
{
result += '"error": null, "result": ' + resp.val;
}
else
{
/// @todo try to build something serializable?
//die('cannot serialize jsonrpcresp objects whose content is native php values');
}
}
else
{
result += '"error": null, "result": ' +
serialize_jsonrpcval(resp.val, charset_encoding);
}
}
result += '\n}';
return result;
}
/**
* Serialize a jsonrpcval (or xmlrpcval) as json.
* @private
*/
function serialize_jsonrpcval(value, charset_encoding)
{
var rs = '';
switch(value.mytype)
{
case 1:
rs += '"' + json_encode_entities(value.me, null, charset_encoding) + '"';
break;
case 4:
if (isFinite(value.me))
{
rs += value.me.toFixed(); // as per Ecma-262, toFixed is better than toString...
}
else
{
rs += '0';
}
break;
case 5:
// add a .0 in case value is integer.
// This helps us carrying around floats in js, and keep them separated from ints
if (isFinite(value.me) && value.me !== null)
{
rs += value.me.toString();
var num = new Number(value.me);
if (num == parseInt(num))
{
rs += '.0';
}
}
else
{
rs += '0';
}
break;
case 6:
rs += (value.me ? 'true' : 'false');
break;
case 7:
rs += '"' + value.me + '"'; /// @todo add some date string validation ???
break;
case 8:
// treat base 64 values as strings ???
rs += '"' + base64_encode(value.me) + '"';
break;
case 9:
rs += "null";
break;
case 2:
// array
rs += "[";
var len = (value.me.length);
if (len)
{
for(var i = 0; i < len-1; ++i)
{
rs += serialize_jsonrpcval(value.me[i], charset_encoding);
rs += ",";
}
rs += serialize_jsonrpcval(value.me[i], charset_encoding);
}
rs += "]";
break;
case 3:
// struct
//if ($value->_php_class)
//{
// /// @todo implement json-rpc extension for object serialization
// $rs.='<struct php_class="' . value._php_class . "\">\n";
//}
//else
//{
//}
for(var val in value.me)
{
rs += ',"' + json_encode_entities(val, null, charset_encoding) + '":';
rs += serialize_jsonrpcval(value.me[val], charset_encoding);
}
rs = '{' + rs.slice(1) + '}';
break;
}
return rs;
}