Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ public function refreshSubscription(string $principalUri, string $uri) {

$sObject = $vObject->serialize();
$uid = $vBase->UID->getValue();
$etag = md5($sObject);
// Strip DTSTAMP lines for etag computation only.
// Many providers (Google Calendar, Outlook, itslearning) set DTSTAMP
// to the current time on every feed request per RFC 5545, causing
// every event to appear modified on every refresh.
// DTSTAMP is kept in the stored data as it is a required property.
$sObjectForEtag = preg_replace('/^DTSTAMP:.*\r?\n/m', '', $sObject);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a risk that the line might be over 75 characters even in this case (for instance if we have a TZID), so I would handle content folding as well in the regex (new line must not be followed by a white-space character).

https://icalendar.org/iCalendar-RFC-5545/3-1-content-lines.html

$etag = md5($sObjectForEtag);

// No existing object with this UID, create it
if (!isset($existingObjects[$uid])) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,9 +460,75 @@ public function testRunCreateCalendarBadRequest(string $body, string $format, st
$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
}

public function testDtstampChangeDoesNotTriggerUpdate(): void {
$refreshWebcalService = new RefreshWebcalService(
$this->caldavBackend,
$this->logger,
$this->connection,
$this->timeFactory,
$this->importService
);

$this->caldavBackend->expects(self::once())
->method('getSubscriptionsForUser')
->with('principals/users/testuser')
->willReturn([
[
'id' => '42',
'uri' => 'sub123',
RefreshWebcalService::STRIP_TODOS => '1',
RefreshWebcalService::STRIP_ALARMS => '1',
RefreshWebcalService::STRIP_ATTACHMENTS => '1',
'source' => 'webcal://foo.bar/bla2',
'lastmodified' => 0,
],
]);

// Feed body has a new DTSTAMP (as happens on every fetch from Google/Outlook)
$body = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Test//Test//EN\r\nBEGIN:VEVENT\r\nUID:dtstamp-test\r\nDTSTAMP:20260209T120000Z\r\nDTSTART:20260301T100000Z\r\nSUMMARY:Test Event\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
$stream = $this->createStreamFromString($body);

$this->connection->expects(self::once())
->method('queryWebcalFeed')
->willReturn(['data' => $stream, 'format' => 'ical']);

// The stored etag was computed from the DTSTAMP-stripped serialization
$existingEtag = md5(preg_replace('/^DTSTAMP:.*\r?\n/m', '', $body));

$this->caldavBackend->expects(self::once())
->method('getLimitedCalendarObjects')
->willReturn([
'dtstamp-test' => [
'id' => 1,
'uid' => 'dtstamp-test',
'etag' => $existingEtag,
'uri' => 'dtstamp-test.ics',
],
]);

$vCalendar = VObject\Reader::read($body);
$generator = function () use ($vCalendar) {
yield $vCalendar;
};

$this->importService->expects(self::once())
->method('importText')
->willReturn($generator());

// DTSTAMP-only change must NOT trigger an update
$this->caldavBackend->expects(self::never())
->method('updateCalendarObject');

$this->caldavBackend->expects(self::never())
->method('createCalendarObject');

$refreshWebcalService->refreshSubscription('principals/users/testuser', 'sub123');
}

public static function identicalDataProvider(): array {
$icalBody = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Sabre//Sabre VObject " . VObject\Version::VERSION . "//EN\r\nCALSCALE:GREGORIAN\r\nBEGIN:VEVENT\r\nUID:12345\r\nDTSTAMP:20160218T133704Z\r\nDTSTART;VALUE=DATE:19000101\r\nDTEND;VALUE=DATE:19000102\r\nRRULE:FREQ=YEARLY\r\nSUMMARY:12345's Birthday (1900)\r\nTRANSP:TRANSPARENT\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
$etag = md5($icalBody);
// Etag is computed from DTSTAMP-stripped serialization
$etag = md5(preg_replace('/^DTSTAMP:.*\r?\n/m', '', $icalBody));

return [
[
Expand Down
Loading