├── test ├── browserjs-tests.js └── xhr-tests.js ├── lib ├── browser.js └── browser │ ├── console.js │ ├── window.js │ ├── timeout.js │ ├── canvas.js │ └── xhr.js ├── package.json ├── engines ├── jsc │ └── lib │ │ └── browser │ │ └── dom.js └── rhino │ └── lib │ └── browser │ └── dom.js └── README.md /test/browserjs-tests.js: -------------------------------------------------------------------------------- 1 | exports.testXHR = require("./xhr-tests.js"); 2 | 3 | require("test/runner").run(exports); 4 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | var window = exports.window = require("./browser/window"); 2 | 3 | for (var property in window) { 4 | if (global[property] === undefined) 5 | global[property] = window[property]; 6 | else 7 | system.log.warn("global clash"); 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "browserjs", 3 | "version": "0.1", 4 | "dependencies": ["narwhal"], 5 | "author": "Tom Robinson (http://tlrobinson.net/)", 6 | "description": "A simulated web browser environment, includes XMLHttpRequest, timeouts, console, a DOM, etc", 7 | "keywords": ["browser", "web browser", "dom", "xmlhttprequest"] 8 | } 9 | -------------------------------------------------------------------------------- /lib/browser/console.js: -------------------------------------------------------------------------------- 1 | var Logger = require("logger").Logger; 2 | 3 | var console = exports; 4 | 5 | var consoleLogger = new Logger(system.stdout); 6 | 7 | console.error = function() { 8 | consoleLogger.error.apply(consoleLogger, arguments); 9 | } 10 | console.warn = function() { 11 | consoleLogger.warn.apply(consoleLogger, arguments); 12 | } 13 | console.log = function() { 14 | consoleLogger.info.apply(consoleLogger, arguments); 15 | } 16 | console.info = function() { 17 | consoleLogger.info.apply(consoleLogger, arguments); 18 | } 19 | console.debug = function() { 20 | consoleLogger.debug.apply(consoleLogger, arguments); 21 | } 22 | 23 | console.setLogger = function(logger) { 24 | consoleLogger = logger; 25 | } 26 | console.setLevel = function(level) { 27 | consoleLogger.level = level; 28 | } 29 | -------------------------------------------------------------------------------- /lib/browser/window.js: -------------------------------------------------------------------------------- 1 | var window = exports; 2 | 3 | window.window = window; 4 | 5 | window.DOMParser = require("./dom").DOMParser; 6 | 7 | window.XMLHttpRequest = require("./xhr").XMLHttpRequest; 8 | 9 | window.setTimeout = require("./timeout").setTimeout; 10 | window.setInterval = require("./timeout").setInterval; 11 | window.clearTimeout = require("./timeout").clearTimeout; 12 | window.clearInterval = require("./timeout").clearInterval; 13 | 14 | window.console = require("./console"); 15 | 16 | window.alert = function() { print.apply(null, arguments); } 17 | window.prompt = function() { print.apply(null, arguments); return ""; } 18 | window.confirm = function() { print.apply(null, arguments); return true; } 19 | 20 | window.Image = function() {}; -------------------------------------------------------------------------------- /engines/jsc/lib/browser/dom.js: -------------------------------------------------------------------------------- 1 | exports.Node = global.Node; 2 | exports.Element = global.Element; 3 | exports.Document = global.Document; 4 | 5 | exports.NodeList = global.NodeList; 6 | 7 | exports.DOMParser = global.DOMParser; 8 | exports.XMLSerializer = global.XMLSerializer; 9 | exports.XPathResult = global.XPathResult; 10 | 11 | exports.createDocument = function(/*String*/ namespaceURI, /*String*/ qualifiedName, /*DocumentType*/ doctype) { 12 | return global.document.implementation.createDocument(namespaceURI, qualifiedName, doctype || null); 13 | } 14 | 15 | exports.evaluate = function(/*String*/ xpathText, /*Node*/ contextNode, /*Function*/ namespaceURLMapper, /*short*/ resultType, /*XPathResult*/ result) { 16 | var doc = contextNode instanceof exports.Document ? contextNode : contextNode.ownerDocument; 17 | return doc.evaluate(xpathText, contextNode, namespaceURLMapper, resultType, result || null); 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | browserjs 2 | ========= 3 | 4 | BrowserJS is a ServerJS compatible package that emulates portions of the browser JavaScript APIs. 5 | 6 | Supported APIs 7 | -------------- 8 | 9 | * XMLHttpRequest 10 | * setTimeout, setInterval, clearTimeout, clearInterval 11 | * console.log, error, warn, debug 12 | * DOMParser and basic DOM operations (Rhino only) 13 | 14 | Usage 15 | ----- 16 | 17 | To get access to individual APIs, require the corresponding module. For example, if you just w 18 | 19 | var XMLHttpRequest = require("browser/xhr").XMLHttpRequest; 20 | 21 | To get the "window" object, which contains all the APIs, require the "browser/window" module: 22 | 23 | var window = require("browser/window"); 24 | 25 | Use the properties of the window object directly: 26 | 27 | var request = new window.XMLHttpRequest(); 28 | 29 | Or you can bring all the properties of the "window" object into scope temporarily (without modifying the global scope) by using a with statement: 30 | 31 | with (window) { 32 | var request = new XMLHttpRequest(); 33 | } 34 | 35 | If you want to permanently modify the global scope to include all the properties of "window" in the global scope, simply require the "browser" module: 36 | 37 | require("browser") 38 | -------------------------------------------------------------------------------- /lib/browser/timeout.js: -------------------------------------------------------------------------------- 1 | // setTimeout, setInterval, clearTimeout, clearInterval 2 | 3 | // This implementation causes all callbacks to run in a single thread (like browsers) 4 | // but uses a timer thread to queue the timeout callbacks (but not execute them). 5 | 6 | exports.setTimeout = function(callback, delay) 7 | { 8 | return _scheduleTimeout(callback, delay, false); 9 | } 10 | 11 | exports.setInterval = function(callback, delay) 12 | { 13 | return _scheduleTimeout(callback, delay, true); 14 | } 15 | 16 | exports.clearTimeout = function(id) 17 | { 18 | if (timeouts[id]){ 19 | timeouts[id].task.cancel(); 20 | timeouts[id].cancelled = true; 21 | } 22 | } 23 | 24 | exports.clearInterval = exports.clearTimeout; 25 | 26 | 27 | var nextId = 1, 28 | timeouts = {}, 29 | timer, 30 | queue; 31 | 32 | var _scheduleTimeout = function(callback, delay, repeat) 33 | { 34 | if (typeof callback == "function") 35 | var func = callback; 36 | else if (typeof callback == "string") 37 | var func = new Function(callback); 38 | else 39 | return; 40 | 41 | var timeout = { 42 | }; 43 | var id = nextId++; 44 | timeouts[id] = timeout; 45 | 46 | timer = timer || new java.util.Timer("JavaScript timer thread", true); 47 | queue = queue || require("event-queue"); 48 | var task = timeout.task = new java.util.TimerTask({ 49 | run: function(){ 50 | queue.enqueue(function(){ 51 | if(!timeout.cancelled){ // check to make sure it wasn't enqueued and then later cancelled 52 | func(); 53 | } 54 | }); 55 | } 56 | }); 57 | delay = Math.floor(delay); 58 | 59 | if(repeat){ 60 | timer.schedule(task, delay, delay); 61 | } 62 | else{ 63 | timer.schedule(task, delay); 64 | } 65 | 66 | return id; 67 | } 68 | 69 | 70 | -------------------------------------------------------------------------------- /test/xhr-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require("test/assert"); 2 | 3 | var XHR = require("browser/xhr").XMLHttpRequest, 4 | File = require("file"), 5 | ByteString = require("binary").ByteString; 6 | 7 | var testFileMissing = "foobarbaz-missing", 8 | testFilePresent = "foobarbaz-present", 9 | contents = "hello world\n", 10 | binaryContents = new ByteString(contents); 11 | 12 | var sendAsBinary = function(req) { return req.sendAsBinary(binaryContents); }; 13 | 14 | exports.setup = function() { 15 | try { File.remove(testFileMissing); } catch (e) {} 16 | 17 | try { 18 | File.write(testFilePresent, contents); 19 | } catch (e) { 20 | system.log.error("Couldn't prepare test file: " + e); 21 | } 22 | } 23 | 24 | exports.teardown = function() { 25 | try { File.remove(testFileMissing); } catch (e) {} 26 | try { File.remove(testFilePresent); } catch (e) {} 27 | } 28 | 29 | exports.testSynchronouseLocalGET = function() { 30 | xhrSynchronousTest("GET", testFilePresent, 200, contents); 31 | } 32 | 33 | exports.testSynchronouseLocalGETMissing = function() { 34 | xhrSynchronousTest("GET", testFileMissing, 404, ""); 35 | } 36 | 37 | exports.testSynchronouseLocalPUT = function() { 38 | xhrSynchronousTest("PUT", testFileMissing, 201, ""); 39 | assert.isEqual(contents, File.read(testFileMissing)); 40 | } 41 | 42 | exports.testSynchronouseLocalDELETE = function() { 43 | xhrSynchronousTest("DELETE", testFilePresent, 200, ""); 44 | assert.isTrue(!File.exists(testFilePresent), "File should be deleted"); 45 | } 46 | 47 | exports.testSynchronouseLocalBinaryGET = function() { 48 | xhrSynchronousTest("GET", testFilePresent, 200, contents, sendAsBinary); 49 | } 50 | 51 | exports.testSynchronouseLocalBinaryGETMissing = function() { 52 | xhrSynchronousTest("GET", testFileMissing, 404, "", sendAsBinary); 53 | } 54 | 55 | exports.testSynchronouseLocalBinaryPUT = function() { 56 | xhrSynchronousTest("PUT", testFileMissing, 201, "", sendAsBinary); 57 | assert.isEqual(binaryContents.decodeToString(64), File.read(testFileMissing, "rb").decodeToString(64)); 58 | } 59 | 60 | exports.testSynchronouseLocalBinaryDELETE = function() { 61 | xhrSynchronousTest("DELETE", testFilePresent, 200, "", sendAsBinary); 62 | assert.isTrue(!File.exists(testFilePresent), "File should be deleted"); 63 | } 64 | 65 | function xhrSynchronousTest(method, url, expectedStatus, expectedText, send) { 66 | var req = new XHR(), 67 | lastState = req.readyState; 68 | 69 | send = send || function(req) { req.send(contents); }; 70 | 71 | assert.isEqual(0, lastState); 72 | req.onreadystatechange = function() { 73 | assert.isTrue(lastState <= req.readyState, "readyState not monotonically increasing"); 74 | lastState = req.readyState; 75 | } 76 | 77 | req.open(method, url, false); 78 | 79 | assert.isEqual(1, req.readyState); 80 | 81 | send(req); 82 | 83 | assert.isEqual(4, req.readyState); 84 | assert.isEqual(4, lastState); 85 | assert.isEqual(expectedStatus, req.status); 86 | assert.isEqual(expectedText, req.responseText); 87 | } 88 | -------------------------------------------------------------------------------- /lib/browser/canvas.js: -------------------------------------------------------------------------------- 1 | var HTMLCanvasElement = exports.HTMLCanvasElement = function() { 2 | }; 3 | 4 | // DOMString toDataURL([Optional] in DOMString type, [Variadic] in any args); 5 | HTMLCanvasElement.prototype.toDataURL = function(type) { 6 | 7 | } 8 | 9 | // Object getContext(in DOMString contextId); 10 | HTMLCanvasElement.prototype.getContext = function(contextId) { 11 | return new CRC2D(this); 12 | } 13 | 14 | var CRC2D = exports.CanvasRenderingContext2D = function(canvas) { 15 | 16 | this.states = []; 17 | 18 | // readonly attribute HTMLCanvasElement canvas; 19 | this.canvas = canvas; 20 | 21 | // attribute float globalAlpha; // (default 1.0) 22 | this.globalAlpha = 1.0; 23 | // attribute DOMString globalCompositeOperation; // (default source-over) 24 | this.globalCompositeOperation = "source-over"; 25 | 26 | // attribute any strokeStyle; // (default black) 27 | this.strokeStyle = "black"; 28 | // attribute any fillStyle; // (default black) 29 | this.fillStyle = "black"; 30 | 31 | // attribute float lineWidth; // (default 1) 32 | this.lineWidth = 1.0; 33 | // attribute DOMString lineCap; // "butt", "round", "square" (default "butt") 34 | this.lineCap = "butt"; 35 | // attribute DOMString lineJoin; // "round", "bevel", "miter" (default "miter") 36 | this.lineJoin = "miter"; 37 | // attribute float miterLimit; // (default 10) 38 | this.miterLimit = 10.0; 39 | 40 | // attribute float shadowOffsetX; // (default 0) 41 | this.shadowOffsetX = 0.0; 42 | // attribute float shadowOffsetY; // (default 0) 43 | this.shadowOffsetY = 0.0; 44 | // attribute float shadowBlur; // (default 0) 45 | this.shadowBlur = 0.0; 46 | // attribute DOMString shadowColor; // (default transparent black) 47 | this.shadowColor = "transparent black"; 48 | 49 | // attribute DOMString font; // (default 10px sans-serif) 50 | this.font = "default 10px sans-serif"; 51 | // attribute DOMString textAlign; // "start", "end", "left", "right", "center" (default: "start") 52 | this.textAlign = "start"; 53 | // attribute DOMString textBaseline; // "top", "hanging", "middle", "alphabetic", "ideographic", "bottom" (default: "alphabetic") 54 | this.textBaseline = "alphabetic"; 55 | } 56 | 57 | 58 | // void save(); // push state on state stack 59 | CRC2D.prototype.save = function() { 60 | this.states.push(); 61 | } 62 | 63 | // void restore(); // pop state stack and restore state 64 | CRC2D.prototype.restore = function() { 65 | this.states.pop(); 66 | } 67 | 68 | 69 | // void scale(in float x, in float y); 70 | CRC2D.prototype.scale = function(x, y) {} 71 | // void rotate(in float angle); 72 | CRC2D.prototype.rotate = function(angle) {} 73 | // void translate(in float x, in float y); 74 | CRC2D.prototype.translate = function(x, y) {} 75 | // void transform(in float m11, in float m12, in float m21, in float m22, in float dx, in float dy); 76 | CRC2D.prototype.transform = function(m11, m12, m21, m22, dx, dy) {} 77 | // void setTransform(in float m11, in float m12, in float m21, in float m22, in float dx, in float dy); 78 | CRC2D.prototype.setTransform = function(m11, m12, m21, m22, dx, dy) {} 79 | 80 | // CanvasGradient createLinearGradient(in float x0, in float y0, in float x1, in float y1); 81 | CRC2D.prototype.createLinearGradient = function(x0, y0, x1, y1) {} 82 | // CanvasGradient createRadialGradient(in float x0, in float y0, in float r0, in float x1, in float y1, in float r1); 83 | CRC2D.prototype.createRadialGradient = function(x0, y0, r0, x1, y1, r1) {} 84 | // CanvasPattern createPattern(in HTMLImageElement image, in DOMString repetition); 85 | // CanvasPattern createPattern(in HTMLCanvasElement image, in DOMString repetition); 86 | CRC2D.prototype.createPattern = function(image, repetition) {} 87 | 88 | // void clearRect(in float x, in float y, in float w, in float h); 89 | CRC2D.prototype.clearRect = function(x, y, w, h) {} 90 | // void fillRect(in float x, in float y, in float w, in float h); 91 | CRC2D.prototype.fillRect = function(x, y, w, h) {} 92 | // void strokeRect(in float x, in float y, in float w, in float h); 93 | CRC2D.prototype.strokeRect = function(x, y, w, h) {} 94 | 95 | // void beginPath(); 96 | CRC2D.prototype.beginPath = function() {} 97 | // void closePath(); 98 | CRC2D.prototype.closePath = function() {} 99 | // void moveTo(in float x, in float y); 100 | CRC2D.prototype.moveTo = function(x, y) {} 101 | // void lineTo(in float x, in float y); 102 | CRC2D.prototype.lineTo = function(x, y) {} 103 | // void quadraticCurveTo(in float cpx, in float cpy, in float x, in float y); 104 | CRC2D.prototype.quadraticCurveTo = function(cpx, cpy, x, y) {} 105 | // void bezierCurveTo(in float cp1x, in float cp1y, in float cp2x, in float cp2y, in float x, in float y); 106 | CRC2D.prototype.bezierCurveTo = function(cp1x, cp1y, cp2x, cp2y, x, y) {} 107 | // void arcTo(in float x1, in float y1, in float x2, in float y2, in float radius); 108 | CRC2D.prototype.arcTo = function(x1, y1, x2, y2, radius) {} 109 | // void rect(in float x, in float y, in float w, in float h); 110 | CRC2D.prototype.rect = function(x, y, w, h) {} 111 | // void arc(in float x, in float y, in float radius, in float startAngle, in float endAngle, in boolean anticlockwise); 112 | CRC2D.prototype.arc = function(x, y, radius, startAngle, endAngle, anticlockwise) {} 113 | // void fill(); 114 | CRC2D.prototype.fill = function() {} 115 | // void stroke(); 116 | CRC2D.prototype.stroke = function() {} 117 | // void clip(); 118 | CRC2D.prototype.clip = function() {} 119 | // boolean isPointInPath(in float x, in float y); 120 | CRC2D.prototype.isPointInPath = function(x, y) {} 121 | 122 | // void fillText(in DOMString text, in float x, in float y, [Optional] in float maxWidth); 123 | CRC2D.prototype.fillText = function(text, x, y, maxWidth) {} 124 | // void strokeText(in DOMString text, in float x, in float y, [Optional] in float maxWidth); 125 | CRC2D.prototype.strokeText = function(text, x, y, maxWidth) {} 126 | // TextMetrics measureText(in DOMString text); 127 | CRC2D.prototype.measureText = function(text) {} 128 | 129 | // void drawImage(in HTMLImageElement image, in float dx, in float dy, [Optional] in float dw, in float dh); 130 | // void drawImage(in HTMLCanvasElement image, in float dx, in float dy, [Optional] in float dw, in float dh); 131 | // void drawImage(in HTMLVideoElement image, in float dx, in float dy, [Optional] in float dw, in float dh); 132 | CRC2D.prototype.drawImage = function(image, dx, dy, dw, dh) {} 133 | // void drawImage(in HTMLImageElement image, in float sx, in float sy, in float sw, in float sh, in float dx, in float dy, in float dw, in float dh); 134 | // void drawImage(in HTMLCanvasElement image, in float sx, in float sy, in float sw, in float sh, in float dx, in float dy, in float dw, in float dh); 135 | // void drawImage(in HTMLVideoElement image, in float sx, in float sy, in float sw, in float sh, in float dx, in float dy, in float dw, in float dh); 136 | CRC2D.prototype.drawImage = function(image, sx, sy, sw, sh, dx, dy, dw, dh) {} 137 | 138 | // ImageData createImageData(in float sw, in float sh); 139 | CRC2D.prototype.createImageData = function(sw, sh) {} 140 | // ImageData getImageData(in float sx, in float sy, in float sw, in float sh); 141 | CRC2D.prototype.getImageData = function(sx, sy, sw, sh) {} 142 | // void putImageData(in ImageData imagedata, in float dx, in float dy, [Optional] in float dirtyX, in float dirtyY, in float dirtyWidth, in float dirtyHeight); 143 | CRC2D.prototype.putImageData = function(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight) {} 144 | -------------------------------------------------------------------------------- /engines/rhino/lib/browser/dom.js: -------------------------------------------------------------------------------- 1 | var builderFactory = Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(); 2 | 3 | // setValidating to false doesn't seem to prevent it from downloading the DTD, but lets do it anyway 4 | builderFactory.setValidating(false); 5 | 6 | var documentBuilder = builderFactory.newDocumentBuilder(), 7 | domImplementation = documentBuilder.getDOMImplementation(), 8 | transformerFactory = Packages.javax.xml.transform.TransformerFactory.newInstance(); 9 | 10 | // suppress log messages, it throws an exception anyway 11 | documentBuilder.setErrorHandler(null); 12 | 13 | // prevent the Java XML parser from downloading the plist DTD from Apple every time we parse a plist 14 | documentBuilder.setEntityResolver(new JavaAdapter(Packages.org.xml.sax.EntityResolver, { 15 | resolveEntity: function(publicId, systemId) { 16 | // TODO: return a local copy of the DTD? 17 | if (String(systemId) === "http://www.apple.com/DTDs/PropertyList-1.0.dtd") 18 | return new Packages.org.xml.sax.InputSource(new Packages.java.io.StringReader("")); 19 | 20 | return null; 21 | } 22 | })); 23 | 24 | transformerFactory.setAttribute("indent-number", new Packages.java.lang.Integer(2)); 25 | 26 | var serializer = transformerFactory.newTransformer(); 27 | serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.VERSION, "1.0"); 28 | serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.ENCODING, "UTF-8"); 29 | //serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.STANDALONE, "no"); 30 | serializer.setOutputProperty(Packages.javax.xml.transform.OutputKeys.INDENT, "yes"); 31 | 32 | exports.createDocument = function(/*String*/ namespaceURI, /*String*/ qualifiedName, /*DocumentType*/ doctype) { 33 | var doc = domImplementation.createDocument(namespaceURI, qualifiedName, doctype || null); 34 | return doc; 35 | } 36 | 37 | exports.XMLSerializer = function() { 38 | } 39 | 40 | exports.XMLSerializer.prototype.serializeToString = function(doc) { 41 | var domSource = new Packages.javax.xml.transform.dom.DOMSource(doc), 42 | stringWriter = new Packages.java.io.StringWriter(), 43 | streamResult = new Packages.javax.xml.transform.stream.StreamResult(stringWriter); 44 | 45 | serializer.transform(domSource, streamResult); 46 | 47 | return String(stringWriter.toString()); 48 | } 49 | 50 | // DOMParser 51 | var validContentTypes = { "text/xml" : true, "application/xml" : true, "application/xhtml+xml" : true }; 52 | 53 | var DOMParser = exports.DOMParser = function() { 54 | } 55 | 56 | DOMParser.prototype.parseFromString = function(/*String*/ text, /*String*/ contentType) { 57 | if (validContentTypes[contentType] !== true) 58 | throw new Error("DOMParser parseFromString() contentType argument must be one of text/xml, application/xml or application/xhtml+xml"); 59 | return documentBuilder.parse(new Packages.org.xml.sax.InputSource(new Packages.java.io.StringReader(String(text)))); 60 | } 61 | 62 | // misc: 63 | 64 | // FIXME: wrap Document so we can add these: 65 | 66 | exports.createExpression = function(/*String*/ xpathText, /*Function*/ namespaceURLMapper) { 67 | return new XPathExpression(xpathText, namespaceURLMapper); 68 | } 69 | 70 | exports.evaluate = function(/*String*/ xpathText, /*Node*/ contextNode, /*Function*/ namespaceURLMapper, /*short*/ resultType, /*XPathResult*/ result) { 71 | return exports.createExpression(xpathText, namespaceURLMapper).evaluate(contextNode, resultType, result); 72 | } 73 | 74 | // XPathExpression: 75 | 76 | var xpathFactory = Packages.javax.xml.xpath.XPathFactory.newInstance();//namespaceURI); 77 | 78 | var XPathExpression = exports.XPathExpression = function(/*String*/ xpathText, /*Function*/ namespaceURLMapper) { 79 | var namespaceContext = null; 80 | if (typeof namespaceURLMapper === "function") { 81 | namespaceContext = function(prefix, methodName) { 82 | if (methodName === "getNamespaceURI") 83 | return namespaceURLMapper(String(prefix)); 84 | return null; 85 | } 86 | } 87 | 88 | this.xpath = xpathFactory.newXPath(); 89 | if (namespaceContext) 90 | this.xpath.setNamespaceContext(namespaceContext); 91 | 92 | this.xpathExpression = this.xpath.compile(xpathText); 93 | } 94 | 95 | XPathExpression.prototype.evaluate = function(/*Node*/ contextNode, /*short*/ resultType, /*XPathResult*/ result) { 96 | var nodes = null; 97 | if (resultType === undefined || resultType === null || QNameMapping[resultType] === null) 98 | nodes = this.xpathExpression.evaluate(contextNode); 99 | else if (QNameMapping[resultType] === undefined) 100 | throw new Error("Invalid resultType"); 101 | else 102 | nodes = this.xpathExpression.evaluate(contextNode, QNameMapping[resultType]); 103 | 104 | if (!result) 105 | result = new XPathResult(); 106 | 107 | result._setResult(nodes, resultType); 108 | 109 | return result; 110 | } 111 | 112 | // XPathResult: 113 | 114 | var XPathResult = exports.XPathResult = function() { 115 | } 116 | 117 | XPathResult.prototype._setResult = function(result, resultType) { 118 | delete this.booleanValue; 119 | delete this.numberValue; 120 | delete this.stringValue; 121 | delete this.singleNodeValue; 122 | 123 | delete this.resultType; 124 | 125 | delete this.snapshotLength; 126 | delete this.invalidIteratorState; 127 | 128 | delete this._snapshot; 129 | delete this._iteratorIndex; 130 | 131 | if (result instanceof Packages.java.lang.Boolean) { 132 | this.booleanValue = Boolean(result); 133 | this.resultType = XPathResult.BOOLEAN_TYPE; 134 | } 135 | else if (result instanceof Packages.java.lang.Number) { 136 | this.numberValue = Number(result); 137 | this.resultType = XPathResult.NUMBER_TYPE; 138 | } 139 | else if (result instanceof Packages.java.lang.String) { 140 | this.stringValue = String(result); 141 | this.resultType = XPathResult.STRING_TYPE; 142 | } 143 | else if (result instanceof Packages.org.w3c.dom.Node) { 144 | this.singleNodeValue = result; 145 | switch (resultType) { 146 | case XPathResult.ANY_UNORDERED_NODE_TYPE: 147 | case XPathResult.FIRST_ORDERED_NODE_TYPE: 148 | this.resultType = resultType; 149 | default: 150 | this.resultType = XPathResult.ANY_UNORDERED_NODE_TYPE; 151 | } 152 | } 153 | else if (result instanceof Packages.org.w3c.dom.NodeList) { 154 | this._snapshot = result; 155 | this._iteratorIndex = 0; 156 | this.snapshotLength = result.getLength(); 157 | this.invalidIteratorState = false; 158 | switch (resultType) { 159 | case XPathResult.UNORDERED_NODE_ITERATOR_TYPE: 160 | case XPathResult.ORDERED_NODE_ITERATOR_TYPE: 161 | case XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE: 162 | case XPathResult.ORDERED_NODE_SNAPSHOT_TYPE: 163 | this.resultType = resultType; 164 | default: 165 | this.resultType = XPathResult.UNORDERED_NODE_ITERATOR_TYPE; 166 | } 167 | } else { 168 | this._iteratorIndex = 0; 169 | this.snapshotLength = 0; 170 | this.invalidIteratorState = false; 171 | this.resultType = XPathResult.UNORDERED_NODE_ITERATOR_TYPE; 172 | } 173 | } 174 | 175 | XPathResult.prototype.iterateNext = function() { 176 | return this.snapshotItem(this._iteratorIndex++); 177 | } 178 | 179 | XPathResult.prototype.snapshotItem = function(index) { 180 | return (index >= this.snapshotLength) ? null : this._snapshot.item(index); 181 | } 182 | 183 | XPathResult.ANY_TYPE = 0; 184 | XPathResult.NUMBER_TYPE = 1; 185 | XPathResult.STRING_TYPE = 2; 186 | XPathResult.BOOLEAN_TYPE = 3; 187 | XPathResult.UNORDERED_NODE_ITERATOR_TYPE = 4; 188 | XPathResult.ORDERED_NODE_ITERATOR_TYPE = 5; 189 | XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE = 6; 190 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE = 7; 191 | XPathResult.ANY_UNORDERED_NODE_TYPE = 8; 192 | XPathResult.FIRST_ORDERED_NODE_TYPE = 9; 193 | 194 | var QNameMapping = {}; 195 | QNameMapping[XPathResult.ANY_TYPE] = null; 196 | QNameMapping[XPathResult.NUMBER_TYPE] = Packages.javax.xml.xpath.XPathConstants.NUMBER; 197 | QNameMapping[XPathResult.STRING_TYPE] = Packages.javax.xml.xpath.XPathConstants.STRING; 198 | QNameMapping[XPathResult.BOOLEAN_TYPE] = Packages.javax.xml.xpath.XPathConstants.BOOLEAN; 199 | QNameMapping[XPathResult.UNORDERED_NODE_ITERATOR_TYPE] = Packages.javax.xml.xpath.XPathConstants.NODESET; 200 | QNameMapping[XPathResult.ORDERED_NODE_ITERATOR_TYPE] = Packages.javax.xml.xpath.XPathConstants.NODESET; 201 | QNameMapping[XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE] = Packages.javax.xml.xpath.XPathConstants.NODESET; 202 | QNameMapping[XPathResult.ORDERED_NODE_SNAPSHOT_TYPE] = Packages.javax.xml.xpath.XPathConstants.NODESET; 203 | QNameMapping[XPathResult.ANY_UNORDERED_NODE_TYPE] = Packages.javax.xml.xpath.XPathConstants.NODE; 204 | QNameMapping[XPathResult.FIRST_ORDERED_NODE_TYPE] = Packages.javax.xml.xpath.XPathConstants.NODE; 205 | 206 | 207 | exports.Node = Packages.org.w3c.dom.Node; 208 | exports.Element = Packages.org.w3c.dom.Element; 209 | exports.NodeList = Packages.org.w3c.dom.NodeList; 210 | exports.Document = Packages.org.w3c.dom.Document; 211 | -------------------------------------------------------------------------------- /lib/browser/xhr.js: -------------------------------------------------------------------------------- 1 | var File = require("file"), 2 | IO = require("io").IO, 3 | URI = require("uri").URI, 4 | HashP = require("hashp").HashP, 5 | ByteString = require("binary").ByteString, 6 | DOMParser = require("./dom").DOMParser; 7 | 8 | // http://www.w3.org/TR/XMLHttpRequest/ 9 | 10 | var parser = new DOMParser(); 11 | 12 | var UNSENT = 0, 13 | OPENED = 1, 14 | HEADERS_RECEIVED = 2, 15 | LOADING = 3, 16 | DONE = 4; 17 | 18 | var XMLHttpRequest = exports.XMLHttpRequest = function() 19 | { 20 | // onreadystatechange of type EventListener 21 | // This attribute is an event handler DOM attribute and must be invoked whenever a readystatechange event is targated at the object. 22 | this.onreadystatechange = null; 23 | 24 | // readyState of type unsigned short, readonly 25 | // On getting the attribute must return the value of the constant corresponding to the object's current state. 26 | this.readyState = UNSENT; 27 | 28 | // responseText of type DOMString, readonly 29 | // On getting, the user agent must run the following steps: 30 | // 1. If the state is not LOADING or DONE return the empty string and terminate these steps. 31 | // 2. Return the text response entity body. 32 | this.responseText = ""; 33 | 34 | // responseXML of type Document, readonly 35 | // On getting, the user agent must run the following steps: 36 | // 1. If the state is not DONE return null and terminate these steps. 37 | // 2. Return the XML response entity body. 38 | this.responseXML = null; 39 | 40 | // status of type unsigned short, readonly 41 | // On getting, if available, it must return the HTTP status code sent by the server (typically 200 for a successful request). Otherwise, if not available, the user agent must raise an INVALID_STATE_ERR exception. 42 | this.status = null; 43 | 44 | // statusText of type DOMString, readonly 45 | // On getting, if available, it must return the HTTP status text sent by the server (appears after the status code). Otherwise, if not available, the user agent must raise an INVALID_STATE_ERR exception. 46 | this.statusText = ""; 47 | 48 | this._method = null; 49 | this._url = null; 50 | this._async = null; 51 | this._user = null; 52 | this._password = null; 53 | 54 | this._requestHeaders = {}; 55 | this._responseHeaders = {}; 56 | 57 | this._responseBody = null; 58 | this._errorFlag = false; 59 | this._sendFlag = false; 60 | } 61 | 62 | // open(method, url, async, user, password), method 63 | XMLHttpRequest.prototype.open = function(method, url, async, user, password) 64 | { 65 | // When invoked, the user agent must follow the following steps (unless otherwise indicated): 66 | 67 | // 1. Let stored method be the method argument. 68 | this._method = method; 69 | 70 | // 2. If stored method does not match the Method production, defined in section 5.1.1 of RFC 2616, raise a SYNTAX_ERR exception and terminate these steps. [RFC2616] 71 | if (typeof this._method !== "string" || !this._method.match(/^[A-Za-z]+$/)) 72 | throw new Error("SYNTAX_ERR"); 73 | 74 | // 3. If stored method case-insensitively matches CONNECT, DELETE, GET, HEAD, OPTIONS POST, PUT, TRACE, or TRACK let stored method be the canonical uppercase form of the matched method name. 75 | if (this._method.match(/^(CONNECT|DELETE|GET|HEAD|OPTIONS|POST|PUT|TRACE|TRACK)$/i)) 76 | this._method = this._method.toUpperCase(); 77 | 78 | // 4. If stored method is one of CONNECT, TRACE, or TRACK the user agent should raise a SECURITY_ERR exception and terminate these steps. 79 | if (this._method.match(/^(CONNECT|TRACE|TRACK)$/)) 80 | throw new Error("SECURITY_ERR"); 81 | 82 | var uri = URI.parse(url); 83 | 84 | if (uri.scheme === null) 85 | uri.scheme = "file"; 86 | 87 | // 5. Drop the fragment identifier (if any) from url and let stored url be the result of that operation. 88 | uri.fragment = null; 89 | 90 | // 6. If stored url is a relative reference resolve it using the current value of the baseURI attribute of the Document pointer. If this fails raise a SYNTAX_ERR exception and terminate these steps. 91 | if (uri.path === null) 92 | uri.path = "/"; 93 | else if (uri.path.charAt(0) !== "/") 94 | uri.path = File.cwd() + "/" + uri.path; 95 | 96 | // 7. If stored url contains an unsupported scheme raise a NOT_SUPPORTED_ERR and terminate these steps. 97 | if (!(/https?|file/).test(uri.scheme)) 98 | throw new Error("NOT_SUPPORTED_ERR"); 99 | 100 | // 8. If the "user:password" format in the userinfo production defined in section 3.2.1 of RFC 3986 is not supported for the relevant scheme and stored url contains this format raise a SYNTAX_ERR and terminate these steps. [RFC3986] 101 | // 9. If stored url contains the "user:password" format let stored user be the user part and stored password be the password part. 102 | // 10. If stored url just contains the "user" format let stored user be the user part. 103 | if (uri.userinfo) { 104 | var components = uri.userinfo.split(":"); 105 | if (components.length === 1) 106 | this._user = components[0]; 107 | else if (components.length === 2) { 108 | this._user = components[0]; 109 | this._password = components[1]; 110 | } 111 | } 112 | 113 | // 11. If stored url is not of the same-origin as the origin of the Document pointer the user agent should raise a SECURITY_ERR exception and terminate these steps. 114 | // TODO? 115 | 116 | // 12. Let async be the value of the async argument or true if it was omitted. 117 | this._async = async === undefined ? true : async; 118 | 119 | // 13. If the user argument was not omitted, and its syntax does not match that specified by the relevant authentication scheme, raise a SYNTAX_ERR exception and terminate these steps. 120 | // 14. If the user argument was not omitted and is not null let stored user be user encoded using the encoding specified in the relevant authentication scheme or UTF-8 if the scheme fails to specify an encoding. 121 | // Note: This step overrides any user that may have been set by the url argument. 122 | // 15. If the user argument was not omitted and is null remove stored user. 123 | if (user !== undefined) 124 | this._user = user; 125 | 126 | 127 | // 16. If the password argument was not omitted and its syntax does not match that specified by the relevant authentication scheme raise a SYNTAX_ERR exception and terminate these steps. 128 | // 17. If the password argument was not omitted and is not null let stored password be password encoded using the encoding specified in the relevant authentication scheme or UTF-8 if the scheme fails to specify an encoding. 129 | // 18. If the password argument was not omitted and is null remove stored password. 130 | if (password !== undefined) 131 | this._password = password; 132 | 133 | this._url = url.toString(); 134 | 135 | // 19. Abort the send() algorithm, set response entity body to "null" and reset the list of request headers. 136 | this._requestHeaders = {}; 137 | this._responseBody = null 138 | 139 | // 20. The user agent should cancel any network activity for which the object is responsible. 140 | // 21. Switch the object to the OPENED state, set the send() flag to "false" and then synchronously dispatch a readystatechange event on the object and return the method call. 141 | 142 | this.readyState = OPENED; 143 | } 144 | 145 | // setRequestHeader(header, value), method 146 | // Each request has a list of request headers with associated values. The setRequestHeader() method can be used to manipulate those values and set new request headers. 147 | // The setRequestHeader() method appends a value if the HTTP header given as argument is already part of the list of request headers. 148 | XMLHttpRequest.prototype.setRequestHeader = function(header, value) 149 | { 150 | // When invoked, the user agent must follow the following steps (unless otherwise indicated): 151 | 152 | // 1. If the state of the object is not OPENED raise an INVALID_STATE_ERR exception and terminate these steps. 153 | if (this.readyState !== OPENED) 154 | throw new Error("INVALID_STATE_ERR"); 155 | 156 | // 2. If the send() flag is "true" raise an INVALID_STATE_ERR exception and terminate these steps. 157 | if (this._sendFlag) 158 | throw new Error("INVALID_STATE_ERR"); 159 | 160 | // 3. If the header argument does not match the field-name production as defined by section 4.2 of RFC 2616 or is null raise a SYNTAX_ERR exception and terminate these steps. [RFC2616] 161 | if (typeof header !== "string") // FIXME 162 | return; 163 | 164 | // 4. If the value argument is null terminate these steps. (Do not raise an exception.) 165 | if (!value) 166 | return; 167 | 168 | // 5. If the value argument does not match the field-value production as defined by section 4.2 of RFC 2616 raise a SYNTAX_ERR and terminate these steps. [RFC2616] 169 | if (typeof value !== "string") // FIXME 170 | return; 171 | 172 | // 6. For security reasons, these steps should be terminated if the header argument case-insensitively matches one of the following headers: 173 | // 7. Also for security reasons, these steps should be terminated if the start of the header argument case-insensitively matches Proxy- or Sec-. 174 | //if (header.match(/(^(Proxy-|Sec-)|^(Accept-Charset|Accept-Encoding|Connection|Content-Length|Content-Transfer-Encoding|Date|Expect|Host|Keep-Alive|Referer|TE|Trailer|Transfer-Encoding|Upgrade|Via)$)/i)) 175 | // return; 176 | 177 | // 8. If the header argument is not in the list of request headers append the header with its associated value to the list and terminate these steps. 178 | // 9. If the header argument is in the list of request headers either use multiple headers, combine the values or use a combination of those (section 4.2, RFC 2616). [RFC2616] 179 | var values = HashP.get(this._requestHeaders, header); 180 | if (values) 181 | values.push(value); 182 | else 183 | HashP.set(this._requestHeaders, header, [value]); 184 | } 185 | 186 | var initSend = function(data){ 187 | // When invoked, the user agent must follow the following steps (unless otherwise noted). Note that this algorithm might get aborted if the open() or abort() method is invoked. When the send() algorithm is aborted the user agent must terminate the algorithm after finishing the step it is on. 188 | 189 | // 1. If the state of the object is not OPENED raise an INVALID_STATE_ERR exception and terminate these steps. 190 | if (this.readyState !== OPENED) 191 | throw new Error("INVALID_STATE_ERR"); 192 | 193 | // 2. If the send() flag is "true" raise an INVALID_STATE_ERR exception and terminate these steps. 194 | if (this._sendFlag) 195 | throw new Error("INVALID_STATE_ERR"); 196 | 197 | // 3. If async is true set the send() flag to "true". 198 | if (this._async) 199 | this._sendFlag = true; 200 | 201 | // 4. If stored method is GET act as if the data argument is null. 202 | // If the data argument has not been omitted and is not null use it for the entity body as defined by section 7.2 of RFC 2616 observing the following rules: [RFC2616] 203 | if (this._method === "GET") 204 | data = null; 205 | 206 | return data; 207 | } 208 | 209 | function sendData(entityBody){ 210 | // 5. Make a request to stored url, using HTTP method stored method, user stored user (if provided) and password stored password (if provided), taking into account the entity body, list of request headers and the rules listed directly after this set of steps. 211 | // 6. Synchronously dispatch a readystatechange event on the object. 212 | if (this.onreadystatechange) 213 | this.onreadystatechange(); 214 | 215 | try { 216 | var uri = URI.parse(this._url); 217 | if (uri.scheme == null || uri.scheme == "file") { 218 | // unclear whether plusses are reserved in the URI path 219 | //uri.path = decodeURIComponent(uri.path.replace(/\+/g, " ")); 220 | if (this._method === "PUT") { 221 | if ((File.exists(uri.path) && File.isWritable(uri.path)) || File.path(uri.path).resolve('..').isWritable()) { 222 | this.output = File.write(uri.path, entityBody || new ByteString(), { mode : "b" }); 223 | this.status = 201; 224 | } else { 225 | this.status = 403; 226 | } 227 | } else if (this._method === "DELETE") { 228 | if (File.exists(uri.path)) { 229 | if (File.path(uri.path).resolve('..').isWritable()) { 230 | File.remove(uri.path); 231 | this.status = 200; 232 | } else { 233 | this.status = 403; 234 | } 235 | } else { 236 | this.status = 404; 237 | } 238 | } else { 239 | if (File.exists(uri.path)) { 240 | this.responseText = File.read(uri.path, { charset : "UTF-8" }); // FIXME: don't assume UTF-8? 241 | this.status = 200; 242 | } else { 243 | this.status = 404; 244 | } 245 | } 246 | } else { 247 | var url = new java.net.URL(this._url), 248 | connection = url.openConnection(); 249 | 250 | connection.setDoInput(true); 251 | 252 | connection.setRequestMethod(this._method); 253 | 254 | for (var header in this._requestHeaders) { 255 | var value = this._requestHeaders[header]; 256 | connection.addRequestProperty(String(header), String(value)); 257 | } 258 | 259 | var input = null; 260 | try { 261 | if (entityBody) { 262 | connection.setDoOutput(true); 263 | 264 | var output = new IO(null, connection.getOutputStream()); 265 | output.write(entityBody); 266 | output.close(); 267 | } 268 | connection.connect(); 269 | 270 | input = new IO(connection.getInputStream(), null); 271 | } catch (e) { 272 | // HttpUrlConnection will throw FileNotFoundException on 404 errors. FIXME: others? 273 | if (e.javaException instanceof java.io.FileNotFoundException) 274 | input = new IO(connection.getErrorStream(), null); 275 | else 276 | throw e; 277 | } 278 | 279 | this.status = Number(connection.getResponseCode()); 280 | this.statusText = String(connection.getResponseMessage() || ""); 281 | 282 | for (var i = 0;; i++) { 283 | var key = connection.getHeaderFieldKey(i), 284 | value = connection.getHeaderField(i); 285 | if (!key && !value) 286 | break; 287 | // returns the HTTP status code with no key, ignore it. 288 | if (key) 289 | this._responseHeaders[String(key)] = String(value); 290 | } 291 | 292 | //this.readyState = HEADERS_RECEIVED; 293 | //this.readyState = LOADING; 294 | 295 | this.responseText = input.read().decodeToString("UTF-8"); // FIXME: don't assume UTF-8? 296 | } 297 | system.log.debug("xhr response: " + this._url + " (status="+this.status+" length="+this.responseText.length+")"); 298 | } 299 | catch (e) { 300 | this.status = 500; 301 | this.responseText = ""; 302 | system.log.warn("xhr exception: " + this.url + " ("+e+")"); 303 | } 304 | 305 | this.responseXML = null; 306 | if (this.responseText) 307 | { 308 | var contentType = HashP.includes(this._responseHeaders, "Content-Type") ? HashP.get(this._responseHeaders, "Content-Type") : "text/xml"; 309 | 310 | if (contentType.match(/((^text\/xml$)|(^application\/xml$)|(\+xml$))/)) 311 | try { this.responseXML = parser.parseFromString(this.responseText, contentType); } catch (e) {} 312 | } 313 | 314 | // 7. If async is true return the send() method call. (Do not terminate the steps in the algorithm though.) 315 | // 8. While downloading the resource the following rules are to be observed. 316 | // 9. When the request has successfully completed loading, synchronously switch the state to DONE and then synchronously dispatch a readystatechange event on the object and return the method call in case of async being false. 317 | 318 | // FIXME: this is very very wrong 319 | this.readyState = DONE; 320 | if (this.onreadystatechange) { 321 | var that = this; 322 | if (this._async) 323 | require("./timeout").setTimeout(function() { that.onreadystatechange() }, 0); // FIXME 324 | else 325 | this.onreadystatechange(); 326 | } 327 | } 328 | 329 | XMLHttpRequest.prototype.sendAsBinary = function(data) { 330 | data = initSend.call(this, data); 331 | 332 | var entityBody; 333 | if (data == null || data === undefined ){ 334 | entityBody = null; 335 | } else if (typeof data === "string"){ 336 | throw new Error("Please use ByteString"); 337 | } else { 338 | entityBody = data; 339 | } 340 | 341 | sendData.call(this, entityBody); 342 | } 343 | 344 | // send(data), method 345 | // The send() method initiates the request and its optional argument provides the entity body. 346 | XMLHttpRequest.prototype.send = function(data) 347 | { 348 | data = initSend.call(this, data); 349 | 350 | var entityBody; 351 | if (data === null || data === undefined) { 352 | entityBody = null; 353 | } 354 | else if (typeof data === "string") { 355 | // data is a DOMString 356 | // Encode data using UTF-8 for transmission. 357 | entityBody = data.toByteString("UTF-8"); 358 | // If a Content-Type header is set using setRequestHeader() set the charset parameter of that header to UTF-8. 359 | var values = HashP.get(this._requestHeaders, "Content-Type"); 360 | if (values) 361 | values[0] = values[0].replace(/(;\s*charset=([^;]+|.*$)|$)/, "; charset=utf-8"); 362 | } 363 | //else if (data instanceof Document) { 364 | // data is a Document 365 | // Serialize data into a namespace well-formed XML document and encoded using the encoding given by data.inputEncoding, when not null, or UTF-8 otherwise. Or, if this fails because the Document cannot be serialized act as if data is null. 366 | // If no Content-Type header has been set using setRequestHeader() append a Content-Type header to the list of request headers with a value of application/xml;charset=charset where charset is the encoding used to encode the document. 367 | //} 368 | else { 369 | // data is not a DOMString or Document 370 | // Use the stringification mechanisms of the host language on data and treat the result as if data is a DOMString. Or, if this fails, act as if the data argument is null. 371 | // If the data argument has been omitted, or is null, no entity body is used in the request. 372 | entityBody = String(data).toByteString("UTF-8"); 373 | } 374 | 375 | sendData.call(this, entityBody); 376 | } 377 | 378 | XMLHttpRequest.prototype.abort = function() 379 | { 380 | // When invoked, the user agent must run the following steps (unless otherwise noted): 381 | 382 | // 1. Abort the send() algorithm, set the response entity body to "null", the error flag to "true" and remove any registered request headers. 383 | this._responseBody = null; 384 | this._errorFlag = true; 385 | this._requestHeaders = {}; 386 | 387 | // 2. The user agent should cancel any network activity for which the object is responsible. 388 | 389 | // 3. If the state is UNSENT, OPENED and the send() flag is "false", or DONE go to the next step. 390 | // 4. Otherwise, switch the state to DONE, set the send() flag to "false" and synchronously dispatch a readystatechange event on the object. 391 | if (!(this.readyState < HEADERS_RECEIVED && this._sendFlag === false) && !(this.readyState < DONE)) { 392 | this.readyState = DONE; 393 | this._sendFlag = false; 394 | if (this.onreadystatechange) 395 | this.onreadystatechange(); 396 | } 397 | 398 | // 5. Switch the state to UNSENT. (Do not dispatch the readystatechange event.) 399 | this.readyState = UNSENT; 400 | } 401 | 402 | XMLHttpRequest.prototype.getAllResponseHeaders = function() 403 | { 404 | // When invoked, the user agent must run the following steps: 405 | 406 | // 1. If the state is UNSENT or OPENED raise an INVALID_STATE_ERR exception and terminate these steps. 407 | if (this.readyState < HEADERS_RECEIVED) 408 | throw new Error("INVALID_STATE_ERR"); 409 | 410 | // 2. If the error flag is "true" return the empty string and terminate these steps. 411 | if (this._errorFlag) 412 | return ""; 413 | 414 | // 3. Return all the HTTP headers, as a single string, with each header line separated by a U+000D (CR) U+000A (LF) pair excluding the status line. 415 | var headerLines = []; 416 | for (var header in this._responseHeaders) 417 | headerLines.push(header + ": " + this._responseHeaders[header]); 418 | return headerLines.join("\n"); 419 | } 420 | 421 | XMLHttpRequest.prototype.getResponseHeader = function(header) 422 | { 423 | // When the method is invoked, the user agent must run the following steps: 424 | 425 | // 1. If the state is UNSENT or OPENED raise an INVALID_STATE_ERR exception and terminate these steps. 426 | if (this.readyState < HEADERS_RECEIVED) 427 | throw new Error("INVALID_STATE_ERR"); 428 | 429 | // 2. If the header argument does not match the field-name production return null and terminate these steps. 430 | // TODO 431 | 432 | // 3. If the error flag is "true" return null and terminate these steps. 433 | if (this._errorFlag) 434 | return null; 435 | 436 | // 4. If the header argument case-insensitively matches multiple HTTP headers for the last request sent, return the values of these headers as a single concatenated string separated from each other by an U+002C followed by an U+0020 character and terminate these steps. 437 | // 5. If the header argument case-insensitively matches a single HTTP header for the last request sent return the value of that header and terminate these steps. 438 | // 6. Return null. 439 | 440 | // FIXME: not quite correct 441 | return HashP.get(this._responseHeaders, header) || null; 442 | } --------------------------------------------------------------------------------