diff --git a/lib/Property/Uri.php b/lib/Property/Uri.php index 9816ffffd..e6b103acf 100644 --- a/lib/Property/Uri.php +++ b/lib/Property/Uri.php @@ -3,6 +3,7 @@ namespace Sabre\VObject\Property; use Sabre\VObject\Parameter; +use Sabre\VObject\Parser\MimeDir; use Sabre\VObject\Property; /** @@ -62,32 +63,41 @@ public function parameters(): array */ public function setRawMimeDirValue(string $val): void { - // Normally we don't need to do any type of unescaping for these - // properties, however, we've noticed that Google Contacts + // For VCard4, we need to unescape comma, backslash and semicolon (and newline). (RFC6350, 3.4) + // + // However, we've noticed that Google Contacts // specifically escapes the colon (:) with a backslash. While I have // no clue why they thought that was a good idea, I'm unescaping it // anyway. // // Good thing backslashes are not allowed in urls. Makes it easy to // assume that a backslash is always intended as an escape character. - if ('URL' === $this->name) { - $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x'; - $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - $newVal = ''; - foreach ($matches as $match) { - switch ($match) { - case '\:': - $newVal .= ':'; - break; - default: - $newVal .= $match; - break; - } + $escapeColon = ('URL' === $this->name) ? '| : ' : ''; + + $regex = '# (?: (\\\\ (?: \\\\ ' . $escapeColon . '| N | n | ; | , ) ) ) #x'; + $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $newVal = ''; + foreach ($matches as $match) { + switch ($match) { + case '\\\\': + $newVal .= '\\'; + break; + case '\;': + $newVal .= ';'; + break; + case '\,': + $newVal .= ','; + break; + case '\:': + $newVal .= ':'; + break; + default: + $newVal .= $match; + break; } - $this->value = $newVal; - } else { - $this->value = strtr($val, ['\,' => ',']); } + + $this->value = $newVal; } /** diff --git a/lib/VCardConverter.php b/lib/VCardConverter.php index 7de4501ba..d76977d8e 100644 --- a/lib/VCardConverter.php +++ b/lib/VCardConverter.php @@ -180,6 +180,10 @@ protected function convertProperty(Component\VCard $input, Component\VCard $outp if ('GROUP' === strtoupper($property->getValue())) { $newProperty = $output->createProperty('KIND', 'GROUP'); } + if ('INDIVIDUAL' === strtoupper($property->getValue())) { + // Individual is implicit, so we skip it. + return; + } break; case 'X-ADDRESSBOOKSERVER-MEMBER': $newProperty = $output->createProperty('MEMBER', $property->getValue()); @@ -336,7 +340,7 @@ protected function convertUriToBinary(Component\VCard $output, Property\Uri $new unset($value); $newProperty['ENCODING'] = 'b'; - switch ($mimeType) { + switch (strtolower($mimeType)) { case 'image/jpeg': $newProperty['TYPE'] = 'JPEG'; break; diff --git a/tests/VObject/Property/UriTest.php b/tests/VObject/Property/UriTest.php index 80810104c..4f0d54bf6 100644 --- a/tests/VObject/Property/UriTest.php +++ b/tests/VObject/Property/UriTest.php @@ -23,4 +23,29 @@ public function testAlwaysEncodeUriVCalendar(): void $output = Reader::read($input)->serialize(); $this->assertStringContainsString('URL;VALUE=URI:http://example.org/', $output); } + + public function testUriUnescapedProperly(): void + { + // The colon should normally not be escaped in URL, but Google Contacts does it and + // vobject contains a workaround for it + $input = <<assertSame('http://www.example.com/hello?world', (string) $output->URL); + $this->assertSame('data:image/JPEG;base64,/9j/4AAQSkZJRgABAQAAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAAv/EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AL//Z', (string) $output->PHOTO); + } } diff --git a/tests/VObject/VCardConverterTest.php b/tests/VObject/VCardConverterTest.php index d27e7ff14..2a9ea62fd 100644 --- a/tests/VObject/VCardConverterTest.php +++ b/tests/VObject/VCardConverterTest.php @@ -38,9 +38,9 @@ public function testConvert30to40(): void TEL;PREF=1;TYPE=HOME:+1 555 666 777 ITEM1.TEL:+1 444 555 666 ITEM1.X-ABLABEL:CustomLabel -PHOTO;TYPE=HOME:data:image/jpeg;base64,Zm9v -PHOTO:data:image/gif;base64,Zm9v -PHOTO;X-PARAM=FOO:data:image/png;base64,Zm9v +PHOTO;TYPE=HOME:data:image/jpeg;base64\\,Zm9v +PHOTO:data:image/gif;base64\\,Zm9v +PHOTO;X-PARAM=FOO:data:image/png;base64\\,Zm9v PHOTO:http://example.org/foo.png KIND:ORG END:VCARD @@ -66,9 +66,9 @@ public function testConvert40to40(): void VERSION:4.0 FN:Steve TEL;PREF=1;TYPE=HOME:+1 555 666 777 -PHOTO:data:image/jpeg;base64,Zm9v -PHOTO:data:image/gif;base64,Zm9v -PHOTO;X-PARAM=FOO:data:image/png;base64,Zm9v +PHOTO:data:image/jpeg;base64\\,Zm9v +PHOTO:data:image/gif;base64\\,Zm9v +PHOTO;X-PARAM=FOO:data:image/png;base64\\,Zm9v PHOTO:http://example.org/foo.png END:VCARD @@ -79,9 +79,9 @@ public function testConvert40to40(): void VERSION:4.0 FN:Steve TEL;PREF=1;TYPE=HOME:+1 555 666 777 -PHOTO:data:image/jpeg;base64,Zm9v -PHOTO:data:image/gif;base64,Zm9v -PHOTO;X-PARAM=FOO:data:image/png;base64,Zm9v +PHOTO:data:image/jpeg;base64\\,Zm9v +PHOTO:data:image/gif;base64\\,Zm9v +PHOTO;X-PARAM=FOO:data:image/png;base64\\,Zm9v PHOTO:http://example.org/foo.png END:VCARD @@ -193,9 +193,9 @@ public function testConvert40to30(): void PRODID:foo FN:Steve TEL;PREF=1;TYPE=HOME:+1 555 666 777 -PHOTO:data:image/jpeg;base64,Zm9v -PHOTO:data:image/gif,foo -PHOTO;X-PARAM=FOO:data:image/png;base64,Zm9v +PHOTO:data:image/JPEG\\;base64\\,Zm9v +PHOTO:data:image/gif\\,foo +PHOTO;X-PARAM=FOO:data:image/png;base64\\,Zm9v PHOTO:http://example.org/foo.png KIND:ORG END:VCARD @@ -409,7 +409,14 @@ public function testConvertIndividualCard(): void $vcard ); - $input = $output; + $input = <<