Skip to content

Commit dc48b99

Browse files
authored
Merge pull request #207 from esokullu/captchafixes
some changes
2 parents aacf076 + b73b7d1 commit dc48b99

7 files changed

Lines changed: 605 additions & 42 deletions

File tree

src/chrome/src/agent/agent.js

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2867,6 +2867,21 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
28672867
frameId: Number.isInteger(entry?.frameId) ? entry.frameId : 0,
28682868
payload: entry?.result,
28692869
}));
2870+
const hiddenChallenges = entries.flatMap(entry => {
2871+
const label = String(entry.payload?.hiddenChallenge?.label || '');
2872+
if (!label) return [];
2873+
const normalized = detectChallengeDialog(
2874+
`dialog ${JSON.stringify(label.slice(0, 200))}`,
2875+
{ allowGenericFailure: options?.allowGenericFailure === true },
2876+
);
2877+
if (!normalized?.normalizedLabel) return [];
2878+
return [{
2879+
frameId: entry.frameId,
2880+
frameUrl: String(entry.payload?.frameContext?.frameUrl || ''),
2881+
label: normalized.label,
2882+
normalizedLabel: normalized.normalizedLabel,
2883+
}];
2884+
});
28702885
const inspectedFrameIds = new Set(entries
28712886
.filter(entry => entry.payload && typeof entry.payload === 'object')
28722887
.map(entry => entry.frameId));
@@ -2876,9 +2891,14 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
28762891
return includeStatus
28772892
? {
28782893
challenge,
2894+
challengeHidden: hiddenChallenges.length > 0,
2895+
hiddenChallenges,
2896+
// With no expected frame, "complete" still requires at least one
2897+
// frame to have actually been inspected.
28792898
inspectionComplete: expectedFrameId === null
2880-
|| inspectedFrameIds.has(expectedFrameId)
2881-
|| (navigationInspectionComplete && !expectedFrameStillExists),
2899+
? inspectedFrameIds.size > 0
2900+
: (inspectedFrameIds.has(expectedFrameId)
2901+
|| (navigationInspectionComplete && !expectedFrameStillExists)),
28822902
}
28832903
: challenge;
28842904
};
@@ -2891,7 +2911,12 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
28912911
return inspected(results);
28922912
} catch {
28932913
return includeStatus
2894-
? { challenge: null, inspectionComplete: false }
2914+
? {
2915+
challenge: null,
2916+
challengeHidden: false,
2917+
hiddenChallenges: [],
2918+
inspectionComplete: false,
2919+
}
28952920
: null;
28962921
}
28972922
}
@@ -2919,6 +2944,7 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
29192944
? challenge.frameId
29202945
: null,
29212946
captchaChallengeFrameUrl: challenge.frameUrl || '',
2947+
captchaChallengeVisibilityConfirmed: true,
29222948
},
29232949
{},
29242950
);
@@ -2937,6 +2963,11 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
29372963
}
29382964

29392965
const activeGate = this._captchaGateStates.get(tabId);
2966+
const treeFilter = String(toolArgs?.filter || 'all').toLowerCase();
2967+
let observedChallengeFrameId = Number.isInteger(toolResult.captchaChallengeFrameId)
2968+
? toolResult.captchaChallengeFrameId
2969+
: null;
2970+
let observedChallengeFrameUrl = String(toolResult.captchaChallengeFrameUrl || '');
29402971
let challenge = detectChallengeDialog(toolResult.pageContent, {
29412972
allowGenericFailure: !!activeGate,
29422973
});
@@ -2946,11 +2977,36 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
29462977
{ allowGenericFailure: !!activeGate },
29472978
);
29482979
}
2980+
if (
2981+
challenge
2982+
&& treeFilter !== 'visible'
2983+
&& toolResult.captchaChallengeVisibilityConfirmed !== true
2984+
) {
2985+
const recheck = await this._detectChallengeDialogBeforeMutation(tabId, {
2986+
includeStatus: true,
2987+
expectedFrameId: observedChallengeFrameId,
2988+
allowGenericFailure: !!activeGate,
2989+
});
2990+
if (recheck.challenge?.label) {
2991+
challenge = detectChallengeDialog(
2992+
`dialog ${JSON.stringify(String(recheck.challenge.label).slice(0, 200))}`,
2993+
{ allowGenericFailure: !!activeGate },
2994+
);
2995+
observedChallengeFrameId = Number.isInteger(recheck.challenge.frameId)
2996+
? recheck.challenge.frameId
2997+
: null;
2998+
observedChallengeFrameUrl = String(recheck.challenge.frameUrl || '');
2999+
}
3000+
// A DOM scan cannot prove that the tree-observed dialog is the same
3001+
// element as a hidden match: a page may retain a hidden template while
3002+
// rendering the active challenge in a closed shadow root. Hidden,
3003+
// missing, and inconclusive scan results therefore remain diagnostic
3004+
// only and keep the gate fail-closed.
3005+
}
29493006
let pageUrl = String(toolResult.currentUrl || toolResult.pageUrl || '');
29503007
if (!pageUrl) {
29513008
try { pageUrl = await this._currentUrl(tabId); } catch {}
29523009
}
2953-
const treeFilter = String(toolArgs?.filter || 'all').toLowerCase();
29543010
const requestedPage = toolArgs?.page;
29553011
const requestedMaxDepth = toolArgs?.maxDepth;
29563012
const parsedMaxDepth = Number(requestedMaxDepth);
@@ -3148,10 +3204,10 @@ Rules: no prose intro, no conclusion, no "this screenshot shows...", no layout d
31483204
key,
31493205
status: publicGate.status,
31503206
publicGate,
3151-
...(Number.isInteger(toolResult.captchaChallengeFrameId)
3207+
...(Number.isInteger(observedChallengeFrameId)
31523208
? {
3153-
challengeFrameId: toolResult.captchaChallengeFrameId,
3154-
challengeFrameUrl: String(toolResult.captchaChallengeFrameUrl || ''),
3209+
challengeFrameId: observedChallengeFrameId,
3210+
challengeFrameUrl: observedChallengeFrameUrl,
31553211
}
31563212
: {}),
31573213
});

src/chrome/src/agent/captcha-frame-runtime.js

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -630,11 +630,22 @@ export function detectCaptchaCandidatesInPage(scope = null) {
630630
const visibleElement = (element) => {
631631
if (!element) return false;
632632
try {
633-
const style = typeof pageWindow?.getComputedStyle === 'function'
633+
const elementStyle = typeof pageWindow?.getComputedStyle === 'function'
634634
? pageWindow.getComputedStyle(element)
635635
: null;
636-
if (style && (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0)) return false;
637-
if (element.hidden || element.getAttribute?.('aria-hidden') === 'true') return false;
636+
if (
637+
elementStyle
638+
&& (elementStyle.visibility === 'hidden' || elementStyle.visibility === 'collapse')
639+
) return false;
640+
let current = element;
641+
for (let depth = 0; current && depth < 30; depth += 1) {
642+
const style = typeof pageWindow?.getComputedStyle === 'function'
643+
? pageWindow.getComputedStyle(current)
644+
: null;
645+
if (style && (style.display === 'none' || Number(style.opacity) === 0)) return false;
646+
if (current.hidden || current.getAttribute?.('aria-hidden') === 'true') return false;
647+
current = current.parentElement || null;
648+
}
638649
if (typeof element.getBoundingClientRect === 'function') {
639650
const rect = element.getBoundingClientRect();
640651
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
@@ -660,13 +671,21 @@ export function detectCaptchaCandidatesInPage(scope = null) {
660671
.map(id => pageDocument.getElementById?.(id)?.textContent || '')
661672
.join(' ');
662673
} catch (_) {}
674+
let renderedHeadings = '';
675+
try {
676+
renderedHeadings = Array.from(
677+
element.querySelectorAll?.('h1, h2, h3, [role="heading"]') || []
678+
)
679+
.filter(visibleElement)
680+
.map(heading => heading.textContent || '')
681+
.join(' ');
682+
} catch (_) {}
663683
return [
664684
element.getAttribute?.('aria-label'),
665685
labelledBy,
666-
element.querySelector?.('h1, h2, h3, [role="heading"]')?.textContent,
686+
renderedHeadings,
667687
element.getAttribute?.('title'),
668688
element.innerText,
669-
element.textContent,
670689
].some(value => challengeDialogRe.test(String(value || '')));
671690
});
672691
const elementInChallengeDialog = (element) => {

src/chrome/src/agent/captcha-gate.js

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,17 @@ export function detectChallengeDialogInPage(options = null) {
107107
};
108108
const visible = (element) => {
109109
try {
110-
const style = getComputedStyle(element);
111-
if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
112-
if (element.hidden || element.getAttribute?.('aria-hidden') === 'true') return false;
110+
const elementStyle = getComputedStyle(element);
111+
if (elementStyle.visibility === 'hidden' || elementStyle.visibility === 'collapse') return false;
112+
let current = element;
113+
for (let depth = 0; current && depth < 30; depth += 1) {
114+
const style = getComputedStyle(current);
115+
if (style.display === 'none' || Number(style.opacity) === 0) return false;
116+
if (current.hidden || current.getAttribute?.('aria-hidden') === 'true') return false;
117+
current = current.parentElement
118+
|| (typeof current.getRootNode === 'function' ? current.getRootNode()?.host : null)
119+
|| null;
120+
}
113121
const rect = element.getBoundingClientRect();
114122
if (rect.width <= 0 || rect.height <= 0) return false;
115123
const viewportWidth = typeof window !== 'undefined' && typeof window.innerWidth === 'number'
@@ -139,33 +147,97 @@ export function detectChallengeDialogInPage(options = null) {
139147
visible: visible(element),
140148
};
141149
});
150+
// A challenge dialog that exists in the DOM but is hidden or off-viewport
151+
// is positive evidence the challenge is inactive. Report it separately so
152+
// callers can distinguish "confirmed hidden" from "not found" — a dialog
153+
// this scan cannot see at all (closed shadow root, unreachable frame) must
154+
// not be treated as disproved.
155+
let hiddenChallenge = null;
142156
const finish = challenge => includeFrameContext
143157
? {
144158
challenge,
159+
...(hiddenChallenge ? { hiddenChallenge } : {}),
145160
frameContext: {
146161
frameUrl,
147162
frameName,
148163
childFrames,
149164
},
150165
}
151166
: challenge;
152-
for (const element of document.querySelectorAll('dialog, [role="dialog"], [role="alertdialog"]')) {
153-
if (!visible(element)) continue;
167+
// The accessibility tree pierces open shadow roots on some sites, where
168+
// frameworks (e.g. LinkedIn's interop shell) render whole dialogs that
169+
// document.querySelectorAll cannot see. Scan open shadow roots too so this
170+
// preflight can confirm the same dialogs the tree reports.
171+
const dialogSelector = 'dialog, [role="dialog"], [role="alertdialog"]';
172+
const dialogCandidates = [];
173+
const collectDialogs = (root, depth) => {
174+
try {
175+
for (const element of root.querySelectorAll(dialogSelector)) {
176+
if (dialogCandidates.length >= 200) break;
177+
dialogCandidates.push(element);
178+
}
179+
} catch {}
180+
if (depth >= 4) return;
181+
let descendants;
182+
try { descendants = root.querySelectorAll('*'); } catch { return; }
183+
for (const host of descendants) {
184+
if (host.shadowRoot) collectDialogs(host.shadowRoot, depth + 1);
185+
}
186+
};
187+
collectDialogs(document, 0);
188+
const recordHiddenChallenge = (element) => {
189+
if (hiddenChallenge) return;
190+
try {
191+
const values = [
192+
element.getAttribute?.('aria-label'),
193+
element.getAttribute?.('title'),
194+
element.innerText,
195+
element.textContent,
196+
...Array.from(element.querySelectorAll?.('h1, h2, h3, [role="heading"]') || [])
197+
.map(heading => heading.textContent || ''),
198+
];
199+
for (const value of values) {
200+
const text = String(value || '');
201+
if (!matchesChallenge(text)) continue;
202+
const line = text.split(/\r?\n/).find(entry => matchesChallenge(entry)) || text;
203+
const label = line.replace(/\s+/g, ' ').trim().slice(0, 200);
204+
if (label) {
205+
hiddenChallenge = { label };
206+
return;
207+
}
208+
}
209+
} catch {}
210+
};
211+
for (const element of dialogCandidates) {
212+
if (!visible(element)) {
213+
recordHiddenChallenge(element);
214+
continue;
215+
}
154216
let labelledBy = '';
155217
try {
218+
const idRoot = (typeof element.getRootNode === 'function' && element.getRootNode()) || document;
156219
labelledBy = String(element.getAttribute('aria-labelledby') || '')
157220
.split(/\s+/)
158221
.filter(Boolean)
159-
.map(id => document.getElementById(id)?.textContent || '')
222+
.map(id => (typeof idRoot.getElementById === 'function' ? idRoot : document)
223+
.getElementById(id)?.textContent || '')
224+
.join(' ');
225+
} catch {}
226+
let renderedHeadings = '';
227+
try {
228+
renderedHeadings = Array.from(
229+
element.querySelectorAll?.('h1, h2, h3, [role="heading"]') || []
230+
)
231+
.filter(visible)
232+
.map(heading => heading.textContent || '')
160233
.join(' ');
161234
} catch {}
162235
const values = [
163236
element.getAttribute?.('aria-label'),
164237
labelledBy,
165-
element.querySelector?.('h1, h2, h3, [role="heading"]')?.textContent,
238+
renderedHeadings,
166239
element.getAttribute?.('title'),
167240
element.innerText,
168-
element.textContent,
169241
];
170242
for (const value of values) {
171243
const text = String(value || '');

0 commit comments

Comments
 (0)