Snapshot: full project state
This commit is contained in:
290
autofill.py
Normal file
290
autofill.py
Normal file
@@ -0,0 +1,290 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user