cherrypy-foundation 1.0.0__py3-none-any.whl → 1.0.0a1__py3-none-any.whl
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.
- cherrypy_foundation/components/ColorModes.jinja +4 -5
- cherrypy_foundation/components/Datatable.jinja +2 -2
- cherrypy_foundation/components/Datatable.js +2 -2
- cherrypy_foundation/components/Field.jinja +6 -16
- cherrypy_foundation/components/Fields.jinja +2 -0
- cherrypy_foundation/components/Typeahead.css +1 -6
- cherrypy_foundation/components/Typeahead.jinja +2 -2
- cherrypy_foundation/components/__init__.py +2 -2
- cherrypy_foundation/components/tests/test_static.py +1 -1
- cherrypy_foundation/error_page.py +17 -20
- cherrypy_foundation/flash.py +15 -17
- cherrypy_foundation/form.py +2 -2
- cherrypy_foundation/logging.py +2 -2
- cherrypy_foundation/passwd.py +2 -2
- cherrypy_foundation/plugins/db.py +9 -35
- cherrypy_foundation/plugins/ldap.py +38 -46
- cherrypy_foundation/plugins/restapi.py +1 -1
- cherrypy_foundation/plugins/scheduler.py +84 -208
- cherrypy_foundation/plugins/smtp.py +46 -78
- cherrypy_foundation/plugins/tests/test_db.py +4 -4
- cherrypy_foundation/plugins/tests/test_ldap.py +3 -76
- cherrypy_foundation/plugins/tests/test_scheduler.py +50 -58
- cherrypy_foundation/plugins/tests/test_smtp.py +7 -40
- cherrypy_foundation/tests/__init__.py +0 -72
- cherrypy_foundation/tests/test_error_page.py +1 -7
- cherrypy_foundation/tests/test_passwd.py +2 -2
- cherrypy_foundation/tools/auth.py +38 -59
- cherrypy_foundation/tools/auth_mfa.py +88 -89
- cherrypy_foundation/tools/errors.py +27 -0
- cherrypy_foundation/tools/i18n.py +153 -246
- cherrypy_foundation/tools/jinja2.py +13 -29
- cherrypy_foundation/tools/ratelimit.py +27 -37
- cherrypy_foundation/tools/secure_headers.py +5 -1
- cherrypy_foundation/tools/sessions_timeout.py +21 -23
- cherrypy_foundation/tools/tests/locales/en/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/locales/{de → en}/LC_MESSAGES/messages.po +2 -2
- cherrypy_foundation/tools/tests/test_auth.py +4 -21
- cherrypy_foundation/tools/tests/test_i18n.py +6 -81
- cherrypy_foundation/tools/tests/test_ratelimit.py +2 -2
- cherrypy_foundation/url.py +25 -25
- cherrypy_foundation/widgets.py +2 -2
- cherrypy_foundation-1.0.0a1.dist-info/METADATA +42 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/RECORD +46 -65
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/WHEEL +1 -1
- cherrypy_foundation/components/Flash.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.jinja +0 -13
- cherrypy_foundation/components/LocaleSelection.js +0 -26
- cherrypy_foundation/plugins/tests/test_scheduler_db.py +0 -107
- cherrypy_foundation/sessions.py +0 -93
- cherrypy_foundation/tests/templates/test_flash.html +0 -9
- cherrypy_foundation/tests/templates/test_form.html +0 -16
- cherrypy_foundation/tests/templates/test_url.html +0 -15
- cherrypy_foundation/tests/test_flash.py +0 -61
- cherrypy_foundation/tests/test_form.py +0 -148
- cherrypy_foundation/tests/test_logging.py +0 -78
- cherrypy_foundation/tests/test_sessions.py +0 -89
- cherrypy_foundation/tests/test_url.py +0 -161
- cherrypy_foundation/tools/tests/components/Button.jinja +0 -2
- cherrypy_foundation/tools/tests/locales/de/LC_MESSAGES/messages.mo +0 -0
- cherrypy_foundation/tools/tests/templates/test_jinja2.html +0 -11
- cherrypy_foundation/tools/tests/templates/test_jinjax.html +0 -9
- cherrypy_foundation/tools/tests/templates/test_jinjax_i18n.html +0 -22
- cherrypy_foundation/tools/tests/test_auth_mfa.py +0 -369
- cherrypy_foundation/tools/tests/test_jinja2.py +0 -153
- cherrypy_foundation/tools/tests/test_secure_headers.py +0 -200
- cherrypy_foundation-1.0.0.dist-info/METADATA +0 -71
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/licenses/LICENSE.md +0 -0
- {cherrypy_foundation-1.0.0.dist-info → cherrypy_foundation-1.0.0a1.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Jinja2 tools for cherrypy
|
|
2
|
-
# Copyright (C) 2021-
|
|
2
|
+
# Copyright (C) 2021-2025 Patrik Dufresne
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
17
|
import importlib
|
|
18
|
-
import logging
|
|
19
18
|
import time
|
|
20
19
|
|
|
21
20
|
import cherrypy
|
|
@@ -52,15 +51,15 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
52
51
|
|
|
53
52
|
def wrap(*args, **kwargs):
|
|
54
53
|
# Call original handler
|
|
55
|
-
|
|
54
|
+
vars = self.oldhandler(*args, **kwargs)
|
|
56
55
|
# Render template.
|
|
57
|
-
return self.render_request(env=env, template=template,
|
|
56
|
+
return self.render_request(env=env, template=template, vars=vars, extra_processor=extra_processor)
|
|
58
57
|
|
|
59
58
|
request = cherrypy.serving.request
|
|
60
59
|
if request.handler is not None:
|
|
61
60
|
# Replace request.handler with self
|
|
62
61
|
if debug:
|
|
63
|
-
cherrypy.log('
|
|
62
|
+
cherrypy.log('Replacing request.handler', 'TOOLS.JINJA2')
|
|
64
63
|
self.oldhandler = request.handler
|
|
65
64
|
request.handler = wrap
|
|
66
65
|
|
|
@@ -80,27 +79,12 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
80
79
|
|
|
81
80
|
# Enable translation if available
|
|
82
81
|
if hasattr(cherrypy.tools, 'i18n'):
|
|
83
|
-
from .i18n import
|
|
84
|
-
format_date,
|
|
85
|
-
format_datetime,
|
|
86
|
-
get_language_name,
|
|
87
|
-
get_timezone_name,
|
|
88
|
-
get_translation,
|
|
89
|
-
list_available_locales,
|
|
90
|
-
list_available_timezones,
|
|
91
|
-
ugettext,
|
|
92
|
-
ungettext,
|
|
93
|
-
)
|
|
82
|
+
from .i18n import format_datetime, get_language_name, ugettext, ungettext
|
|
94
83
|
|
|
95
84
|
env.add_extension('jinja2.ext.i18n')
|
|
96
85
|
env.install_gettext_callables(ugettext, ungettext, newstyle=True)
|
|
97
|
-
env.filters['format_date'] = format_date
|
|
98
86
|
env.filters['format_datetime'] = format_datetime
|
|
99
87
|
env.globals['get_language_name'] = get_language_name
|
|
100
|
-
env.globals['get_translation'] = get_translation
|
|
101
|
-
env.globals['list_available_locales'] = list_available_locales
|
|
102
|
-
env.globals['list_available_timezones'] = list_available_timezones
|
|
103
|
-
env.globals['get_timezone_name'] = get_timezone_name
|
|
104
88
|
|
|
105
89
|
# Update globals, filters and tests
|
|
106
90
|
env.globals.update(globals)
|
|
@@ -120,7 +104,7 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
120
104
|
|
|
121
105
|
return env
|
|
122
106
|
|
|
123
|
-
def render_request(self, template,
|
|
107
|
+
def render_request(self, template, vars={}, env=_UNDEFINED, extra_processor=_UNDEFINED):
|
|
124
108
|
"""
|
|
125
109
|
Render template for a given cherrypy request.
|
|
126
110
|
"""
|
|
@@ -130,24 +114,24 @@ class Jinja2Tool(cherrypy.Tool):
|
|
|
130
114
|
if extra_processor is _UNDEFINED:
|
|
131
115
|
extra_processor = request.config.get('tools.jinja2.extra_processor')
|
|
132
116
|
# Execute extra processor if defined.
|
|
133
|
-
|
|
117
|
+
all_vars = {}
|
|
134
118
|
if extra_processor:
|
|
135
|
-
|
|
136
|
-
|
|
119
|
+
all_vars.update(extra_processor())
|
|
120
|
+
all_vars.update(vars)
|
|
137
121
|
# Render templates
|
|
138
|
-
return self.render(env=env, template=template,
|
|
122
|
+
return self.render(env=env, template=template, vars=all_vars)
|
|
139
123
|
|
|
140
|
-
def render(self, env, template,
|
|
124
|
+
def render(self, env, template, vars={}):
|
|
141
125
|
"""
|
|
142
126
|
Lower level function used to render a template using the given jinja2 environment, template(s) and variable context.
|
|
143
127
|
"""
|
|
144
128
|
# Get the right templates
|
|
145
129
|
if isinstance(template, (list, tuple)):
|
|
146
|
-
names = [t.format(**
|
|
130
|
+
names = [t.format(**vars) for t in template]
|
|
147
131
|
tmpl = env.select_template(names)
|
|
148
132
|
else:
|
|
149
133
|
tmpl = env.get_template(template)
|
|
150
|
-
out = tmpl.render(
|
|
134
|
+
out = tmpl.render(vars)
|
|
151
135
|
|
|
152
136
|
# With JinjaX > 0.60 render explicitly here.
|
|
153
137
|
if 'catalog' in env.globals and getattr(env.globals['catalog'], '_emit_assets_later', False):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Ratelimit tools for cherrypy
|
|
2
|
-
# Copyright (C) 2022-
|
|
2
|
+
# Copyright (C) 2022-2025 Patrik Dufresne
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -22,8 +22,6 @@ from collections import namedtuple
|
|
|
22
22
|
|
|
23
23
|
import cherrypy
|
|
24
24
|
|
|
25
|
-
from cherrypy_foundation.sessions import session_lock
|
|
26
|
-
|
|
27
25
|
Tracker = namedtuple('Tracker', ['token', 'hits', 'timeout'])
|
|
28
26
|
|
|
29
27
|
|
|
@@ -148,29 +146,9 @@ class FileRateLimit(_DataStore):
|
|
|
148
146
|
|
|
149
147
|
|
|
150
148
|
class Ratelimit(cherrypy.Tool):
|
|
151
|
-
CONTEXT = 'TOOLS.RATELIMIT'
|
|
152
|
-
|
|
153
|
-
_datastores = {}
|
|
154
|
-
|
|
155
149
|
def __init__(self, priority=60):
|
|
156
150
|
super().__init__('before_handler', self.check_ratelimit, 'ratelimit', priority)
|
|
157
151
|
|
|
158
|
-
def _get_datastore(self, storage_class, **kwargs):
|
|
159
|
-
"""
|
|
160
|
-
Create new datastore or return existing datastore.
|
|
161
|
-
"""
|
|
162
|
-
# Default to RAM if storage class is not defined.
|
|
163
|
-
if storage_class is None:
|
|
164
|
-
kwargs = {}
|
|
165
|
-
storage_class = RamRateLimit
|
|
166
|
-
# Lookup for matching storage.
|
|
167
|
-
key = (storage_class, str(kwargs))
|
|
168
|
-
datastore = self._datastores.get(key)
|
|
169
|
-
if datastore is None:
|
|
170
|
-
# Create new storage if not found.
|
|
171
|
-
self._datastores[key] = datastore = storage_class(**kwargs)
|
|
172
|
-
return datastore
|
|
173
|
-
|
|
174
152
|
def check_ratelimit(
|
|
175
153
|
self,
|
|
176
154
|
delay=3600,
|
|
@@ -181,8 +159,7 @@ class Ratelimit(cherrypy.Tool):
|
|
|
181
159
|
methods=None,
|
|
182
160
|
debug=False,
|
|
183
161
|
hit=1,
|
|
184
|
-
|
|
185
|
-
**storage_kwargs,
|
|
162
|
+
**conf,
|
|
186
163
|
):
|
|
187
164
|
"""
|
|
188
165
|
Verify the ratelimit. By default return a 429 HTTP error code (Too Many Request). After 25 request within the same hour.
|
|
@@ -195,8 +172,9 @@ class Ratelimit(cherrypy.Tool):
|
|
|
195
172
|
scope: if specify, define the scope of rate limit. Default to path_info.
|
|
196
173
|
methods: if specify, only the methods in the list will be rate limited.
|
|
197
174
|
"""
|
|
198
|
-
|
|
199
|
-
|
|
175
|
+
assert delay > 0, 'invalid delay'
|
|
176
|
+
|
|
177
|
+
# Check if limit is enabled
|
|
200
178
|
if limit <= 0:
|
|
201
179
|
return
|
|
202
180
|
|
|
@@ -204,11 +182,19 @@ class Ratelimit(cherrypy.Tool):
|
|
|
204
182
|
request = cherrypy.request
|
|
205
183
|
if methods is not None and request.method not in methods:
|
|
206
184
|
if debug:
|
|
207
|
-
cherrypy.log(
|
|
185
|
+
cherrypy.log(
|
|
186
|
+
'skip rate limit for HTTP method %s' % (request.method,),
|
|
187
|
+
'TOOLS.RATELIMIT',
|
|
188
|
+
)
|
|
208
189
|
return
|
|
209
190
|
|
|
210
|
-
#
|
|
211
|
-
datastore = self
|
|
191
|
+
# If datastore is not pass as configuration, create it for the first time.
|
|
192
|
+
datastore = getattr(self, '_ratelimit_datastore', None)
|
|
193
|
+
if datastore is None:
|
|
194
|
+
# Create storage using storage class
|
|
195
|
+
storage_class = conf.get('storage_class', RamRateLimit)
|
|
196
|
+
datastore = storage_class(**conf)
|
|
197
|
+
self._ratelimit_datastore = datastore
|
|
212
198
|
|
|
213
199
|
# Identifier: prefer authenticated user; else client IP
|
|
214
200
|
identifier = getattr(cherrypy.serving.request, 'login', None) or cherrypy.request.remote.ip
|
|
@@ -227,7 +213,10 @@ class Ratelimit(cherrypy.Tool):
|
|
|
227
213
|
# Get hits count using datastore.
|
|
228
214
|
hits, timeout = datastore.get_and_increment(token, delay, hit)
|
|
229
215
|
if debug:
|
|
230
|
-
cherrypy.log(
|
|
216
|
+
cherrypy.log(
|
|
217
|
+
'check and increase rate limit for scope %s, limit %s, hits %s' % (token, limit, hits),
|
|
218
|
+
'TOOLS.RATELIMIT',
|
|
219
|
+
)
|
|
231
220
|
|
|
232
221
|
# Verify user has not exceeded rate limit
|
|
233
222
|
remaining = max(0, limit - hits)
|
|
@@ -238,11 +227,10 @@ class Ratelimit(cherrypy.Tool):
|
|
|
238
227
|
cherrypy.response.headers['X-RateLimit-Reset'] = str(timeout)
|
|
239
228
|
|
|
240
229
|
if limit < hits: # block only after 'limit' successful requests
|
|
241
|
-
cherrypy.log(
|
|
230
|
+
cherrypy.log('ratelimit access to `%s`' % request.path_info, 'TOOLS.RATELIMIT')
|
|
242
231
|
if logout:
|
|
243
232
|
if hasattr(cherrypy.serving, 'session'):
|
|
244
|
-
|
|
245
|
-
session.clear()
|
|
233
|
+
cherrypy.serving.session.clear()
|
|
246
234
|
raise cherrypy.HTTPRedirect("/")
|
|
247
235
|
raise cherrypy.HTTPError(return_status)
|
|
248
236
|
|
|
@@ -256,10 +244,12 @@ class Ratelimit(cherrypy.Tool):
|
|
|
256
244
|
|
|
257
245
|
def reset(self):
|
|
258
246
|
"""
|
|
259
|
-
Used to reset the ratelimit.
|
|
247
|
+
Used to reset the ratelimit.
|
|
260
248
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
249
|
+
datastore = getattr(self, '_ratelimit_datastore', False)
|
|
250
|
+
if not datastore:
|
|
251
|
+
return
|
|
252
|
+
datastore.reset()
|
|
263
253
|
|
|
264
254
|
|
|
265
255
|
cherrypy.tools.ratelimit = Ratelimit()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Secure Headers tool for cherrypy
|
|
2
|
-
# Copyright (C) 2012-
|
|
2
|
+
# Copyright (C) 2012-2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
17
|
import http.cookies
|
|
18
|
+
import logging
|
|
18
19
|
|
|
19
20
|
import cherrypy
|
|
20
21
|
|
|
22
|
+
# Define the logger
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
21
25
|
DEFAULT_CSP = {
|
|
22
26
|
'default-src': 'self',
|
|
23
27
|
'style-src': ('self', 'unsafe-inline'),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Session timeout tool for cherrypy
|
|
2
|
-
# Copyright (C) 2012-
|
|
2
|
+
# Copyright (C) 2012-2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -21,8 +21,6 @@ import time
|
|
|
21
21
|
import cherrypy
|
|
22
22
|
from cherrypy.lib import httputil
|
|
23
23
|
|
|
24
|
-
from cherrypy_foundation.sessions import session_lock
|
|
25
|
-
|
|
26
24
|
SESSION_PERSISTENT = '_session_persistent'
|
|
27
25
|
SESSION_START_TIME = '_session_start_time'
|
|
28
26
|
|
|
@@ -59,13 +57,13 @@ class SessionsTimeout(cherrypy.Tool):
|
|
|
59
57
|
absolute_timeout = int(cfg.get('tools.sessions_timeout.absolute_timeout', SESSION_DEFAULT_ABSOLUTE_TIMEOUT))
|
|
60
58
|
return idle_timeout, persistent_timeout, absolute_timeout
|
|
61
59
|
|
|
62
|
-
def _set_cookie_max_age(self,
|
|
60
|
+
def _set_cookie_max_age(self, max_age: int) -> None:
|
|
63
61
|
"""Adjust only Max-Age and Expires for the session cookie, keep other flags intact."""
|
|
64
62
|
cookie_name = self._cookie_name()
|
|
65
63
|
cookie = cherrypy.serving.response.cookie
|
|
66
64
|
# Ensure the cookie key exists (CherryPy sets it when session id is issued/regenerated).
|
|
67
65
|
if cookie_name not in cookie:
|
|
68
|
-
cookie[cookie_name] = session.id
|
|
66
|
+
cookie[cookie_name] = cherrypy.serving.session.id
|
|
69
67
|
cookie[cookie_name]['max-age'] = str(max(0, max_age))
|
|
70
68
|
cookie[cookie_name]['expires'] = httputil.HTTPDate(time.time() + max(0, max_age))
|
|
71
69
|
|
|
@@ -75,21 +73,24 @@ class SessionsTimeout(cherrypy.Tool):
|
|
|
75
73
|
return 0
|
|
76
74
|
return int(math.ceil(seconds / 60.0))
|
|
77
75
|
|
|
78
|
-
def _ensure_start_time(self
|
|
76
|
+
def _ensure_start_time(self) -> None:
|
|
79
77
|
"""Ensure the session has a stable start time anchor for absolute timeout."""
|
|
78
|
+
session = cherrypy.serving.session
|
|
80
79
|
if SESSION_START_TIME not in session or not isinstance(session.get(SESSION_START_TIME), datetime.datetime):
|
|
81
80
|
session[SESSION_START_TIME] = session.now()
|
|
82
81
|
|
|
83
|
-
def _expire_and_restart(self,
|
|
82
|
+
def _expire_and_restart(self, make_persistent: bool | None = None) -> None:
|
|
84
83
|
"""Expire current session and start a fresh one. Optionally set persistent flag."""
|
|
84
|
+
session = cherrypy.serving.session
|
|
85
85
|
session.clear()
|
|
86
86
|
session.regenerate()
|
|
87
87
|
session[SESSION_START_TIME] = session.now()
|
|
88
88
|
if make_persistent is not None:
|
|
89
89
|
session[SESSION_PERSISTENT] = bool(make_persistent)
|
|
90
90
|
|
|
91
|
-
def _update_session_timeout(self
|
|
92
|
-
|
|
91
|
+
def _update_session_timeout(self) -> None:
|
|
92
|
+
session = cherrypy.serving.session
|
|
93
|
+
self._ensure_start_time()
|
|
93
94
|
|
|
94
95
|
now: datetime.datetime = session.now()
|
|
95
96
|
start: datetime.datetime = session[SESSION_START_TIME]
|
|
@@ -119,9 +120,9 @@ class SessionsTimeout(cherrypy.Tool):
|
|
|
119
120
|
# Expired due to either sliding window or absolute cap
|
|
120
121
|
# Start a fresh session; do NOT automatically re-mark persistent here.
|
|
121
122
|
# Authentication/Remember me logic elsewhere can call set_persistent(True) again after login.
|
|
122
|
-
self._expire_and_restart(
|
|
123
|
+
self._expire_and_restart()
|
|
123
124
|
# After regeneration, set a sensible session.timeout baseline:
|
|
124
|
-
session.timeout = idle_timeout_min
|
|
125
|
+
cherrypy.serving.session.timeout = idle_timeout_min
|
|
125
126
|
return
|
|
126
127
|
|
|
127
128
|
# Apply per-request remaining minutes to CherryPy's session timeout
|
|
@@ -129,7 +130,7 @@ class SessionsTimeout(cherrypy.Tool):
|
|
|
129
130
|
|
|
130
131
|
if is_persistent:
|
|
131
132
|
# Keep cookie lifetime aligned with remaining time so it can survive browser restarts.
|
|
132
|
-
self._set_cookie_max_age(
|
|
133
|
+
self._set_cookie_max_age(remaining_sec)
|
|
133
134
|
else:
|
|
134
135
|
# For non-persistent, let CherryPy manage the session cookie (session or transient cookie).
|
|
135
136
|
# If you want strict enforcement client-side too, uncomment the next line:
|
|
@@ -142,26 +143,23 @@ class SessionsTimeout(cherrypy.Tool):
|
|
|
142
143
|
return
|
|
143
144
|
|
|
144
145
|
# Ensure configured values are honored (method signature kept for CherryPy Tool interface)
|
|
145
|
-
|
|
146
|
-
self._update_session_timeout(session)
|
|
146
|
+
self._update_session_timeout()
|
|
147
147
|
|
|
148
148
|
def set_persistent(self, value: bool = True) -> None:
|
|
149
149
|
"""Mark current session as persistent (or not) and reset the start window."""
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
session = cherrypy.serving.session
|
|
151
|
+
session[SESSION_PERSISTENT] = bool(value)
|
|
152
|
+
# Reset absolute window anchor when the persistence policy changes
|
|
153
|
+
session[SESSION_START_TIME] = session.now()
|
|
154
|
+
self._update_session_timeout()
|
|
155
155
|
|
|
156
156
|
def is_persistent(self) -> bool:
|
|
157
157
|
"""Return True if the session is marked persistent."""
|
|
158
|
-
|
|
159
|
-
return bool(session.get(SESSION_PERSISTENT, False))
|
|
158
|
+
return bool(cherrypy.serving.session.get(SESSION_PERSISTENT, False))
|
|
160
159
|
|
|
161
160
|
def get_session_start_time(self) -> datetime.datetime | None:
|
|
162
161
|
"""Return the session start time if any."""
|
|
163
|
-
|
|
164
|
-
return session.get(SESSION_START_TIME, None)
|
|
162
|
+
return cherrypy.serving.session.get(SESSION_START_TIME, None)
|
|
165
163
|
|
|
166
164
|
|
|
167
165
|
cherrypy.tools.sessions_timeout = SessionsTimeout()
|
|
Binary file
|
|
@@ -5,11 +5,11 @@ msgstr ""
|
|
|
5
5
|
"PO-Revision-Date: \n"
|
|
6
6
|
"Last-Translator: \n"
|
|
7
7
|
"Language-Team: \n"
|
|
8
|
-
"Language:
|
|
8
|
+
"Language: en\n"
|
|
9
9
|
"MIME-Version: 1.0\n"
|
|
10
10
|
"Content-Type: text/plain; charset=UTF-8\n"
|
|
11
11
|
"Content-Transfer-Encoding: 8bit\n"
|
|
12
12
|
"X-Generator: Poedit 3.6\n"
|
|
13
13
|
|
|
14
14
|
msgid "Some text to translate"
|
|
15
|
-
msgstr "
|
|
15
|
+
msgstr ""
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# CherryPy
|
|
2
|
-
# Copyright (C)
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -14,12 +14,11 @@
|
|
|
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
|
-
|
|
17
|
+
|
|
18
18
|
from collections import namedtuple
|
|
19
19
|
from urllib.parse import urlencode
|
|
20
20
|
|
|
21
21
|
import cherrypy
|
|
22
|
-
from cherrypy.lib.sessions import FileSession
|
|
23
22
|
from cherrypy.test import helper
|
|
24
23
|
|
|
25
24
|
from .. import auth # noqa
|
|
@@ -43,7 +42,7 @@ def user_from_key_func(userkey):
|
|
|
43
42
|
return None
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
@cherrypy.tools.sessions(
|
|
45
|
+
@cherrypy.tools.sessions()
|
|
47
46
|
@cherrypy.tools.auth(
|
|
48
47
|
user_lookup_func=user_lookup_func,
|
|
49
48
|
user_from_key_func=user_from_key_func,
|
|
@@ -69,24 +68,8 @@ class Root:
|
|
|
69
68
|
class AuthManagerTest(helper.CPWebCase):
|
|
70
69
|
interactive = False
|
|
71
70
|
|
|
72
|
-
@classmethod
|
|
73
|
-
def setup_class(cls):
|
|
74
|
-
cls.tempdir = tempfile.TemporaryDirectory(prefix='cherrypy-foundation-', suffix='-session')
|
|
75
|
-
cls.session_dir = cls.tempdir.name
|
|
76
|
-
super().setup_class()
|
|
77
|
-
|
|
78
|
-
@classmethod
|
|
79
|
-
def teardown_class(cls):
|
|
80
|
-
cls.tempdir.cleanup()
|
|
81
|
-
super().teardown_class()
|
|
82
|
-
|
|
83
71
|
@classmethod
|
|
84
72
|
def setup_server(cls):
|
|
85
|
-
cherrypy.config.update(
|
|
86
|
-
{
|
|
87
|
-
'tools.sessions.storage_path': cls.session_dir,
|
|
88
|
-
}
|
|
89
|
-
)
|
|
90
73
|
cherrypy.tree.mount(Root(), '/')
|
|
91
74
|
|
|
92
75
|
def test_auth_redirect(self):
|
|
@@ -100,7 +83,7 @@ class AuthManagerTest(helper.CPWebCase):
|
|
|
100
83
|
def test_auth_login(self):
|
|
101
84
|
# Given unauthenticated user
|
|
102
85
|
# When posting valid login
|
|
103
|
-
self.getPage('/login/', method='POST', body=urlencode(
|
|
86
|
+
self.getPage('/login/', method='POST', body=urlencode([('username', 'myuser'), ('password', 'changeme')]))
|
|
104
87
|
# Then user is redirect to index.
|
|
105
88
|
self.assertStatus(303)
|
|
106
89
|
self.assertHeaderItemValue('Location', f'http://{self.HOST}:{self.PORT}/')
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# CherryPy
|
|
2
|
-
# Copyright (C)
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -38,6 +38,7 @@ class TestI18n(unittest.TestCase):
|
|
|
38
38
|
def test_search_translation_en(self):
|
|
39
39
|
# Load default translation return translation
|
|
40
40
|
t = i18n._search_translation(self.mo_dir, 'messages', 'en')
|
|
41
|
+
self.assertIsInstance(t, gettext.GNUTranslations)
|
|
41
42
|
self.assertEqual("en", t.locale.language)
|
|
42
43
|
# Test translation object
|
|
43
44
|
self.assertEqual(TEXT_EN, t.gettext(TEXT_EN))
|
|
@@ -53,8 +54,7 @@ class TestI18n(unittest.TestCase):
|
|
|
53
54
|
def test_search_translation_invalid(self):
|
|
54
55
|
# Load invalid translation return None
|
|
55
56
|
t = i18n._search_translation(self.mo_dir, 'messages', 'tr')
|
|
56
|
-
|
|
57
|
-
self.assertIn('NullTranslations', str(t.__class__))
|
|
57
|
+
self.assertIsNone(t)
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
class Root:
|
|
@@ -76,7 +76,6 @@ class AbstractI18nTest(helper.CPWebCase):
|
|
|
76
76
|
'tools.i18n.default': cls.default_lang,
|
|
77
77
|
'tools.i18n.mo_dir': importlib.resources.files(__package__) / 'locales',
|
|
78
78
|
'tools.i18n.domain': 'messages',
|
|
79
|
-
'tools.i18n.cookie_name': 'locale',
|
|
80
79
|
}
|
|
81
80
|
)
|
|
82
81
|
cherrypy.tree.mount(Root(), '/')
|
|
@@ -84,13 +83,6 @@ class AbstractI18nTest(helper.CPWebCase):
|
|
|
84
83
|
|
|
85
84
|
class TestI18nWebCase(AbstractI18nTest):
|
|
86
85
|
|
|
87
|
-
def test_language_with_invalid(self):
|
|
88
|
-
# Query the page without login-in
|
|
89
|
-
self.getPage("/", headers=[("Accept-Language", "invalid")])
|
|
90
|
-
self.assertStatus('200 OK')
|
|
91
|
-
self.assertHeaderItemValue("Content-Language", "en")
|
|
92
|
-
self.assertInBody(TEXT_EN)
|
|
93
|
-
|
|
94
86
|
def test_language_with_unknown(self):
|
|
95
87
|
# Query the page without login-in
|
|
96
88
|
self.getPage("/", headers=[("Accept-Language", "it")])
|
|
@@ -101,13 +93,13 @@ class TestI18nWebCase(AbstractI18nTest):
|
|
|
101
93
|
def test_language_en(self):
|
|
102
94
|
self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8")])
|
|
103
95
|
self.assertStatus('200 OK')
|
|
104
|
-
self.assertHeaderItemValue("Content-Language", "en
|
|
96
|
+
self.assertHeaderItemValue("Content-Language", "en")
|
|
105
97
|
self.assertInBody(TEXT_EN)
|
|
106
98
|
|
|
107
99
|
def test_language_en_fr(self):
|
|
108
100
|
self.getPage("/", headers=[("Accept-Language", "en-US,en;q=0.8,fr-CA;q=0.8")])
|
|
109
101
|
self.assertStatus('200 OK')
|
|
110
|
-
self.assertHeaderItemValue("Content-Language", "en
|
|
102
|
+
self.assertHeaderItemValue("Content-Language", "en")
|
|
111
103
|
self.assertInBody(TEXT_EN)
|
|
112
104
|
|
|
113
105
|
def test_language_fr(self):
|
|
@@ -115,22 +107,8 @@ class TestI18nWebCase(AbstractI18nTest):
|
|
|
115
107
|
self.assertInBody(TEXT_EN)
|
|
116
108
|
self.getPage("/", headers=[("Accept-Language", "fr-CA;q=0.8,fr;q=0.6")])
|
|
117
109
|
self.assertStatus('200 OK')
|
|
118
|
-
self.assertHeaderItemValue("Content-Language", "fr-CA")
|
|
119
|
-
self.assertInBody(TEXT_FR)
|
|
120
|
-
|
|
121
|
-
def test_language_en_US_POSIX(self):
|
|
122
|
-
# When calling with locale variant
|
|
123
|
-
self.getPage("/", headers=[("Accept-Language", "en-US-POSIX")])
|
|
124
|
-
self.assertStatus('200 OK')
|
|
125
|
-
# Tehn page return en-US
|
|
126
|
-
self.assertHeaderItemValue("Content-Language", "en-US")
|
|
127
|
-
|
|
128
|
-
def test_cookie_fr(self):
|
|
129
|
-
# When calling with locale variant
|
|
130
|
-
self.getPage("/", headers=[("Accept-Language", "en-US"), ("Cookie", "locale=fr")])
|
|
131
|
-
self.assertStatus('200 OK')
|
|
132
|
-
# Tehn page return en-US
|
|
133
110
|
self.assertHeaderItemValue("Content-Language", "fr")
|
|
111
|
+
self.assertInBody(TEXT_FR)
|
|
134
112
|
|
|
135
113
|
def test_with_preferred_lang(self):
|
|
136
114
|
# Given a default lang 'en'
|
|
@@ -147,59 +125,6 @@ class TestI18nWebCase(AbstractI18nTest):
|
|
|
147
125
|
self.assertEqual(TEXT_EN, i18n.ugettext(TEXT_EN))
|
|
148
126
|
self.assertIn('March', i18n.format_datetime(date, format='long'))
|
|
149
127
|
|
|
150
|
-
def test_format_datetime_locales(self):
|
|
151
|
-
date = datetime.fromtimestamp(1680111611, timezone.utc)
|
|
152
|
-
with i18n.preferred_timezone('utc'):
|
|
153
|
-
with i18n.preferred_lang('fr'):
|
|
154
|
-
self.assertEqual('29 mars 2023, 17:40:11 TU', i18n.format_datetime(date, format='long'))
|
|
155
|
-
with i18n.preferred_lang('en'):
|
|
156
|
-
self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
|
|
157
|
-
with i18n.preferred_lang('en_US'):
|
|
158
|
-
self.assertEqual('March 29, 2023, 5:40:11\u202fPM UTC', i18n.format_datetime(date, format='long'))
|
|
159
|
-
with i18n.preferred_lang('en_GB'):
|
|
160
|
-
self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
161
|
-
with i18n.preferred_lang('en_CH'):
|
|
162
|
-
self.assertEqual('29 March 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
163
|
-
with i18n.preferred_lang('de'):
|
|
164
|
-
self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
165
|
-
with i18n.preferred_lang('de_CH'):
|
|
166
|
-
self.assertEqual('29. März 2023, 17:40:11 UTC', i18n.format_datetime(date, format='long'))
|
|
167
|
-
|
|
168
|
-
with i18n.preferred_timezone('Europe/Paris'):
|
|
169
|
-
with i18n.preferred_lang('fr'):
|
|
170
|
-
self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
|
|
171
|
-
with i18n.preferred_lang('fr_CH'):
|
|
172
|
-
self.assertEqual('29 mars 2023, 19:40:11 +0200', i18n.format_datetime(date, format='long'))
|
|
173
|
-
with i18n.preferred_lang('en'):
|
|
174
|
-
self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
|
|
175
|
-
with i18n.preferred_lang('en_US'):
|
|
176
|
-
self.assertEqual('March 29, 2023, 7:40:11\u202fPM +0200', i18n.format_datetime(date, format='long'))
|
|
177
|
-
with i18n.preferred_lang('en_GB'):
|
|
178
|
-
self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
|
|
179
|
-
with i18n.preferred_lang('en_CH'):
|
|
180
|
-
self.assertEqual('29 March 2023, 19:40:11 CEST', i18n.format_datetime(date, format='long'))
|
|
181
|
-
with i18n.preferred_lang('de'):
|
|
182
|
-
self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
|
|
183
|
-
with i18n.preferred_lang('de_CH'):
|
|
184
|
-
self.assertEqual('29. März 2023, 19:40:11 MESZ', i18n.format_datetime(date, format='long'))
|
|
185
|
-
|
|
186
|
-
def test_list_available_locales(self):
|
|
187
|
-
self.assertEqual(['de', 'en', 'fr'], sorted([str(l) for l in i18n.list_available_locales()]))
|
|
188
|
-
|
|
189
|
-
def test_list_available_timezones(self):
|
|
190
|
-
timezones = i18n.list_available_timezones()
|
|
191
|
-
self.assertIn('America/Toronto', timezones)
|
|
192
|
-
|
|
193
|
-
def test_get_timezone_name(self):
|
|
194
|
-
with i18n.preferred_lang('en'):
|
|
195
|
-
self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto'))
|
|
196
|
-
self.assertEqual('ET', i18n.get_timezone_name('America/Toronto', width='short'))
|
|
197
|
-
self.assertEqual('Eastern Time', i18n.get_timezone_name('America/Toronto', width='long'))
|
|
198
|
-
with i18n.preferred_lang('fr'):
|
|
199
|
-
self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto'))
|
|
200
|
-
self.assertEqual('HE', i18n.get_timezone_name('America/Toronto', width='short'))
|
|
201
|
-
self.assertEqual('heure de l’Est nord-américain', i18n.get_timezone_name('America/Toronto', width='long'))
|
|
202
|
-
|
|
203
128
|
|
|
204
129
|
class TestI18nDefaultLangWebCase(AbstractI18nTest):
|
|
205
130
|
default_lang = 'FR'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# CherryPy
|
|
2
|
-
# Copyright (C)
|
|
2
|
+
# Copyright (C) 2025 IKUS Software
|
|
3
3
|
#
|
|
4
4
|
# This program is free software: you can redistribute it and/or modify
|
|
5
5
|
# it under the terms of the GNU General Public License as published by
|
|
@@ -49,7 +49,7 @@ class RateLimitTest(helper.CPWebCase):
|
|
|
49
49
|
|
|
50
50
|
@classmethod
|
|
51
51
|
def setup_server(cls):
|
|
52
|
-
rate_limit_storage_class =
|
|
52
|
+
rate_limit_storage_class = ratelimit.RamRateLimit
|
|
53
53
|
if cls.rate_limit_dir:
|
|
54
54
|
rate_limit_storage_class = ratelimit.FileRateLimit
|
|
55
55
|
cherrypy.config.update(
|