653 lines
18 KiB
JavaScript
653 lines
18 KiB
JavaScript
/* 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",
|
|
GleanStopwatch: "resource://gre/modules/GeckoViewTelemetry.sys.mjs",
|
|
});
|
|
|
|
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);
|
|
|
|
this.pageLoadStopwatch = new lazy.GleanStopwatch(
|
|
Glean.geckoview.pageLoadTime
|
|
);
|
|
this.pageReloadStopwatch = new lazy.GleanStopwatch(
|
|
Glean.geckoview.pageReloadTime
|
|
);
|
|
this.pageLoadProgressStopwatch = new lazy.GleanStopwatch(
|
|
Glean.geckoview.pageLoadProgressTime
|
|
);
|
|
|
|
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.pageLoadProgressStopwatch.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.pageLoadProgressStopwatch.finish();
|
|
} else {
|
|
this.pageLoadProgressStopwatch.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 stopwatch = isPageReload
|
|
? this.pageReloadStopwatch
|
|
: this.pageLoadStopwatch;
|
|
|
|
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) {
|
|
stopwatch.start();
|
|
this.start(displaySpec);
|
|
} else if (isStop && !aWebProgress.isLoadingDocument) {
|
|
stopwatch.finish();
|
|
this.stop(aStatus == Cr.NS_OK);
|
|
} else if (isRedirecting) {
|
|
stopwatch.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;
|
|
// if page_load.progressbar_completion is set to 1, we complete the page load progress when
|
|
// DOMContentLoaded is received.
|
|
if (this._progressBarCompletion == 1) {
|
|
data.completeProgress = true;
|
|
} else {
|
|
data.parsed = true;
|
|
}
|
|
break;
|
|
case "MozAfterPaint":
|
|
needsUpdate = needsUpdate || !data.firstPaint;
|
|
// if page_load.progressbar_completion is set to 2, we complete the page load progress at
|
|
// the first MozAfterPaint after DOMContentLoaded is received.
|
|
if (this._progressBarCompletion == 2 && data.parsed) {
|
|
data.completeProgress = true;
|
|
} else {
|
|
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,
|
|
completeProgress: false,
|
|
};
|
|
this._progressBarCompletion = Services.prefs.getIntPref(
|
|
"page_load.progressbar_completion"
|
|
);
|
|
}
|
|
|
|
_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 || data.completeProgress) {
|
|
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) {
|
|
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,
|
|
hasUserGesture: false,
|
|
});
|
|
this.eventDispatcher.sendRequest({
|
|
type: "GeckoView:PageStop",
|
|
success: true,
|
|
});
|
|
}
|
|
|
|
// nsIObserver event handler
|
|
observe(aSubject, aTopic) {
|
|
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");
|