Back to index

moin  1.9.0~rc2
contexts.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Context objects which are passed thru instead of the classic
00004                request objects. Currently contains legacy wrapper code for
00005                a single request object.
00006 
00007     @copyright: 2008-2008 MoinMoin:FlorianKrupicka
00008     @license: GNU GPL, see COPYING for details.
00009 """
00010 
00011 import time, inspect, StringIO, sys, warnings
00012 
00013 from werkzeug import Headers, http_date, create_environ, redirect, abort
00014 from werkzeug.exceptions import Unauthorized, NotFound
00015 
00016 from MoinMoin import i18n, error, user, config, wikiutil
00017 from MoinMoin.config import multiconfig
00018 from MoinMoin.formatter import text_html
00019 from MoinMoin.theme import load_theme_fallback
00020 from MoinMoin.util.clock import Clock
00021 from MoinMoin.web.request import Request, MoinMoinFinish
00022 from MoinMoin.web.utils import UniqueIDGenerator
00023 from MoinMoin.web.exceptions import Forbidden, SurgeProtection
00024 
00025 from MoinMoin import log
00026 logging = log.getLogger(__name__)
00027 NoDefault = object()
00028 
00029 class EnvironProxy(property):
00030     """ Proxy attribute lookups to keys in the environ. """
00031     def __init__(self, name, default=NoDefault):
00032         """
00033         An entry will be proxied to the supplied name in the .environ
00034         object of the property holder. A factory can be supplied, for
00035         values that need to be preinstantiated. If given as first
00036         parameter name is taken from the callable too.
00037 
00038         @param name: key (or factory for convenience)
00039         @param default: literal object or callable
00040         """
00041         if not isinstance(name, basestring):
00042             default = name
00043             name = default.__name__
00044         self.name = 'moin.' + name
00045         self.default = default
00046         property.__init__(self, self.get, self.set, self.delete)
00047 
00048     def get(self, obj):
00049         if self.name in obj.environ:
00050             res = obj.environ[self.name]
00051         else:
00052             factory = self.default
00053             if factory is NoDefault:
00054                 raise AttributeError(self.name)
00055             elif hasattr(factory, '__call__'):
00056                 res = obj.environ.setdefault(self.name, factory(obj))
00057             else:
00058                 res = obj.environ.setdefault(self.name, factory)
00059         return res
00060 
00061     def set(self, obj, value):
00062         obj.environ[self.name] = value
00063 
00064     def delete(self, obj):
00065         del obj.environ[self.name]
00066 
00067     def __repr__(self):
00068         return "<%s for '%s'>" % (self.__class__.__name__,
00069                                   self.name)
00070 
00071 class Context(object):
00072     """ Standard implementation for the context interface.
00073 
00074     This one wraps up a Moin-Request object and the associated
00075     environ and also keeps track of it's changes.
00076     """
00077     def __init__(self, request):
00078         assert isinstance(request, Request)
00079 
00080         self.request = request
00081         self.environ = environ = request.environ
00082         self.personalities = self.environ.setdefault(
00083             'context.personalities', []
00084         )
00085         self.personalities.append(self.__class__.__name__)
00086 
00087     def become(self, cls):
00088         """ Become another context, based on given class.
00089 
00090         @param cls: class to change to, must be a sister class
00091         @rtype: boolean
00092         @return: wether a class change took place
00093         """
00094         if self.__class__ is cls:
00095             return False
00096         else:
00097             self.personalities.append(cls)
00098             self.__class__ = cls
00099             return True
00100 
00101     def __repr__(self):
00102         return "<%s %r>" % (self.__class__.__name__, self.personalities)
00103 
00104 class BaseContext(Context):
00105     """ Implements a basic context, that provides some common attributes.
00106     Most attributes are lazily initialized via descriptors. """
00107 
00108     # first the trivial attributes
00109     action = EnvironProxy('action', lambda o: o.request.values.get('action', 'show'))
00110     clock = EnvironProxy('clock', lambda o: Clock())
00111     user = EnvironProxy('user', lambda o: user.User(o, auth_method='request:invalid'))
00112 
00113     lang = EnvironProxy('lang')
00114     content_lang = EnvironProxy('content_lang', lambda o: o.cfg.language_default)
00115     current_lang = EnvironProxy('current_lang')
00116 
00117     html_formatter = EnvironProxy('html_formatter', lambda o: text_html.Formatter(o))
00118     formatter = EnvironProxy('formatter', lambda o: o.html_formatter)
00119 
00120     page = EnvironProxy('page', None)
00121 
00122     # now the more complex factories
00123     def cfg(self):
00124         if self.request.given_config is not None:
00125             return self.request.given_config('MoinMoin._tests.wikiconfig')
00126         try:
00127             self.clock.start('load_multi_cfg')
00128             cfg = multiconfig.getConfig(self.request.url)
00129             self.clock.stop('load_multi_cfg')
00130             return cfg
00131         except error.NoConfigMatchedError:
00132             raise NotFound('<p>No wiki configuration matching the URL found!</p>')
00133     cfg = EnvironProxy(cfg)
00134 
00135     def getText(self):
00136         lang = self.lang
00137         def _(text, i18n=i18n, request=self, lang=lang, **kw):
00138             return i18n.getText(text, request, lang, **kw)
00139         return _
00140 
00141     getText = property(getText)
00142     _ = getText
00143 
00144     def isSpiderAgent(self):
00145         """ Simple check if useragent is a spider bot. """
00146         cfg = self.cfg
00147         user_agent = self.request.user_agent
00148         if user_agent and cfg.cache.ua_spiders:
00149             return cfg.cache.ua_spiders.search(user_agent.browser) is not None
00150         return False
00151     isSpiderAgent = EnvironProxy(isSpiderAgent)
00152 
00153     def rootpage(self):
00154         from MoinMoin.Page import RootPage
00155         return RootPage(self)
00156     rootpage = EnvironProxy(rootpage)
00157 
00158     def rev(self):
00159         try:
00160             return int(self.values['rev'])
00161         except:
00162             return None
00163     rev = EnvironProxy(rev)
00164 
00165     def _theme(self):
00166         self.initTheme()
00167         return self.theme
00168     theme = EnvironProxy('theme', _theme)
00169 
00170     # finally some methods to act on those attributes
00171     def setContentLanguage(self, lang):
00172         """ Set the content language, used for the content div
00173 
00174         Actions that generate content in the user language, like search,
00175         should set the content direction to the user language before they
00176         call send_title!
00177         """
00178         self.content_lang = lang
00179         self.current_lang = lang
00180 
00181     def initTheme(self):
00182         """ Set theme - forced theme, user theme or wiki default """
00183         if self.cfg.theme_force:
00184             theme_name = self.cfg.theme_default
00185         else:
00186             theme_name = self.user.theme_name
00187         load_theme_fallback(self, theme_name)
00188 
00189 
00190 class HTTPContext(BaseContext):
00191     """ Context that holds attributes and methods for manipulation of
00192     incoming and outgoing HTTP data. """
00193 
00194     session = EnvironProxy('session')
00195     _auth_redirected = EnvironProxy('old._auth_redirected', 0)
00196     cacheable = EnvironProxy('old.cacheable', 0)
00197     writestack = EnvironProxy('old.writestack', lambda o: list())
00198 
00199     # proxy some descriptors of the underlying WSGI request, since
00200     # setting on those does not work over __(g|s)etattr__-proxies
00201     class _proxy(property):
00202         def __init__(self, name):
00203             self.name = name
00204             property.__init__(self, self.get, self.set, self.delete)
00205         def get(self, obj):
00206             return getattr(obj.request, self.name)
00207         def set(self, obj, value):
00208             setattr(obj.request, self.name, value)
00209         def delete(self, obj):
00210             delattr(obj.request, self.name)
00211 
00212     mimetype = _proxy('mimetype')
00213     content_type = _proxy('content_type')
00214     status = _proxy('status')
00215     status_code = _proxy('status_code')
00216 
00217     del _proxy
00218 
00219     # proxy further attribute lookups to the underlying request first
00220     def __getattr__(self, name):
00221         try:
00222             return getattr(self.request, name)
00223         except AttributeError, e:
00224             return super(HTTPContext, self).__getattribute__(name)
00225 
00226     # methods regarding manipulation of HTTP related data
00227     def read(self, n=None):
00228         """ Read n bytes (or everything) from input stream. """
00229         if n is None:
00230             return self.request.in_data
00231         else:
00232             return self.request.in_stream.read(n)
00233 
00234     def makeForbidden(self, resultcode, msg):
00235         status = {401: Unauthorized,
00236                   403: Forbidden,
00237                   404: NotFound,
00238                   503: SurgeProtection}
00239         raise status[resultcode](msg)
00240 
00241     def setHttpHeader(self, header):
00242         header, value = header.split(':', 1)
00243         self.headers.add(header, value)
00244 
00245     def disableHttpCaching(self, level=1):
00246         """ Prevent caching of pages that should not be cached.
00247 
00248         level == 1 means disabling caching when we have a cookie set
00249         level == 2 means completely disabling caching (used by Page*Editor)
00250 
00251         This is important to prevent caches break acl by providing one
00252         user pages meant to be seen only by another user, when both users
00253         share the same caching proxy.
00254 
00255         AVOID using no-cache and no-store for attachments as it is completely broken on IE!
00256 
00257         Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP
00258         """
00259         if level == 1 and self.headers.get('Pragma') == 'no-cache':
00260             return
00261 
00262         if level == 1:
00263             self.headers.set('Cache-Control', 'private, must-revalidate, max-age=10')
00264         elif level == 2:
00265             self.headers.set('Cache-Control', 'no-cache')
00266             self.headers.set('Pragma', 'no-cache')
00267         self.request.expires = time.time() - 3600 * 24 * 365
00268 
00269     def http_redirect(self, url, code=302):
00270         """ Raise a simple redirect exception. """
00271         abort(redirect(url, code=code))
00272 
00273     # the output related methods
00274     def write(self, *data):
00275         """ Write to output stream. """
00276         self.request.stream.writelines(data)
00277 
00278     def redirectedOutput(self, function, *args, **kw):
00279         """ Redirect output during function, return redirected output """
00280         buf = StringIO.StringIO()
00281         self.redirect(buf)
00282         try:
00283             function(*args, **kw)
00284         finally:
00285             self.redirect()
00286         text = buf.getvalue()
00287         buf.close()
00288         return text
00289 
00290     def redirect(self, file=None):
00291         """ Redirect output to file, or restore saved output """
00292         if file:
00293             self.writestack.append(self.write)
00294             self.write = file.write
00295         else:
00296             self.write = self.writestack.pop()
00297 
00298     def send_file(self, fileobj, bufsize=8192, do_flush=None):
00299         """ Send a file to the output stream.
00300 
00301         @param fileobj: a file-like object (supporting read, close)
00302         @param bufsize: size of chunks to read/write
00303         @param do_flush: call flush after writing?
00304         """
00305         def simple_wrapper(fileobj, bufsize):
00306             return iter(lambda: fileobj.read(bufsize), '')
00307         file_wrapper = self.environ.get('wsgi.file_wrapper', simple_wrapper)
00308         self.request.direct_passthrough = True
00309         self.request.response = file_wrapper(fileobj, bufsize)
00310         raise MoinMoinFinish('sent file')
00311 
00312     # fully deprecated functions, with warnings
00313     def getScriptname(self):
00314         warnings.warn(
00315             "request.getScriptname() is deprecated, please use the request's script_root property.",
00316             DeprecationWarning)
00317         return self.request.script_root
00318 
00319     def getBaseURL(self):
00320         warnings.warn(
00321             "request.getBaseURL() is deprecated, please use the request's "
00322             "url_root property or the abs_href object if urls should be generated.",
00323             DeprecationWarning)
00324         return self.request.url_root
00325 
00326     def getQualifiedURL(self, uri=''):
00327         """ Return an absolute URL starting with schema and host.
00328 
00329         Already qualified urls are returned unchanged.
00330 
00331         @param uri: server rooted uri e.g /scriptname/pagename.
00332                     It must start with a slash. Must be ascii and url encoded.
00333         """
00334         import urlparse
00335         scheme = urlparse.urlparse(uri)[0]
00336         if scheme:
00337             return uri
00338 
00339         host_url = self.request.host_url.rstrip('/')
00340         result = "%s%s" % (host_url, uri)
00341 
00342         # This might break qualified urls in redirects!
00343         # e.g. mapping 'http://netloc' -> '/'
00344         result = wikiutil.mapURL(self, result)
00345         return result
00346 
00347 class AuxilaryMixin(object):
00348     """
00349     Mixin for diverse attributes and methods that aren't clearly assignable
00350     to a particular phase of the request.
00351     """
00352 
00353     # several attributes used by other code to hold state across calls
00354     _fmt_hd_counters = EnvironProxy('_fmt_hd_counters')
00355     parsePageLinks_running = EnvironProxy('parsePageLinks_running', lambda o: {})
00356     mode_getpagelinks = EnvironProxy('mode_getpagelinks', 0)
00357 
00358     pragma = EnvironProxy('pragma', lambda o: {})
00359     _login_messages = EnvironProxy('_login_messages', lambda o: [])
00360     _login_multistage = EnvironProxy('_login_multistage', None)
00361     _login_multistage_name = EnvironProxy('_login_multistage_name', None)
00362     _setuid_real_user = EnvironProxy('_setuid_real_user', None)
00363     pages = EnvironProxy('pages', lambda o: {})
00364 
00365     def uid_generator(self):
00366         pagename = None
00367         if hasattr(self, 'page') and hasattr(self.page, 'page_name'):
00368             pagename = self.page.page_name
00369         return UniqueIDGenerator(pagename=pagename)
00370     uid_generator = EnvironProxy(uid_generator)
00371 
00372     def dicts(self):
00373         """ Lazy initialize the dicts on the first access """
00374         dicts = self.cfg.dicts(self)
00375         return dicts
00376     dicts = EnvironProxy(dicts)
00377 
00378     def groups(self):
00379         """ Lazy initialize the groups on the first access """
00380         groups = self.cfg.groups(self)
00381         return groups
00382     groups = EnvironProxy(groups)
00383 
00384     def reset(self):
00385         self.current_lang = self.cfg.language_default
00386         if hasattr(self, '_fmt_hd_counters'):
00387             del self._fmt_hd_counters
00388         if hasattr(self, 'uid_generator'):
00389             del self.uid_generator
00390 
00391     def getPragma(self, key, defval=None):
00392         """ Query a pragma value (#pragma processing instruction)
00393 
00394             Keys are not case-sensitive.
00395         """
00396         return self.pragma.get(key.lower(), defval)
00397 
00398     def setPragma(self, key, value):
00399         """ Set a pragma value (#pragma processing instruction)
00400 
00401             Keys are not case-sensitive.
00402         """
00403         self.pragma[key.lower()] = value
00404 
00405 class XMLRPCContext(HTTPContext, AuxilaryMixin):
00406     """ Context to act during a XMLRPC request. """
00407 
00408 class AllContext(HTTPContext, AuxilaryMixin):
00409     """ Catchall context to be able to quickly test old Moin code. """
00410 
00411 class ScriptContext(AllContext):
00412     """ Context to act in scripting environments (e.g. former request_cli).
00413 
00414     For input, sys.stdin is used as 'wsgi.input', output is written directly
00415     to sys.stdout though.
00416     """
00417     def __init__(self, url=None, pagename=''):
00418         if url is None:
00419             url = 'http://localhost:0/' # just some somehow valid dummy URL
00420         environ = create_environ(base_url=url) # XXX not sure about base_url, but makes "make underlay" work
00421         environ['HTTP_USER_AGENT'] = 'CLI/Script'
00422         environ['wsgi.input'] = sys.stdin
00423         request = Request(environ)
00424         super(ScriptContext, self).__init__(request)
00425         from MoinMoin import wsgiapp
00426         wsgiapp.init(self)
00427 
00428     def write(self, *data):
00429         for d in data:
00430             if isinstance(d, unicode):
00431                 d = d.encode(config.charset)
00432             else:
00433                 d = str(d)
00434             sys.stdout.write(d)