diff options
author | Moonchild <mcwerewolf@gmail.com> | 2018-05-25 22:46:40 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-25 22:46:40 +0200 |
commit | 8f354e52d09d1bf2fadfbbfc141d4f32241599a4 (patch) | |
tree | ca3fcaa2a72700b85221b2fe75bb7d46d5d424ce | |
parent | d5459982f0d1bab08b751dc140c4cbe047032617 (diff) | |
parent | 72bab89cf6c77c9ee95a977c306e6ce317b84835 (diff) | |
download | uxp-PM28.0.0a2_Unstable.tar.gz |
Merge pull request #386 from MoonchildProductions/revert-381-masterPM28.0.0a2_Unstable
Revert "Remove the Social API"
52 files changed, 2895 insertions, 16 deletions
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index ede62fd5e1..e432c511db 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 0000000000..596ede4b30 --- /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 0000000000..94a4e3dbd9 --- /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 9fa90b11c6..3061cccdd1 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 6ea057d93f..d0c3d11cd0 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 0000000000..b470efd3d5 --- /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 ac5bf9e9b6..f03f21c3f7 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 4b8ec864b9..8679bca831 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 4f1b48349c..485471ee37 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 5758cb023d..496e0d1118 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 097caf3670..955184f647 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 0000000000..b5fa6a5c4b --- /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 5ec92d79a8..03854f75d0 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 b77949ea7d..5e8df6ab2d 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 1baccd7103..967da3ebc1 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 3e00d385f9..3e83b081cd 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 077d9c0149..1b8fc02362 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 1a92133592..0b3302f8f1 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 1045977e87..6de17b64fd 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 46b8aabc70..f7f3e93399 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 4574c6a81e..a68f59fe38 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 a6a5789f4c..392462b453 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 0000000000..1569e01220 --- /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 0000000000..95f5e02591 --- /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 a7bbbc258f..852a4c9115 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 3e6b81512d..73d3844a2c 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 0037b5634b..289faa085b 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 1890278120..0bf023f353 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 5a83c74b2a..e8ac9163e2 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 27802843d8..98ba4e6ea8 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 0000000000..73e570aada --- /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 81caf94d6c..c57b592372 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 ba36da9951..b0bb054154 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 b5cdf246a6..dcc1e9dd92 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 0000000000..db28992e2d --- /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 266e1c83e2..7517e4df04 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 86dce73a18..595e911b68 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 Binary files differnew file mode 100644 index 0000000000..7c93aa767e --- /dev/null +++ b/browser/themes/shared/social/gear_clicked.png diff --git a/browser/themes/shared/social/gear_default.png b/browser/themes/shared/social/gear_default.png Binary files differnew file mode 100644 index 0000000000..2a9c8e1989 --- /dev/null +++ b/browser/themes/shared/social/gear_default.png diff --git a/browser/themes/shared/social/social.inc.css b/browser/themes/shared/social/social.inc.css new file mode 100644 index 0000000000..31389b2153 --- /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 c043b8192e..b3b3ffcf89 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 b364dbc36b..a0cdabfb28 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 a01a2f3e38..189a163f3f 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 e8db7eed28..410148645c 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) diff --git a/devtools/client/responsive.html/browser/tunnel.js b/devtools/client/responsive.html/browser/tunnel.js index 42eb010e7b..fdbfe89184 100644 --- a/devtools/client/responsive.html/browser/tunnel.js +++ b/devtools/client/responsive.html/browser/tunnel.js @@ -407,6 +407,8 @@ MessageManagerTunnel.prototype = { "PageInfo:", // Messages sent from printUtils.js "Printing:", + // Messages sent from browser-social.js + "Social:", "PageMetadata:", // Messages sent from viewSourceUtils.js "ViewSource:", @@ -425,6 +427,8 @@ MessageManagerTunnel.prototype = { "PageInfo:", // Messages sent to printUtils.js "Printing:", + // Messages sent to browser-social.js + "Social:", "PageMetadata:", // Messages sent to viewSourceUtils.js "ViewSource:", diff --git a/mobile/android/moz.configure b/mobile/android/moz.configure index 9f622f6ef4..0ab0b113e2 100644 --- a/mobile/android/moz.configure +++ b/mobile/android/moz.configure @@ -62,6 +62,7 @@ option(env='MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER', set_config('MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER', depends_if('MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER')(lambda _: True)) +imply_option('MOZ_SOCIAL', False) imply_option('MOZ_SERVICES_HEALTHREPORT', True) imply_option('MOZ_ANDROID_HISTORY', True) diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index b528f1dfb2..18e23ff2eb 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4853,6 +4853,18 @@ pref("memory.dump_reports_on_oom", false); // Number of stack frames to capture in createObjectURL for about:memory. pref("memory.blob_report.stack_frames", 0); +// comma separated list of domain origins (e.g. https://domain.com) that still +// need localStorage in the frameworker +pref("social.whitelist", "https://mozsocial.cliqz.com"); +// comma separated list of domain origins (e.g. https://domain.com) for +// directory websites (e.g. AMO) that can install providers for other sites +pref("social.directories", "https://activations.cdn.mozilla.net"); +// remote-install allows any website to activate a provider, with extended UI +// notifying user of installation. we can later pref off remote install if +// necessary. This does not affect whitelisted and directory installs. +pref("social.remote-install.enabled", true); +pref("social.toast-notifications.enabled", true); + // Disable idle observer fuzz, because only privileged content can access idle // observers (bug 780507). pref("dom.idle-observers-api.fuzz_time.disabled", true); diff --git a/old-configure.in b/old-configure.in index 13f6915b47..0d169d55fd 100644 --- a/old-configure.in +++ b/old-configure.in @@ -2245,6 +2245,7 @@ MOZ_JETPACK=1 MOZ_DEVTOOLS_SERVER=1 MOZ_DEVTOOLS= MOZ_PLACES=1 +MOZ_SOCIAL=1 MOZ_SERVICES_HEALTHREPORT=1 MOZ_SERVICES_SYNC=1 MOZ_SERVICES_CLOUDSYNC=1 @@ -5032,6 +5033,12 @@ if test "$MOZ_PLACES"; then AC_DEFINE(MOZ_PLACES) fi +dnl Build SocialAPI if required +AC_SUBST(MOZ_SOCIAL) +if test "$MOZ_SOCIAL"; then + AC_DEFINE(MOZ_SOCIAL) +fi + dnl Build Firefox Health Reporter Service AC_SUBST(MOZ_SERVICES_HEALTHREPORT) if test -n "$MOZ_SERVICES_HEALTHREPORT"; then diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js index b1be625190..dbaaf29a82 100644 --- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -588,6 +588,7 @@ Tester.prototype = { sidebar.setAttribute("src", "about:blank"); SelfSupportBackend.uninit(); + SocialShare.uninit(); } // Destroy BackgroundPageThumbs resources. diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json index 69a9ae44da..ade308cfac 100644 --- a/toolkit/components/telemetry/Histograms.json +++ b/toolkit/components/telemetry/Histograms.json @@ -6677,6 +6677,11 @@ "kind": "boolean", "description": "If we are on Windows and neither the Windows countryCode nor the geoip countryCode indicates we are in the US, set to false if they both agree on the value or true otherwise" }, + "SOCIAL_ENABLED_ON_SESSION": { + "expires_in_version": "never", + "kind": "flag", + "description": "Social has been enabled at least once on the current session" + }, "ENABLE_PRIVILEGE_EVER_CALLED": { "expires_in_version": "never", "kind": "flag", @@ -8963,6 +8968,30 @@ "description": "Scaling percentage for the display where the first window is opened (Linux only)", "cpp_guard": "XP_LINUX" }, + "SOCIAL_SIDEBAR_STATE": { + "expires_in_version": "never", + "kind": "boolean", + "description": "Social Sidebar state 0: closed, 1: opened. Toggling between providers will result in a higher opened rate." + }, + "SOCIAL_TOOLBAR_BUTTONS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 3, + "description": "Social toolbar button has been used (0:share, 1:status, 2:bookmark)" + }, + "SOCIAL_PANEL_CLICKS": { + "expires_in_version": "never", + "kind": "enumerated", + "n_values": 4, + "description": "Social content has been interacted with (0:share, 1:status, 2:bookmark, 3: sidebar)" + }, + "SOCIAL_SIDEBAR_OPEN_DURATION": { + "expires_in_version": "never", + "kind": "exponential", + "high": 10000000, + "n_buckets": 10, + "description": "Sidebar showing: seconds that the sidebar has been opened" + }, "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION": { "expires_in_version": "never", "kind": "exponential", diff --git a/toolkit/components/telemetry/histogram-whitelists.json b/toolkit/components/telemetry/histogram-whitelists.json index deb1bd5b3c..4861781998 100644 --- a/toolkit/components/telemetry/histogram-whitelists.json +++ b/toolkit/components/telemetry/histogram-whitelists.json @@ -610,6 +610,11 @@ "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN", "SLOW_ADDON_WARNING_RESPONSE_TIME", "SLOW_ADDON_WARNING_STATES", + "SOCIAL_ENABLED_ON_SESSION", + "SOCIAL_PANEL_CLICKS", + "SOCIAL_SIDEBAR_OPEN_DURATION", + "SOCIAL_SIDEBAR_STATE", + "SOCIAL_TOOLBAR_BUTTONS", "SPDY_CHUNK_RECVD", "SPDY_GOAWAY_LOCAL", "SPDY_GOAWAY_PEER", @@ -1496,6 +1501,11 @@ "SLOW_ADDON_WARNING_RESPONSE_TIME", "SLOW_ADDON_WARNING_STATES", "SLOW_SCRIPT_NOTICE_COUNT", + "SOCIAL_ENABLED_ON_SESSION", + "SOCIAL_PANEL_CLICKS", + "SOCIAL_SIDEBAR_OPEN_DURATION", + "SOCIAL_SIDEBAR_STATE", + "SOCIAL_TOOLBAR_BUTTONS", "SPDY_CHUNK_RECVD", "SPDY_GOAWAY_LOCAL", "SPDY_GOAWAY_PEER", diff --git a/toolkit/modules/Troubleshoot.jsm b/toolkit/modules/Troubleshoot.jsm index 42f3fb8094..e11d477746 100644 --- a/toolkit/modules/Troubleshoot.jsm +++ b/toolkit/modules/Troubleshoot.jsm @@ -83,6 +83,7 @@ const PREFS_WHITELIST = [ "services.sync.lastSync", "services.sync.numClients", "services.sync.engine.", + "social.enabled", "storage.vacuum.last.", "svg.", "toolkit.startup.recent_crashes", |