async-caldav 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/async/caldav/client/addressbook.rb +17 -25
- data/lib/async/caldav/client/calendar.rb +31 -36
- data/lib/async/caldav/client.rb +24 -23
- data/lib/async/caldav/handlers/propfind.rb +71 -81
- data/lib/async/caldav/handlers/proppatch.rb +8 -11
- data/lib/async/caldav/handlers/report.rb +64 -82
- data/lib/async/caldav/storage/filesystem.rb +66 -23
- data/lib/async/caldav/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a322d3a753a3dbee66ebd1afa1e1d8d7c195b96a243c45e0593cbf4a1c64d93c
|
|
4
|
+
data.tar.gz: 377c6989fdc81a44d0e6999dbb012b633f1899e08b7cd33330d318f58c5e9aa0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da4e219d79c691636fa2c6f13ca1f9f7a77bdee9e68005671494288c523115c14db068d585071bac19cd1f6c79b6723e186cbf81a185718225f4447c8b41a67d
|
|
7
|
+
data.tar.gz: b189f9332228946eb7da0ca7a5d23f28bbea5634ca1463661683c4b16328264ca15489a7f43f0c4bcd37737f6192c6c8dff9129d100dcec2c1a69ac99302fece
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
require "scampi"
|
|
5
|
+
require "builder"
|
|
5
6
|
|
|
6
7
|
module Async
|
|
7
8
|
module Caldav
|
|
@@ -16,22 +17,13 @@ module Async
|
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def contacts(filter: nil)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#{filter}
|
|
25
|
-
</cr:addressbook-query>
|
|
26
|
-
XML
|
|
27
|
-
else
|
|
28
|
-
<<~XML
|
|
29
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
30
|
-
<cr:addressbook-query xmlns:d="DAV:" xmlns:cr="urn:ietf:params:xml:ns:carddav">
|
|
31
|
-
<d:prop><d:getetag/><cr:address-data/></d:prop>
|
|
32
|
-
</cr:addressbook-query>
|
|
33
|
-
XML
|
|
20
|
+
x = Builder::XmlMarkup.new
|
|
21
|
+
x.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
22
|
+
x.tag!("cr:addressbook-query", "xmlns:d" => "DAV:", "xmlns:cr" => "urn:ietf:params:xml:ns:carddav") do
|
|
23
|
+
x.tag!("d:prop") { x.tag!("d:getetag"); x.tag!("cr:address-data") }
|
|
24
|
+
x << filter if filter
|
|
34
25
|
end
|
|
26
|
+
body = x.target!
|
|
35
27
|
|
|
36
28
|
status, _, resp_body = @client.request('REPORT', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
|
|
37
29
|
raise Error, "REPORT failed: #{status}" unless status == 207
|
|
@@ -95,17 +87,17 @@ module Async
|
|
|
95
87
|
end
|
|
96
88
|
|
|
97
89
|
def proppatch(displayname: nil)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
90
|
+
x = Builder::XmlMarkup.new
|
|
91
|
+
x.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
92
|
+
x.tag!("d:propertyupdate", "xmlns:d" => "DAV:") do
|
|
93
|
+
x.tag!("d:set") do
|
|
94
|
+
x.tag!("d:prop") do
|
|
95
|
+
x.tag!("d:displayname", displayname) if displayname
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
107
99
|
|
|
108
|
-
status, = @client.request('PROPPATCH', @path, body:
|
|
100
|
+
status, = @client.request('PROPPATCH', @path, body: x.target!, headers: { 'Content-Type' => 'text/xml' })
|
|
109
101
|
raise Error, "PROPPATCH failed: #{status}" unless status == 207
|
|
110
102
|
|
|
111
103
|
@displayname = displayname if displayname
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
require "scampi"
|
|
5
|
+
require "builder"
|
|
5
6
|
|
|
6
7
|
module Async
|
|
7
8
|
module Caldav
|
|
@@ -19,22 +20,13 @@ module Async
|
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def events(filter: nil)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
#{filter}
|
|
28
|
-
</c:calendar-query>
|
|
29
|
-
XML
|
|
30
|
-
else
|
|
31
|
-
<<~XML
|
|
32
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
-
<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
|
34
|
-
<d:prop><d:getetag/><c:calendar-data/></d:prop>
|
|
35
|
-
</c:calendar-query>
|
|
36
|
-
XML
|
|
23
|
+
x = Builder::XmlMarkup.new
|
|
24
|
+
x.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
25
|
+
x.tag!("c:calendar-query", "xmlns:d" => "DAV:", "xmlns:c" => "urn:ietf:params:xml:ns:caldav") do
|
|
26
|
+
x.tag!("d:prop") { x.tag!("d:getetag"); x.tag!("c:calendar-data") }
|
|
27
|
+
x << filter if filter
|
|
37
28
|
end
|
|
29
|
+
body = x.target!
|
|
38
30
|
|
|
39
31
|
status, _, resp_body = @client.request('REPORT', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
|
|
40
32
|
raise Error, "REPORT failed: #{status}" unless status == 207
|
|
@@ -104,19 +96,19 @@ module Async
|
|
|
104
96
|
end
|
|
105
97
|
|
|
106
98
|
def proppatch(displayname: nil, description: nil, color: nil)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
status, = @client.request('PROPPATCH', @path, body:
|
|
99
|
+
x = Builder::XmlMarkup.new
|
|
100
|
+
x.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
101
|
+
x.tag!("d:propertyupdate", "xmlns:d" => "DAV:", "xmlns:c" => "urn:ietf:params:xml:ns:caldav", "xmlns:x" => "http://apple.com/ns/ical/") do
|
|
102
|
+
x.tag!("d:set") do
|
|
103
|
+
x.tag!("d:prop") do
|
|
104
|
+
x.tag!("d:displayname", displayname) if displayname
|
|
105
|
+
x.tag!("c:calendar-description", description) if description
|
|
106
|
+
x.tag!("x:calendar-color", color) if color
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
status, = @client.request('PROPPATCH', @path, body: x.target!, headers: { 'Content-Type' => 'text/xml' })
|
|
120
112
|
raise Error, "PROPPATCH failed: #{status}" unless status == 207
|
|
121
113
|
|
|
122
114
|
@displayname = displayname if displayname
|
|
@@ -126,14 +118,17 @@ module Async
|
|
|
126
118
|
end
|
|
127
119
|
|
|
128
120
|
def sync(token: nil)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
x = Builder::XmlMarkup.new
|
|
122
|
+
x.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
123
|
+
x.tag!("d:sync-collection", "xmlns:d" => "DAV:") do
|
|
124
|
+
x.tag!("d:prop") { x.tag!("d:getetag") }
|
|
125
|
+
if token
|
|
126
|
+
x.tag!("d:sync-token", token)
|
|
127
|
+
else
|
|
128
|
+
x.tag!("d:sync-token")
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
body = x.target!
|
|
137
132
|
|
|
138
133
|
status, _, resp_body = @client.request('REPORT', @path, body: body, headers: { 'Content-Type' => 'text/xml' })
|
|
139
134
|
|
data/lib/async/caldav/client.rb
CHANGED
|
@@ -82,18 +82,18 @@ module Async
|
|
|
82
82
|
|
|
83
83
|
def create_calendar(name, displayname: nil, description: nil, color: nil)
|
|
84
84
|
path = "/calendars/#{@user}/#{name}/"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
status, = request('MKCALENDAR', path, body:
|
|
85
|
+
x = Builder::XmlMarkup.new
|
|
86
|
+
x.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
87
|
+
x.tag!("c:mkcalendar", "xmlns:d" => "DAV:", "xmlns:c" => "urn:ietf:params:xml:ns:caldav") do
|
|
88
|
+
x.tag!("d:set") do
|
|
89
|
+
x.tag!("d:prop") do
|
|
90
|
+
x.tag!("d:displayname", displayname || name) if displayname || name
|
|
91
|
+
x.tag!("c:calendar-description", description) if description
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
status, = request('MKCALENDAR', path, body: x.target!, headers: { 'Content-Type' => 'text/xml' })
|
|
97
97
|
raise Error, "MKCALENDAR failed: #{status}" unless status == 201
|
|
98
98
|
|
|
99
99
|
Calendar.new(self, path, displayname: displayname || name, description: description, color: color)
|
|
@@ -101,17 +101,18 @@ module Async
|
|
|
101
101
|
|
|
102
102
|
def create_addressbook(name, displayname: nil)
|
|
103
103
|
path = "/addressbooks/#{@user}/#{name}/"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
104
|
+
x = Builder::XmlMarkup.new
|
|
105
|
+
x.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
106
|
+
x.tag!("d:mkcol", "xmlns:d" => "DAV:", "xmlns:cr" => "urn:ietf:params:xml:ns:carddav") do
|
|
107
|
+
x.tag!("d:set") do
|
|
108
|
+
x.tag!("d:prop") do
|
|
109
|
+
x.tag!("d:resourcetype") { x.tag!("d:collection"); x.tag!("cr:addressbook") }
|
|
110
|
+
x.tag!("d:displayname", displayname || name)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
status, = request('MKCOL', path, body: x.target!, headers: { 'Content-Type' => 'text/xml' })
|
|
115
116
|
raise Error, "MKCOL failed: #{status}" unless status == 201
|
|
116
117
|
|
|
117
118
|
Addressbook.new(self, path, displayname: displayname || name)
|
|
@@ -13,6 +13,7 @@ module Async
|
|
|
13
13
|
def call(path:, storage:, user:, headers: {}, body: nil, **)
|
|
14
14
|
depth = headers['depth'] || '1'
|
|
15
15
|
propname = body&.include?('propname')
|
|
16
|
+
build = propname ? :build_propname : :build_propfind
|
|
16
17
|
|
|
17
18
|
col_path = path.ensure_trailing_slash
|
|
18
19
|
collection = storage.get_collection(col_path.to_s)
|
|
@@ -23,97 +24,86 @@ module Async
|
|
|
23
24
|
return [404, { 'content-type' => 'text/plain' }, ['Not Found']]
|
|
24
25
|
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
27
|
+
xml = Protocol::Caldav::Multistatus.new.to_xml do |x|
|
|
28
|
+
if collection
|
|
29
|
+
col = Protocol::Caldav::Collection.new(
|
|
30
|
+
path: col_path,
|
|
31
|
+
type: collection[:type],
|
|
32
|
+
displayname: collection[:displayname],
|
|
33
|
+
description: collection[:description],
|
|
34
|
+
color: collection[:color],
|
|
35
|
+
props: collection[:props]
|
|
36
|
+
)
|
|
37
|
+
col.send(build, x)
|
|
38
|
+
|
|
39
|
+
if depth != '0'
|
|
40
|
+
storage.list_collections(col_path.to_s).each do |child_path, child_data|
|
|
41
|
+
child_p = Protocol::Caldav::Path.new(child_path, storage_class: storage)
|
|
42
|
+
child_col = Protocol::Caldav::Collection.new(
|
|
43
|
+
path: child_p,
|
|
44
|
+
type: child_data[:type],
|
|
45
|
+
displayname: child_data[:displayname],
|
|
46
|
+
description: child_data[:description],
|
|
47
|
+
color: child_data[:color],
|
|
48
|
+
props: child_data[:props]
|
|
49
|
+
)
|
|
50
|
+
child_col.send(build, x)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
storage.list_items(col_path.to_s).each do |item_path, data|
|
|
54
|
+
item_p = Protocol::Caldav::Path.new(item_path, storage_class: storage)
|
|
55
|
+
item = Protocol::Caldav::Item.new(
|
|
56
|
+
path: item_p,
|
|
57
|
+
body: data[:body],
|
|
58
|
+
content_type: data[:content_type],
|
|
59
|
+
etag: data[:etag]
|
|
60
|
+
)
|
|
61
|
+
item.send(build, x)
|
|
62
|
+
end
|
|
52
63
|
end
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# Still list child collections/items for depth=1
|
|
79
|
-
if depth != '0'
|
|
80
|
-
storage.list_collections(col_path.to_s).each do |child_path, child_data|
|
|
81
|
-
child_p = Protocol::Caldav::Path.new(child_path, storage_class: storage)
|
|
82
|
-
child_col = Protocol::Caldav::Collection.new(
|
|
83
|
-
path: child_p,
|
|
84
|
-
type: child_data[:type],
|
|
85
|
-
displayname: child_data[:displayname],
|
|
86
|
-
description: child_data[:description],
|
|
87
|
-
color: child_data[:color],
|
|
88
|
-
props: child_data[:props]
|
|
89
|
-
)
|
|
90
|
-
responses << (propname ? child_col.to_propname_xml : child_col.to_propfind_xml)
|
|
64
|
+
elsif item_data
|
|
65
|
+
item = Protocol::Caldav::Item.new(
|
|
66
|
+
path: path,
|
|
67
|
+
body: item_data[:body],
|
|
68
|
+
content_type: item_data[:content_type],
|
|
69
|
+
etag: item_data[:etag]
|
|
70
|
+
)
|
|
71
|
+
item.send(build, x)
|
|
72
|
+
else
|
|
73
|
+
build_discovery(x, path, user)
|
|
74
|
+
|
|
75
|
+
if depth != '0'
|
|
76
|
+
storage.list_collections(col_path.to_s).each do |child_path, child_data|
|
|
77
|
+
child_p = Protocol::Caldav::Path.new(child_path, storage_class: storage)
|
|
78
|
+
child_col = Protocol::Caldav::Collection.new(
|
|
79
|
+
path: child_p,
|
|
80
|
+
type: child_data[:type],
|
|
81
|
+
displayname: child_data[:displayname],
|
|
82
|
+
description: child_data[:description],
|
|
83
|
+
color: child_data[:color],
|
|
84
|
+
props: child_data[:props]
|
|
85
|
+
)
|
|
86
|
+
child_col.send(build, x)
|
|
87
|
+
end
|
|
91
88
|
end
|
|
92
89
|
end
|
|
93
90
|
end
|
|
94
91
|
|
|
95
|
-
xml = Protocol::Caldav::Multistatus.new(responses).to_xml
|
|
96
92
|
[207, Protocol::Caldav::Constants::DAV_HEADERS, [xml]]
|
|
97
93
|
end
|
|
98
94
|
|
|
99
|
-
def
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<cr:addressbook-home-set><d:href>/addressbooks/#{user}/</d:href></cr:addressbook-home-set>
|
|
109
|
-
</d:prop>
|
|
110
|
-
<d:status>HTTP/1.1 200 OK</d:status>
|
|
111
|
-
</d:propstat>
|
|
112
|
-
</d:response>
|
|
113
|
-
XML
|
|
95
|
+
def build_discovery(xml, path, user)
|
|
96
|
+
Protocol::Caldav::XmlBuilder.response(xml, href: path.to_s) do
|
|
97
|
+
Protocol::Caldav::XmlBuilder.propstat_ok(xml) do
|
|
98
|
+
xml.tag!("d:resourcetype") { xml.tag!("d:collection") }
|
|
99
|
+
xml.tag!("d:current-user-principal") { xml.tag!("d:href", "/#{user}/") }
|
|
100
|
+
xml.tag!("c:calendar-home-set") { xml.tag!("d:href", "/calendars/#{user}/") }
|
|
101
|
+
xml.tag!("cr:addressbook-home-set") { xml.tag!("d:href", "/addressbooks/#{user}/") }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
114
104
|
end
|
|
115
105
|
|
|
116
|
-
private_class_method :
|
|
106
|
+
private_class_method :build_discovery
|
|
117
107
|
end
|
|
118
108
|
end
|
|
119
109
|
end
|
|
@@ -37,17 +37,14 @@ module Async
|
|
|
37
37
|
|
|
38
38
|
storage.update_collection(col_path.to_s, updates)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
XML
|
|
49
|
-
|
|
50
|
-
xml = Protocol::Caldav::Multistatus.new([response_xml]).to_xml
|
|
40
|
+
xml = Protocol::Caldav::Multistatus.new.to_xml do |x|
|
|
41
|
+
Protocol::Caldav::XmlBuilder.response(x, href: col_path.to_s) do
|
|
42
|
+
x.tag!("d:propstat") do
|
|
43
|
+
x.tag!("d:prop")
|
|
44
|
+
x.tag!("d:status", "HTTP/1.1 200 OK")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
51
48
|
[207, Protocol::Caldav::Constants::DAV_HEADERS, [xml]]
|
|
52
49
|
end
|
|
53
50
|
end
|
|
@@ -43,45 +43,46 @@ module Async
|
|
|
43
43
|
items = multi.select { |_, data| data }
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
xml = Protocol::Caldav::Multistatus.new.to_xml do |x|
|
|
47
|
+
items.each do |item_path, data|
|
|
48
|
+
next unless data
|
|
49
|
+
|
|
50
|
+
# Apply filter
|
|
51
|
+
if filter
|
|
52
|
+
if resource_type == :addressbook
|
|
53
|
+
card = Protocol::Caldav::Vcard::Parser.parse(data[:body])
|
|
54
|
+
next unless card && Protocol::Caldav::Filter::Match.addressbook?(filter, card)
|
|
55
|
+
else
|
|
56
|
+
component = Protocol::Caldav::Ical::Parser.parse(data[:body])
|
|
57
|
+
next unless component && Protocol::Caldav::Filter::Match.calendar?(filter, component)
|
|
58
|
+
end
|
|
57
59
|
end
|
|
58
|
-
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
item_body = data[:body]
|
|
62
|
+
|
|
63
|
+
# Apply expand if requested (calendar items only)
|
|
64
|
+
if expand_range && resource_type != :addressbook
|
|
65
|
+
component = Protocol::Caldav::Ical::Parser.parse(item_body)
|
|
66
|
+
if component
|
|
67
|
+
item_body = Protocol::Caldav::Ical::Expand.expand(
|
|
68
|
+
component,
|
|
69
|
+
range_start: expand_range[:start],
|
|
70
|
+
range_end: expand_range[:end]
|
|
71
|
+
)
|
|
72
|
+
end
|
|
71
73
|
end
|
|
72
|
-
end
|
|
73
74
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
item_p = Protocol::Caldav::Path.new(item_path, storage_class: storage)
|
|
76
|
+
item = Protocol::Caldav::Item.new(
|
|
77
|
+
path: item_p,
|
|
78
|
+
body: item_body,
|
|
79
|
+
content_type: data[:content_type],
|
|
80
|
+
etag: data[:etag]
|
|
81
|
+
)
|
|
82
|
+
item.build_report(x, data_tag: data_tag)
|
|
83
|
+
end
|
|
82
84
|
end
|
|
83
85
|
|
|
84
|
-
xml = Protocol::Caldav::Multistatus.new(responses).to_xml
|
|
85
86
|
[207, Protocol::Caldav::Constants::DAV_HEADERS, [xml]]
|
|
86
87
|
end
|
|
87
88
|
|
|
@@ -97,67 +98,48 @@ module Async
|
|
|
97
98
|
# Incremental sync
|
|
98
99
|
result = storage.sync_changes(col_path, old_token)
|
|
99
100
|
unless result
|
|
100
|
-
|
|
101
|
-
error_xml
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
XML
|
|
107
|
-
return [403, { 'content-type' => 'application/xml' }, [error_xml]]
|
|
101
|
+
error_xml = Builder::XmlMarkup.new
|
|
102
|
+
error_xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
103
|
+
error_xml.tag!("d:error", "xmlns:d" => "DAV:") do
|
|
104
|
+
error_xml.tag!("d:valid-sync-token")
|
|
105
|
+
end
|
|
106
|
+
return [403, { 'content-type' => 'application/xml' }, [error_xml.target!]]
|
|
108
107
|
end
|
|
109
108
|
|
|
110
109
|
new_token, changes = result
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
<d:prop>
|
|
126
|
-
<d:getetag>#{Protocol::Caldav::Xml.escape(etag)}</d:getetag>
|
|
127
|
-
</d:prop>
|
|
128
|
-
<d:status>HTTP/1.1 200 OK</d:status>
|
|
129
|
-
</d:propstat>
|
|
130
|
-
</d:response>
|
|
131
|
-
XML
|
|
110
|
+
xml = Protocol::Caldav::XmlBuilder.multistatus do |x|
|
|
111
|
+
changes.each do |item_path, status|
|
|
112
|
+
if status == :deleted
|
|
113
|
+
Protocol::Caldav::XmlBuilder.response(x, href: item_path) do
|
|
114
|
+
x.tag!("d:status", "HTTP/1.1 404 Not Found")
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
etag = storage.etag(item_path)
|
|
118
|
+
Protocol::Caldav::XmlBuilder.response(x, href: item_path) do
|
|
119
|
+
Protocol::Caldav::XmlBuilder.propstat_ok(x) do
|
|
120
|
+
x.tag!("d:getetag", etag)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
132
124
|
end
|
|
125
|
+
x.tag!("d:sync-token", new_token)
|
|
133
126
|
end
|
|
134
127
|
else
|
|
135
128
|
# Initial sync — return all items
|
|
136
129
|
new_token = storage.snapshot_sync(col_path)
|
|
137
130
|
items = storage.list_items(col_path)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
</d:propstat>
|
|
148
|
-
</d:response>
|
|
149
|
-
XML
|
|
131
|
+
xml = Protocol::Caldav::XmlBuilder.multistatus do |x|
|
|
132
|
+
items.each do |item_path, data|
|
|
133
|
+
Protocol::Caldav::XmlBuilder.response(x, href: item_path) do
|
|
134
|
+
Protocol::Caldav::XmlBuilder.propstat_ok(x) do
|
|
135
|
+
x.tag!("d:getetag", data[:etag])
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
x.tag!("d:sync-token", new_token)
|
|
150
140
|
end
|
|
151
141
|
end
|
|
152
142
|
|
|
153
|
-
xml = <<~XML
|
|
154
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
155
|
-
<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:cr="urn:ietf:params:xml:ns:carddav" xmlns:cs="http://calendarserver.org/ns/" xmlns:x="http://apple.com/ns/ical/">
|
|
156
|
-
#{responses.join}
|
|
157
|
-
<d:sync-token>#{Protocol::Caldav::Xml.escape(new_token)}</d:sync-token>
|
|
158
|
-
</d:multistatus>
|
|
159
|
-
XML
|
|
160
|
-
|
|
161
143
|
[207, Protocol::Caldav::Constants::DAV_HEADERS, [xml]]
|
|
162
144
|
end
|
|
163
145
|
|
|
@@ -14,6 +14,8 @@ module Async
|
|
|
14
14
|
@root = root
|
|
15
15
|
@sync_snapshots = {}
|
|
16
16
|
FileUtils.mkdir_p(@root)
|
|
17
|
+
@root_normalized = ::File.expand_path(@root)
|
|
18
|
+
@root_with_separator = @root_normalized.end_with?(::File::SEPARATOR) ? @root_normalized : "#{@root_normalized}#{::File::SEPARATOR}"
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
# --- Collections ---
|
|
@@ -39,9 +41,9 @@ module Async
|
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
def delete_collection(path)
|
|
42
|
-
|
|
43
|
-
if
|
|
44
|
-
FileUtils.rm_rf(
|
|
44
|
+
s = stat(path)
|
|
45
|
+
if s && s.directory?
|
|
46
|
+
FileUtils.rm_rf(full_path(path))
|
|
45
47
|
true
|
|
46
48
|
else
|
|
47
49
|
false
|
|
@@ -89,13 +91,15 @@ module Async
|
|
|
89
91
|
# --- Items ---
|
|
90
92
|
|
|
91
93
|
def get_item(path)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
s = stat(path)
|
|
95
|
+
if s && s.file?
|
|
96
|
+
body = File.read(full_path(path))
|
|
97
|
+
content_type = guess_content_type(path)
|
|
98
|
+
etag = Protocol::Caldav::ETag.compute(body)
|
|
99
|
+
{ body: body, content_type: content_type, etag: etag }
|
|
100
|
+
else
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
def put_item(path, body, content_type)
|
|
@@ -109,9 +113,9 @@ module Async
|
|
|
109
113
|
end
|
|
110
114
|
|
|
111
115
|
def delete_item(path)
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
File.delete(
|
|
116
|
+
s = stat(path)
|
|
117
|
+
if s && s.file?
|
|
118
|
+
File.delete(full_path(path))
|
|
115
119
|
true
|
|
116
120
|
else
|
|
117
121
|
false
|
|
@@ -136,13 +140,15 @@ module Async
|
|
|
136
140
|
end
|
|
137
141
|
|
|
138
142
|
def move_item(from_path, to_path)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
s = stat(from_path)
|
|
144
|
+
if s && s.file?
|
|
145
|
+
dst = full_path(to_path)
|
|
146
|
+
FileUtils.mkdir_p(File.dirname(dst))
|
|
147
|
+
FileUtils.mv(full_path(from_path), dst)
|
|
148
|
+
get_item(to_path)
|
|
149
|
+
else
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
146
152
|
end
|
|
147
153
|
|
|
148
154
|
def get_multi(paths)
|
|
@@ -152,8 +158,11 @@ module Async
|
|
|
152
158
|
# --- General ---
|
|
153
159
|
|
|
154
160
|
def exists?(path)
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
if stat(path)
|
|
162
|
+
true
|
|
163
|
+
else
|
|
164
|
+
collection_exists?(path)
|
|
165
|
+
end
|
|
157
166
|
end
|
|
158
167
|
|
|
159
168
|
def etag(path)
|
|
@@ -208,7 +217,26 @@ module Async
|
|
|
208
217
|
private
|
|
209
218
|
|
|
210
219
|
def full_path(path)
|
|
211
|
-
|
|
220
|
+
if valid_path?(path)
|
|
221
|
+
expanded = ::File.expand_path(::File.join(@root, path))
|
|
222
|
+
if expanded == @root_normalized || expanded.start_with?(@root_with_separator)
|
|
223
|
+
expanded
|
|
224
|
+
else
|
|
225
|
+
raise ArgumentError, "path escapes root: #{path.inspect}"
|
|
226
|
+
end
|
|
227
|
+
else
|
|
228
|
+
raise ArgumentError, "invalid path: #{path.inspect}"
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def valid_path?(path)
|
|
233
|
+
path.is_a?(String) && path.valid_encoding? && !path.include?("\0")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def stat(path)
|
|
237
|
+
::File.stat(full_path(path))
|
|
238
|
+
rescue Errno::ENOENT, Errno::ELOOP
|
|
239
|
+
nil
|
|
212
240
|
end
|
|
213
241
|
|
|
214
242
|
def guess_content_type(path)
|
|
@@ -361,6 +389,21 @@ test do
|
|
|
361
389
|
end
|
|
362
390
|
end
|
|
363
391
|
|
|
392
|
+
it "rejects path traversal attempts" do
|
|
393
|
+
Dir.mktmpdir do |dir|
|
|
394
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
395
|
+
lambda { s.get_item("/../../etc/passwd") }.should.raise ArgumentError
|
|
396
|
+
lambda { s.put_item("/cal/../../escape.ics", "x", "text/calendar") }.should.raise ArgumentError
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
it "rejects NUL byte in path" do
|
|
401
|
+
Dir.mktmpdir do |dir|
|
|
402
|
+
s = Async::Caldav::Storage::Filesystem.new(dir)
|
|
403
|
+
lambda { s.get_item("/cal\0/x.ics") }.should.raise ArgumentError
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
364
407
|
it "same body produces same etag across backends" do
|
|
365
408
|
body = "BEGIN:VCALENDAR\r\nEND:VCALENDAR"
|
|
366
409
|
mock = Async::Caldav::Storage::Mock.new
|
data/lib/async/caldav/version.rb
CHANGED