diff options
Diffstat (limited to 'browser/base')
-rw-r--r-- | browser/base/content/aboutProviderDirectory.xhtml | 60 | ||||
-rw-r--r-- | browser/base/content/aboutSocialError.xhtml | 111 | ||||
-rw-r--r-- | browser/base/content/browser-context.inc | 20 | ||||
-rw-r--r-- | browser/base/content/browser-sets.inc | 3 | ||||
-rw-r--r-- | browser/base/content/browser-social.js | 503 | ||||
-rw-r--r-- | browser/base/content/browser.css | 5 | ||||
-rwxr-xr-x | browser/base/content/browser.js | 10 | ||||
-rw-r--r-- | browser/base/content/browser.xul | 26 | ||||
-rw-r--r-- | browser/base/content/content.js | 31 | ||||
-rw-r--r-- | browser/base/content/nsContextMenu.js | 47 | ||||
-rw-r--r-- | browser/base/content/social-content.js | 172 | ||||
-rw-r--r-- | browser/base/jar.mn | 4 |
12 files changed, 983 insertions, 9 deletions
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) |