rack-webdav 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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