From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- toolkit/actors/AboutHttpsOnlyErrorChild.sys.mjs | 83 + toolkit/actors/AboutHttpsOnlyErrorParent.sys.mjs | 48 + toolkit/actors/AudioPlaybackChild.sys.mjs | 20 + toolkit/actors/AudioPlaybackParent.sys.mjs | 42 + toolkit/actors/AutoCompleteChild.sys.mjs | 197 ++ toolkit/actors/AutoCompleteParent.sys.mjs | 516 ++++ toolkit/actors/AutoScrollChild.sys.mjs | 445 +++ toolkit/actors/AutoScrollParent.sys.mjs | 48 + toolkit/actors/AutoplayChild.sys.mjs | 10 + toolkit/actors/AutoplayParent.sys.mjs | 17 + toolkit/actors/BackgroundThumbnailsChild.sys.mjs | 102 + toolkit/actors/BrowserElementChild.sys.mjs | 35 + toolkit/actors/BrowserElementParent.sys.mjs | 36 + toolkit/actors/ClipboardReadPasteChild.sys.mjs | 39 + toolkit/actors/ClipboardReadPasteParent.sys.mjs | 194 ++ toolkit/actors/ContentMetaChild.sys.mjs | 199 ++ toolkit/actors/ContentMetaParent.sys.mjs | 23 + toolkit/actors/ControllersChild.sys.mjs | 63 + toolkit/actors/ControllersParent.sys.mjs | 90 + toolkit/actors/DateTimePickerChild.sys.mjs | 204 ++ toolkit/actors/DateTimePickerParent.sys.mjs | 157 + toolkit/actors/ExtFindChild.sys.mjs | 31 + toolkit/actors/FindBarChild.sys.mjs | 157 + toolkit/actors/FindBarParent.sys.mjs | 47 + toolkit/actors/FinderChild.sys.mjs | 129 + toolkit/actors/InlineSpellCheckerChild.sys.mjs | 38 + toolkit/actors/InlineSpellCheckerParent.sys.mjs | 50 + .../actors/KeyPressEventModelCheckerChild.sys.mjs | 107 + toolkit/actors/NetErrorChild.sys.mjs | 244 ++ toolkit/actors/NetErrorParent.sys.mjs | 353 +++ toolkit/actors/PictureInPictureChild.sys.mjs | 3152 ++++++++++++++++++++ toolkit/actors/PopupBlockingChild.sys.mjs | 147 + toolkit/actors/PopupBlockingParent.sys.mjs | 268 ++ toolkit/actors/PrintingChild.sys.mjs | 260 ++ toolkit/actors/PrintingParent.sys.mjs | 22 + toolkit/actors/PrintingSelectionChild.sys.mjs | 20 + toolkit/actors/PurgeSessionHistoryChild.sys.mjs | 35 + toolkit/actors/RemotePageChild.sys.mjs | 219 ++ toolkit/actors/SelectChild.sys.mjs | 470 +++ toolkit/actors/SelectParent.sys.mjs | 798 +++++ toolkit/actors/TestProcessActorChild.jsm | 59 + toolkit/actors/TestProcessActorChild.sys.mjs | 56 + toolkit/actors/TestProcessActorParent.jsm | 41 + toolkit/actors/TestProcessActorParent.sys.mjs | 38 + toolkit/actors/TestWindowChild.jsm | 102 + toolkit/actors/TestWindowChild.sys.mjs | 99 + toolkit/actors/TestWindowParent.jsm | 49 + toolkit/actors/TestWindowParent.sys.mjs | 46 + toolkit/actors/ThumbnailsChild.sys.mjs | 60 + toolkit/actors/UAWidgetsChild.sys.mjs | 236 ++ toolkit/actors/UnselectedTabHoverChild.sys.mjs | 20 + toolkit/actors/UnselectedTabHoverParent.sys.mjs | 15 + toolkit/actors/ViewSourceChild.sys.mjs | 346 +++ toolkit/actors/ViewSourcePageChild.sys.mjs | 474 +++ toolkit/actors/ViewSourcePageParent.sys.mjs | 165 + toolkit/actors/WebChannelChild.sys.mjs | 132 + toolkit/actors/WebChannelParent.sys.mjs | 92 + .../picture-in-picture-child-video-wrapper-api.rst | 6 + toolkit/actors/moz.build | 85 + 59 files changed, 11236 insertions(+) create mode 100644 toolkit/actors/AboutHttpsOnlyErrorChild.sys.mjs create mode 100644 toolkit/actors/AboutHttpsOnlyErrorParent.sys.mjs create mode 100644 toolkit/actors/AudioPlaybackChild.sys.mjs create mode 100644 toolkit/actors/AudioPlaybackParent.sys.mjs create mode 100644 toolkit/actors/AutoCompleteChild.sys.mjs create mode 100644 toolkit/actors/AutoCompleteParent.sys.mjs create mode 100644 toolkit/actors/AutoScrollChild.sys.mjs create mode 100644 toolkit/actors/AutoScrollParent.sys.mjs create mode 100644 toolkit/actors/AutoplayChild.sys.mjs create mode 100644 toolkit/actors/AutoplayParent.sys.mjs create mode 100644 toolkit/actors/BackgroundThumbnailsChild.sys.mjs create mode 100644 toolkit/actors/BrowserElementChild.sys.mjs create mode 100644 toolkit/actors/BrowserElementParent.sys.mjs create mode 100644 toolkit/actors/ClipboardReadPasteChild.sys.mjs create mode 100644 toolkit/actors/ClipboardReadPasteParent.sys.mjs create mode 100644 toolkit/actors/ContentMetaChild.sys.mjs create mode 100644 toolkit/actors/ContentMetaParent.sys.mjs create mode 100644 toolkit/actors/ControllersChild.sys.mjs create mode 100644 toolkit/actors/ControllersParent.sys.mjs create mode 100644 toolkit/actors/DateTimePickerChild.sys.mjs create mode 100644 toolkit/actors/DateTimePickerParent.sys.mjs create mode 100644 toolkit/actors/ExtFindChild.sys.mjs create mode 100644 toolkit/actors/FindBarChild.sys.mjs create mode 100644 toolkit/actors/FindBarParent.sys.mjs create mode 100644 toolkit/actors/FinderChild.sys.mjs create mode 100644 toolkit/actors/InlineSpellCheckerChild.sys.mjs create mode 100644 toolkit/actors/InlineSpellCheckerParent.sys.mjs create mode 100644 toolkit/actors/KeyPressEventModelCheckerChild.sys.mjs create mode 100644 toolkit/actors/NetErrorChild.sys.mjs create mode 100644 toolkit/actors/NetErrorParent.sys.mjs create mode 100644 toolkit/actors/PictureInPictureChild.sys.mjs create mode 100644 toolkit/actors/PopupBlockingChild.sys.mjs create mode 100644 toolkit/actors/PopupBlockingParent.sys.mjs create mode 100644 toolkit/actors/PrintingChild.sys.mjs create mode 100644 toolkit/actors/PrintingParent.sys.mjs create mode 100644 toolkit/actors/PrintingSelectionChild.sys.mjs create mode 100644 toolkit/actors/PurgeSessionHistoryChild.sys.mjs create mode 100644 toolkit/actors/RemotePageChild.sys.mjs create mode 100644 toolkit/actors/SelectChild.sys.mjs create mode 100644 toolkit/actors/SelectParent.sys.mjs create mode 100644 toolkit/actors/TestProcessActorChild.jsm create mode 100644 toolkit/actors/TestProcessActorChild.sys.mjs create mode 100644 toolkit/actors/TestProcessActorParent.jsm create mode 100644 toolkit/actors/TestProcessActorParent.sys.mjs create mode 100644 toolkit/actors/TestWindowChild.jsm create mode 100644 toolkit/actors/TestWindowChild.sys.mjs create mode 100644 toolkit/actors/TestWindowParent.jsm create mode 100644 toolkit/actors/TestWindowParent.sys.mjs create mode 100644 toolkit/actors/ThumbnailsChild.sys.mjs create mode 100644 toolkit/actors/UAWidgetsChild.sys.mjs create mode 100644 toolkit/actors/UnselectedTabHoverChild.sys.mjs create mode 100644 toolkit/actors/UnselectedTabHoverParent.sys.mjs create mode 100644 toolkit/actors/ViewSourceChild.sys.mjs create mode 100644 toolkit/actors/ViewSourcePageChild.sys.mjs create mode 100644 toolkit/actors/ViewSourcePageParent.sys.mjs create mode 100644 toolkit/actors/WebChannelChild.sys.mjs create mode 100644 toolkit/actors/WebChannelParent.sys.mjs create mode 100644 toolkit/actors/docs/picture-in-picture-child-video-wrapper-api.rst create mode 100644 toolkit/actors/moz.build (limited to 'toolkit/actors') diff --git a/toolkit/actors/AboutHttpsOnlyErrorChild.sys.mjs b/toolkit/actors/AboutHttpsOnlyErrorChild.sys.mjs new file mode 100644 index 0000000000..20313d30c0 --- /dev/null +++ b/toolkit/actors/AboutHttpsOnlyErrorChild.sys.mjs @@ -0,0 +1,83 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "@mozilla.org/network/serialization-helper;1", + "nsISerializationHelper" +); + +export class AboutHttpsOnlyErrorChild extends RemotePageChild { + actorCreated() { + super.actorCreated(); + + // If you add a new function, remember to add it to RemotePageAccessManager.sys.mjs + // to allow content-privileged about:httpsonlyerror to use it. + const exportableFunctions = [ + "RPMTryPingSecureWWWLink", + "RPMOpenSecureWWWLink", + ]; + this.exportFunctions(exportableFunctions); + } + + RPMTryPingSecureWWWLink() { + // try if the page can be reached with www prefix + // if so send message to the parent to send message to the error page to display + // suggestion button for www + + const httpsOnlySuggestionPref = Services.prefs.getBoolPref( + "dom.security.https_only_mode_error_page_user_suggestions" + ); + + // only check if pref is true otherwise return + if (!httpsOnlySuggestionPref) { + return; + } + + // get the host url without the path with www in front + const wwwURL = "https://www." + this.contentWindow.location.host; + fetch(wwwURL, { + credentials: "omit", + cache: "no-store", + }) + .then(data => { + if (data.status === 200) { + this.contentWindow.dispatchEvent( + new this.contentWindow.CustomEvent("pingSecureWWWLinkSuccess") + ); + } + }) + .catch(() => { + dump("No secure www suggestion possible for " + wwwURL); + }); + } + + RPMOpenSecureWWWLink() { + // if user wants to visit suggested secure www page: visit page with www prefix and delete errorpage from history + const context = this.manager.browsingContext; + const docShell = context.docShell; + const httpChannel = docShell.failedChannel.QueryInterface( + Ci.nsIHttpChannel + ); + const webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + const triggeringPrincipal = + docShell.failedChannel.loadInfo.triggeringPrincipal; + const oldURI = httpChannel.URI; + const newWWWURI = oldURI + .mutate() + .setHost("www." + oldURI.host) + .finalize(); + + webNav.loadURI(newWWWURI, { + triggeringPrincipal, + loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY, + }); + } +} diff --git a/toolkit/actors/AboutHttpsOnlyErrorParent.sys.mjs b/toolkit/actors/AboutHttpsOnlyErrorParent.sys.mjs new file mode 100644 index 0000000000..a7a4b23cfa --- /dev/null +++ b/toolkit/actors/AboutHttpsOnlyErrorParent.sys.mjs @@ -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/. */ + +const { HomePage } = ChromeUtils.import("resource:///modules/HomePage.jsm"); +import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; + +export class AboutHttpsOnlyErrorParent extends JSWindowActorParent { + get browser() { + return this.browsingContext.top.embedderElement; + } + + receiveMessage(aMessage) { + switch (aMessage.name) { + case "goBack": + this.goBackFromErrorPage(this.browser); + break; + } + } + + goBackFromErrorPage(aBrowser) { + if (!aBrowser.canGoBack) { + // If the unsafe page is the first or the only one in history, go to the + // start page. + aBrowser.fixupAndLoadURIString( + this.getDefaultHomePage(aBrowser.ownerGlobal), + { + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + } + ); + } else { + aBrowser.goBack(); + } + } + + getDefaultHomePage(win) { + if (PrivateBrowsingUtils.isWindowPrivate(win)) { + return win.BROWSER_NEW_TAB_URL || "about:blank"; + } + let url = HomePage.getDefault(); + // If url is a pipe-delimited set of pages, just take the first one. + if (url.includes("|")) { + url = url.split("|")[0]; + } + return url; + } +} diff --git a/toolkit/actors/AudioPlaybackChild.sys.mjs b/toolkit/actors/AudioPlaybackChild.sys.mjs new file mode 100644 index 0000000000..a392a73464 --- /dev/null +++ b/toolkit/actors/AudioPlaybackChild.sys.mjs @@ -0,0 +1,20 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class AudioPlaybackChild extends JSWindowActorChild { + observe(subject, topic, data) { + if (topic === "audio-playback") { + let name = "AudioPlayback:"; + if (data === "activeMediaBlockStart") { + name += "ActiveMediaBlockStart"; + } else if (data === "activeMediaBlockStop") { + name += "ActiveMediaBlockStop"; + } else { + name += data === "active" ? "Start" : "Stop"; + } + this.sendAsyncMessage(name); + } + } +} diff --git a/toolkit/actors/AudioPlaybackParent.sys.mjs b/toolkit/actors/AudioPlaybackParent.sys.mjs new file mode 100644 index 0000000000..db682fd90b --- /dev/null +++ b/toolkit/actors/AudioPlaybackParent.sys.mjs @@ -0,0 +1,42 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class AudioPlaybackParent extends JSWindowActorParent { + constructor() { + super(); + this._hasAudioPlayback = false; + this._hasBlockMedia = false; + } + receiveMessage(aMessage) { + const browser = this.browsingContext.top.embedderElement; + switch (aMessage.name) { + case "AudioPlayback:Start": + this._hasAudioPlayback = true; + browser.audioPlaybackStarted(); + break; + case "AudioPlayback:Stop": + this._hasAudioPlayback = false; + browser.audioPlaybackStopped(); + break; + case "AudioPlayback:ActiveMediaBlockStart": + this._hasBlockMedia = true; + browser.activeMediaBlockStarted(); + break; + case "AudioPlayback:ActiveMediaBlockStop": + this._hasBlockMedia = false; + browser.activeMediaBlockStopped(); + break; + } + } + didDestroy() { + const browser = this.browsingContext.top.embedderElement; + if (browser && this._hasAudioPlayback) { + browser.audioPlaybackStopped(); + } + if (browser && this._hasBlockMedia) { + browser.activeMediaBlockStopped(); + } + } +} diff --git a/toolkit/actors/AutoCompleteChild.sys.mjs b/toolkit/actors/AutoCompleteChild.sys.mjs new file mode 100644 index 0000000000..a405834ca9 --- /dev/null +++ b/toolkit/actors/AutoCompleteChild.sys.mjs @@ -0,0 +1,197 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +/* eslint no-unused-vars: ["error", {args: "none"}] */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs", + LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", +}); + +let autoCompleteListeners = new Set(); + +export class AutoCompleteChild extends JSWindowActorChild { + constructor() { + super(); + + this._input = null; + this._popupOpen = false; + } + + static addPopupStateListener(listener) { + autoCompleteListeners.add(listener); + } + + static removePopupStateListener(listener) { + autoCompleteListeners.delete(listener); + } + + receiveMessage(message) { + switch (message.name) { + case "FormAutoComplete:HandleEnter": { + this.selectedIndex = message.data.selectedIndex; + + let controller = Cc[ + "@mozilla.org/autocomplete/controller;1" + ].getService(Ci.nsIAutoCompleteController); + controller.handleEnter(message.data.isPopupSelection); + break; + } + + case "FormAutoComplete:PopupClosed": { + this._popupOpen = false; + this.notifyListeners(message.name, message.data); + break; + } + + case "FormAutoComplete:PopupOpened": { + this._popupOpen = true; + this.notifyListeners(message.name, message.data); + break; + } + + case "FormAutoComplete:Focus": { + // XXX See bug 1582722 + // Before bug 1573836, the messages here didn't match + // ("FormAutoComplete:Focus" versus "FormAutoComplete:RequestFocus") + // so this was never called. However this._input is actually a + // nsIAutoCompleteInput, which doesn't have a focus() method, so it + // wouldn't have worked anyway. So for now, I have just disabled this. + /* + if (this._input) { + this._input.focus(); + } + */ + break; + } + } + } + + notifyListeners(messageName, data) { + for (let listener of autoCompleteListeners) { + try { + listener.popupStateChanged(messageName, data, this.contentWindow); + } catch (ex) { + console.error(ex); + } + } + } + + get input() { + return this._input; + } + + set selectedIndex(index) { + this.sendAsyncMessage("FormAutoComplete:SetSelectedIndex", { index }); + } + + get selectedIndex() { + // selectedIndex getter must be synchronous because we need the + // correct value when the controller is in controller::HandleEnter. + // We can't easily just let the parent inform us the new value every + // time it changes because not every action that can change the + // selectedIndex is trivial to catch (e.g. moving the mouse over the + // list). + let selectedIndexResult = Services.cpmm.sendSyncMessage( + "FormAutoComplete:GetSelectedIndex", + { + browsingContext: this.browsingContext, + } + ); + + if ( + selectedIndexResult.length != 1 || + !Number.isInteger(selectedIndexResult[0]) + ) { + throw new Error("Invalid autocomplete selectedIndex"); + } + return selectedIndexResult[0]; + } + + get popupOpen() { + return this._popupOpen; + } + + openAutocompletePopup(input, element) { + if (this._popupOpen || !input) { + return; + } + + let rect = lazy.LayoutUtils.getElementBoundingScreenRect(element); + let window = element.ownerGlobal; + let dir = window.getComputedStyle(element).direction; + let results = this.getResultsFromController(input); + let formOrigin = lazy.LoginHelper.getLoginOrigin( + element.ownerDocument.documentURI + ); + let inputElementIdentifier = lazy.ContentDOMReference.get(element); + + this.sendAsyncMessage("FormAutoComplete:MaybeOpenPopup", { + results, + rect, + dir, + inputElementIdentifier, + formOrigin, + }); + + this._input = input; + } + + closePopup() { + // We set this here instead of just waiting for the + // PopupClosed message to do it so that we don't end + // up in a state where the content thinks that a popup + // is open when it isn't (or soon won't be). + this._popupOpen = false; + this.sendAsyncMessage("FormAutoComplete:ClosePopup", {}); + } + + invalidate() { + if (this._popupOpen) { + let results = this.getResultsFromController(this._input); + this.sendAsyncMessage("FormAutoComplete:Invalidate", { results }); + } + } + + selectBy(reverse, page) { + Services.cpmm.sendSyncMessage("FormAutoComplete:SelectBy", { + browsingContext: this.browsingContext, + reverse, + page, + }); + } + + getResultsFromController(inputField) { + let results = []; + + if (!inputField) { + return results; + } + + let controller = inputField.controller; + if (!(controller instanceof Ci.nsIAutoCompleteController)) { + return results; + } + + for (let i = 0; i < controller.matchCount; ++i) { + let result = {}; + result.value = controller.getValueAt(i); + result.label = controller.getLabelAt(i); + result.comment = controller.getCommentAt(i); + result.style = controller.getStyleAt(i); + result.image = controller.getImageAt(i); + results.push(result); + } + + return results; + } +} + +AutoCompleteChild.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIAutoCompletePopup", +]); diff --git a/toolkit/actors/AutoCompleteParent.sys.mjs b/toolkit/actors/AutoCompleteParent.sys.mjs new file mode 100644 index 0000000000..82ebf22dcf --- /dev/null +++ b/toolkit/actors/AutoCompleteParent.sys.mjs @@ -0,0 +1,516 @@ +/* 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 { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.jsm", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "DELEGATE_AUTOCOMPLETE", + "toolkit.autocomplete.delegate", + false +); + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +const PREF_SECURITY_DELAY = "security.notification_enable_delay"; + +// Stores the actor that has the active popup, used by formfill +let currentActor = null; + +let autoCompleteListeners = new Set(); + +function compareContext(message) { + if ( + !currentActor || + (currentActor.browsingContext != message.data.browsingContext && + currentActor.browsingContext.top != message.data.browsingContext) + ) { + return false; + } + + return true; +} + +// These are two synchronous messages sent by the child. +// The browsingContext within the message data is either the one that has +// the active autocomplete popup or the top-level of the one that has +// the active autocomplete popup. +Services.ppmm.addMessageListener( + "FormAutoComplete:GetSelectedIndex", + message => { + if (compareContext(message)) { + let actor = currentActor; + if (actor && actor.openedPopup) { + return actor.openedPopup.selectedIndex; + } + } + + return -1; + } +); + +Services.ppmm.addMessageListener("FormAutoComplete:SelectBy", message => { + if (compareContext(message)) { + let actor = currentActor; + if (actor && actor.openedPopup) { + actor.openedPopup.selectBy(message.data.reverse, message.data.page); + } + } +}); + +// AutoCompleteResultView is an abstraction around a list of results. +// It implements enough of nsIAutoCompleteController and +// nsIAutoCompleteInput to make the richlistbox popup work. Since only +// one autocomplete popup should be open at a time, this is a singleton. +var AutoCompleteResultView = { + // nsISupports + QueryInterface: ChromeUtils.generateQI([ + "nsIAutoCompleteController", + "nsIAutoCompleteInput", + ]), + + // Private variables + results: [], + + // The AutoCompleteParent currently showing results or null otherwise. + currentActor: null, + + // nsIAutoCompleteController + get matchCount() { + return this.results.length; + }, + + getValueAt(index) { + return this.results[index].value; + }, + + getFinalCompleteValueAt(index) { + return this.results[index].value; + }, + + getLabelAt(index) { + // Backwardly-used by richlist autocomplete - see getCommentAt. + // The label is used for secondary information. + return this.results[index].comment; + }, + + getCommentAt(index) { + // The richlist autocomplete popup uses comment for its main + // display of an item, which is why we're returning the label + // here instead. + return this.results[index].label; + }, + + getStyleAt(index) { + return this.results[index].style; + }, + + getImageAt(index) { + return this.results[index].image; + }, + + handleEnter(aIsPopupSelection) { + if (this.currentActor) { + this.currentActor.handleEnter(aIsPopupSelection); + } + }, + + stopSearch() {}, + + searchString: "", + + // nsIAutoCompleteInput + get controller() { + return this; + }, + + get popup() { + return null; + }, + + _focus() { + if (this.currentActor) { + this.currentActor.requestFocus(); + } + }, + + // Internal JS-only API + clearResults() { + this.currentActor = null; + this.results = []; + }, + + setResults(actor, results) { + this.currentActor = actor; + this.results = results; + }, +}; + +export class AutoCompleteParent extends JSWindowActorParent { + didDestroy() { + if (this.openedPopup) { + this.openedPopup.closePopup(); + } + } + + static getCurrentActor() { + return currentActor; + } + + static addPopupStateListener(listener) { + autoCompleteListeners.add(listener); + } + + static removePopupStateListener(listener) { + autoCompleteListeners.delete(listener); + } + + handleEvent(evt) { + switch (evt.type) { + case "popupshowing": { + this.sendAsyncMessage("FormAutoComplete:PopupOpened", {}); + break; + } + + case "popuphidden": { + let selectedIndex = this.openedPopup.selectedIndex; + let selectedRowComment = + selectedIndex != -1 + ? AutoCompleteResultView.getCommentAt(selectedIndex) + : ""; + let selectedRowStyle = + selectedIndex != -1 + ? AutoCompleteResultView.getStyleAt(selectedIndex) + : ""; + this.sendAsyncMessage("FormAutoComplete:PopupClosed", { + selectedRowComment, + selectedRowStyle, + }); + AutoCompleteResultView.clearResults(); + // adjustHeight clears the height from the popup so that + // we don't have a big shrink effect if we closed with a + // large list, and then open on a small one. + this.openedPopup.adjustHeight(); + this.openedPopup = null; + currentActor = null; + evt.target.removeEventListener("popuphidden", this); + evt.target.removeEventListener("popupshowing", this); + break; + } + } + } + + showPopupWithResults({ rect, dir, results }) { + if (!results.length || this.openedPopup) { + // We shouldn't ever be showing an empty popup, and if we + // already have a popup open, the old one needs to close before + // we consider opening a new one. + return; + } + + let browser = this.browsingContext.top.embedderElement; + let window = browser.ownerGlobal; + // Also check window top in case this is a sidebar. + if ( + Services.focus.activeWindow !== window.top && + Services.focus.focusedWindow.top !== window.top + ) { + // We were sent a message from a window or tab that went into the + // background, so we'll ignore it for now. + return; + } + + // Non-empty result styles + let resultStyles = new Set(results.map(r => r.style).filter(r => !!r)); + currentActor = this; + this.openedPopup = browser.autoCompletePopup; + // the layout varies according to different result type + this.openedPopup.setAttribute("resultstyles", [...resultStyles].join(" ")); + this.openedPopup.hidden = false; + // don't allow the popup to become overly narrow + this.openedPopup.style.setProperty( + "--panel-width", + Math.max(100, rect.width) + "px" + ); + this.openedPopup.style.direction = dir; + + AutoCompleteResultView.setResults(this, results); + this.openedPopup.view = AutoCompleteResultView; + this.openedPopup.selectedIndex = -1; + + // Reset fields that were set from the last time the search popup was open + this.openedPopup.mInput = AutoCompleteResultView; + // Temporarily increase the maxRows as we don't want to show + // the scrollbar in login or form autofill popups. + if ( + resultStyles.size && + (resultStyles.has("autofill-profile") || resultStyles.has("loginsFooter")) + ) { + this.openedPopup._normalMaxRows = this.openedPopup.maxRows; + this.openedPopup.mInput.maxRows = 10; + } + browser.constrainPopup(this.openedPopup); + this.openedPopup.addEventListener("popuphidden", this); + this.openedPopup.addEventListener("popupshowing", this); + this.openedPopup.openPopupAtScreenRect( + "after_start", + rect.left, + rect.top, + rect.width, + rect.height, + false, + false + ); + this.openedPopup.invalidate(); + this._maybeRecordTelemetryEvents(results); + + // This is a temporary solution. We should replace it with + // proper meta information about the popup once such field + // becomes available. + let isCreditCard = results.some(result => + result?.comment?.includes("cc-number") + ); + + if (isCreditCard) { + this.delayPopupInput(); + } + } + + /** + * @param {object[]} results - Non-empty array of autocomplete results. + */ + _maybeRecordTelemetryEvents(results) { + let actor = + this.browsingContext.currentWindowGlobal.getActor("LoginManager"); + actor.maybeRecordPasswordGenerationShownTelemetryEvent(results); + + // Assume the result with the start time (loginsFooter) is last. + let lastResult = results[results.length - 1]; + if (lastResult.style != "loginsFooter") { + return; + } + + // The comment field of `loginsFooter` results have many additional pieces of + // information for telemetry purposes. After bug 1555209, this information + // can be passed to the parent process outside of nsIAutoCompleteResult APIs + // so we won't need this hack. + let rawExtraData = JSON.parse(lastResult.comment).telemetryEventData; + if (!rawExtraData.searchStartTimeMS) { + throw new Error("Invalid autocomplete search start time"); + } + + if (rawExtraData.stringLength > 1) { + // To reduce event volume, only record for lengths 0 and 1. + return; + } + + let duration = + Services.telemetry.msSystemNow() - rawExtraData.searchStartTimeMS; + delete rawExtraData.searchStartTimeMS; + + // Add counts by result style to rawExtraData. + results.reduce((accumulated, r) => { + // Ignore learn more as it is only added after importable logins. + // Do not track generic items in the telemetry. + if (r.style === "importableLearnMore" || r.style === "generic") { + return accumulated; + } + + // Keys can be a maximum of 15 characters and values must be strings. + // Also treat both "loginWithOrigin" and "login" as "login" as extra_keys + // is limited to 10. + let truncatedStyle = r.style.substring( + 0, + r.style === "loginWithOrigin" ? 5 : 15 + ); + accumulated[truncatedStyle] = (accumulated[truncatedStyle] || 0) + 1; + return accumulated; + }, rawExtraData); + + // Convert extra values to strings since recordEvent requires that. + let extraStrings = Object.fromEntries( + Object.entries(rawExtraData).map(([key, val]) => { + let stringVal = ""; + if (typeof val == "boolean") { + stringVal += val ? "1" : "0"; + } else { + stringVal += val; + } + return [key, stringVal]; + }) + ); + + Services.telemetry.recordEvent( + "form_autocomplete", + "show", + "logins", + // Convert to a string + duration + "", + extraStrings + ); + } + + invalidate(results) { + if (!this.openedPopup) { + return; + } + + if (!results.length) { + this.closePopup(); + } else { + AutoCompleteResultView.setResults(this, results); + this.openedPopup.invalidate(); + this._maybeRecordTelemetryEvents(results); + } + } + + closePopup() { + if (this.openedPopup) { + // Note that hidePopup() closes the popup immediately, + // so popuphiding or popuphidden events will be fired + // and handled during this call. + this.openedPopup.hidePopup(); + } + } + + receiveMessage(message) { + let browser = this.browsingContext.top.embedderElement; + + if ( + !browser || + (!lazy.DELEGATE_AUTOCOMPLETE && !browser.autoCompletePopup) + ) { + // If there is no browser or popup, just make sure that the popup has been closed. + if (this.openedPopup) { + this.openedPopup.closePopup(); + } + + // Returning false to pacify ESLint, but this return value is + // ignored by the messaging infrastructure. + return false; + } + + switch (message.name) { + case "FormAutoComplete:SetSelectedIndex": { + let { index } = message.data; + if (this.openedPopup) { + this.openedPopup.selectedIndex = index; + } + break; + } + + case "FormAutoComplete:MaybeOpenPopup": { + let { results, rect, dir, inputElementIdentifier, formOrigin } = + message.data; + if (lazy.DELEGATE_AUTOCOMPLETE) { + lazy.GeckoViewAutocomplete.delegateSelection({ + browsingContext: this.browsingContext, + options: results, + inputElementIdentifier, + formOrigin, + }); + } else { + this.showPopupWithResults({ results, rect, dir }); + this.notifyListeners(); + } + break; + } + + case "FormAutoComplete:Invalidate": { + let { results } = message.data; + this.invalidate(results); + break; + } + + case "FormAutoComplete:ClosePopup": { + if (lazy.DELEGATE_AUTOCOMPLETE) { + lazy.GeckoViewAutocomplete.delegateDismiss(); + break; + } + this.closePopup(); + break; + } + } + // Returning false to pacify ESLint, but this return value is + // ignored by the messaging infrastructure. + return false; + } + + // Imposes a brief period during which the popup will not respond to + // a click, so as to reduce the chances of a successful clickjacking + // attempt + delayPopupInput() { + if (!this.openedPopup) { + return; + } + const popupDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY); + + // Mochitests set this to 0, and many will fail on integration + // if we make the popup items inactive, even briefly. + if (!popupDelay) { + return; + } + + const items = Array.from( + this.openedPopup.getElementsByTagName("richlistitem") + ); + items.forEach(item => (item.disabled = true)); + + lazy.setTimeout( + () => items.forEach(item => (item.disabled = false)), + popupDelay + ); + } + + notifyListeners() { + let window = this.browsingContext.top.embedderElement.ownerGlobal; + for (let listener of autoCompleteListeners) { + try { + listener(window); + } catch (ex) { + console.error(ex); + } + } + } + + /** + * Despite its name, this handleEnter is only called when the user clicks on + * one of the items in the popup since the popup is rendered in the parent process. + * The real controller's handleEnter is called directly in the content process + * for other methods of completing a selection (e.g. using the tab or enter + * keys) since the field with focus is in that process. + * @param {boolean} aIsPopupSelection + */ + handleEnter(aIsPopupSelection) { + if (this.openedPopup) { + this.sendAsyncMessage("FormAutoComplete:HandleEnter", { + selectedIndex: this.openedPopup.selectedIndex, + isPopupSelection: aIsPopupSelection, + }); + } + } + + stopSearch() {} + + /** + * Sends a message to the browser that is requesting the input + * that the open popup should be focused. + */ + requestFocus() { + // Bug 1582722 - See the response in AutoCompleteChild.jsm for why this disabled. + /* + if (this.openedPopup) { + this.sendAsyncMessage("FormAutoComplete:Focus"); + } + */ + } +} diff --git a/toolkit/actors/AutoScrollChild.sys.mjs b/toolkit/actors/AutoScrollChild.sys.mjs new file mode 100644 index 0000000000..88fa078c28 --- /dev/null +++ b/toolkit/actors/AutoScrollChild.sys.mjs @@ -0,0 +1,445 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", +}); + +export class AutoScrollChild extends JSWindowActorChild { + constructor() { + super(); + + this._scrollable = null; + this._scrolldir = ""; + this._startX = null; + this._startY = null; + this._screenX = null; + this._screenY = null; + this._lastFrame = null; + this._autoscrollHandledByApz = false; + this._scrollId = null; + + this.observer = new AutoScrollObserver(this); + this.autoscrollLoop = this.autoscrollLoop.bind(this); + } + + isAutoscrollBlocker(event) { + let mmPaste = Services.prefs.getBoolPref("middlemouse.paste"); + let mmScrollbarPosition = Services.prefs.getBoolPref( + "middlemouse.scrollbarPosition" + ); + let node = event.originalTarget; + let content = node.ownerGlobal; + + // If the node is in editable document or content, we don't want to start + // autoscroll. + if (mmPaste) { + if (node.ownerDocument?.designMode == "on") { + return true; + } + const element = + node.nodeType === content.Node.ELEMENT_NODE ? node : node.parentElement; + if (element.isContentEditable) { + return true; + } + } + + // Don't start if we're on a link. + let [href] = lazy.BrowserUtils.hrefAndLinkNodeForClickEvent(event); + if (href) { + return true; + } + + // Or if we're pasting into an input field of sorts. + let closestInput = mmPaste && node.closest("input,textarea"); + if ( + content.HTMLInputElement.isInstance(closestInput) || + content.HTMLTextAreaElement.isInstance(closestInput) + ) { + return true; + } + + // Or if we're on a scrollbar or XUL + if ( + (mmScrollbarPosition && + content.XULElement.isInstance( + node.closest("scrollbar,scrollcorner") + )) || + content.XULElement.isInstance(node.closest("treechildren")) + ) { + return true; + } + return false; + } + + isScrollableElement(aNode) { + let content = aNode.ownerGlobal; + if (content.HTMLElement.isInstance(aNode)) { + return !content.HTMLSelectElement.isInstance(aNode) || aNode.multiple; + } + + return content.XULElement.isInstance(aNode); + } + + computeWindowScrollDirection(global) { + if (!global.scrollbars.visible) { + return null; + } + if (global.scrollMaxX != global.scrollMinX) { + return global.scrollMaxY != global.scrollMinY ? "NSEW" : "EW"; + } + if (global.scrollMaxY != global.scrollMinY) { + return "NS"; + } + return null; + } + + computeNodeScrollDirection(node) { + if (!this.isScrollableElement(node)) { + return null; + } + + let global = node.ownerGlobal; + + // this is a list of overflow property values that allow scrolling + const scrollingAllowed = ["scroll", "auto"]; + + let cs = global.getComputedStyle(node); + let overflowx = cs.getPropertyValue("overflow-x"); + let overflowy = cs.getPropertyValue("overflow-y"); + // we already discarded non-multiline selects so allow vertical + // scroll for multiline ones directly without checking for a + // overflow property + let scrollVert = + node.scrollTopMax && + (global.HTMLSelectElement.isInstance(node) || + scrollingAllowed.includes(overflowy)); + + // do not allow horizontal scrolling for select elements, it leads + // to visual artifacts and is not the expected behavior anyway + if ( + !global.HTMLSelectElement.isInstance(node) && + node.scrollLeftMin != node.scrollLeftMax && + scrollingAllowed.includes(overflowx) + ) { + return scrollVert ? "NSEW" : "EW"; + } + + if (scrollVert) { + return "NS"; + } + + return null; + } + + findNearestScrollableElement(aNode) { + // go upward in the DOM and find any parent element that has a overflow + // area and can therefore be scrolled + this._scrollable = null; + for (let node = aNode; node; node = node.flattenedTreeParentNode) { + // do not use overflow based autoscroll for and + // Elements or non-html/non-xul elements such as svg or Document nodes + // also make sure to skip select elements that are not multiline + let direction = this.computeNodeScrollDirection(node); + if (direction) { + this._scrolldir = direction; + this._scrollable = node; + break; + } + } + + if (!this._scrollable) { + let direction = this.computeWindowScrollDirection(aNode.ownerGlobal); + if (direction) { + this._scrolldir = direction; + this._scrollable = aNode.ownerGlobal; + } else if (aNode.ownerGlobal.frameElement) { + // Note, in case of out of process iframes frameElement is null, and + // a caller is supposed to communicate to iframe's parent on its own to + // support cross process scrolling. + this.findNearestScrollableElement(aNode.ownerGlobal.frameElement); + } + } + } + + async startScroll(event) { + this.findNearestScrollableElement(event.originalTarget); + if (!this._scrollable) { + this.sendAsyncMessage("Autoscroll:MaybeStartInParent", { + browsingContextId: this.browsingContext.id, + screenX: event.screenX, + screenY: event.screenY, + }); + return; + } + + let content = event.originalTarget.ownerGlobal; + + // In some configurations like Print Preview, content.performance + // (which we use below) is null. Autoscrolling is broken in Print + // Preview anyways (see bug 1393494), so just don't start it at all. + if (!content.performance) { + return; + } + + let domUtils = content.windowUtils; + let scrollable = this._scrollable; + if (scrollable instanceof Ci.nsIDOMWindow) { + // getViewId() needs an element to operate on. + scrollable = scrollable.document.documentElement; + } + this._scrollId = null; + try { + this._scrollId = domUtils.getViewId(scrollable); + } catch (e) { + // No view ID - leave this._scrollId as null. Receiving side will check. + } + let presShellId = domUtils.getPresShellId(); + let { autoscrollEnabled, usingApz } = await this.sendQuery( + "Autoscroll:Start", + { + scrolldir: this._scrolldir, + screenXDevPx: event.screenX * content.devicePixelRatio, + screenYDevPx: event.screenY * content.devicePixelRatio, + scrollId: this._scrollId, + presShellId, + browsingContext: this.browsingContext, + } + ); + if (!autoscrollEnabled) { + this._scrollable = null; + return; + } + + Services.els.addSystemEventListener(this.document, "mousemove", this, true); + Services.els.addSystemEventListener(this.document, "mouseup", this, true); + this.document.addEventListener("pagehide", this, true); + + this._startX = event.screenX; + this._startY = event.screenY; + this._screenX = event.screenX; + this._screenY = event.screenY; + this._scrollErrorX = 0; + this._scrollErrorY = 0; + this._autoscrollHandledByApz = usingApz; + + if (!usingApz) { + // If the browser didn't hand the autoscroll off to APZ, + // scroll here in the main thread. + this.startMainThreadScroll(); + } else { + // Even if the browser did hand the autoscroll to APZ, + // APZ might reject it in which case it will notify us + // and we need to take over. + Services.obs.addObserver(this.observer, "autoscroll-rejected-by-apz"); + } + + if (Cu.isInAutomation) { + Services.obs.notifyObservers(content, "autoscroll-start"); + } + } + + startMainThreadScroll() { + let content = this.document.defaultView; + this._lastFrame = content.performance.now(); + content.requestAnimationFrame(this.autoscrollLoop); + } + + stopScroll() { + if (this._scrollable) { + this._scrollable.mozScrollSnap(); + this._scrollable = null; + + Services.els.removeSystemEventListener( + this.document, + "mousemove", + this, + true + ); + Services.els.removeSystemEventListener( + this.document, + "mouseup", + this, + true + ); + this.document.removeEventListener("pagehide", this, true); + if (this._autoscrollHandledByApz) { + Services.obs.removeObserver( + this.observer, + "autoscroll-rejected-by-apz" + ); + } + } + } + + accelerate(curr, start) { + const speed = 12; + var val = (curr - start) / speed; + + if (val > 1) { + return val * Math.sqrt(val) - 1; + } + if (val < -1) { + return val * Math.sqrt(-val) + 1; + } + return 0; + } + + roundToZero(num) { + if (num > 0) { + return Math.floor(num); + } + return Math.ceil(num); + } + + autoscrollLoop(timestamp) { + if (!this._scrollable) { + // Scrolling has been canceled + return; + } + + // avoid long jumps when the browser hangs for more than + // |maxTimeDelta| ms + const maxTimeDelta = 100; + var timeDelta = Math.min(maxTimeDelta, timestamp - this._lastFrame); + // we used to scroll |accelerate()| pixels every 20ms (50fps) + var timeCompensation = timeDelta / 20; + this._lastFrame = timestamp; + + var actualScrollX = 0; + var actualScrollY = 0; + // don't bother scrolling vertically when the scrolldir is only horizontal + // and the other way around + if (this._scrolldir != "EW") { + var y = this.accelerate(this._screenY, this._startY) * timeCompensation; + var desiredScrollY = this._scrollErrorY + y; + actualScrollY = this.roundToZero(desiredScrollY); + this._scrollErrorY = desiredScrollY - actualScrollY; + } + if (this._scrolldir != "NS") { + var x = this.accelerate(this._screenX, this._startX) * timeCompensation; + var desiredScrollX = this._scrollErrorX + x; + actualScrollX = this.roundToZero(desiredScrollX); + this._scrollErrorX = desiredScrollX - actualScrollX; + } + + this._scrollable.scrollBy({ + left: actualScrollX, + top: actualScrollY, + behavior: "instant", + }); + + this._scrollable.ownerGlobal.requestAnimationFrame(this.autoscrollLoop); + } + + canStartAutoScrollWith(event) { + if ( + !event.isTrusted || + event.defaultPrevented || + event.button !== 1 || + event.clickEventPrevented() + ) { + return false; + } + + for (const modifier of ["shift", "alt", "ctrl", "meta"]) { + if ( + event[modifier + "Key"] && + Services.prefs.getBoolPref( + `general.autoscroll.prevent_to_start.${modifier}Key`, + false + ) + ) { + return false; + } + } + if ( + event.getModifierState("OS") && + Services.prefs.getBoolPref("general.autoscroll.prevent_to_start.osKey") + ) { + return false; + } + return true; + } + + handleEvent(event) { + switch (event.type) { + case "mousemove": + this._screenX = event.screenX; + this._screenY = event.screenY; + break; + case "mousedown": + if ( + this.canStartAutoScrollWith(event) && + !this._scrollable && + !this.isAutoscrollBlocker(event) + ) { + this.startScroll(event); + } + // fallthrough + case "mouseup": + if (this._scrollable) { + // Middle mouse click event shouldn't be fired in web content for + // compatibility with Chrome. + event.preventClickEvent(); + } + break; + case "pagehide": + if (this._scrollable) { + var doc = this._scrollable.ownerDocument || this._scrollable.document; + if (doc == event.target) { + this.sendAsyncMessage("Autoscroll:Cancel"); + this.stopScroll(); + } + } + break; + } + } + + receiveMessage(msg) { + let data = msg.data; + switch (msg.name) { + case "Autoscroll:MaybeStart": + for (let child of this.browsingContext.children) { + if (data.browsingContextId == child.id) { + this.startScroll({ + screenX: data.screenX, + screenY: data.screenY, + originalTarget: child.embedderElement, + }); + break; + } + } + break; + case "Autoscroll:Stop": { + this.stopScroll(); + break; + } + } + } + + rejectedByApz(data) { + // The caller passes in the scroll id via 'data'. + if (data == this._scrollId) { + this._autoscrollHandledByApz = false; + this.startMainThreadScroll(); + Services.obs.removeObserver(this.observer, "autoscroll-rejected-by-apz"); + } + } +} + +class AutoScrollObserver { + constructor(actor) { + this.actor = actor; + } + + observe(subject, topic, data) { + if (topic === "autoscroll-rejected-by-apz") { + this.actor.rejectedByApz(data); + } + } +} diff --git a/toolkit/actors/AutoScrollParent.sys.mjs b/toolkit/actors/AutoScrollParent.sys.mjs new file mode 100644 index 0000000000..1f9f780902 --- /dev/null +++ b/toolkit/actors/AutoScrollParent.sys.mjs @@ -0,0 +1,48 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class AutoScrollParent extends JSWindowActorParent { + receiveMessage(msg) { + let browser = this.manager.browsingContext.top.embedderElement; + if (!browser) { + return null; + } + + // If another tab is activated, we shouldn't start autoscroll requested + // for the previous active window if and only if the browser is a remote + // browser. This is required for web apps which don't prevent default of + // middle click after opening a new window. If the active tab is our + // documents like about:*, we don't need this check since our documents + // should do it correctly. + const requestedInForegroundTab = browser.isRemoteBrowser + ? Services.focus.focusedElement == browser + : true; + + let data = msg.data; + switch (msg.name) { + case "Autoscroll:Start": + // Don't start autoscroll if the tab has already been a background tab. + if (!requestedInForegroundTab) { + return Promise.resolve({ autoscrollEnabled: false, usingAPZ: false }); + } + return Promise.resolve(browser.startScroll(data)); + case "Autoscroll:MaybeStartInParent": + // Don't start autoscroll if the tab has already been a background tab. + if (!requestedInForegroundTab) { + return Promise.resolve({ autoscrollEnabled: false, usingAPZ: false }); + } + let parent = this.browsingContext.parent; + if (parent) { + let actor = parent.currentWindowGlobal.getActor("AutoScroll"); + actor.sendAsyncMessage("Autoscroll:MaybeStart", data); + } + break; + case "Autoscroll:Cancel": + browser.cancelScroll(); + break; + } + return null; + } +} diff --git a/toolkit/actors/AutoplayChild.sys.mjs b/toolkit/actors/AutoplayChild.sys.mjs new file mode 100644 index 0000000000..87fa966cb1 --- /dev/null +++ b/toolkit/actors/AutoplayChild.sys.mjs @@ -0,0 +1,10 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class AutoplayChild extends JSWindowActorChild { + handleEvent(event) { + this.sendAsyncMessage("GloballyAutoplayBlocked", {}); + } +} diff --git a/toolkit/actors/AutoplayParent.sys.mjs b/toolkit/actors/AutoplayParent.sys.mjs new file mode 100644 index 0000000000..3e9f807b3a --- /dev/null +++ b/toolkit/actors/AutoplayParent.sys.mjs @@ -0,0 +1,17 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class AutoplayParent extends JSWindowActorParent { + receiveMessage(aMessage) { + let topBrowsingContext = this.manager.browsingContext.top; + let browser = topBrowsingContext.embedderElement; + let document = browser.ownerDocument; + let event = document.createEvent("CustomEvent"); + event.initCustomEvent("GloballyAutoplayBlocked", true, false, { + url: this.documentURI, + }); + browser.dispatchEvent(event); + } +} diff --git a/toolkit/actors/BackgroundThumbnailsChild.sys.mjs b/toolkit/actors/BackgroundThumbnailsChild.sys.mjs new file mode 100644 index 0000000000..3af51b3b80 --- /dev/null +++ b/toolkit/actors/BackgroundThumbnailsChild.sys.mjs @@ -0,0 +1,102 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PageThumbUtils: "resource://gre/modules/PageThumbUtils.sys.mjs", +}); + +// NOTE: Copied from nsSandboxFlags.h +/** + * This flag prevents content from creating new auxiliary browsing contexts, + * e.g. using the target attribute, or the window.open() method. + */ +const SANDBOXED_AUXILIARY_NAVIGATION = 0x2; + +export class BackgroundThumbnailsChild extends JSWindowActorChild { + receiveMessage(message) { + switch (message.name) { + case "Browser:Thumbnail:ContentInfo": { + if ( + message.data.isImage || + this.contentWindow.ImageDocument.isInstance(this.document) + ) { + // To avoid sending additional messages between processes, we return + // the image data directly with the size info. + return lazy.PageThumbUtils.createImageThumbnailCanvas( + this.contentWindow, + this.document.location, + message.data.targetWidth, + message.data.backgroundColor + ); + } + + let [width, height] = lazy.PageThumbUtils.getContentSize( + this.contentWindow + ); + return { width, height }; + } + + case "Browser:Thumbnail:LoadURL": { + let docShell = this.docShell.QueryInterface(Ci.nsIWebNavigation); + + // We want a low network priority for this service - lower than b/g tabs + // etc - so set it to the lowest priority available. + docShell + .QueryInterface(Ci.nsIDocumentLoader) + .loadGroup.QueryInterface(Ci.nsISupportsPriority).priority = + Ci.nsISupportsPriority.PRIORITY_LOWEST; + + docShell.allowMedia = false; + docShell.allowPlugins = false; + docShell.allowContentRetargeting = false; + let defaultFlags = + Ci.nsIRequest.LOAD_ANONYMOUS | + Ci.nsIRequest.LOAD_BYPASS_CACHE | + Ci.nsIRequest.INHIBIT_CACHING | + Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_HISTORY; + docShell.defaultLoadFlags = defaultFlags; + this.browsingContext.sandboxFlags |= SANDBOXED_AUXILIARY_NAVIGATION; + docShell.useTrackingProtection = true; + + // Get the document to force a content viewer to be created, otherwise + // the first load can fail. + if (!this.document) { + return false; + } + + let loadURIOptions = { + // Bug 1498603 verify usages of systemPrincipal here + triggeringPrincipal: + Services.scriptSecurityManager.getSystemPrincipal(), + loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_STOP_CONTENT, + }; + try { + docShell.loadURI( + Services.io.newURI(message.data.url), + loadURIOptions + ); + } catch (ex) { + return false; + } + + return true; + } + } + + return undefined; + } + + handleEvent(event) { + if (event.type == "DOMDocElementInserted") { + // Arrange to prevent (most) popup dialogs for this window - popups done + // in the parent (eg, auth) aren't prevented, but alert() etc are. + // disableDialogs only works on the current inner window, so it has + // to be called every page load, but before scripts run. + this.contentWindow.windowUtils.disableDialogs(); + } + } +} diff --git a/toolkit/actors/BrowserElementChild.sys.mjs b/toolkit/actors/BrowserElementChild.sys.mjs new file mode 100644 index 0000000000..97e1327821 --- /dev/null +++ b/toolkit/actors/BrowserElementChild.sys.mjs @@ -0,0 +1,35 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class BrowserElementChild extends JSWindowActorChild { + handleEvent(event) { + if ( + event.type == "DOMWindowClose" && + !this.manager.browsingContext.parent + ) { + this.sendAsyncMessage("DOMWindowClose", {}); + } + } + + receiveMessage(message) { + switch (message.name) { + case "EnterModalState": { + this.contentWindow.windowUtils.enterModalState(); + break; + } + + case "LeaveModalState": { + if ( + !message.data.forceLeave && + !this.contentWindow.windowUtils.isInModalState() + ) { + break; + } + this.contentWindow.windowUtils.leaveModalState(); + break; + } + } + } +} diff --git a/toolkit/actors/BrowserElementParent.sys.mjs b/toolkit/actors/BrowserElementParent.sys.mjs new file mode 100644 index 0000000000..c3cb0991a3 --- /dev/null +++ b/toolkit/actors/BrowserElementParent.sys.mjs @@ -0,0 +1,36 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +/** + * The BrowserElementParent is for performing actions on one or more subframes of + * a from the browser element binding. + */ +export class BrowserElementParent extends JSWindowActorParent { + receiveMessage(message) { + switch (message.name) { + case "DOMWindowClose": { + // This message is sent whenever window.close() is called within a window + // that had originally been opened via window.open. Double-check that this is + // coming from a top-level frame, and then dispatch the DOMWindowClose event + // on the browser so that the front-end code can do the right thing with the + // request to close. + if (!this.manager.browsingContext.parent) { + let browser = this.manager.browsingContext.embedderElement; + let win = browser.ownerGlobal; + // If this is a non-remote browser, the DOMWindowClose event will bubble + // up naturally, and doesn't need to be re-dispatched. + if (browser.isRemoteBrowser) { + browser.dispatchEvent( + new win.CustomEvent("DOMWindowClose", { + bubbles: true, + }) + ); + } + } + break; + } + } + } +} diff --git a/toolkit/actors/ClipboardReadPasteChild.sys.mjs b/toolkit/actors/ClipboardReadPasteChild.sys.mjs new file mode 100644 index 0000000000..e8b113bd68 --- /dev/null +++ b/toolkit/actors/ClipboardReadPasteChild.sys.mjs @@ -0,0 +1,39 @@ +/* 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/. */ + +/** + * Propagates "MozClipboardReadPaste" events from a content process to the + * chrome process. + * Receives messages from the chrome process. + */ +export class ClipboardReadPasteChild extends JSWindowActorChild { + constructor() { + super(); + } + + // EventListener interface. + handleEvent(aEvent) { + if (aEvent.type == "MozClipboardReadPaste" && aEvent.isTrusted) { + this.sendAsyncMessage("ClipboardReadPaste:ShowMenupopup", {}); + } + } + + // For JSWindowActorChild. + receiveMessage(value) { + switch (value.name) { + case "ClipboardReadPaste:PasteMenuItemClicked": { + this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup( + true + ); + break; + } + case "ClipboardReadPaste:PasteMenuItemDismissed": { + this.contentWindow.navigator.clipboard.onUserReactedToPasteMenuPopup( + false + ); + break; + } + } + } +} diff --git a/toolkit/actors/ClipboardReadPasteParent.sys.mjs b/toolkit/actors/ClipboardReadPasteParent.sys.mjs new file mode 100644 index 0000000000..a0b37f1041 --- /dev/null +++ b/toolkit/actors/ClipboardReadPasteParent.sys.mjs @@ -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/. */ + +const kMenuPopupId = "clipboardReadPasteMenuPopup"; + +// Exchanges messages with the child actor and handles events from the +// pasteMenuHandler. +export class ClipboardReadPasteParent extends JSWindowActorParent { + constructor() { + super(); + + this._menupopup = null; + this._menuitem = null; + this._delayTimer = null; + this._pasteMenuItemClicked = false; + this._lastBeepTime = 0; + } + + didDestroy() { + if (this._menupopup) { + this._menupopup.hidePopup(true); + } + } + + // EventListener interface. + handleEvent(aEvent) { + switch (aEvent.type) { + case "command": { + this.onCommand(); + break; + } + case "popuphiding": { + this.onPopupHiding(); + break; + } + case "keydown": { + this.onKeyDown(aEvent); + break; + } + } + } + + onCommand() { + this._pasteMenuItemClicked = true; + this.sendAsyncMessage("ClipboardReadPaste:PasteMenuItemClicked"); + } + + onPopupHiding() { + // Remove the listeners before potentially sending the async message + // below, because that might throw. + this._removeMenupopupEventListeners(); + this._clearDelayTimer(); + this._stopWatchingForSpammyActivation(); + + if (this._pasteMenuItemClicked) { + // A message was already sent. Reset the state to handle further + // click/dismiss events properly. + this._pasteMenuItemClicked = false; + } else { + this.sendAsyncMessage("ClipboardReadPaste:PasteMenuItemDismissed"); + } + } + + onKeyDown(aEvent) { + if (!this._menuitem.disabled) { + return; + } + + let accesskey = this._menuitem.getAttribute("accesskey"); + if ( + aEvent.key == accesskey.toLowerCase() || + aEvent.key == accesskey.toUpperCase() + ) { + if (Date.now() - this._lastBeepTime > 1000) { + Cc["@mozilla.org/sound;1"].getService(Ci.nsISound).beep(); + this._lastBeepTime = Date.now(); + } + this._refreshDelayTimer(); + } + } + + // For JSWindowActorParent. + receiveMessage(value) { + if (value.name == "ClipboardReadPaste:ShowMenupopup") { + if (!this._menupopup) { + this._menupopup = this._getMenupopup(); + this._menuitem = this._menupopup.firstElementChild; + } + + this._addMenupopupEventListeners(); + + const browser = this.browsingContext.top.embedderElement; + const window = browser.ownerGlobal; + const windowUtils = window.windowUtils; + + let mouseXInCSSPixels = {}; + let mouseYInCSSPixels = {}; + windowUtils.getLastOverWindowPointerLocationInCSSPixels( + mouseXInCSSPixels, + mouseYInCSSPixels + ); + + this._menuitem.disabled = true; + this._startWatchingForSpammyActivation(); + // `openPopup` is a no-op if the popup is already opened. + // That property is used when `navigator.clipboard.readText()` or + // `navigator.clipboard.read()`is called from two different frames, e.g. + // an iframe and the top level frame. In that scenario, the two frames + // correspond to different `navigator.clipboard` instances. When + // `readText()` or `read()` is called from both frames, an actor pair is + // instantiated for each of them. Both actor parents will call `openPopup` + // on the same `_menupopup` object. If the popup is already open, + // `openPopup` is a no-op. When the popup is clicked or dismissed both + // actor parents will receive the corresponding event. + this._menupopup.openPopup( + null, + "overlap" /* options */, + mouseXInCSSPixels.value, + mouseYInCSSPixels.value, + true /* isContextMenu */ + ); + + this._refreshDelayTimer(); + } + } + + _addMenupopupEventListeners() { + this._menupopup.addEventListener("command", this); + this._menupopup.addEventListener("popuphiding", this); + } + + _removeMenupopupEventListeners() { + this._menupopup.removeEventListener("command", this); + this._menupopup.removeEventListener("popuphiding", this); + } + + _createMenupopup(aChromeDoc) { + let menuitem = aChromeDoc.createXULElement("menuitem"); + menuitem.id = "clipboardReadPasteMenuItem"; + menuitem.setAttribute("data-l10n-id", "text-action-paste"); + + let menupopup = aChromeDoc.createXULElement("menupopup"); + menupopup.id = kMenuPopupId; + menupopup.appendChild(menuitem); + return menupopup; + } + + _getMenupopup() { + let browser = this.browsingContext.top.embedderElement; + let window = browser.ownerGlobal; + let chromeDoc = window.document; + + let menupopup = chromeDoc.getElementById(kMenuPopupId); + if (menupopup == null) { + menupopup = this._createMenupopup(chromeDoc); + const parent = + chromeDoc.querySelector("popupset") || chromeDoc.documentElement; + parent.appendChild(menupopup); + } + + return menupopup; + } + + _startWatchingForSpammyActivation() { + let doc = this._menuitem.ownerDocument; + Services.els.addSystemEventListener(doc, "keydown", this, true); + } + + _stopWatchingForSpammyActivation() { + let doc = this._menuitem.ownerDocument; + Services.els.removeSystemEventListener(doc, "keydown", this, true); + } + + _clearDelayTimer() { + if (this._delayTimer) { + let window = this._menuitem.ownerGlobal; + window.clearTimeout(this._delayTimer); + this._delayTimer = null; + } + } + + _refreshDelayTimer() { + this._clearDelayTimer(); + + let window = this._menuitem.ownerGlobal; + let delay = Services.prefs.getIntPref("security.dialog_enable_delay"); + this._delayTimer = window.setTimeout(() => { + this._menuitem.disabled = false; + this._stopWatchingForSpammyActivation(); + this._delayTimer = null; + }, delay); + } +} diff --git a/toolkit/actors/ContentMetaChild.sys.mjs b/toolkit/actors/ContentMetaChild.sys.mjs new file mode 100644 index 0000000000..929d92db47 --- /dev/null +++ b/toolkit/actors/ContentMetaChild.sys.mjs @@ -0,0 +1,199 @@ +/* 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/. */ + +// Debounce time in milliseconds - this should be long enough to account for +// sync script tags that could appear between desired meta tags +const TIMEOUT_DELAY = 1000; + +const ACCEPTED_PROTOCOLS = ["http:", "https:"]; + +// Possible description tags, listed in order from least favourable to most favourable +const DESCRIPTION_RULES = [ + "twitter:description", + "description", + "og:description", +]; + +// Possible image tags, listed in order from least favourable to most favourable +const PREVIEW_IMAGE_RULES = [ + "thumbnail", + "twitter:image", + "og:image", + "og:image:url", + "og:image:secure_url", +]; + +/* + * Checks if the incoming meta tag has a greater score than the current best + * score by checking the index of the meta tag in the list of rules provided. + * + * @param {Array} aRules + * The list of rules for a given type of meta tag + * @param {String} aTag + * The name or property of the incoming meta tag + * @param {String} aEntry + * The current best entry for the given meta tag + * + * @returns {Boolean} true if the incoming meta tag is better than the current + * best meta tag of that same kind, false otherwise + */ +function shouldExtractMetadata(aRules, aTag, aEntry) { + return aRules.indexOf(aTag) > aEntry.currMaxScore; +} + +/* + * Ensure that the preview image URL is safe and valid before storing + * + * @param {URL} aURL + * A URL object that needs to be checked for valid principal and protocol + * + * @returns {Boolean} true if the preview URL is safe and can be stored, false otherwise + */ +function checkLoadURIStr(aURL) { + if (!ACCEPTED_PROTOCOLS.includes(aURL.protocol)) { + return false; + } + try { + let ssm = Services.scriptSecurityManager; + let principal = ssm.createNullPrincipal({}); + ssm.checkLoadURIStrWithPrincipal( + principal, + aURL.href, + ssm.DISALLOW_INHERIT_PRINCIPAL + ); + } catch (e) { + return false; + } + return true; +} + +/* + * This listens to DOMMetaAdded events and collects relevant metadata about the + * meta tag received. Then, it sends the metadata gathered from the meta tags + * and the url of the page as it's payload to be inserted into moz_places. + */ +export class ContentMetaChild extends JSWindowActorChild { + constructor() { + super(); + + // Store a mapping of the best description and preview + // image collected so far for a given URL. + this.metaTags = new Map(); + } + + didDestroy() { + for (let entry of this.metaTags.values()) { + entry.timeout.cancel(); + } + } + + handleEvent(event) { + switch (event.type) { + case "DOMContentLoaded": + const metaTags = this.contentWindow.document.querySelectorAll("meta"); + for (let metaTag of metaTags) { + this.onMetaTag(metaTag); + } + break; + case "DOMMetaAdded": + this.onMetaTag(event.originalTarget); + break; + default: + } + } + + onMetaTag(metaTag) { + const window = metaTag.ownerGlobal; + + // If there's no meta tag, ignore this. Also verify that the window + // matches just to be safe. + if (!metaTag || !metaTag.ownerDocument || window != this.contentWindow) { + return; + } + + const url = metaTag.ownerDocument.documentURI; + + let name = metaTag.name; + let prop = metaTag.getAttributeNS(null, "property"); + if (!name && !prop) { + return; + } + + let tag = name || prop; + + const entry = this.metaTags.get(url) || { + description: { value: null, currMaxScore: -1 }, + image: { value: null, currMaxScore: -1 }, + timeout: null, + }; + + // Malformed meta tag - do not store it + const content = metaTag.getAttributeNS(null, "content"); + if (!content) { + return; + } + + if (shouldExtractMetadata(DESCRIPTION_RULES, tag, entry.description)) { + // Extract the description + entry.description.value = content; + entry.description.currMaxScore = DESCRIPTION_RULES.indexOf(tag); + } else if (shouldExtractMetadata(PREVIEW_IMAGE_RULES, tag, entry.image)) { + // Extract the preview image + let value; + try { + value = new URL(content, url); + } catch (e) { + return; + } + if (value && checkLoadURIStr(value)) { + entry.image.value = value.href; + entry.image.currMaxScore = PREVIEW_IMAGE_RULES.indexOf(tag); + } + } else { + // We don't care about other meta tags + return; + } + + if (!this.metaTags.has(url)) { + this.metaTags.set(url, entry); + } + + if (entry.timeout) { + entry.timeout.delay = TIMEOUT_DELAY; + } else { + // We want to debounce incoming meta tags until we're certain we have the + // best one for description and preview image, and only store that one + entry.timeout = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + entry.timeout.initWithCallback( + () => { + entry.timeout = null; + this.metaTags.delete(url); + // We try to cancel the timers when we get destroyed, but if + // there's a race, catch it: + if (!this.manager || this.manager.isClosed) { + return; + } + + // Save description and preview image to moz_places + this.sendAsyncMessage("Meta:SetPageInfo", { + url, + description: entry.description.value, + previewImageURL: entry.image.value, + }); + + // Telemetry for recording the size of page metadata + let metadataSize = entry.description.value + ? entry.description.value.length + : 0; + metadataSize += entry.image.value ? entry.image.value.length : 0; + Services.telemetry + .getHistogramById("PAGE_METADATA_SIZE") + .add(metadataSize); + }, + TIMEOUT_DELAY, + Ci.nsITimer.TYPE_ONE_SHOT + ); + } + } +} diff --git a/toolkit/actors/ContentMetaParent.sys.mjs b/toolkit/actors/ContentMetaParent.sys.mjs new file mode 100644 index 0000000000..4cdc6c3c85 --- /dev/null +++ b/toolkit/actors/ContentMetaParent.sys.mjs @@ -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/. */ + +export class ContentMetaParent extends JSWindowActorParent { + receiveMessage(message) { + if (message.name == "Meta:SetPageInfo") { + let browser = this.manager.browsingContext.top.embedderElement; + if (browser) { + let event = new browser.ownerGlobal.CustomEvent("pageinfo", { + bubbles: true, + cancelable: false, + detail: { + url: message.data.url, + description: message.data.description, + previewImageURL: message.data.previewImageURL, + }, + }); + browser.dispatchEvent(event); + } + } + } +} diff --git a/toolkit/actors/ControllersChild.sys.mjs b/toolkit/actors/ControllersChild.sys.mjs new file mode 100644 index 0000000000..d975c1f431 --- /dev/null +++ b/toolkit/actors/ControllersChild.sys.mjs @@ -0,0 +1,63 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class ControllersChild extends JSWindowActorChild { + receiveMessage(message) { + switch (message.name) { + case "ControllerCommands:Do": + if (this.docShell && this.docShell.isCommandEnabled(message.data)) { + this.docShell.doCommand(message.data); + } + break; + + case "ControllerCommands:DoWithParams": + var data = message.data; + if (this.docShell && this.docShell.isCommandEnabled(data.cmd)) { + var params = Cu.createCommandParams(); + let substituteXY = false; + let x = 0; + let y = 0; + if ( + data.cmd == "cmd_lookUpDictionary" && + "x" in data.params && + "y" in data.params && + data.params.x.type == "long" && + data.params.y.type == "long" + ) { + substituteXY = true; + x = parseInt(data.params.x.value); + y = parseInt(data.params.y.value); + + let rect = + this.contentWindow.windowUtils.convertFromParentProcessWidgetToLocal( + x, + y, + 1, + 1 + ); + x = Math.round(rect.x); + y = Math.round(rect.y); + } + + for (var name in data.params) { + var value = data.params[name]; + if (value.type == "long") { + if (substituteXY && name === "x") { + params.setLongValue(name, x); + } else if (substituteXY && name === "y") { + params.setLongValue(name, y); + } else { + params.setLongValue(name, parseInt(value.value)); + } + } else { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + } + this.docShell.doCommandWithParams(data.cmd, params); + } + break; + } + } +} diff --git a/toolkit/actors/ControllersParent.sys.mjs b/toolkit/actors/ControllersParent.sys.mjs new file mode 100644 index 0000000000..05ea166112 --- /dev/null +++ b/toolkit/actors/ControllersParent.sys.mjs @@ -0,0 +1,90 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class ControllersParent extends JSWindowActorParent { + constructor() { + super(); + + // A map of commands that have had their enabled/disabled state assigned. The + // value of each key will be true if enabled, and false if disabled. + this.supportedCommands = {}; + } + + get browser() { + return this.browsingContext.top.embedderElement; + } + + // Update the set of enabled and disabled commands. + enableDisableCommands(aAction, aEnabledCommands, aDisabledCommands) { + // Clear the list first + this.supportedCommands = {}; + + for (let command of aEnabledCommands) { + this.supportedCommands[command] = true; + } + + for (let command of aDisabledCommands) { + this.supportedCommands[command] = false; + } + + let browser = this.browser; + if (browser) { + browser.ownerGlobal.updateCommands(aAction); + } + } + + isCommandEnabled(aCommand) { + return this.supportedCommands[aCommand] || false; + } + + supportsCommand(aCommand) { + return aCommand in this.supportedCommands; + } + + doCommand(aCommand) { + this.sendAsyncMessage("ControllerCommands:Do", aCommand); + } + + getCommandStateWithParams(aCommand, aCommandParams) { + throw Components.Exception("Not implemented", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + doCommandWithParams(aCommand, aCommandParams) { + let cmd = { + cmd: aCommand, + params: null, + }; + if (aCommand == "cmd_lookUpDictionary") { + cmd.params = { + x: { + type: "long", + value: aCommandParams.getLongValue("x"), + }, + y: { + type: "long", + value: aCommandParams.getLongValue("y"), + }, + }; + } else { + throw Components.Exception( + "Not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + this.sendAsyncMessage("ControllerCommands:DoWithParams", cmd); + } + + getSupportedCommands() { + throw Components.Exception("Not implemented", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + onEvent() {} +} + +ControllersParent.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIBrowserController", + "nsIController", + "nsICommandController", +]); diff --git a/toolkit/actors/DateTimePickerChild.sys.mjs b/toolkit/actors/DateTimePickerChild.sys.mjs new file mode 100644 index 0000000000..9ef55af435 --- /dev/null +++ b/toolkit/actors/DateTimePickerChild.sys.mjs @@ -0,0 +1,204 @@ +/* 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/. */ + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + LayoutUtils: "resource://gre/modules/LayoutUtils.sys.mjs", +}); + +/** + * DateTimePickerChild is the communication channel between the input box + * (content) for date/time input types and its picker (chrome). + */ +export class DateTimePickerChild extends JSWindowActorChild { + /** + * On init, just listen for the event to open the picker, once the picker is + * opened, we'll listen for update and close events. + */ + constructor() { + super(); + + this._inputElement = null; + } + + /** + * Cleanup function called when picker is closed. + */ + close() { + this.removeListeners(this._inputElement); + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + this._inputElement = null; + return; + } + + // dateTimeBoxElement is within UA Widget Shadow DOM. + // An event dispatch to it can't be accessed by document. + let win = this._inputElement.ownerGlobal; + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozSetDateTimePickerState", { detail: false }) + ); + + this._inputElement = null; + } + + /** + * Called after picker is opened to start listening for input box update + * events. + */ + addListeners(aElement) { + aElement.ownerGlobal.addEventListener("pagehide", this); + } + + /** + * Stop listeneing for events when picker is closed. + */ + removeListeners(aElement) { + aElement.ownerGlobal.removeEventListener("pagehide", this); + } + + /** + * Helper function that returns the CSS direction property of the element. + */ + getComputedDirection(aElement) { + return aElement.ownerGlobal + .getComputedStyle(aElement) + .getPropertyValue("direction"); + } + + /** + * Helper function that returns the rect of the element, which is the position + * relative to the left/top of the content area. + */ + getBoundingContentRect(aElement) { + return lazy.LayoutUtils.getElementBoundingScreenRect(aElement); + } + + getTimePickerPref() { + return Services.prefs.getBoolPref("dom.forms.datetime.timepicker"); + } + + /** + * nsIMessageListener. + */ + receiveMessage(aMessage) { + switch (aMessage.name) { + case "FormDateTime:PickerClosed": { + if (!this._inputElement) { + return; + } + + this.close(); + break; + } + case "FormDateTime:PickerValueChanged": { + if (!this._inputElement) { + return; + } + + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + return; + } + + let win = this._inputElement.ownerGlobal; + + // dateTimeBoxElement is within UA Widget Shadow DOM. + // An event dispatch to it can't be accessed by document. + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozPickerValueChanged", { + detail: Cu.cloneInto(aMessage.data, win), + }) + ); + break; + } + default: + break; + } + } + + /** + * nsIDOMEventListener, for chrome events sent by the input element and other + * DOM events. + */ + handleEvent(aEvent) { + switch (aEvent.type) { + case "MozOpenDateTimePicker": { + // Time picker is disabled when preffed off + if ( + !aEvent.originalTarget.ownerGlobal.HTMLInputElement.isInstance( + aEvent.originalTarget + ) || + (aEvent.originalTarget.type == "time" && !this.getTimePickerPref()) + ) { + return; + } + + if (this._inputElement) { + // This happens when we're trying to open a picker when another picker + // is still open. We ignore this request to let the first picker + // close gracefully. + return; + } + + this._inputElement = aEvent.originalTarget; + + let dateTimeBoxElement = this._inputElement.dateTimeBoxElement; + if (!dateTimeBoxElement) { + throw new Error("How do we get this event without a UA Widget?"); + } + + // dateTimeBoxElement is within UA Widget Shadow DOM. + // An event dispatch to it can't be accessed by document, because + // the event is not composed. + let win = this._inputElement.ownerGlobal; + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozSetDateTimePickerState", { detail: true }) + ); + + this.addListeners(this._inputElement); + + let value = this._inputElement.getDateTimeInputBoxValue(); + this.sendAsyncMessage("FormDateTime:OpenPicker", { + rect: this.getBoundingContentRect(this._inputElement), + dir: this.getComputedDirection(this._inputElement), + type: this._inputElement.type, + detail: { + // Pass partial value if it's available, otherwise pass input + // element's value. + value: Object.keys(value).length ? value : this._inputElement.value, + min: this._inputElement.getMinimum(), + max: this._inputElement.getMaximum(), + step: this._inputElement.getStep(), + stepBase: this._inputElement.getStepBase(), + }, + }); + break; + } + case "MozUpdateDateTimePicker": { + let value = this._inputElement.getDateTimeInputBoxValue(); + value.type = this._inputElement.type; + this.sendAsyncMessage("FormDateTime:UpdatePicker", { value }); + break; + } + case "MozCloseDateTimePicker": { + this.sendAsyncMessage("FormDateTime:ClosePicker", {}); + this.close(); + break; + } + case "pagehide": { + if ( + this._inputElement && + this._inputElement.ownerDocument == aEvent.target + ) { + this.sendAsyncMessage("FormDateTime:ClosePicker", {}); + this.close(); + } + break; + } + default: + break; + } + } +} diff --git a/toolkit/actors/DateTimePickerParent.sys.mjs b/toolkit/actors/DateTimePickerParent.sys.mjs new file mode 100644 index 0000000000..ba78a39ffb --- /dev/null +++ b/toolkit/actors/DateTimePickerParent.sys.mjs @@ -0,0 +1,157 @@ +/* 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/. */ + +const DEBUG = false; +function debug(aStr) { + if (DEBUG) { + dump("-*- DateTimePickerParent: " + aStr + "\n"); + } +} + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + DateTimePickerPanel: "resource://gre/modules/DateTimePickerPanel.sys.mjs", +}); + +/* + * DateTimePickerParent receives message from content side (input box) and + * is reposible for opening, closing and updating the picker. Similarly, + * DateTimePickerParent listens for picker's events and notifies the content + * side (input box) about them. + */ +export class DateTimePickerParent extends JSWindowActorParent { + receiveMessage(aMessage) { + debug("receiveMessage: " + aMessage.name); + switch (aMessage.name) { + case "FormDateTime:OpenPicker": { + this.showPicker(aMessage.data); + break; + } + case "FormDateTime:ClosePicker": { + if (!this._picker) { + return; + } + this.close(); + break; + } + case "FormDateTime:UpdatePicker": { + if (!this._picker) { + return; + } + this._picker.setPopupValue(aMessage.data); + break; + } + default: + break; + } + } + + handleEvent(aEvent) { + debug("handleEvent: " + aEvent.type); + switch (aEvent.type) { + case "DateTimePickerValueCleared": { + this.sendAsyncMessage("FormDateTime:PickerValueChanged", null); + break; + } + case "DateTimePickerValueChanged": { + this.sendAsyncMessage("FormDateTime:PickerValueChanged", aEvent.detail); + break; + } + case "popuphidden": { + this.sendAsyncMessage("FormDateTime:PickerClosed", {}); + this.close(); + break; + } + default: + break; + } + } + + // Get picker from browser and show it anchored to the input box. + showPicker(aData) { + let rect = aData.rect; + let type = aData.type; + let detail = aData.detail; + + debug("Opening picker with details: " + JSON.stringify(detail)); + let topBC = this.browsingContext.top; + let window = topBC.topChromeWindow; + if (Services.focus.activeWindow != window) { + debug("Not in the active window"); + return; + } + + { + let browser = topBC.embedderElement; + if ( + browser && + browser.ownerGlobal.gBrowser && + browser.ownerGlobal.gBrowser.selectedBrowser != browser + ) { + debug("In background tab"); + return; + } + } + + let doc = window.document; + let panel = doc.getElementById("DateTimePickerPanel"); + if (!panel) { + panel = doc.createXULElement("panel"); + panel.id = "DateTimePickerPanel"; + panel.setAttribute("type", "arrow"); + panel.setAttribute("orient", "vertical"); + panel.setAttribute("ignorekeys", "true"); + panel.setAttribute("noautofocus", "true"); + // This ensures that clicks on the anchored input box are never consumed. + panel.setAttribute("consumeoutsideclicks", "never"); + panel.setAttribute("level", "parent"); + panel.setAttribute("tabspecific", "true"); + let container = + doc.getElementById("mainPopupSet") || + doc.querySelector("popupset") || + doc.documentElement.appendChild(doc.createXULElement("popupset")); + container.appendChild(panel); + } + this._oldFocus = doc.activeElement; + this._picker = new lazy.DateTimePickerPanel(panel); + this._picker.openPicker(type, rect, detail); + this.addPickerListeners(); + } + + // Close the picker and do some cleanup. + close() { + this._picker.closePicker(); + // Restore focus to where it was before the picker opened. + this._oldFocus?.focus(); + this._oldFocus = null; + this.removePickerListeners(); + this._picker = null; + } + + // Listen to picker's event. + addPickerListeners() { + if (!this._picker) { + return; + } + this._picker.element.addEventListener("popuphidden", this); + this._picker.element.addEventListener("DateTimePickerValueChanged", this); + this._picker.element.addEventListener("DateTimePickerValueCleared", this); + } + + // Stop listening to picker's event. + removePickerListeners() { + if (!this._picker) { + return; + } + this._picker.element.removeEventListener("popuphidden", this); + this._picker.element.removeEventListener( + "DateTimePickerValueChanged", + this + ); + this._picker.element.removeEventListener( + "DateTimePickerValueCleared", + this + ); + } +} diff --git a/toolkit/actors/ExtFindChild.sys.mjs b/toolkit/actors/ExtFindChild.sys.mjs new file mode 100644 index 0000000000..dc6d296d88 --- /dev/null +++ b/toolkit/actors/ExtFindChild.sys.mjs @@ -0,0 +1,31 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FindContent: "resource://gre/modules/FindContent.sys.mjs", +}); + +export class ExtFindChild extends JSWindowActorChild { + receiveMessage(message) { + if (!this._findContent) { + this._findContent = new lazy.FindContent(this.docShell); + } + + switch (message.name) { + case "ext-Finder:CollectResults": + this.finderInited = true; + return this._findContent.findRanges(message.data); + case "ext-Finder:HighlightResults": + return this._findContent.highlightResults(message.data); + case "ext-Finder:ClearHighlighting": + this._findContent.highlighter.highlight(false); + break; + } + + return null; + } +} diff --git a/toolkit/actors/FindBarChild.sys.mjs b/toolkit/actors/FindBarChild.sys.mjs new file mode 100644 index 0000000000..a82615a253 --- /dev/null +++ b/toolkit/actors/FindBarChild.sys.mjs @@ -0,0 +1,157 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", +}); + +export class FindBarChild extends JSWindowActorChild { + constructor() { + super(); + + this._findKey = null; + + XPCOMUtils.defineLazyProxy( + this, + "FindBarContent", + () => { + const { FindBarContent } = ChromeUtils.importESModule( + "resource://gre/modules/FindBarContent.sys.mjs" + ); + return new FindBarContent(this); + }, + { inQuickFind: false, inPassThrough: false } + ); + } + + receiveMessage(msg) { + if (msg.name == "Findbar:UpdateState") { + let { FindBarContent } = this; + FindBarContent.updateState(msg.data); + } + } + + /** + * Check whether this key event will start the findbar in the parent, + * in which case we should pass any further key events to the parent to avoid + * them being lost. + * @param aEvent the key event to check. + */ + eventMatchesFindShortcut(aEvent) { + if (!this._findKey) { + this._findKey = Services.cpmm.sharedData.get("Findbar:Shortcut"); + if (!this._findKey) { + return false; + } + } + for (let k in this._findKey) { + if (this._findKey[k] != aEvent[k]) { + return false; + } + } + return true; + } + + handleEvent(event) { + if (event.type == "keypress") { + this.onKeypress(event); + } + } + + onKeypress(event) { + let { FindBarContent } = this; + + if (!FindBarContent.inPassThrough && this.eventMatchesFindShortcut(event)) { + return FindBarContent.start(event); + } + + // disable FAYT in about:blank to prevent FAYT opening unexpectedly. + let location = this.document.location.href; + if (location == "about:blank") { + return null; + } + + if ( + event.ctrlKey || + event.altKey || + event.metaKey || + event.defaultPrevented || + !lazy.BrowserUtils.mimeTypeIsTextBased(this.document.contentType) || + !lazy.BrowserUtils.canFindInPage(location) + ) { + return null; + } + + if (FindBarContent.inPassThrough || FindBarContent.inQuickFind) { + return FindBarContent.onKeypress(event); + } + + if (event.charCode && this.shouldFastFind(event.target)) { + let key = String.fromCharCode(event.charCode); + if ((key == "/" || key == "'") && FindBarChild.manualFAYT) { + return FindBarContent.startQuickFind(event); + } + if (key != " " && FindBarChild.findAsYouType) { + return FindBarContent.startQuickFind(event, true); + } + } + return null; + } + + /** + * Return true if we should FAYT for this node: + * + * @param elt + * The element that is focused + */ + shouldFastFind(elt) { + if (elt) { + let win = elt.ownerGlobal; + if (win.HTMLInputElement.isInstance(elt) && elt.mozIsTextField(false)) { + return false; + } + + if (elt.isContentEditable || win.document.designMode == "on") { + return false; + } + + if ( + win.HTMLTextAreaElement.isInstance(elt) || + win.HTMLSelectElement.isInstance(elt) || + win.HTMLObjectElement.isInstance(elt) || + win.HTMLEmbedElement.isInstance(elt) + ) { + return false; + } + + if ( + (win.HTMLIFrameElement.isInstance(elt) && elt.mozbrowser) || + win.XULFrameElement.isInstance(elt) + ) { + // If we're targeting a mozbrowser iframe or an embedded XULFrameElement + // (e.g. about:addons extensions inline options page), do not activate + // fast find. + return false; + } + } + + return true; + } +} + +XPCOMUtils.defineLazyPreferenceGetter( + FindBarChild, + "findAsYouType", + "accessibility.typeaheadfind" +); +XPCOMUtils.defineLazyPreferenceGetter( + FindBarChild, + "manualFAYT", + "accessibility.typeaheadfind.manual" +); diff --git a/toolkit/actors/FindBarParent.sys.mjs b/toolkit/actors/FindBarParent.sys.mjs new file mode 100644 index 0000000000..eaf42b634e --- /dev/null +++ b/toolkit/actors/FindBarParent.sys.mjs @@ -0,0 +1,47 @@ +/* vim: set ts=2 sw=2 sts=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/. */ + +// Map of browser elements to findbars. +let findbars = new WeakMap(); + +export class FindBarParent extends JSWindowActorParent { + setFindbar(browser, findbar) { + if (findbar) { + findbars.set(browser, findbar); + } else { + findbars.delete(browser, findbar); + } + } + + receiveMessage(message) { + let browser = this.manager.browsingContext.top.embedderElement; + if (!browser) { + return; + } + + let respondToMessage = () => { + let findBar = findbars.get(browser); + if (!findBar) { + return; + } + + switch (message.name) { + case "Findbar:Keypress": + findBar._onBrowserKeypress(message.data); + break; + case "Findbar:Mouseup": + findBar.onMouseUp(); + break; + } + }; + + let findPromise = browser.ownerGlobal.gFindBarPromise; + if (findPromise) { + findPromise.then(respondToMessage); + } else { + respondToMessage(); + } + } +} diff --git a/toolkit/actors/FinderChild.sys.mjs b/toolkit/actors/FinderChild.sys.mjs new file mode 100644 index 0000000000..6a245cd606 --- /dev/null +++ b/toolkit/actors/FinderChild.sys.mjs @@ -0,0 +1,129 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +// vim: set ts=2 sw=2 sts=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/. + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Finder: "resource://gre/modules/Finder.sys.mjs", +}); + +export class FinderChild extends JSWindowActorChild { + get finder() { + if (!this._finder) { + this._finder = new lazy.Finder(this.docShell); + } + return this._finder; + } + + receiveMessage(aMessage) { + let data = aMessage.data; + + switch (aMessage.name) { + case "Finder:CaseSensitive": + this.finder.caseSensitive = data.caseSensitive; + break; + + case "Finder:MatchDiacritics": + this.finder.matchDiacritics = data.matchDiacritics; + break; + + case "Finder:EntireWord": + this.finder.entireWord = data.entireWord; + break; + + case "Finder:SetSearchStringToSelection": { + return new Promise(resolve => { + resolve(this.finder.setSearchStringToSelection()); + }); + } + + case "Finder:GetInitialSelection": { + return new Promise(resolve => { + resolve(this.finder.getActiveSelectionText()); + }); + } + + case "Finder:Find": + return this.finder.find(data); + + case "Finder:Highlight": + return this.finder + .highlight( + data.highlight, + data.searchString, + data.linksOnly, + data.useSubFrames + ) + .then(result => { + if (result) { + result.browsingContextId = this.browsingContext.id; + } + return result; + }); + + case "Finder:UpdateHighlightAndMatchCount": + return this.finder.updateHighlightAndMatchCount(data).then(result => { + if (result) { + result.browsingContextId = this.browsingContext.id; + } + return result; + }); + + case "Finder:HighlightAllChange": + this.finder.onHighlightAllChange(data.highlightAll); + break; + + case "Finder:EnableSelection": + this.finder.enableSelection(); + break; + + case "Finder:RemoveSelection": + this.finder.removeSelection(data.keepHighlight); + break; + + case "Finder:FocusContent": + this.finder.focusContent(); + break; + + case "Finder:FindbarClose": + this.finder.onFindbarClose(); + break; + + case "Finder:FindbarOpen": + this.finder.onFindbarOpen(); + break; + + case "Finder:KeyPress": + var KeyboardEvent = this.finder._getWindow().KeyboardEvent; + this.finder.keyPress(new KeyboardEvent("keypress", data)); + break; + + case "Finder:MatchesCount": + return this.finder + .requestMatchesCount( + data.searchString, + data.linksOnly, + data.useSubFrames + ) + .then(result => { + if (result) { + result.browsingContextId = this.browsingContext.id; + } + return result; + }); + + case "Finder:ModalHighlightChange": + this.finder.onModalHighlightChange(data.useModalHighlight); + break; + + case "Finder:EnableMarkTesting": + this.finder.highlighter.enableTesting(data.enable); + break; + } + + return null; + } +} diff --git a/toolkit/actors/InlineSpellCheckerChild.sys.mjs b/toolkit/actors/InlineSpellCheckerChild.sys.mjs new file mode 100644 index 0000000000..c8262c749a --- /dev/null +++ b/toolkit/actors/InlineSpellCheckerChild.sys.mjs @@ -0,0 +1,38 @@ +/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=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/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + InlineSpellCheckerContent: + "resource://gre/modules/InlineSpellCheckerContent.sys.mjs", +}); + +export class InlineSpellCheckerChild extends JSWindowActorChild { + receiveMessage(msg) { + switch (msg.name) { + case "InlineSpellChecker:selectDictionaries": + lazy.InlineSpellCheckerContent.selectDictionaries(msg.data.localeCodes); + break; + + case "InlineSpellChecker:replaceMisspelling": + lazy.InlineSpellCheckerContent.replaceMisspelling(msg.data.suggestion); + break; + + case "InlineSpellChecker:toggleEnabled": + lazy.InlineSpellCheckerContent.toggleEnabled(); + break; + + case "InlineSpellChecker:recheck": + lazy.InlineSpellCheckerContent.recheck(); + break; + + case "InlineSpellChecker:uninit": + lazy.InlineSpellCheckerContent.uninitContextMenu(); + break; + } + } +} diff --git a/toolkit/actors/InlineSpellCheckerParent.sys.mjs b/toolkit/actors/InlineSpellCheckerParent.sys.mjs new file mode 100644 index 0000000000..ff5f55724e --- /dev/null +++ b/toolkit/actors/InlineSpellCheckerParent.sys.mjs @@ -0,0 +1,50 @@ +/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=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/. */ + +export class InlineSpellCheckerParent extends JSWindowActorParent { + selectDictionaries({ localeCodes }) { + this.sendAsyncMessage("InlineSpellChecker:selectDictionaries", { + localeCodes, + }); + } + + replaceMisspelling({ suggestion }) { + this.sendAsyncMessage("InlineSpellChecker:replaceMisspelling", { + suggestion, + }); + } + + toggleEnabled() { + this.sendAsyncMessage("InlineSpellChecker:toggleEnabled", {}); + } + + recheckSpelling() { + this.sendAsyncMessage("InlineSpellChecker:recheck", {}); + } + + uninit() { + // This method gets called by InlineSpellChecker when the context menu + // goes away and the InlineSpellChecker instance is still alive. + // Stop referencing it and tidy the child end of us. + this.sendAsyncMessage("InlineSpellChecker:uninit", {}); + } + + _destructionObservers = new Set(); + registerDestructionObserver(obj) { + this._destructionObservers.add(obj); + } + + unregisterDestructionObserver(obj) { + this._destructionObservers.delete(obj); + } + + didDestroy() { + for (let obs of this._destructionObservers) { + obs.actorDestroyed(this); + } + this._destructionObservers = null; + } +} diff --git a/toolkit/actors/KeyPressEventModelCheckerChild.sys.mjs b/toolkit/actors/KeyPressEventModelCheckerChild.sys.mjs new file mode 100644 index 0000000000..8b4fe82f17 --- /dev/null +++ b/toolkit/actors/KeyPressEventModelCheckerChild.sys.mjs @@ -0,0 +1,107 @@ +/* -*- mode: js; indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=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/. */ + +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; + +export class KeyPressEventModelCheckerChild extends JSWindowActorChild { + // Currently, the event is dispatched only when the document becomes editable + // because of contenteditable. If you need to add new editor which is in + // designMode, you need to change MaybeDispatchCheckKeyPressEventModelEvent() + // of Document. + handleEvent(aEvent) { + if (!AppConstants.DEBUG) { + // Stop propagation in opt build to save the propagation cost. + // However, the event is necessary for running test_bug1514940.html. + // Therefore, we need to keep propagating it at least on debug build. + aEvent.stopImmediatePropagation(); + } + + // Currently, even if we set Document.KEYPRESS_EVENT_MODEL_CONFLATED + // here, conflated model isn't used forcibly. If you need it, you need + // to change WidgetKeyboardEvent, dom::KeyboardEvent and PresShell. + let model = Document.KEYPRESS_EVENT_MODEL_DEFAULT; + if ( + this._isOldOfficeOnlineServer(aEvent.target) || + this._isOldConfluence(aEvent.target.ownerGlobal) + ) { + model = Document.KEYPRESS_EVENT_MODEL_SPLIT; + } + aEvent.target.setKeyPressEventModel(model); + } + + _isOldOfficeOnlineServer(aDocument) { + let editingElement = aDocument.getElementById( + "WACViewPanel_EditingElement" + ); + // If it's not Office Online Server, don't include it into the telemetry + // because we just need to collect percentage of old version in all loaded + // Office Online Server instances. + if (!editingElement) { + return false; + } + let isOldVersion = !editingElement.classList.contains( + "WACViewPanel_DisableLegacyKeyCodeAndCharCode" + ); + Services.telemetry.keyedScalarAdd( + "dom.event.office_online_load_count", + isOldVersion ? "old" : "new", + 1 + ); + return isOldVersion; + } + + _isOldConfluence(aWindow) { + if (!aWindow) { + return false; + } + // aWindow should be an editor window in