rack-webdav 0.4.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,40 @@
1
+ module RackWebDAV
2
+ class Lock
3
+
4
+ def initialize(args={})
5
+ @args = args
6
+ @store = nil
7
+ @args[:created_at] = Time.now
8
+ @args[:updated_at] = Time.now
9
+ end
10
+
11
+ def store
12
+ @store
13
+ end
14
+
15
+ def store=(s)
16
+ raise TypeError.new 'Expecting LockStore' unless s.respond_to? :remove
17
+ @store = s
18
+ end
19
+
20
+ def destroy
21
+ if(@store)
22
+ @store.remove(self)
23
+ end
24
+ end
25
+
26
+ def remaining_timeout
27
+ @args[:timeout].to_i - (Time.now.to_i - @args[:created_at].to_i)
28
+ end
29
+
30
+ def method_missing(*args)
31
+ if(@args.has_key?(args.first.to_sym))
32
+ @args[args.first.to_sym]
33
+ elsif(args.first.to_s[-1,1] == '=')
34
+ @args[args.first.to_s[0, args.first.to_s.length - 1].to_sym] = args[1]
35
+ else
36
+ raise NoMethodError.new "Undefined method #{args.first} for #{self}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,61 @@
1
+ require 'rack-webdav/lock'
2
+ module RackWebDAV
3
+ class LockStore
4
+ class << self
5
+ def create
6
+ @locks_by_path = {}
7
+ @locks_by_token = {}
8
+ end
9
+ def add(lock)
10
+ @locks_by_path[lock.path] = lock
11
+ @locks_by_token[lock.token] = lock
12
+ end
13
+
14
+ def remove(lock)
15
+ @locks_by_path.delete(lock.path)
16
+ @locks_by_token.delete(lock.token)
17
+ end
18
+
19
+ def find_by_path(path)
20
+ @locks_by_path.map do |lpath, lock|
21
+ lpath == path && lock.remaining_timeout > 0 ? lock : nil
22
+ end.compact.first
23
+ end
24
+
25
+ def find_by_token(token)
26
+ @locks_by_token.map do |ltoken, lock|
27
+ ltoken == token && lock.remaining_timeout > 0 ? lock : nil
28
+ end.compact.first
29
+ end
30
+
31
+ def explicit_locks(path)
32
+ @locks_by_path.map do |lpath, lock|
33
+ lpath == path && lock.remaining_timeout > 0 ? lock : nil
34
+ end.compact
35
+ end
36
+
37
+ def implicit_locks(path)
38
+ @locks_by_path.map do |lpath, lock|
39
+ lpath =~ /^#{Regexp.escape(path)}/ && lock.remaining_timeout > 0 && lock.depth > 0 ? lock : nil
40
+ end.compact
41
+ end
42
+
43
+ def explicitly_locked?(path)
44
+ self.explicit_locks(path).size > 0
45
+ end
46
+
47
+ def implicitly_locked?(path)
48
+ self.implicit_locks(path).size > 0
49
+ end
50
+
51
+ def generate(path, user, token)
52
+ l = Lock.new(:path => path, :user => user, :token => token)
53
+ l.store = self
54
+ add(l)
55
+ l
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ RackWebDAV::LockStore.create
@@ -0,0 +1,30 @@
1
+ require 'logger'
2
+
3
+ module RackWebDAV
4
+ # This is a simple wrapper for the Logger class. It allows easy access
5
+ # to log messages from the library.
6
+ class Logger
7
+ class << self
8
+ # args:: Arguments for Logger -> [path, level] (level is optional) or a Logger instance
9
+ # Set the path to the log file.
10
+ def set(*args)
11
+ if(%w(info debug warn fatal).all?{|meth| args.first.respond_to?(meth)})
12
+ @@logger = args.first
13
+ elsif(args.first.respond_to?(:to_s) && !args.first.to_s.empty?)
14
+ @@logger = ::Logger.new(args.first.to_s, 'weekly')
15
+ elsif(args.first)
16
+ raise 'Invalid type specified for logger'
17
+ end
18
+ if(args.size > 1)
19
+ @@logger.level = args[1]
20
+ end
21
+ end
22
+
23
+ def method_missing(*args)
24
+ if(defined? @@logger)
25
+ @@logger.send *args
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,148 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'digest/sha1'
4
+ require 'rack/file'
5
+
6
+ module RackWebDAV
7
+
8
+ class RemoteFile < Rack::File
9
+
10
+ attr_accessor :path
11
+
12
+ alias :to_path :path
13
+
14
+ # path:: path to file (Actual path, preferably a URL since this is a *REMOTE* file)
15
+ # args:: Hash of arguments:
16
+ # :size -> Integer - number of bytes
17
+ # :mime_type -> String - mime type
18
+ # :last_modified -> String/Time - Time of last modification
19
+ # :sendfile -> True or String to define sendfile header variation
20
+ # :cache_directory -> Where to store cached files
21
+ # :cache_ref -> Reference to be used for cache file name (useful for changing URLs like S3)
22
+ # :sendfile_prefix -> String directory prefix. Eg: 'webdav' will result in: /wedav/#{path.sub('http://', '')}
23
+ # :sendfile_fail_gracefully -> Boolean if true will simply proxy if unable to determine proper sendfile
24
+ def initialize(path, args={})
25
+ @path = path
26
+ @args = args
27
+ @heads = {}
28
+ @cache_file = args[:cache_directory] ? cache_file_path : nil
29
+ @redefine_prefix = nil
30
+ if(@cache_file && File.exists?(@cache_file))
31
+ @root = ''
32
+ @path_info = @cache_file
33
+ @path = @path_info
34
+ elsif(args[:sendfile])
35
+ @redefine_prefix = 'sendfile'
36
+ @sendfile_header = args[:sendfile].is_a?(String) ? args[:sendfile] : nil
37
+ else
38
+ setup_remote
39
+ end
40
+ do_redefines(@redefine_prefix) if @redefine_prefix
41
+ end
42
+
43
+ # env:: Environment variable hash
44
+ # Process the call
45
+ def call(env)
46
+ serving(env)
47
+ end
48
+
49
+ # env:: Environment variable hash
50
+ # Return an empty result with the proper header information
51
+ def sendfile_serving(env)
52
+ header = @sendfile_header || env['sendfile.type'] || env['HTTP_X_SENDFILE_TYPE']
53
+ unless(header)
54
+ raise 'Failed to determine proper sendfile header value' unless @args[:sendfile_fail_gracefully]
55
+ setup_remote
56
+ do_redefines('remote')
57
+ call(env)
58
+ end
59
+ prefix = (@args[:sendfile_prefix] || env['HTTP_X_ACCEL_REMOTE_MAPPING']).to_s.sub(/^\//, '').sub(/\/$/, '')
60
+ [200, {
61
+ "Last-Modified" => last_modified,
62
+ "Content-Type" => content_type,
63
+ "Content-Length" => size,
64
+ "Redirect-URL" => @path,
65
+ "Redirect-Host" => @path.scan(%r{^https?://([^/\?]+)}).first.first,
66
+ header => "/#{prefix}"
67
+ },
68
+ ['']]
69
+ end
70
+
71
+ # env:: Environment variable hash
72
+ # Return self to be processed
73
+ def remote_serving(e)
74
+ [200, {
75
+ "Last-Modified" => last_modified,
76
+ "Content-Type" => content_type,
77
+ "Content-Length" => size
78
+ }, self]
79
+ end
80
+
81
+ # Get the remote file
82
+ def remote_each
83
+ if(@store)
84
+ yield @store
85
+ else
86
+ @con.request_get(@call_path) do |res|
87
+ res.read_body(@store) do |part|
88
+ @cf.write part if @cf
89
+ yield part
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Size based on remote headers or given size
96
+ def size
97
+ @heads['content-length'] || @size
98
+ end
99
+
100
+ private
101
+
102
+ # Content type based on provided or remote headers
103
+ def content_type
104
+ @mime_type || @heads['content-type']
105
+ end
106
+
107
+ # Last modified type based on provided, remote headers or current time
108
+ def last_modified
109
+ @heads['last-modified'] || @modified || Time.now.httpdate
110
+ end
111
+
112
+ # Builds the path for the cached file
113
+ def cache_file_path
114
+ raise IOError.new 'Write permission is required for cache directory' unless File.writable?(@args[:cache_directory])
115
+ "#{@args[:cache_directory]}/#{Digest::SHA1.hexdigest((@args[:cache_ref] || @path).to_s + size.to_s + last_modified.to_s)}.cache"
116
+ end
117
+
118
+ # prefix:: prefix of methods to be redefined
119
+ # Redefine methods to do what we want in the proper situation
120
+ def do_redefines(prefix)
121
+ self.public_methods.each do |method|
122
+ m = method.to_s.dup
123
+ next unless m.slice!(0, prefix.to_s.length + 1) == "#{prefix}_"
124
+ self.class.class_eval "undef :'#{m}'"
125
+ self.class.class_eval "alias :'#{m}' :'#{method}'"
126
+ end
127
+ end
128
+
129
+ # Sets up all the requirements for proxying a remote file
130
+ def setup_remote
131
+ if(@cache_file)
132
+ begin
133
+ @cf = File.open(@cache_file, 'w+')
134
+ rescue
135
+ @cf = nil
136
+ end
137
+ end
138
+ @uri = URI.parse(@path)
139
+ @con = Net::HTTP.new(@uri.host, @uri.port)
140
+ @call_path = @uri.path + (@uri.query ? "?#{@uri.query}" : '')
141
+ res = @con.request_get(@call_path)
142
+ @heads = res.to_hash
143
+ res.value
144
+ @store = nil
145
+ @redefine_prefix = 'remote'
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,485 @@
1
+ require 'uuidtools'
2
+ require 'rack-webdav/http_status'
3
+
4
+ module RackWebDAV
5
+
6
+ class LockFailure < RuntimeError
7
+ attr_reader :path_status
8
+ def initialize(*args)
9
+ super(*args)
10
+ @path_status = {}
11
+ end
12
+
13
+ def add_failure(path, status)
14
+ @path_status[path] = status
15
+ end
16
+ end
17
+
18
+ class Resource
19
+ attr_reader :path, :options, :public_path, :request,
20
+ :response, :propstat_relative_path, :root_xml_attributes
21
+ attr_accessor :user
22
+ @@blocks = {}
23
+
24
+ class << self
25
+
26
+ # This lets us define a bunch of before and after blocks that are
27
+ # either called before all methods on the resource, or only specific
28
+ # methods on the resource
29
+ def method_missing(*args, &block)
30
+ class_sym = self.name.to_sym
31
+ @@blocks[class_sym] ||= {:before => {}, :after => {}}
32
+ m = args.shift
33
+ parts = m.to_s.split('_')
34
+ type = parts.shift.to_s.to_sym
35
+ method = parts.empty? ? nil : parts.join('_').to_sym
36
+ if(@@blocks[class_sym][type] && block_given?)
37
+ if(method)
38
+ @@blocks[class_sym][type][method] ||= []
39
+ @@blocks[class_sym][type][method] << block
40
+ else
41
+ @@blocks[class_sym][type][:'__all__'] ||= []
42
+ @@blocks[class_sym][type][:'__all__'] << block
43
+ end
44
+ else
45
+ raise NoMethodError.new("Undefined method #{m} for class #{self}")
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ include RackWebDAV::HTTPStatus
52
+
53
+ # public_path:: Path received via request
54
+ # path:: Internal resource path (Only different from public path when using root_uri's for webdav)
55
+ # request:: Rack::Request
56
+ # options:: Any options provided for this resource
57
+ # Creates a new instance of the resource.
58
+ # NOTE: path and public_path will only differ if the root_uri has been set for the resource. The
59
+ # controller will strip out the starting path so the resource can easily determine what
60
+ # it is working on. For example:
61
+ # request -> /my/webdav/directory/actual/path
62
+ # public_path -> /my/webdav/directory/actual/path
63
+ # path -> /actual/path
64
+ # NOTE: Customized Resources should not use initialize for setup. Instead
65
+ # use the #setup method
66
+ def initialize(public_path, path, request, response, options)
67
+ @skip_alias = [
68
+ :authenticate, :authentication_error_msg,
69
+ :authentication_realm, :path, :options,
70
+ :public_path, :request, :response, :user,
71
+ :user=, :setup
72
+ ]
73
+ @public_path = public_path.dup
74
+ @path = path.dup
75
+ @propstat_relative_path = !!options.delete(:propstat_relative_path)
76
+ @root_xml_attributes = options.delete(:root_xml_attributes) || {}
77
+ @request = request
78
+ @response = response
79
+ unless(options.has_key?(:lock_class))
80
+ require 'rack-webdav/lock_store'
81
+ @lock_class = LockStore
82
+ else
83
+ @lock_class = options[:lock_class]
84
+ raise NameError.new("Unknown lock type constant provided: #{@lock_class}") unless @lock_class.nil? || defined?(@lock_class)
85
+ end
86
+ @options = options.dup
87
+ @max_timeout = options[:max_timeout] || 86400
88
+ @default_timeout = options[:default_timeout] || 60
89
+ @user = @options[:user] || request.ip
90
+ setup if respond_to?(:setup)
91
+ public_methods(false).each do |method|
92
+ next if @skip_alias.include?(method.to_sym) || method[0,4] == 'DAV_' || method[0,5] == '_DAV_'
93
+ self.class.class_eval "alias :'_DAV_#{method}' :'#{method}'"
94
+ self.class.class_eval "undef :'#{method}'"
95
+ end
96
+ @runner = lambda do |class_sym, kind, method_name|
97
+ [:'__all__', method_name.to_sym].each do |sym|
98
+ if(@@blocks[class_sym] && @@blocks[class_sym][kind] && @@blocks[class_sym][kind][sym])
99
+ @@blocks[class_sym][kind][sym].each do |b|
100
+ args = [self, sym == :'__all__' ? method_name : nil].compact
101
+ b.call(*args)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # This allows us to call before and after blocks
109
+ def method_missing(*args)
110
+ result = nil
111
+ orig = args.shift
112
+ class_sym = self.class.name.to_sym
113
+ m = orig.to_s[0,5] == '_DAV_' ? orig : "_DAV_#{orig}" # If hell is doing the same thing over and over and expecting a different result this is a hell preventer
114
+ raise NoMethodError.new("Undefined method: #{orig} for class #{self}.") unless respond_to?(m)
115
+ @runner.call(class_sym, :before, orig)
116
+ result = send m, *args
117
+ @runner.call(class_sym, :after, orig)
118
+ result
119
+ end
120
+
121
+ # Returns if resource supports locking
122
+ def supports_locking?
123
+ false #true
124
+ end
125
+
126
+ # If this is a collection, return the child resources.
127
+ def children
128
+ NotImplemented
129
+ end
130
+
131
+ # Is this resource a collection?
132
+ def collection?
133
+ NotImplemented
134
+ end
135
+
136
+ # Does this resource exist?
137
+ def exist?
138
+ NotImplemented
139
+ end
140
+
141
+ # Does the parent resource exist?
142
+ def parent_exists?
143
+ parent.exist?
144
+ end
145
+
146
+ # Is the parent resource a collection?
147
+ def parent_collection?
148
+ parent.collection?
149
+ end
150
+
151
+ # Return the creation time.
152
+ def creation_date
153
+ raise NotImplemented
154
+ end
155
+
156
+ # Return the time of last modification.
157
+ def last_modified
158
+ raise NotImplemented
159
+ end
160
+
161
+ # Set the time of last modification.
162
+ def last_modified=(time)
163
+ # Is this correct?
164
+ raise NotImplemented
165
+ end
166
+
167
+ # Return an Etag, an unique hash value for this resource.
168
+ def etag
169
+ raise NotImplemented
170
+ end
171
+
172
+ # Return the resource type. Generally only used to specify
173
+ # resource is a collection.
174
+ def resource_type
175
+ :collection if collection?
176
+ end
177
+
178
+ # Return the mime type of this resource.
179
+ def content_type
180
+ raise NotImplemented
181
+ end
182
+
183
+ # Return the size in bytes for this resource.
184
+ def content_length
185
+ raise NotImplemented
186
+ end
187
+
188
+ # HTTP GET request.
189
+ #
190
+ # Write the content of the resource to the response.body.
191
+ def get(request, response)
192
+ NotImplemented
193
+ end
194
+
195
+ # HTTP PUT request.
196
+ #
197
+ # Save the content of the request.body.
198
+ def put(request, response)
199
+ NotImplemented
200
+ end
201
+
202
+ # HTTP POST request.
203
+ #
204
+ # Usually forbidden.
205
+ def post(request, response)
206
+ NotImplemented
207
+ end
208
+
209
+ # HTTP DELETE request.
210
+ #
211
+ # Delete this resource.
212
+ def delete
213
+ NotImplemented
214
+ end
215
+
216
+ # HTTP COPY request.
217
+ #
218
+ # Copy this resource to given destination resource.
219
+ def copy(dest, overwrite=false)
220
+ NotImplemented
221
+ end
222
+
223
+ # HTTP MOVE request.
224
+ #
225
+ # Move this resource to given destination resource.
226
+ def move(dest, overwrite=false)
227
+ NotImplemented
228
+ end
229
+
230
+ # args:: Hash of lock arguments
231
+ # Request for a lock on the given resource. A valid lock should lock
232
+ # all descendents. Failures should be noted and returned as an exception
233
+ # using LockFailure.
234
+ # Valid args keys: :timeout -> requested timeout
235
+ # :depth -> lock depth
236
+ # :scope -> lock scope
237
+ # :type -> lock type
238
+ # :owner -> lock owner
239
+ # Should return a tuple: [lock_time, locktoken] where lock_time is the
240
+ # given timeout
241
+ # NOTE: See section 9.10 of RFC 4918 for guidance about
242
+ # how locks should be generated and the expected responses
243
+ # (http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10)
244
+
245
+ def lock(args)
246
+ unless(@lock_class)
247
+ NotImplemented
248
+ else
249
+ unless(parent_exists?)
250
+ Conflict
251
+ else
252
+ lock_check(args[:scope])
253
+ lock = @lock_class.explicit_locks(@path).find{|l| l.scope == args[:scope] && l.kind == args[:type] && l.user == @user}
254
+ unless(lock)
255
+ token = UUIDTools::UUID.random_create.to_s
256
+ lock = @lock_class.generate(@path, @user, token)
257
+ lock.scope = args[:scope]
258
+ lock.kind = args[:type]
259
+ lock.owner = args[:owner]
260
+ lock.depth = args[:depth].is_a?(Symbol) ? args[:depth] : args[:depth].to_i
261
+ if(args[:timeout])
262
+ lock.timeout = args[:timeout] <= @max_timeout && args[:timeout] > 0 ? args[:timeout] : @max_timeout
263
+ else
264
+ lock.timeout = @default_timeout
265
+ end
266
+ lock.save if lock.respond_to? :save
267
+ end
268
+ begin
269
+ lock_check(args[:type])
270
+ rescue RackWebDAV::LockFailure => lock_failure
271
+ lock.destroy
272
+ raise lock_failure
273
+ rescue HTTPStatus::Status => status
274
+ status
275
+ end
276
+ [lock.remaining_timeout, lock.token]
277
+ end
278
+ end
279
+ end
280
+
281
+ # lock_scope:: scope of lock
282
+ # Check if resource is locked. Raise RackWebDAV::LockFailure if locks are in place.
283
+ def lock_check(lock_scope=nil)
284
+ return unless @lock_class
285
+ if(@lock_class.explicitly_locked?(@path))
286
+ raise Locked if @lock_class.explicit_locks(@path).find_all{|l|l.scope == 'exclusive' && l.user != @user}.size > 0
287
+ elsif(@lock_class.implicitly_locked?(@path))
288
+ if(lock_scope.to_s == 'exclusive')
289
+ locks = @lock_class.implicit_locks(@path)
290
+ failure = RackWebDAV::LockFailure.new("Failed to lock: #{@path}")
291
+ locks.each do |lock|
292
+ failure.add_failure(@path, Locked)
293
+ end
294
+ raise failure
295
+ else
296
+ locks = @lock_class.implict_locks(@path).find_all{|l| l.scope == 'exclusive' && l.user != @user}
297
+ if(locks.size > 0)
298
+ failure = LockFailure.new("Failed to lock: #{@path}")
299
+ locks.each do |lock|
300
+ failure.add_failure(@path, Locked)
301
+ end
302
+ raise failure
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ # token:: Lock token
309
+ # Remove the given lock
310
+ def unlock(token)
311
+ unless(@lock_class)
312
+ NotImplemented
313
+ else
314
+ token = token.slice(1, token.length - 2)
315
+ if(token.nil? || token.empty?)
316
+ BadRequest
317
+ else
318
+ lock = @lock_class.find_by_token(token)
319
+ if(lock.nil? || lock.user != @user)
320
+ Forbidden
321
+ elsif(lock.path !~ /^#{Regexp.escape(@path)}.*$/)
322
+ Conflict
323
+ else
324
+ lock.destroy
325
+ NoContent
326
+ end
327
+ end
328
+ end
329
+ end
330
+
331
+
332
+ # Create this resource as collection.
333
+ def make_collection
334
+ NotImplemented
335
+ end
336
+
337
+ # other:: Resource
338
+ # Returns if current resource is equal to other resource
339
+ def ==(other)
340
+ path == other.path
341
+ end
342
+
343
+ # Name of the resource
344
+ def name
345
+ File.basename(path)
346
+ end
347
+
348
+ # Name of the resource to be displayed to the client
349
+ def display_name
350
+ name
351
+ end
352
+
353
+ # Available properties
354
+ def properties
355
+ %w(creationdate displayname getlastmodified getetag resourcetype getcontenttype getcontentlength).collect do |prop|
356
+ {:name => prop, :ns_href => 'DAV:'}
357
+ end
358
+ end
359
+
360
+ # name:: String - Property name
361
+ # Returns the value of the given property
362
+ def get_property(element)
363
+ raise NotImplemented if (element[:ns_href] != 'DAV:')
364
+ case element[:name]
365
+ when 'resourcetype' then resource_type
366
+ when 'displayname' then display_name
367
+ when 'creationdate' then use_ms_compat_creationdate? ? creation_date.httpdate : creation_date.xmlschema
368
+ when 'getcontentlength' then content_length.to_s
369
+ when 'getcontenttype' then content_type
370
+ when 'getetag' then etag
371
+ when 'getlastmodified' then last_modified.httpdate
372
+ else raise NotImplemented
373
+ end
374
+ end
375
+
376
+ # name:: String - Property name
377
+ # value:: New value
378
+ # Set the property to the given value
379
+ def set_property(element, value)
380
+ raise NotImplemented if (element[:ns_href] != 'DAV:')
381
+ case element[:name]
382
+ when 'resourcetype' then self.resource_type = value
383
+ when 'getcontenttype' then self.content_type = value
384
+ when 'getetag' then self.etag = value
385
+ when 'getlastmodified' then self.last_modified = Time.httpdate(value)
386
+ else raise NotImplemented
387
+ end
388
+ end
389
+
390
+ # name:: Property name
391
+ # Remove the property from the resource
392
+ def remove_property(element)
393
+ Forbidden
394
+ end
395
+
396
+ # name:: Name of child
397
+ # Create a new child with the given name
398
+ # NOTE:: Include trailing '/' if child is collection
399
+ def child(name)
400
+ new_public = public_path.dup
401
+ new_public = new_public + '/' unless new_public[-1,1] == '/'
402
+ new_public = '/' + new_public unless new_public[0,1] == '/'
403
+ new_path = path.dup
404
+ new_path = new_path + '/' unless new_path[-1,1] == '/'
405
+ new_path = '/' + new_path unless new_path[0,1] == '/'
406
+ self.class.new("#{new_public}#{name}", "#{new_path}#{name}", request, response, options.merge(:user => @user))
407
+ end
408
+
409
+ # Return parent of this resource
410
+ def parent
411
+ unless(@path.to_s.empty?)
412
+ self.class.new(
413
+ File.split(@public_path).first,
414
+ File.split(@path).first,
415
+ @request,
416
+ @response,
417
+ @options.merge(
418
+ :user => @user
419
+ )
420
+ )
421
+ end
422
+ end
423
+
424
+ # Return list of descendants
425
+ def descendants
426
+ list = []
427
+ children.each do |child|
428
+ list << child
429
+ list.concat(child.descendants)
430
+ end
431
+ list
432
+ end
433
+
434
+ # Index page template for GETs on collection
435
+ def index_page
436
+ '<html><head> <title>%s</title>
437
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" /></head>
438
+ <body> <h1>%s</h1> <hr /> <table> <tr> <th class="name">Name</th>
439
+ <th class="size">Size</th> <th class="type">Type</th>
440
+ <th class="mtime">Last Modified</th> </tr> %s </table> <hr /> </body></html>'
441
+ end
442
+
443
+ # Does client allow GET redirection
444
+ # TODO: Get a comprehensive list in here.
445
+ # TODO: Allow this to be dynamic so users can add regexes to match if they know of a client
446
+ # that can be supported that is not listed.
447
+ def allows_redirect?
448
+ [
449
+ %r{cyberduck}i,
450
+ %r{konqueror}i
451
+ ].any? do |regexp|
452
+ (request.respond_to?(:user_agent) ? request.user_agent : request.env['HTTP_USER_AGENT']).to_s =~ regexp
453
+ end
454
+ end
455
+
456
+ def use_compat_mkcol_response?
457
+ @options[:compat_mkcol] || @options[:compat_all]
458
+ end
459
+
460
+ # Returns true if using an MS client
461
+ def use_ms_compat_creationdate?
462
+ if(@options[:compat_ms_mangled_creationdate] || @options[:compat_all])
463
+ is_ms_client?
464
+ end
465
+ end
466
+
467
+ # Basic user agent testing for MS authored client
468
+ def is_ms_client?
469
+ [%r{microsoft-webdav}i, %r{microsoft office}i].any? do |regexp|
470
+ (request.respond_to?(:user_agent) ? request.user_agent : request.env['HTTP_USER_AGENT']).to_s =~ regexp
471
+ end
472
+ end
473
+
474
+ protected
475
+
476
+ # Returns authentication credentials if available in form of [username,password]
477
+ # TODO: Add support for digest
478
+ def auth_credentials
479
+ auth = Rack::Auth::Basic::Request.new(request.env)
480
+ auth.provided? && auth.basic? ? auth.credentials : [nil,nil]
481
+ end
482
+
483
+ end
484
+
485
+ end