fune/browser/extensions/report-site-issue/experimentalAPIs/tabExtras.js

282 lines
7.4 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/. */
"use strict";
/* global ExtensionAPI, XPCOMUtils */
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
function getInfoFrameScript(messageName) {
/* eslint-env mozilla/frame-script */
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
const PREVIEW_MAX_ITEMS = 10;
const LOG_LEVEL_MAP = {
0: "debug",
1: "info",
2: "warn",
3: "error",
};
function getInnerWindowId(window) {
return window.windowUtils.currentInnerWindowID;
}
function getInnerWindowIDsForAllFrames(window) {
const innerWindowID = getInnerWindowId(window);
let ids = [innerWindowID];
if (window.frames) {
for (let i = 0; i < window.frames.length; i++) {
ids = ids.concat(getInnerWindowIDsForAllFrames(window.frames[i]));
}
}
return ids;
}
function getLoggedMessages(window, includePrivate = false) {
const ids = getInnerWindowIDsForAllFrames(window);
return getConsoleMessages(ids)
.concat(getScriptErrors(ids, includePrivate))
.sort((a, b) => a.timeStamp - b.timeStamp)
.map(m => m.message);
}
function getPreview(value) {
switch (typeof value) {
case "function":
return "function ()";
case "object":
if (value === null) {
return null;
}
if (Array.isArray(value)) {
return `(${value.length})[...]`;
}
return "{...}";
case "undefined":
return "undefined";
default:
return value;
}
}
function getArrayPreview(arr) {
const preview = [];
for (const value of arr) {
preview.push(getPreview(value));
if (preview.length === PREVIEW_MAX_ITEMS) {
break;
}
}
return preview;
}
function getObjectPreview(obj) {
const preview = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
preview[key] = getPreview(obj[key]);
}
if (Object.keys(preview).length === PREVIEW_MAX_ITEMS) {
break;
}
}
return preview;
}
function getArgs(value) {
if (typeof value === "object" && value !== null) {
if (Array.isArray(value)) {
return getArrayPreview(value);
}
return getObjectPreview(value);
}
return getPreview(value);
}
function getConsoleMessages(windowIds) {
const ConsoleAPIStorage = Cc[
"@mozilla.org/consoleAPI-storage;1"
].getService(Ci.nsIConsoleAPIStorage);
let messages = [];
for (const id of windowIds) {
messages = messages.concat(ConsoleAPIStorage.getEvents(id) || []);
}
return messages.map(evt => {
const { columnNumber, filename, level, lineNumber, timeStamp } = evt;
const args = evt.arguments.map(getArgs);
const message = {
level,
log: args,
uri: filename,
pos: `${lineNumber}:${columnNumber}`,
};
return { timeStamp, message };
});
}
function getScriptErrors(windowIds, includePrivate = false) {
const messages = Services.console.getMessageArray() || [];
return messages
.filter(message => {
if (message instanceof Ci.nsIScriptError) {
if (!includePrivate && message.isFromPrivateWindow) {
return false;
}
if (windowIds && !windowIds.includes(message.innerWindowID)) {
return false;
}
return true;
}
// If this is not an nsIScriptError and we need to do window-based
// filtering we skip this message.
return false;
})
.map(error => {
const {
timeStamp,
errorMessage,
sourceName,
lineNumber,
columnNumber,
logLevel,
} = error;
const message = {
level: LOG_LEVEL_MAP[logLevel],
log: [errorMessage],
uri: sourceName,
pos: `${lineNumber}:${columnNumber}`,
};
return { timeStamp, message };
});
}
sendAsyncMessage(messageName, {
hasMixedActiveContentBlocked: docShell.hasMixedActiveContentBlocked,
hasMixedDisplayContentBlocked: docShell.hasMixedDisplayContentBlocked,
hasTrackingContentBlocked: docShell.hasTrackingContentBlocked,
log: getLoggedMessages(content),
});
}
this.tabExtras = class extends ExtensionAPI {
getAPI(context) {
const { tabManager } = context.extension;
const {
Management: {
global: { windowTracker },
},
} = ChromeUtils.import("resource://gre/modules/Extension.jsm", null);
const { Services } = ChromeUtils.import(
"resource://gre/modules/Services.jsm"
);
return {
tabExtras: {
async loadURIWithPostData(
tabId,
url,
postDataString,
postDataContentType
) {
const tab = tabManager.get(tabId);
if (!tab || !tab.browser) {
return Promise.reject("Invalid tab");
}
try {
new URL(url);
} catch (_) {
return Promise.reject("Invalid url");
}
if (
typeof postDataString !== "string" &&
!(postDataString instanceof String)
) {
return Promise.reject("postDataString must be a string");
}
const stringStream = Cc[
"@mozilla.org/io/string-input-stream;1"
].createInstance(Ci.nsIStringInputStream);
stringStream.data = postDataString;
const postData = Cc[
"@mozilla.org/network/mime-input-stream;1"
].createInstance(Ci.nsIMIMEInputStream);
postData.addHeader(
"Content-Type",
postDataContentType || "application/x-www-form-urlencoded"
);
postData.setData(stringStream);
return new Promise(resolve => {
const listener = {
onLocationChange(
browser,
webProgress,
request,
locationURI,
flags
) {
if (
webProgress.isTopLevel &&
browser === tab.browser &&
locationURI.spec === url
) {
windowTracker.removeListener("progress", listener);
resolve();
}
},
};
windowTracker.addListener("progress", listener);
let loadURIOptions = {
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
{}
),
postData,
};
tab.browser.webNavigation.loadURI(url, loadURIOptions);
});
},
async getWebcompatInfo(tabId) {
return new Promise(resolve => {
const messageName = "WebExtension:GetWebcompatInfo";
const code = `${getInfoFrameScript.toString()};getInfoFrameScript("${messageName}")`;
const mm = tabManager.get(tabId).browser.messageManager;
mm.loadFrameScript(`data:,${encodeURI(code)}`, false);
mm.addMessageListener(messageName, function receiveFn(message) {
mm.removeMessageListener(messageName, receiveFn);
resolve(message.json);
});
});
},
},
};
}
};