~ubuntu-branches/ubuntu/oneiric/moin/oneiric-security

« back to all changes in this revision

Viewing changes to MoinMoin/web/contexts.py

  • Committer: Bazaar Package Importer
  • Author(s): Jamie Strandboge
  • Date: 2010-03-30 12:55:34 UTC
  • mfrom: (0.1.17 sid)
  • Revision ID: james.westby@ubuntu.com-20100330125534-4c2ufc1rok24447l
Tags: 1.9.2-2ubuntu1
* Merge from Debian testing (LP: #521834). Based on work by Stefan Ebner.
  Remaining changes:
 - Remove python-xml from Suggests field, the package isn't anymore in
   sys.path.
 - Demote fckeditor from Recommends to Suggests; the code was previously
   embedded in moin, but it was also disabled, so there's no reason for us
   to pull this in by default currently. Note: This isn't necessary anymore
   but needs a MIR for fckeditor, so postpone dropping this change until
   lucid+1
* debian/rules:
  - Replace hardcoded python2.5 with python* and hardcore python2.6 for ln
* debian/control.in: drop versioned depends on cdbs

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
"""
 
3
    MoinMoin - Context objects which are passed thru instead of the classic
 
4
               request objects. Currently contains legacy wrapper code for
 
5
               a single request object.
 
6
 
 
7
    @copyright: 2008-2008 MoinMoin:FlorianKrupicka
 
8
    @license: GNU GPL, see COPYING for details.
 
9
"""
 
10
 
 
11
import time, inspect, StringIO, sys, warnings
 
12
 
 
13
from werkzeug import Headers, http_date, create_environ, redirect, abort
 
14
from werkzeug.exceptions import Unauthorized, NotFound
 
15
 
 
16
from MoinMoin import i18n, error, user, config, wikiutil
 
17
from MoinMoin.config import multiconfig
 
18
from MoinMoin.formatter import text_html
 
19
from MoinMoin.theme import load_theme_fallback
 
20
from MoinMoin.util.clock import Clock
 
21
from MoinMoin.web.request import Request, MoinMoinFinish
 
22
from MoinMoin.web.utils import UniqueIDGenerator
 
23
from MoinMoin.web.exceptions import Forbidden, SurgeProtection
 
24
 
 
25
from MoinMoin import log
 
26
logging = log.getLogger(__name__)
 
27
NoDefault = object()
 
28
 
 
29
class EnvironProxy(property):
 
30
    """ Proxy attribute lookups to keys in the environ. """
 
31
    def __init__(self, name, default=NoDefault):
 
32
        """
 
33
        An entry will be proxied to the supplied name in the .environ
 
34
        object of the property holder. A factory can be supplied, for
 
35
        values that need to be preinstantiated. If given as first
 
36
        parameter name is taken from the callable too.
 
37
 
 
38
        @param name: key (or factory for convenience)
 
39
        @param default: literal object or callable
 
40
        """
 
41
        if not isinstance(name, basestring):
 
42
            default = name
 
43
            name = default.__name__
 
44
        self.name = 'moin.' + name
 
45
        self.default = default
 
46
        property.__init__(self, self.get, self.set, self.delete)
 
47
 
 
48
    def get(self, obj):
 
49
        if self.name in obj.environ:
 
50
            res = obj.environ[self.name]
 
51
        else:
 
52
            factory = self.default
 
53
            if factory is NoDefault:
 
54
                raise AttributeError(self.name)
 
55
            elif hasattr(factory, '__call__'):
 
56
                res = obj.environ.setdefault(self.name, factory(obj))
 
57
            else:
 
58
                res = obj.environ.setdefault(self.name, factory)
 
59
        return res
 
60
 
 
61
    def set(self, obj, value):
 
62
        obj.environ[self.name] = value
 
63
 
 
64
    def delete(self, obj):
 
65
        del obj.environ[self.name]
 
66
 
 
67
    def __repr__(self):
 
68
        return "<%s for '%s'>" % (self.__class__.__name__,
 
69
                                  self.name)
 
70
 
 
71
class Context(object):
 
72
    """ Standard implementation for the context interface.
 
73
 
 
74
    This one wraps up a Moin-Request object and the associated
 
75
    environ and also keeps track of it's changes.
 
76
    """
 
77
    def __init__(self, request):
 
78
        assert isinstance(request, Request)
 
79
 
 
80
        self.request = request
 
81
        self.environ = environ = request.environ
 
82
        self.personalities = self.environ.setdefault(
 
83
            'context.personalities', []
 
84
        )
 
85
        self.personalities.append(self.__class__.__name__)
 
86
 
 
87
    def become(self, cls):
 
88
        """ Become another context, based on given class.
 
89
 
 
90
        @param cls: class to change to, must be a sister class
 
91
        @rtype: boolean
 
92
        @return: wether a class change took place
 
93
        """
 
94
        if self.__class__ is cls:
 
95
            return False
 
96
        else:
 
97
            self.personalities.append(cls)
 
98
            self.__class__ = cls
 
99
            return True
 
100
 
 
101
    def __repr__(self):
 
102
        return "<%s %r>" % (self.__class__.__name__, self.personalities)
 
103
 
 
104
class BaseContext(Context):
 
105
    """ Implements a basic context, that provides some common attributes.
 
106
    Most attributes are lazily initialized via descriptors. """
 
107
 
 
108
    # first the trivial attributes
 
109
    action = EnvironProxy('action', lambda o: o.request.values.get('action', 'show'))
 
110
    clock = EnvironProxy('clock', lambda o: Clock())
 
111
    user = EnvironProxy('user', lambda o: user.User(o, auth_method='request:invalid'))
 
112
 
 
113
    lang = EnvironProxy('lang')
 
114
    content_lang = EnvironProxy('content_lang', lambda o: o.cfg.language_default)
 
115
    current_lang = EnvironProxy('current_lang')
 
116
 
 
117
    html_formatter = EnvironProxy('html_formatter', lambda o: text_html.Formatter(o))
 
118
    formatter = EnvironProxy('formatter', lambda o: o.html_formatter)
 
119
 
 
120
    page = EnvironProxy('page', None)
 
121
 
 
122
    # now the more complex factories
 
123
    def cfg(self):
 
124
        if self.request.given_config is not None:
 
125
            return self.request.given_config('MoinMoin._tests.wikiconfig')
 
126
        try:
 
127
            self.clock.start('load_multi_cfg')
 
128
            cfg = multiconfig.getConfig(self.request.url)
 
129
            self.clock.stop('load_multi_cfg')
 
130
            return cfg
 
131
        except error.NoConfigMatchedError:
 
132
            raise NotFound('<p>No wiki configuration matching the URL found!</p>')
 
133
    cfg = EnvironProxy(cfg)
 
134
 
 
135
    def getText(self):
 
136
        lang = self.lang
 
137
        def _(text, i18n=i18n, request=self, lang=lang, **kw):
 
138
            return i18n.getText(text, request, lang, **kw)
 
139
        return _
 
140
 
 
141
    getText = property(getText)
 
142
    _ = getText
 
143
 
 
144
    def isSpiderAgent(self):
 
145
        """ Simple check if useragent is a spider bot. """
 
146
        cfg = self.cfg
 
147
        user_agent = self.http_user_agent
 
148
        if user_agent and cfg.cache.ua_spiders:
 
149
            return cfg.cache.ua_spiders.search(user_agent) is not None
 
150
        return False
 
151
    isSpiderAgent = EnvironProxy(isSpiderAgent)
 
152
 
 
153
    def rootpage(self):
 
154
        from MoinMoin.Page import RootPage
 
155
        return RootPage(self)
 
156
    rootpage = EnvironProxy(rootpage)
 
157
 
 
158
    def rev(self):
 
159
        try:
 
160
            return int(self.values['rev'])
 
161
        except:
 
162
            return None
 
163
    rev = EnvironProxy(rev)
 
164
 
 
165
    def _theme(self):
 
166
        self.initTheme()
 
167
        return self.theme
 
168
    theme = EnvironProxy('theme', _theme)
 
169
 
 
170
    # finally some methods to act on those attributes
 
171
    def setContentLanguage(self, lang):
 
172
        """ Set the content language, used for the content div
 
173
 
 
174
        Actions that generate content in the user language, like search,
 
175
        should set the content direction to the user language before they
 
176
        call send_title!
 
177
        """
 
178
        self.content_lang = lang
 
179
        self.current_lang = lang
 
180
 
 
181
    def initTheme(self):
 
182
        """ Set theme - forced theme, user theme or wiki default """
 
183
        if self.cfg.theme_force:
 
184
            theme_name = self.cfg.theme_default
 
185
        else:
 
186
            theme_name = self.user.theme_name
 
187
        load_theme_fallback(self, theme_name)
 
188
 
 
189
 
 
190
class HTTPContext(BaseContext):
 
191
    """ Context that holds attributes and methods for manipulation of
 
192
    incoming and outgoing HTTP data. """
 
193
 
 
194
    session = EnvironProxy('session')
 
195
    _auth_redirected = EnvironProxy('old._auth_redirected', 0)
 
196
    cacheable = EnvironProxy('old.cacheable', 0)
 
197
    writestack = EnvironProxy('old.writestack', lambda o: list())
 
198
 
 
199
    # proxy some descriptors of the underlying WSGI request, since
 
200
    # setting on those does not work over __(g|s)etattr__-proxies
 
201
    class _proxy(property):
 
202
        def __init__(self, name):
 
203
            self.name = name
 
204
            property.__init__(self, self.get, self.set, self.delete)
 
205
        def get(self, obj):
 
206
            return getattr(obj.request, self.name)
 
207
        def set(self, obj, value):
 
208
            setattr(obj.request, self.name, value)
 
209
        def delete(self, obj):
 
210
            delattr(obj.request, self.name)
 
211
 
 
212
    mimetype = _proxy('mimetype')
 
213
    content_type = _proxy('content_type')
 
214
    status = _proxy('status')
 
215
    status_code = _proxy('status_code')
 
216
 
 
217
    del _proxy
 
218
 
 
219
    # proxy further attribute lookups to the underlying request first
 
220
    def __getattr__(self, name):
 
221
        try:
 
222
            return getattr(self.request, name)
 
223
        except AttributeError, e:
 
224
            return super(HTTPContext, self).__getattribute__(name)
 
225
 
 
226
    # methods regarding manipulation of HTTP related data
 
227
    def read(self, n=None):
 
228
        """ Read n bytes (or everything) from input stream. """
 
229
        if n is None:
 
230
            return self.request.in_data
 
231
        else:
 
232
            return self.request.in_stream.read(n)
 
233
 
 
234
    def makeForbidden(self, resultcode, msg):
 
235
        status = {401: Unauthorized,
 
236
                  403: Forbidden,
 
237
                  404: NotFound,
 
238
                  503: SurgeProtection}
 
239
        raise status[resultcode](msg)
 
240
 
 
241
    def setHttpHeader(self, header):
 
242
        logging.warning("Deprecated call to request.setHttpHeader('k:v'), use request.headers.add/set('k', 'v')")
 
243
        header, value = header.split(':', 1)
 
244
        self.headers.add(header, value)
 
245
 
 
246
    def disableHttpCaching(self, level=1):
 
247
        """ Prevent caching of pages that should not be cached.
 
248
 
 
249
        level == 1 means disabling caching when we have a cookie set
 
250
        level == 2 means completely disabling caching (used by Page*Editor)
 
251
 
 
252
        This is important to prevent caches break acl by providing one
 
253
        user pages meant to be seen only by another user, when both users
 
254
        share the same caching proxy.
 
255
 
 
256
        AVOID using no-cache and no-store for attachments as it is completely broken on IE!
 
257
 
 
258
        Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP
 
259
        """
 
260
        if level == 1 and self.headers.get('Pragma') == 'no-cache':
 
261
            return
 
262
 
 
263
        if level == 1:
 
264
            self.headers['Cache-Control'] = 'private, must-revalidate, max-age=10'
 
265
        elif level == 2:
 
266
            self.headers['Cache-Control'] = 'no-cache'
 
267
            self.headers['Pragma'] = 'no-cache'
 
268
        self.request.expires = time.time() - 3600 * 24 * 365
 
269
 
 
270
    def http_redirect(self, url, code=302):
 
271
        """ Raise a simple redirect exception. """
 
272
        abort(redirect(url, code=code))
 
273
 
 
274
    def http_user_agent(self):
 
275
        return self.environ.get('HTTP_USER_AGENT', '')
 
276
    http_user_agent = EnvironProxy(http_user_agent)
 
277
 
 
278
    def http_referer(self):
 
279
        return self.environ.get('HTTP_REFERER', '')
 
280
    http_referer = EnvironProxy(http_referer)
 
281
 
 
282
    # the output related methods
 
283
    def write(self, *data):
 
284
        """ Write to output stream. """
 
285
        self.request.out_stream.writelines(data)
 
286
 
 
287
    def redirectedOutput(self, function, *args, **kw):
 
288
        """ Redirect output during function, return redirected output """
 
289
        buf = StringIO.StringIO()
 
290
        self.redirect(buf)
 
291
        try:
 
292
            function(*args, **kw)
 
293
        finally:
 
294
            self.redirect()
 
295
        text = buf.getvalue()
 
296
        buf.close()
 
297
        return text
 
298
 
 
299
    def redirect(self, file=None):
 
300
        """ Redirect output to file, or restore saved output """
 
301
        if file:
 
302
            self.writestack.append(self.write)
 
303
            self.write = file.write
 
304
        else:
 
305
            self.write = self.writestack.pop()
 
306
 
 
307
    def send_file(self, fileobj, bufsize=8192, do_flush=None):
 
308
        """ Send a file to the output stream.
 
309
 
 
310
        @param fileobj: a file-like object (supporting read, close)
 
311
        @param bufsize: size of chunks to read/write
 
312
        @param do_flush: call flush after writing?
 
313
        """
 
314
        def simple_wrapper(fileobj, bufsize):
 
315
            return iter(lambda: fileobj.read(bufsize), '')
 
316
        file_wrapper = self.environ.get('wsgi.file_wrapper', simple_wrapper)
 
317
        self.request.direct_passthrough = True
 
318
        self.request.response = file_wrapper(fileobj, bufsize)
 
319
        raise MoinMoinFinish('sent file')
 
320
 
 
321
    # fully deprecated functions, with warnings
 
322
    def getScriptname(self):
 
323
        warnings.warn(
 
324
            "request.getScriptname() is deprecated, please use the request's script_root property.",
 
325
            DeprecationWarning)
 
326
        return self.request.script_root
 
327
 
 
328
    def getBaseURL(self):
 
329
        warnings.warn(
 
330
            "request.getBaseURL() is deprecated, please use the request's "
 
331
            "url_root property or the abs_href object if urls should be generated.",
 
332
            DeprecationWarning)
 
333
        return self.request.url_root
 
334
 
 
335
    def getQualifiedURL(self, uri=''):
 
336
        """ Return an absolute URL starting with schema and host.
 
337
 
 
338
        Already qualified urls are returned unchanged.
 
339
 
 
340
        @param uri: server rooted uri e.g /scriptname/pagename.
 
341
                    It must start with a slash. Must be ascii and url encoded.
 
342
        """
 
343
        import urlparse
 
344
        scheme = urlparse.urlparse(uri)[0]
 
345
        if scheme:
 
346
            return uri
 
347
 
 
348
        host_url = self.request.host_url.rstrip('/')
 
349
        result = "%s%s" % (host_url, uri)
 
350
 
 
351
        # This might break qualified urls in redirects!
 
352
        # e.g. mapping 'http://netloc' -> '/'
 
353
        result = wikiutil.mapURL(self, result)
 
354
        return result
 
355
 
 
356
class AuxilaryMixin(object):
 
357
    """
 
358
    Mixin for diverse attributes and methods that aren't clearly assignable
 
359
    to a particular phase of the request.
 
360
    """
 
361
 
 
362
    # several attributes used by other code to hold state across calls
 
363
    _fmt_hd_counters = EnvironProxy('_fmt_hd_counters')
 
364
    parsePageLinks_running = EnvironProxy('parsePageLinks_running', lambda o: {})
 
365
    mode_getpagelinks = EnvironProxy('mode_getpagelinks', 0)
 
366
 
 
367
    pragma = EnvironProxy('pragma', lambda o: {})
 
368
    _login_messages = EnvironProxy('_login_messages', lambda o: [])
 
369
    _login_multistage = EnvironProxy('_login_multistage', None)
 
370
    _login_multistage_name = EnvironProxy('_login_multistage_name', None)
 
371
    _setuid_real_user = EnvironProxy('_setuid_real_user', None)
 
372
    pages = EnvironProxy('pages', lambda o: {})
 
373
 
 
374
    def uid_generator(self):
 
375
        pagename = None
 
376
        if hasattr(self, 'page') and hasattr(self.page, 'page_name'):
 
377
            pagename = self.page.page_name
 
378
        return UniqueIDGenerator(pagename=pagename)
 
379
    uid_generator = EnvironProxy(uid_generator)
 
380
 
 
381
    def dicts(self):
 
382
        """ Lazy initialize the dicts on the first access """
 
383
        dicts = self.cfg.dicts(self)
 
384
        return dicts
 
385
    dicts = EnvironProxy(dicts)
 
386
 
 
387
    def groups(self):
 
388
        """ Lazy initialize the groups on the first access """
 
389
        groups = self.cfg.groups(self)
 
390
        return groups
 
391
    groups = EnvironProxy(groups)
 
392
 
 
393
    def reset(self):
 
394
        self.current_lang = self.cfg.language_default
 
395
        if hasattr(self, '_fmt_hd_counters'):
 
396
            del self._fmt_hd_counters
 
397
        if hasattr(self, 'uid_generator'):
 
398
            del self.uid_generator
 
399
 
 
400
    def getPragma(self, key, defval=None):
 
401
        """ Query a pragma value (#pragma processing instruction)
 
402
 
 
403
            Keys are not case-sensitive.
 
404
        """
 
405
        return self.pragma.get(key.lower(), defval)
 
406
 
 
407
    def setPragma(self, key, value):
 
408
        """ Set a pragma value (#pragma processing instruction)
 
409
 
 
410
            Keys are not case-sensitive.
 
411
        """
 
412
        self.pragma[key.lower()] = value
 
413
 
 
414
class XMLRPCContext(HTTPContext, AuxilaryMixin):
 
415
    """ Context to act during a XMLRPC request. """
 
416
 
 
417
class AllContext(HTTPContext, AuxilaryMixin):
 
418
    """ Catchall context to be able to quickly test old Moin code. """
 
419
 
 
420
class ScriptContext(AllContext):
 
421
    """ Context to act in scripting environments (e.g. former request_cli).
 
422
 
 
423
    For input, sys.stdin is used as 'wsgi.input', output is written directly
 
424
    to sys.stdout though.
 
425
    """
 
426
    def __init__(self, url=None, pagename=''):
 
427
        if url is None:
 
428
            url = 'http://localhost:0/' # just some somehow valid dummy URL
 
429
        environ = create_environ(base_url=url) # XXX not sure about base_url, but makes "make underlay" work
 
430
        environ['HTTP_USER_AGENT'] = 'CLI/Script'
 
431
        environ['wsgi.input'] = sys.stdin
 
432
        request = Request(environ)
 
433
        super(ScriptContext, self).__init__(request)
 
434
        from MoinMoin import wsgiapp
 
435
        wsgiapp.init(self)
 
436
 
 
437
    def write(self, *data):
 
438
        for d in data:
 
439
            if isinstance(d, unicode):
 
440
                d = d.encode(config.charset)
 
441
            else:
 
442
                d = str(d)
 
443
            sys.stdout.write(d)