From 43a97878ce14b72f0981164f87f2e35e14151312 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 11:22:09 +0200 Subject: Adding upstream version 110.0.1. Signed-off-by: Daniel Baumann --- mobile/android/components/extensions/.eslintrc.js | 9 + .../extensions/ExtensionBrowsingData.jsm | 64 + .../android/components/extensions/ext-android.js | 637 ++++++++++ .../android/components/extensions/ext-android.json | 34 + .../components/extensions/ext-browserAction.js | 194 +++ .../android/components/extensions/ext-c-android.js | 13 + mobile/android/components/extensions/ext-c-tabs.js | 23 + .../android/components/extensions/ext-downloads.js | 300 +++++ .../components/extensions/ext-pageAction.js | 156 +++ mobile/android/components/extensions/ext-tabs.js | 554 +++++++++ .../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 | 17 + .../android/components/extensions/schemas/jar.mn | 7 + .../components/extensions/schemas/moz.build | 7 + .../components/extensions/schemas/tabs.json | 1263 ++++++++++++++++++++ .../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 + .../components/extensions/test/mochitest/head.js | 36 + .../extensions/test/mochitest/mochitest.ini | 38 + .../test/mochitest/test_ext_all_apis.html | 50 + .../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 | 239 ++++ .../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 | 117 ++ .../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 + .../components/extensions/test/xpcshell/head.js | 30 + .../test_ext_native_messaging_permissions.js | 165 +++ .../extensions/test/xpcshell/xpcshell.ini | 7 + .../components/geckoview/ColorPickerDelegate.jsm | 43 + .../components/geckoview/FilePickerDelegate.jsm | 194 +++ .../components/geckoview/GeckoView.manifest | 4 + .../geckoview/GeckoViewExternalAppService.cpp | 100 ++ .../geckoview/GeckoViewExternalAppService.h | 26 + .../components/geckoview/GeckoViewHistory.cpp | 508 ++++++++ .../components/geckoview/GeckoViewHistory.h | 60 + .../components/geckoview/GeckoViewOutputStream.cpp | 56 + .../components/geckoview/GeckoViewOutputStream.h | 29 + .../components/geckoview/GeckoViewPermission.jsm | 41 + .../components/geckoview/GeckoViewPrompt.jsm | 820 +++++++++++++ .../components/geckoview/GeckoViewPrompter.sys.mjs | 208 ++++ .../android/components/geckoview/GeckoViewPush.jsm | 257 ++++ .../components/geckoview/GeckoViewStartup.jsm | 333 ++++++ .../geckoview/GeckoViewStreamListener.cpp | 298 +++++ .../components/geckoview/GeckoViewStreamListener.h | 57 + .../components/geckoview/LoginStorageDelegate.jsm | 138 +++ .../components/geckoview/PromptCollection.jsm | 48 + .../android/components/geckoview/ShareDelegate.jsm | 82 ++ .../android/components/geckoview/components.conf | 93 ++ mobile/android/components/geckoview/moz.build | 49 + mobile/android/components/moz.build | 16 + 74 files changed, 10119 insertions(+) create mode 100644 mobile/android/components/extensions/.eslintrc.js create mode 100644 mobile/android/components/extensions/ExtensionBrowsingData.jsm 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/head.js 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_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/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 create mode 100644 mobile/android/components/geckoview/ColorPickerDelegate.jsm create mode 100644 mobile/android/components/geckoview/FilePickerDelegate.jsm create mode 100644 mobile/android/components/geckoview/GeckoView.manifest create mode 100644 mobile/android/components/geckoview/GeckoViewExternalAppService.cpp create mode 100644 mobile/android/components/geckoview/GeckoViewExternalAppService.h create mode 100644 mobile/android/components/geckoview/GeckoViewHistory.cpp create mode 100644 mobile/android/components/geckoview/GeckoViewHistory.h create mode 100644 mobile/android/components/geckoview/GeckoViewOutputStream.cpp create mode 100644 mobile/android/components/geckoview/GeckoViewOutputStream.h create mode 100644 mobile/android/components/geckoview/GeckoViewPermission.jsm create mode 100644 mobile/android/components/geckoview/GeckoViewPrompt.jsm create mode 100644 mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs create mode 100644 mobile/android/components/geckoview/GeckoViewPush.jsm create mode 100644 mobile/android/components/geckoview/GeckoViewStartup.jsm create mode 100644 mobile/android/components/geckoview/GeckoViewStreamListener.cpp create mode 100644 mobile/android/components/geckoview/GeckoViewStreamListener.h create mode 100644 mobile/android/components/geckoview/LoginStorageDelegate.jsm create mode 100644 mobile/android/components/geckoview/PromptCollection.jsm create mode 100644 mobile/android/components/geckoview/ShareDelegate.jsm create mode 100644 mobile/android/components/geckoview/components.conf create mode 100644 mobile/android/components/geckoview/moz.build create mode 100644 mobile/android/components/moz.build (limited to 'mobile/android/components') 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.jsm b/mobile/android/components/extensions/ExtensionBrowsingData.jsm new file mode 100644 index 0000000000..47321afe7a --- /dev/null +++ b/mobile/android/components/extensions/ExtensionBrowsingData.jsm @@ -0,0 +1,64 @@ +/* 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"; + +const EXPORTED_SYMBOLS = ["BrowsingDataDelegate"]; + +const { ExtensionUtils } = ChromeUtils.import( + "resource://gre/modules/ExtensionUtils.jsm" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", +}); + +const { ExtensionError } = ExtensionUtils; + +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..c782c36541 --- /dev/null +++ b/mobile/android/components/extensions/ext-android.js @@ -0,0 +1,637 @@ +/* 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, { + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +ChromeUtils.defineModuleGetter( + this, + "GeckoViewTabBridge", + "resource://gre/modules/GeckoViewTab.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "mobileWindowTracker", + "resource://gre/modules/GeckoViewWebExtension.jsm" +); + +var { EventDispatcher } = ChromeUtils.importESModule( + "resource://gre/modules/Messaging.sys.mjs" +); + +var { ExtensionCommon } = ChromeUtils.import( + "resource://gre/modules/ExtensionCommon.jsm" +); +var { ExtensionUtils } = ChromeUtils.import( + "resource://gre/modules/ExtensionUtils.jsm" +); + +var { DefaultWeakMap, ExtensionError } = ExtensionUtils; + +var { defineLazyGetter } = ExtensionCommon; + +global.GlobalEventDispatcher = EventDispatcher.instance; + +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); + }, + }; + + GlobalEventDispatcher.registerListener(listener2, [event]); + return () => { + GlobalEventDispatcher.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, browser } = window; + const { windowId, tabId } = this.getBrowserData(browser); + this.emit("tab-removed", { + tab, + 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.loadURI(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..bafc8fe00a --- /dev/null +++ b/mobile/android/components/extensions/ext-android.json @@ -0,0 +1,34 @@ +{ + "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..a0d9428d05 --- /dev/null +++ b/mobile/android/components/extensions/ext-browserAction.js @@ -0,0 +1,194 @@ +/* -*- 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"; + +// The ext-* files are imported into the same scopes. +/* import-globals-from ext-android.js */ + +XPCOMUtils.defineLazyModuleGetters(this, { + GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm", + ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.jsm", +}); + +const { BrowserActionBase } = ChromeUtils.import( + "resource://gre/modules/ExtensionActions.jsm" +); + +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..56fd58c3e2 --- /dev/null +++ b/mobile/android/components/extensions/ext-downloads.js @@ -0,0 +1,300 @@ +/* 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", +}); +XPCOMUtils.defineLazyModuleGetters(this, { + DownloadTracker: "resource://gre/modules/GeckoViewWebExtension.jsm", +}); + +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, + name: "downloads.onChanged", + register: fire => { + const listener = (eventName, event) => { + const { delta, downloadItem } = event; + if (context.privateBrowsingAllowed || !downloadItem.incognito) { + fire.async(delta); + } + }; + + DownloadTracker.on("download-changed", listener); + return () => { + DownloadTracker.off("download-changed", listener); + }; + }, + }).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..fceb5e1c0f --- /dev/null +++ b/mobile/android/components/extensions/ext-pageAction.js @@ -0,0 +1,156 @@ +/* -*- 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"; + +// The ext-* files are imported into the same scopes. +/* import-globals-from ext-android.js */ + +XPCOMUtils.defineLazyModuleGetters(this, { + GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm", + ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.jsm", +}); + +const { PageActionBase } = ChromeUtils.import( + "resource://gre/modules/ExtensionActions.jsm" +); + +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..7fd373a8ea --- /dev/null +++ b/mobile/android/components/extensions/ext-tabs.js @@ -0,0 +1,554 @@ +/* -*- 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, { + PromiseUtils: "resource://gre/modules/PromiseUtils.sys.mjs", +}); + +ChromeUtils.defineModuleGetter( + this, + "GeckoViewTabBridge", + "resource://gre/modules/GeckoViewTab.jsm" +); + +ChromeUtils.defineModuleGetter( + this, + "mobileWindowTracker", + "resource://gre/modules/GeckoViewWebExtension.jsm" +); + +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 ExtensionAPI { + getAPI(context) { + const { extension } = context; + + const { tabManager } = extension; + + 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.loadURI(url, { + flags, + triggeringPrincipal: principal, + }); + } + + const self = { + tabs: { + onActivated: new EventManager({ + context, + name: "tabs.onActivated", + register: fire => { + const listener = (eventName, event) => { + const { windowId, tabId, isPrivate } = event; + if (isPrivate && !context.privateBrowsingAllowed) { + return; + } + fire.async({ + windowId, + tabId, + // In GeckoView each window has only one tab, so previousTabId is omitted. + }); + }; + + mobileWindowTracker.on("tab-activated", listener); + return () => { + mobileWindowTracker.off("tab-activated", listener); + }; + }, + }).api(), + + onCreated: new EventManager({ + context, + name: "tabs.onCreated", + register: fire => { + const listener = (eventName, event) => { + fire.async(tabManager.convert(event.nativeTab)); + }; + + tabTracker.on("tab-created", listener); + return () => { + tabTracker.off("tab-created", listener); + }; + }, + }).api(), + + /** + * Since multiple tabs currently can't be highlighted, onHighlighted + * essentially acts an alias for self.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 }); + } + ), + + 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, + name: "tabs.onRemoved", + register: fire => { + const listener = (eventName, event) => { + fire.async(event.tabId, { + windowId: event.windowId, + isWindowClosing: event.isWindowClosing, + }); + }; + + tabTracker.on("tab-removed", listener); + return () => { + tabTracker.off("tab-removed", listener); + }; + }, + }).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, + name: "tabs.onUpdated", + register: fire => { + 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 () => { + windowTracker.removeListener("status", statusListener); + windowTracker.removeListener("pagetitlechanged", listener); + }; + }, + }).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 window = browser.ownerGlobal; + const zoom = window.windowUtils.fullZoom; + + const tab = tabManager.wrapTab(nativeTab); + return tab.capture(context, zoom, 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.windowUtils.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(); + }, + }, + }; + return self; + } +}; 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..acef8642b1 --- /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.jsm", +] + +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..41ee288ef7 --- /dev/null +++ b/mobile/android/components/extensions/schemas/gecko_view_addons.json @@ -0,0 +1,17 @@ +[ + { + "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..173123a351 --- /dev/null +++ b/mobile/android/components/extensions/schemas/tabs.json @@ -0,0 +1,1263 @@ +// 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/head.js b/mobile/android/components/extensions/test/mochitest/head.js new file mode 100644 index 0000000000..14bb7b663f --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/head.js @@ -0,0 +1,36 @@ +"use strict"; + +/* exported AppConstants, TEST_ICON_ARRAYBUFFER */ + +var { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +var TEST_ICON_DATA = + "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC"; + +var TEST_ICON_ARRAYBUFFER = Uint8Array.from(atob(TEST_ICON_DATA), byte => + byte.charCodeAt(0) +).buffer; + +{ + const chromeScript = SpecialPowers.loadChromeScript( + SimpleTest.getTestFileURL("chrome_cleanup_script.js") + ); + + SimpleTest.registerCleanupFunction(async () => { + await new Promise(resolve => setTimeout(resolve, 0)); + + chromeScript.sendAsyncMessage("check-cleanup"); + + const results = await chromeScript.promiseOneMessage("cleanup-results"); + chromeScript.destroy(); + + if (results.extraWindows.length || results.extraTabs.length) { + ok( + false, + `Test left extra windows or tabs: ${JSON.stringify(results)}\n` + ); + } + }); +} 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..be07e0f35f --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/mochitest.ini @@ -0,0 +1,38 @@ +[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_tab_runtimeConnect.html] +[test_ext_tabs_create.html] +[test_ext_tabs_events.html] +[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 = !is_fennec # 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_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..d91b8f015f --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_events.html @@ -0,0 +1,239 @@ + + + + + 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..26376ebf55 --- /dev/null +++ b/mobile/android/components/extensions/test/mochitest/test_ext_tabs_insertCSS.html @@ -0,0 +1,117 @@ + + + + + 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/head.js b/mobile/android/components/extensions/test/xpcshell/head.js new file mode 100644 index 0000000000..a07a90cff9 --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/head.js @@ -0,0 +1,30 @@ +"use strict"; + +/* exported createHttpServer */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// eslint-disable-next-line no-unused-vars +XPCOMUtils.defineLazyModuleGetters(this, { + AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm", + ExtensionTestUtils: "resource://testing-common/ExtensionXPCShellUtils.jsm", +}); + +// 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..2c7ac3cc2f --- /dev/null +++ b/mobile/android/components/extensions/test/xpcshell/test_ext_native_messaging_permissions.js @@ -0,0 +1,165 @@ +"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] diff --git a/mobile/android/components/geckoview/ColorPickerDelegate.jsm b/mobile/android/components/geckoview/ColorPickerDelegate.jsm new file mode 100644 index 0000000000..b72662a316 --- /dev/null +++ b/mobile/android/components/geckoview/ColorPickerDelegate.jsm @@ -0,0 +1,43 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["ColorPickerDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("ColorPickerDelegate"); + +class ColorPickerDelegate { + // TODO(bug 1805397): Implement default colors + init(aParent, aTitle, aInitialColor, aDefaultColors) { + this._prompt = new lazy.GeckoViewPrompter(aParent); + this._msg = { + type: "color", + title: aTitle, + value: aInitialColor, + predefinedValues: aDefaultColors, + }; + } + + open(aColorPickerShownCallback) { + this._prompt.asyncShowPrompt(this._msg, result => { + // OK: result + // Cancel: !result + aColorPickerShownCallback.done((result && result.color) || ""); + }); + } +} + +ColorPickerDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIColorPicker", +]); diff --git a/mobile/android/components/geckoview/FilePickerDelegate.jsm b/mobile/android/components/geckoview/FilePickerDelegate.jsm new file mode 100644 index 0000000000..32966b65e6 --- /dev/null +++ b/mobile/android/components/geckoview/FilePickerDelegate.jsm @@ -0,0 +1,194 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["FilePickerDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("FilePickerDelegate"); + +class FilePickerDelegate { + /* ---------- nsIFilePicker ---------- */ + init(aParent, aTitle, aMode) { + if ( + aMode === Ci.nsIFilePicker.modeGetFolder || + aMode === Ci.nsIFilePicker.modeSave + ) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + this._prompt = new lazy.GeckoViewPrompter(aParent); + this._msg = { + type: "file", + title: aTitle, + mode: aMode === Ci.nsIFilePicker.modeOpenMultiple ? "multiple" : "single", + }; + this._mode = aMode; + this._mimeTypes = []; + this._capture = 0; + } + + get mode() { + return this._mode; + } + + appendRawFilter(aFilter) { + this._mimeTypes.push(aFilter); + } + + show() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + open(aFilePickerShownCallback) { + this._msg.mimeTypes = this._mimeTypes; + this._msg.capture = this._capture; + this._prompt.asyncShowPrompt(this._msg, result => { + // OK: result + // Cancel: !result + if (!result || !result.files || !result.files.length) { + aFilePickerShownCallback.done(Ci.nsIFilePicker.returnCancel); + } else { + this._resolveFiles(result.files, aFilePickerShownCallback); + } + }); + } + + async _resolveFiles(aFiles, aCallback) { + const fileData = []; + + try { + for (const file of aFiles) { + const domFile = await this._getDOMFile(file); + fileData.push({ + file, + domFile, + }); + } + } catch (ex) { + warn`Error resolving files from file picker: ${ex}`; + aCallback.done(Ci.nsIFilePicker.returnCancel); + return; + } + + this._fileData = fileData; + aCallback.done(Ci.nsIFilePicker.returnOK); + } + + get file() { + if (!this._fileData) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + const fileData = this._fileData[0]; + if (!fileData) { + return null; + } + return new lazy.FileUtils.File(fileData.file); + } + + get fileURL() { + return Services.io.newFileURI(this.file); + } + + *_getEnumerator(aDOMFile) { + if (!this._fileData) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + + for (const fileData of this._fileData) { + if (aDOMFile) { + yield fileData.domFile; + } + yield new lazy.FileUtils.File(fileData.file); + } + } + + get files() { + return this._getEnumerator(/* aDOMFile */ false); + } + + _getDOMFile(aPath) { + if (this._prompt.domWin) { + return this._prompt.domWin.File.createFromFileName(aPath); + } + return File.createFromFileName(aPath); + } + + get domFileOrDirectory() { + if (!this._fileData) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + return this._fileData[0] ? this._fileData[0].domFile : null; + } + + get domFileOrDirectoryEnumerator() { + return this._getEnumerator(/* aDOMFile */ true); + } + + get defaultString() { + return ""; + } + + set defaultString(aValue) {} + + get defaultExtension() { + return ""; + } + + set defaultExtension(aValue) {} + + get filterIndex() { + return 0; + } + + set filterIndex(aValue) {} + + get displayDirectory() { + return null; + } + + set displayDirectory(aValue) {} + + get displaySpecialDirectory() { + return ""; + } + + set displaySpecialDirectory(aValue) {} + + get addToRecentDocs() { + return false; + } + + set addToRecentDocs(aValue) {} + + get okButtonLabel() { + return ""; + } + + set okButtonLabel(aValue) {} + + get capture() { + return this._capture; + } + + set capture(aValue) { + this._capture = aValue; + } +} + +FilePickerDelegate.prototype.classID = Components.ID( + "{e4565e36-f101-4bf5-950b-4be0887785a9}" +); +FilePickerDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIFilePicker", +]); diff --git a/mobile/android/components/geckoview/GeckoView.manifest b/mobile/android/components/geckoview/GeckoView.manifest new file mode 100644 index 0000000000..a26ffe2a05 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoView.manifest @@ -0,0 +1,4 @@ +# GeckoViewStartup.js +category app-startup GeckoViewStartup service,@mozilla.org/geckoview/startup;1 process=main +category content-process-ready-for-script GeckoViewStartup @mozilla.org/geckoview/startup;1 process=content +category profile-after-change GeckoViewStartup @mozilla.org/geckoview/startup;1 process=main diff --git a/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp new file mode 100644 index 0000000000..30dedc9251 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeckoViewExternalAppService.h" + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "nsIChannel.h" + +#include "mozilla/widget/EventDispatcher.h" +#include "mozilla/widget/nsWindow.h" +#include "GeckoViewStreamListener.h" + +#include "JavaBuiltins.h" + +class StreamListener final : public mozilla::GeckoViewStreamListener { + public: + explicit StreamListener(nsWindow* aWindow) + : GeckoViewStreamListener(), mWindow(aWindow) {} + + void SendWebResponse(mozilla::java::WebResponse::Param aResponse) { + mWindow->PassExternalResponse(aResponse); + } + + void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) { + // Currently we don't do anything about errors here + } + + virtual ~StreamListener() {} + + private: + RefPtr mWindow; +}; + +mozilla::StaticRefPtr + GeckoViewExternalAppService::sService; + +/* static */ +already_AddRefed +GeckoViewExternalAppService::GetSingleton() { + if (!sService) { + sService = new GeckoViewExternalAppService(); + } + RefPtr service = sService; + return service.forget(); +} + +GeckoViewExternalAppService::GeckoViewExternalAppService() {} + +NS_IMPL_ISUPPORTS(GeckoViewExternalAppService, nsIExternalHelperAppService); + +NS_IMETHODIMP GeckoViewExternalAppService::DoContent( + const nsACString& aMimeContentType, nsIRequest* aRequest, + nsIInterfaceRequestor* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP GeckoViewExternalAppService::CreateListener( + const nsACString& aMimeContentType, nsIRequest* aRequest, + mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + using namespace mozilla; + using namespace mozilla::dom; + MOZ_ASSERT(XRE_IsParentProcess()); + + nsresult rv; + nsCOMPtr channel(do_QueryInterface(aRequest, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr widget = + aContentContext->Canonical()->GetParentProcessWidgetContaining(); + if (!widget) { + return NS_ERROR_ABORT; + } + + RefPtr window = nsWindow::From(widget); + MOZ_ASSERT(window); + + RefPtr listener = new StreamListener(window); + + rv = channel->SetNotificationCallbacks(listener); + NS_ENSURE_SUCCESS(rv, rv); + + listener.forget(aStreamListener); + return NS_OK; +} + +NS_IMETHODIMP GeckoViewExternalAppService::ApplyDecodingForExtension( + const nsACString& aExtension, const nsACString& aEncodingType, + bool* aApplyDecoding) { + // This currently doesn't matter, because we never read the stream. + *aApplyDecoding = true; + return NS_OK; +} diff --git a/mobile/android/components/geckoview/GeckoViewExternalAppService.h b/mobile/android/components/geckoview/GeckoViewExternalAppService.h new file mode 100644 index 0000000000..1dfb7c9491 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GeckoViewExternalAppService_h__ +#define GeckoViewExternalAppService_h__ + +#include "nsIExternalHelperAppService.h" +#include "mozilla/StaticPtr.h" + +class GeckoViewExternalAppService : public nsIExternalHelperAppService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALHELPERAPPSERVICE + + GeckoViewExternalAppService(); + + static already_AddRefed GetSingleton(); + + private: + virtual ~GeckoViewExternalAppService() {} + static mozilla::StaticRefPtr sService; +}; + +#endif diff --git a/mobile/android/components/geckoview/GeckoViewHistory.cpp b/mobile/android/components/geckoview/GeckoViewHistory.cpp new file mode 100644 index 0000000000..c69e419b18 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewHistory.cpp @@ -0,0 +1,508 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeckoViewHistory.h" + +#include "JavaBuiltins.h" +#include "jsapi.h" +#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject +#include "js/PropertyAndElement.h" // JS_GetElement +#include "nsIURI.h" +#include "nsXULAppAPI.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/StaticPrefs_layout.h" + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Link.h" +#include "mozilla/dom/BrowserChild.h" + +#include "mozilla/ipc/URIUtils.h" + +#include "mozilla/widget/EventDispatcher.h" +#include "mozilla/widget/nsWindow.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; +using namespace mozilla::widget; + +static const char16_t kOnVisitedMessage[] = u"GeckoView:OnVisited"; +static const char16_t kGetVisitedMessage[] = u"GeckoView:GetVisited"; + +// Keep in sync with `GeckoSession.HistoryDelegate.VisitFlags`. +enum class GeckoViewVisitFlags : int32_t { + VISIT_TOP_LEVEL = 1 << 0, + VISIT_REDIRECT_TEMPORARY = 1 << 1, + VISIT_REDIRECT_PERMANENT = 1 << 2, + VISIT_REDIRECT_SOURCE = 1 << 3, + VISIT_REDIRECT_SOURCE_PERMANENT = 1 << 4, + VISIT_UNRECOVERABLE_ERROR = 1 << 5, +}; + +GeckoViewHistory::GeckoViewHistory() {} + +GeckoViewHistory::~GeckoViewHistory() {} + +NS_IMPL_ISUPPORTS(GeckoViewHistory, IHistory) + +StaticRefPtr GeckoViewHistory::sHistory; + +/* static */ +already_AddRefed GeckoViewHistory::GetSingleton() { + if (!sHistory) { + sHistory = new GeckoViewHistory(); + ClearOnShutdown(&sHistory); + } + RefPtr history = sHistory; + return history.forget(); +} + +// Handles a request to fetch visited statuses for new tracked URIs in the +// content process (e10s). +void GeckoViewHistory::QueryVisitedStateInContentProcess( + const PendingVisitedQueries& aQueries) { + // Holds an array of new tracked URIs for a tab in the content process. + struct NewURIEntry { + explicit NewURIEntry(BrowserChild* aBrowserChild, nsIURI* aURI) + : mBrowserChild(aBrowserChild) { + AddURI(aURI); + } + + void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); } + + BrowserChild* mBrowserChild; + nsTArray> mURIs; + }; + + MOZ_ASSERT(XRE_IsContentProcess()); + + // First, serialize all the new URIs that we need to look up. Note that this + // could be written as `nsTHashMap` instead, but, since we don't expect to have many tab + // children, we can avoid the cost of hashing. + AutoTArray newEntries; + for (auto& query : aQueries) { + nsIURI* uri = query.GetKey(); + MOZ_ASSERT(query.GetData().IsEmpty(), + "Shouldn't have parents to notify in child processes"); + auto entry = mTrackedURIs.Lookup(uri); + if (!entry) { + continue; + } + ObservingLinks& links = entry.Data(); + for (Link* link : links.mLinks.BackwardRange()) { + nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement()); + if (!widget) { + continue; + } + BrowserChild* browserChild = widget->GetOwningBrowserChild(); + if (!browserChild) { + continue; + } + // Add to the list of new URIs for this document, or make a new entry. + bool hasEntry = false; + for (NewURIEntry& entry : newEntries) { + if (entry.mBrowserChild == browserChild) { + entry.AddURI(uri); + hasEntry = true; + break; + } + } + if (!hasEntry) { + newEntries.AppendElement(NewURIEntry(browserChild, uri)); + } + } + } + + // Send the request to the parent process, one message per tab child. + for (const NewURIEntry& entry : newEntries) { + Unused << NS_WARN_IF( + !entry.mBrowserChild->SendQueryVisitedState(entry.mURIs)); + } +} + +// Handles a request to fetch visited statuses for new tracked URIs in the +// parent process (non-e10s). +void GeckoViewHistory::QueryVisitedStateInParentProcess( + const PendingVisitedQueries& aQueries) { + // Holds an array of new URIs for a window in the parent process. Unlike + // the content process case, we don't need to track tab children, since we + // have the outer window and can send the request directly to Java. + struct NewURIEntry { + explicit NewURIEntry(nsIWidget* aWidget, nsIURI* aURI) : mWidget(aWidget) { + AddURI(aURI); + } + + void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); } + + nsCOMPtr mWidget; + nsTArray> mURIs; + }; + + MOZ_ASSERT(XRE_IsParentProcess()); + + nsTArray newEntries; + for (const auto& query : aQueries) { + nsIURI* uri = query.GetKey(); + auto entry = mTrackedURIs.Lookup(uri); + if (!entry) { + continue; // Nobody cares about this uri anymore. + } + + ObservingLinks& links = entry.Data(); + nsTObserverArray::BackwardIterator linksIter(links.mLinks); + while (linksIter.HasMore()) { + Link* link = linksIter.GetNext(); + + nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement()); + if (!widget) { + continue; + } + + bool hasEntry = false; + for (NewURIEntry& entry : newEntries) { + if (entry.mWidget != widget) { + continue; + } + entry.AddURI(uri); + hasEntry = true; + } + if (!hasEntry) { + newEntries.AppendElement(NewURIEntry(widget, uri)); + } + } + } + + for (NewURIEntry& entry : newEntries) { + QueryVisitedState(entry.mWidget, nullptr, std::move(entry.mURIs)); + } +} + +void GeckoViewHistory::StartPendingVisitedQueries( + PendingVisitedQueries&& aQueries) { + if (XRE_IsContentProcess()) { + QueryVisitedStateInContentProcess(aQueries); + } else { + QueryVisitedStateInParentProcess(aQueries); + } +} + +/** + * Called from the session handler for the history delegate, after the new + * visit is recorded. + */ +class OnVisitedCallback final : public nsIAndroidEventCallback { + public: + explicit OnVisitedCallback(GeckoViewHistory* aHistory, nsIURI* aURI) + : mHistory(aHistory), mURI(aURI) {} + + NS_DECL_ISUPPORTS + + NS_IMETHOD + OnSuccess(JS::Handle aData, JSContext* aCx) override { + Maybe visitedState = GetVisitedValue(aCx, aData); + JS_ClearPendingException(aCx); + if (visitedState) { + AutoTArray visitedURIs; + visitedURIs.AppendElement(VisitedURI{mURI.get(), *visitedState}); + mHistory->HandleVisitedState(visitedURIs, nullptr); + } + return NS_OK; + } + + NS_IMETHOD + OnError(JS::Handle aData, JSContext* aCx) override { + return NS_OK; + } + + private: + virtual ~OnVisitedCallback() {} + + Maybe GetVisitedValue(JSContext* aCx, JS::Handle aData) { + if (NS_WARN_IF(!aData.isBoolean())) { + return Nothing(); + } + return Some(aData.toBoolean()); + } + + RefPtr mHistory; + nsCOMPtr mURI; +}; + +NS_IMPL_ISUPPORTS(OnVisitedCallback, nsIAndroidEventCallback) + +NS_IMETHODIMP +GeckoViewHistory::VisitURI(nsIWidget* aWidget, nsIURI* aURI, + nsIURI* aLastVisitedURI, uint32_t aFlags, + uint64_t aBrowserId) { + if (!aURI) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + // If we're in the content process, send the visit to the parent. The parent + // will find the matching chrome window for the content process and tab, + // then forward the visit to Java. + if (NS_WARN_IF(!aWidget)) { + return NS_OK; + } + BrowserChild* browserChild = aWidget->GetOwningBrowserChild(); + if (NS_WARN_IF(!browserChild)) { + return NS_OK; + } + Unused << NS_WARN_IF( + !browserChild->SendVisitURI(aURI, aLastVisitedURI, aFlags, aBrowserId)); + return NS_OK; + } + + // Otherwise, we're in the parent process. Wrap the URIs up in a bundle, and + // send them to Java. + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr window = nsWindow::From(aWidget); + if (NS_WARN_IF(!window)) { + return NS_OK; + } + widget::EventDispatcher* dispatcher = window->GetEventDispatcher(); + if (NS_WARN_IF(!dispatcher)) { + return NS_OK; + } + + // If nobody is listening for this, we can stop now. + if (!dispatcher->HasListener(kOnVisitedMessage)) { + return NS_OK; + } + + AutoTArray keys; + AutoTArray values; + + nsAutoCString uriSpec; + if (NS_WARN_IF(NS_FAILED(aURI->GetSpec(uriSpec)))) { + return NS_OK; + } + keys.AppendElement(jni::StringParam(u"url"_ns)); + values.AppendElement(jni::StringParam(uriSpec)); + + if (aLastVisitedURI) { + nsAutoCString lastVisitedURISpec; + if (NS_WARN_IF(NS_FAILED(aLastVisitedURI->GetSpec(lastVisitedURISpec)))) { + return NS_OK; + } + keys.AppendElement(jni::StringParam(u"lastVisitedURL"_ns)); + values.AppendElement(jni::StringParam(lastVisitedURISpec)); + } + + int32_t flags = 0; + if (aFlags & TOP_LEVEL) { + flags |= static_cast(GeckoViewVisitFlags::VISIT_TOP_LEVEL); + } + if (aFlags & REDIRECT_TEMPORARY) { + flags |= + static_cast(GeckoViewVisitFlags::VISIT_REDIRECT_TEMPORARY); + } + if (aFlags & REDIRECT_PERMANENT) { + flags |= + static_cast(GeckoViewVisitFlags::VISIT_REDIRECT_PERMANENT); + } + if (aFlags & REDIRECT_SOURCE) { + flags |= static_cast(GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE); + } + if (aFlags & REDIRECT_SOURCE_PERMANENT) { + flags |= static_cast( + GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE_PERMANENT); + } + if (aFlags & UNRECOVERABLE_ERROR) { + flags |= + static_cast(GeckoViewVisitFlags::VISIT_UNRECOVERABLE_ERROR); + } + keys.AppendElement(jni::StringParam(u"flags"_ns)); + values.AppendElement(java::sdk::Integer::ValueOf(flags)); + + MOZ_ASSERT(keys.Length() == values.Length()); + + auto bundleKeys = jni::ObjectArray::New(keys.Length()); + auto bundleValues = jni::ObjectArray::New(values.Length()); + for (size_t i = 0; i < keys.Length(); ++i) { + bundleKeys->SetElement(i, keys[i]); + bundleValues->SetElement(i, values[i]); + } + auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues); + + nsCOMPtr callback = + new OnVisitedCallback(this, aURI); + + Unused << NS_WARN_IF( + NS_FAILED(dispatcher->Dispatch(kOnVisitedMessage, bundle, callback))); + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewHistory::SetURITitle(nsIURI* aURI, const nsAString& aTitle) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/** + * Called from the session handler for the history delegate, with visited + * statuses for all requested URIs. + */ +class GetVisitedCallback final : public nsIAndroidEventCallback { + public: + explicit GetVisitedCallback(GeckoViewHistory* aHistory, + ContentParent* aInterestedProcess, + nsTArray>&& aURIs) + : mHistory(aHistory), + mInterestedProcess(aInterestedProcess), + mURIs(std::move(aURIs)) {} + + NS_DECL_ISUPPORTS + + NS_IMETHOD + OnSuccess(JS::Handle aData, JSContext* aCx) override { + nsTArray visitedURIs; + if (!ExtractVisitedURIs(aCx, aData, visitedURIs)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + IHistory::ContentParentSet interestedProcesses; + if (mInterestedProcess) { + interestedProcesses.Insert(mInterestedProcess); + } + mHistory->HandleVisitedState(visitedURIs, &interestedProcesses); + return NS_OK; + } + + NS_IMETHOD + OnError(JS::Handle aData, JSContext* aCx) override { + return NS_OK; + } + + private: + virtual ~GetVisitedCallback() {} + + /** + * Unpacks an array of Boolean visited statuses from the session handler into + * an array of `VisitedURI` structs. Each element in the array corresponds to + * a URI in `mURIs`. + * + * Returns `false` on error, `true` if the array is `null` or was successfully + * unpacked. + * + * TODO (bug 1503482): Remove this unboxing. + */ + bool ExtractVisitedURIs(JSContext* aCx, JS::Handle aData, + nsTArray& aVisitedURIs) { + if (aData.isNull()) { + return true; + } + bool isArray = false; + if (NS_WARN_IF(!JS::IsArrayObject(aCx, aData, &isArray))) { + return false; + } + if (NS_WARN_IF(!isArray)) { + return false; + } + JS::Rooted visited(aCx, &aData.toObject()); + uint32_t length = 0; + if (NS_WARN_IF(!JS::GetArrayLength(aCx, visited, &length))) { + return false; + } + if (NS_WARN_IF(length != mURIs.Length())) { + return false; + } + if (!aVisitedURIs.SetCapacity(length, mozilla::fallible)) { + return false; + } + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted value(aCx); + if (NS_WARN_IF(!JS_GetElement(aCx, visited, i, &value))) { + JS_ClearPendingException(aCx); + aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false}); + continue; + } + if (NS_WARN_IF(!value.isBoolean())) { + aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false}); + continue; + } + aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), value.toBoolean()}); + } + return true; + } + + RefPtr mHistory; + RefPtr mInterestedProcess; + nsTArray> mURIs; +}; + +NS_IMPL_ISUPPORTS(GetVisitedCallback, nsIAndroidEventCallback) + +/** + * Queries the history delegate to find which URIs have been visited. This + * is always called in the parent process: from `GetVisited` in non-e10s, and + * from `ContentParent::RecvGetVisited` in e10s. + */ +void GeckoViewHistory::QueryVisitedState(nsIWidget* aWidget, + ContentParent* aInterestedProcess, + nsTArray>&& aURIs) { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr window = nsWindow::From(aWidget); + if (NS_WARN_IF(!window)) { + return; + } + widget::EventDispatcher* dispatcher = window->GetEventDispatcher(); + if (NS_WARN_IF(!dispatcher)) { + return; + } + + // If nobody is listening for this we can stop now + if (!dispatcher->HasListener(kGetVisitedMessage)) { + return; + } + + // Assemble a bundle like `{ urls: ["http://example.com/1", ...] }`. + auto uris = jni::ObjectArray::New(aURIs.Length()); + for (size_t i = 0; i < aURIs.Length(); ++i) { + nsAutoCString uriSpec; + if (NS_WARN_IF(NS_FAILED(aURIs[i]->GetSpec(uriSpec)))) { + continue; + } + jni::String::LocalRef value{jni::StringParam(uriSpec)}; + uris->SetElement(i, value); + } + + auto bundleKeys = jni::ObjectArray::New(1); + jni::String::LocalRef key(jni::StringParam(u"urls"_ns)); + bundleKeys->SetElement(0, key); + + auto bundleValues = jni::ObjectArray::New(1); + jni::Object::LocalRef value(uris); + bundleValues->SetElement(0, value); + + auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues); + + nsCOMPtr callback = + new GetVisitedCallback(this, aInterestedProcess, std::move(aURIs)); + + Unused << NS_WARN_IF( + NS_FAILED(dispatcher->Dispatch(kGetVisitedMessage, bundle, callback))); +} + +/** + * Updates link states for all tracked links, forwarding the visited statuses to + * the content process in e10s. This is always called in the parent process, + * from `VisitedCallback::OnSuccess` and `GetVisitedCallback::OnSuccess`. + */ +void GeckoViewHistory::HandleVisitedState( + const nsTArray& aVisitedURIs, + ContentParentSet* aInterestedProcesses) { + MOZ_ASSERT(XRE_IsParentProcess()); + + for (const VisitedURI& visitedURI : aVisitedURIs) { + auto status = + visitedURI.mVisited ? VisitedStatus::Visited : VisitedStatus::Unvisited; + NotifyVisited(visitedURI.mURI, status, aInterestedProcesses); + } +} diff --git a/mobile/android/components/geckoview/GeckoViewHistory.h b/mobile/android/components/geckoview/GeckoViewHistory.h new file mode 100644 index 0000000000..a3b96ba58f --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewHistory.h @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GECKOVIEWHISTORY_H +#define GECKOVIEWHISTORY_H + +#include "mozilla/BaseHistory.h" +#include "nsTObserverArray.h" +#include "nsURIHashKey.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsIURI.h" + +#include "mozilla/StaticPtr.h" + +class nsIWidget; + +namespace mozilla { +namespace dom { +class Document; +} +} // namespace mozilla + +struct VisitedURI { + nsCOMPtr mURI; + bool mVisited = false; +}; + +class GeckoViewHistory final : public mozilla::BaseHistory { + public: + NS_DECL_ISUPPORTS + + // IHistory + NS_IMETHOD VisitURI(nsIWidget*, nsIURI*, nsIURI* aLastVisitedURI, + uint32_t aFlags, uint64_t aBrowserId) final; + NS_IMETHOD SetURITitle(nsIURI*, const nsAString&) final; + + static already_AddRefed GetSingleton(); + + void StartPendingVisitedQueries(PendingVisitedQueries&&) final; + + GeckoViewHistory(); + + void QueryVisitedState(nsIWidget* aWidget, + mozilla::dom::ContentParent* aInterestedProcess, + nsTArray>&& aURIs); + void HandleVisitedState(const nsTArray& aVisitedURIs, + ContentParentSet* aInterestedProcesses); + + private: + virtual ~GeckoViewHistory(); + + void QueryVisitedStateInContentProcess(const PendingVisitedQueries&); + void QueryVisitedStateInParentProcess(const PendingVisitedQueries&); + + static mozilla::StaticRefPtr sHistory; +}; + +#endif diff --git a/mobile/android/components/geckoview/GeckoViewOutputStream.cpp b/mobile/android/components/geckoview/GeckoViewOutputStream.cpp new file mode 100644 index 0000000000..3744b8e82d --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewOutputStream.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeckoViewOutputStream.h" +#include "mozilla/fallible.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(GeckoViewOutputStream, nsIOutputStream); + +NS_IMETHODIMP +GeckoViewOutputStream::Close() { + mStream->SendEof(); + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewOutputStream::Flush() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +GeckoViewOutputStream::Write(const char* buf, uint32_t count, + uint32_t* retval) { + jni::ByteArray::LocalRef buffer = jni::ByteArray::New( + reinterpret_cast(buf), count, fallible); + if (!buffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + if (NS_FAILED(mStream->AppendBuffer(buffer))) { + // The stream was closed, abort reading this channel. + return NS_BASE_STREAM_CLOSED; + } + // Return amount of bytes written + *retval = count; + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewOutputStream::WriteFrom(nsIInputStream* fromStream, uint32_t count, + uint32_t* retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GeckoViewOutputStream::WriteSegments(nsReadSegmentFun reader, void* closure, + uint32_t count, uint32_t* retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GeckoViewOutputStream::IsNonBlocking(bool* retval) { + *retval = true; + return NS_OK; +} diff --git a/mobile/android/components/geckoview/GeckoViewOutputStream.h b/mobile/android/components/geckoview/GeckoViewOutputStream.h new file mode 100644 index 0000000000..70ab8a9198 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewOutputStream.h @@ -0,0 +1,29 @@ + +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GeckoViewOutputStream_h__ +#define GeckoViewOutputStream_h__ + +#include "mozilla/java/GeckoInputStreamNatives.h" +#include "mozilla/java/GeckoInputStreamWrappers.h" + +#include "nsIOutputStream.h" +#include "nsIRequest.h" + +class GeckoViewOutputStream : public nsIOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + explicit GeckoViewOutputStream( + mozilla::java::GeckoInputStream::GlobalRef aStream) + : mStream(aStream) {} + + private: + const mozilla::java::GeckoInputStream::GlobalRef mStream; + virtual ~GeckoViewOutputStream() = default; +}; + +#endif // GeckoViewOutputStream_h__ diff --git a/mobile/android/components/geckoview/GeckoViewPermission.jsm b/mobile/android/components/geckoview/GeckoViewPermission.jsm new file mode 100644 index 0000000000..88ab64ab9d --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPermission.jsm @@ -0,0 +1,41 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["GeckoViewPermission"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +class GeckoViewPermission { + constructor() { + this.wrappedJSObject = this; + } + + async prompt(aRequest) { + const window = aRequest.window + ? aRequest.window + : aRequest.element.ownerGlobal; + + const actor = window.windowGlobalChild.getActor("GeckoViewPermission"); + const result = await actor.promptPermission(aRequest); + if (!result.allow) { + aRequest.cancel(); + } else { + // Note: permission could be undefined, that's what aRequest expects. + const { permission } = result; + aRequest.allow(permission); + } + } +} + +GeckoViewPermission.prototype.classID = Components.ID( + "{42f3c238-e8e8-4015-9ca2-148723a8afcf}" +); +GeckoViewPermission.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIContentPermissionPrompt", +]); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPermission"); diff --git a/mobile/android/components/geckoview/GeckoViewPrompt.jsm b/mobile/android/components/geckoview/GeckoViewPrompt.jsm new file mode 100644 index 0000000000..6e8d5a5984 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPrompt.jsm @@ -0,0 +1,820 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["PromptFactory"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt"); + +class PromptFactory { + constructor() { + this.wrappedJSObject = this; + } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "mozshowdropdown": + case "mozshowdropdown-sourcetouch": + this._handleSelect(aEvent.composedTarget, /* aIsDropDown = */ true); + break; + case "MozOpenDateTimePicker": + this._handleDateTime(aEvent.composedTarget); + break; + case "click": + this._handleClick(aEvent); + break; + case "DOMPopupBlocked": + this._handlePopupBlocked(aEvent); + break; + } + } + + _handleClick(aEvent) { + const target = aEvent.composedTarget; + const className = ChromeUtils.getClassName(target); + if (className !== "HTMLInputElement" && className !== "HTMLSelectElement") { + return; + } + + if ( + target.isContentEditable || + target.disabled || + target.readOnly || + !target.willValidate + ) { + // target.willValidate is false when any associated fieldset is disabled, + // in which case this element is treated as disabled per spec. + return; + } + + if (className === "HTMLSelectElement") { + if (!target.isCombobox) { + this._handleSelect(target, /* aIsDropDown = */ false); + return; + } + // combobox select is handled by mozshowdropdown. + return; + } + + const type = target.type; + if (type === "month" || type === "week") { + // If there's a shadow root, the MozOpenDateTimePicker event takes care + // of this. Right now for these input types there's never a shadow root. + // Once we support UA widgets for month/week inputs (see bug 888320), we + // can remove this. + if (!target.openOrClosedShadowRoot) { + this._handleDateTime(target); + aEvent.preventDefault(); + } + } + } + + _generateSelectItems(aElement) { + const win = aElement.ownerGlobal; + let id = 0; + const map = {}; + + const items = (function enumList(elem, disabled) { + const items = []; + const children = elem.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (win.getComputedStyle(child).display === "none") { + continue; + } + const item = { + id: String(id), + disabled: disabled || child.disabled, + }; + if (win.HTMLOptGroupElement.isInstance(child)) { + item.label = child.label; + item.items = enumList(child, item.disabled); + } else if (win.HTMLOptionElement.isInstance(child)) { + item.label = child.label || child.text; + item.selected = child.selected; + } else { + continue; + } + items.push(item); + map[id++] = child; + } + return items; + })(aElement); + + return [items, map, id]; + } + + _handleSelect(aElement, aIsDropDown) { + const win = aElement.ownerGlobal; + const [items] = this._generateSelectItems(aElement); + + if (aIsDropDown) { + aElement.openInParentProcess = true; + } + + const prompt = new lazy.GeckoViewPrompter(win); + + // Something changed the and for + // date/time. + aElement.dispatchEvent( + new aElement.ownerGlobal.Event("input", { bubbles: true, composed: true }) + ); + aElement.dispatchEvent( + new aElement.ownerGlobal.Event("change", { bubbles: true }) + ); + } + + _handlePopupBlocked(aEvent) { + const dwi = aEvent.requestingWindow; + const popupWindowURISpec = aEvent.popupWindowURI + ? aEvent.popupWindowURI.displaySpec + : "about:blank"; + + const prompt = new lazy.GeckoViewPrompter(aEvent.requestingWindow); + prompt.asyncShowPrompt( + { + type: "popup", + targetUri: popupWindowURISpec, + }, + ({ response }) => { + if (response && dwi) { + dwi.open( + popupWindowURISpec, + aEvent.popupWindowName, + aEvent.popupWindowFeatures + ); + } + } + ); + } + + /* ---------- nsIPromptFactory ---------- */ + getPrompt(aDOMWin, aIID) { + // Delegated to login manager here, which in turn calls back into us via nsIPromptService. + if (aIID.equals(Ci.nsIAuthPrompt2) || aIID.equals(Ci.nsIAuthPrompt)) { + try { + const pwmgr = Cc[ + "@mozilla.org/passwordmanager/authpromptfactory;1" + ].getService(Ci.nsIPromptFactory); + return pwmgr.getPrompt(aDOMWin, aIID); + } catch (e) { + console.error("Delegation to password manager failed: " + e); + } + } + + const p = new PromptDelegate(aDOMWin); + p.QueryInterface(aIID); + return p; + } + + /* ---------- private memebers ---------- */ + + // nsIPromptService methods proxy to our Prompt class + callProxy(aMethod, aArguments) { + const prompt = new PromptDelegate(aArguments[0]); + let promptArgs; + if (BrowsingContext.isInstance(aArguments[0])) { + // Called by BrowsingContext prompt method, strip modalType. + [, , /*browsingContext*/ /*modalType*/ ...promptArgs] = aArguments; + } else { + [, /*domWindow*/ ...promptArgs] = aArguments; + } + return prompt[aMethod].apply(prompt, promptArgs); + } + + /* ---------- nsIPromptService ---------- */ + + alert() { + return this.callProxy("alert", arguments); + } + alertBC() { + return this.callProxy("alert", arguments); + } + alertCheck() { + return this.callProxy("alertCheck", arguments); + } + alertCheckBC() { + return this.callProxy("alertCheck", arguments); + } + confirm() { + return this.callProxy("confirm", arguments); + } + confirmBC() { + return this.callProxy("confirm", arguments); + } + confirmCheck() { + return this.callProxy("confirmCheck", arguments); + } + confirmCheckBC() { + return this.callProxy("confirmCheck", arguments); + } + confirmEx() { + return this.callProxy("confirmEx", arguments); + } + confirmExBC() { + return this.callProxy("confirmEx", arguments); + } + prompt() { + return this.callProxy("prompt", arguments); + } + promptBC() { + return this.callProxy("prompt", arguments); + } + promptUsernameAndPassword() { + return this.callProxy("promptUsernameAndPassword", arguments); + } + promptUsernameAndPasswordBC() { + return this.callProxy("promptUsernameAndPassword", arguments); + } + promptPassword() { + return this.callProxy("promptPassword", arguments); + } + promptPasswordBC() { + return this.callProxy("promptPassword", arguments); + } + select() { + return this.callProxy("select", arguments); + } + selectBC() { + return this.callProxy("select", arguments); + } + promptAuth() { + return this.callProxy("promptAuth", arguments); + } + promptAuthBC() { + return this.callProxy("promptAuth", arguments); + } + asyncPromptAuth() { + return this.callProxy("asyncPromptAuth", arguments); + } +} + +PromptFactory.prototype.classID = Components.ID( + "{076ac188-23c1-4390-aa08-7ef1f78ca5d9}" +); +PromptFactory.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIPromptFactory", + "nsIPromptService", +]); + +class PromptDelegate { + constructor(aParent) { + this._prompter = new lazy.GeckoViewPrompter(aParent); + } + + BUTTON_TYPE_POSITIVE = 0; + BUTTON_TYPE_NEUTRAL = 1; + BUTTON_TYPE_NEGATIVE = 2; + + /* ---------- internal methods ---------- */ + + _addText(aTitle, aText, aMsg) { + return Object.assign(aMsg, { + title: aTitle, + msg: aText, + }); + } + + _addCheck(aCheckMsg, aCheckState, aMsg) { + return Object.assign(aMsg, { + hasCheck: !!aCheckMsg, + checkMsg: aCheckMsg, + checkValue: aCheckState && aCheckState.value, + }); + } + + /* ---------- nsIPrompt ---------- */ + + alert(aTitle, aText) { + this.alertCheck(aTitle, aText); + } + + alertCheck(aTitle, aText, aCheckMsg, aCheckState) { + const result = this._prompter.showPrompt( + this._addText( + aTitle, + aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "alert", + }) + ) + ); + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + } + + confirm(aTitle, aText) { + // Button 0 is OK. + return this.confirmCheck(aTitle, aText); + } + + confirmCheck(aTitle, aText, aCheckMsg, aCheckState) { + // Button 0 is OK. + return ( + this.confirmEx( + aTitle, + aText, + Ci.nsIPrompt.STD_OK_CANCEL_BUTTONS, + /* aButton0 */ null, + /* aButton1 */ null, + /* aButton2 */ null, + aCheckMsg, + aCheckState + ) == 0 + ); + } + + confirmEx( + aTitle, + aText, + aButtonFlags, + aButton0, + aButton1, + aButton2, + aCheckMsg, + aCheckState + ) { + const btnMap = Array(3).fill(null); + const btnTitle = Array(3).fill(null); + const btnCustomTitle = Array(3).fill(null); + const savedButtonId = []; + for (let i = 0; i < 3; i++) { + const btnFlags = aButtonFlags >> (i * 8); + switch (btnFlags & 0xff) { + case Ci.nsIPrompt.BUTTON_TITLE_OK: + btnMap[this.BUTTON_TYPE_POSITIVE] = i; + btnTitle[this.BUTTON_TYPE_POSITIVE] = "ok"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_CANCEL: + btnMap[this.BUTTON_TYPE_NEGATIVE] = i; + btnTitle[this.BUTTON_TYPE_NEGATIVE] = "cancel"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_YES: + btnMap[this.BUTTON_TYPE_POSITIVE] = i; + btnTitle[this.BUTTON_TYPE_POSITIVE] = "yes"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_NO: + btnMap[this.BUTTON_TYPE_NEGATIVE] = i; + btnTitle[this.BUTTON_TYPE_NEGATIVE] = "no"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING: + // We don't know if this is positive/negative/neutral, so save for later. + savedButtonId.push(i); + break; + case Ci.nsIPrompt.BUTTON_TITLE_SAVE: + case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE: + case Ci.nsIPrompt.BUTTON_TITLE_REVERT: + // Not supported; fall-through. + default: + break; + } + } + + // Put saved buttons into available slots. + for (let i = 0; i < 3 && savedButtonId.length; i++) { + if (btnMap[i] === null) { + btnMap[i] = savedButtonId.shift(); + btnTitle[i] = "custom"; + btnCustomTitle[i] = [aButton0, aButton1, aButton2][btnMap[i]]; + } + } + + const result = this._prompter.showPrompt( + this._addText( + aTitle, + aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "button", + btnTitle, + btnCustomTitle, + }) + ) + ); + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + return result && result.button in btnMap ? btnMap[result.button] : -1; + } + + prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) { + const result = this._prompter.showPrompt( + this._addText( + aTitle, + aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "text", + value: aValue.value, + }) + ) + ); + // OK: result && result.text !== undefined + // Cancel: result && result.text === undefined + // Error: !result + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + if (!result || result.text === undefined) { + return false; + } + aValue.value = result.text || ""; + return true; + } + + promptPassword(aTitle, aText, aPassword) { + return this._promptUsernameAndPassword( + aTitle, + aText, + /* aUsername */ undefined, + aPassword + ); + } + + promptUsernameAndPassword(aTitle, aText, aUsername, aPassword) { + const msg = { + type: "auth", + mode: aUsername ? "auth" : "password", + options: { + flags: aUsername ? 0 : Ci.nsIAuthInformation.ONLY_PASSWORD, + username: aUsername ? aUsername.value : undefined, + password: aPassword.value, + }, + }; + const result = this._prompter.showPrompt(this._addText(aTitle, aText, msg)); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + if (!result || result.password === undefined) { + return false; + } + if (aUsername) { + aUsername.value = result.username || ""; + } + aPassword.value = result.password || ""; + return true; + } + + select(aTitle, aText, aSelectList, aOutSelection) { + const choices = Array.prototype.map.call(aSelectList, (item, index) => ({ + id: String(index), + label: item, + disabled: false, + selected: false, + })); + const result = this._prompter.showPrompt( + this._addText(aTitle, aText, { + type: "choice", + mode: "single", + choices, + }) + ); + // OK: result + // Cancel: !result + if (!result || result.choices === undefined) { + return false; + } + aOutSelection.value = Number(result.choices[0]); + return true; + } + + _getAuthMsg(aChannel, aLevel, aAuthInfo) { + let username; + if ( + aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN && + aAuthInfo.domain + ) { + username = aAuthInfo.domain + "\\" + aAuthInfo.username; + } else { + username = aAuthInfo.username; + } + return this._addText( + /* title */ null, + this._getAuthText(aChannel, aAuthInfo), + { + type: "auth", + mode: + aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD + ? "password" + : "auth", + options: { + flags: aAuthInfo.flags, + uri: aChannel && aChannel.URI.displaySpec, + level: aLevel, + username, + password: aAuthInfo.password, + }, + } + ); + } + + _fillAuthInfo(aAuthInfo, aResult) { + if (!aResult || aResult.password === undefined) { + return false; + } + + aAuthInfo.password = aResult.password || ""; + if (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) { + return true; + } + + const username = aResult.username || ""; + if (aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN) { + // Domain is separated from username by a backslash + var idx = username.indexOf("\\"); + if (idx >= 0) { + aAuthInfo.domain = username.substring(0, idx); + aAuthInfo.username = username.substring(idx + 1); + return true; + } + } + aAuthInfo.username = username; + return true; + } + + promptAuth(aChannel, aLevel, aAuthInfo) { + const result = this._prompter.showPrompt( + this._getAuthMsg(aChannel, aLevel, aAuthInfo) + ); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + return this._fillAuthInfo(aAuthInfo, result); + } + + async asyncPromptAuth(aChannel, aLevel, aAuthInfo) { + const result = await this._prompter.asyncShowPromptPromise( + this._getAuthMsg(aChannel, aLevel, aAuthInfo) + ); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + return this._fillAuthInfo(aAuthInfo, result); + } + + _getAuthText(aChannel, aAuthInfo) { + const isProxy = aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY; + const isPassOnly = aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD; + const isCrossOrig = + aAuthInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE; + + const username = aAuthInfo.username; + const authTarget = this._getAuthTarget(aChannel, aAuthInfo); + const { displayHost } = authTarget; + let { realm } = authTarget; + + // Suppress "the site says: $realm" when we synthesized a missing realm. + if (!aAuthInfo.realm && !isProxy) { + realm = ""; + } + + // Trim obnoxiously long realms. + if (realm.length > 50) { + realm = realm.substring(0, 50) + "\u2026"; + } + + const bundle = Services.strings.createBundle( + "chrome://global/locale/commonDialogs.properties" + ); + let text; + if (isProxy) { + text = bundle.formatStringFromName("EnterLoginForProxy3", [ + realm, + displayHost, + ]); + } else if (isPassOnly) { + text = bundle.formatStringFromName("EnterPasswordFor", [ + username, + displayHost, + ]); + } else if (isCrossOrig) { + text = bundle.formatStringFromName("EnterUserPasswordForCrossOrigin2", [ + displayHost, + ]); + } else if (!realm) { + text = bundle.formatStringFromName("EnterUserPasswordFor2", [ + displayHost, + ]); + } else { + text = bundle.formatStringFromName("EnterLoginForRealm3", [ + realm, + displayHost, + ]); + } + + return text; + } + + _getAuthTarget(aChannel, aAuthInfo) { + // If our proxy is demanding authentication, don't use the + // channel's actual destination. + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + if (!(aChannel instanceof Ci.nsIProxiedChannel)) { + throw new Error("proxy auth needs nsIProxiedChannel"); + } + const info = aChannel.proxyInfo; + if (!info) { + throw new Error("proxy auth needs nsIProxyInfo"); + } + // Proxies don't have a scheme, but we'll use "moz-proxy://" + // so that it's more obvious what the login is for. + const idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + const displayHost = + "moz-proxy://" + + idnService.convertUTF8toACE(info.host) + + ":" + + info.port; + let realm = aAuthInfo.realm; + if (!realm) { + realm = displayHost; + } + return { displayHost, realm }; + } + + const displayHost = + aChannel.URI.scheme + "://" + aChannel.URI.displayHostPort; + // If a HTTP WWW-Authenticate header specified a realm, that value + // will be available here. If it wasn't set or wasn't HTTP, we'll use + // the formatted hostname instead. + let realm = aAuthInfo.realm; + if (!realm) { + realm = displayHost; + } + return { displayHost, realm }; + } +} + +PromptDelegate.prototype.QueryInterface = ChromeUtils.generateQI(["nsIPrompt"]); diff --git a/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs b/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs new file mode 100644 index 0000000000..e6e97822d6 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs @@ -0,0 +1,208 @@ +/* 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 { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompter"); + +export class GeckoViewPrompter { + constructor(aParent) { + this.id = Services.uuid + .generateUUID() + .toString() + .slice(1, -1); // Discard surrounding braces + + if (aParent) { + if (Window.isInstance(aParent)) { + this._domWin = aParent; + } else if (aParent.window) { + this._domWin = aParent.window; + } else { + this._domWin = + aParent.embedderElement && aParent.embedderElement.ownerGlobal; + } + } + + if (!this._domWin) { + this._domWin = Services.wm.getMostRecentWindow("navigator:geckoview"); + } + + this._innerWindowId = this._domWin?.browsingContext.currentWindowContext.innerWindowId; + } + + get domWin() { + return this._domWin; + } + + get prompterActor() { + const actor = this.domWin?.windowGlobalChild.getActor("GeckoViewPrompter"); + return actor; + } + + _changeModalState(aEntering) { + if (!this._domWin) { + // Allow not having a DOM window. + return true; + } + // Accessing the document object can throw if this window no longer exists. See bug 789888. + try { + const winUtils = this._domWin.windowUtils; + if (!aEntering) { + winUtils.leaveModalState(); + } + + const event = this._domWin.document.createEvent("Events"); + event.initEvent( + aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed", + true, + true + ); + winUtils.dispatchEventToChromeOnly(this._domWin, event); + + if (aEntering) { + winUtils.enterModalState(); + } + return true; + } catch (ex) { + console.error("Failed to change modal state: " + ex); + } + return false; + } + + _dismissUi() { + this.prompterActor?.dismissPrompt(this); + } + + accept(aInputText = this.inputText) { + if (this.callback) { + let acceptMsg = {}; + switch (this.message.type) { + case "alert": + acceptMsg = null; + break; + case "button": + acceptMsg.button = 0; + break; + case "text": + acceptMsg.text = aInputText; + break; + default: + acceptMsg = null; + break; + } + this.callback(acceptMsg); + // Notify the UI that this prompt should be hidden. + this._dismissUi(); + } + } + + dismiss() { + this.callback(null); + // Notify the UI that this prompt should be hidden. + this._dismissUi(); + } + + getPromptType() { + switch (this.message.type) { + case "alert": + return this.message.checkValue ? "alertCheck" : "alert"; + case "button": + return this.message.checkValue ? "confirmCheck" : "confirm"; + case "text": + return this.message.checkValue ? "promptCheck" : "prompt"; + default: + return this.message.type; + } + } + + getPromptText() { + return this.message.msg; + } + + getInputText() { + return this.inputText; + } + + setInputText(aInput) { + this.inputText = aInput; + } + + /** + * Shows a native prompt, and then spins the event loop for this thread while we wait + * for a response + */ + showPrompt(aMsg) { + let result = undefined; + if (!this._domWin || !this._changeModalState(/* aEntering */ true)) { + return result; + } + try { + this.asyncShowPrompt(aMsg, res => (result = res)); + + // Spin this thread while we wait for a result + Services.tm.spinEventLoopUntil( + "GeckoViewPrompter.jsm:showPrompt", + () => this._domWin.closed || result !== undefined + ); + } finally { + this._changeModalState(/* aEntering */ false); + } + return result; + } + + checkInnerWindow() { + // Checks that the innerWindow where this prompt was created still matches + // the current innerWindow. + // This checks will fail if the page navigates away, making this prompt + // obsolete. + return ( + this._innerWindowId === + this._domWin.browsingContext.currentWindowContext.innerWindowId + ); + } + + asyncShowPromptPromise(aMsg) { + return new Promise(resolve => { + this.asyncShowPrompt(aMsg, resolve); + }); + } + + async asyncShowPrompt(aMsg, aCallback) { + this.message = aMsg; + this.inputText = aMsg.value; + this.callback = aCallback; + + aMsg.id = this.id; + + let response = null; + try { + if (this.checkInnerWindow()) { + response = await this.prompterActor.prompt(this, aMsg); + } + } catch (error) { + // Nothing we can do really, we will treat this as a dismiss. + warn`Error while prompting: ${error}`; + } + + if (!this.checkInnerWindow()) { + // Page has navigated away, let's dismiss the prompt + aCallback(null); + } else { + aCallback(response); + } + // This callback object is tied to the Java garbage collector because + // it is invoked from Java. Manually release the target callback + // here; otherwise we may hold onto resources for too long, because + // we would be relying on both the Java and the JS garbage collectors + // to run. + aMsg = undefined; + aCallback = undefined; + } + + update(aMsg) { + this.message = aMsg; + aMsg.id = this.id; + this.prompterActor?.updatePrompt(aMsg); + } +} diff --git a/mobile/android/components/geckoview/GeckoViewPush.jsm b/mobile/android/components/geckoview/GeckoViewPush.jsm new file mode 100644 index 0000000000..5899bfd3d8 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPush.jsm @@ -0,0 +1,257 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["PushService"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPush"); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", +}); + +// Observer notification topics for push messages and subscription status +// changes. These are duplicated and used in `nsIPushNotifier`. They're exposed +// on `nsIPushService` so that JS callers only need to import this service. +const OBSERVER_TOPIC_PUSH = "push-message"; +const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change"; +const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified"; + +function createSubscription({ + scope, + principal, + browserPublicKey, + authSecret, + endpoint, + appServerKey, +}) { + const decodedBrowserKey = ChromeUtils.base64URLDecode(browserPublicKey, { + padding: "ignore", + }); + const decodedAuthSecret = ChromeUtils.base64URLDecode(authSecret, { + padding: "ignore", + }); + + return new PushSubscription({ + endpoint, + scope, + p256dhKey: decodedBrowserKey, + authenticationSecret: decodedAuthSecret, + appServerKey, + }); +} + +function scopeWithAttrs(scope, attrs) { + return scope + ChromeUtils.originAttributesToSuffix(attrs); +} + +class PushService { + constructor() { + this.wrappedJSObject = this; + } + + pushTopic = OBSERVER_TOPIC_PUSH; + subscriptionChangeTopic = OBSERVER_TOPIC_SUBSCRIPTION_CHANGE; + subscriptionModifiedTopic = OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED; + + // nsIObserver methods + + observe(subject, topic, data) {} + + // nsIPushService methods + + subscribe(scope, principal, callback) { + this.subscribeWithKey(scope, principal, null, callback); + } + + async subscribeWithKey(scope, principal, appServerKey, callback) { + try { + const response = await lazy.EventDispatcher.instance.sendRequestForResult( + { + type: "GeckoView:PushSubscribe", + scope: scopeWithAttrs(scope, principal.originAttributes), + appServerKey: appServerKey + ? ChromeUtils.base64URLEncode(new Uint8Array(appServerKey), { + pad: true, + }) + : null, + } + ); + + let subscription = null; + if (response) { + subscription = createSubscription({ + ...response, + scope, + principal, + appServerKey, + }); + } + + callback.onPushSubscription(Cr.NS_OK, subscription); + } catch (e) { + callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null); + } + } + + async unsubscribe(scope, principal, callback) { + try { + await lazy.EventDispatcher.instance.sendRequestForResult({ + type: "GeckoView:PushUnsubscribe", + scope: scopeWithAttrs(scope, principal.originAttributes), + }); + + callback.onUnsubscribe(Cr.NS_OK, true); + } catch (e) { + callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false); + } + } + + async getSubscription(scope, principal, callback) { + try { + const response = await lazy.EventDispatcher.instance.sendRequestForResult( + { + type: "GeckoView:PushGetSubscription", + scope: scopeWithAttrs(scope, principal.originAttributes), + } + ); + + let subscription = null; + if (response) { + subscription = createSubscription({ + ...response, + scope, + principal, + }); + } + + callback.onPushSubscription(Cr.NS_OK, subscription); + } catch (e) { + callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null); + } + } + + clearForDomain(domain, callback) { + callback.onClear(Cr.NS_OK); + } + + // nsIPushQuotaManager methods + + notificationForOriginShown(origin) {} + + notificationForOriginClosed(origin) {} + + // nsIPushErrorReporter methods + + reportDeliveryError(messageId, reason) {} +} + +PushService.prototype.classID = Components.ID( + "{a54d84d7-98a4-4fec-b664-e42e512ae9cc}" +); +PushService.prototype.contractID = "@mozilla.org/push/Service;1"; +PushService.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + "nsIPushService", + "nsIPushQuotaManager", + "nsIPushErrorReporter", +]); + +/** `PushSubscription` instances are passed to all subscription callbacks. */ +class PushSubscription { + constructor(props) { + this._props = props; + } + + /** The URL for sending messages to this subscription. */ + get endpoint() { + return this._props.endpoint; + } + + /** The last time a message was sent to this subscription. */ + get lastPush() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + /** The total number of messages sent to this subscription. */ + get pushCount() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + /** + * The app will take care of throttling, so we don't + * care about the quota stuff here. + */ + get quota() { + return -1; + } + + /** + * Indicates whether this subscription was created with the system principal. + * System subscriptions are exempt from the background message quota and + * permission checks. + */ + get isSystemSubscription() { + return false; + } + + /** The private key used to decrypt incoming push messages, in JWK format */ + get p256dhPrivateKey() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + /** + * Indicates whether this subscription is subject to the background message + * quota. + */ + quotaApplies() { + return false; + } + + /** + * Indicates whether this subscription exceeded the background message quota, + * or the user revoked the notification permission. The caller must request a + * new subscription to continue receiving push messages. + */ + isExpired() { + return false; + } + + /** + * Returns a key for encrypting messages sent to this subscription. JS + * callers receive the key buffer as a return value, while C++ callers + * receive the key size and buffer as out parameters. + */ + getKey(name) { + switch (name) { + case "p256dh": + return this._getRawKey(this._props.p256dhKey); + + case "auth": + return this._getRawKey(this._props.authenticationSecret); + + case "appServer": + return this._getRawKey(this._props.appServerKey); + } + return []; + } + + _getRawKey(key) { + if (!key) { + return []; + } + return new Uint8Array(key); + } +} + +PushSubscription.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIPushSubscription", +]); diff --git a/mobile/android/components/geckoview/GeckoViewStartup.jsm b/mobile/android/components/geckoview/GeckoViewStartup.jsm new file mode 100644 index 0000000000..9f0d3934bd --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewStartup.jsm @@ -0,0 +1,333 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["GeckoViewStartup"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ActorManagerParent: "resource://gre/modules/ActorManagerParent.sys.mjs", + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + PdfJs: "resource://pdf.js/PdfJs.jsm", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("Startup"); + +var { DelayedInit } = ChromeUtils.import( + "resource://gre/modules/DelayedInit.jsm" +); + +function InitLater(fn, object, name) { + return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */); +} + +const JSPROCESSACTORS = { + GeckoViewPermissionProcess: { + parent: { + moduleURI: "resource:///actors/GeckoViewPermissionProcessParent.jsm", + }, + child: { + moduleURI: "resource:///actors/GeckoViewPermissionProcessChild.jsm", + observers: [ + "getUserMedia:ask-device-permission", + "getUserMedia:request", + "recording-device-events", + "PeerConnection:request", + ], + }, + }, +}; + +const JSWINDOWACTORS = { + LoadURIDelegate: { + parent: { + moduleURI: "resource:///actors/LoadURIDelegateParent.jsm", + }, + child: { + moduleURI: "resource:///actors/LoadURIDelegateChild.jsm", + }, + messageManagerGroups: ["browsers"], + }, + GeckoViewPermission: { + parent: { + moduleURI: "resource:///actors/GeckoViewPermissionParent.jsm", + }, + child: { + moduleURI: "resource:///actors/GeckoViewPermissionChild.jsm", + }, + allFrames: true, + includeChrome: true, + }, + GeckoViewPrompt: { + child: { + moduleURI: "resource:///actors/GeckoViewPromptChild.jsm", + events: { + click: { capture: false, mozSystemGroup: true }, + contextmenu: { capture: false, mozSystemGroup: true }, + mozshowdropdown: {}, + "mozshowdropdown-sourcetouch": {}, + MozOpenDateTimePicker: {}, + DOMPopupBlocked: { capture: false, mozSystemGroup: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + GeckoViewFormValidation: { + child: { + moduleURI: "resource:///actors/GeckoViewFormValidationChild.jsm", + events: { + MozInvalidForm: {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + GeckoViewClipboardPermission: { + parent: { + moduleURI: "resource:///actors/GeckoViewClipboardPermissionParent.jsm", + }, + child: { + moduleURI: "resource:///actors/GeckoViewClipboardPermissionChild.jsm", + events: { + MozClipboardReadPaste: {}, + deactivate: { mozSystemGroup: true }, + mousedown: { capture: true, mozSystemGroup: true }, + mozvisualscroll: { mozSystemGroup: true }, + pagehide: { capture: true, mozSystemGroup: true }, + }, + }, + allFrames: true, + }, +}; + +class GeckoViewStartup { + /* ---------- nsIObserver ---------- */ + observe(aSubject, aTopic, aData) { + debug`observe: ${aTopic}`; + switch (aTopic) { + case "content-process-ready-for-script": + case "app-startup": { + GeckoViewUtils.addLazyGetter(this, "GeckoViewConsole", { + module: "resource://gre/modules/GeckoViewConsole.jsm", + }); + + GeckoViewUtils.addLazyGetter(this, "GeckoViewStorageController", { + module: "resource://gre/modules/GeckoViewStorageController.jsm", + ged: [ + "GeckoView:ClearData", + "GeckoView:ClearSessionContextData", + "GeckoView:ClearHostData", + "GeckoView:ClearBaseDomainData", + "GeckoView:GetAllPermissions", + "GeckoView:GetPermissionsByURI", + "GeckoView:SetPermission", + "GeckoView:SetPermissionByURI", + "GeckoView:GetCookieBannerModeForDomain", + "GeckoView:SetCookieBannerModeForDomain", + "GeckoView:RemoveCookieBannerModeForDomain", + ], + }); + + GeckoViewUtils.addLazyGetter(this, "GeckoViewPushController", { + module: "resource://gre/modules/GeckoViewPushController.jsm", + ged: ["GeckoView:PushEvent", "GeckoView:PushSubscriptionChanged"], + }); + + GeckoViewUtils.addLazyPrefObserver( + { + name: "geckoview.console.enabled", + default: false, + }, + { + handler: _ => this.GeckoViewConsole, + } + ); + + // Parent process only + if ( + Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT + ) { + lazy.ActorManagerParent.addJSWindowActors(JSWINDOWACTORS); + lazy.ActorManagerParent.addJSProcessActors(JSPROCESSACTORS); + + if (Services.appinfo.sessionHistoryInParent) { + GeckoViewUtils.addLazyGetter(this, "GeckoViewSessionStore", { + module: "resource://gre/modules/GeckoViewSessionStore.jsm", + observers: [ + "browsing-context-did-set-embedder", + "browsing-context-discarded", + ], + }); + } + + GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", { + module: "resource://gre/modules/GeckoViewWebExtension.jsm", + ged: [ + "GeckoView:ActionDelegate:Attached", + "GeckoView:BrowserAction:Click", + "GeckoView:PageAction:Click", + "GeckoView:RegisterWebExtension", + "GeckoView:UnregisterWebExtension", + "GeckoView:WebExtension:CancelInstall", + "GeckoView:WebExtension:Disable", + "GeckoView:WebExtension:Enable", + "GeckoView:WebExtension:EnsureBuiltIn", + "GeckoView:WebExtension:Get", + "GeckoView:WebExtension:Install", + "GeckoView:WebExtension:InstallBuiltIn", + "GeckoView:WebExtension:List", + "GeckoView:WebExtension:PortDisconnect", + "GeckoView:WebExtension:PortMessageFromApp", + "GeckoView:WebExtension:SetPBAllowed", + "GeckoView:WebExtension:Uninstall", + "GeckoView:WebExtension:Update", + ], + observers: [ + "devtools-installed-addon", + "testing-installed-addon", + "testing-uninstalled-addon", + ], + }); + + GeckoViewUtils.addLazyGetter(this, "ChildCrashHandler", { + module: "resource://gre/modules/ChildCrashHandler.jsm", + observers: ["ipc:content-shutdown", "compositor:process-aborted"], + }); + + lazy.EventDispatcher.instance.registerListener(this, [ + "GeckoView:StorageDelegate:Attached", + ]); + } + break; + } + + case "profile-after-change": { + GeckoViewUtils.addLazyGetter(this, "GeckoViewRemoteDebugger", { + module: "resource://gre/modules/GeckoViewRemoteDebugger.jsm", + init: gvrd => gvrd.onInit(), + }); + + GeckoViewUtils.addLazyPrefObserver( + { + name: "devtools.debugger.remote-enabled", + default: false, + }, + { + handler: _ => this.GeckoViewRemoteDebugger, + } + ); + + GeckoViewUtils.addLazyGetter(this, "DownloadTracker", { + module: "resource://gre/modules/GeckoViewWebExtension.jsm", + ged: ["GeckoView:WebExtension:DownloadChanged"], + }); + + ChromeUtils.import("resource://gre/modules/NotificationDB.jsm"); + + // Listen for global EventDispatcher messages + lazy.EventDispatcher.instance.registerListener(this, [ + "GeckoView:ResetUserPrefs", + "GeckoView:SetDefaultPrefs", + "GeckoView:SetLocale", + ]); + + Services.obs.addObserver(this, "browser-idle-startup-tasks-finished"); + Services.obs.addObserver(this, "handlersvc-store-initialized"); + + Services.obs.notifyObservers(null, "geckoview-startup-complete"); + break; + } + case "browser-idle-startup-tasks-finished": { + // TODO bug 1730026: when an alternative is introduced that runs once, + // replace this observer topic with that alternative. + // This only needs to happen once during startup. + Services.obs.removeObserver(this, aTopic); + // Notify the start up crash tracker that the browser has successfully + // started up so the startup cache isn't rebuilt on next startup. + Services.startup.trackStartupCrashEnd(); + break; + } + case "handlersvc-store-initialized": { + // Initialize PdfJs when running in-process and remote. This only + // happens once since PdfJs registers global hooks. If the PdfJs + // extension is installed the init method below will be overridden + // leaving initialization to the extension. + // parent only: configure default prefs, set up pref observers, register + // pdf content handler, and initializes parent side message manager + // shim for privileged api access. + try { + lazy.PdfJs.init(this._isNewProfile); + } catch {} + break; + } + } + } + + onEvent(aEvent, aData, aCallback) { + debug`onEvent ${aEvent}`; + + switch (aEvent) { + case "GeckoView:ResetUserPrefs": { + const prefs = new lazy.Preferences(); + prefs.reset(aData.names); + break; + } + case "GeckoView:SetDefaultPrefs": { + const prefs = new lazy.Preferences({ defaultBranch: true }); + for (const name of Object.keys(aData)) { + try { + prefs.set(name, aData[name]); + } catch (e) { + warn`Failed to set preference ${name}: ${e}`; + } + } + break; + } + case "GeckoView:SetLocale": + if (aData.requestedLocales) { + Services.locale.requestedLocales = aData.requestedLocales; + } + const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + pls.data = aData.acceptLanguages; + Services.prefs.setComplexValue( + "intl.accept_languages", + Ci.nsIPrefLocalizedString, + pls + ); + break; + + case "GeckoView:StorageDelegate:Attached": + InitLater(() => { + const loginDetection = Cc[ + "@mozilla.org/login-detection-service;1" + ].createInstance(Ci.nsILoginDetectionService); + loginDetection.init(); + }); + break; + } + } +} + +GeckoViewStartup.prototype.classID = Components.ID( + "{8e993c34-fdd6-432c-967e-f995d888777f}" +); +GeckoViewStartup.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", +]); diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.cpp b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp new file mode 100644 index 0000000000..71b9fadeb5 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp @@ -0,0 +1,298 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "GeckoViewStreamListener.h" + +#include "mozilla/fallible.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIChannelEventSink.h" +#include "nsIHttpChannel.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIInputStream.h" +#include "nsINSSErrorsService.h" +#include "nsITransportSecurityInfo.h" +#include "nsIWebProgressListener.h" +#include "nsIX509Cert.h" +#include "nsPrintfCString.h" + +#include "nsNetUtil.h" + +#include "JavaBuiltins.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(GeckoViewStreamListener, nsIStreamListener, + nsIInterfaceRequestor, nsIChannelEventSink) + +class HeaderVisitor final : public nsIHttpHeaderVisitor { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit HeaderVisitor(java::WebResponse::Builder::Param aBuilder) + : mBuilder(aBuilder) {} + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + mBuilder->Header(aHeader, aValue); + return NS_OK; + } + + private: + virtual ~HeaderVisitor() {} + + const java::WebResponse::Builder::GlobalRef mBuilder; +}; + +NS_IMPL_ISUPPORTS(HeaderVisitor, nsIHttpHeaderVisitor) + +class StreamSupport final + : public java::GeckoInputStream::Support::Natives { + public: + typedef java::GeckoInputStream::Support::Natives Base; + using Base::AttachNative; + using Base::GetNative; + + explicit StreamSupport(java::GeckoInputStream::Support::Param aInstance, + nsIRequest* aRequest) + : mInstance(aInstance), mRequest(aRequest) {} + + void Close() { + mRequest->Cancel(NS_ERROR_ABORT); + mRequest->Resume(); + + // This is basically `delete this`, so don't run anything else! + Base::DisposeNative(mInstance); + } + + void Resume() { mRequest->Resume(); } + + private: + java::GeckoInputStream::Support::GlobalRef mInstance; + nsCOMPtr mRequest; +}; + +NS_IMETHODIMP +GeckoViewStreamListener::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(!mStream); + + nsresult status; + aRequest->GetStatus(&status); + if (NS_FAILED(status)) { + nsCOMPtr channel = do_QueryInterface(aRequest); + CompleteWithError(status, channel); + return NS_OK; + } + + // We're expecting data later via OnDataAvailable, so create the stream now. + InitializeStreamSupport(aRequest); + + mStream = java::GeckoInputStream::New(mSupport); + + // Suspend the request immediately. It will be resumed when (if) someone + // tries to read the Java stream. + aRequest->Suspend(); + + nsresult rv = HandleWebResponse(aRequest); + if (NS_FAILED(rv)) { + nsCOMPtr channel = do_QueryInterface(aRequest); + CompleteWithError(rv, channel); + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewStreamListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + if (mStream) { + if (NS_FAILED(aStatusCode)) { + mStream->SendError(); + } else { + mStream->SendEof(); + } + } + return NS_OK; +} + +NS_IMETHODIMP GeckoViewStreamListener::OnDataAvailable( + nsIRequest* aRequest, nsIInputStream* aInputStream, uint64_t aOffset, + uint32_t aCount) { + MOZ_ASSERT(mStream); + + // We only need this for the ReadSegments call, the value is unused. + uint32_t countRead; + nsresult rv = + aInputStream->ReadSegments(WriteSegment, this, aCount, &countRead); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +NS_IMETHODIMP +GeckoViewStreamListener::GetInterface(const nsIID& aIID, void** aResultOut) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + *aResultOut = static_cast(this); + NS_ADDREF_THIS(); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +GeckoViewStreamListener::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* callback) { + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +/* static */ +nsresult GeckoViewStreamListener::WriteSegment( + nsIInputStream* aInputStream, void* aClosure, const char* aFromSegment, + uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) { + GeckoViewStreamListener* self = + static_cast(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mStream); + + *aWriteCount = aCount; + + jni::ByteArray::LocalRef buffer = jni::ByteArray::New( + reinterpret_cast(const_cast(aFromSegment)), + *aWriteCount, fallible); + if (!buffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_FAILED(self->mStream->AppendBuffer(buffer))) { + // The stream was closed or something, abort reading this channel. + return NS_ERROR_ABORT; + } + + return NS_OK; +} + +nsresult GeckoViewStreamListener::HandleWebResponse(nsIRequest* aRequest) { + nsresult rv; + + nsCOMPtr channel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // URI + nsCOMPtr uri; + rv = channel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uriSpec; + rv = uri->GetSpec(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + + java::WebResponse::Builder::LocalRef builder = + java::WebResponse::Builder::New(uriSpec); + + // Body stream + if (mStream) { + builder->Body(mStream); + } + + // Redirected + nsCOMPtr loadInfo = channel->LoadInfo(); + builder->Redirected(!loadInfo->RedirectChain().IsEmpty()); + + // Secure status + auto [certBytes, isSecure] = CertificateFromChannel(channel); + builder->IsSecure(isSecure); + if (certBytes) { + rv = builder->CertificateBytes(certBytes); + NS_ENSURE_SUCCESS(rv, rv); + } + + // We might need some additional info for response to http/https request + nsCOMPtr httpChannel(do_QueryInterface(channel, &rv)); + if (httpChannel) { + // Status code + uint32_t statusCode; + rv = httpChannel->GetResponseStatus(&statusCode); + NS_ENSURE_SUCCESS(rv, rv); + builder->StatusCode(statusCode); + + // Headers + RefPtr visitor = new HeaderVisitor(builder); + rv = httpChannel->VisitResponseHeaders(visitor); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Headers for other responses + // try to provide some basic metadata about the response + nsString filename; + if (NS_SUCCEEDED(channel->GetContentDispositionFilename(filename))) { + builder->Header(jni::StringParam(u"content-disposition"_ns), + nsPrintfCString("attachment; filename=\"%s\"", + NS_ConvertUTF16toUTF8(filename).get())); + } + + nsCString contentType; + if (NS_SUCCEEDED(channel->GetContentType(contentType))) { + builder->Header(jni::StringParam(u"content-type"_ns), contentType); + } + + int64_t contentLength = 0; + if (NS_SUCCEEDED(channel->GetContentLength(&contentLength))) { + nsString contentLengthString; + contentLengthString.AppendInt(contentLength); + builder->Header(jni::StringParam(u"content-length"_ns), + contentLengthString); + } + } + + java::WebResponse::GlobalRef response = builder->Build(); + + SendWebResponse(response); + return NS_OK; +} + +void GeckoViewStreamListener::InitializeStreamSupport(nsIRequest* aRequest) { + StreamSupport::Init(); + + mSupport = java::GeckoInputStream::Support::New(); + StreamSupport::AttachNative( + mSupport, mozilla::MakeUnique(mSupport, aRequest)); +} + +std::tuple +GeckoViewStreamListener::CertificateFromChannel(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + + nsCOMPtr securityInfo; + aChannel->GetSecurityInfo(getter_AddRefs(securityInfo)); + if (!securityInfo) { + return std::make_tuple((jni::ByteArray::LocalRef) nullptr, + (java::sdk::Boolean::LocalRef) nullptr); + } + + uint32_t securityState = 0; + securityInfo->GetSecurityState(&securityState); + auto isSecure = securityState == nsIWebProgressListener::STATE_IS_SECURE + ? java::sdk::Boolean::TRUE() + : java::sdk::Boolean::FALSE(); + + nsCOMPtr cert; + securityInfo->GetServerCert(getter_AddRefs(cert)); + if (!cert) { + return std::make_tuple((jni::ByteArray::LocalRef) nullptr, + (java::sdk::Boolean::LocalRef) nullptr); + } + + nsTArray derBytes; + nsresult rv = cert->GetRawDER(derBytes); + NS_ENSURE_SUCCESS(rv, + std::make_tuple((jni::ByteArray::LocalRef) nullptr, + (java::sdk::Boolean::LocalRef) nullptr)); + + auto certBytes = jni::ByteArray::New( + reinterpret_cast(derBytes.Elements()), derBytes.Length()); + + return std::make_tuple(certBytes, isSecure); +} diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.h b/mobile/android/components/geckoview/GeckoViewStreamListener.h new file mode 100644 index 0000000000..b42249f458 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewStreamListener.h @@ -0,0 +1,57 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef GeckoViewStreamListener_h__ +#define GeckoViewStreamListener_h__ + +#include "nsIStreamListener.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" + +#include "mozilla/widget/EventDispatcher.h" +#include "mozilla/java/GeckoInputStreamNatives.h" +#include "mozilla/java/WebResponseWrappers.h" + +#include "JavaBuiltins.h" + +namespace mozilla { + +class GeckoViewStreamListener : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + explicit GeckoViewStreamListener() {} + + static std::tuple + CertificateFromChannel(nsIChannel* aChannel); + + protected: + virtual ~GeckoViewStreamListener() {} + + java::GeckoInputStream::GlobalRef mStream; + java::GeckoInputStream::Support::GlobalRef mSupport; + + void InitializeStreamSupport(nsIRequest* aRequest); + + static nsresult WriteSegment(nsIInputStream* aInputStream, void* aClosure, + const char* aFromSegment, uint32_t aToOffset, + uint32_t aCount, uint32_t* aWriteCount); + + virtual nsresult HandleWebResponse(nsIRequest* aRequest); + + virtual void SendWebResponse(java::WebResponse::Param aResponse) = 0; + + virtual void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) = 0; +}; + +} // namespace mozilla + +#endif // GeckoViewStreamListener_h__ diff --git a/mobile/android/components/geckoview/LoginStorageDelegate.jsm b/mobile/android/components/geckoview/LoginStorageDelegate.jsm new file mode 100644 index 0000000000..c5d3b118f4 --- /dev/null +++ b/mobile/android/components/geckoview/LoginStorageDelegate.jsm @@ -0,0 +1,138 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["LoginStorageDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.jsm", + LoginEntry: "resource://gre/modules/GeckoViewAutocomplete.jsm", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("LoginStorageDelegate"); + +// Sync with LoginSaveOption.Hint in Autocomplete.java. +const LoginStorageHint = { + NONE: 0, + GENERATED: 1 << 0, + LOW_CONFIDENCE: 1 << 1, +}; + +class LoginStorageDelegate { + _createMessage({ dismissed, autoSavedLoginGuid }, aLogins) { + let hint = LoginStorageHint.NONE; + if (dismissed) { + hint |= LoginStorageHint.LOW_CONFIDENCE; + } + if (autoSavedLoginGuid) { + hint |= LoginStorageHint.GENERATED; + } + return { + // Sync with PromptController + type: "Autocomplete:Save:Login", + hint, + logins: aLogins, + }; + } + + promptToSavePassword( + aBrowser, + aLogin, + dismissed = false, + notifySaved = false + ) { + const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal); + prompt.asyncShowPrompt( + this._createMessage({ dismissed }, [ + lazy.LoginEntry.fromLoginInfo(aLogin), + ]), + result => { + const selectedLogin = result?.selection?.value; + + if (!selectedLogin) { + return; + } + + const loginInfo = lazy.LoginEntry.parse(selectedLogin).toLoginInfo(); + Services.obs.notifyObservers(loginInfo, "passwordmgr-prompt-save"); + + lazy.GeckoViewAutocomplete.onLoginSave(selectedLogin); + } + ); + + return { + dismiss() { + prompt.dismiss(); + }, + }; + } + + promptToChangePassword( + aBrowser, + aOldLogin, + aNewLogin, + dismissed = false, + notifySaved = false, + autoSavedLoginGuid = "" + ) { + const newLogin = lazy.LoginEntry.fromLoginInfo(aOldLogin || aNewLogin); + const oldGuid = (aOldLogin && newLogin.guid) || null; + newLogin.origin = aNewLogin.origin; + newLogin.formActionOrigin = aNewLogin.formActionOrigin; + newLogin.password = aNewLogin.password; + newLogin.username = aNewLogin.username; + + const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal); + prompt.asyncShowPrompt( + this._createMessage({ dismissed, autoSavedLoginGuid }, [newLogin]), + result => { + const selectedLogin = result?.selection?.value; + + if (!selectedLogin) { + return; + } + + lazy.GeckoViewAutocomplete.onLoginSave(selectedLogin); + + const loginInfo = lazy.LoginEntry.parse(selectedLogin).toLoginInfo(); + Services.obs.notifyObservers( + loginInfo, + "passwordmgr-prompt-change", + oldGuid + ); + } + ); + + return { + dismiss() { + prompt.dismiss(); + }, + }; + } + + promptToChangePasswordWithUsernames(aBrowser, aLogins, aNewLogin) { + this.promptToChangePassword(aBrowser, null /* oldLogin */, aNewLogin); + } +} + +LoginStorageDelegate.prototype.classID = Components.ID( + "{3d765750-1c3d-11ea-aaef-0800200c9a66}" +); +LoginStorageDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsILoginManagerPrompter", +]); diff --git a/mobile/android/components/geckoview/PromptCollection.jsm b/mobile/android/components/geckoview/PromptCollection.jsm new file mode 100644 index 0000000000..16ec60fd2e --- /dev/null +++ b/mobile/android/components/geckoview/PromptCollection.jsm @@ -0,0 +1,48 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["PromptCollection"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("PromptCollection"); + +class PromptCollection { + confirmRepost(browsingContext) { + const msg = { + type: "repost", + }; + const prompter = new lazy.GeckoViewPrompter(browsingContext); + const result = prompter.showPrompt(msg); + return !!result?.allow; + } + + asyncBeforeUnloadCheck(browsingContext) { + return new Promise(resolve => { + const msg = { + type: "beforeUnload", + }; + const prompter = new lazy.GeckoViewPrompter(browsingContext); + prompter.asyncShowPrompt(msg, resolve); + }).then(result => !!result?.allow); + } + + confirmFolderUpload() { + // Folder upload is not supported by GeckoView yet, see Bug 1674428. + return false; + } +} + +PromptCollection.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIPromptCollection", +]); diff --git a/mobile/android/components/geckoview/ShareDelegate.jsm b/mobile/android/components/geckoview/ShareDelegate.jsm new file mode 100644 index 0000000000..1e3a133953 --- /dev/null +++ b/mobile/android/components/geckoview/ShareDelegate.jsm @@ -0,0 +1,82 @@ +/* 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"; + +var EXPORTED_SYMBOLS = ["ShareDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const domBundle = Services.strings.createBundle( + "chrome://global/locale/dom/dom.properties" +); + +const { debug, warn } = GeckoViewUtils.initLogging("ShareDelegate"); + +class ShareDelegate { + init(aParent) { + this._openerWindow = aParent; + } + + get openerWindow() { + return this._openerWindow; + } + + async share(aTitle, aText, aUri) { + const ABORT = 2; + const FAILURE = 1; + const SUCCESS = 0; + + const msg = { + type: "share", + title: aTitle, + text: aText, + uri: aUri ? aUri.displaySpec : null, + }; + const prompt = new lazy.GeckoViewPrompter(this._openerWindow); + const result = await new Promise(resolve => { + prompt.asyncShowPrompt(msg, resolve); + }); + + if (!result) { + // A null result is treated as a dismissal in GeckoViewPrompter. + throw new DOMException( + domBundle.GetStringFromName("WebShareAPI_Aborted"), + "AbortError" + ); + } + + const res = result && result.response; + switch (res) { + case FAILURE: + throw new DOMException( + domBundle.GetStringFromName("WebShareAPI_Failed"), + "DataError" + ); + case ABORT: // Handle aborted attempt and invalid responses the same. + throw new DOMException( + domBundle.GetStringFromName("WebShareAPI_Aborted"), + "AbortError" + ); + case SUCCESS: + return; + default: + throw new DOMException("Unknown error.", "UnknownError"); + } + } +} + +ShareDelegate.prototype.classID = Components.ID( + "{1201d357-8417-4926-a694-e6408fbedcf8}" +); +ShareDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsISharePicker", +]); diff --git a/mobile/android/components/geckoview/components.conf b/mobile/android/components/geckoview/components.conf new file mode 100644 index 0000000000..d33567bb74 --- /dev/null +++ b/mobile/android/components/geckoview/components.conf @@ -0,0 +1,93 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{3e30d2a0-9934-11ea-bb37-0242ac130002}', + 'contract_ids': ['@mozilla.org/embedcomp/prompt-collection;1'], + 'jsm': 'resource://gre/modules/PromptCollection.jsm', + 'constructor': 'PromptCollection', + }, + { + 'js_name': 'prompt', + 'cid': '{076ac188-23c1-4390-aa08-7ef1f78ca5d9}', + 'contract_ids': [ + '@mozilla.org/prompter;1', + ], + 'interfaces': ['nsIPromptService'], + 'jsm': 'resource://gre/modules/GeckoViewPrompt.jsm', + 'constructor': 'PromptFactory', + }, + { + 'cid': '{8e993c34-fdd6-432c-967e-f995d888777f}', + 'contract_ids': ['@mozilla.org/geckoview/startup;1'], + 'jsm': 'resource://gre/modules/GeckoViewStartup.jsm', + 'constructor': 'GeckoViewStartup', + }, + { + 'cid': '{42f3c238-e8e8-4015-9ca2-148723a8afcf}', + 'contract_ids': ['@mozilla.org/content-permission/prompt;1'], + 'jsm': 'resource://gre/modules/GeckoViewPermission.jsm', + 'constructor': 'GeckoViewPermission', + }, + { + 'cid': '{a54d84d7-98a4-4fec-b664-e42e512ae9cc}', + 'contract_ids': ['@mozilla.org/push/Service;1'], + 'jsm': 'resource://gre/modules/GeckoViewPush.jsm', + 'constructor': 'PushService', + }, + { + 'cid': '{fc4bec74-ddd0-4ea8-9a66-9a5081258e32}', + 'contract_ids': ['@mozilla.org/parent/colorpicker;1'], + 'jsm': 'resource://gre/modules/ColorPickerDelegate.jsm', + 'constructor': 'ColorPickerDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{25fdbae6-f684-4bf0-b773-ff2b7a6273c8}', + 'contract_ids': ['@mozilla.org/parent/filepicker;1'], + 'jsm': 'resource://gre/modules/FilePickerDelegate.jsm', + 'constructor': 'FilePickerDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{1201d357-8417-4926-a694-e6408fbedcf8}', + 'contract_ids': ['@mozilla.org/sharepicker;1'], + 'jsm': 'resource://gre/modules/ShareDelegate.jsm', + 'constructor': 'ShareDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{3d765750-1c3d-11ea-aaef-0800200c9a66}', + 'contract_ids': ['@mozilla.org/login-manager/prompter;1'], + 'jsm': 'resource://gre/modules/LoginStorageDelegate.jsm', + 'constructor': 'LoginStorageDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{91455c77-64a1-4c37-be00-f94eb9c7b8e1}', + 'contract_ids': [ + '@mozilla.org/uriloader/external-helper-app-service;1', + ], + 'type': 'GeckoViewExternalAppService', + 'constructor': 'GeckoViewExternalAppService::GetSingleton', + 'headers': ['GeckoViewExternalAppService.h'], + 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS, + }, +] + +if defined('MOZ_ANDROID_HISTORY'): + Classes += [ + { + 'name': 'History', + 'cid': '{0937a705-91a6-417a-8292-b22eb10da86c}', + 'contract_ids': ['@mozilla.org/browser/history;1'], + 'singleton': True, + 'type': 'GeckoViewHistory', + 'headers': ['GeckoViewHistory.h'], + 'constructor': 'GeckoViewHistory::GetSingleton', + }, + ] diff --git a/mobile/android/components/geckoview/moz.build b/mobile/android/components/geckoview/moz.build new file mode 100644 index 0000000000..607b02f367 --- /dev/null +++ b/mobile/android/components/geckoview/moz.build @@ -0,0 +1,49 @@ +# -*- 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/. + +SOURCES += [ + "GeckoViewExternalAppService.cpp", + "GeckoViewOutputStream.cpp", + "GeckoViewStreamListener.cpp", +] + +EXPORTS += [ + "GeckoViewExternalAppService.h", + "GeckoViewOutputStream.h", + "GeckoViewStreamListener.h", +] + +if CONFIG["MOZ_ANDROID_HISTORY"]: + EXPORTS += [ + "GeckoViewHistory.h", + ] + SOURCES += [ + "GeckoViewHistory.cpp", + ] + include("/ipc/chromium/chromium-config.mozbuild") + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXTRA_COMPONENTS += [ + "GeckoView.manifest", +] + +EXTRA_JS_MODULES += [ + "ColorPickerDelegate.jsm", + "FilePickerDelegate.jsm", + "GeckoViewPermission.jsm", + "GeckoViewPrompt.jsm", + "GeckoViewPrompter.sys.mjs", + "GeckoViewPush.jsm", + "GeckoViewStartup.jsm", + "LoginStorageDelegate.jsm", + "PromptCollection.jsm", + "ShareDelegate.jsm", +] + +FINAL_LIBRARY = "xul" diff --git a/mobile/android/components/moz.build b/mobile/android/components/moz.build new file mode 100644 index 0000000000..a6300de349 --- /dev/null +++ b/mobile/android/components/moz.build @@ -0,0 +1,16 @@ +# -*- 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/. + +with Files("**"): + BUG_COMPONENT = ("GeckoView", "General") + +with Files("extensions/**"): + BUG_COMPONENT = ("WebExtensions", "Android") + +DIRS += [ + "extensions", + "geckoview", +] -- cgit v1.2.3