Class: Tilia::CalDav::IcsExportPlugin

Inherits:
Dav::ServerPlugin show all
Defined in:
lib/tilia/cal_dav/ics_export_plugin.rb

Overview

ICS Exporter

This plugin adds the ability to export entire calendars as .ics files. This is useful for clients that don’t support CalDAV yet. They often do support ics files.

To use this, point a http client to a caldav calendar, and add ?expand to the url.

Further options that can be added to the url:

start=123456789 - Only return events after the given unix timestamp
end=123245679   - Only return events from before the given unix timestamp
expand=1        - Strip timezone information and expand recurring events.
                  If you'd like to expand, you _must_ also specify start
                  and end.

By default this plugin returns data in the text/calendar format (iCalendar 2.0). If you’d like to receive jCal data instead, you can use an Accept header:

Accept: application/calendar+json

Alternatively, you can also specify this in the url using accept=application/calendar+json, or accept=jcal for short. If the url parameter and Accept header is specified, the url parameter wins.

Note that specifying a start or end data implies that only events will be returned. VTODO and VJOURNAL will be stripped.

Instance Method Summary collapse

Methods inherited from Dav::ServerPlugin

#features, #http_methods, #supported_report_set

Instance Method Details

#http_get(request, response) ⇒ Object

Intercepts GET requests on calendar urls ending with ?export.

Parameters:

  • RequestInterface

    request

  • ResponseInterface

    response

Returns:

  • bool



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/tilia/cal_dav/ics_export_plugin.rb', line 62

def http_get(request, response)
  query_params = request.query_parameters
  return true unless query_params.key?('export')

  path = request.path

  node = @server.properties(
    path,
    [
      '{DAV:}resourcetype',
      '{DAV:}displayname',
      '{http://sabredav.org/ns}sync-token',
      '{DAV:}sync-token',
      '{http://apple.com/ns/ical/}calendar-color'
    ]
  )

  return true unless node.key?('{DAV:}resourcetype') && node['{DAV:}resourcetype'].is("{#{Plugin::NS_CALDAV}}calendar")

  # Marking the transactionType, for logging purposes.
  @server.transaction_type = 'get-calendar-export'

  properties = node

  start = nil
  ending = nil
  expand = false
  component_type = ''

  if query_params.key?('start')
    fail Dav::Exception::BadRequest, 'The start= parameter must contain a unix timestamp' unless query_params['start'] =~ /^\d+$/

    start = Time.zone.at(query_params['start'].to_i)
  end

  if query_params.key?('end')
    fail Dav::Exception::BadRequest, 'The end= parameter must contain a unix timestamp' unless query_params['end'] =~ /^\d+$/

    ending = Time.zone.at(query_params['end'].to_i)
  end

  unless query_params['expand'].blank?
    fail Dav::Exception::BadRequest, 'If you\'d like to expand recurrences, you must specify both a start= and end= parameter.' unless start && ending

    expand = true
    component_type = 'VEVENT'
  end

  if query_params.key?('componentType')
    unless %w(VEVENT VTODO VJOURNAL).include?(query_params['componentType'])
      fail Dav::Exception::BadRequest, "You are not allowed to search for components of type: #{query_params['componentType']} here"
    end

    component_type = query_params['componentType']
  end

  format = Http::Util.negotiate(
    request.header('Accept'),
    [
      'text/calendar',
      'application/calendar+json'
    ]
  )

  if query_params.key?('accept')
    if query_params['accept'] == 'application/calendar+json' || query_params['accept'] == 'jcal'
      format = 'application/calendar+json'
    end
  end

  format = 'text/calendar' if format.blank?

  generate_response(path, start, ending, expand, component_type, format, properties, response)

  # Returning false to break the event chain
  false
end

#merge_objects(properties, input_objects) ⇒ Object

Merges all calendar objects, and builds one big iCalendar blob.

Parameters:

  • array

    properties Some CalDAV properties

  • array

    input_objects

Returns:

  • VObjectComponentVCalendar



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/tilia/cal_dav/ics_export_plugin.rb', line 249

def merge_objects(properties, input_objects)
  calendar = VObject::Component::VCalendar.new
  calendar['VERSION'] = '2.0'

  if Dav::Server.expose_version
    calendar['PRODID'] = "-//TiliaDAV//TiliaDAV #{Dav::Version::VERSION}//EN"
  else
    calendar['PRODID'] = '-//SabreDAV//SabreDAV//EN'
  end

  if properties.key?('{DAV:}displayname')
    calendar['X-WR-CALNAME'] = properties['{DAV:}displayname']
  end
  if properties.key?('{http://apple.com/ns/ical/}calendar-color')
    calendar['X-APPLE-CALENDAR-COLOR'] = properties['{http://apple.com/ns/ical/}calendar-color']
  end

  collected_timezones = []

  timezones = []
  objects = []

  input_objects.each do |_href, input_object|
    node_comp = VObject::Reader.read(input_object)

    node_comp.children.each do |child|
      case child.name
      when 'VEVENT', 'VTODO', 'VJOURNAL'
        objects << child.clone
      # VTIMEZONE is special, because we need to filter out the duplicates
      when 'VTIMEZONE'
        # Naively just checking tzid.
        next if collected_timezones.include?(child['TZID'].to_s)

        timezones << child.clone
        collected_timezones << child['TZID'].to_s
      end
    end

    # Destroy circular references to PHP will GC the object.
    node_comp.destroy
    node_comp = nil
  end

  timezones.each { |tz| calendar.add(tz) }
  objects.each { |obj| calendar.add(obj) }

  calendar
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



318
319
320
321
322
323
324
# File 'lib/tilia/cal_dav/ics_export_plugin.rb', line 318

def plugin_info
  {
    'name'        => plugin_name,
    'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.',
    'link'        => 'http://sabre.io/dav/ics-export-plugin/'
  }
end

#plugin_nameObject

Returns a plugin name.

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

Returns:

  • string



305
306
307
# File 'lib/tilia/cal_dav/ics_export_plugin.rb', line 305

def plugin_name
  'ics-export'
end

#setup(server) ⇒ Object

Initializes the plugin and registers event handlers

Parameters:

  • \Sabre\DAV\Server

    server

Returns:

  • void



42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/tilia/cal_dav/ics_export_plugin.rb', line 42

def setup(server)
  @server = server
  @server.on('method:GET', method(:http_get), 90)
  @server.on(
    'browserButtonActions',
    lambda do |path, node, actions|
      if node.is_a?(ICalendar)
        actions.value += '<a href="'
        actions.value += CGI.escapeHTML(path)
        actions.value += '?export"><span class="oi" data-glyph="calendar"></span></a>'
      end
    end
  )
end