Auto detect fields instead of manual labor
This commit is contained in:
@@ -5,7 +5,87 @@ const puppeteer = require('puppeteer-extra');
|
||||
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
||||
puppeteer.use(StealthPlugin());
|
||||
|
||||
// --- Helper Functions ---
|
||||
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 {
|
||||
@@ -25,20 +105,18 @@ function generateRandomData() {
|
||||
const username = `user${randomString}`;
|
||||
const email = `${username}@gmail.com`;
|
||||
const password = `pass${randomString}`;
|
||||
const age = Math.floor(Math.random() * 30 + 20).toString(); // Random age between 20-49
|
||||
return { username, email, password, age };
|
||||
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 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a locator string. If it's an HTML snippet, it generates a CSS selector.
|
||||
* If it's already a selector, it returns it as is.
|
||||
* @param {string} locatorStr - The locator string from the config.
|
||||
* @returns {string} A CSS selector.
|
||||
*/
|
||||
function parseLocator(locatorStr) {
|
||||
locatorStr = locatorStr.trim();
|
||||
if (!locatorStr.startsWith('<')) {
|
||||
return locatorStr; // It's already a selector
|
||||
return locatorStr;
|
||||
}
|
||||
|
||||
const getAttr = (attr) => {
|
||||
@@ -48,9 +126,8 @@ function parseLocator(locatorStr) {
|
||||
|
||||
const tagName = (locatorStr.match(/^<([a-zA-Z0-9]+)/) || [])[1] || '';
|
||||
const id = getAttr('id');
|
||||
// FIX: Handle numeric IDs by creating an attribute selector
|
||||
if (id) {
|
||||
if (/^\\d/.test(id)) { // If id starts with a digit
|
||||
if (/^\d/.test(id)) {
|
||||
return `${tagName}[id='${id}']`;
|
||||
}
|
||||
return `${tagName}#${id}`;
|
||||
@@ -65,16 +142,279 @@ function parseLocator(locatorStr) {
|
||||
const type = getAttr('type');
|
||||
if (type) return `${tagName}[type='${type}']`;
|
||||
|
||||
console.warn(`Could not generate a reliable selector for: ${locatorStr}.`);
|
||||
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 handleCameraBoysGDPR(page) {
|
||||
// ... (This function remains the same)
|
||||
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;
|
||||
}
|
||||
|
||||
// --- Main Execution ---
|
||||
async function main() {
|
||||
const config = loadConfig();
|
||||
const sitesToProcess = config.sites || [];
|
||||
@@ -104,14 +444,13 @@ async function main() {
|
||||
creds = { ...globalCreds };
|
||||
console.log(`Using global credentials for site: ${site.name}`);
|
||||
}
|
||||
// If any credential is still missing, generate it randomly
|
||||
if (!creds.username || !creds.email || !creds.password || !creds.age) {
|
||||
|
||||
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();
|
||||
@@ -123,44 +462,69 @@ async function main() {
|
||||
|
||||
await page.goto(site.url, { waitUntil: 'networkidle2' });
|
||||
|
||||
if (site.needsPreSubmitAction && site.name === 'CameraBoys') {
|
||||
await handleCameraBoysGDPR(page);
|
||||
}
|
||||
|
||||
console.log('Dynamically filling out the form...');
|
||||
|
||||
let fieldsToFill = {};
|
||||
const definedLocators = site.locators || {};
|
||||
|
||||
for (const fieldName in definedLocators) {
|
||||
if (fieldName === 'submit') continue; // Skip the submit button
|
||||
|
||||
const selector = parseLocator(definedLocators[fieldName]);
|
||||
|
||||
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];
|
||||
|
||||
// Special case for 'confirmEmail', use the email value
|
||||
|
||||
if (fieldName === 'confirmEmail') {
|
||||
valueToFill = creds['email'];
|
||||
}
|
||||
|
||||
|
||||
if (selector && valueToFill) {
|
||||
try {
|
||||
console.log(` - Filling field: ${fieldName}`);
|
||||
await page.type(selector, valueToFill, { delay: 100 });
|
||||
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}' with selector '${selector}'. Error: ${e.message}`);
|
||||
console.warn(` - Could not fill field '${fieldName}': ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Form filled. Attempting to submit...');
|
||||
const submitSelector = parseLocator(definedLocators.submit);
|
||||
await page.click(submitSelector);
|
||||
|
||||
console.log(`✅ Submission attempt for ${site.name} is complete.`);
|
||||
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 during execution:', error.message);
|
||||
console.error('❌ A critical error occurred:', error.message);
|
||||
} finally {
|
||||
console.log('\n🎉 All tasks complete. The browser window will remain open for inspection.');
|
||||
console.log('\n🎉 All tasks complete.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user