michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["LightweightThemeManager"]; michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/AddonManager.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: const ID_SUFFIX = "@personas.mozilla.org"; michael@0: const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect"; michael@0: const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; michael@0: const PREF_EM_DSS_ENABLED = "extensions.dss.enabled"; michael@0: const ADDON_TYPE = "theme"; michael@0: michael@0: const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties"; michael@0: michael@0: const STRING_TYPE_NAME = "type.%ID%.name"; michael@0: michael@0: const DEFAULT_MAX_USED_THEMES_COUNT = 30; michael@0: michael@0: const MAX_PREVIEW_SECONDS = 30; michael@0: michael@0: const MANDATORY = ["id", "name", "headerURL"]; michael@0: const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL", michael@0: "previewURL", "author", "description", "homepageURL", michael@0: "updateURL", "version"]; michael@0: michael@0: const PERSIST_ENABLED = true; michael@0: const PERSIST_BYPASS_CACHE = false; michael@0: const PERSIST_FILES = { michael@0: headerURL: "lightweighttheme-header", michael@0: footerURL: "lightweighttheme-footer" michael@0: }; michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer", michael@0: "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm"); michael@0: michael@0: this.__defineGetter__("_prefs", function prefsGetter() { michael@0: delete this._prefs; michael@0: return this._prefs = Services.prefs.getBranch("lightweightThemes."); michael@0: }); michael@0: michael@0: this.__defineGetter__("_maxUsedThemes", function maxUsedThemesGetter() { michael@0: delete this._maxUsedThemes; michael@0: try { michael@0: this._maxUsedThemes = _prefs.getIntPref("maxUsedThemes"); michael@0: } michael@0: catch (e) { michael@0: this._maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT; michael@0: } michael@0: return this._maxUsedThemes; michael@0: }); michael@0: michael@0: this.__defineSetter__("_maxUsedThemes", function maxUsedThemesSetter(aVal) { michael@0: delete this._maxUsedThemes; michael@0: return this._maxUsedThemes = aVal; michael@0: }); michael@0: michael@0: // Holds the ID of the theme being enabled or disabled while sending out the michael@0: // events so cached AddonWrapper instances can return correct values for michael@0: // permissions and pendingOperations michael@0: var _themeIDBeingEnabled = null; michael@0: var _themeIDBeingDisabled = null; michael@0: michael@0: this.LightweightThemeManager = { michael@0: get usedThemes () { michael@0: try { michael@0: return JSON.parse(_prefs.getComplexValue("usedThemes", michael@0: Ci.nsISupportsString).data); michael@0: } catch (e) { michael@0: return []; michael@0: } michael@0: }, michael@0: michael@0: get currentTheme () { michael@0: try { michael@0: if (_prefs.getBoolPref("isThemeSelected")) michael@0: var data = this.usedThemes[0]; michael@0: } catch (e) {} michael@0: michael@0: return data || null; michael@0: }, michael@0: michael@0: get currentThemeForDisplay () { michael@0: var data = this.currentTheme; michael@0: michael@0: if (data && PERSIST_ENABLED) { michael@0: for (let key in PERSIST_FILES) { michael@0: try { michael@0: if (data[key] && _prefs.getBoolPref("persisted." + key)) michael@0: data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec michael@0: + "?" + data.id + ";" + _version(data); michael@0: } catch (e) {} michael@0: } michael@0: } michael@0: michael@0: return data; michael@0: }, michael@0: michael@0: set currentTheme (aData) { michael@0: return _setCurrentTheme(aData, false); michael@0: }, michael@0: michael@0: setLocalTheme: function LightweightThemeManager_setLocalTheme(aData) { michael@0: _setCurrentTheme(aData, true); michael@0: }, michael@0: michael@0: getUsedTheme: function LightweightThemeManager_getUsedTheme(aId) { michael@0: var usedThemes = this.usedThemes; michael@0: for (let usedTheme of usedThemes) { michael@0: if (usedTheme.id == aId) michael@0: return usedTheme; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) { michael@0: let theme = this.getUsedTheme(aId); michael@0: if (!theme) michael@0: return; michael@0: michael@0: let wrapper = new AddonWrapper(theme); michael@0: AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); michael@0: michael@0: var currentTheme = this.currentTheme; michael@0: if (currentTheme && currentTheme.id == aId) { michael@0: this.themeChanged(null); michael@0: AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false); michael@0: } michael@0: michael@0: _updateUsedThemes(_usedThemesExceptId(aId)); michael@0: AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); michael@0: }, michael@0: michael@0: previewTheme: function LightweightThemeManager_previewTheme(aData) { michael@0: if (!aData) michael@0: return; michael@0: michael@0: let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); michael@0: cancel.data = false; michael@0: Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested", michael@0: JSON.stringify(aData)); michael@0: if (cancel.data) michael@0: return; michael@0: michael@0: if (_previewTimer) michael@0: _previewTimer.cancel(); michael@0: else michael@0: _previewTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: _previewTimer.initWithCallback(_previewTimerCallback, michael@0: MAX_PREVIEW_SECONDS * 1000, michael@0: _previewTimer.TYPE_ONE_SHOT); michael@0: michael@0: _notifyWindows(aData); michael@0: }, michael@0: michael@0: resetPreview: function LightweightThemeManager_resetPreview() { michael@0: if (_previewTimer) { michael@0: _previewTimer.cancel(); michael@0: _previewTimer = null; michael@0: _notifyWindows(this.currentThemeForDisplay); michael@0: } michael@0: }, michael@0: michael@0: parseTheme: function LightweightThemeManager_parseTheme(aString, aBaseURI) { michael@0: try { michael@0: return _sanitizeTheme(JSON.parse(aString), aBaseURI, false); michael@0: } catch (e) { michael@0: return null; michael@0: } michael@0: }, michael@0: michael@0: updateCurrentTheme: function LightweightThemeManager_updateCurrentTheme() { michael@0: try { michael@0: if (!_prefs.getBoolPref("update.enabled")) michael@0: return; michael@0: } catch (e) { michael@0: return; michael@0: } michael@0: michael@0: var theme = this.currentTheme; michael@0: if (!theme || !theme.updateURL) michael@0: return; michael@0: michael@0: var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] michael@0: .createInstance(Ci.nsIXMLHttpRequest); michael@0: michael@0: req.mozBackgroundRequest = true; michael@0: req.overrideMimeType("text/plain"); michael@0: req.open("GET", theme.updateURL, true); michael@0: // Prevent the request from reading from the cache. michael@0: req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; michael@0: // Prevent the request from writing to the cache. michael@0: req.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING; michael@0: michael@0: var self = this; michael@0: req.addEventListener("load", function loadEventListener() { michael@0: if (req.status != 200) michael@0: return; michael@0: michael@0: let newData = self.parseTheme(req.responseText, theme.updateURL); michael@0: if (!newData || michael@0: newData.id != theme.id || michael@0: _version(newData) == _version(theme)) michael@0: return; michael@0: michael@0: var currentTheme = self.currentTheme; michael@0: if (currentTheme && currentTheme.id == theme.id) michael@0: self.currentTheme = newData; michael@0: }, false); michael@0: michael@0: req.send(null); michael@0: }, michael@0: michael@0: /** michael@0: * Switches to a new lightweight theme. michael@0: * michael@0: * @param aData michael@0: * The lightweight theme to switch to michael@0: */ michael@0: themeChanged: function LightweightThemeManager_themeChanged(aData) { michael@0: if (_previewTimer) { michael@0: _previewTimer.cancel(); michael@0: _previewTimer = null; michael@0: } michael@0: michael@0: if (aData) { michael@0: let usedThemes = _usedThemesExceptId(aData.id); michael@0: usedThemes.unshift(aData); michael@0: _updateUsedThemes(usedThemes); michael@0: if (PERSIST_ENABLED) { michael@0: LightweightThemeImageOptimizer.purge(); michael@0: _persistImages(aData, function themeChanged_persistImages() { michael@0: _notifyWindows(this.currentThemeForDisplay); michael@0: }.bind(this)); michael@0: } michael@0: } michael@0: michael@0: _prefs.setBoolPref("isThemeSelected", aData != null); michael@0: _notifyWindows(aData); michael@0: Services.obs.notifyObservers(null, "lightweight-theme-changed", null); michael@0: }, michael@0: michael@0: /** michael@0: * Starts the Addons provider and enables the new lightweight theme if michael@0: * necessary. michael@0: */ michael@0: startup: function LightweightThemeManager_startup() { michael@0: if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) { michael@0: let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); michael@0: if (id) michael@0: this.themeChanged(this.getUsedTheme(id)); michael@0: else michael@0: this.themeChanged(null); michael@0: Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); michael@0: } michael@0: michael@0: _prefs.addObserver("", _prefObserver, false); michael@0: }, michael@0: michael@0: /** michael@0: * Shuts down the provider. michael@0: */ michael@0: shutdown: function LightweightThemeManager_shutdown() { michael@0: _prefs.removeObserver("", _prefObserver); michael@0: }, michael@0: michael@0: /** michael@0: * Called when a new add-on has been enabled when only one add-on of that type michael@0: * can be enabled. michael@0: * michael@0: * @param aId michael@0: * The ID of the newly enabled add-on michael@0: * @param aType michael@0: * The type of the newly enabled add-on michael@0: * @param aPendingRestart michael@0: * true if the newly enabled add-on will only become enabled after a michael@0: * restart michael@0: */ michael@0: addonChanged: function LightweightThemeManager_addonChanged(aId, aType, aPendingRestart) { michael@0: if (aType != ADDON_TYPE) michael@0: return; michael@0: michael@0: let id = _getInternalID(aId); michael@0: let current = this.currentTheme; michael@0: michael@0: try { michael@0: let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); michael@0: if (id == next && aPendingRestart) michael@0: return; michael@0: michael@0: Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); michael@0: if (next) { michael@0: AddonManagerPrivate.callAddonListeners("onOperationCancelled", michael@0: new AddonWrapper(this.getUsedTheme(next))); michael@0: } michael@0: else { michael@0: if (id == current.id) { michael@0: AddonManagerPrivate.callAddonListeners("onOperationCancelled", michael@0: new AddonWrapper(current)); michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: catch (e) { michael@0: } michael@0: michael@0: if (current) { michael@0: if (current.id == id) michael@0: return; michael@0: _themeIDBeingDisabled = current.id; michael@0: let wrapper = new AddonWrapper(current); michael@0: if (aPendingRestart) { michael@0: Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, ""); michael@0: AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true); michael@0: } michael@0: else { michael@0: AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false); michael@0: this.themeChanged(null); michael@0: AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); michael@0: } michael@0: _themeIDBeingDisabled = null; michael@0: } michael@0: michael@0: if (id) { michael@0: let theme = this.getUsedTheme(id); michael@0: _themeIDBeingEnabled = id; michael@0: let wrapper = new AddonWrapper(theme); michael@0: if (aPendingRestart) { michael@0: AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true); michael@0: Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id); michael@0: michael@0: // Flush the preferences to disk so they survive any crash michael@0: Services.prefs.savePrefFile(null); michael@0: } michael@0: else { michael@0: AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false); michael@0: this.themeChanged(theme); michael@0: AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); michael@0: } michael@0: _themeIDBeingEnabled = null; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Called to get an Addon with a particular ID. michael@0: * michael@0: * @param aId michael@0: * The ID of the add-on to retrieve michael@0: * @param aCallback michael@0: * A callback to pass the Addon to michael@0: */ michael@0: getAddonByID: function LightweightThemeManager_getAddonByID(aId, aCallback) { michael@0: let id = _getInternalID(aId); michael@0: if (!id) { michael@0: aCallback(null); michael@0: return; michael@0: } michael@0: michael@0: let theme = this.getUsedTheme(id); michael@0: if (!theme) { michael@0: aCallback(null); michael@0: return; michael@0: } michael@0: michael@0: aCallback(new AddonWrapper(theme)); michael@0: }, michael@0: michael@0: /** michael@0: * Called to get Addons of a particular type. michael@0: * michael@0: * @param aTypes michael@0: * An array of types to fetch. Can be null to get all types. michael@0: * @param aCallback michael@0: * A callback to pass an array of Addons to michael@0: */ michael@0: getAddonsByTypes: function LightweightThemeManager_getAddonsByTypes(aTypes, aCallback) { michael@0: if (aTypes && aTypes.indexOf(ADDON_TYPE) == -1) { michael@0: aCallback([]); michael@0: return; michael@0: } michael@0: michael@0: aCallback([new AddonWrapper(a) for each (a in this.usedThemes)]); michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * The AddonWrapper wraps lightweight theme to provide the data visible to michael@0: * consumers of the AddonManager API. michael@0: */ michael@0: function AddonWrapper(aTheme) { michael@0: this.__defineGetter__("id", function AddonWrapper_idGetter() aTheme.id + ID_SUFFIX); michael@0: this.__defineGetter__("type", function AddonWrapper_typeGetter() ADDON_TYPE); michael@0: this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() { michael@0: let current = LightweightThemeManager.currentTheme; michael@0: if (current) michael@0: return aTheme.id == current.id; michael@0: return false; michael@0: }); michael@0: michael@0: this.__defineGetter__("name", function AddonWrapper_nameGetter() aTheme.name); michael@0: this.__defineGetter__("version", function AddonWrapper_versionGetter() { michael@0: return "version" in aTheme ? aTheme.version : ""; michael@0: }); michael@0: michael@0: ["description", "homepageURL", "iconURL"].forEach(function(prop) { michael@0: this.__defineGetter__(prop, function AddonWrapper_optionalPropGetter() { michael@0: return prop in aTheme ? aTheme[prop] : null; michael@0: }); michael@0: }, this); michael@0: michael@0: ["installDate", "updateDate"].forEach(function(prop) { michael@0: this.__defineGetter__(prop, function AddonWrapper_datePropGetter() { michael@0: return prop in aTheme ? new Date(aTheme[prop]) : null; michael@0: }); michael@0: }, this); michael@0: michael@0: this.__defineGetter__("creator", function AddonWrapper_creatorGetter() { michael@0: return new AddonManagerPrivate.AddonAuthor(aTheme.author); michael@0: }); michael@0: michael@0: this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() { michael@0: let url = aTheme.previewURL; michael@0: return [new AddonManagerPrivate.AddonScreenshot(url)]; michael@0: }); michael@0: michael@0: this.__defineGetter__("pendingOperations", michael@0: function AddonWrapper_pendingOperationsGetter() { michael@0: let pending = AddonManager.PENDING_NONE; michael@0: if (this.isActive == this.userDisabled) michael@0: pending |= this.isActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE; michael@0: return pending; michael@0: }); michael@0: michael@0: this.__defineGetter__("operationsRequiringRestart", michael@0: function AddonWrapper_operationsRequiringRestartGetter() { michael@0: // If a non-default theme is in use then a restart will be required to michael@0: // enable lightweight themes unless dynamic theme switching is enabled michael@0: if (Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) { michael@0: try { michael@0: if (Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED)) michael@0: return AddonManager.OP_NEEDS_RESTART_NONE; michael@0: } michael@0: catch (e) { michael@0: } michael@0: return AddonManager.OP_NEEDS_RESTART_ENABLE; michael@0: } michael@0: michael@0: return AddonManager.OP_NEEDS_RESTART_NONE; michael@0: }); michael@0: michael@0: this.__defineGetter__("size", function AddonWrapper_sizeGetter() { michael@0: // The size changes depending on whether the theme is in use or not, this is michael@0: // probably not worth exposing. michael@0: return null; michael@0: }); michael@0: michael@0: this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() { michael@0: let permissions = AddonManager.PERM_CAN_UNINSTALL; michael@0: if (this.userDisabled) michael@0: permissions |= AddonManager.PERM_CAN_ENABLE; michael@0: else michael@0: permissions |= AddonManager.PERM_CAN_DISABLE; michael@0: return permissions; michael@0: }); michael@0: michael@0: this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() { michael@0: if (_themeIDBeingEnabled == aTheme.id) michael@0: return false; michael@0: if (_themeIDBeingDisabled == aTheme.id) michael@0: return true; michael@0: michael@0: try { michael@0: let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); michael@0: return aTheme.id != toSelect; michael@0: } michael@0: catch (e) { michael@0: let current = LightweightThemeManager.currentTheme; michael@0: return !current || current.id != aTheme.id; michael@0: } michael@0: }); michael@0: michael@0: this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) { michael@0: if (val == this.userDisabled) michael@0: return val; michael@0: michael@0: if (val) michael@0: LightweightThemeManager.currentTheme = null; michael@0: else michael@0: LightweightThemeManager.currentTheme = aTheme; michael@0: michael@0: return val; michael@0: }); michael@0: michael@0: this.uninstall = function AddonWrapper_uninstall() { michael@0: LightweightThemeManager.forgetUsedTheme(aTheme.id); michael@0: }; michael@0: michael@0: this.cancelUninstall = function AddonWrapper_cancelUninstall() { michael@0: throw new Error("Theme is not marked to be uninstalled"); michael@0: }; michael@0: michael@0: this.findUpdates = function AddonWrapper_findUpdates(listener, reason, appVersion, platformVersion) { michael@0: AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion); michael@0: }; michael@0: } michael@0: michael@0: AddonWrapper.prototype = { michael@0: // Lightweight themes are never disabled by the application michael@0: get appDisabled() { michael@0: return false; michael@0: }, michael@0: michael@0: // Lightweight themes are always compatible michael@0: get isCompatible() { michael@0: return true; michael@0: }, michael@0: michael@0: get isPlatformCompatible() { michael@0: return true; michael@0: }, michael@0: michael@0: get scope() { michael@0: return AddonManager.SCOPE_PROFILE; michael@0: }, michael@0: michael@0: get foreignInstall() { michael@0: return false; michael@0: }, michael@0: michael@0: // Lightweight themes are always compatible michael@0: isCompatibleWith: function AddonWrapper_isCompatibleWith(appVersion, platformVersion) { michael@0: return true; michael@0: }, michael@0: michael@0: // Lightweight themes are always securely updated michael@0: get providesUpdatesSecurely() { michael@0: return true; michael@0: }, michael@0: michael@0: // Lightweight themes are never blocklisted michael@0: get blocklistState() { michael@0: return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Converts the ID used by the public AddonManager API to an lightweight theme michael@0: * ID. michael@0: * michael@0: * @param id michael@0: * The ID to be converted michael@0: * michael@0: * @return the lightweight theme ID or null if the ID was not for a lightweight michael@0: * theme. michael@0: */ michael@0: function _getInternalID(id) { michael@0: if (!id) michael@0: return null; michael@0: let len = id.length - ID_SUFFIX.length; michael@0: if (len > 0 && id.substring(len) == ID_SUFFIX) michael@0: return id.substring(0, len); michael@0: return null; michael@0: } michael@0: michael@0: function _setCurrentTheme(aData, aLocal) { michael@0: aData = _sanitizeTheme(aData, null, aLocal); michael@0: michael@0: let needsRestart = (ADDON_TYPE == "theme") && michael@0: Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN); michael@0: michael@0: let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); michael@0: cancel.data = false; michael@0: Services.obs.notifyObservers(cancel, "lightweight-theme-change-requested", michael@0: JSON.stringify(aData)); michael@0: michael@0: if (aData) { michael@0: let theme = LightweightThemeManager.getUsedTheme(aData.id); michael@0: let isInstall = !theme || theme.version != aData.version; michael@0: if (isInstall) { michael@0: aData.updateDate = Date.now(); michael@0: if (theme && "installDate" in theme) michael@0: aData.installDate = theme.installDate; michael@0: else michael@0: aData.installDate = aData.updateDate; michael@0: michael@0: var oldWrapper = theme ? new AddonWrapper(theme) : null; michael@0: var wrapper = new AddonWrapper(aData); michael@0: AddonManagerPrivate.callInstallListeners("onExternalInstall", null, michael@0: wrapper, oldWrapper, false); michael@0: AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false); michael@0: } michael@0: michael@0: let current = LightweightThemeManager.currentTheme; michael@0: let usedThemes = _usedThemesExceptId(aData.id); michael@0: if (current && current.id != aData.id) michael@0: usedThemes.splice(1, 0, aData); michael@0: else michael@0: usedThemes.unshift(aData); michael@0: _updateUsedThemes(usedThemes); michael@0: michael@0: if (isInstall) michael@0: AddonManagerPrivate.callAddonListeners("onInstalled", wrapper); michael@0: } michael@0: michael@0: if (cancel.data) michael@0: return null; michael@0: michael@0: AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null, michael@0: ADDON_TYPE, needsRestart); michael@0: michael@0: return LightweightThemeManager.currentTheme; michael@0: } michael@0: michael@0: function _sanitizeTheme(aData, aBaseURI, aLocal) { michael@0: if (!aData || typeof aData != "object") michael@0: return null; michael@0: michael@0: var resourceProtocols = ["http", "https"]; michael@0: if (aLocal) michael@0: resourceProtocols.push("file"); michael@0: var resourceProtocolExp = new RegExp("^(" + resourceProtocols.join("|") + "):"); michael@0: michael@0: function sanitizeProperty(prop) { michael@0: if (!(prop in aData)) michael@0: return null; michael@0: if (typeof aData[prop] != "string") michael@0: return null; michael@0: let val = aData[prop].trim(); michael@0: if (!val) michael@0: return null; michael@0: michael@0: if (!/URL$/.test(prop)) michael@0: return val; michael@0: michael@0: try { michael@0: val = _makeURI(val, aBaseURI ? _makeURI(aBaseURI) : null).spec; michael@0: if ((prop == "updateURL" ? /^https:/ : resourceProtocolExp).test(val)) michael@0: return val; michael@0: return null; michael@0: } michael@0: catch (e) { michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: let result = {}; michael@0: for (let mandatoryProperty of MANDATORY) { michael@0: let val = sanitizeProperty(mandatoryProperty); michael@0: if (!val) michael@0: throw Components.results.NS_ERROR_INVALID_ARG; michael@0: result[mandatoryProperty] = val; michael@0: } michael@0: michael@0: for (let optionalProperty of OPTIONAL) { michael@0: let val = sanitizeProperty(optionalProperty); michael@0: if (!val) michael@0: continue; michael@0: result[optionalProperty] = val; michael@0: } michael@0: michael@0: return result; michael@0: } michael@0: michael@0: function _usedThemesExceptId(aId) michael@0: LightweightThemeManager.usedThemes.filter( michael@0: function usedThemesExceptId_filterID(t) "id" in t && t.id != aId); michael@0: michael@0: function _version(aThemeData) michael@0: aThemeData.version || ""; michael@0: michael@0: function _makeURI(aURL, aBaseURI) michael@0: Services.io.newURI(aURL, null, aBaseURI); michael@0: michael@0: function _updateUsedThemes(aList) { michael@0: // Send uninstall events for all themes that need to be removed. michael@0: while (aList.length > _maxUsedThemes) { michael@0: let wrapper = new AddonWrapper(aList[aList.length - 1]); michael@0: AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); michael@0: aList.pop(); michael@0: AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); michael@0: } michael@0: michael@0: var str = Cc["@mozilla.org/supports-string;1"] michael@0: .createInstance(Ci.nsISupportsString); michael@0: str.data = JSON.stringify(aList); michael@0: _prefs.setComplexValue("usedThemes", Ci.nsISupportsString, str); michael@0: michael@0: Services.obs.notifyObservers(null, "lightweight-theme-list-changed", null); michael@0: } michael@0: michael@0: function _notifyWindows(aThemeData) { michael@0: Services.obs.notifyObservers(null, "lightweight-theme-styling-update", michael@0: JSON.stringify(aThemeData)); michael@0: } michael@0: michael@0: var _previewTimer; michael@0: var _previewTimerCallback = { michael@0: notify: function _previewTimerCallback_notify() { michael@0: LightweightThemeManager.resetPreview(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Called when any of the lightweightThemes preferences are changed. michael@0: */ michael@0: function _prefObserver(aSubject, aTopic, aData) { michael@0: switch (aData) { michael@0: case "maxUsedThemes": michael@0: try { michael@0: _maxUsedThemes = _prefs.getIntPref(aData); michael@0: } michael@0: catch (e) { michael@0: _maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT; michael@0: } michael@0: // Update the theme list to remove any themes over the number we keep michael@0: _updateUsedThemes(LightweightThemeManager.usedThemes); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: function _persistImages(aData, aCallback) { michael@0: function onSuccess(key) function () { michael@0: let current = LightweightThemeManager.currentTheme; michael@0: if (current && current.id == aData.id) { michael@0: _prefs.setBoolPref("persisted." + key, true); michael@0: } michael@0: if (--numFilesToPersist == 0 && aCallback) { michael@0: aCallback(); michael@0: } michael@0: }; michael@0: michael@0: let numFilesToPersist = 0; michael@0: for (let key in PERSIST_FILES) { michael@0: _prefs.setBoolPref("persisted." + key, false); michael@0: if (aData[key]) { michael@0: numFilesToPersist++; michael@0: _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key)); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function _getLocalImageURI(localFileName) { michael@0: var localFile = Services.dirsvc.get("ProfD", Ci.nsIFile); michael@0: localFile.append(localFileName); michael@0: return Services.io.newFileURI(localFile); michael@0: } michael@0: michael@0: function _persistImage(sourceURL, localFileName, successCallback) { michael@0: if (/^file:/.test(sourceURL)) michael@0: return; michael@0: michael@0: var targetURI = _getLocalImageURI(localFileName); michael@0: var sourceURI = _makeURI(sourceURL); michael@0: michael@0: var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"] michael@0: .createInstance(Ci.nsIWebBrowserPersist); michael@0: michael@0: persist.persistFlags = michael@0: Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | michael@0: Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION | michael@0: (PERSIST_BYPASS_CACHE ? michael@0: Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE : michael@0: Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE); michael@0: michael@0: persist.progressListener = new _persistProgressListener(successCallback); michael@0: michael@0: persist.saveURI(sourceURI, null, null, null, null, targetURI, null); michael@0: } michael@0: michael@0: function _persistProgressListener(successCallback) { michael@0: this.onLocationChange = function persistProgressListener_onLocationChange() {}; michael@0: this.onProgressChange = function persistProgressListener_onProgressChange() {}; michael@0: this.onStatusChange = function persistProgressListener_onStatusChange() {}; michael@0: this.onSecurityChange = function persistProgressListener_onSecurityChange() {}; michael@0: this.onStateChange = function persistProgressListener_onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { michael@0: if (aRequest && michael@0: aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && michael@0: aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { michael@0: try { michael@0: if (aRequest.QueryInterface(Ci.nsIHttpChannel).requestSucceeded) { michael@0: // success michael@0: successCallback(); michael@0: return; michael@0: } michael@0: } catch (e) { } michael@0: // failure michael@0: } michael@0: }; michael@0: } michael@0: michael@0: AddonManagerPrivate.registerProvider(LightweightThemeManager, [ michael@0: new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS, michael@0: STRING_TYPE_NAME, michael@0: AddonManager.VIEW_TYPE_LIST, 5000) michael@0: ]);