fune/mobile/android/chrome/content/aboutAddons.js
Kris Maglione 031076f2f3 Bug 1463291: Move docShell getter from Document to Window. r=bz
DocShells are associated with outer DOM Windows, rather than Documents, so
having the getter on the document is a bit odd to begin with. But it's also
considerably less convenient, since most of the times when we want a docShell
from JS, we're dealing most directly with a window, and have to detour through
the document to get it.

MozReview-Commit-ID: LUj1H9nG3QL

--HG--
extra : source : fcfb99baa0f0fb60a7c420a712c6ae7c72576871
extra : histedit_source : 5be9b7b29a52a4b8376ee0bdfc5c08b12e3c775a
2018-05-21 16:58:23 -07:00

753 lines
25 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* globals gChromeWin */
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const AMO_ICON = "chrome://browser/skin/images/amo-logo.png";
const UPDATE_INDICATOR = "chrome://browser/skin/images/extension-update.svg";
var gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutAddons.properties");
XPCOMUtils.defineLazyGetter(window, "gChromeWin", function() {
return window.docShell.rootTreeItem.domWindow
.QueryInterface(Ci.nsIDOMChromeWindow);
});
ChromeUtils.defineModuleGetter(window, "Preferences",
"resource://gre/modules/Preferences.jsm");
var ContextMenus = {
target: null,
init: function() {
document.addEventListener("contextmenu", this);
document.getElementById("contextmenu-enable").addEventListener("click", ContextMenus.enable.bind(this));
document.getElementById("contextmenu-disable").addEventListener("click", ContextMenus.disable.bind(this));
document.getElementById("contextmenu-uninstall").addEventListener("click", ContextMenus.uninstall.bind(this));
// XXX - Hack to fix bug 985867 for now
document.addEventListener("touchstart", function() { });
},
handleEvent: function(event) {
// store the target of context menu events so that we know which app to act on
this.target = event.target;
while (!this.target.hasAttribute("contextmenu")) {
this.target = this.target.parentNode;
}
if (!this.target) {
document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true");
return;
}
let addon = this.target.addon;
if (addon.scope == AddonManager.SCOPE_APPLICATION) {
document.getElementById("contextmenu-uninstall").setAttribute("hidden", "true");
} else {
document.getElementById("contextmenu-uninstall").removeAttribute("hidden");
}
// Hide the enable/disable context menu items if the add-on was disabled by
// Firefox (e.g. unsigned or blocklisted add-on).
if (addon.appDisabled) {
document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
return;
}
let enabled = this.target.getAttribute("isDisabled") != "true";
if (enabled) {
document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
document.getElementById("contextmenu-disable").removeAttribute("hidden");
} else {
document.getElementById("contextmenu-enable").removeAttribute("hidden");
document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
}
},
enable: function(event) {
Addons.setEnabled(true, this.target.addon);
this.target = null;
},
disable: function(event) {
Addons.setEnabled(false, this.target.addon);
this.target = null;
},
uninstall: function(event) {
Addons.uninstall(this.target.addon);
this.target = null;
}
};
function sendEMPong() {
Services.obs.notifyObservers(window, "EM-pong");
}
async function init() {
window.addEventListener("popstate", onPopState);
AddonManager.addInstallListener(Addons);
AddonManager.addAddonListener(Addons);
await Addons.init();
showAddons();
ContextMenus.init();
Services.obs.addObserver(sendEMPong, "EM-ping");
// The addons list has been loaded and rendered, send a notification
// if the openOptionsPage is waiting to be able to select an addon details page.
Services.obs.notifyObservers(window, "EM-loaded");
}
function uninit() {
AddonManager.removeInstallListener(Addons);
AddonManager.removeAddonListener(Addons);
Services.obs.removeObserver(sendEMPong, "EM-ping");
}
function openLink(url) {
let BrowserApp = gChromeWin.BrowserApp;
BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
}
function openOptionsInTab(url) {
let BrowserApp = gChromeWin.BrowserApp;
BrowserApp.selectOrAddTab(url, {
startsWith: true,
selected: true,
parentId: BrowserApp.selectedTab.id
});
}
function onPopState(aEvent) {
// Called when back/forward is used to change the state of the page
if (aEvent.state) {
// Show the detail page for an addon
const listItem = Addons._getElementForAddon(aEvent.state.id);
if (listItem) {
Addons.showDetails(listItem);
} else {
// If the addon doesn't exist anymore, go back in the history.
history.back();
}
} else {
// Clear any previous detail addon
let detailItem = document.querySelector("#addons-details > .addon-item");
detailItem.addon = null;
showAddons();
}
}
function showAddonDetails(addonId) {
const listItem = Addons._getElementForAddon(addonId);
if (listItem) {
Addons.showDetails(listItem);
history.pushState({ id: addonId }, document.title);
} else {
throw new Error(`Addon not found: ${addonId}`);
}
}
function showAddons() {
// Hide the addon options and show the addons list
let details = document.querySelector("#addons-details");
details.classList.add("hidden");
let list = document.querySelector("#addons-list");
list.classList.remove("hidden");
document.documentElement.removeAttribute("details");
// Clean the optionsBox content when switching to the add-ons list view.
let optionsBox = document.querySelector("#addons-details > .addon-item .options-box");
optionsBox.innerHTML = "";
}
function showAddonOptions() {
// Hide the addon list and show the addon options
let list = document.querySelector("#addons-list");
list.classList.add("hidden");
let details = document.querySelector("#addons-details");
details.classList.remove("hidden");
document.documentElement.setAttribute("details", "true");
}
var Addons = {
_restartCount: 0,
_createItem: function _createItem(aAddon) {
let outer = document.createElement("div");
outer.setAttribute("addonID", aAddon.id);
outer.className = "addon-item list-item";
outer.setAttribute("role", "button");
outer.setAttribute("contextmenu", "addonmenu");
outer.addEventListener("click", () => {
this.showDetails(outer);
history.pushState({ id: aAddon.id }, document.title);
}, true);
let img = document.createElement("img");
img.className = "icon";
img.setAttribute("src", aAddon.iconURL || AMO_ICON);
outer.appendChild(img);
let inner = document.createElement("div");
inner.className = "inner";
let details = document.createElement("div");
details.className = "details";
inner.appendChild(details);
let titlePart = document.createElement("div");
titlePart.textContent = aAddon.name;
titlePart.className = "title";
details.appendChild(titlePart);
let versionPart = document.createElement("div");
versionPart.textContent = aAddon.version;
versionPart.className = "version";
details.appendChild(versionPart);
if ("description" in aAddon) {
let descPart = document.createElement("div");
descPart.textContent = aAddon.description;
descPart.className = "description";
inner.appendChild(descPart);
}
outer.appendChild(inner);
let update = document.createElement("img");
update.className = "update-indicator";
update.setAttribute("src", UPDATE_INDICATOR);
outer.appendChild(update);
return outer;
},
_createBrowseItem: function _createBrowseItem() {
let outer = document.createElement("div");
outer.className = "addon-item list-item";
outer.setAttribute("role", "button");
outer.addEventListener("click", function(event) {
try {
openLink(Services.urlFormatter.formatURLPref("extensions.getAddons.browseAddons"));
} catch (e) {
Cu.reportError(e);
}
}, true);
let img = document.createElement("img");
img.className = "icon";
img.setAttribute("src", AMO_ICON);
outer.appendChild(img);
let inner = document.createElement("div");
inner.className = "inner";
let title = document.createElement("div");
title.id = "browse-title";
title.className = "title";
title.textContent = gStringBundle.GetStringFromName("addons.browseAll");
inner.appendChild(title);
outer.appendChild(inner);
return outer;
},
_createItemForAddon: function _createItemForAddon(aAddon) {
let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
let hasUpdate = this._addonHasUpdate(aAddon);
let optionsURL = aAddon.optionsURL || "";
let blocked = "";
switch (aAddon.blocklistState) {
case Ci.nsIBlocklistService.STATE_BLOCKED:
blocked = "blocked";
break;
case Ci.nsIBlocklistService.STATE_SOFTBLOCKED:
blocked = "softBlocked";
break;
case Ci.nsIBlocklistService.STATE_OUTDATED:
blocked = "outdated";
break;
}
let item = this._createItem(aAddon);
item.setAttribute("isDisabled", !aAddon.isActive);
item.setAttribute("isUnsigned", aAddon.signedState <= AddonManager.SIGNEDSTATE_MISSING);
item.setAttribute("opType", opType);
if (blocked)
item.setAttribute("blockedStatus", blocked);
item.setAttribute("optionsURL", optionsURL);
item.setAttribute("hasUpdate", hasUpdate);
item.addon = aAddon;
return item;
},
_getElementForAddon: function(aKey) {
let list = document.getElementById("addons-list");
let element = list.querySelector("div[addonID=\"" + CSS.escape(aKey) + "\"]");
return element;
},
_addonHasUpdate(addon) {
return gChromeWin.ExtensionPermissions.updates.has(addon.id);
},
init: async function init() {
const aAddons = await AddonManager.getAllAddons();
// Clear all content before filling the addons
let list = document.getElementById("addons-list");
list.innerHTML = "";
aAddons.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
for (let i = 0; i < aAddons.length; i++) {
// Don't create item for system add-ons.
if (aAddons[i].isSystem)
continue;
let item = this._createItemForAddon(aAddons[i]);
list.appendChild(item);
}
// Add a "Browse all Firefox Add-ons" item to the bottom of the list.
let browseItem = this._createBrowseItem();
list.appendChild(browseItem);
document.getElementById("update-btn").addEventListener("click", Addons.updateCurrent.bind(this));
document.getElementById("uninstall-btn").addEventListener("click", Addons.uninstallCurrent.bind(this));
document.getElementById("cancel-btn").addEventListener("click", Addons.cancelUninstall.bind(this));
document.getElementById("disable-btn").addEventListener("click", Addons.disable.bind(this));
document.getElementById("enable-btn").addEventListener("click", Addons.enable.bind(this));
document.getElementById("unsigned-learn-more").addEventListener("click", function() {
openLink(Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons");
});
},
_getOpTypeForOperations: function _getOpTypeForOperations(aOperations) {
if (aOperations & AddonManager.PENDING_UNINSTALL)
return "needs-uninstall";
if (aOperations & AddonManager.PENDING_ENABLE)
return "needs-enable";
if (aOperations & AddonManager.PENDING_DISABLE)
return "needs-disable";
return "";
},
showDetails: function showDetails(aListItem) {
let detailItem = document.querySelector("#addons-details > .addon-item");
detailItem.setAttribute("isDisabled", aListItem.getAttribute("isDisabled"));
detailItem.setAttribute("isUnsigned", aListItem.getAttribute("isUnsigned"));
detailItem.setAttribute("opType", aListItem.getAttribute("opType"));
detailItem.setAttribute("optionsURL", aListItem.getAttribute("optionsURL"));
let addon = detailItem.addon = aListItem.addon;
let favicon = document.querySelector("#addons-details > .addon-item .icon");
favicon.setAttribute("src", addon.iconURL || AMO_ICON);
detailItem.querySelector(".title").textContent = addon.name;
detailItem.querySelector(".version").textContent = addon.version;
detailItem.querySelector(".description-full").textContent = addon.description;
detailItem.querySelector(".status-uninstalled").textContent =
gStringBundle.formatStringFromName("addonStatus.uninstalled", [addon.name], 1);
let updateBtn = document.getElementById("update-btn");
if (this._addonHasUpdate(addon)) {
updateBtn.removeAttribute("hidden");
} else {
updateBtn.setAttribute("hidden", true);
}
let enableBtn = document.getElementById("enable-btn");
if (addon.appDisabled) {
enableBtn.setAttribute("disabled", "true");
} else {
enableBtn.removeAttribute("disabled");
}
let uninstallBtn = document.getElementById("uninstall-btn");
if (addon.scope == AddonManager.SCOPE_APPLICATION) {
uninstallBtn.setAttribute("disabled", "true");
} else {
uninstallBtn.removeAttribute("disabled");
}
let addonItem = document.querySelector("#addons-details > .addon-item");
let optionsBox = addonItem.querySelector(".options-box");
let optionsURL = aListItem.getAttribute("optionsURL");
// Always clean the options content before rendering the options of the
// newly selected extension.
optionsBox.innerHTML = "";
switch (parseInt(addon.optionsType)) {
case AddonManager.OPTIONS_TYPE_INLINE_BROWSER:
// Allow the options to use all the available width space.
optionsBox.classList.remove("inner");
this.createWebExtensionOptions(optionsBox, addon, addonItem);
break;
case AddonManager.OPTIONS_TYPE_TAB:
// Keep the usual layout for any options related the legacy (or system) add-ons
// when the options are opened in a new tab from a single button in the addon
// details page.
optionsBox.classList.add("inner");
this.createOptionsInTabButton(optionsBox, addon, addonItem);
break;
}
showAddonOptions();
},
createOptionsInTabButton: function(destination, addon, detailItem) {
let frame = destination.querySelector("iframe#addon-options");
let button = destination.querySelector("button#open-addon-options");
if (frame) {
// Remove any existent options frame (e.g. when the addon updates
// contains the open_in_tab options for the first time).
frame.remove();
}
if (!button) {
button = document.createElement("button");
button.setAttribute("id", "open-addon-options");
button.textContent = gStringBundle.GetStringFromName("addon.options");
destination.appendChild(button);
}
button.onclick = async () => {
if (addon.isWebExtension) {
// WebExtensions are loaded asynchronously and the optionsURL
// may not be available until the addon has been started.
await addon.startupPromise;
}
const {optionsURL} = addon;
openOptionsInTab(optionsURL);
};
// Ensure that the Addon Options are visible (the options box will be hidden if the optionsURL
// attribute is an empty string, which happens when a WebExtensions is still loading).
detailItem.removeAttribute("optionsURL");
},
createWebExtensionOptions: async function(destination, addon, detailItem) {
// WebExtensions are loaded asynchronously and the optionsURL
// may not be available until the addon has been started.
await addon.startupPromise;
const {optionsURL, optionsBrowserStyle} = addon;
let frame = destination.querySelector("iframe#addon-options");
if (!frame) {
let originalHeight;
frame = document.createElement("iframe");
frame.setAttribute("id", "addon-options");
frame.setAttribute("mozbrowser", "true");
frame.setAttribute("style", "width: 100%; overflow: hidden;");
// Adjust iframe height to the iframe content (also between navigation of multiple options
// files).
frame.onload = (evt) => {
if (evt.target !== frame) {
return;
}
const {document} = frame.contentWindow;
const bodyScrollHeight = document.body && document.body.scrollHeight;
const documentScrollHeight = document.documentElement.scrollHeight;
// Set the iframe height to the maximum between the body and the document
// scrollHeight values.
frame.style.height = Math.max(bodyScrollHeight, documentScrollHeight) + "px";
// Restore the original iframe height between option page loads,
// so that we don't force the new document to have the same size
// of the previosuly loaded option page.
frame.contentWindow.addEventListener("unload", () => {
frame.style.height = originalHeight + "px";
}, {once: true});
};
destination.appendChild(frame);
originalHeight = frame.getBoundingClientRect().height;
}
// Loading the URL this way prevents the native back
// button from applying to the iframe.
frame.contentWindow.location.replace(optionsURL);
// Ensure that the Addon Options are visible (the options box will be hidden if the optionsURL
// attribute is an empty string, which happens when a WebExtensions is still loading).
detailItem.removeAttribute("optionsURL");
},
setEnabled: function setEnabled(aValue, aAddon) {
let detailItem = document.querySelector("#addons-details > .addon-item");
let addon = aAddon || detailItem.addon;
if (!addon)
return;
let listItem = this._getElementForAddon(addon.id);
function setDisabled(addon, value) {
if (value) {
return addon.disable();
}
return addon.enable();
}
let opType;
if (addon.type == "theme") {
if (aValue) {
// We can have only one theme enabled, so disable the current one if any
let list = document.getElementById("addons-list");
let item = list.firstElementChild;
while (item) {
if (item.addon && (item.addon.type == "theme") && (item.addon.isActive)) {
item.addon.disable();
item.setAttribute("isDisabled", true);
break;
}
item = item.nextSibling;
}
}
setDisabled(addon, !aValue);
} else if (addon.type == "locale") {
setDisabled(addon, !aValue);
} else {
setDisabled(addon, !aValue);
opType = this._getOpTypeForOperations(addon.pendingOperations);
if ((addon.pendingOperations & AddonManager.PENDING_ENABLE) ||
(addon.pendingOperations & AddonManager.PENDING_DISABLE)) {
this.showRestart();
} else if (listItem && /needs-(enable|disable)/.test(listItem.getAttribute("opType"))) {
this.hideRestart();
}
}
if (addon == detailItem.addon) {
detailItem.setAttribute("isDisabled", !aValue);
if (opType)
detailItem.setAttribute("opType", opType);
else
detailItem.removeAttribute("opType");
// Remove any addon options iframe if the currently selected addon has been disabled.
if (!aValue) {
const addonOptionsIframe = document.querySelector("#addon-options");
if (addonOptionsIframe) {
addonOptionsIframe.remove();
}
}
}
// Sync to the list item
if (listItem) {
listItem.setAttribute("isDisabled", !aValue);
if (opType)
listItem.setAttribute("opType", opType);
else
listItem.removeAttribute("opType");
}
},
enable: function enable() {
this.setEnabled(true);
},
disable: function disable() {
this.setEnabled(false);
},
updateCurrent() {
let detailItem = document.querySelector("#addons-details > .addon-item");
let addon = detailItem.addon;
if (!addon)
return;
gChromeWin.ExtensionPermissions.applyUpdate(addon.id);
},
uninstallCurrent: function uninstallCurrent() {
let detailItem = document.querySelector("#addons-details > .addon-item");
let addon = detailItem.addon;
if (!addon)
return;
this.uninstall(addon);
},
uninstall: function uninstall(aAddon) {
if (!aAddon) {
return;
}
let listItem = this._getElementForAddon(aAddon.id);
aAddon.uninstall();
if (aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
this.showRestart();
// A disabled addon doesn't need a restart so it has no pending ops and
// can't be cancelled
let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
if (!aAddon.isActive && opType == "")
opType = "needs-uninstall";
detailItem.setAttribute("opType", opType);
listItem.setAttribute("opType", opType);
}
},
cancelUninstall: function ev_cancelUninstall() {
let detailItem = document.querySelector("#addons-details > .addon-item");
let addon = detailItem.addon;
if (!addon)
return;
addon.cancelUninstall();
this.hideRestart();
let opType = this._getOpTypeForOperations(addon.pendingOperations);
detailItem.setAttribute("opType", opType);
let listItem = this._getElementForAddon(addon.id);
listItem.setAttribute("opType", opType);
},
showRestart: function showRestart() {
this._restartCount++;
gChromeWin.XPInstallObserver.showRestartPrompt();
},
hideRestart: function hideRestart() {
this._restartCount--;
if (this._restartCount == 0)
gChromeWin.XPInstallObserver.hideRestartPrompt();
},
onEnabled: function(aAddon) {
let listItem = this._getElementForAddon(aAddon.id);
if (!listItem)
return;
// Reload the details to pick up any options now that it's enabled.
listItem.setAttribute("optionsURL", aAddon.optionsURL || "");
let detailItem = document.querySelector("#addons-details > .addon-item");
if (aAddon == detailItem.addon)
this.showDetails(listItem);
},
onInstallEnded: function(aInstall, aAddon) {
let needsRestart = false;
if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
needsRestart = true;
else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
needsRestart = true;
let list = document.getElementById("addons-list");
let element = this._getElementForAddon(aAddon.id);
if (!element) {
element = this._createItemForAddon(aAddon);
list.insertBefore(element, list.firstElementChild);
}
if (needsRestart)
element.setAttribute("opType", "needs-restart");
},
onInstalled: function(aAddon) {
let list = document.getElementById("addons-list");
let element = this._getElementForAddon(aAddon.id);
if (element) {
// Upgrade of an existing addon, update version and description in
// list item and detail view, plus indicators about a pending update.
element.querySelector(".version").textContent = aAddon.version;
let desc = element.querySelector(".description");
if (desc) {
desc.textContent = aAddon.description;
}
element.setAttribute("hasUpdate", false);
document.getElementById("update-btn").setAttribute("hidden", true);
element = document.querySelector("#addons-details > .addon-item");
if (element.addon && element.addon.id == aAddon.id) {
element.querySelector(".version").textContent = aAddon.version;
element.querySelector(".description-full").textContent = aAddon.description;
}
} else {
element = this._createItemForAddon(aAddon);
// Themes aren't considered active on install, so set existing as disabled, and new one enabled.
if (aAddon.type == "theme") {
let item = list.firstElementChild;
while (item) {
if (item.addon && (item.addon.type == "theme")) {
item.setAttribute("isDisabled", true);
}
item = item.nextSibling;
}
element.setAttribute("isDisabled", false);
}
list.insertBefore(element, list.firstElementChild);
}
},
onUninstalled: function(aAddon) {
let list = document.getElementById("addons-list");
let element = this._getElementForAddon(aAddon.id);
list.removeChild(element);
// Go back if we're in the detail view of the add-on that was uninstalled.
let detailItem = document.querySelector("#addons-details > .addon-item");
if (detailItem.addon.id == aAddon.id) {
history.back();
}
},
onInstallFailed: function(aInstall) {
},
onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {
},
onDownloadFailed: function(aInstall) {
},
onDownloadCancelled: function(aInstall) {
}
};
window.addEventListener("load", init);
window.addEventListener("unload", uninit);