diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php index 104eec6b49..702b74bf1b 100644 --- a/apps/dav/lib/CalDAV/BirthdayService.php +++ b/apps/dav/lib/CalDAV/BirthdayService.php @@ -30,6 +30,11 @@ use Exception; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\DAV\GroupPrincipalBackend; use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VCard; +use Sabre\VObject\DateTimeParser; +use Sabre\VObject\Document; +use Sabre\VObject\InvalidDataException; +use Sabre\VObject\Property\VCard\DateAndOrTime; use Sabre\VObject\Reader; class BirthdayService { @@ -129,6 +134,12 @@ class BirthdayService { } try { $doc = Reader::read($cardData); + // We're always converting to vCard 4.0 so we can rely on the + // VCardConverter handling the X-APPLE-OMIT-YEAR property for us. + if (!$doc instanceof VCard) { + return null; + } + $doc = $doc->convert(Document::VCARD40); } catch (Exception $e) { return null; } @@ -136,21 +147,43 @@ class BirthdayService { if (!isset($doc->{$dateField})) { return null; } + if (!isset($doc->FN)) { + return null; + } $birthday = $doc->{$dateField}; if (!(string)$birthday) { return null; } - $title = str_replace('{name}', - strtr((string)$doc->FN, array('\,' => ',', '\;' => ';')), - '{name}' - ); + // Skip if the BDAY property is not of the right type. + if (!$birthday instanceof DateAndOrTime) { + return null; + } + + // Skip if we can't parse the BDAY value. + try { + $dateParts = DateTimeParser::parseVCardDateTime($birthday->getValue()); + } catch (InvalidDataException $e) { + return null; + } + + $unknownYear = false; + if (!$dateParts['year']) { + $birthday = '1900-' . $dateParts['month'] . '-' . $dateParts['date']; + + $unknownYear = true; + } + try { $date = new \DateTime($birthday); } catch (Exception $e) { return null; } - - $summary = $title . ' (' . $summarySymbol . $date->format('Y') . ')'; + if ($unknownYear) { + $summary = $doc->FN->getValue() . ' ' . $summarySymbol; + } else { + $year = (int)$date->format('Y'); + $summary = $doc->FN->getValue() . " ($summarySymbol$year)"; + } $vCal = new VCalendar(); $vCal->VERSION = '2.0'; $vEvent = $vCal->createComponent('VEVENT'); diff --git a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php index 5eeb6772a6..cecf07ef1d 100644 --- a/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php +++ b/apps/dav/tests/unit/CardDAV/BirthdayServiceTest.php @@ -55,18 +55,18 @@ class BirthdayServiceTest extends TestCase { /** * @dataProvider providesVCards - * @param boolean $nullExpected + * @param boolean $expectedSummary * @param string | null $data */ - public function testBuildBirthdayFromContact($nullExpected, $data) { + public function testBuildBirthdayFromContact($expectedSummary, $data) { $cal = $this->service->buildDateFromContact($data, 'BDAY', '*'); - if ($nullExpected) { + if ($expectedSummary === null) { $this->assertNull($cal); } else { $this->assertInstanceOf('Sabre\VObject\Component\VCalendar', $cal); $this->assertTrue(isset($cal->VEVENT)); $this->assertEquals('FREQ=YEARLY', $cal->VEVENT->RRULE->getValue()); - $this->assertEquals('12345 (*1900)', $cal->VEVENT->SUMMARY->getValue()); + $this->assertEquals($expectedSummary, $cal->VEVENT->SUMMARY->getValue()); $this->assertEquals('TRANSPARENT', $cal->VEVENT->TRANSP->getValue()); } } @@ -233,14 +233,19 @@ class BirthdayServiceTest extends TestCase { public function providesVCards() { return [ - [true, null], - [true, ''], - [true, 'yasfewf'], - [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - [true, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - [false, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:1900-01-01\r\nEND:VCARD\r\n", "Dr. Foo Bar"], - [false, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:1900-12-31\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [null, null], + [null, ''], + [null, 'yasfewf'], + [null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:someday\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ['12345 (*1900)', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19000101\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ['12345 (*1900)', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:19001231\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ['12345 *', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:--1231\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ['12345 *', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY;X-APPLE-OMIT-YEAR=1604:16041231\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:;VALUE=text:circa 1800\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + [null, "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nN:12345;;;;\r\nBDAY:20031231\r\nEND:VCARD\r\n", "Dr. Foo Bar"], + ['12345 (*900)', "BEGIN:VCARD\r\nVERSION:3.0\r\nPRODID:-//Sabre//Sabre VObject 4.1.1//EN\r\nUID:12345\r\nFN:12345\r\nN:12345;;;;\r\nBDAY:09001231\r\nEND:VCARD\r\n", "Dr. Foo Bar"], ]; } }