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/. */
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;
19 //dump("BrowserElementChildPreload - " + msg + "\n");
22 function sendAsyncMsg(msg, data) {
23 // Ensure that we don't send any messages before BrowserElementChild.js
25 if (!BrowserElementIsReady)
33 sendAsyncMessage('browser-element-api:call', data);
36 function sendSyncMsg(msg, data) {
37 // Ensure that we don't send any messages before BrowserElementChild.js
39 if (!BrowserElementIsReady)
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',
62 'copy': 'cmd_copyAndCollapseToEnd',
64 'selectall': 'cmd_selectAll'
68 * The BrowserElementChild implements one half of <iframe mozbrowser>.
69 * (The other half is, unsurprisingly, BrowserElementParent.)
71 * This script is injected into an <iframe mozbrowser> via
72 * nsIMessageManager::LoadFrameScript().
74 * Our job here is to listen for events within this frame and bubble them up to
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.
88 // Our docShell is visible iff _forcedVisible and _ownerVisible are both
90 this._forcedVisible = true;
91 this._ownerVisible = true;
93 this._nextPaintHandler = null;
95 this._isContentWindowCreated = false;
96 this._pendingSetInputMethodActive = [];
97 this._selectionStateChangedTarget = null;
102 BrowserElementChild.prototype = {
104 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
105 Ci.nsISupportsWeakReference]),
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');
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
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);
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);
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))
254 if (topic == 'activity-done' && docShell !== subject)
257 case 'fullscreen-origin-change':
258 sendAsyncMsg('fullscreen-origin-change', { _payload_: data });
260 case 'ask-parent-to-exit-fullscreen':
261 sendAsyncMsg('exit-fullscreen');
263 case 'ask-parent-to-rollback-fullscreen':
264 sendAsyncMsg('rollback-fullscreen');
266 case 'activity-done':
267 sendAsyncMsg('activitydone', { success: (data == 'activity-success') });
269 case 'xpcom-shutdown':
270 this._shuttingDown = true;
276 * Called when our TabChildGlobal starts to die. This is not called when the
277 * page inside |content| unloads.
279 _unloadHandler: function() {
280 this._shuttingDown = true;
281 OBSERVED_EVENTS.forEach((aTopic) => {
282 Services.obs.removeObserver(this, aTopic);
286 _tryGetInnerWindowID: function(win) {
287 let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
288 .getInterface(Ci.nsIDOMWindowUtils);
290 return utils.currentInnerWindowID;
298 * Show a modal prompt. Called by BrowserElementPromptService.
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') {
319 _isCommandEnabled: function(cmd) {
320 let command = COMMAND_MAP[cmd];
325 return docShell.isCommandEnabled(command);
329 * Spin in a nested event loop until we receive a unblock-modal-prompt message for
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.");
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
355 if (!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.");
373 thread.processNextEvent(/* mayWait = */ true);
375 debug("Nested event loop - finish");
377 if (win.modalDepth == 0) {
378 delete this._windowIDDict[outerWindowID];
381 // If we exited the loop because the inner window changed, then bail on the
383 if (innerWindowID !== this._tryGetInnerWindowID(win)) {
384 throw Components.Exception("Modal state aborted by navigation",
385 Cr.NS_ERROR_NOT_AVAILABLE);
388 let returnValue = win.modalReturnValue;
389 delete win.modalReturnValue;
391 if (!this._shuttingDown) {
392 utils.leaveModalState();
395 debug("Leaving modal state (outerID=" + outerWindowID + ", " +
396 "innerID=" + innerWindowID + ")");
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);
412 let win = this._windowIDDict[outerID].get();
415 debug("recvStopWaiting, but window is gone\n");
419 if (innerID !== this._tryGetInnerWindowID(win)) {
420 debug("recvStopWaiting, but inner ID has changed\n");
424 debug("recvStopWaiting " + win);
425 win.modalReturnValue = returnValue;
429 _recvExitFullscreen: function() {
430 var utils = content.document.defaultView
431 .QueryInterface(Ci.nsIInterfaceRequestor)
432 .getInterface(Ci.nsIDOMWindowUtils);
433 utils.exitFullscreen();
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 });
446 debug("Not top level!");
450 _maybeCopyAttribute: function(src, target, attribute) {
451 if (src.getAttribute(attribute)) {
452 target[attribute] = src.getAttribute(attribute);
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);
464 _openSearchHandler: function(e) {
465 debug('Got opensearch: (' + e.target.href + ')');
467 if (e.target.type !== "application/opensearchdescription+xml") {
471 sendAsyncMsg('opensearch', { title: e.target.title,
472 href: e.target.href });
476 _manifestChangedHandler: function(e) {
477 debug('Got manifestchanged: (' + e.target.href + ')');
478 let manifest = { href: e.target.href };
479 sendAsyncMsg('manifestchange', manifest);
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!');
494 'icon': this._iconChangedHandler.bind(this),
495 'apple-touch-icon': this._iconChangedHandler.bind(this),
496 'search': this._openSearchHandler,
497 'manifest': this._manifestChangedHandler
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]) {
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!');
518 if (!e.target.name) {
522 debug('Got metaChanged: (' + e.target.name + ') ' + e.target.content);
525 'theme-color': this._themeColorChangedHandler,
526 'application-name': this._applicationNameChangedHandler
529 let handler = handlers[e.target.name];
531 handler(e.type, e.target);
535 _applicationNameChangedHandler: function(eventType, target) {
536 if (eventType !== 'DOMMetaAdded') {
537 // Bug 1037448 - Decide what to do when <meta name="application-name">
542 let meta = { name: 'application-name',
543 content: target.content };
549 !lang && elm && elm.nodeType == target.ELEMENT_NODE;
550 elm = elm.parentNode) {
551 if (elm.hasAttribute('lang')) {
552 lang = elm.getAttribute('lang');
556 if (elm.hasAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang')) {
557 lang = elm.getAttributeNS('http://www.w3.org/XML/1998/namespace', 'lang');
562 // No lang has been detected.
563 if (!lang && elm.nodeType == target.DOCUMENT_NODE) {
564 lang = elm.contentLanguage;
571 sendAsyncMsg('metachange', meta);
574 _ScrollViewChangeHandler: function(e) {
581 sendAsyncMsg('scrollviewchange', detail);
584 _selectionStateChangedHandler: function(e) {
587 if (!this._isContentWindowCreated) {
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
602 if(e.states.length == 0 ||
603 e.states.indexOf('drag') == 0 ||
604 e.states.indexOf('keypress') == 0 ||
605 e.states.indexOf('mousedown') == 0) {
609 // The collapsed SelectionStateChanged event is unnecessary to dispatch,
610 // bypass this event by default, but here comes some exceptional cases
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
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;
635 this._selectionStateChangedTarget = null;
638 let zoomFactor = content.screen.width / content.innerWidth;
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,
650 canSelectAll: this._isCommandEnabled("selectall"),
651 canCut: this._isCommandEnabled("cut"),
652 canCopy: this._isCommandEnabled("copy"),
653 canPaste: this._isCommandEnabled("paste"),
655 zoomFactor: zoomFactor,
657 isCollapsed: (e.selectedText.length == 0),
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;
672 sendAsyncMsg('selectionstatechanged', detail);
675 _themeColorChangedHandler: function(eventType, target) {
678 content: target.content,
679 type: eventType.replace('DOMMeta', '').toLowerCase()
681 sendAsyncMsg('metachange', meta);
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);
695 addEventListener('MozAfterPaint', onMozAfterPaint, /* useCapture = */ true);
696 return onMozAfterPaint;
699 _removeMozAfterPaintHandler: function(listener) {
700 removeEventListener('MozAfterPaint', listener,
701 /* useCapture = */ true);
704 _activateNextPaintListener: function(e) {
705 if (!this._nextPaintHandler) {
706 this._nextPaintHandler = this._addMozAfterPaintHandler(function () {
707 this._nextPaintHandler = null;
708 sendAsyncMsg('nextpaint');
713 _deactivateNextPaintListener: function(e) {
714 if (this._nextPaintHandler) {
715 this._removeMozAfterPaintHandler(this._nextPaintHandler);
716 this._nextPaintHandler = null;
720 _windowCloseHandler: function(e) {
722 if (win != content || e.defaultPrevented) {
726 debug("Closing window " + win);
727 sendAsyncMsg('close');
729 // Inform the window implementation that we handled this close ourselves.
733 _windowCreatedHandler: function(e) {
734 let targetDocShell = e.target.defaultView
735 .QueryInterface(Ci.nsIInterfaceRequestor)
736 .getInterface(Ci.nsIWebNavigation);
737 if (targetDocShell != docShell) {
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');
747 this._isContentWindowCreated = true;
748 // Handle pending SetInputMethodActive request.
749 while (this._pendingSetInputMethodActive.length > 0) {
750 this._recvSetInputMethodActive(this._pendingSetInputMethodActive.shift());
755 _windowResizeHandler: function(e) {
757 if (win != content || e.defaultPrevented) {
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.
768 _contextmenuHandler: function(e) {
769 debug("Got contextmenu");
771 if (e.defaultPrevented) {
776 this._ctxHandlers = {};
779 var menuData = {systemTargets: [], contextmenu: null};
780 var ctxMenuId = null;
782 while (elem && elem.parentNode) {
783 var ctxData = this._getSystemCtxMenuData(elem);
785 menuData.systemTargets.push({
786 nodeName: elem.nodeName,
791 if (!ctxMenuId && 'hasAttribute' in elem && elem.hasAttribute('contextmenu')) {
792 ctxMenuId = elem.getAttribute('contextmenu');
794 elem = elem.parentNode;
798 var menu = e.target.ownerDocument.getElementById(ctxMenuId);
800 menuData.contextmenu = this._buildMenuObj(menu, '');
804 // The value returned by the contextmenu sync call is true iff the embedder
805 // called preventDefault() on its contextmenu event.
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]) {
813 this._ctxHandlers = {};
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)};
823 if (elem instanceof Ci.nsIImageLoadingContent && elem.currentURI) {
824 return {uri: elem.currentURI.spec};
826 if (elem instanceof Ci.nsIDOMHTMLImageElement) {
827 return {uri: elem.src};
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};
837 _scrollEventHandler: function(e) {
838 let win = e.target.defaultView;
839 if (win != content) {
843 debug("scroll event " + win);
844 sendAsyncMsg("scroll", { top: win.scrollY, left: win.scrollX });
847 _recvPurgeHistory: function(data) {
848 debug("Received purgeHistory message: (" + data.json.id + ")");
850 let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
853 if (history && history.count) {
854 history.PurgeHistory(history.count);
858 sendAsyncMsg('got-purge-history', { id: data.json.id, successRv: true });
861 _recvGetScreenshot: function(data) {
862 debug("Received getScreenshot message: (" + data.json.id + ")");
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);
874 let maxDelayMS = 2000;
876 maxDelayMS = Services.prefs.getIntPref('dom.browserElement.maxScreenshotDelayMS');
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
883 Cc['@mozilla.org/message-loop;1'].getService(Ci.nsIMessageLoop).postIdleTask(
884 takeScreenshotClosure, maxDelayMS);
887 _recvGetContentDimensions: function(data) {
888 debug("Received getContentDimensions message: (" + data.json.id + ")");
889 sendAsyncMsg('got-contentdimensions', {
891 successRv: this._getContentDimensions()
895 _mozScrollAreaChanged: function(e) {
896 let dimensions = this._getContentDimensions();
897 sendAsyncMsg('scrollareachanged', {
898 width: dimensions.width,
899 height: dimensions.height
903 _getContentDimensions: function() {
905 width: content.document.body.scrollWidth,
906 height: content.document.body.scrollHeight
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.
915 _takeScreenshot: function(maxWidth, maxHeight, mimeType, domRequestID) {
916 // You can think of the screenshotting algorithm as carrying out the
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.
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.
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.
932 // - Crop the viewport so its width is no larger than maxWidth and its
933 // height is no larger than maxHeight.
935 // - Set mozOpaque to true and background color to solid white
936 // if we are taking a JPEG screenshot, keep transparent if otherwise.
938 // - Return a screenshot of the page's viewport scaled and cropped per
940 debug("Taking a screenshot: maxWidth=" + maxWidth +
941 ", maxHeight=" + maxHeight +
942 ", mimeType=" + mimeType +
943 ", domRequestID=" + domRequestID + ".");
946 // If content is not loaded yet, bail out since even sendAsyncMessage
948 debug("No content yet!");
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);
966 Math.min(maxPixelWidth, Math.round(contentPixelWidth * scale));
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");
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)",
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', {
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 = {};
1009 debug("Ignored invalid contextmenu invocation");
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);
1032 _recvSetVisible: function(data) {
1033 debug("Received setVisible message: (" + data.json.visible + ")");
1034 if (this._forcedVisible == data.json.visible) {
1038 this._forcedVisible = data.json.visible;
1039 this._updateVisibility();
1042 _recvVisible: function(data) {
1043 sendAsyncMsg('got-visible', {
1045 successRv: docShell.isActive
1050 * Called when the window which contains this iframe becomes hidden or
1053 _recvOwnerVisibilityChange: function(data) {
1054 debug("Received ownerVisibilityChange: (" + data.json.visible + ")");
1055 this._ownerVisible = data.json.visible;
1056 this._updateVisibility();
1059 _updateVisibility: function() {
1060 var visible = this._forcedVisible && this._ownerVisible;
1061 if (docShell.isActive !== visible) {
1062 docShell.isActive = visible;
1063 sendAsyncMsg('visibilitychange', {visible: visible});
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);
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,
1085 _recvCanGoBack: function(data) {
1086 var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
1087 sendAsyncMsg('got-can-go-back', {
1089 successRv: webNav.canGoBack
1093 _recvCanGoForward: function(data) {
1094 var webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
1095 sendAsyncMsg('got-can-go-forward', {
1097 successRv: webNav.canGoForward
1101 _recvGoBack: function(data) {
1103 docShell.QueryInterface(Ci.nsIWebNavigation).goBack();
1105 // Silently swallow errors; these happen when we can't go back.
1109 _recvGoForward: function(data) {
1111 docShell.QueryInterface(Ci.nsIWebNavigation).goForward();
1113 // Silently swallow errors; these happen when we can't go forward.
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;
1123 webNav.reload(reloadFlags);
1125 // Silently swallow errors; these can happen if a used cancels reload
1129 _recvStop: function(data) {
1130 let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
1131 webNav.stop(webNav.STOP_NETWORK);
1134 _recvZoom: function(data) {
1135 docShell.contentViewer.fullZoom = data.json.zoom;
1138 _recvDoCommand: function(data) {
1139 if (this._isCommandEnabled(data.json.command)) {
1140 this._selectionStateChangedTarget = null;
1141 docShell.doCommand(COMMAND_MAP[data.json.command]);
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
1151 this._pendingSetInputMethodActive.push(data);
1154 msgData.successRv = null;
1155 sendAsyncMsg('got-set-input-method-active', msgData);
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;
1165 msgData.errorMsg = 'Cannot access mozInputMethod.';
1167 sendAsyncMsg('got-set-input-method-active', msgData);
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) {
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) {
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 });
1196 onStateChange: function(webProgress, request, stateFlags, status) {
1197 if (webProgress != docShell) {
1201 if (stateFlags & Ci.nsIWebProgressListener.STATE_START) {
1202 this._seenLoadStart = true;
1203 sendAsyncMsg('loadstart');
1206 if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
1207 let bgColor = 'transparent';
1209 bgColor = content.getComputedStyle(content.document.body)
1210 .getPropertyValue('background-color');
1212 sendAsyncMsg('loadend', {backgroundColor: bgColor});
1216 case Cr.NS_BINDING_ABORTED :
1217 // Ignoring NS_BINDING_ABORTED, which is set when loading page is
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' });
1227 case Cr.NS_ERROR_FILE_NOT_FOUND :
1228 sendAsyncMsg('error', { type: 'fileNotFound' });
1230 case Cr.NS_ERROR_UNKNOWN_HOST :
1231 sendAsyncMsg('error', { type: 'dnsNotFound' });
1233 case Cr.NS_ERROR_CONNECTION_REFUSED :
1234 sendAsyncMsg('error', { type: 'connectionFailure' });
1236 case Cr.NS_ERROR_NET_INTERRUPT :
1237 sendAsyncMsg('error', { type: 'netInterrupt' });
1239 case Cr.NS_ERROR_NET_TIMEOUT :
1240 sendAsyncMsg('error', { type: 'netTimeout' });
1242 case Cr.NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION :
1243 sendAsyncMsg('error', { type: 'cspBlocked' });
1245 case Cr.NS_ERROR_PHISHING_URI :
1246 sendAsyncMsg('error', { type: 'phishingBlocked' });
1248 case Cr.NS_ERROR_MALWARE_URI :
1249 sendAsyncMsg('error', { type: 'malwareBlocked' });
1252 case Cr.NS_ERROR_OFFLINE :
1253 sendAsyncMsg('error', { type: 'offline' });
1255 case Cr.NS_ERROR_MALFORMED_URI :
1256 sendAsyncMsg('error', { type: 'malformedURI' });
1258 case Cr.NS_ERROR_REDIRECT_LOOP :
1259 sendAsyncMsg('error', { type: 'redirectLoop' });
1261 case Cr.NS_ERROR_UNKNOWN_SOCKET_TYPE :
1262 sendAsyncMsg('error', { type: 'unknownSocketType' });
1264 case Cr.NS_ERROR_NET_RESET :
1265 sendAsyncMsg('error', { type: 'netReset' });
1267 case Cr.NS_ERROR_DOCUMENT_NOT_CACHED :
1268 sendAsyncMsg('error', { type: 'notCached' });
1270 case Cr.NS_ERROR_DOCUMENT_IS_PRINTMODE :
1271 sendAsyncMsg('error', { type: 'isprinting' });
1273 case Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED :
1274 sendAsyncMsg('error', { type: 'deniedPortAccess' });
1276 case Cr.NS_ERROR_UNKNOWN_PROXY_HOST :
1277 sendAsyncMsg('error', { type: 'proxyResolveFailure' });
1279 case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED :
1280 sendAsyncMsg('error', { type: 'proxyConnectFailure' });
1282 case Cr.NS_ERROR_INVALID_CONTENT_ENCODING :
1283 sendAsyncMsg('error', { type: 'contentEncodingFailure' });
1285 case Cr.NS_ERROR_REMOTE_XUL :
1286 sendAsyncMsg('error', { type: 'remoteXUL' });
1288 case Cr.NS_ERROR_UNSAFE_CONTENT_TYPE :
1289 sendAsyncMsg('error', { type: 'unsafeContentType' });
1291 case Cr.NS_ERROR_CORRUPTED_CONTENT :
1292 sendAsyncMsg('error', { type: 'corruptedContentError' });
1296 // getErrorClass() will throw if the error code passed in is not a NSS
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;
1309 errorPage = Services.prefs.getCharPref(CERTIFICATE_ERROR_PAGE_PREF);
1312 if (errorPage == 'certerror') {
1313 sendAsyncMsg('error', { type: 'certerror' });
1319 sendAsyncMsg('error', { type: 'other' });
1325 onSecurityChange: function(webProgress, request, state) {
1326 if (webProgress != docShell) {
1331 if (state & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
1332 stateDesc = 'secure';
1334 else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
1335 stateDesc = 'broken';
1337 else if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
1338 stateDesc = 'insecure';
1341 debug("Unexpected securitychange state!");
1345 var isEV = !!(state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL);
1347 sendAsyncMsg('securitychange', { state: stateDesc, extendedValidation: isEV });
1350 onStatusChange: function(webProgress, request, status, message) {},
1351 onProgressChange: function(webProgress, request, curSelfProgress,
1352 maxSelfProgress, curTotalProgress, maxTotalProgress) {},
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)
1363 get messageManager() {
1364 return this._messageManagerPublic;
1368 var api = new BrowserElementChild();