#! /usr/bin/env python
"""Quick-quick implementation of WikiWikiWeb in Python
"""
# Modifications: Copyright (C) 2009 Darrell Karbott

# --- original piki copyright notice ---
# Copyright (C) 1999, 2000 Martin Pool <mbp@humbug.org.au>

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA

#pylint: disable-msg=C0111, W0331, W0106, C0103
__version__ = '$Revision: 1.62 $'[11:-2];


import cgi, codecs, sys, string, os, re, errno, time
from os import path, environ
from socket import gethostbyaddr
from time import localtime, strftime
# NOTE: cStringIO doesn't work for unicode.
from StringIO import StringIO
import fileoverlay
filefuncs = None

# File to redirect sys.stderr to.
#STDERR_FILE = '/tmp/piki_err' # REDFLAG: Comment out this line
STDERR_FILE = None # Silently drop all output to stderr

PIKI_PNG = 'pikipiki-logo.png'
PIKI_CSS = 'piki.css'
ACTIVELINK = 'activelink.png'
#FAVICON = 'favicon.ico'

# HTTP server doesn't need to serve any other files to make piki work.
PIKI_REQUIRED_FILES = (PIKI_PNG, PIKI_CSS, ACTIVELINK)

# class UnicodeStringIO(StringIO):
#     """ Debugging hack fails early when non low-ASCII 8-bit strings are
#         printed. """
#     def __init__(self, arg=u''):
#         StringIO.__init__(self, arg)
#     def write(self, bytes):
#         if not isinstance(bytes, unicode):
#             # Non-unicode strings should be 7-bit ASCII.
#             # This will raise if the are not.
#             bytes = bytes.decode('ascii')
#         return StringIO.write(self,bytes)

scrub_links = False
LINKS_DISABLED_PAGE = "LinksDisabledWhileEditing"

def scrub(link_text, ss_class=None, force=False):
    """ Cleanup href values so the work with Freenet. """
    if (not scrub_links) and (not force):
        return link_text

    # Handle pages which haven't been written yet gracefully.
    if ss_class == 'nonexistent':
        return "NotWrittenYet"

    if link_text.startswith('/'):
        link_text = link_text[1:]

    if link_text.startswith('freenet:'):
        # HACK: facist fproxy html validator chokes on freenet: links?
        link_text = "/" + link_text[len('freenet:'):]
    # We lean on the fproxy filter to catch external links.
    # hmmm... Do better?
    return link_text

def emit_header():
    print "Content-type: text/html; charset=utf-8"
    print

# Regular expression defining a WikiWord (but this definition
# is also assumed in other places.
word_re_str = r"\b([A-Z][a-z]+){2,}\b"
word_anchored_re = re.compile('^' + word_re_str + '$')
versioned_page_re_str = (r"\b(?P<wikiword>([A-Z][a-z]+){2,})" +
                         r"(_(?P<version>([a-f0-9]{40,40})))?\b")
versioned_page_re = re.compile('^' + versioned_page_re_str + '$')
command_re_str = "(search|edit|fullsearch|titlesearch)\=(.*)"

# Formatting stuff --------------------------------------------------


def get_scriptname():
    return environ.get('SCRIPT_NAME', '')


def send_title(text, link=None, msg=None, is_forked=False):
    print "<head><title>%s</title>" % text
    if css_url:
        print '<link rel="stylesheet" type="text/css" href="%s">' % \
              scrub(css_url)
    print "</head>"
    print '<body><h1>'
    if get_logo_string():
        print link_tag('RemoteChanges', get_logo_string())
    if link:
        classattr = ''
        if is_forked:
            classattr = ' class="forkedtitle" '
        print '<a%s href="%s">%s</a>' % (classattr, scrub(link), text)
    else:
        print text
    print '</h1>'
    if msg: print msg
    print '<hr>'



def link_tag(params, text=None, ss_class=None):
    if text is None:
        text = params                   # default
    if ss_class:
        classattr = 'class="%s" ' % ss_class
    else:
        classattr = ''

    return '<a %s href="%s">%s</a>' % (classattr,
                                       scrub("%s/%s" % (get_scriptname(),
                                                        params), ss_class),
                                       text)

# Search ---------------------------------------------------

def do_fullsearch(needle):
    send_title('Full text search for "%s"' % (needle))

    needle_re = re.compile(needle, re.IGNORECASE)
    hits = []
    all_pages = page_list()
    for page_name in all_pages:
        body = Page(page_name).get_raw_body()
        count = len(needle_re.findall(body))
        if count:
            hits.append((count, page_name))

    # The default comparison for tuples compares elements in order,
    # so this sorts by number of hits
    hits.sort()
    hits.reverse()

    print "<UL>"
    for (count, page_name) in hits:
        print '<LI>' + Page(page_name).link_to()
        print ' . . . . ' + `count`
        print ['match', 'matches'][count <> 1]
    print "</UL>"

    print_search_stats(len(hits), len(all_pages))


def do_titlesearch(needle):
    # TODO: check needle is legal -- but probably we can just accept any
    # RE

    send_title("Title search for \"" + needle + '"')
    
    needle_re = re.compile(needle, re.IGNORECASE)
    all_pages = page_list()
    hits = filter(needle_re.search, all_pages)

    print "<UL>"
    for filename in hits:
        print '<LI>' + Page(filename).link_to()
    print "</UL>"

    print_search_stats(len(hits), len(all_pages))


def print_search_stats(hits, searched):
    print "<p>%d hits " % hits
    print " out of %d pages searched." % searched


def do_edit(pagename):
    Page(pagename).send_editor()

def do_viewsource(pagename):
    Page(pagename).send_editor(True)

def do_viewunmodifiedsource(pagename):
    assert not is_versioned(pagename)
    if not filefuncs.exists(Page(pagename)._text_filename(),
                            True):
        # Doesn't exist!
        send_title("Page Doesn't Exist!", None,
                   "The original version doesn't have a %s page." %
                   pagename)
    else:
        Page(pagename).send_editor(True, True)

def do_savepage(pagename):
    global form
    pg = Page(pagename)
    text = ''
    if 'savetext' in form:
        text = form['savetext'].value
    if text.strip() == '':
        text = ''
        msg = """<b>Locally deleting blank page.</b>"""
    else:
        msg = """<b>Saved local changes. They won't be applied to the
              wiki in Freenet until you explictly <em>submit</em> them. </b>"""

    # Decode the utf8 text from the browser into unicode.
    text = text.decode('utf8')

    # Normalize to UNIX line terminators.
    pg.save_text(text.replace('\r\n', '\n'))

    pg.send_page(msg=msg)

def do_removepage(page_name):
    if not is_versioned(page_name):
        msg = """<b>Locally removed page.</b>"""
    else:
        msg = """<b>Locally marked fork as resolved.</b>"""
    pg = Page(page_name)
    pg.save_text('')
    pg.send_page(msg=msg)

def do_unmodified(pagename):
    if not filefuncs.exists(Page(pagename)._text_filename(),
                            True):
        # Doesn't exist!
        send_title("Page Doesn't Exist!", None,
                   "The original version doesn't have a %s page." %
                   pagename)
    else:
        # Send original.
        Page(pagename).send_page('Original Version', True)

def do_deletelocal(pagename):
    filefuncs.remove_overlay(Page(pagename)._text_filename())

    send_title("Removed Local Edits", None,
               "Removed local edits to %s page." %
               pagename)
    print "Local changes to %s have been deleted. <p>" % Page(
        pagename).link_to()
    print "Here's a link to the %s." % Page('FrontPage').link_to()

def make_index_key():
    s = '<p><center>'
    links = map(lambda ch: '<a href="#%s">%s</a>' % (ch, ch),
                string.lowercase)
    s = s + string.join(links, ' | ')
    s = s + '</center><p>'
    return s


def page_list(include_versioned=False):
    if include_versioned:
        return filter(versioned_page_re.match,
                      filefuncs.list_pages(text_dir))
    return filter(word_anchored_re.match,
                  filefuncs.list_pages(text_dir))

# ----------------------------------------------------------
# Macros
def _macro_TitleSearch():
    return _macro_search("titlesearch")

def _macro_FullSearch():
    return _macro_search("fullsearch")

def _macro_search(type):
    if form.has_key('value'):
        default = form["value"].value.encode('utf8')
    else:
        default = ''
    return """<form method=get accept-charset="UTF-8">
    <input name=%s size=30 value="%s"> 
    <input type=submit value="Go">
    </form>""" % (type, default)

def _macro_GoTo():
    return """<form method=get accept-charset="UTF-8"><input name=goto size=30>
    <input type=submit value="Go">
    </form>"""
    # isindex is deprecated, but it gives the right result here

def _macro_WordIndex():
    s = make_index_key()
    pages = list(page_list())
    map = {}
    word_re = re.compile('[A-Z][a-z]+')
    for name in pages:
        for word in word_re.findall(name):
            try:
                map[word].append(name)
            except KeyError:
                map[word] = [name]

    all_words = map.keys()
    all_words.sort()
    last_letter = None
    for word in all_words:
        letter = string.lower(word[0])
        if letter <> last_letter:
            s = s + '<a name="%s"><h3>%s</h3></a>' % (letter, letter)
            last_letter = letter

        s = s + '<b>%s</b><ul>' % word
        #links = map[word] # <-- Has duplicate links!
        #
        # Quick and dirty fix for muliple link BUG. Revisit.
        links = list(set(map[word]))
        links.sort()
        last_page = None
        for name in links:
            if name == last_page: continue
            s = s + '<li>' + Page(name).link_to()
        s = s + '</ul>'
    return s


def _macro_TitleIndex():
    s = make_index_key()
    pages = list(page_list())
    pages.sort()
    current_letter = None
    for name in pages:
        letter = string.lower(name[0])
        if letter <> current_letter:
            s = s + '<a name="%s"><h3>%s</h3></a>' % (letter, letter)
            current_letter = letter
        else:
            s = s + '<br>'
        s = s + Page(name).link_to()
    return s


def _macro_ActiveLink():
    return '<img src="%s" />' % scrub('/' + ACTIVELINK)


def get_unmerged_versions(overlay, wikitext_dir, page_names):
    # name -> version list
    ret = {}
    for name in page_names:
        ret[name] = set([]) # hmmm paranoia? list ok?

    # REDFLAG: O(N) in total number of pages.  hmmmm...
    for name in overlay.list_pages(wikitext_dir):
        fields = name.split('_')
        if len(fields) < 2:
            continue
        if not fields[0].strip() in page_names:
            continue
        if len(fields[1].strip()) != 40:
            continue
        # hmmmm... validate?
        ret[fields[0].strip()].add(fields[1].strip())

    for name in ret.keys()[:]: # hmmm copy required?
        ret[name] = list(ret[name])
        ret[name].sort()

    return ret

def fork_link(overlay, text_dir_, name, version):
    full_name = '%s_%s' % (name, version)
    css_class = "removedfork"
    if overlay.exists(os.path.join(text_dir_,
                                   full_name)):
        css_class = "existingfork"

    if scrub_links and css_class == "removedfork":
        # Prevent broken linx when dumping freesite.
        full_name = 'AlreadyResolved'

    return link_tag(full_name, '(' + version[:12] + ')',
                    css_class)

def get_fork_html(overlay, text_dir_, name, table):

    return ''.join([fork_link(overlay, text_dir_, name, ver)
                    for ver in table[name]])

def _macro_LocalChanges():
    if not filefuncs.is_overlayed():
        return "<br>Not using overlayed editing!<br>"

    local = set([])
    for name in filefuncs.list_pages(text_dir):
        if filefuncs.has_overlay(os.path.join(text_dir,
                                              name)):
            local.add(name.split('_')[0])
    local = list(local)
    local.sort()
    if len(local) == 0:
        return "<br>No locally edited pages.<br>"

    fork_table = get_unmerged_versions(filefuncs, text_dir, local)
    return '<br>' + '<br>'.join(["%s %s" %
                                 (Page(name).link_to(),
                                  get_fork_html(filefuncs, text_dir,
                                                name, fork_table))
                                 for name in local]) + '<br>'

def get_page_ref(page_name):
    match = versioned_page_re.match(page_name)
    if not match:
        return "ILLEGAL_NAME"
    name = match.group('wikiword')
    ver = match.group('version')
    if not ver:
        return Page(page_name).link_to()

    return "<em>%s(%s)</em>" % (name, ver[:12])

def _macro_RemoteChanges():
    words = ('created', 'modified', 'removed', 'forked')
    reject_reasons = {
        0:"Unknown",
        1:"Server couldn't read submission CHK",
        2:"Insufficient trust",
        3:"Already applied", # Fully applied
        4:"Already applied", # Partially applied.  (not used anymore)
        5:"Conflict",
        6:"Illegal or Malformed submission"
        }

    def file_changes(changes):
        tmps = []

        for index, change in enumerate(changes):
            if len(change) == 0:
                continue
            if index == len(words) - 1:
                # Special case forked files.
                wiki_names = change.keys()
                wiki_names.sort()

                tmps.append("%s:%s" % (words[index],
                                       ','.join([(Page(name).link_to() + " " +
                                                  get_fork_html(filefuncs,
                                                                text_dir,
                                                                name,
                                                                change))
                                                 for name in wiki_names])))
                continue

            tmps.append("%s:%s" % (words[index],
                                   ','.join([get_page_ref(name)
                                             for name in change])))
        return ','.join(tmps)

    def accept_summary(entry, time_tuple):
        return ('<strong>%s accepted from %s</strong><br>%s</br>' %
                (time.strftime(changed_time_fmt, time_tuple),
                 entry[2],
                 file_changes(entry[4])))

    def reject_summary(entry, time_tuple):
        return ('<strong>%s rejected from %s</strong><br>%s</br>' %
                (time.strftime(changed_time_fmt, time_tuple),
                 entry[2],
                 reject_reasons.get(int(entry[4]),
                                    "unknown code:%i" % int(entry[4]))))
    accepted, rejected = read_log_file_entries(data_dir, 20)
    by_time = [(entry[1], True, entry) for entry in accepted]
    for entry in rejected:
        by_time.append((entry[1], False, entry))
    by_time.sort(reverse=True) # Since 2.4. Ok.

    buf = StringIO()
    ratchet_day = None
    for sort_tuple in by_time:
        entry = sort_tuple[2]
        # year, month, day, DoW
        time_tuple = time.gmtime(float(entry[1]))
        day = tuple(time_tuple[0:3])
        if day <> ratchet_day:
            #buf.write('<h3>%s</h3>' % strftime(date_fmt, time_tuple))
            buf.write('<h3>%s</h3>' % strftime(date_fmt, time_tuple))
            ratchet_day = day
        if sort_tuple[1]:
            buf.write(accept_summary(entry, time_tuple))
        else:
            buf.write(reject_summary(entry, time_tuple))
    return buf.getvalue()

def _macro_BookMark():
    try:
        usk, desc, link_name = read_info()
    except ValueError, err:
        return "[BookMark macro failed: %s]" % str(err.args[0])

    if not scrub_links:
        return '<a href="%s">%s</a>' % (LINKS_DISABLED_PAGE, link_name)

    return ('<a href="/?newbookmark=%s&desc=%s">%s</a>'
            % (usk, desc, link_name))


def _macro_FreesiteUri():
    try:
        usk, desc, link_name = read_info()
        fields = usk.split('/')
        if len(fields) > 2 and (not fields[-2].startswith('-')):
            fields[-2] = '-' + fields[-2]
            usk = '/'.join(fields)

    except ValueError, err:
        return "[FreesiteUri macro failed: %s]" % str(err.args[0])

    if not scrub_links:
        return '<a href="%s">freenet:%s</a>' % (LINKS_DISABLED_PAGE, usk)


    return '<a href="/%s">freenet:%s</a>' % (usk, usk)

# ----------------------------------------------------------

# REDFLAG: faster way to do this? does it matter?
def has_illegal_chars(value):
    """ Catch illegal characters in image macros. """
    for char in ('<', '>', '&', '\\', "'", '"'):
        if value.find(char) != -1:
            return True
    return False

class PageFormatter:
    """Object that turns Wiki markup into HTML.

    All formatting commands can be parsed one line at a time, though
    some state is carried over between lines.
    """
    def __init__(self, raw):
        self.raw = raw
        self.is_em = self.is_b = 0
        self.list_indents = []
        self.in_pre = 0
        self.in_table = 0


    def _emph_repl(self, word):
        if len(word) == 3:
            self.is_b = not self.is_b
            return ['</b>', '<b>'][self.is_b]
        else:
            self.is_em = not self.is_em
            return ['</em>', '<em>'][self.is_em]

    def _rule_repl(self, word):
        s = self._undent()
        if len(word) <= 4:
            s = s + "\n<hr>\n"
        else:
            s = s + "\n<hr size=%d>\n" % (len(word) - 2 )
        return s

    def _word_repl(self, word):
        return Page(word).link_to()


    def _url_repl(self, word):
        if not scrub_links:
            return '<a href="%s">%s</a>' % (LINKS_DISABLED_PAGE, word)

        return '<a href="%s">%s</a>' % (scrub(word), word)

    def _img_repl(self, word):
        # REDFLAG:  Can't handle URIs with '|'. Do better.
        # [[[freenet:keyvalue|alt text|title text]]]
        word = word[3:-3] # grrrrr... _macro_repl is doing this too.
        fields = word.strip().split('|')
        if has_illegal_chars(word):
            return (" <br>[ILLEGAL IMAGE MACRO IN WIKITEXT: " +
                    " illegal characters! ]<br> ")

        uri = scrub(fields[0], None, True)

        # ONLY static images are allowed!
        if not (uri.startswith("/CHK@") or uri.startswith("/SSK@")):
            return (" <br>[ILLEGAL IMAGE MACRO IN WIKITEXT: " +
                    " only CHK@ and SSK@ images allowed! ]<br> ")

        if not scrub_links:
            uri = "" # Images disabled when editing.
        alt_attrib = ' alt="[WIKITEXT IMAGE MACRO MISSING ALT TAG!]" '
        title_attrib = ""
        if len(fields) > 1 and len(fields[1].strip()) > 0:
            alt_attrib = ' alt="%s" ' % fields[1]
        if len(fields) > 2 and len(fields[2].strip()) > 0:
            title_attrib = ' title="%s" ' % fields[2]

        return ' <img src="%s"%s%s/> ' % (uri, alt_attrib, title_attrib)


    def _anch_repl(self, word):
        word = word[3:-3]

        if has_illegal_chars(word):
            return (" <br>[ILLEGAL ANCHOR IN WIKITEXT: " +
                    " illegal characters! ]<br> ")

        fields = word.strip().split('|')
        if len(fields) != 2:
            return (" <br>[ILLEGAL ANCHOR IN WIKITEXT: " +
                    " needs to be in the @@@text|label@@@ format! ]<br> ")

        return ('<a class="namedanchor" name="%s">%s</a>' %
                (fields[1], fields[0]))

    def _anchl_repl(self, word):
        word = word[4:-4]
        if has_illegal_chars(word):
            return (" <br>[ILLEGAL ANCHOR LINK IN WIKITEXT: " +
                    " illegal characters! ]<br> ")

        fields = word.strip().split('|')

        if len(fields) != 2:
            return (" <br>[ILLEGAL ANCHOR LINK IN WIKITEXT: " +
                    " needs to be in the @@@text|label@@@ format! ]<br> ")

        return '<a href="#%s">%s</a>' % (fields[1], fields[0])

    def _ent_repl(self, s):
        return {'&': '&',
                '<': '<',
                '>': '>'}[s]

    def _li_repl(self, match):
        return '<li>'


    def _pre_repl(self, word):
        if word == '{{{' and not self.in_pre:
            self.in_pre = 1
            return '<pre>'
        elif self.in_pre:
            self.in_pre = 0
            return '</pre>'
        else:
            return ''

    def _macro_repl(self, word):
        macro_name = word[2:-2]
        # TODO: Somehow get the default value into the search field
        return apply(globals()['_macro_' + macro_name], ())
        
    def _tablerow_repl(self, word):
        if word[0:2] == '||':
            # strip off the beginning and ending ||
            word = word[2:-2]
            rval = ''

            # start the table if we aren't inside one yet
            if self.in_table == 0:
                self.in_table = 1
                rval = rval + '<table'
                
                table_re = re.compile(
                r"(?:(?P<tableborder>\<tableborder:(?P<borderval>\d+(%|px)?)\>)"
                + r"|(?P<tablebordercolor>\<tablebordercolor:(?P<bordercolorval>#[0-9A-Fa-f]{6})\>)"
                + r"|(?P<tablewidth>\<tablewidth:(?P<widthval>\d+(%|px)?)\>)"
                + r"|(?P<tableheight>\<tableheight:(?P<heightval>\d+(%|px)?)\>)"
                + r"|(?P<collapse>\<collapse\>)"
                + r")")
                
                stylestr = ''
                
                for match in table_re.finditer(word):
                    stylestr = stylestr + self.table_replace(match)
                    
                # replace all matches with empty string
                word = re.sub(table_re,'',word)
                
                if stylestr != '':
                    rval = rval + ' style="' + stylestr + '"'
                
                rval = rval + '>'

            # start a new table row
            colspan = 1
            rowspan = 1
            rval = rval + '<tr>'
            for line in word.split('||'):
                if line == '':
                    colspan = colspan + 1
                else:
                    
                    span_re = re.compile(
                    r"(?:(?P<colspan>\<-(?P<csval>\d+)\>)"
                    + r"|(?P<rowspan>\<\|(?P<rsval>\d+)\>)"
                    + r")")
                    
                    for match in span_re.finditer(line):
                        for type, hit in match.groupdict().items():
                            if hit:
                                if type == 'colspan':
                                    colspan = colspan + int(match.group('csval')) - 1
                                elif type == 'rowspan':
                                    rowspan = rowspan + int(match.group('rsval')) - 1
                            
                    line = re.sub(span_re,'',line)
                
                    rval = rval + '<td'
                
                    if colspan > 1:
                        rval = rval + ' colspan="' + str(colspan) + '"'
                    if rowspan > 1:
                        rval = rval + ' rowspan="' + str(rowspan) + '"'
                        
                    td_re = re.compile(
                    r"(?:(?P<tdborder>\<border:(?P<borderval>\d+(%|px)?)\>)"
                    + r"|(?P<tdbordercolor>\<bordercolor:(?P<bordercolorval>#[0-9A-Fa-f]{6})\>)"
                    + r"|(?P<tdwidth>\<width:(?P<widthval>\d+(%|px)?)\>)"
                    + r"|(?P<tdheight>\<height:(?P<heightval>\d+(%|px)?)\>)"
                    + r"|(?P<tdbgcolor>\<bgcolor:(?P<bgcolorval>#[0-9A-Fa-f]{6})\>)"
                    + r"|(?P<tdalign>\<align:(?P<alignval>(left|center|right))\>)"
                    + r"|(?P<tdvalign>\<valign:(?P<valignval>(top|middle|bottom))\>)"
                    + r")")
                    
                    stylestr = ''
                    
                    for match in td_re.finditer(line):
                        stylestr = stylestr + self.table_replace(match)
                        
                    # replace all matches with empty string
                    line = re.sub(td_re,'',line)
                    
                    if stylestr != '':
                        rval = rval + ' style="' + stylestr + '"'
                        
                    rval = rval + '>'
                        
                    # recursive call to pageformatter to format any code within the table data
                    # is there a better way to do this??
                    rval = rval + PageFormatter(line).return_html() + '</td>'
                    colspan = 1
                    rowspan = 1

            # end the current table row - make sure to close final td if one is open
            if colspan > 1:
                rval = rval + '<td colspan="' + str(colspan-1) + '"></td>'
            
            rval = rval + '</tr>'
            return rval
        elif self.in_table == 1:
            self.in_table = 0
            return '</table>'
        else:
            return ''
            
    def table_replace(self, match):
        replaced = ''
        for type, hit in match.groupdict().items():
            if hit:
                if type == 'tableborder' or type == 'tdborder':
                    replaced = 'border-style:solid;border-width:'+match.group('borderval')+';'
                elif type == 'tablebordercolor' or type == 'tdbordercolor':
                    replaced = 'border-color:'+match.group('bordercolorval')+';'
                elif type == 'tablewidth' or type == 'tdwidth':
                    replaced = 'width:'+match.group('widthval')+';'
                elif type == 'tableheight' or type == 'tdheight':
                    replaced = 'height:'+match.group('heightval')+';'
                elif type == 'tdbgcolor':
                    replaced = 'background-color:'+match.group('bgcolorval')+';'
                elif type == 'tdalign':
                    replaced = 'text-align:'+match.group('alignval')+';'
                elif type == 'tdvalign':
                    replaced = 'vertical-align:'+match.group('valignval')+';'
                elif type == 'collapse':
                    replaced = 'border-collapse:collapse;'
        return replaced
            

    def _indent_level(self):
        return len(self.list_indents) and self.list_indents[-1]

    def _indent_to(self, new_level):
        s = ''
        while self._indent_level() > new_level:
            del(self.list_indents[-1])
            s = s + '</ul>\n'
        while self._indent_level() < new_level:
            self.list_indents.append(new_level)
            s = s + '<ul>\n'
        return s

    def _undent(self):
        res = '</ul>' * len(self.list_indents)
        self.list_indents = []
        return res

    def replace(self, match):
        for type, hit in match.groupdict().items():
            if hit:
                replaced = ''
                if self.in_table == 1 and type != 'tablerow':
                    replaced = self._tablerow_repl(hit)
                return replaced + apply(getattr(self, '_' + type + '_repl'), (hit,))
        else:
            raise "Can't handle match " + `match`
            
    def return_html(self):
        returnval = ''
        # For each line, we scan through looking for magic
        # strings, outputting verbatim any intervening text
        scan_re = re.compile(
            r"(?:(?P<emph>'{2,3})"
            + r"|(?P<ent>[<>&])"
            + r"|(?P<anch>@@@([^@]+)@@@)"
            + r"|(?P<anchl>@@@@([^@]+)@@@@)"
            + r"|(?P<word>\b(?:[A-Z][a-z]+){2,}\b)"
            + r"|(?P<rule>-{4,})"
            + r"|(?P<img>\[\[\[(freenet\:[^\]]+)\]\]\])"
            + r"|(?P<url>(freenet|http)\:[^\s'\"]+\S)"
            + r"|(?P<li>^\s+\*)"

            + r"|(?P<pre>(\{\{\{|\}\}\}))"
            + r"|(?P<macro>\[\[(TitleSearch|FullSearch|WordIndex"
                            + r"|TitleIndex|ActiveLink"
                            + r"|LocalChanges|RemoteChanges|BookMark|"
                            + r"FreesiteUri|GoTo)\]\])"
            + r"|(?P<tablerow>^\|\|.*\|\|$)"
            + r")")
        blank_re = re.compile("^\s*$")
        bullet_re = re.compile("^\s+\*")
        indent_re = re.compile("^\s*")
        eol_re = re.compile(r'\r?\n')
        raw = string.expandtabs(self.raw)
        for line in eol_re.split(raw):
            if not self.in_pre:
                # XXX: Should we check these conditions in this order?
                if blank_re.match(line):
                    if self.in_table:
                        returnval = returnval + self._tablerow_repl('')
                    returnval = returnval + '<p>\n'
                    continue
                indent = indent_re.match(line)
                returnval = returnval + self._indent_to(len(indent.group(0)))
            returnval = returnval + re.sub(scan_re, self.replace, line) + '\n'
        if self.in_pre: returnval = returnval + '</pre>\n'
        returnval = returnval + self._undent() + '\n'
        return returnval

    def print_html(self):
        print self.return_html()

# ----------------------------------------------------------
class Page:
    def __init__(self, page_name):
        self.page_name = page_name

    def wiki_name(self):
        return self.page_name.split('_')[0]

    def version(self):
        fields = self.page_name.split('_')
        if len(fields) < 2:
            return ''
        return fields[1]

    def split_title(self):
        # look for the end of words and the start of a new word,
        # and insert a space there
        fields = self.page_name.split('_')
        version = ""
        if len(fields) > 1:
            version = "(" + fields[1][:12] + ")"

        return re.sub('([a-z])([A-Z])', r'\1 \2', fields[0]) + version


    def _text_filename(self):
        return path.join(text_dir, self.page_name)


    def exists(self):
        return filefuncs.exists(self._text_filename())

    def link_to(self):
        word = self.page_name
        if self.exists():
            return link_tag(word)
        else:
            if nonexist_qm:
                return link_tag(word, '?', 'nonexistent') + word
            else:
                return link_tag(word, word, 'nonexistent')


    def get_raw_body(self, unmodified=False):
        try:
            return filefuncs.read(self._text_filename(), 'rb', unmodified)
        except IOError, er:
            if er.errno == errno.ENOENT:
                # just doesn't exist, use default
                return 'Describe %s here.' % self.page_name
            else:
                raise er

    def handled_versioned_page(self, msg=None, unmodified=False):
        if not is_versioned(self.page_name):
            return
        msg = None # Hmmmm...
        full_path = os.path.join(text_dir, self.page_name)
        removed = not filefuncs.exists(full_path, True)
        resolved = filefuncs.has_overlay(full_path)

        link = get_scriptname() + '?fullsearch=' + self.wiki_name()
        send_title(self.split_title(), link, msg, bool(self.version()))
        if unmodified:
            PageFormatter(self.get_raw_body(unmodified)).print_html()
        else:
            if removed:
                print "<b>Already resolved.</b>"
            elif resolved:
                print "<b>Locally marked resolved.</b>"
            else:
                PageFormatter(self.get_raw_body(unmodified)).print_html()

        self.send_footer(True,
                         self._last_modified(),
                         self._text_filename(), unmodified)

        return True

    def send_footer(self, versioned, mod_string=None, page_path=None,
                    unmodified=False):

        base = get_scriptname()
        print '<hr>'
        if is_read_only(data_dir, self.page_name):
            print "<em>The bot owner has marked this page read only.</em>"
            print (('<br><a href="%s?viewunmodifiedsource=%s">'  %
                    (base, self.page_name)) + '[View page source]</a><br>')
            return

        if unmodified:
            print ("<em>Read only original version " +
                   "of a locally modified page.</em>")
            print (('<br><a href="%s?viewunmodifiedsource=%s">'  %
                    (base, self.page_name)) + '[View page source]</a><br>')
            return

        if versioned:
            if page_path is None:
                # Hmmmm...
                return

            if filefuncs.has_overlay(page_path):
                print (('<br><a href="%s?unmodified=%s">' % (base,
                                                             self.page_name)) +
                       '[Show original version]</a><br>')
                print (('<a href="%s?deletelocal=%s">' % (base,
                                                          self.page_name)) +
                       '[Mark unresolved, without confirmation!]</a><br>')

            else:
                if filefuncs.exists(page_path, True):
                    print "<em>This is an unmerged fork of another page!</em>"
                    print (('<br><a href="%s?viewsource=%s">' %
                            (base, self.page_name)) +
                           '[View page source]</a><br>')
                    print (('<br><a href="%s?removepage=%s">' %
                            (base, self.page_name)) +
                           '[Locally mark resolved, ' +
                           'without confirmation!]</a><br>')

            print "<p><em>Wiki dir: %s </em>" % data_dir
            return

        if not page_path is None and filefuncs.has_overlay(page_path):
            print "<strong>This page has local edits!</strong><br>"

        if not page_path is None:
            name = os.path.split(page_path)[1]
            fork_table = get_unmerged_versions(filefuncs, text_dir,
                                               (name,))
            if len(fork_table[name]) > 0:
                print ("<strong>This page has forks: %s!</strong><br>"  %
                       get_fork_html(filefuncs, text_dir, name, fork_table))

        print link_tag('?edit=%s' %  name, 'EditText')
        print "of this page"
        if mod_string:
            print "(last modified %s)" % mod_string
        print '<br>'
        print link_tag('FindPage?value=%s' %  name, 'FindPage')
        print " by browsing, searching, or an index"

        if page_path is None:
            print "<p><em>Wiki dir: %s </em>" % data_dir
            return

        if filefuncs.has_overlay(page_path):
            print (('<br><a href="%s?unmodified=%s">' % (base, name)) +
                   '[Show original version]</a><br>')
            print (('<a href="%s?removepage=%s">' % (base, name)) +
                   '[Locally delete this page without confirmation!]</a><br>')
            print (('<a href="%s?deletelocal=%s">' % (base, name)) +
                   '[Undo local edits without confirmation!]</a><br>')

        print "<p><em>Wiki dir: %s </em>" % data_dir


    def send_page(self, msg=None, unmodified=False):
        if self.handled_versioned_page(msg, unmodified):
            return

        link = get_scriptname() + '?fullsearch=' + self.wiki_name()
        send_title(self.split_title(), link, msg, bool(self.version()))
        PageFormatter(self.get_raw_body(unmodified)).print_html()
        self.send_footer(False, self._last_modified(),
                         self._text_filename(), unmodified)

    def _last_modified(self):
        if not self.exists():
            return None
        modtime = localtime(filefuncs.modtime(self._text_filename()))
        return strftime(datetime_fmt, modtime)

    # hmmm... change function name?
    def send_editor(self, read_only=False, unmodified=False):
        title = 'Edit '
        read_only_value=''
        if read_only:
            title = "View Page Source: "
            read_only_value = 'readonly'

        send_title(title + self.split_title())
        # IMPORTANT: Ask browser to send us utf8
        print '<form method="post" action="%s" accept-charset="UTF-8">' % (get_scriptname())
        print '<input type=hidden name="savepage" value="%s">' % \
              (self.page_name)
        # Encode outgoing raw wikitext into utf8
        raw_body = string.replace(self.get_raw_body(unmodified),
                                  '\r\n', '\n')
        print """<textarea wrap="virtual" name="savetext" rows="17"
                 cols="120" %s >%s</textarea>""" % (
                 read_only_value, raw_body)
        if not read_only:
            print """<br><input type=submit value="Save">
                     <input type=reset value="Reset">
                  """
        print "<br>"
        print "</form>"
        if not read_only:
            print "<p>" + Page('EditingTips').link_to()

    def _write_file(self, text):
        filefuncs.write(self._text_filename(), text, 'wb')

    def save_text(self, newtext):
        self._write_file(newtext)
        remote_name = environ.get('REMOTE_ADDR', '')

# See set_data_dir_from_cfg(), reset_root_dir
data_dir = None
text_dir = None
cgi.logfile = None

def get_logo_string():
    # Returning '' is allowed
    return '<img src="%s" border=0 alt="pikipiki">' % scrub('/' + PIKI_PNG)

#changed_time_fmt = ' . . . . [%I:%M %p]'
changed_time_fmt = '[%I:%M %p]'
#date_fmt = '%a %d %b %Y'
date_fmt = '%a %d %b %Y UTC'
datetime_fmt = '%a %d %b %Y %I:%M %p'
show_hosts = 0                          # show hostnames?
css_url = '/' + PIKI_CSS         # stylesheet link, or ''
nonexist_qm = 0                         # show '?' for nonexistent?

def serve_one_page():

    emit_header()

    try:
        global form
        form = cgi.FieldStorage()

        handlers = { 'fullsearch':  do_fullsearch,
                     'titlesearch': do_titlesearch,
                     'edit':        do_edit,
                     'viewsource':  do_viewsource,
                     'viewunmodifiedsource':  do_viewunmodifiedsource,
                     'savepage':    do_savepage,
                     'unmodified':  do_unmodified,
                     'deletelocal': do_deletelocal,
                     'removepage':  do_removepage}

        for cmd in handlers.keys():
            if form.has_key(cmd):
                apply(handlers[cmd], (form[cmd].value.decode('utf8'),))
                break
        else:
            path_info = environ.get('PATH_INFO', '')

            if form.has_key('goto'):
                query = form['goto'].value.decode('utf8')
            elif len(path_info) and path_info[0] == '/':
                query = path_info[1:] or 'FrontPage'
            else:
                query = environ.get('QUERY_STRING', '') or 'FrontPage'

            #word_match = re.match(word_re_str, query)
            word_match = re.match(versioned_page_re_str, query)
            #sys.stderr.write("query: %s [%s]\n" % (repr(query),
            #                                       repr(word_match)))
            if word_match:
                word = word_match.group('wikiword')
                if not word_match.group('version') is None:
                    word = "%s_%s" % (word, word_match.group('version'))
                Page(word).send_page()
            else:
                print "<p>Can't work out query \"<pre>" + query + "</pre>\""

    except:
        cgi.print_exception()

    sys.stdout.flush()


############################################################
def is_versioned(page_name):
    match = versioned_page_re.match(page_name)
    if match is None:
        return False
    return bool(match.group('version'))

############################################################
# See wikibot.py, update_change_log

# Hmmmm... too much code.
# Returns WikiName->(version, ...) table
def make_fork_list(versioned_names):
    if len(versioned_names) == 0:
        return ()

    table = {}
    for name in versioned_names:
        result = versioned_page_re.match(name)
        assert not result is None
        wiki_name = result.group('wikiword')
        assert not wiki_name is None
        version = result.group('version')
        assert not version is None
        entry = table.get(wiki_name, [])
        entry.append(version)
        table[wiki_name] = entry

    for value in table.values():
        value.sort()

    return table

def is_read_only(base_dir, page_name):
    full_path = os.path.join(base_dir, 'readonly.txt')
    if not os.path.exists(full_path):
        return False
    in_file = open(full_path, 'rb')
    try:
        return page_name in [value.strip()
                             for value in in_file.read().splitlines()]
    finally:
        in_file.close()

# REDFLAG: Revisit.
# Config file is in the directory above the data_dir directory,
# so I don't want to depend on that while running.
# Used info.txt file from the head end instead.
def read_info():
    full_path = os.path.join(data_dir, 'info.txt')
    err_msg = None
    try:
        in_file = codecs.open(full_path, 'rb', 'ascii')
        try:
            usk, desc, link_name = in_file.read().splitlines()[:3]
        finally:
            in_file.close()
    except UnicodeError: # ISA ValueError. Order important!
        raise ValueError("Illegal encoding in info.txt")
    except ValueError:
        raise ValueError("Couldn't parse data from info.txt")
    except IOError:
        raise ValueError("Couldn't read data from info.txt")

    if (has_illegal_chars(usk) or has_illegal_chars(desc) or
        has_illegal_chars(link_name)):
        raise ValueError("Illegal html characters in info.txt")

    return usk, desc, link_name


def read_log_file_entries(base_dir, max_entries):
    accepted = []
    full_path = os.path.join(base_dir, 'accepted.txt')
    if os.path.exists(full_path):
        in_file = open(full_path, 'rb')
        try:
            changes = {}
            # LATER: find/write reverse line iterator?
            for line in reversed(in_file.readlines()):
                if len(accepted) >= max_entries:
                    break
                fields = line.split(':')
                if fields[0] in ('C', 'M', 'R', 'F'):
                    for index in range(1, len(fields)):
                        fields[index] = fields[index].strip()
                    changes[fields[0]] = fields[1:]
                else:
                    fields = fields[:4]
                    fields.append((changes.get('C', ()),
                                   changes.get('M', ()),
                                   changes.get('R', ()),
                                   make_fork_list(changes.get('F', ()))))
                    accepted.append(tuple(fields))
                    changes = {}
        finally:
            in_file.close()

    rejected = []
    full_path = os.path.join(base_dir, 'rejected.txt')
    if os.path.exists(full_path):
        in_file = open(full_path, 'rb')
        try:
            changes = {}
            # LATER: find/write reverse line iterator?
            for line in reversed(in_file.readlines()):
                if len(rejected) >= max_entries:
                    break
                rejected.append(tuple(line.split(':')[:5]))
        finally:
            in_file.close()

    return tuple(accepted), tuple(rejected)


class FreenetPage(Page):
    def __init__(self, page_name):
        Page.__init__(self, page_name)


    def send_footer(self, versioned, dummy_mod_string=None,
                    page_path=None,
                    dummy_unmodified=False):
        print "<hr>"
        print "%s %s %s" % (link_tag('FrontPage', 'FrontPage'),
                            link_tag('TitleIndex', 'TitleIndex'),
                            link_tag('WordIndex', 'WordIndex'))
        if not page_path is None and not versioned:
            name = os.path.split(page_path)[1]
            fork_table = get_unmerged_versions(filefuncs, text_dir,
                                               (name,))
            if len(fork_table[name]) > 0:
                print (("<hr><strong>This page has forks: %s! " %
                        get_fork_html(filefuncs, text_dir, name, fork_table))
                       +
                       "Please consider merging them.</strong><br>")

def reset_root_dir(root_dir, overlayed=False):
    global data_dir, text_dir, filefuncs
    if not os.path.exists(root_dir) or not os.path.isdir(root_dir):
        raise IOError("Base wiki dir doesn't exist: %s" % root_dir)

    data_dir = root_dir
    text_dir = path.join(root_dir, 'wikitext')
    if not os.path.exists(text_dir) or not os.path.isdir(text_dir):
        raise IOError("Wikitext dir doesn't exist: %s" % text_dir)

    cgi.logfile = path.join(data_dir, 'cgi_log')
    filefuncs = fileoverlay.get_file_funcs(root_dir, overlayed)
    if overlayed:
        # Only overlay 'wikitext', not 'www'
        full_path = filefuncs.overlay_path(text_dir)
        if not os.path.exists(full_path):
            os.makedirs(full_path)

CFG_FILE = 'fnwiki.cfg'
WIKIROOT = 'wiki_root'
# REDFLAG: Hacks to make this work in windows binary mercurial distro?
from ConfigParser import ConfigParser
def set_data_dir_from_cfg(base_path=None):
    if base_path is None:
        # REDFLAG: test on windoze.
        base_path = os.getcwd()
    cfg_file = os.path.join(base_path, CFG_FILE)
    parser = ConfigParser()
    parser.read(cfg_file)
    if not parser.has_section('default'):
        raise IOError("Can't read default section from config file: %s." % cfg_file)

    if parser.has_option('default','wiki_root'):
        root_dir = os.path.join(base_path, parser.get('default', 'wiki_root'))
    else:
        root_dir = os.path.join(base_path, WIKIROOT)

    overlayed = True
    if parser.has_option('default','overlayedits'):
        overlayed = parser.getboolean('default','overlayedits')

    reset_root_dir(root_dir, overlayed)

import shutil
def create_default_wiki(base_path):
    if os.path.exists(base_path):
        raise IOError("The directory already exists.")

    shutil.copytree(os.path.join(os.path.dirname(__file__),
                                 'default_files'),
                    base_path)

def dump(output_dir, wiki_root, overlayed=False):
    global form, scrub_links

    form = {}
    scrub_links = True
    reset_root_dir(wiki_root, overlayed)

    old_out = sys.stdout
    try:
        pages = list(page_list(True))
        for name in pages:
            file_name = os.path.join(output_dir, name)
            out = codecs.open(file_name, "wb", 'utf8') # Write utf8
            try:
                page = FreenetPage(name)
                sys.stdout = out
                print '<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">'
                page.send_page()
                sys.stdout.flush()
                out.close()
                sys.stdout = old_out
            finally:
                out.close()
                sys.stdout = old_out
    finally:
        sys.stdout = old_out

    if not os.path.exists(os.path.join(data_dir, 'NotWrittenYet')):
        out = open(os.path.join(output_dir, 'NotWrittenYet'), 'wb')
        out.write("That page doesn't exist in the wiki yet!\n")
        out.close()

    if not os.path.exists(os.path.join(data_dir, 'AlreadyResolved')):
        out = open(os.path.join(output_dir, 'AlreadyResolved'), 'wb')
        out.write("That fork was already resolved.\n")
        out.close()

    # .css, .png
    www_dir = os.path.join(data_dir, 'www')
    for name in PIKI_REQUIRED_FILES:
        shutil.copyfile(os.path.join(www_dir, name),
                        os.path.join(output_dir, name))

# "builtin" when execfile()'d by servepiki.py
if __name__ == "__main__" or __name__ == "__builtin__":

    if not STDERR_FILE is None:
        sys.stderr = open(STDERR_FILE, 'ab')
    else:
        # Disable stderr. hmmm...
        sys.stderr = StringIO()

    # Redirect "print" output into a StringIO so
    # we can encode the html as UTF-8.
    real_out = sys.stdout
    buf = StringIO()
    sys.stdout = buf
    try:
        set_data_dir_from_cfg()
        serve_one_page()
    finally:
        sys.stdout = real_out
        print buf.getvalue().encode('utf8')