Skip to content

Commit 028160e

Browse files
committed
Move parts of the session code to sansio
This will allow it to be used in Quart, thereby reducing duplication, and ensuring the APIs match.
1 parent 6b054f8 commit 028160e

File tree

2 files changed

+225
-203
lines changed

2 files changed

+225
-203
lines changed

src/flask/sansio/sessions.py

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
from __future__ import annotations
2+
3+
import collections.abc as c
4+
import typing as t
5+
from abc import ABCMeta
6+
from collections.abc import MutableMapping
7+
from datetime import datetime
8+
from datetime import timezone
9+
10+
from werkzeug.datastructures import CallbackDict
11+
12+
from .app import App
13+
14+
if t.TYPE_CHECKING: # pragma: no cover
15+
import typing_extensions as te
16+
17+
18+
class SessionMixin(MutableMapping[str, t.Any]):
19+
"""Expands a basic dictionary with session attributes."""
20+
21+
@property
22+
def permanent(self) -> bool:
23+
"""This reflects the ``'_permanent'`` key in the dict."""
24+
return self.get("_permanent", False)
25+
26+
@permanent.setter
27+
def permanent(self, value: bool) -> None:
28+
self["_permanent"] = bool(value)
29+
30+
#: Some implementations can detect whether a session is newly
31+
#: created, but that is not guaranteed. Use with caution. The mixin
32+
# default is hard-coded ``False``.
33+
new = False
34+
35+
#: Some implementations can detect changes to the session and set
36+
#: this when that happens. The mixin default is hard coded to
37+
#: ``True``.
38+
modified = True
39+
40+
#: Some implementations can detect when session data is read or
41+
#: written and set this when that happens. The mixin default is hard
42+
#: coded to ``True``.
43+
accessed = True
44+
45+
46+
class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
47+
"""Base class for sessions based on signed cookies.
48+
49+
This session backend will set the :attr:`modified` and
50+
:attr:`accessed` attributes. It cannot reliably track whether a
51+
session is new (vs. empty), so :attr:`new` remains hard coded to
52+
``False``.
53+
"""
54+
55+
#: When data is changed, this is set to ``True``. Only the session
56+
#: dictionary itself is tracked; if the session contains mutable
57+
#: data (for example a nested dict) then this must be set to
58+
#: ``True`` manually when modifying that data. The session cookie
59+
#: will only be written to the response if this is ``True``.
60+
modified = False
61+
62+
#: When data is read or written, this is set to ``True``. Used by
63+
# :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie``
64+
#: header, which allows caching proxies to cache different pages for
65+
#: different users.
66+
accessed = False
67+
68+
def __init__(
69+
self,
70+
initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None,
71+
) -> None:
72+
def on_update(self: te.Self) -> None:
73+
self.modified = True
74+
self.accessed = True
75+
76+
super().__init__(initial, on_update)
77+
78+
def __getitem__(self, key: str) -> t.Any:
79+
self.accessed = True
80+
return super().__getitem__(key)
81+
82+
def get(self, key: str, default: t.Any = None) -> t.Any:
83+
self.accessed = True
84+
return super().get(key, default)
85+
86+
def setdefault(self, key: str, default: t.Any = None) -> t.Any:
87+
self.accessed = True
88+
return super().setdefault(key, default)
89+
90+
91+
class NullSession(SecureCookieSession):
92+
"""Class used to generate nicer error messages if sessions are not
93+
available. Will still allow read-only access to the empty session
94+
but fail on setting.
95+
"""
96+
97+
def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
98+
raise RuntimeError(
99+
"The session is unavailable because no secret "
100+
"key was set. Set the secret_key on the "
101+
"application to something unique and secret."
102+
)
103+
104+
__setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950
105+
del _fail
106+
107+
108+
class SessionInterface(metaclass=ABCMeta): # noqa: B024
109+
"""This is a SansIO abstract base class used by Flask and Quart to
110+
then define thebasic interface to implement in order to replace
111+
the default session interface of the frameworks.
112+
113+
.. versionadded:: 3.2.0
114+
115+
"""
116+
117+
#: :meth:`make_null_session` will look here for the class that should
118+
#: be created when a null session is requested. Likewise the
119+
#: :meth:`is_null_session` method will perform a typecheck against
120+
#: this type.
121+
null_session_class = NullSession
122+
123+
#: A flag that indicates if the session interface is pickle based.
124+
#: This can be used by Flask extensions to make a decision in regards
125+
#: to how to deal with the session object.
126+
#:
127+
#: .. versionadded:: 0.10
128+
pickle_based = False
129+
130+
def is_null_session(self, obj: object) -> bool:
131+
"""Checks if a given object is a null session. Null sessions are
132+
not asked to be saved.
133+
134+
This checks if the object is an instance of :attr:`null_session_class`
135+
by default.
136+
"""
137+
return isinstance(obj, self.null_session_class)
138+
139+
def get_cookie_name(self, app: App) -> str:
140+
"""The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
141+
return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return]
142+
143+
def get_cookie_domain(self, app: App) -> str | None:
144+
"""The value of the ``Domain`` parameter on the session cookie. If not set,
145+
browsers will only send the cookie to the exact domain it was set from.
146+
Otherwise, they will send it to any subdomain of the given value as well.
147+
148+
Uses the :data:`SESSION_COOKIE_DOMAIN` config.
149+
150+
.. versionchanged:: 2.3
151+
Not set by default, does not fall back to ``SERVER_NAME``.
152+
"""
153+
return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return]
154+
155+
def get_cookie_path(self, app: App) -> str:
156+
"""Returns the path for which the cookie should be valid. The
157+
default implementation uses the value from the ``SESSION_COOKIE_PATH``
158+
config var if it's set, and falls back to ``APPLICATION_ROOT`` or
159+
uses ``/`` if it's ``None``.
160+
"""
161+
return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return]
162+
163+
def get_cookie_httponly(self, app: App) -> bool:
164+
"""Returns True if the session cookie should be httponly. This
165+
currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
166+
config var.
167+
"""
168+
return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return]
169+
170+
def get_cookie_secure(self, app: App) -> bool:
171+
"""Returns True if the cookie should be secure. This currently
172+
just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
173+
"""
174+
return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return]
175+
176+
def get_cookie_samesite(self, app: App) -> str | None:
177+
"""Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
178+
``SameSite`` attribute. This currently just returns the value of
179+
the :data:`SESSION_COOKIE_SAMESITE` setting.
180+
"""
181+
return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return]
182+
183+
def get_cookie_partitioned(self, app: App) -> bool:
184+
"""Returns True if the cookie should be partitioned. By default, uses
185+
the value of :data:`SESSION_COOKIE_PARTITIONED`.
186+
187+
.. versionadded:: 3.1
188+
"""
189+
return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return]
190+
191+
def get_expiration_time(self, app: App, session: SessionMixin) -> datetime | None:
192+
"""A helper method that returns an expiration date for the session
193+
or ``None`` if the session is linked to the browser session. The
194+
default implementation returns now + the permanent session
195+
lifetime configured on the application.
196+
"""
197+
if session.permanent:
198+
return datetime.now(timezone.utc) + app.permanent_session_lifetime
199+
return None
200+
201+
def should_set_cookie(self, app: App, session: SessionMixin) -> bool:
202+
"""Used by session backends to determine if a ``Set-Cookie`` header
203+
should be set for this session cookie for this response. If the session
204+
has been modified, the cookie is set. If the session is permanent and
205+
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
206+
always set.
207+
208+
This check is usually skipped if the session was deleted.
209+
210+
.. versionadded:: 0.11
211+
"""
212+
213+
return session.modified or (
214+
session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
215+
)

0 commit comments

Comments
 (0)