2020-02-10 16:04:38 +01:00
< ? php
declare ( strict_types = 1 );
/**
2024-05-27 17:39:07 +02:00
* SPDX - FileCopyrightText : 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2020-02-10 16:04:38 +01:00
*/
namespace OCA\DAV\CalDAV\WebcalCaching ;
use OCA\DAV\CalDAV\CalDavBackend ;
2024-07-24 16:11:47 +02:00
use OCP\AppFramework\Utility\ITimeFactory ;
2022-03-31 15:34:57 +02:00
use Psr\Log\LoggerInterface ;
2020-02-10 16:04:38 +01:00
use Sabre\DAV\Exception\BadRequest ;
2024-09-25 12:29:12 +02:00
use Sabre\DAV\Exception\Forbidden ;
2020-02-10 16:04:38 +01:00
use Sabre\DAV\PropPatch ;
use Sabre\VObject\Component ;
use Sabre\VObject\DateTimeParser ;
use Sabre\VObject\InvalidDataException ;
use Sabre\VObject\ParseException ;
use Sabre\VObject\Reader ;
2023-11-23 10:22:34 +01:00
use Sabre\VObject\Recur\NoInstancesException ;
2020-02-10 16:04:38 +01:00
use Sabre\VObject\Splitter\ICalendar ;
2020-03-16 15:04:21 +01:00
use Sabre\VObject\UUIDUtil ;
2020-02-10 16:04:38 +01:00
use function count ;
class RefreshWebcalService {
public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate' ;
public const STRIP_ALARMS = '{http://calendarserver.org/ns/}subscribed-strip-alarms' ;
public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments' ;
public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos' ;
2024-07-24 16:11:47 +02:00
public function __construct ( private CalDavBackend $calDavBackend ,
private LoggerInterface $logger ,
private Connection $connection ,
private ITimeFactory $time ) {
2020-02-10 16:04:38 +01:00
}
public function refreshSubscription ( string $principalUri , string $uri ) {
$subscription = $this -> getSubscription ( $principalUri , $uri );
$mutations = [];
if ( ! $subscription ) {
return ;
}
2024-07-24 16:11:47 +02:00
// Check the refresh rate if there is any
if ( ! empty ( $subscription [ '{http://apple.com/ns/ical/}refreshrate' ])) {
// add the refresh interval to the lastmodified timestamp
$refreshInterval = new \DateInterval ( $subscription [ '{http://apple.com/ns/ical/}refreshrate' ]);
$updateTime = $this -> time -> getDateTime ();
$updateTime -> setTimestamp ( $subscription [ 'lastmodified' ]) -> add ( $refreshInterval );
if ( $updateTime -> getTimestamp () > $this -> time -> getTime ()) {
return ;
}
}
2024-08-19 13:28:04 +02:00
$webcalData = $this -> connection -> queryWebcalFeed ( $subscription );
2020-02-10 16:04:38 +01:00
if ( ! $webcalData ) {
return ;
}
2024-07-24 16:11:47 +02:00
$localData = $this -> calDavBackend -> getLimitedCalendarObjects (( int ) $subscription [ 'id' ], CalDavBackend :: CALENDAR_TYPE_SUBSCRIPTION );
2020-02-10 16:04:38 +01:00
$stripTodos = ( $subscription [ self :: STRIP_TODOS ] ? ? 1 ) === 1 ;
$stripAlarms = ( $subscription [ self :: STRIP_ALARMS ] ? ? 1 ) === 1 ;
$stripAttachments = ( $subscription [ self :: STRIP_ATTACHMENTS ] ? ? 1 ) === 1 ;
try {
$splitter = new ICalendar ( $webcalData , Reader :: OPTION_FORGIVING );
while ( $vObject = $splitter -> getNext ()) {
/** @var Component $vObject */
$compName = null ;
2024-07-24 16:11:47 +02:00
$uid = null ;
2020-02-10 16:04:38 +01:00
foreach ( $vObject -> getComponents () as $component ) {
if ( $component -> name === 'VTIMEZONE' ) {
continue ;
}
$compName = $component -> name ;
if ( $stripAlarms ) {
unset ( $component -> { 'VALARM' });
}
if ( $stripAttachments ) {
unset ( $component -> { 'ATTACH' });
}
2024-07-24 16:11:47 +02:00
$uid = $component -> { 'UID' } -> getValue ();
2020-02-10 16:04:38 +01:00
}
if ( $stripTodos && $compName === 'VTODO' ) {
continue ;
}
2024-07-24 16:11:47 +02:00
if ( ! isset ( $uid )) {
continue ;
}
2024-09-25 12:29:12 +02:00
try {
$denormalized = $this -> calDavBackend -> getDenormalizedData ( $vObject -> serialize ());
} catch ( InvalidDataException | Forbidden $ex ) {
$this -> logger -> warning ( 'Unable to denormalize calendar object from subscription {subscriptionId}' , [ 'exception' => $ex , 'subscriptionId' => $subscription [ 'id' ], 'source' => $subscription [ 'source' ]]);
continue ;
}
2024-07-24 16:11:47 +02:00
// Find all identical sets and remove them from the update
if ( isset ( $localData [ $uid ]) && $denormalized [ 'etag' ] === $localData [ $uid ][ 'etag' ]) {
unset ( $localData [ $uid ]);
continue ;
}
$vObjectCopy = clone $vObject ;
$identical = isset ( $localData [ $uid ]) && $this -> compareWithoutDtstamp ( $vObjectCopy , $localData [ $uid ]);
if ( $identical ) {
unset ( $localData [ $uid ]);
continue ;
}
// Find all modified sets and update them
if ( isset ( $localData [ $uid ]) && $denormalized [ 'etag' ] !== $localData [ $uid ][ 'etag' ]) {
$this -> calDavBackend -> updateCalendarObject ( $subscription [ 'id' ], $localData [ $uid ][ 'uri' ], $vObject -> serialize (), CalDavBackend :: CALENDAR_TYPE_SUBSCRIPTION );
unset ( $localData [ $uid ]);
continue ;
}
// Only entirely new events get created here
2020-02-10 16:04:38 +01:00
try {
2024-07-24 16:11:47 +02:00
$objectUri = $this -> getRandomCalendarObjectUri ();
$this -> calDavBackend -> createCalendarObject ( $subscription [ 'id' ], $objectUri , $vObject -> serialize (), CalDavBackend :: CALENDAR_TYPE_SUBSCRIPTION );
2020-09-19 22:47:30 -07:00
} catch ( NoInstancesException | BadRequest $ex ) {
2024-09-25 12:29:12 +02:00
$this -> logger -> warning ( 'Unable to create calendar object from subscription {subscriptionId}' , [ 'exception' => $ex , 'subscriptionId' => $subscription [ 'id' ], 'source' => $subscription [ 'source' ]]);
2020-02-10 16:04:38 +01:00
}
}
2024-07-24 16:11:47 +02:00
$ids = array_map ( static function ( $dataSet ) : int {
return ( int ) $dataSet [ 'id' ];
}, $localData );
$uris = array_map ( static function ( $dataSet ) : string {
return $dataSet [ 'uri' ];
}, $localData );
if ( ! empty ( $ids ) && ! empty ( $uris )) {
// Clean up on aisle 5
// The only events left over in the $localData array should be those that don't exist upstream
// All deleted VObjects from upstream are removed
$this -> calDavBackend -> purgeCachedEventsForSubscription ( $subscription [ 'id' ], $ids , $uris );
}
2020-02-10 16:04:38 +01:00
$newRefreshRate = $this -> checkWebcalDataForRefreshRate ( $subscription , $webcalData );
if ( $newRefreshRate ) {
$mutations [ self :: REFRESH_RATE ] = $newRefreshRate ;
}
$this -> updateSubscription ( $subscription , $mutations );
2020-04-10 14:19:56 +02:00
} catch ( ParseException $ex ) {
2022-03-18 18:07:24 +01:00
$this -> logger -> error ( 'Subscription {subscriptionId} could not be refreshed due to a parsing error' , [ 'exception' => $ex , 'subscriptionId' => $subscription [ 'id' ]]);
2020-02-10 16:04:38 +01:00
}
}
/**
* loads subscription from backend
*/
2022-03-18 18:07:24 +01:00
public function getSubscription ( string $principalUri , string $uri ) : ? array {
2020-02-10 16:04:38 +01:00
$subscriptions = array_values ( array_filter (
$this -> calDavBackend -> getSubscriptionsForUser ( $principalUri ),
2020-04-09 13:53:40 +02:00
function ( $sub ) use ( $uri ) {
2020-02-10 16:04:38 +01:00
return $sub [ 'uri' ] === $uri ;
}
));
if ( count ( $subscriptions ) === 0 ) {
return null ;
}
return $subscriptions [ 0 ];
}
/**
* check if :
* - current subscription stores a refreshrate
* - the webcal feed suggests a refreshrate
* - return suggested refreshrate if user didn ' t set a custom one
*
*/
2022-03-18 18:07:24 +01:00
private function checkWebcalDataForRefreshRate ( array $subscription , string $webcalData ) : ? string {
2020-02-10 16:04:38 +01:00
// if there is no refreshrate stored in the database, check the webcal feed
// whether it suggests any refresh rate and store that in the database
if ( isset ( $subscription [ self :: REFRESH_RATE ]) && $subscription [ self :: REFRESH_RATE ] !== null ) {
return null ;
}
/** @var Component\VCalendar $vCalendar */
$vCalendar = Reader :: read ( $webcalData );
$newRefreshRate = null ;
if ( isset ( $vCalendar -> { 'X-PUBLISHED-TTL' })) {
$newRefreshRate = $vCalendar -> { 'X-PUBLISHED-TTL' } -> getValue ();
}
if ( isset ( $vCalendar -> { 'REFRESH-INTERVAL' })) {
$newRefreshRate = $vCalendar -> { 'REFRESH-INTERVAL' } -> getValue ();
}
if ( ! $newRefreshRate ) {
return null ;
}
// check if new refresh rate is even valid
try {
DateTimeParser :: parseDuration ( $newRefreshRate );
2020-04-10 14:19:56 +02:00
} catch ( InvalidDataException $ex ) {
2020-02-10 16:04:38 +01:00
return null ;
}
return $newRefreshRate ;
}
/**
* update subscription stored in database
* used to set :
* - refreshrate
* - source
*
* @ param array $subscription
* @ param array $mutations
*/
private function updateSubscription ( array $subscription , array $mutations ) {
if ( empty ( $mutations )) {
return ;
}
$propPatch = new PropPatch ( $mutations );
$this -> calDavBackend -> updateSubscription ( $subscription [ 'id' ], $propPatch );
$propPatch -> commit ();
}
/**
2024-07-24 16:11:47 +02:00
* Returns a random uri for a calendar - object
2020-02-10 16:04:38 +01:00
*
2024-07-24 16:11:47 +02:00
* @ return string
2020-02-10 16:04:38 +01:00
*/
2024-07-24 16:11:47 +02:00
public function getRandomCalendarObjectUri () : string {
return UUIDUtil :: getUUID () . '.ics' ;
}
2020-02-10 16:04:38 +01:00
2024-07-24 16:11:47 +02:00
private function compareWithoutDtstamp ( Component $vObject , array $calendarObject ) : bool {
foreach ( $vObject -> getComponents () as $component ) {
unset ( $component -> { 'DTSTAMP' });
2020-02-10 16:04:38 +01:00
}
2024-07-24 16:11:47 +02:00
$localVobject = Reader :: read ( $calendarObject [ 'calendardata' ]);
foreach ( $localVobject -> getComponents () as $component ) {
unset ( $component -> { 'DTSTAMP' });
2020-02-10 16:04:38 +01:00
}
2024-07-24 16:11:47 +02:00
return strcasecmp ( $localVobject -> serialize (), $vObject -> serialize ()) === 0 ;
2020-03-16 15:04:21 +01:00
}
2020-02-10 16:04:38 +01:00
}