summaryrefslogtreecommitdiffstats
path: root/browser/components/preferences/extensionControlled.js
blob: 3c6f78ab4246b0fae8f12ae163cc906da2708cd9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
/* - 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/. */

/* import-globals-from preferences.js */

"use strict";

var { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

// Note: we get loaded in dialogs so we need to define our
// own getters, separate from preferences.js .
ChromeUtils.defineESModuleGetters(this, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",

  ExtensionPreferencesManager:
    "resource://gre/modules/ExtensionPreferencesManager.sys.mjs",

  ExtensionSettingsStore:
    "resource://gre/modules/ExtensionSettingsStore.sys.mjs",

  Management: "resource://gre/modules/Extension.sys.mjs",
});

const PREF_SETTING_TYPE = "prefs";
const PROXY_KEY = "proxy.settings";
const API_PROXY_PREFS = [
  "network.proxy.type",
  "network.proxy.http",
  "network.proxy.http_port",
  "network.proxy.share_proxy_settings",
  "network.proxy.ssl",
  "network.proxy.ssl_port",
  "network.proxy.socks",
  "network.proxy.socks_port",
  "network.proxy.socks_version",
  "network.proxy.socks_remote_dns",
  "network.proxy.no_proxies_on",
  "network.proxy.autoconfig_url",
  "signon.autologin.proxy",
];

let extensionControlledContentIds = {
  "privacy.containers": "browserContainersExtensionContent",
  webNotificationsDisabled: "browserNotificationsPermissionExtensionContent",
  "services.passwordSavingEnabled": "passwordManagerExtensionContent",
  "proxy.settings": "proxyExtensionContent",
  get "websites.trackingProtectionMode"() {
    return {
      button: "contentBlockingDisableTrackingProtectionExtension",
      section: "contentBlockingTrackingProtectionExtensionContentLabel",
    };
  },
};

const extensionControlledL10nKeys = {
  webNotificationsDisabled: "web-notifications",
  "services.passwordSavingEnabled": "password-saving",
  "privacy.containers": "privacy-containers",
  "websites.trackingProtectionMode": "websites-content-blocking-all-trackers",
  "proxy.settings": "proxy-config",
};

let extensionControlledIds = {};

/**
 * Check if a pref is being managed by an extension.
 */
async function getControllingExtensionInfo(type, settingName) {
  await ExtensionSettingsStore.initialize();
  return ExtensionSettingsStore.getSetting(type, settingName);
}

function getControllingExtensionEls(settingName) {
  let idInfo = extensionControlledContentIds[settingName];
  let section = document.getElementById(idInfo.section || idInfo);
  let button = idInfo.button
    ? document.getElementById(idInfo.button)
    : section.querySelector("button");
  return {
    section,
    button,
    description: section.querySelector("description"),
  };
}

async function getControllingExtension(type, settingName) {
  let info = await getControllingExtensionInfo(type, settingName);
  let addon = info && info.id && (await AddonManager.getAddonByID(info.id));
  return addon;
}

async function handleControllingExtension(type, settingName) {
  let addon = await getControllingExtension(type, settingName);

  // Sometimes the ExtensionSettingsStore gets in a bad state where it thinks
  // an extension is controlling a setting but the extension has been uninstalled
  // outside of the regular lifecycle. If the extension isn't currently installed
  // then we should treat the setting as not being controlled.
  // See https://bugzilla.mozilla.org/show_bug.cgi?id=1411046 for an example.
  if (addon) {
    extensionControlledIds[settingName] = addon.id;
    showControllingExtension(settingName, addon);
  } else {
    let elements = getControllingExtensionEls(settingName);
    if (
      extensionControlledIds[settingName] &&
      !document.hidden &&
      elements.button
    ) {
      showEnableExtensionMessage(settingName);
    } else {
      hideControllingExtension(settingName);
    }
    delete extensionControlledIds[settingName];
  }

  return !!addon;
}

function settingNameToL10nID(settingName) {
  if (!extensionControlledL10nKeys.hasOwnProperty(settingName)) {
    throw new Error(
      `Unknown extension controlled setting name: ${settingName}`
    );
  }
  return `extension-controlling-${extensionControlledL10nKeys[settingName]}`;
}

/**
 * Set the localization data for the description of the controlling extension.
 *
 * The function alters the inner DOM structure of the fragment to, depending
 * on the `addon` argument, remove the `<img/>` element or ensure it's
 * set to the correct src.
 * This allows Fluent DOM Overlays to localize the fragment.
 *
 * @param elem {Element}
 *        <description> element to be annotated
 * @param addon {Object?}
 *        Addon object with meta information about the addon (or null)
 * @param settingName {String}
 *        If `addon` is set this handled the name of the setting that will be used
 *        to fetch the l10n id for the given message.
 *        If `addon` is set to null, this will be the full l10n-id assigned to the
 *        element.
 */
function setControllingExtensionDescription(elem, addon, settingName) {
  const existingImg = elem.querySelector("img");
  if (addon === null) {
    // If the element has an image child element,
    // remove it.
    if (existingImg) {
      existingImg.remove();
    }
    document.l10n.setAttributes(elem, settingName);
    return;
  }

  const defaultIcon = "chrome://mozapps/skin/extensions/extensionGeneric.svg";
  const src = addon.iconURL || defaultIcon;

  if (!existingImg) {
    // If an element doesn't have an image child
    // node, add it.
    let image = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
    image.setAttribute("src", src);
    image.setAttribute("data-l10n-name", "icon");
    image.setAttribute("role", "presentation");
    image.classList.add("extension-controlled-icon");
    elem.appendChild(image);
  } else if (existingImg.getAttribute("src") !== src) {
    existingImg.setAttribute("src", src);
  }

  const l10nId = settingNameToL10nID(settingName);
  document.l10n.setAttributes(elem, l10nId, {
    name: addon.name,
  });
}

async function showControllingExtension(settingName, addon) {
  // Tell the user what extension is controlling the setting.
  let elements = getControllingExtensionEls(settingName);

  elements.section.classList.remove("extension-controlled-disabled");
  let description = elements.description;

  setControllingExtensionDescription(description, addon, settingName);

  if (elements.button) {
    elements.button.hidden = false;
  }

  // Show the controlling extension row and hide the old label.
  elements.section.hidden = false;
}

function hideControllingExtension(settingName) {
  let elements = getControllingExtensionEls(settingName);
  elements.section.hidden = true;
  if (elements.button) {
    elements.button.hidden = true;
  }
}

function showEnableExtensionMessage(settingName) {
  let elements = getControllingExtensionEls(settingName);

  elements.button.hidden = true;
  elements.section.classList.add("extension-controlled-disabled");

  elements.description.textContent = "";

  // We replace localization of the <description> with a DOM Fragment containing
  // the enable-extension-enable message. That means a change from:
  //
  // <description data-l10n-id="..."/>
  //
  // to:
  //
  // <description>
  //   <img/>
  //   <label data-l10n-id="..."/>
  // </description>
  //
  // We need to remove the l10n-id annotation from the <description> to prevent
  // Fluent from overwriting the element in case of any retranslation.
  elements.description.removeAttribute("data-l10n-id");

  let icon = (url, name) => {
    let img = document.createElementNS("http://www.w3.org/1999/xhtml", "img");
    img.src = url;
    img.setAttribute("data-l10n-name", name);
    img.setAttribute("role", "presentation");
    img.className = "extension-controlled-icon";
    return img;
  };
  let label = document.createXULElement("label");
  let addonIcon = icon(
    "chrome://mozapps/skin/extensions/extensionGeneric.svg",
    "addons-icon"
  );
  let toolbarIcon = icon("chrome://browser/skin/menu.svg", "menu-icon");
  label.appendChild(addonIcon);
  label.appendChild(toolbarIcon);
  document.l10n.setAttributes(label, "extension-controlled-enable");
  elements.description.appendChild(label);
  let dismissButton = document.createXULElement("image");
  dismissButton.setAttribute("class", "extension-controlled-icon close-icon");
  dismissButton.addEventListener("click", function dismissHandler() {
    hideControllingExtension(settingName);
    dismissButton.removeEventListener("click", dismissHandler);
  });
  elements.description.appendChild(dismissButton);
}

function makeDisableControllingExtension(type, settingName) {
  return async function disableExtension() {
    let { id } = await getControllingExtensionInfo(type, settingName);
    let addon = await AddonManager.getAddonByID(id);
    await addon.disable();
  };
}

/**
 *  Initialize listeners though the Management API to update the UI
 *  when an extension is controlling a pref.
 * @param {string} type
 * @param {string} prefId The unique id of the setting
 * @param {HTMLElement} controlledElement
 */
async function initListenersForPrefChange(type, prefId, controlledElement) {
  await Management.asyncLoadSettingsModules();

  let managementObserver = async () => {
    let managementControlled = await handleControllingExtension(type, prefId);
    // Enterprise policy may have locked the pref, so we need to preserve that
    controlledElement.disabled =
      managementControlled || Services.prefs.prefIsLocked(prefId);
  };
  managementObserver();
  Management.on(`extension-setting-changed:${prefId}`, managementObserver);

  window.addEventListener("unload", () => {
    Management.off(`extension-setting-changed:${prefId}`, managementObserver);
  });
}

function initializeProxyUI(container) {
  let deferredUpdate = new DeferredTask(() => {
    container.updateProxySettingsUI();
  }, 10);
  let proxyObserver = {
    observe: (subject, topic, data) => {
      if (API_PROXY_PREFS.includes(data)) {
        deferredUpdate.arm();
      }
    },
  };
  Services.prefs.addObserver("", proxyObserver);
  window.addEventListener("unload", () => {
    Services.prefs.removeObserver("", proxyObserver);
  });
}