const fs = require('fs'); const path = require('path'); const toml = require('@iarna/toml'); const puppeteer = require('puppeteer-extra'); const StealthPlugin = require('puppeteer-extra-plugin-stealth'); puppeteer.use(StealthPlugin()); const FIELD_PATTERNS = { email: { priority: 1, types: ['email'], names: ['email', 'e-mail', 'mail', 'user_email', 'login_email', 'email_address', 'emailaddress'], ids: ['email', 'e-mail', 'mail', 'user-email', 'login-email'], placeholders: ['email', 'e-mail', 'email address', 'your email', 'enter email'], autocomplete: ['email'] }, confirmEmail: { priority: 2, types: ['email'], names: ['confirm_email', 'confirmemail', 'email_confirm', 'verify_email', 'email_verify'], ids: ['confirm-email', 'confirmemail', 'verify-email', 'email-confirm'], placeholders: ['confirm email', 'verify email', 're-enter email', 'repeat email'], mustHaveConfirmIndicator: true }, firstName: { priority: 3, types: ['text'], names: ['first_name', 'firstname', 'fname', 'first-name', 'vorname', 'prenom', 'fname'], ids: ['first-name', 'firstname', 'fname', 'firstName', 'mce-fname'], placeholders: ['first name', 'firstname', 'first'], autocomplete: ['given-name', 'first-name'] }, lastName: { priority: 4, types: ['text'], names: ['last_name', 'lastname', 'lname', 'surname', 'family_name', 'last-name', 'lname'], ids: ['last-name', 'lastname', 'lname', 'surname', 'lastName', 'mce-lname'], placeholders: ['last name', 'lastname', 'surname', 'last'], autocomplete: ['family-name', 'last-name'] }, username: { priority: 5, types: ['text'], names: ['username', 'user_name', 'login', 'usr', 'uname', 'user-id', 'userid', 'account_name', 'login_id'], ids: ['username', 'user-name', 'login', 'usr', 'uname'], placeholders: ['username', 'user name', 'login', 'username or email'], autocomplete: ['username'] }, password: { priority: 6, types: ['password'], names: ['password', 'pass', 'pwd', 'user_password', 'login_password'], ids: ['password', 'pass', 'pwd'], placeholders: ['password', 'pass', 'enter password'], autocomplete: ['current-password', 'new-password'] }, fullName: { priority: 7, types: ['text'], names: ['full_name', 'fullname', 'name'], ids: ['full-name', 'fullname', 'name'], placeholders: ['full name', 'your name', 'complete name'], autocomplete: ['name'] }, phone: { priority: 8, types: ['tel', 'text'], names: ['phone', 'telephone', 'mobile', 'cell', 'cellphone', 'phone_number'], ids: ['phone', 'telephone', 'mobile', 'cell', 'tel'], placeholders: ['phone', 'telephone', 'mobile number'], autocomplete: ['tel'] }, age: { priority: 9, types: ['number', 'text'], names: ['age', 'user_age'], ids: ['age'], placeholders: ['age', 'your age'] }, company: { priority: 10, types: ['text'], names: ['company', 'organization', 'company_name', 'employer'], ids: ['company', 'organization', 'employer'], placeholders: ['company', 'organization'], autocomplete: ['organization'] } }; function loadConfig() { try { const configPath = path.join(__dirname, 'config.toml'); const tomlString = fs.readFileSync(configPath, 'utf-8'); const config = toml.parse(tomlString); console.log('āœ… Configuration loaded successfully.'); return config; } catch (error) { console.error('āŒ Failed to load or parse config.toml:', error.message); process.exit(1); } } function generateRandomData() { const randomString = Math.random().toString(36).substring(2, 10); const username = `user${randomString}`; const email = `${username}@gmail.com`; const password = `pass${randomString}`; const age = Math.floor(Math.random() * 30 + 20).toString(); const firstName = 'John'; const lastName = 'Doe'; const phone = '+31610488717'; const company = 'Example Corp'; return { username, email, password, age, firstName, lastName, phone, company }; } function parseLocator(locatorStr) { locatorStr = locatorStr.trim(); if (!locatorStr.startsWith('<')) { return locatorStr; } const getAttr = (attr) => { const match = locatorStr.match(new RegExp(`${attr}=["']([^"']+)["']`)); return match ? match[1] : null; }; const tagName = (locatorStr.match(/^<([a-zA-Z0-9]+)/) || [])[1] || ''; const id = getAttr('id'); if (id) { if (/^\d/.test(id)) { return `${tagName}[id='${id}']`; } return `${tagName}#${id}`; } const name = getAttr('name'); if (name) return `${tagName}[name='${name}']`; const placeholder = getAttr('placeholder'); if (placeholder) return `${tagName}[placeholder='${placeholder}']`; const type = getAttr('type'); if (type) return `${tagName}[type='${type}']`; return tagName; } async function detectFormFields(page) { console.log('šŸ” Auto-detecting form fields...'); const detectedFields = await page.evaluate((patterns) => { const results = {}; const usedElements = new Set(); // Get all visible input fields, excluding hidden fields const inputs = Array.from(document.querySelectorAll('input, select, textarea')).filter(input => { const type = input.type?.toLowerCase() || 'text'; const style = window.getComputedStyle(input); const rect = input.getBoundingClientRect(); // Skip hidden fields and honeypots if (type === 'hidden' || type === 'submit' || type === 'button') return false; if (input.tabIndex === -1 && input.style.position === 'absolute') return false; // Likely honeypot if (rect.width === 0 || rect.height === 0) return false; if (style.display === 'none' || style.visibility === 'hidden') return false; return true; }); const calculateScore = (input, pattern, fieldName) => { let score = 0; const inputType = (input.type || 'text').toLowerCase(); const inputName = (input.name || '').toLowerCase(); const inputId = (input.id || '').toLowerCase(); const placeholder = (input.placeholder || '').toLowerCase(); const autocomplete = (input.autocomplete || '').toLowerCase(); // Type matching if (pattern.types && pattern.types.includes(inputType)) { score += 5; } // Name matching - exact matches get high scores if (pattern.names) { for (const name of pattern.names) { const nameLower = name.toLowerCase(); if (inputName === nameLower) { score += 20; // Exact match break; } else if (inputName.includes(nameLower)) { // Partial match - be careful not to match fname with username if (fieldName === 'firstName' && (inputName.includes('user') || inputName.includes('login'))) { score -= 5; // Penalize if it looks like username } else if (fieldName === 'lastName' && (inputName.includes('user') || inputName.includes('login'))) { score -= 5; } else { score += 10; } break; } } } // ID matching - very specific if (pattern.ids) { for (const id of pattern.ids) { const idLower = id.toLowerCase(); if (inputId === idLower) { score += 20; // Exact match break; } else if (inputId.includes(idLower)) { score += 10; break; } } } // Placeholder matching if (pattern.placeholders) { for (const ph of pattern.placeholders) { const phLower = ph.toLowerCase(); if (placeholder === phLower) { score += 15; break; } else if (placeholder.includes(phLower)) { score += 8; break; } } } // Autocomplete matching if (pattern.autocomplete) { for (const ac of pattern.autocomplete) { if (autocomplete === ac.toLowerCase()) { score += 12; break; } } } // Check label text const labels = input.labels; if (labels && labels.length > 0) { const labelText = labels[0].textContent.toLowerCase(); const fieldNameLower = fieldName.toLowerCase().replace(/([A-Z])/g, ' $1').trim(); if (labelText.includes(fieldNameLower) || labelText.includes(pattern.names?.[0] || '')) { score += 15; } } // Penalize if field appears to be a honeypot if (inputName.includes('b_') || inputId.includes('b_')) { score -= 30; // Likely Mailchimp honeypot } // Penalize disabled/readonly if (input.disabled || input.readOnly) { score -= 20; } // Special handling for confirmEmail if (pattern.mustHaveConfirmIndicator) { const hasConfirm = inputId.includes('confirm') || inputId.includes('verify') || inputName.includes('confirm') || inputName.includes('verify') || placeholder.includes('confirm') || placeholder.includes('verify') || placeholder.includes('re-') || placeholder.includes('again'); if (!hasConfirm) { score -= 15; } } return score; }; // Sort patterns by priority const sortedPatterns = Object.entries(patterns).sort((a, b) => a[1].priority - b[1].priority); for (const [fieldName, pattern] of sortedPatterns) { let bestMatch = null; let bestScore = 0; let bestIndex = -1; for (let i = 0; i < inputs.length; i++) { if (usedElements.has(i)) continue; const input = inputs[i]; const score = calculateScore(input, pattern, fieldName); if (score > bestScore && score >= 10) { bestScore = score; bestMatch = input; bestIndex = i; } } if (bestMatch && bestIndex >= 0) { usedElements.add(bestIndex); // Generate selector let selector = ''; if (bestMatch.id) { selector = `#${bestMatch.id}`; } else if (bestMatch.name) { selector = `[name="${bestMatch.name}"]`; } else if (bestMatch.placeholder) { selector = `[placeholder="${bestMatch.placeholder}"]`; } else { const tagName = bestMatch.tagName.toLowerCase(); const type = bestMatch.type ? `[type="${bestMatch.type}"]` : ''; const allSame = Array.from(document.querySelectorAll(`${tagName}${type}`)); const index = allSame.indexOf(bestMatch) + 1; selector = `${tagName}${type}:nth-of-type(${index})`; } results[fieldName] = { selector, confidence: bestScore, name: bestMatch.name, id: bestMatch.id }; } } return results; }, FIELD_PATTERNS); const detectedCount = Object.keys(detectedFields).length; if (detectedCount > 0) { console.log(` āœ… Auto-detected ${detectedCount} fields:`); for (const [field, info] of Object.entries(detectedFields)) { console.log(` - ${field}: ${info.selector} (score: ${info.confidence})`); } } else { console.log(' āš ļø No fields auto-detected'); } return detectedFields; } async function detectSubmitButton(page) { console.log('šŸ” Auto-detecting submit button...'); const submitSelector = await page.evaluate(() => { const buttons = Array.from(document.querySelectorAll('button[type="submit"], input[type="submit"], button, input[type="button"]')); let bestMatch = null; let bestScore = 0; buttons.forEach(btn => { const text = (btn.textContent || btn.value || '').toLowerCase().trim(); const type = btn.type?.toLowerCase() || ''; const className = (btn.className || '').toLowerCase(); let score = 0; if (type === 'submit') score += 20; if (text.includes('subscribe')) score += 15; else if (text.includes('sign up')) score += 12; else if (text.includes('submit')) score += 10; else if (text.includes('join')) score += 8; else if (text.includes('register')) score += 8; else if (text.includes('send')) score += 5; if (className.includes('submit')) score += 8; if (className.includes('subscribe')) score += 8; if (className.includes('close') || text.includes('close')) score -= 20; if (className.includes('cancel') || text.includes('cancel')) score -= 20; if (className.includes('back') || text.includes('back')) score -= 15; if (className.includes('drawer')) score -= 15; const rect = btn.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0 && rect.top > 0) { score += 3; } if (score > bestScore) { bestScore = score; bestMatch = btn; } }); if (bestMatch && bestScore >= 10) { if (bestMatch.id) return `#${bestMatch.id}`; if (bestMatch.name) return `[name="${bestMatch.name}"]`; const className = bestMatch.className; if (className) { const classes = className.split(' ').filter(c => c.length > 0); for (const cls of classes) { const clsLower = cls.toLowerCase(); if (clsLower.includes('submit') || clsLower.includes('subscribe')) { return `.${cls}`; } } for (const cls of classes) { if (!cls.toLowerCase().includes('close') && !cls.toLowerCase().includes('cancel')) { return `.${cls}`; } } } const allBtns = Array.from(document.querySelectorAll('button')); const index = allBtns.indexOf(bestMatch) + 1; return `button:nth-of-type(${index})`; } return null; }); if (submitSelector) { console.log(` āœ… Auto-detected submit button: ${submitSelector}`); } return submitSelector; } async function main() { const config = loadConfig(); const sitesToProcess = config.sites || []; const browserConfig = config.browser || {}; const globalCreds = config.global_credentials || {}; const userDataDir = browserConfig.userDataDir; if (!userDataDir) { console.error('āŒ userDataDir is not set in config.toml. This is required.'); return; } const browser = await puppeteer.launch({ headless: false, userDataDir: userDataDir, args: ["--start-maximized"] }); try { let isFirstSite = true; for (const site of sitesToProcess) { let creds; if (site.credentials && site.credentials.username) { creds = { ...globalCreds, ...site.credentials }; console.log(`Using credentials specified for site: ${site.name}`); } else { creds = { ...globalCreds }; console.log(`Using global credentials for site: ${site.name}`); } if (!creds.username || !creds.email || !creds.password) { const randomData = generateRandomData(); creds = { ...randomData, ...creds }; console.log('Filled in missing credentials with random data.'); } console.log(`\n--- Processing Site: ${site.name} (Category: ${site.category || 'N/A'}) ---`); const page = isFirstSite ? (await browser.pages())[0] : await browser.newPage(); isFirstSite = false; if (browserConfig.userAgent) { await page.setUserAgent(browserConfig.userAgent); } await page.goto(site.url, { waitUntil: 'networkidle2' }); console.log('Dynamically filling out the form...'); let fieldsToFill = {}; const definedLocators = site.locators || {}; if (site.autoDetect !== false) { const detectedFields = await detectFormFields(page); for (const [fieldName, fieldInfo] of Object.entries(detectedFields)) { fieldsToFill[fieldName] = fieldInfo.selector; } } for (const [fieldName, locator] of Object.entries(definedLocators)) { if (fieldName !== 'submit') { fieldsToFill[fieldName] = parseLocator(locator); } } for (const [fieldName, selector] of Object.entries(fieldsToFill)) { let valueToFill = creds[fieldName]; if (fieldName === 'confirmEmail') { valueToFill = creds['email']; } if (selector && valueToFill) { try { console.log(` - Filling field: ${fieldName} with "${valueToFill}"`); await page.evaluate((sel, val) => { const el = document.querySelector(sel); if (el) { el.value = val; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } }, selector, valueToFill); } catch (e) { console.warn(` - Could not fill field '${fieldName}': ${e.message}`); } } } let submitSelector = null; if (definedLocators.submit) { submitSelector = parseLocator(definedLocators.submit); } else if (site.autoDetect !== false) { submitSelector = await detectSubmitButton(page); } if (submitSelector) { console.log('Form filled. Attempting to submit...'); await page.click(submitSelector); console.log(`āœ… Submission attempt for ${site.name} is complete.`); } else { console.warn('āš ļø No submit button found.'); } await new Promise(r => setTimeout(r, 2000)); } } catch(error) { console.error('āŒ A critical error occurred:', error.message); } finally { console.log('\nšŸŽ‰ All tasks complete.'); } } main();