summaryrefslogtreecommitdiffstats
path: root/browser/modules/SocialService.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'browser/modules/SocialService.jsm')
-rw-r--r--browser/modules/SocialService.jsm1097
1 files changed, 1097 insertions, 0 deletions
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)
+]);