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
|
|
46 | 46 | self._cull_frequency = 3 |
47 | 47 | |
48 | 48 | def get(self, key, default=None): |
| 49 | self._validate_key(key) |
49 | 50 | db = router.db_for_read(self.cache_model_class) |
50 | 51 | table = connections[db].ops.quote_name(self._table) |
51 | 52 | cursor = connections[db].cursor() |
… |
… |
|
65 | 66 | return pickle.loads(base64.decodestring(value)) |
66 | 67 | |
67 | 68 | def set(self, key, value, timeout=None): |
| 69 | self._validate_key(key) |
68 | 70 | self._base_set('set', key, value, timeout) |
69 | 71 | |
70 | 72 | def add(self, key, value, timeout=None): |
| 73 | self._validate_key(key) |
71 | 74 | return self._base_set('add', key, value, timeout) |
72 | 75 | |
73 | 76 | def _base_set(self, mode, key, value, timeout=None): |
… |
… |
|
103 | 106 | return True |
104 | 107 | |
105 | 108 | def delete(self, key): |
| 109 | self._validate_key(key) |
106 | 110 | db = router.db_for_write(self.cache_model_class) |
107 | 111 | table = connections[db].ops.quote_name(self._table) |
108 | 112 | cursor = connections[db].cursor() |
… |
… |
|
111 | 115 | transaction.commit_unless_managed(using=db) |
112 | 116 | |
113 | 117 | def has_key(self, key): |
| 118 | self._validate_key(key) |
114 | 119 | db = router.db_for_read(self.cache_model_class) |
115 | 120 | table = connections[db].ops.quote_name(self._table) |
116 | 121 | cursor = connections[db].cursor() |
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/docs/topics/cache.txt b/docs/topics/cache.txt
a
|
b
|
|
537 | 537 | >>> cache.get('my_key') |
538 | 538 | 'hello, world!' |
539 | 539 | |
| 540 | .. note:: |
| 541 | |
| 542 | Cache keys may not be longer than 250 characters, and may not |
| 543 | contain whitespace or control characters. |
| 544 | |
540 | 545 | The ``timeout`` argument is optional and defaults to the ``timeout`` |
541 | 546 | argument in the ``CACHE_BACKEND`` setting (explained above). It's the number of |
542 | 547 | seconds the value should be stored in the cache. |
diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
a
|
b
|
|
366 | 366 | count = count + 1 |
367 | 367 | self.assertEqual(count, final_count) |
368 | 368 | |
| 369 | def test_invalid_keys(self): |
| 370 | """ |
| 371 | All the builtin backends should refuse keys that would be refused by |
| 372 | memcached, so code using the cache API is portable. Refs #6447. |
| 373 | |
| 374 | We assert only that a generic exception is raised because we don't |
| 375 | want to encumber the memcached backend with key-checking code, and the |
| 376 | specific exceptions raised from memcached will depend on the client |
| 377 | library used. |
| 378 | |
| 379 | """ |
| 380 | self.assertRaises(Exception, self.cache.set, 'key with spaces', 'value') |
| 381 | # memcached also limits key length to 250 |
| 382 | self.assertRaises(Exception, self.cache.set, 'a' * 251, 'value') |
| 383 | |
369 | 384 | class DBCacheTests(unittest.TestCase, BaseCacheTests): |
370 | 385 | def setUp(self): |
371 | 386 | # Spaces are used in the table name to ensure quoting/escaping is working |