fix(caldav): use /dav/projects/ as home to make iOS/MacOS reminders work (#2417)
Resolves issue #475 by modifying CalDAV discovery so Apple Reminders can use /dav/projects/ as the home set without exposing that synthetic path as a real task list, preserving the existing principal-based flow. This is because Apple Reminders defaults back to the /dav/projects/ URL, rather than accepting the /dav/principals/username/ URL specified in Vikunja. Resolves #475
This commit is contained in:
parent
9c3fa8e91b
commit
e7f1e99878
|
|
@ -82,7 +82,7 @@ func ProjectHandler(c *echo.Context) error {
|
|||
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
||||
caldav.SetupStorage(storage)
|
||||
caldav.SetupUser("dav/projects")
|
||||
caldav.SetupUser(strings.TrimPrefix(ProjectHomeSetPath, "/"))
|
||||
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
|
||||
response := caldav.HandleRequest(c.Request())
|
||||
response.Write(c.Response())
|
||||
|
|
@ -137,7 +137,7 @@ func PrincipalHandler(c *echo.Context) error {
|
|||
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
||||
caldav.SetupStorage(storage)
|
||||
caldav.SetupUser("dav/principals/" + u.Username)
|
||||
caldav.SetupUser(strings.TrimPrefix(principalPathForUser(u.Username), "/"))
|
||||
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
|
||||
|
||||
response := caldav.HandleRequest(c.Request())
|
||||
|
|
@ -166,7 +166,7 @@ func EntryHandler(c *echo.Context) error {
|
|||
log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
||||
caldav.SetupStorage(storage)
|
||||
caldav.SetupUser("dav/principals/" + u.Username)
|
||||
caldav.SetupUser(strings.TrimPrefix(principalPathForUser(u.Username), "/"))
|
||||
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
|
||||
|
||||
response := caldav.HandleRequest(c.Request())
|
||||
|
|
|
|||
|
|
@ -35,10 +35,16 @@ import (
|
|||
)
|
||||
|
||||
// DavBasePath is the base url path
|
||||
const DavBasePath = `/dav/`
|
||||
const DavBasePath = `/dav`
|
||||
|
||||
// ProjectBasePath is the base path for all projects resources
|
||||
const ProjectBasePath = DavBasePath + `projects`
|
||||
const ProjectBasePath = DavBasePath + `/projects`
|
||||
|
||||
// PrincipalBasePath is the base path for all principal resources
|
||||
const PrincipalBasePath = DavBasePath + `/principals`
|
||||
|
||||
// ProjectHomeSetPath is the CalDAV home-set path Apple clients use after discovery.
|
||||
const ProjectHomeSetPath = ProjectBasePath + `/`
|
||||
|
||||
// VikunjaCaldavProjectStorage represents a project storage
|
||||
type VikunjaCaldavProjectStorage struct {
|
||||
|
|
@ -68,7 +74,7 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
|
|||
// and not /dav/projects. I'm not sure if thats a bug in the client or in caldav-go.
|
||||
|
||||
if vcls.isEntry {
|
||||
r := data.NewResource(rpath, &VikunjaProjectResourceAdapter{
|
||||
r := data.NewResource(withTrailingSlash(rpath), &VikunjaProjectResourceAdapter{
|
||||
isPrincipal: true,
|
||||
isCollection: true,
|
||||
})
|
||||
|
|
@ -77,7 +83,7 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
|
|||
|
||||
// If the request wants the principal url, we'll return that and nothing else
|
||||
if vcls.isPrincipal {
|
||||
r := data.NewResource(DavBasePath+`/projects/`, &VikunjaProjectResourceAdapter{
|
||||
r := data.NewResource(ProjectHomeSetPath, &VikunjaProjectResourceAdapter{
|
||||
isPrincipal: true,
|
||||
isCollection: true,
|
||||
})
|
||||
|
|
@ -128,6 +134,14 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
|
|||
}
|
||||
projects := theprojects.([]*models.Project)
|
||||
|
||||
if !withChildren {
|
||||
r := data.NewResource(withTrailingSlash(rpath), &VikunjaProjectResourceAdapter{
|
||||
isPrincipal: true,
|
||||
isCollection: true,
|
||||
})
|
||||
return []data.Resource{r}, nil
|
||||
}
|
||||
|
||||
var resources []data.Resource
|
||||
for _, l := range projects {
|
||||
rr := VikunjaProjectResourceAdapter{
|
||||
|
|
@ -144,6 +158,20 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren
|
|||
return resources, nil
|
||||
}
|
||||
|
||||
func withTrailingSlash(path string) string {
|
||||
if path == "" {
|
||||
return ProjectHomeSetPath
|
||||
}
|
||||
if strings.HasSuffix(path, "/") {
|
||||
return path
|
||||
}
|
||||
return path + "/"
|
||||
}
|
||||
|
||||
func principalPathForUser(username string) string {
|
||||
return withTrailingSlash(PrincipalBasePath + `/` + username)
|
||||
}
|
||||
|
||||
// GetResourcesByList fetches a list of resources from a slice of paths
|
||||
func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) (resources []data.Resource, err error) {
|
||||
|
||||
|
|
|
|||
|
|
@ -806,6 +806,7 @@ func registerCalDavRoutes(c *echo.Group) {
|
|||
// THIS is the entry point for caldav clients, otherwise projects will show up double
|
||||
c.Any("", caldav.EntryHandler)
|
||||
c.Any("/", caldav.EntryHandler)
|
||||
c.Any("/principals/*", caldav.PrincipalHandler)
|
||||
c.Any("/principals/*/", caldav.PrincipalHandler)
|
||||
c.Any("/projects", caldav.ProjectHandler)
|
||||
c.Any("/projects/", caldav.ProjectHandler)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"code.vikunja.io/api/pkg/routes/caldav"
|
||||
|
||||
ics "github.com/arran4/golang-ical"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -85,6 +86,96 @@ END:VCALENDAR`
|
|||
})
|
||||
}
|
||||
|
||||
func TestCaldavDiscovery(t *testing.T) {
|
||||
t.Run("Project home set depth 1 includes child projects but not itself", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<A:propfind xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
|
||||
<A:prop>
|
||||
<A:current-user-principal />
|
||||
<B:calendar-home-set />
|
||||
<A:resourcetype />
|
||||
</A:prop>
|
||||
</A:propfind>`
|
||||
|
||||
c, rec := createRequest(e, "PROPFIND", propfindBody, nil, nil)
|
||||
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextXML)
|
||||
c.Request().Header.Set("Depth", "1")
|
||||
c.Request().URL.Path = caldav.ProjectBasePath + "/"
|
||||
c.Request().RequestURI = caldav.ProjectBasePath + "/"
|
||||
|
||||
result, _ := caldav.BasicAuth(c, testuser15.Username, "12345678")
|
||||
require.True(t, result)
|
||||
|
||||
err := caldav.ProjectHandler(c)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 207, rec.Result().StatusCode)
|
||||
|
||||
responseBody := rec.Body.String()
|
||||
assert.Contains(t, responseBody, "/dav/projects/36")
|
||||
assert.NotContains(t, responseBody, "<d:href>/dav/projects/</d:href>")
|
||||
assert.NotContains(t, responseBody, "<D:href>/dav/projects/</D:href>")
|
||||
})
|
||||
|
||||
t.Run("Project home set depth 0 returns the home set itself", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<A:propfind xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
|
||||
<A:prop>
|
||||
<A:current-user-principal />
|
||||
<B:calendar-home-set />
|
||||
<A:resourcetype />
|
||||
</A:prop>
|
||||
</A:propfind>`
|
||||
|
||||
c, rec := createRequest(e, "PROPFIND", propfindBody, nil, nil)
|
||||
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextXML)
|
||||
c.Request().Header.Set("Depth", "0")
|
||||
c.Request().URL.Path = caldav.ProjectBasePath + "/"
|
||||
c.Request().RequestURI = caldav.ProjectBasePath + "/"
|
||||
|
||||
result, _ := caldav.BasicAuth(c, testuser15.Username, "12345678")
|
||||
require.True(t, result)
|
||||
|
||||
err := caldav.ProjectHandler(c)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 207, rec.Result().StatusCode)
|
||||
|
||||
responseBody := rec.Body.String()
|
||||
assert.Contains(t, responseBody, "/dav/projects/")
|
||||
})
|
||||
|
||||
t.Run("Principal discovery points to normalized project home set path", func(t *testing.T) {
|
||||
e, _ := setupTestEnv()
|
||||
|
||||
propfindBody := `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<A:propfind xmlns:A="DAV:" xmlns:B="urn:ietf:params:xml:ns:caldav">
|
||||
<A:prop>
|
||||
<A:current-user-principal />
|
||||
<B:calendar-home-set />
|
||||
</A:prop>
|
||||
</A:propfind>`
|
||||
|
||||
c, rec := createRequest(e, "PROPFIND", propfindBody, nil, nil)
|
||||
c.Request().Header.Set(echo.HeaderContentType, echo.MIMETextXML)
|
||||
c.Request().URL.Path = caldav.PrincipalBasePath + "/user15/"
|
||||
c.Request().RequestURI = caldav.PrincipalBasePath + "/user15/"
|
||||
|
||||
result, _ := caldav.BasicAuth(c, testuser15.Username, "12345678")
|
||||
require.True(t, result)
|
||||
|
||||
err := caldav.PrincipalHandler(c)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 207, rec.Result().StatusCode)
|
||||
|
||||
responseBody := rec.Body.String()
|
||||
assert.Contains(t, responseBody, "<D:href>/dav/projects</D:href>")
|
||||
assert.NotContains(t, responseBody, "/dav//projects/")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCaldavSubtasks(t *testing.T) {
|
||||
const vtodoHeader = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue