diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py
a
|
b
|
|
5 | 5 | class InvalidCacheBackendError(ImproperlyConfigured): |
6 | 6 | pass |
7 | 7 | |
| 8 | class InvalidCacheKeyError(Exception): |
| 9 | pass |
| 10 | |
| 11 | # memcached does not accept keys longer than 250 characters |
| 12 | MAX_KEY_LENGTH = 250 |
| 13 | |
8 | 14 | class BaseCache(object): |
9 | 15 | def __init__(self, params): |
10 | 16 | timeout = params.get('timeout', 300) |
… |
… |
|
116 | 122 | def clear(self): |
117 | 123 | """Remove *all* values from the cache at once.""" |
118 | 124 | raise NotImplementedError |
| 125 | |
| 126 | def _validate_key(self, key): |
| 127 | """ |
| 128 | Enforce keys that would be valid on all backends, to keep cache code portable. |
| 129 | |
| 130 | """ |
| 131 | if len(key) > MAX_KEY_LENGTH: |
| 132 | raise InvalidCacheKeyError('Key is too long (max length %s)' % MAX_KEY_LENGTH) |
| 133 | for char in key: |
| 134 | if ord(char) < 33 or ord(char) == 127: |
| 135 | raise InvalidCacheKeyError('Key contains control character') |
| 136 | |
diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py
a
|
b
|
|
25 | 25 | self._cull_frequency = 3 |
26 | 26 | |
27 | 27 | def get(self, key, default=None): |
| 28 | self._validate_key(key) |
28 | 29 | cursor = connection.cursor() |
29 | 30 | cursor.execute("SELECT cache_key, value, expires FROM %s WHERE cache_key = %%s" % self._table, [key]) |
30 | 31 | row = cursor.fetchone() |
… |
… |
|
39 | 40 | return pickle.loads(base64.decodestring(value)) |
40 | 41 | |
41 | 42 | def set(self, key, value, timeout=None): |
| 43 | self._validate_key(key) |
42 | 44 | self._base_set('set', key, value, timeout) |
43 | 45 | |
44 | 46 | def add(self, key, value, timeout=None): |
| 47 | self._validate_key(key) |
45 | 48 | return self._base_set('add', key, value, timeout) |
46 | 49 | |
47 | 50 | def _base_set(self, mode, key, value, timeout=None): |
… |
… |
|
74 | 77 | return True |
75 | 78 | |
76 | 79 | def delete(self, key): |
| 80 | self._validate_key(key) |
77 | 81 | cursor = connection.cursor() |
78 | 82 | cursor.execute("DELETE FROM %s WHERE cache_key = %%s" % self._table, [key]) |
79 | 83 | transaction.commit_unless_managed() |
80 | 84 | |
81 | 85 | def has_key(self, key): |
| 86 | self._validate_key(key) |
82 | 87 | now = datetime.now().replace(microsecond=0) |
83 | 88 | cursor = connection.cursor() |
84 | 89 | cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s and expires > %%s" % self._table, |
diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py
a
|
b
|
|
6 | 6 | def __init__(self, *args, **kwargs): |
7 | 7 | pass |
8 | 8 | |
9 | | def add(self, *args, **kwargs): |
| 9 | def add(self, key, *args, **kwargs): |
| 10 | self._validate_key(key) |
10 | 11 | return True |
11 | 12 | |
12 | 13 | def get(self, key, default=None): |
| 14 | self._validate_key(key) |
13 | 15 | return default |
14 | 16 | |
15 | | def set(self, *args, **kwargs): |
16 | | pass |
| 17 | def set(self, key, *args, **kwargs): |
| 18 | self._validate_key(key) |
17 | 19 | |
18 | | def delete(self, *args, **kwargs): |
19 | | pass |
| 20 | def delete(self, key, *args, **kwargs): |
| 21 | self._validate_key(key) |
20 | 22 | |
21 | 23 | def get_many(self, *args, **kwargs): |
22 | 24 | return {} |
23 | 25 | |
24 | | def has_key(self, *args, **kwargs): |
| 26 | def has_key(self, key, *args, **kwargs): |
| 27 | self._validate_key(key) |
25 | 28 | return False |
26 | 29 | |
27 | 30 | def set_many(self, *args, **kwargs): |
diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py
a
|
b
|
|
32 | 32 | self._createdir() |
33 | 33 | |
34 | 34 | def add(self, key, value, timeout=None): |
| 35 | self._validate_key(key) |
35 | 36 | if self.has_key(key): |
36 | 37 | return False |
37 | 38 | |
… |
… |
|
39 | 40 | return True |
40 | 41 | |
41 | 42 | def get(self, key, default=None): |
| 43 | self._validate_key(key) |
42 | 44 | fname = self._key_to_file(key) |
43 | 45 | try: |
44 | 46 | f = open(fname, 'rb') |
… |
… |
|
56 | 58 | return default |
57 | 59 | |
58 | 60 | def set(self, key, value, timeout=None): |
| 61 | self._validate_key(key) |
59 | 62 | fname = self._key_to_file(key) |
60 | 63 | dirname = os.path.dirname(fname) |
61 | 64 | |
… |
… |
|
79 | 82 | pass |
80 | 83 | |
81 | 84 | def delete(self, key): |
| 85 | self._validate_key(key) |
82 | 86 | try: |
83 | 87 | self._delete(self._key_to_file(key)) |
84 | 88 | except (IOError, OSError): |
… |
… |
|
95 | 99 | pass |
96 | 100 | |
97 | 101 | def has_key(self, key): |
| 102 | self._validate_key(key) |
98 | 103 | fname = self._key_to_file(key) |
99 | 104 | try: |
100 | 105 | f = open(fname, 'rb') |
diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py
a
|
b
|
|
30 | 30 | self._lock = RWLock() |
31 | 31 | |
32 | 32 | def add(self, key, value, timeout=None): |
| 33 | self._validate_key(key) |
33 | 34 | self._lock.writer_enters() |
34 | 35 | try: |
35 | 36 | exp = self._expire_info.get(key) |
… |
… |
|
44 | 45 | self._lock.writer_leaves() |
45 | 46 | |
46 | 47 | def get(self, key, default=None): |
| 48 | self._validate_key(key) |
47 | 49 | self._lock.reader_enters() |
48 | 50 | try: |
49 | 51 | exp = self._expire_info.get(key) |
… |
… |
|
76 | 78 | self._expire_info[key] = time.time() + timeout |
77 | 79 | |
78 | 80 | def set(self, key, value, timeout=None): |
| 81 | self._validate_key(key) |
79 | 82 | self._lock.writer_enters() |
80 | 83 | # Python 2.4 doesn't allow combined try-except-finally blocks. |
81 | 84 | try: |
… |
… |
|
87 | 90 | self._lock.writer_leaves() |
88 | 91 | |
89 | 92 | def has_key(self, key): |
| 93 | self._validate_key(key) |
90 | 94 | self._lock.reader_enters() |
91 | 95 | try: |
92 | 96 | exp = self._expire_info.get(key) |
… |
… |
|
127 | 131 | pass |
128 | 132 | |
129 | 133 | def delete(self, key): |
| 134 | self._validate_key(key) |
130 | 135 | self._lock.writer_enters() |
131 | 136 | try: |
132 | 137 | self._delete(key) |
diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
a
|
b
|
|
352 | 352 | self.assertEqual(self.cache.get('key3'), 'sausage') |
353 | 353 | self.assertEqual(self.cache.get('key4'), 'lobster bisque') |
354 | 354 | |
| 355 | def test_invalid_keys(self): |
| 356 | """ |
| 357 | All the builtin backends should refuse keys that would be refused by |
| 358 | memcached, so code using the cache API is portable. Refs #6447. |
| 359 | |
| 360 | We assert only that a generic exception is raised because we don't |
| 361 | want to encumber the memcached backend with key-checking code, and the |
| 362 | specific exceptions raised from memcached will depend on the client |
| 363 | library used. |
| 364 | |
| 365 | """ |
| 366 | self.assertRaises(Exception, self.cache.set, 'key with spaces', 'value') |
| 367 | # memcached also limits key length to 250 |
| 368 | self.assertRaises(Exception, self.cache.set, 'a' * 251, 'value') |
| 369 | |
355 | 370 | class DBCacheTests(unittest.TestCase, BaseCacheTests): |
356 | 371 | def setUp(self): |
357 | 372 | # Spaces are used in the table name to ensure quoting/escaping is working |