1
# -*- coding: iso-8859-1 -*-
3
MoinMoin - Data associated with a single Request
5
@copyright: 2001-2003 by J�rgen Hermann <jh@web.de>
6
@copyright: 2003-2006 by Thomas Waldmann
7
@license: GNU GPL, see COPYING for details.
10
import os, re, time, sys, cgi, StringIO
12
from MoinMoin import config, wikiutil, user, caching
13
from MoinMoin import error, multiconfig
14
from MoinMoin.util import MoinMoinNoFooter, IsWin9x
16
# umask setting --------------------------------------------------------
17
def set_umask(new_mask=0777^config.umask):
18
""" Set the OS umask value (and ignore potential failures on OSes where
19
this is not supported).
20
Default: the bitwise inverted value of config.umask
23
old_mask = os.umask(new_mask)
25
# maybe we are on win32?
28
# We do this at least once per Python process, when request is imported.
29
# If other software parts (like twistd's daemonize() function) set an
30
# unwanted umask, we have to call this again to set the correct one:
33
# Timing ---------------------------------------------------------------
36
""" Helper class for code profiling
37
we do not use time.clock() as this does not work across threads
44
def start(self, timer):
45
state = self.states.setdefault(timer, 'new')
47
self.timings[timer] = time.time()
48
self.states[timer] = 'running'
49
elif state == 'running':
50
pass # this timer is already running, do nothing
51
elif state == 'stopped':
52
# if a timer is stopped, timings has the sum of all times it was running
53
self.timings[timer] = time.time() - self.timings[timer]
54
self.states[timer] = 'running'
56
def stop(self, timer):
57
state = self.states.setdefault(timer, 'neverstarted')
58
if state == 'running':
59
self.timings[timer] = time.time() - self.timings[timer]
60
self.states[timer] = 'stopped'
61
elif state == 'stopped':
62
pass # this timer already has been stopped, do nothing
63
elif state == 'neverstarted':
64
pass # this timer never has been started, do nothing
66
def value(self, timer):
67
state = self.states.setdefault(timer, 'nosuchtimer')
68
if state == 'stopped':
69
result = "%.3fs" % self.timings[timer]
70
elif state == 'running':
71
result = "%.3fs (still running)" % (time.time() - self.timings[timer])
73
result = "- (%s)" % state
78
for timer in self.timings.keys():
79
value = self.value(timer)
80
outlist.append("%s = %s" % (timer, value))
87
def cgiMetaVariable(header, scheme='http'):
88
""" Return CGI meta variable for header name
90
e.g 'User-Agent' -> 'HTTP_USER_AGENT'
91
See http://www.faqs.org/rfcs/rfc3875.html section 4.1.18
93
var = '%s_%s' % (scheme, header)
94
return var.upper().replace('-', '_')
97
# Request Base ----------------------------------------------------------
99
class RequestBase(object):
100
""" A collection for all data associated with ONE request. """
102
# Defaults (used by sub classes)
103
http_accept_language = 'en'
104
server_name = 'localhost'
107
# Extra headers we support. Both standalone and twisted store
108
# headers as lowercase.
109
moin_location = 'x-moin-location'
110
proxy_host = 'x-forwarded-host'
112
def __init__(self, properties={}):
114
# twistd's daemonize() overrides our umask, so we reset it here every
115
# request. we do it for all request types to avoid similar problems.
118
# Decode values collected by sub classes
119
self.path_info = self.decodePagename(self.path_info)
123
self._available_actions = None
124
self._known_actions = None
126
# Pages meta data that we collect in one request
129
self.sent_headers = 0
130
self.user_headers = []
131
self.cacheable = 0 # may this output get cached by http proxies/caches?
132
self.http_caching_disabled = 0 # see disableHttpCaching()
136
# Fix dircaching problems on Windows 9x
141
# Check for dumb proxy requests
142
# TODO relying on request_uri will not work on all servers, especially
143
# not on external non-Apache servers
144
self.forbidden = False
145
if self.request_uri.startswith('http://'):
146
self.makeForbidden403()
152
self.clock.start('total')
153
# order is important here!
154
self.__dict__.update(properties)
156
self._load_multi_cfg()
157
except error.NoConfigMatchedError:
158
self.makeForbidden(404, 'No wiki configuration matching the URL found!\r\n')
163
self.isSpiderAgent = self.check_spider()
165
# Set decode charsets. Input from the user is always in
166
# config.charset, which is the page charsets. Except
167
# path_info, which may use utf-8, and handled by decodePagename.
168
self.decode_charsets = [config.charset]
170
# hierarchical wiki - set rootpage
171
from MoinMoin.Page import Page
172
#path = self.getPathinfo()
173
#if path.startswith('/'):
174
# pages = path[1:].split('/')
175
# if 0: # len(path) > 1:
176
# ## breaks MainPage/SubPage on flat storage
177
# rootname = u'/'.join(pages[:-1])
179
# # this is the usual case, as it ever was...
182
# # no extra path after script name
188
if not self.query_string.startswith('action=xmlrpc'):
189
self.args = self.form = self.setup_args()
192
self.rootpage = Page(self, rootname, is_rootpage=1)
194
self.user = self.get_user_from_form()
196
if not self.query_string.startswith('action=xmlrpc'):
197
if not self.forbidden and self.isForbidden():
198
self.makeForbidden403()
199
if not self.forbidden and self.surge_protect():
200
self.makeUnavailable503()
204
self.mode_getpagelinks = 0 # is > 0 as long as we are in a getPageLinks call
205
self.parsePageLinks_running = {} # avoid infinite recursion by remembering what we are already running
207
from MoinMoin import i18n
209
self.lang = i18n.requestLanguage(self)
210
# Language for content. Page content should use the wiki default lang,
211
# but generated content like search results should use the user language.
212
self.content_lang = self.cfg.language_default
213
self.getText = lambda text, i18n=self.i18n, request=self, lang=self.lang, **kv: i18n.getText(text, request, lang, kv.get('formatted', True))
217
def surge_protect(self, kick_him=False):
218
""" check if someone requesting too much from us,
219
if kick_him is True, we unconditionally blacklist the current user/ip
221
limits = self.cfg.surge_action_limits
224
validuser = self.user.valid
225
current_id = validuser and self.user.name or self.remote_addr
226
if not validuser and current_id.startswith('127.'): # localnet
228
current_action = self.form.get('action', ['show'])[0]
230
default_limit = self.cfg.surge_action_limits.get('default', (30, 60))
232
now = int(time.time())
234
surge_detected = False
237
cache = caching.CacheEntry(self, 'surgeprotect', 'surge-log')
239
data = cache.content(decode=True)
240
data = data.split("\n")
243
id, t, action, surge_indicator = line.split("\t")
245
maxnum, dt = limits.get(action, default_limit)
247
events = surgedict.setdefault(id, copy.copy({}))
248
timestamps = events.setdefault(action, copy.copy([]))
249
timestamps.append((t, surge_indicator))
250
except StandardError, err:
253
maxnum, dt = limits.get(current_action, default_limit)
254
events = surgedict.setdefault(current_id, copy.copy({}))
255
timestamps = events.setdefault(current_action, copy.copy([]))
256
surge_detected = len(timestamps) > maxnum
258
surge_indicator = surge_detected and "!" or ""
259
timestamps.append((now, surge_indicator))
261
if len(timestamps) < maxnum * 2:
262
timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
264
if current_action != 'AttachFile': # don't add AttachFile accesses to all or picture galleries will trigger SP
265
current_action = 'all' # put a total limit on user's requests
266
maxnum, dt = limits.get(current_action, default_limit)
267
events = surgedict.setdefault(current_id, copy.copy({}))
268
timestamps = events.setdefault(current_action, copy.copy([]))
270
if kick_him: # ban this guy, NOW
271
timestamps.extend([(now + self.cfg.surge_lockout_time, "!")] * (2*maxnum))
273
surge_detected = surge_detected or len(timestamps) > maxnum
275
surge_indicator = surge_detected and "!" or ""
276
timestamps.append((now, surge_indicator))
278
if len(timestamps) < maxnum * 2:
279
timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out
282
for id, events in surgedict.items():
283
for action, timestamps in events.items():
284
for t, surge_indicator in timestamps:
285
data.append("%s\t%d\t%s\t%s" % (id, t, action, surge_indicator))
286
data = "\n".join(data)
287
cache.update(data, encode=True)
288
except StandardError, err:
289
self.log('%s: %s' % (err.__class__.__name__, str(err)))
291
return surge_detected
294
""" Lazy initialize the dicts on the first access """
295
if self._dicts is None:
296
from MoinMoin import wikidicts
297
dicts = wikidicts.GroupDict(self)
303
""" Delete the dicts, used by some tests """
307
dicts = property(getDicts, None, delDicts)
309
def _load_multi_cfg(self):
310
# protect against calling multiple times
311
if not hasattr(self, 'cfg'):
312
self.clock.start('load_multi_cfg')
313
self.cfg = multiconfig.getConfig(self.url)
314
self.clock.stop('load_multi_cfg')
316
def setAcceptedCharsets(self, accept_charset):
317
""" Set accepted_charsets by parsing accept-charset header
319
Set self.accepted_charsets to an ordered list based on http_accept_charset.
321
Reference: http://www.w3.org/Protocols/rfc2616/rfc2616.txt
323
TODO: currently no code use this value.
325
@param accept_charset: accept-charset header
329
accept_charset = accept_charset.lower()
330
# Add iso-8859-1 if needed
331
if (not '*' in accept_charset and
332
accept_charset.find('iso-8859-1') < 0):
333
accept_charset += ',iso-8859-1'
335
# Make a list, sorted by quality value, using Schwartzian Transform
336
# Create list of tuples (value, name) , sort, extract names
337
for item in accept_charset.split(','):
339
name, qval = item.split(';')
340
qval = 1.0 - float(qval.split('=')[1])
343
charsets.append((qval, name))
345
# Remove *, its not clear what we should do with it later
346
charsets = [name for qval, name in charsets if name != '*']
348
self.accepted_charsets = charsets
350
def _setup_vars_from_std_env(self, env):
351
""" Set common request variables from CGI environment
353
Parse a standard CGI environment as created by common web servers.
354
Reference: http://www.faqs.org/rfcs/rfc3875.html
356
@param env: dict like object containing cgi meta variables
358
# Values we can just copy
360
self.http_accept_language = env.get('HTTP_ACCEPT_LANGUAGE', self.http_accept_language)
361
self.server_name = env.get('SERVER_NAME', self.server_name)
362
self.server_port = env.get('SERVER_PORT', self.server_port)
363
self.saved_cookie = env.get('HTTP_COOKIE', '')
364
self.script_name = env.get('SCRIPT_NAME', '')
365
self.path_info = env.get('PATH_INFO', '')
366
self.query_string = env.get('QUERY_STRING', '')
367
self.request_method = env.get('REQUEST_METHOD', None)
368
self.remote_addr = env.get('REMOTE_ADDR', '')
369
self.http_user_agent = env.get('HTTP_USER_AGENT', '')
370
self.if_modified_since = env.get('If-modified-since') or env.get(cgiMetaVariable('If-modified-since'))
371
self.if_none_match = env.get('If-none-match') or env.get(cgiMetaVariable('If-none-match'))
373
# REQUEST_URI is not part of CGI spec, but an addition of Apache.
374
self.request_uri = env.get('REQUEST_URI', '')
376
# Values that need more work
377
self.setHttpReferer(env.get('HTTP_REFERER'))
379
self.setHost(env.get('HTTP_HOST'))
383
##self.debugEnvironment(env)
385
def setHttpReferer(self, referer):
386
""" Set http_referer, making sure its ascii
388
IE might send non-ascii value.
392
value = unicode(referer, 'ascii', 'replace')
393
value = value.encode('ascii', 'replace')
394
self.http_referer = value
396
def setIsSSL(self, env):
399
@param env: dict like object containing cgi meta variables
401
self.is_ssl = bool(env.get('SSL_PROTOCOL') or
402
env.get('SSL_PROTOCOL_VERSION') or
403
env.get('HTTPS') == 'on')
405
def setHost(self, host=None):
408
Create from server name and port if missing. Previous code
409
default to localhost.
413
standardPort = ('80', '443')[self.is_ssl]
414
if self.server_port != standardPort:
415
port = ':' + self.server_port
416
host = self.server_name + port
417
self.http_host = host
419
def fixURI(self, env):
420
""" Fix problems with script_name and path_info
422
Handle the strange charset semantics on Windows and other non
423
posix systems. path_info is transformed into the system code
424
page by the web server. Additionally, paths containing dots let
425
most webservers choke.
427
Broken environment variables in different environments:
428
path_info script_name
429
Apache1 X X PI does not contain dots
430
Apache2 X X PI is not encoded correctly
431
IIS X X path_info include script_name
432
Other ? - ? := Possible and even RFC-compatible.
435
@param env: dict like object containing cgi meta variables
437
# Fix the script_name when using Apache on Windows.
438
server_software = env.get('SERVER_SOFTWARE', '')
439
if os.name == 'nt' and server_software.find('Apache/') != -1:
440
# Removes elements ending in '.' from the path.
441
self.script_name = '/'.join([x for x in self.script_name.split('/')
442
if not x.endswith('.')])
445
if os.name != 'posix' and self.request_uri != '':
446
# Try to recreate path_info from request_uri.
448
scriptAndPath = urlparse.urlparse(self.request_uri)[2]
449
path = scriptAndPath.replace(self.script_name, '', 1)
450
self.path_info = wikiutil.url_unquote(path, want_unicode=False)
451
elif os.name == 'nt':
452
# Recode path_info to utf-8
453
path = wikiutil.decodeWindowsPath(self.path_info)
454
self.path_info = path.encode("utf-8")
456
# Fix bug in IIS/4.0 when path_info contain script_name
457
if self.path_info.startswith(self.script_name):
458
self.path_info = self.path_info[len(self.script_name):]
460
def setURL(self, env):
461
""" Set url, used to locate wiki config
463
This is the place to manipulate url parts as needed.
465
@param env: dict like object containing cgi meta variables or http headers.
467
# If we serve on localhost:8000 and use a proxy on
468
# example.com/wiki, our urls will be example.com/wiki/pagename
469
# Same for the wiki config - they must use the proxy url.
470
self.rewriteHost(env)
473
if not self.request_uri:
474
self.request_uri = self.makeURI()
475
self.url = self.http_host + self.request_uri
477
def rewriteHost(self, env):
478
""" Rewrite http_host transparently
480
Get the proxy host using 'X-Forwarded-Host' header, added by
481
Apache 2 and other proxy software.
483
TODO: Will not work for Apache 1 or others that don't add this header.
485
TODO: If we want to add an option to disable this feature it
486
should be in the server script, because the config is not
487
loaded at this point, and must be loaded after url is set.
489
@param env: dict like object containing cgi meta variables or http headers.
491
proxy_host = (env.get(self.proxy_host) or
492
env.get(cgiMetaVariable(self.proxy_host)))
494
self.http_host = proxy_host
496
def rewriteURI(self, env):
497
""" Rewrite request_uri, script_name and path_info transparently
499
Useful when running mod python or when running behind a proxy,
500
e.g run on localhost:8000/ and serve as example.com/wiki/.
502
Uses private 'X-Moin-Location' header to set the script name.
503
This allow setting the script name when using Apache 2
504
<location> directive::
507
RequestHeader set X-Moin-Location /my/wiki/
510
TODO: does not work for Apache 1 and others that do not allow
511
setting custom headers per request.
513
@param env: dict like object containing cgi meta variables or http headers.
515
location = (env.get(self.moin_location) or
516
env.get(cgiMetaVariable(self.moin_location)))
520
scriptAndPath = self.script_name + self.path_info
521
location = location.rstrip('/')
522
self.script_name = location
524
# This may happen when using mod_python
525
if scriptAndPath.startswith(location):
526
self.path_info = scriptAndPath[len(location):]
528
# Recreate the URI from the modified parts
530
self.request_uri = self.makeURI()
533
""" Return uri created from uri parts """
534
uri = self.script_name + wikiutil.url_quote(self.path_info)
535
if self.query_string:
536
uri += '?' + self.query_string
539
def splitURI(self, uri):
540
""" Return path and query splited from uri
542
Just like CGI environment, the path is unquoted, the query is not.
545
path, query = uri.split('?', 1)
547
path, query = uri, ''
548
return wikiutil.url_unquote(path, want_unicode=False), query
550
def get_user_from_form(self):
551
""" read the maybe present UserPreferences form and call get_user with the values """
552
name = self.form.get('name', [None])[0]
553
password = self.form.get('password', [None])[0]
554
login = self.form.has_key('login')
555
logout = self.form.has_key('logout')
556
u = self.get_user_default_unknown(name=name, password=password,
557
login=login, logout=logout,
561
def get_user_default_unknown(self, **kw):
562
""" call do_auth and if it doesnt return a user object, make some "Unknown User" """
563
user_obj = self.get_user_default_None(**kw)
565
user_obj = user.User(self, auth_method="request:427")
568
def get_user_default_None(self, **kw):
569
""" loop over auth handlers, return a user obj or None """
570
name = kw.get('name')
571
password = kw.get('password')
572
login = kw.get('login')
573
logout = kw.get('logout')
574
user_obj = kw.get('user_obj')
575
for auth in self.cfg.auth:
576
user_obj, continue_flag = auth(self, name=name, password=password,
577
login=login, logout=logout, user_obj=user_obj)
578
if not continue_flag:
583
""" Reset request state.
585
Called after saving a page, before serving the updated
586
page. Solves some practical problems with request state
587
modified during saving.
590
# This is the content language and has nothing to do with
591
# The user interface language. The content language can change
592
# during the rendering of a page by lang macros
593
self.current_lang = self.cfg.language_default
595
self._all_pages = None
598
# keeps track of pagename/heading combinations
599
# parsers should use this dict and not a local one, so that
600
# macros like TableOfContents in combination with Include can work
601
self._page_headings = {}
603
if hasattr(self, "_fmt_hd_counters"):
604
del self._fmt_hd_counters
606
def loadTheme(self, theme_name):
607
""" Load the Theme to use for this request.
609
@param theme_name: the name of the theme
610
@type theme_name: str
612
@return: success code
614
1 if user theme could not be loaded,
615
2 if a hard fallback to modern theme was required.
618
if theme_name == "<default>":
619
theme_name = self.cfg.theme_default
622
Theme = wikiutil.importPlugin(self.cfg, 'theme', theme_name, 'Theme')
623
except wikiutil.PluginMissingError:
626
Theme = wikiutil.importPlugin(self.cfg, 'theme', self.cfg.theme_default, 'Theme')
627
except wikiutil.PluginMissingError:
629
from MoinMoin.theme.modern import Theme
631
self.theme = Theme(self)
634
def setContentLanguage(self, lang):
635
""" Set the content language, used for the content div
637
Actions that generate content in the user language, like search,
638
should set the content direction to the user language before they
641
self.content_lang = lang
642
self.current_lang = lang
644
def getPragma(self, key, defval=None):
645
""" Query a pragma value (#pragma processing instruction)
647
Keys are not case-sensitive.
649
return self.pragma.get(key.lower(), defval)
651
def setPragma(self, key, value):
652
""" Set a pragma value (#pragma processing instruction)
654
Keys are not case-sensitive.
656
self.pragma[key.lower()] = value
658
def getPathinfo(self):
659
""" Return the remaining part of the URL. """
660
return self.path_info
662
def getScriptname(self):
663
""" Return the scriptname part of the URL ('/path/to/my.cgi'). """
664
if self.script_name == '/':
666
return self.script_name
668
def getKnownActions(self):
669
""" Create a dict of avaiable actions
671
Return cached version if avaiable.
674
@return: dict of all known actions
677
self.cfg._known_actions # check
678
except AttributeError:
679
from MoinMoin import wikiaction
680
# Add built in actions from wikiaction
681
actions = [name[3:] for name in wikiaction.__dict__ if name.startswith('do_')]
684
dummy, plugins = wikiaction.getPlugins(self)
685
actions.extend(plugins)
688
from MoinMoin.action import extension_actions
689
actions.extend(extension_actions)
691
# TODO: Use set when we require Python 2.3
692
actions = dict(zip(actions, [''] * len(actions)))
693
self.cfg._known_actions = actions
695
# Return a copy, so clients will not change the dict.
696
return self.cfg._known_actions.copy()
698
def getAvailableActions(self, page):
699
""" Get list of avaiable actions for this request
701
The dict does not contain actions that starts with lower case.
702
Themes use this dict to display the actions to the user.
704
@param page: current page, Page object
706
@return: dict of avaiable actions
708
if self._available_actions is None:
709
# Add actions for existing pages only, including deleted pages.
710
# Fix *OnNonExistingPage bugs.
711
if not (page.exists(includeDeleted=1) and self.user.may.read(page.page_name)):
714
# Filter non ui actions (starts with lower case letter)
715
actions = self.getKnownActions()
716
for key in actions.keys():
720
# Filter wiki excluded actions
721
for key in self.cfg.actions_excluded:
725
# Filter actions by page type, acl and user state
727
if ((page.isUnderlayPage() and not page.isStandardPage()) or
728
not self.user.may.write(page.page_name) or
729
not self.user.may.delete(page.page_name)):
730
# Prevent modification of underlay only pages, or pages
731
# the user can't write and can't delete
732
excluded = [u'RenamePage', u'DeletePage', ] # AttachFile must NOT be here!
737
self._available_actions = actions
739
# Return a copy, so clients will not change the dict.
740
return self._available_actions.copy()
742
def redirectedOutput(self, function, *args, **kw):
743
""" Redirect output during function, return redirected output """
744
buffer = StringIO.StringIO()
745
self.redirect(buffer)
747
function(*args, **kw)
750
text = buffer.getvalue()
754
def redirect(self, file=None):
755
""" Redirect output to file, or restore saved output """
757
self.writestack.append(self.write)
758
self.write = file.write
760
self.write = self.writestack.pop()
762
def reset_output(self):
763
""" restore default output method
765
(useful for error messages)
768
self.write = self.writestack[0]
772
""" Log to stderr, which may be error.log """
775
if isinstance(msg, unicode):
776
msg = msg.encode(config.charset)
778
msg = '[%s] %s\n' % (time.asctime(), msg)
779
sys.stderr.write(msg)
781
def timing_log(self, start, action):
782
""" Log to timing log (for performance analysis) """
787
self.clock.stop('total') # make sure it is stopped
788
total_secs = self.clock.timings['total']
789
# we add some stuff that is easy to grep when searching for peformance problems:
792
elif total_secs > 20:
794
elif total_secs > 10:
798
total = self.clock.value('total')
799
# use + for existing pages, - for non-existing pages
800
indicator += self.page.exists() and '+' or '-'
801
if self.isSpiderAgent:
804
# Add time stamp and process ID
807
timestr = time.strftime("%Y%m%d %H%M%S", time.gmtime(t))
808
msg = '%s %5d %-6s %4s %-10s %s\n' % (timestr, pid, total, indicator, action, self.url)
809
self.timing_logfile.write(msg)
810
self.timing_logfile.flush()
812
def write(self, *data):
813
""" Write to output stream.
815
raise NotImplementedError
817
def encode(self, data):
818
""" encode data (can be both unicode strings and strings),
819
preparing for a single write()
824
if isinstance(d, unicode):
825
# if we are REALLY sure, we can use "strict"
826
d = d.encode(config.charset, 'replace')
831
print >>sys.stderr, "Unicode error on: %s" % repr(d)
834
def decodePagename(self, name):
835
""" Decode path, possibly using non ascii characters
837
Does not change the name, only decode to Unicode.
839
First split the path to pages, then decode each one. This enables
840
us to decode one page using config.charset and another using
841
utf-8. This situation happens when you try to add to a name of
844
See http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1
846
@param name: page name, string
848
@return decoded page name
850
# Split to pages and decode each one
851
pages = name.split('/')
854
# Recode from utf-8 into config charset. If the path
855
# contains user typed parts, they are encoded using 'utf-8'.
856
if config.charset != 'utf-8':
858
page = unicode(page, 'utf-8', 'strict')
859
# Fit data into config.charset, replacing what won't
860
# fit. Better have few "?" in the name than crash.
861
page = page.encode(config.charset, 'replace')
865
# Decode from config.charset, replacing what can't be decoded.
866
page = unicode(page, config.charset, 'replace')
869
# Assemble decoded parts
870
name = u'/'.join(decoded)
873
def normalizePagename(self, name):
874
""" Normalize page name
876
Convert '_' to spaces - allows using nice URLs with spaces, with no
879
Prevent creating page names with invisible characters or funny
880
whitespace that might confuse the users or abuse the wiki, or
881
just does not make sense.
883
Restrict even more group pages, so they can be used inside acl lines.
885
@param name: page name, unicode
887
@return: decoded and sanitized page name
889
# Replace underscores with spaces
890
name = name.replace(u'_', u' ')
892
# Strip invalid characters
893
name = config.page_invalid_chars_regex.sub(u'', name)
895
# Split to pages and normalize each one
896
pages = name.split(u'/')
899
# Ignore empty or whitespace only pages
900
if not page or page.isspace():
903
# Cleanup group pages.
904
# Strip non alpha numeric characters, keep white space
905
if wikiutil.isGroupPage(self, page):
906
page = u''.join([c for c in page
907
if c.isalnum() or c.isspace()])
909
# Normalize white space. Each name can contain multiple
910
# words separated with only one space. Split handle all
911
# 30 unicode spaces (isspace() == True)
912
page = u' '.join(page.split())
914
normalized.append(page)
916
# Assemble components into full pagename
917
name = u'/'.join(normalized)
921
""" Read n bytes from input stream.
923
raise NotImplementedError
926
""" Flush output stream.
928
raise NotImplementedError
930
def check_spider(self):
931
""" check if the user agent for current request is a spider/bot """
933
spiders = self.cfg.ua_spiders
935
ua = self.getUserAgent()
937
isSpider = re.search(spiders, ua, re.I) is not None
940
def isForbidden(self):
941
""" check for web spiders and refuse anything except viewing """
943
# we do not have a parsed query string here, so we can just do simple matching
944
qs = self.query_string
945
if ((qs != '' or self.request_method != 'GET') and
946
not 'action=rss_rc' in qs and
947
# allow spiders to get attachments and do 'show'
948
not ('action=AttachFile' in qs and 'do=get' in qs) and
949
not 'action=show' in qs and
950
not 'action=sitemap' in qs
952
forbidden = self.isSpiderAgent
954
if not forbidden and self.cfg.hosts_deny:
955
ip = self.remote_addr
956
for host in self.cfg.hosts_deny:
957
if host[-1] == '.' and ip.startswith(host):
959
#self.log("hosts_deny (net): %s" % str(forbidden))
963
#self.log("hosts_deny (ip): %s" % str(forbidden))
967
def setup_args(self, form=None):
970
In POST request, invoke _setup_args_from_cgi_form to handle possible
971
file uploads. For other request simply parse the query string.
973
Warning: calling with a form might fail, depending on the type of the
974
request! Only the request know which kind of form it can handle.
976
TODO: The form argument should be removed in 1.5.
978
if form is not None or self.request_method == 'POST':
979
return self._setup_args_from_cgi_form(form)
980
args = cgi.parse_qs(self.query_string, keep_blank_values=1)
981
return self.decodeArgs(args)
983
def _setup_args_from_cgi_form(self, form=None):
984
""" Return args dict from a FieldStorage
986
Create the args from a standard cgi.FieldStorage or from given form.
987
Each key contain a list of values.
989
@param form: a cgi.FieldStorage
991
@return: dict with form keys, each contains a list of values
994
form = cgi.FieldStorage()
999
if not isinstance(values, list):
1003
fixedResult.append(item.value)
1004
if isinstance(item, cgi.FieldStorage) and item.filename:
1005
# Save upload file name in a separate key
1006
args[key + '__filename__'] = item.filename
1007
args[key] = fixedResult
1009
return self.decodeArgs(args)
1011
def decodeArgs(self, args):
1012
""" Decode args dict
1014
Decoding is done in a separate path because it is reused by
1015
other methods and sub classes.
1017
decode = wikiutil.decodeUserInput
1020
if key + '__filename__' in args:
1021
# Copy file data as is
1022
result[key] = args[key]
1023
elif key.endswith('__filename__'):
1024
result[key] = decode(args[key], self.decode_charsets)
1026
result[key] = [decode(value, self.decode_charsets) for value in args[key]]
1029
def getBaseURL(self):
1030
""" Return a fully qualified URL to this script. """
1031
return self.getQualifiedURL(self.getScriptname())
1033
def getQualifiedURL(self, uri=''):
1034
""" Return an absolute URL starting with schema and host.
1036
Already qualified urls are returned unchanged.
1038
@param uri: server rooted uri e.g /scriptname/pagename.
1039
It must start with a slash. Must be ascii and url encoded.
1042
scheme = urlparse.urlparse(uri)[0]
1046
scheme = ('http', 'https')[self.is_ssl]
1047
result = "%s://%s%s" % (scheme, self.http_host, uri)
1049
# This might break qualified urls in redirects!
1050
# e.g. mapping 'http://netloc' -> '/'
1051
return wikiutil.mapURL(self, result)
1053
def getUserAgent(self):
1054
""" Get the user agent. """
1055
return self.http_user_agent
1057
def makeForbidden(self, resultcode, msg):
1061
503: 'Service unavailable',
1064
'Status: %d %s' % (resultcode, statusmsg[resultcode]),
1065
'Content-Type: text/plain'
1068
self.setResponseCode(resultcode)
1069
self.forbidden = True
1071
def makeForbidden403(self):
1072
self.makeForbidden(403, 'You are not allowed to access this!\r\n')
1074
def makeUnavailable503(self):
1075
self.makeForbidden(503, "Warning:\r\n"
1076
"You triggered the wiki's surge protection by doing too many requests in a short time.\r\n"
1077
"Please make a short break reading the stuff you already got.\r\n"
1078
"When you restart doing requests AFTER that, slow down or you might get locked out for a longer time!\r\n")
1080
def initTheme(self):
1081
""" Set theme - forced theme, user theme or wiki default """
1082
if self.cfg.theme_force:
1083
theme_name = self.cfg.theme_default
1085
theme_name = self.user.theme_name
1086
self.loadTheme(theme_name)
1089
# Exit now if __init__ failed or request is forbidden
1090
if self.failed or self.forbidden:
1091
# Don't sleep() here, it binds too much of our resources!
1092
return self.finish()
1095
self.clock.start('run')
1097
from MoinMoin.Page import Page
1099
if self.query_string == 'action=xmlrpc':
1100
from MoinMoin.wikirpc import xmlrpc
1102
return self.finish()
1104
if self.query_string == 'action=xmlrpc2':
1105
from MoinMoin.wikirpc import xmlrpc2
1107
return self.finish()
1109
# parse request data
1113
action = self.form.get('action', [None])[0]
1115
if self.cfg.log_timing:
1116
self.timing_log(True, action)
1118
# The last component in path_info is the page name, if any
1119
path = self.getPathinfo()
1120
if path.startswith('/'):
1121
pagename = self.normalizePagename(path)
1125
# need to inform caches that content changes based on:
1126
# * cookie (even if we aren't sending one now)
1127
# * User-Agent (because a bot might be denied and get no content)
1128
# * Accept-Language (except if moin is told to ignore browser language)
1129
if self.cfg.language_ignore_browser:
1130
self.setHttpHeader("Vary: Cookie,User-Agent")
1132
self.setHttpHeader("Vary: Cookie,User-Agent,Accept-Language")
1134
# Handle request. We have these options:
1136
# 1. If user has a bad user name, delete its bad cookie and
1137
# send him to UserPreferences to make a new account.
1138
if not user.isValidName(self, self.user.name):
1139
msg = _("""Invalid user name {{{'%s'}}}.
1140
Name may contain any Unicode alpha numeric character, with optional one
1141
space between words. Group page name is not allowed.""") % self.user.name
1142
self.user = self.get_user_default_unknown(name=self.user.name, logout=True)
1143
page = wikiutil.getSysPage(self, 'UserPreferences')
1144
page.send_page(self, msg=msg)
1146
# 2. Or jump to page where user left off
1147
elif not pagename and not action and self.user.remember_last_visit:
1148
pagetrail = self.user.getTrail()
1150
# Redirect to last page visited
1151
if ":" in pagetrail[-1]:
1152
wikitag, wikiurl, wikitail, error = wikiutil.resolve_wiki(self, pagetrail[-1])
1153
url = wikiurl + wikiutil.quoteWikinameURL(wikitail)
1155
url = Page(self, pagetrail[-1]).url(self)
1157
# Or to localized FrontPage
1158
url = wikiutil.getFrontPage(self).url(self)
1159
self.http_redirect(url)
1160
return self.finish()
1162
# 3. Or save drawing
1163
elif self.form.has_key('filepath') and self.form.has_key('noredirect'):
1164
# looks like user wants to save a drawing
1165
from MoinMoin.action.AttachFile import execute
1166
# TODO: what if pagename is None?
1167
execute(pagename, self)
1168
raise MoinMoinNoFooter
1170
# 4. Or handle action
1174
# pagename could be empty after normalization e.g. '///' -> ''
1175
# Use localized FrontPage if pagename is empty
1177
self.page = wikiutil.getFrontPage(self)
1179
self.page = Page(self, pagename)
1181
# Complain about unknown actions
1182
if not action in self.getKnownActions():
1184
self.write(u'<html><body><h1>Unknown action %s</h1></body>' % wikiutil.escape(action))
1186
# Disallow non available actions
1187
elif action[0].isupper() and not action in self.getAvailableActions(self.page):
1188
# Send page with error
1189
msg = _("You are not allowed to do %s on this page.") % wikiutil.escape(action)
1190
if not self.user.valid:
1191
# Suggest non valid user to login
1192
msg += " " + _("Login and try again.", formatted=0)
1193
self.page.send_page(self, msg=msg)
1197
from MoinMoin.wikiaction import getHandler
1198
handler = getHandler(self, action)
1200
# Send page with error
1201
msg = _("You are not allowed to do %s on this page.") % wikiutil.escape(action)
1202
if not self.user.valid:
1203
# Suggest non valid user to login
1204
msg += " " + _("Login and try again.", formatted=0)
1205
self.page.send_page(self, msg=msg)
1207
handler(self.page.page_name, self)
1209
# generate page footer (actions that do not want this footer use
1210
# raise util.MoinMoinNoFooter to break out of the default execution
1211
# path, see the "except MoinMoinNoFooter" below)
1213
self.clock.stop('run')
1214
self.clock.stop('total')
1217
if self.cfg.show_timings and action != 'print':
1218
self.write('<ul id="timings">\n')
1219
for t in self.clock.dump():
1220
self.write('<li>%s</li>\n' % t)
1221
self.write('</ul>\n')
1222
#self.write('<!-- auth_method == %s -->' % repr(self.user.auth_method))
1223
self.write('</body>\n</html>\n\n')
1225
except MoinMoinNoFooter:
1227
except Exception, err:
1230
if self.cfg.log_timing:
1231
self.timing_log(False, action)
1233
return self.finish()
1235
def http_redirect(self, url):
1236
""" Redirect to a fully qualified, or server-rooted URL
1238
@param url: relative or absolute url, ascii using url encoding.
1240
url = self.getQualifiedURL(url)
1241
self.http_headers(["Status: 302 Found", "Location: %s" % url])
1242
self.setResponseCode(302)
1244
def setHttpHeader(self, header):
1245
""" Save header for later send.
1247
Attention: although we use a list here, some implementations use a dict,
1248
thus multiple calls with the same header type do NOT work in the end!
1250
self.user_headers.append(header)
1252
def setResponseCode(self, code, message=None):
1255
def fail(self, err):
1256
""" Fail when we can't continue
1258
Send 500 status code with the error name. Reference:
1259
http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1
1261
Log the error, then let failure module handle it.
1263
@param err: Exception instance or subclass.
1265
self.failed = 1 # save state for self.run()
1266
self.http_headers(['Status: 500 MoinMoin Internal Error'])
1267
self.setResponseCode(500)
1268
self.log('%s: %s' % (err.__class__.__name__, str(err)))
1269
from MoinMoin import failure
1270
failure.handle(self)
1272
def open_logs(self):
1275
def makeUniqueID(self, base):
1277
Generates a unique ID using a given base name. Appends a running count to the base.
1279
@param base: the base of the id
1282
@returns: an unique id
1285
if not isinstance(base, unicode):
1286
base = unicode(str(base), 'ascii', 'ignore')
1287
count = self._page_ids.get(base, -1) + 1
1288
self._page_ids[base] = count
1291
return u'%s_%04d' % (base, count)
1293
def httpDate(self, when=None, rfc='1123'):
1294
""" Returns http date string, according to rfc2068
1296
See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-3.3
1298
A http 1.1 server should use only rfc1123 date, but cookie's
1299
"expires" field should use the older obsolete rfc850 date.
1301
Note: we can not use strftime() because that honors the locale
1302
and rfc2822 requires english day and month names.
1304
We can not use email.Utils.formatdate because it formats the
1305
zone as '-0000' instead of 'GMT', and creates only rfc1123
1306
dates. This is a modified version of email.Utils.formatdate
1309
@param when: seconds from epoch, as returned by time.time()
1310
@param rfc: conform to rfc ('1123' or '850')
1312
@return: http date conforming to rfc1123 or rfc850
1316
now = time.gmtime(when)
1317
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
1318
'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.tm_mon - 1]
1320
day = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now.tm_wday]
1321
date = '%02d %s %04d' % (now.tm_mday, month, now.tm_year)
1323
day = ["Monday", "Tuesday", "Wednesday", "Thursday",
1324
"Friday", "Saturday", "Sunday"][now.tm_wday]
1325
date = '%02d-%s-%s' % (now.tm_mday, month, str(now.tm_year)[-2:])
1327
raise ValueError("Invalid rfc value: %s" % rfc)
1329
return '%s, %s %02d:%02d:%02d GMT' % (day, date, now.tm_hour,
1330
now.tm_min, now.tm_sec)
1332
def disableHttpCaching(self, level=1):
1333
""" Prevent caching of pages that should not be cached.
1335
level == 1 means disabling caching when we have a cookie set
1336
level == 2 means completely disabling caching (used by Page*Editor)
1338
This is important to prevent caches break acl by providing one
1339
user pages meant to be seen only by another user, when both users
1340
share the same caching proxy.
1342
AVOID using no-cache and no-store for attachments as it is completely broken on IE!
1344
Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP
1346
if level <= self.http_caching_disabled:
1347
return # only make caching stricter
1350
# Set Cache control header for http 1.1 caches
1351
# See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.3
1352
# and http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.9
1353
#self.setHttpHeader('Cache-Control: no-cache="set-cookie", private, max-age=0')
1354
self.setHttpHeader('Cache-Control: private, must-revalidate, max-age=10')
1356
self.setHttpHeader('Cache-Control: no-cache')
1358
# Set Expires for http 1.0 caches (does not support Cache-Control)
1359
when = time.time() - (3600 * 24 * 365)
1360
self.setHttpHeader('Expires: %s' % self.httpDate(when=when))
1362
# Set Pragma for http 1.0 caches
1363
# See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.32
1364
# DISABLED for level == 1 to fix IE https file attachment downloading trouble.
1366
self.setHttpHeader('Pragma: no-cache')
1368
self.http_caching_disabled = level
1371
""" General cleanup on end of request
1373
Delete circular references - all object that we create using self.name = class(self).
1374
This helps Python to collect these objects and keep our memory footprint lower.
1383
# Debug ------------------------------------------------------------
1385
def debugEnvironment(self, env):
1386
""" Environment debugging aid """
1387
# Keep this one name per line so its easy to comment stuff
1389
# 'http_accept_language',
1392
# 'http_user_agent',
1407
attributes.append(' %s = %r\n' % (name, getattr(self, name, None)))
1408
attributes = ''.join(attributes)
1414
environment.append(' %s = %r\n' % (key, env[key]))
1415
environment = ''.join(environment)
1417
data = '\nRequest Attributes\n%s\nEnviroment\n%s' % (attributes, environment)
1418
f = open('/tmp/env.log', 'a')
1425
# CGI ---------------------------------------------------------------
1427
class RequestCGI(RequestBase):
1428
""" specialized on CGI requests """
1430
def __init__(self, properties={}):
1432
# force input/output to binary
1433
if sys.platform == "win32":
1435
msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
1436
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1438
self._setup_vars_from_std_env(os.environ)
1439
RequestBase.__init__(self, properties)
1441
except Exception, err:
1444
def open_logs(self):
1445
# create log file for catching stderr output
1446
if not self.opened_logs:
1447
sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at', 0)
1448
self.opened_logs = 1
1450
def read(self, n=None):
1451
""" Read from input stream. """
1453
return sys.stdin.read()
1455
return sys.stdin.read(n)
1457
def write(self, *data):
1458
""" Write to output stream. """
1459
sys.stdout.write(self.encode(data))
1465
RequestBase.finish(self)
1466
# flush the output, ignore errors caused by the user closing the socket
1471
if ex.errno != errno.EPIPE: raise
1473
# Headers ----------------------------------------------------------
1475
def http_headers(self, more_headers=[]):
1477
if getattr(self, 'sent_headers', None):
1480
self.sent_headers = 1
1484
for header in more_headers + getattr(self, 'user_headers', []):
1485
if header.lower().startswith("content-type:"):
1486
# don't send content-type multiple times!
1487
if have_ct: continue
1489
if type(header) is unicode:
1490
header = header.encode('ascii')
1491
self.write("%s\r\n" % header)
1494
self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
1498
#from pprint import pformat
1499
#sys.stderr.write(pformat(more_headers))
1500
#sys.stderr.write(pformat(self.user_headers))
1503
# Twisted -----------------------------------------------------------
1505
class RequestTwisted(RequestBase):
1506
""" specialized on Twisted requests """
1508
def __init__(self, twistedRequest, pagename, reactor, properties={}):
1510
self.twistd = twistedRequest
1511
self.reactor = reactor
1514
self.http_accept_language = self.twistd.getHeader('Accept-Language')
1515
self.saved_cookie = self.twistd.getHeader('Cookie')
1516
self.http_user_agent = self.twistd.getHeader('User-Agent')
1517
self.if_modified_since = self.twistd.getHeader('If-Modified-Since')
1518
self.if_none_match = self.twistd.getHeader('If-None-Match')
1520
# Copy values from twisted request
1521
self.server_protocol = self.twistd.clientproto
1522
self.server_name = self.twistd.getRequestHostname().split(':')[0]
1523
self.server_port = str(self.twistd.getHost()[2])
1524
self.is_ssl = self.twistd.isSecure()
1525
self.path_info = '/' + '/'.join([pagename] + self.twistd.postpath)
1526
self.request_method = self.twistd.method
1527
self.remote_addr = self.twistd.getClientIP()
1528
self.request_uri = self.twistd.uri
1529
self.script_name = "/" + '/'.join(self.twistd.prepath[:-1])
1531
# Values that need more work
1532
self.query_string = self.splitURI(self.twistd.uri)[1]
1533
self.setHttpReferer(self.twistd.getHeader('Referer'))
1535
self.setURL(self.twistd.getAllHeaders())
1537
##self.debugEnvironment(twistedRequest.getAllHeaders())
1539
RequestBase.__init__(self, properties)
1541
except MoinMoinNoFooter: # might be triggered by http_redirect
1542
self.http_headers() # send headers (important for sending MOIN_ID cookie)
1545
except Exception, err:
1546
# Wrap err inside an internal error if needed
1547
from MoinMoin import error
1548
if isinstance(err, error.FatalError):
1549
self.delayedError = err
1551
self.delayedError = error.InternalError(str(err))
1554
""" Handle delayed errors then invoke base class run """
1555
if hasattr(self, 'delayedError'):
1556
self.fail(self.delayedError)
1557
return self.finish()
1558
RequestBase.run(self)
1560
def setup_args(self, form=None):
1561
""" Return args dict
1563
Twisted already parsed args, including __filename__ hacking,
1564
but did not decoded the values.
1566
return self.decodeArgs(self.twistd.args)
1568
def read(self, n=None):
1569
""" Read from input stream. """
1570
# XXX why is that wrong?:
1571
#rd = self.reactor.callFromThread(self.twistd.read)
1573
# XXX do we need self.reactor.callFromThread with that?
1574
# XXX if yes, why doesn't it work?
1575
self.twistd.content.seek(0, 0)
1577
rd = self.twistd.content.read()
1579
rd = self.twistd.content.read(n)
1580
#print "request.RequestTwisted.read: data=\n" + str(rd)
1583
def write(self, *data):
1584
""" Write to output stream. """
1585
#print "request.RequestTwisted.write: data=\n" + wd
1586
self.reactor.callFromThread(self.twistd.write, self.encode(data))
1589
pass # XXX is there a flush in twisted?
1592
RequestBase.finish(self)
1593
self.reactor.callFromThread(self.twistd.finish)
1595
def open_logs(self):
1597
# create log file for catching stderr output
1598
if not self.opened_logs:
1599
sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at', 0)
1600
self.opened_logs = 1
1602
# Headers ----------------------------------------------------------
1604
def __setHttpHeader(self, header):
1605
if type(header) is unicode:
1606
header = header.encode('ascii')
1607
key, value = header.split(':', 1)
1608
value = value.lstrip()
1609
if key.lower() == 'set-cookie':
1610
key, value = value.split('=', 1)
1611
self.twistd.addCookie(key, value)
1613
self.twistd.setHeader(key, value)
1614
#print "request.RequestTwisted.setHttpHeader: %s" % header
1616
def http_headers(self, more_headers=[]):
1617
if getattr(self, 'sent_headers', None):
1619
self.sent_headers = 1
1623
for header in more_headers + getattr(self, 'user_headers', []):
1624
if header.lower().startswith("content-type:"):
1625
# don't send content-type multiple times!
1626
if have_ct: continue
1628
self.__setHttpHeader(header)
1631
self.__setHttpHeader("Content-type: text/html;charset=%s" % config.charset)
1633
def http_redirect(self, url):
1634
""" Redirect to a fully qualified, or server-rooted URL
1636
@param url: relative or absolute url, ascii using url encoding.
1638
url = self.getQualifiedURL(url)
1639
self.twistd.redirect(url)
1640
# calling finish here will send the rest of the data to the next
1641
# request. leave the finish call to run()
1642
#self.twistd.finish()
1643
raise MoinMoinNoFooter
1645
def setResponseCode(self, code, message=None):
1646
self.twistd.setResponseCode(code, message)
1648
# CLI ------------------------------------------------------------------
1650
class RequestCLI(RequestBase):
1651
""" specialized on command line interface and script requests """
1653
def __init__(self, url='CLI', pagename='', properties={}):
1654
self.saved_cookie = ''
1655
self.path_info = '/' + pagename
1656
self.query_string = ''
1657
self.remote_addr = '127.0.0.1'
1659
self.http_user_agent = 'CLI/Script'
1661
self.request_method = 'GET'
1662
self.request_uri = '/' + pagename # TODO check
1663
self.http_host = 'localhost'
1664
self.http_referer = ''
1665
self.script_name = '.'
1666
self.if_modified_since = None
1667
self.if_none_match = None
1668
RequestBase.__init__(self, properties)
1669
self.cfg.caching_formats = [] # don't spoil the cache
1670
self.initTheme() # usually request.run() does this, but we don't use it
1672
def read(self, n=None):
1673
""" Read from input stream. """
1675
return sys.stdin.read()
1677
return sys.stdin.read(n)
1679
def write(self, *data):
1680
""" Write to output stream. """
1681
sys.stdout.write(self.encode(data))
1687
RequestBase.finish(self)
1688
# flush the output, ignore errors caused by the user closing the socket
1693
if ex.errno != errno.EPIPE: raise
1695
def isForbidden(self):
1696
""" Nothing is forbidden """
1699
# Accessors --------------------------------------------------------
1701
def getQualifiedURL(self, uri=None):
1702
""" Return a full URL starting with schema and host
1704
TODO: does this create correct pages when you render wiki pages
1705
within a cli request?!
1709
# Headers ----------------------------------------------------------
1711
def setHttpHeader(self, header):
1714
def http_headers(self, more_headers=[]):
1717
def http_redirect(self, url):
1718
""" Redirect to a fully qualified, or server-rooted URL
1720
TODO: Does this work for rendering redirect pages?
1722
raise Exception("Redirect not supported for command line tools!")
1725
# StandAlone Server ----------------------------------------------------
1727
class RequestStandAlone(RequestBase):
1728
""" specialized on StandAlone Server (MoinMoin.server.standalone) requests """
1731
def __init__(self, sa, properties={}):
1733
@param sa: stand alone server object
1734
@param properties: ...
1738
self.wfile = sa.wfile
1739
self.rfile = sa.rfile
1740
self.headers = sa.headers
1744
self.http_accept_language = (sa.headers.getheader('accept-language')
1745
or self.http_accept_language)
1746
self.http_user_agent = sa.headers.getheader('user-agent', '')
1747
co = filter(None, sa.headers.getheaders('cookie'))
1748
self.saved_cookie = ', '.join(co) or ''
1749
self.if_modified_since = sa.headers.getheader('if-modified-since')
1750
self.if_none_match = sa.headers.getheader('if-none-match')
1752
# Copy rest from standalone request
1753
self.server_name = sa.server.server_name
1754
self.server_port = str(sa.server.server_port)
1755
self.request_method = sa.command
1756
self.request_uri = sa.path
1757
self.remote_addr = sa.client_address[0]
1759
# Values that need more work
1760
self.path_info, self.query_string = self.splitURI(sa.path)
1761
self.setHttpReferer(sa.headers.getheader('referer'))
1762
self.setHost(sa.headers.getheader('host'))
1763
self.setURL(sa.headers)
1765
##self.debugEnvironment(sa.headers)
1767
RequestBase.__init__(self, properties)
1769
except Exception, err:
1772
def _setup_args_from_cgi_form(self, form=None):
1773
""" Override to create standlone form """
1774
form = cgi.FieldStorage(self.rfile, headers=self.headers, environ={'REQUEST_METHOD': 'POST'})
1775
return RequestBase._setup_args_from_cgi_form(self, form)
1777
def read(self, n=None):
1778
""" Read from input stream
1780
Since self.rfile.read() will block, content-length will be used instead.
1782
TODO: test with n > content length, or when calling several times
1783
with smaller n but total over content length.
1787
n = int(self.headers.get('content-length'))
1788
except (TypeError, ValueError):
1790
warnings.warn("calling request.read() when content-length is "
1791
"not available will block")
1792
return self.rfile.read()
1793
return self.rfile.read(n)
1795
def write(self, *data):
1796
""" Write to output stream. """
1797
self.wfile.write(self.encode(data))
1803
RequestBase.finish(self)
1806
# Headers ----------------------------------------------------------
1808
def http_headers(self, more_headers=[]):
1809
if getattr(self, 'sent_headers', None):
1812
self.sent_headers = 1
1813
user_headers = getattr(self, 'user_headers', [])
1815
# check for status header and send it
1817
for header in more_headers + user_headers:
1818
if header.lower().startswith("status:"):
1820
our_status = int(header.split(':', 1)[1].strip().split(" ", 1)[0])
1823
# there should be only one!
1826
self.sareq.send_response(our_status)
1830
for header in more_headers + user_headers:
1831
if type(header) is unicode:
1832
header = header.encode('ascii')
1833
if header.lower().startswith("content-type:"):
1834
# don't send content-type multiple times!
1835
if have_ct: continue
1838
self.write("%s\r\n" % header)
1841
self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
1845
#from pprint import pformat
1846
#sys.stderr.write(pformat(more_headers))
1847
#sys.stderr.write(pformat(self.user_headers))
1849
# mod_python/Apache ----------------------------------------------------
1851
class RequestModPy(RequestBase):
1852
""" specialized on mod_python requests """
1854
def __init__(self, req):
1855
""" Saves mod_pythons request and sets basic variables using
1856
the req.subprocess_env, cause this provides a standard
1857
way to access the values we need here.
1859
@param req: the mod_python request instance
1862
# flags if headers sent out contained content-type or status
1864
self._have_status = 0
1866
req.add_common_vars()
1868
# some mod_python 2.7.X has no get method for table objects,
1869
# so we make a real dict out of it first.
1870
if not hasattr(req.subprocess_env, 'get'):
1871
env=dict(req.subprocess_env)
1873
env=req.subprocess_env
1874
self._setup_vars_from_std_env(env)
1875
RequestBase.__init__(self)
1877
except Exception, err:
1880
def fixURI(self, env):
1881
""" Fix problems with script_name and path_info using
1882
PythonOption directive to rewrite URI.
1884
This is needed when using Apache 1 or other server which does
1885
not support adding custom headers per request. With mod_python we
1886
can use the PythonOption directive:
1888
<Location /url/to/mywiki/>
1889
PythonOption X-Moin-Location /url/to/mywiki/
1892
Note that *neither* script_name *nor* path_info can be trusted
1893
when Moin is invoked as a mod_python handler with apache1, so we
1894
must build both using request_uri and the provided PythonOption.
1896
# Be compatible with release 1.3.5 "Location" option
1897
# TODO: Remove in later release, we should have one option only.
1898
old_location = 'Location'
1899
options_table = self.mpyreq.get_options()
1900
if not hasattr(options_table, 'get'):
1901
options = dict(options_table)
1903
options = options_table
1904
location = options.get(self.moin_location) or options.get(old_location)
1906
env[self.moin_location] = location
1907
# Try to recreate script_name and path_info from request_uri.
1909
scriptAndPath = urlparse.urlparse(self.request_uri)[2]
1910
self.script_name = location.rstrip('/')
1911
path = scriptAndPath.replace(self.script_name, '', 1)
1912
self.path_info = wikiutil.url_unquote(path, want_unicode=False)
1914
RequestBase.fixURI(self, env)
1916
def _setup_args_from_cgi_form(self, form=None):
1917
""" Override to use mod_python.util.FieldStorage
1919
Its little different from cgi.FieldStorage, so we need to
1920
duplicate the conversion code.
1922
from mod_python import util
1924
form = util.FieldStorage(self.mpyreq)
1927
for key in form.keys():
1931
if not isinstance(values, list):
1936
# Remember filenames with a name hack
1937
if hasattr(item, 'filename') and item.filename:
1938
args[key + '__filename__'] = item.filename
1939
# mod_python 2.7 might return strings instead of Field
1941
if hasattr(item, 'value'):
1943
fixedResult.append(item)
1944
args[key] = fixedResult
1946
return self.decodeArgs(args)
1949
""" mod_python calls this with its request object. We don't
1950
need it cause its already passed to __init__. So ignore
1951
it and just return RequestBase.run.
1953
@param req: the mod_python request instance
1955
return RequestBase.run(self)
1957
def read(self, n=None):
1958
""" Read from input stream. """
1960
return self.mpyreq.read()
1962
return self.mpyreq.read(n)
1964
def write(self, *data):
1965
""" Write to output stream. """
1966
self.mpyreq.write(self.encode(data))
1969
""" We can't flush it, so do nothing. """
1973
""" Just return apache.OK. Status is set in req.status. """
1974
RequestBase.finish(self)
1975
# is it possible that we need to return something else here?
1976
from mod_python import apache
1979
# Headers ----------------------------------------------------------
1981
def setHttpHeader(self, header):
1982
""" Filters out content-type and status to set them directly
1983
in the mod_python request. Rest is put into the headers_out
1984
member of the mod_python request.
1986
@param header: string, containing valid HTTP header.
1988
if type(header) is unicode:
1989
header = header.encode('ascii')
1990
key, value = header.split(':', 1)
1991
value = value.lstrip()
1992
if key.lower() == 'content-type':
1993
# save content-type for http_headers
1994
if not self._have_ct:
1995
# we only use the first content-type!
1996
self.mpyreq.content_type = value
1998
elif key.lower() == 'status':
1999
# save status for finish
2001
self.mpyreq.status = int(value.split(' ', 1)[0])
2005
self._have_status = 1
2007
# this is a header we sent out
2008
self.mpyreq.headers_out[key]=value
2010
def http_headers(self, more_headers=[]):
2011
""" Sends out headers and possibly sets default content-type
2014
@param more_headers: list of strings, defaults to []
2016
for header in more_headers + getattr(self, 'user_headers', []):
2017
self.setHttpHeader(header)
2018
# if we don't had an content-type header, set text/html
2019
if self._have_ct == 0:
2020
self.mpyreq.content_type = "text/html;charset=%s" % config.charset
2021
# if we don't had a status header, set 200
2022
if self._have_status == 0:
2023
self.mpyreq.status = 200
2024
# this is for mod_python 2.7.X, for 3.X it's a NOP
2025
self.mpyreq.send_http_header()
2027
# FastCGI -----------------------------------------------------------
2029
class RequestFastCGI(RequestBase):
2030
""" specialized on FastCGI requests """
2032
def __init__(self, fcgRequest, env, form, properties={}):
2033
""" Initializes variables from FastCGI environment and saves
2034
FastCGI request and form for further use.
2036
@param fcgRequest: the FastCGI request instance.
2037
@param env: environment passed by FastCGI.
2038
@param form: FieldStorage passed by FastCGI.
2041
self.fcgreq = fcgRequest
2044
self._setup_vars_from_std_env(env)
2045
RequestBase.__init__(self, properties)
2047
except Exception, err:
2050
def _setup_args_from_cgi_form(self, form=None):
2051
""" Override to use FastCGI form """
2054
return RequestBase._setup_args_from_cgi_form(self, form)
2056
def open_logs(self):
2057
# create log file for catching stderr output
2058
if not self.opened_logs:
2059
sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at', 0)
2060
self.opened_logs = 1
2062
def read(self, n=None):
2063
""" Read from input stream. """
2065
return self.fcgreq.stdin.read()
2067
return self.fcgreq.stdin.read(n)
2069
def write(self, *data):
2070
""" Write to output stream. """
2071
self.fcgreq.out.write(self.encode(data))
2074
""" Flush output stream. """
2075
self.fcgreq.flush_out()
2078
""" Call finish method of FastCGI request to finish handling of this request. """
2079
RequestBase.finish(self)
2080
self.fcgreq.finish()
2082
# Headers ----------------------------------------------------------
2084
def http_headers(self, more_headers=[]):
2085
""" Send out HTTP headers. Possibly set a default content-type. """
2086
if getattr(self, 'sent_headers', None):
2088
self.sent_headers = 1
2092
for header in more_headers + getattr(self, 'user_headers', []):
2093
if type(header) is unicode:
2094
header = header.encode('ascii')
2095
if header.lower().startswith("content-type:"):
2096
# don't send content-type multiple times!
2097
if have_ct: continue
2099
self.write("%s\r\n" % header)
2102
self.write("Content-type: text/html;charset=%s\r\n" % config.charset)
2106
#from pprint import pformat
2107
#sys.stderr.write(pformat(more_headers))
2108
#sys.stderr.write(pformat(self.user_headers))
2110
def open_logs(self):
2111
# create log file for catching stderr output
2112
if not self.opened_logs:
2113
sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at')
2114
if self.cfg.log_timing:
2115
self.timing_logfile = open(os.path.join(self.cfg.data_dir, 'timing.log'), 'at')
2116
self.opened_logs = 1
2118
# WSGI --------------------------------------------------------------
2120
class RequestWSGI(RequestBase):
2121
def __init__(self, env):
2124
self.hasContentType = False
2126
self.stdin = env['wsgi.input']
2127
self.stdout = StringIO.StringIO()
2129
self.status = '200 OK'
2132
self._setup_vars_from_std_env(env)
2133
RequestBase.__init__(self, {})
2135
except Exception, err:
2138
def setup_args(self, form=None):
2140
form = cgi.FieldStorage(fp=self.stdin, environ=self.env, keep_blank_values=1)
2141
return self._setup_args_from_cgi_form(form)
2143
def read(self, n=None):
2145
return self.stdin.read()
2147
return self.stdin.read(n)
2149
def write(self, *data):
2150
self.stdout.write(self.encode(data))
2152
def reset_output(self):
2153
self.stdout = StringIO.StringIO()
2155
def setHttpHeader(self, header):
2156
if type(header) is unicode:
2157
header = header.encode('ascii')
2159
key, value = header.split(':', 1)
2160
value = value.lstrip()
2161
if key.lower() == 'content-type':
2162
# save content-type for http_headers
2163
if self.hasContentType:
2164
# we only use the first content-type!
2167
self.hasContentType = True
2169
elif key.lower() == 'status':
2170
# save status for finish
2174
self.headers.append((key, value))
2176
def http_headers(self, more_headers=[]):
2177
for header in more_headers:
2178
self.setHttpHeader(header)
2180
if not self.hasContentType:
2181
self.headers.insert(0, ('Content-Type', 'text/html;charset=%s' % config.charset))
2190
return self.stdout.getvalue()