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,132 @@
|
|
1
|
+
require 'vcard_parser'
|
2
|
+
|
3
|
+
module DAV4Rack
|
4
|
+
module Carddav
|
5
|
+
|
6
|
+
class ContactResource < Resource
|
7
|
+
|
8
|
+
define_properties('DAV:') do
|
9
|
+
property('getetag') do
|
10
|
+
@contact.etag
|
11
|
+
end
|
12
|
+
|
13
|
+
property('creationdate') do
|
14
|
+
@contact.created_at
|
15
|
+
end
|
16
|
+
|
17
|
+
property('getcontentlength') do
|
18
|
+
@contact.vcard.to_s.size
|
19
|
+
end
|
20
|
+
|
21
|
+
property('getcontenttype') do
|
22
|
+
"text/vcard"
|
23
|
+
end
|
24
|
+
|
25
|
+
property('getlastmodified') do
|
26
|
+
@contact.updated_at
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
define_properties(CARDAV_NS) do
|
31
|
+
explicit do
|
32
|
+
property('address-data') do |el|
|
33
|
+
|
34
|
+
fields = el[:children].select{|e| e[:name] == 'prop' }.map{|e| e[:attributes]['name'] }
|
35
|
+
data = @contact.vcard.to_s(fields)
|
36
|
+
|
37
|
+
<<-EOS
|
38
|
+
<C:address-data xmlns:C="#{CARDAV_NS}">
|
39
|
+
<![CDATA[#{data}]]>
|
40
|
+
</C:address-data>
|
41
|
+
EOS
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
def collection?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
|
52
|
+
def exist?
|
53
|
+
Logger.info "ContactR::exist?(#{public_path});"
|
54
|
+
@contact != nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup
|
58
|
+
super
|
59
|
+
|
60
|
+
@address_book = @options[:_parent_] || current_user.current_addressbook()
|
61
|
+
@contact = @options[:_object_] || current_user.current_contact()
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
def put(request, response)
|
66
|
+
b = request.body.read
|
67
|
+
|
68
|
+
# Ensure we only have one vcard per request
|
69
|
+
# Section 5.1:
|
70
|
+
# Address object resources contained in address book collections MUST
|
71
|
+
# contain a single vCard component only.
|
72
|
+
vcards = VCardParser::VCard.parse(b)
|
73
|
+
raise BadRequest if vcards.size != 1
|
74
|
+
vcf = vcards[0]
|
75
|
+
|
76
|
+
uid = vcf['UID'].value
|
77
|
+
|
78
|
+
# [6.3.2] Check for If-None-Match: *
|
79
|
+
# If set, client does want to create a new contact only, raise an error
|
80
|
+
# if contact already exists
|
81
|
+
want_new_contact = (request.env['HTTP_IF_NONE_MATCH'] == '*')
|
82
|
+
|
83
|
+
@contact = @address_book.find_contact(uid)
|
84
|
+
|
85
|
+
# If the client has explicitly stated they want a new contact
|
86
|
+
raise Conflict if (want_new_contact and @contact)
|
87
|
+
|
88
|
+
if @contact
|
89
|
+
Logger.debug("Updating contact #{uid} (#{@contact.object_id})")
|
90
|
+
else
|
91
|
+
Logger.debug("Creating new contact ! (#{uid})")
|
92
|
+
@contact = @address_book.create_contact(uid)
|
93
|
+
end
|
94
|
+
|
95
|
+
@contact.update_from_vcard(vcf)
|
96
|
+
|
97
|
+
if @contact.save
|
98
|
+
@public_path = File.join(@address_book.path, @contact.uid)
|
99
|
+
response['ETag'] = @contact.etag
|
100
|
+
Created
|
101
|
+
else
|
102
|
+
# Is another error more appropriate?
|
103
|
+
raise Conflict
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def parent
|
108
|
+
@address_book
|
109
|
+
end
|
110
|
+
|
111
|
+
def parent_exists?
|
112
|
+
@address_book != nil
|
113
|
+
end
|
114
|
+
|
115
|
+
def parent_collection?
|
116
|
+
true
|
117
|
+
end
|
118
|
+
|
119
|
+
def get(request, response)
|
120
|
+
response.headers['Etag'] = @contact.etag
|
121
|
+
response.body = @contact.vcard.vcard
|
122
|
+
end
|
123
|
+
|
124
|
+
def delete
|
125
|
+
@contact.destroy
|
126
|
+
NoContent
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
module DAV4Rack
|
2
|
+
module Carddav
|
3
|
+
|
4
|
+
class PrincipalResource < Resource
|
5
|
+
|
6
|
+
def exist?
|
7
|
+
ret = (path == '') || (path == '/')
|
8
|
+
return ret
|
9
|
+
end
|
10
|
+
|
11
|
+
def collection?
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
define_properties('DAV:') do
|
17
|
+
property('alternate-URI-set') do
|
18
|
+
# "<D:alternate-URI-set xmlns:D='DAV:' />"
|
19
|
+
end
|
20
|
+
|
21
|
+
property('group-membership') do
|
22
|
+
# "<D:group-membership xmlns:D='DAV:' />"
|
23
|
+
end
|
24
|
+
|
25
|
+
property('group-membership-set') do
|
26
|
+
# "<D:group-membership-set xmlns:D='DAV:' />"
|
27
|
+
end
|
28
|
+
|
29
|
+
# iOS 6.0 expect "/
|
30
|
+
# UA: iOS/6.0 (10A403) Preferences/1.0
|
31
|
+
property('principal-URL') do
|
32
|
+
<<-EOS
|
33
|
+
<D:principal-URL xmlns:D='DAV:'>
|
34
|
+
<D:href>#{principal_uri}</D:href>
|
35
|
+
</D:principal-URL>
|
36
|
+
EOS
|
37
|
+
end
|
38
|
+
|
39
|
+
# This violates the spec that requires an HTTP or HTTPS URL. Unfortunately,
|
40
|
+
# Apple's AddressBook.app treats everything as a pathname. Also, the model
|
41
|
+
# shouldn't need to know about the URL scheme and such.
|
42
|
+
# iOS 6.0 expect /
|
43
|
+
property('current-user-principal') do
|
44
|
+
<<-EOS
|
45
|
+
<D:current-user-principal xmlns:D='DAV:'>
|
46
|
+
<D:href>#{root_uri_path}</D:href>
|
47
|
+
</D:current-user-principal>
|
48
|
+
EOS
|
49
|
+
end
|
50
|
+
|
51
|
+
property('acl') do
|
52
|
+
<<-EOS
|
53
|
+
<D:acl xmlns:D='DAV:'>
|
54
|
+
<D:ace>
|
55
|
+
<D:principal>
|
56
|
+
<D:href>#{root_uri_path}</D:href>
|
57
|
+
</D:principal>
|
58
|
+
<D:protected/>
|
59
|
+
<D:grant>
|
60
|
+
#{get_privileges_aggregate}
|
61
|
+
</D:grant>
|
62
|
+
</D:ace>
|
63
|
+
</D:acl>
|
64
|
+
EOS
|
65
|
+
end
|
66
|
+
|
67
|
+
property('acl-restrictions') do
|
68
|
+
<<-EOS
|
69
|
+
<D:acl-restrictions xmlns:D='DAV:'>
|
70
|
+
<D:grant-only/><D:no-invert/>
|
71
|
+
</D:acl-restrictions>
|
72
|
+
EOS
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
property('resourcetype') do
|
77
|
+
<<-EOS
|
78
|
+
<resourcetype>
|
79
|
+
<D:collection />
|
80
|
+
<D:principal />
|
81
|
+
</resourcetype>
|
82
|
+
EOS
|
83
|
+
end
|
84
|
+
|
85
|
+
property('displayname') do
|
86
|
+
"User Principal Resource"
|
87
|
+
end
|
88
|
+
|
89
|
+
property('creationdate') do
|
90
|
+
current_user.created_at
|
91
|
+
end
|
92
|
+
|
93
|
+
property('getlastmodified') do
|
94
|
+
current_user.updated_at
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
define_properties(CARDAV_NS) do
|
99
|
+
explicit do
|
100
|
+
property('addressbook-home-set') do
|
101
|
+
<<-EOS
|
102
|
+
<C:addressbook-home-set xmlns:C='#{CARDAV_NS}'>
|
103
|
+
<D:href xmlns:D='DAV:'>#{books_collection_url}</D:href>
|
104
|
+
</C:addressbook-home-set>
|
105
|
+
EOS
|
106
|
+
end
|
107
|
+
|
108
|
+
# TODO: should return the user's card url
|
109
|
+
# (ex: /users/schmurfy.vcf ) (RFC 7.1.2)
|
110
|
+
property('principal-address') do
|
111
|
+
""
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
def principal_uri
|
119
|
+
if user_agent.start_with?("iOS/6.0")
|
120
|
+
'/'
|
121
|
+
else
|
122
|
+
root_uri_path
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def books_collection_url
|
127
|
+
File.join(root_uri_path, options[:books_collection])
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'dav4rack'
|
2
|
+
|
3
|
+
require_relative 'handler'
|
4
|
+
require_relative 'helpers/properties'
|
5
|
+
|
6
|
+
require_relative 'carddav/controller'
|
7
|
+
require_relative 'carddav/resource'
|
8
|
+
require_relative 'carddav/resources/principal_resource'
|
9
|
+
require_relative 'carddav/resources/addressbook_collection_resource'
|
10
|
+
require_relative 'carddav/resources/addressbook_resource'
|
11
|
+
require_relative 'carddav/resources/contact_resource'
|
12
|
+
|
13
|
+
require_relative 'carddav/app'
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'dav4rack/http_status'
|
2
|
+
|
3
|
+
module DAV4RackExt
|
4
|
+
|
5
|
+
class Handler
|
6
|
+
# include DAV4Rack::HTTPStatus
|
7
|
+
|
8
|
+
def initialize(options= {})
|
9
|
+
@options = options.dup
|
10
|
+
@logger = options.delete(:logger)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
begin
|
15
|
+
request = Rack::Request.new(env)
|
16
|
+
response = Rack::Response.new
|
17
|
+
|
18
|
+
controller = nil
|
19
|
+
begin
|
20
|
+
|
21
|
+
controller_class = @options[:controller_class] || DAV4Rack::Controller
|
22
|
+
controller = controller_class.new(request, response, @options.dup, env)
|
23
|
+
res = controller.send(request.request_method.downcase)
|
24
|
+
response.status = res.code if res.respond_to?(:code)
|
25
|
+
|
26
|
+
rescue DAV4Rack::HTTPStatus::Status => status
|
27
|
+
response.status = status.code
|
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
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,55 @@
|
|
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)
|
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
|
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
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
obj.instance_eval(&block)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
data/lib/dav4rack_ext.rb
ADDED
data/specs/factories.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
|
3
|
+
factory(:contact, :class => Testing::Contact) do
|
4
|
+
created_at Time.now
|
5
|
+
updated_at Time.now
|
6
|
+
end
|
7
|
+
|
8
|
+
factory(:user, :class => Testing::User) do
|
9
|
+
created_at Time.now
|
10
|
+
updated_at Time.now
|
11
|
+
|
12
|
+
initialize_with { new(env) }
|
13
|
+
end
|
14
|
+
|
15
|
+
factory(:book, :class => Testing::AddressBook) do
|
16
|
+
created_at Time.now
|
17
|
+
updated_at Time.now
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.expand_path('../../spec_helper', __FILE__)
|
2
|
+
|
3
|
+
describe 'RFC 3744: WebDav Access Control Protocol' do
|
4
|
+
before do
|
5
|
+
@dav_ns = "DAV:"
|
6
|
+
|
7
|
+
@user = user = stub('User', username: 'john')
|
8
|
+
|
9
|
+
@root_path = root_path = '/'
|
10
|
+
|
11
|
+
app = Rack::Builder.new do
|
12
|
+
# use XMLSniffer
|
13
|
+
run DAV4Rack::Carddav.app(root_path, current_user: user)
|
14
|
+
end
|
15
|
+
|
16
|
+
serve_app(app)
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '[4] Principal Properties' do
|
20
|
+
it '[4.2] DAV:principal-URL' do
|
21
|
+
response = propfind(@root_path, [
|
22
|
+
['principal-URL', @dav_ns]
|
23
|
+
])
|
24
|
+
|
25
|
+
ensure_element_exists(response, %{D|prop > D|principal-URL > D|href[text()="#{@root_path}"]})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '[5] Access Control Properties' do
|
30
|
+
|
31
|
+
describe '[5.5] DAV::ACL Element' do
|
32
|
+
it '[5.5.1] ACE Principal' do
|
33
|
+
response = propfind(@root_path, [
|
34
|
+
['acl', @dav_ns]
|
35
|
+
])
|
36
|
+
|
37
|
+
# check that there is one principal which is the root
|
38
|
+
ensure_element_exists(response, %{D|prop > D|acl > D|ace D|principal D|href[text()="#{@root_path}"]})
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
it '[5.1] DAV:owner' do
|
44
|
+
response = propfind(@root_path, [
|
45
|
+
['owner', @dav_ns]
|
46
|
+
])
|
47
|
+
|
48
|
+
ensure_element_exists(response, %{D|prop > D|owner > D|href[text()="#{@root_path}"]})
|
49
|
+
end
|
50
|
+
|
51
|
+
it '5.2] DAV:group' do
|
52
|
+
response = propfind(@root_path, [
|
53
|
+
['group', @dav_ns]
|
54
|
+
])
|
55
|
+
|
56
|
+
elements = ensure_element_exists(response, %{D|prop > D|group})
|
57
|
+
elements[0].text.should == ""
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|