dav4rack_ext 0.0.5 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|