Mercurial > moin > 1.9
changeset 4329:d9d151ce2f6f
merged moin/1.8-wsgi-fkrupicka
author | Thomas Waldmann <tw AT waldmann-edv DOT de> |
---|---|
date | Tue, 23 Sep 2008 23:02:50 +0200 |
parents | 99ed52b53e4e (current diff) 7eb12d1f0856 (diff) |
children | b6d5bf04b9e6 |
files | MoinMoin/failure.py MoinMoin/request/__init__.py MoinMoin/request/_tests/test_request.py MoinMoin/request/request_cgi.py MoinMoin/request/request_cli.py MoinMoin/request/request_fcgi.py MoinMoin/request/request_modpython.py MoinMoin/request/request_standalone.py MoinMoin/request/request_twisted.py MoinMoin/request/request_wsgi.py MoinMoin/server/__init__.py MoinMoin/server/daemon.py MoinMoin/server/server_cgi.py MoinMoin/server/server_fastcgi.py MoinMoin/server/server_modpython.py MoinMoin/server/server_standalone.py MoinMoin/server/server_twisted.py MoinMoin/server/server_wsgi.py MoinMoin/support/cgitb.py MoinMoin/support/thfcgi.py wiki/server/moin.fcg wiki/server/moin_flup_wsgi.py wiki/server/moinmodpy.htaccess wiki/server/moinmodpy.py wiki/server/mointwisted wiki/server/mointwisted.cmd wiki/server/mointwisted.py wiki/server/wikiserver.py |
diffstat | 231 files changed, 22267 insertions(+), 6715 deletions(-) [+] |
line wrap: on
line diff
--- a/MANIFEST.in Tue Sep 23 00:44:45 2008 +0200 +++ b/MANIFEST.in Tue Sep 23 23:02:50 2008 +0200 @@ -13,6 +13,9 @@ # include stuff for translators recursive-include MoinMoin/i18n * +# include non-py stuff from werkzeug +recursive-include MoinMoin/support/werkzeug/debug * + # contrib stuff recursive-include contrib *
--- a/MoinMoin/Page.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/Page.py Tue Sep 23 23:02:50 2008 +0200 @@ -109,7 +109,8 @@ (for 'meta') or the complete cache ('pagelists'). @param request: the request object """ - elog = request.editlog + from MoinMoin.logfile import editlog + elog = editlog.EditLog(request) old_pos = self.log_pos new_pos, items = elog.news(old_pos) if items: @@ -753,12 +754,13 @@ url = "%s/%s/%s" % (request.cfg.url_prefix_action, action, url) url = '%s?%s' % (url, querystr) + if not relative: + url = '%s/%s' % (request.script_root, url) + # Add anchor if anchor: url = "%s#%s" % (url, wikiutil.url_quote_plus(anchor)) - if not relative: - url = '%s/%s' % (request.getScriptname(), url) return url def link_to_raw(self, request, text, querystr=None, anchor=None, **kw): @@ -963,26 +965,25 @@ offer a dialogue to save it to disk (used by Save action). """ request = self.request - request.setHttpHeader("Content-type: text/plain; charset=%s" % config.charset) + request.mimetype = 'text/plain' if self.exists(): # use the correct last-modified value from the on-disk file # to ensure cacheability where supported. Because we are sending # RAW (file) content, the file mtime is correct as Last-Modified header. - request.setHttpHeader("Status: 200 OK") - request.setHttpHeader("Last-Modified: %s" % util.timefuncs.formathttpdate(os.path.getmtime(self._text_filename()))) + request.status_code = 200 + request.last_modified = os.path.getmtime(self._text_filename()) text = self.encodeTextMimeType(self.body) #request.setHttpHeader("Content-Length: %d" % len(text)) # XXX WRONG! text is unicode obj, but we send utf-8! if content_disposition: # TODO: fix the encoding here, plain 8 bit is not allowed according to the RFCs # There is no solution that is compatible to IE except stripping non-ascii chars filename_enc = "%s.txt" % self.page_name.encode(config.charset) - request.setHttpHeader('Content-Disposition: %s; filename="%s"' % ( - content_disposition, filename_enc)) + dispo_string = '%s; filename="%s"' % (content_disposition, filename_enc) + request.headers.add('Content-Disposition', dispo_string) else: - request.setHttpHeader('Status: 404 NOTFOUND') + request.status_code = 404 text = u"Page %s not found." % self.page_name - request.emit_http_headers() request.write(text) def send_page(self, **keywords): @@ -1007,7 +1008,7 @@ send_special = keywords.get('send_special', False) print_mode = keywords.get('print_mode', 0) if print_mode: - media = 'media' in request.form and request.form['media'][0] or 'print' + media = request.form.get('media', 'print') else: media = 'screen' self.hilite_re = (keywords.get('hilite_re') or @@ -1026,10 +1027,7 @@ # redirect to another page # note that by including "action=show", we prevent endless looping # (see code in "request") or any cascaded redirection - request.http_redirect('%s/%s?action=show&redirect=%s' % ( - request.getScriptname(), - wikiutil.quoteWikinameURL(pi['redirect']), - wikiutil.url_quote_plus(self.page_name, ''), )) + request.http_redirect(request.href(pi['redirect'], action='show', redirect=self.page_name)) return # if necessary, load the formatter @@ -1076,12 +1074,12 @@ page_exists = self.exists() if not content_only: if emit_headers: - request.setHttpHeader("Content-Type: %s; charset=%s" % (self.output_mimetype, self.output_charset)) + request.content_type = "%s; charset=%s" % (self.output_mimetype, self.output_charset) if page_exists: if not request.user.may.read(self.page_name): - request.setHttpHeader('Status: 403 Permission Denied') + request.status_code = 403 else: - request.setHttpHeader('Status: 200 OK') + request.status_code = 200 if not request.cacheable: # use "nocache" headers if we're using a method that is not simply "display" request.disableHttpCaching(level=2) @@ -1096,8 +1094,7 @@ #request.setHttpHeader("Last-Modified: %s" % util.timefuncs.formathttpdate(lastmod)) pass else: - request.setHttpHeader('Status: 404 NOTFOUND') - request.emit_http_headers() + request.status_code = 404 if not page_exists and self.request.isSpiderAgent: # don't send any 404 content to bots @@ -1117,7 +1114,7 @@ # This redirect message is very annoying. # Less annoying now without the warning sign. if 'redirect' in request.form: - redir = request.form['redirect'][0] + redir = request.form['redirect'] request.theme.add_msg('<strong>%s</strong><br>' % ( _('Redirected from page "%(page)s"') % {'page': wikiutil.link_tag(request, wikiutil.quoteWikinameURL(redir) + "?action=show", self.formatter.text(redir))}), "info")
--- a/MoinMoin/PageEditor.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/PageEditor.py Tue Sep 23 23:02:50 2008 +0200 @@ -197,12 +197,11 @@ # Emmit http_headers after checks (send_page) request.disableHttpCaching(level=2) - request.emit_http_headers() # check if we want to load a draft use_draft = None if 'button_load_draft' in form: - wanted_draft_timestamp = int(form.get('draft_ts', ['0'])[0]) + wanted_draft_timestamp = int(form.get('draft_ts', '0')) if wanted_draft_timestamp: draft = self._load_draft() if draft is not None: @@ -214,7 +213,7 @@ if use_draft is not None: title = _('Draft of "%(pagename)s"') # Propagate original revision - rev = int(form['draft_rev'][0]) + rev = int(form['draft_rev']) self.set_raw_body(use_draft, modified=1) preview = use_draft elif preview is None: @@ -234,7 +233,7 @@ # get request parameters try: - text_rows = int(form['rows'][0]) + text_rows = int(form['rows']) except StandardError: text_rows = self.cfg.edit_rows if request.user.valid: @@ -278,7 +277,7 @@ raw_body = self.get_raw_body() elif 'template' in form: # If the page does not exist, we try to get the content from the template parameter. - template_page = wikiutil.unquoteWikiname(form['template'][0]) + template_page = wikiutil.unquoteWikiname(form['template']) if request.user.may.read(template_page): raw_body = Page(request, template_page).get_raw_body() if raw_body: @@ -332,10 +331,9 @@ raw_body = _('Describe %s here.') % (self.page_name, ) # send form - request.write('<form id="editor" method="post" action="%s/%s#preview" onSubmit="flgChange = false;">' % ( - request.getScriptname(), - wikiutil.quoteWikinameURL(self.page_name), - )) + request.write('<form id="editor" method="post" action="%s#preview" onSubmit="flgChange = false;">' % ( + request.href(self.page_name) + )) # yet another weird workaround for broken IE6 (it expands the text # editor area to the right after you begin to type...). IE sucks... @@ -351,7 +349,7 @@ request.write('<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket(request)) # Save backto in a hidden input - backto = form.get('backto', [None])[0] + backto = form.get('backto') if backto: request.write(unicode(html.INPUT(type="hidden", name="backto", value=backto))) @@ -410,7 +408,7 @@ document.write('<label for="chktrivialtop">%(label)s</label>'); //--> </script> ''' % { - 'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'], + 'checked': ('', 'checked')[form.get('trivial', '0') == '1'], 'label': _("Trivial change"), }) @@ -460,7 +458,7 @@ <label for="chktrivial">%(label)s</label> ''' % { - 'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'], + 'checked': ('', 'checked')[form.get('trivial', '0') == '1'], 'label': _("Trivial change"), }) @@ -469,7 +467,7 @@ <input type="checkbox" name="rstrip" id="chkrstrip" value="1" %(checked)s> <label for="chkrstrip">%(label)s</label> ''' % { - 'checked': ('', 'checked')[form.get('rstrip', ['0'])[0] == '1'], + 'checked': ('', 'checked')[form.get('rstrip', '0') == '1'], 'label': _('Remove trailing whitespace from each line') }) request.write("</p>") @@ -514,7 +512,7 @@ self._save_draft(newtext, rev) # shall we really save a draft on CANCEL? self.lock.release() - backto = request.form.get('backto', [None])[0] + backto = request.form.get('backto') if backto: pg = Page(request, backto) request.http_redirect(pg.url(request, relative=False)) @@ -672,7 +670,7 @@ # Remove cache entry (if exists) pg = Page(self.request, self.page_name) - key = self.request.form.get('key', ['text_html'])[0] + key = self.request.form.get('key', 'text_html') caching.CacheEntry(self.request, pg, key, scope='item').remove() caching.CacheEntry(self.request, pg, "pagelinks", scope='item').remove()
--- a/MoinMoin/PageGraphicalEditor.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/PageGraphicalEditor.py Tue Sep 23 23:02:50 2008 +0200 @@ -52,7 +52,6 @@ form = request.form _ = self._ request.disableHttpCaching(level=2) - request.emit_http_headers() raw_body = '' msg = None @@ -90,7 +89,7 @@ # check if we want to load a draft use_draft = None if 'button_load_draft' in form: - wanted_draft_timestamp = int(form.get('draft_ts', ['0'])[0]) + wanted_draft_timestamp = int(form.get('draft_ts', '0')) if wanted_draft_timestamp: draft = self._load_draft() if draft is not None: @@ -102,7 +101,7 @@ if use_draft is not None: title = _('Draft of "%(pagename)s"') # Propagate original revision - rev = int(form['draft_rev'][0]) + rev = int(form['draft_rev']) self.set_raw_body(use_draft, modified=1) preview = use_draft elif preview is None: @@ -122,7 +121,7 @@ # get request parameters try: - text_rows = int(form['rows'][0]) + text_rows = int(form['rows']) except StandardError: text_rows = self.cfg.edit_rows if request.user.valid: @@ -168,7 +167,7 @@ raw_body = self.get_raw_body() elif 'template' in form: # If the page does not exist, we try to get the content from the template parameter. - template_page = wikiutil.unquoteWikiname(form['template'][0]) + template_page = wikiutil.unquoteWikiname(form['template']) if request.user.may.read(template_page): raw_body = Page(request, template_page).get_raw_body() if raw_body: @@ -222,9 +221,8 @@ raw_body = _('Describe %s here.') % (self.page_name, ) # send form - request.write('<form id="editor" method="post" action="%s/%s#preview">' % ( - request.getScriptname(), - wikiutil.quoteWikinameURL(self.page_name), + request.write('<form id="editor" method="post" action="%s#preview">' % ( + request.href(self.page_name) )) # yet another weird workaround for broken IE6 (it expands the text @@ -245,7 +243,7 @@ request.write('<input type="hidden" name="ticket" value="%s">' % wikiutil.createTicket(request)) # Save backto in a hidden input - backto = form.get('backto', [None])[0] + backto = form.get('backto') if backto: request.write(unicode(html.INPUT(type="hidden", name="backto", value=backto))) @@ -296,7 +294,7 @@ <input type="checkbox" name="trivial" id="chktrivialtop" value="1" %(checked)s onclick="toggle_trivial(this)"> <label for="chktrivialtop">%(label)s</label> ''' % { - 'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'], + 'checked': ('', 'checked')[form.get('trivial', '0') == '1'], 'label': _("Trivial change"), }) @@ -313,7 +311,7 @@ url_prefix_local = request.cfg.url_prefix_local wikipage = wikiutil.quoteWikinameURL(self.page_name) fckbasepath = url_prefix_local + '/applets/FCKeditor' - wikiurl = request.getScriptname() + wikiurl = request.script_root if not wikiurl or wikiurl[-1] != '/': wikiurl += '/' themepath = '%s/%s' % (url_prefix_static, request.theme.name) @@ -375,7 +373,7 @@ <input type="checkbox" name="trivial" id="chktrivial" value="1" %(checked)s onclick="toggle_trivial(this)"> <label for="chktrivial">%(label)s</label> ''' % { - 'checked': ('', 'checked')[form.get('trivial', ['0'])[0] == '1'], + 'checked': ('', 'checked')[form.get('trivial', '0') == '1'], 'label': _("Trivial change"), }) @@ -384,7 +382,7 @@ <input type="checkbox" name="rstrip" id="chkrstrip" value="1" %(checked)s> <label for="chkrstrip">%(label)s</label> </p> ''' % { - 'checked': ('', 'checked')[form.get('rstrip', ['0'])[0] == '1'], + 'checked': ('', 'checked')[form.get('rstrip', '0') == '1'], 'label': _('Remove trailing whitespace from each line') })
--- a/MoinMoin/_tests/ldap_testbase.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/_tests/ldap_testbase.py Tue Sep 23 23:02:50 2008 +0200 @@ -41,7 +41,7 @@ SLAPD_EXECUTABLE = 'slapd' # filename of LDAP server executable - if it is not # in your PATH, you have to give full path/filename. -import os, shutil, tempfile, time +import os, shutil, tempfile, time, base64, md5 from StringIO import StringIO import signal @@ -187,6 +187,8 @@ f.write(db_config) f.close() + rootpw = '{MD5}' + base64.b64encode(md5.new(self.rootpw).digest()) + # create slapd.conf from content template in slapd_config slapd_config = slapd_config % { 'ldap_dir': self.ldap_dir, @@ -194,7 +196,7 @@ 'schema_dir': self.schema_dir, 'basedn': self.basedn, 'rootdn': self.rootdn, - 'rootpw': self.rootpw, + 'rootpw': rootpw, } if isinstance(slapd_config, unicode): slapd_config = slapd_config.encode(self.coding)
--- a/MoinMoin/_tests/ldap_testdata.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/_tests/ldap_testdata.py Tue Sep 23 23:02:50 2008 +0200 @@ -78,14 +78,16 @@ objectClass: account objectClass: simpleSecurityObject uid: usera -userPassword: usera +# this is md5 encoded 'usera' for password +userPassword: {MD5}aXqgOSc5gSW7YoLi9BSmvg== dn: uid=userb,ou=Unit B,ou=Users,ou=testing,dc=example,dc=org cn: Vorname Nachname objectClass: inetOrgPerson sn: Nachname uid: userb -userPassword: userb +# this is md5 encoded 'userb' for password +userPassword: {MD5}ThvfQsM7OQFjqSUQOX2XsA== dn: cn=Group A,ou=Groups,ou=testing,dc=example,dc=org cn: Group A
--- a/MoinMoin/_tests/test_sourcecode.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/_tests/test_sourcecode.py Tue Sep 23 23:02:50 2008 +0200 @@ -19,6 +19,7 @@ EXCLUDE = [ '/contrib/DesktopEdition/setup_py2exe.py', # has crlf '/contrib/TWikiDrawPlugin', # 3rd party java stuff + '/contrib/flup-server', # 3rd party WSGI adapters '/MoinMoin/support', # 3rd party libs or non-broken stdlib stuff '/wiki/htdocs/applets/FCKeditor', # 3rd party GUI editor '/tests/wiki', # this is our test wiki
--- a/MoinMoin/_tests/test_user.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/_tests/test_user.py Tue Sep 23 23:02:50 2008 +0200 @@ -38,11 +38,11 @@ def setup_method(self, method): # Save original user and cookie - self.saved_cookie = self.request.saved_cookie + self.saved_cookie = self.request.cookies self.saved_user = self.request.user # Create anon user for the tests - self.request.saved_cookie = '' + self.request.cookies = {} self.request.user = user.User(self.request) # Prevent user list caching - we create and delete users too fast for that. @@ -64,7 +64,7 @@ del self.user # Restore original user - self.request.saved_cookie = self.saved_cookie + self.request.cookies = self.saved_cookie self.request.user = self.saved_user # Remove user name to id cache, or next test will fail
--- a/MoinMoin/_tests/test_wikiutil.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/_tests/test_wikiutil.py Tue Sep 23 23:02:50 2008 +0200 @@ -9,7 +9,7 @@ import py -from MoinMoin import wikiutil +from MoinMoin import config, wikiutil class TestQueryStringSupport: @@ -969,4 +969,80 @@ assert relative_page == wikiutil.RelPageName(current_page, absolute_page) +class TestNormalizePagename(object): + + def testPageInvalidChars(self): + """ request: normalize pagename: remove invalid unicode chars + + Assume the default setting + """ + test = u'\u0000\u202a\u202b\u202c\u202d\u202e' + expected = u'' + result = wikiutil.normalize_pagename(test, self.request.cfg) + assert result == expected + + def testNormalizeSlashes(self): + """ request: normalize pagename: normalize slashes """ + cases = ( + (u'/////', u''), + (u'/a', u'a'), + (u'a/', u'a'), + (u'a/////b/////c', u'a/b/c'), + (u'a b/////c d/////e f', u'a b/c d/e f'), + ) + for test, expected in cases: + result = wikiutil.normalize_pagename(test, self.request.cfg) + assert result == expected + + def testNormalizeWhitespace(self): + """ request: normalize pagename: normalize whitespace """ + cases = ( + (u' ', u''), + (u' a', u'a'), + (u'a ', u'a'), + (u'a b c', u'a b c'), + (u'a b / c d / e f', u'a b/c d/e f'), + # All 30 unicode spaces + (config.chars_spaces, u''), + ) + for test, expected in cases: + result = wikiutil.normalize_pagename(test, self.request.cfg) + assert result == expected + + def testUnderscoreTestCase(self): + """ request: normalize pagename: underscore convert to spaces and normalized + + Underscores should convert to spaces, then spaces should be + normalized, order is important! + """ + cases = ( + (u' ', u''), + (u' a', u'a'), + (u'a ', u'a'), + (u'a b c', u'a b c'), + (u'a b / c d / e f', u'a b/c d/e f'), + ) + for test, expected in cases: + result = wikiutil.normalize_pagename(test, self.request.cfg) + assert result == expected + +class TestGroupPages(object): + + def testNormalizeGroupName(self): + """ request: normalize pagename: restrict groups to alpha numeric Unicode + + Spaces should normalize after invalid chars removed! + """ + cases = ( + # current acl chars + (u'Name,:Group', u'NameGroup'), + # remove than normalize spaces + (u'Name ! @ # $ % ^ & * ( ) + Group', u'Name Group'), + ) + for test, expected in cases: + # validate we are testing valid group names + if wikiutil.isGroupPage(test, self.request.cfg): + result = wikiutil.normalize_pagename(test, self.request.cfg) + assert result == expected + coverage_modules = ['MoinMoin.wikiutil']
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/_tests/test_wsgiapp.py Tue Sep 23 23:02:50 2008 +0200 @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +""" + MoinMoin - MoinMoin.wsgiapp Tests + + @copyright: 2008 MoinMoin:FlorianKrupicka + @license: GNU GPL, see COPYING for details. +""" +from StringIO import StringIO + +from MoinMoin import wsgiapp + +DOC_TYPE = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">' + +class TestApplication: + + # These should exist + PAGES = ('FrontPage', 'RecentChanges', 'HelpContents', 'FindPage') + # ... and these should not + NO_PAGES = ('FooBar', 'TheNone/ExistantPage/', '%33Strange%74Codes') + + def testWSGIAppExisting(self): + for page in self.PAGES: + def _test_(): + appiter, status, headers = self.client.get('/%s' % page) + print repr(list(appiter)) + assert status[:3] == '200' + assert ('Content-Type', 'text/html; charset=utf-8') in headers + output = ''.join(appiter) + for needle in (DOC_TYPE, page): + assert needle in output + yield _test_ + + def testWSGIAppAbsent(self): + for page in self.NO_PAGES: + def _test_(): + appiter, status, headers = self.client.get('/%s' % page) + assert status[:3] == '404' + output = ''.join(appiter) + for needle in ('new empty page', 'page template'): + assert needle in output + yield _test_
--- a/MoinMoin/action/AttachFile.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/AttachFile.py Tue Sep 23 23:02:50 2008 +0200 @@ -27,7 +27,7 @@ @license: GNU GPL, see COPYING for details. """ -import os, time, zipfile, mimetypes, errno +import os, time, zipfile, mimetypes, errno, datetime from MoinMoin import log logging = log.getLogger(__name__) @@ -76,34 +76,20 @@ else: return u"/".join(pieces[:-1]), pieces[-1] - -def attachUrl(request, pagename, filename=None, **kw): - # filename is not used yet, but should be used later to make a sub-item url - if kw: - qs = '?%s' % wikiutil.makeQueryString(kw, want_unicode=False) - else: - qs = '' - return "%s/%s%s" % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), qs) - - def getAttachUrl(pagename, filename, request, addts=0, escaped=0, do='get', drawing='', upload=False): """ Get URL that points to attachment `filename` of page `pagename`. """ if upload: if not drawing: - url = attachUrl(request, pagename, filename, - rename=wikiutil.taintfilename(filename), action=action_name) + url = request.href(pagename, rename=wikiutil.taintfilename(filename), + action=action_name) else: - url = attachUrl(request, pagename, filename, - rename=wikiutil.taintfilename(filename), drawing=drawing, action=action_name) + url = request.href(pagename, rename=wikiutil.taintfilename(filename), + drawing=drawing, action=action_name) else: if not drawing: - url = attachUrl(request, pagename, filename, - target=filename, action=action_name, do=do) + url = request.href(pagename, target=filename, action=action_name, do=do) else: - url = attachUrl(request, pagename, filename, - drawing=drawing, action=action_name) - if escaped: - url = wikiutil.escape(url) + url = request.href(pagename, drawing=drawing, action=action_name) return url @@ -123,7 +109,7 @@ fmt = request.formatter attach_count = _('[%d attachments]') % len(files) attach_icon = request.theme.make_icon('attach', vars={'attach_count': attach_count}) - attach_link = (fmt.url(1, attachUrl(request, pagename, action=action_name), rel='nofollow') + + attach_link = (fmt.url(1, request.href(pagename, action=action_name), rel='nofollow') + attach_icon + fmt.url(0)) return attach_link @@ -251,10 +237,10 @@ _ = request.getText error = None - if not request.form.get('target', [''])[0]: + if not request.values.get('target'): error = _("Filename of attachment not specified!") else: - filename = wikiutil.taintfilename(request.form['target'][0]) + filename = wikiutil.taintfilename(request.values['target']) fpath = getFilename(request, pagename, filename) if os.path.isfile(fpath): @@ -408,14 +394,14 @@ now = time.time() pubpath = request.cfg.url_prefix_static + "/applets/TWikiDrawPlugin" - basename = request.form['drawing'][0] + basename = request.form['drawing'] drawpath = getAttachUrl(pagename, basename + '.draw', request, escaped=1) pngpath = getAttachUrl(pagename, basename + '.png', request, escaped=1) - pagelink = attachUrl(request, pagename, '', action=action_name, ts=now) + pagelink = request.href(pagename, action=action_name, ts=now) helplink = Page(request, "HelpOnActions/AttachFile").url(request) - savelink = attachUrl(request, pagename, '', action=action_name, do='savedrawing') + savelink = request.href(pagename, action=action_name, do='savedrawing') #savelink = Page(request, pagename).url(request) # XXX include target filename param here for twisted - # request, {'savename': request.form['drawing'][0]+'.draw'} + # request, {'savename': request.form['drawing']+'.draw'} #savelink = '/cgi-bin/dumpform.bat' timestamp = '&ts=%s' % now @@ -460,7 +446,7 @@ if writeable: request.write('<h2>' + _("New Attachment") + '</h2>') request.write(""" -<form action="%(baseurl)s/%(pagename)s" method="POST" enctype="multipart/form-data"> +<form action="%(url)s" method="POST" enctype="multipart/form-data"> <dl> <dt>%(upload_label_file)s</dt> <dd><input type="file" name="file" size="50"></dd> @@ -477,14 +463,13 @@ </p> </form> """ % { - 'baseurl': request.getScriptname(), - 'pagename': wikiutil.quoteWikinameURL(pagename), + 'url': request.href(pagename), 'action_name': action_name, 'upload_label_file': _('File to upload'), 'upload_label_rename': _('Rename to'), - 'rename': request.form.get('rename', [''])[0], + 'rename': request.form.get('rename', ''), 'upload_label_overwrite': _('Overwrite existing attachment of same name'), - 'overwrite_checked': ('', 'checked')[request.form.get('overwrite', ['0'])[0] == '1'], + 'overwrite_checked': ('', 'checked')[request.form.get('overwrite', '0') == '1'], 'upload_button': _('Upload'), 'textcha': TextCha(request).render(), }) @@ -495,7 +480,7 @@ if not writeable: request.write('<p>%s</p>' % _('You are not allowed to attach a file to this page.')) - if writeable and request.form.get('drawing', [None])[0]: + if writeable and request.form.get('drawing'): send_hotdraw(pagename, request) @@ -507,12 +492,12 @@ """ Main dispatcher for the 'AttachFile' action. """ _ = request.getText - do = request.form.get('do', ['upload_form']) - handler = globals().get('_do_%s' % do[0]) + do = request.values.get('do', 'upload_form') + handler = globals().get('_do_%s' % do) if handler: msg = handler(pagename, request) else: - msg = _('Unsupported AttachFile sub-action: %s') % (wikiutil.escape(do[0]), ) + msg = _('Unsupported AttachFile sub-action: %s') % wikiutil.escape(do) if msg: error_msg(pagename, request, msg) @@ -524,7 +509,6 @@ def upload_form(pagename, request, msg=''): _ = request.getText - request.emit_http_headers() # Use user interface language for this generated page request.setContentLanguage(request.lang) request.theme.add_msg(msg, "dialog") @@ -555,9 +539,15 @@ return _('TextCha: Wrong answer! Go back and try again...') form = request.form - overwrite = form.get('overwrite', [u'0'])[0] + + file_upload = request.files.get('file') + if not file_upload: + # This might happen when trying to upload file names + # with non-ascii characters on Safari. + return _("No file content. Delete non ASCII characters from the file name and try again.") + try: - overwrite = int(overwrite) + overwrite = int(form.get('overwrite', '0')) except: overwrite = 0 @@ -567,35 +557,26 @@ if overwrite and not request.user.may.delete(pagename): return _('You are not allowed to overwrite a file attachment of this page.') - filename = form.get('file__filename__') - rename = form.get('rename', [u''])[0].strip() + rename = form.get('rename', u'').strip() if rename: target = rename else: - target = filename + target = file_upload.filename - target = preprocess_filename(target) target = wikiutil.clean_input(target) if not target: return _("Filename of attachment not specified!") - # get file content - filecontent = request.form.get('file', [None])[0] - if filecontent is None: - # This might happen when trying to upload file names - # with non-ascii characters on Safari. - return _("No file content. Delete non ASCII characters from the file name and try again.") - # add the attachment try: - target, bytes = add_attachment(request, pagename, target, filecontent, overwrite=overwrite) + target, bytes = add_attachment(request, pagename, target, file_upload.stream, overwrite=overwrite) msg = _("Attachment '%(target)s' (remote name '%(filename)s')" " with %(bytes)d bytes saved.") % { - 'target': target, 'filename': filename, 'bytes': bytes} + 'target': target, 'filename': file_upload.filename, 'bytes': bytes} except AttachmentAlreadyExists: msg = _("Attachment '%(target)s' (remote name '%(filename)s') already exists.") % { - 'target': target, 'filename': filename} + 'target': target, 'filename': file_upload.filename} # return attachment list upload_form(pagename, request, msg) @@ -607,8 +588,8 @@ if not request.user.may.write(pagename): return _('You are not allowed to save a drawing on this page.') - filename = request.form['filename'][0] - filecontent = request.form['filepath'][0] + filename = request.form['filename'] + filecontent = request.form['filepath'] basepath, basename = os.path.split(filename) basename, ext = os.path.splitext(basename) @@ -644,7 +625,6 @@ if ext == '.map': os.utime(attach_dir, None) - request.emit_http_headers() request.write("OK") @@ -710,17 +690,17 @@ if 'cancel' in request.form: return _('Move aborted!') - if not wikiutil.checkTicket(request, request.form['ticket'][0]): + if not wikiutil.checkTicket(request, request.form['ticket']): return _('Please use the interactive user interface to move attachments!') if not request.user.may.delete(pagename): return _('You are not allowed to move attachments from this page.') if 'newpagename' in request.form: - new_pagename = request.form.get('newpagename')[0] + new_pagename = request.form.get('newpagename') else: upload_form(pagename, request, msg=_("Move aborted because new page name is empty.")) if 'newattachmentname' in request.form: - new_attachment = request.form.get('newattachmentname')[0] + new_attachment = request.form.get('newattachmentname') if new_attachment != wikiutil.taintfilename(new_attachment): upload_form(pagename, request, msg=_("Please use a valid filename for attachment '%(filename)s'.") % { 'filename': new_attachment}) @@ -728,7 +708,7 @@ else: upload_form(pagename, request, msg=_("Move aborted because new attachment name is empty.")) - attachment = request.form.get('oldattachmentname')[0] + attachment = request.form.get('oldattachmentname') move_file(request, pagename, new_pagename, attachment, new_attachment) @@ -743,11 +723,10 @@ # move file d = {'action': action_name, - 'baseurl': request.getScriptname(), + 'url': request.href(pagename), 'do': 'attachment_move', 'ticket': wikiutil.createTicket(request), 'pagename': pagename, - 'pagename_quoted': wikiutil.quoteWikinameURL(pagename), 'attachment_name': filename, 'move': _('Move'), 'cancel': _('Cancel'), @@ -755,7 +734,7 @@ 'attachment_label': _("New attachment name"), } formhtml = ''' -<form action="%(baseurl)s/%(pagename_quoted)s" method="POST"> +<form action="%(url)s" method="POST"> <input type="hidden" name="action" value="%(action)s"> <input type="hidden" name="do" value="%(do)s"> <input type="hidden" name="ticket" value="%(ticket)s"> @@ -796,9 +775,10 @@ if not filename: return # error msg already sent in _access_file - timestamp = timefuncs.formathttpdate(int(os.path.getmtime(fpath))) - if request.if_modified_since == timestamp: - request.emit_http_headers(["Status: 304 Not modified"]) + timestamp = datetime.datetime.fromtimestamp(os.path.getmtime(fpath)) + if_modified = request.if_modified_since + if if_modified and if_modified >= timestamp: + request.status_code = 304 else: mt = wikiutil.MimeType(filename=filename) content_type = mt.content_type() @@ -814,12 +794,11 @@ dangerous = mime_type in request.cfg.mimetypes_xss_protect content_dispo = dangerous and 'attachment' or 'inline' - request.emit_http_headers([ - 'Content-Type: %s' % content_type, - 'Last-Modified: %s' % timestamp, - 'Content-Length: %d' % os.path.getsize(fpath), - 'Content-Disposition: %s; filename="%s"' % (content_dispo, filename_enc), - ]) + request.content_type = content_type + request.last_modified = timestamp + request.content_length = os.path.getsize(fpath) + content_dispo_string = '%s; filename="%s"' % (content_dispo, filename_enc) + request.headers.add('Content-Disposition', content_dispo_string) # send data request.send_file(open(fpath, 'rb')) @@ -1053,7 +1032,6 @@ return # send header & title - request.emit_http_headers() # Use user interface language for this generated page request.setContentLanguage(request.lang) title = _('attachment:%(filename)s of %(pagename)s') % {
--- a/MoinMoin/action/CopyPage.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/CopyPage.py Tue Sep 23 23:02:50 2008 +0200 @@ -46,20 +46,19 @@ """ copy this page to "pagename" """ _ = self._ form = self.form - newpagename = form.get('newpagename', [u''])[0] - newpagename = self.request.normalizePagename(newpagename) - comment = form.get('comment', [u''])[0] + newpagename = form.get('newpagename', u'') + newpagename = wikiutil.normalize_pagename(newpagename, self.cfg) + comment = form.get('comment', u'') comment = wikiutil.clean_input(comment) self.page = PageEditor(self.request, self.pagename) success, msgs = self.page.copyPage(newpagename, comment) copy_subpages = 0 - if form.has_key('copy_subpages'): - try: - copy_subpages = int(form['copy_subpages'][0]) - except: - pass + try: + copy_subpages = int(form['copy_subpages']) + except: + pass if copy_subpages and self.subpages or (not self.users_subpages and self.subpages): for name in self.subpages: @@ -85,7 +84,7 @@ d = { 'subpage': subpages, - 'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', ['0'])[0] == '1'], + 'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', '0') == '1'], 'subpage_label': _('Copy all /subpages too?'), 'pagename': wikiutil.escape(self.pagename, True), 'newname_label': _("New name"),
--- a/MoinMoin/action/DeletePage.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/DeletePage.py Tue Sep 23 23:02:50 2008 +0200 @@ -44,7 +44,7 @@ def do_action(self): """ Delete pagename """ form = self.form - comment = form.get('comment', [u''])[0] + comment = form.get('comment', u'') comment = wikiutil.clean_input(comment) # Create a page editor that does not do editor backups, because @@ -53,11 +53,10 @@ success, msgs = self.page.deletePage(comment) delete_subpages = 0 - if 'delete_subpages' in form: - try: - delete_subpages = int(form['delete_subpages'][0]) - except: - pass + try: + delete_subpages = int(form['delete_subpages']) + except: + pass if delete_subpages and self.subpages: for name in self.subpages: @@ -75,7 +74,7 @@ d = { 'subpage': subpages, - 'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', ['0'])[0] == '1'], + 'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', '0') == '1'], 'subpage_label': _('Delete all /subpages too?'), 'comment_label': _("Optional reason for the deletion"), 'buttons_html': buttons_html,
--- a/MoinMoin/action/Despam.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/Despam.py Tue Sep 23 23:02:50 2008 +0200 @@ -104,14 +104,13 @@ request.write(''' </table> <p> -<form method="post" action="%s/%s"> +<form method="post" action="%s"> <input type="hidden" name="action" value="Despam"> <input type="hidden" name="editor" value="%s"> <input type="submit" name="ok" value="%s"> </form> </p> -''' % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), - wikiutil.url_quote(editor), _("Revert all!"))) +''' % (request.href(pagename), wikiutil.url_quote(editor), _("Revert all!"))) def revert_page(request, pagename, editor): if not request.user.may.revert(pagename): @@ -183,11 +182,10 @@ request.theme.add_msg(_('You are not allowed to use this action.'), "error") return Page.Page(request, pagename).send_page() - editor = request.form.get('editor', [None])[0] + editor = request.form.get('editor') timestamp = time.time() - DAYS * 24 * 3600 - ok = request.form.get('ok', [0])[0] + ok = request.form.get('ok', 0) - request.emit_http_headers() request.theme.send_title("Despam", pagename=pagename) # Start content (important for RTL support) request.write(request.formatter.startContent("content"))
--- a/MoinMoin/action/LikePages.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/LikePages.py Tue Sep 23 23:02:50 2008 +0200 @@ -41,8 +41,6 @@ return # more than one match, list 'em - request.emit_http_headers() - # This action generate data using the user language request.setContentLanguage(request.lang)
--- a/MoinMoin/action/Load.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/Load.py Tue Sep 23 23:02:50 2008 +0200 @@ -36,11 +36,11 @@ form = self.form request = self.request - comment = form.get('comment', [u''])[0] + comment = form.get('comment', u'') comment = wikiutil.clean_input(comment) filename = form.get('file__filename__') - rename = form.get('rename', [''])[0].strip() + rename = form.get('rename', '').strip() if rename: target = rename else: @@ -50,7 +50,7 @@ target = wikiutil.clean_input(target) if target: - filecontent = form['file'][0] + filecontent = form['file'] if hasattr(filecontent, 'read'): # a file-like object filecontent = filecontent.read() # XXX reads complete file into memory! filecontent = wikiutil.decodeUnknownInput(filecontent)
--- a/MoinMoin/action/LocalSiteMap.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/LocalSiteMap.py Tue Sep 23 23:02:50 2008 +0200 @@ -31,7 +31,6 @@ def execute(pagename, request): _ = request.getText - request.emit_http_headers() # This action generate data using the user language request.setContentLanguage(request.lang)
--- a/MoinMoin/action/MyPages.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/MyPages.py Tue Sep 23 23:02:50 2008 +0200 @@ -58,7 +58,6 @@ pagecontent = pagecontent.replace('\n', '\r\n') from MoinMoin.parser.text_moin_wiki import Parser as WikiParser - request.emit_http_headers() # This action generate data using the user language request.setContentLanguage(request.lang)
--- a/MoinMoin/action/PackagePages.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/PackagePages.py Tue Sep 23 23:02:50 2008 +0200 @@ -68,11 +68,11 @@ form = self.request.form # Get new name from form and normalize. - pagelist = form.get('pagelist', [u''])[0] - packagename = form.get('packagename', [u''])[0] - include_attachments = form.get('include_attachments', [False])[0] + pagelist = form.get('pagelist', u'') + packagename = form.get('packagename', u'') + include_attachments = form.get('include_attachments', False) - if not form.get('submit', [None])[0]: + if not form.get('submit'): self.request.theme.add_msg(self.makeform(), "dialog") raise ActionError @@ -115,7 +115,7 @@ error = u'<p class="error">%s</p>\n' % error d = { - 'baseurl': self.request.getScriptname(), + 'url': self.request.href(self.pagename), 'error': error, 'action': self.__class__.__name__, 'pagename': wikiutil.escape(self.pagename, True), @@ -128,7 +128,7 @@ } form = ''' %(error)s -<form method="post" action="%(baseurl)s/%(pagename_quoted)s"> +<form method="post" action="%(url)s"> <input type="hidden" name="action" value="%(action)s"> <table> <tr> @@ -196,7 +196,7 @@ pages = [] for pagename in pagelist: - pagename = self.request.normalizePagename(pagename) + pagename = wikiutil.normalize_pagename(pagename, self.request.cfg) if pagename: page = Page(self.request, pagename) if page.exists() and self.request.user.may.read(pagename):
--- a/MoinMoin/action/RenamePage.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/RenamePage.py Tue Sep 23 23:02:50 2008 +0200 @@ -45,20 +45,19 @@ """ Rename this page to "pagename" """ _ = self._ form = self.form - newpagename = form.get('newpagename', [u''])[0] - newpagename = self.request.normalizePagename(newpagename) - comment = form.get('comment', [u''])[0] + newpagename = form.get('newpagename', u'') + newpagename = wikiutil.normalize_pagename(newpagename, self.cfg) + comment = form.get('comment', u'') comment = wikiutil.clean_input(comment) self.page = PageEditor(self.request, self.pagename) success, msgs = self.page.renamePage(newpagename, comment) rename_subpages = 0 - if 'rename_subpages' in form: - try: - rename_subpages = int(form['rename_subpages'][0]) - except: - pass + try: + rename_subpages = int(form['rename_subpages']) + except: + pass if rename_subpages and self.subpages: for name in self.subpages: @@ -84,7 +83,7 @@ d = { 'subpage': subpages, - 'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', ['0'])[0] == '1'], + 'subpages_checked': ('', 'checked')[self.request.form.get('subpages_checked', '0') == '1'], 'subpage_label': _('Rename all /subpages too?'), 'pagename': wikiutil.escape(self.pagename, True), 'newname_label': _("New name"),
--- a/MoinMoin/action/RenderAsDocbook.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/RenderAsDocbook.py Tue Sep 23 23:02:50 2008 +0200 @@ -9,5 +9,5 @@ def execute(pagename, request): url = Page(request, pagename).url(request, {'action': 'show', 'mimetype': 'text/docbook'}) - request.http_redirect(url) + return request.http_redirect(url)
--- a/MoinMoin/action/SpellCheck.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/SpellCheck.py Tue Sep 23 23:02:50 2008 +0200 @@ -102,7 +102,7 @@ from MoinMoin.PageEditor import PageEditor # get the new words as a string (if any are marked at all) try: - newwords = request.form['newwords'] + newwords = request.form.getlist('newwords') except KeyError: # no new words checked return @@ -187,8 +187,8 @@ # add a form containing the bad words if own_form: - msg = msg + ('<form method="post" action="%s/%s">\n' - '<input type="hidden" name="action" value="%s">\n') % (request.getScriptname(), wikiutil.quoteWikinameURL(page.page_name), action_name) + msg = msg + ('<form method="post" action="%s">\n' + '<input type="hidden" name="action" value="%s">\n') % (request.href(page.page_name), action_name) checkbox = '<input type="checkbox" name="newwords" value="%(word)s">%(word)s ' msg = msg + (
--- a/MoinMoin/action/SubscribeUser.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/SubscribeUser.py Tue Sep 23 23:02:50 2008 +0200 @@ -16,30 +16,28 @@ def show_form(pagename, request): _ = request.getText - request.emit_http_headers() request.theme.send_title(_("Subscribe users to the page %s") % pagename, pagename=pagename) request.write(""" -<form action="%s/%s" method="POST" enctype="multipart/form-data"> +<form action="%s" method="POST" enctype="multipart/form-data"> <input type="hidden" name="action" value="SubscribeUser"> %s <input type="text" name="users" size="50"> <input type="submit" value="Subscribe"> </form> -""" % (request.getScriptname(), wikiutil.quoteWikinameURL(pagename), +""" % (request.href(pagename), _("Enter user names (comma separated):"))) request.theme.send_footer(pagename) request.theme.send_closing_html() def show_result(pagename, request): _ = request.getText - request.emit_http_headers() request.theme.send_title(_("Subscribed for %s:") % pagename, pagename=pagename) from MoinMoin.formatter.text_html import Formatter formatter = Formatter(request) - result = subscribe_users(request, request.form['users'][0].split(","), pagename, formatter) + result = subscribe_users(request, request.form['users'].split(","), pagename, formatter) request.write(result) request.theme.send_footer(pagename) @@ -119,9 +117,8 @@ request_url = "localhost/" # Setup MoinMoin environment - from MoinMoin.request import request_cli - request = request_cli.Request(url=request_url) - request.form = request.args = request.setup_args() + from MoinMoin.web.contexts import ScriptContext + request = ScriptContext(url=request_url) from MoinMoin.formatter.text_plain import Formatter formatter = Formatter(request)
--- a/MoinMoin/action/SyncPages.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/SyncPages.py Tue Sep 23 23:02:50 2008 +0200 @@ -118,7 +118,7 @@ """ Does some fixup on the parameters. """ # Load the password if "password" in self.request.form: - params["password"] = self.request.form["password"][0] + params["password"] = self.request.form["password"] # merge the pageList case into the pageMatch case if params["pageList"] is not None:
--- a/MoinMoin/action/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -18,10 +18,12 @@ Additionally to the usual stuff, we provide an ActionBase class here with some of the usual base functionality for an action, like checking actions_excluded, making and checking tickets, rendering some form, - displaying errors and doing stuff after an action. + displaying errors and doing stuff after an action. Also utility functions + regarding actions are located here. @copyright: 2000-2004 Juergen Hermann <jh@web.de>, 2006 MoinMoin:ThomasWaldmann + 2008 MoinMoin:FlorianKrupicka @license: GNU GPL, see COPYING for details. """ @@ -88,7 +90,7 @@ return True # Require a valid ticket. Make outside attacks harder by # requiring two full HTTP transactions - ticket = self.form.get('ticket', [''])[0] + ticket = self.form.get('ticket', '') return wikiutil.checkTicket(self.request, ticket) # UI --------------------------------------------------------------------- @@ -134,19 +136,17 @@ d = { 'method': self.method, - 'baseurl': self.request.getScriptname(), + 'url': self.request.href(self.pagename), 'enctype': self.enctype, 'error_html': error_html, 'actionname': self.actionname, - 'pagename': self.pagename, - 'pagename_quoted': wikiutil.quoteWikinameURL(self.pagename), 'ticket_html': ticket_html, 'user_html': self.get_form_html(buttons_html), } form_html = ''' %(error_html)s -<form action="%(baseurl)s/%(pagename_quoted)s" method="%(method)s" enctype="%(enctype)s"> +<form action="%(url)s/%(pagename_quoted)s" method="%(method)s" enctype="%(enctype)s"> <div> <input type="hidden" name="action" value="%(actionname)s"> %(ticket_html)s @@ -243,7 +243,7 @@ if not request.user.may.read(pagename): Page(request, pagename).send_page() else: - mimetype = request.form.get('mimetype', [u"text/html"])[0] + mimetype = request.values.get('mimetype', u"text/html") rev = request.rev or 0 if rev == 0: request.cacheable = cacheable @@ -261,15 +261,14 @@ DEPRECATED: remove this action when we don't need it any more for compatibility. """ if 'mimetype' not in request.form: - request.form['mimetype'] = [u"text/plain"] + request.form['mimetype'] = u"text/plain" do_show(pagename, request, count_hit=0, cacheable=0) def do_content(pagename, request): """ same as do_show, but we only show the content """ # XXX temporary fix to make it work until Page.send_page gets refactored - request.setHttpHeader("Content-Type: text/html; charset=%s" % config.charset) - request.setHttpHeader('Status: 200 OK') - request.emit_http_headers() + request.mimetype = 'text/html' + request.status_code = 200 do_show(pagename, request, count_hit=0, content_only=1) def do_print(pagename, request): @@ -283,10 +282,10 @@ def do_refresh(pagename, request): """ Handle refresh action """ # Without arguments, refresh action will refresh the page text_html cache. - arena = request.form.get('arena', ['Page.py'])[0] + arena = request.form.get('arena', 'Page.py') if arena == 'Page.py': arena = Page(request, pagename) - key = request.form.get('key', ['text_html'])[0] + key = request.form.get('key', 'text_html') # Remove cache entry (if exists), and send the page from MoinMoin import caching @@ -296,27 +295,71 @@ def do_goto(pagename, request): """ redirect to another page """ - target = request.form.get('target', [''])[0] + target = request.form.get('target', '') request.http_redirect(Page(request, target).url(request)) # Dispatching ---------------------------------------------------------------- -def getNames(cfg): - if not hasattr(cfg.cache, 'action_names'): - lnames = names[:] - lnames.extend(wikiutil.getPlugins('action', cfg)) - cfg.cache.action_names = lnames # remember it - return cfg.cache.action_names +def get_names(config): + """ Get a list of known actions. -def getHandler(request, action, identifier="execute"): - """ return a handler function for a given action or None """ + @param config: a config object + @rtype: set + @return: set of known actions + """ + if not hasattr(config.cache, 'action_names'): + actions = names[:] + actions.extend(wikiutil.getPlugins('action', config)) + actions = set(action for action in actions + if not action in config.actions_excluded) + config.cache.action_names = actions # remember it + return config.cache.action_names + +def getHandler(cfg, action, identifier="execute"): + """ return a handler function for a given action or None. + + TODO: remove request dependency + """ # check for excluded actions - if action in request.cfg.actions_excluded: + if action in cfg.actions_excluded: return None try: - handler = wikiutil.importPlugin(request.cfg, "action", action, identifier) + handler = wikiutil.importPlugin(cfg, "action", action, identifier) except wikiutil.PluginMissingError: handler = globals().get('do_' + action) return handler +def get_available_actions(config, page, user): + """ Get a list of actions available on a particular page + for a particular user. + + The set does not contain actions that starts with lower case. + Themes use this set to display the actions to the user. + + @param config: a config object (for the per-wiki actions) + @param page: the page to which the actions should apply + @param user: the user which wants to apply an action + @rtype: set + @return: set of avaiable actions + """ + if not user.may.read(page.page_name): + return [] + + + actions = get_names(config) + + # Filter non ui actions (starts with lower case letter) + actions = [action for action in actions if not action[0].islower()] + + # Filter actions by page type, acl and user state + excluded = [] + if (page.isUnderlayPage() and not page.isStandardPage()) or \ + not user.may.write(page.page_name) or \ + not 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! + return set(action for action in actions if not action in excluded) + +
--- a/MoinMoin/action/_tests/test_attachfile.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/_tests/test_attachfile.py Tue Sep 23 23:02:50 2008 +0200 @@ -68,7 +68,7 @@ Tests if AttachFile.getAttachUrl taints a filename """ filename = "<test2.txt>" - expect = "rename=_test2.txt_&" + expect = "rename=_test2.txt_" result = AttachFile.getAttachUrl(self.pagename, filename, self.request, upload=True) assert expect in result
--- a/MoinMoin/action/bookmark.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/bookmark.py Tue Sep 23 23:02:50 2008 +0200 @@ -19,7 +19,7 @@ request.theme.add_msg(_("You must login to use this action: %(action)s.") % {"action": actname}, "error") return Page(request, pagename).send_page() - timestamp = request.form.get('time', [None])[0] + timestamp = request.form.get('time') if timestamp is not None: if timestamp == 'del': tm = None
--- a/MoinMoin/action/cache.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/cache.py Tue Sep 23 23:02:50 2008 +0200 @@ -197,10 +197,7 @@ def url(request, key, do='get'): """ return URL for the object cached for key """ - return "%s/?%s" % ( - request.getScriptname(), - wikiutil.makeQueryString(dict(action=action_name, do=do, key=key), want_unicode=False)) - + return request.href(action=action_name, do=do, key=key) def _get_headers(request, key): """ get last_modified and headers cached for key """ @@ -243,7 +240,7 @@ _do_remove(request, key) def execute(pagename, request): - do = request.form.get('do', [None])[0] - key = request.form.get('key', [None])[0] + do = request.form.get('do') + key = request.form.get('key') _do(request, do, key)
--- a/MoinMoin/action/chart.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/chart.py Tue Sep 23 23:02:50 2008 +0200 @@ -19,7 +19,7 @@ request.theme.add_msg(_("Charts are not available!"), "error") return request.page.send_page() - chart_type = request.form.get('type', [''])[0].strip() + chart_type = request.form.get('type', '').strip() if not chart_type: request.theme.add_msg(_('You need to provide a chart type!'), "error") return request.page.send_page()
--- a/MoinMoin/action/diff.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/diff.py Tue Sep 23 23:02:50 2008 +0200 @@ -21,7 +21,7 @@ return try: - date = request.form['date'][0] + date = request.form['date'] try: date = long(date) # must be long for py 2.2.x except StandardError: @@ -30,11 +30,11 @@ date = 0 try: - rev1 = int(request.form.get('rev1', [-1])[0]) + rev1 = int(request.form.get('rev1', -1)) except StandardError: rev1 = 0 try: - rev2 = int(request.form.get('rev2', [0])[0]) + rev2 = int(request.form.get('rev2', 0)) except StandardError: rev2 = 0 @@ -44,7 +44,7 @@ rev1 = -1 # spacing flag? - ignorews = int(request.form.get('ignorews', [0])[0]) + ignorews = int(request.form.get('ignorews', 0)) _ = request.getText @@ -71,7 +71,6 @@ # This action generates content in the user language request.setContentLanguage(request.lang) - request.emit_http_headers() request.theme.send_title(_('Diff for "%s"') % (pagename, ), pagename=pagename, allow_doubleclick=1) if rev1 > 0 and rev2 > 0 and rev1 > rev2 or rev1 == 0 and rev2 > 0:
--- a/MoinMoin/action/dumpform.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/dumpform.py Tue Sep 23 23:02:50 2008 +0200 @@ -12,6 +12,5 @@ """ dump the form data we received in this request for debugging """ data = util.dumpFormData(request.form) - request.emit_http_headers() request.write("<html><body>%s</body></html>" % data)
--- a/MoinMoin/action/edit.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/edit.py Tue Sep 23 23:02:50 2008 +0200 @@ -33,7 +33,7 @@ if editor not in valideditors: editor = request.cfg.editor_default - editorparam = request.form.get('editor', [editor])[0] + editorparam = request.form.get('editor', editor) if editorparam == "guipossible": lasteditor = editor elif editorparam == "textonly": @@ -49,11 +49,11 @@ editor = 'text' rev = request.rev or 0 - savetext = request.form.get('savetext', [None])[0] - comment = request.form.get('comment', [u''])[0] - category = request.form.get('category', [None])[0] - rstrip = int(request.form.get('rstrip', ['0'])[0]) - trivial = int(request.form.get('trivial', ['0'])[0]) + savetext = request.form.get('savetext') + comment = request.form.get('comment', u'') + category = request.form.get('category') + rstrip = int(request.form.get('rstrip', '0')) + trivial = int(request.form.get('trivial', '0')) if 'button_switch' in request.form: if editor == 'text': @@ -78,7 +78,7 @@ cancelled = 'button_cancel' in request.form if request.cfg.edit_ticketing: - ticket = request.form.get('ticket', [''])[0] + ticket = request.form.get('ticket', '') if not wikiutil.checkTicket(request, ticket): request.theme.add_msg(_('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'edit' }, "error") pg.send_page() @@ -88,7 +88,7 @@ try: if lasteditor == 'gui': # convert input from Graphical editor - format = request.form.get('format', ['wiki'])[0] + format = request.form.get('format', 'wiki') if format == 'wiki': converter_name = 'text_html_text_moin_wiki' else:
--- a/MoinMoin/action/fckdialog.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/fckdialog.py Tue Sep 23 23:02:50 2008 +0200 @@ -15,7 +15,6 @@ def macro_dialog(request): help = get_macro_help(request) - request.emit_http_headers() request.write( '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> @@ -164,13 +163,12 @@ def page_list(request): from MoinMoin import search - name = request.form.get("pagename", [""])[0] + name = request.form.get("pagename", "") if name: searchresult = search.searchPages(request, 't:"%s"' % name) pages = [p.page_name for p in searchresult.hits] else: pages = [name] - request.emit_http_headers() request.write( '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> @@ -201,9 +199,8 @@ ''' % "".join(["<option>%s</option>\n" % p for p in pages])) def link_dialog(request): - request.emit_http_headers() # list of wiki pages - name = request.form.get("pagename", [""])[0] + name = request.form.get("pagename", "") if name: from MoinMoin import search # XXX error handling! @@ -242,7 +239,7 @@ # wiki url url_prefix_static = request.cfg.url_prefix_static - scriptname = request.getScriptname() + scriptname = request.script_root if not scriptname or scriptname[-1] != "/": scriptname += "/" action = scriptname @@ -367,9 +364,8 @@ ############################################################################## def attachment_dialog(request): - request.emit_http_headers() # list of wiki pages - name = request.form.get("pagename", [""])[0] + name = request.form.get("pagename", "") if name: from MoinMoin import search # XXX error handling! @@ -393,7 +389,7 @@ # wiki url url_prefix_static = request.cfg.url_prefix_static - scriptname = request.getScriptname() + scriptname = request.script_root if not scriptname or scriptname[-1] != "/": scriptname += "/" action = scriptname @@ -461,7 +457,6 @@ ############################################################################## def image_dialog(request): - request.emit_http_headers() url_prefix_static = request.cfg.url_prefix_static request.write(''' <!--
--- a/MoinMoin/action/fullsearch.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/fullsearch.py Tue Sep 23 23:02:50 2008 +0200 @@ -22,12 +22,12 @@ 'fullsearch' with localized string. If both missing, default to True (might happen with Safari) if this isn't an advanced search. """ - form = request.form + form = request.values if 'titlesearch' in form and 'fullsearch' in form: ret = -1 # spammer / bot else: try: - ret = int(form['titlesearch'][0]) + ret = int(form['titlesearch']) except ValueError: ret = 1 except KeyError: @@ -37,7 +37,7 @@ def isAdvancedSearch(request): """ Return True if advanced search is requested """ try: - return int(request.form['advancedsearch'][0]) + return int(request.values['advancedsearch']) except KeyError: return False @@ -66,38 +66,40 @@ advancedsearch = isAdvancedSearch(request) + form = request.values + # context is relevant only for full search if titlesearch: context = 0 elif advancedsearch: context = 180 # XXX: hardcoded context count for advancedsearch else: - context = int(request.form.get('context', [0])[0]) + context = int(form.get('context', 0)) # Get other form parameters - needle = request.form.get(fieldname, [''])[0] - case = int(request.form.get('case', [0])[0]) - regex = int(request.form.get('regex', [0])[0]) # no interface currently - hitsFrom = int(request.form.get('from', [0])[0]) + needle = form.get(fieldname, '') + case = int(form.get('case', 0)) + regex = int(form.get('regex', 0)) # no interface currently + hitsFrom = int(form.get('from', 0)) mtime = None msg = '' historysearch = 0 # if advanced search is enabled we construct our own search query if advancedsearch: - and_terms = request.form.get('and_terms', [''])[0].strip() - or_terms = request.form.get('or_terms', [''])[0].strip() - not_terms = request.form.get('not_terms', [''])[0].strip() - #xor_terms = request.form.get('xor_terms', [''])[0].strip() - categories = request.form.get('categories', ['']) - timeframe = request.form.get('time', [''])[0].strip() - language = request.form.get('language', ['']) - mimetype = request.form.get('mimetype', [0]) - excludeunderlay = request.form.get('excludeunderlay', [0])[0] - nosystemitems = request.form.get('nosystemitems', [0])[0] - historysearch = request.form.get('historysearch', [0])[0] + and_terms = form.get('and_terms', '').strip() + or_terms = form.get('or_terms', '').strip() + not_terms = form.get('not_terms', '').strip() + #xor_terms = form.get('xor_terms', '').strip() + categories = form.getlist('categories') or [''] + timeframe = form.get('time', '').strip() + language = form.getlist('language') or [''] + mimetype = form.getlist('mimetype') or [0] + excludeunderlay = form.get('excludeunderlay', 0) + nosystemitems = form.get('nosystemitems', 0) + historysearch = form.get('historysearch', 0) - mtime = request.form.get('mtime', [''])[0] + mtime = form.get('mtime', '') if mtime: mtime_parsed = None @@ -223,12 +225,10 @@ Page(request, pagename).send_page() return - request.emit_http_headers() - # This action generates data using the user language request.setContentLanguage(request.lang) - request.theme.send_title(title % needle, form=request.form, pagename=pagename) + request.theme.send_title(title % needle, form=form, pagename=pagename) # Start content (important for RTL support) request.write(request.formatter.startContent("content"))
--- a/MoinMoin/action/info.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/info.py Tue Sep 23 23:02:50 2008 +0200 @@ -187,8 +187,6 @@ page = Page(request, pagename) title = page.split_title() - request.emit_http_headers() - request.setContentLanguage(request.lang) f = request.formatter @@ -207,14 +205,8 @@ request.write("[%s] " % page.link_to(request, text=text, querystr=querystr, rel='nofollow')) request.write(f.paragraph(0)) - try: - show_hitcounts = int(request.form.get('hitcounts', [0])[0]) != 0 - except ValueError: - show_hitcounts = False - try: - show_general = int(request.form.get('general', [0])[0]) != 0 - except ValueError: - show_general = False + show_hitcounts = int(request.form.get('hitcounts', 0)) != 0 + show_general = int(request.form.get('general', 0)) != 0 if show_hitcounts: from MoinMoin.stats import hitcounts
--- a/MoinMoin/action/links.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/links.py Tue Sep 23 23:02:50 2008 +0200 @@ -15,11 +15,11 @@ # get the MIME type if 'mimetype' in form: - mimetype = form['mimetype'][0] + mimetype = form['mimetype'] else: mimetype = "text/html" - request.emit_http_headers(["Content-Type: %s; charset=%s" % (mimetype, config.charset)]) + request.mimetype = mimetype if mimetype == "text/html": request.theme.send_title(_('Full Link List for "%s"') % request.cfg.sitename)
--- a/MoinMoin/action/login.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/login.py Tue Sep 23 23:02:50 2008 +0200 @@ -38,7 +38,6 @@ form.append(html.INPUT(type='hidden', name='stage', value=request._login_multistage_name)) - request.emit_http_headers() request.theme.send_title(_("Login"), pagename=self.pagename) # Start content (important for RTL support) request.write(request.formatter.startContent("content")) @@ -59,7 +58,7 @@ error = None - islogin = form.get('login', [''])[0] + islogin = form.get('login', '') if islogin: # user pressed login button if request._login_multistage: @@ -74,7 +73,6 @@ return self.page.send_page() else: # show login form - request.emit_http_headers() request.theme.send_title(_("Login"), pagename=self.pagename) # Start content (important for RTL support) request.write(request.formatter.startContent("content"))
--- a/MoinMoin/action/newaccount.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/newaccount.py Tue Sep 23 23:02:50 2008 +0200 @@ -19,7 +19,7 @@ _ = request.getText form = request.form - if request.request_method != 'POST': + if request.method != 'POST': return if not TextCha(request).check_answer_from_form(): @@ -30,7 +30,7 @@ # Require non-empty name try: - theuser.name = form['name'][0] + theuser.name = form['name'] except KeyError: return _("Empty user name. Please enter a user name.") @@ -45,8 +45,8 @@ return _("This user name already belongs to somebody else.") # try to get the password and pw repeat - password = form.get('password1', [''])[0] - password2 = form.get('password2', [''])[0] + password = form.get('password1', '') + password2 = form.get('password2', '') # Check if password is given and matches with password repeat if password != password2: @@ -69,7 +69,7 @@ return "Can't encode password: %s" % str(err) # try to get the email, for new users it is required - email = wikiutil.clean_input(form.get('email', [''])[0]) + email = wikiutil.clean_input(form.get('email', '')) theuser.email = email.strip() if not theuser.email and 'email' not in request.cfg.user_form_remove: return _("Please provide your email address. If you lose your" @@ -172,7 +172,6 @@ request.theme.add_msg(_create_user(request), "dialog") return page.send_page() else: # show create form - request.emit_http_headers() request.theme.send_title(_("Create Account"), pagename=pagename) request.write(request.formatter.startContent("content"))
--- a/MoinMoin/action/newpage.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/newpage.py Tue Sep 23 23:02:50 2008 +0200 @@ -19,8 +19,8 @@ def __init__(self, request, referrer): self.request = request self.referrer = referrer # The page the user came from - self.pagename = self.request.form.get('pagename', [None])[0] - self.nametemplate = self.request.form.get('nametemplate', ['%s'])[0] + self.pagename = self.request.form.get('pagename') + self.nametemplate = self.request.form.get('nametemplate', '%s') self.nametemplate = self.nametemplate.replace('\x00', '') def checkAndCombineArguments(self): @@ -82,11 +82,11 @@ pagename = self.pagename query = {'action': 'edit', 'backto': self.referrer} - template = self.request.form.get('template', [''])[0] + template = self.request.form.get('template', '') if template: query['template'] = template - parent = self.request.form.get('parent', [''])[0] + parent = self.request.form.get('parent', '') if parent: pagename = "%s/%s" % (parent, pagename) @@ -97,7 +97,7 @@ def execute(pagename, request): """ Temporary glue code for current moin action system """ - if request.request_method != 'POST': + if request.method != 'POST': return False, u'' return NewPage(request, pagename).render()
--- a/MoinMoin/action/pollsistersites.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/pollsistersites.py Tue Sep 23 23:02:50 2008 +0200 @@ -54,5 +54,5 @@ except TypeError: # catch bug in python 2.5: "EnvironmentError expected at most 3 arguments, got 4" status.append(u"Site: %s Status: Not updated." % sistername) - request.emit_http_headers(["Content-Type: text/plain; charset=UTF-8"]) + request.mimetype = 'text/plain' request.write("\r\n".join(status).encode("utf-8"))
--- a/MoinMoin/action/recoverpass.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/recoverpass.py Tue Sep 23 23:02:50 2008 +0200 @@ -30,7 +30,7 @@ Contact the owner of the wiki, who can enable email.""") try: - email = wikiutil.clean_input(form['email'][0].lower()) + email = wikiutil.clean_input(form['email'].lower()) if not email: # continue if email not given raise KeyError @@ -42,7 +42,7 @@ pass try: - username = wikiutil.clean_input(form['name'][0]) + username = wikiutil.clean_input(form['name']) if not username: # continue if name not given raise KeyError @@ -160,13 +160,13 @@ page.send_page() return - submitted = form.get('account_sendmail', [''])[0] - token = form.get('token', [''])[0] - newpass = form.get('password', [''])[0] - name = form.get('name', [''])[0] + submitted = form.get('account_sendmail', '') + token = form.get('token', '') + newpass = form.get('password', '') + name = form.get('name', '') if token and name and newpass: - newpass2 = form.get('password_repeat', [''])[0] + newpass2 = form.get('password_repeat', '') msg = _("Passwords don't match!") msg_type = 'error' if newpass == newpass2: @@ -190,7 +190,6 @@ return if token and name: - request.emit_http_headers() request.theme.send_title(_("Password reset"), pagename=pagename) request.write(request.formatter.startContent("content")) @@ -205,13 +204,12 @@ request.theme.send_footer(pagename) request.theme.send_closing_html() elif submitted: # user pressed create button - if request.request_method != 'POST': + if request.method != 'POST': return msg = _do_recover(request) request.theme.add_msg(msg, "dialog") page.send_page() else: # show create form - request.emit_http_headers() request.theme.send_title(_("Lost password"), pagename=pagename) request.write(request.formatter.startContent("content"))
--- a/MoinMoin/action/refresh.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/refresh.py Tue Sep 23 23:02:50 2008 +0200 @@ -11,10 +11,10 @@ def execute(pagename, request): """ Handle refresh action """ # Without arguments, refresh action will refresh the page text_html cache. - arena = request.form.get('arena', ['Page.py'])[0] + arena = request.form.get('arena', 'Page.py') if arena == 'Page.py': arena = Page(request, pagename) - key = request.form.get('key', ['text_html'])[0] + key = request.form.get('key', 'text_html') # Remove cache entry (if exists), and send the page from MoinMoin import caching
--- a/MoinMoin/action/revert.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/revert.py Tue Sep 23 23:02:50 2008 +0200 @@ -48,10 +48,10 @@ def do_action(self): """ revert pagename """ form = self.form - comment = form.get('comment', [u''])[0] + comment = form.get('comment', u'') comment = wikiutil.clean_input(comment) - if self.request.request_method != 'POST': + if self.request.method != 'POST': return False, u'' rev = self.request.rev
--- a/MoinMoin/action/rss_rc.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/rss_rc.py Tue Sep 23 23:02:50 2008 +0200 @@ -24,8 +24,7 @@ """ Send recent changes as an RSS document """ if not wikixml.ok: - httpheaders = ["Content-Type: text/plain; charset=%s" % config.charset] - request.emit_http_headers(httpheaders) + request.mimetype = 'text/plain' request.write("rss_rc action is not supported because of missing pyxml module.") return @@ -34,22 +33,22 @@ # get params items_limit = 100 try: - max_items = int(request.form['items'][0]) + max_items = int(request.form['items']) max_items = min(max_items, items_limit) # not more than `items_limit` except (KeyError, ValueError): # not more than 15 items in a RSS file by default max_items = 15 try: - unique = int(request.form.get('unique', [0])[0]) + unique = int(request.form.get('unique', 0)) except ValueError: unique = 0 try: - diffs = int(request.form.get('diffs', [0])[0]) + diffs = int(request.form.get('diffs', 0)) except ValueError: diffs = 0 ## ddiffs inserted by Ralf Zosel <ralf@zosel.com>, 04.12.2003 try: - ddiffs = int(request.form.get('ddiffs', [0])[0]) + ddiffs = int(request.form.get('ddiffs', 0)) except ValueError: ddiffs = 0 @@ -86,28 +85,26 @@ if request.if_modified_since == timestamp: if request.if_none_match: if request.if_none_match == etag: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 else: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 elif request.if_none_match == etag: if request.if_modified_since: if request.if_modified_since == timestamp: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 else: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 else: # generate an Expires header, using whatever setting the admin # defined for suggested cache lifetime of the RecentChanges RSS doc - expires = timefuncs.formathttpdate(time.time() + cfg.rss_cache) + expires = time.time() + cfg.rss_cache - httpheaders = ["Content-Type: text/xml; charset=%s" % config.charset, - "Expires: %s" % expires, - "Last-Modified: %s" % timestamp, - "Etag: %s" % etag, ] + request.mime_type = 'text/xml' + request.expires = expires + request.last_modified = lastmod + request.headers.add('Etag', etag) # send the generated XML document - request.emit_http_headers(httpheaders) - baseurl = request.getBaseURL() if not baseurl.endswith('/'): baseurl += '/'
--- a/MoinMoin/action/serveopenid.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/serveopenid.py Tue Sep 23 23:02:50 2008 +0200 @@ -18,7 +18,7 @@ from openid.server import server from openid.message import IDENTIFIER_SELECT from MoinMoin.widget import html -from MoinMoin.request import MoinMoinFinish +from MoinMoin.web.request import MoinMoinFinish def execute(pagename, request): return MoinOpenIDServer(pagename, request).handle() @@ -31,9 +31,8 @@ def serveYadisEP(self, endpoint_url): request = self.request - hdrs = ['Content-type: application/xrds+xml'] + request.content_type = 'application/xrds+xml' - request.emit_http_headers(hdrs) user_url = request.getQualifiedURL(request.page.url(request)) self.request.write("""\ <?xml version="1.0" encoding="UTF-8"?> @@ -79,9 +78,8 @@ def serveYadisIDP(self, endpoint_url): request = self.request - hdrs = ['Content-type: application/xrds+xml'] + request.content_type = 'application/xrds+xml' - request.emit_http_headers(hdrs) user_url = request.getQualifiedURL(request.page.url(request)) self.request.write("""\ <?xml version="1.0" encoding="UTF-8"?> @@ -190,7 +188,7 @@ server_url = request.getQualifiedURL( request.page.url(request, querystr={'action': 'serveopenid'})) - yadis_type = form.get('yadis', [None])[0] + yadis_type = form.get('yadis') if yadis_type == 'ep': return self.serveYadisEP(server_url) elif yadis_type == 'idp': @@ -198,7 +196,7 @@ # if the identity is set it must match the server URL # sort of arbitrary, but we have to have some restriction - identity = form.get('openid.identity', [None])[0] + identity = form.get('openid.identity') if identity == IDENTIFIER_SELECT: identity, server_url = self._make_identity() if not identity: @@ -217,7 +215,7 @@ openidsrv = server.Server(store, op_endpoint=server_url) answer = None - if form.has_key('dontapprove'): + if 'dontapprove' in form: answer = self.handle_response(False, username, identity) if answer is None: return @@ -227,8 +225,8 @@ return else: query = {} - for key in form.keys(): - query[key] = form[key][0] + for key in form: + query[key] = form[key] try: openidreq = openidsrv.decodeRequest(query) except Exception, e: @@ -249,10 +247,9 @@ else: answer = openidsrv.handleRequest(openidreq) webanswer = openidsrv.encodeResponse(answer) - headers = ['Status: %d OpenID status' % webanswer.code] + request.status = '%d OpenID status' % webanswer.code for hdr in webanswer.headers: - headers += [hdr+': '+webanswer.headers[hdr]] - request.emit_http_headers(headers) + request.headers.add(hdr, webanswer.headers[hdr]) request.write(webanswer.body) raise MoinMoinFinish @@ -266,7 +263,7 @@ if session_nonce is not None: del self.request.session['openidserver.nonce'] # use empty string if nothing was sent - form_nonce = form.get('nonce', [''])[0] + form_nonce = form.get('nonce', '') if session_nonce != form_nonce: self.request.makeForbidden403() self.request.write('invalid nonce') @@ -285,7 +282,7 @@ return openidreq.answer(False) - if form.get('remember', ['no'])[0] == 'yes': + if form.get('remember', 'no') == 'yes': if not hasattr(request.user, 'openid_trusted_roots'): request.user.openid_trusted_roots = [] request.user.openid_trusted_roots.append(strbase64(openidreq.trust_root)) @@ -324,7 +321,6 @@ Once you have logged in, simply reload this page.''')) return - request.emit_http_headers() request.theme.send_title(_("OpenID Trust verification"), pagename=request.page.page_name) # Start content (important for RTL support) request.write(request.formatter.startContent("content")) @@ -397,7 +393,6 @@ request = self.request _ = self._ - request.emit_http_headers() request.theme.send_title(_("OpenID not served"), pagename=request.page.page_name) # Start content (important for RTL support) request.write(request.formatter.startContent("content"))
--- a/MoinMoin/action/showtags.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/showtags.py Tue Sep 23 23:02:50 2008 +0200 @@ -13,9 +13,7 @@ from MoinMoin.wikisync import TagStore def execute(pagename, request): - mimetype = "text/plain" - - request.emit_http_headers(["Content-Type: %s; charset=%s" % (mimetype, config.charset)]) + request.mimetype = "text/plain" page = Page(request, pagename) tags = TagStore(page)
--- a/MoinMoin/action/sisterpages.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/sisterpages.py Tue Sep 23 23:02:50 2008 +0200 @@ -32,27 +32,25 @@ if request.if_modified_since == timestamp: if request.if_none_match: if request.if_none_match == etag: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 else: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 elif request.if_none_match == etag: if request.if_modified_since: if request.if_modified_since == timestamp: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 else: - request.emit_http_headers(["Status: 304 Not modified"]) + request.status_code = 304 else: # generate an Expires header, using 1d cache lifetime of sisterpages list - expires = timefuncs.formathttpdate(time.time() + 24*3600) + expires = time.time() + 24*3600 - httpheaders = ["Content-Type: text/plain; charset=UTF-8", - "Expires: %s" % expires, - "Last-Modified: %s" % timestamp, - "Etag: %s" % etag, ] + request.mime_type = 'text/plain' + request.expires = expires + request.last_modified = timestamp + request.headers.add("Etag", etag) # send the generated XML document - request.emit_http_headers(httpheaders) - baseurl = request.getBaseURL() if not baseurl.endswith('/'): baseurl += '/'
--- a/MoinMoin/action/sitemap.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/sitemap.py Tue Sep 23 23:02:50 2008 +0200 @@ -65,7 +65,7 @@ form = request.form request.user.datetime_fmt = datetime_fmt - request.emit_http_headers(["Content-Type: text/xml; charset=UTF-8"]) + request.mimetype ='text/xml' # we emit a piece of data so other side doesn't get bored: request.write("""<?xml version="1.0" encoding="UTF-8"?>\r\n""") @@ -86,7 +86,7 @@ # Get page dict readable by current user try: - underlay = int(form.get('underlay', [1])[0]) + underlay = int(form.get('underlay', 1)) except ValueError: underlay = 1 pages = request.rootpage.getPageDict(include_underlay=underlay)
--- a/MoinMoin/action/thread_monitor.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/thread_monitor.py Tue Sep 23 23:02:50 2008 +0200 @@ -33,7 +33,6 @@ else: dump_fname = "nowhere" - request.emit_http_headers() request.write('<html><body>A dump has been saved to %s.</body></html>' % dump_fname) def execute_wiki(pagename, request): @@ -44,8 +43,6 @@ request.theme.add_msg(_('You are not allowed to use this action.'), "error") return Page.Page(request, pagename).send_page() - request.emit_http_headers() - request.theme.send_title("Thread monitor") request.write('<pre>')
--- a/MoinMoin/action/titleindex.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/titleindex.py Tue Sep 23 23:02:50 2008 +0200 @@ -18,11 +18,10 @@ # get the MIME type if 'mimetype' in form: - mimetype = form['mimetype'][0] + mimetype = form['mimetype'] else: mimetype = "text/plain" - - request.emit_http_headers(["Content-Type: %s; charset=%s" % (mimetype, config.charset)]) + request.mimetype = mimetype # Get list of user readable pages pages = request.rootpage.getPageList()
--- a/MoinMoin/action/userprefs.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/userprefs.py Tue Sep 23 23:02:50 2008 +0200 @@ -16,7 +16,7 @@ Return error msg_class, msg tuple or None, None. """ _ = request.getText - sub = request.form.get('handler', [None])[0] + sub = request.form.get('handler') if sub in request.cfg.userprefs_disabled: return None, None @@ -68,7 +68,7 @@ else: msg_class, msg = None, None - sub = request.form.get('sub', [''])[0] + sub = request.args.get('sub', '') cls = None if sub and sub not in request.cfg.userprefs_disabled: try: @@ -101,7 +101,6 @@ title = _("Settings") + ":" + title else: title = _("Settings") - request.emit_http_headers() request.theme.add_msg(msg, msg_class) request.theme.send_title(title, page=request.page, pagename=pagename) # Start content (important for RTL support)
--- a/MoinMoin/action/userprofile.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/action/userprofile.py Tue Sep 23 23:02:50 2008 +0200 @@ -17,9 +17,9 @@ if not request.user.isSuperUser(): request.theme.add_msg(_("Only superuser is allowed to use this action."), "error") else: - user_name = form.get('name', [''])[0] - key = form.get('key', [''])[0] - val = form.get('val', [''])[0] + user_name = form.get('name', '') + key = form.get('key', '') + val = form.get('val', '') if key in cfg.user_checkbox_fields: val = int(val) uid = user.getUserId(request, user_name)
--- a/MoinMoin/auth/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/auth/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -257,3 +257,94 @@ 'userprefslink': userprefslink, 'sendmypasswordlink': sendmypasswordlink} +def handle_login(request, userobj=None, username=None, password=None, + attended=True, openid_identifier=None, stage=None): + """ + Process a 'login' request by going through the configured authentication + methods in turn. The passable keyword arguments are explained in more + detail at the top of this file. + """ + params = { + 'username': username, + 'password': password, + 'attended': attended, + 'openid_identifier': openid_identifier, + 'multistage': (stage and True) or None + } + for authmethod in request.cfg.auth: + if stage and authmethod.name != stage: + continue + ret = authmethod.login(request, userobj, **params) + + userobj = ret.user_obj + cont = ret.continue_flag + if stage: + stage = None + del params['multistage'] + + if ret.multistage: + request._login_multistage = ret.multistage + request._login_multistage_name = authmethod.name + return userobj + + if ret.redirect_to: + nextstage = auth.get_multistage_continuation_url(request, authmethod.name) + url = ret.redirect_to + url = url.replace('%return_form', quote_plus(nextstage)) + url = url.replace('%return', quote(nextstage)) + abort(redirect(url)) + msg = ret.message + if msg and not msg in request._login_messages: + request._login_messages.append(msg) + + if not cont: + break + + return userobj + +def handle_logout(request, userobj): + """ Logout the passed user from every configured authentication method. """ + for authmethod in request.cfg.auth: + userobj, cont = authmethod.logout(request, userobj, cookie=request.cookies) + if not cont: + break + return userobj + +def handle_request(request, userobj): + """ Handle the per-request callbacks of the configured authentication methods. """ + for authmethod in request.cfg.auth: + userobj, cont = authmethod.request(request, userobj, cookie=request.cookies) + if not cont: + break + return userobj + +def setup_setuid(request, userobj): + """ Check for setuid conditions in the session and setup an user + object accordingly. Returns a tuple of the new user objects. + + @param request: a moin request object + @param userobj: a moin user object + @rtype: boolean + @return: (new_user, user) or (user, None) + """ + old_user = None + if 'setuid' in request.session and userobj.isSuperUser(): + old_user = userobj + uid = request.session['setuid'] + userobj = user.User(request, uid, auth_method='setuid') + userobj.valid = True + return (userobj, old_user) + +def setup_from_session(request, session): + userobj = None + if 'user.id' in session: + auth_userid = session['user.id'] + auth_method = session['user.auth_method'] + auth_attrs = session['user.auth_attribs'] + if auth_method and auth_method in \ + [auth.name for auth in request.cfg.auth]: + userobj = user.User(request, id=auth_userid, + auth_method=auth_method, + auth_attribs=auth_attrs) + logging.debug("session started for user %r", userobj) + return userobj
--- a/MoinMoin/auth/_tests/test_auth.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/auth/_tests/test_auth.py Tue Sep 23 23:02:50 2008 +0200 @@ -5,14 +5,11 @@ @copyright: 2008 MoinMoin:ThomasWaldmann @license: GNU GPL, see COPYING for details. """ - import py.test py.test.skip("broken due to test Config refactoring, fix later") -import StringIO, urllib - -from MoinMoin.server.server_wsgi import WsgiConfig -from MoinMoin.request import request_wsgi +from MoinMoin.web.request import TestRequest, evaluate_request +from MoinMoin import wsgiapp from MoinMoin._tests import wikiconfig @@ -25,55 +22,32 @@ Some test needs specific config values, or they will fail. """ - config = WsgiConfig() # you MUST create an instance def teardown_class(cls): """ Stuff that should run to clean up the state of this test class """ - pass - def setup_env(self, **kw): - default_environ = { - 'SERVER_NAME': 'localhost', - 'SERVER_PORT': '80', - 'SCRIPT_NAME': '', - 'PATH_INFO': '/', - 'QUERY_STRING': '', - 'REQUEST_METHOD': 'GET', - 'REMOTE_ADDR': '10.10.10.10', - 'HTTP_HOST': 'localhost', - #'HTTP_COOKIE': '', - #'HTTP_ACCEPT_LANGUAGE': '', - } - env = {} - env.update(default_environ) - env.update(kw) - if 'wsgi.input' not in env: - env['wsgi.input'] = StringIO.StringIO() - return env - - def process_request(self, environ): - request = request_wsgi.Request(environ) - save_user = request.user # keep a reference, request.finish does "del request.user" - request.run() - request.user = save_user - return request # request.status, request.headers, request.output() + def run_request(self, **params): + request = TestRequest(**params) + context = wsgiapp.init(request) + wsgiapp.run(context) + return context class TestNoAuth(AuthTest): def testNoAuth(self): """ run a simple request, no auth, just check if it succeeds """ - environ = self.setup_env() - request = self.process_request(environ) + request = self.run_request() # anon user? assert not request.user.valid + appiter, status, headers = evaluate_request(request.request) # check if the request resulted in normal status, result headers and content - assert request.status == '200 OK' + assert status[:3] == '200' has_ct = has_v = has_cc = False - for k, v in request.headers: + for k, v in headers: if k == 'Content-Type': assert v.startswith('text/html') has_ct = True @@ -90,8 +64,7 @@ assert has_v # XXX BROKEN?: #assert has_cc # cache anon user's content - output = request.output() - assert '</html>' in output + assert '</html>' in ''.join(appiter) class TestAnonSession(AuthTest): class Config(wikiconfig.Config): @@ -103,20 +76,21 @@ trail_expected = [] first = True for pagename in self.PAGES: - environ = self.setup_env(PATH_INFO='/%s' % pagename, - HTTP_COOKIE=cookie) - request = self.process_request(environ) + environ_overrides = {'HTTP_COOKIE': cookie} + request = self.run_request(path='/%s' % pagename, + environ_overrides=environ_overrides) # anon user? assert not request.user.valid # Do we have a session? - assert request.session + assert request.session is not None + appiter, status, headers = evaluate_request(request.request) # check if the request resulted in normal status, result headers and content - assert request.status == '200 OK' + assert status[:3] == '200' has_ct = has_v = has_cc = False - for k, v in request.headers: + for k, v in headers: if k == 'Content-Type': assert v.startswith('text/html') has_ct = True @@ -134,8 +108,7 @@ assert has_v # XX BROKEN #assert not has_cc # do not cache anon user's (with session!) content - output = request.output() - assert '</html>' in output + assert '</html>' in ''.join(appiter) # The trail is only ever saved on the second page display # because otherwise anonymous sessions would be created @@ -163,24 +136,27 @@ def testHttpAuthSession(self): """ run some requests with http auth, check whether session works """ username = u'HttpAuthTestUser' + auth_info = u'%s:%s' % (username, u'testpass') + auth_header = 'Basic %s' % auth_info.encode('base64') cookie = '' trail_expected = [] first = True for pagename in self.PAGES: - environ = self.setup_env(AUTH_TYPE='Basic', REMOTE_USER=str(username), - PATH_INFO='/%s' % pagename, - HTTP_COOKIE=cookie) - request = self.process_request(environ) + environ_overrides = {'HTTP_COOKIE': cookie, + 'HTTP_AUTHORIZATION': auth_header} + request = self.run_request(path='/%s' % pagename, + environ_overrides=environ_overrides) # Login worked? assert request.user.valid assert request.user.name == username # Do we have a session? - assert request.session + assert request.session is not None + appiter, status, headers = evaluate_request(request.request) # check if the request resulted in normal status, result headers and content - assert request.status == '200 OK' + assert status[:3] == '200' has_ct = has_v = has_cc = False for k, v in request.headers: if k == 'Content-Type': @@ -199,8 +175,7 @@ assert has_ct assert has_v assert has_cc # do not cache logged-in user's content - output = request.output() - assert '</html>' in output + assert '</html>' in ''.join(appiter) # The trail is only ever saved on the second page display # because otherwise anonymous sessions would be created @@ -233,30 +208,29 @@ first = True for pagename in self.PAGES: if first: - formdata = urllib.urlencode({ - 'name': username.encode('utf-8'), - 'password': password.encode('utf-8'), + formdata = { + 'name': username, + 'password': password, 'login': 'login', - }) - environ = self.setup_env(PATH_INFO='/%s' % pagename, - HTTP_CONTENT_TYPE='application/x-www-form-urlencoded', - HTTP_CONTENT_LENGTH='%d' % len(formdata), - QUERY_STRING='action=login', REQUEST_METHOD='POST', - **{'wsgi.input': StringIO.StringIO(formdata)}) + } + request = self.run_request(path='/%s' % pagename, + query_string='login=login', + method='POST', form_data=formdata) else: # not first page, use session cookie - environ = self.setup_env(PATH_INFO='/%s' % pagename, - HTTP_COOKIE=cookie) - request = self.process_request(environ) + environ_overrides = {'HTTP_COOKIE': cookie} + request = self.run_request(path='/%s' % pagename, + environ_overrides=environ_overrides) # Login worked? assert request.user.valid assert request.user.name == username # Do we have a session? - assert request.session + assert request.session is not None + appiter, status, headers = evaluate_request(request.request) # check if the request resulted in normal status, result headers and content - assert request.status == '200 OK' + assert status[:3] == '200' has_ct = has_v = has_cc = False for k, v in request.headers: if k == 'Content-Type': @@ -275,8 +249,7 @@ assert has_ct assert has_v assert has_cc # do not cache logged-in user's content - output = request.output() - assert '</html>' in output + assert '</html>' in ''.join(appiter) # The trail is only ever saved on the second page display # because otherwise anonymous sessions would be created
--- a/MoinMoin/auth/_tests/test_ldap_login.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/auth/_tests/test_ldap_login.py Tue Sep 23 23:02:50 2008 +0200 @@ -12,6 +12,7 @@ from MoinMoin._tests.ldap_testbase import LDAPTstBase, LdapEnvironment, check_environ, SLAPD_EXECUTABLE from MoinMoin._tests.ldap_testdata import * from MoinMoin._tests import nuke_user, wikiconfig +from MoinMoin.auth import handle_login # first check if we have python 2.4, python-ldap and slapd: msg = check_environ() @@ -60,21 +61,21 @@ handle_auth = self.request.handle_auth # tests that must not authenticate: - u = handle_auth(None, username='', password='', login=True) + u = handle_login(self.request, None, username='', password='') assert u is None - u = handle_auth(None, username='usera', password='', login=True) + u = handle_login(self.request, None, username='usera', password='') assert u is None - u = handle_auth(None, username='usera', password='userawrong', login=True) + u = handle_login(self.request, None, username='usera', password='userawrong') assert u is None - u = handle_auth(None, username='userawrong', password='usera', login=True) + u = handle_login(self.request, None, username='userawrong', password='usera') assert u is None # tests that must authenticate: - u1 = handle_auth(None, username='usera', password='usera', login=True) + u1 = handle_login(self.request, None, username='usera', password='usera') assert u1 is not None assert u1.valid - u2 = handle_auth(None, username='userb', password='userb', login=True) + u2 = handle_login(self.request, None, username='userb', password='userb') assert u2 is not None assert u2.valid @@ -111,27 +112,25 @@ nuke_user(self.request, u'usera') - handle_auth = self.request.handle_auth - # do a LDAPAuth login (as a side effect, this autocreates the user profile): - u1 = handle_auth(None, username='usera', password='usera', login=True) + u1 = handle_login(self.request, None, username='usera', password='usera') assert u1 is not None assert u1.valid # now we kill the LDAP server: - self.ldap_env.slapd.stop() + #self.ldap_env.slapd.stop() # now try a MoinAuth login: # try the default password that worked in 1.7 up to rc1: - u2 = handle_auth(None, username='usera', password='{SHA}NotStored', login=True) + u2 = handle_login(self.request, None, username='usera', password='{SHA}NotStored') assert u2 is None # try using no password: - u2 = handle_auth(None, username='usera', password='', login=True) + u2 = handle_login(self.request, None, username='usera', password='') assert u2 is None # try using wrong password: - u2 = handle_auth(None, username='usera', password='wrong', login=True) + u2 = handle_login(self.request, None, username='usera', password='wrong') assert u2 is None @@ -221,7 +220,7 @@ handle_auth = self.request.handle_auth # authenticate user (with primary slapd): - u1 = handle_auth(None, username='usera', password='usera', login=True) + u1 = handle_login(self.request, None, username='usera', password='usera') assert u1 is not None assert u1.valid @@ -229,7 +228,7 @@ self.ldap_envs[0].slapd.stop() # try if we can still authenticate (with the second slapd): - u2 = handle_auth(None, username='usera', password='usera', login=True) + u2 = handle_login(self.request, None, username='usera', password='usera') assert u2 is not None assert u2.valid
--- a/MoinMoin/auth/http.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/auth/http.py Tue Sep 23 23:02:50 2008 +0200 @@ -14,7 +14,6 @@ """ from MoinMoin import config, user -from MoinMoin.request import request_twisted, request_cli, request_standalone from MoinMoin.auth import BaseAuth from base64 import decodestring @@ -36,45 +35,10 @@ if user_obj: return user_obj, True - # for standalone, request authorization and verify it, - # deny access if it isn't verified - if isinstance(request, request_standalone.Request): - request.setHttpHeader('WWW-Authenticate: Basic realm="MoinMoin"') - auth = request.headers.get('Authorization') - if auth: - auth = auth.split()[-1] - info = decodestring(auth).split(':', 1) - if len(info) == 2: - u = user.User(request, auth_username=info[0], password=info[1], - auth_method=self.name, auth_attribs=[]) - if not u: - request.makeForbidden(401, _('You need to log in.')) - # for Twisted, just check - elif isinstance(request, request_twisted.Request): - username = request.twistd.getUser().decode(config.charset) - password = request.twistd.getPassword().decode(config.charset) - # when using Twisted http auth, we use username and password from - # the moin user profile, so both can be changed by user. - u = user.User(request, auth_username=username, password=password, - auth_method=self.name, auth_attribs=()) - elif not isinstance(request, request_cli.Request): - env = request.env - auth_type = env.get('AUTH_TYPE', '') - if auth_type in ['Basic', 'Digest', 'NTLM', 'Negotiate', ]: - username = env.get('REMOTE_USER', '').decode(config.charset) - if auth_type in ('NTLM', 'Negotiate', ): - # converting to standard case so the user can even enter wrong case - # (added since windows does not distinguish between e.g. - # "Mike" and "mike") - username = username.split('\\')[-1] # split off domain e.g. - # from DOMAIN\user - # this "normalizes" the login name from {meier, Meier, MEIER} to Meier - # put a comment sign in front of next line if you don't want that: - username = username.title() - # when using http auth, we have external user name and password, - # we don't use the moin user profile for those attributes. - u = user.User(request, auth_username=username, - auth_method=self.name, auth_attribs=('name', 'password')) + authobj = request.authorization + if authobj: + u = user.User(request, auth_username=authobj.username, + auth_method=self.name, auth_attribs=('name', 'password')) if u and self.autocreate: u.create_or_update()
--- a/MoinMoin/auth/openidrp.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/auth/openidrp.py Tue Sep 23 23:02:50 2008 +0200 @@ -136,7 +136,7 @@ MoinOpenIDStore(request)) query = {} for key in request.form: - query[key] = request.form[key][0] + query[key] = request.form[key] current_url = get_multistage_continuation_url(request, self.name, {'oidstage': '1'}) info = oidconsumer.complete(query, current_url)
--- a/MoinMoin/auth/sslclientcert.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/auth/sslclientcert.py Tue Sep 23 23:02:50 2008 +0200 @@ -11,7 +11,6 @@ """ from MoinMoin import config, user -from MoinMoin.request import request_twisted from MoinMoin.auth import BaseAuth class SSLClientCertAuth(BaseAuth): @@ -35,60 +34,53 @@ def request(self, request, user_obj, **kw): u = None changed = False - # check if we are running Twisted - if isinstance(request, request_twisted.Request): - return user_obj, True # not supported if we run twisted - # Addendum: this seems to need quite some twisted insight and coding. - # A pointer i got on #twisted: divmod's vertex.sslverify - # If you really need this, feel free to implement and test it and - # submit a patch if it works. - else: - env = request.env - if env.get('SSL_CLIENT_VERIFY', 'FAILURE') == 'SUCCESS': + + env = request.environ + if env.get('SSL_CLIENT_VERIFY', 'FAILURE') == 'SUCCESS': - # check authority list if given - if self.authorities and env.get('SSL_CLIENT_I_DN_OU') in self.authorities: - return user_obj, True + # check authority list if given + if self.authorities and env.get('SSL_CLIENT_I_DN_OU') in self.authorities: + return user_obj, True - email_lower = None - if self.email_key: - email = env.get('SSL_CLIENT_S_DN_Email', '').decode(config.charset) - email_lower = email.lower() - commonname_lower = None - if self.name_key: - commonname = env.get('SSL_CLIENT_S_DN_CN', '').decode(config.charset) - commonname_lower = commonname.lower() - if email_lower or commonname_lower: - for uid in user.getUserList(request): - u = user.User(request, uid, - auth_method=self.name, auth_attribs=()) - if self.email_key and email_lower and u.email.lower() == email_lower: - u.auth_attribs = ('email', 'password') - if self.use_name and commonname_lower != u.name.lower(): - u.name = commonname - changed = True - u.auth_attribs = ('email', 'name', 'password') - break - if self.name_key and commonname_lower and u.name.lower() == commonname_lower: - u.auth_attribs = ('name', 'password') - if self.use_email and email_lower != u.email.lower(): - u.email = email - changed = True - u.auth_attribs = ('name', 'email', 'password') - break - else: - u = None - if u is None: - # user wasn't found, so let's create a new user object - u = user.User(request, name=commonname_lower, auth_username=commonname_lower, - auth_method=self.name) + email_lower = None + if self.email_key: + email = env.get('SSL_CLIENT_S_DN_Email', '').decode(config.charset) + email_lower = email.lower() + commonname_lower = None + if self.name_key: + commonname = env.get('SSL_CLIENT_S_DN_CN', '').decode(config.charset) + commonname_lower = commonname.lower() + if email_lower or commonname_lower: + for uid in user.getUserList(request): + u = user.User(request, uid, + auth_method=self.name, auth_attribs=()) + if self.email_key and email_lower and u.email.lower() == email_lower: + u.auth_attribs = ('email', 'password') + if self.use_name and commonname_lower != u.name.lower(): + u.name = commonname + changed = True + u.auth_attribs = ('email', 'name', 'password') + break + if self.name_key and commonname_lower and u.name.lower() == commonname_lower: u.auth_attribs = ('name', 'password') - if self.use_email: + if self.use_email and email_lower != u.email.lower(): u.email = email + changed = True u.auth_attribs = ('name', 'email', 'password') - elif user_obj and user_obj.auth_method == self.name: - user_obj.valid = False - return user_obj, False + break + else: + u = None + if u is None: + # user wasn't found, so let's create a new user object + u = user.User(request, name=commonname_lower, auth_username=commonname_lower, + auth_method=self.name) + u.auth_attribs = ('name', 'password') + if self.use_email: + u.email = email + u.auth_attribs = ('name', 'email', 'password') + elif user_obj and user_obj.auth_method == self.name: + user_obj.valid = False + return user_obj, False if u and self.autocreate: u.create_or_update(changed) if u and u.valid:
--- a/MoinMoin/config/multiconfig.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/config/multiconfig.py Tue Sep 23 23:02:50 2008 +0200 @@ -16,13 +16,15 @@ from MoinMoin import log logging = log.getLogger(__name__) -from MoinMoin import config, error, util, wikiutil +from MoinMoin import config, error, util, wikiutil, web from MoinMoin.auth import MoinAuth +import MoinMoin.auth as authmodule import MoinMoin.events as events from MoinMoin.events import PageChangedEvent, PageRenamedEvent from MoinMoin.events import PageDeletedEvent, PageCopiedEvent from MoinMoin.events import PageRevertedEvent, FileAttachedEvent from MoinMoin import session +import MoinMoin.web.session from MoinMoin.packages import packLine from MoinMoin.security import AccessControlList from MoinMoin.support.python_compatibility import set @@ -694,6 +696,8 @@ "See HelpOnSessions."), ('session_id_handler', DefaultExpression('session.MoinCookieSessionIDHandler()'), "Only used by the DefaultSessionHandler, see HelpOnSessions."), + ('session_service', DefaultExpression('web.session.FileSessionService()'), + "New session service (used by the new WSGI layer)"), ('cookie_secure', None, 'Use secure cookie. (None = auto-enable secure cookie for https, True = ever use secure cookie, False = never use secure cookie).'), ('cookie_domain', None,
--- a/MoinMoin/conftest.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/conftest.py Tue Sep 23 23:02:50 2008 +0200 @@ -30,6 +30,8 @@ sys.path.insert(0, str(moindir)) from MoinMoin.support.python_compatibility import set +from MoinMoin.web.request import TestRequest, Client +from MoinMoin.wsgiapp import application, init from MoinMoin._tests import maketestwiki, wikiconfig coverage_modules = set() @@ -55,7 +57,6 @@ coverage.erase() coverage.start() - py.test.config.addoptions('MoinMoin options', py.test.config.Option('-C', '--coverage', action='callback', callback=callback, help='Output information about code coverage (slow!)')) @@ -65,17 +66,12 @@ def init_test_request(given_config=None, static_state=[False]): - from MoinMoin.request import request_cli - from MoinMoin.user import User - from MoinMoin.formatter.text_html import Formatter as HtmlFormatter if not static_state[0]: maketestwiki.run(True) static_state[0] = True - request = request_cli.Request(given_config=given_config) - request.form = request.args = request.setup_args() - request.user = User(request) - request.html_formatter = HtmlFormatter(request) - request.formatter = request.html_formatter + request = TestRequest() + request.given_config = given_config + request = init(request) return request @@ -100,6 +96,7 @@ cls.request = init_test_request(given_config=cls.Config) else: cls.request = self.parent.request + cls.client = Client(application) super(MoinClassCollector, self).setup()
--- a/MoinMoin/converter/_tests/test_text_html_text_moin_wiki.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/converter/_tests/test_text_html_text_moin_wiki.py Tue Sep 23 23:02:50 2008 +0200 @@ -14,7 +14,7 @@ from MoinMoin.converter import text_html_text_moin_wiki as converter from MoinMoin.parser.text_moin_wiki import Parser from MoinMoin.formatter.text_gedit import Formatter -from MoinMoin.request import Clock +from MoinMoin.util.clock import Clock from MoinMoin.error import ConvertError convert = converter.convert
--- a/MoinMoin/converter/text_html_text_moin_wiki.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/converter/text_html_text_moin_wiki.py Tue Sep 23 23:02:50 2008 +0200 @@ -1205,7 +1205,7 @@ href = attrs.pop('href', None) css_class = attrs.get('class') - scriptname = self.request.getScriptname() + scriptname = self.request.script_root if scriptname == "": scriptname = "/"
--- a/MoinMoin/failure.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,208 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" MoinMoin failure - - Handle fatal errors by showing a message and debugging information. - - @copyright: 2004-2005 Nir Soffer <nirs@freeshell.org> - @license: GNU GPL, see COPYING for details. -""" -import sys, os -import traceback - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin.support import cgitb -from MoinMoin.error import ConfigurationError - - -class View(cgitb.View): - """ Display an error message and debugging information - - Additions to cgitb.View: - - Multiple tracebacks support - - Debugging information is shown only in debug mode - - Moin application information - - General help text and links - - Handle multiple paragraphs in exception message - - cgitb is heavily modified cgitb, but fully backward compatible with - the standard cgitb. It should not contain any moin specific code. - - cgitb was refactored to be easy to customize by applications - developers. This moin specific subclass is an example. - """ - debugInfoID = 'debug-info' - - def formatContent(self): - content = ( - self.script(), - self.formatStylesheet(), - self.formatTitle(), - self.formatMessage(), - self.formatButtons(), - self.formatDebugInfo(), - self.formatTextTraceback() - ) - return ''.join(content) - - def script(self): - return ''' -<script type="text/javascript"> -function toggleDebugInfo() { - var tb = document.getElementById('%s'); - if (tb == null) return; - tb.style.display = tb.style.display ? '' : 'none'; -} -</script> -''' % self.debugInfoID - - def stylesheet(self): - return cgitb.View.stylesheet(self) + """ -.cgitb .buttons {margin: 0.5em 0; padding: 5px 10px;} -.cgitb .buttons li {display: inline; margin: 0; padding: 0 0.25em;} -""" - - def formatMessage(self): - """ handle multiple paragraphs messages and add general help """ - f = self.formatter - text = [self.formatExceptionMessage(self.info)] - - if self.info[0] == ConfigurationError: - tbt = traceback.extract_tb(self.info[1].exceptions()[-1][2])[-1] - text.append( - f.paragraph('Error in your configuration file "%s"' - ' around line %d.' % tbt[:2])) - else: - text.append( - f.paragraph("If you want to report a bug, please save " - "this page and attach it to your bug report.")) - return ''.join(text) - - def formatButtons(self): - """ Add 'buttons' to the error dialog """ - f = self.formatter - buttons = [f.link('javascript:toggleDebugInfo()', - 'Show debugging information')] - if self.info[0] != ConfigurationError: - buttons.append( - f.link('http://moinmo.in/MoinMoinBugs', - 'Report bug')) - buttons.append( - f.link('http://moinmo.in/FrontPage', - 'Visit MoinMoin wiki')) - return f.list(buttons, {'class': 'buttons'}) - - def formatDebugInfo(self): - """ Put debugging information in a hidden div """ - attributes = {'id': self.debugInfoID} - info = [self.debugInfoHideScript(), - self.formatTraceback(), - self.formatSystemDetails(), ] - return self.formatter.section(''.join(info), attributes) - - def debugInfoHideScript(self): - """ Hide debug info for javascript enabled browsers """ - if self.debug: - return '' - return ''' -<script type="text/javascript">toggleDebugInfo()</script> -''' - - def formatTraceback(self): - return self.formatAllTracebacks(self.formatOneTraceback) - - def formatTextTraceback(self): - template = self.textTracebackTemplate() - return template % self.formatAllTracebacks(self.formatOneTextTraceback) - - def formatAllTracebacks(self, formatFuction): - """ Format multiple tracebacks using formatFunction """ - tracebacks = [] - for ttype, tvalue, tb in self.exceptions(): - if ttype is None: - break - tracebacks.append(formatFuction((ttype, tvalue, tb))) - del tb - return ''.join(tracebacks) - - def exceptions(self): - """ Return a list of exceptions info, starting at self.info """ - try: - return [self.info] + self.info[1].exceptions() - except AttributeError: - return [self.info] - - def applicationDetails(self): - """ Add MoinMoin details to system details """ - from MoinMoin import version - return ['MoinMoin: Release %s (%s)' % (version.release, - version.revision)] - - def formatExceptionMessage(self, info): - """ Handle multiple paragraphs in exception message """ - text = cgitb.View.exceptionMessage(self, info) - text = text.split('\n\n') - text = ''.join([self.formatter.paragraph(item) for item in text]) - return text - - -def handle(request, err): - """ Handle failures - - Display fancy error view, or fallback to simple text traceback - """ - if 'MOIN_DEBUG' in os.environ: - raise err - - savedError = sys.exc_info() - logging.exception('An exception occurred, URI was "%s".' % request.request_uri) - - try: - display = request.cfg.traceback_show # might fail if we have no cfg yet - except: - # default to True here to allow an admin setting up the wiki - # to see the errors made in the configuration file - display = True - - try: - debug = 'debug' in request.form - except: - debug = False - - try: - # try to output a nice html error page - handler = cgitb.Hook(file=request, display=display, viewClass=View, debug=debug) - handler.handle(savedError) - except: - # if that fails, log the cgitb problem ... - logging.exception('cgitb raised this exception') - # ... and try again with a simpler output method: - request.write('<pre>\n') - printTextException(request, savedError, display) - request.write('\nAdditionally cgitb raised this exception:\n') - printTextException(request, display=display) - request.write('</pre>\n') - - - -def printTextException(request, info=None, display=True): - """ Simple text exception that should never fail - - Print all exceptions in a composite error. - """ - if not display: - request.write("(Traceback display forbidden by configuration)\n") - return - from MoinMoin import wikiutil - if info is None: - info = sys.exc_info() - try: - exceptions = [info] + info[1].exceptions() - except AttributeError: - exceptions = [info] - for info in exceptions: - text = ''.join(traceback.format_exception(*info)) - text = wikiutil.escape(text) - request.write(text) -
--- a/MoinMoin/formatter/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/formatter/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -75,12 +75,12 @@ def startContent(self, content_id="content", **kw): if self.page: - self.request.begin_include(self.page.page_name) + self.request.uid_generator.begin(self.page.page_name) return "" def endContent(self): if self.page: - self.request.end_include() + self.request.uid_generator.end() return "" # Links ############################################################## @@ -94,7 +94,7 @@ return '' if not pagename and page: pagename = page.page_name - pagename = self.request.normalizePagename(pagename) + pagename = wikiutil.normalize_pagename(pagename, self.request.cfg) if pagename and pagename not in self.pagelinks: self.pagelinks.append(pagename) @@ -411,11 +411,11 @@ ''' Take an ID and make it unique in the current namespace. ''' - ns = self.request.include_id + ns = self.request.uid_generator.include_id if not ns is None: ns = self.sanitize_to_id(ns) id = self.sanitize_to_id(id) - id = self.request.make_unique_id(id, ns) + id = self.request.uid_generator(id, ns) return id def qualify_id(self, id): @@ -425,7 +425,7 @@ is suitable if the dot ('.') is valid in IDs for your formatter. ''' - ns = self.request.include_id + ns = self.request.uid_generator.include_id if not ns is None: ns = self.sanitize_to_id(ns) return '%s.%s' % (ns, id)
--- a/MoinMoin/formatter/text_docbook.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/formatter/text_docbook.py Tue Sep 23 23:02:50 2008 +0200 @@ -364,7 +364,7 @@ def url(self, on, url=None, css=None, **kw): if url and url.startswith("/"): # convert to absolute path: - url = "%s%s"%(self.request.getBaseURL(), url) + url = "%s%s"%(self.request.base_url, url) if not on: self._cleanupUlinkNode()
--- a/MoinMoin/formatter/text_html.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/formatter/text_html.py Tue Sep 23 23:02:50 2008 +0200 @@ -410,7 +410,7 @@ """ if hasattr(self, 'page'): - self.request.begin_include(self.page.page_name) + self.request.uid_generator.begin(self.page.page_name) result = [] # Use the content language @@ -434,7 +434,7 @@ result.append(self.anchordef('bottom')) result.append(self._close('div', newline=newline)) if hasattr(self, 'page'): - self.request.end_include() + self.request.uid_generator.end() return ''.join(result) def lang(self, on, lang_name):
--- a/MoinMoin/i18n/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/i18n/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -358,15 +358,12 @@ the request, normalizing to lower case. """ fallback = [] - accepted = request.http_accept_language + accepted = request.accept_languages if accepted: - # Extract the languages names from the string - accepted = accepted.split(',') - accepted = [lang.split(';')[0] for lang in accepted] # Add base language for each sub language. If the user specified # a sub language like "en-us", we will try to to provide it or # a least the base language "en" in this case. - for lang in accepted: + for lang, quality in accepted: lang = lang.lower() fallback.append(lang) if '-' in lang:
--- a/MoinMoin/logfile/editlog.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/logfile/editlog.py Tue Sep 23 23:02:50 2008 +0200 @@ -163,7 +163,7 @@ If `host` is None, it's read from request vars. """ if host is None: - host = request.remote_addr + host = request.remote_addr or '' if request.cfg.log_reverse_dns_lookups: import socket
--- a/MoinMoin/macro/AdvancedSearch.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/AdvancedSearch.py Tue Sep 23 23:02:50 2008 +0200 @@ -46,7 +46,7 @@ @param default: value if not present (default: '') @param escaped: if True, escape value so it can be used for html generation (default: False) """ - value = request.form.get(name, [default])[0] + value = request.form.get(name, default) if escaped: value = wikiutil.escape(value, quote=True) return value @@ -166,7 +166,7 @@ # the dialogue return f.rawHTML('\n'.join([ - u'<form method="get" action="%s/%s">' % (macro.request.getScriptname(), wikiutil.quoteWikinameURL(macro.request.formatter.page.page_name)), + u'<form method="get" action="%s">' % macro.request.href(macro.request.formatter.page.page_name), u'<div>', u'<input type="hidden" name="action" value="fullsearch">', u'<input type="hidden" name="advancedsearch" value="1">',
--- a/MoinMoin/macro/FullSearch.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/FullSearch.py Tue Sep 23 23:02:50 2008 +0200 @@ -43,7 +43,7 @@ """ _ = macro._ if 'value' in macro.form: - default = wikiutil.escape(macro.form["value"][0], quote=1) + default = wikiutil.escape(macro.form["value"], quote=1) else: default = '' @@ -67,7 +67,7 @@ # Format type = (type == "titlesearch") html = [ - u'<form method="get" action="%s/%s">' % (macro.request.getScriptname(), wikiutil.quoteWikinameURL(macro.request.formatter.page.page_name)), + u'<form method="get" action="%s">' % macro.request.href(macro.request.formatter.page.page_name), u'<div>', u'<input type="hidden" name="action" value="fullsearch">', u'<input type="hidden" name="titlesearch" value="%i">' % type,
--- a/MoinMoin/macro/MonthCalendar.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/MonthCalendar.py Tue Sep 23 23:02:50 2008 +0200 @@ -217,7 +217,7 @@ # does the url have calendar params (= somebody has clicked on prev/next links in calendar) ? if 'calparms' in macro.form: has_calparms = 1 # yes! - text2 = macro.form['calparms'][0] + text2 = macro.form['calparms'] try: cparmpagename, cparmyear, cparmmonth, cparmoffset, cparmoffset2, cparmheight6, cparmanniversary, cparmtemplate = \ parseargs(request, text2, thispage, currentyear, currentmonth, 0, 0, False, False, u'')
--- a/MoinMoin/macro/NewPage.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/NewPage.py Tue Sep 23 23:02:50 2008 +0200 @@ -76,7 +76,7 @@ # TODO: better abstract this using the formatter html = [ - u'<form class="macro" method="POST" action="%s/%s"><div>' % (self.request.getScriptname(), wikiutil.quoteWikinameURL(self.formatter.page.page_name)), + u'<form class="macro" method="POST" action="%s"><div>' % self.request.href(self.formatter.page.page_name), u'<input type="hidden" name="action" value="newpage">', u'<input type="hidden" name="parent" value="%s">' % wikiutil.escape(self.parent, 1), u'<input type="hidden" name="template" value="%s">' % wikiutil.escape(self.template, 1),
--- a/MoinMoin/macro/TableOfContents.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/TableOfContents.py Tue Sep 23 23:02:50 2008 +0200 @@ -46,12 +46,12 @@ def startContent(self, *args, **kw): res = FormatterBase.startContent(self, *args, **kw) - self.collected_headings.append([1, self.request.include_id, None]) + self.collected_headings.append([1, self.request.uid_generator.include_id, None]) return res def endContent(self): res = FormatterBase.endContent(self) - self.collected_headings.append([0, self.request.include_id, None]) + self.collected_headings.append([0, self.request.uid_generator.include_id, None]) return res def heading(self, on, depth, **kw): @@ -142,7 +142,7 @@ pname = macro.formatter.page.page_name - macro.request.push_unique_ids() + macro.request.uid_generator.push() macro.request._tocfm_collected_headings = [] macro.request._tocfm_orig_formatter = macro.formatter @@ -224,7 +224,7 @@ result.append(macro.formatter.number_list(0)) lastlvl -= 1 - macro.request.pop_unique_ids() + macro.request.uid_generator.pop() result.append(macro.formatter.div(0)) return ''.join(result)
--- a/MoinMoin/macro/TeudView.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/TeudView.py Tue Sep 23 23:02:50 2008 +0200 @@ -34,7 +34,7 @@ pagename = macro.formatter.page.page_name if 'module' in macro.form: - modname = macro.form["module"][0] + modname = macro.form["module"] try: obj = pydoc.locate(modname) except pydoc.ErrorDuringImport, value:
--- a/MoinMoin/macro/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -280,7 +280,7 @@ """ _ = self._ html = [ - u'<form method="get" action="%s/%s"><div>' % (self.request.getScriptname(), wikiutil.quoteWikinameURL(self.formatter.page.page_name)), + u'<form method="get" action="%s"><div>' % self.request.href(self.formatter.page.page_name), u'<div>', u'<input type="hidden" name="action" value="goto">', u'<input type="text" name="target" size="30">',
--- a/MoinMoin/macro/_tests/test_Action.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/_tests/test_Action.py Tue Sep 23 23:02:50 2008 +0200 @@ -26,7 +26,7 @@ m = make_macro(self.request, self.page) result = Action.macro_Action(m, 'raw') nuke_page(request, self.pagename) - expected = '<a href="./AutoCreatedMoinMoinTemporaryTestPageForAction?action=raw">raw</a>' + expected = '<a href="/AutoCreatedMoinMoinTemporaryTestPageForAction?action=raw">raw</a>' assert result == expected coverage_modules = ['MoinMoin.macro.Action']
--- a/MoinMoin/macro/_tests/test_EmbedObject.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/_tests/test_EmbedObject.py Tue Sep 23 23:02:50 2008 +0200 @@ -54,7 +54,7 @@ m = make_macro(self.request, self.page) filename = 'test.mpg' result = m.execute('EmbedObject', u'%s' % filename) - assert '<object data="./AutoCreatedMoinMoinTemporaryTestPageForEmbedObject?action=AttachFile&do=get&target=test.mpg"' in result + assert '<object data="/AutoCreatedMoinMoinTemporaryTestPageForEmbedObject?action=AttachFile&do=get&target=test.mpg"' in result assert 'align="middle"' in result assert 'value="transparent"' in result @@ -64,7 +64,7 @@ filename = 'test.mpg' height = '50 %' # also tests that space is allowed in there result = m.execute('EmbedObject', u'target=%s, height=%s' % (filename, height)) - assert '<object data="./AutoCreatedMoinMoinTemporaryTestPageForEmbedObject?action=AttachFile&do=get&target=test.mpg"' in result + assert '<object data="/AutoCreatedMoinMoinTemporaryTestPageForEmbedObject?action=AttachFile&do=get&target=test.mpg"' in result assert 'height="50%"' in result assert 'align="middle"' in result
--- a/MoinMoin/macro/_tests/test_StatsChart.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/macro/_tests/test_StatsChart.py Tue Sep 23 23:02:50 2008 +0200 @@ -34,19 +34,19 @@ def testStatsChart_useragents(self): """ macro StatsChart useragents test: 'tests useragents' and clean page scope cache """ result = self._test_macro(u'StatsChart', u'useragents') - expected = u'<form action="./AutoCreatedMoinMoinTemporaryTestPageStatsChart" method="GET"' + expected = u'<form action="/AutoCreatedMoinMoinTemporaryTestPageStatsChart" method="GET"' assert expected in result def testStatsChart_hitcounts(self): """ macro StatsChart hitcounts test: 'tests hitcounts' and clean page scope cache """ result = self._test_macro(u'StatsChart', u'hitcounts') - expected = u'<form action="./AutoCreatedMoinMoinTemporaryTestPageStatsChart" method="GET"' + expected = u'<form action="/AutoCreatedMoinMoinTemporaryTestPageStatsChart" method="GET"' assert expected in result def testStatsChart_languages(self): """ macro StatsChart languages test: 'tests languages' and clean page scope cache """ result = self._test_macro(u'StatsChart', u'hitcounts') - expected = u'<form action="./AutoCreatedMoinMoinTemporaryTestPageStatsChart" method="GET"' + expected = u'<form action="/AutoCreatedMoinMoinTemporaryTestPageStatsChart" method="GET"' assert expected in result coverage_modules = ['MoinMoin.stats']
--- a/MoinMoin/mail/mailimport.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/mail/mailimport.py Tue Sep 23 23:02:50 2008 +0200 @@ -17,7 +17,6 @@ from MoinMoin.action.AttachFile import add_attachment, AttachmentAlreadyExists from MoinMoin.Page import Page from MoinMoin.PageEditor import PageEditor -from MoinMoin.request.request_cli import Request as RequestCLI # python, at least up to 2.4, ships a broken parser for headers from MoinMoin.support.HeaderFixed import decode_header @@ -184,7 +183,7 @@ generate_summary = True pagename = pagename[1:].lstrip() - pagename = request.normalizePagename(pagename) + pagename = wikiutil.normalize_pagename(pagename, request.cfg) if choose_html and msg['html']: content = "{{{#!html\n%s\n}}}" % msg['html'].replace("}}}", "} } }") @@ -313,11 +312,12 @@ if __name__ == "__main__": if len(sys.argv) > 1: - url = sys.argv[1] + request_url = sys.argv[1] else: - url = 'localhost/' + request_url = 'localhost/' - request = RequestCLI(url=url) + from MoinMoin.web.contexts import ScriptContext + request = ScriptContext(url=request_url) try: import_mail_from_file(request, infile)
--- a/MoinMoin/packages.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/packages.py Tue Sep 23 23:02:50 2008 +0200 @@ -529,9 +529,8 @@ request_url = "localhost/" # Setup MoinMoin environment - from MoinMoin.request import request_cli - request = request_cli.Request(url=request_url) - request.form = request.args = request.setup_args() + from MoinMoin.web.contexts import ScriptContext + request = ScriptContext(url=request_url) package = ZipPackage(request, packagefile) if not package.isPackage():
--- a/MoinMoin/parser/_tests/test_text_creole.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/parser/_tests/test_text_creole.py Tue Sep 23 23:02:50 2008 +0200 @@ -369,14 +369,14 @@ needle = re.compile(text % r'(.+)') _tests = [ # test, expected - ('[[SomeNonExistentPage]]', '<a class="nonexistent" href="./SomeNonExistentPage">SomeNonExistentPage</a>'), - ('[[SomeNonExistentPage#anchor]]', '<a class="nonexistent" href="./SomeNonExistentPage#anchor">SomeNonExistentPage#anchor</a>'), - ('[[something]]', '<a class="nonexistent" href="./something">something</a>'), - ('[[some thing]]', '<a class="nonexistent" href="./some%20thing">some thing</a>'), - ('[[something|some text]]', '<a class="nonexistent" href="./something">some text</a>'), - ('[[../something]]', '<a class="nonexistent" href="./something">../something</a>'), - ('[[/something]]', '<a class="nonexistent" href="./%s/something">/something</a>' % PAGENAME), - ('[[something#anchor]]', '<a class="nonexistent" href="./something#anchor">something#anchor</a>'), + ('[[SomeNonExistentPage]]', '<a class="nonexistent" href="/SomeNonExistentPage">SomeNonExistentPage</a>'), + ('[[SomeNonExistentPage#anchor]]', '<a class="nonexistent" href="/SomeNonExistentPage#anchor">SomeNonExistentPage#anchor</a>'), + ('[[something]]', '<a class="nonexistent" href="/something">something</a>'), + ('[[some thing]]', '<a class="nonexistent" href="/some%20thing">some thing</a>'), + ('[[something|some text]]', '<a class="nonexistent" href="/something">some text</a>'), + ('[[../something]]', '<a class="nonexistent" href="/something">../something</a>'), + ('[[/something]]', '<a class="nonexistent" href="/%s/something">/something</a>' % PAGENAME), + ('[[something#anchor]]', '<a class="nonexistent" href="/something#anchor">something#anchor</a>'), ('[[MoinMoin:something]]', '<a class="interwiki" href="http://moinmo.in/something" title="MoinMoin">something</a>'), ('[[MoinMoin:something|some text]]', '<a class="interwiki" href="http://moinmo.in/something" title="MoinMoin">some text</a>'), ('[[MoinMoin:with space]]', '<a class="interwiki" href="http://moinmo.in/with%20space" title="MoinMoin">with space</a>'),
--- a/MoinMoin/parser/_tests/test_text_moin_wiki.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/parser/_tests/test_text_moin_wiki.py Tue Sep 23 23:02:50 2008 +0200 @@ -493,20 +493,20 @@ needle = re.compile(text % r'(.+)') _tests = [ # test, expected - ('SomeNonExistentPage', '<a class="nonexistent" href="./SomeNonExistentPage">SomeNonExistentPage</a>'), - ('SomeNonExistentPage#anchor', '<a class="nonexistent" href="./SomeNonExistentPage#anchor">SomeNonExistentPage#anchor</a>'), - ('[[something]]', '<a class="nonexistent" href="./something">something</a>'), - ('[[some thing]]', '<a class="nonexistent" href="./some%20thing">some thing</a>'), - ('[[something|some text]]', '<a class="nonexistent" href="./something">some text</a>'), - ('[[../something]]', '<a class="nonexistent" href="./something">../something</a>'), - ('[[/something]]', '<a class="nonexistent" href="./%s/something">/something</a>' % PAGENAME), - ('[[something#anchor]]', '<a class="nonexistent" href="./something#anchor">something#anchor</a>'), + ('SomeNonExistentPage', '<a class="nonexistent" href="/SomeNonExistentPage">SomeNonExistentPage</a>'), + ('SomeNonExistentPage#anchor', '<a class="nonexistent" href="/SomeNonExistentPage#anchor">SomeNonExistentPage#anchor</a>'), + ('[[something]]', '<a class="nonexistent" href="/something">something</a>'), + ('[[some thing]]', '<a class="nonexistent" href="/some%20thing">some thing</a>'), + ('[[something|some text]]', '<a class="nonexistent" href="/something">some text</a>'), + ('[[../something]]', '<a class="nonexistent" href="/something">../something</a>'), + ('[[/something]]', '<a class="nonexistent" href="/%s/something">/something</a>' % PAGENAME), + ('[[something#anchor]]', '<a class="nonexistent" href="/something#anchor">something#anchor</a>'), ('MoinMoin:something', '<a class="interwiki" href="http://moinmo.in/something" title="MoinMoin">something</a>'), ('[[MoinMoin:something|some text]]', '<a class="interwiki" href="http://moinmo.in/something" title="MoinMoin">some text</a>'), ('[[MoinMoin:with space]]', '<a class="interwiki" href="http://moinmo.in/with%20space" title="MoinMoin">with space</a>'), ('[[MoinMoin:with space|some text]]', '<a class="interwiki" href="http://moinmo.in/with%20space" title="MoinMoin">some text</a>'), # no interwiki: - ('[[ABC:n]]', '<a class="nonexistent" href="./ABC%3An">ABC:n</a>'), # finnish/swedish abbreviations / possessive + ('[[ABC:n]]', '<a class="nonexistent" href="/ABC%3An">ABC:n</a>'), # finnish/swedish abbreviations / possessive ('ABC:n', 'ABC:n'), # finnish/swedish abbreviations / possessive ('lowercase:nointerwiki', 'lowercase:nointerwiki'), ('[[http://google.com/|google]]', '<a class="http" href="http://google.com/">google</a>'),
--- a/MoinMoin/request/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1683 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - RequestBase Implementation - - @copyright: 2001-2003 Juergen Hermann <jh@web.de>, - 2003-2008 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" - -# Support for remote IP address detection when using (reverse) proxy (or even proxies). -# If you exactly KNOW which (reverse) proxies you can trust, put them into the list -# below, so we can determine the "outside" IP as your trusted proxies see it. - -proxies_trusted = [] # trust noone! -#proxies_trusted = ['127.0.0.1', ] # can be a list of multiple IPs - -from MoinMoin import log -logging = log.getLogger(__name__) - -def find_remote_addr(addrs): - """ Find the last remote IP address before it hits our reverse proxies. - The LAST address in the <addrs> list is the remote IP as detected by the server - (not taken from some x-forwarded-for header). - The FIRST address in the <addrs> list might be the client's IP - if noone cheats - and everyone supports x-f-f header. - - See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/ - - For debug loglevel, we log all <addrs>. - - TODO: refactor request code to first do some basic IP init, then load configuration, - TODO: then do proxy processing. - TODO: add wikiconfig configurability for proxies_trusted - TODO: later, make it possible to put multipe remote IP addrs into edit-log - """ - logging.debug("request.find_remote_addr: addrs == %r" % addrs) - if proxies_trusted: - result = [addr for addr in addrs if addr not in proxies_trusted] - if result: - return result[-1] # last IP before it hit our trusted (reverse) proxies - return addrs[-1] # this is a safe remote_addr, not taken from x-f-f header - - -import os, re, time, sys, cgi, StringIO -import Cookie -import traceback - -from MoinMoin.Page import Page -from MoinMoin import config, wikiutil, user, caching, error -from MoinMoin.config import multiconfig -from MoinMoin.support.python_compatibility import set -from MoinMoin.util import IsWin9x -from MoinMoin.util.clock import Clock -from MoinMoin import auth -from urllib import quote, quote_plus - -# umask setting -------------------------------------------------------- -def set_umask(new_mask=0777^config.umask): - """ Set the OS umask value (and ignore potential failures on OSes where - this is not supported). - Default: the bitwise inverted value of config.umask - """ - try: - old_mask = os.umask(new_mask) - except: - # maybe we are on win32? - pass - -# We do this at least once per Python process, when request is imported. -# If other software parts (like twistd's daemonize() function) set an -# unwanted umask, we have to call this again to set the correct one: -set_umask() - -# Exceptions ----------------------------------------------------------- - -class MoinMoinFinish(Exception): - """ Raised to jump directly to end of run() function, where finish is called """ - - -class HeadersAlreadySentException(Exception): - """ Is raised if the headers were already sent when emit_http_headers is called.""" - - -class RemoteClosedConnection(Exception): - """ Remote end closed connection during request """ - -# Utilities - -def cgiMetaVariable(header, scheme='http'): - """ Return CGI meta variable for header name - - e.g 'User-Agent' -> 'HTTP_USER_AGENT' - See http://www.faqs.org/rfcs/rfc3875.html section 4.1.18 - """ - var = '%s_%s' % (scheme, header) - return var.upper().replace('-', '_') - - -# Request Base ---------------------------------------------------------- - -class RequestBase(object): - """ A collection for all data associated with ONE request. """ - - # Defaults (used by sub classes) - http_accept_language = 'en' - server_name = 'localhost' - server_port = '80' - - # Extra headers we support. Both standalone and twisted store - # headers as lowercase. - moin_location = 'x-moin-location' - proxy_host = 'x-forwarded-host' # original host: header as seen by the proxy (e.g. wiki.example.org) - proxy_xff = 'x-forwarded-for' # list of original remote_addrs as seen by the proxies (e.g. <clientip>,<proxy1>,<proxy2>,...) - - def __init__(self, properties={}, given_config=None): - - # twistd's daemonize() overrides our umask, so we reset it here every - # request. we do it for all request types to avoid similar problems. - set_umask() - - self._finishers = [] - - self._auth_redirected = False - - # Decode values collected by sub classes - self.path_info = self.decodePagename(self.path_info) - - self.failed = 0 - self._available_actions = None - self._known_actions = None - - # Pages meta data that we collect in one request - self.pages = {} - - self.sent_headers = None - self.user_headers = [] - self.cacheable = 0 # may this output get cached by http proxies/caches? - self.http_caching_disabled = 0 # see disableHttpCaching() - self.page = None - self._dicts = None - - # session handling. users cannot rely on a session being - # created, but we should always set request.session - self.session = {} - - # setuid handling requires an attribute in the request - # that stores the real user - self._setuid_real_user = None - - # Check for dumb proxy requests - # TODO relying on request_uri will not work on all servers, especially - # not on external non-Apache servers - self.forbidden = False - if self.request_uri.startswith('http://'): - self.makeForbidden403() - - # Init - else: - self.writestack = [] - self.clock = Clock() - self.clock.start('total') - self.clock.start('base__init__') - # order is important here! - self.__dict__.update(properties) - try: - self._load_multi_cfg(given_config) - except error.NoConfigMatchedError: - self.makeForbidden(404, 'No wiki configuration matching the URL found!\r\n') - return - - self.isSpiderAgent = self.check_spider() - - # Set decode charsets. Input from the user is always in - # config.charset, which is the page charsets. Except - # path_info, which may use utf-8, and handled by decodePagename. - self.decode_charsets = [config.charset] - - if self.query_string.startswith('action=xmlrpc'): - self.args = {} - self.form = {} - self.action = 'xmlrpc' - self.rev = None - else: - try: - self.args = self.form = self.setup_args() - except UnicodeError: - self.makeForbidden(403, "The input you sent could not be understood.") - return - self.action = self.form.get('action', ['show'])[0] - try: - self.rev = int(self.form['rev'][0]) - except: - self.rev = None - - from MoinMoin.Page import RootPage - self.rootpage = RootPage(self) - - from MoinMoin.logfile import editlog - self.editlog = editlog.EditLog(self) - - from MoinMoin import i18n - self.i18n = i18n - i18n.i18n_init(self) - - # authentication might require translated forms, so - # have a try at guessing the language from the browser - lang = i18n.requestLanguage(self, try_user=False) - self.getText = lambda text, i18n=self.i18n, request=self, lang=lang, **kw: i18n.getText(text, request, lang, **kw) - - # session handler start, auth - self.parse_cookie() - user_obj = self.cfg.session_handler.start(self, self.cfg.session_id_handler) - shfinisher = lambda request: self.cfg.session_handler.finish(request, request.user, - self.cfg.session_id_handler) - self.add_finisher(shfinisher) - # set self.user even if _handle_auth_form raises an Exception - self.user = None - self.user = self._handle_auth_form(user_obj) - del user_obj - self.cfg.session_handler.after_auth(self, self.cfg.session_id_handler, self.user) - if not self.user: - self.user = user.User(self, auth_method='request:invalid') - - # setuid handling, check isSuperUser() because the user - # might have lost the permission between requests - if 'setuid' in self.session and self.user.isSuperUser(): - self._setuid_real_user = self.user - uid = self.session['setuid'] - self.user = user.User(self, uid, auth_method='setuid') - # set valid to True so superusers can even switch - # to disable accounts - self.user.valid = True - - if self.action != 'xmlrpc': - if not self.forbidden and self.isForbidden(): - self.makeForbidden403() - if not self.forbidden and self.surge_protect(): - self.makeUnavailable503() - - self.pragma = {} - self.mode_getpagelinks = 0 # is > 0 as long as we are in a getPageLinks call - self.parsePageLinks_running = {} # avoid infinite recursion by remembering what we are already running - - self.lang = i18n.requestLanguage(self) - # Language for content. Page content should use the wiki default lang, - # but generated content like search results should use the user language. - self.content_lang = self.cfg.language_default - self.getText = lambda text, i18n=self.i18n, request=self, lang=self.lang, **kv: i18n.getText(text, request, lang, **kv) - - self.reset() - - from MoinMoin.formatter.text_html import Formatter - self.html_formatter = Formatter(self) - self.formatter = self.html_formatter - - self.clock.stop('base__init__') - - def surge_protect(self, kick_him=False): - """ check if someone requesting too much from us, - if kick_him is True, we unconditionally blacklist the current user/ip - """ - limits = self.cfg.surge_action_limits - if not limits: - return False - - if self.remote_addr.startswith('127.'): # localnet - return False - - validuser = self.user.valid - current_id = validuser and self.user.name or self.remote_addr - current_action = self.action - - default_limit = limits.get('default', (30, 60)) - - now = int(time.time()) - surgedict = {} - surge_detected = False - - try: - # if we have common farm users, we could also use scope='farm': - cache = caching.CacheEntry(self, 'surgeprotect', 'surge-log', scope='wiki', use_encode=True) - if cache.exists(): - data = cache.content() - data = data.split("\n") - for line in data: - try: - id, t, action, surge_indicator = line.split("\t") - t = int(t) - maxnum, dt = limits.get(action, default_limit) - if t >= now - dt: - events = surgedict.setdefault(id, {}) - timestamps = events.setdefault(action, []) - timestamps.append((t, surge_indicator)) - except StandardError: - pass - - maxnum, dt = limits.get(current_action, default_limit) - events = surgedict.setdefault(current_id, {}) - timestamps = events.setdefault(current_action, []) - surge_detected = len(timestamps) > maxnum - - surge_indicator = surge_detected and "!" or "" - timestamps.append((now, surge_indicator)) - if surge_detected: - if len(timestamps) < maxnum * 2: - timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out - - if current_action not in ('cache', 'AttachFile', ): # don't add cache/AttachFile accesses to all or picture galleries will trigger SP - current_action = 'all' # put a total limit on user's requests - maxnum, dt = limits.get(current_action, default_limit) - events = surgedict.setdefault(current_id, {}) - timestamps = events.setdefault(current_action, []) - - if kick_him: # ban this guy, NOW - timestamps.extend([(now + self.cfg.surge_lockout_time, "!")] * (2 * maxnum)) - - surge_detected = surge_detected or len(timestamps) > maxnum - - surge_indicator = surge_detected and "!" or "" - timestamps.append((now, surge_indicator)) - if surge_detected: - if len(timestamps) < maxnum * 2: - timestamps.append((now + self.cfg.surge_lockout_time, surge_indicator)) # continue like that and get locked out - - data = [] - for id, events in surgedict.items(): - for action, timestamps in events.items(): - for t, surge_indicator in timestamps: - data.append("%s\t%d\t%s\t%s" % (id, t, action, surge_indicator)) - data = "\n".join(data) - cache.update(data) - except StandardError: - pass - - if surge_detected and validuser and self.user.auth_method in self.cfg.auth_methods_trusted: - logging.info("Trusted user %s would have triggered surge protection if not trusted." % self.user.name) - return False # do not subject trusted users to surge protection - - return surge_detected - - def getDicts(self): - """ Lazy initialize the dicts on the first access """ - if self._dicts is None: - from MoinMoin import wikidicts - dicts = wikidicts.GroupDict(self) - dicts.load_dicts() - self._dicts = dicts - return self._dicts - - def delDicts(self): - """ Delete the dicts, used by some tests """ - del self._dicts - self._dicts = None - - dicts = property(getDicts, None, delDicts) - - def _load_multi_cfg(self, given_config=None): - # protect against calling multiple times - if not hasattr(self, 'cfg'): - if given_config is None: - self.clock.start('load_multi_cfg') - self.cfg = multiconfig.getConfig(self.url) - self.clock.stop('load_multi_cfg') - else: - self.cfg = given_config('MoinMoin._tests.wikiconfig') # used for tests' TestConfig - - def setAcceptedCharsets(self, accept_charset): - """ Set accepted_charsets by parsing accept-charset header - - Set self.accepted_charsets to an ordered list based on http_accept_charset. - - Reference: http://www.w3.org/Protocols/rfc2616/rfc2616.txt - - TODO: currently no code use this value. - - @param accept_charset: accept-charset header - """ - charsets = [] - if accept_charset: - accept_charset = accept_charset.lower() - # Add iso-8859-1 if needed - if (not '*' in accept_charset and - 'iso-8859-1' not in accept_charset): - accept_charset += ',iso-8859-1' - - # Make a list, sorted by quality value, using Schwartzian Transform - # Create list of tuples (value, name) , sort, extract names - for item in accept_charset.split(','): - if ';' in item: - name, qval = item.split(';') - qval = 1.0 - float(qval.split('=')[1]) - else: - name, qval = item, 0 - charsets.append((qval, name)) - charsets.sort() - # Remove *, its not clear what we should do with it later - charsets = [name for qval, name in charsets if name != '*'] - - self.accepted_charsets = charsets - - def _setup_vars_from_std_env(self, env): - """ Set common request variables from CGI environment - - Parse a standard CGI environment as created by common web servers. - Reference: http://www.faqs.org/rfcs/rfc3875.html - - @param env: dict like object containing cgi meta variables - """ - # Values we can just copy - self.env = env - self.http_accept_language = env.get('HTTP_ACCEPT_LANGUAGE', self.http_accept_language) - self.server_name = env.get('SERVER_NAME', self.server_name) - self.server_port = env.get('SERVER_PORT', self.server_port) - self.saved_cookie = env.get('HTTP_COOKIE', '') - self.script_name = env.get('SCRIPT_NAME', '') - self.path_info = env.get('PATH_INFO', '') - self.query_string = env.get('QUERY_STRING', '') - self.request_method = env.get('REQUEST_METHOD', None) - self.remote_addr = env.get('REMOTE_ADDR', '') - self.http_user_agent = env.get('HTTP_USER_AGENT', '') - try: - self.content_length = int(env.get('CONTENT_LENGTH')) - except (TypeError, ValueError): - self.content_length = None - self.if_modified_since = env.get('If-modified-since') or env.get(cgiMetaVariable('If-modified-since')) - self.if_none_match = env.get('If-none-match') or env.get(cgiMetaVariable('If-none-match')) - - # REQUEST_URI is not part of CGI spec, but an addition of Apache. - self.request_uri = env.get('REQUEST_URI', '') - - # Values that need more work - self.setHttpReferer(env.get('HTTP_REFERER')) - self.setIsSSL(env) - self.setHost(env.get('HTTP_HOST')) - self.fixURI(env) - - self.setURL(env) - #self.debugEnvironment(env) - - def setHttpReferer(self, referer): - """ Set http_referer, making sure its ascii - - IE might send non-ascii value. - """ - value = '' - if referer: - value = unicode(referer, 'ascii', 'replace') - value = value.encode('ascii', 'replace') - self.http_referer = value - - def setIsSSL(self, env): - """ Set is_ssl - - @param env: dict like object containing cgi meta variables - """ - self.is_ssl = bool(env.get('SSL_PROTOCOL') or - env.get('SSL_PROTOCOL_VERSION') or - env.get('HTTPS') == 'on') - - def setHost(self, host=None): - """ Set http_host - - Create from server name and port if missing. Previous code - default to localhost. - """ - if not host: - port = '' - standardPort = ('80', '443')[self.is_ssl] - if self.server_port != standardPort: - port = ':' + self.server_port - host = self.server_name + port - self.http_host = host - - def fixURI(self, env): - """ Fix problems with script_name and path_info - - Handle the strange charset semantics on Windows and other non - posix systems. path_info is transformed into the system code - page by the web server. Additionally, paths containing dots let - most webservers choke. - - Broken environment variables in different environments: - path_info script_name - Apache1 X X PI does not contain dots - Apache2 X X PI is not encoded correctly - IIS X X path_info include script_name - Other ? - ? := Possible and even RFC-compatible. - - := Hopefully not. - - @param env: dict like object containing cgi meta variables - """ - # Fix the script_name when using Apache on Windows. - server_software = env.get('SERVER_SOFTWARE', '') - if os.name == 'nt' and 'Apache/' in server_software: - # Removes elements ending in '.' from the path. - self.script_name = '/'.join([x for x in self.script_name.split('/') - if not x.endswith('.')]) - - # Fix path_info - if os.name != 'posix' and self.request_uri != '': - # Try to recreate path_info from request_uri. - import urlparse - scriptAndPath = urlparse.urlparse(self.request_uri)[2] - path = scriptAndPath.replace(self.script_name, '', 1) - self.path_info = wikiutil.url_unquote(path, want_unicode=False) - elif os.name == 'nt': - # Recode path_info to utf-8 - path = wikiutil.decodeWindowsPath(self.path_info) - self.path_info = path.encode("utf-8") - - # Fix bug in IIS/4.0 when path_info contain script_name - if self.path_info.startswith(self.script_name): - self.path_info = self.path_info[len(self.script_name):] - - def setURL(self, env): - """ Set url, used to locate wiki config - - This is the place to manipulate url parts as needed. - - @param env: dict like object containing cgi meta variables or http headers. - """ - # proxy support - self.rewriteRemoteAddr(env) - self.rewriteHost(env) - - self.rewriteURI(env) - - if not self.request_uri: - self.request_uri = self.makeURI() - self.url = self.http_host + self.request_uri - - def rewriteHost(self, env): - """ Rewrite http_host transparently - - Get the proxy host using 'X-Forwarded-Host' header, added by - Apache 2 and other proxy software. - - TODO: Will not work for Apache 1 or others that don't add this header. - - TODO: If we want to add an option to disable this feature it - should be in the server script, because the config is not - loaded at this point, and must be loaded after url is set. - - @param env: dict like object containing cgi meta variables or http headers. - """ - proxy_host = (env.get(self.proxy_host) or - env.get(cgiMetaVariable(self.proxy_host))) - if proxy_host: - self.http_host = proxy_host - - def rewriteRemoteAddr(self, env): - """ Rewrite remote_addr transparently - - Get the proxy remote addr using 'X-Forwarded-For' header, added by - Apache 2 and other proxy software. - - TODO: Will not work for Apache 1 or others that don't add this header. - - TODO: If we want to add an option to disable this feature it - should be in the server script, because the config is not - loaded at this point, and must be loaded after url is set. - - @param env: dict like object containing cgi meta variables or http headers. - """ - xff = (env.get(self.proxy_xff) or - env.get(cgiMetaVariable(self.proxy_xff))) - if xff: - xff = [addr.strip() for addr in xff.split(',')] - xff.append(self.remote_addr) - self.remote_addr = find_remote_addr(xff) - - def rewriteURI(self, env): - """ Rewrite request_uri, script_name and path_info transparently - - Useful when running mod python or when running behind a proxy, - e.g run on localhost:8000/ and serve as example.com/wiki/. - - Uses private 'X-Moin-Location' header to set the script name. - This allow setting the script name when using Apache 2 - <location> directive:: - - <Location /my/wiki/> - RequestHeader set X-Moin-Location /my/wiki/ - </location> - - TODO: does not work for Apache 1 and others that do not allow - setting custom headers per request. - - @param env: dict like object containing cgi meta variables or http headers. - """ - location = (env.get(self.moin_location) or - env.get(cgiMetaVariable(self.moin_location))) - if location is None: - return - - scriptAndPath = self.script_name + self.path_info - location = location.rstrip('/') - self.script_name = location - - # This may happen when using mod_python - if scriptAndPath.startswith(location): - self.path_info = scriptAndPath[len(location):] - - # Recreate the URI from the modified parts - if self.request_uri: - self.request_uri = self.makeURI() - - def makeURI(self): - """ Return uri created from uri parts """ - uri = self.script_name + wikiutil.url_quote(self.path_info) - if self.query_string: - uri += '?' + self.query_string - return uri - - def splitURI(self, uri): - """ Return path and query splited from uri - - Just like CGI environment, the path is unquoted, the query is not. - """ - if '?' in uri: - path, query = uri.split('?', 1) - else: - path, query = uri, '' - return wikiutil.url_unquote(path, want_unicode=False), query - - def _handle_auth_form(self, user_obj): - username = self.form.get('name', [None])[0] - password = self.form.get('password', [None])[0] - oid = self.form.get('openid_identifier', [None])[0] - login = 'login' in self.form - logout = 'logout' in self.form - stage = self.form.get('stage', [None])[0] - return self.handle_auth(user_obj, attended=True, username=username, - password=password, login=login, logout=logout, - stage=stage, openid_identifier=oid) - - def handle_auth(self, user_obj, attended=False, **kw): - username = kw.get('username') - password = kw.get('password') - oid = kw.get('openid_identifier') - login = kw.get('login') - logout = kw.get('logout') - stage = kw.get('stage') - extra = { - 'cookie': self.cookie, - } - if login: - extra['attended'] = attended - extra['username'] = username - extra['password'] = password - extra['openid_identifier'] = oid - if stage: - extra['multistage'] = True - login_msgs = [] - self._login_multistage = None - - if logout and 'setuid' in self.session: - del self.session['setuid'] - return user_obj - - for authmethod in self.cfg.auth: - if logout: - user_obj, cont = authmethod.logout(self, user_obj, **extra) - elif login: - if stage and authmethod.name != stage: - continue - ret = authmethod.login(self, user_obj, **extra) - user_obj = ret.user_obj - cont = ret.continue_flag - if stage: - stage = None - del extra['multistage'] - if ret.multistage: - self._login_multistage = ret.multistage - self._login_multistage_name = authmethod.name - return user_obj - if ret.redirect_to: - nextstage = auth.get_multistage_continuation_url(self, authmethod.name) - url = ret.redirect_to - url = url.replace('%return_form', quote_plus(nextstage)) - url = url.replace('%return', quote(nextstage)) - self._auth_redirected = True - self.http_redirect(url) - return user_obj - msg = ret.message - if msg and not msg in login_msgs: - login_msgs.append(msg) - else: - user_obj, cont = authmethod.request(self, user_obj, **extra) - if not cont: - break - - self._login_messages = login_msgs - return user_obj - - def handle_jid_auth(self, jid): - return user.get_by_jabber_id(self, jid) - - def parse_cookie(self): - try: - self.cookie = Cookie.SimpleCookie(self.saved_cookie) - except Cookie.CookieError: - self.cookie = None - - def reset(self): - """ Reset request state. - - Called after saving a page, before serving the updated - page. Solves some practical problems with request state - modified during saving. - - """ - # This is the content language and has nothing to do with - # The user interface language. The content language can change - # during the rendering of a page by lang macros - self.current_lang = self.cfg.language_default - - # caches unique ids - self.init_unique_ids() - - if hasattr(self, "_fmt_hd_counters"): - del self._fmt_hd_counters - - def loadTheme(self, theme_name): - """ Load the Theme to use for this request. - - @param theme_name: the name of the theme - @type theme_name: str - @rtype: int - @return: success code - 0 on success - 1 if user theme could not be loaded, - 2 if a hard fallback to modern theme was required. - """ - fallback = 0 - if theme_name == "<default>": - theme_name = self.cfg.theme_default - - try: - Theme = wikiutil.importPlugin(self.cfg, 'theme', theme_name, 'Theme') - except wikiutil.PluginMissingError: - fallback = 1 - try: - Theme = wikiutil.importPlugin(self.cfg, 'theme', self.cfg.theme_default, 'Theme') - except wikiutil.PluginMissingError: - fallback = 2 - from MoinMoin.theme.modern import Theme - - self.theme = Theme(self) - return fallback - - def setContentLanguage(self, lang): - """ Set the content language, used for the content div - - Actions that generate content in the user language, like search, - should set the content direction to the user language before they - call send_title! - """ - self.content_lang = lang - self.current_lang = lang - - def getPragma(self, key, defval=None): - """ Query a pragma value (#pragma processing instruction) - - Keys are not case-sensitive. - """ - return self.pragma.get(key.lower(), defval) - - def setPragma(self, key, value): - """ Set a pragma value (#pragma processing instruction) - - Keys are not case-sensitive. - """ - self.pragma[key.lower()] = value - - def getPathinfo(self): - """ Return the remaining part of the URL. """ - return self.path_info - - def getScriptname(self): - """ Return the scriptname part of the URL ('/path/to/my.cgi'). """ - if self.script_name == '/': - return '' - return self.script_name - - def getKnownActions(self): - """ Create a dict of avaiable actions - - Return cached version if avaiable. - - @rtype: dict - @return: dict of all known actions - """ - try: - self.cfg.cache.known_actions # check - except AttributeError: - from MoinMoin import action - self.cfg.cache.known_actions = set(action.getNames(self.cfg)) - - # Return a copy, so clients will not change the set. - return self.cfg.cache.known_actions.copy() - - def getAvailableActions(self, page): - """ Get list of avaiable actions for this request - - The dict does not contain actions that starts with lower case. - Themes use this dict to display the actions to the user. - - @param page: current page, Page object - @rtype: dict - @return: dict of avaiable actions - """ - if self._available_actions is None: - # some actions might make sense for non-existing pages, so we just - # require read access here. Can be later refined to some action - # specific check: - if not self.user.may.read(page.page_name): - return [] - - # Filter non ui actions (starts with lower case letter) - actions = self.getKnownActions() - actions = [action for action in actions if not action[0].islower()] - - # Filter wiki excluded actions - actions = [action for action in actions if not action in self.cfg.actions_excluded] - - # Filter actions by page type, acl and user state - excluded = [] - if ((page.isUnderlayPage() and not page.isStandardPage()) or - not self.user.may.write(page.page_name) or - not self.user.may.delete(page.page_name)): - # Prevent modification of underlay only pages, or pages - # the user can't write and can't delete - excluded = [u'RenamePage', u'DeletePage', ] # AttachFile must NOT be here! - actions = [action for action in actions if not action in excluded] - - self._available_actions = set(actions) - - # Return a copy, so clients will not change the dict. - return self._available_actions.copy() - - def redirectedOutput(self, function, *args, **kw): - """ Redirect output during function, return redirected output """ - buf = StringIO.StringIO() - self.redirect(buf) - try: - function(*args, **kw) - finally: - self.redirect() - text = buf.getvalue() - buf.close() - return text - - def redirect(self, file=None): - """ Redirect output to file, or restore saved output """ - if file: - self.writestack.append(self.write) - self.write = file.write - else: - self.write = self.writestack.pop() - - def log(self, msg): - """ DEPRECATED - Log msg to logging framework - Please call logging.info(...) directly! - """ - msg = msg.strip() - # Encode unicode msg - if isinstance(msg, unicode): - msg = msg.encode(config.charset) - logging.info(msg) - - def timing_log(self, start, action): - """ Log to timing log (for performance analysis) """ - indicator = '' - if start: - total = "vvv" - else: - self.clock.stop('total') # make sure it is stopped - total_secs = self.clock.timings['total'] - # we add some stuff that is easy to grep when searching for peformance problems: - if total_secs > 50: - indicator += '!4!' - elif total_secs > 20: - indicator += '!3!' - elif total_secs > 10: - indicator += '!2!' - elif total_secs > 2: - indicator += '!1!' - total = self.clock.value('total') - # use + for existing pages, - for non-existing pages - if self.page is not None: - indicator += self.page.exists() and '+' or '-' - if self.isSpiderAgent: - indicator += "B" - - pid = os.getpid() - msg = 'Timing %5d %-6s %4s %-10s %s\n' % (pid, total, indicator, action, self.url) - logging.info(msg) - - def send_file(self, fileobj, bufsize=8192, do_flush=False): - """ Send a file to the output stream. - - @param fileobj: a file-like object (supporting read, close) - @param bufsize: size of chunks to read/write - @param do_flush: call flush after writing? - """ - while True: - buf = fileobj.read(bufsize) - if not buf: - break - self.write(buf) - if do_flush: - self.flush() - - def write(self, *data): - """ Write to output stream. """ - raise NotImplementedError - - def encode(self, data): - """ encode data (can be both unicode strings and strings), - preparing for a single write() - """ - wd = [] - for d in data: - try: - if isinstance(d, unicode): - # if we are REALLY sure, we can use "strict" - d = d.encode(config.charset, 'replace') - elif d is None: - continue - wd.append(d) - except UnicodeError: - logging.error("Unicode error on: %s" % repr(d)) - return ''.join(wd) - - def decodePagename(self, name): - """ Decode path, possibly using non ascii characters - - Does not change the name, only decode to Unicode. - - First split the path to pages, then decode each one. This enables - us to decode one page using config.charset and another using - utf-8. This situation happens when you try to add to a name of - an existing page. - - See http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1 - - @param name: page name, string - @rtype: unicode - @return decoded page name - """ - # Split to pages and decode each one - pages = name.split('/') - decoded = [] - for page in pages: - # Recode from utf-8 into config charset. If the path - # contains user typed parts, they are encoded using 'utf-8'. - if config.charset != 'utf-8': - try: - page = unicode(page, 'utf-8', 'strict') - # Fit data into config.charset, replacing what won't - # fit. Better have few "?" in the name than crash. - page = page.encode(config.charset, 'replace') - except UnicodeError: - pass - - # Decode from config.charset, replacing what can't be decoded. - page = unicode(page, config.charset, 'replace') - decoded.append(page) - - # Assemble decoded parts - name = u'/'.join(decoded) - return name - - def normalizePagename(self, name): - """ Normalize page name - - Prevent creating page names with invisible characters or funny - whitespace that might confuse the users or abuse the wiki, or - just does not make sense. - - Restrict even more group pages, so they can be used inside acl lines. - - @param name: page name, unicode - @rtype: unicode - @return: decoded and sanitized page name - """ - # Strip invalid characters - name = config.page_invalid_chars_regex.sub(u'', name) - - # Split to pages and normalize each one - pages = name.split(u'/') - normalized = [] - for page in pages: - # Ignore empty or whitespace only pages - if not page or page.isspace(): - continue - - # Cleanup group pages. - # Strip non alpha numeric characters, keep white space - if wikiutil.isGroupPage(self, page): - page = u''.join([c for c in page - if c.isalnum() or c.isspace()]) - - # Normalize white space. Each name can contain multiple - # words separated with only one space. Split handle all - # 30 unicode spaces (isspace() == True) - page = u' '.join(page.split()) - - normalized.append(page) - - # Assemble components into full pagename - name = u'/'.join(normalized) - return name - - def read(self, n): - """ Read n bytes from input stream. """ - raise NotImplementedError - - def flush(self): - """ Flush output stream. """ - pass - - def check_spider(self): - """ check if the user agent for current request is a spider/bot """ - isSpider = False - ua = self.getUserAgent() - if ua and self.cfg.cache.ua_spiders: - isSpider = self.cfg.cache.ua_spiders.search(ua) is not None - return isSpider - - def isForbidden(self): - """ check for web spiders and refuse anything except viewing """ - forbidden = 0 - # we do not have a parsed query string here, so we can just do simple matching - qs = self.query_string - action = self.action - if ((qs != '' or self.request_method != 'GET') and - action != 'rss_rc' and - # allow spiders to get attachments and do 'show' - not (action == 'AttachFile' and 'do=get' in qs) and - action != 'show' and - action != 'sitemap' - ): - forbidden = self.isSpiderAgent - - if not forbidden and self.cfg.hosts_deny: - ip = self.remote_addr - for host in self.cfg.hosts_deny: - if host[-1] == '.' and ip.startswith(host): - forbidden = 1 - logging.debug("hosts_deny (net): %s" % str(forbidden)) - break - if ip == host: - forbidden = 1 - logging.debug("hosts_deny (ip): %s" % str(forbidden)) - break - return forbidden - - def setup_args(self): - """ Return args dict - First, we parse the query string (usually this is used in GET methods, - but TwikiDraw uses ?action=AttachFile&do=savedrawing plus posted stuff). - Second, we update what we got in first step by the stuff we get from - the form (or by a POST). We invoke _setup_args_from_cgi_form to handle - possible file uploads. - """ - args = cgi.parse_qs(self.query_string, keep_blank_values=1) - args = self.decodeArgs(args) - # if we have form data (in a POST), those override the stuff we already have: - if self.request_method == 'POST': - postargs = self._setup_args_from_cgi_form() - args.update(postargs) - return args - - def _setup_args_from_cgi_form(self, form=None): - """ Return args dict from a FieldStorage - - Create the args from a given form. Each key contain a list of values. - This method usually gets overridden in classes derived from this - it - is their task to call this method with an appropriate form parameter. - - @param form: a cgi.FieldStorage - @rtype: dict - @return: dict with form keys, each contains a list of values - """ - args = {} - for key in form: - values = form[key] - if not isinstance(values, list): - values = [values] - fixedResult = [] - for item in values: - if isinstance(item, cgi.FieldStorage) and item.filename: - fixedResult.append(item.file) # open data tempfile - # Save upload file name in a separate key - args[key + '__filename__'] = item.filename - else: - fixedResult.append(item.value) - args[key] = fixedResult - - return self.decodeArgs(args) - - def decodeArgs(self, args): - """ Decode args dict - - Decoding is done in a separate path because it is reused by - other methods and sub classes. - """ - decode = wikiutil.decodeUserInput - result = {} - for key in args: - if key + '__filename__' in args: - # Copy file data as is - result[key] = args[key] - elif key.endswith('__filename__'): - result[key] = decode(args[key], self.decode_charsets) - else: - result[key] = [decode(value, self.decode_charsets) for value in args[key]] - return result - - def getBaseURL(self): - """ Return a fully qualified URL to this script. """ - return self.getQualifiedURL(self.getScriptname()) - - def getQualifiedURL(self, uri=''): - """ Return an absolute URL starting with schema and host. - - Already qualified urls are returned unchanged. - - @param uri: server rooted uri e.g /scriptname/pagename. - It must start with a slash. Must be ascii and url encoded. - """ - import urlparse - scheme = urlparse.urlparse(uri)[0] - if scheme: - return uri - - scheme = ('http', 'https')[self.is_ssl] - result = "%s://%s%s" % (scheme, self.http_host, uri) - - # This might break qualified urls in redirects! - # e.g. mapping 'http://netloc' -> '/' - return wikiutil.mapURL(self, result) - - def getUserAgent(self): - """ Get the user agent. """ - return self.http_user_agent - - def makeForbidden(self, resultcode, msg): - statusmsg = { - 401: 'Authorization required', - 403: 'FORBIDDEN', - 404: 'Not found', - 503: 'Service unavailable', - } - headers = [ - 'Status: %d %s' % (resultcode, statusmsg[resultcode]), - 'Content-Type: text/plain; charset=utf-8' - ] - # when surge protection triggered, tell bots to come back later... - if resultcode == 503: - headers.append('Retry-After: %d' % self.cfg.surge_lockout_time) - self.emit_http_headers(headers) - self.write(msg) - self.forbidden = True - - def makeForbidden403(self): - self.makeForbidden(403, 'You are not allowed to access this!\r\n') - - def makeUnavailable503(self): - self.makeForbidden(503, "Warning:\r\n" - "You triggered the wiki's surge protection by doing too many requests in a short time.\r\n" - "Please make a short break reading the stuff you already got.\r\n" - "When you restart doing requests AFTER that, slow down or you might get locked out for a longer time!\r\n") - - def initTheme(self): - """ Set theme - forced theme, user theme or wiki default """ - if self.cfg.theme_force: - theme_name = self.cfg.theme_default - else: - theme_name = self.user.theme_name - self.loadTheme(theme_name) - - def _try_redirect_spaces_page(self, pagename): - if '_' in pagename and not self.page.exists(): - pname = pagename.replace('_', ' ') - pg = Page(self, pname) - if pg.exists(): - url = pg.url(self) - self.http_redirect(url) - return True - return False - - def run(self): - # Exit now if __init__ failed or request is forbidden - if self.failed or self.forbidden or self._auth_redirected: - # Don't sleep() here, it binds too much of our resources! - return self.finish() - - _ = self.getText - self.clock.start('run') - - self.initTheme() - - action_name = self.action - if self.cfg.log_timing: - self.timing_log(True, action_name) - - if action_name == 'xmlrpc': - from MoinMoin import xmlrpc - if self.query_string == 'action=xmlrpc': - xmlrpc.xmlrpc(self) - elif self.query_string == 'action=xmlrpc2': - xmlrpc.xmlrpc2(self) - if self.cfg.log_timing: - self.timing_log(False, action_name) - return self.finish() - - # parse request data - try: - # The last component in path_info is the page name, if any - path = self.getPathinfo() - - # we can have all action URLs like this: /action/ActionName/PageName?action=ActionName&... - # this is just for robots.txt being able to forbid them for crawlers - prefix = self.cfg.url_prefix_action - if prefix is not None: - prefix = '/%s/' % prefix # e.g. '/action/' - if path.startswith(prefix): - # remove prefix and action name - path = path[len(prefix):] - action, path = (path.split('/', 1) + ['', ''])[:2] - path = '/' + path - - if path.startswith('/'): - pagename = self.normalizePagename(path) - else: - pagename = None - - # need to inform caches that content changes based on: - # * cookie (even if we aren't sending one now) - # * User-Agent (because a bot might be denied and get no content) - # * Accept-Language (except if moin is told to ignore browser language) - if self.cfg.language_ignore_browser: - self.setHttpHeader("Vary: Cookie,User-Agent") - else: - self.setHttpHeader("Vary: Cookie,User-Agent,Accept-Language") - - # Handle request. We have these options: - # 1. jump to page where user left off - if not pagename and self.user.remember_last_visit and action_name == 'show': - pagetrail = self.user.getTrail() - if pagetrail: - # Redirect to last page visited - last_visited = pagetrail[-1] - wikiname, pagename = wikiutil.split_interwiki(last_visited) - if wikiname != 'Self': - wikitag, wikiurl, wikitail, error = wikiutil.resolve_interwiki(self, wikiname, pagename) - url = wikiurl + wikiutil.quoteWikinameURL(wikitail) - else: - url = Page(self, pagename).url(self) - else: - # Or to localized FrontPage - url = wikiutil.getFrontPage(self).url(self) - self.http_redirect(url) - return self.finish() - - # 2. handle action - else: - # pagename could be empty after normalization e.g. '///' -> '' - # Use localized FrontPage if pagename is empty - if not pagename: - self.page = wikiutil.getFrontPage(self) - else: - self.page = Page(self, pagename) - if self._try_redirect_spaces_page(pagename): - return self.finish() - - msg = None - # Complain about unknown actions - if not action_name in self.getKnownActions(): - msg = _("Unknown action %(action_name)s.") % { - 'action_name': wikiutil.escape(action_name), } - - # Disallow non available actions - elif action_name[0].isupper() and not action_name in self.getAvailableActions(self.page): - msg = _("You are not allowed to do %(action_name)s on this page.") % { - 'action_name': wikiutil.escape(action_name), } - if not self.user.valid: - # Suggest non valid user to login - msg += " " + _("Login and try again.") - - if msg: - self.theme.add_msg(msg, "error") - self.page.send_page() - # Try action - else: - from MoinMoin import action - handler = action.getHandler(self, action_name) - if handler is None: - msg = _("You are not allowed to do %(action_name)s on this page.") % { - 'action_name': wikiutil.escape(action_name), } - if not self.user.valid: - # Suggest non valid user to login - msg += " " + _("Login and try again.") - self.theme.add_msg(msg, "error") - self.page.send_page() - else: - handler(self.page.page_name, self) - - # every action that didn't use to raise MoinMoinFinish must call this now: - # self.theme.send_closing_html() - - except MoinMoinFinish: - pass - except RemoteClosedConnection: - # at least clean up - pass - except SystemExit: - raise # fcgi uses this to terminate a thread - except Exception, err: - try: - # nothing we can do about further failures! - self.fail(err) - except: - pass - - if self.cfg.log_timing: - self.timing_log(False, action_name) - - return self.finish() - - def http_redirect(self, url): - """ Redirect to a fully qualified, or server-rooted URL - - @param url: relative or absolute url, ascii using url encoding. - """ - url = self.getQualifiedURL(url) - self.emit_http_headers(["Status: 302 Found", "Location: %s" % url]) - - def emit_http_headers(self, more_headers=[], testing=False): - """ emit http headers after some preprocessing / checking - - Makes sure we only emit headers once. - Encodes to ASCII if it gets unicode headers. - Make sure we have exactly one Content-Type and one Status header. - Make sure Status header string begins with a integer number. - - For emitting (testing == False), it calls the server specific - _emit_http_headers method. For testing, it returns the result. - - @param more_headers: list of additional header strings - @param testing: set to True by test code - """ - user_headers = self.user_headers - self.user_headers = [] - tracehere = ''.join(traceback.format_stack()[:-1]) - all_headers = [(hdr, tracehere) for hdr in more_headers] + user_headers - - if self.sent_headers: - # Send headers only once - logging.error("Attempt to send headers twice!") - logging.error("First attempt:\n%s" % self.sent_headers) - logging.error("Second attempt:\n%s" % tracehere) - raise HeadersAlreadySentException("emit_http_headers has already been called before!") - else: - self.sent_headers = tracehere - - # assemble dict of http headers - headers = {} - traces = {} - for header, trace in all_headers: - if isinstance(header, unicode): - header = header.encode('ascii') - key, value = header.split(':', 1) - lkey = key.lower() - value = value.lstrip() - if lkey in headers: - if lkey in ['vary', 'cache-control', 'content-language', ]: - # these headers (list might be incomplete) allow multiple values - # that can be merged into a comma separated list - headers[lkey] = headers[lkey][0], '%s, %s' % (headers[lkey][1], value) - traces[lkey] = trace - else: - logging.warning("Duplicate http header: %r (ignored)" % header) - logging.warning("Header added first at:\n%s" % traces[lkey]) - logging.warning("Header added again at:\n%s" % trace) - else: - headers[lkey] = (key, value) - traces[lkey] = trace - - if 'content-type' not in headers: - headers['content-type'] = ('Content-type', 'text/html; charset=%s' % config.charset) - - if 'status' not in headers: - headers['status'] = ('Status', '200 OK') - else: - # check if we got a valid status - try: - status = headers['status'][1] - int(status.split(' ', 1)[0]) - except: - logging.error("emit_http_headers called with invalid header Status: %r" % status) - headers['status'] = ('Status', '500 Server Error - invalid status header') - - header_format = '%s: %s' - st_header = header_format % headers['status'] - del headers['status'] - ct_header = header_format % headers['content-type'] - del headers['content-type'] - - headers = [header_format % kv_tuple for kv_tuple in headers.values()] # make a list of strings - headers = [st_header, ct_header] + headers # do NOT change order! - if not testing: - self._emit_http_headers(headers) - else: - return headers - - def _emit_http_headers(self, headers): - """ server specific method to emit http headers. - - @param headers: a list of http header strings in this FIXED order: - 1. status header (always present and valid, e.g. "200 OK") - 2. content type header (always present) - 3. other headers (optional) - """ - raise NotImplementedError - - def setHttpHeader(self, header): - """ Save header for later send. - - Attention: although we use a list here, some implementations use a dict, - thus multiple calls with the same header type do NOT work in the end! - """ - # save a traceback with the header for duplicate bug reporting - self.user_headers.append((header, ''.join(traceback.format_stack()[:-1]))) - - def fail(self, err): - """ Fail when we can't continue - - Send 500 status code with the error name. Reference: - http://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1.1 - - Log the error, then let failure module handle it. - - @param err: Exception instance or subclass. - """ - self.failed = 1 # save state for self.run() - # we should not generate the headers two times - if not self.sent_headers: - self.emit_http_headers(['Status: 500 MoinMoin Internal Error']) - from MoinMoin import failure - failure.handle(self, err) - - def make_unique_id(self, base, namespace=None): - """ - Generates a unique ID using a given base name. Appends a running count to the base. - - Needs to stay deterministic! - - @param base: the base of the id - @type base: unicode - @param namespace: the namespace for the ID, used when including pages - - @returns: a unique (relatively to the namespace) ID - @rtype: unicode - """ - if not isinstance(base, unicode): - base = unicode(str(base), 'ascii', 'ignore') - if not namespace in self._page_ids: - self._page_ids[namespace] = {} - count = self._page_ids[namespace].get(base, -1) + 1 - self._page_ids[namespace][base] = count - if not count: - return base - return u'%s-%d' % (base, count) - - def init_unique_ids(self): - '''Initialise everything needed for unique IDs''' - self._unique_id_stack = [] - self._page_ids = {None: {}} - self.include_id = None - self._include_stack = [] - - def push_unique_ids(self): - ''' - Used by the TOC macro, this ensures that the ID namespaces - are reset to the status when the current include started. - This guarantees that doing the ID enumeration twice results - in the same results, on any level. - ''' - self._unique_id_stack.append((self._page_ids, self.include_id)) - self.include_id, pids = self._include_stack[-1] - # make a copy of the containing ID namespaces, that is to say - # go back to the level we had at the previous include - self._page_ids = {} - for namespace in pids: - self._page_ids[namespace] = pids[namespace].copy() - - def pop_unique_ids(self): - ''' - Used by the TOC macro to reset the ID namespaces after - having parsed the page for TOC generation and after - printing the TOC. - ''' - self._page_ids, self.include_id = self._unique_id_stack.pop() - - def begin_include(self, base): - ''' - Called by the formatter when a document begins, which means - that include causing nested documents gives us an include - stack in self._include_id_stack. - ''' - pids = {} - for namespace in self._page_ids: - pids[namespace] = self._page_ids[namespace].copy() - self._include_stack.append((self.include_id, pids)) - self.include_id = self.make_unique_id(base) - # if it's the page name then set it to None so we don't - # prepend anything to IDs, but otherwise keep it. - if self.page and self.page.page_name == self.include_id: - self.include_id = None - - def end_include(self): - ''' - Called by the formatter when a document ends, restores - the current include ID to the previous one and discards - the page IDs state we kept around for push_unique_ids(). - ''' - self.include_id, pids = self._include_stack.pop() - - def httpDate(self, when=None, rfc='1123'): - """ Returns http date string, according to rfc2068 - - See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-3.3 - - A http 1.1 server should use only rfc1123 date, but cookie's - "expires" field should use the older obsolete rfc850 date. - - Note: we can not use strftime() because that honors the locale - and rfc2822 requires english day and month names. - - We can not use email.Utils.formatdate because it formats the - zone as '-0000' instead of 'GMT', and creates only rfc1123 - dates. This is a modified version of email.Utils.formatdate - from Python 2.4. - - @param when: seconds from epoch, as returned by time.time() - @param rfc: conform to rfc ('1123' or '850') - @rtype: string - @return: http date conforming to rfc1123 or rfc850 - """ - if when is None: - when = time.time() - now = time.gmtime(when) - month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', - 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.tm_mon - 1] - if rfc == '1123': - day = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now.tm_wday] - date = '%02d %s %04d' % (now.tm_mday, month, now.tm_year) - elif rfc == '850': - day = ["Monday", "Tuesday", "Wednesday", "Thursday", - "Friday", "Saturday", "Sunday"][now.tm_wday] - date = '%02d-%s-%s' % (now.tm_mday, month, str(now.tm_year)[-2:]) - else: - raise ValueError("Invalid rfc value: %s" % rfc) - - return '%s, %s %02d:%02d:%02d GMT' % (day, date, now.tm_hour, - now.tm_min, now.tm_sec) - - def disableHttpCaching(self, level=1): - """ Prevent caching of pages that should not be cached. - - level == 1 means disabling caching when we have a cookie set - level == 2 means completely disabling caching (used by Page*Editor) - - This is important to prevent caches break acl by providing one - user pages meant to be seen only by another user, when both users - share the same caching proxy. - - AVOID using no-cache and no-store for attachments as it is completely broken on IE! - - Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP - """ - if level <= self.http_caching_disabled: - return # only make caching stricter - - if level == 1: - # Set Cache control header for http 1.1 caches - # See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.3 - # and http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.9 - #self.setHttpHeader('Cache-Control: no-cache="set-cookie", private, max-age=0') - self.setHttpHeader('Cache-Control: private, must-revalidate, max-age=10') - elif level == 2: - self.setHttpHeader('Cache-Control: no-cache') - - # only do this once to avoid 'duplicate header' warnings - # (in case the caching disabling is being made stricter) - if not self.http_caching_disabled: - # Set Expires for http 1.0 caches (does not support Cache-Control) - when = time.time() - (3600 * 24 * 365) - self.setHttpHeader('Expires: %s' % self.httpDate(when=when)) - - # Set Pragma for http 1.0 caches - # See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.32 - # DISABLED for level == 1 to fix IE https file attachment downloading trouble. - if level == 2: - self.setHttpHeader('Pragma: no-cache') - - self.http_caching_disabled = level - - def finish(self): - """ General cleanup on end of request - - Delete circular references - all object that we create using self.name = class(self). - This helps Python to collect these objects and keep our memory footprint lower. - """ - for method in self._finishers: - method(self) - # only execute finishers once - self._finishers = [] - - for attr_name in [ - 'editlog', # avoid leaking file handles for open edit-log - 'theme', - 'dicts', - 'user', - 'rootpage', - 'page', - 'html_formatter', - 'formatter', - #'cfg', -- do NOT delattr cfg - it causes problems in the xapian indexing thread - ]: - try: - delattr(self, attr_name) - except: - pass - - def add_finisher(self, method): - self._finishers.append(method) - - # Debug ------------------------------------------------------------ - - def debugEnvironment(self, env): - """ Environment debugging aid """ - # Keep this one name per line so its easy to comment stuff - names = [ -# 'http_accept_language', -# 'http_host', -# 'http_referer', -# 'http_user_agent', -# 'is_ssl', - 'path_info', - 'query_string', -# 'remote_addr', - 'request_method', -# 'request_uri', -# 'saved_cookie', - 'script_name', -# 'server_name', -# 'server_port', - ] - names.sort() - attributes = [] - for name in names: - attributes.append(' %s = %r\n' % (name, getattr(self, name, None))) - attributes = ''.join(attributes) - - environment = [] - names = env.keys() - names.sort() - for key in names: - environment.append(' %s = %r\n' % (key, env[key])) - environment = ''.join(environment) - - data = '\nRequest Attributes\n%s\nEnvironment\n%s' % (attributes, environment) - f = open('/tmp/env.log', 'a') - try: - f.write(data) - finally: - f.close() -
--- a/MoinMoin/request/_tests/test_request.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- -""" - MoinMoin - MoinMoin.module_tested Tests - - @copyright: 2003-2004 by Juergen Hermann <jh@web.de>, - 2007 by MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" - -import py - -from MoinMoin import config, wikiutil -from MoinMoin._tests import wikiconfig - -from MoinMoin.request import HeadersAlreadySentException - -class TestNormalizePagename(object): - - def testPageInvalidChars(self): - """ request: normalize pagename: remove invalid unicode chars - - Assume the default setting - """ - test = u'\u0000\u202a\u202b\u202c\u202d\u202e' - expected = u'' - result = self.request.normalizePagename(test) - assert result == expected - - def testNormalizeSlashes(self): - """ request: normalize pagename: normalize slashes """ - cases = ( - (u'/////', u''), - (u'/a', u'a'), - (u'a/', u'a'), - (u'a/////b/////c', u'a/b/c'), - (u'a b/////c d/////e f', u'a b/c d/e f'), - ) - for test, expected in cases: - result = self.request.normalizePagename(test) - assert result == expected - - def testNormalizeWhitespace(self): - """ request: normalize pagename: normalize whitespace """ - cases = ( - (u' ', u''), - (u' a', u'a'), - (u'a ', u'a'), - (u'a b c', u'a b c'), - (u'a b / c d / e f', u'a b/c d/e f'), - # All 30 unicode spaces - (config.chars_spaces, u''), - ) - for test, expected in cases: - result = self.request.normalizePagename(test) - assert result == expected - - def testUnderscoreTestCase(self): - """ request: normalize pagename: underscore convert to spaces and normalized - - Underscores should convert to spaces, then spaces should be - normalized, order is important! - """ - cases = ( - (u' ', u''), - (u' a', u'a'), - (u'a ', u'a'), - (u'a b c', u'a b c'), - (u'a b / c d / e f', u'a b/c d/e f'), - ) - for test, expected in cases: - result = self.request.normalizePagename(test) - assert result == expected - - -class TestGroupPages(object): - - def testNormalizeGroupName(self): - """ request: normalize pagename: restrict groups to alpha numeric Unicode - - Spaces should normalize after invalid chars removed! - """ - cases = ( - # current acl chars - (u'Name,:Group', u'NameGroup'), - # remove than normalize spaces - (u'Name ! @ # $ % ^ & * ( ) + Group', u'Name Group'), - ) - for test, expected in cases: - # validate we are testing valid group names - if wikiutil.isGroupPage(self.request, test): - result = self.request.normalizePagename(test) - assert result == expected - - -class TestHTTPDate(object): - - def testRFC1123Date(self): - """ request: httpDate default rfc1123 """ - assert self.request.httpDate(0) == 'Thu, 01 Jan 1970 00:00:00 GMT' - - def testRFC850Date(self): - """ request: httpDate rfc850 """ - assert self.request.httpDate(0, rfc='850') == 'Thursday, 01-Jan-70 00:00:00 GMT' - - -class TestHTTPHeaders(object): - std_headers = ['Status: 200 OK', 'Content-type: text/html; charset=%s' % config.charset] - - def setup_method(self, method): - self.request.sent_headers = None - - def testAutoAddStdHeaders(self): - """ test if the usual headers get auto-added if not specified """ - headers_out = self.request.emit_http_headers(testing=True) - assert headers_out == self.std_headers - - def testHeadersOnlyOnce(self): - """ test if trying to call emit_http_headers multiple times raises an exception """ - self.request.emit_http_headers(testing=True) - py.test.raises(HeadersAlreadySentException, self.request.emit_http_headers, [], {'testing': True}) - - def testDuplicateHeadersIgnored(self): - """ test if duplicate headers get ignored """ - headers_in = self.std_headers + ['Status: 500 Server Error'] - headers_expected = self.std_headers - headers_out = self.request.emit_http_headers(headers_in, testing=True) - assert headers_out == headers_expected - - def testListHeaders(self): - """ test if header values get merged into a list for headers supporting it """ - headers_in = self.std_headers + ['Vary: aaa', 'vary: bbb'] - headers_expected = self.std_headers + ['Vary: aaa, bbb'] - headers_out = self.request.emit_http_headers(headers_in, testing=True) - assert headers_out == headers_expected - -coverage_modules = ['MoinMoin.request'] -
--- a/MoinMoin/request/request_cgi.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - CGI Request Implementation for std. CGI web servers - like Apache or IIS or others. - - @copyright: 2001-2003 by Juergen Hermann <jh@web.de>, - 2003-2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" -import sys, os, cgi - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin.request import RequestBase, RemoteClosedConnection - -class Request(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 _setup_args_from_cgi_form(self): - """ Override to create cgi form """ - form = cgi.FieldStorage(keep_blank_values=1) - return RequestBase._setup_args_from_cgi_form(self, form) - - def read(self, n): - """ Read from input stream. """ - if n is None: - logging.warning("calling request.read(None) might block") - return sys.stdin.read() - else: - return sys.stdin.read(n) - - def write(self, *data): - """ Write to output stream. """ - data = self.encode(data) - try: - sys.stdout.write(data) - except Exception: - raise RemoteClosedConnection() - - 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 _emit_http_headers(self, headers): - """ private method to send out preprocessed list of HTTP headers """ - for header in headers: - self.write("%s\r\n" % header) - self.write("\r\n") -
--- a/MoinMoin/request/request_cli.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,104 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - CLI Request Implementation for commandline usage. - - @copyright: 2001-2003 Juergen Hermann <jh@web.de>, - 2003-2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" -import sys - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin.request import RequestBase, RemoteClosedConnection - -class Request(RequestBase): - """ specialized on command line interface and script requests """ - - def __init__(self, url='CLI', pagename='', properties={}, given_config=None): - 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 if /pagename works as URI for CLI usage - self.http_host = 'localhost' - self.http_referer = '' - self.script_name = '.' - self.content_length = None - self.if_modified_since = None - self.if_none_match = None - RequestBase.__init__(self, properties, given_config) - self.cfg.caching_formats = [] # don't spoil the cache - self.initTheme() # usually request.run() does this, but we don't use it - - def _setup_args_from_cgi_form(self): - """ Override to create cli form """ - #form = cgi.FieldStorage() - #return RequestBase._setup_args_from_cgi_form(self, form) - return {} - - def read(self, n): - """ Read from input stream. """ - if n is None: - logging.warning("calling request.read(None) might block") - return sys.stdin.read() - else: - return sys.stdin.read(n) - - def write(self, *data): - """ Write to output stream. """ - data = self.encode(data) - try: - sys.stdout.write(data) - except IOError: - raise RemoteClosedConnection() - - 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 _emit_http_headers(self, headers): - """ private method to send out preprocessed list of HTTP 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!") - -
--- a/MoinMoin/request/request_fcgi.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - FastCGI Request Implementation for fastcgi and Apache - (and maybe others). - - @copyright: 2001-2003 Juergen Hermann <jh@web.de>, - 2003-2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin.request import RequestBase, RemoteClosedConnection - -class Request(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): - """ Override to use FastCGI form """ - # thfcgi used keep_blank_values=1 internally for fcgform - return RequestBase._setup_args_from_cgi_form(self, self.fcgform) - - def read(self, n): - """ Read from input stream. """ - if n is None: - logging.warning("calling request.read(None) might block") - return self.fcgreq.stdin.read() - else: - return self.fcgreq.stdin.read(n) - - def write(self, *data): - """ Write to output stream. """ - data = self.encode(data) - try: - self.fcgreq.out.write(data) - except Exception: - raise RemoteClosedConnection() - - def send_file(self, fileobj, bufsize=8192, do_flush=True): - # as thfcgi buffers everything we write until we do a flush, we use - # do_flush=True as default here (otherwise the sending of big file - # attachments would consume lots of memory) - return RequestBase.send_file(self, fileobj, bufsize, do_flush) - - 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() - - def _emit_http_headers(self, headers): - """ private method to send out preprocessed list of HTTP headers """ - for header in headers: - self.write("%s\r\n" % header) - self.write("\r\n") -
--- a/MoinMoin/request/request_modpython.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,168 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - mod_python Request Implementation for Apache and mod_python. - - @copyright: 2001-2003 Juergen Hermann <jh@web.de>, - 2003-2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin import wikiutil -from MoinMoin.request import RequestBase, RemoteClosedConnection - -class Request(RequestBase): - """ specialized on mod_python requests """ - - def __init__(self, req, properties={}): - """ 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, properties) - - 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): - """ Override to use mod_python.util.FieldStorage - - It's little different from cgi.FieldStorage, so we need to - duplicate the conversion code. - """ - from mod_python import util - form = util.FieldStorage(self.mpyreq, keep_blank_values=1) # by default this evaluates query string AND body POST data! - - args = {} - - # You cannot get rid of .keys() here - for key in form.keys(): - if key is None: - continue - values = form[key] - if not isinstance(values, list): - values = [values] - fixedResult = [] - for item in values: - if isinstance(item, util.StringField): - fixedResult.append(item.value) - elif isinstance(item, util.Field) and item.filename: - fixedResult.append(item.file) - # Remember filenames with a name hack - args[key + '__filename__'] = item.filename - # XXX Now it gets extremely dirty to work around a problem in mod_python 3.3.1: XXX - # Without the next line, item.file will be closed when item/form leaves this scope. - # I guess some reference counting is not implemented correctly for item.file, - # so we just keep a reference to item to keep it alive... - fixedResult.append(item) # we are lucky, nobody uses the 2nd list item anyway - # If you are reading this, please switch to mod_wsgi. :) - elif isinstance(item, str): - # mod_python 2.7 might return strings instead of Field objects. - fixedResult.append(item) - args[key] = fixedResult - - result = self.decodeArgs(args) - return result # XXX without the hack above, item.file gets closed when returning! XXX - - 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): - """ Read from input stream. """ - if n is None: - logging.warning("calling request.read(None) might block") - return self.mpyreq.read() - else: - return self.mpyreq.read(n) - - def write(self, *data): - """ Write to output stream. """ - data = self.encode(data) - try: - self.mpyreq.write(data) - except Exception: - raise RemoteClosedConnection() - - 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 - - def _emit_http_headers(self, headers): - """ private method to send out preprocessed list of HTTP headers """ - st_header, ct_header, other_headers = headers[0], headers[1], headers[2:] - status = st_header.split(':', 1)[1].lstrip() - self.mpyreq.status = int(status.split(' ', 1)[0]) - self.mpyreq.content_type = ct_header.split(':', 1)[1].lstrip() - for header in other_headers: - key, value = header.split(':', 1) - value = value.lstrip() - self.mpyreq.headers_out[key] = value - # this is for mod_python 2.7.X, for 3.X it's a NOP - self.mpyreq.send_http_header() -
--- a/MoinMoin/request/request_standalone.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,108 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - Standalone Moin Server Request Implementation - - @copyright: 2001-2003 Juergen Hermann <jh@web.de>, - 2003-2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" -import cgi - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin.request import RequestBase, RemoteClosedConnection - -class Request(RequestBase): - """ specialized on StandAlone Server (MoinMoin.server.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 - - # 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', '') - try: - self.content_length = int(sa.headers.getheader('content-length')) - except (TypeError, ValueError): - self.content_length = None - co = [c for c in sa.headers.getheaders('cookie') if c] - self.saved_cookie = ', '.join(co) or '' - self.if_modified_since = sa.headers.getheader('if-modified-since') - self.if_none_match = sa.headers.getheader('if-none-match') - - # 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) - - ##self.debugEnvironment(sa.headers) - - RequestBase.__init__(self, properties) - - except Exception, err: - self.fail(err) - - def _setup_args_from_cgi_form(self): - """ Override to create standalone form """ - form = cgi.FieldStorage(self.rfile, headers=self.headers, environ={'REQUEST_METHOD': 'POST'}, - keep_blank_values=1) - return RequestBase._setup_args_from_cgi_form(self, form) - - def read(self, n): - """ Read from input stream """ - if n is None: - logging.warning("calling request.read(None) might block") - return self.rfile.read() - else: - return self.rfile.read(n) - - def write(self, *data): - """ Write to output stream. """ - data = self.encode(data) - try: - self.wfile.write(data) - except Exception: - raise RemoteClosedConnection() - - def flush(self): - self.wfile.flush() - - def finish(self): - RequestBase.finish(self) - self.wfile.flush() - - # Headers ---------------------------------------------------------- - - def _emit_http_headers(self, headers): - """ private method to send out preprocessed list of HTTP headers """ - st_header, other_headers = headers[0], headers[1:] - status = st_header.split(':', 1)[1].lstrip() - status_code, status_msg = status.split(' ', 1) - status_code = int(status_code) - self.sareq.send_response(status_code, status_msg) - for header in other_headers: - key, value = header.split(':', 1) - value = value.lstrip() - self.sareq.send_header(key, value) - self.sareq.end_headers() -
--- a/MoinMoin/request/request_twisted.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,143 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - Twisted Request Implementation - - @copyright: 2001-2003 Juergen Hermann <jh@web.de>, - 2003-2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin.request import RequestBase, MoinMoinFinish, RemoteClosedConnection - -class Request(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') - try: - self.content_length = int(self.twistd.getHeader('Content-Length')) - except (TypeError, ValueError): - self.content_length = None - self.if_modified_since = self.twistd.getHeader('If-Modified-Since') - self.if_none_match = self.twistd.getHeader('If-None-Match') - - # 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_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 MoinMoinFinish: # might be triggered by http_redirect - self.emit_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): - """ Return args dict - - Twisted already parsed args, including __filename__ hacking, - but did not decode the values. - """ - # All of the arguments, including URL and POST arguments (using keep_blank_values=1 internally). - return self.decodeArgs(self.twistd.args) - - def read(self, n): - """ 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: - logging.warning("calling request.read(None) might block") - 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 - data = self.encode(data) - try: - self.reactor.callFromThread(self.twistd.write, data) - except Exception: - raise RemoteClosedConnection() - - def flush(self): - pass # XXX is there a flush in twisted? - - def finish(self): - RequestBase.finish(self) - self.reactor.callFromThread(self.twistd.finish) - - # Headers ---------------------------------------------------------- - - def _emit_http_headers(self, headers): - """ private method to send out preprocessed list of HTTP headers """ - st_header, other_headers = headers[0], headers[1:] - status = st_header.split(':', 1)[1].lstrip() - status_code, status_msg = status.split(' ', 1) - self.twistd.setResponseCode(int(status_code), status_msg) - for header in other_headers: - 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) - - 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 MoinMoinFinish -
--- a/MoinMoin/request/request_wsgi.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,80 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - WSGI Request Implementation for std. WSGI web servers. - - @copyright: 2001-2003 Juergen Hermann <jh@web.de>, - 2003-2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" -import cgi, StringIO - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin.request import RequestBase, RemoteClosedConnection - -class Request(RequestBase): - """ specialized on WSGI requests """ - def __init__(self, env): - try: - self.env = env - self.hasContentType = False - - self.stdin = env['wsgi.input'] - self.stdout = StringIO.StringIO() - - # used by MoinMoin.server.server_wsgi: - self.status = '200 OK' - self.headers = [] - - # used by send_file() - self._send_file = None - self._send_bufsize = None - - self._setup_vars_from_std_env(env) - RequestBase.__init__(self, {}) - - except Exception, err: - self.fail(err) - - def _setup_args_from_cgi_form(self): - """ Override to create cgi form """ - form = cgi.FieldStorage(fp=self.stdin, environ=self.env, keep_blank_values=1) - return RequestBase._setup_args_from_cgi_form(self, form) - - def read(self, n): - if n is None: - logging.warning("calling request.read(None) might block") - return self.stdin.read() - else: - return self.stdin.read(n) - - def send_file(self, fileobj, bufsize=8192, do_flush=None): - # For now, we just remember fileobj and bufsize for sending it later: - self._send_file = fileobj - self._send_bufsize = bufsize - - def write(self, *data): - data = self.encode(data) - try: - self.stdout.write(data) - except Exception: - raise RemoteClosedConnection() - - def _emit_http_headers(self, headers): - """ private method to send out preprocessed list of HTTP headers """ - st_header, other_headers = headers[0], headers[1:] - self.status = st_header.split(':', 1)[1].lstrip() - for header in other_headers: - key, value = header.split(':', 1) - value = value.lstrip() - self.headers.append((key, value)) - - def flush(self): - pass - - def output(self): - # called by MoinMoin.server.server_wsgi - return self.stdout.getvalue() - -
--- a/MoinMoin/script/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/script/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -173,11 +173,11 @@ def init_request(self): """ create request """ - from MoinMoin.request import request_cli + from MoinMoin.web.contexts import ScriptContext if self.options.wiki_url: - self.request = request_cli.Request(self.options.wiki_url, self.options.page) + self.request = ScriptContext(self.options.wiki_url, self.options.page) else: - self.request = request_cli.Request(pagename=self.options.page) + self.request = ScriptContext(pagename=self.options.page) def mainloop(self): # Insert config dir or the current directory to the start of the path.
--- a/MoinMoin/script/cli/show.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/script/cli/show.py Tue Sep 23 23:02:50 2008 +0200 @@ -7,6 +7,7 @@ """ from MoinMoin.script import MoinScript +from MoinMoin.wsgiapp import run class PluginScript(MoinScript): """\ @@ -27,4 +28,4 @@ def mainloop(self): self.init_request() - self.request.run() + run(self.request)
--- a/MoinMoin/script/server/standalone.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/script/server/standalone.py Tue Sep 23 23:02:50 2008 +0200 @@ -11,8 +11,8 @@ import signal from MoinMoin.script import MoinScript -from MoinMoin.server.server_standalone import StandaloneConfig, run -from MoinMoin.server.daemon import Daemon +from MoinMoin.util.daemon import Daemon +from MoinMoin.web.serving import run_server class PluginScript(MoinScript): def __init__(self, argv, def_values): @@ -38,18 +38,6 @@ help="Set the ip to listen on. Use \"\" for all interfaces. Default: localhost" ) self.parser.add_option( - "--serverClass", dest="serverClass", - help="Set the server model to use. Choices: ThreadPool, serverClass, Forking, Simple. Default: ThreadPool" - ) - self.parser.add_option( - "--threadLimit", dest="threadLimit", type="int", - help="Set the maximum number of threads to use. Default: 10" - ) - self.parser.add_option( - "--requestQueueSize", dest="requestQueueSize", type="int", - help="Set the size of the request queue. Default: 50" - ) - self.parser.add_option( "--start", dest="start", action="store_true", help="Start server in background." ) @@ -61,23 +49,16 @@ "--pidfile", dest="pidfile", help="Set file to store pid of moin daemon in. Default: moin.pid" ) + self.parser.add_option( + "--debug", dest="debug", action="store_true", + help="Enable debug mode of server (show tracebacks)" + ) def mainloop(self): # we don't expect non-option arguments if self.args: self.parser.error("incorrect number of arguments") - thread_choices = ["ThreadPool", "Threading", "Forking", "Simple"] - serverClass = "ThreadPool" - if self.options.serverClass: - thread_choices2 = [x.upper() for x in thread_choices] - thread_choice = self.options.serverClass.upper() - try: - serverClass_index = thread_choices2.index(thread_choice) - except ValueError: - self.parser.error("invalid serverClass type") - serverClass = thread_choices[serverClass_index] - pidfile = "moin.pid" if self.options.pidfile: pidfile = self.options.pidfile @@ -106,6 +87,13 @@ # some other import went wrong raise + # intialize some defaults if missing + for option in ('docs', 'user', 'group', 'port', 'interface', 'debug'): + if not hasattr(Config, option): + value = getattr(DefaultConfig, option) + setattr(Config, option, value) + + # override with cmdline options if self.options.docs: Config.docs = self.options.docs if self.options.user: @@ -116,26 +104,29 @@ Config.port = self.options.port if self.options.interface: Config.interface = self.options.interface - Config.serverClass = serverClass + 'Server' - if self.options.threadLimit: - Config.threadLimit = self.options.threadLimit - if self.options.requestQueueSize: - Config.requestQueueSize = self.options.requestQueueSize + if self.options.debug: + Config.debug = True + + if not hasattr(Config, 'docs'): + docs = os.path.join('wiki', 'htdocs') + if not os.path.exists(docs): + docs = "/usr/share/moin/htdocs" + Config.docs = docs if self.options.start: - daemon = Daemon('moin', pidfile, run, Config) + daemon = Daemon('moin', pidfile, run_server, Config) daemon.do_start() else: - run(Config) + run_server(Config.interface, Config.port, Config.docs, + use_debugger=Config.debug, user=Config.user, + group=Config.group) -class DefaultConfig(StandaloneConfig): +class DefaultConfig: docs = os.path.join('wiki', 'htdocs') if not os.path.exists(docs): docs = "/usr/share/moin/htdocs" - user = '' - group = '' + user = None + group = None port = 8080 interface = 'localhost' - serverClass = 'ThreadPoolServer' - threadLimit = 10 - requestQueueSize = 50 + debug = False
--- a/MoinMoin/search/builtin.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/search/builtin.py Tue Sep 23 23:02:50 2008 +0200 @@ -401,9 +401,9 @@ @param request: current request """ - from MoinMoin.request.request_cli import Request + from MoinMoin.web.contexts import ScriptContext from MoinMoin.security import Permissions - request = Request(request.url) + request = ScriptContext(request.url) class SecurityPolicy(Permissions): def read(self, *args, **kw): return True
--- a/MoinMoin/server/__init__.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,98 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin server package - - Supply common server utilities. - - @copyright: 2004 Nir Soffer - @license: GNU GPL, see COPYING for details. -""" - -import os -from StringIO import StringIO - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin import config - - -def switchUID(uid, gid): - """ Switch identity to safe user and group - - Does not support Windows, because the necessary calls are not available. - TODO: can we use win32api calls to achieve the same effect on Windows? - - Raise RuntimeError if can't switch or trying to switch to root. - """ - if uid == 0 or gid == 0: - # We will not run as root. If you like to run a web - # server as root, then hack this code. - raise RuntimeError('will not run as root!') - - try: - os.setgid(gid) - os.setuid(uid) - except (OSError, AttributeError): - # Either we can't switch, or we are on windows, which does not have - # those calls. - raise RuntimeError("can't change uid/gid to %s/%s" % - (uid, gid)) - logging.info("Running as uid/gid %d/%d" % (uid, gid)) - - -class Config: - """ Base class for server configuration - - When you create a server, you should run it with a Config - instance. Sub class to define the default values. - - This class does all error checking needed for config values, and will - raise a RuntimeError on any fatal error. - """ - # some defaults that should be common for all servers: - url_prefix_static = config.url_prefix_static - docs = None # document root (if supported) - user = None # user we shall use for running (if supported) - group = None # group ... - port = None # tcp port number (if supported) - - def __init__(self): - """ Validate and post process configuration values - - Will raise RuntimeError for any wrong config value. - """ - # Check that docs path is accessible - if self.docs: - self.docs = os.path.normpath(os.path.abspath(self.docs)) - if not os.access(self.docs, os.F_OK | os.R_OK | os.X_OK): - raise RuntimeError("Can't access docs directory '%s'. Check docs " - "setting and permissions." % self.docs) - - # Don't check uid and gid on windows, those calls are not available. - if os.name == 'nt': - self.uid = self.gid = 0 - return - - self.uid = os.getuid() - self.gid = os.getgid() - - # If serving privileged port, we must run as root to bind the port. - # we will give up root privileges later - if self.port and self.port < 1024 and self.uid != 0: - raise RuntimeError('Must run as root to serve port number under 1024. ' - 'Run as root or change port setting.') - - if self.user and self.group and self.uid == 0: - # If we run as root to serve privileged port, we change user and group - # to a safe setting. Get the uid and gid now, switch later. - import pwd, grp - try: - self.uid = pwd.getpwnam(self.user)[2] - except KeyError: - raise RuntimeError("Unknown user: '%s', check user setting" % self.user) - try: - self.gid = grp.getgrnam(self.group)[2] - except KeyError: - raise RuntimeError("Unknown group: '%s', check group setting" % self.group) -
--- a/MoinMoin/server/daemon.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,266 +0,0 @@ -""" -Daemon - daemon script and controller - -This module is based on twisted.scripts.twistd, modified by Nir Soffer -to work with non Twisted code. - -The Daemon class, represents a background process using a pid -file. When you create an instance, the process may be running or not. -After creating an instance, you can call one of its do_xxx() methods. - -The DaemonScript class is a script that control a daemon, with a -functionality similar to apachectl. To create a daemon script, create an -instacne and call its run() method. - -Typical usage:: - - # Daemon script - import daemon - import myserver - script = daemon.DaemonScript('myserver', 'myserver.pid', - myserver.run, myserver.Config) - script.run() - - -Copyright (c) 2005 Nir Soffer <nirs@freeshell.org> - -Twisted, the Framework of Your Internet -Copyright (c) 2001-2004 Twisted Matrix Laboratories. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -import sys, os, errno, signal, time - - -class Error(Exception): - """ Daemon error """ - - -class Daemon: - """ A background process - - Represent a background process, which may be running or not. The - process can be started, stopped, restarted or killed. - """ - commandPrefix = 'do_' - - def __init__(self, name, pidfile, function, *args, **kw): - """ Create a daemon - - @param name: name of the process - @param pidfile: pid filename - @param function: the server main function, will block until the - server is done. - @param args: arguments to pass to function - @param kw: keyword arguments to pass to function - """ - self.name = name - self.function = function - self.args = args - self.kw = kw - self.pidFile = os.path.abspath(pidfile) - - # -------------------------------------------------------------------- - # Commands - - def do_start(self): - """ Start the daemon process - - Start will daemonize then block until the server is killed and - then cleanup the pid file on the way out. - """ - running, pid = self.status() - if running: - raise Error("another application is running with pid %s " - "(try restart)" % pid) - self.daemonize() - self.writePID() - try: - self.function(*self.args, **self.kw) - finally: - self.removePID() - - def do_stop(self): - """ Stop the daemon process - - Terminate or raise an error we can't handle here. On success, - the pid file will be cleaned by the terminated process. - """ - running, pid = self.status() - if not running: - return self.log("%s is not running" % self.name) - os.kill(pid, signal.SIGINT) - - def do_kill(self): - """ Kill the daemon process - - Kill or raise an error which we can't handle here. Clean the - pid file for the killed process. - """ - running, pid = self.status() - if not running: - return self.log("%s is not running" % self.name) - os.kill(pid, signal.SIGKILL) - self.removePID() - - def do_restart(self): - """ stop, wait until pid file gone and start again """ - running, pid = self.status() - if not running: - self.log("%s is not running, trying to start" % self.name) - else: - self.do_stop() - timeoutSeconds = 2.0 - start = time.time() - while time.time() - start < timeoutSeconds: - running, pid = self.status() - if not running: - break - time.sleep(0.1) - else: - raise Error("could not start after %s seconds" % timeoutSeconds) - self.do_start() - - # ------------------------------------------------------------------- - # Private - - def status(self): - """ Return status tuple (running, pid) - - See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC18 - """ - running = False - pid = self.readPID() - if pid is not None: - try: - os.kill(pid, 0) - running = True - except OSError, err: - if err.errno == errno.ESRCH: - # No such process or security enhancements are causing - # the system to deny its existence. - self.log("removing stale pid file: %s" % self.pidFile) - self.removePID() - else: - raise - return running, pid - - def readPID(self): - """ Return the pid from the pid file - - If there is no pid file, return None. If pid file is corrupted, - remove it. If its not readable, raise. - """ - pid = None - try: - pid = int(file(self.pidFile).read()) - except IOError, err: - if err.errno != errno.ENOENT: - raise - except ValueError: - self.warn("removing corrupted pid file: %s" % self.pidFile) - self.removePID() - return pid - - def daemonize(self): - """ Make the current process a daemon - - See http://www.erlenstar.demon.co.uk/unix/faq_toc.html#TOC16 - """ - if os.fork(): # launch child and... - os._exit(0) # kill off parent - os.setsid() - if os.fork(): # launch child and... - os._exit(0) # kill off parent again. - os.umask(0077) - null = os.open('/dev/null', os.O_RDWR) - for i in range(3): - try: - os.dup2(null, i) - except OSError, e: - if e.errno != errno.EBADF: - raise - os.close(null) - - def writePID(self): - pid = str(os.getpid()) - open(self.pidFile, 'wb').write(pid) - - def removePID(self): - try: - os.remove(self.pidFile) - except OSError, err: - if err.errno != errno.ENOENT: - raise - - def warn(self, message): - self.log('warning: %s' % message) - - def log(self, message): - """ TODO: does it work after daemonize? """ - sys.stderr.write(message + '\n') - - -class DaemonScript(Daemon): - """ A script controlling a daemon - - TODO: add --pid-dir option? - """ - - def run(self): - """ Check commandline arguments and run a command """ - args = len(sys.argv) - if args == 1: - self.usage('nothing to do') - elif args > 2: - self.usage("too many arguments") - try: - command = sys.argv[1] - func = getattr(self, self.commandPrefix + command) - func() - except AttributeError: - self.usage('unknown command %r' % command) - except Exception, why: - sys.exit("error: %s" % str(why)) - - def usage(self, message): - sys.stderr.write('error: %s\n' % message) - sys.stderr.write(self.help()) - sys.exit(1) - - def help(self): - return """ -%(name)s - MoinMoin daemon - -usage: %(name)s command - -commands: - start start the server - stop stop the server - restart stop then start the server - kill kill the server - -@copyright: 2004-2005 Thomas Waldmann, Nir Soffer -@license: GNU GPL, see COPYING for details. -""" % { - 'name': self.name, -} -
--- a/MoinMoin/server/server_cgi.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - CGI pseudo Server - - This is not really a server, it is just so that CGI stuff (the real - server is likely Apache or IIS or some other std. CGI server) looks - similar to what we have for Twisted and standalone server. - - Minimal usage: - - from MoinMoin.server.server_cgi import CgiConfig, run - - class Config(CgiConfig): - pass - - run(Config) - - See more options in CgiConfig class. - - @copyright: 2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" - -from MoinMoin.server import Config -from MoinMoin.request import request_cgi - -# Server globals -config = None - -# ------------------------------------------------------------------------ -# Public interface - -class CgiConfig(Config): - """ CGI default config """ - name = 'moin' - properties = {} - - # Development options - hotshotProfile = None # e.g. "moin.prof" - - -def run(configClass): - """ Create and run a Cgi Request - - See CgiConfig for available options - - @param configClass: config class - """ - - config = configClass() - - if config.hotshotProfile: - import hotshot - config.hotshotProfile = hotshot.Profile(config.hotshotProfile) - config.hotshotProfile.start() - - request = request_cgi.Request(properties=config.properties) - request.run() - - if config.hotshotProfile: - config.hotshotProfile.close() - -
--- a/MoinMoin/server/server_fastcgi.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,59 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin.server.server_fastcgi - - This is not really a server, it is just so that fastcgi stuff - (the real server is likely Apache2) fits the model we have for - Twisted and standalone server. - - Minimal usage: - - from MoinMoin.server.server_fastcgi import FastCgiConfig, run - - class Config(FastCgiConfig): - pass - - run(Config) - - See more options in FastCgiConfig class. - - @copyright: 2007 MoinMoin:ThomasWaldmann - - @license: GNU GPL, see COPYING for details. -""" - -from MoinMoin.server import Config -from MoinMoin.request import request_fcgi -from MoinMoin.support import thfcgi - -# Set threads flag, so other code can use proper locking. -from MoinMoin import config -config.use_threads = 1 -del config - -class FastCgiConfig(Config): - """ Set up default server """ - properties = {} - # properties = {'script_name': '/'} - - # how many requests shall be handled by a moin fcgi process before it dies, - # -1 mean "unlimited lifetime": - max_requests = -1 - - # how many threads to use (1 means use only main program, non-threaded) - max_threads = 5 - - # backlog, use in socket.listen(backlog) call - backlog = 5 - - # default port - port = None - -def run(ConfigClass=FastCgiConfig): - config = ConfigClass() - - handle_request = lambda req, env, form, properties=config.properties: \ - request_fcgi.Request(req, env, form, properties=properties).run() - fcg = thfcgi.FCGI(handle_request, port=config.port, max_requests=config.max_requests, backlog=config.backlog, max_threads=config.max_threads) - fcg.run() -
--- a/MoinMoin/server/server_modpython.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,46 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin.server.server_modpython - - This is not really a server, it is just so that modpython stuff - (the real server is likely Apache2) fits the model we have for - Twisted and standalone server. - - Minimal usage: - - from MoinMoin.server.server_modpython import CgiConfig, run - - class Config(CgiConfig): - pass - - run(Config) - - See more options in CgiConfig class. - - @copyright: 2006 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" - -from MoinMoin.server import Config -from MoinMoin.request import request_modpython - -# Set threads flag, so other code can use proper locking. -# TODO: It seems that modpy does not use threads, so we don't need to -# set it here. Do we have another method to check this? -from MoinMoin import config -config.use_threads = 1 -del config - -# Server globals -config = None - -class ModpythonConfig(Config): - """ Set up default server """ - properties = {} - - -def modpythonHandler(request, ConfigClass=ModpythonConfig): - config = ConfigClass() - moinreq = request_modpython.Request(request, config.properties) - return moinreq.run(request) -
--- a/MoinMoin/server/server_standalone.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,637 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin - Stand-alone HTTP Server - - This is a simple, fast and very easy to install server. Its - recommended for personal wikis or public wikis with little load. - - It is not well tested in public wikis with heavy load. In these case - you might want to use twisted, fast cgi or mod python, or if you - can't use those, cgi. - - Minimal usage: - - from MoinMoin.server.server_standalone import StandaloneConfig, run - - class Config(StandaloneConfig): - docs = '/usr/share/moin/wiki/htdocs' - user = 'www-data' - group = 'www-data' - - run(Config) - - See more options in StandaloneConfig class. - - For security, the server will not run as root. If you try to run it - as root, it will run as the user and group in the config. If you run - it as a normal user, it will run with your regular user and group. - - Significant contributions to this module by R. Church <rc@ghostbitch.org> - - @copyright: 2001-2004 MoinMoin:JuergenHermann, - 2005 MoinMoin:AlexanderSchremmer, - 2005 MoinMoin:NirSoffer - @license: GNU GPL, see COPYING for details. -""" - -import os, sys, time, socket, errno, shutil -import BaseHTTPServer, SimpleHTTPServer, SocketServer - -from MoinMoin import log -logging = log.getLogger(__name__) - -from MoinMoin import version, wikiutil -from MoinMoin.server import Config, switchUID -from MoinMoin.request import request_standalone -from MoinMoin.util import timefuncs - -# Server globals -httpd = None -config = None - - -class SimpleServer(BaseHTTPServer.HTTPServer): - """ Simplest server, serving one request after another - - This server is good for personal wiki, or when lowest memory - footprint is needed. - """ - use_threads = False - - def __init__(self, config): - self.htdocs = config.docs - self.request_queue_size = config.requestQueueSize - self._abort = 0 - address = (config.interface, config.port) - BaseHTTPServer.HTTPServer.__init__(self, address, MoinRequestHandler) - - def server_activate(self): - BaseHTTPServer.HTTPServer.server_activate(self) - logging.info("Serving on %s:%d" % self.server_address) - - def serve_forever(self): - """Handle one request at a time until we die """ - while not self._abort: - self.handle_request() - - def die(self): - """Abort this server instance's serving loop """ - # Close hotshot profiler - if config.hotshotProfile: - config.hotshotProfile.close() - - if config.cProfileProfile and config.cProfile: - config.cProfile.dump_stats(config.cProfileProfile) - - # Set abort flag, then make request to wake the server - self._abort = 1 - try: - import httplib - addr = self.server_address - if not addr[0]: - addr = ("localhost", addr[1]) - req = httplib.HTTP('%s:%d' % addr) - req.connect() - req.putrequest('DIE', '/') - req.endheaders() - del req - except socket.error, err: - # Ignore certain errors - if err.args[0] not in [errno.EADDRNOTAVAIL, ]: - raise - - -class ThreadingServer(SimpleServer): - """ Serve each request in a new thread - - This server is used since release 1.3 and seems to work nice with - little load. - - From release 1.3.5 there is a thread limit, that should help to - limit the load on the server. - """ - use_threads = True - - def __init__(self, config): - self.thread_limit = config.threadLimit - from threading import Condition - self.lock = Condition() - SimpleServer.__init__(self, config) - - def process_request(self, request, client_address): - """ Start a new thread to process the request - - If the thread limit has been reached, wait on the lock. The - next thread will notify when finished. - """ - from threading import Thread, activeCount - self.lock.acquire() - try: - if activeCount() > self.thread_limit: - self.lock.wait() - if self._abort: - return - t = Thread(target=self.process_request_thread, - args=(request, client_address)) - t.setDaemon(True) - t.start() - finally: - self.lock.release() - - def process_request_thread(self, request, client_address): - """ Called for each request on a new thread - - Notify the main thread on the end of each request. - """ - try: - self.finish_request(request, client_address) - except: - self.handle_error(request, client_address) - self.close_request(request) - self.lock.acquire() - try: - # Main thread might be waiting - self.lock.notify() - finally: - self.lock.release() - - -class ThreadPoolServer(SimpleServer): - """ Threading server using a pool of threads - - This is a new experimental server, using a pool of threads instead - of creating new thread for each request. This is similar to Apache - worker mpm, with a simpler constant thread pool. - - This server is 5 times faster than ThreadingServer for static - files, and about the same for wiki pages. - """ - use_threads = True - - def __init__(self, config): - self.queue = [] - # The size of the queue need more testing - self.queueSize = config.threadLimit * 2 - self.poolSize = config.threadLimit - from threading import Condition - self.lock = Condition() - SimpleServer.__init__(self, config) - - def serve_forever(self): - """ Create a thread pool then invoke base class method """ - from threading import Thread - for dummy in range(self.poolSize): - t = Thread(target=self.serve_forever_thread) - t.setDaemon(True) - t.start() - SimpleServer.serve_forever(self) - - def process_request(self, request, client_address): - """ Called for each request - - Insert the request into the queue. If the queue is full, wait - until one of the request threads pop a request. During the wait, - new connections might be dropped. - """ - self.lock.acquire() - try: - if len(self.queue) >= self.queueSize: - self.lock.wait() - if self._abort: - return - self.queue.insert(0, (request, client_address)) - self.lock.notify() - finally: - self.lock.release() - - def serve_forever_thread(self): - """ The main loop of request threads - - Pop a request from the queue and process it. - """ - while not self._abort: - request, client_address = self.pop_request() - try: - self.finish_request(request, client_address) - except: - self.handle_error(request, client_address) - self.close_request(request) - - def pop_request(self): - """ Pop a request from the queue - - If the queue is empty, wait for notification. If the queue was - full, notify the main thread which may be waiting. - """ - self.lock.acquire() - try: - while not self._abort: - try: - item = self.queue.pop() - if len(self.queue) == self.queueSize - 1: - # Queue was full - main thread might be waiting - self.lock.notify() - return item - except IndexError: - self.lock.wait() - finally: - self.lock.release() - sys.exit() - - def die(self): - """ Wake all threads then invoke base class die - - Threads should exist when _abort is True. - """ - self._abort = True - self.wake_all_threads() - time.sleep(0.1) - SimpleServer.die(self) - - def wake_all_threads(self): - self.lock.acquire() - try: - self.lock.notifyAll() - finally: - self.lock.release() - - -class ForkingServer(SocketServer.ForkingMixIn, SimpleServer): - """ Serve each request in a new process - - This is new untested server, first tests show rather pathetic cgi - like performance. No data is cached between requests. - - The mixin has its own process limit. - """ - max_children = 10 - - -class MoinRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): - - bufferSize = 8 * 1024 # used to serve static files - staticExpire = 365 * 24 * 3600 # 1 year expiry for static files - - def __init__(self, request, client_address, server): - self.server_version = "MoinMoin %s %s %s" % (version.release, - version.revision, - server.__class__.__name__) - self.expires = 0 - SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, request, - client_address, server) - - def log_message(self, format, *args): - logging.info("%s %s" % (self.address_string(), format % args)) - - # ------------------------------------------------------------------- - # do_METHOD dispatchers - called for each request - - def do_DIE(self): - if self.server._abort: - self.log_error("Shutting down") - - def do_ALL(self): - """ Handle requests (request type GET/HEAD/POST is in self.command) - - Separate between wiki pages and css and image url by similar - system as cgi and twisted, the url_prefix_static prefix. - """ - prefix = config.url_prefix_static - if self.path.startswith(prefix + '/'): - self.path = self.path[len(prefix):] - self.serve_static_file() - elif self.path in ['/favicon.ico', '/robots.txt']: - self.serve_static_file() - else: - self.serve_moin() - - do_POST = do_ALL - do_GET = do_ALL - do_HEAD = do_ALL - - # ------------------------------------------------------------------- - # Serve methods - - def serve_static_file(self): - """ Serve files from the htdocs directory """ - self.expires = self.staticExpire - path = self.path.split("?", 1) - if len(path) > 1: - self.path = path[0] # XXX ?params - - try: - fn = getattr(SimpleHTTPServer.SimpleHTTPRequestHandler, 'do_' + self.command) - fn(self) - except socket.error, err: - # Ignore certain errors - if err.args[0] not in [errno.EPIPE, errno.ECONNABORTED]: - raise - - def serve_moin(self): - """ Serve a request using moin """ - # don't make an Expires header for wiki pages - self.expires = 0 - - try: - req = request_standalone.Request(self, properties=config.properties) - req.run() - except socket.error, err: - # Ignore certain errors - if err.args[0] not in [errno.EPIPE, errno.ECONNABORTED]: - raise - - def translate_path(self, uri): - """ Translate a /-separated PATH to the local filename syntax. - - Components that mean special things to the local file system - (e.g. drive or directory names) are ignored. - """ - path = wikiutil.url_unquote(uri, want_unicode=False) - path = path.replace('\\', '/') - words = path.split('/') - words = filter(None, words) - - path = self.server.htdocs - bad_uri = 0 - for word in words: - drive, word = os.path.splitdrive(word) - if drive: - bad_uri = 1 - head, word = os.path.split(word) - if word in (os.curdir, os.pardir): - bad_uri = 1 - continue - path = os.path.join(path, word) - - if bad_uri: - self.log_error("Detected bad request URI '%s', translated to '%s'" - % (uri, path, )) - return path - - def end_headers(self): - """overload the default end_headers, inserting expires header""" - if self.expires: - now = time.time() - expires = now + self.expires - self.send_header('Expires', timefuncs.formathttpdate(expires)) - SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self) - - def copyfile(self, source, outputfile): - """Copy all data between two file objects. - - Modify the base class method to change the buffer size. Test - shows that for the typical static files we serve, 8K buffer is - faster than the default 16K buffer. - """ - shutil.copyfileobj(source, outputfile, length=self.bufferSize) - - def address_string(self): - """We don't want to do reverse DNS lookups, just return IP address.""" - return self.client_address[0] - - -try: - from tlslite.api import TLSSocketServerMixIn, X509, X509CertChain, SessionCache, parsePEMKey - from tlslite.TLSConnection import TLSConnection -except ImportError: - pass -else: - class SecureRequestRedirect(BaseHTTPServer.BaseHTTPRequestHandler): - def handle(self): - self.close_connection = 1 - try: - self.raw_requestline = self.rfile.readline() - except socket.error: - return - if self.parse_request(): - host = self.headers.get('Host', socket.gethostname()) - path = self.path - else: - host = '%s:%s' % (socket.gethostname(), - self.request.getsockname()[1]) - path = '/' - - self.requestline = 'ERROR: Redirecting to https://%s%s' % (host, path) - self.request_version = 'HTTP/1.1' - self.command = 'GET' - self.send_response(301, 'Document Moved') - self.send_header('Date', self.date_time_string()) - self.send_header('Location', 'https://%s%s' % (host, path)) - self.send_header('Connection', 'close') - self.send_header('Content-Length', '0') - self.wfile.write('\r\n') - - class SecureThreadPoolServer(TLSSocketServerMixIn, ThreadPoolServer): - def __init__(self, config): - ThreadPoolServer.__init__(self, config) - - cert = open(config.ssl_certificate).read() - x509 = X509() - x509.parse(cert) - self.certChain = X509CertChain([x509]) - - priv = open(config.ssl_privkey).read() - self.privateKey = parsePEMKey(priv, private=True) - - self.sessionCache = SessionCache() - - def finish_request(self, sock, client_address): - # Peek into the packet, if it starts with GET or POS(T) then - # redirect, otherwise let TLSLite handle the connection. - peek = sock.recv(3, socket.MSG_PEEK).lower() - if peek == 'get' or peek == 'pos': - SecureRequestRedirect(sock, client_address, self) - return - tls_connection = TLSConnection(sock) - if self.handshake(tls_connection): - self.RequestHandlerClass(tls_connection, client_address, self) - else: - # This will probably fail because the TLSConnection has - # already written SSL stuff to the socket. But not sure what - # else we should do. - SecureRequestRedirect(sock, client_address, self) - - def handshake(self, tls_connection): - try: - tls_connection.handshakeServer(certChain=self.certChain, - privateKey=self.privateKey, - sessionCache=self.sessionCache) - tls_connection.ignoreAbruptClose = True - return True - except: - return False - - -def memoryProfileDecorator(func, profile): - """ Return a profiled function """ - def profiledFunction(*args, **kw): - profile.addRequest() - return func(*args, **kw) - return profiledFunction - - -def hotshotProfileDecorator(func, profile): - """ Return a profiled function """ - profile.moin_requests_done = 0 - def profiledFunction(*args, **kw): - profile.moin_requests_done += 1 - if profile.moin_requests_done == 1: - # Don't profile first request, its not interesting - return func(*args, **kw) - return profile.runcall(func, *args, **kw) - - return profiledFunction - - -def quit(signo, stackframe): - """ Signal handler for aborting signals """ - global httpd, config - logging.info("Thanks for using MoinMoin!") - - fname = config.pycallgraph_output - if fname: - import pycallgraph - if fname.endswith('.png'): - pycallgraph.make_dot_graph(fname) - elif fname.endswith('.dot'): - pycallgraph.save_dot(fname) - - if httpd: - httpd.die() - - -def registerSignalHandlers(func): - """ Register signal handlers on platforms that support it """ - try: - import signal - signal.signal(signal.SIGABRT, func) - signal.signal(signal.SIGINT, func) - signal.signal(signal.SIGTERM, func) - except ImportError: - pass - - -def makeServer(config): - """ Create a new server, based on the the platform capabilities - - Try to create the server class specified in the config. If threads - are not available, fallback to ForkingServer. If fork is not - available, fallback to a SimpleServer. - """ - serverClass = globals()[config.serverClass] - if serverClass.use_threads: - try: - import threading - except ImportError: - serverClass = ForkingServer - if serverClass is ForkingServer and not hasattr(os, "fork"): - serverClass = SimpleServer - if serverClass.__name__ != config.serverClass: - logging.error('%s is not available on this platform, falling back ' - 'to %s\n' % (config.serverClass, serverClass.__name__)) - - from MoinMoin import config as _config - _config.use_threads = serverClass.use_threads - return serverClass(config) - -# ------------------------------------------------------------------------ -# Public interface - -class StandaloneConfig(Config): - """ Standalone server default config """ - name = 'moin' - properties = {} - docs = '/usr/share/moin/htdocs' - user = 'www-data' - group = 'www-data' - port = 8000 - interface = 'localhost' - - # Advanced options - serverClass = 'ThreadPoolServer' - threadLimit = 10 - # The size of the listen backlog. Twisted uses a default of 50. - # Tests on Mac OS X show many failed request with backlog of 5 or 10. - requestQueueSize = 50 - - # Development options - memoryProfile = None - hotshotProfile = None - cProfile = None # internal use only - cProfileProfile = None - pycallgraph_output = None - -def cProfileDecorator(func, profile): - """ Return a profiled function """ - profile.moin_requests_done = 0 - def profiledFunction(*args, **kw): - profile.moin_requests_done += 1 - if profile.moin_requests_done == 1: - # Don't profile first request, it's not interesting - return func(*args, **kw) - return profile.runcall(func, *args, **kw) - - return profiledFunction - -def run(configClass): - """ Create and run a moin server - - See StandaloneConfig for available options - - @param configClass: config class - """ - # Run only once! - global httpd, config - if httpd is not None: - raise RuntimeError("You can run only one server per process!") - - config = configClass() - - if config.hotshotProfile and config.cProfileProfile: - raise RuntimeError("You cannot run two profilers simultaneously.") - - # Install hotshot profiled serve_moin method. To compare with other - # servers, we profile the part that create and run the request. - if config.hotshotProfile: - import hotshot - config.hotshotProfile = hotshot.Profile(config.hotshotProfile) - MoinRequestHandler.serve_moin = hotshotProfileDecorator( - MoinRequestHandler.serve_moin, config.hotshotProfile) - - if config.cProfileProfile: - import cProfile - # Create a new cProfile.Profile object using config.cProfileProfile - # as the path for the output file. - config.cProfile = cProfile.Profile() - MoinRequestHandler.serve_moin = cProfileDecorator( - MoinRequestHandler.serve_moin, config.cProfile) - - # Install a memory profiled serve_moin method - if config.memoryProfile: - config.memoryProfile.sample() - MoinRequestHandler.serve_moin = memoryProfileDecorator( - MoinRequestHandler.serve_moin, config.memoryProfile) - - # initialize pycallgraph, if wanted - if config.pycallgraph_output: - try: - import pycallgraph - pycallgraph.settings['include_stdlib'] = False - pcg_filter = pycallgraph.GlobbingFilter(exclude=['pycallgraph.*', - 'unknown.*', - ], - max_depth=9999) - pycallgraph.start_trace(reset=True, filter_func=pcg_filter) - except ImportError: - config.pycallgraph_output = None - - - registerSignalHandlers(quit) - httpd = makeServer(config) - - # Run as a safe user (posix only) - if os.name == 'posix' and os.getuid() == 0: - switchUID(config.uid, config.gid) - - httpd.serve_forever() -
--- a/MoinMoin/server/server_twisted.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,283 +0,0 @@ -# -*- coding: iso-8859-1 -*- -""" - MoinMoin.server.server_twisted - - Create standalone twisted based server. - - Minimal usage: - - from MoinMoin.server.server_twisted import TwistedConfig, makeApp - - class Config(TwistedConfig): - docs = '/usr/share/moin/wiki/htdocs' - user = 'www-data' - group = 'www-data' - - application = makeApp(Config) - - Then run this code with twistd -y yourcode.py. See moin_twisted script. - - @copyright: 2004 Thomas Waldmann, Oliver Graf, Nir Soffer - @license: GNU GPL, see COPYING for details. -""" - -from twisted.application import internet, service -from twisted.web import static, server, vhost, resource -from twisted.internet import threads, reactor - -try: - from twisted.internet import ssl -except ImportError: - ssl = None - -# Enable threads -from twisted.python import threadable -threadable.init(1) - -# MoinMoin imports -from MoinMoin.request import request_twisted -from MoinMoin.server import Config - -# Set threads flag, so other code can use proper locking -from MoinMoin import config -config.use_threads = True -del config - -# Server globals -config = None - - -class WikiResource(resource.Resource): - """ Wiki resource """ - isLeaf = 1 - - def render(self, request): - return server.NOT_DONE_YET - - -class WikiRoot(resource.Resource): - """ Wiki root resource """ - - def getChild(self, name, request): - # Serve images and css from url_prefix_static - if request.prepath == [] and name == config.url_prefix_static[1:]: - return resource.Resource.getChild(self, name, request) - - # Serve special 'root' files from url_prefix_static - elif name in ['favicon.ico', 'robots.txt'] and request.postpath == []: - return self.children[config.url_prefix_static[1:]].getChild(name, request) - - # All other through moin - - # TODO: fix profile code to include the request init and ignore - # first request. I'm not doing this now since its better done - # with the new twisted code waiting in fix branch. --Nir - else: - if config.memoryProfile: - config.memoryProfile.addRequest() - req = request_twisted.Request(request, name, reactor, properties=config.properties) - if config.hotshotProfile: - threads.deferToThread(config.hotshotProfile.runcall, req.run) - else: - threads.deferToThread(req.run) - return WikiResource() - - -class MoinRequest(server.Request): - """ MoinMoin request - - Enable passing of file-upload filenames - """ - - def requestReceived(self, command, path, version): - """ Called by channel when all data has been received. - - Override server.Request method for POST requests, to fix file - uploads issue. - """ - if command == 'POST': - self.requestReceivedPOST(path, version) - else: - server.Request.requestReceived(self, command, path, version) - - def requestReceivedPOST(self, path, version): - """ Handle POST requests - - This is a modified copy of server.Request.requestRecived, - modified to use cgi.FieldStorage to handle file uploads - correctly. - - Creates an extra member extended_args which also has - filenames of file uploads ( FIELDNAME__filename__ ). - """ - import cgi - - self.content.seek(0, 0) - self.args = {} - self.extended_args = {} - self.stack = [] - - self.method = 'POST' - self.uri = path - self.clientproto = version - x = self.uri.split('?') - - argstring = "" - if len(x) == 1: - self.path = self.uri - else: - if len(x) != 2: - from twisted.python import log - log.msg("May ignore parts of this invalid URI: %s" - % repr(self.uri)) - self.path, argstring = x[0], x[1] - - # cache the client and server information, we'll need this later to be - # serialized and sent with the request so CGIs will work remotely - self.client = self.channel.transport.getPeer() - self.host = self.channel.transport.getHost() - - # create dummy env for cgi.FieldStorage - env = { - 'REQUEST_METHOD': self.method, - 'QUERY_STRING': argstring, - } - form = cgi.FieldStorage(fp=self.content, - environ=env, - headers=self.received_headers) - - # Argument processing - - args = self.args - try: - keys = form.keys() - except TypeError: - pass - else: - for key in keys: - values = form[key] - if not isinstance(values, list): - values = [values] - fixedResult = [] - for i in values: - if isinstance(i, cgi.FieldStorage) and i.filename: - fixedResult.append(i.file) - # multiple uploads to same form field are stupid! - args[key + '__filename__'] = i.filename - else: - fixedResult.append(i.value) - args[key] = fixedResult - - self.process() - - -class MoinSite(server.Site): - """ Moin site """ - requestFactory = MoinRequest - - def startFactory(self): - """ Setup before starting """ - # Memory profile - if config.memoryProfile: - config.memoryProfile.sample() - - # hotshot profile - if config.hotshotProfile: - import hotshot - config.hotshotProfile = hotshot.Profile(config.hotshotProfile) - server.Site.startFactory(self) - - def stopFactory(self): - """ Cleaup before stoping """ - server.Site.stopFactory(self) - if config.hotshotProfile: - config.hotshotProfile.close() - - -class TwistedConfig(Config): - """ Twisted server default config """ - - name = 'mointwisted' - properties = {} - docs = '/usr/share/moin/htdocs' - user = 'www-data' - group = 'www-data' - port = 8080 - interfaces = [''] - threads = 10 - timeout = 15 * 60 # 15 minutes - logPath_twisted = None # Twisted log file - virtualHosts = None - memoryProfile = None - hotshotProfile = None - - # sslcert = ('/whereever/cert/sitekey.pem', '/whereever/cert/sitecert.pem') - sslcert = None - - def __init__(self): - Config.__init__(self) - - # Check for '' in interfaces, then ignore other - if '' in self.interfaces: - self.interfaces = [''] - - -def makeApp(ConfigClass): - """ Generate and return an application - - See MoinMoin.server.Config for config options - - @param ConfigClass: config class - @rtype: application object - @return twisted application, needed by twistd - """ - # Create config instance (raise RuntimeError if config invalid) - global config - config = ConfigClass() - - # Set number of threads - reactor.suggestThreadPoolSize(config.threads) - - # The root of the HTTP hierarchy - default = WikiRoot() - - # Here is where img and css and some special files come from - default.putChild(config.url_prefix_static[1:], static.File(config.docs)) - - # Generate the Site factory - # TODO: Maybe we can use WikiRoot instead of this - # ---------------------------------------------- - root = vhost.NameVirtualHost() - root.default = default - # ---------------------------------------------- - site = MoinSite(root, logPath=config.logPath_twisted, timeout=config.timeout) - - # Make application - application = service.Application("web", uid=config.uid, gid=config.gid) - sc = service.IServiceCollection(application) - - # Listen to all interfaces in config.interfaces - for entry in config.interfaces: - # Add a TCPServer for each interface. - - # This is an hidden experimantal feature: each entry in - # interface may contain a port, using 'ip:port'. - # Note: the format is subject to change! - try: - interface, port = entry.split(':', 1) - except ValueError: - interface, port = entry, config.port - - # Might raise ValueError if not integer. - # TODO: check if we can use string port, like 'http' - port = int(port) - - if port == 443 and ssl and ssl.supported and config.sslcert: - sslContext = ssl.DefaultOpenSSLContextFactory(*config.sslcert) - s = internet.SSLServer(port, site, sslContext, interface=interface) - else: - s = internet.TCPServer(port, site, interface=interface) - s.setServiceParent(sc) - - return application -
--- a/MoinMoin/server/server_wsgi.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,39 +0,0 @@ -""" - MoinMoin - WSGI application - - Minimal code for using this: - - from MoinMoin.server.server_wsgi import WsgiConfig, moinmoinApp - - class Config(WsgiConfig): - pass - - config = Config() # you MUST create an instance - # use moinmoinApp here with your WSGI server / gateway - - @copyright: 2005 Anakim Border <akborder@gmail.com>, - 2007 MoinMoin:ThomasWaldmann - @license: GNU GPL, see COPYING for details. -""" - -from MoinMoin.server import Config -from MoinMoin.request import request_wsgi - -class WsgiConfig(Config): - """ WSGI default config """ - pass - - -def moinmoinApp(environ, start_response): - request = request_wsgi.Request(environ) - request.run() - start_response(request.status, request.headers) - if request._send_file is not None: - # moin wants to send a file (e.g. AttachFile.do_get) - def simple_wrapper(fileobj, bufsize): - return iter(lambda: fileobj.read(bufsize), '') - file_wrapper = environ.get('wsgi.file_wrapper', simple_wrapper) - return file_wrapper(request._send_file, request._send_bufsize) - else: - return [request.output()] # don't we have a filelike there also!? -
--- a/MoinMoin/session.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/session.py Tue Sep 23 23:02:50 2008 +0200 @@ -19,6 +19,7 @@ from MoinMoin import caching from MoinMoin.user import User from MoinMoin.util import random_string +from MoinMoin.web.utils import cookie_date import time, random class SessionData(object): @@ -312,12 +313,12 @@ if cfg.cookie_path: cookie[cookie_name]['path'] = cfg.cookie_path else: - path = request.getScriptname() + path = request.script_root if not path: path = '/' cookie[cookie_name]['path'] = path # Set expires for older clients - cookie[cookie_name]['expires'] = request.httpDate(when=expires, rfc='850') + cookie[cookie_name]['expires'] = cookie_date(expires) # a secure cookie is not transmitted over unsecure connections: if (cfg.cookie_secure or # True means: force secure cookies cfg.cookie_secure is None and request.is_ssl): # None means: https -> secure cookie @@ -326,11 +327,13 @@ def _set_cookie(self, request, cookie_string, expires): """ Set cookie, raw helper. """ - lifetime = int(expires - time.time()) - cookie = self._make_cookie(request, self.cookie_name, cookie_string, - lifetime, expires) + lifetime = expires - time.time() + domain = request.cfg.cookie_domain or None + path = request.cfg.cookie_path or None # Set cookie - request.setHttpHeader(cookie) + request.set_cookie(self.cookie_name, cookie_string, + max_age=lifetime, expires=expires, + path=path, domain=domain) # IMPORTANT: Prevent caching of current page and cookie request.disableHttpCaching() @@ -341,8 +344,10 @@ def get(self, request): session_name = None - if request.cookie and self.cookie_name in request.cookie: - session_name = request.cookie[self.cookie_name].value + if request.cookies and self.cookie_name in request.cookies: + session_name = request.cookies[self.cookie_name] + if hasattr(session_name, 'value'): + session_name = session_name.value session_name = ''.join([c for c in session_name if c in self._SESSION_NAME_CHARS]) session_name = session_name[:self._SESSION_NAME_LEN] @@ -351,7 +356,7 @@ def _get_anon_session_lifetime(request): - if request.cfg.anonymous_session_lifetime: + if hasattr(request.cfg, 'anonymous_session_lifetime'): return request.cfg.anonymous_session_lifetime * 3600 return 0 @@ -411,12 +416,12 @@ if user_obj: sessiondata.is_stored = True else: - store = not (not request.cfg.anonymous_session_lifetime) + store = hasattr(request.cfg, 'anonymous_session_lifetime') sessiondata.is_stored = store else: session_name = session_id_handler.generate_new_id(request) logging.debug("starting session (new session_name %r)" % session_name) - store = not (not request.cfg.anonymous_session_lifetime) + store = hasattr(request.cfg, 'anonymous_session_lifetime') sessiondata = self.dataclass(request, session_name) sessiondata.is_new = True sessiondata.is_stored = store
--- a/MoinMoin/stats/hitcounts.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/stats/hitcounts.py Tue Sep 23 23:02:50 2008 +0200 @@ -151,7 +151,7 @@ filterpage = wikiutil.decodeUserInput(params) if request and request.form and 'page' in request.form: - filterpage = request.form['page'][0] + filterpage = request.form['page'] days, views, edits = get_data(pagename, request, filterpage) @@ -201,7 +201,7 @@ # check params filterpage = None if request and request.form and 'page' in request.form: - filterpage = request.form['page'][0] + filterpage = request.form['page'] days, views, edits = get_data(pagename, request, filterpage) @@ -252,11 +252,8 @@ (request.cfg.chart_options['width'], request.cfg.chart_options['height']), image, days) - headers = [ - "Content-Type: image/gif", - "Content-Length: %d" % len(image.getvalue()), - ] - request.emit_http_headers(headers) + request.content_type = 'image/gif' + request.content_length = len(image.getvalue()) # copy the image image.reset()
--- a/MoinMoin/stats/pagesize.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/stats/pagesize.py Tue Sep 23 23:02:50 2008 +0200 @@ -113,11 +113,8 @@ (request.cfg.chart_options['width'], request.cfg.chart_options['height']), image, labels) - headers = [ - "Content-Type: image/gif", - "Content-Length: %d" % len(image.getvalue()), - ] - request.emit_http_headers(headers) + request.content_type = 'image/gif' + request.content_length = len(image.getvalue()) # copy the image image.reset()
--- a/MoinMoin/stats/useragents.py Tue Sep 23 00:44:45 2008 +0200 +++ b/MoinMoin/stats/useragents.py Tue Sep 23 23:02:50 2008 +0200 @@ -173,11 +173,8 @@ (request.cfg.chart_options['width'], request.cfg.chart_options['height']), image, labels) - headers = [ - "Content-Type: image/gif", - "Content-Length: %d" % len(image.getvalue()), - ] - request.emit_http_headers(headers) + request.content_type = 'image/gif' + request.content_length = len(image.getvalue()) # copy the image image.reset()
--- a/MoinMoin/support/cgitb.py Tue Sep 23 00:44:45 2008 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,619 +0,0 @@ -"""More comprehensive traceback formatting for Python scripts. - -To enable this module, do: - - import cgitb; cgitb.enable() - -at the top of your script. The optional arguments to enable() are: - - display - if true, tracebacks are displayed in the web browser - logdir - if set, tracebacks are written to files in this directory - context - number of lines of source code to show for each stack frame - format - 'text' or 'html' controls the output format - viewClass - sub class of View. Create this if you want to customize the - layout of the traceback. - debug - may be used by viewClass to decide on level of detail - -By default, tracebacks are displayed but not saved, the context is 5 lines -and the output format is 'html' (for backwards compatibility with the -original use of this module). - -Alternatively, if you have caught an exception and want cgitb to display it -for you, call cgitb.handler(). The optional argument to handler() is a -3-item tuple (etype, evalue, etb) just like the value of sys.exc_info(). -The default handler displays output as HTML. - - -2005-04-22 Nir Soffer <nirs@freeshell.org> - -Rewrite: - - Refactor html and text functions to View class, HTMLFormatter and - TextFormatter. No more duplicate formating code. - - Layout is done with minimal html and css, in a way it can't be - affected by surrounding code. - - Built to be easy to subclass and modify without duplicating code. - - Change layout, important details come first. - - Factor frame analyzing and formatting into separate class. - - Add debug argument, can be used to change error display e.g. user - error view, developer error view. - - Add viewClass argument, make it easy to customize the traceback view. - - Easy to customize system details and application details. - -The main goal of this rewrite was to have a traceback that can render -few tracebacks combined. It's needed when you wrap an expection and want -to print both the traceback up to the wrapper exception, and the -original traceback. There is no code to support this here, but it's easy -to add by using your own View sub class. -""" - -__author__ = 'Ka-Ping Yee' -__version__ = '$Revision: 1.10 $' - -import sys, os, pydoc, inspect, linecache, tokenize, keyword - - -def reset(): - """ Reset the CGI and the browser - - Return a string that resets the CGI and browser to a known state. - TODO: probably some of this is not needed any more. - """ - return """<!--: spam -Content-Type: text/html - -<body><font style="color: white; font-size: 1px"> --> -<body><font style="color: white; font-size: 1px"> --> --> -</font> </font> </font> </script> </object> </blockquote> </pre> -</table> </table> </table> </table> </table> </font> </font> </font> -""" - -__UNDEF__ = [] # a special sentinel object - - -class HiddenObject: - def __repr__(self): - return "<HIDDEN>" -HiddenObject = HiddenObject() - -class HTMLFormatter: - """ Minimal html formatter """ - - def attributes(self, attributes=None): - if attributes: - result = [' %s="%s"' % (k, v) for k, v in attributes.items()] - return ''.join(result) - return '' - - def tag(self, name, text, attributes=None): - return '<%s%s>%s</%s>\n' % (name, self.attributes(attributes), text, name) - - def section(self, text, attributes=None): - return self.tag('div', text, attributes) - - def title(self, text, attributes=None): - return self.tag('h1', text, attributes) - - def subTitle(self, text, attributes=None): - return self.tag('h2', text, attributes) - - def subSubTitle(self, text, attributes=None): - return self.tag('h3', text, attributes) - - def paragraph(self, text, attributes=None): - return self.tag('p', text, attributes) - - def list(self, items, attributes=None): - return self.formatList('ul', items, attributes) - - def orderedList(self, items, attributes=None): - return self.formatList('ol', items, attributes) - - def formatList(self, name, items, attributes=None): - """ Send list of raw texts or formatted items. """ - if isinstance(items, (list, tuple)): - items = '\n' + ''.join([self.listItem(i) for i in items]) - return self.tag(name, items, attributes) - - def listItem(self, text, attributes=None): - return self.tag('li', text, attributes) - - def link(self, href, text, attributes=None): - if attributes is None: - attributes = {} - attributes['href'] = href - return self.tag('a', text, attributes) - - def strong(self, text, attributes=None): - return self.tag('strong', text, attributes) - - def em(self, text, attributes=None): - return self.tag('em', text, attributes) - - def repr(self, object): - return pydoc.html.repr(object) - - -class TextFormatter: - """ Plain text formatter """ - - def section(self, text, attributes=None): - return text - - def title(self, text, attributes=None): - lineBellow = '=' * len(text) - return '%s\n%s\n\n' % (text, lineBellow) - - def subTitle(self, text, attributes=None): - lineBellow = '-' * len(text) - return '%s\n%s\n\n' % (text, lineBellow) - - def subSubTitle(self, text, attributes=None): - lineBellow = '~' * len(text) - return '%s\n%s\n\n' % (text, lineBellow) - - def paragraph(self, text, attributes=None): - return text + '\n\n' - - def list(self, items, attributes=None): - if isinstance(items, (list, tuple)): - items = [' * %s\n' % i for i in items] - return ''.join(items) + '\n' - return items - - def orderedList(self, items, attributes=None): - if isinstance(items, (list, tuple)): - result = [] - for i in range(items): - result.append(' %d. %s\n' % (i, items[i])) - return ''.join(result) + '\n' - return items - - def listItem(self, text, attributes=None): - return ' * %s\n' % text - - def link(self, href, text, attributes=None): - return '[[%s]]' % text - - def strong(self, text, attributes=None): - return text - - def em(self, text, attributes=None): - return text - - def repr(self, object): - return repr(object) - - -class Frame: - """ Analyze and format single frame in a traceback """ - - def __init__(self, frame, file, lnum, func, lines, index): - self.frame = frame - self.file = file - self.lnum = lnum - self.func = func - self.lines = lines - self.index = index - - def format(self, formatter): - """ Return formatted content """ - self.formatter = formatter - vars, highlight = self.scan() - items = [self.formatCall(), - self.formatContext(highlight), - self.formatVariables(vars)] - return ''.join(items) - - # ----------------------------------------------------------------- - # Private - formatting - - def formatCall(self): - call = '%s in %s%s' % (self.formatFile(), - self.formatter.strong(self.func), - self.formatArguments(),) - return self.formatter.paragraph(call, {'class': 'call'}) - - def formatFile(self): - """ Return formatted file link """ - if not self.file: - return '?' - file = pydoc.html.escape(os.path.abspath(self.file)) - return self.formatter.link('file://' + file, file) - - def formatArguments(self): - """ Return formated arguments list """ - if self.func == '?': - return '' - - def formatValue(value): - return '=' + self.formatter.repr(value) - - args, varargs, varkw, locals = inspect.getargvalues(self.frame) - return inspect.formatargvalues(args, varargs, varkw, locals, - formatvalue=formatValue) - - def formatContext(self, highlight): - """ Return formatted context, next call highlighted """ - if self.index is None: - return '' - context = [] - i = self.lnum - self.index - for line in self.lines: - line = '%5d %s' % (i, pydoc.html.escape(line)) - attributes = {} - if i in highlight: - attributes = {'class': 'highlight'} - context.append(self.formatter.listItem(line, attributes)) - i += 1 - context = '\n' + ''.join(context) + '\n' - return self.formatter.orderedList(context, {'class': 'context'}) - - def formatVariables(self, vars): - """ Return formatted variables """ - done = {} - dump = [] - for name, where, value in vars: - if name in done: - continue - done[name] = 1 - if value is __UNDEF__: - dump.append('%s %s' % (name, self.formatter.em('undefined'))) - else: - dump.append(self.formatNameValue(name, where, value)) - return self.formatter.list(dump, {'class': 'variables'}) - - def formatNameValue(self, name, where, value): - """ Format variable name and value according to scope """ - if where in ['global', 'builtin']: - name = '%s %s' % (self.formatter.em(where), - self.formatter.strong(name)) - elif where == 'local': - name = self.formatter.strong(name) - else: - name = where + self.formatter.strong(name.split('.')[-1]) - return '%s = %s' % (name, self.formatter.repr(value)) - - # --------------------------------------------------------------- - # Private - analyzing code - - def scan(self): - """ Scan frame for vars while setting highlight line """ - highlight = {} - - def reader(lnum=[self.lnum]): - highlight[lnum[0]] = 1 - try: - return linecache.getline(self.file, lnum[0]) - finally: - lnum[0] += 1 - - vars = self.scanVariables(reader) - return vars, highlight - - def scanVariables(self, reader): - """ Lookup variables in one logical Python line """ - vars, lasttoken, parent, prefix, value = [], None, None, '', __UNDEF__ - for ttype, token, start, end, line in tokenize.generate_tokens(reader): - if ttype == tokenize.NEWLINE: - break - if ttype == tokenize.NAME and token not in keyword.kwlist: - if lasttoken == '.': - if parent is not __UNDEF__: - if self.unsafe_name(token): - value = HiddenObject - else: - value = getattr(parent, token, __UNDEF__) - vars.append((prefix + token, prefix, value)) - else: - where, value = self.lookup(token) - vars.append((token, where, value)) - elif token == '.': - prefix += lasttoken + '.' - parent = value - else: - parent, prefix = None, '' - lasttoken = token - return vars - - def lookup(self, name): - """ Return the scope and the value of name """ - scope = None - value = __UNDEF__ - locals = inspect.getargvalues(self.frame)[3] - if name in locals: - scope, value = 'local', locals[name] - elif name in self.frame.f_globals: - scope, value = 'global', self.frame.f_globals[name] - elif '__builtins__' in self.frame.f_globals: - scope = 'builtin' - builtins = self.frame.f_globals['__builtins__'] - if isinstance(builtins, dict): - value = builtins.get(name, __UNDEF__) - else: - value = getattr(builtins, name, __UNDEF__) - if self.unsafe_name(name): - value = HiddenObject - return scope, value - - def unsafe_name(self, name): - return name in self.frame.f_globals.get("unsafe_names", ()) - -class View: - """ Traceback view """ - - frameClass = Frame # analyze and format a frame - - def __init__(self, info=None, debug=0): - """ Save starting info or current exception info """ - self.info = info or sys.exc_info() - self.debug = debug - - def format(self, formatter, context=5): - self.formatter = formatter - self.context = context - return formatter.section(self.formatContent(), {'class': 'cgitb'}) - - def formatContent(self): - """ General layout - override to change layout """ - content = ( - self.formatStylesheet(), - self.formatTitle(), - self.formatMessage(), - self.formatTraceback(), - self.formatSystemDetails(), - self.formatTextTraceback(), - ) - return ''.join(content) - - # ----------------------------------------------------------------- - # Stylesheet - - def formatStylesheet(self): - """ Format inline html stylesheet """ - return '<style type="text/css">%s</style>' % self.stylesheet() - - def stylesheet(self): - """ Return stylesheet rules. Override to change rules. - - The rules are sparated to make it easy to extend. - - The stylesheet must work even if sorounding code define the same - css names, and it must not change the sorounding code look and - behavior. This is done by having all content in a .traceback - section. - """ - return """ -.cgitb {background: #E6EAF0; border: 1px solid #4D6180; direction: ltr;} -.cgitb p {margin: 0.5em 0; padding: 5px 10px; text-align: left;} -.cgitb ol {margin: 0} -.cgitb li {margin: 0.25em 0;} -.cgitb h1, .cgitb h2, .cgitb h3 {padding: 5px 10px; margin: 0; background: #4D6180; color: white;} -.cgitb h1 {font-size: 1.3em;} -.cgitb h2 {font-size: 1em; margin-top: 1em;} -.cgitb h3 {font-size: 1em;} -.cgitb .frames {margin: 0; padding: 0; color: #606060} -.cgitb .frames li {display: block;} -.cgitb .call {padding: 5px 10px; background: #A3B4CC; color: black} -.cgitb .context {padding: 0; font-family: monospace; } -.cgitb .context li {display: block; white-space: pre;} -.cgitb .context li.highlight {background: #C0D3F0; color: black} -.cgitb .variables {padding: 5px 10px; font-family: monospace;} -.cgitb .variables li {display: inline;} -.cgitb .variables li:after {content: ", ";} -.cgitb .variables li:last-child:after {content: "";} -.cgitb .exception {border: 1px solid #4D6180; margin: 10px} -.cgitb .exception h3 {background: #4D6180; color: white;} -.cgitb .exception p {color: black;} -.cgitb .exception ul {padding: 0 10px; font-family: monospace;} -.cgitb .exception li {display: block;} -""" - - # ----------------------------------------------------------------- - # Head - - def formatTitle(self): - return self.formatter.title(self.exceptionTitle(self.info)) - - def formatMessage(self): - return self.formatter.paragraph(self.exceptionMessage(self.info)) - - # ----------------------------------------------------------------- - # Traceback - - def formatTraceback(self): - """ Return formatted traceback """ - return self.formatOneTraceback(self.info) - - def formatOneTraceback(self, info): - """ Format one traceback - - Separate to enable formatting multiple tracebacks. - """ - output = [self.formatter.subTitle('Traceback'), - self.formatter.paragraph(self.tracebackText(info)), - self.formatter.orderedList(self.tracebackFrames(info), - {'class': 'frames'}), - self.formatter.section(self.formatException(info), - {'class': 'exception'}), ] - return self.formatter.section(''.join(output), {'class': 'traceback'}) - - def tracebackFrames(self, info): - frames = [] - traceback = info[2] - for record in inspect.getinnerframes(traceback, self.context): - frame = self.frameClass(*record) - frames.append(frame.format(self.formatter)) - del traceback - return frames - - def tracebackText(self, info): - return '''A problem occurred in a Python script. Here is the - sequence of function calls leading up to the error, in the - order they occurred.''' - - # -------------------------------------------------------------------- - # Exception - - def formatException(self, info): - items = [self.formatExceptionTitle(info), - self.formatExceptionMessage(info), - self.formatExceptionAttributes(info), ] - return ''.join(items) - - def formatExceptionTitle(self, info): - return self.formatter.subSubTitle(self.exceptionTitle(info)) - - def formatExceptionMessage(self, info): - return self.formatter.paragraph(self.exceptionMessage(info)) - - def formatExceptionAttributes(self, info): - attribtues = [] - for name, value in self.exceptionAttributes(info): - value = self.formatter.repr(value) - attribtues.append('%s = %s' % (name, value)) - return self.formatter.list(attribtues) - - def exceptionAttributes(self, info): - """ Return list of tuples [(name, value), ...] """ - instance = info[1] - attribtues = [] - for name in dir(instance): - if name.startswith('_'): - continue - value = getattr(instance, name) - attribtues.append((name, value)) - return attribtues - - def exceptionTitle(self, info): - type = info[0] - return getattr(type, '__name__', str(type)) - - def exceptionMessage(self, info): - instance = info[1] - return pydoc.html.escape(str(instance)) - - - # ----------------------------------------------------------------- - # System details - - def formatSystemDetails(self): - details = ['Date: %s' % self.date(), - 'Platform: %s' % self.platform(), - 'Python: %s' % self.python(), ] - details += self.applicationDetails() - return (self.formatter.subTitle('System Details') + - self.formatter.list(details, {'class': 'system'})) - - def date(self): - import time - rfc2822Date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime()) - return rfc2822Date - - def platform(self): - try: - return pydoc.html.escape(' '.join(os.uname())) - except: - return pydoc.html.escape('%s (%s)' % (sys.platform, os.name)) - - def python(self): - return 'Python %s (%s)' % (sys.version.split()[0], sys.executable) - - def applicationDetails(self): - """ Override for your application """ - return [] - - # ----------------------------------------------------------------- - # Text traceback - - def formatTextTraceback(self): - template = self.textTracebackTemplate() - return template % self.formatOneTextTraceback(self.info) - - def formatOneTextTraceback(self, info): - """ Separate to enable formatting multiple tracebacks. """ - import traceback - return pydoc.html.escape(''.join(traceback.format_exception(*info))) - - def textTracebackTemplate(self): - return ''' - -<!-- The above is a description of an error in a Python program, - formatted for a Web browser. In case you are not reading this - in a Web browser, here is the original traceback: - -%s ---> -''' - - -class Hook: - """A hook to replace sys.excepthook that shows tracebacks in HTML.""" - - def __init__(self, display=1, logdir=None, context=5, file=None, - format="html", viewClass=View, debug=0): - self.display = display # send tracebacks to browser if true - self.logdir = logdir # log tracebacks to files if not None - self.context = context # number of source code lines per frame - self.file = file or sys.stdout # place to send the output - self.format = format - self.viewClass = viewClass - self.debug = debug - - def __call__(self, etype, evalue, etb): - self.handle((etype, evalue, etb)) - - def handle(self, info=None): - info = info or sys.exc_info() - if self.format.lower() == "html": - formatter = HTMLFormatter() - self.file.write(reset()) - plain = False - else: - formatter = TextFormatter() - plain = True - try: - view = self.viewClass(info, self.debug) - doc = view.format(formatter, self.context) - except: - raise - import traceback - doc = ''.join(traceback.format_exception(*info)) - plain = True - - if self.display: - if plain: - doc = doc.replace('&', '&').replace('<', '<') - self.file.write('<pre>' + doc + '</pre>\n') - else: - self.file.write(doc + '\n') - else: - self.file.write('<p>A problem occurred in a Python script.\n') - - if self.logdir is not None: - import tempfile - suffix = ['.txt', '.html'][self.format == "html"] - (fd, path) = tempfile.mkstemp(suffix=suffix, dir=self.logdir) - try: - file = os.fdopen(fd, 'w') - file.write(doc) - file.close() - msg = '<p> %s contains the description of this error.' % path - except: - msg = '<p> Tried to save traceback to %s, but failed.' % path - self.file.write(msg + '\n') - try: - self.file.flush() - except: pass - - -handler = Hook().handle - -def enable(display=1, logdir=None, context=5, format="html", viewClass=View, debug=0): - """Install an exception handler that formats tracebacks as HTML. - - The optional argument 'display' can be set to 0 to suppress sending the - traceback to the browser, and 'logdir' can be set to a directory to cause - tracebacks to be written to files there.""" - sys.excepthook = Hook(display=display, logdir=logdir, context=context, - format=format, viewClass=viewClass, debug=debug) -
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/flup/ChangeLog Tue Sep 23 23:02:50 2008 +0200 @@ -0,0 +1,243 @@ +2008-07-23 Allan Saddi <allan@saddi.com> + + * Add support for configuring UNIX domain sockets (for servers that + support them) in the paste.server_factory implementations. Thanks + to Dan Roberts for the code. + +2008-07-22 Allan Saddi <allan@saddi.com> + + * Release 1.0.1 + + * Attempt to deduce missing PATH_INFO and/or QUERY_STRING from + REQUEST_URI, if present. Patch provided by Richard Davies. + +2007-09-10 Allan Saddi <allan@saddi.com> + + * Fix readline implementations so size argument is checked + earlier. + +2007-07-14 Allan Saddi <allan@saddi.com> + + * Prevent ThreadPool inconsistences if an exception is + actually raised. Thanks to Tim Chen for the patch. + +2007-06-05 Allan Saddi <allan@saddi.com> + + * Remove publisher and middleware packages. + * Add cgi server for completeness. + +2007-05-17 Allan Saddi <allan@saddi.com> + + * Fix fcgi_fork so it can run on Solaris. Thanks to + Basil Crow for the patch. + +2007-01-22 Allan Saddi <allan@saddi.com> + + * Fix eunuchs import issue. + +2007-01-10 Allan Saddi <allan@saddi.com> + + * Support gzip compression of XHTML pages using the + correct MIME type. + +2006-12-29 Allan Saddi <allan@saddi.com> + + * Deprecate WSGI_SCRIPT_NAME and scriptName in scgi_base. + Modern versions of mod_scgi correctly set SCRIPT_NAME & + PATH_INFO. + +2006-12-13 Allan Saddi <allan@saddi.com> + + * Fix problem in session.py seen when optimization is on. + +2006-12-05 Allan Saddi <allan@saddi.com> + + * Update servers to default to an empty QUERY_STRING if + not present in the environ. + * Update gzip.py: compresslevel -> compress_level + * Update gzip.py by updating docstrings and renaming + classes/methods/functions to better follow Python naming + conventions. NB: mimeTypes keyword parameter is now + mime_types. + +2006-12-02 Allan Saddi <allan@saddi.com> + + * Change intra-package imports into absolute imports. + +2006-12-02 Allan Saddi <allan@saddi.com> + + * Add forceCookieOutput attribute to SessionService to + force Set-Cookie output for the current request. + +2006-12-01 Allan Saddi <allan@saddi.com> + + * Update setup script. + +2006-11-26 Allan Saddi <allan@saddi.com> + + * Don't attempt to install signal handlers under Windows + to improve compatibility. + +2006-11-24 Allan Saddi <allan@saddi.com> + + * Add *_thread egg entry-point aliases. + * Add UNIX domain socket support to scgi, scgi_fork, + scgi_app. + * Add flup.client package which contains various + WSGI -> connector client implentations. (So far: FastCGI, + and SCGI.) + +2006-11-19 Allan Saddi <allan@saddi.com> + + * Change mime-type matching algorithm in GzipMiddleware. + Strip parameters (e.g. "encoding") and accept a list of + regexps. By default, compress 'text/.*' mime-types. + +2006-11-10 Allan Saddi <allan@saddi.com> + + * Add cookieAttributes to SessionService to make it easier + to customize the generated cookie's attributes. + +2006-08-28 Allan Saddi <allan@saddi.com> + + * Add support for FastCGI roles other than FCGI_RESPONDER. + Patch provided by Seairth Jacobs. + +2006-08-02 Allan Saddi <allan@saddi.com> + + * Add cookieExpiration keyword to SessionService / + SessionMiddleware to adjust the session cookie's expiration. + Thanks to Blaise Laflamme for the suggestion. + +2006-06-27 Allan Saddi <allan@saddi.com> + + * Set close-on-exec flag on all server sockets. Thanks to + Ralf Schmitt for reporting the problem. + +2006-06-18 Allan Saddi <allan@saddi.com> + + * Stop ignoring EPIPE exceptions, as this is probably the + wrong thing to do. (Application is unaware of disconnected + clients and the CPU spins when sending large files to a + disconnected client.) Thanks to Ivan Sagalaev for bringing + this to my attention. + + NB: Existing applications that use the flup servers may begin + seeing socket.error exceptions... + +2006-05-18 Allan Saddi <allan@saddi.com> + + * Added umask keyword parameter to fcgi and fcgi_fork, + for use when binding to a UNIX socket. + +2006-05-03 Allan Saddi <allan@saddi.com> + + * Fix illusive problem with AJP implementation. Thanks to + Moshe Van der Sterre for explaining the problem and + providing a fix. + +2006-04-06 Allan Saddi <allan@saddi.com> + + * Catch a strange FieldStorage case. Seen in production. + Not quite sure what causes it. + +2006-03-21 Allan Saddi <allan@saddi.com> + + * Add maxRequests option to PreforkServer. Patch provided by + Wojtek Sobczuk. + +2006-02-23 Allan Saddi <allan@saddi.com> + + * Add paste.server_factory-compliant factories and respective + egg entry points. Thanks to Luis Bruno for the code. + + Add debug option to servers, which is True by default. + Currently, only server-level error handling is affected. + +2006-01-15 Allan Saddi <allan@saddi.com> + + * Change the behavior of ImportingModuleResolver when dealing + with ImportErrors. Previously, it would act as if the module + did not exist. Now, it propagates the exception to another + level (outer middleware or WSGI). Reported by Scot Doyle. + +2006-01-05 Allan Saddi <allan@saddi.com> + + * Improve Windows compatibility by conditionally installing + SIGHUP handler. Thanks to Brad Miller for pointing out the + problem and providing a fix. + +2005-12-19 Allan Saddi <allan@saddi.com> + + * Fix socket leak in eunuchs socketpair() wrapper. Thanks to + Georg Bauer for pointing this out. + +2005-12-16 Allan Saddi <allan@saddi.com> + + * Switch to setuptools for egg support. + * Add higher-level 404 error page support. Thanks to Scot Doyle + for suggesting the idea and providing code. If you previously + subclassed Publisher to provide a custom 404 error page, this + is now broken. It will have to be massaged to fit the new + calling convention. + +2005-11-28 Allan Saddi <allan@saddi.com> + + * Fix issue with FCGI_GET_VALUES handling. Thanks to + Timothy Wright for pointing this out. + +2005-11-18 Allan Saddi <allan@saddi.com> + + * When running under Python < 2.4, attempt to use socketpair() + from eunuchs module. + +2005-09-07 Allan Saddi <allan@saddi.com> + + * Python 2.3 doesn't define socket.SHUT_WR, which affected + the closing of the FastCGI socket with the server. This would + cause output to hang. Thanks to Eugene Lazutkin for bringing + the problem to my attention and going out of his way to help + me debug it! + +2005-07-03 Allan Saddi <allan@saddi.com> + + * Ensure session identifiers only contain ASCII characters when + using a non-ASCII locale. Thanks to Ksenia Marasanova for the + the fix. + +2005-06-12 Allan Saddi <allan@saddi.com> + + * Cleanly close connection socket to avoid sending a TCP RST to + the web server. (fcgi_base) Fix suggested by Dima Barsky. + +2005-05-31 Allan Saddi <allan@saddi.com> + + * Take scriptName from the WSGI_SCRIPT_NAME environment variable + passed from the web server, if present. + * Check if scriptName is None, and if so, don't modify SCRIPT_NAME + & PATH_INFO. For better compatibility with cgi2scgi. (scgi_base) + +2005-05-18 Allan Saddi <allan@saddi.com> + + * Change default allowedServers for ajp and scgi to ['127.0.0.1']. + * Accept PATH_INFO from environment for scgi servers, in case + cgi2scgi is being used. Submitted by Ian Bicking. + * Change threaded servers so wsgi.multiprocess is False by default. + Allow it to be changed by keyword argument. + * Fix wsgi.multiprocess for scgi_fork. (Set to True.) + +2005-05-15 Allan Saddi <allan@saddi.com> + + * Prevent possible deadlock related to DiskSessionStore locking. + * Add logic to SessionStore so that it will block if attempting to + check out a Session that's already been checked out. + +2005-05-14 Allan Saddi <allan@saddi.com> + + * Convert the use of decorators in session.py to something + compatible with Python <2.4. + +2005-04-23 Allan Saddi <allan@saddi.com> + + * Ensure that SessionStore.checkOutSession() never returns an + invalidated Session. Reported by Rene Dudfield.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/flup/NOTES.moin Tue Sep 23 23:02:50 2008 +0200 @@ -0,0 +1,17 @@ +NOTES for bundled flup: +======================= +This directory contains the flup WSGI adapter package. It has been +patched to support completely singlethreaded non-forking servers for +deployment in webservers, that have their own process spawning strategy. + +The shipped version is available via mercurial checkout: + +hg clone -r aa983e43105e http://hg.saddi.com/flup-server + +The singlethreaded server patch was provided via a trac bugreport +(http://trac.saddi.com/flup/ticket/22) and is provided in this directory +as singleserver.diff for convenience. It has already been applied. + +Thanks to Allan Saddi <allan@saddi.com> for writing flup and to +the unnamed contributor <tv@inoi.fi> for the single-thread patch. +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/flup/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -0,0 +1,1 @@ +#
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/flup/client/__init__.py Tue Sep 23 23:02:50 2008 +0200 @@ -0,0 +1,1 @@ +#
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/MoinMoin/support/flup/client/fcgi_app.py Tue Sep 23 23:02:50 2008 +0200 @@ -0,0 +1,461 @@ +# Copyright (c) 2006 Allan Saddi <allan@saddi.com> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# $Id$ + +__author__ = 'Allan Saddi <allan@saddi.com>' +__version__ = '$Revision$' + +import select +import struct +import socket +import errno + +__all__ = ['FCGIApp'] + +# Constants from the spec. +FCGI_LISTENSOCK_FILENO = 0 + +FCGI_HEADER_LEN = 8 + +FCGI_VERSION_1 = 1 + +FCGI_BEGIN_REQUEST = 1 +FCGI_ABORT_REQUEST = 2 +FCGI_END_REQUEST = 3 +FCGI_PARAMS = 4 +FCGI_STDIN = 5 +FCGI_STDOUT = 6 +FCGI_STDERR = 7 +FCGI_DATA = 8 +FCGI_GET_VALUES = 9 +FCGI_GET_VALUES_RESULT = 10 +FCGI_UNKNOWN_TYPE = 11 +FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE + +FCGI_NULL_REQUEST_ID = 0 + +FCGI_KEEP_CONN = 1 + +FCGI_RESPONDER = 1 +FCGI_AUTHORIZER = 2 +FCGI_FILTER = 3 + +FCGI_REQUEST_COMPLETE = 0 +FCGI_CANT_MPX_CONN = 1 +FCGI_OVERLOADED = 2 +FCGI_UNKNOWN_ROLE = 3 + +FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' +FCGI_MAX_REQS = 'FCGI_MAX_REQS' +FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' + +FCGI_Header = '!BBHHBx' +FCGI_BeginRequestBody = '!HB5x' +FCGI_EndRequestBody = '!LB3x' +FCGI_UnknownTypeBody = '!B7x' + +FCGI_BeginRequestBody_LEN = struct.calcsize(FCGI_BeginRequestBody) +FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) +FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) + +if __debug__: + import time + + # Set non-zero to write debug output to a file. + DEBUG = 0 + DEBUGLOG = '/tmp/fcgi_app.log' + + def _debug(level, msg): + if DEBUG < level: + return + + try: + f = open(DEBUGLOG, 'a') + f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg)) + f.close() + except: + pass + +def decode_pair(s, pos=0): + """ + Decodes a name/value pair. + + The number of bytes decoded as well as the name/value pair + are returned. + """ + nameLength = ord(s[pos]) + if nameLength & 128: + nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff + pos += 4 + else: + pos += 1 + + valueLength = ord(s[pos]) + if valueLength & 128: + valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff + pos += 4 + else: + pos += 1 + + name = s[pos:pos+nameLength] + pos += nameLength + value = s[pos:pos+valueLength] + pos += valueLength + + return (pos, (name, value)) + +def encode_pair(name, value): + """ + Encodes a name/value pair. + + The encoded string is returned. + """ + nameLength = len(name) + if nameLength < 128: + s = chr(nameLength) + else: + s = struct.pack('!L', nameLength | 0x80000000L) + + valueLength = len(value) + if valueLength < 128: + s += chr(valueLength) + else: + s += struct.pack('!L', valueLength | 0x80000000L) + + return s + name + value + +class Record(object): + """ + A FastCGI Record. + + Used for encoding/decoding records. + """ + def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): + self.version = FCGI_VERSION_1 + self.type = type + self.requestId = requestId + self.contentLength = 0 + self.paddingLength = 0 + self.contentData = '' + + def _recvall(sock, length): + """ + Attempts to receive length bytes from a socket, blocking if necessary. + (Socket may be blocking or non-blocking.) + """ + dataList = [] + recvLen = 0 + while length: + try: + data = sock.recv(length) + except socket.error, e: + if e[0] == errno.EAGAIN: + select.select([sock], [], []) + continue + else: + raise + if not data: # EOF + break + dataList.append(data) + dataLen = len(data) + recvLen += dataLen + length -= dataLen + return ''.join(dataList), recvLen + _recvall = staticmethod(_recvall) + + def read(self, sock): + """Read and decode a Record from a socket.""" + try: + header, length = self._recvall(sock, FCGI_HEADER_LEN) + except: + raise EOFError + + if length < FCGI_HEADER_LEN: + raise EOFError + + self.version, self.type, self.requestId, self.contentLength, \ + self.paddingLength = struct.unpack(FCGI_Header, header) + + if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, ' + 'contentLength = %d' % + (sock.fileno(), self.type, self.requestId, + self.contentLength)) + + if self.contentLength: + try: + self.contentData, length = self._recvall(sock, + self.contentLength) + except: + raise EOFError + + if length < self.contentLength: + raise EOFError + + if self.paddingLength: + try: + self._recvall(sock, self.paddingLength) + except: + raise EOFError + + def _sendall(sock, data): + """ + Writes data to a socket and does not return until all the data is sent. + """ + length = len(data) + while length: + try: + sent = sock.send(data) + except socket.error, e: + if e[0] == errno.EAGAIN: + select.select([], [sock], []) + continue + else: + raise + data = data[sent:] + length -= sent + _sendall = staticmethod(_sendall) + + def write(self, sock): + """Encode and write a Record to a socket.""" + self.paddingLength = -self.contentLength & 7 + + if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, ' + 'contentLength = %d' % + (sock.fileno(), self.type, self.requestId, + self.contentLength)) + + header = struct.pack(FCGI_Header, self.version, self.type, + self.requestId, self.contentLength, + self.paddingLength) + self._sendall(sock, header) + if self.contentLength: + self._sendall(sock, self.contentData) + if self.paddingLength: + self._sendall(sock, '\x00'*self.paddingLength) + +class FCGIApp(object): + def __init__(self, command=None, connect=None, host=None, port=None, + filterEnviron=True): + if host is not None: + assert port is not None + connect=(host, port) + + assert (command is not None and connect is None) or \ + (command is None and connect is not None) + + self._command = command + self._connect = connect + + self._filterEnviron = filterEnviron + + #sock = self._getConnection() + #print self._fcgiGetValues(sock, ['FCGI_MAX_CONNS', 'FCGI_MAX_REQS', 'FCGI_MPXS_CONNS']) + #sock.close() + + def __call__(self, environ, start_response): + # For sanity's sake, we don't care about FCGI_MPXS_CONN + # (connection multiplexing). For every request, we obtain a new + # transport socket, perform the request, then discard the socket. + # This is, I believe, how mod_fastcgi does things... + + sock = self._getConnection() + + # Since this is going to be the only request on this connection, + # set the request ID to 1. + requestId = 1 + + # Begin the request + rec = Record(FCGI_BEGIN_REQUEST, requestId) + rec.contentData = struct.pack(FCGI_BeginRequestBody, FCGI_RESPONDER, 0) + rec.contentLength = FCGI_BeginRequestBody_LEN + rec.write(sock) + + # Filter WSGI environ and send it as FCGI_PARAMS + if self._filterEnviron: + params = self._defaultFilterEnviron(environ) + else: + params = self._lightFilterEnviron(environ) + # TODO: Anything not from environ that needs to be sent also? + self._fcgiParams(sock, requestId, params) + self._fcgiParams(sock, requestId, {}) + + # Transfer wsgi.input to FCGI_STDIN + content_length = int(environ.get('CONTENT_LENGTH') or 0) + while True: + chunk_size = min(content_length, 4096) + s = environ['wsgi.input'].read(chunk_size) + content_length -= len(s) + rec = Record(FCGI_STDIN, requestId) + rec.contentData = s + rec.contentLength = len(s) + rec.write(sock) + + if not s: break + + # Empty FCGI_DATA stream + rec = Record(FCGI_DATA, requestId) + rec.write(sock) + + # Main loop. Process FCGI_STDOUT, FCGI_STDERR, FCGI_END_REQUEST + # records from the application. + result = [] + while True: + inrec = Record() + inrec.read(sock) + if inrec.type == FCGI_STDOUT: + if inrec.contentData: + result.append(inrec.contentData) + else: + # TODO: Should proba