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