dav4rack_ext 0.0.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.
- 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
|