Conversation
7df84bc to
4c5b46f
Compare
|
As the |
4c5b46f to
36fdb9a
Compare
|
Thanks for the review @tcitworld — you're absolutely right. DTSTAMP is a required property per RFC 5545 and shouldn't be removed from the stored data. Apologies for being too quick with the initial approach. I've reworked the fix to be much more minimal: instead of stripping DTSTAMP from the VObject before serialization, it now only strips the DTSTAMP lines from a copy of the serialized string used for the etag/md5 computation. The stored calendar data is completely unchanged — DTSTAMP is preserved. The diff is now just one line replaced in -$etag = md5($sObject);
+$sObjectForEtag = preg_replace('/^DTSTAMP:.*\r?\n/m', '', $sObject);
+$etag = md5($sObjectForEtag); |
36fdb9a to
6d10452
Compare
9d58868 to
6487bb3
Compare
|
Good point @tcitworld — updated the regex to handle both parameters ( '/^DTSTAMP[;:].*\r?\n([ \t].*\r?\n)*/m'This matches Also added a new test |
acbac30 to
496095a
Compare
kesselb
left a comment
There was a problem hiding this comment.
Thanks a lot for your pr 👍
| // 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([ \t].*\r?\n)*/m', '', $sObject); |
There was a problem hiding this comment.
That seems a bit odd. I think we should rather use the data from $vObject to calculate an etag instead of altering the serialized object with regex.
I'm also seeing the changing dtstamp with my google test account:
Request 1
BEGIN:VEVENT
DTSTART:20250905T163000Z
DTEND:20250905T173000Z
DTSTAMP:20260210T144443Z
UID:1234-1234-1234-1234
CREATED:20250904T204927Z
LAST-MODIFIED:20250904T204927Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Testing 3
TRANSP:OPAQUE
END:VEVENT
Request 2 (a few seconds later)
BEGIN:VEVENT
DTSTART:20250905T163000Z
DTEND:20250905T173000Z
DTSTAMP:20260210T144446Z
UID:1234-1234-1234-1234
CREATED:20250904T204927Z
LAST-MODIFIED:20250904T204927Z
SEQUENCE:0
STATUS:CONFIRMED
SUMMARY:Testing 3
TRANSP:OPAQUE
END:VEVENT
What do you think to calculate the etag from "UID + Last-Modified + Sequence + DTSTART + DTSEND" @tcitworld @SebastianKrupinski?
There was a problem hiding this comment.
I think that approach would work... as long as the source updates those fields properly... which is a big IF...
My idea would be to just serialize the object twice... once with the DTSTAMP once with out... But you idea is more efficient
There was a problem hiding this comment.
Just let me know if you decide you want to build the etag differently. I will update the PR to match what you think is best. I only noticed because the table was growing huge with updates for some of my calendars.
153f75a to
6e9ffc6
Compare
|
Hello there, We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 Thank you for contributing to Nextcloud and we hope to hear from you soon! (If you believe you should not receive this message, you can add yourself to the blocklist.) |
6e9ffc6 to
62b8b93
Compare
512a81a to
11498e0
Compare
| $sObject = $vObject->serialize(); | ||
| $uid = $vBase->UID->getValue(); | ||
| $etag = md5($sObject); | ||
| $etag = md5($vBase->UID?->getValue() . $vBase->SEQUENCE?->getValue() . $vBase->{'LAST-MODIFIED'}?->getValue()); |
There was a problem hiding this comment.
I'm not sure this is a very good solution. Of the 9 calendar subscriptions I have, only 3 contains both last-modified and squence. And for some of the others, which only has sequence and not last-modified, sequence is always 0.
So this means that at least for most of my calendars, changing an event is not picked up as a new etag. I am not sure the approach of only generating it from specific fields is the best way to go. It will miss all changed events where sequence and last-modified is missing. The only problematic field I have seen is dtstamp - which many sources update on every fetch, so i still believe the approach of serializing on the entire object except that field is better.
There was a problem hiding this comment.
Thanks for the heads up 🙏
Mind to replace the current version with something like
$sObject = $vObject->serialize();
$uid = $vBase->UID->getValue();
$etagObject = clone $vObject;
foreach ($etagObject->getComponents() as $component) {
unset($component->DTSTAMP);
}
$etag = md5($etagObject->serialize());4ba0be4 to
7f2ef6c
Compare
|
The etag written back by createCalendarObject and updateCalendarObject is generated at server/apps/dav/lib/CalDAV/CalDavBackend.php Line 3409 in 356ee15 The comparison in RefreshWebCalService is always false and hence will always trigger a write, because the calculate etag cannot match the stored etag. Fix: Add a CalendarObjectEtagService and use it in CalDavBackend and WebCalService. |
7f2ef6c to
c347905
Compare
Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: Olen <regopa@gmail.com> Signed-off-by: SebastianKrupinski <krupinskis05@gmail.com> Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
c347905 to
5060c32
Compare
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Legacy ETags need a migration or backward-compatible reconciliation path to avoid mass false updates after deployment.
Review effort: Lite
Findings: None
What changed in this PR
Updates webcal subscription ETag calculation to ignore volatile DTSTAMP values while preserving stored calendar data.
Changes:
- Adds a shared DTSTAMP-independent ETag helper.
- Applies it during refresh and subscription persistence.
- Adds unit and backend coverage plus Composer autoload entries.
Review findings include two moderate comments requesting backward-compatible reconciliation of legacy ETags, plus nits concerning the issue reference and folded DTSTAMP test fixture.
| File | Summary |
|---|---|
apps/dav/tests/unit/CalDAV/WebcalCaching/RefreshWebcalServiceTest.php |
Tests DTSTAMP and content-change behavior. |
apps/dav/tests/unit/CalDAV/CalendarObjectEtagHelperTest.php |
Tests helper behavior and immutability. |
apps/dav/tests/unit/CalDAV/CalDavBackendTest.php |
Verifies subscription ETag persistence. |
apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php |
Uses DTSTAMP-independent ETags during refresh. |
apps/dav/lib/CalDAV/CalendarObjectEtagHelper.php |
Provides normalized ETag computation. |
apps/dav/lib/CalDAV/CalDavBackend.php |
Applies the new ETag logic to subscriptions. |
apps/dav/composer/composer/autoload_static.php |
Registers the helper class. |
apps/dav/composer/composer/autoload_classmap.php |
Registers the helper class. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Summary
DTSTAMPfrom etag computation inRefreshWebcalServiceto prevent false change detectionProblem
Many iCal providers (Google Calendar, Outlook 365, itslearning) set
DTSTAMPto the current UTC time on every feed request per RFC 5545. Since Nextcloud computes the etag asmd5(serialized_data)and DTSTAMP changes every time, every event appears modified on every refresh — even if nothing actually changed.For a Google Calendar subscription with 2,621 events refreshed every ~15 minutes, this generates ~189K false change rows per day in
oc_calendarchanges.Reported at: #51120 (comment)
Approach
Instead of stripping DTSTAMP from the VObject (which would remove a required property from stored data), the fix strips DTSTAMP lines from a copy of the serialized string used only for etag computation:
The stored calendar data (
$sObject) is completely unchanged.Test plan
testDtstampChangeDoesNotTriggerUpdateverifies a DTSTAMP-only change does not triggerupdateCalendarObjectidenticalDataProvideretag computation to match the new DTSTAMP-stripped hashingRefreshWebcalServiceTestunit testsFixes: #51120
🤖 Generated with Claude Code