Class: Tilia::Dav::Client

Inherits:
Http::Client
  • Object
show all
Defined in:
lib/tilia/dav/client.rb

Overview

SabreDAV DAV client

This client wraps around Curl to provide a convenient API to a WebDAV server.

NOTE: This class is experimental, it’s api will likely change in the future.

Constant Summary collapse

AUTH_BASIC =

Basic authentication

1
AUTH_DIGEST =

Digest authentication

2
AUTH_NTLM =

NTLM authentication

4
ENCODING_IDENTITY =

Identity encoding, which basically does not nothing.

1
ENCODING_DEFLATE =

Deflate encoding

2
ENCODING_GZIP =

Gzip encoding

4
ENCODING_ALL =

Sends all encoding headers.

7

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(settings) ⇒ Client

Constructor

Settings are provided through the ‘settings’ argument. The following settings are supported:

 * baseUri
 * userName (optional)
 * password (optional)
 * proxy (optional)
 * authType (optional)
 * encoding (optional)

authType must be a bitmap, using AUTH_BASIC, AUTH_DIGEST
and AUTH_NTLM. If you know which authentication method will be
used, it's recommended to set it, as it will save a great deal of
requests to 'discover' this information.

Encoding is a bitmap with one of the ENCODING constants.

Parameters:

  • array

    settings



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/tilia/dav/client.rb', line 79

def initialize(settings)
  @property_map = {}
  @encoding = ENCODING_IDENTITY

  unless settings.key?('baseUri')
    fail ArgumentError, 'A baseUri must be provided'
  end

  super()

  @base_uri = settings['baseUri']

  add_curl_setting(:proxy, settings['proxy']) if settings.key?('proxy')

  if settings.key?('userName')
    user_name = settings['userName']
    password = settings['password'] || ''

    if settings.key?('authType')
      curl_type = []
      curl_type << :basic if settings['authType'] & AUTH_BASIC > 0
      curl_type << :digest if settings['authType'] & AUTH_DIGEST > 0
      curl_type << :ntlm if settings['authType'] & AUTH_NTLM > 0
    else
      curl_type = [:basic, :digest]
    end

    add_curl_setting(:httpauth, curl_type)
    add_curl_setting(:userpwd, "#{user_name}:#{password}")
  end

  if settings.key?('encoding')
    encoding = settings['encoding']

    encodings = []
    encodings << 'identity' if encoding & ENCODING_IDENTITY > 0
    encodings << 'deflate' if encoding & ENCODING_DEFLATE > 0
    encodings << 'gzip' if encoding & ENCODING_GZIP > 0

    add_curl_setting(:encoding, encodings.join(','))
  end

  add_curl_setting(:useragent, "tilia-dav/#{Version::VERSION} (http://sabre.io/)")

  @xml = Xml::Service.new
  # BC
  @property_map = @xml.element_map
end

Instance Attribute Details

#property_mapObject

Deprecated.

The elementMap

This property is linked via reference to @xml.element_map. It’s deprecated as of version 3.0.0, and should no longer be used.



24
25
26
# File 'lib/tilia/dav/client.rb', line 24

def property_map
  @property_map
end

#xmlObject

The xml service.

Uset this service to configure the property and namespace maps.



15
16
17
# File 'lib/tilia/dav/client.rb', line 15

def xml
  @xml
end

Instance Method Details

#absolute_url(url) ⇒ Object

Returns the full url based on the given url (which may be relative). All urls are expanded based on the base url as given by the server.

Parameters:

  • string

    url

Returns:

  • string



316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/tilia/dav/client.rb', line 316

def absolute_url(url)
  # If the url starts with http:// or https://, the url is already absolute.
  return url if url =~ /^http(s?):\/\//

  # If the url starts with a slash, we must calculate the url based off
  # the root of the base url.
  if url.index('/') == 0
    parts = Tilia::Uri.parse(@base_uri)
    return "#{parts['scheme']}://#{parts['host']}#{parts['port'] ? ":#{parts['port']}" : ''}#{url}"
  end

  # Otherwise...
  @base_uri + url
end

#optionsObject

Performs an HTTP options request

This method returns all the features from the ‘DAV:’ header as an array. If there was no DAV header, or no contents this method will return an empty array.

Returns:

  • array



259
260
261
262
263
264
265
266
267
268
# File 'lib/tilia/dav/client.rb', line 259

def options
  request = Http::Request.new('OPTIONS', absolute_url(''))
  response = send_request(request)

  dav = response.header('Dav')
  return [] unless dav

  features = dav.split(',')
  features.map(&:strip)
end

#parse_multi_status(body) ⇒ Object

Parses a WebDAV multistatus response body

This method returns an array with the following structure

[

'url/to/resource' => [
  '200' => [
     '{DAV:}property1' => 'value1',
     '{DAV:}property2' => 'value2',
  ],
  '404' => [
     '{DAV:}property1' => null,
     '{DAV:}property2' => null,
  ],
],
'url/to/resource2' => [
   .. etc ..
]

]

Parameters:

  • string

    body xml body

Returns:

  • array



354
355
356
357
358
359
360
361
362
363
364
# File 'lib/tilia/dav/client.rb', line 354

def parse_multi_status(body)
  multistatus = @xml.expect('{DAV:}multistatus', body)

  result = {}

  multistatus.responses.each do |response|
    result[response.href] = response.response_properties
  end

  result
end

#prop_find(url, properties, depth = 0) ⇒ Object

Does a PROPFIND request

The list of requested properties must be specified as an array, in clark notation.

The returned array will contain a list of filenames as keys, and properties as values.

The properties array will contain the list of properties. Only properties that are actually returned from the server (without error) will be returned, anything else is discarded.

Depth should be either 0 or 1. A depth of 1 will cause a request to be made to the server to also return all child resources.

Parameters:

  • string

    url

  • array

    properties

  • int

    depth

Returns:

  • array



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/tilia/dav/client.rb', line 147

def prop_find(url, properties, depth = 0)
  dom = LibXML::XML::Document.new

  root = LibXML::XML::Node.new('d:propfind')
  LibXML::XML::Namespace.new(root, 'd', 'DAV:')
  prop = LibXML::XML::Node.new('d:prop')

  properties.each do |property|
    (namespace, element_name) = Tilia::Xml::Service.parse_clark_notation(property)

    if namespace == 'DAV:'
      element = LibXML::XML::Node.new("d:#{element_name}")
    else
      element = LibXML::XML::Node.new("x:#{element_name}")
      LibXML::XML::Namespace.new(element, 'x', namespace)
    end

    prop << element
  end

  dom.root = root
  root << prop

  body = dom.to_s

  url = absolute_url(url)

  request = Http::Request.new(
    'PROPFIND',
    url,
    {
      'Depth'        => depth,
      'Content-Type' => 'application/xml'
    },
    body
  )

  response = send_request(request)

  fail Http::ClientHttpException.new(response) if response.status.to_i >= 400

  result = parse_multi_status(response.body_as_string)

  # If depth was 0, we only return the top item
  if depth == 0
    result = result.first.second # value of first key/value pair
    return result.key?('200') ? result['200'] : {}
  end

  new_result = {}
  result.each do |href, status_list|
    new_result[href] = status_list.key?('200') ? status_list['200'] : {}
  end

  new_result
end

#prop_patch(url, properties) ⇒ Object

Updates a list of properties on the server

The list of properties must have clark-notation properties for the keys, and the actual (string) value for the value. If the value is null, an attempt is made to delete the property.

Parameters:

  • string

    url

  • array

    properties

Returns:

  • bool



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/tilia/dav/client.rb', line 213

def prop_patch(url, properties)
  prop_patch = Xml::Request::PropPatch.new
  prop_patch.properties = properties
  xml = @xml.write('{DAV:}propertyupdate', prop_patch)

  url = absolute_url(url)
  request = Http::Request.new(
    'PROPPATCH',
    url,
    { 'Content-Type' => 'application/xml' },
    xml
  )

  response = send_request(request)

  fail Http::ClientHttpException.new(response) if response.status.to_i >= 400

  if response.status == 207.to_i
    # If it's a 207, the request could still have failed, but the
    # information is hidden in the response body.
    result = parse_multi_status(response.body_as_string)

    error_properties = []
    result.each do |href, status_list|
      status_list.each do |status, properties|
        next unless status.to_i >= 400

        properties.each do |prop_name, prop_value|
          error_properties << "#{prop_name} (#{status})"
        end
      end
    end

    fail Http::ClientException, "ROPPATCH failed. The following properties errored: #{error_properties.join(', ')}" if error_properties.any?
  end

  true
end

#request(method, url = '', body = nil, headers = {}) ⇒ Object

Performs an actual HTTP request, and returns the result.

If the specified url is relative, it will be expanded based on the base url.

The returned array contains 3 keys:

* body - the response body
* httpCode - a HTTP code (200, 404, etc)
* headers - a list of response http headers. The header names have
  been lowercased.

For large uploads, it’s highly recommended to specify body as a stream resource. You can easily do this by simply passing the result of fopen(…, ‘r’).

This method will throw an exception if an HTTP error was received. Any HTTP status code above 399 is considered an error.

Note that it is no longer recommended to use this method, use the send method instead.

Parameters:

  • string

    method

  • string

    url

  • string|resource|null

    body

  • array

    headers

Returns:

  • array



297
298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/tilia/dav/client.rb', line 297

def request(method, url = '', body = nil, headers = {})
  url = absolute_url(url)

  headers = {}
  response.headers.each { |k, v| headers[k.downcase] = v }

  response = send_request(Http::Request.new(method, url, headers, body))
  {
    'body'       => response.body_as_string,
    'statusCode' => response.status.to_i,
    'headers'    => headers
  }
end