Files
account-onboarding/autofill.py
2026-10-06 23:43:48 -07:00

291 lines
10 KiB
Python

#!/usr/bin/env python3
"""
autofill.py — real-time account signup form filler.
Opens a signup page, scans every visible form field, matches it against your
identity profile, and fills each matched field live (highlighted, one at a
time) while you watch. It NEVER auto-submits by default — you review and hit
submit yourself.
Usage:
python3 autofill.py --url https://example.com/signup
python3 autofill.py --url https://example.com/signup --headless --dump # test mode
python3 autofill.py --url https://example.com/signup --submit # fill + click submit
Profile: autofill_profile.json (your real info; you fill in the blanks).
"""
import argparse
import json
import os
import re
from playwright.sync_api import sync_playwright
BASE = os.path.dirname(os.path.abspath(__file__))
PROFILE_PATH = os.path.join(BASE, "autofill_profile.json")
# Standardized HTML autocomplete attribute -> profile key (highest confidence).
AUTOCOMPLETE_MAP = {
"given-name": "first_name",
"family-name": "last_name",
"name": "full_name",
"email": "email",
"tel": "phone",
"tel-national": "phone",
"tel-local": "phone",
"street-address": "address_line1",
"address-line1": "address_line1",
"address-line2": "address_line2",
"address-level1": "state",
"address-level2": "city",
"postal-code": "zip",
"bday": "dob",
"organization": "employer",
"organization-title": "employer",
}
# Fallback keyword -> profile key, matched against name/id/label/placeholder.
# Deliberately avoids greedy tokens like bare "address", "first", "last".
KEYWORD_MAP = {
"first_name": ["firstname", "givenname", "fname"],
"last_name": ["lastname", "surname", "familyname", "lname"],
"email": ["emailaddress", "email"],
"phone": ["phonenumber", "phonenum", "telephone", "mobile", "cellphone", "phone"],
"address_line1": ["streetaddress", "addressline1", "address1", "mailingaddress", "street"],
"address_line2": ["addressline2", "address2", "suite", "apt", "unit"],
"city": ["city", "town", "municipality"],
"state": ["stateprovince", "province", "state"],
"zip": ["zipcode", "postalcode", "zip", "postal"],
"dob": ["dateofbirth", "birthdate", "birthday", "dob"],
"ssn": [
"socialsecuritynumber", "socialsecurity", "ssnnumber", "ssn",
"taxpayerid", "taxid", "tin", "nationalid",
],
"employer": ["currentemployer", "employername", "employer", "companyname"],
"income": ["annualincome", "incomeamount", "income", "salary"],
}
# US state abbreviation -> full name, for matching <select> options.
STATE_NAMES = {
"AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas",
"CA": "California", "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware",
"FL": "Florida", "GA": "Georgia", "HI": "Hawaii", "ID": "Idaho",
"IL": "Illinois", "IN": "Indiana", "IA": "Iowa", "KS": "Kansas",
"KY": "Kentucky", "LA": "Louisiana", "ME": "Maine", "MD": "Maryland",
"MA": "Massachusetts", "MI": "Michigan", "MN": "Minnesota", "MS": "Mississippi",
"MO": "Missouri", "MT": "Montana", "NE": "Nebraska", "NV": "Nevada",
"NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", "NY": "New York",
"NC": "North Carolina", "ND": "North Dakota", "OH": "Ohio", "OK": "Oklahoma",
"OR": "Oregon", "PA": "Pennsylvania", "RI": "Rhode Island", "SC": "South Carolina",
"SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", "UT": "Utah",
"VT": "Vermont", "VA": "Virginia", "WA": "Washington", "WV": "West Virginia",
"WI": "Wisconsin", "WY": "Wyoming", "DC": "District of Columbia",
}
PASSWORD_LIKE = re.compile(r"password|passwd|passcode|pwd|confirmpassword", re.I)
def norm(s):
return re.sub(r"[^a-z0-9]", "", (s or "").lower())
def load_profile():
with open(PROFILE_PATH) as fh:
return json.load(fh)
# JS: enumerate fillable fields (skip password/hidden/submit/etc).
GATHER_FIELDS_JS = """
() => {
const out = [];
const els = document.querySelectorAll('input, select, textarea');
els.forEach((el, idx) => {
const tag = el.tagName.toLowerCase();
const type = tag === 'input' ? (el.type || 'text').toLowerCase() : tag;
if (['hidden','submit','button','reset','checkbox','radio','file','password'].includes(type)) return;
if (el.offsetParent === null) return;
let label = '';
if (el.id) {
const l = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
if (l) label = l.innerText;
}
if (!label && el.closest('label')) label = el.closest('label').innerText;
if (!label && el.parentElement) label = el.parentElement.innerText.split('\\n')[0];
out.push({
idx,
tag,
type,
name: el.name || '',
id: el.id || '',
ac: el.autocomplete || '',
ph: el.placeholder || '',
al: el.getAttribute('aria-label') || '',
label: (label || '').trim()
});
});
return out;
}
"""
# JS: fill one field. `candidates` = ordered values to try for <select>.
FILL_ONE_JS = """
(arg) => {
const idx = arg.idx;
const candidates = arg.candidates;
const els = document.querySelectorAll('input, select, textarea');
const el = els[idx];
if (!el) return 'missing';
el.scrollIntoView({ block: 'center' });
el.style.outline = '3px solid #6c8cff';
el.style.outlineOffset = '1px';
let ok = false;
if (el.tagName === 'SELECT') {
const opts = Array.from(el.options);
outer:
for (const want of candidates) {
const w = String(want).toLowerCase();
for (const o of opts) {
const t = o.text.trim().toLowerCase();
if (t === w || t.includes(w) || w.includes(t) || o.value.toLowerCase() === w) {
el.value = o.value; ok = true; break outer;
}
}
}
} else {
const proto = el.tagName === 'TEXTAREA'
? window.HTMLTextAreaElement.prototype
: window.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(el, String(candidates[0]));
ok = true;
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
setTimeout(() => { el.style.outline = ok ? '2px solid #4ade80' : '2px solid #f87171'; }, 500);
return ok ? 'filled' : 'no-match';
}
"""
READ_BACK_JS = """
(arg) => {
const els = document.querySelectorAll('input, select, textarea');
const el = els[arg.idx];
return el ? el.value : null;
}
"""
def build_sig(f):
return norm(" ".join([f["name"], f["id"], f["ph"], f["al"], f["label"]]))
def match_profile_key(f):
# 1) autocomplete attribute is authoritative.
ac = f["ac"].strip().lower()
if ac in AUTOCOMPLETE_MAP:
return AUTOCOMPLETE_MAP[ac]
# 2) fall back to keyword matching on name/id/label/placeholder.
sig = build_sig(f)
best_key, best_len = None, 0
for key, kws in KEYWORD_MAP.items():
for kw in kws:
if kw in sig:
if len(kw) > best_len:
best_key, best_len = key, len(kw)
return best_key
def fill_candidates(profile, key, f):
val = profile.get(key, "")
if not val:
return None
cands = [val]
if key == "state":
cands.append(STATE_NAMES.get(val.upper(), val))
if key == "full_name":
first = profile.get("first_name", "")
last = profile.get("last_name", "")
cands = ["%s %s" % (first, last).strip()]
return cands
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--url", required=True)
ap.add_argument("--headless", action="store_true")
ap.add_argument("--dump", action="store_true", help="fill then print what was set (test mode)")
ap.add_argument("--submit", action="store_true", help="click the first submit button after filling")
ap.add_argument("--delay", type=float, default=0.35, help="seconds between field fills")
args = ap.parse_args()
profile = load_profile()
with sync_playwright() as p:
browser = p.chromium.launch(headless=args.headless)
page = browser.new_page()
page.goto(args.url, wait_until="domcontentloaded", timeout=60000)
page.wait_for_timeout(1500)
fields = page.evaluate(GATHER_FIELDS_JS)
results = []
for f in fields:
blob = f["name"] + " " + f["id"] + " " + f["label"] + " " + f["ac"]
if PASSWORD_LIKE.search(blob):
continue
key = match_profile_key(f)
if key is None:
continue
cands = fill_candidates(profile, key, f)
if not cands:
continue
page.evaluate(FILL_ONE_JS, {"idx": f["idx"], "candidates": cands})
results.append((key, f, cands[0]))
if args.delay:
page.wait_for_timeout(int(args.delay * 1000))
print("\n=== FILLED %d FIELDS ===" % len(results))
for key, f, val in results:
shown = "***-**-****" if key == "ssn" else val
tag = f["label"] or f["name"] or f["id"] or f["ph"]
print(f" {key:<14} <- {shown!r:>24} [{tag}]")
if args.dump:
print("\n=== READ-BACK VERIFY ===")
all_ok = True
for key, f, val in results:
actual = page.evaluate(READ_BACK_JS, {"idx": f["idx"]})
ok = actual == val
all_ok = all_ok and ok
print(f" {key:<14} {'OK' if ok else 'MISMATCH':>8} set={val!r} got={actual!r}")
print("\nRESULT:", "ALL MATCH" if all_ok else "FAILURES PRESENT")
if args.submit:
clicked = page.evaluate("""
() => {
const sels = [
'button[type="submit"]', 'input[type="submit"]',
'button:has-text("Continue")', 'button:has-text("Next")',
'button:has-text("Submit")', 'button:has-text("Sign up")',
'button:has-text("Create account")', 'button:has-text("Agree")'
];
for (const s of sels) {
const el = document.querySelector(s);
if (el && el.offsetParent !== null) { el.click(); return s; }
}
return null;
}
""")
print("\n[SUBMIT] clicked:", clicked)
if not args.headless and not args.dump:
print("\nBrowser left open — review the form and submit yourself.")
input("Press Enter in this terminal to close the browser...")
browser.close()
if __name__ == "__main__":
main()