#!/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 . 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()