@protontech/autofill 0.0.22991789

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +1 -0
  2. package/cli.d.ts +2 -0
  3. package/cli.js +128 -0
  4. package/constants/heuristics.d.ts +15 -0
  5. package/constants/heuristics.js +21 -0
  6. package/constants/selectors.d.ts +18 -0
  7. package/constants/selectors.js +52 -0
  8. package/debug.d.ts +1 -0
  9. package/debug.js +17 -0
  10. package/dictionary/generate.d.ts +1 -0
  11. package/dictionary/generate.js +42 -0
  12. package/dictionary/generated/dictionary.d.ts +51 -0
  13. package/dictionary/generated/dictionary.js +51 -0
  14. package/dictionary/source/dictionary.d.ts +11 -0
  15. package/dictionary/source/dictionary.js +375 -0
  16. package/dictionary/source/patterns.d.ts +3 -0
  17. package/dictionary/source/patterns.js +3 -0
  18. package/features/abstract.field.d.ts +123 -0
  19. package/features/abstract.field.js +63 -0
  20. package/features/abstract.form.d.ts +98 -0
  21. package/features/abstract.form.js +281 -0
  22. package/features/field.email.d.ts +18 -0
  23. package/features/field.email.js +43 -0
  24. package/features/field.otp.d.ts +36 -0
  25. package/features/field.otp.js +116 -0
  26. package/features/field.password.d.ts +35 -0
  27. package/features/field.password.js +104 -0
  28. package/features/field.username-hidden.d.ts +15 -0
  29. package/features/field.username-hidden.js +40 -0
  30. package/features/field.username.d.ts +16 -0
  31. package/features/field.username.js +41 -0
  32. package/features/form.combined.d.ts +1 -0
  33. package/features/form.combined.js +6 -0
  34. package/index.d.ts +14 -0
  35. package/index.js +15 -0
  36. package/package.json +29 -0
  37. package/rulesets.d.ts +2 -0
  38. package/rulesets.js +10 -0
  39. package/trainees/field.email.d.ts +2 -0
  40. package/trainees/field.email.js +16 -0
  41. package/trainees/field.identity.d.ts +2 -0
  42. package/trainees/field.identity.js +9 -0
  43. package/trainees/field.otp.d.ts +2 -0
  44. package/trainees/field.otp.js +16 -0
  45. package/trainees/field.password.current.d.ts +2 -0
  46. package/trainees/field.password.current.js +16 -0
  47. package/trainees/field.password.new.d.ts +2 -0
  48. package/trainees/field.password.new.js +16 -0
  49. package/trainees/field.username-hidden.d.ts +2 -0
  50. package/trainees/field.username-hidden.js +22 -0
  51. package/trainees/field.username.d.ts +2 -0
  52. package/trainees/field.username.js +16 -0
  53. package/trainees/form.login.d.ts +2 -0
  54. package/trainees/form.login.js +16 -0
  55. package/trainees/form.noop.d.ts +1 -0
  56. package/trainees/form.noop.js +7 -0
  57. package/trainees/form.password-change.d.ts +2 -0
  58. package/trainees/form.password-change.js +16 -0
  59. package/trainees/form.recovery.d.ts +2 -0
  60. package/trainees/form.recovery.js +16 -0
  61. package/trainees/form.register.d.ts +2 -0
  62. package/trainees/form.register.js +16 -0
  63. package/trainees/index.d.ts +9 -0
  64. package/trainees/index.js +72 -0
  65. package/trainees/results/result.email.d.ts +2 -0
  66. package/trainees/results/result.email.js +17 -0
  67. package/trainees/results/result.login.d.ts +2 -0
  68. package/trainees/results/result.login.js +110 -0
  69. package/trainees/results/result.new-password.d.ts +2 -0
  70. package/trainees/results/result.new-password.js +36 -0
  71. package/trainees/results/result.otp.d.ts +2 -0
  72. package/trainees/results/result.otp.js +43 -0
  73. package/trainees/results/result.password-change.d.ts +2 -0
  74. package/trainees/results/result.password-change.js +110 -0
  75. package/trainees/results/result.password.d.ts +2 -0
  76. package/trainees/results/result.password.js +36 -0
  77. package/trainees/results/result.recovery.d.ts +2 -0
  78. package/trainees/results/result.recovery.js +110 -0
  79. package/trainees/results/result.register.d.ts +2 -0
  80. package/trainees/results/result.register.js +110 -0
  81. package/trainees/results/result.username-hidden.d.ts +2 -0
  82. package/trainees/results/result.username-hidden.js +15 -0
  83. package/trainees/results/result.username.d.ts +2 -0
  84. package/trainees/results/result.username.js +16 -0
  85. package/types/index.d.ts +38 -0
  86. package/types/index.js +20 -0
  87. package/utils/attributes.d.ts +9 -0
  88. package/utils/attributes.js +13 -0
  89. package/utils/clustering.d.ts +1 -0
  90. package/utils/clustering.js +81 -0
  91. package/utils/combinators.d.ts +6 -0
  92. package/utils/combinators.js +4 -0
  93. package/utils/dom.d.ts +25 -0
  94. package/utils/dom.js +104 -0
  95. package/utils/exclusion.d.ts +3 -0
  96. package/utils/exclusion.js +59 -0
  97. package/utils/extract.d.ts +13 -0
  98. package/utils/extract.js +59 -0
  99. package/utils/fathom.d.ts +38 -0
  100. package/utils/fathom.js +68 -0
  101. package/utils/field.d.ts +14 -0
  102. package/utils/field.js +50 -0
  103. package/utils/flags.d.ts +25 -0
  104. package/utils/flags.js +60 -0
  105. package/utils/form.d.ts +7 -0
  106. package/utils/form.js +25 -0
  107. package/utils/identity.d.ts +24 -0
  108. package/utils/identity.js +63 -0
  109. package/utils/memoize.d.ts +5 -0
  110. package/utils/memoize.js +12 -0
  111. package/utils/prepass.d.ts +2 -0
  112. package/utils/prepass.js +31 -0
  113. package/utils/re.d.ts +58 -0
  114. package/utils/re.js +64 -0
  115. package/utils/text.d.ts +3 -0
  116. package/utils/text.js +8 -0
  117. package/utils/visible.d.ts +13 -0
  118. package/utils/visible.js +143 -0
@@ -0,0 +1,38 @@
1
+ import { rule, ruleset } from "@protontech/fathom";
2
+ export type AnyRule = ReturnType<typeof rule>;
3
+ export type Ruleset = ReturnType<typeof ruleset>;
4
+ export type BoundRuleset = ReturnType<Ruleset["against"]>;
5
+ export type Coeff = [string, number];
6
+ export type Bias = [string, number];
7
+ export type RulesetAggregation = {
8
+ rules: AnyRule[];
9
+ coeffs: Coeff[];
10
+ biases: Bias[];
11
+ };
12
+ export type TrainingResults = {
13
+ coeffs: Coeff[];
14
+ bias: number;
15
+ cutoff: number;
16
+ };
17
+ export type Trainee = TrainingResults & {
18
+ name: string;
19
+ getRules: () => AnyRule[];
20
+ };
21
+ export declare enum FormType {
22
+ LOGIN = "login",
23
+ NOOP = "noop",
24
+ PASSWORD_CHANGE = "password-change",
25
+ RECOVERY = "recovery",
26
+ REGISTER = "register"
27
+ }
28
+ export declare enum FieldType {
29
+ EMAIL = "email",
30
+ IDENTITY = "identity",
31
+ OTP = "otp",
32
+ PASSWORD_CURRENT = "password",
33
+ PASSWORD_NEW = "new-password",
34
+ USERNAME = "username",
35
+ USERNAME_HIDDEN = "username-hidden"
36
+ }
37
+ export declare const formTypes: FormType[];
38
+ export declare const fieldTypes: FieldType[];
package/types/index.js ADDED
@@ -0,0 +1,20 @@
1
+ export var FormType;
2
+ (function (FormType) {
3
+ FormType["LOGIN"] = "login";
4
+ FormType["NOOP"] = "noop";
5
+ FormType["PASSWORD_CHANGE"] = "password-change";
6
+ FormType["RECOVERY"] = "recovery";
7
+ FormType["REGISTER"] = "register";
8
+ })(FormType || (FormType = {}));
9
+ export var FieldType;
10
+ (function (FieldType) {
11
+ FieldType["EMAIL"] = "email";
12
+ FieldType["IDENTITY"] = "identity";
13
+ FieldType["OTP"] = "otp";
14
+ FieldType["PASSWORD_CURRENT"] = "password";
15
+ FieldType["PASSWORD_NEW"] = "new-password";
16
+ FieldType["USERNAME"] = "username";
17
+ FieldType["USERNAME_HIDDEN"] = "username-hidden";
18
+ })(FieldType || (FieldType = {}));
19
+ export const formTypes = Object.values(FormType);
20
+ export const fieldTypes = Object.values(FieldType);
@@ -0,0 +1,9 @@
1
+ export declare const TEXT_ATTRIBUTES: string[];
2
+ export declare const EL_ATTRIBUTES: string[];
3
+ export declare const FORM_ATTRIBUTES: string[];
4
+ export declare const FIELD_ATTRIBUTES: string[];
5
+ export declare const getAttributes: (attributes: string[]) => (el: HTMLElement) => string[];
6
+ export declare const getBaseAttributes: (el: HTMLElement) => string[];
7
+ export declare const getTextAttributes: (el: HTMLElement) => string[];
8
+ export declare const getFieldAttributes: (el: HTMLElement) => string[];
9
+ export declare const getFormAttributes: (el: HTMLElement) => string[];
@@ -0,0 +1,13 @@
1
+ import { sanitizeStringWithSpaces } from "./text";
2
+ export const TEXT_ATTRIBUTES = ["title", "label", "aria-label", "aria-labelledby", "aria-describedby", "placeholder", "autocomplete", "legend"];
3
+ export const EL_ATTRIBUTES = ["id", "class", "role", "jsaction", "ng-controller", "data-bind", "ng-model", "v-model", "v-bind", "data-testid", "href"];
4
+ export const FORM_ATTRIBUTES = [EL_ATTRIBUTES, "name", "action"].flat();
5
+ export const FIELD_ATTRIBUTES = [EL_ATTRIBUTES, "name", "inputmode"].flat();
6
+ export const getAttributes = (attributes) => (el) => attributes
7
+ .filter((key) => key !== "data-fathom")
8
+ .map((attr) => el.getAttribute(attr))
9
+ .filter(Boolean).map(sanitizeStringWithSpaces);
10
+ export const getBaseAttributes = getAttributes(EL_ATTRIBUTES);
11
+ export const getTextAttributes = getAttributes(TEXT_ATTRIBUTES);
12
+ export const getFieldAttributes = getAttributes(FIELD_ATTRIBUTES);
13
+ export const getFormAttributes = getAttributes(FORM_ATTRIBUTES);
@@ -0,0 +1 @@
1
+ export declare const resolveFormClusters: (doc: Document) => HTMLElement[];
@@ -0,0 +1,81 @@
1
+ import { clusters as clustering } from "@protontech/fathom";
2
+ import { buttonSelector, inputCandidateSelector, kButtonSubmitSelector, kDomGroupSelector, kFieldSelector } from "../constants/selectors";
3
+ import { findStackedParents, getCommonAncestor, getRectMinDistance, pruneNested, uniqueNodes, walkUpWhile } from "./dom";
4
+ import { isBtnCandidate } from "./field";
5
+ import { isVisibleField } from "./visible";
6
+ import { flagCluster, isClassifiable } from "./flags";
7
+ import { selectFormCandidates } from "./form";
8
+ const { clusters } = clustering;
9
+ const CLUSTER_MAX_X_DIST = 50;
10
+ const CLUSTER_MAX_Y_DIST = 275;
11
+ const CLUSTER_ALIGNMENT_TOLERANCE = 0.05;
12
+ const CLUSTER_MAX_ELEMENTS = 50;
13
+ const context = { cache: new WeakMap() };
14
+ const getElementData = (el) => {
15
+ var _a;
16
+ const data = (_a = context.cache.get(el)) !== null && _a !== void 0 ? _a : {
17
+ isField: el.matches(kFieldSelector) && el.matches(':not([type="submit"])'),
18
+ rect: el.getBoundingClientRect(),
19
+ };
20
+ context.cache.set(el, data);
21
+ return data;
22
+ };
23
+ const compare = (elA, elB) => {
24
+ const a = getElementData(elA);
25
+ const b = getElementData(elB);
26
+ const maxDx = CLUSTER_MAX_X_DIST;
27
+ const maxDy = CLUSTER_MAX_Y_DIST / (a.isField && b.isField ? 2 : 1);
28
+ const { dx, dy } = getRectMinDistance(a.rect, b.rect);
29
+ const leftRatio = Math.abs(a.rect.left / b.rect.left);
30
+ const topRatio = Math.abs(a.rect.top / b.rect.top);
31
+ const xAlign = leftRatio > 1 - CLUSTER_ALIGNMENT_TOLERANCE && leftRatio < 1 + CLUSTER_ALIGNMENT_TOLERANCE;
32
+ const yAlign = topRatio > 1 - CLUSTER_ALIGNMENT_TOLERANCE && topRatio < 1 + CLUSTER_ALIGNMENT_TOLERANCE;
33
+ if (xAlign && yAlign)
34
+ return true;
35
+ if (xAlign && dy < maxDy)
36
+ return true;
37
+ if (yAlign && dx < maxDx)
38
+ return true;
39
+ if (dx < maxDx && dy < maxDy)
40
+ return true;
41
+ return false;
42
+ };
43
+ const handleSingletonCluster = (cluster) => {
44
+ const node = cluster[0];
45
+ return walkUpWhile(node, 5)((_, candidate) => candidate === node || candidate.querySelectorAll(buttonSelector).length === 0);
46
+ };
47
+ export const resolveFormClusters = (doc) => {
48
+ const forms = selectFormCandidates(doc);
49
+ const clusterable = (els) => els.filter((el) => !forms.some((ex) => ex.contains(el)) && isVisibleField(el));
50
+ const fields = Array.from(doc.querySelectorAll(kFieldSelector));
51
+ const fieldsOfInterest = clusterable(fields.filter((el) => isClassifiable(el) && el.getAttribute("type") !== "hidden"));
52
+ const inputs = fieldsOfInterest.filter((field) => field.matches(inputCandidateSelector));
53
+ if (inputs.length === 0 || inputs.length > CLUSTER_MAX_ELEMENTS)
54
+ return [];
55
+ const domGroups = Array.from(doc.querySelectorAll(kDomGroupSelector)).filter((el) => el !== document.body);
56
+ const positionedEls = findStackedParents(inputs, 20);
57
+ const groups = pruneNested(domGroups.filter((el) => !positionedEls.some((stack) => el.contains(stack))).concat(positionedEls));
58
+ const buttons = clusterable(Array.from(document.querySelectorAll(kButtonSubmitSelector)).filter(isBtnCandidate));
59
+ const candidates = uniqueNodes(fieldsOfInterest, buttons);
60
+ if (candidates.length > CLUSTER_MAX_ELEMENTS)
61
+ return [];
62
+ const groupByInput = new WeakMap(candidates.map((el) => [el, groups.find((group) => group.contains(el))]));
63
+ const theClusters = clusters(candidates, 1, (a, b) => {
64
+ if (a.parentElement === b.parentElement)
65
+ return 0;
66
+ const groupA = groupByInput.get(a);
67
+ const groupB = groupByInput.get(b);
68
+ if (groupA !== groupB)
69
+ return Number.MAX_SAFE_INTEGER;
70
+ if (groupA && groupA === groupB)
71
+ return 0;
72
+ return compare(a, b) ? 0 : Number.MAX_SAFE_INTEGER;
73
+ });
74
+ const ancestors = theClusters
75
+ .map((cluster) => (cluster.length === 1 ? handleSingletonCluster(cluster) : cluster.reduce(getCommonAncestor)))
76
+ .filter((ancestor) => document.body !== ancestor && ancestor.querySelectorAll(inputCandidateSelector).length > 0);
77
+ const result = pruneNested(ancestors);
78
+ result.forEach(flagCluster);
79
+ context.cache = new WeakMap();
80
+ return result;
81
+ };
@@ -0,0 +1,6 @@
1
+ type Predicate<T> = (value: T) => boolean;
2
+ export declare const and: <T>(...predicates: Predicate<T>[]) => Predicate<T>;
3
+ export declare const or: <T>(...predicates: Predicate<T>[]) => Predicate<T>;
4
+ export declare const not: <T>(predicate: Predicate<T>) => Predicate<T>;
5
+ export declare const any: <T>(predicate: Predicate<T>) => (values: T[]) => boolean;
6
+ export {};
@@ -0,0 +1,4 @@
1
+ export const and = (...predicates) => (value) => predicates.every((pred) => pred(value));
2
+ export const or = (...predicates) => (value) => predicates.some((pred) => pred(value));
3
+ export const not = (predicate) => (value) => !predicate(value);
4
+ export const any = (predicate) => (values) => values.some(predicate);
package/utils/dom.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ export declare const closestParent: (start: HTMLElement, match: (el: HTMLElement) => boolean) => HTMLElement | null;
2
+ export declare const closest: (start: HTMLElement, match: (parent: HTMLElement) => HTMLElement | null, maxIterations?: number) => HTMLElement | null;
3
+ export declare const walkUpWhile: (start: HTMLElement, maxIterations: number) => (check: (parent: HTMLElement, candidate: HTMLElement) => boolean) => HTMLElement;
4
+ export declare const getNthParent: (el: HTMLElement) => (n: number) => HTMLElement;
5
+ export declare const uniqueNodes: (...nodes: HTMLElement[][]) => HTMLElement[];
6
+ export declare const getNodeRect: (el: HTMLElement) => {
7
+ height: number;
8
+ width: number;
9
+ top: number;
10
+ bottom: number;
11
+ area: number;
12
+ };
13
+ export declare const getSiblingWith: (el: Element, match: (el: Element) => boolean) => Element | null;
14
+ export declare const getLabelFor: (el: HTMLElement) => HTMLElement | null;
15
+ export declare const getRectCenter: (rect: DOMRect) => {
16
+ x: number;
17
+ y: number;
18
+ };
19
+ export declare const getRectMinDistance: (rectA: DOMRect, rectB: DOMRect) => {
20
+ dx: number;
21
+ dy: number;
22
+ };
23
+ export declare const pruneNested: (els: HTMLElement[]) => HTMLElement[];
24
+ export declare const getCommonAncestor: (elementA: HTMLElement, elementB: HTMLElement) => HTMLElement;
25
+ export declare const findStackedParents: (els: HTMLElement[], maxIterations: number) => HTMLElement[];
package/utils/dom.js ADDED
@@ -0,0 +1,104 @@
1
+ export const closestParent = (start, match) => {
2
+ const parent = start.parentElement;
3
+ if (!parent)
4
+ return null;
5
+ return match(parent) ? parent : closestParent(parent, match);
6
+ };
7
+ export const closest = (start, match, maxIterations = 1) => {
8
+ const parent = start === null || start === void 0 ? void 0 : start.parentElement;
9
+ if (!parent)
10
+ return null;
11
+ const result = match(parent);
12
+ return result || maxIterations <= 0 ? result : closest(parent, match, maxIterations - 1);
13
+ };
14
+ export const walkUpWhile = (start, maxIterations) => (check) => {
15
+ const parent = start.parentElement;
16
+ if (maxIterations <= 0 || parent === null)
17
+ return start;
18
+ return check(parent, start) ? walkUpWhile(parent, maxIterations - 1)(check) : start;
19
+ };
20
+ export const getNthParent = (el) => (n) => {
21
+ const parent = el.parentElement;
22
+ return parent === null || n === 0 ? el : getNthParent(parent)(n - 1);
23
+ };
24
+ export const uniqueNodes = (...nodes) => Array.from(new Set(nodes.flat()));
25
+ export const getNodeRect = (el) => {
26
+ const { height, width, top, bottom } = el.getBoundingClientRect();
27
+ const area = height * width;
28
+ return { height, width, top, bottom, area };
29
+ };
30
+ export const getSiblingWith = (el, match) => {
31
+ const prevEl = el.previousElementSibling;
32
+ if (prevEl === null)
33
+ return null;
34
+ if (match(prevEl))
35
+ return prevEl;
36
+ return getSiblingWith(prevEl, match);
37
+ };
38
+ export const getLabelFor = (el) => {
39
+ var _a;
40
+ const forId = (_a = el.getAttribute("id")) !== null && _a !== void 0 ? _a : el.getAttribute("name");
41
+ const label = document.querySelector(`label[for="${forId}"]`);
42
+ if (label)
43
+ return label;
44
+ const parentLabel = el.closest("label");
45
+ if (parentLabel)
46
+ return parentLabel;
47
+ const closestLabel = closest(el, (parent) => parent.querySelector("label, [class*=\"label\"]"), 1);
48
+ if (closestLabel)
49
+ return closestLabel;
50
+ const textNodeAbove = getSiblingWith(el, (el) => el instanceof HTMLElement && el.innerText.trim().length > 0);
51
+ if (textNodeAbove)
52
+ return textNodeAbove;
53
+ return null;
54
+ };
55
+ export const getRectCenter = (rect) => ({
56
+ x: rect.left + rect.width / 2,
57
+ y: rect.top + rect.height / 2,
58
+ });
59
+ export const getRectMinDistance = (rectA, rectB) => {
60
+ const centerA = getRectCenter(rectA);
61
+ const centerB = getRectCenter(rectB);
62
+ const dx = Math.abs(centerA.x - centerB.x) - (rectA.width + rectB.width) / 2;
63
+ const dy = Math.abs(centerA.y - centerB.y) - (rectA.height + rectB.height) / 2;
64
+ return { dx, dy };
65
+ };
66
+ export const pruneNested = (els) => {
67
+ return els.reduce((acc, el) => {
68
+ for (let i = 0; i <= acc.length - 1; i++) {
69
+ if (acc[i] === el)
70
+ continue;
71
+ if (acc[i].contains(el))
72
+ return acc;
73
+ if (el.contains(acc[i])) {
74
+ acc[i] = el;
75
+ return acc;
76
+ }
77
+ }
78
+ acc.push(el);
79
+ return acc;
80
+ }, []);
81
+ };
82
+ export const getCommonAncestor = (elementA, elementB) => {
83
+ if (elementA === elementB)
84
+ return elementA;
85
+ return elementA.contains(elementB) ? elementA : elementA.parentElement ? getCommonAncestor(elementA.parentElement, elementB) : elementA;
86
+ };
87
+ const findStackedParent = (el, cache = [], maxIterations) => {
88
+ if (cache.some((group) => group.contains(el)))
89
+ return null;
90
+ const parent = el.parentElement;
91
+ if (maxIterations === 0 || !parent)
92
+ return null;
93
+ const computedStyle = getComputedStyle(parent);
94
+ const position = computedStyle.getPropertyValue("position");
95
+ if (position === "fixed" || position === "absolute") {
96
+ cache.push(parent);
97
+ return parent;
98
+ }
99
+ return findStackedParent(parent, cache, maxIterations - 1);
100
+ };
101
+ export const findStackedParents = (els, maxIterations) => {
102
+ const cache = [];
103
+ return els.map((input) => findStackedParent(input, cache, maxIterations)).filter((el) => Boolean(el));
104
+ };
@@ -0,0 +1,3 @@
1
+ export declare const excludeForms: (doc?: Document) => void;
2
+ export declare const excludeClusterableNodes: (doc?: Document) => void;
3
+ export declare const excludeFields: (doc?: Document) => void;
@@ -0,0 +1,59 @@
1
+ import { HIDDEN_FIELD_IGNORE_VALUES, MAX_FIELDS_PER_FORM, MAX_HIDDEN_FIELD_VALUE_LENGTH, MAX_INPUTS_PER_FORM, VALID_INPUT_TYPES, } from "../constants/heuristics";
2
+ import { inputCandidateSelector, kEditorSelector, kFieldSelector, kHiddenUsernameSelector } from "../constants/selectors";
3
+ import { getNodeRect } from "../utils/dom";
4
+ import { flagAsIgnored, flagSubtreeAsIgnored, isClassifiable } from "../utils/flags";
5
+ const TABLE_MAX_COLS = 3;
6
+ const TABLE_MAX_AREA = 150000;
7
+ const nodeOfInterest = (el) => isClassifiable(el) && el.querySelector("input") !== null;
8
+ export const excludeForms = (doc = document) => {
9
+ const bodyElCount = document.body.querySelectorAll("*").length;
10
+ return doc.querySelectorAll("form").forEach((form) => {
11
+ if (nodeOfInterest(form)) {
12
+ const fieldCount = form.querySelectorAll(kFieldSelector).length;
13
+ const inputCount = form.querySelectorAll(inputCandidateSelector).length;
14
+ const invalidFieldCount = inputCount === 0 || inputCount > MAX_INPUTS_PER_FORM || fieldCount > MAX_FIELDS_PER_FORM;
15
+ const pageForm = form.matches("body > form");
16
+ const formElCount = form.querySelectorAll("*").length;
17
+ const invalidPageForm = pageForm && formElCount >= bodyElCount * 0.8;
18
+ const invalidCount = invalidFieldCount || invalidPageForm;
19
+ if (invalidCount && !pageForm)
20
+ return flagSubtreeAsIgnored(form);
21
+ if (invalidCount && pageForm)
22
+ return flagAsIgnored(form);
23
+ if (form.matches("table form") && form.closest("table").querySelectorAll("form").length > 2)
24
+ return flagAsIgnored(form);
25
+ }
26
+ });
27
+ };
28
+ export const excludeClusterableNodes = (doc = document) => {
29
+ doc.querySelectorAll("table").forEach((table) => {
30
+ if (nodeOfInterest(table) && !table.querySelector("table") && table.closest("form") === null) {
31
+ const nestedForms = table.querySelectorAll("form");
32
+ if (nestedForms.length > 2)
33
+ return flagSubtreeAsIgnored(table);
34
+ if (!nestedForms.length) {
35
+ if (table.querySelector("thead") !== null)
36
+ return flagSubtreeAsIgnored(table);
37
+ const cellCount = Math.max(...Array.from(table.rows).map((row) => row.cells.length));
38
+ if (cellCount > TABLE_MAX_COLS || getNodeRect(table).area > TABLE_MAX_AREA)
39
+ return flagSubtreeAsIgnored(table);
40
+ }
41
+ }
42
+ });
43
+ doc.querySelectorAll(kEditorSelector).forEach(flagSubtreeAsIgnored);
44
+ };
45
+ export const excludeFields = (doc = document) => {
46
+ doc.querySelectorAll("input").forEach((input) => {
47
+ if (!isClassifiable(input))
48
+ return;
49
+ const invalidType = !VALID_INPUT_TYPES.includes(input.type);
50
+ if (invalidType)
51
+ return flagAsIgnored(input);
52
+ if (input.type === "hidden") {
53
+ const value = input.value.trim();
54
+ const invalidValueLength = !value.length || value.length > MAX_HIDDEN_FIELD_VALUE_LENGTH;
55
+ if (invalidValueLength || HIDDEN_FIELD_IGNORE_VALUES.includes(value) || !input.matches(kHiddenUsernameSelector))
56
+ return flagAsIgnored(input);
57
+ }
58
+ });
59
+ };
@@ -0,0 +1,13 @@
1
+ export declare const getPageDescriptionText: (doc: Document) => string;
2
+ export declare const getNodeText: (node: HTMLElement) => string;
3
+ export declare const getNodeAttributes: (node: HTMLElement) => string;
4
+ export declare const getAllNodeHaystacks: (node: HTMLElement) => string[];
5
+ export declare const getFormText: (form: HTMLElement) => string;
6
+ export declare const getFieldLabelText: (field: HTMLElement) => string;
7
+ export declare const getFieldHaystacks: (field: HTMLElement) => {
8
+ fieldAttrs: string[];
9
+ fieldText: string;
10
+ labelText: string;
11
+ };
12
+ export declare const getAllFieldHaystacks: (field: HTMLElement) => string[];
13
+ export declare const getNearestHeadingsText: (el: HTMLElement) => string;
@@ -0,0 +1,59 @@
1
+ import { getBaseAttributes, getFieldAttributes, getTextAttributes } from "./attributes";
2
+ import { MAX_FORM_HEADING_WALK_UP, MAX_HEADING_HORIZONTAL_DIST, MAX_HEADING_VERTICAL_DIST } from "../constants/heuristics";
3
+ import { kDomGroupSelector, kHeadingSelector } from "../constants/selectors";
4
+ import { getLabelFor, getRectMinDistance, getSiblingWith, walkUpWhile } from "./dom";
5
+ import { sanitizeString, sanitizeStringWithSpaces } from "./text";
6
+ export const getPageDescriptionText = (doc) => {
7
+ var _a;
8
+ const pageTitle = doc.title;
9
+ const metaDescription = doc.querySelector('meta[name="description"]');
10
+ const descriptionContent = (_a = metaDescription === null || metaDescription === void 0 ? void 0 : metaDescription.getAttribute("content")) !== null && _a !== void 0 ? _a : "";
11
+ return sanitizeString(`${pageTitle} ${descriptionContent}`);
12
+ };
13
+ export const getNodeText = (node) => {
14
+ const textAttrs = getTextAttributes(node).join("");
15
+ return sanitizeString(`${node.innerText}${textAttrs}`);
16
+ };
17
+ export const getNodeAttributes = (node) => sanitizeStringWithSpaces(getBaseAttributes(node).join(""));
18
+ export const getAllNodeHaystacks = (node) => [getNodeText(node), getNodeAttributes(node)];
19
+ export const getFormText = (form) => {
20
+ const textAttrs = getTextAttributes(form).join("");
21
+ const fieldsets = Array.from(form.querySelectorAll("fieldset"));
22
+ return sanitizeString(`${textAttrs}${fieldsets.reduce((text, fieldset) => text.concat(getTextAttributes(fieldset).join("")), "")}`);
23
+ };
24
+ export const getFieldLabelText = (field) => {
25
+ const label = getLabelFor(field);
26
+ return label ? getNodeText(label) : "";
27
+ };
28
+ export const getFieldHaystacks = (field) => {
29
+ const isHiddenInput = field instanceof HTMLInputElement && field.type === "hidden";
30
+ const checkLabel = field instanceof HTMLInputElement && ["text", "email", "tel", "password"].includes(field.type);
31
+ const fieldAttrs = getFieldAttributes(field);
32
+ const fieldText = isHiddenInput ? "" : getNodeText(field);
33
+ const labelText = checkLabel && !isHiddenInput ? getFieldLabelText(field) : "";
34
+ return { fieldAttrs, fieldText, labelText };
35
+ };
36
+ export const getAllFieldHaystacks = (field) => {
37
+ const { fieldAttrs, fieldText, labelText } = getFieldHaystacks(field);
38
+ return [fieldText, labelText, ...fieldAttrs];
39
+ };
40
+ export const getNearestHeadingsText = (el) => {
41
+ var _a, _b;
42
+ const originRect = el.getBoundingClientRect();
43
+ const parent = walkUpWhile(el, MAX_FORM_HEADING_WALK_UP)((parentEl, candidate) => {
44
+ if (parentEl === document.body)
45
+ return false;
46
+ if (candidate.matches(kDomGroupSelector))
47
+ return false;
48
+ return true;
49
+ });
50
+ const headings = Array.from(parent.querySelectorAll(kHeadingSelector)).filter((heading) => {
51
+ if (el.contains(heading))
52
+ return true;
53
+ const headingRect = heading.getBoundingClientRect();
54
+ const { dx, dy } = getRectMinDistance(originRect, headingRect);
55
+ return dx < MAX_HEADING_HORIZONTAL_DIST && dy < MAX_HEADING_VERTICAL_DIST;
56
+ });
57
+ const textAbove = (_b = (_a = (headings.length === 0 ? getSiblingWith(el, (el) => el instanceof HTMLElement && el.innerText.trim().length > 0) : null)) === null || _a === void 0 ? void 0 : _a.innerText) !== null && _b !== void 0 ? _b : "";
58
+ return sanitizeString(textAbove + headings.map((el) => el.innerText).join(""));
59
+ };
@@ -0,0 +1,38 @@
1
+ import { Fnode } from "@protontech/fathom";
2
+ import { FormFeatures } from "../features/abstract.form";
3
+ import { EmailFieldFeatures } from "../features/field.email";
4
+ import { OTPFieldFeatures } from "../features/field.otp";
5
+ import { PasswordFieldFeatures } from "../features/field.password";
6
+ import { UsernameFieldFeatures } from "../features/field.username";
7
+ import { HiddenUserFieldFeatures } from "../features/field.username-hidden";
8
+ type FeatureType = "form" | "password-field" | "username-field" | "username-hidden-field" | "email-field" | "otp-field";
9
+ type FeatureKey<T extends FeatureType> = {
10
+ form: FormFeatures;
11
+ "password-field": PasswordFieldFeatures;
12
+ "username-field": UsernameFieldFeatures;
13
+ "username-hidden-field": HiddenUserFieldFeatures;
14
+ "email-field": EmailFieldFeatures;
15
+ "otp-field": OTPFieldFeatures;
16
+ }[T];
17
+ export declare const TOLERANCE_LEVEL = 0.5;
18
+ export declare const boolInt: (val: boolean) => number;
19
+ export declare const safeInt: (val: number, fallback?: number) => number;
20
+ export declare const typeEffect: (type: string) => (fnode: Fnode) => Fnode;
21
+ export declare const processFormEffect: (fnode: Fnode) => Fnode;
22
+ export declare const processFieldEffect: (fnode: Fnode) => Fnode;
23
+ export declare const featureScore: <T extends FeatureType, K = FeatureKey<T>>(noteFor: T, key: keyof K | (keyof K)[]) => any;
24
+ export declare const getParentFormFnode: (fieldFnode: Fnode) => Fnode | null;
25
+ export declare const typeScoreToleranceTest: (type: string) => (fnode: Fnode) => boolean;
26
+ export declare const getTypeScore: (node: Fnode | null, type: string) => any;
27
+ export declare const outRuleWithCache: (candidateType: string, predictionType: string, typeScoreTest?: (predictionType: string) => (fnode: Fnode) => boolean) => any[];
28
+ export declare const combineFeatures: <T>(arr1: T[], arr2: T[]) => [T, T][];
29
+ export declare const withFnodeEl: <T extends HTMLElement>(fn: (el: T) => any) => (fnode: Fnode) => any;
30
+ export declare const getFormClassification: (formFnode: Fnode | null) => {
31
+ login: boolean;
32
+ register: boolean;
33
+ pwChange: boolean;
34
+ recovery: boolean;
35
+ noop: boolean;
36
+ };
37
+ export declare const isNoopForm: (formFnode: Fnode) => boolean;
38
+ export {};
@@ -0,0 +1,68 @@
1
+ import { out, rule, score, type } from "@protontech/fathom";
2
+ import { FormType } from "../types";
3
+ import { flagAsProcessed, getCachedPredictionScore, getParentFormPrediction, isPredictedType, setCachedPredictionScore } from "./flags";
4
+ export const TOLERANCE_LEVEL = 0.5;
5
+ export const boolInt = (val) => Number(val);
6
+ export const safeInt = (val, fallback = 0) => (Number.isFinite(val) ? val : fallback);
7
+ const throughEffect = (effect) => (fnode) => {
8
+ effect(fnode);
9
+ return fnode;
10
+ };
11
+ export const typeEffect = (type) => throughEffect((fnode) => {
12
+ flagAsProcessed(fnode.element);
13
+ if (!fnode.hasNoteFor(`${type}-prediction`)) {
14
+ setCachedPredictionScore(fnode.element, type, getTypeScore(fnode, type));
15
+ }
16
+ });
17
+ export const processFormEffect = throughEffect((fnode) => flagAsProcessed(fnode.element));
18
+ export const processFieldEffect = throughEffect((fnode) => {
19
+ const { visible, type } = fnode.noteFor("field");
20
+ if (visible || type === "hidden")
21
+ flagAsProcessed(fnode.element);
22
+ });
23
+ export const featureScore = (noteFor, key) => score((fnode) => {
24
+ const features = fnode.noteFor(noteFor);
25
+ if (Array.isArray(key))
26
+ return key.map((k) => features[k]).reduce((a, b) => a * b);
27
+ return features[key];
28
+ });
29
+ export const getParentFormFnode = (fieldFnode) => {
30
+ const field = fieldFnode.element;
31
+ const ruleset = fieldFnode._ruleset;
32
+ const parentForms = ruleset.get(type("form"));
33
+ const form = parentForms.find(({ element }) => element.contains(field));
34
+ if (form)
35
+ return form;
36
+ const preDetectedForm = getParentFormPrediction(field);
37
+ if (preDetectedForm)
38
+ return ruleset.get(preDetectedForm);
39
+ return null;
40
+ };
41
+ export const typeScoreToleranceTest = (type) => (fnode) => fnode.scoreFor(type) > TOLERANCE_LEVEL;
42
+ export const getTypeScore = (node, type) => {
43
+ var _a;
44
+ if (!node)
45
+ return 0;
46
+ if (node.hasNoteFor(`${type}-prediction`))
47
+ return (_a = node.noteFor(`${type}-prediction`)) !== null && _a !== void 0 ? _a : 0;
48
+ return node.scoreFor(type);
49
+ };
50
+ export const outRuleWithCache = (candidateType, predictionType, typeScoreTest = typeScoreToleranceTest) => [
51
+ rule(type(candidateType).when(isPredictedType(predictionType)), type(`${predictionType}-prediction`).note(getCachedPredictionScore(predictionType)), {}),
52
+ rule(type(predictionType).when(typeScoreTest(predictionType)), type(`${predictionType}-prediction`), {}),
53
+ rule(type(`${predictionType}-prediction`), out(predictionType).through(typeEffect(predictionType)), {}),
54
+ ];
55
+ export const combineFeatures = (arr1, arr2) => {
56
+ return arr1.flatMap((item1) => arr2.map((item2) => [item1, item2]));
57
+ };
58
+ export const withFnodeEl = (fn) => (fnode) => fn(fnode.element);
59
+ export const getFormClassification = (formFnode) => {
60
+ const login = getTypeScore(formFnode, FormType.LOGIN) > 0.5;
61
+ const register = getTypeScore(formFnode, FormType.REGISTER) > 0.5;
62
+ const pwChange = getTypeScore(formFnode, FormType.PASSWORD_CHANGE) > 0.5;
63
+ const recovery = getTypeScore(formFnode, FormType.RECOVERY) > 0.5;
64
+ const detectionResults = [login, register, pwChange, recovery];
65
+ const noop = detectionResults.every((detected) => !detected);
66
+ return { login, register, pwChange, recovery, noop };
67
+ };
68
+ export const isNoopForm = (formFnode) => getFormClassification(formFnode).noop;
@@ -0,0 +1,14 @@
1
+ import { Fnode } from "@protontech/fathom";
2
+ export declare const splitFieldsByVisibility: (els: HTMLElement[]) => [HTMLElement[], HTMLElement[]];
3
+ export declare const maybeEmail: (value: Fnode) => boolean;
4
+ export declare const maybePassword: (value: Fnode) => boolean;
5
+ export declare const maybeOTP: (value: Fnode) => boolean;
6
+ export declare const maybeUsername: (value: Fnode) => boolean;
7
+ export declare const maybeHiddenUsername: (value: Fnode) => boolean;
8
+ export declare const isUsernameCandidate: (el: HTMLElement) => boolean;
9
+ export declare const isEmailCandidate: (el: HTMLElement) => boolean;
10
+ export declare const isOAuthCandidate: (el: HTMLElement) => boolean;
11
+ export declare const isBtnCandidate: (btn: HTMLElement) => boolean;
12
+ export declare const isProcessableField: (input: HTMLInputElement) => boolean;
13
+ export declare const isClassifiableField: (fnode: Fnode) => boolean;
14
+ export declare const selectInputCandidates: (target?: Document | HTMLElement) => HTMLInputElement[];
package/utils/field.js ADDED
@@ -0,0 +1,50 @@
1
+ import { MIN_AREA_SUBMIT_BTN } from "../constants/heuristics";
2
+ import { inputCandidateSelector, kPasswordSelector, kUsernameSelector, otpSelector } from "../constants/selectors";
3
+ import { and, any, not, or } from "./combinators";
4
+ import { getAllFieldHaystacks } from "./extract";
5
+ import { getParentFormFnode } from "./fathom";
6
+ import { isClassifiable, isHidden, isProcessed } from "./flags";
7
+ import { matchEmail, matchOAuth, matchUsername } from "./re";
8
+ import { isVisible, isVisibleField } from "./visible";
9
+ const isActiveFieldFNode = (fnode) => {
10
+ const { visible, readonly, disabled } = fnode.noteFor("field");
11
+ return visible && !readonly && !disabled;
12
+ };
13
+ export const splitFieldsByVisibility = (els) => {
14
+ return els.reduce((acc, el) => {
15
+ if (isVisibleField(el))
16
+ acc[0].push(el);
17
+ else
18
+ acc[1].push(el);
19
+ return acc;
20
+ }, [[], []]);
21
+ };
22
+ const fType = (types) => (fnode) => types.includes(fnode.element.type);
23
+ const fMatch = (selector) => (fnode) => fnode.element.matches(selector);
24
+ const fInputMode = (inputMode) => (fnode) => fnode.element.inputMode === inputMode;
25
+ const fActive = (fnode) => isActiveFieldFNode(fnode);
26
+ const fList = (fnode) => fnode.element.getAttribute("aria-autocomplete") === "list" || fnode.element.role === "combobox";
27
+ export const maybeEmail = and(not(fList), or(fType(["email", "text"]), fInputMode("email")), fActive);
28
+ export const maybePassword = and(fMatch(kPasswordSelector), fActive);
29
+ export const maybeOTP = and(not(fList), fMatch(otpSelector), fActive);
30
+ export const maybeUsername = and(not(fList), or(and(not(fInputMode("email")), fType(["text", "tel"])), fMatch(kUsernameSelector)), fActive);
31
+ export const maybeHiddenUsername = and(not(fList), fType(["email", "text", "hidden"]), not(fActive));
32
+ export const isUsernameCandidate = (el) => !el.matches('input[type="email"]') && any(matchUsername)(getAllFieldHaystacks(el));
33
+ export const isEmailCandidate = (el) => el.matches('input[type="email"]') || any(matchEmail)(getAllFieldHaystacks(el));
34
+ export const isOAuthCandidate = (el) => any(matchOAuth)(getAllFieldHaystacks(el));
35
+ export const isBtnCandidate = (btn) => {
36
+ if (btn.getAttribute("type") === "submit")
37
+ return true;
38
+ if (btn.innerText.trim().length <= 1)
39
+ return false;
40
+ const height = btn.offsetHeight;
41
+ const width = btn.offsetWidth;
42
+ return height * width > MIN_AREA_SUBMIT_BTN;
43
+ };
44
+ export const isProcessableField = (input) => {
45
+ const processed = isProcessed(input);
46
+ const hidden = isHidden(input);
47
+ return (!processed || hidden) && isVisibleField(input) && isVisible(input, { opacity: false });
48
+ };
49
+ export const isClassifiableField = (fnode) => isClassifiable(fnode.element) && getParentFormFnode(fnode) !== null;
50
+ export const selectInputCandidates = (target = document) => Array.from(target.querySelectorAll(inputCandidateSelector)).filter(isClassifiable);