fix displayed email extraction boundaries
This commit is contained in:
@@ -259,6 +259,112 @@ function getActionText(el) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function hasEmailTokenBoundary(text, start, end) {
|
||||
const before = start > 0 ? text[start - 1] : '';
|
||||
const after = end < text.length ? text[end] : '';
|
||||
return (!before || !/[A-Z0-9._%+-]/i.test(before))
|
||||
&& (!after || !/[A-Z0-9_%+-]/i.test(after));
|
||||
}
|
||||
|
||||
function isValidEmailCandidate(value = '') {
|
||||
const candidate = String(value || '').trim();
|
||||
const atIndex = candidate.lastIndexOf('@');
|
||||
if (atIndex <= 0 || atIndex !== candidate.indexOf('@')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const local = candidate.slice(0, atIndex);
|
||||
const domain = candidate.slice(atIndex + 1);
|
||||
if (!local || !domain || local.startsWith('.') || local.endsWith('.') || local.includes('..')) {
|
||||
return false;
|
||||
}
|
||||
if (!/^[A-Z0-9._%+-]+$/i.test(local)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const labels = domain.split('.');
|
||||
if (labels.length < 2) {
|
||||
return false;
|
||||
}
|
||||
for (const label of labels) {
|
||||
if (!/^[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?$/i.test(label)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const tld = labels[labels.length - 1];
|
||||
return /^[A-Z]{2,63}$/i.test(tld) && !/[a-z][A-Z]/.test(tld);
|
||||
}
|
||||
|
||||
function getEmailFromTextFragment(value = '') {
|
||||
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
const pattern = /[A-Z0-9._%+-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,63}/ig;
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const candidate = String(match[0] || '');
|
||||
const start = Number(match.index || 0);
|
||||
const end = start + candidate.length;
|
||||
if (hasEmailTokenBoundary(text, start, end) && isValidEmailCandidate(candidate)) {
|
||||
return candidate.toLowerCase();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getVisibleTextNodeEmail() {
|
||||
if (typeof document?.createTreeWalker !== 'function' || !document.body) {
|
||||
return '';
|
||||
}
|
||||
const showText = typeof NodeFilter !== 'undefined' ? NodeFilter.SHOW_TEXT : 4;
|
||||
const walker = document.createTreeWalker(document.body, showText);
|
||||
let node = walker.nextNode();
|
||||
while (node) {
|
||||
const parent = node.parentElement || node.parentNode || null;
|
||||
const text = String(node.nodeValue || '').trim();
|
||||
if (text && (!parent || typeof isVisibleElement !== 'function' || isVisibleElement(parent))) {
|
||||
const email = getEmailFromTextFragment(text);
|
||||
if (email) {
|
||||
return email;
|
||||
}
|
||||
}
|
||||
node = walker.nextNode();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getDisplayedEmailFromDocument() {
|
||||
const fieldCandidates = Array.from(document.querySelectorAll?.('input, textarea') || []);
|
||||
for (const field of fieldCandidates) {
|
||||
const email = getEmailFromTextFragment(field?.value || field?.getAttribute?.('value') || '');
|
||||
if (email) {
|
||||
return email;
|
||||
}
|
||||
}
|
||||
|
||||
const attributeCandidates = Array.from(document.querySelectorAll?.('[aria-label], [title]') || []);
|
||||
for (const el of attributeCandidates) {
|
||||
if (typeof isVisibleElement === 'function' && !isVisibleElement(el)) {
|
||||
continue;
|
||||
}
|
||||
const email = getEmailFromTextFragment([
|
||||
el?.getAttribute?.('aria-label'),
|
||||
el?.getAttribute?.('title'),
|
||||
].filter(Boolean).join(' '));
|
||||
if (email) {
|
||||
return email;
|
||||
}
|
||||
}
|
||||
|
||||
const textNodeEmail = getVisibleTextNodeEmail();
|
||||
if (textNodeEmail) {
|
||||
return textNodeEmail;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function isActionEnabled(el) {
|
||||
return Boolean(el)
|
||||
&& !el.disabled
|
||||
@@ -715,11 +821,7 @@ function findSignupEntryTrigger(options = {}) {
|
||||
}
|
||||
|
||||
function getSignupPasswordDisplayedEmail() {
|
||||
const text = (document.body?.innerText || document.body?.textContent || '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const matches = text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig);
|
||||
return matches?.[0] ? String(matches[0]).trim().toLowerCase() : '';
|
||||
return getDisplayedEmailFromDocument();
|
||||
}
|
||||
|
||||
function inspectSignupEntryState() {
|
||||
@@ -3028,9 +3130,7 @@ function getPageTextSnapshot() {
|
||||
}
|
||||
|
||||
function getLoginVerificationDisplayedEmail() {
|
||||
const pageText = getPageTextSnapshot();
|
||||
const matches = pageText.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/ig) || [];
|
||||
return matches[0] ? String(matches[0]).trim().toLowerCase() : '';
|
||||
return getDisplayedEmailFromDocument();
|
||||
}
|
||||
|
||||
function getPhoneVerificationDisplayedPhone() {
|
||||
|
||||
@@ -51,6 +51,12 @@ function extractFunction(name) {
|
||||
}
|
||||
|
||||
const bundle = [
|
||||
extractFunction('hasEmailTokenBoundary'),
|
||||
extractFunction('isValidEmailCandidate'),
|
||||
extractFunction('getEmailFromTextFragment'),
|
||||
extractFunction('getVisibleTextNodeEmail'),
|
||||
extractFunction('getDisplayedEmailFromDocument'),
|
||||
extractFunction('getSignupPasswordDisplayedEmail'),
|
||||
extractFunction('getPageTextSnapshot'),
|
||||
extractFunction('getLoginVerificationDisplayedEmail'),
|
||||
extractFunction('getPhoneVerificationDisplayedPhone'),
|
||||
@@ -74,9 +80,39 @@ const document = {
|
||||
innerText: ${JSON.stringify(overrides.pageText || '')},
|
||||
textContent: ${JSON.stringify(overrides.pageText || '')},
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
if (selector === 'input, textarea') {
|
||||
return ${JSON.stringify(overrides.fields || [])};
|
||||
}
|
||||
if (selector === '[aria-label], [title]') {
|
||||
return ${JSON.stringify(overrides.attributeElements || [])};
|
||||
}
|
||||
return [];
|
||||
},
|
||||
querySelector() {
|
||||
return null;
|
||||
},
|
||||
createTreeWalker() {
|
||||
const textNodeValues = ${JSON.stringify(
|
||||
Object.prototype.hasOwnProperty.call(overrides, 'textNodes')
|
||||
? overrides.textNodes
|
||||
: [overrides.pageText || '']
|
||||
)};
|
||||
const nodes = textNodeValues.map((text) => ({
|
||||
nodeValue: text,
|
||||
parentElement: {
|
||||
getBoundingClientRect() {
|
||||
return { width: 1, height: 1 };
|
||||
},
|
||||
},
|
||||
}));
|
||||
let index = 0;
|
||||
return {
|
||||
nextNode() {
|
||||
return nodes[index++] || null;
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function getLoginTimeoutErrorPageState() {
|
||||
@@ -149,6 +185,8 @@ return {
|
||||
inspectLoginAuthState,
|
||||
isPhoneVerificationPageReady,
|
||||
normalizeStep6Snapshot,
|
||||
getSignupPasswordDisplayedEmail,
|
||||
getLoginVerificationDisplayedEmail,
|
||||
};
|
||||
`)();
|
||||
}
|
||||
@@ -179,6 +217,49 @@ return {
|
||||
assert.strictEqual(snapshot.displayedEmail, 'display.user@example.com');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
verificationTarget: { id: 'otp' },
|
||||
pageText: 'We emailed a code to skater-twine-carve@duck.comChange',
|
||||
textNodes: ['We emailed a code to ', 'skater-twine-carve@duck.com', 'Change'],
|
||||
});
|
||||
|
||||
const snapshot = api.inspectLoginAuthState();
|
||||
assert.strictEqual(snapshot.displayedEmail, 'skater-twine-carve@duck.com');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
verificationTarget: { id: 'otp' },
|
||||
pageText: 'We emailed a code to skater-twine-carve@duck.comChange',
|
||||
textNodes: ['We emailed a code to skater-twine-carve@duck.comChange'],
|
||||
});
|
||||
|
||||
const snapshot = api.inspectLoginAuthState();
|
||||
assert.strictEqual(snapshot.displayedEmail, '');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
pageText: 'Create your account skater-twine-carve@duck.comChange',
|
||||
textNodes: ['Create your account ', 'skater-twine-carve@duck.com', 'Change'],
|
||||
});
|
||||
|
||||
assert.strictEqual(api.getSignupPasswordDisplayedEmail(), 'skater-twine-carve@duck.com');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
verificationTarget: { id: 'otp' },
|
||||
pageText: 'We emailed a code to skater-twine-carve@duck.comChange',
|
||||
textNodes: ['We emailed a code to skater-twine-carve@duck.comChange'],
|
||||
fields: [{ value: 'skater-twine-carve@duck.com' }],
|
||||
});
|
||||
|
||||
const snapshot = api.inspectLoginAuthState();
|
||||
assert.strictEqual(snapshot.displayedEmail, 'skater-twine-carve@duck.com');
|
||||
}
|
||||
|
||||
{
|
||||
const api = createApi({
|
||||
pathname: '/email-verification',
|
||||
|
||||
Reference in New Issue
Block a user