@@ -493,7 +493,7 @@ public function testDtstampChangeDoesNotTriggerUpdate(): void {
493493 ->willReturn (['data ' => $ stream , 'format ' => 'ical ' ]);
494494
495495 // The stored etag was computed from the DTSTAMP-stripped serialization
496- $ existingEtag = md5 (preg_replace ('/^DTSTAMP: .*\r?\n/m ' , '' , $ body ));
496+ $ existingEtag = md5 (preg_replace ('/^DTSTAMP[;:] .*\r?\n([ \t].*\r?\n)* /m ' , '' , $ body ));
497497
498498 $ this ->caldavBackend ->expects (self ::once ())
499499 ->method ('getLimitedCalendarObjects ' )
@@ -525,10 +525,77 @@ public function testDtstampChangeDoesNotTriggerUpdate(): void {
525525 $ refreshWebcalService ->refreshSubscription ('principals/users/testuser ' , 'sub123 ' );
526526 }
527527
528+ public function testFoldedDtstampChangeDoesNotTriggerUpdate (): void {
529+ $ refreshWebcalService = new RefreshWebcalService (
530+ $ this ->caldavBackend ,
531+ $ this ->logger ,
532+ $ this ->connection ,
533+ $ this ->timeFactory ,
534+ $ this ->importService
535+ );
536+
537+ $ this ->caldavBackend ->expects (self ::once ())
538+ ->method ('getSubscriptionsForUser ' )
539+ ->with ('principals/users/testuser ' )
540+ ->willReturn ([
541+ [
542+ 'id ' => '42 ' ,
543+ 'uri ' => 'sub123 ' ,
544+ RefreshWebcalService::STRIP_TODOS => '1 ' ,
545+ RefreshWebcalService::STRIP_ALARMS => '1 ' ,
546+ RefreshWebcalService::STRIP_ATTACHMENTS => '1 ' ,
547+ 'source ' => 'webcal://foo.bar/bla2 ' ,
548+ 'lastmodified ' => 0 ,
549+ ],
550+ ]);
551+
552+ // DTSTAMP with TZID parameter exceeds 75 bytes, triggering RFC 5545 content line folding
553+ $ body = "BEGIN:VCALENDAR \r\nVERSION:2.0 \r\nPRODID:-//Test//Test//EN \r\nBEGIN:VEVENT \r\nUID:folded-dtstamp-test \r\nDTSTAMP;X-VOBJ-ORIGINAL-TZID=America/Argentina/Buenos_Aires:20260209T120000Z \r\nDTSTART:20260301T100000Z \r\nSUMMARY:Test Event \r\nEND:VEVENT \r\nEND:VCALENDAR \r\n" ;
554+ $ stream = $ this ->createStreamFromString ($ body );
555+
556+ $ this ->connection ->expects (self ::once ())
557+ ->method ('queryWebcalFeed ' )
558+ ->willReturn (['data ' => $ stream , 'format ' => 'ical ' ]);
559+
560+ // Compute etag from the serialized output (which will be folded) minus DTSTAMP
561+ $ vCalForEtag = VObject \Reader::read ($ body );
562+ $ serialized = $ vCalForEtag ->serialize ();
563+ $ existingEtag = md5 (preg_replace ('/^DTSTAMP[;:].*\r?\n([ \t].*\r?\n)*/m ' , '' , $ serialized ));
564+
565+ $ this ->caldavBackend ->expects (self ::once ())
566+ ->method ('getLimitedCalendarObjects ' )
567+ ->willReturn ([
568+ 'folded-dtstamp-test ' => [
569+ 'id ' => 1 ,
570+ 'uid ' => 'folded-dtstamp-test ' ,
571+ 'etag ' => $ existingEtag ,
572+ 'uri ' => 'folded-dtstamp-test.ics ' ,
573+ ],
574+ ]);
575+
576+ $ vCalendar = VObject \Reader::read ($ body );
577+ $ generator = function () use ($ vCalendar ) {
578+ yield $ vCalendar ;
579+ };
580+
581+ $ this ->importService ->expects (self ::once ())
582+ ->method ('importText ' )
583+ ->willReturn ($ generator ());
584+
585+ // Folded DTSTAMP change must NOT trigger an update
586+ $ this ->caldavBackend ->expects (self ::never ())
587+ ->method ('updateCalendarObject ' );
588+
589+ $ this ->caldavBackend ->expects (self ::never ())
590+ ->method ('createCalendarObject ' );
591+
592+ $ refreshWebcalService ->refreshSubscription ('principals/users/testuser ' , 'sub123 ' );
593+ }
594+
528595 public static function identicalDataProvider (): array {
529596 $ 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" ;
530597 // Etag is computed from DTSTAMP-stripped serialization
531- $ etag = md5 (preg_replace ('/^DTSTAMP: .*\r?\n/m ' , '' , $ icalBody ));
598+ $ etag = md5 (preg_replace ('/^DTSTAMP[;:] .*\r?\n([ \t].*\r?\n)* /m ' , '' , $ icalBody ));
532599
533600 return [
534601 [
0 commit comments