Back to index

moin  1.9.0~rc2
utils.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Utility functions for the web-layer
00004 
00005     @copyright: 2003-2008 MoinMoin:ThomasWaldmann,
00006                 2008-2008 MoinMoin:FlorianKrupicka
00007     @license: GNU GPL, see COPYING for details.
00008 """
00009 import time
00010 
00011 from werkzeug import abort, redirect, cookie_date, Response
00012 
00013 from MoinMoin import caching
00014 from MoinMoin import log
00015 from MoinMoin import wikiutil
00016 from MoinMoin.Page import Page
00017 from MoinMoin.web.exceptions import Forbidden, SurgeProtection
00018 
00019 logging = log.getLogger(__name__)
00020 
00021 def check_forbidden(request):
00022     """ Simple action and host access checks
00023 
00024     Spider agents are checked against the called actions,
00025     hosts against the blacklist. Raises Forbidden if triggered.
00026     """
00027     args = request.args
00028     action = args.get('action')
00029     if ((args or request.method != 'GET') and
00030         action not in ['rss_rc', 'show', 'sitemap'] and
00031         not (action == 'AttachFile' and args.get('do') == 'get')):
00032         if request.isSpiderAgent:
00033             raise Forbidden()
00034     if request.cfg.hosts_deny:
00035         remote_addr = request.remote_addr
00036         for host in request.cfg.hosts_deny:
00037             if host[-1] == '.' and remote_addr.startswith(host):
00038                 logging.debug("hosts_deny (net): %s" % remote_addr)
00039                 raise Forbidden()
00040             if remote_addr == host:
00041                 logging.debug("hosts_deny (ip): %s" % remote_addr)
00042                 raise Forbidden()
00043     return False
00044 
00045 def check_surge_protect(request, kick=False):
00046     """ Check for excessive requests
00047 
00048     Raises a SurgeProtection exception on wiki overuse.
00049 
00050     @param request: a moin request object
00051     """
00052     limits = request.cfg.surge_action_limits
00053     if not limits:
00054         return False
00055 
00056     remote_addr = request.remote_addr or ''
00057     if remote_addr.startswith('127.'):
00058         return False
00059 
00060     validuser = request.user.valid
00061     current_id = validuser and request.user.name or remote_addr
00062     current_action = request.action
00063 
00064     default_limit = limits.get('default', (30, 60))
00065 
00066     now = int(time.time())
00067     surgedict = {}
00068     surge_detected = False
00069 
00070     try:
00071         # if we have common farm users, we could also use scope='farm':
00072         cache = caching.CacheEntry(request, 'surgeprotect', 'surge-log', scope='wiki', use_encode=True)
00073         if cache.exists():
00074             data = cache.content()
00075             data = data.split("\n")
00076             for line in data:
00077                 try:
00078                     id, t, action, surge_indicator = line.split("\t")
00079                     t = int(t)
00080                     maxnum, dt = limits.get(action, default_limit)
00081                     if t >= now - dt:
00082                         events = surgedict.setdefault(id, {})
00083                         timestamps = events.setdefault(action, [])
00084                         timestamps.append((t, surge_indicator))
00085                 except StandardError:
00086                     pass
00087 
00088         maxnum, dt = limits.get(current_action, default_limit)
00089         events = surgedict.setdefault(current_id, {})
00090         timestamps = events.setdefault(current_action, [])
00091         surge_detected = len(timestamps) > maxnum
00092 
00093         surge_indicator = surge_detected and "!" or ""
00094         timestamps.append((now, surge_indicator))
00095         if surge_detected:
00096             if len(timestamps) < maxnum * 2:
00097                 timestamps.append((now + request.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
00098 
00099         if current_action not in ('cache', 'AttachFile', ): # don't add cache/AttachFile accesses to all or picture galleries will trigger SP
00100             current_action = 'all' # put a total limit on user's requests
00101             maxnum, dt = limits.get(current_action, default_limit)
00102             events = surgedict.setdefault(current_id, {})
00103             timestamps = events.setdefault(current_action, [])
00104 
00105             if kick: # ban this guy, NOW
00106                 timestamps.extend([(now + request.cfg.surge_lockout_time, "!")] * (2 * maxnum))
00107 
00108             surge_detected = surge_detected or len(timestamps) > maxnum
00109 
00110             surge_indicator = surge_detected and "!" or ""
00111             timestamps.append((now, surge_indicator))
00112             if surge_detected:
00113                 if len(timestamps) < maxnum * 2:
00114                     timestamps.append((now + request.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
00115 
00116         data = []
00117         for id, events in surgedict.items():
00118             for action, timestamps in events.items():
00119                 for t, surge_indicator in timestamps:
00120                     data.append("%s\t%d\t%s\t%s" % (id, t, action, surge_indicator))
00121         data = "\n".join(data)
00122         cache.update(data)
00123     except StandardError:
00124         pass
00125 
00126     if surge_detected and validuser and request.user.auth_method in request.cfg.auth_methods_trusted:
00127         logging.info("Trusted user %s would have triggered surge protection if not trusted.", request.user.name)
00128         return False
00129     elif surge_detected:
00130         raise SurgeProtection(retry_after=request.cfg.surge_lockout_time)
00131     else:
00132         return False
00133 
00134 def redirect_last_visited(request):
00135     pagetrail = request.user.getTrail()
00136     if pagetrail:
00137         # Redirect to last page visited
00138         last_visited = pagetrail[-1]
00139         wikiname, pagename = wikiutil.split_interwiki(last_visited)
00140         if wikiname != 'Self':
00141             wikitag, wikiurl, wikitail, error = wikiutil.resolve_interwiki(request, wikiname, pagename)
00142             url = wikiurl + wikiutil.quoteWikinameURL(wikitail)
00143         else:
00144             url = Page(request, pagename).url(request)
00145     else:
00146         # Or to localized FrontPage
00147         url = wikiutil.getFrontPage(request).url(request)
00148     url = request.getQualifiedURL(url)
00149     return abort(redirect(url))
00150 
00151 class UniqueIDGenerator(object):
00152     def __init__(self, pagename=None):
00153         self.unique_stack = []
00154         self.include_stack = []
00155         self.include_id = None
00156         self.page_ids = {None: {}}
00157         self.pagename = pagename
00158 
00159     def push(self):
00160         """
00161         Used by the TOC macro, this ensures that the ID namespaces
00162         are reset to the status when the current include started.
00163         This guarantees that doing the ID enumeration twice results
00164         in the same results, on any level.
00165         """
00166         self.unique_stack.append((self.page_ids, self.include_id))
00167         self.include_id, pids = self.include_stack[-1]
00168         self.page_ids = {}
00169         for namespace in pids:
00170             self.page_ids[namespace] = pids[namespace].copy()
00171 
00172     def pop(self):
00173         """
00174         Used by the TOC macro to reset the ID namespaces after
00175         having parsed the page for TOC generation and after
00176         printing the TOC.
00177         """
00178         self.page_ids, self.include_id = self.unique_stack.pop()
00179         return self.page_ids, self.include_id
00180 
00181     def begin(self, base):
00182         """
00183         Called by the formatter when a document begins, which means
00184         that include causing nested documents gives us an include
00185         stack in self.include_id_stack.
00186         """
00187         pids = {}
00188         for namespace in self.page_ids:
00189             pids[namespace] = self.page_ids[namespace].copy()
00190         self.include_stack.append((self.include_id, pids))
00191         self.include_id = self(base)
00192         # if it's the page name then set it to None so we don't
00193         # prepend anything to IDs, but otherwise keep it.
00194         if self.pagename and self.pagename == self.include_id:
00195             self.include_id = None
00196 
00197     def end(self):
00198         """
00199         Called by the formatter when a document ends, restores
00200         the current include ID to the previous one and discards
00201         the page IDs state we kept around for push().
00202         """
00203         self.include_id, pids = self.include_stack.pop()
00204 
00205     def __call__(self, base, namespace=None):
00206         """
00207         Generates a unique ID using a given base name. Appends a running count to the base.
00208 
00209         Needs to stay deterministic!
00210 
00211         @param base: the base of the id
00212         @type base: unicode
00213         @param namespace: the namespace for the ID, used when including pages
00214 
00215         @returns: a unique (relatively to the namespace) ID
00216         @rtype: unicode
00217         """
00218         if not isinstance(base, unicode):
00219             base = unicode(str(base), 'ascii', 'ignore')
00220         if not namespace in self.page_ids:
00221             self.page_ids[namespace] = {}
00222         count = self.page_ids[namespace].get(base, -1) + 1
00223         self.page_ids[namespace][base] = count
00224         if not count:
00225             return base
00226         return u'%s-%d' % (base, count)
00227 
00228 FATALTMPL = """
00229 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
00230 <html>
00231 <head><title>%(title)s</title></head>
00232 <body><h1>%(title)s</h1>
00233 <pre>
00234 %(body)s
00235 </pre></body></html>
00236 """
00237 def fatal_response(error):
00238     """ Create a response from MoinMoin.error.FatalError instances. """
00239     html = FATALTMPL % dict(title=error.__class__.__name__,
00240                             body=str(error))
00241     return Response(html, status=500, mimetype='text/html')