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