Class: Tilia::Dav::Locks::Plugin

Inherits:
ServerPlugin show all
Defined in:
lib/tilia/dav/locks/plugin.rb

Overview

Locking plugin

This plugin provides locking support to a WebDAV server. The easiest way to get started, is by hooking it up as such:

lock_backend = new SabreDAVLocksBackendFile(‘./mylockdb’) lock_plugin = new SabreDAVLocksPlugin(lock_backend) server.add_plugin(lock_plugin)

Instance Method Summary collapse

Methods inherited from ServerPlugin

#supported_report_set

Constructor Details

#initialize(locks_backend) ⇒ Plugin

__construct

Parameters:

  • Backend\BackendInterface

    locks_backend



28
29
30
# File 'lib/tilia/dav/locks/plugin.rb', line 28

def initialize(locks_backend)
  @locks_backend = locks_backend
end

Instance Method Details

#after_unbind(path) ⇒ Object

This method is called after a node is deleted.

We use this event to clean up any locks that still exist on the node.

Parameters:

  • string

    path

Returns:

  • void



252
253
254
255
256
257
# File 'lib/tilia/dav/locks/plugin.rb', line 252

def after_unbind(path)
  locks = locks(path, include_children = true)
  locks.each do |lock|
    unlock_node(path, lock)
  end
end

#featuresObject

Returns a list of features for the HTTP OPTIONS Dav: header.

In this case this is only the number 2. The 2 in the Dav: header indicates the server supports locks.

Returns:

  • array



89
90
91
# File 'lib/tilia/dav/locks/plugin.rb', line 89

def features
  [2]
end

#http_lock(request, response) ⇒ Object

Locks an uri

The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type of lock (shared or exclusive) and the owner of the lock

If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock

Additionally, a lock can be requested for a non-existent file. In these case we’re obligated to create an empty file as per RFC4918:S7.3

Parameters:

  • RequestInterface

    request

  • ResponseInterface

    response

Returns:

  • bool



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
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
203
204
205
206
207
208
# File 'lib/tilia/dav/locks/plugin.rb', line 121

def http_lock(request, response)
  uri = request.path
  existing_locks = locks(uri)

  body = request.body_as_string
  if !body.blank?
    # This is a new lock request

    existing_lock = nil
    # Checking if there's already non-shared locks on the uri.
    existing_locks.each do |existing_lock|
      if existing_lock.scope == LockInfo::EXCLUSIVE
        fail Exception::ConflictingLock.new(existing_lock)
      end
    end

    lock_info = parse_lock_request(body)
    lock_info.depth = @server.http_depth
    lock_info.uri = uri
    if existing_lock && lock_info.scope != LockInfo::SHARED
      fail Exception::ConflictingLock(existing_lock)
    end
  else
    # Gonna check if this was a lock refresh.
    existing_locks = locks(uri)
    conditions = @server.if_conditions(request)
    found = nil

    existing_locks.each do |existing_lock|
      conditions.each do |condition|
        condition['tokens'].each do |token|
          if token['token'] == 'opaquelocktoken:' + existing_lock.token
            found = existing_lock
            break
          end
        end
        break if found
      end
      break if found
    end

    # If none were found, this request is in error.
    unless found
      if existing_locks.any?
        fail Exception::Locked.new(existing_locks.first)
      else
        fail Exception::BadRequest, 'An xml body is required for lock requests'
      end
    end

    # This must have been a lock refresh
    lock_info = found

    # The resource could have been locked through another uri.
    uri = lock_info.uri unless uri == lock_info.uri
  end

  timeout = timeout_header
  lock_info.timeout = timeout if timeout

  new_file = false

  # If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first
  begin
    @server.tree.node_for_path(uri)

    # We need to call the beforeWriteContent event for RFC3744
    # Edit: looks like this is not used, and causing problems now.
    #
    # See Issue 222
    # @server.emit('beforeWriteContent',array(uri))
  rescue Exception::NotFound => e
    # It didn't, lets create it
    @server.create_file(uri, StringIO.new)
    new_file = true
  end

  lock_node(uri, lock_info)

  response.update_header('Content-Type', 'application/xml; charset=utf-8')
  response.update_header('Lock-Token', '<opaquelocktoken:' + lock_info.token + '>')
  response.status = new_file ? 201 : 200
  response.body = generate_lock_response(lock_info)

  # Returning false will interupt the event chain and mark this method
  # as 'handled'.
  false
end

#http_methods(_uri) ⇒ Object

Use this method to tell the server this plugin defines additional HTTP methods.

This method is passed a uri. It should only return HTTP methods that are available for the specified uri.

Parameters:

  • string

    uri

Returns:

  • array



79
80
81
# File 'lib/tilia/dav/locks/plugin.rb', line 79

def http_methods(_uri)
  ['LOCK', 'UNLOCK']
end

#http_unlock(request, response) ⇒ Object

Unlocks a uri

This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header The server should return 204 (No content) on success

Parameters:

  • RequestInterface

    request

  • ResponseInterface

    response

Returns:

  • void



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
# File 'lib/tilia/dav/locks/plugin.rb', line 218

def http_unlock(request, response)
  lock_token = request.header('Lock-Token')

  # If the locktoken header is not supplied, we need to throw a bad request exception
  fail Exception::BadRequest, 'No lock token was supplied' unless lock_token

  path = request.path
  locks = locks(path)

  # Windows sometimes forgets to include < and > in the Lock-Token
  # header
  lock_token = '<' + lock_token + '>' unless lock_token[0] == '<'

  locks.each do |lock|
    next unless "<opaquelocktoken:#{lock.token}>" == lock_token
    unlock_node(path, lock)
    response.update_header('Content-Length', '0')
    response.status = 204

    # Returning false will break the method chain, and mark the
    # method as 'handled'.
    return false
  end

  # If we got here, it means the locktoken was invalid
  fail Exception::LockTokenMatchesRequestUri
end

#lock_node(uri, lock_info) ⇒ Object

Locks a uri

All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client

Parameters:

  • string

    uri

  • LockInfo

    lock_info

Returns:

  • bool



267
268
269
270
# File 'lib/tilia/dav/locks/plugin.rb', line 267

def lock_node(uri, lock_info)
  return nil unless @server.emit('beforeLock', [uri, lock_info])
  @locks_backend.lock(uri, lock_info)
end

#locks(uri, return_child_locks = false) ⇒ Object

Returns all lock information on a particular uri

This function should return an array with SabreDAVLocksLockInfo objects. If there are no locks on a file, return an empty array.

Additionally there is also the possibility of locks on parent nodes, so we’ll need to traverse every part of the tree If the return_child_locks argument is set to true, we’ll also traverse all the children of the object for any possible locks and return those as well.

Parameters:

  • string

    uri

  • bool

    return_child_locks

Returns:

  • array



104
105
106
# File 'lib/tilia/dav/locks/plugin.rb', line 104

def locks(uri, return_child_locks = false)
  @locks_backend.locks(uri, return_child_locks)
end

#plugin_infoObject

Returns a bunch of meta-data about the plugin.

Providing this information is optional, and is mainly displayed by the Browser plugin.

The description key in the returned array may contain html and will not be sanitized.

Returns:

  • array



457
458
459
460
461
462
463
# File 'lib/tilia/dav/locks/plugin.rb', line 457

def plugin_info
  {
    'name'        => plugin_name,
    'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK',
    'link'        => 'http://sabre.io/dav/locks/'
  }
end

#plugin_nameObject

Returns a plugin name.

Using this name other plugins will be able to access other plugins using SabreDAVServer::getPlugin

Returns:

  • string



56
57
58
# File 'lib/tilia/dav/locks/plugin.rb', line 56

def plugin_name
  'locks'
end

#prop_find(prop_find, _node) ⇒ Object

This method is called after most properties have been found it allows us to add in any Lock-related properties

Parameters:

  • DAV\PropFind

    prop_find

  • DAV\INode

    node

Returns:

  • void



66
67
68
69
# File 'lib/tilia/dav/locks/plugin.rb', line 66

def prop_find(prop_find, _node)
  prop_find.handle('{DAV:}supportedlock', -> { Dav::Xml::Property::SupportedLock.new })
  prop_find.handle('{DAV:}lockdiscovery', -> { Dav::Xml::Property::LockDiscovery.new(locks(prop_find.path)) })
end

#setup(server) ⇒ Object

Initializes the plugin

This method is automatically called by the Server class after addPlugin.

Parameters:

  • DAV\Server

    server

Returns:

  • void



38
39
40
41
42
43
44
45
46
47
48
# File 'lib/tilia/dav/locks/plugin.rb', line 38

def setup(server)
  @server = server

  @server.xml.element_map['{DAV:}lockinfo'] = Xml::Request::Lock

  server.on('method:LOCK',    method(:http_lock))
  server.on('method:UNLOCK',  method(:http_unlock))
  server.on('validateTokens', method(:validate_tokens))
  server.on('propFind',       method(:prop_find))
  server.on('afterUnbind',    method(:after_unbind))
end

#timeout_headerObject

Returns the contents of the HTTP Timeout header.

The method formats the header into an integer.

Returns:

  • int



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# File 'lib/tilia/dav/locks/plugin.rb', line 289

def timeout_header
  header = @server.http_request.header('Timeout')

  if header
    if header.downcase.index('second-') == 0
      header = header[7..-1].to_i
    elsif header.downcase.index('infinite') == 0
      header = LockInfo::TIMEOUT_INFINITE
    else
      fail Exception::BadRequest, 'Invalid HTTP timeout header'
    end
  else
    header = 0
  end

  header
end

#unlock_node(uri, lock_info) ⇒ Object

Unlocks a uri

This method removes a lock from a uri. It is assumed all the supplied information is correct and verified

Parameters:

  • string

    uri

  • LockInfo

    lock_info

Returns:

  • bool



279
280
281
282
# File 'lib/tilia/dav/locks/plugin.rb', line 279

def unlock_node(uri, lock_info)
  return nil unless @server.emit('beforeUnlock', [uri, lock_info])
  @locks_backend.unlock(uri, lock_info)
end

#validate_tokens(request, conditions_box) ⇒ Object

The validateTokens event is triggered before every request.

It’s a moment where this plugin can check all the supplied lock tokens in the If: header, and check if they are valid.

In addition, it will also ensure that it checks any missing lokens that must be present in the request, and reject requests without the proper tokens.

Parameters:

  • RequestInterface

    request

  • mixed

    conditions

Returns:

  • void



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
# File 'lib/tilia/dav/locks/plugin.rb', line 334

def validate_tokens(request, conditions_box)
  conditions = conditions_box.value

  # First we need to gather a list of locks that must be satisfied.
  must_locks = []
  method = request.method

  # Methods not in that list are operations that doesn't alter any
  # resources, and we don't need to check the lock-states for.
  case method
  when 'DELETE'
    must_locks += locks(request.path, true)
  when 'MKCOL', 'MKCALENDAR', 'PROPPATCH', 'PUT', 'PATCH'
    must_locks += locks(request.path, false)
  when 'MOVE'
    must_locks += locks(request.path, true)
    must_locks += locks(@server.calculate_uri(request.header('Destination')), false)
  when 'COPY'
    must_locks += locks(@server.calculate_uri(request.header('Destination')), false)
  when 'LOCK'
    # Temporary measure.. figure out later why this is needed
    # Here we basically ignore all incoming tokens...
    conditions.each_with_index do |condition, ii|
      condition['tokens'].each_with_index do |_token, jj|
        conditions[ii]['tokens'][jj]['validToken'] = true
      end
    end
    conditions_box.value = conditions
    return nil
  end

  # It's possible that there's identical locks, because of shared
  # parents. We're removing the duplicates here.
  tmp = {}
  must_locks.each do |lock|
    tmp[lock.token] = lock
  end
  must_locks = tmp.values

  conditions.each_with_index do |condition, kk|
    condition['tokens'].each_with_index do |token, ii|
      # Lock tokens always start with opaquelocktoken:
      next unless token['token'][0, 16] == 'opaquelocktoken:'

      check_token = token['token'][16..-1]

      # Looping through our list with locks.
      skip = false
      must_locks.each_with_index do |must_lock, jj|
        next unless must_lock.token == check_token
        must_locks.delete_at(jj)

        # Marking the condition as valid.
        conditions[kk]['tokens'][ii]['validToken'] = true

        # Advancing to the next token
        skip = true
        break
      end
      next if skip

      # If we got here, it means that there was a
      # lock-token, but it was not in 'mustLocks'.
      #
      # This is an edge-case, as it could mean that token
      # was specified with a url that was not 'required' to
      # check. So we're doing one extra lookup to make sure
      # we really don't know this token.
      #
      # This also gets triggered when the user specified a
      # lock-token that was expired.
      odd_locks = locks(condition['uri'])
      odd_locks.each do |odd_lock|
        next unless odd_lock.token == check_token
        conditions[kk]['tokens'][ii]['validToken'] = true
        skip = true
        break
      end
      next if skip

      # If we get all the way here, the lock-token was
      # really unknown.
    end
  end
  conditions_box.value = conditions

  # If there's any locks left in the 'mustLocks' array, it means that
  # the resource was locked and we must block it.
  fail Exception::Locked.new(must_locks.first) if must_locks.any?
end