toolkit/mozapps/extensions/LightweightThemeManager.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 this.EXPORTED_SYMBOLS = ["LightweightThemeManager"];
michael@0 8
michael@0 9 const Cc = Components.classes;
michael@0 10 const Ci = Components.interfaces;
michael@0 11
michael@0 12 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 13 Components.utils.import("resource://gre/modules/AddonManager.jsm");
michael@0 14 Components.utils.import("resource://gre/modules/Services.jsm");
michael@0 15
michael@0 16 const ID_SUFFIX = "@personas.mozilla.org";
michael@0 17 const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect";
michael@0 18 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
michael@0 19 const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
michael@0 20 const ADDON_TYPE = "theme";
michael@0 21
michael@0 22 const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
michael@0 23
michael@0 24 const STRING_TYPE_NAME = "type.%ID%.name";
michael@0 25
michael@0 26 const DEFAULT_MAX_USED_THEMES_COUNT = 30;
michael@0 27
michael@0 28 const MAX_PREVIEW_SECONDS = 30;
michael@0 29
michael@0 30 const MANDATORY = ["id", "name", "headerURL"];
michael@0 31 const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL",
michael@0 32 "previewURL", "author", "description", "homepageURL",
michael@0 33 "updateURL", "version"];
michael@0 34
michael@0 35 const PERSIST_ENABLED = true;
michael@0 36 const PERSIST_BYPASS_CACHE = false;
michael@0 37 const PERSIST_FILES = {
michael@0 38 headerURL: "lightweighttheme-header",
michael@0 39 footerURL: "lightweighttheme-footer"
michael@0 40 };
michael@0 41
michael@0 42 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer",
michael@0 43 "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm");
michael@0 44
michael@0 45 this.__defineGetter__("_prefs", function prefsGetter() {
michael@0 46 delete this._prefs;
michael@0 47 return this._prefs = Services.prefs.getBranch("lightweightThemes.");
michael@0 48 });
michael@0 49
michael@0 50 this.__defineGetter__("_maxUsedThemes", function maxUsedThemesGetter() {
michael@0 51 delete this._maxUsedThemes;
michael@0 52 try {
michael@0 53 this._maxUsedThemes = _prefs.getIntPref("maxUsedThemes");
michael@0 54 }
michael@0 55 catch (e) {
michael@0 56 this._maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT;
michael@0 57 }
michael@0 58 return this._maxUsedThemes;
michael@0 59 });
michael@0 60
michael@0 61 this.__defineSetter__("_maxUsedThemes", function maxUsedThemesSetter(aVal) {
michael@0 62 delete this._maxUsedThemes;
michael@0 63 return this._maxUsedThemes = aVal;
michael@0 64 });
michael@0 65
michael@0 66 // Holds the ID of the theme being enabled or disabled while sending out the
michael@0 67 // events so cached AddonWrapper instances can return correct values for
michael@0 68 // permissions and pendingOperations
michael@0 69 var _themeIDBeingEnabled = null;
michael@0 70 var _themeIDBeingDisabled = null;
michael@0 71
michael@0 72 this.LightweightThemeManager = {
michael@0 73 get usedThemes () {
michael@0 74 try {
michael@0 75 return JSON.parse(_prefs.getComplexValue("usedThemes",
michael@0 76 Ci.nsISupportsString).data);
michael@0 77 } catch (e) {
michael@0 78 return [];
michael@0 79 }
michael@0 80 },
michael@0 81
michael@0 82 get currentTheme () {
michael@0 83 try {
michael@0 84 if (_prefs.getBoolPref("isThemeSelected"))
michael@0 85 var data = this.usedThemes[0];
michael@0 86 } catch (e) {}
michael@0 87
michael@0 88 return data || null;
michael@0 89 },
michael@0 90
michael@0 91 get currentThemeForDisplay () {
michael@0 92 var data = this.currentTheme;
michael@0 93
michael@0 94 if (data && PERSIST_ENABLED) {
michael@0 95 for (let key in PERSIST_FILES) {
michael@0 96 try {
michael@0 97 if (data[key] && _prefs.getBoolPref("persisted." + key))
michael@0 98 data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec
michael@0 99 + "?" + data.id + ";" + _version(data);
michael@0 100 } catch (e) {}
michael@0 101 }
michael@0 102 }
michael@0 103
michael@0 104 return data;
michael@0 105 },
michael@0 106
michael@0 107 set currentTheme (aData) {
michael@0 108 return _setCurrentTheme(aData, false);
michael@0 109 },
michael@0 110
michael@0 111 setLocalTheme: function LightweightThemeManager_setLocalTheme(aData) {
michael@0 112 _setCurrentTheme(aData, true);
michael@0 113 },
michael@0 114
michael@0 115 getUsedTheme: function LightweightThemeManager_getUsedTheme(aId) {
michael@0 116 var usedThemes = this.usedThemes;
michael@0 117 for (let usedTheme of usedThemes) {
michael@0 118 if (usedTheme.id == aId)
michael@0 119 return usedTheme;
michael@0 120 }
michael@0 121 return null;
michael@0 122 },
michael@0 123
michael@0 124 forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) {
michael@0 125 let theme = this.getUsedTheme(aId);
michael@0 126 if (!theme)
michael@0 127 return;
michael@0 128
michael@0 129 let wrapper = new AddonWrapper(theme);
michael@0 130 AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
michael@0 131
michael@0 132 var currentTheme = this.currentTheme;
michael@0 133 if (currentTheme && currentTheme.id == aId) {
michael@0 134 this.themeChanged(null);
michael@0 135 AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false);
michael@0 136 }
michael@0 137
michael@0 138 _updateUsedThemes(_usedThemesExceptId(aId));
michael@0 139 AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
michael@0 140 },
michael@0 141
michael@0 142 previewTheme: function LightweightThemeManager_previewTheme(aData) {
michael@0 143 if (!aData)
michael@0 144 return;
michael@0 145
michael@0 146 let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
michael@0 147 cancel.data = false;
michael@0 148 Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested",
michael@0 149 JSON.stringify(aData));
michael@0 150 if (cancel.data)
michael@0 151 return;
michael@0 152
michael@0 153 if (_previewTimer)
michael@0 154 _previewTimer.cancel();
michael@0 155 else
michael@0 156 _previewTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
michael@0 157 _previewTimer.initWithCallback(_previewTimerCallback,
michael@0 158 MAX_PREVIEW_SECONDS * 1000,
michael@0 159 _previewTimer.TYPE_ONE_SHOT);
michael@0 160
michael@0 161 _notifyWindows(aData);
michael@0 162 },
michael@0 163
michael@0 164 resetPreview: function LightweightThemeManager_resetPreview() {
michael@0 165 if (_previewTimer) {
michael@0 166 _previewTimer.cancel();
michael@0 167 _previewTimer = null;
michael@0 168 _notifyWindows(this.currentThemeForDisplay);
michael@0 169 }
michael@0 170 },
michael@0 171
michael@0 172 parseTheme: function LightweightThemeManager_parseTheme(aString, aBaseURI) {
michael@0 173 try {
michael@0 174 return _sanitizeTheme(JSON.parse(aString), aBaseURI, false);
michael@0 175 } catch (e) {
michael@0 176 return null;
michael@0 177 }
michael@0 178 },
michael@0 179
michael@0 180 updateCurrentTheme: function LightweightThemeManager_updateCurrentTheme() {
michael@0 181 try {
michael@0 182 if (!_prefs.getBoolPref("update.enabled"))
michael@0 183 return;
michael@0 184 } catch (e) {
michael@0 185 return;
michael@0 186 }
michael@0 187
michael@0 188 var theme = this.currentTheme;
michael@0 189 if (!theme || !theme.updateURL)
michael@0 190 return;
michael@0 191
michael@0 192 var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
michael@0 193 .createInstance(Ci.nsIXMLHttpRequest);
michael@0 194
michael@0 195 req.mozBackgroundRequest = true;
michael@0 196 req.overrideMimeType("text/plain");
michael@0 197 req.open("GET", theme.updateURL, true);
michael@0 198 // Prevent the request from reading from the cache.
michael@0 199 req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
michael@0 200 // Prevent the request from writing to the cache.
michael@0 201 req.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
michael@0 202
michael@0 203 var self = this;
michael@0 204 req.addEventListener("load", function loadEventListener() {
michael@0 205 if (req.status != 200)
michael@0 206 return;
michael@0 207
michael@0 208 let newData = self.parseTheme(req.responseText, theme.updateURL);
michael@0 209 if (!newData ||
michael@0 210 newData.id != theme.id ||
michael@0 211 _version(newData) == _version(theme))
michael@0 212 return;
michael@0 213
michael@0 214 var currentTheme = self.currentTheme;
michael@0 215 if (currentTheme && currentTheme.id == theme.id)
michael@0 216 self.currentTheme = newData;
michael@0 217 }, false);
michael@0 218
michael@0 219 req.send(null);
michael@0 220 },
michael@0 221
michael@0 222 /**
michael@0 223 * Switches to a new lightweight theme.
michael@0 224 *
michael@0 225 * @param aData
michael@0 226 * The lightweight theme to switch to
michael@0 227 */
michael@0 228 themeChanged: function LightweightThemeManager_themeChanged(aData) {
michael@0 229 if (_previewTimer) {
michael@0 230 _previewTimer.cancel();
michael@0 231 _previewTimer = null;
michael@0 232 }
michael@0 233
michael@0 234 if (aData) {
michael@0 235 let usedThemes = _usedThemesExceptId(aData.id);
michael@0 236 usedThemes.unshift(aData);
michael@0 237 _updateUsedThemes(usedThemes);
michael@0 238 if (PERSIST_ENABLED) {
michael@0 239 LightweightThemeImageOptimizer.purge();
michael@0 240 _persistImages(aData, function themeChanged_persistImages() {
michael@0 241 _notifyWindows(this.currentThemeForDisplay);
michael@0 242 }.bind(this));
michael@0 243 }
michael@0 244 }
michael@0 245
michael@0 246 _prefs.setBoolPref("isThemeSelected", aData != null);
michael@0 247 _notifyWindows(aData);
michael@0 248 Services.obs.notifyObservers(null, "lightweight-theme-changed", null);
michael@0 249 },
michael@0 250
michael@0 251 /**
michael@0 252 * Starts the Addons provider and enables the new lightweight theme if
michael@0 253 * necessary.
michael@0 254 */
michael@0 255 startup: function LightweightThemeManager_startup() {
michael@0 256 if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) {
michael@0 257 let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
michael@0 258 if (id)
michael@0 259 this.themeChanged(this.getUsedTheme(id));
michael@0 260 else
michael@0 261 this.themeChanged(null);
michael@0 262 Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
michael@0 263 }
michael@0 264
michael@0 265 _prefs.addObserver("", _prefObserver, false);
michael@0 266 },
michael@0 267
michael@0 268 /**
michael@0 269 * Shuts down the provider.
michael@0 270 */
michael@0 271 shutdown: function LightweightThemeManager_shutdown() {
michael@0 272 _prefs.removeObserver("", _prefObserver);
michael@0 273 },
michael@0 274
michael@0 275 /**
michael@0 276 * Called when a new add-on has been enabled when only one add-on of that type
michael@0 277 * can be enabled.
michael@0 278 *
michael@0 279 * @param aId
michael@0 280 * The ID of the newly enabled add-on
michael@0 281 * @param aType
michael@0 282 * The type of the newly enabled add-on
michael@0 283 * @param aPendingRestart
michael@0 284 * true if the newly enabled add-on will only become enabled after a
michael@0 285 * restart
michael@0 286 */
michael@0 287 addonChanged: function LightweightThemeManager_addonChanged(aId, aType, aPendingRestart) {
michael@0 288 if (aType != ADDON_TYPE)
michael@0 289 return;
michael@0 290
michael@0 291 let id = _getInternalID(aId);
michael@0 292 let current = this.currentTheme;
michael@0 293
michael@0 294 try {
michael@0 295 let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
michael@0 296 if (id == next && aPendingRestart)
michael@0 297 return;
michael@0 298
michael@0 299 Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
michael@0 300 if (next) {
michael@0 301 AddonManagerPrivate.callAddonListeners("onOperationCancelled",
michael@0 302 new AddonWrapper(this.getUsedTheme(next)));
michael@0 303 }
michael@0 304 else {
michael@0 305 if (id == current.id) {
michael@0 306 AddonManagerPrivate.callAddonListeners("onOperationCancelled",
michael@0 307 new AddonWrapper(current));
michael@0 308 return;
michael@0 309 }
michael@0 310 }
michael@0 311 }
michael@0 312 catch (e) {
michael@0 313 }
michael@0 314
michael@0 315 if (current) {
michael@0 316 if (current.id == id)
michael@0 317 return;
michael@0 318 _themeIDBeingDisabled = current.id;
michael@0 319 let wrapper = new AddonWrapper(current);
michael@0 320 if (aPendingRestart) {
michael@0 321 Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, "");
michael@0 322 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true);
michael@0 323 }
michael@0 324 else {
michael@0 325 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
michael@0 326 this.themeChanged(null);
michael@0 327 AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
michael@0 328 }
michael@0 329 _themeIDBeingDisabled = null;
michael@0 330 }
michael@0 331
michael@0 332 if (id) {
michael@0 333 let theme = this.getUsedTheme(id);
michael@0 334 _themeIDBeingEnabled = id;
michael@0 335 let wrapper = new AddonWrapper(theme);
michael@0 336 if (aPendingRestart) {
michael@0 337 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true);
michael@0 338 Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id);
michael@0 339
michael@0 340 // Flush the preferences to disk so they survive any crash
michael@0 341 Services.prefs.savePrefFile(null);
michael@0 342 }
michael@0 343 else {
michael@0 344 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false);
michael@0 345 this.themeChanged(theme);
michael@0 346 AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
michael@0 347 }
michael@0 348 _themeIDBeingEnabled = null;
michael@0 349 }
michael@0 350 },
michael@0 351
michael@0 352 /**
michael@0 353 * Called to get an Addon with a particular ID.
michael@0 354 *
michael@0 355 * @param aId
michael@0 356 * The ID of the add-on to retrieve
michael@0 357 * @param aCallback
michael@0 358 * A callback to pass the Addon to
michael@0 359 */
michael@0 360 getAddonByID: function LightweightThemeManager_getAddonByID(aId, aCallback) {
michael@0 361 let id = _getInternalID(aId);
michael@0 362 if (!id) {
michael@0 363 aCallback(null);
michael@0 364 return;
michael@0 365 }
michael@0 366
michael@0 367 let theme = this.getUsedTheme(id);
michael@0 368 if (!theme) {
michael@0 369 aCallback(null);
michael@0 370 return;
michael@0 371 }
michael@0 372
michael@0 373 aCallback(new AddonWrapper(theme));
michael@0 374 },
michael@0 375
michael@0 376 /**
michael@0 377 * Called to get Addons of a particular type.
michael@0 378 *
michael@0 379 * @param aTypes
michael@0 380 * An array of types to fetch. Can be null to get all types.
michael@0 381 * @param aCallback
michael@0 382 * A callback to pass an array of Addons to
michael@0 383 */
michael@0 384 getAddonsByTypes: function LightweightThemeManager_getAddonsByTypes(aTypes, aCallback) {
michael@0 385 if (aTypes && aTypes.indexOf(ADDON_TYPE) == -1) {
michael@0 386 aCallback([]);
michael@0 387 return;
michael@0 388 }
michael@0 389
michael@0 390 aCallback([new AddonWrapper(a) for each (a in this.usedThemes)]);
michael@0 391 },
michael@0 392 };
michael@0 393
michael@0 394 /**
michael@0 395 * The AddonWrapper wraps lightweight theme to provide the data visible to
michael@0 396 * consumers of the AddonManager API.
michael@0 397 */
michael@0 398 function AddonWrapper(aTheme) {
michael@0 399 this.__defineGetter__("id", function AddonWrapper_idGetter() aTheme.id + ID_SUFFIX);
michael@0 400 this.__defineGetter__("type", function AddonWrapper_typeGetter() ADDON_TYPE);
michael@0 401 this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
michael@0 402 let current = LightweightThemeManager.currentTheme;
michael@0 403 if (current)
michael@0 404 return aTheme.id == current.id;
michael@0 405 return false;
michael@0 406 });
michael@0 407
michael@0 408 this.__defineGetter__("name", function AddonWrapper_nameGetter() aTheme.name);
michael@0 409 this.__defineGetter__("version", function AddonWrapper_versionGetter() {
michael@0 410 return "version" in aTheme ? aTheme.version : "";
michael@0 411 });
michael@0 412
michael@0 413 ["description", "homepageURL", "iconURL"].forEach(function(prop) {
michael@0 414 this.__defineGetter__(prop, function AddonWrapper_optionalPropGetter() {
michael@0 415 return prop in aTheme ? aTheme[prop] : null;
michael@0 416 });
michael@0 417 }, this);
michael@0 418
michael@0 419 ["installDate", "updateDate"].forEach(function(prop) {
michael@0 420 this.__defineGetter__(prop, function AddonWrapper_datePropGetter() {
michael@0 421 return prop in aTheme ? new Date(aTheme[prop]) : null;
michael@0 422 });
michael@0 423 }, this);
michael@0 424
michael@0 425 this.__defineGetter__("creator", function AddonWrapper_creatorGetter() {
michael@0 426 return new AddonManagerPrivate.AddonAuthor(aTheme.author);
michael@0 427 });
michael@0 428
michael@0 429 this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() {
michael@0 430 let url = aTheme.previewURL;
michael@0 431 return [new AddonManagerPrivate.AddonScreenshot(url)];
michael@0 432 });
michael@0 433
michael@0 434 this.__defineGetter__("pendingOperations",
michael@0 435 function AddonWrapper_pendingOperationsGetter() {
michael@0 436 let pending = AddonManager.PENDING_NONE;
michael@0 437 if (this.isActive == this.userDisabled)
michael@0 438 pending |= this.isActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE;
michael@0 439 return pending;
michael@0 440 });
michael@0 441
michael@0 442 this.__defineGetter__("operationsRequiringRestart",
michael@0 443 function AddonWrapper_operationsRequiringRestartGetter() {
michael@0 444 // If a non-default theme is in use then a restart will be required to
michael@0 445 // enable lightweight themes unless dynamic theme switching is enabled
michael@0 446 if (Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) {
michael@0 447 try {
michael@0 448 if (Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED))
michael@0 449 return AddonManager.OP_NEEDS_RESTART_NONE;
michael@0 450 }
michael@0 451 catch (e) {
michael@0 452 }
michael@0 453 return AddonManager.OP_NEEDS_RESTART_ENABLE;
michael@0 454 }
michael@0 455
michael@0 456 return AddonManager.OP_NEEDS_RESTART_NONE;
michael@0 457 });
michael@0 458
michael@0 459 this.__defineGetter__("size", function AddonWrapper_sizeGetter() {
michael@0 460 // The size changes depending on whether the theme is in use or not, this is
michael@0 461 // probably not worth exposing.
michael@0 462 return null;
michael@0 463 });
michael@0 464
michael@0 465 this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() {
michael@0 466 let permissions = AddonManager.PERM_CAN_UNINSTALL;
michael@0 467 if (this.userDisabled)
michael@0 468 permissions |= AddonManager.PERM_CAN_ENABLE;
michael@0 469 else
michael@0 470 permissions |= AddonManager.PERM_CAN_DISABLE;
michael@0 471 return permissions;
michael@0 472 });
michael@0 473
michael@0 474 this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
michael@0 475 if (_themeIDBeingEnabled == aTheme.id)
michael@0 476 return false;
michael@0 477 if (_themeIDBeingDisabled == aTheme.id)
michael@0 478 return true;
michael@0 479
michael@0 480 try {
michael@0 481 let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
michael@0 482 return aTheme.id != toSelect;
michael@0 483 }
michael@0 484 catch (e) {
michael@0 485 let current = LightweightThemeManager.currentTheme;
michael@0 486 return !current || current.id != aTheme.id;
michael@0 487 }
michael@0 488 });
michael@0 489
michael@0 490 this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) {
michael@0 491 if (val == this.userDisabled)
michael@0 492 return val;
michael@0 493
michael@0 494 if (val)
michael@0 495 LightweightThemeManager.currentTheme = null;
michael@0 496 else
michael@0 497 LightweightThemeManager.currentTheme = aTheme;
michael@0 498
michael@0 499 return val;
michael@0 500 });
michael@0 501
michael@0 502 this.uninstall = function AddonWrapper_uninstall() {
michael@0 503 LightweightThemeManager.forgetUsedTheme(aTheme.id);
michael@0 504 };
michael@0 505
michael@0 506 this.cancelUninstall = function AddonWrapper_cancelUninstall() {
michael@0 507 throw new Error("Theme is not marked to be uninstalled");
michael@0 508 };
michael@0 509
michael@0 510 this.findUpdates = function AddonWrapper_findUpdates(listener, reason, appVersion, platformVersion) {
michael@0 511 AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion);
michael@0 512 };
michael@0 513 }
michael@0 514
michael@0 515 AddonWrapper.prototype = {
michael@0 516 // Lightweight themes are never disabled by the application
michael@0 517 get appDisabled() {
michael@0 518 return false;
michael@0 519 },
michael@0 520
michael@0 521 // Lightweight themes are always compatible
michael@0 522 get isCompatible() {
michael@0 523 return true;
michael@0 524 },
michael@0 525
michael@0 526 get isPlatformCompatible() {
michael@0 527 return true;
michael@0 528 },
michael@0 529
michael@0 530 get scope() {
michael@0 531 return AddonManager.SCOPE_PROFILE;
michael@0 532 },
michael@0 533
michael@0 534 get foreignInstall() {
michael@0 535 return false;
michael@0 536 },
michael@0 537
michael@0 538 // Lightweight themes are always compatible
michael@0 539 isCompatibleWith: function AddonWrapper_isCompatibleWith(appVersion, platformVersion) {
michael@0 540 return true;
michael@0 541 },
michael@0 542
michael@0 543 // Lightweight themes are always securely updated
michael@0 544 get providesUpdatesSecurely() {
michael@0 545 return true;
michael@0 546 },
michael@0 547
michael@0 548 // Lightweight themes are never blocklisted
michael@0 549 get blocklistState() {
michael@0 550 return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
michael@0 551 }
michael@0 552 };
michael@0 553
michael@0 554 /**
michael@0 555 * Converts the ID used by the public AddonManager API to an lightweight theme
michael@0 556 * ID.
michael@0 557 *
michael@0 558 * @param id
michael@0 559 * The ID to be converted
michael@0 560 *
michael@0 561 * @return the lightweight theme ID or null if the ID was not for a lightweight
michael@0 562 * theme.
michael@0 563 */
michael@0 564 function _getInternalID(id) {
michael@0 565 if (!id)
michael@0 566 return null;
michael@0 567 let len = id.length - ID_SUFFIX.length;
michael@0 568 if (len > 0 && id.substring(len) == ID_SUFFIX)
michael@0 569 return id.substring(0, len);
michael@0 570 return null;
michael@0 571 }
michael@0 572
michael@0 573 function _setCurrentTheme(aData, aLocal) {
michael@0 574 aData = _sanitizeTheme(aData, null, aLocal);
michael@0 575
michael@0 576 let needsRestart = (ADDON_TYPE == "theme") &&
michael@0 577 Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN);
michael@0 578
michael@0 579 let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
michael@0 580 cancel.data = false;
michael@0 581 Services.obs.notifyObservers(cancel, "lightweight-theme-change-requested",
michael@0 582 JSON.stringify(aData));
michael@0 583
michael@0 584 if (aData) {
michael@0 585 let theme = LightweightThemeManager.getUsedTheme(aData.id);
michael@0 586 let isInstall = !theme || theme.version != aData.version;
michael@0 587 if (isInstall) {
michael@0 588 aData.updateDate = Date.now();
michael@0 589 if (theme && "installDate" in theme)
michael@0 590 aData.installDate = theme.installDate;
michael@0 591 else
michael@0 592 aData.installDate = aData.updateDate;
michael@0 593
michael@0 594 var oldWrapper = theme ? new AddonWrapper(theme) : null;
michael@0 595 var wrapper = new AddonWrapper(aData);
michael@0 596 AddonManagerPrivate.callInstallListeners("onExternalInstall", null,
michael@0 597 wrapper, oldWrapper, false);
michael@0 598 AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false);
michael@0 599 }
michael@0 600
michael@0 601 let current = LightweightThemeManager.currentTheme;
michael@0 602 let usedThemes = _usedThemesExceptId(aData.id);
michael@0 603 if (current && current.id != aData.id)
michael@0 604 usedThemes.splice(1, 0, aData);
michael@0 605 else
michael@0 606 usedThemes.unshift(aData);
michael@0 607 _updateUsedThemes(usedThemes);
michael@0 608
michael@0 609 if (isInstall)
michael@0 610 AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
michael@0 611 }
michael@0 612
michael@0 613 if (cancel.data)
michael@0 614 return null;
michael@0 615
michael@0 616 AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null,
michael@0 617 ADDON_TYPE, needsRestart);
michael@0 618
michael@0 619 return LightweightThemeManager.currentTheme;
michael@0 620 }
michael@0 621
michael@0 622 function _sanitizeTheme(aData, aBaseURI, aLocal) {
michael@0 623 if (!aData || typeof aData != "object")
michael@0 624 return null;
michael@0 625
michael@0 626 var resourceProtocols = ["http", "https"];
michael@0 627 if (aLocal)
michael@0 628 resourceProtocols.push("file");
michael@0 629 var resourceProtocolExp = new RegExp("^(" + resourceProtocols.join("|") + "):");
michael@0 630
michael@0 631 function sanitizeProperty(prop) {
michael@0 632 if (!(prop in aData))
michael@0 633 return null;
michael@0 634 if (typeof aData[prop] != "string")
michael@0 635 return null;
michael@0 636 let val = aData[prop].trim();
michael@0 637 if (!val)
michael@0 638 return null;
michael@0 639
michael@0 640 if (!/URL$/.test(prop))
michael@0 641 return val;
michael@0 642
michael@0 643 try {
michael@0 644 val = _makeURI(val, aBaseURI ? _makeURI(aBaseURI) : null).spec;
michael@0 645 if ((prop == "updateURL" ? /^https:/ : resourceProtocolExp).test(val))
michael@0 646 return val;
michael@0 647 return null;
michael@0 648 }
michael@0 649 catch (e) {
michael@0 650 return null;
michael@0 651 }
michael@0 652 }
michael@0 653
michael@0 654 let result = {};
michael@0 655 for (let mandatoryProperty of MANDATORY) {
michael@0 656 let val = sanitizeProperty(mandatoryProperty);
michael@0 657 if (!val)
michael@0 658 throw Components.results.NS_ERROR_INVALID_ARG;
michael@0 659 result[mandatoryProperty] = val;
michael@0 660 }
michael@0 661
michael@0 662 for (let optionalProperty of OPTIONAL) {
michael@0 663 let val = sanitizeProperty(optionalProperty);
michael@0 664 if (!val)
michael@0 665 continue;
michael@0 666 result[optionalProperty] = val;
michael@0 667 }
michael@0 668
michael@0 669 return result;
michael@0 670 }
michael@0 671
michael@0 672 function _usedThemesExceptId(aId)
michael@0 673 LightweightThemeManager.usedThemes.filter(
michael@0 674 function usedThemesExceptId_filterID(t) "id" in t && t.id != aId);
michael@0 675
michael@0 676 function _version(aThemeData)
michael@0 677 aThemeData.version || "";
michael@0 678
michael@0 679 function _makeURI(aURL, aBaseURI)
michael@0 680 Services.io.newURI(aURL, null, aBaseURI);
michael@0 681
michael@0 682 function _updateUsedThemes(aList) {
michael@0 683 // Send uninstall events for all themes that need to be removed.
michael@0 684 while (aList.length > _maxUsedThemes) {
michael@0 685 let wrapper = new AddonWrapper(aList[aList.length - 1]);
michael@0 686 AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
michael@0 687 aList.pop();
michael@0 688 AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
michael@0 689 }
michael@0 690
michael@0 691 var str = Cc["@mozilla.org/supports-string;1"]
michael@0 692 .createInstance(Ci.nsISupportsString);
michael@0 693 str.data = JSON.stringify(aList);
michael@0 694 _prefs.setComplexValue("usedThemes", Ci.nsISupportsString, str);
michael@0 695
michael@0 696 Services.obs.notifyObservers(null, "lightweight-theme-list-changed", null);
michael@0 697 }
michael@0 698
michael@0 699 function _notifyWindows(aThemeData) {
michael@0 700 Services.obs.notifyObservers(null, "lightweight-theme-styling-update",
michael@0 701 JSON.stringify(aThemeData));
michael@0 702 }
michael@0 703
michael@0 704 var _previewTimer;
michael@0 705 var _previewTimerCallback = {
michael@0 706 notify: function _previewTimerCallback_notify() {
michael@0 707 LightweightThemeManager.resetPreview();
michael@0 708 }
michael@0 709 };
michael@0 710
michael@0 711 /**
michael@0 712 * Called when any of the lightweightThemes preferences are changed.
michael@0 713 */
michael@0 714 function _prefObserver(aSubject, aTopic, aData) {
michael@0 715 switch (aData) {
michael@0 716 case "maxUsedThemes":
michael@0 717 try {
michael@0 718 _maxUsedThemes = _prefs.getIntPref(aData);
michael@0 719 }
michael@0 720 catch (e) {
michael@0 721 _maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT;
michael@0 722 }
michael@0 723 // Update the theme list to remove any themes over the number we keep
michael@0 724 _updateUsedThemes(LightweightThemeManager.usedThemes);
michael@0 725 break;
michael@0 726 }
michael@0 727 }
michael@0 728
michael@0 729 function _persistImages(aData, aCallback) {
michael@0 730 function onSuccess(key) function () {
michael@0 731 let current = LightweightThemeManager.currentTheme;
michael@0 732 if (current && current.id == aData.id) {
michael@0 733 _prefs.setBoolPref("persisted." + key, true);
michael@0 734 }
michael@0 735 if (--numFilesToPersist == 0 && aCallback) {
michael@0 736 aCallback();
michael@0 737 }
michael@0 738 };
michael@0 739
michael@0 740 let numFilesToPersist = 0;
michael@0 741 for (let key in PERSIST_FILES) {
michael@0 742 _prefs.setBoolPref("persisted." + key, false);
michael@0 743 if (aData[key]) {
michael@0 744 numFilesToPersist++;
michael@0 745 _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key));
michael@0 746 }
michael@0 747 }
michael@0 748 }
michael@0 749
michael@0 750 function _getLocalImageURI(localFileName) {
michael@0 751 var localFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
michael@0 752 localFile.append(localFileName);
michael@0 753 return Services.io.newFileURI(localFile);
michael@0 754 }
michael@0 755
michael@0 756 function _persistImage(sourceURL, localFileName, successCallback) {
michael@0 757 if (/^file:/.test(sourceURL))
michael@0 758 return;
michael@0 759
michael@0 760 var targetURI = _getLocalImageURI(localFileName);
michael@0 761 var sourceURI = _makeURI(sourceURL);
michael@0 762
michael@0 763 var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
michael@0 764 .createInstance(Ci.nsIWebBrowserPersist);
michael@0 765
michael@0 766 persist.persistFlags =
michael@0 767 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
michael@0 768 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION |
michael@0 769 (PERSIST_BYPASS_CACHE ?
michael@0 770 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE :
michael@0 771 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE);
michael@0 772
michael@0 773 persist.progressListener = new _persistProgressListener(successCallback);
michael@0 774
michael@0 775 persist.saveURI(sourceURI, null, null, null, null, targetURI, null);
michael@0 776 }
michael@0 777
michael@0 778 function _persistProgressListener(successCallback) {
michael@0 779 this.onLocationChange = function persistProgressListener_onLocationChange() {};
michael@0 780 this.onProgressChange = function persistProgressListener_onProgressChange() {};
michael@0 781 this.onStatusChange = function persistProgressListener_onStatusChange() {};
michael@0 782 this.onSecurityChange = function persistProgressListener_onSecurityChange() {};
michael@0 783 this.onStateChange = function persistProgressListener_onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
michael@0 784 if (aRequest &&
michael@0 785 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
michael@0 786 aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
michael@0 787 try {
michael@0 788 if (aRequest.QueryInterface(Ci.nsIHttpChannel).requestSucceeded) {
michael@0 789 // success
michael@0 790 successCallback();
michael@0 791 return;
michael@0 792 }
michael@0 793 } catch (e) { }
michael@0 794 // failure
michael@0 795 }
michael@0 796 };
michael@0 797 }
michael@0 798
michael@0 799 AddonManagerPrivate.registerProvider(LightweightThemeManager, [
michael@0 800 new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
michael@0 801 STRING_TYPE_NAME,
michael@0 802 AddonManager.VIEW_TYPE_LIST, 5000)
michael@0 803 ]);

mercurial