author | Shane Caraveo <scaraveo@mozilla.com> |
Wed, 04 Sep 2013 10:01:38 -0700 | |
changeset 145493 | 58a67d0f50bcde0e65e1c5d7f5d07422233a41d2 |
parent 145492 | 00209f73f7b636ae7ad780b86c6eaf831cb0092f |
child 145494 | 7fd6c2e59b65fcc6727f403f0941771c173d3fae |
push id | 25214 |
push user | kwierso@gmail.com |
push date | Thu, 05 Sep 2013 00:02:20 +0000 |
treeherder | mozilla-central@99bd249e5a20 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | markh |
bugs | 891225 |
milestone | 26.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/base/content/browser-social.js +++ b/browser/base/content/browser-social.js @@ -32,16 +32,20 @@ SocialUI = { init: function SocialUI_init() { Services.obs.addObserver(this, "social:ambient-notification-changed", false); Services.obs.addObserver(this, "social:profile-changed", false); Services.obs.addObserver(this, "social:page-mark-config", false); Services.obs.addObserver(this, "social:frameworker-error", false); Services.obs.addObserver(this, "social:provider-set", false); Services.obs.addObserver(this, "social:providers-changed", false); Services.obs.addObserver(this, "social:provider-reload", false); + Services.obs.addObserver(this, "social:provider-installed", false); + Services.obs.addObserver(this, "social:provider-uninstalled", false); + Services.obs.addObserver(this, "social:provider-enabled", false); + Services.obs.addObserver(this, "social:provider-disabled", false); Services.prefs.addObserver("social.sidebar.open", this, false); Services.prefs.addObserver("social.toast-notifications.enabled", this, false); gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true); if (!Social.initialized) { Social.init(); @@ -57,30 +61,46 @@ SocialUI = { uninit: function SocialUI_uninit() { Services.obs.removeObserver(this, "social:ambient-notification-changed"); Services.obs.removeObserver(this, "social:profile-changed"); Services.obs.removeObserver(this, "social:page-mark-config"); Services.obs.removeObserver(this, "social:frameworker-error"); Services.obs.removeObserver(this, "social:provider-set"); Services.obs.removeObserver(this, "social:providers-changed"); Services.obs.removeObserver(this, "social:provider-reload"); + Services.obs.removeObserver(this, "social:provider-installed"); + Services.obs.removeObserver(this, "social:provider-uninstalled"); + Services.obs.removeObserver(this, "social:provider-enabled"); + Services.obs.removeObserver(this, "social:provider-disabled"); Services.prefs.removeObserver("social.sidebar.open", this); Services.prefs.removeObserver("social.toast-notifications.enabled", this); }, _matchesCurrentProvider: function (origin) { return Social.provider && Social.provider.origin == origin; }, observe: function SocialUI_observe(subject, topic, data) { // Exceptions here sometimes don't get reported properly, report them // manually :( try { switch (topic) { + case "social:provider-installed": + SocialStatus.setPosition(data); + break; + case "social:provider-uninstalled": + SocialStatus.removePosition(data); + break; + case "social:provider-enabled": + SocialStatus.populateToolbarPalette(); + break; + case "social:provider-disabled": + SocialStatus.removeProvider(data); + break; case "social:provider-reload": // if the reloaded provider is our current provider, fall through // to social:provider-set so the ui will be reset if (!Social.provider || Social.provider.origin != data) return; // be sure to unload the sidebar as it will not reload if the origin // has not changed, it will be loaded in provider-set below. Other // panels will be unloaded or handle reload. @@ -93,28 +113,31 @@ SocialUI = { this._updateMenuItems(); SocialFlyout.unload(); SocialChatBar.update(); SocialShare.update(); SocialSidebar.update(); SocialMark.update(); SocialToolbar.update(); + SocialStatus.populateToolbarPalette(); SocialMenu.populate(); break; case "social:providers-changed": // the list of providers changed - this may impact the "active" UI. this._updateActiveUI(); // and the multi-provider menu SocialToolbar.populateProviderMenus(); SocialShare.populateProviderMenu(); + SocialStatus.populateToolbarPalette(); break; // Provider-specific notifications case "social:ambient-notification-changed": + SocialStatus.updateNotification(data); if (this._matchesCurrentProvider(data)) { SocialToolbar.updateButton(); SocialMenu.populate(); } break; case "social:profile-changed": if (this._matchesCurrentProvider(data)) { SocialToolbar.updateProvider(); @@ -1051,16 +1074,25 @@ SocialToolbar = { (!Social.provider.haveLoggedInUser() && Social.provider.profile !== undefined)) { // Either no enabled provider, or there is a provider and it has // responded with a profile and the user isn't loggedin. The icons // etc have already been removed by updateButtonHiddenState, so we want // to nuke any cached icons we have and get out of here! Services.prefs.clearUserPref(CACHE_PREF_NAME); return; } + + // If the provider uses the new SocialStatus button, then they do not get + // to use the ambient icons in the old toolbar button. Since the status + // button depends on multiple workers, if not enabled we will ignore this + // limitation. That allows a provider to migrate to the new functionality + // once we enable multiple workers. + if (Social.provider.statusURL && Social.allowMultipleWorkers) + return; + let icons = Social.provider.ambientNotificationIcons; let iconNames = Object.keys(icons); if (Social.provider.profile === undefined) { // provider has not told us about the login state yet - see if we have // a cached version for this provider. let cached; try { @@ -1149,19 +1181,18 @@ SocialToolbar = { ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText", [ariaLabel, badge]); toolbarButton.setAttribute("aria-label", ariaLabel); } let socialToolbarItem = document.getElementById("social-toolbar-item"); socialToolbarItem.insertBefore(toolbarButtons, SocialMark.button); for (let frame of createdFrames) { - if (frame.socialErrorListener) { + if (frame.socialErrorListener) frame.socialErrorListener.remove(); - } if (frame.docShell) { frame.docShell.isActive = false; Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this)); } } }, showAmbientPopup: function SocialToolbar_showAmbientPopup(aToolbarButton) { @@ -1196,21 +1227,20 @@ SocialToolbar = { aToolbarButton.parentNode.removeAttribute("open"); dynamicResizer.stop(); notificationFrame.docShell.isActive = false; dispatchPanelEvent("socialFrameHide"); }); panel.addEventListener("popupshown", function onpopupshown() { panel.removeEventListener("popupshown", onpopupshown); - // This attribute is needed on both the button and the - // containing toolbaritem since the buttons on OS X have - // moz-appearance:none, while their container gets - // moz-appearance:toolbarbutton due to the way that toolbar buttons - // get combined on OS X. + // The "open" attribute is needed on both the button and the containing + // toolbaritem since the buttons on OS X have moz-appearance:none, while + // their container gets moz-appearance:toolbarbutton due to the way that + // toolbar buttons get combined on OS X. aToolbarButton.setAttribute("open", "true"); aToolbarButton.parentNode.setAttribute("open", "true"); notificationFrame.docShell.isActive = true; notificationFrame.docShell.isAppTab = true; if (notificationFrame.contentDocument.readyState == "complete" && wasAlive) { dynamicResizer.start(panel, notificationFrame); dispatchPanelEvent("socialFrameShow"); } else { @@ -1387,9 +1417,358 @@ SocialSidebar = { sbrowser.setAttribute("src", "about:socialerror?mode=workerFailure"); } else { let url = encodeURIComponent(Social.provider.sidebarURL); sbrowser.loadURI("about:socialerror?mode=tryAgain&url=" + url, null, null); } } } +// this helper class is used by removable/customizable buttons to handle +// location persistence and insertion into palette and/or toolbars + +// When a provider is installed we show all their UI so the user will see the +// functionality of what they installed. The user can later customize the UI, +// moving buttons around or off the toolbar. +// +// To make this happen, on install we add a button id to the navbar currentset. +// On enabling the provider (happens just after install) we insert the button +// into the toolbar as well. The button is then persisted on restart (assuming +// it was not removed). +// +// When a provider is disabled, we do not remove the buttons from currentset. +// That way, if the provider is re-enabled during the same session, the buttons +// will reappear where they were before. When a provider is uninstalled, we make +// sure that the id is removed from currentset. +// +// On startup, we insert the buttons of any enabled provider into either the +// apropriate toolbar or the palette. +function ToolbarHelper(type, createButtonFn) { + this._createButton = createButtonFn; + this._type = type; +} + +ToolbarHelper.prototype = { + idFromOrgin: function(origin) { + return this._type + "-" + origin; + }, + + // find a button either in the document or the palette + _getExistingButton: function(id) { + let button = document.getElementById(id); + if (button) + return button; + let palette = document.getElementById("navigator-toolbox").palette; + let paletteItem = palette.firstChild; + while (paletteItem) { + if (paletteItem.id == id) + return paletteItem; + paletteItem = paletteItem.nextSibling; + } + return null; + }, + + setPersistentPosition: function(id) { + // called when a provider is installed. add provider buttons to nav-bar + let toolbar = document.getElementById("nav-bar"); + // first startups will not have a currentset attribute, always rely on + // currentSet since it will be derived from the defaultset in that case. + let currentset = toolbar.currentSet; + if (currentset == "__empty") + currentset = [] + else + currentset = currentset.split(","); + if (currentset.indexOf(id) >= 0) + return; + // we do not set toolbar.currentSet since that will try to add the button, + // and we have not added it yet (happens on provider being enabled) + currentset.push(id); + toolbar.setAttribute("currentset", currentset.join(",")); + document.persist(toolbar.id, "currentset"); + }, + + removeProviderButton: function(origin) { + // this will remove the button from the palette or the toolbar + let button = this._getExistingButton(this.idFromOrgin(origin)); + if (button) + button.parentNode.removeChild(button); + }, + + removePersistence: function(id) { + let persisted = document.querySelectorAll("*[currentset]"); + for (let pent of persisted) { + // the button will have been removed, but left in the currentset attribute + // in case the user re-enables (e.g. undo in addon manager). So we only + // check the attribute here. + let currentset = pent.getAttribute("currentset").split(","); + + let pos = currentset.indexOf(id); + if (pos >= 0) { + currentset.splice(pos, 1); + pent.setAttribute("currentset", currentset.join(",")); + document.persist(pent.id, "currentset"); + return; + } + } + }, + + // if social is entirely disabled, we need to clear the palette, but leave + // the persisted id's in place + clearPalette: function() { + [this.removeProviderButton(p.origin) for (p of Social.providers)]; + }, + + // should be called on startup of each window, otherwise the addon manager + // listener will handle new activations, or enable/disabling of a provider + // XXX we currently call more regularly, will fix during refactoring + populatePalette: function() { + if (!Social.enabled) { + this.clearPalette(); + return; + } + let persisted = document.querySelectorAll("*[currentset]"); + let persistedById = {}; + for (let pent of persisted) { + let pset = pent.getAttribute("currentset").split(','); + for (let id of pset) + persistedById[id] = pent; + } + + // create any buttons that do not exist yet if they have been persisted + // as a part of the UI (otherwise they belong in the palette). + for (let provider of Social.providers) { + let id = this.idFromOrgin(provider.origin); + if (this._getExistingButton(id)) + return; + let button = this._createButton(provider); + if (button && persistedById.hasOwnProperty(id)) { + let parent = persistedById[id]; + let pset = persistedById[id].getAttribute("currentset").split(','); + let pi = pset.indexOf(id) + 1; + let next = document.getElementById(pset[pi]); + parent.insertItem(id, next, null, false); + } + } + } +} + +SocialStatus = { + populateToolbarPalette: function() { + if (!Social.allowMultipleWorkers) + return; + this._toolbarHelper.populatePalette(); + }, + + setPosition: function(origin) { + if (!Social.allowMultipleWorkers) + return; + // this is called during install, before the provider is enabled so we have + // to use the manifest rather than the provider instance as we do elsewhere. + let manifest = Social.getManifestByOrigin(origin); + if (!manifest.statusURL) + return; + let tbh = this._toolbarHelper; + tbh.setPersistentPosition(tbh.idFromOrgin(origin)); + }, + + removePosition: function(origin) { + if (!Social.allowMultipleWorkers) + return; + let tbh = this._toolbarHelper; + tbh.removePersistence(tbh.idFromOrgin(origin)); + }, + + removeProvider: function(origin) { + if (!Social.allowMultipleWorkers) + return; + this._toolbarHelper.removeProviderButton(origin); + }, + + get _toolbarHelper() { + delete this._toolbarHelper; + this._toolbarHelper = new ToolbarHelper("social-status-button", this._createButton.bind(this)); + return this._toolbarHelper; + }, + + get _dynamicResizer() { + delete this._dynamicResizer; + this._dynamicResizer = new DynamicResizeWatcher(); + return this._dynamicResizer; + }, + + _createButton: function(provider) { + if (!provider.statusURL) + return null; + let palette = document.getElementById("navigator-toolbox").palette; + let button = document.createElement("toolbarbutton"); + button.setAttribute("class", "toolbarbutton-1 social-status-button"); + button.setAttribute("type", "badged"); + button.setAttribute("removable", "true"); + button.setAttribute("image", provider.iconURL); + button.setAttribute("label", provider.name); + button.setAttribute("tooltiptext", provider.name); + button.setAttribute("origin", provider.origin); + button.setAttribute("oncommand", "SocialStatus.showPopup(this);"); + button.setAttribute("id", this._toolbarHelper.idFromOrgin(provider.origin)); + palette.appendChild(button); + return button; + }, + + // status panels are one-per button per-process, we swap the docshells between + // windows when necessary + _attachNotificatonPanel: function(aButton, provider) { + let panel = document.getElementById("social-notification-panel"); + panel.hidden = !SocialUI.enabled; + let notificationFrameId = "social-status-" + provider.origin; + let frame = document.getElementById(notificationFrameId); + + if (!frame) { + frame = SharedFrame.createFrame( + notificationFrameId, /* frame name */ + panel, /* parent */ + { + "type": "content", + "mozbrowser": "true", + "class": "social-panel-frame", + "id": notificationFrameId, + "tooltip": "aHTMLTooltip", + + // work around bug 793057 - by making the panel roughly the final size + // we are more likely to have the anchor in the correct position. + "style": "width: " + PANEL_MIN_WIDTH + "px;", + + "origin": provider.origin, + "src": provider.statusURL + } + ); + + if (frame.socialErrorListener) + frame.socialErrorListener.remove(); + if (frame.docShell) { + frame.docShell.isActive = false; + Social.setErrorListener(frame, this.setPanelErrorMessage.bind(this)); + } + } else { + frame.setAttribute("origin", provider.origin); + SharedFrame.updateURL(notificationFrameId, provider.statusURL); + } + aButton.setAttribute("notificationFrameId", notificationFrameId); + }, + + updateNotification: function(origin) { + if (!Social.allowMultipleWorkers) + return; + let provider = Social._getProviderFromOrigin(origin); + let button = document.getElementById(this._toolbarHelper.idFromOrgin(provider.origin)); + if (button) { + // we only grab the first notification, ignore all others + let icons = provider.ambientNotificationIcons; + let iconNames = Object.keys(icons); + let notif = icons[iconNames[0]]; + if (!notif) { + button.setAttribute("badge", ""); + button.setAttribute("aria-label", ""); + button.setAttribute("tooltiptext", ""); + return; + } + + button.style.listStyleImage = "url(" + notif.iconURL || provider.iconURL + ")"; + button.setAttribute("tooltiptext", notif.label); + + let badge = notif.counter || ""; + button.setAttribute("badge", badge); + let ariaLabel = notif.label; + // if there is a badge value, we must use a localizable string to insert it. + if (badge) + ariaLabel = gNavigatorBundle.getFormattedString("social.aria.toolbarButtonBadgeText", + [ariaLabel, badge]); + button.setAttribute("aria-label", ariaLabel); + } + }, + + showPopup: function(aToolbarButton) { + if (!Social.allowMultipleWorkers) + return; + // attach our notification panel if necessary + let origin = aToolbarButton.getAttribute("origin"); + let provider = Social._getProviderFromOrigin(origin); + this._attachNotificatonPanel(aToolbarButton, provider); + + let panel = document.getElementById("social-notification-panel"); + let notificationFrameId = aToolbarButton.getAttribute("notificationFrameId"); + let notificationFrame = document.getElementById(notificationFrameId); + + let wasAlive = SharedFrame.isGroupAlive(notificationFrameId); + SharedFrame.setOwner(notificationFrameId, notificationFrame); + + // Clear dimensions on all browsers so the panel size will + // only use the selected browser. + let frameIter = panel.firstElementChild; + while (frameIter) { + frameIter.collapsed = (frameIter != notificationFrame); + frameIter = frameIter.nextElementSibling; + } + + function dispatchPanelEvent(name) { + let evt = notificationFrame.contentDocument.createEvent("CustomEvent"); + evt.initCustomEvent(name, true, true, {}); + notificationFrame.contentDocument.documentElement.dispatchEvent(evt); + } + + let dynamicResizer = this._dynamicResizer; + panel.addEventListener("popuphidden", function onpopuphiding() { + panel.removeEventListener("popuphidden", onpopuphiding); + aToolbarButton.removeAttribute("open"); + dynamicResizer.stop(); + notificationFrame.docShell.isActive = false; + dispatchPanelEvent("socialFrameHide"); + }); + + panel.addEventListener("popupshown", function onpopupshown() { + panel.removeEventListener("popupshown", onpopupshown); + // This attribute is needed on both the button and the + // containing toolbaritem since the buttons on OS X have + // moz-appearance:none, while their container gets + // moz-appearance:toolbarbutton due to the way that toolbar buttons + // get combined on OS X. + aToolbarButton.setAttribute("open", "true"); + notificationFrame.docShell.isActive = true; + notificationFrame.docShell.isAppTab = true; + if (notificationFrame.contentDocument.readyState == "complete" && wasAlive) { + dynamicResizer.start(panel, notificationFrame); + dispatchPanelEvent("socialFrameShow"); + } else { + // first time load, wait for load and dispatch after load + notificationFrame.addEventListener("load", function panelBrowserOnload(e) { + notificationFrame.removeEventListener("load", panelBrowserOnload, true); + dynamicResizer.start(panel, notificationFrame); + dispatchPanelEvent("socialFrameShow"); + }, true); + } + }); + + let navBar = document.getElementById("nav-bar"); + let anchor = navBar.getAttribute("mode") == "text" ? + document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-text") : + document.getAnonymousElementByAttribute(aToolbarButton, "class", "toolbarbutton-badge-container"); + // Bug 849216 - open the popup in a setTimeout so we avoid the auto-rollup + // handling from preventing it being opened in some cases. + setTimeout(function() { + panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false); + }, 0); + }, + + setPanelErrorMessage: function(aNotificationFrame) { + if (!aNotificationFrame) + return; + + let src = aNotificationFrame.getAttribute("src"); + aNotificationFrame.removeAttribute("src"); + aNotificationFrame.webNavigation.loadURI("about:socialerror?mode=tryAgainOnly&url=" + + encodeURIComponent(src), + null, null, null, null); + let panel = aNotificationFrame.parentNode; + sizeSocialPanelToContent(panel, aNotificationFrame); + }, + +}; + })();
--- a/browser/base/content/test/social/Makefile.in +++ b/browser/base/content/test/social/Makefile.in @@ -26,16 +26,17 @@ MOCHITEST_BROWSER_FILES = \ browser_social_mozSocial_API.js \ browser_social_isVisible.js \ browser_social_chatwindow.js \ browser_social_chatwindow_resize.js \ browser_social_chatwindowfocus.js \ browser_social_multiprovider.js \ browser_social_multiworker.js \ browser_social_errorPage.js \ + browser_social_status.js \ browser_social_window.js \ social_activate.html \ social_activate_iframe.html \ browser_share.js \ social_panel.html \ social_mark_image.png \ social_sidebar.html \ social_chat.html \
--- a/browser/base/content/test/social/browser_addons.js +++ b/browser/base/content/test/social/browser_addons.js @@ -45,19 +45,20 @@ function test() { finish(); }); } function installListener(next, aManifest) { let expectEvent = "onInstalling"; let prefname = getManifestPrefname(aManifest); // wait for the actual removal to call next - SocialService.registerProviderListener(function providerListener(topic, data) { - if (topic == "provider-removed") { + SocialService.registerProviderListener(function providerListener(topic, origin, providers) { + if (topic == "provider-disabled") { SocialService.unregisterProviderListener(providerListener); + is(origin, aManifest.origin, "provider disabled"); executeSoon(next); } }); return { onInstalling: function(addon) { is(expectEvent, "onInstalling", "install started"); is(addon.manifest.origin, aManifest.origin, "provider about to be installed"); @@ -290,24 +291,25 @@ var tests = { let installFrom = doc.nodePrincipal.origin; Services.prefs.setCharPref("social.whitelist", installFrom); Social.installProvider(doc, manifest2, function(addonManifest) { SocialService.addBuiltinProvider(addonManifest.origin, function(provider) { is(provider.manifest.version, 1, "manifest version is 1"); Social.enabled = true; // watch for the provider-update and test the new version - SocialService.registerProviderListener(function providerListener(topic, data) { + SocialService.registerProviderListener(function providerListener(topic, origin, providers) { if (topic != "provider-update") return; + is(origin, addonManifest.origin, "provider updated") SocialService.unregisterProviderListener(providerListener); Services.prefs.clearUserPref("social.whitelist"); - let provider = Social._getProviderFromOrigin(addonManifest.origin); + let provider = Social._getProviderFromOrigin(origin); is(provider.manifest.version, 2, "manifest version is 2"); - Social.uninstallProvider(addonManifest.origin, function() { + Social.uninstallProvider(origin, function() { gBrowser.removeTab(tab); next(); }); }); let port = provider.getWorkerPort(); port.onmessage = function (e) { let topic = e.data.topic;
--- a/browser/base/content/test/social/browser_blocklist.js +++ b/browser/base/content/test/social/browser_blocklist.js @@ -150,24 +150,25 @@ var tests = { onWindowTitleChange: function(aXULWindow, aNewTitle) { } }; Services.wm.addListener(listener); setManifestPref("social.manifest.blocked", manifest_bad); try { SocialService.addProvider(manifest_bad, function(provider) { - // the act of blocking should cause a 'provider-removed' notification + // the act of blocking should cause a 'provider-disabled' notification // from SocialService. - SocialService.registerProviderListener(function providerListener(topic) { - if (topic != "provider-removed") + SocialService.registerProviderListener(function providerListener(topic, origin, providers) { + if (topic != "provider-disabled") return; SocialService.unregisterProviderListener(providerListener); + is(origin, provider.origin, "provider disabled"); SocialService.getProvider(provider.origin, function(p) { - ok(p==null, "blocklisted provider removed"); + ok(p == null, "blocklisted provider disabled"); Services.prefs.clearUserPref("social.manifest.blocked"); resetBlocklist(finish); }); }); // no callback - the act of updating should cause the listener above // to fire. setAndUpdateBlocklist(blocklistURL); });
--- a/browser/base/content/test/social/browser_social_mozSocial_API.js +++ b/browser/base/content/test/social/browser_social_mozSocial_API.js @@ -14,16 +14,23 @@ function test() { }; runSocialTestWithProvider(manifest, function (finishcb) { runSocialTests(tests, undefined, undefined, finishcb); }); } var tests = { testStatusIcons: function(next) { + let icon = { + name: "testIcon", + iconURL: "chrome://browser/skin/Info.png", + contentPanel: "https://example.com/browser/browser/base/content/test/social/social_panel.html", + counter: 1 + }; + let iconsReady = false; let gotSidebarMessage = false; function checkNext() { if (iconsReady && gotSidebarMessage) triggerIconPanel(); } @@ -66,16 +73,16 @@ var tests = { next(); } break; case "got-sidebar-message": // The sidebar message will always come first, since it loads by default ok(true, "got sidebar message"); gotSidebarMessage = true; // load a status panel - port.postMessage({topic: "test-ambient-notification"}); + port.postMessage({topic: "test-ambient-notification", data: icon}); checkNext(); break; } } port.postMessage({topic: "test-init"}); } }
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/social/browser_social_status.js @@ -0,0 +1,220 @@ +/* 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/. */ + +let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService; + +let manifest = { // builtin provider + name: "provider example.com", + 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", + iconURL: "https://example.com/browser/browser/base/content/test/moz.png" +}; +let manifest2 = { // used for testing install + name: "provider test1", + origin: "https://test1.example.com", + workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js", + statusURL: "https://test1.example.com/browser/browser/base/content/test/social/social_panel.html", + iconURL: "https://test1.example.com/browser/browser/base/content/test/moz.png", + version: 1 +}; +let manifest3 = { // used for testing install + name: "provider test2", + origin: "https://test2.example.com", + sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html", + iconURL: "https://test2.example.com/browser/browser/base/content/test/moz.png", + version: 1 +}; + + +function openWindowAndWaitForInit(callback) { + let topic = "browser-delayed-startup-finished"; + let w = OpenBrowserWindow(); + Services.obs.addObserver(function providerSet(subject, topic, data) { + Services.obs.removeObserver(providerSet, topic); + executeSoon(() => callback(w)); + }, topic, false); +} + +function test() { + waitForExplicitFinish(); + + Services.prefs.setBoolPref("social.allowMultipleWorkers", true); + let toolbar = document.getElementById("nav-bar"); + let currentsetAtStart = toolbar.currentSet; + info("tb0 "+currentsetAtStart); + runSocialTestWithProvider(manifest, function () { + runSocialTests(tests, undefined, undefined, function () { + Services.prefs.clearUserPref("social.remote-install.enabled"); + // just in case the tests failed, clear these here as well + Services.prefs.clearUserPref("social.allowMultipleWorkers"); + Services.prefs.clearUserPref("social.whitelist"); + + // This post-test test ensures that a new window maintains the same + // toolbar button set as when we started. That means our insert/removal of + // persistent id's is working correctly + is(currentsetAtStart, toolbar.currentSet, "toolbar currentset unchanged"); + openWindowAndWaitForInit(function(w1) { + checkSocialUI(w1); + // Sometimes the new window adds other buttons to currentSet that are + // outside the scope of what we're checking. So we verify that all + // buttons from startup are in currentSet for a new window, and that the + // provider buttons are properly removed. (e.g on try, window-controls + // was not present in currentsetAtStart, but present on the second + // window) + let tb1 = w1.document.getElementById("nav-bar"); + info("tb0 "+toolbar.currentSet); + info("tb1 "+tb1.currentSet); + let startupSet = Set(toolbar.currentSet.split(',')); + let newSet = Set(tb1.currentSet.split(',')); + let intersect = Set([x for (x of startupSet) if (newSet.has(x))]); + info("intersect "+intersect); + let difference = Set([x for (x of newSet) if (!startupSet.has(x))]); + info("difference "+difference); + is(startupSet.size, intersect.size, "new window toolbar same as old"); + // verify that our provider buttons are not in difference + let id = SocialStatus._toolbarHelper.idFromOrgin(manifest2.origin); + ok(!difference.has(id), "status button not persisted at end"); + w1.close(); + finish(); + }); + }); + }); +} + +var tests = { + testNoButtonOnInstall: function(next) { + // we expect the addon install dialog to appear, we need to accept the + // install from the dialog. + info("Waiting for install dialog"); + let panel = document.getElementById("servicesInstall-notification"); + PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() { + PopupNotifications.panel.removeEventListener("popupshown", onpopupshown); + info("servicesInstall-notification panel opened"); + panel.button.click(); + }) + + let id = "social-status-button-" + manifest3.origin; + let toolbar = document.getElementById("nav-bar"); + let currentset = toolbar.getAttribute("currentset").split(','); + ok(currentset.indexOf(id) < 0, "button is not part of currentset at start"); + + let activationURL = manifest3.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + Social.installProvider(doc, manifest3, function(addonManifest) { + // enable the provider so we know the button would have appeared + SocialService.addBuiltinProvider(manifest3.origin, function(provider) { + ok(provider, "provider is installed"); + currentset = toolbar.getAttribute("currentset").split(','); + ok(currentset.indexOf(id) < 0, "button was not added to currentset"); + Social.uninstallProvider(manifest3.origin, function() { + gBrowser.removeTab(tab); + next(); + }); + }); + }); + }); + }, + testButtonOnInstall: function(next) { + // we expect the addon install dialog to appear, we need to accept the + // install from the dialog. + info("Waiting for install dialog"); + let panel = document.getElementById("servicesInstall-notification"); + PopupNotifications.panel.addEventListener("popupshown", function onpopupshown() { + PopupNotifications.panel.removeEventListener("popupshown", onpopupshown); + info("servicesInstall-notification panel opened"); + panel.button.click(); + }) + + let activationURL = manifest2.origin + "/browser/browser/base/content/test/social/social_activate.html" + addTab(activationURL, function(tab) { + let doc = tab.linkedBrowser.contentDocument; + Social.installProvider(doc, manifest2, function(addonManifest) { + // at this point, we should have a button id in the currentset for our provider + let id = "social-status-button-" + manifest2.origin; + let toolbar = document.getElementById("nav-bar"); + + waitForCondition(function() { + let currentset = toolbar.getAttribute("currentset").split(','); + return currentset.indexOf(id) >= 0; + }, + function() { + // no longer need the tab + gBrowser.removeTab(tab); + next(); + }, "status button added to currentset"); + }); + }); + }, + testButtonOnEnable: function(next) { + // enable the provider now + SocialService.addBuiltinProvider(manifest2.origin, function(provider) { + ok(provider, "provider is installed"); + let id = "social-status-button-" + manifest2.origin; + waitForCondition(function() { return document.getElementById(id) }, + next, "button exists after enabling social"); + }); + }, + testStatusPanel: function(next) { + let icon = { + name: "testIcon", + iconURL: "chrome://browser/skin/Info.png", + counter: 1 + }; + // click on panel to open and wait for visibility + let provider = Social._getProviderFromOrigin(manifest2.origin); + let id = "social-status-button-" + provider.origin; + let btn = document.getElementById(id) + ok(btn, "got a status button"); + let port = provider.getWorkerPort(); + + port.onmessage = function (e) { + let topic = e.data.topic; + switch (topic) { + case "test-init-done": + ok(true, "test-init-done received"); + ok(provider.profile.userName, "profile was set by test worker"); + btn.click(); + break; + case "got-social-panel-visibility": + ok(true, "got the panel message " + e.data.result); + if (e.data.result == "shown") { + let panel = document.getElementById("social-notification-panel"); + panel.hidePopup(); + } else { + port.postMessage({topic: "test-ambient-notification", data: icon}); + port.close(); + waitForCondition(function() { return btn.getAttribute("badge"); }, + function() { + is(btn.style.listStyleImage, "url(\"" + icon.iconURL + "\")", "notification icon updated"); + next(); + }, "button updated by notification"); + } + break; + } + }; + port.postMessage({topic: "test-init"}); + }, + testButtonOnDisable: function(next) { + // enable the provider now + let provider = Social._getProviderFromOrigin(manifest2.origin); + ok(provider, "provider is installed"); + SocialService.removeProvider(manifest2.origin, function() { + let id = "social-status-button-" + manifest2.origin; + waitForCondition(function() { return !document.getElementById(id) }, + next, "button does not exist after disabling the provider"); + }); + }, + testButtonOnUninstall: function(next) { + Social.uninstallProvider(manifest2.origin, function() { + // test that the button is no longer persisted + let id = "social-status-button-" + manifest2.origin; + let toolbar = document.getElementById("nav-bar"); + let currentset = toolbar.getAttribute("currentset").split(','); + is(currentset.indexOf(id), -1, "button no longer in currentset"); + next(); + }); + } +}
--- a/browser/base/content/test/social/head.js +++ b/browser/base/content/test/social/head.js @@ -172,17 +172,17 @@ function runSocialTests(tests, cbPreTest // We run on a timeout as the frameworker also makes use of timeouts, so // this helps keep the debug messages sane. executeSoon(function() { function cleanupAndRunNextTest() { info("sub-test " + name + " complete"); cbPostTest(runNextTest); } cbPreTest(function() { - is(providersAtStart, Social.providers.length, "pre-test: no new providers left enabled"); + info("pre-test: starting with " + Social.providers.length + " providers"); info("sub-test " + name + " starting"); try { func.call(tests, cleanupAndRunNextTest); } catch (ex) { ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack); cleanupAndRunNextTest(); } })
--- a/browser/base/content/test/social/social_activate.html +++ b/browser/base/content/test/social/social_activate.html @@ -9,16 +9,17 @@ var data = { "name": "Demo Social Service", "iconURL": "chrome://branding/content/icon16.png", "icon32URL": "chrome://branding/content/favicon32.png", "icon64URL": "chrome://branding/content/icon64.png", // at least one of these must be defined "sidebarURL": "/browser/browser/base/content/test/social/social_sidebar.html", "workerURL": "/browser/browser/base/content/test/social/social_worker.js", + "statusURL": "/browser/browser/base/content/test/social/social_panel.html", // should be available for display purposes "description": "A short paragraph about this provider", "author": "Shane Caraveo, Mozilla", // optional "version": 1 }
--- a/browser/base/content/test/social/social_worker.js +++ b/browser/base/content/test/social/social_worker.js @@ -114,23 +114,17 @@ onconnect = function(e) { markedTooltip: "Unmark this page", unmarkedLabel: "Mark", markedLabel: "Unmark", } } }); break; case "test-ambient-notification": - let icon = { - name: "testIcon", - iconURL: "chrome://browser/skin/Info.png", - contentPanel: "https://example.com/browser/browser/base/content/test/social/social_panel.html", - counter: 1 - }; - apiPort.postMessage({topic: "social.ambient-notification", data: icon}); + apiPort.postMessage({topic: "social.ambient-notification", data: event.data.data}); break; case "test-isVisible": sidebarPort.postMessage({topic: "test-isVisible"}); break; case "test-isVisible-response": testPort.postMessage({topic: "got-isVisible-response", result: event.data.result}); break; case "share-data-message":
--- a/browser/modules/Social.jsm +++ b/browser/modules/Social.jsm @@ -164,34 +164,38 @@ this.Social = { // Retrieve the current set of providers, and set the current provider. SocialService.getOrderedProviderList(function (providers) { Social._updateProviderCache(providers); Social._updateWorkerState(true); }); } // Register an observer for changes to the provider list - SocialService.registerProviderListener(function providerListener(topic, data) { + SocialService.registerProviderListener(function providerListener(topic, origin, providers) { // An engine change caused by adding/removing a provider should notify. // any providers we receive are enabled in the AddonsManager - if (topic == "provider-added" || topic == "provider-removed") { - Social._updateProviderCache(data); + if (topic == "provider-installed" || topic == "provider-uninstalled") { + // installed/uninstalled do not send the providers param + Services.obs.notifyObservers(null, "social:" + topic, origin); + return; + } + if (topic == "provider-enabled" || topic == "provider-disabled") { + Social._updateProviderCache(providers); Social._updateWorkerState(true); Services.obs.notifyObservers(null, "social:providers-changed", null); + Services.obs.notifyObservers(null, "social:" + topic, origin); return; } if (topic == "provider-update") { // a provider has self-updated its manifest, we need to update our cache // and reload the provider. - let provider = data; - SocialService.getOrderedProviderList(function(providers) { - Social._updateProviderCache(providers); - provider.reload(); - Services.obs.notifyObservers(null, "social:providers-changed", null); - }); + Social._updateProviderCache(providers); + let provider = Social._getProviderFromOrigin(origin); + provider.reload(); + Services.obs.notifyObservers(null, "social:providers-changed", null); } }); }, _updateWorkerState: function(enable) { // ensure that our providers are all disabled, and enabled if we allow // multiple workers if (enable && !Social.allowMultipleWorkers) @@ -254,16 +258,20 @@ this.Social = { for (let p of this.providers) { if (p.origin == origin) { return p; } } return null; }, + getManifestByOrigin: function(origin) { + return SocialService.getManifestByOrigin(origin); + }, + installProvider: function(doc, data, installCallback) { SocialService.installProvider(doc, data, installCallback); }, uninstallProvider: function(origin, aCallback) { SocialService.uninstallProvider(origin, aCallback); },
--- a/toolkit/components/social/SocialService.jsm +++ b/toolkit/components/social/SocialService.jsm @@ -57,24 +57,16 @@ let SocialServiceInternal = { if (manifest && typeof(manifest) == "object" && manifest.origin) yield manifest; } catch (err) { Cu.reportError("SocialService: failed to load manifest: " + pref + ", exception: " + err); } } }, - getManifestByOrigin: function(origin) { - for (let manifest of SocialServiceInternal.manifests) { - if (origin == manifest.origin) { - return manifest; - } - } - return null; - }, getManifestPrefname: function(origin) { // Retrieve the prefname for a given origin/manifest. // If no existing pref, return a generated prefname. let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest."); let prefs = MANIFEST_PREFS.getChildList("", []); for (let pref of prefs) { try { var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data); @@ -386,17 +378,17 @@ this.SocialService = { // provider exists, or the activated provider on success. addBuiltinProvider: function addBuiltinProvider(origin, onDone) { if (SocialServiceInternal.providers[origin]) { schedule(function() { onDone(SocialServiceInternal.providers[origin]); }); return; } - let manifest = SocialServiceInternal.getManifestByOrigin(origin); + let manifest = SocialService.getManifestByOrigin(origin); if (manifest) { let addon = new AddonWrapper(manifest); AddonManagerPrivate.callAddonListeners("onEnabling", addon, false); addon.pendingOperations |= AddonManager.PENDING_ENABLE; this.addProvider(manifest, onDone); addon.pendingOperations -= AddonManager.PENDING_ENABLE; AddonManagerPrivate.callAddonListeners("onEnabled", addon); return; @@ -411,30 +403,30 @@ this.SocialService = { if (SocialServiceInternal.providers[manifest.origin]) throw new Error("SocialService.addProvider: provider with this origin already exists"); let provider = new SocialProvider(manifest); SocialServiceInternal.providers[provider.origin] = provider; ActiveProviders.add(provider.origin); this.getOrderedProviderList(function (providers) { - this._notifyProviderListeners("provider-added", providers); + this._notifyProviderListeners("provider-enabled", provider.origin, providers); if (onDone) onDone(provider); }.bind(this)); }, // Removes a provider with the given origin, and notifies when the removal is // complete. removeProvider: function removeProvider(origin, onDone) { if (!(origin in SocialServiceInternal.providers)) throw new Error("SocialService.removeProvider: no provider with origin " + origin + " exists!"); let provider = SocialServiceInternal.providers[origin]; - let manifest = SocialServiceInternal.getManifestByOrigin(origin); + let manifest = SocialService.getManifestByOrigin(origin); let addon = manifest && new AddonWrapper(manifest); if (addon) { AddonManagerPrivate.callAddonListeners("onDisabling", addon, false); addon.pendingOperations |= AddonManager.PENDING_DISABLE; } provider.enabled = false; ActiveProviders.delete(provider.origin); @@ -445,17 +437,17 @@ this.SocialService = { // we have to do this now so the addon manager ui will update an uninstall // correctly. addon.pendingOperations -= AddonManager.PENDING_DISABLE; AddonManagerPrivate.callAddonListeners("onDisabled", addon); AddonManagerPrivate.notifyAddonChanged(addon.id, ADDON_TYPE_SERVICE, false); } this.getOrderedProviderList(function (providers) { - this._notifyProviderListeners("provider-removed", providers); + this._notifyProviderListeners("provider-disabled", origin, providers); if (onDone) onDone(); }.bind(this)); }, // Returns a single provider object with the specified origin. The provider // must be "installed" (ie, in ActiveProviders) getProvider: function getProvider(origin, onDone) { @@ -466,16 +458,25 @@ this.SocialService = { // Returns an unordered array of installed providers getProviderList: function(onDone) { schedule(function () { onDone(SocialServiceInternal.providerArray); }); }, + getManifestByOrigin: function(origin) { + for (let manifest of SocialServiceInternal.manifests) { + if (origin == manifest.origin) { + return manifest; + } + } + return null; + }, + // Returns an array of installed providers, sorted by frecency getOrderedProviderList: function(onDone) { SocialServiceInternal.orderedProviders(onDone); }, getOriginActivationType: function (origin) { return getOriginActivationType(origin); }, @@ -483,28 +484,28 @@ this.SocialService = { _providerListeners: new Map(), registerProviderListener: function registerProviderListener(listener) { this._providerListeners.set(listener, 1); }, unregisterProviderListener: function unregisterProviderListener(listener) { this._providerListeners.delete(listener); }, - _notifyProviderListeners: function (topic, data) { + _notifyProviderListeners: function (topic, origin, providers) { for (let [listener, ] of this._providerListeners) { try { - listener(topic, data); + listener(topic, origin, providers); } catch (ex) { Components.utils.reportError("SocialService: provider listener threw an exception: " + ex); } } }, _manifestFromData: function(type, data, principal) { - let sameOriginRequired = ['workerURL', 'sidebarURL', 'shareURL']; + let sameOriginRequired = ['workerURL', 'sidebarURL', 'shareURL', 'statusURL']; if (type == 'directory') { // directory provided manifests must have origin in manifest, use that if (!data['origin']) { Cu.reportError("SocialService.manifestFromData directory service provided manifest without origin."); return null; } let URI = Services.io.newURI(data.origin, null, null); @@ -512,18 +513,19 @@ this.SocialService = { } // force/fixup origin data.origin = principal.origin; // workerURL, sidebarURL is required and must be same-origin // iconURL and name are required // iconURL may be a different origin (CDN or data url support) if this is // a whitelisted or directory listed provider - if (!data['workerURL'] && !data['sidebarURL'] && !data['shareURL']) { - Cu.reportError("SocialService.manifestFromData manifest missing required workerURL or sidebarURL."); + let providerHasFeatures = [url for (url of sameOriginRequired) if (data[url])].length > 0; + if (!providerHasFeatures) { + Cu.reportError("SocialService.manifestFromData manifest missing required urls."); return null; } if (!data['name'] || !data['iconURL']) { Cu.reportError("SocialService.manifestFromData manifest missing name or iconURL."); return null; } for (let url of sameOriginRequired) { if (data[url]) { @@ -591,17 +593,20 @@ this.SocialService = { installOrigin + "] is blocklisted"); AddonManager.getAddonByID(id, function(aAddon) { if (aAddon && aAddon.userDisabled) { aAddon.cancelUninstall(); aAddon.userDisabled = false; } schedule(function () { - this._installProvider(aDOMDocument, data, installCallback); + this._installProvider(aDOMDocument, data, aManifest => { + this._notifyProviderListeners("provider-installed", aManifest.origin); + installCallback(aManifest); + }); }.bind(this)); }.bind(this)); }, _installProvider: function(aDOMDocument, data, installCallback) { let sourceURI = aDOMDocument.location.href; let installOrigin = aDOMDocument.nodePrincipal.origin; @@ -656,17 +661,17 @@ this.SocialService = { } }, /** * updateProvider is used from the worker to self-update. Since we do not * have knowledge of the currently selected provider here, we will notify * the front end to deal with any reload. */ - updateProvider: function(aUpdateOrigin, aManifest, aCallback) { + updateProvider: function(aUpdateOrigin, aManifest) { let originUri = Services.io.newURI(aUpdateOrigin, null, null); let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri); let installType = this.getOriginActivationType(aUpdateOrigin); // if we get data, we MUST have a valid manifest generated from the data let manifest = this._manifestFromData(installType, aManifest, principal); if (!manifest) throw new Error("SocialService.installProvider: service configuration is invalid from " + aUpdateOrigin); @@ -677,23 +682,25 @@ this.SocialService = { Services.prefs.setComplexValue(getPrefnameFromOrigin(manifest.origin), Ci.nsISupportsString, string); // overwrite the existing provider then notify the front end so it can // handle any reload that might be necessary. if (ActiveProviders.has(manifest.origin)) { let provider = new SocialProvider(manifest); SocialServiceInternal.providers[provider.origin] = provider; // update the cache and ui, reload provider if necessary - this._notifyProviderListeners("provider-update", provider); + this.getOrderedProviderList(providers => { + this._notifyProviderListeners("provider-update", provider.origin, providers); + }); } }, uninstallProvider: function(origin, aCallback) { - let manifest = SocialServiceInternal.getManifestByOrigin(origin); + let manifest = SocialService.getManifestByOrigin(origin); let addon = new AddonWrapper(manifest); addon.uninstall(aCallback); } }; /** * The SocialProvider object represents a social provider, and allows * access to its FrameWorker (if it has one). @@ -715,16 +722,17 @@ function SocialProvider(input) { this.name = input.name; this.iconURL = input.iconURL; this.icon32URL = input.icon32URL; this.icon64URL = input.icon64URL; this.workerURL = input.workerURL; this.sidebarURL = input.sidebarURL; this.shareURL = input.shareURL; + this.statusURL = input.statusURL; this.origin = input.origin; let originUri = Services.io.newURI(input.origin, null, null); this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri); this.ambientNotificationIcons = {}; this.errorState = null; this.frecency = 0; let activationType = getOriginActivationType(input.origin); @@ -761,17 +769,17 @@ SocialProvider.prototype = { if (enable) { this._activate(); } else { this._terminate(); } }, get manifest() { - return SocialServiceInternal.getManifestByOrigin(this.origin); + return SocialService.getManifestByOrigin(this.origin); }, // Reference to a workerAPI object for this provider. Null if the provider has // no FrameWorker, or is disabled. workerAPI: null, // Contains information related to the user's profile. Populated by the // workerAPI via updateUserProfile. @@ -1007,17 +1015,17 @@ function getAddonIDFromOrigin(origin) { function getPrefnameFromOrigin(origin) { return "social.manifest." + SocialServiceInternal.getManifestPrefname(origin); } function AddonInstaller(sourceURI, aManifest, installCallback) { aManifest.updateDate = Date.now(); // get the existing manifest for installDate - let manifest = SocialServiceInternal.getManifestByOrigin(aManifest.origin); + let manifest = SocialService.getManifestByOrigin(aManifest.origin); let isNewInstall = !manifest; if (manifest && manifest.installDate) aManifest.installDate = manifest.installDate; else aManifest.installDate = aManifest.updateDate; this.sourceURI = sourceURI; this.install = function() { @@ -1083,16 +1091,17 @@ var SocialAddonProvider = { }, removeAddon: function(aAddon, aCallback) { AddonManagerPrivate.callAddonListeners("onUninstalling", aAddon, false); aAddon.pendingOperations |= AddonManager.PENDING_UNINSTALL; Services.prefs.clearUserPref(getPrefnameFromOrigin(aAddon.manifest.origin)); aAddon.pendingOperations -= AddonManager.PENDING_UNINSTALL; AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon); + SocialService._notifyProviderListeners("provider-uninstalled", aAddon.manifest.origin); if (aCallback) schedule(aCallback); } } function AddonWrapper(aManifest) { this.manifest = aManifest;