rdiffweb 7.1.1__tar.gz → 7.2.0b2__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 (259) hide show
  1. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/CHANGELOG.md +1 -1
  2. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/PKG-INFO +5 -5
  3. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/README.md +3 -3
  4. rdiffweb-7.2.0b2/SECURITY.md +33 -0
  5. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/pyproject.toml +1 -1
  6. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwMessages.jinja +6 -4
  7. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwTable.css +9 -1
  8. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RepoCard.jinja +12 -15
  9. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_admin_repos.py +3 -1
  10. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_home.py +3 -2
  11. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_controller.py +5 -0
  12. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_history.py +32 -0
  13. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_login.py +1 -0
  14. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/librdiff.py +13 -3
  15. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_callbacks.py +2 -0
  16. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_message.py +85 -20
  17. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_repo.py +23 -0
  18. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_user.py +36 -14
  19. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/notification.py +13 -19
  20. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/admin_repos.html +15 -12
  21. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/admin_session.html +4 -3
  22. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/admin_users.html +6 -5
  23. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/graphs.html +0 -6
  24. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/home.html +2 -2
  25. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/include/email_kpi_cards.html +2 -2
  26. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/include/email_repo_card.html +5 -8
  27. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/login.html +2 -2
  28. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/main.css +2 -2
  29. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/stats.html +2 -1
  30. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/test.py +14 -11
  31. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb.egg-info/PKG-INFO +5 -5
  32. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb.egg-info/requires.txt +1 -1
  33. rdiffweb-7.2.0b2/rdiffweb.egg-info/scm_version.json +8 -0
  34. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/tox.ini +6 -6
  35. rdiffweb-7.1.1/SECURITY.md +0 -7
  36. rdiffweb-7.1.1/rdiffweb.egg-info/scm_version.json +0 -8
  37. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/FUNDING.yml +0 -0
  38. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/LICENSE +0 -0
  39. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/MANIFEST.in +0 -0
  40. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/babel.cfg +0 -0
  41. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/__init__.py +0 -0
  42. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/Breadcrumb.jinja +0 -0
  43. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/ButtonConfirm.jinja +0 -0
  44. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/Heatmap.css +0 -0
  45. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/Heatmap.jinja +0 -0
  46. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/HeatmapLegend.jinja +0 -0
  47. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/HeatmapYear.jinja +0 -0
  48. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/KpiCard.css +0 -0
  49. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/KpiCard.jinja +0 -0
  50. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/MfaCurrentBadgeWidget.jinja +0 -0
  51. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/ModalConfirm.jinja +0 -0
  52. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/ModalConfirm.js +0 -0
  53. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/ModalDialog.jinja +0 -0
  54. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/ModalDialog.js +0 -0
  55. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/NavItem.jinja +0 -0
  56. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/NavItemRepo.jinja +0 -0
  57. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwChart.jinja +0 -0
  58. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwChart.js +0 -0
  59. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwDateSelector.css +0 -0
  60. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwDateSelector.jinja +0 -0
  61. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwDateSelector.js +0 -0
  62. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwEmpty.jinja +0 -0
  63. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwFooter.jinja +0 -0
  64. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwIcon.css +0 -0
  65. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwIcon.jinja +0 -0
  66. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwLog.css +0 -0
  67. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwLog.jinja +0 -0
  68. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwModal.jinja +0 -0
  69. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwTable.jinja +0 -0
  70. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwTable.js +0 -0
  71. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RdwTime.jinja +0 -0
  72. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RepoStatus.jinja +0 -0
  73. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RepoWarning.css +0 -0
  74. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/RepoWarning.jinja +0 -0
  75. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/SelectMultipleWidget.css +0 -0
  76. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/SelectMultipleWidget.jinja +0 -0
  77. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/components/StorageUsage.jinja +0 -0
  78. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/__init__.py +0 -0
  79. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/api.py +0 -0
  80. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/api_currentuser.py +0 -0
  81. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/api_openapi.py +0 -0
  82. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/dispatch.py +0 -0
  83. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/filter_authorization.py +0 -0
  84. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/formdb.py +0 -0
  85. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_admin.py +0 -0
  86. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_admin_activity.py +0 -0
  87. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_admin_logs.py +0 -0
  88. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_admin_session.py +0 -0
  89. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_admin_sysinfo.py +0 -0
  90. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_admin_users.py +0 -0
  91. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_browse.py +0 -0
  92. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_delete.py +0 -0
  93. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_graphs.py +0 -0
  94. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_history.py +0 -0
  95. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_login.py +0 -0
  96. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_logs.py +0 -0
  97. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_mfa.py +0 -0
  98. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_pref_general.py +0 -0
  99. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_pref_mfa.py +0 -0
  100. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_pref_notification.py +0 -0
  101. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_pref_session.py +0 -0
  102. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_pref_sshkeys.py +0 -0
  103. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_pref_tokens.py +0 -0
  104. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_prefs.py +0 -0
  105. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_repo_activity.py +0 -0
  106. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_restore.py +0 -0
  107. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_settings.py +0 -0
  108. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/page_stats.py +0 -0
  109. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/static.py +0 -0
  110. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/__init__.py +0 -0
  111. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_api.py +0 -0
  112. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_check_links.py +0 -0
  113. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_openapi.py +0 -0
  114. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_admin.py +0 -0
  115. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_admin_activity.py +0 -0
  116. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_admin_logs.py +0 -0
  117. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_admin_repos.py +0 -0
  118. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_admin_session.py +0 -0
  119. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_admin_sysinfo.py +0 -0
  120. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_admin_users.py +0 -0
  121. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_browse.py +0 -0
  122. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_delete.py +0 -0
  123. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_error.py +0 -0
  124. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_graphs.py +0 -0
  125. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_home.py +0 -0
  126. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_logs.py +0 -0
  127. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_mfa.py +0 -0
  128. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_prefs_general.py +0 -0
  129. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_prefs_mfa.py +0 -0
  130. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_prefs_notification.py +0 -0
  131. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_prefs_session.py +0 -0
  132. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_prefs_ssh.py +0 -0
  133. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_prefs_tokens.py +0 -0
  134. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_repo_activity.py +0 -0
  135. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_restore.py +0 -0
  136. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_settings.py +0 -0
  137. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_page_stats.py +0 -0
  138. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/tests/test_secure_headers.py +0 -0
  139. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/controller/widgets.py +0 -0
  140. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/__init__.py +0 -0
  141. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/authorizedkeys.py +0 -0
  142. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/config.py +0 -0
  143. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/dircache.py +0 -0
  144. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/diskusage.py +0 -0
  145. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/__init__.py +0 -0
  146. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_diskusage.py +0 -0
  147. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_session.py +0 -0
  148. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_sshkey.py +0 -0
  149. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_timestamp.py +0 -0
  150. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_token.py +0 -0
  151. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/_update.py +0 -0
  152. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/tests/__init__.py +0 -0
  153. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/tests/test_db_after_create.py +0 -0
  154. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/tests/test_repo.py +0 -0
  155. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/tests/test_token.py +0 -0
  156. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/model/tests/test_user.py +0 -0
  157. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/quota.py +0 -0
  158. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/remove_older.py +0 -0
  159. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/restore.py +0 -0
  160. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/__init__.py +0 -0
  161. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/rdiff-backup-data/error_log.2015-11-19T07:27:46-05:00.data +0 -0
  162. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/rdiff-backup-data/error_log.2015-11-20T07:27:46-05:00.data.gz +0 -0
  163. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/rdiff-backup-data/error_log.2019-05-22T09:19:09-04:00.data.gz +0 -0
  164. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/rdiff-backup-data/file_statistics.2014-11-05T16:05:07-05:00.data +0 -0
  165. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/rdiff-backup-data/file_statistics.2014-11-05T16:05:07-05:00.data.gz +0 -0
  166. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/rdiff-backup-data/session_statistics.2014-11-02T09:16:43-05:00.data +0 -0
  167. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/session_statistics.2014-11-02T09:16:43-05:00.data +0 -0
  168. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_authorized_keys +0 -0
  169. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_authorizedkeys.py +0 -0
  170. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_config.py +0 -0
  171. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_diskusage.py +0 -0
  172. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_librdiff.py +0 -0
  173. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_login.py +0 -0
  174. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_notification.py +0 -0
  175. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_publickey_ssh_dsa.pub +0 -0
  176. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_publickey_ssh_rsa.pub +0 -0
  177. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_quota.py +0 -0
  178. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_remove_older.py +0 -0
  179. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/core/tests/test_restore.py +0 -0
  180. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/ca/LC_MESSAGES/messages.mo +0 -0
  181. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/ca/LC_MESSAGES/messages.po +0 -0
  182. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/de/LC_MESSAGES/messages.mo +0 -0
  183. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/de/LC_MESSAGES/messages.po +0 -0
  184. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/en/LC_MESSAGES/messages.mo +0 -0
  185. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/en/LC_MESSAGES/messages.po +0 -0
  186. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/es/LC_MESSAGES/messages.mo +0 -0
  187. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/es/LC_MESSAGES/messages.po +0 -0
  188. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/fr/LC_MESSAGES/messages.mo +0 -0
  189. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/fr/LC_MESSAGES/messages.po +0 -0
  190. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/messages.pot +0 -0
  191. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/pt/LC_MESSAGES/messages.mo +0 -0
  192. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/pt/LC_MESSAGES/messages.po +0 -0
  193. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/ru/LC_MESSAGES/messages.mo +0 -0
  194. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/locales/ru/LC_MESSAGES/messages.po +0 -0
  195. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/main.py +0 -0
  196. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/plugins/__init__.py +0 -0
  197. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/plugins/restapi.py +0 -0
  198. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/rdw_app.py +0 -0
  199. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/static/favicon.ico +0 -0
  200. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/static/favicon.svg +0 -0
  201. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/static/header-logo.png +0 -0
  202. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/static/header-logo.svg +0 -0
  203. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/static/login-bg.png +0 -0
  204. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/static/main.js +0 -0
  205. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/static/robots.txt +0 -0
  206. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/admin_activity.html +0 -0
  207. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/admin_logs.html +0 -0
  208. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/admin_sysinfo.html +0 -0
  209. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/admin_user_edit.html +0 -0
  210. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/browse.html +0 -0
  211. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_access_token_added.html +0 -0
  212. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_authorizedkey_added.html +0 -0
  213. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_changed.html +0 -0
  214. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_latest.html +0 -0
  215. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_layout.html +0 -0
  216. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_mfa.html +0 -0
  217. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_notification.html +0 -0
  218. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_password_changed.html +0 -0
  219. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_repo_added.html +0 -0
  220. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_repo_deleted.html +0 -0
  221. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_report.html +0 -0
  222. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_storage_usage.html +0 -0
  223. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/email_verification_code.html +0 -0
  224. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/error_page.html +0 -0
  225. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/history.html +0 -0
  226. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/include/email_storage_usage_card.html +0 -0
  227. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/include/session.html +0 -0
  228. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/include/timerange.html +0 -0
  229. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/layout.html +0 -0
  230. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/logs.html +0 -0
  231. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/mfa.html +0 -0
  232. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/prefs_general.html +0 -0
  233. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/prefs_mfa.html +0 -0
  234. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/prefs_notification.html +0 -0
  235. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/prefs_session.html +0 -0
  236. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/prefs_sshkeys.html +0 -0
  237. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/prefs_tokens.html +0 -0
  238. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/repo_activity.html +0 -0
  239. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/restore.html +0 -0
  240. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/settings.html +0 -0
  241. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/templates/sidebar.html +0 -0
  242. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tests/__init__.py +0 -0
  243. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tests/rdw.conf +0 -0
  244. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tests/rdw.db +0 -0
  245. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tests/test_app.py +0 -0
  246. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tests/test_main.py +0 -0
  247. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tests/testcases.tar.gz +0 -0
  248. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tools/__init__.py +0 -0
  249. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tools/enrich_session.py +0 -0
  250. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tools/errors.py +0 -0
  251. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tools/poppath.py +0 -0
  252. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb/tools/required_scope.py +0 -0
  253. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb.egg-info/SOURCES.txt +0 -0
  254. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb.egg-info/dependency_links.txt +0 -0
  255. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb.egg-info/entry_points.txt +0 -0
  256. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb.egg-info/scm_file_list.json +0 -0
  257. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdiffweb.egg-info/top_level.txt +0 -0
  258. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/rdw.conf +0 -0
  259. {rdiffweb-7.1.1 → rdiffweb-7.2.0b2}/setup.cfg +0 -0
@@ -445,7 +445,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
445
445
  ### Fixed
446
446
 
447
447
  - Fix exception raised for invalid locale identifier ([17c6d10](https://gitlab.com/ikus-soft/rdiffweb/commit/17c6d10a5a50d46218649c708a030e6f43d6620e) by Patrik Dufresne).
448
- - Fix token impersonation vulnerability ([4ce35fc](https://gitlab.com/ikus-soft/rdiffweb/commit/4ce35fc2d23ac1cc789186f32bf76603b2eddeb3) by Patrik Dufresne).
448
+ - Fix token impersonation vulnerability CVE-2025-67796 ([4ce35fc](https://gitlab.com/ikus-soft/rdiffweb/commit/4ce35fc2d23ac1cc789186f32bf76603b2eddeb3) by Patrik Dufresne).
449
449
  - Fix selenium execution with bookworm ([1fd8db3](https://gitlab.com/ikus-soft/rdiffweb/commit/1fd8db32b9eb738168f5b1b126f020652674be9d) by Patrik Dufresne).
450
450
  - Fix doc upload #337 ([6d7219b](https://gitlab.com/ikus-soft/rdiffweb/commit/6d7219bd4348ad28e1704a4d708111751151fddc) by Patrik Dufresne).
451
451
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdiffweb
3
- Version: 7.1.1
3
+ Version: 7.2.0b2
4
4
  Summary: A web interface to rdiff-backup repositories.
5
5
  Author-email: Patrik Dufresne <patrik@ikus-soft.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -691,7 +691,7 @@ Classifier: Framework :: CherryPy
691
691
  Requires-Python: <4,>=3.8
692
692
  Description-Content-Type: text/markdown
693
693
  License-File: LICENSE
694
- Requires-Dist: cherrypy-foundation<2,>=1.14.0
694
+ Requires-Dist: cherrypy-foundation<2,>=1.18.0
695
695
  Requires-Dist: cherrypy-foundation[ldap]
696
696
  Requires-Dist: cherrypy-foundation[oauth]
697
697
  Requires-Dist: cherrypy-foundation[passwd]
@@ -727,7 +727,7 @@ Dynamic: license-file
727
727
  <strong>
728
728
  <a href="https://www.rdiffweb.org">website</a>
729
729
  • <a href="https://www.ikus-soft.com/archive/rdiffweb/doc/latest/html/">docs</a>
730
- • <a href="https://groups.google.com/d/forum/rdiffweb">community</a>
730
+ • <a href="https://minarca.discourse.group/">community</a>
731
731
  • <a href="https://rdiffweb-demo.ikus-soft.com/">demo</a>
732
732
  </strong>
733
733
  </p>
@@ -823,11 +823,11 @@ By signing up for, you will receive regular updates and notifications about new
823
823
 
824
824
  [Rdiffweb Newsletter](https://rdiffweb.org/contactus).
825
825
 
826
- ## Google Group
826
+ ## Community
827
827
 
828
828
  Join our growing community to get answers to your technical questions.
829
829
 
830
- Rdiffweb users should use the [Rdiffweb Google Group](https://groups.google.com/forum/#!forum/rdiffweb).
830
+ Rdiffweb users should use the [Discourse Group](https://minarca.discourse.group/).
831
831
 
832
832
  ## Documentation
833
833
 
@@ -4,7 +4,7 @@
4
4
  <strong>
5
5
  <a href="https://www.rdiffweb.org">website</a>
6
6
  • <a href="https://www.ikus-soft.com/archive/rdiffweb/doc/latest/html/">docs</a>
7
- • <a href="https://groups.google.com/d/forum/rdiffweb">community</a>
7
+ • <a href="https://minarca.discourse.group/">community</a>
8
8
  • <a href="https://rdiffweb-demo.ikus-soft.com/">demo</a>
9
9
  </strong>
10
10
  </p>
@@ -100,11 +100,11 @@ By signing up for, you will receive regular updates and notifications about new
100
100
 
101
101
  [Rdiffweb Newsletter](https://rdiffweb.org/contactus).
102
102
 
103
- ## Google Group
103
+ ## Community
104
104
 
105
105
  Join our growing community to get answers to your technical questions.
106
106
 
107
- Rdiffweb users should use the [Rdiffweb Google Group](https://groups.google.com/forum/#!forum/rdiffweb).
107
+ Rdiffweb users should use the [Discourse Group](https://minarca.discourse.group/).
108
108
 
109
109
  ## Documentation
110
110
 
@@ -0,0 +1,33 @@
1
+ # Reporting a Vulnerability
2
+
3
+ If you discover a security vulnerability in Rdiffweb, please report it privately through GitLab:
4
+
5
+ 1. Open a **[New Confidential Issue](https://gitlab.com/ikus-soft/rdiffweb/-/issues/new?issue%5Bconfidential%5D=true)**.
6
+ 2. Make sure the **"This issue is confidential"** checkbox is checked.
7
+ 3. Include the affected version, description, and steps to reproduce (PoC).
8
+
9
+ Thank you for improving the security of Rdiffweb.
10
+
11
+ ## Non-qualifying reports
12
+
13
+ The following are generally **not** considered security vulnerabilities. Please do not report them as such;
14
+ open a regular (public) issue or merge request instead if you believe they are worth improving.
15
+
16
+ - **Content Security Policy (CSP) configuration**: Use of `unsafe-inline` or `unsafe-eval`, or any other
17
+ permissive or missing CSP directive, without a working exploit that does not depend on the CSP itself.
18
+ CSP is a defense-in-depth layer, not Rdiffweb's primary protection against XSS. Rdiffweb's web interface
19
+ currently relies on inline scripts and styles.
20
+ - **Weak TLS / SSL configuration**: TLS is expected to be terminated by the reverse proxy.
21
+ - **Clickjacking** without a demonstrated impact on a sensitive action.
22
+ - **Self-XSS**, or XSS requiring the victim to paste code into their own browser console.
23
+ - **XSS or content injection by administrators**: administrators are trusted by design.
24
+ - **Unsupported browsers**: issues that only affect outdated browsers or require relaxed browser security settings.
25
+ - **Vulnerabilities in third-party dependencies** without a demonstrated exploit path in Rdiffweb.
26
+ - **Findings from automated scanners** submitted without manual verification and a working proof of concept.
27
+ - **Misconfiguration of a deployment** (reverse proxy, OS, container) outside Rdiffweb's default configuration.
28
+ - **Code paths unreachable in a supported installation**: weaknesses that only occur when a declared
29
+ dependency is missing, removed, or deliberately bypassed (e.g. `--no-deps`), or in an environment that
30
+ Rdiffweb does not support. A report must demonstrate that the issue is reachable in a default,
31
+ supported installation.
32
+
33
+ Reports in these categories may be closed without a detailed response.
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Framework :: CherryPy"
22
22
  ]
23
23
  dependencies = [
24
- "cherrypy-foundation>=1.14.0,<2",
24
+ "cherrypy-foundation>=1.18.0,<2",
25
25
  "cherrypy-foundation[ldap]",
26
26
  "cherrypy-foundation[oauth]",
27
27
  "cherrypy-foundation[passwd]",
@@ -42,14 +42,15 @@
42
42
  {% trans %}Event{% endtrans %}
43
43
  </span>
44
44
  {% endset %}
45
- {% set reset_btn = {'text': _('Reset Filters'), 'extend': 'reset', 'className':'btn-secondary ms-3'} %}
45
+ {% set spacer_btn = {"extend": "spacer"} %}
46
+ {% set reset_btn = {'text': _('Reset Filters'), 'extend': 'reset', 'className':'btn-secondary'} %}
46
47
  {% set new_type = {'text': _('Creations'), 'extend': 'filter', 'column': 'type:name', 'search':'new', 'regex': False, 'className':'btn-outline-secondary'} %}
47
48
  {% set dirty_type = {'text': _('Modifications'), 'extend': 'filter', 'column': 'type:name', 'search':'dirty', 'className':'btn-outline-secondary'} %}
48
49
  {% set comment_type = {'text': _('Comments'), 'extend': 'filter', 'column': 'type:name', 'search':'comment', 'className':'btn-outline-secondary'} %}
49
50
  {% set event_type = {'text': _('Events'), 'extend': 'filter', 'column': 'type:name', 'search':'event', 'className':'btn-outline-secondary'} %}
50
51
  {% set delete_type = {'text': _('Deletions'), 'extend': 'filter', 'column': 'type:name', 'search':'deleted', 'className':'btn-outline-secondary'} %}
51
52
  {% if show in ['repo', 'user'] %}
52
- {% set buttons = [new_type, dirty_type, comment_type, event_type, delete_type, reset_btn] %}
53
+ {% set buttons = [new_type, dirty_type, comment_type, event_type, delete_type, spacer_btn, reset_btn] %}
53
54
  {% set columns = [
54
55
  {'name':'date', 'data':'date', 'title':_('Date'), 'render':'datetime'},
55
56
  {'name':'author', 'data':'author_username', 'title':_('Actor'), 'orderable':True, 'render':'actor'},
@@ -58,8 +59,9 @@
58
59
  ] %}
59
60
  {% else %}
60
61
  {% set buttons = [new_type, dirty_type, comment_type, event_type, delete_type,
61
- {'text': _('Users'), 'extend': 'filter', 'column': 'model_name:name', 'search':'user', 'className':'ms-3 btn-outline-primary'},
62
- {'text': _('Repositories'), 'extend': 'filter', 'column': 'model_name:name', 'search':'repo', 'className':'btn-outline-primary'},
62
+ {'text': _('Users'), 'extend': 'filter', 'column': 'model_name:name', 'search':'user', 'className':'ms-3 btn-outline-secondary'},
63
+ {'text': _('Repositories'), 'extend': 'filter', 'column': 'model_name:name', 'search':'repo', 'className':'btn-outline-secondary'},
64
+ spacer_btn,
63
65
  reset_btn
64
66
  ] %}
65
67
  {% set columns = [
@@ -15,9 +15,17 @@
15
15
  width: 100%;
16
16
  color: var(--bs-body-color) !important;
17
17
  background-color: var(--bs-body-bg) !important;
18
- min-width: 25vw;
19
18
  padding:0 !important;
20
19
  }
20
+ /* If the row is too long, make it wrap. */
21
+ .rdw-table .dt-layout-row {
22
+ flex-wrap: wrap;
23
+ }
24
+ /* Add margin arround buttons when content is wrapping. */
25
+ .rdw-table .dt-buttons .btn.btn-sm {
26
+ margin-top: 3px;
27
+ margin-bottom: 3px;
28
+ }
21
29
 
22
30
  /* Hide search label */
23
31
  .rdw-table .dt-search label {
@@ -28,7 +28,7 @@
28
28
  {# Backup count #}
29
29
  <div class="col-6">
30
30
  <div class="bg-light rounded-3 p-2">
31
- <div class="fw-bold text-truncate">{{ repo.session_statistics | length if repo.session_statistics else mdash }}</div>
31
+ <div class="fw-bold text-truncate">{{ repo.backup_count or mdash }}</div>
32
32
  <div class="text-muted" style="font-size:.72rem">{% trans %}Restore points{% endtrans %}</div>
33
33
  </div>
34
34
  </div>
@@ -39,25 +39,22 @@
39
39
  <RdwIcon value="bi-calendar-range" class="me-1" />
40
40
  {%- trans %}Oldest:{% endtrans %}
41
41
  <strong>
42
- <RdwTime :value="repo.session_statistics and repo.session_statistics[0].date" format="short" />
42
+ <RdwTime :value="repo.first_backup_date" format="short" />
43
43
  </strong>
44
44
  </span>
45
45
  {# Error count #}
46
46
  <span class="text-muted small">
47
- {% if repo.session_statistics %}
48
- {% set error_count = repo.session_statistics[-1].errors %}
49
- {% if error_count > 0 %}
50
- <a class="text-danger-emphasis fw-semibold"
51
- href="{{ url_for('logs', repo, date=repo.session_statistics[-1].date) }}">
52
- <RdwIcon value="bi-exclamation-circle" class="me-1" />
53
- {{- _('%(count)s errors', count=error_count) }}
54
- </a>
55
- {% else %}
56
- <RdwIcon value="bi-check-circle" class="text-success" class="me-1" />
57
- {{- _('%(count)s errors', count=error_count) }}
58
- {% endif %}
59
- {% else %}
47
+ {% if repo.last_error_count is none %}
60
48
  {{ mdash }}
49
+ {% elif repo.last_error_date %}
50
+ <a class="text-danger-emphasis fw-semibold"
51
+ href="{{ url_for('logs', repo, date=repo.last_error_date) }}">
52
+ <RdwIcon value="bi-exclamation-circle" class="me-1" />
53
+ {{- _('%(count)s errors', count=repo.last_error_count) }}
54
+ </a>
55
+ {% else %}
56
+ <RdwIcon value="bi-check-circle" class="text-success" class="me-1" />
57
+ {{- _('%(count)s errors', count=repo.last_error_count) }}
61
58
  {% endif %}
62
59
  </span>
63
60
  </div>
@@ -27,6 +27,8 @@ class AdminReposPage:
27
27
  """
28
28
  Show all user repositories
29
29
  """
30
+ repo_objs = RepoObject.query.all()
31
+ RepoObject.attach_statistics_info(repo_objs)
30
32
  return {
31
- "repos": RepoObject.query.all(),
33
+ "repos": repo_objs,
32
34
  }
@@ -23,7 +23,7 @@ from cherrypy_foundation.tools.i18n import gettext_lazy as _
23
23
  from cherrypy_foundation.url import url_for
24
24
 
25
25
  from rdiffweb.core.librdiff import RdiffTime
26
- from rdiffweb.core.model import DiskUsage, UserObject
26
+ from rdiffweb.core.model import DiskUsage, RepoObject, UserObject
27
27
 
28
28
  # Define the logger
29
29
  logger = logging.getLogger(__name__)
@@ -68,6 +68,7 @@ class HomePage:
68
68
 
69
69
  # Add disk usage to repos.
70
70
  repo_objs = DiskUsage.attach_disk_usage(repo_objs)
71
+ repo_objs = RepoObject.attach_statistics_info(repo_objs)
71
72
 
72
73
  activity_end = RdiffTime()
73
74
  activity_start = RdiffTime() - timedelta(days=30)
@@ -82,7 +83,7 @@ class HomePage:
82
83
  "total_interrupted": status_counts["interrupted"],
83
84
  "total_in_progress": status_counts["in_progress"],
84
85
  # Errors
85
- "error_count": sum(r.session_statistics[-1].errors for r in repo_objs if r.last_backup_date),
86
+ "total_error_count": sum(r.last_error_count for r in repo_objs if r.last_error_count),
86
87
  # last_backup
87
88
  "last_backup_date": last_backup.last_backup_date if last_backup else None,
88
89
  "last_backup_repo": last_backup.display_name if last_backup else None,
@@ -86,6 +86,11 @@ class ControllerTest(rdiffweb.test.WebCase):
86
86
  self.getPage('/static//../../test.js')
87
87
  self.assertStatus(403)
88
88
 
89
+ def test_dt_language(self):
90
+ for locale in ['en', 'fr', 'fr_CA']:
91
+ self.getPage(f'/static/dt-language/{locale}')
92
+ self.assertStatus(200)
93
+
89
94
 
90
95
  class ControllerExternalUrlTest(rdiffweb.test.WebCase):
91
96
  default_config = {'external_url': 'https://www.example.com'}
@@ -15,6 +15,7 @@
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
17
  import os
18
+ from pathlib import Path
18
19
 
19
20
  import cherrypy
20
21
  from selenium.webdriver.common.action_chains import ActionChains
@@ -35,6 +36,37 @@ class HistoryPageTest(rdiffweb.test.WebCase):
35
36
  self.getPage(url)
36
37
  self.assertStatus('200 OK')
37
38
 
39
+ def test_history_path_traversal(self):
40
+ # Given a user with single repo
41
+ user = UserObject.get_user(self.USERNAME)
42
+ (Path(self.testcases) / 'broker-repo').rename(Path(self.testcases) / 'testcases-secret')
43
+ user.refresh_repos(delete=True)
44
+ user.commit()
45
+ self.assertEqual(['testcases', 'testcases-secret'], [r.name for r in user.repo_objs])
46
+ # When browsing /history/user/test/../testing
47
+ self.getPage('/history/admin/testcases/%2e%2e/testcases-secret')
48
+ # Then an error should be raised.
49
+ self.assertStatus(403)
50
+
51
+ def test_history_with_single_repo(self):
52
+ """
53
+ Verify if browsing '/history/' for a single repository is working.
54
+ """
55
+ # Change the user setting to match single repo.
56
+ user = UserObject.get_user(self.USERNAME)
57
+ user.user_root = os.path.join(self.testcases, 'testcases')
58
+ user.refresh_repos(delete=True)
59
+ user.commit()
60
+ self.assertEqual([''], [r.name for r in user.repo_objs])
61
+ # Check if browsing is working.
62
+ self.getPage('/history/admin')
63
+ self.assertStatus('200 OK')
64
+ self.assertInBody('Files')
65
+ # Check sub directory browsing
66
+ self.getPage('/history/admin/Revisions/')
67
+ self.assertStatus('200 OK')
68
+ self.assertInBody('Files')
69
+
38
70
  def test_history_with_root(self):
39
71
  self._history(self.USERNAME, self.REPO, limit=10)
40
72
  # Check revisions
@@ -90,6 +90,7 @@ class LoginPageTest(rdiffweb.test.WebCase):
90
90
  cherrypy.scheduler.wait_for_jobs()
91
91
  user = UserObject.get_user(self.USERNAME)
92
92
  self.assertEqual("User login to web application", user.messages[-1].body)
93
+ self.assertEqual(userobj.id, user.messages[-1].author_id)
93
94
 
94
95
  def test_login_case_insensitive(self):
95
96
  # When authenticating with valid credentials with all uppercase username
@@ -53,6 +53,16 @@ STDOUT_ENCODING = 'utf-8'
53
53
  LANG = "en_US." + STDOUT_ENCODING
54
54
 
55
55
 
56
+ def _is_child_of(child, parent):
57
+ """
58
+ Return True if the child is relative to parent path or False.
59
+ This method is string-based.
60
+ """
61
+ child = os.path.normpath(child)
62
+ parent = os.path.normpath(parent)
63
+ return child == parent or child.startswith(parent.rstrip(b'/') + b'/')
64
+
65
+
56
66
  def rdiff_backup_version():
57
67
  """
58
68
  Get rdiff-backup version
@@ -996,9 +1006,9 @@ class RdiffRepo(object):
996
1006
  """Return a new instance of DirEntry to represent the given path."""
997
1007
  # Compute increment directory location.
998
1008
  assert isinstance(path, bytes)
999
- full_path = os.path.normpath(os.path.join(self.full_path, path.strip(b'/')))
1000
- increment_path = os.path.normpath(os.path.join(self._increment_path, path.strip(b'/'), b'..'))
1001
- if not full_path.startswith(self.full_path) or not increment_path.startswith(self.full_path):
1009
+ full_path = os.path.join(self.full_path, path.strip(b'/'))
1010
+ increment_path = os.path.dirname(os.path.join(self._increment_path, path.strip(b'/')))
1011
+ if not _is_child_of(full_path, self.full_path) or not _is_child_of(increment_path, self.full_path):
1002
1012
  raise AccessDeniedError('%s make reference outside the repository' % self._decode(path))
1003
1013
  relative_path = os.path.relpath(full_path, self.full_path)
1004
1014
  if relative_path.startswith(RDIFF_BACKUP_DATA):
@@ -19,6 +19,8 @@ from sqlalchemy.orm import Session
19
19
 
20
20
 
21
21
  def add_post_commit_tasks(session, *args):
22
+ if getattr(session, '_trans_context_manager', False):
23
+ raise RuntimeError('cannot use add_post_commit_tasks with sql context manager')
22
24
  # Add task
23
25
  session.info.setdefault("_after_commit_tasks", []).append(args)
24
26
 
@@ -14,9 +14,12 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
+ import contextvars
17
18
  import itertools
18
19
  import json
19
20
  import logging
21
+ from collections import namedtuple
22
+ from contextlib import contextmanager
20
23
 
21
24
  import cherrypy
22
25
  import cherrypy_foundation.plugins.db # noqa
@@ -64,6 +67,35 @@ class JSONString(TypeDecorator):
64
67
  return None
65
68
 
66
69
 
70
+ class _MessageContext(
71
+ namedtuple('_MessageContext', ['author', 'author_id', 'author_username', 'ip_address', 'user_agent'])
72
+ ):
73
+
74
+ def __new__(cls, author=None, author_id=None, author_username='', ip_address='', user_agent=''):
75
+ # Derive id/username from the author when one is provided.
76
+ if author is not None:
77
+ author_id = author.id
78
+ author_username = author.username
79
+ return super().__new__(cls, author, author_id, author_username, ip_address, user_agent)
80
+
81
+ def _replace(self, **kwds):
82
+ if 'author' in kwds and kwds['author'] is not None:
83
+ kwds.setdefault('author_id', kwds['author'].id)
84
+ kwds.setdefault('author_username', kwds['author'].username)
85
+ return super()._replace(**kwds)
86
+
87
+ @contextmanager
88
+ def use(self):
89
+ ctx = (
90
+ self._replace(author=None) if self.author and inspect(self.author).session != cherrypy.db.session else self
91
+ )
92
+ token = Message._context_var.set(ctx)
93
+ try:
94
+ yield self
95
+ finally:
96
+ Message._context_var.reset(token)
97
+
98
+
67
99
  def get_model_changes(model):
68
100
  """
69
101
  Return a dictionary containing changes made to the model since it was
@@ -142,12 +174,14 @@ class Message(Base):
142
174
  changes = Column('changes', JSONString, nullable=True)
143
175
  date = Column(Timestamp(timezone=True), default=func.now())
144
176
 
177
+ _context_var = contextvars.ContextVar('message_context', default=None)
178
+
145
179
  def __init__(self, **kwargs):
146
180
  """
147
181
  Called only when creating a NEW instance, not when loading from DB.
148
182
  """
149
183
  super().__init__(**kwargs)
150
- self._initialize_from_session()
184
+ self._initialize_from_context()
151
185
 
152
186
  @property
153
187
  def model_object(self):
@@ -164,26 +198,57 @@ class Message(Base):
164
198
  self.author_username = value.username
165
199
  return value
166
200
 
167
- def _initialize_from_session(self):
168
- """Populate fields from the current CherryPy session/request."""
169
- request = cherrypy.request
170
- try:
171
- if not self.author:
172
- self.author = request.currentuser
173
- except AttributeError:
174
- pass # No active request/user
175
-
176
- try:
177
- if not self.ip_address:
178
- self.ip_address = request.remote.ip or ''
179
- except AttributeError:
180
- pass
201
+ def _initialize_from_context(self):
202
+ """
203
+ Populate fields from the current execution context: either the live
204
+ CherryPy request (the common case), or a propagated ExecutionContext
205
+ installed via `copy_context()` in a background thread.
206
+ """
207
+ ctx = Message._current_context()
208
+ if not self.author and ctx.author is not None:
209
+ self.author = ctx.author
210
+ if not self.author_id and ctx.author_id:
211
+ self.author_id = ctx.author_id
212
+ if not self.author_username and ctx.author_username:
213
+ self.author_username = ctx.author_username
214
+ if not self.ip_address and ctx.ip_address:
215
+ self.ip_address = ctx.ip_address
216
+ if not self.user_agent and ctx.user_agent:
217
+ self.user_agent = ctx.user_agent
218
+
219
+ @staticmethod
220
+ def capture_context():
221
+ """
222
+ Capture author/ip/user-agent from the current CherryPy request.
223
+ Call this from the HTTP thread before scheduling background work.
224
+ Returns a context-manager-capable object; use it with `.use()`.
225
+ """
226
+ request = getattr(cherrypy, 'request', None)
227
+ author = getattr(request, 'currentuser', None) if request else None
228
+ author_id = getattr(author, 'id', None) if author else None
229
+ author_username = getattr(author, 'username', None) if author else None
230
+ ip_address = ''
231
+ user_agent = ''
232
+ if request is not None:
233
+ try:
234
+ ip_address = request.remote.ip or ''
235
+ except AttributeError:
236
+ pass
237
+ try:
238
+ user_agent = request.headers.get('User-Agent', '')
239
+ except AttributeError:
240
+ pass
241
+ return _MessageContext(
242
+ author=author,
243
+ author_id=author_id,
244
+ author_username=author_username,
245
+ ip_address=ip_address,
246
+ user_agent=user_agent,
247
+ )
181
248
 
182
- try:
183
- if not self.user_agent:
184
- self.user_agent = request.headers.get('User-Agent', '')
185
- except AttributeError:
186
- pass
249
+ @classmethod
250
+ def _current_context(cls):
251
+ return cls._context_var.get() or cls.capture_context()
187
252
 
188
253
 
189
254
  class MessageMixin:
@@ -192,6 +192,29 @@ class RepoObject(MessageMixin, Base, RdiffRepo):
192
192
  except ValueError:
193
193
  raise DoesNotExistError(path)
194
194
 
195
+ @classmethod
196
+ def attach_statistics_info(cls, repo_objs):
197
+ for repo in repo_objs:
198
+ try:
199
+ stats = repo.session_statistics
200
+ except Exception:
201
+ cherrypy.log('unexpected error trying to get session_statistics', traceback=True)
202
+ stats = None
203
+
204
+ if stats:
205
+ repo.last_error_count = stats[-1].errors
206
+ repo.last_error_date = stats[-1].date
207
+ repo.last_filesize = stats[-1].sourcefilesize
208
+ repo.first_backup_date = stats[0].date
209
+ repo.backup_count = len(stats)
210
+ else:
211
+ repo.last_error_count = None
212
+ repo.last_error_date = None
213
+ repo.last_filesize = None
214
+ repo.first_backup_date = None
215
+ repo.backup_count = 0
216
+ return repo_objs
217
+
195
218
  def __init__(self, *args, **kwargs):
196
219
  Base.__init__(self, *args, **kwargs)
197
220
  self.__init_on_load__()
@@ -21,6 +21,7 @@ import sys
21
21
 
22
22
  import cherrypy
23
23
  from cherrypy_foundation.passwd import check_password, hash_password
24
+ from cherrypy_foundation.tools.i18n import gettext
24
25
  from cherrypy_foundation.tools.i18n import gettext_lazy as _
25
26
  from sqlalchemy import (
26
27
  Boolean,
@@ -42,7 +43,7 @@ from sqlalchemy.ext.hybrid import hybrid_property
42
43
  from sqlalchemy.orm import Session, relationship, validates
43
44
 
44
45
  from ._callbacks import add_post_commit_tasks
45
- from ._message import AUDIT_IGNORE, MessageMixin, get_model_changes
46
+ from ._message import AUDIT_IGNORE, Message, MessageMixin, get_model_changes
46
47
  from ._repo import RepoObject
47
48
  from ._session import SessionObject
48
49
  from ._sshkey import SshKey
@@ -81,22 +82,38 @@ Base = cherrypy.db.base
81
82
  SEP = b'/'
82
83
 
83
84
 
84
- def delete_user_with_data(userid):
85
+ def delete_user_with_data(userid, ctx):
85
86
  """
86
87
  Job to delete user with all data.
87
88
  """
88
- cherrypy.db.clear_sessions()
89
- with cherrypy.db.session.begin():
90
- # Let start by deleting all repositories from disk.
89
+ try:
91
90
  userobj = UserObject.get_user(userid)
92
91
  username = userobj.username
93
- logger.info('deleting user [%s] with data', username)
94
- for repoobj in userobj.repo_objs:
95
- logger.info('deleting repository [%s]', repoobj.display_name)
96
- repoobj.delete_repo()
97
- # Finish by deleting the user it self.
98
- userobj.delete()
99
- logger.info('user [%s] deleted', username)
92
+ except Exception:
93
+ logger.warning("deleting user id [%s] failed", userid, exc_info=True)
94
+ UserObject.session.rollback()
95
+ raise
96
+
97
+ try:
98
+ with ctx.use():
99
+ # Let start by deleting all repositories from disk.
100
+ logger.info('deleting user [%s] with data', username)
101
+ for repoobj in userobj.repo_objs:
102
+ logger.info('deleting repository [%s]', repoobj.display_name)
103
+ repoobj.delete_repo()
104
+ repoobj.commit()
105
+ # Finish by deleting the user it self.
106
+ userobj.delete()
107
+ userobj.commit()
108
+ logger.info('user [%s] deleted', username)
109
+ except Exception:
110
+ logger.warning("deleting user [%s] failed", username, exc_info=True)
111
+ UserObject.session.rollback()
112
+ userobj.add_message(Message(body=gettext("Deletion of user failed"), type=Message.TYPE_EVENT))
113
+ userobj.commit()
114
+ raise
115
+ finally:
116
+ UserObject.session.rollback()
100
117
 
101
118
 
102
119
  class UserObject(MessageMixin, Base):
@@ -362,11 +379,16 @@ class UserObject(MessageMixin, Base):
362
379
  raise ValueError(_("can't delete admin user"))
363
380
  if delete_data:
364
381
  self.status = UserObject.STATUS_DELETING
382
+ self.add_message(Message(body=gettext("Deletion of user has been scheduled."), type=Message.TYPE_EVENT))
365
383
  for repoobj in self.repo_objs:
384
+ repoobj.add_message(
385
+ Message(body=gettext("Deletion of repository has been scheduled."), type=Message.TYPE_EVENT)
386
+ )
366
387
  repoobj._status = RepoObject.STATUS_DELETING
367
- add_post_commit_tasks(self.session, 'scheduler:add_job_now', delete_user_with_data, self.id)
388
+ ctx = Message.capture_context()
389
+ add_post_commit_tasks(self.session, 'scheduler:add_job_now', delete_user_with_data, self.id, ctx)
368
390
  else:
369
- # Otherwise, let delete use directly.
391
+ # Otherwise, let delete user directly.
370
392
  return Base.delete(self)
371
393
 
372
394
  def delete(self):