calligraphy 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +64 -0
- data/lib/calligraphy.rb +58 -0
- data/lib/calligraphy/copy.rb +37 -0
- data/lib/calligraphy/delete.rb +17 -0
- data/lib/calligraphy/file_resource.rb +487 -0
- data/lib/calligraphy/get.rb +12 -0
- data/lib/calligraphy/lock.rb +52 -0
- data/lib/calligraphy/mkcol.rb +20 -0
- data/lib/calligraphy/move.rb +31 -0
- data/lib/calligraphy/propfind.rb +18 -0
- data/lib/calligraphy/proppatch.rb +20 -0
- data/lib/calligraphy/put.rb +12 -0
- data/lib/calligraphy/rails/mapper.rb +120 -0
- data/lib/calligraphy/rails/web_dav_requests_controller.rb +208 -0
- data/lib/calligraphy/resource.rb +93 -0
- data/lib/calligraphy/unlock.rb +15 -0
- data/lib/calligraphy/utils.rb +38 -0
- data/lib/calligraphy/version.rb +3 -0
- data/lib/calligraphy/web_dav_request.rb +31 -0
- data/lib/calligraphy/xml/builder.rb +147 -0
- data/lib/calligraphy/xml/namespace.rb +10 -0
- data/lib/calligraphy/xml/node.rb +23 -0
- data/lib/calligraphy/xml/utils.rb +18 -0
- data/spec/spec_helper.rb +46 -0
- metadata +97 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module Calligraphy
|
2
|
+
class Lock < WebDavRequest
|
3
|
+
include Calligraphy::XML::Utils
|
4
|
+
|
5
|
+
def request
|
6
|
+
if @resource.request_body.blank? && !@resource.locked_to_user?(@headers)
|
7
|
+
lock_properties = @resource.refresh_lock
|
8
|
+
elsif (@resource.locked? && @resource.lock_is_exclusive?) ||
|
9
|
+
(@resource.locked_to_user?(@headers) && !xml_contains_shared_lock?)
|
10
|
+
return :locked
|
11
|
+
else
|
12
|
+
resource_exists_beforehand = @resource.exists?
|
13
|
+
|
14
|
+
xml = xml_for body: body, node: 'lockinfo'
|
15
|
+
return :bad_request if xml == :bad_request
|
16
|
+
|
17
|
+
lock_properties = @resource.lock xml, @headers['Depth']
|
18
|
+
end
|
19
|
+
|
20
|
+
builder = xml_builder
|
21
|
+
xml_res = builder.lock_res lock_properties
|
22
|
+
|
23
|
+
lock_token = lock_properties[-1]
|
24
|
+
.select { |x| x.name == 'locktoken' }[0]
|
25
|
+
.children[0]
|
26
|
+
.text
|
27
|
+
|
28
|
+
response.headers['Lock-Token'] = "<#{lock_token}>"
|
29
|
+
set_xml_content_type
|
30
|
+
|
31
|
+
if resource_exists_beforehand
|
32
|
+
return :ok, xml_res
|
33
|
+
else
|
34
|
+
return :created, xml_res
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def xml_contains_shared_lock?
|
41
|
+
lock_type = nil
|
42
|
+
xml = xml_for body: body, node: 'lockinfo'
|
43
|
+
xml.each do |node|
|
44
|
+
next unless node.is_a? Nokogiri::XML::Element
|
45
|
+
|
46
|
+
lock_type = node.children[0].name if node.name == 'lockscope'
|
47
|
+
end
|
48
|
+
|
49
|
+
lock_type == 'shared'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Calligraphy
|
2
|
+
class Mkcol < WebDavRequest
|
3
|
+
def request
|
4
|
+
return :method_not_allowed if @resource.exists?
|
5
|
+
return :conflict unless @resource.ancestor_exist?
|
6
|
+
return :unsupported_media_type unless @resource.request_body.blank?
|
7
|
+
|
8
|
+
@resource.create_collection
|
9
|
+
set_content_location_header
|
10
|
+
|
11
|
+
return :created
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def set_content_location_header
|
17
|
+
@response.headers['Content-Location'] = @resource.full_request_path
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Calligraphy
|
2
|
+
class Move < Copy
|
3
|
+
def request
|
4
|
+
return :locked if @resource.locked_to_user? @headers
|
5
|
+
|
6
|
+
options = copy_move_options
|
7
|
+
|
8
|
+
if @resource.is_true? options[:overwrite]
|
9
|
+
to_path = options[:destination].tap { |s| s.slice! @resource.mount_point }
|
10
|
+
to_resource = @resource.class.new resource: to_path, req: @request, root_dir: @resource.root_dir
|
11
|
+
|
12
|
+
if to_resource.exists?
|
13
|
+
to_resource.delete_collection
|
14
|
+
to_resource_existed = true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
copy_status = super
|
19
|
+
return copy_status if [:precondition_failed, :conflict].include? copy_status
|
20
|
+
|
21
|
+
@resource.delete_collection
|
22
|
+
|
23
|
+
if copy_status == :created && to_resource_existed
|
24
|
+
return :no_content
|
25
|
+
else
|
26
|
+
response.headers['Location'] = options[:destination] if copy_status == :created
|
27
|
+
return copy_status
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Calligraphy
|
2
|
+
class Propfind < WebDavRequest
|
3
|
+
include Calligraphy::XML::Utils
|
4
|
+
|
5
|
+
def request
|
6
|
+
xml = xml_for body: body, node: 'propfind'
|
7
|
+
return :bad_request if xml == :bad_request
|
8
|
+
|
9
|
+
properties = @resource.propfind xml
|
10
|
+
|
11
|
+
builder = xml_builder
|
12
|
+
xml_res = builder.propfind_res @resource.full_request_path, properties
|
13
|
+
|
14
|
+
set_xml_content_type
|
15
|
+
return :multi_status, xml_res
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Calligraphy
|
2
|
+
class Proppatch < WebDavRequest
|
3
|
+
include Calligraphy::XML::Utils
|
4
|
+
|
5
|
+
def request
|
6
|
+
return :locked if @resource.locked_to_user? @headers
|
7
|
+
|
8
|
+
xml = xml_for body: body, node: 'propertyupdate'
|
9
|
+
return :bad_request if xml == :bad_request
|
10
|
+
|
11
|
+
actions = @resource.proppatch xml
|
12
|
+
|
13
|
+
builder = xml_builder
|
14
|
+
xml_res = builder.proppatch_res @resource.full_request_path, actions
|
15
|
+
|
16
|
+
set_xml_content_type
|
17
|
+
return :multi_status, xml_res
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module ActionDispatch::Routing
|
2
|
+
class Mapper
|
3
|
+
module HttpHelpers
|
4
|
+
def copy(*args, &block)
|
5
|
+
args = set_web_dav_args args
|
6
|
+
map_method :copy, args, &block
|
7
|
+
end
|
8
|
+
|
9
|
+
def head(*args, &block)
|
10
|
+
args = set_web_dav_args args
|
11
|
+
map_method :head, args, &block
|
12
|
+
end
|
13
|
+
|
14
|
+
def lock(*args, &block)
|
15
|
+
args = set_web_dav_args args
|
16
|
+
map_method :lock, args, &block
|
17
|
+
end
|
18
|
+
|
19
|
+
def mkcol(*args, &block)
|
20
|
+
args = set_web_dav_args args
|
21
|
+
map_method :mkcol, args, &block
|
22
|
+
end
|
23
|
+
|
24
|
+
def move(*args, &block)
|
25
|
+
args = set_web_dav_args args
|
26
|
+
map_method :move, args, &block
|
27
|
+
end
|
28
|
+
|
29
|
+
def options(*args, &block)
|
30
|
+
args = set_web_dav_args args
|
31
|
+
map_method :options, args, &block
|
32
|
+
end
|
33
|
+
|
34
|
+
def propfind(*args, &block)
|
35
|
+
args = set_web_dav_args args
|
36
|
+
map_method :propfind, args, &block
|
37
|
+
end
|
38
|
+
|
39
|
+
def proppatch(*args, &block)
|
40
|
+
args = set_web_dav_args args
|
41
|
+
map_method :proppatch, args, &block
|
42
|
+
end
|
43
|
+
|
44
|
+
def unlock(*args, &block)
|
45
|
+
args = set_web_dav_args args
|
46
|
+
map_method :unlock, args, &block
|
47
|
+
end
|
48
|
+
|
49
|
+
def web_dav_delete(*args, &block)
|
50
|
+
args = set_web_dav_args args
|
51
|
+
map_method :delete, args, &block
|
52
|
+
end
|
53
|
+
|
54
|
+
def web_dav_get(*args, &block)
|
55
|
+
args = set_web_dav_args args
|
56
|
+
map_method :get, args, &block
|
57
|
+
end
|
58
|
+
|
59
|
+
def web_dav_put(*args, &block)
|
60
|
+
args = set_web_dav_args args
|
61
|
+
map_method :put, args, &block
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def set_web_dav_args(args)
|
67
|
+
options = {}
|
68
|
+
options[:controller] = 'calligraphy/rails/web_dav_requests'
|
69
|
+
options[:action] = 'invoke_method'
|
70
|
+
[args[0], options]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module Resources
|
75
|
+
class Resource
|
76
|
+
def web_dav_actions
|
77
|
+
if @only
|
78
|
+
Array(@only).map(&:to_sym)
|
79
|
+
elsif @except
|
80
|
+
Calligraphy.web_dav_actions - Array(@except).map(&:to_sym)
|
81
|
+
else
|
82
|
+
Calligraphy.web_dav_actions
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def calligraphy_resource(*resources, &block)
|
88
|
+
options = resources.extract_options!.dup
|
89
|
+
|
90
|
+
if apply_common_behavior_for :calligraphy_resource, resources, options, &block
|
91
|
+
return self
|
92
|
+
end
|
93
|
+
|
94
|
+
with_scope_level(:resource) do
|
95
|
+
options = apply_action_options options
|
96
|
+
singleton_resoure = ActionDispatch::Routing::Mapper::SingletonResource
|
97
|
+
resource_scope(singleton_resoure.new resources.pop, api_only?, @scope[:shallow], options) do
|
98
|
+
yield if block_given?
|
99
|
+
|
100
|
+
concerns(options[:concerns]) if options[:concerns]
|
101
|
+
|
102
|
+
set_mappings_for_web_dav_resources
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def set_mappings_for_web_dav_resources
|
110
|
+
parent_resource.web_dav_actions.each do |action|
|
111
|
+
if [:get, :put, :delete].include? action
|
112
|
+
send "web_dav_#{action.to_s}", '*resource'
|
113
|
+
else
|
114
|
+
send action, '*resource'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
module Calligraphy::Rails
|
2
|
+
class WebDavRequestsController < ActionController::Base
|
3
|
+
before_action :verify_resource_scope
|
4
|
+
before_action :authenticate_with_digest_authentiation
|
5
|
+
before_action :set_resource
|
6
|
+
|
7
|
+
def invoke_method
|
8
|
+
method = request.request_method.downcase
|
9
|
+
|
10
|
+
if check_preconditions
|
11
|
+
if method == 'head'
|
12
|
+
status = get head: true
|
13
|
+
elsif Calligraphy.allowed_methods.include? method
|
14
|
+
set_resource_client_nonce(method) if Calligraphy.enable_digest_authentication
|
15
|
+
|
16
|
+
status, body = send method
|
17
|
+
else
|
18
|
+
status = :method_not_allowed
|
19
|
+
end
|
20
|
+
|
21
|
+
send_response status: status, body: body
|
22
|
+
else
|
23
|
+
send_response status: :precondition_failed
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def verify_resource_scope
|
30
|
+
head :forbidden if params[:resource].include? '..'
|
31
|
+
end
|
32
|
+
|
33
|
+
def authenticate_with_digest_authentiation
|
34
|
+
if Calligraphy.enable_digest_authentication
|
35
|
+
authenticate_or_request_with_http_digest do |username|
|
36
|
+
Calligraphy.digest_password_procedure.call(username)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_resource
|
42
|
+
resource_id = if params[:format]
|
43
|
+
[params[:resource], params[:format]].join '.'
|
44
|
+
else
|
45
|
+
params[:resource]
|
46
|
+
end
|
47
|
+
|
48
|
+
@resource_class = params[:resource_class]
|
49
|
+
@resource_root_path = params[:resource_root_path]
|
50
|
+
@resource = @resource_class.new resource: resource_id, req: request, root_dir: @resource_root_path
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_preconditions
|
54
|
+
return true unless request.headers['If'].present?
|
55
|
+
|
56
|
+
evaluate_if_header
|
57
|
+
end
|
58
|
+
|
59
|
+
def evaluate_if_header
|
60
|
+
conditions_met = false
|
61
|
+
|
62
|
+
condition_lists = get_if_conditions
|
63
|
+
condition_lists.each do |list|
|
64
|
+
conditions = parse_preconditions list
|
65
|
+
|
66
|
+
conditions_met = evaluate_preconditions conditions
|
67
|
+
break if conditions_met
|
68
|
+
end
|
69
|
+
|
70
|
+
conditions_met
|
71
|
+
end
|
72
|
+
|
73
|
+
def get_if_conditions
|
74
|
+
lists = if request.headers['If'][0] == '<'
|
75
|
+
request.headers['If'].split Calligraphy::TAGGED_LIST_REGEX
|
76
|
+
else
|
77
|
+
request.headers['If'].split Calligraphy::UNTAGGAGED_LIST_REGEX
|
78
|
+
end
|
79
|
+
|
80
|
+
lists
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_preconditions(list)
|
84
|
+
conditions = { dav_no_lock: nil, etag: nil, lock_token: nil, resource: nil }
|
85
|
+
|
86
|
+
conditions[:dav_no_lock] = if list =~ Calligraphy::DAV_NO_LOCK_REGEX
|
87
|
+
list =~ Calligraphy::DAV_NOT_NO_LOCK_REGEX ? nil : true
|
88
|
+
end
|
89
|
+
|
90
|
+
if list =~ Calligraphy::RESOURCE_REGEX
|
91
|
+
conditions[:resource] = list.scan(Calligraphy::RESOURCE_REGEX).flatten[0]
|
92
|
+
end
|
93
|
+
|
94
|
+
if list =~ Calligraphy::LOCK_TOKEN_REGEX
|
95
|
+
conditions[:lock_token] = list.scan(Calligraphy::LOCK_TOKEN_REGEX).flatten[0]
|
96
|
+
end
|
97
|
+
|
98
|
+
if list =~ Calligraphy::ETAG_IF_REGEX
|
99
|
+
conditions[:etag] = list.scan(Calligraphy::ETAG_IF_REGEX).flatten[0]
|
100
|
+
end
|
101
|
+
|
102
|
+
conditions
|
103
|
+
end
|
104
|
+
|
105
|
+
def evaluate_preconditions(conditions)
|
106
|
+
conditions_met = true
|
107
|
+
target = if conditions[:resource]
|
108
|
+
@resource_class.new(
|
109
|
+
resource: conditions[:resource],
|
110
|
+
mount: @resource.mount_point
|
111
|
+
)
|
112
|
+
else
|
113
|
+
@resource
|
114
|
+
end
|
115
|
+
|
116
|
+
if conditions[:lock_token]
|
117
|
+
if target.locked?
|
118
|
+
conditions_met = false unless target.lock_tokens&.include? conditions[:lock_token]
|
119
|
+
else
|
120
|
+
conditions_met = false if target.locked_to_user? request.headers
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
if conditions[:etag]
|
125
|
+
validators = [@resource.etag, ""]
|
126
|
+
conditions_met = false unless validate_etag validators, conditions[:etag]
|
127
|
+
end
|
128
|
+
|
129
|
+
conditions_met = false if conditions[:dav_no_lock]
|
130
|
+
conditions_met
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_etag(etag_validators, validate_against)
|
134
|
+
cache_key = ActiveSupport::Cache.expand_cache_key etag_validators
|
135
|
+
"W/\"#{Digest::MD5.hexdigest(cache_key)}\"" == validate_against
|
136
|
+
end
|
137
|
+
|
138
|
+
def web_dav_request
|
139
|
+
{ headers: request.headers, request: request, resource: @resource, response: response }
|
140
|
+
end
|
141
|
+
|
142
|
+
def options
|
143
|
+
response.headers['DAV'] = '1, 2, 3'
|
144
|
+
:ok
|
145
|
+
end
|
146
|
+
|
147
|
+
def get(head: false)
|
148
|
+
fresh_when(@resource, etag: @resource.etag) if @resource.readable?
|
149
|
+
|
150
|
+
Calligraphy::Get.new(web_dav_request).request(head: head)
|
151
|
+
end
|
152
|
+
|
153
|
+
def put
|
154
|
+
Calligraphy::Put.new(web_dav_request).request
|
155
|
+
end
|
156
|
+
|
157
|
+
def delete
|
158
|
+
Calligraphy::Delete.new(web_dav_request).request
|
159
|
+
end
|
160
|
+
|
161
|
+
def copy
|
162
|
+
Calligraphy::Copy.new(web_dav_request).request
|
163
|
+
end
|
164
|
+
|
165
|
+
def move
|
166
|
+
Calligraphy::Move.new(web_dav_request).request
|
167
|
+
end
|
168
|
+
|
169
|
+
def mkcol
|
170
|
+
Calligraphy::Mkcol.new(web_dav_request).request
|
171
|
+
end
|
172
|
+
|
173
|
+
def propfind
|
174
|
+
Calligraphy::Propfind.new(web_dav_request).request
|
175
|
+
end
|
176
|
+
|
177
|
+
def proppatch
|
178
|
+
Calligraphy::Proppatch.new(web_dav_request).request
|
179
|
+
end
|
180
|
+
|
181
|
+
def lock
|
182
|
+
Calligraphy::Lock.new(web_dav_request).request
|
183
|
+
end
|
184
|
+
|
185
|
+
def unlock
|
186
|
+
Calligraphy::Unlock.new(web_dav_request).request
|
187
|
+
end
|
188
|
+
|
189
|
+
def send_response(status:, body: nil)
|
190
|
+
if body.nil?
|
191
|
+
head status
|
192
|
+
else
|
193
|
+
render body: body, status: status
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def set_resource_client_nonce(method)
|
198
|
+
@resource.client_nonce = get_client_nonce
|
199
|
+
end
|
200
|
+
|
201
|
+
def get_client_nonce
|
202
|
+
auth_header = request.headers["HTTP_AUTHORIZATION"]
|
203
|
+
|
204
|
+
auth = ::ActionController::HttpAuthentication::Digest.decode_credentials auth_header
|
205
|
+
auth[:cnonce]
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|