~ubuntu-branches/ubuntu/natty/moin/natty-security

« back to all changes in this revision

Viewing changes to MoinMoin/action/cache.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-07-24 23:50:51 UTC
  • Revision ID: james.westby@ubuntu.com-20080724235051-qzjvwmfslyahnjnx
Tags: 1.7.1-1
* New upstream release.  Closes: bug#492233, thanks to Teodor.
  + Fixes bogus empty page creation.  Closes: bug#489146, thanks to
    Sam Morris.
* Recommend python-xml, needed for RSS feeds.  Closes: bug#488777,
  thanks to Sam Morris.
* Add patch 10001 to disable RenderAsDocbook if python-xml is not
  available. Closes: bug#487741, thanks to Franklin Piat.
* Update cdbs snippets:
  + Move dependency cleanup to new local snippet package-relations.mk.
  + Update copyright-check output to more closely match proposed new
    copyright file format.
  + Update README.cdbs-tweaks.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
"""
 
3
    MoinMoin - Send a raw object from the caching system (and offer utility
 
4
    functions to put data into cache, calculate cache key, etc.).
 
5
 
 
6
    Sample usage
 
7
    ------------
 
8
    Assume we have a big picture (bigpic) and we want to efficiently show some
 
9
    thumbnail (thumbpic) for it:
 
10
 
 
11
    # first calculate a (hard to guess) cache key (this key will change if the
 
12
    # original data (bigpic) changes):
 
13
    key = cache.key(..., attachname=bigpic, ...)
 
14
 
 
15
    # check if we don't have it in cache yet
 
16
    if not cache.exists(..., key):
 
17
        # if we don't have it in cache, we need to render it - this is an
 
18
        # expensive operation that we want to avoid by caching:
 
19
        thumbpic = render_thumb(bigpic)
 
20
        # put expensive operation's results into cache:
 
21
        cache.put(..., key, thumbpic, ...)
 
22
 
 
23
    url = cache.url(..., key)
 
24
    html = '<img src="%s">' % url
 
25
 
 
26
    @copyright: 2008 MoinMoin:ThomasWaldmann
 
27
    @license: GNU GPL, see COPYING for details.
 
28
"""
 
29
 
 
30
import hmac, sha
 
31
 
 
32
from MoinMoin import log
 
33
logging = log.getLogger(__name__)
 
34
 
 
35
# keep both imports below as they are, order is important:
 
36
from MoinMoin import wikiutil
 
37
import mimetypes
 
38
 
 
39
from MoinMoin import config, caching
 
40
from MoinMoin.util import filesys
 
41
from MoinMoin.action import AttachFile
 
42
 
 
43
action_name = __name__.split('.')[-1]
 
44
 
 
45
# Do NOT get this directly from request.form or user would be able to read any cache!
 
46
cache_arena = 'sendcache'  # just using action_name is maybe rather confusing
 
47
 
 
48
# We maybe could use page local caching (not 'wiki' global) to have less directory entries.
 
49
# Local is easier to automatically cleanup if an item changes. Global is easier to manually cleanup.
 
50
# Local makes data_dir much larger, harder to backup.
 
51
cache_scope = 'wiki'
 
52
 
 
53
do_locking = False
 
54
 
 
55
def key(request, wikiname=None, itemname=None, attachname=None, content=None, secret=None):
 
56
    """
 
57
    Calculate a (hard-to-guess) cache key.
 
58
 
 
59
    Important key properties:
 
60
    * The key must be hard to guess (this is because do=get does no ACL checks,
 
61
      so whoever got the key [e.g. from html rendering of an ACL protected wiki
 
62
      page], will be able to see the cached content.
 
63
    * The key must change if the (original) content changes. This is because
 
64
      ACLs on some item may change and even if somebody was allowed to see some
 
65
      revision of some item, it does not implicate that he is allowed to see
 
66
      any other revision also. There will be no harm if he can see exactly the
 
67
      same content again, but there could be harm if he could access a revision
 
68
      with different content.
 
69
 
 
70
    If content is supplied, we will calculate and return a hMAC of the content.
 
71
 
 
72
    If wikiname, itemname, attachname is given, we don't touch the content (nor do
 
73
    we read it ourselves from the attachment file), but we just calculate a key
 
74
    from the given metadata values and some metadata we get from the filesystem.
 
75
 
 
76
    Hint: if you need multiple cache objects for the same source content (e.g.
 
77
          thumbnails of different sizes for the same image), calculate the key
 
78
          only once and then add some different prefixes to it to get the final
 
79
          cache keys.
 
80
 
 
81
    @param request: the request object
 
82
    @param wikiname: the name of the wiki (if not given, will be read from cfg)
 
83
    @param itemname: the name of the page
 
84
    @param attachname: the filename of the attachment
 
85
    @param content: content data as unicode object (e.g. for page content or
 
86
                    parser section content)
 
87
    @param secret: secret for hMAC calculation (default: use secret from cfg)
 
88
    """
 
89
    if secret is None:
 
90
        secret = request.cfg.secrets
 
91
    if content:
 
92
        hmac_data = content
 
93
    elif itemname is not None and attachname is not None:
 
94
        wikiname = wikiname or request.cfg.interwikiname or request.cfg.siteid
 
95
        fuid = filesys.fuid(AttachFile.getFilename(request, itemname, attachname))
 
96
        hmac_data = u''.join([wikiname, itemname, attachname, repr(fuid)])
 
97
    else:
 
98
        raise AssertionError('cache_key called with unsupported parameters')
 
99
 
 
100
    hmac_data = hmac_data.encode('utf-8')
 
101
    key = hmac.new(secret, hmac_data, sha).hexdigest()
 
102
    return key
 
103
 
 
104
 
 
105
def put(request, key, data,
 
106
        filename=None,
 
107
        content_type=None,
 
108
        content_disposition=None,
 
109
        content_length=None,
 
110
        last_modified=None,
 
111
        original=None):
 
112
    """
 
113
    Put an object into the cache to send it with cache action later.
 
114
 
 
115
    @param request: the request object
 
116
    @param key: non-guessable key into cache (str)
 
117
    @param data: content data (str or open file-like obj)
 
118
    @param filename: filename for content-disposition header and for autodetecting
 
119
                     content_type (unicode, default: None)
 
120
    @param content_type: content-type header value (str, default: autodetect from filename)
 
121
    @param content_disposition: type for content-disposition header (str, default: None)
 
122
    @param content_length: data length for content-length header (int, default: autodetect)
 
123
    @param last_modified: last modified timestamp (int, default: autodetect)
 
124
    @param original: location of original object (default: None) - this is just written to
 
125
                     the metadata cache "as is" and could be used for cache cleanup,
 
126
                     use (wikiname, itemname, attachname or None))
 
127
    """
 
128
    import os.path
 
129
    from MoinMoin.util import timefuncs
 
130
 
 
131
    if filename:
 
132
        # make sure we just have a simple filename (without path)
 
133
        filename = os.path.basename(filename)
 
134
 
 
135
        if content_type is None:
 
136
            # try autodetect
 
137
            mt, enc = mimetypes.guess_type(filename)
 
138
            if mt:
 
139
                content_type = mt
 
140
 
 
141
    if content_type is None:
 
142
        content_type = 'application/octet-stream'
 
143
 
 
144
    data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
 
145
    data_cache.update(data)
 
146
    content_length = content_length or data_cache.size()
 
147
    last_modified = last_modified or data_cache.mtime()
 
148
 
 
149
    httpdate_last_modified = timefuncs.formathttpdate(int(last_modified))
 
150
    headers = ['Content-Type: %s' % content_type,
 
151
               'Last-Modified: %s' % httpdate_last_modified,
 
152
               'Content-Length: %s' % content_length,
 
153
              ]
 
154
    if content_disposition and filename:
 
155
        # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs
 
156
        # There is no solution that is compatible to IE except stripping non-ascii chars
 
157
        filename = filename.encode(config.charset)
 
158
        headers.append('Content-Disposition: %s; filename="%s"' % (content_disposition, filename))
 
159
 
 
160
    meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
 
161
    meta_cache.update({
 
162
        'httpdate_last_modified': httpdate_last_modified,
 
163
        'last_modified': last_modified,
 
164
        'headers': headers,
 
165
        'original': original,
 
166
    })
 
167
 
 
168
 
 
169
def exists(request, key, strict=False):
 
170
    """
 
171
    Check if a cached object for this key exists.
 
172
 
 
173
    @param request: the request object
 
174
    @param key: non-guessable key into cache (str)
 
175
    @param strict: if True, also check the data cache, not only meta (bool, default: False)
 
176
    @return: is object cached? (bool)
 
177
    """
 
178
    if strict:
 
179
        data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
 
180
        data_cached = data_cache.exists()
 
181
    else:
 
182
        data_cached = True  # we assume data will be there if meta is there
 
183
 
 
184
    meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
 
185
    meta_cached = meta_cache.exists()
 
186
 
 
187
    return meta_cached and data_cached
 
188
 
 
189
 
 
190
def remove(request, key):
 
191
    """ delete headers/data cache for key """
 
192
    meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
 
193
    meta_cache.remove()
 
194
    data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
 
195
    data_cache.remove()
 
196
 
 
197
 
 
198
def url(request, key, do='get'):
 
199
    """ return URL for the object cached for key """
 
200
    return "%s/?%s" % (
 
201
        request.getScriptname(),
 
202
        wikiutil.makeQueryString(dict(action=action_name, do=do, key=key), want_unicode=False))
 
203
 
 
204
 
 
205
def _get_headers(request, key):
 
206
    """ get last_modified and headers cached for key """
 
207
    meta_cache = caching.CacheEntry(request, cache_arena, key+'.meta', cache_scope, do_locking=do_locking, use_pickle=True)
 
208
    meta = meta_cache.content()
 
209
    return meta['httpdate_last_modified'], meta['headers']
 
210
 
 
211
 
 
212
def _get_datafile(request, key):
 
213
    """ get an open data file for the data cached for key """
 
214
    data_cache = caching.CacheEntry(request, cache_arena, key+'.data', cache_scope, do_locking=do_locking)
 
215
    data_cache.open(mode='r')
 
216
    return data_cache
 
217
 
 
218
 
 
219
def _do_get(request, key):
 
220
    """ send a complete http response with headers/data cached for key """
 
221
    try:
 
222
        last_modified, headers = _get_headers(request, key)
 
223
        if request.if_modified_since == last_modified:
 
224
            request.emit_http_headers(["Status: 304 Not modified"])
 
225
        else:
 
226
            data_file = _get_datafile(request, key)
 
227
            request.emit_http_headers(headers)
 
228
            request.send_file(data_file)
 
229
    except caching.CacheError:
 
230
        request.emit_http_headers(["Status: 404 Not found"])
 
231
 
 
232
 
 
233
def _do_remove(request, key):
 
234
    """ delete headers/data cache for key """
 
235
    remove(request, key)
 
236
    request.emit_http_headers(["Status: 200 OK"])
 
237
 
 
238
 
 
239
def _do(request, do, key):
 
240
    if do == 'get':
 
241
        _do_get(request, key)
 
242
    elif do == 'remove':
 
243
        _do_remove(request, key)
 
244
 
 
245
def execute(pagename, request):
 
246
    do = request.form.get('do', [None])[0]
 
247
    key = request.form.get('key', [None])[0]
 
248
    _do(request, do, key)
 
249