mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-06 17:16:12 +00:00
362 lines
13 KiB
JavaScript
362 lines
13 KiB
JavaScript
/* 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();
|
|
}
|
|
}
|