/* * Copyright (C) 2010 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ var InjectedFakeWorker = function(InjectedScriptHost, inspectedWindow, injectedScriptId) { Worker = function(url) { var impl = new FakeWorker(this, url); if (impl === null) return null; this.isFake = true; this.postMessage = bind(impl.postMessage, impl); this.terminate = bind(impl.terminate, impl); function onmessageGetter() { return impl.channel.port1.onmessage; } function onmessageSetter(callback) { impl.channel.port1.onmessage = callback; } this.__defineGetter__("onmessage", onmessageGetter); this.__defineSetter__("onmessage", onmessageSetter); this.addEventListener = bind(impl.channel.port1.addEventListener, impl.channel.port1); this.removeEventListener = bind(impl.channel.port1.removeEventListener, impl.channel.port1); this.dispatchEvent = bind(impl.channel.port1.dispatchEvent, impl.channel.port1); } function FakeWorker(worker, url) { var scriptURL = this._expandURLAndCheckOrigin(document.baseURI, location.href, url); this._worker = worker; this._id = InjectedScriptHost.nextWorkerId(); this.channel = new MessageChannel(); this._listeners = []; this._buildWorker(scriptURL); InjectedScriptHost.didCreateWorker(this._id, scriptURL.url, false); } FakeWorker.prototype = { postMessage: function(msg, opt_ports) { if (this._frame != null) this.channel.port1.postMessage.apply(this.channel.port1, arguments); else if (this._pendingMessages) this._pendingMessages.push(arguments) else this._pendingMessages = [ arguments ]; }, terminate: function() { InjectedScriptHost.didDestroyWorker(this._id); this.channel.port1.close(); this.channel.port2.close(); if (this._frame != null) this._frame.frameElement.parentNode.removeChild(this._frame.frameElement); this._frame = null; this._worker = null; // Break reference loop. }, _buildWorker: function(url) { var code = this._loadScript(url.url); var iframeElement = document.createElement("iframe"); iframeElement.style.display = "none"; this._document = document; iframeElement.onload = bind(this._onWorkerFrameLoaded, this, iframeElement, url, code); if (document.body) this._attachWorkerFrameToDocument(iframeElement, url, code); else window.addEventListener("load", bind(this._attachWorkerFrameToDocument, this, iframeElement), false); }, _attachWorkerFrameToDocument: function(iframeElement) { document.body.appendChild(iframeElement); }, _onWorkerFrameLoaded: function(iframeElement, url, code) { var frame = iframeElement.contentWindow; this._frame = frame; this._setupWorkerContext(frame, url); var frameContents = '(function() { var location = __devtools.location; var window; ' + code + '})();\n' + '//@ sourceURL=' + url.url; frame.eval(frameContents); if (this._pendingMessages) { for (var msg = 0; msg < this._pendingMessages.length; ++msg) this.postMessage.apply(this, this._pendingMessages[msg]); delete this._pendingMessages; } }, _setupWorkerContext: function(workerFrame, url) { workerFrame.__devtools = { handleException: bind(this._handleException, this), location: url.mockLocation() }; var self = this; function onmessageGetter() { return self.channel.port2.onmessage ? self.channel.port2.onmessage.originalCallback : null; } function onmessageSetter(callback) { var wrappedCallback = bind(self._callbackWrapper, self, callback); wrappedCallback.originalCallback = callback; self.channel.port2.onmessage = wrappedCallback; } workerFrame.__defineGetter__("onmessage", onmessageGetter); workerFrame.__defineSetter__("onmessage", onmessageSetter); workerFrame.addEventListener = bind(this._addEventListener, this); workerFrame.removeEventListener = bind(this._removeEventListener, this); workerFrame.dispatchEvent = bind(this.channel.port2.dispatchEvent, this.channel.port2); workerFrame.postMessage = bind(this.channel.port2.postMessage, this.channel.port2); workerFrame.importScripts = bind(this._importScripts, this, workerFrame); workerFrame.close = bind(this.terminate, this); }, _addEventListener: function(type, callback, useCapture) { var wrappedCallback = bind(this._callbackWrapper, this, callback); wrappedCallback.originalCallback = callback; wrappedCallback.type = type; wrappedCallback.useCapture = Boolean(useCapture); this.channel.port2.addEventListener(type, wrappedCallback, useCapture); this._listeners.push(wrappedCallback); }, _removeEventListener: function(type, callback, useCapture) { var listeners = this._listeners; for (var i = 0; i < listeners.length; ++i) { if (listeners[i].originalCallback === callback && listeners[i].type === type && listeners[i].useCapture === Boolean(useCapture)) { this.channel.port2.removeEventListener(type, listeners[i], useCapture); listeners[i] = listeners[listeners.length - 1]; listeners.pop(); break; } } }, _callbackWrapper: function(callback, msg) { // Shortcut -- if no exception handlers installed, avoid try/catch so as not to obscure line number. if (!this._frame.onerror && !this._worker.onerror) { callback(msg); return; } try { callback(msg); } catch (e) { this._handleException(e, this._frame.onerror, this._worker.onerror); } }, _handleException: function(e) { // NB: it should be an ErrorEvent, but creating it from script is not // currently supported, so emulate it on top of plain vanilla Event. var errorEvent = this._document.createEvent("Event"); errorEvent.initEvent("Event", false, false); errorEvent.message = "Uncaught exception"; for (var i = 1; i < arguments.length; ++i) { if (arguments[i] && arguments[i](errorEvent)) return; } throw e; }, _importScripts: function(targetFrame) { for (var i = 1; i < arguments.length; ++i) { var workerOrigin = targetFrame.__devtools.location.href; var url = this._expandURLAndCheckOrigin(workerOrigin, workerOrigin, arguments[i]); targetFrame.eval(this._loadScript(url.url) + "\n//@ sourceURL= " + url.url); } }, _loadScript: function(url) { var xhr = new XMLHttpRequest(); xhr.open("GET", url, false); xhr.send(null); var text = xhr.responseText; if (xhr.status != 0 && xhr.status/100 !== 2) { // We're getting status === 0 when using file://. console.error("Failed to load worker: " + url + "[" + xhr.status + "]"); text = ""; // We've got error message, not worker code. } return text; }, _expandURLAndCheckOrigin: function(baseURL, origin, url) { var scriptURL = new URL(baseURL).completeWith(url); if (!scriptURL.sameOrigin(origin)) throw new DOMCoreException("SECURITY_ERR",18); return scriptURL; } }; function URL(url) { this.url = url; this.split(); } URL.prototype = { urlRegEx: (/^(http[s]?|file):\/\/([^\/:]*)(:[\d]+)?(?:(\/[^#?]*)(\?[^#]*)?(?:#(.*))?)?$/i), split: function() { function emptyIfNull(str) { return str == null ? "" : str; } var parts = this.urlRegEx.exec(this.url); this.schema = parts[1]; this.host = parts[2]; this.port = emptyIfNull(parts[3]); this.path = emptyIfNull(parts[4]); this.query = emptyIfNull(parts[5]); this.fragment = emptyIfNull(parts[6]); }, mockLocation: function() { var host = this.host.replace(/^[^@]*@/, ""); return { href: this.url, protocol: this.schema + ":", host: host, hostname: host, port: this.port, pathname: this.path, search: this.query, hash: this.fragment }; }, completeWith: function(url) { if (url === "" || /^[^/]*:/.exec(url)) // If given absolute url, return as is now. return new URL(url); var relParts = /^([^#?]*)(.*)$/.exec(url); // => [ url, path, query-andor-fragment ] var path = (relParts[1].slice(0, 1) === "/" ? "" : this.path.replace(/[^/]*$/, "")) + relParts[1]; path = path.replace(/(\/\.)+(\/|$)/g, "/").replace(/[^/]*\/\.\.(\/|$)/g, ""); return new URL(this.schema + "://" + this.host + this.port + path + relParts[2]); }, sameOrigin: function(url) { function normalizePort(schema, port) { var portNo = port.slice(1); return (schema === "https" && portNo == 443 || schema === "http" && portNo == 80) ? "" : port; } var other = new URL(url); return this.schema === other.schema && this.host === other.host && normalizePort(this.schema, this.port) === normalizePort(other.schema, other.port); } }; function DOMCoreException(name, code) { function formatError() { return "Error: " + this.message; } this.name = name; this.message = name + ": DOM Exception " + code; this.code = code; this.toString = bind(formatError, this); } function bind(func, thisObject) { var args = Array.prototype.slice.call(arguments, 2); return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))); }; } function noop() { } }