dav4rack 0.2.11 → 0.3.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.
@@ -0,0 +1,109 @@
1
+ === v0.3.0
2
+ * New minor release towards full litmus pass
3
+ * Litmus passing: basic, copymove, props
4
+ * Litmus in progress: locks
5
+ * Internal reorganization of file structures
6
+ * Propfind updates to provide proper returns (thanks {schmurfy}[https://github.com/schmurfy])
7
+ * Properly detect malformed propfind requests
8
+ * Updated custom attribute storage
9
+ * Allow customized DAV headers (thanks {schmurfy}[https://github.com/schmurfy])
10
+ * Extract parent collection detection to resource (thanks {schmurfy}[https://github.com/schmurfy])
11
+ * Start of new locking implementation
12
+ * Fix rack #serving usage in DAV4Rack::File (thanks {pifleo}[https://github.com/pifleo])
13
+ * Force file namespacing in file resource to prevent conflicts with DAV4Rack::File (thanks {pifleo}[https://github.com/pifleo])
14
+ * Add ability to build xml from within a resource (thanks {mlmorg}[https://github.com/mlmorg])
15
+
16
+ === v0.2.11
17
+ * URL escaping updates (thanks {exabugs}[https://github.com/exabugs])
18
+ * Return status updates to match RFC (thanks {exabugs}[https://github.com/exabugs])
19
+ * Add option to provide httpdate formatted creation date to MS clients (thanks {doxavore}[https://github.com/doxavore])
20
+ * New MongoDB resource (thanks {exabugs}[https://github.com/exabugs])
21
+ * Controller subclass support (thanks {inferiorhumanorgans}[https://github.com/inferiorhumanorgans])
22
+ * Root XML attributes (thanks {inferiorhumanorgans}[https://github.com/inferiorhumanorgans])
23
+ * Allow propstat to return relative paths (apple carddav hack) (thanks {inferiorhumanorgans}[https://github.com/inferiorhumanorgans])
24
+
25
+ === v0.2.10
26
+ * Fix unicorn starting from exec script (thanks {spicyj}[https://github.com/spicyj])
27
+ * Return correct size using #bytesize instead of #size (thanks {TurchenkoAlex}[https://github.com/TurchenkoAlex])
28
+
29
+ === v0.2.9
30
+ * Be less restrictive of Nokogiri dependency
31
+
32
+ === v0.2.8
33
+ * Allow custom logger types to be used
34
+ * Allow resource to handle existence on locking (fixes issue {#21}[https://github.com/chrisroberts/dav4rack/issues/21] thanks {doxavore}[https://github.com/doxavore])
35
+ * Removed exception based control flow in favor of logic based control flow
36
+
37
+ === v0.2.7
38
+ * Include location content within PUT response body (fixes issue described in {#20}[https://github.com/chrisroberts/dav4rack/issues/20])
39
+
40
+ === v0.2.6
41
+ * Update response header from PUT to use Location
42
+
43
+ === v0.2.5
44
+ * Return Created response in favor of current multi status response on PUT (thanks {buffym}[https://github.com/buffym])
45
+ * Show class 1 compliance to be in accordance with WebDAV spec (thanks {buffym}[https://github.com/buffym])
46
+ * Adds setup method to skip alias list for resources (thanks {jbangert}[https://github.com/jbangert])
47
+ * Allow existing logger instance to be provided
48
+
49
+ === v0.2.4
50
+ * Return absolute URI from #mkcol and #put (thanks {teefax}[http://github.com/teefax])
51
+ * Nodes with text children properly serialized (pointed out by {jeffhos}[http://github.com/jeffhos])
52
+ * Fixed bug in file locking (pointed out by {clyfe}[http://github.com/clyfe] with fix provided by {teefax}[http://github.com/teefax])
53
+
54
+ === v0.2.3
55
+ * Completing missed step in last packaging
56
+
57
+ === v0.2.2
58
+ * Fix for port numbers in host (thanks {krug}[http://github.com/krug])
59
+
60
+ === v0.2.1
61
+ * Fix for better handling of MOVEs with badly encoded URLS
62
+
63
+ === v0.2.0
64
+ * Update to remote URL is passed to NGINX for proxying. Use headers instead of request
65
+
66
+ === v0.1.8
67
+ * Better exception handling for error logging
68
+ * Send overwrite flag to Resource#move
69
+
70
+ === v0.1.7
71
+ * Fix in interceptor to use correct File
72
+
73
+ === v0.1.6
74
+ * Add DAV4Rack::File that overloads just enough of Rack::File to allow explicit path setting
75
+
76
+ === v0.1.5
77
+ * Remove support for options[:delete_dotfiles]
78
+ * Allow HTTP methods to be ignored within interceptor
79
+ * Add owner information to lock response
80
+ * Initial update of spec to work with DAV4Rack
81
+ * Copy and delete recursively
82
+ * Add expected overwrite for copy/move on resource (thanks {clyfe}[http://github.com/clyfe])
83
+ * Add overwrite logic for copy/move on FileResource
84
+ * Removed callback authentication from FileResource (uses simple controller based auth)
85
+
86
+ === v0.1.4
87
+ * Fix for Rack::File issue (thanks {clyfe}[http://github.com/clyfe])
88
+ * Logging now optional on executable
89
+ * Include propstats even if empty (this resolves an issue in cyberduck not displaying files)
90
+
91
+ === v0.1.3
92
+ * Fix for Hash modification issues in Ruby 1.9.2 (thanks {antiloopgmbh}[http://github.com/antiloopgmbh])
93
+ * Fix executable to properly fallback
94
+ * Use callback authentication in FileResource to allow for no auth
95
+
96
+ === v0.1.2
97
+ * Add sendfile support (currently only tested on nginx)
98
+
99
+ === v0.1.1
100
+ * Add logging capability
101
+ * Simplify Interceptor mappings (provide options in hash instead of explicit :options)
102
+
103
+ === v0.1.0
104
+ * Callbacks available for resources
105
+ * RemoteFile more aligned with Rack::File
106
+ * Return multistatus responses PUT MKCOL and COPY/MOVE
107
+ * Executable now uses Unicorn, Mongrel and WEBrick in that order
108
+ * Simple resource locking enabled by default
109
+ * Updated FileResource to work properly with new architecture
@@ -286,6 +286,11 @@ Something special to notice in the last example is the DAV_ prefix on authentica
286
286
  any callbacks being applied to the given method. This allows us to provide a public method that the callback can access on the resource
287
287
  without getting stuck in a loop.
288
288
 
289
+ == Software using DAV4Rack!
290
+
291
+ * {meishi}[https://github.com/inferiorhumanorgans/meishi] - Lightweight CardDAV implementation in Rails
292
+ * {dav4rack_ext}[https://github.com/schmurfy/dav4rack_ext] - CardDAV extension. (CalDAV planned)
293
+
289
294
  == Issues/Bugs/Questions
290
295
 
291
296
  === Known Issues
@@ -313,6 +318,9 @@ A big thanks to everyone contributing to help make this project better.
313
318
  * {TurchenkoAlex}[https://github.com/TurchenkoAlex]
314
319
  * {exabugs}[https://github.com/exabugs]
315
320
  * {inferiorhumanorgans}[https://github.com/inferiorhumanorgans]
321
+ * {schmurfy}[https://github.com/schmurfy]
322
+ * {pifleo}[https://github.com/pifleo]
323
+ * {mlmorg}[https://github.com/mlmorg]
316
324
 
317
325
  == License
318
326
 
@@ -15,26 +15,5 @@ Gem::Specification.new do |s|
15
15
  s.add_dependency 'nokogiri', '>= 1.4.2'
16
16
  s.add_dependency 'uuidtools', '~> 2.1.1'
17
17
  s.add_dependency 'rack', '>= 1.1.0'
18
- s.files = %w{
19
- .gitignore
20
- LICENSE
21
- dav4rack.gemspec
22
- lib/dav4rack.rb
23
- lib/dav4rack/file_resource.rb
24
- lib/dav4rack/handler.rb
25
- lib/dav4rack/controller.rb
26
- lib/dav4rack/http_status.rb
27
- lib/dav4rack/resource.rb
28
- lib/dav4rack/interceptor.rb
29
- lib/dav4rack/interceptor_resource.rb
30
- lib/dav4rack/remote_file.rb
31
- lib/dav4rack/file.rb
32
- lib/dav4rack/lock.rb
33
- lib/dav4rack/lock_store.rb
34
- lib/dav4rack/logger.rb
35
- lib/dav4rack/version.rb
36
- bin/dav4rack
37
- spec/handler_spec.rb
38
- README.rdoc
39
- }
18
+ s.files = %w(dav4rack.gemspec README.rdoc CHANGELOG.rdoc LICENSE) + Dir.glob("{bin,lib}/**/*")
40
19
  end
@@ -3,7 +3,12 @@ require 'uri'
3
3
  require 'nokogiri'
4
4
 
5
5
  require 'rack'
6
+ require 'dav4rack/utils'
6
7
  require 'dav4rack/http_status'
7
8
  require 'dav4rack/resource'
8
9
  require 'dav4rack/handler'
9
10
  require 'dav4rack/controller'
11
+
12
+ module DAV4Rack
13
+ IS_18 = RUBY_VERSION[0,3] == '1.8'
14
+ end
@@ -4,6 +4,7 @@ module DAV4Rack
4
4
 
5
5
  class Controller
6
6
  include DAV4Rack::HTTPStatus
7
+ include DAV4Rack::Utils
7
8
 
8
9
  attr_reader :request, :response, :resource
9
10
 
@@ -17,7 +18,15 @@ module DAV4Rack
17
18
  @request = request
18
19
  @response = response
19
20
  @options = options
21
+
22
+ @dav_extensions = options.delete(:dav_extensions) || []
23
+ @always_include_dav_header = options.delete(:always_include_dav_header)
24
+
20
25
  @resource = resource_class.new(actual_path, implied_path, @request, @response, @options)
26
+
27
+ if(@always_include_dav_header)
28
+ add_dav_header
29
+ end
21
30
  end
22
31
 
23
32
  # s:: string
@@ -34,13 +43,20 @@ module DAV4Rack
34
43
  # Unescape URL string
35
44
  def url_unescape(s)
36
45
  URI.unescape(s)
37
- end
46
+ end
47
+
48
+ def add_dav_header
49
+ unless(response['Dav'])
50
+ dav_support = %w(1 2) + @dav_extensions
51
+ response['Dav'] = dav_support.join(', ')
52
+ end
53
+ end
38
54
 
39
55
  # Return response to OPTIONS
40
56
  def options
41
- response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
42
- response["Dav"] = "1, 2"
43
- response["Ms-Author-Via"] = "DAV"
57
+ add_dav_header
58
+ response['Allow'] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
59
+ response['Ms-Author-Via'] = 'DAV'
44
60
  OK
45
61
  end
46
62
 
@@ -76,10 +92,10 @@ module DAV4Rack
76
92
  def put
77
93
  if(resource.collection?)
78
94
  Forbidden
79
- elsif(!resource.parent_exists? || !resource.parent.collection?)
95
+ elsif(!resource.parent_exists? || !resource.parent_collection?)
80
96
  Conflict
81
97
  else
82
- resource.lock_check
98
+ resource.lock_check if resource.supports_locking?
83
99
  status = resource.put(request, response)
84
100
  response['Location'] = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
85
101
  response.body = response['Location']
@@ -95,7 +111,7 @@ module DAV4Rack
95
111
  # Return response to DELETE
96
112
  def delete
97
113
  if(resource.exist?)
98
- resource.lock_check
114
+ resource.lock_check if resource.supports_locking?
99
115
  resource.delete
100
116
  else
101
117
  NotFound
@@ -104,7 +120,7 @@ module DAV4Rack
104
120
 
105
121
  # Return response to MKCOL
106
122
  def mkcol
107
- resource.lock_check
123
+ resource.lock_check if resource.supports_locking?
108
124
  status = resource.make_collection
109
125
  gen_url = "#{scheme}://#{host}:#{port}#{url_format(resource)}" if status == Created
110
126
  if(resource.use_compat_mkcol_response?)
@@ -132,7 +148,7 @@ module DAV4Rack
132
148
  unless(resource.exist?)
133
149
  NotFound
134
150
  else
135
- resource.lock_check unless args.include?(:copy)
151
+ resource.lock_check if resource.supports_locking? && !args.include(:copy)
136
152
  destination = url_unescape(env['HTTP_DESTINATION'].sub(%r{https?://([^/]+)}, ''))
137
153
  dest_host = $1
138
154
  if(dest_host && dest_host.gsub(/:\d{2,5}$/, '') != request.host)
@@ -165,25 +181,33 @@ module DAV4Rack
165
181
  end
166
182
  end
167
183
 
168
- # Return respoonse to PROPFIND
184
+ # Return response to PROPFIND
169
185
  def propfind
170
186
  unless(resource.exist?)
171
187
  NotFound
172
188
  else
173
189
  unless(request_document.xpath("//#{ns}propfind/#{ns}allprop").empty?)
174
- names = resource.property_names
190
+ properties = resource.properties
175
191
  else
176
- names = (
177
- ns.empty? ? request_document.remove_namespaces! : request_document
178
- ).xpath(
179
- "//#{ns}propfind/#{ns}prop"
180
- ).children.find_all{ |item|
181
- item.element? && item.name.start_with?(ns)
182
- }.map{ |item|
183
- item.name.sub("#{ns}::", '')
184
- }
185
- raise BadRequest if names.empty?
186
- names = resource.property_names if names.empty?
192
+ check = request_document.xpath("//#{ns}propfind")
193
+ if(check && !check.empty?)
194
+ properties = request_document.xpath(
195
+ "//#{ns}propfind/#{ns}prop"
196
+ ).children.find_all{ |item|
197
+ item.element?
198
+ }.map{ |item|
199
+ # We should do this, but Nokogiri transforms prefix w/ null href into
200
+ # something valid. Oops.
201
+ # TODO: Hacky grep fix that's horrible
202
+ hsh = to_element_hash(item)
203
+ if(hsh.namespace.nil? && !ns.empty?)
204
+ raise BadRequest if request_document.to_s.scan(%r{<#{item.name}[^>]+xmlns=""}).empty?
205
+ end
206
+ hsh
207
+ }.compact
208
+ else
209
+ raise BadRequest
210
+ end
187
211
  end
188
212
  multistatus do |xml|
189
213
  find_resources.each do |resource|
@@ -193,7 +217,7 @@ module DAV4Rack
193
217
  else
194
218
  xml.href url_format(resource)
195
219
  end
196
- propstats(xml, get_properties(resource, names))
220
+ propstats(xml, get_properties(resource, properties.empty? ? resource.properties : properties))
197
221
  end
198
222
  end
199
223
  end
@@ -205,14 +229,32 @@ module DAV4Rack
205
229
  unless(resource.exist?)
206
230
  NotFound
207
231
  else
208
- resource.lock_check
209
- prop_rem = request_match('/propertyupdate/remove/prop').children.map{|n| [n.name] }
210
- prop_set = request_match('/propertyupdate/set/prop').children.map{|n| [n.name, n.text] }
232
+ resource.lock_check if resource.supports_locking?
233
+ prop_actions = []
234
+ request_document.xpath("/#{ns}propertyupdate").children.each do |element|
235
+ case element.name
236
+ when 'set', 'remove'
237
+ prp = element.children.detect{|e|e.name == 'prop'}
238
+ if(prp)
239
+ prp.children.each do |elm|
240
+ next if elm.name == 'text'
241
+ prop_actions << {:type => element.name, :name => to_element_hash(elm), :value => elm.text}
242
+ end
243
+ end
244
+ end
245
+ end
211
246
  multistatus do |xml|
212
247
  find_resources.each do |resource|
213
248
  xml.response do
214
249
  xml.href "#{scheme}://#{host}:#{port}#{url_format(resource)}"
215
- propstats(xml, set_properties(resource, prop_set))
250
+ prop_actions.each do |action|
251
+ case action[:type]
252
+ when 'set'
253
+ propstats(xml, set_properties(resource, action[:name] => action[:value]))
254
+ when 'remove'
255
+ rm_properties(resource, action[:name] => action[:value])
256
+ end
257
+ end
216
258
  end
217
259
  end
218
260
  end
@@ -260,6 +302,7 @@ module DAV4Rack
260
302
  end
261
303
  end
262
304
  end
305
+ response.headers['Lock-Token'] = locktoken
263
306
  response.status = resource.exist? ? OK : Created
264
307
  rescue LockFailure => e
265
308
  multistatus do |xml|
@@ -300,9 +343,6 @@ module DAV4Rack
300
343
  raise Unauthorized unless authed
301
344
  end
302
345
 
303
- # ************************************************************
304
- # private methods
305
-
306
346
  private
307
347
 
308
348
  # Request environment variables
@@ -402,22 +442,21 @@ module DAV4Rack
402
442
 
403
443
  # Namespace being used within XML document
404
444
  # TODO: Make this better
405
- def ns
445
+ def ns(wanted_uri="DAV:")
406
446
  _ns = ''
407
447
  if(request_document && request_document.root && request_document.root.namespace_definitions.size > 0)
408
- _ns = request_document.root.namespace_definitions.first.prefix.to_s
448
+ _ns = request_document.root.namespace_definitions.collect{|__ns| __ns if __ns.href == wanted_uri}.compact
449
+ if _ns.empty?
450
+ _ns = request_document.root.namespace_definitions.first.prefix.to_s if _ns.empty?
451
+ else
452
+ _ns = _ns.first
453
+ _ns = _ns.prefix.nil? ? 'xmlns' : _ns.prefix.to_s
454
+ end
409
455
  _ns += ':' unless _ns.empty?
410
456
  end
411
457
  _ns
412
458
  end
413
459
 
414
- # pattern:: XPath pattern
415
- # Search XML document for given XPath
416
- # TODO: Stripping namespaces not so great
417
- def request_match(pattern)
418
- request_document.remove_namespaces!.xpath(pattern, request_document.root.namespaces)
419
- end
420
-
421
460
  # root_type:: Root tag name
422
461
  # Render XML and set Rack::Response#body= to final XML
423
462
  def render_xml(root_type)
@@ -462,35 +501,44 @@ module DAV4Rack
462
501
  end
463
502
 
464
503
  # resource:: Resource
465
- # names:: Property names
504
+ # elements:: Property hashes (name, ns_href, children)
466
505
  # Returns array of property values for given names
467
- def get_properties(resource, names)
506
+ def get_properties(resource, elements)
468
507
  stats = Hash.new { |h, k| h[k] = [] }
469
- for name in names
508
+ for element in elements
470
509
  begin
471
- val = resource.get_property(name)
472
- stats[OK].push [name, val]
510
+ val = resource.get_property(element)
511
+ stats[OK] << [element, val]
473
512
  rescue Unauthorized => u
474
513
  raise u
475
514
  rescue Status
476
- stats[$!.class] << name
515
+ stats[$!.class] << element
477
516
  end
478
517
  end
479
518
  stats
480
519
  end
481
520
 
482
521
  # resource:: Resource
483
- # pairs:: name value pairs
522
+ # elements:: Property hashes (name, namespace, children)
523
+ # Removes the given properties from a resource
524
+ def rm_properties(resource, elements)
525
+ for element, value in elements
526
+ resource.remove_property(element)
527
+ end
528
+ end
529
+
530
+ # resource:: Resource
531
+ # elements:: Property hashes (name, namespace, children)
484
532
  # Sets the given properties
485
- def set_properties(resource, pairs)
533
+ def set_properties(resource, elements)
486
534
  stats = Hash.new { |h, k| h[k] = [] }
487
- for name, value in pairs
535
+ for element, value in elements
488
536
  begin
489
- stats[OK] << [name, resource.set_property(name, value)]
537
+ stats[OK] << [element, resource.set_property(element, value)]
490
538
  rescue Unauthorized => u
491
539
  raise u
492
540
  rescue Status
493
- stats[$!.class] << name
541
+ stats[$!.class] << element
494
542
  end
495
543
  end
496
544
  stats
@@ -504,20 +552,36 @@ module DAV4Rack
504
552
  for status, props in stats
505
553
  xml.propstat do
506
554
  xml.prop do
507
- for name, value in props
508
- if(value.is_a?(Nokogiri::XML::DocumentFragment))
509
- xml.__send__ :insert, value
510
- elsif(value.is_a?(Nokogiri::XML::Node))
511
- xml.send(name) do
512
- xml_convert(xml, value)
555
+ for element, value in props
556
+ defn = xml.doc.root.namespace_definitions.find{|ns_def| ns_def.href == element[:ns_href]}
557
+ if defn.nil?
558
+ if element[:ns_href] and not element[:ns_href].empty?
559
+ _ns = "unknown#{rand(65536)}"
560
+ xml.doc.root.add_namespace_definition(_ns, element[:ns_href])
561
+ else
562
+ _ns = nil
513
563
  end
564
+ else
565
+ # Unfortunately Nokogiri won't let the null href, non-null prefix happen
566
+ # So we can't properly handle that error.
567
+ _ns = element[:ns_href].nil? ? nil : defn.prefix
568
+ end
569
+ ns_xml = _ns.nil? ? xml : xml[_ns]
570
+ if (value.is_a?(Nokogiri::XML::Node)) or (value.is_a?(Nokogiri::XML::DocumentFragment))
571
+ xml.__send__ :insert, value
514
572
  elsif(value.is_a?(Symbol))
515
- xml.send(name) do
516
- xml.send(value)
573
+ ns_xml.send(element[:name]) do
574
+ ns_xml.send(value)
517
575
  end
518
576
  else
519
- xml.send(name, value)
577
+ ns_xml.send(element[:name], value) do |x|
578
+ # Make sure we return valid XML
579
+ x.parent.namespace = nil if _ns.nil?
580
+ end
520
581
  end
582
+
583
+ # This is gross, but make sure we set the current namespace back to DAV:
584
+ xml['D']
521
585
  end
522
586
  end
523
587
  xml.status "#{http_version} #{status.status_line}"