tilia-dav 3.1.0.pre.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (499) hide show
  1. checksums.yaml +7 -0
  2. data/.database.travis.yml +6 -0
  3. data/.gitignore +25 -0
  4. data/.rubocop.yml +35 -0
  5. data/.simplecov +4 -0
  6. data/.travis.yml +10 -0
  7. data/CHANGELOG.sabre.md +2084 -0
  8. data/CONTRIBUTING.md +25 -0
  9. data/Gemfile +25 -0
  10. data/Gemfile.lock +103 -0
  11. data/LICENSE +27 -0
  12. data/LICENSE.sabre +27 -0
  13. data/README.md +40 -0
  14. data/Rakefile +18 -0
  15. data/database.sample.yml +6 -0
  16. data/examples/minimal.rb +25 -0
  17. data/lib/tilia/cal_dav.rb +27 -0
  18. data/lib/tilia/cal_dav/backend.rb +17 -0
  19. data/lib/tilia/cal_dav/backend/abstract_backend.rb +194 -0
  20. data/lib/tilia/cal_dav/backend/backend_interface.rb +250 -0
  21. data/lib/tilia/cal_dav/backend/notification_support.rb +38 -0
  22. data/lib/tilia/cal_dav/backend/scheduling_support.rb +57 -0
  23. data/lib/tilia/cal_dav/backend/sequel.rb +1118 -0
  24. data/lib/tilia/cal_dav/backend/sharing_support.rb +239 -0
  25. data/lib/tilia/cal_dav/backend/subscription_support.rb +79 -0
  26. data/lib/tilia/cal_dav/backend/sync_support.rb +75 -0
  27. data/lib/tilia/cal_dav/calendar.rb +426 -0
  28. data/lib/tilia/cal_dav/calendar_home.rb +335 -0
  29. data/lib/tilia/cal_dav/calendar_object.rb +219 -0
  30. data/lib/tilia/cal_dav/calendar_query_validator.rb +294 -0
  31. data/lib/tilia/cal_dav/calendar_root.rb +57 -0
  32. data/lib/tilia/cal_dav/exception.rb +7 -0
  33. data/lib/tilia/cal_dav/exception/invalid_component_type.rb +21 -0
  34. data/lib/tilia/cal_dav/i_calendar.rb +11 -0
  35. data/lib/tilia/cal_dav/i_calendar_object.rb +13 -0
  36. data/lib/tilia/cal_dav/i_calendar_object_container.rb +32 -0
  37. data/lib/tilia/cal_dav/i_shareable_calendar.rb +40 -0
  38. data/lib/tilia/cal_dav/i_shared_calendar.rb +28 -0
  39. data/lib/tilia/cal_dav/ics_export_plugin.rb +327 -0
  40. data/lib/tilia/cal_dav/notifications.rb +12 -0
  41. data/lib/tilia/cal_dav/notifications/collection.rb +131 -0
  42. data/lib/tilia/cal_dav/notifications/i_collection.rb +17 -0
  43. data/lib/tilia/cal_dav/notifications/i_node.rb +30 -0
  44. data/lib/tilia/cal_dav/notifications/node.rb +142 -0
  45. data/lib/tilia/cal_dav/notifications/plugin.rb +138 -0
  46. data/lib/tilia/cal_dav/plugin.rb +891 -0
  47. data/lib/tilia/cal_dav/principal.rb +12 -0
  48. data/lib/tilia/cal_dav/principal/collection.rb +21 -0
  49. data/lib/tilia/cal_dav/principal/i_proxy_read.rb +13 -0
  50. data/lib/tilia/cal_dav/principal/i_proxy_write.rb +13 -0
  51. data/lib/tilia/cal_dav/principal/proxy_read.rb +127 -0
  52. data/lib/tilia/cal_dav/principal/proxy_write.rb +127 -0
  53. data/lib/tilia/cal_dav/principal/user.rb +96 -0
  54. data/lib/tilia/cal_dav/schedule.rb +14 -0
  55. data/lib/tilia/cal_dav/schedule/i_inbox.rb +12 -0
  56. data/lib/tilia/cal_dav/schedule/i_mip_plugin.rb +156 -0
  57. data/lib/tilia/cal_dav/schedule/i_outbox.rb +12 -0
  58. data/lib/tilia/cal_dav/schedule/i_scheduling_object.rb +10 -0
  59. data/lib/tilia/cal_dav/schedule/inbox.rb +211 -0
  60. data/lib/tilia/cal_dav/schedule/outbox.rb +143 -0
  61. data/lib/tilia/cal_dav/schedule/plugin.rb +851 -0
  62. data/lib/tilia/cal_dav/schedule/scheduling_object.rb +126 -0
  63. data/lib/tilia/cal_dav/shareable_calendar.rb +54 -0
  64. data/lib/tilia/cal_dav/shared_calendar.rb +120 -0
  65. data/lib/tilia/cal_dav/sharing_plugin.rb +359 -0
  66. data/lib/tilia/cal_dav/subscriptions.rb +9 -0
  67. data/lib/tilia/cal_dav/subscriptions/i_subscription.rb +37 -0
  68. data/lib/tilia/cal_dav/subscriptions/plugin.rb +83 -0
  69. data/lib/tilia/cal_dav/subscriptions/subscription.rb +205 -0
  70. data/lib/tilia/cal_dav/xml.rb +10 -0
  71. data/lib/tilia/cal_dav/xml/filter.rb +12 -0
  72. data/lib/tilia/cal_dav/xml/filter/calendar_data.rb +64 -0
  73. data/lib/tilia/cal_dav/xml/filter/comp_filter.rb +79 -0
  74. data/lib/tilia/cal_dav/xml/filter/param_filter.rb +66 -0
  75. data/lib/tilia/cal_dav/xml/filter/prop_filter.rb +80 -0
  76. data/lib/tilia/cal_dav/xml/notification.rb +13 -0
  77. data/lib/tilia/cal_dav/xml/notification/invite.rb +253 -0
  78. data/lib/tilia/cal_dav/xml/notification/invite_reply.rb +167 -0
  79. data/lib/tilia/cal_dav/xml/notification/notification_interface.rb +41 -0
  80. data/lib/tilia/cal_dav/xml/notification/system_status.rb +139 -0
  81. data/lib/tilia/cal_dav/xml/property.rb +15 -0
  82. data/lib/tilia/cal_dav/xml/property/allowed_sharing_modes.rb +64 -0
  83. data/lib/tilia/cal_dav/xml/property/email_address_set.rb +60 -0
  84. data/lib/tilia/cal_dav/xml/property/invite.rb +207 -0
  85. data/lib/tilia/cal_dav/xml/property/schedule_calendar_transp.rb +108 -0
  86. data/lib/tilia/cal_dav/xml/property/supported_calendar_component_set.rb +100 -0
  87. data/lib/tilia/cal_dav/xml/property/supported_calendar_data.rb +50 -0
  88. data/lib/tilia/cal_dav/xml/property/supported_collation_set.rb +47 -0
  89. data/lib/tilia/cal_dav/xml/request.rb +14 -0
  90. data/lib/tilia/cal_dav/xml/request/calendar_multi_get_report.rb +99 -0
  91. data/lib/tilia/cal_dav/xml/request/calendar_query_report.rb +112 -0
  92. data/lib/tilia/cal_dav/xml/request/free_busy_query_report.rb +70 -0
  93. data/lib/tilia/cal_dav/xml/request/invite_reply.rb +110 -0
  94. data/lib/tilia/cal_dav/xml/request/mk_calendar.rb +67 -0
  95. data/lib/tilia/cal_dav/xml/request/share.rb +93 -0
  96. data/lib/tilia/card_dav.rb +17 -0
  97. data/lib/tilia/card_dav/address_book.rb +338 -0
  98. data/lib/tilia/card_dav/address_book_home.rb +192 -0
  99. data/lib/tilia/card_dav/address_book_root.rb +58 -0
  100. data/lib/tilia/card_dav/backend.rb +12 -0
  101. data/lib/tilia/card_dav/backend/abstract_backend.rb +30 -0
  102. data/lib/tilia/card_dav/backend/backend_interface.rb +175 -0
  103. data/lib/tilia/card_dav/backend/sequel.rb +476 -0
  104. data/lib/tilia/card_dav/backend/sync_support.rb +80 -0
  105. data/lib/tilia/card_dav/card.rb +193 -0
  106. data/lib/tilia/card_dav/i_address_book.rb +10 -0
  107. data/lib/tilia/card_dav/i_card.rb +11 -0
  108. data/lib/tilia/card_dav/i_directory.rb +14 -0
  109. data/lib/tilia/card_dav/plugin.rb +724 -0
  110. data/lib/tilia/card_dav/vcf_export_plugin.rb +122 -0
  111. data/lib/tilia/card_dav/xml.rb +9 -0
  112. data/lib/tilia/card_dav/xml/filter.rb +11 -0
  113. data/lib/tilia/card_dav/xml/filter/address_data.rb +50 -0
  114. data/lib/tilia/card_dav/xml/filter/param_filter.rb +71 -0
  115. data/lib/tilia/card_dav/xml/filter/prop_filter.rb +77 -0
  116. data/lib/tilia/card_dav/xml/property.rb +10 -0
  117. data/lib/tilia/card_dav/xml/property/supported_address_data.rb +67 -0
  118. data/lib/tilia/card_dav/xml/property/supported_collation_set.rb +38 -0
  119. data/lib/tilia/card_dav/xml/request.rb +10 -0
  120. data/lib/tilia/card_dav/xml/request/address_book_multi_get_report.rb +91 -0
  121. data/lib/tilia/card_dav/xml/request/address_book_query_report.rb +156 -0
  122. data/lib/tilia/dav.rb +94 -0
  123. data/lib/tilia/dav/auth.rb +8 -0
  124. data/lib/tilia/dav/auth/backend.rb +15 -0
  125. data/lib/tilia/dav/auth/backend/abstract_basic.rb +119 -0
  126. data/lib/tilia/dav/auth/backend/abstract_digest.rb +132 -0
  127. data/lib/tilia/dav/auth/backend/apache.rb +85 -0
  128. data/lib/tilia/dav/auth/backend/backend_interface.rb +61 -0
  129. data/lib/tilia/dav/auth/backend/basic_call_back.rb +46 -0
  130. data/lib/tilia/dav/auth/backend/file.rb +61 -0
  131. data/lib/tilia/dav/auth/backend/sequel.rb +46 -0
  132. data/lib/tilia/dav/auth/plugin.rb +157 -0
  133. data/lib/tilia/dav/browser.rb +12 -0
  134. data/lib/tilia/dav/browser/assets/favicon.ico +0 -0
  135. data/lib/tilia/dav/browser/assets/openiconic/ICON-LICENSE +21 -0
  136. data/lib/tilia/dav/browser/assets/openiconic/open-iconic.css +510 -0
  137. data/lib/tilia/dav/browser/assets/openiconic/open-iconic.eot +0 -0
  138. data/lib/tilia/dav/browser/assets/openiconic/open-iconic.otf +0 -0
  139. data/lib/tilia/dav/browser/assets/openiconic/open-iconic.svg +543 -0
  140. data/lib/tilia/dav/browser/assets/openiconic/open-iconic.ttf +0 -0
  141. data/lib/tilia/dav/browser/assets/openiconic/open-iconic.woff +0 -0
  142. data/lib/tilia/dav/browser/assets/sabredav.css +228 -0
  143. data/lib/tilia/dav/browser/assets/sabredav.png +0 -0
  144. data/lib/tilia/dav/browser/guess_content_type.rb +80 -0
  145. data/lib/tilia/dav/browser/html_output.rb +27 -0
  146. data/lib/tilia/dav/browser/html_output_helper.rb +86 -0
  147. data/lib/tilia/dav/browser/map_get_to_prop_find.rb +41 -0
  148. data/lib/tilia/dav/browser/plugin.rb +693 -0
  149. data/lib/tilia/dav/browser/prop_find_all.rb +95 -0
  150. data/lib/tilia/dav/client.rb +341 -0
  151. data/lib/tilia/dav/collection.rb +79 -0
  152. data/lib/tilia/dav/core_plugin.rb +824 -0
  153. data/lib/tilia/dav/exception.rb +59 -0
  154. data/lib/tilia/dav/exception/bad_request.rb +18 -0
  155. data/lib/tilia/dav/exception/conflict.rb +18 -0
  156. data/lib/tilia/dav/exception/conflicting_lock.rb +26 -0
  157. data/lib/tilia/dav/exception/forbidden.rb +18 -0
  158. data/lib/tilia/dav/exception/insufficient_storage.rb +18 -0
  159. data/lib/tilia/dav/exception/invalid_resource_type.rb +23 -0
  160. data/lib/tilia/dav/exception/invalid_sync_token.rb +26 -0
  161. data/lib/tilia/dav/exception/length_required.rb +18 -0
  162. data/lib/tilia/dav/exception/lock_token_matches_request_uri.rb +25 -0
  163. data/lib/tilia/dav/exception/locked.rb +48 -0
  164. data/lib/tilia/dav/exception/method_not_allowed.rb +29 -0
  165. data/lib/tilia/dav/exception/not_authenticated.rb +18 -0
  166. data/lib/tilia/dav/exception/not_found.rb +18 -0
  167. data/lib/tilia/dav/exception/not_implemented.rb +18 -0
  168. data/lib/tilia/dav/exception/payment_required.rb +18 -0
  169. data/lib/tilia/dav/exception/precondition_failed.rb +47 -0
  170. data/lib/tilia/dav/exception/report_not_supported.rb +21 -0
  171. data/lib/tilia/dav/exception/requested_range_not_satisfiable.rb +18 -0
  172. data/lib/tilia/dav/exception/service_unavailable.rb +18 -0
  173. data/lib/tilia/dav/exception/too_many_matches.rb +21 -0
  174. data/lib/tilia/dav/exception/unsupported_media_type.rb +18 -0
  175. data/lib/tilia/dav/file.rb +58 -0
  176. data/lib/tilia/dav/fs.rb +9 -0
  177. data/lib/tilia/dav/fs/directory.rb +119 -0
  178. data/lib/tilia/dav/fs/file.rb +69 -0
  179. data/lib/tilia/dav/fs/node.rb +57 -0
  180. data/lib/tilia/dav/fs_ext.rb +8 -0
  181. data/lib/tilia/dav/fs_ext/directory.rb +175 -0
  182. data/lib/tilia/dav/fs_ext/file.rb +118 -0
  183. data/lib/tilia/dav/i_collection.rb +65 -0
  184. data/lib/tilia/dav/i_extended_collection.rb +36 -0
  185. data/lib/tilia/dav/i_file.rb +70 -0
  186. data/lib/tilia/dav/i_move_target.rb +37 -0
  187. data/lib/tilia/dav/i_multi_get.rb +29 -0
  188. data/lib/tilia/dav/i_node.rb +33 -0
  189. data/lib/tilia/dav/i_properties.rb +39 -0
  190. data/lib/tilia/dav/i_quota.rb +19 -0
  191. data/lib/tilia/dav/locks.rb +9 -0
  192. data/lib/tilia/dav/locks/backend.rb +12 -0
  193. data/lib/tilia/dav/locks/backend/abstract_backend.rb +16 -0
  194. data/lib/tilia/dav/locks/backend/backend_interface.rb +41 -0
  195. data/lib/tilia/dav/locks/backend/file.rb +146 -0
  196. data/lib/tilia/dav/locks/backend/sequel.rb +154 -0
  197. data/lib/tilia/dav/locks/lock_info.rb +60 -0
  198. data/lib/tilia/dav/locks/plugin.rb +467 -0
  199. data/lib/tilia/dav/mk_col.rb +47 -0
  200. data/lib/tilia/dav/mount.rb +7 -0
  201. data/lib/tilia/dav/mount/plugin.rb +62 -0
  202. data/lib/tilia/dav/node.rb +36 -0
  203. data/lib/tilia/dav/partial_update.rb +8 -0
  204. data/lib/tilia/dav/partial_update/i_patch_support.rb +40 -0
  205. data/lib/tilia/dav/partial_update/plugin.rb +179 -0
  206. data/lib/tilia/dav/prop_find.rb +262 -0
  207. data/lib/tilia/dav/prop_patch.rb +278 -0
  208. data/lib/tilia/dav/property_storage.rb +8 -0
  209. data/lib/tilia/dav/property_storage/backend.rb +10 -0
  210. data/lib/tilia/dav/property_storage/backend/backend_interface.rb +69 -0
  211. data/lib/tilia/dav/property_storage/backend/sequel.rb +192 -0
  212. data/lib/tilia/dav/property_storage/plugin.rb +131 -0
  213. data/lib/tilia/dav/server.rb +1388 -0
  214. data/lib/tilia/dav/server_plugin.rb +81 -0
  215. data/lib/tilia/dav/simple_collection.rb +71 -0
  216. data/lib/tilia/dav/simple_file.rb +82 -0
  217. data/lib/tilia/dav/string_util.rb +68 -0
  218. data/lib/tilia/dav/sync.rb +8 -0
  219. data/lib/tilia/dav/sync/i_sync_collection.rb +80 -0
  220. data/lib/tilia/dav/sync/plugin.rb +225 -0
  221. data/lib/tilia/dav/temporary_file_filter_plugin.rb +248 -0
  222. data/lib/tilia/dav/tree.rb +270 -0
  223. data/lib/tilia/dav/uuid_util.rb +45 -0
  224. data/lib/tilia/dav/version.rb +9 -0
  225. data/lib/tilia/dav/xml.rb +11 -0
  226. data/lib/tilia/dav/xml/element.rb +10 -0
  227. data/lib/tilia/dav/xml/element/prop.rb +92 -0
  228. data/lib/tilia/dav/xml/element/response.rb +188 -0
  229. data/lib/tilia/dav/xml/property.rb +16 -0
  230. data/lib/tilia/dav/xml/property/complex.rb +76 -0
  231. data/lib/tilia/dav/xml/property/get_last_modified.rb +79 -0
  232. data/lib/tilia/dav/xml/property/href.rb +137 -0
  233. data/lib/tilia/dav/xml/property/lock_discovery.rb +89 -0
  234. data/lib/tilia/dav/xml/property/resource_type.rb +96 -0
  235. data/lib/tilia/dav/xml/property/supported_lock.rb +48 -0
  236. data/lib/tilia/dav/xml/property/supported_method_set.rb +101 -0
  237. data/lib/tilia/dav/xml/property/supported_report_set.rb +118 -0
  238. data/lib/tilia/dav/xml/request.rb +13 -0
  239. data/lib/tilia/dav/xml/request/lock.rb +67 -0
  240. data/lib/tilia/dav/xml/request/mk_col.rb +69 -0
  241. data/lib/tilia/dav/xml/request/prop_find.rb +70 -0
  242. data/lib/tilia/dav/xml/request/prop_patch.rb +101 -0
  243. data/lib/tilia/dav/xml/request/sync_collection_report.rb +102 -0
  244. data/lib/tilia/dav/xml/response.rb +9 -0
  245. data/lib/tilia/dav/xml/response/multi_status.rb +108 -0
  246. data/lib/tilia/dav/xml/service.rb +42 -0
  247. data/lib/tilia/dav_acl.rb +16 -0
  248. data/lib/tilia/dav_acl/abstract_principal_collection.rb +143 -0
  249. data/lib/tilia/dav_acl/exception.rb +11 -0
  250. data/lib/tilia/dav_acl/exception/ace_conflict.rb +21 -0
  251. data/lib/tilia/dav_acl/exception/need_privileges.rb +65 -0
  252. data/lib/tilia/dav_acl/exception/no_abstract.rb +21 -0
  253. data/lib/tilia/dav_acl/exception/not_recognized_principal.rb +21 -0
  254. data/lib/tilia/dav_acl/exception/not_supported_privilege.rb +21 -0
  255. data/lib/tilia/dav_acl/fs.rb +9 -0
  256. data/lib/tilia/dav_acl/fs/collection.rb +108 -0
  257. data/lib/tilia/dav_acl/fs/file.rb +87 -0
  258. data/lib/tilia/dav_acl/fs/home_collection.rb +148 -0
  259. data/lib/tilia/dav_acl/i_acl.rb +61 -0
  260. data/lib/tilia/dav_acl/i_principal.rb +63 -0
  261. data/lib/tilia/dav_acl/i_principal_collection.rb +52 -0
  262. data/lib/tilia/dav_acl/plugin.rb +1109 -0
  263. data/lib/tilia/dav_acl/principal.rb +213 -0
  264. data/lib/tilia/dav_acl/principal_backend.rb +11 -0
  265. data/lib/tilia/dav_acl/principal_backend/abstract_backend.rb +42 -0
  266. data/lib/tilia/dav_acl/principal_backend/backend_interface.rb +127 -0
  267. data/lib/tilia/dav_acl/principal_backend/create_principal_support.rb +27 -0
  268. data/lib/tilia/dav_acl/principal_backend/sequel.rb +313 -0
  269. data/lib/tilia/dav_acl/principal_collection.rb +117 -0
  270. data/lib/tilia/dav_acl/xml.rb +8 -0
  271. data/lib/tilia/dav_acl/xml/property.rb +13 -0
  272. data/lib/tilia/dav_acl/xml/property/acl.rb +222 -0
  273. data/lib/tilia/dav_acl/xml/property/acl_restrictions.rb +40 -0
  274. data/lib/tilia/dav_acl/xml/property/current_user_privilege_set.rb +125 -0
  275. data/lib/tilia/dav_acl/xml/property/principal.rb +149 -0
  276. data/lib/tilia/dav_acl/xml/property/supported_privilege_set.rb +135 -0
  277. data/lib/tilia/dav_acl/xml/request.rb +11 -0
  278. data/lib/tilia/dav_acl/xml/request/expand_property_report.rb +86 -0
  279. data/lib/tilia/dav_acl/xml/request/principal_property_search_report.rb +111 -0
  280. data/lib/tilia/dav_acl/xml/request/principal_search_property_set_report.rb +49 -0
  281. data/test/cal_dav/backend/abstract_sequel_test.rb +817 -0
  282. data/test/cal_dav/backend/abstract_test.rb +163 -0
  283. data/test/cal_dav/backend/mock.rb +169 -0
  284. data/test/cal_dav/backend/mock_scheduling.rb +84 -0
  285. data/test/cal_dav/backend/mock_sharing.rb +124 -0
  286. data/test/cal_dav/backend/mock_subscription_support.rb +123 -0
  287. data/test/cal_dav/backend/sequel_my_sql_test.rb +102 -0
  288. data/test/cal_dav/backend/sequel_sqlite_test.rb +105 -0
  289. data/test/cal_dav/calendar_home_notifications_test.rb +41 -0
  290. data/test/cal_dav/calendar_home_shared_calendars_test.rb +64 -0
  291. data/test/cal_dav/calendar_home_subscriptions_test.rb +67 -0
  292. data/test/cal_dav/calendar_home_test.rb +144 -0
  293. data/test/cal_dav/calendar_object_test.rb +317 -0
  294. data/test/cal_dav/calendar_query_v_alarm_test.rb +114 -0
  295. data/test/cal_dav/calendar_query_validator_test.rb +820 -0
  296. data/test/cal_dav/calendar_test.rb +203 -0
  297. data/test/cal_dav/expand_events_double_events_test.rb +94 -0
  298. data/test/cal_dav/expand_events_dtstar_tand_dten_dby_day_test.rb +94 -0
  299. data/test/cal_dav/expand_events_dtstar_tand_dtend_test.rb +100 -0
  300. data/test/cal_dav/expand_events_floating_time_test.rb +211 -0
  301. data/test/cal_dav/free_busy_report_test.rb +156 -0
  302. data/test/cal_dav/get_events_by_timerange_test.rb +74 -0
  303. data/test/cal_dav/ics_export_plugin_test.rb +638 -0
  304. data/test/cal_dav/issue166_test.rb +59 -0
  305. data/test/cal_dav/issue172_test.rb +139 -0
  306. data/test/cal_dav/issue203_test.rb +130 -0
  307. data/test/cal_dav/issue205_test.rb +89 -0
  308. data/test/cal_dav/issue211_test.rb +84 -0
  309. data/test/cal_dav/issue220_test.rb +94 -0
  310. data/test/cal_dav/issue228_test.rb +74 -0
  311. data/test/cal_dav/j_cal_transform_test.rb +244 -0
  312. data/test/cal_dav/notifications/collection_test.rb +67 -0
  313. data/test/cal_dav/notifications/node_test.rb +73 -0
  314. data/test/cal_dav/notifications/plugin_test.rb +144 -0
  315. data/test/cal_dav/plugin_test.rb +1049 -0
  316. data/test/cal_dav/principal/collection_test.rb +19 -0
  317. data/test/cal_dav/principal/proxy_read_test.rb +67 -0
  318. data/test/cal_dav/principal/proxy_write_test.rb +29 -0
  319. data/test/cal_dav/principal/user_test.rb +91 -0
  320. data/test/cal_dav/schedule/deliver_new_event_test.rb +81 -0
  321. data/test/cal_dav/schedule/free_busy_request_test.rb +565 -0
  322. data/test/cal_dav/schedule/i_mip/mock_plugin.rb +40 -0
  323. data/test/cal_dav/schedule/i_mip_plugin_test.rb +196 -0
  324. data/test/cal_dav/schedule/inbox_test.rb +150 -0
  325. data/test/cal_dav/schedule/outbox_post_test.rb +124 -0
  326. data/test/cal_dav/schedule/outbox_test.rb +76 -0
  327. data/test/cal_dav/schedule/plugin_basic_test.rb +39 -0
  328. data/test/cal_dav/schedule/plugin_properties_test.rb +96 -0
  329. data/test/cal_dav/schedule/plugin_properties_with_shared_calendar_test.rb +69 -0
  330. data/test/cal_dav/schedule/schedule_deliver_test.rb +605 -0
  331. data/test/cal_dav/schedule/scheduling_object_test.rb +327 -0
  332. data/test/cal_dav/shareable_calendar_test.rb +58 -0
  333. data/test/cal_dav/shared_calendar_test.rb +189 -0
  334. data/test/cal_dav/sharing_plugin_test.rb +373 -0
  335. data/test/cal_dav/subscriptions/create_subscription_test.rb +115 -0
  336. data/test/cal_dav/subscriptions/plugin_test.rb +46 -0
  337. data/test/cal_dav/subscriptions/subscription_test.rb +119 -0
  338. data/test/cal_dav/test_util.rb +164 -0
  339. data/test/cal_dav/validate_i_cal_test.rb +219 -0
  340. data/test/cal_dav/xml/notification/invite_reply_test.rb +136 -0
  341. data/test/cal_dav/xml/notification/invite_test.rb +225 -0
  342. data/test/cal_dav/xml/notification/system_status_test.rb +63 -0
  343. data/test/cal_dav/xml/property/allowed_sharing_modes_test.rb +34 -0
  344. data/test/cal_dav/xml/property/email_address_set_test.rb +35 -0
  345. data/test/cal_dav/xml/property/invite_test.rb +173 -0
  346. data/test/cal_dav/xml/property/schedule_calendar_transp_test.rb +96 -0
  347. data/test/cal_dav/xml/property/supported_calendar_component_set_test.rb +76 -0
  348. data/test/cal_dav/xml/property/supported_calendar_data_test.rb +32 -0
  349. data/test/cal_dav/xml/property/supported_collation_set_test.rb +33 -0
  350. data/test/cal_dav/xml/request/calendar_query_report_test.rb +339 -0
  351. data/test/cal_dav/xml/request/invite_reply_test.rb +68 -0
  352. data/test/cal_dav/xml/request/share_test.rb +79 -0
  353. data/test/card_dav/abstract_plugin_test.rb +24 -0
  354. data/test/card_dav/address_book_home_test.rb +128 -0
  355. data/test/card_dav/address_book_query_test.rb +303 -0
  356. data/test/card_dav/address_book_root_test.rb +26 -0
  357. data/test/card_dav/address_book_test.rb +166 -0
  358. data/test/card_dav/backend/abstract_sequel_test.rb +302 -0
  359. data/test/card_dav/backend/mock.rb +122 -0
  360. data/test/card_dav/backend/sequel_my_sql_test.rb +56 -0
  361. data/test/card_dav/backend/sequel_sqlite_test.rb +59 -0
  362. data/test/card_dav/card_test.rb +164 -0
  363. data/test/card_dav/i_directory_test.rb +22 -0
  364. data/test/card_dav/multi_get_test.rb +97 -0
  365. data/test/card_dav/plugin_test.rb +87 -0
  366. data/test/card_dav/sogo_strip_content_type_test.rb +63 -0
  367. data/test/card_dav/test_util.rb +51 -0
  368. data/test/card_dav/validate_filter_test.rb +210 -0
  369. data/test/card_dav/validate_v_card_test.rb +143 -0
  370. data/test/card_dav/vcf_export_test.rb +66 -0
  371. data/test/card_dav/xml/property/supported_address_data_test.rb +34 -0
  372. data/test/card_dav/xml/property/supported_collation_set_test.rb +34 -0
  373. data/test/card_dav/xml/request/address_book_query_report_test.rb +276 -0
  374. data/test/dav/abstract_server.rb +36 -0
  375. data/test/dav/auth/backend/abstract_basic_test.rb +74 -0
  376. data/test/dav/auth/backend/abstract_digest_test.rb +114 -0
  377. data/test/dav/auth/backend/abstract_sequel_test.rb +25 -0
  378. data/test/dav/auth/backend/apache_test.rb +60 -0
  379. data/test/dav/auth/backend/basic_call_back_test.rb +33 -0
  380. data/test/dav/auth/backend/file_test.rb +43 -0
  381. data/test/dav/auth/backend/mock.rb +73 -0
  382. data/test/dav/auth/backend/sequel_my_sql_test.rb +32 -0
  383. data/test/dav/auth/backend/sequel_sqlite_test.rb +21 -0
  384. data/test/dav/auth/plugin_test.rb +92 -0
  385. data/test/dav/basic_node_test.rb +143 -0
  386. data/test/dav/browser/guess_content_type_test.rb +44 -0
  387. data/test/dav/browser/map_get_to_prop_find_test.rb +37 -0
  388. data/test/dav/browser/plugin_test.rb +165 -0
  389. data/test/dav/browser/prop_find_all_test.rb +59 -0
  390. data/test/dav/client_mock.rb +24 -0
  391. data/test/dav/client_test.rb +231 -0
  392. data/test/dav/copy_test.rb +33 -0
  393. data/test/dav/exception/locked_test.rb +61 -0
  394. data/test/dav/exception/payment_required_test.rb +14 -0
  395. data/test/dav/exception/service_unavailable_test.rb +14 -0
  396. data/test/dav/exception/too_many_matches_test.rb +31 -0
  397. data/test/dav/exception_test.rb +24 -0
  398. data/test/dav/fs_ext/file_test.rb +72 -0
  399. data/test/dav/fs_ext/server_test.rb +251 -0
  400. data/test/dav/get_if_conditions_test.rb +299 -0
  401. data/test/dav/http_delete_test.rb +110 -0
  402. data/test/dav/http_get_test.rb +130 -0
  403. data/test/dav/http_head_test.rb +80 -0
  404. data/test/dav/http_move_test.rb +105 -0
  405. data/test/dav/http_prefer_parsing_test.rb +186 -0
  406. data/test/dav/http_put_test.rb +271 -0
  407. data/test/dav/issue33_test.rb +90 -0
  408. data/test/dav/locks/backend/abstract_test.rb +160 -0
  409. data/test/dav/locks/backend/file_test.rb +24 -0
  410. data/test/dav/locks/backend/mock.rb +82 -0
  411. data/test/dav/locks/backend/sequel_my_sql_test.rb +32 -0
  412. data/test/dav/locks/backend/sequel_test.rb +19 -0
  413. data/test/dav/locks/ms_word_test.rb +119 -0
  414. data/test/dav/locks/plugin2_test.rb +61 -0
  415. data/test/dav/locks/plugin_test.rb +896 -0
  416. data/test/dav/mock/collection.rb +113 -0
  417. data/test/dav/mock/file.rb +100 -0
  418. data/test/dav/mock/properties_collection.rb +80 -0
  419. data/test/dav/mock/streaming_file.rb +66 -0
  420. data/test/dav/mount/plugin_test.rb +48 -0
  421. data/test/dav/object_tree_test.rb +65 -0
  422. data/test/dav/partial_update/file_mock.rb +92 -0
  423. data/test/dav/partial_update/plugin_test.rb +125 -0
  424. data/test/dav/partial_update/specification_test.rb +77 -0
  425. data/test/dav/prop_find_test.rb +87 -0
  426. data/test/dav/prop_patch_test.rb +367 -0
  427. data/test/dav/property_storage/backend/abstract_sequel_test.rb +147 -0
  428. data/test/dav/property_storage/backend/mock.rb +96 -0
  429. data/test/dav/property_storage/backend/sequel_mysql_test.rb +32 -0
  430. data/test/dav/property_storage/backend/sequel_sqlite_test.rb +31 -0
  431. data/test/dav/property_storage/plugin_test.rb +90 -0
  432. data/test/dav/server_copy_move_test.rb +164 -0
  433. data/test/dav/server_events_test.rb +105 -0
  434. data/test/dav/server_mkcol_test.rb +337 -0
  435. data/test/dav/server_mock.rb +10 -0
  436. data/test/dav/server_plugin_test.rb +85 -0
  437. data/test/dav/server_precondition_test.rb +253 -0
  438. data/test/dav/server_props_infinite_depth_test.rb +144 -0
  439. data/test/dav/server_props_test.rb +182 -0
  440. data/test/dav/server_range_test.rb +262 -0
  441. data/test/dav/server_simple_test.rb +388 -0
  442. data/test/dav/server_update_properties_test.rb +93 -0
  443. data/test/dav/simple_file_test.rb +17 -0
  444. data/test/dav/string_util_test.rb +92 -0
  445. data/test/dav/sync/mock_sync_collection.rb +141 -0
  446. data/test/dav/sync/plugin_test.rb +491 -0
  447. data/test/dav/sync_token_property_test.rb +105 -0
  448. data/test/dav/temporary_file_filter_test.rb +179 -0
  449. data/test/dav/test_plugin.rb +24 -0
  450. data/test/dav/tree_test.rb +201 -0
  451. data/test/dav/uuid_util_test.rb +14 -0
  452. data/test/dav/xml/element/prop_test.rb +121 -0
  453. data/test/dav/xml/element/response_test.rb +202 -0
  454. data/test/dav/xml/property/href_test.rb +112 -0
  455. data/test/dav/xml/property/last_modified_test.rb +52 -0
  456. data/test/dav/xml/property/lock_discovery_test.rb +79 -0
  457. data/test/dav/xml/property/supported_method_set_test.rb +54 -0
  458. data/test/dav/xml/property/supported_report_set_test.rb +109 -0
  459. data/test/dav/xml/request/prop_find_test.rb +45 -0
  460. data/test/dav/xml/request/prop_patch_test.rb +47 -0
  461. data/test/dav/xml/request/sync_collection_test.rb +89 -0
  462. data/test/dav/xml/xml_tester.rb +35 -0
  463. data/test/dav_acl/acl_method_test.rb +299 -0
  464. data/test/dav_acl/allow_access_test.rb +94 -0
  465. data/test/dav_acl/block_access_test.rb +161 -0
  466. data/test/dav_acl/exception/ace_conflict_test.rb +33 -0
  467. data/test/dav_acl/exception/need_privileges_exception_test.rb +43 -0
  468. data/test/dav_acl/exception/no_abstract_test.rb +33 -0
  469. data/test/dav_acl/exception/not_recognized_principal_test.rb +33 -0
  470. data/test/dav_acl/exception/not_supported_privilege_test.rb +33 -0
  471. data/test/dav_acl/expand_properties_test.rb +265 -0
  472. data/test/dav_acl/fs/collection_test.rb +39 -0
  473. data/test/dav_acl/fs/file_test.rb +47 -0
  474. data/test/dav_acl/fs/home_collection_test.rb +82 -0
  475. data/test/dav_acl/mock_acl_node.rb +27 -0
  476. data/test/dav_acl/mock_principal.rb +27 -0
  477. data/test/dav_acl/plugin_admin_test.rb +60 -0
  478. data/test/dav_acl/plugin_properties_test.rb +346 -0
  479. data/test/dav_acl/plugin_update_properties_test.rb +82 -0
  480. data/test/dav_acl/principal_backend/abstract_sequel_test.rb +159 -0
  481. data/test/dav_acl/principal_backend/mock.rb +150 -0
  482. data/test/dav_acl/principal_backend/sequel_my_sql_test.rb +43 -0
  483. data/test/dav_acl/principal_backend/sequel_sqlite_test.rb +31 -0
  484. data/test/dav_acl/principal_collection_test.rb +44 -0
  485. data/test/dav_acl/principal_property_search_test.rb +354 -0
  486. data/test/dav_acl/principal_search_property_set_test.rb +125 -0
  487. data/test/dav_acl/principal_test.rb +181 -0
  488. data/test/dav_acl/simple_plugin_test.rb +320 -0
  489. data/test/dav_acl/xml/property/acl_restrictions_test.rb +28 -0
  490. data/test/dav_acl/xml/property/acl_test.rb +325 -0
  491. data/test/dav_acl/xml/property/current_user_privilege_set_test.rb +77 -0
  492. data/test/dav_acl/xml/property/principal_test.rb +158 -0
  493. data/test/dav_acl/xml/property/supported_privilege_set_test.rb +109 -0
  494. data/test/dav_server_test.rb +225 -0
  495. data/test/http/response_mock.rb +16 -0
  496. data/test/http/sapi_mock.rb +29 -0
  497. data/test/test_helper.rb +176 -0
  498. data/tilia-dav.gemspec +28 -0
  499. metadata +726 -0
@@ -0,0 +1,250 @@
1
+ module Tilia
2
+ module CalDav
3
+ module Backend
4
+ # Every CalDAV backend must at least implement this interface.
5
+ module BackendInterface
6
+ # Returns a list of calendars for a principal.
7
+ #
8
+ # Every project is an array with the following keys:
9
+ # * id, a unique id that will be used by other functions to modify the
10
+ # calendar. This can be the same as the uri or a database key.
11
+ # * uri, which is the basename of the uri with which the calendar is
12
+ # accessed.
13
+ # * principaluri. The owner of the calendar. Almost always the same as
14
+ # principalUri passed to this method.
15
+ #
16
+ # Furthermore it can contain webdav properties in clark notation. A very
17
+ # common one is '{DAV:}displayname'.
18
+ #
19
+ # Many clients also require:
20
+ # {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
21
+ # For this property, you can just return an instance of
22
+ # Sabre\CalDAV\Property\SupportedCalendarComponentSet.
23
+ #
24
+ # If you return {http://sabredav.org/ns}read-only and set the value to 1,
25
+ # ACL will automatically be put in read-only mode.
26
+ #
27
+ # @param string principal_uri
28
+ # @return array
29
+ def calendars_for_user(principal_uri)
30
+ end
31
+
32
+ # Creates a new calendar for a principal.
33
+ #
34
+ # If the creation was a success, an id must be returned that can be used to
35
+ # reference this calendar in other methods, such as updateCalendar.
36
+ #
37
+ # @param string principal_uri
38
+ # @param string calendar_uri
39
+ # @param array properties
40
+ # @return void
41
+ def create_calendar(principal_uri, calendar_uri, properties)
42
+ end
43
+
44
+ # Updates properties for a calendar.
45
+ #
46
+ # The list of mutations is stored in a Sabre\DAV\PropPatch object.
47
+ # To do the actual updates, you must tell this object which properties
48
+ # you're going to process with the handle method.
49
+ #
50
+ # Calling the handle method is like telling the PropPatch object "I
51
+ # promise I can handle updating this property".
52
+ #
53
+ # Read the PropPatch documentation for more info and examples.
54
+ #
55
+ # @param string path
56
+ # @param \Sabre\DAV\PropPatch prop_patch
57
+ # @return void
58
+ def update_calendar(calendar_id, prop_patch)
59
+ end
60
+
61
+ # Delete a calendar and all its objects
62
+ #
63
+ # @param mixed calendar_id
64
+ # @return void
65
+ def delete_calendar(calendar_id)
66
+ end
67
+
68
+ # Returns all calendar objects within a calendar.
69
+ #
70
+ # Every item contains an array with the following keys:
71
+ # * calendardata - The iCalendar-compatible calendar data
72
+ # * uri - a unique key which will be used to construct the uri. This can
73
+ # be any arbitrary string, but making sure it ends with '.ics' is a
74
+ # good idea. This is only the basename, or filename, not the full
75
+ # path.
76
+ # * lastmodified - a timestamp of the last modification time
77
+ # * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
78
+ # '"abcdef"')
79
+ # * size - The size of the calendar objects, in bytes.
80
+ # * component - optional, a string containing the type of object, such
81
+ # as 'vevent' or 'vtodo'. If specified, this will be used to populate
82
+ # the Content-Type header.
83
+ #
84
+ # Note that the etag is optional, but it's highly encouraged to return for
85
+ # speed reasons.
86
+ #
87
+ # The calendardata is also optional. If it's not returned
88
+ # 'getCalendarObject' will be called later, which *is* expected to return
89
+ # calendardata.
90
+ #
91
+ # If neither etag or size are specified, the calendardata will be
92
+ # used/fetched to determine these numbers. If both are specified the
93
+ # amount of times this is needed is reduced by a great degree.
94
+ #
95
+ # @param mixed calendar_id
96
+ # @return array
97
+ def calendar_objects(calendar_id)
98
+ end
99
+
100
+ # Returns information from a single calendar object, based on it's object
101
+ # uri.
102
+ #
103
+ # The object uri is only the basename, or filename and not a full path.
104
+ #
105
+ # The returned array must have the same keys as getCalendarObjects. The
106
+ # 'calendardata' object is required here though, while it's not required
107
+ # for getCalendarObjects.
108
+ #
109
+ # This method must return null if the object did not exist.
110
+ #
111
+ # @param mixed calendar_id
112
+ # @param string object_uri
113
+ # @return array|null
114
+ def calendar_object(calendar_id, object_uri)
115
+ end
116
+
117
+ # Returns a list of calendar objects.
118
+ #
119
+ # This method should work identical to getCalendarObject, but instead
120
+ # return all the calendar objects in the list as an array.
121
+ #
122
+ # If the backend supports this, it may allow for some speed-ups.
123
+ #
124
+ # @param mixed calendar_id
125
+ # @param array uris
126
+ # @return array
127
+ def multiple_calendar_objects(calendar_id, uris)
128
+ end
129
+
130
+ # Creates a new calendar object.
131
+ #
132
+ # The object uri is only the basename, or filename and not a full path.
133
+ #
134
+ # It is possible to return an etag from this function, which will be used
135
+ # in the response to this PUT request. Note that the ETag must be
136
+ # surrounded by double-quotes.
137
+ #
138
+ # However, you should only really return this ETag if you don't mangle the
139
+ # calendar-data. If the result of a subsequent GET to this object is not
140
+ # the exact same as this request body, you should omit the ETag.
141
+ #
142
+ # @param mixed calendar_id
143
+ # @param string object_uri
144
+ # @param string calendar_data
145
+ # @return string|null
146
+ def create_calendar_object(calendar_id, object_uri, calendar_data)
147
+ end
148
+
149
+ # Updates an existing calendarobject, based on it's uri.
150
+ #
151
+ # The object uri is only the basename, or filename and not a full path.
152
+ #
153
+ # It is possible return an etag from this function, which will be used in
154
+ # the response to this PUT request. Note that the ETag must be surrounded
155
+ # by double-quotes.
156
+ #
157
+ # However, you should only really return this ETag if you don't mangle the
158
+ # calendar-data. If the result of a subsequent GET to this object is not
159
+ # the exact same as this request body, you should omit the ETag.
160
+ #
161
+ # @param mixed calendar_id
162
+ # @param string object_uri
163
+ # @param string calendar_data
164
+ # @return string|null
165
+ def update_calendar_object(calendar_id, object_uri, calendar_data)
166
+ end
167
+
168
+ # Deletes an existing calendar object.
169
+ #
170
+ # The object uri is only the basename, or filename and not a full path.
171
+ #
172
+ # @param mixed calendar_id
173
+ # @param string object_uri
174
+ # @return void
175
+ def delete_calendar_object(calendar_id, object_uri)
176
+ end
177
+
178
+ # Performs a calendar-query on the contents of this calendar.
179
+ #
180
+ # The calendar-query is defined in RFC4791 : CalDAV. Using the
181
+ # calendar-query it is possible for a client to request a specific set of
182
+ # object, based on contents of iCalendar properties, date-ranges and
183
+ # iCalendar component types (VTODO, VEVENT).
184
+ #
185
+ # This method should just return a list of (relative) urls that match this
186
+ # query.
187
+ #
188
+ # The list of filters are specified as an array. The exact array is
189
+ # documented by Sabre\CalDAV\CalendarQueryParser.
190
+ #
191
+ # Note that it is extremely likely that getCalendarObject for every path
192
+ # returned from this method will be called almost immediately after. You
193
+ # may want to anticipate this to speed up these requests.
194
+ #
195
+ # This method provides a default implementation, which parses *all* the
196
+ # iCalendar objects in the specified calendar.
197
+ #
198
+ # This default may well be good enough for personal use, and calendars
199
+ # that aren't very large. But if you anticipate high usage, big calendars
200
+ # or high loads, you are strongly adviced to optimize certain paths.
201
+ #
202
+ # The best way to do so is override this method and to optimize
203
+ # specifically for 'common filters'.
204
+ #
205
+ # Requests that are extremely common are:
206
+ # * requests for just VEVENTS
207
+ # * requests for just VTODO
208
+ # * requests with a time-range-filter on either VEVENT or VTODO.
209
+ #
210
+ # ..and combinations of these requests. It may not be worth it to try to
211
+ # handle every possible situation and just rely on the (relatively
212
+ # easy to use) CalendarQueryValidator to handle the rest.
213
+ #
214
+ # Note that especially time-range-filters may be difficult to parse. A
215
+ # time-range filter specified on a VEVENT must for instance also handle
216
+ # recurrence rules correctly.
217
+ # A good example of how to interprete all these filters can also simply
218
+ # be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct
219
+ # as possible, so it gives you a good idea on what type of stuff you need
220
+ # to think of.
221
+ #
222
+ # @param mixed calendar_id
223
+ # @param array filters
224
+ # @return array
225
+ def calendar_query(calendar_id, filters)
226
+ end
227
+
228
+ # Searches through all of a users calendars and calendar objects to find
229
+ # an object with a specific UID.
230
+ #
231
+ # This method should return the path to this object, relative to the
232
+ # calendar home, so this path usually only contains two parts:
233
+ #
234
+ # calendarpath/objectpath.ics
235
+ #
236
+ # If the uid is not found, return null.
237
+ #
238
+ # This method should only consider * objects that the principal owns, so
239
+ # any calendars owned by other principals that also appear in this
240
+ # collection should be ignored.
241
+ #
242
+ # @param string principal_uri
243
+ # @param string uid
244
+ # @return string|null
245
+ def calendar_object_by_uid(principal_uri, uid)
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,38 @@
1
+ module Tilia
2
+ module CalDav
3
+ module Backend
4
+ # Adds caldav notification support to a backend.
5
+ #
6
+ # Note: This feature is experimental, and may change in between different
7
+ # SabreDAV versions.
8
+ #
9
+ # Notifications are defined at:
10
+ # http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-notifications.txt
11
+ #
12
+ # These notifications are basically a list of server-generated notifications
13
+ # displayed to the user. Users can dismiss notifications by deleting them.
14
+ #
15
+ # The primary usecase is to allow for calendar-sharing.
16
+ module NotificationSupport
17
+ include BackendInterface
18
+
19
+ # Returns a list of notifications for a given principal url.
20
+ #
21
+ # @param string principal_uri
22
+ # @return NotificationInterface[]
23
+ def notifications_for_principal(principal_uri)
24
+ end
25
+
26
+ # This deletes a specific notifcation.
27
+ #
28
+ # This may be called by a client once it deems a notification handled.
29
+ #
30
+ # @param string principal_uri
31
+ # @param NotificationInterface notification
32
+ # @return void
33
+ def delete_notification(principal_uri, notification)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,57 @@
1
+ module Tilia
2
+ module CalDav
3
+ module Backend
4
+ # Implementing this interface adds CalDAV Scheduling support to your caldav
5
+ # server, as defined in rfc6638.
6
+ module SchedulingSupport
7
+ include BackendInterface
8
+
9
+ # Returns a single scheduling object for the inbox collection.
10
+ #
11
+ # The returned array should contain the following elements:
12
+ # * uri - A unique basename for the object. This will be used to
13
+ # construct a full uri.
14
+ # * calendardata - The iCalendar object
15
+ # * lastmodified - The last modification date. Can be an int for a unix
16
+ # timestamp, or a PHP DateTime object.
17
+ # * etag - A unique token that must change if the object changed.
18
+ # * size - The size of the object, in bytes.
19
+ #
20
+ # @param string principal_uri
21
+ # @param string object_uri
22
+ # @return array
23
+ def scheduling_object(principal_uri, object_uri)
24
+ end
25
+
26
+ # Returns all scheduling objects for the inbox collection.
27
+ #
28
+ # These objects should be returned as an array. Every item in the array
29
+ # should follow the same structure as returned from getSchedulingObject.
30
+ #
31
+ # The main difference is that 'calendardata' is optional.
32
+ #
33
+ # @param string principal_uri
34
+ # @return array
35
+ def scheduling_objects(principal_uri)
36
+ end
37
+
38
+ # Deletes a scheduling object from the inbox collection.
39
+ #
40
+ # @param string principal_uri
41
+ # @param string object_uri
42
+ # @return void
43
+ def delete_scheduling_object(principal_uri, object_uri)
44
+ end
45
+
46
+ # Creates a new scheduling object. This should land in a users' inbox.
47
+ #
48
+ # @param string principal_uri
49
+ # @param string object_uri
50
+ # @param string object_data
51
+ # @return void
52
+ def create_scheduling_object(principal_uri, object_uri, object_data)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,1118 @@
1
+ module Tilia
2
+ module CalDav
3
+ module Backend
4
+ # Sequel CalDAV backend
5
+ #
6
+ # This backend is used to store calendar-data in a Sequel database, such as
7
+ # sqlite or MySQL
8
+ class Sequel < AbstractBackend
9
+ include SyncSupport
10
+ include SubscriptionSupport
11
+ include SchedulingSupport
12
+
13
+ # We need to specify a max date, because we need to stop *somewhere*
14
+ #
15
+ # On 32 bit system the maximum for a signed integer is 2147483647, so
16
+ # MAX_DATE cannot be higher than date('Y-m-d', 2147483647) which results
17
+ # in 2038-01-19 to avoid problems when the date is converted
18
+ # to a unix timestamp.
19
+ MAX_DATE = '2038-01-01'
20
+
21
+ # @!attribute [r] sequel
22
+ # @!visibility private
23
+ # sequel
24
+ #
25
+ # @return [Sequel]
26
+
27
+ # The table name that will be used for calendars
28
+ #
29
+ # @var string
30
+ attr_accessor :calendar_table_name
31
+
32
+ # The table name that will be used for calendar objects
33
+ #
34
+ # @var string
35
+ attr_accessor :calendar_object_table_name
36
+
37
+ # The table name that will be used for tracking changes in calendars.
38
+ #
39
+ # @var string
40
+ attr_accessor :calendar_changes_table_name
41
+
42
+ # The table name that will be used inbox items.
43
+ #
44
+ # @var string
45
+ attr_accessor :scheduling_object_table_name
46
+
47
+ # The table name that will be used for calendar subscriptions.
48
+ #
49
+ # @var string
50
+ attr_accessor :calendar_subscriptions_table_name
51
+
52
+ # List of CalDAV properties, and how they map to database fieldnames
53
+ # Add your own properties by simply adding on to this array.
54
+ #
55
+ # Note that only string-based properties are supported here.
56
+ #
57
+ # @var array
58
+ attr_accessor :property_map
59
+
60
+ # List of subscription properties, and how they map to database fieldnames.
61
+ #
62
+ # @var array
63
+ attr_accessor :public
64
+
65
+ # Creates the backend
66
+ #
67
+ # @param \Sequel sequel
68
+ def initialize(sequel)
69
+ @sequel = sequel
70
+ @calendar_table_name = 'calendars'
71
+ @calendar_object_table_name = 'calendarobjects'
72
+ @calendar_changes_table_name = 'calendarchanges'
73
+ @scheduling_object_table_name = 'schedulingobjects'
74
+ @calendar_subscriptions_table_name = 'calendarsubscriptions'
75
+ @property_map = {
76
+ '{DAV:}displayname' => 'displayname',
77
+ '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description',
78
+ '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone',
79
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
80
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor'
81
+ }
82
+ @subscription_property_map = {
83
+ '{DAV:}displayname' => 'displayname',
84
+ '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate',
85
+ '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder',
86
+ '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor',
87
+ '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos',
88
+ '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms',
89
+ '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments'
90
+ }
91
+ end
92
+
93
+ # Returns a list of calendars for a principal.
94
+ #
95
+ # Every project is an array with the following keys:
96
+ # * id, a unique id that will be used by other functions to modify the
97
+ # calendar. This can be the same as the uri or a database key.
98
+ # * uri. This is just the 'base uri' or 'filename' of the calendar.
99
+ # * principaluri. The owner of the calendar. Almost always the same as
100
+ # principalUri passed to this method.
101
+ #
102
+ # Furthermore it can contain webdav properties in clark notation. A very
103
+ # common one is '{DAV:}displayname'.
104
+ #
105
+ # Many clients also require:
106
+ # {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
107
+ # For this property, you can just return an instance of
108
+ # Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet.
109
+ #
110
+ # If you return {http://sabredav.org/ns}read-only and set the value to 1,
111
+ # ACL will automatically be put in read-only mode.
112
+ #
113
+ # @param string principal_uri
114
+ # @return array
115
+ def calendars_for_user(principal_uri)
116
+ fields = @property_map.values
117
+ fields << 'id'
118
+ fields << 'uri'
119
+ fields << 'synctoken'
120
+ fields << 'components'
121
+ fields << 'principaluri'
122
+ fields << 'transparent'
123
+
124
+ # Making fields a comma-delimited list
125
+ fields = fields.join(', ')
126
+ calendars = []
127
+
128
+ @sequel.fetch("SELECT #{fields} FROM #{@calendar_table_name} WHERE principaluri = ? ORDER BY calendarorder ASC", principal_uri) do |row|
129
+ components = []
130
+ components = row[:components].split(',') unless row[:components].blank?
131
+
132
+ calendar = {
133
+ 'id' => row[:id],
134
+ 'uri' => row[:uri],
135
+ 'principaluri' => row[:principaluri],
136
+ "{#{Plugin::NS_CALENDARSERVER}}getctag" => "http://sabre.io/ns/sync/#{row[:synctoken] ? row[:synctoken] : '0'}",
137
+ '{http://sabredav.org/ns}sync-token' => row[:synctoken] ? row[:synctoken] : '0',
138
+ "{#{Plugin::NS_CALDAV}}supported-calendar-component-set" => Xml::Property::SupportedCalendarComponentSet.new(components),
139
+ "{#{Plugin::NS_CALDAV}}schedule-calendar-transp" => Xml::Property::ScheduleCalendarTransp.new(row[:transparent] ? 'transparent' : 'opaque')
140
+ }
141
+
142
+ @property_map.each do |xml_name, db_name|
143
+ calendar[xml_name] = row[db_name.to_sym]
144
+ end
145
+
146
+ calendars << calendar
147
+ end
148
+
149
+ calendars
150
+ end
151
+
152
+ # Creates a new calendar for a principal.
153
+ #
154
+ # If the creation was a success, an id must be returned that can be used
155
+ # to reference this calendar in other methods, such as updateCalendar.
156
+ #
157
+ # @param string principal_uri
158
+ # @param string calendar_uri
159
+ # @param array properties
160
+ # @return string
161
+ def create_calendar(principal_uri, calendar_uri, properties)
162
+ field_names = [
163
+ 'principaluri',
164
+ 'uri',
165
+ 'synctoken',
166
+ 'transparent'
167
+ ]
168
+ values = {
169
+ principaluri: principal_uri,
170
+ uri: calendar_uri,
171
+ synctoken: 1,
172
+ transparent: 0
173
+ }
174
+
175
+ # Default value
176
+ sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'
177
+ field_names << 'components'
178
+ if !properties.key?(sccs)
179
+ values[:components] = 'VEVENT,VTODO'
180
+ else
181
+ unless properties[sccs].is_a?(Xml::Property::SupportedCalendarComponentSet)
182
+ fail Dav::Exception, "The #{sccs} property must be of type: Tilia::CalDAV::Xml::Property::SupportedCalendarComponentSet"
183
+ end
184
+
185
+ values[:components] = properties[sccs].value.join(',')
186
+ end
187
+
188
+ transp = "{#{Plugin::NS_CALDAV}}schedule-calendar-transp"
189
+
190
+ values[:transparent] = properties[transp].value == 'transparent' if properties.key?(transp)
191
+
192
+ @property_map.each do |xml_name, db_name|
193
+ if properties.key?(xml_name)
194
+ values[db_name.to_sym] = properties[xml_name]
195
+ field_names << db_name
196
+ end
197
+ end
198
+
199
+ ds = @sequel[
200
+ "INSERT INTO #{@calendar_table_name} (#{field_names.join(', ')}) VALUES (#{field_names.map { |k| ":#{k}" }.join(', ')})",
201
+ values
202
+ ]
203
+ ds.insert
204
+ end
205
+
206
+ # Updates properties for a calendar.
207
+ #
208
+ # The list of mutations is stored in a Sabre\DAV\PropPatch object.
209
+ # To do the actual updates, you must tell this object which properties
210
+ # you're going to process with the handle method.
211
+ #
212
+ # Calling the handle method is like telling the PropPatch object "I
213
+ # promise I can handle updating this property".
214
+ #
215
+ # Read the PropPatch documenation for more info and examples.
216
+ #
217
+ # @param string calendar_id
218
+ # @param \Sabre\DAV\PropPatch prop_patch
219
+ # @return void
220
+ def update_calendar(calendar_id, prop_patch)
221
+ supported_properties = @property_map.keys
222
+ supported_properties << "{#{Plugin::NS_CALDAV}}schedule-calendar-transp"
223
+
224
+ prop_patch.handle(
225
+ supported_properties,
226
+ lambda do |mutations|
227
+ new_values = {}
228
+ mutations.each do |property_name, property_value|
229
+ case property_name
230
+ when "{#{Plugin::NS_CALDAV}}schedule-calendar-transp"
231
+ field_name = :transparent
232
+ new_values[field_name] = property_value.value == 'transparent'
233
+ else
234
+ field_name = @property_map[property_name].to_sym
235
+ new_values[field_name] = property_value
236
+ end
237
+ end
238
+
239
+ values_sql = []
240
+ new_values.each do |field_name, _value|
241
+ values_sql << "#{field_name} = :#{field_name}"
242
+ end
243
+
244
+ new_values[:id] = calendar_id
245
+ ds = @sequel["UPDATE #{@calendar_table_name} SET #{values_sql.join(', ')} WHERE id = :id", new_values]
246
+ ds.update
247
+
248
+ add_change(calendar_id, '', 2)
249
+
250
+ return true
251
+ end
252
+ )
253
+ end
254
+
255
+ # Delete a calendar and all it's objects
256
+ #
257
+ # @param string calendar_id
258
+ # @return void
259
+ def delete_calendar(calendar_id)
260
+ ds = @sequel["DELETE FROM #{@calendar_object_table_name} WHERE calendarid = ?", calendar_id]
261
+ ds.delete
262
+
263
+ ds = @sequel["DELETE FROM #{@calendar_table_name} WHERE id = ?", calendar_id]
264
+ ds.delete
265
+
266
+ ds = @sequel["DELETE FROM #{@calendar_changes_table_name} WHERE calendarid = ?", calendar_id]
267
+ ds.delete
268
+ end
269
+
270
+ # Returns all calendar objects within a calendar.
271
+ #
272
+ # Every item contains an array with the following keys:
273
+ # * calendardata - The iCalendar-compatible calendar data
274
+ # * uri - a unique key which will be used to construct the uri. This can
275
+ # be any arbitrary string, but making sure it ends with '.ics' is a
276
+ # good idea. This is only the basename, or filename, not the full
277
+ # path.
278
+ # * lastmodified - a timestamp of the last modification time
279
+ # * etag - An arbitrary string, surrounded by double-quotes. (e.g.:
280
+ # ' "abcdef"')
281
+ # * size - The size of the calendar objects, in bytes.
282
+ # * component - optional, a string containing the type of object, such
283
+ # as 'vevent' or 'vtodo'. If specified, this will be used to populate
284
+ # the Content-Type header.
285
+ #
286
+ # Note that the etag is optional, but it's highly encouraged to return for
287
+ # speed reasons.
288
+ #
289
+ # The calendardata is also optional. If it's not returned
290
+ # 'getCalendarObject' will be called later, which *is* expected to return
291
+ # calendardata.
292
+ #
293
+ # If neither etag or size are specified, the calendardata will be
294
+ # used/fetched to determine these numbers. If both are specified the
295
+ # amount of times this is needed is reduced by a great degree.
296
+ #
297
+ # @param string calendar_id
298
+ # @return array
299
+ def calendar_objects(calendar_id)
300
+ result = []
301
+
302
+ @sequel.fetch("SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM #{@calendar_object_table_name} WHERE calendarid = ?", calendar_id) do |row|
303
+ result << {
304
+ 'id' => row[:id],
305
+ 'uri' => row[:uri],
306
+ 'lastmodified' => row[:lastmodified],
307
+ 'etag' => "\"#{row[:etag]}\"",
308
+ 'calendarid' => row[:calendarid],
309
+ 'size' => row[:size].to_i,
310
+ 'component' => row[:componenttype].downcase
311
+ }
312
+ end
313
+
314
+ result
315
+ end
316
+
317
+ # Returns information from a single calendar object, based on it's object
318
+ # uri.
319
+ #
320
+ # The object uri is only the basename, or filename and not a full path.
321
+ #
322
+ # The returned array must have the same keys as getCalendarObjects. The
323
+ # 'calendardata' object is required here though, while it's not required
324
+ # for getCalendarObjects.
325
+ #
326
+ # This method must return null if the object did not exist.
327
+ #
328
+ # @param string calendar_id
329
+ # @param string object_uri
330
+ # @return array|null
331
+ def calendar_object(calendar_id, object_uri)
332
+ ds = @sequel[
333
+ "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM #{@calendar_object_table_name} WHERE calendarid = ? AND uri = ?",
334
+ calendar_id,
335
+ object_uri
336
+ ]
337
+ row = ds.all.first
338
+
339
+ return nil unless row
340
+
341
+ {
342
+ 'id' => row[:id],
343
+ 'uri' => row[:uri],
344
+ 'lastmodified' => row[:lastmodified],
345
+ 'etag' => "\"#{row[:etag]}\"",
346
+ 'calendarid' => row[:calendarid],
347
+ 'size' => row[:size].to_i,
348
+ 'calendardata' => row[:calendardata],
349
+ 'component' => row[:componenttype].downcase
350
+ }
351
+ end
352
+
353
+ # Returns a list of calendar objects.
354
+ #
355
+ # This method should work identical to getCalendarObject, but instead
356
+ # return all the calendar objects in the list as an array.
357
+ #
358
+ # If the backend supports this, it may allow for some speed-ups.
359
+ #
360
+ # @param mixed calendar_id
361
+ # @param array uris
362
+ # @return array
363
+ def multiple_calendar_objects(calendar_id, uris)
364
+ query = "SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM #{@calendar_object_table_name} WHERE calendarid = ? AND uri IN ("
365
+ # Inserting a whole bunch of question marks
366
+ query << (['?'] * uris.size).join(', ')
367
+ query << ')'
368
+
369
+ result = []
370
+ @sequel.fetch(query, calendar_id, *uris) do |row|
371
+ result << {
372
+ 'id' => row[:id],
373
+ 'uri' => row[:uri],
374
+ 'lastmodified' => row[:lastmodified],
375
+ 'etag' => "\"#{row[:etag]}\"",
376
+ 'calendarid' => row[:calendarid],
377
+ 'size' => row[:size].to_i,
378
+ 'calendardata' => row[:calendardata],
379
+ 'component' => row[:componenttype].downcase
380
+ }
381
+ end
382
+
383
+ result
384
+ end
385
+
386
+ # Creates a new calendar object.
387
+ #
388
+ # The object uri is only the basename, or filename and not a full path.
389
+ #
390
+ # It is possible return an etag from this function, which will be used in
391
+ # the response to this PUT request. Note that the ETag must be surrounded
392
+ # by double-quotes.
393
+ #
394
+ # However, you should only really return this ETag if you don't mangle the
395
+ # calendar-data. If the result of a subsequent GET to this object is not
396
+ # the exact same as this request body, you should omit the ETag.
397
+ #
398
+ # @param mixed calendar_id
399
+ # @param string object_uri
400
+ # @param string calendar_data
401
+ # @return string|null
402
+ def create_calendar_object(calendar_id, object_uri, calendar_data)
403
+ extra_data = denormalized_data(calendar_data)
404
+
405
+ ds = @sequel[
406
+ "INSERT INTO #{@calendar_object_table_name} (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)",
407
+ calendar_id,
408
+ object_uri,
409
+ calendar_data,
410
+ Time.now.to_i,
411
+ extra_data['etag'],
412
+ extra_data['size'],
413
+ extra_data['componentType'],
414
+ extra_data['firstOccurence'],
415
+ extra_data['lastOccurence'],
416
+ extra_data['uid'],
417
+ ]
418
+ ds.insert
419
+
420
+ add_change(calendar_id, object_uri, 1)
421
+
422
+ "\"#{extra_data['etag']}\""
423
+ end
424
+
425
+ # Updates an existing calendarobject, based on it's uri.
426
+ #
427
+ # The object uri is only the basename, or filename and not a full path.
428
+ #
429
+ # It is possible return an etag from this function, which will be used in
430
+ # the response to this PUT request. Note that the ETag must be surrounded
431
+ # by double-quotes.
432
+ #
433
+ # However, you should only really return this ETag if you don't mangle the
434
+ # calendar-data. If the result of a subsequent GET to this object is not
435
+ # the exact same as this request body, you should omit the ETag.
436
+ #
437
+ # @param mixed calendar_id
438
+ # @param string object_uri
439
+ # @param string calendar_data
440
+ # @return string|null
441
+ def update_calendar_object(calendar_id, object_uri, calendar_data)
442
+ extra_data = denormalized_data(calendar_data)
443
+
444
+ ds = @sequel[
445
+ "UPDATE #{@calendar_object_table_name} SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?",
446
+ calendar_data,
447
+ Time.now.to_i,
448
+ extra_data['etag'],
449
+ extra_data['size'],
450
+ extra_data['componentType'],
451
+ extra_data['firstOccurence'],
452
+ extra_data['lastOccurence'],
453
+ extra_data['uid'],
454
+ calendar_id,
455
+ object_uri
456
+ ]
457
+ ds.update
458
+
459
+ add_change(calendar_id, object_uri, 2)
460
+
461
+ "\"#{extra_data['etag']}\""
462
+ end
463
+
464
+ protected
465
+
466
+ # Parses some information from calendar objects, used for optimized
467
+ # calendar-queries.
468
+ #
469
+ # Returns an array with the following keys:
470
+ # * etag - An md5 checksum of the object without the quotes.
471
+ # * size - Size of the object in bytes
472
+ # * componentType - VEVENT, VTODO or VJOURNAL
473
+ # * firstOccurence
474
+ # * lastOccurence
475
+ # * uid - value of the UID property
476
+ #
477
+ # @param string calendar_data
478
+ # @return array
479
+ def denormalized_data(calendar_data)
480
+ v_object = VObject::Reader.read(calendar_data)
481
+
482
+ component_type = nil
483
+ component = nil
484
+ first_occurence = nil
485
+ last_occurence = nil
486
+ uid = nil
487
+
488
+ v_object.components.each do |temp_component|
489
+ next unless temp_component.name != 'VTIMEZONE'
490
+
491
+ component_type = temp_component.name
492
+ uid = temp_component['UID'].to_s
493
+ component = temp_component
494
+ break
495
+ end
496
+
497
+ fail Dav::Exception::BadRequest, 'Calendar objects must have a VJOURNAL, VEVENT or VTODO component' unless component_type
498
+
499
+ if component_type == 'VEVENT'
500
+ first_occurence = component['DTSTART'].date_time.to_i
501
+
502
+ # Finding the last occurence is a bit harder
503
+ if !component.key?('RRULE')
504
+ if component.key?('DTEND')
505
+ last_occurence = component['DTEND'].date_time.to_i
506
+ elsif component.key?('DURATION')
507
+ end_date = component['DTSTART'].date_time + VObject::DateTimeParser.parse(component['DURATION'].value)
508
+ last_occurence = end_date.to_i
509
+ elsif !component['DTSTART'].time?
510
+ end_date = component['DTSTART'].date_time + 1.day
511
+ last_occurence = end_date.to_i
512
+ else
513
+ last_occurence = first_occurence
514
+ end
515
+ else
516
+ it = VObject::Recur::EventIterator.new(v_object, component['UID'].to_s)
517
+
518
+ max_date = Time.zone.parse(MAX_DATE)
519
+
520
+ if it.infinite?
521
+ last_occurence = max_date.to_i
522
+ else
523
+ ending = it.dt_end
524
+ while it.valid && ending < max_date
525
+ ending = it.dt_end
526
+ it.next
527
+ end
528
+
529
+ last_occurence = ending.to_i
530
+ end
531
+ end
532
+ end
533
+
534
+ # Destroy circular references to PHP will GC the object.
535
+ v_object.destroy
536
+
537
+ {
538
+ 'etag' => Digest::MD5.hexdigest(calendar_data),
539
+ 'size' => calendar_data.size,
540
+ 'componentType' => component_type,
541
+ 'firstOccurence' => first_occurence,
542
+ 'lastOccurence' => last_occurence,
543
+ 'uid' => uid
544
+ }
545
+ end
546
+
547
+ public
548
+
549
+ # Deletes an existing calendar object.
550
+ #
551
+ # The object uri is only the basename, or filename and not a full path.
552
+ #
553
+ # @param string calendar_id
554
+ # @param string object_uri
555
+ # @return void
556
+ def delete_calendar_object(calendar_id, object_uri)
557
+ ds = @sequel[
558
+ "DELETE FROM #{@calendar_object_table_name} WHERE calendarid = ? AND uri = ?",
559
+ calendar_id,
560
+ object_uri
561
+ ]
562
+ ds.delete
563
+
564
+ add_change(calendar_id, object_uri, 3)
565
+ end
566
+
567
+ # Performs a calendar-query on the contents of this calendar.
568
+ #
569
+ # The calendar-query is defined in RFC4791 : CalDAV. Using the
570
+ # calendar-query it is possible for a client to request a specific set of
571
+ # object, based on contents of iCalendar properties, date-ranges and
572
+ # iCalendar component types (VTODO, VEVENT).
573
+ #
574
+ # This method should just return a list of (relative) urls that match this
575
+ # query.
576
+ #
577
+ # The list of filters are specified as an array. The exact array is
578
+ # documented by \Sabre\CalDAV\CalendarQueryParser.
579
+ #
580
+ # Note that it is extremely likely that getCalendarObject for every path
581
+ # returned from this method will be called almost immediately after. You
582
+ # may want to anticipate this to speed up these requests.
583
+ #
584
+ # This method provides a default implementation, which parses *all* the
585
+ # iCalendar objects in the specified calendar.
586
+ #
587
+ # This default may well be good enough for personal use, and calendars
588
+ # that aren't very large. But if you anticipate high usage, big calendars
589
+ # or high loads, you are strongly adviced to optimize certain paths.
590
+ #
591
+ # The best way to do so is override this method and to optimize
592
+ # specifically for 'common filters'.
593
+ #
594
+ # Requests that are extremely common are:
595
+ # * requests for just VEVENTS
596
+ # * requests for just VTODO
597
+ # * requests with a time-range-filter on a VEVENT.
598
+ #
599
+ # ..and combinations of these requests. It may not be worth it to try to
600
+ # handle every possible situation and just rely on the (relatively
601
+ # easy to use) CalendarQueryValidator to handle the rest.
602
+ #
603
+ # Note that especially time-range-filters may be difficult to parse. A
604
+ # time-range filter specified on a VEVENT must for instance also handle
605
+ # recurrence rules correctly.
606
+ # A good example of how to interprete all these filters can also simply
607
+ # be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct
608
+ # as possible, so it gives you a good idea on what type of stuff you need
609
+ # to think of.
610
+ #
611
+ # This specific implementation (for the Sequel) backend optimizes filters on
612
+ # specific components, and VEVENT time-ranges.
613
+ #
614
+ # @param string calendar_id
615
+ # @param array filters
616
+ # @return array
617
+ def calendar_query(calendar_id, filters)
618
+ component_type = nil
619
+ require_post_filter = true
620
+ time_range = nil
621
+
622
+ # if no filters were specified, we don't need to filter after a query
623
+ if !(filters['prop-filters'] || filters['prop-filters'].empty?) &&
624
+ !(filters['comp-filters'] || filters['comp-filters'].empty?)
625
+ require_post_filter = false
626
+ end
627
+
628
+ # Figuring out if there's a component filter
629
+ if filters['comp-filters'].size > 0 && !filters['comp-filters'][0]['is-not-defined']
630
+ component_type = filters['comp-filters'][0]['name']
631
+
632
+ # Checking if we need post-filters
633
+ if !filters['prop-filters'] &&
634
+ !filters['comp-filters'][0]['comp-filters'] &&
635
+ !filters['comp-filters'][0]['time-range'] &&
636
+ !filters['comp-filters'][0]['prop-filters']
637
+ require_post_filter = false
638
+ end
639
+
640
+ # There was a time-range filter
641
+ if component_type == 'VEVENT' &&
642
+ filters['comp-filters'][0].key?('time-range')
643
+ time_range = filters['comp-filters'][0]['time-range']
644
+
645
+ # If start time OR the end time is not specified, we can do a
646
+ # 100% accurate mysql query.
647
+ if !filters['prop-filters'] &&
648
+ !filters['comp-filters'][0]['comp-filters'] &&
649
+ !filters['comp-filters'][0]['prop-filters'] &&
650
+ (!time_range['start'] || !time_range['end'])
651
+ require_post_filter = false
652
+ end
653
+ end
654
+ end
655
+
656
+ if require_post_filter
657
+ query = "SELECT uri, calendardata FROM #{@calendar_object_table_name} WHERE calendarid = :calendarid"
658
+ else
659
+ query = "SELECT uri FROM #{@calendar_object_table_name} WHERE calendarid = :calendarid"
660
+ end
661
+
662
+ values = {
663
+ calendarid: calendar_id
664
+ }
665
+
666
+ if component_type
667
+ query << ' AND componenttype = :componenttype'
668
+ values[:componenttype] = component_type
669
+ end
670
+
671
+ if time_range && time_range['start']
672
+ query << ' AND lastoccurence > :startdate'
673
+ values[:startdate] = time_range['start'].to_i
674
+ end
675
+
676
+ if time_range && time_range['end']
677
+ query << ' AND firstoccurence < :enddate'
678
+ values[:enddate] = time_range['end'].to_i
679
+ end
680
+
681
+ result = []
682
+ @sequel.fetch(query, values) do |row|
683
+ # TODO: ATM we use string hashes :-/
684
+ string_hash = {}
685
+ row.each { |k, v| string_hash[k.to_s] = v }
686
+
687
+ if require_post_filter
688
+ next unless validate_filter_for_object(string_hash, filters)
689
+ end
690
+
691
+ result << row[:uri]
692
+ end
693
+
694
+ result
695
+ end
696
+
697
+ # Searches through all of a users calendars and calendar objects to find
698
+ # an object with a specific UID.
699
+ #
700
+ # This method should return the path to this object, relative to the
701
+ # calendar home, so this path usually only contains two parts:
702
+ #
703
+ # calendarpath/objectpath.ics
704
+ #
705
+ # If the uid is not found, return null.
706
+ #
707
+ # This method should only consider * objects that the principal owns, so
708
+ # any calendars owned by other principals that also appear in this
709
+ # collection should be ignored.
710
+ #
711
+ # @param string principal_uri
712
+ # @param string uid
713
+ # @return string|null
714
+ def calendar_object_by_uid(principal_uri, uid)
715
+ query = <<SQL
716
+ SELECT
717
+ calendars.uri AS calendaruri, calendarobjects.uri as objecturi
718
+ FROM
719
+ #{@calendar_object_table_name} AS calendarobjects
720
+ LEFT JOIN
721
+ #{@calendar_table_name} AS calendars
722
+ ON calendarobjects.calendarid = calendars.id
723
+ WHERE
724
+ calendars.principaluri = ?
725
+ AND
726
+ calendarobjects.uid = ?
727
+ SQL
728
+
729
+ @sequel.fetch(query, principal_uri, uid) do |row|
730
+ return row[:calendaruri] + '/' + row[:objecturi]
731
+ end
732
+ nil
733
+ end
734
+
735
+ # The getChanges method returns all the changes that have happened, since
736
+ # the specified syncToken in the specified calendar.
737
+ #
738
+ # This function should return an array, such as the following:
739
+ #
740
+ # [
741
+ # 'syncToken' => 'The current synctoken',
742
+ # 'added' => [
743
+ # 'new.txt',
744
+ # ],
745
+ # 'modified' => [
746
+ # 'modified.txt',
747
+ # ],
748
+ # 'deleted' => [
749
+ # 'foo.php.bak',
750
+ # 'old.txt'
751
+ # ]
752
+ # ]
753
+ #
754
+ # The returned syncToken property should reflect the *current* syncToken
755
+ # of the calendar, as reported in the {http://sabredav.org/ns}sync-token
756
+ # property this is needed here too, to ensure the operation is atomic.
757
+ #
758
+ # If the sync_token argument is specified as null, this is an initial
759
+ # sync, and all members should be reported.
760
+ #
761
+ # The modified property is an array of nodenames that have changed since
762
+ # the last token.
763
+ #
764
+ # The deleted property is an array with nodenames, that have been deleted
765
+ # from collection.
766
+ #
767
+ # The sync_level argument is basically the 'depth' of the report. If it's
768
+ # 1, you only have to report changes that happened only directly in
769
+ # immediate descendants. If it's 2, it should also include changes from
770
+ # the nodes below the child collections. (grandchildren)
771
+ #
772
+ # The limit argument allows a client to specify how many results should
773
+ # be returned at most. If the limit is not specified, it should be treated
774
+ # as infinite.
775
+ #
776
+ # If the limit (infinite or not) is higher than you're willing to return,
777
+ # you should throw a Sabre\DAV\Exception\TooMuchMatches exception.
778
+ #
779
+ # If the syncToken is expired (due to data cleanup) or unknown, you must
780
+ # return null.
781
+ #
782
+ # The limit is 'suggestive'. You are free to ignore it.
783
+ #
784
+ # @param string calendar_id
785
+ # @param string sync_token
786
+ # @param int sync_level
787
+ # @param int limit
788
+ # @return array
789
+ def changes_for_calendar(calendar_id, sync_token, _sync_level, limit = nil)
790
+ # Current synctoken
791
+ ds = @sequel["SELECT synctoken FROM #{@calendar_table_name} WHERE id = ?", calendar_id]
792
+ result = ds.all.first
793
+
794
+ return nil unless result
795
+
796
+ current_token = result[:synctoken]
797
+
798
+ return nil unless current_token
799
+
800
+ result = {
801
+ 'syncToken' => current_token,
802
+ 'added' => [],
803
+ 'modified' => [],
804
+ 'deleted' => []
805
+ }
806
+
807
+ if sync_token
808
+ query = "SELECT uri, operation FROM #{@calendar_changes_table_name} WHERE synctoken >= ? AND synctoken < ? AND calendarid = ? ORDER BY synctoken"
809
+ query << " LIMIT #{limit}" if limit && limit > 0
810
+
811
+ # Fetching all changes
812
+
813
+ changes = {}
814
+
815
+ # This loop ensures that any duplicates are overwritten, only the
816
+ # last change on a node is relevant.
817
+ @sequel.fetch(query, sync_token, current_token, calendar_id) do |row|
818
+ changes[row[:uri]] = row[:operation]
819
+ end
820
+
821
+ changes.each do |uri, operation|
822
+ case operation
823
+ when 1
824
+ result['added'] << uri.to_s
825
+ when 2
826
+ result['modified'] << uri.to_s
827
+ when 3
828
+ result['deleted'] << uri.to_s
829
+ end
830
+ end
831
+ else
832
+ # No synctoken supplied, this is the initial sync.
833
+ ds = @sequel["SELECT uri FROM #{@calendar_object_table_name} WHERE calendarid = ?", calendar_id]
834
+
835
+ # RUBY: concert symbols to strings
836
+ result['added'] = ds.all.map { |e| e[:uri] }
837
+ end
838
+
839
+ result
840
+ end
841
+
842
+ protected
843
+
844
+ # Adds a change record to the calendarchanges table.
845
+ #
846
+ # @param mixed calendar_id
847
+ # @param string object_uri
848
+ # @param int operation 1 = add, 2 = modify, 3 = delete.
849
+ # @return void
850
+ def add_change(calendar_id, object_uri, operation)
851
+ ds = @sequel[
852
+ "INSERT INTO #{@calendar_changes_table_name} (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM #{@calendar_table_name} WHERE id = ?",
853
+ object_uri,
854
+ calendar_id,
855
+ operation,
856
+ calendar_id
857
+ ]
858
+ ds.insert
859
+ ds = @sequel[
860
+ "UPDATE #{@calendar_table_name} SET synctoken = synctoken + 1 WHERE id = ?",
861
+ calendar_id
862
+ ]
863
+ ds.update
864
+ end
865
+
866
+ public
867
+
868
+ # Returns a list of subscriptions for a principal.
869
+ #
870
+ # Every subscription is an array with the following keys:
871
+ # * id, a unique id that will be used by other functions to modify the
872
+ # subscription. This can be the same as the uri or a database key.
873
+ # * uri. This is just the 'base uri' or 'filename' of the subscription.
874
+ # * principaluri. The owner of the subscription. Almost always the same as
875
+ # principalUri passed to this method.
876
+ # * source. Url to the actual feed
877
+ #
878
+ # Furthermore, all the subscription info must be returned too:
879
+ #
880
+ # 1. {DAV:}displayname
881
+ # 2. {http://apple.com/ns/ical/}refreshrate
882
+ # 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos
883
+ # should not be stripped).
884
+ # 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms
885
+ # should not be stripped).
886
+ # 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if
887
+ # attachments should not be stripped).
888
+ # 7. {http://apple.com/ns/ical/}calendar-color
889
+ # 8. {http://apple.com/ns/ical/}calendar-order
890
+ # 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set
891
+ # (should just be an instance of
892
+ # Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of
893
+ # default components).
894
+ #
895
+ # @param string principal_uri
896
+ # @return array
897
+ def subscriptions_for_user(principal_uri)
898
+ fields = @subscription_property_map.values
899
+ fields << 'id'
900
+ fields << 'uri'
901
+ fields << 'source'
902
+ fields << 'principaluri'
903
+ fields << 'lastmodified'
904
+
905
+ # Making fields a comma-delimited list
906
+ fields = fields.join(', ')
907
+
908
+ subscriptions = []
909
+ @sequel.fetch("SELECT #{fields} FROM #{@calendar_subscriptions_table_name} WHERE principaluri = ? ORDER BY calendarorder ASC", principal_uri) do |row|
910
+ subscription = {
911
+ 'id' => row[:id],
912
+ 'uri' => row[:uri],
913
+ 'principaluri' => row[:principaluri],
914
+ 'source' => row[:source],
915
+ 'lastmodified' => row[:lastmodified],
916
+
917
+ "{#{Plugin::NS_CALDAV}}supported-calendar-component-set" => Xml::Property::SupportedCalendarComponentSet.new(['VTODO', 'VEVENT'])
918
+ }
919
+
920
+ @subscription_property_map.each do |xml_name, db_name|
921
+ subscription[xml_name] = row[db_name.to_sym] unless row[db_name.to_sym].nil?
922
+ end
923
+
924
+ subscriptions << subscription
925
+ end
926
+
927
+ subscriptions
928
+ end
929
+
930
+ # Creates a new subscription for a principal.
931
+ #
932
+ # If the creation was a success, an id must be returned that can be used to reference
933
+ # this subscription in other methods, such as updateSubscription.
934
+ #
935
+ # @param string principal_uri
936
+ # @param string uri
937
+ # @param array properties
938
+ # @return mixed
939
+ def create_subscription(principal_uri, uri, properties)
940
+ field_names = [
941
+ 'principaluri',
942
+ 'uri',
943
+ 'source',
944
+ 'lastmodified'
945
+ ]
946
+
947
+ fail Dav::Exception::Forbidden, 'The {http://calendarserver.org/ns/}source property is required when creating subscriptions' unless properties.key?('{http://calendarserver.org/ns/}source')
948
+
949
+ values = {
950
+ principaluri: principal_uri,
951
+ uri: uri,
952
+ source: properties['{http://calendarserver.org/ns/}source'].href,
953
+ lastmodified: Time.now.to_i
954
+ }
955
+
956
+ @subscription_property_map.each do |xml_name, db_name|
957
+ if properties.key?(xml_name)
958
+ values[db_name.to_sym] = properties[xml_name]
959
+ field_names << db_name
960
+ end
961
+ end
962
+
963
+ ds = @sequel[
964
+ "INSERT INTO #{@calendar_subscriptions_table_name} (#{field_names.join(', ')}) VALUES (#{field_names.map { |k| ":#{k}" }.join(', ')})",
965
+ values
966
+ ]
967
+ ds.insert
968
+ end
969
+
970
+ # Updates a subscription
971
+ #
972
+ # The list of mutations is stored in a Sabre\DAV\PropPatch object.
973
+ # To do the actual updates, you must tell this object which properties
974
+ # you're going to process with the handle method.
975
+ #
976
+ # Calling the handle method is like telling the PropPatch object "I
977
+ # promise I can handle updating this property".
978
+ #
979
+ # Read the PropPatch documenation for more info and examples.
980
+ #
981
+ # @param mixed subscription_id
982
+ # @param \Sabre\DAV\PropPatch prop_patch
983
+ # @return void
984
+ def update_subscription(subscription_id, prop_patch)
985
+ supported_properties = @subscription_property_map.keys
986
+ supported_properties << '{http://calendarserver.org/ns/}source'
987
+
988
+ prop_patch.handle(
989
+ supported_properties,
990
+ lambda do |mutations|
991
+ new_values = {}
992
+
993
+ mutations.each do |property_name, property_value|
994
+ if property_name == '{http://calendarserver.org/ns/}source'
995
+ new_values[:source] = property_value.href
996
+ else
997
+ field_name = @subscription_property_map[property_name]
998
+ new_values[field_name.to_sym] = property_value
999
+ end
1000
+ end
1001
+
1002
+ # Now we're generating the sql query.
1003
+ values_sql = []
1004
+ new_values.each do |field_name, _value|
1005
+ values_sql << "#{field_name} = :#{field_name}"
1006
+ end
1007
+ new_values[:lastmodified] = Time.now.to_i
1008
+ new_values[:id] = subscription_id
1009
+
1010
+ ds = @sequel[
1011
+ "UPDATE #{@calendar_subscriptions_table_name} SET #{values_sql.join(', ')}, lastmodified = :lastmodified WHERE id = :id",
1012
+ new_values
1013
+ ]
1014
+ ds.update
1015
+
1016
+ return true
1017
+ end
1018
+ )
1019
+ end
1020
+
1021
+ # Deletes a subscription
1022
+ #
1023
+ # @param mixed subscription_id
1024
+ # @return void
1025
+ def delete_subscription(subscription_id)
1026
+ ds = @sequel["DELETE FROM #{@calendar_subscriptions_table_name} WHERE id = ?", subscription_id]
1027
+ ds.delete
1028
+ end
1029
+
1030
+ # Returns a single scheduling object.
1031
+ #
1032
+ # The returned array should contain the following elements:
1033
+ # * uri - A unique basename for the object. This will be used to
1034
+ # construct a full uri.
1035
+ # * calendardata - The iCalendar object
1036
+ # * lastmodified - The last modification date. Can be an int for a unix
1037
+ # timestamp, or a PHP DateTime object.
1038
+ # * etag - A unique token that must change if the object changed.
1039
+ # * size - The size of the object, in bytes.
1040
+ #
1041
+ # @param string principal_uri
1042
+ # @param string object_uri
1043
+ # @return array
1044
+ def scheduling_object(principal_uri, object_uri)
1045
+ ds = @sequel[
1046
+ "SELECT uri, calendardata, lastmodified, etag, size FROM #{@scheduling_object_table_name} WHERE principaluri = ? AND uri = ?",
1047
+ principal_uri,
1048
+ object_uri
1049
+ ]
1050
+ row = ds.all.first
1051
+
1052
+ return nil unless row
1053
+
1054
+ {
1055
+ 'uri' => row[:uri],
1056
+ 'calendardata' => row[:calendardata],
1057
+ 'lastmodified' => row[:lastmodified],
1058
+ 'etag' => "\"#{row[:etag]}\"",
1059
+ 'size' => row[:size].to_i
1060
+ }
1061
+ end
1062
+
1063
+ # Returns all scheduling objects for the inbox collection.
1064
+ #
1065
+ # These objects should be returned as an array. Every item in the array
1066
+ # should follow the same structure as returned from getSchedulingObject.
1067
+ #
1068
+ # The main difference is that 'calendardata' is optional.
1069
+ #
1070
+ # @param string principal_uri
1071
+ # @return array
1072
+ def scheduling_objects(principal_uri)
1073
+ result = []
1074
+ stmt = @sequel.fetch("SELECT id, calendardata, uri, lastmodified, etag, size FROM #{@scheduling_object_table_name} WHERE principaluri = ?", principal_uri) do |row|
1075
+ result << {
1076
+ 'calendardata' => row[:calendardata],
1077
+ 'uri' => row[:uri],
1078
+ 'lastmodified' => row[:lastmodified],
1079
+ 'etag' => "\"#{row[:etag]}\"",
1080
+ 'size' => row[:size].to_i
1081
+ }
1082
+ end
1083
+
1084
+ result
1085
+ end
1086
+
1087
+ # Deletes a scheduling object
1088
+ #
1089
+ # @param string principal_uri
1090
+ # @param string object_uri
1091
+ # @return void
1092
+ def delete_scheduling_object(principal_uri, object_uri)
1093
+ ds = @sequel["DELETE FROM #{@scheduling_object_table_name} WHERE principaluri = ? AND uri = ?", principal_uri, object_uri]
1094
+ ds.delete
1095
+ end
1096
+
1097
+ # Creates a new scheduling object. This should land in a users' inbox.
1098
+ #
1099
+ # @param string principal_uri
1100
+ # @param string object_uri
1101
+ # @param string object_data
1102
+ # @return void
1103
+ def create_scheduling_object(principal_uri, object_uri, object_data)
1104
+ ds = @sequel[
1105
+ "INSERT INTO #{@scheduling_object_table_name} (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)",
1106
+ principal_uri,
1107
+ object_data,
1108
+ object_uri,
1109
+ Time.now.to_i,
1110
+ Digest::MD5.hexdigest(object_data),
1111
+ object_data.size
1112
+ ]
1113
+ ds.insert
1114
+ end
1115
+ end
1116
+ end
1117
+ end
1118
+ end