Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate to hcaptcha-loader #144

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ plugins {
id "com.github.spotbugs" version "5.2.3"
id "org.owasp.dependencycheck" version "7.1.1"
id "org.sonarqube" version "3.4.0.2513"
id "de.undercouch.download" version "5.5.0"
}

ext {
hcaptchaLoaderVersion = "1.2.4"
}

android {
Expand All @@ -18,7 +23,7 @@ android {
}

defaultConfig {
minSdkVersion 16
minSdkVersion 16 // Android 4.1
targetSdkVersion 34

// See https://developer.android.com/studio/publish/versioning
Expand All @@ -31,6 +36,7 @@ android {
versionName "4.0.0"

buildConfigField 'String', 'VERSION_NAME', "\"${defaultConfig.versionName}_${defaultConfig.versionCode}\""
buildConfigField 'String', 'LOADER_VERSION', "\"${hcaptchaLoaderVersion}\""

consumerProguardFiles "consumer-rules.pro"
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
Expand Down Expand Up @@ -111,7 +117,31 @@ project.afterEvaluate {
}
}

long MAX_AAR_SIZE_KB = 200
long MAX_AAR_SIZE_KB = 250

String sha256Integrity(String filename) {
def file = new File("${buildDir}/generated/assets/hcaptcha/${filename}")
def sha256Hash = file.withInputStream { stream ->
org.apache.commons.codec.digest.DigestUtils.sha256(stream)
}
def base64Hash = org.apache.commons.codec.binary.Base64.encodeBase64String(sha256Hash)

return "sha256-${base64Hash}"
}

tasks.register('downloadPolyfillsJs', Download) {
src "https://www.unpkg.com/@hcaptcha/loader@${hcaptchaLoaderVersion}/dist/polyfills.js"
dest layout.buildDirectory.file("generated/assets/hcaptcha/polyfills.js")
onlyIfModified true
}

tasks.register('downloadHCaptchaLoaderJs', Download) {
src "https://www.unpkg.com/@hcaptcha/loader@${hcaptchaLoaderVersion}/dist/index.es5.js"
dest layout.buildDirectory.file("generated/assets/hcaptcha/loader.js")
onlyIfModified true
}.get().dependsOn('downloadPolyfillsJs')

android.sourceSets.main.assets.srcDirs += [layout.buildDirectory.file("generated/assets")]

android.libraryVariants.all { variant ->
def variantName = variant.name.capitalize()
Expand All @@ -136,7 +166,7 @@ android.libraryVariants.all { variant ->
var aarFile = variant.packageLibraryProvider.get().archiveFile.get().getAsFile()
long aarSizeKb = aarFile.length() / 1024
if (aarSizeKb > MAX_AAR_SIZE_KB) {
throw new GradleException("${aarPath} size exceeded! ${aarSizeKb}Kbyte > ${MAX_AAR_SIZE_KB}Kbyte")
throw new GradleException("${aarFile} size exceeded! ${aarSizeKb}Kbyte > ${MAX_AAR_SIZE_KB}Kbyte")
}
}
})
Expand All @@ -150,9 +180,14 @@ android.libraryVariants.all { variant ->
doFirst {
def outputJavaClass = file("$outputDir/HCaptchaHtml.java")
def template = file("$projectDir/src/main/html/HCaptchaHtml.java.tml").text
def polyfillsIntegrity = sha256Integrity('polyfills.js')
def loaderIntegrity = sha256Integrity('loader.js')
def html = file("$projectDir/src/main/html/hcaptcha.html")
.readLines()
.stream()
.map({l -> "${l.replaceAll('@LOADER_VERSION@', hcaptchaLoaderVersion)}"})
.map({l -> "${l.replaceAll('@POLYFILLS_INTEGRITY@', polyfillsIntegrity)}"})
.map({l -> "${l.replaceAll('@LOADER_INTEGRITY@', loaderIntegrity)}"})
.map({l -> "\"${l.replaceAll('"', '\\\\"')}\\n\""})
.collect(java.util.stream.Collectors.joining("\n${' ' * 16}+ "))

Expand All @@ -167,8 +202,15 @@ android.libraryVariants.all { variant ->
}
}

// preBuild.dependsOn generateTask
variant.registerJavaGeneratingTask(generateTask, outputDir)
generateTask.dependsOn(downloadHCaptchaLoaderJs)

tasks.named("package${variant.name.capitalize()}Assets")
.get()
.dependsOn(downloadHCaptchaLoaderJs)
tasks.named("merge${variant.name.capitalize()}Assets")
.get()
.dependsOn(downloadHCaptchaLoaderJs)
}

apply from: "$rootProject.projectDir/gradle/shared/code-quality.gradle"
182 changes: 93 additions & 89 deletions sdk/src/main/html/hcaptcha.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@
window.sysDebug = JSON.parse(window.JSDI.getSysDebug());
}
</script>
<script type="text/javascript">
<script type="text/javascript" integrity="@POLYFILLS_INTEGRITY@" crossorigin="anonymous" src="https://unpkg.com/@hcaptcha/loader@@LOADER_VERSION@/dist/polyfills.js"></script>
<script type="text/javascript" integrity="@LOADER_INTEGRITY@" crossorigin="anonymous" src="https://unpkg.com/@hcaptcha/loader@@LOADER_VERSION@/dist/index.es5.js"></script>
<script>
// Android will inject this bridge object as `JSInterface`
// Browser is missing it, so we mock it
var BridgeObject = window.JSInterface || {
Expand Down Expand Up @@ -70,20 +72,40 @@
}
};
var bridgeConfig = JSON.parse(BridgeObject.getConfig());
var hCaptchaID = null;
/**
* Called programmatically from HCaptchaWebViewHelper.
*/
function resetAndExecute() {
hcaptcha.reset();
hcaptcha.execute(hCaptchaID);
execute();
}
window.resetAndExecute = resetAndExecute;
function reset() {
hcaptcha.reset();
}
function getTheme(bridgeConfig) {
var theme = bridgeConfig.theme;
var customTheme = bridgeConfig.customTheme;

function errorCallback(error) {
switch(error) {
case "rate-limited":
return BridgeObject.onError(31);
case "network-error":
return BridgeObject.onError(7);
case "invalid-data":
return BridgeObject.onError(8);
case "challenge-error":
return BridgeObject.onError(9);
case "internal-error":
return BridgeObject.onError(10);
default:
// Error not handled? Log it for debugging purposes
console.error(error);
return BridgeObject.onError(29);
}
}

function getTheme(config) {
var theme = config.theme;
var customTheme = config.customTheme;
if (customTheme) {
try {
return JSON.parse(customTheme);
Expand All @@ -94,112 +116,94 @@
}
return theme;
}
function getRenderConfig() {
function getRenderConfig(config) {
return {
sitekey: bridgeConfig.siteKey,
size: bridgeConfig.size,
orientation: bridgeConfig.orientation,
theme: getTheme(bridgeConfig),
sitekey: config.siteKey,
size: config.size,
orientation: config.orientation,
theme: getTheme(config),
host: config.host || config.siteKey + '.android-sdk.hcaptcha.com',
callback: function callback(token) {
console.log("callback");
return BridgeObject.onPass(token);
},
'chalexpired-callback': function chalexpiredCallback() {
console.log("chalexpired-callback");
return BridgeObject.onError(15);
},
'close-callback': function closeCallback() {
console.log("close-callback");
return BridgeObject.onError(30);
},
'error-callback': function errorCallback(error) {
switch(error) {
case "rate-limited":
return BridgeObject.onError(31);
case "network-error":
return BridgeObject.onError(7);
case "invalid-data":
return BridgeObject.onError(8);
case "challenge-error":
return BridgeObject.onError(9);
case "internal-error":
return BridgeObject.onError(10);
default:
// Error not handled? Log it for debugging purposes
console.error(error);
return BridgeObject.onError(29);
}
},
'open-callback': function openCallback() {
return BridgeObject.onOpen();
}
},
'error-callback': errorCallback
};
}
function onHcaptchaLoaded() {
try {
var renderConfig = getRenderConfig();
hCaptchaID = hcaptcha.render('hcaptcha-container', renderConfig);
BridgeObject.onLoaded();
var rqdata = bridgeConfig.rqdata;
if (rqdata) {
hcaptcha.setData(hCaptchaID, { rqdata: rqdata });
}
if (renderConfig.size === 'invisible' && !bridgeConfig.hideDialog) {
// We want to auto execute in case of `invisible` checkbox.
// But not in case of `hideDialog` since verification process
// might be desired to happen at a later time.
hcaptcha.execute(hCaptchaID);
}
} catch (e) {
console.error(e);
BridgeObject.onError(29);
}
}
function addQueryParamIfDefined(url, queryName, queryValue) {
if (queryValue !== undefined && queryValue !== null) {
var link = url.indexOf('?') !== -1 ? '&' : '?';
return url + link + queryName + '=' + encodeURIComponent(queryValue);
}
return url;
}
function loadApi() {
var siteKey = bridgeConfig.siteKey;
var locale = bridgeConfig.locale;
var sentry = bridgeConfig.sentry;
var jsSrc = bridgeConfig.jsSrc;
var endpoint = bridgeConfig.endpoint;
var assethost = bridgeConfig.assethost;
var imghost = bridgeConfig.imghost;
var reportapi = bridgeConfig.reportapi;
var host = bridgeConfig.host || siteKey + '.android-sdk.hcaptcha.com';
var scriptSrc = jsSrc + '?render=explicit&onload=' + onHcaptchaLoaded.name;
scriptSrc = addQueryParamIfDefined(scriptSrc, 'recaptchacompat', 'off');
scriptSrc = addQueryParamIfDefined(scriptSrc, 'hl', locale);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'host', host);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'sentry', sentry);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'endpoint', endpoint);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'assethost', assethost);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'imghost', imghost);
scriptSrc = addQueryParamIfDefined(scriptSrc, 'reportapi', reportapi);
if (bridgeConfig.customTheme) {
scriptSrc = addQueryParamIfDefined(scriptSrc, 'custom', 'true');
}
var script = document.createElement('script');
script.async = true;
script.src = scriptSrc;
script.onerror = function () {
// network issue
BridgeObject.onError(7);
function getScriptParams(config) {
return {
scriptLocation: document.head,
apihost: config.jsSrc,
loadAsync: true,
async: true,
};
document.head.appendChild(script);
}
function getLoaderParams(config) {
var result = getScriptParams(config);

result.render = 'explicit';
result.sentry = config.sentry;
result.custom = !!config.customTheme;
result.assethost = config.assethost;
result.imghost = config.imghost;
result.reportapi = config.reportapi;
result.endpoint = config.endpoint;
result.host = config.host || config.siteKey + '.android-sdk.hcaptcha.com';
result.recaptchacompat = 'off';
result.hl = config.locale;
result.cleanup = true;

return result;
}
var container = document.getElementById("hcaptcha-container");
container.addEventListener("click", function () {
if (window.hcaptcha) {
if (hcaptcha) {
// Allows dismissal of checkbox view
window.hcaptcha.close();
hcaptcha.close();
} else {
BridgeObject.onError(30);
}
});
loadApi();
function execute() {
hcaptcha.execute(getScriptParams(bridgeConfig)).then(function(result) {
var response = result.response;
BridgeObject.onPass(response);
}).catch(function(error) {
errorCallback(error);
});
}
function loadApi(config) {
hCaptchaLoader(getLoaderParams(config)).then(function(hcaptcha) {
var renderConfig = getRenderConfig(config);
hcaptcha.render("hcaptcha-container", renderConfig);
BridgeObject.onLoaded();
var rqdata = config.rqdata;
if (rqdata) {
hcaptcha.setData({ rqdata: rqdata });
}
if (renderConfig.size === 'invisible' && !config.hideDialog) {
// We want to auto execute in case of `invisible` checkbox.
// But not in case of `hideDialog` since verification process
// might be desired to happen at a later time.
execute();
}
}).catch(function(error) {
console.error("loadApi error", error);
BridgeObject.onError(29);
});
}
loadApi(bridgeConfig);
</script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ public void onAnimationEnd(Animator animation) {

@Override
public void onLoaded() {
HCaptchaLog.d("DialogFragment.onLoaded");
assert webViewHelper != null;

if (webViewHelper.getConfig().getSize() != HCaptchaSize.INVISIBLE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public void onSuccess(final String token) {

@Override
public void onLoaded() {
HCaptchaLog.d("HeadlessWebView.onLoaded");
webViewLoaded = true;
if (shouldResetOnLoad) {
shouldResetOnLoad = false;
Expand Down
Loading
Loading