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 RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
26.8
-----
* [**] Resolved an issue where the editor could become impossible to exit when it failed to load.
* [*] Atomic sites can now create application passwords without leaving the app.

26.7
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.wordpress.android.modules

import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.wordpress.android.R
import org.wordpress.android.fluxc.module.ApplicationPasswordsClientId
import org.wordpress.android.util.BuildConfigWrapper
import org.wordpress.android.util.DeviceUtils
import javax.inject.Singleton

@InstallIn(SingletonComponent::class)
@Module
object ApplicationPasswordsClientIdModule {
@Provides
@Singleton
@ApplicationPasswordsClientId
fun provideApplicationPasswordsClientId(
@ApplicationContext context: Context,
buildConfigWrapper: BuildConfigWrapper,
): String {
val deviceName = DeviceUtils.getInstance().getDeviceName(context)
val resId = if (buildConfigWrapper.isJetpackApp) {
R.string.application_password_app_name_jetpack
} else {
R.string.application_password_app_name_wordpress
}
return context.getString(resId, deviceName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,49 +49,54 @@ class ApplicationPasswordLoginHelper @Inject constructor(
) {
private var processedAppPasswordData: String? = null

sealed class DiscoveryResult {
data class Authorized(val authorizationUrl: String) : DiscoveryResult()
data class Failed(val userFacingMessage: String) : DiscoveryResult()
}

@Suppress("TooGenericExceptionCaught")
suspend fun getAuthorizationUrlComplete(siteUrl: String): String =
suspend fun getAuthorizationUrlComplete(siteUrl: String): DiscoveryResult =
try {
getAuthorizationUrlCompleteInternal(siteUrl)
} catch (throwable: Throwable) {
handleAuthenticationDiscoveryError(siteUrl, throwable)
handleAuthenticationDiscoveryError(siteUrl, throwable.message ?: throwable::class.simpleName.orEmpty())
}

private suspend fun getAuthorizationUrlCompleteInternal(siteUrl: String): String = withContext(bgDispatcher) {
when (val urlDiscoveryResult = wpLoginClient.apiDiscovery(siteUrl)) {
is ApiDiscoveryResult.Success -> {
val authorizationUrl =
discoverSuccessWrapper.getApplicationPasswordsAuthenticationUrl(urlDiscoveryResult)
val apiRootUrl = discoverSuccessWrapper.getApiRootUrl(urlDiscoveryResult)
if (apiRootUrl.isNotEmpty()) {
// Store the ApiRootUrl for use it after the login
apiRootUrlCache.put(UrlUtils.normalizeUrl(siteUrl), apiRootUrl)
private suspend fun getAuthorizationUrlCompleteInternal(siteUrl: String): DiscoveryResult =
withContext(bgDispatcher) {
when (val urlDiscoveryResult = wpLoginClient.apiDiscovery(siteUrl)) {
is ApiDiscoveryResult.Success -> {
val authorizationUrl =
discoverSuccessWrapper.getApplicationPasswordsAuthenticationUrl(urlDiscoveryResult)
val apiRootUrl = discoverSuccessWrapper.getApiRootUrl(urlDiscoveryResult)
if (apiRootUrl.isNotEmpty()) {
// Store the ApiRootUrl for use it after the login
apiRootUrlCache.put(UrlUtils.normalizeUrl(siteUrl), apiRootUrl)
}
val authorizationUrlComplete =
uriLoginWrapper.appendParamsToRestAuthorizationUrl(authorizationUrl)
appLogWrapper.d(
AppLog.T.API,
"A_P: Found authorization for $siteUrl URL: $authorizationUrlComplete " +
"API_ROOT_URL $apiRootUrl")
AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_SUCCESSFUL)
DiscoveryResult.Authorized(authorizationUrlComplete)
}
val authorizationUrlComplete =
uriLoginWrapper.appendParamsToRestAuthorizationUrl(authorizationUrl)
appLogWrapper.d(
AppLog.T.API,
"A_P: Found authorization for $siteUrl URL: $authorizationUrlComplete " +
"API_ROOT_URL $apiRootUrl")
AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_SUCCESSFUL)
authorizationUrlComplete
}

is ApiDiscoveryResult.FailureFetchAndParseApiRoot ->
handleAuthenticationDiscoveryError(siteUrl, Exception("FailureFetchAndParseApiRoot"))

is ApiDiscoveryResult.FailureFindApiRoot ->
handleAuthenticationDiscoveryError(siteUrl, Exception("FailureFindApiRoot"))

is ApiDiscoveryResult.FailureParseSiteUrl ->
handleAuthenticationDiscoveryError(siteUrl, urlDiscoveryResult.error)
is ApiDiscoveryResult.FailureFetchAndParseApiRoot,
is ApiDiscoveryResult.FailureFindApiRoot,
is ApiDiscoveryResult.FailureParseSiteUrl ->
handleAuthenticationDiscoveryError(
siteUrl,
urlDiscoveryResult.userFacingErrorMessage(siteUrl).orEmpty()
)
}
}
}

private fun handleAuthenticationDiscoveryError(siteUrl: String, throwable: Throwable): String {
appLogWrapper.e(AppLog.T.API, "A_P: Error during API discovery for $siteUrl - ${throwable.message}")
private fun handleAuthenticationDiscoveryError(siteUrl: String, message: String): DiscoveryResult {
appLogWrapper.e(AppLog.T.API, "A_P: Error during API discovery for $siteUrl - $message")
AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
return ""
return DiscoveryResult.Failed(message)
}

sealed class StoreCredentialsResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,17 @@ class ApplicationPasswordAutoAuthDialogViewModel @Inject constructor(
@Suppress("TooGenericExceptionCaught")
private suspend fun fallbackToManualLogin(siteUrl: String) {
try {
val authUrl = applicationPasswordLoginHelper.getAuthorizationUrlComplete(siteUrl)
_navigationEvent.emit(NavigationEvent.FallbackToManualLogin(authUrl))
when (val result = applicationPasswordLoginHelper.getAuthorizationUrlComplete(siteUrl)) {
is ApplicationPasswordLoginHelper.DiscoveryResult.Authorized ->
_navigationEvent.emit(NavigationEvent.FallbackToManualLogin(result.authorizationUrl))
is ApplicationPasswordLoginHelper.DiscoveryResult.Failed -> {
appLogWrapper.e(
AppLog.T.API,
"A_P: Discovery failed for: $siteUrl - ${result.userFacingMessage}"
)
_navigationEvent.emit(NavigationEvent.Error)
}
}
} catch (e: Exception) {
appLogWrapper.e(
AppLog.T.API,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,16 @@ class ApplicationPasswordDialogViewModel @Inject constructor(
}

try {
val completeAuthUrl = applicationPasswordLoginHelper.getAuthorizationUrlComplete(authenticationUrl)

if (completeAuthUrl.isNotEmpty()) {
_navigationEvent.emit(NavigationEvent.NavigateToLogin(completeAuthUrl))
} else {
appLogWrapper.e(AppLog.T.MAIN, "Failed to process authentication URL")
_navigationEvent.emit(NavigationEvent.ShowError)
when (val result = applicationPasswordLoginHelper.getAuthorizationUrlComplete(authenticationUrl)) {
is ApplicationPasswordLoginHelper.DiscoveryResult.Authorized ->
_navigationEvent.emit(NavigationEvent.NavigateToLogin(result.authorizationUrl))
is ApplicationPasswordLoginHelper.DiscoveryResult.Failed -> {
appLogWrapper.e(
AppLog.T.MAIN,
"Failed to process authentication URL - ${result.userFacingMessage}"
)
_navigationEvent.emit(NavigationEvent.ShowError)
}
}
} catch (e: Throwable) {
appLogWrapper.e(AppLog.T.MAIN, "Error processing authentication URL - ${e.stackTraceToString()}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ class LoginSiteApplicationPasswordViewModel @Inject constructor(
_errorMessage.value = null
_loadingStateFlow.value = true
discoveryJob = viewModelScope.launch {
val discoveryUrl = applicationPasswordLoginHelper
.getAuthorizationUrlComplete(siteUrl)
_discoveryURL.send(discoveryUrl)
when (val result = applicationPasswordLoginHelper.getAuthorizationUrlComplete(siteUrl)) {
is ApplicationPasswordLoginHelper.DiscoveryResult.Authorized ->
_discoveryURL.send(result.authorizationUrl)
is ApplicationPasswordLoginHelper.DiscoveryResult.Failed -> {
_errorMessage.value = result.userFacingMessage
_discoveryURL.send("")
}
}
_loadingStateFlow.value = false
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.wordpress.android.ui.mysite.cards.applicationpassword

import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.util.AppLog
import rs.wordpress.api.kotlin.WpRequestResult
import uniffi.wp_api.RequestExecutionErrorReason
import javax.inject.Inject

/**
* Validates that the SiteModel's application-password credentials still work against the site's
* direct host. Uses [WpApiClientProvider.getApplicationPasswordClient] so the call exercises the
* application password specifically — `getWpApiClient` would route WPCom-flagged sites through the
* bearer-token path and would not catch a revoked password.
*/
class ApplicationPasswordValidator @Inject constructor(
private val wpApiClientProvider: WpApiClientProvider,
private val appLogWrapper: AppLogWrapper,
) {
suspend fun validate(site: SiteModel): Outcome {
appLogWrapper.d(
AppLog.T.MAIN,
"A_P: Validating application password for ${site.url} as user='${site.apiRestUsernamePlain}'"
)
return try {
val client = wpApiClientProvider.getApplicationPasswordClient(site)
val response = client.request { it.users().retrieveMeWithViewContext() }
appLogWrapper.d(AppLog.T.MAIN, "A_P: Validation response: ${response::class.simpleName}")
when (response) {
is WpRequestResult.Success -> {
val user = response.response.data
appLogWrapper.d(
AppLog.T.MAIN,
"A_P: Validation Success returned user id=${user.id}, slug='${user.slug}', name='${user.name}'"
)
Outcome.Valid
}
is WpRequestResult.WpError -> Outcome.Invalid
is WpRequestResult.UnknownError -> Outcome.Invalid
is WpRequestResult.RequestExecutionFailed ->
if (response.reason is RequestExecutionErrorReason.HttpTimeoutError) {
Outcome.NetworkUnavailable
} else {
Outcome.Invalid
}
else -> Outcome.Invalid
}
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
appLogWrapper.e(
AppLog.T.MAIN,
"A_P: Validation exception for ${site.url}: ${e::class.simpleName}: ${e.message}"
)
Outcome.NetworkUnavailable
}
}

enum class Outcome { Valid, Invalid, NetworkUnavailable }
}
Loading
Loading