Skip to content

Commit

Permalink
Move parts of the session code to sansio
Browse files Browse the repository at this point in the history
This will allow it to be used in Quart, thereby reducing duplication,
and ensuring the APIs match.
  • Loading branch information
pgjones committed Dec 24, 2024
1 parent 6b054f8 commit 67f12ad
Show file tree
Hide file tree
Showing 2 changed files with 225 additions and 203 deletions.
215 changes: 215 additions & 0 deletions src/flask/sansio/sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
from __future__ import annotations

import collections.abc as c
import typing as t
from abc import ABCMeta
from collections.abc import MutableMapping
from datetime import datetime
from datetime import timezone

from werkzeug.datastructures import CallbackDict

from .app import App

if t.TYPE_CHECKING: # pragma: no cover
import typing_extensions as te


class SessionMixin(MutableMapping[str, t.Any]):
"""Expands a basic dictionary with session attributes."""

@property
def permanent(self) -> bool:
"""This reflects the ``'_permanent'`` key in the dict."""
return self.get("_permanent", False)

@permanent.setter
def permanent(self, value: bool) -> None:
self["_permanent"] = bool(value)

#: Some implementations can detect whether a session is newly
#: created, but that is not guaranteed. Use with caution. The mixin
# default is hard-coded ``False``.
new = False

#: Some implementations can detect changes to the session and set
#: this when that happens. The mixin default is hard coded to
#: ``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


class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):
"""Base class for sessions based on signed cookies.
This session backend will set the :attr:`modified` and
:attr:`accessed` attributes. It cannot reliably track whether a
session is new (vs. empty), so :attr:`new` remains hard coded to
``False``.
"""

#: When data is changed, this is set to ``True``. Only the session
#: dictionary itself is tracked; if the session contains mutable
#: data (for example a nested dict) then this must be set to
#: ``True`` manually when modifying that data. The session cookie
#: 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,
) -> 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
available. Will still allow read-only access to the empty session
but fail on setting.
"""

def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn:
raise RuntimeError(
"The session is unavailable because no secret "
"key was set. Set the secret_key on the "
"application to something unique and secret."
)

__setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950
del _fail


class SessionInterface(metaclass=ABCMeta):
"""This is a SansIO abstract base class used by Flask and Quart to
then define thebasic interface to implement in order to replace
the default session interface of the frameworks.
.. versionadded:: 3.2.0
"""

#: :meth:`make_null_session` will look here for the class that should
#: be created when a null session is requested. Likewise the
#: :meth:`is_null_session` method will perform a typecheck against
#: this type.
null_session_class = NullSession

#: A flag that indicates if the session interface is pickle based.
#: This can be used by Flask extensions to make a decision in regards
#: to how to deal with the session object.
#:
#: .. versionadded:: 0.10
pickle_based = False

def is_null_session(self, obj: object) -> bool:
"""Checks if a given object is a null session. Null sessions are
not asked to be saved.
This checks if the object is an instance of :attr:`null_session_class`
by default.
"""
return isinstance(obj, self.null_session_class)

def get_cookie_name(self, app: App) -> str:
"""The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``."""
return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return]

def get_cookie_domain(self, app: App) -> str | None:
"""The value of the ``Domain`` parameter on the session cookie. If not set,
browsers will only send the cookie to the exact domain it was set from.
Otherwise, they will send it to any subdomain of the given value as well.
Uses the :data:`SESSION_COOKIE_DOMAIN` config.
.. versionchanged:: 2.3
Not set by default, does not fall back to ``SERVER_NAME``.
"""
return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return]

def get_cookie_path(self, app: App) -> str:
"""Returns the path for which the cookie should be valid. The
default implementation uses the value from the ``SESSION_COOKIE_PATH``
config var if it's set, and falls back to ``APPLICATION_ROOT`` or
uses ``/`` if it's ``None``.
"""
return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return]

def get_cookie_httponly(self, app: App) -> bool:
"""Returns True if the session cookie should be httponly. This
currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
config var.
"""
return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return]

def get_cookie_secure(self, app: App) -> bool:
"""Returns True if the cookie should be secure. This currently
just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
"""
return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return]

def get_cookie_samesite(self, app: App) -> str | None:
"""Return ``'Strict'`` or ``'Lax'`` if the cookie should use the
``SameSite`` attribute. This currently just returns the value of
the :data:`SESSION_COOKIE_SAMESITE` setting.
"""
return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return]

def get_cookie_partitioned(self, app: App) -> bool:
"""Returns True if the cookie should be partitioned. By default, uses
the value of :data:`SESSION_COOKIE_PARTITIONED`.
.. versionadded:: 3.1
"""
return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return]

def get_expiration_time(self, app: App, session: SessionMixin) -> datetime | None:
"""A helper method that returns an expiration date for the session
or ``None`` if the session is linked to the browser session. The
default implementation returns now + the permanent session
lifetime configured on the application.
"""
if session.permanent:
return datetime.now(timezone.utc) + app.permanent_session_lifetime
return None

def should_set_cookie(self, app: App, session: SessionMixin) -> bool:
"""Used by session backends to determine if a ``Set-Cookie`` header
should be set for this session cookie for this response. If the session
has been modified, the cookie is set. If the session is permanent and
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
always set.
This check is usually skipped if the session was deleted.
.. versionadded:: 0.11
"""

return session.modified or (
session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"]
)
Loading

0 comments on commit 67f12ad

Please sign in to comment.