@@ -291,6 +291,7 @@ function candidateSummary(candidate) {
291291 visible : candidate ?. visible === true ,
292292 normalCheckbox : candidate ?. normalCheckbox === true ,
293293 challengeFrame : candidate ?. challengeFrame === true ,
294+ activeChallengeFrame : candidate ?. activeChallengeFrame === true ,
294295 dialogAssociated : candidate ?. dialogAssociated === true ,
295296 frameVisible : candidate ?. frameVisible !== false ,
296297 isInvisible : candidate ?. isInvisible === true ,
@@ -326,7 +327,7 @@ function candidateScore(candidate) {
326327 // Priority is tiered so no combination of secondary signals can make a
327328 // generic visible/background integration outrank an active challenge
328329 // frame or visible checkbox.
329- const activeChallengeFrame = candidate ?. challengeFrame
330+ const activeChallengeFrame = candidate ?. activeChallengeFrame
330331 && candidate ?. visible === true
331332 && candidate ?. frameVisible !== false ;
332333 const primary = ( candidate ?. normalCheckbox && candidate ?. visible ) || activeChallengeFrame ;
@@ -406,6 +407,8 @@ export function selectCaptchaCandidate(candidates, constraints = {}) {
406407 visible : previous . visible === true || candidate . visible === true ,
407408 normalCheckbox : previous . normalCheckbox === true || candidate . normalCheckbox === true ,
408409 challengeFrame : previous . challengeFrame === true || candidate . challengeFrame === true ,
410+ activeChallengeFrame : previous . activeChallengeFrame === true
411+ || candidate . activeChallengeFrame === true ,
409412 dialogAssociated : previous . dialogAssociated === true || candidate . dialogAssociated === true ,
410413 responseField : previous . responseField === true || candidate . responseField === true ,
411414 responseTokenPresent : previous . responseTokenPresent === true
@@ -580,7 +583,7 @@ function selectedReason(candidate, constraints) {
580583 if ( constraints . frameUrl ) return 'exact frameUrl match' ;
581584 if ( constraints . websiteKey ) return 'exact websiteKey match' ;
582585 if ( candidate . normalCheckbox && candidate . visible ) return 'visible checkbox challenge' ;
583- if ( candidate . visible && candidate . challengeFrame && candidate . frameVisible !== false ) return 'visible challenge frame' ;
586+ if ( candidate . visible && candidate . activeChallengeFrame && candidate . frameVisible !== false ) return 'visible challenge frame' ;
584587 if ( candidate . visible ) return 'visible CAPTCHA widget' ;
585588 if ( candidate . challengeFrame && candidate . frameVisible !== false ) return 'challenge frame candidate' ;
586589 return 'only detected CAPTCHA candidate' ;
@@ -627,6 +630,29 @@ export function detectCaptchaCandidatesInPage(scope = null) {
627630 return null ;
628631 }
629632 } ;
633+ // Page-serialized counterpart of captchaActiveChallengeFrameVendor in
634+ // captcha-gate.js. Only challenge routes, never checkbox routes, may arm it.
635+ const activeChallengeFrameVendor = ( urlStr ) => {
636+ try {
637+ const parsed = new URL ( String ( urlStr || '' ) , frameUrl || 'https://dummy.host' ) ;
638+ const host = parsed . hostname . toLowerCase ( ) ;
639+ const path = parsed . pathname . toLowerCase ( ) ;
640+ if (
641+ ( / ( ^ | \. ) 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 ) )
642+ && / \/ 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 )
643+ ) return 'recaptcha' ;
644+ if ( / ( ^ | \. ) h c a p t c h a \. c o m $ / . test ( host ) ) {
645+ const frame = new URLSearchParams ( String ( parsed . hash || '' ) . replace ( / ^ # / , '' ) ) . get ( 'frame' ) ;
646+ if ( frame === 'challenge' ) return 'hcaptcha' ;
647+ }
648+ if (
649+ / ( ^ | \. ) (?: 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 )
650+ && / \/ f c \/ g c (?: \/ | $ ) / . test ( path )
651+ ) return 'arkose' ;
652+ } catch ( _ ) { }
653+ return '' ;
654+ } ;
655+ const documentChallengeVendor = activeChallengeFrameVendor ( frameUrl ) ;
630656 const visibleElement = ( element ) => {
631657 if ( ! element ) return false ;
632658 try {
@@ -714,7 +740,9 @@ export function detectCaptchaCandidatesInPage(scope = null) {
714740 candidates . push ( {
715741 ...serializableCandidate ,
716742 frameUrl,
717- challengeFrame,
743+ challengeFrame : candidate . challengeFrame === true || challengeFrame ,
744+ activeChallengeFrame : candidate . activeChallengeFrame === true
745+ || ! ! documentChallengeVendor ,
718746 responseField,
719747 responseTokenPresent : candidate . responseTokenPresent === true
720748 || alsoResponseTokenPresent === true ,
@@ -873,16 +901,21 @@ export function detectCaptchaCandidatesInPage(scope = null) {
873901 const recaptchaFrames = iframeUrls . filter ( ( { url } ) =>
874902 / 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 )
875903 ) ;
904+ const recaptchaChallengeFrames = iframeUrls . filter ( ( { url } ) =>
905+ activeChallengeFrameVendor ( url ) === 'recaptcha'
906+ ) ;
876907
877908 for ( const { element, url } of iframeUrls ) {
878909 if ( / h c a p t c h a \. c o m / i. test ( url ) ) {
879910 const websiteKey = urlParam ( url , 'sitekey' ) ;
880911 if ( websiteKey ) {
912+ const activeChallengeFrame = activeChallengeFrameVendor ( url ) === 'hcaptcha' ;
881913 add ( {
882914 type : 'hcaptcha' ,
883915 websiteKey,
884916 visible : visibleElement ( element ) ,
885- normalCheckbox : visibleElement ( element ) ,
917+ normalCheckbox : visibleElement ( element ) && ! activeChallengeFrame ,
918+ activeChallengeFrame,
886919 ...responseFieldIdentity (
887920 element ,
888921 'h-captcha-response' ,
@@ -919,10 +952,31 @@ export function detectCaptchaCandidatesInPage(scope = null) {
919952 }
920953 continue ;
921954 }
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 ;
955+ const recaptchaChallengeFrame = activeChallengeFrameVendor ( url ) === 'recaptcha' ;
956+ 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 ;
923957 const websiteKey = urlParam ( url , 'k' ) ;
924958 if ( ! websiteKey ) continue ;
925959 const isEnterprise = / r e c a p t c h a \/ e n t e r p r i s e / i. test ( url ) ;
960+ if ( recaptchaChallengeFrame ) {
961+ const challengeIndex = recaptchaChallengeFrames . findIndex ( frame => frame . element === element ) ;
962+ add ( {
963+ type : isEnterprise ? 'recaptcha_v2_enterprise' : 'recaptcha_v2' ,
964+ websiteKey,
965+ isInvisible : false ,
966+ isEnterprise,
967+ visible : visibleElement ( element ) ,
968+ normalCheckbox : false ,
969+ activeChallengeFrame : true ,
970+ ...responseFieldIdentity (
971+ element ,
972+ 'g-recaptcha-response' ,
973+ challengeIndex ,
974+ recaptchaChallengeFrames . length ,
975+ ) ,
976+ detectedVia : 'url' ,
977+ } , element ) ;
978+ continue ;
979+ }
926980 const isInvisible = urlParam ( url , 'size' ) === 'invisible' ;
927981 const matchingV3Script = scriptUrls . find ( scriptUrl => urlParam ( scriptUrl , 'render' ) === websiteKey ) || null ;
928982 const isV3 = ! ! matchingV3Script ;
@@ -994,13 +1048,15 @@ export function detectCaptchaCandidatesInPage(scope = null) {
9941048 try { url = String ( element . src || '' ) ; } catch ( _ ) { }
9951049 try { loadedUrl = String ( element . contentWindow ?. location ?. href || '' ) ; } catch ( _ ) { }
9961050 try { name = String ( element . name || element . getAttribute ?. ( 'name' ) || '' ) ; } catch ( _ ) { }
1051+ const activeChallengeFrame = ! ! activeChallengeFrameVendor ( loadedUrl || url ) ;
9971052 return {
9981053 index,
9991054 url,
10001055 loadedUrl,
10011056 name,
10021057 visible : visibleElement ( element ) ,
10031058 dialogAssociated : elementInChallengeDialog ( element ) ,
1059+ activeChallengeFrame,
10041060 } ;
10051061 } ) ;
10061062 let frameName = '' ;
0 commit comments