Radicale 3.8.1__tar.gz → 3.8.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. {radicale-3.8.1 → radicale-3.8.2}/CHANGELOG.md +23 -0
  2. {radicale-3.8.1 → radicale-3.8.2}/DOCUMENTATION.md +34 -3
  3. {radicale-3.8.1 → radicale-3.8.2}/PKG-INFO +1 -1
  4. {radicale-3.8.1 → radicale-3.8.2}/Radicale.egg-info/PKG-INFO +1 -1
  5. {radicale-3.8.1 → radicale-3.8.2}/Radicale.egg-info/SOURCES.txt +0 -1
  6. {radicale-3.8.1 → radicale-3.8.2}/config +8 -2
  7. {radicale-3.8.1 → radicale-3.8.2}/pyproject.toml +24 -2
  8. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/__init__.py +16 -8
  9. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/base.py +23 -6
  10. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/delete.py +11 -2
  11. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/get.py +139 -7
  12. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/mkcalendar.py +1 -1
  13. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/mkcol.py +1 -1
  14. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/move.py +11 -4
  15. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/propfind.py +120 -26
  16. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/proppatch.py +7 -4
  17. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/put.py +9 -6
  18. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/report.py +205 -65
  19. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/htpasswd.py +9 -3
  20. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/imap.py +4 -2
  21. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/ldap.py +15 -15
  22. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/oauth2.py +5 -3
  23. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/pam.py +16 -10
  24. {radicale-3.8.1 → radicale-3.8.2}/radicale/config.py +8 -0
  25. {radicale-3.8.1 → radicale-3.8.2}/radicale/hook/__init__.py +3 -1
  26. {radicale-3.8.1 → radicale-3.8.2}/radicale/httputils.py +2 -1
  27. {radicale-3.8.1 → radicale-3.8.2}/radicale/item/__init__.py +40 -9
  28. {radicale-3.8.1 → radicale-3.8.2}/radicale/item/filter.py +9 -3
  29. {radicale-3.8.1 → radicale-3.8.2}/radicale/rights/__init__.py +3 -1
  30. {radicale-3.8.1 → radicale-3.8.2}/radicale/server.py +13 -5
  31. {radicale-3.8.1 → radicale-3.8.2}/radicale/sharing/__init__.py +11 -0
  32. {radicale-3.8.1 → radicale-3.8.2}/radicale/sharing/common.py +6 -4
  33. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/__init__.py +13 -5
  34. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/create_collection.py +1 -1
  35. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/lock.py +31 -16
  36. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/move.py +2 -1
  37. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/upload.py +1 -1
  38. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/main.css +10 -0
  39. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/index.html +8 -0
  40. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/models/collection.js +4 -0
  41. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CollectionsScene.js +31 -12
  42. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CreateEditShareScene.js +2 -2
  43. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +1 -0
  44. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +4 -4
  45. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/utils/misc.js +37 -1
  46. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/utils/permissions.js +40 -16
  47. {radicale-3.8.1 → radicale-3.8.2}/rights +7 -0
  48. radicale-3.8.2/setup.cfg +4 -0
  49. radicale-3.8.1/setup.cfg +0 -27
  50. {radicale-3.8.1 → radicale-3.8.2}/COPYING.md +0 -0
  51. {radicale-3.8.1 → radicale-3.8.2}/MANIFEST.in +0 -0
  52. {radicale-3.8.1 → radicale-3.8.2}/README.md +0 -0
  53. {radicale-3.8.1 → radicale-3.8.2}/Radicale.egg-info/dependency_links.txt +0 -0
  54. {radicale-3.8.1 → radicale-3.8.2}/Radicale.egg-info/entry_points.txt +0 -0
  55. {radicale-3.8.1 → radicale-3.8.2}/Radicale.egg-info/requires.txt +0 -0
  56. {radicale-3.8.1 → radicale-3.8.2}/Radicale.egg-info/top_level.txt +0 -0
  57. {radicale-3.8.1 → radicale-3.8.2}/radicale/__init__.py +0 -0
  58. {radicale-3.8.1 → radicale-3.8.2}/radicale/__main__.py +0 -0
  59. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/head.py +0 -0
  60. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/options.py +0 -0
  61. {radicale-3.8.1 → radicale-3.8.2}/radicale/app/post.py +0 -0
  62. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/__init__.py +0 -0
  63. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/denyall.py +0 -0
  64. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/dovecot.py +0 -0
  65. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/http_remote_user.py +0 -0
  66. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/http_x_remote_user.py +0 -0
  67. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/none.py +0 -0
  68. {radicale-3.8.1 → radicale-3.8.2}/radicale/auth/remote_user.py +0 -0
  69. {radicale-3.8.1 → radicale-3.8.2}/radicale/group/__init__.py +0 -0
  70. {radicale-3.8.1 → radicale-3.8.2}/radicale/group/from_auth.py +0 -0
  71. {radicale-3.8.1 → radicale-3.8.2}/radicale/group/htgroup.py +0 -0
  72. {radicale-3.8.1 → radicale-3.8.2}/radicale/group/none.py +0 -0
  73. {radicale-3.8.1 → radicale-3.8.2}/radicale/hook/email/__init__.py +0 -0
  74. {radicale-3.8.1 → radicale-3.8.2}/radicale/hook/none.py +0 -0
  75. {radicale-3.8.1 → radicale-3.8.2}/radicale/hook/rabbitmq/__init__.py +0 -0
  76. {radicale-3.8.1 → radicale-3.8.2}/radicale/log.py +0 -0
  77. {radicale-3.8.1 → radicale-3.8.2}/radicale/pathutils.py +0 -0
  78. {radicale-3.8.1 → radicale-3.8.2}/radicale/py.typed +0 -0
  79. {radicale-3.8.1 → radicale-3.8.2}/radicale/rights/authenticated.py +0 -0
  80. {radicale-3.8.1 → radicale-3.8.2}/radicale/rights/from_file.py +0 -0
  81. {radicale-3.8.1 → radicale-3.8.2}/radicale/rights/owner_only.py +0 -0
  82. {radicale-3.8.1 → radicale-3.8.2}/radicale/rights/owner_write.py +0 -0
  83. {radicale-3.8.1 → radicale-3.8.2}/radicale/sharing/csv.py +0 -0
  84. {radicale-3.8.1 → radicale-3.8.2}/radicale/sharing/files.py +0 -0
  85. {radicale-3.8.1 → radicale-3.8.2}/radicale/sharing/none.py +0 -0
  86. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/__init__.py +0 -0
  87. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/base.py +0 -0
  88. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/cache.py +0 -0
  89. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/delete.py +0 -0
  90. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/discover.py +0 -0
  91. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/get.py +0 -0
  92. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/history.py +0 -0
  93. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/meta.py +0 -0
  94. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/sync.py +0 -0
  95. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem/verify.py +0 -0
  96. {radicale-3.8.1 → radicale-3.8.2}/radicale/storage/multifilesystem_nolock.py +0 -0
  97. {radicale-3.8.1 → radicale-3.8.2}/radicale/tests/custom/__init__.py +0 -0
  98. {radicale-3.8.1 → radicale-3.8.2}/radicale/tests/custom/auth.py +0 -0
  99. {radicale-3.8.1 → radicale-3.8.2}/radicale/tests/custom/rights.py +0 -0
  100. {radicale-3.8.1 → radicale-3.8.2}/radicale/tests/custom/storage_simple_sync.py +0 -0
  101. {radicale-3.8.1 → radicale-3.8.2}/radicale/tests/custom/web.py +0 -0
  102. {radicale-3.8.1 → radicale-3.8.2}/radicale/types.py +0 -0
  103. {radicale-3.8.1 → radicale-3.8.2}/radicale/utils.py +0 -0
  104. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/__init__.py +0 -0
  105. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal.py +0 -0
  106. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icon.png +0 -0
  107. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/delete.svg +0 -0
  108. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/download.svg +0 -0
  109. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/edit.svg +0 -0
  110. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/eye.svg +0 -0
  111. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/key.svg +0 -0
  112. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/new.svg +0 -0
  113. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/repeat.svg +0 -0
  114. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/share.svg +0 -0
  115. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/icons/upload.svg +0 -0
  116. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/loading.svg +0 -0
  117. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/css/logo.svg +0 -0
  118. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/api/api.js +0 -0
  119. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/api/common.js +0 -0
  120. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/api/sharing.js +0 -0
  121. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/config.js +0 -0
  122. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/constants.js +0 -0
  123. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/main.js +0 -0
  124. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js +0 -0
  125. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/DeleteConfirmationScene.js +0 -0
  126. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/LoadingScene.js +0 -0
  127. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/LoginScene.js +0 -0
  128. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/UploadCollectionScene.js +0 -0
  129. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/scene_manager.js +0 -0
  130. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/utils/collection_utils.js +0 -0
  131. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/utils/collections_cache.js +0 -0
  132. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/utils/error.js +0 -0
  133. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/utils/form_validator.js +0 -0
  134. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/internal_data/js/utils/url_text.js +0 -0
  135. {radicale-3.8.1 → radicale-3.8.2}/radicale/web/none.py +0 -0
  136. {radicale-3.8.1 → radicale-3.8.2}/radicale/xmlutils.py +0 -0
  137. {radicale-3.8.1 → radicale-3.8.2}/radicale.wsgi +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.8.2
4
+ * Add: tests: sharing API plus CalDAV/CardDAV workflow and corrupt CSV handling (covers #2259)
5
+ * Add: tests: mocked LDAP, IMAP, OAuth2 and PAM authentication servers (covers #2259)
6
+ * Improve: storage hook: avoid using preexc_fn, use process_group
7
+ * Add: sharing/token: plain GET and HEAD serve free/busy when Actions config `view` is `freebusy`
8
+ * Add: WebUI read-only free/busy calendar URL under each calendar
9
+ * Add: read-only free/busy view via GET ?view=freebusy
10
+ * Fix: free-busy REPORT returns one VFREEBUSY with FREEBUSY periods and no event details (RFC 4791)
11
+ * Add: rights permission `f` to read free-busy time without event details
12
+ * Fix: free-busy REPORT does not expand transparent recurrences
13
+ * Fix: sharing/bday-conversion: add proper RRULE and RECURRENCE-ID in case of "age" support is triggered
14
+ * Fix: sharing/bday-conversion: do not serve item.vcf in parallel of item.ics
15
+ * Fix: sharing/bday-conversion: adjust etag depending on template (item) or content (collection)
16
+ * Fix: sharing/propfind: remove unexpected additional shares listed in case of request on collection with HTTP_DEPTH=1
17
+ * Add: sharing/bday-conversion: X-RADICALE-NAME to generated bday items
18
+ * Fix: sharing/propfind: suppress (resolved) shares having same URI as native ones
19
+ * Fix: sharing/mkcol+mkcalendar: detect conflict with existing resolved shares
20
+ * Fix: WebUI/sharing: display permissions and share-by-group
21
+ * Performance: guard expensive logging with isEnabledFor (where missed)
22
+ * Performance: use lazy formatting for logger calls (where missed)
23
+ * Fix: delete/sharing: neither allow resolved share nor still mapped collections to be deleted
24
+ * Fix: sharing: resolved share resulting in same path as original requested will not have precedence
25
+
3
26
  ## 3.8.1
4
27
  * Add: tests: optional caldav-server-tester based CalDAV compatibility test suite (caldav_compat_tests)
5
28
  * Fix: sharing/bday-conversion: add missing support for comma separated text components in vcard field N (name)
@@ -2269,6 +2269,35 @@ is thrown instead of returning the results.
2269
2269
 
2270
2270
  Default: 10000
2271
2271
 
2272
+ ##### freebusy_view_past_days
2273
+
2274
+ _(>= 3.8.2)_
2275
+
2276
+ Number of days before today included in the default window of the
2277
+ read-only free/busy view (`GET` or `HEAD` with `?view=freebusy`). The
2278
+ window starts at midnight UTC. `0` starts the window at midnight UTC
2279
+ today. A request that provides both `start` and `end`
2280
+ (`YYYYMMDDTHHMMSSZ`) overrides this default and
2281
+ `freebusy_view_future_days`.
2282
+
2283
+ Default: 365
2284
+
2285
+ ##### freebusy_view_future_days
2286
+
2287
+ _(>= 3.8.2)_
2288
+
2289
+ Number of days after today included in the default window of the
2290
+ read-only free/busy view. The window ends at midnight UTC that many
2291
+ days ahead. `0` ends the window at midnight UTC today.
2292
+
2293
+ Default: 365
2294
+
2295
+ A share-by-token can omit `?view=freebusy`. Set that token's Actions
2296
+ config `view` to `freebusy`. Plain `GET` and `HEAD` of the token then
2297
+ use this view, including an optional `start` and `end`. Any other
2298
+ `view` value is rejected. Other methods keep the token permissions.
2299
+ See [Collection Sharing](https://github.com/Kozea/Radicale/blob/master/SHARING.md).
2300
+
2272
2301
  #### [sharing]
2273
2302
 
2274
2303
  _(>= 3.7.0)_
@@ -2370,7 +2399,7 @@ Default permissions for create token-based sharing
2370
2399
 
2371
2400
  Default: `r`
2372
2401
 
2373
- Supported: `rwEePp`
2402
+ Supported: `rfwEePp`
2374
2403
 
2375
2404
  ##### default_permissions_create_map
2376
2405
 
@@ -2380,7 +2409,7 @@ Default permissions for map-based sharing
2380
2409
 
2381
2410
  Default: `r`
2382
2411
 
2383
- Supported: `rwEePp`
2412
+ Supported: `rfwEePp`
2384
2413
 
2385
2414
  ##### conversion_bday_summary_template
2386
2415
 
@@ -2388,7 +2417,7 @@ _(>= 3.7.5)_
2388
2417
 
2389
2418
  Global template for summary of conversion "bday"
2390
2419
 
2391
- Default: `{{n:f} {n:g}|{fn}|{nickname}} ({year}) (BDAY)`
2420
+ Default: `[{n:f} {n:g}|{fn}|{nickname}] ({year}) (BDAY)`
2392
2421
 
2393
2422
  Supported placeholders (data used from VCARD)
2394
2423
  * `{year}`: year of birthday (RFC6350#6.2.5)
@@ -2677,6 +2706,8 @@ The following `permissions` are recognized:
2677
2706
  * **r:** read address book and calendar collections
2678
2707
  * **i:** subset of **r** that only allows direct access via HTTP method GET
2679
2708
  (CalDAV/CardDAV is susceptible to expensive search requests)
2709
+ * **f:** read the free-busy time of a calendar, without event details
2710
+ (**r** includes this)
2680
2711
  * **W:** write collections (excluding address books and calendars)
2681
2712
  * **w:** write address book and calendar collections
2682
2713
  * **D:** allow deleting a collection in case `permit_delete_collection=False` _(>= 3.3.0)_
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Radicale
3
- Version: 3.8.1
3
+ Version: 3.8.2
4
4
  Summary: CalDAV and CardDAV Server
5
5
  Author-email: Guillaume Ayoub <guillaume.ayoub@kozea.fr>, Unrud <unrud@outlook.com>, Peter Bieringer <pb@bieringer.de>
6
6
  License: GNU GPL v3
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Radicale
3
- Version: 3.8.1
3
+ Version: 3.8.2
4
4
  Summary: CalDAV and CardDAV Server
5
5
  Author-email: Guillaume Ayoub <guillaume.ayoub@kozea.fr>, Unrud <unrud@outlook.com>, Peter Bieringer <pb@bieringer.de>
6
6
  License: GNU GPL v3
@@ -7,7 +7,6 @@ config
7
7
  pyproject.toml
8
8
  radicale.wsgi
9
9
  rights
10
- setup.cfg
11
10
  Radicale.egg-info/PKG-INFO
12
11
  Radicale.egg-info/SOURCES.txt
13
12
  Radicale.egg-info/dependency_links.txt
@@ -372,11 +372,11 @@ type = none
372
372
  #enforce_properties_overlay = true
373
373
 
374
374
  # Default permissions for token-based sharing
375
- # Supported: rwEePp
375
+ # Supported: rfwEePp
376
376
  #default_permissions_create_token = rp
377
377
 
378
378
  # Default permissions for map-based sharing
379
- # Supported: rwEePp
379
+ # Supported: rfwEePp
380
380
  #default_permissions_create_map = r
381
381
 
382
382
  # Global template for summary of conversion "bday"
@@ -526,3 +526,9 @@ Content-Security-Policy = default-src 'self'; object-src 'none'
526
526
  # When returning a free-busy report, limit the number of returned
527
527
  # occurences per event to prevent DoS attacks.
528
528
  #max_freebusy_occurrence = 10000
529
+
530
+ # Days before and after today in the default free-busy view
531
+ # (?view=freebusy). The window is aligned to midnight UTC.
532
+ # A request can override both with start and end.
533
+ #freebusy_view_past_days = 365
534
+ #freebusy_view_future_days = 365
@@ -3,7 +3,7 @@ name = "Radicale"
3
3
  # When the version is updated, a new section in the CHANGELOG.md file must be
4
4
  # added too.
5
5
  readme = "README.md"
6
- version = "3.8.1"
6
+ version = "3.8.2"
7
7
  authors = [{name = "Guillaume Ayoub", email = "guillaume.ayoub@kozea.fr"}, {name = "Unrud", email = "unrud@outlook.com"}, {name = "Peter Bieringer", email = "pb@bieringer.de"}]
8
8
  license = {text = "GNU GPL v3"}
9
9
  description = "CalDAV and CardDAV Server"
@@ -60,6 +60,7 @@ min_version = "4.0"
60
60
  envlist = ["flake8", "isort", "mypy", "py_loglevel_info", "py_radicale_loglevel_info", "py_radicale_loglevel_trace", "py", "integ_test", "caldav_compat_test", "html5validator"]
61
61
 
62
62
  [tool.tox.env.py]
63
+ description = "Server: run 'pytest' (incl. coverage report)"
63
64
  extras = ["test"]
64
65
  deps = [
65
66
  "pytest",
@@ -68,6 +69,7 @@ deps = [
68
69
  commands = [["pytest", "-r", "s", "--cov", "--cov-report=term", "--cov-report=xml", "."]]
69
70
 
70
71
  [tool.tox.env.py_loglevel_info]
72
+ description = "Server: run 'pytest' with log level 'INFO'"
71
73
  extras = ["test"]
72
74
  deps = [
73
75
  "pytest"
@@ -75,6 +77,7 @@ deps = [
75
77
  commands = [["pytest", "-r", "s", "--log-level", "INFO", "."]]
76
78
 
77
79
  [tool.tox.env.py_radicale_loglevel_info]
80
+ description = "Server: run 'pytest' with radicale on log level 'INFO'"
78
81
  extras = ["test"]
79
82
  setenv = { PYTEST_RADICALE_LOGLEVEL = "info" }
80
83
  deps = [
@@ -83,6 +86,7 @@ deps = [
83
86
  commands = [["pytest", "-r", "s", "--log-level", "INFO", "."]]
84
87
 
85
88
  [tool.tox.env.py_radicale_loglevel_trace]
89
+ description = "Server: run 'pytest' with radicale on log level 'TRACE'"
86
90
  extras = ["test"]
87
91
  setenv = { PYTEST_RADICALE_LOGLEVEL = "trace" }
88
92
  deps = [
@@ -91,6 +95,7 @@ deps = [
91
95
  commands = [["pytest", "-r", "s", "--log-level", "5", "."]]
92
96
 
93
97
  [tool.tox.env.py_filesystem_vfat]
98
+ description = "Server: run 'pytest' with collection storage filesystem 'vfat' (on Linux)"
94
99
  allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ]
95
100
  extras = ["test"]
96
101
  deps = [
@@ -118,6 +123,7 @@ commands_post = [
118
123
  ]
119
124
 
120
125
  [tool.tox.env.py_filesystem_vfat_utf8]
126
+ description = "Server: run 'pytest' with collection storage filesystem 'vfat+utf8' (on Linux)"
121
127
  allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ]
122
128
  extras = ["test"]
123
129
  deps = [
@@ -145,6 +151,7 @@ commands_post = [
145
151
  ]
146
152
 
147
153
  [tool.tox.env.py_filesystem_hfsplus]
154
+ description = "Server: run 'pytest' with collection storage filesystem 'hfsplus' (on Linux)"
148
155
  # Fedora
149
156
  allowlist_externals = [ "sudo", "dd", "mkfs.hfsplus", "mkdir", "rm", "rmdir", "chmod" ]
150
157
  extras = ["test"]
@@ -173,6 +180,7 @@ commands_post = [
173
180
  ]
174
181
 
175
182
  [tool.tox.env.py_filesystem_ntfs]
183
+ description = "Server: run 'pytest' with collection storage filesystem 'ntfs' (on Linux)"
176
184
  allowlist_externals = [ "sudo", "dd", "mkfs.ntfs", "mkdir", "rm", "rmdir", "chmod" ]
177
185
  extras = ["test"]
178
186
  deps = [
@@ -200,21 +208,25 @@ commands_post = [
200
208
  ]
201
209
 
202
210
  [tool.tox.env.flake8]
211
+ description = "Server: test code style"
203
212
  deps = ["flake8==7.1.0"]
204
213
  commands = [["flake8", "."]]
205
214
  skip_install = true
206
215
 
207
216
  [tool.tox.env.isort]
217
+ description = "Server: test import sort order"
208
218
  deps = ["isort==5.13.2"]
209
219
  commands = [["isort", "--check", "--diff", "."]]
210
220
  skip_install = true
211
221
 
212
222
  [tool.tox.env.mypy]
223
+ description = "Server: test static types"
213
224
  deps = ["mypy==1.11.0"]
214
225
  commands = [["mypy", "--install-types", "--non-interactive", "."]]
215
226
  skip_install = true
216
227
 
217
228
  [tool.tox.env.integ_test]
229
+ description = "Server+WebUI: run 'playwright' integration tests (with 'chromium')"
218
230
  deps = ["pytest", "pytest-playwright"]
219
231
  # Store Playwright browsers inside .tox for better caching
220
232
  setenv = { PLAYWRIGHT_BROWSERS_PATH = "{toxworkdir}/.playwright" }
@@ -222,15 +234,25 @@ commands_pre = [["playwright", "install", "chromium"]]
222
234
  commands = [["pytest", "integ_tests"]]
223
235
 
224
236
  [tool.tox.env.caldav_compat_test]
237
+ description = "Server: run caldav compatibility tests"
225
238
  extras = ["caldav_test"]
226
239
  deps = [
227
240
  "caldav",
228
- "caldav_server_tester",
241
+ "caldav_server_tester==1.2.0", # synchronize version with .github/workflows/test.yml#caldav-compat-test
229
242
  "pytest"
230
243
  ]
231
244
  commands = [["pytest", "caldav_compat_tests"]]
232
245
 
246
+ [tool.tox.env.js]
247
+ # for optional local test as of now, copied from .github/workflows/test.yml
248
+ # Fedora (will install nodejs): yum install npx
249
+ description = "WebUI: validate JavaScript"
250
+ allowlist_externals = [ "npx" ]
251
+ commands = [["npx", "-y", "-p", "typescript@7.0.2", "tsc", "-p", "radicale/web/jsconfig.json"]]
252
+ skip_install = true
253
+
233
254
  [tool.tox.env.html5validator]
255
+ description = "WebUI: validate HTML"
234
256
  deps = ["html5validator-2"]
235
257
  commands = [["html5validator", "--root", "radicale/web/internal_data/", "--also-check-css"]]
236
258
 
@@ -45,7 +45,7 @@ from radicale import config, httputils, log, pathutils, types, utils
45
45
  from radicale.app import base as app_base
46
46
  from radicale.app.base import ApplicationBase
47
47
  from radicale.app.delete import ApplicationPartDelete
48
- from radicale.app.get import ApplicationPartGet
48
+ from radicale.app.get import ApplicationPartGet, requests_freebusy_view
49
49
  from radicale.app.head import ApplicationPartHead
50
50
  from radicale.app.mkcalendar import ApplicationPartMkcalendar
51
51
  from radicale.app.mkcol import ApplicationPartMkcol
@@ -305,7 +305,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
305
305
  condition=self._response_content_on_notice_condition,
306
306
  value=request_info,
307
307
  ):
308
- logger.notice("Response content (nonXML, log condition passed):\n%s", utils.textwrap_str(answer, self._limit_content))
308
+ if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
309
+ logger.notice("Response content (nonXML, log condition passed):\n%s", utils.textwrap_str(answer, self._limit_content))
309
310
  else:
310
311
  if logger.isEnabledFor(logging.DEBUG):
311
312
  logger.debug("Response content: suppressed by config/option [logging] response_content_on_debug")
@@ -340,7 +341,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
340
341
  condition=self._response_header_on_notice_condition,
341
342
  value=request_info,
342
343
  ):
343
- logger.notice("Response header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(headers), self._limit_content))
344
+ if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
345
+ logger.notice("Response header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(headers), self._limit_content))
344
346
  else:
345
347
  if logger.isEnabledFor(logging.DEBUG):
346
348
  logger.debug("Response header: suppressed by config/option [logging] response_header_on_debug")
@@ -474,8 +476,9 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
474
476
  request_method, unsafe_path, depthinfo,
475
477
  remote_host, remote_useragent_txt, https_info)
476
478
  if self._request_header_on_debug:
477
- logger.debug("Request header:\n%s",
478
- utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
479
+ if logger.isEnabledFor(logging.DEBUG):
480
+ logger.debug("Request header:\n%s",
481
+ utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
479
482
  else:
480
483
  if not self._request_header_on_notice_condition != {}:
481
484
  # conditional request header logging is later
@@ -570,7 +573,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
570
573
  condition=self._request_header_on_notice_condition,
571
574
  value=request_info,
572
575
  ):
573
- logger.notice("Request header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
576
+ if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
577
+ logger.notice("Request header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
574
578
 
575
579
  if user and login == user:
576
580
  logger.info("Successful login: %r (%s)", user, info)
@@ -668,8 +672,12 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
668
672
  profiler_active = True
669
673
 
670
674
  try:
671
- status, headers, answer, xml_request = function(
672
- environ, base_prefix, path, user, request_info)
675
+ if (requests_freebusy_view(environ) and
676
+ request_method not in ("GET", "HEAD")):
677
+ status, headers, answer, xml_request = httputils.METHOD_NOT_ALLOWED
678
+ else:
679
+ status, headers, answer, xml_request = function(
680
+ environ, base_prefix, path, user, request_info)
673
681
  except PermissionError as e:
674
682
  logger.error("PermissionError: %s", e)
675
683
  status, headers, answer, xml_request = httputils.INTERNAL_SERVER_ERROR
@@ -215,7 +215,8 @@ class ApplicationBase:
215
215
  condition=self._request_content_on_notice_condition,
216
216
  value=request_info,
217
217
  ):
218
- logger.notice("Request content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
218
+ if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
219
+ logger.notice("Request content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
219
220
  else:
220
221
  if logger.isEnabledFor(logging.DEBUG):
221
222
  logger.debug("Request content (XML, log condition skipped): suppressed")
@@ -236,7 +237,8 @@ class ApplicationBase:
236
237
  condition=self._response_content_on_notice_condition,
237
238
  value=request_info,
238
239
  ):
239
- logger.notice("Response content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
240
+ if logger.isEnabledFor(log.LOG_LEVEL_NOTICE):
241
+ logger.notice("Response content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
240
242
  else:
241
243
  if logger.isEnabledFor(logging.DEBUG):
242
244
  logger.debug("Response content (XML, log condition skipped): suppressed")
@@ -255,6 +257,12 @@ class ApplicationBase:
255
257
  return status, headers, content, None
256
258
 
257
259
 
260
+ def _with_implied_freebusy(permissions: str) -> str:
261
+ if "r" in permissions and "f" not in permissions:
262
+ return permissions + "f"
263
+ return permissions
264
+
265
+
258
266
  class Access:
259
267
  """Helper class to check access rights of an item"""
260
268
 
@@ -275,8 +283,8 @@ class Access:
275
283
  self.permissions = self._rights.authorization(self.user, self.path)
276
284
  if permissions_filter is not None:
277
285
  self._permissions_filter = permissions_filter
278
- permissions_filtered = intersect(self.permissions, permissions_filter)
279
- self.permissions = permissions_filtered
286
+ self.permissions = intersect(
287
+ _with_implied_freebusy(self.permissions), permissions_filter)
280
288
  self._parent_permissions = None
281
289
 
282
290
  @property
@@ -287,8 +295,9 @@ class Access:
287
295
  self._parent_permissions = self._rights.authorization(
288
296
  self.user, self.parent_path)
289
297
  if self._permissions_filter is not None:
290
- parent_permissions_filtered = intersect(self._parent_permissions, self._permissions_filter)
291
- self._parent_permissions = parent_permissions_filtered
298
+ self._parent_permissions = intersect(
299
+ _with_implied_freebusy(self._parent_permissions),
300
+ self._permissions_filter)
292
301
  return self._parent_permissions
293
302
 
294
303
  def check(self, permission: str,
@@ -310,3 +319,11 @@ class Access:
310
319
  return bool(rights.intersect(self.permissions, permissions) or (
311
320
  self.path != self.parent_path and
312
321
  rights.intersect(self.parent_permissions, parent_permissions)))
322
+
323
+ def allows_freebusy(self, item: Optional[types.CollectionOrItem] = None
324
+ ) -> bool:
325
+ if item is not None and (
326
+ not isinstance(item, storage.BaseCollection) or
327
+ item.tag != "VCALENDAR"):
328
+ return False
329
+ return "r" in self.permissions or "f" in self.permissions
@@ -79,6 +79,9 @@ class ApplicationPartDelete(ApplicationBase):
79
79
  user_lookup += sharing.SHARING_SEPARATOR_GROUP + ','.join(self._rights._user_groups)
80
80
  share = self._sharing.sharing_collection_resolver(path, user_lookup)
81
81
  if share:
82
+ if path.endswith('/'):
83
+ logger.notice("delete of a mapped collection is never permitted: %r -> %r", share['PathOrToken'], path)
84
+ return httputils.NOT_ALLOWED
82
85
  # overwrite and run through extended permission check
83
86
  path = share['PathMapped']
84
87
  user = share['Owner']
@@ -86,6 +89,12 @@ class ApplicationPartDelete(ApplicationBase):
86
89
  access = Access(self._rights, user, path, permissions_filter)
87
90
  if not access.check("w"):
88
91
  return httputils.NOT_ALLOWED
92
+ if self._sharing._enabled:
93
+ if not share:
94
+ collections_share_list = self._sharing.database_list_sharing(PathMapped=path)
95
+ if collections_share_list is not None and len(collections_share_list) > 0:
96
+ logger.notice("delete of a mapped collection in use (%d) is not permitted: %r", len(collections_share_list), path)
97
+ return httputils.CONFLICT
89
98
  with self._storage.acquire_lock("w", user, path=path, request="DELETE"):
90
99
  item = next(iter(self._storage.discover(path)), None)
91
100
  if not item:
@@ -100,11 +109,11 @@ class ApplicationPartDelete(ApplicationBase):
100
109
  if isinstance(item, storage.BaseCollection):
101
110
  if self._permit_delete_collection:
102
111
  if access.check("d", item):
103
- logger.info("delete of collection is permitted by config/option [rights] permit_delete_collection but explicit forbidden by permission 'd': %s", path)
112
+ logger.info("delete of collection is permitted by config/option [rights] permit_delete_collection but explicit forbidden by permission 'd': %r", path)
104
113
  return httputils.NOT_ALLOWED
105
114
  else:
106
115
  if not access.check("D", item):
107
- logger.info("delete of collection is prevented by config/option [rights] permit_delete_collection and not explicit allowed by permission 'D': %s", path)
116
+ logger.info("delete of collection is prevented by config/option [rights] permit_delete_collection and not explicit allowed by permission 'D': %r", path)
108
117
  return httputils.NOT_ALLOWED
109
118
  if self._hook.enabled:
110
119
  for i in item.get_all():
@@ -18,17 +18,22 @@
18
18
  # You should have received a copy of the GNU General Public License
19
19
  # along with Radicale. If not, see <http://www.gnu.org/licenses/>.
20
20
 
21
+ import contextlib
22
+ import datetime
21
23
  import plistlib
22
24
  import posixpath
25
+ import re
23
26
  import urllib
24
27
  import uuid
25
28
  from http import client
26
- from typing import Union
27
- from urllib.parse import quote
29
+ from typing import Tuple, Union
30
+ from urllib.parse import parse_qs, quote
28
31
 
29
32
  from radicale import (httputils, pathutils, sharing, storage, types, utils,
30
33
  xmlutils)
31
34
  from radicale.app.base import Access, ApplicationBase
35
+ from radicale.app.report import freebusy_collection_text
36
+ from radicale.item import get_etag
32
37
  from radicale.log import logger
33
38
 
34
39
 
@@ -57,6 +62,35 @@ def propose_filename(collection: storage.BaseCollection, share: Union[dict, None
57
62
  return title
58
63
 
59
64
 
65
+ _FREEBUSY_VIEW_TIME = re.compile(r"[0-9]{8}T[0-9]{6}Z")
66
+
67
+
68
+ def _parse_freebusy_view_time(value: str) -> datetime.datetime:
69
+ if _FREEBUSY_VIEW_TIME.fullmatch(value) is None:
70
+ raise ValueError("invalid free-busy view time: %r" % value)
71
+ return datetime.datetime.strptime(
72
+ value, "%Y%m%dT%H%M%SZ").replace(tzinfo=datetime.timezone.utc)
73
+
74
+
75
+ def requests_freebusy_view(environ: types.WSGIEnviron) -> bool:
76
+ query = parse_qs(environ.get("QUERY_STRING", ""), keep_blank_values=True)
77
+ view = query.get("view")
78
+ return view is not None and "freebusy" in view
79
+
80
+
81
+ def share_forces_freebusy_view(share: Union[dict, None]) -> bool:
82
+ """A token can request the free/busy view without ``?view=freebusy``."""
83
+ if not share or share.get("ShareType") != "token":
84
+ return False
85
+ actions = share.get("Actions")
86
+ if not isinstance(actions, dict):
87
+ return False
88
+ config = actions.get("config")
89
+ if not isinstance(config, dict):
90
+ return False
91
+ return config.get("view") == "freebusy"
92
+
93
+
60
94
  class ApplicationPartGet(ApplicationBase):
61
95
 
62
96
  def _content_disposition_attachment(self, filename: str) -> str:
@@ -71,6 +105,74 @@ class ApplicationPartGet(ApplicationBase):
71
105
  value += "; filename*=%s''%s" % (self._encoding, encoded_filename)
72
106
  return value
73
107
 
108
+ def _freebusy_view_range(
109
+ self, query: dict) -> Tuple[datetime.datetime, datetime.datetime]:
110
+ starts = query.get("start", [])
111
+ ends = query.get("end", [])
112
+ if not starts and not ends:
113
+ today = datetime.datetime.now(datetime.timezone.utc).replace(
114
+ hour=0, minute=0, second=0, microsecond=0)
115
+ past_days = self.configuration.get(
116
+ "reporting", "freebusy_view_past_days")
117
+ future_days = self.configuration.get(
118
+ "reporting", "freebusy_view_future_days")
119
+ return (today - datetime.timedelta(days=past_days),
120
+ today + datetime.timedelta(days=future_days))
121
+ if len(starts) != 1 or len(ends) != 1:
122
+ raise ValueError("free-busy view requires both start and end")
123
+ start = _parse_freebusy_view_time(starts[0])
124
+ end = _parse_freebusy_view_time(ends[0])
125
+ if end < start:
126
+ raise ValueError("free-busy view end is before start")
127
+ return start, end
128
+
129
+ def _freebusy_view(self, environ: types.WSGIEnviron, path: str, user: str,
130
+ access: Access, share: Union[dict, None]
131
+ ) -> types.WSGIResponse:
132
+ if not access.allows_freebusy() and "i" not in access.permissions:
133
+ return httputils.NOT_ALLOWED
134
+ query = parse_qs(environ.get("QUERY_STRING", ""), keep_blank_values=True)
135
+ with contextlib.ExitStack() as lock_stack:
136
+ lock_stack.enter_context(self._storage.acquire_lock("r", user))
137
+ item = next(iter(self._storage.discover(path)), None)
138
+ if not item:
139
+ return httputils.NOT_FOUND
140
+ if (not isinstance(item, storage.BaseCollection) or
141
+ item.tag != "VCALENDAR"):
142
+ return httputils.NOT_ALLOWED
143
+ if (not access.allows_freebusy(item) and
144
+ "i" not in access.permissions):
145
+ return httputils.NOT_ALLOWED
146
+ if query.get("view") != ["freebusy"]:
147
+ return httputils.BAD_REQUEST
148
+ try:
149
+ range_start, range_end = self._freebusy_view_range(query)
150
+ except ValueError as e:
151
+ logger.warning("Bad free-busy view on %r: %s", path, e,
152
+ exc_info=True)
153
+ return httputils.BAD_REQUEST
154
+ filename = propose_filename(item, share)
155
+ if filename.lower().endswith(".ics"):
156
+ filename = filename[:-4] + "-freebusy.ics"
157
+ else:
158
+ filename += "-freebusy.ics"
159
+ content_disposition = self._content_disposition_attachment(filename)
160
+ max_occurrence = self.configuration.get(
161
+ "reporting", "max_freebusy_occurrence")
162
+ try:
163
+ answer = freebusy_collection_text(
164
+ item, range_start, range_end, max_occurrence,
165
+ lock_stack.close)
166
+ except ValueError as e:
167
+ logger.warning("Bad free-busy view on %r: %s", path, e,
168
+ exc_info=True)
169
+ return httputils.BAD_REQUEST
170
+ headers = {
171
+ "Content-Type": xmlutils.MIMETYPES["VCALENDAR"],
172
+ "ETag": get_etag(answer),
173
+ "Content-Disposition": content_disposition}
174
+ return client.OK, headers, answer, None
175
+
74
176
  def do_GET(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
75
177
  user: str, request_info: dict) -> types.WSGIResponse:
76
178
  """Manage GET request."""
@@ -154,11 +256,30 @@ class ApplicationPartGet(ApplicationBase):
154
256
  user_lookup += sharing.SHARING_SEPARATOR_GROUP + ','.join(self._rights._user_groups)
155
257
  share = self._sharing.sharing_collection_resolver(path, user_lookup)
156
258
  if share:
157
- # overwrite and run through extended permission check
158
- path = share['PathMapped']
159
- user = share['Owner']
160
- permissions_filter = share['Permissions']
259
+ if path != share['PathMapped']:
260
+ # overwrite and run through extended permission check
261
+ path = share['PathMapped']
262
+ user = share['Owner']
263
+ permissions_filter = share['Permissions']
264
+ else:
265
+ logger.trace("GET/shares: skip overlap mapping: path=%r", path)
161
266
  access = Access(self._rights, user, path, permissions_filter)
267
+ if share_forces_freebusy_view(share):
268
+ query = parse_qs(environ.get("QUERY_STRING", ""), keep_blank_values=True)
269
+ view = query.get("view")
270
+ if view is None:
271
+ forced_environ = dict(environ)
272
+ current = str(forced_environ.get("QUERY_STRING") or "")
273
+ if current:
274
+ forced_environ["QUERY_STRING"] = current + "&view=freebusy"
275
+ else:
276
+ forced_environ["QUERY_STRING"] = "view=freebusy"
277
+ return self._freebusy_view(
278
+ forced_environ, path, user, access, share)
279
+ if view != ["freebusy"]:
280
+ return httputils.BAD_REQUEST
281
+ if requests_freebusy_view(environ):
282
+ return self._freebusy_view(environ, path, user, access, share)
162
283
  if not access.check("r") and "i" not in access.permissions:
163
284
  return httputils.NOT_ALLOWED
164
285
  with self._storage.acquire_lock("r", user):
@@ -198,12 +319,23 @@ class ApplicationPartGet(ApplicationBase):
198
319
  headers["Content-Disposition"] = content_disposition
199
320
  if share and share['Conversion'] == "bday":
200
321
  if isinstance(item, storage.BaseCollection):
201
- # convert VCF to ICS
322
+ # convert collection VCF to ICS
202
323
  answer = item.serialize(vcf_to_ics=True, ShareActions=share['Actions'])
324
+ # set etag header (64 <hexdigits>)
325
+ # 4 chars special token: 'bda0'
326
+ # 8 chars date : 00000000
327
+ # 2 chars separator : 00
328
+ # 32 chars : leading content hash
329
+ # 2 chars separator : 00
330
+ # 16 chars : trailing original etag
331
+ headers["ETag"] = '"bda0' + '00000000' + '00' + utils.sha256_str(answer)[:32] + '00' + item.etag[49:]
203
332
  else:
333
+ # convert item VCF to ICS
204
334
  item_converted = item.convert_vcf_to_ics(ShareActions=share['Actions'])
205
335
  if item_converted is not None:
206
336
  answer = item_converted.serialize()
337
+ # set etag header
338
+ headers["ETag"] = item_converted.etag
207
339
  else:
208
340
  return httputils.NOT_FOUND
209
341
  else:
@@ -56,7 +56,7 @@ class ApplicationPartMkcalendar(ApplicationBase):
56
56
  return httputils.BAD_REQUEST
57
57
  if self._sharing._enabled:
58
58
  # check for shared collections (all)
59
- collections_share_list = self._sharing.sharing_collection_list()
59
+ collections_share_list = self._sharing.sharing_collection_list(User=user)
60
60
  if collections_share_list:
61
61
  for share in collections_share_list:
62
62
  if share['PathOrToken'] == path:
@@ -63,7 +63,7 @@ class ApplicationPartMkcol(ApplicationBase):
63
63
  return httputils.NOT_ALLOWED
64
64
  if self._sharing._enabled:
65
65
  # check for shared collections (all)
66
- collections_share_list = self._sharing.sharing_collection_list()
66
+ collections_share_list = self._sharing.sharing_collection_list(User=user)
67
67
  if collections_share_list:
68
68
  for share in collections_share_list:
69
69
  if share['PathOrToken'] == path: