author | Sebastian Hengst <archaeopteryx@coole-files.de> |
Mon, 05 Jun 2017 11:05:49 +0200 | |
changeset 362272 | 275588f4d852d7dc183a9dcc70a311413dc7a063 |
parent 362237 | 7d5df2dead2c9edaa41688a82d9d9dcc86529ef2 (current diff) |
parent 362271 | da5a14ae4ce3084aa63a502e96af1a4c6ef76853 (diff) |
child 362273 | 6d4b65abc362bd5f7b6406d3b98fa538a1f492eb |
child 362290 | 81e0a353d2dc82c8ea71a5dc26956f292a867895 |
child 362318 | 43383407d7c14ca3f1ee006e2b57ec5fe3ff0d3e |
push id | 31966 |
push user | archaeopteryx@coole-files.de |
push date | Mon, 05 Jun 2017 09:06:08 +0000 |
treeherder | mozilla-central@275588f4d852 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge, merge |
milestone | 55.0a1 |
first release with | nightly linux32
275588f4d852
/
55.0a1
/
20170605100313
/
files
nightly linux64
275588f4d852
/
55.0a1
/
20170605100313
/
files
nightly mac
275588f4d852
/
55.0a1
/
20170605030204
/
files
nightly win32
275588f4d852
/
55.0a1
/
20170605030204
/
files
nightly win64
275588f4d852
/
55.0a1
/
20170605030204
/
files
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
releases | nightly linux32
55.0a1
/
20170605100313
/
pushlog to previous
nightly linux64
55.0a1
/
20170605100313
/
pushlog to previous
nightly mac
55.0a1
/
20170605030204
/
pushlog to previous
nightly win32
55.0a1
/
20170605030204
/
pushlog to previous
nightly win64
55.0a1
/
20170605030204
/
pushlog to previous
|
--- a/browser/components/extensions/ext-contextMenus.js +++ b/browser/components/extensions/ext-contextMenus.js @@ -1,14 +1,12 @@ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; -Cu.import("resource://gre/modules/ExtensionManagement.jsm"); -Cu.import("resource://gre/modules/MatchPattern.jsm"); Cu.import("resource://gre/modules/Services.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); var { ExtensionError, IconDetails, @@ -376,21 +374,21 @@ MenuItem.prototype = { if (createProperties[propName] === null) { // Omitted optional argument. continue; } this[propName] = createProperties[propName]; } if (createProperties.documentUrlPatterns != null) { - this.documentUrlMatchPattern = new MatchPattern(this.documentUrlPatterns); + this.documentUrlMatchPattern = new MatchPatternSet(this.documentUrlPatterns); } if (createProperties.targetUrlPatterns != null) { - this.targetUrlMatchPattern = new MatchPattern(this.targetUrlPatterns); + this.targetUrlMatchPattern = new MatchPatternSet(this.targetUrlPatterns); } // If a child MenuItem does not specify any contexts, then it should // inherit the contexts specified from its parent. if (createProperties.parentId && !createProperties.contexts) { this.contexts = this.parent.contexts; } },
--- a/browser/components/extensions/ext-tabs.js +++ b/browser/components/extensions/ext-tabs.js @@ -1,18 +1,16 @@ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService", "@mozilla.org/browser/aboutnewtab-service;1", "nsIAboutNewTabService"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", "resource://gre/modules/PromiseUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); let tabListener = { @@ -473,17 +471,17 @@ this.tabs = class extends ExtensionAPI { async query(queryInfo) { if (queryInfo.url !== null) { if (!extension.hasPermission("tabs")) { return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'}); } queryInfo = Object.assign({}, queryInfo); - queryInfo.url = new MatchPattern(queryInfo.url); + queryInfo.url = new MatchPatternSet([].concat(queryInfo.url)); } return Array.from(tabManager.query(queryInfo, context), tab => tab.convert()); }, async captureVisibleTab(windowId, options) { let window = windowId == null ?
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js +++ b/browser/components/extensions/test/browser/browser_ext_contextMenus_urlPatterns.js @@ -97,60 +97,60 @@ add_task(async function() { title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextAll", documentUrlPatterns: ["*://*/*context.html"], targetUrlPatterns: ["*://*/*ctxmenu-image.png"], contexts: ["all"], }); browser.contextMenus.create({ title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextAll", - documentUrlPatterns: ["*://does-not-match"], + documentUrlPatterns: ["*://*/does-not-match"], targetUrlPatterns: ["*://*/*ctxmenu-image.png"], contexts: ["all"], }); browser.contextMenus.create({ title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextAll", documentUrlPatterns: ["*://*/*context.html"], - targetUrlPatterns: ["*://does-not-match"], + targetUrlPatterns: ["*://*/does-not-match"], contexts: ["all"], }); browser.contextMenus.create({ title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextAll", - documentUrlPatterns: ["*://does-not-match"], - targetUrlPatterns: ["*://does-not-match"], + documentUrlPatterns: ["*://*/does-not-match"], + targetUrlPatterns: ["*://*/does-not-match"], contexts: ["all"], }); browser.contextMenus.create({ title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternMatches-contextImage", documentUrlPatterns: ["*://*/*context.html"], targetUrlPatterns: ["*://*/*ctxmenu-image.png"], contexts: ["image"], }); browser.contextMenus.create({ title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternMatches-contextImage", - documentUrlPatterns: ["*://does-not-match"], + documentUrlPatterns: ["*://*/does-not-match"], targetUrlPatterns: ["*://*/*ctxmenu-image.png"], contexts: ["image"], }); browser.contextMenus.create({ title: "documentUrlPatterns-patternMatches-targetUrlPatterns-patternDoesNotMatch-contextImage", documentUrlPatterns: ["*://*/*context.html"], - targetUrlPatterns: ["*://does-not-match"], + targetUrlPatterns: ["*://*/does-not-match"], contexts: ["image"], }); browser.contextMenus.create({ title: "documentUrlPatterns-patternDoesNotMatch-targetUrlPatterns-patternDoesNotMatch-contextImage", - documentUrlPatterns: ["*://does-not-match"], - targetUrlPatterns: ["*://does-not-match"], + documentUrlPatterns: ["*://*/does-not-match/"], + targetUrlPatterns: ["*://*/does-not-match"], contexts: ["image"], }); browser.test.notifyPass("contextmenus-urlPatterns"); }, }); function confirmContextMenuItems(menu, expected) {
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js @@ -1,14 +1,16 @@ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; +XPCOMUtils.defineLazyPreferenceGetter(this, "useRemoteWebExtensions", + "extensions.webextensions.remote", false); + add_task(async function testExecuteScript() { - let {ExtensionManagement} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); let {MessageChannel} = Cu.import("resource://gre/modules/MessageChannel.jsm", {}); function countMM(messageManagerMap) { let count = 0; // List of permanent message managers in the main process. We should not // count them in the test because MessageChannel unsubscribes when the // message manager closes, which never happens to these, of course. let globalMMs = [ @@ -252,13 +254,13 @@ add_task(async function testExecuteScrip await extension.unload(); await BrowserTestUtils.removeTab(tab); // Make sure that we're not holding on to references to closed message // managers. is(countMM(MessageChannel.messageManagers), messageManagersSize, "Message manager count"); - if (!ExtensionManagement.useRemoteWebExtensions) { + if (!useRemoteWebExtensions) { is(countMM(MessageChannel.responseManagers), responseManagersSize, "Response manager count"); } is(MessageChannel.pendingResponses.size, 0, "Pending response count"); });
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js +++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_frameId0.js @@ -1,26 +1,13 @@ /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; add_task(async function webNavigation_getFrameId_of_existing_main_frame() { - // Whether the frame ID in the extension API is 0 is determined by a map that - // is maintained by |Frames| in ExtensionManagement.jsm. This map is filled - // using data from content processes. But if ExtensionManagement.jsm is not - // imported, then the "Extension:TopWindowID" message gets lost. - // As a result, if the state is not synchronized again, the webNavigation API - // will mistakenly report a non-zero frame ID for top-level frames. - // - // If you want to be absolutely sure that the frame ID is correct, don't open - // tabs before starting an extension, or explicitly load the module in the - // main process: - // Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); - // - // Or simply run the test again. const BASE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/"; const DUMMY_URL = BASE + "file_dummy.html"; let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, DUMMY_URL, true); async function background(DUMMY_URL) { let tabs = await browser.tabs.query({active: true, currentWindow: true}); let frames = await browser.webNavigation.getAllFrames({tabId: tabs[0].id}); browser.test.assertEq(1, frames.length, "The dummy page has one frame");
--- a/browser/components/extensions/test/xpcshell/head.js +++ b/browser/components/extensions/test/xpcshell/head.js @@ -7,18 +7,16 @@ const {classes: Cc, interfaces: Ci, util Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Extension", "resource://gre/modules/Extension.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils", "resource://testing-common/ExtensionXPCShellUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "HttpServer", "resource://testing-common/httpd.js"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
--- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -568,16 +568,17 @@ #if defined(MOZ_DEBUG) || defined(NIGHTLY_BUILD) @RESPATH@/browser/components/testComponents.manifest @RESPATH@/browser/components/startupRecorder.js #endif ; [Extensions] @RESPATH@/components/extensions-toolkit.manifest +@RESPATH@/components/extension-process-script.js @RESPATH@/browser/components/extensions-browser.manifest ; Modules @RESPATH@/browser/modules/* @RESPATH@/modules/* ; Safe Browsing @RESPATH@/components/nsURLClassifier.manifest
--- a/caps/tests/mochitest/test_addonMayLoad.html +++ b/caps/tests/mochitest/test_addonMayLoad.html @@ -10,26 +10,41 @@ https://bugzilla.mozilla.org/show_bug.cg <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> <script type="application/javascript"> /** Test for Bug 1180921 **/ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; - Cu.import("resource://gre/modules/Services.jsm"); + let module = Cu.import("resource://gre/modules/Services.jsm", window); let ssm = Services.scriptSecurityManager; - let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService).wrappedJSObject; + + function StubPolicy(id, subdomain) { + /* globals MatchPatternSet */ + return new module.WebExtensionPolicy({ + id, + mozExtensionHostname: id, + baseURL: `file:///{id}`, + + allowedOrigins: new MatchPatternSet([`*://${subdomain}.example.org/*`]), + localizeCallback(string) {}, + }); + } + + /* globals WebExtensionPolicy */ + let policyA = StubPolicy("addona", "test1"); + let policyB = StubPolicy("addonb", "test2"); + policyA.active = true; + policyB.active = true; SimpleTest.waitForExplicitFinish(); - let oldAddonIdCallback = aps.setExtensionURIToAddonIdCallback(uri => uri.host); SimpleTest.registerCleanupFunction(function() { - aps.setAddonLoadURICallback("addona", null); - aps.setAddonLoadURICallback("addonb", null); - aps.setExtensionURIToAddonIdCallback(oldAddonIdCallback); + policyA.active = false; + policyB.active = false; }); function tryLoad(sb, uri) { let p = new Promise(function(resolve, reject) { Cu.exportFunction(resolve, sb, { defineAs: "finish" }); Cu.exportFunction(reject, sb, { defineAs: "error" }); sb.eval("try { (function () { " + " var xhr = new XMLHttpRequest();" + @@ -43,21 +58,19 @@ https://bugzilla.mozilla.org/show_bug.cg let addonA = new Cu.Sandbox(ssm.createCodebasePrincipal(Services.io.newURI("moz-extension://addonA/"), {}), {wantGlobalProperties: ["XMLHttpRequest"]}); let addonB = new Cu.Sandbox(ssm.createCodebasePrincipal(Services.io.newURI("moz-extension://addonB/"), {}), {wantGlobalProperties: ["XMLHttpRequest"]}); function uriForDomain(d) { return d + "/tests/caps/tests/mochitest/file_data.txt" } - tryLoad(addonA, uriForDomain("http://test1.example.org")) + tryLoad(addonA, uriForDomain("http://test4.example.org")) .then(function(success) { ok(!success, "cross-origin load should fail for addon A"); - aps.setAddonLoadURICallback("addona", function(uri) { return /test1/.test(uri.host); }); - aps.setAddonLoadURICallback("addonb", function(uri) { return /test2/.test(uri.host); }); return tryLoad(addonA, uriForDomain("http://test1.example.org")); }).then(function(success) { ok(success, "whitelisted cross-origin load of test1 should succeed for addon A"); return tryLoad(addonB, uriForDomain("http://test1.example.org")); }).then(function(success) { ok(!success, "non-whitelisted cross-origin load of test1 should fail for addon B"); return tryLoad(addonB, uriForDomain("http://test2.example.org")); }).then(function(success) {
--- a/caps/tests/mochitest/test_extensionURL.html +++ b/caps/tests/mochitest/test_extensionURL.html @@ -8,46 +8,58 @@ https://bugzilla.mozilla.org/show_bug.cg <title>Test for Bug 1161831</title> <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> <script type="application/javascript"> /** Test for Bug 1161831 **/ SimpleTest.waitForExplicitFinish(); - var aps = SpecialPowers.Cc["@mozilla.org/addons/policy-service;1"] - .getService(SpecialPowers.Ci.nsIAddonPolicyService).wrappedJSObject; - var oldLoadCallback = aps.setExtensionURILoadCallback(null); - var oldMapCallback = aps.setExtensionURIToAddonIdCallback(null); + let module = SpecialPowers.Cu.import("resource://gre/modules/Services.jsm", {}); + var {MatchGlob, MatchPatternSet, WebExtensionPolicy} = module; + + var policy1, policy2; + var resourceHandler = SpecialPowers.Services.io.getProtocolHandler("resource") .QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler); var extensionHandler = SpecialPowers.Services.io.getProtocolHandler("moz-extension") .QueryInterface(SpecialPowers.Ci.nsISubstitutingProtocolHandler); SimpleTest.registerCleanupFunction(function() { - extensionHandler.setSubstitution("cherise", null); - extensionHandler.setSubstitution("liebchen", null); - aps.setExtensionURILoadCallback(oldLoadCallback); - aps.setExtensionURIToAddonIdCallback(oldMapCallback); + policy1.active = false; + policy2.active = false; }); addLoadEvent(function() { // First, get a file:// URI to something - open to suggestions on how to do // this more easily. var resURI = SpecialPowers.Services.io.newURI("resource://testing-common/resource_test_file.html"); var filePath = resourceHandler.resolveURI(resURI); ok(filePath.startsWith("file://"), "resource:// URI resolves where we expect: " + filePath); var fileURI = SpecialPowers.Services.io.newURI(filePath); - // Register a moz-extension:// URI. - extensionHandler.setSubstitution("cherise", fileURI); + function StubPolicy(id, accessible) { + let policy = new WebExtensionPolicy(SpecialPowers.Cu.cloneInto({ + id: `imaginaryaddon-${id[0]}`, + mozExtensionHostname: id, + baseURL: fileURI.spec, - // Alias the above. - extensionHandler.setSubstitution("liebchen", SpecialPowers.Services.io.newURI("moz-extension://cherise")); + allowedOrigins: SpecialPowers.unwrap(new MatchPatternSet([])), + webAccessibleResources: accessible ? [SpecialPowers.unwrap(new MatchGlob("*"))] : [], + localizeCallback(string) {}, + }, module, {cloneFunctions: true, wrapReflectors: true})); + + policy.active = true; + return policy; + } + + // Register a moz-extension:// URI. + policy1 = StubPolicy("cherise", false); + policy2 = StubPolicy("liebchen", false); // // Make sure that non-file:// URIs don't work. // // resource:// try { extensionHandler.setSubstitution("interdit", resURI); @@ -71,23 +83,24 @@ https://bugzilla.mozilla.org/show_bug.cg function navigateFromChromeWithWebNav(ifr, url) { SpecialPowers.wrap(ifr).contentWindow .QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) .getInterface(SpecialPowers.Ci.nsIWebNavigation) .loadURI(url, 0, null, null, null); } - function setWhitelistCallback(rgxp) { - var cb = SpecialPowers.wrapCallback(function(uri) { return rgxp.test(uri.spec); }); - aps.setExtensionURILoadCallback(cb); + function setWhitelistCallback(paths) { + policy1.active = false; + policy2.active = false; + + policy1 = StubPolicy("cherise", paths.includes("cherise")); + policy2 = StubPolicy("liebchen", paths.includes("liebchen")); } - aps.setExtensionURIToAddonIdCallback(SpecialPowers.wrapCallback(function(uri) { return "imaginaryaddon-" + uri.host[0]; })); - function testLoad(url, navigate, shouldThrow) { var ifr = document.createElement("iframe"); var p = new Promise(function(resolve, reject) { ifr.onload = function() { ok(true, "Loaded " + url); var prin = SpecialPowers.wrap(ifr.contentWindow).document.nodePrincipal; function stripTrailingSlash(s) { return s.replace(/\/$/, ""); } is(stripTrailingSlash(prin.URI.spec), url, "Principal uri is correct: " + url); @@ -133,22 +146,22 @@ https://bugzilla.mozilla.org/show_bug.cg // // Perform some loads and make sure they work correctly. // testLoad.bind(null, "moz-extension://cherise", navigateFromChromeWithLocation)() .then(testLoad.bind(null, "moz-extension://cherise", navigateFromChromeWithWebNav)) .then(testLoad.bind(null, "moz-extension://cherise", navigateWithLocation, /* shouldThrow = */ true)) .then(testXHR.bind(null, "moz-extension://cherise", /* shouldError = */ true)) - .then(setWhitelistCallback.bind(null, /cherise/)) + .then(setWhitelistCallback.bind(null, ["cherise"])) .then(testLoad.bind(null, "moz-extension://cherise", navigateWithLocation)) .then(testXHR.bind(null, "moz-extension://cherise")) .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithLocation, /* shouldThrow = */ true)) .then(testXHR.bind(null, "moz-extension://liebchen", /* shouldError = */ true)) - .then(setWhitelistCallback.bind(null, /cherise|liebchen/)) + .then(setWhitelistCallback.bind(null, ["cherise", "liebchen"])) .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithLocation)) .then(testLoad.bind(null, "moz-extension://liebchen", navigateWithSrc)) .then(testLoad.bind(null, "moz-extension://cherise", navigateWithSrc)) .then(testLoad.bind(null, "moz-extension://cherise/_blank.html", navigateWithSrc)) .then(SimpleTest.finish.bind(SimpleTest), function(e) { ok(false, "rejected promise: " + e); SimpleTest.finish() } ); });
--- a/devtools/client/aboutdebugging/aboutdebugging.css +++ b/devtools/client/aboutdebugging/aboutdebugging.css @@ -202,16 +202,22 @@ button { .addons-web-ext-tip { display: inline-block; margin-inline-end: 1ch; } .addons-tip { display: flex; align-items: center; + margin-top: 1em; + margin-bottom: 1em; + background-image: url(chrome://devtools/skin/images/help.svg); + background-repeat: no-repeat; + padding-inline-start: 32px; + height: 24px; } .addons-tip-icon { margin-inline-end: 1ch; } .error-page { display: flex; @@ -239,16 +245,17 @@ button { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.24); } .addon-target-container .target { align-items: center; display: flex; margin: 0; padding: 16px 0; + font-size: 14px; } .addon-target-actions { border-top: 1px solid rgba(0, 0, 0, 0.2); } .addon-target-container .target-icon { margin-inline-end: 16px; @@ -257,17 +264,16 @@ button { .addon-target-container .name { align-self: center; font-size: 16px; font-weight: 600; } .addon-target-info { display: grid; - font-size: 14px; grid-template-columns: 128px 1fr; } .addon-target-info-label { padding-inline-end: 8px; padding-bottom: 8px; }
--- a/devtools/client/aboutdebugging/components/addons/panel.js +++ b/devtools/client/aboutdebugging/components/addons/panel.js @@ -145,21 +145,16 @@ module.exports = createClass({ name: temporaryName, targets: temporaryTargets, client, debugDisabled, targetClass, sort: true }), dom.div({ className: "addons-tip"}, - dom.img({ - className: "addons-tip-icon", - role: "presentation", - src: "chrome://devtools/skin/images/help.svg", - }), dom.span({ className: "addons-web-ext-tip", }, Strings.GetStringFromName("webExtTip")), dom.a({ href: WEB_EXT_URL, target: "_blank" }, Strings.GetStringFromName("webExtTip.learnMore") ) ) ),
--- a/devtools/client/aboutdebugging/components/addons/target.js +++ b/devtools/client/aboutdebugging/components/addons/target.js @@ -3,28 +3,31 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint-env browser */ "use strict"; const { createClass, DOM: dom, PropTypes } = require("devtools/client/shared/vendor/react"); -const { debugAddon, uninstallAddon } = require("../../modules/addon"); +const { debugAddon, uninstallAddon, isTemporaryID } = require("../../modules/addon"); const Services = require("Services"); loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm"); loader.lazyRequireGetter(this, "DebuggerClient", "devtools/shared/client/main", true); const Strings = Services.strings.createBundle( "chrome://devtools/locale/aboutdebugging.properties"); +const TEMP_ID_URL = "https://developer.mozilla.org/Add-ons" + + "/WebExtensions/WebExtensions_and_the_Add-on_ID"; + function filePathForTarget(target) { // Only show file system paths, and only for temporarily installed add-ons. if (!target.temporarilyInstalled || !target.url || !target.url.startsWith("file://")) { return []; } let path = target.url.slice("file://".length); return [ dom.dt( @@ -62,16 +65,33 @@ function internalIDForTarget(target) { { href: target.manifestURL, target: "_blank", className: "manifest-url" }, Strings.GetStringFromName("manifestURL"), ), ) ), ]; } +function temporaryID(target) { + if (!isTemporaryID(target.addonID)) { + return []; + } + + return [ + dom.div({ className: "addons-tip" }, + dom.span({ className: "addons-web-ext-tip" }, + Strings.GetStringFromName("temporaryID") + ), + dom.a({ href: TEMP_ID_URL, className: "temporary-id-url", target: "_blank" }, + Strings.GetStringFromName("temporaryID.learnMore") + ) + ) + ]; +} + module.exports = createClass({ displayName: "AddonTarget", propTypes: { client: PropTypes.instanceOf(DebuggerClient).isRequired, debugDisabled: PropTypes.bool, target: PropTypes.shape({ addonActor: PropTypes.string.isRequired, @@ -114,16 +134,17 @@ module.exports = createClass({ dom.div({ className: "target" }, dom.img({ className: "target-icon", role: "presentation", src: target.icon }), dom.span({ className: "target-name", title: target.name }, target.name) ), + ...temporaryID(target), dom.dl( { className: "addon-target-info" }, ...filePathForTarget(target), ...internalIDForTarget(target), ), dom.div({className: "addon-target-actions"}, dom.button({ className: "debug-button addon-target-button",
--- a/devtools/client/aboutdebugging/modules/addon.js +++ b/devtools/client/aboutdebugging/modules/addon.js @@ -2,16 +2,17 @@ * 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"; loader.lazyImporter(this, "BrowserToolboxProcess", "resource://devtools/client/framework/ToolboxProcess.jsm"); loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); +loader.lazyImporter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm"); let toolbox = null; exports.debugAddon = function (addonID) { if (toolbox) { toolbox.close(); } @@ -22,8 +23,12 @@ exports.debugAddon = function (addonID) } }); }; exports.uninstallAddon = async function (addonID) { let addon = await AddonManager.getAddonByID(addonID); return addon && addon.uninstall(); }; + +exports.isTemporaryID = function (addonID) { + return AddonManagerPrivate.isTemporaryInstallID(addonID); +};
new file mode 100644 --- /dev/null +++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-noid/manifest.json @@ -0,0 +1,5 @@ +{ + "manifest_version": 2, + "name": "test-devtools-webextension-noid", + "version": "1.0" +}
--- a/devtools/client/aboutdebugging/test/browser.ini +++ b/devtools/client/aboutdebugging/test/browser.ini @@ -4,16 +4,17 @@ subsuite = devtools support-files = head.js addons/unpacked/bootstrap.js addons/unpacked/install.rdf addons/bad/manifest.json addons/bug1273184.xpi addons/test-devtools-webextension/* addons/test-devtools-webextension-nobg/* + addons/test-devtools-webextension-noid/* service-workers/delay-sw.html service-workers/delay-sw.js service-workers/empty-sw.html service-workers/empty-sw.js service-workers/fetch-sw.html service-workers/fetch-sw.js service-workers/push-sw.html service-workers/push-sw.js
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_info.js +++ b/devtools/client/aboutdebugging/test/browser_addons_debug_info.js @@ -51,8 +51,33 @@ add_task(function* testWebExtension() { let manifestURL = container.querySelector(".manifest-url"); ok(manifestURL.href.startsWith("moz-extension://"), "href for manifestURL exists"); yield uninstallAddon({document, id: addonId, name: addonName}); yield closeAboutDebugging(tab); }); + +add_task(function* testTemporaryWebExtension() { + let addonName = "test-devtools-webextension-noid"; + let { tab, document } = yield openAboutDebugging("addons"); + + yield waitForInitialAddonList(document); + yield installAddon({ + document, + path: "addons/test-devtools-webextension-noid/manifest.json", + name: addonName, + isWebExtension: true + }); + + let addons = document.querySelectorAll("#temporary-extensions .addon-target-container"); + // Assuming that our temporary add-on is now at the top. + let container = addons[addons.length-1]; + let addonId = container.dataset.addonId; + + let temporaryID = container.querySelector(".temporary-id-url"); + ok(temporaryID, "Temporary ID message does appear"); + + yield uninstallAddon({document, id: addonId, name: addonName}); + + yield closeAboutDebugging(tab); +});
--- a/devtools/client/locales/en-US/aboutdebugging.properties +++ b/devtools/client/locales/en-US/aboutdebugging.properties @@ -83,16 +83,25 @@ manifestURL = Manifest URL webExtTip = You can use web-ext to load temporary WebExtensions from the command line. # LOCALIZATION NOTE (webExtTip.learnMore): # This string is displayed as a link next to webExtTip and leads the user to the MDN # documentation page for web-ext. # (https://developer.mozilla.org/Add-ons/WebExtensions/Getting_started_with_web-ext) webExtTip.learnMore = Learn more +# LOCALIZATION NOTE (temporaryID): +# This string is displayed as a message about the add-on having a temporaryID. +temporaryID = This WebExtension has a temporary ID. + +# LOCALIZATION NOTE (temporaryID.learnMore): +# This string is displayed as a link next to the temporaryID message and leads +# the user to MDN. +temporaryID.learnMore = Learn more + # LOCALIZATION NOTE (selectAddonFromFile2): # This string is displayed as the title of the file picker that appears when # the user clicks the 'Load Temporary Add-on' button selectAddonFromFile2 = Select Manifest File or Package (.xpi) # LOCALIZATION NOTE (reload): # This string is displayed as a label of the button that reloads a given addon. reload = Reload
--- a/devtools/client/locales/en-US/layout.properties +++ b/devtools/client/locales/en-US/layout.properties @@ -37,8 +37,12 @@ layout.noGrids=No grids # LOCALIZATION NOTE (layout.overlayMultipleGrids): The header for the list of grid # container elements that can be highlighted in the CSS Grid pane. layout.overlayMultipleGrids=Overlay Multiple Grids # LOCALIZATION NOTE (layout.overlayGrid): Alternate header for the list of grid container # elements if only one item can be selected. layout.overlayGrid=Overlay Grid + +# LOCALIZATION NOTE (layout.rowColumnPositions): The row and column position of a grid +# cell shown in the grid cell infobar when hovering over the CSS grid outline. +layout.rowColumnPositions=Row %S \/ Column %S
--- a/devtools/client/webconsole/test/browser_console_dead_objects.js +++ b/devtools/client/webconsole/test/browser_console_dead_objects.js @@ -25,16 +25,18 @@ function test() { Services.prefs.clearUserPref("devtools.chrome.enabled"); }); Task.spawn(runner).then(finishTest); function* runner() { Services.prefs.setBoolPref("devtools.chrome.enabled", true); yield loadTab(TEST_URI); + let browser = gBrowser.selectedBrowser; + let winID = browser.outerWindowID; info("open the browser console"); hud = yield HUDService.toggleBrowserConsole(); ok(hud, "browser console opened"); let jsterm = hud.jsterm; @@ -45,16 +47,21 @@ function test() { "Cu.import('resource://gre/modules/Services.jsm');" + "chromeWindow = Services.wm.getMostRecentWindow('" + "navigator:browser');" + "foobarzTezt = chromeWindow.content.document;" + "delete chromeWindow"); gBrowser.removeCurrentTab(); + yield TestUtils.topicObserved("outer-window-destroyed", (subject, data) => { + let id = subject.QueryInterface(Components.interfaces.nsISupportsPRUint64).data; + return id == winID; + }); + let msg = yield jsterm.execute("foobarzTezt"); isnot(hud.outputNode.textContent.indexOf("[object DeadObject]"), -1, "dead object found"); jsterm.setInputValue("foobarzTezt"); for (let c of ".hello") {
--- a/devtools/server/actors/highlighters/css-grid.js +++ b/devtools/server/actors/highlighters/css-grid.js @@ -25,16 +25,20 @@ const { apply, translate, multiply, scale, getNodeTransformationMatrix, getNodeTransformOrigin } = require("devtools/shared/layout/dom-matrix-2d"); const { stringifyGridFragments } = require("devtools/server/actors/utils/css-grid-utils"); +const { LocalizationHelper } = require("devtools/shared/l10n"); + +const LAYOUT_STRINGS_URI = "devtools/client/locales/layout.properties"; +const LAYOUT_L10N = new LocalizationHelper(LAYOUT_STRINGS_URI); const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled"; const DEFAULT_GRID_COLOR = "#4B0082"; const COLUMNS = "cols"; const ROWS = "rows"; @@ -824,17 +828,18 @@ CssGridHighlighter.prototype = extend(Au }, _updateGridCellInfobar(rowNumber, columnNumber, x1, x2, y1, y2) { let width = x2 - x1; let height = y2 - y1; let dim = parseFloat(width.toPrecision(6)) + " \u00D7 " + parseFloat(height.toPrecision(6)); - let position = `${rowNumber}\/${columnNumber}`; + let position = LAYOUT_L10N.getFormatStr("layout.rowColumnPositions", + rowNumber, columnNumber); this.getElement("cell-infobar-position").setTextContent(position); this.getElement("cell-infobar-dimensions").setTextContent(dim); let container = this.getElement("cell-infobar-container"); this._moveInfobar(container, x1, x2, y1, y2, { position: "top", hideIfOffscreen: true
--- a/devtools/server/actors/webextension-parent.js +++ b/devtools/server/actors/webextension-parent.js @@ -4,17 +4,16 @@ "use strict"; const {DebuggerServer} = require("devtools/server/main"); const protocol = require("devtools/shared/protocol"); const {webExtensionSpec} = require("devtools/shared/specs/webextension-parent"); loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); -loader.lazyImporter(this, "ExtensionManagement", "resource://gre/modules/ExtensionManagement.jsm"); loader.lazyImporter(this, "ExtensionParent", "resource://gre/modules/ExtensionParent.jsm"); /** * Creates the actor that represents the addon in the parent process, which connects * itself to a WebExtensionChildActor counterpart which is created in the * extension process (or in the main process if the WebExtensions OOP mode is disabled). * * The WebExtensionParentActor subscribes itself as an AddonListener on the AddonManager @@ -60,26 +59,27 @@ const WebExtensionParentActor = protocol reload() { return this.addon.reload().then(() => { return {}; }); }, form() { + let policy = ExtensionParent.WebExtensionPolicy.getByID(this.id); return { actor: this.actorID, id: this.id, name: this.addon.name, url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined, iconURL: this.addon.iconURL, debuggable: this.addon.isDebuggable, temporarilyInstalled: this.addon.temporarilyInstalled, isWebExtension: true, - manifestURL: ExtensionManagement.getURLForExtension(this.id, "manifest.json"), + manifestURL: policy && policy.getURL("manifest.json"), }; }, connect() { if (this._childFormPormise) { return this._childFormPromise; }
--- a/devtools/shared/tests/unit/test_console_filtering.js +++ b/devtools/shared/tests/unit/test_console_filtering.js @@ -2,19 +2,16 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; const { console, ConsoleAPI } = require("resource://gre/modules/Console.jsm"); const { ConsoleAPIListener } = require("devtools/server/actors/utils/webconsole-listeners"); const Services = require("Services"); -// FIXME: This test shouldn't need to rely on low-level internals. -const {Service} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); - var seenMessages = 0; var seenTypes = 0; var callback = { onConsoleAPICall: function (message) { if (message.consoleID && message.consoleID == "addon/foo") { do_check_eq(message.level, "warn"); do_check_eq(message.arguments[0], "Warning from foo"); @@ -27,42 +24,55 @@ var callback = { do_check_eq(message.level, "log"); do_check_eq(message.arguments[0], "Hello from default console"); seenTypes |= 4; } seenMessages++; } }; +let policy; +do_register_cleanup(() => { + policy.active = false; +}); + function createFakeAddonWindow({addonId} = {}) { const uuidGen = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); const uuid = uuidGen.generateUUID().number.slice(1, -1); - const url = `moz-extension://${uuid}/`; - Service.uuidMap.set(uuid, {id: addonId}); + if (policy) { + policy.active = false; + } + /* globals MatchPatternSet, WebExtensionPolicy */ + policy = new WebExtensionPolicy({ + id: addonId, + mozExtensionHostname: uuid, + baseURL: "file:///", + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + }); + policy.active = true; - let baseURI = Services.io.newURI(url); + let baseURI = Services.io.newURI(`moz-extension://${uuid}/`); let principal = Services.scriptSecurityManager .createCodebasePrincipal(baseURI, {}); let chromeWebNav = Services.appShell.createWindowlessBrowser(true); let docShell = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell); docShell.createAboutBlankContentViewer(principal); let addonWindow = docShell.contentViewer.DOMDocument.defaultView; return {addonWindow, chromeWebNav}; } /** * Tests that the consoleID property of the ConsoleAPI options gets passed * through to console messages. */ function run_test() { - Service.init(); - // console1 Test Console.jsm messages tagged by the Addon SDK // are still filtered correctly. let console1 = new ConsoleAPI({ consoleID: "addon/foo" }); // console2 - WebExtension page's console messages tagged // by 'originAttributes.addonId' are filtered correctly.
--- a/dom/base/nsGkAtomList.h +++ b/dom/base/nsGkAtomList.h @@ -409,16 +409,17 @@ GK_ATOM(expr, "expr") GK_ATOM(extends, "extends") GK_ATOM(extensionElementPrefixes, "extension-element-prefixes") GK_ATOM(face, "face") GK_ATOM(fallback, "fallback") GK_ATOM(_false, "false") GK_ATOM(farthest, "farthest") GK_ATOM(field, "field") GK_ATOM(fieldset, "fieldset") +GK_ATOM(file, "file") GK_ATOM(figcaption, "figcaption") GK_ATOM(figure, "figure") GK_ATOM(fixed, "fixed") GK_ATOM(flags, "flags") GK_ATOM(flex, "flex") GK_ATOM(flexgroup, "flexgroup") GK_ATOM(flip, "flip") GK_ATOM(floating, "floating") @@ -2003,16 +2004,22 @@ GK_ATOM(onuserproximity, "onuserproximit GK_ATOM(ondevicelight, "ondevicelight") // MediaDevices device change event GK_ATOM(ondevicechange, "ondevicechange") // HTML element attributes that only exposed to XBL and chrome content GK_ATOM(mozinputrangeignorepreventdefault, "mozinputrangeignorepreventdefault") +// WebExtensions +GK_ATOM(moz_extension, "moz-extension") +GK_ATOM(all_urlsPermission, "<all_urls>") +GK_ATOM(http, "http") +GK_ATOM(https, "https") + //--------------------------------------------------------------------------- // Special atoms //--------------------------------------------------------------------------- // Node types GK_ATOM(cdataTagName, "#cdata-section") GK_ATOM(commentTagName, "#comment") GK_ATOM(documentNodeName, "#document")
--- a/dom/bindings/Bindings.conf +++ b/dom/bindings/Bindings.conf @@ -567,16 +567,29 @@ DOMInterfaces = { 'wrapperCache': False, }, 'LocalMediaStream': { 'headerFile': 'DOMMediaStream.h', 'nativeType': 'mozilla::DOMLocalMediaStream' }, +'MatchGlob': { + 'nativeType': 'mozilla::extensions::MatchGlob', +}, + +'MatchPattern': { + 'nativeType': 'mozilla::extensions::MatchPattern', +}, + +'MatchPatternSet': { + 'headerFile': 'mozilla/extensions/MatchPattern.h', + 'nativeType': 'mozilla::extensions::MatchPatternSet', +}, + 'MediaKeys' : { 'implicitJSContext': [ 'createSession'] }, 'MediaStream': { 'headerFile': 'DOMMediaStream.h', 'nativeType': 'mozilla::DOMMediaStream' }, @@ -1083,16 +1096,24 @@ DOMInterfaces = { 'VTTRegion': { 'nativeType': 'mozilla::dom::TextTrackRegion', }, 'WebAuthentication': { 'implicitJSContext': 'makeCredential', }, +'WebExtensionContentScript': { + 'nativeType': 'mozilla::extensions::WebExtensionContentScript', +}, + +'WebExtensionPolicy': { + 'nativeType': 'mozilla::extensions::WebExtensionPolicy', +}, + 'WindowClient': { 'nativeType': 'mozilla::dom::workers::ServiceWorkerWindowClient', 'headerFile': 'mozilla/dom/workers/bindings/ServiceWorkerWindowClient.h', }, 'WebGLActiveInfo': { 'nativeType': 'mozilla::WebGLActiveInfo', 'headerFile': 'WebGLActiveInfo.h' @@ -1684,21 +1705,25 @@ def addExternalIface(iface, nativeType=N if not nativeType is None: domInterface['nativeType'] = nativeType if not headerFile is None: domInterface['headerFile'] = headerFile domInterface['notflattened'] = notflattened DOMInterfaces[iface] = domInterface addExternalIface('ApplicationCache', nativeType='nsIDOMOfflineResourceList') +addExternalIface('Cookie', nativeType='nsICookie2', + headerFile='nsICookie2.h', notflattened=True) addExternalIface('Counter') addExternalIface('RTCDataChannel', nativeType='nsIDOMDataChannel') addExternalIface('HitRegionOptions', nativeType='nsISupports') addExternalIface('imgINotificationObserver', nativeType='imgINotificationObserver') addExternalIface('imgIRequest', nativeType='imgIRequest', notflattened=True) +addExternalIface('LoadInfo', nativeType='nsILoadInfo', + headerFile='nsILoadInfo.h', notflattened=True) addExternalIface('MenuBuilder', nativeType='nsIMenuBuilder', notflattened=True) addExternalIface('MozControllers', nativeType='nsIControllers') addExternalIface('MozFrameLoader', nativeType='nsIFrameLoader', notflattened=True) addExternalIface('MozObserver', nativeType='nsIObserver', notflattened=True) addExternalIface('MozRDFCompositeDataSource', nativeType='nsIRDFCompositeDataSource', notflattened=True) addExternalIface('MozRDFResource', nativeType='nsIRDFResource', notflattened=True) addExternalIface('MozTreeView', nativeType='nsITreeView',
--- a/dom/tests/browser/browser_ConsoleAPI_originAttributes.js +++ b/dom/tests/browser/browser_ConsoleAPI_originAttributes.js @@ -1,16 +1,15 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"] .getService(Ci.nsIConsoleAPIStorage); -// FIXME: This test shouldn't need to rely on low-level internals. -const {Service} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); +const {WebExtensionPolicy} = Cu.import("resource://gre/modules/Services.jsm", {}); const FAKE_ADDON_ID = "test-webext-addon@mozilla.org"; const EXPECTED_CONSOLE_ID = `addon/${FAKE_ADDON_ID}`; const EXPECTED_CONSOLE_MESSAGE_CONTENT = "fake-webext-addon-test-log-message"; const ConsoleObserver = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), init() { @@ -41,40 +40,48 @@ const ConsoleObserver = { finish(); } } }; function test() { - Service.init(); ConsoleObserver.init(); waitForExplicitFinish(); let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator); let uuid = uuidGenerator.generateUUID().number; uuid = uuid.slice(1, -1); // Strip { and } off the UUID. const url = `moz-extension://${uuid}/`; - Service.uuidMap.set(uuid, {id: FAKE_ADDON_ID}); + /* globals MatchPatternSet, WebExtensionPolicy */ + let policy = new WebExtensionPolicy({ + id: FAKE_ADDON_ID, + mozExtensionHostname: uuid, + baseURL: "file:///", + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + }); + policy.active = true; let baseURI = Services.io.newURI(url); let principal = Services.scriptSecurityManager .createCodebasePrincipal(baseURI, {}); let chromeWebNav = Services.appShell.createWindowlessBrowser(true); let interfaceRequestor = chromeWebNav.QueryInterface(Ci.nsIInterfaceRequestor); let docShell = interfaceRequestor.getInterface(Ci.nsIDocShell); docShell.createAboutBlankContentViewer(principal); info("fake webextension docShell created"); registerCleanupFunction(function() { + policy.active = false; if (chromeWebNav) { chromeWebNav.close(); chromeWebNav = null; } ConsoleObserver.uninit(); }); let window = docShell.contentViewer.DOMDocument.defaultView;
new file mode 100644 --- /dev/null +++ b/dom/webidl/MatchGlob.webidl @@ -0,0 +1,24 @@ +/* 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/. */ + +/** + * Represents a simple glob pattern matcher. Any occurrence of "*" in the glob + * pattern matches any literal string of characters in the string being + * compared. Additionally, if created with `allowQuestion = true`, any + * occurrence of "?" in the glob matches any single literal character. + */ +[Constructor(DOMString glob, optional boolean allowQuestion = true), + ChromeOnly, Exposed=(Window,System)] +interface MatchGlob { + /** + * Returns true if the string matches the glob. + */ + boolean matches(DOMString string); + + /** + * The glob string this MatchGlob represents. + */ + [Constant] + readonly attribute DOMString glob; +};
new file mode 100644 --- /dev/null +++ b/dom/webidl/MatchPattern.webidl @@ -0,0 +1,120 @@ +/* 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/. */ + +interface Cookie; +interface URI; + +/** + * A URL match pattern as used by the WebExtension and Chrome extension APIs. + * + * A match pattern is a string with one of the following formats: + * + * - "<all_urls>" + * The literal string "<all_urls>" matches any URL with a supported + * protocol. + * + * - <proto>://<host>/<path> + * A URL pattern with the following placeholders: + * + * - <proto> + * The protocol to match, or "*" to match either "http" or "https". + * - <host> + * The hostname to match. May be either a complete, literal hostname to + * match a specific host, the wildcard character "*", to match any host, + * or a subdomain pattern, with "*." followed by a domain name, to match + * that domain name or any subdomain thereof. + * - <path> + * A glob pattern for paths to match. A "*" may appear anywhere within + * the path, and will match any string of characters. If no "*" appears, + * the URL path must exactly match the pattern path. + */ +[Constructor(DOMString pattern, optional MatchPatternOptions options), + ChromeOnly, Exposed=(Window,System)] +interface MatchPattern { + /** + * Returns true if the given URI matches the pattern. + * + * If explicit is true, only explicit domain matches, without wildcards, are + * considered. + */ + boolean matches(URI uri, optional boolean explicit = false); + + /** + * Returns true if a URL exists which a) would be able to access the given + * cookie, and b) would be matched by this match pattern. + */ + boolean matchesCookie(Cookie cookie); + + /** + * Returns true if this pattern will match any host which would be matched + * by the given pattern. + */ + boolean subsumes(MatchPattern pattern); + + /** + * Returns true if there is any host which would be matched by both this + * pattern and the given pattern. + */ + boolean overlaps(MatchPattern pattern); + + /** + * The match pattern string represented by this pattern. + */ + [Constant] + readonly attribute DOMString pattern; +}; + +/** + * A set of MatchPattern objects, which implements the MatchPattern API and + * matches when any of its sub-patterns matches. + */ +[Constructor(sequence<(DOMString or MatchPattern)> patterns, optional MatchPatternOptions options), + ChromeOnly, Exposed=(Window,System)] +interface MatchPatternSet { + /** + * Returns true if the given URI matches any sub-pattern. + * + * If explicit is true, only explicit domain matches, without wildcards, are + * considered. + */ + boolean matches(URI uri, optional boolean explicit = false); + + /** + * Returns true if any sub-pattern matches the given cookie. + */ + boolean matchesCookie(Cookie cookie); + + /** + * Returns true if any sub-pattern subsumes the given pattern. + */ + boolean subsumes(MatchPattern pattern); + + /** + * Returns true if any sub-pattern overlaps the given pattern. + */ + boolean overlaps(MatchPattern pattern); + + /** + * Returns true if any sub-pattern overlaps any sub-pattern the given + * pattern set. + */ + boolean overlaps(MatchPatternSet patternSet); + + /** + * Returns true if any sub-pattern overlaps *every* sub-pattern in the given + * pattern set. + */ + boolean overlapsAll(MatchPatternSet patternSet); + + [Cached, Constant, Frozen] + readonly attribute sequence<MatchPattern> patterns; +}; + +dictionary MatchPatternOptions { + /** + * If true, the path portion of the pattern is ignored, and replaced with a + * wildcard. The `pattern` property is updated to reflect this. + */ + boolean ignorePath = false; +};
new file mode 100644 --- /dev/null +++ b/dom/webidl/WebExtensionContentScript.webidl @@ -0,0 +1,161 @@ +/* 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/. */ + +interface LoadInfo; +interface URI; +interface WindowProxy; + +/** + * Describes the earliest point in the load cycle at which a script should + * run. + */ +enum ContentScriptRunAt { + /** + * The point in the load cycle just after the document element has been + * inserted, before any page scripts have been allowed to run. + */ + "document_start", + /** + * The point after which the page DOM has fully loaded, but before all page + * resources have necessarily been loaded. Corresponds approximately to the + * DOMContentLoaded event. + */ + "document_end", + /** + * The first point after the page and all of its resources has fully loaded + * when the event loop is idle, and can run scripts without delaying a paint + * event. + */ + "document_idle", +}; + +[Constructor(WebExtensionPolicy extension, WebExtensionContentScriptInit options), ChromeOnly, Exposed=System] +interface WebExtensionContentScript { + /** + * Returns true if the script's match and exclude patterns match the given + * URI, without reference to attributes such as `allFrames`. + */ + boolean matchesURI(URI uri); + + /** + * Returns true if the script matches the given URI and LoadInfo objects. + * This should be used to determine whether to begin pre-loading a content + * script based on network events. + */ + boolean matchesLoadInfo(URI uri, LoadInfo loadInfo); + + /** + * Returns true if the script matches the given window. This should be used + * to determine whether to run a script in a window at load time. + */ + boolean matchesWindow(WindowProxy window); + + /** + * The policy object for the extension that this script belongs to. + */ + [Constant] + readonly attribute WebExtensionPolicy extension; + + /** + * If true, this script runs in all frames. If false, it only runs in + * top-level frames. + */ + [Constant] + readonly attribute boolean allFrames; + + /** + * If true, this (misleadingly-named, but inherited from Chrome) attribute + * causes the script to run in frames with URLs which inherit a principal + * that matches one of the match patterns, such as about:blank or + * about:srcdoc. If false, the script only runs in frames with an explicit + * matching URL. + */ + [Constant] + readonly attribute boolean matchAboutBlank; + + /** + * The earliest point in the load cycle at which this script should run. For + * static content scripts, in extensions which were present at browser + * startup, the browser makes every effort to make sure that the script runs + * no later than this point in the load cycle. For dynamic content scripts, + * and scripts from extensions installed during this session, the scripts + * may run at a later point. + */ + [Constant] + readonly attribute ContentScriptRunAt runAt; + + /** + * The outer window ID of the frame in which to run the script, or 0 if it + * should run in the top-level frame. Should only be used for + * dynamically-injected scripts. + */ + [Constant] + readonly attribute unsigned long long? frameID; + + /** + * The set of match patterns for URIs of pages in which this script should + * run. This attribute is mandatory, and is a prerequisite for all other + * match patterns. + */ + [Constant] + readonly attribute MatchPatternSet matches; + + /** + * A set of match patterns for URLs in which this script should not run, + * even if they match other include patterns or globs. + */ + [Constant] + readonly attribute MatchPatternSet? excludeMatches; + + /** + * A set of glob matchers for URLs in which this script should run. If this + * list is present, the script will only run in URLs which match the + * `matches` pattern as well as one of these globs. + */ + [Cached, Constant, Frozen] + readonly attribute sequence<MatchGlob>? includeGlobs; + + /** + * A set of glob matchers for URLs in which this script should not run, even + * if they match other include patterns or globs. + */ + [Cached, Constant, Frozen] + readonly attribute sequence<MatchGlob>? excludeGlobs; + + /** + * A set of paths, relative to the extension root, of CSS sheets to inject + * into matching pages. + */ + [Cached, Constant, Frozen] + readonly attribute sequence<DOMString> cssPaths; + + /** + * A set of paths, relative to the extension root, of JavaScript scripts to + * execute in matching pages. + */ + [Cached, Constant, Frozen] + readonly attribute sequence<DOMString> jsPaths; +}; + +dictionary WebExtensionContentScriptInit { + boolean allFrames = false; + + boolean matchAboutBlank = false; + + ContentScriptRunAt runAt = "document_idle"; + + unsigned long long? frameID = null; + + required MatchPatternSet matches; + + MatchPatternSet? excludeMatches = null; + + sequence<MatchGlob>? includeGlobs = null; + + sequence<MatchGlob>? excludeGlobs = null; + + sequence<DOMString> cssPaths = []; + + sequence<DOMString> jsPaths = []; +};
new file mode 100644 --- /dev/null +++ b/dom/webidl/WebExtensionPolicy.webidl @@ -0,0 +1,158 @@ +/* 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/. */ + +interface URI; +interface WindowProxy; + +callback WebExtensionLocalizeCallback = DOMString (DOMString unlocalizedText); + +/** + * Defines the platform-level policies for a WebExtension, including its + * permissions and the characteristics of its moz-extension: URLs. + */ +[Constructor(WebExtensionInit options), ChromeOnly, Exposed=System] +interface WebExtensionPolicy { + /** + * The add-on's internal ID, as specified in its manifest.json file or its + * XPI signature. + */ + [Constant, StoreInSlot] + readonly attribute DOMString id; + + /** + * The hostname part of the extension's moz-extension: URLs. This value is + * generated randomly at install time. + */ + [Constant, StoreInSlot] + readonly attribute ByteString mozExtensionHostname; + + /** + * The file: or jar: URL to use for the base of the extension's + * moz-extension: URL root. + */ + [Constant] + readonly attribute ByteString baseURL; + + /** + * The content security policy string to apply to all pages loaded from the + * extension's moz-extension: protocol. + */ + [Constant] + readonly attribute DOMString contentSecurityPolicy; + + + /** + * The list of currently-active permissions for the extension, as specified + * in its manifest.json file. May be updated to reflect changes in the + * extension's optional permissions. + */ + [Cached, Frozen, Pure] + attribute sequence<DOMString> permissions; + + /** + * Match patterns for the set of web origins to which the extension is + * currently allowed access. May be updated to reflect changes in the + * extension's optional permissions. + */ + [Pure] + attribute MatchPatternSet allowedOrigins; + + /** + * The set of content scripts active for this extension. + */ + [Cached, Constant, Frozen] + readonly attribute sequence<WebExtensionContentScript> contentScripts; + + /** + * True if the extension is currently active, false otherwise. When active, + * the extension's moz-extension: protocol will point to the given baseURI, + * and the set of policies for this object will be active for its ID. + * + * Only one extension policy with a given ID or hostname may be active at a + * time. Attempting to activate a policy while a conflicting policy is + * active will raise an error. + */ + [Affects=Everything, SetterThrows] + attribute boolean active; + + + static readonly attribute boolean isExtensionProcess; + + + /** + * Returns true if the extension has cross-origin access to the given URI. + */ + boolean canAccessURI(URI uri, optional boolean explicit = false); + + /** + * Returns true if the extension currently has the given permission. + */ + boolean hasPermission(DOMString permission); + + /** + * Returns true if the given path relative to the extension's moz-extension: + * URL root may be accessed by web content. + */ + boolean isPathWebAccessible(DOMString pathname); + + /** + * Replaces localization placeholders in the given string with localized + * text from the extension's currently active locale. + */ + DOMString localize(DOMString unlocalizedText); + + /** + * Returns the moz-extension: URL for the given path. + */ + [Throws] + DOMString getURL(optional DOMString path = ""); + + + /** + * Returns the list of currently active extension policies. + */ + static sequence<WebExtensionPolicy> getActiveExtensions(); + + /** + * Returns the currently-active policy for the extension with the given ID, + * or null if no policy is active for that ID. + */ + static WebExtensionPolicy? getByID(DOMString id); + + /** + * Returns the currently-active policy for the extension with the given + * moz-extension: hostname, or null if no policy is active for that + * hostname. + */ + static WebExtensionPolicy? getByHostname(ByteString hostname); + + /** + * Returns the currently-active policy for the extension extension URI, or + * null if the URI is not an extension URI, or no policy is currently active + * for it. + */ + static WebExtensionPolicy? getByURI(URI uri); +}; + +dictionary WebExtensionInit { + required DOMString id; + + required ByteString mozExtensionHostname; + + required DOMString baseURL; + + required WebExtensionLocalizeCallback localizeCallback; + + required MatchPatternSet allowedOrigins; + + sequence<DOMString> permissions = []; + + sequence<MatchGlob> webAccessibleResources = []; + + sequence<WebExtensionContentScriptInit> contentScripts = []; + + DOMString? contentSecurityPolicy = null; + + sequence<DOMString>? backgroundScripts = null; +};
--- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -658,16 +658,18 @@ WEBIDL_FILES = [ 'KeyframeAnimationOptions.webidl', 'KeyframeEffect.webidl', 'KeyIdsInitData.webidl', 'LegacyQueryInterface.webidl', 'LinkStyle.webidl', 'ListBoxObject.webidl', 'LocalMediaStream.webidl', 'Location.webidl', + 'MatchGlob.webidl', + 'MatchPattern.webidl', 'MediaDeviceInfo.webidl', 'MediaDevices.webidl', 'MediaElementAudioSourceNode.webidl', 'MediaEncryptedEvent.webidl', 'MediaError.webidl', 'MediaKeyError.webidl', 'MediaKeyMessageEvent.webidl', 'MediaKeys.webidl', @@ -937,16 +939,18 @@ WEBIDL_FILES = [ 'VRDisplay.webidl', 'VRDisplayEvent.webidl', 'VRServiceTest.webidl', 'VTTCue.webidl', 'VTTRegion.webidl', 'WaveShaperNode.webidl', 'WebAuthentication.webidl', 'WebComponents.webidl', + 'WebExtensionContentScript.webidl', + 'WebExtensionPolicy.webidl', 'WebGL2RenderingContext.webidl', 'WebGLRenderingContext.webidl', 'WebKitCSSMatrix.webidl', 'WebSocket.webidl', 'WheelEvent.webidl', 'WidevineCDMManifest.webidl', 'WindowOrWorkerGlobalScope.webidl', 'WindowRoot.webidl',
--- a/js/public/ProfilingStack.h +++ b/js/public/ProfilingStack.h @@ -50,124 +50,118 @@ class ProfileEntry const char * volatile dynamicString_; // Stack pointer for non-JS entries, the script pointer otherwise. void * volatile spOrScript; // Line number for non-JS entries, the bytecode offset otherwise. int32_t volatile lineOrPcOffset; - // Flags are in the low bits. The category is in the high bits. - uint32_t volatile flagsAndCategory_; + // Bits 0..1 hold the Kind. Bits 2..3 are unused. Bits 4..12 hold the + // Category. + uint32_t volatile kindAndCategory_; static int32_t pcToOffset(JSScript* aScript, jsbytecode* aPc); public: - // These traits are bit masks. Make sure they're powers of 2. - enum Flags : uint32_t { - // Indicate whether a profile entry represents a CPP frame. If not set, - // a JS frame is assumed by default. You're not allowed to publicly - // change the frame type. Instead, initialize the ProfileEntry as either - // a JS or CPP frame with `initJsFrame` or `initCppFrame` respectively. - IS_CPP_ENTRY = 1u << 0, + enum class Kind : uint32_t { + // A normal C++ frame. + CPP_NORMAL = 0, + + // A special C++ frame indicating the start of a run of JS pseudostack + // entries. CPP_MARKER_FOR_JS frames are ignored, except for the sp + // field. + CPP_MARKER_FOR_JS = 1, - // This ProfileEntry is a dummy entry indicating the start of a run - // of JS pseudostack entries. - BEGIN_PSEUDO_JS = 1u << 1, + // A normal JS frame. + JS_NORMAL = 2, - // This flag is used to indicate that an interpreter JS entry has OSR-ed - // into baseline. - OSR = 1u << 2, + // An interpreter JS frame that has OSR-ed into baseline. JS_NORMAL + // frames can be converted to JS_OSR and back. JS_OSR frames are + // ignored. + JS_OSR = 3, - // Union of all flags. - ALL = IS_CPP_ENTRY|BEGIN_PSEUDO_JS|OSR, - - // Mask for removing all flags except the category information. - CATEGORY_MASK = ~ALL + KIND_MASK = 0x3, }; // Keep these in sync with devtools/client/performance/modules/categories.js enum class Category : uint32_t { OTHER = 1u << 4, CSS = 1u << 5, JS = 1u << 6, GC = 1u << 7, CC = 1u << 8, NETWORK = 1u << 9, GRAPHICS = 1u << 10, STORAGE = 1u << 11, EVENTS = 1u << 12, FIRST = OTHER, - LAST = EVENTS + LAST = EVENTS, + + CATEGORY_MASK = ~uint32_t(Kind::KIND_MASK), }; - static_assert((static_cast<int>(Category::FIRST) & Flags::ALL) == 0, - "The category bitflags should not intersect with the other flags!"); + static_assert((uint32_t(Category::FIRST) & uint32_t(Kind::KIND_MASK)) == 0, + "Category overlaps with Kind"); // All of these methods are marked with the 'volatile' keyword because the // Gecko Profiler's representation of the stack is stored such that all // ProfileEntry instances are volatile. These methods would not be // available unless they were marked as volatile as well. - bool isCpp() const volatile { return hasFlag(IS_CPP_ENTRY); } - bool isJs() const volatile { return !isCpp(); } + bool isCpp() const volatile + { + Kind k = kind(); + return k == Kind::CPP_NORMAL || k == Kind::CPP_MARKER_FOR_JS; + } + + bool isJs() const volatile + { + Kind k = kind(); + return k == Kind::JS_NORMAL || k == Kind::JS_OSR; + } void setLabel(const char* aLabel) volatile { label_ = aLabel; } const char* label() const volatile { return label_; } const char* dynamicString() const volatile { return dynamicString_; } void initCppFrame(const char* aLabel, const char* aDynamicString, void* sp, uint32_t aLine, - js::ProfileEntry::Flags aFlags, js::ProfileEntry::Category aCategory) - volatile + Kind aKind, Category aCategory) volatile { label_ = aLabel; dynamicString_ = aDynamicString; spOrScript = sp; lineOrPcOffset = static_cast<int32_t>(aLine); - flagsAndCategory_ = aFlags | js::ProfileEntry::IS_CPP_ENTRY | uint32_t(aCategory); + kindAndCategory_ = uint32_t(aKind) | uint32_t(aCategory); + MOZ_ASSERT(isCpp()); } void initJsFrame(const char* aLabel, const char* aDynamicString, JSScript* aScript, jsbytecode* aPc) volatile { label_ = aLabel; dynamicString_ = aDynamicString; spOrScript = aScript; lineOrPcOffset = pcToOffset(aScript, aPc); - flagsAndCategory_ = uint32_t(js::ProfileEntry::Category::JS); // No flags needed. - } - - void setFlag(uint32_t flag) volatile { - MOZ_ASSERT(flag != IS_CPP_ENTRY); - flagsAndCategory_ |= flag; - } - void unsetFlag(uint32_t flag) volatile { - MOZ_ASSERT(flag != IS_CPP_ENTRY); - flagsAndCategory_ &= ~flag; - } - bool hasFlag(uint32_t flag) const volatile { - return bool(flagsAndCategory_ & flag); + kindAndCategory_ = uint32_t(Kind::JS_NORMAL) | uint32_t(Category::JS); + MOZ_ASSERT(isJs()); } - uint32_t category() const volatile { - return flagsAndCategory_ & CATEGORY_MASK; + void setKind(Kind aKind) volatile { + kindAndCategory_ = uint32_t(aKind) | uint32_t(category()); } - void setOSR() volatile { - MOZ_ASSERT(isJs()); - setFlag(OSR); + Kind kind() const volatile { + return Kind(kindAndCategory_ & uint32_t(Kind::KIND_MASK)); } - void unsetOSR() volatile { - MOZ_ASSERT(isJs()); - unsetFlag(OSR); - } - bool isOSR() const volatile { - return hasFlag(OSR); + + Category category() const volatile { + return Category(kindAndCategory_ & uint32_t(Category::CATEGORY_MASK)); } void* stackAddress() const volatile { MOZ_ASSERT(!isJs()); return spOrScript; } JS_PUBLIC_API(JSScript*) script() const volatile; @@ -219,20 +213,19 @@ class PseudoStack ~PseudoStack() { // The label macros keep a reference to the PseudoStack to avoid a TLS // access. If these are somehow not all cleared we will get a // use-after-free so better to crash now. MOZ_RELEASE_ASSERT(stackPointer == 0); } void pushCppFrame(const char* label, const char* dynamicString, void* sp, uint32_t line, - js::ProfileEntry::Category category, - js::ProfileEntry::Flags flags = js::ProfileEntry::Flags(0)) { + js::ProfileEntry::Kind kind, js::ProfileEntry::Category category) { if (stackPointer < MaxEntries) { - entries[stackPointer].initCppFrame(label, dynamicString, sp, line, flags, category); + entries[stackPointer].initCppFrame(label, dynamicString, sp, line, kind, category); } // This must happen at the end! The compiler will not reorder this // update because stackPointer is Atomic. stackPointer++; } void pushJsFrame(const char* label, const char* dynamicString, JSScript* script,
new file mode 100644 --- /dev/null +++ b/js/src/jit-test/tests/auto-regress/bug1357330.js @@ -0,0 +1,17 @@ +function byteValue(value) { + var isLittleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] !== 0; + var ui8 = new Uint8Array(new Float64Array([value]).buffer); + + var hex = "0123456789ABCDEF"; + var s = ""; + for (var i = 0; i < 8; ++i) { + var v = ui8[isLittleEndian ? 7 - i : i]; + s += hex[(v >> 4) & 0xf] + hex[v & 0xf]; + } + return s; +} + +var obj = {}; +Object.defineProperty(obj, "prop", {value: NaN}); +Object.defineProperty(obj, "prop", {value: -NaN}); +assertEq(byteValue(obj.prop), byteValue(NaN));
--- a/js/src/vm/GeckoProfiler.cpp +++ b/js/src/vm/GeckoProfiler.cpp @@ -370,19 +370,20 @@ GeckoProfilerEntryMarker::GeckoProfilerE MOZ_GUARD_OBJECT_NOTIFIER_INIT; if (!profiler->installed()) { profiler = nullptr; return; } spBefore_ = profiler->stackPointer(); // We want to push a CPP frame so the profiler can correctly order JS and native stacks. + // Only the sp value is important. profiler->pseudoStack_->pushCppFrame( - "js::RunScript", /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0, - js::ProfileEntry::Category::OTHER, js::ProfileEntry::BEGIN_PSEUDO_JS); + /* label = */ "", /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0, + ProfileEntry::Kind::CPP_MARKER_FOR_JS, ProfileEntry::Category::OTHER); profiler->pseudoStack_->pushJsFrame( "js::RunScript", /* dynamicString = */ nullptr, script, script->code()); } GeckoProfilerEntryMarker::~GeckoProfilerEntryMarker() { if (profiler == nullptr) @@ -401,17 +402,18 @@ AutoGeckoProfilerEntry::AutoGeckoProfile MOZ_GUARD_OBJECT_NOTIFIER_INIT; if (!profiler_->installed()) { profiler_ = nullptr; return; } spBefore_ = profiler_->stackPointer(); profiler_->pseudoStack_->pushCppFrame( - label, /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0, category); + label, /* dynamicString = */ nullptr, /* sp = */ this, /* line = */ 0, + ProfileEntry::Kind::CPP_NORMAL, category); } AutoGeckoProfilerEntry::~AutoGeckoProfilerEntry() { if (!profiler_) return; profiler_->pseudoStack_->pop(); @@ -434,33 +436,33 @@ GeckoProfilerBaselineOSRMarker::GeckoPro return; } spBefore_ = sp; if (sp == 0) return; volatile ProfileEntry& entry = profiler->pseudoStack_->entries[sp - 1]; - MOZ_ASSERT(entry.isJs()); - entry.setOSR(); + MOZ_ASSERT(entry.kind() == ProfileEntry::Kind::JS_NORMAL); + entry.setKind(ProfileEntry::Kind::JS_OSR); } GeckoProfilerBaselineOSRMarker::~GeckoProfilerBaselineOSRMarker() { if (profiler == nullptr) return; uint32_t sp = profiler->stackPointer(); MOZ_ASSERT(spBefore_ == sp); if (sp == 0) return; volatile ProfileEntry& entry = profiler->stack()[sp - 1]; - MOZ_ASSERT(entry.isJs()); - entry.unsetOSR(); + MOZ_ASSERT(entry.kind() == ProfileEntry::Kind::JS_OSR); + entry.setKind(ProfileEntry::Kind::JS_NORMAL); } JS_PUBLIC_API(JSScript*) ProfileEntry::script() const volatile { MOZ_ASSERT(isJs()); auto script = reinterpret_cast<JSScript*>(spOrScript); if (!script)
--- a/js/src/vm/NativeObject.cpp +++ b/js/src/vm/NativeObject.cpp @@ -1403,35 +1403,34 @@ GetExistingPropertyValue(JSContext* cx, MOZ_ASSERT(obj->contains(cx, prop.shape())); RootedValue receiver(cx, ObjectValue(*obj)); RootedShape shape(cx, prop.shape()); return GetExistingProperty<CanGC>(cx, receiver, obj, shape, vp); } /* - * If ES6 draft rev 37 9.1.6.3 ValidateAndApplyPropertyDescriptor step 4 would - * return early, because desc is redundant with an existing own property obj[id], - * then set *redundant = true and return true. + * If desc is redundant with an existing own property obj[id], then set + * |*redundant = true| and return true. */ static bool DefinePropertyIsRedundant(JSContext* cx, HandleNativeObject obj, HandleId id, Handle<PropertyResult> prop, unsigned shapeAttrs, Handle<PropertyDescriptor> desc, bool *redundant) { *redundant = false; - if (desc.hasConfigurable() && desc.configurable() != ((shapeAttrs & JSPROP_PERMANENT) == 0)) + if (desc.hasConfigurable() && desc.configurable() != IsConfigurable(shapeAttrs)) return true; - if (desc.hasEnumerable() && desc.enumerable() != ((shapeAttrs & JSPROP_ENUMERATE) != 0)) + if (desc.hasEnumerable() && desc.enumerable() != IsEnumerable(shapeAttrs)) return true; if (desc.isDataDescriptor()) { - if ((shapeAttrs & (JSPROP_GETTER | JSPROP_SETTER)) != 0) + if (IsAccessorDescriptor(shapeAttrs)) return true; - if (desc.hasWritable() && desc.writable() != ((shapeAttrs & JSPROP_READONLY) == 0)) + if (desc.hasWritable() && desc.writable() != IsWritable(shapeAttrs)) return true; if (desc.hasValue()) { // Get the current value of the existing property. RootedValue currentValue(cx); if (!prop.isDenseOrTypedArrayElement() && prop.shape()->hasSlot() && prop.shape()->hasDefaultGetter()) { @@ -1439,18 +1438,18 @@ DefinePropertyIsRedundant(JSContext* cx, // correctness assertion that's too strict for this particular // call site. For details, see bug 1125624 comments 13-16. currentValue.set(obj->getSlot(prop.shape()->slot())); } else { if (!GetExistingPropertyValue(cx, obj, id, prop, ¤tValue)) return false; } - // The specification calls for SameValue here, but it seems to be a - // bug. See <https://bugs.ecmascript.org/show_bug.cgi?id=3508>. + // Don't call SameValue here to ensure we properly update distinct + // NaN values. if (desc.value() != currentValue) return true; } GetterOp existingGetterOp = prop.isDenseOrTypedArrayElement() ? nullptr : prop.shape()->getter(); if (desc.getter() != existingGetterOp) return true; @@ -1478,17 +1477,18 @@ DefinePropertyIsRedundant(JSContext* cx, bool js::NativeDefineProperty(JSContext* cx, HandleNativeObject obj, HandleId id, Handle<PropertyDescriptor> desc_, ObjectOpResult& result) { desc_.assertValid(); - // Section numbers and step numbers below refer to ES2016. + // Section numbers and step numbers below refer to ES2018, draft rev + // 540b827fccf6122a984be99ab9af7be20e3b5562. // // This function aims to implement 9.1.6 [[DefineOwnProperty]] as well as // the [[DefineOwnProperty]] methods described in 9.4.2.1 (arrays), 9.4.4.2 // (arguments), and 9.4.5.3 (typed array views). // Dispense with custom behavior of exotic native objects first. if (obj->is<ArrayObject>()) { // 9.4.2.1 step 2. Redefining an array's length is very special. @@ -1515,18 +1515,18 @@ js::NativeDefineProperty(JSContext* cx, MOZ_ASSERT(!cx->helperThread()); return DefineTypedArrayElement(cx, obj, index, desc_, result); } } else if (obj->is<ArgumentsObject>()) { if (id == NameToId(cx->names().length)) { // Either we are resolving the .length property on this object, or // redefining it. In the latter case only, we must set a bit. To // distinguish the two cases, we note that when resolving, the - // property won't already exist; whereas the first time it is - // redefined, it will. + // JSPROP_RESOLVING mask is set; whereas the first time it is + // redefined, it isn't set. if ((desc_.attributes() & JSPROP_RESOLVING) == 0) obj->as<ArgumentsObject>().markLengthOverridden(); } else if (JSID_IS_SYMBOL(id) && JSID_TO_SYMBOL(id) == cx->wellKnownSymbols().iterator) { // Do same thing as .length for [@@iterator]. if ((desc_.attributes() & JSPROP_RESOLVING) == 0) obj->as<ArgumentsObject>().markIteratorOverridden(); } else if (JSID_IS_INT(id)) { if ((desc_.attributes() & JSPROP_RESOLVING) == 0) @@ -1564,17 +1564,17 @@ js::NativeDefineProperty(JSContext* cx, // Fill in missing desc fields with defaults. CompletePropertyDescriptor(&desc); if (!AddOrChangeProperty<IsAddOrChange::Add>(cx, obj, id, desc)) return false; return result.succeed(); } - // Steps 3-4. (Step 3 is a special case of step 4.) We use shapeAttrs as a + // Step 3 and 7.a.i.3, 8.a.iii, 10 (partially). We use shapeAttrs as a // stand-in for shape in many places below, since shape might not be a // pointer to a real Shape (see IsImplicitDenseOrTypedArrayElement). unsigned shapeAttrs = GetPropertyAttributes(obj, prop); bool redundant; if (!DefinePropertyIsRedundant(cx, obj, id, prop, shapeAttrs, desc, &redundant)) return false; if (redundant) { // In cases involving JSOP_NEWOBJECT and JSOP_INITPROP, obj can have a @@ -1591,33 +1591,33 @@ js::NativeDefineProperty(JSContext* cx, // global. The idea is that a DOM object can never have such a thing on // its proto chain directly on the web, so we should be OK optimizing // access to accessors found on such an object. Bug 1105518 contemplates // removing this hack. bool skipRedefineChecks = (desc.attributes() & JSPROP_REDEFINE_NONCONFIGURABLE) && obj->is<GlobalObject>() && !obj->getClass()->isDOMClass(); - // Step 5. + // Step 4. if (!IsConfigurable(shapeAttrs) && !skipRedefineChecks) { if (desc.hasConfigurable() && desc.configurable()) return result.fail(JSMSG_CANT_REDEFINE_PROP); if (desc.hasEnumerable() && desc.enumerable() != IsEnumerable(shapeAttrs)) return result.fail(JSMSG_CANT_REDEFINE_PROP); } // Fill in desc.[[Configurable]] and desc.[[Enumerable]] if missing. if (!desc.hasConfigurable()) desc.setConfigurable(IsConfigurable(shapeAttrs)); if (!desc.hasEnumerable()) desc.setEnumerable(IsEnumerable(shapeAttrs)); - // Steps 6-9. + // Steps 5-8. if (desc.isGenericDescriptor()) { - // Step 6. No further validation is required. + // Step 5. No further validation is required. // Fill in desc. A generic descriptor has none of these fields, so copy // everything from shape. MOZ_ASSERT(!desc.hasValue()); MOZ_ASSERT(!desc.hasWritable()); MOZ_ASSERT(!desc.hasGetterObject()); MOZ_ASSERT(!desc.hasSetterObject()); if (IsDataDescriptor(shapeAttrs)) { @@ -1626,32 +1626,34 @@ js::NativeDefineProperty(JSContext* cx, return false; desc.setValue(currentValue); desc.setWritable(IsWritable(shapeAttrs)); } else { desc.setGetterObject(prop.shape()->getterObject()); desc.setSetterObject(prop.shape()->setterObject()); } } else if (desc.isDataDescriptor() != IsDataDescriptor(shapeAttrs)) { - // Step 7. + // Step 6. if (!IsConfigurable(shapeAttrs) && !skipRedefineChecks) return result.fail(JSMSG_CANT_REDEFINE_PROP); if (prop.isDenseOrTypedArrayElement()) { MOZ_ASSERT(!obj->is<TypedArrayObject>()); if (!NativeObject::sparsifyDenseElement(cx, obj, JSID_TO_INT(id))) return false; prop.setNativeProperty(obj->lookup(cx, id)); } - // Fill in desc fields with default values (steps 7.b.i and 7.c.i). + // Fill in desc fields with default values (steps 6.b.i and 6.c.i). CompletePropertyDescriptor(&desc); } else if (desc.isDataDescriptor()) { - // Step 8. + // Step 7. bool frozen = !IsConfigurable(shapeAttrs) && !IsWritable(shapeAttrs); + + // Step 7.a.i.1. if (frozen && desc.hasWritable() && desc.writable() && !skipRedefineChecks) return result.fail(JSMSG_CANT_REDEFINE_PROP); if (frozen || !desc.hasValue()) { if (prop.isDenseOrTypedArrayElement()) { MOZ_ASSERT(!obj->is<TypedArrayObject>()); if (!NativeObject::sparsifyDenseElement(cx, obj, JSID_TO_INT(id))) return false; @@ -1661,62 +1663,72 @@ js::NativeDefineProperty(JSContext* cx, RootedValue currentValue(cx); if (!GetExistingPropertyValue(cx, obj, id, prop, ¤tValue)) return false; if (!desc.hasValue()) { // Fill in desc.[[Value]]. desc.setValue(currentValue); } else { - // Step 8.a.ii.1. + // Step 7.a.i.2. bool same; MOZ_ASSERT(!cx->helperThread()); if (!SameValue(cx, desc.value(), currentValue, &same)) return false; if (!same && !skipRedefineChecks) return result.fail(JSMSG_CANT_REDEFINE_PROP); } } + // Step 7.a.i.3. + if (frozen && !skipRedefineChecks) + return result.succeed(); + if (!desc.hasWritable()) desc.setWritable(IsWritable(shapeAttrs)); } else { - // Step 9. + // Step 8. MOZ_ASSERT(prop.shape()->isAccessorDescriptor()); MOZ_ASSERT(desc.isAccessorDescriptor()); // The spec says to use SameValue, but since the values in // question are objects, we can just compare pointers. if (desc.hasSetterObject()) { + // Step 8.a.i. if (!IsConfigurable(shapeAttrs) && desc.setterObject() != prop.shape()->setterObject() && !skipRedefineChecks) { return result.fail(JSMSG_CANT_REDEFINE_PROP); } } else { // Fill in desc.[[Set]] from shape. desc.setSetterObject(prop.shape()->setterObject()); } if (desc.hasGetterObject()) { + // Step 8.a.ii. if (!IsConfigurable(shapeAttrs) && desc.getterObject() != prop.shape()->getterObject() && !skipRedefineChecks) { return result.fail(JSMSG_CANT_REDEFINE_PROP); } } else { // Fill in desc.[[Get]] from shape. desc.setGetterObject(prop.shape()->getterObject()); } + + // Step 8.a.iii (Omitted). } + // Step 9. + if (!AddOrChangeProperty<IsAddOrChange::AddOrChange>(cx, obj, id, desc)) + return false; + // Step 10. - if (!AddOrChangeProperty<IsAddOrChange::AddOrChange>(cx, obj, id, desc)) - return false; return result.succeed(); } bool js::NativeDefineProperty(JSContext* cx, HandleNativeObject obj, HandleId id, HandleValue value, GetterOp getter, SetterOp setter, unsigned attrs, ObjectOpResult& result) {
--- a/js/xpconnect/tests/chrome/test_windowProxyDeadWrapper.html +++ b/js/xpconnect/tests/chrome/test_windowProxyDeadWrapper.html @@ -7,16 +7,17 @@ https://bugzilla.mozilla.org/show_bug.cg <meta charset="utf-8"> <title>Test for Bug 1223372</title> <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> <link rel="stylesheet" type="text/css" href="chrome://global/skin"/> <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> <script type="application/javascript"> /** Test for Bug 1223372 **/ +Components.utils.import("resource://testing-common/TestUtils.jsm"); function go() { SimpleTest.waitForExplicitFinish(); var frame = $('subframe'); frame.onload = null; var w = frame.contentWindow; @@ -35,21 +36,28 @@ function go() { function tryWindow() { if (w.document.title != 'empty test page') { info("Document not loaded yet - retrying"); SimpleTest.executeSoon(tryWindow); return; } + let winID = w.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .outerWindowID; // Remove the frame. This will nuke the WindowProxy wrapper from our chrome - // document's global, so evaluating 'this' in it will return a dead wrapper. + // document's global, so evaluating 'this' in it will return a dead wrapper + // once the window is destroyed. frame.remove(); - SimpleTest.executeSoon(function() { + TestUtils.topicObserved("outer-window-destroyed", (subject, data) => { + let id = subject.QueryInterface(SpecialPowers.Ci.nsISupportsPRUint64).data; + return id == winID; + }).then(() => { ok(checkDead(), "Expected a dead object wrapper"); // Wrapping the Window should throw now. var exc; try { Components.utils.getGlobalForObject(getObject()); } catch(e) { exc = e;
--- a/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js +++ b/js/xpconnect/tests/unit/test_nuke_webextension_wrappers.js @@ -2,44 +2,47 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/Timer.jsm"); Cu.import("resource://testing-common/TestUtils.jsm"); -let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService).wrappedJSObject; - -let oldAddonIdCallback = aps.setExtensionURIToAddonIdCallback(uri => uri.host); -do_register_cleanup(() => { - aps.setExtensionURIToAddonIdCallback(oldAddonIdCallback); -}); - function getWindowlessBrowser(url) { let ssm = Services.scriptSecurityManager; let uri = NetUtil.newURI(url); - // TODO: Remove when addonId origin attribute is removed. - let attrs = {addonId: uri.host}; - let principal = ssm.createCodebasePrincipal(uri, attrs); + let principal = ssm.createCodebasePrincipal(uri, {}); let webnav = Services.appShell.createWindowlessBrowser(false); let docShell = webnav.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDocShell); docShell.createAboutBlankContentViewer(principal); return webnav; } +function StubPolicy(id) { + return new WebExtensionPolicy({ + id, + mozExtensionHostname: id, + baseURL: `file:///{id}`, + + allowedOrigins: new MatchPatternSet([]), + localizeCallback(string) {}, + }); +} + add_task(function*() { - let ssm = Services.scriptSecurityManager; + let policy = StubPolicy("foo"); + policy.active = true; let webnavA = getWindowlessBrowser("moz-extension://foo/a.html"); let webnavB = getWindowlessBrowser("moz-extension://foo/b.html"); let winA = Cu.waiveXrays(webnavA.document.defaultView); let winB = Cu.waiveXrays(webnavB.document.defaultView); winB.winA = winA; @@ -62,9 +65,11 @@ add_task(function*() { yield TestUtils.topicObserved("inner-window-destroyed"); // Check that it can't be accessed after he window has been closed. let result = getThing(); ok(/dead object/.test(result), `Result should show a dead wrapper error: ${result}`); webnavA.close(); + + policy.active = false; });
--- a/layout/base/FrameProperties.h +++ b/layout/base/FrameProperties.h @@ -301,18 +301,16 @@ public: // As for the PropertyValue entries: we don't need to measure the mProperty // field of because it always points to static memory, and we can't measure // mValue because the type is opaque. // XXX Can we do better, e.g. with a method on the descriptor? return mProperties.ShallowSizeOfExcludingThis(aMallocSizeOf); } private: - friend class ::nsIFrame; - // Prevent copying of FrameProperties; we should always return/pass around // references to it, not copies! FrameProperties(const FrameProperties&) = delete; FrameProperties& operator=(const FrameProperties&) = delete; inline void SetInternal(UntypedDescriptor aProperty, void* aValue, const nsIFrame* aFrame);
--- a/layout/base/nsCSSFrameConstructor.cpp +++ b/layout/base/nsCSSFrameConstructor.cpp @@ -1648,17 +1648,18 @@ nsCSSFrameConstructor::NotifyDestroyingF NS_PRECONDITION(mUpdateCount != 0, "Should be in an update while destroying frames"); if (aFrame->GetStateBits() & NS_FRAME_GENERATED_CONTENT) { if (mQuoteList.DestroyNodesFor(aFrame)) QuotesDirty(); } - if (mCounterManager.DestroyNodesFor(aFrame)) { + if (aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE) && + mCounterManager.DestroyNodesFor(aFrame)) { // Technically we don't need to update anything if we destroyed only // USE nodes. However, this is unlikely to happen in the real world // since USE nodes generally go along with INCREMENT nodes. CountersDirty(); } RestyleManager()->NotifyDestroyingFrame(aFrame);
--- a/layout/base/nsCounterManager.cpp +++ b/layout/base/nsCounterManager.cpp @@ -13,22 +13,24 @@ #include "nsContentUtils.h" #include "nsIContent.h" #include "nsTArray.h" using namespace mozilla; bool nsCounterUseNode::InitTextFrame(nsGenConList* aList, - nsIFrame* aPseudoFrame, nsIFrame* aTextFrame) + nsIFrame* aPseudoFrame, + nsIFrame* aTextFrame) { nsCounterNode::InitTextFrame(aList, aPseudoFrame, aTextFrame); - nsCounterList *counterList = static_cast<nsCounterList*>(aList); + nsCounterList* counterList = static_cast<nsCounterList*>(aList); counterList->Insert(this); + aPseudoFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE); bool dirty = counterList->IsDirty(); if (!dirty) { if (counterList->IsLast(this)) { Calc(counterList); nsAutoString contentString; GetText(contentString); aTextFrame->GetContent()->SetText(contentString, false); } else { @@ -40,185 +42,188 @@ nsCounterUseNode::InitTextFrame(nsGenCon } } return false; } // assign the correct |mValueAfter| value to a node that has been inserted // Should be called immediately after calling |Insert|. -void nsCounterUseNode::Calc(nsCounterList *aList) +void +nsCounterUseNode::Calc(nsCounterList* aList) { - NS_ASSERTION(!aList->IsDirty(), - "Why are we calculating with a dirty list?"); - mValueAfter = aList->ValueBefore(this); + NS_ASSERTION(!aList->IsDirty(), + "Why are we calculating with a dirty list?"); + mValueAfter = aList->ValueBefore(this); } // assign the correct |mValueAfter| value to a node that has been inserted // Should be called immediately after calling |Insert|. -void nsCounterChangeNode::Calc(nsCounterList *aList) +void +nsCounterChangeNode::Calc(nsCounterList* aList) { - NS_ASSERTION(!aList->IsDirty(), - "Why are we calculating with a dirty list?"); - if (mType == RESET) { - mValueAfter = mChangeValue; - } else { - NS_ASSERTION(mType == INCREMENT, "invalid type"); - mValueAfter = nsCounterManager::IncrementCounter(aList->ValueBefore(this), - mChangeValue); - } + NS_ASSERTION(!aList->IsDirty(), "Why are we calculating with a dirty list?"); + if (mType == RESET) { + mValueAfter = mChangeValue; + } else { + NS_ASSERTION(mType == INCREMENT, "invalid type"); + mValueAfter = nsCounterManager::IncrementCounter(aList->ValueBefore(this), + mChangeValue); + } } // The text that should be displayed for this counter. void nsCounterUseNode::GetText(nsString& aResult) { - aResult.Truncate(); + aResult.Truncate(); + + AutoTArray<nsCounterNode*, 8> stack; + stack.AppendElement(static_cast<nsCounterNode*>(this)); - AutoTArray<nsCounterNode*, 8> stack; - stack.AppendElement(static_cast<nsCounterNode*>(this)); - - if (mAllCounters && mScopeStart) - for (nsCounterNode *n = mScopeStart; n->mScopePrev; n = n->mScopeStart) - stack.AppendElement(n->mScopePrev); + if (mAllCounters && mScopeStart) { + for (nsCounterNode* n = mScopeStart; n->mScopePrev; n = n->mScopeStart) { + stack.AppendElement(n->mScopePrev); + } + } - WritingMode wm = mPseudoFrame ? - mPseudoFrame->GetWritingMode() : WritingMode(); - for (uint32_t i = stack.Length() - 1;; --i) { - nsCounterNode *n = stack[i]; - nsAutoString text; - bool isTextRTL; - mCounterStyle->GetCounterText(n->mValueAfter, wm, text, isTextRTL); - aResult.Append(text); - if (i == 0) - break; - aResult.Append(mSeparator); + WritingMode wm = mPseudoFrame ? + mPseudoFrame->GetWritingMode() : WritingMode(); + for (uint32_t i = stack.Length() - 1;; --i) { + nsCounterNode* n = stack[i]; + nsAutoString text; + bool isTextRTL; + mCounterStyle->GetCounterText(n->mValueAfter, wm, text, isTextRTL); + aResult.Append(text); + if (i == 0) { + break; } + aResult.Append(mSeparator); + } } void -nsCounterList::SetScope(nsCounterNode *aNode) +nsCounterList::SetScope(nsCounterNode* aNode) { - // This function is responsible for setting |mScopeStart| and - // |mScopePrev| (whose purpose is described in nsCounterManager.h). - // We do this by starting from the node immediately preceding - // |aNode| in content tree order, which is reasonably likely to be - // the previous element in our scope (or, for a reset, the previous - // element in the containing scope, which is what we want). If - // we're not in the same scope that it is, then it's too deep in the - // frame tree, so we walk up parent scopes until we find something - // appropriate. + // This function is responsible for setting |mScopeStart| and + // |mScopePrev| (whose purpose is described in nsCounterManager.h). + // We do this by starting from the node immediately preceding + // |aNode| in content tree order, which is reasonably likely to be + // the previous element in our scope (or, for a reset, the previous + // element in the containing scope, which is what we want). If + // we're not in the same scope that it is, then it's too deep in the + // frame tree, so we walk up parent scopes until we find something + // appropriate. - if (aNode == First()) { - aNode->mScopeStart = nullptr; - aNode->mScopePrev = nullptr; - return; - } + if (aNode == First()) { + aNode->mScopeStart = nullptr; + aNode->mScopePrev = nullptr; + return; + } - // Get the content node for aNode's rendering object's *parent*, - // since scope includes siblings, so we want a descendant check on - // parents. - nsIContent *nodeContent = aNode->mPseudoFrame->GetContent()->GetParent(); + // Get the content node for aNode's rendering object's *parent*, + // since scope includes siblings, so we want a descendant check on + // parents. + nsIContent* nodeContent = aNode->mPseudoFrame->GetContent()->GetParent(); - for (nsCounterNode *prev = Prev(aNode), *start; - prev; prev = start->mScopePrev) { - // If |prev| starts a scope (because it's a real or implied - // reset), we want it as the scope start rather than the start - // of its enclosing scope. Otherwise, there's no enclosing - // scope, so the next thing in prev's scope shares its scope - // start. - start = (prev->mType == nsCounterNode::RESET || !prev->mScopeStart) - ? prev : prev->mScopeStart; + for (nsCounterNode* prev = Prev(aNode), *start; + prev; prev = start->mScopePrev) { + // If |prev| starts a scope (because it's a real or implied + // reset), we want it as the scope start rather than the start + // of its enclosing scope. Otherwise, there's no enclosing + // scope, so the next thing in prev's scope shares its scope + // start. + start = (prev->mType == nsCounterNode::RESET || !prev->mScopeStart) + ? prev : prev->mScopeStart; - // |startContent| is analogous to |nodeContent| (see above). - nsIContent *startContent = start->mPseudoFrame->GetContent()->GetParent(); - NS_ASSERTION(nodeContent || !startContent, - "null check on startContent should be sufficient to " - "null check nodeContent as well, since if nodeContent " - "is for the root, startContent (which is before it) " - "must be too"); + // |startContent| is analogous to |nodeContent| (see above). + nsIContent* startContent = start->mPseudoFrame->GetContent()->GetParent(); + NS_ASSERTION(nodeContent || !startContent, + "null check on startContent should be sufficient to " + "null check nodeContent as well, since if nodeContent " + "is for the root, startContent (which is before it) " + "must be too"); - // A reset's outer scope can't be a scope created by a sibling. - if (!(aNode->mType == nsCounterNode::RESET && - nodeContent == startContent) && - // everything is inside the root (except the case above, - // a second reset on the root) - (!startContent || - nsContentUtils::ContentIsDescendantOf(nodeContent, - startContent))) { - aNode->mScopeStart = start; - aNode->mScopePrev = prev; - return; - } + // A reset's outer scope can't be a scope created by a sibling. + if (!(aNode->mType == nsCounterNode::RESET && + nodeContent == startContent) && + // everything is inside the root (except the case above, + // a second reset on the root) + (!startContent || + nsContentUtils::ContentIsDescendantOf(nodeContent, + startContent))) { + aNode->mScopeStart = start; + aNode->mScopePrev = prev; + return; } + } - aNode->mScopeStart = nullptr; - aNode->mScopePrev = nullptr; + aNode->mScopeStart = nullptr; + aNode->mScopePrev = nullptr; } void nsCounterList::RecalcAll() { mDirty = false; for (nsCounterNode* node = First(); node; node = Next(node)) { SetScope(node); node->Calc(this); if (node->mType == nsCounterNode::USE) { - nsCounterUseNode *useNode = node->UseNode(); + nsCounterUseNode* useNode = node->UseNode(); // Null-check mText, since if the frame constructor isn't // batching, we could end up here while the node is being // constructed. if (useNode->mText) { nsAutoString text; useNode->GetText(text); useNode->mText->SetData(text); } } } } -nsCounterManager::nsCounterManager() - : mNames() +bool +nsCounterManager::AddCounterResetsAndIncrements(nsIFrame* aFrame) { -} + const nsStyleContent* styleContent = aFrame->StyleContent(); + if (!styleContent->CounterIncrementCount() && + !styleContent->CounterResetCount()) { + MOZ_ASSERT(!aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE)); + return false; + } -bool -nsCounterManager::AddCounterResetsAndIncrements(nsIFrame *aFrame) -{ - const nsStyleContent *styleContent = aFrame->StyleContent(); - if (!styleContent->CounterIncrementCount() && - !styleContent->CounterResetCount()) - return false; + aFrame->AddStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE); - // Add in order, resets first, so all the comparisons will be optimized - // for addition at the end of the list. - int32_t i, i_end; - bool dirty = false; - for (i = 0, i_end = styleContent->CounterResetCount(); i != i_end; ++i) - dirty |= AddResetOrIncrement(aFrame, i, styleContent->CounterResetAt(i), - nsCounterChangeNode::RESET); - for (i = 0, i_end = styleContent->CounterIncrementCount(); i != i_end; ++i) - dirty |= - AddResetOrIncrement(aFrame, i, styleContent->CounterIncrementAt(i), - nsCounterChangeNode::INCREMENT); - return dirty; + // Add in order, resets first, so all the comparisons will be optimized + // for addition at the end of the list. + int32_t i, i_end; + bool dirty = false; + for (i = 0, i_end = styleContent->CounterResetCount(); i != i_end; ++i) { + dirty |= AddResetOrIncrement(aFrame, i, styleContent->CounterResetAt(i), + nsCounterChangeNode::RESET); + } + for (i = 0, i_end = styleContent->CounterIncrementCount(); i != i_end; ++i) { + dirty |= AddResetOrIncrement(aFrame, i, styleContent->CounterIncrementAt(i), + nsCounterChangeNode::INCREMENT); + } + return dirty; } bool nsCounterManager::AddResetOrIncrement(nsIFrame* aFrame, int32_t aIndex, const nsStyleCounterData& aCounterData, nsCounterNode::Type aType) { nsCounterChangeNode* node = new nsCounterChangeNode(aFrame, aType, aCounterData.mValue, aIndex); nsCounterList* counterList = CounterListFor(aCounterData.mCounter); - counterList->Insert(node); if (!counterList->IsLast(node)) { // Tell the caller it's responsible for recalculating the entire // list. counterList->SetDirty(); return true; } @@ -228,48 +233,45 @@ nsCounterManager::AddResetOrIncrement(ns node->Calc(counterList); } return false; } nsCounterList* nsCounterManager::CounterListFor(const nsSubstring& aCounterName) { - // XXX Why doesn't nsTHashtable provide an API that allows us to use - // get/put in one hashtable lookup? - nsCounterList *counterList; - if (!mNames.Get(aCounterName, &counterList)) { - counterList = new nsCounterList(); - mNames.Put(aCounterName, counterList); - } - return counterList; + return mNames.LookupForAdd(aCounterName).OrInsert([]() { + return new nsCounterList(); + }); } void nsCounterManager::RecalcAll() { - for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) { - nsCounterList* list = iter.UserData(); - if (list->IsDirty()) { - list->RecalcAll(); - } + for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) { + nsCounterList* list = iter.UserData(); + if (list->IsDirty()) { + list->RecalcAll(); } + } } void nsCounterManager::SetAllDirty() { for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) { iter.UserData()->SetDirty(); } } bool -nsCounterManager::DestroyNodesFor(nsIFrame *aFrame) +nsCounterManager::DestroyNodesFor(nsIFrame* aFrame) { + MOZ_ASSERT(aFrame->HasAnyStateBits(NS_FRAME_HAS_CSS_COUNTER_STYLE), + "why call me?"); bool destroyedAny = false; for (auto iter = mNames.Iter(); !iter.Done(); iter.Next()) { nsCounterList* list = iter.UserData(); if (list->DestroyNodesFor(aFrame)) { destroyedAny = true; list->SetDirty(); } }
--- a/layout/base/nsCounterManager.h +++ b/layout/base/nsCounterManager.h @@ -202,17 +202,16 @@ private: }; /** * The counter manager maintains an |nsCounterList| for each named * counter to keep track of all scopes with that name. */ class nsCounterManager { public: - nsCounterManager(); // Returns true if dirty bool AddCounterResetsAndIncrements(nsIFrame *aFrame); // Gets the appropriate counter list, creating it if necessary. // Guaranteed to return non-null. (Uses an infallible hashtable API.) nsCounterList* CounterListFor(const nsSubstring& aCounterName); // Clean up data in any dirty counter lists.
--- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp @@ -2860,20 +2860,20 @@ nsLayoutUtils::TransformRect(nsIFrame* a Rect(-std::numeric_limits<Float>::max() * 0.5f, -std::numeric_limits<Float>::max() * 0.5f, std::numeric_limits<Float>::max(), std::numeric_limits<Float>::max())), Rect(-std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame * 0.5f, -std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame * 0.5f, std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame, std::numeric_limits<Float>::max() * devPixelsPerAppUnitFromFrame)); - aRect.x = toDevPixels.x / devPixelsPerAppUnitToFrame; - aRect.y = toDevPixels.y / devPixelsPerAppUnitToFrame; - aRect.width = toDevPixels.width / devPixelsPerAppUnitToFrame; - aRect.height = toDevPixels.height / devPixelsPerAppUnitToFrame; + aRect.x = NSToCoordRound(toDevPixels.x / devPixelsPerAppUnitToFrame); + aRect.y = NSToCoordRound(toDevPixels.y / devPixelsPerAppUnitToFrame); + aRect.width = NSToCoordRound(toDevPixels.width / devPixelsPerAppUnitToFrame); + aRect.height = NSToCoordRound(toDevPixels.height / devPixelsPerAppUnitToFrame); return TRANSFORM_SUCCEEDED; } nsRect nsLayoutUtils::GetRectRelativeToFrame(Element* aElement, nsIFrame* aFrame) { if (!aElement || !aFrame) { return nsRect();
--- a/layout/generic/nsFrameStateBits.h +++ b/layout/generic/nsFrameStateBits.h @@ -265,18 +265,18 @@ FRAME_STATE_BIT(Generic, 53, NS_FRAME_IS // Frame has a LayerActivityProperty property FRAME_STATE_BIT(Generic, 54, NS_FRAME_HAS_LAYER_ACTIVITY_PROPERTY) // Frame owns anonymous boxes whose style contexts it will need to update during // a stylo tree traversal. FRAME_STATE_BIT(Generic, 55, NS_FRAME_OWNS_ANON_BOXES) -// ** currently unused Generic bit ** -// FRAME_STATE_BIT(Generic, 56, ...) +// Frame maybe has a counter-reset/increment style +FRAME_STATE_BIT(Generic, 56, NS_FRAME_HAS_CSS_COUNTER_STYLE) // The display list of the frame can be handled by the shortcut for // COMMON CASE. FRAME_STATE_BIT(Generic, 57, NS_FRAME_SIMPLE_DISPLAYLIST) // Set for all descendants of MathML sub/supscript elements (other than the // base frame) to indicate that the SSTY font feature should be used. FRAME_STATE_BIT(Generic, 58, NS_FRAME_MATHML_SCRIPT_DESCENDANT)
--- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -436,16 +436,17 @@ ; [Browser Chrome Files] @BINPATH@/chrome/toolkit@JAREXT@ @BINPATH@/chrome/toolkit.manifest ; [Extensions] @BINPATH@/components/extensions-toolkit.manifest @BINPATH@/components/extensions-mobile.manifest +@BINPATH@/components/extension-process-script.js ; Features @BINPATH@/features/* ; DevTools #ifndef MOZ_GECKOVIEW_JAR @BINPATH@/chrome/devtools@JAREXT@ @BINPATH@/chrome/devtools.manifest
--- a/netwerk/protocol/res/ExtensionProtocolHandler.cpp +++ b/netwerk/protocol/res/ExtensionProtocolHandler.cpp @@ -1,47 +1,55 @@ /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #include "ExtensionProtocolHandler.h" -#include "nsIAddonPolicyService.h" +#include "mozilla/ExtensionPolicyService.h" #include "nsServiceManagerUtils.h" #include "nsIURL.h" #include "nsIChannel.h" #include "nsIStreamListener.h" #include "nsIInputStream.h" #include "nsIOutputStream.h" #include "nsIStreamConverterService.h" #include "nsNetUtil.h" #include "LoadInfo.h" #include "SimpleChannel.h" namespace mozilla { namespace net { +using extensions::URLInfo; + NS_IMPL_QUERY_INTERFACE(ExtensionProtocolHandler, nsISubstitutingProtocolHandler, nsIProtocolHandler, nsIProtocolHandlerWithDynamicFlags, nsISupportsWeakReference) NS_IMPL_ADDREF_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler) NS_IMPL_RELEASE_INHERITED(ExtensionProtocolHandler, SubstitutingProtocolHandler) +static inline ExtensionPolicyService& +EPS() +{ + return ExtensionPolicyService::GetSingleton(); +} + nsresult ExtensionProtocolHandler::GetFlagsForURI(nsIURI* aURI, uint32_t* aFlags) { // In general a moz-extension URI is only loadable by chrome, but a whitelisted // subset are web-accessible (and cross-origin fetchable). Check that whitelist. - nsCOMPtr<nsIAddonPolicyService> aps = do_GetService("@mozilla.org/addons/policy-service;1"); bool loadableByAnyone = false; - if (aps) { - nsresult rv = aps->ExtensionURILoadableByAnyone(aURI, &loadableByAnyone); - NS_ENSURE_SUCCESS(rv, rv); + + URLInfo url(aURI); + if (auto* policy = EPS().GetByURL(url)) { + loadableByAnyone = policy->IsPathWebAccessible(url.FilePath()); } *aFlags = URI_STD | URI_IS_LOCAL_RESOURCE | (loadableByAnyone ? (URI_LOADABLE_BY_ANYONE | URI_FETCHABLE_BY_ANYONE) : URI_DANGEROUS_TO_LOAD); return NS_OK; } bool ExtensionProtocolHandler::ResolveSpecialCases(const nsACString& aHost, @@ -50,32 +58,25 @@ ExtensionProtocolHandler::ResolveSpecial nsACString& aResult) { // Create special moz-extension:-pages such as moz-extension://foo/_blank.html // for all registered extensions. We can't just do this as a substitution // because substitutions can only match on host. if (!SubstitutingProtocolHandler::HasSubstitution(aHost)) { return false; } + if (aPathname.EqualsLiteral("/_blank.html")) { aResult.AssignLiteral("about:blank"); return true; } + if (aPathname.EqualsLiteral("/_generated_background_page.html")) { - nsCOMPtr<nsIAddonPolicyService> aps = - do_GetService("@mozilla.org/addons/policy-service;1"); - if (!aps) { - return false; - } - nsresult rv = aps->GetGeneratedBackgroundPageUrl(aHost, aResult); - NS_ENSURE_SUCCESS(rv, false); - if (!aResult.IsEmpty()) { - MOZ_RELEASE_ASSERT(Substring(aResult, 0, 5).Equals("data:")); - return true; - } + Unused << EPS().GetGeneratedBackgroundPageUrl(aHost, aResult); + return !aResult.IsEmpty(); } return false; } static inline Result<Ok, nsresult> WrapNSResult(nsresult aRv) {
--- a/toolkit/components/build/nsToolkitCompsCID.h +++ b/toolkit/components/build/nsToolkitCompsCID.h @@ -181,8 +181,14 @@ #define NS_APPLICATION_REPUTATION_SERVICE_CID \ { 0x8576c950, 0xf4a2, 0x11e2, { 0xb7, 0x78, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } } #define NS_ADDONCONTENTPOLICY_CID \ { 0xc26a8241, 0xecf4, 0x4aed, { 0x9f, 0x3c, 0xf1, 0xf5, 0xc7, 0x13, 0xb9, 0xa5 } } #define NS_ADDON_PATH_SERVICE_CID \ { 0xa39f39d0, 0xdfb6, 0x11e3, { 0x8b, 0x68, 0x08, 0x00, 0x20, 0x0c, 0x9a, 0x66 } } + +#define NS_ADDON_POLICY_SERVICE_CID \ +{ 0x562de129, 0x8338, 0x482c, { 0xbb, 0x96, 0xa1, 0xff, 0x09, 0xee, 0x53, 0xcc } } + +#define NS_ADDON_POLICY_SERVICE_CONTRACTID \ + "@mozilla.org/addons/policy-service;1"
--- a/toolkit/components/build/nsToolkitCompsModule.cpp +++ b/toolkit/components/build/nsToolkitCompsModule.cpp @@ -32,16 +32,17 @@ #include "nsUrlClassifierPrefixSet.h" #include "nsBrowserStatusFilter.h" #include "mozilla/FinalizationWitnessService.h" #include "mozilla/NativeOSFileInternals.h" #include "mozilla/AddonContentPolicy.h" #include "mozilla/AddonManagerStartup.h" #include "mozilla/AddonPathService.h" +#include "mozilla/ExtensionPolicyService.h" #if defined(XP_WIN) #include "NativeFileWatcherWin.h" #else #include "NativeFileWatcherNotSupported.h" #endif // (XP_WIN) #include "nsWebRequestListener.h" @@ -121,16 +122,17 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(nsUpdateP #endif NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(FinalizationWitnessService, Init) NS_GENERIC_FACTORY_CONSTRUCTOR(NativeOSFileInternalsService) NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init) NS_GENERIC_FACTORY_CONSTRUCTOR(AddonContentPolicy) NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance) NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonManagerStartup, AddonManagerStartup::GetInstance) +NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(ExtensionPolicyService, ExtensionPolicyService::GetInstance) NS_GENERIC_FACTORY_CONSTRUCTOR(nsWebRequestListener) NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID); #if defined(MOZ_HAS_PERFSTATS) NS_DEFINE_NAMED_CID(NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID); #endif // defined (MOZ_HAS_PERFSTATS) @@ -156,16 +158,17 @@ NS_DEFINE_NAMED_CID(NS_BROWSERSTATUSFILT #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID) NS_DEFINE_NAMED_CID(NS_UPDATEPROCESSOR_CID); #endif NS_DEFINE_NAMED_CID(FINALIZATIONWITNESSSERVICE_CID); NS_DEFINE_NAMED_CID(NATIVE_OSFILE_INTERNALS_SERVICE_CID); NS_DEFINE_NAMED_CID(NS_ADDONCONTENTPOLICY_CID); NS_DEFINE_NAMED_CID(NS_ADDON_PATH_SERVICE_CID); NS_DEFINE_NAMED_CID(NS_ADDON_MANAGER_STARTUP_CID); +NS_DEFINE_NAMED_CID(NS_ADDON_POLICY_SERVICE_CID); NS_DEFINE_NAMED_CID(NATIVE_FILEWATCHER_SERVICE_CID); NS_DEFINE_NAMED_CID(NS_WEBREQUESTLISTENER_CID); static const Module::CIDEntry kToolkitCIDs[] = { { &kNS_TOOLKIT_APPSTARTUP_CID, false, nullptr, nsAppStartupConstructor }, #if defined(MOZ_HAS_TERMINATOR) { &kNS_TOOLKIT_TERMINATOR_CID, false, nullptr, nsTerminatorConstructor }, #endif @@ -191,16 +194,17 @@ static const Module::CIDEntry kToolkitCI #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID) { &kNS_UPDATEPROCESSOR_CID, false, nullptr, nsUpdateProcessorConstructor }, #endif { &kFINALIZATIONWITNESSSERVICE_CID, false, nullptr, FinalizationWitnessServiceConstructor }, { &kNATIVE_OSFILE_INTERNALS_SERVICE_CID, false, nullptr, NativeOSFileInternalsServiceConstructor }, { &kNS_ADDONCONTENTPOLICY_CID, false, nullptr, AddonContentPolicyConstructor }, { &kNS_ADDON_PATH_SERVICE_CID, false, nullptr, AddonPathServiceConstructor }, { &kNS_ADDON_MANAGER_STARTUP_CID, false, nullptr, AddonManagerStartupConstructor }, + { &kNS_ADDON_POLICY_SERVICE_CID, false, nullptr, ExtensionPolicyServiceConstructor }, { &kNATIVE_FILEWATCHER_SERVICE_CID, false, nullptr, NativeFileWatcherServiceConstructor }, { &kNS_WEBREQUESTLISTENER_CID, false, nullptr, nsWebRequestListenerConstructor }, { nullptr } }; static const Module::ContractIDEntry kToolkitContracts[] = { { NS_APPSTARTUP_CONTRACTID, &kNS_TOOLKIT_APPSTARTUP_CID }, #if defined(MOZ_HAS_TERMINATOR) @@ -229,16 +233,17 @@ static const Module::ContractIDEntry kTo #if defined(MOZ_UPDATER) && !defined(MOZ_WIDGET_ANDROID) { NS_UPDATEPROCESSOR_CONTRACTID, &kNS_UPDATEPROCESSOR_CID }, #endif { FINALIZATIONWITNESSSERVICE_CONTRACTID, &kFINALIZATIONWITNESSSERVICE_CID }, { NATIVE_OSFILE_INTERNALS_SERVICE_CONTRACTID, &kNATIVE_OSFILE_INTERNALS_SERVICE_CID }, { NS_ADDONCONTENTPOLICY_CONTRACTID, &kNS_ADDONCONTENTPOLICY_CID }, { NS_ADDONPATHSERVICE_CONTRACTID, &kNS_ADDON_PATH_SERVICE_CID }, { NS_ADDONMANAGERSTARTUP_CONTRACTID, &kNS_ADDON_MANAGER_STARTUP_CID }, + { NS_ADDON_POLICY_SERVICE_CONTRACTID, &kNS_ADDON_POLICY_SERVICE_CID }, { NATIVE_FILEWATCHER_SERVICE_CONTRACTID, &kNATIVE_FILEWATCHER_SERVICE_CID }, { NS_WEBREQUESTLISTENER_CONTRACTID, &kNS_WEBREQUESTLISTENER_CID }, { nullptr } }; static const mozilla::Module::CategoryEntry kToolkitCategories[] = { { "content-policy", NS_ADDONCONTENTPOLICY_CONTRACTID, NS_ADDONCONTENTPOLICY_CONTRACTID }, { nullptr }
--- a/toolkit/components/extensions/.eslintrc.js +++ b/toolkit/components/extensions/.eslintrc.js @@ -4,16 +4,23 @@ module.exports = { "globals": { "Cc": true, "Ci": true, "Cr": true, "Cu": true, "TextDecoder": false, "TextEncoder": false, + + "MatchGlob": false, + "MatchPattern": true, + "MatchPatternSet": false, + "WebExtensionContentScript": false, + "WebExtensionPolicy": false, + // Specific to WebExtensions: "AppConstants": true, "Extension": true, "ExtensionAPI": true, "ExtensionManagement": true, "ExtensionUtils": true, "extensions": true, "getContainerForCookieStoreId": true,
--- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -57,20 +57,16 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "ExtensionStorage", "resource://gre/modules/ExtensionStorage.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestCommon", "resource://testing-common/ExtensionTestCommon.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Locale", "resource://gre/modules/Locale.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Log", "resource://gre/modules/Log.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", "resource://gre/modules/MessageChannel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); @@ -78,24 +74,31 @@ XPCOMUtils.defineLazyModuleGetter(this, "resource://gre/modules/Preferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "require", "resource://devtools/shared/Loader.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Schemas", "resource://gre/modules/Schemas.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", "resource://gre/modules/TelemetryStopwatch.jsm"); -Cu.import("resource://gre/modules/ExtensionManagement.jsm"); +XPCOMUtils.defineLazyGetter( + this, "processScript", + () => Cc["@mozilla.org/webextensions/extension-process-script;1"] + .getService().wrappedJSObject); + Cu.import("resource://gre/modules/ExtensionParent.jsm"); Cu.import("resource://gre/modules/ExtensionUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "uuidGen", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); +XPCOMUtils.defineLazyPreferenceGetter(this, "useRemoteWebExtensions", + "extensions.webextensions.remote", false); + var { GlobalManager, ParentAPIManager, apiManager: Management, } = ExtensionParent; const { classifyPermission, @@ -422,17 +425,17 @@ this.ExtensionData = class { // This method should return a structured representation of any // capabilities this extension has access to, as derived from the // manifest. The current implementation just returns the contents // of the permissions attribute, if we add things like url_overrides, // they should also be added here. get userPermissions() { let result = { - origins: this.whiteListedHosts.pat, + origins: this.whiteListedHosts.patterns.map(matcher => matcher.pattern), apis: [...this.apiNames], }; if (Array.isArray(this.manifest.content_scripts)) { for (let entry of this.manifest.content_scripts) { result.origins.push(...entry.matches); } } @@ -530,25 +533,29 @@ this.ExtensionData = class { if (perm === "geckoProfiler") { const acceptedExtensions = Preferences.get("extensions.geckoProfiler.acceptedExtensionIds"); if (!acceptedExtensions.split(",").includes(this.id)) { this.manifestError("Only whitelisted extensions are allowed to access the geckoProfiler."); continue; } } - this.permissions.add(perm); let type = classifyPermission(perm); if (type.origin) { - whitelist.push(perm); + let matcher = new MatchPattern(perm, {ignorePath: true}); + + whitelist.push(matcher); + perm = matcher.pattern; } else if (type.api) { this.apiNames.add(type.api); } + + this.permissions.add(perm); } - this.whiteListedHosts = new MatchPattern(whitelist); + this.whiteListedHosts = new MatchPatternSet(whitelist); for (let api of this.apiNames) { this.dependencies.add(`${api}@experiments.addons.mozilla.org`); } return this.manifest; } @@ -699,17 +706,17 @@ this.Extension = class extends Extension this.addonData = addonData; this.startupReason = startupReason; if (["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)) { StartupCache.clearAddonData(addonData.id); } - this.remote = ExtensionManagement.useRemoteWebExtensions; + this.remote = useRemoteWebExtensions; if (this.remote && processCount !== 1) { throw new Error("Out-of-process WebExtensions are not supported with multiple child processes"); } if (this.remote && !Services.prefs.getBoolPref("layers.popups.compositing.enabled", false)) { Cu.reportError(new Error("Remote extensions should not be enabled without also setting " + "the layers.popups.compositing.enabled preference to true")); } @@ -737,28 +744,40 @@ this.Extension = class extends Extension /* eslint-disable mozilla/balanced-listeners */ this.on("add-permissions", (ignoreEvent, permissions) => { for (let perm of permissions.permissions) { this.permissions.add(perm); } if (permissions.origins.length > 0) { - this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins)); + let patterns = this.whiteListedHosts.patterns.map(host => host.pattern); + + this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins], + {ignorePath: true}); } + + this.policy.permissions = Array.from(this.permissions); + this.policy.allowedOrigins = this.whiteListedHosts; }); this.on("remove-permissions", (ignoreEvent, permissions) => { for (let perm of permissions.permissions) { this.permissions.delete(perm); } - for (let origin of permissions.origins) { - this.whiteListedHosts.removeOne(origin); - } + let origins = permissions.origins.map( + origin => new MatchPattern(origin, {ignorePath: true}).pattern); + + this.whiteListedHosts = new MatchPatternSet( + this.whiteListedHosts.patterns + .filter(host => !origins.includes(host.pattern))); + + this.policy.permissions = Array.from(this.permissions); + this.policy.allowedOrigins = this.whiteListedHosts; }); /* eslint-enable mozilla/balanced-listeners */ } static generateXPI(data) { return ExtensionTestCommon.generateXPI(data); } @@ -855,18 +874,18 @@ this.Extension = class extends Extension return { id: this.id, uuid: this.uuid, instanceId: this.instanceId, manifest: this.manifest, resourceURL: this.addonData.resourceURI.spec, baseURL: this.baseURI.spec, content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase - webAccessibleResources: this.webAccessibleResources.serialize(), - whiteListedHosts: this.whiteListedHosts.serialize(), + webAccessibleResources: this.webAccessibleResources.map(res => res.glob), + whiteListedHosts: this.whiteListedHosts.patterns.map(pat => pat.pattern), localeData: this.localeData.serialize(), permissions: this.permissions, principal: this.principal, optionalPermissions: this.manifest.optional_permissions, }; } broadcast(msg, data) { @@ -899,24 +918,16 @@ this.Extension = class extends Extension Services.obs.addObserver(observer, "message-manager-close"); Services.obs.addObserver(observer, "message-manager-disconnect"); ppmm.broadcastAsyncMessage(msg, data); }); } runManifest(manifest) { - // Strip leading slashes from web_accessible_resources. - let strippedWebAccessibleResources = []; - if (manifest.web_accessible_resources) { - strippedWebAccessibleResources = manifest.web_accessible_resources.map(path => path.replace(/^\/+/, "")); - } - - this.webAccessibleResources = new MatchGlobs(strippedWebAccessibleResources); - let promises = []; for (let directive in manifest) { if (manifest[directive] !== null) { promises.push(Management.emit(`manifest_${directive}`, directive, this, manifest)); promises.push(Management.asyncEmitManifestEntry(this, directive)); } } @@ -966,25 +977,37 @@ this.Extension = class extends Extension } startup() { this.startupPromise = this._startup(); return this.startupPromise; } async _startup() { + // Create a temporary policy object for the devtools and add-on + // manager callers that depend on it being available early. + this.policy = new WebExtensionPolicy({ + id: this.id, + mozExtensionHostname: this.uuid, + baseURL: this.baseURI.spec, + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + }); + if (!WebExtensionPolicy.getByID(this.id)) { + // The add-on manager doesn't handle async startup and shutdown, + // so during upgrades and add-on restarts, startup() gets called + // before the last shutdown has completed, and this fails when + // there's another active add-on with the same ID. + this.policy.active = true; + } + TelemetryStopwatch.start("WEBEXT_EXTENSION_STARTUP_MS", this); - this.started = false; - try { let [, perms] = await Promise.all([this.loadManifest(), ExtensionPermissions.get(this)]); - ExtensionManagement.startupExtension(this.uuid, this.addonData.resourceURI, this); - this.started = true; - if (!this.hasShutdown) { await this.initLocale(); } if (this.errors.length) { return Promise.reject({errors: this.errors}); } @@ -994,38 +1017,50 @@ this.Extension = class extends Extension GlobalManager.init(this); // Apply optional permissions for (let perm of perms.permissions) { this.permissions.add(perm); } if (perms.origins.length > 0) { - this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...perms.origins)); + let patterns = this.whiteListedHosts.patterns.map(host => host.pattern); + + this.whiteListedHosts = new MatchPatternSet([...patterns, ...perms.origins], + {ignorePath: true}); } + // Normalize all patterns to contain a single leading / + let resources = (this.manifest.web_accessible_resources || []) + .map(path => path.replace(/^\/*/, "/")); + + this.webAccessibleResources = resources.map(res => new MatchGlob(res)); + + + this.policy.active = false; + this.policy = processScript.initExtension(this.serialize(), this); + // The "startup" Management event sent on the extension instance itself // is emitted just before the Management "startup" event, // and it is used to run code that needs to be executed before // any of the "startup" listeners. this.emit("startup", this); Management.emit("startup", this); await this.runManifest(this.manifest); Management.emit("ready", this); this.emit("ready"); TelemetryStopwatch.finish("WEBEXT_EXTENSION_STARTUP_MS", this); } catch (e) { dump(`Extension error: ${e.message} ${e.filename || e.fileName}:${e.lineNumber} :: ${e.stack || new Error().stack}\n`); Cu.reportError(e); - if (this.started) { - this.started = false; - ExtensionManagement.shutdownExtension(this.uuid); + if (this.policy) { + this.policy.active = false; } this.cleanupGeneratedFile(); throw e; } this.startupPromise = null; @@ -1057,35 +1092,34 @@ this.Extension = class extends Extension } } catch (e) { Cu.reportError(e); } this.shutdownReason = reason; this.hasShutdown = true; - if (!this.started) { + if (!this.policy) { return; } if (this.cleanupFile || ["ADDON_INSTALL", "ADDON_UNINSTALL", "ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(reason)) { StartupCache.clearAddonData(this.id); } let data = Services.ppmm.initialProcessData; data["Extension:Extensions"] = data["Extension:Extensions"].filter(e => e.id !== this.id); Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this); if (!this.manifest) { - ExtensionManagement.shutdownExtension(this.uuid); + this.policy.active = false; - this.cleanupGeneratedFile(); - return; + return this.cleanupGeneratedFile(); } GlobalManager.uninit(this); for (let obj of this.onShutdown) { obj.close(); } @@ -1093,21 +1127,21 @@ this.Extension = class extends Extension api.destroy(); } ParentAPIManager.shutdownExtension(this.id); Management.emit("shutdown", this); this.emit("shutdown"); - Services.ppmm.broadcastAsyncMessage("Extension:Shutdown", {id: this.id}); + await this.broadcast("Extension:Shutdown", {id: this.id}); MessageChannel.abortResponses({extensionId: this.id}); - ExtensionManagement.shutdownExtension(this.uuid); + this.policy.active = false; return this.cleanupGeneratedFile(); } observe(subject, topic, data) { if (topic === "xpcom-shutdown") { this.cleanupGeneratedFile(); } @@ -1132,13 +1166,13 @@ this.Extension = class extends Extension get name() { return this.manifest.name; } get optionalOrigins() { if (this._optionalOrigins == null) { let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm).origin); - this._optionalOrigins = new MatchPattern(origins); + this._optionalOrigins = new MatchPatternSet(origins, {ignorePath: true}); } return this._optionalOrigins; } };
--- a/toolkit/components/extensions/ExtensionAPI.jsm +++ b/toolkit/components/extensions/ExtensionAPI.jsm @@ -5,17 +5,16 @@ "use strict"; this.EXPORTED_SYMBOLS = ["ExtensionAPI", "ExtensionAPIs"]; /* exported ExtensionAPIs */ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; -Cu.import("resource://gre/modules/ExtensionManagement.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI", "resource://gre/modules/Console.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter", "resource://gre/modules/EventEmitter.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Schemas", @@ -42,17 +41,17 @@ class ExtensionAPI { } getAPI(context) { throw new Error("Not Implemented"); } } var ExtensionAPIs = { - apis: ExtensionManagement.APIs.apis, + apis: new Map(), load(apiName) { let api = this.apis.get(apiName); if (api.loadPromise) { return api.loadPromise; } @@ -90,9 +89,25 @@ var ExtensionAPIs = { let {schema} = api; Schemas.unload(schema); Cu.nukeSandbox(api.sandbox); api.sandbox = null; api.loadPromise = null; }, + + register(namespace, schema, script) { + if (this.apis.has(namespace)) { + throw new Error(`API namespace already exists: ${namespace}`); + } + + this.apis.set(namespace, {schema, script}); + }, + + unregister(namespace) { + if (!this.apis.has(namespace)) { + throw new Error(`API namespace does not exist: ${namespace}`); + } + + this.apis.delete(namespace); + }, };
--- a/toolkit/components/extensions/ExtensionChild.jsm +++ b/toolkit/components/extensions/ExtensionChild.jsm @@ -21,20 +21,16 @@ const Cc = Components.classes; const Cu = Components.utils; const Cr = Components.results; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", "resource://gre/modules/MessageChannel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NativeApp", "resource://gre/modules/NativeMessaging.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils", "resource://gre/modules/PromiseUtils.jsm"); Cu.import("resource://gre/modules/ExtensionCommon.jsm"); @@ -478,18 +474,18 @@ class BrowserExtensionContent extends Ev this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`; Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this); defineLazyGetter(this, "scripts", () => { return data.content_scripts.map(scriptData => new ExtensionContent.Script(this, scriptData)); }); - this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources); - this.whiteListedHosts = new MatchPattern(data.whiteListedHosts); + this.webAccessibleResources = data.webAccessibleResources.map(res => new MatchGlob(res)); + this.whiteListedHosts = new MatchPatternSet(data.whiteListedHosts, {ignorePath: true}); this.permissions = data.permissions; this.optionalPermissions = data.optionalPermissions; this.principal = data.principal; this.localeData = new LocaleData(data.localeData); this.manifest = data.manifest; this.baseURI = Services.io.newURI(data.baseURL); @@ -504,31 +500,47 @@ class BrowserExtensionContent extends Ev this.on("add-permissions", (ignoreEvent, permissions) => { if (permissions.permissions.length > 0) { for (let perm of permissions.permissions) { this.permissions.add(perm); } } if (permissions.origins.length > 0) { - this.whiteListedHosts = new MatchPattern(this.whiteListedHosts.pat.concat(...permissions.origins)); + let patterns = this.whiteListedHosts.patterns.map(host => host.pattern); + + this.whiteListedHosts = new MatchPatternSet([...patterns, ...permissions.origins], + {ignorePath: true}); + } + + if (this.policy) { + this.policy.permissions = Array.from(this.permissions); + this.policy.allowedOrigins = this.whiteListedHosts; } }); this.on("remove-permissions", (ignoreEvent, permissions) => { if (permissions.permissions.length > 0) { for (let perm of permissions.permissions) { this.permissions.delete(perm); } } if (permissions.origins.length > 0) { - for (let origin of permissions.origins) { - this.whiteListedHosts.removeOne(origin); - } + let origins = permissions.origins.map( + origin => new MatchPattern(origin, {ignorePath: true}).pattern); + + this.whiteListedHosts = new MatchPatternSet( + this.whiteListedHosts.patterns + .filter(host => !origins.includes(host.pattern))); + } + + if (this.policy) { + this.policy.permissions = Array.from(this.permissions); + this.policy.allowedOrigins = this.whiteListedHosts; } }); /* eslint-enable mozilla/balanced-listeners */ ExtensionManager.extensions.set(this.id, this); } shutdown() {
--- a/toolkit/components/extensions/ExtensionCommon.jsm +++ b/toolkit/components/extensions/ExtensionCommon.jsm @@ -1062,17 +1062,17 @@ class SchemaAPIManager extends EventEmit * @returns {object} A sandbox that is used as the global by `loadScript`. */ _createExtGlobal() { let global = Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal(), { wantXrays: false, sandboxName: `Namespace of ext-*.js scripts for ${this.processType}`, }); - Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, extensions: this}); + Object.assign(global, {global, Cc, Ci, Cu, Cr, XPCOMUtils, ChromeWorker, MatchPattern, MatchPatternSet, extensions: this}); Cu.import("resource://gre/modules/AppConstants.jsm", global); Cu.import("resource://gre/modules/ExtensionAPI.jsm", global); XPCOMUtils.defineLazyGetter(global, "console", getConsole); XPCOMUtils.defineLazyModuleGetter(global, "ExtensionUtils", "resource://gre/modules/ExtensionUtils.jsm");
--- a/toolkit/components/extensions/ExtensionContent.jsm +++ b/toolkit/components/extensions/ExtensionContent.jsm @@ -191,63 +191,72 @@ defineLazyGetter(BrowserExtensionContent }); defineLazyGetter(BrowserExtensionContent.prototype, "authorCSS", () => { return new CSSCache(Ci.nsIStyleSheetService.AUTHOR_SHEET); }); // Represents a content script. class Script { - constructor(extension, options) { + constructor(extension, matcher) { this.extension = extension; - this.options = options; + this.matcher = matcher; - this.runAt = this.options.run_at; - this.js = this.options.js || []; - this.css = this.options.css || []; - this.remove_css = this.options.remove_css; - this.css_origin = this.options.css_origin; + this.runAt = this.matcher.runAt; + this.js = this.matcher.jsPaths; + this.css = this.matcher.cssPaths; + this.removeCSS = this.matcher.removeCSS; + this.cssOrigin = this.matcher.cssOrigin; - this.cssCache = extension[this.css_origin === "user" ? "userCSS" - : "authorCSS"]; - this.scriptCache = extension[options.wantReturnValue ? "dynamicScripts" + this.cssCache = extension[this.cssOrigin === "user" ? "userCSS" + : "authorCSS"]; + this.scriptCache = extension[matcher.wantReturnValue ? "dynamicScripts" : "staticScripts"]; - if (options.wantReturnValue) { + if (matcher.wantReturnValue) { this.compileScripts(); this.loadCSS(); } - this.requiresCleanup = !this.remove_css && (this.css.length > 0 || options.cssCode); + this.requiresCleanup = !this.removeCss && (this.css.length > 0 || matcher.cssCode); } compileScripts() { return this.js.map(url => this.scriptCache.get(url)); } loadCSS() { return this.cssURLs.map(url => this.cssCache.get(url)); } + preload() { + this.loadCSS(); + this.compileScripts(); + } + cleanup(window) { - if (!this.remove_css && this.cssURLs.length) { + if (!this.removeCss && this.cssURLs.length) { let winUtils = getWinUtils(window); - let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET; + let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET; for (let url of this.cssURLs) { this.cssCache.deleteDocument(url, window.document); runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type); } // Clear any sheets that were kept alive past their timeout as // a result of living in this document. this.cssCache.clear(CSS_EXPIRY_TIMEOUT_MS); } } + matchesWindow(window) { + return this.matcher.matchesWindow(window); + } + async injectInto(window) { let context = this.extension.getContext(window); if (this.runAt === "document_end") { await promiseDocumentReady(window.document); } else if (this.runAt === "document_idle") { await promiseDocumentLoaded(window.document); } @@ -271,19 +280,19 @@ class Script { context.addScript(this); } let cssPromise; if (this.cssURLs.length) { let window = context.contentWindow; let winUtils = getWinUtils(window); - let type = this.css_origin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET; + let type = this.cssOrigin === "user" ? winUtils.USER_SHEET : winUtils.AUTHOR_SHEET; - if (this.remove_css) { + if (this.removeCSS) { for (let url of this.cssURLs) { this.cssCache.deleteDocument(url, window.document); runSafeSyncWithoutClone(winUtils.removeSheetUsingURIString, url, type); } } else { cssPromise = Promise.all(this.loadCSS()).then(sheets => { let window = context.contentWindow; @@ -320,31 +329,31 @@ class Script { } // The evaluations below may throw, in which case the promise will be // automatically rejected. for (let script of scripts) { result = script.executeInGlobal(context.cloneScope); } - if (this.options.jsCode) { - result = Cu.evalInSandbox(this.options.jsCode, context.cloneScope, "latest"); + if (this.matcher.jsCode) { + result = Cu.evalInSandbox(this.matcher.jsCode, context.cloneScope, "latest"); } await cssPromise; return result; } } defineLazyGetter(Script.prototype, "cssURLs", function() { // We can handle CSS urls (css) and CSS code (cssCode). let urls = this.css.slice(); - if (this.options.cssCode) { - urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.options.cssCode)); + if (this.matcher.cssCode) { + urls.push("data:text/css;charset=utf-8," + encodeURIComponent(this.matcher.cssCode)); } return urls; }); /** * An execution context for semi-privileged extension content scripts. *
deleted file mode 100644 --- a/toolkit/components/extensions/ExtensionManagement.jsm +++ /dev/null @@ -1,212 +0,0 @@ -/* 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"; - -this.EXPORTED_SYMBOLS = ["ExtensionManagement"]; - -const Ci = Components.interfaces; -const Cc = Components.classes; -const Cu = Components.utils; -const Cr = Components.results; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils", - "resource:///modules/E10SUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", - "resource://gre/modules/ExtensionUtils.jsm"); - -XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole()); - -XPCOMUtils.defineLazyGetter(this, "UUIDMap", () => { - let {UUIDMap} = Cu.import("resource://gre/modules/Extension.jsm", {}); - return UUIDMap; -}); - -const {appinfo} = Services; -const isParentProcess = appinfo.processType === appinfo.PROCESS_TYPE_DEFAULT; - -var ExtensionManagement; - -/* - * This file should be kept short and simple since it's loaded even - * when no extensions are running. - */ - -var APIs = { - apis: new Map(), - - register(namespace, schema, script) { - if (this.apis.has(namespace)) { - throw new Error(`API namespace already exists: ${namespace}`); - } - - this.apis.set(namespace, {schema, script}); - }, - - unregister(namespace) { - if (!this.apis.has(namespace)) { - throw new Error(`API namespace does not exist: ${namespace}`); - } - - this.apis.delete(namespace); - }, -}; - -function getURLForExtension(id, path = "") { - let uuid = UUIDMap.get(id, false); - if (!uuid) { - Cu.reportError(`Called getURLForExtension on unmapped extension ${id}`); - return null; - } - return `moz-extension://${uuid}/${path}`; -} - -// This object manages various platform-level issues related to -// moz-extension:// URIs. It lives here so that it can be used in both -// the parent and child processes. -// -// moz-extension URIs have the form moz-extension://uuid/path. Each -// extension has its own UUID, unique to the machine it's installed -// on. This is easier and more secure than using the extension ID, -// since it makes it slightly harder to fingerprint for extensions if -// each user uses different URIs for the extension. -var Service = { - initialized: false, - - // Map[uuid -> extension]. - // extension can be an Extension (parent process) or BrowserExtensionContent (child process). - uuidMap: new Map(), - - init() { - let aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService); - aps = aps.wrappedJSObject; - this.aps = aps; - aps.setExtensionURILoadCallback(this.extensionURILoadableByAnyone.bind(this)); - aps.setExtensionURIToAddonIdCallback(this.extensionURIToAddonID.bind(this)); - }, - - // Called when a new extension is loaded. - startupExtension(uuid, uri, extension) { - if (!this.initialized) { - this.initialized = true; - this.init(); - } - - // Create the moz-extension://uuid mapping. - let handler = Services.io.getProtocolHandler("moz-extension"); - handler.QueryInterface(Ci.nsISubstitutingProtocolHandler); - handler.setSubstitution(uuid, uri); - - this.uuidMap.set(uuid, extension); - this.aps.setAddonHasPermissionCallback(extension.id, extension.hasPermission.bind(extension)); - this.aps.setAddonLoadURICallback(extension.id, this.checkAddonMayLoad.bind(this, extension)); - this.aps.setAddonLocalizeCallback(extension.id, extension.localize.bind(extension)); - this.aps.setAddonCSP(extension.id, extension.manifest.content_security_policy); - this.aps.setBackgroundPageUrlCallback(uuid, this.generateBackgroundPageUrl.bind(this, extension)); - }, - - // Called when an extension is unloaded. - shutdownExtension(uuid) { - let extension = this.uuidMap.get(uuid); - if (!extension) { - return; - } - this.uuidMap.delete(uuid); - this.aps.setAddonHasPermissionCallback(extension.id, null); - this.aps.setAddonLoadURICallback(extension.id, null); - this.aps.setAddonLocalizeCallback(extension.id, null); - this.aps.setAddonCSP(extension.id, null); - this.aps.setBackgroundPageUrlCallback(uuid, null); - - let handler = Services.io.getProtocolHandler("moz-extension"); - handler.QueryInterface(Ci.nsISubstitutingProtocolHandler); - handler.setSubstitution(uuid, null); - }, - - // Return true if the given URI can be loaded from arbitrary web - // content. The manifest.json |web_accessible_resources| directive - // determines this. - extensionURILoadableByAnyone(uri) { - let uuid = uri.host; - let extension = this.uuidMap.get(uuid); - if (!extension || !extension.webAccessibleResources) { - return false; - } - - let path = uri.QueryInterface(Ci.nsIURL).filePath; - if (path.length > 0 && path[0] == "/") { - path = path.substr(1); - } - return extension.webAccessibleResources.matches(path); - }, - - // Checks whether a given extension can load this URI (typically via - // an XML HTTP request). The manifest.json |permissions| directive - // determines this. - checkAddonMayLoad(extension, uri, explicit = false) { - return extension.whiteListedHosts.matchesIgnoringPath(uri, explicit); - }, - - generateBackgroundPageUrl(extension) { - let background_scripts = (extension.manifest.background && - extension.manifest.background.scripts); - - if (!background_scripts) { - return; - } - - let html = "<!DOCTYPE html>\n<html>\n<body>\n"; - for (let script of background_scripts) { - script = script.replace(/"/g, """); - html += `<script src="${script}"></script>\n`; - } - html += "</body>\n</html>\n"; - - return "data:text/html;charset=utf-8," + encodeURIComponent(html); - }, - - // Finds the add-on ID associated with a given moz-extension:// URI. - // This is used to set the addonId on the for the nsIPrincipal - // attached to the URI. - extensionURIToAddonID(uri) { - let uuid = uri.host; - let extension = this.uuidMap.get(uuid); - return extension ? extension.id : undefined; - }, -}; - -let cacheInvalidated = 0; -function onCacheInvalidate() { - cacheInvalidated++; -} -Services.obs.addObserver(onCacheInvalidate, "startupcache-invalidate"); - -ExtensionManagement = { - get cacheInvalidated() { - return cacheInvalidated; - }, - - get isExtensionProcess() { - if (this.useRemoteWebExtensions) { - return appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE; - } - return isParentProcess; - }, - - startupExtension: Service.startupExtension.bind(Service), - shutdownExtension: Service.shutdownExtension.bind(Service), - - registerAPI: APIs.register.bind(APIs), - unregisterAPI: APIs.unregister.bind(APIs), - - getURLForExtension, - - APIs, -}; - -XPCOMUtils.defineLazyPreferenceGetter(ExtensionManagement, "useRemoteWebExtensions", - "extensions.webextensions.remote", false);
--- a/toolkit/components/extensions/ExtensionPageChild.jsm +++ b/toolkit/components/extensions/ExtensionPageChild.jsm @@ -18,18 +18,16 @@ const Cc = Components.classes; const Cu = Components.utils; const Cr = Components.results; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChildDevToolsUtils", "resource://gre/modules/ExtensionChildDevToolsUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Schemas", "resource://gre/modules/Schemas.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames", "resource://gre/modules/WebNavigationFrames.jsm"); const CATEGORY_EXTENSION_SCRIPTS_ADDON = "webextension-scripts-addon"; const CATEGORY_EXTENSION_SCRIPTS_DEVTOOLS = "webextension-scripts-devtools"; @@ -401,17 +399,17 @@ ExtensionPageChild = { return; } this.initialized = true; Services.obs.addObserver(this, "inner-window-destroyed"); // eslint-ignore-line mozilla/balanced-listeners }, init(global) { - if (!ExtensionManagement.isExtensionProcess) { + if (!WebExtensionPolicy.isExtensionProcess) { throw new Error("Cannot init extension page global in current process"); } if (!this.contentGlobals.has(global)) { this.contentGlobals.set(global, new ContentGlobal(global)); } }, @@ -435,17 +433,17 @@ ExtensionPageChild = { * * @param {BrowserExtensionContent} extension * The extension for which the context should be created. * @param {nsIDOMWindow} contentWindow The global of the page. */ initExtensionContext(extension, contentWindow) { this._init(); - if (!ExtensionManagement.isExtensionProcess) { + if (!WebExtensionPolicy.isExtensionProcess) { throw new Error("Cannot create an extension page context in current process"); } let windowId = getInnerWindowID(contentWindow); let context = this.extensionContexts.get(windowId); if (context) { if (context.extension !== extension) { throw new Error("A different extension context already exists for this frame");
--- a/toolkit/components/extensions/ExtensionParent.jsm +++ b/toolkit/components/extensions/ExtensionParent.jsm @@ -1070,16 +1070,17 @@ function extensionNameFromURI(uri) { return GlobalManager.getExtension(id).name; } const ExtensionParent = { extensionNameFromURI, GlobalManager, HiddenExtensionPage, ParentAPIManager, + WebExtensionPolicy, apiManager, get baseManifestProperties() { if (gBaseManifestProperties) { return gBaseManifestProperties; } let types = Schemas.schemaJSON.get(BASE_SCHEMA)[0].types; let manifest = types.find(type => type.id === "WebExtensionManifest");
--- a/toolkit/components/extensions/ExtensionPermissions.jsm +++ b/toolkit/components/extensions/ExtensionPermissions.jsm @@ -52,17 +52,19 @@ this.ExtensionPermissions = { let added = emptyPermissions(); for (let perm of perms.permissions) { if (!permissions.includes(perm)) { added.permissions.push(perm); permissions.push(perm); } } + for (let origin of perms.origins) { + origin = new MatchPattern(origin, {ignorePath: true}).pattern; if (!origins.includes(origin)) { added.origins.push(origin); origins.push(origin); } } if (added.permissions.length > 0 || added.origins.length > 0) { prefs.saveSoon(); @@ -84,17 +86,20 @@ this.ExtensionPermissions = { for (let perm of perms.permissions) { let i = permissions.indexOf(perm); if (i >= 0) { removed.permissions.push(perm); permissions.splice(i, 1); } } + for (let origin of perms.origins) { + origin = new MatchPattern(origin, {ignorePath: true}).pattern; + let i = origins.indexOf(origin); if (i >= 0) { removed.origins.push(origin); origins.splice(i, 1); } } if (removed.permissions.length > 0 || removed.origins.length > 0) {
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/ExtensionPolicyService.cpp @@ -0,0 +1,414 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/extensions/WebExtensionContentScript.h" +#include "mozilla/extensions/WebExtensionPolicy.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Preferences.h" +#include "mozilla/Services.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/ContentParent.h" +#include "mozIExtensionProcessScript.h" +#include "nsEscape.h" +#include "nsGkAtoms.h" +#include "nsIChannel.h" +#include "nsIContentPolicy.h" +#include "nsIDOMDocument.h" +#include "nsIDocument.h" +#include "nsILoadInfo.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsXULAppAPI.h" + +namespace mozilla { + +using namespace extensions; + +#define DEFAULT_BASE_CSP \ + "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " \ + "object-src 'self' https://* moz-extension: blob: filesystem:;" + +#define DEFAULT_DEFAULT_CSP \ + "script-src 'self'; object-src 'self';" + + +#define OBS_TOPIC_PRELOAD_SCRIPT "web-extension-preload-content-script" +#define OBS_TOPIC_LOAD_SCRIPT "web-extension-load-content-script" + + +static mozIExtensionProcessScript& +ProcessScript() +{ + static nsCOMPtr<mozIExtensionProcessScript> sProcessScript; + + if (MOZ_UNLIKELY(!sProcessScript)) { + sProcessScript = do_GetService("@mozilla.org/webextensions/extension-process-script;1"); + MOZ_RELEASE_ASSERT(sProcessScript); + ClearOnShutdown(&sProcessScript); + } + return *sProcessScript; +} + +/***************************************************************************** + * ExtensionPolicyService + *****************************************************************************/ + +/* static */ bool ExtensionPolicyService::sRemoteExtensions; + +/* static */ ExtensionPolicyService& +ExtensionPolicyService::GetSingleton() +{ + static RefPtr<ExtensionPolicyService> sExtensionPolicyService; + + if (MOZ_UNLIKELY(!sExtensionPolicyService)) { + sExtensionPolicyService = new ExtensionPolicyService(); + ClearOnShutdown(&sExtensionPolicyService); + } + return *sExtensionPolicyService.get(); +} + +ExtensionPolicyService::ExtensionPolicyService() +{ + mObs = services::GetObserverService(); + MOZ_RELEASE_ASSERT(mObs); + + Preferences::AddBoolVarCache(&sRemoteExtensions, "extensions.webextensions.remote", false); + + RegisterObservers(); +} + + +bool +ExtensionPolicyService::IsExtensionProcess() const +{ + if (sRemoteExtensions && XRE_IsContentProcess()) { + auto& remoteType = dom::ContentChild::GetSingleton()->GetRemoteType(); + return remoteType.EqualsLiteral(EXTENSION_REMOTE_TYPE); + } + return XRE_IsParentProcess(); +} + + +WebExtensionPolicy* +ExtensionPolicyService::GetByURL(const URLInfo& aURL) +{ + if (aURL.Scheme() == nsGkAtoms::moz_extension) { + return GetByHost(aURL.Host()); + } + return nullptr; +} + +void +ExtensionPolicyService::GetAll(nsTArray<RefPtr<WebExtensionPolicy>>& aResult) +{ + for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) { + aResult.AppendElement(iter.Data()); + } +} + +bool +ExtensionPolicyService::RegisterExtension(WebExtensionPolicy& aPolicy) +{ + bool ok = (!GetByID(aPolicy.Id()) && + !GetByHost(aPolicy.MozExtensionHostname())); + MOZ_ASSERT(ok); + + if (!ok) { + return false; + } + + mExtensions.Put(aPolicy.Id(), &aPolicy); + mExtensionHosts.Put(aPolicy.MozExtensionHostname(), &aPolicy); + return true; +} + +bool +ExtensionPolicyService::UnregisterExtension(WebExtensionPolicy& aPolicy) +{ + bool ok = (GetByID(aPolicy.Id()) == &aPolicy && + GetByHost(aPolicy.MozExtensionHostname()) == &aPolicy); + MOZ_ASSERT(ok); + + if (!ok) { + return false; + } + + mExtensions.Remove(aPolicy.Id()); + mExtensionHosts.Remove(aPolicy.MozExtensionHostname()); + return true; +} + + +void +ExtensionPolicyService::BaseCSP(nsAString& aBaseCSP) const +{ + nsresult rv; + + rv = Preferences::GetString("extensions.webextensions.base-content-security-policy", &aBaseCSP); + if (NS_FAILED(rv)) { + aBaseCSP.AssignLiteral(DEFAULT_BASE_CSP); + } +} + +void +ExtensionPolicyService::DefaultCSP(nsAString& aDefaultCSP) const +{ + nsresult rv; + + rv = Preferences::GetString("extensions.webextensions.default-content-security-policy", &aDefaultCSP); + if (NS_FAILED(rv)) { + aDefaultCSP.AssignLiteral(DEFAULT_DEFAULT_CSP); + } +} + + +/***************************************************************************** + * Content script management + *****************************************************************************/ + +void +ExtensionPolicyService::RegisterObservers() +{ + mObs->AddObserver(this, "content-document-global-created", false); + mObs->AddObserver(this, "document-element-inserted", false); + if (XRE_IsContentProcess()) { + mObs->AddObserver(this, "http-on-opening-request", false); + } +} + +void +ExtensionPolicyService::UnregisterObservers() +{ + mObs->RemoveObserver(this, "content-document-global-created"); + mObs->RemoveObserver(this, "document-element-inserted"); + if (XRE_IsContentProcess()) { + mObs->RemoveObserver(this, "http-on-opening-request"); + } +} + +nsresult +ExtensionPolicyService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) +{ + if (!strcmp(aTopic, "content-document-global-created")) { + nsCOMPtr<nsPIDOMWindowOuter> win = do_QueryInterface(aSubject); + if (win) { + CheckWindow(win); + } + } else if (!strcmp(aTopic, "document-element-inserted")) { + nsCOMPtr<nsIDocument> doc = do_QueryInterface(aSubject); + if (doc) { + CheckDocument(doc); + } + } else if (!strcmp(aTopic, "http-on-opening-request")) { + nsCOMPtr<nsIChannel> chan = do_QueryInterface(aSubject); + if (chan) { + CheckRequest(chan); + } + } + return NS_OK; +} + +// Checks a request for matching content scripts, and begins pre-loading them +// if necessary. +void +ExtensionPolicyService::CheckRequest(nsIChannel* aChannel) +{ + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + if (!loadInfo) { + return; + } + + auto loadType = loadInfo->GetExternalContentPolicyType(); + if (loadType != nsIContentPolicy::TYPE_DOCUMENT && + loadType != nsIContentPolicy::TYPE_SUBDOCUMENT) { + return; + } + + nsCOMPtr<nsIURI> uri; + if (NS_FAILED(aChannel->GetURI(getter_AddRefs(uri)))) { + return; + } + + CheckContentScripts({uri.get(), loadInfo}, true); +} + +// Checks a document, just after the document element has been inserted, for +// matching content scripts or extension principals, and loads them if +// necessary. +void +ExtensionPolicyService::CheckDocument(nsIDocument* aDocument) +{ + nsCOMPtr<nsPIDOMWindowOuter> win = aDocument->GetWindow(); + if (win) { + CheckContentScripts(win.get(), false); + + nsIPrincipal* principal = aDocument->NodePrincipal(); + + nsAutoString addonId; + Unused << principal->GetAddonId(addonId); + + RefPtr<WebExtensionPolicy> policy = GetByID(addonId); + if (policy) { + nsCOMPtr<nsIDOMDocument> doc = do_QueryInterface(aDocument); + ProcessScript().InitExtensionDocument(policy, doc); + } + } +} + +// Checks for loads of about:blank into new window globals, and loads any +// matching content scripts. about:blank loads do not trigger document element +// inserted events, so they're the only load type that are special cased this +// way. +void +ExtensionPolicyService::CheckWindow(nsPIDOMWindowOuter* aWindow) +{ + // We only care about non-initial document loads here. The initial + // about:blank document will usually be re-used to load another document. + nsCOMPtr<nsIDocument> doc = aWindow->GetExtantDoc(); + if (!doc || doc->IsInitialDocument()) { + return; + } + + nsCOMPtr<nsIURI> aboutBlank; + NS_ENSURE_SUCCESS_VOID(NS_NewURI(getter_AddRefs(aboutBlank), + "about:blank")); + + nsCOMPtr<nsIURI> uri = doc->GetDocumentURI(); + bool equal; + if (NS_FAILED(uri->EqualsExceptRef(aboutBlank, &equal)) || !equal) { + return; + } + + CheckContentScripts(aWindow, false); +} + +void +ExtensionPolicyService::CheckContentScripts(const DocInfo& aDocInfo, bool aIsPreload) +{ + for (auto iter = mExtensions.Iter(); !iter.Done(); iter.Next()) { + RefPtr<WebExtensionPolicy> policy = iter.Data(); + + for (auto& script : policy->ContentScripts()) { + if (script->Matches(aDocInfo)) { + if (aIsPreload) { + ProcessScript().PreloadContentScript(script); + } else { + ProcessScript().LoadContentScript(script, aDocInfo.GetWindow()); + } + } + } + } +} + + +/***************************************************************************** + * nsIAddonPolicyService + *****************************************************************************/ + +nsresult +ExtensionPolicyService::GetBaseCSP(nsAString& aBaseCSP) +{ + BaseCSP(aBaseCSP); + return NS_OK; +} + +nsresult +ExtensionPolicyService::GetDefaultCSP(nsAString& aDefaultCSP) +{ + DefaultCSP(aDefaultCSP); + return NS_OK; +} + +nsresult +ExtensionPolicyService::GetAddonCSP(const nsAString& aAddonId, + nsAString& aResult) +{ + if (WebExtensionPolicy* policy = GetByID(aAddonId)) { + policy->GetContentSecurityPolicy(aResult); + return NS_OK; + } + return NS_ERROR_INVALID_ARG; +} + +nsresult +ExtensionPolicyService::GetGeneratedBackgroundPageUrl(const nsACString& aHostname, + nsACString& aResult) +{ + if (WebExtensionPolicy* policy = GetByHost(aHostname)) { + nsAutoCString url("data:text/html,"); + + nsCString html = policy->BackgroundPageHTML(); + nsAutoCString escaped; + + url.Append(NS_EscapeURL(html, esc_Minimal, escaped)); + + aResult = url; + return NS_OK; + } + return NS_ERROR_INVALID_ARG; +} + +nsresult +ExtensionPolicyService::AddonHasPermission(const nsAString& aAddonId, + const nsAString& aPerm, + bool* aResult) +{ + if (WebExtensionPolicy* policy = GetByID(aAddonId)) { + *aResult = policy->HasPermission(aPerm); + return NS_OK; + } + return NS_ERROR_INVALID_ARG; +} + +nsresult +ExtensionPolicyService::AddonMayLoadURI(const nsAString& aAddonId, + nsIURI* aURI, + bool aExplicit, + bool* aResult) +{ + if (WebExtensionPolicy* policy = GetByID(aAddonId)) { + *aResult = policy->CanAccessURI(aURI, aExplicit); + return NS_OK; + } + return NS_ERROR_INVALID_ARG; +} + +nsresult +ExtensionPolicyService::ExtensionURILoadableByAnyone(nsIURI* aURI, bool* aResult) +{ + URLInfo url(aURI); + if (WebExtensionPolicy* policy = GetByURL(url)) { + *aResult = policy->IsPathWebAccessible(url.FilePath()); + return NS_OK; + } + return NS_ERROR_INVALID_ARG; +} + +nsresult +ExtensionPolicyService::ExtensionURIToAddonId(nsIURI* aURI, nsAString& aResult) +{ + if (WebExtensionPolicy* policy = GetByURL(aURI)) { + policy->GetId(aResult); + } else { + aResult.SetIsVoid(true); + } + return NS_OK; +} + + +NS_IMPL_CYCLE_COLLECTION(ExtensionPolicyService, mExtensions, mExtensionHosts) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ExtensionPolicyService) + NS_INTERFACE_MAP_ENTRY(nsIAddonPolicyService) + NS_INTERFACE_MAP_ENTRY(nsIObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIAddonPolicyService) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ExtensionPolicyService) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ExtensionPolicyService) + +} // namespace mozilla
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/ExtensionPolicyService.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef mozilla_ExtensionPolicyService_h +#define mozilla_ExtensionPolicyService_h + +#include "mozilla/extensions/WebExtensionPolicy.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsHashKeys.h" +#include "nsIAddonPolicyService.h" +#include "nsIAtom.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsISupports.h" +#include "nsPointerHashKeys.h" +#include "nsRefPtrHashtable.h" + +class nsIChannel; +class nsIObserverService; +class nsIDocument; +class nsIPIDOMWindowOuter; + +namespace mozilla { +namespace extensions { + class DocInfo; +} + +using extensions::DocInfo; +using extensions::WebExtensionPolicy; + +class ExtensionPolicyService final : public nsIAddonPolicyService + , public nsIObserver +{ +public: + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(ExtensionPolicyService, + nsIAddonPolicyService) + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_NSIADDONPOLICYSERVICE + NS_DECL_NSIOBSERVER + + static ExtensionPolicyService& GetSingleton(); + + static already_AddRefed<ExtensionPolicyService> GetInstance() + { + return do_AddRef(&GetSingleton()); + } + + WebExtensionPolicy* + GetByID(const nsIAtom* aAddonId) + { + return mExtensions.GetWeak(aAddonId); + } + + WebExtensionPolicy* GetByID(const nsAString& aAddonId) + { + nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(aAddonId); + return GetByID(atom); + } + + WebExtensionPolicy* GetByURL(const extensions::URLInfo& aURL); + + WebExtensionPolicy* GetByHost(const nsACString& aHost) const + { + return mExtensionHosts.GetWeak(aHost); + } + + void GetAll(nsTArray<RefPtr<WebExtensionPolicy>>& aResult); + + bool RegisterExtension(WebExtensionPolicy& aPolicy); + bool UnregisterExtension(WebExtensionPolicy& aPolicy); + + void BaseCSP(nsAString& aDefaultCSP) const; + void DefaultCSP(nsAString& aDefaultCSP) const; + + bool IsExtensionProcess() const; + +protected: + virtual ~ExtensionPolicyService() = default; + +private: + ExtensionPolicyService(); + + void RegisterObservers(); + void UnregisterObservers(); + + void CheckRequest(nsIChannel* aChannel); + void CheckDocument(nsIDocument* aDocument); + void CheckWindow(nsPIDOMWindowOuter* aWindow); + + void CheckContentScripts(const DocInfo& aDocInfo, bool aIsPreload); + + nsRefPtrHashtable<nsPtrHashKey<const nsIAtom>, WebExtensionPolicy> mExtensions; + nsRefPtrHashtable<nsCStringHashKey, WebExtensionPolicy> mExtensionHosts; + + nsCOMPtr<nsIObserverService> mObs; + + static bool sRemoteExtensions; +}; + +} // namespace mozilla + +#endif // mozilla_ExtensionPolicyService_h
--- a/toolkit/components/extensions/ExtensionTabs.jsm +++ b/toolkit/components/extensions/ExtensionTabs.jsm @@ -543,19 +543,19 @@ class TabBase { if (details.frameId !== null && details.allFrames) { return Promise.reject({message: `'frameId' and 'allFrames' are mutually exclusive`}); } if (this.hasActiveTabPermission) { // If we have the "activeTab" permission for this tab, ignore // the host whitelist. - options.matchesHost = ["<all_urls>"]; + options.matches = ["<all_urls>"]; } else { - options.matchesHost = this.extension.whiteListedHosts.serialize(); + options.matches = this.extension.whiteListedHosts.patterns.map(host => host.pattern); } if (details.code !== null) { options[`${kind}Code`] = details.code; } if (details.file !== null) { let url = context.uri.resolve(details.file); if (!this.extension.isExtensionURL(url)) {
--- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -17,18 +17,16 @@ Cu.import("resource://gre/modules/Servic Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ConsoleAPI", "resource://gre/modules/Console.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "IndexedDB", "resource://gre/modules/IndexedDB.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", "resource://gre/modules/MessageChannel.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Preferences", "resource://gre/modules/Preferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Schemas", "resource://gre/modules/Schemas.jsm"); @@ -57,18 +55,16 @@ let StartupCache = { DB_NAME: "ExtensionStartupCache", SCHEMA_VERSION: 2, STORE_NAMES: Object.freeze(["locales", "manifests", "schemas"]), dbPromise: null, - cacheInvalidated: 0, - initDB(db) { for (let name of StartupCache.STORE_NAMES) { try { db.deleteObjectStore(name); } catch (e) { // Don't worry if the store doesn't already exist. } db.createObjectStore(name, {keyPath: "key"}); @@ -79,42 +75,39 @@ let StartupCache = { let range = IDBKeyRange.bound([id], [id, "\uFFFF"]); return Promise.all([ this.locales.delete(range), this.manifests.delete(range), ]).catch(e => { // Ignore the error. It happens when we try to flush the add-on // data after the AddonManager has flushed the entire startup cache. + this.dbPromise = this.reallyOpen(true).catch(e => {}); }); }, async reallyOpen(invalidate = false) { if (this.dbPromise) { let db = await this.dbPromise; db.close(); } if (invalidate) { - this.cacheInvalidated = ExtensionManagement.cacheInvalidated; - if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT) { IndexedDB.deleteDatabase(this.DB_NAME, {storage: "persistent"}); } } return IndexedDB.open(this.DB_NAME, {storage: "persistent", version: this.SCHEMA_VERSION}, db => this.initDB(db)); }, async open() { - if (ExtensionManagement.cacheInvalidated > this.cacheInvalidated) { - this.dbPromise = this.reallyOpen(true); - } else if (!this.dbPromise) { + if (!this.dbPromise) { this.dbPromise = this.reallyOpen(); } return this.dbPromise; }, observe(subject, topic, data) { if (topic === "startupcache-invalidate") { @@ -143,18 +136,22 @@ class CacheStore { return createFunc(key); } if (result === undefined) { let value = await createFunc(key); result = {key, value}; - db.objectStore(this.storeName, "readwrite") - .put(result); + try { + db.objectStore(this.storeName, "readwrite") + .put(result); + } catch (e) { + Cu.reportError(e); + } } return result && result.value; } async getAll() { let result = new Map(); try {
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/MatchGlob.h @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef mozilla_extensions_MatchGlob_h +#define mozilla_extensions_MatchGlob_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/MatchGlobBinding.h" + +#include "jspubtd.h" +#include "js/RootingAPI.h" + +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace extensions { + +class MatchPattern; + +class MatchGlob final : public nsISupports + , public nsWrapperCache +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MatchGlob) + + static already_AddRefed<MatchGlob> + Constructor(dom::GlobalObject& aGlobal, const nsAString& aGlob, bool aAllowQuestion, + ErrorResult& aRv); + + bool Matches(const nsAString& aString) const; + + bool IsWildcard() const + { + return mIsPrefix && mPathLiteral.IsEmpty(); + } + + void GetGlob(nsAString& aGlob) const + { + aGlob = mGlob; + } + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override; + +protected: + virtual ~MatchGlob(); + +private: + friend class MatchPattern; + + explicit MatchGlob(nsISupports* aParent) : mParent(aParent) {} + + void Init(JSContext* aCx, const nsAString& aGlob, bool aAllowQuestion, ErrorResult& aRv); + + nsCOMPtr<nsISupports> mParent; + + // The original glob string that this glob object represents. + nsString mGlob; + + // The literal path string to match against. If this contains a non-void + // value, the glob matches against this exact literal string, rather than + // performng a pattern match. If mIsPrefix is true, the literal must appear + // at the start of the matched string. If it is false, the the literal must + // be exactly equal to the matched string. + nsString mPathLiteral; + bool mIsPrefix = false; + + // The regular expression object which is equivalent to this glob pattern. + // Used for matching if, and only if, mPathLiteral is non-void. + JS::Heap<JSObject*> mRegExp; +}; + +class MatchGlobSet final : public nsTArray<RefPtr<MatchGlob>> +{ +public: + // Note: We can't use the nsTArray constructors directly, since the static + // analyzer doesn't handle their MOZ_IMPLICIT annotations correctly. + MatchGlobSet() {} + explicit MatchGlobSet(size_type aCapacity) : nsTArray(aCapacity) {} + explicit MatchGlobSet(const nsTArray& aOther) : nsTArray(aOther) {} + MOZ_IMPLICIT MatchGlobSet(nsTArray&& aOther) : nsTArray(mozilla::Move(aOther)) {} + MOZ_IMPLICIT MatchGlobSet(std::initializer_list<RefPtr<MatchGlob>> aIL) : nsTArray(aIL) {} + + bool Matches(const nsAString& aValue) const; +}; + +} // namespace extensions +} // namespace mozilla + +#endif // mozilla_extensions_MatchGlob_h +
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/MatchPattern.cpp @@ -0,0 +1,758 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#include "mozilla/extensions/MatchPattern.h" +#include "mozilla/extensions/MatchGlob.h" + +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/HoldDropJSObjects.h" +#include "mozilla/Unused.h" + +#include "nsGkAtoms.h" +#include "nsIProtocolHandler.h" +#include "nsIURL.h" +#include "nsNetUtil.h" + +namespace mozilla { +namespace extensions { + +using namespace mozilla::dom; + + +/***************************************************************************** + * AtomSet + *****************************************************************************/ + +AtomSet::AtomSet(const nsTArray<nsString>& aElems) +{ + mElems.SetCapacity(aElems.Length()); + + for (const auto& elem : aElems) { + mElems.AppendElement(NS_AtomizeMainThread(elem)); + } + + SortAndUniquify(); +} + +AtomSet::AtomSet(const char** aElems) +{ + for (const char** elemp = aElems; *elemp; elemp++) { + mElems.AppendElement(NS_Atomize(*elemp)); + } + + SortAndUniquify(); +} + +AtomSet::AtomSet(std::initializer_list<nsIAtom*> aIL) +{ + mElems.SetCapacity(aIL.size()); + + for (const auto& elem : aIL) { + mElems.AppendElement(elem); + } + + SortAndUniquify(); +} + +void +AtomSet::SortAndUniquify() +{ + mElems.Sort(); + + nsIAtom* prev = nullptr; + mElems.RemoveElementsBy([&prev] (const RefPtr<nsIAtom>& aAtom) { + bool remove = aAtom == prev; + prev = aAtom; + return remove; + }); + + mElems.Compact(); +} + +bool +AtomSet::Intersects(const AtomSet& aOther) const +{ + for (const auto& atom : *this) { + if (aOther.Contains(atom)) { + return true; + } + } + for (const auto& atom : aOther) { + if (Contains(atom)) { + return true; + } + } + return false; +} + +void +AtomSet::Add(nsIAtom* aAtom) +{ + auto index = mElems.IndexOfFirstElementGt(aAtom); + if (index == 0 || mElems[index - 1] != aAtom) { + mElems.InsertElementAt(index, aAtom); + } +} + +void +AtomSet::Remove(nsIAtom* aAtom) +{ + auto index = mElems.BinaryIndexOf(aAtom); + if (index != mElems.NoIndex) { + mElems.RemoveElementAt(index); + } +} + + +/***************************************************************************** + * URLInfo + *****************************************************************************/ + +nsIAtom* +URLInfo::Scheme() const +{ + if (!mScheme) { + nsCString scheme; + if (NS_SUCCEEDED(mURI->GetScheme(scheme))) { + mScheme = NS_AtomizeMainThread(NS_ConvertASCIItoUTF16(scheme)); + } + } + return mScheme; +} + +const nsCString& +URLInfo::Host() const +{ + if (mHost.IsVoid()) { + Unused << mURI->GetHost(mHost); + } + return mHost; +} + +const nsString& +URLInfo::FilePath() const +{ + if (mFilePath.IsEmpty()) { + nsCString path; + nsCOMPtr<nsIURL> url = do_QueryInterface(mURI); + if (url && NS_SUCCEEDED(url->GetFilePath(path))) { + AppendUTF8toUTF16(path, mFilePath); + } else { + mFilePath = Path(); + } + } + return mFilePath; +} + +const nsString& +URLInfo::Path() const +{ + if (mPath.IsEmpty()) { + nsCString path; + if (NS_SUCCEEDED(URINoRef()->GetPath(path))) { + AppendUTF8toUTF16(path, mPath); + } + } + return mPath; +} + +const nsString& +URLInfo::Spec() const +{ + if (mSpec.IsEmpty()) { + nsCString spec; + if (NS_SUCCEEDED(URINoRef()->GetSpec(spec))) { + AppendUTF8toUTF16(spec, mSpec); + } + } + return mSpec; +} + +nsIURI* +URLInfo::URINoRef() const +{ + if (!mURINoRef) { + if (NS_FAILED(mURI->CloneIgnoringRef(getter_AddRefs(mURINoRef)))) { + mURINoRef = mURI; + } + } + return mURINoRef; +} + +bool +URLInfo::InheritsPrincipal() const +{ + if (!mInheritsPrincipal.isSome()) { + // For our purposes, about:blank and about:srcdoc are treated as URIs that + // inherit principals. + bool inherits = Spec().EqualsLiteral("about:blank") || Spec().EqualsLiteral("about:srcdoc"); + + if (!inherits) { + nsresult rv = NS_URIChainHasFlags(mURI, nsIProtocolHandler::URI_INHERITS_SECURITY_CONTEXT, + &inherits); + Unused << NS_WARN_IF(NS_FAILED(rv)); + } + + mInheritsPrincipal.emplace(inherits); + } + return mInheritsPrincipal.ref(); +} + + +/***************************************************************************** + * CookieInfo + *****************************************************************************/ + +bool +CookieInfo::IsDomain() const +{ + if (mIsDomain.isNothing()) { + mIsDomain.emplace(false); + MOZ_ALWAYS_SUCCEEDS(mCookie->GetIsDomain(mIsDomain.ptr())); + } + return mIsDomain.ref(); +} + +bool +CookieInfo::IsSecure() const +{ + if (mIsSecure.isNothing()) { + mIsSecure.emplace(false); + MOZ_ALWAYS_SUCCEEDS(mCookie->GetIsSecure(mIsSecure.ptr())); + } + return mIsSecure.ref(); +} + +const nsCString& +CookieInfo::Host() const +{ + if (mHost.IsEmpty()) { + MOZ_ALWAYS_SUCCEEDS(mCookie->GetHost(mHost)); + } + return mHost; +} + +const nsCString& +CookieInfo::RawHost() const +{ + if (mRawHost.IsEmpty()) { + MOZ_ALWAYS_SUCCEEDS(mCookie->GetRawHost(mRawHost)); + } + return mRawHost; +} + + +/***************************************************************************** + * MatchPattern + *****************************************************************************/ + +const char* PERMITTED_SCHEMES[] = {"http", "https", "file", "ftp", "data", nullptr}; + +const char* WILDCARD_SCHEMES[] = {"http", "https", nullptr}; + +/* static */ already_AddRefed<MatchPattern> +MatchPattern::Constructor(dom::GlobalObject& aGlobal, + const nsAString& aPattern, + const MatchPatternOptions& aOptions, + ErrorResult& aRv) +{ + RefPtr<MatchPattern> pattern = new MatchPattern(aGlobal.GetAsSupports()); + pattern->Init(aGlobal.Context(), aPattern, aOptions.mIgnorePath, aRv); + if (aRv.Failed()) { + return nullptr; + } + return pattern.forget(); +} + +void +MatchPattern::Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath, ErrorResult& aRv) +{ + RefPtr<AtomSet> permittedSchemes = AtomSet::Get<PERMITTED_SCHEMES>(); + + mPattern = aPattern; + + if (aPattern.EqualsLiteral("<all_urls>")) { + mSchemes = permittedSchemes; + mMatchSubdomain = true; + return; + } + + // The portion of the URL we're currently examining. + uint32_t offset = 0; + auto tail = Substring(aPattern, offset); + + /*************************************************************************** + * Scheme + ***************************************************************************/ + int32_t index = aPattern.FindChar(':'); + if (index <= 0) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + nsCOMPtr<nsIAtom> scheme = NS_AtomizeMainThread(StringHead(aPattern, index)); + if (scheme == nsGkAtoms::_asterisk) { + mSchemes = AtomSet::Get<WILDCARD_SCHEMES>(); + } else if (permittedSchemes->Contains(scheme)) { + mSchemes = new AtomSet({scheme}); + } else { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + /*************************************************************************** + * Host + ***************************************************************************/ + offset = index + 1; + tail.Rebind(aPattern, offset); + + if (!StringHead(tail, 2).EqualsLiteral("//")) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + offset += 2; + tail.Rebind(aPattern, offset); + index = tail.FindChar('/'); + if (index < 0) { + index = tail.Length(); + } + + auto host = StringHead(tail, index); + if (host.IsEmpty() && scheme != nsGkAtoms::file) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + offset += index; + tail.Rebind(aPattern, offset); + + if (host.EqualsLiteral("*")) { + mMatchSubdomain = true; + } else if (StringHead(host, 2).EqualsLiteral("*.")) { + mDomain = NS_ConvertUTF16toUTF8(Substring(host, 2)); + mMatchSubdomain = true; + } else { + mDomain = NS_ConvertUTF16toUTF8(host); + } + + /*************************************************************************** + * Path + ***************************************************************************/ + if (aIgnorePath) { + mPattern.Truncate(offset); + mPattern.AppendLiteral("/*"); + return; + } + + auto path = tail; + if (path.IsEmpty()) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + mPath = new MatchGlob(this); + mPath->Init(aCx, path, false, aRv); +} + + +bool +MatchPattern::MatchesDomain(const nsACString& aDomain) const +{ + if (DomainIsWildcard() || mDomain == aDomain) { + return true; + } + + if (mMatchSubdomain) { + int64_t offset = (int64_t)aDomain.Length() - mDomain.Length(); + if (offset > 0 && aDomain[offset - 1] == '.' && + Substring(aDomain, offset) == mDomain) { + return true; + } + } + + return false; +} + +bool +MatchPattern::Matches(const URLInfo& aURL, bool aExplicit) const +{ + if (aExplicit && mMatchSubdomain) { + return false; + } + + if (!mSchemes->Contains(aURL.Scheme())) { + return false; + } + + if (!DomainIsWildcard() && !MatchesDomain(aURL.Host())) { + return false; + } + + if (mPath && !mPath->IsWildcard() && !mPath->Matches(aURL.Path())) { + return false; + } + + return true; +} + +bool +MatchPattern::MatchesCookie(const CookieInfo& aCookie) const +{ + if (!mSchemes->Contains(nsGkAtoms::https) && + (aCookie.IsSecure() || !mSchemes->Contains(nsGkAtoms::http))) { + return false; + } + + if (MatchesDomain(aCookie.RawHost())) { + return true; + } + + if (!aCookie.IsDomain()) { + return false; + } + + // Things get tricker for domain cookies. The extension needs to be able + // to read any cookies that could be read by any host it has permissions + // for. This means that our normal host matching checks won't work, + // since the pattern "*://*.foo.example.com/" doesn't match ".example.com", + // but it does match "bar.foo.example.com", which can read cookies + // with the domain ".example.com". + // + // So, instead, we need to manually check our filters, and accept any + // with hosts that end with our cookie's host. + + auto& host = aCookie.Host(); + return StringTail(mDomain, host.Length()) == host; +} + +bool +MatchPattern::SubsumesDomain(const MatchPattern& aPattern) const +{ + if (!mMatchSubdomain && aPattern.mMatchSubdomain && aPattern.mDomain == mDomain) { + return false; + } + + return MatchesDomain(aPattern.mDomain); +} + +bool +MatchPattern::Subsumes(const MatchPattern& aPattern) const +{ + for (auto& scheme : *aPattern.mSchemes) { + if (!mSchemes->Contains(scheme)) { + return false; + } + } + + return SubsumesDomain(aPattern); +} + +bool +MatchPattern::Overlaps(const MatchPattern& aPattern) const +{ + if (!mSchemes->Intersects(*aPattern.mSchemes)) { + return false; + } + + return SubsumesDomain(aPattern) || aPattern.SubsumesDomain(*this); +} + + +JSObject* +MatchPattern::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) +{ + return MatchPatternBinding::Wrap(aCx, this, aGivenProto); +} + + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MatchPattern, mPath, mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchPattern) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchPattern) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchPattern) + + +/***************************************************************************** + * MatchPatternSet + *****************************************************************************/ + +/* static */ already_AddRefed<MatchPatternSet> +MatchPatternSet::Constructor(dom::GlobalObject& aGlobal, + const nsTArray<dom::OwningStringOrMatchPattern>& aPatterns, + const MatchPatternOptions& aOptions, + ErrorResult& aRv) +{ + ArrayType patterns; + + for (auto& elem : aPatterns) { + if (elem.IsMatchPattern()) { + patterns.AppendElement(elem.GetAsMatchPattern()); + } else { + RefPtr<MatchPattern> pattern = MatchPattern::Constructor( + aGlobal, elem.GetAsString(), aOptions, aRv); + + if (!pattern) { + return nullptr; + } + patterns.AppendElement(Move(pattern)); + } + } + + RefPtr<MatchPatternSet> patternSet = new MatchPatternSet(aGlobal.GetAsSupports(), + Move(patterns)); + return patternSet.forget(); +} + + +bool +MatchPatternSet::Matches(const URLInfo& aURL, bool aExplicit) const +{ + for (const auto& pattern : mPatterns) { + if (pattern->Matches(aURL, aExplicit)) { + return true; + } + } + return false; +} + +bool +MatchPatternSet::MatchesCookie(const CookieInfo& aCookie) const +{ + for (const auto& pattern : mPatterns) { + if (pattern->MatchesCookie(aCookie)) { + return true; + } + } + return false; +} + +bool +MatchPatternSet::Subsumes(const MatchPattern& aPattern) const +{ + for (const auto& pattern : mPatterns) { + if (pattern->Subsumes(aPattern)) { + return true; + } + } + return false; +} + +bool +MatchPatternSet::Overlaps(const MatchPatternSet& aPatternSet) const +{ + for (const auto& pattern : aPatternSet.mPatterns) { + if (Overlaps(*pattern)) { + return true; + } + } + return false; +} + +bool +MatchPatternSet::Overlaps(const MatchPattern& aPattern) const +{ + for (const auto& pattern : mPatterns) { + if (pattern->Overlaps(aPattern)) { + return true; + } + } + return false; +} + + +bool +MatchPatternSet::OverlapsAll(const MatchPatternSet& aPatternSet) const +{ + for (const auto& pattern : aPatternSet.mPatterns) { + if (!Overlaps(*pattern)) { + return false; + } + } + return aPatternSet.mPatterns.Length() > 0; +} + + +JSObject* +MatchPatternSet::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) +{ + return MatchPatternSetBinding::Wrap(aCx, this, aGivenProto); +} + + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MatchPatternSet, mPatterns, mParent) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchPatternSet) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchPatternSet) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchPatternSet) + + +/***************************************************************************** + * MatchGlob + *****************************************************************************/ + +MatchGlob::~MatchGlob() +{ + mozilla::DropJSObjects(this); +} + +/* static */ already_AddRefed<MatchGlob> +MatchGlob::Constructor(dom::GlobalObject& aGlobal, + const nsAString& aGlob, + bool aAllowQuestion, + ErrorResult& aRv) +{ + RefPtr<MatchGlob> glob = new MatchGlob(aGlobal.GetAsSupports()); + glob->Init(aGlobal.Context(), aGlob, aAllowQuestion, aRv); + if (aRv.Failed()) { + return nullptr; + } + return glob.forget(); +} + +void +MatchGlob::Init(JSContext* aCx, const nsAString& aGlob, bool aAllowQuestion, ErrorResult& aRv) +{ + mGlob = aGlob; + + // Check for a literal match with no glob metacharacters. + auto index = mGlob.FindCharInSet(aAllowQuestion ? "*?" : "*"); + if (index < 0) { + mPathLiteral = mGlob; + return; + } + + // Check for a prefix match, where the only glob metacharacter is a "*" + // at the end of the string. + if (index == (int32_t)mGlob.Length() - 1 && mGlob[index] == '*') { + mPathLiteral = StringHead(mGlob, index); + mIsPrefix = true; + return; + } + + // Fall back to the regexp slow path. + NS_NAMED_LITERAL_CSTRING(metaChars, ".+*?^${}()|[]\\"); + + nsAutoString escaped; + escaped.Append('^'); + + for (uint32_t i = 0; i < mGlob.Length(); i++) { + auto c = mGlob[i]; + if (c == '*') { + escaped.AppendLiteral(".*"); + } else if (c == '?' && aAllowQuestion) { + escaped.Append('.'); + } else { + if (metaChars.Contains(c)) { + escaped.Append('\\'); + } + escaped.Append(c); + } + } + + escaped.Append('$'); + + // TODO: Switch to the Rust regexp crate, when Rust integration is easier. + // It uses a much more efficient, linear time matching algorithm, and + // doesn't require special casing for the literal and prefix cases. + mRegExp = JS_NewUCRegExpObject(aCx, escaped.get(), escaped.Length(), 0); + if (mRegExp) { + mozilla::HoldJSObjects(this); + } else { + aRv.NoteJSContextException(aCx); + } +} + +bool +MatchGlob::Matches(const nsAString& aString) const +{ + if (mRegExp) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + + JSAutoCompartment ac(cx, mRegExp); + + JS::RootedObject regexp(cx, mRegExp); + JS::RootedValue result(cx); + + nsString input(aString); + + size_t index = 0; + if (!JS_ExecuteRegExpNoStatics(cx, regexp, input.BeginWriting(), aString.Length(), + &index, true, &result)) { + return false; + } + + return result.isBoolean() && result.toBoolean(); + } + + if (mIsPrefix) { + return mPathLiteral == StringHead(aString, mPathLiteral.Length()); + } + + return mPathLiteral == aString; +} + + +JSObject* +MatchGlob::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) +{ + return MatchGlobBinding::Wrap(aCx, this, aGivenProto); +} + + +NS_IMPL_CYCLE_COLLECTION_CLASS(MatchGlob) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MatchGlob) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) + tmp->mRegExp = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MatchGlob) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(MatchGlob) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER + NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mRegExp) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MatchGlob) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(MatchGlob) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MatchGlob) + + +/***************************************************************************** + * MatchGlobSet + *****************************************************************************/ + +bool +MatchGlobSet::Matches(const nsAString& aValue) const +{ + for (auto& glob : *this) { + if (glob->Matches(aValue)) { + return true; + } + } + return false; +} + +} // namespace extensions +} // namespace mozilla +
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/MatchPattern.h @@ -0,0 +1,326 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef mozilla_extensions_MatchPattern_h +#define mozilla_extensions_MatchPattern_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/MatchPatternBinding.h" +#include "mozilla/extensions/MatchGlob.h" + +#include "jspubtd.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" +#include "mozilla/RefCounted.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsTArray.h" +#include "nsIAtom.h" +#include "nsICookie2.h" +#include "nsISupports.h" +#include "nsIURI.h" +#include "nsWrapperCache.h" + + +namespace mozilla { +namespace extensions { + +using dom::MatchPatternOptions; + + +// A sorted, binary-search-backed set of atoms, optimized for frequent lookups +// and infrequent updates. +class AtomSet final : public RefCounted<AtomSet> +{ + using ArrayType = AutoTArray<RefPtr<nsIAtom>, 1>; + +public: + MOZ_DECLARE_REFCOUNTED_TYPENAME(AtomSet) + + explicit AtomSet(const nsTArray<nsString>& aElems); + + explicit AtomSet(const char** aElems); + + MOZ_IMPLICIT AtomSet(std::initializer_list<nsIAtom*> aIL); + + bool Contains(const nsAString& elem) const + { + nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(elem); + return Contains(atom); + } + + bool Contains(const nsACString& aElem) const + { + nsCOMPtr<nsIAtom> atom = NS_Atomize(aElem); + return Contains(atom); + } + + bool Contains(const nsIAtom* aAtom) const + { + return mElems.BinaryIndexOf(aAtom) != mElems.NoIndex; + } + + bool Intersects(const AtomSet& aOther) const; + + + void Add(nsIAtom* aElem); + void Remove(nsIAtom* aElem); + + void Add(const nsAString& aElem) + { + nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(aElem); + return Add(atom); + } + + void Remove(const nsAString& aElem) + { + nsCOMPtr<nsIAtom> atom = NS_AtomizeMainThread(aElem); + return Remove(atom); + } + + // Returns a cached, statically-allocated matcher for the given set of + // literal strings. + template <const char** schemes> + static already_AddRefed<AtomSet> + Get() + { + static RefPtr<AtomSet> sMatcher; + + if (MOZ_UNLIKELY(!sMatcher)) { + sMatcher = new AtomSet(schemes); + ClearOnShutdown(&sMatcher); + } + + return do_AddRef(sMatcher); + } + + void + Get(nsTArray<nsString>& aResult) const + { + aResult.SetCapacity(mElems.Length()); + + for (const auto& atom : mElems) { + aResult.AppendElement(nsDependentAtomString(atom)); + } + } + + auto begin() const + -> decltype(DeclVal<const ArrayType>().begin()) + { + return mElems.begin(); + } + + auto end() const + -> decltype(DeclVal<const ArrayType>().end()) + { + return mElems.end(); + } + +private: + ArrayType mElems; + + void SortAndUniquify(); +}; + + +// A helper class to lazily retrieve, transcode, and atomize certain URI +// properties the first time they're used, and cache the results, so that they +// can be used across multiple match operations. +class MOZ_STACK_CLASS URLInfo final +{ +public: + MOZ_IMPLICIT URLInfo(nsIURI* aURI) + : mURI(aURI) + { + mHost.SetIsVoid(true); + } + + URLInfo(const URLInfo& aOther) + : URLInfo(aOther.mURI.get()) + {} + + nsIURI* URI() const { return mURI; } + + nsIAtom* Scheme() const; + const nsCString& Host() const; + const nsString& Path() const; + const nsString& FilePath() const; + const nsString& Spec() const; + + bool InheritsPrincipal() const; + +private: + nsIURI* URINoRef() const; + + nsCOMPtr<nsIURI> mURI; + mutable nsCOMPtr<nsIURI> mURINoRef; + + mutable nsCOMPtr<nsIAtom> mScheme; + mutable nsCString mHost; + + mutable nsAutoString mPath; + mutable nsAutoString mFilePath; + mutable nsAutoString mSpec; + + mutable Maybe<bool> mInheritsPrincipal; +}; + + +// Similar to URLInfo, but for cookies. +class MOZ_STACK_CLASS CookieInfo final +{ +public: + MOZ_IMPLICIT CookieInfo(nsICookie2* aCookie) + : mCookie(aCookie) + {} + + bool IsSecure() const; + bool IsDomain() const; + + const nsCString& Host() const; + const nsCString& RawHost() const; + +private: + nsCOMPtr<nsICookie2> mCookie; + + mutable Maybe<bool> mIsSecure; + mutable Maybe<bool> mIsDomain; + + mutable nsCString mHost; + mutable nsCString mRawHost; +}; + + +class MatchPattern final : public nsISupports + , public nsWrapperCache +{ + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MatchPattern) + + static already_AddRefed<MatchPattern> + Constructor(dom::GlobalObject& aGlobal, + const nsAString& aPattern, + const MatchPatternOptions& aOptions, + ErrorResult& aRv); + + bool Matches(const URLInfo& aURL, bool aExplicit = false) const; + + bool MatchesCookie(const CookieInfo& aCookie) const; + + bool MatchesDomain(const nsACString& aDomain) const; + + bool Subsumes(const MatchPattern& aPattern) const; + + bool Overlaps(const MatchPattern& aPattern) const; + + bool DomainIsWildcard() const + { + return mMatchSubdomain && mDomain.IsEmpty(); + } + + void GetPattern(nsAString& aPattern) const + { + aPattern = mPattern; + } + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override; + +protected: + virtual ~MatchPattern() = default; + +private: + explicit MatchPattern(nsISupports* aParent) : mParent(aParent) {} + + void Init(JSContext* aCx, const nsAString& aPattern, bool aIgnorePath, ErrorResult& aRv); + + bool SubsumesDomain(const MatchPattern& aPattern) const; + + + nsCOMPtr<nsISupports> mParent; + + // The normalized match pattern string that this object represents. + nsString mPattern; + + // The set of atomized URI schemes that this pattern matches. + RefPtr<AtomSet> mSchemes; + + // The domain that this matcher matches. If mMatchSubdomain is false, only + // matches the exact domain. If it's true, matches the domain or any + // subdomain. + // + // For instance, "*.foo.com" gives mDomain = "foo.com" and mMatchSubdomain = true, + // and matches "foo.com" or "bar.foo.com" but not "barfoo.com". + // + // While "foo.com" gives mDomain = "foo.com" and mMatchSubdomain = false, + // and matches "foo.com" but not "bar.foo.com". + nsCString mDomain; + bool mMatchSubdomain = false; + + // The glob against which the URL path must match. If null, the path is + // ignored entirely. If non-null, the path must match this glob. + RefPtr<MatchGlob> mPath; +}; + + +class MatchPatternSet final : public nsISupports + , public nsWrapperCache +{ + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(MatchPatternSet) + + using ArrayType = nsTArray<RefPtr<MatchPattern>>; + + + static already_AddRefed<MatchPatternSet> + Constructor(dom::GlobalObject& aGlobal, + const nsTArray<dom::OwningStringOrMatchPattern>& aPatterns, + const MatchPatternOptions& aOptions, + ErrorResult& aRv); + + + bool Matches(const URLInfo& aURL, bool aExplicit = false) const; + + bool MatchesCookie(const CookieInfo& aCookie) const; + + bool Subsumes(const MatchPattern& aPattern) const; + + bool Overlaps(const MatchPattern& aPattern) const; + + bool Overlaps(const MatchPatternSet& aPatternSet) const; + + bool OverlapsAll(const MatchPatternSet& aPatternSet) const; + + void GetPatterns(ArrayType& aPatterns) + { + aPatterns.AppendElements(mPatterns); + } + + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override; + +protected: + virtual ~MatchPatternSet() = default; + +private: + explicit MatchPatternSet(nsISupports* aParent, ArrayType&& aPatterns) + : mParent(aParent) + , mPatterns(Forward<ArrayType>(aPatterns)) + {} + + nsCOMPtr<nsISupports> mParent; + + ArrayType mPatterns; +}; + +} // namespace extensions +} // namespace mozilla + +#endif // mozilla_extensions_MatchPattern_h
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/WebExtensionContentScript.h @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef mozilla_extensions_WebExtensionContentScript_h +#define mozilla_extensions_WebExtensionContentScript_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/WebExtensionContentScriptBinding.h" + +#include "jspubtd.h" + +#include "mozilla/Maybe.h" +#include "mozilla/Variant.h" +#include "mozilla/extensions/MatchGlob.h" +#include "mozilla/extensions/MatchPattern.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +class nsILoadInfo; +class nsPIDOMWindowOuter; + +namespace mozilla { +namespace extensions { + +using dom::Nullable; +using ContentScriptInit = dom::WebExtensionContentScriptInit; + +class WebExtensionPolicy; + +class MOZ_STACK_CLASS DocInfo final +{ +public: + DocInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo); + + MOZ_IMPLICIT DocInfo(nsPIDOMWindowOuter* aWindow); + + const URLInfo& URL() const { return mURL; } + + nsIPrincipal* Principal() const; + + const URLInfo& PrincipalURL() const; + + bool IsTopLevel() const; + + uint64_t FrameID() const; + + nsPIDOMWindowOuter* GetWindow() const + { + if (mObj.is<Window>()) { + return mObj.as<Window>(); + } + return nullptr; + } + +private: + void SetURL(const URLInfo& aURL); + + const URLInfo mURL; + mutable Maybe<const URLInfo> mPrincipalURL; + + mutable Maybe<bool> mIsTopLevel; + mutable Maybe<nsCOMPtr<nsIPrincipal>> mPrincipal; + mutable Maybe<uint64_t> mFrameID; + + using Window = nsPIDOMWindowOuter*; + using LoadInfo = nsILoadInfo*; + + const Variant<LoadInfo, Window> mObj; +}; + + +class WebExtensionContentScript final : public nsISupports + , public nsWrapperCache +{ + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WebExtensionContentScript) + + + using MatchGlobArray = nsTArray<RefPtr<MatchGlob>>; + using RunAtEnum = dom::ContentScriptRunAt; + + static already_AddRefed<WebExtensionContentScript> + Constructor(dom::GlobalObject& aGlobal, + WebExtensionPolicy& aExtension, + const ContentScriptInit& aInit, + ErrorResult& aRv); + + + bool Matches(const DocInfo& aDoc) const; + bool MatchesURI(const URLInfo& aURL) const; + + bool MatchesLoadInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo) const + { + return Matches({aURL, aLoadInfo}); + } + bool MatchesWindow(nsPIDOMWindowOuter* aWindow) const + { + return Matches(aWindow); + } + + + WebExtensionPolicy* Extension() { return mExtension; } + const WebExtensionPolicy* Extension() const { return mExtension; } + + bool AllFrames() const { return mAllFrames; } + bool MatchAboutBlank() const { return mMatchAboutBlank; } + RunAtEnum RunAt() const { return mRunAt; } + + Nullable<uint64_t> GetFrameID() const { return mFrameID; } + + MatchPatternSet* Matches() { return mMatches; } + const MatchPatternSet* GetMatches() const { return mMatches; } + + MatchPatternSet* GetExcludeMatches() { return mExcludeMatches; } + const MatchPatternSet* GetExcludeMatches() const { return mExcludeMatches; } + + void GetIncludeGlobs(Nullable<MatchGlobArray>& aGlobs) + { + ToNullable(mExcludeGlobs, aGlobs); + } + void GetExcludeGlobs(Nullable<MatchGlobArray>& aGlobs) + { + ToNullable(mExcludeGlobs, aGlobs); + } + + void GetCssPaths(nsTArray<nsString>& aPaths) const + { + aPaths.AppendElements(mCssPaths); + } + void GetJsPaths(nsTArray<nsString>& aPaths) const + { + aPaths.AppendElements(mJsPaths); + } + + + WebExtensionPolicy* GetParentObject() const { return mExtension; } + + virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override; + +protected: + friend class WebExtensionPolicy; + + virtual ~WebExtensionContentScript() = default; + + WebExtensionContentScript(WebExtensionPolicy& aExtension, + const ContentScriptInit& aInit, + ErrorResult& aRv); + +private: + RefPtr<WebExtensionPolicy> mExtension; + + RefPtr<MatchPatternSet> mMatches; + RefPtr<MatchPatternSet> mExcludeMatches; + + Nullable<MatchGlobSet> mIncludeGlobs; + Nullable<MatchGlobSet> mExcludeGlobs; + + nsTArray<nsString> mCssPaths; + nsTArray<nsString> mJsPaths; + + RunAtEnum mRunAt; + + bool mAllFrames; + Nullable<uint64_t> mFrameID; + bool mMatchAboutBlank; + + template <typename T, typename U> + void + ToNullable(const Nullable<T>& aInput, Nullable<U>& aOutput) + { + if (aInput.IsNull()) { + aOutput.SetNull(); + } else { + aOutput.SetValue(aInput.Value()); + } + } +}; + +} // namespace extensions +} // namespace mozilla + +#endif // mozilla_extensions_WebExtensionContentScript_h
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/WebExtensionPolicy.cpp @@ -0,0 +1,507 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#include "mozilla/ExtensionPolicyService.h" +#include "mozilla/extensions/WebExtensionContentScript.h" +#include "mozilla/extensions/WebExtensionPolicy.h" + +#include "mozilla/AddonManagerWebAPI.h" +#include "nsEscape.h" +#include "nsISubstitutingProtocolHandler.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" + +namespace mozilla { +namespace extensions { + +using namespace dom; + +static inline Result<Ok, nsresult> +WrapNSResult(PRStatus aRv) +{ + if (aRv != PR_SUCCESS) { + return Err(NS_ERROR_FAILURE); + } + return Ok(); +} + +static inline Result<Ok, nsresult> +WrapNSResult(nsresult aRv) +{ + if (NS_FAILED(aRv)) { + return Err(aRv); + } + return Ok(); +} + +#define NS_TRY(expr) MOZ_TRY(WrapNSResult(expr)) + +static const char kProto[] = "moz-extension"; + +static const char kBackgroundPageHTMLStart[] = "<!DOCTYPE html>\n\ +<html>\n\ + <head><meta charset=\"utf-8\"></head>\n\ + <body>"; + +static const char kBackgroundPageHTMLScript[] = "\n\ + <script type=\"text/javascript\" src=\"%s\"></script>"; + +static const char kBackgroundPageHTMLEnd[] = "\n\ + <body>\n\ +</html>"; + +class EscapeHTML final : public nsAdoptingCString +{ +public: + explicit EscapeHTML(const nsACString& str) + : nsAdoptingCString(nsEscapeHTML(str.BeginReading())) + {} +}; + + +static inline ExtensionPolicyService& +EPS() +{ + return ExtensionPolicyService::GetSingleton(); +} + +static nsISubstitutingProtocolHandler* +Proto() +{ + static nsCOMPtr<nsISubstitutingProtocolHandler> sHandler; + + if (MOZ_UNLIKELY(!sHandler)) { + nsCOMPtr<nsIIOService> ios = do_GetIOService(); + MOZ_RELEASE_ASSERT(ios); + + nsCOMPtr<nsIProtocolHandler> handler; + ios->GetProtocolHandler(kProto, getter_AddRefs(handler)); + + sHandler = do_QueryInterface(handler); + MOZ_RELEASE_ASSERT(sHandler); + + ClearOnShutdown(&sHandler); + } + + return sHandler; +} + + +/***************************************************************************** + * WebExtensionPolicy + *****************************************************************************/ + +WebExtensionPolicy::WebExtensionPolicy(GlobalObject& aGlobal, + const WebExtensionInit& aInit, + ErrorResult& aRv) + : mId(NS_AtomizeMainThread(aInit.mId)) + , mHostname(aInit.mMozExtensionHostname) + , mContentSecurityPolicy(aInit.mContentSecurityPolicy) + , mLocalizeCallback(aInit.mLocalizeCallback) + , mPermissions(new AtomSet(aInit.mPermissions)) + , mHostPermissions(aInit.mAllowedOrigins) +{ + mWebAccessiblePaths.AppendElements(aInit.mWebAccessibleResources); + + if (!aInit.mBackgroundScripts.IsNull()) { + mBackgroundScripts.SetValue().AppendElements(aInit.mBackgroundScripts.Value()); + } + + if (mContentSecurityPolicy.IsVoid()) { + EPS().DefaultCSP(mContentSecurityPolicy); + } + + mContentScripts.SetCapacity(aInit.mContentScripts.Length()); + for (const auto& scriptInit : aInit.mContentScripts) { + RefPtr<WebExtensionContentScript> contentScript = + new WebExtensionContentScript(*this, scriptInit, aRv); + if (aRv.Failed()) { + return; + } + mContentScripts.AppendElement(Move(contentScript)); + } + + nsresult rv = NS_NewURI(getter_AddRefs(mBaseURI), aInit.mBaseURL); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +already_AddRefed<WebExtensionPolicy> +WebExtensionPolicy::Constructor(GlobalObject& aGlobal, + const WebExtensionInit& aInit, + ErrorResult& aRv) +{ + RefPtr<WebExtensionPolicy> policy = new WebExtensionPolicy(aGlobal, aInit, aRv); + if (aRv.Failed()) { + return nullptr; + } + return policy.forget(); +} + + +/* static */ void +WebExtensionPolicy::GetActiveExtensions(dom::GlobalObject& aGlobal, + nsTArray<RefPtr<WebExtensionPolicy>>& aResults) +{ + EPS().GetAll(aResults); +} + +/* static */ already_AddRefed<WebExtensionPolicy> +WebExtensionPolicy::GetByID(dom::GlobalObject& aGlobal, const nsAString& aID) +{ + return do_AddRef(EPS().GetByID(aID)); +} + +/* static */ already_AddRefed<WebExtensionPolicy> +WebExtensionPolicy::GetByHostname(dom::GlobalObject& aGlobal, const nsACString& aHostname) +{ + return do_AddRef(EPS().GetByHost(aHostname)); +} + +/* static */ already_AddRefed<WebExtensionPolicy> +WebExtensionPolicy::GetByURI(dom::GlobalObject& aGlobal, nsIURI* aURI) +{ + return do_AddRef(EPS().GetByURL(aURI)); +} + + +void +WebExtensionPolicy::SetActive(bool aActive, ErrorResult& aRv) +{ + if (aActive == mActive) { + return; + } + + bool ok = aActive ? Enable() : Disable(); + + if (!ok) { + aRv.Throw(NS_ERROR_UNEXPECTED); + } +} + +bool +WebExtensionPolicy::Enable() +{ + MOZ_ASSERT(!mActive); + + if (!EPS().RegisterExtension(*this)) { + return false; + } + + Unused << Proto()->SetSubstitution(MozExtensionHostname(), mBaseURI); + + mActive = true; + return true; +} + +bool +WebExtensionPolicy::Disable() +{ + MOZ_ASSERT(mActive); + MOZ_ASSERT(EPS().GetByID(Id()) == this); + + if (!EPS().UnregisterExtension(*this)) { + return false; + } + + Unused << Proto()->SetSubstitution(MozExtensionHostname(), nullptr); + + mActive = false; + return true; +} + +void +WebExtensionPolicy::GetURL(const nsAString& aPath, + nsAString& aResult, + ErrorResult& aRv) const +{ + auto result = GetURL(aPath); + if (result.isOk()) { + aResult = result.unwrap(); + } else { + aRv.Throw(result.unwrapErr()); + } +} + +Result<nsString, nsresult> +WebExtensionPolicy::GetURL(const nsAString& aPath) const +{ + nsPrintfCString spec("%s://%s/", kProto, mHostname.get()); + + nsCOMPtr<nsIURI> uri; + NS_TRY(NS_NewURI(getter_AddRefs(uri), spec)); + + NS_TRY(uri->Resolve(NS_ConvertUTF16toUTF8(aPath), spec)); + + return NS_ConvertUTF8toUTF16(spec); +} + +/* static */ bool +WebExtensionPolicy::IsExtensionProcess(GlobalObject& aGlobal) +{ + return EPS().IsExtensionProcess(); +} + +nsCString +WebExtensionPolicy::BackgroundPageHTML() const +{ + nsAutoCString result; + + if (mBackgroundScripts.IsNull()) { + result.SetIsVoid(true); + return result; + } + + result.AppendLiteral(kBackgroundPageHTMLStart); + + for (auto& script : mBackgroundScripts.Value()) { + EscapeHTML escaped{NS_ConvertUTF16toUTF8(script)}; + + result.AppendPrintf(kBackgroundPageHTMLScript, escaped.get()); + } + + result.AppendLiteral(kBackgroundPageHTMLEnd); + return result; +} + +void +WebExtensionPolicy::Localize(const nsAString& aInput, nsString& aOutput) const +{ + mLocalizeCallback->Call(aInput, aOutput); +} + + +JSObject* +WebExtensionPolicy::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) +{ + return WebExtensionPolicyBinding::Wrap(aCx, this, aGivenProto); +} + +void +WebExtensionPolicy::GetContentScripts(nsTArray<RefPtr<WebExtensionContentScript>>& aScripts) const +{ + aScripts.AppendElements(mContentScripts); +} + + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionPolicy, mParent, + mLocalizeCallback, + mHostPermissions, + mWebAccessiblePaths, + mContentScripts) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebExtensionPolicy) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WebExtensionPolicy) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WebExtensionPolicy) + + +/***************************************************************************** + * WebExtensionContentScript + *****************************************************************************/ + +/* static */ already_AddRefed<WebExtensionContentScript> +WebExtensionContentScript::Constructor(GlobalObject& aGlobal, + WebExtensionPolicy& aExtension, + const ContentScriptInit& aInit, + ErrorResult& aRv) +{ + RefPtr<WebExtensionContentScript> script = new WebExtensionContentScript(aExtension, aInit, aRv); + if (aRv.Failed()) { + return nullptr; + } + return script.forget(); +} + +WebExtensionContentScript::WebExtensionContentScript(WebExtensionPolicy& aExtension, + const ContentScriptInit& aInit, + ErrorResult& aRv) + : mExtension(&aExtension) + , mMatches(aInit.mMatches) + , mExcludeMatches(aInit.mExcludeMatches) + , mCssPaths(aInit.mCssPaths) + , mJsPaths(aInit.mJsPaths) + , mRunAt(aInit.mRunAt) + , mAllFrames(aInit.mAllFrames) + , mFrameID(aInit.mFrameID) + , mMatchAboutBlank(aInit.mMatchAboutBlank) +{ + if (!aInit.mIncludeGlobs.IsNull()) { + mIncludeGlobs.SetValue().AppendElements(aInit.mIncludeGlobs.Value()); + } + + if (!aInit.mExcludeGlobs.IsNull()) { + mExcludeGlobs.SetValue().AppendElements(aInit.mExcludeGlobs.Value()); + } +} + + +bool +WebExtensionContentScript::Matches(const DocInfo& aDoc) const +{ + if (!mFrameID.IsNull()) { + if (aDoc.FrameID() != mFrameID.Value()) { + return false; + } + } else { + if (!mAllFrames && !aDoc.IsTopLevel()) { + return false; + } + } + + if (!mMatchAboutBlank && aDoc.URL().InheritsPrincipal()) { + return false; + } + + // Top-level about:blank is a special case. We treat it as a match if + // matchAboutBlank is true and it has the null principal. In all other + // cases, we test the URL of the principal that it inherits. + if (mMatchAboutBlank && aDoc.IsTopLevel() && + aDoc.URL().Spec().EqualsLiteral("about:blank") && + aDoc.Principal()->GetIsNullPrincipal()) { + return true; + } + + return MatchesURI(aDoc.PrincipalURL()); +} + +bool +WebExtensionContentScript::MatchesURI(const URLInfo& aURL) const +{ + if (!mMatches->Matches(aURL)) { + return false; + } + + if (mExcludeMatches && mExcludeMatches->Matches(aURL)) { + return false; + } + + if (!mIncludeGlobs.IsNull() && !mIncludeGlobs.Value().Matches(aURL.Spec())) { + return false; + } + + if (!mExcludeGlobs.IsNull() && mExcludeGlobs.Value().Matches(aURL.Spec())) { + return false; + } + + if (AddonManagerWebAPI::IsValidSite(aURL.URI())) { + return false; + } + + return true; +} + + +JSObject* +WebExtensionContentScript::WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) +{ + return WebExtensionContentScriptBinding::Wrap(aCx, this, aGivenProto); +} + + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(WebExtensionContentScript, + mMatches, mExcludeMatches, + mIncludeGlobs, mExcludeGlobs, + mExtension) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebExtensionContentScript) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(WebExtensionContentScript) +NS_IMPL_CYCLE_COLLECTING_RELEASE(WebExtensionContentScript) + + +/***************************************************************************** + * DocInfo + *****************************************************************************/ + +DocInfo::DocInfo(const URLInfo& aURL, nsILoadInfo* aLoadInfo) + : mURL(aURL) + , mObj(AsVariant(aLoadInfo)) +{} + +DocInfo::DocInfo(nsPIDOMWindowOuter* aWindow) + : mURL(aWindow->GetDocumentURI()) + , mObj(AsVariant(aWindow)) +{} + +bool +DocInfo::IsTopLevel() const +{ + if (mIsTopLevel.isNothing()) { + struct Matcher + { + bool match(Window aWin) { return aWin->IsTopLevelWindow(); } + bool match(LoadInfo aLoadInfo) { return aLoadInfo->GetIsTopLevelLoad(); } + }; + mIsTopLevel.emplace(mObj.match(Matcher())); + } + return mIsTopLevel.ref(); +} + +uint64_t +DocInfo::FrameID() const +{ + if (mFrameID.isNothing()) { + if (IsTopLevel()) { + mFrameID.emplace(0); + } else { + struct Matcher + { + uint64_t match(Window aWin) { return aWin->WindowID(); } + uint64_t match(LoadInfo aLoadInfo) { return aLoadInfo->GetOuterWindowID(); } + }; + mFrameID.emplace(mObj.match(Matcher())); + } + } + return mFrameID.ref(); +} + +nsIPrincipal* +DocInfo::Principal() const +{ + if (mPrincipal.isNothing()) { + struct Matcher + { + nsIPrincipal* match(Window aWin) + { + nsCOMPtr<nsIDocument> doc = aWin->GetDoc(); + return doc->NodePrincipal(); + } + nsIPrincipal* match(LoadInfo aLoadInfo) { return aLoadInfo->PrincipalToInherit(); } + }; + mPrincipal.emplace(mObj.match(Matcher())); + } + return mPrincipal.ref(); +} + +const URLInfo& +DocInfo::PrincipalURL() const +{ + if (!URL().InheritsPrincipal()) { + return URL(); + } + + if (mPrincipalURL.isNothing()) { + nsIPrincipal* prin = Principal(); + nsCOMPtr<nsIURI> uri; + if (prin && NS_SUCCEEDED(prin->GetURI(getter_AddRefs(uri)))) { + mPrincipalURL.emplace(uri); + } else { + mPrincipalURL.emplace(URL()); + } + } + + return mPrincipalURL.ref(); +} + +} // namespace extensions +} // namespace mozilla
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/WebExtensionPolicy.h @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */ +/* 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/. */ + +#ifndef mozilla_extensions_WebExtensionPolicy_h +#define mozilla_extensions_WebExtensionPolicy_h + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/WebExtensionPolicyBinding.h" +#include "mozilla/extensions/MatchPattern.h" + +#include "jspubtd.h" + +#include "mozilla/Result.h" +#include "mozilla/WeakPtr.h" +#include "nsCOMPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupports.h" +#include "nsWrapperCache.h" + +namespace mozilla { +namespace extensions { + +using dom::WebExtensionInit; +using dom::WebExtensionLocalizeCallback; + +class WebExtensionContentScript; + +class WebExtensionPolicy final : public nsISupports + , public nsWrapperCache + , public SupportsWeakPtr<WebExtensionPolicy> +{ +public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(WebExtensionPolicy) + MOZ_DECLARE_WEAKREFERENCE_TYPENAME(WebExtensionPolicy) + + using ScriptArray = nsTArray<RefPtr<WebExtensionContentScript>>; + + static already_AddRefed<WebExtensionPolicy> + Constructor(dom::GlobalObject& aGlobal, const WebExtensionInit& aInit, ErrorResult& aRv); + + nsIAtom* Id() const { return mId; } + void GetId(nsAString& aId) const { aId = nsDependentAtomString(mId); }; + + const nsCString& MozExtensionHostname() const { return mHostname; } + void GetMozExtensionHostname(nsACString& aHostname) const + { + aHostname = MozExtensionHostname(); + } + + void GetBaseURL(nsACString& aBaseURL) const + { + MOZ_ALWAYS_SUCCEEDS(mBaseURI->GetSpec(aBaseURL)); + } + + void GetURL(const nsAString& aPath, nsAString& aURL, ErrorResult& aRv) const; + + Result<nsString, nsresult> GetURL(const nsAString& aPath) const; + + bool CanAccessURI(nsIURI* aURI, bool aExplicit = false) const + { + return mHostPermissions && mHostPermissions->Matches(aURI, aExplicit); + } + + bool IsPathWebAccessible(const nsAString& aPath) const + { + return mWebAccessiblePaths.Matches(aPath); + } + + bool HasPermission(const nsIAtom* aPermission) const + { + return mPermissions->Contains(aPermission); + } + bool HasPermission(const nsAString& aPermission) const + { + return mPermissions->Contains(aPermission); + } + + nsCString BackgroundPageHTML() const; + + void Localize(const nsAString& aInput, nsString& aResult) const; + + const nsString& ContentSecurityPolicy() const + { + return mContentSecurityPolicy; + } + void GetContentSecurityPolicy(nsAString& aCSP) const + { + aCSP = mContentSecurityPolicy; + } + + + already_AddRefed<MatchPatternSet> AllowedOrigins() + { + return do_AddRef(mHostPermissions); + } + void SetAllowedOrigins(MatchPatternSet& aAllowedOrigins) + { + mHostPermissions = &aAllowedOrigins; + } + + void GetPermissions(nsTArray<nsString>& aResult) const + { + mPermissions->Get(aResult); + } + void SetPermissions(const nsTArray<nsString>& aPermissions) + { + mPermissions = new AtomSet(aPermissions); + } + + void GetContentScripts(ScriptArray& aScripts) const; + const ScriptArray& ContentScripts() const { return mContentScripts; } + + + bool Active() const { return mActive; } + void SetActive(bool aActive, ErrorResult& aRv); + + + static void + GetActiveExtensions(dom::GlobalObject& aGlobal, nsTArray<RefPtr<WebExtensionPolicy>>& aResults); + + static already_AddRefed<WebExtensionPolicy> + GetByID(dom::GlobalObject& aGlobal, const nsAString& aID); + + static already_AddRefed<WebExtensionPolicy> + GetByHostname(dom::GlobalObject& aGlobal, const nsACString& aHostname); + + static already_AddRefed<WebExtensionPolicy> + GetByURI(dom::GlobalObject& aGlobal, nsIURI* aURI); + + + static bool IsExtensionProcess(dom::GlobalObject& aGlobal); + + + nsISupports* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, JS::HandleObject aGivenProto) override; + +protected: + virtual ~WebExtensionPolicy() = default; + +private: + WebExtensionPolicy(dom::GlobalObject& aGlobal, const WebExtensionInit& aInit, ErrorResult& aRv); + + bool Enable(); + bool Disable(); + + nsCOMPtr<nsISupports> mParent; + + nsCOMPtr<nsIAtom> mId; + nsCString mHostname; + nsCOMPtr<nsIURI> mBaseURI; + + nsString mContentSecurityPolicy; + + bool mActive = false; + + RefPtr<WebExtensionLocalizeCallback> mLocalizeCallback; + + RefPtr<AtomSet> mPermissions; + RefPtr<MatchPatternSet> mHostPermissions; + MatchGlobSet mWebAccessiblePaths; + + Nullable<nsTArray<nsString>> mBackgroundScripts; + + nsTArray<RefPtr<WebExtensionContentScript>> mContentScripts; +}; + +} // namespace extensions +} // namespace mozilla + +#endif // mozilla_extensions_WebExtensionPolicy_h
--- a/toolkit/components/extensions/ext-cookies.js +++ b/toolkit/components/extensions/ext-cookies.js @@ -64,17 +64,17 @@ function checkSetCookiePermissions(exten // // See source/netwerk/cookie/nsCookieService.cpp, in particular // CheckDomain() and SetCookieInternal(). if (uri.scheme != "http" && uri.scheme != "https") { return false; } - if (!extension.whiteListedHosts.matchesIgnoringPath(uri)) { + if (!extension.whiteListedHosts.matches(uri)) { return false; } if (!cookie.host) { // If no explicit host is specified, this becomes a host-only cookie. cookie.host = uri.host; return true; }
--- a/toolkit/components/extensions/ext-management.js +++ b/toolkit/components/extensions/ext-management.js @@ -51,20 +51,24 @@ function getExtensionInfoForAddon(extens enabled: addon.isActive, optionsUrl: addon.optionsURL || "", installType: installType(addon), type: addon.type, }; if (extension) { let m = extension.manifest; + + let hostPerms = extension.whiteListedHosts.patterns.map(matcher => matcher.pattern); + extInfo.permissions = Array.from(extension.permissions).filter(perm => { - return !extension.whiteListedHosts.pat.includes(perm); + return !hostPerms.includes(perm); }); - extInfo.hostPermissions = extension.whiteListedHosts.pat; + extInfo.hostPermissions = hostPerms; + extInfo.shortName = m.short_name || ""; if (m.icons) { extInfo.icons = Object.keys(m.icons).map(key => { return {size: Number(key), url: m.icons[key]}; }); } }
--- a/toolkit/components/extensions/ext-permissions.js +++ b/toolkit/components/extensions/ext-permissions.js @@ -25,17 +25,17 @@ this.permissions = class extends Extensi for (let perm of permissions) { if (!manifestPermissions.includes(perm)) { throw new ExtensionError(`Cannot request permission ${perm} since it was not declared in optional_permissions`); } } let optionalOrigins = context.extension.optionalOrigins; for (let origin of origins) { - if (!optionalOrigins.subsumes(origin)) { + if (!optionalOrigins.subsumes(new MatchPattern(origin))) { throw new ExtensionError(`Cannot request origin permission for ${origin} since it was not declared in optional_permissions`); } } if (promptsEnabled) { let allow = await new Promise(resolve => { let subject = { wrappedJSObject: { @@ -66,17 +66,17 @@ this.permissions = class extends Extensi async contains(permissions) { for (let perm of permissions.permissions) { if (!context.extension.hasPermission(perm)) { return false; } } for (let origin of permissions.origins) { - if (!context.extension.whiteListedHosts.subsumes(origin)) { + if (!context.extension.whiteListedHosts.subsumes(new MatchPattern(origin))) { return false; } } return true; }, async remove(permissions) {
--- a/toolkit/components/extensions/ext-runtime.js +++ b/toolkit/components/extensions/ext-runtime.js @@ -1,18 +1,16 @@ "use strict"; XPCOMUtils.defineLazyModuleGetter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonManagerPrivate", "resource://gre/modules/AddonManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Extension", "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); this.runtime = class extends ExtensionAPI { getAPI(context) { let {extension} = context; return { runtime: {
--- a/toolkit/components/extensions/ext-webRequest.js +++ b/toolkit/components/extensions/ext-webRequest.js @@ -1,14 +1,10 @@ "use strict"; -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "WebRequest", "resource://gre/modules/WebRequest.jsm"); // EventManager-like class specifically for WebRequest. Inherits from // SingletonEventManager. Takes care of converting |details| parameter // when invoking listeners. function WebRequestEventManager(context, eventName) { let name = `webRequest.${eventName}`; @@ -17,23 +13,23 @@ function WebRequestEventManager(context, // Prevent listening in on requests originating from system principal to // prevent tinkering with OCSP, app and addon updates, etc. if (data.isSystemPrincipal) { return; } // Check hosts permissions for both the resource being requested, const hosts = context.extension.whiteListedHosts; - if (!hosts.matchesIgnoringPath(Services.io.newURI(data.url))) { + if (!hosts.matches(Services.io.newURI(data.url))) { return; } // and the origin that is loading the resource. const origin = data.documentUrl; const own = origin && origin.startsWith(context.extension.getURL()); - if (origin && !own && !hosts.matchesIgnoringPath(Services.io.newURI(origin))) { + if (origin && !own && !hosts.matches(Services.io.newURI(origin))) { return; } let browserData = {tabId: -1, windowId: -1}; if (data.browser) { browserData = tabTracker.getBrowserData(data.browser); } if (filter.tabId != null && browserData.tabId != filter.tabId) { @@ -73,18 +69,22 @@ function WebRequestEventManager(context, } } return fire.sync(data2); }; let filter2 = {}; if (filter.urls) { - filter2.urls = new MatchPattern(filter.urls); - if (!filter2.urls.overlapsPermissions(context.extension.whiteListedHosts, context.extension.optionalOrigins)) { + let perms = new MatchPatternSet([...context.extension.whiteListedHosts.patterns, + ...context.extension.optionalOrigins.patterns]); + + filter2.urls = new MatchPatternSet(filter.urls); + + if (!perms.overlapsAll(filter2.urls)) { Cu.reportError("The webRequest.addListener filter doesn't overlap with host permissions."); } } if (filter.types) { filter2.types = filter.types; } if (filter.tabId) { filter2.tabId = filter.tabId;
--- a/toolkit/components/extensions/extension-process-script.js +++ b/toolkit/components/extensions/extension-process-script.js @@ -10,168 +10,67 @@ * after startup, in *every* browser process live outside of this file. */ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchGlobs", - "resource://gre/modules/MatchPattern.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "MatchPattern", - "resource://gre/modules/MatchPattern.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel", "resource://gre/modules/MessageChannel.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames", - "resource://gre/modules/WebNavigationFrames.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionChild", "resource://gre/modules/ExtensionChild.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPageChild", "resource://gre/modules/ExtensionPageChild.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionUtils", - "resource://gre/modules/ExtensionUtils.jsm"); + +Cu.import("resource://gre/modules/ExtensionUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "console", () => ExtensionUtils.getConsole()); -XPCOMUtils.defineLazyGetter(this, "getInnerWindowID", () => ExtensionUtils.getInnerWindowID); + +const { + DefaultWeakMap, + getInnerWindowID, +} = ExtensionUtils; // We need to avoid touching Services.appinfo here in order to prevent // the wrong version from being cached during xpcshell test startup. const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime); const isContentProcess = appinfo.processType == appinfo.PROCESS_TYPE_CONTENT; - -class ScriptMatcher { - constructor(extension, options) { - this.extension = extension; - this.options = options; - - this._script = null; - - this.allFrames = options.all_frames; - this.matchAboutBlank = options.match_about_blank; - this.frameId = options.frame_id; - this.runAt = options.run_at; - - this.matches = new MatchPattern(options.matches); - this.excludeMatches = new MatchPattern(options.exclude_matches || null); - // TODO: MatchPattern should pre-mangle host-only patterns so that we - // don't need to call a separate match function. - this.matchesHost = new MatchPattern(options.matchesHost || null); - this.includeGlobs = options.include_globs && new MatchGlobs(options.include_globs); - this.excludeGlobs = new MatchGlobs(options.exclude_globs); - } - - toString() { - return `[Script {js: [${this.options.js}], matchAboutBlank: ${this.matchAboutBlank}, runAt: ${this.runAt}, matches: ${this.options.matches}}]`; - } +function parseScriptOptions(options) { + return { + allFrames: options.all_frames, + matchAboutBlank: options.match_about_blank, + frameID: options.frame_id, + runAt: options.run_at, - get script() { - if (!this._script) { - this._script = new ExtensionContent.Script(this.extension.realExtension, - this.options); - } - return this._script; - } - - preload() { - let {script} = this; - - script.loadCSS(); - script.compileScripts(); - } - - matchesLoadInfo(uri, loadInfo) { - if (!this.matchesURI(uri)) { - return false; - } - - if (!this.allFrames && !loadInfo.isTopLevelLoad) { - return false; - } - - return true; - } - - matchesURI(uri) { - if (!(this.matches.matches(uri) || this.matchesHost.matchesIgnoringPath(uri))) { - return false; - } - - if (this.excludeMatches.matches(uri)) { - return false; - } + matches: new MatchPatternSet(options.matches), + excludeMatches: new MatchPatternSet(options.exclude_matches || []), + includeGlobs: options.include_globs && options.include_globs.map(glob => new MatchGlob(glob)), + excludeGlobs: options.exclude_globs && options.exclude_globs.map(glob => new MatchGlob(glob)), - if (this.includeGlobs != null && !this.includeGlobs.matches(uri.spec)) { - return false; - } - - if (this.excludeGlobs.matches(uri.spec)) { - return false; - } - - return true; - } - - matchesWindow(window) { - if (!this.allFrames && this.frameId == null && window.parent !== window) { - return false; - } - - let uri = window.document.documentURIObject; - let principal = window.document.nodePrincipal; - - if (this.matchAboutBlank) { - // When matching top-level about:blank documents, - // allow loading into any with a NullPrincipal. - if (uri.spec === "about:blank" && window === window.parent && principal.isNullPrincipal) { - return true; - } + jsPaths: options.js || [], + cssPaths: options.css || [], + }; +} - // When matching about:blank/srcdoc iframes, the checks below - // need to be performed against the "owner" document's URI. - if (["about:blank", "about:srcdoc"].includes(uri.spec)) { - uri = principal.URI; - } - } - - // Documents from data: URIs also inherit the principal. - if (Services.netUtils.URIChainHasFlags(uri, Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) { - if (!this.matchAboutBlank) { - return false; - } - uri = principal.URI; - } +var extensions = new DefaultWeakMap(policy => { + let extension = new ExtensionChild.BrowserExtensionContent(policy.initData); + extension.policy = policy; + return extension; +}); - if (!this.matchesURI(uri)) { - return false; - } - - if (this.frameId != null && WebNavigationFrames.getFrameId(window) !== this.frameId) { - return false; - } - - // If mozAddonManager is present on this page, don't allow - // content scripts. - if (window.navigator.mozAddonManager !== undefined) { - return false; - } - - return true; - } - - injectInto(window) { - return this.script.injectInto(window); - } -} +var contentScripts = new DefaultWeakMap(matcher => { + return new ExtensionContent.Script(extensions.get(matcher.extension), + matcher); +}); function getMessageManager(window) { let docShell = window.document.docShell.QueryInterface(Ci.nsIInterfaceRequestor); try { return docShell.getInterface(Ci.nsIContentFrameMessageManager); } catch (e) { // Some windows don't support this interface (hidden window). return null; @@ -187,34 +86,42 @@ class ExtensionGlobal { MessageChannel.addListener(global, "Extension:Capture", this); MessageChannel.addListener(global, "Extension:DetectLanguage", this); MessageChannel.addListener(global, "Extension:Execute", this); MessageChannel.addListener(global, "WebNavigation:GetFrame", this); MessageChannel.addListener(global, "WebNavigation:GetAllFrames", this); } - uninit() { - } - get messageFilterStrict() { return { innerWindowID: getInnerWindowID(this.global.content), }; } receiveMessage({target, messageName, recipient, data}) { switch (messageName) { case "Extension:Capture": return ExtensionContent.handleExtensionCapture(this.global, data.width, data.height, data.options); case "Extension:DetectLanguage": return ExtensionContent.handleDetectLanguage(this.global, target); case "Extension:Execute": - let extension = ExtensionManager.get(recipient.extensionId); - let script = new ScriptMatcher(extension, data.options); + let policy = WebExtensionPolicy.getByID(recipient.extensionId); + + let matcher = new WebExtensionContentScript(policy, parseScriptOptions(data.options)); + + Object.assign(matcher, { + wantReturnValue: data.options.wantReturnValue, + removeCSS: data.options.remove_css, + cssOrigin: data.options.css_origin, + cssCode: data.options.cssCode, + jsCode: data.options.jsCode, + }); + + let script = contentScripts.get(matcher); return ExtensionContent.handleExtensionExecute(this.global, target, data.options, script); case "WebNavigation:GetFrame": return ExtensionContent.handleWebNavigationGetFrame(this.global, data.options); case "WebNavigation:GetAllFrames": return ExtensionContent.handleWebNavigationGetAllFrames(this.global); } } @@ -226,53 +133,19 @@ DocumentManager = { globals: new Map(), // Initialize listeners that we need regardless of whether extensions are // enabled. earlyInit() { Services.obs.addObserver(this, "tab-content-frameloader-created"); // eslint-disable-line mozilla/balanced-listeners }, - // Initialize listeners that we need when any extension is enabled. - init() { - Services.obs.addObserver(this, "document-element-inserted"); - }, - uninit() { - Services.obs.removeObserver(this, "document-element-inserted"); - }, - - // Initialize listeners that we need when any extension content script is - // enabled. - initMatchers() { - if (isContentProcess) { - Services.obs.addObserver(this, "http-on-opening-request"); - } - }, - uninitMatchers() { - if (isContentProcess) { - Services.obs.removeObserver(this, "http-on-opening-request"); - } - }, - - // Initialize listeners that we need when any about:blank content script is - // enabled. - // - // Loads of about:blank are special, and do not trigger "document-element-inserted" - // observers. So if we have any scripts that match about:blank, we also need - // to observe "content-document-global-created". - initAboutBlankMatchers() { - Services.obs.addObserver(this, "content-document-global-created"); - }, - uninitAboutBlankMatchers() { - Services.obs.removeObserver(this, "content-document-global-created"); - }, - extensionProcessInitialized: false, initExtensionProcess() { - if (this.extensionProcessInitialized || !ExtensionManagement.isExtensionProcess) { + if (this.extensionProcessInitialized || !WebExtensionPolicy.isExtensionProcess) { return; } this.extensionProcessInitialized = true; for (let global of this.globals.keys()) { ExtensionPageChild.init(global); } }, @@ -281,176 +154,53 @@ DocumentManager = { // into. initGlobal(global) { // Note: {once: true} does not work as expected here. global.addEventListener("unload", event => { // eslint-disable-line mozilla/balanced-listeners this.uninitGlobal(global); }); this.globals.set(global, new ExtensionGlobal(global)); - this.initExtensionProcess(); - if (this.extensionProcessInitialized && ExtensionManagement.isExtensionProcess) { + if (this.extensionProcessInitialized && WebExtensionPolicy.isExtensionProcess) { ExtensionPageChild.init(global); } }, uninitGlobal(global) { if (this.extensionProcessInitialized) { ExtensionPageChild.uninit(global); } - this.globals.get(global).uninit(); this.globals.delete(global); }, initExtension(extension) { - if (this.extensionCount === 0) { - this.init(); - this.initExtensionProcess(); - } - this.extensionCount++; - - for (let script of extension.scripts) { - this.addContentScript(script); - } + this.initExtensionProcess(); this.injectExtensionScripts(extension); }, - uninitExtension(extension) { - for (let script of extension.scripts) { - this.removeContentScript(script); - } - - this.extensionCount--; - if (this.extensionCount === 0) { - this.uninit(); - } - }, - - - extensionCount: 0, - matchAboutBlankCount: 0, - - contentScripts: new Set(), - - addContentScript(script) { - if (this.contentScripts.size == 0) { - this.initMatchers(); - } - - if (script.matchAboutBlank) { - if (this.matchAboutBlankCount == 0) { - this.initAboutBlankMatchers(); - } - this.matchAboutBlankCount++; - } - - this.contentScripts.add(script); - }, - removeContentScript(script) { - this.contentScripts.delete(script); - - if (this.contentScripts.size == 0) { - this.uninitMatchers(); - } - - if (script.matchAboutBlank) { - this.matchAboutBlankCount--; - if (this.matchAboutBlankCount == 0) { - this.uninitAboutBlankMatchers(); - } - } - }, // Listeners - observers: { - async "content-document-global-created"(window) { - // We only care about about:blank here, since it doesn't trigger - // "document-element-inserted". - if ((window.location && window.location.href !== "about:blank") || - // Make sure we only load into frames that belong to tabs, or other - // special areas that we want to load content scripts into. - !this.globals.has(getMessageManager(window))) { - return; - } - - // We can't tell for certain whether the final document will actually be - // about:blank at this point, though, so wait for the DOM to finish - // loading and check again before injecting scripts. - await new Promise(resolve => window.addEventListener( - "DOMContentLoaded", resolve, {once: true, capture: true})); - - if (window.location.href === "about:blank") { - this.injectWindowScripts(window); - } - }, - - "document-element-inserted"(document) { - let window = document.defaultView; - if (!document.location || !window || - // Make sure we only load into frames that belong to tabs, or other - // special areas that we want to load content scripts into. - !this.globals.has(getMessageManager(window))) { - return; - } - - this.injectWindowScripts(window); - this.loadInto(window); - }, - - "http-on-opening-request"(subject, topic, data) { - // If this request is a docshell load, check whether any of our scripts - // are likely to be loaded into it, and begin preloading the ones that - // are. - let {loadInfo} = subject.QueryInterface(Ci.nsIChannel); - if (loadInfo) { - let {externalContentPolicyType: type} = loadInfo; - if (type === Ci.nsIContentPolicy.TYPE_DOCUMENT || - type === Ci.nsIContentPolicy.TYPE_SUBDOCUMENT) { - this.preloadScripts(subject.URI, loadInfo); - } - } - }, - - "tab-content-frameloader-created"(global) { - this.initGlobal(global); - }, - }, - observe(subject, topic, data) { - this.observers[topic].call(this, subject, topic, data); + if (topic == "tab-content-frameloader-created") { + this.initGlobal(subject); + } }, // Script loading injectExtensionScripts(extension) { for (let window of this.enumerateWindows()) { - for (let script of extension.scripts) { + for (let script of extension.contentScripts) { if (script.matchesWindow(window)) { - script.injectInto(window); + contentScripts.get(script).injectInto(window); } } } }, - injectWindowScripts(window) { - for (let script of this.contentScripts) { - if (script.matchesWindow(window)) { - script.injectInto(window); - } - } - }, - - preloadScripts(uri, loadInfo) { - for (let script of this.contentScripts) { - if (script.matchesLoadInfo(uri, loadInfo)) { - script.preload(); - } - } - }, - /** * Checks that all parent frames for the given withdow either have the * same add-on ID, or are special chrome-privileged documents such as * about:addons or developer tools panels. * * @param {Window} window * The window to check. * @param {string} addonId @@ -483,35 +233,26 @@ DocumentManager = { if (principal.addonId !== addonId) { return false; } } return true; }, - loadInto(window) { - let {addonId} = Cu.getObjectPrincipal(window); - if (!addonId) { - return; - } - - let extension = ExtensionManager.get(addonId); - if (!extension) { - throw new Error(`No registered extension for ID ${addonId}`); - } - - if (this.checkParentFrames(window, addonId) && ExtensionManagement.isExtensionProcess) { + loadInto(policy, window) { + let extension = extensions.get(policy); + if (WebExtensionPolicy.isExtensionProcess && this.checkParentFrames(window, policy.id)) { // We're in a top-level extension frame, or a sub-frame thereof, // in the extension process. Inject the full extension page API. - ExtensionPageChild.initExtensionContext(extension.realExtension, window); + ExtensionPageChild.initExtensionContext(extension, window); } else { // We're in a content sub-frame or not in the extension process. // Only inject a minimal content script API. - ExtensionContent.initExtensionContext(extension.realExtension, window); + ExtensionContent.initExtensionContext(extension, window); } }, // Helpers * enumerateWindows(docShell) { if (docShell) { let enum_ = docShell.getDocShellEnumerator(docShell.typeContent, @@ -523,131 +264,99 @@ DocumentManager = { } else { for (let global of this.globals.keys()) { yield* this.enumerateWindows(global.docShell); } } }, }; -/** - * This class is a minimal stub extension object which loads and instantiates a - * real extension object when non-basic functionality is needed. - */ -class StubExtension { - constructor(data) { - this.data = data; - this.id = data.id; - this.uuid = data.uuid; - this.instanceId = data.instanceId; - this.manifest = data.manifest; - - this.scripts = data.content_scripts.map(scriptData => new ScriptMatcher(this, scriptData)); - - this._realExtension = null; - - this.startup(); - } - - startup() { - // Extension.jsm takes care of this in the parent. - if (isContentProcess) { - let uri = Services.io.newURI(this.data.resourceURL); - ExtensionManagement.startupExtension(this.uuid, uri, this); - } - } - - shutdown() { - if (isContentProcess) { - ExtensionManagement.shutdownExtension(this.uuid); - } - if (this._realExtension) { - this._realExtension.shutdown(); - } - } - - // Lazily create the real extension object when needed. - get realExtension() { - if (!this._realExtension) { - this._realExtension = new ExtensionChild.BrowserExtensionContent(this.data); - } - return this._realExtension; - } - - // Forward functions needed by ExtensionManagement. - hasPermission(...args) { - return this.realExtension.hasPermission(...args); - } - localize(...args) { - return this.realExtension.localize(...args); - } - get whiteListedHosts() { - return this.realExtension.whiteListedHosts; - } - get webAccessibleResources() { - return this.realExtension.webAccessibleResources; - } -} - ExtensionManager = { - // Map[extensionId -> StubExtension] - extensions: new Map(), - init() { MessageChannel.setupMessageManagers([Services.cpmm]); Services.cpmm.addMessageListener("Extension:Startup", this); Services.cpmm.addMessageListener("Extension:Shutdown", this); Services.cpmm.addMessageListener("Extension:FlushJarCache", this); let procData = Services.cpmm.initialProcessData || {}; for (let data of procData["Extension:Extensions"] || []) { - let extension = new StubExtension(data); - this.extensions.set(data.id, extension); - DocumentManager.initExtension(extension); + this.initExtension(data); } if (isContentProcess) { // Make sure we handle new schema data until Schemas.jsm is loaded. if (!procData["Extension:Schemas"]) { procData["Extension:Schemas"] = new Map(); } this.schemaJSON = procData["Extension:Schemas"]; Services.cpmm.addMessageListener("Schema:Add", this); } }, - get(extensionId) { - return this.extensions.get(extensionId); + initExtensionPolicy(data, extension) { + let policy = WebExtensionPolicy.getByID(data.id); + if (!policy) { + let localizeCallback = ( + extension ? extension.localize.bind(extension) + : str => extensions.get(policy).localize(str)); + + policy = new WebExtensionPolicy({ + id: data.id, + mozExtensionHostname: data.uuid, + baseURL: data.resourceURL, + + permissions: Array.from(data.permissions), + allowedOrigins: new MatchPatternSet(data.whiteListedHosts), + webAccessibleResources: data.webAccessibleResources.map(host => new MatchGlob(host)), + + contentSecurityPolicy: data.manifest.content_security_policy, + + localizeCallback, + + backgroundScripts: (data.manifest.background && + data.manifest.background.scripts), + + contentScripts: (data.manifest.content_scripts || []).map(parseScriptOptions), + }); + + policy.active = true; + policy.initData = data; + } + return policy; + }, + + initExtension(data) { + let policy = this.initExtensionPolicy(data); + + DocumentManager.initExtension(policy); }, receiveMessage({name, data}) { switch (name) { case "Extension:Startup": { - let extension = new StubExtension(data); - - this.extensions.set(data.id, extension); - - DocumentManager.initExtension(extension); + this.initExtension(data); Services.cpmm.sendAsyncMessage("Extension:StartupComplete"); break; } case "Extension:Shutdown": { - let extension = this.extensions.get(data.id); - this.extensions.delete(data.id); + let policy = WebExtensionPolicy.getByID(data.id); - if (extension) { - extension.shutdown(); + if (extensions.has(policy)) { + extensions.get(policy).shutdown(); + } - DocumentManager.uninitExtension(extension); + if (isContentProcess) { + policy.active = false; } + Services.cpmm.sendAsyncMessage("Extension:ShutdownComplete"); break; } case "Extension:FlushJarCache": { ExtensionUtils.flushJarCache(data.path); Services.cpmm.sendAsyncMessage("Extension:FlushJarCacheComplete"); break; } @@ -655,10 +364,48 @@ ExtensionManager = { case "Schema:Add": { this.schemaJSON.set(data.url, data.schema); break; } } }, }; +function ExtensionProcessScript() { + if (!ExtensionProcessScript.singleton) { + ExtensionProcessScript.singleton = this; + } + return ExtensionProcessScript.singleton; +} + +ExtensionProcessScript.singleton = null; + +ExtensionProcessScript.prototype = { + classID: Components.ID("{21f9819e-4cdf-49f9-85a0-850af91a5058}"), + QueryInterface: XPCOMUtils.generateQI([Ci.mozIExtensionProcessScript]), + + get wrappedJSObject() { return this; }, + + initExtension(data, extension) { + return ExtensionManager.initExtensionPolicy(data, extension); + }, + + initExtensionDocument(policy, doc) { + if (DocumentManager.globals.has(getMessageManager(doc.defaultView))) { + DocumentManager.loadInto(policy, doc.defaultView); + } + }, + + preloadContentScript(contentScript) { + contentScripts.get(contentScript).preload(); + }, + + loadContentScript(contentScript, window) { + if (DocumentManager.globals.has(getMessageManager(window))) { + contentScripts.get(contentScript).injectInto(window); + } + }, +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ExtensionProcessScript]); + DocumentManager.earlyInit(); ExtensionManager.init();
--- a/toolkit/components/extensions/extensions-toolkit.manifest +++ b/toolkit/components/extensions/extensions-toolkit.manifest @@ -2,8 +2,12 @@ category webextension-scripts toolkit chrome://extensions/content/ext-toolkit.js category webextension-scripts-content toolkit chrome://extensions/content/ext-c-toolkit.js category webextension-scripts-devtools toolkit chrome://extensions/content/ext-c-toolkit.js category webextension-scripts-addon toolkit chrome://extensions/content/ext-c-toolkit.js category webextension-schemas events chrome://extensions/content/schemas/events.json category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json category webextension-schemas types chrome://extensions/content/schemas/types.json + + +component {21f9819e-4cdf-49f9-85a0-850af91a5058} extension-process-script.js +contract @mozilla.org/webextensions/extension-process-script;1 {21f9819e-4cdf-49f9-85a0-850af91a5058}
--- a/toolkit/components/extensions/jar.mn +++ b/toolkit/components/extensions/jar.mn @@ -1,15 +1,14 @@ # 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/. toolkit.jar: % content extensions %content/extensions/ - content/extensions/extension-process-script.js content/extensions/ext-alarms.js content/extensions/ext-backgroundPage.js content/extensions/ext-browser-content.js content/extensions/ext-contextualIdentities.js content/extensions/ext-cookies.js content/extensions/ext-downloads.js content/extensions/ext-extension.js content/extensions/ext-geolocation.js
--- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -9,17 +9,16 @@ with Files('**'): EXTRA_JS_MODULES += [ 'Extension.jsm', 'ExtensionAPI.jsm', 'ExtensionChild.jsm', 'ExtensionChildDevToolsUtils.jsm', 'ExtensionCommon.jsm', 'ExtensionContent.jsm', - 'ExtensionManagement.jsm', 'ExtensionPageChild.jsm', 'ExtensionParent.jsm', 'ExtensionPermissions.jsm', 'ExtensionPreferencesManager.jsm', 'ExtensionSettingsStore.jsm', 'ExtensionStorage.jsm', 'ExtensionStorageSync.jsm', 'ExtensionTabs.jsm', @@ -27,37 +26,66 @@ EXTRA_JS_MODULES += [ 'LegacyExtensionsUtils.jsm', 'MessageChannel.jsm', 'NativeMessaging.jsm', 'ProxyScriptContext.jsm', 'Schemas.jsm', ] EXTRA_COMPONENTS += [ + 'extension-process-script.js', 'extensions-toolkit.manifest', ] TESTING_JS_MODULES += [ 'ExtensionTestCommon.jsm', 'ExtensionXPCShellUtils.jsm', ] DIRS += [ 'schemas', 'webrequest', ] +XPIDL_SOURCES += [ + 'mozIExtensionProcessScript.idl', +] + +XPIDL_MODULE = 'webextensions' + +EXPORTS.mozilla = [ + 'ExtensionPolicyService.h', +] + +EXPORTS.mozilla.extensions = [ + 'MatchGlob.h', + 'MatchPattern.h', + 'WebExtensionContentScript.h', + 'WebExtensionPolicy.h', +] + +UNIFIED_SOURCES += [ + 'ExtensionPolicyService.cpp', + 'MatchPattern.cpp', + 'WebExtensionPolicy.cpp', +] + +FINAL_LIBRARY = 'xul' + + JAR_MANIFESTS += ['jar.mn'] BROWSER_CHROME_MANIFESTS += [ 'test/browser/browser.ini', ] MOCHITEST_MANIFESTS += [ 'test/mochitest/mochitest-remote.ini', 'test/mochitest/mochitest.ini' ] MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini'] XPCSHELL_TESTS_MANIFESTS += [ 'test/xpcshell/native_messaging.ini', 'test/xpcshell/xpcshell-remote.ini', 'test/xpcshell/xpcshell.ini', ] + +include('/ipc/chromium/chromium-config.mozbuild')
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/mozIExtensionProcessScript.idl @@ -0,0 +1,18 @@ +/* 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/. */ + +#include "nsISupports.idl" + +interface mozIDOMWindowProxy; +interface nsIDOMDocument; + +[scriptable,uuid(6b09dc51-6caa-4ca7-9d6d-30c87258a630)] +interface mozIExtensionProcessScript : nsISupports +{ + void preloadContentScript(in nsISupports contentScript); + + void loadContentScript(in nsISupports contentScript, in mozIDOMWindowProxy window); + + void initExtensionDocument(in nsISupports extension, in nsIDOMDocument doc); +};
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html +++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_cache.html @@ -62,17 +62,17 @@ add_task(async function test_contentscri let {appinfo} = SpecialPowers.Services; if (appinfo.processType === appinfo.PROCESS_TYPE_CONTENT) { /* globals addMessageListener, assert */ chromeScript = SpecialPowers.loadChromeScript(() => { addMessageListener("check-script-cache", extensionId => { let {ExtensionManager} = Components.utils.import("resource://gre/modules/ExtensionChild.jsm", {}); let ext = ExtensionManager.extensions.get(extensionId); - if (ext) { + if (ext && ext.staticScripts) { assert.equal(ext.staticScripts.size, 0, "Should have no cached scripts in the parent process"); } sendAsyncMessage("done"); }); }); chromeScript.sendAsyncMessage("check-script-cache", extension.id); chromeScriptDone = chromeScript.promiseOneMessage("done");
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_iframe.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Iframe document</title> +</head> +<body> +</body> +</html>
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/data/file_toplevel.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Top-level frame document</title> +</head> +<body> + <iframe src="file_iframe.html"></iframe> + <iframe src="about:blank"></iframe> + <iframe srcdoc="Iframe srcdoc"></iframe> +</body> +</html>
--- a/toolkit/components/extensions/test/xpcshell/head.js +++ b/toolkit/components/extensions/test/xpcshell/head.js @@ -10,18 +10,16 @@ Components.utils.import("resource://gre/ Components.utils.import("resource://testing-common/AddonTestUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ContentTask", "resource://testing-common/ContentTask.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Extension", "resource://gre/modules/Extension.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", "resource://gre/modules/Extension.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils", "resource://testing-common/ExtensionXPCShellUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "HttpServer", "resource://testing-common/httpd.js"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
copy from toolkit/modules/tests/xpcshell/test_MatchPattern.js copy to toolkit/components/extensions/test/xpcshell/test_MatchPattern.js --- a/toolkit/modules/tests/xpcshell/test_MatchPattern.js +++ b/toolkit/components/extensions/test/xpcshell/test_MatchPattern.js @@ -1,51 +1,74 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ - +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ "use strict"; -Components.utils.import("resource://gre/modules/MatchPattern.jsm"); -Components.utils.import("resource://gre/modules/Services.jsm"); +add_task(async function test_MatchPattern_matches() { + function test(url, pattern, normalized = pattern) { + let uri = Services.io.newURI(url); + + pattern = Array.concat(pattern); + normalized = Array.concat(normalized); + + let patterns = pattern.map(pat => new MatchPattern(pat)); + + let set = new MatchPatternSet(pattern); + let set2 = new MatchPatternSet(patterns); -function test_matches() { - function test(url, pattern) { - let uri = Services.io.newURI(url); - let m = new MatchPattern(pattern); - return m.matches(uri); + deepEqual(set2.patterns, patterns, "Patterns in set should equal the input patterns"); + + equal(set.matches(uri), set2.matches(uri), "Single pattern and pattern set should return the same match"); + + for (let [i, pat] of patterns.entries()) { + equal(pat.pattern, normalized[i], "Pattern property should contain correct normalized pattern value"); + } + + if (patterns.length == 1) { + equal(patterns[0].matches(uri), set.matches(uri), "Single pattern and string set should return the same match"); + } + + return set.matches(uri); } - function pass({url, pattern}) { - do_check_true(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`); + function pass({url, pattern, normalized}) { + ok(test(url, pattern, normalized), `Expected match: ${JSON.stringify(pattern)}, ${url}`); } - function fail({url, pattern}) { - do_check_false(test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`); + function fail({url, pattern, normalized}) { + ok(!test(url, pattern, normalized), `Expected no match: ${JSON.stringify(pattern)}, ${url}`); + } + + function invalid({pattern}) { + Assert.throws(() => new MatchPattern(pattern), /.*/, + `Invalid pattern '${pattern}' should throw`); + Assert.throws(() => new MatchPatternSet([pattern]), /.*/, + `Invalid pattern '${pattern}' should throw`); } // Invalid pattern. - fail({url: "http://mozilla.org", pattern: ""}); + invalid({pattern: ""}); // Pattern must include trailing slash. - fail({url: "http://mozilla.org", pattern: "http://mozilla.org"}); + invalid({pattern: "http://mozilla.org"}); // Protocol not allowed. - fail({url: "http://mozilla.org", pattern: "gopher://wuarchive.wustl.edu/"}); + invalid({pattern: "gopher://wuarchive.wustl.edu/"}); pass({url: "http://mozilla.org", pattern: "http://mozilla.org/"}); pass({url: "http://mozilla.org/", pattern: "http://mozilla.org/"}); pass({url: "http://mozilla.org/", pattern: "*://mozilla.org/"}); pass({url: "https://mozilla.org/", pattern: "*://mozilla.org/"}); fail({url: "file://mozilla.org/", pattern: "*://mozilla.org/"}); fail({url: "ftp://mozilla.org/", pattern: "*://mozilla.org/"}); fail({url: "http://mozilla.com", pattern: "http://*mozilla.com*/"}); fail({url: "http://mozilla.com", pattern: "http://mozilla.*/"}); - fail({url: "http://mozilla.com", pattern: "http:/mozilla.com/"}); + invalid({pattern: "http:/mozilla.com/"}); pass({url: "http://google.com", pattern: "http://*.google.com/"}); pass({url: "http://docs.google.com", pattern: "http://*.google.com/"}); pass({url: "http://mozilla.org:8080", pattern: "http://mozilla.org/"}); pass({url: "http://mozilla.org:8080", pattern: "*://mozilla.org/"}); fail({url: "http://mozilla.org:8080", pattern: "http://mozilla.org:8080/"}); @@ -65,17 +88,17 @@ function test_matches() { fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/"}); pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*"}); pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*f"}); pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/a*"}); pass({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*f"}); fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*e"}); fail({url: "http://mozilla.com/abc/def", pattern: "http://mozilla.com/*c"}); - fail({url: "http:///a.html", pattern: "http:///a.html"}); + invalid({pattern: "http:///a.html"}); pass({url: "file:///foo", pattern: "file:///foo*"}); pass({url: "file:///foo/bar.html", pattern: "file:///foo*"}); pass({url: "http://mozilla.org/a", pattern: "<all_urls>"}); pass({url: "https://mozilla.org/a", pattern: "<all_urls>"}); pass({url: "ftp://mozilla.org/a", pattern: "<all_urls>"}); pass({url: "file:///a", pattern: "<all_urls>"}); fail({url: "gopher://wuarchive.wustl.edu/a", pattern: "<all_urls>"}); @@ -83,22 +106,27 @@ function test_matches() { // Multiple patterns. pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/"]}); pass({url: "http://mozilla.org", pattern: ["http://mozilla.org/", "http://mozilla.com/"]}); pass({url: "http://mozilla.com", pattern: ["http://mozilla.org/", "http://mozilla.com/"]}); fail({url: "http://mozilla.biz", pattern: ["http://mozilla.org/", "http://mozilla.com/"]}); // Match url with fragments. pass({url: "http://mozilla.org/base#some-fragment", pattern: "http://mozilla.org/base"}); -} +}); -function test_overlaps() { +add_task(async function test_MatchPattern_overlaps() { function test(filter, hosts, optional) { - const f = new MatchPattern(filter); - return f.overlapsPermissions(new MatchPattern(hosts), new MatchPattern(optional)); + filter = Array.concat(filter); + hosts = Array.concat(hosts); + optional = Array.concat(optional); + + const set = new MatchPatternSet([...hosts, ...optional]); + const pat = new MatchPatternSet(filter); + return set.overlapsAll(pat); } function pass({filter = [], hosts = [], optional = []}) { ok(test(filter, hosts, optional), `Expected overlap: ${filter}, ${hosts} (${optional})`); } function fail({filter = [], hosts = [], optional = []}) { ok(!test(filter, hosts, optional), `Expected no overlap: ${filter}, ${hosts} (${optional})`); @@ -127,17 +155,16 @@ function test_overlaps() { fail({hosts: "http://*.ab.cd/", filter: "http://*.fake-ab.cd/"}); // Wildcard domain. pass({hosts: "http://*/", filter: "http://ab.cd/"}); fail({hosts: "http://*/", filter: "https://ab.cd/"}); // Wildcard wildcards. pass({hosts: "<all_urls>", filter: "ftp://ab.cd/"}); - fail({hosts: "<all_urls>", filter: ""}); fail({hosts: "<all_urls>"}); // Multiple hosts. pass({hosts: ["http://ab.cd/"], filter: ["http://ab.cd/"]}); pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.cd/"}); pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.xy/"}); fail({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: "http://ab.zz/"}); @@ -145,14 +172,57 @@ function test_overlaps() { pass({hosts: ["http://*.ab.cd/"], filter: ["http://ab.cd/", "http://www.ab.cd/"]}); pass({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: ["http://ab.cd/", "http://ab.xy/"]}); fail({hosts: ["http://ab.cd/", "http://ab.xy/"], filter: ["http://ab.cd/", "http://ab.zz/"]}); // Optional. pass({hosts: [], optional: "http://ab.cd/", filter: "http://ab.cd/"}); pass({hosts: "http://ab.cd/", optional: "http://ab.xy/", filter: ["http://ab.cd/", "http://ab.xy/"]}); fail({hosts: "http://ab.cd/", optional: "https://ab.xy/", filter: "http://ab.xy/"}); -} +}); + +add_task(async function test_MatchGlob() { + function test(url, pattern) { + let m = new MatchGlob(pattern[0]); + return m.matches(Services.io.newURI(url).spec); + } + + function pass({url, pattern}) { + ok(test(url, pattern), `Expected match: ${JSON.stringify(pattern)}, ${url}`); + } + + function fail({url, pattern}) { + ok(!test(url, pattern), `Expected no match: ${JSON.stringify(pattern)}, ${url}`); + } + + let moz = "http://mozilla.org"; + + pass({url: moz, pattern: ["*"]}); + pass({url: moz, pattern: ["http://*"]}); + pass({url: moz, pattern: ["*mozilla*"]}); + // pass({url: moz, pattern: ["*example*", "*mozilla*"]}); -function run_test() { - test_matches(); - test_overlaps(); -} + pass({url: moz, pattern: ["*://*"]}); + pass({url: "https://mozilla.org", pattern: ["*://*"]}); + + // Documentation example + pass({url: "http://www.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]}); + pass({url: "http://the.example.com/foo/", pattern: ["http://???.example.com/foo/*"]}); + fail({url: "http://my.example.com/foo/bar", pattern: ["http://???.example.com/foo/*"]}); + fail({url: "http://example.com/foo/", pattern: ["http://???.example.com/foo/*"]}); + fail({url: "http://www.example.com/foo", pattern: ["http://???.example.com/foo/*"]}); + + // Matches path + let path = moz + "/abc/def"; + pass({url: path, pattern: ["*def"]}); + pass({url: path, pattern: ["*c/d*"]}); + pass({url: path, pattern: ["*org/abc*"]}); + fail({url: path + "/", pattern: ["*def"]}); + + // Trailing slash + pass({url: moz, pattern: ["*.org/"]}); + fail({url: moz, pattern: ["*.org"]}); + + // Wrong TLD + fail({url: moz, pattern: ["*oz*.com/"]}); + // Case sensitive + fail({url: moz, pattern: ["*.ORG/"]}); +});
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionContentScript.js @@ -0,0 +1,176 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const {newURI} = Services.io; + +const server = createHttpServer(); +server.registerDirectory("/data/", do_get_file("data")); + +let policy = new WebExtensionPolicy({ + id: "foo@bar.baz", + mozExtensionHostname: "88fb51cd-159f-4859-83db-7065485bc9b2", + baseURL: "file:///foo", + + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, +}); + +add_task(async function test_WebExtensinonContentScript_url_matching() { + let contentScript = new WebExtensionContentScript(policy, { + matches: new MatchPatternSet(["http://foo.com/bar", "*://bar.com/baz/*"]), + + excludeMatches: new MatchPatternSet(["*://bar.com/baz/quux"]), + + includeGlobs: ["*flerg*", "*.com/bar", "*/quux"].map(glob => new MatchGlob(glob)), + + excludeGlobs: ["*glorg*"].map(glob => new MatchGlob(glob)), + }); + + ok(contentScript.matchesURI(newURI("http://foo.com/bar")), + "Simple matches include should match"); + + ok(contentScript.matchesURI(newURI("https://bar.com/baz/xflergx")), + "Simple matches include should match"); + + ok(!contentScript.matchesURI(newURI("https://bar.com/baz/xx")), + "Failed includeGlobs match pattern should not match"); + + ok(!contentScript.matchesURI(newURI("https://bar.com/baz/quux")), + "Excluded match pattern should not match"); + + ok(!contentScript.matchesURI(newURI("https://bar.com/baz/xflergxglorgx")), + "Excluded match glob should not match"); +}); + +async function loadURL(url, {frameCount}) { + let windows = new Map(); + let requests = new Map(); + + let resolveLoad; + let loadPromise = new Promise(resolve => { resolveLoad = resolve; }); + + function requestObserver(request) { + request.QueryInterface(Ci.nsIChannel); + if (request.isDocument) { + requests.set(request.name, request); + } + } + function loadObserver(window) { + windows.set(window.location.href, window); + if (windows.size == frameCount) { + resolveLoad(); + } + } + + Services.obs.addObserver(requestObserver, "http-on-examine-response"); + Services.obs.addObserver(loadObserver, "content-document-global-created"); + + let webNav = Services.appShell.createWindowlessBrowser(false); + webNav.loadURI(url, 0, null, null, null); + + await loadPromise; + + Services.obs.removeObserver(requestObserver, "http-on-examine-response"); + Services.obs.removeObserver(loadObserver, "content-document-global-created"); + + return {webNav, windows, requests}; +} + +add_task(async function test_WebExtensinonContentScript_frame_matching() { + if (AppConstants.platform == "linux") { + // The windowless browser currently does not load correctly on Linux on + // infra. + return; + } + + let baseURL = `http://localhost:${server.identity.primaryPort}/data`; + let urls = { + topLevel: `${baseURL}/file_toplevel.html`, + iframe: `${baseURL}/file_iframe.html`, + srcdoc: "about:srcdoc", + aboutBlank: "about:blank", + }; + + let {webNav, windows, requests} = await loadURL(urls.topLevel, {frameCount: 4}); + + let tests = [ + { + contentScript: { + matches: new MatchPatternSet(["http://localhost/data/*"]), + }, + topLevel: true, + iframe: false, + aboutBlank: false, + srcdoc: false, + }, + + { + contentScript: { + matches: new MatchPatternSet(["http://localhost/data/*"]), + frameID: 0, + }, + topLevel: true, + iframe: false, + aboutBlank: false, + srcdoc: false, + }, + + { + contentScript: { + matches: new MatchPatternSet(["http://localhost/data/*"]), + allFrames: true, + }, + topLevel: true, + iframe: true, + aboutBlank: false, + srcdoc: false, + }, + + { + contentScript: { + matches: new MatchPatternSet(["http://localhost/data/*"]), + allFrames: true, + matchAboutBlank: true, + }, + topLevel: true, + iframe: true, + aboutBlank: true, + srcdoc: true, + }, + + { + contentScript: { + matches: new MatchPatternSet(["http://foo.com/data/*"]), + allFrames: true, + matchAboutBlank: true, + }, + topLevel: false, + iframe: false, + aboutBlank: false, + srcdoc: false, + }, + ]; + + for (let [i, test] of tests.entries()) { + let contentScript = new WebExtensionContentScript(policy, test.contentScript); + + for (let [frame, url] of Object.entries(urls)) { + let should = test[frame] ? "should" : "should not"; + + equal(contentScript.matchesWindow(windows.get(url)), + test[frame], + `Script ${i} ${should} match the ${frame} frame`); + + if (url.startsWith("http")) { + let request = requests.get(url); + + equal(contentScript.matchesLoadInfo(request.URI, request.loadInfo), + test[frame], + `Script ${i} ${should} match the request LoadInfo for ${frame} frame`); + } + } + } + + webNav.close(); +});
new file mode 100644 --- /dev/null +++ b/toolkit/components/extensions/test/xpcshell/test_WebExtensionPolicy.js @@ -0,0 +1,140 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80: */ +"use strict"; + +const {newURI} = Services.io; + +add_task(async function test_WebExtensinonPolicy() { + const id = "foo@bar.baz"; + const uuid = "ca9d3f23-125c-4b24-abfc-1ca2692b0610"; + + const baseURL = "file:///foo/"; + const mozExtURL = `moz-extension://${uuid}/`; + const mozExtURI = newURI(mozExtURL); + + let policy = new WebExtensionPolicy({ + id, + mozExtensionHostname: uuid, + baseURL, + + localizeCallback(str) { + return `<${str}>`; + }, + + allowedOrigins: new MatchPatternSet(["http://foo.bar/", "*://*.baz/"], {ignorePath: true}), + permissions: ["<all_urls>"], + webAccessibleResources: ["/foo/*", "/bar.baz"].map(glob => new MatchGlob(glob)), + }); + + equal(policy.active, false, "Active attribute should initially be false"); + + // GetURL + + equal(policy.getURL(), mozExtURL, "getURL() should return the correct root URL"); + equal(policy.getURL("path/foo.html"), `${mozExtURL}path/foo.html`, "getURL(path) should return the correct URL"); + + + // Permissions + + deepEqual(policy.permissions, ["<all_urls>"], "Initial permissions should be correct"); + + ok(policy.hasPermission("<all_urls>"), "hasPermission should match existing permission"); + ok(!policy.hasPermission("history"), "hasPermission should not match nonexistent permission"); + + Assert.throws(() => { policy.permissions[0] = "foo"; }, + TypeError, + "Permissions array should be frozen"); + + policy.permissions = ["history"]; + deepEqual(policy.permissions, ["history"], "Permissions should be updateable as a set"); + + ok(policy.hasPermission("history"), "hasPermission should match existing permission"); + ok(!policy.hasPermission("<all_urls>"), "hasPermission should not match nonexistent permission"); + + + // Origins + + ok(policy.canAccessURI(newURI("http://foo.bar/quux")), "Should be able to access whitelisted URI"); + ok(policy.canAccessURI(newURI("https://x.baz/foo")), "Should be able to access whitelisted URI"); + + ok(!policy.canAccessURI(newURI("https://foo.bar/quux")), "Should not be able to access non-whitelisted URI"); + + policy.allowedOrigins = new MatchPatternSet(["https://foo.bar/"], {ignorePath: true}); + + ok(policy.canAccessURI(newURI("https://foo.bar/quux")), "Should be able to access updated whitelisted URI"); + ok(!policy.canAccessURI(newURI("https://x.baz/foo")), "Should not be able to access removed whitelisted URI"); + + + // Web-accessible resources + + ok(policy.isPathWebAccessible("/foo/bar"), "Web-accessible glob should be web-accessible"); + ok(policy.isPathWebAccessible("/bar.baz"), "Web-accessible path should be web-accessible"); + ok(!policy.isPathWebAccessible("/bar.baz/quux"), "Non-web-accessible path should not be web-accessible"); + + + // Localization + + equal(policy.localize("foo"), "<foo>", "Localization callback should work as expected"); + + + // Protocol and lookups. + + let proto = Services.io.getProtocolHandler("moz-extension", uuid).QueryInterface(Ci.nsISubstitutingProtocolHandler); + + deepEqual(WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions"); + equal(WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active"); + equal(WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active"); + Assert.throws(() => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/, + "URL should not resolve when not active"); + + policy.active = true; + equal(policy.active, true, "Active attribute should be updated"); + + let exts = WebExtensionPolicy.getActiveExtensions(); + equal(exts.length, 1, "Should have one active extension"); + equal(exts[0], policy, "Should have the correct active extension"); + + equal(WebExtensionPolicy.getByID(id), policy, "ID lookup should return extension when active"); + equal(WebExtensionPolicy.getByHostname(uuid), policy, "Hostname lookup should return extension when active"); + + equal(proto.resolveURI(mozExtURI), baseURL, "URL should resolve correctly while active"); + + policy.active = false; + equal(policy.active, false, "Active attribute should be updated"); + + deepEqual(WebExtensionPolicy.getActiveExtensions(), [], "Should have no active extensions"); + equal(WebExtensionPolicy.getByID(id), null, "ID lookup should not return extension when not active"); + equal(WebExtensionPolicy.getByHostname(uuid), null, "Hostname lookup should not return extension when not active"); + Assert.throws(() => proto.resolveURI(mozExtURI), /NS_ERROR_NOT_AVAILABLE/, + "URL should not resolve when not active"); + + + // Conflicting policies. + + // This asserts in debug builds, so only test in non-debug builds. + if (!AppConstants.DEBUG) { + policy.active = true; + + let attrs = [{id, uuid}, + {id, uuid: "d916886c-cfdf-482e-b7b1-d7f5b0facfa5"}, + {id: "foo@quux", uuid}]; + + // eslint-disable-next-line no-shadow + for (let {id, uuid} of attrs) { + let policy2 = new WebExtensionPolicy({ + id, + mozExtensionHostname: uuid, + baseURL: "file://bar/", + + localizeCallback() {}, + + allowedOrigins: new MatchPatternSet([]), + }); + + Assert.throws(() => { policy2.active = true; }, /NS_ERROR_UNEXPECTED/, + `Should not be able to activate conflicting policy: ${id} ${uuid}`); + } + + policy.active = false; + } +});
--- a/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js +++ b/toolkit/components/extensions/test/xpcshell/test_csp_custom_policies.js @@ -2,37 +2,55 @@ /* vim: set sts=2 sw=2 et tw=80: */ "use strict"; Cu.import("resource://gre/modules/Preferences.jsm"); const ADDON_ID = "test@web.extension"; const aps = Cc["@mozilla.org/addons/policy-service;1"] - .getService(Ci.nsIAddonPolicyService).wrappedJSObject; + .getService(Ci.nsIAddonPolicyService); + +let policy = null; + +function setAddonCSP(csp) { + if (policy) { + policy.active = false; + } + + policy = new WebExtensionPolicy({ + id: ADDON_ID, + mozExtensionHostname: ADDON_ID, + baseURL: "file:///", + + allowedOrigins: new MatchPatternSet([]), + localizeCallback() {}, + + contentSecurityPolicy: csp, + }); + + policy.active = true; +} do_register_cleanup(() => { - aps.setAddonCSP(ADDON_ID, null); + policy.active = false; }); add_task(function* test_addon_csp() { equal(aps.baseCSP, Preferences.get("extensions.webextensions.base-content-security-policy"), "Expected base CSP value"); equal(aps.defaultCSP, Preferences.get("extensions.webextensions.default-content-security-policy"), "Expected default CSP value"); - equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP, - "CSP for unknown add-on ID should be the default CSP"); - const CUSTOM_POLICY = "script-src: 'self' https://xpcshell.test.custom.csp; object-src: 'none'"; - aps.setAddonCSP(ADDON_ID, CUSTOM_POLICY); + setAddonCSP(CUSTOM_POLICY); equal(aps.getAddonCSP(ADDON_ID), CUSTOM_POLICY, "CSP should point to add-on's custom policy"); - aps.setAddonCSP(ADDON_ID, null); + setAddonCSP(null); equal(aps.getAddonCSP(ADDON_ID), aps.defaultCSP, "CSP should revert to default when set to null"); });
--- a/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js @@ -137,18 +137,18 @@ add_task(async function test_experiments Services.obs.removeObserver(observer, "webext-api-loaded"); Services.obs.removeObserver(observer, "webext-api-hello"); }); // Install API add-on. let apiAddon = await AddonManager.installTemporaryAddon(apiAddonFile); - let {APIs} = Cu.import("resource://gre/modules/ExtensionManagement.jsm", {}); - ok(APIs.apis.has("meh"), "Should have meh API."); + let {ExtensionAPIs} = Cu.import("resource://gre/modules/ExtensionAPI.jsm", {}); + ok(ExtensionAPIs.apis.has("meh"), "Should have meh API."); // Install boring WebExtension add-on. let boringAddon = await AddonManager.installTemporaryAddon(boringAddonFile); await promiseAddonStartup(); // Install interesting WebExtension add-on.
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js +++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js @@ -24,18 +24,21 @@ function findWinUtils(extension) { notEqual(bgwin, null, "Found background window for the test extension"); return bgwin.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); } add_task(async function test_permissions() { const REQUIRED_PERMISSIONS = ["downloads"]; const REQUIRED_ORIGINS = ["*://site.com/", "*://*.domain.com/"]; + const REQUIRED_ORIGINS_NORMALIZED = ["*://site.com/*", "*://*.domain.com/*"]; + const OPTIONAL_PERMISSIONS = ["idle", "clipboardWrite"]; const OPTIONAL_ORIGINS = ["http://optionalsite.com/", "https://*.optionaldomain.com/"]; + const OPTIONAL_ORIGINS_NORMALIZED = ["http://optionalsite.com/*", "https://*.optionaldomain.com/*"]; let acceptPrompt = false; const observer = { observe(subject, topic, data) { if (topic == "webextension-optional-permission-prompt") { let {resolve} = subject.wrappedJSObject; resolve(acceptPrompt); } @@ -87,17 +90,17 @@ add_task(async function test_permissions function call(method, arg) { extension.sendMessage(method, arg); return extension.awaitMessage(`${method}.result`); } let result = await call("getAll"); deepEqual(result.permissions, REQUIRED_PERMISSIONS); - deepEqual(result.origins, REQUIRED_ORIGINS); + deepEqual(result.origins, REQUIRED_ORIGINS_NORMALIZED); for (let perm of REQUIRED_PERMISSIONS) { result = await call("contains", {permissions: [perm]}); equal(result, true, `contains() returns true for fixed permission ${perm}`); } for (let origin of REQUIRED_ORIGINS) { result = await call("contains", {origins: [origin]}); equal(result, true, `contains() returns true for fixed origin ${origin}`); @@ -150,17 +153,17 @@ add_task(async function test_permissions }; result = await call("request", allOptional); equal(result.status, "success", "request() returned cleanly"); equal(result.result, true, "request() returned true for accepted permissions"); userInputHandle.destruct(); let allPermissions = { permissions: [...REQUIRED_PERMISSIONS, ...OPTIONAL_PERMISSIONS], - origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS], + origins: [...REQUIRED_ORIGINS_NORMALIZED, ...OPTIONAL_ORIGINS_NORMALIZED], }; result = await call("getAll"); deepEqual(result, allPermissions, "getAll() returns required and runtime requested permissions"); result = await call("contains", allPermissions); equal(result, true, "contains() returns true for runtime requested permissions"); @@ -172,25 +175,25 @@ add_task(async function test_permissions deepEqual(result, allPermissions, "Runtime requested permissions are still present after restart"); // Check remove() result = await call("remove", {permissions: OPTIONAL_PERMISSIONS}); equal(result, true, "remove() succeeded"); let perms = { permissions: REQUIRED_PERMISSIONS, - origins: [...REQUIRED_ORIGINS, ...OPTIONAL_ORIGINS], + origins: [...REQUIRED_ORIGINS_NORMALIZED, ...OPTIONAL_ORIGINS_NORMALIZED], }; result = await call("getAll"); deepEqual(result, perms, "Expected permissions remain after removing some"); result = await call("remove", {origins: OPTIONAL_ORIGINS}); equal(result, true, "remove() succeeded"); - perms.origins = REQUIRED_ORIGINS; + perms.origins = REQUIRED_ORIGINS_NORMALIZED; result = await call("getAll"); deepEqual(result, perms, "Back to default permissions after removing more"); await extension.unload(); }); add_task(async function test_startup() { async function background() { @@ -202,17 +205,17 @@ add_task(async function test_startup() { let all = await browser.permissions.getAll(); browser.test.sendMessage("perms", all); } const PERMS1 = { permissions: ["clipboardRead", "tabs"], }; const PERMS2 = { - origins: ["https://site2.com/"], + origins: ["https://site2.com/*"], }; let extension1 = ExtensionTestUtils.loadExtension({ background, manifest: {optional_permissions: PERMS1.permissions}, useAddonManager: "permanent", }); let extension2 = ExtensionTestUtils.loadExtension({
--- a/toolkit/components/extensions/test/xpcshell/test_locale_converter.js +++ b/toolkit/components/extensions/test/xpcshell/test_locale_converter.js @@ -18,32 +18,32 @@ function StringStream(string) { stream.data = string; return stream; } // Initialize the policy service with a stub localizer for our // add-on ID. add_task(async function init() { - const aps = Cc["@mozilla.org/addons/policy-service;1"] - .getService(Ci.nsIAddonPolicyService).wrappedJSObject; + let policy = new WebExtensionPolicy({ + id: ADDON_ID, + mozExtensionHostname: UUID, + baseURL: "file:///", - let oldCallback = aps.setExtensionURIToAddonIdCallback(uri => { - if (uri.host == UUID) { - return ADDON_ID; - } + allowedOrigins: new MatchPatternSet([]), + + localizeCallback(string) { + return string.replace(/__MSG_(.*?)__/g, "<localized-$1>"); + }, }); - aps.setAddonLocalizeCallback(ADDON_ID, string => { - return string.replace(/__MSG_(.*?)__/g, "<localized-$1>"); - }); + policy.active = true; do_register_cleanup(() => { - aps.setExtensionURIToAddonIdCallback(oldCallback); - aps.setAddonLocalizeCallback(ADDON_ID, null); + policy.active = false; }); }); // Test that the synchronous converter works as expected with a // simple string. add_task(async function testSynchronousConvert() { let stream = StringStream("Foo __MSG_xxx__ bar __MSG_yyy__ baz");
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini +++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini @@ -4,16 +4,20 @@ firefox-appdir = browser skip-if = appname == "thunderbird" dupe-manifest = support-files = data/** head_sync.js xpcshell-content.ini tags = webextensions +[test_MatchPattern.js] +[test_WebExtensionContentScript.js] +[test_WebExtensionPolicy.js] + [test_csp_custom_policies.js] [test_csp_validator.js] [test_ext_alarms.js] [test_ext_alarms_does_not_fire.js] [test_ext_alarms_periodic.js] [test_ext_alarms_replaces.js] [test_ext_api_permissions.js] [test_ext_background_generated_load_events.js]
--- a/toolkit/components/utils/simpleServices.js +++ b/toolkit/components/utils/simpleServices.js @@ -12,224 +12,33 @@ "use strict"; const Cc = Components.classes; const Cu = Components.utils; const Ci = Components.interfaces; const Cr = Components.results; +/* globals WebExtensionPolicy */ + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm"); -function AddonPolicyService() { - this.wrappedJSObject = this; - this.cspStrings = new Map(); - this.backgroundPageUrlCallbacks = new Map(); - this.checkHasPermissionCallbacks = new Map(); - this.mayLoadURICallbacks = new Map(); - this.localizeCallbacks = new Map(); - - XPCOMUtils.defineLazyPreferenceGetter( - this, "baseCSP", "extensions.webextensions.base-content-security-policy", - "script-src 'self' https://* moz-extension: blob: filesystem: 'unsafe-eval' 'unsafe-inline'; " + - "object-src 'self' https://* moz-extension: blob: filesystem:;"); - - XPCOMUtils.defineLazyPreferenceGetter( - this, "defaultCSP", "extensions.webextensions.default-content-security-policy", - "script-src 'self'; object-src 'self';"); -} - -AddonPolicyService.prototype = { - classID: Components.ID("{89560ed3-72e3-498d-a0e8-ffe50334d7c5}"), - QueryInterface: XPCOMUtils.generateQI([Ci.nsIAddonPolicyService]), - - /** - * Returns the content security policy which applies to documents belonging - * to the extension with the given ID. This may be either a custom policy, - * if one was supplied, or the default policy if one was not. - */ - getAddonCSP(aAddonId) { - let csp = this.cspStrings.get(aAddonId); - return csp || this.defaultCSP; - }, - - /** - * Returns the generated background page as a data-URI, if any. If the addon - * does not have an auto-generated background page, an empty string is - * returned. - */ - getGeneratedBackgroundPageUrl(aAddonId) { - let cb = this.backgroundPageUrlCallbacks.get(aAddonId); - return cb && cb(aAddonId) || ""; - }, - - /* - * Invokes a callback (if any) associated with the addon to determine whether - * the addon is granted the |aPerm| API permission. - * - * @see nsIAddonPolicyService.addonHasPermission - */ - addonHasPermission(aAddonId, aPerm) { - let cb = this.checkHasPermissionCallbacks.get(aAddonId); - return cb ? cb(aPerm) : false; - }, - - /* - * Invokes a callback (if any) associated with the addon to determine whether - * unprivileged code running within the addon is allowed to perform loads from - * the given URI. - * - * @see nsIAddonPolicyService.addonMayLoadURI - */ - addonMayLoadURI(aAddonId, aURI, aExplicit = false) { - let cb = this.mayLoadURICallbacks.get(aAddonId); - return cb ? cb(aURI, aExplicit) : false; - }, - - /* - * Invokes a callback (if any) associated with the addon to loclaize a - * resource belonging to that add-on. - */ - localizeAddonString(aAddonId, aString) { - let cb = this.localizeCallbacks.get(aAddonId); - return cb ? cb(aString) : aString; - }, - - /* - * Invokes a callback (if any) to determine if an extension URI should be - * web-accessible. - * - * @see nsIAddonPolicyService.extensionURILoadableByAnyone - */ - extensionURILoadableByAnyone(aURI) { - if (aURI.scheme != "moz-extension") { - throw new TypeError("non-extension URI passed"); - } - - let cb = this.extensionURILoadCallback; - return cb ? cb(aURI) : false; - }, - - /* - * Maps an extension URI to an addon ID. - * - * @see nsIAddonPolicyService.extensionURIToAddonId - */ - extensionURIToAddonId(aURI) { - if (aURI.scheme != "moz-extension") { - throw new TypeError("non-extension URI passed"); - } - - let cb = this.extensionURIToAddonIdCallback; - if (!cb) { - throw new Error("no callback set to map extension URIs to addon Ids"); - } - return cb(aURI); - }, - - /* - * Sets the callbacks used in addonHasPermission above. Not accessible over - * XPCOM - callers should use .wrappedJSObject on the service to call it - * directly. - */ - setAddonHasPermissionCallback(aAddonId, aCallback) { - if (aCallback) { - this.checkHasPermissionCallbacks.set(aAddonId, aCallback); - } else { - this.checkHasPermissionCallbacks.delete(aAddonId); - } - }, - - /* - * Sets the callbacks used in addonMayLoadURI above. Not accessible over - * XPCOM - callers should use .wrappedJSObject on the service to call it - * directly. - */ - setAddonLoadURICallback(aAddonId, aCallback) { - if (aCallback) { - this.mayLoadURICallbacks.set(aAddonId, aCallback); - } else { - this.mayLoadURICallbacks.delete(aAddonId); - } - }, - - /* - * Sets the custom CSP string to be used for the add-on. Not accessible over - * XPCOM - callers should use .wrappedJSObject on the service to call it - * directly. - */ - setAddonCSP(aAddonId, aCSPString) { - if (aCSPString) { - this.cspStrings.set(aAddonId, aCSPString); - } else { - this.cspStrings.delete(aAddonId); - } - }, - - /** - * Set the callback that generates a data-URL for the background page. - */ - setBackgroundPageUrlCallback(aAddonId, aCallback) { - if (aCallback) { - this.backgroundPageUrlCallbacks.set(aAddonId, aCallback); - } else { - this.backgroundPageUrlCallbacks.delete(aAddonId); - } - }, - - /* - * Sets the callbacks used by the stream converter service to localize - * add-on resources. - */ - setAddonLocalizeCallback(aAddonId, aCallback) { - if (aCallback) { - this.localizeCallbacks.set(aAddonId, aCallback); - } else { - this.localizeCallbacks.delete(aAddonId); - } - }, - - /* - * Sets the callback used in extensionURILoadableByAnyone above. Not - * accessible over XPCOM - callers should use .wrappedJSObject on the - * service to call it directly. - */ - setExtensionURILoadCallback(aCallback) { - var old = this.extensionURILoadCallback; - this.extensionURILoadCallback = aCallback; - return old; - }, - - /* - * Sets the callback used in extensionURIToAddonId above. Not accessible over - * XPCOM - callers should use .wrappedJSObject on the service to call it - * directly. - */ - setExtensionURIToAddonIdCallback(aCallback) { - var old = this.extensionURIToAddonIdCallback; - this.extensionURIToAddonIdCallback = aCallback; - return old; - } -}; - /* * This class provides a stream filter for locale messages in CSS files served * by the moz-extension: protocol handler. * * See SubstituteChannel in netwerk/protocol/res/ExtensionProtocolHandler.cpp * for usage. */ function AddonLocalizationConverter() { - this.aps = Cc["@mozilla.org/addons/policy-service;1"].getService(Ci.nsIAddonPolicyService) - .wrappedJSObject; } AddonLocalizationConverter.prototype = { classID: Components.ID("{ded150e3-c92e-4077-a396-0dba9953e39f}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsIStreamConverter]), FROM_TYPE: "application/vnd.mozilla.webext.unlocalized", TO_TYPE: "text/css", @@ -241,71 +50,70 @@ AddonLocalizationConverter.prototype = { } if (aToType != this.TO_TYPE) { throw Components.Exception("Invalid aToType value", Cr.NS_ERROR_INVALID_ARG, Components.stack.caller.caller); } }, // aContext must be a nsIURI object for a valid moz-extension: URL. - getAddonId(aContext) { + getAddon(aContext) { // In this case, we want the add-on ID even if the URL is web accessible, // so check the root rather than the exact path. let uri = Services.io.newURI("/", null, aContext); - let id = this.aps.extensionURIToAddonId(uri); - if (id == undefined) { + let addon = WebExtensionPolicy.getByURI(uri); + if (!addon) { throw new Components.Exception("Invalid context", Cr.NS_ERROR_INVALID_ARG); } - return id; + return addon; }, - convertToStream(aAddonId, aString) { + convertToStream(aAddon, aString) { let stream = Cc["@mozilla.org/io/string-input-stream;1"] .createInstance(Ci.nsIStringInputStream); - stream.data = this.aps.localizeAddonString(aAddonId, aString); + stream.data = aAddon.localize(aString); return stream; }, convert(aStream, aFromType, aToType, aContext) { this.checkTypes(aFromType, aToType); - let addonId = this.getAddonId(aContext); + let addon = this.getAddon(aContext); let string = ( aStream.available() ? NetUtil.readInputStreamToString(aStream, aStream.available()) : "" ); - return this.convertToStream(addonId, string); + return this.convertToStream(addon, string); }, asyncConvertData(aFromType, aToType, aListener, aContext) { this.checkTypes(aFromType, aToType); - this.addonId = this.getAddonId(aContext); + this.addon = this.getAddon(aContext); this.listener = aListener; }, onStartRequest(aRequest, aContext) { this.parts = []; }, onDataAvailable(aRequest, aContext, aInputStream, aOffset, aCount) { this.parts.push(NetUtil.readInputStreamToString(aInputStream, aCount)); }, onStopRequest(aRequest, aContext, aStatusCode) { try { this.listener.onStartRequest(aRequest, null); if (Components.isSuccessCode(aStatusCode)) { let string = this.parts.join(""); - let stream = this.convertToStream(this.addonId, string); + let stream = this.convertToStream(this.addon, string); this.listener.onDataAvailable(aRequest, null, stream, 0, stream.data.length); } } catch (e) { aStatusCode = e.result || Cr.NS_ERROR_FAILURE; } this.listener.onStopRequest(aRequest, null, aStatusCode); }, }; -this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonPolicyService, - AddonLocalizationConverter]); +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AddonLocalizationConverter]);
--- a/toolkit/components/utils/utils.manifest +++ b/toolkit/components/utils/utils.manifest @@ -1,6 +1,4 @@ component {dfd07380-6083-11e4-9803-0800200c9a66} simpleServices.js contract @mozilla.org/addons/remote-tag-service;1 {dfd07380-6083-11e4-9803-0800200c9a66} -component {89560ed3-72e3-498d-a0e8-ffe50334d7c5} simpleServices.js -contract @mozilla.org/addons/policy-service;1 {89560ed3-72e3-498d-a0e8-ffe50334d7c5} component {ded150e3-c92e-4077-a396-0dba9953e39f} simpleServices.js contract @mozilla.org/streamconv;1?from=application/vnd.mozilla.webext.unlocalized&to=text/css {ded150e3-c92e-4077-a396-0dba9953e39f}
--- a/toolkit/modules/addons/MatchPattern.jsm +++ b/toolkit/modules/addons/MatchPattern.jsm @@ -106,16 +106,18 @@ SingleMatchPattern.prototype = { ); }, // Tests if this can possibly overlap with the |other| SingleMatchPattern. overlapsIgnoringPath(other) { return this.schemes.some(scheme => other.schemes.includes(scheme)) && (this.hostMatch(other) || other.hostMatch(this)); }, + + get pattern() { return this.pat; }, }; this.MatchPattern = function(pat) { this.pat = pat; if (!pat) { this.matchers = []; } else if (pat instanceof String || typeof(pat) == "string") { this.matchers = [new SingleMatchPattern(pat)]; @@ -131,16 +133,18 @@ this.MatchPattern = function(pat) { }; MatchPattern.prototype = { // |uri| should be an nsIURI. matches(uri) { return this.matchers.some(matcher => matcher.matches(uri)); }, + get patterns() { return this.matchers; }, + matchesIgnoringPath(uri, explicit = false) { if (explicit) { return this.explicitMatchers.some(matcher => matcher.matches(uri, true)); } return this.matchers.some(matcher => matcher.matches(uri, true)); }, // Checks that this match pattern grants access to read the given
--- a/toolkit/modules/addons/WebRequest.jsm +++ b/toolkit/modules/addons/WebRequest.jsm @@ -304,17 +304,17 @@ var ContentPolicyManager = { addListener(callback, opts) { // Clone opts, since we're going to modify them for IPC. opts = Object.assign({}, opts); let id = this.nextId++; opts.id = id; if (opts.filter.urls) { opts.filter = Object.assign({}, opts.filter); - opts.filter.urls = opts.filter.urls.serialize(); + opts.filter.urls = opts.filter.urls.patterns.map(url => url.pattern); } Services.ppmm.broadcastAsyncMessage("WebRequest:AddContentPolicy", opts); this.policyData.set(id, opts); this.policies.set(id, callback); this.idMap.set(callback, id); },
--- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -96,17 +96,22 @@ XPCOMUtils.defineLazyGetter(this, "CertU let certUtils = {}; Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils); return certUtils; }); XPCOMUtils.defineLazyPreferenceGetter(this, "WEBEXT_PERMISSION_PROMPTS", PREF_WEBEXT_PERM_PROMPTS, false); -Services.ppmm.loadProcessScript("chrome://extensions/content/extension-process-script.js", true); +// Initialize the WebExtension process script service as early as possible, +// since it needs to be able to track things like new frameLoader globals that +// are created before other framework code has been initialized. +Services.ppmm.loadProcessScript( + "data:,Components.classes['@mozilla.org/webextensions/extension-process-script;1'].getService()", + true); const INTEGER = /^[1-9]\d*$/; this.EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ]; const CATEGORY_PROVIDER_MODULE = "addon-provider-module"; // A list of providers to load by default
--- a/toolkit/mozapps/extensions/AddonManagerWebAPI.h +++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h @@ -10,17 +10,16 @@ #include "nsPIDOMWindow.h" namespace mozilla { class AddonManagerWebAPI { public: static bool IsAPIEnabled(JSContext* cx, JSObject* obj); -private: static bool IsValidSite(nsIURI* uri); }; namespace dom { class AddonManagerPermissions { public: static bool IsHostPermitted(const GlobalObject&, const nsAString& host);
--- a/toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js +++ b/toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js @@ -1,17 +1,17 @@ /* 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"; -/* exported startup, shutdown, install, uninstall */ +/* exported startup, shutdown, install, uninstall, ExtensionAPIs */ -Components.utils.import("resource://gre/modules/ExtensionManagement.jsm"); +Components.utils.import("resource://gre/modules/ExtensionAPI.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); var namespace; var resource; var resProto; function install(data, reason) { } @@ -20,22 +20,22 @@ function startup(data, reason) { namespace = data.id.replace(/@.*/, ""); resource = `extension-${namespace}-api`; resProto = Services.io.getProtocolHandler("resource") .QueryInterface(Components.interfaces.nsIResProtocolHandler); resProto.setSubstitution(resource, data.resourceURI); - ExtensionManagement.registerAPI( + ExtensionAPIs.register( namespace, `resource://${resource}/schema.json`, `resource://${resource}/api.js`); } function shutdown(data, reason) { resProto.setSubstitution(resource, null); - ExtensionManagement.unregisterAPI(namespace); + ExtensionAPIs.unregister(namespace); } function uninstall(data, reason) { }
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm +++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm @@ -31,16 +31,18 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyGetter(this, "CertUtils", () => Cu.import("resource://gre/modules/CertUtils.jsm", {})); XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", "resource://gre/modules/ChromeManifestParser.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ExtensionData", "resource://gre/modules/Extension.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Locale", + "resource://gre/modules/Locale.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "IconDetails", () => { return Cu.import("resource://gre/modules/ExtensionUtils.jsm", {}).ExtensionUtils.IconDetails; }); XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -6,33 +6,33 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; const Cu = Components.utils; this.EXPORTED_SYMBOLS = ["XPIProvider", "XPIInternal"]; +/* globals WebExtensionPolicy */ + Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/Preferences.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository", "resource://gre/modules/addons/AddonRepository.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AddonSettings", "resource://gre/modules/addons/AddonSettings.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser", "resource://gre/modules/ChromeManifestParser.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement", - "resource://gre/modules/ExtensionManagement.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Locale", "resource://gre/modules/Locale.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); @@ -5213,20 +5213,21 @@ AddonWrapper.prototype = { let addon = addonFor(this); if (addon.optionsURL) { if (this.isWebExtension || this.hasEmbeddedWebExtension) { // The internal object's optionsURL property comes from the addons // DB and should be a relative URL. However, extensions with // options pages installed before bug 1293721 was fixed got absolute // URLs in the addons db. This code handles both cases. - let base = ExtensionManagement.getURLForExtension(addon.id); - if (!base) { + let policy = WebExtensionPolicy.getByID(addon.id); + if (!policy) { return null; } + let base = policy.getURL(); return new URL(addon.optionsURL, base).href; } return addon.optionsURL; } if (this.hasResource("options.xul")) return this.getResourceURI("options.xul").spec;
--- a/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js @@ -14,17 +14,17 @@ function backgroundGetSelf() { browser.test.notifyFail(`getSelf rejected with error: ${error}`); }); } /* eslint-enable no-undef */ add_task(async function test_management_get_self_complete() { const id = "get_self_test_complete@tests.mozilla.com"; const permissions = ["management", "cookies"]; - const hostPermissions = ["*://example.org/", "https://foo.example.org/"]; + const hostPermissions = ["*://example.org/*", "https://foo.example.org/*"]; let manifest = { applications: { gecko: { id, update_url: "https://updates.mozilla.com/", }, },
--- a/tools/profiler/core/platform.cpp +++ b/tools/profiler/core/platform.cpp @@ -722,21 +722,18 @@ AddDynamicCodeLocationTag(ProfileBuffer* } } static void AddPseudoEntry(PSLockRef aLock, ProfileBuffer* aBuffer, volatile js::ProfileEntry& entry, NotNull<RacyThreadInfo*> aRacyInfo) { - // Pseudo-frames with the BEGIN_PSEUDO_JS flag are just annotations and - // should not be recorded in the profile. - if (entry.hasFlag(js::ProfileEntry::BEGIN_PSEUDO_JS)) { - return; - } + MOZ_ASSERT(entry.kind() == js::ProfileEntry::Kind::CPP_NORMAL || + entry.kind() == js::ProfileEntry::Kind::JS_NORMAL); int lineno = -1; // First entry has kind CodeLocation. const char* label = entry.label(); bool includeDynamicString = !ActivePS::FeaturePrivacy(aLock); const char* dynamicString = includeDynamicString ? entry.dynamicString() : nullptr; @@ -792,22 +789,17 @@ AddPseudoEntry(PSLockRef aLock, ProfileB lineno = entry.line(); } } if (lineno != -1) { aBuffer->addTag(ProfileBufferEntry::LineNumber(lineno)); } - uint32_t category = entry.category(); - MOZ_ASSERT(!(category & js::ProfileEntry::IS_CPP_ENTRY)); - - if (category) { - aBuffer->addTag(ProfileBufferEntry::Category((int)category)); - } + aBuffer->addTag(ProfileBufferEntry::Category(uint32_t(entry.category()))); } struct NativeStack { void** pc_array; void** sp_array; size_t size; size_t count; @@ -911,23 +903,22 @@ MergeStacksIntoProfile(PSLockRef aLock, if (pseudoIndex != pseudoCount) { volatile js::ProfileEntry& pseudoEntry = pseudoEntries[pseudoIndex]; if (pseudoEntry.isCpp()) { lastPseudoCppStackAddr = (uint8_t*) pseudoEntry.stackAddress(); } - // Skip any pseudo-stack JS frames which are marked isOSR. Pseudostack - // frames are marked isOSR when the JS interpreter enters a jit frame on - // a loop edge (via on-stack-replacement, or OSR). To avoid both the - // pseudoframe and jit frame being recorded (and showing up twice), the - // interpreter marks the interpreter pseudostack entry with the OSR flag - // to ensure that it doesn't get counted. - if (pseudoEntry.isJs() && pseudoEntry.isOSR()) { + // Skip any JS_OSR frames. Such frames are used when the JS interpreter + // enters a jit frame on a loop edge (via on-stack-replacement, or OSR). + // To avoid both the pseudoframe and jit frame being recorded (and + // showing up twice), the interpreter marks the interpreter pseudostack + // frame as JS_OSR to ensure that it doesn't get counted. + if (pseudoEntry.kind() == js::ProfileEntry::Kind::JS_OSR) { pseudoIndex++; continue; } MOZ_ASSERT(lastPseudoCppStackAddr); pseudoStackAddr = lastPseudoCppStackAddr; } @@ -957,17 +948,22 @@ MergeStacksIntoProfile(PSLockRef aLock, jsStackAddr != nativeStackAddr); MOZ_ASSERT_IF(nativeStackAddr, nativeStackAddr != pseudoStackAddr && nativeStackAddr != jsStackAddr); // Check to see if pseudoStack frame is top-most. if (pseudoStackAddr > jsStackAddr && pseudoStackAddr > nativeStackAddr) { MOZ_ASSERT(pseudoIndex < pseudoCount); volatile js::ProfileEntry& pseudoEntry = pseudoEntries[pseudoIndex]; - AddPseudoEntry(aLock, aBuffer, pseudoEntry, racyInfo); + + // Pseudo-frames with the CPP_MARKER_FOR_JS kind are just annotations and + // should not be recorded in the profile. + if (pseudoEntry.kind() != js::ProfileEntry::Kind::CPP_MARKER_FOR_JS) { + AddPseudoEntry(aLock, aBuffer, pseudoEntry, racyInfo); + } pseudoIndex++; continue; } // Check to see if JS jit stack frame is top-most if (jsStackAddr > nativeStackAddr) { MOZ_ASSERT(jsIndex >= 0); const JS::ProfilingFrameIterator::Frame& jsFrame = jsFrames[jsIndex];
--- a/tools/profiler/public/GeckoProfiler.h +++ b/tools/profiler/public/GeckoProfiler.h @@ -424,17 +424,18 @@ profiler_call_enter(const char* aLabel, const char* aDynamicString = nullptr) { // This function runs both on and off the main thread. PseudoStack* pseudoStack = sPseudoStack.get(); if (!pseudoStack) { return pseudoStack; } - pseudoStack->pushCppFrame(aLabel, aDynamicString, aFrameAddress, aLine, aCategory); + pseudoStack->pushCppFrame(aLabel, aDynamicString, aFrameAddress, aLine, + js::ProfileEntry::Kind::CPP_NORMAL, aCategory); // The handle is meant to support future changes but for now it is simply // used to avoid having to call TLSInfo::RacyInfo() in profiler_call_exit(). return pseudoStack; } inline void profiler_call_exit(void* aHandle)