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,131 @@
1
+ module Tilia
2
+ module Dav
3
+ module PropertyStorage
4
+ # PropertyStorage Plugin.
5
+ #
6
+ # Adding this plugin to your server allows clients to store any arbitrary
7
+ # WebDAV property.
8
+ #
9
+ # See:
10
+ # http://sabre.io/dav/property-storage/
11
+ #
12
+ # for more information.
13
+ class Plugin < ServerPlugin
14
+ # If you only want this plugin to store properties for a limited set of
15
+ # paths, you can use a pathFilter to do this.
16
+ #
17
+ # The pathFilter should be a callable. The callable retrieves a path as
18
+ # its argument, and should return true or false wether it allows
19
+ # properties to be stored.
20
+ #
21
+ # @var callable
22
+ attr_accessor :path_filter
23
+
24
+ # Creates the plugin
25
+ #
26
+ # @param Backend\BackendInterface backend
27
+ def initialize(backend)
28
+ @backend = backend
29
+ end
30
+
31
+ # This initializes the plugin.
32
+ #
33
+ # This function is called by Sabre\DAV\Server, after
34
+ # addPlugin is called.
35
+ #
36
+ # This method should set up the required event subscriptions.
37
+ #
38
+ # @param Server server
39
+ # @return void
40
+ def setup(server)
41
+ server.on('propFind', method(:prop_find), 130)
42
+ server.on('propPatch', method(:prop_patch), 300)
43
+ server.on('afterMove', method(:after_move))
44
+ server.on('afterUnbind', method(:after_unbind))
45
+ end
46
+
47
+ # Called during PROPFIND operations.
48
+ #
49
+ # If there's any requested properties that don't have a value yet, this
50
+ # plugin will look in the property storage backend to find them.
51
+ #
52
+ # @param PropFind prop_find
53
+ # @param INode node
54
+ # @return void
55
+ def prop_find(prop_find, _node)
56
+ path = prop_find.path
57
+ return nil if path_filter && !path_filter.call(path)
58
+ @backend.prop_find(prop_find.path, prop_find)
59
+ end
60
+
61
+ # Called during PROPPATCH operations
62
+ #
63
+ # If there's any updated properties that haven't been stored, the
64
+ # propertystorage backend can handle it.
65
+ #
66
+ # @param string path
67
+ # @param PropPatch prop_patch
68
+ # @return void
69
+ def prop_patch(path, prop_patch)
70
+ return nil if path_filter && !path_filter.call(path)
71
+ @backend.prop_patch(path, prop_patch)
72
+ end
73
+
74
+ # Called after a node is deleted.
75
+ #
76
+ # This allows the backend to clean up any properties still in the
77
+ # database.
78
+ #
79
+ # @param string path
80
+ # @return void
81
+ def after_unbind(path)
82
+ return nil if path_filter && !path_filter.call(path)
83
+ @backend.delete(path)
84
+ end
85
+
86
+ # Called after a node is moved.
87
+ #
88
+ # This allows the backend to move all the associated properties.
89
+ #
90
+ # @param string source
91
+ # @param string destination
92
+ # @return void
93
+ def after_move(source, destination)
94
+ return nil if path_filter && !path_filter.call(source)
95
+ # If the destination is filtered, afterUnbind will handle cleaning up
96
+ # the properties.
97
+ return nil if path_filter && !path_filter(destination)
98
+
99
+ @backend.move(source, destination)
100
+ end
101
+
102
+ # Returns a plugin name.
103
+ #
104
+ # Using this name other plugins will be able to access other plugins
105
+ # using \Sabre\DAV\Server::getPlugin
106
+ #
107
+ # @return string
108
+ def plugin_name
109
+ 'property-storage'
110
+ end
111
+
112
+ # Returns a bunch of meta-data about the plugin.
113
+ #
114
+ # Providing this information is optional, and is mainly displayed by the
115
+ # Browser plugin.
116
+ #
117
+ # The description key in the returned array may contain html and will not
118
+ # be sanitized.
119
+ #
120
+ # @return array
121
+ def plugin_info
122
+ {
123
+ 'name' => plugin_name,
124
+ 'description' => 'This plugin allows any arbitrary WebDAV property to be set on any resource.',
125
+ 'link' => 'http://sabre.io/dav/property-storage/'
126
+ }
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,1388 @@
1
+ require 'uri'
2
+
3
+ module Tilia
4
+ module Dav
5
+ # Main DAV server class
6
+ class Server < Event::EventEmitter
7
+ # Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
8
+ DEPTH_INFINITY = -1
9
+
10
+ # XML namespace for all SabreDAV related elements
11
+ NS_SABREDAV = 'http://sabredav.org/ns'
12
+
13
+ # The tree object
14
+ #
15
+ # @var Sabre\DAV\Tree
16
+ attr_accessor :tree
17
+
18
+ # The base uri
19
+ #
20
+ # @var string
21
+ # RUBY: attr_accessor :base_uri
22
+
23
+ # httpResponse
24
+ #
25
+ # @var Sabre\HTTP\Response
26
+ attr_accessor :http_response
27
+
28
+ # httpRequest
29
+ #
30
+ # @var Sabre\HTTP\Request
31
+ attr_accessor :http_request
32
+
33
+ # PHP HTTP Sapi
34
+ #
35
+ # @var Sabre\HTTP\Sapi
36
+ attr_accessor :sapi
37
+
38
+ # The list of plugins
39
+ #
40
+ # @var array
41
+ # RUBY: attr_accessor :plugins
42
+
43
+ # This property will be filled with a unique string that describes the
44
+ # transaction. This is useful for performance measuring and logging
45
+ # purposes.
46
+ #
47
+ # By default it will just fill it with a lowercased HTTP method name, but
48
+ # plugins override this. For example, the WebDAV-Sync sync-collection
49
+ # report will set this to 'report-sync-collection'.
50
+ #
51
+ # @var string
52
+ attr_accessor :transaction_type
53
+
54
+ # This is a list of properties that are always server-controlled, and
55
+ # must not get modified with PROPPATCH.
56
+ #
57
+ # Plugins may add to this list.
58
+ #
59
+ # @var string[]
60
+ attr_accessor :protected_properties
61
+
62
+ # This is a flag that allow or not showing file, line and code
63
+ # of the exception in the returned XML
64
+ #
65
+ # @var bool
66
+ attr_accessor :debug_exceptions
67
+
68
+ # This property allows you to automatically add the 'resourcetype' value
69
+ # based on a node's classname or interface.
70
+ #
71
+ # The preset ensures that {DAV:}collection is automatically added for nodes
72
+ # implementing Sabre\DAV\ICollection.
73
+ #
74
+ # @var array
75
+ attr_accessor :resource_type_mapping
76
+
77
+ # This property allows the usage of Depth: infinity on PROPFIND requests.
78
+ #
79
+ # By default Depth: infinity is treated as Depth: 1. Allowing Depth:
80
+ # infinity is potentially risky, as it allows a single client to do a full
81
+ # index of the webdav server, which is an easy DoS attack vector.
82
+ #
83
+ # Only turn this on if you know what you're doing.
84
+ #
85
+ # @var bool
86
+ attr_accessor :enable_propfind_depth_infinity
87
+
88
+ # Reference to the XML utility object.
89
+ #
90
+ # @var Xml\Service
91
+ attr_accessor :xml
92
+
93
+ # If this setting is turned off, SabreDAV's version number will be hidden
94
+ # from various places.
95
+ #
96
+ # Some people feel this is a good security measure.
97
+ #
98
+ # @var bool
99
+ @expose_version = true
100
+
101
+ class << self
102
+ attr_accessor :expose_version
103
+ end
104
+
105
+ # Sets up the server
106
+ #
107
+ # If a Sabre\DAV\Tree object is passed as an argument, it will
108
+ # use it as the directory tree. If a Sabre\DAV\INode is passed, it
109
+ # will create a Sabre\DAV\Tree and use the node as the root.
110
+ #
111
+ # If nothing is passed, a Sabre\DAV\SimpleCollection is created in
112
+ # a Sabre\DAV\Tree.
113
+ #
114
+ # If an array is passed, we automatically create a root node, and use
115
+ # the nodes in the array as top-level children.
116
+ #
117
+ # @param Tree|INode|array|null tree_or_node The tree object
118
+ def initialize(env, tree_or_node = nil)
119
+ super() # super without parenthesis would call initialize with our args
120
+
121
+ @plugins = {}
122
+ @protected_properties = [
123
+ # RFC4918
124
+ '{DAV:}getcontentlength',
125
+ '{DAV:}getetag',
126
+ '{DAV:}getlastmodified',
127
+ '{DAV:}lockdiscovery',
128
+ '{DAV:}supportedlock',
129
+
130
+ # RFC4331
131
+ '{DAV:}quota-available-bytes',
132
+ '{DAV:}quota-used-bytes',
133
+
134
+ # RFC3744
135
+ '{DAV:}supported-privilege-set',
136
+ '{DAV:}current-user-privilege-set',
137
+ '{DAV:}acl',
138
+ '{DAV:}acl-restrictions',
139
+ '{DAV:}inherited-acl-set',
140
+
141
+ # RFC3253
142
+ '{DAV:}supported-method-set',
143
+ '{DAV:}supported-report-set',
144
+
145
+ # RFC6578
146
+ '{DAV:}sync-token',
147
+
148
+ # calendarserver.org extensions
149
+ '{http://calendarserver.org/ns/}ctag',
150
+
151
+ # sabredav extensions
152
+ '{http://sabredav.org/ns}sync-token'
153
+ ]
154
+ @debug_exceptions = false
155
+ @resource_type_mapping = {
156
+ Tilia::Dav::ICollection => '{DAV:}collection'
157
+ }
158
+ @enable_propfind_depth_infinity = false
159
+
160
+ if tree_or_node.is_a?(Tree)
161
+ @tree = tree_or_node
162
+ elsif tree_or_node.is_a?(INode)
163
+ @tree = Tree.new(tree_or_node)
164
+ elsif tree_or_node.is_a?(Array)
165
+ # If it's an array, a list of nodes was passed, and we need to
166
+ # create the root node.
167
+ tree_or_node.each do |node|
168
+ unless node.is_a?(INode)
169
+ fail Exception, 'Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Tilia::Dav::INode'
170
+ end
171
+ end
172
+
173
+ root = SimpleCollection.new('root', tree_or_node)
174
+ @tree = Tree.new(root)
175
+ elsif tree_or_node.nil?
176
+ root = SimpleCollection.new('root')
177
+ @tree = Tree.new(root)
178
+ else
179
+ fail Exception, 'Invalid argument passed to constructor. Argument must either be an instance of Tilia::Dav::Tree, Tilia::Dav::INode, an array or nil'
180
+ end
181
+
182
+ @xml = Xml::Service.new
183
+ @sapi = Http::Sapi.new(env)
184
+ @http_response = Http::Response.new
185
+ @http_request = @sapi.request
186
+ add_plugin(CorePlugin.new)
187
+ end
188
+
189
+ # Starts the DAV Server
190
+ #
191
+ # @return void
192
+ def exec
193
+ # If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
194
+ # origin, we must make sure we send back HTTP/1.0 if this was
195
+ # requested.
196
+ # This is mainly because nginx doesn't support Chunked Transfer
197
+ # Encoding, and this forces the webserver SabreDAV is running on,
198
+ # to buffer entire responses to calculate Content-Length.
199
+ @http_response.http_version = @http_request.http_version
200
+
201
+ # Setting the base url
202
+ @http_request.base_url = base_uri
203
+ invoke_method(@http_request, @http_response)
204
+ rescue Exception => e # use Exception (without ::) for easier debugging
205
+ begin
206
+ emit('exception', [e])
207
+ rescue
208
+ end
209
+
210
+ dom = LibXML::XML::Document.new
211
+
212
+ error = LibXML::XML::Node.new('d:error')
213
+ LibXML::XML::Namespace.new(error, 'd', 'DAV:')
214
+ LibXML::XML::Namespace.new(error, 's', NS_SABREDAV)
215
+ dom.root = error
216
+
217
+ h = lambda do |v|
218
+ CGI.escapeHTML(v)
219
+ end
220
+
221
+ if self.class.expose_version
222
+ error << LibXML::XML::Node.new('s:sabredav-version', h.call(Version::VERSION))
223
+ end
224
+
225
+ error << LibXML::XML::Node.new('s:exception', h.call(e.class.to_s))
226
+ error << LibXML::XML::Node.new('s:message', h.call(e.message))
227
+ if @debug_exceptions
228
+ # writer.write_element('s:file', h.call(e.get_file))
229
+ # writer.write_element('s:line', h.call(e.get_line))
230
+ # writer.write_element('s:code', h.call(e.get_code))
231
+ # writer.write_element('s:stacktrace', h.call(e.get_trace_as_string))
232
+ end
233
+
234
+ if @debug_exceptions
235
+ # previous = e
236
+ # while previous = previous.get_previous
237
+ # x_previous = $DOM.create_element('s:previous-exception')
238
+ # x_previous.append_child($DOM.create_element('s:exception', h.call(get_class(previous))))
239
+ # x_previous.append_child($DOM.create_element('s:message', h.call(previous.get_message)))
240
+ # x_previous.append_child($DOM.create_element('s:file', h.call(previous.get_file)))
241
+ # x_previous.append_child($DOM.create_element('s:line', h.call(previous.get_line)))
242
+ # x_previous.append_child($DOM.create_element('s:code', h.call(previous.get_code)))
243
+ # x_previous.append_child($DOM.create_element('s:stacktrace', h.call(previous.get_trace_as_string)))
244
+ # error.append_child(x_previous)
245
+ # end
246
+ end
247
+
248
+ if e.is_a?(Exception)
249
+ http_code = e.http_code
250
+ e.serialize(self, error)
251
+ headers = e.http_headers(self)
252
+ else
253
+ http_code = 500
254
+ headers = {}
255
+ end
256
+
257
+ headers['Content-Type'] = 'application/xml; charset=utf-8'
258
+ @http_response.status = http_code
259
+ @http_response.update_headers(headers)
260
+ @http_response.body = dom.to_s
261
+ sapi.send_response(@http_response)
262
+ end
263
+
264
+ # Sets the base server uri
265
+ #
266
+ # @param string uri
267
+ # @return void
268
+ def base_uri=(uri)
269
+ # If the baseUri does not end with a slash, we must add it
270
+ uri += '/' unless uri[-1] == '/'
271
+ @base_uri = uri
272
+ end
273
+
274
+ # Returns the base responding uri
275
+ #
276
+ # @return string
277
+ def base_uri
278
+ @base_uri ||= guess_base_uri
279
+ end
280
+
281
+ # This method attempts to detect the base uri.
282
+ # Only the PATH_INFO variable is considered.
283
+ #
284
+ # If this variable is not set, the root (/) is assumed.
285
+ #
286
+ # @return string
287
+ def guess_base_uri
288
+ path_info = @http_request.raw_server_value('PATH_INFO') || ''
289
+ uri = @http_request.raw_server_value('REQUEST_PATH') || ''
290
+
291
+ # If PATH_INFO is found, we can assume it's accurate.
292
+ unless path_info.blank?
293
+ # We need to make sure we ignore the QUERY_STRING part
294
+ pos = uri.index('?')
295
+ uri = uri[0...pos] if pos
296
+
297
+ # PATH_INFO is only set for urls, such as: /example.php/path
298
+ # in that case PATH_INFO contains '/path'.
299
+ # Note that REQUEST_URI is percent encoded, while PATH_INFO is
300
+ # not, Therefore they are only comparable if we first decode
301
+ # REQUEST_INFO as well.
302
+ decoded_uri = Http::UrlUtil.decode_path(uri)
303
+
304
+ # A simple sanity check:
305
+ if decoded_uri[(decoded_uri.length - path_info.length)..-1] == path_info
306
+ base_uri = decoded_uri[0, decoded_uri.length - path_info.length]
307
+ return base_uri.gsub(%r{/+$}, '') + '/'
308
+ end
309
+
310
+ fail Exception, "The REQUEST_URI (#{uri}) did not end with the contents of PATH_INFO (#{path_info}). This server might be misconfigured."
311
+ end
312
+
313
+ # The last fallback is that we're just going to assume the server root.
314
+ '/'
315
+ end
316
+
317
+ # Adds a plugin to the server
318
+ #
319
+ # For more information, console the documentation of Sabre\DAV\ServerPlugin
320
+ #
321
+ # @param ServerPlugin plugin
322
+ # @return void
323
+ def add_plugin(plugin)
324
+ @plugins[plugin.plugin_name] = plugin
325
+ plugin.setup(self)
326
+ end
327
+
328
+ # Returns an initialized plugin by it's name.
329
+ #
330
+ # This function returns null if the plugin was not found.
331
+ #
332
+ # @param string name
333
+ # @return ServerPlugin
334
+ def plugin(name)
335
+ @plugins[name]
336
+ end
337
+
338
+ # Returns all plugins
339
+ #
340
+ # @return array
341
+ attr_reader :plugins
342
+
343
+ # Handles a http request, and execute a method based on its name
344
+ #
345
+ # @param RequestInterface request
346
+ # @param ResponseInterface response
347
+ # @param send_response Whether to send the HTTP response to the DAV client.
348
+ # @return void
349
+ def invoke_method(request, response, _send_response = true)
350
+ method = request.method
351
+
352
+ return nil unless emit("beforeMethod:#{method}", [request, response])
353
+ return nil unless emit('beforeMethod', [request, response])
354
+
355
+ if Server.expose_version
356
+ response.update_header('X-Sabre-Version', Version::VERSION)
357
+ end
358
+
359
+ @transaction_type = method.downcase
360
+
361
+ unless check_preconditions(request, response)
362
+ @sapi.send_response(response)
363
+ return nil
364
+ end
365
+
366
+ if emit("method:#{method}", [request, response])
367
+ if emit('method', [request, response])
368
+ # Unsupported method
369
+ fail Exception::NotImplemented, "'There was no handler found for this \"#{method}\" method"
370
+ end
371
+ end
372
+
373
+ return nil unless emit("afterMethod:#{method}", [request, response])
374
+ return nil unless emit('afterMethod', [request, response])
375
+
376
+ # No need for checking, send_response just returns an array
377
+ response = sapi.send_response(response)
378
+ end
379
+
380
+ # {{{ HTTP/WebDAV protocol helpers
381
+
382
+ # Returns an array with all the supported HTTP methods for a specific uri.
383
+ #
384
+ # @param string path
385
+ # @return array
386
+ def allowed_methods(path)
387
+ methods = [
388
+ 'OPTIONS',
389
+ 'GET',
390
+ 'HEAD',
391
+ 'DELETE',
392
+ 'PROPFIND',
393
+ 'PUT',
394
+ 'PROPPATCH',
395
+ 'COPY',
396
+ 'MOVE',
397
+ 'REPORT'
398
+ ]
399
+
400
+ # The MKCOL is only allowed on an unmapped uri
401
+ begin
402
+ @tree.node_for_path(path)
403
+ rescue Exception::NotFound => e
404
+ methods << 'MKCOL'
405
+ end
406
+
407
+ # We're also checking if any of the plugins register any new methods
408
+ @plugins.each do |_, plugin|
409
+ methods += plugin.http_methods(path)
410
+ end
411
+
412
+ methods.uniq
413
+ end
414
+
415
+ # Gets the uri for the request, keeping the base uri into consideration
416
+ #
417
+ # @return string
418
+ def request_uri
419
+ calculate_uri(@http_request.url)
420
+ end
421
+
422
+ # Calculates the uri for a request, making sure that the base uri is stripped out
423
+ #
424
+ # @param string uri
425
+ # @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
426
+ # @return string
427
+ def calculate_uri(uri)
428
+ if uri[0] != '/' && uri.index('://') && uri.index('://') > 0
429
+ uri = ::URI.split(uri)[5] # path component of uri
430
+ end
431
+
432
+ uri = Uri.normalize(uri.gsub('//', '/'))
433
+ base_uri = Uri.normalize(self.base_uri)
434
+
435
+ if uri.index(base_uri) == 0
436
+ return Http::UrlUtil.decode_path(uri[base_uri.length..-1]).gsub(%r{^/+}, '').gsub(%r{/+$}, '')
437
+
438
+ elsif "#{uri}/" == base_uri
439
+ # A special case, if the baseUri was accessed without a trailing
440
+ # slash, we'll accept it as well.
441
+ return ''
442
+ else
443
+ fail Exception::Forbidden, "Requested uri (#{uri}) is out of base uri (#{self.base_uri})"
444
+ end
445
+ end
446
+
447
+ # Returns the HTTP depth header
448
+ #
449
+ # This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object
450
+ # It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
451
+ #
452
+ # @param mixed default
453
+ # @return int
454
+ def http_depth(default = DEPTH_INFINITY)
455
+ # If its not set, we'll grab the default
456
+ depth = @http_request.header('Depth')
457
+
458
+ return default unless depth
459
+
460
+ return DEPTH_INFINITY if depth == 'infinity'
461
+
462
+ # If its an unknown value. we'll grab the default
463
+ return default unless depth =~ /^[\+\-0-9\.]$/ # TODO: valid replacement for ctype_digit?
464
+
465
+ depth.to_i
466
+ end
467
+
468
+ # Returns the HTTP range header
469
+ #
470
+ # This method returns null if there is no well-formed HTTP range request
471
+ # header or array(start, end).
472
+ #
473
+ # The first number is the offset of the first byte in the range.
474
+ # The second number is the offset of the last byte in the range.
475
+ #
476
+ # If the second offset is null, it should be treated as the offset of the last byte of the entity
477
+ # If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
478
+ #
479
+ # @return array|null
480
+ def http_range
481
+ range = @http_request.header('range')
482
+ return nil unless range
483
+
484
+ # Matching "Range: bytes=1234-5678: both numbers are optional
485
+ matches = /^bytes=([0-9]*)-([0-9]*)$/i.match(range)
486
+ return nil unless matches
487
+
488
+ return nil if matches[1] == '' && matches[2] == ''
489
+
490
+ [
491
+ matches[1] != '' ? matches[1].to_i : nil,
492
+ matches[2] != '' ? matches[2].to_i : nil
493
+ ]
494
+ end
495
+
496
+ # Returns the HTTP Prefer header information.
497
+ #
498
+ # The prefer header is defined in:
499
+ # http://tools.ietf.org/html/draft-snell-http-prefer-14
500
+ #
501
+ # This method will return an array with options.
502
+ #
503
+ # Currently, the following options may be returned:
504
+ # [
505
+ # 'return-asynch' => true,
506
+ # 'return-minimal' => true,
507
+ # 'return-representation' => true,
508
+ # 'wait' => 30,
509
+ # 'strict' => true,
510
+ # 'lenient' => true,
511
+ # ]
512
+ #
513
+ # This method also supports the Brief header, and will also return
514
+ # 'return-minimal' if the brief header was set to 't'.
515
+ #
516
+ # For the boolean options, false will be returned if the headers are not
517
+ # specified. For the integer options it will be 'null'.
518
+ #
519
+ # @return array
520
+ def http_prefer
521
+ result = {
522
+ # can be true or false
523
+ 'respond-async' => false,
524
+ # Could be set to 'representation' or 'minimal'.
525
+ 'return' => nil,
526
+ # Used as a timeout, is usually a number.
527
+ 'wait' => nil,
528
+ # can be 'strict' or 'lenient'.
529
+ 'handling' => false
530
+ }
531
+
532
+ prefer = @http_request.header('Prefer')
533
+ if prefer
534
+ result = result.merge(Tilia::Http.parse_prefer(prefer))
535
+ elsif @http_request.header('Brief') == 't'
536
+ result['return'] = 'minimal'
537
+ end
538
+
539
+ result
540
+ end
541
+
542
+ # Returns information about Copy and Move requests
543
+ #
544
+ # This function is created to help getting information about the source and the destination for the
545
+ # WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
546
+ #
547
+ # The returned value is an array with the following keys:
548
+ # * destination - Destination path
549
+ # * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
550
+ #
551
+ # @param RequestInterface request
552
+ # @throws Exception\BadRequest upon missing or broken request headers
553
+ # @throws Exception\UnsupportedMediaType when trying to copy into a
554
+ # non-collection.
555
+ # @throws Exception\PreconditionFailed If overwrite is set to false, but
556
+ # the destination exists.
557
+ # @throws Exception\Forbidden when source and destination paths are
558
+ # identical.
559
+ # @throws Exception\Conflict When trying to copy a node into its own
560
+ # subtree.
561
+ # @return array
562
+ def copy_and_move_info(request)
563
+ # Collecting the relevant HTTP headers
564
+ unless request.header('Destination')
565
+ fail Exception::BadRequest, 'The destination header was not supplied'
566
+ end
567
+
568
+ destination = calculate_uri(request.header('Destination'))
569
+ overwrite = request.header('Overwrite')
570
+
571
+ overwrite = 'T' unless overwrite
572
+ if overwrite.upcase == 'T'
573
+ overwrite = true
574
+ elsif overwrite.upcase == 'F'
575
+ overwrite = false
576
+ else
577
+ # We need to throw a bad request exception, if the header was invalid
578
+ fail Exception::BadRequest, 'The HTTP Overwrite header should be either T or F'
579
+ end
580
+
581
+ (destination_dir,) = Http::UrlUtil.split_path(destination)
582
+
583
+ begin
584
+ destination_parent = @tree.node_for_path(destination_dir)
585
+
586
+ unless destination_parent.is_a?(ICollection)
587
+ fail Exception::UnsupportedMediaType, 'The destination node is not a collection'
588
+ end
589
+ rescue Exception::NotFound => e
590
+ # If the destination parent node is not found, we throw a 409
591
+ raise Exception::Conflict, 'The destination node is not found'
592
+ end
593
+
594
+ begin
595
+ destination_node = @tree.node_for_path(destination)
596
+
597
+ # If this succeeded, it means the destination already exists
598
+ # we'll need to throw precondition failed in case overwrite is false
599
+ unless overwrite
600
+ fail Exception::PreconditionFailed, 'The destination node already exists, and the overwrite header is set to false', 'Overwrite'
601
+ end
602
+ rescue Exception::NotFound => e
603
+ # Destination didn't exist, we're all good
604
+ destination_node = false
605
+ end
606
+
607
+ request_path = request.path
608
+ if destination == request_path
609
+ fail Exception::Forbidden, 'Source and destination uri are identical.'
610
+ end
611
+ if destination[0..request_path.length] == request_path + '/'
612
+ fail Exception::Conflict, 'The destination may not be part of the same subtree as the source path.'
613
+ end
614
+
615
+ # These are the three relevant properties we need to return
616
+ {
617
+ 'destination' => destination,
618
+ 'destinationExists' => !!destination_node,
619
+ 'destinationNode' => destination_node
620
+ }
621
+ end
622
+
623
+ # Returns a list of properties for a path
624
+ #
625
+ # This is a simplified version getPropertiesForPath. If you aren't
626
+ # interested in status codes, but you just want to have a flat list of
627
+ # properties, use this method.
628
+ #
629
+ # Please note though that any problems related to retrieving properties,
630
+ # such as permission issues will just result in an empty array being
631
+ # returned.
632
+ #
633
+ # @param string path
634
+ # @param array property_names
635
+ def properties(path, property_names)
636
+ result = properties_for_path(path, property_names, 0)
637
+ if result[0].key?(200)
638
+ return result[0][200]
639
+ else
640
+ return []
641
+ end
642
+ end
643
+
644
+ # A kid-friendly way to fetch properties for a node's children.
645
+ #
646
+ # The returned array will be indexed by the path of the of child node.
647
+ # Only properties that are actually found will be returned.
648
+ #
649
+ # The parent node will not be returned.
650
+ #
651
+ # @param string path
652
+ # @param array property_names
653
+ # @return array
654
+ def properties_for_children(path, property_names)
655
+ result = {}
656
+ properties_for_path(path, property_names, 1).each_with_index do |row, k|
657
+ # Skipping the parent path
658
+ next if k == 0
659
+
660
+ result[row['href']] = row[200]
661
+ end
662
+
663
+ result
664
+ end
665
+
666
+ # Returns a list of HTTP headers for a particular resource
667
+ #
668
+ # The generated http headers are based on properties provided by the
669
+ # resource. The method basically provides a simple mapping between
670
+ # DAV property and HTTP header.
671
+ #
672
+ # The headers are intended to be used for HEAD and GET requests.
673
+ #
674
+ # @param string path
675
+ # @return array
676
+ def http_headers(path)
677
+ property_map = {
678
+ '{DAV:}getcontenttype' => 'Content-Type',
679
+ '{DAV:}getcontentlength' => 'Content-Length',
680
+ '{DAV:}getlastmodified' => 'Last-Modified',
681
+ '{DAV:}getetag' => 'ETag'
682
+ }
683
+
684
+ properties = properties(path, property_map.keys)
685
+
686
+ headers = {}
687
+ property_map.each do |property, header|
688
+ next unless properties.key?(property)
689
+
690
+ if properties[property].scalar?
691
+ headers[header] = properties[property]
692
+ elsif properties[property].is_a?(Xml::Property::GetLastModified)
693
+ # GetLastModified gets special cased
694
+ headers[header] = Http::Util.to_http_date(properties[property].time)
695
+ end
696
+ end
697
+
698
+ headers
699
+ end
700
+
701
+ private
702
+
703
+ # Small helper to support PROPFIND with DEPTH_INFINITY.
704
+ #
705
+ # @param array[] prop_find_requests
706
+ # @param PropFind prop_find
707
+ # @return void
708
+ def add_path_nodes_recursively(prop_find_requests, prop_find)
709
+ new_depth = prop_find.depth
710
+ path = prop_find.path
711
+
712
+ new_depth -= 1 unless new_depth == DEPTH_INFINITY
713
+
714
+ @tree.children(path).each do |child_node|
715
+ sub_prop_find = prop_find.clone
716
+ sub_prop_find.depth = new_depth
717
+
718
+ if path != ''
719
+ sub_path = path + '/' + child_node.name
720
+ else
721
+ sub_path = child_node.name
722
+ end
723
+ sub_prop_find.path = sub_path
724
+
725
+ prop_find_requests << [
726
+ sub_prop_find,
727
+ child_node
728
+ ]
729
+
730
+ if (new_depth == DEPTH_INFINITY || new_depth >= 1) && child_node.is_a?(ICollection)
731
+ add_path_nodes_recursively(prop_find_requests, sub_prop_find)
732
+ end
733
+ end
734
+ end
735
+
736
+ public
737
+
738
+ # Returns a list of properties for a given path
739
+ #
740
+ # The path that should be supplied should have the baseUrl stripped out
741
+ # The list of properties should be supplied in Clark notation. If the list is empty
742
+ # 'allprops' is assumed.
743
+ #
744
+ # If a depth of 1 is requested child elements will also be returned.
745
+ #
746
+ # @param string path
747
+ # @param array property_names
748
+ # @param int depth
749
+ # @return array
750
+ def properties_for_path(path, property_names = [], depth = 0)
751
+ property_names = [property_names] unless property_names.is_a?(Array)
752
+
753
+ # The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
754
+ depth = 1 unless @enable_propfind_depth_infinity || depth == 0
755
+
756
+ path = path.gsub(%r{^/+}, '').gsub(%r{/+$}, '')
757
+
758
+ prop_find_type = property_names.any? ? PropFind::NORMAL : PropFind::ALLPROPS
759
+ prop_find = PropFind.new(path, property_names, depth, prop_find_type)
760
+
761
+ parent_node = @tree.node_for_path(path)
762
+
763
+ prop_find_requests = [
764
+ [
765
+ prop_find,
766
+ parent_node
767
+ ]
768
+ ]
769
+
770
+ if (depth > 0 || depth == DEPTH_INFINITY) && parent_node.is_a?(ICollection)
771
+ add_path_nodes_recursively(prop_find_requests, prop_find)
772
+ end
773
+
774
+ return_property_list = []
775
+
776
+ prop_find_requests.each do |prop_find_request|
777
+ (prop_find, node) = prop_find_request
778
+ r = properties_by_node(prop_find, node)
779
+ next unless r
780
+ result = prop_find.result_for_multi_status
781
+ result['href'] = prop_find.path
782
+
783
+ # WebDAV recommends adding a slash to the path, if the path is
784
+ # a collection.
785
+ # Furthermore, iCal also demands this to be the case for
786
+ # principals. This is non-standard, but we support it.
787
+ resource_type = resource_type_for_node(node)
788
+ if resource_type.include?('{DAV:}collection') || resource_type.include?('{DAV:}principal')
789
+ result['href'] += '/'
790
+ end
791
+ return_property_list << result
792
+ end
793
+
794
+ return_property_list
795
+ end
796
+
797
+ # Returns a list of properties for a list of paths.
798
+ #
799
+ # The path that should be supplied should have the baseUrl stripped out
800
+ # The list of properties should be supplied in Clark notation. If the list is empty
801
+ # 'allprops' is assumed.
802
+ #
803
+ # The result is returned as an array, with paths for it's keys.
804
+ # The result may be returned out of order.
805
+ #
806
+ # @param array paths
807
+ # @param array property_names
808
+ # @return array
809
+ def properties_for_multiple_paths(paths, property_names = [])
810
+ result = {}
811
+
812
+ nodes = @tree.multiple_nodes(paths)
813
+
814
+ nodes.each do |path, node|
815
+ prop_find = PropFind.new(path, property_names)
816
+ r = properties_by_node(prop_find, node)
817
+ next unless r
818
+ result[path] = prop_find.result_for_multi_status
819
+ result[path]['href'] = path
820
+
821
+ resource_type = resource_type_for_node(node)
822
+ if resource_type.include?('{DAV:}collection') || resource_type.include?('{DAV:}principal')
823
+ result[path]['href'] += '/'
824
+ end
825
+ end
826
+
827
+ result
828
+ end
829
+
830
+ # Determines all properties for a node.
831
+ #
832
+ # This method tries to grab all properties for a node. This method is used
833
+ # internally getPropertiesForPath and a few others.
834
+ #
835
+ # It could be useful to call this, if you already have an instance of your
836
+ # target node and simply want to run through the system to get a correct
837
+ # list of properties.
838
+ #
839
+ # @param PropFind prop_find
840
+ # @param INode node
841
+ # @return bool
842
+ def properties_by_node(prop_find, node)
843
+ emit('propFind', [prop_find, node])
844
+ end
845
+
846
+ # This method is invoked by sub-systems creating a new file.
847
+ #
848
+ # Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
849
+ # It was important to get this done through a centralized function,
850
+ # allowing plugins to intercept this using the beforeCreateFile event.
851
+ #
852
+ # This method will return true if the file was actually created
853
+ #
854
+ # @param string uri
855
+ # @param resource data
856
+ # @param string etag
857
+ # @return bool
858
+ def create_file(uri, data, etag = Box.new)
859
+ (dir, name) = Http::UrlUtil.split_path(uri)
860
+
861
+ return false unless emit('beforeBind', [uri])
862
+
863
+ parent = @tree.node_for_path(dir)
864
+ unless parent.is_a?(ICollection)
865
+ fail Exception::Conflict, 'Files can only be created as children of collections'
866
+ end
867
+
868
+ # It is possible for an event handler to modify the content of the
869
+ # body, before it gets written. If this is the case, modified
870
+ # should be set to true.
871
+ #
872
+ # If modified is true, we must not send back an ETag.
873
+ modified = Box.new(false)
874
+ box = Box.new(data)
875
+ return false unless emit('beforeCreateFile', [uri, box, parent, modified])
876
+ data = box.value
877
+
878
+ etag.value = parent.create_file(name, data)
879
+ etag.value = nil if modified.value
880
+
881
+ @tree.mark_dirty(dir + '/' + name)
882
+
883
+ emit('afterBind', [uri])
884
+ emit('afterCreateFile', [uri, parent])
885
+
886
+ true
887
+ end
888
+
889
+ # This method is invoked by sub-systems updating a file.
890
+ #
891
+ # This method will return true if the file was actually updated
892
+ #
893
+ # @param string uri
894
+ # @param resource data
895
+ # @param string etag
896
+ # @return bool
897
+ def update_file(uri, data, etag = Box.new)
898
+ node = @tree.node_for_path(uri)
899
+
900
+ # It is possible for an event handler to modify the content of the
901
+ # body, before it gets written. If this is the case, modified
902
+ # should be set to true.
903
+ #
904
+ # If modified is true, we must not send back an ETag.
905
+ modified = Box.new(false)
906
+ data = Box.new(data)
907
+
908
+ return false unless emit('beforeWriteContent', [uri, node, data, modified])
909
+
910
+ etag.value = node.put(data.value)
911
+ etag.value = nil if modified.value
912
+
913
+ emit('afterWriteContent', [uri, node])
914
+
915
+ true
916
+ end
917
+
918
+ # This method is invoked by sub-systems creating a new Directory.
919
+ #
920
+ # @param string uri
921
+ # @return void
922
+ def create_directory(uri)
923
+ create_collection(uri, MkCol.new(['{DAV:}collection'], []))
924
+ end
925
+
926
+ # Use this method to create a new collection
927
+ #
928
+ # @param string uri The new uri
929
+ # @param MkCol mk_col
930
+ # @return array|null
931
+ def create_collection(uri, mk_col)
932
+ (parent_uri, new_name) = Http::UrlUtil.split_path(uri)
933
+
934
+ # Making sure the parent exists
935
+ begin
936
+ parent = @tree.node_for_path(parent_uri)
937
+ rescue Exception::NotFound => e
938
+ raise Exception::Conflict, 'Parent node does not exist'
939
+ end
940
+
941
+ # Making sure the parent is a collection
942
+ unless parent.is_a?(ICollection)
943
+ fail Exception::Conflict, 'Parent node is not a collection'
944
+ end
945
+
946
+ # Making sure the child does not already exist
947
+ begin
948
+ parent.child(new_name)
949
+
950
+ # If we got here.. it means there's already a node on that url, and we need to throw a 405
951
+ fail Exception::MethodNotAllowed, 'The resource you tried to create already exists'
952
+ rescue Exception::NotFound => e
953
+ # NotFound is the expected behavior.
954
+ end
955
+
956
+ return nil unless emit('beforeBind', [uri])
957
+
958
+ if parent.is_a?(IExtendedCollection)
959
+ # If the parent is an instance of IExtendedCollection, it means that
960
+ # we can pass the MkCol object directly as it may be able to store
961
+ # properties immediately.
962
+ parent.create_extended_collection(new_name, mk_col)
963
+ else
964
+ # If the parent is a standard ICollection, it means only
965
+ # 'standard' collections can be created, so we should fail any
966
+ # MKCOL operation that carries extra resourcetypes.
967
+ if mk_col.resource_type.size > 1
968
+ fail Exception::InvalidResourceType, 'The {DAV:}resourcetype you specified is not supported here.'
969
+ end
970
+
971
+ parent.create_directory(new_name)
972
+ end
973
+
974
+ # If there are any properties that have not been handled/stored,
975
+ # we ask the 'propPatch' event to handle them. This will allow for
976
+ # example the propertyStorage system to store properties upon MKCOL.
977
+ emit('propPatch', [uri, mk_col]) if mk_col.remaining_mutations
978
+ success = mk_col.commit
979
+
980
+ unless success
981
+ result = mk_col.result
982
+ # generateMkCol needs the href key to exist.
983
+ result['href'] = uri
984
+ return result
985
+ end
986
+
987
+ @tree.mark_dirty(parent_uri)
988
+ emit('afterBind', [uri])
989
+ end
990
+
991
+ # This method updates a resource's properties
992
+ #
993
+ # The properties array must be a list of properties. Array-keys are
994
+ # property names in clarknotation, array-values are it's values.
995
+ # If a property must be deleted, the value should be null.
996
+ #
997
+ # Note that this request should either completely succeed, or
998
+ # completely fail.
999
+ #
1000
+ # The response is an array with properties for keys, and http status codes
1001
+ # as their values.
1002
+ #
1003
+ # @param string path
1004
+ # @param array properties
1005
+ # @return array
1006
+ def update_properties(path, properties)
1007
+ prop_patch = PropPatch.new(properties)
1008
+ emit('propPatch', [path, prop_patch])
1009
+ prop_patch.commit
1010
+
1011
+ prop_patch.result
1012
+ end
1013
+
1014
+ # This method checks the main HTTP preconditions.
1015
+ #
1016
+ # Currently these are:
1017
+ # * If-Match
1018
+ # * If-None-Match
1019
+ # * If-Modified-Since
1020
+ # * If-Unmodified-Since
1021
+ #
1022
+ # The method will return true if all preconditions are met
1023
+ # The method will return false, or throw an exception if preconditions
1024
+ # failed. If false is returned the operation should be aborted, and
1025
+ # the appropriate HTTP response headers are already set.
1026
+ #
1027
+ # Normally this method will throw 412 Precondition Failed for failures
1028
+ # related to If-None-Match, If-Match and If-Unmodified Since. It will
1029
+ # set the status to 304 Not Modified for If-Modified_since.
1030
+ #
1031
+ # @param RequestInterface request
1032
+ # @param ResponseInterface response
1033
+ # @return bool
1034
+ def check_preconditions(request, response)
1035
+ path = request.path
1036
+ node = nil
1037
+ last_mod = nil
1038
+ etag = nil
1039
+
1040
+ if_match = request.header('If-Match')
1041
+ if if_match
1042
+ # If-Match contains an entity tag. Only if the entity-tag
1043
+ # matches we are allowed to make the request succeed.
1044
+ # If the entity-tag is '*' we are only allowed to make the
1045
+ # request succeed if a resource exists at that url.
1046
+ begin
1047
+ node = @tree.node_for_path(path)
1048
+ rescue Exception::NotFound => e
1049
+ raise Exception::PreconditionFailed.new('If-Match'), 'An If-Match header was specified and the resource did not exist'
1050
+ end
1051
+
1052
+ # Only need to check entity tags if they are not *
1053
+ if if_match != '*'
1054
+ # There can be multiple ETags
1055
+ if_match = if_match.split(',')
1056
+ have_match = false
1057
+ if_match.each do |if_match_item|
1058
+ # Stripping any extra spaces
1059
+ if_match_item = if_match_item.strip
1060
+
1061
+ etag = node.is_a?(IFile) ? node.etag : nil
1062
+ if etag == if_match_item
1063
+ have_match = true
1064
+ else
1065
+ # Evolution has a bug where it sometimes prepends the "
1066
+ # with a \. This is our workaround.
1067
+ have_match = true if if_match_item.gsub('\\"', '"') == etag
1068
+ end
1069
+ end
1070
+
1071
+ unless have_match
1072
+ response.update_header('ETag', etag) if etag
1073
+ fail Exception::PreconditionFailed.new('If-Match'), 'An If-Match header was specified, but none of the specified the ETags matched.'
1074
+ end
1075
+ end
1076
+ end
1077
+
1078
+ if_none_match = request.header('If-None-Match')
1079
+ if if_none_match
1080
+ # The If-None-Match header contains an ETag.
1081
+ # Only if the ETag does not match the current ETag, the request will succeed
1082
+ # The header can also contain *, in which case the request
1083
+ # will only succeed if the entity does not exist at all.
1084
+ node_exists = true
1085
+ unless node
1086
+ begin
1087
+ node = @tree.node_for_path(path)
1088
+ rescue Exception::NotFound => e
1089
+ node_exists = false
1090
+ end
1091
+ end
1092
+
1093
+ if node_exists
1094
+ have_match = false
1095
+ if if_none_match == '*'
1096
+ have_match = true
1097
+ else
1098
+ # There might be multiple ETags
1099
+ if_none_match = if_none_match.split(',')
1100
+ etag = node.is_a?(IFile) ? node.etag : nil
1101
+
1102
+ if_none_match.each do |if_none_match_item|
1103
+ # Stripping any extra spaces
1104
+ if_none_match_item = if_none_match_item.strip
1105
+
1106
+ have_match = true if etag == if_none_match_item
1107
+ end
1108
+ end
1109
+
1110
+ if have_match
1111
+ response.update_header('ETag', etag) if etag
1112
+ if request.method == 'GET'
1113
+ response.status = 304
1114
+ return false
1115
+ else
1116
+ fail Exception::PreconditionFailed.new('If-None-Match'), 'An If-None-Match header was specified, but the ETag matched (or * was specified).'
1117
+ end
1118
+ end
1119
+ end
1120
+ end
1121
+
1122
+ if_modified_since = request.header('If-Modified-Since')
1123
+ if !if_none_match && if_modified_since
1124
+ # The If-Modified-Since header contains a date. We
1125
+ # will only return the entity if it has been changed since
1126
+ # that date. If it hasn't been changed, we return a 304
1127
+ # header
1128
+ # Note that this header only has to be checked if there was no If-None-Match header
1129
+ # as per the HTTP spec.
1130
+ date = Http::Util.parse_http_date(if_modified_since)
1131
+
1132
+ if date
1133
+ node = @tree.node_for_path(path) if node.nil?
1134
+ last_mod = node.last_modified
1135
+ if last_mod
1136
+ last_mod = Time.at(last_mod)
1137
+ if last_mod <= date
1138
+ response.status = 304
1139
+ response.update_header('Last-Modified', Http::Util.to_http_date(last_mod))
1140
+ return false
1141
+ end
1142
+ end
1143
+ end
1144
+ end
1145
+
1146
+ if_unmodified_since = request.header('If-Unmodified-Since')
1147
+ if if_unmodified_since
1148
+ # The If-Unmodified-Since will allow allow the request if the
1149
+ # entity has not changed since the specified date.
1150
+ date = Http::Util.parse_http_date(if_unmodified_since)
1151
+
1152
+ # We must only check the date if it's valid
1153
+ if date
1154
+ node = @tree.node_for_path(path) if node.nil?
1155
+ last_mod = node.last_modified
1156
+ if last_mod
1157
+ last_mod = Time.at(last_mod)
1158
+ if last_mod > date
1159
+ fail Exception::PreconditionFailed.new('If-Unmodified-Since'), 'An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.'
1160
+ end
1161
+ end
1162
+ end
1163
+ end
1164
+
1165
+ # Now the hardest, the If: header. The If: header can contain multiple
1166
+ # urls, ETags and so-called 'state tokens'.
1167
+ #
1168
+ # Examples of state tokens include lock-tokens (as defined in rfc4918)
1169
+ # and sync-tokens (as defined in rfc6578).
1170
+ #
1171
+ # The only proper way to deal with these, is to emit events, that a
1172
+ # Sync and Lock plugin can pick up.
1173
+ if_conditions = if_conditions(request)
1174
+
1175
+ if_conditions.each_with_index do |if_condition, kk|
1176
+ if_condition['tokens'].each_with_index do |_token, ii|
1177
+ if_conditions[kk]['tokens'][ii]['validToken'] = false
1178
+ end
1179
+ end
1180
+
1181
+ # Plugins are responsible for validating all the tokens.
1182
+ # If a plugin deemed a token 'valid', it will set 'validToken' to
1183
+ # true.
1184
+ box = Box.new(if_conditions)
1185
+ emit('validateTokens', [request, box])
1186
+ if_conditions = box.value
1187
+
1188
+ # Now we're going to analyze the result.
1189
+
1190
+ # Every ifCondition needs to validate to true, so we exit as soon as
1191
+ # we have an invalid condition.
1192
+ if_conditions.each do |if_condition|
1193
+ uri = if_condition['uri']
1194
+ tokens = if_condition['tokens']
1195
+
1196
+ # We only need 1 valid token for the condition to succeed.
1197
+ skip = false
1198
+ tokens.each do |token|
1199
+ token_valid = token['validToken'] || token['token'].blank?
1200
+
1201
+ etag_valid = false
1202
+ etag_valid = true if token['etag'].blank?
1203
+
1204
+ # Checking the ETag, only if the token was already deamed
1205
+ # valid and there is one.
1206
+ if !token['etag'].blank? && token_valid
1207
+ # The token was valid, and there was an ETag. We must
1208
+ # grab the current ETag and check it.
1209
+ node = @tree.node_for_path(uri)
1210
+ etag_valid = node.is_a?(IFile) && node.etag == token['etag']
1211
+ end
1212
+
1213
+ next unless (token_valid && etag_valid) ^ token['negate']
1214
+ skip = true
1215
+ break
1216
+ end
1217
+ next if skip
1218
+
1219
+ # If we ended here, it means there was no valid ETag + token
1220
+ # combination found for the current condition. This means we fail!
1221
+ fail Exception::PreconditionFailed.new('If'), "Failed to find a valid token/etag combination for #{uri}"
1222
+ end
1223
+
1224
+ true
1225
+ end
1226
+
1227
+ # This method is created to extract information from the WebDAV HTTP 'If:' header
1228
+ #
1229
+ # The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information
1230
+ # The function will return an array, containing structs with the following keys
1231
+ #
1232
+ # * uri - the uri the condition applies to.
1233
+ # * tokens - The lock token. another 2 dimensional array containing 3 elements
1234
+ #
1235
+ # Example 1:
1236
+ #
1237
+ # If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>)
1238
+ #
1239
+ # Would result in:
1240
+ #
1241
+ # [
1242
+ # [
1243
+ # 'uri' => '/request/uri',
1244
+ # 'tokens' => [
1245
+ # [
1246
+ # [
1247
+ # 'negate' => false,
1248
+ # 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
1249
+ # 'etag' => ""
1250
+ # ]
1251
+ # ]
1252
+ # ],
1253
+ # ]
1254
+ # ]
1255
+ #
1256
+ # Example 2:
1257
+ #
1258
+ # If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"])
1259
+ #
1260
+ # Would result in:
1261
+ #
1262
+ # [
1263
+ # [
1264
+ # 'uri' => 'path',
1265
+ # 'tokens' => [
1266
+ # [
1267
+ # [
1268
+ # 'negate' => true,
1269
+ # 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2',
1270
+ # 'etag' => '"Im An ETag"'
1271
+ # ],
1272
+ # [
1273
+ # 'negate' => false,
1274
+ # 'token' => '',
1275
+ # 'etag' => '"Another ETag"'
1276
+ # ]
1277
+ # ]
1278
+ # ],
1279
+ # ],
1280
+ # [
1281
+ # 'uri' => 'path2',
1282
+ # 'tokens' => [
1283
+ # [
1284
+ # [
1285
+ # 'negate' => true,
1286
+ # 'token' => '',
1287
+ # 'etag' => '"Path2 ETag"'
1288
+ # ]
1289
+ # ]
1290
+ # ],
1291
+ # ],
1292
+ # ]
1293
+ #
1294
+ # @param RequestInterface request
1295
+ # @return array
1296
+ def if_conditions(request)
1297
+ header = request.header('If')
1298
+ return [] unless header
1299
+
1300
+ matches = []
1301
+
1302
+ regex = /(?:\<(?<uri>.*?)\>\s)?\((?<not>Not\s)?(?:\<(?<token>[^\>]*)\>)?(?:\s?)(?:\[(?<etag>[^\]]*)\])?\)/im
1303
+ conditions = []
1304
+
1305
+ header.scan(regex) do |match|
1306
+ # RUBY: #scan returns an Array, but we want a named match.
1307
+ # last_match provides this
1308
+ match = Regexp.last_match
1309
+
1310
+ # If there was no uri specified in this match, and there were
1311
+ # already conditions parsed, we add the condition to the list of
1312
+ # conditions for the previous uri.
1313
+ if !match['uri'] && conditions.any?
1314
+ conditions[conditions.size - 1]['tokens'] << {
1315
+ 'negate' => match['not'] ? true : false,
1316
+ 'token' => match['token'] || '',
1317
+ 'etag' => match['etag'] ? match['etag'] : ''
1318
+ }
1319
+ else
1320
+ if !match['uri']
1321
+ real_uri = request.path
1322
+ else
1323
+ real_uri = calculate_uri(match['uri'])
1324
+ end
1325
+
1326
+ conditions << {
1327
+ 'uri' => real_uri,
1328
+ 'tokens' => [
1329
+ {
1330
+ 'negate' => match['not'] ? true : false,
1331
+ 'token' => match['token'] || '',
1332
+ 'etag' => match['etag'] ? match['etag'] : ''
1333
+ }
1334
+ ]
1335
+ }
1336
+ end
1337
+ end
1338
+
1339
+ conditions
1340
+ end
1341
+
1342
+ # Returns an array with resourcetypes for a node.
1343
+ #
1344
+ # @param INode node
1345
+ # @return array
1346
+ def resource_type_for_node(node)
1347
+ result = []
1348
+ @resource_type_mapping.each do |class_name, resource_type|
1349
+ result << resource_type if node.is_a?(class_name)
1350
+ end
1351
+
1352
+ result
1353
+ end
1354
+
1355
+ # }}}
1356
+ # {{{ XML Readers & Writers
1357
+
1358
+ # Generates a WebDAV propfind response body based on a list of nodes.
1359
+ #
1360
+ # If 'strip404s' is set to true, all 404 responses will be removed.
1361
+ #
1362
+ # @param array file_properties The list with nodes
1363
+ # @param bool strip404s
1364
+ # @return string
1365
+ def generate_multi_status(file_properties, strip404s = false)
1366
+ xml = []
1367
+
1368
+ file_properties.each do |entry|
1369
+ href = entry['href']
1370
+ entry.delete('href')
1371
+
1372
+ entry.delete(404) if strip404s
1373
+
1374
+ response = Xml::Element::Response.new(
1375
+ href.gsub(%r{^/+}, ''),
1376
+ entry
1377
+ )
1378
+ xml << {
1379
+ 'name' => '{DAV:}response',
1380
+ 'value' => response
1381
+ }
1382
+ end
1383
+
1384
+ @xml.write('{DAV:}multistatus', xml, @base_uri)
1385
+ end
1386
+ end
1387
+ end
1388
+ end