Disambiguate shared calendars with a name colliding with the owner's

If a calendar shared with a user has the same name as one of the user's
own calendars (or as another share from a different owner), DAVx5 would
show two calendars with an identical, indistinguishable title. The
shared calendar's CalDAV display name (Calendar.Name, exposed via
displayname/DAVx5's calendar list) now gets the owning user's username
appended in parentheses in that case, e.g. "personal (alice)" — the
underlying calendar path/URL is unaffected, and a user's own calendars
are never renamed (only shared ones can collide with something else).

internal/caldav/backend.go: calendarMeta split into calendarMeta (own
calendars, unchanged name) and calendarMetaNamed (explicit display name);
new sharedDisplayName(requester, owner, realName) checks requester's own
calendars and other shares for a collision. Wired into both
ListCalendars and GetCalendar (the latter used by direct
PROPFIND/REPORT against a shared calendar's own URL).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-20 23:20:17 +02:00
co-authored by Copilot
parent 213b22f821
commit b58a6ba4ea
2 changed files with 103 additions and 3 deletions
+49 -3
View File
@@ -136,7 +136,8 @@ func (b *Backend) ListCalendars(ctx context.Context) ([]caldav.Calendar, error)
continue // owner's calendar no longer exists
}
localName := sharedCalendarName(sh.Owner, sh.CalendarName)
cals = append(cals, b.calendarMeta(sh.Owner, sh.CalendarName, localName))
displayName := b.sharedDisplayName(p.Username, sh.Owner, sh.CalendarName)
cals = append(cals, b.calendarMetaNamed(sh.Owner, sh.CalendarName, localName, displayName))
}
}
@@ -163,7 +164,11 @@ func (b *Backend) GetCalendar(ctx context.Context, calPath string) (*caldav.Cale
if _, err := b.store.GetCollection(owner, "cal-"+realName); err != nil {
return nil, webdav.NewHTTPError(http.StatusNotFound, err)
}
cal := b.calendarMeta(owner, realName, localName)
displayName := realName
if owner != requester {
displayName = b.sharedDisplayName(requester, owner, realName)
}
cal := b.calendarMetaNamed(owner, realName, localName, displayName)
return &cal, nil
}
@@ -369,6 +374,14 @@ func (b *Backend) resolveCalendar(requester, localName string, requireWrite bool
}
func (b *Backend) calendarMeta(owner, realName, localName string) caldav.Calendar {
return b.calendarMetaNamed(owner, realName, localName, realName)
}
// calendarMetaNamed is calendarMeta with an explicit displayName, used for
// a calendar shared with the requester whose name collides with one of
// their own calendars (see sharedDisplayName) — the Description still
// always names the real calendar, only the client-visible Name changes.
func (b *Backend) calendarMetaNamed(owner, realName, localName, displayName string) caldav.Calendar {
desc := fmt.Sprintf("%s's %s calendar", owner, realName)
return caldav.Calendar{
Path: calHomePath() + localName + "/",
@@ -376,13 +389,46 @@ func (b *Backend) calendarMeta(owner, realName, localName string) caldav.Calenda
// "owner~name" synthetic local name used internally to keep a
// shared calendar's URL unique) — clients like DAVx5 show this as
// the calendar's label, and "alice~work" looked confusing there.
Name: realName,
Name: displayName,
Description: desc,
SupportedComponentSet: []string{"VEVENT", "VTODO", "VJOURNAL"},
MaxResourceSize: 10 * 1024 * 1024, // 10 MiB
}
}
// sharedDisplayName returns realName, suffixed with the owning user's
// name in parentheses (e.g. "Work (bob)") if requester has another
// calendar with the exact same display name — either one of their own,
// or another share from a different owner — so DAVx5 (and other clients)
// don't show two calendars with an identical, indistinguishable title.
// Only called for calendars shared with requester; requester's own
// calendars never need disambiguating against themselves.
func (b *Backend) sharedDisplayName(requester, owner, realName string) string {
own, err := b.dbase.ListCalendars(requester)
if err != nil {
b.logger.Warn("listing own calendars for disambiguation", "error", err)
}
for _, c := range own {
if c.Name == realName {
return fmt.Sprintf("%s (%s)", realName, owner)
}
}
shares, err := b.dbase.CalendarsSharedWith(requester)
if err != nil {
b.logger.Warn("listing shared calendars for disambiguation", "error", err)
}
for _, sh := range shares {
if sh.Owner == owner && sh.CalendarName == realName {
continue // the calendar itself, not a collision
}
if sh.CalendarName == realName {
return fmt.Sprintf("%s (%s)", realName, owner)
}
}
return realName
}
func (b *Backend) decodeObject(objPath string, data []byte) (*caldav.CalendarObject, error) {
// Some clients (Outlook/Exchange, some Thunderbird/Lightning setups)
// write Windows timezone names into TZID instead of IANA ones, which