2020-02-10 10:04:38 -05:00
< ? php
declare ( strict_types = 1 );
/**
2024-05-27 11:39:07 -04:00
* SPDX - FileCopyrightText : 2020 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2020-02-10 10:04:38 -05:00
*/
namespace OCA\DAV\CalDAV\WebcalCaching ;
use OCA\DAV\CalDAV\CalDavBackend ;
2025-11-27 09:56:01 -05:00
use OCA\DAV\CalDAV\Import\ImportService ;
2024-07-24 10:11:47 -04:00
use OCP\AppFramework\Utility\ITimeFactory ;
2022-03-31 09:34:57 -04:00
use Psr\Log\LoggerInterface ;
2020-02-10 10:04:38 -05:00
use Sabre\DAV\PropPatch ;
use Sabre\VObject\Component ;
use Sabre\VObject\DateTimeParser ;
use Sabre\VObject\InvalidDataException ;
use Sabre\VObject\ParseException ;
2020-03-16 10:04:21 -04:00
use Sabre\VObject\UUIDUtil ;
2020-02-10 10:04:38 -05: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 10:11:47 -04:00
public function __construct (
private CalDavBackend $calDavBackend ,
private LoggerInterface $logger ,
private Connection $connection ,
private ITimeFactory $time ,
2025-11-27 09:56:01 -05:00
private ImportService $importService ,
2024-07-24 10:11:47 -04:00
) {
2020-02-10 10:04:38 -05:00
}
public function refreshSubscription ( string $principalUri , string $uri ) {
$subscription = $this -> getSubscription ( $principalUri , $uri );
if ( ! $subscription ) {
return ;
}
2024-07-24 10:11:47 -04:00
// Check the refresh rate if there is any
2025-11-27 09:56:01 -05:00
if ( ! empty ( $subscription [ self :: REFRESH_RATE ])) {
// add the refresh interval to the last modified timestamp
$refreshInterval = new \DateInterval ( $subscription [ self :: REFRESH_RATE ]);
2024-07-24 10:11:47 -04:00
$updateTime = $this -> time -> getDateTime ();
$updateTime -> setTimestamp ( $subscription [ 'lastmodified' ]) -> add ( $refreshInterval );
if ( $updateTime -> getTimestamp () > $this -> time -> getTime ()) {
return ;
}
}
2025-11-27 09:56:01 -05:00
$result = $this -> connection -> queryWebcalFeed ( $subscription );
if ( ! $result ) {
2020-02-10 10:04:38 -05:00
return ;
}
2025-11-27 09:56:01 -05:00
$data = $result [ 'data' ];
$format = $result [ 'format' ];
2024-07-24 10:11:47 -04:00
2020-02-10 10:04:38 -05:00
$stripTodos = ( $subscription [ self :: STRIP_TODOS ] ? ? 1 ) === 1 ;
$stripAlarms = ( $subscription [ self :: STRIP_ALARMS ] ? ? 1 ) === 1 ;
$stripAttachments = ( $subscription [ self :: STRIP_ATTACHMENTS ] ? ? 1 ) === 1 ;
try {
2025-11-27 09:56:01 -05:00
$existingObjects = $this -> calDavBackend -> getLimitedCalendarObjects (( int ) $subscription [ 'id' ], CalDavBackend :: CALENDAR_TYPE_SUBSCRIPTION , [ 'id' , 'uid' , 'etag' , 'uri' ]);
2020-02-10 10:04:38 -05:00
2025-11-27 09:56:01 -05:00
$generator = match ( $format ) {
'xcal' => $this -> importService -> importXml ( ... ),
'jcal' => $this -> importService -> importJson ( ... ),
default => $this -> importService -> importText ( ... )
};
2020-02-10 10:04:38 -05:00
2025-11-27 09:56:01 -05:00
foreach ( $generator ( $data ) as $vObject ) {
/** @var Component\VCalendar $vObject */
$vBase = $vObject -> getBaseComponent ();
2024-07-24 10:11:47 -04:00
2026-01-23 09:57:59 -05:00
// Skip if no base component found
if ( ! isset ( $vBase -> UID )) {
2024-09-25 06:29:12 -04:00
continue ;
}
2025-11-27 09:56:01 -05:00
// Some calendar providers (e.g. Google, MS) use very long UIDs
if ( strlen ( $vBase -> UID -> getValue ()) > 512 ) {
$this -> logger -> warning ( 'Skipping calendar object with overly long UID from subscription "{subscriptionId}"' , [
'subscriptionId' => $subscription [ 'id' ],
'uid' => $vBase -> UID -> getValue (),
]);
2024-07-24 10:11:47 -04:00
continue ;
}
2025-11-27 09:56:01 -05:00
if ( $stripTodos && $vBase -> name === 'VTODO' ) {
2024-07-24 10:11:47 -04:00
continue ;
}
2025-11-27 09:56:01 -05:00
if ( $stripAlarms || $stripAttachments ) {
foreach ( $vObject -> getComponents () as $component ) {
if ( $component -> name === 'VTIMEZONE' ) {
continue ;
}
if ( $stripAlarms ) {
$component -> remove ( 'VALARM' );
}
if ( $stripAttachments ) {
$component -> remove ( 'ATTACH' );
}
}
2024-07-24 10:11:47 -04:00
}
2025-11-27 09:56:01 -05:00
$sObject = $vObject -> serialize ();
$uid = $vBase -> UID -> getValue ();
$etag = md5 ( $sObject );
// No existing object with this UID, create it
if ( ! isset ( $existingObjects [ $uid ])) {
try {
$this -> calDavBackend -> createCalendarObject (
$subscription [ 'id' ],
UUIDUtil :: getUUID () . '.ics' ,
$sObject ,
CalDavBackend :: CALENDAR_TYPE_SUBSCRIPTION
);
} catch ( \Exception $ex ) {
$this -> logger -> warning ( 'Unable to create calendar object from subscription {subscriptionId}' , [
'exception' => $ex ,
'subscriptionId' => $subscription [ 'id' ],
'source' => $subscription [ 'source' ],
]);
}
} elseif ( $existingObjects [ $uid ][ 'etag' ] !== $etag ) {
// Existing object with this UID but different etag, update it
$this -> calDavBackend -> updateCalendarObject (
$subscription [ 'id' ],
$existingObjects [ $uid ][ 'uri' ],
$sObject ,
CalDavBackend :: CALENDAR_TYPE_SUBSCRIPTION
);
unset ( $existingObjects [ $uid ]);
} else {
// Existing object with same etag, just remove from tracking
unset ( $existingObjects [ $uid ]);
2020-02-10 10:04:38 -05:00
}
}
2025-11-27 09:56:01 -05:00
// Clean up objects that no longer exist in the remote feed
// The only events left over should be those not found upstream
if ( ! empty ( $existingObjects )) {
$ids = array_map ( 'intval' , array_column ( $existingObjects , 'id' ));
$uris = array_column ( $existingObjects , 'uri' );
$this -> calDavBackend -> purgeCachedEventsForSubscription (( int ) $subscription [ 'id' ], $ids , $uris );
2024-07-24 10:11:47 -04:00
}
2025-11-27 09:56:01 -05:00
// Update refresh rate from the last processed object
if ( isset ( $vObject )) {
$this -> updateRefreshRate ( $subscription , $vObject );
2020-02-10 10:04:38 -05:00
}
} catch ( ParseException $ex ) {
2022-03-18 13:07:24 -04:00
$this -> logger -> error ( 'Subscription {subscriptionId} could not be refreshed due to a parsing error' , [ 'exception' => $ex , 'subscriptionId' => $subscription [ 'id' ]]);
2025-11-27 09:56:01 -05:00
} finally {
// Close the data stream to free resources
if ( is_resource ( $data )) {
fclose ( $data );
}
2020-02-10 10:04:38 -05:00
}
}
/**
* loads subscription from backend
*/
2022-03-18 13:07:24 -04:00
public function getSubscription ( string $principalUri , string $uri ) : ? array {
2020-02-10 10:04:38 -05:00
$subscriptions = array_values ( array_filter (
$this -> calDavBackend -> getSubscriptionsForUser ( $principalUri ),
2020-04-09 07:53:40 -04:00
function ( $sub ) use ( $uri ) {
2020-02-10 10:04:38 -05:00
return $sub [ 'uri' ] === $uri ;
}
));
if ( count ( $subscriptions ) === 0 ) {
return null ;
}
return $subscriptions [ 0 ];
}
/**
2025-11-27 09:56:01 -05:00
* Update refresh rate from calendar object if :
* - current subscription does not store a refreshrate
* - the webcal feed suggests a valid refreshrate
2020-02-10 10:04:38 -05:00
*/
2025-11-27 09:56:01 -05:00
private function updateRefreshRate ( array $subscription , Component\VCalendar $vCalendar ) : void {
// if there is already a refreshrate stored in the database, don't override it
if ( ! empty ( $subscription [ self :: REFRESH_RATE ])) {
return ;
2020-02-10 10:04:38 -05:00
}
2025-11-27 09:56:01 -05:00
$refreshRate = $vCalendar -> { 'REFRESH-INTERVAL' } ? -> getValue ()
? ? $vCalendar -> { 'X-PUBLISHED-TTL' } ? -> getValue ();
2020-02-10 10:04:38 -05:00
2025-11-27 09:56:01 -05:00
if ( $refreshRate === null ) {
return ;
2020-02-10 10:04:38 -05:00
}
2025-11-27 09:56:01 -05:00
// check if refresh rate is valid
2020-02-10 10:04:38 -05:00
try {
2025-11-27 09:56:01 -05:00
DateTimeParser :: parseDuration ( $refreshRate );
} catch ( InvalidDataException ) {
2020-02-10 10:04:38 -05:00
return ;
}
2025-11-27 09:56:01 -05:00
$propPatch = new PropPatch ([ self :: REFRESH_RATE => $refreshRate ]);
2020-02-10 10:04:38 -05:00
$this -> calDavBackend -> updateSubscription ( $subscription [ 'id' ], $propPatch );
$propPatch -> commit ();
}
}