481 lines
17 KiB
JavaScript
481 lines
17 KiB
JavaScript
/**
|
|
* ============================================================================
|
|
* 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;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Extension Compatibility Wrapper
|
|
// ============================================================================
|
|
window.addEventListener('message', (event) => {
|
|
if (event.source !== window) return;
|
|
const message = event.data;
|
|
|
|
if (message && message.type === 'INIT_MODULE' && message.moduleName === 'autoFill') {
|
|
if (window.__autoFillInstance) return;
|
|
try {
|
|
const instance = new AutoFillHandlerModule(message.config);
|
|
instance.init();
|
|
window.__autoFillInstance = instance;
|
|
console.log('[Extension] AutoFill Handler initialized');
|
|
} catch (error) {
|
|
console.error('[Extension] AutoFill init failed:', error);
|
|
}
|
|
}
|
|
|
|
if (message && message.type === 'DESTROY_MODULE' && message.instanceKey === '__autoFillInstance') {
|
|
const instance = window.__autoFillInstance;
|
|
if (instance && typeof instance.destroy === 'function') {
|
|
instance.destroy();
|
|
delete window.__autoFillInstance;
|
|
}
|
|
}
|
|
});
|