UNPKG

xrpc

Version:

Express middleware and router support for XML-RPC

228 lines (195 loc) 8.98 kB
var xml = require('node-xml'); function XmlRpcParser(events) { this.events = events; this.parser = new xml.SaxParser(this.getHandler()); this.data = { method: false, is_response: false, is_fault: false, params: [] }; this.finished = false; this.complete = false; this.isImplicitString = false; this.elementStack = []; this.cdataStack = [[]]; this.valueStack = []; this.nameStack = []; } XmlRpcParser.prototype.parseString = function parseString(str) { this.parser.parseString(str); return this; }; XmlRpcParser.prototype.finish = function finish() { var instance = this; // The flag `this.finished` serves to debounce this function, which // could be called multiple times: eg, in response to the successful // completion of a document or the end of input (possibly premature). if (instance.finished) { return instance; } instance.finished = true; // XML parsing is all async, so this needs to be as well. setTimeout(function () { if (instance.complete) { instance.events.onDone(instance.data); } else { instance.events.onError('End of input reached before document complete'); } }, 0); return this; }; XmlRpcParser.prototype.getHandler = function getHandler() { var instance = this; return function (cb) { // React to the start of elements, detects responses and faults, as // well as initializing a new struct or array in progress. cb.onStartElementNS(function (elem, attrs, prefix, uri, namespaces) { // Push the current element name onto the stack, along with a // collector for CDATA. instance.elementStack.push(elem); instance.cdataStack.push([]); switch (elem) { // Detect a methodResponse. case 'methodResponse': instance.data.is_response = true; break; // Detect a response that's a fault. case 'fault': instance.data.is_fault = true; break; // Push an empty object onto the stack to collect members case 'struct': instance.valueStack.push({}); break; // Push an empty array onto the stack to collect values case 'array': instance.valueStack.push([]); break; // Assume an implicit string, to begin with. case 'value': instance.isImplicitString = true; break; } }); // Collect CDATA if we're in an element cb.onCharacters(function (chars) { if (instance.cdataStack.length) { instance.cdataStack[instance.cdataStack.length - 1].push(chars); } }); // React to the end of elements. cb.onEndElementNS(function (elem, prefix, uri) { var cdata = instance.cdataStack.pop().join('').trim(), last_elem = instance.elementStack.pop(); switch (elem) { // The end of a call or response means a successful end of parsing, case 'methodCall': case 'methodResponse': instance.complete = true; instance.finish(); break; // Record the method name from CDATA, when encountered. case 'methodName': instance.data.method = cdata; break; // The end of a param element or a fault means the current // value on the stack is complete. case 'param': case 'fault': instance.data.params.push(instance.valueStack.pop()); break; // Push this member name onto the stack for safekeeping // until a value comes along. case 'name': instance.nameStack.push(cdata); break; // The end of a member is where we pick up the last // encountered name and value and stick the pair into the // current struct under construction case 'member': var curr_name = instance.nameStack.pop(), curr_value = instance.valueStack.pop(); instance.valueStack[instance.valueStack.length - 1][curr_name] = curr_value; break; // Explicit string, i4, int, and double values are treated the same. // TODO: Should these be parsed more strictly? // TODO: Lots of repetition here, feels icky. case 'string': case 'i4': case 'int': case 'double': instance.isImplicitString = false; instance.valueStack.push(cdata); break; // Parse a boolean into the JS equivalent. case 'boolean': instance.isImplicitString = false; instance.valueStack.push(('true' == cdata) || (1 == cdata)); break; // Parse a date from its ISO 8601 representation. case 'dateTime.iso8601': instance.isImplicitString = false; instance.valueStack.push(parseISO8601(cdata)); break; // Decode a base64 into its binary form. case 'base64': instance.isImplicitString = false; instance.valueStack.push(decodeBase64(cdata)); break; // The end of an array / struct also cancels implicit string case 'struct': case 'array': instance.isImplicitString = false; break; // The end of a value can mean the end of an implicit // string and also the end of an array item. case 'value': if (instance.isImplicitString) { // No type element found within the value element, // so assume an implicit string. instance.valueStack.push(cdata); } if ('data' == instance.elementStack[instance.elementStack.length - 1]) { // Nested inside a data element, so assume an array. var curr_value = instance.valueStack.pop(); instance.valueStack[instance.valueStack.length - 1].push(curr_value); } break; } }); // React to end of document by calling the onDone handler cb.onEndDocument(function () { instance.finish(); }); // React to an error by calling the onError handler cb.onError(function (msg) { instance.options.onError(msg); }); }; }; // from [Soya.Encode.Base64](http://soya.saltstorm.net) function decodeBase64(sEncoded) { // Input must be dividable with 4. if (!sEncoded || (sEncoded.length % 4) > 0) { return sEncoded; } else if (typeof (atob) != 'undefined') { return atob(sEncoded); } var nBits, i, sDecoded = ''; var base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; sEncoded = sEncoded.replace(/\W|=/g, ''); for (i = 0; i < sEncoded.length; i += 4) { nBits = (base64.indexOf(sEncoded.charAt(i)) & 0xff) << 18 | (base64.indexOf(sEncoded.charAt(i + 1)) & 0xff) << 12 | (base64.indexOf(sEncoded.charAt(i + 2)) & 0xff) << 6 | base64.indexOf(sEncoded.charAt(i + 3)) & 0xff; sDecoded += String.fromCharCode( (nBits & 0xff0000) >> 16, (nBits & 0xff00) >> 8, nBits & 0xff); } return sDecoded.substring(0, sDecoded.length - ((sEncoded.charCodeAt(i - 2) == 61) ? 2 : (sEncoded.charCodeAt(i - 1) == 61 ? 1 : 0))); } function parseISO8601(string) { var regexp = "([0-9]{4})(-?([0-9]{2})(-?([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?"; var time, offset = 0, dOut = new Date(), d = string.match(new RegExp(regexp)), date = new Date(d[1], 0, 1); if (d[3]) { date.setMonth(d[3] - 1); } if (d[5]) { date.setDate(d[5]); } if (d[7]) { date.setHours(d[7]); } if (d[8]) { date.setMinutes(d[8]); } if (d[10]) { date.setSeconds(d[10]); } if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); } if (d[14]) { offset = (Number(d[16]) * 60) + Number(d[17]); offset *= ((d[15] == '-') ? 1 : -1); } offset -= date.getTimezoneOffset(); time = (Number(date) + (offset * 60 * 1000)); dOut.setTime(Number(time)); return dOut; } module.exports = XmlRpcParser;