[PM-5743] Implement eslint rule for usage of window object in background script (#7849)

* [PM-5742] Rework Usage of Extension APIs that Cannot be Called with the Background Service Worker

* [PM-5742] Implementing jest tests for the updated BrowserApi methods

* [PM-5742] Implementing jest tests to validate logic within added API calls

* [PM-5742] Implementing jest tests to validate logic within added API calls

* [PM-5742] Fixing broken Jest tests

* [PM-5742] Fixing linter error

* [PM-5887] Refactor WebCryptoFunction to Remove Usage of the window Object in the Background Script

* [PM-5878] Rework `window` call within OverlayBackground to function within AutofillOverlayIframe service

* [PM-6122] Rework `window` call within NotificationBackground to function within content script

* [PM-5881] Adjust usage of the `chrome.extension.getViews` API to ensure expected behavior in manifest v3

* [PM-5881] Reworking how we handle early returns from `reloadOpenWindows`

* [PM-5881] Implementing jest test to validate changes within BrowserApi.reloadOpenWindows

* [PM-5743] Implement eslint rule to impeede usage of the `window` object in the background script

* [PM-5743] Working through fixing eslint rule errors, and setting up ignore statements for lines that will be refactored at a later date

* [PM-5743] Fixing broken jest tests

* [PM-5879] Removing `backgroundWindow` reference used for determing system theme preference in Safari

* [PM-5879] Removing `backgroundWindow` reference used for determing system theme preference in Safari

* [PM-5743] Updating references to NodeJS.Timeout

* [PM-5743] Adding notification bar and overaly content scripts to the eslint excluded files key

* [PM-5743] Adding other excluded files from the eslint rule

* [PM-5743] Reworking implementation to have the .eslintrc.json file present within the browser subdirectory
This commit is contained in:
Cesar Gonzalez 2024-03-29 10:55:23 -05:00 committed by GitHub
parent 9d1219bda6
commit 670f33daa8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 79 additions and 55 deletions

View File

@ -0,0 +1,26 @@
{
"env": {
"browser": true,
"webextensions": true
},
"overrides": [
{
"files": ["src/**/*.ts"],
"excludedFiles": [
"src/**/{content,popup,spec}/**/*.ts",
"src/**/autofill/{notification,overlay}/**/*.ts",
"src/**/autofill/**/{autofill-overlay-content,collect-autofill-content,dom-element-visibility,insert-autofill-content}.service.ts",
"src/**/*.spec.ts"
],
"rules": {
"no-restricted-globals": [
"error",
{
"name": "window",
"message": "The `window` object is not available in service workers and may not be available within the background script. Consider using `self`, `globalThis`, or another global property instead."
}
]
}
}
]
}

View File

@ -17,7 +17,7 @@ export default class WebRequestBackground {
private authService: AuthService,
) {
if (BrowserApi.isManifestVersion(2)) {
this.webRequest = (window as any).chrome.webRequest;
this.webRequest = chrome.webRequest;
}
this.isFirefox = platformUtilsService.isFirefox();
}

View File

@ -10,7 +10,7 @@ function loadAutofiller() {
let pageHref: string = null;
let filledThisHref = false;
let delayFillTimeout: number;
let doFillInterval: NodeJS.Timeout;
let doFillInterval: number | NodeJS.Timeout;
const handleExtensionDisconnect = () => {
clearDoFillInterval();
clearDelayFillTimeout();

View File

@ -139,7 +139,7 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
});
});
window.addEventListener("resize", adjustHeight);
globalThis.addEventListener("resize", adjustHeight);
adjustHeight();
}
@ -384,7 +384,7 @@ function setupLogoLink(i18n: Record<string, string>) {
function setNotificationBarTheme() {
let theme = notificationBarIframeInitData.theme;
if (theme === ThemeType.System) {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
theme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches
? ThemeType.Dark
: ThemeType.Light;
}
@ -393,5 +393,5 @@ function setNotificationBarTheme() {
}
function postMessageToParent(message: NotificationBarWindowMessage) {
window.parent.postMessage(message, windowMessageOrigin || "*");
globalThis.parent.postMessage(message, windowMessageOrigin || "*");
}

View File

@ -211,7 +211,7 @@ class AutofillOverlayIframeService implements AutofillOverlayIframeServiceInterf
let borderColor: string;
let verifiedTheme = theme;
if (verifiedTheme === ThemeType.System) {
verifiedTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
verifiedTheme = globalThis.matchMedia("(prefers-color-scheme: dark)").matches
? ThemeType.Dark
: ThemeType.Light;
}

View File

@ -19,7 +19,7 @@ class AutofillOverlayList extends AutofillOverlayPageElement {
private ciphers: OverlayCipherData[] = [];
private ciphersList: HTMLUListElement;
private cipherListScrollIsDebounced = false;
private cipherListScrollDebounceTimeout: NodeJS.Timeout;
private cipherListScrollDebounceTimeout: number | NodeJS.Timeout;
private currentCipherIndex = 0;
private readonly showCiphersPerPage = 6;
private readonly overlayListWindowMessageHandlers: OverlayListWindowMessageHandlers = {

View File

@ -15,7 +15,7 @@ export interface PageDetail {
export interface AutoFillOptions {
cipher: CipherView;
pageDetails: PageDetail[];
doc?: typeof window.document;
doc?: typeof self.document;
tab: chrome.tabs.Tab;
skipUsernameOnlyFill?: boolean;
onlyEmptyFields?: boolean;

View File

@ -712,7 +712,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
private async getBoundingClientRectFromIntersectionObserver(
formFieldElement: ElementWithOpId<FormFieldElement>,
): Promise<DOMRectReadOnly | null> {
if (!("IntersectionObserver" in window) && !("IntersectionObserverEntry" in window)) {
if (!("IntersectionObserver" in globalThis) && !("IntersectionObserverEntry" in globalThis)) {
return null;
}
@ -901,7 +901,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte
if (
this.focusedFieldData.focusedFieldRects?.top > 0 &&
this.focusedFieldData.focusedFieldRects?.top < window.innerHeight
this.focusedFieldData.focusedFieldRects?.top < globalThis.innerHeight
) {
return;
}

View File

@ -42,7 +42,7 @@ import {
export default class AutofillService implements AutofillServiceInterface {
private openVaultItemPasswordRepromptPopout = openVaultItemPasswordRepromptPopout;
private openPasswordRepromptPopoutDebounce: NodeJS.Timeout;
private openPasswordRepromptPopoutDebounce: number | NodeJS.Timeout;
private currentlyOpeningPasswordRepromptPopout = false;
private autofillScriptPortsSet = new Set<chrome.runtime.Port>();
static searchFieldNamesSet = new Set(AutoFillConstants.SearchFieldNames);

View File

@ -39,7 +39,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
private autofillFieldElements: AutofillFieldElements = new Map();
private currentLocationHref = "";
private mutationObserver: MutationObserver;
private updateAutofillElementsAfterMutationTimeout: NodeJS.Timeout;
private updateAutofillElementsAfterMutationTimeout: number | NodeJS.Timeout;
private readonly updateAfterMutationTimeoutDelay = 1000;
private readonly ignoredInputTypes = new Set([
"hidden",
@ -180,7 +180,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
): AutofillPageDetails {
return {
title: document.title,
url: (document.defaultView || window).location.href,
url: (document.defaultView || globalThis).location.href,
documentUrl: document.location.href,
forms: autofillFormsData,
fields: autofillFieldsData,
@ -240,7 +240,7 @@ class CollectAutofillContentService implements CollectAutofillContentServiceInte
* @private
*/
private getFormActionAttribute(element: ElementWithOpId<HTMLFormElement>): string {
return new URL(this.getPropertyOrAttribute(element, "action"), window.location.href).href;
return new URL(this.getPropertyOrAttribute(element, "action"), globalThis.location.href).href;
}
/**

View File

@ -66,7 +66,7 @@ class DomElementVisibilityService implements domElementVisibilityServiceInterfac
*/
private getElementStyle(element: HTMLElement, styleProperty: string): string {
if (!this.cachedComputedStyle) {
this.cachedComputedStyle = (element.ownerDocument.defaultView || window).getComputedStyle(
this.cachedComputedStyle = (element.ownerDocument.defaultView || globalThis).getComputedStyle(
element,
);
}

View File

@ -47,7 +47,7 @@ const initEventCount = Object.freeze(
);
let confirmSpy: jest.SpyInstance<boolean, [message?: string]>;
let windowSpy: jest.SpyInstance<any>;
let windowLocationSpy: jest.SpyInstance<any>;
let savedURLs: string[] | null = ["https://bitwarden.com"];
function setMockWindowLocation({
protocol,
@ -56,11 +56,9 @@ function setMockWindowLocation({
protocol: "http:" | "https:";
hostname: string;
}) {
windowSpy.mockImplementation(() => ({
location: {
protocol,
hostname,
},
windowLocationSpy.mockImplementation(() => ({
protocol,
hostname,
}));
}
@ -76,8 +74,8 @@ describe("InsertAutofillContentService", () => {
beforeEach(() => {
document.body.innerHTML = mockLoginForm;
confirmSpy = jest.spyOn(window, "confirm");
windowSpy = jest.spyOn(window, "window", "get");
confirmSpy = jest.spyOn(globalThis, "confirm");
windowLocationSpy = jest.spyOn(globalThis, "location", "get");
insertAutofillContentService = new InsertAutofillContentService(
domElementVisibilityService,
collectAutofillContentService,
@ -101,7 +99,7 @@ describe("InsertAutofillContentService", () => {
afterEach(() => {
jest.resetAllMocks();
windowSpy.mockRestore();
windowLocationSpy.mockRestore();
confirmSpy.mockRestore();
document.body.innerHTML = "";
});
@ -245,8 +243,8 @@ describe("InsertAutofillContentService", () => {
});
it("returns true if the frameElement has a sandbox attribute", () => {
Object.defineProperty(globalThis, "window", {
value: { frameElement: { hasAttribute: jest.fn(() => true) } },
Object.defineProperty(globalThis, "frameElement", {
value: { hasAttribute: jest.fn(() => true) },
writable: true,
});
@ -991,11 +989,11 @@ describe("InsertAutofillContentService", () => {
const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement;
inputElement.value = "test";
jest.spyOn(inputElement, "focus");
jest.spyOn(window, "String");
jest.spyOn(globalThis, "String");
insertAutofillContentService["triggerFocusOnElement"](inputElement, true);
expect(window.String).toHaveBeenCalledWith(value);
expect(globalThis.String).toHaveBeenCalledWith(value);
expect(inputElement.focus).toHaveBeenCalled();
expect(inputElement.value).toEqual(value);
});
@ -1005,11 +1003,11 @@ describe("InsertAutofillContentService", () => {
const inputElement = document.querySelector('input[type="text"]') as HTMLInputElement;
inputElement.value = "test";
jest.spyOn(inputElement, "focus");
jest.spyOn(window, "String");
jest.spyOn(globalThis, "String");
insertAutofillContentService["triggerFocusOnElement"](inputElement, false);
expect(window.String).not.toHaveBeenCalledWith();
expect(globalThis.String).not.toHaveBeenCalledWith();
expect(inputElement.focus).toHaveBeenCalled();
expect(inputElement.value).toEqual(value);
});

View File

@ -65,8 +65,8 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
private fillingWithinSandboxedIframe() {
return (
String(self.origin).toLowerCase() === "null" ||
window.frameElement?.hasAttribute("sandbox") ||
window.location.hostname === ""
globalThis.frameElement?.hasAttribute("sandbox") ||
globalThis.location.hostname === ""
);
}
@ -79,8 +79,8 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
*/
private userCancelledInsecureUrlAutofill(savedUrls?: string[] | null): boolean {
if (
!savedUrls?.some((url) => url.startsWith(`https://${window.location.hostname}`)) ||
window.location.protocol !== "http:" ||
!savedUrls?.some((url) => url.startsWith(`https://${globalThis.location.hostname}`)) ||
globalThis.location.protocol !== "http:" ||
!this.isPasswordFieldWithinDocument()
) {
return false;
@ -88,10 +88,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
const confirmationWarning = [
chrome.i18n.getMessage("insecurePageWarning"),
chrome.i18n.getMessage("insecurePageWarningFillPrompt", [window.location.hostname]),
chrome.i18n.getMessage("insecurePageWarningFillPrompt", [globalThis.location.hostname]),
].join("\n\n");
return !confirm(confirmationWarning);
return !globalThis.confirm(confirmationWarning);
}
/**
@ -129,10 +129,10 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf
const confirmationWarning = [
chrome.i18n.getMessage("autofillIframeWarning"),
chrome.i18n.getMessage("autofillIframeWarningTip", [window.location.hostname]),
chrome.i18n.getMessage("autofillIframeWarningTip", [globalThis.location.hostname]),
].join("\n\n");
return !confirm(confirmationWarning);
return !globalThis.confirm(confirmationWarning);
}
/**

View File

@ -11,7 +11,7 @@ const IdleInterval = 60 * 5; // 5 minutes
export default class IdleBackground {
private idle: typeof chrome.idle | typeof browser.idle | null;
private idleTimer: number = null;
private idleTimer: number | NodeJS.Timeout = null;
private idleState = "active";
constructor(
@ -73,7 +73,7 @@ export default class IdleBackground {
private pollIdle(handler: (newState: string) => void) {
if (this.idleTimer != null) {
window.clearTimeout(this.idleTimer);
globalThis.clearTimeout(this.idleTimer);
this.idleTimer = null;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@ -83,7 +83,7 @@ export default class IdleBackground {
this.idleState = state;
handler(state);
}
this.idleTimer = window.setTimeout(() => this.pollIdle(handler), 5000);
this.idleTimer = globalThis.setTimeout(() => this.pollIdle(handler), 5000);
});
}
}

View File

@ -991,7 +991,7 @@ export default class MainBackground {
}
async bootstrap() {
this.containerService.attachToGlobal(window);
this.containerService.attachToGlobal(self);
await this.stateService.init();

View File

@ -89,7 +89,7 @@ export default class RuntimeBackground {
BrowserApi.messageListener("runtime.background", backgroundMessageListener);
if (this.main.popupOnlyContext) {
(window as any).bitwardenBackgroundMessageListener = backgroundMessageListener;
(self as any).bitwardenBackgroundMessageListener = backgroundMessageListener;
}
}

View File

@ -33,7 +33,7 @@ if (BrowserApi.isManifestVersion(3)) {
},
);
} else {
const bitwardenMain = ((window as any).bitwardenMain = new MainBackground());
const bitwardenMain = ((self as any).bitwardenMain = new MainBackground());
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
bitwardenMain.bootstrap().then(() => {

View File

@ -351,11 +351,11 @@ export class BrowserApi {
private static setupUnloadListeners() {
// The MDN recommend using 'visibilitychange' but that event is fired any time the popup window is obscured as well
// 'pagehide' works just like 'unload' but is compatible with the back/forward cache, so we prefer using that one
window.onpagehide = () => {
self.addEventListener("pagehide", () => {
for (const [event, callback] of BrowserApi.trackedChromeEventListeners) {
event.removeListener(callback);
}
};
});
}
static sendMessage(subscriber: string, arg: any = {}) {
@ -423,7 +423,7 @@ export class BrowserApi {
return;
}
const currentHref = window.location.href;
const currentHref = self.location.href;
views
.filter((w) => w.location.href != null && !w.location.href.includes("background.html"))
.filter((w) => !exemptCurrentHref || w.location.href !== currentHref)

View File

@ -29,14 +29,14 @@ class OffscreenDocument implements OffscreenDocumentInterface {
* @param message - The extension message containing the text to copy
*/
private async handleOffscreenCopyToClipboard(message: OffscreenDocumentExtensionMessage) {
await BrowserClipboardService.copy(window, message.text);
await BrowserClipboardService.copy(self, message.text);
}
/**
* Reads the user's clipboard and returns the text.
*/
private async handleOffscreenReadFromClipboard() {
return await BrowserClipboardService.read(window);
return await BrowserClipboardService.read(self);
}
/**

View File

@ -5,8 +5,8 @@ import { FileDownloadRequest } from "@bitwarden/common/platform/abstractions/fil
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SafariApp } from "../../browser/safariApp";
import { BrowserApi } from "../browser/browser-api";
import { SafariApp } from "../../../browser/safariApp";
import { BrowserApi } from "../../browser/browser-api";
@Injectable()
export class BrowserFileDownloadService implements FileDownloadService {

View File

@ -3,6 +3,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService {
send(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
(window as any).bitwardenPopupMainMessageListener(message);
(self as any).bitwardenPopupMainMessageListener(message);
}
}

View File

@ -3,6 +3,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
export default class BrowserMessagingPrivateModePopupService implements MessagingService {
send(subscriber: string, arg: any = {}) {
const message = Object.assign({}, { command: subscriber }, arg);
(window as any).bitwardenBackgroundMessageListener(message);
(self as any).bitwardenBackgroundMessageListener(message);
}
}

View File

@ -140,7 +140,7 @@ export class AppComponent implements OnInit, OnDestroy {
}
};
(window as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener;
(self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener;
this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener);
// eslint-disable-next-line rxjs/no-async-subscribe

View File

@ -92,9 +92,9 @@ import MainBackground from "../../background/main.background";
import { Account } from "../../models/account";
import { BrowserApi } from "../../platform/browser/browser-api";
import BrowserPopupUtils from "../../platform/popup/browser-popup-utils";
import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service";
import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service";
import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service";
import { BrowserFileDownloadService } from "../../platform/services/browser-file-download.service";
import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service";
import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service";
import BrowserMessagingService from "../../platform/services/browser-messaging.service";