dav4rack_ext 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.travis.yml +4 -0
- data/Gemfile +27 -0
- data/Guardfile +5 -0
- data/LICENSE +22 -0
- data/README.md +30 -0
- data/Rakefile +27 -0
- data/dav4rack_ext.gemspec +20 -0
- data/example/config.ru +50 -0
- data/example/rack_sniffer.rb +71 -0
- data/example/report.xml +151 -0
- data/example/test.rb +50 -0
- data/lib/dav4rack_ext/carddav/app.rb +78 -0
- data/lib/dav4rack_ext/carddav/controller.rb +117 -0
- data/lib/dav4rack_ext/carddav/resource.rb +140 -0
- data/lib/dav4rack_ext/carddav/resources/addressbook_collection_resource.rb +23 -0
- data/lib/dav4rack_ext/carddav/resources/addressbook_resource.rb +137 -0
- data/lib/dav4rack_ext/carddav/resources/contact_resource.rb +132 -0
- data/lib/dav4rack_ext/carddav/resources/principal_resource.rb +132 -0
- data/lib/dav4rack_ext/carddav.rb +13 -0
- data/lib/dav4rack_ext/handler.rb +51 -0
- data/lib/dav4rack_ext/helpers/properties.rb +55 -0
- data/lib/dav4rack_ext/version.rb +3 -0
- data/lib/dav4rack_ext.rb +4 -0
- data/specs/factories.rb +20 -0
- data/specs/rfc/rfc3744_spec.rb +62 -0
- data/specs/rfc/rfc5397_spec.rb +38 -0
- data/specs/rfc/rfc6352_spec.rb +284 -0
- data/specs/spec_helper.rb +42 -0
- data/specs/support/models.rb +161 -0
- data/specs/unit/carddav/app_spec.rb +36 -0
- data/specs/unit/carddav/resources/addressbook_collection_resource_spec.rb +51 -0
- data/specs/unit/carddav/resources/principal_resource_spec.rb +44 -0
- data/specs/unit/helpers/properties_spec.rb +58 -0
- metadata +133 -0
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'http_router'
|
2
|
+
|
3
|
+
module DAV4Rack
|
4
|
+
module Carddav
|
5
|
+
|
6
|
+
DAV_EXTENSIONS = ["access-control", "addressbook"].freeze
|
7
|
+
|
8
|
+
def self.app(root_path = '/', opts = {})
|
9
|
+
logger = opts.delete(:logger) || ::Logger.new('/dev/null')
|
10
|
+
current_user = opts.delete(:current_user)
|
11
|
+
root_uri_path = opts.delete(:root_uri_path) || root_path
|
12
|
+
|
13
|
+
if root_path[-1] != '/'
|
14
|
+
root_path << '/'
|
15
|
+
end
|
16
|
+
|
17
|
+
raise "unknown options: #{opts}" unless opts.empty?
|
18
|
+
|
19
|
+
HttpRouter.new do |r|
|
20
|
+
# try to help iOS
|
21
|
+
r.add("#{root_path}/.well_known/carddav/").to do |env|
|
22
|
+
root = env['REQUEST_PATH']
|
23
|
+
pos = root.index('.well_known')
|
24
|
+
[302, {'Location' => root[0...pos]}, []]
|
25
|
+
end
|
26
|
+
|
27
|
+
r.add("#{root_path}").to DAV4RackExt::Handler.new(
|
28
|
+
:logger => logger,
|
29
|
+
:dav_extensions => DAV_EXTENSIONS,
|
30
|
+
:alway_include_dav_header => true,
|
31
|
+
:pretty_xml => true,
|
32
|
+
:root_uri_path => root_uri_path,
|
33
|
+
:resource_class => DAV4Rack::Carddav::PrincipalResource,
|
34
|
+
:controller_class => DAV4Rack::Carddav::Controller,
|
35
|
+
:current_user => current_user,
|
36
|
+
|
37
|
+
# resource options
|
38
|
+
:books_collection => "/books/"
|
39
|
+
)
|
40
|
+
|
41
|
+
r.add("#{root_path}books/").to DAV4RackExt::Handler.new(
|
42
|
+
:logger => logger,
|
43
|
+
:dav_extensions => DAV_EXTENSIONS,
|
44
|
+
:alway_include_dav_header => true,
|
45
|
+
:pretty_xml => true,
|
46
|
+
:root_uri_path => root_uri_path,
|
47
|
+
:resource_class => DAV4Rack::Carddav::AddressbookCollectionResource,
|
48
|
+
:controller_class => DAV4Rack::Carddav::Controller,
|
49
|
+
:current_user => current_user
|
50
|
+
)
|
51
|
+
|
52
|
+
r.add("#{root_path}books/:book_id/:contact_id(.vcf)").to DAV4RackExt::Handler.new(
|
53
|
+
:logger => logger,
|
54
|
+
:dav_extensions => DAV_EXTENSIONS,
|
55
|
+
:alway_include_dav_header => true,
|
56
|
+
:pretty_xml => true,
|
57
|
+
:root_uri_path => root_uri_path,
|
58
|
+
:resource_class => DAV4Rack::Carddav::ContactResource,
|
59
|
+
:controller_class => DAV4Rack::Carddav::Controller,
|
60
|
+
:current_user => current_user
|
61
|
+
)
|
62
|
+
|
63
|
+
r.add("#{root_path}books/:book_id").to DAV4RackExt::Handler.new(
|
64
|
+
:logger => logger,
|
65
|
+
:dav_extensions => DAV_EXTENSIONS,
|
66
|
+
:alway_include_dav_header => true,
|
67
|
+
:pretty_xml => true,
|
68
|
+
:root_uri_path => root_uri_path,
|
69
|
+
:resource_class => DAV4Rack::Carddav::AddressbookResource,
|
70
|
+
:controller_class => DAV4Rack::Carddav::Controller,
|
71
|
+
:current_user => current_user
|
72
|
+
)
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
module Carddav
|
3
|
+
|
4
|
+
class Controller < DAV4Rack::Controller
|
5
|
+
def initialize(*args, options, env)
|
6
|
+
super(*args, options.merge(env: env))
|
7
|
+
end
|
8
|
+
|
9
|
+
def report
|
10
|
+
unless resource.exist?
|
11
|
+
return NotFound
|
12
|
+
end
|
13
|
+
|
14
|
+
if request_document.nil? or request_document.root.nil?
|
15
|
+
render_xml(:error) do |xml|
|
16
|
+
xml.send :'empty-request'
|
17
|
+
end
|
18
|
+
raise BadRequest
|
19
|
+
end
|
20
|
+
|
21
|
+
case request_document.root.name
|
22
|
+
when 'addressbook-multiget'
|
23
|
+
addressbook_multiget(request_document)
|
24
|
+
else
|
25
|
+
render_xml(:error) do |xml|
|
26
|
+
xml.send :'supported-report'
|
27
|
+
end
|
28
|
+
raise Forbidden
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def root_uri_path
|
36
|
+
tmp = @options[:root_uri_path]
|
37
|
+
tmp.respond_to?(:call) ? tmp.call(@options[:env]) : tmp
|
38
|
+
end
|
39
|
+
|
40
|
+
def xpath_element(name, ns_uri=:dav)
|
41
|
+
case ns_uri
|
42
|
+
when :dav
|
43
|
+
ns_uri = 'DAV:'
|
44
|
+
when :carddav
|
45
|
+
ns_uri = 'urn:ietf:params:xml:ns:carddav'
|
46
|
+
end
|
47
|
+
"*[local-name()='#{name}' and namespace-uri()='#{ns_uri}']"
|
48
|
+
end
|
49
|
+
|
50
|
+
include DAV4Rack::Utils
|
51
|
+
|
52
|
+
def xpath_element(name, ns_uri=:dav)
|
53
|
+
case ns_uri
|
54
|
+
when :dav
|
55
|
+
ns_uri = 'DAV:'
|
56
|
+
when :carddav
|
57
|
+
ns_uri = 'urn:ietf:params:xml:ns:carddav'
|
58
|
+
end
|
59
|
+
"*[local-name()='#{name}' and namespace-uri()='#{ns_uri}']"
|
60
|
+
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
|
+
|
73
|
+
props = request_document.xpath("/#{xpath_element('addressbook-multiget', :carddav)}/#{xpath_element('prop')}").children.find_all(&:element?).map{|n|
|
74
|
+
to_element_hash(n)
|
75
|
+
}
|
76
|
+
# Handle the address-data element
|
77
|
+
# - Check for child properties (vCard fields)
|
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
|
91
|
+
end
|
92
|
+
|
93
|
+
multistatus do |xml|
|
94
|
+
hrefs.each do |_href|
|
95
|
+
xml.response do
|
96
|
+
xml.href _href
|
97
|
+
|
98
|
+
path = File.split(URI.parse(_href).path).last
|
99
|
+
Logger.debug "Creating child w/ ORIG=#{resource.public_path} HREF=#{_href} FILE=#{path}!"
|
100
|
+
|
101
|
+
cur_resource = resource.is_self?(_href) ? resource : resource.find_child(File.split(path).last)
|
102
|
+
|
103
|
+
if cur_resource && cur_resource.exist?
|
104
|
+
propstats(xml, get_properties(cur_resource, props))
|
105
|
+
else
|
106
|
+
xml.status "#{http_version} #{NotFound.status_line}"
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
module Carddav
|
3
|
+
|
4
|
+
class Resource < DAV4Rack::Resource
|
5
|
+
extend Helpers::Properties
|
6
|
+
|
7
|
+
CARDAV_NS = 'urn:ietf:params:xml:ns:carddav'.freeze
|
8
|
+
|
9
|
+
PRIVILEGES = %w(read read-acl read-current-user-privilege-set)
|
10
|
+
|
11
|
+
def initialize(*)
|
12
|
+
super
|
13
|
+
raise ArgumentError, "missing current_user lambda" unless options[:current_user]
|
14
|
+
end
|
15
|
+
|
16
|
+
def current_user
|
17
|
+
@current_user ||= options[:current_user].call(env)
|
18
|
+
end
|
19
|
+
|
20
|
+
def user_agent
|
21
|
+
env = options[:env]
|
22
|
+
if env
|
23
|
+
env['HTTP_USER_AGENT'] || ""
|
24
|
+
else
|
25
|
+
""
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def router_params
|
30
|
+
env['router.params'] || {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def setup
|
34
|
+
|
35
|
+
@propstat_relative_path = true
|
36
|
+
@root_xml_attributes = {
|
37
|
+
'xmlns:C' => CARDAV_NS,
|
38
|
+
'xmlns:APPLE1' => 'http://calendarserver.org/ns/'
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def is_self?(other_path)
|
43
|
+
ary = [@public_path]
|
44
|
+
ary.push(@public_path+'/') if @public_path[-1] != '/'
|
45
|
+
ary.push(@public_path[0..-2]) if @public_path[-1] == '/'
|
46
|
+
ary.include? other_path
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_property(element)
|
50
|
+
name = element[:name]
|
51
|
+
namespace = element[:ns_href]
|
52
|
+
|
53
|
+
key = "#{namespace}*#{name}"
|
54
|
+
|
55
|
+
handler = self.class.properties[key]
|
56
|
+
if handler
|
57
|
+
ret = instance_exec(element, &handler[0])
|
58
|
+
# TODO: find better than that
|
59
|
+
if ret.is_a?(String) && ret.include?('<')
|
60
|
+
Nokogiri::XML::DocumentFragment.parse(ret)
|
61
|
+
else
|
62
|
+
ret
|
63
|
+
end
|
64
|
+
else
|
65
|
+
Logger.debug("[#{self.class.name}] no handler for #{namespace}:#{name}")
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
define_properties('DAV:') do
|
71
|
+
|
72
|
+
property('current-user-privilege-set') do
|
73
|
+
<<-EOS
|
74
|
+
<D:current-user-privilege-set xmlns:D="DAV:">
|
75
|
+
#{get_privileges_aggregate}
|
76
|
+
</D:current-user-privilege-set>
|
77
|
+
EOS
|
78
|
+
end
|
79
|
+
|
80
|
+
property('group') do
|
81
|
+
""
|
82
|
+
end
|
83
|
+
|
84
|
+
property('owner') do
|
85
|
+
<<-EOS
|
86
|
+
<D:owner xmlns:D='DAV:'>
|
87
|
+
<D:href>#{root_uri_path}</D:href>
|
88
|
+
</D:owner>
|
89
|
+
EOS
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
def properties
|
95
|
+
selected_properties = self.class.properties.reject{|key, arr| arr[1] == true }
|
96
|
+
ret = {}
|
97
|
+
selected_properties.keys.map do |key|
|
98
|
+
ns, name = key.split('*')
|
99
|
+
{:name => name, :ns_href => ns}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def children
|
104
|
+
[]
|
105
|
+
end
|
106
|
+
|
107
|
+
|
108
|
+
private
|
109
|
+
def env
|
110
|
+
options[:env] || {}
|
111
|
+
end
|
112
|
+
|
113
|
+
def root_uri_path
|
114
|
+
tmp = @options[:root_uri_path]
|
115
|
+
tmp.respond_to?(:call) ? tmp.call(env) : tmp
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_privileges_aggregate
|
119
|
+
privileges_aggregate = PRIVILEGES.inject('') do |ret, priv|
|
120
|
+
ret << '<D:privilege><%s /></privilege>' % priv
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def add_slashes(str)
|
125
|
+
"/#{str}/".squeeze('/')
|
126
|
+
end
|
127
|
+
|
128
|
+
def child(child_class, child, parent = nil)
|
129
|
+
new_public = add_slashes(public_path)
|
130
|
+
new_path = add_slashes(path)
|
131
|
+
|
132
|
+
child_class.new("#{new_public}#{child.path}", "#{new_path}#{child.path}",
|
133
|
+
request, response, options.merge(_object_: child, _parent_: self)
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
module Carddav
|
3
|
+
|
4
|
+
class AddressbookCollectionResource < Resource
|
5
|
+
|
6
|
+
def exist?
|
7
|
+
return true
|
8
|
+
end
|
9
|
+
|
10
|
+
def collection?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def children
|
15
|
+
current_user.all_addressbooks.map do |book|
|
16
|
+
child(AddressbookResource, book)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
module Carddav
|
3
|
+
|
4
|
+
class AddressbookResource < Resource
|
5
|
+
|
6
|
+
define_properties('DAV:') do
|
7
|
+
property('current-user-privilege-set') do
|
8
|
+
privileges = %w(read write write-properties write-content read-acl read-current-user-privilege-set)
|
9
|
+
s='<D:current-user-privilege-set xmlns:D="DAV:">%s</D:current-user-privilege-set>'
|
10
|
+
|
11
|
+
privileges_aggregate = privileges.inject('') do |ret, priv|
|
12
|
+
ret << '<D:privilege><%s /></privilege>' % priv
|
13
|
+
end
|
14
|
+
|
15
|
+
s % privileges_aggregate
|
16
|
+
end
|
17
|
+
|
18
|
+
property('supported-report-set') do
|
19
|
+
reports = %w(addressbook-multiget)
|
20
|
+
s = "<supported-report-set>%s</supported-report-set>"
|
21
|
+
|
22
|
+
reports_aggregate = reports.inject('') do |ret, report|
|
23
|
+
ret << "<report><C:%s xmlns:C='#{CARDAV_NS}'/></report>" % report
|
24
|
+
end
|
25
|
+
|
26
|
+
s % reports_aggregate
|
27
|
+
end
|
28
|
+
|
29
|
+
property('resourcetype') do
|
30
|
+
<<-EOS
|
31
|
+
<resourcetype>
|
32
|
+
<D:collection />
|
33
|
+
<C:addressbook xmlns:C="#{CARDAV_NS}"/>
|
34
|
+
</resourcetype>
|
35
|
+
EOS
|
36
|
+
end
|
37
|
+
|
38
|
+
property('displayname') do
|
39
|
+
@address_book.name
|
40
|
+
end
|
41
|
+
|
42
|
+
property('creationdate') do
|
43
|
+
@address_book.created_at
|
44
|
+
end
|
45
|
+
|
46
|
+
property('getcontenttype') do
|
47
|
+
"httpd/unix-directory"
|
48
|
+
end
|
49
|
+
|
50
|
+
# property('getetag') do
|
51
|
+
# '"None"'
|
52
|
+
# end
|
53
|
+
|
54
|
+
property('getlastmodified') do
|
55
|
+
@address_book.updated_at
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
define_properties(CARDAV_NS) do
|
62
|
+
explicit do
|
63
|
+
property('max-resource-size') do
|
64
|
+
1024
|
65
|
+
end
|
66
|
+
|
67
|
+
property('supported-address-data') do
|
68
|
+
<<-EOS
|
69
|
+
<C:supported-address-data xmlns:C='#{CARDAV_NS}'>
|
70
|
+
<C:address-data-type content-type='text/vcard' version='3.0' />
|
71
|
+
</C:supported-address-data>
|
72
|
+
EOS
|
73
|
+
end
|
74
|
+
|
75
|
+
property('addressbook-description') do
|
76
|
+
@address_book.name
|
77
|
+
end
|
78
|
+
|
79
|
+
property('max-resource-size') do
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
# TODO: fill this
|
84
|
+
property('supported-collation-set') do
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
define_properties('http://calendarserver.org/ns/') do
|
94
|
+
property('getctag') do
|
95
|
+
<<-EOS
|
96
|
+
<APPLE1:getctag xmlns:APPLE1='http://calendarserver.org/ns/'>
|
97
|
+
#{@address_book.updated_at.to_i}
|
98
|
+
</APPLE1:getctag>
|
99
|
+
EOS
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup
|
104
|
+
super
|
105
|
+
@address_book = @options[:_object_] || current_user.current_addressbook()
|
106
|
+
end
|
107
|
+
|
108
|
+
def exist?
|
109
|
+
@address_book != nil
|
110
|
+
end
|
111
|
+
|
112
|
+
def collection?
|
113
|
+
true
|
114
|
+
end
|
115
|
+
|
116
|
+
def children
|
117
|
+
Logger.debug "ABR::children(#{public_path})"
|
118
|
+
@address_book.contacts.collect do |c|
|
119
|
+
Logger.debug "Trying to create this child (contact): #{c.uid.to_s}"
|
120
|
+
child(ContactResource, c, @address_book)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def find_child(uid)
|
125
|
+
uid = File.basename(uid, '.vcf')
|
126
|
+
c = @address_book.find_contact(uid)
|
127
|
+
if c
|
128
|
+
child(ContactResource, c)
|
129
|
+
else
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|