michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: const { classes: Cc, interfaces: Ci, utils: Cu } = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SocialService", "resource://gre/modules/SocialService.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: this.EXPORTED_SYMBOLS = ["MozSocialAPI", "openChatWindow", "findChromeWindowForChats", "closeAllChatWindows"]; michael@0: michael@0: this.MozSocialAPI = { michael@0: _enabled: false, michael@0: _everEnabled: false, michael@0: set enabled(val) { michael@0: let enable = !!val; michael@0: if (enable == this._enabled) { michael@0: return; michael@0: } michael@0: this._enabled = enable; michael@0: michael@0: if (enable) { michael@0: Services.obs.addObserver(injectController, "document-element-inserted", false); michael@0: michael@0: if (!this._everEnabled) { michael@0: this._everEnabled = true; michael@0: Services.telemetry.getHistogramById("SOCIAL_ENABLED_ON_SESSION").add(true); michael@0: } michael@0: michael@0: } else { michael@0: Services.obs.removeObserver(injectController, "document-element-inserted"); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // Called on document-element-inserted, checks that the API should be injected, michael@0: // and then calls attachToWindow as appropriate michael@0: function injectController(doc, topic, data) { michael@0: try { michael@0: let window = doc.defaultView; michael@0: if (!window || PrivateBrowsingUtils.isWindowPrivate(window)) michael@0: return; michael@0: michael@0: // Do not attempt to load the API into about: error pages michael@0: if (doc.documentURIObject.scheme == "about") { michael@0: return; michael@0: } michael@0: michael@0: let containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler; michael@0: // limit injecting into social panels or same-origin browser tabs if michael@0: // social.debug.injectIntoTabs is enabled michael@0: let allowTabs = false; michael@0: try { michael@0: allowTabs = containingBrowser.contentWindow == window && michael@0: Services.prefs.getBoolPref("social.debug.injectIntoTabs"); michael@0: } catch(e) {} michael@0: michael@0: let origin = containingBrowser.getAttribute("origin"); michael@0: if (!allowTabs && !origin) { michael@0: return; michael@0: } michael@0: michael@0: // we always handle window.close on social content, even if they are not michael@0: // "enabled". "enabled" is about the worker state and a provider may michael@0: // still be in e.g. the share panel without having their worker enabled. michael@0: handleWindowClose(window); michael@0: michael@0: SocialService.getProvider(doc.nodePrincipal.origin, function(provider) { michael@0: if (provider && provider.enabled) { michael@0: attachToWindow(provider, window); michael@0: } michael@0: }); michael@0: } catch(e) { michael@0: Cu.reportError("MozSocialAPI injectController: unable to attachToWindow for " + doc.location + ": " + e); michael@0: } michael@0: } michael@0: michael@0: // Loads mozSocial support functions associated with provider into targetWindow michael@0: function attachToWindow(provider, targetWindow) { michael@0: // If the loaded document isn't from the provider's origin (or a protocol michael@0: // that inherits the principal), don't attach the mozSocial API. michael@0: let targetDocURI = targetWindow.document.documentURIObject; michael@0: if (!provider.isSameOrigin(targetDocURI)) { michael@0: let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin + michael@0: " to " + targetDocURI.spec + " since origins differ." michael@0: Services.console.logStringMessage(msg); michael@0: return; michael@0: } michael@0: michael@0: let port = provider.workerURL ? provider.getWorkerPort(targetWindow) : null; michael@0: michael@0: let mozSocialObj = { michael@0: // Use a method for backwards compat with existing providers, but we michael@0: // should deprecate this in favor of a simple .port getter. michael@0: getWorker: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: true, michael@0: value: function() { michael@0: return { michael@0: port: port, michael@0: __exposedProps__: { michael@0: port: "r" michael@0: } michael@0: }; michael@0: } michael@0: }, michael@0: hasBeenIdleFor: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: true, michael@0: value: function() { michael@0: return false; michael@0: } michael@0: }, michael@0: openChatWindow: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: true, michael@0: value: function(toURL, callback) { michael@0: let url = targetWindow.document.documentURIObject.resolve(toURL); michael@0: openChatWindow(getChromeWindow(targetWindow), provider, url, callback); michael@0: } michael@0: }, michael@0: openPanel: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: true, michael@0: value: function(toURL, offset, callback) { michael@0: let chromeWindow = getChromeWindow(targetWindow); michael@0: if (!chromeWindow.SocialFlyout) michael@0: return; michael@0: let url = targetWindow.document.documentURIObject.resolve(toURL); michael@0: if (!provider.isSameOrigin(url)) michael@0: return; michael@0: chromeWindow.SocialFlyout.open(url, offset, callback); michael@0: } michael@0: }, michael@0: closePanel: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: true, michael@0: value: function(toURL, offset, callback) { michael@0: let chromeWindow = getChromeWindow(targetWindow); michael@0: if (!chromeWindow.SocialFlyout || !chromeWindow.SocialFlyout.panel) michael@0: return; michael@0: chromeWindow.SocialFlyout.panel.hidePopup(); michael@0: } michael@0: }, michael@0: // allow a provider to share to other providers through the browser michael@0: share: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: true, michael@0: value: function(data) { michael@0: let chromeWindow = getChromeWindow(targetWindow); michael@0: if (!chromeWindow.SocialShare || chromeWindow.SocialShare.shareButton.hidden) michael@0: throw new Error("Share is unavailable"); michael@0: // ensure user action initates the share michael@0: let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: if (!dwu.isHandlingUserInput) michael@0: throw new Error("Attempt to share without user input"); michael@0: michael@0: // limit to a few params we want to support for now michael@0: let dataOut = {}; michael@0: for (let sub of ["url", "title", "description", "source"]) { michael@0: dataOut[sub] = data[sub]; michael@0: } michael@0: if (data.image) michael@0: dataOut.previews = [data.image]; michael@0: michael@0: chromeWindow.SocialShare.sharePage(null, dataOut); michael@0: } michael@0: }, michael@0: getAttention: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: writable: true, michael@0: value: function() { michael@0: getChromeWindow(targetWindow).getAttention(); michael@0: } michael@0: }, michael@0: isVisible: { michael@0: enumerable: true, michael@0: configurable: true, michael@0: get: function() { michael@0: return targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell).isActive; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: let contentObj = Cu.createObjectIn(targetWindow); michael@0: Object.defineProperties(contentObj, mozSocialObj); michael@0: Cu.makeObjectPropsNormal(contentObj); michael@0: michael@0: targetWindow.navigator.wrappedJSObject.__defineGetter__("mozSocial", function() { michael@0: // We do this in a getter, so that we create these objects michael@0: // only on demand (this is a potential concern, since michael@0: // otherwise we might add one per iframe, and keep them michael@0: // alive for as long as the window is alive). michael@0: delete targetWindow.navigator.wrappedJSObject.mozSocial; michael@0: return targetWindow.navigator.wrappedJSObject.mozSocial = contentObj; michael@0: }); michael@0: michael@0: if (port) { michael@0: targetWindow.addEventListener("unload", function () { michael@0: // We want to close the port, but also want the target window to be michael@0: // able to use the port during an unload event they setup - so we michael@0: // set a timer which will fire after the unload events have all fired. michael@0: schedule(function () { port.close(); }); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: function handleWindowClose(targetWindow) { michael@0: // We allow window.close() to close the panel, so add an event handler for michael@0: // this, then cancel the event (so the window itself doesn't die) and michael@0: // close the panel instead. michael@0: // However, this is typically affected by the dom.allow_scripts_to_close_windows michael@0: // preference, but we can avoid that check by setting a flag on the window. michael@0: let dwu = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: dwu.allowScriptsToClose(); michael@0: michael@0: targetWindow.addEventListener("DOMWindowClose", function _mozSocialDOMWindowClose(evt) { michael@0: let elt = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShell) michael@0: .chromeEventHandler; michael@0: while (elt) { michael@0: if (elt.localName == "panel") { michael@0: elt.hidePopup(); michael@0: break; michael@0: } else if (elt.localName == "chatbox") { michael@0: elt.close(); michael@0: break; michael@0: } michael@0: elt = elt.parentNode; michael@0: } michael@0: // preventDefault stops the default window.close() function being called, michael@0: // which doesn't actually close anything but causes things to get into michael@0: // a bad state (an internal 'closed' flag is set and debug builds start michael@0: // asserting as the window is used.). michael@0: // None of the windows we inject this API into are suitable for this michael@0: // default close behaviour, so even if we took no action above, we avoid michael@0: // the default close from doing anything. michael@0: evt.preventDefault(); michael@0: }, true); michael@0: } michael@0: michael@0: function schedule(callback) { michael@0: Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); michael@0: } michael@0: michael@0: function getChromeWindow(contentWin) { michael@0: return contentWin.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebNavigation) michael@0: .QueryInterface(Ci.nsIDocShellTreeItem) michael@0: .rootTreeItem michael@0: .QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindow); michael@0: } michael@0: michael@0: function isWindowGoodForChats(win) { michael@0: return win.SocialChatBar michael@0: && win.SocialChatBar.isAvailable michael@0: && !PrivateBrowsingUtils.isWindowPrivate(win); michael@0: } michael@0: michael@0: function findChromeWindowForChats(preferredWindow) { michael@0: if (preferredWindow && isWindowGoodForChats(preferredWindow)) michael@0: return preferredWindow; michael@0: // no good - we just use the "most recent" browser window which can host michael@0: // chats (we used to try and "group" all chats in the same browser window, michael@0: // but that didn't work out so well - see bug 835111 michael@0: michael@0: // Try first the most recent window as getMostRecentWindow works michael@0: // even on platforms where getZOrderDOMWindowEnumerator is broken michael@0: // (ie. Linux). This will handle most cases, but won't work if the michael@0: // foreground window is a popup. michael@0: michael@0: let mostRecent = Services.wm.getMostRecentWindow("navigator:browser"); michael@0: if (isWindowGoodForChats(mostRecent)) michael@0: return mostRecent; michael@0: michael@0: let topMost, enumerator; michael@0: // *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and michael@0: // Windows. We use BROKEN_WM_Z_ORDER as that is what some other code uses michael@0: // and a few bugs recommend searching mxr for this symbol to identify the michael@0: // workarounds - we want this code to be hit in such searches. michael@0: let os = Services.appinfo.OS; michael@0: const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin"; michael@0: if (BROKEN_WM_Z_ORDER) { michael@0: // this is oldest to newest and no way to change the order. michael@0: enumerator = Services.wm.getEnumerator("navigator:browser"); michael@0: } else { michael@0: // here we explicitly ask for bottom-to-top so we can use the same logic michael@0: // where BROKEN_WM_Z_ORDER is true. michael@0: enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false); michael@0: } michael@0: while (enumerator.hasMoreElements()) { michael@0: let win = enumerator.getNext(); michael@0: if (!win.closed && isWindowGoodForChats(win)) michael@0: topMost = win; michael@0: } michael@0: return topMost; michael@0: } michael@0: michael@0: this.openChatWindow = michael@0: function openChatWindow(chromeWindow, provider, url, callback, mode) { michael@0: chromeWindow = findChromeWindowForChats(chromeWindow); michael@0: if (!chromeWindow) { michael@0: Cu.reportError("Failed to open a social chat window - no host window could be found."); michael@0: return; michael@0: } michael@0: let fullURI = provider.resolveUri(url); michael@0: if (!provider.isSameOrigin(fullURI)) { michael@0: Cu.reportError("Failed to open a social chat window - the requested URL is not the same origin as the provider."); michael@0: return; michael@0: } michael@0: if (!chromeWindow.SocialChatBar.openChat(provider, fullURI.spec, callback, mode)) { michael@0: Cu.reportError("Failed to open a social chat window - the chatbar is not available in the target window."); michael@0: return; michael@0: } michael@0: // getAttention is ignored if the target window is already foreground, so michael@0: // we can call it unconditionally. michael@0: chromeWindow.getAttention(); michael@0: } michael@0: michael@0: this.closeAllChatWindows = michael@0: function closeAllChatWindows(provider) { michael@0: // close all attached chat windows michael@0: let winEnum = Services.wm.getEnumerator("navigator:browser"); michael@0: while (winEnum.hasMoreElements()) { michael@0: let win = winEnum.getNext(); michael@0: if (!win.SocialChatBar) michael@0: continue; michael@0: let chats = [c for (c of win.SocialChatBar.chatbar.children) if (c.content.getAttribute("origin") == provider.origin)]; michael@0: [c.close() for (c of chats)]; michael@0: } michael@0: michael@0: // close all standalone chat windows michael@0: winEnum = Services.wm.getEnumerator("Social:Chat"); michael@0: while (winEnum.hasMoreElements()) { michael@0: let win = winEnum.getNext(); michael@0: if (win.closed) michael@0: continue; michael@0: let origin = win.document.getElementById("chatter").content.getAttribute("origin"); michael@0: if (provider.origin == origin) michael@0: win.close(); michael@0: } michael@0: }