diff options
Diffstat (limited to 'browser/modules')
-rw-r--r-- | browser/modules/BrowserUITelemetry.jsm | 10 | ||||
-rw-r--r-- | browser/modules/Social.jsm | 272 | ||||
-rw-r--r-- | browser/modules/SocialService.jsm | 1097 | ||||
-rw-r--r-- | browser/modules/moz.build | 2 |
4 files changed, 1376 insertions, 5 deletions
diff --git a/browser/modules/BrowserUITelemetry.jsm b/browser/modules/BrowserUITelemetry.jsm index a6a5789f4..392462b45 100644 --- a/browser/modules/BrowserUITelemetry.jsm +++ b/browser/modules/BrowserUITelemetry.jsm @@ -694,7 +694,7 @@ this.BrowserUITelemetry = { "spell-undo-add-to-dictionary", "openlinkincurrent", "openlinkintab", "openlink", // "openlinkprivate" intentionally omitted for privacy reasons. See bug 1176391. - "bookmarklink", "savelink", + "bookmarklink", "sharelink", "savelink", "marklinkMenu", "copyemail", "copylink", "media-play", "media-pause", "media-mute", "media-unmute", "media-playbackrate", "media-playbackrate-050x", "media-playbackrate-100x", @@ -702,12 +702,12 @@ this.BrowserUITelemetry = { "media-showcontrols", "media-hidecontrols", "video-fullscreen", "leave-dom-fullscreen", "reloadimage", "viewimage", "viewvideo", "copyimage-contents", "copyimage", - "copyvideourl", "copyaudiourl", "saveimage", "sendimage", + "copyvideourl", "copyaudiourl", "saveimage", "shareimage", "sendimage", "setDesktopBackground", "viewimageinfo", "viewimagedesc", "savevideo", - "saveaudio", "video-saveimage", "sendvideo", "sendaudio", - "ctp-play", "ctp-hide", "savepage", "pocket", "markpageMenu", + "sharevideo", "saveaudio", "video-saveimage", "sendvideo", "sendaudio", + "ctp-play", "ctp-hide", "sharepage", "savepage", "pocket", "markpageMenu", "viewbgimage", "undo", "cut", "copy", "paste", "delete", "selectall", - "keywordfield", "searchselect", "frame", "showonlythisframe", + "keywordfield", "searchselect", "shareselect", "frame", "showonlythisframe", "openframeintab", "openframe", "reloadframe", "bookmarkframe", "saveframe", "printframe", "viewframesource", "viewframeinfo", "viewpartialsource-selection", "viewpartialsource-mathml", diff --git a/browser/modules/Social.jsm b/browser/modules/Social.jsm new file mode 100644 index 000000000..1569e0122 --- /dev/null +++ b/browser/modules/Social.jsm @@ -0,0 +1,272 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["Social", "OpenGraphBuilder", + "DynamicResizeWatcher", "sizeSocialPanelToContent"]; + +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cu = Components.utils; + +// The minimum sizes for the auto-resize panel code, minimum size necessary to +// properly show the error page in the panel. +const PANEL_MIN_HEIGHT = 190; +const PANEL_MIN_WIDTH = 330; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", + "resource:///modules/CustomizableUI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "SocialService", + "resource:///modules/SocialService.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PageMetadata", + "resource://gre/modules/PageMetadata.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", + "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); + + +this.Social = { + initialized: false, + lastEventReceived: 0, + providers: [], + _disabledForSafeMode: false, + + init: function Social_init() { + this._disabledForSafeMode = Services.appinfo.inSafeMode && this.enabled; + let deferred = Promise.defer(); + + if (this.initialized) { + deferred.resolve(true); + return deferred.promise; + } + this.initialized = true; + // if SocialService.hasEnabledProviders, retreive the providers so the + // front-end can generate UI + if (SocialService.hasEnabledProviders) { + // Retrieve the current set of providers, and set the current provider. + SocialService.getOrderedProviderList(function (providers) { + Social._updateProviderCache(providers); + Social._updateEnabledState(SocialService.enabled); + deferred.resolve(false); + }); + } else { + deferred.resolve(false); + } + + // Register an observer for changes to the provider list + SocialService.registerProviderListener(function providerListener(topic, origin, providers) { + // An engine change caused by adding/removing a provider should notify. + // any providers we receive are enabled in the AddonsManager + if (topic == "provider-installed" || topic == "provider-uninstalled") { + // installed/uninstalled do not send the providers param + Services.obs.notifyObservers(null, "social:" + topic, origin); + return; + } + if (topic == "provider-enabled") { + Social._updateProviderCache(providers); + Social._updateEnabledState(true); + Services.obs.notifyObservers(null, "social:" + topic, origin); + return; + } + if (topic == "provider-disabled") { + // a provider was removed from the list of providers, update states + Social._updateProviderCache(providers); + Social._updateEnabledState(providers.length > 0); + Services.obs.notifyObservers(null, "social:" + topic, origin); + return; + } + if (topic == "provider-update") { + // a provider has self-updated its manifest, we need to update our cache + // and reload the provider. + Social._updateProviderCache(providers); + let provider = Social._getProviderFromOrigin(origin); + provider.reload(); + } + }); + return deferred.promise; + }, + + _updateEnabledState: function(enable) { + for (let p of Social.providers) { + p.enabled = enable; + } + }, + + // Called to update our cache of providers and set the current provider + _updateProviderCache: function (providers) { + this.providers = providers; + Services.obs.notifyObservers(null, "social:providers-changed", null); + }, + + get enabled() { + return !this._disabledForSafeMode && this.providers.length > 0; + }, + + _getProviderFromOrigin: function (origin) { + for (let p of this.providers) { + if (p.origin == origin) { + return p; + } + } + return null; + }, + + getManifestByOrigin: function(origin) { + return SocialService.getManifestByOrigin(origin); + }, + + installProvider: function(data, installCallback, options={}) { + SocialService.installProvider(data, installCallback, options); + }, + + uninstallProvider: function(origin, aCallback) { + SocialService.uninstallProvider(origin, aCallback); + }, + + // Activation functionality + activateFromOrigin: function (origin, callback) { + // It's OK if the provider has already been activated - we still get called + // back with it. + SocialService.enableProvider(origin, callback); + } +}; + +function sizeSocialPanelToContent(panel, iframe, requestedSize) { + let doc = iframe.contentDocument; + if (!doc || !doc.body) { + return; + } + // We need an element to use for sizing our panel. See if the body defines + // an id for that element, otherwise use the body itself. + let body = doc.body; + let docEl = doc.documentElement; + let bodyId = body.getAttribute("contentid"); + if (bodyId) { + body = doc.getElementById(bodyId) || doc.body; + } + // offsetHeight/Width don't include margins, so account for that. + let cs = doc.defaultView.getComputedStyle(body); + let width = Math.max(PANEL_MIN_WIDTH, docEl.offsetWidth); + let height = Math.max(PANEL_MIN_HEIGHT, docEl.offsetHeight); + // if the panel is preloaded prior to being shown, cs will be null. in that + // case use the minimum size for the panel until it is shown. + if (cs) { + let computedHeight = parseInt(cs.marginTop) + body.offsetHeight + parseInt(cs.marginBottom); + height = Math.max(computedHeight, height); + let computedWidth = parseInt(cs.marginLeft) + body.offsetWidth + parseInt(cs.marginRight); + width = Math.max(computedWidth, width); + } + + // if our scrollHeight is still larger than the iframe, the css calculations + // above did not work for this site, increase the height. This can happen if + // the site increases its height for additional UI. + if (docEl.scrollHeight > iframe.boxObject.height) + height = docEl.scrollHeight; + + // if a size was defined in the manifest use it as a minimum + if (requestedSize) { + if (requestedSize.height) + height = Math.max(height, requestedSize.height); + if (requestedSize.width) + width = Math.max(width, requestedSize.width); + } + + // add the extra space used by the panel (toolbar, borders, etc) if the iframe + // has been loaded + if (iframe.boxObject.width && iframe.boxObject.height) { + // add extra space the panel needs if any + width += panel.boxObject.width - iframe.boxObject.width; + height += panel.boxObject.height - iframe.boxObject.height; + } + + // using panel.sizeTo will ignore css transitions, set size via style + if (Math.abs(panel.boxObject.width - width) >= 2) + panel.style.width = width + "px"; + if (Math.abs(panel.boxObject.height - height) >= 2) + panel.style.height = height + "px"; +} + +function DynamicResizeWatcher() { + this._mutationObserver = null; +} + +DynamicResizeWatcher.prototype = { + start: function DynamicResizeWatcher_start(panel, iframe, requestedSize) { + this.stop(); // just in case... + let doc = iframe.contentDocument; + this._mutationObserver = new iframe.contentWindow.MutationObserver((mutations) => { + sizeSocialPanelToContent(panel, iframe, requestedSize); + }); + // Observe anything that causes the size to change. + let config = {attributes: true, characterData: true, childList: true, subtree: true}; + this._mutationObserver.observe(doc, config); + // and since this may be setup after the load event has fired we do an + // initial resize now. + sizeSocialPanelToContent(panel, iframe, requestedSize); + }, + stop: function DynamicResizeWatcher_stop() { + if (this._mutationObserver) { + try { + this._mutationObserver.disconnect(); + } catch (ex) { + // may get "TypeError: can't access dead object" which seems strange, + // but doesn't seem to indicate a real problem, so ignore it... + } + this._mutationObserver = null; + } + } +} + + +this.OpenGraphBuilder = { + generateEndpointURL: function(URLTemplate, pageData) { + // support for existing oexchange style endpoints by supporting their + // querystring arguments. parse the query string template and do + // replacements where necessary the query names may be different than ours, + // so we could see u=%{url} or url=%{url} + let [endpointURL, queryString] = URLTemplate.split("?"); + let query = {}; + if (queryString) { + queryString.split('&').forEach(function (val) { + let [name, value] = val.split('='); + let p = /%\{(.+)\}/.exec(value); + if (!p) { + // preserve non-template query vars + query[name] = value; + } else if (pageData[p[1]]) { + if (p[1] == "previews") + query[name] = pageData[p[1]][0]; + else + query[name] = pageData[p[1]]; + } else if (p[1] == "body") { + // build a body for emailers + let body = ""; + if (pageData.title) + body += pageData.title + "\n\n"; + if (pageData.description) + body += pageData.description + "\n\n"; + if (pageData.text) + body += pageData.text + "\n\n"; + body += pageData.url; + query["body"] = body; + } + }); + // if the url template doesn't have title and no text was provided, add the title as the text. + if (!query.text && !query.title && pageData.title) { + query.text = pageData.title; + } + } + var str = []; + for (let p in query) + str.push(p + "=" + encodeURIComponent(query[p])); + if (str.length) + endpointURL = endpointURL + "?" + str.join("&"); + return endpointURL; + }, +}; diff --git a/browser/modules/SocialService.jsm b/browser/modules/SocialService.jsm new file mode 100644 index 000000000..95f5e0259 --- /dev/null +++ b/browser/modules/SocialService.jsm @@ -0,0 +1,1097 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +this.EXPORTED_SYMBOLS = ["SocialService"]; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/AddonManager.jsm"); +Cu.import("resource://gre/modules/PlacesUtils.jsm"); + +const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; +const ADDON_TYPE_SERVICE = "service"; +const ID_SUFFIX = "@services.mozilla.org"; +const STRING_TYPE_NAME = "type.%ID%.name"; + +XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "etld", + "@mozilla.org/network/effective-tld-service;1", + "nsIEffectiveTLDService"); + +/** + * The SocialService is the public API to social providers - it tracks which + * providers are installed and enabled, and is the entry-point for access to + * the provider itself. + */ + +// Internal helper methods and state +var SocialServiceInternal = { + get enabled() { + return this.providerArray.length > 0; + }, + + get providerArray() { + return Object.keys(this.providers).map(origin => this.providers[origin]); + }, + *manifestsGenerator() { + // Retrieve the manifests of installed providers from prefs + let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest."); + let prefs = MANIFEST_PREFS.getChildList("", []); + for (let pref of prefs) { + // we only consider manifests in user level prefs to be *installed* + if (!MANIFEST_PREFS.prefHasUserValue(pref)) + continue; + try { + var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data); + if (manifest && typeof(manifest) == "object" && manifest.origin) + yield manifest; + } catch (err) { + Cu.reportError("SocialService: failed to load manifest: " + pref + + ", exception: " + err); + } + } + }, + get manifests() { + return this.manifestsGenerator(); + }, + getManifestPrefname: function(origin) { + // Retrieve the prefname for a given origin/manifest. + // If no existing pref, return a generated prefname. + let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest."); + let prefs = MANIFEST_PREFS.getChildList("", []); + for (let pref of prefs) { + try { + var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data); + if (manifest.origin == origin) { + return pref; + } + } catch (err) { + Cu.reportError("SocialService: failed to load manifest: " + pref + + ", exception: " + err); + } + } + let originUri = Services.io.newURI(origin, null, null); + return originUri.hostPort.replace('.', '-'); + }, + orderedProviders: function(aCallback) { + if (SocialServiceInternal.providerArray.length < 2) { + schedule(function () { + aCallback(SocialServiceInternal.providerArray); + }); + return; + } + // query moz_hosts for frecency. since some providers may not have a + // frecency entry, we need to later sort on our own. We use the providers + // object below as an easy way to later record the frecency on the provider + // object from the query results. + let hosts = []; + let providers = {}; + + for (let p of SocialServiceInternal.providerArray) { + p.frecency = 0; + providers[p.domain] = p; + hosts.push(p.domain); + } + + // cannot bind an array to stmt.params so we have to build the string + let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) + .DBConnection.createAsyncStatement( + "SELECT host, frecency FROM moz_hosts WHERE host IN (" + + hosts.map(host => '"' + host + '"').join(",") + ") " + ); + + try { + stmt.executeAsync({ + handleResult: function(aResultSet) { + let row; + while ((row = aResultSet.getNextRow())) { + let rh = row.getResultByName("host"); + let frecency = row.getResultByName("frecency"); + providers[rh].frecency = parseInt(frecency) || 0; + } + }, + handleError: function(aError) { + Cu.reportError(aError.message + " (Result = " + aError.result + ")"); + }, + handleCompletion: function(aReason) { + // the query may not have returned all our providers, so we have + // stamped the frecency on the provider and sort here. This makes sure + // all enabled providers get sorted even with frecency zero. + let providerList = SocialServiceInternal.providerArray; + // reverse sort + aCallback(providerList.sort((a, b) => b.frecency - a.frecency)); + } + }); + } finally { + stmt.finalize(); + } + } +}; + +XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () { + initService(); + let providers = {}; + for (let manifest of this.manifests) { + try { + if (ActiveProviders.has(manifest.origin)) { + // enable the api when a provider is enabled + let provider = new SocialProvider(manifest); + providers[provider.origin] = provider; + } + } catch (err) { + Cu.reportError("SocialService: failed to load provider: " + manifest.origin + + ", exception: " + err); + } + } + return providers; +}); + +function getOriginActivationType(origin) { + // if this is an about uri, treat it as a directory + let URI = Services.io.newURI(origin, null, null); + let principal = Services.scriptSecurityManager.createCodebasePrincipal(URI, {}); + if (Services.scriptSecurityManager.isSystemPrincipal(principal) || origin == "moz-safe-about:home") { + return "internal"; + } + + let directories = Services.prefs.getCharPref("social.directories").split(','); + if (directories.indexOf(origin) >= 0) + return "directory"; + + return "foreign"; +} + +var ActiveProviders = { + get _providers() { + delete this._providers; + this._providers = {}; + try { + let pref = Services.prefs.getComplexValue("social.activeProviders", + Ci.nsISupportsString); + this._providers = JSON.parse(pref); + } catch (ex) {} + return this._providers; + }, + + has: function (origin) { + return (origin in this._providers); + }, + + add: function (origin) { + this._providers[origin] = 1; + this._deferredTask.arm(); + }, + + delete: function (origin) { + delete this._providers[origin]; + this._deferredTask.arm(); + }, + + flush: function () { + this._deferredTask.disarm(); + this._persist(); + }, + + get _deferredTask() { + delete this._deferredTask; + return this._deferredTask = new DeferredTask(this._persist.bind(this), 0); + }, + + _persist: function () { + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(this._providers); + Services.prefs.setComplexValue("social.activeProviders", + Ci.nsISupportsString, string); + } +}; + +function migrateSettings() { + let activeProviders, enabled; + try { + activeProviders = Services.prefs.getCharPref("social.activeProviders"); + } catch (e) { + // not set, we'll check if we need to migrate older prefs + } + if (Services.prefs.prefHasUserValue("social.enabled")) { + enabled = Services.prefs.getBoolPref("social.enabled"); + } + if (activeProviders) { + // migration from fx21 to fx22 or later + // ensure any *builtin* provider in activeproviders is in user level prefs + for (let origin in ActiveProviders._providers) { + let prefname; + let manifest; + let defaultManifest; + try { + prefname = getPrefnameFromOrigin(origin); + manifest = JSON.parse(Services.prefs.getComplexValue(prefname, Ci.nsISupportsString).data); + } catch (e) { + // Our preference is missing or bad, remove from ActiveProviders and + // continue. This is primarily an error-case and should only be + // reached by either messing with preferences or hitting the one or + // two days of nightly that ran into it, so we'll flush right away. + ActiveProviders.delete(origin); + ActiveProviders.flush(); + continue; + } + let needsUpdate = !manifest.updateDate; + // fx23 may have built-ins with shareURL + try { + defaultManifest = Services.prefs.getDefaultBranch(null) + .getComplexValue(prefname, Ci.nsISupportsString).data; + defaultManifest = JSON.parse(defaultManifest); + } catch (e) { + // not a built-in, continue + } + if (defaultManifest) { + if (defaultManifest.shareURL && !manifest.shareURL) { + manifest.shareURL = defaultManifest.shareURL; + needsUpdate = true; + } + if (defaultManifest.version && (!manifest.version || defaultManifest.version > manifest.version)) { + manifest = defaultManifest; + needsUpdate = true; + } + } + if (needsUpdate) { + // the provider was installed with an older build, so we will update the + // timestamp and ensure the manifest is in user prefs + delete manifest.builtin; + // we're potentially updating for share, so always mark the updateDate + manifest.updateDate = Date.now(); + if (!manifest.installDate) + manifest.installDate = 0; // we don't know when it was installed + + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(manifest); + Services.prefs.setComplexValue(prefname, Ci.nsISupportsString, string); + } + // as of fx 29, we no longer rely on social.enabled. migration from prior + // versions should disable all service addons if social.enabled=false + if (enabled === false) { + ActiveProviders.delete(origin); + } + } + ActiveProviders.flush(); + Services.prefs.clearUserPref("social.enabled"); + return; + } + + // primary migration from pre-fx21 + let active; + try { + active = Services.prefs.getBoolPref("social.active"); + } catch (e) {} + if (!active) + return; + + // primary difference from SocialServiceInternal.manifests is that we + // only read the default branch here. + let manifestPrefs = Services.prefs.getDefaultBranch("social.manifest."); + let prefs = manifestPrefs.getChildList("", []); + for (let pref of prefs) { + try { + let manifest; + try { + manifest = JSON.parse(manifestPrefs.getComplexValue(pref, Ci.nsISupportsString).data); + } catch (e) { + // bad or missing preference, we wont update this one. + continue; + } + if (manifest && typeof(manifest) == "object" && manifest.origin) { + // our default manifests have been updated with the builtin flags as of + // fx22, delete it so we can set the user-pref + delete manifest.builtin; + if (!manifest.updateDate) { + manifest.updateDate = Date.now(); + manifest.installDate = 0; // we don't know when it was installed + } + + let string = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(manifest); + // pref here is just the branch name, set the full pref name + Services.prefs.setComplexValue("social.manifest." + pref, Ci.nsISupportsString, string); + ActiveProviders.add(manifest.origin); + ActiveProviders.flush(); + // social.active was used at a time that there was only one + // builtin, we'll assume that is still the case + return; + } + } catch (err) { + Cu.reportError("SocialService: failed to load manifest: " + pref + ", exception: " + err); + } + } +} + +function initService() { + Services.obs.addObserver(function xpcomShutdown() { + ActiveProviders.flush(); + SocialService._providerListeners = null; + Services.obs.removeObserver(xpcomShutdown, "xpcom-shutdown"); + }, "xpcom-shutdown", false); + + try { + migrateSettings(); + } catch (e) { + // no matter what, if migration fails we do not want to render social + // unusable. Worst case scenario is that, when upgrading Firefox, previously + // enabled providers are not migrated. + Cu.reportError("Error migrating social settings: " + e); + } +} + +function schedule(callback) { + Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); +} + +// Public API +this.SocialService = { + get hasEnabledProviders() { + // used as an optimization during startup, can be used to check if further + // initialization should be done (e.g. creating the instances of + // SocialProvider and turning on UI). ActiveProviders may have changed and + // not yet flushed so we check the active providers array + for (let p in ActiveProviders._providers) { + return true; + } + return false; + }, + get enabled() { + return SocialServiceInternal.enabled; + }, + set enabled(val) { + throw new Error("not allowed to set SocialService.enabled"); + }, + + // Enables a provider, the manifest must already exist in prefs. The provider + // may or may not have previously been added. onDone is always called + // - with null if no such provider exists, or the activated provider on + // success. + enableProvider: function enableProvider(origin, onDone) { + if (SocialServiceInternal.providers[origin]) { + schedule(function() { + onDone(SocialServiceInternal.providers[origin]); + }); + return; + } + let manifest = SocialService.getManifestByOrigin(origin); + if (manifest) { + let addon = new AddonWrapper(manifest); + AddonManagerPrivate.callAddonListeners("onEnabling", addon, false); + addon.pendingOperations |= AddonManager.PENDING_ENABLE; + this.addProvider(manifest, onDone); + addon.pendingOperations -= AddonManager.PENDING_ENABLE; + AddonManagerPrivate.callAddonListeners("onEnabled", addon); + return; + } + schedule(function() { + onDone(null); + }); + }, + + // Adds a provider given a manifest, and returns the added provider. + addProvider: function addProvider(manifest, onDone) { + if (SocialServiceInternal.providers[manifest.origin]) + throw new Error("SocialService.addProvider: provider with this origin already exists"); + + // enable the api when a provider is enabled + let provider = new SocialProvider(manifest); + SocialServiceInternal.providers[provider.origin] = provider; + ActiveProviders.add(provider.origin); + + this.getOrderedProviderList(function (providers) { + this._notifyProviderListeners("provider-enabled", provider.origin, providers); + if (onDone) + onDone(provider); + }.bind(this)); + }, + + // Removes a provider with the given origin, and notifies when the removal is + // complete. + disableProvider: function disableProvider(origin, onDone) { + if (!(origin in SocialServiceInternal.providers)) + throw new Error("SocialService.disableProvider: no provider with origin " + origin + " exists!"); + + let provider = SocialServiceInternal.providers[origin]; + let manifest = SocialService.getManifestByOrigin(origin); + let addon = manifest && new AddonWrapper(manifest); + if (addon) { + AddonManagerPrivate.callAddonListeners("onDisabling", addon, false); + addon.pendingOperations |= AddonManager.PENDING_DISABLE; + } + provider.enabled = false; + + ActiveProviders.delete(provider.origin); + + delete SocialServiceInternal.providers[origin]; + + if (addon) { + // we have to do this now so the addon manager ui will update an uninstall + // correctly. + addon.pendingOperations -= AddonManager.PENDING_DISABLE; + AddonManagerPrivate.callAddonListeners("onDisabled", addon); + } + + this.getOrderedProviderList(function (providers) { + this._notifyProviderListeners("provider-disabled", origin, providers); + if (onDone) + onDone(); + }.bind(this)); + }, + + // Returns a single provider object with the specified origin. The provider + // must be "installed" (ie, in ActiveProviders) + getProvider: function getProvider(origin, onDone) { + schedule((function () { + onDone(SocialServiceInternal.providers[origin] || null); + }).bind(this)); + }, + + // Returns an unordered array of installed providers + getProviderList: function(onDone) { + schedule(function () { + onDone(SocialServiceInternal.providerArray); + }); + }, + + getManifestByOrigin: function(origin) { + for (let manifest of SocialServiceInternal.manifests) { + if (origin == manifest.origin) { + return manifest; + } + } + return null; + }, + + // Returns an array of installed providers, sorted by frecency + getOrderedProviderList: function(onDone) { + SocialServiceInternal.orderedProviders(onDone); + }, + + getOriginActivationType: function (origin) { + return getOriginActivationType(origin); + }, + + _providerListeners: new Map(), + registerProviderListener: function registerProviderListener(listener) { + this._providerListeners.set(listener, 1); + }, + unregisterProviderListener: function unregisterProviderListener(listener) { + this._providerListeners.delete(listener); + }, + + _notifyProviderListeners: function (topic, origin, providers) { + for (let [listener, ] of this._providerListeners) { + try { + listener(topic, origin, providers); + } catch (ex) { + Components.utils.reportError("SocialService: provider listener threw an exception: " + ex); + } + } + }, + + _manifestFromData: function(type, data, installOrigin) { + let featureURLs = ['shareURL']; + let resolveURLs = featureURLs.concat(['postActivationURL']); + + if (type == 'directory' || type == 'internal') { + // directory provided manifests must have origin in manifest, use that + if (!data['origin']) { + Cu.reportError("SocialService.manifestFromData directory service provided manifest without origin."); + return null; + } + installOrigin = data.origin; + } + // force/fixup origin + let URI = Services.io.newURI(installOrigin, null, null); + let principal = Services.scriptSecurityManager.createCodebasePrincipal(URI, {}); + data.origin = principal.origin; + + // iconURL and name are required + let providerHasFeatures = featureURLs.some(url => data[url]); + if (!providerHasFeatures) { + Cu.reportError("SocialService.manifestFromData manifest missing required urls."); + return null; + } + if (!data['name'] || !data['iconURL']) { + Cu.reportError("SocialService.manifestFromData manifest missing name or iconURL."); + return null; + } + for (let url of resolveURLs) { + if (data[url]) { + try { + let resolved = Services.io.newURI(principal.URI.resolve(data[url]), null, null); + if (!(resolved.schemeIs("http") || resolved.schemeIs("https"))) { + Cu.reportError("SocialService.manifestFromData unsupported scheme '" + resolved.scheme + "' for " + principal.origin); + return null; + } + data[url] = resolved.spec; + } catch (e) { + Cu.reportError("SocialService.manifestFromData unable to resolve '" + url + "' for " + principal.origin); + return null; + } + } + } + return data; + }, + + _showInstallNotification: function(data, aAddonInstaller) { + let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties"); + let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); + + // internal/directory activations need to use the manifest origin, any other + // use the domain activation is occurring on + let url = data.url; + if (data.installType == "internal" || data.installType == "directory") { + url = data.manifest.origin; + } + let requestingURI = Services.io.newURI(url, null, null); + let productName = brandBundle.GetStringFromName("brandShortName"); + + let message = browserBundle.formatStringFromName("service.install.description", + [requestingURI.host, productName], 2); + + let action = { + label: browserBundle.GetStringFromName("service.install.ok.label"), + accessKey: browserBundle.GetStringFromName("service.install.ok.accesskey"), + callback: function() { + aAddonInstaller.install(); + }, + }; + + let options = { + learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api", + }; + let anchor = "servicesInstall-notification-icon"; + let notificationid = "servicesInstall"; + data.window.PopupNotifications.show(data.window.gBrowser.selectedBrowser, + notificationid, message, anchor, + action, [], options); + }, + + installProvider: function(data, installCallback, options={}) { + data.installType = getOriginActivationType(data.origin); + // if we get data, we MUST have a valid manifest generated from the data + let manifest = this._manifestFromData(data.installType, data.manifest, data.origin); + if (!manifest) + throw new Error("SocialService.installProvider: service configuration is invalid from " + data.url); + + let addon = new AddonWrapper(manifest); + if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) + throw new Error("installProvider: provider with origin [" + + data.origin + "] is blocklisted"); + // manifestFromData call above will enforce correct origin. To support + // activation from about: uris, we need to be sure to use the updated + // origin on the manifest. + data.manifest = manifest; + let id = getAddonIDFromOrigin(manifest.origin); + AddonManager.getAddonByID(id, function(aAddon) { + if (aAddon && aAddon.userDisabled) { + aAddon.cancelUninstall(); + aAddon.userDisabled = false; + } + schedule(function () { + try { + this._installProvider(data, options, aManifest => { + this._notifyProviderListeners("provider-installed", aManifest.origin); + installCallback(aManifest); + }); + } catch (e) { + Cu.reportError("Activation failed: " + e); + installCallback(null); + } + }.bind(this)); + }.bind(this)); + }, + + _installProvider: function(data, options, installCallback) { + if (!data.manifest) + throw new Error("Cannot install provider without manifest data"); + + if (data.installType == "foreign" && !Services.prefs.getBoolPref("social.remote-install.enabled")) + throw new Error("Remote install of services is disabled"); + + // if installing from any website, the install must happen over https. + // "internal" are installs from about:home or similar + if (data.installType != "internal" && !Services.io.newURI(data.origin, null, null).schemeIs("https")) { + throw new Error("attempt to activate provider over unsecured channel: " + data.origin); + } + + let installer = new AddonInstaller(data.url, data.manifest, installCallback); + let bypassPanel = options.bypassInstallPanel || + (data.installType == "internal" && data.manifest.oneclick); + if (bypassPanel) + installer.install(); + else + this._showInstallNotification(data, installer); + }, + + createWrapper: function(manifest) { + return new AddonWrapper(manifest); + }, + + /** + * updateProvider is used from the worker to self-update. Since we do not + * have knowledge of the currently selected provider here, we will notify + * the front end to deal with any reload. + */ + updateProvider: function(aUpdateOrigin, aManifest) { + let installType = this.getOriginActivationType(aUpdateOrigin); + // if we get data, we MUST have a valid manifest generated from the data + let manifest = this._manifestFromData(installType, aManifest, aUpdateOrigin); + if (!manifest) + throw new Error("SocialService.installProvider: service configuration is invalid from " + aUpdateOrigin); + + // overwrite the preference + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(manifest); + Services.prefs.setComplexValue(getPrefnameFromOrigin(manifest.origin), Ci.nsISupportsString, string); + + // overwrite the existing provider then notify the front end so it can + // handle any reload that might be necessary. + if (ActiveProviders.has(manifest.origin)) { + let provider = SocialServiceInternal.providers[manifest.origin]; + provider.enabled = false; + provider = new SocialProvider(manifest); + SocialServiceInternal.providers[provider.origin] = provider; + // update the cache and ui, reload provider if necessary + this.getOrderedProviderList(providers => { + this._notifyProviderListeners("provider-update", provider.origin, providers); + }); + } + + }, + + uninstallProvider: function(origin, aCallback) { + let manifest = SocialService.getManifestByOrigin(origin); + let addon = new AddonWrapper(manifest); + addon.uninstall(aCallback); + } +}; + +/** + * The SocialProvider object represents a social provider. + * + * @constructor + * @param {jsobj} object representing the manifest file describing this provider + * @param {bool} boolean indicating whether this provider is "built in" + */ +function SocialProvider(input) { + if (!input.name) + throw new Error("SocialProvider must be passed a name"); + if (!input.origin) + throw new Error("SocialProvider must be passed an origin"); + + let addon = new AddonWrapper(input); + if (addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) + throw new Error("SocialProvider: provider with origin [" + + input.origin + "] is blocklisted"); + + this.name = input.name; + this.iconURL = input.iconURL; + this.icon32URL = input.icon32URL; + this.icon64URL = input.icon64URL; + this.shareURL = input.shareURL; + this.postActivationURL = input.postActivationURL; + this.origin = input.origin; + let originUri = Services.io.newURI(input.origin, null, null); + this.principal = Services.scriptSecurityManager.createCodebasePrincipal(originUri, {}); + this.ambientNotificationIcons = {}; + this.errorState = null; + this.frecency = 0; + + try { + this.domain = etld.getBaseDomainFromHost(originUri.host); + } catch (e) { + this.domain = originUri.host; + } +} + +SocialProvider.prototype = { + reload: function() { + // calling terminate/activate does not set the enabled state whereas setting + // enabled will call terminate/activate + this.enabled = false; + this.enabled = true; + Services.obs.notifyObservers(null, "social:provider-reload", this.origin); + }, + + // Provider enabled/disabled state. + _enabled: false, + get enabled() { + return this._enabled; + }, + set enabled(val) { + let enable = !!val; + if (enable == this._enabled) + return; + + this._enabled = enable; + + if (enable) { + this._activate(); + } else { + this._terminate(); + } + }, + + get manifest() { + return SocialService.getManifestByOrigin(this.origin); + }, + + getPageSize: function(name) { + let manifest = this.manifest; + if (manifest && manifest.pageSize) + return manifest.pageSize[name]; + return undefined; + }, + + // Internal helper methods + _activate: function _activate() { + }, + + _terminate: function _terminate() { + this.errorState = null; + }, + + /** + * Checks if a given URI is of the same origin as the provider. + * + * Returns true or false. + * + * @param {URI or string} uri + */ + isSameOrigin: function isSameOrigin(uri, allowIfInheritsPrincipal) { + if (!uri) + return false; + if (typeof uri == "string") { + try { + uri = Services.io.newURI(uri, null, null); + } catch (ex) { + // an invalid URL can't be loaded! + return false; + } + } + try { + this.principal.checkMayLoad( + uri, // the thing to check. + false, // reportError - we do our own reporting when necessary. + allowIfInheritsPrincipal + ); + return true; + } catch (ex) { + return false; + } + }, + + /** + * Resolve partial URLs for a provider. + * + * Returns nsIURI object or null on failure + * + * @param {string} url + */ + resolveUri: function resolveUri(url) { + try { + let fullURL = this.principal.URI.resolve(url); + return Services.io.newURI(fullURL, null, null); + } catch (ex) { + Cu.reportError("mozSocial: failed to resolve window URL: " + url + "; " + ex); + return null; + } + } +}; + +function getAddonIDFromOrigin(origin) { + let originUri = Services.io.newURI(origin, null, null); + return originUri.host + ID_SUFFIX; +} + +function getPrefnameFromOrigin(origin) { + return "social.manifest." + SocialServiceInternal.getManifestPrefname(origin); +} + +function AddonInstaller(sourceURI, aManifest, installCallback) { + aManifest.updateDate = Date.now(); + // get the existing manifest for installDate + let manifest = SocialService.getManifestByOrigin(aManifest.origin); + let isNewInstall = !manifest; + if (manifest && manifest.installDate) + aManifest.installDate = manifest.installDate; + else + aManifest.installDate = aManifest.updateDate; + + this.sourceURI = sourceURI; + this.install = function() { + let addon = this.addon; + if (isNewInstall) { + AddonManagerPrivate.callInstallListeners("onExternalInstall", null, addon, null, false); + AddonManagerPrivate.callAddonListeners("onInstalling", addon, false); + } + + let string = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + string.data = JSON.stringify(aManifest); + Services.prefs.setComplexValue(getPrefnameFromOrigin(aManifest.origin), Ci.nsISupportsString, string); + + if (isNewInstall) { + AddonManagerPrivate.callAddonListeners("onInstalled", addon); + } + installCallback(aManifest); + }; + this.cancel = function() { + Services.prefs.clearUserPref(getPrefnameFromOrigin(aManifest.origin)); + }; + this.addon = new AddonWrapper(aManifest); +} + +var SocialAddonProvider = { + startup: function() {}, + + shutdown: function() {}, + + updateAddonAppDisabledStates: function() { + // we wont bother with "enabling" services that are released from blocklist + for (let manifest of SocialServiceInternal.manifests) { + try { + if (ActiveProviders.has(manifest.origin)) { + let addon = new AddonWrapper(manifest); + if (addon.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) { + SocialService.disableProvider(manifest.origin); + } + } + } catch (e) { + Cu.reportError(e); + } + } + }, + + getAddonByID: function(aId, aCallback) { + for (let manifest of SocialServiceInternal.manifests) { + if (aId == getAddonIDFromOrigin(manifest.origin)) { + aCallback(new AddonWrapper(manifest)); + return; + } + } + aCallback(null); + }, + + getAddonsByTypes: function(aTypes, aCallback) { + if (aTypes && aTypes.indexOf(ADDON_TYPE_SERVICE) == -1) { + aCallback([]); + return; + } + aCallback([...SocialServiceInternal.manifests].map(a => new AddonWrapper(a))); + }, + + removeAddon: function(aAddon, aCallback) { + AddonManagerPrivate.callAddonListeners("onUninstalling", aAddon, false); + aAddon.pendingOperations |= AddonManager.PENDING_UNINSTALL; + Services.prefs.clearUserPref(getPrefnameFromOrigin(aAddon.manifest.origin)); + aAddon.pendingOperations -= AddonManager.PENDING_UNINSTALL; + AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon); + SocialService._notifyProviderListeners("provider-uninstalled", aAddon.manifest.origin); + if (aCallback) + schedule(aCallback); + } +}; + + +function AddonWrapper(aManifest) { + this.manifest = aManifest; + this.id = getAddonIDFromOrigin(this.manifest.origin); + this._pending = AddonManager.PENDING_NONE; +} +AddonWrapper.prototype = { + get type() { + return ADDON_TYPE_SERVICE; + }, + + get appDisabled() { + return this.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED; + }, + + set softDisabled(val) { + this.userDisabled = val; + }, + + get softDisabled() { + return this.userDisabled; + }, + + get isCompatible() { + return true; + }, + + get isPlatformCompatible() { + return true; + }, + + get scope() { + return AddonManager.SCOPE_PROFILE; + }, + + get foreignInstall() { + return false; + }, + + isCompatibleWith: function(appVersion, platformVersion) { + return true; + }, + + get providesUpdatesSecurely() { + return true; + }, + + get blocklistState() { + return Services.blocklist.getAddonBlocklistState(this); + }, + + get blocklistURL() { + return Services.blocklist.getAddonBlocklistURL(this); + }, + + get screenshots() { + return []; + }, + + get pendingOperations() { + return this._pending || AddonManager.PENDING_NONE; + }, + set pendingOperations(val) { + this._pending = val; + }, + + get operationsRequiringRestart() { + return AddonManager.OP_NEEDS_RESTART_NONE; + }, + + get size() { + return null; + }, + + get permissions() { + let permissions = 0; + // any "user defined" manifest can be removed + if (Services.prefs.prefHasUserValue(getPrefnameFromOrigin(this.manifest.origin))) + permissions = AddonManager.PERM_CAN_UNINSTALL; + if (!this.appDisabled) { + if (this.userDisabled) { + permissions |= AddonManager.PERM_CAN_ENABLE; + } else { + permissions |= AddonManager.PERM_CAN_DISABLE; + } + } + return permissions; + }, + + findUpdates: function(listener, reason, appVersion, platformVersion) { + if ("onNoCompatibilityUpdateAvailable" in listener) + listener.onNoCompatibilityUpdateAvailable(this); + if ("onNoUpdateAvailable" in listener) + listener.onNoUpdateAvailable(this); + if ("onUpdateFinished" in listener) + listener.onUpdateFinished(this); + }, + + get isActive() { + return ActiveProviders.has(this.manifest.origin); + }, + + get name() { + return this.manifest.name; + }, + get version() { + return this.manifest.version ? this.manifest.version.toString() : ""; + }, + + get iconURL() { + return this.manifest.icon32URL ? this.manifest.icon32URL : this.manifest.iconURL; + }, + get icon64URL() { + return this.manifest.icon64URL; + }, + get icons() { + let icons = { + 16: this.manifest.iconURL + }; + if (this.manifest.icon32URL) + icons[32] = this.manifest.icon32URL; + if (this.manifest.icon64URL) + icons[64] = this.manifest.icon64URL; + return icons; + }, + + get description() { + return this.manifest.description; + }, + get homepageURL() { + return this.manifest.homepageURL; + }, + get defaultLocale() { + return this.manifest.defaultLocale; + }, + get selectedLocale() { + return this.manifest.selectedLocale; + }, + + get installDate() { + return this.manifest.installDate ? new Date(this.manifest.installDate) : null; + }, + get updateDate() { + return this.manifest.updateDate ? new Date(this.manifest.updateDate) : null; + }, + + get creator() { + return new AddonManagerPrivate.AddonAuthor(this.manifest.author); + }, + + get userDisabled() { + return this.appDisabled || !ActiveProviders.has(this.manifest.origin); + }, + + set userDisabled(val) { + if (val == this.userDisabled) + return val; + if (val) { + SocialService.disableProvider(this.manifest.origin); + } else if (!this.appDisabled) { + SocialService.enableProvider(this.manifest.origin); + } + return val; + }, + + uninstall: function(aCallback) { + let prefName = getPrefnameFromOrigin(this.manifest.origin); + if (Services.prefs.prefHasUserValue(prefName)) { + if (ActiveProviders.has(this.manifest.origin)) { + SocialService.disableProvider(this.manifest.origin, function() { + SocialAddonProvider.removeAddon(this, aCallback); + }.bind(this)); + } else { + SocialAddonProvider.removeAddon(this, aCallback); + } + } else { + schedule(aCallback); + } + }, + + cancelUninstall: function() { + this._pending -= AddonManager.PENDING_UNINSTALL; + AddonManagerPrivate.callAddonListeners("onOperationCancelled", this); + } +}; + + +AddonManagerPrivate.registerProvider(SocialAddonProvider, [ + new AddonManagerPrivate.AddonType(ADDON_TYPE_SERVICE, URI_EXTENSION_STRINGS, + STRING_TYPE_NAME, + AddonManager.VIEW_TYPE_LIST, 10000) +]); diff --git a/browser/modules/moz.build b/browser/modules/moz.build index a7bbbc258..852a4c911 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -35,6 +35,8 @@ EXTRA_JS_MODULES += [ 'Sanitizer.jsm', 'SelfSupportBackend.jsm', 'SitePermissions.jsm', + 'Social.jsm', + 'SocialService.jsm', 'TransientPrefs.jsm', 'URLBarZoom.jsm', 'webrtcUI.jsm', |