From 26cf3bfac2c5438e4496f41b6179ac6f5104fcfc Mon Sep 17 00:00:00 2001 From: Bernardo Innocenti Date: Sat, 8 Sep 2007 20:57:09 +0200 Subject: [PATCH] Initial import --- pikiplus.css | 256 +++++++++++++++++ pikiplus.py | 708 ++++++++++++++++++++++++++++++++++++++++++++++ post_edit_hook.sh | 20 ++ wiki_apache.conf | 8 + 4 files changed, 992 insertions(+) create mode 100755 pikiplus.css create mode 100755 pikiplus.py create mode 100755 post_edit_hook.sh create mode 100644 wiki_apache.conf diff --git a/pikiplus.css b/pikiplus.css new file mode 100755 index 0000000..666659f --- /dev/null +++ b/pikiplus.css @@ -0,0 +1,256 @@ +/* + * $Id: intrafm.css,v 1.10 2002/12/04 17:57:21 gerry Exp $ + * + * Author: Gerardo Poggiali + */ + +body { + font-family: sans-serif; + background-color: #F0F0F8; + color: #000000; + margin-top: 0px; + margin-left: 0px; + margin-right: 0px; +} + +a { + color: #1010FF; +} +a:visited { + color: #501080; +} + +a.nonexistent { + color: #D04040; +} + +a.navlink { + font-size: smaller; +} + +a.external { + font-style: italic; +} + +h1 { + font-size: x-large; + font-weight: bold; + color: #3377FF; + border: #5599EE; + background-color: #C2E2FF; + + margin-bottom: 5px; + margin-top: 5px; + padding-right: 10px; + padding-left: 10px; + + border-top-width: 1px; + border-bottom-width: 1px; + border-right-width: 0px; + border-left-width: 0px; + border-style: solid; +} + +h2 { + font-size: large; + font-weight: bold; + color: #22AA33; + border: #22AA33; + background-color: #E8F8F0; + + margin-left: 5px; + margin-right: 5px; + margin-bottom: 5px; + margin-top: 5px; + padding-right: 10px; + padding-left: 10px; + + border-top-width: 1px; + border-bottom-width: 1px; + border-left-width: 0px; + border-right-width: 0px; + border-style: solid; +} + +h3 { + font-size: large; + font-weight: bold; + color: #DD2255; + border: #BB5599; + + margin-left: 10px; + margin-right: 10px; + padding-right: 5px; + padding-left: 5px; + + border-top-width: 0px; + border-bottom-width: 1px; + border-right-width: 0px; + border-left-width: 0px; + border-style: solid; +} + +h4 { + font-size: medium; + font-weight: bold; + color: #2222AA; + + margin-left: 10px; + margin-right: 10px; + padding-right: 5px; + padding-left: 5px; + + border: #333399; + border-top-width: 0px; + border-bottom-width: 1px; + border-right-width: 0px; + border-left-width: 0px; + border-style: solid; +} + +h5 { + font-size: medium; + font-weight: bold; + color: #222288; + + margin-left: 10px; + margin-right: 10px; + padding-right: 5px; + padding-left: 5px; +} + +div { + margin-left: 10px; + margin-right: 10px; + text-align: justify; +} +div.wiki { + margin-left: 0; + margin-right: 0; +} + +div.preview { + border: 2px dashed #FF0000; +} + +div.navigator { + border-color: #cc9933; + background-color: #ffcc66; + border-style: solid; + border-top-width: 0px; + border-bottom-width: 2px; + border-left-width: 0px; + border-right-width: 0px; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 5px; + padding-right: 5px; + margin-top: 0px; + margin-bottom: 10px; + margin-left: 0px; + margin-right: 0px; +} + +div.footer { + font-size: smaller; + color: #888888; + border-color: black; + + margin-top: 10px; + margin-left: 0px; + margin-right: 0px; + + border-style: solid; + border-top-width: 1px; + border-right-width: 0px; + border-bottom-width: 0px; + border-left-width: 0px; + + text-align: right; +} + +p.dialog { + border-color: black; + background-color: white; + padding-left: 5px; + margin-left: 10px; + margin-right: 10px; + border-style: solid; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px +} + +p { + margin-left: 10px; + margin-right: 10px; + margin-bottom: 10px; + text-align: justify; +} + +caption { + background-color: #4682B4; + color: #FFFFFF; + font-weight: bold; + border: #000000; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 0px; + border-left-width: 1px; + border-style: solid; +} + +pre { + color: #222222; + border-color: #E9E609; + background: #FFFDD0; + + margin-left: 10px; + margin-right: 10px; + padding-left: 5px; + padding-right: 5px; + padding-top: 5px; + padding-bottom: 5px; + border-style: solid; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; +} + +pre.error { + color: #FF0000; + background: #000000; + border-top-color: #FF0000; + border-color: #FF0000; + border-style: solid; + border-width: 6px; + margin: 0px; +} + +pre.success { + color: #00FF00; + background: #000000; + border-color: #00FF00; + border-style: solid; + border-width: 6px; + margin: 0px; +} + +pre.notice { + color: #FFCC00; + background: #000000; + border-color: #FFCC00; + border-style: solid; + border-width: 6px; + margin: 0px; +} + +#editor { + font-family: monospace; + width: 100%; + color: black; + background-color: white; + border: 1px solid #8cacbb; +} diff --git a/pikiplus.py b/pikiplus.py new file mode 100755 index 0000000..e7001ab --- /dev/null +++ b/pikiplus.py @@ -0,0 +1,708 @@ +#! /usr/bin/env python +"""Quick-quick implementation of WikiWikiWeb in Python +""" +# +# Copyright (C) 1999, 2000 Martin Pool +# This version includes additional changes by Gerardo Poggiali (2002) +# This version includes additional changes by Bernardo Innocenti (2007) +# +# 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 3 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, see . + +__version__ = '$Revision: 1.63+gerry+bernie $'[11:-2]; + +import cgi, sys, string, os, re, errno, time, stat +from os import path, environ +from socket import gethostbyaddr +from time import localtime, strftime + +# Regular expression defining a WikiWord +# (but this definition is also assumed in other places) +file_re = re.compile(r"^\b([A-Za-z0-9_\.\-]+)\b$") +word_re = re.compile(r"^\b([A-Z][a-z]+){2,}\b$") +img_re = re.compile(r"^.*\.(png|gif|jpg|jpeg)$", re.IGNORECASE) +url_re = re.compile(r"^[a-z]{3,8}://[^\s'\"]+\S$") + +title_done = False + +def emit_header(type="text/html"): + print "Content-type: " + type + "; charset=utf-8" + print + + +# Formatting stuff -------------------------------------------------- + +def get_scriptname(): + return environ.get('SCRIPT_NAME', '') + +def get_hostname(addr): + try: + return gethostbyaddr(addr)[0] + ' (' + addr + ')' + except: + return addr; + +def send_guru(msg, msg_type): + if msg is None or len(msg) == 0: return + print '
'
+    if msg_type == 'error':
+        print '    Software Failure.  Press left mouse button to continue.\n'
+    print msg
+    if msg_type == 'error':
+        print '      Guru Meditation #DEADBEEF.ABADC0DE'
+    print '
' + # FIXME: This simple JS code is harder to pass than ACID 3.0 + print """ + """ + +def send_title(name, text="Limbo", msg=None, msg_type = 'error'): + global title_done + if title_done: return + + # Head + emit_header() + print """ + +""" + print "%s: %s" % (site_name, text) + print ' ' + if not name: + print ' ' + if css_url: + print ' ' % css_url + print '' + + # Body + if name and allow_edit: + print '' + else: + print '' + + send_guru(msg, msg_type) + + # Navbar + print '' + + title_done = True + + +def link_tag(params, text=None, ss_class=None): + if text is None: + text = params # default + classattr = '' + # Prevent crawlers from following links to generated pages + # and links added by potential spammers + if ss_class == 'external' or ss_class == 'navlink': + classattr += 'rel="nofollow" ' + if ss_class: + classattr += 'class="%s" ' % ss_class + return '%s' % (classattr, get_scriptname(), + params, text) + +# Search --------------------------------------------------- + +def do_fullsearch(needle): + send_title(None, '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 "
    " + for (count, page_name) in hits: + print '
  • ' + Page(page_name).link_to() + print ' . . . . ' + `count` + print ['match', 'matches'][count <> 1] + print '

  • ' + print "
" + + 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(None, "Title search for \"" + needle + '"') + + needle_re = re.compile(needle, re.IGNORECASE) + all_pages = page_list() + hits = filter(needle_re.search, all_pages) + + print "
    " + for filename in hits: + print '
  • ' + Page(filename).link_to() + "

  • " + print "
" + + print_search_stats(len(hits), len(all_pages)) + + +def print_search_stats(hits, searched): + print "

%d hits out of %d pages searched.

" % (hits, searched) + +def do_edit(pagename): + Page(pagename).send_editor() + +def do_raw(pagename): + Page(pagename).send_raw() + +def do_savepage(pagename): + if not allow_edit: + raise 'editing disallowed for ' + pagename + + global form + pg = Page(pagename) + if 'preview' in form: + pg.send_editor(form['savetext'].value) + elif 'save' in form: + pg.save_text(form['savetext'].value) + pg.send_page() + elif 'cancel' in form: + pg.msg = 'Editing cancelled' + pg.msg_type = 'notice' + pg.send_page() + else: + raise 'What did you press?' + +def make_index_key(): + s = '

' + links = map(lambda ch: '%s' % (ch, ch), + string.lowercase) + s = s + string.join(links, ' | ') + s = s + '

' + return s + + +def page_list(): + return filter(word_re.match, os.listdir(text_dir)) + + +def send_footer(name, mod_string=None): + print '' + + +# ---------------------------------------------------------- +# 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 """
""" % (type, default) + +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 + # set title + for word in all_words: + letter = string.lower(word[0]) + if letter <> last_letter: + s = s + ';

%s

' % (letter, letter) + last_letter = letter + + s = s + '%s
    ' % word + links = map[word] + links.sort() + last_page = None + for name in links: + if name == last_page: continue + s = s + '
  • ' + Page(name).link_to() + s = s + '
' + 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 + '

%s

' % (letter, letter) + current_letter = letter + else: + s = s + '
' + s = s + Page(name).link_to() + return s + + +# ---------------------------------------------------------- +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.h_level = 0 + self.list_indents = [] + self.in_pre = 0 + self.in_var = 0 + self.in_header = True + + def _emph_repl(self, word): + if len(word) == 3: + self.is_b = not self.is_b + return ['', ''][self.is_b] + else: + self.is_em = not self.is_em + return ['', ''][self.is_em] + + def _tit_repl(self, word): + if self.h_level: + result = "" % self.h_level + self.h_level = 0 + else: + self.h_level = len(word) - 1 + result = "" % self.h_level + return result; + + def _rule_repl(self, word): + s = self._undent() + if len(word) <= 3: + s = s + "\n
\n" + else: + s = s + "\n
\n" % (len(word) - 2 ) + return s + + def _word_repl(self, word): + return Page(word).link_to() + + def _img_repl(self, word): + return '' % (get_scriptname(), word) + + def _url_repl(self, word): + if img_re.match(word): + return '' % word + else: + return '%s' % (word, word) + + def _hurl_repl(self, word): + m = re.compile("\[\[(\S+)\ (.+)\]\]").match(word) + anchor = m.group(1) + descr = m.group(2) + if img_re.match(anchor): + return '%s' % (anchor, descr) + elif url_re.match(anchor): + return '%s' % (anchor, descr) + elif anchor.startswith('/'): + return '%s' % (anchor, descr) + else: + return link_tag(anchor, descr) + + def _email_repl(self, word): + return '%s' % (word, word) + + + def _ent_repl(self, s): + return {'&': '&', + '<': '<', + '>': '>'}[s] + + + def _li_repl(self, match): + return '
  • ' + + + def _pre_repl(self, word): + if word == '{{{' and not self.in_pre: + self.in_pre = 1 + return '
    '
    +        elif self.in_pre:
    +            self.in_pre = 0
    +            return '
    ' + else: + return '' + + def _var_repl(self, word): + if word == '{{' and not self.in_var: + self.in_var = 1 + return '' + elif self.in_var: + self.in_var = 0 + return '' + 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): + if self._indent_level() == new_level: + return '' + s = '

    ' + while self._indent_level() > new_level: + del(self.list_indents[-1]) + s += '\n' + while self._indent_level() < new_level: + self.list_indents.append(new_level) + s += '
      \n' + s += '

      ' + return s + + def _undent(self): + res = '

      ' + res += '
    ' * len(self.list_indents) + res += '

    ' + 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): + print "

    " + + # For each line, we scan through looking for magic + # strings, outputting verbatim any intervening text + scan_re = re.compile( + r"(?:" + + r"(?P'{2,3})" + + r"|(?P\={2,6})" + + r"|(?P[<>&])" + + r"|(?P\b[a-zA-Z0-9_-]+\.(png|gif|jpg|jpeg|bmp))" + + r"|(?P\b(?:[A-Z][a-z]+){2,}\b)" + + r"|(?P^-{3,})" + + r"|(?P\[\[\S+\s+.+\]\])" + + r"|(?P(http|ftp|nntp|news|mailto)\:[^\s'\"]+\S)" + + r"|(?P[-\w._+]+\@[\w.-]+)" + + r"|(?P

  • ^\s+\*)" + + r"|(?P
    (\{\{\{|\s*\}\}\}))"
    +            + r"|(?P(\{\{|\}\}))"
    +            + r"|(?P\[\[(TitleSearch|FullSearch|WordIndex|TitleIndex)\]\])"
    +            + r")")
    +        pre_re = re.compile(
    +            r"(?:"
    +            + r"(?P
    \s*\}\}\})"
    +            + r")")
    +        blank_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):
    +            # Skip ACLs
    +            if self.in_header:
    +                if line.startswith('#'):
    +                   continue
    +                self.in_header = False
    +            if self.in_pre:
    +                print re.sub(pre_re, self.replace, line)
    +            else:
    +                # XXX: Should we check these conditions in this order?
    +                if blank_re.match(line):
    +                    print '

    ' + 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 '

    ' + print self._undent() + print "

    " + +# ---------------------------------------------------------- +class Page: + def __init__(self, page_name): + self.page_name = page_name + self.remote_user = 'AnonymousCoward' + self.remote_host = environ.get('REMOTE_ADDR', '') + self.msg = '' + self.msg_type = 'error' + self.attrs = {} + + 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, word, 'wikilink') + 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 get_attrs(self): + if self.attrs: + return self.attrs + try: + file = open(self._text_filename(), 'rt') + attr_re = re.compile(r"^#(\S*)(.*)$") + for line in file: + m = attr_re.match(line) + if not m: + break + self.attrs[m.group(1)] = m.group(2).strip() + #print "bernie: attrs[" + m.group(1) + "] = " + m.group(2) + "
    \n" + except IOError, er: + if er.errno != errno.ENOENT: + raise er + return self.attrs + + def can_edit(self): + attrs = self.get_attrs() + try: + # SomeUser:read,write All:read + acl = attrs["acl"] + for rule in acl.split(): + (user,perms) = acl.split(':') + if user == self.remote_user or user == "All": + if 'write' in perms.split(','): + return True + return False + except: + pass + return True + + def send_page(self): + page_name = None + if self.can_edit(): + page_name = self.page_name + send_title(page_name, self.split_title(), msg=self.msg, msg_type=self.msg_type) + PageFormatter(self.get_raw_body()).print_html() + send_footer(page_name, 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, preview=None): + send_title(None, 'Edit ' + self.split_title(), msg=self.msg, msg_type=self.msg_type) + + print ('

    Editing ' + self.page_name + + ' for ' + self.remote_user + + ' from ' + get_hostname(self.remote_host) + + '

    ') + print '
    ' % (get_scriptname(), self.page_name) + print '' % (self.page_name) + print """""" % (preview or self.get_raw_body()) + print """ +
    + + + + +
    +
    """ + print "

    " + Page('EditingTips').link_to() + "

    " + if preview: + print "
    " + PageFormatter(preview).print_html() + print "
    " + + def send_raw(self): + emit_header("text/plain") + print self.get_raw_body() + + def _write_file(self, text): + tmp_filename = self._tmp_filename() + open(tmp_filename, 'wt').write(text.replace('\r\n', '\n')) + 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) + rc = 0 + if post_edit_hook: + cmd = ( post_edit_hook + + ' ' + text_dir + '/' + self.page_name + + ' ' + self.remote_user + + ' ' + self.remote_host + ) + out = os.popen(cmd) + msg = out.read() + rc = out.close() + if rc: + self.msg += "Post-editing hook returned %d.\n" % rc + self.msg += 'Command was: ' + cmd + '\n' + if msg: + self.msg += 'Output follows:\n' + msg + else: + self.msg = 'Thankyou for your contribution. Your attention to detail is appreciated.' + self.msg_type = 'success' + +def send_verbatim(filename, mime_type='application/octet-stream'): + pathname = path.join(text_dir, filename) + data = open(pathname, 'rb').read() + emit_header(mime_type) + sys.stdout.write(data) + +# --------------------------------------------------------------- +try: + # Configuration values + data_dir = '/home/bernie/public_html/wiki' + text_dir = path.join(data_dir, 'text') + allow_edit = True # Is it possible to edit pages? + site_name = 'codewiz.org' + changed_time_fmt = '[%I:%M %p] ' + date_fmt = '%a %d %b %Y' + datetime_fmt = '%a %d %b %Y %I:%M %p' + show_hosts = True # show hostnames? + css_url = '../wikidata/pikiplus.css' # stylesheet link, or '' + nonexist_qm = False # show '?' for nonexistent? + post_edit_hook = './post_edit_hook.sh' + + form = cgi.FieldStorage() + + handlers = { 'fullsearch': do_fullsearch, + 'titlesearch': do_titlesearch, + 'edit': do_edit, + 'raw': do_raw, + '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 len(path_info) and path_info[0] == '/': + query = path_info[1:] or 'FrontPage' + else: + query = environ.get('QUERY_STRING', '') or 'FrontPage' + + if file_re.match(query): + if word_re.match(query): + Page(query).send_page() + elif img_re.match(query): + send_verbatim(query, 'image/jpeg') + else: + send_verbatim(query) + else: + # TODO: return 404? + send_title(None, msg='Can\'t work out query: ' + query) +except: + import traceback + msg=traceback.format_exc() + if title_done: + send_guru(msg, "error") + else: + send_title(None, msg=msg) + send_footer(None) + +sys.stdout.flush() diff --git a/post_edit_hook.sh b/post_edit_hook.sh new file mode 100755 index 0000000..d12d0a7 --- /dev/null +++ b/post_edit_hook.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +# Die easily and make sure the cgi collects stderr too +set -e +exec 2>&1 + +if [ $# -ne 3 ]; then + echo "Usage $0 " + exit 1 +fi + +repo=`dirname "$1"` +file=`basename "$1"` + +cd $repo +git-add $file # In case it's a new page + +GIT_COMMITTER_NAME='Piki Plus' \ +GIT_COMMITTER_EMAIL='webmaster@codewiz.org' \ +git-commit -a -m "Edit page $file" --author="$2 <$2@$3>" diff --git a/wiki_apache.conf b/wiki_apache.conf new file mode 100644 index 0000000..0666594 --- /dev/null +++ b/wiki_apache.conf @@ -0,0 +1,8 @@ +# Where the wiki should appear +ScriptAlias /wiki /home/bernie/public_html/wiki/pikiplus.py + +# Path to gitweb scriopt to browse the wiki +ScriptAlias /wikigit /home/bernie/public_html/wiki/gitweb.cgi + +# Data files (optional) +Alias /wikidata /home/bernie/public_html/wiki -- 2.25.1