forked from orbit-oss/flask
make session serializer extensible
support serializing 1-item dicts with tag as key refactor serializer into flask.json.tag module continues #1452, closes #1438, closes #1908
This commit is contained in:
parent
ea2e9609bc
commit
5e1ced3c05
4 changed files with 201 additions and 140 deletions
3
CHANGES
3
CHANGES
|
|
@ -65,6 +65,8 @@ Major release, unreleased
|
||||||
- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode.
|
- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode.
|
||||||
``BadRequestKeyError`` has a message with the bad key in debug mode instead
|
``BadRequestKeyError`` has a message with the bad key in debug mode instead
|
||||||
of the generic bad request message. (`#2348`_)
|
of the generic bad request message. (`#2348`_)
|
||||||
|
- Allow registering new tags with ``TaggedJSONSerializer`` to support
|
||||||
|
storing other types in the session cookie. (`#2352`_)
|
||||||
|
|
||||||
.. _#1489: https://github.com/pallets/flask/pull/1489
|
.. _#1489: https://github.com/pallets/flask/pull/1489
|
||||||
.. _#1621: https://github.com/pallets/flask/pull/1621
|
.. _#1621: https://github.com/pallets/flask/pull/1621
|
||||||
|
|
@ -84,6 +86,7 @@ Major release, unreleased
|
||||||
.. _#2319: https://github.com/pallets/flask/pull/2319
|
.. _#2319: https://github.com/pallets/flask/pull/2319
|
||||||
.. _#2326: https://github.com/pallets/flask/pull/2326
|
.. _#2326: https://github.com/pallets/flask/pull/2326
|
||||||
.. _#2348: https://github.com/pallets/flask/pull/2348
|
.. _#2348: https://github.com/pallets/flask/pull/2348
|
||||||
|
.. _#2352: https://github.com/pallets/flask/pull/2352
|
||||||
|
|
||||||
Version 0.12.2
|
Version 0.12.2
|
||||||
--------------
|
--------------
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,9 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
|
||||||
flask.json
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
Implementation helpers for the JSON support in Flask.
|
|
||||||
|
|
||||||
:copyright: (c) 2015 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import io
|
import io
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from .globals import current_app, request
|
from flask.globals import current_app, request
|
||||||
from ._compat import text_type, PY2
|
from flask._compat import text_type, PY2
|
||||||
|
|
||||||
from werkzeug.http import http_date
|
from werkzeug.http import http_date
|
||||||
from jinja2 import Markup
|
from jinja2 import Markup
|
||||||
188
flask/json/tag.py
Normal file
188
flask/json/tag.py
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
from datetime import datetime
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from jinja2 import Markup
|
||||||
|
from werkzeug.http import http_date, parse_date
|
||||||
|
|
||||||
|
from flask._compat import iteritems, text_type
|
||||||
|
from flask.json import dumps, loads
|
||||||
|
|
||||||
|
|
||||||
|
class JSONTag(object):
|
||||||
|
__slots__ = ()
|
||||||
|
key = None
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def to_json(self, serializer, value):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def to_python(self, serializer, value):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def tag(self, serializer, value):
|
||||||
|
return {self.key: self.to_json(serializer, value)}
|
||||||
|
|
||||||
|
|
||||||
|
class TagDict(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = ' di'
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
return isinstance(value, dict)
|
||||||
|
|
||||||
|
def to_json(self, serializer, value, key=None):
|
||||||
|
if key is not None:
|
||||||
|
return {key + '__': serializer._tag(value[key])}
|
||||||
|
|
||||||
|
return dict((k, serializer._tag(v)) for k, v in iteritems(value))
|
||||||
|
|
||||||
|
def to_python(self, serializer, value):
|
||||||
|
key = next(iter(value))
|
||||||
|
return {key[:-2]: value[key]}
|
||||||
|
|
||||||
|
def tag(self, serializer, value):
|
||||||
|
if len(value) == 1:
|
||||||
|
key = next(iter(value))
|
||||||
|
|
||||||
|
if key in serializer._tags:
|
||||||
|
return {self.key: self.to_json(serializer, value, key=key)}
|
||||||
|
|
||||||
|
return self.to_json(serializer, value)
|
||||||
|
|
||||||
|
|
||||||
|
class TagTuple(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = ' t'
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
return isinstance(value, tuple)
|
||||||
|
|
||||||
|
def to_json(self, serializer, value):
|
||||||
|
return [serializer._tag(item) for item in value]
|
||||||
|
|
||||||
|
def to_python(self, serializer, value):
|
||||||
|
return tuple(value)
|
||||||
|
|
||||||
|
|
||||||
|
class PassList(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
return isinstance(value, list)
|
||||||
|
|
||||||
|
def to_json(self, serializer, value):
|
||||||
|
return [serializer._tag(item) for item in value]
|
||||||
|
|
||||||
|
tag = to_json
|
||||||
|
|
||||||
|
|
||||||
|
class TagBytes(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = ' b'
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
return isinstance(value, bytes)
|
||||||
|
|
||||||
|
def to_json(self, serializer, value):
|
||||||
|
return b64encode(value).decode('ascii')
|
||||||
|
|
||||||
|
def to_python(self, serializer, value):
|
||||||
|
return b64decode(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TagMarkup(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = ' m'
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
return callable(getattr(value, '__html__', None))
|
||||||
|
|
||||||
|
def to_json(self, serializer, value):
|
||||||
|
return text_type(value.__html__())
|
||||||
|
|
||||||
|
def to_python(self, serializer, value):
|
||||||
|
return Markup(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TagUUID(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = ' u'
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
return isinstance(value, UUID)
|
||||||
|
|
||||||
|
def to_json(self, serializer, value):
|
||||||
|
return value.hex
|
||||||
|
|
||||||
|
def to_python(self, serializer, value):
|
||||||
|
return UUID(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TagDateTime(JSONTag):
|
||||||
|
__slots__ = ()
|
||||||
|
key = ' d'
|
||||||
|
|
||||||
|
def check(self, serializer, value):
|
||||||
|
return isinstance(value, datetime)
|
||||||
|
|
||||||
|
def to_json(self, serializer, value):
|
||||||
|
return http_date(value)
|
||||||
|
|
||||||
|
def to_python(self, serializer, value):
|
||||||
|
return parse_date(value)
|
||||||
|
|
||||||
|
|
||||||
|
class TaggedJSONSerializer(object):
|
||||||
|
__slots__ = ('_tags', '_order')
|
||||||
|
_default_tags = [
|
||||||
|
TagDict(), TagTuple(), PassList(), TagBytes(), TagMarkup(), TagUUID(),
|
||||||
|
TagDateTime(),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._tags = {}
|
||||||
|
self._order = []
|
||||||
|
|
||||||
|
for tag in self._default_tags:
|
||||||
|
self.register(tag)
|
||||||
|
|
||||||
|
def register(self, tag, force=False, index=-1):
|
||||||
|
key = tag.key
|
||||||
|
|
||||||
|
if key is not None:
|
||||||
|
if not force and key in self._tags:
|
||||||
|
raise KeyError("Tag '{0}' is already registered.".format(key))
|
||||||
|
|
||||||
|
self._tags[key] = tag
|
||||||
|
|
||||||
|
if index == -1:
|
||||||
|
self._order.append(tag)
|
||||||
|
else:
|
||||||
|
self._order.insert(index, tag)
|
||||||
|
|
||||||
|
def _tag(self, value):
|
||||||
|
for tag in self._order:
|
||||||
|
if tag.check(self, value):
|
||||||
|
return tag.tag(self, value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _untag(self, value):
|
||||||
|
if len(value) != 1:
|
||||||
|
return value
|
||||||
|
|
||||||
|
key = next(iter(value))
|
||||||
|
|
||||||
|
if key not in self._tags:
|
||||||
|
return value
|
||||||
|
|
||||||
|
return self._tags[key].to_python(self, value[key])
|
||||||
|
|
||||||
|
def dumps(self, value):
|
||||||
|
return dumps(self._tag(value), separators=(',', ':'))
|
||||||
|
|
||||||
|
def loads(self, value):
|
||||||
|
return loads(value, object_hook=self._untag)
|
||||||
|
|
@ -9,18 +9,14 @@
|
||||||
:license: BSD, see LICENSE for more details.
|
:license: BSD, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
import hashlib
|
import hashlib
|
||||||
import uuid
|
|
||||||
import warnings
|
import warnings
|
||||||
from base64 import b64decode, b64encode
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from itsdangerous import BadSignature, URLSafeTimedSerializer
|
from itsdangerous import BadSignature, URLSafeTimedSerializer
|
||||||
from werkzeug.datastructures import CallbackDict
|
from werkzeug.datastructures import CallbackDict
|
||||||
from werkzeug.http import http_date, parse_date
|
|
||||||
|
|
||||||
from . import Markup, json
|
from flask.helpers import is_ip, total_seconds
|
||||||
from ._compat import iteritems, text_type
|
from flask.json.tag import TaggedJSONSerializer
|
||||||
from .helpers import is_ip, total_seconds
|
|
||||||
|
|
||||||
|
|
||||||
class SessionMixin(object):
|
class SessionMixin(object):
|
||||||
|
|
@ -57,126 +53,6 @@ class SessionMixin(object):
|
||||||
#: from being served the same cache.
|
#: from being served the same cache.
|
||||||
accessed = True
|
accessed = 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 __init__(self):
|
|
||||||
self.conversions = [
|
|
||||||
{
|
|
||||||
'check': lambda value: self._is_dict_with_used_key(value),
|
|
||||||
'tag': lambda value: self._tag_dict_used_with_key(value),
|
|
||||||
'untag': lambda value: self._untag_dict_used_with_key(value),
|
|
||||||
'key': ' di',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: isinstance(value, tuple),
|
|
||||||
'tag': lambda value: [self._tag(x) for x in value],
|
|
||||||
'untag': lambda value: tuple(value),
|
|
||||||
'key': ' t',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: isinstance(value, uuid.UUID),
|
|
||||||
'tag': lambda value: value.hex,
|
|
||||||
'untag': lambda value: uuid.UUID(value),
|
|
||||||
'key': ' u',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: isinstance(value, bytes),
|
|
||||||
'tag': lambda value: b64encode(value).decode('ascii'),
|
|
||||||
'untag': lambda value: b64decode(value),
|
|
||||||
'key': ' b',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: callable(getattr(value, '__html__',
|
|
||||||
None)),
|
|
||||||
'tag': lambda value: text_type(value.__html__()),
|
|
||||||
'untag': lambda value: Markup(value),
|
|
||||||
'key': ' m',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: isinstance(value, list),
|
|
||||||
'tag': lambda value: [self._tag(x) for x in value],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: isinstance(value, datetime),
|
|
||||||
'tag': lambda value: http_date(value),
|
|
||||||
'untag': lambda value: parse_date(value),
|
|
||||||
'key': ' d',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: isinstance(value, dict),
|
|
||||||
'tag': lambda value: dict((k, self._tag(v)) for k, v in
|
|
||||||
iteritems(value)),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'check': lambda value: isinstance(value, str),
|
|
||||||
'tag': lambda value: self._tag_string(value),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def keys(self):
|
|
||||||
return [c['key'] for c in self.conversions if c.get('key')]
|
|
||||||
|
|
||||||
def _get_conversion_untag(self, key):
|
|
||||||
return next(
|
|
||||||
(c['untag'] for c in self.conversions if c.get('key') == key),
|
|
||||||
lambda v: None
|
|
||||||
)
|
|
||||||
|
|
||||||
def _is_dict_with_used_key(self, v):
|
|
||||||
return isinstance(v, dict) and len(v) == 1 and list(v)[0] in self.keys
|
|
||||||
|
|
||||||
def _was_dict_with_used_key(self, k):
|
|
||||||
return k.endswith('__') and k[:-2] in self.keys
|
|
||||||
|
|
||||||
def _tag_string(self, value):
|
|
||||||
try:
|
|
||||||
return text_type(value)
|
|
||||||
except UnicodeError:
|
|
||||||
from flask.debughelpers import UnexpectedUnicodeError
|
|
||||||
raise UnexpectedUnicodeError(u'A byte string with '
|
|
||||||
u'non-ASCII data was passed to the session system '
|
|
||||||
u'which can only store unicode strings. Consider '
|
|
||||||
u'base64 encoding your string (String was %r)' % value)
|
|
||||||
|
|
||||||
def _tag_dict_used_with_key(self, value):
|
|
||||||
k, v = next(iteritems(value))
|
|
||||||
return {'%s__' % k: v}
|
|
||||||
|
|
||||||
def _tag(self, value):
|
|
||||||
for tag_ops in self.conversions:
|
|
||||||
if tag_ops['check'](value):
|
|
||||||
tag = tag_ops.get('key')
|
|
||||||
if tag:
|
|
||||||
return {tag: tag_ops['tag'](value)}
|
|
||||||
return tag_ops['tag'](value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def _untag_dict_used_with_key(self, the_value):
|
|
||||||
k, v = next(iteritems(the_value))
|
|
||||||
if self._was_dict_with_used_key(k):
|
|
||||||
return {k[:-2]: self._untag(v)}
|
|
||||||
|
|
||||||
def _untag(self, obj):
|
|
||||||
if len(obj) != 1:
|
|
||||||
return obj
|
|
||||||
the_key, the_value = next(iteritems(obj))
|
|
||||||
untag = self._get_conversion_untag(the_key)
|
|
||||||
new_value = untag(the_value)
|
|
||||||
return new_value if new_value else obj
|
|
||||||
|
|
||||||
def dumps(self, value):
|
|
||||||
return json.dumps(self._tag(value), separators=(',', ':'))
|
|
||||||
|
|
||||||
def loads(self, value):
|
|
||||||
return json.loads(value, object_hook=self._untag)
|
|
||||||
|
|
||||||
|
|
||||||
session_json_serializer = TaggedJSONSerializer()
|
|
||||||
|
|
||||||
|
|
||||||
class SecureCookieSession(CallbackDict, SessionMixin):
|
class SecureCookieSession(CallbackDict, SessionMixin):
|
||||||
"""Base class for sessions based on signed cookies."""
|
"""Base class for sessions based on signed cookies."""
|
||||||
|
|
@ -404,6 +280,9 @@ class SessionInterface(object):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
session_json_serializer = TaggedJSONSerializer()
|
||||||
|
|
||||||
|
|
||||||
class SecureCookieSessionInterface(SessionInterface):
|
class SecureCookieSessionInterface(SessionInterface):
|
||||||
"""The default session interface that stores sessions in signed cookies
|
"""The default session interface that stores sessions in signed cookies
|
||||||
through the :mod:`itsdangerous` module.
|
through the :mod:`itsdangerous` module.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue