--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1287,16 +1287,19 @@ pref("services.sync.prefs.sync.security.
pref("services.sync.prefs.sync.security.default_personal_cert", true);
pref("services.sync.prefs.sync.security.tls.version.min", true);
pref("services.sync.prefs.sync.security.tls.version.max", true);
pref("services.sync.prefs.sync.signon.rememberSignons", true);
pref("services.sync.prefs.sync.spellchecker.dictionary", true);
pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
#endif
+// Developer edition preferences
+pref("browser.devedition.theme.enabled", false);
+
// Disable the error console
pref("devtools.errorconsole.enabled", false);
// Developer toolbar and GCLI preferences
pref("devtools.toolbar.enabled", true);
pref("devtools.toolbar.visible", false);
pref("devtools.commands.dir", "");
@@ -1599,16 +1602,17 @@ pref("loop.throttled", false);
pref("loop.enabled", true);
pref("loop.throttled", true);
pref("loop.soft_start_ticket_number", -1);
pref("loop.soft_start_hostname", "soft-start.loop.services.mozilla.com");
#endif
pref("loop.server", "https://loop.services.mozilla.com");
pref("loop.seenToS", "unseen");
+pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
pref("loop.legal.ToS_url", "https://call.mozilla.com/legal/terms/");
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
pref("loop.do_not_disturb", false);
pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg");
pref("loop.retry_delay.start", 60000);
pref("loop.retry_delay.limit", 300000);
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
pref("loop.feedback.product", "Loop");
@@ -1627,16 +1631,25 @@ pref("loop.rooms.enabled", false);
pref("loop.fxa_oauth.tokendata", "");
pref("loop.fxa_oauth.profile", "");
// serverURL to be assigned by services team
pref("services.push.serverURL", "wss://push.services.mozilla.com/");
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.
+#ifdef EARLY_BETA_OR_EARLIER
+pref("social.share.activationPanelEnabled", true);
+#else
+pref("social.share.activationPanelEnabled", false);
+#endif
+pref("social.shareDirectory", "https://activations.cdn.mozilla.net/sharePanel.html");
+
pref("dom.identity.enabled", false);
// Block insecure active content on https pages
pref("security.mixed_content.block_active_content", true);
// 1 = allow MITM for certificate pinning checks.
pref("security.cert_pinning.enforcement_level", 1);
new file mode 100644
--- /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>
--- a/browser/base/content/aboutSocialError.xhtml
+++ b/browser/base/content/aboutSocialError.xhtml
@@ -37,37 +37,36 @@
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource:///modules/Social.jsm");
let config = {
tryAgainCallback: reloadProvider
}
function parseQueryString() {
- let url = document.documentURI;
- let queryString = url.replace(/^about:socialerror\??/, "");
-
- let modeMatch = queryString.match(/mode=([^&]+)/);
- let mode = modeMatch && modeMatch[1] ? modeMatch[1] : "";
- let originMatch = queryString.match(/origin=([^&]+)/);
- config.origin = originMatch && originMatch[1] ? decodeURIComponent(originMatch[1]) : "";
+ let searchParams = new URLSearchParams(location.href.split("?")[1]);
+ let mode = searchParams.get("mode");
+ config.directory = searchParams.get("directory");
+ config.origin = searchParams.get("origin");
+ let encodedURL = searchParams.get("url");
+ let url = decodeURIComponent(encodedURL);
+ if (config.directory) {
+ let URI = Services.io.newURI(url, null, null);
+ config.origin = Services.scriptSecurityManager.getNoAppCodebasePrincipal(URI).origin;
+ }
switch (mode) {
case "compactInfo":
document.getElementById("btnTryAgain").style.display = 'none';
document.getElementById("btnCloseSidebar").style.display = 'none';
break;
case "tryAgainOnly":
document.getElementById("btnCloseSidebar").style.display = 'none';
//intentional fall-through
case "tryAgain":
- let urlMatch = queryString.match(/url=([^&]+)/);
- let encodedURL = urlMatch && urlMatch[1] ? urlMatch[1] : "";
- url = decodeURIComponent(encodedURL);
-
config.tryAgainCallback = loadQueryURL;
config.queryURL = url;
break;
case "workerFailure":
config.tryAgainCallback = reloadProvider;
break;
default:
break;
@@ -75,17 +74,17 @@
}
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;
+ let providerName = provider ? provider.name : config.origin;
// Sets up the error message
let msg = browserBundle.formatStringFromName("social.error.message", [productName, providerName], 2);
document.getElementById("main-error-msg").textContent = msg;
// Sets up the buttons' labels and accesskeys
let btnTryAgain = document.getElementById("btnTryAgain");
btnTryAgain.textContent = browserBundle.GetStringFromName("social.error.tryAgain.label");
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -67,18 +67,18 @@
accesskey="&openLinkInPrivateWindowCmd.accesskey;"
oncommand="gContextMenu.openLinkInPrivateWindow();"/>
<menuseparator id="context-sep-open"/>
<menuitem id="context-bookmarklink"
label="&bookmarkThisLinkCmd.label;"
accesskey="&bookmarkThisLinkCmd.accesskey;"
oncommand="gContextMenu.bookmarkLink();"/>
<menuitem id="context-sharelink"
- label="&shareLinkCmd.label;"
- accesskey="&shareLinkCmd.accesskey;"
+ label="&shareLink.label;"
+ accesskey="&shareLink.accesskey;"
oncommand="gContextMenu.shareLink();"/>
<menuitem id="context-savelink"
label="&saveLinkCmd.label;"
accesskey="&saveLinkCmd.accesskey;"
oncommand="gContextMenu.saveLink();"/>
<menu id="context-marklinkMenu" label="&social.marklinkMenu.label;"
accesskey="&social.marklinkMenu.accesskey;">
<menupopup/>
@@ -195,18 +195,18 @@
accesskey="©AudioURLCmd.accesskey;"
oncommand="gContextMenu.copyMediaLocation();"/>
<menuseparator id="context-sep-copyimage"/>
<menuitem id="context-saveimage"
label="&saveImageCmd.label;"
accesskey="&saveImageCmd.accesskey;"
oncommand="gContextMenu.saveMedia();"/>
<menuitem id="context-shareimage"
- label="&shareImageCmd.label;"
- accesskey="&shareImageCmd.accesskey;"
+ label="&shareImage.label;"
+ accesskey="&shareImage.accesskey;"
oncommand="gContextMenu.shareImage();"/>
<menuitem id="context-sendimage"
label="&emailImageCmd.label;"
accesskey="&emailImageCmd.accesskey;"
oncommand="gContextMenu.sendMedia();"/>
<menuitem id="context-setDesktopBackground"
label="&setDesktopBackgroundCmd.label;"
accesskey="&setDesktopBackgroundCmd.accesskey;"
@@ -220,18 +220,18 @@
accesskey="&viewImageDescCmd.accesskey;"
oncommand="gContextMenu.viewImageDesc(event);"
onclick="checkForMiddleClick(this, event);"/>
<menuitem id="context-savevideo"
label="&saveVideoCmd.label;"
accesskey="&saveVideoCmd.accesskey;"
oncommand="gContextMenu.saveMedia();"/>
<menuitem id="context-sharevideo"
- label="&shareVideoCmd.label;"
- accesskey="&shareVideoCmd.accesskey;"
+ label="&shareVideo.label;"
+ accesskey="&shareVideo.accesskey;"
oncommand="gContextMenu.shareVideo();"/>
<menuitem id="context-saveaudio"
label="&saveAudioCmd.label;"
accesskey="&saveAudioCmd.accesskey;"
oncommand="gContextMenu.saveMedia();"/>
<menuitem id="context-video-saveimage"
accesskey="&videoSaveImage.accesskey;"
label="&videoSaveImage.label;"
@@ -300,18 +300,18 @@
<menuseparator id="context-sep-selectall"/>
<menuitem id="context-keywordfield"
label="&keywordfield.label;"
accesskey="&keywordfield.accesskey;"
oncommand="AddKeywordForSearchField();"/>
<menuitem id="context-searchselect"
oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
<menuitem id="context-shareselect"
- label="&shareSelectCmd.label;"
- accesskey="&shareSelectCmd.accesskey;"
+ label="&shareSelect.label;"
+ accesskey="&shareSelect.accesskey;"
oncommand="gContextMenu.shareSelect(getBrowserSelection());"/>
<menuseparator id="frame-sep"/>
<menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
<menupopup>
<menuitem id="context-showonlythisframe"
label="&showOnlyThisFrameCmd.label;"
accesskey="&showOnlyThisFrameCmd.accesskey;"
oncommand="gContextMenu.showOnlyThisFrame();"/>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-devedition.js
@@ -0,0 +1,85 @@
+# 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/.
+
+/**
+ * Listeners for the DevEdition theme. This adds an extra stylesheet
+ * to browser.xul if a pref is set and no other themes are applied.
+ */
+let DevEdition = {
+ _prefName: "browser.devedition.theme.enabled",
+ _themePrefName: "general.skins.selectedSkin",
+ _lwThemePrefName: "lightweightThemes.isThemeSelected",
+ _devtoolsThemePrefName: "devtools.theme",
+
+ styleSheetLocation: "chrome://browser/skin/devedition.css",
+ styleSheet: null,
+
+ init: function () {
+ this._updateDevtoolsThemeAttribute();
+ this._updateStyleSheet();
+
+ // Listen for changes to all prefs except for complete themes.
+ // No need for this since changing a complete theme requires a
+ // restart.
+ Services.prefs.addObserver(this._lwThemePrefName, this, false);
+ Services.prefs.addObserver(this._prefName, this, false);
+ Services.prefs.addObserver(this._devtoolsThemePrefName, this, false);
+ },
+
+ observe: function (subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ if (data == this._devtoolsThemePrefName) {
+ this._updateDevtoolsThemeAttribute();
+ } else {
+ this._updateStyleSheet();
+ }
+ }
+ },
+
+ _updateDevtoolsThemeAttribute: function() {
+ // Set an attribute on root element to make it possible
+ // to change colors based on the selected devtools theme.
+ document.documentElement.setAttribute("devtoolstheme",
+ Services.prefs.getCharPref(this._devtoolsThemePrefName));
+ },
+
+ _updateStyleSheet: function() {
+ // Only try to apply the dev edition theme if it is preffered
+ // on and there are no other themes applied.
+ let lightweightThemeSelected = false;
+ try {
+ lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
+ } catch(e) {}
+
+ let defaultThemeSelected = false;
+ try {
+ defaultThemeSelected = Services.prefs.getCharPref(this._themePrefName) == "classic/1.0";
+ } catch(e) {}
+
+ let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
+ !lightweightThemeSelected && defaultThemeSelected;
+
+ if (deveditionThemeEnabled && !this.styleSheet) {
+ let styleSheetAttr = `href="${this.styleSheetLocation}" type="text/css"`;
+ let styleSheet = this.styleSheet = document.createProcessingInstruction(
+ 'xml-stylesheet', styleSheetAttr);
+ this.styleSheet.addEventListener("load", function onLoad() {
+ styleSheet.removeEventListener("load", onLoad);
+ ToolbarIconColor.inferFromText();
+ });
+ document.insertBefore(this.styleSheet, document.documentElement);
+ } else if (!deveditionThemeEnabled && this.styleSheet) {
+ this.styleSheet.remove();
+ this.styleSheet = null;
+ ToolbarIconColor.inferFromText();
+ }
+ },
+
+ uninit: function () {
+ Services.prefs.removeObserver(this._lwThemePrefName, this);
+ Services.prefs.removeObserver(this._prefName, this);
+ Services.prefs.removeObserver(this._devtoolsThemePrefName, this);
+ this.styleSheet = null;
+ }
+};
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -114,31 +114,32 @@
#ifdef E10S_TESTING_ONLY
<command id="Tools:RemoteWindow"
oncommand="OpenBrowserWindow({remote: true});"/>
<command id="Tools:NonRemoteWindow"
oncommand="OpenBrowserWindow({remote: false});"/>
#endif
<command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
<command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
- <command id="Social:SharePage" oncommand="SocialShare.sharePage();" disabled="true"/>
+ <command id="Social:SharePage" oncommand="SocialShare.sharePage();"/>
<command id="Social:ToggleSidebar" oncommand="SocialSidebar.toggleSidebar();" hidden="true"/>
<command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="true"/>
<command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/>
<command id="Chat:Focus" oncommand="Cu.import('resource:///modules/Chat.jsm', {}).Chat.focus(window);"/>
</commandset>
<commandset id="placesCommands">
<command id="Browser:ShowAllBookmarks"
oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/>
<command id="Browser:ShowAllHistory"
oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/>
</commandset>
<broadcasterset id="mainBroadcasterSet">
+ <broadcaster id="Social:PageShareOrMark" disabled="true"/>
<broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
oncommand="toggleSidebar('viewBookmarksSidebar');"/>
<!-- for both places and non-places, the sidebar lives at
chrome://browser/content/history/history-panel.xul so there are no
problems when switching between versions -->
<broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -43,16 +43,22 @@ XPCOMUtils.defineLazyGetter(this, "Creat
});
XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() {
let tmp = {};
Cu.import("resource:///modules/Social.jsm", tmp);
return tmp.CreateSocialMarkWidget;
});
+XPCOMUtils.defineLazyGetter(this, "hookWindowCloseForPanelClose", function() {
+ let tmp = {};
+ Cu.import("resource://gre/modules/MozSocialAPI.jsm", tmp);
+ return tmp.hookWindowCloseForPanelClose;
+});
+
SocialUI = {
_initialized: false,
// Called on delayed startup to initialize the UI
init: function SocialUI_init() {
if (this._initialized) {
return;
}
@@ -63,17 +69,17 @@ SocialUI = {
Services.obs.addObserver(this, "social:providers-changed", false);
Services.obs.addObserver(this, "social:provider-reload", false);
Services.obs.addObserver(this, "social:provider-enabled", false);
Services.obs.addObserver(this, "social:provider-disabled", false);
Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true);
- PanelUI.panel.addEventListener("popupshown", SocialUI.updatePanelState, true);
+ CustomizableUI.addListener(this);
// menupopups that list social providers. we only populate them when shown,
// and if it has not been done already.
document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
Social.init().then((update) => {
if (update)
@@ -96,18 +102,18 @@ SocialUI = {
Services.obs.removeObserver(this, "social:profile-changed");
Services.obs.removeObserver(this, "social:frameworker-error");
Services.obs.removeObserver(this, "social:providers-changed");
Services.obs.removeObserver(this, "social:provider-reload");
Services.obs.removeObserver(this, "social:provider-enabled");
Services.obs.removeObserver(this, "social:provider-disabled");
Services.prefs.removeObserver("social.toast-notifications.enabled", this);
+ CustomizableUI.removeListener(this);
- PanelUI.panel.removeEventListener("popupshown", SocialUI.updatePanelState, true);
document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
this._initialized = false;
},
observe: function SocialUI_observe(subject, topic, data) {
switch (topic) {
@@ -161,39 +167,40 @@ SocialUI = {
},
_providersChanged: function() {
SocialSidebar.clearProviderMenus();
SocialSidebar.update();
SocialShare.populateProviderMenu();
SocialStatus.populateToolbarPalette();
SocialMarks.populateToolbarPalette();
- SocialShare.update();
},
// This handles "ActivateSocialFeature" events fired against content documents
// in this window. If this activation happens from within Firefox, such as
// about:home or the share panel, we bypass the enable prompt. Any website
// activation, such as from the activations directory or a providers website
// will still get the prompt.
- _activationEventHandler: function SocialUI_activationHandler(e, aBypassUserEnable=false) {
+ _activationEventHandler: function SocialUI_activationHandler(e, options={}) {
let targetDoc;
let node;
if (e.target instanceof HTMLDocument) {
// version 0 support
targetDoc = e.target;
node = targetDoc.documentElement
} else {
targetDoc = e.target.ownerDocument;
node = e.target;
}
if (!(targetDoc instanceof HTMLDocument))
return;
- if (!aBypassUserEnable && targetDoc.defaultView != content)
+ // The share panel iframe will not match "content" so it passes a bypass
+ // flag
+ if (!options.bypassContentCheck && targetDoc.defaultView != content)
return;
// If we are in PB mode, we silently do nothing (bug 829404 exists to
// do something sensible here...)
if (PrivateBrowsingUtils.isWindowPrivate(window))
return;
// If the last event was received < 1s ago, ignore this one
@@ -219,21 +226,46 @@ SocialUI = {
return;
}
}
Social.installProvider(targetDoc, data, function(manifest) {
Social.activateFromOrigin(manifest.origin, function(provider) {
if (provider.sidebarURL) {
SocialSidebar.show(provider.origin);
}
+ 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 (!widget.areaType) {
+ 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) {
- openUILinkIn(provider.postActivationURL, "tab");
+ // 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"});
}
});
- }, aBypassUserEnable);
+ }, options);
},
showLearnMore: function() {
let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
openUILinkIn(url, "tab");
},
closeSocialPanelForLinkTraversal: function (target, linkNode) {
@@ -273,31 +305,59 @@ SocialUI = {
get enabled() {
// Returns whether social is enabled *for this window*.
if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window))
return false;
return Social.providers.length > 0;
},
- updatePanelState :function(event) {
- // we only want to update when the panel is initially opened, not during
- // multiview changes
- if (event.target != PanelUI.panel)
+ canShareOrMarkPage: function(aURI) {
+ // Bug 898706 we do not enable social in private sessions since frameworker
+ // would be shared between private and non-private windows
+ if (PrivateBrowsingUtils.isWindowPrivate(window))
+ return false;
+
+ return (aURI && (aURI.schemeIs('http') || aURI.schemeIs('https')));
+ },
+
+ onCustomizeEnd: function(aWindow) {
+ if (aWindow != window)
return;
- SocialUI.updateState();
+ // customization mode gets buttons out of sync with command updating, fix
+ // the disabled state
+ let canShare = this.canShareOrMarkPage(gBrowser.currentURI);
+ let shareButton = SocialShare.shareButton;
+ if (shareButton) {
+ if (canShare) {
+ shareButton.removeAttribute("disabled")
+ } else {
+ shareButton.setAttribute("disabled", "true")
+ }
+ }
+ // update the disabled state of the button based on the command
+ for (let node of SocialMarks.nodes) {
+ if (canShare) {
+ node.removeAttribute("disabled")
+ } else {
+ node.setAttribute("disabled", "true")
+ }
+ }
},
// called on tab/urlbar/location changes and after customization. Update
// anything that is tab specific.
updateState: function() {
+ if (location == "about:customizing")
+ return;
+ goSetCommandEnabled("Social:PageShareOrMark", this.canShareOrMarkPage(gBrowser.currentURI));
if (!SocialUI.enabled)
return;
+ // larger update that may change button icons
SocialMarks.update();
- SocialShare.update();
}
}
SocialFlyout = {
get panel() {
return document.getElementById("social-flyout-panel");
},
@@ -428,16 +488,22 @@ SocialFlyout = {
Cu.reportError(e);
}
}
});
}
}
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;
},
get panel() {
return document.getElementById("social-share-panel");
@@ -446,143 +512,107 @@ SocialShare = {
get iframe() {
// first element is our menu vbox.
if (this.panel.childElementCount == 1)
return null;
else
return this.panel.lastChild;
},
+ _activationHandler: function(event) {
+ if (!Services.prefs.getBoolPref("social.share.activationPanelEnabled"))
+ return;
+ SocialUI._activationEventHandler(event, { bypassContentCheck: true, bypassInstallPanel: true });
+ },
+
uninit: function () {
if (this.iframe) {
+ this.iframe.removeEventListener("ActivateSocialFeature", this._activationHandler, true, true);
this.iframe.remove();
}
},
_createFrame: function() {
let panel = this.panel;
- if (!SocialUI.enabled || this.iframe)
+ 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");
panel.appendChild(iframe);
+ this.iframe.addEventListener("ActivateSocialFeature", this._activationHandler, true, true);
this.populateProviderMenu();
},
getSelectedProvider: function() {
let provider;
let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin");
if (lastProviderOrigin) {
provider = Social._getProviderFromOrigin(lastProviderOrigin);
}
- // if they have a provider selected in the sidebar use that for the initial
- // default in share
- if (!provider)
- provider = SocialSidebar.provider;
- // if our provider has no shareURL, select the first one that does
- if (!provider || !provider.shareURL) {
- let providers = [p for (p of Social.providers) if (p.shareURL)];
- provider = providers.length > 0 && providers[0];
- }
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 = [p for (p of Social.providers) if (p.shareURL)];
let hbox = document.getElementById("social-share-provider-buttons");
- // selectable providers are inserted before the provider-menu seperator,
- // remove any menuitems in that area
- while (hbox.firstChild) {
+ // 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.firstChild != addButton) {
hbox.removeChild(hbox.firstChild);
}
- // reset our share toolbar
- // only show a selection if there is more than one
- if (!SocialUI.enabled || providers.length < 2) {
- this.panel.firstChild.hidden = true;
- return;
- }
let selectedProvider = this.getSelectedProvider();
for (let provider of providers) {
let button = document.createElement("toolbarbutton");
button.setAttribute("class", "toolbarbutton share-provider-button");
button.setAttribute("type", "radio");
button.setAttribute("group", "share-providers");
button.setAttribute("image", provider.iconURL);
- button.setAttribute("tooltiptext", provider.name);
+ button.setAttribute("tooltip", "share-button-tooltip");
button.setAttribute("origin", provider.origin);
- button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;");
+ button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
if (provider == selectedProvider) {
this.defaultButton = button;
}
- hbox.appendChild(button);
+ hbox.insertBefore(button, addButton);
}
if (!this.defaultButton) {
- this.defaultButton = hbox.firstChild
+ this.defaultButton = addButton;
}
this.defaultButton.setAttribute("checked", "true");
- this.panel.firstChild.hidden = false;
},
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;
},
- canSharePage: function(aURI) {
- // we do not enable sharing from private sessions
- if (PrivateBrowsingUtils.isWindowPrivate(window))
- return false;
-
- if (!aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https')))
- return false;
- return true;
- },
-
- update: function() {
- let widget = CustomizableUI.getWidget("social-share-button");
- if (!widget)
- return;
- let shareButton = widget.forWindow(window).node;
- // hidden state is based on available share providers and location of
- // button. It's always visible and disabled in the customization palette.
- shareButton.hidden = !SocialUI.enabled || (widget.areaType &&
- [p for (p of Social.providers) if (p.shareURL)].length == 0);
- let disabled = !widget.areaType || shareButton.hidden || !this.canSharePage(gBrowser.currentURI);
-
- // 1. update the relevent command's disabled state so the keyboard
- // shortcut only works when available.
- // 2. If the button has been relocated to a place that is not visible by
- // default (e.g. menu panel) then the disabled attribute will not update
- // correctly based on the command, so we update the attribute directly as.
- let cmd = document.getElementById("Social:SharePage");
- if (disabled) {
- cmd.setAttribute("disabled", "true");
- shareButton.setAttribute("disabled", "true");
- } else {
- cmd.removeAttribute("disabled");
- shareButton.removeAttribute("disabled");
- }
- },
-
_onclick: function() {
Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(0);
},
onShowing: function() {
this.anchor.setAttribute("open", "true");
this.iframe.addEventListener("click", this._onclick, true);
},
@@ -602,46 +632,44 @@ SocialShare = {
}
},
setErrorMessage: function() {
let iframe = this.iframe;
if (!iframe)
return;
- iframe.removeAttribute("src");
- iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
- encodeURIComponent(iframe.getAttribute("origin")),
- null, null, null, null);
+ let url;
+ let origin = iframe.getAttribute("origin");
+ if (!origin) {
+ // directory site is down
+ url = "about:socialerror?mode=tryAgainOnly&directory=1&url=" + encodeURIComponent(iframe.getAttribute("src"));
+ } else {
+ url = "about:socialerror?mode=compactInfo&origin=" + encodeURIComponent(origin);
+ }
+ iframe.webNavigation.loadURI(url, null, null, null, null);
sizeSocialPanelToContent(this.panel, iframe);
},
sharePage: function(providerOrigin, graphData, target) {
// 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;
- let provider;
- if (providerOrigin)
- provider = Social._getProviderFromOrigin(providerOrigin);
- else
- provider = this.getSelectedProvider();
- if (!provider || !provider.shareURL)
- return;
// 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 (!this.canSharePage(sharedURI))
+ if (!SocialUI.canShareOrMarkPage(sharedURI))
return;
// 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.
if (!pageData || sharedURI == gBrowser.currentURI) {
pageData = OpenGraphBuilder.getData(gBrowser);
@@ -653,42 +681,48 @@ SocialShare = {
}
}
// if this is a share of a selected item, get any microdata
if (!pageData.microdata && target) {
pageData.microdata = OpenGraphBuilder.getMicrodata(gBrowser, target);
}
this.currentShare = pageData;
+ let provider;
+ if (providerOrigin)
+ provider = Social._getProviderFromOrigin(providerOrigin);
+ else
+ provider = this.getSelectedProvider();
+ if (!provider || !provider.shareURL) {
+ this.showDirectory();
+ 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);
let size = provider.getPageSize("share");
if (size) {
- if (this._dynamicResizer) {
- this._dynamicResizer.stop();
- this._dynamicResizer = null;
- }
- let {width, height} = size;
- width += this.panel.boxObject.width - iframe.boxObject.width;
- height += this.panel.boxObject.height - iframe.boxObject.height;
- this.panel.sizeTo(width, height);
- } else {
- this._dynamicResizer = new DynamicResizeWatcher();
+ this._dynamicResizer.stop();
}
// if we've already loaded this provider/page share endpoint, we don't want
// to add another load event listener.
let reload = true;
let endpointMatch = shareEndpoint == iframe.getAttribute("src");
let docLoaded = iframe.contentDocument && iframe.contentDocument.readyState == "complete";
if (endpointMatch && docLoaded) {
reload = shareEndpoint != iframe.contentDocument.location.spec;
}
if (!reload) {
- if (this._dynamicResizer)
+ if (!size)
this._dynamicResizer.start(this.panel, iframe);
iframe.docShell.isActive = true;
iframe.docShell.isAppTab = true;
let evt = iframe.contentDocument.createEvent("CustomEvent");
evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
iframe.contentDocument.documentElement.dispatchEvent(evt);
} else {
// first time load, wait for load and dispatch after load
@@ -696,17 +730,23 @@ SocialShare = {
iframe.removeEventListener("load", panelBrowserOnload, true);
iframe.docShell.isActive = true;
iframe.docShell.isAppTab = true;
// to support standard share endpoints mimick window.open by setting
// window.opener, some share endpoints rely on w.opener to know they
// should close the window when done.
iframe.contentWindow.opener = iframe.contentWindow;
setTimeout(function() {
- if (SocialShare._dynamicResizer) { // may go null if hidden quickly
+ if (size) {
+ let panel = SocialShare.panel;
+ let {width, height} = size;
+ width += panel.boxObject.width - iframe.boxObject.width;
+ height += panel.boxObject.height - iframe.boxObject.height;
+ panel.sizeTo(width, height);
+ } else {
SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
}
}, 0);
let evt = iframe.contentDocument.createEvent("CustomEvent");
evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
iframe.contentDocument.documentElement.dispatchEvent(evt);
}, true);
}
@@ -717,20 +757,41 @@ SocialShare = {
if (purge > 0)
iframe.sessionHistory.PurgeHistory(purge);
}
// 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();
+ },
+ showDirectory: function() {
+ this._createFrame();
+ let iframe = this.iframe;
+ iframe.removeAttribute("origin");
+ iframe.addEventListener("load", function panelBrowserOnload(e) {
+ iframe.removeEventListener("load", panelBrowserOnload, true);
+ hookWindowCloseForPanelClose(iframe.contentWindow);
+ SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
+
+ iframe.addEventListener("unload", function panelBrowserOnload(e) {
+ iframe.removeEventListener("unload", panelBrowserOnload, true);
+ SocialShare._dynamicResizer.stop();
+ }, true);
+ }, true);
+ iframe.setAttribute("src", "about:providerdirectory");
+ this._openPanel();
+ },
+
+ _openPanel: function() {
let anchor = document.getAnonymousElementByAttribute(this.anchor, "class", "toolbarbutton-icon");
this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
- Social.setErrorListener(iframe, this.setErrorMessage.bind(this));
+ Social.setErrorListener(this.iframe, this.setErrorMessage.bind(this));
Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
}
};
SocialSidebar = {
_openStartTime: 0,
// Whether the sidebar can be shown for this window.
@@ -1301,30 +1362,37 @@ SocialStatus = {
/**
* SocialMarks
*
* Handles updates to toolbox and signals all buttons to update when necessary.
*/
SocialMarks = {
- update: function() {
- // querySelectorAll does not work on the menu panel the panel, so we have to
- // do this the hard way.
- let providers = SocialMarks.getProviders();
+ get nodes() {
+ let providers = [p for (p of Social.providers) if (p.markURL)];
for (let p of providers) {
let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin);
let widget = CustomizableUI.getWidget(widgetId);
if (!widget)
continue;
let node = widget.forWindow(window).node;
+ if (node)
+ yield node;
+ }
+ },
+ update: function() {
+ // querySelectorAll does not work on the menu panel, so we have to do this
+ // the hard way.
+ for (let node of this.nodes) {
// xbl binding is not complete on startup when buttons are not in toolbar,
// verify update is available
- if (node && node.update)
+ if (node.update) {
node.update();
+ }
}
},
getProviders: function() {
// only rely on providers that the user has placed in the UI somewhere. This
// also means that populateToolbarPalette must be called prior to using this
// method, otherwise you get a big fat zero. For our use case with context
// menu's, this is ok.
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -194,16 +194,17 @@ let gInitialPages = [
"about:home",
"about:privatebrowsing",
"about:welcomeback",
"about:sessionrestore"
];
#include browser-addons.js
#include browser-customization.js
+#include browser-devedition.js
#include browser-feeds.js
#include browser-fullScreen.js
#include browser-fullZoom.js
#include browser-loop.js
#include browser-places.js
#include browser-plugins.js
#include browser-safebrowsing.js
#include browser-social.js
@@ -782,17 +783,17 @@ function gKeywordURIFixup({ target: brow
};
gDNSService.asyncResolve(hostName, 0, onLookupComplete, Services.tm.mainThread);
}
// Called when a docshell has attempted to load a page in an incorrect process.
// This function is responsible for loading the page in the correct process.
function RedirectLoad({ target: browser, data }) {
- let tab = gBrowser._getTabForBrowser(browser);
+ let tab = gBrowser.getTabForBrowser(browser);
// Flush the tab state before getting it
TabState.flush(browser);
let tabState = JSON.parse(SessionStore.getTabState(tab));
if (data.historyIndex < 0) {
// Add a pseudo-history state for the new url to load
let newEntry = {
url: data.uri,
@@ -829,16 +830,17 @@ var gBrowserInit = {
// These routines add message listeners. They must run before
// loading the frame script to ensure that we don't miss any
// message sent between when the frame script is loaded and when
// the listener is registered.
DOMLinkHandler.init();
gPageStyleMenu.init();
LanguageDetectionListener.init();
BrowserOnClick.init();
+ DevEdition.init();
let mm = window.getGroupMessageManager("browsers");
mm.loadFrameScript("chrome://browser/content/content.js", true);
// initialize observers and listeners
// and give C++ access to gBrowser
XULBrowserWindow.init();
window.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -1386,16 +1388,18 @@ var gBrowserInit = {
BookmarkingUI.uninit();
TabsInTitlebar.uninit();
ToolbarIconColor.uninit();
BrowserOnClick.uninit();
+ DevEdition.uninit();
+
var enumerator = Services.wm.getEnumerator(null);
enumerator.getNext();
if (!enumerator.hasMoreElements()) {
document.persist("sidebar-box", "sidebarcommand");
document.persist("sidebar-box", "width");
document.persist("sidebar-box", "src");
document.persist("sidebar-title", "value");
}
@@ -3765,17 +3769,17 @@ var XULBrowserWindow = {
} else {
this.reloadCommand.removeAttribute("disabled");
}
if (gURLBar) {
URLBarSetURI(aLocationURI);
BookmarkingUI.onLocationChange();
- SocialUI.updateState();
+ SocialUI.updateState(location);
}
// Utility functions for disabling find
var shouldDisableFind = function shouldDisableFind(aDocument) {
let docElt = aDocument.documentElement;
return docElt && docElt.getAttribute("disablefastfind") == "true";
}
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -241,17 +241,21 @@
<panel id="social-share-panel"
class="social-panel"
type="arrow"
orient="horizontal"
onpopupshowing="SocialShare.onShowing()"
onpopuphidden="SocialShare.onHidden()"
hidden="true">
<vbox class="social-share-toolbar">
- <arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1"/>
+ <arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1">
+ <toolbarbutton id="add-share-provider" class="toolbarbutton share-provider-button" type="radio"
+ group="share-providers" tooltiptext="&findShareServices.label;"
+ oncommand="SocialShare.showDirectory()"/>
+ </arrowscrollbox>
</vbox>
</panel>
<panel id="social-notification-panel"
class="social-panel"
type="arrow"
hidden="true"
noautofocus="true"/>
@@ -491,16 +495,21 @@
<label class="tooltip-label" value="&forwardButton.tooltip;"/>
#ifdef XP_MACOSX
<label class="tooltip-label" value="&backForwardButtonMenuMac.tooltip;"/>
#else
<label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/>
#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
<hbox id="downloads-animation-container" mousethrough="always">
<vbox id="downloads-notification-anchor">
<vbox id="downloads-indicator-notification"/>
</vbox>
@@ -664,17 +673,17 @@
Should you need to add items to the toolbar here, make sure to also add them
to the default placements of buttons in CustomizableUI.jsm, so the
customization code doesn't get confused.
-->
<toolbar id="nav-bar" class="toolbar-primary chromeclass-toolbar"
aria-label="&navbarCmd.label;"
fullscreentoolbar="true" mode="icons" customizable="true"
iconsize="small"
- defaultset="urlbar-container,search-container,bookmarks-menu-button,downloads-button,home-button,loop-call-button,social-share-button,social-toolbar-item"
+ defaultset="urlbar-container,search-container,bookmarks-menu-button,downloads-button,home-button,loop-call-button"
customizationtarget="nav-bar-customization-target"
overflowable="true"
overflowbutton="nav-bar-overflow-button"
overflowtarget="widget-overflow-list"
overflowpanel="widget-overflow"
context="toolbar-context-menu">
<hbox id="nav-bar-customization-target" flex="1">
@@ -911,25 +920,16 @@
ondragover="homeButtonObserver.onDragOver(event)"
ondragenter="homeButtonObserver.onDragOver(event)"
ondrop="homeButtonObserver.onDrop(event)"
ondragexit="homeButtonObserver.onDragExit(event)"
key="goHome"
onclick="BrowserGoHome(event);"
cui-areatype="toolbar"
aboutHomeOverrideTooltip="&abouthome.pageTitle;"/>
-
- <toolbarbutton id="social-share-button"
- class="toolbarbutton-1 chromeclass-toolbar-additional"
- label="&sharePageCmd.label;"
- tooltiptext="&sharePageCmd.label;"
- cui-areatype="toolbar"
- removable="true"
- hidden="true"
- command="Social:SharePage"/>
</hbox>
<toolbarbutton id="nav-bar-overflow-button"
class="toolbarbutton-1 chromeclass-toolbar-additional overflow-button"
skipintoolbarset="true"
tooltiptext="&navbarOverflow.label;"/>
<toolbaritem id="PanelUI-button"
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -340,17 +340,17 @@ nsContextMenu.prototype = {
linkmenus = document.getElementsByClassName("context-marklink");
[m.hidden = !enableLinkMarkItems for (m of linkmenus)];
// 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.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);
},
--- a/browser/base/content/socialmarks.xml
+++ b/browser/base/content/socialmarks.xml
@@ -3,17 +3,17 @@
<bindings id="socialMarkBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="toolbarbutton-marks" display="xul:button"
extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
- <content disabled="true">
+ <content>
<xul:panel anonid="panel" hidden="true" type="arrow" class="social-panel"/>
<xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label"/>
<xul:label class="toolbarbutton-text" crop="right" flex="1"
xbl:inherits="value=label,accesskey,crop,wrap"/>
<xul:label class="toolbarbutton-multiline-text" flex="1"
xbl:inherits="xbl:text=label,accesskey,wrap"/>
</content>
<implementation implements="nsIDOMEventListener, nsIObserver">
@@ -108,26 +108,21 @@
this._dynamicResizer.stop();
this._dynamicResizer = null;
}
this.content.setAttribute("src", "about:blank");
// called during onhidden, make sure the docshell is updated
if (this._frame.docShell)
this._frame.docShell.createAboutBlankContentViewer(null);
- // do we have a savable page loaded?
- let aURI = gBrowser.currentURI;
- let disabled = !aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https'));
- // when overflowed in toolbar, we must have the attribute set
- if (disabled) {
- this.setAttribute("disabled", "true");
+ // disabled attr is set by Social:PageShareOrMark command
+ if (this.hasAttribute("disabled")) {
this.isMarked = false;
} else {
- this.removeAttribute("disabled");
- Social.isURIMarked(provider.origin, aURI, (isMarked) => {
+ Social.isURIMarked(provider.origin, gBrowser.currentURI, (isMarked) => {
this.isMarked = isMarked;
});
}
this.content.setAttribute("origin", provider.origin);
let panel = this.panel;
// if customization is currently happening, we may not have a panel
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -384,17 +384,17 @@
// When not using remote browsers, we can take a fast path by getting
// directly from the content window to the browser without looping
// over all browsers.
if (!gMultiProcessBrowser) {
let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
- return this._getTabForBrowser(browser);
+ return this.getTabForBrowser(browser);
}
for (let i = 0; i < this.browsers.length; i++) {
// NB: We use contentWindowAsCPOW so that this code works both
// for remote browsers as well. aWindow may be a CPOW.
if (this.browsers[i].contentWindowAsCPOW == aWindow)
return this.tabs[i];
}
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -304,16 +304,17 @@ skip-if = e10s
[browser_contextSearchTabPosition.js]
skip-if = os == "mac" || e10s # bug 967013, bug 926729
[browser_ctrlTab.js]
skip-if = e10s # Bug ????? - thumbnail captures need e10s love (tabPreviews_capture fails with Argument 1 of CanvasRenderingContext2D.drawWindow does not implement interface Window.)
[browser_customize_popupNotification.js]
skip-if = e10s
[browser_datareporting_notification.js]
run-if = datareporting
+[browser_devedition.js]
[browser_devices_get_user_media.js]
skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent?
[browser_devices_get_user_media_about_urls.js]
skip-if = e10s # Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent?
[browser_discovery.js]
skip-if = e10s # Bug 918663 - DOMLinkAdded events don't make their way to chrome
[browser_duplicateIDs.js]
[browser_drag.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_devedition.js
@@ -0,0 +1,66 @@
+/*
+ * Testing changes for Developer Edition theme.
+ * A special stylesheet should be added to the browser.xul document
+ * when browser.devedition.theme.enabled is set to true and no themes
+ * are applied.
+ */
+
+const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled";
+const PREF_THEME = "general.skins.selectedSkin";
+const PREF_LWTHEME = "lightweightThemes.isThemeSelected";
+const PREF_DEVTOOLS_THEME = "devtools.theme";
+
+registerCleanupFunction(() => {
+ // Set preferences back to their original values
+ Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
+ Services.prefs.clearUserPref(PREF_THEME);
+ Services.prefs.clearUserPref(PREF_LWTHEME);
+ Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+});
+
+function test() {
+ waitForExplicitFinish();
+ startTests();
+}
+
+function startTests() {
+ ok (!DevEdition.styleSheet, "There is no devedition style sheet by default.");
+
+ info ("Setting browser.devedition.theme.enabled to true.");
+ Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
+ ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set.");
+
+ info ("Adding a lightweight theme.");
+ Services.prefs.setBoolPref(PREF_LWTHEME, true);
+ ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied.");
+
+ info ("Removing a lightweight theme.");
+ Services.prefs.setBoolPref(PREF_LWTHEME, false);
+ ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
+
+ // There are no listeners for the complete theme pref since applying the theme
+ // requires a restart.
+ info ("Setting general.skins.selectedSkin to a custom string.");
+ Services.prefs.setCharPref(PREF_THEME, "custom-theme");
+ ok (DevEdition.styleSheet, "The devedition stylesheet is still here when a complete theme is added.");
+
+ info ("Resetting general.skins.selectedSkin to default value.");
+ Services.prefs.clearUserPref(PREF_THEME);
+ ok (DevEdition.styleSheet, "The devedition stylesheet is still here when a complete theme is removed.");
+
+ info ("Setting browser.devedition.theme.enabled to false.");
+ Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
+ ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed.");
+
+ info ("Checking :root attributes based on devtools theme.");
+ is (document.documentElement.getAttribute("devtoolstheme"), "light",
+ "The documentElement has an attribute based on devtools theme.");
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+ is (document.documentElement.getAttribute("devtoolstheme"), "dark",
+ "The documentElement has an attribute based on devtools theme.");
+ Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
+ is (document.documentElement.getAttribute("devtoolstheme"), "light",
+ "The documentElement has an attribute based on devtools theme.");
+
+ finish();
+}
--- a/browser/base/content/test/social/browser.ini
+++ b/browser/base/content/test/social/browser.ini
@@ -6,16 +6,17 @@ support-files =
head.js
opengraph/og_invalid_url.html
opengraph/opengraph.html
opengraph/shortlink_linkrel.html
opengraph/shorturl_link.html
opengraph/shorturl_linkrel.html
microdata.html
share.html
+ share_activate.html
social_activate.html
social_activate_iframe.html
social_chat.html
social_crash_content_helper.js
social_flyout.html
social_mark.html
social_panel.html
social_postActivation.html
--- a/browser/base/content/test/social/browser_aboutHome_activation.js
+++ b/browser/base/content/test/social/browser_aboutHome_activation.js
@@ -14,17 +14,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
let snippet =
' <script>' +
' var manifest = {' +
' "name": "Demo Social Service",' +
' "origin": "https://example.com",' +
' "iconURL": "chrome://branding/content/icon16.png",' +
' "icon32URL": "chrome://branding/content/favicon32.png",' +
' "icon64URL": "chrome://branding/content/icon64.png",' +
-' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
' };' +
' function activateProvider(node) {' +
' node.setAttribute("data-service", JSON.stringify(manifest));' +
' var event = new CustomEvent("ActivateSocialFeature");' +
' node.dispatchEvent(event);' +
' }' +
' </script>' +
@@ -36,17 +36,17 @@ let snippet =
let snippet2 =
' <script>' +
' var manifest = {' +
' "name": "Demo Social Service",' +
' "origin": "https://example.com",' +
' "iconURL": "chrome://branding/content/icon16.png",' +
' "icon32URL": "chrome://branding/content/favicon32.png",' +
' "icon64URL": "chrome://branding/content/icon64.png",' +
-' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+' "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
' "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
' "oneclick": true' +
' };' +
' function activateProvider(node) {' +
' node.setAttribute("data-service", JSON.stringify(manifest));' +
' var event = new CustomEvent("ActivateSocialFeature");' +
' node.dispatchEvent(event);' +
' }' +
--- a/browser/base/content/test/social/browser_share.js
+++ b/browser/base/content/test/social/browser_share.js
@@ -5,21 +5,57 @@ let baseURL = "https://example.com/brows
let manifest = { // normal provider
name: "provider 1",
origin: "https://example.com",
workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
shareURL: "https://example.com/browser/browser/base/content/test/social/share.html"
};
+let activationPage = "https://example.com/browser/browser/base/content/test/social/share_activate.html";
+
+function sendActivationEvent(subframe) {
+ // hack Social.lastEventReceived so we don't hit the "too many events" check.
+ Social.lastEventReceived = 0;
+ let doc = subframe.contentDocument;
+ // if our test has a frame, use it
+ let button = doc.getElementById("activation");
+ ok(!!button, "got the activation button");
+ EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView);
+}
+
+function promiseShareFrameEvent(iframe, eventName) {
+ let deferred = Promise.defer();
+ iframe.addEventListener(eventName, function load() {
+ info("page load is " + iframe.contentDocument.location.href);
+ if (iframe.contentDocument.location.href != "data:text/plain;charset=utf8,") {
+ iframe.removeEventListener(eventName, load, true);
+ deferred.resolve();
+ }
+ }, true);
+ return deferred.promise;
+}
function test() {
waitForExplicitFinish();
-
- runSocialTests(tests);
+ Services.prefs.setCharPref("social.shareDirectory", activationPage);
+ registerCleanupFunction(function () {
+ Services.prefs.clearUserPref("social.directories");
+ Services.prefs.clearUserPref("social.shareDirectory");
+ Services.prefs.clearUserPref("social.share.activationPanelEnabled");
+ });
+ runSocialTests(tests, undefined, function(next) {
+ let shareButton = SocialShare.shareButton;
+ if (shareButton) {
+ CustomizableUI.removeWidgetFromArea("social-share-button", CustomizableUI.AREA_NAVBAR)
+ shareButton.remove();
+ }
+ ok(CustomizableUI.inDefaultState, "Should start in default state.");
+ next();
+ });
}
let corpus = [
{
url: baseURL+"opengraph/opengraph.html",
options: {
// og:title
title: ">This is my title<",
@@ -67,26 +103,16 @@ let corpus = [
options: {
previews: ["http://example.com/1234/56789.jpg"],
url: "http://www.example.com/photos/56789/",
shortUrl: "http://imshort/p/abcde"
}
}
];
-function loadURLInTab(url, callback) {
- info("Loading tab with "+url);
- let tab = gBrowser.selectedTab = gBrowser.addTab(url);
- tab.linkedBrowser.addEventListener("load", function listener() {
- is(tab.linkedBrowser.currentURI.spec, url, "tab loaded")
- tab.linkedBrowser.removeEventListener("load", listener, true);
- executeSoon(function() { callback(tab) });
- }, true);
-}
-
function hasoptions(testOptions, options) {
let msg;
for (let option in testOptions) {
let data = testOptions[option];
info("data: "+JSON.stringify(data));
let message_data = options[option];
info("message_data: "+JSON.stringify(message_data));
if (Array.isArray(data)) {
@@ -100,40 +126,49 @@ function hasoptions(testOptions, options
}
}
var tests = {
testShareDisabledOnActivation: function(next) {
// starting on about:blank page, share should be visible but disabled when
// adding provider
is(gBrowser.contentDocument.location.href, "about:blank");
+
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+
SocialService.addProvider(manifest, function(provider) {
is(SocialUI.enabled, true, "SocialUI is enabled");
checkSocialUI();
// share should not be enabled since we only have about:blank page
let shareButton = SocialShare.shareButton;
- is(shareButton.disabled, true, "share button is disabled");
// verify the attribute for proper css
is(shareButton.getAttribute("disabled"), "true", "share button attribute is disabled");
// button should be visible
is(shareButton.hidden, false, "share button is visible");
SocialService.disableProvider(manifest.origin, next);
});
},
testShareEnabledOnActivation: function(next) {
// starting from *some* page, share should be visible and enabled when
// activating provider
+ // initialize the button into the navbar
+ CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+ // ensure correct state
+ SocialUI.onCustomizeEnd(window);
+
let testData = corpus[0];
- loadURLInTab(testData.url, function(tab) {
+ addTab(testData.url, function(tab) {
SocialService.addProvider(manifest, function(provider) {
is(SocialUI.enabled, true, "SocialUI is enabled");
checkSocialUI();
// share should not be enabled since we only have about:blank page
let shareButton = SocialShare.shareButton;
- is(shareButton.disabled, false, "share button is enabled");
// verify the attribute for proper css
ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
// button should be visible
is(shareButton.hidden, false, "share button is visible");
gBrowser.removeTab(tab);
next();
});
});
@@ -142,19 +177,19 @@ var tests = {
let provider = Social._getProviderFromOrigin(manifest.origin);
let port = provider.getWorkerPort();
ok(port, "provider has a port");
let testTab;
let testIndex = 0;
let testData = corpus[testIndex++];
function runOneTest() {
- loadURLInTab(testData.url, function(tab) {
+ addTab(testData.url, function(tab) {
testTab = tab;
- SocialShare.sharePage();
+ SocialShare.sharePage(manifest.origin);
});
}
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "got-share-data-message":
gBrowser.removeTab(testTab);
@@ -236,10 +271,53 @@ var tests = {
let url = "https://example.com/browser/browser/base/content/test/social/microdata.html"
addTab(url, function(tab) {
testTab = tab;
let doc = tab.linkedBrowser.contentDocument;
target = doc.getElementById("simple-hcard");
SocialShare.sharePage(manifest.origin, null, target);
});
});
+ },
+ testSharePanelActivation: function(next) {
+ let testTab;
+ // cleared in the cleanup function
+ Services.prefs.setCharPref("social.directories", "https://example.com");
+ Services.prefs.setBoolPref("social.share.activationPanelEnabled", true);
+ // make the iframe so we can wait on the load
+ SocialShare._createFrame();
+ let iframe = SocialShare.iframe;
+
+ promiseShareFrameEvent(iframe, "load").then(() => {
+ let subframe = iframe.contentDocument.getElementById("activation-frame");
+ waitForCondition(() => {
+ // sometimes the iframe is ready before the panel is open, we need to
+ // wait for both conditions
+ return SocialShare.panel.state == "open"
+ && subframe.contentDocument
+ && subframe.contentDocument.readyState == "complete";
+ }, () => {
+ is(subframe.contentDocument.location.href, activationPage, "activation page loaded");
+ promiseObserverNotified("social:provider-enabled").then(() => {
+ let provider = Social._getProviderFromOrigin(manifest.origin);
+ let port = provider.getWorkerPort();
+ ok(!!port, "got port");
+ port.onmessage = function (e) {
+ let topic = e.data.topic;
+ switch (topic) {
+ case "got-share-data-message":
+ ok(true, "share completed");
+ gBrowser.removeTab(testTab);
+ SocialService.uninstallProvider(manifest.origin, next);
+ break;
+ }
+ }
+ port.postMessage({topic: "test-init"});
+ });
+ sendActivationEvent(subframe);
+ }, "share panel did not open and load share page");
+ });
+ addTab(activationPage, function(tab) {
+ testTab = tab;
+ SocialShare.sharePage();
+ });
}
}
--- a/browser/base/content/test/social/browser_social_chatwindow.js
+++ b/browser/base/content/test/social/browser_social_chatwindow.js
@@ -23,67 +23,80 @@ let manifests = [
name: "provider@test2",
origin: "https://test2.example.com",
sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?test2",
workerURL: "https://test2.example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "chrome://branding/content/icon48.png"
}
];
+let ports = [];
+function getProviderPort(provider) {
+ let port = provider.getWorkerPort();
+ ok(port, "provider has a port");
+ ports.push(port);
+ return port;
+}
let chatId = 0;
function openChat(provider, callback) {
let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
- let port = provider.getWorkerPort();
+ let port = getProviderPort(provider);
port.onmessage = function(e) {
if (e.data.topic == "got-chatbox-message") {
- port.close();
callback();
}
}
let url = chatUrl + "?" + (chatId++);
port.postMessage({topic: "test-init"});
port.postMessage({topic: "test-worker-chat", data: url});
gURLsNotRemembered.push(url);
+ return port;
}
function windowHasChats(win) {
return !!getChatBar().firstElementChild;
}
function test() {
requestLongerTimeout(2); // only debug builds seem to need more time...
waitForExplicitFinish();
let oldwidth = window.outerWidth; // we futz with these, so we restore them
let oldleft = window.screenX;
window.moveTo(0, window.screenY)
let postSubTest = function(cb) {
+ // ensure ports are closed
+ for (let port of ports) {
+ port.close()
+ ok(port._closed, "port closed");
+ }
+ ports = [];
+
let chats = document.getElementById("pinnedchats");
ok(chats.children.length == 0, "no chatty children left behind");
cb();
};
runSocialTestWithProvider(manifests, function (finishcb) {
ok(Social.enabled, "Social is enabled");
- ok(Social.providers[0].getWorkerPort(), "provider 0 has port");
- ok(Social.providers[1].getWorkerPort(), "provider 1 has port");
- ok(Social.providers[2].getWorkerPort(), "provider 2 has port");
+ ok(getProviderPort(Social.providers[0]), "provider 0 has port");
+ ok(getProviderPort(Social.providers[1]), "provider 1 has port");
+ ok(getProviderPort(Social.providers[2]), "provider 2 has port");
SocialSidebar.show();
runSocialTests(tests, undefined, postSubTest, function() {
window.moveTo(oldleft, window.screenY)
window.resizeTo(oldwidth, window.outerHeight);
finishcb();
});
});
}
var tests = {
testOpenCloseChat: function(next) {
let chats = document.getElementById("pinnedchats");
- let port = SocialSidebar.provider.getWorkerPort();
- ok(port, "provider has a port");
+ let port = getProviderPort(SocialSidebar.provider);
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "got-sidebar-message":
port.postMessage({topic: "test-chatbox-open"});
break;
case "got-chatbox-visibility":
if (e.data.result == "hidden") {
@@ -91,17 +104,16 @@ var tests = {
chats.selectedChat.toggle();
} else if (e.data.result == "shown") {
ok(true, "chatbox got shown");
// close it now
let content = chats.selectedChat.content;
content.addEventListener("unload", function chatUnload() {
content.removeEventListener("unload", chatUnload, true);
ok(true, "got chatbox unload on close");
- port.close();
next();
}, true);
chats.selectedChat.close();
}
break;
case "got-chatbox-message":
ok(true, "got chatbox message");
ok(e.data.result == "ok", "got chatbox windowRef result: "+e.data.result);
@@ -109,42 +121,39 @@ var tests = {
break;
}
}
port.postMessage({topic: "test-init", data: { id: 1 }});
},
testWorkerChatWindow: function(next) {
const chatUrl = SocialSidebar.provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
let chats = document.getElementById("pinnedchats");
- let port = SocialSidebar.provider.getWorkerPort();
- ok(port, "provider has a port");
+ let port = getProviderPort(SocialSidebar.provider);
port.postMessage({topic: "test-init"});
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "got-chatbox-message":
ok(true, "got a chat window opened");
ok(chats.selectedChat, "chatbox from worker opened");
while (chats.selectedChat) {
chats.selectedChat.close();
}
ok(!chats.selectedChat, "chats are all closed");
gURLsNotRemembered.push(chatUrl);
- port.close();
next();
break;
}
}
ok(!chats.selectedChat, "chats are all closed");
port.postMessage({topic: "test-worker-chat", data: chatUrl});
},
testCloseSelf: function(next) {
let chats = document.getElementById("pinnedchats");
- let port = SocialSidebar.provider.getWorkerPort();
- ok(port, "provider has a port");
+ let port = getProviderPort(SocialSidebar.provider);
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "test-init-done":
port.postMessage({topic: "test-chatbox-open"});
break;
case "got-chatbox-visibility":
is(e.data.result, "shown", "chatbox shown");
@@ -152,31 +161,32 @@ var tests = {
let chat = chats.selectedChat;
ok(chat.parentNode, "chat has a parent node before it is closed");
// ask it to close itself.
let doc = chat.contentDocument;
let evt = doc.createEvent("CustomEvent");
evt.initCustomEvent("socialTest-CloseSelf", true, true, {});
doc.documentElement.dispatchEvent(evt);
ok(!chat.parentNode, "chat is now closed");
- port.close();
next();
break;
}
}
port.postMessage({topic: "test-init", data: { id: 1 }});
},
// Check what happens when you close the only visible chat.
testCloseOnlyVisible: function(next) {
let chatbar = getChatBar();
let chatWidth = undefined;
let num = 0;
is(chatbar.childNodes.length, 0, "chatbar starting empty");
is(chatbar.menupopup.childNodes.length, 0, "popup starting empty");
+ let port = getProviderPort(SocialSidebar.provider);
+ port.postMessage({topic: "test-init"});
makeChat("normal", "first chat", function() {
// got the first one.
checkPopup();
ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible");
// we kinda cheat here and get the width of the first chat, assuming
// that all future chats will have the same width when open.
chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat);
@@ -197,107 +207,102 @@ var tests = {
closeAllChats();
next();
});
});
});
},
testShowWhenCollapsed: function(next) {
- let port = SocialSidebar.provider.getWorkerPort();
+ let port = getProviderPort(SocialSidebar.provider);
port.postMessage({topic: "test-init"});
get3ChatsForCollapsing("normal", function(first, second, third) {
let chatbar = getChatBar();
chatbar.showChat(first);
ok(!first.collapsed, "first should no longer be collapsed");
- ok(second.collapsed || third.collapsed, false, "one of the others should be collapsed");
+ is(second.collapsed || third.collapsed, true, "one of the others should be collapsed");
closeAllChats();
- port.close();
next();
});
},
testOnlyOneCallback: function(next) {
let chats = document.getElementById("pinnedchats");
- let port = SocialSidebar.provider.getWorkerPort();
+ let port = getProviderPort(SocialSidebar.provider);
let numOpened = 0;
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "test-init-done":
port.postMessage({topic: "test-chatbox-open"});
break;
case "chatbox-opened":
numOpened += 1;
port.postMessage({topic: "ping"});
break;
case "pong":
executeSoon(function() {
is(numOpened, 1, "only got one open message");
chats.selectedChat.close();
- port.close();
next();
});
}
}
port.postMessage({topic: "test-init", data: { id: 1 }});
},
testMultipleProviderChat: function(next) {
// test incomming chats from all providers
- openChat(Social.providers[0], function() {
- openChat(Social.providers[1], function() {
- openChat(Social.providers[2], function() {
+ let port0 = openChat(Social.providers[0], function() {
+ let port1 = openChat(Social.providers[1], function() {
+ let port2 = openChat(Social.providers[2], function() {
let chats = document.getElementById("pinnedchats");
waitForCondition(function() chats.children.length == Social.providers.length,
function() {
ok(true, "one chat window per provider opened");
// test logout of a single provider
- let provider = Social.providers[2];
- let port = provider.getWorkerPort();
- port.postMessage({topic: "test-logout"});
+ port2.postMessage({topic: "test-logout"});
waitForCondition(function() chats.children.length == Social.providers.length - 1,
function() {
closeAllChats();
waitForCondition(function() chats.children.length == 0,
function() {
ok(!chats.selectedChat, "multiprovider chats are all closed");
- port.close();
next();
},
"chat windows didn't close");
},
"chat window didn't close");
}, "chat windows did not open");
});
});
});
},
// XXX - note this must be the last test until we restore the login state
// between tests...
testCloseOnLogout: function(next) {
const chatUrl = SocialSidebar.provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
let port = SocialSidebar.provider.getWorkerPort();
+ ports.push(port);
ok(port, "provider has a port");
let opened = false;
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "test-init-done":
info("open first chat window");
port.postMessage({topic: "test-worker-chat", data: chatUrl});
break;
case "got-chatbox-message":
ok(true, "got a chat window opened");
if (opened) {
port.postMessage({topic: "test-logout"});
waitForCondition(function() document.getElementById("pinnedchats").firstChild == null,
function() {
- port.close();
next();
},
"chat windows didn't close");
} else {
// open a second chat window
opened = true;
port.postMessage({topic: "test-worker-chat", data: chatUrl+"?id=1"});
}
--- a/browser/base/content/test/social/browser_social_errorPage.js
+++ b/browser/base/content/test/social/browser_social_errorPage.js
@@ -10,208 +10,217 @@ function gc() {
}
let openChatWindow = Cu.import("resource://gre/modules/MozSocialAPI.jsm", {}).openChatWindow;
// Support for going on and offline.
// (via browser/base/content/test/browser_bookmark_titles.js)
let origProxyType = Services.prefs.getIntPref('network.proxy.type');
+function toggleOfflineStatus(goOffline) {
+ // Bug 968887 fix. when going on/offline, wait for notification before continuing
+ let deferred = Promise.defer();
+ if (!goOffline) {
+ Services.prefs.setIntPref('network.proxy.type', origProxyType);
+ }
+ if (goOffline != Services.io.offline) {
+ info("initial offline state " + Services.io.offline);
+ let expect = !Services.io.offline;
+ Services.obs.addObserver(function offlineChange(subject, topic, data) {
+ Services.obs.removeObserver(offlineChange, "network:offline-status-changed");
+ info("offline state changed to " + Services.io.offline);
+ is(expect, Services.io.offline, "network:offline-status-changed successful toggle");
+ deferred.resolve();
+ }, "network:offline-status-changed", false);
+ BrowserOffline.toggleOfflineStatus();
+ } else {
+ deferred.resolve();
+ }
+ if (goOffline) {
+ Services.prefs.setIntPref('network.proxy.type', 0);
+ // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
+ Services.cache2.clear();
+ }
+ return deferred.promise;
+}
+
function goOffline() {
// Simulate a network outage with offline mode. (Localhost is still
// accessible in offline mode, so disable the test proxy as well.)
- if (!Services.io.offline)
- BrowserOffline.toggleOfflineStatus();
- Services.prefs.setIntPref('network.proxy.type', 0);
- // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
- Services.cache2.clear();
+ return toggleOfflineStatus(true);
}
function goOnline(callback) {
- Services.prefs.setIntPref('network.proxy.type', origProxyType);
- if (Services.io.offline)
- BrowserOffline.toggleOfflineStatus();
- if (callback)
- callback();
+ return toggleOfflineStatus(false);
}
function openPanel(url, panelCallback, loadCallback) {
// open a flyout
SocialFlyout.open(url, 0, panelCallback);
- SocialFlyout.panel.firstChild.addEventListener("load", function panelLoad(evt) {
- if (evt.target != SocialFlyout.panel.firstChild.contentDocument) {
- return;
- }
- SocialFlyout.panel.firstChild.removeEventListener("load", panelLoad, true);
- loadCallback();
- }, true);
+ // wait for both open and loaded before callback. Since the test doesn't close
+ // the panel between opens, we cannot rely on events here. We need to ensure
+ // popupshown happens before we finish out the tests.
+ waitForCondition(function() {
+ return SocialFlyout.panel.state == "open" &&
+ SocialFlyout.iframe.contentDocument.readyState == "complete";
+ },
+ function () { executeSoon(loadCallback) },
+ "flyout is open and loaded");
}
function openChat(url, panelCallback, loadCallback) {
// open a chat window
let chatbar = getChatBar();
openChatWindow(null, SocialSidebar.provider, url, panelCallback);
chatbar.firstChild.addEventListener("DOMContentLoaded", function panelLoad() {
chatbar.firstChild.removeEventListener("DOMContentLoaded", panelLoad, true);
- loadCallback();
+ executeSoon(loadCallback);
}, true);
}
function onSidebarLoad(callback) {
let sbrowser = document.getElementById("social-sidebar-browser");
sbrowser.addEventListener("load", function load() {
sbrowser.removeEventListener("load", load, true);
- callback();
+ executeSoon(callback);
}, true);
}
-function ensureWorkerLoaded(provider, callback) {
- // once the worker responds to a ping we know it must be up.
- let port = provider.getWorkerPort();
- port.onmessage = function(msg) {
- if (msg.data.topic == "pong") {
- port.close();
- callback();
- }
- }
- port.postMessage({topic: "ping"})
-}
-
let manifest = { // normal provider
name: "provider 1",
origin: "https://example.com",
- sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
- workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
+ sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",
iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
};
function test() {
waitForExplicitFinish();
runSocialTestWithProvider(manifest, function (finishcb) {
- runSocialTests(tests, undefined, goOnline, finishcb);
+ runSocialTests(tests, undefined, function(next) { goOnline().then(next) }, finishcb);
});
}
var tests = {
testSidebar: function(next) {
let sbrowser = document.getElementById("social-sidebar-browser");
onSidebarLoad(function() {
- ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
+ ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "sidebar is on social error page");
gc();
// Add a new load listener, then find and click the "try again" button.
onSidebarLoad(function() {
// should still be on the error page.
- ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is still on social error page");
+ ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "sidebar is still on social error page");
// go online and try again - this should work.
- goOnline();
- onSidebarLoad(function() {
- // should now be on the correct page.
- is(sbrowser.contentDocument.location.href, manifest.sidebarURL, "is now on social sidebar page");
- next();
+ goOnline().then(function () {
+ onSidebarLoad(function() {
+ // should now be on the correct page.
+ is(sbrowser.contentDocument.location.href, manifest.sidebarURL, "sidebar is now on social sidebar page");
+ next();
+ });
+ sbrowser.contentDocument.getElementById("btnTryAgain").click();
});
- sbrowser.contentDocument.getElementById("btnTryAgain").click();
});
sbrowser.contentDocument.getElementById("btnTryAgain").click();
});
- // we want the worker to be fully loaded before going offline, otherwise
- // it might fail due to going offline.
- ensureWorkerLoaded(SocialSidebar.provider, function() {
- // go offline then attempt to load the sidebar - it should fail.
- goOffline();
+ // go offline then attempt to load the sidebar - it should fail.
+ goOffline().then(function() {
SocialSidebar.show();
- });
+ });
},
testFlyout: function(next) {
let panelCallbackCount = 0;
let panel = document.getElementById("social-flyout-panel");
- // go offline and open a flyout.
- goOffline();
- openPanel(
- "https://example.com/browser/browser/base/content/test/social/social_panel.html",
- function() { // the panel api callback
- panelCallbackCount++;
- },
- function() { // the "load" callback.
- executeSoon(function() {
+ goOffline().then(function() {
+ openPanel(
+ manifest.sidebarURL, /* empty html page */
+ function() { // the panel api callback
+ panelCallbackCount++;
+ },
+ function() { // the "load" callback.
todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
- ok(panel.firstChild.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
+ let href = panel.firstChild.contentDocument.location.href;
+ ok(href.indexOf("about:socialerror?")==0, "flyout is on social error page");
// Bug 832943 - the listeners previously stopped working after a GC, so
// force a GC now and try again.
gc();
openPanel(
- "https://example.com/browser/browser/base/content/test/social/social_panel.html",
+ manifest.sidebarURL, /* empty html page */
function() { // the panel api callback
panelCallbackCount++;
},
function() { // the "load" callback.
- executeSoon(function() {
- todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
- ok(panel.firstChild.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
- gc();
- executeSoon(function() {
- SocialFlyout.unload();
- next();
- });
- });
+ todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
+ let href = panel.firstChild.contentDocument.location.href;
+ ok(href.indexOf("about:socialerror?")==0, "flyout is on social error page");
+ gc();
+ SocialFlyout.unload();
+ next();
}
);
- });
- }
- );
+ }
+ );
+ });
},
testChatWindow: function(next) {
let panelCallbackCount = 0;
- // go offline and open a chat.
- goOffline();
- openChat(
- "https://example.com/browser/browser/base/content/test/social/social_chat.html",
- function() { // the panel api callback
- panelCallbackCount++;
- },
- function() { // the "load" callback.
- executeSoon(function() {
+ // chatwindow tests throw errors, which muddy test output, if the worker
+ // doesn't get test-init
+ goOffline().then(function() {
+ openChat(
+ manifest.sidebarURL, /* empty html page */
+ function() { // the panel api callback
+ panelCallbackCount++;
+ },
+ function() { // the "load" callback.
todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
let chat = getChatBar().selectedChat;
- waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
+ waitForCondition(function() chat.content != null && chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
function() {
chat.close();
next();
},
"error page didn't appear");
- });
- }
- );
+ }
+ );
+ });
},
testChatWindowAfterTearOff: function(next) {
// Ensure that the error listener survives the chat window being detached.
- let url = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
+ let url = manifest.sidebarURL; /* empty html page */
let panelCallbackCount = 0;
+ // chatwindow tests throw errors, which muddy test output, if the worker
+ // doesn't get test-init
// open a chat while we are still online.
openChat(
url,
null,
function() { // the "load" callback.
- executeSoon(function() {
- let chat = getChatBar().selectedChat;
- is(chat.contentDocument.location.href, url, "correct url loaded");
- // toggle to a detached window.
- chat.swapWindows().then(
- chat => {
+ let chat = getChatBar().selectedChat;
+ is(chat.contentDocument.location.href, url, "correct url loaded");
+ // toggle to a detached window.
+ chat.swapWindows().then(
+ chat => {
+ ok(!!chat.content, "we have chat content 1");
+ waitForCondition(function() chat.content != null && chat.contentDocument.readyState == "complete",
+ function() {
// now go offline and reload the chat - about:socialerror should be loaded.
- goOffline();
- chat.contentDocument.location.reload();
- waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
- function() {
- chat.close();
- next();
- },
- "error page didn't appear");
- }
- );
- });
+ goOffline().then(function() {
+ ok(!!chat.content, "we have chat content 2");
+ chat.contentDocument.location.reload();
+ info("chat reload called");
+ waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
+ function() {
+ chat.close();
+ next();
+ },
+ "error page didn't appear");
+ });
+ }, "swapped window loaded");
+ }
+ );
}
);
}
}
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -28,16 +28,26 @@ function waitForCondition(condition, nex
if (conditionPassed) {
moveOn();
}
tries++;
}, 100);
var moveOn = function() { clearInterval(interval); nextTest(); };
}
+
+function promiseObserverNotified(aTopic) {
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(onNotification, aTopic);
+ deferred.resolve({subject: aSubject, data: aData});
+ }, aTopic, false);
+ return deferred.promise;
+}
+
// Check that a specified (string) URL hasn't been "remembered" (ie, is not
// in history, will not appear in about:newtab or auto-complete, etc.)
function promiseSocialUrlNotRemembered(url) {
let deferred = Promise.defer();
let uri = Services.io.newURI(url, null, null);
PlacesUtils.asyncHistory.isURIVisited(uri, function(aURI, aIsVisited) {
ok(!aIsVisited, "social URL " + url + " should not be in global history");
deferred.resolve();
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/share_activate.html
@@ -0,0 +1,36 @@
+<html>
+<!-- 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/. -->
+<head>
+ <title>Activation test</title>
+</head>
+<script>
+
+var data = {
+ // currently required
+ "name": "Demo Social Service",
+ // browser_share.js serves this page from "https://example.com"
+ "origin": "https://example.com",
+ "iconURL": "chrome://branding/content/icon16.png",
+ "icon32URL": "chrome://branding/content/favicon32.png",
+ "icon64URL": "chrome://branding/content/icon64.png",
+ "workerURL": "/browser/browser/base/content/test/social/social_worker.js",
+ "shareURL": "/browser/browser/base/content/test/social/share.html"
+}
+
+function activate(node) {
+ node.setAttribute("data-service", JSON.stringify(data));
+ var event = new CustomEvent("ActivateSocialFeature");
+ node.dispatchEvent(event);
+}
+
+</script>
+<body>
+
+nothing to see here
+
+<button id="activation" onclick="activate(this, true)">Activate the share provider</button>
+
+</body>
+</html>
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -63,16 +63,17 @@ browser.jar:
content/browser/aboutaccounts/images/graphic_sync_intro@2x.png (content/aboutaccounts/images/graphic_sync_intro@2x.png)
content/browser/certerror/aboutCertError.xhtml (content/aboutcerterror/aboutCertError.xhtml)
content/browser/certerror/aboutCertError.css (content/aboutcerterror/aboutCertError.css)
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.js (content/aboutTabCrashed.js)
content/browser/aboutTabCrashed.xhtml (content/aboutTabCrashed.xhtml)
* content/browser/browser.css (content/browser.css)
* content/browser/browser.js (content/browser.js)
* content/browser/browser.xul (content/browser.xul)
* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
* content/browser/chatWindow.xul (content/chatWindow.xul)
content/browser/content.js (content/content.js)
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -42,16 +42,19 @@ static RedirEntry kRedirMap[] = {
#endif
{ "certerror", "chrome://browser/content/certerror/aboutCertError.xhtml",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
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 |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
{ "feeds", "chrome://browser/content/feeds/subscribe.xhtml",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -85,16 +85,17 @@ static const mozilla::Module::ContractID
{ NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
#endif
{ NS_FEEDSNIFFER_CONTRACTID, &kNS_FEEDSNIFFER_CID },
#ifdef MOZ_SAFE_BROWSING
{ 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 },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "rights", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "robots", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "sessionrestore", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "welcomeback", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#ifdef MOZ_SERVICES_SYNC
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -202,17 +202,16 @@ let CustomizableUIInternal = {
overflowable: true,
defaultPlacements: [
"urlbar-container",
"search-container",
"bookmarks-menu-button",
"downloads-button",
"home-button",
"loop-call-button",
- "social-share-button",
],
defaultCollapsed: false,
}, true);
#ifndef XP_MACOSX
this.registerArea(CustomizableUI.AREA_MENUBAR, {
legacy: true,
type: CustomizableUI.TYPE_TOOLBAR,
defaultPlacements: [
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -389,16 +389,34 @@ const CustomizableWidgets = [
fillSubviewFromMenuItems([...menu.children], doc.getElementById("PanelUI-sidebarItems"));
},
onViewHiding: function(aEvent) {
let doc = aEvent.target.ownerDocument;
clearSubview(doc.getElementById("PanelUI-sidebarItems"));
}
}, {
+ id: "social-share-button",
+ tooltiptext: "social-share-button.label",
+ label: "social-share-button.tooltiptext",
+ // 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:PageShareOrMark");
+ node.setAttribute("command", "Social:SharePage");
+ return node;
+ }
+ }, {
id: "add-ons-button",
shortcutId: "key_openAddons",
tooltiptext: "add-ons-button.tooltiptext3",
defaultArea: CustomizableUI.AREA_PANEL,
onCommand: function(aEvent) {
let win = aEvent.target &&
aEvent.target.ownerDocument &&
aEvent.target.ownerDocument.defaultView;
@@ -900,19 +918,18 @@ const CustomizableWidgets = [
tooltiptext: "email-link-button.tooltiptext3",
onCommand: function(aEvent) {
let win = aEvent.view;
win.MailIntegration.sendLinkForWindow(win.content);
}
}, {
id: "loop-call-button",
type: "custom",
- // XXX Bug 1013989 will provide a label for the button
- label: "loop-call-button.label",
- tooltiptext: "loop-call-button.tooltiptext",
+ label: "loop-call-button2.label",
+ tooltiptext: "loop-call-button2.tooltiptext",
defaultArea: CustomizableUI.AREA_NAVBAR,
introducedInVersion: 1,
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.classList.add("badged-button");
--- a/browser/components/loop/GoogleImporter.jsm
+++ b/browser/components/loop/GoogleImporter.jsm
@@ -217,17 +217,17 @@ this.GoogleImporter.prototype = {
// The following loops runs as long as the OAuth windows' titlebar doesn't
// yield a response from the Google service. If an error occurs, the loop
// will terminate early.
while (!code) {
if (!gAuthWindow || gAuthWindow.closed) {
throw new Error("Popup window was closed before authentication succeeded");
}
- let matches = gAuthWindow.document.title.match(/(error|code)=(.*)$/);
+ let matches = gAuthWindow.document.title.match(/(error|code)=([^\s]+)/);
if (matches && matches.length) {
let [, type, message] = matches;
gAuthWindow.close();
gAuthWindow = null;
if (type == "error") {
throw new Error("Google authentication failed with error: " + message.trim());
} else if (type == "code") {
code = message.trim();
--- a/browser/components/loop/LoopStorage.jsm
+++ b/browser/components/loop/LoopStorage.jsm
@@ -20,17 +20,19 @@ Cu.import("resource://gre/modules/Servic
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
return new EventEmitter();
});
this.EXPORTED_SYMBOLS = ["LoopStorage"];
-const kDatabaseName = "loop";
+const kDatabasePrefix = "loop-";
+const kDefaultDatabaseName = "default";
+let gDatabaseName = kDatabasePrefix + kDefaultDatabaseName;
const kDatabaseVersion = 1;
let gWaitForOpenCallbacks = new Set();
let gDatabase = null;
let gClosed = false;
/**
* Properly shut the database instance down. This is done on application shutdown.
@@ -78,26 +80,26 @@ const ensureDatabaseOpen = function(onOp
let invokeCallbacks = err => {
for (let callback of gWaitForOpenCallbacks) {
callback(err, gDatabase);
}
gWaitForOpenCallbacks.clear();
};
- let openRequest = indexedDB.open(kDatabaseName, kDatabaseVersion);
+ let openRequest = indexedDB.open(gDatabaseName, kDatabaseVersion);
openRequest.onblocked = function(event) {
invokeCallbacks(new Error("Database cannot be upgraded cause in use: " + event.target.error));
};
openRequest.onerror = function(event) {
// Try to delete the old database so that we can start this process over
// next time.
- indexedDB.deleteDatabase(kDatabaseName);
+ indexedDB.deleteDatabase(gDatabaseName);
invokeCallbacks(new Error("Error while opening database: " + event.target.errorCode));
};
openRequest.onupgradeneeded = function(event) {
let db = event.target.result;
eventEmitter.emit("upgrade", db, event.oldVersion, kDatabaseVersion);
};
@@ -105,16 +107,43 @@ const ensureDatabaseOpen = function(onOp
gDatabase = event.target.result;
invokeCallbacks();
// Close the database instance properly on application shutdown.
Services.obs.addObserver(closeDatabase, "quit-application", false);
};
};
/**
+ * Switch to a database with a different name by closing the current connection
+ * and making sure that the next connection attempt will be made using the updated
+ * name.
+ *
+ * @param {String} name New name of the database to switch to.
+ */
+const switchDatabase = function(name) {
+ if (!name) {
+ name = kDefaultDatabaseName;
+ }
+ name = kDatabasePrefix + name;
+ if (name == gDatabaseName) {
+ // This is already the current database, so there's no need to switch.
+ return;
+ }
+
+ gDatabaseName = name;
+ if (gDatabase) {
+ try {
+ gDatabase.close();
+ } finally {
+ gDatabase = null;
+ }
+ }
+};
+
+/**
* Start a transaction on the loop database and return it.
*
* @param {String} store Name of the object store to start a transaction on
* @param {Function} callback Callback to be invoked once a database connection
* is established and a transaction can be started.
* It takes an Error object as first argument and the
* transaction object as second argument.
* @param {String} mode Mode of the transaction. May be 'readonly' or 'readwrite'
@@ -175,28 +204,46 @@ const getStore = function(store, callbac
*
* LoopStorage implements the EventEmitter interface by exposing two methods, `on`
* and `off`, to subscribe to events.
* At this point only the `upgrade` event will be emitted. This happens when the
* database is loaded in memory and consumers will be able to change its structure.
*/
this.LoopStorage = Object.freeze({
/**
+ * @var {String} databaseName The name of the database that is currently active,
+ * WITHOUT the prefix
+ */
+ get databaseName() {
+ return gDatabaseName.substr(kDatabasePrefix.length);
+ },
+
+ /**
* Open a connection to the IndexedDB database and return the database object.
*
* @param {Function} callback Callback to be invoked once a database connection
* is established. It takes an Error object as first
* argument and the database connection object as
* second argument, if successful.
*/
getSingleton: function(callback) {
ensureDatabaseOpen(callback);
},
/**
+ * Switch to a database with a different name.
+ *
+ * @param {String} name New name of the database to switch to. Defaults to
+ * `kDefaultDatabaseName`
+ */
+ switchDatabase: function(name = kDefaultDatabaseName) {
+ switchDatabase(name);
+ },
+
+ /**
* Start a transaction on the loop database and return it.
* If only two arguments are passed, the default mode will be assumed and the
* second argument is assumed to be a callback.
*
* @param {String} store Name of the object store to start a transaction on
* @param {Function} callback Callback to be invoked once a database connection
* is established and a transaction can be started.
* It takes an Error object as first argument and the
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -51,16 +51,19 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/FxAccountsProfileClient.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
"resource://services-common/hawkclient.js");
XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
"resource://services-common/hawkrequest.js");
+XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
+ "resource:///modules/loop/LoopStorage.jsm");
+
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
"resource:///modules/loop/MozLoopPushHandler.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator");
XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
@@ -362,16 +365,18 @@ let MozLoopServiceInternal = {
*/
set doNotDisturb(aFlag) {
Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
this.notifyStatusChanged();
},
notifyStatusChanged: function(aReason = null) {
log.debug("notifyStatusChanged with reason:", aReason);
+ let profile = MozLoopService.userProfile;
+ LoopStorage.switchDatabase(profile ? profile.uid : null);
Services.obs.notifyObservers(null, "loop-status-changed", aReason);
},
/**
* Record an error and notify interested UI with the relevant user-facing strings attached.
*
* @param {String} errorType a key to identify the type of error. Only one
* error of a type will be saved at a time. This value may be used to
@@ -1343,17 +1348,17 @@ this.MozLoopService = {
*
* @param {key} The element id to get strings for.
* @return {String} A JSON string containing the localized
* attribute/value pairs for the element.
*/
getStrings: function(key) {
var stringData = MozLoopServiceInternal.localizedStrings;
if (!(key in stringData)) {
- Cu.reportError('No string for key: ' + key + 'found');
+ log.error("No string found for key: ", key);
return "";
}
return JSON.stringify(stringData[key]);
},
/**
* Returns a new GUID (UUID) in curly braces format.
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -148,24 +148,24 @@ loop.contacts = (function(_, mozL10n) {
this.setState({showMenu: false});
}
},
componentWillUnmount: function() {
document.body.removeEventListener("click", this._onBodyClick);
},
- componentShouldUpdate: function(nextProps, nextState) {
+ shouldComponentUpdate: function(nextProps, nextState) {
let currContact = this.props.contact;
let nextContact = nextProps.contact;
return (
currContact.name[0] !== nextContact.name[0] ||
currContact.blocked !== nextContact.blocked ||
- getPreferredEmail(currContact).value !==
- getPreferredEmail(nextContact).value
+ getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value ||
+ nextState.showMenu !== this.state.showMenu
);
},
handleAction: function(actionName) {
if (this.props.handleContactAction) {
this.props.handleContactAction(this.props.contact, actionName);
}
},
@@ -210,45 +210,78 @@ loop.contacts = (function(_, mozL10n) {
)
);
}
});
const ContactsList = React.createClass({displayName: 'ContactsList',
mixins: [React.addons.LinkedStateMixin],
+ /**
+ * Contacts collection object
+ */
+ contacts: null,
+
+ /**
+ * User profile
+ */
+ _userProfile: null,
+
getInitialState: function() {
return {
- contacts: {},
importBusy: false,
filter: "",
};
},
- componentDidMount: function() {
+ refresh: function(callback = function() {}) {
let contactsAPI = navigator.mozLoop.contacts;
+ this.handleContactRemoveAll();
+
contactsAPI.getAll((err, contacts) => {
if (err) {
- throw err;
+ callback(err);
+ return;
}
// Add contacts already present in the DB. We do this in timed chunks to
// circumvent blocking the main event loop.
let addContactsInChunks = () => {
contacts.splice(0, CONTACTS_CHUNK_SIZE).forEach(contact => {
this.handleContactAddOrUpdate(contact, false);
});
if (contacts.length) {
setTimeout(addContactsInChunks, 0);
+ } else {
+ callback();
}
this.forceUpdate();
};
addContactsInChunks(contacts);
+ });
+ },
+
+ componentWillMount: function() {
+ // Take the time to initialize class variables that are used outside
+ // `this.state`.
+ this.contacts = {};
+ this._userProfile = navigator.mozLoop.userProfile;
+ },
+
+ componentDidMount: function() {
+ window.addEventListener("LoopStatusChanged", this._onStatusChanged);
+
+ this.refresh(err => {
+ if (err) {
+ throw err;
+ }
+
+ let contactsAPI = navigator.mozLoop.contacts;
// Listen for contact changes/ updates.
contactsAPI.on("add", (eventName, contact) => {
this.handleContactAddOrUpdate(contact);
});
contactsAPI.on("remove", (eventName, contact) => {
this.handleContactRemove(contact);
});
@@ -256,37 +289,55 @@ loop.contacts = (function(_, mozL10n) {
this.handleContactRemoveAll();
});
contactsAPI.on("update", (eventName, contact) => {
this.handleContactAddOrUpdate(contact);
});
});
},
+ componentWillUnmount: function() {
+ window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
+ },
+
+ _onStatusChanged: function() {
+ let profile = navigator.mozLoop.userProfile;
+ let currUid = this._userProfile ? this._userProfile.uid : null;
+ let newUid = profile ? profile.uid : null;
+ if (currUid != newUid) {
+ // On profile change (login, logout), reload all contacts.
+ this._userProfile = profile;
+ // The following will do a forceUpdate() for us.
+ this.refresh();
+ }
+ },
+
handleContactAddOrUpdate: function(contact, render = true) {
- let contacts = this.state.contacts;
+ let contacts = this.contacts;
let guid = String(contact._guid);
contacts[guid] = contact;
if (render) {
this.forceUpdate();
}
},
handleContactRemove: function(contact) {
- let contacts = this.state.contacts;
+ let contacts = this.contacts;
let guid = String(contact._guid);
if (!contacts[guid]) {
return;
}
delete contacts[guid];
this.forceUpdate();
},
handleContactRemoveAll: function() {
- this.setState({contacts: {}});
+ // Do not allow any race conditions when removing all contacts.
+ this.contacts = {};
+ this.forceUpdate();
},
handleImportButtonClick: function() {
this.setState({ importBusy: true });
navigator.mozLoop.startImport({
service: "google"
}, (err, stats) => {
this.setState({ importBusy: false });
@@ -359,21 +410,21 @@ loop.contacts = (function(_, mozL10n) {
},
render: function() {
let viewForItem = item => {
return ContactDetail({key: item._guid, contact: item,
handleContactAction: this.handleContactAction})
};
- let shownContacts = _.groupBy(this.state.contacts, function(contact) {
+ let shownContacts = _.groupBy(this.contacts, function(contact) {
return contact.blocked ? "blocked" : "available";
});
- let showFilter = Object.getOwnPropertyNames(this.state.contacts).length >=
+ let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
MIN_CONTACTS_FOR_FILTERING;
if (showFilter) {
let filter = this.state.filter.trim().toLocaleLowerCase();
if (filter) {
let filterFn = contact => {
return contact.name[0].toLocaleLowerCase().contains(filter) ||
getPreferredEmail(contact).value.toLocaleLowerCase().contains(filter);
};
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -148,24 +148,24 @@ loop.contacts = (function(_, mozL10n) {
this.setState({showMenu: false});
}
},
componentWillUnmount: function() {
document.body.removeEventListener("click", this._onBodyClick);
},
- componentShouldUpdate: function(nextProps, nextState) {
+ shouldComponentUpdate: function(nextProps, nextState) {
let currContact = this.props.contact;
let nextContact = nextProps.contact;
return (
currContact.name[0] !== nextContact.name[0] ||
currContact.blocked !== nextContact.blocked ||
- getPreferredEmail(currContact).value !==
- getPreferredEmail(nextContact).value
+ getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value ||
+ nextState.showMenu !== this.state.showMenu
);
},
handleAction: function(actionName) {
if (this.props.handleContactAction) {
this.props.handleContactAction(this.props.contact, actionName);
}
},
@@ -210,45 +210,78 @@ loop.contacts = (function(_, mozL10n) {
</li>
);
}
});
const ContactsList = React.createClass({
mixins: [React.addons.LinkedStateMixin],
+ /**
+ * Contacts collection object
+ */
+ contacts: null,
+
+ /**
+ * User profile
+ */
+ _userProfile: null,
+
getInitialState: function() {
return {
- contacts: {},
importBusy: false,
filter: "",
};
},
- componentDidMount: function() {
+ refresh: function(callback = function() {}) {
let contactsAPI = navigator.mozLoop.contacts;
+ this.handleContactRemoveAll();
+
contactsAPI.getAll((err, contacts) => {
if (err) {
- throw err;
+ callback(err);
+ return;
}
// Add contacts already present in the DB. We do this in timed chunks to
// circumvent blocking the main event loop.
let addContactsInChunks = () => {
contacts.splice(0, CONTACTS_CHUNK_SIZE).forEach(contact => {
this.handleContactAddOrUpdate(contact, false);
});
if (contacts.length) {
setTimeout(addContactsInChunks, 0);
+ } else {
+ callback();
}
this.forceUpdate();
};
addContactsInChunks(contacts);
+ });
+ },
+
+ componentWillMount: function() {
+ // Take the time to initialize class variables that are used outside
+ // `this.state`.
+ this.contacts = {};
+ this._userProfile = navigator.mozLoop.userProfile;
+ },
+
+ componentDidMount: function() {
+ window.addEventListener("LoopStatusChanged", this._onStatusChanged);
+
+ this.refresh(err => {
+ if (err) {
+ throw err;
+ }
+
+ let contactsAPI = navigator.mozLoop.contacts;
// Listen for contact changes/ updates.
contactsAPI.on("add", (eventName, contact) => {
this.handleContactAddOrUpdate(contact);
});
contactsAPI.on("remove", (eventName, contact) => {
this.handleContactRemove(contact);
});
@@ -256,37 +289,55 @@ loop.contacts = (function(_, mozL10n) {
this.handleContactRemoveAll();
});
contactsAPI.on("update", (eventName, contact) => {
this.handleContactAddOrUpdate(contact);
});
});
},
+ componentWillUnmount: function() {
+ window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
+ },
+
+ _onStatusChanged: function() {
+ let profile = navigator.mozLoop.userProfile;
+ let currUid = this._userProfile ? this._userProfile.uid : null;
+ let newUid = profile ? profile.uid : null;
+ if (currUid != newUid) {
+ // On profile change (login, logout), reload all contacts.
+ this._userProfile = profile;
+ // The following will do a forceUpdate() for us.
+ this.refresh();
+ }
+ },
+
handleContactAddOrUpdate: function(contact, render = true) {
- let contacts = this.state.contacts;
+ let contacts = this.contacts;
let guid = String(contact._guid);
contacts[guid] = contact;
if (render) {
this.forceUpdate();
}
},
handleContactRemove: function(contact) {
- let contacts = this.state.contacts;
+ let contacts = this.contacts;
let guid = String(contact._guid);
if (!contacts[guid]) {
return;
}
delete contacts[guid];
this.forceUpdate();
},
handleContactRemoveAll: function() {
- this.setState({contacts: {}});
+ // Do not allow any race conditions when removing all contacts.
+ this.contacts = {};
+ this.forceUpdate();
},
handleImportButtonClick: function() {
this.setState({ importBusy: true });
navigator.mozLoop.startImport({
service: "google"
}, (err, stats) => {
this.setState({ importBusy: false });
@@ -359,21 +410,21 @@ loop.contacts = (function(_, mozL10n) {
},
render: function() {
let viewForItem = item => {
return <ContactDetail key={item._guid} contact={item}
handleContactAction={this.handleContactAction} />
};
- let shownContacts = _.groupBy(this.state.contacts, function(contact) {
+ let shownContacts = _.groupBy(this.contacts, function(contact) {
return contact.blocked ? "blocked" : "available";
});
- let showFilter = Object.getOwnPropertyNames(this.state.contacts).length >=
+ let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
MIN_CONTACTS_FOR_FILTERING;
if (showFilter) {
let filter = this.state.filter.trim().toLocaleLowerCase();
if (filter) {
let filterFn = contact => {
return contact.name[0].toLocaleLowerCase().contains(filter) ||
getPreferredEmail(contact).value.toLocaleLowerCase().contains(filter);
};
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -163,17 +163,17 @@ loop.panel = (function(_, mozL10n) {
return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
},
render: function() {
if (this.state.seenToS == "unseen") {
var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
var tosHTML = __("legal_text_and_links3", {
- "clientShortname": __("client_shortname_fallback"),
+ "clientShortname": __("clientShortname2"),
"terms_of_use": React.renderComponentToStaticMarkup(
React.DOM.a({href: terms_of_use_url, target: "_blank"},
__("legal_text_tos")
)
),
"privacy_notice": React.renderComponentToStaticMarkup(
React.DOM.a({href: privacy_notice_url, target: "_blank"},
__("legal_text_privacy")
@@ -347,48 +347,62 @@ loop.panel = (function(_, mozL10n) {
} else {
try {
var callUrl = new window.URL(callUrlData.callUrl);
// XXX the current server vers does not implement the callToken field
// but it exists in the API. This workaround should be removed in the future
var token = callUrlData.callToken ||
callUrl.pathname.split('/').pop();
+ // Now that a new URL is available, indicate it has not been shared.
+ this.linkExfiltrated = false;
+
this.setState({pending: false, copied: false,
callUrl: callUrl.href,
callUrlExpiry: callUrlData.expiresAt});
} catch(e) {
console.log(e);
this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState());
}
}
},
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
- navigator.mozLoop.composeEmail(__("share_email_subject3"),
- __("share_email_body3", { callUrl: this.state.callUrl }));
+ navigator.mozLoop.composeEmail(
+ __("share_email_subject4", { clientShortname: __("clientShortname2")}),
+ __("share_email_body4", { callUrl: this.state.callUrl,
+ clientShortname: __("clientShortname2"),
+ learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
},
handleCopyButtonClick: function(event) {
this.handleLinkExfiltration(event);
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
},
+ linkExfiltrated: false,
+
handleLinkExfiltration: function(event) {
- try {
- navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
- } catch (err) {
- console.error("Error recording telemetry", err);
+ // Update the count of shared URLs only once per generated URL.
+ if (!this.linkExfiltrated) {
+ this.linkExfiltrated = true;
+ try {
+ navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
+ } catch (err) {
+ console.error("Error recording telemetry", err);
+ }
}
+
+ // Note URL expiration every time it is shared.
if (this.state.callUrlExpiry) {
navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
}
},
render: function() {
// XXX setting elem value from a state (in the callUrl input)
// makes it immutable ie read only but that is fine in our case.
@@ -620,17 +634,19 @@ loop.panel = (function(_, mozL10n) {
});
} else {
this.props.notifications.remove(this.props.notifications.get("service-error"));
}
},
_onStatusChanged: function() {
var profile = navigator.mozLoop.userProfile;
- if (profile != this.state.userProfile) {
+ var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
+ var newUid = profile ? profile.uid : null;
+ if (currUid != newUid) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab("call");
}
this.setState({userProfile: profile});
this.updateServiceErrors();
},
/**
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -163,17 +163,17 @@ loop.panel = (function(_, mozL10n) {
return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
},
render: function() {
if (this.state.seenToS == "unseen") {
var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
var tosHTML = __("legal_text_and_links3", {
- "clientShortname": __("client_shortname_fallback"),
+ "clientShortname": __("clientShortname2"),
"terms_of_use": React.renderComponentToStaticMarkup(
<a href={terms_of_use_url} target="_blank">
{__("legal_text_tos")}
</a>
),
"privacy_notice": React.renderComponentToStaticMarkup(
<a href={privacy_notice_url} target="_blank">
{__("legal_text_privacy")}
@@ -347,48 +347,62 @@ loop.panel = (function(_, mozL10n) {
} else {
try {
var callUrl = new window.URL(callUrlData.callUrl);
// XXX the current server vers does not implement the callToken field
// but it exists in the API. This workaround should be removed in the future
var token = callUrlData.callToken ||
callUrl.pathname.split('/').pop();
+ // Now that a new URL is available, indicate it has not been shared.
+ this.linkExfiltrated = false;
+
this.setState({pending: false, copied: false,
callUrl: callUrl.href,
callUrlExpiry: callUrlData.expiresAt});
} catch(e) {
console.log(e);
this.props.notifications.errorL10n("unable_retrieve_url");
this.setState(this.getInitialState());
}
}
},
handleEmailButtonClick: function(event) {
this.handleLinkExfiltration(event);
- navigator.mozLoop.composeEmail(__("share_email_subject3"),
- __("share_email_body3", { callUrl: this.state.callUrl }));
+ navigator.mozLoop.composeEmail(
+ __("share_email_subject4", { clientShortname: __("clientShortname2")}),
+ __("share_email_body4", { callUrl: this.state.callUrl,
+ clientShortname: __("clientShortname2"),
+ learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
},
handleCopyButtonClick: function(event) {
this.handleLinkExfiltration(event);
// XXX the mozLoop object should be passed as a prop, to ease testing and
// using a fake implementation in UI components showcase.
navigator.mozLoop.copyString(this.state.callUrl);
this.setState({copied: true});
},
+ linkExfiltrated: false,
+
handleLinkExfiltration: function(event) {
- try {
- navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
- } catch (err) {
- console.error("Error recording telemetry", err);
+ // Update the count of shared URLs only once per generated URL.
+ if (!this.linkExfiltrated) {
+ this.linkExfiltrated = true;
+ try {
+ navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
+ } catch (err) {
+ console.error("Error recording telemetry", err);
+ }
}
+
+ // Note URL expiration every time it is shared.
if (this.state.callUrlExpiry) {
navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
}
},
render: function() {
// XXX setting elem value from a state (in the callUrl input)
// makes it immutable ie read only but that is fine in our case.
@@ -620,17 +634,19 @@ loop.panel = (function(_, mozL10n) {
});
} else {
this.props.notifications.remove(this.props.notifications.get("service-error"));
}
},
_onStatusChanged: function() {
var profile = navigator.mozLoop.userProfile;
- if (profile != this.state.userProfile) {
+ var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
+ var newUid = profile ? profile.uid : null;
+ if (currUid != newUid) {
// On profile change (login, logout), switch back to the default tab.
this.selectTab("call");
}
this.setState({userProfile: profile});
this.updateServiceErrors();
},
/**
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -113,17 +113,17 @@ loop.webapp = (function($, _, OT, mozL10
}
});
var ConversationBranding = React.createClass({displayName: 'ConversationBranding',
render: function() {
return (
React.DOM.h1({className: "standalone-header-title"},
React.DOM.strong(null, mozL10n.get("brandShortname")),
- mozL10n.get("clientShortname")
+ mozL10n.get("clientShortname2")
)
);
}
});
/**
* The Firefox Marketplace exposes a web page that contains a postMesssage
* based API that wraps a small set of functionality from the WebApps API
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -113,17 +113,17 @@ loop.webapp = (function($, _, OT, mozL10
}
});
var ConversationBranding = React.createClass({
render: function() {
return (
<h1 className="standalone-header-title">
<strong>{mozL10n.get("brandShortname")}</strong>
- {mozL10n.get("clientShortname")}
+ {mozL10n.get("clientShortname2")}
</h1>
);
}
});
/**
* The Firefox Marketplace exposes a web page that contains a postMesssage
* based API that wraps a small set of functionality from the WebApps API
--- a/browser/components/loop/standalone/content/l10n/loop.en-US.properties
+++ b/browser/components/loop/standalone/content/l10n/loop.en-US.properties
@@ -36,18 +36,25 @@ get_firefox_button=Get {{brandShortname}
initiate_call_button_label2=Ready to start your conversation?
initiate_audio_video_call_button2=Start
initiate_audio_video_call_tooltip2=Start a video conversation
initiate_audio_call_button2=Voice conversation
initiate_call_cancel_button=Cancel
legal_text_and_links=By using this product you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}
terms_of_use_link_text=Terms of use
privacy_notice_link_text=Privacy notice
+invite_header_text=Invite someone to join you.
+
+## LOCALIZATION NOTE(brandShortname): This should not be localized and
+## should remain "Firefox" for all locales.
brandShortname=Firefox
-clientShortname=WebRTC!
+## LOCALIZATION NOTE(clientShortname2): This should not be localized and
+## should remain "Firefox Hello" for all locales.
+clientShortname2=Firefox Hello
+
## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
call_url_creation_date_label=(from {{call_url_creation_date}})
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.
feedback_call_experience_heading2=How was your conversation?
feedback_what_makes_you_sad=What makes you sad?
@@ -72,8 +79,31 @@ feedback_window_will_close_in2[other] =
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
+
+## LOCALIZATION_NOTE(first_time_experience.title): clientShortname will be
+## replaced by the brand name
+first_time_experience_title={{clientShortname}} — Join the conversation
+first_time_experience_button_label=Get Started
+
+help_label=Help
+tour_label=Tour
+
+rooms_default_room_name_template=Conversation {{conversationLabel}}
+rooms_leave_button_label=Leave
+rooms_list_copy_url_tooltip=Copy Link
+rooms_list_delete_tooltip=Delete conversation
+rooms_list_deleteConfirmation_label=Are you sure?
+rooms_name_this_room_label=Name this conversation
+rooms_new_room_button_label=Start a conversation
+rooms_only_occupant_label=You're the first one here.
+rooms_panel_title=Choose a conversation or start a new one
+rooms_room_full_label=There are already two people in this conversation.
+rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
+rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
+rooms_room_joined_label=Someone has joined the conversation!
+rooms_room_join_label=Join the conversation
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -389,19 +389,19 @@ describe("loop.panel", function() {
describe("Rendering the component should generate a call URL", function() {
beforeEach(function() {
document.mozL10n.initialize({
getStrings: function(key) {
var text;
- if (key === "share_email_subject3")
+ if (key === "share_email_subject4")
text = "email-subject";
- else if (key === "share_email_body3")
+ else if (key === "share_email_body4")
text = "{{callUrl}}";
return JSON.stringify({textContent: text});
}
});
});
it("should make a request to requestCallUrl", function() {
@@ -508,16 +508,18 @@ describe("loop.panel", function() {
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
+ // Multiple clicks should result in the URL being counted only once.
+ TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-copy"));
sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_SHARED",
true);
});
@@ -549,16 +551,18 @@ describe("loop.panel", function() {
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
+ // Multiple clicks should result in the URL being counted only once.
+ TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
TestUtils.Simulate.click(view.getDOMNode().querySelector(".button-email"));
sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_SHARED",
true);
});
@@ -591,18 +595,20 @@ describe("loop.panel", function() {
}));
view.setState({
pending: false,
copied: false,
callUrl: "http://example.com",
callUrlExpiry: 6000
});
+ // Multiple copies should result in the URL being counted only once.
var urlField = view.getDOMNode().querySelector("input[type='url']");
TestUtils.Simulate.copy(urlField);
+ TestUtils.Simulate.copy(urlField);
sinon.assert.calledOnce(navigator.mozLoop.telemetryAdd);
sinon.assert.calledWith(navigator.mozLoop.telemetryAdd,
"LOOP_CLIENT_CALL_URL_SHARED",
true);
});
it("should notify the user when the operation failed", function() {
--- a/browser/components/loop/test/mochitest/browser_LoopContacts.js
+++ b/browser/components/loop/test/mochitest/browser_LoopContacts.js
@@ -1,12 +1,17 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const {LoopContacts} = Cu.import("resource:///modules/loop/LoopContacts.jsm", {});
+const {LoopStorage} = Cu.import("resource:///modules/loop/LoopStorage.jsm", {});
+
+XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
+ "@mozilla.org/uuid-generator;1",
+ "nsIUUIDGenerator");
const kContacts = [{
id: 1,
name: ["Ally Avocado"],
email: [{
"pref": true,
"type": ["work"],
"value": "ally@mail.com"
@@ -395,8 +400,36 @@ add_task(function* () {
// Test if the event emitter implementation doesn't leak and is working as expected.
add_task(function* () {
yield promiseLoadContacts();
Assert.strictEqual(gExpectedAdds.length, 0, "No contact additions should be expected anymore");
Assert.strictEqual(gExpectedRemovals.length, 0, "No contact removals should be expected anymore");
Assert.strictEqual(gExpectedUpdates.length, 0, "No contact updates should be expected anymore");
});
+
+// Test switching between different databases.
+add_task(function* () {
+ Assert.equal(LoopStorage.databaseName, "default", "First active partition should be the default");
+ yield promiseLoadContacts();
+
+ let uuid = uuidgen.generateUUID().toString().replace(/[{}]+/g, "");
+ LoopStorage.switchDatabase(uuid);
+ Assert.equal(LoopStorage.databaseName, uuid, "The active partition should have changed");
+
+ yield promiseLoadContacts();
+
+ let contacts = yield promiseLoadContacts();
+ for (let i = 0, l = contacts.length; i < l; ++i) {
+ compareContacts(contacts[i], kContacts[i]);
+ }
+
+ LoopStorage.switchDatabase();
+ Assert.equal(LoopStorage.databaseName, "default", "The active partition should have changed");
+
+ LoopContacts.getAll(function(err, contacts) {
+ Assert.equal(err, null, "There shouldn't be an error");
+
+ for (let i = 0, l = contacts.length; i < l; ++i) {
+ compareContacts(contacts[i], kContacts[i]);
+ }
+ });
+});
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -735,23 +735,16 @@ BrowserGlue.prototype = {
WebappManager.uninit();
#ifdef NIGHTLY_BUILD
if (Services.prefs.getBoolPref("dom.identity.enabled")) {
SignInToWebsiteUX.uninit();
}
#endif
webrtcUI.uninit();
FormValidationHandler.uninit();
-
- // XXX: Temporary hack to allow Loop FxA login after a restart to work.
- // Remove this once bug 1071247 is deployed.
- if (Services.prefs.getPrefType("loop.autologin-after-restart") != Ci.nsIPrefBranch.PREF_BOOL ||
- !Services.prefs.getBoolPref("loop.autologin-after-restart")) {
- Services.prefs.clearUserPref("loop.hawk-session-token.fxa");
- }
},
// All initial windows have opened.
_onWindowsRestored: function BG__onWindowsRestored() {
// Show update notification, if needed.
if (Services.prefs.prefHasUserValue("app.update.postupdate"))
this._showUpdateNotification();
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/RevivableWindows.jsm
@@ -0,0 +1,45 @@
+/* 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 = ["RevivableWindows"];
+
+// List of closed windows that we can revive when closing
+// windows in succession until the browser quits.
+let closedWindows = [];
+
+/**
+ * This module keeps track of closed windows that are revivable. On Windows
+ * and Linux we can revive windows before saving to disk - i.e. moving them
+ * from state._closedWindows[] to state.windows[] so that they're opened
+ * automatically on next startup. This feature lets us properly support
+ * closing windows in succession until the browser quits.
+ *
+ * The length of the list is not capped by max_undo_windows unlike
+ * state._closedWindows[].
+ */
+this.RevivableWindows = Object.freeze({
+ // Returns whether there are windows to revive.
+ get isEmpty() {
+ return closedWindows.length == 0;
+ },
+
+ // Add a window to the list.
+ add(winState) {
+#ifndef XP_MACOSX
+ closedWindows.push(winState);
+#endif
+ },
+
+ // Get the list of revivable windows.
+ get() {
+ return [...closedWindows];
+ },
+
+ // Clear the list of revivable windows.
+ clear() {
+ closedWindows.length = 0;
+ }
+});
--- a/browser/components/sessionstore/SessionSaver.jsm
+++ b/browser/components/sessionstore/SessionSaver.jsm
@@ -14,16 +14,18 @@ Cu.import("resource://gre/modules/Timer.
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/TelemetryStopwatch.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
"resource:///modules/sessionstore/PrivacyFilter.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RevivableWindows",
+ "resource:///modules/sessionstore/RevivableWindows.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionFile",
"resource:///modules/sessionstore/SessionFile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
// Minimal interval between two save operations (in milliseconds).
@@ -200,33 +202,34 @@ let SessionSaverInternal = {
// Make sure that we keep the previous session if we started with a single
// private window and no non-private windows have been opened, yet.
if (state.deferredInitialState) {
state.windows = state.deferredInitialState.windows || [];
delete state.deferredInitialState;
}
-#ifndef XP_MACOSX
- // We want to restore closed windows that are marked with _shouldRestore.
- // We're doing this here because we want to control this only when saving
- // the file.
- while (state._closedWindows.length) {
- let i = state._closedWindows.length - 1;
+ // We want to revive closed windows that have been closed in succession
+ // without any user action in between closing those. This happens here in
+ // the SessionSaver because we only want to revive when saving to disk.
+ // On Mac OS X this list will always be empty.
+ let windowsToRevive = RevivableWindows.get();
+ state.windows.unshift(...windowsToRevive);
+ let revivedWindows = state._closedWindows.splice(0, windowsToRevive.length);
+#ifdef DEBUG
+ // Check that the windows to revive equal the windows
+ // that we removed from the list of closed windows.
+ let match = revivedWindows.every((win, idx) => {
+ return win == windowsToRevive[windowsToRevive.length - 1 - idx];
+ });
- if (!state._closedWindows[i]._shouldRestore) {
- // We only need to go until _shouldRestore
- // is falsy since we're going in reverse.
- break;
- }
-
- delete state._closedWindows[i]._shouldRestore;
- state.windows.unshift(state._closedWindows.pop());
+ if (!match) {
+ throw new Error("SessionStore: revived windows didn't match closed windows");
}
-#endif
+#endif DEBUG
stopWatchFinish("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
return this._writeState(state);
},
/**
* Saves the current session state. Collects data asynchronously and calls
* _saveState() to collect data again (with a cache hit rate of hopefully
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -102,16 +102,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
"resource:///modules/RecentWindow.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "GlobalState",
"resource:///modules/sessionstore/GlobalState.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyFilter",
"resource:///modules/sessionstore/PrivacyFilter.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "RevivableWindows",
+ "resource:///modules/sessionstore/RevivableWindows.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "RunState",
"resource:///modules/sessionstore/RunState.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScratchpadManager",
"resource:///modules/devtools/scratchpad-manager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionSaver",
"resource:///modules/sessionstore/SessionSaver.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "SessionCookies",
"resource:///modules/sessionstore/SessionCookies.jsm");
@@ -696,17 +698,19 @@ let SessionStoreInternal = {
this.onTabHide(win, aEvent.originalTarget);
break;
case "TabPinned":
case "TabUnpinned":
case "SwapDocShells":
this.saveStateDelayed(win);
break;
}
- this._clearRestoringWindows();
+
+ // Any event handled here indicates a user action.
+ RevivableWindows.clear();
},
/**
* Generate a unique window identifier
* @return string
* A unique string to identify a window
*/
_generateWindowID: function ssi_generateWindowID() {
@@ -1008,21 +1012,19 @@ let SessionStoreInternal = {
if (isFullyLoaded) {
winData.title = tabbrowser.selectedBrowser.contentTitle || tabbrowser.selectedTab.label;
winData.title = this._replaceLoadingTitle(winData.title, tabbrowser,
tabbrowser.selectedTab);
SessionCookies.update([winData]);
}
-#ifndef XP_MACOSX
// Until we decide otherwise elsewhere, this window is part of a series
// of closing windows to quit.
- winData._shouldRestore = true;
-#endif
+ RevivableWindows.add(winData);
// Store the window's close date to figure out when each individual tab
// was closed. This timestamp should allow re-arranging data based on how
// recently something was closed.
winData.closedAt = Date.now();
// Save non-private windows if they have at
// least one saveable tab or are the last window.
@@ -1034,19 +1036,18 @@ let SessionStoreInternal = {
let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
// When closing windows one after the other until Firefox quits, we
// will move those closed in series back to the "open windows" bucket
// before writing to disk. If however there is only a single window
// with tabs we deem not worth saving then we might end up with a
// random closed or even a pop-up window re-opened. To prevent that
// we explicitly allow saving an "empty" window state.
- let isLastWindow =
- Object.keys(this._windows).length == 1 &&
- !this._closedWindows.some(win => win._shouldRestore || false);
+ let numOpenWindows = Object.keys(this._windows).length;
+ let isLastWindow = numOpenWindows == 1 && RevivableWindows.isEmpty;
if (hasSaveableTabs || isLastWindow) {
// we don't want to save the busy state
delete winData.busy;
this._closedWindows.unshift(winData);
this._capClosedWindows();
}
@@ -1150,27 +1151,28 @@ let SessionStoreInternal = {
// also clear all data about closed tabs and windows
for (let ix in this._windows) {
if (ix in openWindows) {
this._windows[ix]._closedTabs = [];
} else {
delete this._windows[ix];
}
}
+
// also clear all data about closed windows
this._closedWindows = [];
+ RevivableWindows.clear();
+
// give the tabbrowsers a chance to clear their histories first
var win = this._getMostRecentBrowserWindow();
if (win) {
win.setTimeout(() => SessionSaver.run(), 0);
} else if (RunState.isRunning) {
SessionSaver.run();
}
-
- this._clearRestoringWindows();
},
/**
* On purge of domain data
* @param aData
* String domain data
*/
onPurgeDomainData: function ssi_onPurgeDomainData(aData) {
@@ -1214,21 +1216,22 @@ let SessionStoreInternal = {
// some duplication from restoreHistory - make sure we get the correct title
let activeIndex = (selectedTab.index || selectedTab.entries.length) - 1;
if (activeIndex >= selectedTab.entries.length)
activeIndex = selectedTab.entries.length - 1;
this._closedWindows[ix].title = selectedTab.entries[activeIndex].title;
}
}
+ // Purging domain data indicates a user action.
+ RevivableWindows.clear();
+
if (RunState.isRunning) {
SessionSaver.run();
}
-
- this._clearRestoringWindows();
},
/**
* On preference change
* @param aData
* String preference changed
*/
onPrefChange: function ssi_onPrefChange(aData) {
@@ -3342,31 +3345,16 @@ let SessionStoreInternal = {
normalWindowIndex++;
if (normalWindowIndex >= this._max_windows_undo)
spliceTo = normalWindowIndex + 1;
#endif
this._closedWindows.splice(spliceTo, this._closedWindows.length);
},
/**
- * Clears the set of windows that are "resurrected" before writing to disk to
- * make closing windows one after the other until shutdown work as expected.
- *
- * This function should only be called when we are sure that there has been
- * a user action that indicates the browser is actively being used and all
- * windows that have been closed before are not part of a series of closing
- * windows.
- */
- _clearRestoringWindows: function ssi_clearRestoringWindows() {
- for (let i = 0; i < this._closedWindows.length; i++) {
- delete this._closedWindows[i]._shouldRestore;
- }
- },
-
- /**
* Reset state to prepare for a new session state to be restored.
*/
_resetRestoringState: function ssi_initRestoringState() {
TabRestoreQueue.reset();
this._tabsRestoringCount = 0;
},
/**
--- a/browser/components/sessionstore/moz.build
+++ b/browser/components/sessionstore/moz.build
@@ -41,12 +41,13 @@ EXTRA_JS_MODULES.sessionstore = [
'SessionWorker.jsm',
'TabAttributes.jsm',
'TabState.jsm',
'TabStateCache.jsm',
'Utils.jsm',
]
EXTRA_PP_JS_MODULES.sessionstore += [
+ 'RevivableWindows.jsm',
'SessionSaver.jsm',
'SessionStore.jsm',
]
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -76,16 +76,17 @@ skip-if = buildapp == 'mulet'
[browser_frametree.js]
[browser_frame_history.js]
[browser_global_store.js]
[browser_history_cap.js]
[browser_label_and_icon.js]
[browser_merge_closed_tabs.js]
[browser_pageStyle.js]
[browser_privatetabs.js]
+[browser_revive_windows.js]
[browser_scrollPositions.js]
[browser_sessionHistory.js]
skip-if = e10s
[browser_sessionStorage.js]
skip-if = e10s
[browser_swapDocShells.js]
skip-if = e10s # See bug 918634
[browser_telemetry.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_revive_windows.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const IS_MAC = ("nsILocalFileMac" in Ci);
+const URL_PREFIX = "about:mozilla?t=browser_revive_windows&r=";
+const PREF_MAX_UNDO = "browser.sessionstore.max_windows_undo";
+
+const URL_MAIN_WINDOW = URL_PREFIX + Math.random();
+const URL_ADD_WINDOW1 = URL_PREFIX + Math.random();
+const URL_ADD_WINDOW2 = URL_PREFIX + Math.random();
+const URL_CLOSED_WINDOW = URL_PREFIX + Math.random();
+
+add_task(function* setup() {
+ registerCleanupFunction(() => Services.prefs.clearUserPref(PREF_MAX_UNDO));
+});
+
+/**
+ * This test ensure that when closing windows in succession until the browser
+ * quits we are able to revive more windows than we keep around for the
+ * "Undo Close Window" feature.
+ */
+add_task(function* test_revive_windows() {
+ // We can restore a single window max.
+ Services.prefs.setIntPref(PREF_MAX_UNDO, 1);
+
+ // Clear list of closed windows.
+ forgetClosedWindows();
+
+ let windows = [];
+
+ // Create three windows.
+ for (let i = 0; i < 3; i++) {
+ let win = yield promiseNewWindow();
+ windows.push(win);
+
+ let tab = win.gBrowser.addTab("about:mozilla");
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ }
+
+ // Close all windows.
+ for (let win of windows) {
+ yield promiseWindowClosed(win);
+ }
+
+ is(ss.getClosedWindowCount(), 1, "one window restorable");
+
+ // Save to disk and read.
+ let state = JSON.parse(yield promiseRecoveryFileContents());
+
+ // Check number of windows.
+ if (IS_MAC) {
+ is(state.windows.length, 1, "one open window");
+ is(state._closedWindows.length, 1, "one closed window");
+ } else {
+ is(state.windows.length, 4, "four open windows");
+ is(state._closedWindows.length, 0, "closed windows");
+ }
+});
+
+/**
+ * This test ensures that when closing windows one after the other until the
+ * browser shuts down (on Windows and Linux) we revive closed windows in the
+ * right order.
+ */
+add_task(function* test_revive_windows_order() {
+ // We can restore up to three windows max.
+ Services.prefs.setIntPref(PREF_MAX_UNDO, 3);
+
+ // Clear list of closed windows.
+ forgetClosedWindows();
+
+ let tab = gBrowser.addTab(URL_MAIN_WINDOW);
+ yield promiseBrowserLoaded(tab.linkedBrowser);
+ TabState.flush(tab.linkedBrowser);
+ registerCleanupFunction(() => gBrowser.removeTab(tab));
+
+ let win0 = yield promiseNewWindow();
+ let tab0 = win0.gBrowser.addTab(URL_CLOSED_WINDOW);
+ yield promiseBrowserLoaded(tab0.linkedBrowser);
+
+ yield promiseWindowClosed(win0);
+ let data = ss.getClosedWindowData();
+ ok(data.contains(URL_CLOSED_WINDOW), "window is restorable");
+
+ let win1 = yield promiseNewWindow();
+ let tab1 = win1.gBrowser.addTab(URL_ADD_WINDOW1);
+ yield promiseBrowserLoaded(tab1.linkedBrowser);
+
+ let win2 = yield promiseNewWindow();
+ let tab2 = win2.gBrowser.addTab(URL_ADD_WINDOW2);
+ yield promiseBrowserLoaded(tab2.linkedBrowser);
+
+ // Close both windows so that |win1| will be opened first and would be
+ // behind |win2| that was closed later.
+ yield promiseWindowClosed(win1);
+ yield promiseWindowClosed(win2);
+
+ // Repeat the checks once.
+ for (let i = 0; i < 2; i++) {
+ info(`checking window data, iteration #${i}`);
+ let contents = yield promiseRecoveryFileContents();
+ let {windows, _closedWindows: closedWindows} = JSON.parse(contents);
+
+ if (IS_MAC) {
+ // Check number of windows.
+ is(windows.length, 1, "one open window");
+ is(closedWindows.length, 3, "three closed windows");
+
+ // Check open window.
+ ok(JSON.stringify(windows).contains(URL_MAIN_WINDOW),
+ "open window is correct");
+
+ // Check closed windows.
+ ok(JSON.stringify(closedWindows[0]).contains(URL_ADD_WINDOW2),
+ "correct first additional window");
+ ok(JSON.stringify(closedWindows[1]).contains(URL_ADD_WINDOW1),
+ "correct second additional window");
+ ok(JSON.stringify(closedWindows[2]).contains(URL_CLOSED_WINDOW),
+ "correct main window");
+ } else {
+ // Check number of windows.
+ is(windows.length, 3, "three open windows");
+ is(closedWindows.length, 1, "one closed window");
+
+ // Check closed window.
+ ok(JSON.stringify(closedWindows).contains(URL_CLOSED_WINDOW),
+ "closed window is correct");
+
+ // Check that windows are in the right order.
+ ok(JSON.stringify(windows[0]).contains(URL_ADD_WINDOW1),
+ "correct first additional window");
+ ok(JSON.stringify(windows[1]).contains(URL_ADD_WINDOW2),
+ "correct second additional window");
+ ok(JSON.stringify(windows[2]).contains(URL_MAIN_WINDOW),
+ "correct main window");
+ }
+ }
+});
+
+function promiseNewWindow() {
+ return new Promise(resolve => whenNewWindowLoaded({private: false}, resolve));
+}
+
+function forgetClosedWindows() {
+ while (ss.getClosedWindowCount()) {
+ ss.forgetClosedWindow(0);
+ }
+}
--- a/browser/devtools/debugger/test/browser.ini
+++ b/browser/devtools/debugger/test/browser.ini
@@ -134,17 +134,16 @@ skip-if = os == "mac" || e10s # Bug 8954
[browser_dbg_breakpoints-contextmenu.js]
[browser_dbg_breakpoints-disabled-reload.js]
[browser_dbg_breakpoints-editor.js]
[browser_dbg_breakpoints-highlight.js]
[browser_dbg_breakpoints-new-script.js]
[browser_dbg_breakpoints-other-tabs.js]
[browser_dbg_breakpoints-pane.js]
[browser_dbg_breakpoints-reload.js]
-skip-if = (os == "linux") && debug # Bug 1076830
[browser_dbg_chrome-create.js]
[browser_dbg_chrome-debugging.js]
[browser_dbg_clean-exit-window.js]
skip-if = true # Bug 933950 (leaky test)
[browser_dbg_clean-exit.js]
[browser_dbg_closure-inspection.js]
[browser_dbg_cmd-blackbox.js]
[browser_dbg_cmd-break.js]
--- a/browser/devtools/debugger/test/doc_breakpoints-reload.html
+++ b/browser/devtools/debugger/test/doc_breakpoints-reload.html
@@ -1,12 +1,13 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!DOCTYPE html>
<head>
<meta charset="utf-8"/>
<title>Debugger Breakpoints Other Tabs Test Page</title>
</head>
<script>
- (function () {
+ function theTest() {
window.foo = "break on me";
- }());
+ }
+ theTest();
</script>
--- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
+++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm
@@ -72,17 +72,17 @@ function getChromeWindow(domWindow) {
function getFindBar(domWindow) {
if (PdfjsContentUtils.isRemote) {
return PdfjsContentUtils.getFindBar(domWindow);
}
var browser = getContainingBrowser(domWindow);
try {
var tabbrowser = browser.getTabBrowser();
- var tab = tabbrowser._getTabForBrowser(browser);
+ var tab = tabbrowser.getTabForBrowser(browser);
return tabbrowser.getFindBar(tab);
} catch (e) {
// FF22 has no _getTabForBrowser, and FF24 has no getFindBar
var chromeWindow = browser.ownerDocument.defaultView;
return chromeWindow.gFindBar;
}
}
--- a/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
+++ b/browser/extensions/pdfjs/content/PdfjsChromeUtils.jsm
@@ -280,17 +280,17 @@ let PdfjsChromeUtils = {
/*
* CPOW security features require chrome objects declare exposed
* properties via __exposedProps__. We don't want to expose things
* directly on the findbar, so we wrap the findbar in a smaller
* object here that supports the features pdf.js needs.
*/
function PdfjsFindbarWrapper(aBrowser) {
let tabbrowser = aBrowser.getTabBrowser();
- let tab = tabbrowser._getTabForBrowser(aBrowser);
+ let tab = tabbrowser.getTabForBrowser(aBrowser);
this._findbar = tabbrowser.getFindBar(tab);
};
PdfjsFindbarWrapper.prototype = {
__exposedProps__: {
addEventListener: "r",
removeEventListener: "r",
updateControlState: "r",
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -135,27 +135,32 @@ These should match what Safari and other
<!ENTITY closeWindow.accesskey "d">
<!ENTITY bookmarksMenu.label "Bookmarks">
<!ENTITY bookmarksMenu.accesskey "B">
<!ENTITY bookmarkThisPageCmd.label "Bookmark This Page">
<!ENTITY editThisBookmarkCmd.label "Edit This Bookmark">
<!ENTITY bookmarkThisPageCmd.commandkey "d">
<!ENTITY markPageCmd.commandkey "l">
+<!-- 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">
-<!ENTITY shareLinkCmd.label "Share This Link">
-<!ENTITY shareLinkCmd.accesskey "s">
-<!ENTITY shareImageCmd.label "Share This Image">
-<!ENTITY shareImageCmd.accesskey "s">
-<!ENTITY shareSelectCmd.label "Share Selection">
-<!ENTITY shareSelectCmd.accesskey "s">
-<!ENTITY shareVideoCmd.label "Share This Video">
-<!ENTITY shareVideoCmd.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 feedsMenu.label "Subscribe">
<!ENTITY subscribeToPageMenupopup.label "Subscribe to This Page">
<!ENTITY subscribeToPageMenuitem.label "Subscribe to This Page…">
<!ENTITY addCurPagesCmd.label "Bookmark All Tabs…">
<!ENTITY showAllBookmarks2.label "Show All Bookmarks">
<!ENTITY unsortedBookmarksCmd.label "Unsorted Bookmarks">
<!ENTITY bookmarksToolbarChevron.tooltip "Show more bookmarks">
@@ -687,17 +692,21 @@ just addresses the organization to follo
<!ENTITY social.activated.description "Services from <label/> have been enabled. You can change your settings for services in the <label class='text-link'>Add-on Manager</label>.">
<!ENTITY social.activated.undo.label "Oops, undo this!">
<!ENTITY social.activated.undo.accesskey "U">
<!ENTITY social.learnMore.label "Learn more…">
<!ENTITY social.learnMore.accesskey "l">
<!ENTITY social.closeNotificationItem.label "Not Now">
-
+<!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.tabTitle "Customize &brandShortName;">
<!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">
<!ENTITY customizeMode.restoreDefaults "Restore Defaults">
<!ENTITY customizeMode.toolbars "Show / Hide Toolbars">
<!ENTITY customizeMode.titlebar "Title Bar">
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
+++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
@@ -92,16 +92,19 @@ email-link-button.tooltiptext3 = Email a
# LOCALIZATION NOTE(quit-button.tooltiptext.linux2): %1$S is the brand name (e.g. Firefox),
# %2$S is the keyboard shortcut
quit-button.tooltiptext.linux2 = Quit %1$S (%2$S)
# LOCALIZATION NOTE(quit-button.tooltiptext.mac): %1$S is the brand name (e.g. Firefox),
# %2$S is the keyboard shortcut
quit-button.tooltiptext.mac = Quit %1$S (%2$S)
-loop-call-button.label = Invite someone to talk
-loop-call-button.tooltiptext = Invite someone to talk
+loop-call-button2.label = Start a conversation
+loop-call-button2.tooltiptext = Start a conversation
+
+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
web-apps-button.label = Apps
web-apps-button.tooltiptext = Discover Apps
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -1,15 +1,25 @@
# 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/.
# Panel Strings
+## LOCALIZATION NOTE(clientShortname2): This should not be localized and
+## should remain "Firefox Hello" for all locales.
+clientShortname2=Firefox Hello
+
+## LOCALIZATION_NOTE(first_time_experience.title): clientShortname will be
+## replaced by the brand name
+first_time_experience_title={{clientShortname}} — Join the conversation
+first_time_experience_button_label=Get Started
+
share_link_header_text=Share this link to invite someone to talk:
+invite_header_text=Invite someone to join you.
## LOCALIZATION NOTE(invitee_name_label): Displayed when obtaining a url.
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#precall-firstrun
## Click the label icon at the end of the url field.
invitee_name_label=Who are you inviting?
## LOCALIZATION NOTE(invitee_expire_days_label): Allows the user to adjust
## the expiry time. Click the label icon at the end of the url field to see where
## this is:
@@ -47,22 +57,24 @@ login_expired=Your Login Has Expired
service_not_available=Service Unavailable At This Time
problem_accessing_account=There Was A Problem Accessing Your Account
## LOCALIZATION NOTE(retry_button): Displayed when there is an error to retry
## the appropriate action.
## See https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error for location
retry_button=Retry
-share_email_subject3=You have been invited to a conversation
-## LOCALIZATION NOTE (share_email_body3): In this item, don't translate the
+share_email_subject4={{clientShortname}} — Join the conversation
+## LOCALIZATION NOTE (share_email_body4): In this item, don't translate the
## part between {{..}} and leave the \r\n\r\n part alone
-share_email_body3=To accept this invitation, just copy or click this link to start your conversation:\r\n\r\n{{callUrl}}
+share_email_body4=Hello!\r\n\r\nJoin me for a video conversation using {{clientShortname}}:\r\n\r\nYou don't have to download or install anything. Just copy and paste this URL into your browser:\r\n\r\n{{callUrl}}\r\n\r\nIf you want, you can also learn more about {{clientShortname}} at {{learnMoreUrl}}\r\n\r\nTalk to you soon!
share_button=Email
+share_button2=Email Link
copy_url_button=Copy
+copy_url_button2=Copy Link
copied_url_button=Copied!
panel_footer_signin_or_signup_link=Sign In or Sign Up
settings_menu_item_account=Account
settings_menu_item_settings=Settings
settings_menu_item_signout=Sign Out
settings_menu_item_signin=Sign In
@@ -237,22 +249,27 @@ cancel_button=Cancel
cannot_start_call_session_not_ready=Can't start call, session is not ready.
network_disconnected=The network connection terminated abruptly.
connection_error_see_console_notification=Call failed; see console for details.
## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
## parts between {{..}} because these will be replaced with links with the labels
## from legal_text_tos and legal_text_privacy. clientShortname will be replaced
-## by the brand name, or fall back to client_shortname_fallback
+## by the brand name.
legal_text_and_links3=By using {{clientShortname}} you agree to the {{terms_of_use}} \
and {{privacy_notice}}.
legal_text_tos = Terms of Use
legal_text_privacy = Privacy Notice
-client_shortname_fallback=this product
+
+## LOCALIZATION NOTE (powered_by_beforeLogo, powered_by_afterLogo):
+## These 2 strings are displayed before and after a 'Telefonica'
+## logo.
+powered_by_beforeLogo=Powered by
+powered_by_afterLogo=
feedback_call_experience_heading2=How was your conversation?
feedback_what_makes_you_sad=What makes you sad?
feedback_thank_you_heading=Thank you for your feedback!
feedback_category_audio_quality=Audio quality
feedback_category_video_quality=Video quality
feedback_category_was_disconnected=Was disconnected
feedback_category_confusing=Confusing
@@ -268,12 +285,31 @@ feedback_window_will_close_in2=This wind
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
+help_label=Help
+tour_label=Tour
+
+## LOCALIZATION NOTE(rooms_default_room_name_template): {{conversationLabel}}
+## will be replaced by a number. For example "Conversation 1" or "Conversation 12".
+rooms_default_room_name_template=Conversation {{conversationLabel}}
+rooms_leave_button_label=Leave
+rooms_list_copy_url_tooltip=Copy Link
## LOCALIZATION NOTE (rooms_list_current_conversations): We prefer to have no
## number in the string, but if you need it for your language please use {{num}}.
rooms_list_current_conversations=Current conversation;Current conversations
+rooms_list_delete_tooltip=Delete conversation
+rooms_list_deleteConfirmation_label=Are you sure?
rooms_list_no_current_conversations=No current conversations
+rooms_name_this_room_label=Name this conversation
+rooms_new_room_button_label=Start a conversation
+rooms_only_occupant_label=You're the first one here.
+rooms_panel_title=Choose a conversation or start a new one
+rooms_room_full_label=There are already two people in this conversation.
+rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
+rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
+rooms_room_joined_label=Someone has joined the conversation!
+rooms_room_join_label=Join the conversation
--- a/browser/modules/Social.jsm
+++ b/browser/modules/Social.jsm
@@ -7,18 +7,19 @@
this.EXPORTED_SYMBOLS = ["Social", "CreateSocialStatusWidget",
"CreateSocialMarkWidget", "OpenGraphBuilder",
"DynamicResizeWatcher", "sizeSocialPanelToContent"];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
-// The minimum sizes for the auto-resize panel code.
-const PANEL_MIN_HEIGHT = 100;
+// 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 = 200;
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",
@@ -308,16 +309,17 @@ function CreateSocialMarkWidget(aId, aPr
node.setAttribute('type', "socialmark");
node.style.listStyleImage = "url(" + (aProvider.unmarkedIcon || aProvider.icon32URL || aProvider.iconURL) + ")";
node.setAttribute("origin", aProvider.origin);
let window = aDocument.defaultView;
let menuLabel = window.gNavigatorBundle.getFormattedString("social.markpageMenu.label", [aProvider.name]);
node.setAttribute("label", menuLabel);
node.setAttribute("tooltiptext", menuLabel);
+ node.setAttribute("observes", "Social:PageShareOrMark");
return node;
}
});
};
// Error handling class used to listen for network errors in the social frames
// and replace them with a social-specific error page
@@ -345,40 +347,48 @@ SocialErrorListener.prototype = {
},
onStateChange: function SPL_onStateChange(aWebProgress, aRequest, aState, aStatus) {
let failure = false;
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)
+ // and 5xx (server errors). responseStatus throws if it is not set.
failure = aRequest.responseStatus >= 400 &&
aRequest.responseStatus < 600;
- } catch (e) {}
+ } catch (e) {
+ failure = aStatus == Components.results.NS_ERROR_CONNECTION_REFUSED;
+ }
}
}
// Calling cancel() will raise some OnStateChange notifications by itself,
// so avoid doing that more than once
if (failure && aStatus != Components.results.NS_BINDING_ABORTED) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
- let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
- provider.errorState = "content-error";
+ let origin = this.iframe.getAttribute("origin");
+ if (origin) {
+ let provider = Social._getProviderFromOrigin(origin);
+ provider.errorState = "content-error";
+ }
this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler);
}
},
onLocationChange: function SPL_onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) {
aRequest.cancel(Components.results.NS_BINDING_ABORTED);
- let provider = Social._getProviderFromOrigin(this.iframe.getAttribute("origin"));
- if (!provider.errorState)
- provider.errorState = "content-error";
+ let origin = this.iframe.getAttribute("origin");
+ if (origin) {
+ let provider = Social._getProviderFromOrigin(origin);
+ if (!provider.errorState)
+ provider.errorState = "content-error";
+ }
schedule(function() {
this.setErrorMessage(aWebProgress.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler);
}.bind(this));
}
},
onProgressChange: function SPL_onProgressChange() {},
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -60,17 +60,17 @@ this.webrtcUI = {
aScreen && state.screen;
}).map(aStream => {
let state = aStream.state;
let types = {camera: state.camera, microphone: state.microphone,
screen: state.screen};
let browser = aStream.browser;
let browserWindow = browser.ownerDocument.defaultView;
let tab = browserWindow.gBrowser &&
- browserWindow.gBrowser._getTabForBrowser(browser);
+ browserWindow.gBrowser.getTabForBrowser(browser);
return {uri: state.documentURI, tab: tab, browser: browser, types: types};
});
},
showSharingDoorhanger: function(aActiveStream, aType) {
let browserWindow = aActiveStream.browser.ownerDocument.defaultView;
if (aActiveStream.tab) {
browserWindow.gBrowser.selectedTab = aActiveStream.tab;
deleted file mode 100644
--- a/browser/themes/linux/aboutSocialError.css
+++ /dev/null
@@ -1,98 +0,0 @@
-body {
- background-color: rgb(241, 244, 248);
- margin-top: 2em;
- font: message-box;
- font-size: 100%;
-}
-
-p {
- font-size: .8em;
-}
-
-#error-box {
- background: url('chrome://global/skin/icons/information-24.png') no-repeat left 4px;
- -moz-padding-start: 30px;
-}
-
-#error-box:-moz-locale-dir(rtl) {
- background-position: right 4px;
-}
-
-#main-error-msg {
- color: #4b4b4b;
- font-weight: bold;
-}
-
-
-#button-box {
- text-align: center;
- width: 75%;
- margin: 0 auto;
-}
-
-@media all and (min-width: 300px) {
- #error-box {
- max-width: 50%;
- margin: 0 auto;
- background-image: url('chrome://global/skin/icons/information-32.png');
- min-height: 36px;
- -moz-padding-start: 38px;
- }
-
- button {
- width: auto !important;
- min-width: 150px;
- }
-}
-
-@media all and (min-width: 780px) {
- #error-box {
- max-width: 30%;
- }
-}
-
-button {
- font: message-box;
- font-size: 0.6875em;
- -moz-appearance: none;
- -moz-user-select: none;
- width: 100%;
- margin: 2px 0;
- padding: 2px 6px;
- line-height: 1.2;
- background-color: hsla(210,30%,95%,.1);
- background-image: linear-gradient(hsla(0,0%,100%,.6), hsla(0,0%,100%,.1));
- background-clip: padding-box;
- border: 1px solid hsla(210,15%,25%,.4);
- border-color: hsla(210,15%,25%,.3) hsla(210,15%,25%,.35) hsla(210,15%,25%,.4);
- border-radius: 3px;
- box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
- 0 0 0 1px hsla(0,0%,100%,.3) inset,
- 0 1px 0 hsla(0,0%,100%,.1);
-
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 150ms;
- transition-timing-function: ease;
-
-}
-
-button:hover {
- background-color: hsla(210,30%,95%,.8);
- border-color: hsla(210,15%,25%,.45) hsla(210,15%,25%,.5) hsla(210,15%,25%,.55);
- box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
- 0 0 0 1px hsla(0,0%,100%,.3) inset,
- 0 1px 0 hsla(0,0%,100%,.1),
- 0 0 3px hsla(210,15%,25%,.1);
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 150ms;
- transition-timing-function: ease;
-}
-
-button:hover:active {
- background-color: hsla(210,15%,25%,.2);
- box-shadow: 0 1px 1px hsla(210,15%,25%,.2) inset,
- 0 0 2px hsla(210,15%,25%,.4) inset;
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 10ms;
- transition-timing-function: linear;
-}
new file mode 100644
--- /dev/null
+++ b/browser/themes/linux/devedition.css
@@ -0,0 +1,5 @@
+% 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/.
+
+%include ../shared/devedition.inc.css
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -10,23 +10,25 @@ browser.jar:
skin/classic/browser/aboutSessionRestore-window-icon.png
skin/classic/browser/aboutWelcomeBack.css (../shared/aboutWelcomeBack.css)
skin/classic/browser/aboutCertError.css
skin/classic/browser/aboutCertError_sectionCollapsed.png
skin/classic/browser/aboutCertError_sectionCollapsed-rtl.png
skin/classic/browser/aboutCertError_sectionExpanded.png
skin/classic/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
- skin/classic/browser/aboutSocialError.css
+ skin/classic/browser/aboutSocialError.css (../shared/aboutSocialError.css)
+* skin/classic/browser/aboutProviderDirectory.css (../shared/aboutProviderDirectory.css)
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/aboutSyncTabs.css
#endif
skin/classic/browser/aboutTabCrashed.css
skin/classic/browser/actionicon-tab.png
* skin/classic/browser/browser.css
+* skin/classic/browser/devedition.css
* skin/classic/browser/browser-lightweightTheme.css
skin/classic/browser/click-to-play-warning-stripes.png
* skin/classic/browser/content-contextmenu.svg
* skin/classic/browser/engineManager.css
skin/classic/browser/fullscreen-darknoise.png
skin/classic/browser/Geolocation-16.png
skin/classic/browser/Geolocation-64.png
skin/classic/browser/identity.png
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1363,16 +1363,21 @@ toolbar .toolbarbutton-1 > .toolbarbutto
#PanelUI-fxa-status > .toolbarbutton-icon,
#PanelUI-quit > .toolbarbutton-icon,
#PanelUI-customize > .toolbarbutton-icon,
#PanelUI-help > .toolbarbutton-icon {
width: 16px;
}
+ #add-share-provider {
+ list-style-image: url(chrome://browser/skin/menuPanel-small@2x.png);
+ -moz-image-region: rect(0px, 192px, 32px, 160px);
+ }
+
#loop-call-button > .toolbarbutton-badge-container {
list-style-image: url("chrome://browser/skin/loop/toolbar@2x.png");
-moz-image-region: rect(0, 36px, 36px, 0);
}
toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
list-style-image: url("chrome://browser/skin/loop/toolbar-inverted@2x.png");
}
new file mode 100644
--- /dev/null
+++ b/browser/themes/osx/devedition.css
@@ -0,0 +1,5 @@
+% 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/.
+
+%include ../shared/devedition.inc.css
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -9,24 +9,26 @@ browser.jar:
skin/classic/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
* skin/classic/browser/aboutSessionRestore.css (aboutSessionRestore.css)
skin/classic/browser/aboutSessionRestore-window-icon.png
skin/classic/browser/aboutWelcomeBack.css (../shared/aboutWelcomeBack.css)
skin/classic/browser/aboutCertError.css
skin/classic/browser/aboutCertError_sectionCollapsed.png
skin/classic/browser/aboutCertError_sectionCollapsed-rtl.png
skin/classic/browser/aboutCertError_sectionExpanded.png
- skin/classic/browser/aboutSocialError.css
+ skin/classic/browser/aboutSocialError.css (../shared/aboutSocialError.css)
+* skin/classic/browser/aboutProviderDirectory.css (../shared/aboutProviderDirectory.css)
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/aboutSyncTabs.css
#endif
skin/classic/browser/aboutTabCrashed.css
skin/classic/browser/actionicon-tab.png
skin/classic/browser/actionicon-tab@2x.png
* skin/classic/browser/browser.css (browser.css)
+* skin/classic/browser/devedition.css
* skin/classic/browser/browser-lightweightTheme.css
skin/classic/browser/click-to-play-warning-stripes.png
* skin/classic/browser/content-contextmenu.svg
* skin/classic/browser/engineManager.css (engineManager.css)
skin/classic/browser/fullscreen-darknoise.png
skin/classic/browser/Geolocation-16.png
skin/classic/browser/Geolocation-16@2x.png
skin/classic/browser/Geolocation-64.png
new file mode 100644
--- /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;
+ -moz-padding-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;
+}
rename from browser/themes/osx/aboutSocialError.css
rename to browser/themes/shared/aboutSocialError.css
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/devedition.inc.css
@@ -0,0 +1,3 @@
+% 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/.
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -229,8 +229,13 @@ toolbarpaletteitem[place="palette"] > #e
toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-out-button {
-moz-image-region: rect(0px, 80px, 16px, 64px);
}
#zoom-controls@inAnyPanel@ > #zoom-in-button,
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.png);
+ -moz-image-region: rect(0px, 96px, 16px, 80px);
+}
\ No newline at end of file
deleted file mode 100644
--- a/browser/themes/windows/aboutSocialError.css
+++ /dev/null
@@ -1,98 +0,0 @@
-body {
- background-color: rgb(241, 244, 248);
- margin-top: 2em;
- font: message-box;
- font-size: 100%;
-}
-
-p {
- font-size: .8em;
-}
-
-#error-box {
- background: url('chrome://global/skin/icons/information-24.png') no-repeat left 4px;
- -moz-padding-start: 30px;
-}
-
-#error-box:-moz-locale-dir(rtl) {
- background-position: right 4px;
-}
-
-#main-error-msg {
- color: #4b4b4b;
- font-weight: bold;
-}
-
-
-#button-box {
- text-align: center;
- width: 75%;
- margin: 0 auto;
-}
-
-@media all and (min-width: 300px) {
- #error-box {
- max-width: 50%;
- margin: 0 auto;
- background-image: url('chrome://global/skin/icons/information-32.png');
- min-height: 36px;
- -moz-padding-start: 38px;
- }
-
- button {
- width: auto !important;
- min-width: 150px;
- }
-}
-
-@media all and (min-width: 780px) {
- #error-box {
- max-width: 30%;
- }
-}
-
-button {
- font: message-box;
- font-size: 0.6875em;
- -moz-appearance: none;
- -moz-user-select: none;
- width: 100%;
- margin: 2px 0;
- padding: 2px 6px;
- line-height: 1.2;
- background-color: hsla(210,30%,95%,.1);
- background-image: linear-gradient(hsla(0,0%,100%,.6), hsla(0,0%,100%,.1));
- background-clip: padding-box;
- border: 1px solid hsla(210,15%,25%,.4);
- border-color: hsla(210,15%,25%,.3) hsla(210,15%,25%,.35) hsla(210,15%,25%,.4);
- border-radius: 3px;
- box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
- 0 0 0 1px hsla(0,0%,100%,.3) inset,
- 0 1px 0 hsla(0,0%,100%,.1);
-
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 150ms;
- transition-timing-function: ease;
-
-}
-
-button:hover {
- background-color: hsla(210,30%,95%,.8);
- border-color: hsla(210,15%,25%,.45) hsla(210,15%,25%,.5) hsla(210,15%,25%,.55);
- box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset,
- 0 0 0 1px hsla(0,0%,100%,.3) inset,
- 0 1px 0 hsla(0,0%,100%,.1),
- 0 0 3px hsla(210,15%,25%,.1);
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 150ms;
- transition-timing-function: ease;
-}
-
-button:hover:active {
- background-color: hsla(210,15%,25%,.2);
- box-shadow: 0 1px 1px hsla(210,15%,25%,.2) inset,
- 0 0 2px hsla(210,15%,25%,.4) inset;
- transition-property: background-color, border-color, box-shadow;
- transition-duration: 10ms;
- transition-timing-function: linear;
-}
new file mode 100644
--- /dev/null
+++ b/browser/themes/windows/devedition-aero.css
@@ -0,0 +1,7 @@
+% 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/.
+
+%define WINDOWS_AERO
+%include devedition.css
+%undef WINDOWS_AERO
new file mode 100644
--- /dev/null
+++ b/browser/themes/windows/devedition.css
@@ -0,0 +1,5 @@
+% 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/.
+
+%include ../shared/devedition.inc.css
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -12,23 +12,25 @@ browser.jar:
skin/classic/browser/aboutSessionRestore-window-icon.png (preferences/application.png)
skin/classic/browser/aboutWelcomeBack.css (../shared/aboutWelcomeBack.css)
skin/classic/browser/aboutCertError.css
skin/classic/browser/aboutCertError_sectionCollapsed.png
skin/classic/browser/aboutCertError_sectionCollapsed-rtl.png
skin/classic/browser/aboutCertError_sectionExpanded.png
skin/classic/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
- skin/classic/browser/aboutSocialError.css
+ skin/classic/browser/aboutSocialError.css (../shared/aboutSocialError.css)
+* skin/classic/browser/aboutProviderDirectory.css (../shared/aboutProviderDirectory.css)
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/aboutSyncTabs.css
#endif
skin/classic/browser/aboutTabCrashed.css
skin/classic/browser/actionicon-tab.png
* skin/classic/browser/browser.css
+* skin/classic/browser/devedition.css
* skin/classic/browser/browser-lightweightTheme.css
skin/classic/browser/click-to-play-warning-stripes.png
* skin/classic/browser/content-contextmenu.svg
* skin/classic/browser/engineManager.css
skin/classic/browser/fullscreen-darknoise.png
skin/classic/browser/Geolocation-16.png
skin/classic/browser/Geolocation-64.png
skin/classic/browser/Info.png
@@ -433,24 +435,26 @@ browser.jar:
* skin/classic/aero/browser/aboutSessionRestore.css (aboutSessionRestore.css)
skin/classic/aero/browser/aboutSessionRestore-window-icon.png (aboutSessionRestore-window-icon-aero.png)
skin/classic/aero/browser/aboutCertError.css
skin/classic/aero/browser/aboutCertError_sectionCollapsed.png
skin/classic/aero/browser/aboutCertError_sectionCollapsed-rtl.png
skin/classic/aero/browser/aboutCertError_sectionExpanded.png
skin/classic/aero/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/aero/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
- skin/classic/aero/browser/aboutSocialError.css
+ skin/classic/aero/browser/aboutSocialError.css (../shared/aboutSocialError.css)
+* skin/classic/aero/browser/aboutProviderDirectory.css (../shared/aboutProviderDirectory.css)
#ifdef MOZ_SERVICES_SYNC
skin/classic/aero/browser/aboutSyncTabs.css
#endif
skin/classic/aero/browser/aboutTabCrashed.css
skin/classic/aero/browser/aboutWelcomeBack.css (../shared/aboutWelcomeBack.css)
skin/classic/aero/browser/actionicon-tab.png
* skin/classic/aero/browser/browser.css (browser-aero.css)
+* skin/classic/aero/browser/devedition.css (devedition-aero.css)
* skin/classic/aero/browser/browser-lightweightTheme.css
skin/classic/aero/browser/click-to-play-warning-stripes.png
* skin/classic/aero/browser/content-contextmenu.svg
* skin/classic/aero/browser/engineManager.css
skin/classic/aero/browser/fullscreen-darknoise.png
skin/classic/aero/browser/Geolocation-16.png
skin/classic/aero/browser/Geolocation-64.png
skin/classic/aero/browser/Info.png (Info-aero.png)
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -224,16 +224,18 @@ public class BrowserApp extends GeckoApp
private SharedPreferencesHelper mSharedPreferencesHelper;
private OrderedBroadcastHelper mOrderedBroadcastHelper;
private BroadcastReceiver mOnboardingReceiver;
private BrowserHealthReporter mBrowserHealthReporter;
+ private SystemBarTintManager mTintManager;
+
// The tab to be selected on editing mode exit.
private Integer mTargetTabForEditingMode;
// The animator used to toggle HomePager visibility has a race where if the HomePager is shown
// (starting the animation), the HomePager is hidden, and the HomePager animation completes,
// both the web content and the HomePager will be hidden. This flag is used to prevent the
// race by determining if the web content should be hidden at the animation's end.
private boolean mHideWebContentOnAnimationEnd;
@@ -676,19 +678,19 @@ public class BrowserApp extends GeckoApp
private void setupSystemUITinting() {
if (!Versions.feature19Plus) {
return;
}
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- SystemBarTintManager tintManager = new SystemBarTintManager(this);
- tintManager.setTintColor(getResources().getColor(R.color.background_tabs));
- tintManager.setStatusBarTintEnabled(true);
+ mTintManager = new SystemBarTintManager(this);
+ mTintManager.setTintColor(getResources().getColor(R.color.background_tabs));
+ mTintManager.setStatusBarTintEnabled(true);
}
/**
* Check and show Onboarding start pane if Firefox has never been launched and
* is not opening an external link from another application.
*
* @param context Context of application; used to show Start Pane if appropriate
* @param intentAction Intent that launched this activity
@@ -2369,16 +2371,21 @@ public class BrowserApp extends GeckoApp
// We need to account for scroll state for the touched view otherwise
// tapping on an "empty" part of the view will still be considered a
// valid touch event.
if (view.getScrollX() != 0 || view.getScrollY() != 0) {
view.getHitRect(mTempRect);
mTempRect.offset(-view.getScrollX(), -view.getScrollY());
+ if (mTintManager != null) {
+ SystemBarTintManager.SystemBarConfig config = mTintManager.getConfig();
+ mTempRect.offset(0, -config.getPixelInsetTop(false));
+ }
+
int[] viewCoords = new int[2];
view.getLocationOnScreen(viewCoords);
int x = (int) event.getRawX() - viewCoords[0];
int y = (int) event.getRawY() - viewCoords[1];
if (!mTempRect.contains(x, y))
return false;
@@ -2644,16 +2651,20 @@ public class BrowserApp extends GeckoApp
}
} else {
mBrowserChrome.setVisibility(View.VISIBLE);
if (mDynamicToolbar.isEnabled()) {
mDynamicToolbar.setVisible(true, VisibilityTransition.IMMEDIATE);
mLayerView.getLayerMarginsAnimator().setMaxMargins(0, mToolbarHeight, 0, 0);
}
}
+
+ if (mTintManager != null) {
+ mTintManager.setStatusBarTintEnabled(!fullscreen);
+ }
}
});
}
@Override
public boolean onPrepareOptionsMenu(Menu aMenu) {
if (aMenu == null)
return false;
--- a/mobile/android/base/RestrictedProfiles.java
+++ b/mobile/android/base/RestrictedProfiles.java
@@ -1,31 +1,27 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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/. */
package org.mozilla.gecko;
-import java.util.Set;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.lang.StringBuilder;
import java.util.ArrayList;
import java.util.List;
-import java.util.HashSet;
-
+import java.util.Set;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.mozglue.RobocopTarget;
-import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
-
+import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.UserManager;
import android.util.Log;
@RobocopTarget
public class RestrictedProfiles {
private static final String LOGTAG = "GeckoRestrictedProfiles";
@@ -82,57 +78,77 @@ public class RestrictedProfiles {
if (rest.id == action) {
return rest;
}
}
throw new IllegalArgumentException("Unknown action " + action);
}
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@RobocopTarget
private static Bundle getRestrictions() {
final UserManager mgr = (UserManager) GeckoAppShell.getContext().getSystemService(Context.USER_SERVICE);
return mgr.getUserRestrictions();
}
+ /**
+ * This method does the system version check for you.
+ *
+ * Returns false if the system doesn't support restrictions,
+ * or the provided value is not present in the set of user
+ * restrictions.
+ *
+ * Returns true otherwise.
+ */
+ private static boolean getRestriction(final String name) {
+ // Early versions don't support restrictions at all,
+ // so no action can be restricted.
+ if (Versions.preJBMR2) {
+ return false;
+ }
+
+ return getRestrictions().getBoolean(name, false);
+ }
+
private static boolean canLoadUrl(final String url) {
- // Null urls are always allowed
+ // Null URLs are always permitted.
if (url == null) {
return true;
}
try {
// If we're not in guest mode, and the system restriction isn't in place, everything is allowed.
if (!getInGuest() &&
- !getRestrictions().getBoolean(Restriction.DISALLOW_BROWSE_FILES.name, false)) {
+ !getRestriction(Restriction.DISALLOW_BROWSE_FILES.name)) {
return true;
}
- } catch(IllegalArgumentException ex) {
+ } catch (IllegalArgumentException ex) {
Log.i(LOGTAG, "Invalid action", ex);
}
final Uri u = Uri.parse(url);
final String scheme = u.getScheme();
if (BANNED_SCHEMES.contains(scheme)) {
return false;
}
for (String banned : BANNED_URLS) {
if (url.startsWith(banned)) {
return false;
}
}
- // TODO: The UserManager should support blacklisting urls by the device owner.
+ // TODO: The UserManager should support blacklisting URLs by the device owner.
return true;
}
@WrapElementForJNI
public static boolean isUserRestricted() {
- // Guest mode is supported in all Android versions
+ // Guest mode is supported in all Android versions.
if (getInGuest()) {
return true;
}
if (Versions.preJBMR2) {
return false;
}
@@ -140,44 +156,37 @@ public class RestrictedProfiles {
}
public static boolean isAllowed(Restriction action) {
return isAllowed(action.id, null);
}
@WrapElementForJNI
public static boolean isAllowed(int action, String url) {
+ // Guest users can't do anything.
+ if (getInGuest()) {
+ return false;
+ }
+
final Restriction restriction;
try {
restriction = geckoActionToRestriction(action);
- } catch(IllegalArgumentException ex) {
- return true;
+ } catch (IllegalArgumentException ex) {
+ // Unknown actions represent a coding error, so we
+ // refuse the action and log.
+ Log.e(LOGTAG, "Unknown action " + action + "; check calling code.");
+ return false;
}
if (Restriction.DISALLOW_BROWSE_FILES == restriction) {
return canLoadUrl(url);
}
- // ALl actions are blocked in Guest mode
- if (getInGuest()) {
- return false;
- }
-
- if (Versions.preJBMR2) {
- return true;
- }
-
- try {
- // NOTE: Restrictions hold the opposite intention, so we need to flip it
- return !getRestrictions().getBoolean(restriction.name, false);
- } catch(IllegalArgumentException ex) {
- Log.i(LOGTAG, "Invalid action", ex);
- }
-
- return true;
+ // NOTE: Restrictions hold the opposite intention, so we need to flip it.
+ return !getRestriction(restriction.name);
}
@WrapElementForJNI
public static String getUserRestrictions() {
// Guest mode is supported in all Android versions
if (getInGuest()) {
StringBuilder builder = new StringBuilder("{ ");
--- a/mobile/android/base/util/StringUtils.java
+++ b/mobile/android/base/util/StringUtils.java
@@ -6,16 +6,17 @@
package org.mozilla.gecko.util;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
public class StringUtils {
+ private static final String LOGTAG = "GeckoStringUtils";
private static final String FILTER_URL_PREFIX = "filter://";
private static final String USER_ENTERED_URL_PREFIX = "user-entered:";
/*
* This method tries to guess if the given string could be a search query or URL,
* and returns a previous result if there is ambiguity
*
@@ -188,13 +189,16 @@ public class StringUtils {
public static String encodeUserEnteredUrl(String url) {
return Uri.fromParts("user-entered", url, null).toString();
}
public static String getStringExtra(Intent intent, String name) {
try {
return intent.getStringExtra(name);
} catch (android.os.BadParcelableException ex) {
- Log.w("GeckoUtils", "Couldn't get string extra: malformed intent.");
+ Log.w(LOGTAG, "Couldn't get string extra: malformed intent.");
+ return null;
+ } catch (RuntimeException re) {
+ Log.w(LOGTAG, "Couldn't get string extra.", re);
return null;
}
}
}
--- a/services/fxaccounts/FxAccountsOAuthClient.jsm
+++ b/services/fxaccounts/FxAccountsOAuthClient.jsm
@@ -180,17 +180,17 @@ this.FxAccountsOAuthClient.prototype = {
this.tearDown();
// if the message asked to close the tab
if (data.closeWindow && target) {
// for e10s reasons the best way is to use the TabBrowser to close the tab.
let tabbrowser = target.getTabBrowser();
if (tabbrowser) {
- let tab = tabbrowser._getTabForBrowser(target);
+ let tab = tabbrowser.getTabForBrowser(target);
if (tab) {
tabbrowser.removeTab(tab);
log.debug("OAuth flow closed the tab.");
} else {
log.debug("OAuth flow failed to close the tab. Tab not found in TabBrowser.");
}
} else {
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/History.jsm
@@ -0,0 +1,259 @@
+/* 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";
+
+/**
+ * Asynchronous API for managing history.
+ *
+ *
+ * The API makes use of `PageInfo` and `VisitInfo` objects, defined as follows.
+ *
+ * A `PageInfo` object is any object that contains A SUBSET of the
+ * following properties:
+ * - guid: (string)
+ * The globally unique id of the page.
+ * - uri: (URL)
+ * or (nsIURI)
+ * or (string)
+ * The full URI of the page. Note that `PageInfo` values passed as
+ * argument may hold `nsIURI` or `string` values for property `uri`,
+ * but `PageInfo` objects returned by this module always hold `URL`
+ * values.
+ * - title: (string)
+ * The title associated with the page, if any.
+ * - frecency: (number)
+ * The frecency of the page, if any.
+ * See https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Places/Frecency_algorithm
+ * Note that this property may not be used to change the actualy frecency
+ * score of a page, only to retrieve it. In other words, any `frecency` field
+ * passed as argument to a function of this API will be ignored.
+ * - visits: (Array<VisitInfo>)
+ * All the visits for this page, if any.
+ *
+ * See the documentation of individual methods to find out which properties
+ * are required for `PageInfo` arguments or returned for `PageInfo` results.
+ *
+ * A `VisitInfo` object is any object that contains A SUBSET of the following
+ * properties:
+ * - date: (Date)
+ * The time the visit occurred.
+ * - transition: (number)
+ * How the user reached the page. See constants `TRANSITION_*`
+ * for the possible transition types.
+ * - referrer: (URL)
+ * or (nsIURI)
+ * or (string)
+ * The referring URI of this visit. Note that `VisitInfo` passed
+ * as argument may hold `nsIURI` or `string` values for property `referrer`,
+ * but `VisitInfo` objects returned by this module always hold `URL`
+ * values.
+ * See the documentation of individual methods to find out which properties
+ * are required for `VisitInfo` arguments or returned for `VisitInfo` results.
+ *
+ *
+ *
+ * Each successful operation notifies through the nsINavHistoryObserver
+ * interface. To listen to such notifications you must register using
+ * nsINavHistoryService `addObserver` and `removeObserver` methods.
+ * @see nsINavHistoryObserver
+ */
+
+this.EXPORTED_SYMBOLS = [ "History" ];
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Promise",
+ "resource://gre/modules/Promise.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
+ "resource://gre/modules/Sqlite.jsm");
+
+
+this.History = Object.freeze({
+ /**
+ * Fetch the available information for one page.
+ *
+ * @param guidOrURI: (URL or nsIURI)
+ * The full URI of the page.
+ * or (string)
+ * Either the full URI of the page or the GUID of the page.
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete.
+ * @resolves (PageInfo | null) If the page could be found, the information
+ * on that page. Note that this `PageInfo` does NOT contain the visit
+ * data (i.e. `visits` is `undefined`).
+ *
+ * @throws (Error)
+ * If `guidOrURI` does not have the expected type or if it is a string
+ * that may be parsed neither as a valid URL nor as a valid GUID.
+ */
+ fetch: function (guidOrURI) {
+ throw new Error("Method not implemented");
+ },
+
+ /**
+ * Adds a set of visits for one or more page.
+ *
+ * Any change may be observed through nsINavHistoryObserver
+ *
+ * @note This function recomputes the frecency of the page automatically,
+ * regardless of the value of property `frecency` passed as argument.
+ * @note If there is no entry for the page, the entry is created.
+ *
+ * @param infos: (PageInfo)
+ * Information on a page. This `PageInfo` MUST contain
+ * - either a property `guid` or a property `uri`, as specified
+ * by the definition of `PageInfo`;
+ * - a property `visits`, as specified by the definition of
+ * `PageInfo`, which MUST contain at least one visit.
+ * If a property `title` is provided, the title of the page
+ * is updated.
+ * If the `visitDate` of a visit is not provided, it defaults
+ * to now.
+ * or (Array<PageInfo>)
+ * An array of the above, to batch requests.
+ * @param onResult: (function(PageInfo), [optional])
+ * A callback invoked for each page, with the updated
+ * information on that page. Note that this `PageInfo`
+ * does NOT contain the visit data (i.e. `visits` is
+ * `undefined`).
+ *
+ * @return (Promise)
+ * A promise resolved once the operation is complete, including
+ * all calls to `onResult`.
+ * @resolves (bool)
+ * `true` if at least one page entry was created, `false` otherwise
+ * (i.e. if page entries were updated but not created).
+ *
+ * @throws (Error)
+ * If the `uri` specified was for a protocol that should not be
+ * stored (e.g. "chrome:", "mailbox:", "about:", "imap:", "news:",
+ * "moz-anno:", "view-source:", "resource:", "data:", "wyciwyg:",
+ * "javascript:", "blob:").
+ * @throws (Error)
+ * If `infos` has an unexpected type.
+ * @throws (Error)
+ * If a `PageInfo` has neither `guid` nor `uri`,
+ * @throws (Error)
+ * If a `guid` property provided is not a valid GUID.
+ * @throws (Error)
+ * If a `PageInfo` does not have a `visits` property or if the
+ * value of `visits` is ill-typed or is an empty array.
+ * @throws (Error)
+ * If an element of `visits` has an invalid `date`.
+ * @throws (Error)
+ * If an element of `visits` is missing `transition` or if
+ * the value of `transition` is invalid.
+ */
+ update: function (infos, onResult) {
+ throw new Error("Method not implemented");
+ },
+
+ /**
+ * Remove pages from the database.
+ *
+ * Any change may be observed through nsINavHistoryObserver
+ *
+ *
+ * @param page: (URL or nsIURI)
+ * The full URI of the page.
+ * or (string)
+ * Either the full URI of the page or the GUID of the page.
+ * or (Array<URL|nsIURI|string>)
+ * An array of the above, to batch requests.
+ * @param onResult: (function(PageInfo))
+ * A callback invoked for each page found.
+ *
+ * @return (Promise)
+ * A promise resoled once the operation is complete.
+ * @resolve (bool)
+ * `true` if at least one page was removed, `false` otherwise.
+ * @throws (Error)
+ * If `pages` has an unexpected type or if a string provided
+ * is neither a valid GUID nor a valid URI.
+ */
+ remove: function (pages, onResult) {
+ throw new Error("Method not implemented");
+ },
+
+ /**
+ * Determine if a page has been visited.
+ *
+ * @param pages: (URL or nsIURI)
+ * The full URI of the page.
+ * or (string)
+ * The full URI of the page or the GUID of the page.
+ *
+ * @return (Promise)
+ * A promise resoled once the operation is complete.
+ * @resolve (bool)
+ * `true` if the page has been visited, `false` otherwise.
+ * @throws (Error)
+ * If `pages` has an unexpected type or if a string provided
+ * is neither not a valid GUID nor a valid URI.
+ */
+ hasVisits: function(page, onResult) {
+ throw new Error("Method not implemented");
+ },
+
+ /**
+ * Possible values for the `transition` property of `VisitInfo`
+ * objects.
+ */
+
+ /**
+ * The user followed a link and got a new toplevel window.
+ */
+ TRANSITION_LINK: Ci.nsINavHistoryService.TRANSITION_LINK,
+
+ /**
+ * The user typed the page's URL in the URL bar or selected it from
+ * URL bar autocomplete results, clicked on it from a history query
+ * (from the History sidebar, History menu, or history query in the
+ * personal toolbar or Places organizer.
+ */
+ TRANSITION_TYPED: Ci.nsINavHistoryService.TRANSITION_TYPED,
+
+ /**
+ * The user followed a bookmark to get to the page.
+ */
+ TRANSITION_BOOKMARK: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+
+ /**
+ * Some inner content is loaded. This is true of all images on a
+ * page, and the contents of the iframe. It is also true of any
+ * content in a frame if the user did not explicitly follow a link
+ * to get there.
+ */
+ TRANSITION_EMBED: Ci.nsINavHistoryService.TRANSITION_EMBED,
+
+ /**
+ * Set when the transition was a permanent redirect.
+ */
+ TRANSITION_REDIRECT_PERMANENT: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+
+ /**
+ * Set when the transition was a temporary redirect.
+ */
+ TRANSITION_REDIRECT_TEMPORARY: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
+
+ /**
+ * Set when the transition is a download.
+ */
+ TRANSITION_DOWNLOAD: Ci.nsINavHistoryService.TRANSITION_REDIRECT_DOWNLOAD,
+
+ /**
+ * The user followed a link and got a visit in a frame.
+ */
+ TRANSITION_FRAMED_LINK: Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
+});
+
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -39,16 +39,18 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
"resource://gre/modules/Bookmarks.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "History",
+ "resource://gre/modules/History.jsm");
// The minimum amount of transactions before starting a batch. Usually we do
// do incremental updates, a batch will cause views to completely
// refresh instead.
const MIN_TRANSACTIONS_FOR_BATCH = 5;
#ifdef XP_MACOSX
// On Mac OSX, the transferable system converts "\r\n" to "\n\n", where we
@@ -1826,20 +1828,36 @@ this.PlacesUtils = {
rootItemCreationEx);
}
return rootItem;
})
};
XPCOMUtils.defineLazyGetter(PlacesUtils, "history", function() {
- return Cc["@mozilla.org/browser/nav-history-service;1"]
- .getService(Ci.nsINavHistoryService)
- .QueryInterface(Ci.nsIBrowserHistory)
- .QueryInterface(Ci.nsPIPlacesDatabase);
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService)
+ .QueryInterface(Ci.nsIBrowserHistory)
+ .QueryInterface(Ci.nsPIPlacesDatabase);
+ return Object.freeze(new Proxy(hs, {
+ get: function(target, name) {
+ let property, object;
+ if (name in target) {
+ property = target[name];
+ object = target;
+ } else {
+ property = History[name];
+ object = History;
+ }
+ if (typeof property == "function") {
+ return property.bind(object);
+ }
+ return property;
+ }
+ }));
});
XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "asyncHistory",
"@mozilla.org/browser/history;1",
"mozIAsyncHistory");
XPCOMUtils.defineLazyGetter(PlacesUtils, "bhistory", function() {
return PlacesUtils.history;
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -61,16 +61,17 @@ if CONFIG['MOZ_PLACES']:
EXTRA_JS_MODULES += [
'BookmarkHTMLUtils.jsm',
'BookmarkJSONUtils.jsm',
'Bookmarks.jsm',
'ClusterLib.js',
'ColorAnalyzer_worker.js',
'ColorConversion.js',
+ 'History.jsm',
'PlacesBackups.jsm',
'PlacesDBUtils.jsm',
'PlacesSearchAutocompleteProvider.jsm',
'PlacesTransactions.jsm',
]
EXTRA_PP_JS_MODULES += [
'PlacesUtils.jsm',
--- a/toolkit/components/social/SocialService.jsm
+++ b/toolkit/components/social/SocialService.jsm
@@ -147,18 +147,19 @@ XPCOMUtils.defineLazyGetter(SocialServic
", exception: " + err);
}
}
return providers;
});
function getOriginActivationType(origin) {
// if this is an about uri, treat it as a directory
- let originUri = Services.io.newURI(origin, null, null);
- if (originUri.scheme == "moz-safe-about") {
+ let URI = Services.io.newURI(origin, null, null);
+ let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(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';
@@ -580,89 +581,65 @@ this.SocialService = {
learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api",
};
let anchor = "servicesInstall-notification-icon";
let notificationid = "servicesInstall";
chromeWin.PopupNotifications.show(browser, notificationid, message, anchor,
action, [], options);
},
- installProvider: function(aDOMDocument, data, installCallback, aBypassUserEnable=false) {
+ installProvider: function(aDOMDocument, data, installCallback, options={}) {
let manifest;
let installOrigin = aDOMDocument.nodePrincipal.origin;
- if (data) {
- let installType = getOriginActivationType(installOrigin);
- // if we get data, we MUST have a valid manifest generated from the data
- manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
- if (!manifest)
- throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href);
+ let installType = getOriginActivationType(installOrigin);
+ // if we get data, we MUST have a valid manifest generated from the data
+ manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal);
+ if (!manifest)
+ throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href);
- let addon = new AddonWrapper(manifest);
- if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
- throw new Error("installProvider: provider with origin [" +
- installOrigin + "] 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.
- installOrigin = manifest.origin;
- }
+ let addon = new AddonWrapper(manifest);
+ if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
+ throw new Error("installProvider: provider with origin [" +
+ installOrigin + "] 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.
+ installOrigin = manifest.origin;
let id = getAddonIDFromOrigin(installOrigin);
AddonManager.getAddonByID(id, function(aAddon) {
if (aAddon && aAddon.userDisabled) {
aAddon.cancelUninstall();
aAddon.userDisabled = false;
}
schedule(function () {
- this._installProvider(aDOMDocument, manifest, aBypassUserEnable, aManifest => {
+ this._installProvider(aDOMDocument, manifest, options, aManifest => {
this._notifyProviderListeners("provider-installed", aManifest.origin);
installCallback(aManifest);
});
}.bind(this));
}.bind(this));
},
- _installProvider: function(aDOMDocument, manifest, aBypassUserEnable, installCallback) {
- let sourceURI = aDOMDocument.location.href;
- let installOrigin = aDOMDocument.nodePrincipal.origin;
+ _installProvider: function(aDOMDocument, manifest, options, installCallback) {
+ if (!manifest)
+ throw new Error("Cannot install provider without manifest data");
- let installType = getOriginActivationType(installOrigin);
- let installer;
- switch(installType) {
- case "foreign":
- if (!Services.prefs.getBoolPref("social.remote-install.enabled"))
- throw new Error("Remote install of services is disabled");
- if (!manifest)
- throw new Error("Cannot install provider without manifest data");
+ let installType = getOriginActivationType(aDOMDocument.nodePrincipal.origin);
+ if (installType == "foreign" && !Services.prefs.getBoolPref("social.remote-install.enabled"))
+ throw new Error("Remote install of services is disabled");
- installer = new AddonInstaller(sourceURI, manifest, installCallback);
- this._showInstallNotification(aDOMDocument, installer);
- break;
- case "internal":
- // double check here since "builtin" falls through this as well.
- aBypassUserEnable = installType == "internal" && manifest.oneclick;
- case "directory":
- // a manifest is requried, and will have been vetted by reviewers. We
- // also handle in-product installations without the verification step.
- if (aBypassUserEnable) {
- installer = new AddonInstaller(sourceURI, manifest, installCallback);
- installer.install();
- return;
- }
- // a manifest is required, we'll catch a missing manifest below.
- if (!manifest)
- throw new Error("Cannot install provider without manifest data");
- installer = new AddonInstaller(sourceURI, manifest, installCallback);
- this._showInstallNotification(aDOMDocument, installer);
- break;
- default:
- throw new Error("SocialService.installProvider: Invalid install type "+installType+"\n");
- break;
- }
+ let installer = new AddonInstaller(aDOMDocument.location.href, manifest, installCallback);
+ let bypassPanel = options.bypassInstallPanel ||
+ (installType == "internal" && manifest.oneclick);
+ if (bypassPanel)
+ installer.install();
+ else
+ this._showInstallNotification(aDOMDocument, installer);
},
createWrapper: function(manifest) {
return new AddonWrapper(manifest);
},
/**
* updateProvider is used from the worker to self-update. Since we do not