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.
@@ -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