diff --git a/Makefile b/Makefile index 94ad0077..62d763d2 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean-pyc test +.PHONY: clean-pyc test upload-docs all: clean-pyc test diff --git a/docs/api.rst b/docs/api.rst index d285dbfd..3961dc99 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -174,6 +174,13 @@ To access the current session you can use the :class:`session` object: # so mark it as modified yourself session.modified = True + .. attribute:: permanent + + If set to `True` the session life for + :attr:`~flask.Flask.permanent_session_lifetime` seconds. The + default is 31 days. If set to `False` (which is the default) the + session will be deleted when the user closes the browser. + Application Globals ------------------- diff --git a/flask.py b/flask.py index 216f9af8..32b690ca 100644 --- a/flask.py +++ b/flask.py @@ -13,6 +13,7 @@ from __future__ import with_statement import os import sys import types +from datetime import datetime, timedelta from jinja2 import Environment, PackageLoader, FileSystemLoader from werkzeug import Request as RequestBase, Response as ResponseBase, \ @@ -86,7 +87,20 @@ class _RequestGlobals(object): pass -class _NullSession(SecureCookie): +class Session(SecureCookie): + """Expands the session for support for switching between permanent + and non-permanent sessions. + """ + + 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 + + +class _NullSession(Session): """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. @@ -317,6 +331,11 @@ class Flask(object): #: The secure cookie uses this for the name of the session cookie session_cookie_name = 'session' + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + permanent_session_lifetime = timedelta(days=31) + #: options that are passed directly to the Jinja2 environment jinja_options = ImmutableDict( autoescape=True, @@ -493,8 +512,8 @@ class Flask(object): """ key = self.secret_key if key is not None: - return SecureCookie.load_cookie(request, self.session_cookie_name, - secret_key=key) + return Session.load_cookie(request, self.session_cookie_name, + secret_key=key) def save_session(self, session, response): """Saves the session if it needs updates. For the default @@ -505,7 +524,11 @@ class Flask(object): object) :param response: an instance of :attr:`response_class` """ - session.save_cookie(response, self.session_cookie_name) + expires = None + if session.permanent: + expires = datetime.utcnow() + self.permanent_session_lifetime + session.save_cookie(response, self.session_cookie_name, + expires=expires, httponly=True) def add_url_rule(self, rule, endpoint, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` diff --git a/tests/flask_tests.py b/tests/flask_tests.py index b976015a..1759f8b9 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -11,11 +11,14 @@ """ from __future__ import with_statement import os +import re import sys import flask import unittest import tempfile import warnings +from datetime import datetime +from werkzeug import parse_date example_path = os.path.join(os.path.dirname(__file__), '..', 'examples') @@ -118,6 +121,30 @@ class BasicFunctionalityTestCase(unittest.TestCase): expect_exception(flask.session.__setitem__, 'foo', 42) expect_exception(flask.session.pop, 'foo') + def test_session_expiration(self): + permanent = True + app = flask.Flask(__name__) + app.secret_key = 'testkey' + @app.route('/') + def index(): + flask.session['test'] = 42 + flask.session.permanent = permanent + return '' + rv = app.test_client().get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + expires = parse_date(match.group()) + expected = datetime.utcnow() + app.permanent_session_lifetime + assert expires.year == expected.year + assert expires.month == expected.month + assert expires.day == expected.day + + permanent = False + rv = app.test_client().get('/') + assert 'set-cookie' in rv.headers + match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) + assert match is None + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey'