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
|
- name: build
|
||||||
image: vikunja/golang-build:latest
|
image: vikunja/golang-build:latest
|
||||||
pull: true
|
pull: true
|
||||||
|
environment:
|
||||||
|
GOFLAGS: '-mod=vendor'
|
||||||
commands:
|
commands:
|
||||||
- make lint
|
- make lint
|
||||||
- make fmt-check
|
- make fmt-check
|
||||||
|
|
|
@ -48,10 +48,10 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||||
* [x] Pagination
|
* [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.
|
* 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
|
* [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] Basics
|
||||||
* [x] Reminders
|
* [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)
|
* [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
|
* Dazu am Besten nen pseudonamespace anlegen (id -1 oder so), der hat das dann alles
|
||||||
* [x] Testing mit locust: https://locust.io/
|
* [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
|
* [ ] Some kind of milestones for tasks
|
||||||
* [ ] Create tasks from a text/markdown file (probably frontend only)
|
* [ ] Create tasks from a text/markdown file (probably frontend only)
|
||||||
* [ ] Label-view: Get a bunch of tasks by label
|
* [ ] Label-view: Get a bunch of tasks by label
|
||||||
* [ ] Better caldav support (VTODO)
|
|
||||||
* [ ] Debian package should have a service file
|
* [ ] Debian package should have a service file
|
||||||
* [ ] Downloads should be served via nginx (with theme?), minio should only be used for pushing artifacts.
|
* [ ] 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)
|
* [ ] 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.
|
-> 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
|
* [ ] 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
|
* [ ] 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
|
### Refactor
|
||||||
|
|
||||||
|
@ -217,6 +244,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
|
||||||
|
|
||||||
### Later
|
### Later
|
||||||
|
|
||||||
|
* [ ] Backgrounds for lists -> needs uploading and storing and so on
|
||||||
* [ ] Plugins
|
* [ ] Plugins
|
||||||
* [ ] Rename Namespaces?
|
* [ ] Rename Namespaces?
|
||||||
* [ ] Namespaces to collections and n-n (one list can be in multiple collections)?
|
* [ ] 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
|
* [ ] Nozbe
|
||||||
* [ ] Lanes
|
* [ ] Lanes
|
||||||
* [ ] Nirvana
|
* [ ] Nirvana
|
||||||
|
* [ ] Any.do
|
||||||
* [ ] Good ol' Caldav (Tasks)
|
* [ ] Good ol' Caldav (Tasks)
|
||||||
* [ ] More auth providers
|
* [ ] More auth providers
|
||||||
* [ ] LDAP/AD
|
* [ ] LDAP/AD
|
||||||
|
|
|
@ -16,6 +16,8 @@ service:
|
||||||
# If set to true, enables a /metrics endpoint for prometheus to collect metrics about the system
|
# 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
|
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||||
enablemetrics: false
|
enablemetrics: false
|
||||||
|
# Enable the caldav endpoint, see the docs for more details
|
||||||
|
enablecaldav: true
|
||||||
|
|
||||||
database:
|
database:
|
||||||
# Database type to use. Supported types are mysql and sqlite.
|
# 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
|
# 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
|
# You'll need to use redis for this in order to enable common metrics over multiple nodes
|
||||||
enablemetrics: false
|
enablemetrics: false
|
||||||
|
# Enable the caldav endpoint, see the docs for more details
|
||||||
|
enablecaldav: true
|
||||||
|
|
||||||
database:
|
database:
|
||||||
# Database type to use. Supported types are mysql and sqlite.
|
# 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
|
code.vikunja.io/web v0.0.0-20190507193736-edb39812af9c
|
||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
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/client9/misspell v0.3.4
|
||||||
github.com/d4l3k/messagediff v1.2.1 // indirect
|
github.com/d4l3k/messagediff v1.2.1 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
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/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb
|
||||||
github.com/labstack/echo/v4 v4.1.5
|
github.com/labstack/echo/v4 v4.1.5
|
||||||
github.com/labstack/gommon v0.2.8
|
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/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 // indirect
|
||||||
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 // indirect
|
github.com/mattn/go-oci8 v0.0.0-20181130072307-052f5d97b9b6 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.4 // 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/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pelletier/go-toml v1.4.0 // indirect
|
github.com/pelletier/go-toml v1.4.0 // indirect
|
||||||
github.com/prometheus/client_golang v0.9.2
|
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/afero v1.2.2 // indirect
|
||||||
github.com/spf13/cobra v0.0.3
|
github.com/spf13/cobra v0.0.3
|
||||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
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/xgo v0.0.0-20190507142556-a5b29ecb0ff4
|
||||||
src.techknowlogick.com/xormigrate v0.0.0-20190321151057-24497c23c09c
|
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/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 h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
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 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
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=
|
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/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 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
|
||||||
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
|
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 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
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/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 h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
|
||||||
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
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 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||||
|
|
|
@ -17,11 +17,16 @@
|
||||||
package caldav
|
package caldav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/models"
|
||||||
"code.vikunja.io/api/pkg/utils"
|
"code.vikunja.io/api/pkg/utils"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// DateFormat ist the caldav date format
|
||||||
|
const DateFormat = `20060102T150405`
|
||||||
|
|
||||||
// Event holds a single caldav event
|
// Event holds a single caldav event
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Summary string
|
Summary string
|
||||||
|
@ -34,6 +39,29 @@ type Event struct {
|
||||||
EndUnix int64
|
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
|
// Alarm holds infos about an alarm from a caldav event
|
||||||
type Alarm struct {
|
type Alarm struct {
|
||||||
TimeUnix int64
|
TimeUnix int64
|
||||||
|
@ -92,10 +120,89 @@ END:VCALENDAR` // Need a line break
|
||||||
return
|
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) {
|
func makeCalDavTimeFromUnixTime(unixtime int64) (caldavtime string) {
|
||||||
tz, _ := time.LoadLocation("UTC")
|
tz, _ := time.LoadLocation("UTC")
|
||||||
tm := time.Unix(unixtime, 0).In(tz)
|
tm := time.Unix(unixtime, 0).In(tz)
|
||||||
return tm.Format("20060102T150405")
|
return tm.Format(DateFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcAlarmDateFromReminder(eventStartUnix, reminderUnix int64) (alarmTime string) {
|
func calcAlarmDateFromReminder(eventStartUnix, reminderUnix int64) (alarmTime string) {
|
||||||
|
|
|
@ -40,6 +40,7 @@ func InitConfig() {
|
||||||
viper.SetDefault("service.JWTSecret", random)
|
viper.SetDefault("service.JWTSecret", random)
|
||||||
viper.SetDefault("service.interface", ":3456")
|
viper.SetDefault("service.interface", ":3456")
|
||||||
viper.SetDefault("service.frontendurl", "")
|
viper.SetDefault("service.frontendurl", "")
|
||||||
|
viper.SetDefault("service.enablecaldav", true)
|
||||||
|
|
||||||
ex, err := os.Executable()
|
ex, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -80,33 +80,33 @@ func TestListTask(t *testing.T) {
|
||||||
t.Run("by priority", func(t *testing.T) {
|
t.Run("by priority", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priority"}}, nil)
|
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priority"}}, nil)
|
||||||
assert.NoError(t, err)
|
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) {
|
t.Run("by priority desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"prioritydesc"}}, nil)
|
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"prioritydesc"}}, nil)
|
||||||
assert.NoError(t, err)
|
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) {
|
t.Run("by priority asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priorityasc"}}, nil)
|
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priorityasc"}}, nil)
|
||||||
assert.NoError(t, err)
|
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
|
// should equal duedate desc
|
||||||
t.Run("by duedate", func(t *testing.T) {
|
t.Run("by duedate", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadate"}}, nil)
|
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadate"}}, nil)
|
||||||
assert.NoError(t, err)
|
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) {
|
t.Run("by duedate desc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadatedesc"}}, nil)
|
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadatedesc"}}, nil)
|
||||||
assert.NoError(t, err)
|
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) {
|
t.Run("by duedate asc", func(t *testing.T) {
|
||||||
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedateasc"}}, nil)
|
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedateasc"}}, nil)
|
||||||
assert.NoError(t, err)
|
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) {
|
t.Run("invalid parameter", func(t *testing.T) {
|
||||||
// Invalid parameter should not sort at all
|
// 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
|
// Insert it
|
||||||
_, err = x.Insert(lt)
|
_, err = x.Insert(lt)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListByTaskID(lt.TaskID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,6 +277,8 @@ func (t *ListTask) updateTaskLabels(creator web.Auth, labels []*Label) (err erro
|
||||||
}
|
}
|
||||||
t.Labels = append(t.Labels, label)
|
t.Labels = append(t.Labels, label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,7 +306,7 @@ type LabelTaskBulk struct {
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /tasks/{taskID}/labels/bulk [post]
|
// @Router /tasks/{taskID}/labels/bulk [post]
|
||||||
func (ltb *LabelTaskBulk) Create(a web.Auth) (err error) {
|
func (ltb *LabelTaskBulk) Create(a web.Auth) (err error) {
|
||||||
task, err := GetListTaskByID(ltb.TaskID)
|
task, err := GetTaskByID(ltb.TaskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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
|
// always the same check for either deleting or adding a label to a task
|
||||||
func canDoLabelTask(taskID int64, a web.Auth) (bool, error) {
|
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
|
// 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 {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,6 +166,8 @@ func TestLabelTask_Create(t *testing.T) {
|
||||||
a: &User{ID: 1},
|
a: &User{ID: 1},
|
||||||
},
|
},
|
||||||
wantForbidden: true,
|
wantForbidden: true,
|
||||||
|
wantErr: true,
|
||||||
|
errType: IsErrListTaskDoesNotExist,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|
|
@ -71,6 +71,21 @@ func (l *List) Update() (err error) {
|
||||||
return CreateOrUpdateList(l)
|
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
|
// Create implements the create method of CRUDable
|
||||||
// @Summary Creates a new list
|
// @Summary Creates a new list
|
||||||
// @Description Creates a new list in a given namespace. The user needs write-access to the namespace.
|
// @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)
|
t.setTaskAssignees(assignees)
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +154,11 @@ func (t *ListTask) setTaskAssignees(assignees []*User) {
|
||||||
// @Router /tasks/{taskID}/assignees/{userID} [delete]
|
// @Router /tasks/{taskID}/assignees/{userID} [delete]
|
||||||
func (la *ListTaskAssginee) Delete() (err error) {
|
func (la *ListTaskAssginee) Delete() (err error) {
|
||||||
_, err = x.Delete(&ListTaskAssginee{TaskID: la.TaskID, UserID: la.UserID})
|
_, err = x.Delete(&ListTaskAssginee{TaskID: la.TaskID, UserID: la.UserID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListByTaskID(la.TaskID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +205,11 @@ func (t *ListTask) addNewAssigneeByID(newAssigneeID int64, list *List) (err erro
|
||||||
TaskID: t.ID,
|
TaskID: t.ID,
|
||||||
UserID: newAssigneeID,
|
UserID: newAssigneeID,
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +260,7 @@ type BulkAssignees struct {
|
||||||
// @Failure 500 {object} models.Message "Internal error"
|
// @Failure 500 {object} models.Message "Internal error"
|
||||||
// @Router /tasks/{taskID}/assignees/bulk [post]
|
// @Router /tasks/{taskID}/assignees/bulk [post]
|
||||||
func (ba *BulkAssignees) Create(a web.Auth) (err error) {
|
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 {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,8 @@ type ListTask struct {
|
||||||
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
|
Description string `xorm:"varchar(250)" json:"description" valid:"runelength(0|250)" maxLength:"250"`
|
||||||
// Whether a task is done or not.
|
// Whether a task is done or not.
|
||||||
Done bool `xorm:"INDEX null" json:"done"`
|
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.
|
// A unix timestamp when the task is due.
|
||||||
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"`
|
DueDateUnix int64 `xorm:"int(11) INDEX null" json:"dueDate"`
|
||||||
// An array of unix timestamps when the user wants to be reminded of the task.
|
// 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
|
// The task color in hex
|
||||||
HexColor string `xorm:"varchar(6) null" json:"hexColor" valid:"runelength(0|6)" maxLength:"6"`
|
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
|
Sorting string `xorm:"-" json:"-" query:"sort"` // Parameter to sort by
|
||||||
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
|
StartDateSortUnix int64 `xorm:"-" json:"-" query:"startdate"`
|
||||||
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
|
EndDateSortUnix int64 `xorm:"-" json:"-" query:"enddate"`
|
||||||
|
@ -88,96 +93,36 @@ func GetTasksByListID(listID int64) (tasks []*ListTask, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to iterate over users and stuff if the list doesn't has tasks
|
tasks, err = addMoreInfoToTasks(taskMap)
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
return
|
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 {
|
if taskID < 1 {
|
||||||
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
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 {
|
if err != nil {
|
||||||
return ListTask{}, err
|
return ListTask{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
if !exists {
|
||||||
return ListTask{}, ErrListTaskDoesNotExist{taskID}
|
return ListTask{}, ErrListTaskDoesNotExist{t.ID}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetListTaskByID returns all tasks a list has
|
// GetTaskByID returns all tasks a list has
|
||||||
func GetListTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
func GetTaskByID(listTaskID int64) (listTask ListTask, err error) {
|
||||||
listTask, err = getTaskByIDSimple(listTaskID)
|
listTask, err = GetTaskByIDSimple(listTaskID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We use a map, to avoid looping over two slices at once
|
bt.Tasks, err = addMoreInfoToTasks(taskMap)
|
||||||
var usermapids = make(map[int64]bool) // Bool ist just something, doesn't acutually matter
|
return
|
||||||
for _, list := range bt.Tasks {
|
}
|
||||||
usermapids[list.CreatedByID] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a slice from the map
|
// GetTasksByUIDs gets all tasks from a bunch of uids
|
||||||
var userids []int64
|
func GetTasksByUIDs(uids []string) (tasks []*ListTask, err error) {
|
||||||
for uid := range usermapids {
|
taskMap := make(map[int64]*ListTask)
|
||||||
userids = append(userids, uid)
|
err = x.In("uid", uids).Find(&taskMap)
|
||||||
}
|
|
||||||
|
|
||||||
// Get all users for the tasks
|
|
||||||
var users []*User
|
|
||||||
err = x.In("id", userids).Find(&users)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for in, task := range bt.Tasks {
|
tasks, err = addMoreInfoToTasks(taskMap)
|
||||||
for _, u := range users {
|
return
|
||||||
if task.CreatedByID == u.ID {
|
}
|
||||||
bt.Tasks[in].CreatedBy = *u
|
|
||||||
}
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,10 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.vikunja.io/api/pkg/metrics"
|
"code.vikunja.io/api/pkg/metrics"
|
||||||
|
"code.vikunja.io/api/pkg/utils"
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
"github.com/imdario/mergo"
|
"github.com/imdario/mergo"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create is the implementation to create a list task
|
// Create is the implementation to create a list task
|
||||||
|
@ -60,6 +62,11 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
||||||
return err
|
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.CreatedByID = u.ID
|
||||||
t.CreatedBy = u
|
t.CreatedBy = u
|
||||||
if _, err = x.Insert(t); err != nil {
|
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)
|
metrics.UpdateCount(1, metrics.TaskCountKey)
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +100,7 @@ func (t *ListTask) Create(a web.Auth) (err error) {
|
||||||
// @Router /tasks/{id} [post]
|
// @Router /tasks/{id} [post]
|
||||||
func (t *ListTask) Update() (err error) {
|
func (t *ListTask) Update() (err error) {
|
||||||
// Check if the task exists
|
// Check if the task exists
|
||||||
ot, err := GetListTaskByID(t.ID)
|
ot, err := GetTaskByID(t.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -189,12 +198,20 @@ func (t *ListTask) Update() (err error) {
|
||||||
"priority",
|
"priority",
|
||||||
"start_date_unix",
|
"start_date_unix",
|
||||||
"end_date_unix",
|
"end_date_unix",
|
||||||
"hex_color").
|
"hex_color",
|
||||||
|
"done_at_unix").
|
||||||
Update(ot)
|
Update(ot)
|
||||||
*t = ot
|
*t = ot
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||||
return
|
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) {
|
func updateDone(oldTask *ListTask, newTask *ListTask) {
|
||||||
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
|
if !oldTask.Done && newTask.Done && oldTask.RepeatAfter > 0 {
|
||||||
oldTask.DueDateUnix = oldTask.DueDateUnix + oldTask.RepeatAfter // assuming we'll save the old task (merged)
|
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
|
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) {
|
func (t *ListTask) Delete() (err error) {
|
||||||
|
|
||||||
// Check if it exists
|
// Check if it exists
|
||||||
_, err = GetListTaskByID(t.ID)
|
_, err = GetTaskByID(t.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -51,5 +51,7 @@ func (t *ListTask) Delete() (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.UpdateCount(-1, metrics.TaskCountKey)
|
metrics.UpdateCount(-1, metrics.TaskCountKey)
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: t.ListID})
|
||||||
return
|
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) {
|
func (t *ListTask) CanRead(a web.Auth) (canRead bool, err error) {
|
||||||
//return t.canDoListTask(a)
|
//return t.canDoListTask(a)
|
||||||
// Get the task, error out if it doesn't exist
|
// Get the task, error out if it doesn't exist
|
||||||
*t, err = getTaskByIDSimple(t.ID)
|
*t, err = GetTaskByIDSimple(t.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ func (t *ListTask) canDoListTask(a web.Auth) (bool, error) {
|
||||||
doer := getUserForRights(a)
|
doer := getUserForRights(a)
|
||||||
|
|
||||||
// Get the task
|
// Get the task
|
||||||
lI, err := getTaskByIDSimple(t.ID)
|
lI, err := GetTaskByIDSimple(t.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ func TestListTask_Create(t *testing.T) {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
// Check if it was updated
|
// Check if it was updated
|
||||||
li, err := GetListTaskByID(listtask.ID)
|
li, err := GetTaskByID(listtask.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, li.Text, "Test34")
|
assert.Equal(t, li.Text, "Test34")
|
||||||
|
|
||||||
|
@ -91,3 +91,18 @@ func TestListTask_Create(t *testing.T) {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.True(t, IsErrUserDoesNotExist(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
|
// Insert user <-> list relation
|
||||||
_, err = x.Insert(lu)
|
_, err = x.Insert(lu)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(l)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,5 +51,10 @@ func (lu *ListUser) Delete() (err error) {
|
||||||
|
|
||||||
_, err = x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
_, err = x.Where("user_id = ? AND list_id = ?", lu.UserID, lu.ListID).
|
||||||
Delete(&ListUser{})
|
Delete(&ListUser{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: lu.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,5 +44,10 @@ func (lu *ListUser) Update() (err error) {
|
||||||
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
Where("list_id = ? AND user_id = ?", lu.ListID, lu.UserID).
|
||||||
Cols("right").
|
Cols("right").
|
||||||
Update(lu)
|
Update(lu)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: lu.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,5 +65,10 @@ func (tl *TeamList) Create(a web.Auth) (err error) {
|
||||||
|
|
||||||
// Insert the new team
|
// Insert the new team
|
||||||
_, err = x.Insert(tl)
|
_, err = x.Insert(tl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(l)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,6 +53,10 @@ func (tl *TeamList) Delete() (err error) {
|
||||||
_, err = x.Where("team_id = ?", tl.TeamID).
|
_, err = x.Where("team_id = ?", tl.TeamID).
|
||||||
And("list_id = ?", tl.ListID).
|
And("list_id = ?", tl.ListID).
|
||||||
Delete(TeamList{})
|
Delete(TeamList{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: tl.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,5 +44,10 @@ func (tl *TeamList) Update() (err error) {
|
||||||
Where("list_id = ? AND team_id = ?", tl.ListID, tl.TeamID).
|
Where("list_id = ? AND team_id = ?", tl.ListID, tl.TeamID).
|
||||||
Cols("right").
|
Cols("right").
|
||||||
Update(tl)
|
Update(tl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = updateListLastUpdated(&List{ID: tl.ListID})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,7 +115,7 @@ func CreateUser(user User) (newUser User, err error) {
|
||||||
|
|
||||||
// HashPassword hashes a password
|
// HashPassword hashes a password
|
||||||
func hashPassword(password string) (string, error) {
|
func hashPassword(password string) (string, error) {
|
||||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), 11)
|
||||||
return string(bytes), err
|
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/metrics"
|
||||||
"code.vikunja.io/api/pkg/models"
|
"code.vikunja.io/api/pkg/models"
|
||||||
apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
|
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/api/pkg/swagger" // To generate swagger docs
|
||||||
"code.vikunja.io/web"
|
"code.vikunja.io/web"
|
||||||
"code.vikunja.io/web/handler"
|
"code.vikunja.io/web/handler"
|
||||||
|
@ -52,6 +53,7 @@ import (
|
||||||
elog "github.com/labstack/gommon/log"
|
elog "github.com/labstack/gommon/log"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CustomValidator is a dummy struct to use govalidator with echo
|
// 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
|
// RegisterRoutes registers all routes for the application
|
||||||
func RegisterRoutes(e *echo.Echo) {
|
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
|
// CORS_SHIT
|
||||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||||
AllowOrigins: []string{"*"},
|
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
|
// API Routes
|
||||||
a := e.Group("/api/v1")
|
a := e.Group("/api/v1")
|
||||||
|
registerAPIRoutes(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerAPIRoutes(a *echo.Group) {
|
||||||
|
|
||||||
// Docs
|
// Docs
|
||||||
a.GET("/docs.json", apiv1.DocsJSON)
|
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/password/reset", apiv1.UserResetPassword)
|
||||||
a.POST("/user/confirm", apiv1.UserConfirmEmail)
|
a.POST("/user/confirm", apiv1.UserConfirmEmail)
|
||||||
|
|
||||||
// Caldav, with auth
|
|
||||||
a.GET("/tasks/caldav", apiv1.Caldav)
|
|
||||||
|
|
||||||
// ===== Routes with Authetification =====
|
// ===== Routes with Authetification =====
|
||||||
// Authetification
|
// Authetification
|
||||||
a.Use(middleware.JWT([]byte(viper.GetString("service.JWTSecret"))))
|
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.PUT("/teams/:team/members", teamMemberHandler.CreateWeb)
|
||||||
a.DELETE("/teams/:team/members/:user", teamMemberHandler.DeleteWeb)
|
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
|
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
|
||||||
// This file was generated by swaggo/swag at
|
// 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
|
package swagger
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ import (
|
||||||
var doc = `{
|
var doc = `{
|
||||||
"swagger": "2.0",
|
"swagger": "2.0",
|
||||||
"info": {
|
"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",
|
"title": "Vikunja API",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "General Vikunja contact",
|
"name": "General Vikunja contact",
|
||||||
|
@ -391,7 +391,7 @@ var doc = `{
|
||||||
"JWTKeyAuth": []
|
"JWTKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Returns a team by its ID.",
|
"description": "Returns a list by its ID.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
@ -399,13 +399,13 @@ var doc = `{
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"team"
|
"list"
|
||||||
],
|
],
|
||||||
"summary": "Gets one team",
|
"summary": "Gets one list",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Team ID",
|
"description": "List ID",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
@ -413,14 +413,14 @@ var doc = `{
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "The team",
|
"description": "The list",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"$ref": "#/definitions/models.Team"
|
"$ref": "#/definitions/models.List"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "The user does not have access to the team",
|
"description": "The user does not have access to the list",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
|
"$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}": {
|
"/tasks/{id}": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -3688,6 +3657,10 @@ var doc = `{
|
||||||
"description": "Whether a task is done or not.",
|
"description": "Whether a task is done or not.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"doneAt": {
|
||||||
|
"description": "The unix timestamp when a task was marked as done.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"dueDate": {
|
"dueDate": {
|
||||||
"description": "A unix timestamp when the task is due.",
|
"description": "A unix timestamp when the task is due.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
@ -3905,6 +3878,10 @@ var doc = `{
|
||||||
"description": "Whether a task is done or not.",
|
"description": "Whether a task is done or not.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"doneAt": {
|
||||||
|
"description": "The unix timestamp when a task was marked as done.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"dueDate": {
|
"dueDate": {
|
||||||
"description": "A unix timestamp when the task is due.",
|
"description": "A unix timestamp when the task is due.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"swagger": "2.0",
|
"swagger": "2.0",
|
||||||
"info": {
|
"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",
|
"title": "Vikunja API",
|
||||||
"contact": {
|
"contact": {
|
||||||
"name": "General Vikunja contact",
|
"name": "General Vikunja contact",
|
||||||
|
@ -378,7 +378,7 @@
|
||||||
"JWTKeyAuth": []
|
"JWTKeyAuth": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Returns a team by its ID.",
|
"description": "Returns a list by its ID.",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
@ -386,13 +386,13 @@
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": [
|
||||||
"team"
|
"list"
|
||||||
],
|
],
|
||||||
"summary": "Gets one team",
|
"summary": "Gets one list",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "Team ID",
|
"description": "List ID",
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true
|
"required": true
|
||||||
|
@ -400,14 +400,14 @@
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "The team",
|
"description": "The list",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"$ref": "#/definitions/models.Team"
|
"$ref": "#/definitions/models.List"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
"403": {
|
||||||
"description": "The user does not have access to the team",
|
"description": "The user does not have access to the list",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
|
"$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}": {
|
"/tasks/{id}": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -3674,6 +3643,10 @@
|
||||||
"description": "Whether a task is done or not.",
|
"description": "Whether a task is done or not.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"doneAt": {
|
||||||
|
"description": "The unix timestamp when a task was marked as done.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"dueDate": {
|
"dueDate": {
|
||||||
"description": "A unix timestamp when the task is due.",
|
"description": "A unix timestamp when the task is due.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
@ -3891,6 +3864,10 @@
|
||||||
"description": "Whether a task is done or not.",
|
"description": "Whether a task is done or not.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"doneAt": {
|
||||||
|
"description": "The unix timestamp when a task was marked as done.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"dueDate": {
|
"dueDate": {
|
||||||
"description": "A unix timestamp when the task is due.",
|
"description": "A unix timestamp when the task is due.",
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
|
|
@ -51,6 +51,9 @@ definitions:
|
||||||
done:
|
done:
|
||||||
description: Whether a task is done or not.
|
description: Whether a task is done or not.
|
||||||
type: boolean
|
type: boolean
|
||||||
|
doneAt:
|
||||||
|
description: The unix timestamp when a task was marked as done.
|
||||||
|
type: integer
|
||||||
dueDate:
|
dueDate:
|
||||||
description: A unix timestamp when the task is due.
|
description: A unix timestamp when the task is due.
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -223,6 +226,9 @@ definitions:
|
||||||
done:
|
done:
|
||||||
description: Whether a task is done or not.
|
description: Whether a task is done or not.
|
||||||
type: boolean
|
type: boolean
|
||||||
|
doneAt:
|
||||||
|
description: The unix timestamp when a task was marked as done.
|
||||||
|
type: integer
|
||||||
dueDate:
|
dueDate:
|
||||||
description: A unix timestamp when the task is due.
|
description: A unix timestamp when the task is due.
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -645,13 +651,7 @@ info:
|
||||||
email: hello@vikunja.io
|
email: hello@vikunja.io
|
||||||
name: General Vikunja contact
|
name: General Vikunja contact
|
||||||
url: http://vikunja.io/en/contact/
|
url: http://vikunja.io/en/contact/
|
||||||
description: |-
|
description: '<!-- ReDoc-Inject: <security-definitions> -->'
|
||||||
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> -->
|
|
||||||
license:
|
license:
|
||||||
name: GPLv3
|
name: GPLv3
|
||||||
url: http://code.vikunja.io/api/src/branch/master/LICENSE
|
url: http://code.vikunja.io/api/src/branch/master/LICENSE
|
||||||
|
@ -935,9 +935,9 @@ paths:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: Returns a team by its ID.
|
description: Returns a list by its ID.
|
||||||
parameters:
|
parameters:
|
||||||
- description: Team ID
|
- description: List ID
|
||||||
in: path
|
in: path
|
||||||
name: id
|
name: id
|
||||||
required: true
|
required: true
|
||||||
|
@ -946,12 +946,12 @@ paths:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: The team
|
description: The list
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/models.Team'
|
$ref: '#/definitions/models.List'
|
||||||
type: object
|
type: object
|
||||||
"403":
|
"403":
|
||||||
description: The user does not have access to the team
|
description: The user does not have access to the list
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
|
||||||
type: object
|
type: object
|
||||||
|
@ -962,9 +962,9 @@ paths:
|
||||||
type: object
|
type: object
|
||||||
security:
|
security:
|
||||||
- JWTKeyAuth: []
|
- JWTKeyAuth: []
|
||||||
summary: Gets one team
|
summary: Gets one list
|
||||||
tags:
|
tags:
|
||||||
- team
|
- list
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
|
@ -2638,27 +2638,6 @@ paths:
|
||||||
summary: Update a bunch of tasks at once
|
summary: Update a bunch of tasks at once
|
||||||
tags:
|
tags:
|
||||||
- task
|
- 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:
|
/teams:
|
||||||
get:
|
get:
|
||||||
consumes:
|
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"
|
charsetUTF8 = "charset=UTF-8"
|
||||||
// PROPFIND Method can be used on collection and property resources.
|
// PROPFIND Method can be used on collection and property resources.
|
||||||
PROPFIND = "PROPFIND"
|
PROPFIND = "PROPFIND"
|
||||||
|
// REPORT Method can be used to get information about a resource, see rfc 3253
|
||||||
|
REPORT = "REPORT"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
|
@ -251,6 +253,7 @@ var (
|
||||||
PROPFIND,
|
PROPFIND,
|
||||||
http.MethodPut,
|
http.MethodPut,
|
||||||
http.MethodTrace,
|
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
|
propfind HandlerFunc
|
||||||
put HandlerFunc
|
put HandlerFunc
|
||||||
trace HandlerFunc
|
trace HandlerFunc
|
||||||
|
report HandlerFunc
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -247,6 +248,8 @@ func (n *node) addHandler(method string, h HandlerFunc) {
|
||||||
n.methodHandler.put = h
|
n.methodHandler.put = h
|
||||||
case http.MethodTrace:
|
case http.MethodTrace:
|
||||||
n.methodHandler.trace = h
|
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
|
return n.methodHandler.put
|
||||||
case http.MethodTrace:
|
case http.MethodTrace:
|
||||||
return n.methodHandler.trace
|
return n.methodHandler.trace
|
||||||
|
case REPORT:
|
||||||
|
return n.methodHandler.report
|
||||||
default:
|
default:
|
||||||
return nil
|
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/alecthomas/template/parse
|
||||||
# github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
# github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||||
github.com/asaskevich/govalidator
|
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 v0.0.0-20180321164747-3a771d992973
|
||||||
github.com/beorn7/perks/quantile
|
github.com/beorn7/perks/quantile
|
||||||
# github.com/client9/misspell v0.3.4
|
# 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 v0.0.0-20170703170152-9740945f5dcb
|
||||||
github.com/jgautheron/goconst/cmd/goconst
|
github.com/jgautheron/goconst/cmd/goconst
|
||||||
github.com/jgautheron/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
|
||||||
github.com/labstack/echo/v4/middleware
|
github.com/labstack/echo/v4/middleware
|
||||||
# github.com/labstack/gommon v0.2.8
|
# 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/color
|
||||||
github.com/labstack/gommon/bytes
|
github.com/labstack/gommon/bytes
|
||||||
github.com/labstack/gommon/random
|
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 v1.8.0
|
||||||
github.com/magiconair/properties
|
github.com/magiconair/properties
|
||||||
# github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983
|
# 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/nfs
|
||||||
github.com/prometheus/procfs/xfs
|
github.com/prometheus/procfs/xfs
|
||||||
github.com/prometheus/procfs/internal/util
|
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 v1.2.2
|
||||||
github.com/spf13/afero
|
github.com/spf13/afero
|
||||||
github.com/spf13/afero/mem
|
github.com/spf13/afero/mem
|
||||||
|
|
Loading…
Reference in a new issue