blob: 0f871ff1d560b17652922aa3a8955a0f24241d58 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { messageTypes } from "@/UserALEWebExtension/messageTypes";
import * as userale from "@/main";
import { browser, configKey } from "@/UserALEWebExtension/globals";
import { Extension, Logging } from "@/types";
// Initalize userale plugin options
const defaultConfig: Extension.ConfigPayload = {
useraleConfig: {
url: "http://localhost:8000",
userId: "pluginUser",
authHeader: null,
toolName: "useralePlugin",
toolVersion: userale.version,
},
pluginConfig: {
// Default to a regex that will match no string
urlWhitelist: "(?!x)x",
},
};
let urlWhitelist: RegExp;
const tabToHttpSession: { [id: string]: any } = {};
let browserSessionId: string | null = null;
/**
* Add log to UserALE buffer for sending to backend
* @param {any} message The message to send
* @return {void}
*/
function addLog(message: any) {
let log = message.payload;
log.browserSessionId = browserSessionId;
// Apply url filter to logs generated outside the background page.
log = filterUrl(log);
if (log) {
userale.log(log);
}
}
/**
* add tab id to http session id mapping
* @param {any} message The message to send
* @param {browser.runtime.MessageSender} sender The sender of the message
* @return {void}
*/
function updateTabToHttpSessionMapping(
message: any,
sender: browser.runtime.MessageSender,
) {
if (sender.tab?.id) {
tabToHttpSession[sender.tab.id] = message.payload;
}
}
/**
* Apply the extension config to both the background and content instances of userale
* @param {Extension.ConfigPayload} config The extension config to apply
* @return {void}
*/
function updateConfig(payload: Extension.ConfigPayload) {
urlWhitelist = new RegExp(payload.pluginConfig.urlWhitelist);
userale.options(payload.useraleConfig);
browser.storage.local.set({ [configKey]: payload });
dispatchTabMessage({
type: messageTypes.CONFIG_CHANGE,
payload: payload.useraleConfig,
});
}
/**
* Send a message to all tabs
* @param {any} message The message to send
* @return {void}
*/
function dispatchTabMessage(message: any) {
// @ts-expect-error Typescript is not aware that firefox's broswer is overloaded
// to support chromium style MV2 callbacks
browser.tabs.query({}, function (tabs: browser.tabs.Tab[]) {
tabs.forEach(function (tab) {
if (!tab.id) return;
browser.tabs.sendMessage(tab.id, message);
});
});
}
/**
* Send a message to the current tab
* @param {any} message The message to send
* @return {void}
*/
function messageCurrentTab(message: any) {
// @ts-expect-error Typescript is not aware that firefox's broswer is overloaded
// to support chromium style MV2 callbacks
browser.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (tabs.length > 0) {
const activeTab = tabs[0];
browser.tabs.sendMessage(activeTab.id, message);
} else {
console.error("No active tab found");
}
});
}
/**
* Callback for filtering out logs with urls that do not match the regex defined in extension options.
* @param {Logging.Log} log The candidate log
* @return {Object} The transformed log
*/
function filterUrl(log: Logging.Log) {
if (urlWhitelist.test(log.pageUrl as string)) {
return log;
}
return false;
}
/**
* Callback for setting the session id's of tab logs to that of the target tab
* @param {Logging.Log} log The candidate log
* @return {Object} The transformed log
*/
function injectSessions(log: Logging.Log) {
const id = (log.details as Logging.JSONObject)?.id as string | undefined;
if (id && id in tabToHttpSession) {
log.httpSessionId = tabToHttpSession[id];
} else {
log.httpSessionId = null;
}
log.browserSessionId = browserSessionId;
return log;
}
// @ts-expect-error Typescript is not aware that firefox's broswer is overloaded
// to support chromium style MV2 callbacks
browser.storage.local.get([configKey], (res) => {
if (res[configKey]) {
updateConfig(res[configKey]);
} else {
updateConfig(defaultConfig);
}
// Apply url filter to logs generated by the background page.
userale.addCallbacks({ filterUrl, injectSessions });
const userAleHttpSessionId = window.sessionStorage.getItem(
"userAleHttpSessionId",
);
browserSessionId = userAleHttpSessionId
? JSON.parse(userAleHttpSessionId)
: null;
});
browser.runtime.onMessage.addListener(function (message, sender) {
switch (message.type) {
case messageTypes.ADD_LOG:
addLog(message);
break;
case messageTypes.HTTP_SESSION:
updateTabToHttpSessionMapping(message, sender);
break;
case messageTypes.CONFIG_CHANGE:
updateConfig(message.payload);
break;
case messageTypes.ISSUE_REPORT:
messageCurrentTab(message);
break;
default:
console.log("got unknown message type ", message);
}
});
/**
* Extract tab details then log a tab event
* @param {integer} tabId The id of the target tab
* @param {Logging.CustomLog} data The data of the tab event
* @param {String} type The type of tab event
* @return {undefined}
*/
function packageTabLog(tabId: number, data: Logging.CustomLog, type: string) {
// @ts-expect-error Typescript is not aware that firefox's broswer is overloaded
// to support chromium style MV2 callbacks
browser.tabs.get(tabId, (tab) => {
packageDetailedTabLog(tab, data, type);
});
}
/**
* Log a tab event with tab details
* @param {browser.tabs.Tab} tab The target tab object
* @param {Logging.CustomLog} data The data of the tab event
* @param {String} type The type of tab event
* @return {undefined}
*/
function packageDetailedTabLog(
tab: Partial<browser.tabs.Tab>,
data: Logging.CustomLog,
type: string,
) {
Object.assign(data, { type });
// Two fields in the Tab interface do not match our serializable JSONObject interface
// remove them before create custom log
const { mutedInfo, sharingState, ...restOfTab } = tab;
const payload: Logging.JSONObject = { ...restOfTab };
userale.packageCustomLog(
data,
() => {
return payload;
},
true,
);
}
// Attach Handlers for tab events
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs
browser.tabs.onActivated.addListener((activeInfo) => {
// Adds needed string indexing signature to payload that to
// make it conform to the JSONObject interface
const payload: Logging.CustomLog = { ...activeInfo };
packageTabLog(activeInfo.tabId, payload, "tabs.onActivated");
});
browser.tabs.onAttached.addListener((tabId, attachInfo) => {
const payload: Logging.CustomLog = { ...attachInfo };
packageTabLog(tabId, payload, "tabs.onAttached");
});
browser.tabs.onCreated.addListener((tab) => {
packageDetailedTabLog(tab, {}, "tabs.onCreated");
});
browser.tabs.onDetached.addListener((tabId, detachInfo) => {
const payload: Logging.CustomLog = { ...detachInfo };
packageTabLog(tabId, payload, "tabs.onDetached");
});
browser.tabs.onMoved.addListener((tabId, moveInfo) => {
const payload: Logging.CustomLog = { ...moveInfo };
packageTabLog(tabId, payload, "tabs.onMoved");
});
browser.tabs.onRemoved.addListener((tabId, removeInfo) => {
const payload: Logging.CustomLog = { ...removeInfo };
packageDetailedTabLog({ id: tabId }, payload, "tabs.onRemoved");
});
browser.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => {
const { mutedInfo, sharingState, ...restOfTab } = changeInfo;
const payload: Logging.JSONObject = { ...restOfTab };
packageDetailedTabLog(tab, payload, "tabs.onUpdated");
});
browser.tabs.onZoomChange.addListener((ZoomChangeInfo) => {
const { zoomSettings, ...restOfTab } = ZoomChangeInfo;
const payload: Logging.CustomLog = { ...restOfTab };
packageTabLog(ZoomChangeInfo.tabId, payload, "tabs.onZoomChange");
});