Bumping manifests a=b2g-bump
[gecko.git] / dom / browser-element / BrowserElementChildPreload.js
blob55337af36489d25bb49696f7848be05723f825aa
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 let kLongestReturnedString = 128;
18 function debug(msg) {
19   //dump("BrowserElementChildPreload - " + msg + "\n");
22 function sendAsyncMsg(msg, data) {
23   // Ensure that we don't send any messages before BrowserElementChild.js
24   // finishes loading.
25   if (!BrowserElementIsReady)
26     return;
28   if (!data) {
29     data = { };
30   }
32   data.msg_name = msg;
33   sendAsyncMessage('browser-element-api:call', data);
36 function sendSyncMsg(msg, data) {
37   // Ensure that we don't send any messages before BrowserElementChild.js
38   // finishes loading.
39   if (!BrowserElementIsReady)
40     return;
42   if (!data) {
43     data = { };
44   }
46   data.msg_name = msg;
47   return sendSyncMessage('browser-element-api:call', data);
50 let CERTIFICATE_ERROR_PAGE_PREF = 'security.alternate_certificate_error_page';
52 const OBSERVED_EVENTS = [
53   'fullscreen-origin-change',
54   'ask-parent-to-exit-fullscreen',
55   'ask-parent-to-rollback-fullscreen',
56   'xpcom-shutdown',
57   'activity-done'
60 const COMMAND_MAP = {
61   'cut': 'cmd_cut',
62   'copy': 'cmd_copyAndCollapseToEnd',
63   'paste': 'cmd_paste',
64   'selectall': 'cmd_selectAll'
67 /**
68  * The BrowserElementChild implements one half of <iframe mozbrowser>.
69  * (The other half is, unsurprisingly, BrowserElementParent.)
70  *
71  * This script is injected into an <iframe mozbrowser> via
72  * nsIMessageManager::LoadFrameScript().
73  *
74  * Our job here is to listen for events within this frame and bubble them up to
75  * the parent process.
76  */
78 var global = this;
80 function BrowserElementChild() {
81   // Maps outer window id --> weak ref to window.  Used by modal dialog code.
82   this._windowIDDict = {};
84   // _forcedVisible corresponds to the visibility state our owner has set on us
85   // (via iframe.setVisible).  ownerVisible corresponds to whether the docShell
86   // whose window owns this element is visible.
87   //
88   // Our docShell is visible iff _forcedVisible and _ownerVisible are both
89   // true.
90   this._forcedVisible = true;
91   this._ownerVisible = true;
93   this._nextPaintHandler = null;
95   this._isContentWindowCreated = false;
96   this._pendingSetInputMethodActive = [];
97   this._selectionStateChangedTarget = null;
99   this._init();
102 BrowserElementChild.prototype = {
104   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
105                                          Ci.nsISupportsWeakReference]),
107   _init: function() {
108     debug("Starting up.");
110     BrowserElementPromptService.mapWindowToBrowserElementChild(content, this);
112     docShell.QueryInterface(Ci.nsIWebProgress)
113             .addProgressListener(this._progressListener,
114                                  Ci.nsIWebProgress.NOTIFY_LOCATION |
115                                  Ci.nsIWebProgress.NOTIFY_SECURITY |
116                                  Ci.nsIWebProgress.NOTIFY_STATE_WINDOW);
118     docShell.QueryInterface(Ci.nsIWebNavigation)
119             .sessionHistory = Cc["@mozilla.org/browser/shistory;1"]
120                                 .createInstance(Ci.nsISHistory);
122     // This is necessary to get security web progress notifications.
123     var securityUI = Cc['@mozilla.org/secure_browser_ui;1']
124                        .createInstance(Ci.nsISecureBrowserUI);
125     securityUI.init(content);
127     // A cache of the menuitem dom objects keyed by the id we generate
128     // and pass to the embedder
129     this._ctxHandlers = {};
130     // Counter of contextmenu events fired
131     this._ctxCounter = 0;
133     this._shuttingDown = false;
135     addEventListener('DOMTitleChanged',
136                      this._titleChangedHandler.bind(this),
137                      /* useCapture = */ true,
138                      /* wantsUntrusted = */ false);
140     addEventListener('DOMLinkAdded',
141                      this._linkAddedHandler.bind(this),
142                      /* useCapture = */ true,
143                      /* wantsUntrusted = */ false);
145     addEventListener('MozScrolledAreaChanged',
146                      this._mozScrollAreaChanged.bind(this),
147                      /* useCapture = */ true,
148                      /* wantsUntrusted = */ false);
150     addEventListener('DOMMetaAdded',
151                      this._metaChangedHandler.bind(this),
152                      /* useCapture = */ true,
153                      /* wantsUntrusted = */ false);
155     addEventListener('DOMMetaChanged',
156                      this._metaChangedHandler.bind(this),
157                      /* useCapture = */ true,
158                      /* wantsUntrusted = */ false);
160     addEventListener('DOMMetaRemoved',
161                      this._metaChangedHandler.bind(this),
162                      /* useCapture = */ true,
163                      /* wantsUntrusted = */ false);
165     addEventListener('mozselectionstatechanged',
166                      this._selectionStateChangedHandler.bind(this),
167                      /* useCapture = */ true,
168                      /* wantsUntrusted = */ false);
170     addEventListener('scrollviewchange',
171                      this._ScrollViewChangeHandler.bind(this),
172                      /* useCapture = */ true,
173                      /* wantsUntrusted = */ false);
175     // This listens to unload events from our message manager, but /not/ from
176     // the |content| window.  That's because the window's unload event doesn't
177     // bubble, and we're not using a capturing listener.  If we'd used
178     // useCapture == true, we /would/ hear unload events from the window, which
179     // is not what we want!
180     addEventListener('unload',
181                      this._unloadHandler.bind(this),
182                      /* useCapture = */ false,
183                      /* wantsUntrusted = */ false);
185     // Registers a MozAfterPaint handler for the very first paint.
186     this._addMozAfterPaintHandler(function () {
187       sendAsyncMsg('firstpaint');
188     });
190     let self = this;
192     let mmCalls = {
193       "purge-history": this._recvPurgeHistory,
194       "get-screenshot": this._recvGetScreenshot,
195       "get-contentdimensions": this._recvGetContentDimensions,
196       "set-visible": this._recvSetVisible,
197       "get-visible": this._recvVisible,
198       "send-mouse-event": this._recvSendMouseEvent,
199       "send-touch-event": this._recvSendTouchEvent,
200       "get-can-go-back": this._recvCanGoBack,
201       "get-can-go-forward": this._recvCanGoForward,
202       "go-back": this._recvGoBack,
203       "go-forward": this._recvGoForward,
204       "reload": this._recvReload,
205       "stop": this._recvStop,
206       "zoom": this._recvZoom,
207       "unblock-modal-prompt": this._recvStopWaiting,
208       "fire-ctx-callback": this._recvFireCtxCallback,
209       "owner-visibility-change": this._recvOwnerVisibilityChange,
210       "exit-fullscreen": this._recvExitFullscreen.bind(this),
211       "activate-next-paint-listener": this._activateNextPaintListener.bind(this),
212       "set-input-method-active": this._recvSetInputMethodActive.bind(this),
213       "deactivate-next-paint-listener": this._deactivateNextPaintListener.bind(this),
214       "do-command": this._recvDoCommand
215     }
217     addMessageListener("browser-element-api:call", function(aMessage) {
218       if (aMessage.data.msg_name in mmCalls) {
219         return mmCalls[aMessage.data.msg_name].apply(self, arguments);
220       }
221     });
223     let els = Cc["@mozilla.org/eventlistenerservice;1"]
224                 .getService(Ci.nsIEventListenerService);
226     // We are using the system group for those events so if something in the
227     // content called .stopPropagation() this will still be called.
228     els.addSystemEventListener(global, 'DOMWindowClose',
229                                this._windowCloseHandler.bind(this),
230                                /* useCapture = */ false);
231     els.addSystemEventListener(global, 'DOMWindowCreated',
232                                this._windowCreatedHandler.bind(this),
233                                /* useCapture = */ true);
234     els.addSystemEventListener(global, 'DOMWindowResize',
235                                this._windowResizeHandler.bind(this),
236                                /* useCapture = */ false);
237     els.addSystemEventListener(global, 'contextmenu',
238                                this._contextmenuHandler.bind(this),
239                                /* useCapture = */ false);
240     els.addSystemEventListener(global, 'scroll',
241                                this._scrollEventHandler.bind(this),
242                                /* useCapture = */ false);
244     OBSERVED_EVENTS.forEach((aTopic) => {
245       Services.obs.addObserver(this, aTopic, false);
246     });
247   },
249   observe: function(subject, topic, data) {
250     // Ignore notifications not about our document.  (Note that |content| /can/
251     // be null; see bug 874900.)
252     if (topic !== 'activity-done' && (!content || subject != content.document))
253       return;
254     if (topic == 'activity-done' && docShell !== subject)
255       return;
256     switch (topic) {
257       case 'fullscreen-origin-change':
258         sendAsyncMsg('fullscreen-origin-change', { _payload_: data });
259         break;
260       case 'ask-parent-to-exit-fullscreen':
261         sendAsyncMsg('exit-fullscreen');
262         break;
263       case 'ask-parent-to-rollback-fullscreen':
264         sendAsyncMsg('rollback-fullscreen');
265         break;
266       case 'activity-done':
267         sendAsyncMsg('activitydone', { success: (data == 'activity-success') });
268         break;
269       case 'xpcom-shutdown':
270         this._shuttingDown = true;
271         break;
272     }
273   },
275   /**
276    * Called when our TabChildGlobal starts to die.  This is not called when the
277    * page inside |content| unloads.
278    */
279   _unloadHandler: function() {
280     this._shuttingDown = true;
281     OBSERVED_EVENTS.forEach((aTopic) => {
282       Services.obs.removeObserver(this, aTopic);
283     });
284   },
286   _tryGetInnerWindowID: function(win) {
287     let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
288                    .getInterface(Ci.nsIDOMWindowUtils);
289     try {
290       return utils.currentInnerWindowID;
291     }
292     catch(e) {
293       return null;
294     }
295   },
297   /**
298    * Show a modal prompt.  Called by BrowserElementPromptService.
299    */
300   showModalPrompt: function(win, args) {
301     let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
302                    .getInterface(Ci.nsIDOMWindowUtils);
304     args.windowID = { outer: utils.outerWindowID,
305                       inner: this._tryGetInnerWindowID(win) };
306     sendAsyncMsg('showmodalprompt', args);
308     let returnValue = this._waitForResult(win);
310     Services.obs.notifyObservers(null, 'BEC:ShownModalPrompt', null);
312     if (args.promptType == 'prompt' ||
313         args.promptType == 'confirm' ||
314         args.promptType == 'custom-prompt') {
315       return returnValue;
316     }
317   },
319   _isCommandEnabled: function(cmd) {
320     let command = COMMAND_MAP[cmd];
321     if (!command) {
322       return false;
323     }
325     return docShell.isCommandEnabled(command);
326   },
328   /**
329    * Spin in a nested event loop until we receive a unblock-modal-prompt message for
330    * this window.
331    */
332   _waitForResult: function(win) {
333     debug("_waitForResult(" + win + ")");
334     let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
335                    .getInterface(Ci.nsIDOMWindowUtils);
337     let outerWindowID = utils.outerWindowID;
338     let innerWindowID = this._tryGetInnerWindowID(win);
339     if (innerWindowID === null) {
340       // I have no idea what waiting for a result means when there's no inner
341       // window, so let's just bail.
342       debug("_waitForResult: No inner window. Bailing.");
343       return;
344     }
346     this._windowIDDict[outerWindowID] = Cu.getWeakReference(win);
348     debug("Entering modal state (outerWindowID=" + outerWindowID + ", " +
349                                 "innerWindowID=" + innerWindowID + ")");
351     utils.enterModalState();
353     // We'll decrement win.modalDepth when we receive a unblock-modal-prompt message
354     // for the window.
355     if (!win.modalDepth) {
356       win.modalDepth = 0;
357     }
358     win.modalDepth++;
359     let origModalDepth = win.modalDepth;
361     let thread = Services.tm.currentThread;
362     debug("Nested event loop - begin");
363     while (win.modalDepth == origModalDepth && !this._shuttingDown) {
364       // Bail out of the loop if the inner window changed; that means the
365       // window navigated.  Bail out when we're shutting down because otherwise
366       // we'll leak our window.
367       if (this._tryGetInnerWindowID(win) !== innerWindowID) {
368         debug("_waitForResult: Inner window ID changed " +
369               "while in nested event loop.");
370         break;
371       }
373       thread.processNextEvent(/* mayWait = */ true);
374     }
375     debug("Nested event loop - finish");
377     if (win.modalDepth == 0) {
378       delete this._windowIDDict[outerWindowID];
379     }
381     // If we exited the loop because the inner window changed, then bail on the
382     // modal prompt.
383     if (innerWindowID !== this._tryGetInnerWindowID(win)) {
384       throw Components.Exception("Modal state aborted by navigation",
385                                  Cr.NS_ERROR_NOT_AVAILABLE);
386     }
388     let returnValue = win.modalReturnValue;
389     delete win.modalReturnValue;
391     if (!this._shuttingDown) {
392       utils.leaveModalState();
393     }
395     debug("Leaving modal state (outerID=" + outerWindowID + ", " +
396                                "innerID=" + innerWindowID + ")");
397     return returnValue;
398   },
400   _recvStopWaiting: function(msg) {
401     let outerID = msg.json.windowID.outer;
402     let innerID = msg.json.windowID.inner;
403     let returnValue = msg.json.returnValue;
404     debug("recvStopWaiting(outer=" + outerID + ", inner=" + innerID +
405           ", returnValue=" + returnValue + ")");
407     if (!this._windowIDDict[outerID]) {
408       debug("recvStopWaiting: No record of outer window ID " + outerID);
409       return;
410     }
412     let win = this._windowIDDict[outerID].get();
414     if (!win) {
415       debug("recvStopWaiting, but window is gone\n");
416       return;
417     }
419     if (innerID !== this._tryGetInnerWindowID(win)) {
420       debug("recvStopWaiting, but inner ID has changed\n");
421       return;
422     }
424     debug("recvStopWaiting " + win);
425     win.modalReturnValue = returnValue;
426     win.modalDepth--;
427   },
429   _recvExitFullscreen: function() {
430     var utils = content.document.defaultView
431                        .QueryInterface(Ci.nsIInterfaceRequestor)
432                        .getInterface(Ci.nsIDOMWindowUtils);
433     utils.exitFullscreen();
434   },
436   _titleChangedHandler: function(e) {
437     debug("Got titlechanged: (" + e.target.title + ")");
438     var win = e.target.defaultView;
440     // Ignore titlechanges which don't come from the top-level
441     // <iframe mozbrowser> window.
442     if (win == content) {
443       sendAsyncMsg('titlechange', { _payload_: e.target.title });
444     }
445     else {
446       debug("Not top level!");
447     }
448   },
450   _maybeCopyAttribute: function(src, target, attribute) {
451     if (src.getAttribute(attribute)) {
452       target[attribute] = src.getAttribute(attribute);
453     }
454   },
456   _iconChangedHandler: function(e) {
457     debug('Got iconchanged: (' + e.target.href + ')');
458     let icon = { href: e.target.href };
459     this._maybeCopyAttribute(e.target, icon, 'sizes');
460     this._maybeCopyAttribute(e.target, icon, 'rel');
461     sendAsyncMsg('iconchange', icon);
462   },
464   _openSearchHandler: function(e) {
465     debug('Got opensearch: (' + e.target.href + ')');
467     if (e.target.type !== "application/opensearchdescription+xml") {
468       return;
469     }
471     sendAsyncMsg('opensearch', { title: e.target.title,
472                                  href: e.target.href });
474   },
476   _manifestChangedHandler: function(e) {
477     debug('Got manifestchanged: (' + e.target.href + ')');
478     let manifest = { href: e.target.href };
479     sendAsyncMsg('manifestchange', manifest);
481   },
483   // Processes the "rel" field in <link> tags and forward to specific handlers.
484   _linkAddedHandler: function(e) {
485     let win = e.target.ownerDocument.defaultView;
486     // Ignore links which don't come from the top-level
487     // <iframe mozbrowser> window.
488     if (win != content) {
489       debug('Not top level!');
490       return;
491     }
493     let handlers = {
494       'icon': this._iconChangedHandler.bind(this),
495       'apple-touch-icon': this._iconChangedHandler.bind(this),
496       'search': this._openSearchHandler,
497       'manifest': this._manifestChangedHandler
498     };
500     debug('Got linkAdded: (' + e.target.href + ') ' + e.target.rel);
501     e.target.rel.split(' ').forEach(function(x) {
502       let token = x.toLowerCase();
503       if (handlers[token]) {
504         handlers[token](e);
505       }
506     }, this);
507   },
509   _metaChangedHandler: function(e) {
510     let win = e.target.ownerDocument.defaultView;
511     // Ignore metas which don't come from the top-level
512     // <iframe mozbrowser> window.
513     if (win != content) {
514       debug('Not top level!');
515       return;
516     }
518     if (!e.target.name) {
519       return;
520     }
522     debug('Got metaChanged: (' + e.target.name + ') ' + e.target.content);
524     let handlers = {
525       'theme-color': this._themeColorChangedHandler,
526       'application-name': this._applicationNameChangedHandler
527     };
529     let handler = handlers[e.target.name];
530     if (handler) {
531       handler(e.type, e.target);
532     }
533   },
535   _applicationNameChangedHandler: function(eventType, target) {
536     if (eventType !== 'DOMMetaAdded') {
537       // Bug 1037448 - Decide what to do when <meta name="application-name">
538       // changes
539       return;
540     }
542     let meta = { name: 'application-name',
543                  content: target.content };
545     let lang;
546     let elm;
548     for (elm = target;
549          !lang && elm && elm.nodeType == target.ELEMENT_NODE;
550          elm = elm.parentNode) {
551       if (elm.hasAttribute('lang')) {
552         lang = elm.getAttribute('lang');
553         continue;
554       }
556       if (elm.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')) {
557         lang = elm.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang');
558         continue;
559       }
560     }
562     // No lang has been detected.
563     if (!lang && elm.nodeType == target.DOCUMENT_NODE) {
564       lang = elm.contentLanguage;
565     }
567     if (lang) {
568       meta.lang = lang;
569     }
571     sendAsyncMsg('metachange', meta);
572   },
574   _ScrollViewChangeHandler: function(e) {
575     e.stopPropagation();
576     let detail = {
577       state: e.state,
578       scrollX: e.scrollX,
579       scrollY: e.scrollY,
580     };
581     sendAsyncMsg('scrollviewchange', detail);
582   },
584   _selectionStateChangedHandler: function(e) {
585     e.stopPropagation();
587     if (!this._isContentWindowCreated) {
588       return;
589     }
591     let boundingClientRect = e.boundingClientRect;
593     let isCollapsed = (e.selectedText.length == 0);
594     let isMouseUp = (e.states.indexOf('mouseup') == 0);
595     let canPaste = this._isCommandEnabled("paste");
597     if (this._selectionStateChangedTarget != e.target) {
598       // SelectionStateChanged events with the following states are not
599       // necessary to trigger the text dialog, bypass these events
600       // by default.
601       //
602       if(e.states.length == 0 ||
603          e.states.indexOf('drag') == 0 ||
604          e.states.indexOf('keypress') == 0 ||
605          e.states.indexOf('mousedown') == 0) {
606         return;
607       }
609       // The collapsed SelectionStateChanged event is unnecessary to dispatch,
610       // bypass this event by default, but here comes some exceptional cases
611       if (isCollapsed) {
612         if (isMouseUp && canPaste) {
613           // Always dispatch to support shortcut mode which can paste previous
614           // copied content easily
615         } else if (e.states.indexOf('blur') == 0) {
616           // Always dispatch to notify the blur for the focus content
617         } else if (e.states.indexOf('taponcaret') == 0) {
618           // Always dispatch to notify the caret be touched
619         } else {
620           return;
621         }
622       }
623     }
625     // If we select something and selection range is visible, we cache current
626     // event's target to selectionStateChangedTarget.
627     // And dispatch the next SelectionStateChagne event if target is matched, so
628     // that the parent side can hide the text dialog.
629     // We clear selectionStateChangedTarget if selection carets are invisible.
630     if (e.visible && !isCollapsed) {
631       this._selectionStateChangedTarget = e.target;
632     } else if (canPaste && isCollapsed) {
633       this._selectionStateChangedTarget = e.target;
634     } else {
635       this._selectionStateChangedTarget = null;
636     }
638     let zoomFactor = content.screen.width / content.innerWidth;
640     let detail = {
641       rect: {
642         width: boundingClientRect ? boundingClientRect.width : 0,
643         height: boundingClientRect ? boundingClientRect.height : 0,
644         top: boundingClientRect ? boundingClientRect.top : 0,
645         bottom: boundingClientRect ? boundingClientRect.bottom : 0,
646         left: boundingClientRect ? boundingClientRect.left : 0,
647         right: boundingClientRect ? boundingClientRect.right : 0,
648       },
649       commands: {
650         canSelectAll: this._isCommandEnabled("selectall"),
651         canCut: this._isCommandEnabled("cut"),
652         canCopy: this._isCommandEnabled("copy"),
653         canPaste: this._isCommandEnabled("paste"),
654       },
655       zoomFactor: zoomFactor,
656       states: e.states,
657       isCollapsed: (e.selectedText.length == 0),
658       visible: e.visible,
659     };
661     // Get correct geometry information if we have nested iframe.
662     let currentWindow = e.target.defaultView;
663     while (currentWindow.realFrameElement) {
664       let currentRect = currentWindow.realFrameElement.getBoundingClientRect();
665       detail.rect.top += currentRect.top;
666       detail.rect.bottom += currentRect.top;
667       detail.rect.left += currentRect.left;
668       detail.rect.right += currentRect.left;
669       currentWindow = currentWindow.realFrameElement.ownerDocument.defaultView;
670     }
672     sendAsyncMsg('selectionstatechanged', detail);
673   },
675   _themeColorChangedHandler: function(eventType, target) {
676     let meta = {
677       name: 'theme-color',
678       content: target.content,
679       type: eventType.replace('DOMMeta', '').toLowerCase()
680     };
681     sendAsyncMsg('metachange', meta);
682   },
684   _addMozAfterPaintHandler: function(callback) {
685     function onMozAfterPaint() {
686       let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
687       if (uri.spec != "about:blank") {
688         debug("Got afterpaint event: " + uri.spec);
689         removeEventListener('MozAfterPaint', onMozAfterPaint,
690                             /* useCapture = */ true);
691         callback();
692       }
693     }
695     addEventListener('MozAfterPaint', onMozAfterPaint, /* useCapture = */ true);
696     return onMozAfterPaint;
697   },
699   _removeMozAfterPaintHandler: function(listener) {
700     removeEventListener('MozAfterPaint', listener,
701                         /* useCapture = */ true);
702   },
704   _activateNextPaintListener: function(e) {
705     if (!this._nextPaintHandler) {
706       this._nextPaintHandler = this._addMozAfterPaintHandler(function () {
707         this._nextPaintHandler = null;
708         sendAsyncMsg('nextpaint');
709       }.bind(this));
710     }
711   },
713   _deactivateNextPaintListener: function(e) {
714     if (this._nextPaintHandler) {
715       this._removeMozAfterPaintHandler(this._nextPaintHandler);
716       this._nextPaintHandler = null;
717     }
718   },
720   _windowCloseHandler: function(e) {
721     let win = e.target;
722     if (win != content || e.defaultPrevented) {
723       return;
724     }
726     debug("Closing window " + win);
727     sendAsyncMsg('close');
729     // Inform the window implementation that we handled this close ourselves.
730     e.preventDefault();
731   },
733   _windowCreatedHandler: function(e) {
734     let targetDocShell = e.target.defaultView
735           .QueryInterface(Ci.nsIInterfaceRequestor)
736           .getInterface(Ci.nsIWebNavigation);
737     if (targetDocShell != docShell) {
738       return;
739     }
741     let uri = docShell.QueryInterface(Ci.nsIWebNavigation).currentURI;
742     debug("Window created: " + uri.spec);
743     if (uri.spec != "about:blank") {
744       this._addMozAfterPaintHandler(function () {
745         sendAsyncMsg('documentfirstpaint');
746       });
747       this._isContentWindowCreated = true;
748       // Handle pending SetInputMethodActive request.
749       while (this._pendingSetInputMethodActive.length > 0) {
750         this._recvSetInputMethodActive(this._pendingSetInputMethodActive.shift());
751       }
752     }
753   },
755   _windowResizeHandler: function(e) {
756     let win = e.target;
757     if (win != content || e.defaultPrevented) {
758       return;
759     }
761     debug("resizing window " + win);
762     sendAsyncMsg('resize', { width: e.detail.width, height: e.detail.height });
764     // Inform the window implementation that we handled this resize ourselves.
765     e.preventDefault();
766   },
768   _contextmenuHandler: function(e) {
769     debug("Got contextmenu");
771     if (e.defaultPrevented) {
772       return;
773     }
775     this._ctxCounter++;
776     this._ctxHandlers = {};
778     var elem = e.target;
779     var menuData = {systemTargets: [], contextmenu: null};
780     var ctxMenuId = null;
782     while (elem && elem.parentNode) {
783       var ctxData = this._getSystemCtxMenuData(elem);
784       if (ctxData) {
785         menuData.systemTargets.push({
786           nodeName: elem.nodeName,
787           data: ctxData
788         });
789       }
791       if (!ctxMenuId && 'hasAttribute' in elem && elem.hasAttribute('contextmenu')) {
792         ctxMenuId = elem.getAttribute('contextmenu');
793       }
794       elem = elem.parentNode;
795     }
797     if (ctxMenuId) {
798       var menu = e.target.ownerDocument.getElementById(ctxMenuId);
799       if (menu) {
800         menuData.contextmenu = this._buildMenuObj(menu, '');
801       }
802     }
804     // The value returned by the contextmenu sync call is true iff the embedder
805     // called preventDefault() on its contextmenu event.
806     //
807     // We call preventDefault() on our contextmenu event iff the embedder called
808     // preventDefault() on /its/ contextmenu event.  This way, if the embedder
809     // ignored the contextmenu event, TabChild will fire a click.
810     if (sendSyncMsg('contextmenu', menuData)[0]) {
811       e.preventDefault();
812     } else {
813       this._ctxHandlers = {};
814     }
815   },
817   _getSystemCtxMenuData: function(elem) {
818     if ((elem instanceof Ci.nsIDOMHTMLAnchorElement && elem.href) ||
819         (elem instanceof Ci.nsIDOMHTMLAreaElement && elem.href)) {
820       return {uri: elem.href,
821               text: elem.textContent.substring(0, kLongestReturnedString)};
822     }
823     if (elem instanceof Ci.nsIImageLoadingContent && elem.currentURI) {
824       return {uri: elem.currentURI.spec};
825     }
826     if (elem instanceof Ci.nsIDOMHTMLImageElement) {
827       return {uri: elem.src};
828     }
829     if (elem instanceof Ci.nsIDOMHTMLMediaElement) {
830       let hasVideo = !(elem.readyState >= elem.HAVE_METADATA &&
831                        (elem.videoWidth == 0 || elem.videoHeight == 0));
832       return {uri: elem.currentSrc || elem.src, hasVideo: hasVideo};
833     }
834     return false;
835   },
837   _scrollEventHandler: function(e) {
838     let win = e.target.defaultView;
839     if (win != content) {
840       return;
841     }
843     debug("scroll event " + win);
844     sendAsyncMsg("scroll", { top: win.scrollY, left: win.scrollX });
845   },
847   _recvPurgeHistory: function(data) {
848     debug("Received purgeHistory message: (" + data.json.id + ")");
850     let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
852     try {
853       if (history && history.count) {
854         history.PurgeHistory(history.count);
855       }
856     } catch(e) {}
858     sendAsyncMsg('got-purge-history', { id: data.json.id, successRv: true });
859   },
861   _recvGetScreenshot: function(data) {
862     debug("Received getScreenshot message: (" + data.json.id + ")");
864     let self = this;
865     let maxWidth = data.json.args.width;
866     let maxHeight = data.json.args.height;
867     let mimeType = data.json.args.mimeType;
868     let domRequestID = data.json.id;
870     let takeScreenshotClosure = function() {
871       self._takeScreenshot(maxWidth, maxHeight, mimeType, domRequestID);
872     };
874     let maxDelayMS = 2000;
875     try {
876       maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS');
877     }
878     catch(e) {}
880     // Try to wait for the event loop to go idle before we take the screenshot,
881     // but once we've waited maxDelayMS milliseconds, go ahead and take it
882     // anyway.
883     Cc['@mozilla.org/message-loop;1'].getService(Ci.nsIMessageLoop).postIdleTask(
884       takeScreenshotClosure, maxDelayMS);
885   },
887   _recvGetContentDimensions: function(data) {
888     debug("Received getContentDimensions message: (" + data.json.id + ")");
889     sendAsyncMsg('got-contentdimensions', {
890       id: data.json.id,
891       successRv: this._getContentDimensions()
892     });
893   },
895   _mozScrollAreaChanged: function(e) {
896     let dimensions = this._getContentDimensions();
897     sendAsyncMsg('scrollareachanged', {
898       width: dimensions.width,
899       height: dimensions.height
900     });
901   },
903   _getContentDimensions: function() {
904     return {
905       width: content.document.body.scrollWidth,
906       height: content.document.body.scrollHeight
907     }
908   },
910   /**
911    * Actually take a screenshot and foward the result up to our parent, given
912    * the desired maxWidth and maxHeight (in CSS pixels), and given the
913    * DOMRequest ID associated with the request from the parent.
914    */
915   _takeScreenshot: function(maxWidth, maxHeight, mimeType, domRequestID) {
916     // You can think of the screenshotting algorithm as carrying out the
917     // following steps:
918     //
919     // - Calculate maxWidth, maxHeight, and viewport's width and height in the
920     //   dimension of device pixels by multiply the numbers with
921     //   window.devicePixelRatio.
922     //
923     // - Let scaleWidth be the factor by which we'd need to downscale the
924     //   viewport pixel width so it would fit within maxPixelWidth.
925     //   (If the viewport's pixel width is less than maxPixelWidth, let
926     //   scaleWidth be 1.) Compute scaleHeight the same way.
927     //
928     // - Scale the viewport by max(scaleWidth, scaleHeight).  Now either the
929     //   viewport's width is no larger than maxWidth, the viewport's height is
930     //   no larger than maxHeight, or both.
931     //
932     // - Crop the viewport so its width is no larger than maxWidth and its
933     //   height is no larger than maxHeight.
934     //
935     // - Set mozOpaque to true and background color to solid white
936     //   if we are taking a JPEG screenshot, keep transparent if otherwise.
937     //
938     // - Return a screenshot of the page's viewport scaled and cropped per
939     //   above.
940     debug("Taking a screenshot: maxWidth=" + maxWidth +
941           ", maxHeight=" + maxHeight +
942           ", mimeType=" + mimeType +
943           ", domRequestID=" + domRequestID + ".");
945     if (!content) {
946       // If content is not loaded yet, bail out since even sendAsyncMessage
947       // fails...
948       debug("No content yet!");
949       return;
950     }
952     let devicePixelRatio = content.devicePixelRatio;
954     let maxPixelWidth = Math.round(maxWidth * devicePixelRatio);
955     let maxPixelHeight = Math.round(maxHeight * devicePixelRatio);
957     let contentPixelWidth = content.innerWidth * devicePixelRatio;
958     let contentPixelHeight = content.innerHeight * devicePixelRatio;
960     let scaleWidth = Math.min(1, maxPixelWidth / contentPixelWidth);
961     let scaleHeight = Math.min(1, maxPixelHeight / contentPixelHeight);
963     let scale = Math.max(scaleWidth, scaleHeight);
965     let canvasWidth =
966       Math.min(maxPixelWidth, Math.round(contentPixelWidth * scale));
967     let canvasHeight =
968       Math.min(maxPixelHeight, Math.round(contentPixelHeight * scale));
970     let transparent = (mimeType !== 'image/jpeg');
972     var canvas = content.document
973       .createElementNS("http://www.w3.org/1999/xhtml", "canvas");
974     if (!transparent)
975       canvas.mozOpaque = true;
976     canvas.width = canvasWidth;
977     canvas.height = canvasHeight;
979     let ctx = canvas.getContext("2d", { willReadFrequently: true });
980     ctx.scale(scale * devicePixelRatio, scale * devicePixelRatio);
982     let flags = ctx.DRAWWINDOW_DRAW_VIEW |
983                 ctx.DRAWWINDOW_USE_WIDGET_LAYERS |
984                 ctx.DRAWWINDOW_DO_NOT_FLUSH |
985                 ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES;
986     ctx.drawWindow(content, 0, 0, content.innerWidth, content.innerHeight,
987                    transparent ? "rgba(255,255,255,0)" : "rgb(255,255,255)",
988                    flags);
990     // Take a JPEG screenshot by default instead of PNG with alpha channel.
991     // This requires us to unpremultiply the alpha channel, which
992     // is expensive on ARM processors because they lack a hardware integer
993     // division instruction.
994     canvas.toBlob(function(blob) {
995       sendAsyncMsg('got-screenshot', {
996         id: domRequestID,
997         successRv: blob
998       });
999     }, mimeType);
1000   },
1002   _recvFireCtxCallback: function(data) {
1003     debug("Received fireCtxCallback message: (" + data.json.menuitem + ")");
1004     // We silently ignore if the embedder uses an incorrect id in the callback
1005     if (data.json.menuitem in this._ctxHandlers) {
1006       this._ctxHandlers[data.json.menuitem].click();
1007       this._ctxHandlers = {};
1008     } else {
1009       debug("Ignored invalid contextmenu invocation");
1010     }
1011   },
1013   _buildMenuObj: function(menu, idPrefix) {
1014     var menuObj = {type: 'menu', items: []};
1015     this._maybeCopyAttribute(menu, menuObj, 'label');
1017     for (var i = 0, child; child = menu.children[i++];) {
1018       if (child.nodeName === 'MENU') {
1019         menuObj.items.push(this._buildMenuObj(child, idPrefix + i + '_'));
1020       } else if (child.nodeName === 'MENUITEM') {
1021         var id = this._ctxCounter + '_' + idPrefix + i;
1022         var menuitem = {id: id, type: 'menuitem'};
1023         this._maybeCopyAttribute(child, menuitem, 'label');
1024         this._maybeCopyAttribute(child, menuitem, 'icon');
1025         this._ctxHandlers[id] = child;
1026         menuObj.items.push(menuitem);
1027       }
1028     }
1029     return menuObj;
1030   },
1032   _recvSetVisible: function(data) {
1033     debug("Received setVisible message: (" + data.json.visible + ")");
1034     if (this._forcedVisible == data.json.visible) {
1035       return;
1036     }
1038     this._forcedVisible = data.json.visible;
1039     this._updateVisibility();
1040   },
1042   _recvVisible: function(data) {
1043     sendAsyncMsg('got-visible', {
1044       id: data.json.id,
1045       successRv: docShell.isActive
1046     });
1047   },
1049   /**
1050    * Called when the window which contains this iframe becomes hidden or
1051    * visible.
1052    */
1053   _recvOwnerVisibilityChange: function(data) {
1054     debug("Received ownerVisibilityChange: (" + data.json.visible + ")");
1055     this._ownerVisible = data.json.visible;
1056     this._updateVisibility();
1057   },
1059   _updateVisibility: function() {
1060     var visible = this._forcedVisible && this._ownerVisible;
1061     if (docShell.isActive !== visible) {
1062       docShell.isActive = visible;
1063       sendAsyncMsg('visibilitychange', {visible: visible});
1064     }
1065   },
1067   _recvSendMouseEvent: function(data) {
1068     let json = data.json;
1069     let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
1070                        .getInterface(Ci.nsIDOMWindowUtils);
1071     utils.sendMouseEventToWindow(json.type, json.x, json.y, json.button,
1072                                  json.clickCount, json.modifiers);
1073   },
1075   _recvSendTouchEvent: function(data) {
1076     let json = data.json;
1077     let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
1078                        .getInterface(Ci.nsIDOMWindowUtils);
1079     utils.sendTouchEventToWindow(json.type, json.identifiers, json.touchesX,
1080                                  json.touchesY, json.radiisX, json.radiisY,
1081                                  json.rotationAngles, json.forces, json.count,
1082                                  json.modifiers);
1083   },
1085   _recvCanGoBack: function(data) {
1086     var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
1087     sendAsyncMsg('got-can-go-back', {
1088       id: data.json.id,
1089       successRv: webNav.canGoBack
1090     });
1091   },
1093   _recvCanGoForward: function(data) {
1094     var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
1095     sendAsyncMsg('got-can-go-forward', {
1096       id: data.json.id,
1097       successRv: webNav.canGoForward
1098     });
1099   },
1101   _recvGoBack: function(data) {
1102     try {
1103       docShell.QueryInterface(Ci.nsIWebNavigation).goBack();
1104     } catch(e) {
1105       // Silently swallow errors; these happen when we can't go back.
1106     }
1107   },
1109   _recvGoForward: function(data) {
1110     try {
1111       docShell.QueryInterface(Ci.nsIWebNavigation).goForward();
1112     } catch(e) {
1113       // Silently swallow errors; these happen when we can't go forward.
1114     }
1115   },
1117   _recvReload: function(data) {
1118     let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
1119     let reloadFlags = data.json.hardReload ?
1120       webNav.LOAD_FLAGS_BYPASS_PROXY | webNav.LOAD_FLAGS_BYPASS_CACHE :
1121       webNav.LOAD_FLAGS_NONE;
1122     try {
1123       webNav.reload(reloadFlags);
1124     } catch(e) {
1125       // Silently swallow errors; these can happen if a used cancels reload
1126     }
1127   },
1129   _recvStop: function(data) {
1130     let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
1131     webNav.stop(webNav.STOP_NETWORK);
1132   },
1134   _recvZoom: function(data) {
1135     docShell.contentViewer.fullZoom = data.json.zoom;
1136   },
1138   _recvDoCommand: function(data) {
1139     if (this._isCommandEnabled(data.json.command)) {
1140       this._selectionStateChangedTarget = null;
1141       docShell.doCommand(COMMAND_MAP[data.json.command]);
1142     }
1143   },
1145   _recvSetInputMethodActive: function(data) {
1146     let msgData = { id: data.json.id };
1147     if (!this._isContentWindowCreated) {
1148       if (data.json.args.isActive) {
1149         // To activate the input method, we should wait before the content
1150         // window is ready.
1151         this._pendingSetInputMethodActive.push(data);
1152         return;
1153       }
1154       msgData.successRv = null;
1155       sendAsyncMsg('got-set-input-method-active', msgData);
1156       return;
1157     }
1158     // Unwrap to access webpage content.
1159     let nav = XPCNativeWrapper.unwrap(content.document.defaultView.navigator);
1160     if (nav.mozInputMethod) {
1161       // Wrap to access the chrome-only attribute setActive.
1162       new XPCNativeWrapper(nav.mozInputMethod).setActive(data.json.args.isActive);
1163       msgData.successRv = null;
1164     } else {
1165       msgData.errorMsg = 'Cannot access mozInputMethod.';
1166     }
1167     sendAsyncMsg('got-set-input-method-active', msgData);
1168   },
1170   // The docShell keeps a weak reference to the progress listener, so we need
1171   // to keep a strong ref to it ourselves.
1172   _progressListener: {
1173     QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
1174                                            Ci.nsISupportsWeakReference]),
1175     _seenLoadStart: false,
1177     onLocationChange: function(webProgress, request, location, flags) {
1178       // We get progress events from subshells here, which is kind of weird.
1179       if (webProgress != docShell) {
1180         return;
1181       }
1183       // Ignore locationchange events which occur before the first loadstart.
1184       // These are usually about:blank loads we don't care about.
1185       if (!this._seenLoadStart) {
1186         return;
1187       }
1189       // Remove password and wyciwyg from uri.
1190       location = Cc["@mozilla.org/docshell/urifixup;1"]
1191         .getService(Ci.nsIURIFixup).createExposableURI(location);
1193       sendAsyncMsg('locationchange', { _payload_: location.spec });
1194     },
1196     onStateChange: function(webProgress, request, stateFlags, status) {
1197       if (webProgress != docShell) {
1198         return;
1199       }
1201       if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
1202         this._seenLoadStart = true;
1203         sendAsyncMsg('loadstart');
1204       }
1206       if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
1207         let bgColor = 'transparent';
1208         try {
1209           bgColor = content.getComputedStyle(content.document.body)
1210                            .getPropertyValue('background-color');
1211         } catch (e) {}
1212         sendAsyncMsg('loadend', {backgroundColor: bgColor});
1214         switch (status) {
1215           case Cr.NS_OK :
1216           case Cr.NS_BINDING_ABORTED :
1217             // Ignoring NS_BINDING_ABORTED, which is set when loading page is
1218             // stopped.
1219             return;
1221           // TODO See nsDocShell::DisplayLoadError to see what extra
1222           // information we should be annotating this first block of errors
1223           // with. Bug 1107091.
1224           case Cr.NS_ERROR_UNKNOWN_PROTOCOL :
1225             sendAsyncMsg('error', { type: 'unknownProtocolFound' });
1226             return;
1227           case Cr.NS_ERROR_FILE_NOT_FOUND :
1228             sendAsyncMsg('error', { type: 'fileNotFound' });
1229             return;
1230           case Cr.NS_ERROR_UNKNOWN_HOST :
1231             sendAsyncMsg('error', { type: 'dnsNotFound' });
1232             return;
1233           case Cr.NS_ERROR_CONNECTION_REFUSED :
1234             sendAsyncMsg('error', { type: 'connectionFailure' });
1235             return;
1236           case Cr.NS_ERROR_NET_INTERRUPT :
1237             sendAsyncMsg('error', { type: 'netInterrupt' });
1238             return;
1239           case Cr.NS_ERROR_NET_TIMEOUT :
1240             sendAsyncMsg('error', { type: 'netTimeout' });
1241             return;
1242           case Cr.NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION :
1243             sendAsyncMsg('error', { type: 'cspBlocked' });
1244             return;
1245           case Cr.NS_ERROR_PHISHING_URI :
1246             sendAsyncMsg('error', { type: 'phishingBlocked' });
1247             return;
1248           case Cr.NS_ERROR_MALWARE_URI :
1249             sendAsyncMsg('error', { type: 'malwareBlocked' });
1250             return;
1252           case Cr.NS_ERROR_OFFLINE :
1253             sendAsyncMsg('error', { type: 'offline' });
1254             return;
1255           case Cr.NS_ERROR_MALFORMED_URI :
1256             sendAsyncMsg('error', { type: 'malformedURI' });
1257             return;
1258           case Cr.NS_ERROR_REDIRECT_LOOP :
1259             sendAsyncMsg('error', { type: 'redirectLoop' });
1260             return;
1261           case Cr.NS_ERROR_UNKNOWN_SOCKET_TYPE :
1262             sendAsyncMsg('error', { type: 'unknownSocketType' });
1263             return;
1264           case Cr.NS_ERROR_NET_RESET :
1265             sendAsyncMsg('error', { type: 'netReset' });
1266             return;
1267           case Cr.NS_ERROR_DOCUMENT_NOT_CACHED :
1268             sendAsyncMsg('error', { type: 'notCached' });
1269             return;
1270           case Cr.NS_ERROR_DOCUMENT_IS_PRINTMODE :
1271             sendAsyncMsg('error', { type: 'isprinting' });
1272             return;
1273           case Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED :
1274             sendAsyncMsg('error', { type: 'deniedPortAccess' });
1275             return;
1276           case Cr.NS_ERROR_UNKNOWN_PROXY_HOST :
1277             sendAsyncMsg('error', { type: 'proxyResolveFailure' });
1278             return;
1279           case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED :
1280             sendAsyncMsg('error', { type: 'proxyConnectFailure' });
1281             return;
1282           case Cr.NS_ERROR_INVALID_CONTENT_ENCODING :
1283             sendAsyncMsg('error', { type: 'contentEncodingFailure' });
1284             return;
1285           case Cr.NS_ERROR_REMOTE_XUL :
1286             sendAsyncMsg('error', { type: 'remoteXUL' });
1287             return;
1288           case Cr.NS_ERROR_UNSAFE_CONTENT_TYPE :
1289             sendAsyncMsg('error', { type: 'unsafeContentType' });
1290             return;
1291           case Cr.NS_ERROR_CORRUPTED_CONTENT :
1292             sendAsyncMsg('error', { type: 'corruptedContentError' });
1293             return;
1295           default:
1296             // getErrorClass() will throw if the error code passed in is not a NSS
1297             // error code.
1298             try {
1299               let nssErrorsService = Cc['@mozilla.org/nss_errors_service;1']
1300                                        .getService(Ci.nsINSSErrorsService);
1301               if (nssErrorsService.getErrorClass(status)
1302                     == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) {
1303                 // XXX Is there a point firing the event if the error page is not
1304                 // certerror? If yes, maybe we should add a property to the
1305                 // event to to indicate whether there is a custom page. That would
1306                 // let the embedder have more control over the desired behavior.
1307                 let errorPage = null;
1308                 try {
1309                   errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF);
1310                 } catch (e) {}
1312                 if (errorPage == 'certerror') {
1313                   sendAsyncMsg('error', { type: 'certerror' });
1314                   return;
1315                 }
1316               }
1317             } catch (e) {}
1319             sendAsyncMsg('error', { type: 'other' });
1320             return;
1321         }
1322       }
1323     },
1325     onSecurityChange: function(webProgress, request, state) {
1326       if (webProgress != docShell) {
1327         return;
1328       }
1330       var stateDesc;
1331       if (state & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
1332         stateDesc = 'secure';
1333       }
1334       else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
1335         stateDesc = 'broken';
1336       }
1337       else if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
1338         stateDesc = 'insecure';
1339       }
1340       else {
1341         debug("Unexpected securitychange state!");
1342         stateDesc = '???';
1343       }
1345       var isEV = !!(state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL);
1347       sendAsyncMsg('securitychange', { state: stateDesc, extendedValidation: isEV });
1348     },
1350     onStatusChange: function(webProgress, request, status, message) {},
1351     onProgressChange: function(webProgress, request, curSelfProgress,
1352                                maxSelfProgress, curTotalProgress, maxTotalProgress) {},
1353   },
1355   // Expose the message manager for WebApps and others.
1356   _messageManagerPublic: {
1357     sendAsyncMessage: global.sendAsyncMessage.bind(global),
1358     sendSyncMessage: global.sendSyncMessage.bind(global),
1359     addMessageListener: global.addMessageListener.bind(global),
1360     removeMessageListener: global.removeMessageListener.bind(global)
1361   },
1363   get messageManager() {
1364     return this._messageManagerPublic;
1365   }
1368 var api = new BrowserElementChild();