Skip to content

Commit

Permalink
Add reCaptcha v3 handler
Browse files Browse the repository at this point in the history
Migrate JavaScript to TypeScript
  • Loading branch information
Cyperghost committed Dec 12, 2024
1 parent c07328a commit 110332a
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 205 deletions.
233 changes: 40 additions & 193 deletions com.woltlab.wcf/templates/shared_recaptcha.tpl
Original file line number Diff line number Diff line change
@@ -1,202 +1,49 @@
{if $recaptchaLegacyMode|empty}
{include file='shared_captcha'}
{else}
{if RECAPTCHA_PUBLICKEY && RECAPTCHA_PRIVATEKEY}
{if $supportsAsyncCaptcha|isset && $supportsAsyncCaptcha && RECAPTCHA_PUBLICKEY_INVISIBLE && RECAPTCHA_PRIVATEKEY_INVISIBLE}
{assign var="recaptchaBucketID" value=true|microtime|sha1}
<dl class="{if $errorField|isset && $errorField == 'recaptchaString'}formError{/if}">
<dt><label>{lang}wcf.recaptcha.title{/lang}</label></dt>
<dd>
<input type="hidden" name="recaptcha-type" value="invisible">
<div id="recaptchaBucket{$recaptchaBucketID}"></div>
<noscript>
<div style="width: 302px; height: 473px;">
<div style="width: 302px; height: 422px; position: relative;">
<div style="width: 302px; height: 422px; position: relative;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k={RECAPTCHA_PUBLICKEY_INVISIBLE|encodeJS}" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; position: relative; border-style: none; bottom: 12px; left: 0; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea name="g-recaptcha-response" class="g-recaptcha-response" style="width: 290px; height: 50px; border: 1px solid #c1c1c1; margin: 5px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
{if (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
{if $errorType|is_array && $errorType[recaptchaString]|isset}
{assign var='__errorType' value=$errorType[recaptchaString]}
{else}
{assign var='__errorType' value=$errorType}
{/if}
<small class="innerError">
{if $__errorType == 'empty'}
{lang}wcf.global.form.error.empty{/lang}
{else}
{lang}wcf.captcha.recaptchaInvisible.error.recaptchaString.{$__errorType}{/lang}
{/if}
</small>
{/if}
</dd>
</dl>
<script data-relocate="true">
if (!WCF.recaptcha) {
WCF.recaptcha = {
queue: [],
callbackCalled: false,
mapping: { }
};
// this needs to be in global scope
function recaptchaCallback() {
var bucketId;
WCF.recaptcha.callbackCalled = true;
// clear queue
while (config = WCF.recaptcha.queue.shift()) {
(function (config) {
var bucketId = config.bucket;
require(['Dom/Traverse', 'Dom/Util'], function (DomTraverse, DomUtil) {
var bucket = elById(bucketId);
var promise = new Promise(function (resolve, reject) {
WCF.recaptcha.mapping['recaptchaBucket{$recaptchaBucketID}'] = grecaptcha.render(bucket, {
sitekey: '{RECAPTCHA_PUBLICKEY_INVISIBLE|encodeJS}',
size: 'invisible',
badge: 'inline',
callback: resolve,
theme: document.documentElement.dataset.colorScheme === "dark" ? "dark" : "light"
});
});
if (config.ajaxCaptcha) {
WCF.System.Captcha.addCallback(config.ajaxCaptcha, function() {
grecaptcha.execute(WCF.recaptcha.mapping['recaptchaBucket{$recaptchaBucketID}']);
return promise.then(function (token) {
return {
'g-recaptcha-response': token,
'recaptcha-type': 'invisible'
};
});
});
}
else {
var form = DomTraverse.parentByTag(bucket, 'FORM');
var pressed = undefined;
elBySelAll('input[type=submit]', form, function (button) {
button.addEventListener('click', function (event) {
pressed = button;
});
});
var listener = function (event) {
event.preventDefault();
promise.then(function (token) {
form.removeEventListener('submit', listener);
pressed.disabled = false;
pressed.click();
});
grecaptcha.execute(WCF.recaptcha.mapping['recaptchaBucket{$recaptchaBucketID}']);
}
form.addEventListener('submit', listener);
}
});
})(config);
}
}
}
// add captcha to queue
WCF.recaptcha.queue.push({
bucket: 'recaptchaBucket{$recaptchaBucketID}'
{if $ajaxCaptcha|isset && $ajaxCaptcha}
, ajaxCaptcha: '{$captchaID}'
{/if}
});
// trigger callback immediately, if API already is available
if (WCF.recaptcha.callbackCalled) setTimeout(recaptchaCallback, 1);
// ensure recaptcha API is loaded at most once
if (!window.grecaptcha) $.getScript('https://www.google.com/recaptcha/api.js?render=explicit&onload=recaptchaCallback');
</script>
{if RECAPTCHA_PUBLICKEY_V3 && RECAPTCHA_PRIVATEKEY_V3}
{assign var="recaptchaType" value="v3"}
{assign var="recaptchaPublicKey" value=RECAPTCHA_PUBLICKEY_V3}
{elseif RECAPTCHA_PUBLICKEY && RECAPTCHA_PRIVATEKEY}
{if RECAPTCHA_PUBLICKEY_INVISIBLE && RECAPTCHA_PRIVATEKEY_INVISIBLE}
{assign var="recaptchaType" value="invisible"}
{assign var="recaptchaPublicKey" value=RECAPTCHA_PUBLICKEY_INVISIBLE}
{else}
{assign var="recaptchaBucketID" value=true|microtime|sha1}
<dl class="{if $errorField|isset && $errorField == 'recaptchaString'}formError{/if}">
<dt><label>{lang}wcf.recaptcha.title{/lang}</label></dt>
<dd>
<input type="hidden" name="recaptcha-type" value="v2">
<div id="recaptchaBucket{$recaptchaBucketID}"></div>
<noscript>
<div style="width: 302px; height: 473px;">
<div style="width: 302px; height: 422px; position: relative;">
<div style="width: 302px; height: 422px; position: relative;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k={RECAPTCHA_PUBLICKEY|encodeJS}" frameborder="0" scrolling="no" style="width: 302px; height:422px; border-style: none;"></iframe>
</div>
<div style="width: 300px; height: 60px; position: relative; border-style: none; bottom: 12px; left: 0; margin: 0px; padding: 0px; right: 25px; background: #f9f9f9; border: 1px solid #c1c1c1; border-radius: 3px;">
<textarea name="g-recaptcha-response" class="g-recaptcha-response" style="width: 290px; height: 50px; border: 1px solid #c1c1c1; margin: 5px; padding: 0px; resize: none;"></textarea>
</div>
</div>
</div>
</noscript>
{if (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
{if $errorType|is_array && $errorType[recaptchaString]|isset}
{assign var='__errorType' value=$errorType[recaptchaString]}
{assign var="recaptchaType" value="v2"}
{assign var="recaptchaPublicKey" value=RECAPTCHA_PUBLICKEY}
{/if}
{/if}
{if !$ajaxCaptcha|isset}
{assign var="ajaxCaptcha" value=false}
{/if}

{if $recaptchaType|isset && $recaptchaPublicKey|isset}
{assign var="recaptchaBucketID" value=true|microtime|sha1}
<dl class="{if $errorField|isset && $errorField == 'recaptchaString'}formError{/if}">
<dt>{if $recaptchaType !== "v3"}<label>{lang}wcf.recaptcha.title{/lang}</label>{/if}</dt>
<dd>
<input type="hidden" name="recaptcha-type" value="{$recaptchaType}">
<div id="recaptchaBucket{$recaptchaBucketID}"></div>
{if (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
{if $errorType|is_array && $errorType[recaptchaString]|isset}
{assign var='__errorType' value=$errorType[recaptchaString]}
{else}
{assign var='__errorType' value=$errorType}
{/if}
<small class="innerError">
{if $__errorType == 'empty'}
{lang}wcf.global.form.error.empty{/lang}
{else}
{assign var='__errorType' value=$errorType}
{lang}wcf.captcha.recaptcha{$recaptchaType|ucfirst}.error.recaptchaString.{$__errorType}{/lang}
{/if}
<small class="innerError">
{if $__errorType == 'empty'}
{lang}wcf.global.form.error.empty{/lang}
{else}
{lang}wcf.captcha.recaptchaV2.error.recaptchaString.{$__errorType}{/lang}
{/if}
</small>
{/if}
</dd>
</dl>
<script data-relocate="true">
if (!WCF.recaptcha) {
WCF.recaptcha = {
queue: [],
callbackCalled: false,
mapping: { }
};
// this needs to be in global scope
function recaptchaCallback() {
var bucket;
WCF.recaptcha.callbackCalled = true;
// clear queue
while (bucket = WCF.recaptcha.queue.shift()) {
WCF.recaptcha.mapping[bucket] = grecaptcha.render(bucket, {
'sitekey' : '{RECAPTCHA_PUBLICKEY|encodeJS}',
theme: document.documentElement.dataset.colorScheme === "dark" ? "dark" : "light"
});
}
}
}
// add captcha to queue
WCF.recaptcha.queue.push('recaptchaBucket{$recaptchaBucketID}');
// trigger callback immediately, if API already is available
if (WCF.recaptcha.callbackCalled) setTimeout(recaptchaCallback, 1);
{if $ajaxCaptcha|isset && $ajaxCaptcha}
WCF.System.Captcha.addCallback('{$captchaID}', function() {
return {
'g-recaptcha-response': grecaptcha.getResponse(WCF.recaptcha.mapping['recaptchaBucket{$recaptchaBucketID}']),
'type': 'v2'
};
</small>
{/if}
</dd>
</dl>
<script data-relocate="true">
require(['WoltLabSuite/Core/Component/Captcha/Recaptcha'], ({ Recaptcha }) => {
new Recaptcha('{$recaptchaType}', '{$recaptchaPublicKey|encodeJS}', 'recaptchaBucket{$recaptchaBucketID}'{if $ajaxCaptcha}, '{$captchaID|encodeJS}'{/if});
});
{/if}
// ensure recaptcha API is loaded at most once
if (!window.grecaptcha) $.getScript('https://www.google.com/recaptcha/api.js?render=explicit&onload=recaptchaCallback');
</script>
{/if}
</script>
{/if}
{/if}
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@googlemaps/markerclusterer": "2.5.3",
"@types/facebook-js-sdk": "^3.3.12",
"@types/google.maps": "^3.58.1",
"@types/grecaptcha": "^3.0.9",
"@types/jquery": "^3.5.32",
"@types/pica": "5.1.3",
"@types/prismjs": "^1.26.5",
Expand Down
Loading

0 comments on commit 110332a

Please sign in to comment.