Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | dump("######################## BrowserElementChildPreload.js loaded\n"); |
michael@0 | 8 | |
michael@0 | 9 | var BrowserElementIsReady = false; |
michael@0 | 10 | |
michael@0 | 11 | let { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components; |
michael@0 | 12 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 13 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 14 | Cu.import("resource://gre/modules/BrowserElementPromptService.jsm"); |
michael@0 | 15 | |
michael@0 | 16 | // Event whitelisted for bubbling. |
michael@0 | 17 | let whitelistedEvents = [ |
michael@0 | 18 | Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE, // Back button. |
michael@0 | 19 | Ci.nsIDOMKeyEvent.DOM_VK_SLEEP, // Power button. |
michael@0 | 20 | Ci.nsIDOMKeyEvent.DOM_VK_CONTEXT_MENU, |
michael@0 | 21 | Ci.nsIDOMKeyEvent.DOM_VK_F5, // Search button. |
michael@0 | 22 | Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP, // Volume up. |
michael@0 | 23 | Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN // Volume down. |
michael@0 | 24 | ]; |
michael@0 | 25 | |
michael@0 | 26 | function debug(msg) { |
michael@0 | 27 | //dump("BrowserElementChildPreload - " + msg + "\n"); |
michael@0 | 28 | } |
michael@0 | 29 | |
michael@0 | 30 | function sendAsyncMsg(msg, data) { |
michael@0 | 31 | // Ensure that we don't send any messages before BrowserElementChild.js |
michael@0 | 32 | // finishes loading. |
michael@0 | 33 | if (!BrowserElementIsReady) |
michael@0 | 34 | return; |
michael@0 | 35 | |
michael@0 | 36 | if (!data) { |
michael@0 | 37 | data = { }; |
michael@0 | 38 | } |
michael@0 | 39 | |
michael@0 | 40 | data.msg_name = msg; |
michael@0 | 41 | sendAsyncMessage('browser-element-api:call', data); |
michael@0 | 42 | } |
michael@0 | 43 | |
michael@0 | 44 | function sendSyncMsg(msg, data) { |
michael@0 | 45 | // Ensure that we don't send any messages before BrowserElementChild.js |
michael@0 | 46 | // finishes loading. |
michael@0 | 47 | if (!BrowserElementIsReady) |
michael@0 | 48 | return; |
michael@0 | 49 | |
michael@0 | 50 | if (!data) { |
michael@0 | 51 | data = { }; |
michael@0 | 52 | } |
michael@0 | 53 | |
michael@0 | 54 | data.msg_name = msg; |
michael@0 | 55 | return sendSyncMessage('browser-element-api:call', data); |
michael@0 | 56 | } |
michael@0 | 57 | |
michael@0 | 58 | let CERTIFICATE_ERROR_PAGE_PREF = 'security.alternate_certificate_error_page'; |
michael@0 | 59 | |
michael@0 | 60 | let NS_ERROR_MODULE_BASE_OFFSET = 0x45; |
michael@0 | 61 | let NS_ERROR_MODULE_SECURITY= 21; |
michael@0 | 62 | function NS_ERROR_GET_MODULE(err) { |
michael@0 | 63 | return ((((err) >> 16) - NS_ERROR_MODULE_BASE_OFFSET) & 0x1fff); |
michael@0 | 64 | } |
michael@0 | 65 | |
michael@0 | 66 | function NS_ERROR_GET_CODE(err) { |
michael@0 | 67 | return ((err) & 0xffff); |
michael@0 | 68 | } |
michael@0 | 69 | |
michael@0 | 70 | let SEC_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SEC_ERROR_BASE; |
michael@0 | 71 | let SEC_ERROR_UNKNOWN_ISSUER = (SEC_ERROR_BASE + 13); |
michael@0 | 72 | let SEC_ERROR_CA_CERT_INVALID = (SEC_ERROR_BASE + 36); |
michael@0 | 73 | let SEC_ERROR_UNTRUSTED_ISSUER = (SEC_ERROR_BASE + 20); |
michael@0 | 74 | let SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE = (SEC_ERROR_BASE + 30); |
michael@0 | 75 | let SEC_ERROR_UNTRUSTED_CERT = (SEC_ERROR_BASE + 21); |
michael@0 | 76 | let SEC_ERROR_EXPIRED_CERTIFICATE = (SEC_ERROR_BASE + 11); |
michael@0 | 77 | let SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED = (SEC_ERROR_BASE + 176); |
michael@0 | 78 | |
michael@0 | 79 | let SSL_ERROR_BASE = Ci.nsINSSErrorsService.NSS_SSL_ERROR_BASE; |
michael@0 | 80 | let SSL_ERROR_BAD_CERT_DOMAIN = (SSL_ERROR_BASE + 12); |
michael@0 | 81 | |
michael@0 | 82 | function getErrorClass(errorCode) { |
michael@0 | 83 | let NSPRCode = -1 * NS_ERROR_GET_CODE(errorCode); |
michael@0 | 84 | |
michael@0 | 85 | switch (NSPRCode) { |
michael@0 | 86 | case SEC_ERROR_UNKNOWN_ISSUER: |
michael@0 | 87 | case SEC_ERROR_UNTRUSTED_ISSUER: |
michael@0 | 88 | case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE: |
michael@0 | 89 | case SEC_ERROR_UNTRUSTED_CERT: |
michael@0 | 90 | case SSL_ERROR_BAD_CERT_DOMAIN: |
michael@0 | 91 | case SEC_ERROR_EXPIRED_CERTIFICATE: |
michael@0 | 92 | case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED: |
michael@0 | 93 | case SEC_ERROR_CA_CERT_INVALID: |
michael@0 | 94 | return Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT; |
michael@0 | 95 | default: |
michael@0 | 96 | return Ci.nsINSSErrorsService.ERROR_CLASS_SSL_PROTOCOL; |
michael@0 | 97 | } |
michael@0 | 98 | |
michael@0 | 99 | return null; |
michael@0 | 100 | } |
michael@0 | 101 | |
michael@0 | 102 | const OBSERVED_EVENTS = [ |
michael@0 | 103 | 'fullscreen-origin-change', |
michael@0 | 104 | 'ask-parent-to-exit-fullscreen', |
michael@0 | 105 | 'ask-parent-to-rollback-fullscreen', |
michael@0 | 106 | 'xpcom-shutdown', |
michael@0 | 107 | 'activity-done' |
michael@0 | 108 | ]; |
michael@0 | 109 | |
michael@0 | 110 | /** |
michael@0 | 111 | * The BrowserElementChild implements one half of <iframe mozbrowser>. |
michael@0 | 112 | * (The other half is, unsurprisingly, BrowserElementParent.) |
michael@0 | 113 | * |
michael@0 | 114 | * This script is injected into an <iframe mozbrowser> via |
michael@0 | 115 | * nsIMessageManager::LoadFrameScript(). |
michael@0 | 116 | * |
michael@0 | 117 | * Our job here is to listen for events within this frame and bubble them up to |
michael@0 | 118 | * the parent process. |
michael@0 | 119 | */ |
michael@0 | 120 | |
michael@0 | 121 | var global = this; |
michael@0 | 122 | |
michael@0 | 123 | function BrowserElementChild() { |
michael@0 | 124 | // Maps outer window id --> weak ref to window. Used by modal dialog code. |
michael@0 | 125 | this._windowIDDict = {}; |
michael@0 | 126 | |
michael@0 | 127 | // _forcedVisible corresponds to the visibility state our owner has set on us |
michael@0 | 128 | // (via iframe.setVisible). ownerVisible corresponds to whether the docShell |
michael@0 | 129 | // whose window owns this element is visible. |
michael@0 | 130 | // |
michael@0 | 131 | // Our docShell is visible iff _forcedVisible and _ownerVisible are both |
michael@0 | 132 | // true. |
michael@0 | 133 | this._forcedVisible = true; |
michael@0 | 134 | this._ownerVisible = true; |
michael@0 | 135 | |
michael@0 | 136 | this._nextPaintHandler = null; |
michael@0 | 137 | |
michael@0 | 138 | this._isContentWindowCreated = false; |
michael@0 | 139 | this._pendingSetInputMethodActive = []; |
michael@0 | 140 | |
michael@0 | 141 | this._init(); |
michael@0 | 142 | }; |
michael@0 | 143 | |
michael@0 | 144 | BrowserElementChild.prototype = { |
michael@0 | 145 | |
michael@0 | 146 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, |
michael@0 | 147 | Ci.nsISupportsWeakReference]), |
michael@0 | 148 | |
michael@0 | 149 | _init: function() { |
michael@0 | 150 | debug("Starting up."); |
michael@0 | 151 | |
michael@0 | 152 | BrowserElementPromptService.mapWindowToBrowserElementChild(content, this); |
michael@0 | 153 | |
michael@0 | 154 | docShell.QueryInterface(Ci.nsIWebProgress) |
michael@0 | 155 | .addProgressListener(this._progressListener, |
michael@0 | 156 | Ci.nsIWebProgress.NOTIFY_LOCATION | |
michael@0 | 157 | Ci.nsIWebProgress.NOTIFY_SECURITY | |
michael@0 | 158 | Ci.nsIWebProgress.NOTIFY_STATE_WINDOW); |
michael@0 | 159 | |
michael@0 | 160 | docShell.QueryInterface(Ci.nsIWebNavigation) |
michael@0 | 161 | .sessionHistory = Cc["@mozilla.org/browser/shistory;1"] |
michael@0 | 162 | .createInstance(Ci.nsISHistory); |
michael@0 | 163 | |
michael@0 | 164 | // This is necessary to get security web progress notifications. |
michael@0 | 165 | var securityUI = Cc['@mozilla.org/secure_browser_ui;1'] |
michael@0 | 166 | .createInstance(Ci.nsISecureBrowserUI); |
michael@0 | 167 | securityUI.init(content); |
michael@0 | 168 | |
michael@0 | 169 | // A cache of the menuitem dom objects keyed by the id we generate |
michael@0 | 170 | // and pass to the embedder |
michael@0 | 171 | this._ctxHandlers = {}; |
michael@0 | 172 | // Counter of contextmenu events fired |
michael@0 | 173 | this._ctxCounter = 0; |
michael@0 | 174 | |
michael@0 | 175 | this._shuttingDown = false; |
michael@0 | 176 | |
michael@0 | 177 | addEventListener('DOMTitleChanged', |
michael@0 | 178 | this._titleChangedHandler.bind(this), |
michael@0 | 179 | /* useCapture = */ true, |
michael@0 | 180 | /* wantsUntrusted = */ false); |
michael@0 | 181 | |
michael@0 | 182 | addEventListener('DOMLinkAdded', |
michael@0 | 183 | this._linkAddedHandler.bind(this), |
michael@0 | 184 | /* useCapture = */ true, |
michael@0 | 185 | /* wantsUntrusted = */ false); |
michael@0 | 186 | |
michael@0 | 187 | addEventListener('DOMMetaAdded', |
michael@0 | 188 | this._metaAddedHandler.bind(this), |
michael@0 | 189 | /* useCapture = */ true, |
michael@0 | 190 | /* wantsUntrusted = */ false); |
michael@0 | 191 | |
michael@0 | 192 | // This listens to unload events from our message manager, but /not/ from |
michael@0 | 193 | // the |content| window. That's because the window's unload event doesn't |
michael@0 | 194 | // bubble, and we're not using a capturing listener. If we'd used |
michael@0 | 195 | // useCapture == true, we /would/ hear unload events from the window, which |
michael@0 | 196 | // is not what we want! |
michael@0 | 197 | addEventListener('unload', |
michael@0 | 198 | this._unloadHandler.bind(this), |
michael@0 | 199 | /* useCapture = */ false, |
michael@0 | 200 | /* wantsUntrusted = */ false); |
michael@0 | 201 | |
michael@0 | 202 | // Registers a MozAfterPaint handler for the very first paint. |
michael@0 | 203 | this._addMozAfterPaintHandler(function () { |
michael@0 | 204 | sendAsyncMsg('firstpaint'); |
michael@0 | 205 | }); |
michael@0 | 206 | |
michael@0 | 207 | let self = this; |
michael@0 | 208 | |
michael@0 | 209 | let mmCalls = { |
michael@0 | 210 | "purge-history": this._recvPurgeHistory, |
michael@0 | 211 | "get-screenshot": this._recvGetScreenshot, |
michael@0 | 212 | "set-visible": this._recvSetVisible, |
michael@0 | 213 | "get-visible": this._recvVisible, |
michael@0 | 214 | "send-mouse-event": this._recvSendMouseEvent, |
michael@0 | 215 | "send-touch-event": this._recvSendTouchEvent, |
michael@0 | 216 | "get-can-go-back": this._recvCanGoBack, |
michael@0 | 217 | "get-can-go-forward": this._recvCanGoForward, |
michael@0 | 218 | "go-back": this._recvGoBack, |
michael@0 | 219 | "go-forward": this._recvGoForward, |
michael@0 | 220 | "reload": this._recvReload, |
michael@0 | 221 | "stop": this._recvStop, |
michael@0 | 222 | "unblock-modal-prompt": this._recvStopWaiting, |
michael@0 | 223 | "fire-ctx-callback": this._recvFireCtxCallback, |
michael@0 | 224 | "owner-visibility-change": this._recvOwnerVisibilityChange, |
michael@0 | 225 | "exit-fullscreen": this._recvExitFullscreen.bind(this), |
michael@0 | 226 | "activate-next-paint-listener": this._activateNextPaintListener.bind(this), |
michael@0 | 227 | "set-input-method-active": this._recvSetInputMethodActive.bind(this), |
michael@0 | 228 | "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this) |
michael@0 | 229 | } |
michael@0 | 230 | |
michael@0 | 231 | addMessageListener("browser-element-api:call", function(aMessage) { |
michael@0 | 232 | if (aMessage.data.msg_name in mmCalls) { |
michael@0 | 233 | return mmCalls[aMessage.data.msg_name].apply(self, arguments); |
michael@0 | 234 | } |
michael@0 | 235 | }); |
michael@0 | 236 | |
michael@0 | 237 | let els = Cc["@mozilla.org/eventlistenerservice;1"] |
michael@0 | 238 | .getService(Ci.nsIEventListenerService); |
michael@0 | 239 | |
michael@0 | 240 | // We are using the system group for those events so if something in the |
michael@0 | 241 | // content called .stopPropagation() this will still be called. |
michael@0 | 242 | els.addSystemEventListener(global, 'keydown', |
michael@0 | 243 | this._keyEventHandler.bind(this), |
michael@0 | 244 | /* useCapture = */ true); |
michael@0 | 245 | els.addSystemEventListener(global, 'keypress', |
michael@0 | 246 | this._keyEventHandler.bind(this), |
michael@0 | 247 | /* useCapture = */ true); |
michael@0 | 248 | els.addSystemEventListener(global, 'keyup', |
michael@0 | 249 | this._keyEventHandler.bind(this), |
michael@0 | 250 | /* useCapture = */ true); |
michael@0 | 251 | els.addSystemEventListener(global, 'DOMWindowClose', |
michael@0 | 252 | this._windowCloseHandler.bind(this), |
michael@0 | 253 | /* useCapture = */ false); |
michael@0 | 254 | els.addSystemEventListener(global, 'DOMWindowCreated', |
michael@0 | 255 | this._windowCreatedHandler.bind(this), |
michael@0 | 256 | /* useCapture = */ true); |
michael@0 | 257 | els.addSystemEventListener(global, 'DOMWindowResize', |
michael@0 | 258 | this._windowResizeHandler.bind(this), |
michael@0 | 259 | /* useCapture = */ false); |
michael@0 | 260 | els.addSystemEventListener(global, 'contextmenu', |
michael@0 | 261 | this._contextmenuHandler.bind(this), |
michael@0 | 262 | /* useCapture = */ false); |
michael@0 | 263 | els.addSystemEventListener(global, 'scroll', |
michael@0 | 264 | this._scrollEventHandler.bind(this), |
michael@0 | 265 | /* useCapture = */ false); |
michael@0 | 266 | |
michael@0 | 267 | OBSERVED_EVENTS.forEach((aTopic) => { |
michael@0 | 268 | Services.obs.addObserver(this, aTopic, false); |
michael@0 | 269 | }); |
michael@0 | 270 | }, |
michael@0 | 271 | |
michael@0 | 272 | observe: function(subject, topic, data) { |
michael@0 | 273 | // Ignore notifications not about our document. (Note that |content| /can/ |
michael@0 | 274 | // be null; see bug 874900.) |
michael@0 | 275 | if (topic !== 'activity-done' && (!content || subject != content.document)) |
michael@0 | 276 | return; |
michael@0 | 277 | if (topic == 'activity-done' && docShell !== subject) |
michael@0 | 278 | return; |
michael@0 | 279 | switch (topic) { |
michael@0 | 280 | case 'fullscreen-origin-change': |
michael@0 | 281 | sendAsyncMsg('fullscreen-origin-change', { _payload_: data }); |
michael@0 | 282 | break; |
michael@0 | 283 | case 'ask-parent-to-exit-fullscreen': |
michael@0 | 284 | sendAsyncMsg('exit-fullscreen'); |
michael@0 | 285 | break; |
michael@0 | 286 | case 'ask-parent-to-rollback-fullscreen': |
michael@0 | 287 | sendAsyncMsg('rollback-fullscreen'); |
michael@0 | 288 | break; |
michael@0 | 289 | case 'activity-done': |
michael@0 | 290 | sendAsyncMsg('activitydone', { success: (data == 'activity-success') }); |
michael@0 | 291 | break; |
michael@0 | 292 | case 'xpcom-shutdown': |
michael@0 | 293 | this._shuttingDown = true; |
michael@0 | 294 | break; |
michael@0 | 295 | } |
michael@0 | 296 | }, |
michael@0 | 297 | |
michael@0 | 298 | /** |
michael@0 | 299 | * Called when our TabChildGlobal starts to die. This is not called when the |
michael@0 | 300 | * page inside |content| unloads. |
michael@0 | 301 | */ |
michael@0 | 302 | _unloadHandler: function() { |
michael@0 | 303 | this._shuttingDown = true; |
michael@0 | 304 | OBSERVED_EVENTS.forEach((aTopic) => { |
michael@0 | 305 | Services.obs.removeObserver(this, aTopic); |
michael@0 | 306 | }); |
michael@0 | 307 | }, |
michael@0 | 308 | |
michael@0 | 309 | _tryGetInnerWindowID: function(win) { |
michael@0 | 310 | let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 311 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 312 | try { |
michael@0 | 313 | return utils.currentInnerWindowID; |
michael@0 | 314 | } |
michael@0 | 315 | catch(e) { |
michael@0 | 316 | return null; |
michael@0 | 317 | } |
michael@0 | 318 | }, |
michael@0 | 319 | |
michael@0 | 320 | /** |
michael@0 | 321 | * Show a modal prompt. Called by BrowserElementPromptService. |
michael@0 | 322 | */ |
michael@0 | 323 | showModalPrompt: function(win, args) { |
michael@0 | 324 | let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 325 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 326 | |
michael@0 | 327 | args.windowID = { outer: utils.outerWindowID, |
michael@0 | 328 | inner: this._tryGetInnerWindowID(win) }; |
michael@0 | 329 | sendAsyncMsg('showmodalprompt', args); |
michael@0 | 330 | |
michael@0 | 331 | let returnValue = this._waitForResult(win); |
michael@0 | 332 | |
michael@0 | 333 | Services.obs.notifyObservers(null, 'BEC:ShownModalPrompt', null); |
michael@0 | 334 | |
michael@0 | 335 | if (args.promptType == 'prompt' || |
michael@0 | 336 | args.promptType == 'confirm' || |
michael@0 | 337 | args.promptType == 'custom-prompt') { |
michael@0 | 338 | return returnValue; |
michael@0 | 339 | } |
michael@0 | 340 | }, |
michael@0 | 341 | |
michael@0 | 342 | /** |
michael@0 | 343 | * Spin in a nested event loop until we receive a unblock-modal-prompt message for |
michael@0 | 344 | * this window. |
michael@0 | 345 | */ |
michael@0 | 346 | _waitForResult: function(win) { |
michael@0 | 347 | debug("_waitForResult(" + win + ")"); |
michael@0 | 348 | let utils = win.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 349 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 350 | |
michael@0 | 351 | let outerWindowID = utils.outerWindowID; |
michael@0 | 352 | let innerWindowID = this._tryGetInnerWindowID(win); |
michael@0 | 353 | if (innerWindowID === null) { |
michael@0 | 354 | // I have no idea what waiting for a result means when there's no inner |
michael@0 | 355 | // window, so let's just bail. |
michael@0 | 356 | debug("_waitForResult: No inner window. Bailing."); |
michael@0 | 357 | return; |
michael@0 | 358 | } |
michael@0 | 359 | |
michael@0 | 360 | this._windowIDDict[outerWindowID] = Cu.getWeakReference(win); |
michael@0 | 361 | |
michael@0 | 362 | debug("Entering modal state (outerWindowID=" + outerWindowID + ", " + |
michael@0 | 363 | "innerWindowID=" + innerWindowID + ")"); |
michael@0 | 364 | |
michael@0 | 365 | utils.enterModalState(); |
michael@0 | 366 | |
michael@0 | 367 | // We'll decrement win.modalDepth when we receive a unblock-modal-prompt message |
michael@0 | 368 | // for the window. |
michael@0 | 369 | if (!win.modalDepth) { |
michael@0 | 370 | win.modalDepth = 0; |
michael@0 | 371 | } |
michael@0 | 372 | win.modalDepth++; |
michael@0 | 373 | let origModalDepth = win.modalDepth; |
michael@0 | 374 | |
michael@0 | 375 | let thread = Services.tm.currentThread; |
michael@0 | 376 | debug("Nested event loop - begin"); |
michael@0 | 377 | while (win.modalDepth == origModalDepth && !this._shuttingDown) { |
michael@0 | 378 | // Bail out of the loop if the inner window changed; that means the |
michael@0 | 379 | // window navigated. Bail out when we're shutting down because otherwise |
michael@0 | 380 | // we'll leak our window. |
michael@0 | 381 | if (this._tryGetInnerWindowID(win) !== innerWindowID) { |
michael@0 | 382 | debug("_waitForResult: Inner window ID changed " + |
michael@0 | 383 | "while in nested event loop."); |
michael@0 | 384 | break; |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | thread.processNextEvent(/* mayWait = */ true); |
michael@0 | 388 | } |
michael@0 | 389 | debug("Nested event loop - finish"); |
michael@0 | 390 | |
michael@0 | 391 | // If we exited the loop because the inner window changed, then bail on the |
michael@0 | 392 | // modal prompt. |
michael@0 | 393 | if (innerWindowID !== this._tryGetInnerWindowID(win)) { |
michael@0 | 394 | throw Components.Exception("Modal state aborted by navigation", |
michael@0 | 395 | Cr.NS_ERROR_NOT_AVAILABLE); |
michael@0 | 396 | } |
michael@0 | 397 | |
michael@0 | 398 | let returnValue = win.modalReturnValue; |
michael@0 | 399 | delete win.modalReturnValue; |
michael@0 | 400 | |
michael@0 | 401 | if (!this._shuttingDown) { |
michael@0 | 402 | utils.leaveModalState(); |
michael@0 | 403 | } |
michael@0 | 404 | |
michael@0 | 405 | debug("Leaving modal state (outerID=" + outerWindowID + ", " + |
michael@0 | 406 | "innerID=" + innerWindowID + ")"); |
michael@0 | 407 | return returnValue; |
michael@0 | 408 | }, |
michael@0 | 409 | |
michael@0 | 410 | _recvStopWaiting: function(msg) { |
michael@0 | 411 | let outerID = msg.json.windowID.outer; |
michael@0 | 412 | let innerID = msg.json.windowID.inner; |
michael@0 | 413 | let returnValue = msg.json.returnValue; |
michael@0 | 414 | debug("recvStopWaiting(outer=" + outerID + ", inner=" + innerID + |
michael@0 | 415 | ", returnValue=" + returnValue + ")"); |
michael@0 | 416 | |
michael@0 | 417 | if (!this._windowIDDict[outerID]) { |
michael@0 | 418 | debug("recvStopWaiting: No record of outer window ID " + outerID); |
michael@0 | 419 | return; |
michael@0 | 420 | } |
michael@0 | 421 | |
michael@0 | 422 | let win = this._windowIDDict[outerID].get(); |
michael@0 | 423 | delete this._windowIDDict[outerID]; |
michael@0 | 424 | |
michael@0 | 425 | if (!win) { |
michael@0 | 426 | debug("recvStopWaiting, but window is gone\n"); |
michael@0 | 427 | return; |
michael@0 | 428 | } |
michael@0 | 429 | |
michael@0 | 430 | if (innerID !== this._tryGetInnerWindowID(win)) { |
michael@0 | 431 | debug("recvStopWaiting, but inner ID has changed\n"); |
michael@0 | 432 | return; |
michael@0 | 433 | } |
michael@0 | 434 | |
michael@0 | 435 | debug("recvStopWaiting " + win); |
michael@0 | 436 | win.modalReturnValue = returnValue; |
michael@0 | 437 | win.modalDepth--; |
michael@0 | 438 | }, |
michael@0 | 439 | |
michael@0 | 440 | _recvExitFullscreen: function() { |
michael@0 | 441 | var utils = content.document.defaultView |
michael@0 | 442 | .QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 443 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 444 | utils.exitFullscreen(); |
michael@0 | 445 | }, |
michael@0 | 446 | |
michael@0 | 447 | _titleChangedHandler: function(e) { |
michael@0 | 448 | debug("Got titlechanged: (" + e.target.title + ")"); |
michael@0 | 449 | var win = e.target.defaultView; |
michael@0 | 450 | |
michael@0 | 451 | // Ignore titlechanges which don't come from the top-level |
michael@0 | 452 | // <iframe mozbrowser> window. |
michael@0 | 453 | if (win == content) { |
michael@0 | 454 | sendAsyncMsg('titlechange', { _payload_: e.target.title }); |
michael@0 | 455 | } |
michael@0 | 456 | else { |
michael@0 | 457 | debug("Not top level!"); |
michael@0 | 458 | } |
michael@0 | 459 | }, |
michael@0 | 460 | |
michael@0 | 461 | _iconChangedHandler: function(e) { |
michael@0 | 462 | debug('Got iconchanged: (' + e.target.href + ')'); |
michael@0 | 463 | let icon = { href: e.target.href }; |
michael@0 | 464 | if (e.target.getAttribute('sizes')) { |
michael@0 | 465 | icon.sizes = e.target.getAttribute('sizes'); |
michael@0 | 466 | } |
michael@0 | 467 | |
michael@0 | 468 | sendAsyncMsg('iconchange', icon); |
michael@0 | 469 | }, |
michael@0 | 470 | |
michael@0 | 471 | _openSearchHandler: function(e) { |
michael@0 | 472 | debug('Got opensearch: (' + e.target.href + ')'); |
michael@0 | 473 | |
michael@0 | 474 | if (e.target.type !== "application/opensearchdescription+xml") { |
michael@0 | 475 | return; |
michael@0 | 476 | } |
michael@0 | 477 | |
michael@0 | 478 | sendAsyncMsg('opensearch', { title: e.target.title, |
michael@0 | 479 | href: e.target.href }); |
michael@0 | 480 | |
michael@0 | 481 | }, |
michael@0 | 482 | |
michael@0 | 483 | _manifestChangedHandler: function(e) { |
michael@0 | 484 | debug('Got manifestchanged: (' + e.target.href + ')'); |
michael@0 | 485 | let manifest = { href: e.target.href }; |
michael@0 | 486 | sendAsyncMsg('manifestchange', manifest); |
michael@0 | 487 | |
michael@0 | 488 | }, |
michael@0 | 489 | |
michael@0 | 490 | // Processes the "rel" field in <link> tags and forward to specific handlers. |
michael@0 | 491 | _linkAddedHandler: function(e) { |
michael@0 | 492 | let win = e.target.ownerDocument.defaultView; |
michael@0 | 493 | // Ignore links which don't come from the top-level |
michael@0 | 494 | // <iframe mozbrowser> window. |
michael@0 | 495 | if (win != content) { |
michael@0 | 496 | debug('Not top level!'); |
michael@0 | 497 | return; |
michael@0 | 498 | } |
michael@0 | 499 | |
michael@0 | 500 | let handlers = { |
michael@0 | 501 | 'icon': this._iconChangedHandler, |
michael@0 | 502 | 'search': this._openSearchHandler, |
michael@0 | 503 | 'manifest': this._manifestChangedHandler |
michael@0 | 504 | }; |
michael@0 | 505 | |
michael@0 | 506 | debug('Got linkAdded: (' + e.target.href + ') ' + e.target.rel); |
michael@0 | 507 | e.target.rel.split(' ').forEach(function(x) { |
michael@0 | 508 | let token = x.toLowerCase(); |
michael@0 | 509 | if (handlers[token]) { |
michael@0 | 510 | handlers[token](e); |
michael@0 | 511 | } |
michael@0 | 512 | }, this); |
michael@0 | 513 | }, |
michael@0 | 514 | |
michael@0 | 515 | _metaAddedHandler: function(e) { |
michael@0 | 516 | let win = e.target.ownerDocument.defaultView; |
michael@0 | 517 | // Ignore metas which don't come from the top-level |
michael@0 | 518 | // <iframe mozbrowser> window. |
michael@0 | 519 | if (win != content) { |
michael@0 | 520 | debug('Not top level!'); |
michael@0 | 521 | return; |
michael@0 | 522 | } |
michael@0 | 523 | |
michael@0 | 524 | if (!e.target.name) { |
michael@0 | 525 | return; |
michael@0 | 526 | } |
michael@0 | 527 | |
michael@0 | 528 | debug('Got metaAdded: (' + e.target.name + ') ' + e.target.content); |
michael@0 | 529 | if (e.target.name == 'application-name') { |
michael@0 | 530 | let meta = { name: e.target.name, |
michael@0 | 531 | content: e.target.content }; |
michael@0 | 532 | |
michael@0 | 533 | let lang; |
michael@0 | 534 | let elm; |
michael@0 | 535 | |
michael@0 | 536 | for (elm = e.target; |
michael@0 | 537 | !lang && elm && elm.nodeType == e.target.ELEMENT_NODE; |
michael@0 | 538 | elm = elm.parentNode) { |
michael@0 | 539 | if (elm.hasAttribute('lang')) { |
michael@0 | 540 | lang = elm.getAttribute('lang'); |
michael@0 | 541 | continue; |
michael@0 | 542 | } |
michael@0 | 543 | |
michael@0 | 544 | if (elm.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')) { |
michael@0 | 545 | lang = elm.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang'); |
michael@0 | 546 | continue; |
michael@0 | 547 | } |
michael@0 | 548 | } |
michael@0 | 549 | |
michael@0 | 550 | // No lang has been detected. |
michael@0 | 551 | if (!lang && elm.nodeType == e.target.DOCUMENT_NODE) { |
michael@0 | 552 | lang = elm.contentLanguage; |
michael@0 | 553 | } |
michael@0 | 554 | |
michael@0 | 555 | if (lang) { |
michael@0 | 556 | meta.lang = lang; |
michael@0 | 557 | } |
michael@0 | 558 | |
michael@0 | 559 | sendAsyncMsg('metachange', meta); |
michael@0 | 560 | } |
michael@0 | 561 | }, |
michael@0 | 562 | |
michael@0 | 563 | _addMozAfterPaintHandler: function(callback) { |
michael@0 | 564 | function onMozAfterPaint() { |
michael@0 | 565 | let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI; |
michael@0 | 566 | if (uri.spec != "about:blank") { |
michael@0 | 567 | debug("Got afterpaint event: " + uri.spec); |
michael@0 | 568 | removeEventListener('MozAfterPaint', onMozAfterPaint, |
michael@0 | 569 | /* useCapture = */ true); |
michael@0 | 570 | callback(); |
michael@0 | 571 | } |
michael@0 | 572 | } |
michael@0 | 573 | |
michael@0 | 574 | addEventListener('MozAfterPaint', onMozAfterPaint, /* useCapture = */ true); |
michael@0 | 575 | return onMozAfterPaint; |
michael@0 | 576 | }, |
michael@0 | 577 | |
michael@0 | 578 | _removeMozAfterPaintHandler: function(listener) { |
michael@0 | 579 | removeEventListener('MozAfterPaint', listener, |
michael@0 | 580 | /* useCapture = */ true); |
michael@0 | 581 | }, |
michael@0 | 582 | |
michael@0 | 583 | _activateNextPaintListener: function(e) { |
michael@0 | 584 | if (!this._nextPaintHandler) { |
michael@0 | 585 | this._nextPaintHandler = this._addMozAfterPaintHandler(function () { |
michael@0 | 586 | this._nextPaintHandler = null; |
michael@0 | 587 | sendAsyncMsg('nextpaint'); |
michael@0 | 588 | }.bind(this)); |
michael@0 | 589 | } |
michael@0 | 590 | }, |
michael@0 | 591 | |
michael@0 | 592 | _deactivateNextPaintListener: function(e) { |
michael@0 | 593 | if (this._nextPaintHandler) { |
michael@0 | 594 | this._removeMozAfterPaintHandler(this._nextPaintHandler); |
michael@0 | 595 | this._nextPaintHandler = null; |
michael@0 | 596 | } |
michael@0 | 597 | }, |
michael@0 | 598 | |
michael@0 | 599 | _windowCloseHandler: function(e) { |
michael@0 | 600 | let win = e.target; |
michael@0 | 601 | if (win != content || e.defaultPrevented) { |
michael@0 | 602 | return; |
michael@0 | 603 | } |
michael@0 | 604 | |
michael@0 | 605 | debug("Closing window " + win); |
michael@0 | 606 | sendAsyncMsg('close'); |
michael@0 | 607 | |
michael@0 | 608 | // Inform the window implementation that we handled this close ourselves. |
michael@0 | 609 | e.preventDefault(); |
michael@0 | 610 | }, |
michael@0 | 611 | |
michael@0 | 612 | _windowCreatedHandler: function(e) { |
michael@0 | 613 | let targetDocShell = e.target.defaultView |
michael@0 | 614 | .QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 615 | .getInterface(Ci.nsIWebNavigation); |
michael@0 | 616 | if (targetDocShell != docShell) { |
michael@0 | 617 | return; |
michael@0 | 618 | } |
michael@0 | 619 | |
michael@0 | 620 | let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI; |
michael@0 | 621 | debug("Window created: " + uri.spec); |
michael@0 | 622 | if (uri.spec != "about:blank") { |
michael@0 | 623 | this._addMozAfterPaintHandler(function () { |
michael@0 | 624 | sendAsyncMsg('documentfirstpaint'); |
michael@0 | 625 | }); |
michael@0 | 626 | this._isContentWindowCreated = true; |
michael@0 | 627 | // Handle pending SetInputMethodActive request. |
michael@0 | 628 | while (this._pendingSetInputMethodActive.length > 0) { |
michael@0 | 629 | this._recvSetInputMethodActive(this._pendingSetInputMethodActive.shift()); |
michael@0 | 630 | } |
michael@0 | 631 | } |
michael@0 | 632 | }, |
michael@0 | 633 | |
michael@0 | 634 | _windowResizeHandler: function(e) { |
michael@0 | 635 | let win = e.target; |
michael@0 | 636 | if (win != content || e.defaultPrevented) { |
michael@0 | 637 | return; |
michael@0 | 638 | } |
michael@0 | 639 | |
michael@0 | 640 | debug("resizing window " + win); |
michael@0 | 641 | sendAsyncMsg('resize', { width: e.detail.width, height: e.detail.height }); |
michael@0 | 642 | |
michael@0 | 643 | // Inform the window implementation that we handled this resize ourselves. |
michael@0 | 644 | e.preventDefault(); |
michael@0 | 645 | }, |
michael@0 | 646 | |
michael@0 | 647 | _contextmenuHandler: function(e) { |
michael@0 | 648 | debug("Got contextmenu"); |
michael@0 | 649 | |
michael@0 | 650 | if (e.defaultPrevented) { |
michael@0 | 651 | return; |
michael@0 | 652 | } |
michael@0 | 653 | |
michael@0 | 654 | this._ctxCounter++; |
michael@0 | 655 | this._ctxHandlers = {}; |
michael@0 | 656 | |
michael@0 | 657 | var elem = e.target; |
michael@0 | 658 | var menuData = {systemTargets: [], contextmenu: null}; |
michael@0 | 659 | var ctxMenuId = null; |
michael@0 | 660 | |
michael@0 | 661 | while (elem && elem.parentNode) { |
michael@0 | 662 | var ctxData = this._getSystemCtxMenuData(elem); |
michael@0 | 663 | if (ctxData) { |
michael@0 | 664 | menuData.systemTargets.push({ |
michael@0 | 665 | nodeName: elem.nodeName, |
michael@0 | 666 | data: ctxData |
michael@0 | 667 | }); |
michael@0 | 668 | } |
michael@0 | 669 | |
michael@0 | 670 | if (!ctxMenuId && 'hasAttribute' in elem && elem.hasAttribute('contextmenu')) { |
michael@0 | 671 | ctxMenuId = elem.getAttribute('contextmenu'); |
michael@0 | 672 | } |
michael@0 | 673 | elem = elem.parentNode; |
michael@0 | 674 | } |
michael@0 | 675 | |
michael@0 | 676 | if (ctxMenuId) { |
michael@0 | 677 | var menu = e.target.ownerDocument.getElementById(ctxMenuId); |
michael@0 | 678 | if (menu) { |
michael@0 | 679 | menuData.contextmenu = this._buildMenuObj(menu, ''); |
michael@0 | 680 | } |
michael@0 | 681 | } |
michael@0 | 682 | |
michael@0 | 683 | // The value returned by the contextmenu sync call is true iff the embedder |
michael@0 | 684 | // called preventDefault() on its contextmenu event. |
michael@0 | 685 | // |
michael@0 | 686 | // We call preventDefault() on our contextmenu event iff the embedder called |
michael@0 | 687 | // preventDefault() on /its/ contextmenu event. This way, if the embedder |
michael@0 | 688 | // ignored the contextmenu event, TabChild will fire a click. |
michael@0 | 689 | if (sendSyncMsg('contextmenu', menuData)[0]) { |
michael@0 | 690 | e.preventDefault(); |
michael@0 | 691 | } else { |
michael@0 | 692 | this._ctxHandlers = {}; |
michael@0 | 693 | } |
michael@0 | 694 | }, |
michael@0 | 695 | |
michael@0 | 696 | _getSystemCtxMenuData: function(elem) { |
michael@0 | 697 | if ((elem instanceof Ci.nsIDOMHTMLAnchorElement && elem.href) || |
michael@0 | 698 | (elem instanceof Ci.nsIDOMHTMLAreaElement && elem.href)) { |
michael@0 | 699 | return {uri: elem.href}; |
michael@0 | 700 | } |
michael@0 | 701 | if (elem instanceof Ci.nsIImageLoadingContent && elem.currentURI) { |
michael@0 | 702 | return {uri: elem.currentURI.spec}; |
michael@0 | 703 | } |
michael@0 | 704 | if (elem instanceof Ci.nsIDOMHTMLImageElement) { |
michael@0 | 705 | return {uri: elem.src}; |
michael@0 | 706 | } |
michael@0 | 707 | if (elem instanceof Ci.nsIDOMHTMLMediaElement) { |
michael@0 | 708 | let hasVideo = !(elem.readyState >= elem.HAVE_METADATA && |
michael@0 | 709 | (elem.videoWidth == 0 || elem.videoHeight == 0)); |
michael@0 | 710 | return {uri: elem.currentSrc || elem.src, hasVideo: hasVideo}; |
michael@0 | 711 | } |
michael@0 | 712 | return false; |
michael@0 | 713 | }, |
michael@0 | 714 | |
michael@0 | 715 | _scrollEventHandler: function(e) { |
michael@0 | 716 | let win = e.target.defaultView; |
michael@0 | 717 | if (win != content) { |
michael@0 | 718 | return; |
michael@0 | 719 | } |
michael@0 | 720 | |
michael@0 | 721 | debug("scroll event " + win); |
michael@0 | 722 | sendAsyncMsg("scroll", { top: win.scrollY, left: win.scrollX }); |
michael@0 | 723 | }, |
michael@0 | 724 | |
michael@0 | 725 | _recvPurgeHistory: function(data) { |
michael@0 | 726 | debug("Received purgeHistory message: (" + data.json.id + ")"); |
michael@0 | 727 | |
michael@0 | 728 | let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory; |
michael@0 | 729 | |
michael@0 | 730 | try { |
michael@0 | 731 | if (history && history.count) { |
michael@0 | 732 | history.PurgeHistory(history.count); |
michael@0 | 733 | } |
michael@0 | 734 | } catch(e) {} |
michael@0 | 735 | |
michael@0 | 736 | sendAsyncMsg('got-purge-history', { id: data.json.id, successRv: true }); |
michael@0 | 737 | }, |
michael@0 | 738 | |
michael@0 | 739 | _recvGetScreenshot: function(data) { |
michael@0 | 740 | debug("Received getScreenshot message: (" + data.json.id + ")"); |
michael@0 | 741 | |
michael@0 | 742 | let self = this; |
michael@0 | 743 | let maxWidth = data.json.args.width; |
michael@0 | 744 | let maxHeight = data.json.args.height; |
michael@0 | 745 | let mimeType = data.json.args.mimeType; |
michael@0 | 746 | let domRequestID = data.json.id; |
michael@0 | 747 | |
michael@0 | 748 | let takeScreenshotClosure = function() { |
michael@0 | 749 | self._takeScreenshot(maxWidth, maxHeight, mimeType, domRequestID); |
michael@0 | 750 | }; |
michael@0 | 751 | |
michael@0 | 752 | let maxDelayMS = 2000; |
michael@0 | 753 | try { |
michael@0 | 754 | maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS'); |
michael@0 | 755 | } |
michael@0 | 756 | catch(e) {} |
michael@0 | 757 | |
michael@0 | 758 | // Try to wait for the event loop to go idle before we take the screenshot, |
michael@0 | 759 | // but once we've waited maxDelayMS milliseconds, go ahead and take it |
michael@0 | 760 | // anyway. |
michael@0 | 761 | Cc['@mozilla.org/message-loop;1'].getService(Ci.nsIMessageLoop).postIdleTask( |
michael@0 | 762 | takeScreenshotClosure, maxDelayMS); |
michael@0 | 763 | }, |
michael@0 | 764 | |
michael@0 | 765 | /** |
michael@0 | 766 | * Actually take a screenshot and foward the result up to our parent, given |
michael@0 | 767 | * the desired maxWidth and maxHeight (in CSS pixels), and given the |
michael@0 | 768 | * DOMRequest ID associated with the request from the parent. |
michael@0 | 769 | */ |
michael@0 | 770 | _takeScreenshot: function(maxWidth, maxHeight, mimeType, domRequestID) { |
michael@0 | 771 | // You can think of the screenshotting algorithm as carrying out the |
michael@0 | 772 | // following steps: |
michael@0 | 773 | // |
michael@0 | 774 | // - Calculate maxWidth, maxHeight, and viewport's width and height in the |
michael@0 | 775 | // dimension of device pixels by multiply the numbers with |
michael@0 | 776 | // window.devicePixelRatio. |
michael@0 | 777 | // |
michael@0 | 778 | // - Let scaleWidth be the factor by which we'd need to downscale the |
michael@0 | 779 | // viewport pixel width so it would fit within maxPixelWidth. |
michael@0 | 780 | // (If the viewport's pixel width is less than maxPixelWidth, let |
michael@0 | 781 | // scaleWidth be 1.) Compute scaleHeight the same way. |
michael@0 | 782 | // |
michael@0 | 783 | // - Scale the viewport by max(scaleWidth, scaleHeight). Now either the |
michael@0 | 784 | // viewport's width is no larger than maxWidth, the viewport's height is |
michael@0 | 785 | // no larger than maxHeight, or both. |
michael@0 | 786 | // |
michael@0 | 787 | // - Crop the viewport so its width is no larger than maxWidth and its |
michael@0 | 788 | // height is no larger than maxHeight. |
michael@0 | 789 | // |
michael@0 | 790 | // - Set mozOpaque to true and background color to solid white |
michael@0 | 791 | // if we are taking a JPEG screenshot, keep transparent if otherwise. |
michael@0 | 792 | // |
michael@0 | 793 | // - Return a screenshot of the page's viewport scaled and cropped per |
michael@0 | 794 | // above. |
michael@0 | 795 | debug("Taking a screenshot: maxWidth=" + maxWidth + |
michael@0 | 796 | ", maxHeight=" + maxHeight + |
michael@0 | 797 | ", mimeType=" + mimeType + |
michael@0 | 798 | ", domRequestID=" + domRequestID + "."); |
michael@0 | 799 | |
michael@0 | 800 | if (!content) { |
michael@0 | 801 | // If content is not loaded yet, bail out since even sendAsyncMessage |
michael@0 | 802 | // fails... |
michael@0 | 803 | debug("No content yet!"); |
michael@0 | 804 | return; |
michael@0 | 805 | } |
michael@0 | 806 | |
michael@0 | 807 | let devicePixelRatio = content.devicePixelRatio; |
michael@0 | 808 | |
michael@0 | 809 | let maxPixelWidth = Math.round(maxWidth * devicePixelRatio); |
michael@0 | 810 | let maxPixelHeight = Math.round(maxHeight * devicePixelRatio); |
michael@0 | 811 | |
michael@0 | 812 | let contentPixelWidth = content.innerWidth * devicePixelRatio; |
michael@0 | 813 | let contentPixelHeight = content.innerHeight * devicePixelRatio; |
michael@0 | 814 | |
michael@0 | 815 | let scaleWidth = Math.min(1, maxPixelWidth / contentPixelWidth); |
michael@0 | 816 | let scaleHeight = Math.min(1, maxPixelHeight / contentPixelHeight); |
michael@0 | 817 | |
michael@0 | 818 | let scale = Math.max(scaleWidth, scaleHeight); |
michael@0 | 819 | |
michael@0 | 820 | let canvasWidth = |
michael@0 | 821 | Math.min(maxPixelWidth, Math.round(contentPixelWidth * scale)); |
michael@0 | 822 | let canvasHeight = |
michael@0 | 823 | Math.min(maxPixelHeight, Math.round(contentPixelHeight * scale)); |
michael@0 | 824 | |
michael@0 | 825 | let transparent = (mimeType !== 'image/jpeg'); |
michael@0 | 826 | |
michael@0 | 827 | var canvas = content.document |
michael@0 | 828 | .createElementNS("http://www.w3.org/1999/xhtml", "canvas"); |
michael@0 | 829 | if (!transparent) |
michael@0 | 830 | canvas.mozOpaque = true; |
michael@0 | 831 | canvas.width = canvasWidth; |
michael@0 | 832 | canvas.height = canvasHeight; |
michael@0 | 833 | |
michael@0 | 834 | let ctx = canvas.getContext("2d", { willReadFrequently: true }); |
michael@0 | 835 | ctx.scale(scale * devicePixelRatio, scale * devicePixelRatio); |
michael@0 | 836 | |
michael@0 | 837 | let flags = ctx.DRAWWINDOW_DRAW_VIEW | |
michael@0 | 838 | ctx.DRAWWINDOW_USE_WIDGET_LAYERS | |
michael@0 | 839 | ctx.DRAWWINDOW_DO_NOT_FLUSH | |
michael@0 | 840 | ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES; |
michael@0 | 841 | ctx.drawWindow(content, 0, 0, content.innerWidth, content.innerHeight, |
michael@0 | 842 | transparent ? "rgba(255,255,255,0)" : "rgb(255,255,255)", |
michael@0 | 843 | flags); |
michael@0 | 844 | |
michael@0 | 845 | // Take a JPEG screenshot by default instead of PNG with alpha channel. |
michael@0 | 846 | // This requires us to unpremultiply the alpha channel, which |
michael@0 | 847 | // is expensive on ARM processors because they lack a hardware integer |
michael@0 | 848 | // division instruction. |
michael@0 | 849 | canvas.toBlob(function(blob) { |
michael@0 | 850 | sendAsyncMsg('got-screenshot', { |
michael@0 | 851 | id: domRequestID, |
michael@0 | 852 | successRv: blob |
michael@0 | 853 | }); |
michael@0 | 854 | }, mimeType); |
michael@0 | 855 | }, |
michael@0 | 856 | |
michael@0 | 857 | _recvFireCtxCallback: function(data) { |
michael@0 | 858 | debug("Received fireCtxCallback message: (" + data.json.menuitem + ")"); |
michael@0 | 859 | // We silently ignore if the embedder uses an incorrect id in the callback |
michael@0 | 860 | if (data.json.menuitem in this._ctxHandlers) { |
michael@0 | 861 | this._ctxHandlers[data.json.menuitem].click(); |
michael@0 | 862 | this._ctxHandlers = {}; |
michael@0 | 863 | } else { |
michael@0 | 864 | debug("Ignored invalid contextmenu invocation"); |
michael@0 | 865 | } |
michael@0 | 866 | }, |
michael@0 | 867 | |
michael@0 | 868 | _buildMenuObj: function(menu, idPrefix) { |
michael@0 | 869 | function maybeCopyAttribute(src, target, attribute) { |
michael@0 | 870 | if (src.getAttribute(attribute)) { |
michael@0 | 871 | target[attribute] = src.getAttribute(attribute); |
michael@0 | 872 | } |
michael@0 | 873 | } |
michael@0 | 874 | |
michael@0 | 875 | var menuObj = {type: 'menu', items: []}; |
michael@0 | 876 | maybeCopyAttribute(menu, menuObj, 'label'); |
michael@0 | 877 | |
michael@0 | 878 | for (var i = 0, child; child = menu.children[i++];) { |
michael@0 | 879 | if (child.nodeName === 'MENU') { |
michael@0 | 880 | menuObj.items.push(this._buildMenuObj(child, idPrefix + i + '_')); |
michael@0 | 881 | } else if (child.nodeName === 'MENUITEM') { |
michael@0 | 882 | var id = this._ctxCounter + '_' + idPrefix + i; |
michael@0 | 883 | var menuitem = {id: id, type: 'menuitem'}; |
michael@0 | 884 | maybeCopyAttribute(child, menuitem, 'label'); |
michael@0 | 885 | maybeCopyAttribute(child, menuitem, 'icon'); |
michael@0 | 886 | this._ctxHandlers[id] = child; |
michael@0 | 887 | menuObj.items.push(menuitem); |
michael@0 | 888 | } |
michael@0 | 889 | } |
michael@0 | 890 | return menuObj; |
michael@0 | 891 | }, |
michael@0 | 892 | |
michael@0 | 893 | _recvSetVisible: function(data) { |
michael@0 | 894 | debug("Received setVisible message: (" + data.json.visible + ")"); |
michael@0 | 895 | if (this._forcedVisible == data.json.visible) { |
michael@0 | 896 | return; |
michael@0 | 897 | } |
michael@0 | 898 | |
michael@0 | 899 | this._forcedVisible = data.json.visible; |
michael@0 | 900 | this._updateVisibility(); |
michael@0 | 901 | }, |
michael@0 | 902 | |
michael@0 | 903 | _recvVisible: function(data) { |
michael@0 | 904 | sendAsyncMsg('got-visible', { |
michael@0 | 905 | id: data.json.id, |
michael@0 | 906 | successRv: docShell.isActive |
michael@0 | 907 | }); |
michael@0 | 908 | }, |
michael@0 | 909 | |
michael@0 | 910 | /** |
michael@0 | 911 | * Called when the window which contains this iframe becomes hidden or |
michael@0 | 912 | * visible. |
michael@0 | 913 | */ |
michael@0 | 914 | _recvOwnerVisibilityChange: function(data) { |
michael@0 | 915 | debug("Received ownerVisibilityChange: (" + data.json.visible + ")"); |
michael@0 | 916 | this._ownerVisible = data.json.visible; |
michael@0 | 917 | this._updateVisibility(); |
michael@0 | 918 | }, |
michael@0 | 919 | |
michael@0 | 920 | _updateVisibility: function() { |
michael@0 | 921 | var visible = this._forcedVisible && this._ownerVisible; |
michael@0 | 922 | if (docShell.isActive !== visible) { |
michael@0 | 923 | docShell.isActive = visible; |
michael@0 | 924 | sendAsyncMsg('visibilitychange', {visible: visible}); |
michael@0 | 925 | } |
michael@0 | 926 | }, |
michael@0 | 927 | |
michael@0 | 928 | _recvSendMouseEvent: function(data) { |
michael@0 | 929 | let json = data.json; |
michael@0 | 930 | let utils = content.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 931 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 932 | utils.sendMouseEventToWindow(json.type, json.x, json.y, json.button, |
michael@0 | 933 | json.clickCount, json.modifiers); |
michael@0 | 934 | }, |
michael@0 | 935 | |
michael@0 | 936 | _recvSendTouchEvent: function(data) { |
michael@0 | 937 | let json = data.json; |
michael@0 | 938 | let utils = content.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 939 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 940 | utils.sendTouchEventToWindow(json.type, json.identifiers, json.touchesX, |
michael@0 | 941 | json.touchesY, json.radiisX, json.radiisY, |
michael@0 | 942 | json.rotationAngles, json.forces, json.count, |
michael@0 | 943 | json.modifiers); |
michael@0 | 944 | }, |
michael@0 | 945 | |
michael@0 | 946 | _recvCanGoBack: function(data) { |
michael@0 | 947 | var webNav = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 948 | sendAsyncMsg('got-can-go-back', { |
michael@0 | 949 | id: data.json.id, |
michael@0 | 950 | successRv: webNav.canGoBack |
michael@0 | 951 | }); |
michael@0 | 952 | }, |
michael@0 | 953 | |
michael@0 | 954 | _recvCanGoForward: function(data) { |
michael@0 | 955 | var webNav = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 956 | sendAsyncMsg('got-can-go-forward', { |
michael@0 | 957 | id: data.json.id, |
michael@0 | 958 | successRv: webNav.canGoForward |
michael@0 | 959 | }); |
michael@0 | 960 | }, |
michael@0 | 961 | |
michael@0 | 962 | _recvGoBack: function(data) { |
michael@0 | 963 | try { |
michael@0 | 964 | docShell.QueryInterface(Ci.nsIWebNavigation).goBack(); |
michael@0 | 965 | } catch(e) { |
michael@0 | 966 | // Silently swallow errors; these happen when we can't go back. |
michael@0 | 967 | } |
michael@0 | 968 | }, |
michael@0 | 969 | |
michael@0 | 970 | _recvGoForward: function(data) { |
michael@0 | 971 | try { |
michael@0 | 972 | docShell.QueryInterface(Ci.nsIWebNavigation).goForward(); |
michael@0 | 973 | } catch(e) { |
michael@0 | 974 | // Silently swallow errors; these happen when we can't go forward. |
michael@0 | 975 | } |
michael@0 | 976 | }, |
michael@0 | 977 | |
michael@0 | 978 | _recvReload: function(data) { |
michael@0 | 979 | let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 980 | let reloadFlags = data.json.hardReload ? |
michael@0 | 981 | webNav.LOAD_FLAGS_BYPASS_PROXY | webNav.LOAD_FLAGS_BYPASS_CACHE : |
michael@0 | 982 | webNav.LOAD_FLAGS_NONE; |
michael@0 | 983 | try { |
michael@0 | 984 | webNav.reload(reloadFlags); |
michael@0 | 985 | } catch(e) { |
michael@0 | 986 | // Silently swallow errors; these can happen if a used cancels reload |
michael@0 | 987 | } |
michael@0 | 988 | }, |
michael@0 | 989 | |
michael@0 | 990 | _recvStop: function(data) { |
michael@0 | 991 | let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); |
michael@0 | 992 | webNav.stop(webNav.STOP_NETWORK); |
michael@0 | 993 | }, |
michael@0 | 994 | |
michael@0 | 995 | _recvSetInputMethodActive: function(data) { |
michael@0 | 996 | let msgData = { id: data.json.id }; |
michael@0 | 997 | if (!this._isContentWindowCreated) { |
michael@0 | 998 | if (data.json.args.isActive) { |
michael@0 | 999 | // To activate the input method, we should wait before the content |
michael@0 | 1000 | // window is ready. |
michael@0 | 1001 | this._pendingSetInputMethodActive.push(data); |
michael@0 | 1002 | return; |
michael@0 | 1003 | } |
michael@0 | 1004 | msgData.successRv = null; |
michael@0 | 1005 | sendAsyncMsg('got-set-input-method-active', msgData); |
michael@0 | 1006 | return; |
michael@0 | 1007 | } |
michael@0 | 1008 | // Unwrap to access webpage content. |
michael@0 | 1009 | let nav = XPCNativeWrapper.unwrap(content.document.defaultView.navigator); |
michael@0 | 1010 | if (nav.mozInputMethod) { |
michael@0 | 1011 | // Wrap to access the chrome-only attribute setActive. |
michael@0 | 1012 | new XPCNativeWrapper(nav.mozInputMethod).setActive(data.json.args.isActive); |
michael@0 | 1013 | msgData.successRv = null; |
michael@0 | 1014 | } else { |
michael@0 | 1015 | msgData.errorMsg = 'Cannot access mozInputMethod.'; |
michael@0 | 1016 | } |
michael@0 | 1017 | sendAsyncMsg('got-set-input-method-active', msgData); |
michael@0 | 1018 | }, |
michael@0 | 1019 | |
michael@0 | 1020 | _keyEventHandler: function(e) { |
michael@0 | 1021 | if (whitelistedEvents.indexOf(e.keyCode) != -1 && !e.defaultPrevented) { |
michael@0 | 1022 | sendAsyncMsg('keyevent', { |
michael@0 | 1023 | type: e.type, |
michael@0 | 1024 | keyCode: e.keyCode, |
michael@0 | 1025 | charCode: e.charCode, |
michael@0 | 1026 | }); |
michael@0 | 1027 | } |
michael@0 | 1028 | }, |
michael@0 | 1029 | |
michael@0 | 1030 | // The docShell keeps a weak reference to the progress listener, so we need |
michael@0 | 1031 | // to keep a strong ref to it ourselves. |
michael@0 | 1032 | _progressListener: { |
michael@0 | 1033 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener, |
michael@0 | 1034 | Ci.nsISupportsWeakReference]), |
michael@0 | 1035 | _seenLoadStart: false, |
michael@0 | 1036 | |
michael@0 | 1037 | onLocationChange: function(webProgress, request, location, flags) { |
michael@0 | 1038 | // We get progress events from subshells here, which is kind of weird. |
michael@0 | 1039 | if (webProgress != docShell) { |
michael@0 | 1040 | return; |
michael@0 | 1041 | } |
michael@0 | 1042 | |
michael@0 | 1043 | // Ignore locationchange events which occur before the first loadstart. |
michael@0 | 1044 | // These are usually about:blank loads we don't care about. |
michael@0 | 1045 | if (!this._seenLoadStart) { |
michael@0 | 1046 | return; |
michael@0 | 1047 | } |
michael@0 | 1048 | |
michael@0 | 1049 | // Remove password and wyciwyg from uri. |
michael@0 | 1050 | location = Cc["@mozilla.org/docshell/urifixup;1"] |
michael@0 | 1051 | .getService(Ci.nsIURIFixup).createExposableURI(location); |
michael@0 | 1052 | |
michael@0 | 1053 | sendAsyncMsg('locationchange', { _payload_: location.spec }); |
michael@0 | 1054 | }, |
michael@0 | 1055 | |
michael@0 | 1056 | onStateChange: function(webProgress, request, stateFlags, status) { |
michael@0 | 1057 | if (webProgress != docShell) { |
michael@0 | 1058 | return; |
michael@0 | 1059 | } |
michael@0 | 1060 | |
michael@0 | 1061 | if (stateFlags & Ci.nsIWebProgressListener.STATE_START) { |
michael@0 | 1062 | this._seenLoadStart = true; |
michael@0 | 1063 | sendAsyncMsg('loadstart'); |
michael@0 | 1064 | } |
michael@0 | 1065 | |
michael@0 | 1066 | if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) { |
michael@0 | 1067 | let bgColor = 'transparent'; |
michael@0 | 1068 | try { |
michael@0 | 1069 | bgColor = content.getComputedStyle(content.document.body) |
michael@0 | 1070 | .getPropertyValue('background-color'); |
michael@0 | 1071 | } catch (e) {} |
michael@0 | 1072 | sendAsyncMsg('loadend', {backgroundColor: bgColor}); |
michael@0 | 1073 | |
michael@0 | 1074 | // Ignoring NS_BINDING_ABORTED, which is set when loading page is |
michael@0 | 1075 | // stopped. |
michael@0 | 1076 | if (status == Cr.NS_OK || |
michael@0 | 1077 | status == Cr.NS_BINDING_ABORTED) { |
michael@0 | 1078 | return; |
michael@0 | 1079 | } |
michael@0 | 1080 | |
michael@0 | 1081 | if (NS_ERROR_GET_MODULE(status) == NS_ERROR_MODULE_SECURITY && |
michael@0 | 1082 | getErrorClass(status) == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) { |
michael@0 | 1083 | |
michael@0 | 1084 | // XXX Is there a point firing the event if the error page is not |
michael@0 | 1085 | // certerror? If yes, maybe we should add a property to the |
michael@0 | 1086 | // event to to indicate whether there is a custom page. That would |
michael@0 | 1087 | // let the embedder have more control over the desired behavior. |
michael@0 | 1088 | var errorPage = null; |
michael@0 | 1089 | try { |
michael@0 | 1090 | errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF); |
michael@0 | 1091 | } catch(e) {} |
michael@0 | 1092 | |
michael@0 | 1093 | if (errorPage == 'certerror') { |
michael@0 | 1094 | sendAsyncMsg('error', { type: 'certerror' }); |
michael@0 | 1095 | return; |
michael@0 | 1096 | } |
michael@0 | 1097 | } |
michael@0 | 1098 | |
michael@0 | 1099 | // TODO See nsDocShell::DisplayLoadError for a list of all the error |
michael@0 | 1100 | // codes (the status param) we should eventually handle here. |
michael@0 | 1101 | sendAsyncMsg('error', { type: 'other' }); |
michael@0 | 1102 | } |
michael@0 | 1103 | }, |
michael@0 | 1104 | |
michael@0 | 1105 | onSecurityChange: function(webProgress, request, state) { |
michael@0 | 1106 | if (webProgress != docShell) { |
michael@0 | 1107 | return; |
michael@0 | 1108 | } |
michael@0 | 1109 | |
michael@0 | 1110 | var stateDesc; |
michael@0 | 1111 | if (state & Ci.nsIWebProgressListener.STATE_IS_SECURE) { |
michael@0 | 1112 | stateDesc = 'secure'; |
michael@0 | 1113 | } |
michael@0 | 1114 | else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN) { |
michael@0 | 1115 | stateDesc = 'broken'; |
michael@0 | 1116 | } |
michael@0 | 1117 | else if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE) { |
michael@0 | 1118 | stateDesc = 'insecure'; |
michael@0 | 1119 | } |
michael@0 | 1120 | else { |
michael@0 | 1121 | debug("Unexpected securitychange state!"); |
michael@0 | 1122 | stateDesc = '???'; |
michael@0 | 1123 | } |
michael@0 | 1124 | |
michael@0 | 1125 | // XXX Until bug 764496 is fixed, this will always return false. |
michael@0 | 1126 | var isEV = !!(state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL); |
michael@0 | 1127 | |
michael@0 | 1128 | sendAsyncMsg('securitychange', { state: stateDesc, extendedValidation: isEV }); |
michael@0 | 1129 | }, |
michael@0 | 1130 | |
michael@0 | 1131 | onStatusChange: function(webProgress, request, status, message) {}, |
michael@0 | 1132 | onProgressChange: function(webProgress, request, curSelfProgress, |
michael@0 | 1133 | maxSelfProgress, curTotalProgress, maxTotalProgress) {}, |
michael@0 | 1134 | }, |
michael@0 | 1135 | |
michael@0 | 1136 | // Expose the message manager for WebApps and others. |
michael@0 | 1137 | _messageManagerPublic: { |
michael@0 | 1138 | sendAsyncMessage: global.sendAsyncMessage.bind(global), |
michael@0 | 1139 | sendSyncMessage: global.sendSyncMessage.bind(global), |
michael@0 | 1140 | addMessageListener: global.addMessageListener.bind(global), |
michael@0 | 1141 | removeMessageListener: global.removeMessageListener.bind(global) |
michael@0 | 1142 | }, |
michael@0 | 1143 | |
michael@0 | 1144 | get messageManager() { |
michael@0 | 1145 | return this._messageManagerPublic; |
michael@0 | 1146 | } |
michael@0 | 1147 | }; |
michael@0 | 1148 | |
michael@0 | 1149 | var api = new BrowserElementChild(); |
michael@0 | 1150 |