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)