Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | this.EXPORTED_SYMBOLS = ["SocialService"]; |
michael@0 | 6 | |
michael@0 | 7 | const { classes: Cc, interfaces: Ci, utils: Cu } = Components; |
michael@0 | 8 | |
michael@0 | 9 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 10 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
michael@0 | 11 | Cu.import("resource://gre/modules/AddonManager.jsm"); |
michael@0 | 12 | Cu.import("resource://gre/modules/PlacesUtils.jsm"); |
michael@0 | 13 | |
michael@0 | 14 | const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; |
michael@0 | 15 | const ADDON_TYPE_SERVICE = "service"; |
michael@0 | 16 | const ID_SUFFIX = "@services.mozilla.org"; |
michael@0 | 17 | const STRING_TYPE_NAME = "type.%ID%.name"; |
michael@0 | 18 | |
michael@0 | 19 | XPCOMUtils.defineLazyModuleGetter(this, "getFrameWorkerHandle", "resource://gre/modules/FrameWorker.jsm"); |
michael@0 | 20 | XPCOMUtils.defineLazyModuleGetter(this, "WorkerAPI", "resource://gre/modules/WorkerAPI.jsm"); |
michael@0 | 21 | XPCOMUtils.defineLazyModuleGetter(this, "MozSocialAPI", "resource://gre/modules/MozSocialAPI.jsm"); |
michael@0 | 22 | XPCOMUtils.defineLazyModuleGetter(this, "closeAllChatWindows", "resource://gre/modules/MozSocialAPI.jsm"); |
michael@0 | 23 | XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); |
michael@0 | 24 | |
michael@0 | 25 | XPCOMUtils.defineLazyServiceGetter(this, "etld", |
michael@0 | 26 | "@mozilla.org/network/effective-tld-service;1", |
michael@0 | 27 | "nsIEffectiveTLDService"); |
michael@0 | 28 | |
michael@0 | 29 | /** |
michael@0 | 30 | * The SocialService is the public API to social providers - it tracks which |
michael@0 | 31 | * providers are installed and enabled, and is the entry-point for access to |
michael@0 | 32 | * the provider itself. |
michael@0 | 33 | */ |
michael@0 | 34 | |
michael@0 | 35 | // Internal helper methods and state |
michael@0 | 36 | let SocialServiceInternal = { |
michael@0 | 37 | get enabled() this.providerArray.length > 0, |
michael@0 | 38 | |
michael@0 | 39 | get providerArray() { |
michael@0 | 40 | return [p for ([, p] of Iterator(this.providers))]; |
michael@0 | 41 | }, |
michael@0 | 42 | get manifests() { |
michael@0 | 43 | // Retrieve the manifests of installed providers from prefs |
michael@0 | 44 | let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest."); |
michael@0 | 45 | let prefs = MANIFEST_PREFS.getChildList("", []); |
michael@0 | 46 | for (let pref of prefs) { |
michael@0 | 47 | // we only consider manifests in user level prefs to be *installed* |
michael@0 | 48 | if (!MANIFEST_PREFS.prefHasUserValue(pref)) |
michael@0 | 49 | continue; |
michael@0 | 50 | try { |
michael@0 | 51 | var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data); |
michael@0 | 52 | if (manifest && typeof(manifest) == "object" && manifest.origin) |
michael@0 | 53 | yield manifest; |
michael@0 | 54 | } catch (err) { |
michael@0 | 55 | Cu.reportError("SocialService: failed to load manifest: " + pref + |
michael@0 | 56 | ", exception: " + err); |
michael@0 | 57 | } |
michael@0 | 58 | } |
michael@0 | 59 | }, |
michael@0 | 60 | getManifestPrefname: function(origin) { |
michael@0 | 61 | // Retrieve the prefname for a given origin/manifest. |
michael@0 | 62 | // If no existing pref, return a generated prefname. |
michael@0 | 63 | let MANIFEST_PREFS = Services.prefs.getBranch("social.manifest."); |
michael@0 | 64 | let prefs = MANIFEST_PREFS.getChildList("", []); |
michael@0 | 65 | for (let pref of prefs) { |
michael@0 | 66 | try { |
michael@0 | 67 | var manifest = JSON.parse(MANIFEST_PREFS.getComplexValue(pref, Ci.nsISupportsString).data); |
michael@0 | 68 | if (manifest.origin == origin) { |
michael@0 | 69 | return pref; |
michael@0 | 70 | } |
michael@0 | 71 | } catch (err) { |
michael@0 | 72 | Cu.reportError("SocialService: failed to load manifest: " + pref + |
michael@0 | 73 | ", exception: " + err); |
michael@0 | 74 | } |
michael@0 | 75 | } |
michael@0 | 76 | let originUri = Services.io.newURI(origin, null, null); |
michael@0 | 77 | return originUri.hostPort.replace('.','-'); |
michael@0 | 78 | }, |
michael@0 | 79 | orderedProviders: function(aCallback) { |
michael@0 | 80 | if (SocialServiceInternal.providerArray.length < 2) { |
michael@0 | 81 | schedule(function () { |
michael@0 | 82 | aCallback(SocialServiceInternal.providerArray); |
michael@0 | 83 | }); |
michael@0 | 84 | return; |
michael@0 | 85 | } |
michael@0 | 86 | // query moz_hosts for frecency. since some providers may not have a |
michael@0 | 87 | // frecency entry, we need to later sort on our own. We use the providers |
michael@0 | 88 | // object below as an easy way to later record the frecency on the provider |
michael@0 | 89 | // object from the query results. |
michael@0 | 90 | let hosts = []; |
michael@0 | 91 | let providers = {}; |
michael@0 | 92 | |
michael@0 | 93 | for (let p of SocialServiceInternal.providerArray) { |
michael@0 | 94 | p.frecency = 0; |
michael@0 | 95 | providers[p.domain] = p; |
michael@0 | 96 | hosts.push(p.domain); |
michael@0 | 97 | }; |
michael@0 | 98 | |
michael@0 | 99 | // cannot bind an array to stmt.params so we have to build the string |
michael@0 | 100 | let stmt = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) |
michael@0 | 101 | .DBConnection.createAsyncStatement( |
michael@0 | 102 | "SELECT host, frecency FROM moz_hosts WHERE host IN (" + |
michael@0 | 103 | [ '"' + host + '"' for each (host in hosts) ].join(",") + ") " |
michael@0 | 104 | ); |
michael@0 | 105 | |
michael@0 | 106 | try { |
michael@0 | 107 | stmt.executeAsync({ |
michael@0 | 108 | handleResult: function(aResultSet) { |
michael@0 | 109 | let row; |
michael@0 | 110 | while ((row = aResultSet.getNextRow())) { |
michael@0 | 111 | let rh = row.getResultByName("host"); |
michael@0 | 112 | let frecency = row.getResultByName("frecency"); |
michael@0 | 113 | providers[rh].frecency = parseInt(frecency) || 0; |
michael@0 | 114 | } |
michael@0 | 115 | }, |
michael@0 | 116 | handleError: function(aError) { |
michael@0 | 117 | Cu.reportError(aError.message + " (Result = " + aError.result + ")"); |
michael@0 | 118 | }, |
michael@0 | 119 | handleCompletion: function(aReason) { |
michael@0 | 120 | // the query may not have returned all our providers, so we have |
michael@0 | 121 | // stamped the frecency on the provider and sort here. This makes sure |
michael@0 | 122 | // all enabled providers get sorted even with frecency zero. |
michael@0 | 123 | let providerList = SocialServiceInternal.providerArray; |
michael@0 | 124 | // reverse sort |
michael@0 | 125 | aCallback(providerList.sort(function(a, b) b.frecency - a.frecency)); |
michael@0 | 126 | } |
michael@0 | 127 | }); |
michael@0 | 128 | } finally { |
michael@0 | 129 | stmt.finalize(); |
michael@0 | 130 | } |
michael@0 | 131 | } |
michael@0 | 132 | }; |
michael@0 | 133 | |
michael@0 | 134 | XPCOMUtils.defineLazyGetter(SocialServiceInternal, "providers", function () { |
michael@0 | 135 | initService(); |
michael@0 | 136 | let providers = {}; |
michael@0 | 137 | for (let manifest of this.manifests) { |
michael@0 | 138 | try { |
michael@0 | 139 | if (ActiveProviders.has(manifest.origin)) { |
michael@0 | 140 | // enable the api when a provider is enabled |
michael@0 | 141 | MozSocialAPI.enabled = true; |
michael@0 | 142 | let provider = new SocialProvider(manifest); |
michael@0 | 143 | providers[provider.origin] = provider; |
michael@0 | 144 | } |
michael@0 | 145 | } catch (err) { |
michael@0 | 146 | Cu.reportError("SocialService: failed to load provider: " + manifest.origin + |
michael@0 | 147 | ", exception: " + err); |
michael@0 | 148 | } |
michael@0 | 149 | } |
michael@0 | 150 | return providers; |
michael@0 | 151 | }); |
michael@0 | 152 | |
michael@0 | 153 | function getOriginActivationType(origin) { |
michael@0 | 154 | let prefname = SocialServiceInternal.getManifestPrefname(origin); |
michael@0 | 155 | if (Services.prefs.getDefaultBranch("social.manifest.").getPrefType(prefname) == Services.prefs.PREF_STRING) |
michael@0 | 156 | return 'builtin'; |
michael@0 | 157 | |
michael@0 | 158 | let whitelist = Services.prefs.getCharPref("social.whitelist").split(','); |
michael@0 | 159 | if (whitelist.indexOf(origin) >= 0) |
michael@0 | 160 | return 'whitelist'; |
michael@0 | 161 | |
michael@0 | 162 | let directories = Services.prefs.getCharPref("social.directories").split(','); |
michael@0 | 163 | if (directories.indexOf(origin) >= 0) |
michael@0 | 164 | return 'directory'; |
michael@0 | 165 | |
michael@0 | 166 | return 'foreign'; |
michael@0 | 167 | } |
michael@0 | 168 | |
michael@0 | 169 | let ActiveProviders = { |
michael@0 | 170 | get _providers() { |
michael@0 | 171 | delete this._providers; |
michael@0 | 172 | this._providers = {}; |
michael@0 | 173 | try { |
michael@0 | 174 | let pref = Services.prefs.getComplexValue("social.activeProviders", |
michael@0 | 175 | Ci.nsISupportsString); |
michael@0 | 176 | this._providers = JSON.parse(pref); |
michael@0 | 177 | } catch(ex) {} |
michael@0 | 178 | return this._providers; |
michael@0 | 179 | }, |
michael@0 | 180 | |
michael@0 | 181 | has: function (origin) { |
michael@0 | 182 | return (origin in this._providers); |
michael@0 | 183 | }, |
michael@0 | 184 | |
michael@0 | 185 | add: function (origin) { |
michael@0 | 186 | this._providers[origin] = 1; |
michael@0 | 187 | this._deferredTask.arm(); |
michael@0 | 188 | }, |
michael@0 | 189 | |
michael@0 | 190 | delete: function (origin) { |
michael@0 | 191 | delete this._providers[origin]; |
michael@0 | 192 | this._deferredTask.arm(); |
michael@0 | 193 | }, |
michael@0 | 194 | |
michael@0 | 195 | flush: function () { |
michael@0 | 196 | this._deferredTask.disarm(); |
michael@0 | 197 | this._persist(); |
michael@0 | 198 | }, |
michael@0 | 199 | |
michael@0 | 200 | get _deferredTask() { |
michael@0 | 201 | delete this._deferredTask; |
michael@0 | 202 | return this._deferredTask = new DeferredTask(this._persist.bind(this), 0); |
michael@0 | 203 | }, |
michael@0 | 204 | |
michael@0 | 205 | _persist: function () { |
michael@0 | 206 | let string = Cc["@mozilla.org/supports-string;1"]. |
michael@0 | 207 | createInstance(Ci.nsISupportsString); |
michael@0 | 208 | string.data = JSON.stringify(this._providers); |
michael@0 | 209 | Services.prefs.setComplexValue("social.activeProviders", |
michael@0 | 210 | Ci.nsISupportsString, string); |
michael@0 | 211 | } |
michael@0 | 212 | }; |
michael@0 | 213 | |
michael@0 | 214 | function migrateSettings() { |
michael@0 | 215 | let activeProviders, enabled; |
michael@0 | 216 | try { |
michael@0 | 217 | activeProviders = Services.prefs.getCharPref("social.activeProviders"); |
michael@0 | 218 | } catch(e) { |
michael@0 | 219 | // not set, we'll check if we need to migrate older prefs |
michael@0 | 220 | } |
michael@0 | 221 | if (Services.prefs.prefHasUserValue("social.enabled")) { |
michael@0 | 222 | enabled = Services.prefs.getBoolPref("social.enabled"); |
michael@0 | 223 | } |
michael@0 | 224 | if (activeProviders) { |
michael@0 | 225 | // migration from fx21 to fx22 or later |
michael@0 | 226 | // ensure any *builtin* provider in activeproviders is in user level prefs |
michael@0 | 227 | for (let origin in ActiveProviders._providers) { |
michael@0 | 228 | let prefname; |
michael@0 | 229 | let manifest; |
michael@0 | 230 | let defaultManifest; |
michael@0 | 231 | try { |
michael@0 | 232 | prefname = getPrefnameFromOrigin(origin); |
michael@0 | 233 | manifest = JSON.parse(Services.prefs.getComplexValue(prefname, Ci.nsISupportsString).data); |
michael@0 | 234 | } catch(e) { |
michael@0 | 235 | // Our preference is missing or bad, remove from ActiveProviders and |
michael@0 | 236 | // continue. This is primarily an error-case and should only be |
michael@0 | 237 | // reached by either messing with preferences or hitting the one or |
michael@0 | 238 | // two days of nightly that ran into it, so we'll flush right away. |
michael@0 | 239 | ActiveProviders.delete(origin); |
michael@0 | 240 | ActiveProviders.flush(); |
michael@0 | 241 | continue; |
michael@0 | 242 | } |
michael@0 | 243 | let needsUpdate = !manifest.updateDate; |
michael@0 | 244 | // fx23 may have built-ins with shareURL |
michael@0 | 245 | try { |
michael@0 | 246 | defaultManifest = Services.prefs.getDefaultBranch(null) |
michael@0 | 247 | .getComplexValue(prefname, Ci.nsISupportsString).data; |
michael@0 | 248 | defaultManifest = JSON.parse(defaultManifest); |
michael@0 | 249 | } catch(e) { |
michael@0 | 250 | // not a built-in, continue |
michael@0 | 251 | } |
michael@0 | 252 | if (defaultManifest) { |
michael@0 | 253 | if (defaultManifest.shareURL && !manifest.shareURL) { |
michael@0 | 254 | manifest.shareURL = defaultManifest.shareURL; |
michael@0 | 255 | needsUpdate = true; |
michael@0 | 256 | } |
michael@0 | 257 | if (defaultManifest.version && (!manifest.version || defaultManifest.version > manifest.version)) { |
michael@0 | 258 | manifest = defaultManifest; |
michael@0 | 259 | needsUpdate = true; |
michael@0 | 260 | } |
michael@0 | 261 | } |
michael@0 | 262 | if (needsUpdate) { |
michael@0 | 263 | // the provider was installed with an older build, so we will update the |
michael@0 | 264 | // timestamp and ensure the manifest is in user prefs |
michael@0 | 265 | delete manifest.builtin; |
michael@0 | 266 | // we're potentially updating for share, so always mark the updateDate |
michael@0 | 267 | manifest.updateDate = Date.now(); |
michael@0 | 268 | if (!manifest.installDate) |
michael@0 | 269 | manifest.installDate = 0; // we don't know when it was installed |
michael@0 | 270 | |
michael@0 | 271 | let string = Cc["@mozilla.org/supports-string;1"]. |
michael@0 | 272 | createInstance(Ci.nsISupportsString); |
michael@0 | 273 | string.data = JSON.stringify(manifest); |
michael@0 | 274 | Services.prefs.setComplexValue(prefname, Ci.nsISupportsString, string); |
michael@0 | 275 | } |
michael@0 | 276 | // as of fx 29, we no longer rely on social.enabled. migration from prior |
michael@0 | 277 | // versions should disable all service addons if social.enabled=false |
michael@0 | 278 | if (enabled === false) { |
michael@0 | 279 | ActiveProviders.delete(origin); |
michael@0 | 280 | } |
michael@0 | 281 | } |
michael@0 | 282 | ActiveProviders.flush(); |
michael@0 | 283 | Services.prefs.clearUserPref("social.enabled"); |
michael@0 | 284 | return; |
michael@0 | 285 | } |
michael@0 | 286 | |
michael@0 | 287 | // primary migration from pre-fx21 |
michael@0 | 288 | let active; |
michael@0 | 289 | try { |
michael@0 | 290 | active = Services.prefs.getBoolPref("social.active"); |
michael@0 | 291 | } catch(e) {} |
michael@0 | 292 | if (!active) |
michael@0 | 293 | return; |
michael@0 | 294 | |
michael@0 | 295 | // primary difference from SocialServiceInternal.manifests is that we |
michael@0 | 296 | // only read the default branch here. |
michael@0 | 297 | let manifestPrefs = Services.prefs.getDefaultBranch("social.manifest."); |
michael@0 | 298 | let prefs = manifestPrefs.getChildList("", []); |
michael@0 | 299 | for (let pref of prefs) { |
michael@0 | 300 | try { |
michael@0 | 301 | let manifest; |
michael@0 | 302 | try { |
michael@0 | 303 | manifest = JSON.parse(manifestPrefs.getComplexValue(pref, Ci.nsISupportsString).data); |
michael@0 | 304 | } catch(e) { |
michael@0 | 305 | // bad or missing preference, we wont update this one. |
michael@0 | 306 | continue; |
michael@0 | 307 | } |
michael@0 | 308 | if (manifest && typeof(manifest) == "object" && manifest.origin) { |
michael@0 | 309 | // our default manifests have been updated with the builtin flags as of |
michael@0 | 310 | // fx22, delete it so we can set the user-pref |
michael@0 | 311 | delete manifest.builtin; |
michael@0 | 312 | if (!manifest.updateDate) { |
michael@0 | 313 | manifest.updateDate = Date.now(); |
michael@0 | 314 | manifest.installDate = 0; // we don't know when it was installed |
michael@0 | 315 | } |
michael@0 | 316 | |
michael@0 | 317 | let string = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString); |
michael@0 | 318 | string.data = JSON.stringify(manifest); |
michael@0 | 319 | // pref here is just the branch name, set the full pref name |
michael@0 | 320 | Services.prefs.setComplexValue("social.manifest." + pref, Ci.nsISupportsString, string); |
michael@0 | 321 | ActiveProviders.add(manifest.origin); |
michael@0 | 322 | ActiveProviders.flush(); |
michael@0 | 323 | // social.active was used at a time that there was only one |
michael@0 | 324 | // builtin, we'll assume that is still the case |
michael@0 | 325 | return; |
michael@0 | 326 | } |
michael@0 | 327 | } catch (err) { |
michael@0 | 328 | Cu.reportError("SocialService: failed to load manifest: " + pref + ", exception: " + err); |
michael@0 | 329 | } |
michael@0 | 330 | } |
michael@0 | 331 | } |
michael@0 | 332 | |
michael@0 | 333 | function initService() { |
michael@0 | 334 | Services.obs.addObserver(function xpcomShutdown() { |
michael@0 | 335 | ActiveProviders.flush(); |
michael@0 | 336 | SocialService._providerListeners = null; |
michael@0 | 337 | Services.obs.removeObserver(xpcomShutdown, "xpcom-shutdown"); |
michael@0 | 338 | }, "xpcom-shutdown", false); |
michael@0 | 339 | |
michael@0 | 340 | try { |
michael@0 | 341 | migrateSettings(); |
michael@0 | 342 | } catch(e) { |
michael@0 | 343 | // no matter what, if migration fails we do not want to render social |
michael@0 | 344 | // unusable. Worst case scenario is that, when upgrading Firefox, previously |
michael@0 | 345 | // enabled providers are not migrated. |
michael@0 | 346 | Cu.reportError("Error migrating social settings: " + e); |
michael@0 | 347 | } |
michael@0 | 348 | } |
michael@0 | 349 | |
michael@0 | 350 | function schedule(callback) { |
michael@0 | 351 | Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); |
michael@0 | 352 | } |
michael@0 | 353 | |
michael@0 | 354 | // Public API |
michael@0 | 355 | this.SocialService = { |
michael@0 | 356 | get hasEnabledProviders() { |
michael@0 | 357 | // used as an optimization during startup, can be used to check if further |
michael@0 | 358 | // initialization should be done (e.g. creating the instances of |
michael@0 | 359 | // SocialProvider and turning on UI). ActiveProviders may have changed and |
michael@0 | 360 | // not yet flushed so we check the active providers array |
michael@0 | 361 | for (let p in ActiveProviders._providers) { |
michael@0 | 362 | return true; |
michael@0 | 363 | }; |
michael@0 | 364 | return false; |
michael@0 | 365 | }, |
michael@0 | 366 | get enabled() { |
michael@0 | 367 | return SocialServiceInternal.enabled; |
michael@0 | 368 | }, |
michael@0 | 369 | set enabled(val) { |
michael@0 | 370 | throw new Error("not allowed to set SocialService.enabled"); |
michael@0 | 371 | }, |
michael@0 | 372 | |
michael@0 | 373 | // Adds and activates a builtin provider. The provider may or may not have |
michael@0 | 374 | // previously been added. onDone is always called - with null if no such |
michael@0 | 375 | // provider exists, or the activated provider on success. |
michael@0 | 376 | addBuiltinProvider: function addBuiltinProvider(origin, onDone) { |
michael@0 | 377 | if (SocialServiceInternal.providers[origin]) { |
michael@0 | 378 | schedule(function() { |
michael@0 | 379 | onDone(SocialServiceInternal.providers[origin]); |
michael@0 | 380 | }); |
michael@0 | 381 | return; |
michael@0 | 382 | } |
michael@0 | 383 | let manifest = SocialService.getManifestByOrigin(origin); |
michael@0 | 384 | if (manifest) { |
michael@0 | 385 | let addon = new AddonWrapper(manifest); |
michael@0 | 386 | AddonManagerPrivate.callAddonListeners("onEnabling", addon, false); |
michael@0 | 387 | addon.pendingOperations |= AddonManager.PENDING_ENABLE; |
michael@0 | 388 | this.addProvider(manifest, onDone); |
michael@0 | 389 | addon.pendingOperations -= AddonManager.PENDING_ENABLE; |
michael@0 | 390 | AddonManagerPrivate.callAddonListeners("onEnabled", addon); |
michael@0 | 391 | return; |
michael@0 | 392 | } |
michael@0 | 393 | schedule(function() { |
michael@0 | 394 | onDone(null); |
michael@0 | 395 | }); |
michael@0 | 396 | }, |
michael@0 | 397 | |
michael@0 | 398 | // Adds a provider given a manifest, and returns the added provider. |
michael@0 | 399 | addProvider: function addProvider(manifest, onDone) { |
michael@0 | 400 | if (SocialServiceInternal.providers[manifest.origin]) |
michael@0 | 401 | throw new Error("SocialService.addProvider: provider with this origin already exists"); |
michael@0 | 402 | |
michael@0 | 403 | // enable the api when a provider is enabled |
michael@0 | 404 | MozSocialAPI.enabled = true; |
michael@0 | 405 | let provider = new SocialProvider(manifest); |
michael@0 | 406 | SocialServiceInternal.providers[provider.origin] = provider; |
michael@0 | 407 | ActiveProviders.add(provider.origin); |
michael@0 | 408 | |
michael@0 | 409 | this.getOrderedProviderList(function (providers) { |
michael@0 | 410 | this._notifyProviderListeners("provider-enabled", provider.origin, providers); |
michael@0 | 411 | if (onDone) |
michael@0 | 412 | onDone(provider); |
michael@0 | 413 | }.bind(this)); |
michael@0 | 414 | }, |
michael@0 | 415 | |
michael@0 | 416 | // Removes a provider with the given origin, and notifies when the removal is |
michael@0 | 417 | // complete. |
michael@0 | 418 | removeProvider: function removeProvider(origin, onDone) { |
michael@0 | 419 | if (!(origin in SocialServiceInternal.providers)) |
michael@0 | 420 | throw new Error("SocialService.removeProvider: no provider with origin " + origin + " exists!"); |
michael@0 | 421 | |
michael@0 | 422 | let provider = SocialServiceInternal.providers[origin]; |
michael@0 | 423 | let manifest = SocialService.getManifestByOrigin(origin); |
michael@0 | 424 | let addon = manifest && new AddonWrapper(manifest); |
michael@0 | 425 | if (addon) { |
michael@0 | 426 | AddonManagerPrivate.callAddonListeners("onDisabling", addon, false); |
michael@0 | 427 | addon.pendingOperations |= AddonManager.PENDING_DISABLE; |
michael@0 | 428 | } |
michael@0 | 429 | provider.enabled = false; |
michael@0 | 430 | |
michael@0 | 431 | ActiveProviders.delete(provider.origin); |
michael@0 | 432 | |
michael@0 | 433 | delete SocialServiceInternal.providers[origin]; |
michael@0 | 434 | // disable the api if we have no enabled providers |
michael@0 | 435 | MozSocialAPI.enabled = SocialServiceInternal.enabled; |
michael@0 | 436 | |
michael@0 | 437 | if (addon) { |
michael@0 | 438 | // we have to do this now so the addon manager ui will update an uninstall |
michael@0 | 439 | // correctly. |
michael@0 | 440 | addon.pendingOperations -= AddonManager.PENDING_DISABLE; |
michael@0 | 441 | AddonManagerPrivate.callAddonListeners("onDisabled", addon); |
michael@0 | 442 | AddonManagerPrivate.notifyAddonChanged(addon.id, ADDON_TYPE_SERVICE, false); |
michael@0 | 443 | } |
michael@0 | 444 | |
michael@0 | 445 | this.getOrderedProviderList(function (providers) { |
michael@0 | 446 | this._notifyProviderListeners("provider-disabled", origin, providers); |
michael@0 | 447 | if (onDone) |
michael@0 | 448 | onDone(); |
michael@0 | 449 | }.bind(this)); |
michael@0 | 450 | }, |
michael@0 | 451 | |
michael@0 | 452 | // Returns a single provider object with the specified origin. The provider |
michael@0 | 453 | // must be "installed" (ie, in ActiveProviders) |
michael@0 | 454 | getProvider: function getProvider(origin, onDone) { |
michael@0 | 455 | schedule((function () { |
michael@0 | 456 | onDone(SocialServiceInternal.providers[origin] || null); |
michael@0 | 457 | }).bind(this)); |
michael@0 | 458 | }, |
michael@0 | 459 | |
michael@0 | 460 | // Returns an unordered array of installed providers |
michael@0 | 461 | getProviderList: function(onDone) { |
michael@0 | 462 | schedule(function () { |
michael@0 | 463 | onDone(SocialServiceInternal.providerArray); |
michael@0 | 464 | }); |
michael@0 | 465 | }, |
michael@0 | 466 | |
michael@0 | 467 | getManifestByOrigin: function(origin) { |
michael@0 | 468 | for (let manifest of SocialServiceInternal.manifests) { |
michael@0 | 469 | if (origin == manifest.origin) { |
michael@0 | 470 | return manifest; |
michael@0 | 471 | } |
michael@0 | 472 | } |
michael@0 | 473 | return null; |
michael@0 | 474 | }, |
michael@0 | 475 | |
michael@0 | 476 | // Returns an array of installed providers, sorted by frecency |
michael@0 | 477 | getOrderedProviderList: function(onDone) { |
michael@0 | 478 | SocialServiceInternal.orderedProviders(onDone); |
michael@0 | 479 | }, |
michael@0 | 480 | |
michael@0 | 481 | getOriginActivationType: function (origin) { |
michael@0 | 482 | return getOriginActivationType(origin); |
michael@0 | 483 | }, |
michael@0 | 484 | |
michael@0 | 485 | _providerListeners: new Map(), |
michael@0 | 486 | registerProviderListener: function registerProviderListener(listener) { |
michael@0 | 487 | this._providerListeners.set(listener, 1); |
michael@0 | 488 | }, |
michael@0 | 489 | unregisterProviderListener: function unregisterProviderListener(listener) { |
michael@0 | 490 | this._providerListeners.delete(listener); |
michael@0 | 491 | }, |
michael@0 | 492 | |
michael@0 | 493 | _notifyProviderListeners: function (topic, origin, providers) { |
michael@0 | 494 | for (let [listener, ] of this._providerListeners) { |
michael@0 | 495 | try { |
michael@0 | 496 | listener(topic, origin, providers); |
michael@0 | 497 | } catch (ex) { |
michael@0 | 498 | Components.utils.reportError("SocialService: provider listener threw an exception: " + ex); |
michael@0 | 499 | } |
michael@0 | 500 | } |
michael@0 | 501 | }, |
michael@0 | 502 | |
michael@0 | 503 | _manifestFromData: function(type, data, principal) { |
michael@0 | 504 | let featureURLs = ['workerURL', 'sidebarURL', 'shareURL', 'statusURL', 'markURL']; |
michael@0 | 505 | let resolveURLs = featureURLs.concat(['postActivationURL']); |
michael@0 | 506 | |
michael@0 | 507 | if (type == 'directory') { |
michael@0 | 508 | // directory provided manifests must have origin in manifest, use that |
michael@0 | 509 | if (!data['origin']) { |
michael@0 | 510 | Cu.reportError("SocialService.manifestFromData directory service provided manifest without origin."); |
michael@0 | 511 | return null; |
michael@0 | 512 | } |
michael@0 | 513 | let URI = Services.io.newURI(data.origin, null, null); |
michael@0 | 514 | principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(URI); |
michael@0 | 515 | } |
michael@0 | 516 | // force/fixup origin |
michael@0 | 517 | data.origin = principal.origin; |
michael@0 | 518 | |
michael@0 | 519 | // iconURL and name are required |
michael@0 | 520 | // iconURL may be a different origin (CDN or data url support) if this is |
michael@0 | 521 | // a whitelisted or directory listed provider |
michael@0 | 522 | let providerHasFeatures = [url for (url of featureURLs) if (data[url])].length > 0; |
michael@0 | 523 | if (!providerHasFeatures) { |
michael@0 | 524 | Cu.reportError("SocialService.manifestFromData manifest missing required urls."); |
michael@0 | 525 | return null; |
michael@0 | 526 | } |
michael@0 | 527 | if (!data['name'] || !data['iconURL']) { |
michael@0 | 528 | Cu.reportError("SocialService.manifestFromData manifest missing name or iconURL."); |
michael@0 | 529 | return null; |
michael@0 | 530 | } |
michael@0 | 531 | for (let url of resolveURLs) { |
michael@0 | 532 | if (data[url]) { |
michael@0 | 533 | try { |
michael@0 | 534 | let resolved = Services.io.newURI(principal.URI.resolve(data[url]), null, null); |
michael@0 | 535 | if (!(resolved.schemeIs("http") || resolved.schemeIs("https"))) { |
michael@0 | 536 | Cu.reportError("SocialService.manifestFromData unsupported scheme '" + resolved.scheme + "' for " + principal.origin); |
michael@0 | 537 | return null; |
michael@0 | 538 | } |
michael@0 | 539 | data[url] = resolved.spec; |
michael@0 | 540 | } catch(e) { |
michael@0 | 541 | Cu.reportError("SocialService.manifestFromData unable to resolve '" + url + "' for " + principal.origin); |
michael@0 | 542 | return null; |
michael@0 | 543 | } |
michael@0 | 544 | } |
michael@0 | 545 | } |
michael@0 | 546 | return data; |
michael@0 | 547 | }, |
michael@0 | 548 | |
michael@0 | 549 | _getChromeWindow: function(aWindow) { |
michael@0 | 550 | var chromeWin = aWindow |
michael@0 | 551 | .QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 552 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 553 | .QueryInterface(Ci.nsIDocShellTreeItem) |
michael@0 | 554 | .rootTreeItem |
michael@0 | 555 | .QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 556 | .getInterface(Ci.nsIDOMWindow) |
michael@0 | 557 | .QueryInterface(Ci.nsIDOMChromeWindow); |
michael@0 | 558 | return chromeWin; |
michael@0 | 559 | }, |
michael@0 | 560 | |
michael@0 | 561 | _showInstallNotification: function(aDOMDocument, aAddonInstaller) { |
michael@0 | 562 | let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties"); |
michael@0 | 563 | let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties"); |
michael@0 | 564 | let requestingWindow = aDOMDocument.defaultView.top; |
michael@0 | 565 | let chromeWin = this._getChromeWindow(requestingWindow).wrappedJSObject; |
michael@0 | 566 | let browser = chromeWin.gBrowser.getBrowserForDocument(aDOMDocument); |
michael@0 | 567 | let requestingURI = Services.io.newURI(aDOMDocument.location.href, null, null); |
michael@0 | 568 | |
michael@0 | 569 | let productName = brandBundle.GetStringFromName("brandShortName"); |
michael@0 | 570 | |
michael@0 | 571 | let message = browserBundle.formatStringFromName("service.install.description", |
michael@0 | 572 | [requestingURI.host, productName], 2); |
michael@0 | 573 | |
michael@0 | 574 | let action = { |
michael@0 | 575 | label: browserBundle.GetStringFromName("service.install.ok.label"), |
michael@0 | 576 | accessKey: browserBundle.GetStringFromName("service.install.ok.accesskey"), |
michael@0 | 577 | callback: function() { |
michael@0 | 578 | aAddonInstaller.install(); |
michael@0 | 579 | }, |
michael@0 | 580 | }; |
michael@0 | 581 | |
michael@0 | 582 | let options = { |
michael@0 | 583 | learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api", |
michael@0 | 584 | }; |
michael@0 | 585 | let anchor = "servicesInstall-notification-icon"; |
michael@0 | 586 | let notificationid = "servicesInstall"; |
michael@0 | 587 | chromeWin.PopupNotifications.show(browser, notificationid, message, anchor, |
michael@0 | 588 | action, [], options); |
michael@0 | 589 | }, |
michael@0 | 590 | |
michael@0 | 591 | installProvider: function(aDOMDocument, data, installCallback) { |
michael@0 | 592 | let manifest; |
michael@0 | 593 | let installOrigin = aDOMDocument.nodePrincipal.origin; |
michael@0 | 594 | |
michael@0 | 595 | if (data) { |
michael@0 | 596 | let installType = getOriginActivationType(installOrigin); |
michael@0 | 597 | // if we get data, we MUST have a valid manifest generated from the data |
michael@0 | 598 | manifest = this._manifestFromData(installType, data, aDOMDocument.nodePrincipal); |
michael@0 | 599 | if (!manifest) |
michael@0 | 600 | throw new Error("SocialService.installProvider: service configuration is invalid from " + aDOMDocument.location.href); |
michael@0 | 601 | |
michael@0 | 602 | let addon = new AddonWrapper(manifest); |
michael@0 | 603 | if (addon && addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) |
michael@0 | 604 | throw new Error("installProvider: provider with origin [" + |
michael@0 | 605 | installOrigin + "] is blocklisted"); |
michael@0 | 606 | } |
michael@0 | 607 | |
michael@0 | 608 | let id = getAddonIDFromOrigin(installOrigin); |
michael@0 | 609 | AddonManager.getAddonByID(id, function(aAddon) { |
michael@0 | 610 | if (aAddon && aAddon.userDisabled) { |
michael@0 | 611 | aAddon.cancelUninstall(); |
michael@0 | 612 | aAddon.userDisabled = false; |
michael@0 | 613 | } |
michael@0 | 614 | schedule(function () { |
michael@0 | 615 | this._installProvider(aDOMDocument, manifest, aManifest => { |
michael@0 | 616 | this._notifyProviderListeners("provider-installed", aManifest.origin); |
michael@0 | 617 | installCallback(aManifest); |
michael@0 | 618 | }); |
michael@0 | 619 | }.bind(this)); |
michael@0 | 620 | }.bind(this)); |
michael@0 | 621 | }, |
michael@0 | 622 | |
michael@0 | 623 | _installProvider: function(aDOMDocument, manifest, installCallback) { |
michael@0 | 624 | let sourceURI = aDOMDocument.location.href; |
michael@0 | 625 | let installOrigin = aDOMDocument.nodePrincipal.origin; |
michael@0 | 626 | |
michael@0 | 627 | let installType = getOriginActivationType(installOrigin); |
michael@0 | 628 | let installer; |
michael@0 | 629 | switch(installType) { |
michael@0 | 630 | case "foreign": |
michael@0 | 631 | if (!Services.prefs.getBoolPref("social.remote-install.enabled")) |
michael@0 | 632 | throw new Error("Remote install of services is disabled"); |
michael@0 | 633 | if (!manifest) |
michael@0 | 634 | throw new Error("Cannot install provider without manifest data"); |
michael@0 | 635 | |
michael@0 | 636 | installer = new AddonInstaller(sourceURI, manifest, installCallback); |
michael@0 | 637 | this._showInstallNotification(aDOMDocument, installer); |
michael@0 | 638 | break; |
michael@0 | 639 | case "builtin": |
michael@0 | 640 | // for builtin, we already have a manifest, but it can be overridden |
michael@0 | 641 | // we need to return the manifest in the installcallback, so fetch |
michael@0 | 642 | // it if we have it. If there is no manifest data for the builtin, |
michael@0 | 643 | // the install request MUST be from the provider, otherwise we have |
michael@0 | 644 | // no way to know what provider we're trying to enable. This is |
michael@0 | 645 | // primarily an issue for "version zero" providers that did not |
michael@0 | 646 | // send the manifest with the dom event for activation. |
michael@0 | 647 | if (!manifest) { |
michael@0 | 648 | let prefname = getPrefnameFromOrigin(installOrigin); |
michael@0 | 649 | manifest = Services.prefs.getDefaultBranch(null) |
michael@0 | 650 | .getComplexValue(prefname, Ci.nsISupportsString).data; |
michael@0 | 651 | manifest = JSON.parse(manifest); |
michael@0 | 652 | // ensure we override a builtin manifest by having a different value in it |
michael@0 | 653 | if (manifest.builtin) |
michael@0 | 654 | delete manifest.builtin; |
michael@0 | 655 | } |
michael@0 | 656 | case "directory": |
michael@0 | 657 | // a manifest is requried, and will have been vetted by reviewers |
michael@0 | 658 | case "whitelist": |
michael@0 | 659 | // a manifest is required, we'll catch a missing manifest below. |
michael@0 | 660 | if (!manifest) |
michael@0 | 661 | throw new Error("Cannot install provider without manifest data"); |
michael@0 | 662 | installer = new AddonInstaller(sourceURI, manifest, installCallback); |
michael@0 | 663 | this._showInstallNotification(aDOMDocument, installer); |
michael@0 | 664 | break; |
michael@0 | 665 | default: |
michael@0 | 666 | throw new Error("SocialService.installProvider: Invalid install type "+installType+"\n"); |
michael@0 | 667 | break; |
michael@0 | 668 | } |
michael@0 | 669 | }, |
michael@0 | 670 | |
michael@0 | 671 | createWrapper: function(manifest) { |
michael@0 | 672 | return new AddonWrapper(manifest); |
michael@0 | 673 | }, |
michael@0 | 674 | |
michael@0 | 675 | /** |
michael@0 | 676 | * updateProvider is used from the worker to self-update. Since we do not |
michael@0 | 677 | * have knowledge of the currently selected provider here, we will notify |
michael@0 | 678 | * the front end to deal with any reload. |
michael@0 | 679 | */ |
michael@0 | 680 | updateProvider: function(aUpdateOrigin, aManifest) { |
michael@0 | 681 | let originUri = Services.io.newURI(aUpdateOrigin, null, null); |
michael@0 | 682 | let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri); |
michael@0 | 683 | let installType = this.getOriginActivationType(aUpdateOrigin); |
michael@0 | 684 | // if we get data, we MUST have a valid manifest generated from the data |
michael@0 | 685 | let manifest = this._manifestFromData(installType, aManifest, principal); |
michael@0 | 686 | if (!manifest) |
michael@0 | 687 | throw new Error("SocialService.installProvider: service configuration is invalid from " + aUpdateOrigin); |
michael@0 | 688 | |
michael@0 | 689 | // overwrite the preference |
michael@0 | 690 | let string = Cc["@mozilla.org/supports-string;1"]. |
michael@0 | 691 | createInstance(Ci.nsISupportsString); |
michael@0 | 692 | string.data = JSON.stringify(manifest); |
michael@0 | 693 | Services.prefs.setComplexValue(getPrefnameFromOrigin(manifest.origin), Ci.nsISupportsString, string); |
michael@0 | 694 | |
michael@0 | 695 | // overwrite the existing provider then notify the front end so it can |
michael@0 | 696 | // handle any reload that might be necessary. |
michael@0 | 697 | if (ActiveProviders.has(manifest.origin)) { |
michael@0 | 698 | let provider = new SocialProvider(manifest); |
michael@0 | 699 | SocialServiceInternal.providers[provider.origin] = provider; |
michael@0 | 700 | // update the cache and ui, reload provider if necessary |
michael@0 | 701 | this.getOrderedProviderList(providers => { |
michael@0 | 702 | this._notifyProviderListeners("provider-update", provider.origin, providers); |
michael@0 | 703 | }); |
michael@0 | 704 | } |
michael@0 | 705 | |
michael@0 | 706 | }, |
michael@0 | 707 | |
michael@0 | 708 | uninstallProvider: function(origin, aCallback) { |
michael@0 | 709 | let manifest = SocialService.getManifestByOrigin(origin); |
michael@0 | 710 | let addon = new AddonWrapper(manifest); |
michael@0 | 711 | addon.uninstall(aCallback); |
michael@0 | 712 | } |
michael@0 | 713 | }; |
michael@0 | 714 | |
michael@0 | 715 | /** |
michael@0 | 716 | * The SocialProvider object represents a social provider, and allows |
michael@0 | 717 | * access to its FrameWorker (if it has one). |
michael@0 | 718 | * |
michael@0 | 719 | * @constructor |
michael@0 | 720 | * @param {jsobj} object representing the manifest file describing this provider |
michael@0 | 721 | * @param {bool} boolean indicating whether this provider is "built in" |
michael@0 | 722 | */ |
michael@0 | 723 | function SocialProvider(input) { |
michael@0 | 724 | if (!input.name) |
michael@0 | 725 | throw new Error("SocialProvider must be passed a name"); |
michael@0 | 726 | if (!input.origin) |
michael@0 | 727 | throw new Error("SocialProvider must be passed an origin"); |
michael@0 | 728 | |
michael@0 | 729 | let addon = new AddonWrapper(input); |
michael@0 | 730 | if (addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) |
michael@0 | 731 | throw new Error("SocialProvider: provider with origin [" + |
michael@0 | 732 | input.origin + "] is blocklisted"); |
michael@0 | 733 | |
michael@0 | 734 | this.name = input.name; |
michael@0 | 735 | this.iconURL = input.iconURL; |
michael@0 | 736 | this.icon32URL = input.icon32URL; |
michael@0 | 737 | this.icon64URL = input.icon64URL; |
michael@0 | 738 | this.workerURL = input.workerURL; |
michael@0 | 739 | this.sidebarURL = input.sidebarURL; |
michael@0 | 740 | this.shareURL = input.shareURL; |
michael@0 | 741 | this.statusURL = input.statusURL; |
michael@0 | 742 | this.markURL = input.markURL; |
michael@0 | 743 | this.markedIcon = input.markedIcon; |
michael@0 | 744 | this.unmarkedIcon = input.unmarkedIcon; |
michael@0 | 745 | this.postActivationURL = input.postActivationURL; |
michael@0 | 746 | this.origin = input.origin; |
michael@0 | 747 | let originUri = Services.io.newURI(input.origin, null, null); |
michael@0 | 748 | this.principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(originUri); |
michael@0 | 749 | this.ambientNotificationIcons = {}; |
michael@0 | 750 | this.errorState = null; |
michael@0 | 751 | this.frecency = 0; |
michael@0 | 752 | |
michael@0 | 753 | let activationType = getOriginActivationType(input.origin); |
michael@0 | 754 | this.blessed = activationType == "builtin" || |
michael@0 | 755 | activationType == "whitelist"; |
michael@0 | 756 | |
michael@0 | 757 | try { |
michael@0 | 758 | this.domain = etld.getBaseDomainFromHost(originUri.host); |
michael@0 | 759 | } catch(e) { |
michael@0 | 760 | this.domain = originUri.host; |
michael@0 | 761 | } |
michael@0 | 762 | } |
michael@0 | 763 | |
michael@0 | 764 | SocialProvider.prototype = { |
michael@0 | 765 | reload: function() { |
michael@0 | 766 | this._terminate(); |
michael@0 | 767 | this._activate(); |
michael@0 | 768 | Services.obs.notifyObservers(null, "social:provider-reload", this.origin); |
michael@0 | 769 | }, |
michael@0 | 770 | |
michael@0 | 771 | // Provider enabled/disabled state. Disabled providers do not have active |
michael@0 | 772 | // connections to their FrameWorkers. |
michael@0 | 773 | _enabled: false, |
michael@0 | 774 | get enabled() { |
michael@0 | 775 | return this._enabled; |
michael@0 | 776 | }, |
michael@0 | 777 | set enabled(val) { |
michael@0 | 778 | let enable = !!val; |
michael@0 | 779 | if (enable == this._enabled) |
michael@0 | 780 | return; |
michael@0 | 781 | |
michael@0 | 782 | this._enabled = enable; |
michael@0 | 783 | |
michael@0 | 784 | if (enable) { |
michael@0 | 785 | this._activate(); |
michael@0 | 786 | } else { |
michael@0 | 787 | this._terminate(); |
michael@0 | 788 | } |
michael@0 | 789 | }, |
michael@0 | 790 | |
michael@0 | 791 | get manifest() { |
michael@0 | 792 | return SocialService.getManifestByOrigin(this.origin); |
michael@0 | 793 | }, |
michael@0 | 794 | |
michael@0 | 795 | // Reference to a workerAPI object for this provider. Null if the provider has |
michael@0 | 796 | // no FrameWorker, or is disabled. |
michael@0 | 797 | workerAPI: null, |
michael@0 | 798 | |
michael@0 | 799 | // Contains information related to the user's profile. Populated by the |
michael@0 | 800 | // workerAPI via updateUserProfile. |
michael@0 | 801 | // Properties: |
michael@0 | 802 | // iconURL, portrait, userName, displayName, profileURL |
michael@0 | 803 | // See https://github.com/mozilla/socialapi-dev/blob/develop/docs/socialAPI.md |
michael@0 | 804 | // A value of null or an empty object means 'user not logged in'. |
michael@0 | 805 | // A value of undefined means the service has not yet told us the status of |
michael@0 | 806 | // the profile (ie, the service is still loading/initing, or the provider has |
michael@0 | 807 | // no FrameWorker) |
michael@0 | 808 | // This distinction might be used to cache certain data between runs - eg, |
michael@0 | 809 | // browser-social.js caches the notification icons so they can be displayed |
michael@0 | 810 | // quickly at startup without waiting for the provider to initialize - |
michael@0 | 811 | // 'undefined' means 'ok to use cached values' versus 'null' meaning 'cached |
michael@0 | 812 | // values aren't to be used as the user is logged out'. |
michael@0 | 813 | profile: undefined, |
michael@0 | 814 | |
michael@0 | 815 | // Map of objects describing the provider's notification icons, whose |
michael@0 | 816 | // properties include: |
michael@0 | 817 | // name, iconURL, counter, contentPanel |
michael@0 | 818 | // See https://developer.mozilla.org/en-US/docs/Social_API |
michael@0 | 819 | ambientNotificationIcons: null, |
michael@0 | 820 | |
michael@0 | 821 | // Called by the workerAPI to update our profile information. |
michael@0 | 822 | updateUserProfile: function(profile) { |
michael@0 | 823 | if (!profile) |
michael@0 | 824 | profile = {}; |
michael@0 | 825 | let accountChanged = !this.profile || this.profile.userName != profile.userName; |
michael@0 | 826 | this.profile = profile; |
michael@0 | 827 | |
michael@0 | 828 | // Sanitize the portrait from any potential script-injection. |
michael@0 | 829 | if (profile.portrait) { |
michael@0 | 830 | try { |
michael@0 | 831 | let portraitUri = Services.io.newURI(profile.portrait, null, null); |
michael@0 | 832 | |
michael@0 | 833 | let scheme = portraitUri ? portraitUri.scheme : ""; |
michael@0 | 834 | if (scheme != "data" && scheme != "http" && scheme != "https") { |
michael@0 | 835 | profile.portrait = ""; |
michael@0 | 836 | } |
michael@0 | 837 | } catch (ex) { |
michael@0 | 838 | profile.portrait = ""; |
michael@0 | 839 | } |
michael@0 | 840 | } |
michael@0 | 841 | |
michael@0 | 842 | if (profile.iconURL) |
michael@0 | 843 | this.iconURL = profile.iconURL; |
michael@0 | 844 | |
michael@0 | 845 | if (!profile.displayName) |
michael@0 | 846 | profile.displayName = profile.userName; |
michael@0 | 847 | |
michael@0 | 848 | // if no userName, consider this a logged out state, emtpy the |
michael@0 | 849 | // users ambient notifications. notify both profile and ambient |
michael@0 | 850 | // changes to clear everything |
michael@0 | 851 | if (!profile.userName) { |
michael@0 | 852 | this.profile = {}; |
michael@0 | 853 | this.ambientNotificationIcons = {}; |
michael@0 | 854 | Services.obs.notifyObservers(null, "social:ambient-notification-changed", this.origin); |
michael@0 | 855 | } |
michael@0 | 856 | |
michael@0 | 857 | Services.obs.notifyObservers(null, "social:profile-changed", this.origin); |
michael@0 | 858 | if (accountChanged) |
michael@0 | 859 | closeAllChatWindows(this); |
michael@0 | 860 | }, |
michael@0 | 861 | |
michael@0 | 862 | haveLoggedInUser: function () { |
michael@0 | 863 | return !!(this.profile && this.profile.userName); |
michael@0 | 864 | }, |
michael@0 | 865 | |
michael@0 | 866 | // Called by the workerAPI to add/update a notification icon. |
michael@0 | 867 | setAmbientNotification: function(notification) { |
michael@0 | 868 | if (!this.profile.userName) |
michael@0 | 869 | throw new Error("unable to set notifications while logged out"); |
michael@0 | 870 | if (!this.ambientNotificationIcons[notification.name] && |
michael@0 | 871 | Object.keys(this.ambientNotificationIcons).length >= 3) { |
michael@0 | 872 | throw new Error("ambient notification limit reached"); |
michael@0 | 873 | } |
michael@0 | 874 | this.ambientNotificationIcons[notification.name] = notification; |
michael@0 | 875 | |
michael@0 | 876 | Services.obs.notifyObservers(null, "social:ambient-notification-changed", this.origin); |
michael@0 | 877 | }, |
michael@0 | 878 | |
michael@0 | 879 | // Internal helper methods |
michael@0 | 880 | _activate: function _activate() { |
michael@0 | 881 | // Initialize the workerAPI and its port first, so that its initialization |
michael@0 | 882 | // occurs before any other messages are processed by other ports. |
michael@0 | 883 | let workerAPIPort = this.getWorkerPort(); |
michael@0 | 884 | if (workerAPIPort) |
michael@0 | 885 | this.workerAPI = new WorkerAPI(this, workerAPIPort); |
michael@0 | 886 | }, |
michael@0 | 887 | |
michael@0 | 888 | _terminate: function _terminate() { |
michael@0 | 889 | closeAllChatWindows(this); |
michael@0 | 890 | if (this.workerURL) { |
michael@0 | 891 | try { |
michael@0 | 892 | getFrameWorkerHandle(this.workerURL).terminate(); |
michael@0 | 893 | } catch (e) { |
michael@0 | 894 | Cu.reportError("SocialProvider FrameWorker termination failed: " + e); |
michael@0 | 895 | } |
michael@0 | 896 | } |
michael@0 | 897 | if (this.workerAPI) { |
michael@0 | 898 | this.workerAPI.terminate(); |
michael@0 | 899 | } |
michael@0 | 900 | this.errorState = null; |
michael@0 | 901 | this.workerAPI = null; |
michael@0 | 902 | this.profile = undefined; |
michael@0 | 903 | }, |
michael@0 | 904 | |
michael@0 | 905 | /** |
michael@0 | 906 | * Instantiates a FrameWorker for the provider if one doesn't exist, and |
michael@0 | 907 | * returns a reference to a new port to that FrameWorker. |
michael@0 | 908 | * |
michael@0 | 909 | * Returns null if this provider has no workerURL, or is disabled. |
michael@0 | 910 | * |
michael@0 | 911 | * @param {DOMWindow} window (optional) |
michael@0 | 912 | */ |
michael@0 | 913 | getWorkerPort: function getWorkerPort(window) { |
michael@0 | 914 | if (!this.workerURL || !this.enabled) |
michael@0 | 915 | return null; |
michael@0 | 916 | // Only allow localStorage in the frameworker for blessed providers |
michael@0 | 917 | let allowLocalStorage = this.blessed; |
michael@0 | 918 | let handle = getFrameWorkerHandle(this.workerURL, window, |
michael@0 | 919 | "SocialProvider:" + this.origin, this.origin, |
michael@0 | 920 | allowLocalStorage); |
michael@0 | 921 | return handle.port; |
michael@0 | 922 | }, |
michael@0 | 923 | |
michael@0 | 924 | /** |
michael@0 | 925 | * Checks if a given URI is of the same origin as the provider. |
michael@0 | 926 | * |
michael@0 | 927 | * Returns true or false. |
michael@0 | 928 | * |
michael@0 | 929 | * @param {URI or string} uri |
michael@0 | 930 | */ |
michael@0 | 931 | isSameOrigin: function isSameOrigin(uri, allowIfInheritsPrincipal) { |
michael@0 | 932 | if (!uri) |
michael@0 | 933 | return false; |
michael@0 | 934 | if (typeof uri == "string") { |
michael@0 | 935 | try { |
michael@0 | 936 | uri = Services.io.newURI(uri, null, null); |
michael@0 | 937 | } catch (ex) { |
michael@0 | 938 | // an invalid URL can't be loaded! |
michael@0 | 939 | return false; |
michael@0 | 940 | } |
michael@0 | 941 | } |
michael@0 | 942 | try { |
michael@0 | 943 | this.principal.checkMayLoad( |
michael@0 | 944 | uri, // the thing to check. |
michael@0 | 945 | false, // reportError - we do our own reporting when necessary. |
michael@0 | 946 | allowIfInheritsPrincipal |
michael@0 | 947 | ); |
michael@0 | 948 | return true; |
michael@0 | 949 | } catch (ex) { |
michael@0 | 950 | return false; |
michael@0 | 951 | } |
michael@0 | 952 | }, |
michael@0 | 953 | |
michael@0 | 954 | /** |
michael@0 | 955 | * Resolve partial URLs for a provider. |
michael@0 | 956 | * |
michael@0 | 957 | * Returns nsIURI object or null on failure |
michael@0 | 958 | * |
michael@0 | 959 | * @param {string} url |
michael@0 | 960 | */ |
michael@0 | 961 | resolveUri: function resolveUri(url) { |
michael@0 | 962 | try { |
michael@0 | 963 | let fullURL = this.principal.URI.resolve(url); |
michael@0 | 964 | return Services.io.newURI(fullURL, null, null); |
michael@0 | 965 | } catch (ex) { |
michael@0 | 966 | Cu.reportError("mozSocial: failed to resolve window URL: " + url + "; " + ex); |
michael@0 | 967 | return null; |
michael@0 | 968 | } |
michael@0 | 969 | } |
michael@0 | 970 | } |
michael@0 | 971 | |
michael@0 | 972 | function getAddonIDFromOrigin(origin) { |
michael@0 | 973 | let originUri = Services.io.newURI(origin, null, null); |
michael@0 | 974 | return originUri.host + ID_SUFFIX; |
michael@0 | 975 | } |
michael@0 | 976 | |
michael@0 | 977 | function getPrefnameFromOrigin(origin) { |
michael@0 | 978 | return "social.manifest." + SocialServiceInternal.getManifestPrefname(origin); |
michael@0 | 979 | } |
michael@0 | 980 | |
michael@0 | 981 | function AddonInstaller(sourceURI, aManifest, installCallback) { |
michael@0 | 982 | aManifest.updateDate = Date.now(); |
michael@0 | 983 | // get the existing manifest for installDate |
michael@0 | 984 | let manifest = SocialService.getManifestByOrigin(aManifest.origin); |
michael@0 | 985 | let isNewInstall = !manifest; |
michael@0 | 986 | if (manifest && manifest.installDate) |
michael@0 | 987 | aManifest.installDate = manifest.installDate; |
michael@0 | 988 | else |
michael@0 | 989 | aManifest.installDate = aManifest.updateDate; |
michael@0 | 990 | |
michael@0 | 991 | this.sourceURI = sourceURI; |
michael@0 | 992 | this.install = function() { |
michael@0 | 993 | let addon = this.addon; |
michael@0 | 994 | if (isNewInstall) { |
michael@0 | 995 | AddonManagerPrivate.callInstallListeners("onExternalInstall", null, addon, null, false); |
michael@0 | 996 | AddonManagerPrivate.callAddonListeners("onInstalling", addon, false); |
michael@0 | 997 | } |
michael@0 | 998 | |
michael@0 | 999 | let string = Cc["@mozilla.org/supports-string;1"]. |
michael@0 | 1000 | createInstance(Ci.nsISupportsString); |
michael@0 | 1001 | string.data = JSON.stringify(aManifest); |
michael@0 | 1002 | Services.prefs.setComplexValue(getPrefnameFromOrigin(aManifest.origin), Ci.nsISupportsString, string); |
michael@0 | 1003 | |
michael@0 | 1004 | if (isNewInstall) { |
michael@0 | 1005 | AddonManagerPrivate.callAddonListeners("onInstalled", addon); |
michael@0 | 1006 | } |
michael@0 | 1007 | installCallback(aManifest); |
michael@0 | 1008 | }; |
michael@0 | 1009 | this.cancel = function() { |
michael@0 | 1010 | Services.prefs.clearUserPref(getPrefnameFromOrigin(aManifest.origin)) |
michael@0 | 1011 | }, |
michael@0 | 1012 | this.addon = new AddonWrapper(aManifest); |
michael@0 | 1013 | }; |
michael@0 | 1014 | |
michael@0 | 1015 | var SocialAddonProvider = { |
michael@0 | 1016 | startup: function() {}, |
michael@0 | 1017 | |
michael@0 | 1018 | shutdown: function() {}, |
michael@0 | 1019 | |
michael@0 | 1020 | updateAddonAppDisabledStates: function() { |
michael@0 | 1021 | // we wont bother with "enabling" services that are released from blocklist |
michael@0 | 1022 | for (let manifest of SocialServiceInternal.manifests) { |
michael@0 | 1023 | try { |
michael@0 | 1024 | if (ActiveProviders.has(manifest.origin)) { |
michael@0 | 1025 | let addon = new AddonWrapper(manifest); |
michael@0 | 1026 | if (addon.blocklistState != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) { |
michael@0 | 1027 | SocialService.removeProvider(manifest.origin); |
michael@0 | 1028 | } |
michael@0 | 1029 | } |
michael@0 | 1030 | } catch(e) { |
michael@0 | 1031 | Cu.reportError(e); |
michael@0 | 1032 | } |
michael@0 | 1033 | } |
michael@0 | 1034 | }, |
michael@0 | 1035 | |
michael@0 | 1036 | getAddonByID: function(aId, aCallback) { |
michael@0 | 1037 | for (let manifest of SocialServiceInternal.manifests) { |
michael@0 | 1038 | if (aId == getAddonIDFromOrigin(manifest.origin)) { |
michael@0 | 1039 | aCallback(new AddonWrapper(manifest)); |
michael@0 | 1040 | return; |
michael@0 | 1041 | } |
michael@0 | 1042 | } |
michael@0 | 1043 | aCallback(null); |
michael@0 | 1044 | }, |
michael@0 | 1045 | |
michael@0 | 1046 | getAddonsByTypes: function(aTypes, aCallback) { |
michael@0 | 1047 | if (aTypes && aTypes.indexOf(ADDON_TYPE_SERVICE) == -1) { |
michael@0 | 1048 | aCallback([]); |
michael@0 | 1049 | return; |
michael@0 | 1050 | } |
michael@0 | 1051 | aCallback([new AddonWrapper(a) for each (a in SocialServiceInternal.manifests)]); |
michael@0 | 1052 | }, |
michael@0 | 1053 | |
michael@0 | 1054 | removeAddon: function(aAddon, aCallback) { |
michael@0 | 1055 | AddonManagerPrivate.callAddonListeners("onUninstalling", aAddon, false); |
michael@0 | 1056 | aAddon.pendingOperations |= AddonManager.PENDING_UNINSTALL; |
michael@0 | 1057 | Services.prefs.clearUserPref(getPrefnameFromOrigin(aAddon.manifest.origin)); |
michael@0 | 1058 | aAddon.pendingOperations -= AddonManager.PENDING_UNINSTALL; |
michael@0 | 1059 | AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon); |
michael@0 | 1060 | SocialService._notifyProviderListeners("provider-uninstalled", aAddon.manifest.origin); |
michael@0 | 1061 | if (aCallback) |
michael@0 | 1062 | schedule(aCallback); |
michael@0 | 1063 | } |
michael@0 | 1064 | } |
michael@0 | 1065 | |
michael@0 | 1066 | |
michael@0 | 1067 | function AddonWrapper(aManifest) { |
michael@0 | 1068 | this.manifest = aManifest; |
michael@0 | 1069 | this.id = getAddonIDFromOrigin(this.manifest.origin); |
michael@0 | 1070 | this._pending = AddonManager.PENDING_NONE; |
michael@0 | 1071 | } |
michael@0 | 1072 | AddonWrapper.prototype = { |
michael@0 | 1073 | get type() { |
michael@0 | 1074 | return ADDON_TYPE_SERVICE; |
michael@0 | 1075 | }, |
michael@0 | 1076 | |
michael@0 | 1077 | get appDisabled() { |
michael@0 | 1078 | return this.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED; |
michael@0 | 1079 | }, |
michael@0 | 1080 | |
michael@0 | 1081 | set softDisabled(val) { |
michael@0 | 1082 | this.userDisabled = val; |
michael@0 | 1083 | }, |
michael@0 | 1084 | |
michael@0 | 1085 | get softDisabled() { |
michael@0 | 1086 | return this.userDisabled; |
michael@0 | 1087 | }, |
michael@0 | 1088 | |
michael@0 | 1089 | get isCompatible() { |
michael@0 | 1090 | return true; |
michael@0 | 1091 | }, |
michael@0 | 1092 | |
michael@0 | 1093 | get isPlatformCompatible() { |
michael@0 | 1094 | return true; |
michael@0 | 1095 | }, |
michael@0 | 1096 | |
michael@0 | 1097 | get scope() { |
michael@0 | 1098 | return AddonManager.SCOPE_PROFILE; |
michael@0 | 1099 | }, |
michael@0 | 1100 | |
michael@0 | 1101 | get foreignInstall() { |
michael@0 | 1102 | return false; |
michael@0 | 1103 | }, |
michael@0 | 1104 | |
michael@0 | 1105 | isCompatibleWith: function(appVersion, platformVersion) { |
michael@0 | 1106 | return true; |
michael@0 | 1107 | }, |
michael@0 | 1108 | |
michael@0 | 1109 | get providesUpdatesSecurely() { |
michael@0 | 1110 | return true; |
michael@0 | 1111 | }, |
michael@0 | 1112 | |
michael@0 | 1113 | get blocklistState() { |
michael@0 | 1114 | return Services.blocklist.getAddonBlocklistState(this); |
michael@0 | 1115 | }, |
michael@0 | 1116 | |
michael@0 | 1117 | get blocklistURL() { |
michael@0 | 1118 | return Services.blocklist.getAddonBlocklistURL(this); |
michael@0 | 1119 | }, |
michael@0 | 1120 | |
michael@0 | 1121 | get screenshots() { |
michael@0 | 1122 | return []; |
michael@0 | 1123 | }, |
michael@0 | 1124 | |
michael@0 | 1125 | get pendingOperations() { |
michael@0 | 1126 | return this._pending || AddonManager.PENDING_NONE; |
michael@0 | 1127 | }, |
michael@0 | 1128 | set pendingOperations(val) { |
michael@0 | 1129 | this._pending = val; |
michael@0 | 1130 | }, |
michael@0 | 1131 | |
michael@0 | 1132 | get operationsRequiringRestart() { |
michael@0 | 1133 | return AddonManager.OP_NEEDS_RESTART_NONE; |
michael@0 | 1134 | }, |
michael@0 | 1135 | |
michael@0 | 1136 | get size() { |
michael@0 | 1137 | return null; |
michael@0 | 1138 | }, |
michael@0 | 1139 | |
michael@0 | 1140 | get permissions() { |
michael@0 | 1141 | let permissions = 0; |
michael@0 | 1142 | // any "user defined" manifest can be removed |
michael@0 | 1143 | if (Services.prefs.prefHasUserValue(getPrefnameFromOrigin(this.manifest.origin))) |
michael@0 | 1144 | permissions = AddonManager.PERM_CAN_UNINSTALL; |
michael@0 | 1145 | if (!this.appDisabled) { |
michael@0 | 1146 | if (this.userDisabled) { |
michael@0 | 1147 | permissions |= AddonManager.PERM_CAN_ENABLE; |
michael@0 | 1148 | } else { |
michael@0 | 1149 | permissions |= AddonManager.PERM_CAN_DISABLE; |
michael@0 | 1150 | } |
michael@0 | 1151 | } |
michael@0 | 1152 | return permissions; |
michael@0 | 1153 | }, |
michael@0 | 1154 | |
michael@0 | 1155 | findUpdates: function(listener, reason, appVersion, platformVersion) { |
michael@0 | 1156 | if ("onNoCompatibilityUpdateAvailable" in listener) |
michael@0 | 1157 | listener.onNoCompatibilityUpdateAvailable(this); |
michael@0 | 1158 | if ("onNoUpdateAvailable" in listener) |
michael@0 | 1159 | listener.onNoUpdateAvailable(this); |
michael@0 | 1160 | if ("onUpdateFinished" in listener) |
michael@0 | 1161 | listener.onUpdateFinished(this); |
michael@0 | 1162 | }, |
michael@0 | 1163 | |
michael@0 | 1164 | get isActive() { |
michael@0 | 1165 | return ActiveProviders.has(this.manifest.origin); |
michael@0 | 1166 | }, |
michael@0 | 1167 | |
michael@0 | 1168 | get name() { |
michael@0 | 1169 | return this.manifest.name; |
michael@0 | 1170 | }, |
michael@0 | 1171 | get version() { |
michael@0 | 1172 | return this.manifest.version ? this.manifest.version : ""; |
michael@0 | 1173 | }, |
michael@0 | 1174 | |
michael@0 | 1175 | get iconURL() { |
michael@0 | 1176 | return this.manifest.icon32URL ? this.manifest.icon32URL : this.manifest.iconURL; |
michael@0 | 1177 | }, |
michael@0 | 1178 | get icon64URL() { |
michael@0 | 1179 | return this.manifest.icon64URL; |
michael@0 | 1180 | }, |
michael@0 | 1181 | get icons() { |
michael@0 | 1182 | let icons = { |
michael@0 | 1183 | 16: this.manifest.iconURL |
michael@0 | 1184 | }; |
michael@0 | 1185 | if (this.manifest.icon32URL) |
michael@0 | 1186 | icons[32] = this.manifest.icon32URL; |
michael@0 | 1187 | if (this.manifest.icon64URL) |
michael@0 | 1188 | icons[64] = this.manifest.icon64URL; |
michael@0 | 1189 | return icons; |
michael@0 | 1190 | }, |
michael@0 | 1191 | |
michael@0 | 1192 | get description() { |
michael@0 | 1193 | return this.manifest.description; |
michael@0 | 1194 | }, |
michael@0 | 1195 | get homepageURL() { |
michael@0 | 1196 | return this.manifest.homepageURL; |
michael@0 | 1197 | }, |
michael@0 | 1198 | get defaultLocale() { |
michael@0 | 1199 | return this.manifest.defaultLocale; |
michael@0 | 1200 | }, |
michael@0 | 1201 | get selectedLocale() { |
michael@0 | 1202 | return this.manifest.selectedLocale; |
michael@0 | 1203 | }, |
michael@0 | 1204 | |
michael@0 | 1205 | get installDate() { |
michael@0 | 1206 | return this.manifest.installDate ? new Date(this.manifest.installDate) : null; |
michael@0 | 1207 | }, |
michael@0 | 1208 | get updateDate() { |
michael@0 | 1209 | return this.manifest.updateDate ? new Date(this.manifest.updateDate) : null; |
michael@0 | 1210 | }, |
michael@0 | 1211 | |
michael@0 | 1212 | get creator() { |
michael@0 | 1213 | return new AddonManagerPrivate.AddonAuthor(this.manifest.author); |
michael@0 | 1214 | }, |
michael@0 | 1215 | |
michael@0 | 1216 | get userDisabled() { |
michael@0 | 1217 | return this.appDisabled || !ActiveProviders.has(this.manifest.origin); |
michael@0 | 1218 | }, |
michael@0 | 1219 | |
michael@0 | 1220 | set userDisabled(val) { |
michael@0 | 1221 | if (val == this.userDisabled) |
michael@0 | 1222 | return val; |
michael@0 | 1223 | if (val) { |
michael@0 | 1224 | SocialService.removeProvider(this.manifest.origin); |
michael@0 | 1225 | } else if (!this.appDisabled) { |
michael@0 | 1226 | SocialService.addBuiltinProvider(this.manifest.origin); |
michael@0 | 1227 | } |
michael@0 | 1228 | return val; |
michael@0 | 1229 | }, |
michael@0 | 1230 | |
michael@0 | 1231 | uninstall: function(aCallback) { |
michael@0 | 1232 | let prefName = getPrefnameFromOrigin(this.manifest.origin); |
michael@0 | 1233 | if (Services.prefs.prefHasUserValue(prefName)) { |
michael@0 | 1234 | if (ActiveProviders.has(this.manifest.origin)) { |
michael@0 | 1235 | SocialService.removeProvider(this.manifest.origin, function() { |
michael@0 | 1236 | SocialAddonProvider.removeAddon(this, aCallback); |
michael@0 | 1237 | }.bind(this)); |
michael@0 | 1238 | } else { |
michael@0 | 1239 | SocialAddonProvider.removeAddon(this, aCallback); |
michael@0 | 1240 | } |
michael@0 | 1241 | } else { |
michael@0 | 1242 | schedule(aCallback); |
michael@0 | 1243 | } |
michael@0 | 1244 | }, |
michael@0 | 1245 | |
michael@0 | 1246 | cancelUninstall: function() { |
michael@0 | 1247 | this._pending -= AddonManager.PENDING_UNINSTALL; |
michael@0 | 1248 | AddonManagerPrivate.callAddonListeners("onOperationCancelled", this); |
michael@0 | 1249 | } |
michael@0 | 1250 | }; |
michael@0 | 1251 | |
michael@0 | 1252 | |
michael@0 | 1253 | AddonManagerPrivate.registerProvider(SocialAddonProvider, [ |
michael@0 | 1254 | new AddonManagerPrivate.AddonType(ADDON_TYPE_SERVICE, URI_EXTENSION_STRINGS, |
michael@0 | 1255 | STRING_TYPE_NAME, |
michael@0 | 1256 | AddonManager.VIEW_TYPE_LIST, 10000) |
michael@0 | 1257 | ]); |