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