aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav/lib/CalDAV/BirthdayService.php
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav/lib/CalDAV/BirthdayService.php')
-rw-r--r--apps/dav/lib/CalDAV/BirthdayService.php245
1 files changed, 116 insertions, 129 deletions
diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php
index bdcf0796283..680b228766f 100644
--- a/apps/dav/lib/CalDAV/BirthdayService.php
+++ b/apps/dav/lib/CalDAV/BirthdayService.php
@@ -3,32 +3,9 @@
declare(strict_types=1);
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @copyright Copyright (c) 2019, Georg Ehrke
- *
- * @author Achim Königs <garfonso@tratschtante.de>
- * @author Christian Weiske <cweiske@cweiske.de>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author Georg Ehrke <oc.list@georgehrke.com>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Sven Strickroth <email@cs-ware.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Valdnet <47037905+Valdnet@users.noreply.github.com>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\DAV\CalDAV;
@@ -53,63 +30,33 @@ use Sabre\VObject\Reader;
*/
class BirthdayService {
public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays';
-
- /** @var GroupPrincipalBackend */
- private $principalBackend;
-
- /** @var CalDavBackend */
- private $calDavBackEnd;
-
- /** @var CardDavBackend */
- private $cardDavBackEnd;
-
- /** @var IConfig */
- private $config;
-
- /** @var IDBConnection */
- private $dbConnection;
-
- /** @var IL10N */
- private $l10n;
+ public const EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME = 'X-NC-EXCLUDE-FROM-BIRTHDAY-CALENDAR';
/**
* BirthdayService constructor.
- *
- * @param CalDavBackend $calDavBackEnd
- * @param CardDavBackend $cardDavBackEnd
- * @param GroupPrincipalBackend $principalBackend
- * @param IConfig $config
- * @param IDBConnection $dbConnection
- * @param IL10N $l10n
*/
- public function __construct(CalDavBackend $calDavBackEnd,
- CardDavBackend $cardDavBackEnd,
- GroupPrincipalBackend $principalBackend,
- IConfig $config,
- IDBConnection $dbConnection,
- IL10N $l10n) {
- $this->calDavBackEnd = $calDavBackEnd;
- $this->cardDavBackEnd = $cardDavBackEnd;
- $this->principalBackend = $principalBackend;
- $this->config = $config;
- $this->dbConnection = $dbConnection;
- $this->l10n = $l10n;
+ public function __construct(
+ private CalDavBackend $calDavBackEnd,
+ private CardDavBackend $cardDavBackEnd,
+ private GroupPrincipalBackend $principalBackend,
+ private IConfig $config,
+ private IDBConnection $dbConnection,
+ private IL10N $l10n,
+ ) {
}
- /**
- * @param int $addressBookId
- * @param string $cardUri
- * @param string $cardData
- */
public function onCardChanged(int $addressBookId,
- string $cardUri,
- string $cardData) {
+ string $cardUri,
+ string $cardData): void {
if (!$this->isGloballyEnabled()) {
return;
}
$targetPrincipals = $this->getAllAffectedPrincipals($addressBookId);
$book = $this->cardDavBackEnd->getAddressBookById($addressBookId);
+ if ($book === null) {
+ return;
+ }
$targetPrincipals[] = $book['principaluri'];
$datesToSync = [
['postfix' => '', 'field' => 'BDAY'],
@@ -122,19 +69,20 @@ class BirthdayService {
continue;
}
+ $reminderOffset = $this->getReminderOffsetForUser($principalUri);
+
$calendar = $this->ensureCalendarExists($principalUri);
+ if ($calendar === null) {
+ return;
+ }
foreach ($datesToSync as $type) {
- $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type);
+ $this->updateCalendar($cardUri, $cardData, $book, (int)$calendar['id'], $type, $reminderOffset);
}
}
}
- /**
- * @param int $addressBookId
- * @param string $cardUri
- */
public function onCardDeleted(int $addressBookId,
- string $cardUri) {
+ string $cardUri): void {
if (!$this->isGloballyEnabled()) {
return;
}
@@ -149,18 +97,16 @@ class BirthdayService {
$calendar = $this->ensureCalendarExists($principalUri);
foreach (['', '-death', '-anniversary'] as $tag) {
- $objectUri = $book['uri'] . '-' . $cardUri . $tag .'.ics';
+ $objectUri = $book['uri'] . '-' . $cardUri . $tag . '.ics';
$this->calDavBackEnd->deleteCalendarObject($calendar['id'], $objectUri, CalDavBackend::CALENDAR_TYPE_CALENDAR, true);
}
}
}
/**
- * @param string $principal
- * @return array|null
* @throws \Sabre\DAV\Exception\BadRequest
*/
- public function ensureCalendarExists(string $principal):?array {
+ public function ensureCalendarExists(string $principal): ?array {
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
if (!is_null($calendar)) {
return $calendar;
@@ -178,12 +124,14 @@ class BirthdayService {
* @param $cardData
* @param $dateField
* @param $postfix
+ * @param $reminderOffset
* @return VCalendar|null
* @throws InvalidDataException
*/
public function buildDateFromContact(string $cardData,
- string $dateField,
- string $postfix):?VCalendar {
+ string $dateField,
+ string $postfix,
+ ?string $reminderOffset):?VCalendar {
if (empty($cardData)) {
return null;
}
@@ -199,6 +147,10 @@ class BirthdayService {
return null;
}
+ if (isset($doc->{self::EXCLUDE_FROM_BIRTHDAY_CALENDAR_PROPERTY_NAME})) {
+ return null;
+ }
+
if (!isset($doc->{$dateField})) {
return null;
}
@@ -220,33 +172,26 @@ class BirthdayService {
} catch (InvalidDataException $e) {
return null;
}
+ if ($dateParts['year'] !== null) {
+ $parameters = $birthday->parameters();
+ $omitYear = (isset($parameters['X-APPLE-OMIT-YEAR'])
+ && $parameters['X-APPLE-OMIT-YEAR'] === $dateParts['year']);
+ // 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
+ if ($omitYear || (int)$dateParts['year'] === 1604) {
+ $dateParts['year'] = null;
+ }
+ }
- $unknownYear = false;
$originalYear = null;
- if (!$dateParts['year']) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
+ if ($dateParts['year'] !== null) {
+ $originalYear = (int)$dateParts['year'];
+ }
- $unknownYear = true;
- } else {
- $parameters = $birthday->parameters();
- if (isset($parameters['X-APPLE-OMIT-YEAR'])) {
- $omitYear = $parameters['X-APPLE-OMIT-YEAR'];
- if ($dateParts['year'] === $omitYear) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- $unknownYear = true;
- }
- } else {
- $originalYear = (int)$dateParts['year'];
- // 'X-APPLE-OMIT-YEAR' is not always present, at least iOS 12.4 uses the hard coded date of 1604 (the start of the gregorian calendar) when the year is unknown
- if ($originalYear == 1604) {
- $originalYear = null;
- $unknownYear = true;
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- }
- if ($originalYear < 1970) {
- $birthday = '1970-' . $dateParts['month'] . '-' . $dateParts['date'];
- }
- }
+ $leapDay = ((int)$dateParts['month'] === 2
+ && (int)$dateParts['date'] === 29);
+ if ($dateParts['year'] === null || $originalYear < 1970) {
+ $birthday = ($leapDay ? '1972-' : '1970-')
+ . $dateParts['month'] . '-' . $dateParts['date'];
}
try {
@@ -281,18 +226,25 @@ class BirthdayService {
$vEvent->DTEND['VALUE'] = 'DATE';
$vEvent->{'UID'} = $doc->UID . $postfix;
$vEvent->{'RRULE'} = 'FREQ=YEARLY';
+ if ($leapDay) {
+ /* Sabre\VObject supports BYMONTHDAY only if BYMONTH
+ * is also set */
+ $vEvent->{'RRULE'} = 'FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1';
+ }
$vEvent->{'SUMMARY'} = $summary;
$vEvent->{'TRANSP'} = 'TRANSPARENT';
$vEvent->{'X-NEXTCLOUD-BC-FIELD-TYPE'} = $dateField;
- $vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $unknownYear ? '1' : '0';
+ $vEvent->{'X-NEXTCLOUD-BC-UNKNOWN-YEAR'} = $dateParts['year'] === null ? '1' : '0';
if ($originalYear !== null) {
- $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear;
+ $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string)$originalYear;
+ }
+ if ($reminderOffset) {
+ $alarm = $vCal->createComponent('VALARM');
+ $alarm->add($vCal->createProperty('TRIGGER', $reminderOffset, ['VALUE' => 'DURATION']));
+ $alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
+ $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
+ $vEvent->add($alarm);
}
- $alarm = $vCal->createComponent('VALARM');
- $alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION']));
- $alarm->add($vCal->createProperty('ACTION', 'DISPLAY'));
- $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'}));
- $vEvent->add($alarm);
$vCal->add($vEvent);
return $vCal;
}
@@ -301,8 +253,11 @@ class BirthdayService {
* @param string $user
*/
public function resetForUser(string $user):void {
- $principal = 'principals/users/'.$user;
+ $principal = 'principals/users/' . $user;
$calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI);
+ if (!$calendar) {
+ return; // The user's birthday calendar doesn't exist, no need to purge it
+ }
$calendarObjects = $this->calDavBackEnd->getCalendarObjects($calendar['id'], CalDavBackend::CALENDAR_TYPE_CALENDAR);
foreach ($calendarObjects as $calendarObject) {
@@ -315,13 +270,13 @@ class BirthdayService {
* @throws \Sabre\DAV\Exception\BadRequest
*/
public function syncUser(string $user):void {
- $principal = 'principals/users/'.$user;
+ $principal = 'principals/users/' . $user;
$this->ensureCalendarExists($principal);
$books = $this->cardDavBackEnd->getAddressBooksForUser($principal);
foreach ($books as $book) {
$cards = $this->cardDavBackEnd->getCards($book['id']);
foreach ($cards as $card) {
- $this->onCardChanged((int) $book['id'], $card['uri'], $card['carddata']);
+ $this->onCardChanged((int)$book['id'], $card['uri'], $card['carddata']);
}
}
}
@@ -332,7 +287,7 @@ class BirthdayService {
* @return bool
*/
public function birthdayEvenChanged(string $existingCalendarData,
- VCalendar $newCalendarData):bool {
+ VCalendar $newCalendarData):bool {
try {
$existingBirthday = Reader::read($existingCalendarData);
} catch (Exception $ex) {
@@ -340,8 +295,8 @@ class BirthdayService {
}
return (
- $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue() ||
- $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
+ $newCalendarData->VEVENT->DTSTART->getValue() !== $existingBirthday->VEVENT->DTSTART->getValue()
+ || $newCalendarData->VEVENT->SUMMARY->getValue() !== $existingBirthday->VEVENT->SUMMARY->getValue()
);
}
@@ -371,16 +326,18 @@ class BirthdayService {
* @param array $book
* @param int $calendarId
* @param array $type
+ * @param string $reminderOffset
* @throws InvalidDataException
* @throws \Sabre\DAV\Exception\BadRequest
*/
private function updateCalendar(string $cardUri,
- string $cardData,
- array $book,
- int $calendarId,
- array $type):void {
+ string $cardData,
+ array $book,
+ int $calendarId,
+ array $type,
+ ?string $reminderOffset):void {
$objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics';
- $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix']);
+ $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix'], $reminderOffset);
$existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri);
if ($calendarData === null) {
if ($existing !== null) {
@@ -421,14 +378,27 @@ class BirthdayService {
}
/**
+ * Extracts the userId part of a principal
+ *
+ * @param string $userPrincipal
+ * @return string|null
+ */
+ private function principalToUserId(string $userPrincipal):?string {
+ if (str_starts_with($userPrincipal, 'principals/users/')) {
+ return substr($userPrincipal, 17);
+ }
+ return null;
+ }
+
+ /**
* Checks if the user opted-out of birthday calendars
*
* @param string $userPrincipal The user principal to check for
* @return bool
*/
private function isUserEnabled(string $userPrincipal):bool {
- if (strpos($userPrincipal, 'principals/users/') === 0) {
- $userId = substr($userPrincipal, 17);
+ $userId = $this->principalToUserId($userPrincipal);
+ if ($userId !== null) {
$isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes');
return $isEnabled === 'yes';
}
@@ -438,6 +408,23 @@ class BirthdayService {
}
/**
+ * Get the reminder offset value for a user. This is a duration string (e.g.
+ * PT9H) or null if no reminder is wanted.
+ *
+ * @param string $userPrincipal
+ * @return string|null
+ */
+ private function getReminderOffsetForUser(string $userPrincipal):?string {
+ $userId = $this->principalToUserId($userPrincipal);
+ if ($userId !== null) {
+ return $this->config->getUserValue($userId, 'dav', 'birthdayCalendarReminderOffset', 'PT9H') ?: null;
+ }
+
+ // not sure how we got here, just be on the safe side and return the default value
+ return 'PT9H';
+ }
+
+ /**
* Formats title of Birthday event
*
* @param string $field Field name like BDAY, ANNIVERSARY, ...
@@ -447,9 +434,9 @@ class BirthdayService {
* @return string The formatted title
*/
private function formatTitle(string $field,
- string $name,
- int $year = null,
- bool $supports4Byte = true):string {
+ string $name,
+ ?int $year = null,
+ bool $supports4Byte = true):string {
if ($supports4Byte) {
switch ($field) {
case 'BDAY':