
Pour lancer le thème crans-www sur le www Ah, et j'ai aussi modifié le logo darcs-hash:20081209174926-bd074-85b41aca2d387200ab6d2cf77540fc9d4de85af2.gz
1684 lines
64 KiB
Python
1684 lines
64 KiB
Python
# -*- coding: iso-8859-1 -*-
|
|
"""
|
|
MoinMoin - RequestBase Implementation
|
|
|
|
@copyright: 2001-2003 Juergen Hermann <jh@web.de>,
|
|
2003-2008 MoinMoin:ThomasWaldmann
|
|
@license: GNU GPL, see COPYING for details.
|
|
"""
|
|
|
|
# Support for remote IP address detection when using (reverse) proxy (or even proxies).
|
|
# If you exactly KNOW which (reverse) proxies you can trust, put them into the list
|
|
# below, so we can determine the "outside" IP as your trusted proxies see it.
|
|
|
|
proxies_trusted = [] # trust noone!
|
|
#proxies_trusted = ['127.0.0.1', ] # can be a list of multiple IPs
|
|
|
|
from MoinMoin import log
|
|
logging = log.getLogger(__name__)
|
|
|
|
def find_remote_addr(addrs):
|
|
""" Find the last remote IP address before it hits our reverse proxies.
|
|
The LAST address in the <addrs> list is the remote IP as detected by the server
|
|
(not taken from some x-forwarded-for header).
|
|
The FIRST address in the <addrs> list might be the client's IP - if noone cheats
|
|
and everyone supports x-f-f header.
|
|
|
|
See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/
|
|
|
|
For debug loglevel, we log all <addrs>.
|
|
|
|
TODO: refactor request code to first do some basic IP init, then load configuration,
|
|
TODO: then do proxy processing.
|
|
TODO: add wikiconfig configurability for proxies_trusted
|
|
TODO: later, make it possible to put multipe remote IP addrs into edit-log
|
|
"""
|
|
logging.debug("request.find_remote_addr: addrs == %r" % addrs)
|
|
if proxies_trusted:
|
|
result = [addr for addr in addrs if addr not in proxies_trusted]
|
|
if result:
|
|
return result[-1] # last IP before it hit our trusted (reverse) proxies
|
|
return addrs[-1] # this is a safe remote_addr, not taken from x-f-f header
|
|
|
|
|
|
import os, re, time, sys, cgi, StringIO
|
|
import Cookie
|
|
import traceback
|
|
|
|
from MoinMoin.Page import Page
|
|
from MoinMoin import config, wikiutil, user, caching, error
|
|
from MoinMoin.config import multiconfig
|
|
from MoinMoin.support.python_compatibility import set
|
|
from MoinMoin.util import IsWin9x
|
|
from MoinMoin.util.clock import Clock
|
|
from MoinMoin import auth
|
|
from urllib import quote, quote_plus
|
|
|
|
# umask setting --------------------------------------------------------
|
|
def set_umask(new_mask=0777^config.umask):
|
|
""" Set the OS umask value (and ignore potential failures on OSes where
|
|
this is not supported).
|
|
Default: the bitwise inverted value of config.umask
|
|
"""
|
|
try:
|
|
old_mask = os.umask(new_mask)
|
|
except:
|
|
# maybe we are on win32?
|
|
pass
|
|
|
|
# We do this at least once per Python process, when request is imported.
|
|
# If other software parts (like twistd's daemonize() function) set an
|
|
# unwanted umask, we have to call this again to set the correct one:
|
|
set_umask()
|
|
|
|
# Exceptions -----------------------------------------------------------
|
|
|
|
class MoinMoinFinish(Exception):
|
|
""" Raised to jump directly to end of run() function, where finish is called """
|
|
|
|
|
|
class HeadersAlreadySentException(Exception):
|
|
""" Is raised if the headers were already sent when emit_http_headers is called."""
|
|
|
|
|
|
class RemoteClosedConnection(Exception):
|
|
""" Remote end closed connection during request """
|
|
|
|
# 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(object):
|
|
""" A collection for all data associated with ONE request. """
|
|
|
|
# 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' # original host: header as seen by the proxy (e.g. wiki.example.org)
|
|
proxy_xff = 'x-forwarded-for' # list of original remote_addrs as seen by the proxies (e.g. <clientip>,<proxy1>,<proxy2>,...)
|
|
|
|
def __init__(self, properties={}):
|
|
|
|
# twistd's daemonize() overrides our umask, so we reset it here every
|
|
# request. we do it for all request types to avoid similar problems.
|
|
set_umask()
|
|
|
|
self._finishers = []
|
|
|
|
self._auth_redirected = False
|
|
|
|
# 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 = None
|
|
self.user_headers = []
|
|
self.cacheable = 0 # may this output get cached by http proxies/caches?
|
|
self.http_caching_disabled = 0 # see disableHttpCaching()
|
|
self.page = None
|
|
self._dicts = None
|
|
|
|
# session handling. users cannot rely on a session being
|
|
# created, but we should always set request.session
|
|
self.session = {}
|
|
|
|
# setuid handling requires an attribute in the request
|
|
# that stores the real user
|
|
self._setuid_real_user = None
|
|
|
|
# Check for dumb proxy requests
|
|
# TODO relying on request_uri will not work on all servers, especially
|
|
# not on external non-Apache servers
|
|
self.forbidden = False
|
|
if self.request_uri.startswith('http://'):
|
|
self.makeForbidden403()
|
|
|
|
# Init
|
|
else:
|
|
self.writestack = []
|
|
self.clock = Clock()
|
|
self.clock.start('total')
|
|
self.clock.start('base__init__')
|
|
# order is important here!
|
|
self.__dict__.update(properties)
|
|
try:
|
|
self._load_multi_cfg()
|
|
except error.NoConfigMatchedError:
|
|
self.makeForbidden(404, 'No wiki configuration matching the URL found!\r\n')
|
|
return
|
|
|
|
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.
|
|
self.decode_charsets = [config.charset]
|
|
|
|
if self.query_string.startswith('action=xmlrpc'):
|
|
self.args = {}
|
|
self.form = {}
|
|
self.action = 'xmlrpc'
|
|
self.rev = None
|
|
else:
|
|
try:
|
|
self.args = self.form = self.setup_args()
|
|
except UnicodeError:
|
|
self.makeForbidden(403, "The input you sent could not be understood.")
|
|
return
|
|
self.action = self.form.get('action', ['show'])[0]
|
|
try:
|
|
self.rev = int(self.form['rev'][0])
|
|
except:
|
|
self.rev = None
|
|
|
|
from MoinMoin.Page import RootPage
|
|
self.rootpage = RootPage(self)
|
|
|
|
from MoinMoin.logfile import editlog
|
|
self.editlog = editlog.EditLog(self)
|
|
|
|
from MoinMoin import i18n
|
|
self.i18n = i18n
|
|
i18n.i18n_init(self)
|
|
|
|
# authentication might require translated forms, so
|
|
# have a try at guessing the language from the browser
|
|
lang = i18n.requestLanguage(self, try_user=False)
|
|
self.getText = lambda text, i18n=self.i18n, request=self, lang=lang, **kw: i18n.getText(text, request, lang, **kw)
|
|
|
|
# session handler start, auth
|
|
self.parse_cookie()
|
|
user_obj = self.cfg.session_handler.start(self, self.cfg.session_id_handler)
|
|
shfinisher = lambda request: self.cfg.session_handler.finish(request, request.user,
|
|
self.cfg.session_id_handler)
|
|
self.add_finisher(shfinisher)
|
|
# set self.user even if _handle_auth_form raises an Exception
|
|
self.user = None
|
|
self.user = self._handle_auth_form(user_obj)
|
|
del user_obj
|
|
self.cfg.session_handler.after_auth(self, self.cfg.session_id_handler, self.user)
|
|
if not self.user:
|
|
self.user = user.User(self, auth_method='request:invalid')
|
|
|
|
# setuid handling, check isSuperUser() because the user
|
|
# might have lost the permission between requests
|
|
if 'setuid' in self.session and self.user.isSuperUser():
|
|
self._setuid_real_user = self.user
|
|
uid = self.session['setuid']
|
|
self.user = user.User(self, uid, auth_method='setuid')
|
|
# set valid to True so superusers can even switch
|
|
# to disable accounts
|
|
self.user.valid = True
|
|
|
|
if self.action != 'xmlrpc':
|
|
if not self.forbidden and self.isForbidden():
|
|
self.makeForbidden403()
|
|
if not self.forbidden and self.surge_protect():
|
|
self.makeUnavailable503()
|
|
|
|
self.pragma = {}
|
|
self.mode_getpagelinks = 0 # is > 0 as long as we are in a getPageLinks call
|
|
self.parsePageLinks_running = {} # avoid infinite recursion by remembering what we are already running
|
|
|
|
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.language_default
|
|
self.getText = lambda text, i18n=self.i18n, request=self, lang=self.lang, **kv: i18n.getText(text, request, lang, **kv)
|
|
|
|
self.reset()
|
|
|
|
from MoinMoin.formatter.text_html import Formatter
|
|
self.html_formatter = Formatter(self)
|
|
self.formatter = self.html_formatter
|
|
|
|
self.clock.stop('base__init__')
|
|
|
|
def surge_protect(self, kick_him=False):
|
|
""" check if someone requesting too much from us,
|
|
if kick_him is True, we unconditionally blacklist the current user/ip
|
|
"""
|
|
limits = self.cfg.surge_action_limits
|
|
if not limits:
|
|
return False
|
|
|
|
if self.remote_addr.startswith('127.'): # localnet
|
|
return False
|
|
|
|
validuser = self.user.valid
|
|
current_id = validuser and self.user.name or self.remote_addr
|
|
current_action = self.action
|
|
|
|
default_limit = limits.get('default', (30, 60))
|
|
|
|
now = int(time.time())
|
|
surgedict = {}
|
|
surge_detected = False
|
|
|
|
try:
|
|
# if we have common farm users, we could also use scope='farm':
|
|
cache = caching.CacheEntry(self, 'surgeprotect', 'surge-log', scope='wiki', use_encode=True)
|
|
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, {})
|
|
timestamps = events.setdefault(action, [])
|
|
timestamps.append((t, surge_indicator))
|
|
except StandardError:
|
|
pass
|
|
|
|
maxnum, dt = limits.get(current_action, default_limit)
|
|
events = surgedict.setdefault(current_id, {})
|
|
timestamps = events.setdefault(current_action, [])
|
|
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 not in ('cache', 'AttachFile', ): # don't add cache/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, {})
|
|
timestamps = events.setdefault(current_action, [])
|
|
|
|
if kick_him: # ban this guy, NOW
|
|
timestamps.extend([(now + self.cfg.surge_lockout_time, "!")] * (2 * maxnum))
|
|
|
|
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:
|
|
pass
|
|
|
|
if surge_detected and validuser and self.user.auth_method in self.cfg.auth_methods_trusted:
|
|
logging.info("Trusted user %s would have triggered surge protection if not trusted." % self.user.name)
|
|
return False # do not subject trusted users to surge protection
|
|
|
|
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.load_dicts()
|
|
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'):
|
|
self.clock.start('load_multi_cfg')
|
|
self.cfg = multiconfig.getConfig(self.url)
|
|
self.clock.stop('load_multi_cfg')
|
|
|
|
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
|
|
|
|
TODO: currently no code use this value.
|
|
|
|
@param accept_charset: accept-charset header
|
|
"""
|
|
charsets = []
|
|
if accept_charset:
|
|
accept_charset = accept_charset.lower()
|
|
# Add iso-8859-1 if needed
|
|
if (not '*' in accept_charset and
|
|
'iso-8859-1' not in accept_charset):
|
|
accept_charset += ',iso-8859-1'
|
|
|
|
# Make a list, sorted by quality value, using Schwartzian Transform
|
|
# Create list of tuples (value, name) , sort, extract names
|
|
for item in accept_charset.split(','):
|
|
if ';' in item:
|
|
name, qval = item.split(';')
|
|
qval = 1.0 - float(qval.split('=')[1])
|
|
else:
|
|
name, qval = item, 0
|
|
charsets.append((qval, name))
|
|
charsets.sort()
|
|
# Remove *, its not clear what we should do with it later
|
|
charsets = [name for qval, name in charsets if name != '*']
|
|
|
|
self.accepted_charsets = charsets
|
|
|
|
def _setup_vars_from_std_env(self, env):
|
|
""" 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: dict like object containing cgi meta variables
|
|
"""
|
|
# 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.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', '')
|
|
try:
|
|
self.content_length = int(env.get('CONTENT_LENGTH'))
|
|
except (TypeError, ValueError):
|
|
self.content_length = None
|
|
self.if_modified_since = env.get('If-modified-since') or env.get(cgiMetaVariable('If-modified-since'))
|
|
self.if_none_match = env.get('If-none-match') or env.get(cgiMetaVariable('If-none-match'))
|
|
|
|
# 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 'Apache/' in server_software:
|
|
# 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.
|
|
"""
|
|
# proxy support
|
|
self.rewriteRemoteAddr(env)
|
|
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 rewriteRemoteAddr(self, env):
|
|
""" Rewrite remote_addr transparently
|
|
|
|
Get the proxy remote addr using 'X-Forwarded-For' 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.
|
|
"""
|
|
xff = (env.get(self.proxy_xff) or
|
|
env.get(cgiMetaVariable(self.proxy_xff)))
|
|
if xff:
|
|
xff = [addr.strip() for addr in xff.split(',')]
|
|
xff.append(self.remote_addr)
|
|
self.remote_addr = find_remote_addr(xff)
|
|
|
|
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
|
|
<location> directive::
|
|
|
|
<Location /my/wiki/>
|
|
RequestHeader set X-Moin-Location /my/wiki/
|
|
</location>
|
|
|
|
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:
|
|
path, query = uri, ''
|
|
return wikiutil.url_unquote(path, want_unicode=False), query
|
|
|
|
def _handle_auth_form(self, user_obj):
|
|
username = self.form.get('name', [None])[0]
|
|
password = self.form.get('password', [None])[0]
|
|
oid = self.form.get('openid_identifier', [None])[0]
|
|
login = 'login' in self.form
|
|
logout = 'logout' in self.form
|
|
stage = self.form.get('stage', [None])[0]
|
|
return self.handle_auth(user_obj, attended=True, username=username,
|
|
password=password, login=login, logout=logout,
|
|
stage=stage, openid_identifier=oid)
|
|
|
|
def handle_auth(self, user_obj, attended=False, **kw):
|
|
username = kw.get('username')
|
|
password = kw.get('password')
|
|
oid = kw.get('openid_identifier')
|
|
login = kw.get('login')
|
|
logout = kw.get('logout')
|
|
stage = kw.get('stage')
|
|
extra = {
|
|
'cookie': self.cookie,
|
|
}
|
|
if login:
|
|
extra['attended'] = attended
|
|
extra['username'] = username
|
|
extra['password'] = password
|
|
extra['openid_identifier'] = oid
|
|
if stage:
|
|
extra['multistage'] = True
|
|
login_msgs = []
|
|
self._login_multistage = None
|
|
|
|
if logout and 'setuid' in self.session:
|
|
del self.session['setuid']
|
|
return user_obj
|
|
|
|
for authmethod in self.cfg.auth:
|
|
if logout:
|
|
user_obj, cont = authmethod.logout(self, user_obj, **extra)
|
|
elif login:
|
|
if stage and authmethod.name != stage:
|
|
continue
|
|
ret = authmethod.login(self, user_obj, **extra)
|
|
user_obj = ret.user_obj
|
|
cont = ret.continue_flag
|
|
if stage:
|
|
stage = None
|
|
del extra['multistage']
|
|
if ret.multistage:
|
|
self._login_multistage = ret.multistage
|
|
self._login_multistage_name = authmethod.name
|
|
return user_obj
|
|
if ret.redirect_to:
|
|
nextstage = auth.get_multistage_continuation_url(self, authmethod.name)
|
|
url = ret.redirect_to
|
|
url = url.replace('%return_form', quote_plus(nextstage))
|
|
url = url.replace('%return', quote(nextstage))
|
|
self._auth_redirected = True
|
|
self.http_redirect(url)
|
|
return user_obj
|
|
msg = ret.message
|
|
if msg and not msg in login_msgs:
|
|
login_msgs.append(msg)
|
|
else:
|
|
user_obj, cont = authmethod.request(self, user_obj, **extra)
|
|
if not cont:
|
|
break
|
|
|
|
self._login_messages = login_msgs
|
|
return user_obj
|
|
|
|
def handle_jid_auth(self, jid):
|
|
return user.get_by_jabber_id(self, jid)
|
|
|
|
def parse_cookie(self):
|
|
try:
|
|
self.cookie = Cookie.SimpleCookie(self.saved_cookie)
|
|
except Cookie.CookieError:
|
|
self.cookie = None
|
|
|
|
def reset(self):
|
|
""" Reset request state.
|
|
|
|
Called after saving a page, before serving the updated
|
|
page. Solves some practical problems with request state
|
|
modified during saving.
|
|
|
|
"""
|
|
# 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.language_default
|
|
|
|
# caches unique ids
|
|
self.init_unique_ids()
|
|
|
|
if hasattr(self, "_fmt_hd_counters"):
|
|
del self._fmt_hd_counters
|
|
|
|
def loadTheme(self, theme_name):
|
|
""" Load the Theme to use for this request.
|
|
|
|
@param theme_name: the name of the theme
|
|
@type theme_name: str
|
|
@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
|
|
if theme_name == "<default>":
|
|
theme_name = self.cfg.theme_default
|
|
|
|
try:
|
|
Theme = wikiutil.importPlugin(self.cfg, 'theme', theme_name, 'Theme')
|
|
except wikiutil.PluginMissingError:
|
|
fallback = 1
|
|
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):
|
|
""" Set the content language, used for the content div
|
|
|
|
Actions that generate content in the user language, like search,
|
|
should set the content direction to the user language before they
|
|
call send_title!
|
|
"""
|
|
self.content_lang = lang
|
|
self.current_lang = lang
|
|
|
|
def getPragma(self, key, defval=None):
|
|
""" Query a pragma value (#pragma processing instruction)
|
|
|
|
Keys are not case-sensitive.
|
|
"""
|
|
return self.pragma.get(key.lower(), defval)
|
|
|
|
def setPragma(self, key, value):
|
|
""" Set a pragma value (#pragma processing instruction)
|
|
|
|
Keys are not case-sensitive.
|
|
"""
|
|
self.pragma[key.lower()] = value
|
|
|
|
def getPathinfo(self):
|
|
""" Return the remaining part of the URL. """
|
|
return self.path_info
|
|
|
|
def getScriptname(self):
|
|
""" Return the scriptname part of the URL ('/path/to/my.cgi'). """
|
|
if self.script_name == '/':
|
|
return ''
|
|
return self.script_name
|
|
|
|
def getKnownActions(self):
|
|
""" Create a dict of avaiable actions
|
|
|
|
Return cached version if avaiable.
|
|
|
|
@rtype: dict
|
|
@return: dict of all known actions
|
|
"""
|
|
try:
|
|
self.cfg.cache.known_actions # check
|
|
except AttributeError:
|
|
from MoinMoin import action
|
|
self.cfg.cache.known_actions = set(action.getNames(self.cfg))
|
|
|
|
# Return a copy, so clients will not change the set.
|
|
return self.cfg.cache.known_actions.copy()
|
|
|
|
def getAvailableActions(self, page):
|
|
""" Get list of avaiable actions for this request
|
|
|
|
The dict does not contain actions that starts with lower case.
|
|
Themes use this dict to display the actions to the user.
|
|
|
|
@param page: current page, Page object
|
|
@rtype: dict
|
|
@return: dict of avaiable actions
|
|
"""
|
|
if self._available_actions is None:
|
|
# some actions might make sense for non-existing pages, so we just
|
|
# require read access here. Can be later refined to some action
|
|
# specific check:
|
|
if not self.user.may.read(page.page_name):
|
|
return []
|
|
|
|
# Filter non ui actions (starts with lower case letter)
|
|
actions = self.getKnownActions()
|
|
actions = [action for action in actions if not action[0].islower()]
|
|
|
|
# Filter wiki excluded actions
|
|
actions = [action for action in actions if not action in self.cfg.actions_excluded]
|
|
|
|
# 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) or
|
|
not self.user.may.delete(page.page_name)):
|
|
# Prevent modification of underlay only pages, or pages
|
|
# the user can't write and can't delete
|
|
excluded = [u'RenamePage', u'DeletePage', ] # AttachFile must NOT be here!
|
|
actions = [action for action in actions if not action in excluded]
|
|
|
|
self._available_actions = set(actions)
|
|
|
|
# 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 """
|
|
buf = StringIO.StringIO()
|
|
self.redirect(buf)
|
|
try:
|
|
function(*args, **kw)
|
|
finally:
|
|
self.redirect()
|
|
text = buf.getvalue()
|
|
buf.close()
|
|
return text
|
|
|
|
def redirect(self, file=None):
|
|
""" Redirect output to file, or restore saved output """
|
|
if file:
|
|
self.writestack.append(self.write)
|
|
self.write = file.write
|
|
else:
|
|
self.write = self.writestack.pop()
|
|
|
|
def log(self, msg):
|
|
""" DEPRECATED - Log msg to logging framework
|
|
Please call logging.info(...) directly!
|
|
"""
|
|
msg = msg.strip()
|
|
# Encode unicode msg
|
|
if isinstance(msg, unicode):
|
|
msg = msg.encode(config.charset)
|
|
logging.info(msg)
|
|
|
|
def timing_log(self, start, action):
|
|
""" Log to timing log (for performance analysis) """
|
|
indicator = ''
|
|
if start:
|
|
total = "vvv"
|
|
else:
|
|
self.clock.stop('total') # make sure it is stopped
|
|
total_secs = self.clock.timings['total']
|
|
# we add some stuff that is easy to grep when searching for peformance problems:
|
|
if total_secs > 50:
|
|
indicator += '!4!'
|
|
elif total_secs > 20:
|
|
indicator += '!3!'
|
|
elif total_secs > 10:
|
|
indicator += '!2!'
|
|
elif total_secs > 2:
|
|
indicator += '!1!'
|
|
total = self.clock.value('total')
|
|
# use + for existing pages, - for non-existing pages
|
|
if self.page is not None:
|
|
indicator += self.page.exists() and '+' or '-'
|
|
if self.isSpiderAgent:
|
|
indicator += "B"
|
|
|
|
pid = os.getpid()
|
|
msg = 'Timing %5d %-6s %4s %-10s %s\n' % (pid, total, indicator, action, self.url)
|
|
logging.info(msg)
|
|
|
|
def send_file(self, fileobj, bufsize=8192, do_flush=False):
|
|
""" Send a file to the output stream.
|
|
|
|
@param fileobj: a file-like object (supporting read, close)
|
|
@param bufsize: size of chunks to read/write
|
|
@param do_flush: call flush after writing?
|
|
"""
|
|
while True:
|
|
buf = fileobj.read(bufsize)
|
|
if not buf:
|
|
break
|
|
self.write(buf)
|
|
if do_flush:
|
|
self.flush()
|
|
|
|
def write(self, *data):
|
|
""" Write to output stream. """
|
|
raise NotImplementedError
|
|
|
|
def encode(self, data):
|
|
""" encode data (can be both unicode strings and strings),
|
|
preparing for a single write()
|
|
"""
|
|
wd = []
|
|
for d in data:
|
|
try:
|
|
if isinstance(d, unicode):
|
|
# if we are REALLY sure, we can use "strict"
|
|
d = d.encode(config.charset, 'replace')
|
|
elif d is None:
|
|
continue
|
|
wd.append(d)
|
|
except UnicodeError:
|
|
logging.error("Unicode error on: %s" % repr(d))
|
|
return ''.join(wd)
|
|
|
|
def decodePagename(self, name):
|
|
""" Decode path, possibly using non ascii characters
|
|
|
|
Does not change the name, only decode to Unicode.
|
|
|
|
First split the path to pages, then decode each one. This enables
|
|
us to decode one page using config.charset and another using
|
|
utf-8. This situation happens when you try to add to a name of
|
|
an existing page.
|
|
|
|
See http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1
|
|
|
|
@param name: page name, string
|
|
@rtype: unicode
|
|
@return decoded page name
|
|
"""
|
|
# Split to pages and decode each one
|
|
pages = name.split('/')
|
|
decoded = []
|
|
for page in pages:
|
|
# Recode from utf-8 into config charset. If the path
|
|
# contains user typed parts, they are encoded using 'utf-8'.
|
|
if config.charset != 'utf-8':
|
|
try:
|
|
page = unicode(page, 'utf-8', 'strict')
|
|
# Fit data into config.charset, replacing what won't
|
|
# fit. Better have few "?" in the name than crash.
|
|
page = page.encode(config.charset, 'replace')
|
|
except UnicodeError:
|
|
pass
|
|
|
|
# Decode from config.charset, replacing what can't be decoded.
|
|
page = unicode(page, config.charset, 'replace')
|
|
decoded.append(page)
|
|
|
|
# Assemble decoded parts
|
|
name = u'/'.join(decoded)
|
|
return name
|
|
|
|
def normalizePagename(self, name):
|
|
""" Normalize page name
|
|
|
|
Prevent creating page names with invisible characters or funny
|
|
whitespace that might confuse the users or abuse the wiki, or
|
|
just does not make sense.
|
|
|
|
Restrict even more group pages, so they can be used inside acl lines.
|
|
|
|
@param name: page name, unicode
|
|
@rtype: unicode
|
|
@return: decoded and sanitized page name
|
|
"""
|
|
# Strip invalid characters
|
|
name = config.page_invalid_chars_regex.sub(u'', name)
|
|
|
|
# Split to pages and normalize each one
|
|
pages = name.split(u'/')
|
|
normalized = []
|
|
for page in pages:
|
|
# Ignore empty or whitespace only pages
|
|
if not page or page.isspace():
|
|
continue
|
|
|
|
# Cleanup group pages.
|
|
# Strip non alpha numeric characters, keep white space
|
|
if wikiutil.isGroupPage(self, page):
|
|
page = u''.join([c for c in page
|
|
if c.isalnum() or c.isspace()])
|
|
|
|
# Normalize white space. Each name can contain multiple
|
|
# words separated with only one space. Split handle all
|
|
# 30 unicode spaces (isspace() == True)
|
|
page = u' '.join(page.split())
|
|
|
|
normalized.append(page)
|
|
|
|
# Assemble components into full pagename
|
|
name = u'/'.join(normalized)
|
|
return name
|
|
|
|
def read(self, n):
|
|
""" Read n bytes from input stream. """
|
|
raise NotImplementedError
|
|
|
|
def flush(self):
|
|
""" Flush output stream. """
|
|
pass
|
|
|
|
def check_spider(self):
|
|
""" check if the user agent for current request is a spider/bot """
|
|
isSpider = False
|
|
ua = self.getUserAgent()
|
|
if ua and self.cfg.cache.ua_spiders:
|
|
isSpider = self.cfg.cache.ua_spiders.search(ua) 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
|
|
qs = self.query_string
|
|
action = self.action
|
|
if ((qs != '' or self.request_method != 'GET') and
|
|
action != 'rss_rc' and
|
|
# allow spiders to get attachments and do 'show'
|
|
not (action == 'AttachFile' and 'do=get' in qs) and
|
|
action != 'show' and
|
|
action != 'sitemap'
|
|
):
|
|
forbidden = self.isSpiderAgent
|
|
|
|
if not forbidden and self.cfg.hosts_deny:
|
|
ip = self.remote_addr
|
|
for host in self.cfg.hosts_deny:
|
|
if host[-1] == '.' and ip.startswith(host):
|
|
forbidden = 1
|
|
logging.debug("hosts_deny (net): %s" % str(forbidden))
|
|
break
|
|
if ip == host:
|
|
forbidden = 1
|
|
logging.debug("hosts_deny (ip): %s" % str(forbidden))
|
|
break
|
|
return forbidden
|
|
|
|
def setup_args(self):
|
|
""" Return args dict
|
|
First, we parse the query string (usually this is used in GET methods,
|
|
but TwikiDraw uses ?action=AttachFile&do=savedrawing plus posted stuff).
|
|
Second, we update what we got in first step by the stuff we get from
|
|
the form (or by a POST). We invoke _setup_args_from_cgi_form to handle
|
|
possible file uploads.
|
|
"""
|
|
args = cgi.parse_qs(self.query_string, keep_blank_values=1)
|
|
args = self.decodeArgs(args)
|
|
# if we have form data (in a POST), those override the stuff we already have:
|
|
if self.request_method == 'POST':
|
|
postargs = self._setup_args_from_cgi_form()
|
|
args.update(postargs)
|
|
return args
|
|
|
|
def _setup_args_from_cgi_form(self, form=None):
|
|
""" Return args dict from a FieldStorage
|
|
|
|
Create the args from a given form. Each key contain a list of values.
|
|
This method usually gets overridden in classes derived from this - it
|
|
is their task to call this method with an appropriate form parameter.
|
|
|
|
@param form: a cgi.FieldStorage
|
|
@rtype: dict
|
|
@return: dict with form keys, each contains a list of values
|
|
"""
|
|
args = {}
|
|
for key in form:
|
|
values = form[key]
|
|
if not isinstance(values, list):
|
|
values = [values]
|
|
fixedResult = []
|
|
for item in values:
|
|
if isinstance(item, cgi.FieldStorage) and item.filename:
|
|
fixedResult.append(item.file) # open data tempfile
|
|
# Save upload file name in a separate key
|
|
args[key + '__filename__'] = item.filename
|
|
else:
|
|
fixedResult.append(item.value)
|
|
args[key] = fixedResult
|
|
|
|
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.
|
|
|
|
Already qualified urls are returned unchanged.
|
|
|
|
@param uri: server rooted uri e.g /scriptname/pagename.
|
|
It must start with a slash. Must be ascii and url encoded.
|
|
"""
|
|
import urlparse
|
|
scheme = urlparse.urlparse(uri)[0]
|
|
if scheme:
|
|
return uri
|
|
|
|
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, resultcode, msg):
|
|
statusmsg = {
|
|
401: 'Authorization required',
|
|
403: 'FORBIDDEN',
|
|
404: 'Not found',
|
|
503: 'Service unavailable',
|
|
}
|
|
headers = [
|
|
'Status: %d %s' % (resultcode, statusmsg[resultcode]),
|
|
'Content-Type: text/plain; charset=utf-8'
|
|
]
|
|
# when surge protection triggered, tell bots to come back later...
|
|
if resultcode == 503:
|
|
headers.append('Retry-After: %d' % self.cfg.surge_lockout_time)
|
|
self.emit_http_headers(headers)
|
|
self.write(msg)
|
|
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 """
|
|
### HACK SAUVAGE 1/1
|
|
if self.remote_addr == '138.231.136.67':
|
|
theme_name = 'crans-www'
|
|
elif self.cfg.theme_force:
|
|
### FIN HACK 1/1
|
|
theme_name = self.cfg.theme_default
|
|
else:
|
|
theme_name = self.user.theme_name
|
|
self.loadTheme(theme_name)
|
|
|
|
def _try_redirect_spaces_page(self, pagename):
|
|
if '_' in pagename and not self.page.exists():
|
|
pname = pagename.replace('_', ' ')
|
|
pg = Page(self, pname)
|
|
if pg.exists():
|
|
url = pg.url(self)
|
|
self.http_redirect(url)
|
|
return True
|
|
return False
|
|
|
|
def run(self):
|
|
# Exit now if __init__ failed or request is forbidden
|
|
if self.failed or self.forbidden or self._auth_redirected:
|
|
# Don't sleep() here, it binds too much of our resources!
|
|
return self.finish()
|
|
|
|
_ = self.getText
|
|
self.clock.start('run')
|
|
|
|
self.initTheme()
|
|
|
|
action_name = self.action
|
|
if self.cfg.log_timing:
|
|
self.timing_log(True, action_name)
|
|
|
|
if action_name == 'xmlrpc':
|
|
from MoinMoin import xmlrpc
|
|
if self.query_string == 'action=xmlrpc':
|
|
xmlrpc.xmlrpc(self)
|
|
elif self.query_string == 'action=xmlrpc2':
|
|
xmlrpc.xmlrpc2(self)
|
|
if self.cfg.log_timing:
|
|
self.timing_log(False, action_name)
|
|
return self.finish()
|
|
|
|
# parse request data
|
|
try:
|
|
# The last component in path_info is the page name, if any
|
|
path = self.getPathinfo()
|
|
|
|
# we can have all action URLs like this: /action/ActionName/PageName?action=ActionName&...
|
|
# this is just for robots.txt being able to forbid them for crawlers
|
|
prefix = self.cfg.url_prefix_action
|
|
if prefix is not None:
|
|
prefix = '/%s/' % prefix # e.g. '/action/'
|
|
if path.startswith(prefix):
|
|
# remove prefix and action name
|
|
path = path[len(prefix):]
|
|
action, path = (path.split('/', 1) + ['', ''])[:2]
|
|
path = '/' + path
|
|
|
|
if path.startswith('/'):
|
|
pagename = self.normalizePagename(path)
|
|
else:
|
|
pagename = None
|
|
|
|
# need to inform caches that content changes based on:
|
|
# * cookie (even if we aren't sending one now)
|
|
# * User-Agent (because a bot might be denied and get no content)
|
|
# * Accept-Language (except if moin is told to ignore browser language)
|
|
if self.cfg.language_ignore_browser:
|
|
self.setHttpHeader("Vary: Cookie,User-Agent")
|
|
else:
|
|
self.setHttpHeader("Vary: Cookie,User-Agent,Accept-Language")
|
|
|
|
# Handle request. We have these options:
|
|
# 1. jump to page where user left off
|
|
if not pagename and self.user.remember_last_visit and action_name == 'show':
|
|
pagetrail = self.user.getTrail()
|
|
if pagetrail:
|
|
# Redirect to last page visited
|
|
last_visited = pagetrail[-1]
|
|
wikiname, pagename = wikiutil.split_interwiki(last_visited)
|
|
if wikiname != 'Self':
|
|
wikitag, wikiurl, wikitail, error = wikiutil.resolve_interwiki(self, wikiname, pagename)
|
|
url = wikiurl + wikiutil.quoteWikinameURL(wikitail)
|
|
else:
|
|
url = Page(self, pagename).url(self)
|
|
else:
|
|
# Or to localized FrontPage
|
|
url = wikiutil.getFrontPage(self).url(self)
|
|
self.http_redirect(url)
|
|
return self.finish()
|
|
|
|
# 2. handle action
|
|
else:
|
|
# pagename could be empty after normalization e.g. '///' -> ''
|
|
# Use localized FrontPage if pagename is empty
|
|
if not pagename:
|
|
self.page = wikiutil.getFrontPage(self)
|
|
else:
|
|
self.page = Page(self, pagename)
|
|
if self._try_redirect_spaces_page(pagename):
|
|
return self.finish()
|
|
|
|
msg = None
|
|
# Complain about unknown actions
|
|
if not action_name in self.getKnownActions():
|
|
msg = _("Unknown action %(action_name)s.") % {
|
|
'action_name': wikiutil.escape(action_name), }
|
|
|
|
# Disallow non available actions
|
|
elif action_name[0].isupper() and not action_name in self.getAvailableActions(self.page):
|
|
msg = _("You are not allowed to do %(action_name)s on this page.") % {
|
|
'action_name': wikiutil.escape(action_name), }
|
|
if not self.user.valid:
|
|
# Suggest non valid user to login
|
|
msg += " " + _("Login and try again.")
|
|
|
|
if msg:
|
|
self.theme.add_msg(msg, "error")
|
|
self.page.send_page()
|
|
# Try action
|
|
else:
|
|
from MoinMoin import action
|
|
handler = action.getHandler(self, action_name)
|
|
if handler is None:
|
|
msg = _("You are not allowed to do %(action_name)s on this page.") % {
|
|
'action_name': wikiutil.escape(action_name), }
|
|
if not self.user.valid:
|
|
# Suggest non valid user to login
|
|
msg += " " + _("Login and try again.")
|
|
self.theme.add_msg(msg, "error")
|
|
self.page.send_page()
|
|
else:
|
|
handler(self.page.page_name, self)
|
|
|
|
# every action that didn't use to raise MoinMoinFinish must call this now:
|
|
# self.theme.send_closing_html()
|
|
|
|
except MoinMoinFinish:
|
|
pass
|
|
except RemoteClosedConnection:
|
|
# at least clean up
|
|
pass
|
|
except SystemExit:
|
|
raise # fcgi uses this to terminate a thread
|
|
except Exception, err:
|
|
try:
|
|
# nothing we can do about further failures!
|
|
self.fail(err)
|
|
except:
|
|
pass
|
|
|
|
if self.cfg.log_timing:
|
|
self.timing_log(False, action_name)
|
|
|
|
return self.finish()
|
|
|
|
def http_redirect(self, url):
|
|
""" Redirect to a fully qualified, or server-rooted URL
|
|
|
|
@param url: relative or absolute url, ascii using url encoding.
|
|
"""
|
|
url = self.getQualifiedURL(url)
|
|
self.emit_http_headers(["Status: 302 Found", "Location: %s" % url])
|
|
|
|
def emit_http_headers(self, more_headers=[], testing=False):
|
|
""" emit http headers after some preprocessing / checking
|
|
|
|
Makes sure we only emit headers once.
|
|
Encodes to ASCII if it gets unicode headers.
|
|
Make sure we have exactly one Content-Type and one Status header.
|
|
Make sure Status header string begins with a integer number.
|
|
|
|
For emitting (testing == False), it calls the server specific
|
|
_emit_http_headers method. For testing, it returns the result.
|
|
|
|
@param more_headers: list of additional header strings
|
|
@param testing: set to True by test code
|
|
"""
|
|
user_headers = self.user_headers
|
|
self.user_headers = []
|
|
tracehere = ''.join(traceback.format_stack()[:-1])
|
|
all_headers = [(hdr, tracehere) for hdr in more_headers] + user_headers
|
|
|
|
if self.sent_headers:
|
|
# Send headers only once
|
|
logging.error("Attempt to send headers twice!")
|
|
logging.error("First attempt:\n%s" % self.sent_headers)
|
|
logging.error("Second attempt:\n%s" % tracehere)
|
|
raise HeadersAlreadySentException("emit_http_headers has already been called before!")
|
|
else:
|
|
self.sent_headers = tracehere
|
|
|
|
# assemble dict of http headers
|
|
headers = {}
|
|
traces = {}
|
|
for header, trace in all_headers:
|
|
if isinstance(header, unicode):
|
|
header = header.encode('ascii')
|
|
key, value = header.split(':', 1)
|
|
lkey = key.lower()
|
|
value = value.lstrip()
|
|
if lkey in headers:
|
|
if lkey in ['vary', 'cache-control', 'content-language', ]:
|
|
# these headers (list might be incomplete) allow multiple values
|
|
# that can be merged into a comma separated list
|
|
headers[lkey] = headers[lkey][0], '%s, %s' % (headers[lkey][1], value)
|
|
traces[lkey] = trace
|
|
else:
|
|
logging.warning("Duplicate http header: %r (ignored)" % header)
|
|
logging.warning("Header added first at:\n%s" % traces[lkey])
|
|
logging.warning("Header added again at:\n%s" % trace)
|
|
else:
|
|
headers[lkey] = (key, value)
|
|
traces[lkey] = trace
|
|
|
|
if 'content-type' not in headers:
|
|
headers['content-type'] = ('Content-type', 'text/html; charset=%s' % config.charset)
|
|
|
|
if 'status' not in headers:
|
|
headers['status'] = ('Status', '200 OK')
|
|
else:
|
|
# check if we got a valid status
|
|
try:
|
|
status = headers['status'][1]
|
|
int(status.split(' ', 1)[0])
|
|
except:
|
|
logging.error("emit_http_headers called with invalid header Status: %r" % status)
|
|
headers['status'] = ('Status', '500 Server Error - invalid status header')
|
|
|
|
header_format = '%s: %s'
|
|
st_header = header_format % headers['status']
|
|
del headers['status']
|
|
ct_header = header_format % headers['content-type']
|
|
del headers['content-type']
|
|
|
|
headers = [header_format % kv_tuple for kv_tuple in headers.values()] # make a list of strings
|
|
headers = [st_header, ct_header] + headers # do NOT change order!
|
|
if not testing:
|
|
self._emit_http_headers(headers)
|
|
else:
|
|
return headers
|
|
|
|
def _emit_http_headers(self, headers):
|
|
""" server specific method to emit http headers.
|
|
|
|
@param headers: a list of http header strings in this FIXED order:
|
|
1. status header (always present and valid, e.g. "200 OK")
|
|
2. content type header (always present)
|
|
3. other headers (optional)
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
def setHttpHeader(self, header):
|
|
""" Save header for later send.
|
|
|
|
Attention: although we use a list here, some implementations use a dict,
|
|
thus multiple calls with the same header type do NOT work in the end!
|
|
"""
|
|
# save a traceback with the header for duplicate bug reporting
|
|
self.user_headers.append((header, ''.join(traceback.format_stack()[:-1])))
|
|
|
|
def fail(self, err):
|
|
""" Fail when we can't continue
|
|
|
|
Send 500 status code with the error name. Reference:
|
|
http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
|
|
|
|
Log the error, then let failure module handle it.
|
|
|
|
@param err: Exception instance or subclass.
|
|
"""
|
|
self.failed = 1 # save state for self.run()
|
|
# we should not generate the headers two times
|
|
if not self.sent_headers:
|
|
self.emit_http_headers(['Status: 500 MoinMoin Internal Error'])
|
|
from MoinMoin import failure
|
|
failure.handle(self, err)
|
|
|
|
def make_unique_id(self, base, namespace=None):
|
|
"""
|
|
Generates a unique ID using a given base name. Appends a running count to the base.
|
|
|
|
Needs to stay deterministic!
|
|
|
|
@param base: the base of the id
|
|
@type base: unicode
|
|
@param namespace: the namespace for the ID, used when including pages
|
|
|
|
@returns: a unique (relatively to the namespace) ID
|
|
@rtype: unicode
|
|
"""
|
|
if not isinstance(base, unicode):
|
|
base = unicode(str(base), 'ascii', 'ignore')
|
|
if not namespace in self._page_ids:
|
|
self._page_ids[namespace] = {}
|
|
count = self._page_ids[namespace].get(base, -1) + 1
|
|
self._page_ids[namespace][base] = count
|
|
if not count:
|
|
return base
|
|
return u'%s-%d' % (base, count)
|
|
|
|
def init_unique_ids(self):
|
|
'''Initialise everything needed for unique IDs'''
|
|
self._unique_id_stack = []
|
|
self._page_ids = {None: {}}
|
|
self.include_id = None
|
|
self._include_stack = []
|
|
|
|
def push_unique_ids(self):
|
|
'''
|
|
Used by the TOC macro, this ensures that the ID namespaces
|
|
are reset to the status when the current include started.
|
|
This guarantees that doing the ID enumeration twice results
|
|
in the same results, on any level.
|
|
'''
|
|
self._unique_id_stack.append((self._page_ids, self.include_id))
|
|
self.include_id, pids = self._include_stack[-1]
|
|
# make a copy of the containing ID namespaces, that is to say
|
|
# go back to the level we had at the previous include
|
|
self._page_ids = {}
|
|
for namespace in pids:
|
|
self._page_ids[namespace] = pids[namespace].copy()
|
|
|
|
def pop_unique_ids(self):
|
|
'''
|
|
Used by the TOC macro to reset the ID namespaces after
|
|
having parsed the page for TOC generation and after
|
|
printing the TOC.
|
|
'''
|
|
self._page_ids, self.include_id = self._unique_id_stack.pop()
|
|
|
|
def begin_include(self, base):
|
|
'''
|
|
Called by the formatter when a document begins, which means
|
|
that include causing nested documents gives us an include
|
|
stack in self._include_id_stack.
|
|
'''
|
|
pids = {}
|
|
for namespace in self._page_ids:
|
|
pids[namespace] = self._page_ids[namespace].copy()
|
|
self._include_stack.append((self.include_id, pids))
|
|
self.include_id = self.make_unique_id(base)
|
|
# if it's the page name then set it to None so we don't
|
|
# prepend anything to IDs, but otherwise keep it.
|
|
if self.page and self.page.page_name == self.include_id:
|
|
self.include_id = None
|
|
|
|
def end_include(self):
|
|
'''
|
|
Called by the formatter when a document ends, restores
|
|
the current include ID to the previous one and discards
|
|
the page IDs state we kept around for push_unique_ids().
|
|
'''
|
|
self.include_id, pids = self._include_stack.pop()
|
|
|
|
def httpDate(self, when=None, rfc='1123'):
|
|
""" Returns http date string, according to rfc2068
|
|
|
|
See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-3.3
|
|
|
|
A http 1.1 server should use only rfc1123 date, but cookie's
|
|
"expires" field should use the older obsolete rfc850 date.
|
|
|
|
Note: we can not use strftime() because that honors the locale
|
|
and rfc2822 requires english day and month names.
|
|
|
|
We can not use email.Utils.formatdate because it formats the
|
|
zone as '-0000' instead of 'GMT', and creates only rfc1123
|
|
dates. This is a modified version of email.Utils.formatdate
|
|
from Python 2.4.
|
|
|
|
@param when: seconds from epoch, as returned by time.time()
|
|
@param rfc: conform to rfc ('1123' or '850')
|
|
@rtype: string
|
|
@return: http date conforming to rfc1123 or rfc850
|
|
"""
|
|
if when is None:
|
|
when = time.time()
|
|
now = time.gmtime(when)
|
|
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
|
|
'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.tm_mon - 1]
|
|
if rfc == '1123':
|
|
day = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now.tm_wday]
|
|
date = '%02d %s %04d' % (now.tm_mday, month, now.tm_year)
|
|
elif rfc == '850':
|
|
day = ["Monday", "Tuesday", "Wednesday", "Thursday",
|
|
"Friday", "Saturday", "Sunday"][now.tm_wday]
|
|
date = '%02d-%s-%s' % (now.tm_mday, month, str(now.tm_year)[-2:])
|
|
else:
|
|
raise ValueError("Invalid rfc value: %s" % rfc)
|
|
|
|
return '%s, %s %02d:%02d:%02d GMT' % (day, date, now.tm_hour,
|
|
now.tm_min, now.tm_sec)
|
|
|
|
def disableHttpCaching(self, level=1):
|
|
""" Prevent caching of pages that should not be cached.
|
|
|
|
level == 1 means disabling caching when we have a cookie set
|
|
level == 2 means completely disabling caching (used by Page*Editor)
|
|
|
|
This is important to prevent caches break acl by providing one
|
|
user pages meant to be seen only by another user, when both users
|
|
share the same caching proxy.
|
|
|
|
AVOID using no-cache and no-store for attachments as it is completely broken on IE!
|
|
|
|
Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP
|
|
"""
|
|
if level <= self.http_caching_disabled:
|
|
return # only make caching stricter
|
|
|
|
if level == 1:
|
|
# Set Cache control header for http 1.1 caches
|
|
# See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.3
|
|
# and http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.9
|
|
#self.setHttpHeader('Cache-Control: no-cache="set-cookie", private, max-age=0')
|
|
self.setHttpHeader('Cache-Control: private, must-revalidate, max-age=10')
|
|
elif level == 2:
|
|
self.setHttpHeader('Cache-Control: no-cache')
|
|
|
|
# only do this once to avoid 'duplicate header' warnings
|
|
# (in case the caching disabling is being made stricter)
|
|
if not self.http_caching_disabled:
|
|
# Set Expires for http 1.0 caches (does not support Cache-Control)
|
|
when = time.time() - (3600 * 24 * 365)
|
|
self.setHttpHeader('Expires: %s' % self.httpDate(when=when))
|
|
|
|
# Set Pragma for http 1.0 caches
|
|
# See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.32
|
|
# DISABLED for level == 1 to fix IE https file attachment downloading trouble.
|
|
if level == 2:
|
|
self.setHttpHeader('Pragma: no-cache')
|
|
|
|
self.http_caching_disabled = level
|
|
|
|
def finish(self):
|
|
""" General cleanup on end of request
|
|
|
|
Delete circular references - all object that we create using self.name = class(self).
|
|
This helps Python to collect these objects and keep our memory footprint lower.
|
|
"""
|
|
for method in self._finishers:
|
|
method(self)
|
|
# only execute finishers once
|
|
self._finishers = []
|
|
|
|
for attr_name in [
|
|
'editlog', # avoid leaking file handles for open edit-log
|
|
'theme',
|
|
'dicts',
|
|
'user',
|
|
'rootpage',
|
|
'page',
|
|
'html_formatter',
|
|
'formatter',
|
|
#'cfg', -- do NOT delattr cfg - it causes problems in the xapian indexing thread
|
|
]:
|
|
try:
|
|
delattr(self, attr_name)
|
|
except:
|
|
pass
|
|
|
|
def add_finisher(self, method):
|
|
self._finishers.append(method)
|
|
|
|
# Debug ------------------------------------------------------------
|
|
|
|
def debugEnvironment(self, env):
|
|
""" Environment debugging aid """
|
|
# Keep this one name per line so its easy to comment stuff
|
|
names = [
|
|
# 'http_accept_language',
|
|
# 'http_host',
|
|
# 'http_referer',
|
|
# 'http_user_agent',
|
|
# 'is_ssl',
|
|
'path_info',
|
|
'query_string',
|
|
# 'remote_addr',
|
|
'request_method',
|
|
# 'request_uri',
|
|
# 'saved_cookie',
|
|
'script_name',
|
|
# 'server_name',
|
|
# 'server_port',
|
|
]
|
|
names.sort()
|
|
attributes = []
|
|
for name in names:
|
|
attributes.append(' %s = %r\n' % (name, getattr(self, name, None)))
|
|
attributes = ''.join(attributes)
|
|
|
|
environment = []
|
|
names = env.keys()
|
|
names.sort()
|
|
for key in names:
|
|
environment.append(' %s = %r\n' % (key, env[key]))
|
|
environment = ''.join(environment)
|
|
|
|
data = '\nRequest Attributes\n%s\nEnvironment\n%s' % (attributes, environment)
|
|
f = open('/tmp/env.log', 'a')
|
|
try:
|
|
f.write(data)
|
|
finally:
|
|
f.close()
|
|
|