677 lines
20 KiB
JavaScript
677 lines
20 KiB
JavaScript
const ext = globalThis.browser ?? globalThis.chrome;
|
|
const isNodeTestEnv = typeof module !== "undefined" && module.exports;
|
|
const usePromiseAPI = typeof globalThis.browser !== "undefined";
|
|
|
|
function runtimeSend(message) {
|
|
if (usePromiseAPI) {
|
|
return ext.runtime.sendMessage(message);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
ext.runtime.sendMessage(message, (response) => {
|
|
const error = ext.runtime.lastError;
|
|
if (error) {
|
|
reject(new Error(error.message));
|
|
return;
|
|
}
|
|
resolve(response);
|
|
});
|
|
});
|
|
}
|
|
|
|
function isVisibleInput(input) {
|
|
if (!(input instanceof HTMLInputElement)) {
|
|
return false;
|
|
}
|
|
if (input.disabled || input.readOnly) {
|
|
return false;
|
|
}
|
|
const style = window.getComputedStyle(input);
|
|
if (style.display === "none" || style.visibility === "hidden") {
|
|
return false;
|
|
}
|
|
return input.offsetParent !== null || style.position === "fixed";
|
|
}
|
|
|
|
function normalizeRole(rawRole) {
|
|
switch (String(rawRole || "").trim().toLowerCase()) {
|
|
case "password":
|
|
return "password";
|
|
default:
|
|
return "username";
|
|
}
|
|
}
|
|
|
|
function describeFieldRole(input) {
|
|
const type = String(input?.getAttribute?.("type") || "").toLowerCase();
|
|
if (type === "password") {
|
|
return "password";
|
|
}
|
|
const autocomplete = String(input?.autocomplete || "").toLowerCase();
|
|
if (autocomplete.includes("username") || autocomplete.includes("email")) {
|
|
return "username";
|
|
}
|
|
return "username";
|
|
}
|
|
|
|
function isUsernameCandidate(input) {
|
|
if (!isVisibleInput(input)) {
|
|
return false;
|
|
}
|
|
return describeFieldRole(input) === "username";
|
|
}
|
|
|
|
function isPasswordCandidate(input) {
|
|
return isVisibleInput(input) && describeFieldRole(input) === "password";
|
|
}
|
|
|
|
function dispatchFillEvents(input) {
|
|
if (typeof InputEvent === "function") {
|
|
input.dispatchEvent(new InputEvent("input", { bubbles: true, data: input.value, inputType: "insertText" }));
|
|
} else {
|
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
}
|
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
}
|
|
|
|
function setInputValue(input, value) {
|
|
const prototype = Object.getPrototypeOf(input);
|
|
const descriptor = prototype ? Object.getOwnPropertyDescriptor(prototype, "value") : null;
|
|
if (descriptor?.set) {
|
|
descriptor.set.call(input, value);
|
|
return;
|
|
}
|
|
input.value = value;
|
|
}
|
|
|
|
function visibleInputs(scope) {
|
|
return Array.from(scope.querySelectorAll("input")).filter(isVisibleInput);
|
|
}
|
|
|
|
function resolveFormInputs(anchorInput) {
|
|
if (anchorInput?.form instanceof HTMLFormElement) {
|
|
return visibleInputs(anchorInput.form);
|
|
}
|
|
return visibleInputs(document);
|
|
}
|
|
|
|
function firstVisiblePassword(scope) {
|
|
return visibleInputs(scope).find(isPasswordCandidate) || null;
|
|
}
|
|
|
|
function firstVisibleUsername(scope) {
|
|
return visibleInputs(scope).find(isUsernameCandidate) || null;
|
|
}
|
|
|
|
function associatedFieldsForAnchor(anchorInput) {
|
|
const scopeInputs = resolveFormInputs(anchorInput);
|
|
const passwordInput = scopeInputs.find(isPasswordCandidate) || firstVisiblePassword(document);
|
|
const usernameInScope = scopeInputs.filter(isUsernameCandidate);
|
|
let usernameInput = usernameInScope[0] || null;
|
|
if (passwordInput && usernameInScope.length !== 0) {
|
|
const priorSibling = usernameInScope.find((input) =>
|
|
typeof input.compareDocumentPosition === "function" &&
|
|
Boolean(input.compareDocumentPosition(passwordInput) & Node.DOCUMENT_POSITION_FOLLOWING)
|
|
);
|
|
usernameInput = priorSibling || usernameInScope[0] || null;
|
|
}
|
|
if (!usernameInput) {
|
|
usernameInput = firstVisibleUsername(document);
|
|
}
|
|
return { usernameInput, passwordInput };
|
|
}
|
|
|
|
function buildFieldDescriptor(input, role) {
|
|
if (!(input instanceof HTMLInputElement)) {
|
|
return null;
|
|
}
|
|
const normalizedRole = normalizeRole(role || describeFieldRole(input));
|
|
const form = input.form instanceof HTMLFormElement ? input.form : null;
|
|
const scope = form || document;
|
|
const inputs = visibleInputs(scope);
|
|
const fieldIndex = inputs.indexOf(input);
|
|
const forms = Array.from(document.forms || []);
|
|
return {
|
|
role: normalizedRole,
|
|
formIndex: form ? forms.indexOf(form) : -1,
|
|
fieldIndex,
|
|
id: String(input.id || ""),
|
|
name: String(input.name || ""),
|
|
autocomplete: String(input.autocomplete || "").toLowerCase()
|
|
};
|
|
}
|
|
|
|
function resolveFieldDescriptor(descriptor) {
|
|
if (!descriptor || typeof descriptor !== "object") {
|
|
return null;
|
|
}
|
|
const normalizedRole = normalizeRole(descriptor.role);
|
|
const forms = Array.from(document.forms || []);
|
|
const form = Number.isInteger(descriptor.formIndex) && descriptor.formIndex >= 0 ? forms[descriptor.formIndex] || null : null;
|
|
const scope = form || document;
|
|
const inputs = visibleInputs(scope);
|
|
if (Number.isInteger(descriptor.fieldIndex) && descriptor.fieldIndex >= 0 && descriptor.fieldIndex < inputs.length) {
|
|
const candidate = inputs[descriptor.fieldIndex];
|
|
if (describeFieldRole(candidate) === normalizedRole) {
|
|
return candidate;
|
|
}
|
|
}
|
|
if (descriptor.id) {
|
|
const byID = scope.querySelector(`#${CSS.escape(descriptor.id)}`);
|
|
if (byID instanceof HTMLInputElement && isVisibleInput(byID) && describeFieldRole(byID) === normalizedRole) {
|
|
return byID;
|
|
}
|
|
}
|
|
if (descriptor.name) {
|
|
const byName = visibleInputs(scope).find((input) => input.name === descriptor.name && describeFieldRole(input) === normalizedRole);
|
|
if (byName) {
|
|
return byName;
|
|
}
|
|
}
|
|
if (normalizedRole === "password") {
|
|
return firstVisiblePassword(scope) || firstVisiblePassword(document);
|
|
}
|
|
return firstVisibleUsername(scope) || firstVisibleUsername(document);
|
|
}
|
|
|
|
function chooseFillTargets(targetDescriptor) {
|
|
const anchorInput = resolveFieldDescriptor(targetDescriptor) || (document.activeElement instanceof HTMLInputElement ? document.activeElement : null);
|
|
const associated = associatedFieldsForAnchor(anchorInput);
|
|
if (normalizeRole(targetDescriptor?.role) === "password" && anchorInput instanceof HTMLInputElement) {
|
|
return {
|
|
usernameInput: associated.usernameInput,
|
|
passwordInput: isPasswordCandidate(anchorInput) ? anchorInput : associated.passwordInput,
|
|
anchorInput
|
|
};
|
|
}
|
|
if (normalizeRole(targetDescriptor?.role) === "username" && anchorInput instanceof HTMLInputElement) {
|
|
return {
|
|
usernameInput: isUsernameCandidate(anchorInput) ? anchorInput : associated.usernameInput,
|
|
passwordInput: associated.passwordInput,
|
|
anchorInput
|
|
};
|
|
}
|
|
return {
|
|
usernameInput: associated.usernameInput,
|
|
passwordInput: associated.passwordInput,
|
|
anchorInput: anchorInput || associated.passwordInput || associated.usernameInput || null
|
|
};
|
|
}
|
|
|
|
function scanLoginFields() {
|
|
const activeElement = document.activeElement instanceof HTMLInputElement ? document.activeElement : null;
|
|
const activeUsable = activeElement && isVisibleInput(activeElement) ? activeElement : null;
|
|
const targets = chooseFillTargets(buildFieldDescriptor(activeUsable, describeFieldRole(activeUsable)));
|
|
const anchorInput = activeUsable || targets.passwordInput || targets.usernameInput;
|
|
const focusTarget = buildFieldDescriptor(anchorInput, describeFieldRole(anchorInput));
|
|
const allVisible = visibleInputs(document);
|
|
const roles = allVisible
|
|
.filter((input) => isUsernameCandidate(input) || isPasswordCandidate(input))
|
|
.map((input) => {
|
|
const descriptor = buildFieldDescriptor(input, describeFieldRole(input));
|
|
return `${descriptor.formIndex}:${descriptor.fieldIndex}:${descriptor.role}`;
|
|
});
|
|
return {
|
|
pageHasLoginForm: Boolean(targets.usernameInput || targets.passwordInput),
|
|
usernameInput: targets.usernameInput,
|
|
passwordInput: targets.passwordInput,
|
|
anchorInput,
|
|
focusTarget,
|
|
signature: roles.join("|")
|
|
};
|
|
}
|
|
|
|
function fillCredential(credential, targetDescriptor) {
|
|
const { passwordInput, usernameInput } = chooseFillTargets(targetDescriptor);
|
|
|
|
if (usernameInput && credential.username) {
|
|
usernameInput.focus();
|
|
setInputValue(usernameInput, credential.username);
|
|
dispatchFillEvents(usernameInput);
|
|
}
|
|
if (passwordInput && credential.password) {
|
|
passwordInput.focus();
|
|
setInputValue(passwordInput, credential.password);
|
|
dispatchFillEvents(passwordInput);
|
|
}
|
|
|
|
if (!usernameInput && !passwordInput) {
|
|
return { ok: false, error: "No fillable username or password fields were found." };
|
|
}
|
|
return { ok: true };
|
|
}
|
|
|
|
function domainLabel(rawURL) {
|
|
try {
|
|
return new URL(rawURL).host || "";
|
|
} catch (_error) {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function inlineMatchSummary(match) {
|
|
const parts = [];
|
|
if (match.username) {
|
|
parts.push(match.username);
|
|
}
|
|
if (match.url) {
|
|
const host = domainLabel(match.url);
|
|
if (host) {
|
|
parts.push(host);
|
|
}
|
|
}
|
|
if (Array.isArray(match.path) && match.path.length !== 0) {
|
|
parts.push(match.path.join(" / "));
|
|
}
|
|
return parts.join(" · ") || "No username";
|
|
}
|
|
|
|
function shouldShowInlineOverlay(state, hasTarget, suppressed) {
|
|
if (suppressed || !hasTarget) {
|
|
return false;
|
|
}
|
|
return Boolean(
|
|
state?.pageHasLoginForm &&
|
|
(
|
|
state?.pendingFill ||
|
|
(state?.configured && state?.success && !state?.status?.locked && Array.isArray(state?.matches) && state.matches.length > 0)
|
|
)
|
|
);
|
|
}
|
|
|
|
const contentTestExports = {
|
|
normalizeRole,
|
|
describeFieldRole,
|
|
buildFieldDescriptor,
|
|
resolveFieldDescriptor,
|
|
chooseFillTargets,
|
|
inlineMatchSummary,
|
|
domainLabel,
|
|
shouldShowInlineOverlay
|
|
};
|
|
|
|
if (isNodeTestEnv) {
|
|
module.exports = contentTestExports;
|
|
} else {
|
|
let pageState = {
|
|
configured: true,
|
|
success: true,
|
|
matches: [],
|
|
pageHasLoginForm: false,
|
|
pendingFill: false,
|
|
error: "",
|
|
focusTarget: null
|
|
};
|
|
let chooserOpen = false;
|
|
let inlineSuppressed = false;
|
|
let refreshTimer = null;
|
|
let lastReportedSignature = "";
|
|
let lastReportedTarget = "";
|
|
|
|
const root = document.createElement("div");
|
|
root.id = "keepassgo-inline-root";
|
|
root.setAttribute("aria-live", "polite");
|
|
const shadow = root.attachShadow({ mode: "open" });
|
|
shadow.innerHTML = `
|
|
<style>
|
|
:host {
|
|
all: initial;
|
|
}
|
|
.dock {
|
|
position: fixed;
|
|
z-index: 2147483647;
|
|
display: none;
|
|
min-width: 220px;
|
|
max-width: min(340px, calc(100vw - 24px));
|
|
font: 13px/1.35 "Noto Sans", "Liberation Sans", sans-serif;
|
|
color: #214f44;
|
|
}
|
|
.dock[data-open="true"] .panel {
|
|
display: block;
|
|
}
|
|
.trigger {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 8px 12px;
|
|
border: 1px solid #c6d8cf;
|
|
border-radius: 999px;
|
|
background: linear-gradient(180deg, #ffffff, #edf5f0);
|
|
color: #214f44;
|
|
box-shadow: 0 12px 26px rgba(33, 79, 68, 0.16);
|
|
cursor: pointer;
|
|
}
|
|
.trigger[data-tone="warning"] {
|
|
border-color: #e4d0ae;
|
|
background: linear-gradient(180deg, #fff8ed, #f9edd6);
|
|
color: #7f4b09;
|
|
}
|
|
.trigger[data-tone="error"] {
|
|
border-color: #e4bcbc;
|
|
background: linear-gradient(180deg, #fff5f5, #f9e7e7);
|
|
color: #8c2f2f;
|
|
}
|
|
.brand {
|
|
font-weight: 700;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
.meta {
|
|
color: #4d6d66;
|
|
font-size: 12px;
|
|
}
|
|
.panel {
|
|
display: none;
|
|
margin-top: 8px;
|
|
border: 1px solid #d7e3dc;
|
|
border-radius: 16px;
|
|
background: #fffdfa;
|
|
box-shadow: 0 18px 42px rgba(33, 79, 68, 0.22);
|
|
overflow: hidden;
|
|
}
|
|
.panel-header {
|
|
padding: 12px 14px 10px;
|
|
border-bottom: 1px solid #e6efea;
|
|
background: linear-gradient(180deg, #f8fbf8, #f1f6f3);
|
|
}
|
|
.panel-title {
|
|
font-weight: 700;
|
|
}
|
|
.panel-copy {
|
|
margin-top: 4px;
|
|
color: #4d6d66;
|
|
font-size: 12px;
|
|
}
|
|
.match-list {
|
|
display: grid;
|
|
gap: 0;
|
|
max-height: 280px;
|
|
overflow: auto;
|
|
}
|
|
.match {
|
|
display: grid;
|
|
gap: 3px;
|
|
width: 100%;
|
|
padding: 12px 14px;
|
|
border: 0;
|
|
border-top: 1px solid #eef4f0;
|
|
background: #fffdfa;
|
|
color: #214f44;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.match:hover,
|
|
.match:focus-visible {
|
|
background: #edf5f0;
|
|
outline: none;
|
|
}
|
|
.match strong {
|
|
font-size: 13px;
|
|
}
|
|
.pill {
|
|
display: inline-flex;
|
|
width: fit-content;
|
|
padding: 2px 6px;
|
|
border-radius: 999px;
|
|
background: #e7f1eb;
|
|
color: #255f4a;
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
.subtle {
|
|
color: #4d6d66;
|
|
font-size: 12px;
|
|
}
|
|
.empty {
|
|
padding: 12px 14px;
|
|
color: #4d6d66;
|
|
font-size: 12px;
|
|
}
|
|
</style>
|
|
<div class="dock" data-open="false">
|
|
<button type="button" class="trigger" data-tone="ready">
|
|
<span class="brand">KeePassGO</span>
|
|
<span class="meta">Checking this form</span>
|
|
</button>
|
|
<div class="panel">
|
|
<div class="panel-header">
|
|
<div class="panel-title">KeePassGO suggestions</div>
|
|
<div class="panel-copy">Select a matching login for this field.</div>
|
|
</div>
|
|
<div class="match-list"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const dock = shadow.querySelector(".dock");
|
|
const trigger = shadow.querySelector(".trigger");
|
|
const meta = shadow.querySelector(".meta");
|
|
const matchList = shadow.querySelector(".match-list");
|
|
const panelCopy = shadow.querySelector(".panel-copy");
|
|
|
|
function ensureRootMounted() {
|
|
if (!root.isConnected) {
|
|
document.documentElement.appendChild(root);
|
|
}
|
|
}
|
|
|
|
function currentTarget() {
|
|
return chooseFillTargets(pageState.focusTarget).anchorInput;
|
|
}
|
|
|
|
function hideDock() {
|
|
chooserOpen = false;
|
|
dock.style.display = "none";
|
|
dock.dataset.open = "false";
|
|
}
|
|
|
|
function positionDock() {
|
|
const anchor = currentTarget();
|
|
if (!anchor || dock.style.display === "none") {
|
|
return;
|
|
}
|
|
const rect = anchor.getBoundingClientRect();
|
|
const width = Math.min(340, Math.max(220, rect.width));
|
|
const left = Math.min(window.innerWidth - width - 12, Math.max(12, rect.left));
|
|
const top = Math.min(window.innerHeight - 16, rect.bottom + 8);
|
|
dock.style.left = `${left}px`;
|
|
dock.style.top = `${top}px`;
|
|
dock.style.width = `${width}px`;
|
|
}
|
|
|
|
function renderMatches() {
|
|
matchList.textContent = "";
|
|
if (pageState.pendingFill) {
|
|
const pending = document.createElement("div");
|
|
pending.className = "empty";
|
|
pending.textContent = pageState.pendingMessage || "Approve or deny the fill request in KeePassGO.";
|
|
matchList.appendChild(pending);
|
|
return;
|
|
}
|
|
if (!Array.isArray(pageState.matches) || pageState.matches.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "empty";
|
|
empty.textContent = pageState.error || "No matching entries were found for this page.";
|
|
matchList.appendChild(empty);
|
|
return;
|
|
}
|
|
|
|
for (const match of pageState.matches) {
|
|
const row = document.createElement("button");
|
|
row.type = "button";
|
|
row.className = "match";
|
|
const title = document.createElement("strong");
|
|
title.textContent = match.title;
|
|
const summary = document.createElement("span");
|
|
summary.className = "subtle";
|
|
summary.textContent = inlineMatchSummary(match);
|
|
const quality = document.createElement("span");
|
|
quality.className = "pill";
|
|
quality.textContent = match.quality || "Candidate";
|
|
row.appendChild(title);
|
|
row.appendChild(summary);
|
|
row.appendChild(quality);
|
|
row.addEventListener("click", async () => {
|
|
row.disabled = true;
|
|
inlineSuppressed = true;
|
|
hideDock();
|
|
try {
|
|
await runtimeSend({
|
|
type: "keepassgo-fill-entry",
|
|
entryId: match.id,
|
|
target: pageState.focusTarget
|
|
});
|
|
} catch (_error) {
|
|
pageState = {
|
|
...pageState,
|
|
pendingFill: false,
|
|
error: "KeePassGO could not fill this page."
|
|
};
|
|
renderInlineState();
|
|
} finally {
|
|
row.disabled = false;
|
|
}
|
|
});
|
|
matchList.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function renderInlineState() {
|
|
const target = currentTarget();
|
|
const shouldShow = shouldShowInlineOverlay(pageState, Boolean(target), inlineSuppressed);
|
|
|
|
if (!shouldShow) {
|
|
hideDock();
|
|
return;
|
|
}
|
|
|
|
ensureRootMounted();
|
|
dock.style.display = "block";
|
|
trigger.dataset.tone = pageState.pendingFill ? "warning" : (pageState.error ? "error" : "ready");
|
|
if (pageState.pendingFill) {
|
|
meta.textContent = "Approval needed in KeePassGO";
|
|
panelCopy.textContent = pageState.pendingMessage || "Approve or deny the fill request in KeePassGO.";
|
|
} else {
|
|
const count = Array.isArray(pageState.matches) ? pageState.matches.length : 0;
|
|
meta.textContent = count === 1 ? "1 login ready" : `${count} logins ready`;
|
|
panelCopy.textContent = "Select a matching login for this field.";
|
|
}
|
|
dock.dataset.open = chooserOpen ? "true" : "false";
|
|
renderMatches();
|
|
positionDock();
|
|
}
|
|
|
|
function reportFieldState(force) {
|
|
const scan = scanLoginFields();
|
|
pageState = {
|
|
...pageState,
|
|
pageHasLoginForm: scan.pageHasLoginForm,
|
|
focusTarget: scan.focusTarget
|
|
};
|
|
renderInlineState();
|
|
const nextTarget = JSON.stringify(scan.focusTarget || null);
|
|
if (!force && scan.signature === lastReportedSignature && nextTarget === lastReportedTarget) {
|
|
return;
|
|
}
|
|
lastReportedSignature = scan.signature;
|
|
lastReportedTarget = nextTarget;
|
|
void runtimeSend({
|
|
type: "keepassgo-page-ready",
|
|
force: Boolean(force),
|
|
pageHasLoginForm: scan.pageHasLoginForm,
|
|
focusTarget: scan.focusTarget,
|
|
signature: scan.signature
|
|
}).then((response) => {
|
|
if (response && typeof response === "object" && !("success" in response && response.success === false)) {
|
|
pageState = {
|
|
...pageState,
|
|
...response,
|
|
pageHasLoginForm: Boolean(response.pageHasLoginForm),
|
|
focusTarget: response.focusTarget || pageState.focusTarget
|
|
};
|
|
renderInlineState();
|
|
}
|
|
}).catch(() => null);
|
|
}
|
|
|
|
function scheduleRefresh(force) {
|
|
if (refreshTimer !== null) {
|
|
clearTimeout(refreshTimer);
|
|
}
|
|
refreshTimer = window.setTimeout(() => {
|
|
refreshTimer = null;
|
|
reportFieldState(force);
|
|
}, force ? 0 : 120);
|
|
}
|
|
|
|
trigger.addEventListener("click", () => {
|
|
chooserOpen = !chooserOpen;
|
|
renderInlineState();
|
|
});
|
|
|
|
document.addEventListener("focusin", () => {
|
|
scheduleRefresh(false);
|
|
});
|
|
|
|
document.addEventListener("input", () => {
|
|
scheduleRefresh(false);
|
|
}, true);
|
|
|
|
document.addEventListener("click", (event) => {
|
|
if (!root.contains(event.target)) {
|
|
chooserOpen = false;
|
|
renderInlineState();
|
|
}
|
|
});
|
|
|
|
window.addEventListener("scroll", () => {
|
|
positionDock();
|
|
}, true);
|
|
|
|
window.addEventListener("resize", () => {
|
|
positionDock();
|
|
});
|
|
|
|
const observer = new MutationObserver((records) => {
|
|
if (records.some((record) => record.type === "childList")) {
|
|
scheduleRefresh(false);
|
|
}
|
|
});
|
|
|
|
observer.observe(document.documentElement, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
|
|
ext.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
if (message?.type === "keepassgo-fill-credential") {
|
|
try {
|
|
sendResponse(fillCredential(message.credential || {}, message.target || pageState.focusTarget));
|
|
} catch (error) {
|
|
sendResponse({ ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
return false;
|
|
}
|
|
if (message?.type === "keepassgo-page-state") {
|
|
pageState = {
|
|
...pageState,
|
|
...(message.state || {})
|
|
};
|
|
renderInlineState();
|
|
sendResponse({ ok: true });
|
|
return false;
|
|
}
|
|
if (message?.type === "keepassgo-page-scan") {
|
|
const scan = scanLoginFields();
|
|
sendResponse({
|
|
pageHasLoginForm: scan.pageHasLoginForm,
|
|
focusTarget: scan.focusTarget,
|
|
signature: scan.signature
|
|
});
|
|
return false;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
reportFieldState(true);
|
|
}
|