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