Merge branch 'stable'
This commit is contained in:
commit
daca74d93a
6 changed files with 64 additions and 47 deletions
|
|
@ -27,6 +27,15 @@ Unreleased
|
|||
it's disabled in config. Previously, only disabling worked. :issue:`5916`
|
||||
|
||||
|
||||
Version 3.1.3
|
||||
-------------
|
||||
|
||||
Released 2026-02-18
|
||||
|
||||
- The session is marked as accessed for operations that only access the keys
|
||||
but not the values, such as ``in`` and ``len``. :ghsa:`68rp-wp8r-4726`
|
||||
|
||||
|
||||
Version 3.1.2
|
||||
-------------
|
||||
|
||||
|
|
|
|||
|
|
@ -1411,8 +1411,8 @@ class Flask(App):
|
|||
for func in reversed(self.after_request_funcs[name]):
|
||||
response = self.ensure_sync(func)(response)
|
||||
|
||||
if not self.session_interface.is_null_session(ctx.session):
|
||||
self.session_interface.save_session(self, ctx.session, response)
|
||||
if not self.session_interface.is_null_session(ctx._session):
|
||||
self.session_interface.save_session(self, ctx._session, response)
|
||||
|
||||
return response
|
||||
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ class AppContext:
|
|||
def session(self) -> SessionMixin:
|
||||
"""The session object associated with this context. Accessed through
|
||||
:data:`.session`. Only available in request contexts, otherwise raises
|
||||
:exc:`RuntimeError`.
|
||||
:exc:`RuntimeError`. Accessing this sets :attr:`.SessionMixin.accessed`.
|
||||
"""
|
||||
if self._request is None:
|
||||
raise RuntimeError("There is no request in this context.")
|
||||
|
|
@ -393,6 +393,7 @@ class AppContext:
|
|||
if self._session is None:
|
||||
self._session = si.make_null_session(self.app)
|
||||
|
||||
self._session.accessed = True
|
||||
return self._session
|
||||
|
||||
def match_request(self) -> None:
|
||||
|
|
|
|||
|
|
@ -43,10 +43,15 @@ class SessionMixin(MutableMapping[str, t.Any]):
|
|||
#: ``True``.
|
||||
modified = True
|
||||
|
||||
#: Some implementations can detect when session data is read or
|
||||
#: written and set this when that happens. The mixin default is hard
|
||||
#: coded to ``True``.
|
||||
accessed = True
|
||||
accessed = False
|
||||
"""Indicates if the session was accessed, even if it was not modified. This
|
||||
is set when the session object is accessed through the request context,
|
||||
including the global :data:`.session` proxy. A ``Vary: cookie`` header will
|
||||
be added if this is ``True``.
|
||||
|
||||
.. versionchanged:: 3.1.3
|
||||
This is tracked by the request context.
|
||||
"""
|
||||
|
||||
|
||||
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
|
||||
|
|
@ -65,34 +70,15 @@ class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
|
|||
#: will only be written to the response if this is ``True``.
|
||||
modified = False
|
||||
|
||||
#: When data is read or written, this is set to ``True``. Used by
|
||||
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
|
||||
#: header, which allows caching proxies to cache different pages for
|
||||
#: different users.
|
||||
accessed = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
|
||||
initial: c.Mapping[str, t.Any] | None = None,
|
||||
) -> None:
|
||||
def on_update(self: te.Self) -> None:
|
||||
self.modified = True
|
||||
self.accessed = True
|
||||
|
||||
super().__init__(initial, on_update)
|
||||
|
||||
def __getitem__(self, key: str) -> t.Any:
|
||||
self.accessed = True
|
||||
return super().__getitem__(key)
|
||||
|
||||
def get(self, key: str, default: t.Any = None) -> t.Any:
|
||||
self.accessed = True
|
||||
return super().get(key, default)
|
||||
|
||||
def setdefault(self, key: str, default: t.Any = None) -> t.Any:
|
||||
self.accessed = True
|
||||
return super().setdefault(key, default)
|
||||
|
||||
|
||||
class NullSession(SecureCookieSession):
|
||||
"""Class used to generate nicer error messages if sessions are not
|
||||
|
|
|
|||
|
|
@ -19,15 +19,16 @@ if t.TYPE_CHECKING: # pragma: no cover
|
|||
|
||||
|
||||
def _default_template_ctx_processor() -> dict[str, t.Any]:
|
||||
"""Default template context processor. Injects `request`,
|
||||
`session` and `g`.
|
||||
"""Default template context processor. Replaces the ``request`` and ``g``
|
||||
proxies with their concrete objects for faster access.
|
||||
"""
|
||||
ctx = app_ctx._get_current_object()
|
||||
rv: dict[str, t.Any] = {"g": ctx.g}
|
||||
|
||||
if ctx.has_request:
|
||||
rv["request"] = ctx.request
|
||||
rv["session"] = ctx.session
|
||||
# The session proxy cannot be replaced, accessing it gets
|
||||
# RequestContext.session, which sets session.accessed.
|
||||
|
||||
return rv
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from werkzeug.routing import BuildError
|
|||
from werkzeug.routing import RequestRedirect
|
||||
|
||||
import flask
|
||||
from flask.globals import app_ctx
|
||||
from flask.testing import FlaskClient
|
||||
|
||||
require_cpython_gc = pytest.mark.skipif(
|
||||
|
|
@ -230,27 +231,46 @@ def test_endpoint_decorator(app, client):
|
|||
assert client.get("/foo/bar").data == b"bar"
|
||||
|
||||
|
||||
def test_session(app, client):
|
||||
@app.route("/set", methods=["POST"])
|
||||
def set():
|
||||
assert not flask.session.accessed
|
||||
assert not flask.session.modified
|
||||
def test_session_accessed(app: flask.Flask, client: FlaskClient) -> None:
|
||||
@app.post("/")
|
||||
def do_set():
|
||||
flask.session["value"] = flask.request.form["value"]
|
||||
assert flask.session.accessed
|
||||
assert flask.session.modified
|
||||
return "value set"
|
||||
|
||||
@app.route("/get")
|
||||
def get():
|
||||
assert not flask.session.accessed
|
||||
assert not flask.session.modified
|
||||
v = flask.session.get("value", "None")
|
||||
assert flask.session.accessed
|
||||
assert not flask.session.modified
|
||||
return v
|
||||
@app.get("/")
|
||||
def do_get():
|
||||
return flask.session.get("value", "None")
|
||||
|
||||
assert client.post("/set", data={"value": "42"}).data == b"value set"
|
||||
assert client.get("/get").data == b"42"
|
||||
@app.get("/nothing")
|
||||
def do_nothing() -> str:
|
||||
return ""
|
||||
|
||||
with client:
|
||||
rv = client.get("/nothing")
|
||||
assert "cookie" not in rv.vary
|
||||
assert not app_ctx._session.accessed
|
||||
assert not app_ctx._session.modified
|
||||
|
||||
with client:
|
||||
rv = client.post(data={"value": "42"})
|
||||
assert rv.text == "value set"
|
||||
assert "cookie" in rv.vary
|
||||
assert app_ctx._session.accessed
|
||||
assert app_ctx._session.modified
|
||||
|
||||
with client:
|
||||
rv = client.get()
|
||||
assert rv.text == "42"
|
||||
assert "cookie" in rv.vary
|
||||
assert app_ctx._session.accessed
|
||||
assert not app_ctx._session.modified
|
||||
|
||||
with client:
|
||||
rv = client.get("/nothing")
|
||||
assert rv.text == ""
|
||||
assert "cookie" not in rv.vary
|
||||
assert not app_ctx._session.accessed
|
||||
assert not app_ctx._session.modified
|
||||
|
||||
|
||||
def test_session_path(app, client):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue