|
1 /*jshint browser:true */ |
|
2 /*global jQuery */ |
|
3 (function($) { |
|
4 "use strict"; |
|
5 |
|
6 var XmlRpcFault = function() { |
|
7 Error.apply(this, arguments); |
|
8 }; |
|
9 XmlRpcFault.prototype = new Error(); |
|
10 XmlRpcFault.prototype.type = 'XML-RPC fault'; |
|
11 |
|
12 var xmlrpc = $.xmlrpc = function(url, settings) { |
|
13 |
|
14 if (arguments.length === 2) { |
|
15 settings.url = url; |
|
16 } else { |
|
17 settings = url; |
|
18 url = settings.url; |
|
19 } |
|
20 |
|
21 settings.dataType = 'xml json'; |
|
22 settings.type = 'POST'; |
|
23 settings.contentType = 'text/xml'; |
|
24 settings.converters = {'xml json': xmlrpc.parseDocument}; |
|
25 |
|
26 var xmlDoc = xmlrpc.document(settings.methodName, settings.params || []); |
|
27 |
|
28 if ("XMLSerializer" in window) { |
|
29 settings.data = new window.XMLSerializer().serializeToString(xmlDoc); |
|
30 } else { |
|
31 // IE does not have XMLSerializer |
|
32 settings.data = xmlDoc.xml; |
|
33 } |
|
34 |
|
35 return $.ajax(settings); |
|
36 }; |
|
37 |
|
38 /** |
|
39 * Make an XML document node. |
|
40 */ |
|
41 xmlrpc.createXMLDocument = function () { |
|
42 |
|
43 if (document.implementation && "createDocument" in document.implementation) { |
|
44 // Most browsers support createDocument |
|
45 return document.implementation.createDocument(null, null, null); |
|
46 |
|
47 } else { |
|
48 // IE uses ActiveXObject instead of the above. |
|
49 var i, length, activeX = [ |
|
50 "MSXML6.DomDocument", "MSXML3.DomDocument", |
|
51 "MSXML2.DomDocument", "MSXML.DomDocument", "Microsoft.XmlDom" |
|
52 ]; |
|
53 for (i = 0, length = activeX.length; i < length; i++) { |
|
54 try { |
|
55 return new ActiveXObject(activeX[i]); |
|
56 } catch(_) {} |
|
57 } |
|
58 } |
|
59 }; |
|
60 |
|
61 /** |
|
62 * Make an XML-RPC document from a method name and a set of parameters |
|
63 */ |
|
64 xmlrpc.document = function(name, params) { |
|
65 var doc = xmlrpc.createXMLDocument(); |
|
66 |
|
67 |
|
68 var $xml = function(name) { |
|
69 return $(doc.createElement(name)); |
|
70 }; |
|
71 |
|
72 var $methodName = $xml('methodName').text(name); |
|
73 var $params = $xml('params').append($.map(params, function(param) { |
|
74 var $value = $xml('value').append(xmlrpc.toXmlRpc(param, $xml)); |
|
75 return $xml('param').append($value); |
|
76 })); |
|
77 var $methodCall = $xml('methodCall').append($methodName, $params); |
|
78 doc.appendChild($methodCall.get(0)); |
|
79 return doc; |
|
80 }; |
|
81 |
|
82 var _isInt = function(x) { |
|
83 return (x === parseInt(x, 10)) && !isNaN(x); |
|
84 }; |
|
85 |
|
86 /** |
|
87 * Take a JavaScript value, and return an XML node representing the value |
|
88 * in XML-RPC style. If the value is one of the `XmlRpcType`s, that type is |
|
89 * used. Otherwise, a best guess is made as to its type. The best guess is |
|
90 * good enough in the vast majority of cases. |
|
91 */ |
|
92 xmlrpc.toXmlRpc = function(item, $xml) { |
|
93 |
|
94 if (item instanceof XmlRpcType) { |
|
95 return item.toXmlRpc($xml); |
|
96 } |
|
97 |
|
98 var types = $.xmlrpc.types; |
|
99 var type = $.type(item); |
|
100 |
|
101 switch (type) { |
|
102 case "undefined": |
|
103 case "null": |
|
104 return types.nil.encode(item, $xml); |
|
105 |
|
106 case "date": |
|
107 return types['datetime.iso8601'].encode(item, $xml); |
|
108 |
|
109 case "object": |
|
110 if (item instanceof ArrayBuffer) { |
|
111 return types.base64.encode(item, $xml); |
|
112 } else { |
|
113 return types.struct.encode(item, $xml); |
|
114 } |
|
115 break; |
|
116 |
|
117 |
|
118 case "number": |
|
119 // Ints and Floats encode differently |
|
120 if (_isInt(item)) { |
|
121 return types['int'].encode(item, $xml); |
|
122 } else { |
|
123 return types['double'].encode(item, $xml); |
|
124 } |
|
125 break; |
|
126 |
|
127 case "array": |
|
128 case "boolean": |
|
129 case "string": |
|
130 return types[type].encode(item, $xml); |
|
131 |
|
132 default: |
|
133 throw new Error("Unknown type", item); |
|
134 } |
|
135 }; |
|
136 |
|
137 /** |
|
138 * Take an XML-RPC document and decode it to an equivalent JavaScript |
|
139 * representation. |
|
140 * |
|
141 * If the XML-RPC document represents a fault, then an equivalent |
|
142 * XmlRpcFault will be thrown instead |
|
143 */ |
|
144 xmlrpc.parseDocument = function(doc) { |
|
145 var $doc = $(doc); |
|
146 var $response = $doc.children('methodresponse'); |
|
147 |
|
148 var $fault = $response.find('> fault'); |
|
149 if ($fault.length === 0) { |
|
150 var $params = $response.find('> params > param > value > *'); |
|
151 var json = $params.toArray().map(xmlrpc.parseNode); |
|
152 return json; |
|
153 } else { |
|
154 var fault = xmlrpc.parseNode($fault.find('> value > *').get(0)); |
|
155 var err = new XmlRpcFault(fault.faultString); |
|
156 err.msg = err.message = fault.faultString; |
|
157 err.type = err.code = fault.faultCode; |
|
158 throw err; |
|
159 } |
|
160 }; |
|
161 |
|
162 /* |
|
163 * Take an XML-RPC node, and return the JavaScript equivalent |
|
164 */ |
|
165 xmlrpc.parseNode = function(node) { |
|
166 |
|
167 // Some XML-RPC services return empty <value /> elements. This is not |
|
168 // legal XML-RPC, but we may as well handle it. |
|
169 if (node === undefined) { |
|
170 return null; |
|
171 } |
|
172 var nodename = node.nodeName.toLowerCase(); |
|
173 if (nodename in xmlrpc.types) { |
|
174 return xmlrpc.types[nodename].decode(node); |
|
175 } else { |
|
176 throw new Error('Unknown type ' + nodename); |
|
177 } |
|
178 }; |
|
179 |
|
180 /* |
|
181 * Take a <value> node, and return the JavaScript equivalent. |
|
182 */ |
|
183 xmlrpc.parseValue = function(value) { |
|
184 var child = $(value).children()[0]; |
|
185 if (child) { |
|
186 // Child nodes should be decoded. |
|
187 return xmlrpc.parseNode(child); |
|
188 } else { |
|
189 // If no child nodes, the value is a plain text node. |
|
190 return $(value).text(); |
|
191 } |
|
192 }; |
|
193 |
|
194 var XmlRpcType = function() { }; |
|
195 |
|
196 $.xmlrpc.types = {}; |
|
197 |
|
198 /** |
|
199 * Make a XML-RPC type. We use these to encode and decode values. You can |
|
200 * also force a values type using this. See `$.xmlrpc.force()` |
|
201 */ |
|
202 xmlrpc.makeType = function(tagName, simple, encode, decode) { |
|
203 var Type; |
|
204 |
|
205 Type = function(value) { |
|
206 this.value = value; |
|
207 }; |
|
208 Type.prototype = new XmlRpcType(); |
|
209 Type.prototype.tagName = tagName; |
|
210 |
|
211 if (simple) { |
|
212 var simpleEncode = encode, simpleDecode = decode; |
|
213 encode = function(value, $xml) { |
|
214 var text = simpleEncode(value); |
|
215 return $xml(Type.tagName).text(text); |
|
216 }; |
|
217 decode = function(node) { |
|
218 return simpleDecode($(node).text(), node); |
|
219 }; |
|
220 } |
|
221 Type.prototype.toXmlRpc = function($xml) { |
|
222 return Type.encode(this.value, $xml); |
|
223 }; |
|
224 |
|
225 Type.tagName = tagName; |
|
226 Type.encode = encode; |
|
227 Type.decode = decode; |
|
228 |
|
229 xmlrpc.types[tagName.toLowerCase()] = Type; |
|
230 }; |
|
231 |
|
232 |
|
233 // Number types |
|
234 var _fromInt = function(value) { return '' + Math.floor(value); }; |
|
235 var _toInt = function(text, _) { return parseInt(text, 10); }; |
|
236 |
|
237 xmlrpc.makeType('int', true, _fromInt, _toInt); |
|
238 xmlrpc.makeType('i4', true, _fromInt, _toInt); |
|
239 xmlrpc.makeType('i8', true, _fromInt, _toInt); |
|
240 xmlrpc.makeType('i16', true, _fromInt, _toInt); |
|
241 xmlrpc.makeType('i32', true, _fromInt, _toInt); |
|
242 |
|
243 xmlrpc.makeType('double', true, String, function(text) { |
|
244 return parseFloat(text, 10); |
|
245 }); |
|
246 |
|
247 // String type. Fairly simple |
|
248 xmlrpc.makeType('string', true, String, String); |
|
249 |
|
250 // Boolean type. True == '1', False == '0' |
|
251 xmlrpc.makeType('boolean', true, function(value) { |
|
252 return value ? '1' : '0'; |
|
253 }, function(text) { |
|
254 return text === '1'; |
|
255 }); |
|
256 |
|
257 // Dates are a little trickier |
|
258 var _pad = function(n) { return n<10 ? '0'+n : n; }; |
|
259 |
|
260 xmlrpc.makeType('dateTime.iso8601', true, function(d) { |
|
261 return [ |
|
262 d.getUTCFullYear(), '-', _pad(d.getUTCMonth()+1), '-', |
|
263 _pad(d.getUTCDate()), 'T', _pad(d.getUTCHours()), ':', |
|
264 _pad(d.getUTCMinutes()), ':', _pad(d.getUTCSeconds()), 'Z' |
|
265 ].join(''); |
|
266 }, function(text) { |
|
267 return new Date(text); |
|
268 }); |
|
269 |
|
270 // Go between a base64 string and an ArrayBuffer |
|
271 xmlrpc.binary = (function() { |
|
272 var pad = '='; |
|
273 var toChars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + |
|
274 'abcdefghijklmnopqrstuvwxyz0123456789+/').split(""); |
|
275 var fromChars = toChars.reduce(function(acc, chr, i) { |
|
276 acc[chr] = i; |
|
277 return acc; |
|
278 }, {}); |
|
279 |
|
280 /* |
|
281 * In the following, three bytes are added together into a 24-bit |
|
282 * number, which is then split up in to 4 6-bit numbers - or vice versa. |
|
283 * That is why there is lots of shifting by multiples of 6 and 8, and |
|
284 * the magic numbers 3 and 4. |
|
285 * |
|
286 * The modulo 64 is for converting to base 64, and the modulo 256 is for |
|
287 * converting to 8-bit numbers. |
|
288 */ |
|
289 return { |
|
290 toBase64: function(ab) { |
|
291 var acc = []; |
|
292 |
|
293 var int8View = new Uint8Array(ab); |
|
294 var int8Index = 0, int24; |
|
295 for (; int8Index < int8View.length; int8Index += 3) { |
|
296 |
|
297 // Grab three bytes |
|
298 int24 = |
|
299 (int8View[int8Index + 0] << 16) + |
|
300 (int8View[int8Index + 1] << 8) + |
|
301 (int8View[int8Index + 2] << 0); |
|
302 |
|
303 // Push four chars |
|
304 acc.push(toChars[(int24 >> 18) % 64]); |
|
305 acc.push(toChars[(int24 >> 12) % 64]); |
|
306 acc.push(toChars[(int24 >> 6) % 64]); |
|
307 acc.push(toChars[(int24 >> 0)% 64]); |
|
308 } |
|
309 |
|
310 // Set the last few characters to the padding character |
|
311 var padChars = 3 - ((ab.byteLength % 3) || 3); |
|
312 while (padChars--) { |
|
313 acc[acc.length - padChars - 1] = pad; |
|
314 } |
|
315 |
|
316 return acc.join(''); |
|
317 }, |
|
318 |
|
319 fromBase64: function(base64) { |
|
320 var base64Len = base64.length; |
|
321 |
|
322 // Work out the length of the data, accommodating for padding |
|
323 var abLen = (base64Len / 4) * 3; |
|
324 if (base64.charAt(base64Len - 1) === pad) { abLen--; } |
|
325 if (base64.charAt(base64Len - 2) === pad) { abLen--; } |
|
326 |
|
327 // Make the ArrayBuffer, and an Int8Array to work with it |
|
328 var ab = new ArrayBuffer(abLen); |
|
329 var int8View = new Uint8Array(ab); |
|
330 |
|
331 var base64Index = 0, int8Index = 0, int24; |
|
332 for (; base64Index < base64Len; base64Index += 4, int8Index += 3) { |
|
333 |
|
334 // Grab four chars |
|
335 int24 = |
|
336 (fromChars[base64[base64Index + 0]] << 18) + |
|
337 (fromChars[base64[base64Index + 1]] << 12) + |
|
338 (fromChars[base64[base64Index + 2]] << 6) + |
|
339 (fromChars[base64[base64Index + 3]] << 0); |
|
340 |
|
341 // Push three bytes |
|
342 int8View[int8Index + 0] = (int24 >> 16) % 256; |
|
343 int8View[int8Index + 1] = (int24 >> 8) % 256; |
|
344 int8View[int8Index + 2] = (int24 >> 0) % 256; |
|
345 |
|
346 } |
|
347 |
|
348 return ab; |
|
349 } |
|
350 }; |
|
351 })(); |
|
352 |
|
353 xmlrpc.makeType('base64', true, function(ab) { |
|
354 return xmlrpc.binary.toBase64(ab); |
|
355 }, function(text) { |
|
356 return xmlrpc.binary.fromBase64(text); |
|
357 }); |
|
358 |
|
359 // Nil/null |
|
360 xmlrpc.makeType('nil', false, |
|
361 function(val, $xml) { return $xml('nil'); }, |
|
362 function(_) { return null; } |
|
363 ); |
|
364 |
|
365 // Structs/Objects |
|
366 xmlrpc.makeType('struct', false, function(value, $xml) { |
|
367 var $struct = $xml('struct'); |
|
368 |
|
369 $.each(value, function(name, value) { |
|
370 var $name = $xml('name').text(name); |
|
371 var $value = $xml('value').append(xmlrpc.toXmlRpc(value, $xml)); |
|
372 $struct.append($xml('member').append($name, $value)); |
|
373 }); |
|
374 |
|
375 return $struct; |
|
376 |
|
377 }, function(node) { |
|
378 return $(node) |
|
379 .find('> member') |
|
380 .toArray() |
|
381 .reduce(function(struct, el) { |
|
382 var $el = $(el); |
|
383 var key = $el.find('> name').text(); |
|
384 var value = xmlrpc.parseValue($el.find('> value')); |
|
385 |
|
386 struct[key] = value; |
|
387 return struct; |
|
388 }, {}); |
|
389 |
|
390 }); |
|
391 |
|
392 // Arrays |
|
393 xmlrpc.makeType('array', false, function(value, $xml) { |
|
394 var $array = $xml('array'); |
|
395 var $data = $xml('data'); |
|
396 $.each(value, function(i, val) { |
|
397 $data.append($xml('value').append(xmlrpc.toXmlRpc(val, $xml))); |
|
398 }); |
|
399 $array.append($data); |
|
400 return $array; |
|
401 }, function(node) { |
|
402 return $(node).find('> data > value').toArray() |
|
403 .map(xmlrpc.parseValue); |
|
404 }); |
|
405 |
|
406 |
|
407 /** |
|
408 * Force a value to an XML-RPC type. All the usual XML-RPC types are |
|
409 * supported |
|
410 */ |
|
411 xmlrpc.force = function(type, value) { |
|
412 return new xmlrpc.types[type](value); |
|
413 }; |
|
414 |
|
415 })(jQuery); |