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)