summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-social.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-social.js')
-rw-r--r--browser/base/content/browser-social.js503
1 files changed, 503 insertions, 0 deletions
diff --git a/browser/base/content/browser-social.js b/browser/base/content/browser-social.js
new file mode 100644
index 000000000..b470efd3d
--- /dev/null
+++ b/browser/base/content/browser-social.js
@@ -0,0 +1,503 @@
+/* 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/. */
+
+// the "exported" symbols
+var SocialUI,
+ SocialShare,
+ SocialActivationListener;
+
+(function() {
+
+XPCOMUtils.defineLazyGetter(this, "OpenGraphBuilder", function() {
+ let tmp = {};
+ Cu.import("resource:///modules/Social.jsm", tmp);
+ return tmp.OpenGraphBuilder;
+});
+
+XPCOMUtils.defineLazyGetter(this, "DynamicResizeWatcher", function() {
+ let tmp = {};
+ Cu.import("resource:///modules/Social.jsm", tmp);
+ return tmp.DynamicResizeWatcher;
+});
+
+SocialUI = {
+ _initialized: false,
+
+ // Called on delayed startup to initialize the UI
+ init: function SocialUI_init() {
+ if (this._initialized) {
+ return;
+ }
+ let mm = window.getGroupMessageManager("social");
+ mm.loadFrameScript("chrome://browser/content/content.js", true);
+ mm.loadFrameScript("chrome://browser/content/social-content.js", true);
+
+ Services.obs.addObserver(this, "social:providers-changed", false);
+
+ CustomizableUI.addListener(this);
+ SocialActivationListener.init();
+
+ Social.init().then((update) => {
+ if (update)
+ this._providersChanged();
+ });
+
+ this._initialized = true;
+ },
+
+ // Called on window unload
+ uninit: function SocialUI_uninit() {
+ if (!this._initialized) {
+ return;
+ }
+ Services.obs.removeObserver(this, "social:providers-changed");
+
+ CustomizableUI.removeListener(this);
+ SocialActivationListener.uninit();
+
+ this._initialized = false;
+ },
+
+ observe: function SocialUI_observe(subject, topic, data) {
+ switch (topic) {
+ case "social:providers-changed":
+ this._providersChanged();
+ break;
+ }
+ },
+
+ _providersChanged: function() {
+ SocialShare.populateProviderMenu();
+ },
+
+ showLearnMore: function() {
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
+ openUILinkIn(url, "tab");
+ },
+
+ closeSocialPanelForLinkTraversal: function (target, linkNode) {
+ // No need to close the panel if this traversal was not retargeted
+ if (target == "" || target == "_self")
+ return;
+
+ // Check to see whether this link traversal was in a social panel
+ let win = linkNode.ownerGlobal;
+ let container = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .chromeEventHandler;
+ let containerParent = container.parentNode;
+ if (containerParent.classList.contains("social-panel") &&
+ containerParent instanceof Ci.nsIDOMXULPopupElement) {
+ // allow the link traversal to finish before closing the panel
+ setTimeout(() => {
+ containerParent.hidePopup();
+ }, 0);
+ }
+ },
+
+ get _chromeless() {
+ // Is this a popup window that doesn't want chrome shown?
+ let docElem = document.documentElement;
+ // extrachrome is not restored during session restore, so we need
+ // to check for the toolbar as well.
+ let chromeless = docElem.getAttribute("chromehidden").includes("extrachrome") ||
+ docElem.getAttribute('chromehidden').includes("toolbar");
+ // This property is "fixed" for a window, so avoid doing the check above
+ // multiple times...
+ delete this._chromeless;
+ this._chromeless = chromeless;
+ return chromeless;
+ },
+
+ get enabled() {
+ // Returns whether social is enabled *for this window*.
+ if (this._chromeless)
+ return false;
+ return Social.providers.length > 0;
+ },
+
+ canSharePage: function(aURI) {
+ return (aURI && (aURI.schemeIs('http') || aURI.schemeIs('https')));
+ },
+
+ onCustomizeEnd: function(aWindow) {
+ if (aWindow != window)
+ return;
+ // customization mode gets buttons out of sync with command updating, fix
+ // the disabled state
+ let canShare = this.canSharePage(gBrowser.currentURI);
+ let shareButton = SocialShare.shareButton;
+ if (shareButton) {
+ if (canShare) {
+ shareButton.removeAttribute("disabled")
+ } else {
+ shareButton.setAttribute("disabled", "true")
+ }
+ }
+ },
+
+ // called on tab/urlbar/location changes and after customization. Update
+ // anything that is tab specific.
+ updateState: function() {
+ goSetCommandEnabled("Social:PageShareable", this.canSharePage(gBrowser.currentURI));
+ }
+}
+
+// message manager handlers
+SocialActivationListener = {
+ init: function() {
+ messageManager.addMessageListener("Social:Activation", this);
+ },
+ uninit: function() {
+ messageManager.removeMessageListener("Social:Activation", this);
+ },
+ receiveMessage: function(aMessage) {
+ let data = aMessage.json;
+ let browser = aMessage.target;
+ data.window = window;
+ // if the source if the message is the share panel, we do a one-click
+ // installation. The source of activations is controlled by the
+ // social.directories preference
+ let options;
+ if (browser == SocialShare.iframe && Services.prefs.getBoolPref("social.share.activationPanelEnabled")) {
+ options = { bypassContentCheck: true, bypassInstallPanel: true };
+ }
+
+ Social.installProvider(data, function(manifest) {
+ Social.activateFromOrigin(manifest.origin, function(provider) {
+ if (provider.shareURL) {
+ // Ensure that the share button is somewhere usable.
+ // SocialShare.shareButton may return null if it is in the menu-panel
+ // and has never been visible, so we check the widget directly. If
+ // there is no area for the widget we move it into the toolbar.
+ let widget = CustomizableUI.getWidget("social-share-button");
+ // If the panel is already open, we can be sure that the provider can
+ // already be accessed, possibly anchored to another toolbar button.
+ // In that case we don't move the widget.
+ if (!widget.areaType && SocialShare.panel.state != "open") {
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // Ensure correct state.
+ SocialUI.onCustomizeEnd(window);
+ }
+
+ // make this new provider the selected provider. If the panel hasn't
+ // been opened, we need to make the frame first.
+ SocialShare._createFrame();
+ SocialShare.iframe.setAttribute('src', 'data:text/plain;charset=utf8,');
+ SocialShare.iframe.setAttribute('origin', provider.origin);
+ // get the right button selected
+ SocialShare.populateProviderMenu();
+ if (SocialShare.panel.state == "open") {
+ SocialShare.sharePage(provider.origin);
+ }
+ }
+ if (provider.postActivationURL) {
+ // if activated from an open share panel, we load the landing page in
+ // a background tab
+ gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
+ }
+ });
+ }, options);
+ }
+}
+
+SocialShare = {
+ get _dynamicResizer() {
+ delete this._dynamicResizer;
+ this._dynamicResizer = new DynamicResizeWatcher();
+ return this._dynamicResizer;
+ },
+
+ // Share panel may be attached to the overflow or menu button depending on
+ // customization, we need to manage open state of the anchor.
+ get anchor() {
+ let widget = CustomizableUI.getWidget("social-share-button");
+ return widget.forWindow(window).anchor;
+ },
+ // Holds the anchor node in use whilst the panel is open, because it may vary.
+ _currentAnchor: null,
+
+ get panel() {
+ return document.getElementById("social-share-panel");
+ },
+
+ get iframe() {
+ // panel.firstChild is our toolbar hbox, panel.lastChild is the iframe
+ // container hbox used for an interstitial "loading" graphic
+ return this.panel.lastChild.firstChild;
+ },
+
+ uninit: function () {
+ if (this.iframe) {
+ let mm = this.messageManager;
+ mm.removeMessageListener("PageVisibility:Show", this);
+ mm.removeMessageListener("PageVisibility:Hide", this);
+ mm.removeMessageListener("Social:DOMWindowClose", this);
+ this.iframe.removeEventListener("load", this);
+ this.iframe.remove();
+ }
+ },
+
+ _createFrame: function() {
+ let panel = this.panel;
+ if (this.iframe)
+ return;
+ this.panel.hidden = false;
+ // create and initialize the panel for this window
+ let iframe = document.createElement("browser");
+ iframe.setAttribute("type", "content");
+ iframe.setAttribute("class", "social-share-frame");
+ iframe.setAttribute("context", "contentAreaContextMenu");
+ iframe.setAttribute("tooltip", "aHTMLTooltip");
+ iframe.setAttribute("disableglobalhistory", "true");
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("message", "true");
+ iframe.setAttribute("messagemanagergroup", "social");
+ panel.lastChild.appendChild(iframe);
+ let mm = this.messageManager;
+ mm.addMessageListener("PageVisibility:Show", this);
+ mm.addMessageListener("PageVisibility:Hide", this);
+ mm.sendAsyncMessage("Social:SetErrorURL",
+ { template: "about:socialerror?mode=compactInfo&origin=%{origin}&url=%{url}" });
+ iframe.addEventListener("load", this, true);
+ mm.addMessageListener("Social:DOMWindowClose", this);
+
+ this.populateProviderMenu();
+ },
+
+ get messageManager() {
+ // The xbl bindings for the iframe may not exist yet, so we can't
+ // access iframe.messageManager directly - but can get at it with this dance.
+ return this.iframe.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader.messageManager;
+ },
+
+ receiveMessage: function(aMessage) {
+ let iframe = this.iframe;
+ switch(aMessage.name) {
+ case "PageVisibility:Show":
+ SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
+ break;
+ case "PageVisibility:Hide":
+ SocialShare._dynamicResizer.stop();
+ break;
+ case "Social:DOMWindowClose":
+ this.panel.hidePopup();
+ break;
+ }
+ },
+
+ handleEvent: function(event) {
+ switch (event.type) {
+ case "load": {
+ this.iframe.parentNode.removeAttribute("loading");
+ if (this.currentShare)
+ SocialShare.messageManager.sendAsyncMessage("Social:OpenGraphData", this.currentShare);
+ }
+ }
+ },
+
+ getSelectedProvider: function() {
+ let provider;
+ let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin");
+ if (lastProviderOrigin) {
+ provider = Social._getProviderFromOrigin(lastProviderOrigin);
+ }
+ return provider;
+ },
+
+ createTooltip: function(event) {
+ let tt = event.target;
+ let provider = Social._getProviderFromOrigin(tt.triggerNode.getAttribute("origin"));
+ tt.firstChild.setAttribute("value", provider.name);
+ tt.lastChild.setAttribute("value", provider.origin);
+ },
+
+ populateProviderMenu: function() {
+ if (!this.iframe)
+ return;
+ let providers = Social.providers.filter(p => p.shareURL);
+ let hbox = document.getElementById("social-share-provider-buttons");
+ // remove everything before the add-share-provider button (which should also
+ // be lastChild if any share providers were added)
+ let addButton = document.getElementById("add-share-provider");
+ while (hbox.lastChild != addButton) {
+ hbox.removeChild(hbox.lastChild);
+ }
+ let selectedProvider = this.getSelectedProvider();
+ for (let provider of providers) {
+ let button = document.createElement("toolbarbutton");
+ button.setAttribute("class", "toolbarbutton-1 share-provider-button");
+ button.setAttribute("type", "radio");
+ button.setAttribute("group", "share-providers");
+ button.setAttribute("image", provider.iconURL);
+ button.setAttribute("tooltip", "share-button-tooltip");
+ button.setAttribute("origin", provider.origin);
+ button.setAttribute("label", provider.name);
+ button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
+ if (provider == selectedProvider) {
+ this.defaultButton = button;
+ }
+ hbox.appendChild(button);
+ }
+ if (!this.defaultButton) {
+ this.defaultButton = addButton;
+ }
+ this.defaultButton.setAttribute("checked", "true");
+ },
+
+ get shareButton() {
+ // web-panels (bookmark/sidebar) don't include customizableui, so
+ // nsContextMenu fails when accessing shareButton, breaking
+ // browser_bug409481.js.
+ if (!window.CustomizableUI)
+ return null;
+ let widget = CustomizableUI.getWidget("social-share-button");
+ if (!widget || !widget.areaType)
+ return null;
+ return widget.forWindow(window).node;
+ },
+
+ _onclick: function() {
+ Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(0);
+ },
+
+ onShowing: function() {
+ (this._currentAnchor || this.anchor).setAttribute("open", "true");
+ this.iframe.addEventListener("click", this._onclick, true);
+ },
+
+ onHidden: function() {
+ (this._currentAnchor || this.anchor).removeAttribute("open");
+ this._currentAnchor = null;
+ this.iframe.docShellIsActive = false;
+ this.iframe.removeEventListener("click", this._onclick, true);
+ this.iframe.setAttribute("src", "data:text/plain;charset=utf8,");
+ // make sure that the frame is unloaded after it is hidden
+ this.messageManager.sendAsyncMessage("Social:ClearFrame");
+ this.currentShare = null;
+ // share panel use is over, purge any history
+ this.iframe.purgeSessionHistory();
+ },
+
+ sharePage: function(providerOrigin, graphData, target, anchor) {
+ // if providerOrigin is undefined, we use the last-used provider, or the
+ // current/default provider. The provider selection in the share panel
+ // will call sharePage with an origin for us to switch to.
+ this._createFrame();
+ let iframe = this.iframe;
+
+ // graphData is an optional param that either defines the full set of data
+ // to be shared, or partial data about the current page. It is set by a call
+ // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST
+ // define at least url. If it is undefined, we're sharing the current url in
+ // the browser tab.
+ let pageData = graphData ? graphData : this.currentShare;
+ let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) :
+ gBrowser.currentURI;
+ if (!SocialUI.canSharePage(sharedURI))
+ return;
+
+ let browserMM = gBrowser.selectedBrowser.messageManager;
+
+ // the point of this action type is that we can use existing share
+ // endpoints (e.g. oexchange) that do not support additional
+ // socialapi functionality. One tweak is that we shoot an event
+ // containing the open graph data.
+ let _dataFn;
+ if (!pageData || sharedURI == gBrowser.currentURI) {
+ browserMM.addMessageListener("PageMetadata:PageDataResult", _dataFn = (msg) => {
+ browserMM.removeMessageListener("PageMetadata:PageDataResult", _dataFn);
+ let pageData = msg.json;
+ if (graphData) {
+ // overwrite data retreived from page with data given to us as a param
+ for (let p in graphData) {
+ pageData[p] = graphData[p];
+ }
+ }
+ this.sharePage(providerOrigin, pageData, target, anchor);
+ });
+ browserMM.sendAsyncMessage("PageMetadata:GetPageData", null, { target });
+ return;
+ }
+ // if this is a share of a selected item, get any microformats
+ if (!pageData.microformats && target) {
+ browserMM.addMessageListener("PageMetadata:MicroformatsResult", _dataFn = (msg) => {
+ browserMM.removeMessageListener("PageMetadata:MicroformatsResult", _dataFn);
+ pageData.microformats = msg.data;
+ this.sharePage(providerOrigin, pageData, target, anchor);
+ });
+ browserMM.sendAsyncMessage("PageMetadata:GetMicroformats", null, { target });
+ return;
+ }
+ this.currentShare = pageData;
+
+ let provider;
+ if (providerOrigin)
+ provider = Social._getProviderFromOrigin(providerOrigin);
+ else
+ provider = this.getSelectedProvider();
+ if (!provider || !provider.shareURL) {
+ this.showDirectory(anchor);
+ return;
+ }
+ // check the menu button
+ let hbox = document.getElementById("social-share-provider-buttons");
+ let btn = hbox.querySelector("[origin='" + provider.origin + "']");
+ if (btn)
+ btn.checked = true;
+
+ let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
+
+ this._dynamicResizer.stop();
+ let size = provider.getPageSize("share");
+ if (size) {
+ // let the css on the share panel define width, but height
+ // calculations dont work on all sites, so we allow that to be
+ // defined.
+ delete size.width;
+ }
+
+ // if we've already loaded this provider/page share endpoint, we don't want
+ // to add another load event listener.
+ let endpointMatch = shareEndpoint == iframe.getAttribute("src");
+ if (endpointMatch) {
+ this._dynamicResizer.start(iframe.parentNode, iframe, size);
+ iframe.docShellIsActive = true;
+ SocialShare.messageManager.sendAsyncMessage("Social:OpenGraphData", this.currentShare);
+ } else {
+ iframe.parentNode.setAttribute("loading", "true");
+ }
+ // if the user switched between share providers we do not want that history
+ // available.
+ iframe.purgeSessionHistory();
+
+ // always ensure that origin belongs to the endpoint
+ let uri = Services.io.newURI(shareEndpoint, null, null);
+ iframe.setAttribute("origin", provider.origin);
+ iframe.setAttribute("src", shareEndpoint);
+ this._openPanel(anchor);
+ },
+
+ showDirectory: function(anchor) {
+ this._createFrame();
+ let iframe = this.iframe;
+ if (iframe.getAttribute("src") == "about:providerdirectory")
+ return;
+ iframe.removeAttribute("origin");
+ iframe.parentNode.setAttribute("loading", "true");
+
+ iframe.setAttribute("src", "about:providerdirectory");
+ this._openPanel(anchor);
+ },
+
+ _openPanel: function(anchor) {
+ this._currentAnchor = anchor || this.anchor;
+ anchor = document.getAnonymousElementByAttribute(this._currentAnchor, "class", "toolbarbutton-icon");
+ this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
+ Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
+ }
+};
+
+})();