X-Git-Url: https://codewiz.org/gitweb?a=blobdiff_plain;f=geekigeeki.py;h=a7e99ed10121c9af816cfc76eddc0707edeeec94;hb=b2d45e5e3f780e9a21aeaf8cf8babaaf5f57a74d;hp=57992d288b93eb1774a2a82ce051dba49cfea5ab;hpb=10a9870db7d40c7375470061274902fc9f38f3a6;p=geekigeeki.git
diff --git a/geekigeeki.py b/geekigeeki.py
index 57992d2..e15db21 100755
--- a/geekigeeki.py
+++ b/geekigeeki.py
@@ -1,173 +1,192 @@
-#! /usr/bin/env python
-"""Quick-quick implementation of WikiWikiWeb in Python
-"""
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
#
# Copyright (C) 1999, 2000 Martin Pool
-# This version includes additional changes by Gerardo Poggiali (2002)
-# This version includes additional changes by Bernardo Innocenti (2007)
+# 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__ = '$Revision: 1.63+gerry+bernie $'[11:-2]
-
-import cgi, sys, string, os, re, errno, time, stat
-from os import path, environ
-
-# 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$")
+__version__ = '4.0-' + '$Id$'[4:11]
+from time import clock, localtime, gmtime, strftime
+start_time = clock()
title_done = False
+import cgi, sys, os, re, errno, stat, glob
-# CGI stuff ---------------------------------------------------------
+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"\.([^\./]+)$")
+
+def config_get(key, default=None):
+ return globals().get(key, default)
def script_name():
- return environ.get('SCRIPT_NAME', '')
+ return os.environ.get('SCRIPT_NAME', '')
+
+#TODO: move post-edit hook into wiki, then kill this
+def script_path():
+ return os.path.split(os.environ.get('SCRIPT_FILENAME', ''))[0]
+
+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 emit_header(type="text/html"):
- print "Content-type: " + type + "; charset=utf-8"
- print
+def is_external_url(pathname):
+ return (url_re.match(pathname) or pathname.startswith('/'))
-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 "%s: %s" % (site_name, text)
- print ' '
- if not name:
- print ' '
- if 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():
- 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*\|\|(=|)\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(text_dir, self.page_name)
+ def _filename(self):
+ return self.page_name
def _tmp_filename(self):
- return path.join(text_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 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')
+ return os.stat(self._filename()).st_mtime
+ except OSError, err:
+ if err.errno == errno.ENOENT:
+ return None
+ raise err
+ def exists(self):
+ if self._mtime():
+ return True
+ return False
- def get_raw_body(self):
+ def get_raw_body(self, default=None):
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
+ 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:
- raise er
+ out += ' * [[' + self.page_name + '/' + filename + ']]\n'
+ return out + images_out
- 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()
+ 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):
+ 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) = acl.split(':')
+ (user, perms) = rule.split(':')
if user == remote_user() or user == "All":
- if 'write' in perms.split(','):
- return True
+ return action in perms.split(',')
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
- from time import localtime, strftime
- modtime = localtime(os.stat(self._text_filename())[stat.ST_MTIME])
- return strftime(datetime_fmt, modtime)
+ except Exception:
+ if acl:
+ self.msg_text = 'Illegal acl line: ' + acl
+ return default
+
+ def can_write(self):
+ return self.can("write", True)
+
+ def can_read(self):
+ return self.can("read", True)
+
+ 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():
+ WikiFormatter(self.get_raw_body(), kvargs).print_html()
+ else:
+ send_guru("Read access denied by ACLs", "notice")
+
+ 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)
-
- print ('