1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,803 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +this.EXPORTED_SYMBOLS = ["LightweightThemeManager"]; 1.11 + 1.12 +const Cc = Components.classes; 1.13 +const Ci = Components.interfaces; 1.14 + 1.15 +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); 1.16 +Components.utils.import("resource://gre/modules/AddonManager.jsm"); 1.17 +Components.utils.import("resource://gre/modules/Services.jsm"); 1.18 + 1.19 +const ID_SUFFIX = "@personas.mozilla.org"; 1.20 +const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect"; 1.21 +const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; 1.22 +const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; 1.23 +const ADDON_TYPE = "theme"; 1.24 + 1.25 +const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; 1.26 + 1.27 +const STRING_TYPE_NAME = "type.%ID%.name"; 1.28 + 1.29 +const DEFAULT_MAX_USED_THEMES_COUNT = 30; 1.30 + 1.31 +const MAX_PREVIEW_SECONDS = 30; 1.32 + 1.33 +const MANDATORY = ["id", "name", "headerURL"]; 1.34 +const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL", 1.35 + "previewURL", "author", "description", "homepageURL", 1.36 + "updateURL", "version"]; 1.37 + 1.38 +const PERSIST_ENABLED = true; 1.39 +const PERSIST_BYPASS_CACHE = false; 1.40 +const PERSIST_FILES = { 1.41 + headerURL: "lightweighttheme-header", 1.42 + footerURL: "lightweighttheme-footer" 1.43 +}; 1.44 + 1.45 +XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer", 1.46 + "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm"); 1.47 + 1.48 +this.__defineGetter__("_prefs", function prefsGetter() { 1.49 + delete this._prefs; 1.50 + return this._prefs = Services.prefs.getBranch("lightweightThemes."); 1.51 +}); 1.52 + 1.53 +this.__defineGetter__("_maxUsedThemes", function maxUsedThemesGetter() { 1.54 + delete this._maxUsedThemes; 1.55 + try { 1.56 + this._maxUsedThemes = _prefs.getIntPref("maxUsedThemes"); 1.57 + } 1.58 + catch (e) { 1.59 + this._maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT; 1.60 + } 1.61 + return this._maxUsedThemes; 1.62 +}); 1.63 + 1.64 +this.__defineSetter__("_maxUsedThemes", function maxUsedThemesSetter(aVal) { 1.65 + delete this._maxUsedThemes; 1.66 + return this._maxUsedThemes = aVal; 1.67 +}); 1.68 + 1.69 +// Holds the ID of the theme being enabled or disabled while sending out the 1.70 +// events so cached AddonWrapper instances can return correct values for 1.71 +// permissions and pendingOperations 1.72 +var _themeIDBeingEnabled = null; 1.73 +var _themeIDBeingDisabled = null; 1.74 + 1.75 +this.LightweightThemeManager = { 1.76 + get usedThemes () { 1.77 + try { 1.78 + return JSON.parse(_prefs.getComplexValue("usedThemes", 1.79 + Ci.nsISupportsString).data); 1.80 + } catch (e) { 1.81 + return []; 1.82 + } 1.83 + }, 1.84 + 1.85 + get currentTheme () { 1.86 + try { 1.87 + if (_prefs.getBoolPref("isThemeSelected")) 1.88 + var data = this.usedThemes[0]; 1.89 + } catch (e) {} 1.90 + 1.91 + return data || null; 1.92 + }, 1.93 + 1.94 + get currentThemeForDisplay () { 1.95 + var data = this.currentTheme; 1.96 + 1.97 + if (data && PERSIST_ENABLED) { 1.98 + for (let key in PERSIST_FILES) { 1.99 + try { 1.100 + if (data[key] && _prefs.getBoolPref("persisted." + key)) 1.101 + data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec 1.102 + + "?" + data.id + ";" + _version(data); 1.103 + } catch (e) {} 1.104 + } 1.105 + } 1.106 + 1.107 + return data; 1.108 + }, 1.109 + 1.110 + set currentTheme (aData) { 1.111 + return _setCurrentTheme(aData, false); 1.112 + }, 1.113 + 1.114 + setLocalTheme: function LightweightThemeManager_setLocalTheme(aData) { 1.115 + _setCurrentTheme(aData, true); 1.116 + }, 1.117 + 1.118 + getUsedTheme: function LightweightThemeManager_getUsedTheme(aId) { 1.119 + var usedThemes = this.usedThemes; 1.120 + for (let usedTheme of usedThemes) { 1.121 + if (usedTheme.id == aId) 1.122 + return usedTheme; 1.123 + } 1.124 + return null; 1.125 + }, 1.126 + 1.127 + forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) { 1.128 + let theme = this.getUsedTheme(aId); 1.129 + if (!theme) 1.130 + return; 1.131 + 1.132 + let wrapper = new AddonWrapper(theme); 1.133 + AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); 1.134 + 1.135 + var currentTheme = this.currentTheme; 1.136 + if (currentTheme && currentTheme.id == aId) { 1.137 + this.themeChanged(null); 1.138 + AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false); 1.139 + } 1.140 + 1.141 + _updateUsedThemes(_usedThemesExceptId(aId)); 1.142 + AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); 1.143 + }, 1.144 + 1.145 + previewTheme: function LightweightThemeManager_previewTheme(aData) { 1.146 + if (!aData) 1.147 + return; 1.148 + 1.149 + let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); 1.150 + cancel.data = false; 1.151 + Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested", 1.152 + JSON.stringify(aData)); 1.153 + if (cancel.data) 1.154 + return; 1.155 + 1.156 + if (_previewTimer) 1.157 + _previewTimer.cancel(); 1.158 + else 1.159 + _previewTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 1.160 + _previewTimer.initWithCallback(_previewTimerCallback, 1.161 + MAX_PREVIEW_SECONDS * 1000, 1.162 + _previewTimer.TYPE_ONE_SHOT); 1.163 + 1.164 + _notifyWindows(aData); 1.165 + }, 1.166 + 1.167 + resetPreview: function LightweightThemeManager_resetPreview() { 1.168 + if (_previewTimer) { 1.169 + _previewTimer.cancel(); 1.170 + _previewTimer = null; 1.171 + _notifyWindows(this.currentThemeForDisplay); 1.172 + } 1.173 + }, 1.174 + 1.175 + parseTheme: function LightweightThemeManager_parseTheme(aString, aBaseURI) { 1.176 + try { 1.177 + return _sanitizeTheme(JSON.parse(aString), aBaseURI, false); 1.178 + } catch (e) { 1.179 + return null; 1.180 + } 1.181 + }, 1.182 + 1.183 + updateCurrentTheme: function LightweightThemeManager_updateCurrentTheme() { 1.184 + try { 1.185 + if (!_prefs.getBoolPref("update.enabled")) 1.186 + return; 1.187 + } catch (e) { 1.188 + return; 1.189 + } 1.190 + 1.191 + var theme = this.currentTheme; 1.192 + if (!theme || !theme.updateURL) 1.193 + return; 1.194 + 1.195 + var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] 1.196 + .createInstance(Ci.nsIXMLHttpRequest); 1.197 + 1.198 + req.mozBackgroundRequest = true; 1.199 + req.overrideMimeType("text/plain"); 1.200 + req.open("GET", theme.updateURL, true); 1.201 + // Prevent the request from reading from the cache. 1.202 + req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; 1.203 + // Prevent the request from writing to the cache. 1.204 + req.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; 1.205 + 1.206 + var self = this; 1.207 + req.addEventListener("load", function loadEventListener() { 1.208 + if (req.status != 200) 1.209 + return; 1.210 + 1.211 + let newData = self.parseTheme(req.responseText, theme.updateURL); 1.212 + if (!newData || 1.213 + newData.id != theme.id || 1.214 + _version(newData) == _version(theme)) 1.215 + return; 1.216 + 1.217 + var currentTheme = self.currentTheme; 1.218 + if (currentTheme && currentTheme.id == theme.id) 1.219 + self.currentTheme = newData; 1.220 + }, false); 1.221 + 1.222 + req.send(null); 1.223 + }, 1.224 + 1.225 + /** 1.226 + * Switches to a new lightweight theme. 1.227 + * 1.228 + * @param aData 1.229 + * The lightweight theme to switch to 1.230 + */ 1.231 + themeChanged: function LightweightThemeManager_themeChanged(aData) { 1.232 + if (_previewTimer) { 1.233 + _previewTimer.cancel(); 1.234 + _previewTimer = null; 1.235 + } 1.236 + 1.237 + if (aData) { 1.238 + let usedThemes = _usedThemesExceptId(aData.id); 1.239 + usedThemes.unshift(aData); 1.240 + _updateUsedThemes(usedThemes); 1.241 + if (PERSIST_ENABLED) { 1.242 + LightweightThemeImageOptimizer.purge(); 1.243 + _persistImages(aData, function themeChanged_persistImages() { 1.244 + _notifyWindows(this.currentThemeForDisplay); 1.245 + }.bind(this)); 1.246 + } 1.247 + } 1.248 + 1.249 + _prefs.setBoolPref("isThemeSelected", aData != null); 1.250 + _notifyWindows(aData); 1.251 + Services.obs.notifyObservers(null, "lightweight-theme-changed", null); 1.252 + }, 1.253 + 1.254 + /** 1.255 + * Starts the Addons provider and enables the new lightweight theme if 1.256 + * necessary. 1.257 + */ 1.258 + startup: function LightweightThemeManager_startup() { 1.259 + if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) { 1.260 + let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); 1.261 + if (id) 1.262 + this.themeChanged(this.getUsedTheme(id)); 1.263 + else 1.264 + this.themeChanged(null); 1.265 + Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); 1.266 + } 1.267 + 1.268 + _prefs.addObserver("", _prefObserver, false); 1.269 + }, 1.270 + 1.271 + /** 1.272 + * Shuts down the provider. 1.273 + */ 1.274 + shutdown: function LightweightThemeManager_shutdown() { 1.275 + _prefs.removeObserver("", _prefObserver); 1.276 + }, 1.277 + 1.278 + /** 1.279 + * Called when a new add-on has been enabled when only one add-on of that type 1.280 + * can be enabled. 1.281 + * 1.282 + * @param aId 1.283 + * The ID of the newly enabled add-on 1.284 + * @param aType 1.285 + * The type of the newly enabled add-on 1.286 + * @param aPendingRestart 1.287 + * true if the newly enabled add-on will only become enabled after a 1.288 + * restart 1.289 + */ 1.290 + addonChanged: function LightweightThemeManager_addonChanged(aId, aType, aPendingRestart) { 1.291 + if (aType != ADDON_TYPE) 1.292 + return; 1.293 + 1.294 + let id = _getInternalID(aId); 1.295 + let current = this.currentTheme; 1.296 + 1.297 + try { 1.298 + let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); 1.299 + if (id == next && aPendingRestart) 1.300 + return; 1.301 + 1.302 + Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); 1.303 + if (next) { 1.304 + AddonManagerPrivate.callAddonListeners("onOperationCancelled", 1.305 + new AddonWrapper(this.getUsedTheme(next))); 1.306 + } 1.307 + else { 1.308 + if (id == current.id) { 1.309 + AddonManagerPrivate.callAddonListeners("onOperationCancelled", 1.310 + new AddonWrapper(current)); 1.311 + return; 1.312 + } 1.313 + } 1.314 + } 1.315 + catch (e) { 1.316 + } 1.317 + 1.318 + if (current) { 1.319 + if (current.id == id) 1.320 + return; 1.321 + _themeIDBeingDisabled = current.id; 1.322 + let wrapper = new AddonWrapper(current); 1.323 + if (aPendingRestart) { 1.324 + Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, ""); 1.325 + AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true); 1.326 + } 1.327 + else { 1.328 + AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false); 1.329 + this.themeChanged(null); 1.330 + AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); 1.331 + } 1.332 + _themeIDBeingDisabled = null; 1.333 + } 1.334 + 1.335 + if (id) { 1.336 + let theme = this.getUsedTheme(id); 1.337 + _themeIDBeingEnabled = id; 1.338 + let wrapper = new AddonWrapper(theme); 1.339 + if (aPendingRestart) { 1.340 + AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true); 1.341 + Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id); 1.342 + 1.343 + // Flush the preferences to disk so they survive any crash 1.344 + Services.prefs.savePrefFile(null); 1.345 + } 1.346 + else { 1.347 + AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false); 1.348 + this.themeChanged(theme); 1.349 + AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); 1.350 + } 1.351 + _themeIDBeingEnabled = null; 1.352 + } 1.353 + }, 1.354 + 1.355 + /** 1.356 + * Called to get an Addon with a particular ID. 1.357 + * 1.358 + * @param aId 1.359 + * The ID of the add-on to retrieve 1.360 + * @param aCallback 1.361 + * A callback to pass the Addon to 1.362 + */ 1.363 + getAddonByID: function LightweightThemeManager_getAddonByID(aId, aCallback) { 1.364 + let id = _getInternalID(aId); 1.365 + if (!id) { 1.366 + aCallback(null); 1.367 + return; 1.368 + } 1.369 + 1.370 + let theme = this.getUsedTheme(id); 1.371 + if (!theme) { 1.372 + aCallback(null); 1.373 + return; 1.374 + } 1.375 + 1.376 + aCallback(new AddonWrapper(theme)); 1.377 + }, 1.378 + 1.379 + /** 1.380 + * Called to get Addons of a particular type. 1.381 + * 1.382 + * @param aTypes 1.383 + * An array of types to fetch. Can be null to get all types. 1.384 + * @param aCallback 1.385 + * A callback to pass an array of Addons to 1.386 + */ 1.387 + getAddonsByTypes: function LightweightThemeManager_getAddonsByTypes(aTypes, aCallback) { 1.388 + if (aTypes && aTypes.indexOf(ADDON_TYPE) == -1) { 1.389 + aCallback([]); 1.390 + return; 1.391 + } 1.392 + 1.393 + aCallback([new AddonWrapper(a) for each (a in this.usedThemes)]); 1.394 + }, 1.395 +}; 1.396 + 1.397 +/** 1.398 + * The AddonWrapper wraps lightweight theme to provide the data visible to 1.399 + * consumers of the AddonManager API. 1.400 + */ 1.401 +function AddonWrapper(aTheme) { 1.402 + this.__defineGetter__("id", function AddonWrapper_idGetter() aTheme.id + ID_SUFFIX); 1.403 + this.__defineGetter__("type", function AddonWrapper_typeGetter() ADDON_TYPE); 1.404 + this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() { 1.405 + let current = LightweightThemeManager.currentTheme; 1.406 + if (current) 1.407 + return aTheme.id == current.id; 1.408 + return false; 1.409 + }); 1.410 + 1.411 + this.__defineGetter__("name", function AddonWrapper_nameGetter() aTheme.name); 1.412 + this.__defineGetter__("version", function AddonWrapper_versionGetter() { 1.413 + return "version" in aTheme ? aTheme.version : ""; 1.414 + }); 1.415 + 1.416 + ["description", "homepageURL", "iconURL"].forEach(function(prop) { 1.417 + this.__defineGetter__(prop, function AddonWrapper_optionalPropGetter() { 1.418 + return prop in aTheme ? aTheme[prop] : null; 1.419 + }); 1.420 + }, this); 1.421 + 1.422 + ["installDate", "updateDate"].forEach(function(prop) { 1.423 + this.__defineGetter__(prop, function AddonWrapper_datePropGetter() { 1.424 + return prop in aTheme ? new Date(aTheme[prop]) : null; 1.425 + }); 1.426 + }, this); 1.427 + 1.428 + this.__defineGetter__("creator", function AddonWrapper_creatorGetter() { 1.429 + return new AddonManagerPrivate.AddonAuthor(aTheme.author); 1.430 + }); 1.431 + 1.432 + this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() { 1.433 + let url = aTheme.previewURL; 1.434 + return [new AddonManagerPrivate.AddonScreenshot(url)]; 1.435 + }); 1.436 + 1.437 + this.__defineGetter__("pendingOperations", 1.438 + function AddonWrapper_pendingOperationsGetter() { 1.439 + let pending = AddonManager.PENDING_NONE; 1.440 + if (this.isActive == this.userDisabled) 1.441 + pending |= this.isActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE; 1.442 + return pending; 1.443 + }); 1.444 + 1.445 + this.__defineGetter__("operationsRequiringRestart", 1.446 + function AddonWrapper_operationsRequiringRestartGetter() { 1.447 + // If a non-default theme is in use then a restart will be required to 1.448 + // enable lightweight themes unless dynamic theme switching is enabled 1.449 + if (Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) { 1.450 + try { 1.451 + if (Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED)) 1.452 + return AddonManager.OP_NEEDS_RESTART_NONE; 1.453 + } 1.454 + catch (e) { 1.455 + } 1.456 + return AddonManager.OP_NEEDS_RESTART_ENABLE; 1.457 + } 1.458 + 1.459 + return AddonManager.OP_NEEDS_RESTART_NONE; 1.460 + }); 1.461 + 1.462 + this.__defineGetter__("size", function AddonWrapper_sizeGetter() { 1.463 + // The size changes depending on whether the theme is in use or not, this is 1.464 + // probably not worth exposing. 1.465 + return null; 1.466 + }); 1.467 + 1.468 + this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() { 1.469 + let permissions = AddonManager.PERM_CAN_UNINSTALL; 1.470 + if (this.userDisabled) 1.471 + permissions |= AddonManager.PERM_CAN_ENABLE; 1.472 + else 1.473 + permissions |= AddonManager.PERM_CAN_DISABLE; 1.474 + return permissions; 1.475 + }); 1.476 + 1.477 + this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() { 1.478 + if (_themeIDBeingEnabled == aTheme.id) 1.479 + return false; 1.480 + if (_themeIDBeingDisabled == aTheme.id) 1.481 + return true; 1.482 + 1.483 + try { 1.484 + let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); 1.485 + return aTheme.id != toSelect; 1.486 + } 1.487 + catch (e) { 1.488 + let current = LightweightThemeManager.currentTheme; 1.489 + return !current || current.id != aTheme.id; 1.490 + } 1.491 + }); 1.492 + 1.493 + this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) { 1.494 + if (val == this.userDisabled) 1.495 + return val; 1.496 + 1.497 + if (val) 1.498 + LightweightThemeManager.currentTheme = null; 1.499 + else 1.500 + LightweightThemeManager.currentTheme = aTheme; 1.501 + 1.502 + return val; 1.503 + }); 1.504 + 1.505 + this.uninstall = function AddonWrapper_uninstall() { 1.506 + LightweightThemeManager.forgetUsedTheme(aTheme.id); 1.507 + }; 1.508 + 1.509 + this.cancelUninstall = function AddonWrapper_cancelUninstall() { 1.510 + throw new Error("Theme is not marked to be uninstalled"); 1.511 + }; 1.512 + 1.513 + this.findUpdates = function AddonWrapper_findUpdates(listener, reason, appVersion, platformVersion) { 1.514 + AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion); 1.515 + }; 1.516 +} 1.517 + 1.518 +AddonWrapper.prototype = { 1.519 + // Lightweight themes are never disabled by the application 1.520 + get appDisabled() { 1.521 + return false; 1.522 + }, 1.523 + 1.524 + // Lightweight themes are always compatible 1.525 + get isCompatible() { 1.526 + return true; 1.527 + }, 1.528 + 1.529 + get isPlatformCompatible() { 1.530 + return true; 1.531 + }, 1.532 + 1.533 + get scope() { 1.534 + return AddonManager.SCOPE_PROFILE; 1.535 + }, 1.536 + 1.537 + get foreignInstall() { 1.538 + return false; 1.539 + }, 1.540 + 1.541 + // Lightweight themes are always compatible 1.542 + isCompatibleWith: function AddonWrapper_isCompatibleWith(appVersion, platformVersion) { 1.543 + return true; 1.544 + }, 1.545 + 1.546 + // Lightweight themes are always securely updated 1.547 + get providesUpdatesSecurely() { 1.548 + return true; 1.549 + }, 1.550 + 1.551 + // Lightweight themes are never blocklisted 1.552 + get blocklistState() { 1.553 + return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; 1.554 + } 1.555 +}; 1.556 + 1.557 +/** 1.558 + * Converts the ID used by the public AddonManager API to an lightweight theme 1.559 + * ID. 1.560 + * 1.561 + * @param id 1.562 + * The ID to be converted 1.563 + * 1.564 + * @return the lightweight theme ID or null if the ID was not for a lightweight 1.565 + * theme. 1.566 + */ 1.567 +function _getInternalID(id) { 1.568 + if (!id) 1.569 + return null; 1.570 + let len = id.length - ID_SUFFIX.length; 1.571 + if (len > 0 && id.substring(len) == ID_SUFFIX) 1.572 + return id.substring(0, len); 1.573 + return null; 1.574 +} 1.575 + 1.576 +function _setCurrentTheme(aData, aLocal) { 1.577 + aData = _sanitizeTheme(aData, null, aLocal); 1.578 + 1.579 + let needsRestart = (ADDON_TYPE == "theme") && 1.580 + Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN); 1.581 + 1.582 + let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); 1.583 + cancel.data = false; 1.584 + Services.obs.notifyObservers(cancel, "lightweight-theme-change-requested", 1.585 + JSON.stringify(aData)); 1.586 + 1.587 + if (aData) { 1.588 + let theme = LightweightThemeManager.getUsedTheme(aData.id); 1.589 + let isInstall = !theme || theme.version != aData.version; 1.590 + if (isInstall) { 1.591 + aData.updateDate = Date.now(); 1.592 + if (theme && "installDate" in theme) 1.593 + aData.installDate = theme.installDate; 1.594 + else 1.595 + aData.installDate = aData.updateDate; 1.596 + 1.597 + var oldWrapper = theme ? new AddonWrapper(theme) : null; 1.598 + var wrapper = new AddonWrapper(aData); 1.599 + AddonManagerPrivate.callInstallListeners("onExternalInstall", null, 1.600 + wrapper, oldWrapper, false); 1.601 + AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false); 1.602 + } 1.603 + 1.604 + let current = LightweightThemeManager.currentTheme; 1.605 + let usedThemes = _usedThemesExceptId(aData.id); 1.606 + if (current && current.id != aData.id) 1.607 + usedThemes.splice(1, 0, aData); 1.608 + else 1.609 + usedThemes.unshift(aData); 1.610 + _updateUsedThemes(usedThemes); 1.611 + 1.612 + if (isInstall) 1.613 + AddonManagerPrivate.callAddonListeners("onInstalled", wrapper); 1.614 + } 1.615 + 1.616 + if (cancel.data) 1.617 + return null; 1.618 + 1.619 + AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null, 1.620 + ADDON_TYPE, needsRestart); 1.621 + 1.622 + return LightweightThemeManager.currentTheme; 1.623 +} 1.624 + 1.625 +function _sanitizeTheme(aData, aBaseURI, aLocal) { 1.626 + if (!aData || typeof aData != "object") 1.627 + return null; 1.628 + 1.629 + var resourceProtocols = ["http", "https"]; 1.630 + if (aLocal) 1.631 + resourceProtocols.push("file"); 1.632 + var resourceProtocolExp = new RegExp("^(" + resourceProtocols.join("|") + "):"); 1.633 + 1.634 + function sanitizeProperty(prop) { 1.635 + if (!(prop in aData)) 1.636 + return null; 1.637 + if (typeof aData[prop] != "string") 1.638 + return null; 1.639 + let val = aData[prop].trim(); 1.640 + if (!val) 1.641 + return null; 1.642 + 1.643 + if (!/URL$/.test(prop)) 1.644 + return val; 1.645 + 1.646 + try { 1.647 + val = _makeURI(val, aBaseURI ? _makeURI(aBaseURI) : null).spec; 1.648 + if ((prop == "updateURL" ? /^https:/ : resourceProtocolExp).test(val)) 1.649 + return val; 1.650 + return null; 1.651 + } 1.652 + catch (e) { 1.653 + return null; 1.654 + } 1.655 + } 1.656 + 1.657 + let result = {}; 1.658 + for (let mandatoryProperty of MANDATORY) { 1.659 + let val = sanitizeProperty(mandatoryProperty); 1.660 + if (!val) 1.661 + throw Components.results.NS_ERROR_INVALID_ARG; 1.662 + result[mandatoryProperty] = val; 1.663 + } 1.664 + 1.665 + for (let optionalProperty of OPTIONAL) { 1.666 + let val = sanitizeProperty(optionalProperty); 1.667 + if (!val) 1.668 + continue; 1.669 + result[optionalProperty] = val; 1.670 + } 1.671 + 1.672 + return result; 1.673 +} 1.674 + 1.675 +function _usedThemesExceptId(aId) 1.676 + LightweightThemeManager.usedThemes.filter( 1.677 + function usedThemesExceptId_filterID(t) "id" in t && t.id != aId); 1.678 + 1.679 +function _version(aThemeData) 1.680 + aThemeData.version || ""; 1.681 + 1.682 +function _makeURI(aURL, aBaseURI) 1.683 + Services.io.newURI(aURL, null, aBaseURI); 1.684 + 1.685 +function _updateUsedThemes(aList) { 1.686 + // Send uninstall events for all themes that need to be removed. 1.687 + while (aList.length > _maxUsedThemes) { 1.688 + let wrapper = new AddonWrapper(aList[aList.length - 1]); 1.689 + AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); 1.690 + aList.pop(); 1.691 + AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); 1.692 + } 1.693 + 1.694 + var str = Cc["@mozilla.org/supports-string;1"] 1.695 + .createInstance(Ci.nsISupportsString); 1.696 + str.data = JSON.stringify(aList); 1.697 + _prefs.setComplexValue("usedThemes", Ci.nsISupportsString, str); 1.698 + 1.699 + Services.obs.notifyObservers(null, "lightweight-theme-list-changed", null); 1.700 +} 1.701 + 1.702 +function _notifyWindows(aThemeData) { 1.703 + Services.obs.notifyObservers(null, "lightweight-theme-styling-update", 1.704 + JSON.stringify(aThemeData)); 1.705 +} 1.706 + 1.707 +var _previewTimer; 1.708 +var _previewTimerCallback = { 1.709 + notify: function _previewTimerCallback_notify() { 1.710 + LightweightThemeManager.resetPreview(); 1.711 + } 1.712 +}; 1.713 + 1.714 +/** 1.715 + * Called when any of the lightweightThemes preferences are changed. 1.716 + */ 1.717 +function _prefObserver(aSubject, aTopic, aData) { 1.718 + switch (aData) { 1.719 + case "maxUsedThemes": 1.720 + try { 1.721 + _maxUsedThemes = _prefs.getIntPref(aData); 1.722 + } 1.723 + catch (e) { 1.724 + _maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT; 1.725 + } 1.726 + // Update the theme list to remove any themes over the number we keep 1.727 + _updateUsedThemes(LightweightThemeManager.usedThemes); 1.728 + break; 1.729 + } 1.730 +} 1.731 + 1.732 +function _persistImages(aData, aCallback) { 1.733 + function onSuccess(key) function () { 1.734 + let current = LightweightThemeManager.currentTheme; 1.735 + if (current && current.id == aData.id) { 1.736 + _prefs.setBoolPref("persisted." + key, true); 1.737 + } 1.738 + if (--numFilesToPersist == 0 && aCallback) { 1.739 + aCallback(); 1.740 + } 1.741 + }; 1.742 + 1.743 + let numFilesToPersist = 0; 1.744 + for (let key in PERSIST_FILES) { 1.745 + _prefs.setBoolPref("persisted." + key, false); 1.746 + if (aData[key]) { 1.747 + numFilesToPersist++; 1.748 + _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key)); 1.749 + } 1.750 + } 1.751 +} 1.752 + 1.753 +function _getLocalImageURI(localFileName) { 1.754 + var localFile = Services.dirsvc.get("ProfD", Ci.nsIFile); 1.755 + localFile.append(localFileName); 1.756 + return Services.io.newFileURI(localFile); 1.757 +} 1.758 + 1.759 +function _persistImage(sourceURL, localFileName, successCallback) { 1.760 + if (/^file:/.test(sourceURL)) 1.761 + return; 1.762 + 1.763 + var targetURI = _getLocalImageURI(localFileName); 1.764 + var sourceURI = _makeURI(sourceURL); 1.765 + 1.766 + var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] 1.767 + .createInstance(Ci.nsIWebBrowserPersist); 1.768 + 1.769 + persist.persistFlags = 1.770 + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | 1.771 + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION | 1.772 + (PERSIST_BYPASS_CACHE ? 1.773 + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE : 1.774 + Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE); 1.775 + 1.776 + persist.progressListener = new _persistProgressListener(successCallback); 1.777 + 1.778 + persist.saveURI(sourceURI, null, null, null, null, targetURI, null); 1.779 +} 1.780 + 1.781 +function _persistProgressListener(successCallback) { 1.782 + this.onLocationChange = function persistProgressListener_onLocationChange() {}; 1.783 + this.onProgressChange = function persistProgressListener_onProgressChange() {}; 1.784 + this.onStatusChange = function persistProgressListener_onStatusChange() {}; 1.785 + this.onSecurityChange = function persistProgressListener_onSecurityChange() {}; 1.786 + this.onStateChange = function persistProgressListener_onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { 1.787 + if (aRequest && 1.788 + aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && 1.789 + aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { 1.790 + try { 1.791 + if (aRequest.QueryInterface(Ci.nsIHttpChannel).requestSucceeded) { 1.792 + // success 1.793 + successCallback(); 1.794 + return; 1.795 + } 1.796 + } catch (e) { } 1.797 + // failure 1.798 + } 1.799 + }; 1.800 +} 1.801 + 1.802 +AddonManagerPrivate.registerProvider(LightweightThemeManager, [ 1.803 + new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, 1.804 + STRING_TYPE_NAME, 1.805 + AddonManager.VIEW_TYPE_LIST, 5000) 1.806 +]);