@@ -263,18 +263,28 @@ export function applyCaptchaFrameVisibility(candidates, frameContexts, navigatio
263263 const candidate = reconcileAncestorLoader ( rawCandidate ) ;
264264 const frameVisible = frameIsVisible ( candidate ?. frameId )
265265 && candidate ?. frameVisibleWithinAnchor !== false ;
266+ const visible = candidate ?. visible === true && frameVisible ;
266267 return {
267268 ...candidate ,
268269 frameVisible,
269270 websiteURL : nearestHttpUrl ( candidate ) ,
270271 dialogAssociated : candidate ?. dialogAssociated === true
271272 || frameIsDialogAssociated ( candidate ?. frameId ) ,
272- visible : candidate ?. visible === true && frameVisible ,
273+ visible,
273274 normalCheckbox : candidate ?. normalCheckbox === true && candidate ?. visible === true && frameVisible ,
275+ activeChallengeFrameVisible : candidate ?. activeChallengeFrame === true && visible ,
274276 } ;
275277 } ) ;
276278}
277279
280+ function isVisibleActiveChallengeFrame ( candidate ) {
281+ if ( candidate ?. activeChallengeFrameVisible === true ) return true ;
282+ if ( candidate ?. activeChallengeFrameVisible === false ) return false ;
283+ return candidate ?. activeChallengeFrame === true
284+ && candidate ?. visible === true
285+ && candidate ?. frameVisible !== false ;
286+ }
287+
278288function candidateSummary ( candidate ) {
279289 return {
280290 frameId : Number . isInteger ( candidate ?. frameId ) ? candidate . frameId : null ,
@@ -291,6 +301,8 @@ function candidateSummary(candidate) {
291301 visible : candidate ?. visible === true ,
292302 normalCheckbox : candidate ?. normalCheckbox === true ,
293303 challengeFrame : candidate ?. challengeFrame === true ,
304+ activeChallengeFrame : candidate ?. activeChallengeFrame === true ,
305+ activeChallengeFrameVisible : isVisibleActiveChallengeFrame ( candidate ) ,
294306 dialogAssociated : candidate ?. dialogAssociated === true ,
295307 frameVisible : candidate ?. frameVisible !== false ,
296308 isInvisible : candidate ?. isInvisible === true ,
@@ -326,9 +338,7 @@ function candidateScore(candidate) {
326338 // Priority is tiered so no combination of secondary signals can make a
327339 // generic visible/background integration outrank an active challenge
328340 // frame or visible checkbox.
329- const activeChallengeFrame = candidate ?. challengeFrame
330- && candidate ?. visible === true
331- && candidate ?. frameVisible !== false ;
341+ const activeChallengeFrame = isVisibleActiveChallengeFrame ( candidate ) ;
332342 const primary = ( candidate ?. normalCheckbox && candidate ?. visible ) || activeChallengeFrame ;
333343 const tier = primary ? 3 : ( candidate ?. visible ? 2 : 1 ) ;
334344 let score = tier * 1000 ;
@@ -406,6 +416,10 @@ export function selectCaptchaCandidate(candidates, constraints = {}) {
406416 visible : previous . visible === true || candidate . visible === true ,
407417 normalCheckbox : previous . normalCheckbox === true || candidate . normalCheckbox === true ,
408418 challengeFrame : previous . challengeFrame === true || candidate . challengeFrame === true ,
419+ activeChallengeFrame : previous . activeChallengeFrame === true
420+ || candidate . activeChallengeFrame === true ,
421+ activeChallengeFrameVisible : isVisibleActiveChallengeFrame ( previous )
422+ || isVisibleActiveChallengeFrame ( candidate ) ,
409423 dialogAssociated : previous . dialogAssociated === true || candidate . dialogAssociated === true ,
410424 responseField : previous . responseField === true || candidate . responseField === true ,
411425 responseTokenPresent : previous . responseTokenPresent === true
@@ -580,7 +594,7 @@ function selectedReason(candidate, constraints) {
580594 if ( constraints . frameUrl ) return 'exact frameUrl match' ;
581595 if ( constraints . websiteKey ) return 'exact websiteKey match' ;
582596 if ( candidate . normalCheckbox && candidate . visible ) return 'visible checkbox challenge' ;
583- if ( candidate . visible && candidate . challengeFrame && candidate . frameVisible !== false ) return 'visible challenge frame' ;
597+ if ( isVisibleActiveChallengeFrame ( candidate ) ) return 'visible challenge frame' ;
584598 if ( candidate . visible ) return 'visible CAPTCHA widget' ;
585599 if ( candidate . challengeFrame && candidate . frameVisible !== false ) return 'challenge frame candidate' ;
586600 return 'only detected CAPTCHA candidate' ;
@@ -627,6 +641,29 @@ export function detectCaptchaCandidatesInPage(scope = null) {
627641 return null ;
628642 }
629643 } ;
644+ // Page-serialized counterpart of captchaActiveChallengeFrameVendor in
645+ // captcha-gate.js. Only challenge routes, never checkbox routes, may arm it.
646+ const activeChallengeFrameVendor = ( urlStr ) => {
647+ try {
648+ const parsed = new URL ( String ( urlStr || '' ) , frameUrl || 'https://dummy.host' ) ;
649+ const host = parsed . hostname . toLowerCase ( ) ;
650+ const path = parsed . pathname . toLowerCase ( ) ;
651+ if (
652+ ( / ( ^ | \. ) g o o g l e \. c o m $ / . test ( host ) || / ( ^ | \. ) r e c a p t c h a \. n e t $ / . test ( host ) )
653+ && / \/ r e c a p t c h a \/ (?: a p i 2 | e n t e r p r i s e ) \/ b f r a m e (?: \/ | $ ) / . test ( path )
654+ ) return 'recaptcha' ;
655+ if ( / ( ^ | \. ) h c a p t c h a \. c o m $ / . test ( host ) ) {
656+ const frame = new URLSearchParams ( String ( parsed . hash || '' ) . replace ( / ^ # / , '' ) ) . get ( 'frame' ) ;
657+ if ( frame === 'challenge' ) return 'hcaptcha' ;
658+ }
659+ if (
660+ / ( ^ | \. ) (?: a r k o s e l a b s | f u n c a p t c h a ) \. c o m $ / . test ( host )
661+ && / \/ f c \/ g c (?: \/ | $ ) / . test ( path )
662+ ) return 'arkose' ;
663+ } catch ( _ ) { }
664+ return '' ;
665+ } ;
666+ const documentChallengeVendor = activeChallengeFrameVendor ( frameUrl ) ;
630667 const visibleElement = ( element ) => {
631668 if ( ! element ) return false ;
632669 try {
@@ -714,7 +751,9 @@ export function detectCaptchaCandidatesInPage(scope = null) {
714751 candidates . push ( {
715752 ...serializableCandidate ,
716753 frameUrl,
717- challengeFrame,
754+ challengeFrame : candidate . challengeFrame === true || challengeFrame ,
755+ activeChallengeFrame : candidate . activeChallengeFrame === true
756+ || ! ! documentChallengeVendor ,
718757 responseField,
719758 responseTokenPresent : candidate . responseTokenPresent === true
720759 || alsoResponseTokenPresent === true ,
@@ -873,16 +912,21 @@ export function detectCaptchaCandidatesInPage(scope = null) {
873912 const recaptchaFrames = iframeUrls . filter ( ( { url } ) =>
874913 / r e c a p t c h a \/ ( a p i 2 | e n t e r p r i s e ) \/ a n c h o r / i. test ( url )
875914 ) ;
915+ const recaptchaChallengeFrames = iframeUrls . filter ( ( { url } ) =>
916+ activeChallengeFrameVendor ( url ) === 'recaptcha'
917+ ) ;
876918
877919 for ( const { element, url } of iframeUrls ) {
878920 if ( / h c a p t c h a \. c o m / i. test ( url ) ) {
879921 const websiteKey = urlParam ( url , 'sitekey' ) ;
880922 if ( websiteKey ) {
923+ const activeChallengeFrame = activeChallengeFrameVendor ( url ) === 'hcaptcha' ;
881924 add ( {
882925 type : 'hcaptcha' ,
883926 websiteKey,
884927 visible : visibleElement ( element ) ,
885- normalCheckbox : visibleElement ( element ) ,
928+ normalCheckbox : visibleElement ( element ) && ! activeChallengeFrame ,
929+ activeChallengeFrame,
886930 ...responseFieldIdentity (
887931 element ,
888932 'h-captcha-response' ,
@@ -919,10 +963,31 @@ export function detectCaptchaCandidatesInPage(scope = null) {
919963 }
920964 continue ;
921965 }
922- if ( ! / r e c a p t c h a \/ ( a p i 2 | e n t e r p r i s e ) \/ a n c h o r / i. test ( url ) ) continue ;
966+ const recaptchaChallengeFrame = activeChallengeFrameVendor ( url ) === 'recaptcha' ;
967+ if ( ! recaptchaChallengeFrame && ! / r e c a p t c h a \/ ( a p i 2 | e n t e r p r i s e ) \/ a n c h o r / i. test ( url ) ) continue ;
923968 const websiteKey = urlParam ( url , 'k' ) ;
924969 if ( ! websiteKey ) continue ;
925970 const isEnterprise = / r e c a p t c h a \/ e n t e r p r i s e / i. test ( url ) ;
971+ if ( recaptchaChallengeFrame ) {
972+ const challengeIndex = recaptchaChallengeFrames . findIndex ( frame => frame . element === element ) ;
973+ add ( {
974+ type : isEnterprise ? 'recaptcha_v2_enterprise' : 'recaptcha_v2' ,
975+ websiteKey,
976+ isInvisible : false ,
977+ isEnterprise,
978+ visible : visibleElement ( element ) ,
979+ normalCheckbox : false ,
980+ activeChallengeFrame : true ,
981+ ...responseFieldIdentity (
982+ element ,
983+ 'g-recaptcha-response' ,
984+ challengeIndex ,
985+ recaptchaChallengeFrames . length ,
986+ ) ,
987+ detectedVia : 'url' ,
988+ } , element ) ;
989+ continue ;
990+ }
926991 const isInvisible = urlParam ( url , 'size' ) === 'invisible' ;
927992 const matchingV3Script = scriptUrls . find ( scriptUrl => urlParam ( scriptUrl , 'render' ) === websiteKey ) || null ;
928993 const isV3 = ! ! matchingV3Script ;
@@ -994,13 +1059,15 @@ export function detectCaptchaCandidatesInPage(scope = null) {
9941059 try { url = String ( element . src || '' ) ; } catch ( _ ) { }
9951060 try { loadedUrl = String ( element . contentWindow ?. location ?. href || '' ) ; } catch ( _ ) { }
9961061 try { name = String ( element . name || element . getAttribute ?. ( 'name' ) || '' ) ; } catch ( _ ) { }
1062+ const activeChallengeFrame = ! ! activeChallengeFrameVendor ( loadedUrl || url ) ;
9971063 return {
9981064 index,
9991065 url,
10001066 loadedUrl,
10011067 name,
10021068 visible : visibleElement ( element ) ,
10031069 dialogAssociated : elementInChallengeDialog ( element ) ,
1070+ activeChallengeFrame,
10041071 } ;
10051072 } ) ;
10061073 let frameName = '' ;
0 commit comments