#!/usr/bin/python

import MySQLdb
import sys, os, urllib
import sha
import array
import re

uname_re_str = r'^[a-zA-Z0-9][a-zA-Z0-9\.\-_]{,19}$'

class Ctx:
    def __init__(self, path_info, query_string = None):
	self.path_info = path_info
	path_l = path_info.split('/')
	if path_l[0] == '': path_l = path_l[1:]
	self.path_l = path_l
	self.query_string = query_string
	if query_string == None:
	    self.query = None
	else:
	    self.query = {}
	    for q in query_string.split('&'):
		kv = q.split('=', 1)
		if len(kv) == 2:
		    k, v = kv
		    v = unicode(urllib.unquote_plus(v), 'utf-8')
		    self.query[k] = v
	self.status = '200 Found'
	self.headers = []
	self.content = None
	self.finalstr = []
	self.db = None
	self.uname = None
	self.ustatus = None
	self.urlbase = '/'
	self.wikibase = 'wiki/'
    def flush_headers(self):
	if self.headers:
	    print 'Status: ' + self.status
	    for k, v in self.headers:
		print k + ': ' + v
	    print
	    self.headers = None
    def flush(self):
	self.write(self.finalstr)
	if self.content != None:
	    for l in self.content:
		print l
	    self.content = None
    def header(self, kw, val):
	self.headers.append((kw, val))
    def write(self, str):
	if type(str) == type([]):
	    for s in str:
		self.write_str(s)
	else:
	    self.write_str(str)
    def write_str(self, str):
	if type(str) == type(u''):
	    str = str.encode('utf-8')
	self.flush_headers()
	if self.content == None:
	    print str
	else:
	    self.content.append(str)
    def cursor(self):
	if self.db == None:
	    self.db = MySQLdb.connect(getattr(self, 'dbhost', 'localhost'),
				      getattr(self, 'dbuname', 'root'),
				      getattr(self, 'dbpass', ''),
				      getattr(self, 'dbdbname', 'junk'))
	self.dbversion = map(int, MySQLdb.get_client_info().split('.'))
	return self.db.cursor()

def ctx_from_cgi():
    path_info = os.getenv('PATH_INFO')
    cl = os.getenv('CONTENT_LENGTH')
    if cl:
	query_string = sys.stdin.read(int(cl))
    else:
	query_string = os.getenv('QUERY_STRING')
    ctx = Ctx(path_info, query_string)
    cookie = os.getenv('HTTP_COOKIE')
    if cookie:
	ctx.cookie = cookie
    else:
	ctx.cookie = None
    return ctx

def auth_user(ctx):
    if ctx.ustatus == None:
	ctx.ustatus = 'f'
	if ctx.cookie == None: return ctx.ustatus
	s = ctx.cookie.split('=')
	if len(s) != 2: return ctx.ustatus
	if s[0] == 'id':
	    s = s[1].split(':')
	    if len(s) != 2: return ctx.ustatus
	    uname, qcookie = s
	c = ctx.cursor()
	c.execute('select cookie, status from users where uname = %s', uname)
	response = c.fetchone()
	if response and response[0] == qcookie:
	    ctx.uname = uname
	    ctx.ustatus = response[1]
    return ctx.ustatus

# Convert a blob to a Unicode string
def uniblob(s):
    return unicode(s.tostring(), 'utf-8')

def htmlquote(s):
    s = s.replace('&', '&amp;')
    s = s.replace('<', '&lt;')
    s = s.replace('>', '&gt;')
    return s

def urlquote(s):
    s = s.replace('"', '%22')
    return s

def urlquoteplus(s):
    s = urlquote(s)
    s = s.replace('+', '%2b')
    s = s.replace(' ', '+')
    return s

def randhex(n):
    randstr = file('/dev/urandom', 'rb').read(n)
    return ''.join(['%x' % (ord(x) & 15) for x in randstr])

servtab = {}

def stdpage(ctx, title):
    ctx.header('Content-Type', 'text/html; charset=utf-8')
    ctx.write(['<html><head>',
	       '<style type="text/css">',
	       #'p {font-family: helvetica, arial, sans}',
	       'A.wiki {text-decoration: none}',
	       'A.wiki:hover {text-decoration: underline}',
	       'A.new {text-decoration: none; color: red}',
	       'A.new:hover {text-decoration: underline}',
	       'A.user {text-decoration: none; color: #040}',
	       'A.user:hover {text-decoration: underline}',
	       'h1 {font-family: helvetica, arial, sans}',
	       'h2 {font-family: helvetica, arial, sans}',
	       'h3 {font-family: helvetica, arial, sans}',
	       'h4 {font-family: helvetica, arial, sans}',
	       'h5 {font-family: helvetica, arial, sans}',
	       'h6 {font-family: helvetica, arial, sans}',
	       '.navbar { margin: 0; padding: 0.25em 0; border-top: 2px solid #a98; text-align: right; line-height: normal; font-family: helvetica, arial, sans; font-size: 90%}',
	       '.navbar a { background-color: #fff8e8; border: 1px solid #a97; padding: 0.25em 0.5em; text-decoration: none; color: #00a }',
	       '.navbar a:hover { background-color: #fffaf4; border: 1px solid #653; text-decoration: none; color: #00d }',
	       '.sexp { color: #008 }',
	       '</style>',
	       '<title>', title, '</title></head>',
	       '<body><h1>', title, '</h1>'])
    ctx.finalstr.insert(0, '</body></html>')

def serve_404(ctx):
    ctx.status = '404 Not Found'
    stdpage(ctx, 'Not found')
    ctx.write('<p>What you are looking for may exist, but it is not here.</p>')

def navbar(ctx, items = []):
    items.append(wikilink(ctx, '', True))
    items.append(wikilink(ctx, 'RecentChanges', True))
    if auth_user(ctx) == 'f':
	items.append('<a class="wiki" href="' + ctx.urlbase + '">Log in</a>')
    ctx.write('<p class="navbar">' + ' '.join(items) + '</p>')

def serve_home(ctx):
    stdpage(ctx, 'Home page')
    if auth_user(ctx) != 'f':
	navbar(ctx)
    else:
	ctx.write(
	    ['<p>Log in below, or go straight to the <a href="wiki/">wiki</a>.</p>',
	     '<form id="theform" method="POST" action="loginsub.html" accept-charset="UTF-8">',
	     '<div style="background-color: #bce; border: #008 1px solid; padding: 8px; width: 250px">'
	     '<p style="margin: 2px">Username:<br />',
	     '<input id="first" name="u" size="20" type="text" />',
	     '</p>',
	     '<p style="margin: 2px">Password:<br />',
	     '<input name="pass" size="20" type="password" />',
	     '</p>',
	     '<p style="margin: 2px">',
	     '<input type="submit" value="Login" />',
	     '</p>',
	     '</div></form>',
	     '<script type="text/javascript">document.theform.first.focus()</script>',
	     '<p>If you don\'t have an account and want one, please <a href="signup.html">sign up</a>.</p>'])

def serve_signup(ctx, extra = None):
    stdpage(ctx, 'Sign up for a new account')
    ctx.write(
	['<p>Sign up for a new account here.</p>',
	 '<form id="theform" method="POST" action="signupsub.html" accept-charset="UTF-8">'])
    if extra != None:
	ctx.write(extra)
    ctx.write(
	['<p>Username (up to 20 characters, alphanumeric and <tt>_.-</tt>):<br />',
	 '<input id="first" name="u" size="20" type="text" />',
	 '</p>',
	 '<p>Password:<br />',
	 '<input name="pass" size="20" type="password" />',
	 '</p>',
	 '<p>Real name:<br />',
	 '<input name="realname" size="60" type="text" />',
	 '</p>',
	 '<p>After you sign up, you\'ll get your own wiki page where you can link your homepage, describe yourself, and so on.</p>',
	 '<p>',
	 '<input type="submit" value="Signup" />',
	 '</p>',
	 '</form>',
	 '<script type="text/javascript">document.theform.first.focus()</script>'])

def serve_loginsub(ctx):
    uname = ctx.query['u']
    c = ctx.cursor()
    c.execute("select salt, hash, cookie from users where uname = %s", uname)
    response = c.fetchone()
    if response:
	salt, hash, cookie = response
    else:
	salt, hash = '', ''

    qhash = sha.sha(salt + ctx.query['pass'].encode('utf-8')).hexdigest()

    if hash == qhash:
	ctx.header('Set-Cookie',  'id=' + uname + ':' + cookie)
	stdpage(ctx, 'Login successful')
	ctx.write("<p>Username and password match, setting cookie.</p>")
    else:
	stdpage(ctx, 'Login failed')
	ctx.write(
	    ['<p>Oops, your login failed.</p>'])
    navbar(ctx)

def serve_signupsub(ctx):
    uname = ctx.query['u']
    if not re.match(uname_re_str, uname):
	serve_signup(ctx, '<p><span style="color: #c00">Invalid username.</span> The username must be no longer than 20 characters, begin with an alphanumeric, and consist of alphanumerics, period, underscore, or dash. In other words, it must match the regular expression syntax: <tt>' + uname_re_str + '</tt></p>')
	return
    c = ctx.cursor()
    c.execute("select realname from users where uname = %s", uname)
    response = c.fetchone()
    if response:
	realname = uniblob(response[0])
	serve_signup(ctx, '<p><span style="color: #c00">User already exists.</span> The username <tt>' + uname + '</tt> already belongs to ' + htmlquote(realname) + '.</p>')
	return
    passwd = ctx.query['pass']
    if len(passwd) < 4:
	serve_signup(ctx, '<p><span style="color: #c00">Password too short.</span> The password must be at least 4 characters.</p>')
	return
    realname = ctx.query['realname'].strip()
    if len(realname) < 1:
	serve_signup(ctx, '<p><span style="color: #c00">Give your real name.</span> You must put include your name.</p>')
    entropy = randhex(52)
    cookie = entropy[:40]
    salt = entropy[40:]
    hash = sha.sha(salt + passwd.encode('utf-8')).hexdigest()
    c.execute("insert into users (uname, salt, hash, status, cookie, realname, ctime) values (%s, %s, %s, 'y', %s, %s, now())",
		(uname, salt, hash, cookie, realname.encode('utf-8')))
    ctx.header('Set-Cookie',  'id=' + uname + ':' + cookie)
    stdpage(ctx, 'Signup successful')
    ctx.write("<p>Congratulations! You have created the account <tt>" + uname +
	      "</tt>. Next, you'll probably want to edit the wiki page for your account: " +
	      userlink(ctx, uname) + ".</p>")
    navbar(ctx)

def check_auth(ctx, perms = 'r'):
    status = auth_user(ctx)
    if status == 'y' or not 'w' in perms:
	return True
    else:
	ctx.status = '403 Forbidden'
	stdpage(ctx, 'Not authorized')
	ctx.write("<p>Sorry, you're not authorized to make changes to the wiki.</p>")
	navbar(ctx)
	return False

def userlink(ctx, uname, body = None):
    c = ctx.cursor()
    c.execute('select realname from users where uname = %s', uname)
    response = c.fetchone()
    if response:
	realname = uniblob(response[0])
	if body == None:
	    body = realname
	return '<a class="user" href="' + ctx.urlbase + 'user/' + uname + '">' + htmlquote(body) + '</a>'
    else:
	return '[deleted account ' + uname + ']'

def wikiexists(ctx, page):
    # We should probably just always prepend the wikibase.
    if ctx.wikibase != 'wiki/' and page.find('/') < 0:
	page = ctx.wikibase + page
	page += '/_thm' # hack
    if page in ('RecentChanges',): return True
    c = ctx.cursor()
    if ctx.dbversion >= [4, 1, 0]:
	c.execute('select uname, mtime from log where seqno = (select max(seqno) from log where what = %s)', page)
	return c.fetchone()
    else:
	c.execute('select max(seqno) from log where what = %s', page)
	response = c.fetchone()
	if response:
	    c.execute('select uname, mtime from log where seqno = %s', response)
	    return c.fetchone()

def getwiki(ctx, page):
    c = ctx.cursor()
    if ctx.dbversion >= [4, 1, 0]:
	c.execute('select data, uname, mtime from log where seqno = (select max(seqno) from log where what = %s)', page)
	response = c.fetchone()
    else:
	# Subqueries are the right way to do this. We'll want to take this
	# garbage out as soon as we know we're only running on 4.1.0 or higher.
	c.execute('select max(seqno) from log where what = %s', page)
	response = c.fetchone()
	if response:
	    c.execute('select data, uname, mtime from log where seqno = %s', response)
	    response = c.fetchone()
    if response:
	data = uniblob(response[0])
	uname = response[1]
	mtime = response[2]
	return data, uname, mtime
    else:
	return None

# Determine URL for given wiki resource
def wikiurl(ctx, page):
    if page.find('/') >= 0:
	pref = ''
    else:
	pref = ctx.wikibase
    return ctx.urlbase + pref + urlquoteplus(page)

def wikilink(ctx, str, exists = None, ispreblock = False):
    linkl = str.split('|', 1)
    if len(linkl) == 2:
	link, body = linkl
    else:
	link, = linkl
	body = None
    aclass = ''
    if re.match(r'[a-z\+]*://', link):
	url = link
    elif re.match('user/', link):
	return userlink(ctx, link[5:], body)
    else:
	if exists or (exists == None and wikiexists(ctx, str)):
	    aclass = 'class="wiki" '
	else:
	    aclass = 'class="new" '
	if str == '' and body == None:
	    body = 'wiki home page'
	url = wikiurl(ctx, link)
    if body == None:
	body = link
    return '<a ' + aclass + 'href="' + url + '">' + htmlquote(body) + '</a>'

def wikigh(ctx, str, ispreblock):
    return '<span class="sexp">' + htmlquote(str) + '</span>'

def wikipre(ctx, str, ispreblock):
    if ispreblock:
	return '<pre>' + htmlquote(str) + '</pre>'
    else:
	return '<tt>' + htmlquote(str) + '</tt>'

class ListState:
    def __init__(self):
	self.level = 0
    def begblock(self, result, level, tag = ''):
	if level > self.level:
	    result.append('<ul>' * (level - self.level) + tag)
	elif level < self.level:
	    result.append('</ul>' * (self.level - level) + tag)
	elif tag != '':
	    result.append(tag)
	self.level = level
	if tag != '': return '</' + tag[1:]

def wikiformat(ctx, str):
    eol_re = re.compile(r'\r?\n')
    blank_re = re.compile(r'\s*$')
    rule_re = re.compile(r'\s*\-{4,}\s*$')
    bullet_re = re.compile(r'(\*+)\s+')
    head_re = re.compile(r'(={2,6})\s*')
    special_re = re.compile(r'''
        [<>&\\] |          # HTML entities and backslash
	(?<!-)---?(?!-) |  # en and em dash, exactly 2 or 3 hyphens
	(?<!:)// |         # begin italic markup, but avoid URL's
	\*\* |             # begin bold markup
        \[\[ |             # open link
        \{\{\{ |           # open preformatted
        (?<!\S)            # single char markup must follow whitespace
          ([#_\*\[])
          (?=\S)           # and not be followed by whitespace
	''', re.VERBOSE)
    repls = {'<': '&lt;', '>': '&gt;', '&': '&amp;',
	     '--': '&#x2013;', '---': '&#x2014;'}
    emphs = {'_': 'i', '*' : 'b', '//': 'em', '**': 'strong'}
    embeds = {'[': (r'\]', wikilink), '[[': (r'\]\]', wikilink), 
	      '#': (r'#', wikigh),
	      '{{{': (r'\}\}\}(?!\})', wikipre)}
    for open, (close, embedfn) in embeds.items():
	embeds[open] = (re.compile(close), embedfn)
    result = []
    pclose = None
    stack = []
    liststate = ListState()
    lines = eol_re.split(str) + ['']
    i = 0
    while i < len(lines):
	line = lines[i]
	i += 1
	if pclose in ('</li>', None):
	    bullet_m = bullet_re.match(line)
	else:
	    bullet_m = None
	if pclose != None and (blank_re.match(line) or bullet_m):
	    if stack:
		stack.reverse()
		result.append(''.join([htmlclose for wikiclose, htmlclose in stack]))
		stack = []
	    result.append(pclose)
	    pclose = None
	elif pclose == None and rule_re.match(line):
	    liststate.begblock(result, 0, '<hr />')
	    continue
	if blank_re.match(line): continue
	if pclose == None:
	    head_m = head_re.match(line)
	    if bullet_m:
		level = len(bullet_m.group(1))
		pclose = liststate.begblock(result, level, '<li>')
		pos = bullet_m.end()
	    elif head_m:
		delim = head_m.group(1)
		pclose = liststate.begblock(result, 0, '<h%d>' % len(delim))
		stack.append((delim, ''))
		pos = head_m.end()
	    else:
		pclose = liststate.begblock(result, 0, '<p>')
		pos = 0
	else:
	    pos = 0
	rline = ''
	while pos < len(line):
	    m = special_re.search(line, pos)
	    if len(stack):
		# Handle closing tags for inline markup
		pos2 = line.find(stack[-1][0], pos)
		if pos2 >= 0 and (not m or pos2 <= m.start()):
		    rline += line[pos:pos2] + stack[-1][1]
		    pos = pos2 + len(stack[-1][0])
		    del stack[-1]
		    continue
	    if m:
		rline += line[pos:m.start()]
		g = m.group()
		mend = m.end()
		if repls.has_key(g):
		    rline += repls[g]
		    pos = mend
		elif emphs.has_key(g):
		    rline += '<' + emphs[g] + '>'
		    stack.append((g, '</' + emphs[g] + '>'))
		    pos = mend
		elif g in embeds:
		    wikiclose, embedfn = embeds[g]
		    sline, spos = i - 1, mend
		    ispreblock = g == '{{{' and mend == 3 and blank_re.match(line, 3)
		    while sline < len(lines):
			end_m = wikiclose.search(lines[sline], spos)
			if end_m and ispreblock and (end_m.start() != 0 or
					  not blank_re.match(lines[sline], 3)):
			    end_m = None
			if end_m:
			    break
			sline, spos = sline + 1, 0
		    if end_m:
			if sline == i - 1:
			    str = line[mend:end_m.start()]
			else:
			    str = line[mend:] + '\n'
			    for j in range(i, sline):
				str += lines[j] + '\n'
			    str += lines[sline][:end_m.start()]
			rline += embedfn(ctx, str, ispreblock = ispreblock)
			i = sline + 1
			line = lines[sline]
			pos = end_m.end()
			continue
		elif g == '\\':
		    if mend < len(line) and line[mend] in '\\{}_#[]*-/':
			rline += line[mend]
			pos = mend + 1
		if pos < mend:
		    # Nothing matched above, pass verbatim.
		    rline += g
		    pos = mend
	    else:
		rline += line[pos:]
		break
	result.append(rline)
    liststate.begblock(result, 0)
    return result

def serve_wiki_post(ctx, page):
    if not check_auth(ctx, 'w'): return
    data = ctx.query['data'].encode('utf-8')
    data = data.replace('\r', '')
    if ctx.query['submit'] == 'Preview':
	return serve_wiki_edit(ctx, page, data)
    c = ctx.cursor()
    c.execute("insert into log (uname, what, cmd, mtime, data) values (%s, %s, 'u', now(), %s)", (ctx.uname, page, data))
    stdpage(ctx, 'Thanks')
    ctx.write('<p>Thank you for your update to ' + wikilink(ctx, page) + '.</p>')
    navbar(ctx)

def serve_wiki_edit(ctx, page, data = None):
    if not check_auth(ctx, 'w'): return

    if data == None:
	response = getwiki(ctx, page)
	if response:
	    data = response[0]
	    stdpage(ctx, 'Editing ' + htmlquote(page))
	else:
	    stdpage(ctx, 'Creating ' + htmlquote(page))
	    data = ''
    else:
	stdpage(ctx, 'Preview of ' + htmlquote(page))
    ctx.write(wikiformat(ctx, data))
    ctx.write(['<form id="theform" method="POST" action="' + wikiurl(ctx, page) +
	       '?a=post" accept-charset="UTF-8">',
	       '<input type="hidden" name="a" value="post" />',
	       '<p><textarea id="first" name="data" cols="80" rows="24">'])
    ctx.write(htmlquote(data) + '</textarea>')
    ctx.write(['</p>',
	       '<p>',
	       '<input type="submit" name="submit" value="Preview" />',
	       '<input type="submit" name="submit" value="Save" />',
	       '</p>',
	       '</form>',
	       '<script type="text/javascript">document.theform.first.focus()</script>'])

def serve_wiki_recent(ctx, uniq = True):
    stdpage(ctx, 'Recent Changes')

    c = ctx.cursor()
    c.execute('select what, uname, mtime from log order by seqno desc limit 100')
    pages = {}
    closelist = None
    date = None
    for page, name, mtime in c.fetchall():
	page = unicode(page, 'utf-8')
	if not (uniq and pages.has_key(page)):
	    datestr = mtime.isoformat(' ')
	    if date != datestr[:10]:
		date = datestr[:10]
		if closelist: ctx.write(closelist)
		ctx.write('<p><b>' + date + '</b></p><ul>')
		closelist = '</ul>'
	    ctx.write('<li>' + datestr[11:] + ' ' + wikilink(ctx, page, exists = True) + ' . . . ' + userlink(ctx, name) + '</li>')
	    pages[page] = True
    if closelist: ctx.write(closelist)
    navbar(ctx)

def serve_wiki_page(ctx, page, extra = None):
    if ctx.query.has_key('a'):
	if ctx.query['a'] == 'edit':
	    return serve_wiki_edit(ctx, page)
	elif ctx.query['a'] == 'post':
	    return serve_wiki_post(ctx, page)
	else:
	    stdpage(ctx, 'Action unknown')
	    ctx.write('<p>The wiki action "' +
		      htmlquote(ctx.query['a']) + '" is not known.<p>')
    stdpage(ctx, 'Wiki: ' + htmlquote(page))

    response = getwiki(ctx, page)
    if response:
	content, uname, mtime = response
	if extra != None: ctx.write(extra)
	#ctx.write(`content`)
	ctx.write(wikiformat(ctx, content))
	ctx.write('<p>Last edited ' + mtime.isoformat(' ') + ' by ' + userlink(ctx, uname) + '</p>')
    else:
	if extra != None: ctx.write(extra)
	ctx.write('<p>No entry exists yet for ' + htmlquote(page) + '.</p>')

    items = []
    if auth_user(ctx) == 'y':
	items.append('<a href="' + wikiurl(ctx, page) + '?a=edit">Edit</a>')
    navbar(ctx, items)

def serve_wiki(ctx):
    if len(ctx.path_l) == 1:
	page = urllib.unquote_plus(ctx.path_l[0])
    else:
	page = ''

    if page == 'RecentChanges':
	return serve_wiki_recent(ctx)

    serve_wiki_page(ctx, page)

def serve_set(ctx):
    if len(ctx.path_l) == 1:
	label = urllib.unquote_plus(ctx.path_l[0])
    else:
	return serve_404(ctx)

    stdpage(ctx, 'Theorem: ' + htmlquote(label))
    response = getwiki(ctx, 'mm/set/%s/_norm' % label)
    if response:
	content, uname, mtime = response
	ctx.wikibase = 'mm/set/'
	ctx.write(wikiformat(ctx, content))
    response = getwiki(ctx, 'mm/set/%s/_thm' % label)
    if response:
	content, uname, mtime = response
	ctx.write('<pre>')
	ctx.write([htmlquote(line) for line in content.split('\n')])
	ctx.write('</pre>')
    ctx.wikibase = 'wiki/'
    navbar(ctx)

def serve_user(ctx):
    if len(ctx.path_l) == 1 and re.match(uname_re_str, ctx.path_l[0]):
	uname = ctx.path_l[0]
	c = ctx.cursor()
	c.execute('select realname from users where uname = %s', uname)
	response = c.fetchone()
	if response:
	    realname = uniblob(response[0])
	    extra = ['<p>Information for ' + htmlquote(realname) +
			    ' (username <tt>' + uname + '</tt>).</p>']
	    c.execute('select what, mtime from log where uname = %s order by seqno desc limit 1', uname)
	    response = c.fetchone()
	    if response:
		page, mtime = response
		page = unicode(page, 'utf-8')
		extra.append('<p>Last page edited: ' + wikilink(ctx, page) +
			     ' on ' + mtime.isoformat(' ') + '.</p>')
	    extra.append('<hr />')
	    serve_wiki_page(ctx, 'user/' + uname, extra)
	    return
    serve_404(ctx)

servtab[''] = serve_home
servtab['loginsub.html'] = serve_loginsub
servtab['signup.html'] = serve_signup
servtab['signupsub.html'] = serve_signupsub
servtab['wiki'] = serve_wiki
servtab['user'] = serve_user
servtab['mm'] = {} # hacky, but it'll do for now
servtab['mm']['set'] = serve_set

def runcgi():
    ctx = ctx_from_cgi()
    run(ctx)

def run(ctx):
    dispatch = servtab
    while type(dispatch) == type({}):
	first = ctx.path_l[0]
	if dispatch.has_key(first):
	    del ctx.path_l[0]
	    dispatch = dispatch[first]
	else:
	    dispatch = serve_404
	    break
    dispatch(ctx)
    ctx.flush()

if __name__ == '__main__':
    runcgi()
