URL-encode deep link path segments in DefaultDeepLinkCreator
Decode them later in `DefaultDeepLinkParser` too
This commit is contained in:
committed by
Jorge Martin Espinosa
parent
1e52e1139f
commit
a59b9c86e9
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.androidutils.text
|
||||
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.Charset
|
||||
|
||||
fun String.urlEncoded(charset: Charset = Charsets.UTF_8): String = URLEncoder.encode(this, charset.name())
|
||||
fun String.urlDecoded(charset: Charset = Charsets.UTF_8): String = URLDecoder.decode(this, charset.name())
|
||||
@@ -10,6 +10,7 @@ package io.element.android.libraries.deeplink.impl
|
||||
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.text.urlEncoded
|
||||
import io.element.android.libraries.deeplink.api.DeepLinkCreator
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
@@ -21,13 +22,13 @@ class DefaultDeepLinkCreator : DeepLinkCreator {
|
||||
override fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?, eventId: EventId?): String {
|
||||
return buildString {
|
||||
append("$SCHEME://$HOST/")
|
||||
append(sessionId.value)
|
||||
append(sessionId.value.urlEncoded())
|
||||
append("/")
|
||||
append(roomId?.value.orEmpty())
|
||||
append(roomId?.value?.urlEncoded().orEmpty())
|
||||
append("/")
|
||||
append(threadId?.value.orEmpty())
|
||||
append(threadId?.value?.urlEncoded().orEmpty())
|
||||
append("/")
|
||||
append(eventId?.value.orEmpty())
|
||||
append(eventId?.value?.urlEncoded().orEmpty())
|
||||
}
|
||||
// Remove all possible trailing '/' characters:
|
||||
// No event id
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.androidutils.text.urlDecoded
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkParser
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
@@ -31,7 +32,7 @@ class DefaultDeeplinkParser : DeeplinkParser {
|
||||
private fun Uri.toDeeplinkData(): DeeplinkData? {
|
||||
if (scheme != SCHEME) return null
|
||||
if (host != HOST) return null
|
||||
val pathBits = path.orEmpty().split("/").drop(1)
|
||||
val pathBits = encodedPath.orEmpty().split("/").drop(1).map { it.urlDecoded() }
|
||||
val sessionId = pathBits.elementAtOrNull(0)?.let(::SessionId) ?: return null
|
||||
|
||||
return when (val screenPathComponent = pathBits.elementAtOrNull(1)) {
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
package io.element.android.libraries.deeplink.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.androidutils.text.urlEncoded
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
@@ -19,15 +24,43 @@ class DefaultDeepLinkCreatorTest {
|
||||
@Test
|
||||
fun create() {
|
||||
val sut = DefaultDeepLinkCreator()
|
||||
assertThat(sut.create(A_SESSION_ID, null, null, null))
|
||||
.isEqualTo("elementx://open/@alice:server.org")
|
||||
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null, null))
|
||||
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain")
|
||||
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID, null))
|
||||
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
||||
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID, AN_EVENT_ID))
|
||||
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId/\$anEventId")
|
||||
assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null, AN_EVENT_ID))
|
||||
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain//\$anEventId")
|
||||
val sessionId = A_SESSION_ID
|
||||
val roomId = A_ROOM_ID
|
||||
val threadId = A_THREAD_ID
|
||||
val eventId = AN_EVENT_ID
|
||||
assertThat(sut.create(sessionId, null, null, null))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, null, null))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, threadId, null))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}/${threadId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, threadId, eventId))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}/${threadId.urlEncoded()}/${eventId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, null, eventId))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}//${eventId.urlEncoded()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create - with escaped invalid characters`() {
|
||||
val sut = DefaultDeepLinkCreator()
|
||||
val sessionId = SessionId("@a/:domain")
|
||||
val roomId = RoomId("!a/RoomId:domain")
|
||||
val threadId = ThreadId("\$a/ThreadId")
|
||||
val eventId = EventId("\$an/EventId")
|
||||
assertThat(sut.create(sessionId, null, null, null))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, null, null))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, threadId, null))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}/${threadId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, threadId, eventId))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}/${threadId.urlEncoded()}/${eventId.urlEncoded()}")
|
||||
assertThat(sut.create(sessionId, roomId, null, eventId))
|
||||
.isEqualTo("elementx://open/${sessionId.urlEncoded()}/${roomId.urlEncoded()}//${eventId.urlEncoded()}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun SessionId.urlEncoded() = this.value.urlEncoded()
|
||||
private fun RoomId.urlEncoded() = this.value.urlEncoded()
|
||||
private fun ThreadId.urlEncoded() = this.value.urlEncoded()
|
||||
private fun EventId.urlEncoded() = this.value.urlEncoded()
|
||||
|
||||
@@ -12,6 +12,10 @@ import android.content.Intent
|
||||
import androidx.core.net.toUri
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.deeplink.api.DeeplinkData
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.SessionId
|
||||
import io.element.android.libraries.matrix.api.core.ThreadId
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
@@ -34,6 +38,8 @@ class DefaultDeeplinkParserTest {
|
||||
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId/\$anEventId"
|
||||
const val A_URI_WITH_ROOM_WITH_EVENT_AND_NO_THREAD =
|
||||
"elementx://open/@alice:server.org/!aRoomId:domain//\$anEventId"
|
||||
const val A_URI_WITH_ROOM_WITH_THREAD_AND_EVENT_AND_INVALID_CHARACTERS =
|
||||
"elementx://open/@a%2Flice:server.org/!a%2FRoomId:domain/\$a%2FThreadId/\$an%2FEventId"
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,6 +55,15 @@ class DefaultDeeplinkParserTest {
|
||||
.isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID, AN_EVENT_ID))
|
||||
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_EVENT_AND_NO_THREAD)))
|
||||
.isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, null, AN_EVENT_ID))
|
||||
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD_AND_EVENT_AND_INVALID_CHARACTERS)))
|
||||
.isEqualTo(
|
||||
DeeplinkData.Room(
|
||||
sessionId = SessionId("@a/lice:server.org"),
|
||||
roomId = RoomId("!a/RoomId:domain"),
|
||||
threadId = ThreadId("\$a/ThreadId"),
|
||||
eventId = EventId("\$an/EventId"),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user