calligraphy 0.2.0
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.
- 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
|