-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathindex.ts
544 lines (484 loc) · 16.2 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
import { AutofillPort } from "../enums/autofill-port.enum";
import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types";
/**
* Generates a random string of characters.
*
* @param length - The length of the random string to generate.
*/
export function generateRandomChars(length: number): string {
const chars = "abcdefghijklmnopqrstuvwxyz";
const randomChars = [];
const randomBytes = new Uint8Array(length);
globalThis.crypto.getRandomValues(randomBytes);
for (let byteIndex = 0; byteIndex < randomBytes.length; byteIndex++) {
const byte = randomBytes[byteIndex];
randomChars.push(chars[byte % chars.length]);
}
return randomChars.join("");
}
/**
* Polyfills the requestIdleCallback API with a setTimeout fallback.
*
* @param callback - The callback function to run when the browser is idle.
* @param options - The options to pass to the requestIdleCallback function.
*/
export function requestIdleCallbackPolyfill(
callback: () => void,
options?: Record<string, any>,
): number | NodeJS.Timeout {
if ("requestIdleCallback" in globalThis) {
return globalThis.requestIdleCallback(() => callback(), options);
}
return globalThis.setTimeout(() => callback(), 1);
}
/**
* Polyfills the cancelIdleCallback API with a clearTimeout fallback.
*
* @param id - The ID of the idle callback to cancel.
*/
export function cancelIdleCallbackPolyfill(id: NodeJS.Timeout | number) {
if ("cancelIdleCallback" in globalThis) {
return globalThis.cancelIdleCallback(id as number);
}
return globalThis.clearTimeout(id);
}
/**
* Generates a random string of characters that formatted as a custom element name.
*/
export function generateRandomCustomElementName(): string {
const length = Math.floor(Math.random() * 5) + 8; // Between 8 and 12 characters
const numHyphens = Math.min(Math.max(Math.floor(Math.random() * 4), 1), length - 1); // At least 1, maximum of 3 hyphens
const hyphenIndices: number[] = [];
while (hyphenIndices.length < numHyphens) {
const index = Math.floor(Math.random() * (length - 1)) + 1;
if (!hyphenIndices.includes(index)) {
hyphenIndices.push(index);
}
}
hyphenIndices.sort((a, b) => a - b);
let randomString = "";
let prevIndex = 0;
for (let index = 0; index < hyphenIndices.length; index++) {
const hyphenIndex = hyphenIndices[index];
randomString = randomString + generateRandomChars(hyphenIndex - prevIndex) + "-";
prevIndex = hyphenIndex;
}
randomString += generateRandomChars(length - prevIndex);
return randomString;
}
/**
* Builds a DOM element from an SVG string.
*
* @param svgString - The SVG string to build the DOM element from.
* @param ariaHidden - Determines whether the SVG should be hidden from screen readers.
*/
export function buildSvgDomElement(svgString: string, ariaHidden = true): HTMLElement {
const domParser = new DOMParser();
const svgDom = domParser.parseFromString(svgString, "image/svg+xml");
const domElement = svgDom.documentElement;
domElement.setAttribute("aria-hidden", `${ariaHidden}`);
return domElement;
}
/**
* Sends a message to the extension.
*
* @param command - The command to send.
* @param options - The options to send with the command.
*/
export async function sendExtensionMessage(
command: string,
options: Record<string, any> = {},
): Promise<any> {
if (
typeof browser !== "undefined" &&
typeof browser.runtime !== "undefined" &&
typeof browser.runtime.sendMessage !== "undefined"
) {
return browser.runtime.sendMessage({ command, ...options });
}
return new Promise((resolve) =>
chrome.runtime.sendMessage(Object.assign({ command }, options), (response) => {
if (chrome.runtime.lastError) {
resolve(null);
}
resolve(response);
}),
);
}
/**
* Sets CSS styles on an element.
*
* @param element - The element to set the styles on.
* @param styles - The styles to set on the element.
* @param priority - Determines whether the styles should be set as important.
*/
export function setElementStyles(
element: HTMLElement,
styles: Partial<CSSStyleDeclaration>,
priority?: boolean,
) {
if (!element || !styles || !Object.keys(styles).length) {
return;
}
for (const styleProperty in styles) {
element.style.setProperty(
styleProperty.replace(/([a-z])([A-Z])/g, "$1-$2"), // Convert camelCase to kebab-case
styles[styleProperty],
priority ? "important" : undefined,
);
}
}
/**
* Sets up a long-lived connection with the extension background
* and triggers an onDisconnect event if the extension context
* is invalidated.
*
* @param callback - Callback export function to run when the extension disconnects
*/
export function setupExtensionDisconnectAction(callback: (port: chrome.runtime.Port) => void) {
const port = chrome.runtime.connect({ name: AutofillPort.InjectedScript });
const onDisconnectCallback = (disconnectedPort: chrome.runtime.Port) => {
callback(disconnectedPort);
port.onDisconnect.removeListener(onDisconnectCallback);
};
port.onDisconnect.addListener(onDisconnectCallback);
}
/**
* Handles setup of the extension disconnect action for the autofill init class
* in both instances where the overlay might or might not be initialized.
*
* @param windowContext - The global window context
*/
export function setupAutofillInitDisconnectAction(windowContext: Window) {
if (!windowContext.bitwardenAutofillInit) {
return;
}
const onDisconnectCallback = () => {
windowContext.bitwardenAutofillInit.destroy();
delete windowContext.bitwardenAutofillInit;
};
setupExtensionDisconnectAction(onDisconnectCallback);
}
/**
* Identifies whether an element is a fillable form field.
* This is determined by whether the element is a form field and not a span.
*
* @param formFieldElement - The form field element to check.
*/
export function elementIsFillableFormField(
formFieldElement: FormFieldElement,
): formFieldElement is FillableFormFieldElement {
return !elementIsSpanElement(formFieldElement);
}
/**
* Identifies whether an element is an instance of a specific tag name.
*
* @param element - The element to check.
* @param tagName - The tag name to check against.
*/
export function elementIsInstanceOf<T extends Element>(
element: Element,
tagName: string,
): element is T {
return nodeIsElement(element) && element.tagName.toLowerCase() === tagName;
}
/**
* Identifies whether an element is a span element.
*
* @param element - The element to check.
*/
export function elementIsSpanElement(element: Element): element is HTMLSpanElement {
return elementIsInstanceOf<HTMLSpanElement>(element, "span");
}
/**
* Identifies whether an element is an input field.
*
* @param element - The element to check.
*/
export function elementIsInputElement(element: Element): element is HTMLInputElement {
return elementIsInstanceOf<HTMLInputElement>(element, "input");
}
/**
* Identifies whether an element is a select field.
*
* @param element - The element to check.
*/
export function elementIsSelectElement(element: Element): element is HTMLSelectElement {
return elementIsInstanceOf<HTMLSelectElement>(element, "select");
}
/**
* Identifies whether an element is a textarea field.
*
* @param element - The element to check.
*/
export function elementIsTextAreaElement(element: Element): element is HTMLTextAreaElement {
return elementIsInstanceOf<HTMLTextAreaElement>(element, "textarea");
}
/**
* Identifies whether an element is a form element.
*
* @param element - The element to check.
*/
export function elementIsFormElement(element: Element): element is HTMLFormElement {
return elementIsInstanceOf<HTMLFormElement>(element, "form");
}
/**
* Identifies whether an element is a label element.
*
* @param element - The element to check.
*/
export function elementIsLabelElement(element: Element): element is HTMLLabelElement {
return elementIsInstanceOf<HTMLLabelElement>(element, "label");
}
/**
* Identifies whether an element is a description details `dd` element.
*
* @param element - The element to check.
*/
export function elementIsDescriptionDetailsElement(element: Element): element is HTMLElement {
return elementIsInstanceOf<HTMLElement>(element, "dd");
}
/**
* Identifies whether an element is a description term `dt` element.
*
* @param element - The element to check.
*/
export function elementIsDescriptionTermElement(element: Element): element is HTMLElement {
return elementIsInstanceOf<HTMLElement>(element, "dt");
}
/**
* Identifies whether a node is an HTML element.
*
* @param node - The node to check.
*/
export function nodeIsElement(node: Node): node is Element {
if (!node) {
return false;
}
return node?.nodeType === Node.ELEMENT_NODE;
}
/**
* Identifies whether a node is an input element.
*
* @param node - The node to check.
*/
export function nodeIsInputElement(node: Node): node is HTMLInputElement {
return nodeIsElement(node) && elementIsInputElement(node);
}
/**
* Identifies whether a node is a form element.
*
* @param node - The node to check.
*/
export function nodeIsFormElement(node: Node): node is HTMLFormElement {
return nodeIsElement(node) && elementIsFormElement(node);
}
export function nodeIsTypeSubmitElement(node: Node): node is HTMLElement {
return nodeIsElement(node) && getPropertyOrAttribute(node as HTMLElement, "type") === "submit";
}
export function nodeIsButtonElement(node: Node): node is HTMLButtonElement {
return (
nodeIsElement(node) &&
(elementIsInstanceOf<HTMLButtonElement>(node, "button") ||
getPropertyOrAttribute(node as HTMLElement, "type") === "button")
);
}
export function nodeIsAnchorElement(node: Node): node is HTMLAnchorElement {
return nodeIsElement(node) && elementIsInstanceOf<HTMLAnchorElement>(node, "a");
}
/**
* Returns a boolean representing the attribute value of an element.
*
* @param element
* @param attributeName
* @param checkString
*/
export function getAttributeBoolean(
element: HTMLElement,
attributeName: string,
checkString = false,
): boolean {
if (checkString) {
return getPropertyOrAttribute(element, attributeName) === "true";
}
return Boolean(getPropertyOrAttribute(element, attributeName));
}
/**
* Get the value of a property or attribute from a FormFieldElement.
*
* @param element
* @param attributeName
*/
export function getPropertyOrAttribute(element: HTMLElement, attributeName: string): string | null {
if (attributeName in element) {
return (element as FormElementWithAttribute)[attributeName];
}
return element.getAttribute(attributeName);
}
/**
* Throttles a callback function to run at most once every `limit` milliseconds.
*
* @param callback - The callback function to throttle.
* @param limit - The time in milliseconds to throttle the callback.
*/
export function throttle(callback: (_args: any) => any, limit: number) {
let waitingDelay = false;
return function (...args: unknown[]) {
if (!waitingDelay) {
callback.apply(this, args);
waitingDelay = true;
globalThis.setTimeout(() => (waitingDelay = false), limit);
}
};
}
/**
* Debounces a callback function to run after a delay of `delay` milliseconds.
*
* @param callback - The callback function to debounce.
* @param delay - The time in milliseconds to debounce the callback.
* @param immediate - Determines whether the callback should run immediately.
*/
export function debounce(callback: (_args: any) => any, delay: number, immediate?: boolean) {
let timeout: NodeJS.Timeout;
return function (...args: unknown[]) {
const callImmediately = !!immediate && !timeout;
if (timeout) {
globalThis.clearTimeout(timeout);
}
timeout = globalThis.setTimeout(() => {
timeout = null;
if (!callImmediately) {
callback.apply(this, args);
}
}, delay);
if (callImmediately) {
callback.apply(this, args);
}
};
}
/**
* Gathers and normalizes keywords from a potential submit button element. Used
* to verify if the element submits a login or change password form.
*
* @param element - The element to gather keywords from.
*/
export function getSubmitButtonKeywordsSet(element: HTMLElement): Set<string> {
const keywords = [
element.textContent,
element.getAttribute("type"),
element.getAttribute("value"),
element.getAttribute("aria-label"),
element.getAttribute("aria-labelledby"),
element.getAttribute("aria-describedby"),
element.getAttribute("title"),
element.getAttribute("id"),
element.getAttribute("name"),
element.getAttribute("class"),
];
const keywordsSet = new Set<string>();
for (let i = 0; i < keywords.length; i++) {
if (typeof keywords[i] === "string") {
// Iterate over all keywords metadata and split them by non-letter characters.
// This ensures we check against individual words and not the entire string.
keywords[i]
.toLowerCase()
.replace(/[-\s]/g, "")
.split(/[^\p{L}]+/gu)
.forEach((keyword) => {
if (keyword) {
keywordsSet.add(keyword);
}
});
}
}
return keywordsSet;
}
/**
* Generates the origin and subdomain match patterns for the URL.
*
* @param url - The URL of the tab
*/
export function generateDomainMatchPatterns(url: string): string[] {
try {
const extensionUrlPattern =
/^(chrome|chrome-extension|moz-extension|safari-web-extension):\/\/\/?/;
if (extensionUrlPattern.test(url)) {
return [];
}
// Add protocol to URL if it is missing to allow for parsing the hostname correctly
const urlPattern = /^(https?|file):\/\/\/?/;
if (!urlPattern.test(url)) {
url = `https://${url}`;
}
let protocolGlob = "*://";
if (url.startsWith("file:///")) {
protocolGlob = "*:///"; // File URLs require three slashes to be a valid match pattern
}
const parsedUrl = new URL(url);
const originMatchPattern = `${protocolGlob}${parsedUrl.hostname}/*`;
const splitHost = parsedUrl.hostname.split(".");
const domain = splitHost.slice(-2).join(".");
const subDomainMatchPattern = `${protocolGlob}*.${domain}/*`;
return [originMatchPattern, subDomainMatchPattern];
} catch {
return [];
}
}
/**
* Determines if the status code of the web response is invalid. An invalid status code is
* any status code that is not in the 200-299 range.
*
* @param statusCode - The status code of the web response
*/
export function isInvalidResponseStatusCode(statusCode: number) {
return statusCode < 200 || statusCode >= 300;
}
/**
* Determines if the current context is within a sandboxed iframe.
*/
export function currentlyInSandboxedIframe(): boolean {
return (
String(self.origin).toLowerCase() === "null" ||
globalThis.frameElement?.hasAttribute("sandbox") ||
globalThis.location.hostname === ""
);
}
/**
* This object allows us to map a special character to a key name. The key name is used
* in gathering the i18n translation of the written version of the special character.
*/
export const specialCharacterToKeyMap: Record<string, string> = {
" ": "spaceCharacterDescriptor",
"~": "tildeCharacterDescriptor",
"`": "backtickCharacterDescriptor",
"!": "exclamationCharacterDescriptor",
"@": "atSignCharacterDescriptor",
"#": "hashSignCharacterDescriptor",
$: "dollarSignCharacterDescriptor",
"%": "percentSignCharacterDescriptor",
"^": "caretCharacterDescriptor",
"&": "ampersandCharacterDescriptor",
"*": "asteriskCharacterDescriptor",
"(": "parenLeftCharacterDescriptor",
")": "parenRightCharacterDescriptor",
"-": "hyphenCharacterDescriptor",
_: "underscoreCharacterDescriptor",
"+": "plusCharacterDescriptor",
"=": "equalsCharacterDescriptor",
"{": "braceLeftCharacterDescriptor",
"}": "braceRightCharacterDescriptor",
"[": "bracketLeftCharacterDescriptor",
"]": "bracketRightCharacterDescriptor",
"|": "pipeCharacterDescriptor",
"\\": "backSlashCharacterDescriptor",
":": "colonCharacterDescriptor",
";": "semicolonCharacterDescriptor",
'"': "doubleQuoteCharacterDescriptor",
"'": "singleQuoteCharacterDescriptor",
"<": "lessThanCharacterDescriptor",
">": "greaterThanCharacterDescriptor",
",": "commaCharacterDescriptor",
".": "periodCharacterDescriptor",
"?": "questionCharacterDescriptor",
"/": "forwardSlashCharacterDescriptor",
};