From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- mobile/android/components/extensions/.eslintrc.js | 9 + .../extensions/ExtensionBrowsingData.sys.mjs | 59 + .../android/components/extensions/ext-android.js | 630 +++++++++ .../android/components/extensions/ext-android.json | 31 + .../components/extensions/ext-browserAction.js | 191 +++ .../android/components/extensions/ext-c-android.js | 13 + mobile/android/components/extensions/ext-c-tabs.js | 23 + .../android/components/extensions/ext-downloads.js | 310 +++++ .../components/extensions/ext-pageAction.js | 153 +++ mobile/android/components/extensions/ext-tabs.js | 584 ++++++++ .../extensions/extensions-mobile.manifest | 5 + mobile/android/components/extensions/jar.mn | 14 + mobile/android/components/extensions/moz.build | 21 + .../extensions/schemas/gecko_view_addons.json | 16 + .../android/components/extensions/schemas/jar.mn | 7 + .../components/extensions/schemas/moz.build | 7 + .../components/extensions/schemas/tabs.json | 1395 ++++++++++++++++++++ .../extensions/test/mochitest/.eslintrc.js | 6 + .../extensions/test/mochitest/chrome.ini | 7 + .../extensions/test/mochitest/context.html | 24 + .../mochitest/context_tabs_onUpdated_iframe.html | 22 + .../mochitest/context_tabs_onUpdated_page.html | 21 + .../test/mochitest/file_bypass_cache.sjs | 13 + .../extensions/test/mochitest/file_dummy.html | 10 + .../test/mochitest/file_iframe_document.html | 11 + .../test/mochitest/file_slowed_document.sjs | 49 + .../extensions/test/mochitest/mochitest.ini | 41 + .../test/mochitest/test_ext_all_apis.html | 50 + .../mochitest/test_ext_downloads_event_page.html | 102 ++ .../test/mochitest/test_ext_options_ui.html | 498 +++++++ .../mochitest/test_ext_tab_runtimeConnect.html | 89 ++ .../test/mochitest/test_ext_tabs_create.html | 153 +++ .../test/mochitest/test_ext_tabs_events.html | 302 +++++ .../mochitest/test_ext_tabs_executeScript.html | 252 ++++ .../mochitest/test_ext_tabs_executeScript_bad.html | 151 +++ .../test_ext_tabs_executeScript_no_create.html | 83 ++ .../test_ext_tabs_executeScript_runAt.html | 128 ++ .../test/mochitest/test_ext_tabs_get.html | 36 + .../test/mochitest/test_ext_tabs_getCurrent.html | 70 + .../mochitest/test_ext_tabs_goBack_goForward.html | 134 ++ .../test/mochitest/test_ext_tabs_insertCSS.html | 124 ++ .../test/mochitest/test_ext_tabs_lastAccessed.html | 64 + .../test/mochitest/test_ext_tabs_onUpdated.html | 187 +++ .../test/mochitest/test_ext_tabs_query.html | 46 + .../test/mochitest/test_ext_tabs_reload.html | 66 + .../test_ext_tabs_reload_bypass_cache.html | 87 ++ .../test/mochitest/test_ext_tabs_sendMessage.html | 277 ++++ .../test/mochitest/test_ext_tabs_update_url.html | 125 ++ .../test_ext_webNavigation_onCommitted.html | 50 + .../extensions/test/xpcshell/.eslintrc.js | 6 + .../components/extensions/test/xpcshell/head.js | 24 + .../test_ext_native_messaging_permissions.js | 167 +++ .../extensions/test/xpcshell/xpcshell.ini | 7 + 53 files changed, 6950 insertions(+) create mode 100644 mobile/android/components/extensions/.eslintrc.js create mode 100644 mobile/android/components/extensions/ExtensionBrowsingData.sys.mjs create mode 100644 mobile/android/components/extensions/ext-android.js create mode 100644 mobile/android/components/extensions/ext-android.json create mode 100644 mobile/android/components/extensions/ext-browserAction.js create mode 100644 mobile/android/components/extensions/ext-c-android.js create mode 100644 mobile/android/components/extensions/ext-c-tabs.js create mode 100644 mobile/android/components/extensions/ext-downloads.js create mode 100644 mobile/android/components/extensions/ext-pageAction.js create mode 100644 mobile/android/components/extensions/ext-tabs.js create mode 100644 mobile/android/components/extensions/extensions-mobile.manifest create mode 100644 mobile/android/components/extensions/jar.mn create mode 100644 mobile/android/components/extensions/moz.build create mode 100644 mobile/android/components/extensions/schemas/gecko_view_addons.json create mode 100644 mobile/android/components/extensions/schemas/jar.mn create mode 100644 mobile/android/components/extensions/schemas/moz.build create mode 100644 mobile/android/components/extensions/schemas/tabs.json create mode 100644 mobile/android/components/extensions/test/mochitest/.eslintrc.js create mode 100644 mobile/android/components/extensions/test/mochitest/chrome.ini create mode 100644 mobile/android/components/extensions/test/mochitest/context.html create mode 100644 mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html create mode 100644 mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html create mode 100644 mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs create mode 100644 mobile/android/components/extensions/test/mochitest/file_dummy.html create mode 100644 mobile/android/components/extensions/test/mochitest/file_iframe_document.html create mode 100644 mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs create mode 100644 mobile/android/components/extensions/test/mochitest/mochitest.ini create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html create mode 100644 mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html create mode 100644 mobile/android/components/extensions/test/xpcshell/.eslintrc.js create mode 100644 mobile/android/components/extensions/test/xpcshell/head.js create mode 100644 mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js create mode 100644 mobile/android/components/extensions/test/xpcshell/xpcshell.ini (limited to 'mobile/android/components/extensions') diff --git a/mobile/android/components/extensions/.eslintrc.js b/mobile/android/components/extensions/.eslintrc.js new file mode 100644 index 0000000000..7726338490 --- /dev/null +++ b/mobile/android/components/extensions/.eslintrc.js @@ -0,0 +1,9 @@ +/* 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"; + +module.exports = { + extends: "../../../../toolkit/components/extensions/.eslintrc.js", +}; diff --git a/mobile/android/components/extensions/ExtensionBrowsingData.sys.mjs b/mobile/android/components/extensions/ExtensionBrowsingData.sys.mjs new file mode 100644 index 0000000000..fb4155f897 --- /dev/null +++ b/mobile/android/components/extensions/ExtensionBrowsingData.sys.mjs @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", +}); + +const { ExtensionError } = ExtensionUtils; + +export class BrowsingDataDelegate { + constructor(extension) { + this.extension = extension; + } + + async sendRequestForResult(type, data) { + try { + const result = await lazy.EventDispatcher.instance.sendRequestForResult({ + type, + extensionId: this.extension.id, + ...data, + }); + return result; + } catch (errorMessage) { + throw new ExtensionError(errorMessage); + } + } + + async settings() { + return this.sendRequestForResult("GeckoView:BrowsingData:GetSettings"); + } + + async sendClear(dataType, options) { + const { since } = options; + return this.sendRequestForResult("GeckoView:BrowsingData:Clear", { + dataType, + since, + }); + } + + // This method returns undefined for all data types that are _not_ handled by + // this delegate. + handleRemoval(dataType, options) { + switch (dataType) { + case "downloads": + case "formData": + case "history": + case "passwords": + return this.sendClear(dataType, options); + + default: + return undefined; + } + } +} diff --git a/mobile/android/components/extensions/ext-android.js b/mobile/android/components/extensions/ext-android.js new file mode 100644 index 0000000000..a417811c8c --- /dev/null +++ b/mobile/android/components/extensions/ext-android.js @@ -0,0 +1,630 @@ +/* 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"; + +/** + * NOTE: If you change the globals in this file, you must check if the globals + * list in mobile/android/.eslintrc.js also needs updating. + */ + +ChromeUtils.defineESModuleGetters(this, { + GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", +}); + +var { EventDispatcher } = ChromeUtils.importESModule( + "resource://gre/modules/Messaging.sys.mjs" +); + +var { ExtensionCommon } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionCommon.sys.mjs" +); +var { ExtensionUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionUtils.sys.mjs" +); + +var { DefaultWeakMap, ExtensionError } = ExtensionUtils; + +var { defineLazyGetter } = ExtensionCommon; + +const BrowserStatusFilter = Components.Constructor( + "@mozilla.org/appshell/component/browser-status-filter;1", + "nsIWebProgress", + "addProgressListener" +); + +const WINDOW_TYPE = "navigator:geckoview"; + +// We need let to break cyclic dependency +/* eslint-disable-next-line prefer-const */ +let windowTracker; + +/** + * A nsIWebProgressListener for a specific XUL browser, which delegates the + * events that it receives to a tab progress listener, and prepends the browser + * to their arguments list. + * + * @param {XULElement} browser + * A XUL browser element. + * @param {object} listener + * A tab progress listener object. + * @param {integer} flags + * The web progress notification flags with which to filter events. + */ +class BrowserProgressListener { + constructor(browser, listener, flags) { + this.listener = listener; + this.browser = browser; + this.filter = new BrowserStatusFilter(this, flags); + this.browser.addProgressListener(this.filter, flags); + } + + /** + * Destroy the listener, and perform any necessary cleanup. + */ + destroy() { + this.browser.removeProgressListener(this.filter); + this.filter.removeProgressListener(this); + } + + /** + * Calls the appropriate listener in the wrapped tab progress listener, with + * the wrapped XUL browser object as its first argument, and the additional + * arguments in `args`. + * + * @param {string} method + * The name of the nsIWebProgressListener method which is being + * delegated. + * @param {*} args + * The arguments to pass to the delegated listener. + * @private + */ + delegate(method, ...args) { + if (this.listener[method]) { + this.listener[method](this.browser, ...args); + } + } + + onLocationChange(webProgress, request, locationURI, flags) { + const window = this.browser.ownerGlobal; + // GeckoView windows can become popups at any moment, so we need to check + // here + if (!windowTracker.isBrowserWindow(window)) { + return; + } + + this.delegate("onLocationChange", webProgress, request, locationURI, flags); + } + onStateChange(webProgress, request, stateFlags, status) { + this.delegate("onStateChange", webProgress, request, stateFlags, status); + } +} + +const PROGRESS_LISTENER_FLAGS = + Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION; + +class ProgressListenerWrapper { + constructor(window, listener) { + this.listener = new BrowserProgressListener( + window.browser, + listener, + PROGRESS_LISTENER_FLAGS + ); + } + + destroy() { + this.listener.destroy(); + } +} + +class WindowTracker extends WindowTrackerBase { + constructor(...args) { + super(...args); + + this.progressListeners = new DefaultWeakMap(() => new WeakMap()); + } + + getCurrentWindow(context) { + // In GeckoView the popup is on a separate window so getCurrentWindow for + // the popup should return whatever is the topWindow. + // TODO: Bug 1651506 use context?.viewType === "popup" instead + if (context?.currentWindow?.moduleManager.settings.isPopup) { + return this.topWindow; + } + return super.getCurrentWindow(context); + } + + get topWindow() { + return mobileWindowTracker.topWindow; + } + + get topNonPBWindow() { + return mobileWindowTracker.topNonPBWindow; + } + + isBrowserWindow(window) { + const { documentElement } = window.document; + return documentElement.getAttribute("windowtype") === WINDOW_TYPE; + } + + addProgressListener(window, listener) { + const listeners = this.progressListeners.get(window); + if (!listeners.has(listener)) { + const wrapper = new ProgressListenerWrapper(window, listener); + listeners.set(listener, wrapper); + } + } + + removeProgressListener(window, listener) { + const listeners = this.progressListeners.get(window); + const wrapper = listeners.get(listener); + if (wrapper) { + wrapper.destroy(); + listeners.delete(listener); + } + } +} + +/** + * Helper to create an event manager which listens for an event in the Android + * global EventDispatcher, and calls the given listener function whenever the + * event is received. That listener function receives a `fire` object, + * which it can use to dispatch events to the extension, and an object + * detailing the EventDispatcher event that was received. + * + * @param {BaseContext} context + * The extension context which the event manager belongs to. + * @param {string} name + * The API name of the event manager, e.g.,"runtime.onMessage". + * @param {string} event + * The name of the EventDispatcher event to listen for. + * @param {Function} listener + * The listener function to call when an EventDispatcher event is + * recieved. + * + * @returns {object} An injectable api for the new event. + */ +global.makeGlobalEvent = function makeGlobalEvent( + context, + name, + event, + listener +) { + return new EventManager({ + context, + name, + register: fire => { + const listener2 = { + onEvent(event, data, callback) { + listener(fire, data); + }, + }; + + EventDispatcher.instance.registerListener(listener2, [event]); + return () => { + EventDispatcher.instance.unregisterListener(listener2, [event]); + }; + }, + }).api(); +}; + +class TabTracker extends TabTrackerBase { + init() { + if (this.initialized) { + return; + } + this.initialized = true; + + windowTracker.addOpenListener(window => { + const nativeTab = window.tab; + this.emit("tab-created", { nativeTab }); + }); + + windowTracker.addCloseListener(window => { + const { tab: nativeTab, browser } = window; + const { windowId, tabId } = this.getBrowserData(browser); + this.emit("tab-removed", { + nativeTab, + tabId, + windowId, + // In GeckoView, it is not meaningful to speak of "window closed", because a tab is a window. + // Until we have a meaningful way to group tabs (and close multiple tabs at once), + // let's use isWindowClosing: false + isWindowClosing: false, + }); + }); + } + + getId(nativeTab) { + return nativeTab.id; + } + + getTab(id, default_ = undefined) { + const windowId = GeckoViewTabBridge.tabIdToWindowId(id); + const window = windowTracker.getWindow(windowId, null, false); + + if (window) { + const { tab } = window; + if (tab) { + return tab; + } + } + + if (default_ !== undefined) { + return default_; + } + throw new ExtensionError(`Invalid tab ID: ${id}`); + } + + getBrowserData(browser) { + const window = browser.ownerGlobal; + const { tab } = window; + if (!tab) { + return { + tabId: -1, + windowId: -1, + }; + } + + const windowId = windowTracker.getId(window); + + if (!windowTracker.isBrowserWindow(window)) { + return { + windowId, + tabId: -1, + }; + } + + return { + windowId, + tabId: this.getId(tab), + }; + } + + get activeTab() { + const window = windowTracker.topWindow; + if (window) { + return window.tab; + } + return null; + } +} + +windowTracker = new WindowTracker(); +const tabTracker = new TabTracker(); + +Object.assign(global, { tabTracker, windowTracker }); + +class Tab extends TabBase { + get _favIconUrl() { + return undefined; + } + + get attention() { + return false; + } + + get audible() { + return this.nativeTab.playingAudio; + } + + get browser() { + return this.nativeTab.browser; + } + + get discarded() { + return this.browser.getAttribute("pending") === "true"; + } + + get cookieStoreId() { + return getCookieStoreIdForTab(this, this.nativeTab); + } + + get height() { + return this.browser.clientHeight; + } + + get incognito() { + return PrivateBrowsingUtils.isBrowserPrivate(this.browser); + } + + get index() { + return 0; + } + + get mutedInfo() { + return { muted: false }; + } + + get lastAccessed() { + return this.nativeTab.lastTouchedAt; + } + + get pinned() { + return false; + } + + get active() { + return this.nativeTab.getActive(); + } + + get highlighted() { + return this.active; + } + + get status() { + if (this.browser.webProgress.isLoadingDocument) { + return "loading"; + } + return "complete"; + } + + get successorTabId() { + return -1; + } + + get width() { + return this.browser.clientWidth; + } + + get window() { + return this.browser.ownerGlobal; + } + + get windowId() { + return windowTracker.getId(this.window); + } + + // TODO: Just return false for these until properly implemented on Android. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1402924 + get isArticle() { + return false; + } + + get isInReaderMode() { + return false; + } + + get hidden() { + return false; + } + + get sharingState() { + return { + screen: undefined, + microphone: false, + camera: false, + }; + } +} + +// Manages tab-specific context data and dispatches tab select and close events. +class TabContext extends EventEmitter { + constructor(getDefaultPrototype) { + super(); + + windowTracker.addListener("progress", this); + + this.getDefaultPrototype = getDefaultPrototype; + this.tabData = new Map(); + } + + onLocationChange(browser, webProgress, request, locationURI, flags) { + if (!webProgress.isTopLevel) { + // Only pageAction and browserAction are consuming the "location-change" event + // to update their per-tab status, and they should only do so in response of + // location changes related to the top level frame (See Bug 1493470 for a rationale). + return; + } + const { tab } = browser.ownerGlobal; + // fromBrowse will be false in case of e.g. a hash change or history.pushState + const fromBrowse = !( + flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT + ); + this.emit( + "location-change", + { + id: tab.id, + linkedBrowser: browser, + // TODO: we don't support selected so we just alway say we are + selected: true, + }, + fromBrowse + ); + } + + get(tabId) { + if (!this.tabData.has(tabId)) { + const data = Object.create(this.getDefaultPrototype(tabId)); + this.tabData.set(tabId, data); + } + + return this.tabData.get(tabId); + } + + clear(tabId) { + this.tabData.delete(tabId); + } + + shutdown() { + windowTracker.removeListener("progress", this); + } +} + +class Window extends WindowBase { + get focused() { + return this.window.document.hasFocus(); + } + + isCurrentFor(context) { + // In GeckoView the popup is on a separate window so the current window for + // the popup is whatever is the topWindow. + // TODO: Bug 1651506 use context?.viewType === "popup" instead + if (context?.currentWindow?.moduleManager.settings.isPopup) { + return mobileWindowTracker.topWindow == this.window; + } + return super.isCurrentFor(context); + } + + get top() { + return this.window.screenY; + } + + get left() { + return this.window.screenX; + } + + get width() { + return this.window.outerWidth; + } + + get height() { + return this.window.outerHeight; + } + + get incognito() { + return PrivateBrowsingUtils.isWindowPrivate(this.window); + } + + get alwaysOnTop() { + return false; + } + + get isLastFocused() { + return this.window === windowTracker.topWindow; + } + + get state() { + return "fullscreen"; + } + + *getTabs() { + yield this.activeTab; + } + + *getHighlightedTabs() { + yield this.activeTab; + } + + get activeTab() { + const { tabManager } = this.extension; + return tabManager.getWrapper(this.window.tab); + } + + getTabAtIndex(index) { + if (index == 0) { + return this.activeTab; + } + } +} + +Object.assign(global, { Tab, TabContext, Window }); + +class TabManager extends TabManagerBase { + get(tabId, default_ = undefined) { + const nativeTab = tabTracker.getTab(tabId, default_); + + if (nativeTab) { + return this.getWrapper(nativeTab); + } + return default_; + } + + addActiveTabPermission(nativeTab = tabTracker.activeTab) { + return super.addActiveTabPermission(nativeTab); + } + + revokeActiveTabPermission(nativeTab = tabTracker.activeTab) { + return super.revokeActiveTabPermission(nativeTab); + } + + canAccessTab(nativeTab) { + return ( + this.extension.privateBrowsingAllowed || + !PrivateBrowsingUtils.isBrowserPrivate(nativeTab.browser) + ); + } + + wrapTab(nativeTab) { + return new Tab(this.extension, nativeTab, nativeTab.id); + } +} + +class WindowManager extends WindowManagerBase { + get(windowId, context) { + const window = windowTracker.getWindow(windowId, context); + + return this.getWrapper(window); + } + + *getAll(context) { + for (const window of windowTracker.browserWindows()) { + if (!this.canAccessWindow(window, context)) { + continue; + } + const wrapped = this.getWrapper(window); + if (wrapped) { + yield wrapped; + } + } + } + + wrapWindow(window) { + return new Window(this.extension, window, windowTracker.getId(window)); + } +} + +// eslint-disable-next-line mozilla/balanced-listeners +extensions.on("startup", (type, extension) => { + defineLazyGetter(extension, "tabManager", () => new TabManager(extension)); + defineLazyGetter( + extension, + "windowManager", + () => new WindowManager(extension) + ); +}); + +/* eslint-disable mozilla/balanced-listeners */ +extensions.on("page-shutdown", (type, context) => { + if (context.viewType == "tab") { + const window = context.xulBrowser.ownerGlobal; + GeckoViewTabBridge.closeTab({ + window, + extensionId: context.extension.id, + }); + } +}); +/* eslint-enable mozilla/balanced-listeners */ + +global.openOptionsPage = async extension => { + const { options_ui } = extension.manifest; + const extensionId = extension.id; + + if (options_ui.open_in_tab) { + // Delegate new tab creation and open the options page in the new tab. + const tab = await GeckoViewTabBridge.createNewTab({ + extensionId, + createProperties: { + url: options_ui.page, + active: true, + }, + }); + + const { browser } = tab; + const flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + + browser.fixupAndLoadURIString(options_ui.page, { + flags, + triggeringPrincipal: extension.principal, + }); + + const newWindow = browser.ownerGlobal; + mobileWindowTracker.setTabActive(newWindow, true); + return; + } + + // Delegate option page handling to the app. + return GeckoViewTabBridge.openOptionsPage(extensionId); +}; diff --git a/mobile/android/components/extensions/ext-android.json b/mobile/android/components/extensions/ext-android.json new file mode 100644 index 0000000000..987dcc14b3 --- /dev/null +++ b/mobile/android/components/extensions/ext-android.json @@ -0,0 +1,31 @@ +{ + "browserAction": { + "url": "chrome://geckoview/content/ext-browserAction.js", + "schema": "chrome://extensions/content/schemas/browser_action.json", + "scopes": ["addon_parent"], + "manifest": ["browser_action", "action"], + "paths": [["browserAction"], ["action"]] + }, + "browsingData": { + "url": "chrome://extensions/content/parent/ext-browsingData.js", + "schema": "chrome://extensions/content/schemas/browsing_data.json", + "scopes": ["addon_parent"], + "paths": [["browsingData"]] + }, + "pageAction": { + "url": "chrome://geckoview/content/ext-pageAction.js", + "schema": "chrome://extensions/content/schemas/page_action.json", + "scopes": ["addon_parent"], + "manifest": ["page_action"], + "paths": [["pageAction"]] + }, + "tabs": { + "url": "chrome://geckoview/content/ext-tabs.js", + "schema": "chrome://geckoview/content/schemas/tabs.json", + "scopes": ["addon_parent"], + "paths": [["tabs"]] + }, + "geckoViewAddons": { + "schema": "chrome://geckoview/content/schemas/gecko_view_addons.json" + } +} diff --git a/mobile/android/components/extensions/ext-browserAction.js b/mobile/android/components/extensions/ext-browserAction.js new file mode 100644 index 0000000000..f8c3c3ce74 --- /dev/null +++ b/mobile/android/components/extensions/ext-browserAction.js @@ -0,0 +1,191 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et 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/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", + ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", +}); + +const { BrowserActionBase } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionActions.sys.mjs" +); + +const BROWSER_ACTION_PROPERTIES = [ + "title", + "icon", + "popup", + "badgeText", + "badgeBackgroundColor", + "badgeTextColor", + "enabled", + "patternMatching", +]; + +class BrowserAction extends BrowserActionBase { + constructor(extension, clickDelegate) { + const tabContext = new TabContext(tabId => this.getContextData(null)); + super(tabContext, extension); + this.clickDelegate = clickDelegate; + this.helper = new ExtensionActionHelper({ + extension, + tabTracker, + windowTracker, + tabContext, + properties: BROWSER_ACTION_PROPERTIES, + }); + } + + updateOnChange(tab) { + const tabId = tab ? tab.id : null; + const action = tab + ? this.getContextData(tab) + : this.helper.extractProperties(this.globals); + this.helper.sendRequest(tabId, { + action, + type: "GeckoView:BrowserAction:Update", + }); + } + + openPopup(tab, openPopupWithoutUserInteraction = false) { + const popupUri = openPopupWithoutUserInteraction + ? this.getPopupUrl(tab) + : this.triggerClickOrPopup(tab); + const actionObject = this.getContextData(tab); + const action = this.helper.extractProperties(actionObject); + this.helper.sendRequest(tab.id, { + action, + type: "GeckoView:BrowserAction:OpenPopup", + popupUri, + }); + } + + triggerClickOrPopup(tab = tabTracker.activeTab) { + return super.triggerClickOrPopup(tab); + } + + getTab(tabId) { + return this.helper.getTab(tabId); + } + + getWindow(windowId) { + return this.helper.getWindow(windowId); + } + + dispatchClick() { + this.clickDelegate.onClick(); + } +} + +this.browserAction = class extends ExtensionAPIPersistent { + static for(extension) { + return GeckoViewWebExtension.browserActions.get(extension); + } + + async onManifestEntry(entryName) { + const { extension } = this; + this.action = new BrowserAction(extension, this); + await this.action.loadIconData(); + + GeckoViewWebExtension.browserActions.set(extension, this.action); + + // Notify the embedder of this action + this.action.updateOnChange(null); + } + + onShutdown() { + const { extension } = this; + this.action.onShutdown(); + GeckoViewWebExtension.browserActions.delete(extension); + } + + onClick() { + this.emit("click", tabTracker.activeTab); + } + + PERSISTENT_EVENTS = { + onClicked({ context, fire }) { + const { extension } = this; + const { tabManager } = extension; + async function listener(_event, tab) { + if (fire.wakeup) { + await fire.wakeup(); + } + // TODO: we should double-check if the tab is already being closed by the time + // the background script got started and we converted the primed listener. + fire.sync(tabManager.convert(tab)); + } + this.on("click", listener); + return { + unregister: () => { + this.off("click", listener); + }, + convert(newFire, extContext) { + fire = newFire; + context = extContext; + }, + }; + }, + }; + + getAPI(context) { + const { extension } = context; + const { action } = this; + const namespace = + extension.manifestVersion < 3 ? "browserAction" : "action"; + + return { + [namespace]: { + ...action.api(context), + + onClicked: new EventManager({ + context, + // module name is "browserAction" because it the name used in the + // ext-browser.json, independently from the manifest version. + module: "browserAction", + event: "onClicked", + // NOTE: Firefox Desktop event has inputHandling set to true here. + // inputHandling: true, + extensionApi: this, + }).api(), + + openPopup: options => { + const isHandlingUserInput = + context.callContextData?.isHandlingUserInput; + + if ( + !Services.prefs.getBoolPref( + "extensions.openPopupWithoutUserGesture.enabled" + ) && + !isHandlingUserInput + ) { + throw new ExtensionError("openPopup requires a user gesture"); + } + + const currentWindow = windowTracker.getCurrentWindow(context); + + const window = + typeof options?.windowId === "number" + ? windowTracker.getWindow(options.windowId, context) + : currentWindow; + + if (window !== currentWindow) { + throw new ExtensionError( + "Only the current window is supported on Android." + ); + } + + if (this.action.getPopupUrl(window.tab, true)) { + action.openPopup(window.tab, !isHandlingUserInput); + } + }, + }, + }; + } +}; + +global.browserActionFor = this.browserAction.for; diff --git a/mobile/android/components/extensions/ext-c-android.js b/mobile/android/components/extensions/ext-c-android.js new file mode 100644 index 0000000000..3f2392a9c2 --- /dev/null +++ b/mobile/android/components/extensions/ext-c-android.js @@ -0,0 +1,13 @@ +/* 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"; + +extensions.registerModules({ + tabs: { + url: "chrome://geckoview/content/ext-c-tabs.js", + scopes: ["addon_child"], + paths: [["tabs"]], + }, +}); diff --git a/mobile/android/components/extensions/ext-c-tabs.js b/mobile/android/components/extensions/ext-c-tabs.js new file mode 100644 index 0000000000..cad1e29051 --- /dev/null +++ b/mobile/android/components/extensions/ext-c-tabs.js @@ -0,0 +1,23 @@ +/* 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.tabs = class extends ExtensionAPI { + getAPI(context) { + return { + tabs: { + connect(tabId, options) { + const { frameId = null, name = "" } = options || {}; + return context.messenger.connect({ name, tabId, frameId }); + }, + + sendMessage(tabId, message, options, callback) { + const arg = { tabId, frameId: options?.frameId, message, callback }; + return context.messenger.sendRuntimeMessage(arg); + }, + }, + }; + } +}; diff --git a/mobile/android/components/extensions/ext-downloads.js b/mobile/android/components/extensions/ext-downloads.js new file mode 100644 index 0000000000..116f976cbf --- /dev/null +++ b/mobile/android/components/extensions/ext-downloads.js @@ -0,0 +1,310 @@ +/* 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"; + +ChromeUtils.defineESModuleGetters(this, { + DownloadPaths: "resource://gre/modules/DownloadPaths.sys.mjs", + DownloadTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", +}); + +Cu.importGlobalProperties(["PathUtils"]); + +var { ignoreEvent } = ExtensionCommon; + +const REQUEST_DOWNLOAD_MESSAGE = "GeckoView:WebExtension:Download"; + +const FORBIDDEN_HEADERS = [ + "ACCEPT-CHARSET", + "ACCEPT-ENCODING", + "ACCESS-CONTROL-REQUEST-HEADERS", + "ACCESS-CONTROL-REQUEST-METHOD", + "CONNECTION", + "CONTENT-LENGTH", + "COOKIE", + "COOKIE2", + "DATE", + "DNT", + "EXPECT", + "HOST", + "KEEP-ALIVE", + "ORIGIN", + "TE", + "TRAILER", + "TRANSFER-ENCODING", + "UPGRADE", + "VIA", +]; + +const FORBIDDEN_PREFIXES = /^PROXY-|^SEC-/i; + +const State = { + IN_PROGRESS: "in_progress", + INTERRUPTED: "interrupted", + COMPLETE: "complete", +}; + +const STATE_MAP = new Map([ + [0, State.IN_PROGRESS], + [1, State.INTERRUPTED], + [2, State.COMPLETE], +]); + +const INTERRUPT_REASON_MAP = new Map([ + [0, undefined], + [1, "FILE_FAILED"], + [2, "FILE_ACCESS_DENIED"], + [3, "FILE_NO_SPACE"], + [4, "FILE_NAME_TOO_LONG"], + [5, "FILE_TOO_LARGE"], + [6, "FILE_VIRUS_INFECTED"], + [7, "FILE_TRANSIENT_ERROR"], + [8, "FILE_BLOCKED"], + [9, "FILE_SECURITY_CHECK_FAILED"], + [10, "FILE_TOO_SHORT"], + [11, "NETWORK_FAILED"], + [12, "NETWORK_TIMEOUT"], + [13, "NETWORK_DISCONNECTED"], + [14, "NETWORK_SERVER_DOWN"], + [15, "NETWORK_INVALID_REQUEST"], + [16, "SERVER_FAILED"], + [17, "SERVER_NO_RANGE"], + [18, "SERVER_BAD_CONTENT"], + [19, "SERVER_UNAUTHORIZED"], + [20, "SERVER_CERT_PROBLEM"], + [21, "SERVER_FORBIDDEN"], + [22, "USER_CANCELED"], + [23, "USER_SHUTDOWN"], + [24, "CRASH"], +]); + +// TODO Bug 1247794: make id and extension info persistent +class DownloadItem { + /** + * Initializes an object that represents a download + * + * @param {object} downloadInfo - an object from Java when creating a download + * @param {object} options - an object passed in to download() function + * @param {Extension} extension - instance of an extension object + */ + constructor(downloadInfo, options, extension) { + this.id = downloadInfo.id; + this.url = options.url; + this.referrer = downloadInfo.referrer || ""; + this.filename = downloadInfo.filename || ""; + this.incognito = options.incognito; + this.danger = "safe"; // todo; not implemented in desktop either + this.mime = downloadInfo.mime || ""; + this.startTime = downloadInfo.startTime; + this.state = STATE_MAP.get(downloadInfo.state); + this.paused = downloadInfo.paused; + this.canResume = downloadInfo.canResume; + this.bytesReceived = downloadInfo.bytesReceived; + this.totalBytes = downloadInfo.totalBytes; + this.fileSize = downloadInfo.fileSize; + this.exists = downloadInfo.exists; + this.byExtensionId = extension?.id; + this.byExtensionName = extension?.name; + } + + /** + * This function updates the download item it was called on. + * + * @param {object} data that arrived from the app (Java) + * @returns {object | null} an object of component == "..")) { + return Promise.reject({ + message: "filename must not contain back-references (..)", + }); + } + + if ( + pathComponents.some(component => { + const sanitized = DownloadPaths.sanitize(component, { + compressWhitespaces: false, + }); + return component != sanitized; + }) + ) { + return Promise.reject({ + message: "filename must not contain illegal characters", + }); + } + } + + if (options.incognito && !context.privateBrowsingAllowed) { + return Promise.reject({ + message: "Private browsing access not allowed", + }); + } + + if (options.cookieStoreId != null) { + // https://bugzilla.mozilla.org/show_bug.cgi?id=1721460 + throw new ExtensionError("Not implemented"); + } + + if (options.headers) { + for (const { name } of options.headers) { + if ( + FORBIDDEN_HEADERS.includes(name.toUpperCase()) || + name.match(FORBIDDEN_PREFIXES) + ) { + return Promise.reject({ + message: "Forbidden request header name", + }); + } + } + } + + return EventDispatcher.instance + .sendRequestForResult({ + type: REQUEST_DOWNLOAD_MESSAGE, + options, + extensionId: extension.id, + }) + .then(value => { + const downloadItem = new DownloadItem(value, options, extension); + DownloadTracker.addDownloadItem(downloadItem); + return downloadItem.id; + }); + }, + + removeFile(downloadId) { + throw new ExtensionError("Not implemented"); + }, + + search(query) { + throw new ExtensionError("Not implemented"); + }, + + pause(downloadId) { + throw new ExtensionError("Not implemented"); + }, + + resume(downloadId) { + throw new ExtensionError("Not implemented"); + }, + + cancel(downloadId) { + throw new ExtensionError("Not implemented"); + }, + + showDefaultFolder() { + throw new ExtensionError("Not implemented"); + }, + + erase(query) { + throw new ExtensionError("Not implemented"); + }, + + open(downloadId) { + throw new ExtensionError("Not implemented"); + }, + + show(downloadId) { + throw new ExtensionError("Not implemented"); + }, + + getFileIcon(downloadId, options) { + throw new ExtensionError("Not implemented"); + }, + + onChanged: new EventManager({ + context, + module: "downloads", + event: "onChanged", + extensionApi: this, + }).api(), + + onCreated: ignoreEvent(context, "downloads.onCreated"), + + onErased: ignoreEvent(context, "downloads.onErased"), + + onDeterminingFilename: ignoreEvent( + context, + "downloads.onDeterminingFilename" + ), + }, + }; + } +}; diff --git a/mobile/android/components/extensions/ext-pageAction.js b/mobile/android/components/extensions/ext-pageAction.js new file mode 100644 index 0000000000..a73d7dd11a --- /dev/null +++ b/mobile/android/components/extensions/ext-pageAction.js @@ -0,0 +1,153 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et 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/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", + ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", +}); + +const { PageActionBase } = ChromeUtils.importESModule( + "resource://gre/modules/ExtensionActions.sys.mjs" +); + +const PAGE_ACTION_PROPERTIES = [ + "title", + "icon", + "popup", + "badgeText", + "enabled", + "patternMatching", +]; + +class PageAction extends PageActionBase { + constructor(extension, clickDelegate) { + const tabContext = new TabContext(tabId => this.getContextData(null)); + super(tabContext, extension); + this.clickDelegate = clickDelegate; + this.helper = new ExtensionActionHelper({ + extension, + tabTracker, + windowTracker, + tabContext, + properties: PAGE_ACTION_PROPERTIES, + }); + } + + updateOnChange(tab) { + const tabId = tab ? tab.id : null; + // The embedder only gets the override, not the full object + const action = tab + ? this.getContextData(tab) + : this.helper.extractProperties(this.globals); + this.helper.sendRequest(tabId, { + action, + type: "GeckoView:PageAction:Update", + }); + } + + openPopup() { + const tab = tabTracker.activeTab; + const popupUri = this.triggerClickOrPopup(tab); + const actionObject = this.getContextData(tab); + const action = this.helper.extractProperties(actionObject); + this.helper.sendRequest(tab.id, { + action, + type: "GeckoView:PageAction:OpenPopup", + popupUri, + }); + } + + triggerClickOrPopup(tab = tabTracker.activeTab) { + return super.triggerClickOrPopup(tab); + } + + getTab(tabId) { + return this.helper.getTab(tabId); + } + + dispatchClick() { + this.clickDelegate.onClick(); + } +} + +this.pageAction = class extends ExtensionAPIPersistent { + static for(extension) { + return GeckoViewWebExtension.pageActions.get(extension); + } + + async onManifestEntry(entryName) { + const { extension } = this; + const action = new PageAction(extension, this); + await action.loadIconData(); + this.action = action; + + GeckoViewWebExtension.pageActions.set(extension, action); + + // Notify the embedder of this action + action.updateOnChange(null); + } + + onClick() { + this.emit("click", tabTracker.activeTab); + } + + onShutdown() { + const { extension, action } = this; + action.onShutdown(); + GeckoViewWebExtension.pageActions.delete(extension); + } + + PERSISTENT_EVENTS = { + onClicked({ fire }) { + const { extension } = this; + const { tabManager } = extension; + + const listener = async (_event, tab) => { + if (fire.wakeup) { + await fire.wakeup(); + } + // TODO: we should double-check if the tab is already being closed by the time + // the background script got started and we converted the primed listener. + fire.async(tabManager.convert(tab)); + }; + + this.on("click", listener); + return { + unregister: () => { + this.off("click", listener); + }, + convert(newFire, _extContext) { + fire = newFire; + }, + }; + }, + }; + + getAPI(context) { + const { action } = this; + + return { + pageAction: { + ...action.api(context), + + onClicked: new EventManager({ + context, + module: "pageAction", + event: "onClicked", + extensionApi: this, + }).api(), + + openPopup() { + action.openPopup(); + }, + }, + }; + } +}; + +global.pageActionFor = this.pageAction.for; diff --git a/mobile/android/components/extensions/ext-tabs.js b/mobile/android/components/extensions/ext-tabs.js new file mode 100644 index 0000000000..3d964dd164 --- /dev/null +++ b/mobile/android/components/extensions/ext-tabs.js @@ -0,0 +1,584 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et 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/. */ + +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.sys.mjs", + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", + mobileWindowTracker: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", +}); + +const getBrowserWindow = window => { + return window.browsingContext.topChromeWindow; +}; + +const tabListener = { + tabReadyInitialized: false, + tabReadyPromises: new WeakMap(), + initializingTabs: new WeakSet(), + + initTabReady() { + if (!this.tabReadyInitialized) { + windowTracker.addListener("progress", this); + + this.tabReadyInitialized = true; + } + }, + + onLocationChange(browser, webProgress, request, locationURI, flags) { + if (webProgress.isTopLevel) { + const { tab } = browser.ownerGlobal; + + // Ignore initial about:blank + if (!request && this.initializingTabs.has(tab)) { + return; + } + + // Now we are certain that the first page in the tab was loaded. + this.initializingTabs.delete(tab); + + // browser.innerWindowID is now set, resolve the promises if any. + const deferred = this.tabReadyPromises.get(tab); + if (deferred) { + deferred.resolve(tab); + this.tabReadyPromises.delete(tab); + } + } + }, + + /** + * Returns a promise that resolves when the tab is ready. + * Tabs created via the `tabs.create` method are "ready" once the location + * changes to the requested URL. Other tabs are assumed to be ready once their + * inner window ID is known. + * + * @param {NativeTab} nativeTab The native tab object. + * @returns {Promise} Resolves with the given tab once ready. + */ + awaitTabReady(nativeTab) { + let deferred = this.tabReadyPromises.get(nativeTab); + if (!deferred) { + deferred = PromiseUtils.defer(); + if ( + !this.initializingTabs.has(nativeTab) && + (nativeTab.browser.innerWindowID || + nativeTab.browser.currentURI.spec === "about:blank") + ) { + deferred.resolve(nativeTab); + } else { + this.initTabReady(); + this.tabReadyPromises.set(nativeTab, deferred); + } + } + return deferred.promise; + }, +}; + +this.tabs = class extends ExtensionAPIPersistent { + tabEventRegistrar({ event, listener }) { + const { extension } = this; + const { tabManager } = extension; + return ({ fire }) => { + const listener2 = (eventName, eventData, ...args) => { + if (!tabManager.canAccessTab(eventData.nativeTab)) { + return; + } + + listener(fire, eventData, ...args); + }; + + tabTracker.on(event, listener2); + return { + unregister() { + tabTracker.off(event, listener2); + }, + convert(_fire) { + fire = _fire; + }, + }; + }; + } + + PERSISTENT_EVENTS = { + onActivated({ fire, context }, params) { + const listener = (eventName, event) => { + const { windowId, tabId, isPrivate } = event; + if (isPrivate && !context.privateBrowsingAllowed) { + return; + } + // In GeckoView each window has only one tab, so previousTabId is omitted. + fire.async({ windowId, tabId }); + }; + + mobileWindowTracker.on("tab-activated", listener); + return { + unregister() { + mobileWindowTracker.off("tab-activated", listener); + }, + convert(_fire, _context) { + fire = _fire; + context = _context; + }, + }; + }, + onCreated: this.tabEventRegistrar({ + event: "tab-created", + listener: (fire, event) => { + const { tabManager } = this.extension; + fire.async(tabManager.convert(event.nativeTab)); + }, + }), + onRemoved: this.tabEventRegistrar({ + event: "tab-removed", + listener: (fire, event) => { + fire.async(event.tabId, { + windowId: event.windowId, + isWindowClosing: event.isWindowClosing, + }); + }, + }), + onUpdated({ fire, context }, params) { + const { tabManager } = this.extension; + const restricted = ["url", "favIconUrl", "title"]; + + function sanitize(tab, changeInfo) { + const result = {}; + let nonempty = false; + for (const prop in changeInfo) { + // In practice, changeInfo contains at most one property from + // restricted. Therefore it is not necessary to cache the value + // of tab.hasTabPermission outside the loop. + if (!restricted.includes(prop) || tab.hasTabPermission) { + nonempty = true; + result[prop] = changeInfo[prop]; + } + } + return [nonempty, result]; + } + + const fireForTab = (tab, changed) => { + const [needed, changeInfo] = sanitize(tab, changed); + if (needed) { + fire.async(tab.id, changeInfo, tab.convert()); + } + }; + + const listener = event => { + const needed = []; + let nativeTab; + switch (event.type) { + case "pagetitlechanged": { + const window = getBrowserWindow(event.target.ownerGlobal); + nativeTab = window.tab; + + needed.push("title"); + break; + } + + case "DOMAudioPlaybackStarted": + case "DOMAudioPlaybackStopped": { + const window = event.target.ownerGlobal; + nativeTab = window.tab; + needed.push("audible"); + break; + } + } + + if (!nativeTab) { + return; + } + + const tab = tabManager.getWrapper(nativeTab); + const changeInfo = {}; + for (const prop of needed) { + changeInfo[prop] = tab[prop]; + } + + fireForTab(tab, changeInfo); + }; + + const statusListener = ({ browser, status, url }) => { + const { tab } = browser.ownerGlobal; + if (tab) { + const changed = { status }; + if (url) { + changed.url = url; + } + + fireForTab(tabManager.wrapTab(tab), changed); + } + }; + + windowTracker.addListener("status", statusListener); + windowTracker.addListener("pagetitlechanged", listener); + + return { + unregister() { + windowTracker.removeListener("status", statusListener); + windowTracker.removeListener("pagetitlechanged", listener); + }, + convert(_fire, _context) { + fire = _fire; + context = _context; + }, + }; + }, + }; + + getAPI(context) { + const { extension } = context; + const { tabManager } = extension; + const extensionApi = this; + const module = "tabs"; + + function getTabOrActive(tabId) { + if (tabId !== null) { + return tabTracker.getTab(tabId); + } + return tabTracker.activeTab; + } + + async function promiseTabWhenReady(tabId) { + let tab; + if (tabId !== null) { + tab = tabManager.get(tabId); + } else { + tab = tabManager.getWrapper(tabTracker.activeTab); + } + if (!tab) { + throw new ExtensionError( + tabId == null ? "Cannot access activeTab" : `Invalid tab ID: ${tabId}` + ); + } + + await tabListener.awaitTabReady(tab.nativeTab); + + return tab; + } + + function loadURIInTab(nativeTab, url) { + const { browser } = nativeTab; + + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + let { principal } = context; + const isAboutUrl = url.startsWith("about:"); + if ( + isAboutUrl || + (url.startsWith("moz-extension://") && + !context.checkLoadURL(url, { dontReportErrors: true })) + ) { + // Falling back to content here as about: requires it, however is safe. + principal = + Services.scriptSecurityManager.getLoadContextContentPrincipal( + Services.io.newURI(url), + browser.loadContext + ); + } + if (isAboutUrl) { + // Make sure things like about:blank and other about: URIs never + // inherit, and instead always get a NullPrincipal. + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL; + } + + browser.fixupAndLoadURIString(url, { + flags, + triggeringPrincipal: principal, + }); + } + + return { + tabs: { + onActivated: new EventManager({ + context, + module, + event: "onActivated", + extensionApi, + }).api(), + + onCreated: new EventManager({ + context, + module, + event: "onCreated", + extensionApi, + }).api(), + + /** + * Since multiple tabs currently can't be highlighted, onHighlighted + * essentially acts an alias for tabs.onActivated but returns + * the tabId in an array to match the API. + * + * @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted + */ + onHighlighted: makeGlobalEvent( + context, + "tabs.onHighlighted", + "Tab:Selected", + (fire, data) => { + const tab = tabManager.get(data.id); + + fire.async({ tabIds: [tab.id], windowId: tab.windowId }); + } + ), + + // Some events below are not be persisted because they are not implemented. + // They do not have an "extensionApi" property with an entry in + // PERSISTENT_EVENTS, but instead an empty "register" method. + onAttached: new EventManager({ + context, + name: "tabs.onAttached", + register: fire => { + return () => {}; + }, + }).api(), + + onDetached: new EventManager({ + context, + name: "tabs.onDetached", + register: fire => { + return () => {}; + }, + }).api(), + + onRemoved: new EventManager({ + context, + module, + event: "onRemoved", + extensionApi, + }).api(), + + onReplaced: new EventManager({ + context, + name: "tabs.onReplaced", + register: fire => { + return () => {}; + }, + }).api(), + + onMoved: new EventManager({ + context, + name: "tabs.onMoved", + register: fire => { + return () => {}; + }, + }).api(), + + onUpdated: new EventManager({ + context, + module, + event: "onUpdated", + extensionApi, + }).api(), + + async create({ + active, + cookieStoreId, + discarded, + index, + openInReaderMode, + pinned, + title, + url, + } = {}) { + if (active === null) { + active = true; + } + + tabListener.initTabReady(); + + if (url !== null) { + url = context.uri.resolve(url); + + if ( + !url.startsWith("moz-extension://") && + !context.checkLoadURL(url, { dontReportErrors: true }) + ) { + return Promise.reject({ message: `Illegal URL: ${url}` }); + } + } + + if (cookieStoreId) { + cookieStoreId = getUserContextIdForCookieStoreId( + extension, + cookieStoreId, + false // TODO bug 1372178: support creation of private browsing tabs + ); + } + cookieStoreId = cookieStoreId ? cookieStoreId.toString() : undefined; + + const nativeTab = await GeckoViewTabBridge.createNewTab({ + extensionId: context.extension.id, + createProperties: { + active, + cookieStoreId, + discarded, + index, + openInReaderMode, + pinned, + url, + }, + }); + + // Make sure things like about:blank URIs never inherit, + // and instead always get a NullPrincipal. + if (url !== null) { + tabListener.initializingTabs.add(nativeTab); + } else { + url = "about:blank"; + } + + loadURIInTab(nativeTab, url); + + if (active) { + const newWindow = nativeTab.browser.ownerGlobal; + mobileWindowTracker.setTabActive(newWindow, true); + } + + return tabManager.convert(nativeTab); + }, + + async remove(tabs) { + if (!Array.isArray(tabs)) { + tabs = [tabs]; + } + + await Promise.all( + tabs.map(async tabId => { + const windowId = GeckoViewTabBridge.tabIdToWindowId(tabId); + const window = windowTracker.getWindow(windowId, context, false); + if (!window) { + throw new ExtensionError(`Invalid tab ID ${tabId}`); + } + await GeckoViewTabBridge.closeTab({ + window, + extensionId: context.extension.id, + }); + }) + ); + }, + + async update( + tabId, + { active, autoDiscardable, highlighted, muted, pinned, url } = {} + ) { + const nativeTab = getTabOrActive(tabId); + const window = nativeTab.browser.ownerGlobal; + + if (url !== null) { + url = context.uri.resolve(url); + + if ( + !url.startsWith("moz-extension://") && + !context.checkLoadURL(url, { dontReportErrors: true }) + ) { + return Promise.reject({ message: `Illegal URL: ${url}` }); + } + } + + await GeckoViewTabBridge.updateTab({ + window, + extensionId: context.extension.id, + updateProperties: { + active, + autoDiscardable, + highlighted, + muted, + pinned, + url, + }, + }); + + if (url !== null) { + loadURIInTab(nativeTab, url); + } + + // FIXME: openerTabId, successorTabId + if (active) { + mobileWindowTracker.setTabActive(window, true); + } + + return tabManager.convert(nativeTab); + }, + + async reload(tabId, reloadProperties) { + const nativeTab = getTabOrActive(tabId); + + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE; + if (reloadProperties && reloadProperties.bypassCache) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + } + nativeTab.browser.reloadWithFlags(flags); + }, + + async get(tabId) { + return tabManager.get(tabId).convert(); + }, + + async getCurrent() { + if (context.tabId) { + return tabManager.get(context.tabId).convert(); + } + }, + + async query(queryInfo) { + return Array.from(tabManager.query(queryInfo, context), tab => + tab.convert() + ); + }, + + async captureTab(tabId, options) { + const nativeTab = getTabOrActive(tabId); + await tabListener.awaitTabReady(nativeTab); + + const { browser } = nativeTab; + const tab = tabManager.wrapTab(nativeTab); + return tab.capture(context, browser.fullZoom, options); + }, + + async captureVisibleTab(windowId, options) { + const window = + windowId == null + ? windowTracker.topWindow + : windowTracker.getWindow(windowId, context); + + const tab = tabManager.wrapTab(window.tab); + await tabListener.awaitTabReady(tab.nativeTab); + const zoom = window.browsingContext.fullZoom; + + return tab.capture(context, zoom, options); + }, + + async executeScript(tabId, details) { + const tab = await promiseTabWhenReady(tabId); + + return tab.executeScript(context, details); + }, + + async insertCSS(tabId, details) { + const tab = await promiseTabWhenReady(tabId); + + return tab.insertCSS(context, details); + }, + + async removeCSS(tabId, details) { + const tab = await promiseTabWhenReady(tabId); + + return tab.removeCSS(context, details); + }, + + goForward(tabId) { + const { browser } = getTabOrActive(tabId); + browser.goForward(); + }, + + goBack(tabId) { + const { browser } = getTabOrActive(tabId); + browser.goBack(); + }, + }, + }; + } +}; diff --git a/mobile/android/components/extensions/extensions-mobile.manifest b/mobile/android/components/extensions/extensions-mobile.manifest new file mode 100644 index 0000000000..34850bed8b --- /dev/null +++ b/mobile/android/components/extensions/extensions-mobile.manifest @@ -0,0 +1,5 @@ +# modules +category webextension-modules android chrome://geckoview/content/ext-android.json + +category webextension-scripts c-android chrome://geckoview/content/ext-android.js +category webextension-scripts-addon android chrome://geckoview/content/ext-c-android.js diff --git a/mobile/android/components/extensions/jar.mn b/mobile/android/components/extensions/jar.mn new file mode 100644 index 0000000000..29d4cd01cb --- /dev/null +++ b/mobile/android/components/extensions/jar.mn @@ -0,0 +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/. + +geckoview.jar: + content/ext-android.js + content/ext-android.json + content/ext-browserAction.js + content/ext-c-android.js + content/ext-c-tabs.js + content/ext-pageAction.js + content/ext-tabs.js + content/ext-downloads.js +% override chrome://extensions/content/parent/ext-downloads.js chrome://geckoview/content/ext-downloads.js diff --git a/mobile/android/components/extensions/moz.build b/mobile/android/components/extensions/moz.build new file mode 100644 index 0000000000..9c61bf0e94 --- /dev/null +++ b/mobile/android/components/extensions/moz.build @@ -0,0 +1,21 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] + +EXTRA_JS_MODULES += [ + "ExtensionBrowsingData.sys.mjs", +] + +EXTRA_COMPONENTS += [ + "extensions-mobile.manifest", +] + +DIRS += ["schemas"] + +MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"] +MOCHITEST_CHROME_MANIFESTS += ["test/mochitest/chrome.ini"] +XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"] diff --git a/mobile/android/components/extensions/schemas/gecko_view_addons.json b/mobile/android/components/extensions/schemas/gecko_view_addons.json new file mode 100644 index 0000000000..b60d346d19 --- /dev/null +++ b/mobile/android/components/extensions/schemas/gecko_view_addons.json @@ -0,0 +1,16 @@ +[ + { + "namespace": "manifest", + "types": [ + { + "$extend": "PermissionPrivileged", + "choices": [ + { + "type": "string", + "enum": ["geckoViewAddons", "nativeMessagingFromContent"] + } + ] + } + ] + } +] diff --git a/mobile/android/components/extensions/schemas/jar.mn b/mobile/android/components/extensions/schemas/jar.mn new file mode 100644 index 0000000000..9f15031cd1 --- /dev/null +++ b/mobile/android/components/extensions/schemas/jar.mn @@ -0,0 +1,7 @@ +# 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/. + +geckoview.jar: + content/schemas/gecko_view_addons.json + content/schemas/tabs.json diff --git a/mobile/android/components/extensions/schemas/moz.build b/mobile/android/components/extensions/schemas/moz.build new file mode 100644 index 0000000000..d988c0ff9b --- /dev/null +++ b/mobile/android/components/extensions/schemas/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ["jar.mn"] diff --git a/mobile/android/components/extensions/schemas/tabs.json b/mobile/android/components/extensions/schemas/tabs.json new file mode 100644 index 0000000000..6d304affa1 --- /dev/null +++ b/mobile/android/components/extensions/schemas/tabs.json @@ -0,0 +1,1395 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +[ + { + "namespace": "manifest", + "types": [ + { + "$extend": "OptionalPermissionNoPrompt", + "choices": [ + { + "type": "string", + "enum": ["activeTab"] + } + ] + }, + { + "$extend": "OptionalPermission", + "choices": [ + { + "type": "string", + "enum": ["tabs"] + } + ] + } + ] + }, + { + "namespace": "tabs", + "description": "Use the browser.tabs API to interact with the browser's tab system. You can use this API to create, modify, and rearrange tabs in the browser.", + "types": [ + { + "id": "MutedInfoReason", + "type": "string", + "description": "An event that caused a muted state change.", + "enum": [ + { + "name": "user", + "description": "A user input action has set/overridden the muted state." + }, + { + "name": "capture", + "description": "Tab capture started, forcing a muted state change." + }, + { + "name": "extension", + "description": "An extension, identified by the extensionId field, set the muted state." + } + ] + }, + { + "id": "MutedInfo", + "type": "object", + "description": "Tab muted state and the reason for the last state change.", + "properties": { + "muted": { + "type": "boolean", + "description": "Whether the tab is prevented from playing sound (but hasn't necessarily recently produced sound). Equivalent to whether the muted audio indicator is showing." + }, + "reason": { + "$ref": "MutedInfoReason", + "optional": true, + "description": "The reason the tab was muted or unmuted. Not set if the tab's mute state has never been changed." + }, + "extensionId": { + "type": "string", + "optional": true, + "description": "The ID of the extension that changed the muted state. Not set if an extension was not the reason the muted state last changed." + } + } + }, + { + "id": "SharingState", + "type": "object", + "description": "Tab sharing state for screen, microphone and camera. Currently unsupported on Android.", + "properties": { + "screen": { + "type": "string", + "optional": true, + "description": "If the tab is sharing the screen the value will be one of \"Screen\", \"Window\", or \"Application\", or undefined if not screen sharing." + }, + "camera": { + "type": "boolean", + "description": "True if the tab is using the camera." + }, + "microphone": { + "type": "boolean", + "description": "True if the tab is using the microphone." + } + } + }, + { + "id": "Tab", + "type": "object", + "properties": { + "id": { + "type": "integer", + "minimum": -1, + "optional": true, + "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows." + }, + "index": { + "type": "integer", + "minimum": -1, + "description": "The zero-based index of the tab within its window." + }, + "windowId": { + "type": "integer", + "optional": true, + "minimum": 0, + "description": "The ID of the window the tab is contained within." + }, + "openerTabId": { + "unsupported": true, + "type": "integer", + "minimum": 0, + "optional": true, + "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists." + }, + "highlighted": { + "type": "boolean", + "description": "Whether the tab is highlighted. Works as an alias of active." + }, + "active": { + "type": "boolean", + "description": "Whether the tab is active in its window. (Does not necessarily mean the window is focused.)" + }, + "pinned": { + "type": "boolean", + "description": "Whether the tab is pinned." + }, + "lastAccessed": { + "type": "integer", + "optional": true, + "description": "The last time the tab was accessed as the number of milliseconds since epoch." + }, + "audible": { + "type": "boolean", + "optional": true, + "description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing." + }, + "mutedInfo": { + "$ref": "MutedInfo", + "optional": true, + "description": "Current tab muted state and the reason for the last state change." + }, + "url": { + "type": "string", + "optional": true, + "permissions": ["tabs"], + "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the \"tabs\" permission." + }, + "title": { + "type": "string", + "optional": true, + "permissions": ["tabs"], + "description": "The title of the tab. This property is only present if the extension's manifest includes the \"tabs\" permission." + }, + "favIconUrl": { + "type": "string", + "optional": true, + "permissions": ["tabs"], + "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the \"tabs\" permission. It may also be an empty string if the tab is loading." + }, + "status": { + "type": "string", + "optional": true, + "description": "Either loading or complete." + }, + "discarded": { + "type": "boolean", + "optional": true, + "description": "True while the tab is not loaded with content." + }, + "incognito": { + "type": "boolean", + "description": "Whether the tab is in an incognito window." + }, + "width": { + "type": "integer", + "optional": true, + "description": "The width of the tab in pixels." + }, + "height": { + "type": "integer", + "optional": true, + "description": "The height of the tab in pixels." + }, + "hidden": { + "type": "boolean", + "optional": true, + "description": "True if the tab is hidden." + }, + "sessionId": { + "type": "string", + "optional": true, + "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API." + }, + "cookieStoreId": { + "type": "string", + "optional": true, + "description": "The CookieStoreId used for the tab." + }, + "isArticle": { + "type": "boolean", + "optional": true, + "description": "Whether the document in the tab can be rendered in reader mode." + }, + "isInReaderMode": { + "type": "boolean", + "optional": true, + "description": "Whether the document in the tab is being rendered in reader mode." + }, + "sharingState": { + "$ref": "SharingState", + "optional": true, + "description": "Current tab sharing state for screen, microphone and camera." + }, + "attention": { + "type": "boolean", + "optional": true, + "description": "Whether the tab is drawing attention." + }, + "successorTabId": { + "type": "integer", + "optional": true, + "minimum": -1, + "description": "The ID of this tab's successor, if any; $(ref:tabs.TAB_ID_NONE) otherwise." + } + } + }, + { + "id": "ZoomSettingsMode", + "type": "string", + "description": "Defines how zoom changes are handled, i.e. which entity is responsible for the actual scaling of the page; defaults to automatic.", + "enum": [ + { + "name": "automatic", + "description": "Zoom changes are handled automatically by the browser." + }, + { + "name": "manual", + "description": "Overrides the automatic handling of zoom changes. The onZoomChange event will still be dispatched, and it is the responsibility of the extension to listen for this event and manually scale the page. This mode does not support per-origin zooming, and will thus ignore the scope zoom setting and assume per-tab." + }, + { + "name": "disabled", + "description": "Disables all zooming in the tab. The tab will revert to the default zoom level, and all attempted zoom changes will be ignored." + } + ] + }, + { + "id": "ZoomSettingsScope", + "type": "string", + "description": "Defines whether zoom changes will persist for the page's origin, or only take effect in this tab; defaults to per-origin when in automatic mode, and per-tab otherwise.", + "enum": [ + { + "name": "per-origin", + "description": "Zoom changes will persist in the zoomed page's origin, i.e. all other tabs navigated to that same origin will be zoomed as well. Moreover, per-origin zoom changes are saved with the origin, meaning that when navigating to other pages in the same origin, they will all be zoomed to the same zoom factor. The per-origin scope is only available in the automatic mode." + }, + { + "name": "per-tab", + "description": "Zoom changes only take effect in this tab, and zoom changes in other tabs will not affect the zooming of this tab. Also, per-tab zoom changes are reset on navigation; navigating a tab will always load pages with their per-origin zoom factors." + } + ] + }, + { + "id": "ZoomSettings", + "type": "object", + "description": "Defines how zoom changes in a tab are handled and at what scope.", + "properties": { + "mode": { + "$ref": "ZoomSettingsMode", + "description": "Defines how zoom changes are handled, i.e. which entity is responsible for the actual scaling of the page; defaults to automatic.", + "optional": true + }, + "scope": { + "$ref": "ZoomSettingsScope", + "description": "Defines whether zoom changes will persist for the page's origin, or only take effect in this tab; defaults to per-origin when in automatic mode, and per-tab otherwise.", + "optional": true + }, + "defaultZoomFactor": { + "type": "number", + "optional": true, + "description": "Used to return the default zoom level for the current tab in calls to tabs.getZoomSettings." + } + } + }, + { + "id": "TabStatus", + "type": "string", + "enum": ["loading", "complete"], + "description": "Whether the tabs have completed loading." + }, + { + "id": "WindowType", + "type": "string", + "enum": ["normal", "popup", "panel", "app", "devtools"], + "description": "The type of window." + } + ], + "properties": { + "TAB_ID_NONE": { + "value": -1, + "description": "An ID which represents the absence of a browser tab." + } + }, + "functions": [ + { + "name": "get", + "type": "function", + "description": "Retrieves details about the specified tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0 + }, + { + "type": "function", + "name": "callback", + "parameters": [{ "name": "tab", "$ref": "Tab" }] + } + ] + }, + { + "name": "getCurrent", + "type": "function", + "description": "Gets the tab that this script call is being made from. May be undefined if called from a non-tab context (for example: a background page or popup view).", + "async": "callback", + "parameters": [ + { + "type": "function", + "name": "callback", + "parameters": [ + { + "name": "tab", + "$ref": "Tab", + "optional": true + } + ] + } + ] + }, + { + "name": "connect", + "type": "function", + "description": "Connects to the content script(s) in the specified tab. The $(ref:runtime.onConnect) event is fired in each content script running in the specified tab for the current extension. For more details, see $(topic:messaging)[Content Script Messaging].", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0 + }, + { + "type": "object", + "name": "connectInfo", + "properties": { + "name": { + "type": "string", + "optional": true, + "description": "Will be passed into onConnect for content scripts that are listening for the connection event." + }, + "frameId": { + "type": "integer", + "optional": true, + "minimum": 0, + "description": "Open a port to a specific $(topic:frame_ids)[frame] identified by frameId instead of all frames in the tab." + } + }, + "optional": true + } + ], + "returns": { + "$ref": "runtime.Port", + "description": "A port that can be used to communicate with the content scripts running in the specified tab. The port's $(ref:runtime.Port) event is fired if the tab closes or does not exist. " + } + }, + { + "name": "sendMessage", + "type": "function", + "description": "Sends a single message to the content script(s) in the specified tab, with an optional callback to run when a response is sent back. The $(ref:runtime.onMessage) event is fired in each content script running in the specified tab for the current extension.", + "async": "responseCallback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0 + }, + { + "type": "any", + "name": "message" + }, + { + "type": "object", + "name": "options", + "properties": { + "frameId": { + "type": "integer", + "optional": true, + "minimum": 0, + "description": "Send a message to a specific $(topic:frame_ids)[frame] identified by frameId instead of all frames in the tab." + } + }, + "optional": true + }, + { + "type": "function", + "name": "responseCallback", + "optional": true, + "parameters": [ + { + "name": "response", + "type": "any", + "description": "The JSON response object sent by the handler of the message. If an error occurs while connecting to the specified tab, the callback will be called with no arguments and $(ref:runtime.lastError) will be set to the error message." + } + ] + } + ] + }, + { + "name": "create", + "type": "function", + "description": "Creates a new tab.", + "async": "callback", + "parameters": [ + { + "type": "object", + "name": "createProperties", + "properties": { + "windowId": { + "type": "integer", + "minimum": -2, + "optional": true, + "description": "The window to create the new tab in. Defaults to the $(topic:current-window)[current window]." + }, + "index": { + "type": "integer", + "minimum": 0, + "optional": true, + "description": "The position the tab should take in the window. The provided value will be clamped to between zero and the number of tabs in the window." + }, + "url": { + "type": "string", + "optional": true, + "description": "The URL to navigate the tab to initially. Fully-qualified URLs must include a scheme (i.e. 'http://www.google.com', not 'www.google.com'). Relative URLs will be relative to the current page within the extension. Defaults to the New Tab Page." + }, + "active": { + "type": "boolean", + "optional": true, + "description": "Whether the tab should become the active tab in the window. Does not affect whether the window is focused (see $(ref:windows.update)). Defaults to true." + }, + "pinned": { + "type": "boolean", + "optional": true, + "description": "Whether the tab should be pinned. Defaults to false" + }, + "openerTabId": { + "unsupported": true, + "type": "integer", + "minimum": 0, + "optional": true, + "description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as the newly created tab." + }, + "cookieStoreId": { + "type": "string", + "optional": true, + "description": "The CookieStoreId for the tab that opened this tab." + } + } + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [ + { + "name": "tab", + "$ref": "Tab", + "optional": true, + "description": "Details about the created tab. Will contain the ID of the new tab." + } + ] + } + ] + }, + { + "name": "duplicate", + "unsupported": true, + "type": "function", + "description": "Duplicates a tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "description": "The ID of the tab which is to be duplicated." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [ + { + "name": "tab", + "optional": true, + "description": "Details about the duplicated tab. The $(ref:tabs.Tab) object doesn't contain url, title and favIconUrl if the \"tabs\" permission has not been requested.", + "$ref": "Tab" + } + ] + } + ] + }, + { + "name": "query", + "type": "function", + "description": "Gets all tabs that have the specified properties, or all tabs if no properties are specified.", + "async": "callback", + "parameters": [ + { + "type": "object", + "name": "queryInfo", + "properties": { + "active": { + "type": "boolean", + "optional": true, + "description": "Whether the tabs are active in their windows." + }, + "pinned": { + "type": "boolean", + "optional": true, + "description": "Whether the tabs are pinned." + }, + "audible": { + "type": "boolean", + "optional": true, + "description": "Whether the tabs are audible." + }, + "muted": { + "type": "boolean", + "optional": true, + "description": "Whether the tabs are muted." + }, + "highlighted": { + "type": "boolean", + "optional": true, + "description": "Whether the tabs are highlighted. Works as an alias of active." + }, + "currentWindow": { + "type": "boolean", + "optional": true, + "description": "Whether the tabs are in the $(topic:current-window)[current window]." + }, + "lastFocusedWindow": { + "type": "boolean", + "optional": true, + "description": "Whether the tabs are in the last focused window." + }, + "status": { + "$ref": "TabStatus", + "optional": true, + "description": "Whether the tabs have completed loading." + }, + "discarded": { + "type": "boolean", + "optional": true, + "description": "True while the tabs are not loaded with content." + }, + "title": { + "type": "string", + "optional": true, + "description": "Match page titles against a pattern." + }, + "url": { + "choices": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ], + "optional": true, + "description": "Match tabs against one or more $(topic:match_patterns)[URL patterns]. Note that fragment identifiers are not matched." + }, + "windowId": { + "type": "integer", + "optional": true, + "minimum": -2, + "description": "The ID of the parent window, or $(ref:windows.WINDOW_ID_CURRENT) for the $(topic:current-window)[current window]." + }, + "windowType": { + "$ref": "WindowType", + "optional": true, + "description": "The type of window the tabs are in." + }, + "index": { + "type": "integer", + "optional": true, + "minimum": 0, + "description": "The position of the tabs within their windows." + }, + "cookieStoreId": { + "choices": [ + { + "type": "array", + "items": { "type": "string" } + }, + { + "type": "string" + } + ], + "optional": true, + "description": "The CookieStoreId used for the tab." + } + } + }, + { + "type": "function", + "name": "callback", + "parameters": [ + { + "name": "result", + "type": "array", + "items": { + "$ref": "Tab" + } + } + ] + } + ] + }, + { + "name": "highlight", + "type": "function", + "description": "Highlights the given tabs.", + "async": "callback", + "unsupported": true, + "parameters": [ + { + "type": "object", + "name": "highlightInfo", + "properties": { + "windowId": { + "type": "integer", + "optional": true, + "description": "The window that contains the tabs.", + "minimum": -2 + }, + "tabs": { + "description": "One or more tab indices to highlight.", + "choices": [ + { + "type": "array", + "items": { "type": "integer", "minimum": 0 } + }, + { "type": "integer" } + ] + } + } + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [ + { + "name": "window", + "$ref": "windows.Window", + "description": "Contains details about the window whose tabs were highlighted." + } + ] + } + ] + }, + { + "name": "update", + "type": "function", + "description": "Modifies the properties of a tab. Properties that are not specified in updateProperties are not modified.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "Defaults to the selected tab of the $(topic:current-window)[current window]." + }, + { + "type": "object", + "name": "updateProperties", + "properties": { + "url": { + "type": "string", + "optional": true, + "description": "A URL to navigate the tab to." + }, + "active": { + "type": "boolean", + "optional": true, + "description": "Whether the tab should be active. Does not affect whether the window is focused (see $(ref:windows.update))." + }, + "highlighted": { + "unsupported": true, + "type": "boolean", + "optional": true, + "description": "Adds or removes the tab from the current selection." + }, + "pinned": { + "type": "boolean", + "unsupported": true, + "optional": true, + "description": "Whether the tab should be pinned." + }, + "muted": { + "type": "boolean", + "unsupported": true, + "optional": true, + "description": "Whether the tab should be muted." + }, + "openerTabId": { + "unsupported": true, + "type": "integer", + "minimum": 0, + "optional": true, + "description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab." + } + } + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [ + { + "name": "tab", + "$ref": "Tab", + "optional": true, + "description": "Details about the updated tab. The $(ref:tabs.Tab) object doesn't contain url, title and favIconUrl if the \"tabs\" permission has not been requested." + } + ] + } + ] + }, + { + "name": "move", + "unsupported": true, + "type": "function", + "description": "Moves one or more tabs to a new position within its window, or to a new window. Note that tabs can only be moved to and from normal (window.type === \"normal\") windows.", + "async": "callback", + "parameters": [ + { + "name": "tabIds", + "description": "The tab or list of tabs to move.", + "choices": [ + { "type": "integer", "minimum": 0 }, + { "type": "array", "items": { "type": "integer", "minimum": 0 } } + ] + }, + { + "type": "object", + "name": "moveProperties", + "properties": { + "windowId": { + "type": "integer", + "minimum": -2, + "optional": true, + "description": "Defaults to the window the tab is currently in." + }, + "index": { + "type": "integer", + "minimum": -1, + "description": "The position to move the window to. -1 will place the tab at the end of the window." + } + } + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [ + { + "name": "tabs", + "description": "Details about the moved tabs.", + "choices": [ + { "$ref": "Tab" }, + { "type": "array", "items": { "$ref": "Tab" } } + ] + } + ] + } + ] + }, + { + "name": "reload", + "type": "function", + "description": "Reload a tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab to reload; defaults to the selected tab of the current window." + }, + { + "type": "object", + "name": "reloadProperties", + "optional": true, + "properties": { + "bypassCache": { + "type": "boolean", + "optional": true, + "description": "Whether using any local cache. Default is false." + } + } + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [] + } + ] + }, + { + "name": "remove", + "type": "function", + "description": "Closes one or more tabs.", + "async": "callback", + "parameters": [ + { + "name": "tabIds", + "description": "The tab or list of tabs to close.", + "choices": [ + { "type": "integer", "minimum": 0 }, + { "type": "array", "items": { "type": "integer", "minimum": 0 } } + ] + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [] + } + ] + }, + { + "name": "detectLanguage", + "unsupported": true, + "type": "function", + "description": "Detects the primary language of the content in a tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "Defaults to the active tab of the $(topic:current-window)[current window]." + }, + { + "type": "function", + "name": "callback", + "parameters": [ + { + "type": "string", + "name": "language", + "description": "An ISO language code such as en or fr. For a complete list of languages supported by this method, see kLanguageInfoTable. The 2nd to 4th columns will be checked and the first non-NULL value will be returned except for Simplified Chinese for which zh-CN will be returned. For an unknown language, und will be returned." + } + ] + } + ] + }, + { + "name": "captureTab", + "type": "function", + "description": "Captures an area of a specified tab. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.", + "permissions": [""], + "async": true, + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The tab to capture. Defaults to the active tab of the current window." + }, + { + "$ref": "extensionTypes.ImageDetails", + "name": "options", + "optional": true + } + ] + }, + { + "name": "captureVisibleTab", + "type": "function", + "description": "Captures an area of the currently active tab in the specified window. You must have $(topic:declare_permissions)[<all_urls>] permission to use this method.", + "permissions": [""], + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "windowId", + "minimum": -2, + "optional": true, + "description": "The target window. Defaults to the $(topic:current-window)[current window]." + }, + { + "$ref": "extensionTypes.ImageDetails", + "name": "options", + "optional": true + }, + { + "type": "function", + "name": "callback", + "parameters": [ + { + "type": "string", + "name": "dataUrl", + "description": "A data URL which encodes an image of the visible area of the captured tab. May be assigned to the 'src' property of an HTML Image element for display." + } + ] + } + ] + }, + { + "name": "executeScript", + "type": "function", + "max_manifest_version": 2, + "description": "Injects JavaScript code into a page. For details, see the $(topic:content_scripts)[programmatic injection] section of the content scripts doc.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab in which to run the script; defaults to the active tab of the current window." + }, + { + "$ref": "extensionTypes.InjectDetails", + "name": "details", + "description": "Details of the script to run." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "description": "Called after all the JavaScript has been executed.", + "parameters": [ + { + "name": "result", + "optional": true, + "type": "array", + "items": { "type": "any" }, + "description": "The result of the script in every injected frame." + } + ] + } + ] + }, + { + "name": "insertCSS", + "type": "function", + "max_manifest_version": 2, + "description": "Injects CSS into a page. For details, see the $(topic:content_scripts)[programmatic injection] section of the content scripts doc.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab in which to insert the CSS; defaults to the active tab of the current window." + }, + { + "$ref": "extensionTypes.InjectDetails", + "name": "details", + "description": "Details of the CSS text to insert." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "description": "Called when all the CSS has been inserted.", + "parameters": [] + } + ] + }, + { + "name": "removeCSS", + "type": "function", + "max_manifest_version": 2, + "description": "Removes injected CSS from a page. For details, see the $(topic:content_scripts)[programmatic injection] section of the content scripts doc.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab from which to remove the injected CSS; defaults to the active tab of the current window." + }, + { + "$ref": "extensionTypes.InjectDetails", + "name": "details", + "description": "Details of the CSS text to remove." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "description": "Called when all the CSS has been removed.", + "parameters": [] + } + ] + }, + { + "name": "setZoom", + "unsupported": true, + "type": "function", + "description": "Zooms a specified tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab to zoom; defaults to the active tab of the current window." + }, + { + "type": "number", + "name": "zoomFactor", + "description": "The new zoom factor. Use a value of 0 here to set the tab to its current default zoom factor. Values greater than zero specify a (possibly non-default) zoom factor for the tab." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "description": "Called after the zoom factor has been changed.", + "parameters": [] + } + ] + }, + { + "name": "getZoom", + "unsupported": true, + "type": "function", + "description": "Gets the current zoom factor of a specified tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab to get the current zoom factor from; defaults to the active tab of the current window." + }, + { + "type": "function", + "name": "callback", + "description": "Called with the tab's current zoom factor after it has been fetched.", + "parameters": [ + { + "type": "number", + "name": "zoomFactor", + "description": "The tab's current zoom factor." + } + ] + } + ] + }, + { + "name": "setZoomSettings", + "unsupported": true, + "type": "function", + "description": "Sets the zoom settings for a specified tab, which define how zoom changes are handled. These settings are reset to defaults upon navigating the tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "optional": true, + "minimum": 0, + "description": "The ID of the tab to change the zoom settings for; defaults to the active tab of the current window." + }, + { + "$ref": "ZoomSettings", + "name": "zoomSettings", + "description": "Defines how zoom changes are handled and at what scope." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "description": "Called after the zoom settings have been changed.", + "parameters": [] + } + ] + }, + { + "name": "getZoomSettings", + "unsupported": true, + "type": "function", + "description": "Gets the current zoom settings of a specified tab.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "optional": true, + "minimum": 0, + "description": "The ID of the tab to get the current zoom settings from; defaults to the active tab of the current window." + }, + { + "type": "function", + "name": "callback", + "description": "Called with the tab's current zoom settings.", + "parameters": [ + { + "$ref": "ZoomSettings", + "name": "zoomSettings", + "description": "The tab's current zoom settings." + } + ] + } + ] + }, + { + "name": "goForward", + "type": "function", + "description": "Navigate to next page in tab's history, if available.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab to navigate forward." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [] + } + ] + }, + { + "name": "goBack", + "type": "function", + "description": "Navigate to previous page in tab's history, if available.", + "async": "callback", + "parameters": [ + { + "type": "integer", + "name": "tabId", + "minimum": 0, + "optional": true, + "description": "The ID of the tab to navigate backward." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [] + } + ] + } + ], + "events": [ + { + "name": "onCreated", + "type": "function", + "description": "Fired when a tab is created. Note that the tab's URL may not be set at the time this event fired, but you can listen to onUpdated events to be notified when a URL is set.", + "parameters": [ + { + "$ref": "Tab", + "name": "tab", + "description": "Details of the tab that was created." + } + ] + }, + { + "name": "onUpdated", + "type": "function", + "description": "Fired when a tab is updated.", + "parameters": [ + { "type": "integer", "name": "tabId", "minimum": 0 }, + { + "type": "object", + "name": "changeInfo", + "description": "Lists the changes to the state of the tab that was updated.", + "properties": { + "status": { + "type": "string", + "optional": true, + "description": "The status of the tab. Can be either loading or complete." + }, + "discarded": { + "type": "boolean", + "optional": true, + "description": "True while the tab is not loaded with content." + }, + "url": { + "type": "string", + "optional": true, + "description": "The tab's URL if it has changed." + }, + "pinned": { + "type": "boolean", + "optional": true, + "description": "The tab's new pinned state." + }, + "audible": { + "type": "boolean", + "optional": true, + "description": "The tab's new audible state." + }, + "mutedInfo": { + "$ref": "MutedInfo", + "optional": true, + "description": "The tab's new muted state and the reason for the change." + }, + "favIconUrl": { + "type": "string", + "optional": true, + "description": "The tab's new favicon URL." + } + } + }, + { + "$ref": "Tab", + "name": "tab", + "description": "Gives the state of the tab that was updated." + } + ] + }, + { + "name": "onMoved", + "type": "function", + "description": "Fired when a tab is moved within a window. Only one move event is fired, representing the tab the user directly moved. Move events are not fired for the other tabs that must move in response. This event is not fired when a tab is moved between windows. For that, see $(ref:tabs.onDetached).", + "parameters": [ + { "type": "integer", "name": "tabId", "minimum": 0 }, + { + "type": "object", + "name": "moveInfo", + "properties": { + "windowId": { "type": "integer", "minimum": 0 }, + "fromIndex": { "type": "integer", "minimum": 0 }, + "toIndex": { "type": "integer", "minimum": 0 } + } + } + ] + }, + { + "name": "onActivated", + "type": "function", + "description": "Fires when the active tab in a window changes. Note that the tab's URL may not be set at the time this event fired, but you can listen to onUpdated events to be notified when a URL is set.", + "parameters": [ + { + "type": "object", + "name": "activeInfo", + "properties": { + "tabId": { + "type": "integer", + "minimum": 0, + "description": "The ID of the tab that has become active." + }, + "previousTabId": { + "type": "integer", + "minimum": 0, + "optional": true, + "description": "The ID of the tab that was previously active, if that tab is still open." + }, + "windowId": { + "type": "integer", + "minimum": 0, + "description": "The ID of the window the active tab changed inside of." + } + } + } + ] + }, + { + "name": "onHighlighted", + "type": "function", + "description": "Fired when the highlighted or selected tabs in a window changes.", + "parameters": [ + { + "type": "object", + "name": "highlightInfo", + "properties": { + "windowId": { + "type": "integer", + "minimum": 0, + "description": "The window whose tabs changed." + }, + "tabIds": { + "type": "array", + "items": { "type": "integer", "minimum": 0 }, + "description": "All highlighted tabs in the window." + } + } + } + ] + }, + { + "name": "onDetached", + "type": "function", + "description": "Fired when a tab is detached from a window, for example because it is being moved between windows.", + "parameters": [ + { "type": "integer", "name": "tabId", "minimum": 0 }, + { + "type": "object", + "name": "detachInfo", + "properties": { + "oldWindowId": { "type": "integer", "minimum": 0 }, + "oldPosition": { "type": "integer", "minimum": 0 } + } + } + ] + }, + { + "name": "onAttached", + "type": "function", + "description": "Fired when a tab is attached to a window, for example because it was moved between windows.", + "parameters": [ + { "type": "integer", "name": "tabId", "minimum": 0 }, + { + "type": "object", + "name": "attachInfo", + "properties": { + "newWindowId": { "type": "integer", "minimum": 0 }, + "newPosition": { "type": "integer", "minimum": 0 } + } + } + ] + }, + { + "name": "onRemoved", + "type": "function", + "description": "Fired when a tab is closed.", + "parameters": [ + { "type": "integer", "name": "tabId", "minimum": 0 }, + { + "type": "object", + "name": "removeInfo", + "properties": { + "windowId": { + "type": "integer", + "minimum": 0, + "description": "The window whose tab is closed." + }, + "isWindowClosing": { + "type": "boolean", + "description": "True when the tab is being closed because its window is being closed." + } + } + } + ] + }, + { + "name": "onReplaced", + "type": "function", + "description": "Fired when a tab is replaced with another tab due to prerendering or instant.", + "parameters": [ + { "type": "integer", "name": "addedTabId", "minimum": 0 }, + { "type": "integer", "name": "removedTabId", "minimum": 0 } + ] + }, + { + "name": "onZoomChange", + "unsupported": true, + "type": "function", + "description": "Fired when a tab is zoomed.", + "parameters": [ + { + "type": "object", + "name": "ZoomChangeInfo", + "properties": { + "tabId": { "type": "integer", "minimum": 0 }, + "oldZoomFactor": { "type": "number" }, + "newZoomFactor": { "type": "number" }, + "zoomSettings": { "$ref": "ZoomSettings" } + } + } + ] + } + ] + } +] diff --git a/mobile/android/components/extensions/test/mochitest/.eslintrc.js b/mobile/android/components/extensions/test/mochitest/.eslintrc.js new file mode 100644 index 0000000000..7d6fe2eb1a --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + extends: + "../../../../../../toolkit/components/extensions/test/mochitest/.eslintrc.js", +}; diff --git a/mobile/android/components/extensions/test/mochitest/chrome.ini b/mobile/android/components/extensions/test/mochitest/chrome.ini new file mode 100644 index 0000000000..901c516d0a --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/chrome.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + head.js + ../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js +tags = webextensions + +[test_ext_options_ui.html] diff --git a/mobile/android/components/extensions/test/mochitest/context.html b/mobile/android/components/extensions/test/mochitest/context.html new file mode 100644 index 0000000000..1e25c6e851 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/context.html @@ -0,0 +1,24 @@ + + + + + + just some text 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890 + + +

+ Some link +

+ +

+ + + +

+ +

+
+ +

+ + diff --git a/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html new file mode 100644 index 0000000000..1e2afec6fa --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_iframe.html @@ -0,0 +1,22 @@ + + + + + +

test iframe

+ + + diff --git a/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html new file mode 100644 index 0000000000..3fa93979fa --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/context_tabs_onUpdated_page.html @@ -0,0 +1,21 @@ + + + + + +

test page

+ + + + diff --git a/mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs b/mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs new file mode 100644 index 0000000000..eed8a6ef49 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_bypass_cache.sjs @@ -0,0 +1,13 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/plain; charset=UTF-8", false); + + if (request.hasHeader("pragma") && request.hasHeader("cache-control")) { + response.write( + `${request.getHeader("pragma")}:${request.getHeader("cache-control")}` + ); + } +} diff --git a/mobile/android/components/extensions/test/mochitest/file_dummy.html b/mobile/android/components/extensions/test/mochitest/file_dummy.html new file mode 100644 index 0000000000..49ad37128d --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_dummy.html @@ -0,0 +1,10 @@ + + + +Dummy test page + + + +

Dummy test page

+ + diff --git a/mobile/android/components/extensions/test/mochitest/file_iframe_document.html b/mobile/android/components/extensions/test/mochitest/file_iframe_document.html new file mode 100644 index 0000000000..3bb2bd5dcf --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_iframe_document.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs b/mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs new file mode 100644 index 0000000000..3816cf045b --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/file_slowed_document.sjs @@ -0,0 +1,49 @@ +/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sts=2 sw=2 et tw=80 ft=javascript: */ +"use strict"; + +// This script slows the load of an HTML document so that we can reliably test +// all phases of the load cycle supported by the extension API. + +/* eslint-disable no-unused-vars */ + +const URL = "file_slowed_document.sjs"; + +const DELAY = 2 * 1000; // Delay one second before completing the request. + +const nsTimer = Components.Constructor( + "@mozilla.org/timer;1", + "nsITimer", + "initWithCallback" +); + +let timer; + +function handleRequest(request, response) { + response.processAsync(); + + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write(` + + + + + + + `); + + // Note: We need to store a reference to the timer to prevent it from being + // canceled when it's GCed. + timer = new nsTimer( + () => { + if (request.queryString.includes("with-iframe")) { + response.write(``); + } + response.write(``); + response.finish(); + }, + DELAY, + Ci.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/mobile/android/components/extensions/test/mochitest/mochitest.ini b/mobile/android/components/extensions/test/mochitest/mochitest.ini new file mode 100644 index 0000000000..a8cadf7b5d --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini @@ -0,0 +1,41 @@ +[DEFAULT] +support-files = + ../../../../../../toolkit/components/extensions/test/mochitest/test_ext_all_apis.js + ../../../../../../toolkit/components/extensions/test/mochitest/file_sample.html + ../../../../../../toolkit/components/extensions/test/mochitest/chrome_cleanup_script.js + context.html + context_tabs_onUpdated_iframe.html + context_tabs_onUpdated_page.html + file_bypass_cache.sjs + file_dummy.html + file_iframe_document.html + file_slowed_document.sjs + head.js +tags = webextensions +prefs = + javascript.options.asyncstack_capture_debuggee_only=false + +[test_ext_all_apis.html] +[test_ext_downloads_event_page.html] +[test_ext_tab_runtimeConnect.html] +[test_ext_tabs_create.html] +[test_ext_tabs_events.html] +skip-if = + fission # Bug 1827754 +[test_ext_tabs_executeScript.html] +[test_ext_tabs_executeScript_bad.html] +[test_ext_tabs_executeScript_no_create.html] +[test_ext_tabs_executeScript_runAt.html] +[test_ext_tabs_get.html] +[test_ext_tabs_getCurrent.html] +[test_ext_tabs_goBack_goForward.html] +[test_ext_tabs_insertCSS.html] +[test_ext_tabs_lastAccessed.html] +skip-if = true # tab.lastAccessed not implemented +[test_ext_tabs_reload.html] +[test_ext_tabs_reload_bypass_cache.html] +[test_ext_tabs_onUpdated.html] +[test_ext_tabs_query.html] +[test_ext_tabs_sendMessage.html] +[test_ext_tabs_update_url.html] +[test_ext_webNavigation_onCommitted.html] diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html new file mode 100644 index 0000000000..c1220ff4a5 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_all_apis.html @@ -0,0 +1,50 @@ + + + + WebExtension test + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html b/mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html new file mode 100644 index 0000000000..78691114df --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_downloads_event_page.html @@ -0,0 +1,102 @@ + + + + + Downloads Events Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html new file mode 100644 index 0000000000..138bb054a9 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_options_ui.html @@ -0,0 +1,498 @@ + + + + + PageAction Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html b/mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html new file mode 100644 index 0000000000..48904c2990 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tab_runtimeConnect.html @@ -0,0 +1,89 @@ + + + + + Tabs runtimeConnect Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html new file mode 100644 index 0000000000..027b231fa7 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_create.html @@ -0,0 +1,153 @@ + + + + + Tabs create Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html new file mode 100644 index 0000000000..cd708d942a --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html @@ -0,0 +1,302 @@ + + + + + Tabs Events Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html new file mode 100644 index 0000000000..09e42d73cf --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript.html @@ -0,0 +1,252 @@ + + + + + Tabs executeScript Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html new file mode 100644 index 0000000000..da645ef738 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_bad.html @@ -0,0 +1,151 @@ + + + + + Tabs executeScript Bad Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html new file mode 100644 index 0000000000..fa25568619 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_no_create.html @@ -0,0 +1,83 @@ + + + + + Tabs executeScript noCreate Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html new file mode 100644 index 0000000000..2e82320f8c --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_executeScript_runAt.html @@ -0,0 +1,128 @@ + + + + + Tabs executeScript runAt Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html new file mode 100644 index 0000000000..109ab8f65c --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_get.html @@ -0,0 +1,36 @@ + + + + + Tabs get Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html new file mode 100644 index 0000000000..c32f93f44a --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_getCurrent.html @@ -0,0 +1,70 @@ + + + + + Tabs getCurrent Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html new file mode 100644 index 0000000000..0d143e2ac6 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_goBack_goForward.html @@ -0,0 +1,134 @@ + + + + + Tabs goBack and goForward Test + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html new file mode 100644 index 0000000000..718c2d6de4 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html @@ -0,0 +1,124 @@ + + + + + Tabs executeScript Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html new file mode 100644 index 0000000000..5bb44ab645 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_lastAccessed.html @@ -0,0 +1,64 @@ + + + + + Tabs lastAccessed Test + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html new file mode 100644 index 0000000000..8d96e79cc2 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_onUpdated.html @@ -0,0 +1,187 @@ + + + + + Tabs onUpdated Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html new file mode 100644 index 0000000000..9a907f47de --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_query.html @@ -0,0 +1,46 @@ + + + + + Tabs create Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html new file mode 100644 index 0000000000..30379f02a1 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload.html @@ -0,0 +1,66 @@ + + + + + Tabs reload Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html new file mode 100644 index 0000000000..87f90ad855 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_reload_bypass_cache.html @@ -0,0 +1,87 @@ + + + + + Tabs executeScript bypassCache Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html new file mode 100644 index 0000000000..320ce4dde6 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_sendMessage.html @@ -0,0 +1,277 @@ + + + + + Tabs sendMessage Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html new file mode 100644 index 0000000000..9332efd516 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_update_url.html @@ -0,0 +1,125 @@ + + + + + Tabs update Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html b/mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html new file mode 100644 index 0000000000..33f178492d --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_webNavigation_onCommitted.html @@ -0,0 +1,50 @@ + + + + + WebNavigation onCommitted Test + + + + + + + + + + + diff --git a/mobile/android/components/extensions/test/xpcshell/.eslintrc.js b/mobile/android/components/extensions/test/xpcshell/.eslintrc.js new file mode 100644 index 0000000000..2e6d214f4b --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/.eslintrc.js @@ -0,0 +1,6 @@ +"use strict"; + +module.exports = { + extends: + "../../../../../../toolkit/components/extensions/test/xpcshell/.eslintrc.js", +}; diff --git a/mobile/android/components/extensions/test/xpcshell/head.js b/mobile/android/components/extensions/test/xpcshell/head.js new file mode 100644 index 0000000000..e79781fba6 --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/head.js @@ -0,0 +1,24 @@ +"use strict"; + +ChromeUtils.defineESModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs", + ExtensionTestUtils: + "resource://testing-common/ExtensionXPCShellUtils.sys.mjs", +}); + +// Remove this pref once bug 1535365 is fixed. +Services.prefs.setBoolPref("extensions.webextensions.remote", false); + +// https_first automatically upgrades http to https, but the tests are not +// designed to expect that. And it is not easy to change that because +// nsHttpServer does not support https (bug 1742061). So disable https_first. +Services.prefs.setBoolPref("dom.security.https_first", false); + +ExtensionTestUtils.init(this); + +Services.io.offline = true; + +var createHttpServer = (...args) => { + AddonTestUtils.maybeInit(this); + return AddonTestUtils.createHttpServer(...args); +}; diff --git a/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js new file mode 100644 index 0000000000..63f64b487e --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js @@ -0,0 +1,167 @@ +"use strict"; + +const server = createHttpServer({ hosts: ["example.com"] }); +server.registerPathHandler("/dum", (request, response) => { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html; charset=utf-8", false); + response.write(""); +}); + +async function testNativeMessaging({ + isPrivileged = false, + permissions, + testBackground, + testContent, +}) { + async function runTest(testFn, completionMessage) { + try { + dump(`Running test before sending ${completionMessage}\n`); + await testFn(); + } catch (e) { + browser.test.fail(`Unexpected error: ${e}`); + } + browser.test.sendMessage(completionMessage); + } + const extension = ExtensionTestUtils.loadExtension({ + isPrivileged, + background: `(${runTest})(${testBackground}, "background_done");`, + manifest: { + content_scripts: [ + { + run_at: "document_end", + js: ["test.js"], + matches: ["http://example.com/dummy"], + }, + ], + permissions, + }, + files: { + "test.js": `(${runTest})(${testContent}, "content_done");`, + }, + }); + + // Run background script. + await extension.startup(); + await extension.awaitMessage("background_done"); + + // Run content script. + const page = await ExtensionTestUtils.loadContentPage( + "http://example.com/dummy" + ); + await extension.awaitMessage("content_done"); + await page.close(); + + await extension.unload(); +} + +// Checks that unprivileged extensions cannot use any of the nativeMessaging +// APIs on Android. +add_task(async function test_nativeMessaging_unprivileged() { + function testScript() { + browser.test.assertEq( + browser.runtime.connectNative, + undefined, + "connectNative should not be available in unprivileged extensions" + ); + browser.test.assertEq( + browser.runtime.sendNativeMessage, + undefined, + "sendNativeMessage should not be available in unprivileged extensions" + ); + } + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + await testNativeMessaging({ + isPrivileged: false, + permissions: [ + "geckoViewAddons", + "nativeMessaging", + "nativeMessagingFromContent", + ], + testBackground: testScript, + testContent: testScript, + }); + }); + AddonTestUtils.checkMessages(messages, { + expected: [ + { message: /Invalid extension permission: geckoViewAddons/ }, + { message: /Invalid extension permission: nativeMessaging/ }, + { message: /Invalid extension permission: nativeMessagingFromContent/ }, + ], + }); +}); + +// Checks that privileged extensions can still not use native messaging without +// the geckoViewAddons permission. +add_task(async function test_geckoViewAddons_missing() { + const ERROR_NATIVE_MESSAGE_FROM_BACKGROUND = + "Native manifests are not supported on android"; + const ERROR_NATIVE_MESSAGE_FROM_CONTENT = + /^Native messaging not allowed: \{.*"envType":"content_child","url":"http:\/\/example\.com\/dummy"\}$/; + + async function testBackground() { + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"), + // Redacted error: ERROR_NATIVE_MESSAGE_FROM_BACKGROUND + "An unexpected error occurred", + "Background script cannot use nativeMessaging without geckoViewAddons" + ); + } + async function testContent() { + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"), + // Redacted error: ERROR_NATIVE_MESSAGE_FROM_CONTENT + "An unexpected error occurred", + "Content script cannot use nativeMessaging without geckoViewAddons" + ); + } + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + await testNativeMessaging({ + isPrivileged: true, + permissions: ["nativeMessaging", "nativeMessagingFromContent"], + testBackground, + testContent, + }); + }); + AddonTestUtils.checkMessages(messages, { + expected: [ + { errorMessage: ERROR_NATIVE_MESSAGE_FROM_BACKGROUND }, + { errorMessage: ERROR_NATIVE_MESSAGE_FROM_CONTENT }, + ], + }); +}); + +// Checks that privileged extensions cannot use native messaging from content +// without the nativeMessagingFromContent permission. +add_task(async function test_nativeMessagingFromContent_missing() { + const ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM = + /^Unexpected messaging sender: \{.*"envType":"content_child","url":"http:\/\/example\.com\/dummy"\}$/; + function testBackground() { + // sendNativeMessage / connectNative are expected to succeed, but we + // are not testing that here because XpcshellTestRunnerService does not + // have a WebExtension.MessageDelegate that handles the message. + // There are plenty of mochitests that rely on connectNative, so we are + // not testing that here. + } + async function testContent() { + await browser.test.assertRejects( + browser.runtime.sendNativeMessage("dummy_nativeApp", "DummyMsg"), + // Redacted error: ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM + "An unexpected error occurred", + "Trying to get through to native messaging but without luck" + ); + } + + const { messages } = await AddonTestUtils.promiseConsoleOutput(async () => { + await testNativeMessaging({ + isPrivileged: true, + permissions: ["geckoViewAddons", "nativeMessaging"], + testBackground, + testContent, + }); + }); + AddonTestUtils.checkMessages(messages, { + expected: [{ errorMessage: ERROR_NATIVE_MESSAGE_FROM_CONTENT_NO_PERM }], + }); +}); diff --git a/mobile/android/components/extensions/test/xpcshell/xpcshell.ini b/mobile/android/components/extensions/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..f004140076 --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/xpcshell.ini @@ -0,0 +1,7 @@ +[DEFAULT] +head = head.js +firefox-appdir = browser +tags = webextensions in-process-webextensions +skip-if = os != "android" + +[test_ext_native_messaging_permissions.js] -- cgit v1.2.3