tilia-dav 3.1.0.pre.alpha6 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.sabre.md +97 -1
  4. data/Gemfile +1 -16
  5. data/Gemfile.lock +44 -41
  6. data/LICENSE +1 -1
  7. data/LICENSE.sabre +1 -1
  8. data/examples/addressbookserver.rb +52 -0
  9. data/examples/calendarserver.rb +58 -0
  10. data/examples/fileserver.rb +57 -0
  11. data/examples/groupwareserver.rb +69 -0
  12. data/examples/sql/mysql.addressbook.sql +28 -0
  13. data/examples/sql/mysql.calendars.sql +64 -0
  14. data/examples/sql/mysql.locks.sql +13 -0
  15. data/examples/sql/mysql.principals.sql +21 -0
  16. data/examples/sql/mysql.propertystorage.sql +8 -0
  17. data/examples/sql/mysql.users.sql +9 -0
  18. data/examples/sql/pgsql.addressbook.sql +52 -0
  19. data/examples/sql/pgsql.calendars.sql +93 -0
  20. data/examples/sql/pgsql.locks.sql +19 -0
  21. data/examples/sql/pgsql.principals.sql +38 -0
  22. data/examples/sql/pgsql.propertystorage.sql +13 -0
  23. data/examples/sql/pgsql.users.sql +14 -0
  24. data/examples/sql/sqlite.addressbooks.sql +28 -0
  25. data/examples/sql/sqlite.calendars.sql +64 -0
  26. data/examples/sql/sqlite.locks.sql +12 -0
  27. data/examples/sql/sqlite.principals.sql +20 -0
  28. data/examples/sql/sqlite.propertystorage.sql +10 -0
  29. data/examples/sql/sqlite.users.sql +9 -0
  30. data/lib/tilia/cal_dav/ics_export_plugin.rb +1 -1
  31. data/lib/tilia/cal_dav/plugin.rb +27 -11
  32. data/lib/tilia/cal_dav/schedule/i_mip_plugin.rb +2 -2
  33. data/lib/tilia/cal_dav/schedule/plugin.rb +7 -0
  34. data/lib/tilia/dav/auth/backend.rb +1 -0
  35. data/lib/tilia/dav/auth/backend/abstract_basic.rb +3 -2
  36. data/lib/tilia/dav/auth/backend/abstract_bearer.rb +116 -0
  37. data/lib/tilia/dav/auth/backend/abstract_digest.rb +3 -2
  38. data/lib/tilia/dav/auth/backend/apache.rb +2 -1
  39. data/lib/tilia/dav/auth/backend/sequel.rb +2 -9
  40. data/lib/tilia/dav/client.rb +29 -3
  41. data/lib/tilia/dav/core_plugin.rb +1 -2
  42. data/lib/tilia/dav/server.rb +16 -4
  43. data/lib/tilia/dav/temporary_file_filter_plugin.rb +3 -0
  44. data/lib/tilia/dav/tree.rb +4 -3
  45. data/lib/tilia/dav/version.rb +1 -1
  46. data/lib/tilia/dav/xml/element/response.rb +20 -2
  47. data/lib/tilia/dav_acl/principal_backend/sequel.rb +50 -6
  48. data/test/cal_dav/ics_export_plugin_test.rb +1 -0
  49. data/test/cal_dav/plugin_test.rb +4 -4
  50. data/test/cal_dav/schedule/plugin_properties_test.rb +51 -0
  51. data/test/card_dav/backend/sequel_my_sql_test.rb +3 -3
  52. data/test/card_dav/vcf_export_test.rb +11 -1
  53. data/test/dav/auth/backend/abstract_bearer_test.rb +71 -0
  54. data/test/dav/client_test.rb +42 -4
  55. data/test/dav/core_plugin_test.rb +12 -0
  56. data/test/dav/fs_ext/server_test.rb +1 -1
  57. data/test/dav/http_copy_test.rb +185 -0
  58. data/test/dav/mock/collection.rb +6 -9
  59. data/test/dav/mock/file.rb +10 -9
  60. data/test/dav/mock/streaming_file.rb +1 -3
  61. data/test/dav/server_events_test.rb +8 -6
  62. data/test/dav/server_range_test.rb +135 -155
  63. data/test/dav/server_simple_test.rb +14 -0
  64. data/test/dav/xml/element/response_test.rb +56 -1
  65. data/test/dav/xml/property/href_test.rb +14 -0
  66. data/test/dav_acl/principal_backend/abstract_sequel_test.rb +16 -0
  67. data/test/dav_acl/principal_backend/mock.rb +1 -1
  68. data/test/dav_server_test.rb +1 -1
  69. data/tilia-dav.gemspec +2 -2
  70. metadata +38 -14
  71. data/test/dav/copy_test.rb +0 -33
  72. data/test/dav/server_copy_move_test.rb +0 -164
@@ -4,15 +4,6 @@ module Tilia
4
4
  module Backend
5
5
  # This is an authentication backend that uses a database to manage passwords.
6
6
  class Sequel < AbstractDigest
7
- protected
8
-
9
- # Reference to PDO connection
10
- #
11
- # @var PDO
12
- attr_accessor :pdo
13
-
14
- public
15
-
16
7
  # PDO table name we'll be using
17
8
  #
18
9
  # @var string
@@ -26,6 +17,8 @@ module Tilia
26
17
  def initialize(sequel)
27
18
  @sequel = sequel
28
19
  @table_name = 'users'
20
+
21
+ super()
29
22
  end
30
23
 
31
24
  # Returns the digest hash for a user.
@@ -118,6 +118,8 @@ module Tilia
118
118
  add_curl_setting(:encoding, encodings.join(','))
119
119
  end
120
120
 
121
+ add_curl_setting(:useragent, "tilia-dav/#{Version::VERSION} (http://sabre.io/)")
122
+
121
123
  @xml = Xml::Service.new
122
124
  # BC
123
125
  @property_map = @xml.element_map
@@ -181,7 +183,7 @@ module Tilia
181
183
 
182
184
  response = send_request(request)
183
185
 
184
- fail Exception, "HTTP error: #{response.status}" if response.status.to_i >= 400
186
+ fail Http::ClientHttpException.new(response) if response.status.to_i >= 400
185
187
 
186
188
  result = parse_multi_status(response.body_as_string)
187
189
 
@@ -207,7 +209,7 @@ module Tilia
207
209
  #
208
210
  # @param string url
209
211
  # @param array properties
210
- # @return void
212
+ # @return bool
211
213
  def prop_patch(url, properties)
212
214
  prop_patch = Xml::Request::PropPatch.new
213
215
  prop_patch.properties = properties
@@ -220,7 +222,31 @@ module Tilia
220
222
  { 'Content-Type' => 'application/xml' },
221
223
  xml
222
224
  )
223
- send_request(request)
225
+
226
+ response = send_request(request)
227
+
228
+ fail Http::ClientHttpException.new(response) if response.status.to_i >= 400
229
+
230
+ if response.status == 207.to_i
231
+ # If it's a 207, the request could still have failed, but the
232
+ # information is hidden in the response body.
233
+ result = parse_multi_status(response.body_as_string)
234
+
235
+ error_properties = []
236
+ result.each do |href, status_list|
237
+ status_list.each do |status, properties|
238
+ next unless status.to_i >= 400
239
+
240
+ properties.each do |prop_name, prop_value|
241
+ error_properties << "#{prop_name} (#{status})"
242
+ end
243
+ end
244
+ end
245
+
246
+ fail Http::ClientException, "ROPPATCH failed. The following properties errored: #{error_properties.join(', ')}" if error_properties.any?
247
+ end
248
+
249
+ true
224
250
  end
225
251
 
226
252
  # Performs an HTTP options request
@@ -589,13 +589,12 @@ module Tilia
589
589
 
590
590
  copy_info = @server.copy_and_move_info(request)
591
591
 
592
+ return false unless @server.emit('beforeBind', [copy_info['destination']])
592
593
  if copy_info['destinationExists']
593
594
  return false unless @server.emit('beforeUnbind', [copy_info['destination']])
594
595
  @server.tree.delete(copy_info['destination'])
595
596
  end
596
597
 
597
- return false unless @server.emit('beforeBind', [copy_info['destination']])
598
-
599
598
  @server.tree.copy(path, copy_info['destination'])
600
599
 
601
600
  @server.emit('afterBind', [copy_info['destination']])
@@ -327,11 +327,18 @@ module Tilia
327
327
 
328
328
  if emit("method:#{method}", [request, response])
329
329
  if emit('method', [request, response])
330
- # Unsupported method
331
- fail Exception::NotImplemented, "'There was no handler found for this \"#{method}\" method"
330
+ ex_message = "There was no plugin in the system that was willing to handle this #{method} method."
331
+ if method == "GET"
332
+ ex_message += " Enable the Browser plugin to get a better result here."
333
+ end
334
+
335
+ # Unsupported method
336
+ fail Exception::NotImplemented, ex_message
332
337
  end
333
338
  end
334
339
 
340
+ fail Exception, 'No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.' unless response.status
341
+
335
342
  return nil unless emit("afterMethod:#{method}", [request, response])
336
343
  return nil unless emit('afterMethod', [request, response])
337
344
 
@@ -381,7 +388,12 @@ module Tilia
381
388
  calculate_uri(@http_request.url)
382
389
  end
383
390
 
384
- # Calculates the uri for a request, making sure that the base uri is stripped out
391
+ # Turns a URI such as the REQUEST_URI into a local path.
392
+ #
393
+ # This method:
394
+ # * strips off the base path
395
+ # * normalizes the path
396
+ # * uri-decodes the path
385
397
  #
386
398
  # @param string uri
387
399
  # @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
@@ -677,7 +689,7 @@ module Tilia
677
689
  sub_prop_find = prop_find.clone
678
690
  sub_prop_find.depth = new_depth
679
691
 
680
- if path != ''
692
+ if path.present?
681
693
  sub_path = path + '/' + child_node.name
682
694
  else
683
695
  sub_path = child_node.name
@@ -107,6 +107,9 @@ module Tilia
107
107
  #
108
108
  # @param string uri
109
109
  # @param resource data
110
+ # @param DAV\ICollection _parent_node
111
+ # @param bool _modified Should be set to true, if this event handler
112
+ # changed &data.
110
113
  # @return bool
111
114
  def before_create_file(uri, data, _parent, _modified)
112
115
  temp_path = temp_file?(uri)
@@ -49,7 +49,7 @@ module Tilia
49
49
  # Otherwise, we recursively grab the parent and ask him/her.
50
50
  parent = node_for_path(parent_name)
51
51
 
52
- unless parent.is_a? ICollection
52
+ unless parent.is_a?(ICollection)
53
53
  fail Exception::NotFound, "Could not find node at path: #{path}"
54
54
  end
55
55
 
@@ -145,10 +145,11 @@ module Tilia
145
145
  def children(path)
146
146
  node = node_for_path(path)
147
147
  children = node.children
148
- basePath = path.gsub(/^\/+|\/+$/, '') + '/'
148
+ base_path = path.gsub(/^\/+|\/+$/, '')
149
+ base_path += '/' unless base_path.blank?
149
150
 
150
151
  children.each do |child|
151
- cache[basePath + child.name] = child
152
+ cache[base_path + child.name] = child
152
153
  end
153
154
 
154
155
  children
@@ -3,7 +3,7 @@ module Tilia
3
3
  # This class contains the SabreDAV version constants.
4
4
  class Version
5
5
  # Full version number
6
- VERSION = '3.1.0-alpha6'
6
+ VERSION = '3.1.1'.freeze
7
7
  end
8
8
  end
9
9
  end
@@ -81,7 +81,9 @@ module Tilia
81
81
  if @http_status
82
82
  writer.write_element('{DAV:}status', "HTTP/1.1 #{@http_status} #{Tilia::Http::Response.status_codes[@http_status]}")
83
83
  end
84
- writer.write_element('{DAV:}href', writer.context_uri + href)
84
+ writer.write_element('{DAV:}href', writer.context_uri + Http::encode_path(href))
85
+
86
+ empty = true
85
87
 
86
88
  @response_properties.each do |status, properties|
87
89
  # Skipping empty lists
@@ -90,11 +92,27 @@ module Tilia
90
92
  next if properties.blank?
91
93
  next if status.to_i.to_s != status.to_s
92
94
 
95
+ empty = false
96
+
93
97
  writer.start_element('{DAV:}propstat')
94
98
  writer.write_element('{DAV:}prop', properties)
95
99
  writer.write_element('{DAV:}status', "HTTP/1.1 #{status} #{Tilia::Http::Response.status_codes[status]}")
96
100
  writer.end_element # {DAV:}propstat
97
101
  end
102
+
103
+ if empty
104
+ # The WebDAV spec _requires_ at least one DAV:propstat to appear for
105
+ # every DAV:response. In some circumstances however, there are no
106
+ # properties to encode.
107
+ #
108
+ # In those cases we MUST specify at least one DAV:propstat anyway, with
109
+ # no properties.
110
+ writer.write_element(
111
+ '{DAV:}propstat',
112
+ '{DAV:}prop' => [],
113
+ '{DAV:}status' => "HTTP/1.1 418 #{Http::Response.status_codes[418]}"
114
+ )
115
+ end
98
116
  end
99
117
 
100
118
  # The deserialize method is called during xml parsing.
@@ -173,7 +191,7 @@ module Tilia
173
191
  status = elem['value']['{DAV:}status']
174
192
  status = status.split(' ')[1]
175
193
  properties = elem['value'].key?('{DAV:}prop') ? elem['value']['{DAV:}prop'] : []
176
- property_lists[status] = properties
194
+ property_lists[status] = properties if properties.any?
177
195
  when '{DAV:}status'
178
196
  status_code = elem['value'].split(' ')[1]
179
197
  end
@@ -198,22 +198,26 @@ module Tilia
198
198
  # @param array search_properties
199
199
  # @param string test
200
200
  # @return array
201
- def search_principals(prefix_path, search_properties, _test = 'allof')
202
- query = "SELECT uri FROM #{@table_name} WHERE 1=1 "
201
+ def search_principals(prefix_path, search_properties, test = 'allof')
202
+ return [] if search_properties.empty? # No criteria
203
+
204
+ query = "SELECT uri FROM #{@table_name} WHERE "
203
205
  values = []
204
206
 
205
207
  search_properties.each do |property, value|
206
208
  case property
207
209
  when '{DAV:}displayname'
208
- query << ' AND displayname LIKE ?'
209
- values << "%#{value}%"
210
+ column = 'displayname'
210
211
  when '{http://sabredav.org/ns}email-address'
211
- query << ' AND email LIKE ?'
212
- values << "%#{value}%"
212
+ column = 'email'
213
213
  else
214
214
  # Unsupported property
215
215
  return []
216
216
  end
217
+
218
+ query += test == 'anyof' ? ' OR ' : ' AND ' if values.any?
219
+ query += "lower(#{column}) LIKE lower(?)"
220
+ values << "%#{value}%"
217
221
  end
218
222
 
219
223
  principals = []
@@ -228,6 +232,46 @@ module Tilia
228
232
  principals
229
233
  end
230
234
 
235
+ # Finds a principal by its URI.
236
+ #
237
+ # This method may receive any type of uri, but mailto: addresses will be
238
+ # the most common.
239
+ #
240
+ # Implementation of this API is optional. It is currently used by the
241
+ # CalDAV system to find principals based on their email addresses. If this
242
+ # API is not implemented, some features may not work correctly.
243
+ #
244
+ # This method must return a relative principal path, or null, if the
245
+ # principal was not found or you refuse to find it.
246
+ #
247
+ # @param string uri
248
+ # @param string principal_prefix
249
+ # @return string, nil
250
+ def find_by_uri(uri, principal_prefix)
251
+ value = nil
252
+ scheme = nil
253
+ (scheme, value) = uri.split(":", 2)
254
+ return nil unless value
255
+
256
+ uri = nil
257
+ case scheme
258
+ when "mailto"
259
+ @sequel.fetch("SELECT uri FROM #{@table_name} WHERE lower(email)=lower(?)", [value]) do |row|
260
+ # Checking if the principal is in the prefix
261
+ row_prefix = Http::UrlUtil.split_path(row[:uri]).first
262
+ next unless row_prefix == principal_prefix
263
+
264
+ uri = row[:uri]
265
+ break # Stop on first match
266
+ end
267
+ else
268
+ #unsupported uri scheme
269
+ return nil
270
+ end
271
+
272
+ uri
273
+ end
274
+
231
275
  # Returns the list of members for a group-principal
232
276
  #
233
277
  # @param string principal
@@ -8,6 +8,7 @@ module Tilia
8
8
  server = Dav::ServerMock.new
9
9
  server.add_plugin(plugin)
10
10
  assert_equal(plugin, server.plugin('ics-export'))
11
+ assert_equal('ics-export', plugin.plugin_info['name'])
11
12
  end
12
13
 
13
14
  def test_before_method
@@ -593,7 +593,7 @@ XML
593
593
  utc = ActiveSupport::TimeZone.new('UTC')
594
594
  expected_ical = DatabaseUtil.get_test_calendar_data
595
595
  expected_ical = VObject::Reader.read(expected_ical)
596
- expected_ical.expand(
596
+ expected_ical = expected_ical.expand(
597
597
  utc.parse('2011-01-01 00:00:00'),
598
598
  utc.parse('2011-12-31 23:59:59')
599
599
  )
@@ -651,7 +651,7 @@ XML
651
651
  utc = ActiveSupport::TimeZone.new('UTC')
652
652
  expected_ical = DatabaseUtil.get_test_calendar_data
653
653
  expected_ical = VObject::Reader.read(expected_ical)
654
- expected_ical.expand(
654
+ expected_ical = expected_ical.expand(
655
655
  utc.parse('2000-01-01 00:00:00'),
656
656
  utc.parse('2010-12-31 23:59:59')
657
657
  )
@@ -711,7 +711,7 @@ XML
711
711
  utc = ActiveSupport::TimeZone.new('UTC')
712
712
  expected_ical = DatabaseUtil.get_test_calendar_data
713
713
  expected_ical = VObject::Reader.read(expected_ical)
714
- expected_ical.expand(
714
+ expected_ical = expected_ical.expand(
715
715
  utc.parse('2000-01-01 00:00:00'),
716
716
  utc.parse('2010-12-31 23:59:59')
717
717
  )
@@ -865,7 +865,7 @@ XML
865
865
  utc = ActiveSupport::TimeZone.new('UTC')
866
866
  expected_ical = DatabaseUtil.get_test_calendar_data
867
867
  expected_ical = VObject::Reader.read(expected_ical)
868
- expected_ical.expand(
868
+ expected_ical = expected_ical.expand(
869
869
  utc.parse('2000-01-01 00:00:00'),
870
870
  utc.parse('2010-12-31 23:59:59')
871
871
  )
@@ -16,6 +16,9 @@ module Tilia
16
16
  'default',
17
17
  {}
18
18
  )
19
+ @principal_backend.add_principal(
20
+ 'uri' => 'principals/user1/calendar-proxy-read'
21
+ )
19
22
  end
20
23
 
21
24
  def test_principal_properties
@@ -57,6 +60,54 @@ module Tilia
57
60
  assert_equal('calendars/user1/default/', prop.href)
58
61
  end
59
62
 
63
+ def test_principal_properties_bad_principal
64
+ props = @server.properties_for_path(
65
+ 'principals/user1/calendar-proxy-read',
66
+ [
67
+ '{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL',
68
+ '{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL',
69
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-address-set',
70
+ '{urn:ietf:params:xml:ns:caldav}calendar-user-type',
71
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL',
72
+ ]
73
+ )
74
+
75
+ assert(props[0])
76
+ assert_has_key(200, props[0])
77
+ assert_has_key(404, props[0])
78
+
79
+ assert_has_key('{urn:ietf:params:xml:ns:caldav}schedule-outbox-URL', props[0][404])
80
+ assert_has_key('{urn:ietf:params:xml:ns:caldav}schedule-inbox-URL', props[0][404])
81
+
82
+ prop = props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-address-set']
83
+ assert(prop.is_a?(Dav::Xml::Property::Href))
84
+ assert_equal(['/principals/user1/calendar-proxy-read/'], prop.hrefs)
85
+
86
+ assert_has_key('{urn:ietf:params:xml:ns:caldav}calendar-user-type', props[0][200])
87
+ prop = props[0][200]['{urn:ietf:params:xml:ns:caldav}calendar-user-type']
88
+ assert_equal('INDIVIDUAL', prop)
89
+
90
+ assert_has_key('{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', props[0][404])
91
+ end
92
+
93
+ def test_no_default_calendar
94
+ @caldav_backend.calendars_for_user('principals/user1').each do |calendar|
95
+ @caldav_backend.delete_calendar(calendar['id'])
96
+ end
97
+
98
+ props = @server.properties_for_path(
99
+ '/principals/user1',
100
+ [
101
+ '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'
102
+ ]
103
+ )
104
+
105
+ assert(props[0])
106
+ assert_has_key(404, props[0])
107
+
108
+ assert_has_key('{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL', props[0][404])
109
+ end
110
+
60
111
  # There are two properties for availability. The server should
61
112
  # automatically map the old property to the standard property.
62
113
  def test_availability_mapping
@@ -21,7 +21,7 @@ CREATE TABLE addressbooks (
21
21
  description VARCHAR(10000),
22
22
  synctoken INT(11) UNSIGNED NOT NULL DEFAULT '1',
23
23
  UNIQUE(principaluri(100), uri(100))
24
- ) #{TestUtil.mysql_engine} DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
24
+ ) #{TestUtil.mysql_engine} DEFAULT CHARSET=utf8mb4
25
25
  QUERY
26
26
  )
27
27
  db.run(<<QUERY
@@ -33,7 +33,7 @@ CREATE TABLE cards (
33
33
  lastmodified INT(11) UNSIGNED,
34
34
  etag VARBINARY(32),
35
35
  size INT(11) UNSIGNED NOT NULL
36
- ) #{TestUtil.mysql_engine} DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
36
+ ) #{TestUtil.mysql_engine} DEFAULT CHARSET=utf8mb4
37
37
  QUERY
38
38
  )
39
39
  db.run(<<QUERY
@@ -44,7 +44,7 @@ CREATE TABLE addressbookchanges (
44
44
  addressbookid INT(11) UNSIGNED NOT NULL,
45
45
  operation SMALLINT(2) NOT NULL,
46
46
  INDEX addressbookid_synctoken (addressbookid, synctoken)
47
- ) #{TestUtil.mysql_engine} DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci
47
+ ) #{TestUtil.mysql_engine} DEFAULT CHARSET=utf8mb4
48
48
  QUERY
49
49
  )
50
50