2016-02-03 17:43:45 +03:00
< ? php
2019-12-03 21:57:53 +03:00
2019-08-21 11:35:17 +03:00
declare ( strict_types = 1 );
2019-12-03 21:57:53 +03:00
2016-02-03 17:43:45 +03:00
/**
2016-07-21 17:49:16 +03:00
* @ copyright Copyright ( c ) 2016 , ownCloud , Inc .
2019-08-21 11:35:17 +03:00
* @ copyright Copyright ( c ) 2019 , Georg Ehrke
2016-07-21 17:49:16 +03:00
*
2016-05-26 20:56:05 +03:00
* @ author Achim Königs < garfonso @ tratschtante . de >
2020-04-29 12:57:22 +03:00
* @ author Christoph Wurst < christoph @ winzerhof - wurst . at >
2017-11-06 22:15:27 +03:00
* @ author Georg Ehrke < oc . list @ georgehrke . com >
2016-07-21 19:13:36 +03:00
* @ author Robin Appelman < robin @ icewind . nl >
2020-12-16 16:54:15 +03:00
* @ author Sven Strickroth < email @ cs - ware . de >
2016-02-03 17:43:45 +03:00
* @ author Thomas Müller < thomas . mueller @ tmit . eu >
*
* @ 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 ,
2019-12-03 21:57:53 +03:00
* along with this program . If not , see < http :// www . gnu . org / licenses />
2016-02-03 17:43:45 +03:00
*
*/
namespace OCA\DAV\CalDAV ;
use Exception ;
use OCA\DAV\CardDAV\CardDavBackend ;
2016-03-23 16:12:50 +03:00
use OCA\DAV\DAV\GroupPrincipalBackend ;
2017-11-11 03:27:48 +03:00
use OCP\IConfig ;
2018-11-27 20:41:40 +03:00
use OCP\IDBConnection ;
2019-08-21 11:35:17 +03:00
use OCP\IL10N ;
2016-02-03 17:43:45 +03:00
use Sabre\VObject\Component\VCalendar ;
2016-12-08 01:10:11 +03:00
use Sabre\VObject\Component\VCard ;
use Sabre\VObject\DateTimeParser ;
use Sabre\VObject\Document ;
use Sabre\VObject\InvalidDataException ;
use Sabre\VObject\Property\VCard\DateAndOrTime ;
2016-02-03 17:43:45 +03:00
use Sabre\VObject\Reader ;
2019-08-21 11:35:17 +03:00
/**
* Class BirthdayService
*
* @ package OCA\DAV\CalDAV
*/
2016-02-03 17:43:45 +03:00
class BirthdayService {
2020-04-10 17:54:27 +03:00
public const BIRTHDAY_CALENDAR_URI = 'contact_birthdays' ;
2016-02-03 17:43:45 +03:00
2016-03-23 16:12:50 +03:00
/** @var GroupPrincipalBackend */
private $principalBackend ;
2016-07-05 23:45:05 +03:00
/** @var CalDavBackend */
private $calDavBackEnd ;
/** @var CardDavBackend */
private $cardDavBackEnd ;
2017-11-11 03:27:48 +03:00
/** @var IConfig */
private $config ;
2018-11-27 20:41:40 +03:00
/** @var IDBConnection */
private $dbConnection ;
2019-08-21 11:35:17 +03:00
/** @var IL10N */
private $l10n ;
2016-02-03 17:43:45 +03:00
/**
* BirthdayService constructor .
*
* @ param CalDavBackend $calDavBackEnd
* @ param CardDavBackend $cardDavBackEnd
2016-03-23 16:12:50 +03:00
* @ param GroupPrincipalBackend $principalBackend
2019-08-21 11:35:17 +03:00
* @ param IConfig $config
* @ param IDBConnection $dbConnection
* @ param IL10N $l10n
2016-02-03 17:43:45 +03:00
*/
2019-08-21 11:35:17 +03:00
public function __construct ( CalDavBackend $calDavBackEnd ,
CardDavBackend $cardDavBackEnd ,
GroupPrincipalBackend $principalBackend ,
IConfig $config ,
IDBConnection $dbConnection ,
IL10N $l10n ) {
2016-02-03 17:43:45 +03:00
$this -> calDavBackEnd = $calDavBackEnd ;
$this -> cardDavBackEnd = $cardDavBackEnd ;
2016-03-23 16:12:50 +03:00
$this -> principalBackend = $principalBackend ;
2017-11-11 03:27:48 +03:00
$this -> config = $config ;
2018-11-27 20:41:40 +03:00
$this -> dbConnection = $dbConnection ;
2019-08-21 11:35:17 +03:00
$this -> l10n = $l10n ;
2016-02-03 17:43:45 +03:00
}
/**
* @ param int $addressBookId
* @ param string $cardUri
* @ param string $cardData
*/
2019-08-21 11:35:17 +03:00
public function onCardChanged ( int $addressBookId ,
string $cardUri ,
string $cardData ) {
2017-11-11 03:27:48 +03:00
if ( ! $this -> isGloballyEnabled ()) {
return ;
}
2016-03-23 16:12:50 +03:00
$targetPrincipals = $this -> getAllAffectedPrincipals ( $addressBookId );
2016-02-03 17:43:45 +03:00
$book = $this -> cardDavBackEnd -> getAddressBookById ( $addressBookId );
2016-03-23 14:28:54 +03:00
$targetPrincipals [] = $book [ 'principaluri' ];
2016-09-30 11:29:27 +03:00
$datesToSync = [
2019-08-21 11:35:17 +03:00
[ 'postfix' => '' , 'field' => 'BDAY' ],
[ 'postfix' => '-death' , 'field' => 'DEATHDATE' ],
[ 'postfix' => '-anniversary' , 'field' => 'ANNIVERSARY' ],
2016-09-30 11:29:27 +03:00
];
2019-08-21 11:35:17 +03:00
2016-03-23 14:28:54 +03:00
foreach ( $targetPrincipals as $principalUri ) {
2017-11-11 03:27:48 +03:00
if ( ! $this -> isUserEnabled ( $principalUri )) {
continue ;
}
2016-03-23 14:28:54 +03:00
$calendar = $this -> ensureCalendarExists ( $principalUri );
2016-09-30 11:29:27 +03:00
foreach ( $datesToSync as $type ) {
2019-08-21 11:35:17 +03:00
$this -> updateCalendar ( $cardUri , $cardData , $book , ( int ) $calendar [ 'id' ], $type );
2016-02-03 17:43:45 +03:00
}
}
}
/**
* @ param int $addressBookId
* @ param string $cardUri
*/
2019-08-21 11:35:17 +03:00
public function onCardDeleted ( int $addressBookId ,
string $cardUri ) {
2017-11-11 03:27:48 +03:00
if ( ! $this -> isGloballyEnabled ()) {
return ;
}
2016-03-23 16:12:50 +03:00
$targetPrincipals = $this -> getAllAffectedPrincipals ( $addressBookId );
2016-02-03 17:43:45 +03:00
$book = $this -> cardDavBackEnd -> getAddressBookById ( $addressBookId );
2016-03-23 14:28:54 +03:00
$targetPrincipals [] = $book [ 'principaluri' ];
foreach ( $targetPrincipals as $principalUri ) {
2017-11-11 03:27:48 +03:00
if ( ! $this -> isUserEnabled ( $principalUri )) {
continue ;
}
2016-03-23 14:28:54 +03:00
$calendar = $this -> ensureCalendarExists ( $principalUri );
2016-09-30 11:29:27 +03:00
foreach ([ '' , '-death' , '-anniversary' ] as $tag ) {
$objectUri = $book [ 'uri' ] . '-' . $cardUri . $tag . '.ics' ;
$this -> calDavBackEnd -> deleteCalendarObject ( $calendar [ 'id' ], $objectUri );
}
2016-03-23 14:28:54 +03:00
}
2016-02-03 17:43:45 +03:00
}
/**
* @ param string $principal
* @ return array | null
* @ throws \Sabre\DAV\Exception\BadRequest
*/
2019-08-21 11:35:17 +03:00
public function ensureCalendarExists ( string $principal ) : ? array {
2019-02-16 18:18:58 +03:00
$calendar = $this -> calDavBackEnd -> getCalendarByUri ( $principal , self :: BIRTHDAY_CALENDAR_URI );
if ( ! is_null ( $calendar )) {
return $calendar ;
2016-02-03 17:43:45 +03:00
}
2016-03-16 19:19:14 +03:00
$this -> calDavBackEnd -> createCalendar ( $principal , self :: BIRTHDAY_CALENDAR_URI , [
'{DAV:}displayname' => 'Contact birthdays' ,
2020-01-14 18:48:48 +03:00
'{http://apple.com/ns/ical/}calendar-color' => '#E9D859' ,
2020-10-05 16:12:57 +03:00
'components' => 'VEVENT' ,
2016-03-16 19:19:14 +03:00
]);
2016-02-03 17:43:45 +03:00
2016-03-16 19:19:14 +03:00
return $this -> calDavBackEnd -> getCalendarByUri ( $principal , self :: BIRTHDAY_CALENDAR_URI );
2016-02-03 17:43:45 +03:00
}
/**
2019-08-21 11:35:17 +03:00
* @ param $cardData
* @ param $dateField
* @ param $postfix
* @ return VCalendar | null
* @ throws InvalidDataException
2016-02-03 17:43:45 +03:00
*/
2019-08-21 11:35:17 +03:00
public function buildDateFromContact ( string $cardData ,
string $dateField ,
string $postfix ) : ? VCalendar {
2016-02-03 17:43:45 +03:00
if ( empty ( $cardData )) {
return null ;
}
try {
$doc = Reader :: read ( $cardData );
2016-12-08 01:10:11 +03:00
// 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 );
2016-02-03 17:43:45 +03:00
} catch ( Exception $e ) {
return null ;
}
2016-09-30 11:29:27 +03:00
if ( ! isset ( $doc -> { $dateField })) {
2016-02-03 17:43:45 +03:00
return null ;
}
2016-12-08 01:10:11 +03:00
if ( ! isset ( $doc -> FN )) {
return null ;
}
2016-09-30 11:29:27 +03:00
$birthday = $doc -> { $dateField };
2016-02-03 17:43:45 +03:00
if ( ! ( string ) $birthday ) {
return null ;
}
2016-12-08 01:10:11 +03:00
// 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 ;
2018-10-14 22:20:04 +03:00
$originalYear = null ;
2016-12-08 01:10:11 +03:00
if ( ! $dateParts [ 'year' ]) {
2018-10-14 22:20:04 +03:00
$birthday = '1970-' . $dateParts [ 'month' ] . '-' . $dateParts [ 'date' ];
2016-12-08 01:10:11 +03:00
$unknownYear = true ;
2018-10-14 22:20:04 +03:00
} 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' ];
2020-11-13 17:57:12 +03:00
// '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' ];
}
2018-10-14 22:20:04 +03:00
if ( $originalYear < 1970 ) {
$birthday = '1970-' . $dateParts [ 'month' ] . '-' . $dateParts [ 'date' ];
}
}
2016-12-08 01:10:11 +03:00
}
2016-02-03 17:43:45 +03:00
try {
2019-09-16 16:47:42 +03:00
if ( $birthday instanceof DateAndOrTime ) {
$date = $birthday -> getDateTime ();
} else {
$date = new \DateTimeImmutable ( $birthday );
}
2016-02-03 17:43:45 +03:00
} catch ( Exception $e ) {
return null ;
}
2019-08-21 11:35:17 +03:00
$summary = $this -> formatTitle ( $dateField , $doc -> FN -> getValue (), $originalYear , $this -> dbConnection -> supports4ByteText ());
2018-11-27 20:41:40 +03:00
2016-02-03 17:43:45 +03:00
$vCal = new VCalendar ();
$vCal -> VERSION = '2.0' ;
2020-03-10 16:13:43 +03:00
$vCal -> PRODID = '-//IDN nextcloud.com//Birthday calendar//EN' ;
2016-02-03 17:43:45 +03:00
$vEvent = $vCal -> createComponent ( 'VEVENT' );
$vEvent -> add ( 'DTSTART' );
$vEvent -> DTSTART -> setDateTime (
$date
);
$vEvent -> DTSTART [ 'VALUE' ] = 'DATE' ;
$vEvent -> add ( 'DTEND' );
2019-09-16 16:47:42 +03:00
$dtEndDate = ( new \DateTime ()) -> setTimestamp ( $date -> getTimeStamp ());
$dtEndDate -> add ( new \DateInterval ( 'P1D' ));
2016-02-03 17:43:45 +03:00
$vEvent -> DTEND -> setDateTime (
2019-09-16 16:47:42 +03:00
$dtEndDate
2016-02-03 17:43:45 +03:00
);
2019-09-16 16:47:42 +03:00
2016-02-03 17:43:45 +03:00
$vEvent -> DTEND [ 'VALUE' ] = 'DATE' ;
2018-01-04 22:15:24 +03:00
$vEvent -> { 'UID' } = $doc -> UID . $postfix ;
2016-02-03 17:43:45 +03:00
$vEvent -> { 'RRULE' } = 'FREQ=YEARLY' ;
2016-11-17 00:06:36 +03:00
$vEvent -> { 'SUMMARY' } = $summary ;
2016-02-03 17:43:45 +03:00
$vEvent -> { 'TRANSP' } = 'TRANSPARENT' ;
2018-10-14 22:20:04 +03:00
$vEvent -> { 'X-NEXTCLOUD-BC-FIELD-TYPE' } = $dateField ;
$vEvent -> { 'X-NEXTCLOUD-BC-UNKNOWN-YEAR' } = $unknownYear ? '1' : '0' ;
if ( $originalYear !== null ) {
$vEvent -> { 'X-NEXTCLOUD-BC-YEAR' } = ( string ) $originalYear ;
}
2016-03-23 01:47:34 +03:00
$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 );
2016-02-03 17:43:45 +03:00
$vCal -> add ( $vEvent );
return $vCal ;
}
2019-02-16 18:18:58 +03:00
/**
* @ param string $user
*/
2019-08-21 11:35:17 +03:00
public function resetForUser ( string $user ) : void {
2019-02-16 18:18:58 +03:00
$principal = 'principals/users/' . $user ;
$calendar = $this -> calDavBackEnd -> getCalendarByUri ( $principal , self :: BIRTHDAY_CALENDAR_URI );
$calendarObjects = $this -> calDavBackEnd -> getCalendarObjects ( $calendar [ 'id' ], CalDavBackend :: CALENDAR_TYPE_CALENDAR );
2020-04-10 15:19:56 +03:00
foreach ( $calendarObjects as $calendarObject ) {
2019-02-16 18:18:58 +03:00
$this -> calDavBackEnd -> deleteCalendarObject ( $calendar [ 'id' ], $calendarObject [ 'uri' ], CalDavBackend :: CALENDAR_TYPE_CALENDAR );
}
}
2016-02-18 16:49:45 +03:00
/**
* @ param string $user
2019-08-21 11:35:17 +03:00
* @ throws \Sabre\DAV\Exception\BadRequest
2016-02-18 16:49:45 +03:00
*/
2019-08-21 11:35:17 +03:00
public function syncUser ( string $user ) : void {
2016-03-16 19:19:14 +03:00
$principal = 'principals/users/' . $user ;
$this -> ensureCalendarExists ( $principal );
$books = $this -> cardDavBackEnd -> getAddressBooksForUser ( $principal );
2020-04-10 15:19:56 +03:00
foreach ( $books as $book ) {
2016-02-18 16:49:45 +03:00
$cards = $this -> cardDavBackEnd -> getCards ( $book [ 'id' ]);
2020-04-10 15:19:56 +03:00
foreach ( $cards as $card ) {
2019-09-16 16:47:42 +03:00
$this -> onCardChanged (( int ) $book [ 'id' ], $card [ 'uri' ], $card [ 'carddata' ]);
2016-02-18 16:49:45 +03:00
}
}
}
/**
* @ param string $existingCalendarData
* @ param VCalendar $newCalendarData
* @ return bool
*/
2019-08-21 11:35:17 +03:00
public function birthdayEvenChanged ( string $existingCalendarData ,
VCalendar $newCalendarData ) : bool {
2016-02-18 16:49:45 +03:00
try {
$existingBirthday = Reader :: read ( $existingCalendarData );
} catch ( Exception $ex ) {
return true ;
}
2019-08-21 11:35:17 +03:00
return (
$newCalendarData -> VEVENT -> DTSTART -> getValue () !== $existingBirthday -> VEVENT -> DTSTART -> getValue () ||
2016-02-18 16:49:45 +03:00
$newCalendarData -> VEVENT -> SUMMARY -> getValue () !== $existingBirthday -> VEVENT -> SUMMARY -> getValue ()
2019-08-21 11:35:17 +03:00
);
2016-02-18 16:49:45 +03:00
}
2016-03-23 16:12:50 +03:00
/**
2016-04-08 18:11:37 +03:00
* @ param integer $addressBookId
2016-03-23 16:12:50 +03:00
* @ return mixed
*/
2019-08-21 11:35:17 +03:00
protected function getAllAffectedPrincipals ( int $addressBookId ) {
2016-03-23 16:12:50 +03:00
$targetPrincipals = [];
$shares = $this -> cardDavBackEnd -> getShares ( $addressBookId );
foreach ( $shares as $share ) {
if ( $share [ '{http://owncloud.org/ns}group-share' ]) {
$users = $this -> principalBackend -> getGroupMemberSet ( $share [ '{http://owncloud.org/ns}principal' ]);
foreach ( $users as $user ) {
$targetPrincipals [] = $user [ 'uri' ];
}
} else {
$targetPrincipals [] = $share [ '{http://owncloud.org/ns}principal' ];
}
}
return array_values ( array_unique ( $targetPrincipals , SORT_STRING ));
}
2016-09-30 11:29:27 +03:00
/**
* @ param string $cardUri
2019-08-21 11:35:17 +03:00
* @ param string $cardData
2016-09-30 11:29:27 +03:00
* @ param array $book
* @ param int $calendarId
2019-08-21 11:35:17 +03:00
* @ param array $type
* @ throws InvalidDataException
* @ throws \Sabre\DAV\Exception\BadRequest
2016-09-30 11:29:27 +03:00
*/
2019-08-21 11:35:17 +03:00
private function updateCalendar ( string $cardUri ,
string $cardData ,
array $book ,
int $calendarId ,
array $type ) : void {
2016-09-30 11:29:27 +03:00
$objectUri = $book [ 'uri' ] . '-' . $cardUri . $type [ 'postfix' ] . '.ics' ;
2019-08-21 11:35:17 +03:00
$calendarData = $this -> buildDateFromContact ( $cardData , $type [ 'field' ], $type [ 'postfix' ]);
2016-09-30 11:29:27 +03:00
$existing = $this -> calDavBackEnd -> getCalendarObject ( $calendarId , $objectUri );
if ( is_null ( $calendarData )) {
if ( ! is_null ( $existing )) {
$this -> calDavBackEnd -> deleteCalendarObject ( $calendarId , $objectUri );
}
} else {
if ( is_null ( $existing )) {
$this -> calDavBackEnd -> createCalendarObject ( $calendarId , $objectUri , $calendarData -> serialize ());
} else {
if ( $this -> birthdayEvenChanged ( $existing [ 'calendardata' ], $calendarData )) {
$this -> calDavBackEnd -> updateCalendarObject ( $calendarId , $objectUri , $calendarData -> serialize ());
}
}
}
}
2017-11-11 03:27:48 +03:00
/**
* checks if the admin opted - out of birthday calendars
*
* @ return bool
*/
2019-08-21 11:35:17 +03:00
private function isGloballyEnabled () : bool {
return $this -> config -> getAppValue ( 'dav' , 'generateBirthdayCalendar' , 'yes' ) === 'yes' ;
2017-11-11 03:27:48 +03:00
}
/**
2019-08-21 11:35:17 +03:00
* Checks if the user opted - out of birthday calendars
2017-11-11 03:27:48 +03:00
*
2019-08-21 11:35:17 +03:00
* @ param string $userPrincipal The user principal to check for
2017-11-11 03:27:48 +03:00
* @ return bool
*/
2019-08-21 11:35:17 +03:00
private function isUserEnabled ( string $userPrincipal ) : bool {
2017-11-11 03:27:48 +03:00
if ( strpos ( $userPrincipal , 'principals/users/' ) === 0 ) {
$userId = substr ( $userPrincipal , 17 );
$isEnabled = $this -> config -> getUserValue ( $userId , 'dav' , 'generateBirthdayCalendar' , 'yes' );
return $isEnabled === 'yes' ;
}
// not sure how we got here, just be on the safe side and return true
return true ;
}
2019-08-21 11:35:17 +03:00
/**
* Formats title of Birthday event
*
* @ param string $field Field name like BDAY , ANNIVERSARY , ...
* @ param string $name Name of contact
* @ param int | null $year Year of birth , anniversary , ...
* @ param bool $supports4Byte Whether or not the database supports 4 byte chars
* @ return string The formatted title
*/
private function formatTitle ( string $field ,
string $name ,
2020-10-05 16:12:57 +03:00
int $year = null ,
bool $supports4Byte = true ) : string {
2019-08-21 11:35:17 +03:00
if ( $supports4Byte ) {
switch ( $field ) {
case 'BDAY' :
return implode ( '' , [
'🎂 ' ,
$name ,
$year ? ( ' (' . $year . ')' ) : '' ,
]);
case 'DEATHDATE' :
return implode ( '' , [
$this -> l10n -> t ( 'Death of %s' , [ $name ]),
$year ? ( ' (' . $year . ')' ) : '' ,
]);
case 'ANNIVERSARY' :
return implode ( '' , [
'💍 ' ,
$name ,
$year ? ( ' (' . $year . ')' ) : '' ,
]);
default :
return '' ;
}
} else {
2020-04-10 15:19:56 +03:00
switch ( $field ) {
2019-08-21 11:35:17 +03:00
case 'BDAY' :
return implode ( '' , [
$name ,
' ' ,
$year ? ( '(*' . $year . ')' ) : '*' ,
]);
case 'DEATHDATE' :
return implode ( '' , [
$this -> l10n -> t ( 'Death of %s' , [ $name ]),
$year ? ( ' (' . $year . ')' ) : '' ,
]);
case 'ANNIVERSARY' :
return implode ( '' , [
$name ,
' ' ,
$year ? ( '(⚭' . $year . ')' ) : '⚭' ,
]);
default :
return '' ;
}
}
}
2016-02-03 17:43:45 +03:00
}