summaryrefslogtreecommitdiffstats
path: root/browser
diff options
context:
space:
mode:
authorMoonchild <mcwerewolf@gmail.com>2018-05-25 22:46:40 +0200
committerGitHub <noreply@github.com>2018-05-25 22:46:40 +0200
commit8f354e52d09d1bf2fadfbbfc141d4f32241599a4 (patch)
treeca3fcaa2a72700b85221b2fe75bb7d46d5d424ce /browser
parentd5459982f0d1bab08b751dc140c4cbe047032617 (diff)
parent72bab89cf6c77c9ee95a977c306e6ce317b84835 (diff)
downloadUXP-8f354e52d09d1bf2fadfbbfc141d4f32241599a4.tar
UXP-8f354e52d09d1bf2fadfbbfc141d4f32241599a4.tar.gz
UXP-8f354e52d09d1bf2fadfbbfc141d4f32241599a4.tar.lz
UXP-8f354e52d09d1bf2fadfbbfc141d4f32241599a4.tar.xz
UXP-8f354e52d09d1bf2fadfbbfc141d4f32241599a4.zip
Merge pull request #386 from MoonchildProductions/revert-381-masterPM28.0.0a2_Unstable
Revert "Remove the Social API"
Diffstat (limited to 'browser')
-rw-r--r--browser/app/profile/firefox.js7
-rw-r--r--browser/base/content/aboutProviderDirectory.xhtml60
-rw-r--r--browser/base/content/aboutSocialError.xhtml111
-rw-r--r--browser/base/content/browser-context.inc20
-rw-r--r--browser/base/content/browser-sets.inc3
-rw-r--r--browser/base/content/browser-social.js503
-rw-r--r--browser/base/content/browser.css5
-rwxr-xr-xbrowser/base/content/browser.js10
-rw-r--r--browser/base/content/browser.xul26
-rw-r--r--browser/base/content/content.js31
-rw-r--r--browser/base/content/nsContextMenu.js47
-rw-r--r--browser/base/content/social-content.js172
-rw-r--r--browser/base/jar.mn4
-rw-r--r--browser/components/about/AboutRedirector.cpp10
-rw-r--r--browser/components/build/nsModule.cpp2
-rw-r--r--browser/components/customizableui/CustomizableWidgets.jsm41
-rw-r--r--browser/components/customizableui/content/panelUI.inc.xul2
-rw-r--r--browser/docs/UITelemetry.rst1
-rw-r--r--browser/locales/en-US/chrome/browser/browser.dtd25
-rw-r--r--browser/locales/en-US/chrome/browser/browser.properties23
-rw-r--r--browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties3
-rw-r--r--browser/modules/BrowserUITelemetry.jsm10
-rw-r--r--browser/modules/Social.jsm272
-rw-r--r--browser/modules/SocialService.jsm1097
-rw-r--r--browser/modules/moz.build2
-rw-r--r--browser/themes/linux/browser.css50
-rw-r--r--browser/themes/linux/customizableui/panelUI.css8
-rw-r--r--browser/themes/linux/jar.mn4
-rw-r--r--browser/themes/osx/browser.css88
-rw-r--r--browser/themes/osx/jar.mn4
-rw-r--r--browser/themes/shared/aboutProviderDirectory.css30
-rw-r--r--browser/themes/shared/browser.inc2
-rw-r--r--browser/themes/shared/customizableui/panelUI.inc.css4
-rw-r--r--browser/themes/shared/jar.inc.mn5
-rw-r--r--browser/themes/shared/menuPanel-small.svg16
-rw-r--r--browser/themes/shared/menupanel.inc.css10
-rw-r--r--browser/themes/shared/notification-icons.inc.css22
-rw-r--r--browser/themes/shared/social/gear_clicked.pngbin0 -> 1262 bytes
-rw-r--r--browser/themes/shared/social/gear_default.pngbin0 -> 1271 bytes
-rw-r--r--browser/themes/shared/social/social.inc.css23
-rw-r--r--browser/themes/shared/toolbarbuttons.inc.css8
-rw-r--r--browser/themes/windows/browser.css75
-rw-r--r--browser/themes/windows/customizableui/panelUI.css8
-rw-r--r--browser/themes/windows/jar.mn2
44 files changed, 2830 insertions, 16 deletions
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index ede62fd5e..e432c511d 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1118,6 +1118,13 @@ pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
// (This is intentionally on the high side; see bug 746055.)
pref("image.mem.max_decoded_image_kb", 256000);
+pref("social.sidebar.unload_timeout_ms", 10000);
+
+// Activation from inside of share panel is possible if activationPanelEnabled
+// is true. Pref'd off for release while usage testing is done through beta.
+pref("social.share.activationPanelEnabled", true);
+pref("social.shareDirectory", "https://activations.cdn.mozilla.net/sharePanel.html");
+
// Block insecure active content on https pages
pref("security.mixed_content.block_active_content", true);
diff --git a/browser/base/content/aboutProviderDirectory.xhtml b/browser/base/content/aboutProviderDirectory.xhtml
new file mode 100644
index 000000000..596ede4b3
--- /dev/null
+++ b/browser/base/content/aboutProviderDirectory.xhtml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+ %browserDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&social.directory.label;</title>
+ <link rel="stylesheet" type="text/css" media="all"
+ href="chrome://browser/skin/aboutProviderDirectory.css"/>
+ </head>
+
+ <body>
+ <div id="activation-link" hidden="true">
+ <div id="message-box">
+ <p>&social.directory.text;</p>
+ </div>
+ <div id="button-box">
+ <button onclick="openDirectory()">&social.directory.button;</button>
+ </div>
+ </div>
+ <div id="activation" hidden="true">
+ <p>&social.directory.introText;</p>
+ <div><iframe id="activation-frame"/></div>
+ <p><a class="link" onclick="openDirectory()">&social.directory.viewmore.text;</a></p>
+ </div>
+ </body>
+
+ <script type="text/javascript;version=1.8"><![CDATA[
+ const Cu = Components.utils;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+
+ function openDirectory() {
+ let url = Services.prefs.getCharPref("social.directories").split(',')[0];
+ window.open(url);
+ window.close();
+ }
+
+ if (Services.prefs.getBoolPref("social.share.activationPanelEnabled")) {
+ let url = Services.prefs.getCharPref("social.shareDirectory");
+ document.getElementById("activation-frame").setAttribute("src", url);
+ document.getElementById("activation").removeAttribute("hidden");
+ } else {
+ document.getElementById("activation-link").removeAttribute("hidden");
+ }
+ ]]></script>
+</html>
diff --git a/browser/base/content/aboutSocialError.xhtml b/browser/base/content/aboutSocialError.xhtml
new file mode 100644
index 000000000..94a4e3dbd
--- /dev/null
+++ b/browser/base/content/aboutSocialError.xhtml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % htmlDTD
+ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+ "DTD/xhtml1-strict.dtd">
+ %htmlDTD;
+ <!ENTITY % netErrorDTD SYSTEM "chrome://global/locale/netError.dtd">
+ %netErrorDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <title>&loadError.label;</title>
+ <link rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
+ <link rel="stylesheet" type="text/css" media="all" href="chrome://browser/skin/aboutSocialError.css"/>
+ <link rel="icon" type="image/png" id="favicon" href="chrome://global/skin/icons/warning-16.png"/>
+ </head>
+
+ <body>
+ <div id="errorPageContainer">
+
+ <!-- Error Title -->
+ <div id="errorTitle">
+ <p id="errorShortDescText" >foo</p>
+ </div>
+
+ <div id="button-box">
+ <button id="btnTryAgain" onclick="tryAgainButton()"/>
+ </div>
+ </div>
+ </body>
+
+ <script type="text/javascript;version=1.8"><![CDATA[
+ const Cu = Components.utils;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource:///modules/Social.jsm");
+
+ let config = {
+ tryAgainCallback: reloadProvider
+ }
+
+ function parseQueryString() {
+ let searchParams = new URLSearchParams(document.documentURI.split("?")[1]);
+ let mode = searchParams.get("mode");
+ config.origin = searchParams.get("origin");
+ let encodedURL = searchParams.get("url");
+ let url = decodeURIComponent(encodedURL);
+ // directory does not have origin set, in that case use the url origin for
+ // the error message.
+ if (!config.origin) {
+ let URI = Services.io.newURI(url, null, null);
+ config.origin =
+ Services.scriptSecurityManager.createCodebasePrincipal(URI, {}).origin;
+ }
+
+ switch (mode) {
+ case "compactInfo":
+ document.getElementById("btnTryAgain").style.display = 'none';
+ break;
+ case "tryAgainOnly":
+ //intentional fall-through
+ case "tryAgain":
+ config.tryAgainCallback = loadQueryURL;
+ config.queryURL = url;
+ break;
+ default:
+ break;
+ }
+ }
+
+ function setUpStrings() {
+ let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+ let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let productName = brandBundle.GetStringFromName("brandShortName");
+ let provider = Social._getProviderFromOrigin(config.origin);
+ let providerName = provider ? provider.name : config.origin;
+
+ // Sets up the error message
+ let msg = browserBundle.formatStringFromName("social.error.message", [productName, providerName], 2);
+ document.getElementById("errorShortDescText").textContent = msg;
+
+ // Sets up the buttons' labels and accesskeys
+ let btnTryAgain = document.getElementById("btnTryAgain");
+ btnTryAgain.textContent = browserBundle.GetStringFromName("social.error.tryAgain.label");
+ btnTryAgain.accessKey = browserBundle.GetStringFromName("social.error.tryAgain.accesskey");
+ }
+
+ function tryAgainButton() {
+ config.tryAgainCallback();
+ }
+
+ function loadQueryURL() {
+ window.location.href = config.queryURL;
+ }
+
+ function reloadProvider() {
+ let provider = Social._getProviderFromOrigin(config.origin);
+ provider.reload();
+ }
+
+ parseQueryString();
+ setUpStrings();
+ ]]></script>
+</html>
diff --git a/browser/base/content/browser-context.inc b/browser/base/content/browser-context.inc
index 9fa90b11c..3061cccdd 100644
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -85,6 +85,10 @@
label="&bookmarkThisLinkCmd.label;"
accesskey="&bookmarkThisLinkCmd.accesskey;"
oncommand="gContextMenu.bookmarkLink();"/>
+ <menuitem id="context-sharelink"
+ label="&shareLink.label;"
+ accesskey="&shareLink.accesskey;"
+ oncommand="gContextMenu.shareLink();"/>
<menuitem id="context-savelink"
label="&saveLinkCmd.label;"
accesskey="&saveLinkCmd.accesskey;"
@@ -208,6 +212,10 @@
label="&saveImageCmd.label;"
accesskey="&saveImageCmd.accesskey;"
oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-shareimage"
+ label="&shareImage.label;"
+ accesskey="&shareImage.accesskey;"
+ oncommand="gContextMenu.shareImage();"/>
<menuitem id="context-sendimage"
label="&emailImageCmd.label;"
accesskey="&emailImageCmd.accesskey;"
@@ -229,6 +237,10 @@
label="&saveVideoCmd.label;"
accesskey="&saveVideoCmd.accesskey;"
oncommand="gContextMenu.saveMedia();"/>
+ <menuitem id="context-sharevideo"
+ label="&shareVideo.label;"
+ accesskey="&shareVideo.accesskey;"
+ oncommand="gContextMenu.shareVideo();"/>
<menuitem id="context-saveaudio"
label="&saveAudioCmd.label;"
accesskey="&saveAudioCmd.accesskey;"
@@ -259,6 +271,10 @@
accesskey="&hidePluginCmd.accesskey;"
oncommand="gContextMenu.hidePlugin();"/>
<menuseparator id="context-sep-ctp"/>
+ <menuitem id="context-sharepage"
+ label="&sharePageCmd.label;"
+ accesskey="&sharePageCmd.accesskey;"
+ oncommand="SocialShare.sharePage();"/>
<menuitem id="context-savepage"
label="&savePageCmd.label;"
accesskey="&savePageCmd.accesskey2;"
@@ -318,6 +334,10 @@
<menupopup id="context-sendlinktodevice-popup"
onpopupshowing="gFxAccounts.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
</menu>
+ <menuitem id="context-shareselect"
+ label="&shareSelect.label;"
+ accesskey="&shareSelect.accesskey;"
+ oncommand="gContextMenu.shareSelect();"/>
<menuseparator id="frame-sep"/>
<menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
<menupopup>
diff --git a/browser/base/content/browser-sets.inc b/browser/base/content/browser-sets.inc
index 6ea057d93..d0c3d11cd 100644
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -105,6 +105,8 @@
oncommand="OpenBrowserWindow({private: true});" reserved="true"/>
<command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
<command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
+ <command id="Social:SharePage" oncommand="SocialShare.sharePage();"/>
+ <command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/>
</commandset>
<commandset id="placesCommands">
@@ -115,6 +117,7 @@
</commandset>
<broadcasterset id="mainBroadcasterSet">
+ <broadcaster id="Social:PageShareable" disabled="true"/>
<broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
oncommand="SidebarUI.toggle('viewBookmarksSidebar');"/>
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);
+ }
+};
+
+})();
diff --git a/browser/base/content/browser.css b/browser/base/content/browser.css
index ac5bf9e9b..f03f21c3f 100644
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -933,6 +933,11 @@ html|*#gcli-output-frame,
transition: none;
}
+panelview > .social-panel-frame {
+ width: auto;
+ height: auto;
+}
+
/* Translation */
notification[value="translation"] {
-moz-binding: url("chrome://browser/content/translation-infobar.xml#translationbar");
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 4b8ec864b..8679bca83 100755
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -44,6 +44,7 @@ Cu.import("resource://gre/modules/NotificationDB.jsm");
["ShortcutUtils", "resource://gre/modules/ShortcutUtils.jsm"],
["SimpleServiceDiscovery", "resource://gre/modules/SimpleServiceDiscovery.jsm"],
["SitePermissions", "resource:///modules/SitePermissions.jsm"],
+ ["Social", "resource:///modules/Social.jsm"],
["TabCrashHandler", "resource:///modules/ContentCrashHandlers.jsm"],
["Task", "resource://gre/modules/Task.jsm"],
["TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm"],
@@ -1404,6 +1405,8 @@ var gBrowserInit = {
// Enable the Restore Last Session command if needed
RestoreLastSessionObserver.init();
+ SocialUI.init();
+
// Start monitoring slow add-ons
AddonWatcher.init();
@@ -1534,6 +1537,7 @@ var gBrowserInit = {
gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
ctrlTab.uninit();
+ SocialUI.uninit();
gBrowserThumbnails.uninit();
FullZoom.destroy();
@@ -4293,7 +4297,9 @@ var XULBrowserWindow = {
// Called before links are navigated to to allow us to retarget them if needed.
onBeforeLinkTraversal: function(originalTarget, linkURI, linkNode, isAppTab) {
- return BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
+ let target = BrowserUtils.onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab);
+ SocialUI.closeSocialPanelForLinkTraversal(target, linkNode);
+ return target;
},
// Check whether this URI should load in the current process
@@ -4474,6 +4480,8 @@ var XULBrowserWindow = {
gIdentityHandler.onLocationChange();
+ SocialUI.updateState();
+
UITour.onLocationChange(location);
gTabletModePageCounter.inc();
diff --git a/browser/base/content/browser.xul b/browser/base/content/browser.xul
index 4f1b48349..485471ee3 100644
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -268,6 +268,27 @@
<box id="UITourHighlight"></box>
</panel>
+ <panel id="social-share-panel"
+ class="social-panel"
+ type="arrow"
+ orient="vertical"
+ onpopupshowing="SocialShare.onShowing()"
+ onpopuphidden="SocialShare.onHidden()"
+ hidden="true">
+ <hbox class="social-share-toolbar">
+ <toolbarbutton id="manage-share-providers" class="share-provider-button"
+ tooltiptext="&social.addons.label;"
+ oncommand="BrowserOpenAddonsMgr('addons://list/service');
+ this.parentNode.parentNode.hidePopup();"/>
+ <arrowscrollbox id="social-share-provider-buttons" orient="horizontal" flex="1" pack="end">
+ <toolbarbutton id="add-share-provider" class="share-provider-button" type="radio"
+ group="share-providers" tooltiptext="&findShareServices.label;"
+ oncommand="SocialShare.showDirectory()"/>
+ </arrowscrollbox>
+ </hbox>
+ <hbox id="share-container" flex="1"/>
+ </panel>
+
<menupopup id="toolbar-context-menu"
onpopupshowing="onViewToolbarsPopupShowing(event, document.getElementById('viewToolbarsMenuSeparator'));">
<menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
@@ -405,6 +426,11 @@
#endif
</tooltip>
+ <tooltip id="share-button-tooltip" onpopupshowing="SocialShare.createTooltip(event);">
+ <label class="tooltip-label"/>
+ <label class="tooltip-label"/>
+ </tooltip>
+
#include popup-notifications.inc
#include ../../components/customizableui/content/panelUI.inc.xul
diff --git a/browser/base/content/content.js b/browser/base/content/content.js
index 5758cb023..496e0d111 100644
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -698,6 +698,37 @@ var PageMetadataMessenger = {
}
PageMetadataMessenger.init();
+addEventListener("ActivateSocialFeature", function (aEvent) {
+ let document = content.document;
+ let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ if (!dwu.isHandlingUserInput) {
+ Cu.reportError("attempt to activate provider without user input from " + document.nodePrincipal.origin);
+ return;
+ }
+
+ let node = aEvent.target;
+ let ownerDocument = node.ownerDocument;
+ let data = node.getAttribute("data-service");
+ if (data) {
+ try {
+ data = JSON.parse(data);
+ } catch (e) {
+ Cu.reportError("Social Service manifest parse error: " + e);
+ return;
+ }
+ } else {
+ Cu.reportError("Social Service manifest not available");
+ return;
+ }
+
+ sendAsyncMessage("Social:Activation", {
+ url: ownerDocument.location.href,
+ origin: ownerDocument.nodePrincipal.origin,
+ manifest: data
+ });
+}, true, true);
+
addMessageListener("ContextMenu:SaveVideoFrameAsImage", (message) => {
let video = message.objects.target;
let canvas = content.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js
index 097caf367..955184f64 100644
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -174,15 +174,15 @@ nsContextMenu.prototype = {
initNavigationItems: function CM_initNavigationItems() {
var shouldShow = !(this.isContentSelected || this.onLink || this.onImage ||
this.onCanvas || this.onVideo || this.onAudio ||
- this.onTextInput);
+ this.onTextInput || this.onSocial);
this.showItem("context-navigation", shouldShow);
this.showItem("context-sep-navigation", shouldShow);
let stopped = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true";
let stopReloadItem = "";
- if (shouldShow) {
- stopReloadItem = (stopped) ? "reload" : "stop";
+ if (shouldShow || this.onSocial) {
+ stopReloadItem = (stopped || this.onSocial) ? "reload" : "stop";
}
this.showItem("context-reload", stopReloadItem == "reload");
@@ -249,7 +249,7 @@ nsContextMenu.prototype = {
this.onImage || this.onCanvas ||
this.onVideo || this.onAudio ||
this.onLink || this.onTextInput);
- var showInspect = gPrefService.getBoolPref("devtools.inspector.enabled");
+ var showInspect = !this.onSocial && gPrefService.getBoolPref("devtools.inspector.enabled");
this.showItem("context-viewsource", shouldShow);
this.showItem("context-viewinfo", shouldShow);
this.showItem("inspect-separator", showInspect);
@@ -306,11 +306,12 @@ nsContextMenu.prototype = {
let bookmarkPage = document.getElementById("context-bookmarkpage");
this.showItem(bookmarkPage,
!(this.isContentSelected || this.onTextInput || this.onLink ||
- this.onImage || this.onVideo || this.onAudio || this.onCanvas));
+ this.onImage || this.onVideo || this.onAudio || this.onSocial ||
+ this.onCanvas));
bookmarkPage.setAttribute("tooltiptext", bookmarkPage.getAttribute("buttontooltiptext"));
- this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink) ||
- this.onPlainTextLink);
+ this.showItem("context-bookmarklink", (this.onLink && !this.onMailtoLink &&
+ !this.onSocial) || this.onPlainTextLink);
this.showItem("context-keywordfield",
this.onTextInput && this.onKeywordField);
this.showItem("frame", this.inFrame);
@@ -348,6 +349,19 @@ nsContextMenu.prototype = {
this.onTextInput && !this.onNumeric && top.gBidiUI);
this.showItem("context-bidi-page-direction-toggle",
!this.onTextInput && top.gBidiUI);
+
+ // SocialShare
+ let shareButton = SocialShare.shareButton;
+ let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
+ let pageShare = shareEnabled && !(this.isContentSelected ||
+ this.onTextInput || this.onLink || this.onImage ||
+ this.onVideo || this.onAudio || this.onCanvas);
+ this.showItem("context-sharepage", pageShare);
+ this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
+ this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);
+ this.showItem("context-shareimage", shareEnabled && this.onImage);
+ this.showItem("context-sharevideo", shareEnabled && this.onVideo);
+ this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL || this.mediaURL.startsWith("blob:"));
},
initSpellingItems: function() {
@@ -667,6 +681,7 @@ nsContextMenu.prototype = {
.getInterface(Ci.nsIDOMWindowUtils)
.outerWindowID;
}
+ this.onSocial = !!this.browser.getAttribute("origin");
// Check if we are in a synthetic document (stand alone image, video, etc.).
this.inSyntheticDoc = ownerDoc.mozSyntheticDocument;
@@ -1711,6 +1726,22 @@ nsContextMenu.prototype = {
mm.sendAsyncMessage("ContextMenu:BookmarkFrame", null, { target: this.target });
},
+ shareLink: function CM_shareLink() {
+ SocialShare.sharePage(null, { url: this.linkURI.spec }, this.target);
+ },
+
+ shareImage: function CM_shareImage() {
+ SocialShare.sharePage(null, { url: this.imageURL, previews: [ this.mediaURL ] }, this.target);
+ },
+
+ shareVideo: function CM_shareVideo() {
+ SocialShare.sharePage(null, { url: this.mediaURL, source: this.mediaURL }, this.target);
+ },
+
+ shareSelect: function CM_shareSelect() {
+ SocialShare.sharePage(null, { url: this.browser.currentURI.spec, text: this.textSelected }, this.target);
+ },
+
savePageAs: function CM_savePageAs() {
saveBrowser(this.browser);
},
@@ -1825,7 +1856,7 @@ nsContextMenu.prototype = {
_getTelemetryPageContextInfo: function() {
let rv = [];
for (let k of ["isContentSelected", "onLink", "onImage", "onCanvas", "onVideo", "onAudio",
- "onTextInput"]) {
+ "onTextInput", "onSocial"]) {
if (this[k]) {
rv.push(k.replace(/^(?:is|on)(.)/, (match, firstLetter) => firstLetter.toLowerCase()));
}
diff --git a/browser/base/content/social-content.js b/browser/base/content/social-content.js
new file mode 100644
index 000000000..b5fa6a5c4
--- /dev/null
+++ b/browser/base/content/social-content.js
@@ -0,0 +1,172 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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/. */
+
+/* This content script is intended for use by iframes in the share panel. */
+
+var {interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// social frames are always treated as app tabs
+docShell.isAppTab = true;
+
+addEventListener("DOMContentLoaded", function(event) {
+ if (event.target != content.document)
+ return;
+ // Some share panels (e.g. twitter and facebook) check content.opener, and if
+ // it doesn't exist they act like they are in a browser tab. We want them to
+ // act like they are in a dialog (which is the typical case).
+ if (content && !content.opener) {
+ content.opener = content;
+ }
+ hookWindowClose();
+ disableDialogs();
+});
+
+addMessageListener("Social:OpenGraphData", (message) => {
+ let ev = new content.CustomEvent("OpenGraphData", { detail: JSON.stringify(message.data) });
+ content.dispatchEvent(ev);
+});
+
+addMessageListener("Social:ClearFrame", () => {
+ docShell.createAboutBlankContentViewer(null);
+});
+
+addEventListener("DOMWindowClose", (evt) => {
+ // 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();
+
+ // Tells the SocialShare class to close the panel
+ sendAsyncMessage("Social:DOMWindowClose");
+});
+
+function hookWindowClose() {
+ // Allow scripts to close the "window". Because we are in a panel and not
+ // in a full dialog, the DOMWindowClose listener above will only receive the
+ // event if we do this.
+ let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ dwu.allowScriptsToClose();
+}
+
+function disableDialogs() {
+ let windowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIDOMWindowUtils);
+ windowUtils.disableDialogs();
+}
+
+// Error handling class used to listen for network errors in the social frames
+// and replace them with a social-specific error page
+const SocialErrorListener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
+ Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference,
+ Ci.nsISupports]),
+
+ defaultTemplate: "about:socialerror?mode=tryAgainOnly&url=%{url}&origin=%{origin}",
+ urlTemplate: null,
+
+ init() {
+ addMessageListener("Social:SetErrorURL", this);
+ let webProgress = docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebProgress);
+ webProgress.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_STATE_REQUEST |
+ Ci.nsIWebProgress.NOTIFY_LOCATION);
+ },
+
+ receiveMessage(message) {
+ switch (message.name) {
+ case "Social:SetErrorURL":
+ // Either a url or null to reset to default template.
+ this.urlTemplate = message.data.template;
+ break;
+ }
+ },
+
+ setErrorPage() {
+ // if this is about:providerdirectory, use the directory iframe
+ let frame = docShell.chromeEventHandler;
+ let origin = frame.getAttribute("origin");
+ let src = frame.getAttribute("src");
+ if (src == "about:providerdirectory") {
+ frame = content.document.getElementById("activation-frame");
+ src = frame.getAttribute("src");
+ }
+
+ let url = this.urlTemplate || this.defaultTemplate;
+ url = url.replace("%{url}", encodeURIComponent(src));
+ url = url.replace("%{origin}", encodeURIComponent(origin));
+ if (frame != docShell.chromeEventHandler) {
+ // Unable to access frame.docShell here. This is our own frame and doesn't
+ // provide reload, so we'll just set the src.
+ frame.setAttribute("src", url);
+ } else {
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(url, null, null, null, null);
+ }
+ sendAsyncMessage("Social:ErrorPageNotify", {
+ origin: origin,
+ url: src
+ });
+ },
+
+ onStateChange(aWebProgress, aRequest, aState, aStatus) {
+ let failure = false;
+ if ((aState & Ci.nsIWebProgressListener.STATE_IS_REQUEST))
+ return;
+ if ((aState & Ci.nsIWebProgressListener.STATE_STOP)) {
+ if (aRequest instanceof Ci.nsIHttpChannel) {
+ try {
+ // Change the frame to an error page on 4xx (client errors)
+ // and 5xx (server errors). responseStatus throws if it is not set.
+ failure = aRequest.responseStatus >= 400 &&
+ aRequest.responseStatus < 600;
+ } catch (e) {
+ failure = aStatus != Components.results.NS_OK;
+ }
+ }
+ }
+
+ // Calling cancel() will raise some OnStateChange notifications by itself,
+ // so avoid doing that more than once
+ if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
+ // if tp is enabled and we get a failure, ignore failures (ie. STATE_STOP)
+ // on child resources since they *may* have been blocked. We don't have an
+ // easy way to know if a particular url is blocked by TP, only that
+ // something was.
+ if (docShell.hasTrackingContentBlocked) {
+ let frame = docShell.chromeEventHandler;
+ let src = frame.getAttribute("src");
+ if (aRequest && aRequest.name != src) {
+ Cu.reportError("SocialErrorListener ignoring blocked content error for " + aRequest.name);
+ return;
+ }
+ }
+
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ this.setErrorPage();
+ }
+ },
+
+ onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
+ if (aRequest && aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
+ aRequest.cancel(Components.results.NS_BINDING_ABORTED);
+ this.setErrorPage();
+ }
+ },
+
+ onProgressChange() {},
+ onStatusChange() {},
+ onSecurityChange() {},
+};
+
+SocialErrorListener.init();
diff --git a/browser/base/jar.mn b/browser/base/jar.mn
index 5ec92d79a..03854f75d 100644
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -61,6 +61,8 @@ browser.jar:
content/browser/aboutRobots-icon.png (content/aboutRobots-icon.png)
content/browser/aboutRobots-widget-left.png (content/aboutRobots-widget-left.png)
+ content/browser/aboutSocialError.xhtml (content/aboutSocialError.xhtml)
+ content/browser/aboutProviderDirectory.xhtml (content/aboutProviderDirectory.xhtml)
content/browser/aboutTabCrashed.css (content/aboutTabCrashed.css)
content/browser/aboutTabCrashed.js (content/aboutTabCrashed.js)
content/browser/aboutTabCrashed.xhtml (content/aboutTabCrashed.xhtml)
@@ -86,6 +88,7 @@ browser.jar:
content/browser/browser-safebrowsing.js (content/browser-safebrowsing.js)
#endif
content/browser/browser-sidebar.js (content/browser-sidebar.js)
+ content/browser/browser-social.js (content/browser-social.js)
* content/browser/browser-syncui.js (content/browser-syncui.js)
* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
#ifdef CAN_DRAW_IN_TITLEBAR
@@ -97,6 +100,7 @@ browser.jar:
content/browser/browser-trackingprotection.js (content/browser-trackingprotection.js)
* content/browser/tab-content.js (content/tab-content.js)
content/browser/content.js (content/content.js)
+ content/browser/social-content.js (content/social-content.js)
content/browser/defaultthemes/1.footer.jpg (content/defaultthemes/1.footer.jpg)
content/browser/defaultthemes/1.header.jpg (content/defaultthemes/1.header.jpg)
content/browser/defaultthemes/1.icon.jpg (content/defaultthemes/1.icon.jpg)
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp
index b77949ea7..5e8df6ab2 100644
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -57,6 +57,16 @@ static RedirEntry kRedirMap[] = {
nsIAboutModule::HIDE_FROM_ABOUTABOUT
},
{
+ "socialerror", "chrome://browser/content/aboutSocialError.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT |
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT
+ },
+ {
+ "providerdirectory", "chrome://browser/content/aboutProviderDirectory.xhtml",
+ nsIAboutModule::ALLOW_SCRIPT |
+ nsIAboutModule::HIDE_FROM_ABOUTABOUT
+ },
+ {
"tabcrashed", "chrome://browser/content/aboutTabCrashed.xhtml",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
diff --git a/browser/components/build/nsModule.cpp b/browser/components/build/nsModule.cpp
index 1baccd710..967da3ebc 100644
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -90,6 +90,8 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "blocked", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#endif
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "certerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+ { NS_ABOUT_MODULE_CONTRACTID_PREFIX "socialerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+ { NS_ABOUT_MODULE_CONTRACTID_PREFIX "providerdirectory", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "tabcrashed", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "feeds", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "privatebrowsing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
diff --git a/browser/components/customizableui/CustomizableWidgets.jsm b/browser/components/customizableui/CustomizableWidgets.jsm
index 3e00d385f..3e83b081c 100644
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -557,6 +557,47 @@ const CustomizableWidgets = [
fillSubviewFromMenuItems([...menu.children], sidebarItems);
}
}, {
+ id: "social-share-button",
+ // custom build our button so we can attach to the share command
+ type: "custom",
+ onBuild: function(aDocument) {
+ let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
+ node.setAttribute("id", this.id);
+ node.classList.add("toolbarbutton-1");
+ node.classList.add("chromeclass-toolbar-additional");
+ node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
+ node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
+ node.setAttribute("removable", "true");
+ node.setAttribute("observes", "Social:PageShareable");
+ node.setAttribute("command", "Social:SharePage");
+
+ let listener = {
+ onWidgetAdded: (aWidgetId) => {
+ if (aWidgetId != this.id)
+ return;
+
+ Services.obs.notifyObservers(null, "social:" + this.id + "-added", null);
+ },
+
+ onWidgetRemoved: aWidgetId => {
+ if (aWidgetId != this.id)
+ return;
+
+ Services.obs.notifyObservers(null, "social:" + this.id + "-removed", null);
+ },
+
+ onWidgetInstanceRemoved: (aWidgetId, aDoc) => {
+ if (aWidgetId != this.id || aDoc != aDocument)
+ return;
+
+ CustomizableUI.removeListener(listener);
+ }
+ };
+ CustomizableUI.addListener(listener);
+
+ return node;
+ }
+ }, {
id: "add-ons-button",
shortcutId: "key_openAddons",
tooltiptext: "add-ons-button.tooltiptext3",
diff --git a/browser/components/customizableui/content/panelUI.inc.xul b/browser/components/customizableui/content/panelUI.inc.xul
index 077d9c014..1b8fc0236 100644
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -240,6 +240,8 @@
onclick="PanelUI.hide();"/>
</panelview>
+ <panelview id="PanelUI-socialapi" flex="1"/>
+
<panelview id="PanelUI-feeds" flex="1" oncommand="FeedHandler.subscribeToFeed(null, event);">
<label value="&feedsMenu2.label;" class="panel-subview-header"/>
</panelview>
diff --git a/browser/docs/UITelemetry.rst b/browser/docs/UITelemetry.rst
index 1a9213359..0b3302f8f 100644
--- a/browser/docs/UITelemetry.rst
+++ b/browser/docs/UITelemetry.rst
@@ -128,6 +128,7 @@ divide the following different context menu situations:
- ``canvas`` if the user opened the context menu on a canvas (that isn't a link);
- ``media`` if the user opened the context menu on an HTML video or audio element;
- ``input`` if the user opened the context menu on a text input element;
+- ``social`` if the user opened the context menu inside a social frame;
- ``other`` for all other openings of the content menu;
Each of these objects (if they exist) then gets a "withcustom" and/or a "withoutcustom" property
diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd
index 1045977e8..6de17b64f 100644
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -163,7 +163,22 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY bookmarkThisPageCmd.label "Bookmark This Page">
<!ENTITY editThisBookmarkCmd.label "Edit This Bookmark">
<!ENTITY bookmarkThisPageCmd.commandkey "d">
-
+<!-- LOCALIZATION NOTE (findShareServices.label):
+ - Use the unicode ellipsis char, \u2026,
+ - or use "..." if \u2026 doesn't suit traditions in your locale. -->
+<!ENTITY findShareServices.label "Find more Share services…">
+<!ENTITY sharePageCmd.label "Share This Page">
+<!ENTITY sharePageCmd.commandkey "S">
+<!ENTITY sharePageCmd.accesskey "s">
+<!-- LOCALIZATION NOTE (shareLink.accesskey): must be different than the following share access keys -->
+<!ENTITY shareLink.label "Share This Link">
+<!ENTITY shareLink.accesskey "h">
+<!ENTITY shareImage.label "Share This Image">
+<!ENTITY shareImage.accesskey "r">
+<!ENTITY shareSelect.label "Share Selection">
+<!ENTITY shareSelect.accesskey "r">
+<!ENTITY shareVideo.label "Share This Video">
+<!ENTITY shareVideo.accesskey "r">
<!ENTITY feedsMenu2.label "Subscribe to This Page">
<!ENTITY subscribeToPageMenupopup.label "Subscribe to This Page">
<!ENTITY subscribeToPageMenuitem.label "Subscribe to This Page…">
@@ -765,6 +780,14 @@ you can use these alternative items. Otherwise, their values should be empty. -
<!ENTITY syncReAuthItem.accesskey "R">
<!ENTITY syncToolbarButton.label "Sync">
+<!ENTITY social.addons.label "Manage Services…">
+
+<!ENTITY social.directory.label "Activations Directory">
+<!ENTITY social.directory.text "You can activate Share services from the directory.">
+<!ENTITY social.directory.button "Take me there!">
+<!ENTITY social.directory.introText "Click on a service to add it to &brandShortName;.">
+<!ENTITY social.directory.viewmore.text "View More">
+
<!ENTITY customizeMode.menuAndToolbars.header2 "Additional Tools and Features">
<!ENTITY customizeMode.menuAndToolbars.empty "Want more tools?">
<!ENTITY customizeMode.menuAndToolbars.emptyLink "Choose from thousands of add-ons">
diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties
index 46b8aabc7..f7f3e9339 100644
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -453,6 +453,29 @@ processHang.button_debug.accessKey = D
# LOCALIZATION NOTE (fullscreenButton.tooltip): %S is the keyboard shortcut for full screen
fullscreenButton.tooltip=Display the window in full screen (%S)
+service.toolbarbutton.label=Services
+service.toolbarbutton.tooltiptext=Services
+
+# LOCALIZATION NOTE (social.install.description): %1$S is the hostname of the social provider, %2$S is brandShortName (e.g. Firefox)
+service.install.description=Would you like to enable services from %1$S to display in your %2$S toolbar and sidebar?
+service.install.ok.label=Enable Services
+service.install.ok.accesskey=E
+
+# LOCALIZATION NOTE (social.markpageMenu.label): %S is the name of the social provider
+social.markpageMenu.label=Save Page to %S
+# LOCALIZATION NOTE (social.marklinkMenu.label): %S is the name of the social provider
+social.marklinkMenu.label=Save Link to %S
+
+# LOCALIZATION NOTE (social.error.message): %1$S is brandShortName (e.g. Firefox), %2$S is the name of the social provider
+social.error.message=%1$S is unable to connect with %2$S right now.
+social.error.tryAgain.label=Try Again
+social.error.tryAgain.accesskey=T
+social.error.closeSidebar.label=Close This Sidebar
+social.error.closeSidebar.accesskey=C
+
+# LOCALIZATION NOTE: %1$S is the label for the toolbar button, %2$S is the associated badge numbering that the social provider may provide.
+social.aria.toolbarButtonBadgeText=%1$S (%2$S)
+
# LOCALIZATION NOTE (getUserMedia.shareCamera.message, getUserMedia.shareMicrophone.message,
# getUserMedia.shareScreen.message, getUserMedia.shareCameraAndMicrophone.message,
# getUserMedia.shareScreenAndMicrophone.message, getUserMedia.shareCameraAndAudioCapture.message,
diff --git a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
index 4574c6a81..a68f59fe3 100644
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
+++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
@@ -95,6 +95,9 @@ quit-button.tooltiptext.linux2 = Quit %1$S (%2$S)
# %2$S is the keyboard shortcut
quit-button.tooltiptext.mac = Quit %1$S (%2$S)
+social-share-button.label = Share This Page
+social-share-button.tooltiptext = Share this page
+
panic-button.label = Forget
panic-button.tooltiptext = Forget about some browsing history
diff --git a/browser/modules/BrowserUITelemetry.jsm b/browser/modules/BrowserUITelemetry.jsm
index a6a5789f4..392462b45 100644
--- a/browser/modules/BrowserUITelemetry.jsm
+++ b/browser/modules/BrowserUITelemetry.jsm
@@ -694,7 +694,7 @@ this.BrowserUITelemetry = {
"spell-undo-add-to-dictionary", "openlinkincurrent", "openlinkintab",
"openlink",
// "openlinkprivate" intentionally omitted for privacy reasons. See bug 1176391.
- "bookmarklink", "savelink",
+ "bookmarklink", "sharelink", "savelink",
"marklinkMenu", "copyemail", "copylink", "media-play", "media-pause",
"media-mute", "media-unmute", "media-playbackrate",
"media-playbackrate-050x", "media-playbackrate-100x",
@@ -702,12 +702,12 @@ this.BrowserUITelemetry = {
"media-showcontrols", "media-hidecontrols",
"video-fullscreen", "leave-dom-fullscreen",
"reloadimage", "viewimage", "viewvideo", "copyimage-contents", "copyimage",
- "copyvideourl", "copyaudiourl", "saveimage", "sendimage",
+ "copyvideourl", "copyaudiourl", "saveimage", "shareimage", "sendimage",
"setDesktopBackground", "viewimageinfo", "viewimagedesc", "savevideo",
- "saveaudio", "video-saveimage", "sendvideo", "sendaudio",
- "ctp-play", "ctp-hide", "savepage", "pocket", "markpageMenu",
+ "sharevideo", "saveaudio", "video-saveimage", "sendvideo", "sendaudio",
+ "ctp-play", "ctp-hide", "sharepage", "savepage", "pocket", "markpageMenu",
"viewbgimage", "undo", "cut", "copy", "paste", "delete", "selectall",
- "keywordfield", "searchselect", "frame", "showonlythisframe",
+ "keywordfield", "searchselect", "shareselect", "frame", "showonlythisframe",
"openframeintab", "openframe", "reloadframe", "bookmarkframe", "saveframe",
"printframe", "viewframesource", "viewframeinfo",
"viewpartialsource-selection", "viewpartialsource-mathml",
diff --git a/browser/modules/Social.jsm b/browser/modules/Social.jsm
new file mode 100644
index 000000000..1569e0122
--- /dev/null
+++ b/browser/modules/Social.jsm
@@ -0,0 +1,272 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["Social", "OpenGraphBuilder",
+ "DynamicResizeWatcher", "sizeSocialPanelToContent"];
+
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cu = Components.utils;
+
+// The minimum sizes for the auto-resize panel code, minimum size necessary to
+// properly show the error page in the panel.
+const PANEL_MIN_HEIGHT = 190;
+const PANEL_MIN_WIDTH = 330;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
+ "resource:///modules/CustomizableUI.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
+ "resource:///modules/SocialService.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata",
+ "resource://gre/modules/PageMetadata.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+
+
+this.Social = {
+ initialized: false,
+ lastEventReceived: 0,
+ providers: [],
+ _disabledForSafeMode: false,
+
+ init: function Social_init() {
+ this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled;
+ let deferred = Promise.defer();
+
+ if (this.initialized) {
+ deferred.resolve(true);
+ return deferred.promise;
+ }
+ this.initialized = true;
+ // if SocialService.hasEnabledProviders, retreive the providers so the
+ // front-end can generate UI
+ if (SocialService.hasEnabledProviders) {
+ // Retrieve the current set of providers, and set the current provider.
+ SocialService.getOrderedProviderList(function (providers) {
+ Social._updateProviderCache(providers);
+ Social._updateEnabledState(SocialService.enabled);
+ deferred.resolve(false);
+ });
+ } else {
+ deferred.resolve(false);
+ }
+
+ // Register an observer for changes to the provider list
+ SocialService.registerProviderListener(function providerListener(topic, origin, providers) {
+ // An engine change caused by adding/removing a provider should notify.
+ // any providers we receive are enabled in the AddonsManager
+ if (topic == "provider-installed" || topic == "provider-uninstalled") {
+ // installed/uninstalled do not send the providers param
+ Services.obs.notifyObservers(null, "social:" + topic, origin);
+ return;
+ }
+ if (topic == "provider-enabled") {
+ Social._updateProviderCache(providers);
+ Social._updateEnabledState(true);
+ Services.obs.notifyObservers(null, "social:" + topic, origin);
+ return;
+ }
+ if (topic == "provider-disabled") {
+ // a provider was removed from the list of providers, update states
+ Social._updateProviderCache(providers);
+ Social._updateEnabledState(providers.length > 0);
+ Services.obs.notifyObservers(null, "social:" + topic, origin);
+ return;
+ }
+ if (topic == "provider-update") {
+ // a provider has self-updated its manifest, we need to update our cache
+ // and reload the provider.
+ Social._updateProviderCache(providers);
+ let provider = Social._getProviderFromOrigin(origin);
+ provider.reload();
+ }
+ });
+ return deferred.promise;
+ },
+
+ _updateEnabledState: function(enable) {
+ for (let p of Social.providers) {
+ p.enabled = enable;
+ }
+ },
+
+ // Called to update our cache of providers and set the current provider
+ _updateProviderCache: function (providers) {
+ this.providers = providers;
+ Services.obs.notifyObservers(null, "social:providers-changed", null);
+ },
+
+ get enabled() {
+ return !this._disabledForSafeMode && this.providers.length > 0;
+ },
+
+ _getProviderFromOrigin: function (origin) {
+ for (let p of this.providers) {
+ if (p.origin == origin) {
+ return p;
+ }
+ }
+ return null;
+ },
+
+ getManifestByOrigin: function(origin) {
+ return SocialService.getManifestByOrigin(origin);
+ },
+
+ installProvider: function(data, installCallback, options={}) {
+ SocialService.installProvider(data, installCallback, options);
+ },
+
+ uninstallProvider: function(origin, aCallback) {
+ SocialService.uninstallProvider(origin, aCallback);
+ },
+
+ // Activation functionality
+ activateFromOrigin: function (origin, callback) {
+ // It's OK if the provider has already been activated - we still get called
+ // back with it.
+ SocialService.enableProvider(origin, callback);
+ }
+};
+
+function sizeSocialPanelToContent(panel, iframe, requestedSize) {
+ let doc = iframe.contentDocument;
+ if (!doc || !doc.body) {
+ return;
+ }
+ // We need an element to use for sizing our panel. See if the body defines
+ // an id for that element, otherwise use the body itself.
+ let body = doc.body;
+ let docEl = doc.documentElement;
+ let bodyId = body.getAttribute("contentid");
+ if (bodyId) {
+ body = doc.getElementById(bodyId) || doc.body;
+ }
+ // offsetHeight/Width don't include margins, so account for that.
+ let cs = doc.defaultView.getComputedStyle(body);
+ let width = Math.max(PANEL_MIN_WIDTH, docEl.offsetWidth);
+ let height = Math.max(PANEL_MIN_HEIGHT, docEl.offsetHeight);
+ // if the panel is preloaded prior to being shown, cs will be null. in that
+ // case use the minimum size for the panel until it is shown.
+ if (cs) {
+ let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom);
+ height = Math.max(computedHeight, height);
+ let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight);
+ width = Math.max(computedWidth, width);
+ }
+
+ // if our scrollHeight is still larger than the iframe, the css calculations
+ // above did not work for this site, increase the height. This can happen if
+ // the site increases its height for additional UI.
+ if (docEl.scrollHeight > iframe.boxObject.height)
+ height = docEl.scrollHeight;
+
+ // if a size was defined in the manifest use it as a minimum
+ if (requestedSize) {
+ if (requestedSize.height)
+ height = Math.max(height, requestedSize.height);
+ if (requestedSize.width)
+ width = Math.max(width, requestedSize.width);
+ }
+
+ // add the extra space used by the panel (toolbar, borders, etc) if the iframe
+ // has been loaded
+ if (iframe.boxObject.width && iframe.boxObject.height) {
+ // add extra space the panel needs if any
+ width += panel.boxObject.width - iframe.boxObject.width;
+ height += panel.boxObject.height - iframe.boxObject.height;
+ }
+
+ // using panel.sizeTo will ignore css transitions, set size via style
+ if (Math.abs(panel.boxObject.width - width) >= 2)
+ panel.style.width = width + "px";
+ if (Math.abs(panel.boxObject.height - height) >= 2)
+ panel.style.height = height + "px";
+}
+
+function DynamicResizeWatcher() {
+ this._mutationObserver = null;
+}
+
+DynamicResizeWatcher.prototype = {
+ start: function DynamicResizeWatcher_start(panel, iframe, requestedSize) {
+ this.stop(); // just in case...
+ let doc = iframe.contentDocument;
+ this._mutationObserver = new iframe.contentWindow.MutationObserver((mutations) => {
+ sizeSocialPanelToContent(panel, iframe, requestedSize);
+ });
+ // Observe anything that causes the size to change.
+ let config = {attributes: true, characterData: true, childList: true, subtree: true};
+ this._mutationObserver.observe(doc, config);
+ // and since this may be setup after the load event has fired we do an
+ // initial resize now.
+ sizeSocialPanelToContent(panel, iframe, requestedSize);
+ },
+ stop: function DynamicResizeWatcher_stop() {
+ if (this._mutationObserver) {
+ try {
+ this._mutationObserver.disconnect();
+ } catch (ex) {
+ // may get "TypeError: can't access dead object" which seems strange,
+ // but doesn't seem to indicate a real problem, so ignore it...
+ }
+ this._mutationObserver = null;
+ }
+ }
+}
+
+
+this.OpenGraphBuilder = {
+ generateEndpointURL: function(URLTemplate, pageData) {
+ // support for existing oexchange style endpoints by supporting their
+ // querystring arguments. parse the query string template and do
+ // replacements where necessary the query names may be different than ours,
+ // so we could see u=%{url} or url=%{url}
+ let [endpointURL, queryString] = URLTemplate.split("?");
+ let query = {};
+ if (queryString) {
+ queryString.split('&').forEach(function (val) {
+ let [name, value] = val.split('=');
+ let p = /%\{(.+)\}/.exec(value);
+ if (!p) {
+ // preserve non-template query vars
+ query[name] = value;
+ } else if (pageData[p[1]]) {
+ if (p[1] == "previews")
+ query[name] = pageData[p[1]][0];
+ else
+ query[name] = pageData[p[1]];
+ } else if (p[1] == "body") {
+ // build a body for emailers
+ let body = "";
+ if (pageData.title)
+ body += pageData.title + "\n\n";
+ if (pageData.description)
+ body += pageData.description + "\n\n";
+ if (pageData.text)
+ body += pageData.text + "\n\n";
+ body += pageData.url;
+ query["body"] = body;
+ }
+ });
+ // if the url template doesn't have title and no text was provided, add the title as the text.
+ if (!query.text && !query.title && pageData.title) {
+ query.text = pageData.title;
+ }
+ }
+ var str = [];
+ for (let p in query)
+ str.push(p + "=" + encodeURIComponent(query[p]));
+ if (str.length)
+ endpointURL = endpointURL + "?" + str.join("&");
+ return endpointURL;
+ },
+};
diff --git a/browser/modules/SocialService.jsm b/browser/modules/SocialService.jsm
new file mode 100644
index 000000000..95f5e0259
--- /dev/null
+++ b/browser/modules/SocialService.jsm
@@ -0,0 +1,1097 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["SocialService"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
+const ADDON_TYPE_SERVICE = "service";
+const ID_SUFFIX = "@services.mozilla.org";
+const STRING_TYPE_NAME = "type.%ID%.name";
+
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "etld",
+ "@mozilla.org/network/effective-tld-service;1",
+ "nsIEffectiveTLDService");
+
+/**
+ * The SocialService is the public API to social providers - it tracks which
+ * providers are installed and enabled, and is the entry-point for access to
+ * the provider itself.
+ */
+
+// Internal helper methods and state
+var SocialServiceInternal = {
+ get enabled() {
+ return this.providerArray.length > 0;
+ },
+
+ get providerArray() {
+ return Object.keys(this.providers).map(origin => this.providers[origin]);
+ },
+ *manifestsGenerator() {
+ // Retrieve the manifests of installed providers from prefs
+ let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
+ let prefs = MANIFEST_PREFS.getChildList("", []);
+ for (let pref of prefs) {
+ // we only consider manifests in user level prefs to be *installed*
+ if (!MANIFEST_PREFS.prefHasUserValue(pref))
+ continue;
+ try {
+ var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data);
+ if (manifest && typeof(manifest) == "object" && manifest.origin)
+ yield manifest;
+ } catch (err) {
+ Cu.reportError("SocialService: failed to load manifest: " + pref +
+ ", exception: " + err);
+ }
+ }
+ },
+ get manifests() {
+ return this.manifestsGenerator();
+ },
+ getManifestPrefname: function(origin) {
+ // Retrieve the prefname for a given origin/manifest.
+ // If no existing pref, return a generated prefname.
+ let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
+ let prefs = MANIFEST_PREFS.getChildList("", []);
+ for (let pref of prefs) {
+ try {
+ var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data);
+ if (manifest.origin == origin) {
+ return pref;
+ }
+ } catch (err) {
+ Cu.reportError("SocialService: failed to load manifest: " + pref +
+ ", exception: " + err);
+ }
+ }
+ let originUri = Services.io.newURI(origin, null, null);
+ return originUri.hostPort.replace('.', '-');
+ },
+ orderedProviders: function(aCallback) {
+ if (SocialServiceInternal.providerArray.length < 2) {
+ schedule(function () {
+ aCallback(SocialServiceInternal.providerArray);
+ });
+ return;
+ }
+ // query moz_hosts for frecency. since some providers may not have a
+ // frecency entry, we need to later sort on our own. We use the providers
+ // object below as an easy way to later record the frecency on the provider
+ // object from the query results.
+ let hosts = [];
+ let providers = {};
+
+ for (let p of SocialServiceInternal.providerArray) {
+ p.frecency = 0;
+ providers[p.domain] = p;
+ hosts.push(p.domain);
+ }
+
+ // cannot bind an array to stmt.params so we have to build the string
+ let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .DBConnection.createAsyncStatement(
+ "SELECT host, frecency FROM moz_hosts WHERE host IN (" +
+ hosts.map(host => '"' + host + '"').join(",") + ") "
+ );
+
+ try {
+ stmt.executeAsync({
+ handleResult: function(aResultSet) {
+ let row;
+ while ((row = aResultSet.getNextRow())) {
+ let rh = row.getResultByName("host");
+ let frecency = row.getResultByName("frecency");
+ providers[rh].frecency = parseInt(frecency) || 0;
+ }
+ },
+ handleError: function(aError) {
+ Cu.reportError(aError.message + " (Result = " + aError.result + ")");
+ },
+ handleCompletion: function(aReason) {
+ // the query may not have returned all our providers, so we have
+ // stamped the frecency on the provider and sort here. This makes sure
+ // all enabled providers get sorted even with frecency zero.
+ let providerList = SocialServiceInternal.providerArray;
+ // reverse sort
+ aCallback(providerList.sort((a, b) => b.frecency - a.frecency));
+ }
+ });
+ } finally {
+ stmt.finalize();
+ }
+ }
+};
+
+XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () {
+ initService();
+ let providers = {};
+ for (let manifest of this.manifests) {
+ try {
+ if (ActiveProviders.has(manifest.origin)) {
+ // enable the api when a provider is enabled
+ let provider = new SocialProvider(manifest);
+ providers[provider.origin] = provider;
+ }
+ } catch (err) {
+ Cu.reportError("SocialService: failed to load provider: " + manifest.origin +
+ ", exception: " + err);
+ }
+ }
+ return providers;
+});
+
+function getOriginActivationType(origin) {
+ // if this is an about uri, treat it as a directory
+ let URI = Services.io.newURI(origin, null, null);
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(URI, {});
+ if (Services.scriptSecurityManager.isSystemPrincipal(principal) || origin == "moz-safe-about:home") {
+ return "internal";
+ }
+
+ let directories = Services.prefs.getCharPref("social.directories").split(',');
+ if (directories.indexOf(origin) >= 0)
+ return "directory";
+
+ return "foreign";
+}
+
+var ActiveProviders = {
+ get _providers() {
+ delete this._providers;
+ this._providers = {};
+ try {
+ let pref = Services.prefs.getComplexValue("social.activeProviders",
+ Ci.nsISupportsString);
+ this._providers = JSON.parse(pref);
+ } catch (ex) {}
+ return this._providers;
+ },
+
+ has: function (origin) {
+ return (origin in this._providers);
+ },
+
+ add: function (origin) {
+ this._providers[origin] = 1;
+ this._deferredTask.arm();
+ },
+
+ delete: function (origin) {
+ delete this._providers[origin];
+ this._deferredTask.arm();
+ },
+
+ flush: function () {
+ this._deferredTask.disarm();
+ this._persist();
+ },
+
+ get _deferredTask() {
+ delete this._deferredTask;
+ return this._deferredTask = new DeferredTask(this._persist.bind(this), 0);
+ },
+
+ _persist: function () {
+ let string = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ string.data = JSON.stringify(this._providers);
+ Services.prefs.setComplexValue("social.activeProviders",
+ Ci.nsISupportsString, string);
+ }
+};
+
+function migrateSettings() {
+ let activeProviders, enabled;
+ try {
+ activeProviders = Services.prefs.getCharPref("social.activeProviders");
+ } catch (e) {
+ // not set, we'll check if we need to migrate older prefs
+ }
+ if (Services.prefs.prefHasUserValue("social.enabled")) {
+ enabled = Services.prefs.getBoolPref("social.enabled");
+ }
+ if (activeProviders) {
+ // migration from fx21 to fx22 or later
+ // ensure any *builtin* provider in activeproviders is in user level prefs
+ for (let origin in ActiveProviders._providers) {
+ let prefname;
+ let manifest;
+ let defaultManifest;
+ try {
+ prefname = getPrefnameFromOrigin(origin);
+ manifest = JSON.parse(Services.prefs.getComplexValue(prefname, Ci.nsISupportsString).data);
+ } catch (e) {
+ // Our preference is missing or bad, remove from ActiveProviders and
+ // continue. This is primarily an error-case and should only be
+ // reached by either messing with preferences or hitting the one or
+ // two days of nightly that ran into it, so we'll flush right away.
+ ActiveProviders.delete(origin);
+ ActiveProviders.flush();
+ continue;
+ }
+ let needsUpdate = !manifest.updateDate;
+ // fx23 may have built-ins with shareURL
+ try {
+ defaultManifest = Services.prefs.getDefaultBranch(null)
+ .getComplexValue(prefname, Ci.nsISupportsString).data;
+ defaultManifest = JSON.parse(defaultManifest);
+ } catch (e) {
+ // not a built-in, continue
+ }
+ if (defaultManifest) {
+ if (defaultManifest.shareURL && !manifest.shareURL) {
+ manifest.shareURL = defaultManifest.shareURL;
+ needsUpdate = true;
+ }
+ if (defaultManifest.version && (!manifest.version || defaultManifest.version > manifest.version)) {
+ manifest = defaultManifest;
+ needsUpdate = true;
+ }
+ }
+ if (needsUpdate) {
+ // the provider was installed with an older build, so we will update the
+ // timestamp and ensure the manifest is in user prefs
+ delete manifest.builtin;
+ // we're potentially updating for share, so always mark the updateDate
+ manifest.updateDate = Date.now();
+ if (!manifest.installDate)
+ manifest.installDate = 0; // we don't know when it was installed
+
+ let string = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ string.data = JSON.stringify(manifest);
+ Services.prefs.setComplexValue(prefname, Ci.nsISupportsString, string);
+ }
+ // as of fx 29, we no longer rely on social.enabled. migration from prior
+ // versions should disable all service addons if social.enabled=false
+ if (enabled === false) {
+ ActiveProviders.delete(origin);
+ }
+ }
+ ActiveProviders.flush();
+ Services.prefs.clearUserPref("social.enabled");
+ return;
+ }
+
+ // primary migration from pre-fx21
+ let active;
+ try {
+ active = Services.prefs.getBoolPref("social.active");
+ } catch (e) {}
+ if (!active)
+ return;
+
+ // primary difference from SocialServiceInternal.manifests is that we
+ // only read the default branch here.
+ let manifestPrefs = Services.prefs.getDefaultBranch("social.manifest.");
+ let prefs = manifestPrefs.getChildList("", []);
+ for (let pref of prefs) {
+ try {
+ let manifest;
+ try {
+ manifest = JSON.parse(manifestPrefs.getComplexValue(pref, Ci.nsISupportsString).data);
+ } catch (e) {
+ // bad or missing preference, we wont update this one.
+ continue;
+ }
+ if (manifest && typeof(manifest) == "object" && manifest.origin) {
+ // our default manifests have been updated with the builtin flags as of
+ // fx22, delete it so we can set the user-pref
+ delete manifest.builtin;
+ if (!manifest.updateDate) {
+ manifest.updateDate = Date.now();
+ manifest.installDate = 0; // we don't know when it was installed
+ }
+
+ let string = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ string.data = JSON.stringify(manifest);
+ // pref here is just the branch name, set the full pref name
+ Services.prefs.setComplexValue("social.manifest." + pref, Ci.nsISupportsString, string);
+ ActiveProviders.add(manifest.origin);
+ ActiveProviders.flush();
+ // social.active was used at a time that there was only one
+ // builtin, we'll assume that is still the case
+ return;
+ }
+ } catch (err) {
+ Cu.reportError("SocialService: failed to load manifest: " + pref + ", exception: " + err);
+ }
+ }
+}
+
+function initService() {
+ Services.obs.addObserver(function xpcomShutdown() {
+ ActiveProviders.flush();
+ SocialService._providerListeners = null;
+ Services.obs.removeObserver(xpcomShutdown, "xpcom-shutdown");
+ }, "xpcom-shutdown", false);
+
+ try {
+ migrateSettings();
+ } catch (e) {
+ // no matter what, if migration fails we do not want to render social
+ // unusable. Worst case scenario is that, when upgrading Firefox, previously
+ // enabled providers are not migrated.
+ Cu.reportError("Error migrating social settings: " + e);
+ }
+}
+
+function schedule(callback) {
+ Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+}
+
+// Public API
+this.SocialService = {
+ get hasEnabledProviders() {
+ // used as an optimization during startup, can be used to check if further
+ // initialization should be done (e.g. creating the instances of
+ // SocialProvider and turning on UI). ActiveProviders may have changed and
+ // not yet flushed so we check the active providers array
+ for (let p in ActiveProviders._providers) {
+ return true;
+ }
+ return false;
+ },
+ get enabled() {
+ return SocialServiceInternal.enabled;
+ },
+ set enabled(val) {
+ throw new Error("not allowed to set SocialService.enabled");
+ },
+
+ // Enables a provider, the manifest must already exist in prefs. The provider
+ // may or may not have previously been added. onDone is always called
+ // - with null if no such provider exists, or the activated provider on
+ // success.
+ enableProvider: function enableProvider(origin, onDone) {
+ if (SocialServiceInternal.providers[origin]) {
+ schedule(function() {
+ onDone(SocialServiceInternal.providers[origin]);
+ });
+ return;
+ }
+ let manifest = SocialService.getManifestByOrigin(origin);
+ if (manifest) {
+ let addon = new AddonWrapper(manifest);
+ AddonManagerPrivate.callAddonListeners("onEnabling", addon, false);
+ addon.pendingOperations |= AddonManager.PENDING_ENABLE;
+ this.addProvider(manifest, onDone);
+ addon.pendingOperations -= AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onEnabled", addon);
+ return;
+ }
+ schedule(function() {
+ onDone(null);
+ });
+ },
+
+ // Adds a provider given a manifest, and returns the added provider.
+ addProvider: function addProvider(manifest, onDone) {
+ if (SocialServiceInternal.providers[manifest.origin])
+ throw new Error("SocialService.addProvider: provider with this origin already exists");
+
+ // enable the api when a provider is enabled
+ let provider = new SocialProvider(manifest);
+ SocialServiceInternal.providers[provider.origin] = provider;
+ ActiveProviders.add(provider.origin);
+
+ this.getOrderedProviderList(function (providers) {
+ this._notifyProviderListeners("provider-enabled", provider.origin, providers);
+ if (onDone)
+ onDone(provider);
+ }.bind(this));
+ },
+
+ // Removes a provider with the given origin, and notifies when the removal is
+ // complete.
+ disableProvider: function disableProvider(origin, onDone) {
+ if (!(origin in SocialServiceInternal.providers))
+ throw new Error("SocialService.disableProvider: no provider with origin " + origin + " exists!");
+
+ let provider = SocialServiceInternal.providers[origin];
+ let manifest = SocialService.getManifestByOrigin(origin);
+ let addon = manifest && new AddonWrapper(manifest);
+ if (addon) {
+ AddonManagerPrivate.callAddonListeners("onDisabling", addon, false);
+ addon.pendingOperations |= AddonManager.PENDING_DISABLE;
+ }
+ provider.enabled = false;
+
+ ActiveProviders.delete(provider.origin);
+
+ delete SocialServiceInternal.providers[origin];
+
+ if (addon) {
+ // we have to do this now so the addon manager ui will update an uninstall
+ // correctly.
+ addon.pendingOperations -= AddonManager.PENDING_DISABLE;
+ AddonManagerPrivate.callAddonListeners("onDisabled", addon);
+ }
+
+ this.getOrderedProviderList(function (providers) {
+ this._notifyProviderListeners("provider-disabled", origin, providers);
+ if (onDone)
+ onDone();
+ }.bind(this));
+ },
+
+ // Returns a single provider object with the specified origin. The provider
+ // must be "installed" (ie, in ActiveProviders)
+ getProvider: function getProvider(origin, onDone) {
+ schedule((function () {
+ onDone(SocialServiceInternal.providers[origin] || null);
+ }).bind(this));
+ },
+
+ // Returns an unordered array of installed providers
+ getProviderList: function(onDone) {
+ schedule(function () {
+ onDone(SocialServiceInternal.providerArray);
+ });
+ },
+
+ getManifestByOrigin: function(origin) {
+ for (let manifest of SocialServiceInternal.manifests) {
+ if (origin == manifest.origin) {
+ return manifest;
+ }
+ }
+ return null;
+ },
+
+ // Returns an array of installed providers, sorted by frecency
+ getOrderedProviderList: function(onDone) {
+ SocialServiceInternal.orderedProviders(onDone);
+ },
+
+ getOriginActivationType: function (origin) {
+ return getOriginActivationType(origin);
+ },
+
+ _providerListeners: new Map(),
+ registerProviderListener: function registerProviderListener(listener) {
+ this._providerListeners.set(listener, 1);
+ },
+ unregisterProviderListener: function unregisterProviderListener(listener) {
+ this._providerListeners.delete(listener);
+ },
+
+ _notifyProviderListeners: function (topic, origin, providers) {
+ for (let [listener, ] of this._providerListeners) {
+ try {
+ listener(topic, origin, providers);
+ } catch (ex) {
+ Components.utils.reportError("SocialService: provider listener threw an exception: " + ex);
+ }
+ }
+ },
+
+ _manifestFromData: function(type, data, installOrigin) {
+ let featureURLs = ['shareURL'];
+ let resolveURLs = featureURLs.concat(['postActivationURL']);
+
+ if (type == 'directory' || type == 'internal') {
+ // directory provided manifests must have origin in manifest, use that
+ if (!data['origin']) {
+ Cu.reportError("SocialService.manifestFromData directory service provided manifest without origin.");
+ return null;
+ }
+ installOrigin = data.origin;
+ }
+ // force/fixup origin
+ let URI = Services.io.newURI(installOrigin, null, null);
+ let principal = Services.scriptSecurityManager.createCodebasePrincipal(URI, {});
+ data.origin = principal.origin;
+
+ // iconURL and name are required
+ let providerHasFeatures = featureURLs.some(url => data[url]);
+ if (!providerHasFeatures) {
+ Cu.reportError("SocialService.manifestFromData manifest missing required urls.");
+ return null;
+ }
+ if (!data['name'] || !data['iconURL']) {
+ Cu.reportError("SocialService.manifestFromData manifest missing name or iconURL.");
+ return null;
+ }
+ for (let url of resolveURLs) {
+ if (data[url]) {
+ try {
+ let resolved = Services.io.newURI(principal.URI.resolve(data[url]), null, null);
+ if (!(resolved.schemeIs("http") || resolved.schemeIs("https"))) {
+ Cu.reportError("SocialService.manifestFromData unsupported scheme '" + resolved.scheme + "' for " + principal.origin);
+ return null;
+ }
+ data[url] = resolved.spec;
+ } catch (e) {
+ Cu.reportError("SocialService.manifestFromData unable to resolve '" + url + "' for " + principal.origin);
+ return null;
+ }
+ }
+ }
+ return data;
+ },
+
+ _showInstallNotification: function(data, aAddonInstaller) {
+ let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+ let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ // internal/directory activations need to use the manifest origin, any other
+ // use the domain activation is occurring on
+ let url = data.url;
+ if (data.installType == "internal" || data.installType == "directory") {
+ url = data.manifest.origin;
+ }
+ let requestingURI = Services.io.newURI(url, null, null);
+ let productName = brandBundle.GetStringFromName("brandShortName");
+
+ let message = browserBundle.formatStringFromName("service.install.description",
+ [requestingURI.host, productName], 2);
+
+ let action = {
+ label: browserBundle.GetStringFromName("service.install.ok.label"),
+ accessKey: browserBundle.GetStringFromName("service.install.ok.accesskey"),
+ callback: function() {
+ aAddonInstaller.install();
+ },
+ };
+
+ let options = {
+ learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api",
+ };
+ let anchor = "servicesInstall-notification-icon";
+ let notificationid = "servicesInstall";
+ data.window.PopupNotifications.show(data.window.gBrowser.selectedBrowser,
+ notificationid, message, anchor,
+ action, [], options);
+ },
+
+ installProvider: function(data, installCallback, options={}) {
+ data.installType = getOriginActivationType(data.origin);
+ // if we get data, we MUST have a valid manifest generated from the data
+ let manifest = this._manifestFromData(data.installType, data.manifest, data.origin);
+ if (!manifest)
+ throw new Error("SocialService.installProvider: service configuration is invalid from " + data.url);
+
+ let addon = new AddonWrapper(manifest);
+ if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
+ throw new Error("installProvider: provider with origin [" +
+ data.origin + "] is blocklisted");
+ // manifestFromData call above will enforce correct origin. To support
+ // activation from about: uris, we need to be sure to use the updated
+ // origin on the manifest.
+ data.manifest = manifest;
+ let id = getAddonIDFromOrigin(manifest.origin);
+ AddonManager.getAddonByID(id, function(aAddon) {
+ if (aAddon && aAddon.userDisabled) {
+ aAddon.cancelUninstall();
+ aAddon.userDisabled = false;
+ }
+ schedule(function () {
+ try {
+ this._installProvider(data, options, aManifest => {
+ this._notifyProviderListeners("provider-installed", aManifest.origin);
+ installCallback(aManifest);
+ });
+ } catch (e) {
+ Cu.reportError("Activation failed: " + e);
+ installCallback(null);
+ }
+ }.bind(this));
+ }.bind(this));
+ },
+
+ _installProvider: function(data, options, installCallback) {
+ if (!data.manifest)
+ throw new Error("Cannot install provider without manifest data");
+
+ if (data.installType == "foreign" && !Services.prefs.getBoolPref("social.remote-install.enabled"))
+ throw new Error("Remote install of services is disabled");
+
+ // if installing from any website, the install must happen over https.
+ // "internal" are installs from about:home or similar
+ if (data.installType != "internal" && !Services.io.newURI(data.origin, null, null).schemeIs("https")) {
+ throw new Error("attempt to activate provider over unsecured channel: " + data.origin);
+ }
+
+ let installer = new AddonInstaller(data.url, data.manifest, installCallback);
+ let bypassPanel = options.bypassInstallPanel ||
+ (data.installType == "internal" && data.manifest.oneclick);
+ if (bypassPanel)
+ installer.install();
+ else
+ this._showInstallNotification(data, installer);
+ },
+
+ createWrapper: function(manifest) {
+ return new AddonWrapper(manifest);
+ },
+
+ /**
+ * updateProvider is used from the worker to self-update. Since we do not
+ * have knowledge of the currently selected provider here, we will notify
+ * the front end to deal with any reload.
+ */
+ updateProvider: function(aUpdateOrigin, aManifest) {
+ let installType = this.getOriginActivationType(aUpdateOrigin);
+ // if we get data, we MUST have a valid manifest generated from the data
+ let manifest = this._manifestFromData(installType, aManifest, aUpdateOrigin);
+ if (!manifest)
+ throw new Error("SocialService.installProvider: service configuration is invalid from " + aUpdateOrigin);
+
+ // overwrite the preference
+ let string = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ string.data = JSON.stringify(manifest);
+ Services.prefs.setComplexValue(getPrefnameFromOrigin(manifest.origin), Ci.nsISupportsString, string);
+
+ // overwrite the existing provider then notify the front end so it can
+ // handle any reload that might be necessary.
+ if (ActiveProviders.has(manifest.origin)) {
+ let provider = SocialServiceInternal.providers[manifest.origin];
+ provider.enabled = false;
+ provider = new SocialProvider(manifest);
+ SocialServiceInternal.providers[provider.origin] = provider;
+ // update the cache and ui, reload provider if necessary
+ this.getOrderedProviderList(providers => {
+ this._notifyProviderListeners("provider-update", provider.origin, providers);
+ });
+ }
+
+ },
+
+ uninstallProvider: function(origin, aCallback) {
+ let manifest = SocialService.getManifestByOrigin(origin);
+ let addon = new AddonWrapper(manifest);
+ addon.uninstall(aCallback);
+ }
+};
+
+/**
+ * The SocialProvider object represents a social provider.
+ *
+ * @constructor
+ * @param {jsobj} object representing the manifest file describing this provider
+ * @param {bool} boolean indicating whether this provider is "built in"
+ */
+function SocialProvider(input) {
+ if (!input.name)
+ throw new Error("SocialProvider must be passed a name");
+ if (!input.origin)
+ throw new Error("SocialProvider must be passed an origin");
+
+ let addon = new AddonWrapper(input);
+ if (addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
+ throw new Error("SocialProvider: provider with origin [" +
+ input.origin + "] is blocklisted");
+
+ this.name = input.name;
+ this.iconURL = input.iconURL;
+ this.icon32URL = input.icon32URL;
+ this.icon64URL = input.icon64URL;
+ this.shareURL = input.shareURL;
+ this.postActivationURL = input.postActivationURL;
+ this.origin = input.origin;
+ let originUri = Services.io.newURI(input.origin, null, null);
+ this.principal = Services.scriptSecurityManager.createCodebasePrincipal(originUri, {});
+ this.ambientNotificationIcons = {};
+ this.errorState = null;
+ this.frecency = 0;
+
+ try {
+ this.domain = etld.getBaseDomainFromHost(originUri.host);
+ } catch (e) {
+ this.domain = originUri.host;
+ }
+}
+
+SocialProvider.prototype = {
+ reload: function() {
+ // calling terminate/activate does not set the enabled state whereas setting
+ // enabled will call terminate/activate
+ this.enabled = false;
+ this.enabled = true;
+ Services.obs.notifyObservers(null, "social:provider-reload", this.origin);
+ },
+
+ // Provider enabled/disabled state.
+ _enabled: false,
+ get enabled() {
+ return this._enabled;
+ },
+ set enabled(val) {
+ let enable = !!val;
+ if (enable == this._enabled)
+ return;
+
+ this._enabled = enable;
+
+ if (enable) {
+ this._activate();
+ } else {
+ this._terminate();
+ }
+ },
+
+ get manifest() {
+ return SocialService.getManifestByOrigin(this.origin);
+ },
+
+ getPageSize: function(name) {
+ let manifest = this.manifest;
+ if (manifest && manifest.pageSize)
+ return manifest.pageSize[name];
+ return undefined;
+ },
+
+ // Internal helper methods
+ _activate: function _activate() {
+ },
+
+ _terminate: function _terminate() {
+ this.errorState = null;
+ },
+
+ /**
+ * Checks if a given URI is of the same origin as the provider.
+ *
+ * Returns true or false.
+ *
+ * @param {URI or string} uri
+ */
+ isSameOrigin: function isSameOrigin(uri, allowIfInheritsPrincipal) {
+ if (!uri)
+ return false;
+ if (typeof uri == "string") {
+ try {
+ uri = Services.io.newURI(uri, null, null);
+ } catch (ex) {
+ // an invalid URL can't be loaded!
+ return false;
+ }
+ }
+ try {
+ this.principal.checkMayLoad(
+ uri, // the thing to check.
+ false, // reportError - we do our own reporting when necessary.
+ allowIfInheritsPrincipal
+ );
+ return true;
+ } catch (ex) {
+ return false;
+ }
+ },
+
+ /**
+ * Resolve partial URLs for a provider.
+ *
+ * Returns nsIURI object or null on failure
+ *
+ * @param {string} url
+ */
+ resolveUri: function resolveUri(url) {
+ try {
+ let fullURL = this.principal.URI.resolve(url);
+ return Services.io.newURI(fullURL, null, null);
+ } catch (ex) {
+ Cu.reportError("mozSocial: failed to resolve window URL: " + url + "; " + ex);
+ return null;
+ }
+ }
+};
+
+function getAddonIDFromOrigin(origin) {
+ let originUri = Services.io.newURI(origin, null, null);
+ return originUri.host + ID_SUFFIX;
+}
+
+function getPrefnameFromOrigin(origin) {
+ return "social.manifest." + SocialServiceInternal.getManifestPrefname(origin);
+}
+
+function AddonInstaller(sourceURI, aManifest, installCallback) {
+ aManifest.updateDate = Date.now();
+ // get the existing manifest for installDate
+ let manifest = SocialService.getManifestByOrigin(aManifest.origin);
+ let isNewInstall = !manifest;
+ if (manifest && manifest.installDate)
+ aManifest.installDate = manifest.installDate;
+ else
+ aManifest.installDate = aManifest.updateDate;
+
+ this.sourceURI = sourceURI;
+ this.install = function() {
+ let addon = this.addon;
+ if (isNewInstall) {
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, addon, null, false);
+ AddonManagerPrivate.callAddonListeners("onInstalling", addon, false);
+ }
+
+ let string = Cc["@mozilla.org/supports-string;1"].
+ createInstance(Ci.nsISupportsString);
+ string.data = JSON.stringify(aManifest);
+ Services.prefs.setComplexValue(getPrefnameFromOrigin(aManifest.origin), Ci.nsISupportsString, string);
+
+ if (isNewInstall) {
+ AddonManagerPrivate.callAddonListeners("onInstalled", addon);
+ }
+ installCallback(aManifest);
+ };
+ this.cancel = function() {
+ Services.prefs.clearUserPref(getPrefnameFromOrigin(aManifest.origin));
+ };
+ this.addon = new AddonWrapper(aManifest);
+}
+
+var SocialAddonProvider = {
+ startup: function() {},
+
+ shutdown: function() {},
+
+ updateAddonAppDisabledStates: function() {
+ // we wont bother with "enabling" services that are released from blocklist
+ for (let manifest of SocialServiceInternal.manifests) {
+ try {
+ if (ActiveProviders.has(manifest.origin)) {
+ let addon = new AddonWrapper(manifest);
+ if (addon.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+ SocialService.disableProvider(manifest.origin);
+ }
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ },
+
+ getAddonByID: function(aId, aCallback) {
+ for (let manifest of SocialServiceInternal.manifests) {
+ if (aId == getAddonIDFromOrigin(manifest.origin)) {
+ aCallback(new AddonWrapper(manifest));
+ return;
+ }
+ }
+ aCallback(null);
+ },
+
+ getAddonsByTypes: function(aTypes, aCallback) {
+ if (aTypes && aTypes.indexOf(ADDON_TYPE_SERVICE) == -1) {
+ aCallback([]);
+ return;
+ }
+ aCallback([...SocialServiceInternal.manifests].map(a => new AddonWrapper(a)));
+ },
+
+ removeAddon: function(aAddon, aCallback) {
+ AddonManagerPrivate.callAddonListeners("onUninstalling", aAddon, false);
+ aAddon.pendingOperations |= AddonManager.PENDING_UNINSTALL;
+ Services.prefs.clearUserPref(getPrefnameFromOrigin(aAddon.manifest.origin));
+ aAddon.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+ AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon);
+ SocialService._notifyProviderListeners("provider-uninstalled", aAddon.manifest.origin);
+ if (aCallback)
+ schedule(aCallback);
+ }
+};
+
+
+function AddonWrapper(aManifest) {
+ this.manifest = aManifest;
+ this.id = getAddonIDFromOrigin(this.manifest.origin);
+ this._pending = AddonManager.PENDING_NONE;
+}
+AddonWrapper.prototype = {
+ get type() {
+ return ADDON_TYPE_SERVICE;
+ },
+
+ get appDisabled() {
+ return this.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED;
+ },
+
+ set softDisabled(val) {
+ this.userDisabled = val;
+ },
+
+ get softDisabled() {
+ return this.userDisabled;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get scope() {
+ return AddonManager.SCOPE_PROFILE;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ isCompatibleWith: function(appVersion, platformVersion) {
+ return true;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get blocklistState() {
+ return Services.blocklist.getAddonBlocklistState(this);
+ },
+
+ get blocklistURL() {
+ return Services.blocklist.getAddonBlocklistURL(this);
+ },
+
+ get screenshots() {
+ return [];
+ },
+
+ get pendingOperations() {
+ return this._pending || AddonManager.PENDING_NONE;
+ },
+ set pendingOperations(val) {
+ this._pending = val;
+ },
+
+ get operationsRequiringRestart() {
+ return AddonManager.OP_NEEDS_RESTART_NONE;
+ },
+
+ get size() {
+ return null;
+ },
+
+ get permissions() {
+ let permissions = 0;
+ // any "user defined" manifest can be removed
+ if (Services.prefs.prefHasUserValue(getPrefnameFromOrigin(this.manifest.origin)))
+ permissions = AddonManager.PERM_CAN_UNINSTALL;
+ if (!this.appDisabled) {
+ if (this.userDisabled) {
+ permissions |= AddonManager.PERM_CAN_ENABLE;
+ } else {
+ permissions |= AddonManager.PERM_CAN_DISABLE;
+ }
+ }
+ return permissions;
+ },
+
+ findUpdates: function(listener, reason, appVersion, platformVersion) {
+ if ("onNoCompatibilityUpdateAvailable" in listener)
+ listener.onNoCompatibilityUpdateAvailable(this);
+ if ("onNoUpdateAvailable" in listener)
+ listener.onNoUpdateAvailable(this);
+ if ("onUpdateFinished" in listener)
+ listener.onUpdateFinished(this);
+ },
+
+ get isActive() {
+ return ActiveProviders.has(this.manifest.origin);
+ },
+
+ get name() {
+ return this.manifest.name;
+ },
+ get version() {
+ return this.manifest.version ? this.manifest.version.toString() : "";
+ },
+
+ get iconURL() {
+ return this.manifest.icon32URL ? this.manifest.icon32URL : this.manifest.iconURL;
+ },
+ get icon64URL() {
+ return this.manifest.icon64URL;
+ },
+ get icons() {
+ let icons = {
+ 16: this.manifest.iconURL
+ };
+ if (this.manifest.icon32URL)
+ icons[32] = this.manifest.icon32URL;
+ if (this.manifest.icon64URL)
+ icons[64] = this.manifest.icon64URL;
+ return icons;
+ },
+
+ get description() {
+ return this.manifest.description;
+ },
+ get homepageURL() {
+ return this.manifest.homepageURL;
+ },
+ get defaultLocale() {
+ return this.manifest.defaultLocale;
+ },
+ get selectedLocale() {
+ return this.manifest.selectedLocale;
+ },
+
+ get installDate() {
+ return this.manifest.installDate ? new Date(this.manifest.installDate) : null;
+ },
+ get updateDate() {
+ return this.manifest.updateDate ? new Date(this.manifest.updateDate) : null;
+ },
+
+ get creator() {
+ return new AddonManagerPrivate.AddonAuthor(this.manifest.author);
+ },
+
+ get userDisabled() {
+ return this.appDisabled || !ActiveProviders.has(this.manifest.origin);
+ },
+
+ set userDisabled(val) {
+ if (val == this.userDisabled)
+ return val;
+ if (val) {
+ SocialService.disableProvider(this.manifest.origin);
+ } else if (!this.appDisabled) {
+ SocialService.enableProvider(this.manifest.origin);
+ }
+ return val;
+ },
+
+ uninstall: function(aCallback) {
+ let prefName = getPrefnameFromOrigin(this.manifest.origin);
+ if (Services.prefs.prefHasUserValue(prefName)) {
+ if (ActiveProviders.has(this.manifest.origin)) {
+ SocialService.disableProvider(this.manifest.origin, function() {
+ SocialAddonProvider.removeAddon(this, aCallback);
+ }.bind(this));
+ } else {
+ SocialAddonProvider.removeAddon(this, aCallback);
+ }
+ } else {
+ schedule(aCallback);
+ }
+ },
+
+ cancelUninstall: function() {
+ this._pending -= AddonManager.PENDING_UNINSTALL;
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
+ }
+};
+
+
+AddonManagerPrivate.registerProvider(SocialAddonProvider, [
+ new AddonManagerPrivate.AddonType(ADDON_TYPE_SERVICE, URI_EXTENSION_STRINGS,
+ STRING_TYPE_NAME,
+ AddonManager.VIEW_TYPE_LIST, 10000)
+]);
diff --git a/browser/modules/moz.build b/browser/modules/moz.build
index a7bbbc258..852a4c911 100644
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -35,6 +35,8 @@ EXTRA_JS_MODULES += [
'Sanitizer.jsm',
'SelfSupportBackend.jsm',
'SitePermissions.jsm',
+ 'Social.jsm',
+ 'SocialService.jsm',
'TransientPrefs.jsm',
'URLBarZoom.jsm',
'webrtcUI.jsm',
diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css
index 3e6b81512..73d3844a2 100644
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1172,6 +1172,56 @@ html|span.ac-emphasize-text-url {
-moz-image-region: rect(0, 48px, 16px, 32px);
}
+/* social share panel */
+%include ../shared/social/social.inc.css
+
+.social-share-frame {
+ border-top: 1px solid #f8f8f8;
+ width: 756px;
+ height: 150px;
+}
+
+#share-container {
+ min-width: 756px;
+ background-color: white;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+#share-container[loading] {
+ background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
+}
+#share-container > browser {
+ transition: opacity 150ms ease-in-out;
+ opacity: 1;
+}
+#share-container[loading] > browser {
+ opacity: 0;
+}
+
+.social-share-toolbar {
+ border-bottom: 1px solid #dedede;
+ padding: 2px;
+}
+
+#social-share-provider-buttons {
+ padding: 0;
+ margin: 0;
+}
+
+.share-provider-button {
+ padding: 5px;
+ margin: 2px;
+}
+
+.share-provider-button > .toolbarbutton-text {
+ display: none;
+}
+.share-provider-button > .toolbarbutton-icon {
+ width: 16px;
+ min-height: 16px;
+ max-height: 16px;
+}
+
/* bookmarks menu-button */
#bookmarks-menu-button[cui-areatype="toolbar"] > .toolbarbutton-menubutton-dropmarker {
diff --git a/browser/themes/linux/customizableui/panelUI.css b/browser/themes/linux/customizableui/panelUI.css
index 0037b5634..289faa085 100644
--- a/browser/themes/linux/customizableui/panelUI.css
+++ b/browser/themes/linux/customizableui/panelUI.css
@@ -49,6 +49,14 @@
padding-inline-start: 0;
}
+/* subviewbutton entries for social sidebars have images that come from external
+/* sources, and are not guaranteed to be the size we want, so force the size on
+/* those icons. */
+toolbarbutton.social-provider-menuitem > .toolbarbutton-icon {
+ width: 16px;
+ height: 16px;
+}
+
.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .restoreallitem, .bookmark-item)[checked="true"] > .toolbarbutton-icon {
visibility: hidden;
}
diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn
index 189027812..0bf023f35 100644
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -81,6 +81,10 @@ browser.jar:
* skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
* skin/classic/browser/preferences/in-content/dialog.css (preferences/in-content/dialog.css)
skin/classic/browser/preferences/applications.css (preferences/applications.css)
+ skin/classic/browser/social/services-16.png (social/services-16.png)
+ skin/classic/browser/social/services-64.png (social/services-64.png)
+ skin/classic/browser/social/share-button.png (social/share-button.png)
+ skin/classic/browser/social/share-button-active.png (social/share-button-active.png)
skin/classic/browser/tabbrowser/alltabs.png (tabbrowser/alltabs.png)
skin/classic/browser/tabbrowser/alltabs-inverted.png (tabbrowser/alltabs-inverted.png)
skin/classic/browser/tabbrowser/newtab.svg (tabbrowser/newtab.svg)
diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css
index 5a83c74b2..e8ac9163e 100644
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -808,6 +808,10 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
-moz-image-region: rect(18px, 288px, 36px, 270px);
}
+ #social-share-button@toolbarButtonPressed@ {
+ -moz-image-region: rect(18px, 306px, 36px, 288px);
+ }
+
#characterencoding-button@toolbarButtonPressed@ {
-moz-image-region: rect(18px, 324px, 36px, 306px);
}
@@ -963,6 +967,10 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
-moz-image-region: rect(36px, 576px, 72px, 540px);
}
+ #social-share-button@toolbarButtonPressed@ {
+ -moz-image-region: rect(36px, 612px, 72px, 576px);
+ }
+
#characterencoding-button@toolbarButtonPressed@ {
-moz-image-region: rect(36px, 648px, 72px, 612px);
}
@@ -2021,6 +2029,59 @@ html|span.ac-emphasize-text-url {
-moz-image-region: rect(0, 48px, 16px, 32px);
}
+/* social share panel */
+.social-share-frame {
+ border-top: 1px solid #f8f8f8;
+ min-width: 756px;
+ height: 150px;
+ /* we resize our panels dynamically, make it look nice */
+}
+
+#share-container {
+ min-width: 756px;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+#share-container[loading] {
+ background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
+}
+#share-container > browser {
+ transition: opacity 150ms ease-in-out;
+ opacity: 1;
+}
+#share-container[loading] > browser {
+ opacity: 0;
+}
+
+#manage-share-providers {
+ -moz-image-region: rect(18px, 468px, 36px, 450px);
+}
+
+.social-share-toolbar {
+ border-bottom: 1px solid #dedede;
+ padding: 2px;
+}
+
+#social-share-provider-buttons {
+ padding: 0;
+ margin: 0;
+}
+
+.share-provider-button {
+ padding: 5px;
+ margin: 2px;
+}
+
+.share-provider-button > .toolbarbutton-text {
+ display: none;
+}
+
+.share-provider-button > .toolbarbutton-icon {
+ width: 16px;
+ min-height: 16px;
+ max-height: 16px;
+}
+
/* BOOKMARKING PANEL */
#editBookmarkPanelStarIcon {
list-style-image: url("chrome://browser/skin/places/starred48.png");
@@ -3121,6 +3182,33 @@ menulist.translate-infobar-element > .menulist-dropmarker {
border-radius: 1px;
}
+/* Share */
+%include ../shared/social/social.inc.css
+
+#social-share-panel {
+ min-height: 100px;
+ min-width: 300px;
+ transition: height .3s ease-in-out, width .3s ease-in-out;
+}
+
+#share-container,
+.social-share-frame {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: inherit;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: inherit;
+}
+
+#social-share-panel > .social-share-toolbar {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
+#social-share-provider-buttons {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
/* Customization mode */
%include ../shared/customizableui/customizeMode.inc.css
diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn
index 27802843d..98ba4e6ea 100644
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -124,6 +124,10 @@ browser.jar:
* skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
* skin/classic/browser/preferences/in-content/dialog.css (preferences/in-content/dialog.css)
skin/classic/browser/preferences/applications.css (preferences/applications.css)
+ skin/classic/browser/social/services-16.png (social/services-16.png)
+ skin/classic/browser/social/services-16@2x.png (social/services-16@2x.png)
+ skin/classic/browser/social/services-64.png (social/services-64.png)
+ skin/classic/browser/social/services-64@2x.png (social/services-64@2x.png)
skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon.png (tabbrowser/alltabs-box-bkgnd-icon.png)
skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon-inverted.png (tabbrowser/alltabs-box-bkgnd-icon-inverted.png)
skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon-inverted@2x.png (tabbrowser/alltabs-box-bkgnd-icon-inverted@2x.png)
diff --git a/browser/themes/shared/aboutProviderDirectory.css b/browser/themes/shared/aboutProviderDirectory.css
new file mode 100644
index 000000000..73e570aad
--- /dev/null
+++ b/browser/themes/shared/aboutProviderDirectory.css
@@ -0,0 +1,30 @@
+%include aboutSocialError.css
+
+body {
+ width: 310px;
+ margin: 1em auto;
+}
+
+#message-box {
+ margin-top: 2em;
+ background: url('chrome://global/skin/icons/information-24.png') no-repeat left 4px;
+ padding-inline-start: 30px;
+}
+
+#activation-frame {
+ border: none;
+ margin: 0;
+ width: 310px;
+ height: 200px;
+}
+#activation > p {
+ width: 100%;
+ text-align: center;
+ margin: 0;
+ line-height: 2em;
+}
+.link {
+ text-decoration: none;
+ color: -moz-nativehyperlinktext;
+ cursor: pointer;
+}
diff --git a/browser/themes/shared/browser.inc b/browser/themes/shared/browser.inc
index 81caf94d6..c57b59237 100644
--- a/browser/themes/shared/browser.inc
+++ b/browser/themes/shared/browser.inc
@@ -2,7 +2,7 @@
% Note that zoom-reset-button is a bit different since it doesn't use an image and thus has the image with display: none.
%define nestedButtons #zoom-out-button, #zoom-reset-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
-%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@, #e10s-button, #panic-button, #webide-button, #containers-panelmenu
+%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@, #e10s-button, #panic-button, #webide-button, #containers-panelmenu
%ifdef XP_MACOSX
% Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button to exit fullscreen
diff --git a/browser/themes/shared/customizableui/panelUI.inc.css b/browser/themes/shared/customizableui/panelUI.inc.css
index ba36da995..b0bb05415 100644
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -951,6 +951,7 @@ panelview .toolbarbutton-1,
.subviewbutton,
.widget-overflow-list .toolbarbutton-1,
.panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button,
+.share-provider-button,
.toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton {
-moz-appearance: none;
padding: 0 6px;
@@ -963,6 +964,7 @@ panelview .toolbarbutton-1,
panelview .toolbarbutton-1,
.subviewbutton,
.widget-overflow-list .toolbarbutton-1,
+.share-provider-button,
.toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton {
border-width: 1px;
}
@@ -1036,6 +1038,7 @@ panelview .toolbarbutton-1@buttonStateHover@,
toolbarbutton.subviewbutton@buttonStateHover@,
menu.subviewbutton@menuStateHover@,
menuitem.subviewbutton@menuStateHover@,
+.share-provider-button@buttonStateHover@:not([checked="true"]),
.widget-overflow-list .toolbarbutton-1@buttonStateHover@,
.toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton@buttonStateHover@ {
background-color: var(--arrowpanel-dimmed);
@@ -1050,6 +1053,7 @@ panelview .toolbarbutton-1:-moz-any(@buttonStateActive@,[checked=true]),
toolbarbutton.subviewbutton@buttonStateActive@,
menu.subviewbutton@menuStateActive@,
menuitem.subviewbutton@menuStateActive@,
+.share-provider-button:-moz-any(@buttonStateActive@,[checked=true]),
.widget-overflow-list .toolbarbutton-1@buttonStateActive@,
.toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton@buttonStateActive@ {
background-color: var(--arrowpanel-dimmed-further);
diff --git a/browser/themes/shared/jar.inc.mn b/browser/themes/shared/jar.inc.mn
index b5cdf246a..dcc1e9dd9 100644
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -10,7 +10,9 @@
skin/classic/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/browser/blockedSite.css (../shared/blockedSite.css)
skin/classic/browser/error-pages.css (../shared/error-pages.css)
+* skin/classic/browser/aboutProviderDirectory.css (../shared/aboutProviderDirectory.css)
* skin/classic/browser/aboutSessionRestore.css (../shared/aboutSessionRestore.css)
+ skin/classic/browser/aboutSocialError.css (../shared/aboutSocialError.css)
skin/classic/browser/aboutTabCrashed.css (../shared/aboutTabCrashed.css)
skin/classic/browser/aboutWelcomeBack.css (../shared/aboutWelcomeBack.css)
skin/classic/browser/content-contextmenu.svg (../shared/content-contextmenu.svg)
@@ -63,6 +65,7 @@
* skin/classic/browser/identity-icon.svg (../shared/identity-block/identity-icon.svg)
skin/classic/browser/info.svg (../shared/info.svg)
* skin/classic/browser/menuPanel.svg (../shared/menuPanel.svg)
+* skin/classic/browser/menuPanel-small.svg (../shared/menuPanel-small.svg)
* skin/classic/browser/notification-icons.svg (../shared/notification-icons.svg)
* skin/classic/browser/tracking-protection-16.svg (../shared/identity-block/tracking-protection-16.svg)
skin/classic/browser/newtab/close.png (../shared/newtab/close.png)
@@ -97,6 +100,8 @@
skin/classic/browser/search-indicator-magnifying-glass.svg (../shared/search/search-indicator-magnifying-glass.svg)
skin/classic/browser/search-arrow-go.svg (../shared/search/search-arrow-go.svg)
skin/classic/browser/gear.svg (../shared/search/gear.svg)
+ skin/classic/browser/social/gear_default.png (../shared/social/gear_default.png)
+ skin/classic/browser/social/gear_clicked.png (../shared/social/gear_clicked.png)
skin/classic/browser/tabbrowser/connecting.png (../shared/tabbrowser/connecting.png)
skin/classic/browser/tabbrowser/connecting@2x.png (../shared/tabbrowser/connecting@2x.png)
skin/classic/browser/tabbrowser/crashed.svg (../shared/tabbrowser/crashed.svg)
diff --git a/browser/themes/shared/menuPanel-small.svg b/browser/themes/shared/menuPanel-small.svg
new file mode 100644
index 000000000..db28992e2
--- /dev/null
+++ b/browser/themes/shared/menuPanel-small.svg
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="96" height="16" viewBox="0 0 96 16"
+ class="fieldtext">
+#include icon-colors.inc.svg
+
+ <path id="placeholder" d="M8,16a8,8,0,1,1,8-8A8,8,0,0,1,8,16ZM12,4H4v8h8V4ZM5,9.939V6.061L6.939,8ZM9.939,11H6.061L8,9.061ZM11,11h0Zm0-4.939V9.939L9.061,8ZM11,5h0ZM6.061,5H9.939L8,6.939Z"/>
+ <path id="cut" d="M29.63,15a2.426,2.426,0,0,1-2.282-1.277c-0.761-1.109-1.694-2.488-1.694-2.488S25,10.329,24.549,9.623a1.05,1.05,0,0,0-1.106-.538S20.6,4.437,20.124,3.706C19.465,2.689,20.7,1,20.7,1l4.4,7.044a19.333,19.333,0,0,0,1.867,2.286c0.519,0.4,1.382-.373,2.8.908C31.7,12.984,31.048,15,29.63,15ZM29.423,12.11c-0.933-1.042-1.728-.908-1.936-0.639a2.093,2.093,0,0,0,.38,1.748,1.612,1.612,0,0,0,1.383.74C29.838,13.959,30.356,13.153,29.423,12.11ZM25.582,7.372L24.4,5.6,27.276,1s1.233,1.69.575,2.708C27.568,4.142,26.445,5.967,25.582,7.372Zm-4.576,2.956A12.482,12.482,0,0,0,22.43,8.645l0.826,1.239c-0.428.65-.937,1.352-0.937,1.352s-0.933,1.378-1.694,2.488A2.426,2.426,0,0,1,18.344,15c-1.417,0-2.074-2.017-.138-3.765C19.624,9.956,20.487,10.732,21.006,10.329ZM18.551,12.11c-0.933,1.042-.415,1.849.173,1.849a1.612,1.612,0,0,0,1.383-.74,2.093,2.093,0,0,0,.38-1.748C20.28,11.2,19.485,11.068,18.551,12.11Z"/>
+ <path id="copy" d="M46,15H40a1,1,0,0,1-1-1V6a1,1,0,0,1,1-1h4.953C45,5,47,6.984,47,7.047V14A1,1,0,0,1,46,15ZM44,6V8h2ZM38,4.886V11H34a1,1,0,0,1-1-1V2a1,1,0,0,1,1-1h4.953C39,1,41,2.985,41,3.047v1.34H38.5A0.5,0.5,0,0,0,38,4.886ZM38,2V4h2Z"/>
+ <path id="paste" d="M59.5,15h-7A1.5,1.5,0,0,1,51,13.5v-9A1.5,1.5,0,0,1,52.5,3H54a2,2,0,1,1,4,0h1.5A1.5,1.5,0,0,1,61,4.5v9A1.5,1.5,0,0,1,59.5,15ZM58.682,4L57.61,3.5a1.613,1.613,0,0,0-3.219,0L53.318,4,52.781,5h6.437ZM58.82,5.688H54.074L51.059,7.428l2.849,4.935,6.574-3.8Z"/>
+ <rect id="zoomOut" x="67" y="7" width="10" height="2"/>
+ <path id="zoomIn" d="M93,9H89v4H87V9H83V7h4V3h2V7h4V9Z"/>
+</svg>
diff --git a/browser/themes/shared/menupanel.inc.css b/browser/themes/shared/menupanel.inc.css
index 266e1c83e..7517e4df0 100644
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -63,6 +63,11 @@ toolbarpaletteitem[place="palette"] > #feed-button {
-moz-image-region: rect(0px, 416px, 32px, 384px);
}
+#social-share-button[cui-areatype="menu-panel"],
+toolbarpaletteitem[place="palette"] > #social-share-button {
+ -moz-image-region: rect(0px, 448px, 32px, 416px);
+}
+
#characterencoding-button[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > #characterencoding-button {
-moz-image-region: rect(0px, 480px, 32px, 448px);
@@ -171,3 +176,8 @@ toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-in-button {
-moz-image-region: rect(0px, 96px, 16px, 80px);
}
+#add-share-provider {
+ list-style-image: url(chrome://browser/skin/menuPanel-small.svg);
+ -moz-image-region: rect(0px, 96px, 16px, 80px);
+}
+
diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css
index 86dce73a1..595e911b6 100644
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -271,6 +271,28 @@ html|*#webRTC-previewVideo {
}
}
+/* SOCIAL API */
+
+.popup-notification-icon[popupid="servicesInstall"] {
+ list-style-image: url(chrome://browser/skin/social/services-64.png);
+}
+
+.service-icon {
+ list-style-image: url(chrome://browser/skin/social/services-16.png);
+}
+
+%ifdef XP_MACOSX
+@media (min-resolution: 1.1dppx) {
+ .popup-notification-icon[popupid="servicesInstall"] {
+ list-style-image: url(chrome://browser/skin/social/services-64@2x.png);
+ }
+
+ .service-icon {
+ list-style-image: url(chrome://browser/skin/social/services-16@2x.png);
+ }
+}
+%endif
+
/* TRANSLATION */
.translation-icon {
diff --git a/browser/themes/shared/social/gear_clicked.png b/browser/themes/shared/social/gear_clicked.png
new file mode 100644
index 000000000..7c93aa767
--- /dev/null
+++ b/browser/themes/shared/social/gear_clicked.png
Binary files differ
diff --git a/browser/themes/shared/social/gear_default.png b/browser/themes/shared/social/gear_default.png
new file mode 100644
index 000000000..2a9c8e198
--- /dev/null
+++ b/browser/themes/shared/social/gear_default.png
Binary files differ
diff --git a/browser/themes/shared/social/social.inc.css b/browser/themes/shared/social/social.inc.css
new file mode 100644
index 000000000..31389b215
--- /dev/null
+++ b/browser/themes/shared/social/social.inc.css
@@ -0,0 +1,23 @@
+%if 0
+/* 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/. */
+%endif
+
+#manage-share-providers {
+ list-style-image: url("chrome://browser/skin/Toolbar.png");
+ -moz-image-region: rect(0, 468px, 18px, 450px);
+}
+
+#manage-share-providers > .toolbarbutton-icon {
+ min-height: 18px;
+ min-width: 18px;
+}
+
+.social-panel > .panel-arrowcontainer > .panel-arrowcontent {
+ padding: 0;
+}
+/* fixup corners for share panel */
+.social-panel > .social-panel-frame {
+ border-radius: inherit;
+}
diff --git a/browser/themes/shared/toolbarbuttons.inc.css b/browser/themes/shared/toolbarbuttons.inc.css
index c043b8192..b3b3ffcf8 100644
--- a/browser/themes/shared/toolbarbuttons.inc.css
+++ b/browser/themes/shared/toolbarbuttons.inc.css
@@ -64,6 +64,10 @@ toolbar[brighttext] #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarke
-moz-image-region: rect(0, 288px, 18px, 270px);
}
+#social-share-button[cui-areatype="toolbar"] {
+ -moz-image-region: rect(0px, 306px, 18px, 288px);
+}
+
#characterencoding-button[cui-areatype="toolbar"]{
-moz-image-region: rect(0, 324px, 18px, 306px);
}
@@ -238,6 +242,10 @@ toolbar[brighttext] #bookmarks-menu-button > .toolbarbutton-menubutton-dropmarke
-moz-image-region: rect(0, 576px, 36px, 540px);
}
+ #social-share-button[cui-areatype="toolbar"] {
+ -moz-image-region: rect(0, 612px, 36px, 576px);
+ }
+
#characterencoding-button[cui-areatype="toolbar"] {
-moz-image-region: rect(0, 648px, 36px, 612px);
}
diff --git a/browser/themes/windows/browser.css b/browser/themes/windows/browser.css
index b364dbc36..a0cdabfb2 100644
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1784,6 +1784,81 @@ html|span.ac-emphasize-text-url {
-moz-image-region: rect(0, 48px, 16px, 32px);
}
+/* social share panel */
+%include ../shared/social/social.inc.css
+
+.social-panel-frame {
+ border-radius: inherit;
+}
+
+.social-share-frame {
+ min-width: 756px;
+ height: 150px;
+}
+#share-container {
+ min-width: 756px;
+ background-color: white;
+ background-repeat: no-repeat;
+ background-position: center center;
+}
+#share-container[loading] {
+ background-image: url(chrome://browser/skin/tabbrowser/pendingpaint.png);
+}
+#share-container > browser {
+ transition: opacity 150ms ease-in-out;
+ opacity: 1;
+}
+#share-container[loading] > browser {
+ opacity: 0;
+}
+
+.social-share-toolbar {
+ border-bottom: 1px solid #e2e5e8;
+ padding: 2px;
+}
+
+#social-share-provider-buttons {
+ padding: 0;
+ margin: 0;
+}
+
+.share-provider-button {
+ padding: 5px;
+ margin: 2px;
+}
+
+.share-provider-button > .toolbarbutton-text {
+ display: none;
+}
+.share-provider-button > .toolbarbutton-icon {
+ width: 16px;
+ min-height: 16px;
+ max-height: 16px;
+}
+
+#social-share-panel {
+ min-height: 100px;
+ min-width: 766px;
+}
+
+#share-container,
+.social-share-frame {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: inherit;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: inherit;
+}
+
+#social-share-panel > .social-share-toolbar {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
+#social-share-provider-buttons {
+ border-top-left-radius: inherit;
+ border-top-right-radius: inherit;
+}
+
/* bookmarks menu-button */
#nav-bar #bookmarks-menu-button[cui-areatype="toolbar"]:not([overflowedItem=true]) > .toolbarbutton-menubutton-dropmarker > .dropmarker-icon {
diff --git a/browser/themes/windows/customizableui/panelUI.css b/browser/themes/windows/customizableui/panelUI.css
index a01a2f3e3..189a163f3 100644
--- a/browser/themes/windows/customizableui/panelUI.css
+++ b/browser/themes/windows/customizableui/panelUI.css
@@ -97,6 +97,14 @@ menuitem[type="checkbox"].subviewbutton {
padding-inline-start: 0;
}
+/* subviewbutton entries for social sidebars have images that come from external
+/* sources, and are not guaranteed to be the size we want, so force the size on
+/* those icons. */
+toolbarbutton.social-provider-menuitem > .toolbarbutton-icon {
+ width: 16px;
+ height: 16px;
+}
+
.subviewbutton:-moz-any([image],[targetURI],.cui-withicon, .restoreallitem, .bookmark-item)[checked="true"] > .toolbarbutton-icon {
visibility: hidden;
}
diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn
index e8db7eed2..410148645 100644
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -109,6 +109,8 @@ browser.jar:
* skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
* skin/classic/browser/preferences/in-content/dialog.css (preferences/in-content/dialog.css)
skin/classic/browser/preferences/applications.css (preferences/applications.css)
+ skin/classic/browser/social/services-16.png (social/services-16.png)
+ skin/classic/browser/social/services-64.png (social/services-64.png)
skin/classic/browser/tabbrowser/newtab.svg (tabbrowser/newtab.svg)
skin/classic/browser/tabbrowser/newtab-win7.svg (tabbrowser/newtab-win7.svg)
skin/classic/browser/tabbrowser/newtab-inverted.svg (tabbrowser/newtab-inverted.svg)