This is a CGI program that maintains a user-editable FAQ. It uses RCS
to keep track of changes to individual FAQ entries. It is fully
configurable; everything you might want to change when using this
program to maintain some other FAQ than the Python FAQ is contained in
the configuration module, faqconf.py.
Note that this is not an executable script; it's an importable module.
The actual script to place in cgi-bin is faqw.py.
"""
import sys, time, os, stat, re, cgi, faqconf
from faqconf import * # This imports all uppercase names
now = time.time()
class FileError:
def __init__(self, file):
self.file = file
def emit(format, *args, **kw):
try:
f = kw['_file']
except KeyError:
f = sys.stdout
f.write(_interpolate(format, args, kw))
translate_prog = None
def translate(text, pre=0):
global translate_prog
if not translate_prog:
translate_prog = prog = re.compile(
r'\b(http|ftp|https)://\S+(\b|/)|\b[-.\w]+@[-.\w]+')
else:
prog = translate_prog
i = 0
list = []
while 1:
m = prog.search(text, i)
if not m:
break
j = m.start()
list.append(escape(text[i:j]))
i = j
url = m.group(0)
while url[-1] in '();:,.?\'"<>':
url = url[:-1]
i = i + len(url)
url = escape(url)
if not pre or (pre and PROCESS_PREFORMAT):
if ':' in url:
repl = '<A HREF="%s">%s</A>' % (url, url)
else:
repl = '<A HREF="mailto:%s">%s</A>' % (url, url)
else:
repl = url
list.append(repl)
j = len(text)
list.append(escape(text[i:j]))
return ''.join(list)
def revparse(rev):
global revparse_prog
if not revparse_prog:
revparse_prog = re.compile(r'^(\d{1,3})\.(\d{1,4})$')
m = revparse_prog.match(rev)
if not m:
return None
[major, minor] = map(int, m.group(1, 2))
return major, minor
def load_cookies():
if not os.environ.has_key('HTTP_COOKIE'):
return {}
raw = os.environ['HTTP_COOKIE']
words = [s.strip() for s in raw.split(';')]
cookies = {}
for word in words:
i = word.find('=')
if i >= 0:
key, value = word[:i], word[i+1:]
cookies[key] = value
return cookies
def load_my_cookie():
cookies = load_cookies()
try:
value = cookies[COOKIE_NAME]
except KeyError:
return {}
import urllib
value = urllib.unquote(value)
words = value.split('/')
while len(words) < 3:
words.append('')
author = '/'.join(words[:-2])
email = words[-2]
password = words[-1]
return {'author': author,
'email': email,
'password': password}
def send_my_cookie(ui):
name = COOKIE_NAME
value = "%s/%s/%s" % (ui.author, ui.email, ui.password)
import urllib
value = urllib.quote(value)
then = now + COOKIE_LIFETIME
gmt = time.gmtime(then)
path = os.environ.get('SCRIPT_NAME', '/cgi-bin/')
print "Set-Cookie: %s=%s; path=%s;" % (name, value, path),
print time.strftime("expires=%a, %d-%b-%y %X GMT", gmt)
class MagicDict:
def __init__(self, d, quote):
self.__d = d
self.__quote = quote
def __getitem__(self, key):
for d in self.__d:
try:
value = d[key]
if value:
value = str(value)
if self.__quote:
value = escapeq(value)
return value
except KeyError:
pass
return ''
def load_version(self):
command = interpolate(SH_RLOG_H, self)
p = os.popen(command)
version = ''
while 1:
line = p.readline()
if not line:
break
if line[:5] == 'head:':
version = line[5:].strip()
p.close()
self.version = version
def getmtime(self):
if not self.last_changed_date:
return 0
try:
return os.stat(self.file)[stat.ST_MTIME]
except os.error:
return 0
def emit_marks(self):
mtime = self.getmtime()
if mtime >= now - DT_VERY_RECENT:
emit(MARK_VERY_RECENT, self)
elif mtime >= now - DT_RECENT:
emit(MARK_RECENT, self)
def show(self, edit=1):
emit(ENTRY_HEADER1, self)
self.emit_marks()
emit(ENTRY_HEADER2, self)
pre = 0
raw = 0
for line in self.body.split('\n'):
# Allow the user to insert raw html into a FAQ answer
# (Skip Montanaro, with changes by Guido)
tag = line.rstrip().lower()
if tag == '<html>':
raw = 1
continue
if tag == '</html>':
raw = 0
continue
if raw:
print line
continue
if not line.strip():
if pre:
print '</PRE>'
pre = 0
else:
print '<P>'
else:
if not line[0].isspace():
if pre:
print '</PRE>'
pre = 0
else:
if not pre:
print '<PRE>'
pre = 1
if '/' in line or '@' in line:
line = translate(line, pre)
elif '<' in line or '&' in line:
line = escape(line)
if not pre and '*' in line:
line = emphasize(line)
print line
if pre:
print '</PRE>'
pre = 0
if edit:
print '<P>'
emit(ENTRY_FOOTER, self)
if self.last_changed_date:
emit(ENTRY_LOGINFO, self)
print '<P>'
class FaqDir:
entryclass = FaqEntry
__okprog = re.compile(OKFILENAME)
def __init__(self, dir=os.curdir):
self.__dir = dir
self.__files = None
def __fill(self):
if self.__files is not None:
return
self.__files = files = []
okprog = self.__okprog
for file in os.listdir(self.__dir):
if self.__okprog.match(file):
files.append(file)
files.sort()
def do_search(self):
query = self.ui.query
if not query:
self.error("Empty query string!")
return
if self.ui.querytype == 'simple':
query = re.escape(query)
queries = [query]
elif self.ui.querytype in ('anykeywords', 'allkeywords'):
words = filter(None, re.split('\W+', query))
if not words:
self.error("No keywords specified!")
return
words = map(lambda w: r'\b%s\b' % w, words)
if self.ui.querytype[:3] == 'any':
queries = ['|'.join(words)]
else:
# Each of the individual queries must match
queries = words
else:
# Default to regular expression
queries = [query]
self.prologue(T_SEARCH)
progs = []
for query in queries:
if self.ui.casefold == 'no':
p = re.compile(query)
else:
p = re.compile(query, re.IGNORECASE)
progs.append(p)
hits = []
for file in self.dir.list():
try:
entry = self.dir.open(file)
except FileError:
constants
for p in progs:
if not p.search(entry.title) and not p.search(entry.body):
break
else:
hits.append(file)
if not hits:
emit(NO_HITS, self.ui, count=0)
elif len(hits) <= MAXHITS:
if len(hits) == 1:
emit(ONE_HIT, count=1)
else:
emit(FEW_HITS, count=len(hits))
self.format_all(hits, headers=0)
else:
emit(MANY_HITS, count=len(hits))
self.format_index(hits)
def format_index(self, files, add=0, localrefs=0):
sec = 0
for file in files:
try:
entry = self.dir.open(file)
except NoSuchFile:
continue
if entry.sec != sec:
if sec:
if add:
emit(INDEX_ADDSECTION, sec=sec)
emit(INDEX_ENDSECTION, sec=sec)
sec = entry.sec
try:
title = SECTION_TITLES[sec]
except KeyError:
title = "Untitled"
emit(INDEX_SECTION, sec=sec, title=title)
if localrefs:
emit(LOCAL_ENTRY, entry)
else:
emit(INDEX_ENTRY, entry)
entry.emit_marks()
if sec:
if add:
emit(INDEX_ADDSECTION, sec=sec)
emit(INDEX_ENDSECTION, sec=sec)
def do_recent(self):
if not self.ui.days:
days = 1
else:
days = float(self.ui.days)
try:
cutoff = now - days * 24 * 3600
except OverflowError:
cutoff = 0
list = []
for file in self.dir.list():
entry = self.dir.open(file)
if not entry:
continue
mtime = entry.getmtime()
if mtime >= cutoff:
list.append((mtime, file))
list.sort()
list.reverse()
self.prologue(T_RECENT)
if days <= 1:
period = "%.2g hours" % (days*24)
else:
period = "%.6g days" % days
if not list:
emit(NO_RECENT, period=period)
elif len(list) == 1:
emit(ONE_RECENT, period=period)
else:
emit(SOME_RECENT, period=period, count=len(list))
self.format_all(map(lambda (mtime, file): file, list), headers=0)
emit(TAIL_RECENT)
def do_roulette(self):
import random
files = self.dir.list()
if not files:
self.error("No entries.")
return
file = random.choice(files)
self.prologue(T_ROULETTE)
emit(ROULETTE)
self.dir.show(file)
def rlog(self, command, entry=None):
output = os.popen(command).read()
sys.stdout.write('<PRE>')
athead = 0
lines = output.split('\n')
while lines and not lines[-1]:
del lines[-1]
if lines:
line = lines[-1]
if line[:1] == '=' and len(line) >= 40 and \
line == line[0]*len(line):
del lines[-1]
headrev = None
for line in lines:
if entry and athead and line[:9] == 'revision ':
rev = line[9:].split()
mami = revparse(rev)
if not mami:
print line
else:
emit(REVISIONLINK, entry, rev=rev, line=line)
if mami[1] > 1:
prev = "%d.%d" % (mami[0], mami[1]-1)
emit(DIFFLINK, entry, prev=prev, rev=rev)
if headrev:
emit(DIFFLINK, entry, prev=rev, rev=headrev)
else:
headrev = rev
print
athead = 0
else:
athead = 0
if line[:1] == '-' and len(line) >= 20 and \
line == len(line) * line[0]:
athead = 1
sys.stdout.write('<HR>')
else:
print line
print '</PRE>'
def do_review(self):
send_my_cookie(self.ui)
if self.ui.editversion == '*new*':
sec, num = self.dir.parse(self.ui.file)
entry = self.dir.new(section=sec)
entry.version = "*new*"
if entry.file != self.ui.file:
self.error("Commit version conflict!")
emit(NEWCONFLICT, self.ui, sec=sec, num=num)
return
else:
entry = self.dir.open(self.ui.file)
entry.load_version()
# Check that the FAQ entry number didn't change
if self.ui.title.split()[:1] != entry.title.split()[:1]:
self.error("Don't change the entry number please!")
return
# Check that the edited version is the current version
if entry.version != self.ui.editversion:
self.error("Commit version conflict!")
emit(VERSIONCONFLICT, entry, self.ui)
return
commit_ok = ((not PASSWORD
or self.ui.password == PASSWORD)
and self.ui.author
and '@' in self.ui.email
and self.ui.log)
if self.ui.commit:
if not commit_ok:
self.cantcommit()
else:
self.commit(entry)
return
self.prologue(T_REVIEW)
emit(REVIEWHEAD)
entry.body = self.ui.body
entry.title = self.ui.title
entry.show(edit=0)
emit(EDITFORM1, self.ui, entry)
if commit_ok:
emit(COMMIT)
else:
emit(NOCOMMIT_HEAD)
self.errordetail()
emit(NOCOMMIT_TAIL)
emit(EDITFORM2, self.ui, entry, load_my_cookie())
emit(EDITFORM3)
def errordetail(self):
if PASSWORD and self.ui.password != PASSWORD:
emit(NEED_PASSWD)
if not self.ui.log:
emit(NEED_LOG)
if not self.ui.author:
emit(NEED_AUTHOR)
if not self.ui.email:
emit(NEED_EMAIL)
def commit(self, entry):
file = entry.file
# Normalize line endings in body
if '\r' in self.ui.body:
self.ui.body = re.sub('\r\n?', '\n', self.ui.body)
# Normalize whitespace in title
self.ui.title = ' '.join(self.ui.title.split())
# Check that there were any changes
if self.ui.body == entry.body and self.ui.title == entry.title:
self.error("You didn't make any changes!")
return
# need to lock here because otherwise the file exists and is not writable (on NT)
command = interpolate(SH_LOCK, file=file)
p = os.popen(command)
output = p.read()