Home | History | Annotate | Download | only in front-end
      1 /*
      2  * Copyright (C) 2010 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are
      6  * met:
      7  *
      8  *     * Redistributions of source code must retain the above copyright
      9  * notice, this list of conditions and the following disclaimer.
     10  *     * Redistributions in binary form must reproduce the above
     11  * copyright notice, this list of conditions and the following disclaimer
     12  * in the documentation and/or other materials provided with the
     13  * distribution.
     14  *     * Neither the name of Google Inc. nor the names of its
     15  * contributors may be used to endorse or promote products derived from
     16  * this software without specific prior written permission.
     17  *
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 
     31 var InjectedFakeWorker = function(InjectedScriptHost, inspectedWindow, injectedScriptId)
     32 {
     33 
     34 Worker = function(url)
     35 {
     36     var impl = new FakeWorker(this, url);
     37     if (impl === null)
     38         return null;
     39 
     40     this.isFake = true;
     41     this.postMessage = bind(impl.postMessage, impl);
     42     this.terminate = bind(impl.terminate, impl);
     43 
     44     function onmessageGetter()
     45     {
     46         return impl.channel.port1.onmessage;
     47     }
     48     function onmessageSetter(callback)
     49     {
     50         impl.channel.port1.onmessage = callback;
     51     }
     52     this.__defineGetter__("onmessage", onmessageGetter);
     53     this.__defineSetter__("onmessage", onmessageSetter);
     54     this.addEventListener = bind(impl.channel.port1.addEventListener, impl.channel.port1);
     55     this.removeEventListener = bind(impl.channel.port1.removeEventListener, impl.channel.port1);
     56     this.dispatchEvent = bind(impl.channel.port1.dispatchEvent, impl.channel.port1);
     57 }
     58 
     59 function FakeWorker(worker, url)
     60 {
     61     var scriptURL = this._expandURLAndCheckOrigin(document.baseURI, location.href, url);
     62 
     63     this._worker = worker;
     64     this._id = InjectedScriptHost.nextWorkerId();
     65     this.channel = new MessageChannel();
     66     this._listeners = [];
     67     this._buildWorker(scriptURL);
     68 
     69     InjectedScriptHost.didCreateWorker(this._id, scriptURL.url, false);
     70 }
     71 
     72 FakeWorker.prototype = {
     73     postMessage: function(msg, opt_ports)
     74     {
     75         if (this._frame != null)
     76             this.channel.port1.postMessage.apply(this.channel.port1, arguments);
     77         else if (this._pendingMessages)
     78             this._pendingMessages.push(arguments)
     79         else
     80             this._pendingMessages = [ arguments ];
     81     },
     82 
     83     terminate: function()
     84     {
     85         InjectedScriptHost.didDestroyWorker(this._id);
     86 
     87         this.channel.port1.close();
     88         this.channel.port2.close();
     89         if (this._frame != null)
     90             this._frame.frameElement.parentNode.removeChild(this._frame.frameElement);
     91         this._frame = null;
     92         this._worker = null; // Break reference loop.
     93     },
     94 
     95     _buildWorker: function(url)
     96     {
     97         var code = this._loadScript(url.url);
     98         var iframeElement = document.createElement("iframe");
     99         iframeElement.style.display = "none";
    100 
    101         this._document = document;
    102         iframeElement.onload = bind(this._onWorkerFrameLoaded, this, iframeElement, url, code);
    103 
    104         if (document.body)
    105             this._attachWorkerFrameToDocument(iframeElement, url, code);
    106         else
    107             window.addEventListener("load", bind(this._attachWorkerFrameToDocument, this, iframeElement), false);
    108     },
    109 
    110     _attachWorkerFrameToDocument: function(iframeElement)
    111     {
    112         document.body.appendChild(iframeElement);
    113     },
    114 
    115     _onWorkerFrameLoaded: function(iframeElement, url, code)
    116     {
    117         var frame = iframeElement.contentWindow;
    118         this._frame = frame;
    119         this._setupWorkerContext(frame, url);
    120 
    121         var frameContents = '(function() { var location = __devtools.location; var window; ' + code + '})();\n' + '//@ sourceURL=' + url.url;
    122 
    123         frame.eval(frameContents);
    124         if (this._pendingMessages) {
    125             for (var msg = 0; msg < this._pendingMessages.length; ++msg)
    126                 this.postMessage.apply(this, this._pendingMessages[msg]);
    127             delete this._pendingMessages;
    128         }
    129     },
    130 
    131     _setupWorkerContext: function(workerFrame, url)
    132     {
    133         workerFrame.__devtools = {
    134             handleException: bind(this._handleException, this),
    135             location: url.mockLocation()
    136         };
    137 
    138         var self = this;
    139 
    140         function onmessageGetter()
    141         {
    142             return self.channel.port2.onmessage ? self.channel.port2.onmessage.originalCallback : null;
    143         }
    144 
    145         function onmessageSetter(callback)
    146         {
    147             var wrappedCallback = bind(self._callbackWrapper, self, callback);
    148             wrappedCallback.originalCallback = callback;
    149             self.channel.port2.onmessage = wrappedCallback;
    150         }
    151 
    152         workerFrame.__defineGetter__("onmessage", onmessageGetter);
    153         workerFrame.__defineSetter__("onmessage", onmessageSetter);
    154         workerFrame.addEventListener = bind(this._addEventListener, this);
    155         workerFrame.removeEventListener = bind(this._removeEventListener, this);
    156         workerFrame.dispatchEvent = bind(this.channel.port2.dispatchEvent, this.channel.port2);
    157         workerFrame.postMessage = bind(this.channel.port2.postMessage, this.channel.port2);
    158         workerFrame.importScripts = bind(this._importScripts, this, workerFrame);
    159         workerFrame.close = bind(this.terminate, this);
    160     },
    161 
    162     _addEventListener: function(type, callback, useCapture)
    163     {
    164         var wrappedCallback = bind(this._callbackWrapper, this, callback);
    165         wrappedCallback.originalCallback = callback;
    166         wrappedCallback.type = type;
    167         wrappedCallback.useCapture = Boolean(useCapture);
    168 
    169         this.channel.port2.addEventListener(type, wrappedCallback, useCapture);
    170         this._listeners.push(wrappedCallback);
    171     },
    172 
    173     _removeEventListener: function(type, callback, useCapture)
    174     {
    175         var listeners = this._listeners;
    176         for (var i = 0; i < listeners.length; ++i) {
    177             if (listeners[i].originalCallback === callback &&
    178                 listeners[i].type === type &&
    179                 listeners[i].useCapture === Boolean(useCapture)) {
    180                 this.channel.port2.removeEventListener(type, listeners[i], useCapture);
    181                 listeners[i] = listeners[listeners.length - 1];
    182                 listeners.pop();
    183                 break;
    184             }
    185         }
    186     },
    187 
    188     _callbackWrapper: function(callback, msg)
    189     {
    190         // Shortcut -- if no exception handlers installed, avoid try/catch so as not to obscure line number.
    191         if (!this._frame.onerror && !this._worker.onerror) {
    192             callback(msg);
    193             return;
    194         }
    195 
    196         try {
    197             callback(msg);
    198         } catch (e) {
    199             this._handleException(e, this._frame.onerror, this._worker.onerror);
    200         }
    201     },
    202 
    203     _handleException: function(e)
    204     {
    205         // NB: it should be an ErrorEvent, but creating it from script is not
    206         // currently supported, so emulate it on top of plain vanilla Event.
    207         var errorEvent = this._document.createEvent("Event");
    208         errorEvent.initEvent("Event", false, false);
    209         errorEvent.message = "Uncaught exception";
    210 
    211         for (var i = 1; i < arguments.length; ++i) {
    212             if (arguments[i] && arguments[i](errorEvent))
    213                 return;
    214         }
    215 
    216         throw e;
    217     },
    218 
    219     _importScripts: function(targetFrame)
    220     {
    221         for (var i = 1; i < arguments.length; ++i) {
    222             var workerOrigin = targetFrame.__devtools.location.href;
    223             var url = this._expandURLAndCheckOrigin(workerOrigin, workerOrigin, arguments[i]);
    224             targetFrame.eval(this._loadScript(url.url) + "\n//@ sourceURL= " + url.url);
    225         }
    226     },
    227 
    228     _loadScript: function(url)
    229     {
    230         var xhr = new XMLHttpRequest();
    231         xhr.open("GET", url, false);
    232         xhr.send(null);
    233 
    234         var text = xhr.responseText;
    235         if (xhr.status != 0 && xhr.status/100 !== 2) { // We're getting status === 0 when using file://.
    236             console.error("Failed to load worker: " + url + "[" + xhr.status + "]");
    237             text = ""; // We've got error message, not worker code.
    238         }
    239         return text;
    240     },
    241 
    242     _expandURLAndCheckOrigin: function(baseURL, origin, url)
    243     {
    244         var scriptURL = new URL(baseURL).completeWith(url);
    245 
    246         if (!scriptURL.sameOrigin(origin))
    247             throw new DOMCoreException("SECURITY_ERR",18);
    248         return scriptURL;
    249     }
    250 };
    251 
    252 function URL(url)
    253 {
    254     this.url = url;
    255     this.split();
    256 }
    257 
    258 URL.prototype = {
    259     urlRegEx: (/^(http[s]?|file):\/\/([^\/:]*)(:[\d]+)?(?:(\/[^#?]*)(\?[^#]*)?(?:#(.*))?)?$/i),
    260 
    261     split: function()
    262     {
    263         function emptyIfNull(str)
    264         {
    265             return str == null ? "" : str;
    266         }
    267         var parts = this.urlRegEx.exec(this.url);
    268 
    269         this.schema = parts[1];
    270         this.host = parts[2];
    271         this.port = emptyIfNull(parts[3]);
    272         this.path = emptyIfNull(parts[4]);
    273         this.query = emptyIfNull(parts[5]);
    274         this.fragment = emptyIfNull(parts[6]);
    275     },
    276 
    277     mockLocation: function()
    278     {
    279         var host = this.host.replace(/^[^@]*@/, "");
    280 
    281         return {
    282             href:     this.url,
    283             protocol: this.schema + ":",
    284             host:     host,
    285             hostname: host,
    286             port:     this.port,
    287             pathname: this.path,
    288             search:   this.query,
    289             hash:     this.fragment
    290         };
    291     },
    292 
    293     completeWith: function(url)
    294     {
    295         if (url === "" || /^[^/]*:/.exec(url)) // If given absolute url, return as is now.
    296             return new URL(url);
    297 
    298         var relParts = /^([^#?]*)(.*)$/.exec(url); // => [ url, path, query-andor-fragment ]
    299 
    300         var path = (relParts[1].slice(0, 1) === "/" ? "" : this.path.replace(/[^/]*$/, "")) + relParts[1];
    301         path = path.replace(/(\/\.)+(\/|$)/g, "/").replace(/[^/]*\/\.\.(\/|$)/g, "");
    302 
    303         return new URL(this.schema + "://" + this.host + this.port + path + relParts[2]);
    304     },
    305 
    306     sameOrigin: function(url)
    307     {
    308         function normalizePort(schema, port)
    309         {
    310             var portNo = port.slice(1);
    311             return (schema === "https" && portNo == 443 || schema === "http" && portNo == 80) ? "" : port;
    312         }
    313 
    314         var other = new URL(url);
    315 
    316         return this.schema === other.schema &&
    317             this.host === other.host &&
    318             normalizePort(this.schema, this.port) === normalizePort(other.schema, other.port);
    319     }
    320 };
    321 
    322 function DOMCoreException(name, code)
    323 {
    324     function formatError()
    325     {
    326         return "Error: " + this.message;
    327     }
    328 
    329     this.name = name;
    330     this.message = name + ": DOM Exception " + code;
    331     this.code = code;
    332     this.toString = bind(formatError, this);
    333 }
    334 
    335 function bind(func, thisObject)
    336 {
    337     var args = Array.prototype.slice.call(arguments, 2);
    338     return function() { return func.apply(thisObject, args.concat(Array.prototype.slice.call(arguments, 0))); };
    339 }
    340 
    341 function noop()
    342 {
    343 }
    344 
    345 }
    346