Radicale 3.7.2__tar.gz → 3.7.3__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 (131) hide show
  1. {radicale-3.7.2 → radicale-3.7.3}/CHANGELOG.md +8 -0
  2. {radicale-3.7.2 → radicale-3.7.3}/DOCUMENTATION.md +88 -29
  3. {radicale-3.7.2 → radicale-3.7.3}/PKG-INFO +1 -1
  4. {radicale-3.7.2 → radicale-3.7.3}/Radicale.egg-info/PKG-INFO +1 -1
  5. {radicale-3.7.2 → radicale-3.7.3}/config +12 -0
  6. {radicale-3.7.2 → radicale-3.7.3}/pyproject.toml +1 -1
  7. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/__init__.py +57 -10
  8. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/base.py +43 -14
  9. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/delete.py +2 -2
  10. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/get.py +16 -6
  11. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/head.py +2 -2
  12. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/mkcalendar.py +3 -3
  13. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/mkcol.py +2 -2
  14. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/move.py +2 -2
  15. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/options.py +1 -1
  16. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/post.py +3 -3
  17. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/propfind.py +57 -22
  18. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/proppatch.py +7 -4
  19. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/put.py +4 -3
  20. {radicale-3.7.2 → radicale-3.7.3}/radicale/app/report.py +10 -6
  21. {radicale-3.7.2 → radicale-3.7.3}/radicale/config.py +79 -2
  22. {radicale-3.7.2 → radicale-3.7.3}/radicale/httputils.py +19 -6
  23. {radicale-3.7.2 → radicale-3.7.3}/radicale/log.py +103 -1
  24. {radicale-3.7.2 → radicale-3.7.3}/radicale/sharing/__init__.py +8 -3
  25. {radicale-3.7.2 → radicale-3.7.3}/radicale/tests/custom/web.py +4 -4
  26. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/__init__.py +8 -3
  27. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal.py +3 -2
  28. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/main.css +11 -0
  29. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/index.html +1 -0
  30. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/api/api.js +6 -8
  31. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/models/collection.js +3 -1
  32. radicale-3.7.3/radicale/web/internal_data/js/scenes/CollectionsScene.js +364 -0
  33. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/CreateEditShareScene.js +1 -5
  34. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/IncomingSharingScene.js +1 -1
  35. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/LoginScene.js +6 -0
  36. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/ShareCollectionScene.js +10 -14
  37. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/utils/collection_utils.js +1 -1
  38. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/utils/collections_cache.js +17 -10
  39. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/none.py +2 -2
  40. radicale-3.7.2/radicale/web/internal_data/js/scenes/CollectionsScene.js +0 -321
  41. {radicale-3.7.2 → radicale-3.7.3}/COPYING.md +0 -0
  42. {radicale-3.7.2 → radicale-3.7.3}/MANIFEST.in +0 -0
  43. {radicale-3.7.2 → radicale-3.7.3}/README.md +0 -0
  44. {radicale-3.7.2 → radicale-3.7.3}/Radicale.egg-info/SOURCES.txt +0 -0
  45. {radicale-3.7.2 → radicale-3.7.3}/Radicale.egg-info/dependency_links.txt +0 -0
  46. {radicale-3.7.2 → radicale-3.7.3}/Radicale.egg-info/entry_points.txt +0 -0
  47. {radicale-3.7.2 → radicale-3.7.3}/Radicale.egg-info/requires.txt +0 -0
  48. {radicale-3.7.2 → radicale-3.7.3}/Radicale.egg-info/top_level.txt +0 -0
  49. {radicale-3.7.2 → radicale-3.7.3}/radicale/__init__.py +0 -0
  50. {radicale-3.7.2 → radicale-3.7.3}/radicale/__main__.py +0 -0
  51. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/__init__.py +0 -0
  52. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/denyall.py +0 -0
  53. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/dovecot.py +0 -0
  54. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/htpasswd.py +0 -0
  55. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/http_remote_user.py +0 -0
  56. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/http_x_remote_user.py +0 -0
  57. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/imap.py +0 -0
  58. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/ldap.py +0 -0
  59. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/none.py +0 -0
  60. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/oauth2.py +0 -0
  61. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/pam.py +0 -0
  62. {radicale-3.7.2 → radicale-3.7.3}/radicale/auth/remote_user.py +0 -0
  63. {radicale-3.7.2 → radicale-3.7.3}/radicale/hook/__init__.py +0 -0
  64. {radicale-3.7.2 → radicale-3.7.3}/radicale/hook/email/__init__.py +0 -0
  65. {radicale-3.7.2 → radicale-3.7.3}/radicale/hook/none.py +0 -0
  66. {radicale-3.7.2 → radicale-3.7.3}/radicale/hook/rabbitmq/__init__.py +0 -0
  67. {radicale-3.7.2 → radicale-3.7.3}/radicale/item/__init__.py +0 -0
  68. {radicale-3.7.2 → radicale-3.7.3}/radicale/item/filter.py +0 -0
  69. {radicale-3.7.2 → radicale-3.7.3}/radicale/pathutils.py +0 -0
  70. {radicale-3.7.2 → radicale-3.7.3}/radicale/py.typed +0 -0
  71. {radicale-3.7.2 → radicale-3.7.3}/radicale/rights/__init__.py +0 -0
  72. {radicale-3.7.2 → radicale-3.7.3}/radicale/rights/authenticated.py +0 -0
  73. {radicale-3.7.2 → radicale-3.7.3}/radicale/rights/from_file.py +0 -0
  74. {radicale-3.7.2 → radicale-3.7.3}/radicale/rights/owner_only.py +0 -0
  75. {radicale-3.7.2 → radicale-3.7.3}/radicale/rights/owner_write.py +0 -0
  76. {radicale-3.7.2 → radicale-3.7.3}/radicale/server.py +0 -0
  77. {radicale-3.7.2 → radicale-3.7.3}/radicale/sharing/csv.py +0 -0
  78. {radicale-3.7.2 → radicale-3.7.3}/radicale/sharing/files.py +0 -0
  79. {radicale-3.7.2 → radicale-3.7.3}/radicale/sharing/none.py +0 -0
  80. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/__init__.py +0 -0
  81. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/__init__.py +0 -0
  82. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/base.py +0 -0
  83. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/cache.py +0 -0
  84. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/create_collection.py +0 -0
  85. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/delete.py +0 -0
  86. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/discover.py +0 -0
  87. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/get.py +0 -0
  88. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/history.py +0 -0
  89. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/lock.py +0 -0
  90. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/meta.py +0 -0
  91. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/move.py +0 -0
  92. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/sync.py +0 -0
  93. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/upload.py +0 -0
  94. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem/verify.py +0 -0
  95. {radicale-3.7.2 → radicale-3.7.3}/radicale/storage/multifilesystem_nolock.py +0 -0
  96. {radicale-3.7.2 → radicale-3.7.3}/radicale/tests/custom/__init__.py +0 -0
  97. {radicale-3.7.2 → radicale-3.7.3}/radicale/tests/custom/auth.py +0 -0
  98. {radicale-3.7.2 → radicale-3.7.3}/radicale/tests/custom/rights.py +0 -0
  99. {radicale-3.7.2 → radicale-3.7.3}/radicale/tests/custom/storage_simple_sync.py +0 -0
  100. {radicale-3.7.2 → radicale-3.7.3}/radicale/types.py +0 -0
  101. {radicale-3.7.2 → radicale-3.7.3}/radicale/utils.py +0 -0
  102. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icon.png +0 -0
  103. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/delete.svg +0 -0
  104. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/download.svg +0 -0
  105. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/edit.svg +0 -0
  106. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/eye.svg +0 -0
  107. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/key.svg +0 -0
  108. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/new.svg +0 -0
  109. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/repeat.svg +0 -0
  110. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/share.svg +0 -0
  111. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/icons/upload.svg +0 -0
  112. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/loading.svg +0 -0
  113. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/css/logo.svg +0 -0
  114. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/api/common.js +0 -0
  115. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/api/sharing.js +0 -0
  116. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/constants.js +0 -0
  117. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/main.js +0 -0
  118. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/CreateEditCollectionScene.js +0 -0
  119. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/DeleteConfirmationScene.js +0 -0
  120. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/LoadingScene.js +0 -0
  121. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/UploadCollectionScene.js +0 -0
  122. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/scenes/scene_manager.js +0 -0
  123. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/utils/error.js +0 -0
  124. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/utils/form_validator.js +0 -0
  125. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/utils/misc.js +0 -0
  126. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/utils/permissions.js +0 -0
  127. {radicale-3.7.2 → radicale-3.7.3}/radicale/web/internal_data/js/utils/url_text.js +0 -0
  128. {radicale-3.7.2 → radicale-3.7.3}/radicale/xmlutils.py +0 -0
  129. {radicale-3.7.2 → radicale-3.7.3}/radicale.wsgi +0 -0
  130. {radicale-3.7.2 → radicale-3.7.3}/rights +0 -0
  131. {radicale-3.7.2 → radicale-3.7.3}/setup.cfg +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.7.3
4
+ * Extension: expose RADICALE:version for authenticated users via PROPFIND
5
+ * Fix: sharing: GET request on single item with bday conversion
6
+ * Fix: sharing: PROPFIND response on single item with bday conversion
7
+ * Fix: sharing: PROPFIND response related to C:supported-calendar-component-set
8
+ * Feature: conditional log of request/response header/content by new options *_on_notice_condition
9
+ * Fix: user/path validation related to 'unicode-letter'
10
+
3
11
  ## 3.7.2
4
12
  * Fix: broken storage/mtime granularity detection on vfat
5
13
  * Improve: `path_to_filesystem()` by pre-detection of collision-free file system
@@ -927,6 +927,35 @@ Strip script name from URI if called by reverse proxy
927
927
 
928
928
  Default: (taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME)
929
929
 
930
+ ##### validate_user_value
931
+
932
+ _(>= 3.7.2)_
933
+
934
+ Validate user value content
935
+
936
+ Available types are:
937
+ * `none`
938
+ * `minimal` (control and some special chars)
939
+ * `unicode-letter` (unicode letters)
940
+ * `no-unicode` (no unicode)
941
+ * `strict` (reduced ASCII set)
942
+
943
+ Default: `minimal`
944
+
945
+ ##### validate_path_value
946
+
947
+ _(>= 3.7.2)_
948
+
949
+ Validate path value content
950
+
951
+ * `none`
952
+ * `minimal` (control and some special chars)
953
+ * `unicode-letter` (unicode letters)
954
+ * `no-unicode` (no unicode)
955
+ * `strict` (reduced ASCII set)
956
+
957
+ Default: `minimal`
958
+
930
959
  #### [encoding]
931
960
 
932
961
  ##### request
@@ -1617,35 +1646,6 @@ Strict preconditions check on PUT in case item already exists [RFC6352#9.2](http
1617
1646
 
1618
1647
  Default: `False`
1619
1648
 
1620
- ##### validate_user_value
1621
-
1622
- _(>= 3.7.2)_
1623
-
1624
- Validate user value content
1625
-
1626
- Available types are:
1627
- * `none`
1628
- * `minimal` (control and some special chars)
1629
- * `unicode-letter` (unicode letters)
1630
- * `no-unicode` (no unicode)
1631
- * `strict` (reduced ASCII set)
1632
-
1633
- Default: `minimum`
1634
-
1635
- ##### validate_path_type
1636
-
1637
- _(>= 3.7.2)_
1638
-
1639
- Validate path value content
1640
-
1641
- * `none`
1642
- * `minimal` (control and some special chars)
1643
- * `unicode-letter` (unicode letters)
1644
- * `no-unicode` (no unicode)
1645
- * `strict` (reduced ASCII set)
1646
-
1647
- Default: `minimum`
1648
-
1649
1649
  ##### hook
1650
1650
 
1651
1651
  Command that is run after changes to storage. See the
@@ -1803,6 +1803,65 @@ Log response content (body) on `level = debug`
1803
1803
 
1804
1804
  Default: `False`
1805
1805
 
1806
+ ##### request_header_on_notice_condition
1807
+
1808
+ _(>= 3.7.3)_
1809
+
1810
+ Log request header on `level = notice` if condition is fulfilled
1811
+
1812
+ Default: `{}`
1813
+
1814
+ Format: JSON structure as text
1815
+
1816
+ Supported tokens:
1817
+ * `method`
1818
+ * `path`
1819
+ * `useragent`
1820
+ * `host` (IPv4/IPv6 address/network)
1821
+ * `login`
1822
+ * `status` (only supported on responses)
1823
+
1824
+ Supported matches:
1825
+ * str: `startswith`, `endswith`, `equal`, `re`
1826
+ * int: `==`, `<`, `<=`, `>=`, `>`, `!=`
1827
+ * IP address: `equal`, `==`, `!=`
1828
+ * IP network: `included`, `excluded`
1829
+
1830
+ Examples:
1831
+ * `{"method": {"match": "equal", "value": "GET"}, "login": {"match": "equal", "value": "owner"}}`
1832
+ * `{"method": {"match": "equal", "value": "PUT"}, "status": {"match": ">=", "value": "201"}, "host": {"match": "included", "value": "127.0.0.0/8"}}`
1833
+ * `{"method": {"match": "equal", "value": "PUT"}, "path": {"match": "re", "value": "ev?nt[01]"}}`
1834
+
1835
+ ##### request_content_on_notice_condition
1836
+
1837
+ _(>= 3.7.3)_
1838
+
1839
+ Log request content (body) on `level = notice` if condition is fulfilled
1840
+
1841
+ Default: `{}`
1842
+
1843
+ Format: see `request_header_on_notice_condition`
1844
+
1845
+ ##### response_header_on_notice_condition
1846
+
1847
+ _(>= 3.7.3)_
1848
+
1849
+ Log response header on `level = notice` if condition is fulfilled
1850
+
1851
+ Default: `{}`
1852
+
1853
+ Format: see `request_header_on_notice_condition`
1854
+
1855
+ ##### response_content_on_notice_condition
1856
+
1857
+ _(>= 3.7.3)_
1858
+
1859
+ Log response content (body) on `level = notice` if condition is fulfilled
1860
+
1861
+ Default: `{}`
1862
+
1863
+ Format: see `request_header_on_notice_condition`
1864
+
1806
1865
  ##### rights_rule_doesnt_match_on_debug
1807
1866
 
1808
1867
  _(>= 3.2.3)_
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Radicale
3
- Version: 3.7.2
3
+ Version: 3.7.3
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.7.2
3
+ Version: 3.7.3
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
@@ -397,6 +397,18 @@
397
397
  # Log response content on level=debug
398
398
  #response_content_on_debug = False
399
399
 
400
+ # Log request header on level=notice with condition
401
+ #request_header_on_notice_condition = {}
402
+
403
+ # Log request content on level=notice with condition
404
+ #request_content_on_notice_condition = {}
405
+
406
+ # Log response header on level=notice with condition
407
+ #response_header_on_notice_condition = {}
408
+
409
+ # Log response content on level=notice with condition
410
+ #response_content_on_notice_condition = {}
411
+
400
412
  # Log rights rule which doesn't match on level=debug
401
413
  #rights_rule_doesnt_match_on_debug = False
402
414
 
@@ -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.7.2"
6
+ version = "3.7.3"
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"
@@ -130,6 +130,14 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
130
130
  logger.debug("log request content on debug: %s", self._request_content_on_debug)
131
131
  logger.debug("log response header on debug: %s", self._response_header_on_debug)
132
132
  logger.debug("log response content on debug: %s", self._response_content_on_debug)
133
+ self._request_header_on_notice_condition = configuration.get("logging", "request_header_on_notice_condition")
134
+ self._request_content_on_notice_condition = configuration.get("logging", "request_content_on_notice_condition")
135
+ self._response_header_on_notice_condition = configuration.get("logging", "response_header_on_notice_condition")
136
+ self._response_content_on_notice_condition = configuration.get("logging", "response_content_on_notice_condition")
137
+ logger.notice("log request header on notice condition: %s", self._request_header_on_notice_condition)
138
+ logger.notice("log request content on notice condition: %s", self._request_content_on_notice_condition)
139
+ logger.notice("log response header on notice condition: %s", self._response_header_on_notice_condition)
140
+ logger.notice("log response content on notice condition: %s", self._response_content_on_notice_condition)
133
141
  self._limit_content = configuration.get("logging", "limit_content")
134
142
  logger.debug("log limit for content: %d", self._limit_content)
135
143
  self._auth_delay = configuration.get("auth", "delay")
@@ -274,7 +282,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
274
282
  """Manage a request."""
275
283
  def response(status: int, headers: types.WSGIResponseHeaders,
276
284
  answer: Union[None, str, bytes],
277
- xml_request: Union[None, str] = None) -> _IntermediateResponse:
285
+ xml_request: Union[None, str] = None, request_info: dict = {}) -> _IntermediateResponse:
278
286
  """Helper to create response from internal types.WSGIResponse"""
279
287
  headers = dict(headers)
280
288
  content_encoding = "plain"
@@ -286,8 +294,16 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
286
294
  if logger.isEnabledFor(logging.DEBUG):
287
295
  logger.debug("Response content (nonXML):\n%s", utils.textwrap_str(answer, self._limit_content))
288
296
  else:
289
- if logger.isEnabledFor(logging.DEBUG):
290
- logger.debug("Response content: suppressed by config/option [logging] response_content_on_debug")
297
+ if self._response_content_on_notice_condition != {}:
298
+ if log.log_conditional(
299
+ "response-content",
300
+ condition=self._response_content_on_notice_condition,
301
+ value=request_info,
302
+ ):
303
+ logger.notice("Response content (nonXML, log condition passed):\n%s", utils.textwrap_str(answer, self._limit_content))
304
+ else:
305
+ if logger.isEnabledFor(logging.DEBUG):
306
+ logger.debug("Response content: suppressed by config/option [logging] response_content_on_debug")
291
307
  headers["Content-Type"] += "; charset=%s" % self._encoding
292
308
  answer = answer.encode(self._encoding)
293
309
  accept_encoding = [
@@ -307,12 +323,22 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
307
323
  # Add extra headers set in configuration
308
324
  headers.update(self._extra_headers)
309
325
 
326
+ request_info["status"] = status
327
+
310
328
  if self._response_header_on_debug:
311
329
  if logger.isEnabledFor(logging.DEBUG):
312
330
  logger.debug("Response header:\n%s", utils.textwrap_str(pprint.pformat(headers), self._limit_content))
313
331
  else:
314
- if logger.isEnabledFor(logging.DEBUG):
315
- logger.debug("Response header: suppressed by config/option [logging] response_header_on_debug")
332
+ if self._response_header_on_notice_condition != {}:
333
+ if log.log_conditional(
334
+ "response-header",
335
+ condition=self._response_header_on_notice_condition,
336
+ value=request_info,
337
+ ):
338
+ logger.notice("Response header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(headers), self._limit_content))
339
+ else:
340
+ if logger.isEnabledFor(logging.DEBUG):
341
+ logger.debug("Response header: suppressed by config/option [logging] response_header_on_debug")
316
342
 
317
343
  # Start response
318
344
  # delay on error
@@ -428,8 +454,10 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
428
454
  if environ.get("HTTP_X_FORWARDED_HOST") or environ.get("HTTP_X_FORWARDED_PROTO") or environ.get("HTTP_X_FORWARDED_SERVER"):
429
455
  reverse_proxy = True
430
456
  remote_useragent = ""
457
+ remote_useragent_txt = ""
431
458
  if environ.get("HTTP_USER_AGENT"):
432
- remote_useragent = " using %r" % environ["HTTP_USER_AGENT"]
459
+ remote_useragent = environ["HTTP_USER_AGENT"]
460
+ remote_useragent_txt = " using %r" % environ["HTTP_USER_AGENT"]
433
461
  depthinfo = ""
434
462
  if environ.get("HTTP_DEPTH"):
435
463
  depthinfo = " with depth %r" % environ["HTTP_DEPTH"]
@@ -439,12 +467,14 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
439
467
  https_info = ""
440
468
  logger.info("%s request for %r%s received from %s%s%s",
441
469
  request_method, unsafe_path, depthinfo,
442
- remote_host, remote_useragent, https_info)
470
+ remote_host, remote_useragent_txt, https_info)
443
471
  if self._request_header_on_debug:
444
472
  logger.debug("Request header:\n%s",
445
473
  utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
446
474
  else:
447
- logger.debug("Request header: suppressed by config/option [logging] request_header_on_debug")
475
+ if not self._request_header_on_notice_condition != {}:
476
+ # conditional request header logging is later
477
+ logger.debug("Request header: suppressed by config/option [logging] request_header_on_debug")
448
478
 
449
479
  # SCRIPT_NAME is already removed from PATH_INFO, according to the
450
480
  # WSGI specification.
@@ -527,6 +557,23 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
527
557
  self._rights._user_groups = self._auth._ldap_groups
528
558
  except AttributeError:
529
559
  pass
560
+
561
+ request_info: dict = {
562
+ "method": request_method,
563
+ "login": login, # not 'user' in this step
564
+ "path": path,
565
+ "useragent": remote_useragent,
566
+ "host": remote_host,
567
+ }
568
+
569
+ if not self._request_header_on_debug and self._request_header_on_notice_condition != {}:
570
+ if log.log_conditional(
571
+ "request-header",
572
+ condition=self._request_header_on_notice_condition,
573
+ value=request_info,
574
+ ):
575
+ logger.notice("Request header (log condition passed):\n%s", utils.textwrap_str(pprint.pformat(self._scrub_headers(environ)), self._limit_content))
576
+
530
577
  if user and login == user:
531
578
  logger.info("Successful login: %r (%s)", user, info)
532
579
  elif user:
@@ -611,7 +658,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
611
658
 
612
659
  try:
613
660
  status, headers, answer, xml_request = function(
614
- environ, base_prefix, path, user, remote_host, remote_useragent)
661
+ environ, base_prefix, path, user, request_info)
615
662
  except PermissionError as e:
616
663
  logger.error("PermissionError: %s", e)
617
664
  status, headers, answer, xml_request = httputils.INTERNAL_SERVER_ERROR
@@ -648,4 +695,4 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
648
695
  "WWW-Authenticate":
649
696
  "Basic realm=\"%s\"" % self._auth_realm})
650
697
 
651
- return response(status, headers, answer, xml_request)
698
+ return response(status, headers, answer, xml_request, request_info)
@@ -23,7 +23,7 @@ import unicodedata
23
23
  import xml.etree.ElementTree as ET
24
24
  from typing import Optional, Union
25
25
 
26
- from radicale import (auth, config, hook, httputils, pathutils, rights,
26
+ from radicale import (auth, config, hook, httputils, log, pathutils, rights,
27
27
  sharing, storage, types, utils, web, xmlutils)
28
28
  from radicale.log import logger
29
29
  from radicale.rights import intersect
@@ -41,8 +41,10 @@ PATH_PATTERN_STRICT_RE: str = "^[" + PATH_PATTERN_STRICT + "]+$"
41
41
  USER_BLACKLIST_MINIMAL: list = [":", "'", '"', '*', '?']
42
42
  PATH_BLACKLIST_MINIMAL: list = USER_BLACKLIST_MINIMAL
43
43
 
44
- USER_WHITELIST_UNICODE: list = ["-", ".", "@", "_"] # from USER_PATTERN_STRICT
45
- PATH_WHITELIST_UNICODE: list = ["-", ".", "@", "_", "/", "~"] # from PATH_PATTERN_STRICT
44
+ BASE_WHITELIST_UNICODE: list = ["-", ".", "@", "_", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0"]
45
+
46
+ USER_WHITELIST_UNICODE: list = BASE_WHITELIST_UNICODE # from USER_PATTERN_STRICT
47
+ PATH_WHITELIST_UNICODE: list = BASE_WHITELIST_UNICODE + ["/", "~"] # from PATH_PATTERN_STRICT
46
48
 
47
49
 
48
50
  def _check_format(self: storage.BaseStorage,
@@ -72,6 +74,7 @@ def _check_format(self: storage.BaseStorage,
72
74
  if check_unicode_letter:
73
75
  if c not in whitelist_unicode:
74
76
  if unicodedata.category(c)[0] != "L":
77
+ logger.trace("_check_format found %r (%r)", c, unicodedata.category(c)[0])
75
78
  return False
76
79
  if check_no_unicode:
77
80
  if ord(c) > 255:
@@ -137,12 +140,16 @@ class ApplicationBase:
137
140
  self._log_bad_put_request_content = configuration.get("logging", "bad_put_request_content")
138
141
  self._response_content_on_debug = configuration.get("logging", "response_content_on_debug")
139
142
  self._request_content_on_debug = configuration.get("logging", "request_content_on_debug")
143
+ self._response_content_on_notice_condition = configuration.get("logging", "response_content_on_notice_condition")
144
+ self._request_content_on_notice_condition = configuration.get("logging", "request_content_on_notice_condition")
140
145
  self._limit_content = configuration.get("logging", "limit_content")
141
146
  self._validate_user_value = configuration.get("server", "validate_user_value")
142
147
  self._validate_path_value = configuration.get("server", "validate_path_value")
143
148
  self._hook = hook.load(configuration)
144
149
 
145
- def _read_xml_request_body(self, environ: types.WSGIEnviron
150
+ def _read_xml_request_body(self,
151
+ environ: types.WSGIEnviron,
152
+ request_info: dict,
146
153
  ) -> Optional[ET.Element]:
147
154
  content = httputils.decode_request(
148
155
  self.configuration, environ,
@@ -154,19 +161,41 @@ class ApplicationBase:
154
161
  except ET.ParseError as e:
155
162
  logger.debug("Request content (Invalid XML):\n%s", content)
156
163
  raise RuntimeError("Failed to parse XML: %s" % e) from e
157
- if logger.isEnabledFor(logging.DEBUG):
158
- if self._request_content_on_debug:
159
- logger.debug("Request content (XML):\n%s",
160
- utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
164
+ if self._request_content_on_debug:
165
+ if logger.isEnabledFor(logging.DEBUG):
166
+ logger.debug("Request content (XML):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
167
+ else:
168
+ if self._request_content_on_notice_condition != {}:
169
+ if log.log_conditional(
170
+ "request-content",
171
+ condition=self._request_content_on_notice_condition,
172
+ value=request_info,
173
+ ):
174
+ logger.notice("Request content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
175
+ else:
176
+ if logger.isEnabledFor(logging.DEBUG):
177
+ logger.debug("Request content (XML, log condition skipped): suppressed")
161
178
  else:
162
- logger.debug("Request content (XML): suppressed by config/option [logging] request_content_on_debug")
179
+ if logger.isEnabledFor(logging.DEBUG):
180
+ logger.debug("Request content (XML): suppressed by config/option [logging] request_content_on_debug")
163
181
  return xml_content
164
182
 
165
- def _xml_response(self, xml_content: ET.Element) -> bytes:
166
- if logger.isEnabledFor(logging.DEBUG):
167
- if self._response_content_on_debug:
183
+ def _xml_response(self, xml_content: ET.Element, request_info: dict) -> bytes:
184
+ if self._response_content_on_debug:
185
+ if logger.isEnabledFor(logging.DEBUG):
168
186
  logger.debug("Response content (XML):\n%s",
169
187
  utils.textwrap_str(xmlutils.pretty_xml(xml_content), self._limit_content))
188
+ else:
189
+ if self._response_content_on_notice_condition != {}:
190
+ if log.log_conditional(
191
+ "response-content",
192
+ condition=self._response_content_on_notice_condition,
193
+ value=request_info,
194
+ ):
195
+ logger.notice("Response content (XML, log condition passed):\n%s", utils.textwrap_str(xmlutils.pretty_xml(xml_content)))
196
+ else:
197
+ if logger.isEnabledFor(logging.DEBUG):
198
+ logger.debug("Response content (XML, log condition skipped): suppressed")
170
199
  else:
171
200
  logger.debug("Response content (XML): suppressed by config/option [logging] response_content_on_debug")
172
201
  f = io.BytesIO()
@@ -174,11 +203,11 @@ class ApplicationBase:
174
203
  xml_declaration=True)
175
204
  return f.getvalue()
176
205
 
177
- def _webdav_error_response(self, status: int, human_tag: str
206
+ def _webdav_error_response(self, status: int, human_tag: str, request_info: dict
178
207
  ) -> types.WSGIResponse:
179
208
  """Generate XML error response."""
180
209
  headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
181
- content = self._xml_response(xmlutils.webdav_error(human_tag))
210
+ content = self._xml_response(xmlutils.webdav_error(human_tag), request_info)
182
211
  return status, headers, content, None
183
212
 
184
213
 
@@ -55,7 +55,7 @@ def xml_delete(base_prefix: str, path: str, collection: storage.BaseCollection,
55
55
  class ApplicationPartDelete(ApplicationBase):
56
56
 
57
57
  def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str,
58
- path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
58
+ path: str, user: str, request_info: dict) -> types.WSGIResponse:
59
59
  """Manage DELETE request."""
60
60
  actor = user
61
61
  permissions_filter = None
@@ -126,4 +126,4 @@ class ApplicationPartDelete(ApplicationBase):
126
126
  for notification_item in hook_notification_item_list: # Will be empty if hook not enabled
127
127
  self._hook.notify(notification_item)
128
128
  headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
129
- return client.OK, headers, self._xml_response(xml_answer), None
129
+ return client.OK, headers, self._xml_response(xml_answer, request_info), None
@@ -68,7 +68,7 @@ class ApplicationPartGet(ApplicationBase):
68
68
  return value
69
69
 
70
70
  def do_GET(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
71
- user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
71
+ user: str, request_info: dict) -> types.WSGIResponse:
72
72
  """Manage GET request."""
73
73
  # Redirect to /.web if the root path is requested
74
74
  if not pathutils.strip_path(path):
@@ -84,7 +84,7 @@ class ApplicationPartGet(ApplicationBase):
84
84
  base_prefix + unsafe_path, location)
85
85
  return httputils.redirect(location, client.MOVED_PERMANENTLY)
86
86
  # Dispatch /.web path to web module
87
- return self._web.get(environ, base_prefix, path, user)
87
+ return self._web.get(environ, base_prefix, path, user, request_info)
88
88
  permissions_filter = None
89
89
  share = None
90
90
  if self._sharing._enabled:
@@ -121,7 +121,10 @@ class ApplicationPartGet(ApplicationBase):
121
121
  elif limited_access:
122
122
  return httputils.NOT_ALLOWED
123
123
  else:
124
- content_type = xmlutils.OBJECT_MIMETYPES[item.name]
124
+ if share and share['Conversion'] == "bday":
125
+ content_type = xmlutils.MIMETYPES["VCALENDAR"]
126
+ else:
127
+ content_type = xmlutils.OBJECT_MIMETYPES[item.name]
125
128
  content_disposition = ""
126
129
  assert item.last_modified
127
130
  headers = {
@@ -130,9 +133,16 @@ class ApplicationPartGet(ApplicationBase):
130
133
  "ETag": item.etag}
131
134
  if content_disposition:
132
135
  headers["Content-Disposition"] = content_disposition
133
- if isinstance(item, storage.BaseCollection) and share and share['Conversion'] == "bday":
134
- # convert VCF to ICS
135
- answer = item.serialize(vcf_to_ics=True)
136
+ if share and share['Conversion'] == "bday":
137
+ if isinstance(item, storage.BaseCollection):
138
+ # convert VCF to ICS
139
+ answer = item.serialize(vcf_to_ics=True)
140
+ else:
141
+ item_converted = item.convert_vcf_to_ics()
142
+ if item_converted is not None:
143
+ answer = item_converted.serialize()
144
+ else:
145
+ return httputils.NOT_FOUND
136
146
  else:
137
147
  answer = item.serialize()
138
148
  return client.OK, headers, answer, None
@@ -26,7 +26,7 @@ from radicale.app.get import ApplicationPartGet
26
26
  class ApplicationPartHead(ApplicationPartGet, ApplicationBase):
27
27
 
28
28
  def do_HEAD(self, environ: types.WSGIEnviron, base_prefix: str, path: str,
29
- user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
29
+ user: str, request_info: dict) -> types.WSGIResponse:
30
30
  """Manage HEAD request."""
31
31
  # Body is dropped in `Application.__call__` for HEAD requests
32
- return self.do_GET(environ, base_prefix, path, user, remote_host, remote_useragent)
32
+ return self.do_GET(environ, base_prefix, path, user, request_info)
@@ -32,12 +32,12 @@ from radicale.log import logger
32
32
  class ApplicationPartMkcalendar(ApplicationBase):
33
33
 
34
34
  def do_MKCALENDAR(self, environ: types.WSGIEnviron, base_prefix: str,
35
- path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
35
+ path: str, user: str, request_info: dict) -> types.WSGIResponse:
36
36
  """Manage MKCALENDAR request."""
37
37
  if "w" not in self._rights.authorization(user, path):
38
38
  return httputils.NOT_ALLOWED
39
39
  try:
40
- xml_content = self._read_xml_request_body(environ)
40
+ xml_content = self._read_xml_request_body(environ, request_info)
41
41
  except RuntimeError as e:
42
42
  logger.warning(
43
43
  "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True)
@@ -67,7 +67,7 @@ class ApplicationPartMkcalendar(ApplicationBase):
67
67
  item = next(iter(self._storage.discover(path)), None)
68
68
  if item:
69
69
  return self._webdav_error_response(
70
- client.CONFLICT, "D:resource-must-be-null")
70
+ client.CONFLICT, "D:resource-must-be-null", request_info)
71
71
  parent_path = pathutils.parent_path(path)
72
72
  parent_item = next(iter(self._storage.discover(parent_path)), None)
73
73
  if not parent_item:
@@ -32,13 +32,13 @@ from radicale.log import logger
32
32
  class ApplicationPartMkcol(ApplicationBase):
33
33
 
34
34
  def do_MKCOL(self, environ: types.WSGIEnviron, base_prefix: str,
35
- path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
35
+ path: str, user: str, request_info: dict) -> types.WSGIResponse:
36
36
  """Manage MKCOL request."""
37
37
  permissions = self._rights.authorization(user, path)
38
38
  if not rights.intersect(permissions, "Ww"):
39
39
  return httputils.NOT_ALLOWED
40
40
  try:
41
- xml_content = self._read_xml_request_body(environ)
41
+ xml_content = self._read_xml_request_body(environ, request_info)
42
42
  except RuntimeError as e:
43
43
  logger.warning(
44
44
  "Bad MKCOL request on %r: %s", path, e, exc_info=True)
@@ -49,7 +49,7 @@ def get_server_netloc(environ: types.WSGIEnviron, force_port: bool = False):
49
49
  class ApplicationPartMove(ApplicationBase):
50
50
 
51
51
  def do_MOVE(self, environ: types.WSGIEnviron, base_prefix: str,
52
- path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
52
+ path: str, user: str, request_info: dict) -> types.WSGIResponse:
53
53
  """Manage MOVE request."""
54
54
  raw_dest = environ.get("HTTP_DESTINATION", "")
55
55
 
@@ -136,7 +136,7 @@ class ApplicationPartMove(ApplicationBase):
136
136
  to_collection.has_uid(item.uid)):
137
137
  return self._webdav_error_response(
138
138
  client.CONFLICT, "%s:no-uid-conflict" % (
139
- "C" if collection_tag == "VCALENDAR" else "CR"))
139
+ "C" if collection_tag == "VCALENDAR" else "CR"), request_info)
140
140
  to_href = posixpath.basename(pathutils.strip_path(to_path))
141
141
  try:
142
142
  self._storage.move(item, to_collection, to_href)
@@ -28,7 +28,7 @@ from radicale.app.base import ApplicationBase
28
28
  class ApplicationPartOptions(ApplicationBase):
29
29
 
30
30
  def do_OPTIONS(self, environ: types.WSGIEnviron, base_prefix: str,
31
- path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
31
+ path: str, user: str, request_info: dict) -> types.WSGIResponse:
32
32
  """Manage OPTIONS request."""
33
33
  headers = {
34
34
  "Allow": ", ".join(
@@ -26,10 +26,10 @@ from radicale.app.base import ApplicationBase
26
26
  class ApplicationPartPost(ApplicationBase):
27
27
 
28
28
  def do_POST(self, environ: types.WSGIEnviron, base_prefix: str,
29
- path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
29
+ path: str, user: str, request_info: dict) -> types.WSGIResponse:
30
30
  """Manage POST request."""
31
31
  if path == "/.web" or path.startswith("/.web/"):
32
- return self._web.post(environ, base_prefix, path, user)
32
+ return self._web.post(environ, base_prefix, path, user, request_info)
33
33
  elif path == "/.sharing" or path.startswith("/.sharing/"):
34
- return self._sharing.post(environ, base_prefix, path, user)
34
+ return self._sharing.post(environ, base_prefix, path, user, request_info)
35
35
  return httputils.METHOD_NOT_ALLOWED