scripts/wiki/request.py
glondu d294a22d41 Application des patches crans.
darcs-hash:20070601130217-68412-6031a22f65c4203b17066a69c164031db0110c19.gz
2007-06-01 15:02:17 +02:00

2161 lines
77 KiB
Python

# -*- coding: iso-8859-1 -*-
"""
MoinMoin - Data associated with a single Request
@copyright: 2001-2003 by Jürgen Hermann <jh@web.de>
@copyright: 2003-2004 by Thomas Waldmann
@license: GNU GPL, see COPYING for details.
"""
import os, re, time, sys, cgi, StringIO
import copy
from MoinMoin import config, wikiutil, user, caching
from MoinMoin.util import MoinMoinNoFooter, IsWin9x
# Timing ---------------------------------------------------------------
class Clock:
""" Helper class for code profiling
we do not use time.clock() as this does not work across threads
"""
def __init__(self):
self.timings = {'total': time.time()}
def start(self, timer):
self.timings[timer] = time.time() - self.timings.get(timer, 0)
def stop(self, timer):
self.timings[timer] = time.time() - self.timings[timer]
def value(self, timer):
return "%.3f" % (self.timings[timer],)
def dump(self):
outlist = []
for timing in self.timings.items():
outlist.append("%s = %.3fs" % timing)
outlist.sort()
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(object):
""" A collection for all data associated with ONE request. """
# Header set to force misbehaved proxies and browsers to keep their
# hands off a page
# Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP
nocache = [
"Pragma: no-cache",
"Cache-Control: no-cache",
"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
dircache.reset()
# 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()
# 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.
self.decode_charsets = [config.charset]
# hierarchical wiki - set rootpage
from MoinMoin.Page import Page
#path = self.getPathinfo()
#if path.startswith('/'):
# pages = path[1:].split('/')
# if 0: # len(path) > 1:
# ## breaks MainPage/SubPage on flat storage
# rootname = u'/'.join(pages[:-1])
# else:
# # this is the usual case, as it ever was...
# rootname = u""
#else:
# # 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 = 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
self.logger = None
self.pragma = {}
self.mode_getpagelinks = 0
self.no_closing_html_code = 0
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.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 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 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
accept_charset.find('iso-8859-1') < 0):
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', '')
# 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
<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 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
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.
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
self._all_pages = None
# caches unique ids
self._page_ids = {}
# keeps track of pagename/heading combinations
# parsers should use this dict and not a local one, so that
# macros like TableOfContents in combination with Include
# can work
self._page_headings = {}
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 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.
@rtype: dict
@return: dict of all known actions
"""
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__
if name.startswith('do_')]
# Add plugins
dummy, plugins = wikiaction.getPlugins(self)
actions.extend(plugins)
# Add extensions
from MoinMoin.action import extension_actions
actions.extend(extension_actions)
# TODO: Use set when we require Python 2.3
actions = dict(zip(actions, [''] * len(actions)))
self.cfg._known_actions = actions
# Return a copy, so clients will not change the dict.
return self.cfg._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:
# 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)):
return []
# Filter non ui actions (starts with lower case letter)
actions = self.getKnownActions()
for key in actions.keys():
if key[0].islower():
del actions[key]
# Filter wiki 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) 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!
for key in excluded:
if key in actions:
del actions[key]
self._available_actions = 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 """
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):
""" 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 reset_output(self):
""" restore default output method
destroy output stack
(useful for error messages)
"""
if self.writestack:
self.write = self.writestack[0]
self.writestack = []
def log(self, msg):
""" Log to stderr, which may be error.log """
msg = msg.strip()
# Encode unicode msg
if isinstance(msg, unicode):
msg = msg.encode(config.charset)
# Add time stamp
msg = '[%s] %s\n' % (time.asctime(), msg)
sys.stderr.write(msg)
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')
wd.append(d)
except UnicodeError:
print >>sys.stderr, "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
Convert '_' to spaces - allows using nice URLs with spaces, with no
need to quote.
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
"""
# Replace underscores with spaces
name = name.replace(u'_', u' ')
# 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.
"""
raise NotImplementedError
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
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'
not ('action=AttachFile' in qs and 'do=get' in qs) and
not 'action=show' in qs
):
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
#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 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):
""" Return args dict from a FieldStorage
Create the args from a standard cgi.FieldStorage or from given
form. Each key contain a list of values.
@param form: a cgi.FieldStorage
@rtype: dict
@return: dict with form keys, each contains a list of values
"""
if form is None:
form = cgi.FieldStorage()
args = {}
for key in form:
values = form[key]
if not isinstance(values, list):
values = [values]
fixedResult = []
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 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 = {
403: 'FORBIDDEN',
503: 'Service unavailable',
}
self.http_headers([
'Status: %d %s' % (resultcode, statusmsg[resultcode]),
'Content-Type: text/plain'
])
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
#### 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)
def run(self):
# Exit now if __init__ failed or request is forbidden
if self.failed or self.forbidden:
#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
self.clock.start('run')
# Imports
from MoinMoin.Page import Page
if self.query_string == 'action=xmlrpc':
from MoinMoin.wikirpc import xmlrpc
xmlrpc(self)
return self.finish()
if self.query_string == 'action=xmlrpc2':
from MoinMoin.wikirpc import xmlrpc2
xmlrpc2(self)
return self.finish()
# parse request data
try:
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
# The last component in path_info is the page name, if any
path = self.getPathinfo()
if path.startswith('/'):
pagename = self.normalizePagename(path)
else:
pagename = None
# Handle request. We have these options:
# 1. If user has a bad user name, delete its bad cookie and
# send him to UserPreferences to make a new account.
if not user.isValidName(self, self.user.name):
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.user = self.get_user_default_unknown(name=self.user.name, logout=True)
page = wikiutil.getSysPage(self, 'UserPreferences')
page.send_page(self, msg=msg)
# 2. Or jump to page where user left off
elif not pagename and not action and self.user.remember_last_visit:
pagetrail = self.user.getTrail()
if pagetrail:
# Redirect to last page visited
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)
self.http_redirect(url)
return self.finish()
# 3. Or save drawing
elif (self.form.has_key('filepath') and
self.form.has_key('noredirect')):
# looks like user wants to save a drawing
from MoinMoin.action.AttachFile import execute
# TODO: what if pagename is None?
execute(pagename, self)
raise MoinMoinNoFooter
# 4. Or handle action
elif action:
# Use localized FrontPage if pagename is empty
if not pagename:
self.page = wikiutil.getFrontPage(self)
else:
self.page = Page(self, pagename)
# Complain about unknown actions
if not action in self.getKnownActions():
self.http_headers()
self.write(u'<html><body><h1>Unknown action %s</h1></body>' % wikiutil.escape(action))
# Disallow non available actions
elif (action[0].isupper() and
not action in self.getAvailableActions(self.page)):
# Send page with error
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
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
else:
from MoinMoin.wikiaction import getHandler
handler = getHandler(self, action)
handler(self.page.page_name, self)
# 5. Or redirect to another page
elif self.form.has_key('goto'):
self.http_redirect(Page(self, self.form['goto'][0]).url(self))
return self.finish()
# 6. Or (at last) visit pagename
else:
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
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
# default execution path, see the "except MoinMoinNoFooter"
# below)
self.clock.stop('run')
self.clock.stop('total')
# Close html code
if not self.no_closing_html_code:
if (self.cfg.show_timings and
self.form.get('action', [None])[0] != 'print'):
self.write('<ul id="timings">\n')
for t in self.clock.dump():
self.write('<li>%s</li>\n' % t)
self.write('</ul>\n')
#self.write('<!-- auth_method == %s -->' % repr(self.user.auth_method))
self.write('</body>\n</html>\n\n')
except MoinMoinNoFooter:
pass
except Exception, err:
self.fail(err)
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)
#### DEBUT HACK : pour le www, on redirige vers du www
if self.remote_addr in self.cfg.ip_url_replace.keys():
url = url.replace(self.cfg.ip_url_replace[self.remote_addr][0],self.cfg.ip_url_replace[self.remote_addr][1])
#### FIN DU HACK
self.http_headers(["Status: 302 Found", "Location: %s" % url])
def setHttpHeader(self, header):
""" Save header for later send. """
self.user_headers.append(header)
def setResponseCode(self, code, message=None):
pass
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()
self.http_headers(['Status: 500 MoinMoin Internal Error'])
self.setResponseCode(500)
self.log('%s: %s' % (err.__class__.__name__, str(err)))
from MoinMoin import failure
failure.handle(self)
def open_logs(self):
pass
def makeUniqueID(self, base):
"""
Generates a unique ID using a given base name. Appends a
running count to the base.
@param base: the base of the id
@type base: unicode
@returns: an unique id
@rtype: unicode
"""
if not isinstance(base, unicode):
base = unicode(str(base), 'ascii', 'ignore')
count = self._page_ids.get(base, -1) + 1
self._page_ids[base] = count
if count == 0:
return base
return u'%s_%04d' % (base, count)
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):
""" Prevent caching of pages that should not be cached
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.
"""
# Run only once
if hasattr(self, 'http_caching_disabled'):
return
self.http_caching_disabled = 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"')
self.setHttpHeader('Cache-Control: private')
self.setHttpHeader('Cache-Control: max-age=0')
# Set Expires for http 1.0 caches (does not support Cache-Control)
yearago = time.time() - (3600 * 24 * 365)
self.setHttpHeader('Expires: %s' % self.httpDate(when=yearago))
# Set Pragma for http 1.0 caches
# See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.32
self.setHttpHeader('Pragma: no-cache')
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
"""
try:
del self.user
del self.theme
del self.dicts
except:
pass
# ------------------------------------------------------------------
# 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\nEnviroment\n%s' % (attributes,
environment)
f = open('/tmp/env.log','a')
try:
f.write(data)
finally:
f.close()
# CGI ---------------------------------------------------------------
class RequestCGI(RequestBase):
""" specialized on CGI requests """
def __init__(self, properties={}):
try:
# force input/output to binary
if sys.platform == "win32":
import msvcrt
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
self._setup_vars_from_std_env(os.environ)
RequestBase.__init__(self, properties)
except Exception, err:
self.fail(err)
def open_logs(self):
# create log file for catching stderr output
if not self.opened_logs:
sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at')
self.opened_logs = 1
def read(self, n=None):
""" Read from input stream.
"""
if n is None:
return sys.stdin.read()
else:
return sys.stdin.read(n)
def write(self, *data):
""" Write to output stream.
"""
sys.stdout.write(self.encode(data))
def flush(self):
sys.stdout.flush()
def finish(self):
RequestBase.finish(self)
# flush the output, ignore errors caused by the user closing the socket
try:
sys.stdout.flush()
except IOError, ex:
import errno
if ex.errno != errno.EPIPE: raise
# Headers ----------------------------------------------------------
def http_headers(self, more_headers=[]):
# Send only once
if getattr(self, 'sent_headers', None):
return
self.sent_headers = 1
have_ct = 0
# send http headers
for header in more_headers + getattr(self, 'user_headers', []):
if header.lower().startswith("content-type:"):
# don't send content-type multiple times!
if have_ct: continue
have_ct = 1
if type(header) is unicode:
header = header.encode('ascii')
self.write("%s\r\n" % header)
if not have_ct:
self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
self.write('\r\n')
#from pprint import pformat
#sys.stderr.write(pformat(more_headers))
#sys.stderr.write(pformat(self.user_headers))
# Twisted -----------------------------------------------------------
class RequestTwisted(RequestBase):
""" specialized on Twisted requests """
def __init__(self, twistedRequest, pagename, reactor, properties={}):
try:
self.twistd = twistedRequest
self.reactor = reactor
# Copy headers
self.http_accept_language = self.twistd.getHeader('Accept-Language')
self.saved_cookie = self.twistd.getHeader('Cookie')
self.http_user_agent = self.twistd.getHeader('User-Agent')
# Copy values from twisted request
self.server_protocol = self.twistd.clientproto
self.server_name = self.twistd.getRequestHostname().split(':')[0]
self.server_port = str(self.twistd.getHost()[2])
self.is_ssl = self.twistd.isSecure()
self.path_info = '/' + '/'.join([pagename] + self.twistd.postpath)
self.request_method = self.twistd.method
self.remote_host = self.twistd.getClient()
self.remote_addr = self.twistd.getClientIP()
self.request_uri = self.twistd.uri
self.script_name = "/" + '/'.join(self.twistd.prepath[:-1])
# Values that need more work
self.query_string = self.splitURI(self.twistd.uri)[1]
self.setHttpReferer(self.twistd.getHeader('Referer'))
self.setHost()
self.setURL(self.twistd.getAllHeaders())
##self.debugEnvironment(twistedRequest.getAllHeaders())
RequestBase.__init__(self, properties)
except MoinMoinNoFooter: # might be triggered by http_redirect
self.http_headers() # send headers (important for sending MOIN_ID cookie)
self.finish()
except Exception, err:
# Wrap err inside an internal error if needed
from MoinMoin import error
if isinstance(err, error.FatalError):
self.delayedError = err
else:
self.delayedError = error.InternalError(str(err))
def run(self):
""" Handle delayed errors then invoke base class run """
if hasattr(self, 'delayedError'):
self.fail(self.delayedError)
return self.finish()
RequestBase.run(self)
def setup_args(self, form=None):
""" Return args dict
Twisted already parsed args, including __filename__ hacking,
but did not decoded the values.
"""
return self.decodeArgs(self.twistd.args)
def read(self, n=None):
""" Read from input stream.
"""
# XXX why is that wrong?:
#rd = self.reactor.callFromThread(self.twistd.read)
# XXX do we need self.reactor.callFromThread with that?
# XXX if yes, why doesn't it work?
self.twistd.content.seek(0, 0)
if n is None:
rd = self.twistd.content.read()
else:
rd = self.twistd.content.read(n)
#print "request.RequestTwisted.read: data=\n" + str(rd)
return rd
def write(self, *data):
""" Write to output stream.
"""
#print "request.RequestTwisted.write: data=\n" + wd
self.reactor.callFromThread(self.twistd.write, self.encode(data))
def flush(self):
pass # XXX is there a flush in twisted?
def finish(self):
RequestBase.finish(self)
self.reactor.callFromThread(self.twistd.finish)
def open_logs(self):
return
# create log file for catching stderr output
if not self.opened_logs:
sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at')
self.opened_logs = 1
# Headers ----------------------------------------------------------
def __setHttpHeader(self, header):
if type(header) is unicode:
header = header.encode('ascii')
key, value = header.split(':',1)
value = value.lstrip()
if key.lower() == 'set-cookie':
key, value = value.split('=',1)
self.twistd.addCookie(key, value)
else:
self.twistd.setHeader(key, value)
#print "request.RequestTwisted.setHttpHeader: %s" % header
def http_headers(self, more_headers=[]):
if getattr(self, 'sent_headers', None):
return
self.sent_headers = 1
have_ct = 0
# set http headers
for header in more_headers + getattr(self, 'user_headers', []):
if header.lower().startswith("content-type:"):
# don't send content-type multiple times!
if have_ct: continue
have_ct = 1
self.__setHttpHeader(header)
if not have_ct:
self.__setHttpHeader("Content-type: text/html;charset=%s" % config.charset)
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.twistd.redirect(url)
# calling finish here will send the rest of the data to the next
# request. leave the finish call to run()
#self.twistd.finish()
raise MoinMoinNoFooter
def setResponseCode(self, code, message=None):
self.twistd.setResponseCode(code, message)
# CLI ------------------------------------------------------------------
class RequestCLI(RequestBase):
""" specialized on command line interface and script requests """
def __init__(self, url='CLI', pagename='', properties={}):
self.saved_cookie = ''
self.path_info = '/' + pagename
self.query_string = ''
self.remote_addr = '127.0.0.1'
self.is_ssl = 0
self.http_user_agent = 'CLI/Script'
self.url = url
self.request_method = 'GET'
self.request_uri = '/' + pagename # TODO check
self.http_host = 'localhost'
self.http_referer = ''
self.script_name = '.'
RequestBase.__init__(self, properties)
self.cfg.caching_formats = [] # don't spoil the cache
self.initTheme() # usually request.run() does this, but we don't use it
def read(self, n=None):
""" Read from input stream.
"""
if n is None:
return sys.stdin.read()
else:
return sys.stdin.read(n)
def write(self, *data):
""" Write to output stream.
"""
sys.stdout.write(self.encode(data))
def flush(self):
sys.stdout.flush()
def finish(self):
RequestBase.finish(self)
# flush the output, ignore errors caused by the user closing the socket
try:
sys.stdout.flush()
except IOError, ex:
import errno
if ex.errno != errno.EPIPE: raise
def isForbidden(self):
""" Nothing is forbidden """
return 0
# Accessors --------------------------------------------------------
def getQualifiedURL(self, uri=None):
""" Return a full URL starting with schema and host
TODO: does this create correct pages when you render wiki pages
within a cli request?!
"""
return uri
# Headers ----------------------------------------------------------
def setHttpHeader(self, header):
pass
def http_headers(self, more_headers=[]):
pass
def http_redirect(self, url):
""" Redirect to a fully qualified, or server-rooted URL
TODO: Does this work for rendering redirect pages?
"""
raise Exception("Redirect not supported for command line tools!")
# StandAlone Server ----------------------------------------------------
class RequestStandAlone(RequestBase):
"""
specialized on StandAlone Server (MoinMoin.server.standalone) requests
"""
script_name = ''
def __init__(self, sa, properties={}):
"""
@param sa: stand alone server object
@param properties: ...
"""
try:
self.sareq = sa
self.wfile = sa.wfile
self.rfile = sa.rfile
self.headers = sa.headers
self.is_ssl = 0
# TODO: remove in 1.5
#accept = []
#for line in sa.headers.getallmatchingheaders('accept'):
# if line[:1] in string.whitespace:
# accept.append(line.strip())
# else:
# accept = accept + line[7:].split(',')
#
#env['HTTP_ACCEPT'] = ','.join(accept)
# Copy headers
self.http_accept_language = (sa.headers.getheader('accept-language')
or self.http_accept_language)
self.http_user_agent = sa.headers.getheader('user-agent', '')
co = filter(None, sa.headers.getheaders('cookie'))
self.saved_cookie = ', '.join(co) or ''
# Copy rest from standalone request
self.server_name = sa.server.server_name
self.server_port = str(sa.server.server_port)
self.request_method = sa.command
self.request_uri = sa.path
self.remote_addr = sa.client_address[0]
# Values that need more work
self.path_info, self.query_string = self.splitURI(sa.path)
self.setHttpReferer(sa.headers.getheader('referer'))
self.setHost(sa.headers.getheader('host'))
self.setURL(sa.headers)
# TODO: remove in 1.5
# from standalone script:
# XXX AUTH_TYPE
# XXX REMOTE_USER
# XXX REMOTE_IDENT
#env['PATH_TRANSLATED'] = uqrest #self.translate_path(uqrest)
#host = self.address_string()
#if host != self.client_address[0]:
# env['REMOTE_HOST'] = host
# env['SERVER_PROTOCOL'] = self.protocol_version
##self.debugEnvironment(sa.headers)
RequestBase.__init__(self, properties)
except Exception, err:
self.fail(err)
def _setup_args_from_cgi_form(self, form=None):
""" Override to create standlone form """
form = cgi.FieldStorage(self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD': 'POST'})
return RequestBase._setup_args_from_cgi_form(self, form)
def read(self, n=None):
""" Read from input stream
Since self.rfile.read() will block, content-length will be used
instead.
TODO: test with n > content length, or when calling several times
with smaller n but total over content length.
"""
if n is None:
try:
n = int(self.headers.get('content-length'))
except (TypeError, ValueError):
import warnings
warnings.warn("calling request.read() when content-length is "
"not available will block")
return self.rfile.read()
return self.rfile.read(n)
def write(self, *data):
""" Write to output stream.
"""
self.wfile.write(self.encode(data))
def flush(self):
self.wfile.flush()
def finish(self):
RequestBase.finish(self)
self.wfile.flush()
# Headers ----------------------------------------------------------
def http_headers(self, more_headers=[]):
if getattr(self, 'sent_headers', None):
return
self.sent_headers = 1
user_headers = getattr(self, 'user_headers', [])
# check for status header and send it
our_status = 200
for header in more_headers + user_headers:
if header.lower().startswith("status:"):
try:
our_status = int(header.split(':',1)[1].strip().split(" ", 1)[0])
except:
pass
# there should be only one!
break
# send response
self.sareq.send_response(our_status)
# send http headers
have_ct = 0
for header in more_headers + user_headers:
if type(header) is unicode:
header = header.encode('ascii')
if header.lower().startswith("content-type:"):
# don't send content-type multiple times!
if have_ct: continue
have_ct = 1
self.write("%s\r\n" % header)
if not have_ct:
self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
self.write('\r\n')
#from pprint import pformat
#sys.stderr.write(pformat(more_headers))
#sys.stderr.write(pformat(self.user_headers))
# mod_python/Apache ----------------------------------------------------
class RequestModPy(RequestBase):
""" specialized on mod_python requests """
def __init__(self, req):
""" Saves mod_pythons request and sets basic variables using
the req.subprocess_env, cause this provides a standard
way to access the values we need here.
@param req: the mod_python request instance
"""
try:
# flags if headers sent out contained content-type or status
self._have_ct = 0
self._have_status = 0
req.add_common_vars()
self.mpyreq = req
# some mod_python 2.7.X has no get method for table objects,
# so we make a real dict out of it first.
if not hasattr(req.subprocess_env,'get'):
env=dict(req.subprocess_env)
else:
env=req.subprocess_env
self._setup_vars_from_std_env(env)
RequestBase.__init__(self)
except Exception, err:
self.fail(err)
def fixURI(self, env):
""" Fix problems with script_name and path_info using
PythonOption directive to rewrite URI.
This is needed when using Apache 1 or other server which does
not support adding custom headers per request. With mod_python we
can use the PythonOption directive:
<Location /url/to/mywiki/>
PythonOption X-Moin-Location /url/to/mywiki/
</location>
Note that *neither* script_name *nor* path_info can be trusted
when Moin is invoked as a mod_python handler with apache1, so we
must build both using request_uri and the provided PythonOption.
"""
# Be compatible with release 1.3.5 "Location" option
# TODO: Remove in later release, we should have one option only.
old_location = 'Location'
options_table = self.mpyreq.get_options()
if not hasattr(options_table, 'get'):
options = dict(options_table)
else:
options = options_table
location = options.get(self.moin_location) or options.get(old_location)
if location:
env[self.moin_location] = location
# Try to recreate script_name and path_info from request_uri.
import urlparse
scriptAndPath = urlparse.urlparse(self.request_uri)[2]
self.script_name = location.rstrip('/')
path = scriptAndPath.replace(self.script_name, '', 1)
self.path_info = wikiutil.url_unquote(path, want_unicode=False)
RequestBase.fixURI(self, env)
def _setup_args_from_cgi_form(self, form=None):
""" Override to use mod_python.util.FieldStorage
Its little different from cgi.FieldStorage, so we need to
duplicate the conversion code.
"""
from mod_python import util
if form is None:
form = util.FieldStorage(self.mpyreq)
args = {}
for key in form.keys():
values = form[key]
if not isinstance(values, list):
values = [values]
fixedResult = []
for item in values:
# Remember filenames with a name hack
if hasattr(item, 'filename') and item.filename:
args[key + '__filename__'] = item.filename
# mod_python 2.7 might return strings instead of Field
# objects.
if hasattr(item, 'value'):
item = item.value
fixedResult.append(item)
args[key] = fixedResult
return self.decodeArgs(args)
def run(self, req):
""" mod_python calls this with its request object. We don't
need it cause its already passed to __init__. So ignore
it and just return RequestBase.run.
@param req: the mod_python request instance
"""
return RequestBase.run(self)
def read(self, n=None):
""" Read from input stream.
"""
if n is None:
return self.mpyreq.read()
else:
return self.mpyreq.read(n)
def write(self, *data):
""" Write to output stream.
"""
self.mpyreq.write(self.encode(data))
def flush(self):
""" We can't flush it, so do nothing.
"""
pass
def finish(self):
""" Just return apache.OK. Status is set in req.status.
"""
RequestBase.finish(self)
# is it possible that we need to return something else here?
from mod_python import apache
return apache.OK
# Headers ----------------------------------------------------------
def setHttpHeader(self, header):
""" Filters out content-type and status to set them directly
in the mod_python request. Rest is put into the headers_out
member of the mod_python request.
@param header: string, containing valid HTTP header.
"""
if type(header) is unicode:
header = header.encode('ascii')
key, value = header.split(':',1)
value = value.lstrip()
if key.lower() == 'content-type':
# save content-type for http_headers
if not self._have_ct:
# we only use the first content-type!
self.mpyreq.content_type = value
self._have_ct = 1
elif key.lower() == 'status':
# save status for finish
try:
self.mpyreq.status = int(value.split(' ',1)[0])
except:
pass
else:
self._have_status = 1
else:
# this is a header we sent out
self.mpyreq.headers_out[key]=value
def http_headers(self, more_headers=[]):
""" Sends out headers and possibly sets default content-type
and status.
@param more_headers: list of strings, defaults to []
"""
for header in more_headers + getattr(self, 'user_headers', []):
self.setHttpHeader(header)
# if we don't had an content-type header, set text/html
if self._have_ct == 0:
self.mpyreq.content_type = "text/html;charset=%s" % config.charset
# if we don't had a status header, set 200
if self._have_status == 0:
self.mpyreq.status = 200
# this is for mod_python 2.7.X, for 3.X it's a NOP
self.mpyreq.send_http_header()
# FastCGI -----------------------------------------------------------
class RequestFastCGI(RequestBase):
""" specialized on FastCGI requests """
def __init__(self, fcgRequest, env, form, properties={}):
""" Initializes variables from FastCGI environment and saves
FastCGI request and form for further use.
@param fcgRequest: the FastCGI request instance.
@param env: environment passed by FastCGI.
@param form: FieldStorage passed by FastCGI.
"""
try:
self.fcgreq = fcgRequest
self.fcgenv = env
self.fcgform = form
self._setup_vars_from_std_env(env)
RequestBase.__init__(self, properties)
except Exception, err:
self.fail(err)
def _setup_args_from_cgi_form(self, form=None):
""" Override to use FastCGI form """
if form is None:
form = self.fcgform
return RequestBase._setup_args_from_cgi_form(self, form)
def read(self, n=None):
""" Read from input stream.
"""
if n is None:
return self.fcgreq.stdin.read()
else:
return self.fcgreq.stdin.read(n)
def write(self, *data):
""" Write to output stream.
"""
self.fcgreq.out.write(self.encode(data))
def flush(self):
""" Flush output stream.
"""
self.fcgreq.flush_out()
def finish(self):
""" Call finish method of FastCGI request to finish handling
of this request.
"""
RequestBase.finish(self)
self.fcgreq.finish()
# Headers ----------------------------------------------------------
def http_headers(self, more_headers=[]):
""" Send out HTTP headers. Possibly set a default content-type.
"""
if getattr(self, 'sent_headers', None):
return
self.sent_headers = 1
have_ct = 0
# send http headers
for header in more_headers + getattr(self, 'user_headers', []):
if type(header) is unicode:
header = header.encode('ascii')
if header.lower().startswith("content-type:"):
# don't send content-type multiple times!
if have_ct: continue
have_ct = 1
self.write("%s\r\n" % header)
if not have_ct:
self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
self.write('\r\n')
#from pprint import pformat
#sys.stderr.write(pformat(more_headers))
#sys.stderr.write(pformat(self.user_headers))
# WSGI --------------------------------------------------------------
class RequestWSGI(RequestBase):
def __init__(self, env):
try:
self.env = env
self.hasContentType = False
self.stdin = env['wsgi.input']
self.stdout = StringIO.StringIO()
self.status = '200 OK'
self.headers = []
self._setup_vars_from_std_env(env)
RequestBase.__init__(self, {})
except Exception, err:
self.fail(err)
def setup_args(self, form=None):
if form is None:
form = cgi.FieldStorage(fp=self.stdin, environ=self.env, keep_blank_values=1)
return self._setup_args_from_cgi_form(form)
def read(self, n=None):
if n is None:
return self.stdin.read()
else:
return self.stdin.read(n)
def write(self, *data):
self.stdout.write(self.encode(data))
def reset_output(self):
self.stdout = StringIO.StringIO()
def setHttpHeader(self, header):
if type(header) is unicode:
header = header.encode('ascii')
key, value = header.split(':', 1)
value = value.lstrip()
if key.lower() == 'content-type':
# save content-type for http_headers
if self.hasContentType:
# we only use the first content-type!
return
else:
self.hasContentType = True
elif key.lower() == 'status':
# save status for finish
self.status = value
return
self.headers.append((key, value))
def http_headers(self, more_headers=[]):
for header in more_headers:
self.setHttpHeader(header)
if not self.hasContentType:
self.headers.insert(0, ('Content-Type', 'text/html;charset=%s' % config.charset))
def flush(self):
pass
def finish(self):
pass
def output(self):
return self.stdout.getvalue()