--- a/toolkit/components/extensions/ext-browserSettings.js
+++ b/toolkit/components/extensions/ext-browserSettings.js
@@ -12,47 +12,67 @@ XPCOMUtils.defineLazyServiceGetter(this,
"nsIAboutNewTabService");
Cu.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
var {
ExtensionError,
} = ExtensionUtils;
+const proxySvc = Ci.nsIProtocolProxyService;
+
+const PROXY_TYPES_MAP = new Map([
+ ["none", proxySvc.PROXYCONFIG_DIRECT],
+ ["autoDetect", proxySvc.PROXYCONFIG_WPAD],
+ ["system", proxySvc.PROXYCONFIG_SYSTEM],
+ ["manual", proxySvc.PROXYCONFIG_MANUAL],
+ ["autoConfig", proxySvc.PROXYCONFIG_PAC],
+]);
+
const HOMEPAGE_OVERRIDE_SETTING = "homepage_override";
const HOMEPAGE_URL_PREF = "browser.startup.homepage";
const URL_STORE_TYPE = "url_overrides";
const NEW_TAB_OVERRIDE_SETTING = "newTabURL";
const PERM_DENY_ACTION = Services.perms.DENY_ACTION;
-const getSettingsAPI = (extension, name, callback, storeType, readOnly = false) => {
+const checkUnsupported = (name, unsupportedPlatforms) => {
+ if (unsupportedPlatforms.includes(AppConstants.platform)) {
+ throw new ExtensionError(
+ `${AppConstants.platform} is not a supported platform for the ${name} setting.`);
+ }
+};
+
+const getSettingsAPI = (extension, name, callback, storeType, readOnly = false, unsupportedPlatforms = []) => {
return {
async get(details) {
+ checkUnsupported(name, unsupportedPlatforms);
let levelOfControl = details.incognito ?
"not_controllable" :
await ExtensionPreferencesManager.getLevelOfControl(
extension.id, name, storeType);
levelOfControl =
(readOnly && levelOfControl === "controllable_by_this_extension") ?
"not_controllable" :
levelOfControl;
return {
levelOfControl,
value: await callback(),
};
},
set(details) {
+ checkUnsupported(name, unsupportedPlatforms);
if (!readOnly) {
return ExtensionPreferencesManager.setSetting(
extension.id, name, details.value);
}
return false;
},
clear(details) {
+ checkUnsupported(name, unsupportedPlatforms);
if (!readOnly) {
return ExtensionPreferencesManager.removeSetting(extension.id, name);
}
return false;
},
};
};
@@ -120,16 +140,66 @@ ExtensionPreferencesManager.addSetting("
"browser.search.openintab",
],
setCallback(value) {
return {[this.prefNames[0]]: value};
},
});
+ExtensionPreferencesManager.addSetting("proxyConfig", {
+ prefNames: [
+ "network.proxy.type",
+ "network.proxy.http",
+ "network.proxy.http_port",
+ "network.proxy.share_proxy_settings",
+ "network.proxy.ftp",
+ "network.proxy.ftp_port",
+ "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",
+ ],
+
+ setCallback(value) {
+ let prefs = {
+ "network.proxy.type": PROXY_TYPES_MAP.get(value.proxyType),
+ "signon.autologin.proxy": value.autoLogin,
+ "network.proxy.socks_remote_dns": value.proxyDNS,
+ "network.proxy.autoconfig_url": value.autoConfigUrl,
+ "network.proxy.share_proxy_settings": value.httpProxyAll,
+ "network.proxy.socks_version": value.socksVersion,
+ "network.proxy.no_proxies_on": value.passthrough,
+ };
+
+ for (let prop of ["http", "ftp", "ssl", "socks"]) {
+ if (value[prop]) {
+ let url = new URL(prop === "socks" ?
+ `http://${value[prop]}` :
+ value[prop]);
+ prefs[`network.proxy.${prop}`] = prop === "socks" ?
+ url.hostname :
+ `${url.protocol}//${url.hostname}`;
+ let port = parseInt(url.port, 10);
+ prefs[`network.proxy.${prop}_port`] = isNaN(port) ? 0 : port;
+ } else {
+ prefs[`network.proxy.${prop}`] = undefined;
+ prefs[`network.proxy.${prop}_port`] = undefined;
+ }
+ }
+
+ return prefs;
+ },
+});
+
ExtensionPreferencesManager.addSetting("webNotificationsDisabled", {
prefNames: [
"permissions.default.desktop-notification",
],
setCallback(value) {
return {[this.prefNames[0]]: value ? PERM_DENY_ACTION : undefined};
},
@@ -200,16 +270,96 @@ this.browserSettings = class extends Ext
() => {
return Services.prefs.getBoolPref("browser.tabs.loadBookmarksInTabs");
}),
openSearchResultsInNewTabs: getSettingsAPI(
extension, "openSearchResultsInNewTabs",
() => {
return Services.prefs.getBoolPref("browser.search.openintab");
}),
+ proxyConfig: Object.assign(
+ getSettingsAPI(
+ extension, "proxyConfig",
+ () => {
+ let prefValue = Services.prefs.getIntPref("network.proxy.type");
+ let proxyConfig = {
+ proxyType:
+ Array.from(
+ PROXY_TYPES_MAP.entries()).find(entry => entry[1] === prefValue)[0],
+ autoConfigUrl: Services.prefs.getCharPref("network.proxy.autoconfig_url"),
+ autoLogin: Services.prefs.getBoolPref("signon.autologin.proxy"),
+ proxyDNS: Services.prefs.getBoolPref("network.proxy.socks_remote_dns"),
+ httpProxyAll: Services.prefs.getBoolPref("network.proxy.share_proxy_settings"),
+ socksVersion: Services.prefs.getIntPref("network.proxy.socks_version"),
+ passthrough: Services.prefs.getCharPref("network.proxy.no_proxies_on"),
+ };
+
+ for (let prop of ["http", "ftp", "ssl", "socks"]) {
+ let url = Services.prefs.getCharPref(`network.proxy.${prop}`);
+ let port = Services.prefs.getIntPref(`network.proxy.${prop}_port`);
+ proxyConfig[prop] = port ? `${url}:${port}` : url;
+ }
+
+ return proxyConfig;
+ },
+ // proxyConfig is unsupported on android.
+ undefined, false, ["android"]
+ ),
+ {
+ set: details => {
+ if (AppConstants.platform === "android") {
+ throw new ExtensionError(
+ "proxyConfig is not supported on android.");
+ }
+
+ let value = details.value;
+
+ if (!PROXY_TYPES_MAP.has(value.proxyType)) {
+ throw new ExtensionError(
+ `${value.proxyType} is not a valid value for proxyType.`);
+ }
+
+ for (let prop of ["http", "ftp", "ssl", "socks"]) {
+ let url = value[prop];
+ if (url) {
+ if (prop === "socks") {
+ url = `http://${url}`;
+ }
+ try {
+ new URL(url);
+ } catch (e) {
+ throw new ExtensionError(
+ `${value[prop]} is not a valid value for ${prop}.`);
+ }
+ }
+ }
+
+ if (value.proxyType === "autoConfig" || value.autoConfigUrl) {
+ try {
+ new URL(value.autoConfigUrl);
+ } catch (e) {
+ throw new ExtensionError(
+ `${value.autoConfigUrl} is not a valid value for autoConfigUrl.`);
+ }
+ }
+
+ if (value.socksVersion !== undefined) {
+ if (!Number.isInteger(value.socksVersion) ||
+ value.socksVersion < 4 ||
+ value.socksVersion > 5) {
+ throw new ExtensionError(
+ `${value.socksVersion} is not a valid value for socksVersion.`);
+ }
+ }
+
+ return ExtensionPreferencesManager.setSetting(
+ extension.id, "proxyConfig", value);
+ },
+ }
+ ),
webNotificationsDisabled: getSettingsAPI(
extension, "webNotificationsDisabled",
() => {
let prefValue =
Services.prefs.getIntPref(
"permissions.default.desktop-notification", null);
return prefValue === PERM_DENY_ACTION;
}),
--- a/toolkit/components/extensions/schemas/browser_settings.json
+++ b/toolkit/components/extensions/schemas/browser_settings.json
@@ -3,22 +3,24 @@
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "OptionalPermission",
- "choices": [{
- "type": "string",
- "enum": [
- "browserSettings"
- ]
- }]
+ "choices": [
+ {
+ "type": "string",
+ "enum": [
+ "browserSettings"
+ ]
+ }
+ ]
}
]
},
{
"namespace": "browserSettings",
"description": "Use the <code>browser.browserSettings</code> API to control global settings of the browser.",
"permissions": ["browserSettings"],
"types": [
@@ -28,16 +30,87 @@
"enum": ["normal", "none", "once"],
"description": "How images should be animated in the browser."
},
{
"id": "ContextMenuMouseEvent",
"type": "string",
"enum": ["mouseup", "mousedown"],
"description": "After which mouse event context menus should popup."
+ },
+ {
+ "id": "ProxyConfig",
+ "type": "object",
+ "description": "An object which describes proxy settings.",
+ "properties": {
+ "proxyType": {
+ "type": "string",
+ "optional": true,
+ "enum": [
+ "none",
+ "autoDetect",
+ "system",
+ "manual",
+ "autoConfig"
+ ],
+ "description": "The type of proxy to use."
+ },
+ "http": {
+ "type": "string",
+ "optional": true,
+ "description": "The address of the http proxy, can include a port."
+ },
+ "httpProxyAll":{
+ "type": "boolean",
+ "optional": true,
+ "description": "Use the http proxy server for all protocols."
+ },
+ "ftp": {
+ "type": "string",
+ "optional": true,
+ "description": "The address of the ftp proxy, can include a port."
+ },
+ "ssl": {
+ "type": "string",
+ "optional": true,
+ "description": "The address of the ssl proxy, can include a port."
+ },
+ "socks": {
+ "type": "string",
+ "optional": true,
+ "description": "The address of the socks proxy, can include a port."
+ },
+ "socksVersion": {
+ "type": "integer",
+ "optional": true,
+ "description": "The version of the socks proxy.",
+ "minimum": 4,
+ "maximum": 5
+ },
+ "passthrough": {
+ "type": "string",
+ "optional": true,
+ "description": "A list of hosts which should not be proxied."
+ },
+ "autoConfigUrl": {
+ "type": "string",
+ "optional": true,
+ "description": "A URL to use to configure the proxy."
+ },
+ "autoLogin": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Do not prompt for authentication if password is saved."
+ },
+ "proxyDNS": {
+ "type": "boolean",
+ "optional": true,
+ "description": "Proxy DNS when using SOCKS v5."
+ }
+ }
}
],
"properties": {
"allowPopupsForUserEvents": {
"$ref": "types.Setting",
"description": "Allows or disallows pop-up windows from opening in response to user events."
},
"cacheEnabled": {
@@ -63,15 +136,19 @@
"openBookmarksInNewTabs": {
"$ref": "types.Setting",
"description": "This boolean setting controls whether bookmarks are opened in the current tab or in a new tab."
},
"openSearchResultsInNewTabs": {
"$ref": "types.Setting",
"description": "This boolean setting controls whether search results are opened in the current tab or in a new tab."
},
+ "proxyConfig": {
+ "$ref": "types.Setting",
+ "description": "Configures proxy settings. This setting's value is an object of type ProxyConfig."
+ },
"webNotificationsDisabled": {
"$ref": "types.Setting",
"description": "Disables webAPI notifications."
}
}
}
]
--- a/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_browserSettings.js
@@ -13,29 +13,45 @@ const {
promiseStartupManager,
} = AddonTestUtils;
AddonTestUtils.init(this);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
add_task(async function test_browser_settings() {
+ const proxySvc = Ci.nsIProtocolProxyService;
const PERM_DENY_ACTION = Services.perms.DENY_ACTION;
const PERM_UNKNOWN_ACTION = Services.perms.UNKNOWN_ACTION;
// Create an object to hold the values to which we will initialize the prefs.
const PREFS = {
"browser.cache.disk.enable": true,
"browser.cache.memory.enable": true,
"dom.popup_allowed_events": Preferences.get("dom.popup_allowed_events"),
"image.animation_mode": "none",
"permissions.default.desktop-notification": PERM_UNKNOWN_ACTION,
"ui.context_menus.after_mouseup": false,
"browser.tabs.loadBookmarksInTabs": false,
"browser.search.openintab": false,
+ "network.proxy.type": proxySvc.PROXYCONFIG_SYSTEM,
+ "network.proxy.http": "",
+ "network.proxy.http_port": 0,
+ "network.proxy.share_proxy_settings": false,
+ "network.proxy.ftp": "",
+ "network.proxy.ftp_port": 0,
+ "network.proxy.ssl": "",
+ "network.proxy.ssl_port": 0,
+ "network.proxy.socks": "",
+ "network.proxy.socks_port": 0,
+ "network.proxy.socks_version": 5,
+ "network.proxy.socks_remote_dns": false,
+ "network.proxy.no_proxies_on": "localhost, 127.0.0.1",
+ "network.proxy.autoconfig_url": "",
+ "signon.autologin.proxy": false,
};
async function background() {
browser.test.onMessage.addListener(async (msg, apiName, value) => {
let apiObj = browser.browserSettings[apiName];
let result = await apiObj.set({value});
if (msg === "set") {
browser.test.assertTrue(result, "set returns true.");
@@ -65,21 +81,21 @@ add_task(async function test_browser_set
permissions: ["browserSettings"],
},
useAddonManager: "temporary",
});
await promiseStartupManager();
await extension.startup();
- async function testSetting(setting, value, expected) {
+ async function testSetting(setting, value, expected, expectedValue = value) {
extension.sendMessage("set", setting, value);
let data = await extension.awaitMessage("settingData");
- equal(data.value, value,
- `The ${setting} setting has the expected value.`);
+ deepEqual(data.value, expectedValue,
+ `The ${setting} setting has the expected value.`);
equal(data.levelOfControl, "controlled_by_this_extension",
`The ${setting} setting has the expected levelOfControl.`);
for (let pref in expected) {
equal(Preferences.get(pref), expected[pref], `${pref} set correctly for ${value}`);
}
}
async function testNoOpSetting(setting, value, expected) {
@@ -157,16 +173,163 @@ add_task(async function test_browser_set
await testSetting(
"openSearchResultsInNewTabs", true,
{"browser.search.openintab": true});
await testSetting(
"openSearchResultsInNewTabs", false,
{"browser.search.openintab": false});
+ async function testProxy(config, expectedPrefs) {
+ // proxyConfig is not supported on Android.
+ if (AppConstants.platform === "android") {
+ return Promise.resolve();
+ }
+
+ let proxyConfig = {
+ proxyType: "system",
+ autoConfigUrl: "",
+ autoLogin: false,
+ proxyDNS: false,
+ httpProxyAll: false,
+ socksVersion: 5,
+ passthrough: "localhost, 127.0.0.1",
+ http: "",
+ ftp: "",
+ ssl: "",
+ socks: "",
+ };
+ return testSetting(
+ "proxyConfig", config, expectedPrefs, Object.assign(proxyConfig, config)
+ );
+ }
+
+ await testProxy(
+ {proxyType: "none"},
+ {"network.proxy.type": proxySvc.PROXYCONFIG_DIRECT},
+ );
+
+ await testProxy(
+ {
+ proxyType: "autoDetect",
+ autoLogin: true,
+ proxyDNS: true,
+ },
+ {
+ "network.proxy.type": proxySvc.PROXYCONFIG_WPAD,
+ "signon.autologin.proxy": true,
+ "network.proxy.socks_remote_dns": true,
+ },
+ );
+
+ await testProxy(
+ {
+ proxyType: "system",
+ autoLogin: false,
+ proxyDNS: false,
+ },
+ {
+ "network.proxy.type": proxySvc.PROXYCONFIG_SYSTEM,
+ "signon.autologin.proxy": false,
+ "network.proxy.socks_remote_dns": false,
+ },
+ );
+
+ await testProxy(
+ {
+ proxyType: "autoConfig",
+ autoConfigUrl: "http://mozilla.org",
+ },
+ {
+ "network.proxy.type": proxySvc.PROXYCONFIG_PAC,
+ "network.proxy.autoconfig_url": "http://mozilla.org",
+ },
+ );
+
+ await testProxy(
+ {
+ proxyType: "manual",
+ http: "http://www.mozilla.org",
+ autoConfigUrl: "",
+ },
+ {
+ "network.proxy.type": proxySvc.PROXYCONFIG_MANUAL,
+ "network.proxy.http": "http://www.mozilla.org",
+ "network.proxy.http_port": 0,
+ "network.proxy.autoconfig_url": "",
+ }
+ );
+
+ await testProxy(
+ {
+ proxyType: "manual",
+ http: "http://www.mozilla.org:8080",
+ httpProxyAll: true,
+ },
+ {
+ "network.proxy.type": proxySvc.PROXYCONFIG_MANUAL,
+ "network.proxy.http": "http://www.mozilla.org",
+ "network.proxy.http_port": 8080,
+ "network.proxy.share_proxy_settings": true,
+ }
+ );
+
+ await testProxy(
+ {
+ proxyType: "manual",
+ http: "http://www.mozilla.org:8080",
+ httpProxyAll: false,
+ ftp: "http://www.mozilla.org:8081",
+ ssl: "http://www.mozilla.org:8082",
+ socks: "mozilla.org:8083",
+ socksVersion: 4,
+ passthrough: ".mozilla.org",
+ },
+ {
+ "network.proxy.type": proxySvc.PROXYCONFIG_MANUAL,
+ "network.proxy.http": "http://www.mozilla.org",
+ "network.proxy.http_port": 8080,
+ "network.proxy.share_proxy_settings": false,
+ "network.proxy.ftp": "http://www.mozilla.org",
+ "network.proxy.ftp_port": 8081,
+ "network.proxy.ssl": "http://www.mozilla.org",
+ "network.proxy.ssl_port": 8082,
+ "network.proxy.socks": "mozilla.org",
+ "network.proxy.socks_port": 8083,
+ "network.proxy.socks_version": 4,
+ "network.proxy.no_proxies_on": ".mozilla.org",
+ }
+ );
+
+ // Test resetting values.
+ await testProxy(
+ {
+ proxyType: "none",
+ http: "",
+ ftp: "",
+ ssl: "",
+ socks: "",
+ socksVersion: 5,
+ passthrough: "",
+ },
+ {
+ "network.proxy.type": proxySvc.PROXYCONFIG_DIRECT,
+ "network.proxy.http": "",
+ "network.proxy.http_port": 0,
+ "network.proxy.ftp": "",
+ "network.proxy.ftp_port": 0,
+ "network.proxy.ssl": "",
+ "network.proxy.ssl_port": 0,
+ "network.proxy.socks": "",
+ "network.proxy.socks_port": 0,
+ "network.proxy.socks_version": 5,
+ "network.proxy.no_proxies_on": "",
+ }
+ );
+
await extension.unload();
await promiseShutdownManager();
});
add_task(async function test_bad_value() {
async function background() {
await browser.test.assertRejects(
browser.browserSettings.contextMenuShowEvent.set({value: "bad"}),
@@ -182,8 +345,93 @@ add_task(async function test_bad_value()
permissions: ["browserSettings"],
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});
+
+add_task(async function test_bad_value_proxy_config() {
+ let background = AppConstants.platform === "android" ?
+ async () => {
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.set({value: {
+ proxyType: "none",
+ }}),
+ /proxyConfig is not supported on android/,
+ "proxyConfig.set rejects on Android.");
+
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.get({}),
+ /android is not a supported platform for the proxyConfig setting/,
+ "proxyConfig.get rejects on Android.");
+
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.clear({}),
+ /android is not a supported platform for the proxyConfig setting/,
+ "proxyConfig.clear rejects on Android.");
+
+ browser.test.sendMessage("done");
+ } :
+ async () => {
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.set({value: {
+ proxyType: "abc",
+ }}),
+ /abc is not a valid value for proxyType/,
+ "proxyConfig.set rejects with an invalid proxyType value.");
+
+ for (let protocol of ["http", "ftp", "ssl"]) {
+ let value = {proxyType: "manual"};
+ value[protocol] = "abc";
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.set({value}),
+ `abc is not a valid value for ${protocol}.`,
+ `proxyConfig.set rejects with an invalid ${protocol} value.`);
+ }
+
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.set({value: {
+ proxyType: "autoConfig",
+ }}),
+ /undefined is not a valid value for autoConfigUrl/,
+ "proxyConfig.set for type autoConfig rejects with an empty autoConfigUrl value.");
+
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.set({value: {
+ proxyType: "autoConfig",
+ autoConfigUrl: "abc",
+ }}),
+ /abc is not a valid value for autoConfigUrl/,
+ "proxyConfig.set rejects with an invalid autoConfigUrl value.");
+
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.set({value: {
+ proxyType: "manual",
+ socksVersion: "abc",
+ }}),
+ /abc is not a valid value for socksVersion/,
+ "proxyConfig.set rejects with an invalid socksVersion value.");
+
+ await browser.test.assertRejects(
+ browser.browserSettings.proxyConfig.set({value: {
+ proxyType: "manual",
+ socksVersion: 3,
+ }}),
+ /3 is not a valid value for socksVersion/,
+ "proxyConfig.set rejects with an invalid socksVersion value.");
+
+ browser.test.sendMessage("done");
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ background,
+ manifest: {
+ permissions: ["browserSettings"],
+ },
+ });
+
+ await extension.startup();
+ await extension.awaitMessage("done");
+ await extension.unload();
+});