Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ class InputParametersUnitTest {

private val wrongDomainParameter = arrayListOf(
"",
"https://api.mindbox.ru",
"api.mindbox.ru/",
"https://api.mindbox.ru/",
"hgkkjhhv",
"4854-t789"
)

private val normalizedDomainParameter = arrayListOf(
"https://api.mindbox.ru",
"api.mindbox.ru/",
"https://api.mindbox.ru/"
)

private val wrongUuidParameters = arrayListOf(
"ларалтка ыфдво",
"7659d 79",
Expand Down Expand Up @@ -115,45 +118,22 @@ class InputParametersUnitTest {
}

@Test
fun domain_startsWithHttps() {
val errors = SdkValidation.validateConfiguration(
domain = wrongDomainParameter[1],
endpointId = rightEndpointParameter,
previousDeviceUUID = rightUuidParameter,
previousInstallationId = rightUuidParameter
)
assertEquals(1, errors.size)
assertEquals(SdkValidation.Error.INVALID_FORMAT_DOMAIN, errors[0])
}

@Test
fun domain_endsWithSlash() {
val errors = SdkValidation.validateConfiguration(
domain = wrongDomainParameter[2],
endpointId = rightEndpointParameter,
previousDeviceUUID = rightUuidParameter,
previousInstallationId = rightUuidParameter
)
assertEquals(1, errors.size)
assertEquals(SdkValidation.Error.INVALID_FORMAT_DOMAIN, errors[0])
}

@Test
fun domain_startsWithHttpsAndEndsWithSlash() {
val errors = SdkValidation.validateConfiguration(
domain = wrongDomainParameter[3],
endpointId = rightEndpointParameter,
previousDeviceUUID = rightUuidParameter,
previousInstallationId = rightUuidParameter
)
assertEquals(1, errors.size)
assertEquals(SdkValidation.Error.INVALID_FORMAT_DOMAIN, errors[0])
fun domain_withSchemeOrTrailingSlash_isNormalized() {
normalizedDomainParameter.forEach { input ->
val errors = SdkValidation.validateConfiguration(
domain = input,
endpointId = rightEndpointParameter,
previousDeviceUUID = rightUuidParameter,
previousInstallationId = rightUuidParameter
)
assertEquals("Expected 0 errors for '$input'", 0, errors.size)
}
}

@Test
fun domain_InvalidFormat() {
val errors4 = SdkValidation.validateConfiguration(
domain = wrongDomainParameter[4],
domain = wrongDomainParameter[1],
endpointId = rightEndpointParameter,
previousDeviceUUID = rightUuidParameter,
previousInstallationId = rightUuidParameter
Expand All @@ -162,7 +142,7 @@ class InputParametersUnitTest {
assertEquals(SdkValidation.Error.INVALID_DOMAIN, errors4[0])

val errors5 = SdkValidation.validateConfiguration(
domain = wrongDomainParameter[5],
domain = wrongDomainParameter[2],
endpointId = rightEndpointParameter,
previousDeviceUUID = rightUuidParameter,
previousInstallationId = rightUuidParameter
Expand Down
26 changes: 7 additions & 19 deletions sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,7 @@ public object Mindbox : MindboxLog {
endpointId = configuration.endpointId,
previousDeviceUUID = configuration.previousDeviceUUID,
previousInstallationId = configuration.previousInstallationId,
operationsDomain = configuration.operationsDomain,
Comment thread
enotniy marked this conversation as resolved.
)

return if (validationErrors.isEmpty()) {
Expand All @@ -1446,27 +1447,14 @@ public object Mindbox : MindboxLog {
throw InitializeMindboxException(validationErrors.toString())
}
MindboxLoggerImpl.e(this, "Invalid configuration parameters found: $validationErrors")
val isDeviceIdError = validationErrors.contains(
SdkValidation.Error.INVALID_DEVICE_ID,
)
val isInstallationIdError = validationErrors.contains(
SdkValidation.Error.INVALID_INSTALLATION_ID,
)

val previousDeviceUUID = if (isDeviceIdError) {
""
} else {
configuration.previousDeviceUUID
}
val previousInstallationId = if (isInstallationIdError) {
""
} else {
configuration.previousInstallationId
}
val isDeviceIdError = validationErrors.contains(SdkValidation.Error.INVALID_DEVICE_ID)
val isInstallationIdError = validationErrors.contains(SdkValidation.Error.INVALID_INSTALLATION_ID)
val isOperationsDomainError = validationErrors.contains(SdkValidation.Error.INVALID_OPERATIONS_DOMAIN)

configuration.copy(
previousDeviceUUID = previousDeviceUUID,
previousInstallationId = previousInstallationId,
previousDeviceUUID = if (isDeviceIdError) "" else configuration.previousDeviceUUID,
previousInstallationId = if (isInstallationIdError) "" else configuration.previousInstallationId,
operationsDomain = if (isOperationsDomainError) null else configuration.operationsDomain,
)
}
}
Expand Down
21 changes: 19 additions & 2 deletions sdk/src/main/java/cloud/mindbox/mobile_sdk/MindboxConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class MindboxConfiguration private constructor(
internal val subscribeCustomerIfCreated: Boolean,
internal val shouldCreateCustomer: Boolean,
internal val uuidDebugEnabled: Boolean,
internal val operationsDomain: String? = null,
) {

public constructor(builder: Builder) : this(
Expand All @@ -35,6 +36,7 @@ public class MindboxConfiguration private constructor(
subscribeCustomerIfCreated = builder.subscribeCustomerIfCreated,
shouldCreateCustomer = builder.shouldCreateCustomer,
uuidDebugEnabled = builder.uuidDebugEnabled,
operationsDomain = builder.operationsDomain,
)

internal fun copy(
Expand All @@ -48,6 +50,7 @@ public class MindboxConfiguration private constructor(
subscribeCustomerIfCreated: Boolean = this.subscribeCustomerIfCreated,
shouldCreateCustomer: Boolean = this.shouldCreateCustomer,
uuidDebugEnabled: Boolean = this.uuidDebugEnabled,
operationsDomain: String? = this.operationsDomain,
) = MindboxConfiguration(
previousInstallationId = previousInstallationId,
previousDeviceUUID = previousDeviceUUID,
Expand All @@ -59,6 +62,7 @@ public class MindboxConfiguration private constructor(
subscribeCustomerIfCreated = subscribeCustomerIfCreated,
shouldCreateCustomer = shouldCreateCustomer,
uuidDebugEnabled = uuidDebugEnabled,
operationsDomain = operationsDomain,
)

override fun toString(): String {
Expand All @@ -71,7 +75,8 @@ public class MindboxConfiguration private constructor(
"versionCode = $versionCode, " +
"subscribeCustomerIfCreated = $subscribeCustomerIfCreated, " +
"shouldCreateCustomer = $shouldCreateCustomer, " +
"uuidDebugEnabled = $uuidDebugEnabled)"
"uuidDebugEnabled = $uuidDebugEnabled, " +
"operationsDomain = $operationsDomain)"
}

/**
Expand All @@ -94,6 +99,7 @@ public class MindboxConfiguration private constructor(
internal var versionCode: String = PLACEHOLDER_APP_VERSION_CODE
internal var shouldCreateCustomer: Boolean = true
internal var uuidDebugEnabled: Boolean = true
internal var operationsDomain: String? = null

/**
* Specifies deviceUUID for Mindbox
Expand Down Expand Up @@ -149,6 +155,17 @@ public class MindboxConfiguration private constructor(
return this
}

/**
* Optional host for operations (/v3/operations/async, /v3/operations/sync,
* /v1.1/customer/mobile-track-visit). Use when your project routes operations through
* an anonymizer proxy. A blank value is treated as not set. An invalid value is logged
* and ignored during SDK initialization.
*/
public fun operationsDomain(operationsDomain: String): Builder {
this.operationsDomain = operationsDomain.trim().takeIf { it.isNotBlank() }
return this
}

/**
* Creates a new MindboxConfiguration.Builder.
*/
Expand All @@ -175,7 +192,7 @@ public class MindboxConfiguration private constructor(
// need for scheduling and stopping one-time background service
SharedPreferencesManager.with(context)
MindboxPreferences.hostAppName = packageName
} catch (e: Exception) {
} catch (_: Exception) {
MindboxLoggerImpl.e(
this,
"Getting app info failed. Identified as an unknown application",
Expand Down
64 changes: 52 additions & 12 deletions sdk/src/main/java/cloud/mindbox/mobile_sdk/SdkValidation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,68 @@ internal object SdkValidation {
enum class Error(val critical: Boolean, val message: String) {

EMPTY_DOMAIN(true, "Domain must not be empty"),
INVALID_FORMAT_DOMAIN(true, "The domain must not start with https:// and must not end with /"),
INVALID_FORMAT_DOMAIN(true, "The domain format is not valid"),
INVALID_DOMAIN(true, "The domain is not valid"),
EMPTY_ENDPOINT(true, "Endpoint must not be empty"),
INVALID_DEVICE_ID(false, "Invalid previous device UUID format"),
INVALID_INSTALLATION_ID(false, "Invalid UUID format of previous installationId");
INVALID_INSTALLATION_ID(false, "Invalid UUID format of previous installationId"),
INVALID_OPERATIONS_DOMAIN(false, "The operationsDomain is not valid, it will be ignored");

override fun toString() = "$name(critical=$critical, message=$message)"
}

/**
* Strips http:// or https:// scheme and trailing slashes from [input].
* "https://api.mindbox.ru/" → "api.mindbox.ru"
* "api.mindbox.ru/" → "api.mindbox.ru"
*/
fun extractHost(input: String): String =
input.trim()
.removePrefix("https://")
.removePrefix("http://")
.trimEnd('/')

/**
* Returns a full base URL. If [hostOrUrl] already contains a scheme (http:// or https://),
* it is preserved. Otherwise https:// is prepended.
* "api.mindbox.ru" → "https://api.mindbox.ru"
* "http://proxy.example.com" → "http://proxy.example.com"
*/
fun toBaseUrl(hostOrUrl: String): String {
val trimmed = hostOrUrl.trim()
return if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
trimmed.trimEnd('/')
} else {
"https://${trimmed.trimEnd('/')}"
}
}

/**
* Returns true if [domain] is a valid domain host, accepting optional http:// or https:// prefix
* and optional trailing slash.
*/
fun isValidDomain(domain: String): Boolean {
val host = extractHost(domain)
return host.isNotBlank() && isDomainValid(host)
}

fun validateConfiguration(
domain: String,
endpointId: String,
previousDeviceUUID: String,
previousInstallationId: String
previousInstallationId: String,
operationsDomain: String? = null,
) = LoggingExceptionHandler.runCatching(defaultValue = listOf()) {
mutableListOf<Error>().apply {
when {
domain.isBlank() -> add(Error.EMPTY_DOMAIN)
!isDomainWellFormatted(domain) -> add(Error.INVALID_FORMAT_DOMAIN)
!isDomainValid(domain) -> add(Error.INVALID_DOMAIN)
else -> {
val host = extractHost(domain)
when {
host.isBlank() -> add(Error.INVALID_FORMAT_DOMAIN)
!isDomainValid(host) -> add(Error.INVALID_DOMAIN)
}
}
}

if (endpointId.isBlank()) {
Expand All @@ -41,14 +83,12 @@ internal object SdkValidation {
if (previousInstallationId.isNotEmpty() && !previousInstallationId.isUuid()) {
add(Error.INVALID_INSTALLATION_ID)
}

if (operationsDomain != null && !isValidDomain(operationsDomain)) {
add(Error.INVALID_OPERATIONS_DOMAIN)
}
}
}

private fun isDomainWellFormatted(domain: String) = !domain.startsWith("http") &&
!domain.startsWith("/") &&
!domain.endsWith("/")

private fun isDomainValid(
domain: String
) = PatternsCompat.DOMAIN_NAME.matcher(domain).matches()
private fun isDomainValid(domain: String) = PatternsCompat.DOMAIN_NAME.matcher(domain).matches()
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@ internal class MobileConfigSerializationManagerImpl(private val gson: Gson) :
mindboxLogE("Failed to parse featureToggles block in settings section")
}

SettingsDtoBlank(operations, ttl, slidingExpiration, inappSettings, featureToggles)
val baseAddresses = runCatching {
gson.fromJson(json.asJsonObject.get("baseAddresses"), BaseAddressesDtoBlank::class.java)?.copy()
}.getOrNull {
mindboxLogE("Failed to parse baseAddresses block in settings section")
}

SettingsDtoBlank(operations, ttl, slidingExpiration, inappSettings, featureToggles, baseAddresses)
}
}.getOrNull {
mindboxLogE("Failed to parse settings block", it)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ internal class MobileConfigRepositoryImpl(
mobileConfigSettingsManager.checkPushTokenKeepalive(config = filteredConfig)
inappSettingsManager.applySettings(config = filteredConfig)
featureToggleManager.applyToggles(config = filteredConfig)
persistOperationsDomain(filteredConfig)
configState.value = updatedInAppConfig
mindboxLogI(message = "Providing config: $updatedInAppConfig")
}
Expand Down Expand Up @@ -190,7 +191,37 @@ internal class MobileConfigRepositoryImpl(
mindboxLogW("Unable to get featureToggles settings $it")
}

return SettingsDto(operations, ttl, slidingExpiration, inappSettings, featureToggles)
val baseAddresses = runCatching { getBaseAddresses(configBlank) }.getOrNull {
mindboxLogW("Unable to get baseAddresses settings $it")
}

return SettingsDto(operations, ttl, slidingExpiration, inappSettings, featureToggles, baseAddresses)
}

private fun getBaseAddresses(configBlank: InAppConfigResponseBlank?): BaseAddressesDto? {
val operations = configBlank?.settings?.baseAddresses?.operations
?.trim()
?.takeIf { it.isNotBlank() }
?: return null
return BaseAddressesDto(operations = operations)
}

private fun persistOperationsDomain(config: InAppConfigResponse) {
val raw = config.settings?.baseAddresses?.operations
val stored = MindboxPreferences.operationsDomainFromConfig
when (val action = operationsDomainConfigPolicyAction(raw, stored)) {
is OperationsDomainConfigPolicyAction.Save -> {
mindboxLogD("operationsDomain: saving '${action.value}'")
MindboxPreferences.operationsDomainFromConfig = action.value
}
is OperationsDomainConfigPolicyAction.Clear -> {
mindboxLogD("operationsDomain: clearing stored value '$stored'")
MindboxPreferences.operationsDomainFromConfig = null
}
is OperationsDomainConfigPolicyAction.Keep -> {
mindboxLogD("operationsDomain: keeping existing value '$stored'")
}
}
}

private fun getInAppTtl(configBlank: InAppConfigResponseBlank?): TtlDto? =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cloud.mindbox.mobile_sdk.inapp.data.repositories

import cloud.mindbox.mobile_sdk.SdkValidation

internal sealed class OperationsDomainConfigPolicyAction {
data class Save(val value: String) : OperationsDomainConfigPolicyAction()

object Clear : OperationsDomainConfigPolicyAction()

object Keep : OperationsDomainConfigPolicyAction()
}

internal fun operationsDomainConfigPolicyAction(
raw: String?,
currentlyStored: String?,
): OperationsDomainConfigPolicyAction {
val value = raw?.trim()?.takeIf { it.isNotBlank() }
?: return if (currentlyStored != null) {
OperationsDomainConfigPolicyAction.Clear
} else {
OperationsDomainConfigPolicyAction.Keep
}

if (!SdkValidation.isValidDomain(value)) return OperationsDomainConfigPolicyAction.Keep

return if (value == currentlyStored) {
OperationsDomainConfigPolicyAction.Keep
} else {
OperationsDomainConfigPolicyAction.Save(value)
}
Comment thread
enotniy marked this conversation as resolved.
}
Loading
Loading