Skip to content
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
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ dependencies {
implementation libs.drawerlayout
implementation libs.swiperefreshlayout
implementation libs.work.runtime.ktx
implementation libs.hcaptcha
implementation libs.metrics.platform

implementation libs.okhttp.tls
Expand Down
73 changes: 73 additions & 0 deletions app/src/main/java/org/wikipedia/captcha/HCaptchaHelper.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package org.wikipedia.captcha

import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import com.hcaptcha.sdk.HCaptcha
import com.hcaptcha.sdk.HCaptchaConfig
import com.hcaptcha.sdk.HCaptchaTheme
import com.hcaptcha.sdk.HCaptchaTokenResponse
import org.wikipedia.WikipediaApp
import org.wikipedia.settings.RemoteConfig
import org.wikipedia.util.FeedbackUtil
import org.wikipedia.util.log.L
import kotlin.String

class HCaptchaHelper(
private val activity: FragmentActivity,
private val callback: Callback
) {
fun interface Callback {
fun onSuccess(token: String)
}

private var hCaptcha: HCaptcha? = null
private var tokenResponse: HCaptchaTokenResponse? = null

private val configDefault get() = RemoteConfig.RemoteConfigHCaptcha(
baseURL = "https://meta.wikimedia.org",
jsSrc = "https://assets-hcaptcha.wikimedia.org/1/api.js",
endpoint = "https://hcaptcha.wikimedia.org",
assetHost = "https://assets-hcaptcha.wikimedia.org",
imgHost = "https://imgs-hcaptcha.wikimedia.org",
reportApi = "https://report-hcaptcha.wikimedia.org",
sentry = false,
siteKey = "e11698d6-51ca-4980-875c-72309c6678cc"
)

fun show() {
if (hCaptcha == null) {
val config = RemoteConfig.config.androidv1?.hCaptcha ?: configDefault
hCaptcha = HCaptcha.getClient(activity)
hCaptcha?.setup(
HCaptchaConfig.builder()
.theme(if (WikipediaApp.instance.currentTheme.isDark) HCaptchaTheme.DARK else HCaptchaTheme.LIGHT)
.siteKey(config.siteKey)
.host(config.baseURL.toUri().host)
.jsSrc(config.jsSrc)
.endpoint(config.endpoint)
.assethost(config.assetHost)
.imghost(config.imgHost)
.reportapi(config.reportApi)
.sentry(config.sentry)
.loading(true)
.build())

hCaptcha?.addOnSuccessListener { response ->
tokenResponse = response
callback.onSuccess(response.tokenResult)
}?.addOnFailureListener { e ->
tokenResponse = null
L.e("hCaptcha failed: ${e.message} (${e.statusCode})")
FeedbackUtil.showMessage(activity, "${e.message} (${e.statusCode})")
}?.addOnOpenListener {
L.d("hCaptcha opened")
}
}
hCaptcha?.verifyWithHCaptcha()
}

fun cleanup() {
hCaptcha?.reset()
hCaptcha = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import org.wikipedia.analytics.eventplatform.CreateAccountEvent
import org.wikipedia.auth.AccountUtil
import org.wikipedia.captcha.CaptchaHandler
import org.wikipedia.captcha.CaptchaResult
import org.wikipedia.captcha.HCaptchaHelper
import org.wikipedia.databinding.ActivityCreateAccountBinding
import org.wikipedia.page.PageTitle
import org.wikipedia.util.DeviceUtil
Expand All @@ -49,6 +50,10 @@ class CreateAccountActivity : BaseActivity() {
private var userNameTextWatcher: TextWatcher? = null
private val viewModel: CreateAccountActivityViewModel by viewModels()

private val hCaptchaHelper = HCaptchaHelper(this) { token ->
doCreateAccount(viewModel.token.orEmpty(), hCaptchaToken = token)
}

public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCreateAccountBinding.inflate(layoutInflater)
Expand Down Expand Up @@ -92,6 +97,9 @@ class CreateAccountActivity : BaseActivity() {
is CreateAccountActivityViewModel.AccountInfoState.DoCreateAccount -> {
doCreateAccount(it.token)
}
is CreateAccountActivityViewModel.AccountInfoState.HandleHCaptcha -> {
hCaptchaHelper.show()
}
is CreateAccountActivityViewModel.AccountInfoState.HandleCaptcha -> {
captchaHandler.handleCaptcha(it.token, CaptchaResult(it.captchaId))
}
Expand Down Expand Up @@ -195,13 +203,15 @@ class CreateAccountActivity : BaseActivity() {
L.w("Account creation failed with result $message")
}

private fun doCreateAccount(token: String) {
private fun doCreateAccount(token: String, hCaptchaToken: String? = null) {
showProgressBar(true)
val email = getText(binding.createAccountEmail).ifEmpty { null }
val password = getText(binding.createAccountPasswordInput)
val repeat = getText(binding.createAccountPasswordRepeat)
val userName = getText(binding.createAccountUsername)
viewModel.doCreateAccount(token, captchaHandler.captchaId().toString(), captchaHandler.captchaWord().toString(), userName, password, repeat, email)
viewModel.doCreateAccount(token, captchaHandler.captchaId(),
if (hCaptchaToken.isNullOrEmpty()) captchaHandler.captchaWord() else hCaptchaToken,
userName, password, repeat, email)
}

public override fun onStop() {
Expand All @@ -210,6 +220,7 @@ class CreateAccountActivity : BaseActivity() {
}

public override fun onDestroy() {
hCaptchaHelper.cleanup()
captchaHandler.dispose()
userNameTextWatcher?.let { binding.createAccountUsername.editText?.removeTextChangedListener(it) }
super.onDestroy()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,30 @@ class CreateAccountActivityViewModel : ViewModel() {
private val _verifyUserNameState = MutableSharedFlow<UserNameState>()
val verifyUserNameState = _verifyUserNameState.asSharedFlow()

var token: String? = null

private var verifyUserNameJob: Job? = null

fun createAccountInfo() {
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
_createAccountInfoState.value = AccountInfoState.Error(throwable)
}) {
val response = ServiceFactory.get(WikipediaApp.instance.wikiSite).getAuthManagerInfo()
val token = response.query?.createAccountToken()
token = response.query?.createAccountToken()
val captchaId = response.query?.captchaId()
if (token.isNullOrEmpty()) {
_createAccountInfoState.value = AccountInfoState.InvalidToken
} else if (response.query?.hasHCaptchaRequest() == true) {
_createAccountInfoState.value = AccountInfoState.HandleHCaptcha(token!!)
} else if (!captchaId.isNullOrEmpty()) {
_createAccountInfoState.value = AccountInfoState.HandleCaptcha(token, captchaId)
_createAccountInfoState.value = AccountInfoState.HandleCaptcha(token!!, captchaId)
} else {
_createAccountInfoState.value = AccountInfoState.DoCreateAccount(token)
_createAccountInfoState.value = AccountInfoState.DoCreateAccount(token!!)
}
}
}

fun doCreateAccount(token: String, captchaId: String, captchaWord: String, userName: String, password: String, repeat: String, email: String?) {
fun doCreateAccount(token: String, captchaId: String?, captchaWord: String?, userName: String, password: String, repeat: String, email: String?) {
viewModelScope.launch(CoroutineExceptionHandler { _, throwable ->
_doCreateAccountState.value = CreateAccountState.Error(throwable)
}) {
Expand Down Expand Up @@ -84,7 +88,8 @@ class CreateAccountActivityViewModel : ViewModel() {

open class AccountInfoState {
data class DoCreateAccount(val token: String) : AccountInfoState()
data class HandleCaptcha(val token: String?, val captchaId: String) : AccountInfoState()
data class HandleCaptcha(val token: String, val captchaId: String) : AccountInfoState()
data class HandleHCaptcha(val token: String) : AccountInfoState()
data object InvalidToken : AccountInfoState()
data class Error(val throwable: Throwable) : AccountInfoState()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ internal class MwAuthManagerInfo {
internal class Request(val id: String? = null,
private val metadata: Map<String, String>? = null,
private val required: String? = null,
private val provider: String? = null,
val provider: String? = null,
private val account: String? = null,
val fields: Map<String, Field>? = null)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ class MwQueryResult {
return amInfo?.requests?.find { it.fields?.containsKey(key) == true }?.fields?.get(key)?.value
}

fun hasHCaptchaRequest(): Boolean {
return amInfo?.requests?.find { it.provider.orEmpty().lowercase().contains("hcaptcha") } != null
}

fun getUserResponse(userName: String): UserInfo? {
// MediaWiki user names are case sensitive, but the first letter is always capitalized.
return users?.find { StringUtil.capitalize(userName) == it.name }
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ googlePayVersion = "19.4.0"
googleServices = "4.4.3"
gradle = "8.13.0"
hamcrest = "3.0"
hcaptcha = "4.2.3"
installreferrer = "2.2"
jsoup = "1.21.2"
junit = "4.13.2"
Expand Down Expand Up @@ -86,6 +87,7 @@ fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragm
google-services = { module = "com.google.gms:google-services", version.ref = "googleServices" }
gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" }
hamcrest = { module = "org.hamcrest:hamcrest", version.ref = "hamcrest" }
hcaptcha = { module = "com.github.hCaptcha.hcaptcha-android-sdk:sdk", version.ref = "hcaptcha" }
installreferrer = { module = "com.android.installreferrer:installreferrer", version.ref = "installreferrer" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { module = "junit:junit", version.ref = "junit" }
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencyResolutionManagement {
google()
mavenCentral()
mavenLocal()
maven { setUrl("https://jitpack.io") }
}
}

Expand Down
Loading