diff --git a/browser/components/extensions/parent/ext-tabs.js b/browser/components/extensions/parent/ext-tabs.js index af2b24102e4e..c88d35dc516e 100644 --- a/browser/components/extensions/parent/ext-tabs.js +++ b/browser/components/extensions/parent/ext-tabs.js @@ -214,10 +214,10 @@ class TabsUpdateFilterEventManager extends EventManager { filter.properties = allProperties; } - function sanitize(extension, changeInfo) { + function sanitize(tab, changeInfo) { let result = {}; let nonempty = false; - let hasTabs = extension.hasPermission("tabs"); + const hasTabs = tab.hasTabPermission; for (let prop in changeInfo) { if (hasTabs || !restricted.has(prop)) { nonempty = true; @@ -264,7 +264,7 @@ class TabsUpdateFilterEventManager extends EventManager { return; } - let changeInfo = sanitize(extension, changed); + let changeInfo = sanitize(tab, changed); if (changeInfo) { tabTracker.maybeWaitForTabOpen(nativeTab).then(() => { if (!nativeTab.parentNode) { @@ -418,22 +418,6 @@ class TabsUpdateFilterEventManager extends EventManager { register, }); } - - addListener(callback, filter) { - let { extension } = this.context; - if ( - filter && - filter.urls && - !extension.hasPermission("tabs") && - !extension.hasPermission("activeTab") - ) { - Cu.reportError( - 'Url filtering in tabs.onUpdated requires "tabs" or "activeTab" permission.' - ); - return false; - } - return super.addListener(callback, filter); - } } function TabEventManager({ context, name, event, listener }) { @@ -946,14 +930,6 @@ this.tabs = class extends ExtensionAPI { }, async query(queryInfo) { - if (!extension.hasPermission("tabs")) { - if (queryInfo.url !== null || queryInfo.title !== null) { - return Promise.reject({ - message: - 'The "tabs" permission is required to use the query API with the "url" or "title" parameters', - }); - } - } return Array.from(tabManager.query(queryInfo, context), tab => tab.convert() ); diff --git a/browser/components/extensions/test/browser/browser_ext_tabs_query.js b/browser/components/extensions/test/browser/browser_ext_tabs_query.js index 45ef8197d316..2aa860d8c2ce 100644 --- a/browser/components/extensions/test/browser/browser_ext_tabs_query.js +++ b/browser/components/extensions/test/browser/browser_ext_tabs_query.js @@ -354,36 +354,6 @@ add_task(async function testQueryPermissions() { await extension.unload(); }); -add_task(async function testQueryWithoutURLOrTitlePermissions() { - let extension = ExtensionTestUtils.loadExtension({ - manifest: { - permissions: [], - }, - - async background() { - await browser.test.assertRejects( - browser.tabs.query({ url: "http://www.bbc.com/" }), - 'The "tabs" permission is required to use the query API with the "url" or "title" parameters', - "Expected tabs.query with 'url' or 'title' to fail with permissions error message" - ); - - await browser.test.assertRejects( - browser.tabs.query({ title: "Foo" }), - 'The "tabs" permission is required to use the query API with the "url" or "title" parameters', - "Expected tabs.query with 'url' or 'title' to fail with permissions error message" - ); - - browser.test.notifyPass("testQueryWithoutURLOrTitlePermissions"); - }, - }); - - await extension.startup(); - - await extension.awaitFinish("testQueryWithoutURLOrTitlePermissions"); - - await extension.unload(); -}); - add_task(async function testInvalidUrl() { let extension = ExtensionTestUtils.loadExtension({ manifest: { diff --git a/mobile/android/components/extensions/ext-tabs.js b/mobile/android/components/extensions/ext-tabs.js index ee5abe40171a..e71a35cd1340 100644 --- a/mobile/android/components/extensions/ext-tabs.js +++ b/mobile/android/components/extensions/ext-tabs.js @@ -230,14 +230,12 @@ this.tabs = class extends ExtensionAPI { register: fire => { const restricted = ["url", "favIconUrl", "title"]; - function sanitize(extension, changeInfo) { + function sanitize(tab, changeInfo) { const result = {}; let nonempty = false; + const hasTabs = tab.hasTabPermission; for (const prop in changeInfo) { - if ( - extension.hasPermission("tabs") || - !restricted.includes(prop) - ) { + if (hasTabs || !restricted.includes(prop)) { nonempty = true; result[prop] = changeInfo[prop]; } @@ -246,7 +244,7 @@ this.tabs = class extends ExtensionAPI { } const fireForTab = (tab, changed) => { - const [needed, changeInfo] = sanitize(extension, changed); + const [needed, changeInfo] = sanitize(tab, changed); if (needed) { fire.async(tab.id, changeInfo, tab.convert()); } @@ -256,7 +254,7 @@ this.tabs = class extends ExtensionAPI { const needed = []; let nativeTab; switch (event.type) { - case "DOMTitleChanged": { + case "pagetitlechanged": { const window = getBrowserWindow(event.target.ownerGlobal); nativeTab = window.tab; @@ -299,10 +297,10 @@ this.tabs = class extends ExtensionAPI { }; windowTracker.addListener("status", statusListener); - windowTracker.addListener("DOMTitleChanged", listener); + windowTracker.addListener("pagetitlechanged", listener); return () => { windowTracker.removeListener("status", statusListener); - windowTracker.removeListener("DOMTitleChanged", listener); + windowTracker.removeListener("pagetitlechanged", listener); }; }, }).api(), @@ -455,14 +453,6 @@ this.tabs = class extends ExtensionAPI { }, async query(queryInfo) { - if (!extension.hasPermission("tabs")) { - if (queryInfo.url !== null || queryInfo.title !== null) { - return Promise.reject({ - message: - 'The "tabs" permission is required to use the query API with the "url" or "title" parameters', - }); - } - } return Array.from(tabManager.query(queryInfo, context), tab => tab.convert() ); diff --git a/toolkit/components/extensions/ExtensionTestCommon.jsm b/toolkit/components/extensions/ExtensionTestCommon.jsm index 53ced84ed141..51198154a94a 100644 --- a/toolkit/components/extensions/ExtensionTestCommon.jsm +++ b/toolkit/components/extensions/ExtensionTestCommon.jsm @@ -134,6 +134,10 @@ class MockExtension { return this._extension.testMessage(...args); } + get tabManager() { + return this._extension.tabManager; + } + on(...args) { this._extensionPromise.then(extension => { extension.on(...args); diff --git a/toolkit/components/extensions/parent/ext-tabs-base.js b/toolkit/components/extensions/parent/ext-tabs-base.js index 5aaab7868169..d6c59b81dc01 100644 --- a/toolkit/components/extensions/parent/ext-tabs-base.js +++ b/toolkit/components/extensions/parent/ext-tabs-base.js @@ -169,7 +169,11 @@ class TabBase { * @readonly */ get hasTabPermission() { - return this.extension.hasPermission("tabs") || this.hasActiveTabPermission; + return ( + this.extension.hasPermission("tabs") || + this.hasActiveTabPermission || + this.matchesHostPermission + ); } /** @@ -190,6 +194,15 @@ class TabBase { ); } + /** + * @property {boolean} matchesHostPermission + * Returns true if the extensions host permissions match the current tab url. + * @readonly + */ + get matchesHostPermission() { + return this.extension.allowedOrigins.matches(this._url); + } + /** * @property {boolean} incognito * Returns true if this is a private browsing tab, false otherwise. @@ -612,12 +625,17 @@ class TabBase { return false; } } - - if (queryInfo.url && !queryInfo.url.matches(this.uri)) { - return false; - } - if (queryInfo.title && !queryInfo.title.matches(this.title)) { - return false; + if (queryInfo.url || queryInfo.title) { + if (!this.hasTabPermission) { + return false; + } + // Using _url and _title instead of url/title to avoid repeated permission checks. + if (queryInfo.url && !queryInfo.url.matches(this._url)) { + return false; + } + if (queryInfo.title && !queryInfo.title.matches(this._title)) { + return false; + } } return true; diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html new file mode 100644 index 000000000000..63f503ad3c66 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page1.html @@ -0,0 +1,10 @@ + + + + + The Title + + + + + diff --git a/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html new file mode 100644 index 000000000000..87ac7a2f64f4 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/file_tabs_permission_page2.html @@ -0,0 +1,11 @@ + + + + + Another Title + + + + + + diff --git a/toolkit/components/extensions/test/mochitest/mochitest-common.ini b/toolkit/components/extensions/test/mochitest/mochitest-common.ini index 12fb63de88db..2e32a951e2fc 100644 --- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini +++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini @@ -65,6 +65,8 @@ support-files = !/toolkit/components/passwordmgr/test/authenticate.sjs file_redirect_data_uri.html file_redirect_cors_bypass.html + file_tabs_permission_page1.html + file_tabs_permission_page2.html prefs = security.mixed_content.upgrade_display_content=false browser.chrome.guess_favicon=true @@ -152,7 +154,7 @@ skip-if = xorigin # JavaScript Error: "SecurityError: Permission denied to acces scheme=https [test_ext_storage_smoke_test.html] [test_ext_streamfilter_multiple.html] -skip-if = +skip-if = !debug # Bug 1628642 os == 'linux' # Bug 1628642 [test_ext_streamfilter_processswitch.html] @@ -160,6 +162,7 @@ skip-if = skip-if = os == 'android' || verify # bug 1489771 [test_ext_tabs_captureTab.html] [test_ext_tabs_query_popup.html] +[test_ext_tabs_permissions.html] [test_ext_tabs_sendMessage.html] [test_ext_test.html] [test_ext_unlimitedStorage.html] diff --git a/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html new file mode 100644 index 000000000000..183a42cd9fe1 --- /dev/null +++ b/toolkit/components/extensions/test/mochitest/test_ext_tabs_permissions.html @@ -0,0 +1,780 @@ + + + + Tabs permissions test + + + + + + + + + + + \ No newline at end of file