05/09 Update: make hooks smarter
This commit is contained in:
@@ -18,6 +18,10 @@
|
|||||||
const PK_DROPDOWN_ID = "__pk_dropdown__";
|
const PK_DROPDOWN_ID = "__pk_dropdown__";
|
||||||
const VAULT_URL = "https://pwkeeper.ngodanguyen.tech/vault";
|
const VAULT_URL = "https://pwkeeper.ngodanguyen.tech/vault";
|
||||||
|
|
||||||
|
// Never inject on the PassKeeper vault itself — our own inputs would get decorated.
|
||||||
|
const OWN_ORIGINS = ["pwkeeper.ngodanguyen.tech"];
|
||||||
|
if (OWN_ORIGINS.includes(location.hostname)) return;
|
||||||
|
|
||||||
let _bannerEl = null;
|
let _bannerEl = null;
|
||||||
let _hasNotifiedForm = false;
|
let _hasNotifiedForm = false;
|
||||||
let _formObserver = null;
|
let _formObserver = null;
|
||||||
@@ -107,7 +111,41 @@
|
|||||||
el.getAttribute("aria-label") || "",
|
el.getAttribute("aria-label") || "",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
|
|
||||||
return CRED_HINTS.test(attrs);
|
if (!CRED_HINTS.test(attrs)) return false;
|
||||||
|
|
||||||
|
// Final gate: require a password field to be nearby (same form, or within
|
||||||
|
// 5 ancestor levels) — this prevents hooking standalone search / filter
|
||||||
|
// inputs that happen to carry a name like "user" or "email".
|
||||||
|
return _hasPasswordSibling(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when `el` shares a form (or close ancestor) with at least one
|
||||||
|
* visible password input. This is the key signal that we are on a login form,
|
||||||
|
* not a generic site-search or profile page.
|
||||||
|
*/
|
||||||
|
function _hasPasswordSibling(el) {
|
||||||
|
// 1. Prefer the explicit <form> ancestor.
|
||||||
|
const form =
|
||||||
|
el.closest("form") || el.closest('[role="form"]');
|
||||||
|
if (form) {
|
||||||
|
return !!form.querySelector(
|
||||||
|
'input[type="password"]:not([disabled])',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// 2. No <form>? Walk up to 5 ancestor elements looking for a password input
|
||||||
|
// in any subtree (covers React/Vue apps that render outside <form>).
|
||||||
|
let node = el.parentElement;
|
||||||
|
for (let i = 0; i < 5 && node; i++, node = node.parentElement) {
|
||||||
|
if (node.querySelector('input[type="password"]:not([disabled])')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3. Last resort: any visible password field on the entire page.
|
||||||
|
// Only accept this if there is exactly one password field — avoids
|
||||||
|
// false-positives on complex pages (account settings, checkout, etc.).
|
||||||
|
const pwFields = visiblePasswordFields();
|
||||||
|
return pwFields.length === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findUsernameField(pwField) {
|
function findUsernameField(pwField) {
|
||||||
@@ -827,6 +865,11 @@
|
|||||||
* Pass nothing/undefined to let this function read storage itself.
|
* Pass nothing/undefined to let this function read storage itself.
|
||||||
*/
|
*/
|
||||||
async function decorateFields(knownItems) {
|
async function decorateFields(knownItems) {
|
||||||
|
// Fast-path: if there are no password fields anywhere on the page, there is
|
||||||
|
// nothing to decorate. This prevents false-positive hooks on pages with
|
||||||
|
// standalone text inputs (search bars, filter fields, etc.).
|
||||||
|
if (!visiblePasswordFields().length) return;
|
||||||
|
|
||||||
if (knownItems != null) {
|
if (knownItems != null) {
|
||||||
// Caller supplied pre-filtered items — trust them, skip the storage read.
|
// Caller supplied pre-filtered items — trust them, skip the storage read.
|
||||||
_matchingItems = knownItems;
|
_matchingItems = knownItems;
|
||||||
@@ -1193,30 +1236,44 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Debounce + narrow the MutationObserver: only re-scan when something that
|
||||||
|
// looks like a form input or a whole subtree with inputs was added/removed.
|
||||||
|
// This prevents constant re-scanning on SPA re-renders (tooltip shows,
|
||||||
|
// React state updates, etc.) that don't touch login form elements.
|
||||||
|
var _mutationTimer = null;
|
||||||
_formObserver = new MutationObserver(function (mutations) {
|
_formObserver = new MutationObserver(function (mutations) {
|
||||||
// Ignore mutations caused by the extension's own injected elements
|
// Ignore mutations caused by the extension's own injected elements.
|
||||||
// (dropdown, icon buttons, save banner) to prevent re-decoration loops
|
var hasRelevantChange = mutations.some(function (m) {
|
||||||
// on SPAs that react to every DOM change.
|
// addedNodes contains an <input>, <form>, or a subtree with either.
|
||||||
var ownMutation = mutations.every(function (m) {
|
return Array.from(m.addedNodes).some(function (node) {
|
||||||
return Array.from(m.addedNodes)
|
if (!node || node.nodeType !== 1) return false;
|
||||||
.concat(Array.from(m.removedNodes))
|
// Skip our own injected nodes.
|
||||||
.every(function (node) {
|
var cls = node.className || "";
|
||||||
if (!node || node.nodeType !== 1) return true;
|
var id = node.id || "";
|
||||||
var cls = node.className || "";
|
if (cls.indexOf("__pk") !== -1 || id.indexOf("__pk") !== -1)
|
||||||
var id = node.id || "";
|
return false;
|
||||||
return (
|
// A new <input> or <form> element, or a container with one inside.
|
||||||
cls.indexOf("__pk") !== -1 ||
|
var tag = node.tagName;
|
||||||
id.indexOf("__pk") !== -1 ||
|
if (tag === "INPUT" || tag === "FORM") return true;
|
||||||
(node.querySelector &&
|
if (
|
||||||
(node.querySelector("." + PK_BTN_CLASS) ||
|
node.querySelector &&
|
||||||
node.querySelector("#" + PK_DROPDOWN_ID)))
|
(node.querySelector('input[type="password"]') ||
|
||||||
);
|
node.querySelector("input") ||
|
||||||
});
|
node.querySelector("form"))
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
if (ownMutation) return;
|
if (!hasRelevantChange) return;
|
||||||
_hasNotifiedForm = false;
|
|
||||||
notifyFormDetected();
|
// Debounce: wait 300 ms after the last relevant mutation before scanning.
|
||||||
decorateFields();
|
clearTimeout(_mutationTimer);
|
||||||
|
_mutationTimer = setTimeout(function () {
|
||||||
|
_hasNotifiedForm = false;
|
||||||
|
notifyFormDetected();
|
||||||
|
decorateFields();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
_formObserver.observe(document.body, { childList: true, subtree: true });
|
_formObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user