Skip to content

Commit 98a11b7

Browse files
authored
Merge branch 'main' into add-counter
2 parents 44822d6 + 40b121f commit 98a11b7

14 files changed

Lines changed: 112 additions & 109 deletions

File tree

cms/grading/ParameterTypes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929

3030
from abc import ABCMeta, abstractmethod
3131

32-
from jinja2 import Markup, Template
32+
from jinja2 import Template
33+
from markupsafe import Markup
3334
import typing
3435

3536
if typing.TYPE_CHECKING:

cms/io/web_rpc.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ def __init__(
8181
self._service = service
8282
self._auth = auth
8383
self._url_map = Map([Rule("/<service>/<int:shard>/<method>",
84-
methods=["POST"], endpoint="rpc")],
85-
encoding_errors="strict")
84+
methods=["POST"], endpoint="rpc")])
8685

8786
def __call__(self, environ, start_response):
8887
"""Execute this instance as a WSGI application.

cms/io/web_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
import tornado.wsgi
3232
from gevent.pywsgi import WSGIServer
33-
from werkzeug.contrib.fixers import ProxyFix
33+
from werkzeug.middleware.proxy_fix import ProxyFix
3434
from werkzeug.middleware.dispatcher import DispatcherMiddleware
3535
from werkzeug.middleware.shared_data import SharedDataMiddleware
3636

cms/server/admin/authentication.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,17 @@
1919

2020
from collections.abc import Callable
2121
import json
22+
import math
23+
import typing
2224

23-
from werkzeug.contrib.securecookie import SecureCookie
25+
from tornado.web import create_signed_value, decode_signed_value
2426
from werkzeug.local import Local, LocalManager
2527
from werkzeug.wrappers import Request, Response
2628

2729
from cms import config
28-
from cmscommon.binary import hex_to_bin
2930
from cmscommon.datetime import make_timestamp
3031

3132

32-
class UTF8JSON:
33-
@staticmethod
34-
def dumps(d: object) -> bytes:
35-
return json.dumps(d).encode('utf-8')
36-
37-
@staticmethod
38-
def loads(e: bytes) -> object:
39-
return json.loads(e.decode('utf-8'))
40-
41-
42-
class JSONSecureCookie(SecureCookie):
43-
serialization_method = UTF8JSON
44-
45-
4633
class AWSAuthMiddleware:
4734
"""Handler for the low-level tasks of admin authentication.
4835
@@ -70,7 +57,7 @@ def __init__(self, app: Callable):
7057
self.wsgi_app = self._local_manager.make_middleware(self.wsgi_app)
7158

7259
self._request: Request = self._local("request")
73-
self._cookie: JSONSecureCookie = self._local("cookie")
60+
self._cookie: dict[str, typing.Any] = self._local("cookie")
7461

7562
@property
7663
def admin_id(self) -> int | None:
@@ -128,9 +115,20 @@ def wsgi_app(self, environ: dict, start_response: Callable):
128115
129116
"""
130117
self._local.request = Request(environ)
131-
self._local.cookie = JSONSecureCookie.load_cookie(
132-
self._request, AWSAuthMiddleware.COOKIE,
133-
hex_to_bin(config.web_server.secret_key))
118+
cookie_str = decode_signed_value(
119+
bytes.fromhex(config.web_server.secret_key),
120+
AWSAuthMiddleware.COOKIE,
121+
self._request.cookies.get(AWSAuthMiddleware.COOKIE),
122+
# We do our own expiry checking, so an upper bound is fine here
123+
max_age_days=math.ceil(
124+
config.admin_web_server.cookie_duration / 60 / 60 / 24
125+
),
126+
)
127+
if cookie_str is not None:
128+
self._local.cookie = json.loads(cookie_str.decode())
129+
else:
130+
self._local.cookie = {}
131+
134132
self._verify_cookie()
135133

136134
def my_start_response(status, headers, exc_info=None):
@@ -142,9 +140,20 @@ def my_start_response(status, headers, exc_info=None):
142140
143141
"""
144142
response = Response(status=status, headers=headers)
145-
self._cookie.save_cookie(
146-
response, AWSAuthMiddleware.COOKIE, httponly=True,
147-
max_age=config.admin_web_server.cookie_duration)
143+
# json.dumps doesn't like LocalProxy objects, so we grab the actual
144+
# underlying value here with _get_current_object
145+
cookie_str = json.dumps(self._cookie._get_current_object())
146+
cookie_signed = create_signed_value(
147+
bytes.fromhex(config.web_server.secret_key),
148+
AWSAuthMiddleware.COOKIE,
149+
cookie_str,
150+
).decode()
151+
response.set_cookie(
152+
AWSAuthMiddleware.COOKIE,
153+
cookie_signed,
154+
httponly=True,
155+
max_age=config.admin_web_server.cookie_duration,
156+
)
148157
return start_response(
149158
status, response.headers.to_wsgi_list(), exc_info)
150159

cms/server/admin/templates/submission.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ <h2 id="title_details" class="toggling_on">Submission details</h2>
163163
{{- sr.compilation_sandbox_paths[loop.index0] -}}
164164
</a>
165165
{% endfor %}
166-
{% else %}
166+
{% elif sr.compilation_sandbox_paths is not none %}
167167
{{ sr.compilation_sandbox_paths|join(" ") }}
168168
{% endif %}
169169

@@ -297,7 +297,7 @@ <h2 id="title_evaluation_admin" class="toggling_on">Evaluation (as seen by the a
297297
{{- ev.evaluation_sandbox_paths[loop.index0] -}}
298298
</a>
299299
{% endfor %}
300-
{% else %}
300+
{% elif ev.evaluation_sandbox_paths is not none %}
301301
{{ ev.evaluation_sandbox_paths|join(" ") }}
302302
{% endif %}
303303

cms/server/contest/jinja2_toolbox.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
2424
"""
2525

26-
from jinja2 import contextfilter, PackageLoader
26+
from jinja2 import pass_context, PackageLoader
2727

2828
from cms.server.jinja2_toolbox import GLOBAL_ENVIRONMENT
2929
from .formatting import format_token_rules, get_score_class
@@ -38,7 +38,7 @@ def instrument_cms_toolbox(env):
3838
env.filters["extract_token_params"] = extract_token_params
3939

4040

41-
@contextfilter
41+
@pass_context
4242
def wrapped_format_token_rules(ctx, tokens, t_type=None):
4343
translation = ctx["translation"]
4444
return format_token_rules(tokens, t_type, translation=translation)

cms/server/jinja2_toolbox.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
"""
2626

2727
from datetime import datetime, timedelta, tzinfo
28-
from jinja2 import Environment, StrictUndefined, contextfilter, \
29-
contextfunction, environmentfunction
28+
from jinja2 import Environment, StrictUndefined, pass_context, \
29+
pass_environment
3030
from jinja2.runtime import Context
3131
import markdown_it
3232
import markupsafe
@@ -45,7 +45,7 @@
4545
from cmscommon.mimetypes import get_type_for_file_name, get_icon_for_type
4646

4747

48-
@contextfilter
48+
@pass_context
4949
def all_(ctx: Context, l: list, test: str | None = None, *args) -> bool:
5050
"""Check if all elements of the given list pass the given test.
5151
@@ -69,7 +69,7 @@ def all_(ctx: Context, l: list, test: str | None = None, *args) -> bool:
6969
return True
7070

7171

72-
@contextfilter
72+
@pass_context
7373
def any_(ctx: Context, l: list, test: str | None = None, *args) -> bool:
7474
"""Check if any element of the given list passes the given test.
7575
@@ -93,7 +93,7 @@ def any_(ctx: Context, l: list, test: str | None = None, *args) -> bool:
9393
return False
9494

9595

96-
@contextfilter
96+
@pass_context
9797
def dictselect(
9898
ctx: Context, d: dict, test: str | None = None, *args, by: str = "key"
9999
) -> dict:
@@ -122,7 +122,7 @@ def dictselect(
122122
if ctx.call(test, {"key": k, "value": v}[by], *args))
123123

124124

125-
@contextfunction
125+
@pass_context
126126
def today(ctx: Context, dt: datetime) -> bool:
127127
"""Returns whether the given datetime is today.
128128
@@ -185,7 +185,7 @@ def instrument_generic_toolbox(env: Environment):
185185
env.tests["today"] = today
186186

187187

188-
@environmentfunction
188+
@pass_environment
189189
def safe_get_task_type(env: Environment, *, dataset: Dataset):
190190
try:
191191
return dataset.task_type_object
@@ -195,7 +195,7 @@ def safe_get_task_type(env: Environment, *, dataset: Dataset):
195195
return env.undefined("TaskType not found: %s" % err)
196196

197197

198-
@environmentfunction
198+
@pass_environment
199199
def safe_get_score_type(env: Environment, *, dataset: Dataset):
200200
try:
201201
return dataset.score_type_object
@@ -215,59 +215,59 @@ def instrument_cms_toolbox(env: Environment):
215215
env.filters["to_language"] = get_language
216216

217217

218-
@contextfilter
218+
@pass_context
219219
def format_datetime(ctx: Context, dt: datetime):
220220
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
221221
timezone: tzinfo = ctx.get("timezone", local_tz)
222222
return translation.format_datetime(dt, timezone)
223223

224224

225-
@contextfilter
225+
@pass_context
226226
def format_time(ctx: Context, dt: datetime):
227227
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
228228
timezone: tzinfo = ctx.get("timezone", local_tz)
229229
return translation.format_time(dt, timezone)
230230

231231

232-
@contextfilter
232+
@pass_context
233233
def format_datetime_smart(ctx: Context, dt: datetime):
234234
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
235235
now: datetime = ctx.get("now", make_datetime())
236236
timezone: tzinfo = ctx.get("timezone", local_tz)
237237
return translation.format_datetime_smart(dt, now, timezone)
238238

239239

240-
@contextfilter
240+
@pass_context
241241
def format_timedelta(ctx: Context, td: timedelta):
242242
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
243243
return translation.format_timedelta(td)
244244

245245

246-
@contextfilter
246+
@pass_context
247247
def format_duration(ctx: Context, d: float, length: str = "short"):
248248
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
249249
return translation.format_duration(d, length)
250250

251251

252-
@contextfilter
252+
@pass_context
253253
def format_size(ctx: Context, s: int):
254254
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
255255
return translation.format_size(s)
256256

257257

258-
@contextfilter
258+
@pass_context
259259
def format_decimal(ctx: Context, n: int):
260260
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
261261
return translation.format_decimal(n)
262262

263263

264-
@contextfilter
264+
@pass_context
265265
def format_locale(ctx: Context, n: str):
266266
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
267267
return translation.format_locale(n)
268268

269269

270-
@contextfilter
270+
@pass_context
271271
def wrapped_format_status_text(ctx: Context, status_text: list[str]):
272272
translation: Translation = ctx.get("translation", DEFAULT_TRANSLATION)
273273
return format_status_text(status_text, translation=translation)

cmscommon/eventsource.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,12 @@ def wsgi_app(self, environ, start_response):
322322
# XMLHttpRequest it has been probably sent from a polyfill (not
323323
# from the native browser implementation) which will be able to
324324
# read the response body only when it has been fully received.
325-
if environ["SERVER_PROTOCOL"] != "HTTP/1.1" or request.is_xhr:
325+
326+
# XXX: this used to also check request.is_xhr, which was removed in a
327+
# newer werkzeug version. But all modern browsers support SSE natively
328+
# so this check isn't necessary nowadays. (Well, the http/1.1 check
329+
# probably isn't necessary either, to be honest...)
330+
if environ["SERVER_PROTOCOL"] != "HTTP/1.1":
326331
one_shot = True
327332
else:
328333
one_shot = False

0 commit comments

Comments
 (0)