386 lines
12 KiB
JavaScript
386 lines
12 KiB
JavaScript
/* vim: set ts=2 sw=2 et tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { Troubleshoot } from "resource://gre/modules/Troubleshoot.sys.mjs";
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
export class ReportBrokenSiteParent extends JSWindowActorParent {
|
|
#getAntitrackingBlockList() {
|
|
// If content-track-digest256 is in the tracking table,
|
|
// the user has enabled the strict list.
|
|
const trackingTable = Services.prefs.getCharPref(
|
|
"urlclassifier.trackingTable"
|
|
);
|
|
return trackingTable.includes("content") ? "strict" : "basic";
|
|
}
|
|
|
|
#getETPCategory() {
|
|
// Note that the pref will be set to "custom" if the user disables ETP on
|
|
// mobile.
|
|
const etpState = Services.prefs.getStringPref(
|
|
"browser.contentblocking.category",
|
|
"standard"
|
|
);
|
|
return etpState;
|
|
}
|
|
|
|
#getAntitrackingInfo(browsingContext) {
|
|
// Ask BounceTrackingProtection whether it has recently purged state for the
|
|
// site in the current top level context.
|
|
let btpHasPurgedSite = false;
|
|
if (
|
|
Services.prefs.getIntPref("privacy.bounceTrackingProtection.mode") !=
|
|
Ci.nsIBounceTrackingProtection.MODE_DISABLED
|
|
) {
|
|
let bounceTrackingProtection = Cc[
|
|
"@mozilla.org/bounce-tracking-protection;1"
|
|
].getService(Ci.nsIBounceTrackingProtection);
|
|
|
|
let { currentWindowGlobal } = browsingContext;
|
|
if (currentWindowGlobal) {
|
|
let { documentPrincipal } = currentWindowGlobal;
|
|
let { baseDomain } = documentPrincipal;
|
|
btpHasPurgedSite =
|
|
bounceTrackingProtection.hasRecentlyPurgedSite(baseDomain);
|
|
}
|
|
}
|
|
|
|
return {
|
|
blockList: this.#getAntitrackingBlockList(),
|
|
isPrivateBrowsing: browsingContext.usePrivateBrowsing,
|
|
hasTrackingContentBlocked: !!(
|
|
browsingContext.currentWindowGlobal.contentBlockingEvents &
|
|
Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT
|
|
),
|
|
hasMixedActiveContentBlocked: !!(
|
|
browsingContext.secureBrowserUI.state &
|
|
Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT
|
|
),
|
|
hasMixedDisplayContentBlocked: !!(
|
|
browsingContext.secureBrowserUI.state &
|
|
Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT
|
|
),
|
|
btpHasPurgedSite,
|
|
etpCategory: this.#getETPCategory(),
|
|
};
|
|
}
|
|
|
|
#parseGfxInfo(info) {
|
|
const get = name => {
|
|
try {
|
|
return info[name];
|
|
} catch (e) {}
|
|
return undefined;
|
|
};
|
|
|
|
const clean = rawObj => {
|
|
const obj = JSON.parse(JSON.stringify(rawObj));
|
|
if (!Object.keys(obj).length) {
|
|
return undefined;
|
|
}
|
|
return obj;
|
|
};
|
|
|
|
const cleanDevice = (vendorID, deviceID, subsysID) => {
|
|
return clean({ vendorID, deviceID, subsysID });
|
|
};
|
|
|
|
const d1 = cleanDevice(
|
|
get("adapterVendorID"),
|
|
get("adapterDeviceID"),
|
|
get("adapterSubsysID")
|
|
);
|
|
const d2 = cleanDevice(
|
|
get("adapterVendorID2"),
|
|
get("adapterDeviceID2"),
|
|
get("adapterSubsysID2")
|
|
);
|
|
const devices = (get("isGPU2Active") ? [d2, d1] : [d1, d2]).filter(
|
|
v => v !== undefined
|
|
);
|
|
|
|
return clean({
|
|
direct2DEnabled: get("direct2DEnabled"),
|
|
directWriteEnabled: get("directWriteEnabled"),
|
|
directWriteVersion: get("directWriteVersion"),
|
|
hasTouchScreen: info.ApzTouchInput == 1,
|
|
clearTypeParameters: get("clearTypeParameters"),
|
|
targetFrameRate: get("targetFrameRate"),
|
|
devices,
|
|
});
|
|
}
|
|
|
|
#parseCodecSupportInfo(codecSupportInfo) {
|
|
if (!codecSupportInfo) {
|
|
return undefined;
|
|
}
|
|
|
|
const codecs = {};
|
|
for (const item of codecSupportInfo.split("\n")) {
|
|
const [codec, ...types] = item.split(" ");
|
|
if (!codecs[codec]) {
|
|
codecs[codec] = { hardware: false, software: false };
|
|
}
|
|
codecs[codec].software ||= types.includes("SW");
|
|
codecs[codec].hardware ||= types.includes("HW");
|
|
}
|
|
return codecs;
|
|
}
|
|
|
|
#parseFeatureLog(featureLog = {}) {
|
|
const { features } = featureLog;
|
|
if (!features) {
|
|
return undefined;
|
|
}
|
|
|
|
const parsedFeatures = {};
|
|
for (let { name, log, status } of features) {
|
|
for (const item of log.reverse()) {
|
|
if (!item.failureId || item.status != status) {
|
|
continue;
|
|
}
|
|
status = `${status} (${item.message || item.failureId})`;
|
|
}
|
|
parsedFeatures[name] = status;
|
|
}
|
|
return parsedFeatures;
|
|
}
|
|
|
|
#getGraphicsInfo(troubleshoot) {
|
|
const { graphics, media } = troubleshoot;
|
|
const { featureLog } = graphics;
|
|
const data = this.#parseGfxInfo(graphics);
|
|
data.drivers = [
|
|
{
|
|
renderer: graphics.webgl1Renderer,
|
|
version: graphics.webgl1Version,
|
|
},
|
|
{
|
|
renderer: graphics.webgl2Renderer,
|
|
version: graphics.webgl2Version,
|
|
},
|
|
].filter(({ version }) => version && version != "-");
|
|
|
|
data.codecSupport = this.#parseCodecSupportInfo(media.codecSupportInfo);
|
|
data.features = this.#parseFeatureLog(featureLog);
|
|
|
|
const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
|
|
data.monitors = gfxInfo.getMonitors();
|
|
|
|
return data;
|
|
}
|
|
|
|
#getAppInfo(troubleshootingInfo) {
|
|
const { application } = troubleshootingInfo;
|
|
return {
|
|
applicationName: application.name,
|
|
buildId: application.buildID,
|
|
defaultUserAgent: application.userAgent,
|
|
updateChannel: application.updateChannel,
|
|
version: application.version,
|
|
};
|
|
}
|
|
|
|
#getSysinfoProperty(propertyName, defaultValue) {
|
|
try {
|
|
return Services.sysinfo.getProperty(propertyName);
|
|
} catch (e) {}
|
|
return defaultValue;
|
|
}
|
|
|
|
#getPrefs() {
|
|
const prefs = {};
|
|
for (const name of [
|
|
"layers.acceleration.force-enabled",
|
|
"gfx.webrender.software",
|
|
"browser.opaqueResponseBlocking",
|
|
"extensions.InstallTrigger.enabled",
|
|
"layout.css.h1-in-section-ua-styles.enabled",
|
|
"privacy.resistFingerprinting",
|
|
"privacy.globalprivacycontrol.enabled",
|
|
"network.cookie.cookieBehavior.optInPartitioning",
|
|
"network.cookie.cookieBehavior.optInPartitioning.pbmode",
|
|
]) {
|
|
prefs[name] = Services.prefs.getBoolPref(name, undefined);
|
|
}
|
|
const cookieBehavior = "network.cookie.cookieBehavior";
|
|
prefs[cookieBehavior] = Services.prefs.getIntPref(cookieBehavior, -1);
|
|
return prefs;
|
|
}
|
|
|
|
async #getPlatformInfo(troubleshootingInfo) {
|
|
const { application } = troubleshootingInfo;
|
|
const { memorySizeBytes, fissionAutoStart } = application;
|
|
|
|
let memoryMB = memorySizeBytes;
|
|
if (memoryMB) {
|
|
memoryMB = Math.round(memoryMB / 1024 / 1024);
|
|
}
|
|
|
|
const info = {
|
|
fissionEnabled: fissionAutoStart,
|
|
memoryMB,
|
|
osArchitecture: this.#getSysinfoProperty("arch", null),
|
|
osName: this.#getSysinfoProperty("name", null),
|
|
osVersion: this.#getSysinfoProperty("version", null),
|
|
name: AppConstants.platform,
|
|
};
|
|
if (info.os === "android") {
|
|
info.device = this.#getSysinfoProperty("device", null);
|
|
info.isTablet = this.#getSysinfoProperty("tablet", false);
|
|
}
|
|
if (
|
|
info.osName == "Windows_NT" &&
|
|
(await Services.sysinfo.processInfo).isWindowsSMode
|
|
) {
|
|
info.osVersion += " S";
|
|
}
|
|
return info;
|
|
}
|
|
|
|
#getSecurityInfo(troubleshootingInfo) {
|
|
const result = {};
|
|
for (const [k, v] of Object.entries(troubleshootingInfo.securitySoftware)) {
|
|
result[k.replace("registered", "").toLowerCase()] = v
|
|
? v.split(";")
|
|
: null;
|
|
}
|
|
|
|
// Right now, security data is only available for Windows builds, and
|
|
// we might as well not return anything at all if no data is available.
|
|
if (!Object.values(result).filter(e => e).length) {
|
|
return undefined;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static AUTOMATION_ADDON_IDS = [
|
|
"mochikit@mozilla.org",
|
|
"special-powers@mozilla.org",
|
|
];
|
|
|
|
static WANTED_ADDON_LOCATIONS = ["app-profile", "app-temporary"];
|
|
|
|
#getActiveAddons(troubleshootingInfo) {
|
|
const { addons } = troubleshootingInfo;
|
|
if (!addons) {
|
|
return [];
|
|
}
|
|
// We only care about enabled addons (not themes) the user
|
|
// installed, not ones bundled with Firefox.
|
|
const toReport = addons.filter(
|
|
({ id, isActive, type, locationName }) =>
|
|
(!Cu.isInAutomation ||
|
|
!ReportBrokenSiteParent.AUTOMATION_ADDON_IDS.includes(id)) &&
|
|
isActive &&
|
|
type === "extension" &&
|
|
ReportBrokenSiteParent.WANTED_ADDON_LOCATIONS.includes(locationName)
|
|
);
|
|
return toReport.map(({ id, name, version, locationName }) => {
|
|
return {
|
|
id,
|
|
name,
|
|
temporary: locationName === "app-temporary",
|
|
version,
|
|
};
|
|
});
|
|
}
|
|
|
|
#getActiveExperiments(troubleshootingInfo) {
|
|
if (!troubleshootingInfo?.normandy) {
|
|
return [];
|
|
}
|
|
const {
|
|
normandy: { nimbusExperiments, nimbusRollouts },
|
|
} = troubleshootingInfo;
|
|
return [
|
|
nimbusExperiments.map(({ slug, branch }) => {
|
|
return { slug, branch: branch.slug, kind: "nimbusExperiment" };
|
|
}),
|
|
nimbusRollouts.map(({ slug, branch }) => {
|
|
return { slug, branch: branch.slug, kind: "nimbusRollout" };
|
|
}),
|
|
]
|
|
.flat()
|
|
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
}
|
|
|
|
async #getBrowserInfo() {
|
|
const troubleshootingInfo = await Troubleshoot.snapshot();
|
|
return {
|
|
addons: this.#getActiveAddons(troubleshootingInfo),
|
|
app: this.#getAppInfo(troubleshootingInfo),
|
|
experiments: this.#getActiveExperiments(troubleshootingInfo),
|
|
graphics: this.#getGraphicsInfo(troubleshootingInfo),
|
|
locales: troubleshootingInfo.intl.localeService.available,
|
|
prefs: this.#getPrefs(),
|
|
platform: await this.#getPlatformInfo(troubleshootingInfo),
|
|
security: this.#getSecurityInfo(troubleshootingInfo),
|
|
};
|
|
}
|
|
|
|
async #getScreenshot(browsingContext, format, quality) {
|
|
const zoom = browsingContext.fullZoom;
|
|
const scale = browsingContext.topChromeWindow?.devicePixelRatio || 1;
|
|
const wgp = browsingContext.currentWindowGlobal;
|
|
|
|
const image = await wgp.drawSnapshot(
|
|
undefined, // rect
|
|
scale * zoom,
|
|
"white",
|
|
undefined // resetScrollPosition
|
|
);
|
|
|
|
const canvas = new OffscreenCanvas(image.width, image.height);
|
|
|
|
const ctx = canvas.getContext("bitmaprenderer", { alpha: false });
|
|
ctx.transferFromImageBitmap(image);
|
|
|
|
const blob = await canvas.convertToBlob({
|
|
type: `image/${format}`,
|
|
quality: quality / 100,
|
|
});
|
|
|
|
const dataURL = await new Promise((resolve, reject) => {
|
|
let reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = () => reject(reader.error);
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
|
|
return dataURL;
|
|
}
|
|
|
|
async receiveMessage(msg) {
|
|
switch (msg.name) {
|
|
case "GetWebcompatInfoFromParentProcess": {
|
|
const { browsingContext } = msg.target;
|
|
const { format, quality } = msg.data;
|
|
const screenshot = await this.#getScreenshot(
|
|
browsingContext,
|
|
format,
|
|
quality
|
|
).catch(e => {
|
|
console.error("Report Broken Site: getting a screenshot failed", e);
|
|
return Promise.resolve(undefined);
|
|
});
|
|
|
|
const zoom = browsingContext.fullZoom;
|
|
const scale = browsingContext.topChromeWindow?.devicePixelRatio || 1;
|
|
const devicePixelRatio = scale * zoom;
|
|
|
|
return {
|
|
antitracking: this.#getAntitrackingInfo(msg.target.browsingContext),
|
|
browser: await this.#getBrowserInfo(),
|
|
devicePixelRatio,
|
|
screenshot,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|