From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- browser/actors/AboutNewTabChild.sys.mjs | 104 ++ browser/actors/AboutNewTabParent.sys.mjs | 148 ++ browser/actors/AboutPluginsChild.sys.mjs | 8 + browser/actors/AboutPluginsParent.sys.mjs | 40 + browser/actors/AboutPocketChild.sys.mjs | 8 + browser/actors/AboutPocketParent.sys.mjs | 132 ++ browser/actors/AboutPrivateBrowsingChild.sys.mjs | 64 + browser/actors/AboutPrivateBrowsingParent.sys.mjs | 181 +++ browser/actors/AboutProtectionsChild.sys.mjs | 24 + browser/actors/AboutProtectionsParent.sys.mjs | 444 ++++++ browser/actors/AboutReaderChild.sys.mjs | 252 ++++ browser/actors/AboutReaderParent.sys.mjs | 331 +++++ browser/actors/AboutTabCrashedChild.sys.mjs | 8 + browser/actors/AboutTabCrashedParent.sys.mjs | 92 ++ browser/actors/BlockedSiteChild.sys.mjs | 187 +++ browser/actors/BlockedSiteParent.sys.mjs | 70 + browser/actors/BrowserProcessChild.sys.mjs | 41 + browser/actors/BrowserTabChild.sys.mjs | 55 + browser/actors/ClickHandlerChild.sys.mjs | 174 +++ browser/actors/ClickHandlerParent.sys.mjs | 156 +++ browser/actors/ContentSearchChild.sys.mjs | 35 + browser/actors/ContentSearchParent.sys.mjs | 671 +++++++++ browser/actors/ContextMenuChild.sys.mjs | 1239 +++++++++++++++++ browser/actors/ContextMenuParent.sys.mjs | 117 ++ browser/actors/DOMFullscreenChild.sys.mjs | 164 +++ browser/actors/DOMFullscreenParent.sys.mjs | 318 +++++ browser/actors/DecoderDoctorChild.sys.mjs | 28 + browser/actors/DecoderDoctorParent.sys.mjs | 272 ++++ browser/actors/EncryptedMediaChild.sys.mjs | 121 ++ browser/actors/EncryptedMediaParent.sys.mjs | 267 ++++ browser/actors/FormValidationChild.sys.mjs | 193 +++ browser/actors/FormValidationParent.sys.mjs | 204 +++ browser/actors/LightweightThemeChild.sys.mjs | 82 ++ browser/actors/LinkHandlerChild.sys.mjs | 177 +++ browser/actors/LinkHandlerParent.sys.mjs | 156 +++ browser/actors/PageInfoChild.sys.mjs | 392 ++++++ browser/actors/PageStyleChild.sys.mjs | 199 +++ browser/actors/PageStyleParent.sys.mjs | 72 + browser/actors/PluginChild.sys.mjs | 92 ++ browser/actors/PluginParent.sys.mjs | 212 +++ browser/actors/PointerLockChild.sys.mjs | 17 + browser/actors/PointerLockParent.sys.mjs | 22 + browser/actors/PromptParent.sys.mjs | 465 +++++++ browser/actors/RFPHelperChild.sys.mjs | 25 + browser/actors/RFPHelperParent.sys.mjs | 33 + browser/actors/RefreshBlockerChild.sys.mjs | 234 ++++ browser/actors/RefreshBlockerParent.sys.mjs | 17 + browser/actors/ScreenshotsComponentChild.sys.mjs | 273 ++++ browser/actors/SearchSERPTelemetryChild.sys.mjs | 1071 ++++++++++++++ browser/actors/SearchSERPTelemetryParent.sys.mjs | 34 + .../actors/SwitchDocumentDirectionChild.sys.mjs | 27 + browser/actors/WebRTCChild.sys.mjs | 585 ++++++++ browser/actors/WebRTCParent.sys.mjs | 1471 ++++++++++++++++++++ browser/actors/moz.build | 92 ++ browser/actors/test/browser/browser.ini | 4 + .../test/browser/browser_nested_link_clicks.js | 45 + .../test/browser/browser_untrusted_click_event.js | 107 ++ browser/actors/test/browser/click.html | 76 + 58 files changed, 12128 insertions(+) create mode 100644 browser/actors/AboutNewTabChild.sys.mjs create mode 100644 browser/actors/AboutNewTabParent.sys.mjs create mode 100644 browser/actors/AboutPluginsChild.sys.mjs create mode 100644 browser/actors/AboutPluginsParent.sys.mjs create mode 100644 browser/actors/AboutPocketChild.sys.mjs create mode 100644 browser/actors/AboutPocketParent.sys.mjs create mode 100644 browser/actors/AboutPrivateBrowsingChild.sys.mjs create mode 100644 browser/actors/AboutPrivateBrowsingParent.sys.mjs create mode 100644 browser/actors/AboutProtectionsChild.sys.mjs create mode 100644 browser/actors/AboutProtectionsParent.sys.mjs create mode 100644 browser/actors/AboutReaderChild.sys.mjs create mode 100644 browser/actors/AboutReaderParent.sys.mjs create mode 100644 browser/actors/AboutTabCrashedChild.sys.mjs create mode 100644 browser/actors/AboutTabCrashedParent.sys.mjs create mode 100644 browser/actors/BlockedSiteChild.sys.mjs create mode 100644 browser/actors/BlockedSiteParent.sys.mjs create mode 100644 browser/actors/BrowserProcessChild.sys.mjs create mode 100644 browser/actors/BrowserTabChild.sys.mjs create mode 100644 browser/actors/ClickHandlerChild.sys.mjs create mode 100644 browser/actors/ClickHandlerParent.sys.mjs create mode 100644 browser/actors/ContentSearchChild.sys.mjs create mode 100644 browser/actors/ContentSearchParent.sys.mjs create mode 100644 browser/actors/ContextMenuChild.sys.mjs create mode 100644 browser/actors/ContextMenuParent.sys.mjs create mode 100644 browser/actors/DOMFullscreenChild.sys.mjs create mode 100644 browser/actors/DOMFullscreenParent.sys.mjs create mode 100644 browser/actors/DecoderDoctorChild.sys.mjs create mode 100644 browser/actors/DecoderDoctorParent.sys.mjs create mode 100644 browser/actors/EncryptedMediaChild.sys.mjs create mode 100644 browser/actors/EncryptedMediaParent.sys.mjs create mode 100644 browser/actors/FormValidationChild.sys.mjs create mode 100644 browser/actors/FormValidationParent.sys.mjs create mode 100644 browser/actors/LightweightThemeChild.sys.mjs create mode 100644 browser/actors/LinkHandlerChild.sys.mjs create mode 100644 browser/actors/LinkHandlerParent.sys.mjs create mode 100644 browser/actors/PageInfoChild.sys.mjs create mode 100644 browser/actors/PageStyleChild.sys.mjs create mode 100644 browser/actors/PageStyleParent.sys.mjs create mode 100644 browser/actors/PluginChild.sys.mjs create mode 100644 browser/actors/PluginParent.sys.mjs create mode 100644 browser/actors/PointerLockChild.sys.mjs create mode 100644 browser/actors/PointerLockParent.sys.mjs create mode 100644 browser/actors/PromptParent.sys.mjs create mode 100644 browser/actors/RFPHelperChild.sys.mjs create mode 100644 browser/actors/RFPHelperParent.sys.mjs create mode 100644 browser/actors/RefreshBlockerChild.sys.mjs create mode 100644 browser/actors/RefreshBlockerParent.sys.mjs create mode 100644 browser/actors/ScreenshotsComponentChild.sys.mjs create mode 100644 browser/actors/SearchSERPTelemetryChild.sys.mjs create mode 100644 browser/actors/SearchSERPTelemetryParent.sys.mjs create mode 100644 browser/actors/SwitchDocumentDirectionChild.sys.mjs create mode 100644 browser/actors/WebRTCChild.sys.mjs create mode 100644 browser/actors/WebRTCParent.sys.mjs create mode 100644 browser/actors/moz.build create mode 100644 browser/actors/test/browser/browser.ini create mode 100644 browser/actors/test/browser/browser_nested_link_clicks.js create mode 100644 browser/actors/test/browser/browser_untrusted_click_event.js create mode 100644 browser/actors/test/browser/click.html (limited to 'browser/actors') diff --git a/browser/actors/AboutNewTabChild.sys.mjs b/browser/actors/AboutNewTabChild.sys.mjs new file mode 100644 index 0000000000..87e8a99135 --- /dev/null +++ b/browser/actors/AboutNewTabChild.sys.mjs @@ -0,0 +1,104 @@ +/* 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"; +import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; +import { PrivateBrowsingUtils } from "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"; +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "ACTIVITY_STREAM_DEBUG", + "browser.newtabpage.activity-stream.debug", + false +); + +let gNextPortID = 0; + +export class AboutNewTabChild extends RemotePageChild { + handleEvent(event) { + if (event.type == "DOMDocElementInserted") { + let portID = Services.appinfo.processID + ":" + ++gNextPortID; + + this.sendAsyncMessage("Init", { + portID, + url: this.contentWindow.document.documentURI.replace(/[\#|\?].*$/, ""), + }); + } else if (event.type == "load") { + this.sendAsyncMessage("Load"); + } else if (event.type == "DOMContentLoaded") { + if (!this.contentWindow.document.body.firstElementChild) { + return; // about:newtab is a blank page + } + + // If the separate about:welcome page is enabled, we can skip all of this, + // since that mode doesn't load any of the Activity Stream bits. + if ( + (lazy.NimbusFeatures.aboutwelcome.getVariable("enabled") ?? true) && + this.contentWindow.location.pathname.includes("welcome") + ) { + return; + } + + const debug = !AppConstants.RELEASE_OR_BETA && lazy.ACTIVITY_STREAM_DEBUG; + const debugString = debug ? "-dev" : ""; + + // This list must match any similar ones in render-activity-stream-html.js. + const scripts = [ + "chrome://browser/content/contentSearchUI.js", + "chrome://browser/content/contentSearchHandoffUI.js", + "chrome://browser/content/contentTheme.js", + `resource://activity-stream/vendor/react${debugString}.js`, + `resource://activity-stream/vendor/react-dom${debugString}.js`, + "resource://activity-stream/vendor/prop-types.js", + "resource://activity-stream/vendor/react-transition-group.js", + "resource://activity-stream/vendor/redux.js", + "resource://activity-stream/vendor/react-redux.js", + "resource://activity-stream/data/content/activity-stream.bundle.js", + "resource://activity-stream/data/content/newtab-render.js", + ]; + + for (let script of scripts) { + Services.scriptloader.loadSubScript(script, this.contentWindow); + } + } else if (event.type == "unload") { + try { + this.sendAsyncMessage("Unload"); + } catch (e) { + // If the tab has been closed the frame message manager has already been + // destroyed + } + } else if ( + (event.type == "pageshow" || event.type == "visibilitychange") && + // The default browser notification shouldn't be shown on about:welcome + // since we don't want to distract from the onboarding wizard. + !this.contentWindow.location.pathname.includes("welcome") + ) { + // Don't show the notification in non-permanent private windows + // since it is expected to have very little opt-in here. + let contentWindowPrivate = PrivateBrowsingUtils.isContentWindowPrivate( + this.contentWindow + ); + if ( + this.document.visibilityState == "visible" && + (!contentWindowPrivate || + (contentWindowPrivate && + PrivateBrowsingUtils.permanentPrivateBrowsing)) + ) { + this.sendAsyncMessage("AboutNewTabVisible"); + + // Note: newtab feature info is currently being loaded in PrefsFeed.jsm, + // But we're recording exposure events here. + lazy.NimbusFeatures.newtab.recordExposureEvent({ once: true }); + } + } + } +} diff --git a/browser/actors/AboutNewTabParent.sys.mjs b/browser/actors/AboutNewTabParent.sys.mjs new file mode 100644 index 0000000000..27d5f82c0c --- /dev/null +++ b/browser/actors/AboutNewTabParent.sys.mjs @@ -0,0 +1,148 @@ +/* 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, { + AboutNewTab: "resource:///modules/AboutNewTab.jsm", + ASRouter: "resource://activity-stream/lib/ASRouter.jsm", +}); + +// A mapping of loaded new tab pages, where the mapping is: +// browser -> { actor, browser, browsingContext, portID, url, loaded } +let gLoadedTabs = new Map(); + +export class AboutNewTabParent extends JSWindowActorParent { + static get loadedTabs() { + return gLoadedTabs; + } + + getTabDetails(message) { + let browsingContext = message.target.browsingContext; + let browser = browsingContext.top.embedderElement; + return browser ? gLoadedTabs.get(browser) : null; + } + + handleEvent(event) { + if (event.type == "SwapDocShells") { + let oldBrowser = this.browsingContext.top.embedderElement; + let newBrowser = event.detail; + + let tabDetails = gLoadedTabs.get(oldBrowser); + if (tabDetails) { + tabDetails.browser = newBrowser; + gLoadedTabs.delete(oldBrowser); + gLoadedTabs.set(newBrowser, tabDetails); + + oldBrowser.removeEventListener("SwapDocShells", this); + newBrowser.addEventListener("SwapDocShells", this); + } + } + } + + async receiveMessage(message) { + switch (message.name) { + case "AboutNewTabVisible": + await lazy.ASRouter.waitForInitialized; + lazy.ASRouter.sendTriggerMessage({ + browser: this.browsingContext.top.embedderElement, + // triggerId and triggerContext + id: "defaultBrowserCheck", + context: { source: "newtab" }, + }); + break; + + case "Init": { + let actor = message.target; + let browsingContext = actor.browsingContext; + let browser = browsingContext.top.embedderElement; + if (!browser) { + return; + } + + let tabDetails = { + actor, + browser, + browsingContext, + portID: message.data.portID, + url: message.data.url, + }; + gLoadedTabs.set(browser, tabDetails); + + browser.addEventListener("SwapDocShells", this); + browser.addEventListener("EndSwapDocShells", this); + + this.notifyActivityStreamChannel("onNewTabInit", message, tabDetails); + break; + } + + case "Load": + this.notifyActivityStreamChannel("onNewTabLoad", message); + break; + + case "Unload": { + let tabDetails = this.getTabDetails(message); + if (!tabDetails) { + // When closing a tab, the embedderElement can already be disconnected, so + // an a backup, look up the tab details by browsing context. + tabDetails = this.getByBrowsingContext( + message.target.browsingContext + ); + } + + if (!tabDetails) { + return; + } + + tabDetails.browser.removeEventListener("EndSwapDocShells", this); + + gLoadedTabs.delete(tabDetails.browser); + + this.notifyActivityStreamChannel("onNewTabUnload", message, tabDetails); + break; + } + + case "ActivityStream:ContentToMain": + this.notifyActivityStreamChannel("onMessage", message); + break; + } + } + + notifyActivityStreamChannel(name, message, tabDetails) { + if (!tabDetails) { + tabDetails = this.getTabDetails(message); + if (!tabDetails) { + return; + } + } + + let channel = this.getChannel(); + if (!channel) { + return; + } + + let messageToSend = { + target: message.target, + data: message.data || {}, + }; + + channel[name](messageToSend, tabDetails); + } + + getByBrowsingContext(expectedBrowsingContext) { + for (let tabDetails of AboutNewTabParent.loadedTabs.values()) { + if (tabDetails.browsingContext === expectedBrowsingContext) { + return tabDetails; + } + } + + return null; + } + + getChannel() { + return lazy.AboutNewTab.activityStream?.store?.getMessageChannel(); + } +} diff --git a/browser/actors/AboutPluginsChild.sys.mjs b/browser/actors/AboutPluginsChild.sys.mjs new file mode 100644 index 0000000000..8529e43ec1 --- /dev/null +++ b/browser/actors/AboutPluginsChild.sys.mjs @@ -0,0 +1,8 @@ +/* -*- 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/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +export class AboutPluginsChild extends RemotePageChild {} diff --git a/browser/actors/AboutPluginsParent.sys.mjs b/browser/actors/AboutPluginsParent.sys.mjs new file mode 100644 index 0000000000..efde242dff --- /dev/null +++ b/browser/actors/AboutPluginsParent.sys.mjs @@ -0,0 +1,40 @@ +/* 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, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", +}); + +// Lists all the properties that plugins.html needs +const NEEDED_PROPS = [ + "name", + "pluginLibraries", + "pluginFullpath", + "version", + "isActive", + "blocklistState", + "description", +]; + +export class AboutPluginsParent extends JSWindowActorParent { + async receiveMessage(message) { + switch (message.name) { + case "RequestPlugins": + function filterProperties(plugin) { + let filtered = {}; + for (let prop of NEEDED_PROPS) { + filtered[prop] = plugin[prop]; + } + return filtered; + } + + let plugins = await lazy.AddonManager.getAddonsByTypes(["plugin"]); + return plugins.map(filterProperties); + } + + return undefined; + } +} diff --git a/browser/actors/AboutPocketChild.sys.mjs b/browser/actors/AboutPocketChild.sys.mjs new file mode 100644 index 0000000000..e7f382882a --- /dev/null +++ b/browser/actors/AboutPocketChild.sys.mjs @@ -0,0 +1,8 @@ +/* -*- 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/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +export class AboutPocketChild extends RemotePageChild {} diff --git a/browser/actors/AboutPocketParent.sys.mjs b/browser/actors/AboutPocketParent.sys.mjs new file mode 100644 index 0000000000..ae40dfa1d9 --- /dev/null +++ b/browser/actors/AboutPocketParent.sys.mjs @@ -0,0 +1,132 @@ +/* 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, { + pktApi: "chrome://pocket/content/pktApi.sys.mjs", + SaveToPocket: "chrome://pocket/content/SaveToPocket.sys.mjs", +}); + +export class AboutPocketParent extends JSWindowActorParent { + sendResponseMessageToPanel(messageId, payload) { + this.sendAsyncMessage(`${messageId}_response`, payload); + } + + isPanalAvailable() { + return !!this.manager && !this.manager.isClosed; + } + + async receiveMessage(message) { + switch (message.name) { + case "PKT_show_signup": { + this.browsingContext.topChromeWindow?.pktUI.onShowSignup(); + break; + } + case "PKT_show_saved": { + this.browsingContext.topChromeWindow?.pktUI.onShowSaved(); + break; + } + case "PKT_show_home": { + this.browsingContext.topChromeWindow?.pktUI.onShowHome(); + break; + } + case "PKT_close": { + this.browsingContext.topChromeWindow?.pktUI.closePanel(); + break; + } + case "PKT_openTabWithUrl": { + this.browsingContext.topChromeWindow?.pktUI.onOpenTabWithUrl( + message.data, + this.browsingContext.embedderElement.contentPrincipal, + this.browsingContext.embedderElement.csp + ); + break; + } + case "PKT_openTabWithPocketUrl": { + this.browsingContext.topChromeWindow?.pktUI.onOpenTabWithPocketUrl( + message.data, + this.browsingContext.embedderElement.contentPrincipal, + this.browsingContext.embedderElement.csp + ); + break; + } + case "PKT_resizePanel": { + this.browsingContext.topChromeWindow?.pktUI.resizePanel(message.data); + this.sendResponseMessageToPanel("PKT_resizePanel"); + break; + } + case "PKT_getTags": { + this.sendResponseMessageToPanel("PKT_getTags", lazy.pktApi.getTags()); + break; + } + case "PKT_getRecentTags": { + this.sendResponseMessageToPanel( + "PKT_getRecentTags", + lazy.pktApi.getRecentTags() + ); + break; + } + case "PKT_getSuggestedTags": { + // Ask for suggested tags based on passed url + const result = await new Promise(resolve => { + lazy.pktApi.getSuggestedTagsForURL(message.data.url, { + success: data => { + var successResponse = { + status: "success", + value: { + suggestedTags: data.suggested_tags, + }, + }; + resolve(successResponse); + }, + error: error => resolve({ status: "error", error }), + }); + }); + + // If the doorhanger is still open, send the result. + if (this.isPanalAvailable()) { + this.sendResponseMessageToPanel("PKT_getSuggestedTags", result); + } + break; + } + case "PKT_addTags": { + // Pass url and array list of tags, add to existing save item accordingly + const result = await new Promise(resolve => { + lazy.pktApi.addTagsToURL(message.data.url, message.data.tags, { + success: () => resolve({ status: "success" }), + error: error => resolve({ status: "error", error }), + }); + }); + + // If the doorhanger is still open, send the result. + if (this.isPanalAvailable()) { + this.sendResponseMessageToPanel("PKT_addTags", result); + } + break; + } + case "PKT_deleteItem": { + // Based on clicking "remove page" CTA, and passed unique item id, remove the item + const result = await new Promise(resolve => { + lazy.pktApi.deleteItem(message.data.itemId, { + success: () => { + resolve({ status: "success" }); + lazy.SaveToPocket.itemDeleted(); + }, + error: error => resolve({ status: "error", error }), + }); + }); + + // If the doorhanger is still open, send the result. + if (this.isPanalAvailable()) { + this.sendResponseMessageToPanel("PKT_deleteItem", result); + } + break; + } + case "PKT_log": { + console.log(...Object.values(message.data)); + break; + } + } + } +} diff --git a/browser/actors/AboutPrivateBrowsingChild.sys.mjs b/browser/actors/AboutPrivateBrowsingChild.sys.mjs new file mode 100644 index 0000000000..1433c9cb79 --- /dev/null +++ b/browser/actors/AboutPrivateBrowsingChild.sys.mjs @@ -0,0 +1,64 @@ +/* -*- 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/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", + ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", +}); + +export class AboutPrivateBrowsingChild extends RemotePageChild { + actorCreated() { + super.actorCreated(); + let window = this.contentWindow; + + Cu.exportFunction(this.PrivateBrowsingRecordClick.bind(this), window, { + defineAs: "PrivateBrowsingRecordClick", + }); + Cu.exportFunction( + this.PrivateBrowsingShouldHideDefault.bind(this), + window, + { + defineAs: "PrivateBrowsingShouldHideDefault", + } + ); + Cu.exportFunction(this.PrivateBrowsingEnableNewLogo.bind(this), window, { + defineAs: "PrivateBrowsingEnableNewLogo", + }); + Cu.exportFunction( + this.PrivateBrowsingExposureTelemetry.bind(this), + window, + { defineAs: "PrivateBrowsingExposureTelemetry" } + ); + } + + PrivateBrowsingRecordClick(source) { + const experiment = lazy.ExperimentAPI.getExperimentMetaData({ + featureId: "pbNewtab", + }); + if (experiment) { + Services.telemetry.recordEvent("aboutprivatebrowsing", "click", source); + } + return experiment; + } + + PrivateBrowsingShouldHideDefault() { + const config = lazy.NimbusFeatures.pbNewtab.getAllVariables() || {}; + return config?.content?.hideDefault; + } + + PrivateBrowsingEnableNewLogo() { + return lazy.NimbusFeatures.majorRelease2022.getVariable( + "feltPrivacyPBMNewLogo" + ); + } + + PrivateBrowsingExposureTelemetry() { + lazy.NimbusFeatures.pbNewtab.recordExposureEvent({ once: false }); + } +} diff --git a/browser/actors/AboutPrivateBrowsingParent.sys.mjs b/browser/actors/AboutPrivateBrowsingParent.sys.mjs new file mode 100644 index 0000000000..9e8b1cb1f8 --- /dev/null +++ b/browser/actors/AboutPrivateBrowsingParent.sys.mjs @@ -0,0 +1,181 @@ +/* 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 { ASRouter } = ChromeUtils.import( + "resource://activity-stream/lib/ASRouter.jsm" +); +import { BrowserUtils } from "resource://gre/modules/BrowserUtils.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const SHOWN_PREF = "browser.search.separatePrivateDefault.ui.banner.shown"; +const lazy = {}; +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "MAX_SEARCH_BANNER_SHOW_COUNT", + "browser.search.separatePrivateDefault.ui.banner.max", + 0 +); + +XPCOMUtils.defineLazyPreferenceGetter( + lazy, + "isPrivateSearchUIEnabled", + "browser.search.separatePrivateDefault.ui.enabled", + false +); + +ChromeUtils.defineESModuleGetters(lazy, { + SpecialMessageActions: + "resource://messaging-system/lib/SpecialMessageActions.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", +}); + +// We only show the private search banner once per browser session. +let gSearchBannerShownThisSession; + +export class AboutPrivateBrowsingParent extends JSWindowActorParent { + constructor() { + super(); + Services.telemetry.setEventRecordingEnabled("aboutprivatebrowsing", true); + } + // Used by tests + static setShownThisSession(shown) { + gSearchBannerShownThisSession = shown; + } + + receiveMessage(aMessage) { + let browser = this.browsingContext.top.embedderElement; + if (!browser) { + return undefined; + } + + let win = browser.ownerGlobal; + + switch (aMessage.name) { + case "OpenPrivateWindow": { + win.OpenBrowserWindow({ private: true }); + break; + } + case "OpenSearchPreferences": { + win.openPreferences("search", { origin: "about-privatebrowsing" }); + break; + } + case "SearchHandoff": { + let urlBar = win.gURLBar; + let searchEngine = Services.search.defaultPrivateEngine; + let isFirstChange = true; + + if (!aMessage.data || !aMessage.data.text) { + urlBar.setHiddenFocus(); + } else { + // Pass the provided text to the awesomebar + urlBar.handoff(aMessage.data.text, searchEngine); + isFirstChange = false; + } + + let checkFirstChange = () => { + // Check if this is the first change since we hidden focused. If it is, + // remove hidden focus styles, prepend the search alias and hide the + // in-content search. + if (isFirstChange) { + isFirstChange = false; + urlBar.removeHiddenFocus(true); + urlBar.handoff("", searchEngine); + this.sendAsyncMessage("DisableSearch"); + urlBar.removeEventListener("compositionstart", checkFirstChange); + urlBar.removeEventListener("paste", checkFirstChange); + } + }; + + let onKeydown = ev => { + // Check if the keydown will cause a value change. + if (ev.key.length === 1 && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { + checkFirstChange(); + } + // If the Esc button is pressed, we are done. Show in-content search and cleanup. + if (ev.key === "Escape") { + onDone(); + } + }; + + let onDone = ev => { + // We are done. Show in-content search again and cleanup. + this.sendAsyncMessage("ShowSearch"); + + const forceSuppressFocusBorder = ev?.type === "mousedown"; + urlBar.removeHiddenFocus(forceSuppressFocusBorder); + + urlBar.removeEventListener("keydown", onKeydown); + urlBar.removeEventListener("mousedown", onDone); + urlBar.removeEventListener("blur", onDone); + urlBar.removeEventListener("compositionstart", checkFirstChange); + urlBar.removeEventListener("paste", checkFirstChange); + }; + + urlBar.addEventListener("keydown", onKeydown); + urlBar.addEventListener("mousedown", onDone); + urlBar.addEventListener("blur", onDone); + urlBar.addEventListener("compositionstart", checkFirstChange); + urlBar.addEventListener("paste", checkFirstChange); + break; + } + case "ShouldShowSearch": { + let engineName = Services.prefs.getStringPref( + "browser.urlbar.placeholderName.private", + "" + ); + let shouldHandOffToSearchMode = lazy.UrlbarPrefs.get( + "shouldHandOffToSearchMode" + ); + return [engineName, shouldHandOffToSearchMode]; + } + case "ShouldShowSearchBanner": { + // If this is a pre-loaded private browsing new tab, then we don't want + // to display the banner - it might never get displayed to the user + // and we won't know, or it might get displayed at the wrong time. + // This has the minor downside of not displaying the banner if + // you go into private browsing via opening a link, and then opening + // a new tab, we won't display the banner, for now, that's ok. + if (browser.getAttribute("preloadedState") === "preloaded") { + return null; + } + + if (!lazy.isPrivateSearchUIEnabled || gSearchBannerShownThisSession) { + return null; + } + gSearchBannerShownThisSession = true; + const shownTimes = Services.prefs.getIntPref(SHOWN_PREF, 0); + if (shownTimes >= lazy.MAX_SEARCH_BANNER_SHOW_COUNT) { + return null; + } + Services.prefs.setIntPref(SHOWN_PREF, shownTimes + 1); + return new Promise(resolve => { + Services.search.getDefaultPrivate().then(engine => { + resolve(engine.name); + }); + }); + } + case "SearchBannerDismissed": { + Services.prefs.setIntPref( + SHOWN_PREF, + lazy.MAX_SEARCH_BANNER_SHOW_COUNT + ); + break; + } + case "ShouldShowPromo": { + return BrowserUtils.shouldShowPromo( + BrowserUtils.PromoType[aMessage.data.type] + ); + } + case "SpecialMessageActionDispatch": { + lazy.SpecialMessageActions.handleAction(aMessage.data, browser); + break; + } + case "IsPromoBlocked": { + return !ASRouter.isUnblockedMessage(aMessage.data); + } + } + + return undefined; + } +} diff --git a/browser/actors/AboutProtectionsChild.sys.mjs b/browser/actors/AboutProtectionsChild.sys.mjs new file mode 100644 index 0000000000..62a8b1ddff --- /dev/null +++ b/browser/actors/AboutProtectionsChild.sys.mjs @@ -0,0 +1,24 @@ +/* -*- 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/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +export class AboutProtectionsChild extends RemotePageChild { + actorCreated() { + super.actorCreated(); + + this.exportFunctions(["RPMRecordTelemetryEvent"]); + } + + RPMRecordTelemetryEvent(category, event, object, value, extra) { + return Services.telemetry.recordEvent( + category, + event, + object, + value, + extra + ); + } +} diff --git a/browser/actors/AboutProtectionsParent.sys.mjs b/browser/actors/AboutProtectionsParent.sys.mjs new file mode 100644 index 0000000000..c1586559a6 --- /dev/null +++ b/browser/actors/AboutProtectionsParent.sys.mjs @@ -0,0 +1,444 @@ +/* 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, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", + LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs", + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + Region: "resource://gre/modules/Region.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.js", + FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.js", +}); + +XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => { + return ChromeUtils.importESModule( + "resource://gre/modules/FxAccounts.sys.mjs" + ).getFxAccountsSingleton(); +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "TrackingDBService", + "@mozilla.org/tracking-db-service;1", + "nsITrackingDBService" +); + +let idToTextMap = new Map([ + [Ci.nsITrackingDBService.TRACKERS_ID, "tracker"], + [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookie"], + [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominer"], + [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinter"], + [Ci.nsITrackingDBService.SOCIAL_ID, "social"], +]); + +const MONITOR_API_ENDPOINT = Services.urlFormatter.formatURLPref( + "browser.contentblocking.report.endpoint_url" +); + +const SECURE_PROXY_ADDON_ID = "secure-proxy@mozilla.com"; + +const SCOPE_MONITOR = [ + "profile:uid", + "https://identity.mozilla.com/apps/monitor", +]; + +const SCOPE_VPN = "profile https://identity.mozilla.com/account/subscriptions"; +const VPN_ENDPOINT = `${Services.prefs.getStringPref( + "identity.fxaccounts.auth.uri" +)}oauth/subscriptions/active`; + +// The ID of the vpn subscription, if we see this ID attached to a user's account then they have subscribed to vpn. +const VPN_SUB_ID = Services.prefs.getStringPref( + "browser.contentblocking.report.vpn_sub_id" +); + +// Error messages +const INVALID_OAUTH_TOKEN = "Invalid OAuth token"; +const USER_UNSUBSCRIBED_TO_MONITOR = "User is not subscribed to Monitor"; +const SERVICE_UNAVAILABLE = "Service unavailable"; +const UNEXPECTED_RESPONSE = "Unexpected response"; +const UNKNOWN_ERROR = "Unknown error"; + +// Valid response info for successful Monitor data +const MONITOR_RESPONSE_PROPS = [ + "monitoredEmails", + "numBreaches", + "passwords", + "numBreachesResolved", + "passwordsResolved", +]; + +let gTestOverride = null; +let monitorResponse = null; +let entrypoint = "direct"; + +export class AboutProtectionsParent extends JSWindowActorParent { + constructor() { + super(); + } + + // Some tests wish to override certain functions with ones that mostly do nothing. + static setTestOverride(callback) { + gTestOverride = callback; + } + + /** + * Fetches and validates data from the Monitor endpoint. If successful, then return + * expected data. Otherwise, throw the appropriate error depending on the status code. + * + * @return valid data from endpoint. + */ + async fetchUserBreachStats(token) { + if (monitorResponse && monitorResponse.timestamp) { + var timeDiff = Date.now() - monitorResponse.timestamp; + let oneDayInMS = 24 * 60 * 60 * 1000; + if (timeDiff >= oneDayInMS) { + monitorResponse = null; + } else { + return monitorResponse; + } + } + + // Make the request + const headers = new Headers(); + headers.append("Authorization", `Bearer ${token}`); + const request = new Request(MONITOR_API_ENDPOINT, { headers }); + const response = await fetch(request); + + if (response.ok) { + // Validate the shape of the response is what we're expecting. + const json = await response.json(); + + // Make sure that we're getting the expected data. + let isValid = null; + for (let prop in json) { + isValid = MONITOR_RESPONSE_PROPS.includes(prop); + + if (!isValid) { + break; + } + } + + monitorResponse = isValid ? json : new Error(UNEXPECTED_RESPONSE); + if (isValid) { + monitorResponse.timestamp = Date.now(); + } + } else { + // Check the reason for the error + switch (response.status) { + case 400: + case 401: + monitorResponse = new Error(INVALID_OAUTH_TOKEN); + break; + case 404: + monitorResponse = new Error(USER_UNSUBSCRIBED_TO_MONITOR); + break; + case 503: + monitorResponse = new Error(SERVICE_UNAVAILABLE); + break; + default: + monitorResponse = new Error(UNKNOWN_ERROR); + break; + } + } + + if (monitorResponse instanceof Error) { + throw monitorResponse; + } + return monitorResponse; + } + + /** + * Retrieves login data for the user. + * + * @return {{ + * numLogins: Number, + * potentiallyBreachedLogins: Number, + * mobileDeviceConnected: Boolean }} + */ + async getLoginData() { + if (gTestOverride && "getLoginData" in gTestOverride) { + return gTestOverride.getLoginData(); + } + + try { + if (await lazy.fxAccounts.getSignedInUser()) { + await lazy.fxAccounts.device.refreshDeviceList(); + } + } catch (e) { + console.error("There was an error fetching login data: ", e.message); + } + + const userFacingLogins = + Services.logins.countLogins("", "", "") - + Services.logins.countLogins( + lazy.FXA_PWDMGR_HOST, + null, + lazy.FXA_PWDMGR_REALM + ); + + let potentiallyBreachedLogins = null; + // Get the stats for number of potentially breached Lockwise passwords + // if the Primary Password isn't locked. + if (userFacingLogins && Services.logins.isLoggedIn) { + const logins = await lazy.LoginHelper.getAllUserFacingLogins(); + potentiallyBreachedLogins = + await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins); + } + + let mobileDeviceConnected = + lazy.fxAccounts.device.recentDeviceList && + lazy.fxAccounts.device.recentDeviceList.filter( + device => device.type == "mobile" + ).length; + + return { + numLogins: userFacingLogins, + potentiallyBreachedLogins: potentiallyBreachedLogins + ? potentiallyBreachedLogins.size + : 0, + mobileDeviceConnected, + }; + } + + /** + * Retrieves monitor data for the user. + * + * @return {{ monitoredEmails: Number, + * numBreaches: Number, + * passwords: Number, + * userEmail: String|null, + * error: Boolean }} + * Monitor data. + */ + async getMonitorData() { + if (gTestOverride && "getMonitorData" in gTestOverride) { + monitorResponse = gTestOverride.getMonitorData(); + monitorResponse.timestamp = Date.now(); + // In a test, expect this to not fetch from the monitor endpoint due to the timestamp guaranteeing we use the cache. + monitorResponse = await this.fetchUserBreachStats(); + return monitorResponse; + } + + let monitorData = {}; + let userEmail = null; + let token = await this.getMonitorScopedOAuthToken(); + + try { + if (token) { + monitorData = await this.fetchUserBreachStats(token); + + // Send back user's email so the protections report can direct them to the proper + // OAuth flow on Monitor. + const { email } = await lazy.fxAccounts.getSignedInUser(); + userEmail = email; + } else { + // If no account exists, then the user is not logged in with an fxAccount. + monitorData = { + errorMessage: "No account", + }; + } + } catch (e) { + console.error(e.message); + monitorData.errorMessage = e.message; + + // If the user's OAuth token is invalid, we clear the cached token and refetch + // again. If OAuth token is invalid after the second fetch, then the monitor UI + // will simply show the "no logins" UI version. + if (e.message === INVALID_OAUTH_TOKEN) { + await lazy.fxAccounts.removeCachedOAuthToken({ token }); + token = await this.getMonitorScopedOAuthToken(); + + try { + monitorData = await this.fetchUserBreachStats(token); + } catch (_) { + console.error(e.message); + } + } else if (e.message === USER_UNSUBSCRIBED_TO_MONITOR) { + // Send back user's email so the protections report can direct them to the proper + // OAuth flow on Monitor. + const { email } = await lazy.fxAccounts.getSignedInUser(); + userEmail = email; + } else { + monitorData.errorMessage = e.message || "An error ocurred."; + } + } + + return { + ...monitorData, + userEmail, + error: !!monitorData.errorMessage, + }; + } + + async getMonitorScopedOAuthToken() { + let token = null; + + try { + token = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_MONITOR }); + } catch (e) { + console.error( + "There was an error fetching the user's token: ", + e.message + ); + } + + return token; + } + + /** + * The proxy card will only show if the user is in the US, has the browser language in "en-US", + * and does not yet have Proxy installed. + */ + async shouldShowProxyCard() { + const region = lazy.Region.home || ""; + const languages = Services.prefs.getComplexValue( + "intl.accept_languages", + Ci.nsIPrefLocalizedString + ); + const alreadyInstalled = await lazy.AddonManager.getAddonByID( + SECURE_PROXY_ADDON_ID + ); + + return ( + region.toLowerCase() === "us" && + !alreadyInstalled && + languages.data.toLowerCase().includes("en-us") + ); + } + + async VPNSubStatus() { + // For testing, set vpn sub status manually + if (gTestOverride && "vpnOverrides" in gTestOverride) { + return gTestOverride.vpnOverrides(); + } + + let vpnToken; + try { + vpnToken = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_VPN }); + } catch (e) { + console.error( + "There was an error fetching the user's token: ", + e.message + ); + // there was an error, assume user is not subscribed to VPN + return false; + } + let headers = new Headers(); + headers.append("Authorization", `Bearer ${vpnToken}`); + const request = new Request(VPN_ENDPOINT, { headers }); + const res = await fetch(request); + if (res.ok) { + const result = await res.json(); + for (let sub of result) { + if (sub.subscriptionId == VPN_SUB_ID) { + return true; + } + } + return false; + } + // unknown logic: assume user is not subscribed to VPN + return false; + } + + async receiveMessage(aMessage) { + let win = this.browsingContext.top.embedderElement.ownerGlobal; + switch (aMessage.name) { + case "OpenAboutLogins": + lazy.LoginHelper.openPasswordManager(win, { + entryPoint: "aboutprotections", + }); + break; + case "OpenContentBlockingPreferences": + win.openPreferences("privacy-trackingprotection", { + origin: "about-protections", + }); + break; + case "OpenSyncPreferences": + win.openTrustedLinkIn("about:preferences#sync", "tab"); + break; + case "FetchContentBlockingEvents": + let dataToSend = {}; + let displayNames = new Services.intl.DisplayNames(undefined, { + type: "weekday", + style: "abbreviated", + calendar: "gregory", + }); + + // Weekdays starting Sunday (7) to Saturday (6). + let weekdays = [7, 1, 2, 3, 4, 5, 6].map(day => displayNames.of(day)); + dataToSend.weekdays = weekdays; + + if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) { + dataToSend.isPrivate = true; + return dataToSend; + } + let sumEvents = await lazy.TrackingDBService.sumAllEvents(); + let earliestDate = + await lazy.TrackingDBService.getEarliestRecordedDate(); + let eventsByDate = await lazy.TrackingDBService.getEventsByDateRange( + aMessage.data.from, + aMessage.data.to + ); + let largest = 0; + + for (let result of eventsByDate) { + let count = result.getResultByName("count"); + let type = result.getResultByName("type"); + let timestamp = result.getResultByName("timestamp"); + dataToSend[timestamp] = dataToSend[timestamp] || { total: 0 }; + dataToSend[timestamp][idToTextMap.get(type)] = count; + dataToSend[timestamp].total += count; + // Record the largest amount of tracking events found per day, + // to create the tallest column on the graph and compare other days to. + if (largest < dataToSend[timestamp].total) { + largest = dataToSend[timestamp].total; + } + } + dataToSend.largest = largest; + dataToSend.earliestDate = earliestDate; + dataToSend.sumEvents = sumEvents; + + return dataToSend; + + case "FetchMonitorData": + return this.getMonitorData(); + + case "FetchUserLoginsData": + return this.getLoginData(); + + case "ClearMonitorCache": + monitorResponse = null; + break; + + case "GetShowProxyCard": + let card = await this.shouldShowProxyCard(); + return card; + + case "RecordEntryPoint": + entrypoint = aMessage.data.entrypoint; + break; + + case "FetchEntryPoint": + return entrypoint; + + case "FetchVPNSubStatus": + return this.VPNSubStatus(); + + case "FetchShowVPNCard": + return lazy.BrowserUtils.shouldShowVPNPromo(); + } + + return undefined; + } +} diff --git a/browser/actors/AboutReaderChild.sys.mjs b/browser/actors/AboutReaderChild.sys.mjs new file mode 100644 index 0000000000..b92078aaec --- /dev/null +++ b/browser/actors/AboutReaderChild.sys.mjs @@ -0,0 +1,252 @@ +/* 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, { + AboutReader: "resource://gre/modules/AboutReader.sys.mjs", + ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs", + Readerable: "resource://gre/modules/Readerable.sys.mjs", +}); + +var gUrlsToDocContentType = new Map(); +var gUrlsToDocTitle = new Map(); + +export class AboutReaderChild extends JSWindowActorChild { + constructor() { + super(); + + this._reader = null; + this._articlePromise = null; + this._isLeavingReaderableReaderMode = false; + } + + didDestroy() { + this.cancelPotentialPendingReadabilityCheck(); + this.readerModeHidden(); + } + + readerModeHidden() { + if (this._reader) { + this._reader.clearActor(); + } + this._reader = null; + } + + async receiveMessage(message) { + switch (message.name) { + case "Reader:ToggleReaderMode": + if (!this.isAboutReader) { + gUrlsToDocContentType.set( + this.document.URL, + this.document.contentType + ); + gUrlsToDocTitle.set(this.document.URL, this.document.title); + this._articlePromise = lazy.ReaderMode.parseDocument( + this.document + ).catch(console.error); + + // Get the article data and cache it in the parent process. The reader mode + // page will retrieve it when it has loaded. + let article = await this._articlePromise; + this.sendAsyncMessage("Reader:EnterReaderMode", article); + } else { + this.closeReaderMode(); + } + break; + + case "Reader:PushState": + this.updateReaderButton(!!(message.data && message.data.isArticle)); + break; + case "Reader:EnterReaderMode": { + lazy.ReaderMode.enterReaderMode(this.docShell, this.contentWindow); + break; + } + case "Reader:LeaveReaderMode": { + lazy.ReaderMode.leaveReaderMode(this.docShell, this.contentWindow); + break; + } + } + + // Forward the message to the reader if it has been created. + if (this._reader) { + this._reader.receiveMessage(message); + } + } + + get isAboutReader() { + if (!this.document) { + return false; + } + return this.document.documentURI.startsWith("about:reader"); + } + + get isReaderableAboutReader() { + return this.isAboutReader && !this.document.documentElement.dataset.isError; + } + + handleEvent(aEvent) { + if (aEvent.originalTarget.defaultView != this.contentWindow) { + return; + } + + switch (aEvent.type) { + case "DOMContentLoaded": + if (!this.isAboutReader) { + this.updateReaderButton(); + return; + } + + if (this.document.body) { + let url = this.document.documentURI; + if (!this._articlePromise) { + url = decodeURIComponent(url.substr("about:reader?url=".length)); + this._articlePromise = this.sendQuery("Reader:GetCachedArticle", { + url, + }); + } + // Update the toolbar icon to show the "reader active" icon. + this.sendAsyncMessage("Reader:UpdateReaderButton"); + let docContentType = + gUrlsToDocContentType.get(url) === "text/plain" + ? "text/plain" + : "document"; + + let docTitle = gUrlsToDocTitle.get(url); + this._reader = new lazy.AboutReader( + this, + this._articlePromise, + docContentType, + docTitle + ); + this._articlePromise = null; + } + break; + + case "pagehide": + this.cancelPotentialPendingReadabilityCheck(); + // this._isLeavingReaderableReaderMode is used here to keep the Reader Mode icon + // visible in the location bar when transitioning from reader-mode page + // back to the readable source page. + this.sendAsyncMessage("Reader:UpdateReaderButton", { + isArticle: this._isLeavingReaderableReaderMode, + }); + this._isLeavingReaderableReaderMode = false; + break; + + case "pageshow": + // If a page is loaded from the bfcache, we won't get a "DOMContentLoaded" + // event, so we need to rely on "pageshow" in this case. + if (aEvent.persisted && this.canDoReadabilityCheck()) { + this.performReadabilityCheckNow(); + } + break; + } + } + + /** + * NB: this function will update the state of the reader button asynchronously + * after the next mozAfterPaint call (assuming reader mode is enabled and + * this is a suitable document). Calling it on things which won't be + * painted is not going to work. + */ + updateReaderButton(forceNonArticle) { + if (!this.canDoReadabilityCheck()) { + return; + } + + this.scheduleReadabilityCheckPostPaint(forceNonArticle); + } + + canDoReadabilityCheck() { + return ( + lazy.Readerable.isEnabledForParseOnLoad && + !this.isAboutReader && + this.contentWindow && + this.contentWindow.windowRoot && + this.contentWindow.HTMLDocument.isInstance(this.document) && + !this.document.mozSyntheticDocument + ); + } + + cancelPotentialPendingReadabilityCheck() { + if (this._pendingReadabilityCheck) { + if (this._listenerWindow) { + this._listenerWindow.removeEventListener( + "MozAfterPaint", + this._pendingReadabilityCheck + ); + } + delete this._pendingReadabilityCheck; + delete this._listenerWindow; + } + } + + scheduleReadabilityCheckPostPaint(forceNonArticle) { + if (this._pendingReadabilityCheck) { + // We need to stop this check before we re-add one because we don't know + // if forceNonArticle was true or false last time. + this.cancelPotentialPendingReadabilityCheck(); + } + this._pendingReadabilityCheck = this.onPaintWhenWaitedFor.bind( + this, + forceNonArticle + ); + + this._listenerWindow = this.contentWindow.windowRoot; + this.contentWindow.windowRoot.addEventListener( + "MozAfterPaint", + this._pendingReadabilityCheck + ); + } + + onPaintWhenWaitedFor(forceNonArticle, event) { + // In non-e10s, we'll get called for paints other than ours, and so it's + // possible that this page hasn't been laid out yet, in which case we + // should wait until we get an event that does relate to our layout. We + // determine whether any of our this.contentWindow got painted by checking + // if there are any painted rects. + if (!event.clientRects.length) { + return; + } + + this.performReadabilityCheckNow(forceNonArticle); + } + + performReadabilityCheckNow(forceNonArticle) { + this.cancelPotentialPendingReadabilityCheck(); + + // Ignore errors from actors that have been unloaded before the + // paint event timer fires. + let document; + try { + document = this.document; + } catch (ex) { + return; + } + + // Only send updates when there are articles; there's no point updating with + // |false| all the time. + if ( + lazy.Readerable.shouldCheckUri(document.baseURIObject, true) && + lazy.Readerable.isProbablyReaderable(document) + ) { + this.sendAsyncMessage("Reader:UpdateReaderButton", { + isArticle: true, + }); + } else if (forceNonArticle) { + this.sendAsyncMessage("Reader:UpdateReaderButton", { + isArticle: false, + }); + } + } + + closeReaderMode() { + if (this.isAboutReader) { + this._isLeavingReaderableReaderMode = this.isReaderableAboutReader; + this.sendAsyncMessage("Reader:LeaveReaderMode", {}); + } + } +} diff --git a/browser/actors/AboutReaderParent.sys.mjs b/browser/actors/AboutReaderParent.sys.mjs new file mode 100644 index 0000000000..8b58638f9c --- /dev/null +++ b/browser/actors/AboutReaderParent.sys.mjs @@ -0,0 +1,331 @@ +// -*- 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, { + pktApi: "chrome://pocket/content/pktApi.sys.mjs", + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + ReaderMode: "resource://gre/modules/ReaderMode.sys.mjs", +}); + +// A set of all of the AboutReaderParent actors that exist. +// See bug 1631146 for a request for a less manual way of doing this. +let gAllActors = new Set(); + +// A map of message names to listeners that listen to messages +// received by the AboutReaderParent actors. +let gListeners = new Map(); + +// As a reader mode document could be loaded in a different process than +// the source article, temporarily cache the article data here in the +// parent while switching to it. +let gCachedArticles = new Map(); + +export class AboutReaderParent extends JSWindowActorParent { + didDestroy() { + gAllActors.delete(this); + + if (this.isReaderMode()) { + let url = this.manager.documentURI.spec; + url = decodeURIComponent(url.substr("about:reader?url=".length)); + gCachedArticles.delete(url); + } + } + + isReaderMode() { + return this.manager.documentURI.spec.startsWith("about:reader"); + } + + static addMessageListener(name, listener) { + if (!gListeners.has(name)) { + gListeners.set(name, new Set([listener])); + } else { + gListeners.get(name).add(listener); + } + } + + static removeMessageListener(name, listener) { + if (!gListeners.has(name)) { + return; + } + + gListeners.get(name).delete(listener); + } + + static broadcastAsyncMessage(name, data) { + for (let actor of gAllActors) { + // Ignore errors for actors that might not be valid yet or anymore. + try { + actor.sendAsyncMessage(name, data); + } catch (ex) {} + } + } + + callListeners(message) { + let listeners = gListeners.get(message.name); + if (!listeners) { + return; + } + + message.target = this.browsingContext.embedderElement; + for (let listener of listeners.values()) { + try { + listener.receiveMessage(message); + } catch (e) { + console.error(e); + } + } + } + + async receiveMessage(message) { + switch (message.name) { + case "Reader:EnterReaderMode": { + gCachedArticles.set(message.data.url, message.data); + this.enterReaderMode(message.data.url); + break; + } + case "Reader:LeaveReaderMode": { + this.leaveReaderMode(); + break; + } + case "Reader:GetCachedArticle": { + let cachedArticle = gCachedArticles.get(message.data.url); + gCachedArticles.delete(message.data.url); + return cachedArticle; + } + case "Reader:PocketLoginStatusRequest": { + return lazy.pktApi.isUserLoggedIn(); + } + case "Reader:PocketGetArticleInfo": { + return new Promise(resolve => { + lazy.pktApi.getArticleInfo(message.data.url, { + success: data => { + resolve(data); + }, + error: error => { + resolve(null); + }, + }); + }); + } + case "Reader:PocketGetArticleRecs": { + return new Promise(resolve => { + lazy.pktApi.getRecsForItem(message.data.itemID, { + success: data => { + resolve(data); + }, + error: error => { + resolve(null); + }, + }); + }); + } + case "Reader:PocketSaveArticle": { + return new Promise(resolve => { + lazy.pktApi.addLink(message.data.url, { + success: data => { + resolve(data); + }, + error: error => { + resolve(null); + }, + }); + }); + } + case "Reader:FaviconRequest": { + try { + let preferredWidth = message.data.preferredWidth || 0; + let uri = Services.io.newURI(message.data.url); + + let result = await new Promise(resolve => { + lazy.PlacesUtils.favicons.getFaviconURLForPage( + uri, + iconUri => { + if (iconUri) { + iconUri = + lazy.PlacesUtils.favicons.getFaviconLinkForIcon(iconUri); + resolve({ + url: message.data.url, + faviconUrl: iconUri.pathQueryRef.replace(/^favicon:/, ""), + }); + } else { + resolve(null); + } + }, + preferredWidth + ); + }); + + this.callListeners(message); + return result; + } catch (ex) { + console.error( + "Error requesting favicon URL for about:reader content: ", + ex + ); + } + + break; + } + + case "Reader:UpdateReaderButton": { + let browser = this.browsingContext.embedderElement; + if (!browser) { + return undefined; + } + + if (message.data && message.data.isArticle !== undefined) { + browser.isArticle = message.data.isArticle; + } + this.updateReaderButton(browser); + this.callListeners(message); + break; + } + + case "RedirectTo": { + gCachedArticles.set(message.data.newURL, message.data.article); + // This is setup as a query so we can navigate the page after we've + // cached the relevant info in the parent. + return true; + } + + default: + this.callListeners(message); + break; + } + + return undefined; + } + + static updateReaderButton(browser) { + let windowGlobal = browser.browsingContext.currentWindowGlobal; + let actor = windowGlobal.getActor("AboutReader"); + actor.updateReaderButton(browser); + } + + updateReaderButton(browser) { + let tabBrowser = browser.getTabBrowser(); + if (!tabBrowser || browser != tabBrowser.selectedBrowser) { + return; + } + + let doc = browser.ownerGlobal.document; + let button = doc.getElementById("reader-mode-button"); + let menuitem = doc.getElementById("menu_readerModeItem"); + let key = doc.getElementById("key_toggleReaderMode"); + if (this.isReaderMode()) { + gAllActors.add(this); + + button.setAttribute("readeractive", true); + button.hidden = false; + doc.l10n.setAttributes(button, "reader-view-close-button"); + + menuitem.hidden = false; + doc.l10n.setAttributes(menuitem, "menu-view-close-readerview"); + + key.setAttribute("disabled", false); + + Services.obs.notifyObservers(null, "reader-mode-available"); + } else { + button.removeAttribute("readeractive"); + button.hidden = !browser.isArticle; + doc.l10n.setAttributes(button, "reader-view-enter-button"); + + menuitem.hidden = !browser.isArticle; + doc.l10n.setAttributes(menuitem, "menu-view-enter-readerview"); + + key.setAttribute("disabled", !browser.isArticle); + + if (browser.isArticle) { + Services.obs.notifyObservers(null, "reader-mode-available"); + } + } + } + + static forceShowReaderIcon(browser) { + browser.isArticle = true; + AboutReaderParent.updateReaderButton(browser); + } + + static buttonClick(event) { + if (event.button != 0) { + return; + } + AboutReaderParent.toggleReaderMode(event); + } + + static toggleReaderMode(event) { + let win = event.target.ownerGlobal; + if (win.gBrowser) { + let browser = win.gBrowser.selectedBrowser; + + let windowGlobal = browser.browsingContext.currentWindowGlobal; + let actor = windowGlobal.getActor("AboutReader"); + if (actor) { + if (actor.isReaderMode()) { + gAllActors.delete(this); + } + actor.sendAsyncMessage("Reader:ToggleReaderMode", {}); + } + } + } + + hasReaderModeEntryAtOffset(url, offset) { + if (Services.appinfo.sessionHistoryInParent) { + let browsingContext = this.browsingContext; + if (browsingContext.childSessionHistory.canGo(offset)) { + let shistory = browsingContext.sessionHistory; + let nextEntry = shistory.getEntryAtIndex(shistory.index + offset); + let nextURL = nextEntry.URI.spec; + return nextURL && (nextURL == url || !url); + } + } + + return false; + } + + enterReaderMode(url) { + let readerURL = "about:reader?url=" + encodeURIComponent(url); + if (this.hasReaderModeEntryAtOffset(readerURL, +1)) { + let browsingContext = this.browsingContext; + browsingContext.childSessionHistory.go(+1); + return; + } + + this.sendAsyncMessage("Reader:EnterReaderMode", {}); + } + + leaveReaderMode() { + let browsingContext = this.browsingContext; + let url = browsingContext.currentWindowGlobal.documentURI.spec; + let originalURL = lazy.ReaderMode.getOriginalUrl(url); + if (this.hasReaderModeEntryAtOffset(originalURL, -1)) { + browsingContext.childSessionHistory.go(-1); + return; + } + + this.sendAsyncMessage("Reader:LeaveReaderMode", {}); + } + + /** + * Gets an article for a given URL. This method will download and parse a document. + * + * @param url The article URL. + * @param browser The browser where the article is currently loaded. + * @return {Promise} + * @resolves JS object representing the article, or null if no article is found. + */ + async _getArticle(url, browser) { + return lazy.ReaderMode.downloadAndParseDocument(url).catch(e => { + if (e && e.newURL) { + // Pass up the error so we can navigate the browser in question to the new URL: + throw e; + } + console.error("Error downloading and parsing document: ", e); + return null; + }); + } +} diff --git a/browser/actors/AboutTabCrashedChild.sys.mjs b/browser/actors/AboutTabCrashedChild.sys.mjs new file mode 100644 index 0000000000..52d633b90f --- /dev/null +++ b/browser/actors/AboutTabCrashedChild.sys.mjs @@ -0,0 +1,8 @@ +/* -*- 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/. */ + +import { RemotePageChild } from "resource://gre/actors/RemotePageChild.sys.mjs"; + +export class AboutTabCrashedChild extends RemotePageChild {} diff --git a/browser/actors/AboutTabCrashedParent.sys.mjs b/browser/actors/AboutTabCrashedParent.sys.mjs new file mode 100644 index 0000000000..4c11c3430d --- /dev/null +++ b/browser/actors/AboutTabCrashedParent.sys.mjs @@ -0,0 +1,92 @@ +/* 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, { + SessionStore: "resource:///modules/sessionstore/SessionStore.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm", +}); + +// A list of all of the open about:tabcrashed pages. +let gAboutTabCrashedPages = new Map(); + +export class AboutTabCrashedParent extends JSWindowActorParent { + didDestroy() { + this.removeCrashedPage(); + } + + async receiveMessage(message) { + let browser = this.browsingContext.top.embedderElement; + if (!browser) { + // If there is no browser, remove the crashed page from the set + // and return. + this.removeCrashedPage(); + return; + } + + let gBrowser = browser.getTabBrowser(); + let tab = gBrowser.getTabForBrowser(browser); + + switch (message.name) { + case "Load": { + gAboutTabCrashedPages.set(this, browser); + this.updateTabCrashedCount(); + + let report = lazy.TabCrashHandler.onAboutTabCrashedLoad(browser); + this.sendAsyncMessage("SetCrashReportAvailable", report); + break; + } + + case "closeTab": { + lazy.TabCrashHandler.maybeSendCrashReport(browser, message); + gBrowser.removeTab(tab, { animate: true }); + break; + } + + case "restoreTab": { + lazy.TabCrashHandler.maybeSendCrashReport(browser, message); + lazy.SessionStore.reviveCrashedTab(tab); + break; + } + + case "restoreAll": { + lazy.TabCrashHandler.maybeSendCrashReport(browser, message); + lazy.SessionStore.reviveAllCrashedTabs(); + break; + } + } + } + + removeCrashedPage() { + let browser = + this.browsingContext.top.embedderElement || + gAboutTabCrashedPages.get(this); + + gAboutTabCrashedPages.delete(this); + this.updateTabCrashedCount(); + + lazy.TabCrashHandler.onAboutTabCrashedUnload(browser); + } + + updateTabCrashedCount() { + // Broadcast to all about:tabcrashed pages a count of + // how many about:tabcrashed pages exist, so that they + // can decide whether or not to display the "Restore All + // Crashed Tabs" button. + let count = gAboutTabCrashedPages.size; + + for (let actor of gAboutTabCrashedPages.keys()) { + let browser = actor.browsingContext.top.embedderElement; + if (browser) { + browser.sendMessageToActor("UpdateCount", { count }, "AboutTabCrashed"); + } + } + } +} diff --git a/browser/actors/BlockedSiteChild.sys.mjs b/browser/actors/BlockedSiteChild.sys.mjs new file mode 100644 index 0000000000..cbe37c8688 --- /dev/null +++ b/browser/actors/BlockedSiteChild.sys.mjs @@ -0,0 +1,187 @@ +/* -*- 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, { + SafeBrowsing: "resource://gre/modules/SafeBrowsing.sys.mjs", +}); + +function getSiteBlockedErrorDetails(docShell) { + let blockedInfo = {}; + if (docShell.failedChannel) { + let classifiedChannel = docShell.failedChannel.QueryInterface( + Ci.nsIClassifiedChannel + ); + if (classifiedChannel) { + let httpChannel = docShell.failedChannel.QueryInterface( + Ci.nsIHttpChannel + ); + + let reportUri = httpChannel.URI; + + // Remove the query to avoid leaking sensitive data + if (reportUri instanceof Ci.nsIURL) { + reportUri = reportUri.mutate().setQuery("").finalize(); + } + + let triggeringPrincipal = docShell.failedChannel.loadInfo + ? docShell.failedChannel.loadInfo.triggeringPrincipal + : null; + blockedInfo = { + list: classifiedChannel.matchedList, + triggeringPrincipal, + provider: classifiedChannel.matchedProvider, + uri: reportUri.asciiSpec, + }; + } + } + return blockedInfo; +} + +export class BlockedSiteChild extends JSWindowActorChild { + receiveMessage(msg) { + if (msg.name == "DeceptiveBlockedDetails") { + return getSiteBlockedErrorDetails(this.docShell); + } + return null; + } + + handleEvent(event) { + if (event.type == "AboutBlockedLoaded") { + this.onAboutBlockedLoaded(event); + } else if (event.type == "click" && event.button == 0) { + this.onClick(event); + } + } + + onAboutBlockedLoaded(aEvent) { + let content = aEvent.target.ownerGlobal; + + let blockedInfo = getSiteBlockedErrorDetails(this.docShell); + let provider = blockedInfo.provider || ""; + + let doc = content.document; + + /** + * Set error description link in error details. + * For example, the "reported as a deceptive site" link for + * blocked phishing pages. + */ + let desc = Services.prefs.getCharPref( + "browser.safebrowsing.provider." + provider + ".reportURL", + "" + ); + if (desc) { + doc + .getElementById("error_desc_link") + .setAttribute("href", desc + encodeURIComponent(aEvent.detail.url)); + } + + // Set other links in error details. + switch (aEvent.detail.err) { + case "malware": + doc + .getElementById("report_detection") + .setAttribute( + "href", + lazy.SafeBrowsing.getReportURL("MalwareMistake", blockedInfo) + ); + break; + case "unwanted": + doc + .getElementById("learn_more_link") + .setAttribute( + "href", + "https://www.google.com/about/unwanted-software-policy.html" + ); + break; + case "phishing": + doc + .getElementById("report_detection") + .setAttribute( + "href", + lazy.SafeBrowsing.getReportURL("PhishMistake", blockedInfo) || + "https://safebrowsing.google.com/safebrowsing/report_error/?tpl=mozilla" + ); + doc + .getElementById("learn_more_link") + .setAttribute("href", "https://www.antiphishing.org//"); + break; + } + + // Set the firefox support url. + doc + .getElementById("firefox_support") + .setAttribute( + "href", + Services.urlFormatter.formatURLPref("app.support.baseURL") + + "phishing-malware" + ); + + // Show safe browsing details on load if the pref is set to true. + let showDetails = Services.prefs.getBoolPref( + "browser.xul.error_pages.show_safe_browsing_details_on_load" + ); + if (showDetails) { + let details = content.document.getElementById( + "errorDescriptionContainer" + ); + details.removeAttribute("hidden"); + } + + // Set safe browsing advisory link. + let advisoryUrl = Services.prefs.getCharPref( + "browser.safebrowsing.provider." + provider + ".advisoryURL", + "" + ); + let advisoryDesc = content.document.getElementById("advisoryDescText"); + if (!advisoryUrl) { + advisoryDesc.remove(); + return; + } + + let advisoryLinkText = Services.prefs.getCharPref( + "browser.safebrowsing.provider." + provider + ".advisoryName", + "" + ); + if (!advisoryLinkText) { + advisoryDesc.remove(); + return; + } + + content.document.l10n.setAttributes( + advisoryDesc, + "safeb-palm-advisory-desc", + { advisoryname: advisoryLinkText } + ); + content.document + .getElementById("advisory_provider") + .setAttribute("href", advisoryUrl); + } + + onClick(event) { + let ownerDoc = event.target.ownerDocument; + if (!ownerDoc) { + return; + } + + var reason = "phishing"; + if (/e=malwareBlocked/.test(ownerDoc.documentURI)) { + reason = "malware"; + } else if (/e=unwantedBlocked/.test(ownerDoc.documentURI)) { + reason = "unwanted"; + } else if (/e=harmfulBlocked/.test(ownerDoc.documentURI)) { + reason = "harmful"; + } + + this.sendAsyncMessage("Browser:SiteBlockedError", { + location: ownerDoc.location.href, + reason, + elementId: event.target.getAttribute("id"), + blockedInfo: getSiteBlockedErrorDetails(this.docShell), + }); + } +} diff --git a/browser/actors/BlockedSiteParent.sys.mjs b/browser/actors/BlockedSiteParent.sys.mjs new file mode 100644 index 0000000000..25d90ce2be --- /dev/null +++ b/browser/actors/BlockedSiteParent.sys.mjs @@ -0,0 +1,70 @@ +/* -*- 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/. */ + +export class BlockedSiteParent extends JSWindowActorParent { + receiveMessage(msg) { + switch (msg.name) { + case "Browser:SiteBlockedError": + this._onAboutBlocked( + msg.data.elementId, + msg.data.reason, + this.browsingContext === this.browsingContext.top, + msg.data.blockedInfo + ); + break; + } + } + + _onAboutBlocked(elementId, reason, isTopFrame, blockedInfo) { + let browser = this.browsingContext.top.embedderElement; + if (!browser) { + return; + } + let { BrowserOnClick } = browser.ownerGlobal; + // Depending on what page we are displaying here (malware/phishing/unwanted) + // use the right strings and links for each. + let bucketName = ""; + let sendTelemetry = false; + if (reason === "malware") { + sendTelemetry = true; + bucketName = "WARNING_MALWARE_PAGE_"; + } else if (reason === "phishing") { + sendTelemetry = true; + bucketName = "WARNING_PHISHING_PAGE_"; + } else if (reason === "unwanted") { + sendTelemetry = true; + bucketName = "WARNING_UNWANTED_PAGE_"; + } else if (reason === "harmful") { + sendTelemetry = true; + bucketName = "WARNING_HARMFUL_PAGE_"; + } + let secHistogram = Services.telemetry.getHistogramById( + "URLCLASSIFIER_UI_EVENTS" + ); + let nsISecTel = Ci.IUrlClassifierUITelemetry; + bucketName += isTopFrame ? "TOP_" : "FRAME_"; + + switch (elementId) { + case "goBackButton": + if (sendTelemetry) { + secHistogram.add(nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]); + } + browser.ownerGlobal.getMeOutOfHere(this.browsingContext); + break; + case "ignore_warning_link": + if (Services.prefs.getBoolPref("browser.safebrowsing.allowOverride")) { + if (sendTelemetry) { + secHistogram.add(nsISecTel[bucketName + "IGNORE_WARNING"]); + } + BrowserOnClick.ignoreWarningLink( + reason, + blockedInfo, + this.browsingContext + ); + } + break; + } + } +} diff --git a/browser/actors/BrowserProcessChild.sys.mjs b/browser/actors/BrowserProcessChild.sys.mjs new file mode 100644 index 0000000000..99b45a7d2a --- /dev/null +++ b/browser/actors/BrowserProcessChild.sys.mjs @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + WebRTCChild: "resource:///actors/WebRTCChild.sys.mjs", +}); + +ChromeUtils.defineModuleGetter( + lazy, + "AboutHomeStartupCacheChild", + "resource:///modules/AboutNewTabService.jsm" +); + +export class BrowserProcessChild extends JSProcessActorChild { + receiveMessage(message) { + switch (message.name) { + case "AboutHomeStartupCache:InputStreams": + let { pageInputStream, scriptInputStream } = message.data; + lazy.AboutHomeStartupCacheChild.init( + pageInputStream, + scriptInputStream + ); + break; + } + } + + observe(subject, topic, data) { + switch (topic) { + case "getUserMedia:request": + case "recording-device-stopped": + case "PeerConnection:request": + case "recording-device-events": + case "recording-window-ended": + lazy.WebRTCChild.observe(subject, topic, data); + break; + } + } +} diff --git a/browser/actors/BrowserTabChild.sys.mjs b/browser/actors/BrowserTabChild.sys.mjs new file mode 100644 index 0000000000..04c6e2f17a --- /dev/null +++ b/browser/actors/BrowserTabChild.sys.mjs @@ -0,0 +1,55 @@ +/* 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, { + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", +}); + +export class BrowserTabChild extends JSWindowActorChild { + constructor() { + super(); + } + + receiveMessage(message) { + let context = this.manager.browsingContext; + let docShell = context.docShell; + + switch (message.name) { + // XXX(nika): Should we try to call this in the parent process instead? + case "Browser:Reload": + /* First, we'll try to use the session history object to reload so + * that framesets are handled properly. If we're in a special + * window (such as view-source) that has no session history, fall + * back on using the web navigation's reload method. + */ + let webNav = docShell.QueryInterface(Ci.nsIWebNavigation); + try { + if (webNav.sessionHistory) { + webNav = webNav.sessionHistory; + } + } catch (e) {} + + let reloadFlags = message.data.flags; + if (message.data.handlingUserInput) { + reloadFlags |= Ci.nsIWebNavigation.LOAD_FLAGS_USER_ACTIVATION; + } + + try { + lazy.E10SUtils.wrapHandlingUserInput( + this.document.defaultView, + message.data.handlingUserInput, + () => webNav.reload(reloadFlags) + ); + } catch (e) {} + break; + + case "ForceEncodingDetection": + docShell.forceEncodingDetection(); + break; + } + } +} diff --git a/browser/actors/ClickHandlerChild.sys.mjs b/browser/actors/ClickHandlerChild.sys.mjs new file mode 100644 index 0000000000..2fa0a87df4 --- /dev/null +++ b/browser/actors/ClickHandlerChild.sys.mjs @@ -0,0 +1,174 @@ +/* -*- 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/. */ + +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs", + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", +}); + +export class MiddleMousePasteHandlerChild extends JSWindowActorChild { + handleEvent(clickEvent) { + if ( + clickEvent.defaultPrevented || + clickEvent.button != 1 || + MiddleMousePasteHandlerChild.autoscrollEnabled + ) { + return; + } + this.manager + .getActor("ClickHandler") + .handleClickEvent( + clickEvent, + /* is from middle mouse paste handler */ true + ); + } + + onProcessedClick(data) { + this.sendAsyncMessage("MiddleClickPaste", data); + } +} + +XPCOMUtils.defineLazyPreferenceGetter( + MiddleMousePasteHandlerChild, + "autoscrollEnabled", + "general.autoScroll", + true +); + +export class ClickHandlerChild extends JSWindowActorChild { + handleEvent(wrapperEvent) { + this.handleClickEvent(wrapperEvent.sourceEvent); + } + + handleClickEvent(event, isFromMiddleMousePasteHandler = false) { + if (event.defaultPrevented || event.button == 2) { + return; + } + // Don't do anything on editable things, we shouldn't open links in + // contenteditables, and editor needs to possibly handle middlemouse paste + let composedTarget = event.composedTarget; + if ( + composedTarget.isContentEditable || + (composedTarget.ownerDocument && + composedTarget.ownerDocument.designMode == "on") || + ChromeUtils.getClassName(composedTarget) == "HTMLInputElement" || + ChromeUtils.getClassName(composedTarget) == "HTMLTextAreaElement" + ) { + return; + } + + let originalTarget = event.originalTarget; + let ownerDoc = originalTarget.ownerDocument; + if (!ownerDoc) { + return; + } + + // Handle click events from about pages + if (event.button == 0) { + if (ownerDoc.documentURI.startsWith("about:blocked")) { + return; + } + } + + // For untrusted events, require a valid transient user gesture activation. + if (!event.isTrusted && !ownerDoc.hasValidTransientUserGestureActivation) { + return; + } + + let [href, node, principal] = + lazy.BrowserUtils.hrefAndLinkNodeForClickEvent(event); + + let csp = ownerDoc.csp; + if (csp) { + csp = lazy.E10SUtils.serializeCSP(csp); + } + + let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance( + Ci.nsIReferrerInfo + ); + if (node) { + referrerInfo.initWithElement(node); + } else { + referrerInfo.initWithDocument(ownerDoc); + } + referrerInfo = lazy.E10SUtils.serializeReferrerInfo(referrerInfo); + + let json = { + button: event.button, + shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, + metaKey: event.metaKey, + altKey: event.altKey, + href: null, + title: null, + csp, + referrerInfo, + }; + + if (href && !isFromMiddleMousePasteHandler) { + try { + Services.scriptSecurityManager.checkLoadURIStrWithPrincipal( + principal, + href + ); + } catch (e) { + return; + } + + if ( + !event.isTrusted && + lazy.BrowserUtils.whereToOpenLink(event) != "current" + ) { + // If we'll open the link, we want to consume the user gesture + // activation to ensure that we don't allow multiple links to open + // from one user gesture. + // Avoid doing so for links opened in the current tab, which get + // handled later, by gecko, as otherwise its popup blocker will stop + // the link from opening. + // We will do the same check (whereToOpenLink) again in the parent and + // avoid handling the click for such links... but we still need the + // click information in the parent because otherwise places link + // tracking breaks. (bug 1742894 tracks improving this.) + ownerDoc.consumeTransientUserGestureActivation(); + // We don't care about the return value because we already checked that + // hasValidTransientUserGestureActivation was true earlier in this + // function. + } + + json.href = href; + if (node) { + json.title = node.getAttribute("title"); + } + + if ( + (ownerDoc.URL === "about:newtab" || ownerDoc.URL === "about:home") && + node.dataset.isSponsoredLink === "true" + ) { + json.globalHistoryOptions = { triggeringSponsoredURL: href }; + } + + // If a link element is clicked with middle button, user wants to open + // the link somewhere rather than pasting clipboard content. Therefore, + // when it's clicked with middle button, we should prevent multiple + // actions here to avoid leaking clipboard content unexpectedly. + // Note that whether the link will work actually or not does not matter + // because in this case, user does not intent to paste clipboard content. + // We also need to do this to prevent multiple tabs opening if there are + // nested link elements. + event.preventMultipleActions(); + + this.sendAsyncMessage("Content:Click", json); + } + + // This might be middle mouse navigation, in which case pass this back: + if (!href && event.button == 1 && isFromMiddleMousePasteHandler) { + this.manager.getActor("MiddleMousePasteHandler").onProcessedClick(json); + } + } +} diff --git a/browser/actors/ClickHandlerParent.sys.mjs b/browser/actors/ClickHandlerParent.sys.mjs new file mode 100644 index 0000000000..4078c6404f --- /dev/null +++ b/browser/actors/ClickHandlerParent.sys.mjs @@ -0,0 +1,156 @@ +/* -*- mode: js; 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, { + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", + PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + WebNavigationFrames: "resource://gre/modules/WebNavigationFrames.sys.mjs", +}); + +let gContentClickListeners = new Set(); + +// Fill in fields which are not sent by the content process for the click event +// based on known data in the parent process. +function fillInClickEvent(actor, data) { + const wgp = actor.manager; + data.frameID = lazy.WebNavigationFrames.getFrameId(wgp.browsingContext); + data.triggeringPrincipal = wgp.documentPrincipal; + data.originPrincipal = wgp.documentPrincipal; + data.originStoragePrincipal = wgp.documentStoragePrincipal; + data.originAttributes = wgp.documentPrincipal?.originAttributes ?? {}; + data.isContentWindowPrivate = wgp.browsingContext.usePrivateBrowsing; +} + +export class MiddleMousePasteHandlerParent extends JSWindowActorParent { + receiveMessage(message) { + if (message.name == "MiddleClickPaste") { + // This is heavily based on contentAreaClick from browser.js (Bug 903016) + // The data is set up in a way to look like an Event. + let browser = this.manager.browsingContext.top.embedderElement; + if (!browser) { + // Can be null if the tab disappeared by the time we got the message. + // Just bail. + return; + } + fillInClickEvent(this, message.data); + browser.ownerGlobal.middleMousePaste(message.data); + } + } +} + +export class ClickHandlerParent extends JSWindowActorParent { + static addContentClickListener(listener) { + gContentClickListeners.add(listener); + } + + static removeContentClickListener(listener) { + gContentClickListeners.delete(listener); + } + + receiveMessage(message) { + switch (message.name) { + case "Content:Click": + fillInClickEvent(this, message.data); + this.contentAreaClick(message.data); + this.notifyClickListeners(message.data); + break; + } + } + + /** + * Handles clicks in the content area. + * + * @param data {Object} object that looks like an Event + * @param browser {Element} + */ + contentAreaClick(data) { + // This is heavily based on contentAreaClick from browser.js (Bug 903016) + // The data is set up in a way to look like an Event. + let browser = this.manager.browsingContext.top.embedderElement; + if (!browser) { + // Can be null if the tab disappeared by the time we got the message. + // Just bail. + return; + } + let window = browser.ownerGlobal; + + // If the browser is not in a place where we can open links, bail out. + // This can happen in osx sheets, dialogs, etc. that are not browser + // windows. Specifically the payments UI is in an osx sheet. + if (window.openLinkIn === undefined) { + return; + } + + // Mark the page as a user followed link. This is done so that history can + // distinguish automatic embed visits from user activated ones. For example + // pages loaded in frames are embed visits and lost with the session, while + // visits across frames should be preserved. + try { + if (!lazy.PrivateBrowsingUtils.isWindowPrivate(window)) { + lazy.PlacesUIUtils.markPageAsFollowedLink(data.href); + } + } catch (ex) { + /* Skip invalid URIs. */ + } + + // This part is based on handleLinkClick. + var where = window.whereToOpenLink(data); + if (where == "current") { + return; + } + + // Todo(903022): code for where == save + + let params = { + charset: browser.characterSet, + referrerInfo: lazy.E10SUtils.deserializeReferrerInfo(data.referrerInfo), + isContentWindowPrivate: data.isContentWindowPrivate, + originPrincipal: data.originPrincipal, + originStoragePrincipal: data.originStoragePrincipal, + triggeringPrincipal: data.triggeringPrincipal, + csp: data.csp ? lazy.E10SUtils.deserializeCSP(data.csp) : null, + frameID: data.frameID, + openerBrowser: browser, + // The child ensures that untrusted events have a valid user activation. + hasValidUserGestureActivation: true, + triggeringRemoteType: this.manager.domProcess?.remoteType, + }; + + if (data.globalHistoryOptions) { + params.globalHistoryOptions = data.globalHistoryOptions; + } else { + params.globalHistoryOptions = { + triggeringSponsoredURL: browser.getAttribute("triggeringSponsoredURL"), + triggeringSponsoredURLVisitTimeMS: browser.getAttribute( + "triggeringSponsoredURLVisitTimeMS" + ), + }; + } + + // The new tab/window must use the same userContextId. + if (data.originAttributes.userContextId) { + params.userContextId = data.originAttributes.userContextId; + } + + params.allowInheritPrincipal = true; + + window.openLinkIn(data.href, where, params); + } + + notifyClickListeners(data) { + for (let listener of gContentClickListeners) { + try { + let browser = this.browsingContext.top.embedderElement; + + listener.onContentClick(browser, data); + } catch (ex) { + console.error(ex); + } + } + } +} diff --git a/browser/actors/ContentSearchChild.sys.mjs b/browser/actors/ContentSearchChild.sys.mjs new file mode 100644 index 0000000000..79cca36bae --- /dev/null +++ b/browser/actors/ContentSearchChild.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 ContentSearchChild extends JSWindowActorChild { + handleEvent(event) { + // The event gets translated into a message that + // is then sent to the parent. + if (event.type == "ContentSearchClient") { + this.sendAsyncMessage(event.detail.type, event.detail.data); + } + } + + receiveMessage(msg) { + // The message gets translated into an event that + // is then sent to the content. + this._fireEvent(msg.name, msg.data); + } + + _fireEvent(type, data = null) { + let event = Cu.cloneInto( + { + detail: { + type, + data, + }, + }, + this.contentWindow + ); + this.contentWindow.dispatchEvent( + new this.contentWindow.CustomEvent("ContentSearchService", event) + ); + } +} diff --git a/browser/actors/ContentSearchParent.sys.mjs b/browser/actors/ContentSearchParent.sys.mjs new file mode 100644 index 0000000000..b7030d6c8c --- /dev/null +++ b/browser/actors/ContentSearchParent.sys.mjs @@ -0,0 +1,671 @@ +/* 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, { + BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.sys.mjs", + FormHistory: "resource://gre/modules/FormHistory.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", + SearchSuggestionController: + "resource://gre/modules/SearchSuggestionController.sys.mjs", + UrlbarPrefs: "resource:///modules/UrlbarPrefs.sys.mjs", +}); + +const MAX_LOCAL_SUGGESTIONS = 3; +const MAX_SUGGESTIONS = 6; +const SEARCH_ENGINE_PLACEHOLDER_ICON = + "chrome://browser/skin/search-engine-placeholder.png"; + +// Set of all ContentSearch actors, used to broadcast messages to all of them. +let gContentSearchActors = new Set(); + +/** + * Inbound messages have the following types: + * + * AddFormHistoryEntry + * Adds an entry to the search form history. + * data: the entry, a string + * GetSuggestions + * Retrieves an array of search suggestions given a search string. + * data: { engineName, searchString } + * GetState + * Retrieves the current search engine state. + * data: null + * GetStrings + * Retrieves localized search UI strings. + * data: null + * ManageEngines + * Opens the search engine management window. + * data: null + * RemoveFormHistoryEntry + * Removes an entry from the search form history. + * data: the entry, a string + * Search + * Performs a search. + * Any GetSuggestions messages in the queue from the same target will be + * cancelled. + * data: { engineName, searchString, healthReportKey, searchPurpose } + * SetCurrentEngine + * Sets the current engine. + * data: the name of the engine + * SpeculativeConnect + * Speculatively connects to an engine. + * data: the name of the engine + * + * Outbound messages have the following types: + * + * CurrentEngine + * Broadcast when the current engine changes. + * data: see _currentEngineObj + * CurrentState + * Broadcast when the current search state changes. + * data: see currentStateObj + * State + * Sent in reply to GetState. + * data: see currentStateObj + * Strings + * Sent in reply to GetStrings + * data: Object containing string names and values for the current locale. + * Suggestions + * Sent in reply to GetSuggestions. + * data: see _onMessageGetSuggestions + * SuggestionsCancelled + * Sent in reply to GetSuggestions when pending GetSuggestions events are + * cancelled. + * data: null + */ + +export let ContentSearch = { + initialized: false, + + // Inbound events are queued and processed in FIFO order instead of handling + // them immediately, which would result in non-FIFO responses due to the + // asynchrononicity added by converting image data URIs to ArrayBuffers. + _eventQueue: [], + _currentEventPromise: null, + + // This is used to handle search suggestions. It maps xul:browsers to objects + // { controller, previousFormHistoryResults }. See _onMessageGetSuggestions. + _suggestionMap: new WeakMap(), + + // Resolved when we finish shutting down. + _destroyedPromise: null, + + // The current controller and browser in _onMessageGetSuggestions. Allows + // fetch cancellation from _cancelSuggestions. + _currentSuggestion: null, + + init() { + if (!this.initialized) { + Services.obs.addObserver(this, "browser-search-engine-modified"); + Services.obs.addObserver(this, "browser-search-service"); + Services.obs.addObserver(this, "shutdown-leaks-before-check"); + Services.prefs.addObserver("browser.search.hiddenOneOffs", this); + lazy.UrlbarPrefs.addObserver(this); + + this.initialized = true; + } + }, + + get searchSuggestionUIStrings() { + if (this._searchSuggestionUIStrings) { + return this._searchSuggestionUIStrings; + } + this._searchSuggestionUIStrings = {}; + let searchBundle = Services.strings.createBundle( + "chrome://browser/locale/search.properties" + ); + let stringNames = [ + "searchHeader", + "searchForSomethingWith2", + "searchWithHeader", + "searchSettings", + ]; + + for (let name of stringNames) { + this._searchSuggestionUIStrings[name] = + searchBundle.GetStringFromName(name); + } + return this._searchSuggestionUIStrings; + }, + + destroy() { + if (!this.initialized) { + return new Promise(); + } + + if (this._destroyedPromise) { + return this._destroyedPromise; + } + + Services.prefs.removeObserver("browser.search.hiddenOneOffs", this); + Services.obs.removeObserver(this, "browser-search-engine-modified"); + Services.obs.removeObserver(this, "browser-search-service"); + Services.obs.removeObserver(this, "shutdown-leaks-before-check"); + + this._eventQueue.length = 0; + this._destroyedPromise = Promise.resolve(this._currentEventPromise); + return this._destroyedPromise; + }, + + observe(subj, topic, data) { + switch (topic) { + case "browser-search-service": + if (data != "init-complete") { + break; + } + // fall through + case "nsPref:changed": + case "browser-search-engine-modified": + this._eventQueue.push({ + type: "Observe", + data, + }); + this._processEventQueue(); + break; + case "shutdown-leaks-before-check": + subj.wrappedJSObject.client.addBlocker( + "ContentSearch: Wait until the service is destroyed", + () => this.destroy() + ); + break; + } + }, + + /** + * Observes changes in prefs tracked by UrlbarPrefs. + * @param {string} pref + * The name of the pref, relative to `browser.urlbar.` if the pref is + * in that branch. + */ + onPrefChanged(pref) { + if (lazy.UrlbarPrefs.shouldHandOffToSearchModePrefs.includes(pref)) { + this._eventQueue.push({ + type: "Observe", + data: "shouldHandOffToSearchMode", + }); + this._processEventQueue(); + } + }, + + removeFormHistoryEntry(browser, entry) { + let browserData = this._suggestionDataForBrowser(browser); + if (browserData?.previousFormHistoryResults) { + let result = browserData.previousFormHistoryResults.find( + e => e.text == entry + ); + lazy.FormHistory.update({ + op: "remove", + fieldname: browserData.controller.formHistoryParam, + value: entry, + guid: result.guid, + }).catch(err => + console.error("Error removing form history entry: ", err) + ); + } + }, + + performSearch(actor, browser, data) { + this._ensureDataHasProperties(data, [ + "engineName", + "searchString", + "healthReportKey", + "searchPurpose", + ]); + let engine = Services.search.getEngineByName(data.engineName); + let submission = engine.getSubmission( + data.searchString, + "", + data.searchPurpose + ); + let win = browser.ownerGlobal; + if (!win) { + // The browser may have been closed between the time its content sent the + // message and the time we handle it. + return; + } + let where = win.whereToOpenLink(data.originalEvent); + + // There is a chance that by the time we receive the search message, the user + // has switched away from the tab that triggered the search. If, based on the + // event, we need to load the search in the same tab that triggered it (i.e. + // where === "current"), openUILinkIn will not work because that tab is no + // longer the current one. For this case we manually load the URI. + if (where === "current") { + // Since we're going to load the search in the same browser, blur the search + // UI to prevent further interaction before we start loading. + this._reply(actor, "Blur"); + browser.loadURI(submission.uri, { + postData: submission.postData, + triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( + { + userContextId: + win.gBrowser.selectedBrowser.getAttribute("userContextId"), + } + ), + }); + } else { + let params = { + postData: submission.postData, + inBackground: Services.prefs.getBoolPref( + "browser.tabs.loadInBackground" + ), + }; + win.openTrustedLinkIn(submission.uri.spec, where, params); + } + lazy.BrowserSearchTelemetry.recordSearch( + browser, + engine, + data.healthReportKey, + { + selection: data.selection, + url: submission.uri, + } + ); + }, + + async getSuggestions(engineName, searchString, browser) { + let engine = Services.search.getEngineByName(engineName); + if (!engine) { + throw new Error("Unknown engine name: " + engineName); + } + + let browserData = this._suggestionDataForBrowser(browser, true); + let { controller } = browserData; + let ok = lazy.SearchSuggestionController.engineOffersSuggestions(engine); + controller.maxLocalResults = ok ? MAX_LOCAL_SUGGESTIONS : MAX_SUGGESTIONS; + controller.maxRemoteResults = ok ? MAX_SUGGESTIONS : 0; + let priv = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser); + // fetch() rejects its promise if there's a pending request, but since we + // process our event queue serially, there's never a pending request. + this._currentSuggestion = { controller, browser }; + let suggestions = await controller.fetch(searchString, priv, engine); + + // Simplify results since we do not support rich results in this component. + suggestions.local = suggestions.local.map(e => e.value); + // We shouldn't show tail suggestions in their full-text form. + let nonTailEntries = suggestions.remote.filter( + e => !e.matchPrefix && !e.tail + ); + suggestions.remote = nonTailEntries.map(e => e.value); + + this._currentSuggestion = null; + + // suggestions will be null if the request was cancelled + let result = {}; + if (!suggestions) { + return result; + } + + // Keep the form history results so RemoveFormHistoryEntry can remove entries + // from it. Keeping only one result isn't foolproof because the client may + // try to remove an entry from one set of suggestions after it has requested + // more but before it's received them. In that case, the entry may not + // appear in the new suggestions. But that should happen rarely. + browserData.previousFormHistoryResults = suggestions.formHistoryResults; + result = { + engineName, + term: suggestions.term, + local: suggestions.local, + remote: suggestions.remote, + }; + return result; + }, + + async addFormHistoryEntry(browser, entry = null) { + let isPrivate = false; + try { + // isBrowserPrivate assumes that the passed-in browser has all the normal + // properties, which won't be true if the browser has been destroyed. + // That may be the case here due to the asynchronous nature of messaging. + isPrivate = lazy.PrivateBrowsingUtils.isBrowserPrivate(browser); + } catch (err) { + return false; + } + if ( + isPrivate || + !entry || + entry.value.length > + lazy.SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + ) { + return false; + } + let browserData = this._suggestionDataForBrowser(browser, true); + lazy.FormHistory.update({ + op: "bump", + fieldname: browserData.controller.formHistoryParam, + value: entry.value, + source: entry.engineName, + }).catch(err => console.error("Error adding form history entry: ", err)); + return true; + }, + + async currentStateObj(window) { + let state = { + engines: [], + currentEngine: await this._currentEngineObj(false), + currentPrivateEngine: await this._currentEngineObj(true), + }; + + let pref = Services.prefs.getStringPref("browser.search.hiddenOneOffs"); + let hiddenList = pref ? pref.split(",") : []; + for (let engine of await Services.search.getVisibleEngines()) { + state.engines.push({ + name: engine.name, + iconData: await this._getEngineIconURL(engine), + hidden: hiddenList.includes(engine.name), + isAppProvided: engine.isAppProvided, + }); + } + + if (window) { + state.isInPrivateBrowsingMode = + lazy.PrivateBrowsingUtils.isContentWindowPrivate(window); + state.isAboutPrivateBrowsing = + window.gBrowser.currentURI.spec == "about:privatebrowsing"; + } + + return state; + }, + + _processEventQueue() { + if (this._currentEventPromise || !this._eventQueue.length) { + return; + } + + let event = this._eventQueue.shift(); + + this._currentEventPromise = (async () => { + try { + await this["_on" + event.type](event); + } catch (err) { + console.error(err); + } finally { + this._currentEventPromise = null; + + this._processEventQueue(); + } + })(); + }, + + _cancelSuggestions({ actor, browser }) { + let cancelled = false; + // cancel active suggestion request + if ( + this._currentSuggestion && + this._currentSuggestion.browser === browser + ) { + this._currentSuggestion.controller.stop(); + cancelled = true; + } + // cancel queued suggestion requests + for (let i = 0; i < this._eventQueue.length; i++) { + let m = this._eventQueue[i]; + if (actor === m.actor && m.name === "GetSuggestions") { + this._eventQueue.splice(i, 1); + cancelled = true; + i--; + } + } + if (cancelled) { + this._reply(actor, "SuggestionsCancelled"); + } + }, + + async _onMessage(eventItem) { + let methodName = "_onMessage" + eventItem.name; + if (methodName in this) { + await this._initService(); + await this[methodName](eventItem); + eventItem.browser.removeEventListener("SwapDocShells", eventItem, true); + } + }, + + _onMessageGetState({ actor, browser }) { + return this.currentStateObj(browser.ownerGlobal).then(state => { + this._reply(actor, "State", state); + }); + }, + + _onMessageGetEngine({ actor, browser }) { + return this.currentStateObj(browser.ownerGlobal).then(state => { + this._reply(actor, "Engine", { + isPrivateEngine: state.isInPrivateBrowsingMode, + isAboutPrivateBrowsing: state.isAboutPrivateBrowsing, + engine: state.isInPrivateBrowsingMode + ? state.currentPrivateEngine + : state.currentEngine, + }); + }); + }, + + _onMessageGetHandoffSearchModePrefs({ actor }) { + this._reply( + actor, + "HandoffSearchModePrefs", + lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") + ); + }, + + _onMessageGetStrings({ actor }) { + this._reply(actor, "Strings", this.searchSuggestionUIStrings); + }, + + _onMessageSearch({ actor, browser, data }) { + this.performSearch(actor, browser, data); + }, + + _onMessageSetCurrentEngine({ data }) { + Services.search.setDefault( + Services.search.getEngineByName(data), + Ci.nsISearchService.CHANGE_REASON_USER_SEARCHBAR + ); + }, + + _onMessageManageEngines({ browser }) { + browser.ownerGlobal.openPreferences("paneSearch"); + }, + + async _onMessageGetSuggestions({ actor, browser, data }) { + this._ensureDataHasProperties(data, ["engineName", "searchString"]); + let { engineName, searchString } = data; + let suggestions = await this.getSuggestions( + engineName, + searchString, + browser + ); + + this._reply(actor, "Suggestions", { + engineName: data.engineName, + searchString: suggestions.term, + formHistory: suggestions.local, + remote: suggestions.remote, + }); + }, + + async _onMessageAddFormHistoryEntry({ browser, data: entry }) { + await this.addFormHistoryEntry(browser, entry); + }, + + _onMessageRemoveFormHistoryEntry({ browser, data: entry }) { + this.removeFormHistoryEntry(browser, entry); + }, + + _onMessageSpeculativeConnect({ browser, data: engineName }) { + let engine = Services.search.getEngineByName(engineName); + if (!engine) { + throw new Error("Unknown engine name: " + engineName); + } + if (browser.contentWindow) { + engine.speculativeConnect({ + window: browser.contentWindow, + originAttributes: browser.contentPrincipal.originAttributes, + }); + } + }, + + async _onObserve(eventItem) { + let engine; + switch (eventItem.data) { + case "engine-default": + engine = await this._currentEngineObj(false); + this._broadcast("CurrentEngine", engine); + break; + case "engine-default-private": + engine = await this._currentEngineObj(true); + this._broadcast("CurrentPrivateEngine", engine); + break; + case "shouldHandOffToSearchMode": + this._broadcast( + "HandoffSearchModePrefs", + lazy.UrlbarPrefs.get("shouldHandOffToSearchMode") + ); + break; + default: + let state = await this.currentStateObj(); + this._broadcast("CurrentState", state); + break; + } + }, + + _suggestionDataForBrowser(browser, create = false) { + let data = this._suggestionMap.get(browser); + if (!data && create) { + // Since one SearchSuggestionController instance is meant to be used per + // autocomplete widget, this means that we assume each xul:browser has at + // most one such widget. + data = { + controller: new lazy.SearchSuggestionController(), + }; + this._suggestionMap.set(browser, data); + } + return data; + }, + + _reply(actor, type, data) { + actor.sendAsyncMessage(type, data); + }, + + _broadcast(type, data) { + for (let actor of gContentSearchActors) { + actor.sendAsyncMessage(type, data); + } + }, + + async _currentEngineObj(usePrivate) { + let engine = + Services.search[usePrivate ? "defaultPrivateEngine" : "defaultEngine"]; + let obj = { + name: engine.name, + iconData: await this._getEngineIconURL(engine), + isAppProvided: engine.isAppProvided, + }; + return obj; + }, + + /** + * Converts the engine's icon into an appropriate URL for display at + */ + async _getEngineIconURL(engine) { + let url = engine.getIconURLBySize(16, 16); + if (!url) { + return SEARCH_ENGINE_PLACEHOLDER_ICON; + } + + // The uri received here can be of two types + // 1 - moz-extension://[uuid]/path/to/icon.ico + // 2 - data:image/x-icon;base64,VERY-LONG-STRING + // + // If the URI is not a data: URI, there's no point in converting + // it to an arraybuffer (which is used to optimize passing the data + // accross processes): we can just pass the original URI, which is cheaper. + if (!url.startsWith("data:")) { + return url; + } + + return new Promise(resolve => { + let xhr = new XMLHttpRequest(); + xhr.open("GET", url, true); + xhr.responseType = "arraybuffer"; + xhr.onload = () => { + resolve(xhr.response); + }; + xhr.onerror = + xhr.onabort = + xhr.ontimeout = + () => { + resolve(SEARCH_ENGINE_PLACEHOLDER_ICON); + }; + try { + // This throws if the URI is erroneously encoded. + xhr.send(); + } catch (err) { + resolve(SEARCH_ENGINE_PLACEHOLDER_ICON); + } + }); + }, + + _ensureDataHasProperties(data, requiredProperties) { + for (let prop of requiredProperties) { + if (!(prop in data)) { + throw new Error("Message data missing required property: " + prop); + } + } + }, + + _initService() { + if (!this._initServicePromise) { + this._initServicePromise = Services.search.init(); + } + return this._initServicePromise; + }, +}; + +export class ContentSearchParent extends JSWindowActorParent { + constructor() { + super(); + ContentSearch.init(); + gContentSearchActors.add(this); + } + + didDestroy() { + gContentSearchActors.delete(this); + } + + receiveMessage(msg) { + // Add a temporary event handler that exists only while the message is in + // the event queue. If the message's source docshell changes browsers in + // the meantime, then we need to update the browser. event.detail will be + // the docshell's new parent element. + let browser = this.browsingContext.top.embedderElement; + let eventItem = { + type: "Message", + name: msg.name, + data: msg.data, + browser, + actor: this, + handleEvent: event => { + let browserData = ContentSearch._suggestionMap.get(eventItem.browser); + if (browserData) { + ContentSearch._suggestionMap.delete(eventItem.browser); + ContentSearch._suggestionMap.set(event.detail, browserData); + } + browser.removeEventListener("SwapDocShells", eventItem, true); + eventItem.browser = event.detail; + eventItem.browser.addEventListener("SwapDocShells", eventItem, true); + }, + }; + browser.addEventListener("SwapDocShells", eventItem, true); + + // Search requests cause cancellation of all Suggestion requests from the + // same browser. + if (msg.name === "Search") { + ContentSearch._cancelSuggestions(eventItem); + } + + ContentSearch._eventQueue.push(eventItem); + ContentSearch._processEventQueue(); + } +} diff --git a/browser/actors/ContextMenuChild.sys.mjs b/browser/actors/ContextMenuChild.sys.mjs new file mode 100644 index 0000000000..235a0d268a --- /dev/null +++ b/browser/actors/ContextMenuChild.sys.mjs @@ -0,0 +1,1239 @@ +/* -*- 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, { + ContentDOMReference: "resource://gre/modules/ContentDOMReference.sys.mjs", + E10SUtils: "resource://gre/modules/E10SUtils.sys.mjs", + InlineSpellCheckerContent: + "resource://gre/modules/InlineSpellCheckerContent.sys.mjs", + LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs", + LoginManagerChild: "resource://gre/modules/LoginManagerChild.sys.mjs", + SelectionUtils: "resource://gre/modules/SelectionUtils.sys.mjs", + SpellCheckHelper: "resource://gre/modules/InlineSpellChecker.sys.mjs", +}); + +let contextMenus = new WeakMap(); + +export class ContextMenuChild extends JSWindowActorChild { + // PUBLIC + constructor() { + super(); + + this.target = null; + this.context = null; + this.lastMenuTarget = null; + } + + static getTarget(browsingContext, message, key) { + let actor = contextMenus.get(browsingContext); + if (!actor) { + throw new Error( + "Can't find ContextMenu actor for browsing context with " + + "ID: " + + browsingContext.id + ); + } + return actor.getTarget(message, key); + } + + static getLastTarget(browsingContext) { + let contextMenu = contextMenus.get(browsingContext); + return contextMenu && contextMenu.lastMenuTarget; + } + + receiveMessage(message) { + switch (message.name) { + case "ContextMenu:GetFrameTitle": { + let target = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + return Promise.resolve(target.ownerDocument.title); + } + + case "ContextMenu:Canvas:ToBlobURL": { + let target = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + return new Promise(resolve => { + target.toBlob(blob => { + let blobURL = URL.createObjectURL(blob); + resolve(blobURL); + }); + }); + } + + case "ContextMenu:Hiding": { + this.context = null; + this.target = null; + break; + } + + case "ContextMenu:MediaCommand": { + lazy.E10SUtils.wrapHandlingUserInput( + this.contentWindow, + message.data.handlingUserInput, + () => { + let media = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + + switch (message.data.command) { + case "play": + media.play(); + break; + case "pause": + media.pause(); + break; + case "loop": + media.loop = !media.loop; + break; + case "mute": + media.muted = true; + break; + case "unmute": + media.muted = false; + break; + case "playbackRate": + media.playbackRate = message.data.data; + break; + case "hidecontrols": + media.removeAttribute("controls"); + break; + case "showcontrols": + media.setAttribute("controls", "true"); + break; + case "fullscreen": + if (this.document.fullscreenEnabled) { + media.requestFullscreen(); + } + break; + case "pictureinpicture": + if (!media.isCloningElementVisually) { + Services.telemetry.keyedScalarAdd( + "pictureinpicture.opened_method", + "contextmenu", + 1 + ); + } + let event = new this.contentWindow.CustomEvent( + "MozTogglePictureInPicture", + { + bubbles: true, + detail: { reason: "contextMenu" }, + }, + this.contentWindow + ); + media.dispatchEvent(event); + break; + } + } + ); + break; + } + + case "ContextMenu:ReloadFrame": { + let target = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + target.ownerDocument.location.reload(message.data.forceReload); + break; + } + + case "ContextMenu:GetImageText": { + let img = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + const { direction } = this.contentWindow.getComputedStyle(img); + + return img.recognizeCurrentImageText().then(results => { + return { results, direction }; + }); + } + + case "ContextMenu:ToggleRevealPassword": { + let target = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + target.revealPassword = !target.revealPassword; + break; + } + + case "ContextMenu:UseRelayMask": { + const input = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + input.setUserInput(message.data.emailMask); + break; + } + + case "ContextMenu:ReloadImage": { + let image = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + + if (image instanceof Ci.nsIImageLoadingContent) { + image.forceReload(); + } + break; + } + + case "ContextMenu:SearchFieldBookmarkData": { + let node = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + let charset = node.ownerDocument.characterSet; + let formBaseURI = Services.io.newURI(node.form.baseURI, charset); + let formURI = Services.io.newURI( + node.form.getAttribute("action"), + charset, + formBaseURI + ); + let spec = formURI.spec; + let isURLEncoded = + node.form.method.toUpperCase() == "POST" && + (node.form.enctype == "application/x-www-form-urlencoded" || + node.form.enctype == ""); + let title = node.ownerDocument.title; + + function escapeNameValuePair([aName, aValue]) { + if (isURLEncoded) { + return escape(aName + "=" + aValue); + } + + return escape(aName) + "=" + escape(aValue); + } + let formData = new this.contentWindow.FormData(node.form); + formData.delete(node.name); + formData = Array.from(formData).map(escapeNameValuePair); + formData.push( + escape(node.name) + (isURLEncoded ? escape("=%s") : "=%s") + ); + + let postData; + + if (isURLEncoded) { + postData = formData.join("&"); + } else { + let separator = spec.includes("?") ? "&" : "?"; + spec += separator + formData.join("&"); + } + + return Promise.resolve({ spec, title, postData, charset }); + } + + case "ContextMenu:SaveVideoFrameAsImage": { + let video = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + let canvas = this.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + let ctxDraw = canvas.getContext("2d"); + ctxDraw.drawImage(video, 0, 0); + + // Note: if changing the content type, don't forget to update + // consumers that also hardcode this content type. + return Promise.resolve(canvas.toDataURL("image/jpeg", "")); + } + + case "ContextMenu:SetAsDesktopBackground": { + let target = lazy.ContentDOMReference.resolve( + message.data.targetIdentifier + ); + + // Paranoia: check disableSetDesktopBackground again, in case the + // image changed since the context menu was initiated. + let disable = this._disableSetDesktopBackground(target); + + if (!disable) { + try { + Services.scriptSecurityManager.checkLoadURIWithPrincipal( + target.ownerDocument.nodePrincipal, + target.currentURI + ); + let canvas = this.document.createElement("canvas"); + canvas.width = target.naturalWidth; + canvas.height = target.naturalHeight; + let ctx = canvas.getContext("2d"); + ctx.drawImage(target, 0, 0); + let dataURL = canvas.toDataURL(); + let url = new URL(target.ownerDocument.location.href).pathname; + let imageName = url.substr(url.lastIndexOf("/") + 1); + return Promise.resolve({ failed: false, dataURL, imageName }); + } catch (e) { + console.error(e); + } + } + + return Promise.resolve({ + failed: true, + dataURL: null, + imageName: null, + }); + } + } + + return undefined; + } + + /** + * Returns the event target of the context menu, using a locally stored + * reference if possible. If not, and aMessage.objects is defined, + * aMessage.objects[aKey] is returned. Otherwise null. + * @param {Object} aMessage Message with a objects property + * @param {String} aKey Key for the target on aMessage.objects + * @return {Object} Context menu target + */ + getTarget(aMessage, aKey = "target") { + return this.target || (aMessage.objects && aMessage.objects[aKey]); + } + + // PRIVATE + _isXULTextLinkLabel(aNode) { + const XUL_NS = + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + return ( + aNode.namespaceURI == XUL_NS && + aNode.tagName == "label" && + aNode.classList.contains("text-link") && + aNode.href + ); + } + + // Generate fully qualified URL for clicked-on link. + _getLinkURL() { + let href = this.context.link.href; + + if (href) { + // Handle SVG links: + if (typeof href == "object" && href.animVal) { + return this._makeURLAbsolute(this.context.link.baseURI, href.animVal); + } + + return href; + } + + href = + this.context.link.getAttribute("href") || + this.context.link.getAttributeNS("http://www.w3.org/1999/xlink", "href"); + + if (!href || !href.match(/\S/)) { + // Without this we try to save as the current doc, + // for example, HTML case also throws if empty + throw new Error("Empty href"); + } + + return this._makeURLAbsolute(this.context.link.baseURI, href); + } + + _getLinkURI() { + try { + return Services.io.newURI(this.context.linkURL); + } catch (ex) { + // e.g. empty URL string + } + + return null; + } + + // Get text of link. + _getLinkText() { + let text = this._gatherTextUnder(this.context.link); + + if (!text || !text.match(/\S/)) { + text = this.context.link.getAttribute("title"); + if (!text || !text.match(/\S/)) { + text = this.context.link.getAttribute("alt"); + if (!text || !text.match(/\S/)) { + text = this.context.linkURL; + } + } + } + + return text; + } + + _getLinkProtocol() { + if (this.context.linkURI) { + return this.context.linkURI.scheme; // can be |undefined| + } + + return null; + } + + // Returns true if clicked-on link targets a resource that can be saved. + _isLinkSaveable(aLink) { + // We don't do the Right Thing for news/snews yet, so turn them off + // until we do. + return ( + this.context.linkProtocol && + !( + this.context.linkProtocol == "mailto" || + this.context.linkProtocol == "tel" || + this.context.linkProtocol == "javascript" || + this.context.linkProtocol == "news" || + this.context.linkProtocol == "snews" + ) + ); + } + + // Gather all descendent text under given document node. + _gatherTextUnder(root) { + let text = ""; + let node = root.firstChild; + let depth = 1; + while (node && depth > 0) { + // See if this node is text. + if (node.nodeType == node.TEXT_NODE) { + // Add this text to our collection. + text += " " + node.data; + } else if (this.contentWindow.HTMLImageElement.isInstance(node)) { + // If it has an "alt" attribute, add that. + let altText = node.getAttribute("alt"); + if (altText && altText != "") { + text += " " + altText; + } + } + // Find next node to test. + // First, see if this node has children. + if (node.hasChildNodes()) { + // Go to first child. + node = node.firstChild; + depth++; + } else { + // No children, try next sibling (or parent next sibling). + while (depth > 0 && !node.nextSibling) { + node = node.parentNode; + depth--; + } + if (node.nextSibling) { + node = node.nextSibling; + } + } + } + + // Strip leading and tailing whitespace. + text = text.trim(); + // Compress remaining whitespace. + text = text.replace(/\s+/g, " "); + return text; + } + + // Returns a "url"-type computed style attribute value, with the url() stripped. + _getComputedURL(aElem, aProp) { + let urls = aElem.ownerGlobal.getComputedStyle(aElem).getCSSImageURLs(aProp); + + if (!urls.length) { + return null; + } + + if (urls.length != 1) { + throw new Error("found multiple URLs"); + } + + return urls[0]; + } + + _makeURLAbsolute(aBase, aUrl) { + return Services.io.newURI(aUrl, null, Services.io.newURI(aBase)).spec; + } + + _isProprietaryDRM() { + return ( + this.context.target.isEncrypted && + this.context.target.mediaKeys && + this.context.target.mediaKeys.keySystem != "org.w3.clearkey" + ); + } + + _isMediaURLReusable(aURL) { + if (aURL.startsWith("blob:")) { + return URL.isValidObjectURL(aURL); + } + + return true; + } + + _isTargetATextBox(node) { + if (this.contentWindow.HTMLInputElement.isInstance(node)) { + return node.mozIsTextField(false); + } + + return this.contentWindow.HTMLTextAreaElement.isInstance(node); + } + + /** + * Check if we are in the parent process and the current iframe is the RDM iframe. + */ + _isTargetRDMFrame(node) { + return ( + Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT && + node.tagName === "iframe" && + node.hasAttribute("mozbrowser") + ); + } + + _isSpellCheckEnabled(aNode) { + // We can always force-enable spellchecking on textboxes + if (this._isTargetATextBox(aNode)) { + return true; + } + + // We can never spell check something which is not content editable + let editable = aNode.isContentEditable; + + if (!editable && aNode.ownerDocument) { + editable = aNode.ownerDocument.designMode == "on"; + } + + if (!editable) { + return false; + } + + // Otherwise make sure that nothing in the parent chain disables spellchecking + return aNode.spellcheck; + } + + _disableSetDesktopBackground(aTarget) { + // Disable the Set as Desktop Background menu item if we're still trying + // to load the image or the load failed. + if (!(aTarget instanceof Ci.nsIImageLoadingContent)) { + return true; + } + + if ("complete" in aTarget && !aTarget.complete) { + return true; + } + + if (aTarget.currentURI.schemeIs("javascript")) { + return true; + } + + let request = aTarget.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); + + if (!request) { + return true; + } + + return false; + } + + async handleEvent(aEvent) { + contextMenus.set(this.browsingContext, this); + + let defaultPrevented = aEvent.defaultPrevented; + + if ( + // If the event is not from a chrome-privileged document, and if + // `dom.event.contextmenu.enabled` is false, force defaultPrevented=false. + !aEvent.composedTarget.nodePrincipal.isSystemPrincipal && + !Services.prefs.getBoolPref("dom.event.contextmenu.enabled") + ) { + defaultPrevented = false; + } + + if (defaultPrevented) { + return; + } + + if (this._isTargetRDMFrame(aEvent.composedTarget)) { + // The target is in the DevTools RDM iframe, a proper context menu event + // will be created from the RDM browser. + return; + } + + let doc = aEvent.composedTarget.ownerDocument; + let { + mozDocumentURIIfNotForErrorPages: docLocation, + characterSet: charSet, + baseURI, + } = doc; + docLocation = docLocation && docLocation.spec; + const loginManagerChild = lazy.LoginManagerChild.forWindow(doc.defaultView); + const docState = loginManagerChild.stateForDocument(doc); + const loginFillInfo = docState.getFieldContext(aEvent.composedTarget); + + let disableSetDesktopBackground = null; + + // Media related cache info parent needs for saving + let contentType = null; + let contentDisposition = null; + if ( + aEvent.composedTarget.nodeType == aEvent.composedTarget.ELEMENT_NODE && + aEvent.composedTarget instanceof Ci.nsIImageLoadingContent && + aEvent.composedTarget.currentURI + ) { + disableSetDesktopBackground = this._disableSetDesktopBackground( + aEvent.composedTarget + ); + + try { + let imageCache = Cc["@mozilla.org/image/tools;1"] + .getService(Ci.imgITools) + .getImgCacheForDocument(doc); + // The image cache's notion of where this image is located is + // the currentURI of the image loading content. + let props = imageCache.findEntryProperties( + aEvent.composedTarget.currentURI, + doc + ); + + try { + contentType = props.get("type", Ci.nsISupportsCString).data; + } catch (e) {} + + try { + contentDisposition = props.get( + "content-disposition", + Ci.nsISupportsCString + ).data; + } catch (e) {} + } catch (e) {} + } + + let selectionInfo = lazy.SelectionUtils.getSelectionDetails( + this.contentWindow + ); + + this._setContext(aEvent); + let context = this.context; + this.target = context.target; + + let spellInfo = null; + let editFlags = null; + + let referrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance( + Ci.nsIReferrerInfo + ); + referrerInfo.initWithElement(aEvent.composedTarget); + referrerInfo = lazy.E10SUtils.serializeReferrerInfo(referrerInfo); + + // In the case "onLink" we may have to send link referrerInfo to use in + // _openLinkInParameters + let linkReferrerInfo = null; + if (context.onLink) { + linkReferrerInfo = Cc["@mozilla.org/referrer-info;1"].createInstance( + Ci.nsIReferrerInfo + ); + linkReferrerInfo.initWithElement(context.link); + } + + let target = context.target; + if (target) { + this._cleanContext(); + } + + editFlags = lazy.SpellCheckHelper.isEditable( + aEvent.composedTarget, + this.contentWindow + ); + + if (editFlags & lazy.SpellCheckHelper.SPELLCHECKABLE) { + spellInfo = lazy.InlineSpellCheckerContent.initContextMenu( + aEvent, + editFlags, + this + ); + } + + // Set the event target first as the copy image command needs it to + // determine what was context-clicked on. Then, update the state of the + // commands on the context menu. + this.docShell.contentViewer + .QueryInterface(Ci.nsIContentViewerEdit) + .setCommandNode(aEvent.composedTarget); + aEvent.composedTarget.ownerGlobal.updateCommands("contentcontextmenu"); + + let data = { + context, + charSet, + baseURI, + referrerInfo, + editFlags, + contentType, + docLocation, + loginFillInfo, + selectionInfo, + contentDisposition, + disableSetDesktopBackground, + }; + + if (context.inFrame && !context.inSrcdocFrame) { + data.frameReferrerInfo = lazy.E10SUtils.serializeReferrerInfo( + doc.referrerInfo + ); + } + + if (linkReferrerInfo) { + data.linkReferrerInfo = + lazy.E10SUtils.serializeReferrerInfo(linkReferrerInfo); + } + + // Notify observers (currently only webextensions) of the context menu being + // prepared, allowing them to set webExtContextData for us. + let prepareContextMenu = { + principal: doc.nodePrincipal, + setWebExtContextData(webExtContextData) { + data.webExtContextData = webExtContextData; + }, + }; + Services.obs.notifyObservers(prepareContextMenu, "on-prepare-contextmenu"); + + // In the event that the content is running in the parent process, we don't + // actually want the contextmenu events to reach the parent - we'll dispatch + // a new contextmenu event after the async message has reached the parent + // instead. + aEvent.stopPropagation(); + + data.spellInfo = null; + if (!spellInfo) { + this.sendAsyncMessage("contextmenu", data); + return; + } + + try { + data.spellInfo = await spellInfo; + } catch (ex) {} + this.sendAsyncMessage("contextmenu", data); + } + + /** + * Some things are not serializable, so we either have to only send + * their needed data or regenerate them in nsContextMenu.js + * - target and target.ownerDocument + * - link + * - linkURI + */ + _cleanContext(aEvent) { + const context = this.context; + const cleanTarget = Object.create(null); + + cleanTarget.ownerDocument = { + // used for nsContextMenu.initLeaveDOMFullScreenItems and + // nsContextMenu.initMediaPlayerItems + fullscreen: context.target.ownerDocument.fullscreen, + + // used for nsContextMenu.initMiscItems + contentType: context.target.ownerDocument.contentType, + }; + + // used for nsContextMenu.initMediaPlayerItems + Object.assign(cleanTarget, { + ended: context.target.ended, + muted: context.target.muted, + paused: context.target.paused, + controls: context.target.controls, + duration: context.target.duration, + }); + + const onMedia = context.onVideo || context.onAudio; + + if (onMedia) { + Object.assign(cleanTarget, { + loop: context.target.loop, + error: context.target.error, + networkState: context.target.networkState, + playbackRate: context.target.playbackRate, + NETWORK_NO_SOURCE: context.target.NETWORK_NO_SOURCE, + }); + + if (context.onVideo) { + Object.assign(cleanTarget, { + readyState: context.target.readyState, + HAVE_CURRENT_DATA: context.target.HAVE_CURRENT_DATA, + }); + } + } + + context.target = cleanTarget; + + if (context.link) { + context.link = { href: context.linkURL }; + } + + delete context.linkURI; + } + + _setContext(aEvent) { + this.context = Object.create(null); + const context = this.context; + + context.timeStamp = aEvent.timeStamp; + context.screenXDevPx = aEvent.screenX * this.contentWindow.devicePixelRatio; + context.screenYDevPx = aEvent.screenY * this.contentWindow.devicePixelRatio; + context.mozInputSource = aEvent.mozInputSource; + + let node = aEvent.composedTarget; + + // Set the node to containing