#
# Copyright (c) 2009-2010 Gintautas Miliauskas
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
"""
Pickle field implementation for Django.
Modified for Evennia by Griatch and the Evennia community.
"""
from ast import literal_eval
from base64 import b64decode, b64encode
from copy import Error as CopyError
from copy import deepcopy
from datetime import datetime
from pickle import dumps, loads
from zlib import compress, decompress
# import six # this is actually a pypy component, not in default syslib
from django.core.exceptions import ValidationError
from django.db import models
from django.forms.fields import CharField
from django.forms.widgets import Textarea
from django.utils.encoding import force_str
from evennia.utils.dbserialize import pack_dbobj
DEFAULT_PROTOCOL = 4
[docs]class PickledObject(str):
"""
A subclass of string so it can be told whether a string is a pickled
object or not (if the object is an instance of this class then it must
[well, should] be a pickled one).
Only really useful for passing pre-encoded values to ``default``
with ``dbsafe_encode``, not that doing so is necessary. If you
remove PickledObject and its references, you won't be able to pass
in pre-encoded values anymore, but you can always just pass in the
python objects themselves.
"""
class _ObjectWrapper(object):
"""
A class used to wrap object that have properties that may clash with the
ORM internals.
For example, objects with the `prepare_database_save` property such as
`django.db.Model` subclasses won't work under certain conditions and the
same apply for trying to retrieve any `callable` object.
"""
__slots__ = ("_obj",)
def __init__(self, obj):
self._obj = obj
[docs]def wrap_conflictual_object(obj):
if hasattr(obj, "prepare_database_save") or callable(obj):
obj = _ObjectWrapper(obj)
return obj
[docs]def dbsafe_encode(value, compress_object=False, pickle_protocol=DEFAULT_PROTOCOL):
# We use deepcopy() here to avoid a problem with cPickle, where dumps
# can generate different character streams for same lookup value if
# they are referenced differently.
# The reason this is important is because we do all of our lookups as
# simple string matches, thus the character streams must be the same
# for the lookups to work properly. See tests.py for more information.
try:
value = deepcopy(value)
except CopyError:
# this can happen on a manager query where the search query string is a
# database model.
value = pack_dbobj(value)
value = dumps(value, protocol=pickle_protocol)
if compress_object:
value = compress(value)
value = b64encode(value).decode() # decode bytes to str
return PickledObject(value)
[docs]def dbsafe_decode(value, compress_object=False):
value = value.encode() # encode str to bytes
value = b64decode(value)
if compress_object:
value = decompress(value)
return loads(value)
[docs]class PickledObjectField(models.Field):
"""
A field that will accept *any* python object and store it in the
database. PickledObjectField will optionally compress its values if
declared with the keyword argument ``compress=True``.
Does not actually encode and compress ``None`` objects (although you
can still do lookups using None). This way, it is still possible to
use the ``isnull`` lookup type correctly.
"""
[docs] def __init__(self, *args, **kwargs):
self.compress = kwargs.pop("compress", False)
self.protocol = kwargs.pop("protocol", DEFAULT_PROTOCOL)
super().__init__(*args, **kwargs)
[docs] def get_default(self):
"""
Returns the default value for this field.
The default implementation on models.Field calls force_str
on the default, which means you can't set arbitrary Python
objects as the default. To fix this, we just return the value
without calling force_str on it. Note that if you set a
callable as a default, the field will still call it. It will
*not* try to pickle and encode it.
"""
if self.has_default():
if callable(self.default):
return self.default()
return self.default
# If the field doesn't have a default, then we punt to models.Field.
return super().get_default()
[docs] def from_db_value(self, value, *args):
"""
B64decode and unpickle the object, optionally decompressing it.
If an error is raised in de-pickling and we're sure the value is
a definite pickle, the error is allowed to propagate. If we
aren't sure if the value is a pickle or not, then we catch the
error and return the original value instead.
"""
if value is not None:
try:
value = dbsafe_decode(value, self.compress)
except Exception:
# If the value is a definite pickle; and an error is raised in
# de-pickling it should be allowed to propogate.
if isinstance(value, PickledObject):
raise
else:
if isinstance(value, _ObjectWrapper):
return value._obj
return value
[docs] def pre_save(self, model_instance, add):
value = super().pre_save(model_instance, add)
return wrap_conflictual_object(value)
[docs] def get_db_prep_value(self, value, connection=None, prepared=False):
"""
Pickle and b64encode the object, optionally compressing it.
The pickling protocol is specified explicitly (by default 2),
rather than as -1 or HIGHEST_PROTOCOL, because we don't want the
protocol to change over time. If it did, ``exact`` and ``in``
lookups would likely fail, since pickle would now be generating
a different string.
"""
if value is not None and not isinstance(value, PickledObject):
# We call force_str here explicitly, so that the encoded string
# isn't rejected by the postgresql backend. Alternatively,
# we could have just registered PickledObject with the psycopg
# marshaller (telling it to store it like it would a string), but
# since both of these methods result in the same value being stored,
# doing things this way is much easier.
value = force_str(dbsafe_encode(value, self.compress, self.protocol))
return value
[docs] def value_to_string(self, obj):
value = self.value_from_object(obj)
return self.get_db_prep_value(value)
[docs] def get_internal_type(self):
return "TextField"
[docs] def get_db_prep_lookup(self, lookup_type, value, connection=None, prepared=False):
if lookup_type not in ["exact", "in", "isnull"]:
raise TypeError("Lookup type %s is not supported." % lookup_type)
# The Field model already calls get_db_prep_value before doing the
# actual lookup, so all we need to do is limit the lookup types.
return super().get_db_prep_lookup(
lookup_type, value, connection=connection, prepared=prepared
)