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