dav4rack 0.0.1

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,4 @@
1
+ *~
2
+ doc/*
3
+ pkg/*
4
+ test/repo
data/LICENSE ADDED
@@ -0,0 +1,42 @@
1
+ Current DAV4Rack license:
2
+
3
+ Copyright (c) 2010 Chris Roberts <chrisroberts.code@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to
7
+ deal in the Software without restriction, including without limitation the
8
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
9
+ sell copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+
23
+ Original rack_dav source license:
24
+
25
+ Copyright (c) 2009 Matthias Georgi <http://www.matthias-georgi.de>
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to
29
+ deal in the Software without restriction, including without limitation the
30
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
31
+ sell copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in
35
+ all copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
40
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
41
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
42
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,137 @@
1
+ == DAV4Rack - Web Authoring for Rack
2
+
3
+ DAV4Rack is a framework for providing WebDAV via Rack allowing content
4
+ authoring over HTTP. It is based off the {original RackDAV framework}[http://github.com/georgi/rack_dav]
5
+ to provide better Resource support for building customized WebDAV resources
6
+ completely abstracted from the filesystem. Support for authentication and
7
+ locking has been added as well as a move from REXML to Nokogiri. Enjoy!
8
+
9
+ == Install
10
+
11
+ === Via RubyGems
12
+
13
+ gem install dav4rack
14
+
15
+ == Quickstart
16
+
17
+ If you just want to share a folder over WebDAV, you can just start a
18
+ simple server with:
19
+
20
+ dav4rack
21
+
22
+ This will start a WEBrick server on port 3000, which you can connect
23
+ to without authentication. The simple file resource allows very basic authentication
24
+ which is used for an example. To enable it:
25
+
26
+ dav4rack --username=user --password=pass
27
+
28
+ == Rack Handler
29
+
30
+ Using DAV4Rack within a rack application is pretty simple. A very slim
31
+ rackup script would look something like this:
32
+
33
+ require 'rubygems'
34
+ require 'dav4rack'
35
+
36
+ use Rack::CommonLogger
37
+ run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare')
38
+
39
+ This will use the included FileResource and set the share path. However,
40
+ DAV4Rack has some nifty little extras that can be enabled in the rackup script. First,
41
+ an example of how to use a custom resource:
42
+
43
+ run DAV4Rack::Handler.new(:resource_class => CustomResource, :custom => 'options', :passed => 'to resource')
44
+
45
+ Next, lets venture into mapping a path for our WebDAV access. In this example, we
46
+ will use default FileResource like in the first example, but instead of connecting
47
+ directly to the host, we will have to connect to: http://host/webdav/share/
48
+
49
+ require 'rubygems'
50
+ require 'dav4rack'
51
+
52
+ use Rack::CommonLogger
53
+
54
+ app = Rack::Builder.new{
55
+ map '/webdav/share/' do
56
+ run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
57
+ end
58
+ }.to_app
59
+ run app
60
+
61
+ Aside from the #map block, notice the new option passed to the Handler's initialization, :root_uri_path. When
62
+ DAV4Rack receives a request, it will automatically convert the request to the proper path and pass it to
63
+ the resource.
64
+
65
+ Another tool available when building the rackup script is the Interceptor. The Interceptor's job is to simply
66
+ intecept WebDAV requests received up the path hierarchy where no resources are currently mapped. For example,
67
+ lets continue with the last example but this time include the interceptor:
68
+
69
+ require 'rubygems'
70
+ require 'dav4rack'
71
+
72
+ use Rack::CommonLogger
73
+ app = Rack::Builder.new{
74
+ map '/webdav/share/' do
75
+ run DAV4Rack::Handler.new(:root => '/path/to/public/fileshare', :root_uri_path => '/webdav/share/')
76
+ end
77
+ map '/webdav/share2/' do
78
+ run DAV4Rack::Handler.new(:resource_class => CustomResource, :root_uri_path => '/webdav/share2/')
79
+ end
80
+ map '/' do
81
+ use DAV4Rack::Interceptor, :mappings => {
82
+ '/webdav/share/' => {:resource_class => FileResource, :options => {:custom => 'option'}},
83
+ '/webdav/share2/' => {:resource_class => CustomResource}
84
+ }
85
+ use Rails::Rack::Static
86
+ run ActionController::Dispatcher.new
87
+ end
88
+ }.to_app
89
+ run app
90
+
91
+ In this example we have two WebDAV resources restricted by path. This means those resources will handle requests to /webdav/share/*
92
+ and /webdav/share2/* but nothing above that. To allow webdav to respond, we provide the Interceptor. The Interceptor does not
93
+ provide any authentication support. It simply creates a virtual file system view to the provided mapped paths. Once the actual
94
+ resources have been reached, authentication will be enforced based on the requirements defined by the individual resource. Also
95
+ note in the root map you can see we are running a Rails application. This is how you can easily enable DAV4Rack with your Rails
96
+ application.
97
+
98
+ == Custom Resources
99
+
100
+ Creating your own resource is easy. Simply inherit the DAV4Rack::Resource class, and start redefining all the methods
101
+ you want to customize. The DAV4Rack::Resource class only has implementations for methods that can be provided extremely
102
+ generically. This means that most things will require at least some sort of implementation. However, because the Resource
103
+ is defined so generically, and the Controller simply passes the request on to the Resource, it is easy to create fully
104
+ virtualized resources.
105
+
106
+ == Helpers
107
+
108
+ There are some helpers worth mentioning that make things a little easier. DAV4Rack::Resource#accept_redirect? method is available to Resources.
109
+ If true, the currently connected client will accept and properly use a 302 redirect for a GET request. Most clients do not properly
110
+ support this, which can be a real pain when working with virtualized files that may located some where else, like S3. To deal with
111
+ those clients that don't support redirects, a helper has been provided so resources don't have to deal with proxying themselves. The
112
+ DAV4Rack::RemoteFile allows the resource to simple tell Rack to download and send the file from the provided resource and go away, allowing the
113
+ process to be freed up to deal with other waiters. A very simple example:
114
+
115
+ class MyResource < DAV4Rack::Resource
116
+ def initialize(*args)
117
+ super(*args)
118
+ @item = method_to_fill_this_properly
119
+ end
120
+
121
+ def get
122
+ if(accept_redirect?)
123
+ response.redirect item[:url]
124
+ else
125
+ response.body = DAV4Rack::RemoteFile.new(item[:url], :size => content_length, :mime_type => content_type)
126
+ OK
127
+ end
128
+ end
129
+ end
130
+
131
+ == Issues/Bugs/Questions
132
+
133
+ Please use the issues at github: http://github.com/chrisroberts/dav4rack
134
+
135
+ == License
136
+
137
+ Just like RackDAV before it, this software is distributed under the MIT license.
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'dav4rack'
5
+ require 'getoptlong'
6
+
7
+ def print_help_msg
8
+ print_version_info
9
+ puts "Usage: dav4rack [opts]"
10
+ puts " --help Print help message"
11
+ puts " --version Print version information"
12
+ puts " --username name Set username"
13
+ puts " --password pass Set password"
14
+ puts " --root /share/path Set path to share directory"
15
+ end
16
+
17
+ def print_version_info
18
+ puts "DAV 4 Rack - Rack based WebDAV Framework - Version: #{DAV4Rack::VERSION}"
19
+ end
20
+
21
+ opts = GetoptLong.new(
22
+ ['--username', '-u', GetoptLong::REQUIRED_ARGUMENT],
23
+ ['--password', '-p', GetoptLong::REQUIRED_ARGUMENT],
24
+ ['--help', '-h', GetoptLong::NO_ARGUMENT],
25
+ ['--version', '-v', GetoptLong::NO_ARGUMENT],
26
+ ['--root', '-r', GetoptLong::REQUIRED_ARGUMENT]
27
+ )
28
+
29
+ credentials = {}
30
+
31
+ opts.each do |opt,arg|
32
+ case opt
33
+ when '--help'
34
+ print_help_msg
35
+ exit(0)
36
+ when '--username'
37
+ credentials[:username] = arg
38
+ when '--password'
39
+ credentials[:password] = arg
40
+ when '--root'
41
+ credentials[:root] = arg
42
+ unless(File.exists?(arg) && File.directory?(arg))
43
+ puts "ERROR: Path provided is not a valid directory (#{arg})"
44
+ exit(-1)
45
+ end
46
+ when '--version'
47
+ print_version_info
48
+ exit(0)
49
+ else
50
+ puts "ERROR: Unknown option provided"
51
+ exit(-1)
52
+ end
53
+ end
54
+
55
+ app = Rack::Builder.new do
56
+ use Rack::ShowExceptions
57
+ use Rack::CommonLogger
58
+ use Rack::Reloader
59
+ use Rack::Lint
60
+
61
+ run DAV4Rack::Handler.new(credentials)
62
+
63
+ end.to_app
64
+
65
+ Rack::Handler::WEBrick.run(app, :Port => 3000)
@@ -0,0 +1,31 @@
1
+ require 'dav4rack'
2
+ Gem::Specification.new do |s|
3
+ s.name = 'dav4rack'
4
+ s.version = DAV4Rack::VERSION
5
+ s.summary = 'WebDAV handler for Rack'
6
+ s.author = 'Chris Roberts'
7
+ s.email = 'chris@chrisroberts.org'
8
+ s.homepage = 'http://github.com/dav4rack'
9
+ s.description = 'WebDAV handler for Rack'
10
+ s.require_path = 'lib'
11
+ s.executables << 'dav4rack'
12
+ s.has_rdoc = true
13
+ s.extra_rdoc_files = ['README.rdoc']
14
+ s.files = %w{
15
+ .gitignore
16
+ LICENSE
17
+ dav4rack.gemspec
18
+ lib/dav4rack.rb
19
+ lib/dav4rack/file_resource.rb
20
+ lib/dav4rack/handler.rb
21
+ lib/dav4rack/controller.rb
22
+ lib/dav4rack/http_status.rb
23
+ lib/dav4rack/resource.rb
24
+ lib/dav4rack/interceptor.rb
25
+ lib/dav4rack/interceptor_resource.rb
26
+ lib/dav4rack/remote_file.rb
27
+ bin/dav4rack
28
+ spec/handler_spec.rb
29
+ README.rdoc
30
+ }
31
+ end
@@ -0,0 +1,13 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require 'nokogiri'
4
+
5
+ require 'rack'
6
+ require 'dav4rack/http_status'
7
+ require 'dav4rack/resource'
8
+ require 'dav4rack/handler'
9
+ require 'dav4rack/controller'
10
+
11
+ module DAV4Rack
12
+ VERSION = '0.0.1'
13
+ end
@@ -0,0 +1,457 @@
1
+ module DAV4Rack
2
+
3
+ class Controller
4
+ include DAV4Rack::HTTPStatus
5
+
6
+ attr_reader :request, :response, :resource
7
+
8
+ # request:: Rack::Request
9
+ # response:: Rack::Response
10
+ # options:: Options hash
11
+ # Create a new Controller.
12
+ # NOTE: options will be passed to Resource
13
+ def initialize(request, response, options={})
14
+ @request = request
15
+ @response = response
16
+ @options = options
17
+ @resource = resource_class.new(actual_path, implied_path, @request, @options)
18
+ authenticate
19
+ raise Forbidden if request.path_info.include?('../')
20
+ end
21
+
22
+ # s:: string
23
+ # Escape URL string
24
+ def url_escape(s)
25
+ s.gsub(/([^\/a-zA-Z0-9_.-]+)/n) do
26
+ '%' + $1.unpack('H2' * $1.size).join('%').upcase
27
+ end.tr(' ', '+')
28
+ end
29
+
30
+ # s:: string
31
+ # Unescape URL string
32
+ def url_unescape(s)
33
+ s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
34
+ [$1.delete('%')].pack('H*')
35
+ end
36
+ end
37
+
38
+ # Return response to OPTIONS
39
+ def options
40
+ response["Allow"] = 'OPTIONS,HEAD,GET,PUT,POST,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK'
41
+ response["Dav"] = "2"
42
+ response["Ms-Author-Via"] = "DAV"
43
+ NoContent
44
+ end
45
+
46
+ # Return response to HEAD
47
+ def head
48
+ raise NotFound unless resource.exist?
49
+ response['Etag'] = resource.etag
50
+ response['Content-Type'] = resource.content_type
51
+ response['Last-Modified'] = resource.last_modified.httpdate
52
+ NoContent
53
+ end
54
+
55
+ # Return response to GET
56
+ def get
57
+ raise NotFound unless resource.exist?
58
+ res = resource.get(request, response)
59
+ if(response.status == 200 && !resource.collection?)
60
+ response['Etag'] = resource.etag
61
+ response['Content-Type'] = resource.content_type
62
+ response['Content-Length'] = resource.content_length.to_s
63
+ response['Last-Modified'] = resource.last_modified.httpdate
64
+ end
65
+ res
66
+ end
67
+
68
+ # Return response to PUT
69
+ def put
70
+ raise Forbidden if resource.collection?
71
+ resource.put(request, response)
72
+ end
73
+
74
+ # Return response to POST
75
+ def post
76
+ resource.post(request, response)
77
+ end
78
+
79
+ # Return response to DELETE
80
+ def delete
81
+ raise NotFound unless resource.exist?
82
+ resource.delete
83
+ end
84
+
85
+ # Return response to MKCOL
86
+ def mkcol
87
+ resource.make_collection
88
+ end
89
+
90
+ # Return response to COPY
91
+ def copy
92
+ move(:copy)
93
+ end
94
+
95
+ # args:: Only argument used: :copy
96
+ # Move Resource to new location. If :copy is provided,
97
+ # Resource will be copied (implementation ease)
98
+ def move(*args)
99
+ raise NotFound unless resource.exist?
100
+ dest_uri = URI.parse(env['HTTP_DESTINATION'])
101
+ destination = url_unescape(dest_uri.path)
102
+ raise BadGateway if dest_uri.host and dest_uri.host != request.host
103
+ raise Forbidden if destination == resource.public_path
104
+ dest = resource_class.new(destination, clean_path(destination), @request, @options)
105
+ if(args.include?(:copy))
106
+ resource.copy(dest, overwrite)
107
+ else
108
+ raise Conflict unless depth.is_a?(Symbol) || depth > 1
109
+ resource.move(dest)
110
+ end
111
+ end
112
+
113
+ # Return respoonse to PROPFIND
114
+ def propfind
115
+ raise NotFound unless resource.exist?
116
+ unless(request_document.xpath("//#{ns}propfind/#{ns}allprop").empty?)
117
+ names = resource.property_names
118
+ else
119
+ names = request_document.xpath("//#{ns}propfind/#{ns}prop").children.find_all{|n|n.element?}.map{|n|n.name}
120
+ names = resource.property_names if names.empty?
121
+ end
122
+ multistatus do |xml|
123
+ find_resources.each do |resource|
124
+ xml.response do
125
+ xml.href "#{scheme}://#{host}:#{port}#{url_escape(resource.public_path)}"
126
+ propstats(xml, get_properties(resource, names))
127
+ end
128
+ end
129
+ end
130
+ end
131
+
132
+ # Return response to PROPPATCH
133
+ def proppatch
134
+ raise NotFound unless resource.exist?
135
+ prop_rem = request_match('/propertyupdate/remove/prop').children.map{|n| [n.name] }
136
+ prop_set = request_match('/propertyupdate/set/prop').children.map{|n| [n.name, n.text] }
137
+ multistatus do |xml|
138
+ find_resources.each do |resource|
139
+ xml.response do
140
+ xml.href "#{scheme}://#{host}:#{port}#{url_escape(resource.public_path)}"
141
+ propstats(xml, set_properties(resource, prop_set))
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+
148
+ # Lock current resource
149
+ # NOTE: This will pass an argument hash to Resource#lock and
150
+ # wait for a success/failure response.
151
+ def lock
152
+ raise NotFound unless resource.exist?
153
+ lockinfo = request_document.xpath("//#{ns}lockinfo")
154
+ asked = {}
155
+ asked[:timeout] = request.env['Timeout'].split(',').map{|x|x.strip} if request.env['Timeout']
156
+ asked[:depth] = depth
157
+ raise BadRequest unless [0, :infinity].include?(asked[:depth])
158
+ asked[:scope] = lockinfo.xpath("//#{ns}lockscope").children.find_all{|n|n.element?}.map{|n|n.name}.first
159
+ asked[:type] = lockinfo.xpath("#{ns}locktype").children.find_all{|n|n.element?}.map{|n|n.name}.first
160
+ asked[:owner] = lockinfo.xpath("//#{ns}owner/#{ns}href").children.map{|n|n.text}.first
161
+ begin
162
+ lock_time, locktoken = resource.lock(asked)
163
+ render_xml(:prop) do |xml|
164
+ xml.lockdiscovery do
165
+ xml.activelock do
166
+ if(asked[:scope])
167
+ xml.lockscope do
168
+ xml.send(asked[:scope])
169
+ end
170
+ end
171
+ if(asked[:type])
172
+ xml.locktype do
173
+ xml.send(asked[:type])
174
+ end
175
+ end
176
+ xml.depth asked[:depth].to_s
177
+ xml.timeout lock_time ? "Second-#{lock_time}" : 'infinity'
178
+ xml.locktoken do
179
+ xml.href locktoken
180
+ end
181
+ end
182
+ end
183
+ end
184
+ response.status = resource.exist? ? OK : Created
185
+ rescue LockFailure => e
186
+ multistatus do |xml|
187
+ e.path_status.each_pair do |path, status|
188
+ xml.response do
189
+ xml.href path
190
+ xml.status "#{http_version} #{status.status_line}"
191
+ end
192
+ end
193
+ end
194
+ response.status = MultiStatus
195
+ end
196
+ end
197
+
198
+ # Unlock current resource
199
+ def unlock
200
+ resource.unlock(lock_token)
201
+ end
202
+
203
+ # ************************************************************
204
+ # private methods
205
+
206
+ private
207
+
208
+ # Request environment variables
209
+ def env
210
+ @request.env
211
+ end
212
+
213
+ # Current request scheme (http/https)
214
+ def scheme
215
+ request.scheme
216
+ end
217
+
218
+ # Request host
219
+ def host
220
+ request.host
221
+ end
222
+
223
+ # Request port
224
+ def port
225
+ request.port
226
+ end
227
+
228
+ # Class of the resource in use
229
+ def resource_class
230
+ @options[:resource_class]
231
+ end
232
+
233
+ # Root URI path for the resource
234
+ def root_uri_path
235
+ @options[:root_uri_path]
236
+ end
237
+
238
+ # Returns Resource path with root URI removed
239
+ def implied_path
240
+ clean_path(@request.path.dup)
241
+ end
242
+
243
+ # x:: request path
244
+ # Unescapes path and removes root URI if applicable
245
+ def clean_path(x)
246
+ ip = url_unescape(x)
247
+ ip.gsub!(/^#{Regexp.escape(root_uri_path)}/, '') if root_uri_path
248
+ ip
249
+ end
250
+
251
+ # Unescaped request path
252
+ def actual_path
253
+ url_unescape(@request.path.dup)
254
+ end
255
+
256
+ # Lock token if provided by client
257
+ def lock_token
258
+ env['HTTP_LOCK_TOKEN'] || nil
259
+ end
260
+
261
+ # Requested depth
262
+ def depth
263
+ d = env['HTTP_DEPTH']
264
+ if(d =~ /^\d+$/)
265
+ d = d.to_i
266
+ else
267
+ d = :infinity
268
+ end
269
+ d
270
+ end
271
+
272
+ # Current HTTP version being used
273
+ def http_version
274
+ env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.0'
275
+ end
276
+
277
+ # Overwrite is allowed
278
+ def overwrite
279
+ env['HTTP_OVERWRITE'].to_s.upcase != 'F'
280
+ end
281
+
282
+ # Find resources at depth requested
283
+ def find_resources
284
+ ary = nil
285
+ case depth
286
+ when 0
287
+ ary = [resource]
288
+ when 1
289
+ ary = resource.children
290
+ else
291
+ ary = resource.descendants
292
+ end
293
+ ary ? ary : []
294
+ end
295
+
296
+ # XML parsed request
297
+ def request_document
298
+ @request_document ||= Nokogiri.XML(request.body.read)
299
+ rescue
300
+ raise BadRequest
301
+ end
302
+
303
+ # Namespace being used within XML document
304
+ # TODO: Make this better
305
+ def ns
306
+ _ns = ''
307
+ if(request_document && request_document.root && request_document.root.namespace_definitions.size > 0)
308
+ _ns = request_document.root.namespace_definitions.first.prefix.to_s
309
+ _ns += ':' unless _ns.empty?
310
+ end
311
+ _ns
312
+ end
313
+
314
+ # pattern:: XPath pattern
315
+ # Search XML document for given XPath
316
+ def request_match(pattern)
317
+ nil unless request_document
318
+ request_document.xpath(pattern, request_document.root.namespaces)
319
+ end
320
+
321
+ # root_type:: Root tag name
322
+ # Render XML and set Rack::Response#body= to final XML
323
+ def render_xml(root_type)
324
+ raise ArgumentError.new 'Expecting block' unless block_given?
325
+ doc = Nokogiri::XML::Builder.new do |xml|
326
+ xml.send(root_type.to_s, 'xmlns' => 'DAV:') do
327
+ # xml.parent.namespace = xml.parent.namespace_definitions.first
328
+ yield xml
329
+ end
330
+ end
331
+
332
+ response.body = doc.to_xml
333
+ response["Content-Type"] = 'text/xml; charset="utf-8"'
334
+ response["Content-Length"] = response.body.size.to_s
335
+ end
336
+
337
+ # block:: block
338
+ # Creates a multistatus response using #render_xml and
339
+ # returns the correct status
340
+ def multistatus(&block)
341
+ render_xml(:multistatus, &block)
342
+ MultiStatus
343
+ end
344
+
345
+ # xml:: Nokogiri::XML::Builder
346
+ # errors:: Array of errors
347
+ # Crafts responses for errors
348
+ def response_errors(xml, errors)
349
+ for path, status in errors
350
+ xml.response do
351
+ xml.href "#{scheme}://#{host}:#{port}#{path}"
352
+ xml.status "#{http_version} #{status.status_line}"
353
+ end
354
+ end
355
+ end
356
+
357
+ # resource:: Resource
358
+ # names:: Property names
359
+ # Returns array of property values for given names
360
+ def get_properties(resource, names)
361
+ stats = Hash.new { |h, k| h[k] = [] }
362
+ for name in names
363
+ begin
364
+ val = resource.get_property(name)
365
+ stats[OK].push [name, val] if val
366
+ rescue Status
367
+ stats[$!] << name
368
+ end
369
+ end
370
+ stats
371
+ end
372
+
373
+ # resource:: Resource
374
+ # pairs:: name value pairs
375
+ # Sets the given properties
376
+ def set_properties(resource, pairs)
377
+ stats = Hash.new { |h, k| h[k] = [] }
378
+ for name, value in pairs
379
+ begin
380
+ stats[OK] << [name, resource.set_property(name, value)]
381
+ rescue Status
382
+ stats[$!] << name
383
+ end
384
+ end
385
+ stats
386
+ end
387
+
388
+ # xml:: Nokogiri::XML::Builder
389
+ # stats:: Array of stats
390
+ # Build propstats response
391
+ def propstats(xml, stats)
392
+ return if stats.empty?
393
+ for status, props in stats
394
+ xml.propstat do
395
+ xml.prop do
396
+ for name, value in props
397
+ if(value.is_a?(Nokogiri::XML::Node))
398
+ xml.send(name) do
399
+ xml_convert(xml, value)
400
+ end
401
+ elsif(value.is_a?(Symbol))
402
+ xml.send(name) do
403
+ xml.send(value)
404
+ end
405
+ else
406
+ xml.send(name, value)
407
+ end
408
+ end
409
+ end
410
+ xml.status "#{http_version} #{status.status_line}"
411
+ end
412
+ end
413
+ end
414
+
415
+ # xml:: Nokogiri::XML::Builder
416
+ # element:: Nokogiri::XML::Element
417
+ # Converts element into proper text
418
+ def xml_convert(xml, element)
419
+ if element.children.empty?
420
+ if element.text?
421
+ xml.send(element.name, element.text, element.attributes)
422
+ else
423
+ xml.send(element.name, element.attributes)
424
+ end
425
+ else
426
+ xml.send(element.name, element.attributes) do
427
+ element.elements.each do |child|
428
+ xml_convert(xml, child)
429
+ end
430
+ end
431
+ end
432
+ end
433
+
434
+ # Perform authentication
435
+ # NOTE: Authentication will only be performed if the Resource
436
+ # has defined an #authenticate method
437
+ def authenticate
438
+ authed = true
439
+ if(resource.respond_to?(:authenticate))
440
+ authed = false
441
+ if(request.env['HTTP_AUTHORIZATION'])
442
+ auth = Rack::Auth::Basic::Request.new(request.env)
443
+ if(auth.basic? && auth.credentials)
444
+ authed = resource.authenticate(auth.credentials[0], auth.credentials[1])
445
+ end
446
+ end
447
+ end
448
+ unless(authed)
449
+ response.body = resource.respond_to?(:authentication_error_msg) ? resource.authentication_error_msg : 'Not Authorized'
450
+ response['WWW-Authenticate'] = "Basic realm=\"#{resource.respond_to?(:authentication_realm) ? resource.authentication_realm : 'Locked content'}\""
451
+ raise Unauthorized.new unless authed
452
+ end
453
+ end
454
+
455
+ end
456
+
457
+ end