From d4415dd6653adb25b89b6276dd140141266ba46b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 1 Jul 2012 12:26:45 +0100 Subject: [PATCH 0001/2743] Fixed an rst syntax error --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 501a2f81..bc40428d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -113,7 +113,7 @@ def stream_with_context(generator_or_function): yield '!' return Response(generate()) - Alternatively it can also be used around a specific generator: + Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response From 56f5224ef7cdc48a05b4ce6dcc37043feab0c0bb Mon Sep 17 00:00:00 2001 From: Laurens Van Houtven <_@lvh.cc> Date: Sun, 1 Jul 2012 19:31:53 +0300 Subject: [PATCH 0002/2743] CHANGES: July 1th should be July 1st --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6d0c4925..5889aa92 100644 --- a/CHANGES +++ b/CHANGES @@ -77,7 +77,7 @@ Released on July 1st 2012, codename Camapri. Version 0.8.1 ------------- -Bugfix release, released on July 1th 2012 +Bugfix release, released on July 1st 2012 - Fixed an issue with the undocumented `flask.session` module to not work properly on Python 2.5. It should not be used but did cause From d8e5a37d8a5e9dda1154c6c7c614bb7a4c42afdd Mon Sep 17 00:00:00 2001 From: esaurito Date: Mon, 2 Jul 2012 00:38:27 +0300 Subject: [PATCH 0003/2743] Fixed codename --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 5889aa92..7d1aeca1 100644 --- a/CHANGES +++ b/CHANGES @@ -6,7 +6,7 @@ Here you can see the full list of changes between each Flask release. Version 0.9 ----------- -Released on July 1st 2012, codename Camapri. +Released on July 1st 2012, codename Campari. - The :func:`flask.Request.on_json_loading_failed` now returns a JSON formatted response by default. From 1beb0f2e7f8c60df41cd7b198c1ed1748a0fea3d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Mon, 2 Jul 2012 09:11:24 +0100 Subject: [PATCH 0004/2743] Fixed a syntax error --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 501a2f81..bc40428d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -113,7 +113,7 @@ def stream_with_context(generator_or_function): yield '!' return Response(generate()) - Alternatively it can also be used around a specific generator: + Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response From 0553bbdefe55709a1fc36dce445f552f398a428e Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Wed, 4 Jul 2012 21:12:41 +0200 Subject: [PATCH 0005/2743] Made it explicitly clear where changes should go in the tutorial --- docs/tutorial/dbinit.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index b546a1a8..15a107cf 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -23,14 +23,17 @@ for you to the application. If you want to do that, you first have to import the :func:`contextlib.closing` function from the contextlib package. If you want to use Python 2.5 it's also necessary to enable the `with` statement -first (`__future__` imports must be the very first import):: +first (`__future__` imports must be the very first import). Accordingly, +the following lines should be to your existing import lines in +`flaskr.py`:: from __future__ import with_statement from contextlib import closing Next we can create a function called `init_db` that initializes the database. For this we can use the `connect_db` function we defined -earlier. Just add that function below the `connect_db` function:: +earlier. Just add that function below the `connect_db` function in +`flask.py`:: def init_db(): with closing(connect_db()) as db: From 690b0c34ff3a9160aad16c613f29b1a8e25ca232 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Wed, 4 Jul 2012 21:19:41 +0200 Subject: [PATCH 0006/2743] Fix chose -> choose typo --- docs/tutorial/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst index c72bbd7d..1dcc40ed 100644 --- a/docs/tutorial/introduction.rst +++ b/docs/tutorial/introduction.rst @@ -3,7 +3,7 @@ Introducing Flaskr ================== -We will call our blogging application flaskr here, feel free to chose a +We will call our blogging application flaskr here, feel free to choose a less web-2.0-ish name ;) Basically we want it to do the following things: 1. let the user sign in and out with credentials specified in the From ea2a0629c9fc3874319e98b3842365ffd889f43d Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Wed, 4 Jul 2012 15:27:15 -0400 Subject: [PATCH 0007/2743] Touch up dbinit tutorial doc. Discussion on #pocoo with svenstaro, dAnjou, noob13. --- docs/tutorial/dbinit.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 15a107cf..79479397 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -24,8 +24,7 @@ If you want to do that, you first have to import the :func:`contextlib.closing` function from the contextlib package. If you want to use Python 2.5 it's also necessary to enable the `with` statement first (`__future__` imports must be the very first import). Accordingly, -the following lines should be to your existing import lines in -`flaskr.py`:: +add the following lines to your existing imports in `flaskr.py`:: from __future__ import with_statement from contextlib import closing From c3f651dccbba801fa07426cef1e6604b579b8fe7 Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Thu, 12 Jul 2012 16:31:53 +0300 Subject: [PATCH 0008/2743] Remove the unused `ScriptNameStripper.to_strip` in the FastCGI doc example. Alernatively, `environ['SCRIPT_NAME'] = ''` should be replaced with something like: if environ['SCRIPT_NAME'].startswith(self.to_strip): environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'][len(self.to_strip):] --- docs/deploying/fastcgi.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 0e2f6cdc..8bce7d74 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -95,8 +95,6 @@ Set yourapplication.fcgi:: from yourapplication import app class ScriptNameStripper(object): - to_strip = '/yourapplication.fcgi' - def __init__(self, app): self.app = app From b9df128ba86ec5bc5513bf38b5484f74866ff447 Mon Sep 17 00:00:00 2001 From: Akai Kitsune Date: Mon, 16 Jul 2012 21:08:02 +0300 Subject: [PATCH 0009/2743] Added directions for mod_wsgi vhost configuration under Apache on Windows --- docs/deploying/mod_wsgi.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index c4cd3d61..8fd2c0bb 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -91,6 +91,20 @@ execute the application under a different user for security reasons: +Note: WSGIDaemonProcess isn't implemented in Windows and Apache will +refuse to run with the above configuration. On a Windows system, eliminate those lines: + +.. sourcecode:: apache + + + ServerName example.com + WSGIScriptAlias / C:\yourdir\yourapp.wsgi + + Order deny,allow + Allow from all + + + For more information consult the `mod_wsgi wiki`_. .. _mod_wsgi: http://code.google.com/p/modwsgi/ From 40ccc0a99aa9c1f975bcc7f69d452afd46fc8b90 Mon Sep 17 00:00:00 2001 From: Randall Degges Date: Fri, 20 Jul 2012 14:29:10 -0700 Subject: [PATCH 0010/2743] Fixing some wording in the design documentation. --- docs/design.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/design.rst b/docs/design.rst index cc247f3b..ee83840e 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -90,9 +90,9 @@ since decorators could be fired in undefined order when the application is split into multiple modules. Another design decision with the Werkzeug routing system is that routes -in Werkzeug try to ensure that there is that URLs are unique. Werkzeug -will go quite far with that in that it will automatically redirect to a -canonical URL if a route is ambiguous. +in Werkzeug try to ensure that URLs are unique. Werkzeug will go quite far +with that in that it will automatically redirect to a canonical URL if a route +is ambiguous. One Template Engine From 3800c7396b20d90ff3a84a3f26c9f12e7365a085 Mon Sep 17 00:00:00 2001 From: Ramiro Gomez Date: Sat, 21 Jul 2012 13:55:45 +0200 Subject: [PATCH 0011/2743] Try to correct confusing sentence in doc and fixed word duplication. --- docs/appcontext.rst | 3 +-- docs/quickstart.rst | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index e9e1ad8f..346cb09b 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -44,8 +44,7 @@ you can have more than one application in the same Python process. So how does the code find the “right” application? In the past we recommended passing applications around explicitly, but that caused issues -with libraries that were not designed with that in mind for libraries for -which it was too inconvenient to make this work. +with libraries that were not designed with that in mind. A common workaround for that problem was to use the :data:`~flask.current_app` proxy later on, which was bound to the current diff --git a/docs/quickstart.rst b/docs/quickstart.rst index e8b71ca9..84ffb488 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -377,7 +377,7 @@ package it's actually inside your package: /hello.html For templates you can use the full power of Jinja2 templates. Head over -to the the official `Jinja2 Template Documentation +to the official `Jinja2 Template Documentation `_ for more information. Here is an example template: From b0fdae4e1f45274e0b399523cca9b55627d9afde Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Sat, 21 Jul 2012 21:00:44 +0200 Subject: [PATCH 0012/2743] Fix regex in lighttpd example config to only match static/ and sub-items, and not all directories that start with 'static' --- docs/deploying/fastcgi.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 0e2f6cdc..f6ba7cee 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -128,7 +128,7 @@ A basic FastCGI configuration for lighttpd looks like that:: ) url.rewrite-once = ( - "^(/static.*)$" => "$1", + "^(/static($|/.*))$" => "$1", "^(/.*)$" => "/yourapplication.fcgi$1" Remember to enable the FastCGI, alias and rewrite modules. This configuration From 20a542fc8ac6dadbcbac92e84fb554e00ccc897a Mon Sep 17 00:00:00 2001 From: Paul McMillan Date: Thu, 26 Jul 2012 09:56:01 -0700 Subject: [PATCH 0013/2743] docstring typo --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 8460f476..9cac4025 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1446,7 +1446,7 @@ class Flask(_PackageBoundObject): .. versionchanged:: 0.9 This can now also be called without a request object when the - UR adapter is created for the application context. + URL adapter is created for the application context. """ if request is not None: return self.url_map.bind_to_environ(request.environ, From ed1619adadc56641b8ab1ba2c03d45023960aaf2 Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Wed, 1 Aug 2012 11:27:28 +0300 Subject: [PATCH 0014/2743] Docs: Mention SERVER_NAME in the url_for() docstring --- flask/helpers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 501a2f81..2e8d0da1 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -305,7 +305,9 @@ def url_for(endpoint, **values): :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule - :param _external: if set to `True`, an absolute URL is generated. + :param _external: if set to `True`, an absolute URL is generated. Server + address can be changed via `SERVER_NAME` configuration variable which + defaults to `localhost`. :param _anchor: if provided this is added as anchor to the URL. :param _method: if provided this explicitly specifies an HTTP method. """ From e3b3e05052ea36456cb557b8799b7b0ed00865bd Mon Sep 17 00:00:00 2001 From: Priit Laes Date: Wed, 1 Aug 2012 11:29:40 +0300 Subject: [PATCH 0015/2743] Docs: Fix docstring formatting --- flask/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 2e8d0da1..7e20c97d 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -113,7 +113,7 @@ def stream_with_context(generator_or_function): yield '!' return Response(generate()) - Alternatively it can also be used around a specific generator: + Alternatively it can also be used around a specific generator:: from flask import stream_with_context, request, Response From 4df3bf2058954624f9376fd16774a769299dc40a Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 11 Aug 2012 02:36:14 +0100 Subject: [PATCH 0016/2743] Implemented experimental JSON based sessions --- CHANGES | 9 +++++ docs/api.rst | 7 ++++ docs/upgrading.rst | 51 +++++++++++++++++++++++++ flask/sessions.py | 80 +++++++++++++++++++++++++++++++++++++++- flask/testsuite/basic.py | 26 +++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 6d0c4925..1948b5ab 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,15 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.10 +------------ + +Release date to be decided. + +- Changed default cookie serialization format from pickle to JSON to + limit the impact an attacker can do if the secret key leaks. See + :ref:`upgrading-to-010` for more information. + Version 0.9 ----------- diff --git a/docs/api.rst b/docs/api.rst index 8a7b5ce0..e808e771 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -215,6 +215,13 @@ implementation that Flask is using. .. autoclass:: SecureCookieSessionInterface :members: +.. autoclass:: UpgradeSecureCookieSessionInterface + +.. autoclass:: SecureCookieSession + :members: + +.. autoclass:: UpgradeSecureCookieSession + .. autoclass:: NullSession :members: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7226d60e..01393983 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -19,6 +19,57 @@ installation, make sure to pass it the ``-U`` parameter:: $ easy_install -U Flask +.. _upgrading-to-010: + +Version 0.10 +------------ + +The biggest change going from 0.9 to 0.10 is that the cookie serialization +format changed from pickle to a specialized JSON format. This change has +been done in order to avoid the damage an attacker can do if the secret +key is leaked. When you upgrade you will notice two major changes: all +sessions that were issued before the upgrade are invalidated and you can +only store a limited amount of types in the session. There are two ways +to avoid these problems on upgrading: + +Automatically Upgrade Sessions +`````````````````````````````` + +The first method is to allow pickle based sessions for a limited amount of +time. This can be done by using the +:class:`~flask.sessions.UpgradeSecureCookieSession` session +implementation:: + + from flask import Flask + from flask.sessions import UpgradeSecureCookieSessionInterface + + app = Flask(__name__) + app.session_interface = UpgradeSecureCookieSessionInterface + +For as long as this class is being used both pickle and json sessions are +supported but changes are written in JSON format only. + +Revert to Pickle Sessions +````````````````````````` + +You can also revert to pickle based sessions if you want:: + + import pickle + from flask import Flask + from flask.sessions import SecureCookieSession, \ + SecureCookieSessionInterface + + class PickleSessionInterface(SecureCookieSessionInterface): + class session_class(SecureCookieSession): + serialization_method = pickle + + app = Flask(__name__) + app.session_interface = PickleSessionInterface + +If you want to continue to use pickle based data we strongly recommend +switching to a server side session store however. + + Version 0.9 ----------- diff --git a/flask/sessions.py b/flask/sessions.py index 2795bb1f..75f4a614 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -10,8 +10,12 @@ :license: BSD, see LICENSE for more details. """ +import cPickle as pickle from datetime import datetime from werkzeug.contrib.securecookie import SecureCookie +from werkzeug.http import http_date, parse_date +from .helpers import json, _assert_have_json +from . import Markup class SessionMixin(object): @@ -41,10 +45,74 @@ class SessionMixin(object): modified = True +class TaggedJSONSerializer(object): + """A customized JSON serializer that supports a few extra types that + we take for granted when serializing (tuples, markup objects, datetime). + """ + + def dumps(self, value): + if __debug__: + _assert_have_json() + def _tag(value): + if isinstance(value, tuple): + return {'##t': [_tag(x) for x in value]} + elif callable(getattr(value, '__html__', None)): + return {'##m': unicode(value.__html__())} + elif isinstance(value, list): + return [_tag(x) for x in value] + elif isinstance(value, datetime): + return {'##d': http_date(value)} + elif isinstance(value, dict): + return dict((k, _tag(v)) for k, v in value.iteritems()) + return value + return json.dumps(_tag(value), separators=(',', ':')) + + def loads(self, value): + if __debug__: + _assert_have_json() + def object_hook(obj): + if len(obj) != 1: + return obj + the_key, the_value = obj.iteritems().next() + if the_key == '##t': + return tuple(the_value) + elif the_key == '##m': + return Markup(the_value) + elif the_key == '##d': + return parse_date(the_value) + return obj + return json.loads(value, object_hook=object_hook) + + +session_json_serializer = TaggedJSONSerializer() + + class SecureCookieSession(SecureCookie, SessionMixin): """Expands the session with support for switching between permanent - and non-permanent sessions. + and non-permanent sessions and changes the default pickle based + serialization format to a tagged json one. """ + serialization_method = session_json_serializer + + +class _UpgradeSerializer(object): + def dumps(self, value): + return session_json_serializer.dumps(value) + def loads(self, value): + try: + return session_json_serializer.loads(value) + except Exception: + return pickle.loads(value) + + +class UpgradeSecureCookieSession(SecureCookieSession): + """This cookie sesion implementation tries json first but will also + support pickle based session. This exists mainly to upgrade existing + pickle based users transparently to json. + + .. versionadded:: 0.10 + """ + serialization_method = _UpgradeSerializer() class NullSession(SecureCookieSession): @@ -203,3 +271,13 @@ class SecureCookieSessionInterface(SessionInterface): session.save_cookie(response, app.session_cookie_name, path=path, expires=expires, httponly=httponly, secure=secure, domain=domain) + + +class UpgradeSecureCookieSessionInterface(SecureCookieSessionInterface): + """This session interface works exactly like the regular one but uses + the :class:`UpgradeSecureCookieSession` classes to upgrade from pickle + sessions to JSON sessions. + + .. versionadded:: 0.10 + """ + session_class = UpgradeSecureCookieSession diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index 388b5a8e..3d758b3a 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -13,6 +13,7 @@ from __future__ import with_statement import re import flask +import pickle import unittest from datetime import datetime from threading import Thread @@ -297,6 +298,31 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(c.get('/').data, 'None') self.assert_equal(c.get('/').data, '42') + def test_session_special_types(self): + app = flask.Flask(__name__) + app.secret_key = 'development-key' + app.testing = True + now = datetime.utcnow().replace(microsecond=0) + + @app.after_request + def modify_session(response): + flask.session['m'] = flask.Markup('Hello!') + flask.session['dt'] = now + flask.session['t'] = (1, 2, 3) + return response + + @app.route('/') + def dump_session_contents(): + return pickle.dumps(dict(flask.session)) + + c = app.test_client() + c.get('/') + rv = pickle.loads(c.get('/').data) + self.assert_equal(rv['m'], flask.Markup('Hello!')) + self.assert_equal(type(rv['m']), flask.Markup) + self.assert_equal(rv['dt'], now) + self.assert_equal(rv['t'], (1, 2, 3)) + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey' From ee28dcf2cff53aae0c88db34ff872e0b042c6bf5 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 11 Aug 2012 02:36:29 +0100 Subject: [PATCH 0017/2743] Added changelog entry for 0.10 --- CHANGES | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES b/CHANGES index 6d0c4925..625d22dd 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,11 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.10 +------------ + +Release date to be decided. + Version 0.9 ----------- From b87919348104774e9c959fc00976966e38161129 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 11 Aug 2012 02:37:03 +0100 Subject: [PATCH 0018/2743] Set current dev version number to 0.10 --- flask/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index b170ba5f..196eb033 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.9' +__version__ = '0.10-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/setup.py b/setup.py index 9b185fc0..1d0761fe 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ class run_audit(Command): setup( name='Flask', - version='0.9', + version='0.10-dev', url='http://github.com/mitsuhiko/flask/', license='BSD', author='Armin Ronacher', From 3f82d1b68ea6f5bf2970c2df8ff5cf991439a9bf Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 11 Aug 2012 03:09:14 +0100 Subject: [PATCH 0019/2743] Switch to itsdangerous --- flask/sessions.py | 100 +++++++++++++++++++--------------------------- setup.py | 3 +- 2 files changed, 44 insertions(+), 59 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 75f4a614..3db1fb60 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -10,13 +10,14 @@ :license: BSD, see LICENSE for more details. """ -import cPickle as pickle from datetime import datetime -from werkzeug.contrib.securecookie import SecureCookie from werkzeug.http import http_date, parse_date -from .helpers import json, _assert_have_json +from werkzeug.datastructures import CallbackDict +from .helpers import json from . import Markup +from itsdangerous import URLSafeTimedSerializer, BadSignature + class SessionMixin(object): """Expands a basic dictionary with an accessors that are expected @@ -51,8 +52,6 @@ class TaggedJSONSerializer(object): """ def dumps(self, value): - if __debug__: - _assert_have_json() def _tag(value): if isinstance(value, tuple): return {'##t': [_tag(x) for x in value]} @@ -68,8 +67,6 @@ class TaggedJSONSerializer(object): return json.dumps(_tag(value), separators=(',', ':')) def loads(self, value): - if __debug__: - _assert_have_json() def object_hook(obj): if len(obj) != 1: return obj @@ -87,32 +84,14 @@ class TaggedJSONSerializer(object): session_json_serializer = TaggedJSONSerializer() -class SecureCookieSession(SecureCookie, SessionMixin): - """Expands the session with support for switching between permanent - and non-permanent sessions and changes the default pickle based - serialization format to a tagged json one. - """ - serialization_method = session_json_serializer +class SecureCookieSession(CallbackDict, SessionMixin): + """Baseclass for sessions based on signed cookies.""" - -class _UpgradeSerializer(object): - def dumps(self, value): - return session_json_serializer.dumps(value) - def loads(self, value): - try: - return session_json_serializer.loads(value) - except Exception: - return pickle.loads(value) - - -class UpgradeSecureCookieSession(SecureCookieSession): - """This cookie sesion implementation tries json first but will also - support pickle based session. This exists mainly to upgrade existing - pickle based users transparently to json. - - .. versionadded:: 0.10 - """ - serialization_method = _UpgradeSerializer() + def __init__(self, initial=None): + def on_update(self): + self.modified = True + CallbackDict.__init__(self, initial, on_update) + self.modified = False class NullSession(SecureCookieSession): @@ -246,38 +225,43 @@ class SessionInterface(object): class SecureCookieSessionInterface(SessionInterface): - """The cookie session interface that uses the Werkzeug securecookie - as client side session backend. - """ + salt = 'cookie-session' session_class = SecureCookieSession + serializer = session_json_serializer + + def get_serializer(self, app): + if not app.secret_key: + return None + return URLSafeTimedSerializer(app.secret_key, + salt=self.salt, + serializer=self.serializer) 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) + s = self.get_serializer(app) + if s is None: + return None + val = request.cookies.get(app.session_cookie_name) + if not val: + return self.session_class() + max_age = app.permanent_session_lifetime.total_seconds() + try: + data = s.loads(val, max_age=max_age) + return self.session_class(data) + except BadSignature: + return self.session_class() def save_session(self, app, session, response): - expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) - if session.modified and not session: - response.delete_cookie(app.session_cookie_name, path=path, - domain=domain) - else: - session.save_cookie(response, app.session_cookie_name, path=path, - expires=expires, httponly=httponly, - secure=secure, domain=domain) - - -class UpgradeSecureCookieSessionInterface(SecureCookieSessionInterface): - """This session interface works exactly like the regular one but uses - the :class:`UpgradeSecureCookieSession` classes to upgrade from pickle - sessions to JSON sessions. - - .. versionadded:: 0.10 - """ - session_class = UpgradeSecureCookieSession + if not session: + if session.modified: + response.delete_cookie(app.session_cookie_name, + domain=domain, path=path) + return + expires = self.get_expiration_time(app, session) + val = self.get_serializer(app).dumps(dict(session)) + response.set_cookie(app.session_cookie_name, val, + expires=expires, httponly=httponly, + domain=domain, path=path, secure=secure) diff --git a/setup.py b/setup.py index 1d0761fe..0225e5fc 100644 --- a/setup.py +++ b/setup.py @@ -91,7 +91,8 @@ setup( platforms='any', install_requires=[ 'Werkzeug>=0.7', - 'Jinja2>=2.4' + 'Jinja2>=2.4', + 'itsdangerous>=0.16' ], classifiers=[ 'Development Status :: 4 - Beta', From c3d38a21c664440fb8284aaaf8fbcce4a1c0849f Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 11 Aug 2012 03:11:40 +0100 Subject: [PATCH 0020/2743] Removed json_available hack --- flask/__init__.py | 6 +++--- flask/helpers.py | 31 ++++--------------------------- flask/wrappers.py | 4 +--- 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/flask/__init__.py b/flask/__init__.py index 196eb033..ac1a3a00 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -20,7 +20,7 @@ from jinja2 import Markup, escape from .app import Flask, Request, Response from .config import Config -from .helpers import url_for, jsonify, json_available, flash, \ +from .helpers import url_for, jsonify, flash, \ send_file, send_from_directory, get_flashed_messages, \ get_template_attribute, make_response, safe_join, \ stream_with_context @@ -37,8 +37,8 @@ from .signals import signals_available, template_rendered, request_started, \ request_finished, got_request_exception, request_tearing_down # only import json if it's available -if json_available: - from .helpers import json +from .helpers import json # backwards compat, goes away in 1.0 from .sessions import SecureCookieSession as Session +json_available = True diff --git a/flask/helpers.py b/flask/helpers.py index 7e20c97d..b2d34a71 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -23,21 +23,9 @@ from werkzeug.routing import BuildError from werkzeug.urls import url_quote from functools import update_wrapper -# try to load the best simplejson implementation available. If JSON -# is not installed, we add a failing class. -json_available = True -json = None -try: - import simplejson as json -except ImportError: - try: - import json - except ImportError: - try: - # Google Appengine offers simplejson via django - from django.utils import simplejson as json - except ImportError: - json_available = False +# Use the same json implementation as itsdangerous on which we +# depend anyways. +from itsdangerous import simplejson as json from werkzeug.datastructures import Headers @@ -55,19 +43,10 @@ from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request -def _assert_have_json(): - """Helper function that fails if JSON is unavailable.""" - if not json_available: - raise RuntimeError('simplejson not installed') - - # figure out if simplejson escapes slashes. This behavior was changed # from one version to another without reason. -if not json_available or '\\/' not in json.dumps('/'): - +if '\\/' not in json.dumps('/'): def _tojson_filter(*args, **kwargs): - if __debug__: - _assert_have_json() return json.dumps(*args, **kwargs).replace('/', '\\/') else: _tojson_filter = json.dumps @@ -192,8 +171,6 @@ def jsonify(*args, **kwargs): .. versionadded:: 0.2 """ - if __debug__: - _assert_have_json() return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') diff --git a/flask/wrappers.py b/flask/wrappers.py index 3ee718ff..060ee25b 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -14,7 +14,7 @@ from werkzeug.utils import cached_property from .exceptions import JSONBadRequest from .debughelpers import attach_enctype_error_multidict -from .helpers import json, _assert_have_json +from .helpers import json from .globals import _request_ctx_stack @@ -95,8 +95,6 @@ class Request(RequestBase): This requires Python 2.6 or an installed version of simplejson. """ - if __debug__: - _assert_have_json() if self.mimetype == 'application/json': request_charset = self.mimetype_params.get('charset') try: From a4977cfe2b57218579ca224af7cfea0864e6665b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 11 Aug 2012 03:13:16 +0100 Subject: [PATCH 0021/2743] Removed outdated section in the docs --- docs/api.rst | 4 ---- docs/upgrading.rst | 41 ++--------------------------------------- 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e808e771..316c76ab 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -215,13 +215,9 @@ implementation that Flask is using. .. autoclass:: SecureCookieSessionInterface :members: -.. autoclass:: UpgradeSecureCookieSessionInterface - .. autoclass:: SecureCookieSession :members: -.. autoclass:: UpgradeSecureCookieSession - .. autoclass:: NullSession :members: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 01393983..c295fb1c 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -29,46 +29,9 @@ format changed from pickle to a specialized JSON format. This change has been done in order to avoid the damage an attacker can do if the secret key is leaked. When you upgrade you will notice two major changes: all sessions that were issued before the upgrade are invalidated and you can -only store a limited amount of types in the session. There are two ways -to avoid these problems on upgrading: - -Automatically Upgrade Sessions -`````````````````````````````` - -The first method is to allow pickle based sessions for a limited amount of -time. This can be done by using the -:class:`~flask.sessions.UpgradeSecureCookieSession` session -implementation:: - - from flask import Flask - from flask.sessions import UpgradeSecureCookieSessionInterface - - app = Flask(__name__) - app.session_interface = UpgradeSecureCookieSessionInterface - -For as long as this class is being used both pickle and json sessions are -supported but changes are written in JSON format only. - -Revert to Pickle Sessions -````````````````````````` - -You can also revert to pickle based sessions if you want:: - - import pickle - from flask import Flask - from flask.sessions import SecureCookieSession, \ - SecureCookieSessionInterface - - class PickleSessionInterface(SecureCookieSessionInterface): - class session_class(SecureCookieSession): - serialization_method = pickle - - app = Flask(__name__) - app.session_interface = PickleSessionInterface - -If you want to continue to use pickle based data we strongly recommend -switching to a server side session store however. +only store a limited amount of types in the session. +TODO: add external module for session upgrading Version 0.9 ----------- From fe85970665ea3a38f9c6a8ef4756ff3a913850b6 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sat, 11 Aug 2012 03:38:46 +0100 Subject: [PATCH 0022/2743] Various improvements in regards to the itsdangerous usage, bumped to 0.17 --- flask/sessions.py | 55 ++++++++++++++++++++++++++++++++++------------- setup.py | 2 +- 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 3db1fb60..ba0b0ed7 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -10,6 +10,7 @@ :license: BSD, see LICENSE for more details. """ +import hashlib from datetime import datetime from werkzeug.http import http_date, parse_date from werkzeug.datastructures import CallbackDict @@ -54,13 +55,13 @@ class TaggedJSONSerializer(object): def dumps(self, value): def _tag(value): if isinstance(value, tuple): - return {'##t': [_tag(x) for x in value]} + return {' t': [_tag(x) for x in value]} elif callable(getattr(value, '__html__', None)): - return {'##m': unicode(value.__html__())} + return {' m': unicode(value.__html__())} elif isinstance(value, list): return [_tag(x) for x in value] elif isinstance(value, datetime): - return {'##d': http_date(value)} + return {' d': http_date(value)} elif isinstance(value, dict): return dict((k, _tag(v)) for k, v in value.iteritems()) return value @@ -71,11 +72,11 @@ class TaggedJSONSerializer(object): if len(obj) != 1: return obj the_key, the_value = obj.iteritems().next() - if the_key == '##t': + if the_key == ' t': return tuple(the_value) - elif the_key == '##m': + elif the_key == ' m': return Markup(the_value) - elif the_key == '##d': + elif the_key == ' d': return parse_date(the_value) return obj return json.loads(value, object_hook=object_hook) @@ -145,6 +146,13 @@ class SessionInterface(object): #: 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 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 @@ -225,19 +233,36 @@ class SessionInterface(object): class SecureCookieSessionInterface(SessionInterface): + """The default session interface that stores sessions in signed cookies + through the :mod:`itsdangerous` module. + """ + #: the salt that should be applied on top of the secret key for the + #: signing of cookie based sessions. salt = 'cookie-session' - session_class = SecureCookieSession + #: the hash function to use for the signature. The default is sha1 + digest_method = staticmethod(hashlib.sha1) + #: the name of the itsdangerous supported key derivation. The default + #: is hmac. + key_derivation = 'hmac' + #: A python serializer for the payload. The default is a compact + #: JSON derived serializer with support for some extra Python types + #: such as datetime objects or tuples. serializer = session_json_serializer + session_class = SecureCookieSession - def get_serializer(self, app): + def get_signing_serializer(self, app): if not app.secret_key: return None - return URLSafeTimedSerializer(app.secret_key, - salt=self.salt, - serializer=self.serializer) + signer_kwargs = dict( + key_derivation=self.key_derivation, + digest_method=self.digest_method + ) + return URLSafeTimedSerializer(app.secret_key, salt=self.salt, + serializer=self.serializer, + signer_kwargs=signer_kwargs) def open_session(self, app, request): - s = self.get_serializer(app) + s = self.get_signing_serializer(app) if s is None: return None val = request.cookies.get(app.session_cookie_name) @@ -253,15 +278,15 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) - httponly = self.get_cookie_httponly(app) - secure = self.get_cookie_secure(app) if not session: if session.modified: response.delete_cookie(app.session_cookie_name, domain=domain, path=path) return + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) expires = self.get_expiration_time(app, session) - val = self.get_serializer(app).dumps(dict(session)) + val = self.get_signing_serializer(app).dumps(dict(session)) response.set_cookie(app.session_cookie_name, val, expires=expires, httponly=httponly, domain=domain, path=path, secure=secure) diff --git a/setup.py b/setup.py index 0225e5fc..4a9182b9 100644 --- a/setup.py +++ b/setup.py @@ -92,7 +92,7 @@ setup( install_requires=[ 'Werkzeug>=0.7', 'Jinja2>=2.4', - 'itsdangerous>=0.16' + 'itsdangerous>=0.17' ], classifiers=[ 'Development Status :: 4 - Beta', From 77c2c3b183cf54f4cd678ccb4545246ea33fc52b Mon Sep 17 00:00:00 2001 From: Ben Rousch Date: Fri, 17 Aug 2012 16:45:04 -0300 Subject: [PATCH 0023/2743] Added _ to fix link to extensions in Hook. Extend. I missed the trailing _ to make a link in my first patch to add the link to extensions. Sorry about that. --- docs/becomingbig.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 8d531620..62f456bd 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -26,7 +26,7 @@ response objects. Dig deeper on the APIs you use, and look for the customizations which are available out of the box in a Flask release. Look for ways in which your project can be refactored into a collection of utilities and Flask extensions. Explore the many `extensions -` in the community, and look for patterns to +`_ in the community, and look for patterns to build your own extensions if you do not find the tools you need. Subclass. From 11c746189dd8b49740a833d0e8428a85b6174799 Mon Sep 17 00:00:00 2001 From: Alex Morega Date: Mon, 20 Aug 2012 16:57:18 +0300 Subject: [PATCH 0024/2743] Fix code example in pluggable views documentation --- docs/views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/views.rst b/docs/views.rst index 02c62704..210d8f18 100644 --- a/docs/views.rst +++ b/docs/views.rst @@ -36,7 +36,7 @@ based view you would do this:: users = User.query.all() return render_template('users.html', objects=users) - app.add_url_rule('/users/', ShowUsers.as_view('show_users')) + app.add_url_rule('/users/', view_func=ShowUsers.as_view('show_users')) As you can see what you have to do is to create a subclass of :class:`flask.views.View` and implement From 8d330a368a7f0aa6ba8508cb2b4b64f6415a0c7b Mon Sep 17 00:00:00 2001 From: d1ffuz0r Date: Wed, 29 Aug 2012 22:26:39 +0400 Subject: [PATCH 0025/2743] fixed issue #524 --- scripts/flaskext_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/flaskext_compat.py b/scripts/flaskext_compat.py index cb0b436c..050fd7e0 100644 --- a/scripts/flaskext_compat.py +++ b/scripts/flaskext_compat.py @@ -44,7 +44,7 @@ class ExtensionImporter(object): def install(self): sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] - def find_module(self, fullname, path=None): + def find_module(self, fullname): if fullname.startswith(self.prefix): return self From 69e419e020ea731a5bf4d4e95a6227761f01bcc7 Mon Sep 17 00:00:00 2001 From: pinchsp Date: Tue, 4 Sep 2012 00:51:45 -0300 Subject: [PATCH 0026/2743] Fixed small mistake in sqlalchemy pattern --- docs/patterns/sqlalchemy.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/patterns/sqlalchemy.rst b/docs/patterns/sqlalchemy.rst index 270fa79e..c6200187 100644 --- a/docs/patterns/sqlalchemy.rst +++ b/docs/patterns/sqlalchemy.rst @@ -42,7 +42,7 @@ Here the example `database.py` module for your application:: engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True) db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, - bind=engine)) + bind=engine)) Base = declarative_base() Base.query = db_session.query_property() @@ -130,7 +130,7 @@ Here is an example `database.py` module for your application:: metadata = MetaData() db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, - bind=engine)) + bind=engine)) def init_db(): metadata.create_all(bind=engine) @@ -189,7 +189,7 @@ To insert data you can use the `insert` method. We have to get a connection first so that we can use a transaction: >>> con = engine.connect() ->>> con.execute(users.insert(name='admin', email='admin@localhost')) +>>> con.execute(users.insert(), name='admin', email='admin@localhost') SQLAlchemy will automatically commit for us. From 48f7cdd01699fdc4048b18d827fccfbf9f266c9f Mon Sep 17 00:00:00 2001 From: Finbarr O'Callaghan Date: Thu, 6 Sep 2012 18:04:51 +0100 Subject: [PATCH 0027/2743] various typo fixes --- docs/appcontext.rst | 4 ++-- docs/upgrading.rst | 2 +- flask/app.py | 18 +++++++++--------- flask/exthook.py | 2 +- flask/testsuite/subclassing.py | 8 ++++---- flask/wrappers.py | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 346cb09b..928ffc8a 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -37,7 +37,7 @@ context local. Purpose of the Application Context ---------------------------------- -The main reason for the application's context existance is that in the +The main reason for the application's context existence is that in the past a bunch of functionality was attached to the request context in lack of a better solution. Since one of the pillar's of Flask's design is that you can have more than one application in the same Python process. @@ -58,7 +58,7 @@ Creating an Application Context To make an application context there are two ways. The first one is the implicit one: whenever a request context is pushed, an application context will be created alongside if this is necessary. As a result of that, you -can ignore the existance of the application context unless you need it. +can ignore the existence of the application context unless you need it. The second way is the explicit way using the :meth:`~flask.Flask.app_context` method:: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 7226d60e..f3e3b021 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -191,7 +191,7 @@ Manual Error Handler Attaching While it is still possible to attach error handlers to :attr:`Flask.error_handlers` it's discouraged to do so and in fact -deprecated. In generaly we no longer recommend custom error handler +deprecated. In generally we no longer recommend custom error handler attaching via assignments to the underlying dictionary due to the more complex internal handling to support arbitrary exception classes and blueprints. See :meth:`Flask.errorhandler` for more information. diff --git a/flask/app.py b/flask/app.py index f8526d5c..68767d03 100644 --- a/flask/app.py +++ b/flask/app.py @@ -541,8 +541,8 @@ class Flask(_PackageBoundObject): Here some examples:: app.logger.debug('A value for debugging') - app.logger.warning('A warning ocurred (%d apples)', 42) - app.logger.error('An error occoured') + app.logger.warning('A warning occurred (%d apples)', 42) + app.logger.error('An error occurred') .. versionadded:: 0.3 """ @@ -846,7 +846,7 @@ class Flask(_PackageBoundObject): first_registration = False if blueprint.name in self.blueprints: assert self.blueprints[blueprint.name] is blueprint, \ - 'A blueprint\'s name collision ocurred between %r and ' \ + 'A blueprint\'s name collision occurred between %r and ' \ '%r. Both share the same name "%s". Blueprints that ' \ 'are created on the fly need unique names.' % \ (blueprint, self.blueprints[blueprint.name], blueprint.name) @@ -1108,7 +1108,7 @@ class Flask(_PackageBoundObject): a new response object or the same (see :meth:`process_response`). As of Flask 0.7 this function might not be executed at the end of the - request in case an unhandled exception ocurred. + request in case an unhandled exception occurred. """ self.after_request_funcs.setdefault(None, []).append(f) return f @@ -1132,10 +1132,10 @@ class Flask(_PackageBoundObject): stack of active contexts. This becomes relevant if you are using such constructs in tests. - Generally teardown functions must take every necesary step to avoid + Generally teardown functions must take every necessary step to avoid that they will fail. If they do execute code that might fail they will have to surround the execution of these code by try/except - statements and log ocurring errors. + statements and log occurring errors. When a teardown function was called because of a exception it will be passed an error object. @@ -1265,7 +1265,7 @@ class Flask(_PackageBoundObject): def handle_exception(self, e): """Default exception handling that kicks in when an exception - occours that is not caught. In debug mode the exception will + occurs that is not caught. In debug mode the exception will be re-raised immediately, otherwise it is logged and the handler for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. @@ -1522,7 +1522,7 @@ class Flask(_PackageBoundObject): request handling is stopped. This also triggers the :meth:`url_value_processor` functions before - the actualy :meth:`before_request` functions are called. + the actually :meth:`before_request` functions are called. """ bp = _request_ctx_stack.top.request.blueprint @@ -1675,7 +1675,7 @@ class Flask(_PackageBoundObject): The behavior of the before and after request callbacks was changed under error conditions and a new callback was added that will always execute at the end of the request, independent on if an - error ocurred or not. See :ref:`callbacks-and-errors`. + error occurred or not. See :ref:`callbacks-and-errors`. :param environ: a WSGI environment :param start_response: a callable accepting a status code, diff --git a/flask/exthook.py b/flask/exthook.py index bb1deb29..26578f0f 100644 --- a/flask/exthook.py +++ b/flask/exthook.py @@ -110,7 +110,7 @@ class ExtensionImporter(object): if module_name == important_module: return True - # Some python verisons will will clean up modules so early that the + # Some python versions will will clean up modules so early that the # module name at that point is no longer set. Try guessing from # the filename then. filename = os.path.abspath(tb.tb_frame.f_code.co_filename) diff --git a/flask/testsuite/subclassing.py b/flask/testsuite/subclassing.py index e56ad563..89aa9150 100644 --- a/flask/testsuite/subclassing.py +++ b/flask/testsuite/subclassing.py @@ -18,14 +18,14 @@ from flask.testsuite import FlaskTestCase class FlaskSubclassingTestCase(FlaskTestCase): - def test_supressed_exception_logging(self): - class SupressedFlask(flask.Flask): + def test_suppressed_exception_logging(self): + class SuppressedFlask(flask.Flask): def log_exception(self, exc_info): pass out = StringIO() - app = SupressedFlask(__name__) - app.logger_name = 'flask_tests/test_supressed_exception_logging' + app = SuppressedFlask(__name__) + app.logger_name = 'flask_tests/test_suppressed_exception_logging' app.logger.addHandler(StreamHandler(out)) @app.route('/') diff --git a/flask/wrappers.py b/flask/wrappers.py index 3ee718ff..50dfdc2a 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -108,7 +108,7 @@ class Request(RequestBase): def on_json_loading_failed(self, e): """Called if decoding of the JSON data failed. The return value of - this method is used by :attr:`json` when an error ocurred. The default + this method is used by :attr:`json` when an error occurred. The default implementation raises a :class:`JSONBadRequest`, which is a subclass of :class:`~werkzeug.exceptions.BadRequest` which sets the ``Content-Type`` to ``application/json`` and provides a JSON-formatted From e8d50a7abafff3306c5076f10d82b66224c0ebd4 Mon Sep 17 00:00:00 2001 From: Finbarr O'Callaghan Date: Thu, 6 Sep 2012 18:19:50 +0100 Subject: [PATCH 0028/2743] fixed spelling but not the grammar! --- docs/upgrading.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index f3e3b021..77c78ae4 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -191,7 +191,7 @@ Manual Error Handler Attaching While it is still possible to attach error handlers to :attr:`Flask.error_handlers` it's discouraged to do so and in fact -deprecated. In generally we no longer recommend custom error handler +deprecated. In general we no longer recommend custom error handler attaching via assignments to the underlying dictionary due to the more complex internal handling to support arbitrary exception classes and blueprints. See :meth:`Flask.errorhandler` for more information. From e93447f25e5f0cf0c10c32daa10b02e23ba2bc1a Mon Sep 17 00:00:00 2001 From: Finbarr O'Callaghan Date: Thu, 6 Sep 2012 18:30:41 +0100 Subject: [PATCH 0029/2743] actually to actual, again, fixed spelling, not grammar --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 68767d03..b47e5a69 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1522,7 +1522,7 @@ class Flask(_PackageBoundObject): request handling is stopped. This also triggers the :meth:`url_value_processor` functions before - the actually :meth:`before_request` functions are called. + the actual :meth:`before_request` functions are called. """ bp = _request_ctx_stack.top.request.blueprint From 9ecbd20286aebcd2040cf7463d5288726787c1f3 Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Thu, 13 Sep 2012 15:16:38 -0300 Subject: [PATCH 0030/2743] Update flask/templating.py Fixed a typo in the docstring. --- flask/templating.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flask/templating.py b/flask/templating.py index c809a63f..e5d896cb 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -129,8 +129,8 @@ def render_template_string(source, **context): """Renders a template from the given template source string with the given context. - :param template_name: the sourcecode of the template to be - rendered + :param source: the sourcecode of the template to be + rendered :param context: the variables that should be available in the context of the template. """ From 22af78a96f98222c2292d71dd1914f5be56362a0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 21 Sep 2012 01:02:42 +0900 Subject: [PATCH 0031/2743] Removed outdated sentence in the testing docs --- docs/testing.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 1e00fe80..d4d9259c 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -270,10 +270,6 @@ happen. With Flask 0.4 this is possible by using the If you were to use just the :meth:`~flask.Flask.test_client` without the `with` block, the `assert` would fail with an error because `request` is no longer available (because you are trying to use it outside of the actual request). -However, keep in mind that any :meth:`~flask.Flask.after_request` functions -are already called at this point so your database connection and -everything involved is probably already closed down. - Accessing and Modifying Sessions -------------------------------- From 7233a3e0a223e8d426ff937f5c03d88636b0ade4 Mon Sep 17 00:00:00 2001 From: Ryan Macy Date: Mon, 1 Oct 2012 14:45:02 -0500 Subject: [PATCH 0032/2743] Fixed typo occours to occurs Fixed a typo in the docstring of handle_exception. Was occours, now occurs. --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index f8526d5c..08fc3248 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1265,7 +1265,7 @@ class Flask(_PackageBoundObject): def handle_exception(self, e): """Default exception handling that kicks in when an exception - occours that is not caught. In debug mode the exception will + occurs that is not caught. In debug mode the exception will be re-raised immediately, otherwise it is logged and the handler for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. From 639817b6212465264d9c24faf4e43cefa2e7ae50 Mon Sep 17 00:00:00 2001 From: jfinkels Date: Fri, 5 Oct 2012 02:49:55 -0300 Subject: [PATCH 0033/2743] Update docs/quickstart.rst Removed incorrect syntax and simplified remaining sentence. --- docs/quickstart.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 1d72e510..e9d6b388 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -627,9 +627,9 @@ For this also see :ref:`about-responses`. Redirects and Errors -------------------- -To redirect a user to somewhere else you can use the -:func:`~flask.redirect` function. To abort a request early with an error -code use the :func:`~flask.abort` function. Here an example how this works:: +To redirect a user to another endpoint, use the :func:`~flask.redirect` +function; to abort a request early with an error code, use the +:func:`~flask.abort` function:: from flask import abort, redirect, url_for From 261c4a6aee88361e0de5d86061d873bbad2cb3a9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 12:40:59 +0200 Subject: [PATCH 0034/2743] Updated documentation for the new sessions --- docs/upgrading.rst | 9 +++++++-- flask/sessions.py | 5 ++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/upgrading.rst b/docs/upgrading.rst index c295fb1c..34f54f42 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -29,9 +29,14 @@ format changed from pickle to a specialized JSON format. This change has been done in order to avoid the damage an attacker can do if the secret key is leaked. When you upgrade you will notice two major changes: all sessions that were issued before the upgrade are invalidated and you can -only store a limited amount of types in the session. +only store a limited amount of types in the session. The new sessions are +by design much more restricted to only allow JSON with a few small +extensions for tuples and strings with HTML markup. -TODO: add external module for session upgrading +In order to not break people's sessions it is possible to continue using +the old session system by using the `Flask-OldSessions_` extension. + +.. _Flask-OldSessions: http://packages.python.org/Flask-OldSessions/ Version 0.9 ----------- diff --git a/flask/sessions.py b/flask/sessions.py index ba0b0ed7..4790f73a 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -3,10 +3,9 @@ flask.sessions ~~~~~~~~~~~~~~ - Implements cookie based sessions based on Werkzeug's secure cookie - system. + Implements cookie based sessions based on itsdangerous. - :copyright: (c) 2011 by Armin Ronacher. + :copyright: (c) 2012 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ From 4f1cb4212376acbbe7ab14fb8caa8a5025b4659e Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 12:48:19 +0200 Subject: [PATCH 0035/2743] make_test_environ_builder when used with subdomains was not working correctly, now it uses urlparse module for detecting full URL and changing path and base_url correctly --- flask/testing.py | 6 +++++- flask/testsuite/testing.py | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/flask/testing.py b/flask/testing.py index 782b40f6..bdd3860f 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -15,6 +15,7 @@ from __future__ import with_statement from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack +from urlparse import urlparse def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): @@ -22,9 +23,12 @@ def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): http_host = app.config.get('SERVER_NAME') app_root = app.config.get('APPLICATION_ROOT') if base_url is None: - base_url = 'http://%s/' % (http_host or 'localhost') + url = urlparse(path) + base_url = 'http://%s/' % (url.netloc or http_host or 'localhost') if app_root: base_url += app_root.lstrip('/') + if url.netloc: + path = url.path return EnvironBuilder(path, base_url, *args, **kwargs) diff --git a/flask/testsuite/testing.py b/flask/testsuite/testing.py index 0e6feb60..92e3f267 100644 --- a/flask/testsuite/testing.py +++ b/flask/testsuite/testing.py @@ -198,7 +198,46 @@ class TestToolsTestCase(FlaskTestCase): self.assert_equal(called, [None, None]) +class SubdomainTestCase(FlaskTestCase): + + def setUp(self): + self.app = flask.Flask(__name__) + self.app.config['SERVER_NAME'] = 'example.com' + self.client = self.app.test_client() + + self._ctx = self.app.test_request_context() + self._ctx.push() + + def tearDown(self): + if self._ctx is not None: + self._ctx.pop() + + def test_subdomain(self): + @self.app.route('/', subdomain='') + def view(company_id): + return company_id + + url = flask.url_for('view', company_id='xxx') + response = self.client.get(url) + + self.assertEquals(200, response.status_code) + self.assertEquals('xxx', response.data) + + + def test_nosubdomain(self): + @self.app.route('/') + def view(company_id): + return company_id + + url = flask.url_for('view', company_id='xxx') + response = self.client.get(url) + + self.assertEquals(200, response.status_code) + self.assertEquals('xxx', response.data) + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestToolsTestCase)) + suite.addTest(unittest.makeSuite(SubdomainTestCase)) return suite From f034d8d3451f590fca1badeac5da0230bed9b148 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 12:51:46 +0200 Subject: [PATCH 0036/2743] Add @template_test() decorator for creating custom jinja2 tests, like existing @template_filter() for filters. Fixes #332 --- flask/app.py | 38 ++++++ flask/blueprints.py | 28 +++++ flask/testsuite/blueprints.py | 116 ++++++++++++++++++- flask/testsuite/templates/template_test.html | 3 + flask/testsuite/templating.py | 91 +++++++++++++-- 5 files changed, 265 insertions(+), 11 deletions(-) create mode 100644 flask/testsuite/templates/template_test.html diff --git a/flask/app.py b/flask/app.py index 08fc3248..7940a349 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1086,6 +1086,44 @@ class Flask(_PackageBoundObject): """ self.jinja_env.filters[name or f.__name__] = f + @setupmethod + def template_test(self, name=None): + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in xrange(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + def decorator(f): + self.add_template_test(f, name=name) + return f + return decorator + + @setupmethod + def add_template_test(self, f, name=None): + """Register a custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod def before_request(self, f): """Registers a function to run before each request.""" diff --git a/flask/blueprints.py b/flask/blueprints.py index 9c557028..7ce23bbc 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -209,6 +209,34 @@ class Blueprint(_PackageBoundObject): state.app.jinja_env.filters[name or f.__name__] = f self.record_once(register_template) + def app_template_test(self, name=None): + """Register a custom template test, available application wide. Like + :meth:`Flask.template_test` but for a blueprint. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + def decorator(f): + self.add_app_template_test(f, name=name) + return f + return decorator + + def add_app_template_test(self, f, name=None): + """Register a custom template test, available application wide. Like + :meth:`Flask.add_template_test` but for a blueprint. Works exactly + like the :meth:`app_template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + def register_template(state): + state.app.jinja_env.tests[name or f.__name__] = f + self.record_once(register_template) + def before_request(self, f): """Like :meth:`Flask.before_request` but for a blueprint. This function is only executed before each request that is handled by a function of diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index c9622121..ea047918 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -548,7 +548,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -559,7 +559,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse) app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -570,7 +570,7 @@ class BlueprintTestCase(FlaskTestCase): return s[::-1] app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -581,7 +581,7 @@ class BlueprintTestCase(FlaskTestCase): bp.add_app_template_filter(my_reverse, 'strrev') app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -650,6 +650,114 @@ class BlueprintTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.data, 'dcba') + def test_template_test(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() + def is_boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('is_boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) + self.assert_(app.jinja_env.tests['is_boolean'](False)) + + def test_add_template_test(self): + bp = flask.Blueprint('bp', __name__) + def is_boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(is_boolean) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('is_boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean) + self.assert_(app.jinja_env.tests['is_boolean'](False)) + + def test_template_test_with_name(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_add_template_test_with_name(self): + bp = flask.Blueprint('bp', __name__) + def is_boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(is_boolean, 'boolean') + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_template_test_with_template(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() + def boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_template_test_after_route_with_template(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() + def boolean(value): + return isinstance(value, bool) + app.register_blueprint(bp, url_prefix='/py') + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_template(self): + bp = flask.Blueprint('bp', __name__) + def boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(boolean) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_template_test_with_name_and_template(self): + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_name_and_template(self): + bp = flask.Blueprint('bp', __name__) + def is_boolean(value): + return isinstance(value, bool) + bp.add_app_template_test(is_boolean, 'boolean') + app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) def suite(): suite = unittest.TestSuite() diff --git a/flask/testsuite/templates/template_test.html b/flask/testsuite/templates/template_test.html new file mode 100644 index 00000000..92d5561b --- /dev/null +++ b/flask/testsuite/templates/template_test.html @@ -0,0 +1,3 @@ +{% if value is boolean %} + Success! +{% endif %} diff --git a/flask/testsuite/templating.py b/flask/testsuite/templating.py index 4a0ebdbc..1df4292d 100644 --- a/flask/testsuite/templating.py +++ b/flask/testsuite/templating.py @@ -89,7 +89,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter() def my_reverse(s): return s[::-1] - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -98,7 +98,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse) - self.assert_('my_reverse' in app.jinja_env.filters.keys()) + self.assert_('my_reverse' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse) self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba') @@ -107,7 +107,7 @@ class TemplatingTestCase(FlaskTestCase): @app.template_filter('strrev') def my_reverse(s): return s[::-1] - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -116,7 +116,7 @@ class TemplatingTestCase(FlaskTestCase): def my_reverse(s): return s[::-1] app.add_template_filter(my_reverse, 'strrev') - self.assert_('strrev' in app.jinja_env.filters.keys()) + self.assert_('strrev' in app.jinja_env.filters.keys()) self.assert_equal(app.jinja_env.filters['strrev'], my_reverse) self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba') @@ -164,6 +164,86 @@ class TemplatingTestCase(FlaskTestCase): rv = app.test_client().get('/') self.assert_equal(rv.data, 'dcba') + def test_template_test(self): + app = flask.Flask(__name__) + @app.template_test() + def boolean(value): + return isinstance(value, bool) + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_add_template_test(self): + app = flask.Flask(__name__) + def boolean(value): + return isinstance(value, bool) + app.add_template_test(boolean) + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_template_test_with_name(self): + app = flask.Flask(__name__) + @app.template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_add_template_test_with_name(self): + app = flask.Flask(__name__) + def is_boolean(value): + return isinstance(value, bool) + app.add_template_test(is_boolean, 'boolean') + self.assert_('boolean' in app.jinja_env.tests.keys()) + self.assert_equal(app.jinja_env.tests['boolean'], is_boolean) + self.assert_(app.jinja_env.tests['boolean'](False)) + + def test_template_test_with_template(self): + app = flask.Flask(__name__) + @app.template_test() + def boolean(value): + return isinstance(value, bool) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_template(self): + app = flask.Flask(__name__) + def boolean(value): + return isinstance(value, bool) + app.add_template_test(boolean) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_template_test_with_name_and_template(self): + app = flask.Flask(__name__) + @app.template_test('boolean') + def is_boolean(value): + return isinstance(value, bool) + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + + def test_add_template_test_with_name_and_template(self): + app = flask.Flask(__name__) + def is_boolean(value): + return isinstance(value, bool) + app.add_template_test(is_boolean, 'boolean') + @app.route('/') + def index(): + return flask.render_template('template_test.html', value=False) + rv = app.test_client().get('/') + self.assert_('Success!' in rv.data) + def test_custom_template_loader(self): class MyFlask(flask.Flask): def create_global_jinja_loader(self): @@ -177,7 +257,6 @@ class TemplatingTestCase(FlaskTestCase): rv = c.get('/') self.assert_equal(rv.data, 'Hello Custom World!') - def test_iterable_loader(self): app = flask.Flask(__name__) @app.context_processor @@ -195,8 +274,6 @@ class TemplatingTestCase(FlaskTestCase): self.assert_equal(rv.data, '

Jameson

') - - def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TemplatingTestCase)) From c9a7fdf1b02ee79676d39bb614007bf922931a9c Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 12:53:36 +0200 Subject: [PATCH 0037/2743] Documented latest commit in changelog --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index a7c97cd7..be7ede79 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,8 @@ Release date to be decided. - Changed default cookie serialization format from pickle to JSON to limit the impact an attacker can do if the secret key leaks. See :ref:`upgrading-to-010` for more information. +- Added ``template_test`` methods in addition to the already existing + ``template_filter`` method family. Version 0.9 ----------- From 18413ed1bf08261acf6d40f8ba65a98ae586bb29 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 13:02:05 +0200 Subject: [PATCH 0038/2743] Added HTTP override middleware to docs. This fixes #582 --- docs/patterns/index.rst | 1 + docs/patterns/methodoverrides.rst | 43 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 docs/patterns/methodoverrides.rst diff --git a/docs/patterns/index.rst b/docs/patterns/index.rst index 964b1e17..2b1eaa9a 100644 --- a/docs/patterns/index.rst +++ b/docs/patterns/index.rst @@ -37,3 +37,4 @@ Snippet Archives `_. favicon streaming deferredcallbacks + methodoverrides diff --git a/docs/patterns/methodoverrides.rst b/docs/patterns/methodoverrides.rst new file mode 100644 index 00000000..d5c187b6 --- /dev/null +++ b/docs/patterns/methodoverrides.rst @@ -0,0 +1,43 @@ +Adding HTTP Method Overrides +============================ + +Some HTTP proxies do not support arbitrary HTTP methods or newer HTTP +methods (such as PATCH). In that case it's possible to “proxy” HTTP +methods through another HTTP method in total violation of the protocol. + +The way this works is by letting the client do an HTTP POST request and +set the ``X-HTTP-Method-Override`` header and set the value to the +intended HTTP method (such as ``PATCH``). + +This can easily be accomplished with an HTTP middleware:: + + class HTTPMethodOverrideMiddleware(object): + allowed_methods = frozenset([ + 'GET', + 'HEAD', + 'POST', + 'DELETE', + 'PUT', + 'PATCH', + 'OPTIONS' + ]) + bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE']) + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper() + if method in self.allowed_methods: + method = method.encode('ascii', 'replace') + environ['REQUEST_METHOD'] = method + if method in self.bodyless_methods: + environ['CONTENT_LENGTH'] = '0' + return self.app(environ, start_response) + +To use this with Flask this is all that is necessary:: + + from flask import Flask + + app = Flask(__name__) + app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app) From 5b462dd38224648e7a3e533449784a9e236f957d Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 14:50:21 +0200 Subject: [PATCH 0039/2743] Fixed a typo in the docs. This fixes #576 and #575 --- docs/signals.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/signals.rst b/docs/signals.rst index 959c53bd..5f0af048 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -285,7 +285,7 @@ The following signals exist in Flask: def close_db_connection(sender, **extra): session.close() - from flask import request_tearing_down + from flask import appcontext_tearing_down appcontext_tearing_down.connect(close_db_connection, app) This will also be passed an `exc` keyword argument that has a reference From 7f8709147435bea69fa15ae0de9a2326ab5dfc41 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 14:51:26 +0200 Subject: [PATCH 0040/2743] Added a missing exposed import. This fixes #575 --- flask/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/flask/__init__.py b/flask/__init__.py index ac1a3a00..fda94f39 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -34,7 +34,8 @@ from .templating import render_template, render_template_string # the signals from .signals import signals_available, template_rendered, request_started, \ - request_finished, got_request_exception, request_tearing_down + request_finished, got_request_exception, request_tearing_down, \ + appcontext_tearing_down # only import json if it's available from .helpers import json From de5038f2fb25189e6d2ba8bc479d3d940a8ea43b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 14:56:02 +0200 Subject: [PATCH 0041/2743] Added total_seconds() helper for pythons before 2.7 --- flask/sessions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/flask/sessions.py b/flask/sessions.py index 4790f73a..343fa94d 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -19,6 +19,10 @@ from . import Markup from itsdangerous import URLSafeTimedSerializer, BadSignature +def total_seconds(td): + return td.days * 60 * 60 * 24 + td.seconds + + class SessionMixin(object): """Expands a basic dictionary with an accessors that are expected by Flask extensions and users for the session. @@ -267,7 +271,7 @@ class SecureCookieSessionInterface(SessionInterface): val = request.cookies.get(app.session_cookie_name) if not val: return self.session_class() - max_age = app.permanent_session_lifetime.total_seconds() + max_age = total_seconds(app.permanent_session_lifetime) try: data = s.loads(val, max_age=max_age) return self.session_class(data) From 3bec75d230507335eaf9066605d3893e185b9e11 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 15:24:03 +0200 Subject: [PATCH 0042/2743] Set the content-length header for sendfile. Fixes #447 --- CHANGES | 1 + flask/helpers.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index be7ede79..0c7a8e1e 100644 --- a/CHANGES +++ b/CHANGES @@ -13,6 +13,7 @@ Release date to be decided. :ref:`upgrading-to-010` for more information. - Added ``template_test`` methods in addition to the already existing ``template_filter`` method family. +- Set the content-length header for x-sendfile. Version 0.9 ----------- diff --git a/flask/helpers.py b/flask/helpers.py index b2d34a71..07c8add2 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -541,6 +541,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if file is not None: file.close() headers['X-Sendfile'] = filename + headers['Content-Length'] = os.path.getsize(filename) data = None else: if file is None: From c4f2075f4c4c27856fe0af77250fb75c61c0d86b Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 7 Oct 2012 15:33:25 +0200 Subject: [PATCH 0043/2743] tojson no longer escapes script blocks in HTML5 parsers. Fixed #605 --- CHANGES | 1 + flask/helpers.py | 12 +++++++----- flask/testsuite/helpers.py | 2 ++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index 0c7a8e1e..10a2fe1c 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,7 @@ Release date to be decided. - Added ``template_test`` methods in addition to the already existing ``template_filter`` method family. - Set the content-length header for x-sendfile. +- ``tojson`` filter now does not escape script blocks in HTML5 parsers. Version 0.9 ----------- diff --git a/flask/helpers.py b/flask/helpers.py index 07c8add2..9491ac55 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -45,11 +45,13 @@ from .globals import session, _request_ctx_stack, _app_ctx_stack, \ # figure out if simplejson escapes slashes. This behavior was changed # from one version to another without reason. -if '\\/' not in json.dumps('/'): - def _tojson_filter(*args, **kwargs): - return json.dumps(*args, **kwargs).replace('/', '\\/') -else: - _tojson_filter = json.dumps +_slash_escape = '\\/' not in json.dumps('/') + +def _tojson_filter(*args, **kwargs): + rv = json.dumps(*args, **kwargs) + if _slash_escape: + rv = rv.replace('/', '\\/') + return rv.replace('"') rv = render('{{ "<\0/script>"|tojson|safe }}') self.assert_equal(rv, '"<\\u0000\\/script>"') + rv = render('{{ " +
+

{% endif %} {% endif %} -