# -*- coding: iso-8859-1 -*- """ MoinMoin - Data associated with a single Request @copyright: 2001-2003 by Jürgen Hermann @copyright: 2003-2004 by Thomas Waldmann @license: GNU GPL, see COPYING for details. """ import os, time, sys, types, cgi from MoinMoin import config, wikiutil, user, error from MoinMoin.util import MoinMoinNoFooter, IsWin9x, FixScriptName # this needs sitecustomize.py in python path to work! # use our encoding as default encoding: ## sys.setappdefaultencoding(config.charset) # this is the default python uses without this hack: ## sys.setappdefaultencoding("ascii") # we could maybe use this to find places where implicit encodings are used: ## sys.setappdefaultencoding("undefined") # Timing --------------------------------------------------------------- class Clock: """ Helper class for code profiling we do not use time.clock() as this does not work across threads """ def __init__(self): self.timings = {'total': time.time()} def start(self, timer): self.timings[timer] = time.time() - self.timings.get(timer, 0) def stop(self, timer): self.timings[timer] = time.time() - self.timings[timer] def value(self, timer): return "%.3f" % (self.timings[timer],) def dump(self): outlist = [] for timing in self.timings.items(): outlist.append("%s = %.3fs" % timing) outlist.sort() return outlist # Request Base ---------------------------------------------------------- class RequestBase: """ A collection for all data associated with ONE request. """ # Header set to force misbehaved proxies and browsers to keep their # hands off a page # Details: http://support.microsoft.com/support/kb/articles/Q234/0/67.ASP nocache = [ "Pragma: no-cache", "Cache-Control: no-cache", "Expires: -1", ] def __init__(self, properties={}): self.failed = 0 self._available_actions = None self._known_actions = None # Pages meta data that we collect in one request self.pages = {} self.sent_headers = 0 self.user_headers = [] self.page = None # Fix dircaching problems on Windows 9x if IsWin9x(): import dircache dircache.reset() # Check for dumb proxy requests # TODO relying on request_uri will not work on all servers, especially # not on external non-Apache servers self.forbidden = False if self.request_uri.startswith('http://'): self.makeForbidden() # Init else: self.writestack = [] self.clock = Clock() # order is important here! self._load_multi_cfg() # Set decode charsets. Input from the user is always in # config.charset, which is the page charsets. Except # path_info, which may use utf-8, and handled by decodePagename. self.decode_charsets = [config.charset] # hierarchical wiki - set rootpage from MoinMoin.Page import Page #path = self.getPathinfo() #if path.startswith('/'): # pages = path[1:].split('/') # if 0: # len(path) > 1: # ## breaks MainPage/SubPage on flat storage # rootname = u'/'.join(pages[:-1]) # else: # # this is the usual case, as it ever was... # rootname = u"" #else: # # no extra path after script name # rootname = u"" rootname = u'' self.rootpage = Page(self, rootname, is_rootpage=1) self.user = user.User(self) ## self.dicts = self.initdicts() from MoinMoin import i18n # Set theme - forced theme, user theme or wiki default if self.cfg.theme_force: theme_name = self.cfg.theme_default #### DEBUT HACK : Utilisation d'un thème différent pour www.crans.org if self.remote_addr == '138.231.136.3': theme_name = self.cfg.theme_www #### FIN DU HACK else: theme_name = self.user.theme_name self.loadTheme(theme_name) self.args = None self.form = None self.logger = None self.pragma = {} self.mode_getpagelinks = 0 self.no_closing_html_code = 0 self.__dict__.update(properties) self.i18n = i18n self.lang = i18n.requestLanguage(self) # Language for content. Page content should use the wiki # default lang, but generated content like search results # should use the user language. self.content_lang = self.cfg.default_lang self.getText = lambda text, i18n=self.i18n, request=self, lang=self.lang, **kv: i18n.getText(text, request, lang, kv.get('formatted', True)) self.opened_logs = 0 self.reset() def __getattr__(self, name): "load things on demand" if name == "dicts": self.dicts = self.initdicts() return self.dicts raise AttributeError def _load_multi_cfg(self): # protect against calling multiple times if not hasattr(self, 'cfg'): from MoinMoin import multiconfig self.cfg = multiconfig.getConfig(self.url) def parse_accept_charset(self, accept_charset): """ Parse http accept charset header Set self.accepted_charsets to an ordered list based on http_accept_charset. Reference: http://www.w3.org/Protocols/rfc2616/rfc2616.txt @param accept_charset: HTTP_ACCEPT_CHARSET string @rtype: list of strings @return: sorted list of accepted charsets """ charsets = [] if accept_charset: accept_charset = accept_charset.lower() # Add iso-8859-1 if needed if (not '*' in accept_charset and accept_charset.find('iso-8859-1') < 0): accept_charset += ',iso-8859-1' # Make a list, sorted by quality value, using Schwartzian Transform # Create list of tuples (value, name) , sort, extract names for item in accept_charset.split(','): if ';' in item: name, qval = item.split(';') qval = 1.0 - float(qval.split('=')[1]) else: name, qval = item, 0 charsets.append((qval, name)) charsets.sort() # Remove *, its not clear what we should do with it later charsets = [name for qval, name in charsets if name != '*'] return charsets def _setup_vars_from_std_env(self, env): """ Sets the common Request members by parsing a standard HTTPD environment (as created as environment by most common webservers. To be used by derived classes. @param env: the environment to use """ self.http_accept_language = env.get('HTTP_ACCEPT_LANGUAGE', 'en') self.server_name = env.get('SERVER_NAME', 'localhost') self.server_port = env.get('SERVER_PORT', '80') self.http_host = env.get('HTTP_HOST','localhost') # Make sure http referer use only ascii (IE again) self.http_referer = unicode(env.get('HTTP_REFERER', ''), 'ascii', 'replace').encode('ascii', 'replace') self.saved_cookie = env.get('HTTP_COOKIE', '') self.script_name = env.get('SCRIPT_NAME', '') self.request_uri = env.get('REQUEST_URI', '') path_info = env.get('PATH_INFO', '') query_string = env.get('QUERY_STRING', '') self.query_string = self.decodePagename(query_string) server_software = env.get('SERVER_SOFTWARE', '') # Handle the strange charset semantics on *Windows* # path_info is transformed into the system code page by the webserver # Additionally, paths containing dots let most webservers choke. # Fig. I - Broken environment variables in different environments: # path_info script_name # Apache1 X X PI does not contain dots # Apache2 X X PI is not encoded correctly # IIS X X (is fixed somewhere else) # Other ? - ? := Possible and even RFC-compatible. # - := Hopefully not. # Fix the script_name if run on Windows/Apache if os.name == 'nt' and server_software.find('Apache/') != -1: self.script_name = FixScriptName(self.script_name) # Check if we can use Apache's request_uri variable in order to # gather the correct user input if (os.name != 'posix' and self.request_uri != ''): import urllib path_info = urllib.unquote(self.request_uri.replace( self.script_name, '', 1).replace('?' + query_string, '', 1)) # Decode according to filesystem semantics if we cannot gather the # request_uri elif os.name == 'nt': path_info = wikiutil.decodeWindowsPath(path_info).encode("utf-8") self.path_info = self.decodePagename(path_info) self.request_method = env.get('REQUEST_METHOD', None) self.remote_addr = env.get('REMOTE_ADDR', '') self.http_user_agent = env.get('HTTP_USER_AGENT', '') self.is_ssl = env.get('SSL_PROTOCOL', '') != '' \ or env.get('SSL_PROTOCOL_VERSION', '') != '' \ or env.get('HTTPS', 'off') == 'on' # We cannot rely on request_uri being set. In fact, # it is just an addition of Apache to the CGI specs. if self.request_uri == '': import urllib if self.server_port.strip() and self.server_port != '80': port = ':' + str(self.server_port) else: port = '' self.url = (self.server_name + port + self.script_name + urllib.quote(self.path_info.replace( self.script_name, '', 1).encode("utf-8")) + '?' + self.query_string) else: self.url = self.server_name + self.request_uri ac = env.get('HTTP_ACCEPT_CHARSET', '') self.accepted_charsets = self.parse_accept_charset(ac) self.auth_username = None # need config here, so check: self._load_multi_cfg() if self.cfg.auth_http_enabled: auth_type = env.get('AUTH_TYPE','') if auth_type in ['Basic', 'Digest', 'NTLM', ]: username = env.get('REMOTE_USER','') if auth_type == 'NTLM': # converting to standard case so that the user can even enter wrong case # (added since windows does not distinguish between e.g. "Mike" and "mike") username = username.split('\\')[-1] # split off domain e.g. from DOMAIN\user # this "normalizes" the login name from {meier, Meier, MEIER} to Meier # put a comment sign in front of next line if you don't want that: username = username.title() self.auth_username = username ## f=open('/tmp/env.log','a') ## f.write('---ENV\n') ## f.write('script_name = %s\n'%(self.script_name)) ## f.write('path_info = %s\n'%(self.path_info)) ## f.write('server_name = %s\n'%(self.server_name)) ## f.write('server_port = %s\n'%(self.server_port)) ## f.write('http_host = %s\n'%(self.http_host)) ## f.write('------\n') ## f.write('%s\n'%(repr(env))) ## f.write('------\n') ## f.close() def 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.default_lang self._footer_fragments = {} self._all_pages = None # caches unique ids self._page_ids = {} # keeps track of pagename/heading combinations # parsers should use this dict and not a local one, so that # macros like TableOfContents in combination with Include # can work self._page_headings = {} if hasattr(self, "_fmt_hd_counters"): del self._fmt_hd_counters def loadTheme(self, theme_name): """ Load the Theme to use for this request. @param theme_name: the name of the theme @type theme_name: str @returns: 0 on success, 1 if user theme could not be loaded, 2 if a hard fallback to modern theme was required. @rtype: int @return: success code """ fallback = 0 Theme = wikiutil.importPlugin(self.cfg, 'theme', theme_name, 'Theme') if Theme is None: fallback = 1 Theme = wikiutil.importPlugin(self.cfg, 'theme', self.cfg.theme_default, 'Theme') if Theme is None: 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 add2footer(self, key, htmlcode): """ Add a named HTML fragment to the footer, after the default links """ self._footer_fragments[key] = htmlcode def getPragma(self, key, defval=None): """ Query a pragma value (#pragma processing instruction) 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. TODO: when we have a wiki object in long running process, we should get it from it. @rtype: dict @return: dict of all known actions """ if self._known_actions is None: from MoinMoin import wikiaction # Add built in actions from wikiaction actions = [name[3:] for name in wikiaction.__dict__ if name.startswith('do_')] # Add plugins dummy, plugins = wikiaction.getPlugins(self) actions.extend(plugins) # Add extensions from MoinMoin.action import extension_actions actions.extend(extension_actions) # TODO: Use set when we require Python 2.3 actions = dict(zip(actions, [''] * len(actions))) self._known_actions = actions # Return a copy, so clients will not change the dict. return self._known_actions.copy() def getAvailableActions(self, page): """ Get list of avaiable actions for this request The dict does not contain actions that starts with lower case. Themes use this dict to display the actions to the user. @param page: current page, Page object @rtype: dict @return: dict of avaiable actions """ if self._available_actions is None: # Add actions for existing pages only!, incliding deleted pages. # Fix *OnNonExistingPage bugs. if not (page.exists(includeDeleted=1) and self.user.may.read(page.page_name)): return [] # Filter non ui actions (starts with lower case letter) actions = self.getKnownActions() for key in actions.keys(): if key[0].islower(): del actions[key] # Filter wiki excluded actions for key in self.cfg.excluded_actions: if key in actions: del actions[key] # Filter actions by page type, acl and user state excluded = [] if ((page.isUnderlayPage() and not page.isStandardPage()) or not self.user.may.write(page.page_name)): # Prevent modification of underlay only pages, or pages # the user can't write to excluded = [u'RenamePage', u'DeletePage',] # AttachFile must NOT be here! elif not self.user.valid: # Prevent rename and delete for non registered users excluded = [u'RenamePage', u'DeletePage'] for key in excluded: if key in actions: del actions[key] self._available_actions = actions # Return a copy, so clients will not change the dict. return self._available_actions.copy() def redirect(self, file=None): if file: # redirect output to "file" self.writestack.append(self.write) self.write = file.write else: # restore saved output file self.write = self.writestack.pop() def reset_output(self): """ restore default output method destroy output stack (useful for error messages) """ if self.writestack: self.write = self.writestack[0] self.writestack = [] def log(self, msg): """ Log to stderr, which may be error.log """ msg = msg.strip() # Encode unicode msg if isinstance(msg, unicode): msg = msg.encode(config.charset) # Add time stamp msg = '[%s] %s\n' % (time.asctime(), msg) sys.stderr.write(msg) def write(self, *data): """ Write to output stream. """ raise NotImplementedError def encode(self, data): """ encode data (can be both unicode strings and strings), preparing for a single write() """ wd = [] for d in data: try: if isinstance(d, type(u'')): # if we are REALLY sure, we can use "strict" d = d.encode(config.charset, 'replace') wd.append(d) except UnicodeError: print >>sys.stderr, "Unicode error on: %s" % repr(d) return ''.join(wd) def decodePagename(self, name): """ Decode path, possibly using non ascii characters Does not change the name, only decode to Unicode. First split the path to pages, then decode each one. This enables us to decode one page using config.charset and another using utf-8. This situation happens when you try to add to a name of an existing page. See http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1 @param name: page name, string @rtype: unicode @return decoded page name """ # Split to pages and decode each one pages = name.split('/') decoded = [] for page in pages: # Recode from utf-8 into config charset. If the path # contains user typed parts, they are encoded using 'utf-8'. if config.charset != 'utf-8': try: page = unicode(page, 'utf-8', 'strict') # Fit data into config.charset, replacing what won't # fit. Better have few "?" in the name then crash. page = page.encode(config.charset, 'replace') except UnicodeError: pass # Decode from config.charset, replacing what can't be decoded. page = unicode(page, config.charset, 'replace') decoded.append(page) # Assemble decoded parts name = u'/'.join(decoded) return name def normalizePagename(self, name): """ Normalize page name Convert '_' to spaces - allows using nice URLs with spaces, with no need to quote. Prevent creating page names with invisible characters or funny whitespace that might confuse the users or abuse the wiki, or just does not make sense. Restrict even more group pages, so they can be used inside acl lines. @param name: page name, unicode @rtype: unicode @return: decoded and sanitized page name """ # Replace underscores with spaces name = name.replace(u'_', u' ') # Strip invalid characters name = config.page_invalid_chars_regex.sub(u'', name) # Split to pages and normalize each one pages = name.split(u'/') normalized = [] for page in pages: # Ignore empty or whitespace only pages if not page or page.isspace(): continue # Cleanup group pages. # Strip non alpha numeric characters, keep white space if wikiutil.isGroupPage(self, page): page = u''.join([c for c in page if c.isalnum() or c.isspace()]) # Normalize white space. Each name can contain multiple # words separated with only one space. Split handle all # 30 unicode spaces (isspace() == True) page = u' '.join(page.split()) normalized.append(page) # Assemble components into full pagename name = u'/'.join(normalized) return name def read(self, n): """ Read n bytes from input stream. """ raise NotImplementedError def flush(self): """ Flush output stream. """ raise NotImplementedError def initdicts(self): from MoinMoin import wikidicts dicts = wikidicts.GroupDict(self) dicts.scandicts() return dicts def isForbidden(self): """ check for web spiders and refuse anything except viewing """ forbidden = 0 # we do not have a parsed query string here # so we can just do simple matching if ((self.query_string != '' or self.request_method != 'GET') and self.query_string != 'action=rss_rc' and not # allow spiders to get attachments and do 'show' (self.query_string.find('action=AttachFile') >= 0 and self.query_string.find('do=get') >= 0) and not (self.query_string.find('action=show') >= 0) ): from MoinMoin.util import web forbidden = web.isSpiderAgent(self) if not forbidden and self.cfg.hosts_deny: ip = self.remote_addr for host in self.cfg.hosts_deny: if ip == host or host[-1] == '.' and ip.startswith(host): forbidden = 1 break return forbidden def setup_args(self, form=None): return {} def _setup_args_from_cgi_form(self, form=None): """ Setup args from a FieldStorage form Create the args from a standard cgi.FieldStorage to be used by derived classes, or from given form. All values are decoded using config.charset. @keyword form: a cgi.FieldStorage @rtype: dict @return dict with form keys, each contains a list of values """ decode = wikiutil.decodeUserInput # Use cgi.FieldStorage by default if form is None: form = cgi.FieldStorage() args = {} # Convert form keys to dict keys, each key contains a list of # values. for key in form.keys(): values = form[key] if not isinstance(values, types.ListType): values = [values] fixedResult = [] for i in values: if isinstance(i, cgi.MiniFieldStorage): data = decode(i.value, self.decode_charsets) elif isinstance(i, cgi.FieldStorage): data = i.value if i.filename: # multiple uploads to same form field are stupid! args[key+'__filename__'] = decode(i.filename, self.decode_charsets) else: # we do not have a file upload, so we decode: data = decode(data, self.decode_charsets) # Append decoded value fixedResult.append(data) args[key] = fixedResult return args def getBaseURL(self): """ Return a fully qualified URL to this script. """ return self.getQualifiedURL(self.getScriptname()) def getQualifiedURL(self, uri=None): """ Return a full URL starting with schema, server name and port. *uri* -- append this server-rooted uri (must start with a slash) """ if uri and uri[:4] == "http": return uri schema, stdport = (('http', '80'), ('https', '443'))[self.is_ssl] host = self.http_host if not host: host = self.server_name port = self.server_port if port != stdport: host = "%s:%s" % (host, port) result = "%s://%s" % (schema, host) if uri: result = result + uri return result def getUserAgent(self): """ Get the user agent. """ return self.http_user_agent def makeForbidden(self): self.forbidden = True self.http_headers([ 'Status: 403 FORBIDDEN', 'Content-Type: text/plain' ]) self.write('You are not allowed to access this!\r\n') self.setResponseCode(403) return self.finish() def run(self): # __init__ may have failed if self.failed or self.forbidden: return if self.isForbidden(): self.makeForbidden() if self.forbidden: return self.open_logs() _ = self.getText self.clock.start('run') # Imports from MoinMoin.Page import Page if self.query_string == 'action=xmlrpc': from MoinMoin.wikirpc import xmlrpc xmlrpc(self) return self.finish() if self.query_string == 'action=xmlrpc2': from MoinMoin.wikirpc import xmlrpc2 xmlrpc2(self) return self.finish() # parse request data try: self.args = self.setup_args() self.form = self.args action = self.form.get('action',[None])[0] # Get pagename # The last component in path_info is the page name, if any path = self.getPathinfo() if path.startswith('/'): pagename = self.normalizePagename(path) else: pagename = None except: # catch and print any exception self.reset_output() self.http_headers() self.print_exception() return self.finish() try: # Handle request. We have these options: # 1. If user has a bad user name, delete its bad cookie and # send him to UserPreferences to make a new account. if not user.isValidName(self, self.user.name): msg = _("""Invalid user name {{{'%s'}}}. Name may contain any Unicode alpha numeric character, with optional one space between words. Group page name is not allowed.""") % self.user.name self.deleteCookie() page = wikiutil.getSysPage(self, 'UserPreferences') page.send_page(self, msg=msg) # 2. Or jump to page where user left off elif not pagename and not action and self.user.remember_last_visit: pagetrail = self.user.getTrail() if pagetrail: # Redirect to last page visited url = Page(self, pagetrail[-1]).url(self) else: # Or to localized FrontPage url = wikiutil.getFrontPage(self).url(self) self.http_redirect(url) return self.finish() # 3. Or save drawing elif (self.form.has_key('filepath') and self.form.has_key('noredirect')): # looks like user wants to save a drawing from MoinMoin.action.AttachFile import execute # TODO: what if pagename is None? execute(pagename, self) raise MoinMoinNoFooter # 4. Or handle action elif action: # Use localized FrontPage if pagename is empty if not pagename: self.page = wikiutil.getFrontPage(self) else: self.page = Page(self, pagename) # Complain about unknown actions if not action in self.getKnownActions(): self.http_headers() self.write(u'

Unknown action %s

' % wikiutil.escape(action)) # Disallow non available actions elif (action[0].isupper() and not action in self.getAvailableActions(self.page)): # Send page with error msg = _("You are not allowed to do %s on this page.") % wikiutil.escape(action) if not self.user.valid: # Suggest non valid user to login login = wikiutil.getSysPage(self, 'UserPreferences') login = login.link_to(self, _('Login')) msg += _(" %s and try again.", formatted=0) % login self.page.send_page(self, msg=msg) # Try action else: from MoinMoin.wikiaction import getHandler handler = getHandler(self, action) handler(self.page.page_name, self) # 5. Or redirect to another page elif self.form.has_key('goto'): self.http_redirect(Page(self, self.form['goto'][0]).url(self)) return self.finish() # 6. Or (at last) visit pagename else: if not pagename: # Get pagename from the query string pagename = self.normalizePagename(self.query_string) # pagename could be empty after normalization e.g. '///' -> '' if not pagename: pagename = wikiutil.getFrontPage(self).page_name # Visit pagename if self.cfg.allow_extended_names: self.page = Page(self, pagename) self.page.send_page(self, count_hit=1) else: # TODO: kill this. Why disable allow extended names? from MoinMoin.parser.wiki import Parser import re word_match = re.match(Parser.word_rule, pagename) if word_match: word = word_match.group(0) self.page = Page(self, word) self.page.send_page(self, count_hit=1) else: self.http_headers() err = u'

%s "

%s
"' % ( _("Can't work out query"), pagename) self.write(err) # generate page footer (actions that do not want this footer # use raise util.MoinMoinNoFooter to break out of the # default execution path, see the "except MoinMoinNoFooter" # below) self.clock.stop('run') self.clock.stop('total') # Close html code if not self.no_closing_html_code: if (self.cfg.show_timings and self.form.get('action', [None])[0] != 'print'): self.write('\n') self.write('\n\n\n') except MoinMoinNoFooter: pass except error.FatalError, err: self.fail(err) except: # catch and print any exception saved_exc = sys.exc_info() self.reset_output() self.http_headers() self.write(u"\n\n") try: from MoinMoin.support import cgitb except: # no cgitb, for whatever reason self.print_exception(*saved_exc) else: try: cgitb.Hook(file=self).handle(saved_exc) # was: cgitb.handler() except: self.print_exception(*saved_exc) self.write("\n\n
\n") self.write("

Additionally, cgitb raised this exception:

\n") self.print_exception() del saved_exc return self.finish() def http_redirect(self, url): """ Redirect to a fully qualified, or server-rooted URL """ if url.find("://") == -1: url = self.getQualifiedURL(url) #### DEBUT HACK : pour le www, on redirige vers du www if self.remote_addr == '138.231.136.3': url = url.replace('http://wiki','http://www') #### FIN DU HACK self.http_headers(["Status: 302", "Location: %s" % url]) def setResponseCode(self, code, message=None): pass def fail(self, err): """ Fail with nice error message when we can't continue Log the error, then try to print nice error message. @param err: MoinMoin.error.FatalError instance or subclass. """ self.failed = 1 # save state for self.run() self.log(err.asLog()) self.http_headers() self.write(err.asHTML()) return self.finish() def print_exception(self, type=None, value=None, tb=None, limit=None): if type is None: type, value, tb = sys.exc_info() import traceback self.write("

request.print_exception handler

\n") self.write("

Traceback (most recent call last):

\n") list = traceback.format_tb(tb, limit) + \ traceback.format_exception_only(type, value) self.write("
%s%s
\n" % ( wikiutil.escape("".join(list[:-1])), wikiutil.escape(list[-1]),)) del tb def open_logs(self): pass def makeUniqueID(self, base): """ Generates a unique ID using a given base name. Appends a running count to the base. @param base: the base of the id @type base: unicode @returns: an unique id @rtype: unicode """ if not isinstance(base, types.UnicodeType): base=unicode(str(base),'ascii','ignore') count = self._page_ids.get(base,-1) + 1 self._page_ids[base] = count if count==0: return base return u'%s_%04d' % (base, count) def httpDate(self, when=None, rfc='1123'): """ Returns http date string, according to rfc2068 See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-3.3 Http 1.1 server should use only rfc1123 date, but cookies expires field should use older rfc850 date. @param when: seconds from epoch, as returned by time.time() @param rfc: either '1123' or '850' @rtype: string @return: http date """ if when is None: when = time.time() if rfc not in ['1123', '850']: raise ValueError("%s: Invalid rfc value" % rfc) import locale t = time.gmtime(when) # TODO: make this a critical section for persistent environments # Should acquire lock here loc=locale.setlocale(locale.LC_TIME, 'C') if rfc == '1123': date = time.strftime("%A, %d %b %Y %H:%M:%S GMT", t) else: date = time.strftime("%A, %d-%b-%Y %H:%M:%S GMT", t) locale.setlocale(locale.LC_TIME, loc) # Should release lock here return date def disableHttpCaching(self): """ Prevent caching of pages that should not be cached This is important to prevent caches break acl by providing one user pages meant to be seen only by another user, when both users share the same caching proxy. """ # Run only once if hasattr(self, 'http_caching_disabled'): return self.http_caching_disabled = 1 # Set Cache control header for http 1.1 caches # See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.3 # and http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.9 self.setHttpHeader('Cache-Control: no-cache="set-cookie"') self.setHttpHeader('Cache-Control: private') self.setHttpHeader('Cache-Control: max-age=0') # Set Expires for http 1.0 caches (does not support Cache-Control) yearago = time.time() - (3600 * 24 * 365) self.setHttpHeader('Expires: %s' % self.httpDate(when=yearago)) # Set Pragma for http 1.0 caches # See http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2068.html#sec-14.32 self.setHttpHeader('Pragma: no-cache') def setCookie(self): """ Set cookie for the current user cfg.cookie_lifetime and the user 'remember_me' setting set the lifetime of the cookie. lifetime in int hours, see table: value cookie lifetime ---------------------------------------------------------------- = 0 forever, ignoring user 'remember_me' setting > 0 n hours, or forever if user checked 'remember_me' < 0 -n hours, ignoring user 'remember_me' setting TODO: do we really need this cookie_lifetime setting? """ # Calculate cookie maxage and expires lifetime = int(self.cfg.cookie_lifetime) * 3600 forever = 10*365*24*3600 # 10 years now = time.time() if not lifetime: maxage = forever elif lifetime > 0: if self.user.remember_me: maxage = forever else: maxage = lifetime elif lifetime < 0: maxage = (-lifetime) expires = now + maxage # Set the cookie from Cookie import SimpleCookie c = SimpleCookie() c['MOIN_ID'] = self.user.id c['MOIN_ID']['max-age'] = maxage c['MOIN_ID']['path'] = self.getScriptname() # Set expires for older clients c['MOIN_ID']['expires'] = self.httpDate(when=expires, rfc='850') self.setHttpHeader(c.output()) # Update the saved cookie, so other code works with new setup self.saved_cookie = c.output() # IMPORTANT: Prevent caching of current page and cookie self.disableHttpCaching() def deleteCookie(self): """ Delete the user cookie by sending expired cookie with null value According to http://www.cse.ohio-state.edu/cgi-bin/rfc/rfc2109.html#sec-4.2.2 Deleted cookie should have Max-Age=0. We also have expires attribute, which is probably needed for older browsers. Finally, delete the saved cookie and create a new user based on the new settings. """ # Set cookie from Cookie import SimpleCookie c = SimpleCookie() c['MOIN_ID'] = '' c['MOIN_ID']['path'] = self.getScriptname() c['MOIN_ID']['max-age'] = 0 # Set expires to one year ago for older clients yearago = time.time() - (3600 * 24 * 365) c['MOIN_ID']['expires'] = self.httpDate(when=yearago, rfc='850') self.setHttpHeader(c.output()) # Update saved cookie and set new unregistered user self.saved_cookie = '' self.auth_username = '' self.user = user.User(self) # IMPORTANT: Prevent caching of current page and cookie self.disableHttpCaching() def finish(self): """ General cleanup on end of request Delete circular references - all object that we create using self.name = class(self) This helps Python to collect these objects and keep our memory footprint lower """ try: del self.user del self.theme del self.dicts except: pass # CGI --------------------------------------------------------------- class RequestCGI(RequestBase): """ specialized on CGI requests """ def __init__(self, properties={}): try: self._setup_vars_from_std_env(os.environ) #sys.stderr.write("----\n") #for key in os.environ.keys(): # sys.stderr.write(" %s = '%s'\n" % (key, os.environ[key])) RequestBase.__init__(self, properties) # 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) except error.FatalError, err: self.fail(err) def open_logs(self): # create log file for catching stderr output if not self.opened_logs: sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at') self.opened_logs = 1 def setup_args(self, form=None): return self._setup_args_from_cgi_form(form) def read(self, n=None): """ Read from input stream. """ if n is None: return sys.stdin.read() else: return sys.stdin.read(n) def write(self, *data): """ Write to output stream. """ sys.stdout.write(self.encode(data)) def flush(self): sys.stdout.flush() def finish(self): RequestBase.finish(self) # flush the output, ignore errors caused by the user closing the socket try: sys.stdout.flush() except IOError, ex: import errno if ex.errno != errno.EPIPE: raise # Accessors -------------------------------------------------------- def getPathinfo(self): """ Return the remaining part of the URL. """ pathinfo = self.path_info # Fix for bug in IIS/4.0 if os.name == 'nt': scriptname = self.getScriptname() if pathinfo.startswith(scriptname): pathinfo = pathinfo[len(scriptname):] return pathinfo # Headers ---------------------------------------------------------- def setHttpHeader(self, header): self.user_headers.append(header) def http_headers(self, more_headers=[]): # Send only once if getattr(self, 'sent_headers', None): return self.sent_headers = 1 have_ct = 0 # send http headers for header in more_headers + getattr(self, 'user_headers', []): if header.lower().startswith("content-type:"): # don't send content-type multiple times! if have_ct: continue have_ct = 1 if type(header) is unicode: header = header.encode('ascii') self.write("%s\r\n" % header) if not have_ct: self.write("Content-type: text/html;charset=%s\r\n" % config.charset) self.write('\r\n') #from pprint import pformat #sys.stderr.write(pformat(more_headers)) #sys.stderr.write(pformat(self.user_headers)) # Twisted ----------------------------------------------------------- class RequestTwisted(RequestBase): """ specialized on Twisted requests """ def __init__(self, twistedRequest, pagename, reactor, properties={}): try: self.twistd = twistedRequest self.http_accept_language = self.twistd.getHeader('Accept-Language') self.reactor = reactor self.saved_cookie = self.twistd.getHeader('Cookie') 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() if self.server_port != ('80', '443')[self.is_ssl]: self.http_host = self.server_name + ':' + self.server_port else: self.http_host = self.server_name self.script_name = "/" + '/'.join(self.twistd.prepath[:-1]) path_info = '/' + '/'.join([pagename] + self.twistd.postpath) self.path_info = self.decodePagename(path_info) self.request_method = self.twistd.method self.remote_host = self.twistd.getClient() self.remote_addr = self.twistd.getClientIP() self.http_user_agent = self.twistd.getHeader('User-Agent') self.request_uri = self.twistd.uri self.url = self.http_host + self.request_uri # was: self.server_name + self.request_uri ac = self.twistd.getHeader('Accept-Charset') or '' self.accepted_charsets = self.parse_accept_charset(ac) qindex = self.request_uri.find('?') if qindex != -1: query_string = self.request_uri[qindex+1:] self.query_string = self.decodePagename(query_string) else: self.query_string = '' self.outputlist = [] self.auth_username = None # need config here, so check: self._load_multi_cfg() if self.cfg.auth_http_enabled and self.cfg.auth_http_insecure: self.auth_username = self.twistd.getUser() # TODO password check, twisted does NOT do that for us # this maybe requires bigger or critical changes, so we delay that # to 1.4's new auth stuff RequestBase.__init__(self, properties) #print "request.RequestTwisted.__init__: received_headers=\n" + str(self.twistd.received_headers) except error.FatalError, err: self.fail(err) def setup_args(self, form=None): args = {} for key,values in self.twistd.args.items(): if key[-12:] == "__filename__": args[key] = wikiutil.decodeUserInput(values, self.decode_charsets) continue if not isinstance(values, types.ListType): values = [values] fixedResult = [] for v in values: if not self.twistd.args.has_key(key+"__filename__"): v = wikiutil.decodeUserInput(v, self.decode_charsets) fixedResult.append(v) args[key] = fixedResult return args def read(self, n=None): """ Read from input stream. """ # XXX why is that wrong?: #rd = self.reactor.callFromThread(self.twistd.read) # XXX do we need self.reactor.callFromThread with that? # XXX if yes, why doesn't it work? self.twistd.content.seek(0, 0) if n is None: rd = self.twistd.content.read() else: rd = self.twistd.content.read(n) #print "request.RequestTwisted.read: data=\n" + str(rd) return rd def write(self, *data): """ Write to output stream. """ #print "request.RequestTwisted.write: data=\n" + wd self.reactor.callFromThread(self.twistd.write, self.encode(data)) def flush(self): pass # XXX is there a flush in twisted? def finish(self): RequestBase.finish(self) self.reactor.callFromThread(self.twistd.finish) def open_logs(self): return # create log file for catching stderr output if not self.opened_logs: sys.stderr = open(os.path.join(self.cfg.data_dir, 'error.log'), 'at') self.opened_logs = 1 # Headers ---------------------------------------------------------- def setHttpHeader(self, header): self.user_headers.append(header) def __setHttpHeader(self, header): if type(header) is unicode: header = header.encode('ascii') key, value = header.split(':',1) value = value.lstrip() if key.lower()=='set-cookie': key, value = value.split('=',1) self.twistd.addCookie(key, value) else: self.twistd.setHeader(key, value) #print "request.RequestTwisted.setHttpHeader: %s" % header def http_headers(self, more_headers=[]): if getattr(self, 'sent_headers', None): return self.sent_headers = 1 have_ct = 0 # set http headers for header in more_headers + self.user_headers: if header.lower().startswith("content-type:"): # don't send content-type multiple times! if have_ct: continue have_ct = 1 self.__setHttpHeader(header) if not have_ct: self.__setHttpHeader("Content-type: text/html;charset=%s" % config.charset) def http_redirect(self, url): """ Redirect to a fully qualified, or server-rooted URL """ if url.count("://") == 0: # no https method?? url = "http://%s:%s%s" % (self.server_name, self.server_port, url) if isinstance(url, type(u'')): url = url.encode('ascii') self.twistd.redirect(url) # calling finish here will send the rest of the data to the next # request. leave the finish call to run() #self.twistd.finish() raise MoinMoinNoFooter def setResponseCode(self, code, message=None): self.twistd.setResponseCode(code, message) # CLI ------------------------------------------------------------------ class RequestCLI(RequestBase): """ specialized on command line interface and script requests """ def __init__(self, url='CLI', pagename='', properties={}): self.http_accept_language = '' self.saved_cookie = '' self.path_info = self.decodePagename('/' + pagename) self.query_string = '' self.remote_addr = '127.0.0.127' self.is_ssl = 0 self.auth_username = None self.http_user_agent = 'CLI/Script' self.outputlist = [] self.url = url self.accepted_charsets = ['utf-8'] self.decode_charsets = self.accepted_charsets self.request_method = 'GET' self.request_uri = '/' + pagename # TODO check self.server_name = 'localhost' self.server_port = '80' self.http_host = 'localhost' self.http_referer = '' self.script_name = '' RequestBase.__init__(self, properties) self.cfg.caching_formats = [] # don't spoil the cache def read(self, n=None): """ Read from input stream. """ if n is None: return sys.stdin.read() else: return sys.stdin.read(n) def write(self, *data): """ Write to output stream. """ sys.stdout.write(self.encode(data)) def flush(self): sys.stdout.flush() def finish(self): RequestBase.finish(self) # flush the output, ignore errors caused by the user closing the socket try: sys.stdout.flush() except IOError, ex: import errno if ex.errno != errno.EPIPE: raise def isForbidden(self): """ check for web spiders and refuse anything except viewing """ return 0 # Accessors -------------------------------------------------------- def getScriptname(self): """ Return the scriptname part of the URL ("/path/to/my.cgi"). """ return '.' def getQualifiedURL(self, uri = None): """ Return a full URL starting with schema, server name and port. *uri* -- append this server-rooted uri (must start with a slash) """ return uri def getBaseURL(self): """ Return a fully qualified URL to this script. """ return self.getQualifiedURL(self.getScriptname()) # Headers ---------------------------------------------------------- def setHttpHeader(self, header): pass def http_headers(self, more_headers=[]): pass def http_redirect(self, url): """ Redirect to a fully qualified, or server-rooted URL """ raise Exception("Redirect not supported for command line tools!") # StandAlone Server ---------------------------------------------------- class RequestStandAlone(RequestBase): """ specialized on StandAlone Server (MoinMoin.server.standalone) requests """ 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 # Split path and query string and unquote path # query is unquoted by setup_args import urllib if '?' in sa.path: path, query = sa.path.split('?', 1) else: path, query = sa.path, '' path = urllib.unquote(path) #HTTP headers self.env = {} for hline in sa.headers.headers: key = sa.headers.isheader(hline) if key: hdr = sa.headers.getheader(key) self.env[key] = hdr #accept = [] #for line in sa.headers.getallmatchingheaders('accept'): # if line[:1] in string.whitespace: # accept.append(line.strip()) # else: # accept = accept + line[7:].split(',') # #env['HTTP_ACCEPT'] = ','.join(accept) co = filter(None, sa.headers.getheaders('cookie')) self.http_accept_language = sa.headers.getheader('Accept-Language') self.server_name = sa.server.server_name self.server_port = str(sa.server.server_port) self.http_host = sa.headers.getheader('host') # Make sure http referer use only ascii referer = sa.headers.getheader('referer') or '' self.http_referer = unicode(referer, 'ascii', 'replace').encode('ascii', 'replace') self.saved_cookie = ', '.join(co) or '' self.script_name = '' self.request_method = sa.command self.request_uri = sa.path self.remote_addr = sa.client_address[0] self.http_user_agent = sa.headers.getheader('user-agent') or '' # next must be http_host as server_name == reverse_lookup(IP) if self.http_host: self.url = self.http_host + self.request_uri else: self.url = "localhost" + self.request_uri ac = sa.headers.getheader('Accept-Charset') or '' self.accepted_charsets = self.parse_accept_charset(ac) # Decode path_info and query string, they may contain non-ascii self.path_info = self.decodePagename(path) self.query_string = self.decodePagename(query) # from standalone script: # XXX AUTH_TYPE # XXX REMOTE_USER # XXX REMOTE_IDENT self.auth_username = None #env['PATH_TRANSLATED'] = uqrest #self.translate_path(uqrest) #host = self.address_string() #if host != self.client_address[0]: # env['REMOTE_HOST'] = host # env['SERVER_PROTOCOL'] = self.protocol_version RequestBase.__init__(self, properties) except error.FatalError, err: self.fail(err) def setup_args(self, form=None): self.env['REQUEST_METHOD'] = self.request_method # This is a ugly hack to get the query into the environment, due # to the wired way standalone form is created. self.env['QUERY_STRING'] = self.query_string.encode(config.charset) ct = self.headers.getheader('content-type') if ct: self.env['CONTENT_TYPE'] = ct cl = self.headers.getheader('content-length') if cl: self.env['CONTENT_LENGTH'] = cl #print "env = ", self.env #form = cgi.FieldStorage(self, headers=self.env, environ=self.env) if form is None: form = cgi.FieldStorage(self.rfile, environ=self.env) return self._setup_args_from_cgi_form(form) def read(self, n=None): """ Read from input stream. """ if n is None: return self.rfile.read() else: return self.rfile.read(n) def write(self, *data): """ Write to output stream. """ self.wfile.write(self.encode(data)) def flush(self): self.wfile.flush() def finish(self): RequestBase.finish(self) self.wfile.flush() # Headers ---------------------------------------------------------- def setHttpHeader(self, header): self.user_headers.append(header) def http_headers(self, more_headers=[]): if getattr(self, 'sent_headers', None): return self.sent_headers = 1 # check for status header and send it our_status = 200 for header in more_headers + self.user_headers: if header.lower().startswith("status:"): try: our_status = int(header.split(':',1)[1].strip().split(" ", 1)[0]) except: pass # there should be only one! break # send response self.sareq.send_response(our_status) # send http headers have_ct = 0 for header in more_headers + self.user_headers: if type(header) is unicode: header = header.encode('ascii') if header.lower().startswith("content-type:"): # don't send content-type multiple times! if have_ct: continue have_ct = 1 self.write("%s\r\n" % header) if not have_ct: self.write("Content-type: text/html;charset=%s\r\n" % config.charset) self.write('\r\n') #from pprint import pformat #sys.stderr.write(pformat(more_headers)) #sys.stderr.write(pformat(self.user_headers)) # mod_python/Apache ---------------------------------------------------- class RequestModPy(RequestBase): """ specialized on mod_python requests """ def __init__(self, req): """ Saves mod_pythons request and sets basic variables using the req.subprocess_env, cause this provides a standard way to access the values we need here. @param req: the mod_python request instance """ try: # flags if headers sent out contained content-type or status self._have_ct = 0 self._have_status = 0 req.add_common_vars() self.mpyreq = req # some mod_python 2.7.X has no get method for table objects, # so we make a real dict out of it first. if not hasattr(req.subprocess_env,'get'): env=dict(req.subprocess_env) else: env=req.subprocess_env self._setup_vars_from_std_env(env) RequestBase.__init__(self) except error.FatalError, err: self.fail(err) def setup_args(self, form=None): """ Sets up args by using mod_python.util.FieldStorage, which is different to cgi.FieldStorage. So we need a separate method for this. """ from mod_python import util if form is None: form = util.FieldStorage(self.mpyreq) args = {} for key in form.keys(): values = form[key] if not isinstance(values, types.ListType): values = [values] fixedResult = [] for i in values: ## if object has a filename attribute, remember it ## with a name hack if hasattr(i,'filename') and i.filename: args[key+'__filename__']=i.filename has_fn=1 else: has_fn=0 ## mod_python 2.7 might return strings instead ## of Field objects if hasattr(i,'value'): i = i.value ## decode if it is not a file if not has_fn: i = wikiutil.decodeUserInput(i, self.decode_charsets) fixedResult.append(i) args[key] = fixedResult return args def run(self, req): """ mod_python calls this with its request object. We don't need it cause its already passed to __init__. So ignore it and just return RequestBase.run. @param req: the mod_python request instance """ return RequestBase.run(self) def read(self, n=None): """ Read from input stream. """ if n is None: return self.mpyreq.read() else: return self.mpyreq.read(n) def write(self, *data): """ Write to output stream. """ self.mpyreq.write(self.encode(data)) def flush(self): """ We can't flush it, so do nothing. """ pass def finish(self): """ Just return apache.OK. Status is set in req.status. """ RequestBase.finish(self) # is it possible that we need to return something else here? from mod_python import apache return apache.OK # Headers ---------------------------------------------------------- def setHttpHeader(self, header): """ Filters out content-type and status to set them directly in the mod_python request. Rest is put into the headers_out member of the mod_python request. @param header: string, containing valid HTTP header. """ if type(header) is unicode: header = header.encode('ascii') key, value = header.split(':',1) value = value.lstrip() if key.lower() == 'content-type': # save content-type for http_headers if not self._have_ct: # we only use the first content-type! self.mpyreq.content_type = value self._have_ct = 1 elif key.lower() == 'status': # save status for finish try: self.mpyreq.status = int(value.split(' ',1)[0]) except: pass else: self._have_status = 1 else: # this is a header we sent out self.mpyreq.headers_out[key]=value def http_headers(self, more_headers=[]): """ Sends out headers and possibly sets default content-type and status. @keyword more_headers: list of strings, defaults to [] """ for header in more_headers: self.setHttpHeader(header) # if we don't had an content-type header, set text/html if self._have_ct == 0: self.mpyreq.content_type = "text/html;charset=%s" % config.charset # if we don't had a status header, set 200 if self._have_status == 0: self.mpyreq.status = 200 # this is for mod_python 2.7.X, for 3.X it's a NOP self.mpyreq.send_http_header() # FastCGI ----------------------------------------------------------- class RequestFastCGI(RequestBase): """ specialized on FastCGI requests """ def __init__(self, fcgRequest, env, form, properties={}): """ Initializes variables from FastCGI environment and saves FastCGI request and form for further use. @param fcgRequest: the FastCGI request instance. @param env: environment passed by FastCGI. @param form: FieldStorage passed by FastCGI. """ try: self.fcgreq = fcgRequest self.fcgenv = env self.fcgform = form self._setup_vars_from_std_env(env) RequestBase.__init__(self, properties) except error.FatalError, err: self.fail(err) def setup_args(self, form=None): """ Use the FastCGI form to setup arguments. """ if form is None: form = self.fcgform return self._setup_args_from_cgi_form(form) def read(self, n=None): """ Read from input stream. """ if n is None: return self.fcgreq.stdin.read() else: return self.fcgreq.stdin.read(n) def write(self, *data): """ Write to output stream. """ self.fcgreq.out.write(self.encode(data)) def flush(self): """ Flush output stream. """ self.fcgreq.flush_out() def finish(self): """ Call finish method of FastCGI request to finish handling of this request. """ RequestBase.finish(self) self.fcgreq.finish() # Accessors -------------------------------------------------------- def getPathinfo(self): """ Return the remaining part of the URL. """ pathinfo = self.path_info # Fix for bug in IIS/4.0 if os.name == 'nt': scriptname = self.getScriptname() if pathinfo.startswith(scriptname): pathinfo = pathinfo[len(scriptname):] return pathinfo # Headers ---------------------------------------------------------- def setHttpHeader(self, header): """ Save header for later send. """ self.user_headers.append(header) def http_headers(self, more_headers=[]): """ Send out HTTP headers. Possibly set a default content-type. """ if getattr(self, 'sent_headers', None): return self.sent_headers = 1 have_ct = 0 # send http headers for header in more_headers + self.user_headers: if type(header) is unicode: header = header.encode('ascii') if header.lower().startswith("content-type:"): # don't send content-type multiple times! if have_ct: continue have_ct = 1 self.write("%s\r\n" % header) if not have_ct: self.write("Content-type: text/html;charset=%s\r\n" % config.charset) self.write('\r\n') #from pprint import pformat #sys.stderr.write(pformat(more_headers)) #sys.stderr.write(pformat(self.user_headers))