class Tilia::Dav::Sync::Plugin

This plugin all WebDAV-sync capabilities to the Server.

WebDAV-sync is defined by rfc6578

The sync capabilities only work with collections that implement SabreDAVSyncISyncCollection.

Constants

SYNCTOKEN_PREFIX

Reference to server object

@var DAVServer RUBY: attr_accessor :server

Public Instance Methods

plugin_info() click to toggle source

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.

@return array

# File lib/tilia/dav/sync/plugin.rb, line 215
def plugin_info
  {
    'name'        => plugin_name,
    'description' => 'Adds support for WebDAV Collection Sync (rfc6578)',
    'link'        => 'http://sabre.io/dav/sync/'
  }
end
plugin_name() click to toggle source

Returns a plugin name.

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

@return string

# File lib/tilia/dav/sync/plugin.rb, line 24
def plugin_name
  'sync'
end
prop_find(prop_find, node) click to toggle source

This method is triggered whenever properties are requested for a node. We intercept this to see if we must return a {DAV:}sync-token.

@param DAVPropFind prop_find @param DAVINode node @return void

# File lib/tilia/dav/sync/plugin.rb, line 163
def prop_find(prop_find, node)
  prop_find.handle(
    '{DAV:}sync-token',
    lambda do
      if !node.is_a?(ISyncCollection)
        return nil
      else
        token = node.sync_token
        return nil unless token
      end
      return SYNCTOKEN_PREFIX + token.to_s
    end
  )
end
setup(server) click to toggle source

Initializes the plugin.

This is when the plugin registers it's hooks.

@param DAVServer server @return void

# File lib/tilia/dav/sync/plugin.rb, line 34
def setup(server)
  @server = server
  server.xml.element_map['{DAV:}sync-collection'] = Xml::Request::SyncCollectionReport

  server.on(
    'report',
    lambda do |report_name, dom, uri|
      if report_name == '{DAV:}sync-collection'
        @server.transaction_type = 'report-sync-collection'
        sync_collection(uri, dom)
        return false
      end
    end
  )

  server.on('propFind',       method(:prop_find))
  server.on('validateTokens', method(:validate_tokens))
end
supported_report_set(uri) click to toggle source

Returns a list of reports this plugin supports.

This will be used in the {DAV:}supported-report-set property. Note that you still need to subscribe to the 'report' event to actually implement them

@param string uri @return array

# File lib/tilia/dav/sync/plugin.rb, line 61
def supported_report_set(uri)
  node = @server.tree.node_for_path(uri)
  if node.is_a?(ISyncCollection) && node.sync_token
    return ['{DAV:}sync-collection']
  end

  []
end
sync_collection(uri, report) click to toggle source

This method handles the {DAV:}sync-collection HTTP REPORT.

@param string uri @param SyncCollectionReport report @return void

# File lib/tilia/dav/sync/plugin.rb, line 75
def sync_collection(uri, report)
  # Getting the data
  node = @server.tree.node_for_path(uri)
  unless node.is_a?(ISyncCollection)
    fail Exception::ReportNotSupported, 'The {DAV:}sync-collection REPORT is not supported on this url.'
  end

  token = node.sync_token
  unless token
    fail Exception::ReportNotSupported, 'No sync information is available at this node'
  end

  sync_token = report.sync_token
  if sync_token
    # Sync-token must start with our prefix
    unless sync_token[0, SYNCTOKEN_PREFIX.length] == SYNCTOKEN_PREFIX
      fail Exception::InvalidSyncToken, 'Invalid or unknown sync token'
    end

    sync_token = sync_token[SYNCTOKEN_PREFIX.length..-1]
  end

  change_info = node.changes(sync_token, report.sync_level, report.limit)

  unless change_info
    fail Exception::InvalidSyncToken, 'Invalid or unknown sync token'
  end

  # Encoding the response
  send_sync_collection_response(
    change_info['syncToken'],
    uri,
    change_info['added'],
    change_info['modified'],
    change_info['deleted'],
    report.properties
  )
end
validate_tokens(_request, conditions_box) click to toggle source

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.

@param RequestInterface request @param array conditions @return void

# File lib/tilia/dav/sync/plugin.rb, line 186
def validate_tokens(_request, conditions_box)
  conditions = conditions_box.value
  conditions.each_with_index do |condition, kk|
    condition['tokens'].each_with_index do |token, ii|
      # Sync-tokens must always start with our designated prefix.
      if token['token'][0, SYNCTOKEN_PREFIX.length] != SYNCTOKEN_PREFIX
        next
      end

      # Checking if the token is a match.
      node = @server.tree.node_for_path(condition['uri'])

      if node.is_a?(ISyncCollection) && node.sync_token.to_s == token['token'][SYNCTOKEN_PREFIX.length..-1]
        conditions[kk]['tokens'][ii]['validToken'] = true
      end
    end
  end
  conditions_box.value = conditions
end

Protected Instance Methods

send_sync_collection_response(sync_token, collection_url, added, modified, deleted, properties) click to toggle source

Sends the response to a sync-collection request.

@param string sync_token @param string collection_url @param array added @param array modified @param array deleted @param array properties @return void

# File lib/tilia/dav/sync/plugin.rb, line 125
def send_sync_collection_response(sync_token, collection_url, added, modified, deleted, properties)
  full_paths = []

  # Pre-fetching children, if this is possible.
  (added + modified).each do |item|
    full_path = collection_url + '/' + item
    full_paths << full_path
  end

  responses = []
  @server.properties_for_multiple_paths(full_paths, properties).each do |full_path, props|
    # The 'Property_Response' class is responsible for generating a
    # single {DAV:}response xml element.
    responses << Xml::Element::Response.new(full_path, props)
  end

  # Deleted items also show up as 'responses'. They have no properties,
  # and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'.
  deleted.each do |item|
    full_path = collection_url + '/' + item
    responses << Xml::Element::Response.new(full_path, {}, 404)
  end

  multi_status = Xml::Response::MultiStatus.new(responses, SYNCTOKEN_PREFIX + sync_token.to_s)

  @server.http_response.status = 207
  @server.http_response.update_header('Content-Type', 'application/xml; charset=utf-8')
  @server.http_response.body = @server.xml.write('{DAV:}multistatus', multi_status, @server.base_uri)
end