infocalypse

(djk)
2009-11-11: Small wiki based on piki. See: attribution.txt

Small wiki based on piki. See: attribution.txt

diff --git a/fniki/SimpleAsyncServer.py b/fniki/SimpleAsyncServer.py
new file mode 100644
--- /dev/null
+++ b/fniki/SimpleAsyncServer.py
@@ -0,0 +1,177 @@
+# ATTRIBUTION: Pierre Quentel
+# http://code.activestate.com/recipes/511453/
+# LICENSE: MIT
+# http://code.activestate.com/help/terms/
+# http://www.opensource.org/licenses/mit-license.php
+
+"""A generic, multi-protocol asynchronous server
+
+Usage :
+- create a server on a specific host and port : server = Server(host,port)
+- call the loop() function, passing it the server and the class used to 
+manage the protocol (a subclass of ClientHandler) : loop(server,ProtocolClass)
+
+An example of protocol class is provided, LengthSepBody : the client sends
+the message length, the line feed character and the message body
+"""
+
+import cStringIO
+import socket
+import select
+
+# the dictionary holding one client handler for each connected client
+# key = client socket, value = instance of (a subclass of) ClientHandler
+client_handlers = {}
+
+# =======================================================================
+# The server class. Creating an instance starts a server on the specified
+# host and port
+# =======================================================================
+class Server:
+
+    def __init__(self,host='localhost',port=80):
+        self.host,self.port = host,port
+        self.socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
+        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self.socket.setblocking(0)
+        self.socket.bind((host,port))
+        self.socket.listen(5)
+
+# =====================================================================
+# Generic client handler. An instance of this class is created for each
+# request sent by a client to the server
+# =====================================================================
+class ClientHandler:
+
+    blocksize = 2048
+
+    def __init__(self, server, client_socket, client_address):
+        self.server = server
+        self.client_address = client_address
+        self.client_socket = client_socket
+        self.client_socket.setblocking(0)
+        self.host = socket.getfqdn(client_address[0])
+        self.incoming = '' # receives incoming data
+        self.writable = False
+        self.close_when_done = True
+ 
+    def handle_error(self):
+        self.close()
+        
+    def handle_read(self):
+        """Reads the data received"""
+        try:
+            buff = self.client_socket.recv(1024)
+            if not buff:  # the connection is closed
+                self.close()
+            # buffer the data in self.incoming
+            self.incoming += buff #.write(buff)
+            self.process_incoming()
+        except socket.error:
+            self.close()
+
+    def process_incoming(self):
+        """Test if request is complete ; if so, build the response
+        and set self.writable to True"""
+        if not self.request_complete():
+            return
+        self.response = self.make_response()
+        self.writable = True
+
+    def request_complete(self):
+        """Return True if the request is complete, False otherwise
+        Override this method in subclasses"""
+        return True
+    
+    def make_response(self):
+        """Return the list of strings or file objects whose content will
+        be sent to the client
+        Override this method in subclasses"""
+        return ["xxx"]
+
+    def handle_write(self):
+        """Send (a part of) the response on the socket
+        Finish the request if the whole response has been sent
+        self.response is a list of strings or file objects
+        """
+        # get next piece of data from self.response
+        buff = ''
+        while self.response and not buff:
+            if isinstance(self.response[0],str):
+                buff = self.response.pop(0)
+            else:
+                buff = self.response[0].read(self.blocksize)
+                if not buff:
+                    self.response.pop(0)
+        if buff:
+            try:
+                self.client_socket.sendall(buff)
+            except socket.error:
+                self.close()
+            if self.response:
+                return
+        # nothing left in self.response
+        if self.close_when_done:
+            self.close() # close socket
+        else:
+            # reset for next request
+            self.writable = False
+            self.incoming = ''
+    
+    def close(self):
+        del client_handlers[self.client_socket]
+        self.client_socket.close()
+
+# ==============================================================
+# A protocol with message length + line feed (\n) + message body
+# This implementation just echoes the message body
+# ==============================================================
+class LengthSepBody(ClientHandler):
+
+    def request_complete(self):
+        """The request is complete if the separator is present and the
+        number of bytes received equals the specified message length"""
+        recv = self.incoming.split('\n',1)
+        if len(recv)==1 or len(recv[1]) != int(recv[0]):
+            return False
+        self.msg_body = recv[1]
+        return True
+
+    def make_response(self):
+        """Override this method to actually process the data"""
+        return [self.msg_body]
+
+# ============================================================================
+# Main loop, calling the select() function on the sockets to see if new 
+# clients are trying to connect, if some clients have sent data and if those
+# for which the response is complete are ready to receive it
+# For each event, call the appropriate method of the server or of the instance
+# of ClientHandler managing the dialog with the client : handle_read() or 
+# handle_write()
+# ============================================================================
+def loop(server,handler,timeout=30):
+    while True:
+        k = client_handlers.keys()
+        # w = sockets to which there is something to send
+        # we must test if we can send data
+        w = [ cl for cl in client_handlers if client_handlers[cl].writable ]
+        # the heart of the program ! "r" will have the sockets that have sent
+        # data, and the server socket if a new client has tried to connect
+        r,w,e = select.select(k+[server.socket],w,k,timeout)
+        for e_socket in e:
+            client_handlers[e_socket].handle_error()
+        for r_socket in r:
+            if r_socket is server.socket:
+                # server socket readable means a new connection request
+                try:
+                    client_socket,client_address = server.socket.accept()
+                    client_handlers[client_socket] = handler(server,
+                        client_socket,client_address)
+                except socket.error:
+                    pass
+            else:
+                # the client connected on r_socket has sent something
+                client_handlers[r_socket].handle_read()
+        w = set(w) & set(client_handlers.keys()) # remove deleted sockets
+        for w_socket in w:
+            client_handlers[w_socket].handle_write()
diff --git a/fniki/attribution.txt b/fniki/attribution.txt
new file mode 100644
--- /dev/null
+++ b/fniki/attribution.txt
@@ -0,0 +1,28 @@
+djk20091111
+
+This directory contains a simple stand-alone wiki implementation 
+derived from piki and a small python CGI webserver.
+
+Thanks to Martin Pool and Pierre Quentel for making their code 
+available.
+
+---
+pipipiki:
+Attribution: Martin Pool
+http://sourcefrog.net/projects/piki/
+
+I started hacking from:
+http://sourcefrog.net/projects/piki/piki-1.62.zip
+sha1:e71779f4f8fea7dc851ffb1e74f18c613a94b086  /home/dkarbott/piki-1.62.zip
+
+LICENSE: GPL2
+---
+CGI webserver:
+Attribution: Pierre Quentel
+http://code.activestate.com/recipes/511454/
+http://code.activestate.com/recipes/511453/
+
+LICENSE: MIT
+http://code.activestate.com/help/terms/
+http://www.opensource.org/licenses/mit-license.php
+
diff --git a/fniki/piki.css b/fniki/piki.css
new file mode 100644
--- /dev/null
+++ b/fniki/piki.css
@@ -0,0 +1,3 @@
+BODY { background-color: #FFFFFF; color: #000000 }
+A { color: #1f6b9e }
+A.nonexistent { background-color: #CCCCCC }
\ No newline at end of file
diff --git a/fniki/piki.py b/fniki/piki.py
new file mode 100644
--- /dev/null
+++ b/fniki/piki.py
@@ -0,0 +1,752 @@
+#! /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
+
+__version__ = '$Revision: 1.62 $'[11:-2];
+
+
+import cgi, sys, string, os, re, errno, time, stat
+from cgi import log
+from os import path, environ
+from socket import gethostbyaddr
+from time import localtime, strftime
+from cStringIO import StringIO
+
+PIKI_PNG = 'pikipiki-logo.png'
+PIKI_CSS = 'piki.css'
+
+# HTTP server doesn't need to serve any other files to make piki work.
+PIKI_REQUIRED_FILES = (PIKI_PNG, PIKI_CSS)
+
+scrub_links = False
+def scrub(link_text, ss_class=None):
+    """ Cleanup href values so the work with Freenet. """
+    if not scrub_links:
+        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"
+    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 + '$')
+command_re_str = "(search|edit|fullsearch|titlesearch)\=(.*)"
+
+# Editlog -----------------------------------------------------------
+
+# Functions to keep track of when people have changed pages, so we can
+# do the recent changes page and so on.
+# The editlog is stored with one record per line, as tab-separated
+# words: page_name, host, time
+
+# TODO: Check values written in are reasonable
+
+def editlog_add(page_name, host):
+    editlog = open(editlog_name, 'a+')
+    try: 
+        # fcntl.flock(editlog.fileno(), fcntl.LOCK_EX)
+        editlog.seek(0, 2)                  # to end
+        editlog.write(string.join((page_name, host, `time.time()`), "\t") + "\n")
+    finally:
+        # fcntl.flock(editlog.fileno(), fcntl.LOCK_UN)
+        editlog.close()
+
+
+def editlog_raw_lines():
+    editlog = open(editlog_name, 'rt')
+    try:
+        # fcntl.flock(editlog.fileno(), fcntl.LOCK_SH)
+        return editlog.readlines()
+    finally:
+        # fcntl.flock(editlog.fileno(), fcntl.LOCK_UN)
+        editlog.close()
+
+
+    
+
+
+# Formatting stuff --------------------------------------------------
+
+
+def get_scriptname():
+    return environ.get('SCRIPT_NAME', '')
+
+
+def send_title(text, link=None, msg=None):
+    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('RecentChanges', get_logo_string())
+    if link:
+        print '<a href="%s">%s</a>' % (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_savepage(pagename):
+    global form
+    pg = Page(pagename)
+    pg.save_text(form['savetext'].value)
+    msg = """<b>Thankyou for your changes.  Your attention to
+    detail is appreciated.</b>"""
+    
+    pg.send_page(msg=msg)
+
+
+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():
+    return filter(word_anchored_re.match, os.listdir(text_dir))
+
+
+def print_footer(name, editable=1, mod_string=None):
+    base = get_scriptname()
+    print '<hr>'
+    if editable:
+        print link_tag('?edit='+name, 'EditText')
+        print "of this page"
+        if mod_string:
+            print "(last modified %s)" % mod_string
+        print '<br>'
+    print link_tag('FindPage?value='+name, 'FindPage')
+    print " by browsing, searching, or an index"
+
+
+# ----------------------------------------------------------
+# 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
+    else:
+        default = ''
+    return """<form method=get>
+    <input name=%s size=30 value="%s"> 
+    <input type=submit value="Go">
+    </form>""" % (type, default)
+
+def _macro_GoTo():
+    return """<form method=get><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]
+        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_RecentChanges():
+    lines = editlog_raw_lines()
+    lines.reverse()
+    
+    ratchet_day = None
+    done_words = {}
+    buf = StringIO()
+    for line in lines:
+        page_name, addr, ed_time = string.split(line, '\t')
+        # year, month, day, DoW
+        time_tuple = localtime(float(ed_time))
+        day = tuple(time_tuple[0:3])
+        if day <> ratchet_day:
+            buf.write('<h3>%s</h3>' % strftime(date_fmt, time_tuple))
+            ratchet_day = day
+
+        if done_words.has_key(page_name):
+            continue
+
+        done_words[page_name] = 1
+        buf.write(Page(page_name).link_to())
+        if show_hosts:
+            buf.write(' . . . . ')
+            try:
+                buf.write(gethostbyaddr(addr)[0])
+            except:
+                buf.write("(unknown)")
+        if changed_time_fmt:
+            buf.write(time.strftime(changed_time_fmt, time_tuple))
+        buf.write('<br>')
+
+    return buf.getvalue()
+
+
+
+# ----------------------------------------------------------
+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
+
+
+    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):
+        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('|')
+        uri = fields[0]
+        alt_attrib = ""
+        title_attrib = ""
+        if len(fields) > 1:
+            alt_attrib = ' alt="%s" ' % fields[1]
+        if len(fields) > 2:
+            title_attrib = ' title="%s" ' % fields[2]
+
+        return ' <img src="%s"%s%s/> ' % (scrub(uri), alt_attrib, title_attrib)
+
+    def _email_repl(self, word):
+        return '<a href="mailto:%s">%s</a>' % (scrub(word), word)
+
+
+    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 _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:
+                return apply(getattr(self, '_' + type + '_repl'), (hit,))
+        else:
+            raise "Can't handle match " + `match`
+        
+
+    def print_html(self):
+        # 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<word>\b(?:[A-Z][a-z]+){2,}\b)"
+            + r"|(?P<rule>-{4,})"
+            + r"|(?P<img>\[\[\[(freenet\:[^\]]+)\]\]\])"
+            + r"|(?P<url>(freenet|http|ftp|nntp|news|mailto)\:[^\s'\"]+\S)"
+            #+ r"|(?P<email>[-\w._+]+\@[\w.-]+)"
+            + r"|(?P<li>^\s+\*)"
+
+            + r"|(?P<pre>(\{\{\{|\}\}\}))"
+            + r"|(?P<macro>\[\[(TitleSearch|FullSearch|WordIndex"
+                            + r"|TitleIndex|RecentChanges|GoTo)\]\])"
+            + 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):
+                    print '<p>'
+                    continue
+                indent = indent_re.match(line)
+                print self._indent_to(len(indent.group(0)))
+            print re.sub(scan_re, self.replace, line)
+        if self.in_pre: print '</pre>'
+        print self._undent()
+        
+
+# ----------------------------------------------------------
+class Page:
+    def __init__(self, page_name):
+        self.page_name = page_name
+
+    def split_title(self):
+        # look for the end of words and the start of a new word,
+        # and insert a space there
+        return re.sub('([a-z])([A-Z])', r'\1 \2', self.page_name)
+
+
+    def _text_filename(self):
+        return path.join(text_dir, self.page_name)
+
+
+    def _tmp_filename(self):
+        return path.join(text_dir, ('#' + self.page_name + '.' + `os.getpid()` + '#'))
+
+
+    def exists(self):
+        try:
+            os.stat(self._text_filename())
+            return 1
+        except OSError, er:
+            if er.errno == errno.ENOENT:
+                return 0
+            else:
+                raise er
+        
+
+    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):
+        try:
+            return open(self._text_filename(), 'rt').read()
+        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 send_page(self, msg=None):
+        link = get_scriptname() + '?fullsearch=' + self.page_name
+        send_title(self.split_title(), link, msg)
+        PageFormatter(self.get_raw_body()).print_html()
+        print_footer(self.page_name, 1, self._last_modified())
+
+
+    def _last_modified(self):
+        if not self.exists():
+            return None
+        modtime = localtime(os.stat(self._text_filename())[stat.ST_MTIME])
+        return strftime(datetime_fmt, modtime)
+
+
+    def send_editor(self):
+        send_title('Edit ' + self.split_title())
+        print '<form method="post" action="%s">' % (get_scriptname())
+        print '<input type=hidden name="savepage" value="%s">' % (self.page_name)
+        raw_body = string.replace(self.get_raw_body(), '\r\n', '\n')
+        print """<textarea wrap="virtual" name="savetext" rows="17"
+                 cols="120">%s</textarea>""" % raw_body
+        print """<br><input type=submit value="Save">
+                 <input type=reset value="Reset">
+                 """
+        print "<br>"
+        print Page("UploadFile").link_to()
+        print "<input type=file name=imagefile>"
+        print "(not enabled yet)"
+        print "</form>"
+        print "<p>" + Page('EditingTips').link_to()
+                 
+
+    def _write_file(self, text):
+        tmp_filename = self._tmp_filename()
+        open(tmp_filename, 'wt').write(text)
+        text = self._text_filename()
+        if os.name == 'nt':
+            # Bad Bill!  POSIX rename ought to replace. :-(
+            try:
+                os.remove(text)
+            except OSError, er:
+                if er.errno <> errno.ENOENT: raise er
+        os.rename(tmp_filename, text)
+
+
+    def save_text(self, newtext):
+        self._write_file(newtext)
+        remote_name = environ.get('REMOTE_ADDR', '')
+        editlog_add(self.page_name, remote_name)
+
+# See set_data_dir_from_cfg(), reset_root_cfg
+data_dir = None
+text_dir = None
+editlog_name = 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]'
+date_fmt = '%a %d %b %Y'
+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,
+                     'savepage':    do_savepage }
+
+        for cmd in handlers.keys():
+            if form.has_key(cmd):
+                apply(handlers[cmd], (form[cmd].value,))
+                break
+        else:
+            path_info = environ.get('PATH_INFO', '')
+
+            if form.has_key('goto'):
+                query = form['goto'].value
+            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)
+            if word_match:
+                word = word_match.group(0)
+                Page(word).send_page()
+            else:
+                print "<p>Can't work out query \"<pre>" + query + "</pre>\""
+
+    except:
+        cgi.print_exception()
+
+    sys.stdout.flush()
+
+############################################################
+# Gross, but at least it keeps the hacks in one place.
+class NoFooterPage(Page):
+    def __init__(self, page_name):
+        Page.__init__(self, page_name)
+
+    def send_page(self, msg=None):
+        link = get_scriptname() + '?fullsearch=' + self.page_name
+        send_title(self.split_title(), link, msg)
+        PageFormatter(self.get_raw_body()).print_html()
+        #print_footer(self.page_name, 1, self._last_modified())
+
+        print '<hr>'
+
+
+def reset_root_dir(root_dir):
+    global data_dir, text_dir, editlog_name
+    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)
+
+    editlog_name = path.join(data_dir, 'editlog')
+    cgi.logfile = path.join(data_dir, 'cgi_log')
+
+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)
+
+    reset_root_dir(root_dir)
+
+import shutil
+def create_empty_wiki(base_path, www_src):
+    if os.path.exists(base_path):
+        raise IOError("The directory already exists.")
+    os.makedirs(base_path)
+    new_text = os.path.join(base_path, 'wikitext')
+    new_www = os.path.join(base_path, 'www')
+    os.makedirs(new_text)
+    os.makedirs(new_www)
+    out = open(os.path.join(new_text, 'FrontPage'), 'wt')
+    out.write("Empty wiki.\nStart editing :-)\n")
+    out.close()
+    if www_src is None:
+        return
+    for name in PIKI_REQUIRED_FILES:
+        shutil.copyfile(os.path.join(www_src, name),
+                        os.path.join(new_www, name))
+
+def dump(output_dir, wiki_root):
+    global form, scrub_links
+    form = {}
+    scrub_links = True
+
+    reset_root_dir(wiki_root)
+
+    old_out = sys.stdout
+    try:
+        pages = list(page_list())
+        for name in pages:
+            file_name = os.path.join(output_dir, name)
+            out = open(file_name, "wb")
+            try:
+                page = NoFooterPage(name)
+                sys.stdout = out
+                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()
+
+    # .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__":
+    sys.stderr = open('/tmp/mbp_piki_err', 'at') # REDFLAG: FIX
+    set_data_dir_from_cfg()
+    serve_one_page()
+
diff --git a/fniki/pikipiki-logo.png b/fniki/pikipiki-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..6e28b57014b8a74a979202453db71761dbd5fb3d
GIT binary patch
literal 4582
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
QPyhe`07*qoM6N<$f?Yw#P5=M^

diff --git a/fniki/servepiki.py b/fniki/servepiki.py
new file mode 100755
--- /dev/null
+++ b/fniki/servepiki.py
@@ -0,0 +1,297 @@
+#! /usr/bin/env python
+
+# ATTRIBUTION: Pierre Quentel
+# http://code.activestate.com/recipes/511454/
+# LICENSE: MIT
+# http://code.activestate.com/help/terms/
+# http://www.opensource.org/licenses/mit-license.php
+#
+# Modifications: Copyright (C) 2009 Darrell Karbott
+
+# djk20091109 -- I modified this file to run piki from the local dir
+#                and do nothing else.
+#                DONT TRY TO USE IT AS A GENERIC SERVER.
+
+import SimpleAsyncServer
+
+# =============================================================
+# An implementation of the HTTP protocol, supporting persistent
+# connections and CGI
+# =============================================================
+import sys
+import os
+import traceback
+import datetime
+import mimetypes
+import urlparse
+import urllib
+import cStringIO
+import re
+
+import piki
+
+# Absolute path to the cgi python script.
+SCRIPT_PATH = piki.__file__
+if SCRIPT_PATH[-3:] == 'pyc':
+    # We need the python source, *NOT* the compiled code.
+    SCRIPT_PATH = SCRIPT_PATH[:-1]
+
+# Name *without* any '/' chars
+SCRIPT_NAME = 'piki'
+
+SCRIPT_REGEX = re.compile(r'/%s($|[\?/])' % SCRIPT_NAME)
+
+class HTTP(SimpleAsyncServer.ClientHandler):
+    # parameters to override if necessary
+    root = os.getcwd()  # the directory to serve files from
+
+    # djk20091109 HACK. *only* runs piki script from this directory.
+    # Don't need cgi_directories.
+    # cgi_directories = ['/cgi-bin']  # subdirectories for cgi scripts
+
+    script_name = None
+    script_path = None
+    script_regex = None
+
+    logging = True      # print logging info for each request ?
+    blocksize = 2 << 16 # size of blocks to read from files and send
+
+    def request_complete(self):
+        """In the HTTP protocol, a request is complete if the "end of headers"
+        sequence ('\r\n\r\n') has been received
+        If the request is POST, stores the request body in a StringIO before
+        returning True"""
+        terminator = self.incoming.find('\r\n\r\n')
+        if terminator == -1:
+            return False
+        lines = self.incoming[:terminator].split('\r\n')
+        self.requestline = lines[0]
+        try:
+            self.method,self.url,self.protocol = lines[0].strip().split()
+        except:
+            self.method = None # indicates bad request
+            return True
+        # put request headers in a dictionary
+        self.headers = {}
+        for line in lines[1:]:
+            k,v = line.split(':',1)
+            self.headers[k.lower().strip()] = v.strip()
+        # persistent connection
+        close_conn = self.headers.get("connection","")
+        if (self.protocol == "HTTP/1.1"
+            and close_conn.lower() == "keep-alive"):
+            self.close_when_done = False
+        # parse the url
+        scheme,netloc,path,params,query,fragment = urlparse.urlparse(self.url)
+        self.path,self.rest = path,(params,query,fragment)
+
+        if self.method == 'POST':
+            # for POST requests, read the request body
+            # its length must be specified in the content-length header
+            content_length = int(self.headers.get('content-length',0))
+            body = self.incoming[terminator+4:]
+            # request is incomplete if not all message body received
+            if len(body)<content_length:
+                return False
+            f_body = cStringIO.StringIO(body)
+            f_body.seek(0)
+            sys.stdin = f_body # compatibility with CGI
+
+        return True
+
+    def make_response(self):
+        """Build the response : a list of strings or files"""
+        if self.method is None: # bad request
+            return self.err_resp(400,'Bad request : %s' %self.requestline)
+        resp_headers, resp_body, resp_file = '','',None
+        if not self.method in ['GET','POST','HEAD']:
+            return self.err_resp(501,'Unsupported method (%s)' %self.method)
+        else:
+            file_name = self.file_name = self.translate_path()
+            # djk20091109 Keep trailing PATH_INFO for script from tripping 404.
+            if ((not self.managed()) and
+                (not os.path.exists(file_name) or not os.path.isfile(file_name))):
+                if self.path.strip() == '/':
+                    # Redirect instead of 404ing for no path.
+                    return self.redirect_resp('/%s/', 'Redirecting to % cgi.' %
+                                              (HTTP.script_name, HTTP.script_name))
+                return self.err_resp(404,'File not found')
+            elif self.managed():
+                response = self.mngt_method()
+            else:
+                ext = os.path.splitext(file_name)[1]
+                c_type = mimetypes.types_map.get(ext,'text/plain')
+                resp_line = "%s 200 Ok\r\n" %self.protocol
+                size = os.stat(file_name).st_size
+                resp_headers = "Content-Type: %s\r\n" %c_type
+                resp_headers += "Content-Length: %s\r\n" %size
+                resp_headers += '\r\n'
+                if self.method == "HEAD":
+                    resp_string = resp_line + resp_headers
+                elif size > HTTP.blocksize:
+                    resp_string = resp_line + resp_headers
+                    resp_file = open(file_name,'rb')
+                else:
+                    resp_string = resp_line + resp_headers + \
+                        open(file_name,'rb').read()
+                response = [resp_string]
+                if resp_file:
+                    response.append(resp_file)
+        self.log(200)
+        return response
+
+    def translate_path(self):
+        """Translate URL path into a path in the file system"""
+        return os.path.join(HTTP.root,*self.path.split('/'))
+
+    def managed(self):
+        """Test if the request can be processed by a specific method
+        If so, set self.mngt_method to the method used
+        This implementation tests if the script is in a cgi directory"""
+        if self.is_cgi():
+            self.mngt_method = self.run_cgi
+            return True
+        return False
+
+    # djk20091109 HACKED to run only piki.
+    def is_cgi(self):
+        """Only run the piki cgi."""
+        return bool(HTTP.script_regex.match(self.path.strip()))
+
+    def run_cgi(self):
+        # set CGI environment variables
+        self.make_cgi_env()
+        # redirect print statements to a cStringIO
+
+        save_stdout = sys.stdout
+        sys.stdout = cStringIO.StringIO()
+        # run the script
+        try:
+            # djk20091109 There was a bug here. You need the {} in order to run
+            # global functions.
+            #
+            #execfile(self.file_name)
+
+            # djk20091109 HACKED to run only piki script.
+            execfile(HTTP.script_path, {})
+        except:
+            sys.stdout = cStringIO.StringIO()
+            sys.stdout.write("Content-type:text/plain\r\n\r\n")
+            traceback.print_exc(file=sys.stdout)
+
+        response = sys.stdout.getvalue()
+        if self.method == "HEAD":
+            # for HEAD request, don't send message body even if the script
+            # returns one (RFC 3875)
+            head_lines = []
+            for line in response.split('\n'):
+                if not line:
+                    break
+                head_lines.append(line)
+            response = '\n'.join(head_lines)
+        sys.stdout = save_stdout # restore sys.stdout
+        # close connection in case there is no content-length header
+        self.close_when_done = True
+        resp_line = "%s 200 Ok\r\n" %self.protocol
+        return [resp_line + response]
+
+    def make_cgi_env(self):
+        """Set CGI environment variables"""
+        env = {}
+        env['SERVER_SOFTWARE'] = "AsyncServer"
+        env['SERVER_NAME'] = "AsyncServer"
+        env['GATEWAY_INTERFACE'] = 'CGI/1.1'
+        env['DOCUMENT_ROOT'] = HTTP.root
+        env['SERVER_PROTOCOL'] = "HTTP/1.1"
+        env['SERVER_PORT'] = str(self.server.port)
+
+        env['REQUEST_METHOD'] = self.method
+        env['REQUEST_URI'] = self.url
+        env['PATH_TRANSLATED'] = self.translate_path()
+
+        #env['SCRIPT_NAME'] = self.path
+        # djk20091109 HACK
+        env['SCRIPT_NAME'] = '/' + HTTP.script_name
+        # djk20091109 BUG? I think this was just wrong.
+        #env['PATH_INFO'] = urlparse.urlunparse(("","","",self.rest[0],"",""))
+        env['PATH_INFO'] = self.path[len("/" + HTTP.script_name):]
+        env['QUERY_STRING'] = self.rest[1]
+        if not self.host == self.client_address[0]:
+            env['REMOTE_HOST'] = self.host
+        env['REMOTE_ADDR'] = self.client_address[0]
+        env['CONTENT_LENGTH'] = str(self.headers.get('content-length',''))
+        for k in ['USER_AGENT','COOKIE','ACCEPT','ACCEPT_CHARSET',
+            'ACCEPT_ENCODING','ACCEPT_LANGUAGE','CONNECTION']:
+            hdr = k.lower().replace("_","-")
+            env['HTTP_%s' %k.upper()] = str(self.headers.get(hdr,''))
+        os.environ.update(env)
+
+    def err_resp(self,code,msg):
+        """Return an error message"""
+        resp_line = "%s %s %s\r\n" %(self.protocol,code,msg)
+        self.close_when_done = True
+        self.log(code)
+        return [resp_line]
+
+    def redirect_resp(self, url, msg):
+        """Return a 301 redirect"""
+        resp_line = "%s %s %s\r\n" %(self.protocol,301,msg)
+        resp_line += "Location: %s\r\n" % url
+        self.close_when_done = True
+        self.log(301)
+        return [resp_line]
+
+    def log(self,code):
+        """Write a trace of the request on stderr"""
+        if HTTP.logging:
+            date_str = datetime.datetime.now().strftime('[%d/%b/%Y %H:%M:%S]')
+            sys.stderr.write('%s - - %s "%s" %s\n' %(self.host,
+                date_str,self.requestline,code))
+
+def default_out_func(text):
+    print text
+
+def serve_wiki(port=8081, bind_to='localhost', out_func=default_out_func):
+    print sys.version
+
+    out_func("Reading parameters from fniki.cfg...")
+    piki.set_data_dir_from_cfg()
+    out_func("Running wiki from:")
+    out_func(piki.text_dir + " (wiki text)")
+    www_dir = os.path.join(piki.data_dir, 'www')
+    out_func(www_dir + " (.css, .png)")
+    print
+    bound_to = bind_to
+    if bound_to == '':
+        bound_to = 'all interfaces!'
+
+    out_func("Starting HTTP server on port %s, bound to: %s " %
+             (port, bound_to))
+    out_func("Press Ctrl+C to stop")
+
+    # Change to 'localhost' to '' to bind to all interface. Not recommended.
+    server = SimpleAsyncServer.Server(bind_to, port)
+
+    # Must set these.
+    HTTP.script_name = SCRIPT_NAME
+    HTTP.script_path = SCRIPT_PATH
+    HTTP.script_regex = SCRIPT_REGEX
+    
+    HTTP.logging = False
+    HTTP.root = www_dir # for .css, .png
+
+    try:
+        SimpleAsyncServer.loop(server,HTTP)
+    except KeyboardInterrupt:
+        # djk20091109 Just wrong. Did I grab the wrong file for the base class??? hmmmm...
+        #
+        #for s in server.client_handlers:
+
+        # BUG: Still wrong... REDFLAG: figure out what the correct thing to do is.
+        #for s in SimpleAsyncServer.client_handlers:
+        #    server.close_client(s)
+
+        out_func('Ctrl+C pressed. Closing')
+
+if __name__=="__main__":
+    serve_wiki(8081)