InjectedFakeWorker.js   [plain text]


/*
 * 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()
{
}

}