diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /mobile/android/modules/geckoview/GeckoViewProgress.sys.mjs | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'mobile/android/modules/geckoview/GeckoViewProgress.sys.mjs')
-rw-r--r-- | mobile/android/modules/geckoview/GeckoViewProgress.sys.mjs | 637 |
1 files changed, 637 insertions, 0 deletions
diff --git a/mobile/android/modules/geckoview/GeckoViewProgress.sys.mjs b/mobile/android/modules/geckoview/GeckoViewProgress.sys.mjs new file mode 100644 index 0000000000..12ea264661 --- /dev/null +++ b/mobile/android/modules/geckoview/GeckoViewProgress.sys.mjs @@ -0,0 +1,637 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { GeckoViewModule } from "resource://gre/modules/GeckoViewModule.sys.mjs"; +import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; + +const lazy = {}; + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "OverrideService", + "@mozilla.org/security/certoverride;1", + "nsICertOverrideService" +); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "IDNService", + "@mozilla.org/network/idn-service;1", + "nsIIDNService" +); + +ChromeUtils.defineESModuleGetters(lazy, { + BrowserTelemetryUtils: "resource://gre/modules/BrowserTelemetryUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + HistogramStopwatch: "resource://gre/modules/GeckoViewTelemetry.jsm", +}); + +var IdentityHandler = { + // The definitions below should be kept in sync with those in GeckoView.ProgressListener.SecurityInformation + // No trusted identity information. No site identity icon is shown. + IDENTITY_MODE_UNKNOWN: 0, + + // Domain-Validation SSL CA-signed domain verification (DV). + IDENTITY_MODE_IDENTIFIED: 1, + + // Extended-Validation SSL CA-signed identity information (EV). A more rigorous validation process. + IDENTITY_MODE_VERIFIED: 2, + + // The following mixed content modes are only used if "security.mixed_content.block_active_content" + // is enabled. Our Java frontend coalesces them into one indicator. + + // No mixed content information. No mixed content icon is shown. + MIXED_MODE_UNKNOWN: 0, + + // Blocked active mixed content. + MIXED_MODE_CONTENT_BLOCKED: 1, + + // Loaded active mixed content. + MIXED_MODE_CONTENT_LOADED: 2, + + /** + * Determines the identity mode corresponding to the icon we show in the urlbar. + */ + getIdentityMode: function getIdentityMode(aState) { + if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) { + return this.IDENTITY_MODE_VERIFIED; + } + + if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) { + return this.IDENTITY_MODE_IDENTIFIED; + } + + return this.IDENTITY_MODE_UNKNOWN; + }, + + getMixedDisplayMode: function getMixedDisplayMode(aState) { + if (aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) { + return this.MIXED_MODE_CONTENT_LOADED; + } + + if ( + aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT + ) { + return this.MIXED_MODE_CONTENT_BLOCKED; + } + + return this.MIXED_MODE_UNKNOWN; + }, + + getMixedActiveMode: function getActiveDisplayMode(aState) { + // Only show an indicator for loaded mixed content if the pref to block it is enabled + if ( + aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT && + !Services.prefs.getBoolPref("security.mixed_content.block_active_content") + ) { + return this.MIXED_MODE_CONTENT_LOADED; + } + + if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) { + return this.MIXED_MODE_CONTENT_BLOCKED; + } + + return this.MIXED_MODE_UNKNOWN; + }, + + /** + * Determine the identity of the page being displayed by examining its SSL cert + * (if available). Return the data needed to update the UI. + */ + checkIdentity: function checkIdentity(aState, aBrowser) { + const identityMode = this.getIdentityMode(aState); + const mixedDisplay = this.getMixedDisplayMode(aState); + const mixedActive = this.getMixedActiveMode(aState); + const result = { + mode: { + identity: identityMode, + mixed_display: mixedDisplay, + mixed_active: mixedActive, + }, + }; + + if (aBrowser.contentPrincipal) { + result.origin = aBrowser.contentPrincipal.originNoSuffix; + } + + // Don't show identity data for pages with an unknown identity or if any + // mixed content is loaded (mixed display content is loaded by default). + if ( + identityMode === this.IDENTITY_MODE_UNKNOWN || + aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN || + aState & Ci.nsIWebProgressListener.STATE_IS_INSECURE + ) { + result.secure = false; + return result; + } + + result.secure = true; + + let uri = aBrowser.currentURI || {}; + try { + uri = Services.io.createExposableURI(uri); + } catch (e) {} + + try { + result.host = lazy.IDNService.convertToDisplayIDN(uri.host, {}); + } catch (e) { + result.host = uri.host; + } + + const cert = aBrowser.securityUI.secInfo.serverCert; + + result.certificate = + aBrowser.securityUI.secInfo.serverCert.getBase64DERString(); + + try { + result.securityException = lazy.OverrideService.hasMatchingOverride( + uri.host, + uri.port, + {}, + cert, + {} + ); + + // If an override exists, the connection is being allowed but should not + // be considered secure. + result.secure = !result.securityException; + } catch (e) {} + + return result; + }, +}; + +class Tracker { + constructor(aModule) { + this._module = aModule; + } + + get eventDispatcher() { + return this._module.eventDispatcher; + } + + get browser() { + return this._module.browser; + } + + QueryInterface = ChromeUtils.generateQI(["nsIWebProgressListener"]); +} + +class ProgressTracker extends Tracker { + constructor(aModule) { + super(aModule); + const window = aModule.browser.ownerGlobal; + this.pageLoadProbe = new lazy.HistogramStopwatch("GV_PAGE_LOAD_MS", window); + this.pageReloadProbe = new lazy.HistogramStopwatch( + "GV_PAGE_RELOAD_MS", + window + ); + this.pageLoadProgressProbe = new lazy.HistogramStopwatch( + "GV_PAGE_LOAD_PROGRESS_MS", + window + ); + + this.clear(); + this._eventReceived = null; + } + + start(aUri) { + debug`ProgressTracker start ${aUri}`; + + if (this._eventReceived) { + // A request was already in process, let's cancel it + this.stop(/* isSuccess */ false); + } + + this._eventReceived = new Set(); + this.clear(); + const data = this._data; + + if (aUri === "about:blank") { + data.uri = null; + return; + } + + this.pageLoadProgressProbe.start(); + + data.uri = aUri; + data.pageStart = true; + this.updateProgress(); + } + + changeLocation(aUri) { + debug`ProgressTracker changeLocation ${aUri}`; + + const data = this._data; + data.locationChange = true; + data.uri = aUri; + } + + stop(aIsSuccess) { + debug`ProgressTracker stop`; + + if (!this._eventReceived) { + // No request in progress + return; + } + + if (aIsSuccess) { + this.pageLoadProgressProbe.finish(); + } else { + this.pageLoadProgressProbe.cancel(); + } + + const data = this._data; + data.pageStop = true; + this.updateProgress(); + this._eventReceived = null; + } + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + debug`ProgressTracker onStateChange: isTopLevel=${aWebProgress.isTopLevel}, + flags=${aStateFlags}, status=${aStatus}`; + + if (!aWebProgress || !aWebProgress.isTopLevel) { + return; + } + + const { displaySpec } = aRequest.QueryInterface(Ci.nsIChannel).URI; + + if (aRequest.URI.schemeIs("about")) { + return; + } + + debug`ProgressTracker onStateChange: uri=${displaySpec}`; + + const isPageReload = + (aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_RELOAD) != 0; + const probe = isPageReload ? this.pageReloadProbe : this.pageLoadProbe; + + const isStart = (aStateFlags & Ci.nsIWebProgressListener.STATE_START) != 0; + const isStop = (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) != 0; + const isRedirecting = + (aStateFlags & Ci.nsIWebProgressListener.STATE_REDIRECTING) != 0; + + if (isStart) { + probe.start(); + this.start(displaySpec); + } else if (isStop && !aWebProgress.isLoadingDocument) { + probe.finish(); + this.stop(aStatus == Cr.NS_OK); + } else if (isRedirecting) { + probe.start(); + this.start(displaySpec); + } + + // During history naviation, global window is recycled, so pagetitlechanged isn't fired + // Although Firefox Desktop always set title by onLocationChange, to reduce title change call, + // we only send title during history navigation. + if ((aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) != 0) { + this.eventDispatcher.sendRequest({ + type: "GeckoView:PageTitleChanged", + title: this.browser.contentTitle, + }); + } + } + + onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + if ( + !aWebProgress || + !aWebProgress.isTopLevel || + !aLocationURI || + aLocationURI.schemeIs("about") + ) { + return; + } + + debug`ProgressTracker onLocationChange: location=${aLocationURI.displaySpec}, + flags=${aFlags}`; + + if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_ERROR_PAGE) { + this.stop(/* isSuccess */ false); + } else { + this.changeLocation(aLocationURI.displaySpec); + } + } + + handleEvent(aEvent) { + if (!this._eventReceived || this._eventReceived.has(aEvent.name)) { + // Either we're not tracking or we have received this event already + return; + } + + const data = this._data; + + if (!data.uri || data.uri !== aEvent.data?.uri) { + return; + } + + debug`ProgressTracker handleEvent: ${aEvent.name}`; + + let needsUpdate = false; + + switch (aEvent.name) { + case "DOMContentLoaded": + needsUpdate = needsUpdate || !data.parsed; + data.parsed = true; + break; + case "MozAfterPaint": + needsUpdate = needsUpdate || !data.firstPaint; + data.firstPaint = true; + break; + case "pageshow": + needsUpdate = needsUpdate || !data.pageShow; + data.pageShow = true; + break; + } + + this._eventReceived.add(aEvent.name); + + if (needsUpdate) { + this.updateProgress(); + } + } + + clear() { + this._data = { + prev: 0, + uri: null, + locationChange: false, + pageStart: false, + pageStop: false, + firstPaint: false, + pageShow: false, + parsed: false, + }; + } + + _debugData() { + return { + prev: this._data.prev, + uri: this._data.uri, + locationChange: this._data.locationChange, + pageStart: this._data.pageStart, + pageStop: this._data.pageStop, + firstPaint: this._data.firstPaint, + pageShow: this._data.pageShow, + parsed: this._data.parsed, + }; + } + + updateProgress() { + debug`ProgressTracker updateProgress`; + + const data = this._data; + + if (!this._eventReceived || !data.uri) { + return; + } + + let progress = 0; + if (data.pageStop || data.pageShow) { + progress = 100; + } else if (data.firstPaint) { + progress = 80; + } else if (data.parsed) { + progress = 55; + } else if (data.locationChange) { + progress = 30; + } else if (data.pageStart) { + progress = 15; + } + + if (data.prev >= progress) { + return; + } + + debug`ProgressTracker updateProgress data=${this._debugData()} + progress=${progress}`; + + this.eventDispatcher.sendRequest({ + type: "GeckoView:ProgressChanged", + progress, + }); + + data.prev = progress; + } +} + +class StateTracker extends Tracker { + constructor(aModule) { + super(aModule); + this._inProgress = false; + this._uri = null; + } + + start(aUri) { + this._inProgress = true; + this._uri = aUri; + this.eventDispatcher.sendRequest({ + type: "GeckoView:PageStart", + uri: aUri, + }); + } + + stop(aIsSuccess) { + if (!this._inProgress) { + // No request in progress + return; + } + + this._inProgress = false; + this._uri = null; + + this.eventDispatcher.sendRequest({ + type: "GeckoView:PageStop", + success: aIsSuccess, + }); + + lazy.BrowserTelemetryUtils.recordSiteOriginTelemetry( + Services.wm.getEnumerator("navigator:geckoview"), + true + ); + } + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + debug`StateTracker onStateChange: isTopLevel=${aWebProgress.isTopLevel}, + flags=${aStateFlags}, status=${aStatus} + loadType=${aWebProgress.loadType}`; + + if (!aWebProgress.isTopLevel) { + return; + } + + const { displaySpec } = aRequest.QueryInterface(Ci.nsIChannel).URI; + const isStart = (aStateFlags & Ci.nsIWebProgressListener.STATE_START) != 0; + const isStop = (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) != 0; + + if (isStart) { + this.start(displaySpec); + } else if (isStop && !aWebProgress.isLoadingDocument) { + this.stop(aStatus == Cr.NS_OK); + } + } +} + +class SecurityTracker extends Tracker { + constructor(aModule) { + super(aModule); + this._hostChanged = false; + } + + onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) { + debug`SecurityTracker onLocationChange: location=${aLocationURI.displaySpec}, + flags=${aFlags}`; + + this._hostChanged = true; + } + + onSecurityChange(aWebProgress, aRequest, aState) { + debug`onSecurityChange`; + + // Don't need to do anything if the data we use to update the UI hasn't changed + if (this._state === aState && !this._hostChanged) { + return; + } + + this._state = aState; + this._hostChanged = false; + + const identity = IdentityHandler.checkIdentity(aState, this.browser); + + this.eventDispatcher.sendRequest({ + type: "GeckoView:SecurityChanged", + identity, + }); + } +} + +export class GeckoViewProgress extends GeckoViewModule { + onEnable() { + debug`onEnable`; + + this._fireInitialLoad(); + this._initialAboutBlank = true; + + this._progressTracker = new ProgressTracker(this); + this._securityTracker = new SecurityTracker(this); + this._stateTracker = new StateTracker(this); + + const flags = + Ci.nsIWebProgress.NOTIFY_STATE_NETWORK | + Ci.nsIWebProgress.NOTIFY_SECURITY | + Ci.nsIWebProgress.NOTIFY_LOCATION; + this.progressFilter = Cc[ + "@mozilla.org/appshell/component/browser-status-filter;1" + ].createInstance(Ci.nsIWebProgress); + this.progressFilter.addProgressListener(this, flags); + this.browser.addProgressListener(this.progressFilter, flags); + Services.obs.addObserver(this, "oop-frameloader-crashed"); + this.registerListener("GeckoView:FlushSessionState"); + } + + onDisable() { + debug`onDisable`; + + if (this.progressFilter) { + this.progressFilter.removeProgressListener(this); + this.browser.removeProgressListener(this.progressFilter); + } + + Services.obs.removeObserver(this, "oop-frameloader-crashed"); + this.unregisterListener("GeckoView:FlushSessionState"); + } + + receiveMessage(aMsg) { + debug`receiveMessage: ${aMsg.name}`; + + switch (aMsg.name) { + case "DOMContentLoaded": // fall-through + case "MozAfterPaint": // fall-through + case "pageshow": { + this._progressTracker?.handleEvent(aMsg); + break; + } + } + } + + onEvent(aEvent, aData, aCallback) { + debug`onEvent: event=${aEvent}, data=${aData}`; + + switch (aEvent) { + case "GeckoView:FlushSessionState": + this.messageManager.sendAsyncMessage("GeckoView:FlushSessionState"); + break; + } + } + + onStateChange(...args) { + // GeckoView never gets PageStart or PageStop for about:blank because we + // set nodefaultsrc to true unconditionally so we can assume here that + // we're starting a page load for a non-blank page (or a consumer-initiated + // about:blank load). + this._initialAboutBlank = false; + + this._progressTracker.onStateChange(...args); + this._stateTracker.onStateChange(...args); + } + + onSecurityChange(...args) { + // We don't report messages about the initial about:blank + if (this._initialAboutBlank) { + return; + } + + this._securityTracker.onSecurityChange(...args); + } + + onLocationChange(...args) { + this._securityTracker.onLocationChange(...args); + this._progressTracker.onLocationChange(...args); + } + + // The initial about:blank load events are unreliable because docShell starts + // up concurrently with loading geckoview.js so we're never guaranteed to get + // the events. + // What we do instead is ignore all initial about:blank events and fire them + // manually once the child process has booted up. + _fireInitialLoad() { + this.eventDispatcher.sendRequest({ + type: "GeckoView:PageStart", + uri: "about:blank", + }); + this.eventDispatcher.sendRequest({ + type: "GeckoView:LocationChange", + uri: "about:blank", + canGoBack: false, + canGoForward: false, + isTopLevel: true, + }); + this.eventDispatcher.sendRequest({ + type: "GeckoView:PageStop", + success: true, + }); + } + + // nsIObserver event handler + observe(aSubject, aTopic, aData) { + debug`observe: topic=${aTopic}`; + + switch (aTopic) { + case "oop-frameloader-crashed": { + const browser = aSubject.ownerElement; + if (!browser || browser != this.browser) { + return; + } + + this._progressTracker?.stop(/* isSuccess */ false); + this._stateTracker?.stop(/* isSuccess */ false); + } + } + } +} + +const { debug, warn } = GeckoViewProgress.initLogging("GeckoViewProgress"); |