Better caldav support (#73)
This commit is contained in:
parent
de24fcc2f8
commit
7107d030fc
91 changed files with 7060 additions and 323 deletions
|
@ -26,6 +26,8 @@ steps:
|
|||
- name: build
|
||||
image: vikunja/golang-build:latest
|
||||
pull: true
|
||||
environment:
|
||||
GOFLAGS: '-mod=vendor'
|
||||
commands:
|
||||
- make lint
|
||||
- make fmt-check
|
||||
|
|
|
@ -48,10 +48,10 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
|||
* [x] Pagination
|
||||
* Sollte in der Config definierbar sein, wie viel pro Seite angezeigt werden soll, die CRUD-Methoden übergeben dann ein "gibt mir die Seite sowieso" an die CRUDable-Funktionenen, die müssen das dann Auswerten. Geht leider nicht anders, wenn man erst 2342352 Einträge hohlt und die dann nachträglich auf 200 begrenzt ist das ne massive Ressourcenverschwendung.
|
||||
* [x] Testen, ob man über die Routen methode von echo irgendwie ein swagger spec generieren könnte -> Andere Swagger library
|
||||
* [ ] CalDAV
|
||||
* [x] CalDAV
|
||||
* [x] Basics
|
||||
* [x] Reminders
|
||||
* [ ] Discovery, stichwort PROPFIND
|
||||
* [x] Discovery, stichwort PROPFIND
|
||||
* [x] Wir brauchen noch ne gute idee, wie man die listen kriegt, auf die man nur so Zugriff hat (ohne namespace)
|
||||
* Dazu am Besten nen pseudonamespace anlegen (id -1 oder so), der hat das dann alles
|
||||
* [x] Testing mit locust: https://locust.io/
|
||||
|
@ -171,7 +171,6 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
|||
* [ ] Some kind of milestones for tasks
|
||||
* [ ] Create tasks from a text/markdown file (probably frontend only)
|
||||
* [ ] Label-view: Get a bunch of tasks by label
|
||||
* [ ] Better caldav support (VTODO)
|
||||
* [ ] Debian package should have a service file
|
||||
* [ ] Downloads should be served via nginx (with theme?), minio should only be used for pushing artifacts.
|
||||
* [ ] User struct should have a field for the avatar url (-> gravatar md5 calculated by the backend)
|
||||
|
@ -179,6 +178,34 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
|||
-> Check if there's a way to do that efficently. Maybe only implementing it in the web handler.
|
||||
* [ ] List stats to see how many tasks are done, how many are there in total, how many people have acces to a list etc
|
||||
* [ ] Colors for tasks
|
||||
* [ ] Better caldav support
|
||||
* [x] VTODO
|
||||
* [x] Fix organizer prop
|
||||
* [x] Depricate the events thing for now
|
||||
* [x] PROPFIND/OPTIONS : caldav discovery
|
||||
* [x] Create new tasks
|
||||
* [x] Save uid from the client
|
||||
* [x] Update tasks
|
||||
* [x] Marking as done
|
||||
* [x] Fix OPTIONS Requests to the rest of the api being broken
|
||||
* [x] Parse all props defined in rfc5545
|
||||
* [x] COMPLETED -> Need to actually save the time the task was completed
|
||||
* [x] Whenever a task ist created/updated, update the `updated` prop on the list so the etag changes and clients get notified
|
||||
* [x] Fix not all tasks being displayed (My guess: Something about that f*cking etag)
|
||||
* [x] Delete tasks
|
||||
* [x] Last modified
|
||||
* [x] Content Size
|
||||
* [x] Modify the caldav lib as proposed in the pr
|
||||
* [x] Improve login performance, each request taking > 1.5 sec is just too much, maybe just use the default value for hash iterations in the login/register function
|
||||
* [x] Only show priority when we have one
|
||||
* [x] Show a proper calendar title
|
||||
* [x] Fix home principal propfind stuff
|
||||
* [x] Docs
|
||||
* [x] Setting to disable caldav completely
|
||||
* [ ] Make it work with the app
|
||||
* [ ] Cleanup the whole mess I made with the handlers and storage providers etc -> Probably a good idea to create a seperate storage provider etc for lists and tasks
|
||||
* [ ] Tests
|
||||
* [ ] Check if only needed things are queried from the db when accessing dav (for ex. no need to get all tasks when we act
|
||||
|
||||
### Refactor
|
||||
|
||||
|
@ -217,6 +244,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
|||
|
||||
### Later
|
||||
|
||||
* [ ] Backgrounds for lists -> needs uploading and storing and so on
|
||||
* [ ] Plugins
|
||||
* [ ] Rename Namespaces?
|
||||
* [ ] Namespaces to collections and n-n (one list can be in multiple collections)?
|
||||
|
@ -258,6 +286,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
|||
* [ ] Nozbe
|
||||
* [ ] Lanes
|
||||
* [ ] Nirvana
|
||||
* [ ] Any.do
|
||||
* [ ] Good ol' Caldav (Tasks)
|
||||
* [ ] More auth providers
|
||||
* [ ] LDAP/AD
|
||||
|
|
|
@ -16,6 +16,8 @@ service:
|
|||
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
|
||||
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||
enablemetrics: false
|
||||
# Enable the caldav endpoint, see the docs for more details
|
||||
enablecaldav: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql and sqlite.
|
||||
|
|
|
@ -59,6 +59,8 @@ service:
|
|||
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
|
||||
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||
enablemetrics: false
|
||||
# Enable the caldav endpoint, see the docs for more details
|
||||
enablecaldav: true
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql and sqlite.
|
||||
|
|
140
docs/content/doc/usage/caldav.md
Normal file
140
docs/content/doc/usage/caldav.md
Normal file
|
@ -0,0 +1,140 @@
|
|||
---
|
||||
date: "2019-05-12:00:00+01:00"
|
||||
title: "Caldav"
|
||||
draft: false
|
||||
type: "doc"
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "usage"
|
||||
---
|
||||
|
||||
# Caldav
|
||||
|
||||
> **Warning:** The caldav integration is in an early alpha stage and has bugs.
|
||||
> It works well with some clients while having issues with others.
|
||||
> If you encounter issues, please [report them](https://code.vikunja.io/api/issues/new?body=[caldav])
|
||||
|
||||
Vikunja supports managing tasks via the [caldav VTODO](https://tools.ietf.org/html/rfc5545#section-3.6.2) extension.
|
||||
|
||||
## URLs
|
||||
|
||||
All urls are located under the `/dav` subspace.
|
||||
|
||||
Urls are:
|
||||
|
||||
* `/principals/<username>/`: Returns urls for list discovery. *Use this url to initially make connections to new clients.*
|
||||
* `/lists/`: Used to manage lists
|
||||
* `/lists/<List ID>/`: Used to manage a single list
|
||||
* `/lists/<List ID>/<Task UID>`: Used to manage a task on a list
|
||||
|
||||
## Supported properties
|
||||
|
||||
Vikunja currently supports the following properties:
|
||||
|
||||
* `UID`
|
||||
* `SUMMARY`
|
||||
* `DESCRIPTION`
|
||||
* `PRIORITY`
|
||||
* `COMPLETED`
|
||||
* `DUE`
|
||||
* `DTSTART`
|
||||
* `DURATION`
|
||||
* `ORGANIZER`
|
||||
* `RELATED-TO`
|
||||
* `CREATED`
|
||||
* `DTSTAMP`
|
||||
* `LAST-MODIFIED`
|
||||
|
||||
Vikunja **currently does not** support these properties:
|
||||
|
||||
* `ATTACH`
|
||||
* `CATEGORIES`
|
||||
* `CLASS`
|
||||
* `COMMENT`
|
||||
* `GEO`
|
||||
* `LOCATION`
|
||||
* `PERCENT-COMPLETE`
|
||||
* `RESOURCES`
|
||||
* `STATUS`
|
||||
* `CONTACT`
|
||||
* `RECURRENCE-ID`
|
||||
* `URL`
|
||||
* Recurrence
|
||||
* `SEQUENCE`
|
||||
|
||||
## Tested Clients
|
||||
|
||||
#### Working
|
||||
|
||||
* [Evolution](https://wiki.gnome.org/Apps/Evolution/)
|
||||
|
||||
#### Not working
|
||||
|
||||
* [Tasks (Android)](https://tasks.org/)
|
||||
|
||||
## Dev logs
|
||||
|
||||
The whole thing is not optimized at all and probably pretty inefficent.
|
||||
|
||||
Request body and headers are logged if the debug output is enabled.
|
||||
|
||||
```
|
||||
Creating a new task:
|
||||
PUT /dav/lists/1/cd4dd0e1b3c19cc9d787829b6e08be536e3df3a4.ics
|
||||
|
||||
Body:
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTODO
|
||||
UID:cd4dd0e1b3c19cc9d787829b6e08be536e3df3a4
|
||||
DTSTAMP:20190508T134538Z
|
||||
SUMMARY:test2000
|
||||
PRIORITY:0
|
||||
CLASS:PUBLIC
|
||||
CREATED:20190508T134710Z
|
||||
LAST-MODIFIED:20190508T134710Z
|
||||
END:VTODO
|
||||
END:VCALENDAR
|
||||
|
||||
|
||||
Marking a task as done:
|
||||
|
||||
BEGIN:VCALENDAR
|
||||
CALSCALE:GREGORIAN
|
||||
PRODID:-//Ximian//NONSGML Evolution Calendar//EN
|
||||
VERSION:2.0
|
||||
BEGIN:VTODO
|
||||
UID:3ada92f28b4ceda38562ebf047c6ff05400d4c572352a
|
||||
DTSTAMP:20190511T183631
|
||||
DTSTART:19700101T000000
|
||||
DTEND:19700101T000000
|
||||
SUMMARY:sdgs
|
||||
ORGANIZER;CN=:user
|
||||
CREATED:20190511T183631
|
||||
PRIORITY:0
|
||||
LAST-MODIFIED:20190512T193428Z
|
||||
COMPLETED:20190512T193428Z
|
||||
PERCENT-COMPLETE:100
|
||||
STATUS:COMPLETED
|
||||
END:VTODO
|
||||
END:VCALENDAR
|
||||
|
||||
Requests from the app:::
|
||||
|
||||
[CALDAV] Request Body: <?xml version="1.0" encoding="UTF-8" ?><propfind xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav"><prop><current-user-principal /></prop></propfind>
|
||||
[CALDAV] GetResources: rpath: /dav/
|
||||
2019-05-18T23:25:49.971140654+02:00: WEB ▶ 192.168.1.134 PROPFIND 207 /dav/ 1.021705664s - okhttp/3.12.2
|
||||
|
||||
[CALDAV] Request Body: <?xml version="1.0" encoding="UTF-8" ?><propfind xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav"><prop><CAL:calendar-home-set /></prop></propfind>
|
||||
[CALDAV] GetResources: rpath: /dav/
|
||||
2019-05-18T23:25:52.166996113+02:00: WEB ▶ 192.168.1.134 PROPFIND 207 /dav/ 1.042834467s - okhttp/3.12.2
|
||||
|
||||
And then it just stops.
|
||||
... and complains about not being able to find the home set
|
||||
... without even requesting it...
|
||||
|
||||
|
||||
```
|
8
go.mod
8
go.mod
|
@ -21,6 +21,7 @@ require (
|
|||
code.vikunja.io/web v0.0.0-20190507193736-edb39812af9c
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||
github.com/beevik/etree v1.1.0 // indirect
|
||||
github.com/client9/misspell v0.3.4
|
||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
|
@ -43,6 +44,7 @@ require (
|
|||
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb
|
||||
github.com/labstack/echo/v4 v4.1.5
|
||||
github.com/labstack/gommon v0.2.8
|
||||
github.com/laurent22/ical-go v0.1.0
|
||||
github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 // indirect
|
||||
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.4 // indirect
|
||||
|
@ -53,6 +55,7 @@ require (
|
|||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||
github.com/prometheus/client_golang v0.9.2
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible
|
||||
github.com/spf13/afero v1.2.2 // indirect
|
||||
github.com/spf13/cobra v0.0.3
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
|
@ -71,3 +74,8 @@ require (
|
|||
src.techknowlogick.com/xgo v0.0.0-20190507142556-a5b29ecb0ff4
|
||||
src.techknowlogick.com/xormigrate v0.0.0-20190321151057-24497c23c09c
|
||||
)
|
||||
|
||||
//replace (
|
||||
// github.com/labstack/echo/v4 => ../../github.com/labstack/echo // Branch: feature/report-method, PR https://github.com/labstack/echo/pull/1332
|
||||
// github.com/samedi/caldav-go => ../../github.com/samedi/caldav-go // Branch: feature/dynamic-supported-components, PR: https://github.com/samedi/caldav-go/pull/6
|
||||
//)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -16,6 +16,9 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
|
|||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beevik/etree v0.0.0-20171015221209-af219c0c7ea1/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
|
@ -118,6 +121,9 @@ github.com/labstack/echo/v4 v4.1.5 h1:RztCXCvfMljychg0G/IzW5T7hL6ADqqwREwcX279Q1
|
|||
github.com/labstack/echo/v4 v4.1.5/go.mod h1:3LbYC6VkwmUnmLPZ8WFdHdQHG77e9GQbjyhWdb1QvC4=
|
||||
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
||||
github.com/laurent22/ical-go v0.0.0-20170824131750-e4fec3492969/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/laurent22/ical-go v0.1.0 h1:4vZHBD3/+ne+IN+c3B+v6d9Ff8+70pzTjCWsjfDRvL0=
|
||||
github.com/laurent22/ical-go v0.1.0/go.mod h1:4LATl0uhhtytR6p9n1AlktDyIz4u2iUnWEdI3L/hXiw=
|
||||
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
|
@ -175,6 +181,8 @@ github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jO
|
|||
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
|
||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible h1:OrYCCUPvQTyGybtf64G232NPkWuQRl5hc2CC6rQNT/U=
|
||||
github.com/samedi/caldav-go v3.0.0+incompatible/go.mod h1:zY411fqgoxSEcCL75WxwchM+8p4n/MnRUPfVusQke3Q=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
|
|
|
@ -17,11 +17,16 @@
|
|||
package caldav
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DateFormat ist the caldav date format
|
||||
const DateFormat = `20060102T150405`
|
||||
|
||||
// Event holds a single caldav event
|
||||
type Event struct {
|
||||
Summary string
|
||||
|
@ -34,6 +39,29 @@ type Event struct {
|
|||
EndUnix int64
|
||||
}
|
||||
|
||||
// Todo holds a single VTODO
|
||||
type Todo struct {
|
||||
// Required
|
||||
TimestampUnix int64
|
||||
UID string
|
||||
|
||||
// Optional
|
||||
Summary string
|
||||
Description string
|
||||
CompletedUnix int64
|
||||
Organizer *models.User
|
||||
Priority int64 // 0-9, 1 is highest
|
||||
RelatedToUID string
|
||||
|
||||
StartUnix int64
|
||||
EndUnix int64
|
||||
DueDateUnix int64
|
||||
Duration time.Duration
|
||||
|
||||
CreatedUnix int64
|
||||
UpdatedUnix int64 // last-mod
|
||||
}
|
||||
|
||||
// Alarm holds infos about an alarm from a caldav event
|
||||
type Alarm struct {
|
||||
TimeUnix int64
|
||||
|
@ -92,10 +120,89 @@ END:VCALENDAR` // Need a line break
|
|||
return
|
||||
}
|
||||
|
||||
// ParseTodos returns a caldav vcalendar string with todos
|
||||
func ParseTodos(config *Config, todos []*Todo) (caldavtodos string) {
|
||||
caldavtodos = `BEGIN:VCALENDAR
|
||||
VERSION:2.0
|
||||
METHOD:PUBLISH
|
||||
X-PUBLISHED-TTL:PT4H
|
||||
X-WR-CALNAME:` + config.Name + `
|
||||
PRODID:-//` + config.ProdID + `//EN`
|
||||
|
||||
for _, t := range todos {
|
||||
if t.UID == "" {
|
||||
t.UID = makeCalDavTimeFromUnixTime(t.TimestampUnix) + utils.Sha256(t.Summary)
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
BEGIN:VTODO
|
||||
UID:` + t.UID + `
|
||||
DTSTAMP:` + makeCalDavTimeFromUnixTime(t.TimestampUnix) + `
|
||||
SUMMARY:` + t.Summary
|
||||
|
||||
if t.StartUnix != 0 {
|
||||
caldavtodos += `
|
||||
DTSTART: ` + makeCalDavTimeFromUnixTime(t.StartUnix)
|
||||
}
|
||||
if t.EndUnix != 0 {
|
||||
caldavtodos += `
|
||||
DTEND: ` + makeCalDavTimeFromUnixTime(t.EndUnix)
|
||||
}
|
||||
if t.Description != "" {
|
||||
caldavtodos += `
|
||||
DESCRIPTION:` + t.Description
|
||||
}
|
||||
if t.CompletedUnix != 0 {
|
||||
caldavtodos += `
|
||||
COMPLETED: ` + makeCalDavTimeFromUnixTime(t.CompletedUnix)
|
||||
}
|
||||
if t.Organizer != nil {
|
||||
caldavtodos += `
|
||||
ORGANIZER;CN=:` + t.Organizer.Username
|
||||
}
|
||||
|
||||
if t.RelatedToUID != "" {
|
||||
caldavtodos += `
|
||||
RELATED-TO:` + t.RelatedToUID
|
||||
}
|
||||
|
||||
if t.DueDateUnix != 0 {
|
||||
caldavtodos += `
|
||||
DUE:` + makeCalDavTimeFromUnixTime(t.DueDateUnix)
|
||||
}
|
||||
|
||||
if t.CreatedUnix != 0 {
|
||||
caldavtodos += `
|
||||
CREATED:` + makeCalDavTimeFromUnixTime(t.CreatedUnix)
|
||||
}
|
||||
|
||||
if t.Duration != 0 {
|
||||
caldavtodos += `
|
||||
DURATION:PT` + fmt.Sprintf("%.6f", t.Duration.Hours()) + `H` + fmt.Sprintf("%.6f", t.Duration.Minutes()) + `M` + fmt.Sprintf("%.6f", t.Duration.Seconds()) + `S`
|
||||
}
|
||||
|
||||
if t.Priority != 0 {
|
||||
caldavtodos += `
|
||||
PRIORITY:` + strconv.Itoa(int(t.Priority))
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
LAST-MODIFIED:` + makeCalDavTimeFromUnixTime(t.UpdatedUnix)
|
||||
|
||||
caldavtodos += `
|
||||
END:VTODO`
|
||||
}
|
||||
|
||||
caldavtodos += `
|
||||
END:VCALENDAR` // Need a line break
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeCalDavTimeFromUnixTime(unixtime int64) (caldavtime string) {
|
||||
tz, _ := time.LoadLocation("UTC")
|
||||
tm := time.Unix(unixtime, 0).In(tz)
|
||||
return tm.Format("20060102T150405")
|
||||
return tm.Format(DateFormat)
|
||||
}
|
||||
|
||||
func calcAlarmDateFromReminder(eventStartUnix, reminderUnix int64) (alarmTime string) {
|
||||
|
|
|
@ -40,6 +40,7 @@ func InitConfig() {
|
|||
viper.SetDefault("service.JWTSecret", random)
|
||||
viper.SetDefault("service.interface", ":3456")
|
||||
viper.SetDefault("service.frontendurl", "")
|
||||
viper.SetDefault("service.enablecaldav", true)
|
||||
|
||||
ex, err := os.Executable()
|
||||
if err != nil {
|
||||
|
|
|
@ -80,33 +80,33 @@ func TestListTask(t *testing.T) {
|
|||
t.Run("by priority", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priority"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"prioritydesc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
|
||||
})
|
||||
t.Run("by priority asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priorityasc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
// should equal duedate desc
|
||||
t.Run("by duedate", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadate"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
})
|
||||
t.Run("by duedate desc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadatedesc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
|
||||
})
|
||||
t.Run("by duedate asc", func(t *testing.T) {
|
||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedateasc"}}, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","created":0,"updated":0}}]`)
|
||||
})
|
||||
t.Run("invalid parameter", func(t *testing.T) {
|
||||
// Invalid parameter should not sort at all
|
||||
|
|
84
pkg/migration/20190511202210.go
Normal file
84
pkg/migration/20190511202210.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"github.com/go-xorm/xorm"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
)
|
||||
|
||||
type listTask20190511202210 struct {
|
||||
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"listtask"`
|
||||
Text string `xorm:"varchar(250) not null" json:"text" valid:"runelength(3|250)" minLength:"3" maxLength:"250"`
|
||||
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
|
||||
Done bool `xorm:"INDEX null" json:"done"`
|
||||
DoneAtUnix int64 `xorm:"INDEX null" json:"doneAt"`
|
||||
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"`
|
||||
RemindersUnix []int64 `xorm:"JSON TEXT null" json:"reminderDates"`
|
||||
CreatedByID int64 `xorm:"int(11) not null" json:"-"` // ID of the user who put that task on the list
|
||||
ListID int64 `xorm:"int(11) INDEX not null" json:"listID" param:"list"`
|
||||
RepeatAfter int64 `xorm:"int(11) INDEX null" json:"repeatAfter"`
|
||||
ParentTaskID int64 `xorm:"int(11) INDEX null" json:"parentTaskID"`
|
||||
Priority int64 `xorm:"int(11) null" json:"priority"`
|
||||
StartDateUnix int64 `xorm:"int(11) INDEX null" json:"startDate" query:"-"`
|
||||
EndDateUnix int64 `xorm:"int(11) INDEX null" json:"endDate" query:"-"`
|
||||
HexColor string `xorm:"varchar(6) null" json:"hexColor" valid:"runelength(0|6)" maxLength:"6"`
|
||||
UID string `xorm:"varchar(250) null" json:"-"`
|
||||
Sorting string `xorm:"-" json:"-" query:"sort"` // Parameter to sort by
|
||||
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
|
||||
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
|
||||
Created int64 `xorm:"created not null" json:"created"`
|
||||
Updated int64 `xorm:"updated not null" json:"updated"`
|
||||
}
|
||||
|
||||
func (listTask20190511202210) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20190511202210",
|
||||
Description: "Add task uid",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
err := tx.Sync2(listTask20190511202210{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get all tasks and generate a random uid for them
|
||||
var allTasks []*listTask20190511202210
|
||||
err = tx.Find(&allTasks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, t := range allTasks {
|
||||
t.UID = utils.MakeRandomString(40)
|
||||
_, err = tx.Where("id = ?", t.ID).Cols("uid").Update(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return dropTableColum(tx, "tasks", "uid")
|
||||
},
|
||||
})
|
||||
}
|
43
pkg/migration/20190514192749.go
Normal file
43
pkg/migration/20190514192749.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"github.com/go-xorm/xorm"
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
)
|
||||
|
||||
type listTask20190514192749 struct {
|
||||
DoneAtUnix int64 `xorm:"INDEX null" json:"doneAt"`
|
||||
}
|
||||
|
||||
func (listTask20190514192749) TableName() string {
|
||||
return "tasks"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20190514192749",
|
||||
Description: "Add task done at",
|
||||
Migrate: func(tx *xorm.Engine) error {
|
||||
return tx.Sync2(listTask20190514192749{})
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return dropTableColum(tx, "tasks", "done_at_unix")
|
||||
},
|
||||
})
|
||||
}
|
|
@ -86,6 +86,11 @@ func (lt *LabelTask) Create(a web.Auth) (err error) {
|
|||
|
||||
// Insert it
|
||||
_, err = x.Insert(lt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListByTaskID(lt.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -272,6 +277,8 @@ func (t *ListTask) updateTaskLabels(creator web.Auth, labels []*Label) (err erro
|
|||
}
|
||||
t.Labels = append(t.Labels, label)
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -299,7 +306,7 @@ type LabelTaskBulk struct {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/labels/bulk [post]
|
||||
func (ltb *LabelTaskBulk) Create(a web.Auth) (err error) {
|
||||
task, err := GetListTaskByID(ltb.TaskID)
|
||||
task, err := GetTaskByID(ltb.TaskID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ func (ltb *LabelTaskBulk) CanCreate(a web.Auth) (bool, error) {
|
|||
// always the same check for either deleting or adding a label to a task
|
||||
func canDoLabelTask(taskID int64, a web.Auth) (bool, error) {
|
||||
// A user can add a label to a task if he can write to the task
|
||||
task, err := getTaskByIDSimple(taskID)
|
||||
task, err := GetTaskByIDSimple(taskID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
@ -166,6 +166,8 @@ func TestLabelTask_Create(t *testing.T) {
|
|||
a: &User{ID: 1},
|
||||
},
|
||||
wantForbidden: true,
|
||||
wantErr: true,
|
||||
errType: IsErrListTaskDoesNotExist,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
@ -71,6 +71,21 @@ func (l *List) Update() (err error) {
|
|||
return CreateOrUpdateList(l)
|
||||
}
|
||||
|
||||
func updateListLastUpdated(list *List) error {
|
||||
_, err := x.ID(list.ID).Cols("updated").Update(list)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateListByTaskID(taskID int64) (err error) {
|
||||
// need to get the task to update the list last updated timestamp
|
||||
task, err := GetTaskByIDSimple(taskID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return updateListLastUpdated(&List{ID: task.ListID})
|
||||
}
|
||||
|
||||
// Create implements the create method of CRUDable
|
||||
// @Summary Creates a new list
|
||||
// @Description Creates a new list in a given namespace. The user needs write-access to the namespace.
|
||||
|
|
|
@ -125,6 +125,8 @@ func (t *ListTask) updateTaskAssignees(assignees []*User) (err error) {
|
|||
}
|
||||
|
||||
t.setTaskAssignees(assignees)
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -152,6 +154,11 @@ func (t *ListTask) setTaskAssignees(assignees []*User) {
|
|||
// @Router /tasks/{taskID}/assignees/{userID} [delete]
|
||||
func (la *ListTaskAssginee) Delete() (err error) {
|
||||
_, err = x.Delete(&ListTaskAssginee{TaskID: la.TaskID, UserID: la.UserID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListByTaskID(la.TaskID)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -198,7 +205,11 @@ func (t *ListTask) addNewAssigneeByID(newAssigneeID int64, list *List) (err erro
|
|||
TaskID: t.ID,
|
||||
UserID: newAssigneeID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -249,7 +260,7 @@ type BulkAssignees struct {
|
|||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/assignees/bulk [post]
|
||||
func (ba *BulkAssignees) Create(a web.Auth) (err error) {
|
||||
task, err := GetListTaskByID(ba.TaskID) // We need to use the full method here because we need all current assignees.
|
||||
task, err := GetTaskByID(ba.TaskID) // We need to use the full method here because we need all current assignees.
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -31,6 +31,8 @@ type ListTask struct {
|
|||
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
|
||||
// Whether a task is done or not.
|
||||
Done bool `xorm:"INDEX null" json:"done"`
|
||||
// The unix timestamp when a task was marked as done.
|
||||
DoneAtUnix int64 `xorm:"INDEX null" json:"doneAt"`
|
||||
// A unix timestamp when the task is due.
|
||||
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"`
|
||||
// An array of unix timestamps when the user wants to be reminded of the task.
|
||||
|
@ -55,6 +57,9 @@ type ListTask struct {
|
|||
// The task color in hex
|
||||
HexColor string `xorm:"varchar(6) null" json:"hexColor" valid:"runelength(0|6)" maxLength:"6"`
|
||||
|
||||
// The UID is currently not used for anything other than caldav, which is why we don't expose it over json
|
||||
UID string `xorm:"varchar(250) null" json:"-"`
|
||||
|
||||
Sorting string `xorm:"-" json:"-" query:"sort"` // Parameter to sort by
|
||||
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
|
||||
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
|
||||
|
@ -88,96 +93,36 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// No need to iterate over users and stuff if the list doesn't has tasks
|
||||
if len(taskMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all users & task ids and put them into the array
|
||||
var userIDs []int64
|
||||
var taskIDs []int64
|
||||
for _, i := range taskMap {
|
||||
taskIDs = append(taskIDs, i.ID)
|
||||
userIDs = append(userIDs, i.CreatedByID)
|
||||
}
|
||||
|
||||
// Get all assignees
|
||||
taskAssignees, err := getRawTaskAssigneesForTasks(taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Put the assignees in the task map
|
||||
for _, a := range taskAssignees {
|
||||
if a != nil {
|
||||
taskMap[a.TaskID].Assignees = append(taskMap[a.TaskID].Assignees, &a.User)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all labels for the tasks
|
||||
labels, err := getLabelsByTaskIDs(&LabelByTaskIDsOptions{TaskIDs: taskIDs})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, l := range labels {
|
||||
if l != nil {
|
||||
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &l.Label)
|
||||
}
|
||||
}
|
||||
|
||||
users := make(map[int64]*User)
|
||||
err = x.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add all user objects to the appropriate tasks
|
||||
for _, task := range taskMap {
|
||||
|
||||
// Make created by user objects
|
||||
taskMap[task.ID].CreatedBy = *users[task.CreatedByID]
|
||||
|
||||
// Reorder all subtasks
|
||||
if task.ParentTaskID != 0 {
|
||||
taskMap[task.ParentTaskID].Subtasks = append(taskMap[task.ParentTaskID].Subtasks, task)
|
||||
delete(taskMap, task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// make a complete slice from the map
|
||||
tasks = []*ListTask{}
|
||||
for _, t := range taskMap {
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
|
||||
// Sort the output. In Go, contents on a map are put on that map in no particular order (saved on heap).
|
||||
// Because of this, tasks are not sorted anymore in the output, this leads to confiusion.
|
||||
// To avoid all this, we need to sort the slice afterwards
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].ID < tasks[j].ID
|
||||
})
|
||||
|
||||
tasks, err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
func getTaskByIDSimple(taskID int64) (task ListTask, err error) {
|
||||
// GetTaskByIDSimple returns a raw task without extra data by the task ID
|
||||
func GetTaskByIDSimple(taskID int64) (task ListTask, err error) {
|
||||
if taskID < 1 {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
||||
}
|
||||
|
||||
exists, err := x.ID(taskID).Get(&task)
|
||||
return GetTaskSimple(&ListTask{ID: taskID})
|
||||
}
|
||||
|
||||
// GetTaskSimple returns a raw task without extra data
|
||||
func GetTaskSimple(t *ListTask) (task ListTask, err error) {
|
||||
task = *t
|
||||
exists, err := x.Get(&task)
|
||||
if err != nil {
|
||||
return ListTask{}, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
||||
return ListTask{}, ErrListTaskDoesNotExist{t.ID}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetListTaskByID returns all tasks a list has
|
||||
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||
listTask, err = getTaskByIDSimple(listTaskID)
|
||||
// GetTaskByID returns all tasks a list has
|
||||
func GetTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||
listTask, err = GetTaskByIDSimple(listTaskID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -221,37 +166,101 @@ func (bt *BulkTask) GetTasksByIDs() (err error) {
|
|||
}
|
||||
}
|
||||
|
||||
err = x.In("id", bt.IDs).Find(&bt.Tasks)
|
||||
taskMap := make(map[int64]*ListTask, len(bt.Tasks))
|
||||
err = x.In("id", bt.IDs).Find(&taskMap)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
// We use a map, to avoid looping over two slices at once
|
||||
var usermapids = make(map[int64]bool) // Bool ist just something, doesn't acutually matter
|
||||
for _, list := range bt.Tasks {
|
||||
usermapids[list.CreatedByID] = true
|
||||
}
|
||||
bt.Tasks, err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
// Make a slice from the map
|
||||
var userids []int64
|
||||
for uid := range usermapids {
|
||||
userids = append(userids, uid)
|
||||
}
|
||||
|
||||
// Get all users for the tasks
|
||||
var users []*User
|
||||
err = x.In("id", userids).Find(&users)
|
||||
// GetTasksByUIDs gets all tasks from a bunch of uids
|
||||
func GetTasksByUIDs(uids []string) (tasks []*ListTask, err error) {
|
||||
taskMap := make(map[int64]*ListTask)
|
||||
err = x.In("uid", uids).Find(&taskMap)
|
||||
if err != nil {
|
||||
return err
|
||||
return
|
||||
}
|
||||
|
||||
for in, task := range bt.Tasks {
|
||||
for _, u := range users {
|
||||
if task.CreatedByID == u.ID {
|
||||
bt.Tasks[in].CreatedBy = *u
|
||||
}
|
||||
tasks, err = addMoreInfoToTasks(taskMap)
|
||||
return
|
||||
}
|
||||
|
||||
// This function takes a map with pointers and returns a slice with pointers to tasks
|
||||
// It adds more stuff like assignees/labels/etc to a bunch of tasks
|
||||
func addMoreInfoToTasks(taskMap map[int64]*ListTask) (tasks []*ListTask, err error) {
|
||||
|
||||
// No need to iterate over users and stuff if the list doesn't has tasks
|
||||
if len(taskMap) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Get all users & task ids and put them into the array
|
||||
var userIDs []int64
|
||||
var taskIDs []int64
|
||||
for _, i := range taskMap {
|
||||
taskIDs = append(taskIDs, i.ID)
|
||||
userIDs = append(userIDs, i.CreatedByID)
|
||||
}
|
||||
|
||||
// Get all assignees
|
||||
taskAssignees, err := getRawTaskAssigneesForTasks(taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Put the assignees in the task map
|
||||
for _, a := range taskAssignees {
|
||||
if a != nil {
|
||||
taskMap[a.TaskID].Assignees = append(taskMap[a.TaskID].Assignees, &a.User)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all labels for all the tasks
|
||||
labels, err := getLabelsByTaskIDs(&LabelByTaskIDsOptions{TaskIDs: taskIDs})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, l := range labels {
|
||||
if l != nil {
|
||||
taskMap[l.TaskID].Labels = append(taskMap[l.TaskID].Labels, &l.Label)
|
||||
}
|
||||
}
|
||||
|
||||
// Get all users of a task
|
||||
// aka the ones who created a task
|
||||
users := make(map[int64]*User)
|
||||
err = x.In("id", userIDs).Find(&users)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add all user objects to the appropriate tasks
|
||||
for _, task := range taskMap {
|
||||
|
||||
// Make created by user objects
|
||||
taskMap[task.ID].CreatedBy = *users[task.CreatedByID]
|
||||
|
||||
// Reorder all subtasks
|
||||
if task.ParentTaskID != 0 {
|
||||
taskMap[task.ParentTaskID].Subtasks = append(taskMap[task.ParentTaskID].Subtasks, task)
|
||||
delete(taskMap, task.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// make a complete slice from the map
|
||||
tasks = []*ListTask{}
|
||||
for _, t := range taskMap {
|
||||
tasks = append(tasks, t)
|
||||
}
|
||||
|
||||
// Sort the output. In Go, contents on a map are put on that map in no particular order.
|
||||
// Because of this, tasks are not sorted anymore in the output, this leads to confiusion.
|
||||
// To avoid all this, we need to sort the slice afterwards
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
return tasks[i].ID < tasks[j].ID
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -18,8 +18,10 @@ package models
|
|||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/web"
|
||||
"github.com/imdario/mergo"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Create is the implementation to create a list task
|
||||
|
@ -60,6 +62,11 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// Generate a uuid if we don't already have one
|
||||
if t.UID == "" {
|
||||
t.UID = utils.MakeRandomString(40)
|
||||
}
|
||||
|
||||
t.CreatedByID = u.ID
|
||||
t.CreatedBy = u
|
||||
if _, err = x.Insert(t); err != nil {
|
||||
|
@ -72,6 +79,8 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
|||
}
|
||||
|
||||
metrics.UpdateCount(1, metrics.TaskCountKey)
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -91,7 +100,7 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
|||
// @Router /tasks/{id} [post]
|
||||
func (t *ListTask) Update() (err error) {
|
||||
// Check if the task exists
|
||||
ot, err := GetListTaskByID(t.ID)
|
||||
ot, err := GetTaskByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -189,12 +198,20 @@ func (t *ListTask) Update() (err error) {
|
|||
"priority",
|
||||
"start_date_unix",
|
||||
"end_date_unix",
|
||||
"hex_color").
|
||||
"hex_color",
|
||||
"done_at_unix").
|
||||
Update(ot)
|
||||
*t = ot
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
||||
// This helper function updates the reminders and doneAtUnix of the *old* task (since that's the one we're inserting
|
||||
// with updated values into the db)
|
||||
func updateDone(oldTask *ListTask, newTask *ListTask) {
|
||||
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
|
||||
oldTask.DueDateUnix = oldTask.DueDateUnix + oldTask.RepeatAfter // assuming we'll save the old task (merged)
|
||||
|
@ -205,4 +222,13 @@ func updateDone(oldTask *ListTask, newTask *ListTask) {
|
|||
|
||||
newTask.Done = false
|
||||
}
|
||||
|
||||
// Update the "done at" timestamp
|
||||
if !oldTask.Done && newTask.Done {
|
||||
oldTask.DoneAtUnix = time.Now().Unix()
|
||||
}
|
||||
// When unmarking a task as done, reset the timestamp
|
||||
if oldTask.Done && !newTask.Done {
|
||||
oldTask.DoneAtUnix = 0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ import (
|
|||
func (t *ListTask) Delete() (err error) {
|
||||
|
||||
// Check if it exists
|
||||
_, err = GetListTaskByID(t.ID)
|
||||
_, err = GetTaskByID(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -51,5 +51,7 @@ func (t *ListTask) Delete() (err error) {
|
|||
}
|
||||
|
||||
metrics.UpdateCount(-1, metrics.TaskCountKey)
|
||||
|
||||
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func (t *ListTask) CanCreate(a web.Auth) (bool, error) {
|
|||
func (t *ListTask) CanRead(a web.Auth) (canRead bool, err error) {
|
||||
//return t.canDoListTask(a)
|
||||
// Get the task, error out if it doesn't exist
|
||||
*t, err = getTaskByIDSimple(t.ID)
|
||||
*t, err = GetTaskByIDSimple(t.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ func (t *ListTask) canDoListTask(a web.Auth) (bool, error) {
|
|||
doer := getUserForRights(a)
|
||||
|
||||
// Get the task
|
||||
lI, err := getTaskByIDSimple(t.ID)
|
||||
lI, err := GetTaskByIDSimple(t.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ func TestListTask_Create(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Check if it was updated
|
||||
li, err := GetListTaskByID(listtask.ID)
|
||||
li, err := GetTaskByID(listtask.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, li.Text, "Test34")
|
||||
|
||||
|
@ -91,3 +91,18 @@ func TestListTask_Create(t *testing.T) {
|
|||
assert.Error(t, err)
|
||||
assert.True(t, IsErrUserDoesNotExist(err))
|
||||
}
|
||||
|
||||
func TestUpdateDone(t *testing.T) {
|
||||
t.Run("marking a task as done", func(t *testing.T) {
|
||||
oldTask := &ListTask{Done: false}
|
||||
newTask := &ListTask{Done: true}
|
||||
updateDone(oldTask, newTask)
|
||||
assert.NotEqual(t, int64(0), oldTask.DoneAtUnix)
|
||||
})
|
||||
t.Run("unmarking a task as done", func(t *testing.T) {
|
||||
oldTask := &ListTask{Done: true}
|
||||
newTask := &ListTask{Done: false}
|
||||
updateDone(oldTask, newTask)
|
||||
assert.Equal(t, int64(0), oldTask.DoneAtUnix)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -67,6 +67,10 @@ func (lu *ListUser) Create(a web.Auth) (err error) {
|
|||
|
||||
// Insert user <-> list relation
|
||||
_, err = x.Insert(lu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(l)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -51,5 +51,10 @@ func (lu *ListUser) Delete() (err error) {
|
|||
|
||||
_, err = x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||
Delete(&ListUser{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: lu.ListID})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -44,5 +44,10 @@ func (lu *ListUser) Update() (err error) {
|
|||
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
||||
Cols("right").
|
||||
Update(lu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: lu.ListID})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -65,5 +65,10 @@ func (tl *TeamList) Create(a web.Auth) (err error) {
|
|||
|
||||
// Insert the new team
|
||||
_, err = x.Insert(tl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(l)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -53,6 +53,10 @@ func (tl *TeamList) Delete() (err error) {
|
|||
_, err = x.Where("team_id = ?", tl.TeamID).
|
||||
And("list_id = ?", tl.ListID).
|
||||
Delete(TeamList{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: tl.ListID})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -44,5 +44,10 @@ func (tl *TeamList) Update() (err error) {
|
|||
Where("list_id = ? AND team_id = ?", tl.ListID, tl.TeamID).
|
||||
Cols("right").
|
||||
Update(tl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = updateListLastUpdated(&List{ID: tl.ListID})
|
||||
return
|
||||
}
|
||||
|
|
|
@ -115,7 +115,7 @@ func CreateUser(user User) (newUser User, err error) {
|
|||
|
||||
// HashPassword hashes a password
|
||||
func hashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 11)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2018 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/caldav"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/web/handler"
|
||||
"github.com/labstack/echo/v4"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Caldav returns a caldav-readable format with all tasks
|
||||
// @Summary CalDAV-readable format with all tasks as calendar events.
|
||||
// @Description Returns a calDAV-parsable format with all tasks as calendar events. Only returns tasks with a due date. Also creates reminders when the task has one.
|
||||
// @tags task
|
||||
// @Produce text/plain
|
||||
// @Security BasicAuth
|
||||
// @Success 200 {string} string "The caldav events."
|
||||
// @Failure 403 {string} string "Unauthorized."
|
||||
// @Router /tasks/caldav [get]
|
||||
func Caldav(c echo.Context) error {
|
||||
|
||||
// Request basic auth
|
||||
user, pass, ok := c.Request().BasicAuth()
|
||||
|
||||
// Check credentials
|
||||
creds := &models.UserLogin{
|
||||
Username: user,
|
||||
Password: pass,
|
||||
}
|
||||
u, err := models.CheckUserCredentials(creds)
|
||||
|
||||
if !ok || err != nil {
|
||||
c.Response().Header().Set("WWW-Authenticate", `Basic realm="Vikunja cal"`)
|
||||
return c.String(http.StatusUnauthorized, "Unauthorized.")
|
||||
}
|
||||
|
||||
// Get all tasks for that user
|
||||
tasks, err := models.GetTasksByUser("", &u, -1, models.SortTasksByUnsorted, time.Now(), time.Now().Add(24*356*time.Hour))
|
||||
if err != nil {
|
||||
return handler.HandleHTTPError(err, c)
|
||||
}
|
||||
|
||||
hour := int64(time.Hour.Seconds())
|
||||
var caldavTasks []*caldav.Event
|
||||
for _, t := range tasks {
|
||||
if t.DueDateUnix != 0 {
|
||||
event := &caldav.Event{
|
||||
Summary: t.Text,
|
||||
Description: t.Description,
|
||||
UID: "",
|
||||
TimestampUnix: t.Updated,
|
||||
StartUnix: t.DueDateUnix,
|
||||
EndUnix: t.DueDateUnix + hour,
|
||||
}
|
||||
|
||||
if len(t.RemindersUnix) > 0 {
|
||||
for _, rem := range t.RemindersUnix {
|
||||
event.Alarms = append(event.Alarms, caldav.Alarm{TimeUnix: rem})
|
||||
}
|
||||
}
|
||||
|
||||
caldavTasks = append(caldavTasks, event)
|
||||
}
|
||||
}
|
||||
|
||||
caldavConfig := &caldav.Config{
|
||||
Name: "Vikunja Calendar for " + u.Username,
|
||||
ProdID: "Vikunja Todo App",
|
||||
}
|
||||
|
||||
return c.String(http.StatusOK, caldav.ParseEvents(caldavConfig, caldavTasks))
|
||||
}
|
184
pkg/routes/caldav/handler.go
Normal file
184
pkg/routes/caldav/handler.go
Normal file
|
@ -0,0 +1,184 @@
|
|||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package caldav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/web/handler"
|
||||
"fmt"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/samedi/caldav-go"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getBasicAuthUserFromContext(c echo.Context) (user models.User, err error) {
|
||||
u, is := c.Get("userBasicAuth").(models.User)
|
||||
if !is {
|
||||
return models.User{}, fmt.Errorf("user is not user element, is %s", reflect.TypeOf(c.Get("userBasicAuth")))
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ListHandler returns all tasks from a list
|
||||
func ListHandler(c echo.Context) error {
|
||||
listID, err := getIntParam(c, "list")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := getBasicAuthUserFromContext(c)
|
||||
if err != nil {
|
||||
log.Log.Error(err)
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
storage := &VikunjaCaldavListStorage{
|
||||
list: &models.List{ID: listID},
|
||||
user: &u,
|
||||
}
|
||||
|
||||
// Try to parse a task from the request payload
|
||||
body, _ := ioutil.ReadAll(c.Request().Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
// Parse it
|
||||
vtodo := string(body)
|
||||
if vtodo != "" && strings.HasPrefix(vtodo, `BEGIN:VCALENDAR`) {
|
||||
storage.task, err = parseTaskFromVTODO(vtodo)
|
||||
if err != nil {
|
||||
log.Log.Error(err)
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
log.Log.Debugf("[CALDAV] Request Body: %v\n", string(body))
|
||||
log.Log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
||||
caldav.SetupStorage(storage)
|
||||
caldav.SetupUser("dav/lists")
|
||||
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
|
||||
response := caldav.HandleRequest(c.Request())
|
||||
response.Write(c.Response())
|
||||
return nil
|
||||
}
|
||||
|
||||
// TaskHandler is the handler which manages updating/deleting a single task
|
||||
func TaskHandler(c echo.Context) error {
|
||||
listID, err := getIntParam(c, "list")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u, err := getBasicAuthUserFromContext(c)
|
||||
if err != nil {
|
||||
log.Log.Error(err)
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
// Get the task uid
|
||||
taskUID := strings.TrimSuffix(c.Param("task"), ".ics")
|
||||
|
||||
storage := &VikunjaCaldavListStorage{
|
||||
list: &models.List{ID: listID},
|
||||
task: &models.ListTask{UID: taskUID},
|
||||
user: &u,
|
||||
}
|
||||
|
||||
caldav.SetupStorage(storage)
|
||||
response := caldav.HandleRequest(c.Request())
|
||||
response.Write(c.Response())
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrincipalHandler handles all request to principal resources
|
||||
func PrincipalHandler(c echo.Context) error {
|
||||
u, err := getBasicAuthUserFromContext(c)
|
||||
if err != nil {
|
||||
log.Log.Error(err)
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
storage := &VikunjaCaldavListStorage{
|
||||
user: &u,
|
||||
isPrincipal: true,
|
||||
}
|
||||
|
||||
// Try to parse a task from the request payload
|
||||
body, _ := ioutil.ReadAll(c.Request().Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
log.Log.Debugf("[CALDAV] Request Body: %v\n", string(body))
|
||||
log.Log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
||||
caldav.SetupStorage(storage)
|
||||
caldav.SetupUser("dav/principals/" + u.Username)
|
||||
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
|
||||
|
||||
response := caldav.HandleRequest(c.Request())
|
||||
response.Write(c.Response())
|
||||
return nil
|
||||
}
|
||||
|
||||
// EntryHandler handles all request to principal resources
|
||||
func EntryHandler(c echo.Context) error {
|
||||
u, err := getBasicAuthUserFromContext(c)
|
||||
if err != nil {
|
||||
log.Log.Error(err)
|
||||
return echo.ErrInternalServerError
|
||||
}
|
||||
|
||||
storage := &VikunjaCaldavListStorage{
|
||||
user: &u,
|
||||
isEntry: true,
|
||||
}
|
||||
|
||||
// Try to parse a task from the request payload
|
||||
body, _ := ioutil.ReadAll(c.Request().Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
|
||||
log.Log.Debugf("[CALDAV] Request Body: %v\n", string(body))
|
||||
log.Log.Debugf("[CALDAV] Request Headers: %v\n", c.Request().Header)
|
||||
|
||||
caldav.SetupStorage(storage)
|
||||
caldav.SetupUser("dav/principals/" + u.Username)
|
||||
caldav.SetupSupportedComponents([]string{lib.VCALENDAR, lib.VTODO})
|
||||
|
||||
response := caldav.HandleRequest(c.Request())
|
||||
response.Write(c.Response())
|
||||
return nil
|
||||
}
|
||||
|
||||
func getIntParam(c echo.Context, paramName string) (intParam int64, err error) {
|
||||
param := c.Param(paramName)
|
||||
if param == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
intParam, err = strconv.ParseInt(param, 10, 64)
|
||||
if err != nil {
|
||||
return 0, handler.HandleHTTPError(err, c)
|
||||
}
|
||||
return
|
||||
}
|
404
pkg/routes/caldav/listStorageProvider.go
Normal file
404
pkg/routes/caldav/listStorageProvider.go
Normal file
|
@ -0,0 +1,404 @@
|
|||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package caldav
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DavBasePath is the base url path
|
||||
const DavBasePath = `/dav/`
|
||||
|
||||
// ListBasePath is the base path for all lists resources
|
||||
const ListBasePath = DavBasePath + `lists`
|
||||
|
||||
// VikunjaCaldavListStorage represents a list storage
|
||||
type VikunjaCaldavListStorage struct {
|
||||
// Used when handling a list
|
||||
list *models.List
|
||||
// Used when handling a single task, like updating
|
||||
task *models.ListTask
|
||||
// The current user
|
||||
user *models.User
|
||||
isPrincipal bool
|
||||
isEntry bool // Entry level handling should only return a link to the principal url
|
||||
}
|
||||
|
||||
// GetResources returns either all lists, links to the principal, or only one list, depending on the request
|
||||
func (vcls *VikunjaCaldavListStorage) GetResources(rpath string, withChildren bool) ([]data.Resource, error) {
|
||||
|
||||
// It looks like we need to have the same handler for returning both the calendar home set and the user principal
|
||||
// Since the client seems to ignore the whatever is being returned in the first request and just makes a second one
|
||||
// to the same url but requesting the calendar home instead
|
||||
// The problem with this is caldav-go just return whatever ressource it gets and making that the requested path
|
||||
// And for us here, there is no easy (I can think of at least one hacky way) to figure out if the client is requesting
|
||||
// the home or principal url. Ough.
|
||||
|
||||
// Ok, maybe the problem is more the client making a request to /dav/ and getting a response which says
|
||||
// something like "hey, for /dav/lists, the calendar home is /dav/lists", but the client expects a
|
||||
// response to go something like "hey, for /dav/, the calendar home is /dav/lists" since it requested /dav/
|
||||
// and not /dav/lists. I'm not sure if thats a bug in the client or in caldav-go.
|
||||
|
||||
if vcls.isEntry {
|
||||
r := data.NewResource(rpath, &VikunjaListResourceAdapter{
|
||||
isPrincipal: true,
|
||||
isCollection: true,
|
||||
})
|
||||
return []data.Resource{r}, nil
|
||||
}
|
||||
|
||||
// If the request wants the principal url, we'll return that and nothing else
|
||||
if vcls.isPrincipal {
|
||||
r := data.NewResource(DavBasePath+`/lists/`, &VikunjaListResourceAdapter{
|
||||
isPrincipal: true,
|
||||
isCollection: true,
|
||||
})
|
||||
return []data.Resource{r}, nil
|
||||
}
|
||||
|
||||
// If vcls.list.ID is != 0, this means the user is doing a PROPFIND request to /lists/:list
|
||||
// Which means we need to get only one list
|
||||
if vcls.list != nil && vcls.list.ID != 0 {
|
||||
rr, err := vcls.getListRessource(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := data.NewResource(rpath, &rr)
|
||||
r.Name = vcls.list.Title
|
||||
return []data.Resource{r}, nil
|
||||
}
|
||||
|
||||
// Otherwise get all lists
|
||||
thelists, err := vcls.list.ReadAll("", vcls.user, -1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lists := thelists.([]*models.List)
|
||||
|
||||
var resources []data.Resource
|
||||
for _, l := range lists {
|
||||
rr := VikunjaListResourceAdapter{
|
||||
list: l,
|
||||
isCollection: true,
|
||||
}
|
||||
r := data.NewResource(ListBasePath+"/"+strconv.FormatInt(l.ID, 10), &rr)
|
||||
r.Name = l.Title
|
||||
resources = append(resources, r)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// GetResourcesByList fetches a list of resources from a slice of paths
|
||||
func (vcls *VikunjaCaldavListStorage) GetResourcesByList(rpaths []string) ([]data.Resource, error) {
|
||||
|
||||
// Parse the set of resourcepaths into usable uids
|
||||
// A path looks like this: /dav/lists/10/a6eb526d5748a5c499da202fe74f36ed1aea2aef.ics
|
||||
// So we split the url in parts, take the last one and strip the ".ics" at the end
|
||||
var uids []string
|
||||
for _, path := range rpaths {
|
||||
parts := strings.Split(path, "/")
|
||||
uid := []rune(parts[4]) // The 4th part is the id with ".ics" suffix
|
||||
endlen := len(uid) - 4 // ".ics" are 4 bytes
|
||||
uids = append(uids, string(uid[:endlen]))
|
||||
}
|
||||
|
||||
// GetTasksByUIDs...
|
||||
// Parse these into ressources...
|
||||
tasks, err := models.GetTasksByUIDs(uids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resources []data.Resource
|
||||
for _, t := range tasks {
|
||||
rr := VikunjaListResourceAdapter{
|
||||
task: t,
|
||||
}
|
||||
r := data.NewResource(getTaskURL(t), &rr)
|
||||
r.Name = t.Text
|
||||
resources = append(resources, r)
|
||||
}
|
||||
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// GetResourcesByFilters fetches a list of resources with a filter
|
||||
func (vcls *VikunjaCaldavListStorage) GetResourcesByFilters(rpath string, filters *data.ResourceFilter) ([]data.Resource, error) {
|
||||
|
||||
// If we already have a list saved, that means the user is making a REPORT request to find out if
|
||||
// anything changed, in that case we need to return all tasks.
|
||||
// That list is coming from a previous "getListRessource" in L177
|
||||
if vcls.list.Tasks != nil {
|
||||
var resources []data.Resource
|
||||
for _, t := range vcls.list.Tasks {
|
||||
rr := VikunjaListResourceAdapter{
|
||||
list: vcls.list,
|
||||
task: t,
|
||||
isCollection: false,
|
||||
}
|
||||
r := data.NewResource(getTaskURL(t), &rr)
|
||||
r.Name = t.Text
|
||||
resources = append(resources, r)
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// This is used to get all
|
||||
rr, err := vcls.getListRessource(false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := data.NewResource(rpath, &rr)
|
||||
r.Name = vcls.list.Title
|
||||
return []data.Resource{r}, nil
|
||||
// For now, filtering is disabled.
|
||||
//return vcls.GetResources(rpath, false)
|
||||
}
|
||||
|
||||
func getTaskURL(task *models.ListTask) string {
|
||||
return ListBasePath + "/" + strconv.FormatInt(task.ListID, 10) + `/` + task.UID + `.ics`
|
||||
}
|
||||
|
||||
// GetResource fetches a single resource
|
||||
func (vcls *VikunjaCaldavListStorage) GetResource(rpath string) (*data.Resource, bool, error) {
|
||||
|
||||
// If the task is not nil, we need to get the task and not the list
|
||||
if vcls.task != nil {
|
||||
// save and override the updated unix date to not break any later etag checks
|
||||
updated := vcls.task.Updated
|
||||
task, err := models.GetTaskSimple(&models.ListTask{ID: vcls.task.ID, UID: vcls.task.UID})
|
||||
if err != nil {
|
||||
if models.IsErrListTaskDoesNotExist(err) {
|
||||
return nil, false, errs.ResourceNotFoundError
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
vcls.task = &task
|
||||
if updated > 0 {
|
||||
vcls.task.Updated = updated
|
||||
}
|
||||
|
||||
rr := VikunjaListResourceAdapter{
|
||||
list: vcls.list,
|
||||
task: &task,
|
||||
}
|
||||
r := data.NewResource(rpath, &rr)
|
||||
return &r, true, nil
|
||||
}
|
||||
|
||||
// Otherwise get the list with all tasks
|
||||
rr, err := vcls.getListRessource(true)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
r := data.NewResource(rpath, &rr)
|
||||
return &r, true, nil
|
||||
}
|
||||
|
||||
// GetShallowResource gets a ressource without childs
|
||||
// Since Vikunja has no children, this is the same as GetResource
|
||||
func (vcls *VikunjaCaldavListStorage) GetShallowResource(rpath string) (*data.Resource, bool, error) {
|
||||
// Since Vikunja has no childs, this just returns the same as GetResource()
|
||||
// FIXME: This should just get the list with no tasks whatsoever, nothing else
|
||||
return vcls.GetResource(rpath)
|
||||
}
|
||||
|
||||
// CreateResource creates a new resource
|
||||
func (vcls *VikunjaCaldavListStorage) CreateResource(rpath, content string) (*data.Resource, error) {
|
||||
|
||||
vTask, err := parseTaskFromVTODO(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vTask.ListID = vcls.list.ID
|
||||
|
||||
// Check the rights
|
||||
canCreate, err := vTask.CanCreate(vcls.user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !canCreate {
|
||||
return nil, errs.ForbiddenError
|
||||
}
|
||||
|
||||
// Create the task
|
||||
err = vTask.Create(vcls.user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build up the proper response
|
||||
rr := VikunjaListResourceAdapter{
|
||||
list: vcls.list,
|
||||
task: vTask,
|
||||
}
|
||||
r := data.NewResource(rpath, &rr)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// UpdateResource updates a resource
|
||||
func (vcls *VikunjaCaldavListStorage) UpdateResource(rpath, content string) (*data.Resource, error) {
|
||||
|
||||
vTask, err := parseTaskFromVTODO(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// At this point, we already have the right task in vcls.task, so we can use that ID directly
|
||||
vTask.ID = vcls.task.ID
|
||||
|
||||
// Check the rights
|
||||
canUpdate, err := vTask.CanUpdate(vcls.user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !canUpdate {
|
||||
return nil, errs.ForbiddenError
|
||||
}
|
||||
|
||||
// Update the task
|
||||
err = vTask.Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build up the proper response
|
||||
rr := VikunjaListResourceAdapter{
|
||||
list: vcls.list,
|
||||
task: vTask,
|
||||
}
|
||||
r := data.NewResource(rpath, &rr)
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// DeleteResource deletes a resource
|
||||
func (vcls *VikunjaCaldavListStorage) DeleteResource(rpath string) error {
|
||||
if vcls.task != nil {
|
||||
// Check the rights
|
||||
canDelete, err := vcls.task.CanDelete(vcls.user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !canDelete {
|
||||
return errs.ForbiddenError
|
||||
}
|
||||
|
||||
// Delete it
|
||||
return vcls.task.Delete()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VikunjaListResourceAdapter holds the actual resource
|
||||
type VikunjaListResourceAdapter struct {
|
||||
list *models.List
|
||||
task *models.ListTask
|
||||
|
||||
isPrincipal bool
|
||||
isCollection bool
|
||||
}
|
||||
|
||||
// IsCollection checks if the resoure in the adapter is a collection
|
||||
func (vlra *VikunjaListResourceAdapter) IsCollection() bool {
|
||||
// If the discovery does not work, setting this to true makes it work again.
|
||||
return vlra.isCollection
|
||||
}
|
||||
|
||||
// CalculateEtag returns the etag of a resource
|
||||
func (vlra *VikunjaListResourceAdapter) CalculateEtag() string {
|
||||
|
||||
// If we're updating a task, the client sends the etag of the list instead of the one from the task.
|
||||
// And therefore, updating the task fails since these etags don't match.
|
||||
// To fix that, we use this extra field to determine if we're currently updating a task and return the
|
||||
// etag of the list instead.
|
||||
//if vlra.list != nil {
|
||||
// return `"` + strconv.FormatInt(vlra.list.ID, 10) + `-` + strconv.FormatInt(vlra.list.Updated, 10) + `"`
|
||||
//}
|
||||
|
||||
// Return the etag of a task if we have one
|
||||
if vlra.task != nil {
|
||||
return `"` + strconv.FormatInt(vlra.task.ID, 10) + `-` + strconv.FormatInt(vlra.task.Updated, 10) + `"`
|
||||
}
|
||||
// This also returns the etag of the list, and not of the task,
|
||||
// which becomes problematic because the client uses this etag (= the one from the list) to make
|
||||
// Requests to update a task. These do not match and thus updating a task fails.
|
||||
return `"` + strconv.FormatInt(vlra.list.ID, 10) + `-` + strconv.FormatInt(vlra.list.Updated, 10) + `"`
|
||||
}
|
||||
|
||||
// GetContent returns the content string of a resource (a task in our case)
|
||||
func (vlra *VikunjaListResourceAdapter) GetContent() string {
|
||||
if vlra.list != nil && vlra.list.Tasks != nil {
|
||||
return getCaldavTodosForTasks(vlra.list)
|
||||
}
|
||||
|
||||
if vlra.task != nil {
|
||||
list := models.List{Tasks: []*models.ListTask{vlra.task}}
|
||||
return getCaldavTodosForTasks(&list)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetContentSize is the size of a caldav content
|
||||
func (vlra *VikunjaListResourceAdapter) GetContentSize() int64 {
|
||||
return int64(len(vlra.GetContent()))
|
||||
}
|
||||
|
||||
// GetModTime returns when the resource was last modified
|
||||
func (vlra *VikunjaListResourceAdapter) GetModTime() time.Time {
|
||||
if vlra.task != nil {
|
||||
return time.Unix(vlra.task.Updated, 0)
|
||||
}
|
||||
|
||||
if vlra.list != nil {
|
||||
return time.Unix(vlra.list.Updated, 0)
|
||||
}
|
||||
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (vcls *VikunjaCaldavListStorage) getListRessource(isCollection bool) (rr VikunjaListResourceAdapter, err error) {
|
||||
can, err := vcls.list.CanRead(vcls.user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !can {
|
||||
log.Log.Errorf("User %v tried to access a caldav resource (List %v) which they are not allowed to access", vcls.user.Username, vcls.list.ID)
|
||||
return rr, models.ErrUserDoesNotHaveAccessToList{ListID: vcls.list.ID}
|
||||
}
|
||||
err = vcls.list.ReadOne()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rr = VikunjaListResourceAdapter{
|
||||
list: vcls.list,
|
||||
isCollection: isCollection,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
125
pkg/routes/caldav/parsing.go
Normal file
125
pkg/routes/caldav/parsing.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
// Vikunja is a todo-list application to facilitate your life.
|
||||
// Copyright 2019 Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package caldav
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/caldav"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"github.com/laurent22/ical-go/ical"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getCaldavTodosForTasks(list *models.List) string {
|
||||
|
||||
// Make caldav todos from Vikunja todos
|
||||
var caldavtodos []*caldav.Todo
|
||||
for _, t := range list.Tasks {
|
||||
|
||||
durationString := t.EndDateUnix - t.StartDateUnix
|
||||
duration, _ := time.ParseDuration(strconv.FormatInt(durationString, 10) + `s`)
|
||||
|
||||
caldavtodos = append(caldavtodos, &caldav.Todo{
|
||||
TimestampUnix: t.Updated,
|
||||
UID: t.UID,
|
||||
Summary: t.Text,
|
||||
Description: t.Description,
|
||||
CompletedUnix: t.DoneAtUnix,
|
||||
// Organizer: &t.CreatedBy, // Disabled until we figure out how this works
|
||||
Priority: t.Priority,
|
||||
StartUnix: t.StartDateUnix,
|
||||
EndUnix: t.EndDateUnix,
|
||||
CreatedUnix: t.Created,
|
||||
UpdatedUnix: t.Updated,
|
||||
DueDateUnix: t.DueDateUnix,
|
||||
Duration: duration,
|
||||
})
|
||||
}
|
||||
|
||||
caldavConfig := &caldav.Config{
|
||||
Name: list.Title,
|
||||
ProdID: "Vikunja Todo App",
|
||||
}
|
||||
|
||||
return caldav.ParseTodos(caldavConfig, caldavtodos)
|
||||
}
|
||||
|
||||
func parseTaskFromVTODO(content string) (vTask *models.ListTask, err error) {
|
||||
parsed, err := ical.ParseCalendar(content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We put the task details in a map to be able to handle them more easily
|
||||
task := make(map[string]string)
|
||||
for _, c := range parsed.Children {
|
||||
if c.Name == "VTODO" {
|
||||
for _, entry := range c.Children {
|
||||
task[entry.Name] = entry.Value
|
||||
}
|
||||
// Breaking, to only process the first task
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the UID
|
||||
var priority int64
|
||||
if _, ok := task["PRIORITY"]; ok {
|
||||
priority, err = strconv.ParseInt(task["PRIORITY"], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the enddate
|
||||
duration, _ := time.ParseDuration(task["DURATION"])
|
||||
|
||||
vTask = &models.ListTask{
|
||||
UID: task["UID"],
|
||||
Text: task["SUMMARY"],
|
||||
Description: task["DESCRIPTION"],
|
||||
Priority: priority,
|
||||
DueDateUnix: caldavTimeToUnixTimestamp(task["DUE"]),
|
||||
Updated: caldavTimeToUnixTimestamp(task["DTSTAMP"]),
|
||||
StartDateUnix: caldavTimeToUnixTimestamp(task["DTSTART"]),
|
||||
DoneAtUnix: caldavTimeToUnixTimestamp(task["COMPLETED"]),
|
||||
}
|
||||
|
||||
if task["STATUS"] == "COMPLETED" {
|
||||
vTask.Done = true
|
||||
}
|
||||
|
||||
if duration > 0 && vTask.StartDateUnix > 0 {
|
||||
vTask.EndDateUnix = vTask.StartDateUnix + int64(duration.Seconds())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func caldavTimeToUnixTimestamp(tstring string) int64 {
|
||||
if tstring == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
t, err := time.Parse(caldav.DateFormat, tstring)
|
||||
if err != nil {
|
||||
log.Log.Warningf("Error while parsing caldav time %s to unix time: %s", tstring, err)
|
||||
return 0
|
||||
}
|
||||
return t.Unix()
|
||||
}
|
|
@ -43,6 +43,7 @@ import (
|
|||
"code.vikunja.io/api/pkg/metrics"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
|
||||
"code.vikunja.io/api/pkg/routes/caldav"
|
||||
_ "code.vikunja.io/api/pkg/swagger" // To generate swagger docs
|
||||
"code.vikunja.io/web"
|
||||
"code.vikunja.io/web/handler"
|
||||
|
@ -52,6 +53,7 @@ import (
|
|||
elog "github.com/labstack/gommon/log"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/viper"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CustomValidator is a dummy struct to use govalidator with echo
|
||||
|
@ -119,13 +121,34 @@ func NewEcho() *echo.Echo {
|
|||
// RegisterRoutes registers all routes for the application
|
||||
func RegisterRoutes(e *echo.Echo) {
|
||||
|
||||
if viper.GetBool("service.enablecaldav") {
|
||||
// Caldav routes
|
||||
wkg := e.Group("/.well-known")
|
||||
wkg.Use(middleware.BasicAuth(caldavBasicAuth))
|
||||
wkg.Any("/caldav", caldav.PrincipalHandler)
|
||||
wkg.Any("/caldav/", caldav.PrincipalHandler)
|
||||
c := e.Group("/dav")
|
||||
registerCalDavRoutes(c)
|
||||
}
|
||||
|
||||
// CORS_SHIT
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{"*"},
|
||||
Skipper: func(context echo.Context) bool {
|
||||
// Since it is not possible to register this middleware just for the api group,
|
||||
// we just disable it when for caldav requests.
|
||||
// Caldav requires OPTIONS requests to be answered in a specific manner,
|
||||
// not doing this would break the caldav implementation
|
||||
return strings.HasPrefix(context.Path(), "/dav")
|
||||
},
|
||||
}))
|
||||
|
||||
// API Routes
|
||||
a := e.Group("/api/v1")
|
||||
registerAPIRoutes(a)
|
||||
}
|
||||
|
||||
func registerAPIRoutes(a *echo.Group) {
|
||||
|
||||
// Docs
|
||||
a.GET("/docs.json", apiv1.DocsJSON)
|
||||
|
@ -192,9 +215,6 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
a.POST("/user/password/reset", apiv1.UserResetPassword)
|
||||
a.POST("/user/confirm", apiv1.UserConfirmEmail)
|
||||
|
||||
// Caldav, with auth
|
||||
a.GET("/tasks/caldav", apiv1.Caldav)
|
||||
|
||||
// ===== Routes with Authetification =====
|
||||
// Authetification
|
||||
a.Use(middleware.JWT([]byte(viper.GetString("service.JWTSecret"))))
|
||||
|
@ -363,3 +383,34 @@ func RegisterRoutes(e *echo.Echo) {
|
|||
a.PUT("/teams/:team/members", teamMemberHandler.CreateWeb)
|
||||
a.DELETE("/teams/:team/members/:user", teamMemberHandler.DeleteWeb)
|
||||
}
|
||||
|
||||
func registerCalDavRoutes(c *echo.Group) {
|
||||
|
||||
// Basic auth middleware
|
||||
c.Use(middleware.BasicAuth(caldavBasicAuth))
|
||||
|
||||
// THIS is the entry point for caldav clients, otherwise lists will show up double
|
||||
c.Any("", caldav.EntryHandler)
|
||||
c.Any("/", caldav.EntryHandler)
|
||||
c.Any("/principals/*/", caldav.PrincipalHandler)
|
||||
c.Any("/lists", caldav.ListHandler)
|
||||
c.Any("/lists/", caldav.ListHandler)
|
||||
c.Any("/lists/:list", caldav.ListHandler)
|
||||
c.Any("/lists/:list/", caldav.ListHandler)
|
||||
c.Any("/lists/:list/:task", caldav.TaskHandler) // Mostly used for editing
|
||||
}
|
||||
|
||||
func caldavBasicAuth(username, password string, c echo.Context) (bool, error) {
|
||||
creds := &models.UserLogin{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
u, err := models.CheckUserCredentials(creds)
|
||||
if err != nil {
|
||||
log.Log.Errorf("Error during basic auth for caldav: %v", err)
|
||||
return false, nil
|
||||
}
|
||||
// Save the user in echo context for later use
|
||||
c.Set("userBasicAuth", u)
|
||||
return true, nil
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||
// This file was generated by swaggo/swag at
|
||||
// 2019-04-30 11:26:19.179895431 +0200 CEST m=+0.133585470
|
||||
// 2019-05-22 19:24:37.734465408 +0200 CEST m=+0.660846954
|
||||
|
||||
package swagger
|
||||
|
||||
|
@ -14,7 +14,7 @@ import (
|
|||
var doc = `{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is the documentation for the [Vikunja](http://vikunja.io) API. Vikunja is a cross-plattform Todo-application with a lot of features, such as sharing lists with users or teams. \u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs ` + "`" + `Authorization: Bearer \u003cjwt-token\u003e` + "`" + `-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n\u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e",
|
||||
"description": "\u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e",
|
||||
"title": "Vikunja API",
|
||||
"contact": {
|
||||
"name": "General Vikunja contact",
|
||||
|
@ -391,7 +391,7 @@ var doc = `{
|
|||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a team by its ID.",
|
||||
"description": "Returns a list by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -399,13 +399,13 @@ var doc = `{
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"team"
|
||||
"list"
|
||||
],
|
||||
"summary": "Gets one team",
|
||||
"summary": "Gets one list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Team ID",
|
||||
"description": "List ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
|
@ -413,14 +413,14 @@ var doc = `{
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The team",
|
||||
"description": "The list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.Team"
|
||||
"$ref": "#/definitions/models.List"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the team",
|
||||
"description": "The user does not have access to the list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
||||
|
@ -2381,37 +2381,6 @@ var doc = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/tasks/caldav": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a calDAV-parsable format with all tasks as calendar events. Only returns tasks with a due date. Also creates reminders when the task has one.",
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
],
|
||||
"summary": "CalDAV-readable format with all tasks as calendar events.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The caldav events.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -3688,6 +3657,10 @@ var doc = `{
|
|||
"description": "Whether a task is done or not.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"doneAt": {
|
||||
"description": "The unix timestamp when a task was marked as done.",
|
||||
"type": "integer"
|
||||
},
|
||||
"dueDate": {
|
||||
"description": "A unix timestamp when the task is due.",
|
||||
"type": "integer"
|
||||
|
@ -3905,6 +3878,10 @@ var doc = `{
|
|||
"description": "Whether a task is done or not.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"doneAt": {
|
||||
"description": "The unix timestamp when a task was marked as done.",
|
||||
"type": "integer"
|
||||
},
|
||||
"dueDate": {
|
||||
"description": "A unix timestamp when the task is due.",
|
||||
"type": "integer"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"description": "This is the documentation for the [Vikunja](http://vikunja.io) API. Vikunja is a cross-plattform Todo-application with a lot of features, such as sharing lists with users or teams. \u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e\n# Authorization\n**JWT-Auth:** Main authorization method, used for most of the requests. Needs ` + \"`\" + `Authorization: Bearer \u003cjwt-token\u003e` + \"`\" + `-header to authenticate successfully.\n\n**BasicAuth:** Only used when requesting tasks via caldav.\n\u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e",
|
||||
"description": "\u003c!-- ReDoc-Inject: \u003csecurity-definitions\u003e --\u003e",
|
||||
"title": "Vikunja API",
|
||||
"contact": {
|
||||
"name": "General Vikunja contact",
|
||||
|
@ -378,7 +378,7 @@
|
|||
"JWTKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a team by its ID.",
|
||||
"description": "Returns a list by its ID.",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
|
@ -386,13 +386,13 @@
|
|||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"team"
|
||||
"list"
|
||||
],
|
||||
"summary": "Gets one team",
|
||||
"summary": "Gets one list",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Team ID",
|
||||
"description": "List ID",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
|
@ -400,14 +400,14 @@
|
|||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The team",
|
||||
"description": "The list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/models.Team"
|
||||
"$ref": "#/definitions/models.List"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "The user does not have access to the team",
|
||||
"description": "The user does not have access to the list",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
||||
|
@ -2368,37 +2368,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/tasks/caldav": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"BasicAuth": []
|
||||
}
|
||||
],
|
||||
"description": "Returns a calDAV-parsable format with all tasks as calendar events. Only returns tasks with a due date. Also creates reminders when the task has one.",
|
||||
"produces": [
|
||||
"text/plain"
|
||||
],
|
||||
"tags": [
|
||||
"task"
|
||||
],
|
||||
"summary": "CalDAV-readable format with all tasks as calendar events.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The caldav events.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Unauthorized.",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/tasks/{id}": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -3674,6 +3643,10 @@
|
|||
"description": "Whether a task is done or not.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"doneAt": {
|
||||
"description": "The unix timestamp when a task was marked as done.",
|
||||
"type": "integer"
|
||||
},
|
||||
"dueDate": {
|
||||
"description": "A unix timestamp when the task is due.",
|
||||
"type": "integer"
|
||||
|
@ -3891,6 +3864,10 @@
|
|||
"description": "Whether a task is done or not.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"doneAt": {
|
||||
"description": "The unix timestamp when a task was marked as done.",
|
||||
"type": "integer"
|
||||
},
|
||||
"dueDate": {
|
||||
"description": "A unix timestamp when the task is due.",
|
||||
"type": "integer"
|
||||
|
|
|
@ -51,6 +51,9 @@ definitions:
|
|||
done:
|
||||
description: Whether a task is done or not.
|
||||
type: boolean
|
||||
doneAt:
|
||||
description: The unix timestamp when a task was marked as done.
|
||||
type: integer
|
||||
dueDate:
|
||||
description: A unix timestamp when the task is due.
|
||||
type: integer
|
||||
|
@ -223,6 +226,9 @@ definitions:
|
|||
done:
|
||||
description: Whether a task is done or not.
|
||||
type: boolean
|
||||
doneAt:
|
||||
description: The unix timestamp when a task was marked as done.
|
||||
type: integer
|
||||
dueDate:
|
||||
description: A unix timestamp when the task is due.
|
||||
type: integer
|
||||
|
@ -645,13 +651,7 @@ info:
|
|||
email: hello@vikunja.io
|
||||
name: General Vikunja contact
|
||||
url: http://vikunja.io/en/contact/
|
||||
description: |-
|
||||
This is the documentation for the [Vikunja](http://vikunja.io) API. Vikunja is a cross-plattform Todo-application with a lot of features, such as sharing lists with users or teams. <!-- ReDoc-Inject: <security-definitions> -->
|
||||
# Authorization
|
||||
**JWT-Auth:** Main authorization method, used for most of the requests. Needs ` + "`" + `Authorization: Bearer <jwt-token>` + "`" + `-header to authenticate successfully.
|
||||
|
||||
**BasicAuth:** Only used when requesting tasks via caldav.
|
||||
<!-- ReDoc-Inject: <security-definitions> -->
|
||||
description: '<!-- ReDoc-Inject: <security-definitions> -->'
|
||||
license:
|
||||
name: GPLv3
|
||||
url: http://code.vikunja.io/api/src/branch/master/LICENSE
|
||||
|
@ -935,9 +935,9 @@ paths:
|
|||
get:
|
||||
consumes:
|
||||
- application/json
|
||||
description: Returns a team by its ID.
|
||||
description: Returns a list by its ID.
|
||||
parameters:
|
||||
- description: Team ID
|
||||
- description: List ID
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
|
@ -946,12 +946,12 @@ paths:
|
|||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: The team
|
||||
description: The list
|
||||
schema:
|
||||
$ref: '#/definitions/models.Team'
|
||||
$ref: '#/definitions/models.List'
|
||||
type: object
|
||||
"403":
|
||||
description: The user does not have access to the team
|
||||
description: The user does not have access to the list
|
||||
schema:
|
||||
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||
type: object
|
||||
|
@ -962,9 +962,9 @@ paths:
|
|||
type: object
|
||||
security:
|
||||
- JWTKeyAuth: []
|
||||
summary: Gets one team
|
||||
summary: Gets one list
|
||||
tags:
|
||||
- team
|
||||
- list
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
|
@ -2638,27 +2638,6 @@ paths:
|
|||
summary: Update a bunch of tasks at once
|
||||
tags:
|
||||
- task
|
||||
/tasks/caldav:
|
||||
get:
|
||||
description: Returns a calDAV-parsable format with all tasks as calendar events.
|
||||
Only returns tasks with a due date. Also creates reminders when the task has
|
||||
one.
|
||||
produces:
|
||||
- text/plain
|
||||
responses:
|
||||
"200":
|
||||
description: The caldav events.
|
||||
schema:
|
||||
type: string
|
||||
"403":
|
||||
description: Unauthorized.
|
||||
schema:
|
||||
type: string
|
||||
security:
|
||||
- BasicAuth: []
|
||||
summary: CalDAV-readable format with all tasks as calendar events.
|
||||
tags:
|
||||
- task
|
||||
/teams:
|
||||
get:
|
||||
consumes:
|
||||
|
|
14
vendor/github.com/beevik/etree/.travis.yml
generated
vendored
Normal file
14
vendor/github.com/beevik/etree/.travis.yml
generated
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
language: go
|
||||
sudo: false
|
||||
|
||||
go:
|
||||
- 1.11.x
|
||||
- tip
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
script:
|
||||
- go vet ./...
|
||||
- go test -v ./...
|
10
vendor/github.com/beevik/etree/CONTRIBUTORS
generated
vendored
Normal file
10
vendor/github.com/beevik/etree/CONTRIBUTORS
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
Brett Vickers (beevik)
|
||||
Felix Geisendörfer (felixge)
|
||||
Kamil Kisiel (kisielk)
|
||||
Graham King (grahamking)
|
||||
Matt Smith (ma314smith)
|
||||
Michal Jemala (michaljemala)
|
||||
Nicolas Piganeau (npiganeau)
|
||||
Chris Brown (ccbrown)
|
||||
Earncef Sequeira (earncef)
|
||||
Gabriel de Labachelerie (wuzuf)
|
24
vendor/github.com/beevik/etree/LICENSE
generated
vendored
Normal file
24
vendor/github.com/beevik/etree/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
Copyright 2015-2019 Brett Vickers. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions
|
||||
are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDER OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
||||
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
205
vendor/github.com/beevik/etree/README.md
generated
vendored
Normal file
205
vendor/github.com/beevik/etree/README.md
generated
vendored
Normal file
|
@ -0,0 +1,205 @@
|
|||
[![Build Status](https://travis-ci.org/beevik/etree.svg?branch=master)](https://travis-ci.org/beevik/etree)
|
||||
[![GoDoc](https://godoc.org/github.com/beevik/etree?status.svg)](https://godoc.org/github.com/beevik/etree)
|
||||
|
||||
etree
|
||||
=====
|
||||
|
||||
The etree package is a lightweight, pure go package that expresses XML in
|
||||
the form of an element tree. Its design was inspired by the Python
|
||||
[ElementTree](http://docs.python.org/2/library/xml.etree.elementtree.html)
|
||||
module.
|
||||
|
||||
Some of the package's capabilities and features:
|
||||
|
||||
* Represents XML documents as trees of elements for easy traversal.
|
||||
* Imports, serializes, modifies or creates XML documents from scratch.
|
||||
* Writes and reads XML to/from files, byte slices, strings and io interfaces.
|
||||
* Performs simple or complex searches with lightweight XPath-like query APIs.
|
||||
* Auto-indents XML using spaces or tabs for better readability.
|
||||
* Implemented in pure go; depends only on standard go libraries.
|
||||
* Built on top of the go [encoding/xml](http://golang.org/pkg/encoding/xml)
|
||||
package.
|
||||
|
||||
### Creating an XML document
|
||||
|
||||
The following example creates an XML document from scratch using the etree
|
||||
package and outputs its indented contents to stdout.
|
||||
```go
|
||||
doc := etree.NewDocument()
|
||||
doc.CreateProcInst("xml", `version="1.0" encoding="UTF-8"`)
|
||||
doc.CreateProcInst("xml-stylesheet", `type="text/xsl" href="style.xsl"`)
|
||||
|
||||
people := doc.CreateElement("People")
|
||||
people.CreateComment("These are all known people")
|
||||
|
||||
jon := people.CreateElement("Person")
|
||||
jon.CreateAttr("name", "Jon")
|
||||
|
||||
sally := people.CreateElement("Person")
|
||||
sally.CreateAttr("name", "Sally")
|
||||
|
||||
doc.Indent(2)
|
||||
doc.WriteTo(os.Stdout)
|
||||
```
|
||||
|
||||
Output:
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml-stylesheet type="text/xsl" href="style.xsl"?>
|
||||
<People>
|
||||
<!--These are all known people-->
|
||||
<Person name="Jon"/>
|
||||
<Person name="Sally"/>
|
||||
</People>
|
||||
```
|
||||
|
||||
### Reading an XML file
|
||||
|
||||
Suppose you have a file on disk called `bookstore.xml` containing the
|
||||
following data:
|
||||
|
||||
```xml
|
||||
<bookstore xmlns:p="urn:schemas-books-com:prices">
|
||||
|
||||
<book category="COOKING">
|
||||
<title lang="en">Everyday Italian</title>
|
||||
<author>Giada De Laurentiis</author>
|
||||
<year>2005</year>
|
||||
<p:price>30.00</p:price>
|
||||
</book>
|
||||
|
||||
<book category="CHILDREN">
|
||||
<title lang="en">Harry Potter</title>
|
||||
<author>J K. Rowling</author>
|
||||
<year>2005</year>
|
||||
<p:price>29.99</p:price>
|
||||
</book>
|
||||
|
||||
<book category="WEB">
|
||||
<title lang="en">XQuery Kick Start</title>
|
||||
<author>James McGovern</author>
|
||||
<author>Per Bothner</author>
|
||||
<author>Kurt Cagle</author>
|
||||
<author>James Linn</author>
|
||||
<author>Vaidyanathan Nagarajan</author>
|
||||
<year>2003</year>
|
||||
<p:price>49.99</p:price>
|
||||
</book>
|
||||
|
||||
<book category="WEB">
|
||||
<title lang="en">Learning XML</title>
|
||||
<author>Erik T. Ray</author>
|
||||
<year>2003</year>
|
||||
<p:price>39.95</p:price>
|
||||
</book>
|
||||
|
||||
</bookstore>
|
||||
```
|
||||
|
||||
This code reads the file's contents into an etree document.
|
||||
```go
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromFile("bookstore.xml"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
```
|
||||
|
||||
You can also read XML from a string, a byte slice, or an `io.Reader`.
|
||||
|
||||
### Processing elements and attributes
|
||||
|
||||
This example illustrates several ways to access elements and attributes using
|
||||
etree selection queries.
|
||||
```go
|
||||
root := doc.SelectElement("bookstore")
|
||||
fmt.Println("ROOT element:", root.Tag)
|
||||
|
||||
for _, book := range root.SelectElements("book") {
|
||||
fmt.Println("CHILD element:", book.Tag)
|
||||
if title := book.SelectElement("title"); title != nil {
|
||||
lang := title.SelectAttrValue("lang", "unknown")
|
||||
fmt.Printf(" TITLE: %s (%s)\n", title.Text(), lang)
|
||||
}
|
||||
for _, attr := range book.Attr {
|
||||
fmt.Printf(" ATTR: %s=%s\n", attr.Key, attr.Value)
|
||||
}
|
||||
}
|
||||
```
|
||||
Output:
|
||||
```
|
||||
ROOT element: bookstore
|
||||
CHILD element: book
|
||||
TITLE: Everyday Italian (en)
|
||||
ATTR: category=COOKING
|
||||
CHILD element: book
|
||||
TITLE: Harry Potter (en)
|
||||
ATTR: category=CHILDREN
|
||||
CHILD element: book
|
||||
TITLE: XQuery Kick Start (en)
|
||||
ATTR: category=WEB
|
||||
CHILD element: book
|
||||
TITLE: Learning XML (en)
|
||||
ATTR: category=WEB
|
||||
```
|
||||
|
||||
### Path queries
|
||||
|
||||
This example uses etree's path functions to select all book titles that fall
|
||||
into the category of 'WEB'. The double-slash prefix in the path causes the
|
||||
search for book elements to occur recursively; book elements may appear at any
|
||||
level of the XML hierarchy.
|
||||
```go
|
||||
for _, t := range doc.FindElements("//book[@category='WEB']/title") {
|
||||
fmt.Println("Title:", t.Text())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
Title: XQuery Kick Start
|
||||
Title: Learning XML
|
||||
```
|
||||
|
||||
This example finds the first book element under the root bookstore element and
|
||||
outputs the tag and text of each of its child elements.
|
||||
```go
|
||||
for _, e := range doc.FindElements("./bookstore/book[1]/*") {
|
||||
fmt.Printf("%s: %s\n", e.Tag, e.Text())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
title: Everyday Italian
|
||||
author: Giada De Laurentiis
|
||||
year: 2005
|
||||
price: 30.00
|
||||
```
|
||||
|
||||
This example finds all books with a price of 49.99 and outputs their titles.
|
||||
```go
|
||||
path := etree.MustCompilePath("./bookstore/book[p:price='49.99']/title")
|
||||
for _, e := range doc.FindElementsPath(path) {
|
||||
fmt.Println(e.Text())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
```
|
||||
XQuery Kick Start
|
||||
```
|
||||
|
||||
Note that this example uses the FindElementsPath function, which takes as an
|
||||
argument a pre-compiled path object. Use precompiled paths when you plan to
|
||||
search with the same path more than once.
|
||||
|
||||
### Other features
|
||||
|
||||
These are just a few examples of the things the etree package can do. See the
|
||||
[documentation](http://godoc.org/github.com/beevik/etree) for a complete
|
||||
description of its capabilities.
|
||||
|
||||
### Contributing
|
||||
|
||||
This project accepts contributions. Just fork the repo and submit a pull
|
||||
request!
|
109
vendor/github.com/beevik/etree/RELEASE_NOTES.md
generated
vendored
Normal file
109
vendor/github.com/beevik/etree/RELEASE_NOTES.md
generated
vendored
Normal file
|
@ -0,0 +1,109 @@
|
|||
Release v1.1.0
|
||||
==============
|
||||
|
||||
**New Features**
|
||||
|
||||
* New attribute helpers.
|
||||
* Added the `Element.SortAttrs` method, which lexicographically sorts an
|
||||
element's attributes by key.
|
||||
* New `ReadSettings` properties.
|
||||
* Added `Entity` for the support of custom entity maps.
|
||||
* New `WriteSettings` properties.
|
||||
* Added `UseCRLF` to allow the output of CR-LF newlines instead of the
|
||||
default LF newlines. This is useful on Windows systems.
|
||||
* Additional support for text and CDATA sections.
|
||||
* The `Element.Text` method now returns the concatenation of all consecutive
|
||||
character data tokens immediately following an element's opening tag.
|
||||
* Added `Element.SetCData` to replace the character data immediately
|
||||
following an element's opening tag with a CDATA section.
|
||||
* Added `Element.CreateCData` to create and add a CDATA section child
|
||||
`CharData` token to an element.
|
||||
* Added `Element.CreateText` to create and add a child text `CharData` token
|
||||
to an element.
|
||||
* Added `NewCData` to create a parentless CDATA section `CharData` token.
|
||||
* Added `NewText` to create a parentless text `CharData`
|
||||
token.
|
||||
* Added `CharData.IsCData` to detect if the token contains a CDATA section.
|
||||
* Added `CharData.IsWhitespace` to detect if the token contains whitespace
|
||||
inserted by one of the document Indent functions.
|
||||
* Modified `Element.SetText` so that it replaces a run of consecutive
|
||||
character data tokens following the element's opening tag (instead of just
|
||||
the first one).
|
||||
* New "tail text" support.
|
||||
* Added the `Element.Tail` method, which returns the text immediately
|
||||
following an element's closing tag.
|
||||
* Added the `Element.SetTail` method, which modifies the text immediately
|
||||
following an element's closing tag.
|
||||
* New element child insertion and removal methods.
|
||||
* Added the `Element.InsertChildAt` method, which inserts a new child token
|
||||
before the specified child token index.
|
||||
* Added the `Element.RemoveChildAt` method, which removes the child token at
|
||||
the specified child token index.
|
||||
* New element and attribute queries.
|
||||
* Added the `Element.Index` method, which returns the element's index within
|
||||
its parent element's child token list.
|
||||
* Added the `Element.NamespaceURI` method to return the namespace URI
|
||||
associated with an element.
|
||||
* Added the `Attr.NamespaceURI` method to return the namespace URI
|
||||
associated with an element.
|
||||
* Added the `Attr.Element` method to return the element that an attribute
|
||||
belongs to.
|
||||
* New Path filter functions.
|
||||
* Added `[local-name()='val']` to keep elements whose unprefixed tag matches
|
||||
the desired value.
|
||||
* Added `[name()='val']` to keep elements whose full tag matches the desired
|
||||
value.
|
||||
* Added `[namespace-prefix()='val']` to keep elements whose namespace prefix
|
||||
matches the desired value.
|
||||
* Added `[namespace-uri()='val']` to keep elements whose namespace URI
|
||||
matches the desired value.
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* A default XML `CharSetReader` is now used to prevent failed parsing of XML
|
||||
documents using certain encodings.
|
||||
([Issue](https://github.com/beevik/etree/issues/53)).
|
||||
* All characters are now properly escaped according to XML parsing rules.
|
||||
([Issue](https://github.com/beevik/etree/issues/55)).
|
||||
* The `Document.Indent` and `Document.IndentTabs` functions no longer insert
|
||||
empty string `CharData` tokens.
|
||||
|
||||
**Deprecated**
|
||||
|
||||
* `Element`
|
||||
* The `InsertChild` method is deprecated. Use `InsertChildAt` instead.
|
||||
* The `CreateCharData` method is deprecated. Use `CreateText` instead.
|
||||
* `CharData`
|
||||
* The `NewCharData` method is deprecated. Use `NewText` instead.
|
||||
|
||||
|
||||
Release v1.0.1
|
||||
==============
|
||||
|
||||
**Changes**
|
||||
|
||||
* Added support for absolute etree Path queries. An absolute path begins with
|
||||
`/` or `//` and begins its search from the element's document root.
|
||||
* Added [`GetPath`](https://godoc.org/github.com/beevik/etree#Element.GetPath)
|
||||
and [`GetRelativePath`](https://godoc.org/github.com/beevik/etree#Element.GetRelativePath)
|
||||
functions to the [`Element`](https://godoc.org/github.com/beevik/etree#Element)
|
||||
type.
|
||||
|
||||
**Breaking changes**
|
||||
|
||||
* A path starting with `//` is now interpreted as an absolute path.
|
||||
Previously, it was interpreted as a relative path starting from the element
|
||||
whose
|
||||
[`FindElement`](https://godoc.org/github.com/beevik/etree#Element.FindElement)
|
||||
method was called. To remain compatible with this release, all paths
|
||||
prefixed with `//` should be prefixed with `.//` when called from any
|
||||
element other than the document's root.
|
||||
* [**edit 2/1/2019**]: Minor releases should not contain breaking changes.
|
||||
Even though this breaking change was very minor, it was a mistake to include
|
||||
it in this minor release. In the future, all breaking changes will be
|
||||
limited to major releases (e.g., version 2.0.0).
|
||||
|
||||
Release v1.0.0
|
||||
==============
|
||||
|
||||
Initial release.
|
1453
vendor/github.com/beevik/etree/etree.go
generated
vendored
Normal file
1453
vendor/github.com/beevik/etree/etree.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
276
vendor/github.com/beevik/etree/helpers.go
generated
vendored
Normal file
276
vendor/github.com/beevik/etree/helpers.go
generated
vendored
Normal file
|
@ -0,0 +1,276 @@
|
|||
// Copyright 2015-2019 Brett Vickers.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package etree
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A simple stack
|
||||
type stack struct {
|
||||
data []interface{}
|
||||
}
|
||||
|
||||
func (s *stack) empty() bool {
|
||||
return len(s.data) == 0
|
||||
}
|
||||
|
||||
func (s *stack) push(value interface{}) {
|
||||
s.data = append(s.data, value)
|
||||
}
|
||||
|
||||
func (s *stack) pop() interface{} {
|
||||
value := s.data[len(s.data)-1]
|
||||
s.data[len(s.data)-1] = nil
|
||||
s.data = s.data[:len(s.data)-1]
|
||||
return value
|
||||
}
|
||||
|
||||
func (s *stack) peek() interface{} {
|
||||
return s.data[len(s.data)-1]
|
||||
}
|
||||
|
||||
// A fifo is a simple first-in-first-out queue.
|
||||
type fifo struct {
|
||||
data []interface{}
|
||||
head, tail int
|
||||
}
|
||||
|
||||
func (f *fifo) add(value interface{}) {
|
||||
if f.len()+1 >= len(f.data) {
|
||||
f.grow()
|
||||
}
|
||||
f.data[f.tail] = value
|
||||
if f.tail++; f.tail == len(f.data) {
|
||||
f.tail = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fifo) remove() interface{} {
|
||||
value := f.data[f.head]
|
||||
f.data[f.head] = nil
|
||||
if f.head++; f.head == len(f.data) {
|
||||
f.head = 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func (f *fifo) len() int {
|
||||
if f.tail >= f.head {
|
||||
return f.tail - f.head
|
||||
}
|
||||
return len(f.data) - f.head + f.tail
|
||||
}
|
||||
|
||||
func (f *fifo) grow() {
|
||||
c := len(f.data) * 2
|
||||
if c == 0 {
|
||||
c = 4
|
||||
}
|
||||
buf, count := make([]interface{}, c), f.len()
|
||||
if f.tail >= f.head {
|
||||
copy(buf[0:count], f.data[f.head:f.tail])
|
||||
} else {
|
||||
hindex := len(f.data) - f.head
|
||||
copy(buf[0:hindex], f.data[f.head:])
|
||||
copy(buf[hindex:count], f.data[:f.tail])
|
||||
}
|
||||
f.data, f.head, f.tail = buf, 0, count
|
||||
}
|
||||
|
||||
// countReader implements a proxy reader that counts the number of
|
||||
// bytes read from its encapsulated reader.
|
||||
type countReader struct {
|
||||
r io.Reader
|
||||
bytes int64
|
||||
}
|
||||
|
||||
func newCountReader(r io.Reader) *countReader {
|
||||
return &countReader{r: r}
|
||||
}
|
||||
|
||||
func (cr *countReader) Read(p []byte) (n int, err error) {
|
||||
b, err := cr.r.Read(p)
|
||||
cr.bytes += int64(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// countWriter implements a proxy writer that counts the number of
|
||||
// bytes written by its encapsulated writer.
|
||||
type countWriter struct {
|
||||
w io.Writer
|
||||
bytes int64
|
||||
}
|
||||
|
||||
func newCountWriter(w io.Writer) *countWriter {
|
||||
return &countWriter{w: w}
|
||||
}
|
||||
|
||||
func (cw *countWriter) Write(p []byte) (n int, err error) {
|
||||
b, err := cw.w.Write(p)
|
||||
cw.bytes += int64(b)
|
||||
return b, err
|
||||
}
|
||||
|
||||
// isWhitespace returns true if the byte slice contains only
|
||||
// whitespace characters.
|
||||
func isWhitespace(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if c := s[i]; c != ' ' && c != '\t' && c != '\n' && c != '\r' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// spaceMatch returns true if namespace a is the empty string
|
||||
// or if namespace a equals namespace b.
|
||||
func spaceMatch(a, b string) bool {
|
||||
switch {
|
||||
case a == "":
|
||||
return true
|
||||
default:
|
||||
return a == b
|
||||
}
|
||||
}
|
||||
|
||||
// spaceDecompose breaks a namespace:tag identifier at the ':'
|
||||
// and returns the two parts.
|
||||
func spaceDecompose(str string) (space, key string) {
|
||||
colon := strings.IndexByte(str, ':')
|
||||
if colon == -1 {
|
||||
return "", str
|
||||
}
|
||||
return str[:colon], str[colon+1:]
|
||||
}
|
||||
|
||||
// Strings used by indentCRLF and indentLF
|
||||
const (
|
||||
indentSpaces = "\r\n "
|
||||
indentTabs = "\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"
|
||||
)
|
||||
|
||||
// indentCRLF returns a CRLF newline followed by n copies of the first
|
||||
// non-CRLF character in the source string.
|
||||
func indentCRLF(n int, source string) string {
|
||||
switch {
|
||||
case n < 0:
|
||||
return source[:2]
|
||||
case n < len(source)-1:
|
||||
return source[:n+2]
|
||||
default:
|
||||
return source + strings.Repeat(source[2:3], n-len(source)+2)
|
||||
}
|
||||
}
|
||||
|
||||
// indentLF returns a LF newline followed by n copies of the first non-LF
|
||||
// character in the source string.
|
||||
func indentLF(n int, source string) string {
|
||||
switch {
|
||||
case n < 0:
|
||||
return source[1:2]
|
||||
case n < len(source)-1:
|
||||
return source[1 : n+2]
|
||||
default:
|
||||
return source[1:] + strings.Repeat(source[2:3], n-len(source)+2)
|
||||
}
|
||||
}
|
||||
|
||||
// nextIndex returns the index of the next occurrence of sep in s,
|
||||
// starting from offset. It returns -1 if the sep string is not found.
|
||||
func nextIndex(s, sep string, offset int) int {
|
||||
switch i := strings.Index(s[offset:], sep); i {
|
||||
case -1:
|
||||
return -1
|
||||
default:
|
||||
return offset + i
|
||||
}
|
||||
}
|
||||
|
||||
// isInteger returns true if the string s contains an integer.
|
||||
func isInteger(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if (s[i] < '0' || s[i] > '9') && !(i == 0 && s[i] == '-') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type escapeMode byte
|
||||
|
||||
const (
|
||||
escapeNormal escapeMode = iota
|
||||
escapeCanonicalText
|
||||
escapeCanonicalAttr
|
||||
)
|
||||
|
||||
// escapeString writes an escaped version of a string to the writer.
|
||||
func escapeString(w *bufio.Writer, s string, m escapeMode) {
|
||||
var esc []byte
|
||||
last := 0
|
||||
for i := 0; i < len(s); {
|
||||
r, width := utf8.DecodeRuneInString(s[i:])
|
||||
i += width
|
||||
switch r {
|
||||
case '&':
|
||||
esc = []byte("&")
|
||||
case '<':
|
||||
esc = []byte("<")
|
||||
case '>':
|
||||
if m == escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte(">")
|
||||
case '\'':
|
||||
if m != escapeNormal {
|
||||
continue
|
||||
}
|
||||
esc = []byte("'")
|
||||
case '"':
|
||||
if m == escapeCanonicalText {
|
||||
continue
|
||||
}
|
||||
esc = []byte(""")
|
||||
case '\t':
|
||||
if m != escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte("	")
|
||||
case '\n':
|
||||
if m != escapeCanonicalAttr {
|
||||
continue
|
||||
}
|
||||
esc = []byte("
")
|
||||
case '\r':
|
||||
if m == escapeNormal {
|
||||
continue
|
||||
}
|
||||
esc = []byte("
")
|
||||
default:
|
||||
if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) {
|
||||
esc = []byte("\uFFFD")
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
w.WriteString(s[last : i-width])
|
||||
w.Write(esc)
|
||||
last = i
|
||||
}
|
||||
w.WriteString(s[last:])
|
||||
}
|
||||
|
||||
func isInCharacterRange(r rune) bool {
|
||||
return r == 0x09 ||
|
||||
r == 0x0A ||
|
||||
r == 0x0D ||
|
||||
r >= 0x20 && r <= 0xD7FF ||
|
||||
r >= 0xE000 && r <= 0xFFFD ||
|
||||
r >= 0x10000 && r <= 0x10FFFF
|
||||
}
|
582
vendor/github.com/beevik/etree/path.go
generated
vendored
Normal file
582
vendor/github.com/beevik/etree/path.go
generated
vendored
Normal file
|
@ -0,0 +1,582 @@
|
|||
// Copyright 2015-2019 Brett Vickers.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package etree
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/*
|
||||
A Path is a string that represents a search path through an etree starting
|
||||
from the document root or an arbitrary element. Paths are used with the
|
||||
Element object's Find* methods to locate and return desired elements.
|
||||
|
||||
A Path consists of a series of slash-separated "selectors", each of which may
|
||||
be modified by one or more bracket-enclosed "filters". Selectors are used to
|
||||
traverse the etree from element to element, while filters are used to narrow
|
||||
the list of candidate elements at each node.
|
||||
|
||||
Although etree Path strings are similar to XPath strings
|
||||
(https://www.w3.org/TR/1999/REC-xpath-19991116/), they have a more limited set
|
||||
of selectors and filtering options.
|
||||
|
||||
The following selectors are supported by etree Path strings:
|
||||
|
||||
. Select the current element.
|
||||
.. Select the parent of the current element.
|
||||
* Select all child elements of the current element.
|
||||
/ Select the root element when used at the start of a path.
|
||||
// Select all descendants of the current element.
|
||||
tag Select all child elements with a name matching the tag.
|
||||
|
||||
The following basic filters are supported by etree Path strings:
|
||||
|
||||
[@attrib] Keep elements with an attribute named attrib.
|
||||
[@attrib='val'] Keep elements with an attribute named attrib and value matching val.
|
||||
[tag] Keep elements with a child element named tag.
|
||||
[tag='val'] Keep elements with a child element named tag and text matching val.
|
||||
[n] Keep the n-th element, where n is a numeric index starting from 1.
|
||||
|
||||
The following function filters are also supported:
|
||||
|
||||
[text()] Keep elements with non-empty text.
|
||||
[text()='val'] Keep elements whose text matches val.
|
||||
[local-name()='val'] Keep elements whose un-prefixed tag matches val.
|
||||
[name()='val'] Keep elements whose full tag exactly matches val.
|
||||
[namespace-prefix()='val'] Keep elements whose namespace prefix matches val.
|
||||
[namespace-uri()='val'] Keep elements whose namespace URI matches val.
|
||||
|
||||
Here are some examples of Path strings:
|
||||
|
||||
- Select the bookstore child element of the root element:
|
||||
/bookstore
|
||||
|
||||
- Beginning from the root element, select the title elements of all
|
||||
descendant book elements having a 'category' attribute of 'WEB':
|
||||
//book[@category='WEB']/title
|
||||
|
||||
- Beginning from the current element, select the first descendant
|
||||
book element with a title child element containing the text 'Great
|
||||
Expectations':
|
||||
.//book[title='Great Expectations'][1]
|
||||
|
||||
- Beginning from the current element, select all child elements of
|
||||
book elements with an attribute 'language' set to 'english':
|
||||
./book/*[@language='english']
|
||||
|
||||
- Beginning from the current element, select all child elements of
|
||||
book elements containing the text 'special':
|
||||
./book/*[text()='special']
|
||||
|
||||
- Beginning from the current element, select all descendant book
|
||||
elements whose title child element has a 'language' attribute of 'french':
|
||||
.//book/title[@language='french']/..
|
||||
|
||||
- Beginning from the current element, select all book elements
|
||||
belonging to the http://www.w3.org/TR/html4/ namespace:
|
||||
.//book[namespace-uri()='http://www.w3.org/TR/html4/']
|
||||
|
||||
*/
|
||||
type Path struct {
|
||||
segments []segment
|
||||
}
|
||||
|
||||
// ErrPath is returned by path functions when an invalid etree path is provided.
|
||||
type ErrPath string
|
||||
|
||||
// Error returns the string describing a path error.
|
||||
func (err ErrPath) Error() string {
|
||||
return "etree: " + string(err)
|
||||
}
|
||||
|
||||
// CompilePath creates an optimized version of an XPath-like string that
|
||||
// can be used to query elements in an element tree.
|
||||
func CompilePath(path string) (Path, error) {
|
||||
var comp compiler
|
||||
segments := comp.parsePath(path)
|
||||
if comp.err != ErrPath("") {
|
||||
return Path{nil}, comp.err
|
||||
}
|
||||
return Path{segments}, nil
|
||||
}
|
||||
|
||||
// MustCompilePath creates an optimized version of an XPath-like string that
|
||||
// can be used to query elements in an element tree. Panics if an error
|
||||
// occurs. Use this function to create Paths when you know the path is
|
||||
// valid (i.e., if it's hard-coded).
|
||||
func MustCompilePath(path string) Path {
|
||||
p, err := CompilePath(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// A segment is a portion of a path between "/" characters.
|
||||
// It contains one selector and zero or more [filters].
|
||||
type segment struct {
|
||||
sel selector
|
||||
filters []filter
|
||||
}
|
||||
|
||||
func (seg *segment) apply(e *Element, p *pather) {
|
||||
seg.sel.apply(e, p)
|
||||
for _, f := range seg.filters {
|
||||
f.apply(p)
|
||||
}
|
||||
}
|
||||
|
||||
// A selector selects XML elements for consideration by the
|
||||
// path traversal.
|
||||
type selector interface {
|
||||
apply(e *Element, p *pather)
|
||||
}
|
||||
|
||||
// A filter pares down a list of candidate XML elements based
|
||||
// on a path filter in [brackets].
|
||||
type filter interface {
|
||||
apply(p *pather)
|
||||
}
|
||||
|
||||
// A pather is helper object that traverses an element tree using
|
||||
// a Path object. It collects and deduplicates all elements matching
|
||||
// the path query.
|
||||
type pather struct {
|
||||
queue fifo
|
||||
results []*Element
|
||||
inResults map[*Element]bool
|
||||
candidates []*Element
|
||||
scratch []*Element // used by filters
|
||||
}
|
||||
|
||||
// A node represents an element and the remaining path segments that
|
||||
// should be applied against it by the pather.
|
||||
type node struct {
|
||||
e *Element
|
||||
segments []segment
|
||||
}
|
||||
|
||||
func newPather() *pather {
|
||||
return &pather{
|
||||
results: make([]*Element, 0),
|
||||
inResults: make(map[*Element]bool),
|
||||
candidates: make([]*Element, 0),
|
||||
scratch: make([]*Element, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// traverse follows the path from the element e, collecting
|
||||
// and then returning all elements that match the path's selectors
|
||||
// and filters.
|
||||
func (p *pather) traverse(e *Element, path Path) []*Element {
|
||||
for p.queue.add(node{e, path.segments}); p.queue.len() > 0; {
|
||||
p.eval(p.queue.remove().(node))
|
||||
}
|
||||
return p.results
|
||||
}
|
||||
|
||||
// eval evalutes the current path node by applying the remaining
|
||||
// path's selector rules against the node's element.
|
||||
func (p *pather) eval(n node) {
|
||||
p.candidates = p.candidates[0:0]
|
||||
seg, remain := n.segments[0], n.segments[1:]
|
||||
seg.apply(n.e, p)
|
||||
|
||||
if len(remain) == 0 {
|
||||
for _, c := range p.candidates {
|
||||
if in := p.inResults[c]; !in {
|
||||
p.inResults[c] = true
|
||||
p.results = append(p.results, c)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, c := range p.candidates {
|
||||
p.queue.add(node{c, remain})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A compiler generates a compiled path from a path string.
|
||||
type compiler struct {
|
||||
err ErrPath
|
||||
}
|
||||
|
||||
// parsePath parses an XPath-like string describing a path
|
||||
// through an element tree and returns a slice of segment
|
||||
// descriptors.
|
||||
func (c *compiler) parsePath(path string) []segment {
|
||||
// If path ends with //, fix it
|
||||
if strings.HasSuffix(path, "//") {
|
||||
path = path + "*"
|
||||
}
|
||||
|
||||
var segments []segment
|
||||
|
||||
// Check for an absolute path
|
||||
if strings.HasPrefix(path, "/") {
|
||||
segments = append(segments, segment{new(selectRoot), []filter{}})
|
||||
path = path[1:]
|
||||
}
|
||||
|
||||
// Split path into segments
|
||||
for _, s := range splitPath(path) {
|
||||
segments = append(segments, c.parseSegment(s))
|
||||
if c.err != ErrPath("") {
|
||||
break
|
||||
}
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
func splitPath(path string) []string {
|
||||
pieces := make([]string, 0)
|
||||
start := 0
|
||||
inquote := false
|
||||
for i := 0; i+1 <= len(path); i++ {
|
||||
if path[i] == '\'' {
|
||||
inquote = !inquote
|
||||
} else if path[i] == '/' && !inquote {
|
||||
pieces = append(pieces, path[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
return append(pieces, path[start:])
|
||||
}
|
||||
|
||||
// parseSegment parses a path segment between / characters.
|
||||
func (c *compiler) parseSegment(path string) segment {
|
||||
pieces := strings.Split(path, "[")
|
||||
seg := segment{
|
||||
sel: c.parseSelector(pieces[0]),
|
||||
filters: []filter{},
|
||||
}
|
||||
for i := 1; i < len(pieces); i++ {
|
||||
fpath := pieces[i]
|
||||
if fpath[len(fpath)-1] != ']' {
|
||||
c.err = ErrPath("path has invalid filter [brackets].")
|
||||
break
|
||||
}
|
||||
seg.filters = append(seg.filters, c.parseFilter(fpath[:len(fpath)-1]))
|
||||
}
|
||||
return seg
|
||||
}
|
||||
|
||||
// parseSelector parses a selector at the start of a path segment.
|
||||
func (c *compiler) parseSelector(path string) selector {
|
||||
switch path {
|
||||
case ".":
|
||||
return new(selectSelf)
|
||||
case "..":
|
||||
return new(selectParent)
|
||||
case "*":
|
||||
return new(selectChildren)
|
||||
case "":
|
||||
return new(selectDescendants)
|
||||
default:
|
||||
return newSelectChildrenByTag(path)
|
||||
}
|
||||
}
|
||||
|
||||
var fnTable = map[string]struct {
|
||||
hasFn func(e *Element) bool
|
||||
getValFn func(e *Element) string
|
||||
}{
|
||||
"local-name": {nil, (*Element).name},
|
||||
"name": {nil, (*Element).FullTag},
|
||||
"namespace-prefix": {nil, (*Element).namespacePrefix},
|
||||
"namespace-uri": {nil, (*Element).NamespaceURI},
|
||||
"text": {(*Element).hasText, (*Element).Text},
|
||||
}
|
||||
|
||||
// parseFilter parses a path filter contained within [brackets].
|
||||
func (c *compiler) parseFilter(path string) filter {
|
||||
if len(path) == 0 {
|
||||
c.err = ErrPath("path contains an empty filter expression.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter contains [@attr='val'], [fn()='val'], or [tag='val']?
|
||||
eqindex := strings.Index(path, "='")
|
||||
if eqindex >= 0 {
|
||||
rindex := nextIndex(path, "'", eqindex+2)
|
||||
if rindex != len(path)-1 {
|
||||
c.err = ErrPath("path has mismatched filter quotes.")
|
||||
return nil
|
||||
}
|
||||
|
||||
key := path[:eqindex]
|
||||
value := path[eqindex+2 : rindex]
|
||||
|
||||
switch {
|
||||
case key[0] == '@':
|
||||
return newFilterAttrVal(key[1:], value)
|
||||
case strings.HasSuffix(key, "()"):
|
||||
fn := key[:len(key)-2]
|
||||
if t, ok := fnTable[fn]; ok && t.getValFn != nil {
|
||||
return newFilterFuncVal(t.getValFn, value)
|
||||
}
|
||||
c.err = ErrPath("path has unknown function " + fn)
|
||||
return nil
|
||||
default:
|
||||
return newFilterChildText(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter contains [@attr], [N], [tag] or [fn()]
|
||||
switch {
|
||||
case path[0] == '@':
|
||||
return newFilterAttr(path[1:])
|
||||
case strings.HasSuffix(path, "()"):
|
||||
fn := path[:len(path)-2]
|
||||
if t, ok := fnTable[fn]; ok && t.hasFn != nil {
|
||||
return newFilterFunc(t.hasFn)
|
||||
}
|
||||
c.err = ErrPath("path has unknown function " + fn)
|
||||
return nil
|
||||
case isInteger(path):
|
||||
pos, _ := strconv.Atoi(path)
|
||||
switch {
|
||||
case pos > 0:
|
||||
return newFilterPos(pos - 1)
|
||||
default:
|
||||
return newFilterPos(pos)
|
||||
}
|
||||
default:
|
||||
return newFilterChild(path)
|
||||
}
|
||||
}
|
||||
|
||||
// selectSelf selects the current element into the candidate list.
|
||||
type selectSelf struct{}
|
||||
|
||||
func (s *selectSelf) apply(e *Element, p *pather) {
|
||||
p.candidates = append(p.candidates, e)
|
||||
}
|
||||
|
||||
// selectRoot selects the element's root node.
|
||||
type selectRoot struct{}
|
||||
|
||||
func (s *selectRoot) apply(e *Element, p *pather) {
|
||||
root := e
|
||||
for root.parent != nil {
|
||||
root = root.parent
|
||||
}
|
||||
p.candidates = append(p.candidates, root)
|
||||
}
|
||||
|
||||
// selectParent selects the element's parent into the candidate list.
|
||||
type selectParent struct{}
|
||||
|
||||
func (s *selectParent) apply(e *Element, p *pather) {
|
||||
if e.parent != nil {
|
||||
p.candidates = append(p.candidates, e.parent)
|
||||
}
|
||||
}
|
||||
|
||||
// selectChildren selects the element's child elements into the
|
||||
// candidate list.
|
||||
type selectChildren struct{}
|
||||
|
||||
func (s *selectChildren) apply(e *Element, p *pather) {
|
||||
for _, c := range e.Child {
|
||||
if c, ok := c.(*Element); ok {
|
||||
p.candidates = append(p.candidates, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// selectDescendants selects all descendant child elements
|
||||
// of the element into the candidate list.
|
||||
type selectDescendants struct{}
|
||||
|
||||
func (s *selectDescendants) apply(e *Element, p *pather) {
|
||||
var queue fifo
|
||||
for queue.add(e); queue.len() > 0; {
|
||||
e := queue.remove().(*Element)
|
||||
p.candidates = append(p.candidates, e)
|
||||
for _, c := range e.Child {
|
||||
if c, ok := c.(*Element); ok {
|
||||
queue.add(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// selectChildrenByTag selects into the candidate list all child
|
||||
// elements of the element having the specified tag.
|
||||
type selectChildrenByTag struct {
|
||||
space, tag string
|
||||
}
|
||||
|
||||
func newSelectChildrenByTag(path string) *selectChildrenByTag {
|
||||
s, l := spaceDecompose(path)
|
||||
return &selectChildrenByTag{s, l}
|
||||
}
|
||||
|
||||
func (s *selectChildrenByTag) apply(e *Element, p *pather) {
|
||||
for _, c := range e.Child {
|
||||
if c, ok := c.(*Element); ok && spaceMatch(s.space, c.Space) && s.tag == c.Tag {
|
||||
p.candidates = append(p.candidates, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// filterPos filters the candidate list, keeping only the
|
||||
// candidate at the specified index.
|
||||
type filterPos struct {
|
||||
index int
|
||||
}
|
||||
|
||||
func newFilterPos(pos int) *filterPos {
|
||||
return &filterPos{pos}
|
||||
}
|
||||
|
||||
func (f *filterPos) apply(p *pather) {
|
||||
if f.index >= 0 {
|
||||
if f.index < len(p.candidates) {
|
||||
p.scratch = append(p.scratch, p.candidates[f.index])
|
||||
}
|
||||
} else {
|
||||
if -f.index <= len(p.candidates) {
|
||||
p.scratch = append(p.scratch, p.candidates[len(p.candidates)+f.index])
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterAttr filters the candidate list for elements having
|
||||
// the specified attribute.
|
||||
type filterAttr struct {
|
||||
space, key string
|
||||
}
|
||||
|
||||
func newFilterAttr(str string) *filterAttr {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterAttr{s, l}
|
||||
}
|
||||
|
||||
func (f *filterAttr) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, a := range c.Attr {
|
||||
if spaceMatch(f.space, a.Space) && f.key == a.Key {
|
||||
p.scratch = append(p.scratch, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterAttrVal filters the candidate list for elements having
|
||||
// the specified attribute with the specified value.
|
||||
type filterAttrVal struct {
|
||||
space, key, val string
|
||||
}
|
||||
|
||||
func newFilterAttrVal(str, value string) *filterAttrVal {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterAttrVal{s, l, value}
|
||||
}
|
||||
|
||||
func (f *filterAttrVal) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, a := range c.Attr {
|
||||
if spaceMatch(f.space, a.Space) && f.key == a.Key && f.val == a.Value {
|
||||
p.scratch = append(p.scratch, c)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterFunc filters the candidate list for elements satisfying a custom
|
||||
// boolean function.
|
||||
type filterFunc struct {
|
||||
fn func(e *Element) bool
|
||||
}
|
||||
|
||||
func newFilterFunc(fn func(e *Element) bool) *filterFunc {
|
||||
return &filterFunc{fn}
|
||||
}
|
||||
|
||||
func (f *filterFunc) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
if f.fn(c) {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterFuncVal filters the candidate list for elements containing a value
|
||||
// matching the result of a custom function.
|
||||
type filterFuncVal struct {
|
||||
fn func(e *Element) string
|
||||
val string
|
||||
}
|
||||
|
||||
func newFilterFuncVal(fn func(e *Element) string, value string) *filterFuncVal {
|
||||
return &filterFuncVal{fn, value}
|
||||
}
|
||||
|
||||
func (f *filterFuncVal) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
if f.fn(c) == f.val {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterChild filters the candidate list for elements having
|
||||
// a child element with the specified tag.
|
||||
type filterChild struct {
|
||||
space, tag string
|
||||
}
|
||||
|
||||
func newFilterChild(str string) *filterChild {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterChild{s, l}
|
||||
}
|
||||
|
||||
func (f *filterChild) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, cc := range c.Child {
|
||||
if cc, ok := cc.(*Element); ok &&
|
||||
spaceMatch(f.space, cc.Space) &&
|
||||
f.tag == cc.Tag {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
||||
|
||||
// filterChildText filters the candidate list for elements having
|
||||
// a child element with the specified tag and text.
|
||||
type filterChildText struct {
|
||||
space, tag, text string
|
||||
}
|
||||
|
||||
func newFilterChildText(str, text string) *filterChildText {
|
||||
s, l := spaceDecompose(str)
|
||||
return &filterChildText{s, l, text}
|
||||
}
|
||||
|
||||
func (f *filterChildText) apply(p *pather) {
|
||||
for _, c := range p.candidates {
|
||||
for _, cc := range c.Child {
|
||||
if cc, ok := cc.(*Element); ok &&
|
||||
spaceMatch(f.space, cc.Space) &&
|
||||
f.tag == cc.Tag &&
|
||||
f.text == cc.Text() {
|
||||
p.scratch = append(p.scratch, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
p.candidates, p.scratch = p.scratch, p.candidates[0:0]
|
||||
}
|
3
vendor/github.com/labstack/echo/v4/echo.go
generated
vendored
3
vendor/github.com/labstack/echo/v4/echo.go
generated
vendored
|
@ -170,6 +170,8 @@ const (
|
|||
charsetUTF8 = "charset=UTF-8"
|
||||
// PROPFIND Method can be used on collection and property resources.
|
||||
PROPFIND = "PROPFIND"
|
||||
// REPORT Method can be used to get information about a resource, see rfc 3253
|
||||
REPORT = "REPORT"
|
||||
)
|
||||
|
||||
// Headers
|
||||
|
@ -251,6 +253,7 @@ var (
|
|||
PROPFIND,
|
||||
http.MethodPut,
|
||||
http.MethodTrace,
|
||||
REPORT,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
5
vendor/github.com/labstack/echo/v4/router.go
generated
vendored
5
vendor/github.com/labstack/echo/v4/router.go
generated
vendored
|
@ -33,6 +33,7 @@ type (
|
|||
propfind HandlerFunc
|
||||
put HandlerFunc
|
||||
trace HandlerFunc
|
||||
report HandlerFunc
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -247,6 +248,8 @@ func (n *node) addHandler(method string, h HandlerFunc) {
|
|||
n.methodHandler.put = h
|
||||
case http.MethodTrace:
|
||||
n.methodHandler.trace = h
|
||||
case REPORT:
|
||||
n.methodHandler.report = h
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -272,6 +275,8 @@ func (n *node) findHandler(method string) HandlerFunc {
|
|||
return n.methodHandler.put
|
||||
case http.MethodTrace:
|
||||
return n.methodHandler.trace
|
||||
case REPORT:
|
||||
return n.methodHandler.report
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
17
vendor/github.com/laurent22/ical-go/ical/calendar.go
generated
vendored
Normal file
17
vendor/github.com/laurent22/ical-go/ical/calendar.go
generated
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
package ical
|
||||
|
||||
type Calendar struct {
|
||||
Items []CalendarEvent
|
||||
}
|
||||
|
||||
func (this *Calendar) Serialize() string {
|
||||
serializer := calSerializer{
|
||||
calendar: this,
|
||||
buffer: new(strBuffer),
|
||||
}
|
||||
return serializer.serialize()
|
||||
}
|
||||
|
||||
func (this *Calendar) ToICS() string {
|
||||
return this.Serialize()
|
||||
}
|
50
vendor/github.com/laurent22/ical-go/ical/calendar_event.go
generated
vendored
Normal file
50
vendor/github.com/laurent22/ical-go/ical/calendar_event.go
generated
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
package ical
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type CalendarEvent struct {
|
||||
Id string
|
||||
Summary string
|
||||
Description string
|
||||
Location string
|
||||
CreatedAtUTC *time.Time
|
||||
ModifiedAtUTC *time.Time
|
||||
StartAt *time.Time
|
||||
EndAt *time.Time
|
||||
}
|
||||
|
||||
func (this *CalendarEvent) StartAtUTC() *time.Time {
|
||||
return inUTC(this.StartAt)
|
||||
}
|
||||
|
||||
func (this *CalendarEvent) EndAtUTC() *time.Time {
|
||||
return inUTC(this.EndAt)
|
||||
}
|
||||
|
||||
func (this *CalendarEvent) Serialize() string {
|
||||
buffer := new(strBuffer)
|
||||
return this.serializeWithBuffer(buffer)
|
||||
}
|
||||
|
||||
func (this *CalendarEvent) ToICS() string {
|
||||
return this.Serialize()
|
||||
}
|
||||
|
||||
func (this *CalendarEvent) serializeWithBuffer(buffer *strBuffer) string {
|
||||
serializer := calEventSerializer{
|
||||
event: this,
|
||||
buffer: buffer,
|
||||
}
|
||||
return serializer.serialize()
|
||||
}
|
||||
|
||||
func inUTC(t *time.Time) *time.Time {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tUTC := t.UTC()
|
||||
return &tUTC
|
||||
}
|
18
vendor/github.com/laurent22/ical-go/ical/lib.go
generated
vendored
Normal file
18
vendor/github.com/laurent22/ical-go/ical/lib.go
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
package ical
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
)
|
||||
|
||||
type strBuffer struct {
|
||||
buffer bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *strBuffer) Write(format string, elem ...interface{}) {
|
||||
b.buffer.WriteString(fmt.Sprintf(format, elem...))
|
||||
}
|
||||
|
||||
func (b *strBuffer) String() string {
|
||||
return b.buffer.String()
|
||||
}
|
168
vendor/github.com/laurent22/ical-go/ical/node.go
generated
vendored
Normal file
168
vendor/github.com/laurent22/ical-go/ical/node.go
generated
vendored
Normal file
|
@ -0,0 +1,168 @@
|
|||
package ical
|
||||
|
||||
import (
|
||||
"time"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
Name string
|
||||
Value string
|
||||
Type int // 1 = Object, 0 = Name/Value
|
||||
Parameters map[string]string
|
||||
Children []*Node
|
||||
}
|
||||
|
||||
func (this *Node) ChildrenByName(name string) []*Node {
|
||||
var output []*Node
|
||||
for _, child := range this.Children {
|
||||
if child.Name == name {
|
||||
output = append(output, child)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func (this *Node) ChildByName(name string) *Node {
|
||||
for _, child := range this.Children {
|
||||
if child.Name == name {
|
||||
return child
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Node) PropString(name string, defaultValue string) string {
|
||||
for _, child := range this.Children {
|
||||
if child.Name == name {
|
||||
return child.Value
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (this *Node) PropDate(name string, defaultValue time.Time) time.Time {
|
||||
node := this.ChildByName(name)
|
||||
if node == nil { return defaultValue }
|
||||
tzid := node.Parameter("TZID", "")
|
||||
var output time.Time
|
||||
var err error
|
||||
if tzid != "" {
|
||||
loc, err := time.LoadLocation(tzid)
|
||||
if err != nil { panic(err) }
|
||||
output, err = time.ParseInLocation("20060102T150405", node.Value, loc)
|
||||
} else {
|
||||
output, err = time.Parse("20060102T150405Z", node.Value)
|
||||
}
|
||||
|
||||
if err != nil { panic(err) }
|
||||
return output
|
||||
}
|
||||
|
||||
func (this *Node) PropDuration(name string) time.Duration {
|
||||
durStr := this.PropString(name, "")
|
||||
|
||||
if durStr == "" {
|
||||
return time.Duration(0)
|
||||
}
|
||||
|
||||
durRgx := regexp.MustCompile("PT(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+)S)?")
|
||||
matches := durRgx.FindStringSubmatch(durStr)
|
||||
|
||||
if len(matches) != 4 {
|
||||
return time.Duration(0)
|
||||
}
|
||||
|
||||
strToDuration := func(value string) time.Duration {
|
||||
d := 0
|
||||
if value != "" {
|
||||
d, _ = strconv.Atoi(value)
|
||||
}
|
||||
return time.Duration(d)
|
||||
}
|
||||
|
||||
hours := strToDuration(matches[1])
|
||||
min := strToDuration(matches[2])
|
||||
sec := strToDuration(matches[3])
|
||||
|
||||
return hours * time.Hour + min * time.Minute + sec * time.Second
|
||||
}
|
||||
|
||||
func (this *Node) PropInt(name string, defaultValue int) int {
|
||||
n := this.PropString(name, "")
|
||||
if n == "" { return defaultValue }
|
||||
output, err := strconv.Atoi(n)
|
||||
if err != nil { panic(err) }
|
||||
return output
|
||||
}
|
||||
|
||||
func (this *Node) DigProperty(propPath... string) (string, bool) {
|
||||
return this.dig("prop", propPath...)
|
||||
}
|
||||
|
||||
func (this *Node) Parameter(name string, defaultValue string) string {
|
||||
if len(this.Parameters) <= 0 { return defaultValue }
|
||||
v, ok := this.Parameters[name]
|
||||
if !ok { return defaultValue }
|
||||
return v
|
||||
}
|
||||
|
||||
func (this *Node) DigParameter(paramPath... string) (string, bool) {
|
||||
return this.dig("param", paramPath...)
|
||||
}
|
||||
|
||||
// Digs a value based on a given value path.
|
||||
// valueType: can be "param" or "prop".
|
||||
// valuePath: the path to access the value.
|
||||
// Returns ("", false) when not found or (value, true) when found.
|
||||
//
|
||||
// Example:
|
||||
// dig("param", "VCALENDAR", "VEVENT", "DTEND", "TYPE") -> It will search for "VCALENDAR" node,
|
||||
// then a "VEVENT" node, then a "DTEND" note, then finally the "TYPE" param.
|
||||
func (this *Node) dig(valueType string, valuePath... string) (string, bool) {
|
||||
current := this
|
||||
lastIndex := len(valuePath) - 1
|
||||
for _, v := range valuePath[:lastIndex] {
|
||||
current = current.ChildByName(v)
|
||||
|
||||
if current == nil {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
target := valuePath[lastIndex]
|
||||
|
||||
value := ""
|
||||
if valueType == "param" {
|
||||
value = current.Parameter(target, "")
|
||||
} else if valueType == "prop" {
|
||||
value = current.PropString(target, "")
|
||||
}
|
||||
|
||||
if value == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (this *Node) String() string {
|
||||
s := ""
|
||||
if this.Type == 1 {
|
||||
s += "===== " + this.Name
|
||||
s += "\n"
|
||||
} else {
|
||||
s += this.Name
|
||||
s += ":" + this.Value
|
||||
s += "\n"
|
||||
}
|
||||
for _, child := range this.Children {
|
||||
s += child.String()
|
||||
}
|
||||
if this.Type == 1 {
|
||||
s += "===== /" + this.Name
|
||||
s += "\n"
|
||||
}
|
||||
return s
|
||||
}
|
106
vendor/github.com/laurent22/ical-go/ical/parsers.go
generated
vendored
Normal file
106
vendor/github.com/laurent22/ical-go/ical/parsers.go
generated
vendored
Normal file
|
@ -0,0 +1,106 @@
|
|||
package ical
|
||||
|
||||
import (
|
||||
"log"
|
||||
"errors"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseCalendar(data string) (*Node, error) {
|
||||
r := regexp.MustCompile("([\r|\t| ]*\n[\r|\t| ]*)+")
|
||||
lines := r.Split(strings.TrimSpace(data), -1)
|
||||
node, _, err, _ := parseCalendarNode(lines, 0)
|
||||
|
||||
return node, err
|
||||
}
|
||||
|
||||
func parseCalendarNode(lines []string, lineIndex int) (*Node, bool, error, int) {
|
||||
line := strings.TrimSpace(lines[lineIndex])
|
||||
_ = log.Println
|
||||
colonIndex := strings.Index(line, ":")
|
||||
if colonIndex <= 0 {
|
||||
return nil, false, errors.New("Invalid value/pair: " + line), lineIndex + 1
|
||||
}
|
||||
name := line[0:colonIndex]
|
||||
splitted := strings.Split(name, ";")
|
||||
var parameters map[string]string
|
||||
if len(splitted) >= 2 {
|
||||
name = splitted[0]
|
||||
parameters = make(map[string]string)
|
||||
for i := 1; i < len(splitted); i++ {
|
||||
p := strings.Split(splitted[i], "=")
|
||||
if len(p) != 2 { panic("Invalid parameter format: " + name) }
|
||||
parameters[p[0]] = p[1]
|
||||
}
|
||||
}
|
||||
value := line[colonIndex+1:len(line)]
|
||||
|
||||
if name == "BEGIN" {
|
||||
node := new(Node)
|
||||
node.Name = value
|
||||
node.Type = 1
|
||||
lineIndex = lineIndex + 1
|
||||
for {
|
||||
child, finished, _, newLineIndex := parseCalendarNode(lines, lineIndex)
|
||||
if finished {
|
||||
return node, false, nil, newLineIndex
|
||||
} else {
|
||||
if child != nil {
|
||||
node.Children = append(node.Children, child)
|
||||
}
|
||||
lineIndex = newLineIndex
|
||||
}
|
||||
}
|
||||
} else if name == "END" {
|
||||
return nil, true, nil, lineIndex + 1
|
||||
} else {
|
||||
node := new(Node)
|
||||
node.Name = name
|
||||
if name == "DESCRIPTION" || name == "SUMMARY" {
|
||||
text, newLineIndex := parseTextType(lines, lineIndex)
|
||||
node.Value = text
|
||||
node.Parameters = parameters
|
||||
return node, false, nil, newLineIndex
|
||||
} else {
|
||||
node.Value = value
|
||||
node.Parameters = parameters
|
||||
return node, false, nil, lineIndex + 1
|
||||
}
|
||||
}
|
||||
|
||||
panic("Unreachable")
|
||||
return nil, false, nil, lineIndex + 1
|
||||
}
|
||||
|
||||
func parseTextType(lines []string, lineIndex int) (string, int) {
|
||||
line := lines[lineIndex]
|
||||
colonIndex := strings.Index(line, ":")
|
||||
output := strings.TrimSpace(line[colonIndex+1:len(line)])
|
||||
lineIndex++
|
||||
for {
|
||||
line := lines[lineIndex]
|
||||
if line == "" || line[0] != ' ' {
|
||||
return unescapeTextType(output), lineIndex
|
||||
}
|
||||
output += line[1:len(line)]
|
||||
lineIndex++
|
||||
}
|
||||
return unescapeTextType(output), lineIndex
|
||||
}
|
||||
|
||||
func escapeTextType(input string) string {
|
||||
output := strings.Replace(input, "\\", "\\\\", -1)
|
||||
output = strings.Replace(output, ";", "\\;", -1)
|
||||
output = strings.Replace(output, ",", "\\,", -1)
|
||||
output = strings.Replace(output, "\n", "\\n", -1)
|
||||
return output
|
||||
}
|
||||
|
||||
func unescapeTextType(s string) string {
|
||||
s = strings.Replace(s, "\\;", ";", -1)
|
||||
s = strings.Replace(s, "\\,", ",", -1)
|
||||
s = strings.Replace(s, "\\n", "\n", -1)
|
||||
s = strings.Replace(s, "\\\\", "\\", -1)
|
||||
return s
|
||||
}
|
9
vendor/github.com/laurent22/ical-go/ical/properties.go
generated
vendored
Normal file
9
vendor/github.com/laurent22/ical-go/ical/properties.go
generated
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
package ical
|
||||
|
||||
const (
|
||||
VCALENDAR = "VCALENDAR"
|
||||
VEVENT = "VEVENT"
|
||||
DTSTART = "DTSTART"
|
||||
DTEND = "DTEND"
|
||||
DURATION = "DURATION"
|
||||
)
|
121
vendor/github.com/laurent22/ical-go/ical/serializers.go
generated
vendored
Normal file
121
vendor/github.com/laurent22/ical-go/ical/serializers.go
generated
vendored
Normal file
|
@ -0,0 +1,121 @@
|
|||
package ical
|
||||
|
||||
import (
|
||||
"time"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type calSerializer struct {
|
||||
calendar *Calendar
|
||||
buffer *strBuffer
|
||||
}
|
||||
|
||||
func (this *calSerializer) serialize() string {
|
||||
this.serializeCalendar()
|
||||
return strings.TrimSpace(this.buffer.String())
|
||||
}
|
||||
|
||||
func (this *calSerializer) serializeCalendar() {
|
||||
this.begin()
|
||||
this.version()
|
||||
this.items()
|
||||
this.end()
|
||||
}
|
||||
|
||||
func (this *calSerializer) begin() {
|
||||
this.buffer.Write("BEGIN:VCALENDAR\n")
|
||||
}
|
||||
|
||||
func (this *calSerializer) end() {
|
||||
this.buffer.Write("END:VCALENDAR\n")
|
||||
}
|
||||
|
||||
func (this *calSerializer) version() {
|
||||
this.buffer.Write("VERSION:2.0\n")
|
||||
}
|
||||
|
||||
func (this *calSerializer) items() {
|
||||
for _, item := range this.calendar.Items {
|
||||
item.serializeWithBuffer(this.buffer)
|
||||
}
|
||||
}
|
||||
|
||||
type calEventSerializer struct {
|
||||
event *CalendarEvent
|
||||
buffer *strBuffer
|
||||
}
|
||||
|
||||
const (
|
||||
eventSerializerTimeFormat = "20060102T150405Z"
|
||||
)
|
||||
|
||||
func (this *calEventSerializer) serialize() string {
|
||||
this.serializeEvent()
|
||||
return strings.TrimSpace(this.buffer.String())
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) serializeEvent() {
|
||||
this.begin()
|
||||
this.uid()
|
||||
this.created()
|
||||
this.lastModified()
|
||||
this.dtstart()
|
||||
this.dtend()
|
||||
this.summary()
|
||||
this.description()
|
||||
this.location()
|
||||
this.end()
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) begin() {
|
||||
this.buffer.Write("BEGIN:VEVENT\n")
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) end() {
|
||||
this.buffer.Write("END:VEVENT\n")
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) uid() {
|
||||
this.serializeStringProp("UID", this.event.Id)
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) summary() {
|
||||
this.serializeStringProp("SUMMARY", this.event.Summary)
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) description() {
|
||||
this.serializeStringProp("DESCRIPTION", this.event.Description)
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) location() {
|
||||
this.serializeStringProp("LOCATION", this.event.Location)
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) dtstart() {
|
||||
this.serializeTimeProp("DTSTART", this.event.StartAtUTC())
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) dtend() {
|
||||
this.serializeTimeProp("DTEND", this.event.EndAtUTC())
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) created() {
|
||||
this.serializeTimeProp("CREATED", this.event.CreatedAtUTC)
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) lastModified() {
|
||||
this.serializeTimeProp("LAST-MODIFIED", this.event.ModifiedAtUTC)
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) serializeStringProp(name, value string) {
|
||||
if value != "" {
|
||||
escapedValue := escapeTextType(value)
|
||||
this.buffer.Write("%s:%s\n", name, escapedValue)
|
||||
}
|
||||
}
|
||||
|
||||
func (this *calEventSerializer) serializeTimeProp(name string, value *time.Time) {
|
||||
if value != nil {
|
||||
this.buffer.Write("%s:%s\n", name, value.Format(eventSerializerTimeFormat))
|
||||
}
|
||||
}
|
94
vendor/github.com/laurent22/ical-go/ical/todo.go
generated
vendored
Normal file
94
vendor/github.com/laurent22/ical-go/ical/todo.go
generated
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
package ical
|
||||
|
||||
// import (
|
||||
// "time"
|
||||
// "strconv"
|
||||
// "strings"
|
||||
// )
|
||||
//
|
||||
// func TodoFromNode(node *Node) Todo {
|
||||
// if node.Name != "VTODO" { panic("Node is not a VTODO") }
|
||||
//
|
||||
// var todo Todo
|
||||
// todo.SetId(node.PropString("UID", ""))
|
||||
// todo.SetSummary(node.PropString("SUMMARY", ""))
|
||||
// todo.SetDescription(node.PropString("DESCRIPTION", ""))
|
||||
// todo.SetDueDate(node.PropDate("DUE", time.Time{}))
|
||||
// //todo.SetAlarmDate(this.TimestampBytesToTime(reminderDate))
|
||||
// todo.SetCreatedDate(node.PropDate("CREATED", time.Time{}))
|
||||
// todo.SetModifiedDate(node.PropDate("DTSTAMP", time.Time{}))
|
||||
// todo.SetPriority(node.PropInt("PRIORITY", 0))
|
||||
// todo.SetPercentComplete(node.PropInt("PERCENT-COMPLETE", 0))
|
||||
// return todo
|
||||
// }
|
||||
//
|
||||
// type Todo struct {
|
||||
// CalendarItem
|
||||
// dueDate time.Time
|
||||
// }
|
||||
//
|
||||
// func (this *Todo) SetDueDate(v time.Time) { this.dueDate = v }
|
||||
// func (this *Todo) DueDate() time.Time { return this.dueDate }
|
||||
//
|
||||
// func (this *Todo) ICalString(target string) string {
|
||||
// s := "BEGIN:VTODO\n"
|
||||
//
|
||||
// if target == "macTodo" {
|
||||
// status := "NEEDS-ACTION"
|
||||
// if this.PercentComplete() == 100 {
|
||||
// status = "COMPLETED"
|
||||
// }
|
||||
// s += "STATUS:" + status + "\n"
|
||||
// }
|
||||
//
|
||||
// s += encodeDateProperty("CREATED", this.CreatedDate()) + "\n"
|
||||
// s += "UID:" + this.Id() + "\n"
|
||||
// s += "SUMMARY:" + escapeTextType(this.Summary()) + "\n"
|
||||
// if this.PercentComplete() == 100 && !this.CompletedDate().IsZero() {
|
||||
// s += encodeDateProperty("COMPLETED", this.CompletedDate()) + "\n"
|
||||
// }
|
||||
// s += encodeDateProperty("DTSTAMP", this.ModifiedDate()) + "\n"
|
||||
// if this.Priority() != 0 {
|
||||
// s += "PRIORITY:" + strconv.Itoa(this.Priority()) + "\n"
|
||||
// }
|
||||
// if this.PercentComplete() != 0 {
|
||||
// s += "PERCENT-COMPLETE:" + strconv.Itoa(this.PercentComplete()) + "\n"
|
||||
// }
|
||||
// if target == "macTodo" {
|
||||
// s += "SEQUENCE:" + strconv.Itoa(this.Sequence()) + "\n"
|
||||
// }
|
||||
// if this.Description() != "" {
|
||||
// s += "DESCRIPTION:" + encodeTextType(this.Description()) + "\n"
|
||||
// }
|
||||
//
|
||||
// s += "END:VTODO\n"
|
||||
//
|
||||
// return s
|
||||
// }
|
||||
//
|
||||
// func encodeDateProperty(name string, t time.Time) string {
|
||||
// var output string
|
||||
// zone, _ := t.Zone()
|
||||
// if zone != "UTC" && zone != "" {
|
||||
// output = ";TZID=" + zone + ":" + t.Format("20060102T150405")
|
||||
// } else {
|
||||
// output = ":" + t.Format("20060102T150405") + "Z"
|
||||
// }
|
||||
// return name + output
|
||||
// }
|
||||
//
|
||||
//
|
||||
// func encodeTextType(s string) string {
|
||||
// output := ""
|
||||
// s = escapeTextType(s)
|
||||
// lineLength := 0
|
||||
// for _, c := range s {
|
||||
// if lineLength + len(string(c)) > 75 {
|
||||
// output += "\n "
|
||||
// lineLength = 1
|
||||
// }
|
||||
// output += string(c)
|
||||
// lineLength += len(string(c))
|
||||
// }
|
||||
// return output
|
||||
// }
|
2
vendor/github.com/samedi/caldav-go/.gitignore
generated
vendored
Normal file
2
vendor/github.com/samedi/caldav-go/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
test-data/
|
||||
vendor
|
69
vendor/github.com/samedi/caldav-go/CHANGELOG.md
generated
vendored
Normal file
69
vendor/github.com/samedi/caldav-go/CHANGELOG.md
generated
vendored
Normal file
|
@ -0,0 +1,69 @@
|
|||
# CHANGELOG
|
||||
|
||||
v3.0.0
|
||||
-----------
|
||||
2017-08-01 Daniel Ferraz <d.ferrazm@gmail.com>
|
||||
|
||||
Main change:
|
||||
|
||||
Add two ways to get resources from the storage: shallow or not.
|
||||
|
||||
`data.GetShallowResource`: means that, if it's a collection resource, it will not include its child VEVENTs in the ICS data.
|
||||
This is used throughout the palces where children don't matter.
|
||||
|
||||
`data.GetResource`: means that the child VEVENTs will be included in the returned ICS content data for collection resources.
|
||||
This is used when sending a GET request to fetch a specific resource and expecting its full ICS data in response.
|
||||
|
||||
Other changes:
|
||||
|
||||
* Removed the need to pass the useless `writer http.ResponseWriter` parameter when calling the `caldav.HandleRequest` function.
|
||||
* Added a `caldav.HandleRequestWithStorage` function that makes it easy to pass a custom storage to be used and handle the request with a single function call.
|
||||
|
||||
|
||||
v2.0.0
|
||||
-----------
|
||||
2017-05-10 Daniel Ferraz <d.ferrazm@gmail.com>
|
||||
|
||||
All commits squashed and LICENSE updated to release as OSS in github.
|
||||
Feature-wise it remains the same.
|
||||
|
||||
|
||||
v1.0.1
|
||||
-----------
|
||||
2017-01-25 Daniel Ferraz <d.ferrazm@gmail.com>
|
||||
|
||||
Escape the contents in `<calendar-data>` and `<displayname>` in the `multistatus` XML responses. Fixing possible bugs
|
||||
related to having special characters (e.g. &) in the XML multistatus responses that would possibly break the encoding.
|
||||
|
||||
v1.0.0
|
||||
-----------
|
||||
2017-01-18 Daniel Ferraz <d.ferrazm@gmail.com>
|
||||
|
||||
Main feature:
|
||||
|
||||
* Handles the `Prefer` header on PROPFIND and REPORT requests (defined in this [draft/proposal](https://tools.ietf.org/html/draft-murchison-webdav-prefer-05)). Useful to shrink down possible big and verbose responses when the client demands. Ex: current iOS calendar client uses this feature on its PROPFIND requests.
|
||||
|
||||
Other changes:
|
||||
|
||||
* Added the `handlers.Response` to allow clients of the lib to interact with the generated response before being written/sent back to the client.
|
||||
* Added `GetResourcesByFilters` to the storage interface to allow filtering of resources in the storage level. Useful to provide an already filtered and smaller resource collection to a the REPORT handler when dealing with a filtered REPORT request.
|
||||
* Added `GetResourcesByList` to the storage interface to fetch a set a of resources based on a set of paths. Useful to provide, in one call, the correct resource collection to the REPORT handler when dealing with a REPORT request for specific `hrefs`.
|
||||
* Remove useless `IsResourcePresent` from the storage interface.
|
||||
|
||||
|
||||
v0.1.0
|
||||
-----------
|
||||
2016-09-23 Daniel Ferraz <d.ferrazm@gmail.com>
|
||||
|
||||
This version implements:
|
||||
|
||||
* Allow: "GET, HEAD, PUT, DELETE, OPTIONS, PROPFIND, REPORT"
|
||||
* DAV: "1, 3, calendar-access"
|
||||
* Also only handles the following components: `VCALENDAR`, `VEVENT`
|
||||
|
||||
Currently unsupported:
|
||||
|
||||
* Components `VTODO`, `VJOURNAL`, `VFREEBUSY`
|
||||
* `VEVENT` recurrences
|
||||
* Resource locking
|
||||
* User authentication
|
20
vendor/github.com/samedi/caldav-go/LICENSE
generated
vendored
Normal file
20
vendor/github.com/samedi/caldav-go/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
Copyright 2017 samedi GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NON INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
197
vendor/github.com/samedi/caldav-go/README.md
generated
vendored
Normal file
197
vendor/github.com/samedi/caldav-go/README.md
generated
vendored
Normal file
|
@ -0,0 +1,197 @@
|
|||
# go CalDAV
|
||||
|
||||
This is a Go lib that aims to implement the CalDAV specification ([RFC4791]). It allows the quick implementation of a CalDAV server in Go. Basically, it provides the request handlers that will handle the several CalDAV HTTP requests, fetch the appropriate resources, build and return the responses.
|
||||
|
||||
### How to install
|
||||
|
||||
```
|
||||
go get github.com/samedi/caldav-go
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
For dependency management, `glide` is used.
|
||||
|
||||
```bash
|
||||
# install glide (once!)
|
||||
curl https://glide.sh/get | sh
|
||||
|
||||
# install dependencies
|
||||
glide install
|
||||
```
|
||||
|
||||
### How to use it
|
||||
|
||||
The easiest way to quickly implement a CalDAV server is by just using the lib's request handler. Example:
|
||||
|
||||
```go
|
||||
package mycaldav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/samedi/caldav-go"
|
||||
)
|
||||
|
||||
func runServer() {
|
||||
http.HandleFunc(PATH, caldav.RequestHandler)
|
||||
http.ListenAndServe(PORT, nil)
|
||||
}
|
||||
```
|
||||
|
||||
With that, all the HTTP requests (GET, PUT, REPORT, PROPFIND, etc) will be handled and responded by the `caldav` handler. In case of any HTTP methods not supported by the lib, a `501 Not Implemented` response will be returned.
|
||||
|
||||
In case you want more flexibility to handle the requests, e.g., if you wanted to access the generated response before being sent back to the caller, you could do like:
|
||||
|
||||
```go
|
||||
package mycaldav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/samedi/caldav-go"
|
||||
)
|
||||
|
||||
func runServer() {
|
||||
http.HandleFunc(PATH, myHandler)
|
||||
http.ListenAndServe(PORT, nil)
|
||||
}
|
||||
|
||||
func myHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
response := caldav.HandleRequest(request)
|
||||
// ... do something with the response object before writing it back to the client ...
|
||||
response.Write(writer)
|
||||
}
|
||||
```
|
||||
|
||||
### Storage & Resources
|
||||
|
||||
The storage is where the CalDAV resources are stored. To interact with that, the caldav lib needs only a type that conforms with the `data.Storage` interface to operate on top of the storage. Basically, this interface defines all the CRUD functions to work on top of the resources. With that, resources can be stored anywhere: in the filesystem, in the cloud, database, etc. As long as the used storage implements all the required storage interface functions, the caldav lib will work fine.
|
||||
|
||||
For example, we could use the following dummy read-only storage implementation, which returns dummy hard-coded resources:
|
||||
|
||||
```go
|
||||
type DummyStorage struct{
|
||||
resources map[string]string{
|
||||
"/foo": `BEGING:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20160914T170000\nEND:VEVENT\nEND:VCALENDAR`,
|
||||
"/bar": `BEGING:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20160915T180000\nEND:VEVENT\nEND:VCALENDAR`,
|
||||
"/baz": `BEGING:VCALENDAR\nBEGIN:VEVENT\nDTSTART:20160916T190000\nEND:VEVENT\nEND:VCALENDAR`,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResources(rpath string, withChildren bool) ([]Resource, error) {
|
||||
return d.GetResourcesByList([]string{rpath})
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResourcesByFilters(rpath string, filters *ResourceFilter) ([]Resource, error) {
|
||||
return nil, errors.New("filters are not supported")
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResourcesByList(rpaths []string) ([]Resource, error) {
|
||||
result := []Resource{}
|
||||
|
||||
for _, rpath := range rpaths {
|
||||
resource, found, _ := d.GetResource(rpath)
|
||||
if found {
|
||||
result = append(result, resource)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetResource(rpath string) (*Resource, bool, error) {
|
||||
return d.GetShallowResource(rpath)
|
||||
}
|
||||
|
||||
func (d *DummyStorage) GetShallowResource(rpath string) (*Resource, bool, error) {
|
||||
result := []Resource{}
|
||||
resContent := d.resources[rpath]
|
||||
|
||||
if resContent != "" {
|
||||
resource = NewResource(rpath, DummyResourceAdapter{rpath, resContent})
|
||||
return &resource, true, nil
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
func (d *DummyStorage) CreateResource(rpath, content string) (*Resource, error) {
|
||||
return nil, errors.New("creating resources are not supported")
|
||||
}
|
||||
|
||||
func (d *DummyStorage) UpdateResource(rpath, content string) (*Resource, error) {
|
||||
return nil, errors.New("updating resources are not supported")
|
||||
}
|
||||
|
||||
func (d *DummyStorage) DeleteResource(rpath string) error {
|
||||
return nil, errors.New("deleting resources are not supported")
|
||||
}
|
||||
```
|
||||
|
||||
Normally, when you provide your own storage implementation, you will need to provide also a custom `data.ResourceAdapter` interface implementation.
|
||||
The resource adapter deals with the specificities of how resources are stored, which formats and how to deal with them. For example,
|
||||
for file resources, the resources contents are the content read from the file itself, for resources in the cloud, it could be in JSON needing
|
||||
some additional processing to parse the content, etc.
|
||||
|
||||
In our example here, we could say that the adapter for this case would be:
|
||||
|
||||
```go
|
||||
type DummyResourceAdapter struct {
|
||||
resourcePath string
|
||||
resourceData string
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) IsCollection() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) GetContent() string {
|
||||
return a.resourceData
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) GetContentSize() int64 {
|
||||
return len(a.GetContent())
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) CalculateEtag() string {
|
||||
return hashify(a.GetContent())
|
||||
}
|
||||
|
||||
func (a *DummyResourceAdapter) GetModTime() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
```
|
||||
|
||||
Note that this adapter implementation is passed over whenever we initialize a new `Resource` instance in the storage implementation.
|
||||
|
||||
Then we just need to tell the caldav lib to use our dummy storage:
|
||||
|
||||
```go
|
||||
dummyStg := new(DummyStorage)
|
||||
caldav.SetupStorage(dummyStg)
|
||||
```
|
||||
|
||||
All the CRUD operations on resources will then be forwarded to our dummy storage.
|
||||
|
||||
The default storage used (if none is explicitly set) is the `data.FileStorage` which deals with resources as files in the File System.
|
||||
|
||||
The resources can be of two types: collection and non-collection. A collection resource is basically a resource that has children resources, but does not have any data content. A non-collection resource is a resource that does not have children, but has data. In the case of a file storage, collections correspond to directories and non-collection to plain files. The data of a caldav resource is all the info that shows up in the calendar client, in the [iCalendar](https://en.wikipedia.org/wiki/ICalendar) format.
|
||||
|
||||
### Features
|
||||
|
||||
Please check the **CHANGELOG** to see specific features that are currently implemented.
|
||||
|
||||
### Contributing and testing
|
||||
|
||||
Everyone is welcome to contribute. Please raise an issue or pull request accordingly.
|
||||
|
||||
To run the tests:
|
||||
|
||||
```
|
||||
./test.sh
|
||||
```
|
||||
|
||||
### License
|
||||
|
||||
MIT License.
|
||||
|
||||
[RFC4791]: https://tools.ietf.org/html/rfc4791
|
24
vendor/github.com/samedi/caldav-go/config.go
generated
vendored
Normal file
24
vendor/github.com/samedi/caldav-go/config.go
generated
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
package caldav
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
)
|
||||
|
||||
// SetupStorage sets the storage to be used by the server. The storage is where the resources data will be fetched from.
|
||||
// You can provide a custom storage for your own purposes (which might be looking for data in the cloud, DB, etc).
|
||||
// Just make sure it implements the `data.Storage` interface.
|
||||
func SetupStorage(stg data.Storage) {
|
||||
global.Storage = stg
|
||||
}
|
||||
|
||||
// SetupUser sets the current user which is currently interacting with the calendar.
|
||||
// It is used, for example, in some of the CALDAV responses, when rendering the path where to find the user's resources.
|
||||
func SetupUser(username string) {
|
||||
global.User = &data.CalUser{Name: username}
|
||||
}
|
||||
|
||||
// SetupSupportedComponents sets all components which are supported by this storage implementation.
|
||||
func SetupSupportedComponents(components []string) {
|
||||
global.SupportedComponents = components
|
||||
}
|
363
vendor/github.com/samedi/caldav-go/data/filters.go
generated
vendored
Normal file
363
vendor/github.com/samedi/caldav-go/data/filters.go
generated
vendored
Normal file
|
@ -0,0 +1,363 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/beevik/etree"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
const (
|
||||
TAG_FILTER = "filter"
|
||||
TAG_COMP_FILTER = "comp-filter"
|
||||
TAG_PROP_FILTER = "prop-filter"
|
||||
TAG_PARAM_FILTER = "param-filter"
|
||||
TAG_TIME_RANGE = "time-range"
|
||||
TAG_TEXT_MATCH = "text-match"
|
||||
TAG_IS_NOT_DEFINED = "is-not-defined"
|
||||
|
||||
// From the RFC, the time range `start` and `end` attributes MUST be in UTC and in this specific format
|
||||
FILTER_TIME_FORMAT = "20060102T150405Z"
|
||||
)
|
||||
|
||||
// ResourceFilter represents filters to filter out resources.
|
||||
// Filters are basically a set of rules used to retrieve a range of resources.
|
||||
// It is used primarily on REPORT requests and is described in details in RFC4791#7.8.
|
||||
type ResourceFilter struct {
|
||||
name string
|
||||
text string
|
||||
attrs map[string]string
|
||||
children []ResourceFilter // collection of child filters.
|
||||
etreeElem *etree.Element // holds the parsed XML node/tag as an `etree` element.
|
||||
}
|
||||
|
||||
// ParseResourceFilters initializes a new `ResourceFilter` object from a snippet of XML string.
|
||||
func ParseResourceFilters(xml string) (*ResourceFilter, error) {
|
||||
doc := etree.NewDocument()
|
||||
if err := doc.ReadFromString(xml); err != nil {
|
||||
log.Printf("ERROR: Could not parse filter from XML string. XML:\n%s", xml)
|
||||
return new(ResourceFilter), err
|
||||
}
|
||||
|
||||
// Right now we're searching for a <filter> tag to initialize the filter struct from it.
|
||||
// It SHOULD be a valid XML CALDAV:filter tag (RFC4791#9.7). We're not checking namespaces yet.
|
||||
// TODO: check for XML namespaces and restrict it to accept only CALDAV:filter tag.
|
||||
elem := doc.FindElement("//" + TAG_FILTER)
|
||||
if elem == nil {
|
||||
log.Printf("WARNING: The filter XML should contain a <%s> element. XML:\n%s", TAG_FILTER, xml)
|
||||
return new(ResourceFilter), errors.New("invalid XML filter")
|
||||
}
|
||||
|
||||
filter := newFilterFromEtreeElem(elem)
|
||||
return &filter, nil
|
||||
}
|
||||
|
||||
func newFilterFromEtreeElem(elem *etree.Element) ResourceFilter {
|
||||
// init filter from etree element
|
||||
filter := ResourceFilter{
|
||||
name: elem.Tag,
|
||||
text: strings.TrimSpace(elem.Text()),
|
||||
etreeElem: elem,
|
||||
attrs: make(map[string]string),
|
||||
}
|
||||
|
||||
// set attributes
|
||||
for _, attr := range elem.Attr {
|
||||
filter.attrs[attr.Key] = attr.Value
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
// Attr searches an attribute by its name in the list of filter attributes and returns it.
|
||||
func (f *ResourceFilter) Attr(attrName string) string {
|
||||
return f.attrs[attrName]
|
||||
}
|
||||
|
||||
// TimeAttr searches and returns a filter attribute as a `time.Time` object.
|
||||
func (f *ResourceFilter) TimeAttr(attrName string) *time.Time {
|
||||
|
||||
t, err := time.Parse(FILTER_TIME_FORMAT, f.attrs[attrName])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &t
|
||||
}
|
||||
|
||||
// GetTimeRangeFilter checks if the current filter has a child "time-range" filter and
|
||||
// returns it (wrapped in a `ResourceFilter` type). It returns nil if the current filter does
|
||||
// not contain any "time-range" filter.
|
||||
func (f *ResourceFilter) GetTimeRangeFilter() *ResourceFilter {
|
||||
return f.findChild(TAG_TIME_RANGE, true)
|
||||
}
|
||||
|
||||
// Match returns whether a provided resource matches the filters.
|
||||
func (f *ResourceFilter) Match(target ResourceInterface) bool {
|
||||
if f.name == TAG_FILTER {
|
||||
return f.rootFilterMatch(target)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) rootFilterMatch(target ResourceInterface) bool {
|
||||
if f.isEmpty() {
|
||||
return false
|
||||
}
|
||||
|
||||
return f.rootChildrenMatch(target)
|
||||
}
|
||||
|
||||
// checks if all the root's child filters match the target resource
|
||||
func (f *ResourceFilter) rootChildrenMatch(target ResourceInterface) bool {
|
||||
scope := []string{}
|
||||
|
||||
for _, child := range f.getChildren() {
|
||||
// root filters only accept comp filters as children
|
||||
if child.name != TAG_COMP_FILTER || !child.compMatch(target, scope) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.1.
|
||||
func (f *ResourceFilter) compMatch(target ResourceInterface, scope []string) bool {
|
||||
targetComp := target.ComponentName()
|
||||
compName := f.attrs["name"]
|
||||
|
||||
if f.isEmpty() {
|
||||
// Point #1 of RFC4791#9.7.1
|
||||
return compName == targetComp
|
||||
} else if f.contains(TAG_IS_NOT_DEFINED) {
|
||||
// Point #2 of RFC4791#9.7.1
|
||||
return compName != targetComp
|
||||
} else {
|
||||
// check each child of the current filter if they all match.
|
||||
childrenScope := append(scope, compName)
|
||||
return f.compChildrenMatch(target, childrenScope)
|
||||
}
|
||||
}
|
||||
|
||||
// checks if all the comp's child filters match the target resource
|
||||
func (f *ResourceFilter) compChildrenMatch(target ResourceInterface, scope []string) bool {
|
||||
for _, child := range f.getChildren() {
|
||||
var match bool
|
||||
|
||||
switch child.name {
|
||||
case TAG_TIME_RANGE:
|
||||
// Point #3 of RFC4791#9.7.1
|
||||
match = child.timeRangeMatch(target)
|
||||
case TAG_PROP_FILTER:
|
||||
// Point #4 of RFC4791#9.7.1
|
||||
match = child.propMatch(target, scope)
|
||||
case TAG_COMP_FILTER:
|
||||
// Point #4 of RFC4791#9.7.1
|
||||
match = child.compMatch(target, scope)
|
||||
}
|
||||
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.9
|
||||
func (f *ResourceFilter) timeRangeMatch(target ResourceInterface) bool {
|
||||
startAttr := f.attrs["start"]
|
||||
endAttr := f.attrs["end"]
|
||||
|
||||
// at least one of the two MUST be present
|
||||
if startAttr == "" && endAttr == "" {
|
||||
// if both of them are missing, return false
|
||||
return false
|
||||
} else if startAttr == "" {
|
||||
// if missing only the `start`, set it open ended to the left
|
||||
startAttr = "00010101T000000Z"
|
||||
} else if endAttr == "" {
|
||||
// if missing only the `end`, set it open ended to the right
|
||||
endAttr = "99991231T235959Z"
|
||||
}
|
||||
|
||||
// The logic below is only applicable for VEVENT components. So
|
||||
// we return false if the resource is not a VEVENT component.
|
||||
if target.ComponentName() != lib.VEVENT {
|
||||
return false
|
||||
}
|
||||
|
||||
rangeStart, err := time.Parse(FILTER_TIME_FORMAT, startAttr)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not parse start time in time-range filter.\nError: %s.\nStart attr: %s", err, startAttr)
|
||||
return false
|
||||
}
|
||||
|
||||
rangeEnd, err := time.Parse(FILTER_TIME_FORMAT, endAttr)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not parse end time in time-range filter.\nError: %s.\nEnd attr: %s", err, endAttr)
|
||||
return false
|
||||
}
|
||||
|
||||
// the following logic is inferred from the rules table for VEVENT components,
|
||||
// described in RFC4791-9.9.
|
||||
overlapRange := func(dtStart, dtEnd, rangeStart, rangeEnd time.Time) bool {
|
||||
if dtStart.Equal(dtEnd) {
|
||||
// Lines 3 and 4 of the table deal when the DTSTART and DTEND dates are equals.
|
||||
// In this case we use the rule: (start <= DTSTART && end > DTSTART)
|
||||
return (rangeStart.Before(dtStart) || rangeStart.Equal(dtStart)) && rangeEnd.After(dtStart)
|
||||
} else {
|
||||
// Lines 1, 2 and 6 of the table deal when the DTSTART and DTEND dates are different.
|
||||
// In this case we use the rule: (start < DTEND && end > DTSTART)
|
||||
return rangeStart.Before(dtEnd) && rangeEnd.After(dtStart)
|
||||
}
|
||||
}
|
||||
|
||||
// first we check each of the target recurrences (if any).
|
||||
for _, recurrence := range target.Recurrences() {
|
||||
// if any of them overlap the filter range, we return true right away
|
||||
if overlapRange(recurrence.StartTime, recurrence.EndTime, rangeStart, rangeEnd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// if none of the recurrences match, we just return if the actual
|
||||
// resource's `start` and `end` times match the filter range
|
||||
return overlapRange(target.StartTimeUTC(), target.EndTimeUTC(), rangeStart, rangeEnd)
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.2.
|
||||
func (f *ResourceFilter) propMatch(target ResourceInterface, scope []string) bool {
|
||||
propName := f.attrs["name"]
|
||||
propPath := append(scope, propName)
|
||||
|
||||
if f.isEmpty() {
|
||||
// Point #1 of RFC4791#9.7.2
|
||||
return target.HasProperty(propPath...)
|
||||
} else if f.contains(TAG_IS_NOT_DEFINED) {
|
||||
// Point #2 of RFC4791#9.7.2
|
||||
return !target.HasProperty(propPath...)
|
||||
} else {
|
||||
// check each child of the current filter if they all match.
|
||||
return f.propChildrenMatch(target, propPath)
|
||||
}
|
||||
}
|
||||
|
||||
// checks if all the prop's child filters match the target resource
|
||||
func (f *ResourceFilter) propChildrenMatch(target ResourceInterface, propPath []string) bool {
|
||||
for _, child := range f.getChildren() {
|
||||
var match bool
|
||||
|
||||
switch child.name {
|
||||
case TAG_TIME_RANGE:
|
||||
// Point #3 of RFC4791#9.7.2
|
||||
// TODO: this point is not very clear on how to match time range against properties.
|
||||
// So we're returning `false` in the meantime.
|
||||
match = false
|
||||
case TAG_TEXT_MATCH:
|
||||
// Point #4 of RFC4791#9.7.2
|
||||
propText := target.GetPropertyValue(propPath...)
|
||||
match = child.textMatch(propText)
|
||||
case TAG_PARAM_FILTER:
|
||||
// Point #4 of RFC4791#9.7.2
|
||||
match = child.paramMatch(target, propPath)
|
||||
}
|
||||
|
||||
if !match {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.3
|
||||
func (f *ResourceFilter) paramMatch(target ResourceInterface, parentPropPath []string) bool {
|
||||
paramName := f.attrs["name"]
|
||||
paramPath := append(parentPropPath, paramName)
|
||||
|
||||
if f.isEmpty() {
|
||||
// Point #1 of RFC4791#9.7.3
|
||||
return target.HasPropertyParam(paramPath...)
|
||||
} else if f.contains(TAG_IS_NOT_DEFINED) {
|
||||
// Point #2 of RFC4791#9.7.3
|
||||
return !target.HasPropertyParam(paramPath...)
|
||||
} else {
|
||||
child := f.getChildren()[0]
|
||||
// param filters can also have (only-one) nested text-match filter
|
||||
if child.name == TAG_TEXT_MATCH {
|
||||
paramValue := target.GetPropertyParamValue(paramPath...)
|
||||
return child.textMatch(paramValue)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// See RFC4791-9.7.5
|
||||
func (f *ResourceFilter) textMatch(targetText string) bool {
|
||||
// TODO: collations are not being considered/supported yet.
|
||||
// Texts are lowered to be case-insensitive, almost as the "i;ascii-casemap" value.
|
||||
|
||||
targetText = strings.ToLower(targetText)
|
||||
expectedSubstr := strings.ToLower(f.text)
|
||||
|
||||
match := strings.Contains(targetText, expectedSubstr)
|
||||
|
||||
if f.attrs["negate-condition"] == "yes" {
|
||||
return !match
|
||||
}
|
||||
|
||||
return match
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) isEmpty() bool {
|
||||
return len(f.getChildren()) == 0 && f.text == ""
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) contains(filterName string) bool {
|
||||
if f.findChild(filterName, false) != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *ResourceFilter) findChild(filterName string, dig bool) *ResourceFilter {
|
||||
for _, child := range f.getChildren() {
|
||||
if child.name == filterName {
|
||||
return &child
|
||||
}
|
||||
|
||||
if !dig {
|
||||
continue
|
||||
}
|
||||
|
||||
dugChild := child.findChild(filterName, true)
|
||||
|
||||
if dugChild != nil {
|
||||
return dugChild
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// lazy evaluation of the child filters
|
||||
func (f *ResourceFilter) getChildren() []ResourceFilter {
|
||||
if f.children == nil {
|
||||
f.children = []ResourceFilter{}
|
||||
|
||||
for _, childElem := range f.etreeElem.ChildElements() {
|
||||
childFilter := newFilterFromEtreeElem(childElem)
|
||||
f.children = append(f.children, childFilter)
|
||||
}
|
||||
}
|
||||
|
||||
return f.children
|
||||
}
|
370
vendor/github.com/samedi/caldav-go/data/resource.go
generated
vendored
Normal file
370
vendor/github.com/samedi/caldav-go/data/resource.go
generated
vendored
Normal file
|
@ -0,0 +1,370 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/laurent22/ical-go/ical"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/samedi/caldav-go/files"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
// ResourceInterface defines the main interface of a CalDAV resource object. This
|
||||
// interface exists only to define the common resource operation and should not be custom-implemented.
|
||||
// The default and canonical implementation is provided by `data.Resource`, convering all the commonalities.
|
||||
// Any specifics in implementations should be handled by the `data.ResourceAdapter`.
|
||||
type ResourceInterface interface {
|
||||
ComponentName() string
|
||||
StartTimeUTC() time.Time
|
||||
EndTimeUTC() time.Time
|
||||
Recurrences() []ResourceRecurrence
|
||||
HasProperty(propPath ...string) bool
|
||||
GetPropertyValue(propPath ...string) string
|
||||
HasPropertyParam(paramName ...string) bool
|
||||
GetPropertyParamValue(paramName ...string) string
|
||||
}
|
||||
|
||||
// ResourceAdapter serves as the object to abstract all the specicities in different resources implementations.
|
||||
// For example, the way to tell whether a resource is a collection or how to read its content differentiates
|
||||
// on resources stored in the file system, coming from a relational DB or from the cloud as JSON. These differentiations
|
||||
// should be covered by providing a specific implementation of the `ResourceAdapter` interface. So, depending on the current
|
||||
// resource storage strategy, a matching resource adapter implementation should be provided whenever a new resource is initialized.
|
||||
type ResourceAdapter interface {
|
||||
IsCollection() bool
|
||||
CalculateEtag() string
|
||||
GetContent() string
|
||||
GetContentSize() int64
|
||||
GetModTime() time.Time
|
||||
}
|
||||
|
||||
// ResourceRecurrence represents a recurrence for a resource.
|
||||
// NOTE: recurrences are not supported yet.
|
||||
type ResourceRecurrence struct {
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
}
|
||||
|
||||
// Resource represents the CalDAV resource. Basically, it has a name it's accessible based on path.
|
||||
// A resource can be a collection, meaning it doesn't have any data content, but it has child resources.
|
||||
// A non-collection is the actual resource which has the data in iCal format and which will feed the calendar.
|
||||
// When visualizing the whole resources set in a tree representation, the collection resource would be the inner nodes and
|
||||
// the non-collection would be the leaves.
|
||||
type Resource struct {
|
||||
Name string
|
||||
Path string
|
||||
|
||||
pathSplit []string
|
||||
adapter ResourceAdapter
|
||||
|
||||
emptyTime time.Time
|
||||
}
|
||||
|
||||
// NewResource initializes a new `Resource` instance based on its path and the `ResourceAdapter` implementation to be used.
|
||||
func NewResource(rawPath string, adp ResourceAdapter) Resource {
|
||||
pClean := lib.ToSlashPath(rawPath)
|
||||
pSplit := strings.Split(strings.Trim(pClean, "/"), "/")
|
||||
|
||||
return Resource{
|
||||
Name: pSplit[len(pSplit)-1],
|
||||
Path: pClean,
|
||||
pathSplit: pSplit,
|
||||
adapter: adp,
|
||||
}
|
||||
}
|
||||
|
||||
// IsCollection tells whether a resource is a collection or not.
|
||||
func (r *Resource) IsCollection() bool {
|
||||
return r.adapter.IsCollection()
|
||||
}
|
||||
|
||||
// IsPrincipal tells whether a resource is the principal resource or not.
|
||||
// A principal resource means it's a root resource.
|
||||
func (r *Resource) IsPrincipal() bool {
|
||||
return len(r.pathSplit) <= 1
|
||||
}
|
||||
|
||||
// ComponentName returns the type of the resource. VCALENDAR for collection resources, VEVENT otherwise.
|
||||
func (r *Resource) ComponentName() string {
|
||||
if r.IsCollection() {
|
||||
return lib.VCALENDAR
|
||||
}
|
||||
|
||||
return lib.VEVENT
|
||||
}
|
||||
|
||||
// StartTimeUTC returns the start time in UTC of a VEVENT resource.
|
||||
func (r *Resource) StartTimeUTC() time.Time {
|
||||
vevent := r.icalVEVENT()
|
||||
dtstart := vevent.PropDate(ical.DTSTART, r.emptyTime)
|
||||
|
||||
if dtstart == r.emptyTime {
|
||||
log.Printf("WARNING: The property DTSTART was not found in the resource's ical data.\nResource path: %s", r.Path)
|
||||
return r.emptyTime
|
||||
}
|
||||
|
||||
return dtstart.UTC()
|
||||
}
|
||||
|
||||
// EndTimeUTC returns the end time in UTC of a VEVENT resource.
|
||||
func (r *Resource) EndTimeUTC() time.Time {
|
||||
vevent := r.icalVEVENT()
|
||||
dtend := vevent.PropDate(ical.DTEND, r.emptyTime)
|
||||
|
||||
// when the DTEND property is not present, we just add the DURATION (if any) to the DTSTART
|
||||
if dtend == r.emptyTime {
|
||||
duration := vevent.PropDuration(ical.DURATION)
|
||||
dtend = r.StartTimeUTC().Add(duration)
|
||||
}
|
||||
|
||||
return dtend.UTC()
|
||||
}
|
||||
|
||||
// Recurrences returns an array of resource recurrences.
|
||||
// NOTE: Recurrences are not supported yet. An empty array will always be returned.
|
||||
func (r *Resource) Recurrences() []ResourceRecurrence {
|
||||
// TODO: Implement. This server does not support iCal recurrences yet. We just return an empty array.
|
||||
return []ResourceRecurrence{}
|
||||
}
|
||||
|
||||
// HasProperty tells whether the resource has the provided property in its iCal content.
|
||||
// The path to the property should be provided in case of nested properties.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// DTSTART:20160914T170000
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// HasProperty("VEVENT", "DTSTART") => returns true
|
||||
// HasProperty("VEVENT", "DTEND") => returns false
|
||||
func (r *Resource) HasProperty(propPath ...string) bool {
|
||||
return r.GetPropertyValue(propPath...) != ""
|
||||
}
|
||||
|
||||
// GetPropertyValue gets a property value from the resource's iCal content.
|
||||
// The path to the property should be provided in case of nested properties.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// DTSTART:20160914T170000
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// GetPropertyValue("VEVENT", "DTSTART") => returns "20160914T170000"
|
||||
// GetPropertyValue("VEVENT", "DTEND") => returns ""
|
||||
func (r *Resource) GetPropertyValue(propPath ...string) string {
|
||||
if propPath[0] == ical.VCALENDAR {
|
||||
propPath = propPath[1:]
|
||||
}
|
||||
|
||||
prop, _ := r.icalendar().DigProperty(propPath...)
|
||||
return prop
|
||||
}
|
||||
|
||||
// HasPropertyParam tells whether the resource has the provided property param in its iCal content.
|
||||
// The path to the param should be provided in case of nested params.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// ATTENDEE;PARTSTAT=NEEDS-ACTION:FOO
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// HasPropertyParam("VEVENT", "ATTENDEE", "PARTSTAT") => returns true
|
||||
// HasPropertyParam("VEVENT", "ATTENDEE", "OTHER") => returns false
|
||||
func (r *Resource) HasPropertyParam(paramPath ...string) bool {
|
||||
return r.GetPropertyParamValue(paramPath...) != ""
|
||||
}
|
||||
|
||||
// GetPropertyParamValue gets a property param value from the resource's iCal content.
|
||||
// The path to the param should be provided in case of nested params.
|
||||
// Example, suppose the resource has this content:
|
||||
//
|
||||
// BEGIN:VCALENDAR
|
||||
// BEGIN:VEVENT
|
||||
// ATTENDEE;PARTSTAT=NEEDS-ACTION:FOO
|
||||
// END:VEVENT
|
||||
// END:VCALENDAR
|
||||
//
|
||||
// GetPropertyParamValue("VEVENT", "ATTENDEE", "PARTSTAT") => returns "NEEDS-ACTION"
|
||||
// GetPropertyParamValue("VEVENT", "ATTENDEE", "OTHER") => returns ""
|
||||
func (r *Resource) GetPropertyParamValue(paramPath ...string) string {
|
||||
if paramPath[0] == ical.VCALENDAR {
|
||||
paramPath = paramPath[1:]
|
||||
}
|
||||
|
||||
param, _ := r.icalendar().DigParameter(paramPath...)
|
||||
return param
|
||||
}
|
||||
|
||||
// GetEtag returns the ETag of the resource and a flag saying if the ETag is present.
|
||||
// For collection resource, it returns an empty string and false.
|
||||
func (r *Resource) GetEtag() (string, bool) {
|
||||
if r.IsCollection() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return r.adapter.CalculateEtag(), true
|
||||
}
|
||||
|
||||
// GetContentType returns the type of the content of the resource.
|
||||
// Collection resources are "text/calendar". Non-collection resources are "text/calendar; component=vcalendar".
|
||||
func (r *Resource) GetContentType() (string, bool) {
|
||||
if r.IsCollection() {
|
||||
return "text/calendar", true
|
||||
}
|
||||
|
||||
return "text/calendar; component=vcalendar", true
|
||||
}
|
||||
|
||||
// GetDisplayName returns the name/identifier of the resource.
|
||||
func (r *Resource) GetDisplayName() (string, bool) {
|
||||
return r.Name, true
|
||||
}
|
||||
|
||||
// GetContentData reads and returns the raw content of the resource as string and flag saying if the content was found.
|
||||
// If the resource does not have content (like collection resource), it returns an empty string and false.
|
||||
func (r *Resource) GetContentData() (string, bool) {
|
||||
data := r.adapter.GetContent()
|
||||
found := data != ""
|
||||
|
||||
return data, found
|
||||
}
|
||||
|
||||
// GetContentLength returns the length of the resource's content and flag saying if the length is present.
|
||||
// If the resource does not have content (like collection resource), it returns an empty string and false.
|
||||
func (r *Resource) GetContentLength() (string, bool) {
|
||||
// If its collection, it does not have any content, so mark it as not found
|
||||
if r.IsCollection() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
contentSize := r.adapter.GetContentSize()
|
||||
return strconv.FormatInt(contentSize, 10), true
|
||||
}
|
||||
|
||||
// GetLastModified returns the last time the resource was modified. The returned time
|
||||
// is returned formatted in the provided `format`.
|
||||
func (r *Resource) GetLastModified(format string) (string, bool) {
|
||||
return r.adapter.GetModTime().Format(format), true
|
||||
}
|
||||
|
||||
// GetOwner returns the owner of the resource. This is usually the principal resource associated (the root resource).
|
||||
// If the resource does not have a owner (for example it's a principal resource alread), it returns an empty string.
|
||||
func (r *Resource) GetOwner() (string, bool) {
|
||||
var owner string
|
||||
if len(r.pathSplit) > 1 {
|
||||
owner = r.pathSplit[0]
|
||||
} else {
|
||||
owner = ""
|
||||
}
|
||||
|
||||
return owner, true
|
||||
}
|
||||
|
||||
// GetOwnerPath returns the path to this resource's owner, or an empty string when the resource does not have any owner.
|
||||
func (r *Resource) GetOwnerPath() (string, bool) {
|
||||
owner, _ := r.GetOwner()
|
||||
|
||||
if owner != "" {
|
||||
return fmt.Sprintf("/%s/", owner), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// TODO: memoize
|
||||
func (r *Resource) icalVEVENT() *ical.Node {
|
||||
vevent := r.icalendar().ChildByName(ical.VEVENT)
|
||||
|
||||
// if nil, log it and return an empty vevent
|
||||
if vevent == nil {
|
||||
log.Printf("WARNING: The resource's ical data is missing the VEVENT property.\nResource path: %s", r.Path)
|
||||
|
||||
return &ical.Node{
|
||||
Name: ical.VEVENT,
|
||||
}
|
||||
}
|
||||
|
||||
return vevent
|
||||
}
|
||||
|
||||
// TODO: memoize
|
||||
func (r *Resource) icalendar() *ical.Node {
|
||||
data, found := r.GetContentData()
|
||||
|
||||
if !found {
|
||||
log.Printf("WARNING: The resource's ical data does not have any data.\nResource path: %s", r.Path)
|
||||
return &ical.Node{
|
||||
Name: ical.VCALENDAR,
|
||||
}
|
||||
}
|
||||
|
||||
icalNode, err := ical.ParseCalendar(data)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not parse the resource's ical data.\nError: %s.\nResource path: %s", err, r.Path)
|
||||
return &ical.Node{
|
||||
Name: ical.VCALENDAR,
|
||||
}
|
||||
}
|
||||
|
||||
return icalNode
|
||||
}
|
||||
|
||||
// FileResourceAdapter implements the `ResourceAdapter` for resources stored as files in the file system.
|
||||
type FileResourceAdapter struct {
|
||||
finfo os.FileInfo
|
||||
resourcePath string
|
||||
}
|
||||
|
||||
// IsCollection tells whether the file resource is a directory or not.
|
||||
func (adp *FileResourceAdapter) IsCollection() bool {
|
||||
return adp.finfo.IsDir()
|
||||
}
|
||||
|
||||
// GetContent reads the file content and returns it as string. For collection resources (directories), it
|
||||
// returns an empty string.
|
||||
func (adp *FileResourceAdapter) GetContent() string {
|
||||
if adp.IsCollection() {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(files.AbsPath(adp.resourcePath))
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not read file content for the resource.\nError: %s.\nResource path: %s.", err, adp.resourcePath)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// GetContentSize returns the content length.
|
||||
func (adp *FileResourceAdapter) GetContentSize() int64 {
|
||||
return adp.finfo.Size()
|
||||
}
|
||||
|
||||
// CalculateEtag calculates an ETag based on the file current modification status and returns it.
|
||||
func (adp *FileResourceAdapter) CalculateEtag() string {
|
||||
// returns ETag as the concatenated hex values of a file's
|
||||
// modification time and size. This is not a reliable synchronization
|
||||
// mechanism for directories, so for collections we return empty.
|
||||
if adp.IsCollection() {
|
||||
return ""
|
||||
}
|
||||
|
||||
fi := adp.finfo
|
||||
return fmt.Sprintf(`"%x%x"`, fi.ModTime().UnixNano(), fi.Size())
|
||||
}
|
||||
|
||||
// GetModTime returns the time when the file was last modified.
|
||||
func (adp *FileResourceAdapter) GetModTime() time.Time {
|
||||
return adp.finfo.ModTime()
|
||||
}
|
229
vendor/github.com/samedi/caldav-go/data/storage.go
generated
vendored
Normal file
229
vendor/github.com/samedi/caldav-go/data/storage.go
generated
vendored
Normal file
|
@ -0,0 +1,229 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/files"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Storage is the inteface responsible for the CRUD operations on the CalDAV resources. It represents
|
||||
// where the resources should be fetched from and the various operations which can be performed on it.
|
||||
// This is the interface one should implement in case it needs a custom storage strategy, like fetching
|
||||
// data from the cloud, local DB, etc. After that, the custom storage implementation can be setup to be used
|
||||
// in the server by passing the object instance to `caldav.SetupStorage`.
|
||||
type Storage interface {
|
||||
// GetResources gets a list of resources based on a given `rpath`. The
|
||||
// `rpath` is the path to the original resource that's being requested. The resultant list
|
||||
// will/must contain that original resource in it, apart from any additional resources. It also receives
|
||||
// `withChildren` flag to say if the result must also include all the original resource`s
|
||||
// children (if original is a collection resource). If `true`, the result will have the requested resource + children.
|
||||
// If `false`, it will have only the requested original resource (from the `rpath` path).
|
||||
// It returns errors if anything went wrong or if it could not find any resource on `rpath` path.
|
||||
GetResources(rpath string, withChildren bool) ([]Resource, error)
|
||||
// GetResourcesByList fetches a list of resources by path from the storage.
|
||||
// This method fetches all the `rpaths` and return an array of the reosurces found.
|
||||
// No error 404 will be returned if one of the resources cannot be found.
|
||||
// Errors are returned if any errors other than "not found" happens.
|
||||
GetResourcesByList(rpaths []string) ([]Resource, error)
|
||||
// GetResourcesByFilters returns the filtered children of a target collection resource.
|
||||
// The target collection resource is the one pointed by the `rpath` parameter. All of its children
|
||||
// will be checked against a set of `filters` and the matching ones are returned. The results
|
||||
// contains only the filtered children and does NOT include the target resource. If the target resource
|
||||
// is not a collection, an empty array is returned as the result.
|
||||
GetResourcesByFilters(rpath string, filters *ResourceFilter) ([]Resource, error)
|
||||
// GetResource gets the requested resource based on a given `rpath` path. It returns the resource (if found) or
|
||||
// nil (if not found). Also returns a flag specifying if the resource was found or not.
|
||||
GetResource(rpath string) (*Resource, bool, error)
|
||||
// GetShallowResource has the same behaviour of `storage.GetResource`. The only difference is that, for collection resources,
|
||||
// it does not return its children in the collection `storage.Resource` struct (hence the name shallow). The motive is
|
||||
// for optimizations reasons, as this function is used on places where the collection's children are not important.
|
||||
GetShallowResource(rpath string) (*Resource, bool, error)
|
||||
// CreateResource creates a new resource on the `rpath` path with a given `content`.
|
||||
CreateResource(rpath, content string) (*Resource, error)
|
||||
// UpdateResource udpates a resource on the `rpath` path with a given `content`.
|
||||
UpdateResource(rpath, content string) (*Resource, error)
|
||||
// DeleteResource deletes a resource on the `rpath` path.
|
||||
DeleteResource(rpath string) error
|
||||
}
|
||||
|
||||
// FileStorage is the storage that deals with resources as files in the file system. So, a collection resource
|
||||
// is treated as a folder/directory and its children resources are the files it contains. Non-collection resources are just plain files.
|
||||
// Each file represents then a CalAV resource and the data expects to contain the iCal data to feed the calendar events.
|
||||
type FileStorage struct {
|
||||
}
|
||||
|
||||
// GetResources get the file resources based on the `rpath`. See `Storage.GetResources` doc.
|
||||
func (fs *FileStorage) GetResources(rpath string, withChildren bool) ([]Resource, error) {
|
||||
result := []Resource{}
|
||||
|
||||
// tries to open the file by the given path
|
||||
f, e := fs.openResourceFile(rpath, os.O_RDONLY)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// add it as a resource to the result list
|
||||
finfo, _ := f.Stat()
|
||||
resource := NewResource(rpath, &FileResourceAdapter{finfo, rpath})
|
||||
result = append(result, resource)
|
||||
|
||||
// if the file is a dir, add its children to the result list
|
||||
if withChildren && finfo.IsDir() {
|
||||
dirFiles, _ := f.Readdir(0)
|
||||
for _, finfo := range dirFiles {
|
||||
childPath := files.JoinPaths(rpath, finfo.Name())
|
||||
resource = NewResource(childPath, &FileResourceAdapter{finfo, childPath})
|
||||
result = append(result, resource)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetResourcesByFilters get the file resources based on the `rpath` and a set of filters. See `Storage.GetResourcesByFilters` doc.
|
||||
func (fs *FileStorage) GetResourcesByFilters(rpath string, filters *ResourceFilter) ([]Resource, error) {
|
||||
result := []Resource{}
|
||||
|
||||
childPaths := fs.getDirectoryChildPaths(rpath)
|
||||
for _, path := range childPaths {
|
||||
resource, _, err := fs.GetShallowResource(path)
|
||||
|
||||
if err != nil {
|
||||
// if we can't find this resource, something weird went wrong, but not that serious, so we log it and continue
|
||||
log.Printf("WARNING: returned error when trying to get resource with path %s from collection with path %s. Error: %s", path, rpath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// only add it if the resource matches the filters
|
||||
if filters == nil || filters.Match(resource) {
|
||||
result = append(result, *resource)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetResourcesByList get a list of file resources based on a list of `rpaths`. See `Storage.GetResourcesByList` doc.
|
||||
func (fs *FileStorage) GetResourcesByList(rpaths []string) ([]Resource, error) {
|
||||
results := []Resource{}
|
||||
|
||||
for _, rpath := range rpaths {
|
||||
resource, found, err := fs.GetShallowResource(rpath)
|
||||
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found {
|
||||
results = append(results, *resource)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetResource fetches and returns a single resource for a `rpath`. See `Storage.GetResoure` doc.
|
||||
func (fs *FileStorage) GetResource(rpath string) (*Resource, bool, error) {
|
||||
// For simplicity we just return the shallow resource.
|
||||
return fs.GetShallowResource(rpath)
|
||||
}
|
||||
|
||||
// GetShallowResource fetches and returns a single resource file/directory without any related children. See `Storage.GetShallowResource` doc.
|
||||
func (fs *FileStorage) GetShallowResource(rpath string) (*Resource, bool, error) {
|
||||
resources, err := fs.GetResources(rpath, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if resources == nil || len(resources) == 0 {
|
||||
return nil, false, errs.ResourceNotFoundError
|
||||
}
|
||||
|
||||
res := resources[0]
|
||||
return &res, true, nil
|
||||
}
|
||||
|
||||
// CreateResource creates a file resource with the provided `content`. See `Storage.CreateResource` doc.
|
||||
func (fs *FileStorage) CreateResource(rpath, content string) (*Resource, error) {
|
||||
rAbsPath := files.AbsPath(rpath)
|
||||
|
||||
if fs.isResourcePresent(rAbsPath) {
|
||||
return nil, errs.ResourceAlreadyExistsError
|
||||
}
|
||||
|
||||
// create parent directories (if needed)
|
||||
if err := os.MkdirAll(files.DirPath(rAbsPath), os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create file/resource and write content
|
||||
f, err := os.Create(rAbsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.WriteString(content)
|
||||
|
||||
finfo, _ := f.Stat()
|
||||
res := NewResource(rpath, &FileResourceAdapter{finfo, rpath})
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// UpdateResource updates a file resource with the provided `content`. See `Storage.UpdateResource` doc.
|
||||
func (fs *FileStorage) UpdateResource(rpath, content string) (*Resource, error) {
|
||||
f, e := fs.openResourceFile(rpath, os.O_RDWR)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
// update content
|
||||
f.Truncate(0)
|
||||
f.WriteString(content)
|
||||
|
||||
finfo, _ := f.Stat()
|
||||
res := NewResource(rpath, &FileResourceAdapter{finfo, rpath})
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// DeleteResource deletes a file resource (and possibly all its children in case of a collection). See `Storage.DeleteResource` doc.
|
||||
func (fs *FileStorage) DeleteResource(rpath string) error {
|
||||
err := os.Remove(files.AbsPath(rpath))
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *FileStorage) isResourcePresent(rpath string) bool {
|
||||
_, found, _ := fs.GetShallowResource(rpath)
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
func (fs *FileStorage) openResourceFile(filepath string, mode int) (*os.File, error) {
|
||||
f, e := os.OpenFile(files.AbsPath(filepath), mode, 0666)
|
||||
if e != nil {
|
||||
if os.IsNotExist(e) {
|
||||
return nil, errs.ResourceNotFoundError
|
||||
}
|
||||
return nil, e
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (fs *FileStorage) getDirectoryChildPaths(dirpath string) []string {
|
||||
content, err := ioutil.ReadDir(files.AbsPath(dirpath))
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Could not read resource as file directory.\nError: %s.\nResource path: %s.", err, dirpath)
|
||||
return nil
|
||||
}
|
||||
|
||||
result := []string{}
|
||||
for _, file := range content {
|
||||
fpath := files.JoinPaths(dirpath, file.Name())
|
||||
result = append(result, fpath)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
8
vendor/github.com/samedi/caldav-go/data/user.go
generated
vendored
Normal file
8
vendor/github.com/samedi/caldav-go/data/user.go
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
package data
|
||||
|
||||
// CalUser represents the calendar user. It is used, for example, to
|
||||
// keep track globally what is the current user interacting with the calendar.
|
||||
// This user data can be used in various places, including in some of the CALDAV responses.
|
||||
type CalUser struct {
|
||||
Name string
|
||||
}
|
12
vendor/github.com/samedi/caldav-go/errs/errors.go
generated
vendored
Normal file
12
vendor/github.com/samedi/caldav-go/errs/errors.go
generated
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
package errs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ResourceNotFoundError = errors.New("caldav: resource not found")
|
||||
ResourceAlreadyExistsError = errors.New("caldav: resource already exists")
|
||||
UnauthorizedError = errors.New("caldav: unauthorized. credentials needed.")
|
||||
ForbiddenError = errors.New("caldav: forbidden operation.")
|
||||
)
|
34
vendor/github.com/samedi/caldav-go/files/paths.go
generated
vendored
Normal file
34
vendor/github.com/samedi/caldav-go/files/paths.go
generated
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
package files
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
Separator = string(filepath.Separator)
|
||||
)
|
||||
|
||||
// AbsPath converts the path into absolute path based on the current working directory.
|
||||
func AbsPath(path string) string {
|
||||
path = strings.Trim(path, "/")
|
||||
absPath, _ := filepath.Abs(path)
|
||||
|
||||
return absPath
|
||||
}
|
||||
|
||||
// DirPath returns all but the last element of path, typically the path's directory.
|
||||
func DirPath(path string) string {
|
||||
return filepath.Dir(path)
|
||||
}
|
||||
|
||||
// JoinPaths joins two or more paths into a single path.
|
||||
func JoinPaths(paths ...string) string {
|
||||
return filepath.Join(paths...)
|
||||
}
|
||||
|
||||
// ToSlashPath slashify the path, using '/' as separator.
|
||||
func ToSlashPath(path string) string {
|
||||
return lib.ToSlashPath(path)
|
||||
}
|
8
vendor/github.com/samedi/caldav-go/glide.lock
generated
vendored
Normal file
8
vendor/github.com/samedi/caldav-go/glide.lock
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
hash: ff037054c7b689c20a4fd4368897b8998d6ffa31b0f3f4a79e523a132e6a9e94
|
||||
updated: 2018-04-04T08:38:45.523594-04:00
|
||||
imports:
|
||||
- name: github.com/beevik/etree
|
||||
version: af219c0c7ea1b67ec263c0b1b1b96d284a9181ce
|
||||
- name: github.com/laurent22/ical-go
|
||||
version: e4fec34929693e2a4ba299d16380c55bac3fb42c
|
||||
testImports: []
|
4
vendor/github.com/samedi/caldav-go/glide.yaml
generated
vendored
Normal file
4
vendor/github.com/samedi/caldav-go/glide.yaml
generated
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
package: github.com/samedi/caldav-go
|
||||
import:
|
||||
- package: github.com/beevik/etree
|
||||
- package: github.com/laurent22/ical-go
|
17
vendor/github.com/samedi/caldav-go/global/global.go
generated
vendored
Normal file
17
vendor/github.com/samedi/caldav-go/global/global.go
generated
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Package global defines the globally accessible variables in the caldav server
|
||||
// and the interface to setup them.
|
||||
package global
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
// Storage represents the global storage used in the CRUD operations of resources. Default storage is the `data.FileStorage`.
|
||||
var Storage data.Storage = new(data.FileStorage)
|
||||
|
||||
// User defines the current caldav user, which is the user currently interacting with the calendar.
|
||||
var User *data.CalUser
|
||||
|
||||
// SupportedComponents contains all components which are supported by the current storage implementation
|
||||
var SupportedComponents = []string{lib.VCALENDAR, lib.VEVENT}
|
8
vendor/github.com/samedi/caldav-go/go.mod
generated
vendored
Normal file
8
vendor/github.com/samedi/caldav-go/go.mod
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
module github.com/samedi/caldav-go
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/beevik/etree v0.0.0-20171015221209-af219c0c7ea1
|
||||
github.com/laurent22/ical-go v0.0.0-20170824131750-e4fec3492969
|
||||
)
|
29
vendor/github.com/samedi/caldav-go/handler.go
generated
vendored
Normal file
29
vendor/github.com/samedi/caldav-go/handler.go
generated
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
package caldav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/handlers"
|
||||
)
|
||||
|
||||
// RequestHandler handles the given CALDAV request and writes the reponse righ away. This function is to be
|
||||
// used by passing it directly as the handle func to the `http` lib. Example: http.HandleFunc("/", caldav.RequestHandler).
|
||||
func RequestHandler(writer http.ResponseWriter, request *http.Request) {
|
||||
response := HandleRequest(request)
|
||||
response.Write(writer)
|
||||
}
|
||||
|
||||
// HandleRequest handles the given CALDAV request and returns the response. Useful when the caller
|
||||
// wants to do something else with the response before writing it to the response stream.
|
||||
func HandleRequest(request *http.Request) *handlers.Response {
|
||||
handler := handlers.NewHandler(request)
|
||||
return handler.Handle()
|
||||
}
|
||||
|
||||
// HandleRequestWithStorage handles the request the same way as `HandleRequest` does, but before,
|
||||
// it sets the given storage that will be used throughout the request handling flow.
|
||||
func HandleRequestWithStorage(request *http.Request, stg data.Storage) *handlers.Response {
|
||||
SetupStorage(stg)
|
||||
return HandleRequest(request)
|
||||
}
|
36
vendor/github.com/samedi/caldav-go/handlers/builder.go
generated
vendored
Normal file
36
vendor/github.com/samedi/caldav-go/handlers/builder.go
generated
vendored
Normal file
|
@ -0,0 +1,36 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HandlerInterface represents a CalDAV request handler. It has only one function `Handle`,
|
||||
// which is used to handle the CalDAV request and returns the response.
|
||||
type HandlerInterface interface {
|
||||
Handle() *Response
|
||||
}
|
||||
|
||||
// NewHandler returns a new CalDAV request handler object based on the provided request.
|
||||
// With the returned request handler, you can call `Handle()` to handle the request.
|
||||
func NewHandler(request *http.Request) HandlerInterface {
|
||||
response := NewResponse()
|
||||
|
||||
switch request.Method {
|
||||
case "GET":
|
||||
return getHandler{request, response, false}
|
||||
case "HEAD":
|
||||
return getHandler{request, response, true}
|
||||
case "PUT":
|
||||
return putHandler{request, response}
|
||||
case "DELETE":
|
||||
return deleteHandler{request, response}
|
||||
case "PROPFIND":
|
||||
return propfindHandler{request, response}
|
||||
case "OPTIONS":
|
||||
return optionsHandler{response}
|
||||
case "REPORT":
|
||||
return reportHandler{request, response}
|
||||
default:
|
||||
return notImplementedHandler{response}
|
||||
}
|
||||
}
|
40
vendor/github.com/samedi/caldav-go/handlers/delete.go
generated
vendored
Normal file
40
vendor/github.com/samedi/caldav-go/handlers/delete.go
generated
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type deleteHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (dh deleteHandler) Handle() *Response {
|
||||
precond := requestPreconditions{dh.request}
|
||||
|
||||
// get the event from the storage
|
||||
resource, _, err := global.Storage.GetShallowResource(dh.request.URL.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
|
||||
// TODO: Handle delete on collections
|
||||
if resource.IsCollection() {
|
||||
return dh.response.Set(http.StatusMethodNotAllowed, "")
|
||||
}
|
||||
|
||||
// check ETag pre-condition
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if !precond.IfMatch(resourceEtag) {
|
||||
return dh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
// delete event after pre-condition passed
|
||||
err = global.Storage.DeleteResource(resource.Path)
|
||||
if err != nil {
|
||||
return dh.response.SetError(err)
|
||||
}
|
||||
|
||||
return dh.response.Set(http.StatusNoContent, "")
|
||||
}
|
37
vendor/github.com/samedi/caldav-go/handlers/get.go
generated
vendored
Normal file
37
vendor/github.com/samedi/caldav-go/handlers/get.go
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type getHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
onlyHeaders bool
|
||||
}
|
||||
|
||||
func (gh getHandler) Handle() *Response {
|
||||
resource, _, err := global.Storage.GetResource(gh.request.URL.Path)
|
||||
if err != nil {
|
||||
return gh.response.SetError(err)
|
||||
}
|
||||
|
||||
var response string
|
||||
if gh.onlyHeaders {
|
||||
response = ""
|
||||
} else {
|
||||
response, _ = resource.GetContentData()
|
||||
}
|
||||
|
||||
etag, _ := resource.GetEtag()
|
||||
lastm, _ := resource.GetLastModified(http.TimeFormat)
|
||||
ctype, _ := resource.GetContentType()
|
||||
|
||||
gh.response.SetHeader("ETag", etag).
|
||||
SetHeader("Last-Modified", lastm).
|
||||
SetHeader("Content-Type", ctype).
|
||||
Set(http.StatusOK, response)
|
||||
|
||||
return gh.response
|
||||
}
|
27
vendor/github.com/samedi/caldav-go/handlers/headers.go
generated
vendored
Normal file
27
vendor/github.com/samedi/caldav-go/handlers/headers.go
generated
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
HD_DEPTH = "Depth"
|
||||
HD_DEPTH_DEEP = "1"
|
||||
HD_PREFER = "Prefer"
|
||||
HD_PREFER_MINIMAL = "return=minimal"
|
||||
HD_PREFERENCE_APPLIED = "Preference-Applied"
|
||||
)
|
||||
|
||||
type headers struct {
|
||||
http.Header
|
||||
}
|
||||
|
||||
func (h headers) IsDeep() bool {
|
||||
depth := h.Get(HD_DEPTH)
|
||||
return (depth == HD_DEPTH_DEEP)
|
||||
}
|
||||
|
||||
func (h headers) IsMinimal() bool {
|
||||
prefer := h.Get(HD_PREFER)
|
||||
return (prefer == HD_PREFER_MINIMAL)
|
||||
}
|
207
vendor/github.com/samedi/caldav-go/handlers/multistatus.go
generated
vendored
Normal file
207
vendor/github.com/samedi/caldav-go/handlers/multistatus.go
generated
vendored
Normal file
|
@ -0,0 +1,207 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Wraps a multistatus response. It contains the set of `Responses`
|
||||
// that will serve to build the final XML. Multistatus responses are
|
||||
// used by the REPORT and PROPFIND methods.
|
||||
type multistatusResp struct {
|
||||
// The set of multistatus responses used to build each of the <DAV:response> nodes.
|
||||
Responses []msResponse
|
||||
// Flag that XML should be minimal or not
|
||||
// [defined in the draft https://tools.ietf.org/html/draft-murchison-webdav-prefer-05]
|
||||
Minimal bool
|
||||
}
|
||||
|
||||
type msResponse struct {
|
||||
Href string
|
||||
Found bool
|
||||
Propstats msPropstats
|
||||
}
|
||||
|
||||
type msPropstats map[int]msProps
|
||||
|
||||
// Adds a msProp to the map with the key being the prop status.
|
||||
func (stats msPropstats) Add(prop msProp) {
|
||||
stats[prop.Status] = append(stats[prop.Status], prop)
|
||||
}
|
||||
|
||||
func (stats msPropstats) Clone() msPropstats {
|
||||
clone := make(msPropstats)
|
||||
|
||||
for k, v := range stats {
|
||||
clone[k] = v
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
type msProps []msProp
|
||||
|
||||
type msProp struct {
|
||||
Tag xml.Name
|
||||
Content string
|
||||
Contents []string
|
||||
Status int
|
||||
}
|
||||
|
||||
// Function that processes all the required props for a given resource.
|
||||
// ## Params
|
||||
// resource: the target calendar resource.
|
||||
// reqprops: set of required props that must be processed for the resource.
|
||||
// ## Returns
|
||||
// The set of props (msProp) processed. Each prop is mapped to a HTTP status code.
|
||||
// So if a prop is found and processed ok, it'll be mapped to 200. If it's not found,
|
||||
// it'll be mapped to 404, and so on.
|
||||
func (ms *multistatusResp) Propstats(resource *data.Resource, reqprops []xml.Name) msPropstats {
|
||||
if resource == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(msPropstats)
|
||||
|
||||
for _, ptag := range reqprops {
|
||||
pvalue := msProp{
|
||||
Tag: ptag,
|
||||
Status: http.StatusOK,
|
||||
}
|
||||
|
||||
pfound := false
|
||||
switch ptag {
|
||||
case ixml.CALENDAR_DATA_TG:
|
||||
pvalue.Content, pfound = resource.GetContentData()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_ETAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.GET_CONTENT_TYPE_TG:
|
||||
pvalue.Content, pfound = resource.GetContentType()
|
||||
case ixml.GET_CONTENT_LENGTH_TG:
|
||||
pvalue.Content, pfound = resource.GetContentLength()
|
||||
case ixml.DISPLAY_NAME_TG:
|
||||
pvalue.Content, pfound = resource.GetDisplayName()
|
||||
if pfound {
|
||||
pvalue.Content = ixml.EscapeText(pvalue.Content)
|
||||
}
|
||||
case ixml.GET_LAST_MODIFIED_TG:
|
||||
pvalue.Content, pfound = resource.GetLastModified(http.TimeFormat)
|
||||
case ixml.OWNER_TG:
|
||||
pvalue.Content, pfound = resource.GetOwnerPath()
|
||||
case ixml.GET_CTAG_TG:
|
||||
pvalue.Content, pfound = resource.GetEtag()
|
||||
case ixml.PRINCIPAL_URL_TG,
|
||||
ixml.PRINCIPAL_COLLECTION_SET_TG,
|
||||
ixml.CALENDAR_USER_ADDRESS_SET_TG,
|
||||
ixml.CALENDAR_HOME_SET_TG:
|
||||
pvalue.Content, pfound = ixml.HrefTag(resource.Path), true
|
||||
case ixml.RESOURCE_TYPE_TG:
|
||||
if resource.IsCollection() {
|
||||
pvalue.Content, pfound = ixml.Tag(ixml.COLLECTION_TG, "")+ixml.Tag(ixml.CALENDAR_TG, ""), true
|
||||
|
||||
if resource.IsPrincipal() {
|
||||
pvalue.Content += ixml.Tag(ixml.PRINCIPAL_TG, "")
|
||||
}
|
||||
} else {
|
||||
// resourcetype must be returned empty for non-collection elements
|
||||
pvalue.Content, pfound = "", true
|
||||
}
|
||||
case ixml.CURRENT_USER_PRINCIPAL_TG:
|
||||
if global.User != nil {
|
||||
path := fmt.Sprintf("/%s/", global.User.Name)
|
||||
pvalue.Content, pfound = ixml.HrefTag(path), true
|
||||
}
|
||||
case ixml.SUPPORTED_CALENDAR_COMPONENT_SET_TG:
|
||||
if resource.IsCollection() {
|
||||
for _, component := range global.SupportedComponents {
|
||||
// TODO: use ixml somehow to build the below tag
|
||||
compTag := fmt.Sprintf(`<C:comp name="%s"/>`, component)
|
||||
pvalue.Contents = append(pvalue.Contents, compTag)
|
||||
}
|
||||
pfound = true
|
||||
}
|
||||
}
|
||||
|
||||
if !pfound {
|
||||
pvalue.Status = http.StatusNotFound
|
||||
}
|
||||
|
||||
result.Add(pvalue)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Adds a new `msResponse` to the `Responses` array.
|
||||
func (ms *multistatusResp) AddResponse(href string, found bool, propstats msPropstats) {
|
||||
ms.Responses = append(ms.Responses, msResponse{
|
||||
Href: href,
|
||||
Found: found,
|
||||
Propstats: propstats,
|
||||
})
|
||||
}
|
||||
|
||||
func (ms *multistatusResp) ToXML() string {
|
||||
// init multistatus
|
||||
var bf lib.StringBuffer
|
||||
bf.Write(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||
bf.Write(`<D:multistatus %s>`, ixml.Namespaces())
|
||||
|
||||
// iterate over event hrefs and build multistatus XML on the fly
|
||||
for _, response := range ms.Responses {
|
||||
bf.Write("<D:response>")
|
||||
bf.Write(ixml.HrefTag(response.Href))
|
||||
|
||||
if response.Found {
|
||||
propstats := response.Propstats.Clone()
|
||||
|
||||
if ms.Minimal {
|
||||
delete(propstats, http.StatusNotFound)
|
||||
|
||||
if len(propstats) == 0 {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop/>")
|
||||
bf.Write(ixml.StatusTag(http.StatusOK))
|
||||
bf.Write("</D:propstat>")
|
||||
bf.Write("</D:response>")
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for status, props := range propstats {
|
||||
bf.Write("<D:propstat>")
|
||||
bf.Write("<D:prop>")
|
||||
for _, prop := range props {
|
||||
bf.Write(ms.propToXML(prop))
|
||||
}
|
||||
bf.Write("</D:prop>")
|
||||
bf.Write(ixml.StatusTag(status))
|
||||
bf.Write("</D:propstat>")
|
||||
}
|
||||
} else {
|
||||
// if does not find the resource set 404
|
||||
bf.Write(ixml.StatusTag(http.StatusNotFound))
|
||||
}
|
||||
bf.Write("</D:response>")
|
||||
}
|
||||
bf.Write("</D:multistatus>")
|
||||
|
||||
return bf.String()
|
||||
}
|
||||
|
||||
func (ms *multistatusResp) propToXML(prop msProp) string {
|
||||
for _, content := range prop.Contents {
|
||||
prop.Content += content
|
||||
}
|
||||
xmlString := ixml.Tag(prop.Tag, prop.Content)
|
||||
return xmlString
|
||||
}
|
13
vendor/github.com/samedi/caldav-go/handlers/not_implemented.go
generated
vendored
Normal file
13
vendor/github.com/samedi/caldav-go/handlers/not_implemented.go
generated
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type notImplementedHandler struct {
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (h notImplementedHandler) Handle() *Response {
|
||||
return h.response.Set(http.StatusNotImplemented, "")
|
||||
}
|
23
vendor/github.com/samedi/caldav-go/handlers/options.go
generated
vendored
Normal file
23
vendor/github.com/samedi/caldav-go/handlers/options.go
generated
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type optionsHandler struct {
|
||||
response *Response
|
||||
}
|
||||
|
||||
// Returns the allowed methods and the DAV features implemented by the current server.
|
||||
// For more information about the values and format read RFC4918 Sections 10.1 and 18.
|
||||
func (oh optionsHandler) Handle() *Response {
|
||||
// Set the DAV compliance header:
|
||||
// 1: Server supports all the requirements specified in RFC2518
|
||||
// 3: Server supports all the revisions specified in RFC4918
|
||||
// calendar-access: Server supports all the extensions specified in RFC4791
|
||||
oh.response.SetHeader("DAV", "1, 3, calendar-access").
|
||||
SetHeader("Allow", "GET, HEAD, PUT, DELETE, OPTIONS, PROPFIND, REPORT").
|
||||
Set(http.StatusOK, "")
|
||||
|
||||
return oh.response
|
||||
}
|
23
vendor/github.com/samedi/caldav-go/handlers/preconditions.go
generated
vendored
Normal file
23
vendor/github.com/samedi/caldav-go/handlers/preconditions.go
generated
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type requestPreconditions struct {
|
||||
request *http.Request
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfMatch(etag string) bool {
|
||||
etagMatch := p.request.Header["If-Match"]
|
||||
return len(etagMatch) == 0 || etagMatch[0] == "*" || etagMatch[0] == etag
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfMatchPresent() bool {
|
||||
return len(p.request.Header["If-Match"]) != 0
|
||||
}
|
||||
|
||||
func (p *requestPreconditions) IfNoneMatch(value string) bool {
|
||||
valueMatch := p.request.Header["If-None-Match"]
|
||||
return len(valueMatch) == 1 && valueMatch[0] == value
|
||||
}
|
49
vendor/github.com/samedi/caldav-go/handlers/propfind.go
generated
vendored
Normal file
49
vendor/github.com/samedi/caldav-go/handlers/propfind.go
generated
vendored
Normal file
|
@ -0,0 +1,49 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type propfindHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (ph propfindHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(ph.request)
|
||||
header := headers{ph.request.Header}
|
||||
|
||||
// get the target resources based on the request URL
|
||||
resources, err := global.Storage.GetResources(ph.request.URL.Path, header.IsDeep())
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
// read body string to xml struct
|
||||
type XMLProp2 struct {
|
||||
Tags []xml.Name `xml:",any"`
|
||||
}
|
||||
type XMLRoot2 struct {
|
||||
XMLName xml.Name
|
||||
Prop XMLProp2 `xml:"DAV: prop"`
|
||||
}
|
||||
var requestXML XMLRoot2
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, resource := range resources {
|
||||
propstats := multistatus.Propstats(&resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(resource.Path, true, propstats)
|
||||
}
|
||||
|
||||
if multistatus.Minimal {
|
||||
ph.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
|
||||
return ph.response.Set(207, multistatus.ToXML())
|
||||
}
|
65
vendor/github.com/samedi/caldav-go/handlers/put.go
generated
vendored
Normal file
65
vendor/github.com/samedi/caldav-go/handlers/put.go
generated
vendored
Normal file
|
@ -0,0 +1,65 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type putHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
func (ph putHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(ph.request)
|
||||
precond := requestPreconditions{ph.request}
|
||||
success := false
|
||||
|
||||
// check if resource exists
|
||||
resourcePath := ph.request.URL.Path
|
||||
resource, found, err := global.Storage.GetShallowResource(resourcePath)
|
||||
if err != nil && err != errs.ResourceNotFoundError {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
// PUT is allowed in 2 cases:
|
||||
//
|
||||
// 1. Item NOT FOUND and there is NO ETAG match header: CREATE a new item
|
||||
if !found && !precond.IfMatchPresent() {
|
||||
// create new event resource
|
||||
resource, err = global.Storage.CreateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
success = true
|
||||
}
|
||||
|
||||
if found {
|
||||
// TODO: Handle PUT on collections
|
||||
if resource.IsCollection() {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
// 2. Item exists, the resource etag is verified and there's no IF-NONE-MATCH=* header: UPDATE the item
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
if found && precond.IfMatch(resourceEtag) && !precond.IfNoneMatch("*") {
|
||||
// update resource
|
||||
resource, err = global.Storage.UpdateResource(resourcePath, requestBody)
|
||||
if err != nil {
|
||||
return ph.response.SetError(err)
|
||||
}
|
||||
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
return ph.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
resourceEtag, _ := resource.GetEtag()
|
||||
return ph.response.SetHeader("ETag", resourceEtag).
|
||||
Set(http.StatusCreated, "")
|
||||
}
|
168
vendor/github.com/samedi/caldav-go/handlers/report.go
generated
vendored
Normal file
168
vendor/github.com/samedi/caldav-go/handlers/report.go
generated
vendored
Normal file
|
@ -0,0 +1,168 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/samedi/caldav-go/data"
|
||||
"github.com/samedi/caldav-go/global"
|
||||
"github.com/samedi/caldav-go/ixml"
|
||||
)
|
||||
|
||||
type reportHandler struct {
|
||||
request *http.Request
|
||||
response *Response
|
||||
}
|
||||
|
||||
// See more at RFC4791#section-7.1
|
||||
func (rh reportHandler) Handle() *Response {
|
||||
requestBody := readRequestBody(rh.request)
|
||||
header := headers{rh.request.Header}
|
||||
|
||||
urlResource, found, err := global.Storage.GetShallowResource(rh.request.URL.Path)
|
||||
if !found {
|
||||
return rh.response.Set(http.StatusNotFound, "")
|
||||
} else if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
|
||||
// read body string to xml struct
|
||||
var requestXML reportRootXML
|
||||
xml.Unmarshal([]byte(requestBody), &requestXML)
|
||||
|
||||
// The resources to be reported are fetched by the type of the request. If it is
|
||||
// a `calendar-multiget`, the resources come based on a set of `hrefs` in the request body.
|
||||
// If it is a `calendar-query`, the resources are calculated based on set of filters in the request.
|
||||
var resourcesToReport []reportRes
|
||||
switch requestXML.XMLName {
|
||||
case ixml.CALENDAR_MULTIGET_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByList(urlResource, requestXML.Hrefs)
|
||||
case ixml.CALENDAR_QUERY_TG:
|
||||
resourcesToReport, err = rh.fetchResourcesByFilters(urlResource, requestXML.Filters)
|
||||
default:
|
||||
return rh.response.Set(http.StatusPreconditionFailed, "")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return rh.response.SetError(err)
|
||||
}
|
||||
|
||||
multistatus := &multistatusResp{
|
||||
Minimal: header.IsMinimal(),
|
||||
}
|
||||
// for each href, build the multistatus responses
|
||||
for _, r := range resourcesToReport {
|
||||
propstats := multistatus.Propstats(r.resource, requestXML.Prop.Tags)
|
||||
multistatus.AddResponse(r.href, r.found, propstats)
|
||||
}
|
||||
|
||||
if multistatus.Minimal {
|
||||
rh.response.SetHeader(HD_PREFERENCE_APPLIED, HD_PREFER_MINIMAL)
|
||||
}
|
||||
|
||||
return rh.response.Set(207, multistatus.ToXML())
|
||||
}
|
||||
|
||||
type reportPropXML struct {
|
||||
Tags []xml.Name `xml:",any"`
|
||||
}
|
||||
|
||||
type reportRootXML struct {
|
||||
XMLName xml.Name
|
||||
Prop reportPropXML `xml:"DAV: prop"`
|
||||
Hrefs []string `xml:"DAV: href"`
|
||||
Filters reportFilterXML `xml:"urn:ietf:params:xml:ns:caldav filter"`
|
||||
}
|
||||
|
||||
type reportFilterXML struct {
|
||||
XMLName xml.Name
|
||||
InnerContent string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
func (rfXml reportFilterXML) toString() string {
|
||||
return fmt.Sprintf("<%s>%s</%s>", rfXml.XMLName.Local, rfXml.InnerContent, rfXml.XMLName.Local)
|
||||
}
|
||||
|
||||
// Wraps a resource that has to be reported, either fetched by filters or by a list.
|
||||
// Basically it contains the original requested `href`, the actual `resource` (can be nil)
|
||||
// and if the `resource` was `found` or not
|
||||
type reportRes struct {
|
||||
href string
|
||||
resource *data.Resource
|
||||
found bool
|
||||
}
|
||||
|
||||
// The resources are fetched based on the origin resource and a set of filters.
|
||||
// If the origin resource is a collection, the filters are checked against each of the collection's resources
|
||||
// to see if they match. The collection's resources that match the filters are returned. The ones that will be returned
|
||||
// are the resources that were not found (does not exist) and the ones that matched the filters. The ones that did not
|
||||
// match the filter will not appear in the response result.
|
||||
// If the origin resource is not a collection, the function just returns it and ignore any filter processing.
|
||||
// [See RFC4791#section-7.8]
|
||||
func (rh reportHandler) fetchResourcesByFilters(origin *data.Resource, filtersXML reportFilterXML) ([]reportRes, error) {
|
||||
// The list of resources that has to be reported back in the response.
|
||||
reps := []reportRes{}
|
||||
|
||||
if origin.IsCollection() {
|
||||
filters, _ := data.ParseResourceFilters(filtersXML.toString())
|
||||
resources, err := global.Storage.GetResourcesByFilters(origin.Path, filters)
|
||||
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
|
||||
for in, resource := range resources {
|
||||
reps = append(reps, reportRes{resource.Path, &resources[in], true})
|
||||
}
|
||||
} else {
|
||||
// the origin resource is not a collection, so returns just that as the result
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
|
||||
return reps, nil
|
||||
}
|
||||
|
||||
// The hrefs can come from (1) the request URL or (2) from the request body itself.
|
||||
// If the origin resource from the URL points to a collection (2), we will check the request body
|
||||
// to get the requested `hrefs` (resource paths). Each requested href has to be related to the collection.
|
||||
// The ones that are not, we simply ignore them.
|
||||
// If the resource from the URL is NOT a collection (1) we process the the report only for this resource
|
||||
// and ignore any othre requested hrefs that might be present in the request body.
|
||||
// [See RFC4791#section-7.9]
|
||||
func (rh reportHandler) fetchResourcesByList(origin *data.Resource, requestedPaths []string) ([]reportRes, error) {
|
||||
reps := []reportRes{}
|
||||
|
||||
if origin.IsCollection() {
|
||||
resources, err := global.Storage.GetResourcesByList(requestedPaths)
|
||||
|
||||
if err != nil {
|
||||
return reps, err
|
||||
}
|
||||
|
||||
// we put all the resources found in a map path -> resource.
|
||||
// this will be used later to query which requested resource was found
|
||||
// or not and mount the response
|
||||
resourcesMap := make(map[string]*data.Resource)
|
||||
for _, resource := range resources {
|
||||
r := resource
|
||||
resourcesMap[resource.Path] = &r
|
||||
}
|
||||
|
||||
for _, requestedPath := range requestedPaths {
|
||||
// if the requested path does not belong to the origin collection, skip
|
||||
// ('belonging' means that the path's prefix is the same as the collection path)
|
||||
if !strings.HasPrefix(requestedPath, origin.Path) {
|
||||
continue
|
||||
}
|
||||
|
||||
resource, found := resourcesMap[requestedPath]
|
||||
reps = append(reps, reportRes{requestedPath, resource, found})
|
||||
}
|
||||
} else {
|
||||
reps = append(reps, reportRes{origin.Path, origin, true})
|
||||
}
|
||||
|
||||
return reps, nil
|
||||
}
|
72
vendor/github.com/samedi/caldav-go/handlers/response.go
generated
vendored
Normal file
72
vendor/github.com/samedi/caldav-go/handlers/response.go
generated
vendored
Normal file
|
@ -0,0 +1,72 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/samedi/caldav-go/errs"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Response represents the handled CalDAV response. Used this when one needs to proxy the generated
|
||||
// response before being sent back to the client.
|
||||
type Response struct {
|
||||
Status int
|
||||
Header http.Header
|
||||
Body string
|
||||
Error error
|
||||
}
|
||||
|
||||
// NewResponse initializes a new response object.
|
||||
func NewResponse() *Response {
|
||||
return &Response{
|
||||
Header: make(http.Header),
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the the status and body of the response.
|
||||
func (r *Response) Set(status int, body string) *Response {
|
||||
r.Status = status
|
||||
r.Body = body
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// SetHeader adds a header to the response.
|
||||
func (r *Response) SetHeader(key, value string) *Response {
|
||||
r.Header.Set(key, value)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// SetError sets the response as an error. It inflects the response status based on the provided error.
|
||||
func (r *Response) SetError(err error) *Response {
|
||||
r.Error = err
|
||||
|
||||
switch err {
|
||||
case errs.ResourceNotFoundError:
|
||||
r.Status = http.StatusNotFound
|
||||
case errs.UnauthorizedError:
|
||||
r.Status = http.StatusUnauthorized
|
||||
case errs.ForbiddenError:
|
||||
r.Status = http.StatusForbidden
|
||||
default:
|
||||
r.Status = http.StatusInternalServerError
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Write writes the response back to the client using the provided `ResponseWriter`.
|
||||
func (r *Response) Write(writer http.ResponseWriter) {
|
||||
if r.Error == errs.UnauthorizedError {
|
||||
r.SetHeader("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
}
|
||||
|
||||
for key, values := range r.Header {
|
||||
for _, value := range values {
|
||||
writer.Header().Set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteHeader(r.Status)
|
||||
io.WriteString(writer, r.Body)
|
||||
}
|
18
vendor/github.com/samedi/caldav-go/handlers/shared.go
generated
vendored
Normal file
18
vendor/github.com/samedi/caldav-go/handlers/shared.go
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// This function reads the request body and restore its content, so that
|
||||
// the request body can be read a second time.
|
||||
func readRequestBody(request *http.Request) string {
|
||||
// Read the content
|
||||
body, _ := ioutil.ReadAll(request.Body)
|
||||
// Restore the io.ReadCloser to its original state
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
// Use the content
|
||||
return string(body)
|
||||
}
|
94
vendor/github.com/samedi/caldav-go/ixml/ixml.go
generated
vendored
Normal file
94
vendor/github.com/samedi/caldav-go/ixml/ixml.go
generated
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
package ixml
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/samedi/caldav-go/lib"
|
||||
)
|
||||
|
||||
const (
|
||||
DAV_NS = "DAV:"
|
||||
CALDAV_NS = "urn:ietf:params:xml:ns:caldav"
|
||||
CALSERV_NS = "http://calendarserver.org/ns/"
|
||||
)
|
||||
|
||||
var NS_PREFIXES = map[string]string{
|
||||
DAV_NS: "D",
|
||||
CALDAV_NS: "C",
|
||||
CALSERV_NS: "CS",
|
||||
}
|
||||
|
||||
var (
|
||||
CALENDAR_TG = xml.Name{CALDAV_NS, "calendar"}
|
||||
CALENDAR_DATA_TG = xml.Name{CALDAV_NS, "calendar-data"}
|
||||
CALENDAR_HOME_SET_TG = xml.Name{CALDAV_NS, "calendar-home-set"}
|
||||
CALENDAR_QUERY_TG = xml.Name{CALDAV_NS, "calendar-query"}
|
||||
CALENDAR_MULTIGET_TG = xml.Name{CALDAV_NS, "calendar-multiget"}
|
||||
CALENDAR_USER_ADDRESS_SET_TG = xml.Name{CALDAV_NS, "calendar-user-address-set"}
|
||||
COLLECTION_TG = xml.Name{DAV_NS, "collection"}
|
||||
CURRENT_USER_PRINCIPAL_TG = xml.Name{DAV_NS, "current-user-principal"}
|
||||
DISPLAY_NAME_TG = xml.Name{DAV_NS, "displayname"}
|
||||
GET_CONTENT_LENGTH_TG = xml.Name{DAV_NS, "getcontentlength"}
|
||||
GET_CONTENT_TYPE_TG = xml.Name{DAV_NS, "getcontenttype"}
|
||||
GET_CTAG_TG = xml.Name{CALSERV_NS, "getctag"}
|
||||
GET_ETAG_TG = xml.Name{DAV_NS, "getetag"}
|
||||
GET_LAST_MODIFIED_TG = xml.Name{DAV_NS, "getlastmodified"}
|
||||
HREF_TG = xml.Name{DAV_NS, "href"}
|
||||
OWNER_TG = xml.Name{DAV_NS, "owner"}
|
||||
PRINCIPAL_TG = xml.Name{DAV_NS, "principal"}
|
||||
PRINCIPAL_COLLECTION_SET_TG = xml.Name{DAV_NS, "principal-collection-set"}
|
||||
PRINCIPAL_URL_TG = xml.Name{DAV_NS, "principal-URL"}
|
||||
RESOURCE_TYPE_TG = xml.Name{DAV_NS, "resourcetype"}
|
||||
STATUS_TG = xml.Name{DAV_NS, "status"}
|
||||
SUPPORTED_CALENDAR_COMPONENT_SET_TG = xml.Name{CALDAV_NS, "supported-calendar-component-set"}
|
||||
)
|
||||
|
||||
// Namespaces returns the default XML namespaces in for CalDAV contents.
|
||||
func Namespaces() string {
|
||||
bf := new(lib.StringBuffer)
|
||||
bf.Write(`xmlns:%s="%s" `, NS_PREFIXES[DAV_NS], DAV_NS)
|
||||
bf.Write(`xmlns:%s="%s" `, NS_PREFIXES[CALDAV_NS], CALDAV_NS)
|
||||
bf.Write(`xmlns:%s="%s"`, NS_PREFIXES[CALSERV_NS], CALSERV_NS)
|
||||
|
||||
return bf.String()
|
||||
}
|
||||
|
||||
// Tag returns a XML tag as string based on the given tag name and content. It
|
||||
// takes in consideration the namespace and also if it is an empty content or not.
|
||||
func Tag(xmlName xml.Name, content string) string {
|
||||
name := xmlName.Local
|
||||
ns := NS_PREFIXES[xmlName.Space]
|
||||
|
||||
if ns != "" {
|
||||
ns = ns + ":"
|
||||
}
|
||||
|
||||
if content != "" {
|
||||
return fmt.Sprintf("<%s%s>%s</%s%s>", ns, name, content, ns, name)
|
||||
} else {
|
||||
return fmt.Sprintf("<%s%s/>", ns, name)
|
||||
}
|
||||
}
|
||||
|
||||
// HrefTag returns a DAV <D:href> tag with the given href path.
|
||||
func HrefTag(href string) (tag string) {
|
||||
return Tag(HREF_TG, href)
|
||||
}
|
||||
|
||||
// StatusTag returns a DAV <D:status> tag with the given HTTP status. The
|
||||
// status is translated into a label, e.g.: HTTP/1.1 404 NotFound.
|
||||
func StatusTag(status int) string {
|
||||
statusText := fmt.Sprintf("HTTP/1.1 %d %s", status, http.StatusText(status))
|
||||
return Tag(STATUS_TG, statusText)
|
||||
}
|
||||
|
||||
// EscapeText escapes any special character in the given text and returns the result.
|
||||
func EscapeText(text string) string {
|
||||
buffer := bytes.NewBufferString("")
|
||||
xml.EscapeText(buffer, []byte(text))
|
||||
|
||||
return buffer.String()
|
||||
}
|
8
vendor/github.com/samedi/caldav-go/lib/components.go
generated
vendored
Normal file
8
vendor/github.com/samedi/caldav-go/lib/components.go
generated
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
package lib
|
||||
|
||||
const (
|
||||
VCALENDAR = "VCALENDAR"
|
||||
VEVENT = "VEVENT"
|
||||
VJOURNAL = "VJOURNAL"
|
||||
VTODO = "VTODO"
|
||||
)
|
10
vendor/github.com/samedi/caldav-go/lib/paths.go
generated
vendored
Normal file
10
vendor/github.com/samedi/caldav-go/lib/paths.go
generated
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func ToSlashPath(path string) string {
|
||||
cleanPath := filepath.Clean(path)
|
||||
return filepath.ToSlash(cleanPath)
|
||||
}
|
18
vendor/github.com/samedi/caldav-go/lib/strbuff.go
generated
vendored
Normal file
18
vendor/github.com/samedi/caldav-go/lib/strbuff.go
generated
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type StringBuffer struct {
|
||||
buffer bytes.Buffer
|
||||
}
|
||||
|
||||
func (b *StringBuffer) Write(format string, elem ...interface{}) {
|
||||
b.buffer.WriteString(fmt.Sprintf(format, elem...))
|
||||
}
|
||||
|
||||
func (b *StringBuffer) String() string {
|
||||
return b.buffer.String()
|
||||
}
|
4
vendor/github.com/samedi/caldav-go/test.sh
generated
vendored
Normal file
4
vendor/github.com/samedi/caldav-go/test.sh
generated
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
go test -race ./...
|
||||
rm -rf test-data
|
5
vendor/github.com/samedi/caldav-go/version.go
generated
vendored
Normal file
5
vendor/github.com/samedi/caldav-go/version.go
generated
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
package caldav
|
||||
|
||||
const (
|
||||
VERSION = "3.0.0"
|
||||
)
|
15
vendor/modules.txt
vendored
15
vendor/modules.txt
vendored
|
@ -12,6 +12,8 @@ github.com/alecthomas/template
|
|||
github.com/alecthomas/template/parse
|
||||
# github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||
github.com/asaskevich/govalidator
|
||||
# github.com/beevik/etree v1.1.0
|
||||
github.com/beevik/etree
|
||||
# github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973
|
||||
github.com/beorn7/perks/quantile
|
||||
# github.com/client9/misspell v0.3.4
|
||||
|
@ -78,7 +80,7 @@ github.com/inconshreveable/mousetrap
|
|||
# github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb
|
||||
github.com/jgautheron/goconst/cmd/goconst
|
||||
github.com/jgautheron/goconst
|
||||
# github.com/labstack/echo/v4 v4.1.5
|
||||
# github.com/labstack/echo/v4 v4.1.5 => ../../github.com/labstack/echo
|
||||
github.com/labstack/echo/v4
|
||||
github.com/labstack/echo/v4/middleware
|
||||
# github.com/labstack/gommon v0.2.8
|
||||
|
@ -86,6 +88,8 @@ github.com/labstack/gommon/log
|
|||
github.com/labstack/gommon/color
|
||||
github.com/labstack/gommon/bytes
|
||||
github.com/labstack/gommon/random
|
||||
# github.com/laurent22/ical-go v0.1.0
|
||||
github.com/laurent22/ical-go/ical
|
||||
# github.com/magiconair/properties v1.8.0
|
||||
github.com/magiconair/properties
|
||||
# github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983
|
||||
|
@ -130,6 +134,15 @@ github.com/prometheus/procfs
|
|||
github.com/prometheus/procfs/nfs
|
||||
github.com/prometheus/procfs/xfs
|
||||
github.com/prometheus/procfs/internal/util
|
||||
# github.com/samedi/caldav-go v3.0.0+incompatible => ../../github.com/samedi/caldav-go
|
||||
github.com/samedi/caldav-go
|
||||
github.com/samedi/caldav-go/data
|
||||
github.com/samedi/caldav-go/errs
|
||||
github.com/samedi/caldav-go/lib
|
||||
github.com/samedi/caldav-go/global
|
||||
github.com/samedi/caldav-go/handlers
|
||||
github.com/samedi/caldav-go/files
|
||||
github.com/samedi/caldav-go/ixml
|
||||
# github.com/spf13/afero v1.2.2
|
||||
github.com/spf13/afero
|
||||
github.com/spf13/afero/mem
|
||||
|
|
Loading…
Reference in a new issue