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
31var InjectedFakeWorker = function(InjectedScriptHost, inspectedWindow, injectedScriptId)
32{
33
34Worker = 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
59function 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
72FakeWorker.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
252function URL(url)
253{
254    this.url = url;
255    this.split();
256}
257
258URL.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
322function 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
335function 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
341function noop()
342{
343}
344
345}
346