dav4rack_ext 0.0.2

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