Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
michael@0 | 6 | |
michael@0 | 7 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 8 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 9 | |
michael@0 | 10 | XPCOMUtils.defineLazyModuleGetter(this, "SocialService", "resource://gre/modules/SocialService.jsm"); |
michael@0 | 11 | XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); |
michael@0 | 12 | |
michael@0 | 13 | this.EXPORTED_SYMBOLS = ["MozSocialAPI", "openChatWindow", "findChromeWindowForChats", "closeAllChatWindows"]; |
michael@0 | 14 | |
michael@0 | 15 | this.MozSocialAPI = { |
michael@0 | 16 | _enabled: false, |
michael@0 | 17 | _everEnabled: false, |
michael@0 | 18 | set enabled(val) { |
michael@0 | 19 | let enable = !!val; |
michael@0 | 20 | if (enable == this._enabled) { |
michael@0 | 21 | return; |
michael@0 | 22 | } |
michael@0 | 23 | this._enabled = enable; |
michael@0 | 24 | |
michael@0 | 25 | if (enable) { |
michael@0 | 26 | Services.obs.addObserver(injectController, "document-element-inserted", false); |
michael@0 | 27 | |
michael@0 | 28 | if (!this._everEnabled) { |
michael@0 | 29 | this._everEnabled = true; |
michael@0 | 30 | Services.telemetry.getHistogramById("SOCIAL_ENABLED_ON_SESSION").add(true); |
michael@0 | 31 | } |
michael@0 | 32 | |
michael@0 | 33 | } else { |
michael@0 | 34 | Services.obs.removeObserver(injectController, "document-element-inserted"); |
michael@0 | 35 | } |
michael@0 | 36 | } |
michael@0 | 37 | }; |
michael@0 | 38 | |
michael@0 | 39 | // Called on document-element-inserted, checks that the API should be injected, |
michael@0 | 40 | // and then calls attachToWindow as appropriate |
michael@0 | 41 | function injectController(doc, topic, data) { |
michael@0 | 42 | try { |
michael@0 | 43 | let window = doc.defaultView; |
michael@0 | 44 | if (!window || PrivateBrowsingUtils.isWindowPrivate(window)) |
michael@0 | 45 | return; |
michael@0 | 46 | |
michael@0 | 47 | // Do not attempt to load the API into about: error pages |
michael@0 | 48 | if (doc.documentURIObject.scheme == "about") { |
michael@0 | 49 | return; |
michael@0 | 50 | } |
michael@0 | 51 | |
michael@0 | 52 | let containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 53 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 54 | .QueryInterface(Ci.nsIDocShell) |
michael@0 | 55 | .chromeEventHandler; |
michael@0 | 56 | // limit injecting into social panels or same-origin browser tabs if |
michael@0 | 57 | // social.debug.injectIntoTabs is enabled |
michael@0 | 58 | let allowTabs = false; |
michael@0 | 59 | try { |
michael@0 | 60 | allowTabs = containingBrowser.contentWindow == window && |
michael@0 | 61 | Services.prefs.getBoolPref("social.debug.injectIntoTabs"); |
michael@0 | 62 | } catch(e) {} |
michael@0 | 63 | |
michael@0 | 64 | let origin = containingBrowser.getAttribute("origin"); |
michael@0 | 65 | if (!allowTabs && !origin) { |
michael@0 | 66 | return; |
michael@0 | 67 | } |
michael@0 | 68 | |
michael@0 | 69 | // we always handle window.close on social content, even if they are not |
michael@0 | 70 | // "enabled". "enabled" is about the worker state and a provider may |
michael@0 | 71 | // still be in e.g. the share panel without having their worker enabled. |
michael@0 | 72 | handleWindowClose(window); |
michael@0 | 73 | |
michael@0 | 74 | SocialService.getProvider(doc.nodePrincipal.origin, function(provider) { |
michael@0 | 75 | if (provider && provider.enabled) { |
michael@0 | 76 | attachToWindow(provider, window); |
michael@0 | 77 | } |
michael@0 | 78 | }); |
michael@0 | 79 | } catch(e) { |
michael@0 | 80 | Cu.reportError("MozSocialAPI injectController: unable to attachToWindow for " + doc.location + ": " + e); |
michael@0 | 81 | } |
michael@0 | 82 | } |
michael@0 | 83 | |
michael@0 | 84 | // Loads mozSocial support functions associated with provider into targetWindow |
michael@0 | 85 | function attachToWindow(provider, targetWindow) { |
michael@0 | 86 | // If the loaded document isn't from the provider's origin (or a protocol |
michael@0 | 87 | // that inherits the principal), don't attach the mozSocial API. |
michael@0 | 88 | let targetDocURI = targetWindow.document.documentURIObject; |
michael@0 | 89 | if (!provider.isSameOrigin(targetDocURI)) { |
michael@0 | 90 | let msg = "MozSocialAPI: not attaching mozSocial API for " + provider.origin + |
michael@0 | 91 | " to " + targetDocURI.spec + " since origins differ." |
michael@0 | 92 | Services.console.logStringMessage(msg); |
michael@0 | 93 | return; |
michael@0 | 94 | } |
michael@0 | 95 | |
michael@0 | 96 | let port = provider.workerURL ? provider.getWorkerPort(targetWindow) : null; |
michael@0 | 97 | |
michael@0 | 98 | let mozSocialObj = { |
michael@0 | 99 | // Use a method for backwards compat with existing providers, but we |
michael@0 | 100 | // should deprecate this in favor of a simple .port getter. |
michael@0 | 101 | getWorker: { |
michael@0 | 102 | enumerable: true, |
michael@0 | 103 | configurable: true, |
michael@0 | 104 | writable: true, |
michael@0 | 105 | value: function() { |
michael@0 | 106 | return { |
michael@0 | 107 | port: port, |
michael@0 | 108 | __exposedProps__: { |
michael@0 | 109 | port: "r" |
michael@0 | 110 | } |
michael@0 | 111 | }; |
michael@0 | 112 | } |
michael@0 | 113 | }, |
michael@0 | 114 | hasBeenIdleFor: { |
michael@0 | 115 | enumerable: true, |
michael@0 | 116 | configurable: true, |
michael@0 | 117 | writable: true, |
michael@0 | 118 | value: function() { |
michael@0 | 119 | return false; |
michael@0 | 120 | } |
michael@0 | 121 | }, |
michael@0 | 122 | openChatWindow: { |
michael@0 | 123 | enumerable: true, |
michael@0 | 124 | configurable: true, |
michael@0 | 125 | writable: true, |
michael@0 | 126 | value: function(toURL, callback) { |
michael@0 | 127 | let url = targetWindow.document.documentURIObject.resolve(toURL); |
michael@0 | 128 | openChatWindow(getChromeWindow(targetWindow), provider, url, callback); |
michael@0 | 129 | } |
michael@0 | 130 | }, |
michael@0 | 131 | openPanel: { |
michael@0 | 132 | enumerable: true, |
michael@0 | 133 | configurable: true, |
michael@0 | 134 | writable: true, |
michael@0 | 135 | value: function(toURL, offset, callback) { |
michael@0 | 136 | let chromeWindow = getChromeWindow(targetWindow); |
michael@0 | 137 | if (!chromeWindow.SocialFlyout) |
michael@0 | 138 | return; |
michael@0 | 139 | let url = targetWindow.document.documentURIObject.resolve(toURL); |
michael@0 | 140 | if (!provider.isSameOrigin(url)) |
michael@0 | 141 | return; |
michael@0 | 142 | chromeWindow.SocialFlyout.open(url, offset, callback); |
michael@0 | 143 | } |
michael@0 | 144 | }, |
michael@0 | 145 | closePanel: { |
michael@0 | 146 | enumerable: true, |
michael@0 | 147 | configurable: true, |
michael@0 | 148 | writable: true, |
michael@0 | 149 | value: function(toURL, offset, callback) { |
michael@0 | 150 | let chromeWindow = getChromeWindow(targetWindow); |
michael@0 | 151 | if (!chromeWindow.SocialFlyout || !chromeWindow.SocialFlyout.panel) |
michael@0 | 152 | return; |
michael@0 | 153 | chromeWindow.SocialFlyout.panel.hidePopup(); |
michael@0 | 154 | } |
michael@0 | 155 | }, |
michael@0 | 156 | // allow a provider to share to other providers through the browser |
michael@0 | 157 | share: { |
michael@0 | 158 | enumerable: true, |
michael@0 | 159 | configurable: true, |
michael@0 | 160 | writable: true, |
michael@0 | 161 | value: function(data) { |
michael@0 | 162 | let chromeWindow = getChromeWindow(targetWindow); |
michael@0 | 163 | if (!chromeWindow.SocialShare || chromeWindow.SocialShare.shareButton.hidden) |
michael@0 | 164 | throw new Error("Share is unavailable"); |
michael@0 | 165 | // ensure user action initates the share |
michael@0 | 166 | let dwu = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 167 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 168 | if (!dwu.isHandlingUserInput) |
michael@0 | 169 | throw new Error("Attempt to share without user input"); |
michael@0 | 170 | |
michael@0 | 171 | // limit to a few params we want to support for now |
michael@0 | 172 | let dataOut = {}; |
michael@0 | 173 | for (let sub of ["url", "title", "description", "source"]) { |
michael@0 | 174 | dataOut[sub] = data[sub]; |
michael@0 | 175 | } |
michael@0 | 176 | if (data.image) |
michael@0 | 177 | dataOut.previews = [data.image]; |
michael@0 | 178 | |
michael@0 | 179 | chromeWindow.SocialShare.sharePage(null, dataOut); |
michael@0 | 180 | } |
michael@0 | 181 | }, |
michael@0 | 182 | getAttention: { |
michael@0 | 183 | enumerable: true, |
michael@0 | 184 | configurable: true, |
michael@0 | 185 | writable: true, |
michael@0 | 186 | value: function() { |
michael@0 | 187 | getChromeWindow(targetWindow).getAttention(); |
michael@0 | 188 | } |
michael@0 | 189 | }, |
michael@0 | 190 | isVisible: { |
michael@0 | 191 | enumerable: true, |
michael@0 | 192 | configurable: true, |
michael@0 | 193 | get: function() { |
michael@0 | 194 | return targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 195 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 196 | .QueryInterface(Ci.nsIDocShell).isActive; |
michael@0 | 197 | } |
michael@0 | 198 | } |
michael@0 | 199 | }; |
michael@0 | 200 | |
michael@0 | 201 | let contentObj = Cu.createObjectIn(targetWindow); |
michael@0 | 202 | Object.defineProperties(contentObj, mozSocialObj); |
michael@0 | 203 | Cu.makeObjectPropsNormal(contentObj); |
michael@0 | 204 | |
michael@0 | 205 | targetWindow.navigator.wrappedJSObject.__defineGetter__("mozSocial", function() { |
michael@0 | 206 | // We do this in a getter, so that we create these objects |
michael@0 | 207 | // only on demand (this is a potential concern, since |
michael@0 | 208 | // otherwise we might add one per iframe, and keep them |
michael@0 | 209 | // alive for as long as the window is alive). |
michael@0 | 210 | delete targetWindow.navigator.wrappedJSObject.mozSocial; |
michael@0 | 211 | return targetWindow.navigator.wrappedJSObject.mozSocial = contentObj; |
michael@0 | 212 | }); |
michael@0 | 213 | |
michael@0 | 214 | if (port) { |
michael@0 | 215 | targetWindow.addEventListener("unload", function () { |
michael@0 | 216 | // We want to close the port, but also want the target window to be |
michael@0 | 217 | // able to use the port during an unload event they setup - so we |
michael@0 | 218 | // set a timer which will fire after the unload events have all fired. |
michael@0 | 219 | schedule(function () { port.close(); }); |
michael@0 | 220 | }); |
michael@0 | 221 | } |
michael@0 | 222 | } |
michael@0 | 223 | |
michael@0 | 224 | function handleWindowClose(targetWindow) { |
michael@0 | 225 | // We allow window.close() to close the panel, so add an event handler for |
michael@0 | 226 | // this, then cancel the event (so the window itself doesn't die) and |
michael@0 | 227 | // close the panel instead. |
michael@0 | 228 | // However, this is typically affected by the dom.allow_scripts_to_close_windows |
michael@0 | 229 | // preference, but we can avoid that check by setting a flag on the window. |
michael@0 | 230 | let dwu = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 231 | .getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 232 | dwu.allowScriptsToClose(); |
michael@0 | 233 | |
michael@0 | 234 | targetWindow.addEventListener("DOMWindowClose", function _mozSocialDOMWindowClose(evt) { |
michael@0 | 235 | let elt = targetWindow.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 236 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 237 | .QueryInterface(Ci.nsIDocShell) |
michael@0 | 238 | .chromeEventHandler; |
michael@0 | 239 | while (elt) { |
michael@0 | 240 | if (elt.localName == "panel") { |
michael@0 | 241 | elt.hidePopup(); |
michael@0 | 242 | break; |
michael@0 | 243 | } else if (elt.localName == "chatbox") { |
michael@0 | 244 | elt.close(); |
michael@0 | 245 | break; |
michael@0 | 246 | } |
michael@0 | 247 | elt = elt.parentNode; |
michael@0 | 248 | } |
michael@0 | 249 | // preventDefault stops the default window.close() function being called, |
michael@0 | 250 | // which doesn't actually close anything but causes things to get into |
michael@0 | 251 | // a bad state (an internal 'closed' flag is set and debug builds start |
michael@0 | 252 | // asserting as the window is used.). |
michael@0 | 253 | // None of the windows we inject this API into are suitable for this |
michael@0 | 254 | // default close behaviour, so even if we took no action above, we avoid |
michael@0 | 255 | // the default close from doing anything. |
michael@0 | 256 | evt.preventDefault(); |
michael@0 | 257 | }, true); |
michael@0 | 258 | } |
michael@0 | 259 | |
michael@0 | 260 | function schedule(callback) { |
michael@0 | 261 | Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); |
michael@0 | 262 | } |
michael@0 | 263 | |
michael@0 | 264 | function getChromeWindow(contentWin) { |
michael@0 | 265 | return contentWin.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 266 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 267 | .QueryInterface(Ci.nsIDocShellTreeItem) |
michael@0 | 268 | .rootTreeItem |
michael@0 | 269 | .QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 270 | .getInterface(Ci.nsIDOMWindow); |
michael@0 | 271 | } |
michael@0 | 272 | |
michael@0 | 273 | function isWindowGoodForChats(win) { |
michael@0 | 274 | return win.SocialChatBar |
michael@0 | 275 | && win.SocialChatBar.isAvailable |
michael@0 | 276 | && !PrivateBrowsingUtils.isWindowPrivate(win); |
michael@0 | 277 | } |
michael@0 | 278 | |
michael@0 | 279 | function findChromeWindowForChats(preferredWindow) { |
michael@0 | 280 | if (preferredWindow && isWindowGoodForChats(preferredWindow)) |
michael@0 | 281 | return preferredWindow; |
michael@0 | 282 | // no good - we just use the "most recent" browser window which can host |
michael@0 | 283 | // chats (we used to try and "group" all chats in the same browser window, |
michael@0 | 284 | // but that didn't work out so well - see bug 835111 |
michael@0 | 285 | |
michael@0 | 286 | // Try first the most recent window as getMostRecentWindow works |
michael@0 | 287 | // even on platforms where getZOrderDOMWindowEnumerator is broken |
michael@0 | 288 | // (ie. Linux). This will handle most cases, but won't work if the |
michael@0 | 289 | // foreground window is a popup. |
michael@0 | 290 | |
michael@0 | 291 | let mostRecent = Services.wm.getMostRecentWindow("navigator:browser"); |
michael@0 | 292 | if (isWindowGoodForChats(mostRecent)) |
michael@0 | 293 | return mostRecent; |
michael@0 | 294 | |
michael@0 | 295 | let topMost, enumerator; |
michael@0 | 296 | // *sigh* - getZOrderDOMWindowEnumerator is broken except on Mac and |
michael@0 | 297 | // Windows. We use BROKEN_WM_Z_ORDER as that is what some other code uses |
michael@0 | 298 | // and a few bugs recommend searching mxr for this symbol to identify the |
michael@0 | 299 | // workarounds - we want this code to be hit in such searches. |
michael@0 | 300 | let os = Services.appinfo.OS; |
michael@0 | 301 | const BROKEN_WM_Z_ORDER = os != "WINNT" && os != "Darwin"; |
michael@0 | 302 | if (BROKEN_WM_Z_ORDER) { |
michael@0 | 303 | // this is oldest to newest and no way to change the order. |
michael@0 | 304 | enumerator = Services.wm.getEnumerator("navigator:browser"); |
michael@0 | 305 | } else { |
michael@0 | 306 | // here we explicitly ask for bottom-to-top so we can use the same logic |
michael@0 | 307 | // where BROKEN_WM_Z_ORDER is true. |
michael@0 | 308 | enumerator = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", false); |
michael@0 | 309 | } |
michael@0 | 310 | while (enumerator.hasMoreElements()) { |
michael@0 | 311 | let win = enumerator.getNext(); |
michael@0 | 312 | if (!win.closed && isWindowGoodForChats(win)) |
michael@0 | 313 | topMost = win; |
michael@0 | 314 | } |
michael@0 | 315 | return topMost; |
michael@0 | 316 | } |
michael@0 | 317 | |
michael@0 | 318 | this.openChatWindow = |
michael@0 | 319 | function openChatWindow(chromeWindow, provider, url, callback, mode) { |
michael@0 | 320 | chromeWindow = findChromeWindowForChats(chromeWindow); |
michael@0 | 321 | if (!chromeWindow) { |
michael@0 | 322 | Cu.reportError("Failed to open a social chat window - no host window could be found."); |
michael@0 | 323 | return; |
michael@0 | 324 | } |
michael@0 | 325 | let fullURI = provider.resolveUri(url); |
michael@0 | 326 | if (!provider.isSameOrigin(fullURI)) { |
michael@0 | 327 | Cu.reportError("Failed to open a social chat window - the requested URL is not the same origin as the provider."); |
michael@0 | 328 | return; |
michael@0 | 329 | } |
michael@0 | 330 | if (!chromeWindow.SocialChatBar.openChat(provider, fullURI.spec, callback, mode)) { |
michael@0 | 331 | Cu.reportError("Failed to open a social chat window - the chatbar is not available in the target window."); |
michael@0 | 332 | return; |
michael@0 | 333 | } |
michael@0 | 334 | // getAttention is ignored if the target window is already foreground, so |
michael@0 | 335 | // we can call it unconditionally. |
michael@0 | 336 | chromeWindow.getAttention(); |
michael@0 | 337 | } |
michael@0 | 338 | |
michael@0 | 339 | this.closeAllChatWindows = |
michael@0 | 340 | function closeAllChatWindows(provider) { |
michael@0 | 341 | // close all attached chat windows |
michael@0 | 342 | let winEnum = Services.wm.getEnumerator("navigator:browser"); |
michael@0 | 343 | while (winEnum.hasMoreElements()) { |
michael@0 | 344 | let win = winEnum.getNext(); |
michael@0 | 345 | if (!win.SocialChatBar) |
michael@0 | 346 | continue; |
michael@0 | 347 | let chats = [c for (c of win.SocialChatBar.chatbar.children) if (c.content.getAttribute("origin") == provider.origin)]; |
michael@0 | 348 | [c.close() for (c of chats)]; |
michael@0 | 349 | } |
michael@0 | 350 | |
michael@0 | 351 | // close all standalone chat windows |
michael@0 | 352 | winEnum = Services.wm.getEnumerator("Social:Chat"); |
michael@0 | 353 | while (winEnum.hasMoreElements()) { |
michael@0 | 354 | let win = winEnum.getNext(); |
michael@0 | 355 | if (win.closed) |
michael@0 | 356 | continue; |
michael@0 | 357 | let origin = win.document.getElementById("chatter").content.getAttribute("origin"); |
michael@0 | 358 | if (provider.origin == origin) |
michael@0 | 359 | win.close(); |
michael@0 | 360 | } |
michael@0 | 361 | } |