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,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
@@ -0,0 +1,3 @@
1
+ module Dav4rackExt
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'dav4rack_ext/version'
2
+
3
+ require_relative 'dav4rack_ext/carddav'
4
+ # require_relative 'dav4rack_ext/calddav'
@@ -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