/** * ============================================================================ * AutoFill Handler Module (Modularized Version) * ============================================================================ * 功能: * 1. 智能識別並填充支付表單 (信用卡、賬單地址) * 2. 模擬真實用戶輸入事件 (繞過 React/Vue/Angular 的狀態檢查) * 3. MutationObserver 實時監聽動態加載的表單 * 4. 內置多國賬單地址生成庫 (AVS 繞過) * 5. 與其他模塊 (Card Generator) 聯動 * * 使用方式: * const autofill = new AutoFillHandlerModule(config); * autofill.init(); * ============================================================================ */ class AutoFillHandlerModule { constructor(config = {}) { this.config = { enabled: true, debug: false, fillCardData: true, fillBillingData: true, fillDelay: 500, // 填充延遲 (毫秒),模擬人類思考 typeDelay: 10, // 字符間隔延遲 (可選,用於更高級模擬) autoSubmit: false, // 是否填充後自動提交 targetCountry: 'US', // 默認賬單國家 // 字段選擇器映射 (支持 CSS 選擇器數組) selectors: { cardNumber: [ 'input[name*="card"][name*="number"]', 'input[name="cardNumber"]', 'input[id*="cardNumber"]', '.card-number input', 'input[autocomplete="cc-number"]', 'input[placeholder*="Card number"]' ], expDate: [ 'input[name*="exp"]', 'input[id*="exp"]', 'input[autocomplete="cc-exp"]', 'input[placeholder*="MM / YY"]' ], cvc: [ 'input[name*="cvc"]', 'input[name*="cvv"]', 'input[autocomplete="cc-csc"]', 'input[placeholder*="CVC"]' ], holderName: [ 'input[name*="name"]', 'input[autocomplete="cc-name"]', 'input[id*="holder"]', 'input[placeholder*="Cardholder"]' ], address: ['input[name*="address"]', 'input[autocomplete="street-address"]', 'input[id*="address"]'], city: ['input[name*="city"]', 'input[autocomplete="address-level2"]'], state: ['input[name*="state"]', 'select[name*="state"]', 'input[autocomplete="address-level1"]'], zip: ['input[name*="zip"]', 'input[name*="postal"]', 'input[autocomplete="postal-code"]'], country: ['select[name*="country"]', 'select[id*="country"]', 'input[name*="country"]'], phone: ['input[name*="phone"]', 'input[type="tel"]'] }, // 外部數據源 (如果提供,將覆蓋內部庫) billingData: null, ...config }; // 內部狀態 this.observer = null; this.lastFilledTime = 0; this.cachedCard = null; // 內置地址庫 (簡化版,實際使用可擴展) this.billingDB = { 'US': { name: 'James Smith', address: '450 West 33rd Street', city: 'New York', state: 'NY', zip: '10001', country: 'US', phone: '2125550199' }, 'GB': { name: 'Arthur Dent', address: '42 Islington High St', city: 'London', state: '', zip: 'N1 8EQ', country: 'GB', phone: '02079460123' }, 'CN': { name: 'Zhang Wei', address: 'No. 1 Fuxingmen Inner Street', city: 'Beijing', state: 'Beijing', zip: '100031', country: 'CN', phone: '13910998888' } }; // 合併配置中的數據 if (this.config.billingData) { Object.assign(this.billingDB, this.config.billingData); } // 綁定方法 this.handleMutations = this.handleMutations.bind(this); } /** * 初始化模塊 */ init() { this.log('Initializing AutoFill Handler Module...'); // 1. 啟動 DOM 監聽 this.startObserver(); // 2. 監聽卡號生成事件 (來自 GOG/Stripe 模塊) this.setupEventListeners(); // 3. 檢查頁面上已有的表單 if (document.readyState === 'complete' || document.readyState === 'interactive') { this.scanAndFill(document.body); } else { document.addEventListener('DOMContentLoaded', () => this.scanAndFill(document.body)); } // 4. 暴露全局 API this.exposeGlobalAPI(); this.log('Module initialized. Waiting for forms...'); } /** * 銷毀模塊 */ destroy() { if (this.observer) { this.observer.disconnect(); this.observer = null; } this.log('Module destroyed'); } /** * ======================================================================== * 事件監聽與聯動 * ======================================================================== */ setupEventListeners() { // 監聽 GOG/Stripe 模塊的卡號生成事件 const eventNames = ['GOG_CARD_GENERATED', 'STRIPE_CARD_GENERATED', 'CARD_GENERATED']; eventNames.forEach(evt => { document.addEventListener(evt, (e) => { this.log(`Received card data from ${evt}`, e.detail); if (e.detail && e.detail.card) { this.setCardData(e.detail.card); } }); }); // 監聽 postMessage window.addEventListener('message', (event) => { if (event.data?.eventType === 'CARD_GENERATED') { this.log('Received card data from postMessage', event.data.card); this.setCardData(event.data.card); } }); } setCardData(card) { this.cachedCard = card; // 收到新卡後立即重新掃描頁面 this.scanAndFill(document.body); } /** * ======================================================================== * Mutation Observer (DOM 監聽) * ======================================================================== */ startObserver() { if (this.observer) return; this.observer = new MutationObserver(this.handleMutations); this.observer.observe(document.body, { childList: true, subtree: true }); this.log('DOM Observer started'); } handleMutations(mutations) { let shouldScan = false; for (const mutation of mutations) { if (mutation.addedNodes.length > 0) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { // 元素節點 // 簡單過濾:只關注包含 input/select/iframe 的節點 if (node.tagName === 'INPUT' || node.tagName === 'SELECT' || node.tagName === 'IFRAME' || node.querySelector('input, select, iframe')) { shouldScan = true; break; } } } } if (shouldScan) break; } if (shouldScan) { // 防抖動:不要頻繁掃描 if (this._scanTimeout) clearTimeout(this._scanTimeout); this._scanTimeout = setTimeout(() => { this.scanAndFill(document.body); }, 200); } } /** * ======================================================================== * 核心填充邏輯 * ======================================================================== */ async scanAndFill(container) { if (!this.config.enabled) return; // 獲取卡數據:優先使用緩存的 (剛生成的),其次嘗試從 Storage 讀取 let cardData = this.cachedCard; if (!cardData) { cardData = this.loadCardFromStorage(); } if (!cardData && this.config.fillCardData) { // 如果沒有卡數據,我們只能填充地址 this.log('No card data available yet. Skipping card fields.'); } // 獲取賬單數據 const billingProfile = this.billingDB[this.config.targetCountry] || this.billingDB['US']; this.log('Scanning container for fields...'); // 1. 填充信用卡字段 if (this.config.fillCardData && cardData) { await this.fillField(container, this.config.selectors.cardNumber, cardData.number); // 日期處理:有的表單是 MM / YY 分開,有的是合併 // 這裡簡單處理合併的情況,或者可以擴展檢測邏輯 const expVal = `${cardData.month} / ${cardData.year.slice(-2)}`; await this.fillField(container, this.config.selectors.expDate, expVal); await this.fillField(container, this.config.selectors.cvc, cardData.cvc); } // 2. 填充賬單字段 if (this.config.fillBillingData && billingProfile) { await this.fillField(container, this.config.selectors.holderName, billingProfile.name); await this.fillField(container, this.config.selectors.address, billingProfile.address); await this.fillField(container, this.config.selectors.city, billingProfile.city); await this.fillField(container, this.config.selectors.state, billingProfile.state); await this.fillField(container, this.config.selectors.zip, billingProfile.zip); await this.fillField(container, this.config.selectors.phone, billingProfile.phone); // 國家字段比較特殊,通常是 Select const countryEl = this.findElement(container, this.config.selectors.country); if (countryEl) { this.log('Found country field, attempting to set:', billingProfile.country); this.simulateSelect(countryEl, billingProfile.country); } } } /** * 查找並填充單個字段 */ async fillField(container, selectors, value) { if (!value) return; const element = this.findElement(container, selectors); if (element) { // 檢查是否已經填充過,避免覆蓋用戶手動輸入 if (element.value && element.value === value) return; if (element.getAttribute('data-autofilled') === 'true') return; this.log(`Filling field [${element.name || element.id}] with value length: ${value.length}`); // 延遲模擬 await this.sleep(this.config.fillDelay); this.simulateInput(element, value); element.setAttribute('data-autofilled', 'true'); // 高亮顯示 (可選,便於調試) if (this.config.debug) { element.style.backgroundColor = '#e8f0fe'; element.style.transition = 'background-color 0.5s'; } } } /** * 輔助函數:根據選擇器列表查找元素 */ findElement(container, selectors) { for (const selector of selectors) { // 嘗試查找 const el = container.querySelector(selector); // 確保元素可見且可編輯 if (el && !el.disabled && el.offsetParent !== null) { return el; } } return null; } /** * ======================================================================== * 輸入模擬 (核心黑魔法) * ======================================================================== */ /** * 模擬輸入事件序列 * 這是繞過 React/Angular 狀態綁定的關鍵 */ simulateInput(element, value) { if (!element) return; // 1. 獲取並保存原始值屬性描述符 const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; // 2. 聚焦 element.focus(); element.dispatchEvent(new Event('focus', { bubbles: true })); // 3. 設置值 (使用原生 Setter 繞過框架代理) nativeInputValueSetter.call(element, value); // 4. 觸發一系列事件 const events = [ new KeyboardEvent('keydown', { bubbles: true }), new KeyboardEvent('keypress', { bubbles: true }), new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }), new KeyboardEvent('keyup', { bubbles: true }), new Event('change', { bubbles: true }) ]; events.forEach(event => element.dispatchEvent(event)); // 5. 失焦 element.blur(); element.dispatchEvent(new Event('blur', { bubbles: true })); } /** * 模擬下拉框選擇 */ simulateSelect(element, value) { if (!element) return; // 嘗試匹配選項 (Value 或 Text) let found = false; for (let i = 0; i < element.options.length; i++) { const option = element.options[i]; if (option.value === value || option.text.includes(value)) { element.selectedIndex = i; found = true; break; } } if (found) { element.dispatchEvent(new Event('change', { bubbles: true })); element.dispatchEvent(new Event('input', { bubbles: true })); } } /** * ======================================================================== * 工具方法 * ======================================================================== */ loadCardFromStorage() { try { // 嘗試讀取 GOG 模塊的存儲 let json = localStorage.getItem('gogBypasserLastCardJSON'); if (json) return JSON.parse(json); // 嘗試讀取通用存儲 json = localStorage.getItem('StripeBypasserLastCard'); if (json) return JSON.parse(json); } catch (e) { // ignore } return null; } sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** * 全局 API */ exposeGlobalAPI() { window.autofillHandler = { module: this, fill: () => this.scanAndFill(document.body), setCard: (card) => this.setCardData(card), updateConfig: (cfg) => { Object.assign(this.config, cfg); }, getBillingProfile: (country) => this.billingDB[country] }; } log(...args) { if (this.config.debug) { console.log('[AutoFill]', ...args); } } } /** * ============================================================================ * 自動初始化 * ============================================================================ */ window.AutoFillHandlerModule = AutoFillHandlerModule; if (typeof AUTOFILL_AUTO_INIT !== 'undefined' && AUTOFILL_AUTO_INIT) { const instance = new AutoFillHandlerModule({ debug: true }); instance.init(); window.__autofill = instance; }