mirror of https://github.com/mitsuhiko/flask.git
Armin Ronacher
14 years ago
1 changed files with 168 additions and 0 deletions
@ -0,0 +1,168 @@ |
|||||||
|
# -*- coding: utf-8 -*- |
||||||
|
""" |
||||||
|
flask.sessions |
||||||
|
~~~~~~~~~~~~~~ |
||||||
|
|
||||||
|
Implements cookie based sessions based on Werkzeug's secure cookie |
||||||
|
system. |
||||||
|
|
||||||
|
:copyright: (c) 2010 by Armin Ronacher. |
||||||
|
:license: BSD, see LICENSE for more details. |
||||||
|
""" |
||||||
|
|
||||||
|
from datetime import datetime |
||||||
|
from werkzeug.contrib.securecookie import SecureCookie |
||||||
|
|
||||||
|
|
||||||
|
class SessionMixin(object): |
||||||
|
"""Expands a basic dictionary with an accessors that are expected |
||||||
|
by Flask extensions and users for the session. |
||||||
|
""" |
||||||
|
|
||||||
|
def _get_permanent(self): |
||||||
|
return self.get('_permanent', False) |
||||||
|
|
||||||
|
def _set_permanent(self, value): |
||||||
|
self['_permanent'] = bool(value) |
||||||
|
|
||||||
|
permanent = property(_get_permanent, _set_permanent) |
||||||
|
del _get_permanent, _set_permanent |
||||||
|
|
||||||
|
new = False |
||||||
|
modified = True |
||||||
|
|
||||||
|
|
||||||
|
class SecureCookieSession(SecureCookie, SessionMixin): |
||||||
|
"""Expands the session with support for switching between permanent |
||||||
|
and non-permanent sessions. |
||||||
|
""" |
||||||
|
|
||||||
|
|
||||||
|
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, **kwargs): |
||||||
|
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 |
||||||
|
del _fail |
||||||
|
|
||||||
|
|
||||||
|
class SessionInterface(object): |
||||||
|
"""The basic interface you have to implement in order to replace the |
||||||
|
default session interface which uses werkzeug's securecookie |
||||||
|
implementation. The only methods you have to implement are |
||||||
|
:meth:`open_session` and :meth:`save_session`, the others have |
||||||
|
useful defaults which you don't need to change. |
||||||
|
|
||||||
|
The session object returned by the :meth:`open_session` method has to |
||||||
|
provide a dictionary like interface plus the properties and methods |
||||||
|
from the :class:`SessionMixin`. We recommend just subclassing a dict |
||||||
|
and adding that mixin:: |
||||||
|
|
||||||
|
class Session(dict, SessionMixin): |
||||||
|
pass |
||||||
|
|
||||||
|
If :meth:`open_session` returns `None` Flask will call into |
||||||
|
:meth:`make_null_session` to create a session that acts as replacement |
||||||
|
if the session support cannot work because some requirement is not |
||||||
|
fulfilled. The default :class:`NullSession` class that is created |
||||||
|
will complain that the secret key was not set. |
||||||
|
|
||||||
|
To replace the session interface on an application all you have to do |
||||||
|
is to assign :attr:`flask.Flask.session_interface`:: |
||||||
|
|
||||||
|
app = Flask(__name__) |
||||||
|
app.session_interface = MySessionInterface() |
||||||
|
|
||||||
|
.. versionadded:: 0.7 |
||||||
|
""" |
||||||
|
|
||||||
|
#: :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 |
||||||
|
|
||||||
|
def make_null_session(self, app): |
||||||
|
"""Creates a null session which acts as a replacement object if the |
||||||
|
real session support could not be loaded due to a configuration |
||||||
|
error. This mainly aids the user experience because the job of the |
||||||
|
null session is to still support lookup without complaining but |
||||||
|
modifications are answered with a helpful error message of what |
||||||
|
failed. |
||||||
|
|
||||||
|
This creates an instance of :attr:`null_session_class` by default. |
||||||
|
""" |
||||||
|
return self.null_session_class() |
||||||
|
|
||||||
|
def is_null_session(self, obj): |
||||||
|
"""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_domain(self, app): |
||||||
|
"""Helpful helper method that returns the cookie domain that should |
||||||
|
be used for the session cookie if session cookies are used. |
||||||
|
""" |
||||||
|
if app.config['SERVER_NAME'] is not None: |
||||||
|
# chop of the port which is usually not supported by browsers |
||||||
|
return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] |
||||||
|
|
||||||
|
def get_expiration_time(self, app, session): |
||||||
|
"""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.utcnow() + app.permanent_session_lifetime |
||||||
|
|
||||||
|
def open_session(self, app, request): |
||||||
|
"""This method has to be implemented and must either return `None` |
||||||
|
in case the loading failed because of a configuration error or an |
||||||
|
instance of a session object which implements a dictionary like |
||||||
|
interface + the methods and attributes on :class:`SessionMixin`. |
||||||
|
""" |
||||||
|
raise NotImplementedError() |
||||||
|
|
||||||
|
def save_session(self, app, session, response): |
||||||
|
"""This is called for actual sessions returned by :meth:`open_session` |
||||||
|
at the end of the request. This is still called during a request |
||||||
|
context so if you absolutely need access to the request you can do |
||||||
|
that. |
||||||
|
""" |
||||||
|
raise NotImplementedError() |
||||||
|
|
||||||
|
|
||||||
|
class SecureCookieSessionInterface(SessionInterface): |
||||||
|
"""The cookie session interface that uses the Werkzeug securecookie |
||||||
|
as client side session backend. |
||||||
|
""" |
||||||
|
session_class = SecureCookieSession |
||||||
|
|
||||||
|
def open_session(self, app, request): |
||||||
|
key = app.secret_key |
||||||
|
if key is not None: |
||||||
|
return self.session_class.load_cookie(request, |
||||||
|
app.session_cookie_name, |
||||||
|
secret_key=key) |
||||||
|
|
||||||
|
def save_session(self, app, session, response): |
||||||
|
expires = self.get_expiration_time(app, session) |
||||||
|
domain = self.get_cookie_domain(app) |
||||||
|
if session.modified and not session: |
||||||
|
response.delete_cookie(app.session_cookie_name, |
||||||
|
domain=domain) |
||||||
|
else: |
||||||
|
session.save_cookie(response, app.session_cookie_name, |
||||||
|
expires=expires, httponly=True, domain=domain) |
Loading…
Reference in new issue