1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/social/MozSocialAPI.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,361 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; 1.9 + 1.10 +Cu.import("resource://gre/modules/Services.jsm"); 1.11 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.12 + 1.13 +XPCOMUtils.defineLazyModuleGetter(this, "SocialService", "resource://gre/modules/SocialService.jsm"); 1.14 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); 1.15 + 1.16 +this.EXPORTED_SYMBOLS = ["MozSocialAPI", "openChatWindow", "findChromeWindowForChats", "closeAllChatWindows"]; 1.17 + 1.18 +this.MozSocialAPI = { 1.19 + _enabled: false, 1.20 + _everEnabled: false, 1.21 + set enabled(val) { 1.22 + let enable = !!val; 1.23 + if (enable == this._enabled) { 1.24 + return; 1.25 + } 1.26 + this._enabled = enable; 1.27 + 1.28 + if (enable) { 1.29 + Services.obs.addObserver(injectController, "document-element-inserted", false); 1.30 + 1.31 + if (!this._everEnabled) { 1.32 + this._everEnabled = true; 1.33 + Services.telemetry.getHistogramById("SOCIAL_ENABLED_ON_SESSION").add(true); 1.34 + } 1.35 + 1.36 + } else { 1.37 + Services.obs.removeObserver(injectController, "document-element-inserted"); 1.38 + } 1.39 + } 1.40 +}; 1.41 + 1.42 +// Called on document-element-inserted, checks that the API should be injected, 1.43 +// and then calls attachToWindow as appropriate 1.44 +function injectController(doc, topic, data) { 1.45 + try { 1.46 + let window = doc.defaultView; 1.47 + if (!window || PrivateBrowsingUtils.isWindowPrivate(window)) 1.48 + return; 1.49 + 1.50 + // Do not attempt to load the API into about: error pages 1.51 + if (doc.documentURIObject.scheme == "about") { 1.52 + return; 1.53 + } 1.54 + 1.55 + let containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor) 1.56 + .getInterface(Ci.nsIWebNavigation) 1.57 + .QueryInterface(Ci.nsIDocShell) 1.58 + .chromeEventHandler; 1.59 + // limit injecting into social panels or same-origin browser tabs if 1.60 + // social.debug.injectIntoTabs is enabled 1.61 + let allowTabs = false; 1.62 + try { 1.63 + allowTabs = containingBrowser.contentWindow == window && 1.64 + Services.prefs.getBoolPref("social.debug.injectIntoTabs"); 1.65 + } catch(e) {} 1.66 + 1.67 + let origin = containingBrowser.getAttribute("origin"); 1.68 + if (!allowTabs && !origin) { 1.69 + return; 1.70 + } 1.71 + 1.72 + // we always handle window.close on social content, even if they are not 1.73 + // "enabled". "enabled" is about the worker state and a provider may 1.74 + // still be in e.g. the share panel without having their worker enabled. 1.75 + handleWindowClose(window); 1.76 + 1.77 + SocialService.getProvider(doc.nodePrincipal.origin, function(provider) { 1.78 + if (provider && provider.enabled) { 1.79 + attachToWindow(provider, window); 1.80 + } 1.81 + }); 1.82 + } catch(e) { 1.83 + Cu.reportError("MozSocialAPI injectController: unable to attachToWindow for " + doc.location + ": " + e); 1.84 + } 1.85 +} 1.86 + 1.87 +// Loads mozSocial support functions associated with provider into targetWindow 1.88 +function attachToWindow(provider, targetWindow) { 1.89 + // If the loaded document isn't from the provider's origin (or a protocol 1.90 + // that inherits the principal), don't attach the mozSocial API. 1.91 + let targetDocURI = targetWindow.document.documentURIObject; 1.92 + if (!provider.isSameOrigin(targetDocURI)) { 1.93 + let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin + 1.94 + " to " + targetDocURI.spec + " since origins differ." 1.95 + Services.console.logStringMessage(msg); 1.96 + return; 1.97 + } 1.98 + 1.99 + let port = provider.workerURL ? provider.getWorkerPort(targetWindow) : null; 1.100 + 1.101 + let mozSocialObj = { 1.102 + // Use a method for backwards compat with existing providers, but we 1.103 + // should deprecate this in favor of a simple .port getter. 1.104 + getWorker: { 1.105 + enumerable: true, 1.106 + configurable: true, 1.107 + writable: true, 1.108 + value: function() { 1.109 + return { 1.110 + port: port, 1.111 + __exposedProps__: { 1.112 + port: "r" 1.113 + } 1.114 + }; 1.115 + } 1.116 + }, 1.117 + hasBeenIdleFor: { 1.118 + enumerable: true, 1.119 + configurable: true, 1.120 + writable: true, 1.121 + value: function() { 1.122 + return false; 1.123 + } 1.124 + }, 1.125 + openChatWindow: { 1.126 + enumerable: true, 1.127 + configurable: true, 1.128 + writable: true, 1.129 + value: function(toURL, callback) { 1.130 + let url = targetWindow.document.documentURIObject.resolve(toURL); 1.131 + openChatWindow(getChromeWindow(targetWindow), provider, url, callback); 1.132 + } 1.133 + }, 1.134 + openPanel: { 1.135 + enumerable: true, 1.136 + configurable: true, 1.137 + writable: true, 1.138 + value: function(toURL, offset, callback) { 1.139 + let chromeWindow = getChromeWindow(targetWindow); 1.140 + if (!chromeWindow.SocialFlyout) 1.141 + return; 1.142 + let url = targetWindow.document.documentURIObject.resolve(toURL); 1.143 + if (!provider.isSameOrigin(url)) 1.144 + return; 1.145 + chromeWindow.SocialFlyout.open(url, offset, callback); 1.146 + } 1.147 + }, 1.148 + closePanel: { 1.149 + enumerable: true, 1.150 + configurable: true, 1.151 + writable: true, 1.152 + value: function(toURL, offset, callback) { 1.153 + let chromeWindow = getChromeWindow(targetWindow); 1.154 + if (!chromeWindow.SocialFlyout || !chromeWindow.SocialFlyout.panel) 1.155 + return; 1.156 + chromeWindow.SocialFlyout.panel.hidePopup(); 1.157 + } 1.158 + }, 1.159 + // allow a provider to share to other providers through the browser 1.160 + share: { 1.161 + enumerable: true, 1.162 + configurable: true, 1.163 + writable: true, 1.164 + value: function(data) { 1.165 + let chromeWindow = getChromeWindow(targetWindow); 1.166 + if (!chromeWindow.SocialShare || chromeWindow.SocialShare.shareButton.hidden) 1.167 + throw new Error("Share is unavailable"); 1.168 + // ensure user action initates the share 1.169 + let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.170 + .getInterface(Ci.nsIDOMWindowUtils); 1.171 + if (!dwu.isHandlingUserInput) 1.172 + throw new Error("Attempt to share without user input"); 1.173 + 1.174 + // limit to a few params we want to support for now 1.175 + let dataOut = {}; 1.176 + for (let sub of ["url", "title", "description", "source"]) { 1.177 + dataOut[sub] = data[sub]; 1.178 + } 1.179 + if (data.image) 1.180 + dataOut.previews = [data.image]; 1.181 + 1.182 + chromeWindow.SocialShare.sharePage(null, dataOut); 1.183 + } 1.184 + }, 1.185 + getAttention: { 1.186 + enumerable: true, 1.187 + configurable: true, 1.188 + writable: true, 1.189 + value: function() { 1.190 + getChromeWindow(targetWindow).getAttention(); 1.191 + } 1.192 + }, 1.193 + isVisible: { 1.194 + enumerable: true, 1.195 + configurable: true, 1.196 + get: function() { 1.197 + return targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.198 + .getInterface(Ci.nsIWebNavigation) 1.199 + .QueryInterface(Ci.nsIDocShell).isActive; 1.200 + } 1.201 + } 1.202 + }; 1.203 + 1.204 + let contentObj = Cu.createObjectIn(targetWindow); 1.205 + Object.defineProperties(contentObj, mozSocialObj); 1.206 + Cu.makeObjectPropsNormal(contentObj); 1.207 + 1.208 + targetWindow.navigator.wrappedJSObject.__defineGetter__("mozSocial", function() { 1.209 + // We do this in a getter, so that we create these objects 1.210 + // only on demand (this is a potential concern, since 1.211 + // otherwise we might add one per iframe, and keep them 1.212 + // alive for as long as the window is alive). 1.213 + delete targetWindow.navigator.wrappedJSObject.mozSocial; 1.214 + return targetWindow.navigator.wrappedJSObject.mozSocial = contentObj; 1.215 + }); 1.216 + 1.217 + if (port) { 1.218 + targetWindow.addEventListener("unload", function () { 1.219 + // We want to close the port, but also want the target window to be 1.220 + // able to use the port during an unload event they setup - so we 1.221 + // set a timer which will fire after the unload events have all fired. 1.222 + schedule(function () { port.close(); }); 1.223 + }); 1.224 + } 1.225 +} 1.226 + 1.227 +function handleWindowClose(targetWindow) { 1.228 + // We allow window.close() to close the panel, so add an event handler for 1.229 + // this, then cancel the event (so the window itself doesn't die) and 1.230 + // close the panel instead. 1.231 + // However, this is typically affected by the dom.allow_scripts_to_close_windows 1.232 + // preference, but we can avoid that check by setting a flag on the window. 1.233 + let dwu = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.234 + .getInterface(Ci.nsIDOMWindowUtils); 1.235 + dwu.allowScriptsToClose(); 1.236 + 1.237 + targetWindow.addEventListener("DOMWindowClose", function _mozSocialDOMWindowClose(evt) { 1.238 + let elt = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) 1.239 + .getInterface(Ci.nsIWebNavigation) 1.240 + .QueryInterface(Ci.nsIDocShell) 1.241 + .chromeEventHandler; 1.242 + while (elt) { 1.243 + if (elt.localName == "panel") { 1.244 + elt.hidePopup(); 1.245 + break; 1.246 + } else if (elt.localName == "chatbox") { 1.247 + elt.close(); 1.248 + break; 1.249 + } 1.250 + elt = elt.parentNode; 1.251 + } 1.252 + // preventDefault stops the default window.close() function being called, 1.253 + // which doesn't actually close anything but causes things to get into 1.254 + // a bad state (an internal 'closed' flag is set and debug builds start 1.255 + // asserting as the window is used.). 1.256 + // None of the windows we inject this API into are suitable for this 1.257 + // default close behaviour, so even if we took no action above, we avoid 1.258 + // the default close from doing anything. 1.259 + evt.preventDefault(); 1.260 + }, true); 1.261 +} 1.262 + 1.263 +function schedule(callback) { 1.264 + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); 1.265 +} 1.266 + 1.267 +function getChromeWindow(contentWin) { 1.268 + return contentWin.QueryInterface(Ci.nsIInterfaceRequestor) 1.269 + .getInterface(Ci.nsIWebNavigation) 1.270 + .QueryInterface(Ci.nsIDocShellTreeItem) 1.271 + .rootTreeItem 1.272 + .QueryInterface(Ci.nsIInterfaceRequestor) 1.273 + .getInterface(Ci.nsIDOMWindow); 1.274 +} 1.275 + 1.276 +function isWindowGoodForChats(win) { 1.277 + return win.SocialChatBar 1.278 + && win.SocialChatBar.isAvailable 1.279 + && !PrivateBrowsingUtils.isWindowPrivate(win); 1.280 +} 1.281 + 1.282 +function findChromeWindowForChats(preferredWindow) { 1.283 + if (preferredWindow && isWindowGoodForChats(preferredWindow)) 1.284 + return preferredWindow; 1.285 + // no good - we just use the "most recent" browser window which can host 1.286 + // chats (we used to try and "group" all chats in the same browser window, 1.287 + // but that didn't work out so well - see bug 835111 1.288 + 1.289 + // Try first the most recent window as getMostRecentWindow works 1.290 + // even on platforms where getZOrderDOMWindowEnumerator is broken 1.291 + // (ie. Linux). This will handle most cases, but won't work if the 1.292 + // foreground window is a popup. 1.293 + 1.294 + let mostRecent = Services.wm.getMostRecentWindow("navigator:browser"); 1.295 + if (isWindowGoodForChats(mostRecent)) 1.296 + return mostRecent; 1.297 + 1.298 + let topMost, enumerator; 1.299 + // *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and 1.300 + // Windows. We use BROKEN_WM_Z_ORDER as that is what some other code uses 1.301 + // and a few bugs recommend searching mxr for this symbol to identify the 1.302 + // workarounds - we want this code to be hit in such searches. 1.303 + let os = Services.appinfo.OS; 1.304 + const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin"; 1.305 + if (BROKEN_WM_Z_ORDER) { 1.306 + // this is oldest to newest and no way to change the order. 1.307 + enumerator = Services.wm.getEnumerator("navigator:browser"); 1.308 + } else { 1.309 + // here we explicitly ask for bottom-to-top so we can use the same logic 1.310 + // where BROKEN_WM_Z_ORDER is true. 1.311 + enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false); 1.312 + } 1.313 + while (enumerator.hasMoreElements()) { 1.314 + let win = enumerator.getNext(); 1.315 + if (!win.closed && isWindowGoodForChats(win)) 1.316 + topMost = win; 1.317 + } 1.318 + return topMost; 1.319 +} 1.320 + 1.321 +this.openChatWindow = 1.322 + function openChatWindow(chromeWindow, provider, url, callback, mode) { 1.323 + chromeWindow = findChromeWindowForChats(chromeWindow); 1.324 + if (!chromeWindow) { 1.325 + Cu.reportError("Failed to open a social chat window - no host window could be found."); 1.326 + return; 1.327 + } 1.328 + let fullURI = provider.resolveUri(url); 1.329 + if (!provider.isSameOrigin(fullURI)) { 1.330 + Cu.reportError("Failed to open a social chat window - the requested URL is not the same origin as the provider."); 1.331 + return; 1.332 + } 1.333 + if (!chromeWindow.SocialChatBar.openChat(provider, fullURI.spec, callback, mode)) { 1.334 + Cu.reportError("Failed to open a social chat window - the chatbar is not available in the target window."); 1.335 + return; 1.336 + } 1.337 + // getAttention is ignored if the target window is already foreground, so 1.338 + // we can call it unconditionally. 1.339 + chromeWindow.getAttention(); 1.340 +} 1.341 + 1.342 +this.closeAllChatWindows = 1.343 + function closeAllChatWindows(provider) { 1.344 + // close all attached chat windows 1.345 + let winEnum = Services.wm.getEnumerator("navigator:browser"); 1.346 + while (winEnum.hasMoreElements()) { 1.347 + let win = winEnum.getNext(); 1.348 + if (!win.SocialChatBar) 1.349 + continue; 1.350 + let chats = [c for (c of win.SocialChatBar.chatbar.children) if (c.content.getAttribute("origin") == provider.origin)]; 1.351 + [c.close() for (c of chats)]; 1.352 + } 1.353 + 1.354 + // close all standalone chat windows 1.355 + winEnum = Services.wm.getEnumerator("Social:Chat"); 1.356 + while (winEnum.hasMoreElements()) { 1.357 + let win = winEnum.getNext(); 1.358 + if (win.closed) 1.359 + continue; 1.360 + let origin = win.document.getElementById("chatter").content.getAttribute("origin"); 1.361 + if (provider.origin == origin) 1.362 + win.close(); 1.363 + } 1.364 +}