dom/browser-element/BrowserElementChildPreload.js

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

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

mercurial