X-Git-Url: https://codewiz.org/gitweb?a=blobdiff_plain;f=geekigeeki.py;h=a7e99ed10121c9af816cfc76eddc0707edeeec94;hb=b2d45e5e3f780e9a21aeaf8cf8babaaf5f57a74d;hp=abaa4338ee109158cc2fd4f90009423fe990e4f2;hpb=1ef0274d5e544e83d31eb4b17d1bb517fdcb1c60;p=geekigeeki.git
diff --git a/geekigeeki.py b/geekigeeki.py
index abaa433..e15db21 100755
--- a/geekigeeki.py
+++ b/geekigeeki.py
@@ -1,176 +1,192 @@
-#! /usr/bin/env python
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
#
-# Copyright 1999, 2000 Martin Pool
-# Copyright 2002 Gerardo Poggiali
-# Copyright 2007 Bernardo Innocenti
+# Copyright (C) 1999, 2000 Martin Pool
+# Copyright (C) 2002 Gerardo Poggiali
+# Copyright (C) 2007, 2008, 2009, 2010 Bernie Innocenti
#
# 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
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-__version__ = '$Id$'[4:12]
+__version__ = '4.0-' + '$Id$'[4:11]
-from time import clock
+from time import clock, localtime, gmtime, strftime
start_time = clock()
+title_done = False
-import cgi, sys, string, os, re, errno, stat
-from os import path, environ
+import cgi, sys, os, re, errno, stat, glob
-# 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$")
+image_ext = 'png|gif|jpg|jpeg|bmp|ico'
+video_ext = "ogg|ogv|oga|webm" # Not supported by Firefox 3.6: mkv|mpg|mpeg|mp4|avi|asf|flv|wmv|qt
+image_re = re.compile(r".*\.(" + image_ext + "|" + video_ext + ")$", re.IGNORECASE)
+video_re = re.compile(r".*\.(" + video_ext + ")$", re.IGNORECASE)
+# FIXME: we accept stuff like foo/../bar and we shouldn't
+file_re = re.compile(r"([A-Za-z0-9_\-][A-Za-z0-9_\.\-/ ]*)$")
+url_re = re.compile(r"[a-z]{3,8}://[^\s'\"]+\S$")
+ext_re = re.compile(r"\.([^\./]+)$")
-title_done = False
+def config_get(key, default=None):
+ return globals().get(key, default)
+def script_name():
+ return os.environ.get('SCRIPT_NAME', '')
-# CGI stuff ---------------------------------------------------------
+#TODO: move post-edit hook into wiki, then kill this
+def script_path():
+ return os.path.split(os.environ.get('SCRIPT_FILENAME', ''))[0]
-def script_name():
- return environ.get('SCRIPT_NAME', '')
+def query_string():
+ path_info = os.environ.get('PATH_INFO', '')
+ if len(path_info) and path_info[0] == '/':
+ return path_info[1:] or 'FrontPage'
+ else:
+ return os.environ.get('QUERY_STRING', '') or 'FrontPage'
-def privileged_path():
- return privileged_url or script_name()
+def is_privileged():
+ purl = config_get('privileged_url')
+ return (purl is not None) and os.environ.get('SCRIPT_URI', '').startswith(purl)
def remote_user():
- user = environ.get('REMOTE_USER', '')
+ user = os.environ.get('REMOTE_USER', '')
if user is None or user == '' or user == 'anonymous':
user = 'AnonymousCoward'
return user
def remote_host():
- return environ.get('REMOTE_ADDR', '')
+ return os.environ.get('REMOTE_ADDR', '')
def get_hostname(addr):
try:
from socket import gethostbyaddr
return gethostbyaddr(addr)[0] + ' (' + addr + ')'
- except:
+ except Exception:
return addr
-# Formatting stuff --------------------------------------------------
+def is_external_url(pathname):
+ return (url_re.match(pathname) or pathname.startswith('/'))
-def emit_header(type="text/html"):
- print "Content-type: " + type + "; charset=utf-8"
- print
-
-def send_guru(msg, msg_type):
- if msg is None or msg == '': return
- print '
'
+def relative_url(pathname, privileged=False):
+ if not is_external_url(pathname):
+ if privileged:
+ url = config_get('privileged_url') or script_name()
+ else:
+ url = script_name()
+ pathname = url + '/' + pathname
+ return cgi.escape(pathname, quote=True)
+
+def permalink(s):
+ return re.sub(' ', '-', re.sub('[^a-z0-9_ ]', '', s.lower()).strip())
+
+def humanlink(s):
+ return re.sub(r'(?:.*[/:]|)([^:/\.]+)(?:\.[^/:]+|)$', r'\1', s.replace('_', ' '))
+
+# Split arg lists like "blah|blah blah| width=100 | align = center",
+# return a list containing anonymous arguments and a map containing the named arguments
+def parse_args(s):
+ args = []
+ kvargs = {}
+ for arg in s.strip('<[{}]>').split('|'):
+ m = re.match('\s*(\w+)\s*=\s*(.+)\s*', arg)
+ if m is not None:
+ kvargs[m.group(1)] = m.group(2)
+ else:
+ args.append(arg.strip())
+ return (args, kvargs)
+
+def url_args(kvargs):
+ argv = []
+ for k, v in kvargs.items():
+ argv.append(k + '=' + v)
+ if argv:
+ return '?' + '&'.join(argv)
+ return ''
+
+def emit_header(mtime=None, mime_type="text/html"):
+ if mtime:
+ # Prevent caching when the wiki engine gets updated
+ mtime = max(mtime, os.stat(__file__).st_mtime)
+ print("Last-Modified: " + strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime(mtime)))
+ if mime_type:
+ print("Content-type: " + mime_type + "; charset=utf-8")
+ print('')
+
+def send_guru(msg_text, msg_type):
+ if not msg_text: return
+ print('
')
if msg_type == 'error':
- print ' Software Failure. Press left mouse button to continue.\n'
- print msg
+ print(' Software Failure. Press left mouse button to continue.\n')
+ print(cgi.escape(msg_text))
if msg_type == 'error':
- print ' Guru Meditation #DEADBEEF.ABADC0DE'
- print '
'
- # FIXME: This simple JS snippet 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 ''
-
- site_name = globals().get('site_name', 'Unconfigured Site')
- print "%s: %s" % (site_name, text)
- print ' '
- if not name:
- print ' '
- if globals().has_key('css_url'):
- print ' ' % css_url
- print ''
-
- # Body
- if name and privileged_url is not None:
- print ''
- else:
- print ''
-
- title_done = True
- send_guru(msg, msg_type)
-
- # Navbar
- print '
' % (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(*vargs):
- 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 + '
' % (letter, letter)
- current_letter = letter
- else:
- s = s + ' '
- s = s + Page(name).link_to()
- return s
+ links = ['%s' % (ch, ch) for ch in 'abcdefghijklmnopqrstuvwxyz']
+ return '
' + ' | '.join(links) + '
'
+
+def page_list(dirname=None, search_re=None):
+ if search_re is None:
+ # FIXME: WikiWord is too restrictive now!
+ search_re = re.compile(r"^\b((([A-Z][a-z0-9]+){2,}/)*([A-Z][a-z0-9]+){2,})\b$")
+ return sorted(filter(search_re.match, os.listdir(dirname or '.')))
+def _macro_ELAPSED_TIME(*args, **kvargs):
+ return "%03f" % (clock() - start_time)
-# ----------------------------------------------------------
-class PageFormatter:
- """Object that turns Wiki markup into HTML.
+def _macro_VERSION(*args, **kvargs):
+ return __version__
- All formatting commands can be parsed one line at a time, though
- some state is carried over between lines.
- """
- def __init__(self, raw):
+class WikiFormatter:
+ """Object that turns Wiki markup into HTML."""
+ def __init__(self, raw, kvargs=None):
self.raw = raw
- self.is_em = self.is_b = 0
+ self.kvargs = kvargs or {}
self.h_level = 0
- self.h_count = 0
- self.list_indents = []
- self.in_pre = False
- self.in_table = False
- self.tr_cnt = 0
- self.in_var = False
+ self.in_pre = self.in_html = self.in_table = self.in_li = False
self.in_header = True
+ self.list_indents = [] # a list of pairs (indent_level, list_type) to track nested lists
+ self.tr_cnt = 0
+ self.styles = {
+ #wiki html enabled?
+ "//": ["em", False],
+ "**": ["b", False],
+ "##": ["tt", False],
+ "__": ["u", False],
+ "--": ["del", False],
+ "^^": ["sup", False],
+ ",,": ["sub", False],
+ "''": ["em", False], # LEGACY
+ "'''": ["b", False], # LEGACY
+ }
- 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 _b_repl(self, word):
+ style = self.styles[word]
+ style[1] = not style[1]
+ return ['', '<'][style[1]] + style[0] + '>'
+
+ def _glyph_repl(self, word):
+ return '—'
def _tit_repl(self, word):
if self.h_level:
- result = '' % self.h_level
+ result = '
\s*\|\|(=|)\s*)
+
+ # TODO: highlight search words (look at referrer)
+ )""", re.VERBOSE)
+ pre_re = re.compile("""(?:
+ (?P
\s*\}\}\})
+ | (?P[<>&])"
+ )""", re.VERBOSE)
blank_re = re.compile(r"^\s*$")
- indent_re = re.compile(r"^\s*")
+ indent_re = re.compile(r"^(\s*)(\*|\#|)")
tr_re = re.compile(r"^\s*\|\|")
eol_re = re.compile(r"\r?\n")
- raw = string.expandtabs(self.raw)
- for line in eol_re.split(raw):
- # Skip ACLs
+ # For each line, we scan through looking for magic strings, outputting verbatim any intervening text
+ #3.0: for self.line in eol_re.split(str(self.raw.expandtabs(), 'utf-8')):
+ for self.line in eol_re.split(str(self.raw.expandtabs())):
+ # Skip pragmas
if self.in_header:
- if line.startswith('#'):
- continue
+ if self.line.startswith('#'):
+ continue
self.in_header = False
if self.in_pre:
- print re.sub(pre_re, self.replace, line)
+ print(re.sub(pre_re, self.replace, self.line))
else:
- if self.in_table and not tr_re.match(line):
+ if self.in_table and not tr_re.match(self.line):
self.in_table = False
- print '
'
+ print('
')
- if blank_re.match(line):
- print '
'
+ if blank_re.match(self.line):
+ print('
')
else:
- indent = indent_re.match(line)
- print self._indent_to(len(indent.group(0)))
- print re.sub(scan_re, self.replace, line)
+ indent = indent_re.match(self.line)
+ print(self._indent_to(len(indent.group(1)), indent.group(2)))
+ # Stand back! Here we apply the monster regex that does all the parsing
+ print(re.sub(scan_re, self.replace, self.line))
+
+ if self.in_pre: print('')
+ if self.in_table: print('
')
+ print(self._indent_to(0))
+ print('
')
- if self.in_pre: print ''
- if self.in_table: print '
'
- print self._undent()
- print '
'
+class HttpException(Exception):
+ def __init__(self, error, query):
+ self.error = error
+ self.query = query
-# ----------------------------------------------------------
class Page:
- def __init__(self, page_name):
- self.page_name = page_name
- self.msg = ''
+ def __init__(self, page_name="Limbo"):
+ self.page_name = page_name.rstrip('/');
+ self.msg_text = ''
self.msg_type = 'error'
- self.attrs = {}
+ if not file_re.match(self.page_name):
+ raise HttpException("403 Forbidden", self.page_name)
def split_title(self):
- # look for the end of words and the start of a new word,
- # and insert a space there
+ # 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(data_dir, self.page_name)
+ def _filename(self):
+ return self.page_name
def _tmp_filename(self):
- return path.join(data_dir, ('#' + self.page_name + '.' + `os.getpid()` + '#'))
+ return self.page_name + '.tmp' + str(os.getpid()) + '#'
- def exists(self):
+ def _mtime(self):
try:
- os.stat(self._text_filename())
- return True
- except OSError, er:
- if er.errno == errno.ENOENT:
- return False
- else:
- raise er
+ return os.stat(self._filename()).st_mtime
+ except OSError, err:
+ if err.errno == errno.ENOENT:
+ return None
+ raise err
- def link_to(self):
- word = self.page_name
- if self.exists():
- return link_tag(word, word, 'wikilink')
- else:
- return link_tag(word, nonexist_pfx + word, 'nonexistent')
+ def exists(self):
+ if self._mtime():
+ return True
+ return False
- def get_raw_body(self):
- try:
- return open(self._text_filename(), 'rt').read()
- except IOError, er:
- if er.errno == errno.ENOENT:
- return '' # just doesn't exist, use default
- raise er
-
- def get_attrs(self):
- if self.attrs:
- return self.attrs
+ def get_raw_body(self, default=None):
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
+ return open(self._filename(), 'rb').read()
+ except IOError, err:
+ if err.errno == errno.ENOENT:
+ if default is None:
+ default = '//[[?a=edit&q=%s|Describe %s]]//' % (self.page_name, self.page_name)
+ return default
+ if err.errno == errno.EISDIR:
+ return self.format_dir()
+ raise err
+
+ def format_dir(self):
+ out = '== '
+ pathname = ''
+ for dirname in self.page_name.strip('/').split('/'):
+ pathname = (pathname and pathname + '/' ) + dirname
+ out += '[[' + pathname + '|' + dirname + ']]/'
+ out += ' ==\n'
+ images_out = '\n'
+
+ for filename in page_list(self._filename(), file_re):
+ if image_re.match(filename):
+ maxwidth = config_get('image_maxwidth', '400')
+ if maxwidth:
+ maxwidth = ' | maxwidth=' + str(maxwidth)
+ images_out += '{{' + self.page_name + '/' + filename + ' | ' + humanlink(filename) + maxwidth + ' | class=thumbleft}}\n'
+ else:
+ out += ' * [[' + self.page_name + '/' + filename + ']]\n'
+ return out + images_out
+
+ def pragmas(self):
+ if not '_pragmas' in self.__dict__:
+ self._pragmas = {}
+ try:
+ file = open(self._filename(), 'rt')
+ attr_re = re.compile(r"^#(\S*)(.*)$")
+ for line in file:
+ m = attr_re.match(line)
+ if not m:
+ break
+ self._pragmas[m.group(1)] = m.group(2).strip()
+ #print "bernie: pragmas[" + m.group(1) + "] = " + m.group(2) + " \n"
+ except IOError, err:
+ if err.errno != errno.ENOENT and err.errno != errno.EISDIR:
+ raise err
+ return self._pragmas
+
+ def pragma(self, name, default):
+ return self.pragmas().get(name, default)
def can(self, action, default=True):
- attrs = self.get_attrs()
+ acl = None
try:
- # SomeUser:read,write All:read
- acl = attrs["acl"]
+ #acl SomeUser:read,write All:read
+ acl = self.pragma("acl", None)
for rule in acl.split():
- (user,perms) = rule.split(':')
+ (user, perms) = rule.split(':')
if user == remote_user() or user == "All":
- if action in perms.split(','):
- return True
- else:
- return False
+ return action in perms.split(',')
return False
- except Exception, er:
- pass
+ except Exception:
+ if acl:
+ self.msg_text = 'Illegal acl line: ' + acl
return default
def can_write(self):
@@ -637,149 +641,208 @@ class Page:
def can_read(self):
return self.can("read", True)
- def send_page(self):
- page_name = None
- if self.can_write():
- page_name = self.page_name
- send_title(page_name, self.split_title(), msg=self.msg, msg_type=self.msg_type)
+ def send_title(self, name=None, text="Limbo", msg_text=None, msg_type='error'):
+ global title_done
+ if title_done: return
+
+ # HEAD
+ emit_header(self._mtime())
+ print('\n')
+ print("%s: %s" % (config_get('site_name', "Unconfigured Wiki"), text))
+ print(' ')
+ if not name:
+ print(' ')
+
+ for http_equiv, content in config_get('meta_urls', {}):
+ print(' ' % (http_equiv, relative_url(content)))
+
+ for link in config_get('link_urls', {}):
+ rel, href = link
+ print(' ' % (rel, relative_url(href)))
+
+ editable = name and self.can_write() and is_privileged()
+ if editable:
+ print(' ' \
+ % relative_url('?a=edit&q=' + name, privileged=True))
+
+ history = config_get('history_url')
+ if history is not None:
+ print(' ' \
+ % relative_url(history + '?a=rss'))
+
+ print('')
+
+ # BODY
+ if editable:
+ print('')
+ else:
+ print('')
+
+ title_done = True
+ send_guru(msg_text, msg_type)
+
+ if self.pragma("navbar", "on") != "on":
+ return
+
+ # NAVBAR
+ print('')
+
+ def send_footer(self):
+ if config_get('debug_cgi', False):
+ cgi.print_arguments()
+ cgi.print_form(form)
+ cgi.print_environ()
+ footer = self.pragma("footer", "sys/footer")
+ if footer != "off":
+ link_inline(footer, kvargs = {
+ 'LAST_MODIFIED': strftime(config_get('datetime_fmt', '%Y-%m-%dT%I:%M:%S%p'), localtime(self._mtime()))
+ })
+ print("")
+
+ def send_naked(self, kvargs=None):
if self.can_read():
- PageFormatter(self.get_raw_body()).print_html()
+ WikiFormatter(self.get_raw_body(), kvargs).print_html()
else:
send_guru("Read access denied by ACLs", "notice")
- send_footer(page_name, self._last_modified())
- def _last_modified(self):
- try:
- from time import localtime, strftime
- modtime = localtime(os.stat(self._text_filename())[stat.ST_MTIME])
- except OSError, er:
- if er.errno != errno.ENOENT:
- raise er
- return None
- return strftime(datetime_fmt, modtime)
+ def send(self):
+ #css foo.css
+ value = self.pragma("css", None)
+ if value:
+ global link_urls
+ link_urls += [ [ "stylesheet", value ] ]
+
+ self.send_title(name=self.page_name, text=self.split_title(), msg_text=self.msg_text, msg_type=self.msg_type)
+ self.send_naked()
+ self.send_footer()
+
+ def send_atom(self):
+ emit_header(self._mtime(), 'application/atom+xml')
+ self.in_html = True
+ link_inline("sys/atom_header", kvargs = {
+ 'LAST_MODIFIED': strftime(config_get('datetime_fmt', '%a, %d %b %Y %I:%M:%S %p'), localtime(self._mtime()))
+ })
+ self.in_html = False
+ self.send_naked()
+ self.in_html = True
+ link_inline("sys/atom_footer")
+ self.in_html = False
def send_editor(self, preview=None):
- send_title(None, 'Edit ' + self.split_title(), msg=self.msg, msg_type=self.msg_type)
+ self.send_title(text='Edit ' + self.split_title(), msg_text=self.msg_text, msg_type=self.msg_type)
if not self.can_write():
send_guru("Write access denied by ACLs", "error")
return
- print ('