tilia-dav 3.1.0.pre.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.database.travis.yml +6 -0
- data/.gitignore +25 -0
- data/.rubocop.yml +35 -0
- data/.simplecov +4 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.sabre.md +2084 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +103 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +40 -0
- data/Rakefile +18 -0
- data/database.sample.yml +6 -0
- data/examples/minimal.rb +25 -0
- data/lib/tilia/cal_dav.rb +27 -0
- data/lib/tilia/cal_dav/backend.rb +17 -0
- data/lib/tilia/cal_dav/backend/abstract_backend.rb +194 -0
- data/lib/tilia/cal_dav/backend/backend_interface.rb +250 -0
- data/lib/tilia/cal_dav/backend/notification_support.rb +38 -0
- data/lib/tilia/cal_dav/backend/scheduling_support.rb +57 -0
- data/lib/tilia/cal_dav/backend/sequel.rb +1118 -0
- data/lib/tilia/cal_dav/backend/sharing_support.rb +239 -0
- data/lib/tilia/cal_dav/backend/subscription_support.rb +79 -0
- data/lib/tilia/cal_dav/backend/sync_support.rb +75 -0
- data/lib/tilia/cal_dav/calendar.rb +426 -0
- data/lib/tilia/cal_dav/calendar_home.rb +335 -0
- data/lib/tilia/cal_dav/calendar_object.rb +219 -0
- data/lib/tilia/cal_dav/calendar_query_validator.rb +294 -0
- data/lib/tilia/cal_dav/calendar_root.rb +57 -0
- data/lib/tilia/cal_dav/exception.rb +7 -0
- data/lib/tilia/cal_dav/exception/invalid_component_type.rb +21 -0
- data/lib/tilia/cal_dav/i_calendar.rb +11 -0
- data/lib/tilia/cal_dav/i_calendar_object.rb +13 -0
- data/lib/tilia/cal_dav/i_calendar_object_container.rb +32 -0
- data/lib/tilia/cal_dav/i_shareable_calendar.rb +40 -0
- data/lib/tilia/cal_dav/i_shared_calendar.rb +28 -0
- data/lib/tilia/cal_dav/ics_export_plugin.rb +327 -0
- data/lib/tilia/cal_dav/notifications.rb +12 -0
- data/lib/tilia/cal_dav/notifications/collection.rb +131 -0
- data/lib/tilia/cal_dav/notifications/i_collection.rb +17 -0
- data/lib/tilia/cal_dav/notifications/i_node.rb +30 -0
- data/lib/tilia/cal_dav/notifications/node.rb +142 -0
- data/lib/tilia/cal_dav/notifications/plugin.rb +138 -0
- data/lib/tilia/cal_dav/plugin.rb +891 -0
- data/lib/tilia/cal_dav/principal.rb +12 -0
- data/lib/tilia/cal_dav/principal/collection.rb +21 -0
- data/lib/tilia/cal_dav/principal/i_proxy_read.rb +13 -0
- data/lib/tilia/cal_dav/principal/i_proxy_write.rb +13 -0
- data/lib/tilia/cal_dav/principal/proxy_read.rb +127 -0
- data/lib/tilia/cal_dav/principal/proxy_write.rb +127 -0
- data/lib/tilia/cal_dav/principal/user.rb +96 -0
- data/lib/tilia/cal_dav/schedule.rb +14 -0
- data/lib/tilia/cal_dav/schedule/i_inbox.rb +12 -0
- data/lib/tilia/cal_dav/schedule/i_mip_plugin.rb +156 -0
- data/lib/tilia/cal_dav/schedule/i_outbox.rb +12 -0
- data/lib/tilia/cal_dav/schedule/i_scheduling_object.rb +10 -0
- data/lib/tilia/cal_dav/schedule/inbox.rb +211 -0
- data/lib/tilia/cal_dav/schedule/outbox.rb +143 -0
- data/lib/tilia/cal_dav/schedule/plugin.rb +851 -0
- data/lib/tilia/cal_dav/schedule/scheduling_object.rb +126 -0
- data/lib/tilia/cal_dav/shareable_calendar.rb +54 -0
- data/lib/tilia/cal_dav/shared_calendar.rb +120 -0
- data/lib/tilia/cal_dav/sharing_plugin.rb +359 -0
- data/lib/tilia/cal_dav/subscriptions.rb +9 -0
- data/lib/tilia/cal_dav/subscriptions/i_subscription.rb +37 -0
- data/lib/tilia/cal_dav/subscriptions/plugin.rb +83 -0
- data/lib/tilia/cal_dav/subscriptions/subscription.rb +205 -0
- data/lib/tilia/cal_dav/xml.rb +10 -0
- data/lib/tilia/cal_dav/xml/filter.rb +12 -0
- data/lib/tilia/cal_dav/xml/filter/calendar_data.rb +64 -0
- data/lib/tilia/cal_dav/xml/filter/comp_filter.rb +79 -0
- data/lib/tilia/cal_dav/xml/filter/param_filter.rb +66 -0
- data/lib/tilia/cal_dav/xml/filter/prop_filter.rb +80 -0
- data/lib/tilia/cal_dav/xml/notification.rb +13 -0
- data/lib/tilia/cal_dav/xml/notification/invite.rb +253 -0
- data/lib/tilia/cal_dav/xml/notification/invite_reply.rb +167 -0
- data/lib/tilia/cal_dav/xml/notification/notification_interface.rb +41 -0
- data/lib/tilia/cal_dav/xml/notification/system_status.rb +139 -0
- data/lib/tilia/cal_dav/xml/property.rb +15 -0
- data/lib/tilia/cal_dav/xml/property/allowed_sharing_modes.rb +64 -0
- data/lib/tilia/cal_dav/xml/property/email_address_set.rb +60 -0
- data/lib/tilia/cal_dav/xml/property/invite.rb +207 -0
- data/lib/tilia/cal_dav/xml/property/schedule_calendar_transp.rb +108 -0
- data/lib/tilia/cal_dav/xml/property/supported_calendar_component_set.rb +100 -0
- data/lib/tilia/cal_dav/xml/property/supported_calendar_data.rb +50 -0
- data/lib/tilia/cal_dav/xml/property/supported_collation_set.rb +47 -0
- data/lib/tilia/cal_dav/xml/request.rb +14 -0
- data/lib/tilia/cal_dav/xml/request/calendar_multi_get_report.rb +99 -0
- data/lib/tilia/cal_dav/xml/request/calendar_query_report.rb +112 -0
- data/lib/tilia/cal_dav/xml/request/free_busy_query_report.rb +70 -0
- data/lib/tilia/cal_dav/xml/request/invite_reply.rb +110 -0
- data/lib/tilia/cal_dav/xml/request/mk_calendar.rb +67 -0
- data/lib/tilia/cal_dav/xml/request/share.rb +93 -0
- data/lib/tilia/card_dav.rb +17 -0
- data/lib/tilia/card_dav/address_book.rb +338 -0
- data/lib/tilia/card_dav/address_book_home.rb +192 -0
- data/lib/tilia/card_dav/address_book_root.rb +58 -0
- data/lib/tilia/card_dav/backend.rb +12 -0
- data/lib/tilia/card_dav/backend/abstract_backend.rb +30 -0
- data/lib/tilia/card_dav/backend/backend_interface.rb +175 -0
- data/lib/tilia/card_dav/backend/sequel.rb +476 -0
- data/lib/tilia/card_dav/backend/sync_support.rb +80 -0
- data/lib/tilia/card_dav/card.rb +193 -0
- data/lib/tilia/card_dav/i_address_book.rb +10 -0
- data/lib/tilia/card_dav/i_card.rb +11 -0
- data/lib/tilia/card_dav/i_directory.rb +14 -0
- data/lib/tilia/card_dav/plugin.rb +724 -0
- data/lib/tilia/card_dav/vcf_export_plugin.rb +122 -0
- data/lib/tilia/card_dav/xml.rb +9 -0
- data/lib/tilia/card_dav/xml/filter.rb +11 -0
- data/lib/tilia/card_dav/xml/filter/address_data.rb +50 -0
- data/lib/tilia/card_dav/xml/filter/param_filter.rb +71 -0
- data/lib/tilia/card_dav/xml/filter/prop_filter.rb +77 -0
- data/lib/tilia/card_dav/xml/property.rb +10 -0
- data/lib/tilia/card_dav/xml/property/supported_address_data.rb +67 -0
- data/lib/tilia/card_dav/xml/property/supported_collation_set.rb +38 -0
- data/lib/tilia/card_dav/xml/request.rb +10 -0
- data/lib/tilia/card_dav/xml/request/address_book_multi_get_report.rb +91 -0
- data/lib/tilia/card_dav/xml/request/address_book_query_report.rb +156 -0
- data/lib/tilia/dav.rb +94 -0
- data/lib/tilia/dav/auth.rb +8 -0
- data/lib/tilia/dav/auth/backend.rb +15 -0
- data/lib/tilia/dav/auth/backend/abstract_basic.rb +119 -0
- data/lib/tilia/dav/auth/backend/abstract_digest.rb +132 -0
- data/lib/tilia/dav/auth/backend/apache.rb +85 -0
- data/lib/tilia/dav/auth/backend/backend_interface.rb +61 -0
- data/lib/tilia/dav/auth/backend/basic_call_back.rb +46 -0
- data/lib/tilia/dav/auth/backend/file.rb +61 -0
- data/lib/tilia/dav/auth/backend/sequel.rb +46 -0
- data/lib/tilia/dav/auth/plugin.rb +157 -0
- data/lib/tilia/dav/browser.rb +12 -0
- data/lib/tilia/dav/browser/assets/favicon.ico +0 -0
- data/lib/tilia/dav/browser/assets/openiconic/ICON-LICENSE +21 -0
- data/lib/tilia/dav/browser/assets/openiconic/open-iconic.css +510 -0
- data/lib/tilia/dav/browser/assets/openiconic/open-iconic.eot +0 -0
- data/lib/tilia/dav/browser/assets/openiconic/open-iconic.otf +0 -0
- data/lib/tilia/dav/browser/assets/openiconic/open-iconic.svg +543 -0
- data/lib/tilia/dav/browser/assets/openiconic/open-iconic.ttf +0 -0
- data/lib/tilia/dav/browser/assets/openiconic/open-iconic.woff +0 -0
- data/lib/tilia/dav/browser/assets/sabredav.css +228 -0
- data/lib/tilia/dav/browser/assets/sabredav.png +0 -0
- data/lib/tilia/dav/browser/guess_content_type.rb +80 -0
- data/lib/tilia/dav/browser/html_output.rb +27 -0
- data/lib/tilia/dav/browser/html_output_helper.rb +86 -0
- data/lib/tilia/dav/browser/map_get_to_prop_find.rb +41 -0
- data/lib/tilia/dav/browser/plugin.rb +693 -0
- data/lib/tilia/dav/browser/prop_find_all.rb +95 -0
- data/lib/tilia/dav/client.rb +341 -0
- data/lib/tilia/dav/collection.rb +79 -0
- data/lib/tilia/dav/core_plugin.rb +824 -0
- data/lib/tilia/dav/exception.rb +59 -0
- data/lib/tilia/dav/exception/bad_request.rb +18 -0
- data/lib/tilia/dav/exception/conflict.rb +18 -0
- data/lib/tilia/dav/exception/conflicting_lock.rb +26 -0
- data/lib/tilia/dav/exception/forbidden.rb +18 -0
- data/lib/tilia/dav/exception/insufficient_storage.rb +18 -0
- data/lib/tilia/dav/exception/invalid_resource_type.rb +23 -0
- data/lib/tilia/dav/exception/invalid_sync_token.rb +26 -0
- data/lib/tilia/dav/exception/length_required.rb +18 -0
- data/lib/tilia/dav/exception/lock_token_matches_request_uri.rb +25 -0
- data/lib/tilia/dav/exception/locked.rb +48 -0
- data/lib/tilia/dav/exception/method_not_allowed.rb +29 -0
- data/lib/tilia/dav/exception/not_authenticated.rb +18 -0
- data/lib/tilia/dav/exception/not_found.rb +18 -0
- data/lib/tilia/dav/exception/not_implemented.rb +18 -0
- data/lib/tilia/dav/exception/payment_required.rb +18 -0
- data/lib/tilia/dav/exception/precondition_failed.rb +47 -0
- data/lib/tilia/dav/exception/report_not_supported.rb +21 -0
- data/lib/tilia/dav/exception/requested_range_not_satisfiable.rb +18 -0
- data/lib/tilia/dav/exception/service_unavailable.rb +18 -0
- data/lib/tilia/dav/exception/too_many_matches.rb +21 -0
- data/lib/tilia/dav/exception/unsupported_media_type.rb +18 -0
- data/lib/tilia/dav/file.rb +58 -0
- data/lib/tilia/dav/fs.rb +9 -0
- data/lib/tilia/dav/fs/directory.rb +119 -0
- data/lib/tilia/dav/fs/file.rb +69 -0
- data/lib/tilia/dav/fs/node.rb +57 -0
- data/lib/tilia/dav/fs_ext.rb +8 -0
- data/lib/tilia/dav/fs_ext/directory.rb +175 -0
- data/lib/tilia/dav/fs_ext/file.rb +118 -0
- data/lib/tilia/dav/i_collection.rb +65 -0
- data/lib/tilia/dav/i_extended_collection.rb +36 -0
- data/lib/tilia/dav/i_file.rb +70 -0
- data/lib/tilia/dav/i_move_target.rb +37 -0
- data/lib/tilia/dav/i_multi_get.rb +29 -0
- data/lib/tilia/dav/i_node.rb +33 -0
- data/lib/tilia/dav/i_properties.rb +39 -0
- data/lib/tilia/dav/i_quota.rb +19 -0
- data/lib/tilia/dav/locks.rb +9 -0
- data/lib/tilia/dav/locks/backend.rb +12 -0
- data/lib/tilia/dav/locks/backend/abstract_backend.rb +16 -0
- data/lib/tilia/dav/locks/backend/backend_interface.rb +41 -0
- data/lib/tilia/dav/locks/backend/file.rb +146 -0
- data/lib/tilia/dav/locks/backend/sequel.rb +154 -0
- data/lib/tilia/dav/locks/lock_info.rb +60 -0
- data/lib/tilia/dav/locks/plugin.rb +467 -0
- data/lib/tilia/dav/mk_col.rb +47 -0
- data/lib/tilia/dav/mount.rb +7 -0
- data/lib/tilia/dav/mount/plugin.rb +62 -0
- data/lib/tilia/dav/node.rb +36 -0
- data/lib/tilia/dav/partial_update.rb +8 -0
- data/lib/tilia/dav/partial_update/i_patch_support.rb +40 -0
- data/lib/tilia/dav/partial_update/plugin.rb +179 -0
- data/lib/tilia/dav/prop_find.rb +262 -0
- data/lib/tilia/dav/prop_patch.rb +278 -0
- data/lib/tilia/dav/property_storage.rb +8 -0
- data/lib/tilia/dav/property_storage/backend.rb +10 -0
- data/lib/tilia/dav/property_storage/backend/backend_interface.rb +69 -0
- data/lib/tilia/dav/property_storage/backend/sequel.rb +192 -0
- data/lib/tilia/dav/property_storage/plugin.rb +131 -0
- data/lib/tilia/dav/server.rb +1388 -0
- data/lib/tilia/dav/server_plugin.rb +81 -0
- data/lib/tilia/dav/simple_collection.rb +71 -0
- data/lib/tilia/dav/simple_file.rb +82 -0
- data/lib/tilia/dav/string_util.rb +68 -0
- data/lib/tilia/dav/sync.rb +8 -0
- data/lib/tilia/dav/sync/i_sync_collection.rb +80 -0
- data/lib/tilia/dav/sync/plugin.rb +225 -0
- data/lib/tilia/dav/temporary_file_filter_plugin.rb +248 -0
- data/lib/tilia/dav/tree.rb +270 -0
- data/lib/tilia/dav/uuid_util.rb +45 -0
- data/lib/tilia/dav/version.rb +9 -0
- data/lib/tilia/dav/xml.rb +11 -0
- data/lib/tilia/dav/xml/element.rb +10 -0
- data/lib/tilia/dav/xml/element/prop.rb +92 -0
- data/lib/tilia/dav/xml/element/response.rb +188 -0
- data/lib/tilia/dav/xml/property.rb +16 -0
- data/lib/tilia/dav/xml/property/complex.rb +76 -0
- data/lib/tilia/dav/xml/property/get_last_modified.rb +79 -0
- data/lib/tilia/dav/xml/property/href.rb +137 -0
- data/lib/tilia/dav/xml/property/lock_discovery.rb +89 -0
- data/lib/tilia/dav/xml/property/resource_type.rb +96 -0
- data/lib/tilia/dav/xml/property/supported_lock.rb +48 -0
- data/lib/tilia/dav/xml/property/supported_method_set.rb +101 -0
- data/lib/tilia/dav/xml/property/supported_report_set.rb +118 -0
- data/lib/tilia/dav/xml/request.rb +13 -0
- data/lib/tilia/dav/xml/request/lock.rb +67 -0
- data/lib/tilia/dav/xml/request/mk_col.rb +69 -0
- data/lib/tilia/dav/xml/request/prop_find.rb +70 -0
- data/lib/tilia/dav/xml/request/prop_patch.rb +101 -0
- data/lib/tilia/dav/xml/request/sync_collection_report.rb +102 -0
- data/lib/tilia/dav/xml/response.rb +9 -0
- data/lib/tilia/dav/xml/response/multi_status.rb +108 -0
- data/lib/tilia/dav/xml/service.rb +42 -0
- data/lib/tilia/dav_acl.rb +16 -0
- data/lib/tilia/dav_acl/abstract_principal_collection.rb +143 -0
- data/lib/tilia/dav_acl/exception.rb +11 -0
- data/lib/tilia/dav_acl/exception/ace_conflict.rb +21 -0
- data/lib/tilia/dav_acl/exception/need_privileges.rb +65 -0
- data/lib/tilia/dav_acl/exception/no_abstract.rb +21 -0
- data/lib/tilia/dav_acl/exception/not_recognized_principal.rb +21 -0
- data/lib/tilia/dav_acl/exception/not_supported_privilege.rb +21 -0
- data/lib/tilia/dav_acl/fs.rb +9 -0
- data/lib/tilia/dav_acl/fs/collection.rb +108 -0
- data/lib/tilia/dav_acl/fs/file.rb +87 -0
- data/lib/tilia/dav_acl/fs/home_collection.rb +148 -0
- data/lib/tilia/dav_acl/i_acl.rb +61 -0
- data/lib/tilia/dav_acl/i_principal.rb +63 -0
- data/lib/tilia/dav_acl/i_principal_collection.rb +52 -0
- data/lib/tilia/dav_acl/plugin.rb +1109 -0
- data/lib/tilia/dav_acl/principal.rb +213 -0
- data/lib/tilia/dav_acl/principal_backend.rb +11 -0
- data/lib/tilia/dav_acl/principal_backend/abstract_backend.rb +42 -0
- data/lib/tilia/dav_acl/principal_backend/backend_interface.rb +127 -0
- data/lib/tilia/dav_acl/principal_backend/create_principal_support.rb +27 -0
- data/lib/tilia/dav_acl/principal_backend/sequel.rb +313 -0
- data/lib/tilia/dav_acl/principal_collection.rb +117 -0
- data/lib/tilia/dav_acl/xml.rb +8 -0
- data/lib/tilia/dav_acl/xml/property.rb +13 -0
- data/lib/tilia/dav_acl/xml/property/acl.rb +222 -0
- data/lib/tilia/dav_acl/xml/property/acl_restrictions.rb +40 -0
- data/lib/tilia/dav_acl/xml/property/current_user_privilege_set.rb +125 -0
- data/lib/tilia/dav_acl/xml/property/principal.rb +149 -0
- data/lib/tilia/dav_acl/xml/property/supported_privilege_set.rb +135 -0
- data/lib/tilia/dav_acl/xml/request.rb +11 -0
- data/lib/tilia/dav_acl/xml/request/expand_property_report.rb +86 -0
- data/lib/tilia/dav_acl/xml/request/principal_property_search_report.rb +111 -0
- data/lib/tilia/dav_acl/xml/request/principal_search_property_set_report.rb +49 -0
- data/test/cal_dav/backend/abstract_sequel_test.rb +817 -0
- data/test/cal_dav/backend/abstract_test.rb +163 -0
- data/test/cal_dav/backend/mock.rb +169 -0
- data/test/cal_dav/backend/mock_scheduling.rb +84 -0
- data/test/cal_dav/backend/mock_sharing.rb +124 -0
- data/test/cal_dav/backend/mock_subscription_support.rb +123 -0
- data/test/cal_dav/backend/sequel_my_sql_test.rb +102 -0
- data/test/cal_dav/backend/sequel_sqlite_test.rb +105 -0
- data/test/cal_dav/calendar_home_notifications_test.rb +41 -0
- data/test/cal_dav/calendar_home_shared_calendars_test.rb +64 -0
- data/test/cal_dav/calendar_home_subscriptions_test.rb +67 -0
- data/test/cal_dav/calendar_home_test.rb +144 -0
- data/test/cal_dav/calendar_object_test.rb +317 -0
- data/test/cal_dav/calendar_query_v_alarm_test.rb +114 -0
- data/test/cal_dav/calendar_query_validator_test.rb +820 -0
- data/test/cal_dav/calendar_test.rb +203 -0
- data/test/cal_dav/expand_events_double_events_test.rb +94 -0
- data/test/cal_dav/expand_events_dtstar_tand_dten_dby_day_test.rb +94 -0
- data/test/cal_dav/expand_events_dtstar_tand_dtend_test.rb +100 -0
- data/test/cal_dav/expand_events_floating_time_test.rb +211 -0
- data/test/cal_dav/free_busy_report_test.rb +156 -0
- data/test/cal_dav/get_events_by_timerange_test.rb +74 -0
- data/test/cal_dav/ics_export_plugin_test.rb +638 -0
- data/test/cal_dav/issue166_test.rb +59 -0
- data/test/cal_dav/issue172_test.rb +139 -0
- data/test/cal_dav/issue203_test.rb +130 -0
- data/test/cal_dav/issue205_test.rb +89 -0
- data/test/cal_dav/issue211_test.rb +84 -0
- data/test/cal_dav/issue220_test.rb +94 -0
- data/test/cal_dav/issue228_test.rb +74 -0
- data/test/cal_dav/j_cal_transform_test.rb +244 -0
- data/test/cal_dav/notifications/collection_test.rb +67 -0
- data/test/cal_dav/notifications/node_test.rb +73 -0
- data/test/cal_dav/notifications/plugin_test.rb +144 -0
- data/test/cal_dav/plugin_test.rb +1049 -0
- data/test/cal_dav/principal/collection_test.rb +19 -0
- data/test/cal_dav/principal/proxy_read_test.rb +67 -0
- data/test/cal_dav/principal/proxy_write_test.rb +29 -0
- data/test/cal_dav/principal/user_test.rb +91 -0
- data/test/cal_dav/schedule/deliver_new_event_test.rb +81 -0
- data/test/cal_dav/schedule/free_busy_request_test.rb +565 -0
- data/test/cal_dav/schedule/i_mip/mock_plugin.rb +40 -0
- data/test/cal_dav/schedule/i_mip_plugin_test.rb +196 -0
- data/test/cal_dav/schedule/inbox_test.rb +150 -0
- data/test/cal_dav/schedule/outbox_post_test.rb +124 -0
- data/test/cal_dav/schedule/outbox_test.rb +76 -0
- data/test/cal_dav/schedule/plugin_basic_test.rb +39 -0
- data/test/cal_dav/schedule/plugin_properties_test.rb +96 -0
- data/test/cal_dav/schedule/plugin_properties_with_shared_calendar_test.rb +69 -0
- data/test/cal_dav/schedule/schedule_deliver_test.rb +605 -0
- data/test/cal_dav/schedule/scheduling_object_test.rb +327 -0
- data/test/cal_dav/shareable_calendar_test.rb +58 -0
- data/test/cal_dav/shared_calendar_test.rb +189 -0
- data/test/cal_dav/sharing_plugin_test.rb +373 -0
- data/test/cal_dav/subscriptions/create_subscription_test.rb +115 -0
- data/test/cal_dav/subscriptions/plugin_test.rb +46 -0
- data/test/cal_dav/subscriptions/subscription_test.rb +119 -0
- data/test/cal_dav/test_util.rb +164 -0
- data/test/cal_dav/validate_i_cal_test.rb +219 -0
- data/test/cal_dav/xml/notification/invite_reply_test.rb +136 -0
- data/test/cal_dav/xml/notification/invite_test.rb +225 -0
- data/test/cal_dav/xml/notification/system_status_test.rb +63 -0
- data/test/cal_dav/xml/property/allowed_sharing_modes_test.rb +34 -0
- data/test/cal_dav/xml/property/email_address_set_test.rb +35 -0
- data/test/cal_dav/xml/property/invite_test.rb +173 -0
- data/test/cal_dav/xml/property/schedule_calendar_transp_test.rb +96 -0
- data/test/cal_dav/xml/property/supported_calendar_component_set_test.rb +76 -0
- data/test/cal_dav/xml/property/supported_calendar_data_test.rb +32 -0
- data/test/cal_dav/xml/property/supported_collation_set_test.rb +33 -0
- data/test/cal_dav/xml/request/calendar_query_report_test.rb +339 -0
- data/test/cal_dav/xml/request/invite_reply_test.rb +68 -0
- data/test/cal_dav/xml/request/share_test.rb +79 -0
- data/test/card_dav/abstract_plugin_test.rb +24 -0
- data/test/card_dav/address_book_home_test.rb +128 -0
- data/test/card_dav/address_book_query_test.rb +303 -0
- data/test/card_dav/address_book_root_test.rb +26 -0
- data/test/card_dav/address_book_test.rb +166 -0
- data/test/card_dav/backend/abstract_sequel_test.rb +302 -0
- data/test/card_dav/backend/mock.rb +122 -0
- data/test/card_dav/backend/sequel_my_sql_test.rb +56 -0
- data/test/card_dav/backend/sequel_sqlite_test.rb +59 -0
- data/test/card_dav/card_test.rb +164 -0
- data/test/card_dav/i_directory_test.rb +22 -0
- data/test/card_dav/multi_get_test.rb +97 -0
- data/test/card_dav/plugin_test.rb +87 -0
- data/test/card_dav/sogo_strip_content_type_test.rb +63 -0
- data/test/card_dav/test_util.rb +51 -0
- data/test/card_dav/validate_filter_test.rb +210 -0
- data/test/card_dav/validate_v_card_test.rb +143 -0
- data/test/card_dav/vcf_export_test.rb +66 -0
- data/test/card_dav/xml/property/supported_address_data_test.rb +34 -0
- data/test/card_dav/xml/property/supported_collation_set_test.rb +34 -0
- data/test/card_dav/xml/request/address_book_query_report_test.rb +276 -0
- data/test/dav/abstract_server.rb +36 -0
- data/test/dav/auth/backend/abstract_basic_test.rb +74 -0
- data/test/dav/auth/backend/abstract_digest_test.rb +114 -0
- data/test/dav/auth/backend/abstract_sequel_test.rb +25 -0
- data/test/dav/auth/backend/apache_test.rb +60 -0
- data/test/dav/auth/backend/basic_call_back_test.rb +33 -0
- data/test/dav/auth/backend/file_test.rb +43 -0
- data/test/dav/auth/backend/mock.rb +73 -0
- data/test/dav/auth/backend/sequel_my_sql_test.rb +32 -0
- data/test/dav/auth/backend/sequel_sqlite_test.rb +21 -0
- data/test/dav/auth/plugin_test.rb +92 -0
- data/test/dav/basic_node_test.rb +143 -0
- data/test/dav/browser/guess_content_type_test.rb +44 -0
- data/test/dav/browser/map_get_to_prop_find_test.rb +37 -0
- data/test/dav/browser/plugin_test.rb +165 -0
- data/test/dav/browser/prop_find_all_test.rb +59 -0
- data/test/dav/client_mock.rb +24 -0
- data/test/dav/client_test.rb +231 -0
- data/test/dav/copy_test.rb +33 -0
- data/test/dav/exception/locked_test.rb +61 -0
- data/test/dav/exception/payment_required_test.rb +14 -0
- data/test/dav/exception/service_unavailable_test.rb +14 -0
- data/test/dav/exception/too_many_matches_test.rb +31 -0
- data/test/dav/exception_test.rb +24 -0
- data/test/dav/fs_ext/file_test.rb +72 -0
- data/test/dav/fs_ext/server_test.rb +251 -0
- data/test/dav/get_if_conditions_test.rb +299 -0
- data/test/dav/http_delete_test.rb +110 -0
- data/test/dav/http_get_test.rb +130 -0
- data/test/dav/http_head_test.rb +80 -0
- data/test/dav/http_move_test.rb +105 -0
- data/test/dav/http_prefer_parsing_test.rb +186 -0
- data/test/dav/http_put_test.rb +271 -0
- data/test/dav/issue33_test.rb +90 -0
- data/test/dav/locks/backend/abstract_test.rb +160 -0
- data/test/dav/locks/backend/file_test.rb +24 -0
- data/test/dav/locks/backend/mock.rb +82 -0
- data/test/dav/locks/backend/sequel_my_sql_test.rb +32 -0
- data/test/dav/locks/backend/sequel_test.rb +19 -0
- data/test/dav/locks/ms_word_test.rb +119 -0
- data/test/dav/locks/plugin2_test.rb +61 -0
- data/test/dav/locks/plugin_test.rb +896 -0
- data/test/dav/mock/collection.rb +113 -0
- data/test/dav/mock/file.rb +100 -0
- data/test/dav/mock/properties_collection.rb +80 -0
- data/test/dav/mock/streaming_file.rb +66 -0
- data/test/dav/mount/plugin_test.rb +48 -0
- data/test/dav/object_tree_test.rb +65 -0
- data/test/dav/partial_update/file_mock.rb +92 -0
- data/test/dav/partial_update/plugin_test.rb +125 -0
- data/test/dav/partial_update/specification_test.rb +77 -0
- data/test/dav/prop_find_test.rb +87 -0
- data/test/dav/prop_patch_test.rb +367 -0
- data/test/dav/property_storage/backend/abstract_sequel_test.rb +147 -0
- data/test/dav/property_storage/backend/mock.rb +96 -0
- data/test/dav/property_storage/backend/sequel_mysql_test.rb +32 -0
- data/test/dav/property_storage/backend/sequel_sqlite_test.rb +31 -0
- data/test/dav/property_storage/plugin_test.rb +90 -0
- data/test/dav/server_copy_move_test.rb +164 -0
- data/test/dav/server_events_test.rb +105 -0
- data/test/dav/server_mkcol_test.rb +337 -0
- data/test/dav/server_mock.rb +10 -0
- data/test/dav/server_plugin_test.rb +85 -0
- data/test/dav/server_precondition_test.rb +253 -0
- data/test/dav/server_props_infinite_depth_test.rb +144 -0
- data/test/dav/server_props_test.rb +182 -0
- data/test/dav/server_range_test.rb +262 -0
- data/test/dav/server_simple_test.rb +388 -0
- data/test/dav/server_update_properties_test.rb +93 -0
- data/test/dav/simple_file_test.rb +17 -0
- data/test/dav/string_util_test.rb +92 -0
- data/test/dav/sync/mock_sync_collection.rb +141 -0
- data/test/dav/sync/plugin_test.rb +491 -0
- data/test/dav/sync_token_property_test.rb +105 -0
- data/test/dav/temporary_file_filter_test.rb +179 -0
- data/test/dav/test_plugin.rb +24 -0
- data/test/dav/tree_test.rb +201 -0
- data/test/dav/uuid_util_test.rb +14 -0
- data/test/dav/xml/element/prop_test.rb +121 -0
- data/test/dav/xml/element/response_test.rb +202 -0
- data/test/dav/xml/property/href_test.rb +112 -0
- data/test/dav/xml/property/last_modified_test.rb +52 -0
- data/test/dav/xml/property/lock_discovery_test.rb +79 -0
- data/test/dav/xml/property/supported_method_set_test.rb +54 -0
- data/test/dav/xml/property/supported_report_set_test.rb +109 -0
- data/test/dav/xml/request/prop_find_test.rb +45 -0
- data/test/dav/xml/request/prop_patch_test.rb +47 -0
- data/test/dav/xml/request/sync_collection_test.rb +89 -0
- data/test/dav/xml/xml_tester.rb +35 -0
- data/test/dav_acl/acl_method_test.rb +299 -0
- data/test/dav_acl/allow_access_test.rb +94 -0
- data/test/dav_acl/block_access_test.rb +161 -0
- data/test/dav_acl/exception/ace_conflict_test.rb +33 -0
- data/test/dav_acl/exception/need_privileges_exception_test.rb +43 -0
- data/test/dav_acl/exception/no_abstract_test.rb +33 -0
- data/test/dav_acl/exception/not_recognized_principal_test.rb +33 -0
- data/test/dav_acl/exception/not_supported_privilege_test.rb +33 -0
- data/test/dav_acl/expand_properties_test.rb +265 -0
- data/test/dav_acl/fs/collection_test.rb +39 -0
- data/test/dav_acl/fs/file_test.rb +47 -0
- data/test/dav_acl/fs/home_collection_test.rb +82 -0
- data/test/dav_acl/mock_acl_node.rb +27 -0
- data/test/dav_acl/mock_principal.rb +27 -0
- data/test/dav_acl/plugin_admin_test.rb +60 -0
- data/test/dav_acl/plugin_properties_test.rb +346 -0
- data/test/dav_acl/plugin_update_properties_test.rb +82 -0
- data/test/dav_acl/principal_backend/abstract_sequel_test.rb +159 -0
- data/test/dav_acl/principal_backend/mock.rb +150 -0
- data/test/dav_acl/principal_backend/sequel_my_sql_test.rb +43 -0
- data/test/dav_acl/principal_backend/sequel_sqlite_test.rb +31 -0
- data/test/dav_acl/principal_collection_test.rb +44 -0
- data/test/dav_acl/principal_property_search_test.rb +354 -0
- data/test/dav_acl/principal_search_property_set_test.rb +125 -0
- data/test/dav_acl/principal_test.rb +181 -0
- data/test/dav_acl/simple_plugin_test.rb +320 -0
- data/test/dav_acl/xml/property/acl_restrictions_test.rb +28 -0
- data/test/dav_acl/xml/property/acl_test.rb +325 -0
- data/test/dav_acl/xml/property/current_user_privilege_set_test.rb +77 -0
- data/test/dav_acl/xml/property/principal_test.rb +158 -0
- data/test/dav_acl/xml/property/supported_privilege_set_test.rb +109 -0
- data/test/dav_server_test.rb +225 -0
- data/test/http/response_mock.rb +16 -0
- data/test/http/sapi_mock.rb +29 -0
- data/test/test_helper.rb +176 -0
- data/tilia-dav.gemspec +28 -0
- 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
|