dav4rack_ext 0.0.5 → 0.0.6
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.
- data/README.md +13 -0
- data/lib/dav4rack_ext/carddav/app.rb +2 -2
- data/lib/dav4rack_ext/carddav/controller.rb +37 -52
- data/lib/dav4rack_ext/carddav/resource.rb +4 -12
- data/lib/dav4rack_ext/carddav/resources/addressbook_collection_resource.rb +1 -1
- data/lib/dav4rack_ext/carddav/resources/contact_resource.rb +6 -14
- data/lib/dav4rack_ext/carddav/resources/principal_resource.rb +3 -7
- data/lib/dav4rack_ext/handler.rb +31 -37
- data/lib/dav4rack_ext/helpers/properties.rb +51 -51
- data/lib/dav4rack_ext/version.rb +1 -1
- data/specs/rfc/rfc6352_spec.rb +3 -0
- metadata +5 -5
data/README.md
CHANGED
@@ -13,6 +13,19 @@ This gem extends dav4rack to provide a CardDAV extension, CalDAV is not currentl
|
|
13
13
|
|
14
14
|
Have a look at the examle folder, this is a standard Rack application and should run with any compliant server.
|
15
15
|
|
16
|
+
You can run the example with thin like this:
|
17
|
+
|
18
|
+
```bash
|
19
|
+
$ cd example
|
20
|
+
$ bundle exec thin start
|
21
|
+
```
|
22
|
+
|
23
|
+
Once the server is started you can connect to it using http://127.0.0.1:3000/u/cards with any login/password
|
24
|
+
(the example has no authentication set up)
|
25
|
+
|
26
|
+
# Supported clients
|
27
|
+
- Mac OS X (tested with Mountain Lion )
|
28
|
+
- iPhone 5.x and 6.x
|
16
29
|
|
17
30
|
# Setting up development environment
|
18
31
|
|
@@ -6,8 +6,8 @@ module DAV4Rack
|
|
6
6
|
DAV_EXTENSIONS = ["access-control", "addressbook"].freeze
|
7
7
|
|
8
8
|
def self.app(root_path = '/', opts = {})
|
9
|
-
logger
|
10
|
-
current_user
|
9
|
+
logger = opts.delete(:logger) || ::Logger.new('/dev/null')
|
10
|
+
current_user = opts.delete(:current_user)
|
11
11
|
root_uri_path = opts.delete(:root_uri_path) || root_path
|
12
12
|
|
13
13
|
if (root_path != '/') && root_path[-1] == '/'
|
@@ -2,6 +2,13 @@ module DAV4Rack
|
|
2
2
|
module Carddav
|
3
3
|
|
4
4
|
class Controller < DAV4Rack::Controller
|
5
|
+
include DAV4Rack::Utils
|
6
|
+
|
7
|
+
NAMESPACES = {
|
8
|
+
'D' => 'DAV:',
|
9
|
+
'C' => 'urn:ietf:params:xml:ns:carddav'
|
10
|
+
}
|
11
|
+
|
5
12
|
def initialize(*args, options, env)
|
6
13
|
super(*args, options.merge(env: env))
|
7
14
|
end
|
@@ -46,67 +53,45 @@ module DAV4Rack
|
|
46
53
|
end
|
47
54
|
"*[local-name()='#{name}' and namespace-uri()='#{ns_uri}']"
|
48
55
|
end
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
ns_uri = 'urn:ietf:params:xml:ns:carddav'
|
56
|
+
|
57
|
+
def addressbook_multiget(request_document)
|
58
|
+
# TODO: Include a DAV:error response
|
59
|
+
# CardDAV §8.7 clearly states Depth must equal zero for this report
|
60
|
+
# But Apple's AddressBook.app sets the depth to infinity anyhow.
|
61
|
+
unless depth == 0 or depth == :infinity
|
62
|
+
render_xml(:error) do |xml|
|
63
|
+
xml.send :'invalid-depth'
|
58
64
|
end
|
59
|
-
|
65
|
+
raise BadRequest
|
60
66
|
end
|
61
|
-
|
62
|
-
def addressbook_multiget(request_document)
|
63
|
-
# TODO: Include a DAV:error response
|
64
|
-
# CardDAV §8.7 clearly states Depth must equal zero for this report
|
65
|
-
# But Apple's AddressBook.app sets the depth to infinity anyhow.
|
66
|
-
unless depth == 0 or depth == :infinity
|
67
|
-
render_xml(:error) do |xml|
|
68
|
-
xml.send :'invalid-depth'
|
69
|
-
end
|
70
|
-
raise BadRequest
|
71
|
-
end
|
72
67
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
# - Check for mime-type and version. If present they must match vCard 3.0 for now since we don't support anything else.
|
79
|
-
hrefs = request_document.xpath("/#{xpath_element('addressbook-multiget', :carddav)}/#{xpath_element('href')}").collect{|n|
|
80
|
-
text = n.text
|
81
|
-
# TODO: Make sure that the hrefs passed into the report are either paths or fully qualified URLs with the right host+protocol+port prefix
|
82
|
-
path = URI.parse(text).path
|
83
|
-
Logger.debug "Scanned this HREF: #{text} PATH: #{path}"
|
84
|
-
text
|
85
|
-
}.compact
|
86
|
-
|
87
|
-
if hrefs.empty?
|
88
|
-
xml_error(BadRequest) do |err|
|
89
|
-
err.send :'href-missing'
|
90
|
-
end
|
68
|
+
# props = request_document.css("C|addressbook-multiget C|address-data > C|prop", namespaces).map do |el|
|
69
|
+
props = []
|
70
|
+
request_document.css("C|addressbook-multiget > D|prop", NAMESPACES).each do |el|
|
71
|
+
el.children.select(&:element?).each do |child|
|
72
|
+
props << to_element_hash(child)
|
91
73
|
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# collect the requested urls
|
77
|
+
hrefs = request_document.css("C|addressbook-multiget D|href", NAMESPACES).map(&:content)
|
92
78
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
79
|
+
multistatus do |xml|
|
80
|
+
hrefs.each do |_href|
|
81
|
+
xml.response do
|
82
|
+
xml.href _href
|
97
83
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
cur_resource = resource.is_self?(_href) ? resource : resource.find_child(File.split(path).last)
|
84
|
+
path = File.split(URI.parse(_href).path).last
|
85
|
+
Logger.debug "Creating child w/ ORIG=#{resource.public_path} HREF=#{_href} FILE=#{path}!"
|
102
86
|
|
103
|
-
|
104
|
-
propstats(xml, get_properties(cur_resource, props))
|
105
|
-
else
|
106
|
-
xml.status "#{http_version} #{NotFound.status_line}"
|
107
|
-
end
|
87
|
+
cur_resource = resource.is_self?(_href) ? resource : resource.find_child(File.split(path).last)
|
108
88
|
|
89
|
+
if cur_resource && cur_resource.exist?
|
90
|
+
propstats(xml, get_properties(cur_resource, props))
|
91
|
+
else
|
92
|
+
xml.status "#{http_version} #{NotFound.status_line}"
|
109
93
|
end
|
94
|
+
|
110
95
|
end
|
111
96
|
end
|
112
97
|
end
|
@@ -18,12 +18,7 @@ module DAV4Rack
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def user_agent
|
21
|
-
|
22
|
-
if env
|
23
|
-
env['HTTP_USER_AGENT'] || ""
|
24
|
-
else
|
25
|
-
""
|
26
|
-
end
|
21
|
+
options[:env]['HTTP_USER_AGENT'].to_s rescue ""
|
27
22
|
end
|
28
23
|
|
29
24
|
def router_params
|
@@ -31,10 +26,9 @@ module DAV4Rack
|
|
31
26
|
end
|
32
27
|
|
33
28
|
def setup
|
34
|
-
|
35
29
|
@propstat_relative_path = true
|
36
30
|
@root_xml_attributes = {
|
37
|
-
'xmlns:C' => CARDAV_NS,
|
31
|
+
'xmlns:C' => CARDAV_NS,
|
38
32
|
'xmlns:APPLE1' => 'http://calendarserver.org/ns/'
|
39
33
|
}
|
40
34
|
end
|
@@ -51,9 +45,8 @@ module DAV4Rack
|
|
51
45
|
namespace = element[:ns_href]
|
52
46
|
|
53
47
|
key = "#{namespace}*#{name}"
|
54
|
-
|
55
|
-
handler = self.class.properties[key]
|
56
|
-
if handler
|
48
|
+
|
49
|
+
if handler = self.class.properties[key]
|
57
50
|
ret = instance_exec(element, &handler[0])
|
58
51
|
# TODO: find better than that
|
59
52
|
if ret.is_a?(String) && ret.include?('<')
|
@@ -93,7 +86,6 @@ module DAV4Rack
|
|
93
86
|
|
94
87
|
def properties
|
95
88
|
selected_properties = self.class.properties.reject{|key, arr| arr[1] == true }
|
96
|
-
ret = {}
|
97
89
|
selected_properties.keys.map do |key|
|
98
90
|
ns, name = key.split('*')
|
99
91
|
{:name => name, :ns_href => ns}
|
@@ -42,8 +42,6 @@ module DAV4Rack
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
45
|
-
|
46
|
-
|
47
45
|
|
48
46
|
def collection?
|
49
47
|
false
|
@@ -56,10 +54,8 @@ module DAV4Rack
|
|
56
54
|
|
57
55
|
def setup
|
58
56
|
super
|
59
|
-
|
60
57
|
@address_book = @options[:_parent_] || current_user.current_addressbook()
|
61
58
|
@contact = @options[:_object_] || current_user.current_contact()
|
62
|
-
|
63
59
|
end
|
64
60
|
|
65
61
|
def put(request, response)
|
@@ -84,9 +80,8 @@ module DAV4Rack
|
|
84
80
|
|
85
81
|
# If the client has explicitly stated they want a new contact
|
86
82
|
raise Conflict if (want_new_contact and @contact)
|
87
|
-
|
88
|
-
if_match = request.env['HTTP_IF_MATCH']
|
89
|
-
if if_match
|
83
|
+
|
84
|
+
if if_match = request.env['HTTP_IF_MATCH']
|
90
85
|
# client wants to update a contact, return an error if no
|
91
86
|
# contact was found
|
92
87
|
if (if_match == '*') || !@contact
|
@@ -109,18 +104,15 @@ module DAV4Rack
|
|
109
104
|
|
110
105
|
@contact.update_from_vcard(vcf)
|
111
106
|
|
112
|
-
if @contact.save
|
107
|
+
if @contact.save(user_agent)
|
113
108
|
new_public = @public_path.split('/')[0...-1]
|
114
109
|
new_public << @contact.uid.to_s
|
115
|
-
|
110
|
+
|
116
111
|
@public_path = new_public.join('/')
|
117
112
|
response['ETag'] = @contact.etag
|
118
|
-
Created
|
119
|
-
else
|
120
|
-
# Mac OS X Contact will reload the contact
|
121
|
-
# from the server
|
122
|
-
raise Forbidden
|
123
113
|
end
|
114
|
+
|
115
|
+
Created
|
124
116
|
end
|
125
117
|
|
126
118
|
def parent
|
@@ -4,15 +4,13 @@ module DAV4Rack
|
|
4
4
|
class PrincipalResource < Resource
|
5
5
|
|
6
6
|
def exist?
|
7
|
-
|
8
|
-
return ret
|
7
|
+
(path == '') || (path == '/')
|
9
8
|
end
|
10
9
|
|
11
10
|
def collection?
|
12
|
-
|
11
|
+
true
|
13
12
|
end
|
14
|
-
|
15
|
-
|
13
|
+
|
16
14
|
define_properties('DAV:') do
|
17
15
|
property('alternate-URI-set') do
|
18
16
|
# "<D:alternate-URI-set xmlns:D='DAV:' />"
|
@@ -66,7 +64,6 @@ module DAV4Rack
|
|
66
64
|
EOS
|
67
65
|
end
|
68
66
|
|
69
|
-
|
70
67
|
property('resourcetype') do
|
71
68
|
<<-EOS
|
72
69
|
<resourcetype>
|
@@ -104,7 +101,6 @@ module DAV4Rack
|
|
104
101
|
property('principal-address') do
|
105
102
|
""
|
106
103
|
end
|
107
|
-
|
108
104
|
end
|
109
105
|
end
|
110
106
|
|
data/lib/dav4rack_ext/handler.rb
CHANGED
@@ -1,51 +1,45 @@
|
|
1
1
|
require 'dav4rack/http_status'
|
2
2
|
|
3
3
|
module DAV4RackExt
|
4
|
-
|
5
4
|
class Handler
|
6
5
|
# include DAV4Rack::HTTPStatus
|
7
|
-
|
8
|
-
def initialize(options= {})
|
9
|
-
@options
|
10
|
-
@logger
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@options = options.dup
|
9
|
+
@logger = options.delete(:logger)
|
10
|
+
@controller_class = options.delete(:controller_class) || DAV4Rack::Controller
|
11
11
|
end
|
12
12
|
|
13
13
|
def call(env)
|
14
|
+
request = Rack::Request.new(env)
|
15
|
+
response = Rack::Response.new
|
16
|
+
|
14
17
|
begin
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
response['Content-Length'] = response.body.to_s.bytesize unless response['Content-Length'] || !response.body.is_a?(String)
|
31
|
-
response.body = [response.body] unless response.body.respond_to? :each
|
32
|
-
response.status = response.status ? response.status.to_i : 200
|
33
|
-
response.headers.keys.each do |k|
|
34
|
-
response.headers[k] = response[k].to_s
|
35
|
-
end
|
36
|
-
|
37
|
-
while request.body.read(8192)
|
38
|
-
# Apache wants the body dealt with, so just read it and junk it
|
39
|
-
end
|
40
|
-
|
41
|
-
response.finish
|
42
|
-
rescue Exception => e
|
43
|
-
@logger.error "DAV Error: #{e}\n#{e.backtrace.join("\n")}"
|
44
|
-
raise e
|
18
|
+
controller = @controller_class.new(request, response, @options.dup, env)
|
19
|
+
res = controller.send(request.request_method.downcase)
|
20
|
+
response.status = res.code if res.respond_to?(:code)
|
21
|
+
|
22
|
+
rescue DAV4Rack::HTTPStatus::Status => status
|
23
|
+
response.status = status.code
|
24
|
+
end
|
25
|
+
|
26
|
+
response['Content-Length'] = response.body.to_s.bytesize unless response['Content-Length'] || !response.body.is_a?(String)
|
27
|
+
response.body = [response.body] unless response.body.respond_to? :each
|
28
|
+
response.status = response.status ? response.status.to_i : 200
|
29
|
+
response.headers.keys.each do |k|
|
30
|
+
response.headers[k] = response[k].to_s
|
45
31
|
end
|
32
|
+
|
33
|
+
while request.body.read(8192)
|
34
|
+
# Apache wants the body dealt with, so just read it and junk it
|
35
|
+
end
|
36
|
+
|
37
|
+
response.finish
|
38
|
+
rescue Exception => e
|
39
|
+
@logger.error "DAV Error: #{e}\n#{e.backtrace.join("\n")}"
|
40
|
+
raise e
|
46
41
|
end
|
47
|
-
|
48
|
-
end
|
49
42
|
|
43
|
+
end
|
50
44
|
end
|
51
45
|
|
@@ -1,55 +1,55 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.extended(klass)
|
17
|
-
class << klass
|
18
|
-
include MetaClassMethods
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
# inheritable accessor
|
23
|
-
module MetaClassMethods
|
24
|
-
def define_property(namespace, name, explicit = false, &block)
|
25
|
-
_properties["#{namespace}*#{name}"] = [block, explicit]
|
26
|
-
end
|
27
|
-
|
28
|
-
def properties
|
29
|
-
inherited = superclass.respond_to?(:properties) ? superclass.properties : {}
|
30
|
-
inherited.merge(_properties)
|
31
|
-
end
|
32
|
-
|
33
|
-
def _properties
|
34
|
-
@properties ||= {}
|
35
|
-
end
|
1
|
+
module Helpers
|
2
|
+
module Properties
|
3
|
+
class MethodMissingRedirector
|
4
|
+
def initialize(*methods, &block)
|
5
|
+
@block = block
|
6
|
+
@methods = methods
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(name, *args, &block)
|
10
|
+
if @methods.empty? || @methods.include?(name)
|
11
|
+
@block.call(name, *args, &block)
|
36
12
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.extended(klass)
|
17
|
+
class << klass
|
18
|
+
include MetaClassMethods
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# inheritable accessor
|
23
|
+
module MetaClassMethods
|
24
|
+
def define_property(namespace, name, explicit = false, &block)
|
25
|
+
_properties["#{namespace}*#{name}"] = [block, explicit]
|
26
|
+
end
|
27
|
+
|
28
|
+
def properties
|
29
|
+
inherited = superclass.respond_to?(:properties) ? superclass.properties : {}
|
30
|
+
inherited.merge(_properties)
|
31
|
+
end
|
32
|
+
|
33
|
+
def _properties
|
34
|
+
@properties ||= {}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def define_properties(namespace, &block)
|
39
|
+
explicit = false
|
40
|
+
obj = MethodMissingRedirector.new(:property, :explicit) do |method_name, name, &block|
|
41
|
+
if method_name == :property
|
42
|
+
define_property(namespace, name.to_s, explicit, &block)
|
43
|
+
elsif method_name == :explicit
|
44
|
+
explicit = true
|
45
|
+
block.call
|
46
|
+
else
|
47
|
+
raise NoMethodError, method_name
|
52
48
|
end
|
53
|
-
|
54
49
|
end
|
50
|
+
|
51
|
+
obj.instance_eval(&block)
|
55
52
|
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
data/lib/dav4rack_ext/version.rb
CHANGED
data/specs/rfc/rfc6352_spec.rb
CHANGED
@@ -255,6 +255,9 @@ END:VCARD
|
|
255
255
|
# '*=' = include
|
256
256
|
ensure_element_exists(response, %{D|href[text()*="1234-5678-9000-2"] + D|status[text()*="404"]}, 'D' => @dav_ns)
|
257
257
|
|
258
|
+
# <D:getetag>"23ba4d-ff11fb"</D:getetag>
|
259
|
+
etag = ensure_element_exists(response, %{D|href[text()*="1234-5678-9000-1"] + D|propstat > D|prop > D|getetag}, 'D' => @dav_ns)
|
260
|
+
etag.text.should == 'ETAG'
|
258
261
|
|
259
262
|
vcard = ensure_element_exists(response, %{D|href[text()*="1234-5678-9000-1"] + D|propstat > D|prop > C|address-data}, 'D' => @dav_ns, 'C' => @carddav_ns)
|
260
263
|
vcard.text.should.include? <<-EOS
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dav4rack_ext
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.6
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: dav4rack
|
@@ -116,7 +116,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
116
116
|
version: '0'
|
117
117
|
segments:
|
118
118
|
- 0
|
119
|
-
hash:
|
119
|
+
hash: -2586617098076442739
|
120
120
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
121
|
none: false
|
122
122
|
requirements:
|
@@ -125,10 +125,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
125
125
|
version: '0'
|
126
126
|
segments:
|
127
127
|
- 0
|
128
|
-
hash:
|
128
|
+
hash: -2586617098076442739
|
129
129
|
requirements: []
|
130
130
|
rubyforge_project:
|
131
|
-
rubygems_version: 1.8.
|
131
|
+
rubygems_version: 1.8.24
|
132
132
|
signing_key:
|
133
133
|
specification_version: 3
|
134
134
|
summary: CardDAV / CalDAV implementation.
|