diff options
Diffstat (limited to 'application/basilisk/base/content/tab-content.js')
-rw-r--r-- | application/basilisk/base/content/tab-content.js | 1061 |
1 files changed, 1061 insertions, 0 deletions
diff --git a/application/basilisk/base/content/tab-content.js b/application/basilisk/base/content/tab-content.js new file mode 100644 index 000000000..062694fd6 --- /dev/null +++ b/application/basilisk/base/content/tab-content.js @@ -0,0 +1,1061 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This content script contains code that requires a tab browser. */ + +var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/ExtensionContent.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", + "resource:///modules/E10SUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils", + "resource://gre/modules/BrowserUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", + "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "AboutReader", + "resource://gre/modules/AboutReader.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", + "resource://gre/modules/ReaderMode.jsm"); +XPCOMUtils.defineLazyGetter(this, "SimpleServiceDiscovery", function() { + let ssdp = Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm", {}).SimpleServiceDiscovery; + // Register targets + ssdp.registerDevice({ + id: "roku:ecp", + target: "roku:ecp", + factory(aService) { + Cu.import("resource://gre/modules/RokuApp.jsm"); + return new RokuApp(aService); + }, + types: ["video/mp4"], + extensions: ["mp4"] + }); + return ssdp; +}); + +// TabChildGlobal +var global = this; + + +addEventListener("MozDOMPointerLock:Entered", function(aEvent) { + sendAsyncMessage("PointerLock:Entered", { + originNoSuffix: aEvent.target.nodePrincipal.originNoSuffix + }); +}); + +addEventListener("MozDOMPointerLock:Exited", function(aEvent) { + sendAsyncMessage("PointerLock:Exited"); +}); + + +addMessageListener("Browser:HideSessionRestoreButton", function(message) { + // Hide session restore button on about:home + let doc = content.document; + let container; + if (doc.documentURI.toLowerCase() == "about:home" && + (container = doc.getElementById("sessionRestoreContainer"))) { + container.hidden = true; + } +}); + + +addMessageListener("Browser:Reload", function(message) { + /* First, we'll try to use the session history object to reload so + * that framesets are handled properly. If we're in a special + * window (such as view-source) that has no session history, fall + * back on using the web navigation's reload method. + */ + + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + try { + let sh = webNav.sessionHistory; + if (sh) + webNav = sh.QueryInterface(Ci.nsIWebNavigation); + } catch (e) { + } + + let reloadFlags = message.data.flags; + try { + E10SUtils.wrapHandlingUserInput(content, message.data.handlingUserInput, + () => webNav.reload(reloadFlags)); + } catch (e) { + } +}); + +addMessageListener("MixedContent:ReenableProtection", function() { + docShell.mixedContentChannel = null; +}); + +addMessageListener("SecondScreen:tab-mirror", function(message) { + if (!Services.prefs.getBoolPref("browser.casting.enabled")) { + return; + } + let app = SimpleServiceDiscovery.findAppForService(message.data.service); + if (app) { + let width = content.innerWidth; + let height = content.innerHeight; + let viewport = {cssWidth: width, cssHeight: height, width, height}; + app.mirror(function() {}, content, viewport, function() {}, content); + } +}); + +var AboutHomeListener = { + init(chromeGlobal) { + chromeGlobal.addEventListener("AboutHomeLoad", this, false, true); + }, + + get isAboutHome() { + return content.document.documentURI.toLowerCase() == "about:home"; + }, + + handleEvent(aEvent) { + if (!this.isAboutHome) { + return; + } + switch (aEvent.type) { + case "AboutHomeLoad": + this.onPageLoad(); + break; + case "click": + this.onClick(aEvent); + break; + case "pagehide": + this.onPageHide(aEvent); + break; + } + }, + + receiveMessage(aMessage) { + if (!this.isAboutHome) { + return; + } + switch (aMessage.name) { + case "AboutHome:Update": + this.onUpdate(aMessage.data); + break; + } + }, + + onUpdate(aData) { + let doc = content.document; + if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isContentWindowPrivate(content)) + doc.getElementById("launcher").setAttribute("session", "true"); + + // Inject search engine and snippets URL. + let docElt = doc.documentElement; + // Set snippetsVersion last, which triggers to show the snippets when it's set. + docElt.setAttribute("snippetsURL", aData.snippetsURL); + if (aData.showKnowYourRights) + docElt.setAttribute("showKnowYourRights", "true"); + docElt.setAttribute("snippetsVersion", aData.snippetsVersion); + }, + + onPageLoad() { + addMessageListener("AboutHome:Update", this); + addEventListener("click", this, true); + addEventListener("pagehide", this, true); + + sendAsyncMessage("AboutHome:MaybeShowAutoMigrationUndoNotification"); + sendAsyncMessage("AboutHome:RequestUpdate"); + }, + + onClick(aEvent) { + if (!aEvent.isTrusted || // Don't trust synthetic events + aEvent.button == 2 || aEvent.target.localName != "button") { + return; + } + + let originalTarget = aEvent.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + if (ownerDoc.documentURI != "about:home") { + // This shouldn't happen, but we're being defensive. + return; + } + + let elmId = originalTarget.getAttribute("id"); + + switch (elmId) { + case "restorePreviousSession": + sendAsyncMessage("AboutHome:RestorePreviousSession"); + ownerDoc.getElementById("launcher").removeAttribute("session"); + break; + + case "downloads": + sendAsyncMessage("AboutHome:Downloads"); + break; + + case "bookmarks": + sendAsyncMessage("AboutHome:Bookmarks"); + break; + + case "history": + sendAsyncMessage("AboutHome:History"); + break; + + case "addons": + sendAsyncMessage("AboutHome:Addons"); + break; + + case "sync": + sendAsyncMessage("AboutHome:Sync"); + break; + + case "settings": + sendAsyncMessage("AboutHome:Settings"); + break; + } + }, + + onPageHide(aEvent) { + if (aEvent.target.defaultView.frameElement) { + return; + } + removeMessageListener("AboutHome:Update", this); + removeEventListener("click", this, true); + removeEventListener("pagehide", this, true); + }, +}; +AboutHomeListener.init(this); + +var AboutPrivateBrowsingListener = { + init(chromeGlobal) { + chromeGlobal.addEventListener("AboutPrivateBrowsingOpenWindow", this, + false, true); + chromeGlobal.addEventListener("AboutPrivateBrowsingToggleTrackingProtection", this, + false, true); + chromeGlobal.addEventListener("AboutPrivateBrowsingDontShowIntroPanelAgain", this, + false, true); + }, + + get isAboutPrivateBrowsing() { + return content.document.documentURI.toLowerCase() == "about:privatebrowsing"; + }, + + handleEvent(aEvent) { + if (!this.isAboutPrivateBrowsing) { + return; + } + switch (aEvent.type) { + case "AboutPrivateBrowsingOpenWindow": + sendAsyncMessage("AboutPrivateBrowsing:OpenPrivateWindow"); + break; + case "AboutPrivateBrowsingToggleTrackingProtection": + sendAsyncMessage("AboutPrivateBrowsing:ToggleTrackingProtection"); + break; + case "AboutPrivateBrowsingDontShowIntroPanelAgain": + sendAsyncMessage("AboutPrivateBrowsing:DontShowIntroPanelAgain"); + break; + } + }, +}; +AboutPrivateBrowsingListener.init(this); + +var AboutReaderListener = { + + _articlePromise: null, + + _isLeavingReaderableReaderMode: false, + + init() { + addEventListener("AboutReaderContentLoaded", this, false, true); + addEventListener("DOMContentLoaded", this, false); + addEventListener("pageshow", this, false); + addEventListener("pagehide", this, false); + addMessageListener("Reader:ToggleReaderMode", this); + addMessageListener("Reader:PushState", this); + }, + + receiveMessage(message) { + switch (message.name) { + case "Reader:ToggleReaderMode": + if (!this.isAboutReader) { + this._articlePromise = ReaderMode.parseDocument(content.document).catch(Cu.reportError); + ReaderMode.enterReaderMode(docShell, content); + } else { + this._isLeavingReaderableReaderMode = this.isReaderableAboutReader; + ReaderMode.leaveReaderMode(docShell, content); + } + break; + + case "Reader:PushState": + this.updateReaderButton(!!(message.data && message.data.isArticle)); + break; + } + }, + + get isAboutReader() { + if (!content) { + return false; + } + return content.document.documentURI.startsWith("about:reader"); + }, + + get isReaderableAboutReader() { + return this.isAboutReader && + !content.document.documentElement.dataset.isError; + }, + + handleEvent(aEvent) { + if (aEvent.originalTarget.defaultView != content) { + return; + } + + switch (aEvent.type) { + case "AboutReaderContentLoaded": + if (!this.isAboutReader) { + return; + } + + if (content.document.body) { + // Update the toolbar icon to show the "reader active" icon. + sendAsyncMessage("Reader:UpdateReaderButton"); + new AboutReader(global, content, this._articlePromise); + this._articlePromise = null; + } + break; + + case "pagehide": + this.cancelPotentialPendingReadabilityCheck(); + // this._isLeavingReaderableReaderMode is used here to keep the Reader Mode icon + // visible in the location bar when transitioning from reader-mode page + // back to the readable source page. + sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: this._isLeavingReaderableReaderMode }); + if (this._isLeavingReaderableReaderMode) { + this._isLeavingReaderableReaderMode = false; + } + break; + + case "pageshow": + // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded" + // event, so we need to rely on "pageshow" in this case. + if (aEvent.persisted) { + this.updateReaderButton(); + } + break; + case "DOMContentLoaded": + this.updateReaderButton(); + break; + + } + }, + + /** + * NB: this function will update the state of the reader button asynchronously + * after the next mozAfterPaint call (assuming reader mode is enabled and + * this is a suitable document). Calling it on things which won't be + * painted is not going to work. + */ + updateReaderButton(forceNonArticle) { + if (!ReaderMode.isEnabledForParseOnLoad || this.isAboutReader || + !content || !(content.document instanceof content.HTMLDocument) || + content.document.mozSyntheticDocument) { + return; + } + + this.scheduleReadabilityCheckPostPaint(forceNonArticle); + }, + + cancelPotentialPendingReadabilityCheck() { + if (this._pendingReadabilityCheck) { + removeEventListener("MozAfterPaint", this._pendingReadabilityCheck); + delete this._pendingReadabilityCheck; + } + }, + + scheduleReadabilityCheckPostPaint(forceNonArticle) { + if (this._pendingReadabilityCheck) { + // We need to stop this check before we re-add one because we don't know + // if forceNonArticle was true or false last time. + this.cancelPotentialPendingReadabilityCheck(); + } + this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind(this, forceNonArticle); + addEventListener("MozAfterPaint", this._pendingReadabilityCheck); + }, + + onPaintWhenWaitedFor(forceNonArticle, event) { + // In non-e10s, we'll get called for paints other than ours, and so it's + // possible that this page hasn't been laid out yet, in which case we + // should wait until we get an event that does relate to our layout. We + // determine whether any of our content got painted by checking if there + // are any painted rects. + if (!event.clientRects.length) { + return; + } + + this.cancelPotentialPendingReadabilityCheck(); + // Only send updates when there are articles; there's no point updating with + // |false| all the time. + if (ReaderMode.isProbablyReaderable(content.document)) { + sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: true }); + } else if (forceNonArticle) { + sendAsyncMessage("Reader:UpdateReaderButton", { isArticle: false }); + } + }, +}; +AboutReaderListener.init(); + + +var ContentSearchMediator = { + + whitelist: new Set([ + "about:home", + "about:newtab", + ]), + + init(chromeGlobal) { + chromeGlobal.addEventListener("ContentSearchClient", this, true, true); + addMessageListener("ContentSearch", this); + }, + + handleEvent(event) { + if (this._contentWhitelisted) { + this._sendMsg(event.detail.type, event.detail.data); + } + }, + + receiveMessage(msg) { + if (msg.data.type == "AddToWhitelist") { + for (let uri of msg.data.data) { + this.whitelist.add(uri); + } + this._sendMsg("AddToWhitelistAck"); + return; + } + if (this._contentWhitelisted) { + this._fireEvent(msg.data.type, msg.data.data); + } + }, + + get _contentWhitelisted() { + return this.whitelist.has(content.document.documentURI); + }, + + _sendMsg(type, data = null) { + sendAsyncMessage("ContentSearch", { + type, + data, + }); + }, + + _fireEvent(type, data = null) { + let event = Cu.cloneInto({ + detail: { + type, + data, + }, + }, content); + content.dispatchEvent(new content.CustomEvent("ContentSearchService", + event)); + }, +}; +ContentSearchMediator.init(this); + +var PageStyleHandler = { + init() { + addMessageListener("PageStyle:Switch", this); + addMessageListener("PageStyle:Disable", this); + addEventListener("pageshow", () => this.sendStyleSheetInfo()); + }, + + get markupDocumentViewer() { + return docShell.contentViewer; + }, + + sendStyleSheetInfo() { + let filteredStyleSheets = this._filterStyleSheets(this.getAllStyleSheets()); + + sendAsyncMessage("PageStyle:StyleSheets", { + filteredStyleSheets, + authorStyleDisabled: this.markupDocumentViewer.authorStyleDisabled, + preferredStyleSheetSet: content.document.preferredStyleSheetSet + }); + }, + + getAllStyleSheets(frameset = content) { + let selfSheets = Array.slice(frameset.document.styleSheets); + let subSheets = Array.map(frameset.frames, frame => this.getAllStyleSheets(frame)); + return selfSheets.concat(...subSheets); + }, + + receiveMessage(msg) { + switch (msg.name) { + case "PageStyle:Switch": + this.markupDocumentViewer.authorStyleDisabled = false; + this._stylesheetSwitchAll(content, msg.data.title); + break; + + case "PageStyle:Disable": + this.markupDocumentViewer.authorStyleDisabled = true; + break; + } + + this.sendStyleSheetInfo(); + }, + + _stylesheetSwitchAll(frameset, title) { + if (!title || this._stylesheetInFrame(frameset, title)) { + this._stylesheetSwitchFrame(frameset, title); + } + + for (let i = 0; i < frameset.frames.length; i++) { + // Recurse into sub-frames. + this._stylesheetSwitchAll(frameset.frames[i], title); + } + }, + + _stylesheetSwitchFrame(frame, title) { + var docStyleSheets = frame.document.styleSheets; + + for (let i = 0; i < docStyleSheets.length; ++i) { + let docStyleSheet = docStyleSheets[i]; + if (docStyleSheet.title) { + docStyleSheet.disabled = (docStyleSheet.title != title); + } else if (docStyleSheet.disabled) { + docStyleSheet.disabled = false; + } + } + }, + + _stylesheetInFrame(frame, title) { + return Array.some(frame.document.styleSheets, (styleSheet) => styleSheet.title == title); + }, + + _filterStyleSheets(styleSheets) { + let result = []; + + for (let currentStyleSheet of styleSheets) { + if (!currentStyleSheet.title) + continue; + + // Skip any stylesheets that don't match the screen media type. + if (currentStyleSheet.media.length > 0) { + let mediaQueryList = currentStyleSheet.media.mediaText; + if (!content.matchMedia(mediaQueryList).matches) { + continue; + } + } + + let URI; + try { + if (!currentStyleSheet.ownerNode || + // special-case style nodes, which have no href + currentStyleSheet.ownerNode.nodeName.toLowerCase() != "style") { + URI = Services.io.newURI(currentStyleSheet.href); + } + } catch (e) { + if (e.result != Cr.NS_ERROR_MALFORMED_URI) { + throw e; + } + continue; + } + + // We won't send data URIs all of the way up to the parent, as these + // can be arbitrarily large. + let sentURI = (!URI || URI.scheme == "data") ? null : URI.spec; + + result.push({ + title: currentStyleSheet.title, + disabled: currentStyleSheet.disabled, + href: sentURI, + }); + } + + return result; + }, +}; +PageStyleHandler.init(); + +// Keep a reference to the translation content handler to avoid it it being GC'ed. +var trHandler = null; +if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) { + Cu.import("resource:///modules/translation/TranslationContentHandler.jsm"); + trHandler = new TranslationContentHandler(global, docShell); +} + +function gKeywordURIFixup(fixupInfo) { + fixupInfo.QueryInterface(Ci.nsIURIFixupInfo); + if (!fixupInfo.consumer) { + return; + } + + // Ignore info from other docshells + let parent = fixupInfo.consumer.QueryInterface(Ci.nsIDocShellTreeItem).sameTypeRootTreeItem; + if (parent != docShell) + return; + + let data = {}; + for (let f of Object.keys(fixupInfo)) { + if (f == "consumer" || typeof fixupInfo[f] == "function") + continue; + + if (fixupInfo[f] && fixupInfo[f] instanceof Ci.nsIURI) { + data[f] = fixupInfo[f].spec; + } else { + data[f] = fixupInfo[f]; + } + } + + sendAsyncMessage("Browser:URIFixup", data); +} +Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup", false); +addEventListener("unload", () => { + Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup"); +}, false); + +addMessageListener("Browser:AppTab", function(message) { + if (docShell) { + docShell.isAppTab = message.data.isAppTab; + } +}); + +let PrerenderContentHandler = { + init() { + this._pending = []; + this._idMonotonic = 0; + this._initialized = true; + addMessageListener("Prerender:Canceled", this); + addMessageListener("Prerender:Swapped", this); + }, + + get initialized() { + return !!this._initialized; + }, + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "Prerender:Canceled": { + for (let i = 0; i < this._pending.length; ++i) { + if (this._pending[i].id === aMessage.data.id) { + if (this._pending[i].failure) { + this._pending[i].failure.run(); + } + // Remove the item from the array + this._pending.splice(i, 1); + break; + } + } + break; + } + case "Prerender:Swapped": { + for (let i = 0; i < this._pending.length; ++i) { + if (this._pending[i].id === aMessage.data.id) { + if (this._pending[i].success) { + this._pending[i].success.run(); + } + // Remove the item from the array + this._pending.splice(i, 1); + break; + } + } + break; + } + } + }, + + startPrerenderingDocument(aHref, aReferrer) { + // XXX: Make this constant a pref + if (this._pending.length >= 2) { + return; + } + + let id = ++this._idMonotonic; + sendAsyncMessage("Prerender:Request", { + href: aHref.spec, + referrer: aReferrer ? aReferrer.spec : null, + id, + }); + + this._pending.push({ + href: aHref, + referrer: aReferrer, + id, + success: null, + failure: null, + }); + }, + + shouldSwitchToPrerenderedDocument(aHref, aReferrer, aSuccess, aFailure) { + // Check if we think there is a prerendering document pending for the given + // href and referrer. If we think there is one, we will send a message to + // the parent process asking it to do a swap, and hook up the success and + // failure listeners. + for (let i = 0; i < this._pending.length; ++i) { + let p = this._pending[i]; + if (p.href.equals(aHref) && p.referrer.equals(aReferrer)) { + p.success = aSuccess; + p.failure = aFailure; + sendAsyncMessage("Prerender:Swap", {id: p.id}); + return true; + } + } + + return false; + } +}; + +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + // We only want to initialize the PrerenderContentHandler in the content + // process. Outside of the content process, this should be unused. + PrerenderContentHandler.init(); +} + +var WebBrowserChrome = { + onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) { + return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab); + }, + + // Check whether this URI should load in the current process + shouldLoadURI(aDocShell, aURI, aReferrer, aHasPostData, aTriggeringPrincipal) { + if (!E10SUtils.shouldLoadURI(aDocShell, aURI, aReferrer, aHasPostData)) { + E10SUtils.redirectLoad(aDocShell, aURI, aReferrer, aTriggeringPrincipal, false); + return false; + } + + return true; + }, + + shouldLoadURIInThisProcess(aURI) { + return E10SUtils.shouldLoadURIInThisProcess(aURI); + }, + + // Try to reload the currently active or currently loading page in a new process. + reloadInFreshProcess(aDocShell, aURI, aReferrer, aTriggeringPrincipal, aLoadFlags) { + E10SUtils.redirectLoad(aDocShell, aURI, aReferrer, aTriggeringPrincipal, true, aLoadFlags); + return true; + }, + + startPrerenderingDocument(aHref, aReferrer) { + if (PrerenderContentHandler.initialized) { + PrerenderContentHandler.startPrerenderingDocument(aHref, aReferrer); + } + }, + + shouldSwitchToPrerenderedDocument(aHref, aReferrer, aSuccess, aFailure) { + if (PrerenderContentHandler.initialized) { + return PrerenderContentHandler.shouldSwitchToPrerenderedDocument( + aHref, aReferrer, aSuccess, aFailure); + } + return false; + } +}; + +if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) { + let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsITabChild); + tabchild.webBrowserChrome = WebBrowserChrome; +} + + +var DOMFullscreenHandler = { + + init() { + addMessageListener("DOMFullscreen:Entered", this); + addMessageListener("DOMFullscreen:CleanUp", this); + addEventListener("MozDOMFullscreen:Request", this); + addEventListener("MozDOMFullscreen:Entered", this); + addEventListener("MozDOMFullscreen:NewOrigin", this); + addEventListener("MozDOMFullscreen:Exit", this); + addEventListener("MozDOMFullscreen:Exited", this); + }, + + get _windowUtils() { + if (!content) { + return null; + } + return content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils); + }, + + receiveMessage(aMessage) { + let windowUtils = this._windowUtils; + switch (aMessage.name) { + case "DOMFullscreen:Entered": { + this._lastTransactionId = windowUtils.lastTransactionId; + if (!windowUtils.handleFullscreenRequests() && + !content.document.fullscreenElement) { + // If we don't actually have any pending fullscreen request + // to handle, neither we have been in fullscreen, tell the + // parent to just exit. + sendAsyncMessage("DOMFullscreen:Exit"); + } + break; + } + case "DOMFullscreen:CleanUp": { + // If we've exited fullscreen at this point, no need to record + // transaction id or call exit fullscreen. This is especially + // important for non-e10s, since in that case, it is possible + // that no more paint would be triggered after this point. + if (content.document.fullscreenElement && windowUtils) { + this._lastTransactionId = windowUtils.lastTransactionId; + windowUtils.exitFullscreen(); + } + break; + } + } + }, + + handleEvent(aEvent) { + switch (aEvent.type) { + case "MozDOMFullscreen:Request": { + sendAsyncMessage("DOMFullscreen:Request"); + break; + } + case "MozDOMFullscreen:NewOrigin": { + sendAsyncMessage("DOMFullscreen:NewOrigin", { + originNoSuffix: aEvent.target.nodePrincipal.originNoSuffix, + }); + break; + } + case "MozDOMFullscreen:Exit": { + sendAsyncMessage("DOMFullscreen:Exit"); + break; + } + case "MozDOMFullscreen:Entered": + case "MozDOMFullscreen:Exited": { + addEventListener("MozAfterPaint", this); + if (!content || !content.document.fullscreenElement) { + // If we receive any fullscreen change event, and find we are + // actually not in fullscreen, also ask the parent to exit to + // ensure that the parent always exits fullscreen when we do. + sendAsyncMessage("DOMFullscreen:Exit"); + } + break; + } + case "MozAfterPaint": { + // Only send Painted signal after we actually finish painting + // the transition for the fullscreen change. + // Note that this._lastTransactionId is not set when in non-e10s + // mode, so we need to check that explicitly. + if (!this._lastTransactionId || + aEvent.transactionId > this._lastTransactionId) { + removeEventListener("MozAfterPaint", this); + sendAsyncMessage("DOMFullscreen:Painted"); + } + break; + } + } + } +}; +DOMFullscreenHandler.init(); + +var RefreshBlocker = { + PREF: "accessibility.blockautorefresh", + + // Bug 1247100 - When a refresh is caused by an HTTP header, + // onRefreshAttempted will be fired before onLocationChange. + // When a refresh is caused by a <meta> tag in the document, + // onRefreshAttempted will be fired after onLocationChange. + // + // We only ever want to send a message to the parent after + // onLocationChange has fired, since the parent uses the + // onLocationChange update to clear transient notifications. + // Sending the message before onLocationChange will result in + // us creating the notification, and then clearing it very + // soon after. + // + // To account for both cases (onRefreshAttempted before + // onLocationChange, and onRefreshAttempted after onLocationChange), + // we'll hold a mapping of DOM Windows that we see get + // sent through both onLocationChange and onRefreshAttempted. + // When either run, they'll check the WeakMap for the existence + // of the DOM Window. If it doesn't exist, it'll add it. If + // it finds it, it'll know that it's safe to send the message + // to the parent, since we know that both have fired. + // + // The DOM Window is removed from blockedWindows when we notice + // the nsIWebProgress change state to STATE_STOP for the + // STATE_IS_WINDOW case. + // + // DOM Windows are mapped to a JS object that contains the data + // to be sent to the parent to show the notification. Since that + // data is only known when onRefreshAttempted is fired, it's only + // ever stashed in the map if onRefreshAttempted fires first - + // otherwise, null is set as the value of the mapping. + blockedWindows: new WeakMap(), + + init() { + if (Services.prefs.getBoolPref(this.PREF)) { + this.enable(); + } + + Services.prefs.addObserver(this.PREF, this, false); + }, + + uninit() { + if (Services.prefs.getBoolPref(this.PREF)) { + this.disable(); + } + + Services.prefs.removeObserver(this.PREF, this); + }, + + observe(subject, topic, data) { + if (topic == "nsPref:changed" && data == this.PREF) { + if (Services.prefs.getBoolPref(this.PREF)) { + this.enable(); + } else { + this.disable(); + } + } + }, + + enable() { + this._filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"] + .createInstance(Ci.nsIWebProgress); + this._filter.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_ALL); + + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.addProgressListener(this._filter, Ci.nsIWebProgress.NOTIFY_ALL); + + addMessageListener("RefreshBlocker:Refresh", this); + }, + + disable() { + let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebProgress); + webProgress.removeProgressListener(this._filter); + + this._filter.removeProgressListener(this); + this._filter = null; + + removeMessageListener("RefreshBlocker:Refresh", this); + }, + + send(data) { + sendAsyncMessage("RefreshBlocker:Blocked", data); + }, + + /** + * Notices when the nsIWebProgress transitions to STATE_STOP for + * the STATE_IS_WINDOW case, which will clear any mappings from + * blockedWindows. + */ + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW && + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + this.blockedWindows.delete(aWebProgress.DOMWindow); + } + }, + + /** + * Notices when the location has changed. If, when running, + * onRefreshAttempted has already fired for this DOM Window, will + * send the appropriate refresh blocked data to the parent. + */ + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + let win = aWebProgress.DOMWindow; + if (this.blockedWindows.has(win)) { + let data = this.blockedWindows.get(win); + if (data) { + // We saw onRefreshAttempted before onLocationChange, so + // send the message to the parent to show the notification. + this.send(data); + } + } else { + this.blockedWindows.set(win, null); + } + }, + + /** + * Notices when a refresh / reload was attempted. If, when running, + * onLocationChange has not yet run, will stash the appropriate data + * into the blockedWindows map to be sent when onLocationChange fires. + */ + onRefreshAttempted(aWebProgress, aURI, aDelay, aSameURI) { + let win = aWebProgress.DOMWindow; + let outerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .outerWindowID; + + let data = { + URI: aURI.spec, + originCharset: aURI.originCharset, + delay: aDelay, + sameURI: aSameURI, + outerWindowID, + }; + + if (this.blockedWindows.has(win)) { + // onLocationChange must have fired before, so we can tell the + // parent to show the notification. + this.send(data); + } else { + // onLocationChange hasn't fired yet, so stash the data in the + // map so that onLocationChange can send it when it fires. + this.blockedWindows.set(win, data); + } + + return false; + }, + + receiveMessage(message) { + let data = message.data; + + if (message.name == "RefreshBlocker:Refresh") { + let win = Services.wm.getOuterWindowWithId(data.outerWindowID); + let refreshURI = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDocShell) + .QueryInterface(Ci.nsIRefreshURI); + + let URI = BrowserUtils.makeURI(data.URI, data.originCharset, null); + + refreshURI.forceRefreshURI(URI, data.delay, true); + } + }, + + QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener2, + Ci.nsIWebProgressListener, + Ci.nsISupportsWeakReference, + Ci.nsISupports]), +}; + +RefreshBlocker.init(); + +var UserContextIdNotifier = { + init() { + addEventListener("DOMWindowCreated", this); + }, + + uninit() { + removeEventListener("DOMWindowCreated", this); + }, + + handleEvent(aEvent) { + // When the window is created, we want to inform the tabbrowser about + // the userContextId in use in order to update the UI correctly. + // Just because we cannot change the userContextId from an active docShell, + // we don't need to check DOMContentLoaded again. + this.uninit(); + + // We use the docShell because content.document can have been loaded before + // setting the originAttributes. + let loadContext = docShell.QueryInterface(Ci.nsILoadContext); + let userContextId = loadContext.originAttributes.userContextId; + + sendAsyncMessage("Browser:WindowCreated", { userContextId }); + } +}; + +UserContextIdNotifier.init(); + +ExtensionContent.init(this); +addEventListener("unload", () => { + ExtensionContent.uninit(this); + RefreshBlocker.uninit(); +}); + +addMessageListener("AllowScriptsToClose", () => { + content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindowUtils) + .allowScriptsToClose(); +}); + +addEventListener("MozAfterPaint", function onFirstPaint() { + removeEventListener("MozAfterPaint", onFirstPaint); + sendAsyncMessage("Browser:FirstPaint"); +}); |