Radicale 3.8.0__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 (138) hide show
  1. {radicale-3.8.0 → radicale-3.8.2}/CHANGELOG.md +32 -0
  2. {radicale-3.8.0 → radicale-3.8.2}/DOCUMENTATION.md +36 -4
  3. {radicale-3.8.0 → radicale-3.8.2}/PKG-INFO +5 -1
  4. {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/PKG-INFO +5 -1
  5. {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/SOURCES.txt +0 -1
  6. {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/requires.txt +7 -0
  7. {radicale-3.8.0 → radicale-3.8.2}/config +8 -2
  8. {radicale-3.8.0 → radicale-3.8.2}/pyproject.toml +37 -5
  9. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/__init__.py +16 -8
  10. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/base.py +23 -6
  11. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/delete.py +11 -2
  12. radicale-3.8.2/radicale/app/get.py +343 -0
  13. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/mkcalendar.py +1 -1
  14. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/mkcol.py +1 -1
  15. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/move.py +13 -22
  16. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/propfind.py +120 -26
  17. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/proppatch.py +7 -4
  18. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/put.py +9 -6
  19. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/report.py +205 -65
  20. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/htpasswd.py +9 -3
  21. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/imap.py +4 -2
  22. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/ldap.py +15 -15
  23. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/oauth2.py +5 -3
  24. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/pam.py +16 -10
  25. {radicale-3.8.0 → radicale-3.8.2}/radicale/config.py +8 -0
  26. {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/__init__.py +3 -1
  27. {radicale-3.8.0 → radicale-3.8.2}/radicale/httputils.py +2 -1
  28. {radicale-3.8.0 → radicale-3.8.2}/radicale/item/__init__.py +43 -9
  29. {radicale-3.8.0 → radicale-3.8.2}/radicale/item/filter.py +9 -3
  30. {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/__init__.py +3 -1
  31. {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/from_file.py +19 -2
  32. {radicale-3.8.0 → radicale-3.8.2}/radicale/server.py +13 -5
  33. {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/__init__.py +19 -8
  34. {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/common.py +6 -4
  35. {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/files.py +32 -31
  36. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/__init__.py +13 -5
  37. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/cache.py +4 -3
  38. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/create_collection.py +1 -1
  39. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/discover.py +15 -14
  40. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/get.py +10 -9
  41. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/history.py +3 -2
  42. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/lock.py +31 -16
  43. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/move.py +2 -1
  44. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/upload.py +1 -1
  45. {radicale-3.8.0 → radicale-3.8.2}/radicale/utils.py +22 -2
  46. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/main.css +21 -0
  47. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/index.html +11 -0
  48. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/models/collection.js +4 -0
  49. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CollectionsScene.js +92 -33
  50. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CreateEditShareScene.js +2 -2
  51. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +1 -0
  52. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +4 -4
  53. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/misc.js +37 -1
  54. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/permissions.js +40 -16
  55. {radicale-3.8.0 → radicale-3.8.2}/rights +7 -0
  56. radicale-3.8.2/setup.cfg +4 -0
  57. radicale-3.8.0/radicale/app/get.py +0 -151
  58. radicale-3.8.0/setup.cfg +0 -27
  59. {radicale-3.8.0 → radicale-3.8.2}/COPYING.md +0 -0
  60. {radicale-3.8.0 → radicale-3.8.2}/MANIFEST.in +0 -0
  61. {radicale-3.8.0 → radicale-3.8.2}/README.md +0 -0
  62. {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/dependency_links.txt +0 -0
  63. {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/entry_points.txt +0 -0
  64. {radicale-3.8.0 → radicale-3.8.2}/Radicale.egg-info/top_level.txt +0 -0
  65. {radicale-3.8.0 → radicale-3.8.2}/radicale/__init__.py +0 -0
  66. {radicale-3.8.0 → radicale-3.8.2}/radicale/__main__.py +0 -0
  67. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/head.py +0 -0
  68. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/options.py +0 -0
  69. {radicale-3.8.0 → radicale-3.8.2}/radicale/app/post.py +0 -0
  70. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/__init__.py +0 -0
  71. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/denyall.py +0 -0
  72. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/dovecot.py +0 -0
  73. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/http_remote_user.py +0 -0
  74. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/http_x_remote_user.py +0 -0
  75. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/none.py +0 -0
  76. {radicale-3.8.0 → radicale-3.8.2}/radicale/auth/remote_user.py +0 -0
  77. {radicale-3.8.0 → radicale-3.8.2}/radicale/group/__init__.py +0 -0
  78. {radicale-3.8.0 → radicale-3.8.2}/radicale/group/from_auth.py +0 -0
  79. {radicale-3.8.0 → radicale-3.8.2}/radicale/group/htgroup.py +0 -0
  80. {radicale-3.8.0 → radicale-3.8.2}/radicale/group/none.py +0 -0
  81. {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/email/__init__.py +0 -0
  82. {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/none.py +0 -0
  83. {radicale-3.8.0 → radicale-3.8.2}/radicale/hook/rabbitmq/__init__.py +0 -0
  84. {radicale-3.8.0 → radicale-3.8.2}/radicale/log.py +0 -0
  85. {radicale-3.8.0 → radicale-3.8.2}/radicale/pathutils.py +0 -0
  86. {radicale-3.8.0 → radicale-3.8.2}/radicale/py.typed +0 -0
  87. {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/authenticated.py +0 -0
  88. {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/owner_only.py +0 -0
  89. {radicale-3.8.0 → radicale-3.8.2}/radicale/rights/owner_write.py +0 -0
  90. {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/csv.py +0 -0
  91. {radicale-3.8.0 → radicale-3.8.2}/radicale/sharing/none.py +0 -0
  92. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/__init__.py +0 -0
  93. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/base.py +0 -0
  94. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/delete.py +0 -0
  95. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/meta.py +0 -0
  96. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/sync.py +0 -0
  97. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem/verify.py +0 -0
  98. {radicale-3.8.0 → radicale-3.8.2}/radicale/storage/multifilesystem_nolock.py +0 -0
  99. {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/__init__.py +0 -0
  100. {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/auth.py +0 -0
  101. {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/rights.py +0 -0
  102. {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/storage_simple_sync.py +0 -0
  103. {radicale-3.8.0 → radicale-3.8.2}/radicale/tests/custom/web.py +0 -0
  104. {radicale-3.8.0 → radicale-3.8.2}/radicale/types.py +0 -0
  105. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/__init__.py +0 -0
  106. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal.py +0 -0
  107. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icon.png +0 -0
  108. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/delete.svg +0 -0
  109. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/download.svg +0 -0
  110. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/edit.svg +0 -0
  111. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/eye.svg +0 -0
  112. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/key.svg +0 -0
  113. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/new.svg +0 -0
  114. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/repeat.svg +0 -0
  115. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/share.svg +0 -0
  116. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/icons/upload.svg +0 -0
  117. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/loading.svg +0 -0
  118. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/css/logo.svg +0 -0
  119. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/api/api.js +0 -0
  120. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/api/common.js +0 -0
  121. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/api/sharing.js +0 -0
  122. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/config.js +0 -0
  123. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/constants.js +0 -0
  124. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/main.js +0 -0
  125. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js +1 -1
  126. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/DeleteConfirmationScene.js +0 -0
  127. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/LoadingScene.js +0 -0
  128. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/LoginScene.js +0 -0
  129. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/UploadCollectionScene.js +0 -0
  130. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/scenes/scene_manager.js +0 -0
  131. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/collection_utils.js +0 -0
  132. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/collections_cache.js +0 -0
  133. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/error.js +0 -0
  134. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/form_validator.js +0 -0
  135. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/internal_data/js/utils/url_text.js +0 -0
  136. {radicale-3.8.0 → radicale-3.8.2}/radicale/web/none.py +0 -0
  137. {radicale-3.8.0 → radicale-3.8.2}/radicale/xmlutils.py +0 -0
  138. {radicale-3.8.0 → radicale-3.8.2}/radicale.wsgi +0 -0
@@ -1,5 +1,37 @@
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
+
26
+ ## 3.8.1
27
+ * Add: tests: optional caldav-server-tester based CalDAV compatibility test suite (caldav_compat_tests)
28
+ * Fix: sharing/bday-conversion: add missing support for comma separated text components in vcard field N (name)
29
+ * Improvement: rights/from_file: extend detection of conflicting/unsupported permissions on startup
30
+ * Fix: sharing: fix logic of permit/deny permissions for map (mM) and token (tT)
31
+ * Add: support for /.mobileconfig for authenticated user
32
+ * Fix: WebUI sharing/propatch issue
33
+ * Fix: storage/sharing close unguarded os.scandir() iterators
34
+
3
35
  ## 3.8.0
4
36
  * Fix: storage/multifilesystem: depth:1 PROPFIND no longer re-runs the filesystem collision check (path_to_filesystem) for every item in a collection; this made listing large collections O(n^2) on file systems not detected as collision-free
5
37
  * Improve: storage/multifilesystem: avoid redundant stat() calls per item in get/upload when use_mtime_and_size_for_item_cache is enabled
@@ -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)
@@ -2450,7 +2479,8 @@ Default: `99`
2450
2479
  Radicale has been tested with:
2451
2480
 
2452
2481
  * [Android](https://android.com/) with
2453
- [DAVx⁵](https://www.davx5.com/) (formerly DAVdroid),
2482
+ [DAVx⁵](https://www.davx5.com/) (formerly DAVdroid) or
2483
+ [KashCal](https://f-droid.org/packages/org.onekash.kashcal),
2454
2484
  * [OneCalendar](https://www.onecalendar.nl/)
2455
2485
  * [GNOME Calendar](https://wiki.gnome.org/Apps/Calendar),
2456
2486
  [Contacts](https://wiki.gnome.org/Apps/Contacts) and
@@ -2676,6 +2706,8 @@ The following `permissions` are recognized:
2676
2706
  * **r:** read address book and calendar collections
2677
2707
  * **i:** subset of **r** that only allows direct access via HTTP method GET
2678
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)
2679
2711
  * **W:** write collections (excluding address books and calendars)
2680
2712
  * **w:** write address book and calendar collections
2681
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.0
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
@@ -54,6 +54,10 @@ Requires-Dist: mypy; extra == "dev"
54
54
  Requires-Dist: pytest; extra == "dev"
55
55
  Requires-Dist: pytest-playwright; extra == "dev"
56
56
  Requires-Dist: html5validator; extra == "dev"
57
+ Provides-Extra: caldav-test
58
+ Requires-Dist: pytest>=7; extra == "caldav-test"
59
+ Requires-Dist: caldav>=3.0; python_version >= "3.10" and extra == "caldav-test"
60
+ Requires-Dist: caldav-server-tester<2,>=1.2; python_version >= "3.10" and extra == "caldav-test"
57
61
  Dynamic: license-file
58
62
 
59
63
  # Radicale
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Radicale
3
- Version: 3.8.0
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
@@ -54,6 +54,10 @@ Requires-Dist: mypy; extra == "dev"
54
54
  Requires-Dist: pytest; extra == "dev"
55
55
  Requires-Dist: pytest-playwright; extra == "dev"
56
56
  Requires-Dist: html5validator; extra == "dev"
57
+ Provides-Extra: caldav-test
58
+ Requires-Dist: pytest>=7; extra == "caldav-test"
59
+ Requires-Dist: caldav>=3.0; python_version >= "3.10" and extra == "caldav-test"
60
+ Requires-Dist: caldav-server-tester<2,>=1.2; python_version >= "3.10" and extra == "caldav-test"
57
61
  Dynamic: license-file
58
62
 
59
63
  # Radicale
@@ -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
@@ -10,6 +10,13 @@ argon2-cffi
10
10
  [bcrypt]
11
11
  bcrypt
12
12
 
13
+ [caldav_test]
14
+ pytest>=7
15
+
16
+ [caldav_test:python_version >= "3.10"]
17
+ caldav>=3.0
18
+ caldav-server-tester<2,>=1.2
19
+
13
20
  [dev]
14
21
  flake8
15
22
  isort
@@ -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.0"
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"
@@ -46,6 +46,7 @@ argon2 = ["argon2-cffi"]
46
46
  ldap = ["ldap3"]
47
47
  pam = ["pam"]
48
48
  dev = ["flake8", "isort", "mypy", "pytest", "pytest-playwright", "html5validator"]
49
+ caldav_test = ["pytest>=7", "caldav>=3.0; python_version >= '3.10'", "caldav-server-tester>=1.2,<2; python_version >= '3.10'"]
49
50
 
50
51
  [project.scripts]
51
52
  radicale = "radicale.__main__:run"
@@ -56,9 +57,10 @@ build-backend = "setuptools.build_meta"
56
57
 
57
58
  [tool.tox]
58
59
  min_version = "4.0"
59
- envlist = ["flake8", "isort", "mypy", "py_loglevel_info", "py_radicale_loglevel_info", "py_radicale_loglevel_trace", "py", "integ_test", "html5validator"]
60
+ envlist = ["flake8", "isort", "mypy", "py_loglevel_info", "py_radicale_loglevel_info", "py_radicale_loglevel_trace", "py", "integ_test", "caldav_compat_test", "html5validator"]
60
61
 
61
62
  [tool.tox.env.py]
63
+ description = "Server: run 'pytest' (incl. coverage report)"
62
64
  extras = ["test"]
63
65
  deps = [
64
66
  "pytest",
@@ -67,6 +69,7 @@ deps = [
67
69
  commands = [["pytest", "-r", "s", "--cov", "--cov-report=term", "--cov-report=xml", "."]]
68
70
 
69
71
  [tool.tox.env.py_loglevel_info]
72
+ description = "Server: run 'pytest' with log level 'INFO'"
70
73
  extras = ["test"]
71
74
  deps = [
72
75
  "pytest"
@@ -74,6 +77,7 @@ deps = [
74
77
  commands = [["pytest", "-r", "s", "--log-level", "INFO", "."]]
75
78
 
76
79
  [tool.tox.env.py_radicale_loglevel_info]
80
+ description = "Server: run 'pytest' with radicale on log level 'INFO'"
77
81
  extras = ["test"]
78
82
  setenv = { PYTEST_RADICALE_LOGLEVEL = "info" }
79
83
  deps = [
@@ -82,6 +86,7 @@ deps = [
82
86
  commands = [["pytest", "-r", "s", "--log-level", "INFO", "."]]
83
87
 
84
88
  [tool.tox.env.py_radicale_loglevel_trace]
89
+ description = "Server: run 'pytest' with radicale on log level 'TRACE'"
85
90
  extras = ["test"]
86
91
  setenv = { PYTEST_RADICALE_LOGLEVEL = "trace" }
87
92
  deps = [
@@ -90,6 +95,7 @@ deps = [
90
95
  commands = [["pytest", "-r", "s", "--log-level", "5", "."]]
91
96
 
92
97
  [tool.tox.env.py_filesystem_vfat]
98
+ description = "Server: run 'pytest' with collection storage filesystem 'vfat' (on Linux)"
93
99
  allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ]
94
100
  extras = ["test"]
95
101
  deps = [
@@ -117,6 +123,7 @@ commands_post = [
117
123
  ]
118
124
 
119
125
  [tool.tox.env.py_filesystem_vfat_utf8]
126
+ description = "Server: run 'pytest' with collection storage filesystem 'vfat+utf8' (on Linux)"
120
127
  allowlist_externals = [ "sudo", "dd", "mkfs.vfat", "mkdir", "rm", "rmdir", "chmod" ]
121
128
  extras = ["test"]
122
129
  deps = [
@@ -144,6 +151,7 @@ commands_post = [
144
151
  ]
145
152
 
146
153
  [tool.tox.env.py_filesystem_hfsplus]
154
+ description = "Server: run 'pytest' with collection storage filesystem 'hfsplus' (on Linux)"
147
155
  # Fedora
148
156
  allowlist_externals = [ "sudo", "dd", "mkfs.hfsplus", "mkdir", "rm", "rmdir", "chmod" ]
149
157
  extras = ["test"]
@@ -172,6 +180,7 @@ commands_post = [
172
180
  ]
173
181
 
174
182
  [tool.tox.env.py_filesystem_ntfs]
183
+ description = "Server: run 'pytest' with collection storage filesystem 'ntfs' (on Linux)"
175
184
  allowlist_externals = [ "sudo", "dd", "mkfs.ntfs", "mkdir", "rm", "rmdir", "chmod" ]
176
185
  extras = ["test"]
177
186
  deps = [
@@ -199,34 +208,57 @@ commands_post = [
199
208
  ]
200
209
 
201
210
  [tool.tox.env.flake8]
211
+ description = "Server: test code style"
202
212
  deps = ["flake8==7.1.0"]
203
213
  commands = [["flake8", "."]]
204
214
  skip_install = true
205
215
 
206
216
  [tool.tox.env.isort]
217
+ description = "Server: test import sort order"
207
218
  deps = ["isort==5.13.2"]
208
219
  commands = [["isort", "--check", "--diff", "."]]
209
220
  skip_install = true
210
221
 
211
222
  [tool.tox.env.mypy]
223
+ description = "Server: test static types"
212
224
  deps = ["mypy==1.11.0"]
213
225
  commands = [["mypy", "--install-types", "--non-interactive", "."]]
214
226
  skip_install = true
215
227
 
216
228
  [tool.tox.env.integ_test]
229
+ description = "Server+WebUI: run 'playwright' integration tests (with 'chromium')"
217
230
  deps = ["pytest", "pytest-playwright"]
218
231
  # Store Playwright browsers inside .tox for better caching
219
232
  setenv = { PLAYWRIGHT_BROWSERS_PATH = "{toxworkdir}/.playwright" }
220
233
  commands_pre = [["playwright", "install", "chromium"]]
221
234
  commands = [["pytest", "integ_tests"]]
222
235
 
236
+ [tool.tox.env.caldav_compat_test]
237
+ description = "Server: run caldav compatibility tests"
238
+ extras = ["caldav_test"]
239
+ deps = [
240
+ "caldav",
241
+ "caldav_server_tester==1.2.0", # synchronize version with .github/workflows/test.yml#caldav-compat-test
242
+ "pytest"
243
+ ]
244
+ commands = [["pytest", "caldav_compat_tests"]]
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
+
223
254
  [tool.tox.env.html5validator]
255
+ description = "WebUI: validate HTML"
224
256
  deps = ["html5validator-2"]
225
257
  commands = [["html5validator", "--root", "radicale/web/internal_data/", "--also-check-css"]]
226
258
 
227
259
 
228
260
  [tool.pytest.ini_options]
229
- norecursedirs = ["integ_tests"]
261
+ norecursedirs = ["integ_tests", "caldav_compat_tests"]
230
262
 
231
263
 
232
264
  [tool.setuptools]
@@ -234,7 +266,7 @@ platforms = ["Any"]
234
266
  include-package-data = false
235
267
 
236
268
  [tool.setuptools.packages.find]
237
- exclude = ["*.tests", "integ_tests"] # *.tests.*; tests.*; tests
269
+ exclude = ["*.tests", "integ_tests", "caldav_compat_tests"] # *.tests.*; tests.*; tests
238
270
  namespaces = false
239
271
 
240
272
  [tool.setuptools.package-data]
@@ -266,7 +298,7 @@ exclude = "(^|/)build($|/)"
266
298
  [tool.coverage.run]
267
299
  branch = true
268
300
  source = ["radicale"]
269
- omit = ["tests/*", "*/tests/*", "integ_tests/*", "*/integ_tests/*"]
301
+ omit = ["tests/*", "*/tests/*", "integ_tests/*", "*/integ_tests/*", "caldav_compat_tests/*", "*/caldav_compat_tests/*"]
270
302
 
271
303
  [tool.coverage.report]
272
304
  # Regexes for lines to exclude from consideration
@@ -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():