|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils; |
|
8 |
|
9 Cu.import("resource://gre/modules/Services.jsm") |
|
10 Cu.import("resource://gre/modules/AddonManager.jsm"); |
|
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
12 |
|
13 const AMO_ICON = "chrome://browser/skin/images/amo-logo.png"; |
|
14 |
|
15 let gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutAddons.properties"); |
|
16 |
|
17 XPCOMUtils.defineLazyGetter(window, "gChromeWin", function() |
|
18 window.QueryInterface(Ci.nsIInterfaceRequestor) |
|
19 .getInterface(Ci.nsIWebNavigation) |
|
20 .QueryInterface(Ci.nsIDocShellTreeItem) |
|
21 .rootTreeItem |
|
22 .QueryInterface(Ci.nsIInterfaceRequestor) |
|
23 .getInterface(Ci.nsIDOMWindow) |
|
24 .QueryInterface(Ci.nsIDOMChromeWindow)); |
|
25 |
|
26 var ContextMenus = { |
|
27 target: null, |
|
28 |
|
29 init: function() { |
|
30 document.addEventListener("contextmenu", this, false); |
|
31 |
|
32 document.getElementById("contextmenu-enable").addEventListener("click", ContextMenus.enable.bind(this), false); |
|
33 document.getElementById("contextmenu-disable").addEventListener("click", ContextMenus.disable.bind(this), false); |
|
34 document.getElementById("contextmenu-uninstall").addEventListener("click", ContextMenus.uninstall.bind(this), false); |
|
35 |
|
36 // XXX - Hack to fix bug 985867 for now |
|
37 document.addEventListener("touchstart", function() { }); |
|
38 }, |
|
39 |
|
40 handleEvent: function(event) { |
|
41 // store the target of context menu events so that we know which app to act on |
|
42 this.target = event.target; |
|
43 while (!this.target.hasAttribute("contextmenu")) { |
|
44 this.target = this.target.parentNode; |
|
45 } |
|
46 |
|
47 if (!this.target) { |
|
48 document.getElementById("contextmenu-enable").setAttribute("hidden", "true"); |
|
49 document.getElementById("contextmenu-disable").setAttribute("hidden", "true"); |
|
50 document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true"); |
|
51 return; |
|
52 } |
|
53 |
|
54 let addon = this.target.addon; |
|
55 if (addon.scope == AddonManager.SCOPE_APPLICATION) { |
|
56 document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true"); |
|
57 } else { |
|
58 document.getElementById("contextmenu-uninstall").removeAttribute("hidden"); |
|
59 } |
|
60 |
|
61 let enabled = this.target.getAttribute("isDisabled") != "true"; |
|
62 if (enabled) { |
|
63 document.getElementById("contextmenu-enable").setAttribute("hidden", "true"); |
|
64 document.getElementById("contextmenu-disable").removeAttribute("hidden"); |
|
65 } else { |
|
66 document.getElementById("contextmenu-enable").removeAttribute("hidden"); |
|
67 document.getElementById("contextmenu-disable").setAttribute("hidden", "true"); |
|
68 } |
|
69 }, |
|
70 |
|
71 enable: function(event) { |
|
72 Addons.setEnabled(true, this.target.addon); |
|
73 this.target = null; |
|
74 }, |
|
75 |
|
76 disable: function (event) { |
|
77 Addons.setEnabled(false, this.target.addon); |
|
78 this.target = null; |
|
79 }, |
|
80 |
|
81 uninstall: function (event) { |
|
82 Addons.uninstall(this.target.addon); |
|
83 this.target = null; |
|
84 } |
|
85 } |
|
86 |
|
87 function init() { |
|
88 window.addEventListener("popstate", onPopState, false); |
|
89 |
|
90 AddonManager.addInstallListener(Addons); |
|
91 AddonManager.addAddonListener(Addons); |
|
92 Addons.init(); |
|
93 showList(); |
|
94 ContextMenus.init(); |
|
95 |
|
96 document.getElementById("header-button").addEventListener("click", openLink, false); |
|
97 } |
|
98 |
|
99 |
|
100 function uninit() { |
|
101 AddonManager.removeInstallListener(Addons); |
|
102 AddonManager.removeAddonListener(Addons); |
|
103 } |
|
104 |
|
105 function openLink(aEvent) { |
|
106 try { |
|
107 let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter); |
|
108 |
|
109 let url = formatter.formatURLPref(aEvent.currentTarget.getAttribute("pref")); |
|
110 let BrowserApp = gChromeWin.BrowserApp; |
|
111 BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id }); |
|
112 } catch (ex) {} |
|
113 } |
|
114 |
|
115 function onPopState(aEvent) { |
|
116 // Called when back/forward is used to change the state of the page |
|
117 if (aEvent.state) { |
|
118 // Show the detail page for an addon |
|
119 Addons.showDetails(Addons._getElementForAddon(aEvent.state.id)); |
|
120 } else { |
|
121 // Clear any previous detail addon |
|
122 let detailItem = document.querySelector("#addons-details > .addon-item"); |
|
123 detailItem.addon = null; |
|
124 |
|
125 showList(); |
|
126 } |
|
127 } |
|
128 |
|
129 function showList() { |
|
130 // Hide the detail page and show the list |
|
131 let details = document.querySelector("#addons-details"); |
|
132 details.style.display = "none"; |
|
133 let list = document.querySelector("#addons-list"); |
|
134 list.style.display = "block"; |
|
135 } |
|
136 |
|
137 var Addons = { |
|
138 _restartCount: 0, |
|
139 |
|
140 _createItem: function _createItem(aAddon) { |
|
141 let outer = document.createElement("div"); |
|
142 outer.setAttribute("addonID", aAddon.id); |
|
143 outer.className = "addon-item list-item"; |
|
144 outer.setAttribute("role", "button"); |
|
145 outer.setAttribute("contextmenu", "addonmenu"); |
|
146 outer.addEventListener("click", function() { |
|
147 this.showDetails(outer); |
|
148 history.pushState({ id: aAddon.id }, document.title); |
|
149 }.bind(this), true); |
|
150 |
|
151 let img = document.createElement("img"); |
|
152 img.className = "icon"; |
|
153 img.setAttribute("src", aAddon.iconURL || AMO_ICON); |
|
154 outer.appendChild(img); |
|
155 |
|
156 let inner = document.createElement("div"); |
|
157 inner.className = "inner"; |
|
158 |
|
159 let details = document.createElement("div"); |
|
160 details.className = "details"; |
|
161 inner.appendChild(details); |
|
162 |
|
163 let titlePart = document.createElement("div"); |
|
164 titlePart.textContent = aAddon.name; |
|
165 titlePart.className = "title"; |
|
166 details.appendChild(titlePart); |
|
167 |
|
168 let versionPart = document.createElement("div"); |
|
169 versionPart.textContent = aAddon.version; |
|
170 versionPart.className = "version"; |
|
171 details.appendChild(versionPart); |
|
172 |
|
173 if ("description" in aAddon) { |
|
174 let descPart = document.createElement("div"); |
|
175 descPart.textContent = aAddon.description; |
|
176 descPart.className = "description"; |
|
177 inner.appendChild(descPart); |
|
178 } |
|
179 |
|
180 outer.appendChild(inner); |
|
181 return outer; |
|
182 }, |
|
183 |
|
184 _createBrowseItem: function _createBrowseItem() { |
|
185 let outer = document.createElement("div"); |
|
186 outer.className = "addon-item list-item"; |
|
187 outer.setAttribute("role", "button"); |
|
188 outer.setAttribute("pref", "extensions.getAddons.browseAddons"); |
|
189 outer.addEventListener("click", openLink, true); |
|
190 |
|
191 let img = document.createElement("img"); |
|
192 img.className = "icon"; |
|
193 img.setAttribute("src", AMO_ICON); |
|
194 outer.appendChild(img); |
|
195 |
|
196 let inner = document.createElement("div"); |
|
197 inner.className = "inner"; |
|
198 |
|
199 let title = document.createElement("div"); |
|
200 title.id = "browse-title"; |
|
201 title.className = "title"; |
|
202 title.textContent = gStringBundle.GetStringFromName("addons.browseAll"); |
|
203 inner.appendChild(title); |
|
204 |
|
205 outer.appendChild(inner); |
|
206 return outer; |
|
207 }, |
|
208 |
|
209 _createItemForAddon: function _createItemForAddon(aAddon) { |
|
210 let appManaged = (aAddon.scope == AddonManager.SCOPE_APPLICATION); |
|
211 let opType = this._getOpTypeForOperations(aAddon.pendingOperations); |
|
212 let updateable = (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE) > 0; |
|
213 let uninstallable = (aAddon.permissions & AddonManager.PERM_CAN_UNINSTALL) > 0; |
|
214 |
|
215 let blocked = ""; |
|
216 switch(aAddon.blocklistState) { |
|
217 case Ci.nsIBlocklistService.STATE_BLOCKED: |
|
218 blocked = "blocked"; |
|
219 break; |
|
220 case Ci.nsIBlocklistService.STATE_SOFTBLOCKED: |
|
221 blocked = "softBlocked"; |
|
222 break; |
|
223 case Ci.nsIBlocklistService.STATE_OUTDATED: |
|
224 blocked = "outdated"; |
|
225 break; |
|
226 } |
|
227 |
|
228 let item = this._createItem(aAddon); |
|
229 item.setAttribute("isDisabled", !aAddon.isActive); |
|
230 item.setAttribute("opType", opType); |
|
231 item.setAttribute("updateable", updateable); |
|
232 if (blocked) |
|
233 item.setAttribute("blockedStatus", blocked); |
|
234 item.setAttribute("optionsURL", aAddon.optionsURL || ""); |
|
235 item.addon = aAddon; |
|
236 |
|
237 return item; |
|
238 }, |
|
239 |
|
240 _getElementForAddon: function(aKey) { |
|
241 let list = document.getElementById("addons-list"); |
|
242 let element = list.querySelector("div[addonID=" + aKey.quote() + "]"); |
|
243 return element; |
|
244 }, |
|
245 |
|
246 init: function init() { |
|
247 let self = this; |
|
248 AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function(aAddons) { |
|
249 // Clear all content before filling the addons |
|
250 let list = document.getElementById("addons-list"); |
|
251 list.innerHTML = ""; |
|
252 |
|
253 for (let i=0; i<aAddons.length; i++) { |
|
254 let item = self._createItemForAddon(aAddons[i]); |
|
255 list.appendChild(item); |
|
256 } |
|
257 |
|
258 // Add a "Browse all Firefox Add-ons" item to the bottom of the list. |
|
259 let browseItem = self._createBrowseItem(); |
|
260 list.appendChild(browseItem); |
|
261 }); |
|
262 |
|
263 document.getElementById("uninstall-btn").addEventListener("click", Addons.uninstallCurrent.bind(this), false); |
|
264 document.getElementById("cancel-btn").addEventListener("click", Addons.cancelUninstall.bind(this), false); |
|
265 document.getElementById("disable-btn").addEventListener("click", Addons.disable.bind(this), false); |
|
266 document.getElementById("enable-btn").addEventListener("click", Addons.enable.bind(this), false); |
|
267 }, |
|
268 |
|
269 _getOpTypeForOperations: function _getOpTypeForOperations(aOperations) { |
|
270 if (aOperations & AddonManager.PENDING_UNINSTALL) |
|
271 return "needs-uninstall"; |
|
272 if (aOperations & AddonManager.PENDING_ENABLE) |
|
273 return "needs-enable"; |
|
274 if (aOperations & AddonManager.PENDING_DISABLE) |
|
275 return "needs-disable"; |
|
276 return ""; |
|
277 }, |
|
278 |
|
279 showDetails: function showDetails(aListItem) { |
|
280 // This function removes and returns the text content of aNode without |
|
281 // removing any child elements. Removing the text nodes ensures any XBL |
|
282 // bindings apply properly. |
|
283 function stripTextNodes(aNode) { |
|
284 var text = ""; |
|
285 for (var i = 0; i < aNode.childNodes.length; i++) { |
|
286 if (aNode.childNodes[i].nodeType != document.ELEMENT_NODE) { |
|
287 text += aNode.childNodes[i].textContent; |
|
288 aNode.removeChild(aNode.childNodes[i--]); |
|
289 } else { |
|
290 text += stripTextNodes(aNode.childNodes[i]); |
|
291 } |
|
292 } |
|
293 return text; |
|
294 } |
|
295 |
|
296 let detailItem = document.querySelector("#addons-details > .addon-item"); |
|
297 detailItem.setAttribute("isDisabled", aListItem.getAttribute("isDisabled")); |
|
298 detailItem.setAttribute("opType", aListItem.getAttribute("opType")); |
|
299 detailItem.setAttribute("optionsURL", aListItem.getAttribute("optionsURL")); |
|
300 let addon = detailItem.addon = aListItem.addon; |
|
301 |
|
302 let favicon = document.querySelector("#addons-details > .addon-item .icon"); |
|
303 favicon.setAttribute("src", addon.iconURL || AMO_ICON); |
|
304 |
|
305 detailItem.querySelector(".title").textContent = addon.name; |
|
306 detailItem.querySelector(".version").textContent = addon.version; |
|
307 detailItem.querySelector(".description-full").textContent = addon.description; |
|
308 detailItem.querySelector(".status-uninstalled").textContent = |
|
309 gStringBundle.formatStringFromName("addonStatus.uninstalled", [addon.name], 1); |
|
310 |
|
311 let enableBtn = document.getElementById("enable-btn"); |
|
312 if (addon.appDisabled) |
|
313 enableBtn.setAttribute("disabled", "true"); |
|
314 else |
|
315 enableBtn.removeAttribute("disabled"); |
|
316 |
|
317 let uninstallBtn = document.getElementById("uninstall-btn"); |
|
318 if (addon.scope == AddonManager.SCOPE_APPLICATION) |
|
319 uninstallBtn.setAttribute("disabled", "true"); |
|
320 else |
|
321 uninstallBtn.removeAttribute("disabled"); |
|
322 |
|
323 let box = document.querySelector("#addons-details > .addon-item .options-box"); |
|
324 box.innerHTML = ""; |
|
325 |
|
326 // Retrieve the extensions preferences |
|
327 try { |
|
328 let optionsURL = aListItem.getAttribute("optionsURL"); |
|
329 let xhr = new XMLHttpRequest(); |
|
330 xhr.open("GET", optionsURL, false); |
|
331 xhr.send(); |
|
332 if (xhr.responseXML) { |
|
333 // Only allow <setting> for now |
|
334 let settings = xhr.responseXML.querySelectorAll(":root > setting"); |
|
335 if (settings.length > 0) { |
|
336 for (let i = 0; i < settings.length; i++) { |
|
337 var setting = settings[i]; |
|
338 var desc = stripTextNodes(setting).trim(); |
|
339 if (!setting.hasAttribute("desc")) |
|
340 setting.setAttribute("desc", desc); |
|
341 box.appendChild(setting); |
|
342 } |
|
343 // Send an event so add-ons can prepopulate any non-preference based |
|
344 // settings |
|
345 let event = document.createEvent("Events"); |
|
346 event.initEvent("AddonOptionsLoad", true, false); |
|
347 window.dispatchEvent(event); |
|
348 |
|
349 // Also send a notification to match the behavior of desktop Firefox |
|
350 let id = aListItem.getAttribute("addonID"); |
|
351 Services.obs.notifyObservers(document, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, id); |
|
352 } else { |
|
353 // No options, so hide the header and reset the list item |
|
354 detailItem.setAttribute("optionsURL", ""); |
|
355 aListItem.setAttribute("optionsURL", ""); |
|
356 } |
|
357 } |
|
358 } catch (e) { } |
|
359 |
|
360 let list = document.querySelector("#addons-list"); |
|
361 list.style.display = "none"; |
|
362 let details = document.querySelector("#addons-details"); |
|
363 details.style.display = "block"; |
|
364 }, |
|
365 |
|
366 setEnabled: function setEnabled(aValue, aAddon) { |
|
367 let detailItem = document.querySelector("#addons-details > .addon-item"); |
|
368 let addon = aAddon || detailItem.addon; |
|
369 if (!addon) |
|
370 return; |
|
371 |
|
372 let listItem = this._getElementForAddon(addon.id); |
|
373 |
|
374 let opType; |
|
375 if (addon.type == "theme") { |
|
376 if (aValue) { |
|
377 // We can have only one theme enabled, so disable the current one if any |
|
378 let list = document.getElementById("addons-list"); |
|
379 let item = list.firstElementChild; |
|
380 while (item) { |
|
381 if (item.addon && (item.addon.type == "theme") && (item.addon.isActive)) { |
|
382 item.addon.userDisabled = true; |
|
383 item.setAttribute("isDisabled", true); |
|
384 break; |
|
385 } |
|
386 item = item.nextSibling; |
|
387 } |
|
388 } |
|
389 addon.userDisabled = !aValue; |
|
390 } else if (addon.type == "locale") { |
|
391 addon.userDisabled = !aValue; |
|
392 } else { |
|
393 addon.userDisabled = !aValue; |
|
394 opType = this._getOpTypeForOperations(addon.pendingOperations); |
|
395 |
|
396 if ((addon.pendingOperations & AddonManager.PENDING_ENABLE) || |
|
397 (addon.pendingOperations & AddonManager.PENDING_DISABLE)) { |
|
398 this.showRestart(); |
|
399 } else if (listItem && /needs-(enable|disable)/.test(listItem.getAttribute("opType"))) { |
|
400 this.hideRestart(); |
|
401 } |
|
402 } |
|
403 |
|
404 if (addon == detailItem.addon) { |
|
405 detailItem.setAttribute("isDisabled", !aValue); |
|
406 if (opType) |
|
407 detailItem.setAttribute("opType", opType); |
|
408 else |
|
409 detailItem.removeAttribute("opType"); |
|
410 } |
|
411 |
|
412 // Sync to the list item |
|
413 if (listItem) { |
|
414 listItem.setAttribute("isDisabled", !aValue); |
|
415 if (opType) |
|
416 listItem.setAttribute("opType", opType); |
|
417 else |
|
418 listItem.removeAttribute("opType"); |
|
419 } |
|
420 }, |
|
421 |
|
422 enable: function enable() { |
|
423 this.setEnabled(true); |
|
424 }, |
|
425 |
|
426 disable: function disable() { |
|
427 this.setEnabled(false); |
|
428 }, |
|
429 |
|
430 uninstallCurrent: function uninstallCurrent() { |
|
431 let detailItem = document.querySelector("#addons-details > .addon-item"); |
|
432 |
|
433 let addon = detailItem.addon; |
|
434 if (!addon) |
|
435 return; |
|
436 |
|
437 this.uninstall(addon); |
|
438 }, |
|
439 |
|
440 uninstall: function uninstall(aAddon) { |
|
441 let list = document.getElementById("addons-list"); |
|
442 |
|
443 if (!aAddon) { |
|
444 return; |
|
445 } |
|
446 |
|
447 let listItem = this._getElementForAddon(aAddon.id); |
|
448 |
|
449 aAddon.uninstall(); |
|
450 if (aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL) { |
|
451 this.showRestart(); |
|
452 |
|
453 // A disabled addon doesn't need a restart so it has no pending ops and |
|
454 // can't be cancelled |
|
455 let opType = this._getOpTypeForOperations(aAddon.pendingOperations); |
|
456 if (!aAddon.isActive && opType == "") |
|
457 opType = "needs-uninstall"; |
|
458 |
|
459 detailItem.setAttribute("opType", opType); |
|
460 listItem.setAttribute("opType", opType); |
|
461 } else { |
|
462 list.removeChild(listItem); |
|
463 history.back(); |
|
464 } |
|
465 }, |
|
466 |
|
467 cancelUninstall: function ev_cancelUninstall() { |
|
468 let detailItem = document.querySelector("#addons-details > .addon-item"); |
|
469 let addon = detailItem.addon; |
|
470 if (!addon) |
|
471 return; |
|
472 |
|
473 addon.cancelUninstall(); |
|
474 this.hideRestart(); |
|
475 |
|
476 let opType = this._getOpTypeForOperations(addon.pendingOperations); |
|
477 detailItem.setAttribute("opType", opType); |
|
478 |
|
479 let listItem = this._getElementForAddon(addon.id); |
|
480 listItem.setAttribute("opType", opType); |
|
481 }, |
|
482 |
|
483 showRestart: function showRestart() { |
|
484 this._restartCount++; |
|
485 gChromeWin.XPInstallObserver.showRestartPrompt(); |
|
486 }, |
|
487 |
|
488 hideRestart: function hideRestart() { |
|
489 this._restartCount--; |
|
490 if (this._restartCount == 0) |
|
491 gChromeWin.XPInstallObserver.hideRestartPrompt(); |
|
492 }, |
|
493 |
|
494 onEnabled: function(aAddon) { |
|
495 let listItem = this._getElementForAddon(aAddon.id); |
|
496 if (!listItem) |
|
497 return; |
|
498 |
|
499 // Reload the details to pick up any options now that it's enabled. |
|
500 listItem.setAttribute("optionsURL", aAddon.optionsURL || ""); |
|
501 let detailItem = document.querySelector("#addons-details > .addon-item"); |
|
502 if (aAddon == detailItem.addon) |
|
503 this.showDetails(listItem); |
|
504 }, |
|
505 |
|
506 onInstallEnded: function(aInstall, aAddon) { |
|
507 let needsRestart = false; |
|
508 if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE)) |
|
509 needsRestart = true; |
|
510 else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL) |
|
511 needsRestart = true; |
|
512 |
|
513 let list = document.getElementById("addons-list"); |
|
514 let element = this._getElementForAddon(aAddon.id); |
|
515 if (!element) { |
|
516 element = this._createItemForAddon(aAddon); |
|
517 list.insertBefore(element, list.firstElementChild); |
|
518 } |
|
519 |
|
520 if (needsRestart) |
|
521 element.setAttribute("opType", "needs-restart"); |
|
522 }, |
|
523 |
|
524 onInstallFailed: function(aInstall) { |
|
525 }, |
|
526 |
|
527 onDownloadProgress: function xpidm_onDownloadProgress(aInstall) { |
|
528 }, |
|
529 |
|
530 onDownloadFailed: function(aInstall) { |
|
531 }, |
|
532 |
|
533 onDownloadCancelled: function(aInstall) { |
|
534 } |
|
535 } |
|
536 |
|
537 window.addEventListener("load", init, false); |
|
538 window.addEventListener("unload", uninit, false); |