Skip to content
This repository was archived by the owner on Aug 21, 2025. It is now read-only.

Commit 682ac7c

Browse files
committed
Escape vCard fields
1 parent a16ae6b commit 682ac7c

File tree

2 files changed

+42
-10
lines changed
  • homeUi/src
    • main/kotlin/com/gravatar/app/homeUi/presentation/home/share/model
    • test/kotlin/com/gravatar/app/homeUi/presentation/home/share/model

2 files changed

+42
-10
lines changed

homeUi/src/main/kotlin/com/gravatar/app/homeUi/presentation/home/share/model/VCard.kt

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,28 @@ internal class VCard private constructor(
2121
val lastName = lastName.orEmpty()
2222
val nickname = nickname.orEmpty()
2323
if (firstName.isNotEmpty() || lastName.isNotEmpty()) {
24-
contentBuilder.append("N:$lastName;$firstName;;;\n")
25-
.append("FN:${("$firstName $lastName".trim()).ifEmpty { nickname }}\n")
24+
contentBuilder.append("N:${lastName.escaped()};${firstName.escaped()};;;\n")
25+
.append(
26+
"FN:${("${firstName.escaped()} ${lastName.escaped()}".trim()).ifEmpty { nickname.escaped() }}\n"
27+
)
2628
}
27-
val calculatedNickname = nickname.ifEmpty { "$firstName $lastName".trim() }
29+
val calculatedNickname = nickname.ifEmpty { "$firstName $lastName".trim() }.escaped()
2830

29-
calculatedNickname.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("NICKNAME:$it\n") }
30-
organization?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("ORG:$it\n") }
31-
title?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("TITLE:$it\n") }
32-
profileUrl?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("URL:$it\n") }
33-
note?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("NOTE:$it\n") }
34-
phoneNumber?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("TEL;TYPE=cell:$it\n") }
35-
email?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("EMAIL:$it\n") }
31+
calculatedNickname.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("NICKNAME:${it.escaped()}\n") }
32+
organization?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("ORG:${it.escaped()}\n") }
33+
title?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("TITLE:${it.escaped()}\n") }
34+
profileUrl?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("URL:${it.escaped()}\n") }
35+
note?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("NOTE:${it.escaped()}\n") }
36+
phoneNumber?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("TEL;TYPE=cell:${it.escaped()}\n") }
37+
email?.takeIf { it.isNotEmpty() }?.let { contentBuilder.append("EMAIL:${it.escaped()}\n") }
3638

3739
contentBuilder.append("END:VCARD")
3840
return contentBuilder.toString()
3941
}
4042

43+
// We've seen issues with newlines in the vCard content causing problems when importing the contact so removing them
44+
private fun String.escaped() = this.replace("\n", " ")
45+
4146
class Builder(
4247
private var firstName: String? = null,
4348
private var lastName: String? = null,

homeUi/src/test/kotlin/com/gravatar/app/homeUi/presentation/home/share/model/VCardTest.kt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,31 @@ class VCardTest {
168168
vCardString
169169
)
170170
}
171+
172+
@Test
173+
fun `builder replaces newlines with spaces in field values`() {
174+
val vCard = VCard.Builder()
175+
.firstName("First\nName")
176+
.lastName("Last\nName")
177+
.nickname("Nick\nName")
178+
.organization("Org\nName")
179+
.title("Job\nTitle")
180+
.profileUrl("http://example.com/profile\nurl")
181+
.note("This is a\nnote with\nnewlines.")
182+
.phoneNumber("123\n456\n7890")
183+
.email("user\nname@example.com")
184+
.build()
185+
186+
val vCardString = vCard.toString()
187+
188+
assertTrue(vCardString.contains("N:Last Name;First Name;;;"))
189+
assertTrue(vCardString.contains("FN:First Name Last Name"))
190+
assertTrue(vCardString.contains("NICKNAME:Nick Name"))
191+
assertTrue(vCardString.contains("ORG:Org Name"))
192+
assertTrue(vCardString.contains("TITLE:Job Title"))
193+
assertTrue(vCardString.contains("URL:http://example.com/profile url"))
194+
assertTrue(vCardString.contains("NOTE:This is a note with newlines."))
195+
assertTrue(vCardString.contains("TEL;TYPE=cell:123 456 7890"))
196+
assertTrue(vCardString.contains("EMAIL:user name@example.com"))
197+
}
171198
}

0 commit comments

Comments
 (0)