diff --git a/wiki/request.py b/wiki/request.py
index 62845603..26849f25 100644
--- a/wiki/request.py
+++ b/wiki/request.py
@@ -7,17 +7,10 @@
@license: GNU GPL, see COPYING for details.
"""
-import os, time, sys, types, cgi
-from MoinMoin import config, wikiutil, user, error
-from MoinMoin.util import MoinMoinNoFooter, IsWin9x, FixScriptName
-
-# this needs sitecustomize.py in python path to work!
-# use our encoding as default encoding:
-## sys.setappdefaultencoding(config.charset)
-# this is the default python uses without this hack:
-## sys.setappdefaultencoding("ascii")
-# we could maybe use this to find places where implicit encodings are used:
-## sys.setappdefaultencoding("undefined")
+import os, re, time, sys, cgi, StringIO
+import copy
+from MoinMoin import config, wikiutil, user, caching
+from MoinMoin.util import MoinMoinNoFooter, IsWin9x
# Timing ---------------------------------------------------------------
@@ -46,10 +39,21 @@ class Clock:
return outlist
+# Utilities
+
+def cgiMetaVariable(header, scheme='http'):
+ """ Return CGI meta variable for header name
+
+ e.g 'User-Agent' -> 'HTTP_USER_AGENT'
+ See http://www.faqs.org/rfcs/rfc3875.html section 4.1.18
+ """
+ var = '%s_%s' % (scheme, header)
+ return var.upper().replace('-', '_')
+
# Request Base ----------------------------------------------------------
-class RequestBase:
+class RequestBase(object):
""" A collection for all data associated with ONE request. """
# Header set to force misbehaved proxies and browsers to keep their
@@ -61,18 +65,33 @@ class RequestBase:
"Expires: -1",
]
+ # Defaults (used by sub classes)
+ http_accept_language = 'en'
+ server_name = 'localhost'
+ server_port = '80'
+
+ # Extra headers we support. Both standalone and twisted store
+ # headers as lowercase.
+ moin_location = 'x-moin-location'
+ proxy_host = 'x-forwarded-host'
+
def __init__(self, properties={}):
+ # Decode values collected by sub classes
+ self.path_info = self.decodePagename(self.path_info)
+
self.failed = 0
self._available_actions = None
self._known_actions = None
# Pages meta data that we collect in one request
self.pages = {}
-
+
self.sent_headers = 0
self.user_headers = []
+ self.cacheable = 0 # may this output get cached by http proxies/caches?
self.page = None
-
+ self._dicts = None
+
# Fix dircaching problems on Windows 9x
if IsWin9x():
import dircache
@@ -83,15 +102,18 @@ class RequestBase:
# not on external non-Apache servers
self.forbidden = False
if self.request_uri.startswith('http://'):
- self.makeForbidden()
+ self.makeForbidden403()
# Init
else:
self.writestack = []
self.clock = Clock()
# order is important here!
+ self.__dict__.update(properties)
self._load_multi_cfg()
+ self.isSpiderAgent = self.check_spider()
+
# Set decode charsets. Input from the user is always in
# config.charset, which is the page charsets. Except
# path_info, which may use utf-8, and handled by decodePagename.
@@ -112,70 +134,143 @@ class RequestBase:
# # no extra path after script name
# rootname = u""
+ self.args = {}
+ self.form = {}
+
+ # MOVED: this was in run() method, but moved here for auth module being able to use it
+ if not self.query_string.startswith('action=xmlrpc'):
+ self.args = self.form = self.setup_args()
+
rootname = u''
self.rootpage = Page(self, rootname, is_rootpage=1)
- self.user = user.User(self)
- ## self.dicts = self.initdicts()
+ self.user = self.get_user_from_form()
+
+ if not self.query_string.startswith('action=xmlrpc'):
+ if not self.forbidden and self.isForbidden():
+ self.makeForbidden403()
+ if not self.forbidden and self.surge_protect():
+ self.makeUnavailable503()
from MoinMoin import i18n
- # Set theme - forced theme, user theme or wiki default
- if self.cfg.theme_force:
- theme_name = self.cfg.theme_default
- #### DEBUT HACK : Utilisation d'un thème différent pour www.crans.org
- if self.remote_addr in self.cfg.ip_theme.keys():
- theme_name = self.cfg.ip_theme[self.remote_addr]
- #### FIN DU HACK
- else:
- theme_name = self.user.theme_name
- self.loadTheme(theme_name)
-
- self.args = None
- self.form = None
self.logger = None
self.pragma = {}
self.mode_getpagelinks = 0
self.no_closing_html_code = 0
- self.__dict__.update(properties)
-
self.i18n = i18n
self.lang = i18n.requestLanguage(self)
# Language for content. Page content should use the wiki
# default lang, but generated content like search results
# should use the user language.
- self.content_lang = self.cfg.default_lang
+ self.content_lang = self.cfg.language_default
self.getText = lambda text, i18n=self.i18n, request=self, lang=self.lang, **kv: i18n.getText(text, request, lang, kv.get('formatted', True))
self.opened_logs = 0
-
self.reset()
- def __getattr__(self, name):
- "load things on demand"
- if name == "dicts":
- self.dicts = self.initdicts()
- return self.dicts
- raise AttributeError
+ def surge_protect(self):
+ """ check if someone requesting too much from us """
+ validuser = self.user.valid
+ current_id = validuser and self.user.name or self.remote_addr
+ if not validuser and current_id.startswith('127.'): # localnet
+ return False
+ current_action = self.form.get('action', ['show'])[0]
+
+ limits = self.cfg.surge_action_limits
+ default_limit = self.cfg.surge_action_limits.get('default', (30, 60))
+
+ now = int(time.time())
+ surgedict = {}
+ surge_detected = False
+
+ try:
+ cache = caching.CacheEntry(self, 'surgeprotect', 'surge-log')
+ if cache.exists():
+ data = cache.content()
+ data = data.split("\n")
+ for line in data:
+ try:
+ id, t, action, surge_indicator = line.split("\t")
+ t = int(t)
+ maxnum, dt = limits.get(action, default_limit)
+ if t >= now - dt:
+ events = surgedict.setdefault(id, copy.copy({}))
+ timestamps = events.setdefault(action, copy.copy([]))
+ timestamps.append((t, surge_indicator))
+ except StandardError, err:
+ pass
+
+ maxnum, dt = limits.get(current_action, default_limit)
+ events = surgedict.setdefault(current_id, copy.copy({}))
+ timestamps = events.setdefault(current_action, copy.copy([]))
+ surge_detected = len(timestamps) > maxnum
+ surge_indicator = surge_detected and "!" or ""
+ timestamps.append((now, surge_indicator))
+ if surge_detected:
+ if len(timestamps) < maxnum*2:
+ timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
+
+ if current_action != 'AttachFile': # don't add AttachFile accesses to all or picture galleries will trigger SP
+ current_action = 'all' # put a total limit on user's requests
+ maxnum, dt = limits.get(current_action, default_limit)
+ events = surgedict.setdefault(current_id, copy.copy({}))
+ timestamps = events.setdefault(current_action, copy.copy([]))
+ surge_detected = surge_detected or len(timestamps) > maxnum
+
+ surge_indicator = surge_detected and "!" or ""
+ timestamps.append((now, surge_indicator))
+ if surge_detected:
+ if len(timestamps) < maxnum*2:
+ timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
+
+ data = []
+ for id, events in surgedict.items():
+ for action, timestamps in events.items():
+ for t, surge_indicator in timestamps:
+ data.append("%s\t%d\t%s\t%s" % (id, t, action, surge_indicator))
+ data = "\n".join(data)
+ cache.update(data)
+ except StandardError, err:
+ pass
+
+ return surge_detected
+
+ def getDicts(self):
+ """ Lazy initialize the dicts on the first access """
+ if self._dicts is None:
+ from MoinMoin import wikidicts
+ dicts = wikidicts.GroupDict(self)
+ dicts.scandicts()
+ self._dicts = dicts
+ return self._dicts
+
+ def delDicts(self):
+ """ Delete the dicts, used by some tests """
+ del self._dicts
+ self._dicts = None
+
+ dicts = property(getDicts, None, delDicts)
+
def _load_multi_cfg(self):
# protect against calling multiple times
if not hasattr(self, 'cfg'):
from MoinMoin import multiconfig
self.cfg = multiconfig.getConfig(self.url)
- def parse_accept_charset(self, accept_charset):
- """ Parse http accept charset header
+ def setAcceptedCharsets(self, accept_charset):
+ """ Set accepted_charsets by parsing accept-charset header
Set self.accepted_charsets to an ordered list based on
http_accept_charset.
Reference: http://www.w3.org/Protocols/rfc2616/rfc2616.txt
- @param accept_charset: HTTP_ACCEPT_CHARSET string
- @rtype: list of strings
- @return: sorted list of accepted charsets
+ TODO: currently no code use this value.
+
+ @param accept_charset: accept-charset header
"""
charsets = []
if accept_charset:
@@ -198,117 +293,246 @@ class RequestBase:
# Remove *, its not clear what we should do with it later
charsets = [name for qval, name in charsets if name != '*']
- return charsets
-
+ self.accepted_charsets = charsets
+
def _setup_vars_from_std_env(self, env):
- """ Sets the common Request members by parsing a standard
- HTTPD environment (as created as environment by most common
- webservers. To be used by derived classes.
+ """ Set common request variables from CGI environment
+
+ Parse a standard CGI environment as created by common web
+ servers. Reference: http://www.faqs.org/rfcs/rfc3875.html
- @param env: the environment to use
+ @param env: dict like object containing cgi meta variables
"""
- self.http_accept_language = env.get('HTTP_ACCEPT_LANGUAGE', 'en')
- self.server_name = env.get('SERVER_NAME', 'localhost')
- self.server_port = env.get('SERVER_PORT', '80')
- self.http_host = env.get('HTTP_HOST','localhost')
- # Make sure http referer use only ascii (IE again)
- self.http_referer = unicode(env.get('HTTP_REFERER', ''), 'ascii',
- 'replace').encode('ascii', 'replace')
+ # Values we can just copy
+ self.env = env
+ self.http_accept_language = env.get('HTTP_ACCEPT_LANGUAGE',
+ self.http_accept_language)
+ self.server_name = env.get('SERVER_NAME', self.server_name)
+ self.server_port = env.get('SERVER_PORT', self.server_port)
self.saved_cookie = env.get('HTTP_COOKIE', '')
self.script_name = env.get('SCRIPT_NAME', '')
-
- self.request_uri = env.get('REQUEST_URI', '')
- path_info = env.get('PATH_INFO', '')
-
- query_string = env.get('QUERY_STRING', '')
- self.query_string = self.decodePagename(query_string)
- server_software = env.get('SERVER_SOFTWARE', '')
-
- # Handle the strange charset semantics on *Windows*
- # path_info is transformed into the system code page by the webserver
- # Additionally, paths containing dots let most webservers choke.
-
- # Fig. I - Broken environment variables in different environments:
- # path_info script_name
- # Apache1 X X PI does not contain dots
- # Apache2 X X PI is not encoded correctly
- # IIS X X (is fixed somewhere else)
- # Other ? - ? := Possible and even RFC-compatible.
- # - := Hopefully not.
-
- # Fix the script_name if run on Windows/Apache
- if os.name == 'nt' and server_software.find('Apache/') != -1:
- self.script_name = FixScriptName(self.script_name)
-
- # Check if we can use Apache's request_uri variable in order to
- # gather the correct user input
- if (os.name != 'posix' and self.request_uri != ''):
- import urllib
- path_info = urllib.unquote(self.request_uri.replace(
- self.script_name, '', 1).replace('?' + query_string, '', 1))
-
- # Decode according to filesystem semantics if we cannot gather the
- # request_uri
- elif os.name == 'nt':
- path_info = wikiutil.decodeWindowsPath(path_info).encode("utf-8")
-
- self.path_info = self.decodePagename(path_info)
+ self.path_info = env.get('PATH_INFO', '')
+ self.query_string = env.get('QUERY_STRING', '')
self.request_method = env.get('REQUEST_METHOD', None)
-
self.remote_addr = env.get('REMOTE_ADDR', '')
self.http_user_agent = env.get('HTTP_USER_AGENT', '')
- self.is_ssl = env.get('SSL_PROTOCOL', '') != '' \
- or env.get('SSL_PROTOCOL_VERSION', '') != '' \
- or env.get('HTTPS', 'off') == 'on'
- # We cannot rely on request_uri being set. In fact,
- # it is just an addition of Apache to the CGI specs.
- if self.request_uri == '':
- import urllib
- if self.server_port.strip() and self.server_port != '80':
- port = ':' + str(self.server_port)
- else:
- port = ''
- self.url = (self.server_name + port + self.script_name +
- urllib.quote(self.path_info.replace(
- self.script_name, '', 1).encode("utf-8"))
- + '?' + self.query_string)
+ # REQUEST_URI is not part of CGI spec, but an addition of Apache.
+ self.request_uri = env.get('REQUEST_URI', '')
+
+ # Values that need more work
+ self.setHttpReferer(env.get('HTTP_REFERER'))
+ self.setIsSSL(env)
+ self.setHost(env.get('HTTP_HOST'))
+ self.fixURI(env)
+ self.setURL(env)
+
+ ##self.debugEnvironment(env)
+
+ def setHttpReferer(self, referer):
+ """ Set http_referer, making sure its ascii
+
+ IE might send non-ascii value.
+ """
+ value = ''
+ if referer:
+ value = unicode(referer, 'ascii', 'replace')
+ value = value.encode('ascii', 'replace')
+ self.http_referer = value
+
+ def setIsSSL(self, env):
+ """ Set is_ssl
+
+ @param env: dict like object containing cgi meta variables
+ """
+ self.is_ssl = bool(env.get('SSL_PROTOCOL') or
+ env.get('SSL_PROTOCOL_VERSION') or
+ env.get('HTTPS') == 'on')
+
+ def setHost(self, host=None):
+ """ Set http_host
+
+ Create from server name and port if missing. Previous code
+ default to localhost.
+ """
+ if not host:
+ port = ''
+ standardPort = ('80', '443')[self.is_ssl]
+ if self.server_port != standardPort:
+ port = ':' + self.server_port
+ host = self.server_name + port
+ self.http_host = host
+
+ def fixURI(self, env):
+ """ Fix problems with script_name and path_info
+
+ Handle the strange charset semantics on Windows and other non
+ posix systems. path_info is transformed into the system code
+ page by the web server. Additionally, paths containing dots let
+ most webservers choke.
+
+ Broken environment variables in different environments:
+ path_info script_name
+ Apache1 X X PI does not contain dots
+ Apache2 X X PI is not encoded correctly
+ IIS X X path_info include script_name
+ Other ? - ? := Possible and even RFC-compatible.
+ - := Hopefully not.
+
+ @param env: dict like object containing cgi meta variables
+ """
+ # Fix the script_name when using Apache on Windows.
+ server_software = env.get('SERVER_SOFTWARE', '')
+ if os.name == 'nt' and server_software.find('Apache/') != -1:
+ # Removes elements ending in '.' from the path.
+ self.script_name = '/'.join([x for x in self.script_name.split('/')
+ if not x.endswith('.')])
+
+ # Fix path_info
+ if os.name != 'posix' and self.request_uri != '':
+ # Try to recreate path_info from request_uri.
+ import urlparse
+ scriptAndPath = urlparse.urlparse(self.request_uri)[2]
+ path = scriptAndPath.replace(self.script_name, '', 1)
+ self.path_info = wikiutil.url_unquote(path, want_unicode=False)
+ elif os.name == 'nt':
+ # Recode path_info to utf-8
+ path = wikiutil.decodeWindowsPath(self.path_info)
+ self.path_info = path.encode("utf-8")
+
+ # Fix bug in IIS/4.0 when path_info contain script_name
+ if self.path_info.startswith(self.script_name):
+ self.path_info = self.path_info[len(self.script_name):]
+
+ def setURL(self, env):
+ """ Set url, used to locate wiki config
+
+ This is the place to manipulate url parts as needed.
+
+ @param env: dict like object containing cgi meta variables or
+ http headers.
+ """
+ # If we serve on localhost:8000 and use a proxy on
+ # example.com/wiki, our urls will be example.com/wiki/pagename
+ # Same for the wiki config - they must use the proxy url.
+ self.rewriteHost(env)
+ self.rewriteURI(env)
+
+ if not self.request_uri:
+ self.request_uri = self.makeURI()
+ self.url = self.http_host + self.request_uri
+
+ def rewriteHost(self, env):
+ """ Rewrite http_host transparently
+
+ Get the proxy host using 'X-Forwarded-Host' header, added by
+ Apache 2 and other proxy software.
+
+ TODO: Will not work for Apache 1 or others that don't add this
+ header.
+
+ TODO: If we want to add an option to disable this feature it
+ should be in the server script, because the config is not
+ loaded at this point, and must be loaded after url is set.
+
+ @param env: dict like object containing cgi meta variables or
+ http headers.
+ """
+ proxy_host = (env.get(self.proxy_host) or
+ env.get(cgiMetaVariable(self.proxy_host)))
+ if proxy_host:
+ self.http_host = proxy_host
+
+ def rewriteURI(self, env):
+ """ Rewrite request_uri, script_name and path_info transparently
+
+ Useful when running mod python or when running behind a proxy,
+ e.g run on localhost:8000/ and serve as example.com/wiki/.
+
+ Uses private 'X-Moin-Location' header to set the script name.
+ This allow setting the script name when using Apache 2
+ directive::
+
+
+ RequestHeader set X-Moin-Location /my/wiki/
+
+
+ TODO: does not work for Apache 1 and others that do not allow
+ setting custom headers per request.
+
+ @param env: dict like object containing cgi meta variables or
+ http headers.
+ """
+ location = (env.get(self.moin_location) or
+ env.get(cgiMetaVariable(self.moin_location)))
+ if location is None:
+ return
+
+ scriptAndPath = self.script_name + self.path_info
+ location = location.rstrip('/')
+ self.script_name = location
+
+ # This may happen when using mod_python
+ if scriptAndPath.startswith(location):
+ self.path_info = scriptAndPath[len(location):]
+
+ # Recreate the URI from the modified parts
+ if self.request_uri:
+ self.request_uri = self.makeURI()
+
+ def makeURI(self):
+ """ Return uri created from uri parts """
+ uri = self.script_name + wikiutil.url_quote(self.path_info)
+ if self.query_string:
+ uri += '?' + self.query_string
+ return uri
+
+ def splitURI(self, uri):
+ """ Return path and query splited from uri
+
+ Just like CGI environment, the path is unquoted, the query is
+ not.
+ """
+ if '?' in uri:
+ path, query = uri.split('?', 1)
else:
- self.url = self.server_name + self.request_uri
-
- ac = env.get('HTTP_ACCEPT_CHARSET', '')
- self.accepted_charsets = self.parse_accept_charset(ac)
-
- self.auth_username = None
+ path, query = uri, ''
+ return wikiutil.url_unquote(path, want_unicode=False), query
- # need config here, so check:
- self._load_multi_cfg()
+ def get_user_from_form(self):
+ """ read the maybe present UserPreferences form and call get_user with the values """
+ name = self.form.get('name', [None])[0]
+ password = self.form.get('password', [None])[0]
+ login = self.form.has_key('login')
+ logout = self.form.has_key('logout')
+ u = self.get_user_default_unknown(name=name, password=password,
+ login=login, logout=logout,
+ user_obj=None)
+ return u
+
+ def get_user_default_unknown(self, **kw):
+ """ call do_auth and if it doesnt return a user object, make some "Unknown User" """
+ user_obj = self.get_user_default_None(**kw)
+ if user_obj is None:
+ user_obj = user.User(self, auth_method="request:427")
+ return user_obj
- if self.cfg.auth_http_enabled:
- auth_type = env.get('AUTH_TYPE','')
- if auth_type in ['Basic', 'Digest', 'NTLM', ]:
- username = env.get('REMOTE_USER','')
- if auth_type == 'NTLM':
- # converting to standard case so that the user can even enter wrong case
- # (added since windows does not distinguish between e.g. "Mike" and "mike")
- username = username.split('\\')[-1] # split off domain e.g. from DOMAIN\user
- # this "normalizes" the login name from {meier, Meier, MEIER} to Meier
- # put a comment sign in front of next line if you don't want that:
- username = username.title()
- self.auth_username = username
-
-## f=open('/tmp/env.log','a')
-## f.write('---ENV\n')
-## f.write('script_name = %s\n'%(self.script_name))
-## f.write('path_info = %s\n'%(self.path_info))
-## f.write('server_name = %s\n'%(self.server_name))
-## f.write('server_port = %s\n'%(self.server_port))
-## f.write('http_host = %s\n'%(self.http_host))
-## f.write('------\n')
-## f.write('%s\n'%(repr(env)))
-## f.write('------\n')
-## f.close()
-
+ def get_user_default_None(self, **kw):
+ """ loop over auth handlers, return a user obj or None """
+ name = kw.get('name')
+ password = kw.get('password')
+ login = kw.get('login')
+ logout = kw.get('logout')
+ user_obj = kw.get('user_obj')
+ for auth in self.cfg.auth:
+ user_obj, continue_flag = auth(self,
+ name=name, password=password,
+ login=login, logout=logout,
+ user_obj=user_obj)
+ if not continue_flag:
+ break
+ return user_obj
+
def reset(self):
""" Reset request state.
@@ -320,9 +544,8 @@ class RequestBase:
# This is the content language and has nothing to do with
# The user interface language. The content language can change
# during the rendering of a page by lang macros
- self.current_lang = self.cfg.default_lang
+ self.current_lang = self.cfg.language_default
- self._footer_fragments = {}
self._all_pages = None
# caches unique ids
self._page_ids = {}
@@ -340,22 +563,29 @@ class RequestBase:
@param theme_name: the name of the theme
@type theme_name: str
- @returns: 0 on success, 1 if user theme could not be loaded,
- 2 if a hard fallback to modern theme was required.
@rtype: int
@return: success code
+ 0 on success
+ 1 if user theme could not be loaded,
+ 2 if a hard fallback to modern theme was required.
"""
fallback = 0
- Theme = wikiutil.importPlugin(self.cfg, 'theme', theme_name, 'Theme')
- if Theme is None:
+ if theme_name == "":
+ theme_name = self.cfg.theme_default
+
+ try:
+ Theme = wikiutil.importPlugin(self.cfg, 'theme', theme_name,
+ 'Theme')
+ except wikiutil.PluginMissingError:
fallback = 1
- Theme = wikiutil.importPlugin(self.cfg, 'theme',
- self.cfg.theme_default, 'Theme')
- if Theme is None:
+ try:
+ Theme = wikiutil.importPlugin(self.cfg, 'theme',
+ self.cfg.theme_default, 'Theme')
+ except wikiutil.PluginMissingError:
fallback = 2
from MoinMoin.theme.modern import Theme
+
self.theme = Theme(self)
-
return fallback
def setContentLanguage(self, lang):
@@ -368,12 +598,6 @@ class RequestBase:
self.content_lang = lang
self.current_lang = lang
- def add2footer(self, key, htmlcode):
- """ Add a named HTML fragment to the footer, after the default links
- """
- self._footer_fragments[key] = htmlcode
-
-
def getPragma(self, key, defval=None):
""" Query a pragma value (#pragma processing instruction)
@@ -381,7 +605,6 @@ class RequestBase:
"""
return self.pragma.get(key.lower(), defval)
-
def setPragma(self, key, value):
""" Set a pragma value (#pragma processing instruction)
@@ -399,16 +622,29 @@ class RequestBase:
return ''
return self.script_name
+ def getPageNameFromQueryString(self):
+ """ Try to get pagename from the query string
+
+ Support urls like http://netloc/script/?page_name. Allow
+ solving path_info encoding problems by calling with the page
+ name as a query.
+ """
+ pagename = wikiutil.url_unquote(self.query_string, want_unicode=False)
+ pagename = self.decodePagename(pagename)
+ pagename = self.normalizePagename(pagename)
+ return pagename
+
def getKnownActions(self):
""" Create a dict of avaiable actions
- Return cached version if avaiable. TODO: when we have a wiki
- object in long running process, we should get it from it.
+ Return cached version if avaiable.
@rtype: dict
@return: dict of all known actions
"""
- if self._known_actions is None:
+ try:
+ self.cfg._known_actions # check
+ except AttributeError:
from MoinMoin import wikiaction
# Add built in actions from wikiaction
actions = [name[3:] for name in wikiaction.__dict__
@@ -424,10 +660,10 @@ class RequestBase:
# TODO: Use set when we require Python 2.3
actions = dict(zip(actions, [''] * len(actions)))
- self._known_actions = actions
+ self.cfg._known_actions = actions
# Return a copy, so clients will not change the dict.
- return self._known_actions.copy()
+ return self.cfg._known_actions.copy()
def getAvailableActions(self, page):
""" Get list of avaiable actions for this request
@@ -440,7 +676,7 @@ class RequestBase:
@return: dict of avaiable actions
"""
if self._available_actions is None:
- # Add actions for existing pages only!, incliding deleted pages.
+ # Add actions for existing pages only, including deleted pages.
# Fix *OnNonExistingPage bugs.
if not (page.exists(includeDeleted=1) and
self.user.may.read(page.page_name)):
@@ -453,20 +689,18 @@ class RequestBase:
del actions[key]
# Filter wiki excluded actions
- for key in self.cfg.excluded_actions:
+ for key in self.cfg.actions_excluded:
if key in actions:
del actions[key]
# Filter actions by page type, acl and user state
excluded = []
if ((page.isUnderlayPage() and not page.isStandardPage()) or
- not self.user.may.write(page.page_name)):
+ not self.user.may.write(page.page_name) or
+ not self.user.may.delete(page.page_name)):
# Prevent modification of underlay only pages, or pages
- # the user can't write to
+ # the user can't write and can't delete
excluded = [u'RenamePage', u'DeletePage',] # AttachFile must NOT be here!
- elif not self.user.valid:
- # Prevent rename and delete for non registered users
- excluded = [u'RenamePage', u'DeletePage']
for key in excluded:
if key in actions:
del actions[key]
@@ -475,12 +709,25 @@ class RequestBase:
# Return a copy, so clients will not change the dict.
return self._available_actions.copy()
-
+
+ def redirectedOutput(self, function, *args, **kw):
+ """ Redirect output during function, return redirected output """
+ buffer = StringIO.StringIO()
+ self.redirect(buffer)
+ try:
+ function(*args, **kw)
+ finally:
+ self.redirect()
+ text = buffer.getvalue()
+ buffer.close()
+ return text
+
def redirect(self, file=None):
- if file: # redirect output to "file"
+ """ Redirect output to file, or restore saved output """
+ if file:
self.writestack.append(self.write)
self.write = file.write
- else: # restore saved output file
+ else:
self.write = self.writestack.pop()
def reset_output(self):
@@ -514,7 +761,7 @@ class RequestBase:
wd = []
for d in data:
try:
- if isinstance(d, type(u'')):
+ if isinstance(d, unicode):
# if we are REALLY sure, we can use "strict"
d = d.encode(config.charset, 'replace')
wd.append(d)
@@ -548,7 +795,7 @@ class RequestBase:
try:
page = unicode(page, 'utf-8', 'strict')
# Fit data into config.charset, replacing what won't
- # fit. Better have few "?" in the name then crash.
+ # fit. Better have few "?" in the name than crash.
page = page.encode(config.charset, 'replace')
except UnicodeError:
pass
@@ -619,133 +866,174 @@ class RequestBase:
"""
raise NotImplementedError
- def initdicts(self):
- from MoinMoin import wikidicts
- dicts = wikidicts.GroupDict(self)
- dicts.scandicts()
- return dicts
-
+ def check_spider(self):
+ """ check if the user agent for current request is a spider/bot """
+ isSpider = False
+ spiders = self.cfg.ua_spiders
+ if spiders:
+ ua = self.getUserAgent()
+ if ua:
+ isSpider = re.search(spiders, ua, re.I) is not None
+ return isSpider
+
def isForbidden(self):
""" check for web spiders and refuse anything except viewing """
forbidden = 0
- # we do not have a parsed query string here
- # so we can just do simple matching
- if ((self.query_string != '' or self.request_method != 'GET') and
- self.query_string != 'action=rss_rc' and not
+ # we do not have a parsed query string here, so we can just do simple matching
+ qs = self.query_string
+ if ((qs != '' or self.request_method != 'GET') and
+ not 'action=rss_rc' in qs and
# allow spiders to get attachments and do 'show'
- (self.query_string.find('action=AttachFile') >= 0 and self.query_string.find('do=get') >= 0) and not
- (self.query_string.find('action=show') >= 0)
+ not ('action=AttachFile' in qs and 'do=get' in qs) and
+ not 'action=show' in qs
):
- from MoinMoin.util import web
- forbidden = web.isSpiderAgent(self)
+ forbidden = self.isSpiderAgent
if not forbidden and self.cfg.hosts_deny:
ip = self.remote_addr
for host in self.cfg.hosts_deny:
- if ip == host or host[-1] == '.' and ip.startswith(host):
+ if host[-1] == '.' and ip.startswith(host):
forbidden = 1
+ #self.log("hosts_deny (net): %s" % str(forbidden))
+ break
+ if ip == host:
+ forbidden = 1
+ #self.log("hosts_deny (ip): %s" % str(forbidden))
break
return forbidden
-
def setup_args(self, form=None):
- return {}
+ """ Return args dict
+
+ In POST request, invoke _setup_args_from_cgi_form to handle
+ possible file uploads. For other request simply parse the query
+ string.
+
+ Warning: calling with a form might fail, depending on the type
+ of the request! Only the request know which kind of form it can
+ handle.
+
+ TODO: The form argument should be removed in 1.5.
+ """
+ if form is not None or self.request_method == 'POST':
+ return self._setup_args_from_cgi_form(form)
+ args = cgi.parse_qs(self.query_string, keep_blank_values=1)
+ return self.decodeArgs(args)
def _setup_args_from_cgi_form(self, form=None):
- """ Setup args from a FieldStorage form
+ """ Return args dict from a FieldStorage
- Create the args from a standard cgi.FieldStorage to be used by
- derived classes, or from given form.
+ Create the args from a standard cgi.FieldStorage or from given
+ form. Each key contain a list of values.
- All values are decoded using config.charset.
-
- @keyword form: a cgi.FieldStorage
+ @param form: a cgi.FieldStorage
@rtype: dict
- @return dict with form keys, each contains a list of values
+ @return: dict with form keys, each contains a list of values
"""
- decode = wikiutil.decodeUserInput
-
- # Use cgi.FieldStorage by default
if form is None:
form = cgi.FieldStorage()
args = {}
- # Convert form keys to dict keys, each key contains a list of
- # values.
- for key in form.keys():
+ for key in form:
values = form[key]
- if not isinstance(values, types.ListType):
+ if not isinstance(values, list):
values = [values]
fixedResult = []
- for i in values:
- if isinstance(i, cgi.MiniFieldStorage):
- data = decode(i.value, self.decode_charsets)
- elif isinstance(i, cgi.FieldStorage):
- data = i.value
- if i.filename:
- # multiple uploads to same form field are stupid!
- args[key+'__filename__'] = decode(i.filename, self.decode_charsets)
- else:
- # we do not have a file upload, so we decode:
- data = decode(data, self.decode_charsets)
- # Append decoded value
- fixedResult.append(data)
-
+ for item in values:
+ fixedResult.append(item.value)
+ if isinstance(item, cgi.FieldStorage) and item.filename:
+ # Save upload file name in a separate key
+ args[key + '__filename__'] = item.filename
args[key] = fixedResult
- return args
+ return self.decodeArgs(args)
+
+ def decodeArgs(self, args):
+ """ Decode args dict
+
+ Decoding is done in a separate path because it is reused by
+ other methods and sub classes.
+ """
+ decode = wikiutil.decodeUserInput
+ result = {}
+ for key in args:
+ if key + '__filename__' in args:
+ # Copy file data as is
+ result[key] = args[key]
+ elif key.endswith('__filename__'):
+ result[key] = decode(args[key], self.decode_charsets)
+ else:
+ result[key] = [decode(value, self.decode_charsets)
+ for value in args[key]]
+ return result
def getBaseURL(self):
""" Return a fully qualified URL to this script. """
return self.getQualifiedURL(self.getScriptname())
+ def getQualifiedURL(self, uri=''):
+ """ Return an absolute URL starting with schema and host.
- def getQualifiedURL(self, uri=None):
- """ Return a full URL starting with schema, server name and port.
+ Already qualified urls are returned unchanged.
- *uri* -- append this server-rooted uri (must start with a slash)
+ @param uri: server rooted uri e.g /scriptname/pagename. It
+ must start with a slash. Must be ascii and url encoded.
"""
- if uri and uri[:4] == "http":
+ import urlparse
+ scheme = urlparse.urlparse(uri)[0]
+ if scheme:
return uri
- schema, stdport = (('http', '80'), ('https', '443'))[self.is_ssl]
- host = self.http_host
- if not host:
- host = self.server_name
- port = self.server_port
- if port != stdport:
- host = "%s:%s" % (host, port)
-
- result = "%s://%s" % (schema, host)
- if uri:
- result = result + uri
-
- return result
+ scheme = ('http', 'https')[self.is_ssl]
+ result = "%s://%s%s" % (scheme, self.http_host, uri)
+ # This might break qualified urls in redirects!
+ # e.g. mapping 'http://netloc' -> '/'
+ return wikiutil.mapURL(self, result)
def getUserAgent(self):
""" Get the user agent. """
return self.http_user_agent
- def makeForbidden(self):
- self.forbidden = True
+ def makeForbidden(self, resultcode, msg):
+ statusmsg = {
+ 403: 'FORBIDDEN',
+ 503: 'Service unavailable',
+ }
self.http_headers([
- 'Status: 403 FORBIDDEN',
+ 'Status: %d %s' % (resultcode, statusmsg[resultcode]),
'Content-Type: text/plain'
])
- self.write('You are not allowed to access this!\r\n')
- self.setResponseCode(403)
- return self.finish()
+ self.write(msg)
+ self.setResponseCode(resultcode)
+ self.forbidden = True
+
+ def makeForbidden403(self):
+ self.makeForbidden(403, 'You are not allowed to access this!\r\n')
+
+ def makeUnavailable503(self):
+ self.makeForbidden(503, "Warning:\r\n"
+ "You triggered the wiki's surge protection by doing too many requests in a short time.\r\n"
+ "Please make a short break reading the stuff you already got.\r\n"
+ "When you restart doing requests AFTER that, slow down or you might get locked out for a longer time!\r\n")
+
+ def initTheme(self):
+ """ Set theme - forced theme, user theme or wiki default """
+ if self.cfg.theme_force:
+ theme_name = self.cfg.theme_default
+ else:
+ theme_name = self.user.theme_name
+ self.loadTheme(theme_name)
def run(self):
- # __init__ may have failed
+ # Exit now if __init__ failed or request is forbidden
if self.failed or self.forbidden:
- return
-
- if self.isForbidden():
- self.makeForbidden()
- if self.forbidden:
- return
+ #Don't sleep()! Seems to bind too much resources, so twisted will
+ #run out of threads, files, whatever (with low CPU load) and stop
+ #serving requests.
+ #if self.forbidden:
+ # time.sleep(10) # let the sucker wait!
+ return self.finish()
self.open_logs()
_ = self.getText
@@ -766,8 +1054,12 @@ class RequestBase:
# parse request data
try:
- self.args = self.setup_args()
- self.form = self.args
+ self.initTheme()
+
+ # MOVED: moved to __init__() for auth module being able to use it
+ #self.args = self.setup_args()
+ #self.form = self.args
+
action = self.form.get('action',[None])[0]
# Get pagename
@@ -777,13 +1069,7 @@ class RequestBase:
pagename = self.normalizePagename(path)
else:
pagename = None
- except: # catch and print any exception
- self.reset_output()
- self.http_headers()
- self.print_exception()
- return self.finish()
- try:
# Handle request. We have these options:
# 1. If user has a bad user name, delete its bad cookie and
@@ -792,7 +1078,7 @@ class RequestBase:
msg = _("""Invalid user name {{{'%s'}}}.
Name may contain any Unicode alpha numeric character, with optional one
space between words. Group page name is not allowed.""") % self.user.name
- self.deleteCookie()
+ self.user = self.get_user_default_unknown(name=self.user.name, logout=True)
page = wikiutil.getSysPage(self, 'UserPreferences')
page.send_page(self, msg=msg)
@@ -801,7 +1087,11 @@ space between words. Group page name is not allowed.""") % self.user.name
pagetrail = self.user.getTrail()
if pagetrail:
# Redirect to last page visited
- url = Page(self, pagetrail[-1]).url(self)
+ if ":" in pagetrail[-1]:
+ wikitag, wikiurl, wikitail, error = wikiutil.resolve_wiki(self, pagetrail[-1])
+ url = wikiurl + wikiutil.quoteWikinameURL(wikitail)
+ else:
+ url = Page(self, pagetrail[-1]).url(self)
else:
# Or to localized FrontPage
url = wikiutil.getFrontPage(self).url(self)
@@ -837,9 +1127,7 @@ space between words. Group page name is not allowed.""") % self.user.name
msg = _("You are not allowed to do %s on this page.") % wikiutil.escape(action)
if not self.user.valid:
# Suggest non valid user to login
- login = wikiutil.getSysPage(self, 'UserPreferences')
- login = login.link_to(self, _('Login'))
- msg += _(" %s and try again.", formatted=0) % login
+ msg += _(" %s and try again.", formatted=0) % _('Login') # XXX merge into 1 string after 1.5.3 release
self.page.send_page(self, msg=msg)
# Try action
@@ -855,32 +1143,15 @@ space between words. Group page name is not allowed.""") % self.user.name
# 6. Or (at last) visit pagename
else:
- if not pagename:
- # Get pagename from the query string
- pagename = self.normalizePagename(self.query_string)
-
+ if not pagename and self.query_string:
+ pagename = self.getPageNameFromQueryString()
# pagename could be empty after normalization e.g. '///' -> ''
if not pagename:
pagename = wikiutil.getFrontPage(self).page_name
# Visit pagename
- if self.cfg.allow_extended_names:
- self.page = Page(self, pagename)
- self.page.send_page(self, count_hit=1)
- else:
- # TODO: kill this. Why disable allow extended names?
- from MoinMoin.parser.wiki import Parser
- import re
- word_match = re.match(Parser.word_rule, pagename)
- if word_match:
- word = word_match.group(0)
- self.page = Page(self, word)
- self.page.send_page(self, count_hit=1)
- else:
- self.http_headers()
- err = u'%s "
%s
"' % (
- _("Can't work out query"), pagename)
- self.write(err)
+ self.page = Page(self, pagename)
+ self.page.send_page(self, count_hit=1)
# generate page footer (actions that do not want this footer
# use raise util.MoinMoinNoFooter to break out of the
@@ -898,78 +1169,47 @@ space between words. Group page name is not allowed.""") % self.user.name
for t in self.clock.dump():
self.write('%s\n' % t)
self.write('\n')
-
+ #self.write('' % repr(self.user.auth_method))
self.write('