3
This is a CGI program that maintains a user-editable FAQ. It uses RCS
4
to keep track of changes to individual FAQ entries. It is fully
5
configurable; everything you might want to change when using this
6
program to maintain some other FAQ than the Python FAQ is contained in
7
the configuration module, faqconf.py.
9
Note that this is not an executable script; it's an importable module.
10
The actual script to place in cgi-bin is faqw.py.
14
import sys, time, os, stat, re, cgi, faqconf
15
from faqconf import * # This imports all uppercase names
19
def __init__(self, file):
22
class InvalidFile(FileError):
25
class NoSuchSection(FileError):
26
def __init__(self, section):
27
FileError.__init__(self, NEWFILENAME %(section, 1))
28
self.section = section
30
class NoSuchFile(FileError):
31
def __init__(self, file, why=None):
32
FileError.__init__(self, file)
36
s = s.replace('&', '&')
37
s = s.replace('<', '<')
38
s = s.replace('>', '>')
43
s = s.replace('"', '"')
46
def _interpolate(format, args, kw):
51
d = (kw,) + args + (faqconf.__dict__,)
52
m = MagicDict(d, quote)
55
def interpolate(format, *args, **kw):
56
return _interpolate(format, args, kw)
58
def emit(format, *args, **kw):
63
f.write(_interpolate(format, args, kw))
67
def translate(text, pre=0):
69
if not translate_prog:
70
translate_prog = prog = re.compile(
71
r'\b(http|ftp|https)://\S+(\b|/)|\b[-.\w]+@[-.\w]+')
77
m = prog.search(text, i)
81
list.append(escape(text[i:j]))
84
while url[-1] in '();:,.?\'"<>':
88
if not pre or (pre and PROCESS_PREFORMAT):
90
repl = '<A HREF="%s">%s</A>' % (url, url)
92
repl = '<A HREF="mailto:%s">%s</A>' % (url, url)
97
list.append(escape(text[i:j]))
101
return re.sub(r'\*([a-zA-Z]+)\*', r'<I>\1</I>', line)
107
if not revparse_prog:
108
revparse_prog = re.compile(r'^(\d{1,3})\.(\d{1,4})$')
109
m = revparse_prog.match(rev)
112
[major, minor] = map(int, m.group(1, 2))
118
logfile = open("logfile", "a")
119
logfile.write(text + "\n")
123
if 'HTTP_COOKIE' not in os.environ:
125
raw = os.environ['HTTP_COOKIE']
126
words = [s.strip() for s in raw.split(';')]
131
key, value = word[:i], word[i+1:]
135
def load_my_cookie():
136
cookies = load_cookies()
138
value = cookies[COOKIE_NAME]
142
value = urllib.parse.unquote(value)
143
words = value.split('/')
144
while len(words) < 3:
146
author = '/'.join(words[:-2])
149
return {'author': author,
151
'password': password}
153
def send_my_cookie(ui):
155
value = "%s/%s/%s" % (ui.author, ui.email, ui.password)
157
value = urllib.parse.quote(value)
158
then = now + COOKIE_LIFETIME
159
gmt = time.gmtime(then)
160
path = os.environ.get('SCRIPT_NAME', '/cgi-bin/')
161
print("Set-Cookie: %s=%s; path=%s;" % (name, value, path), end=' ')
162
print(time.strftime("expires=%a, %d-%b-%y %X GMT", gmt))
166
def __init__(self, d, quote):
170
def __getitem__(self, key):
177
value = escapeq(value)
186
self.__form = cgi.FieldStorage()
187
#log("\n\nbody: " + self.body)
189
def __getattr__(self, name):
193
value = self.__form[name].value
194
except (TypeError, KeyError):
197
value = value.strip()
198
setattr(self, name, value)
201
def __getitem__(self, key):
202
return getattr(self, key)
206
def __init__(self, fp, file, sec_num):
208
self.sec, self.num = sec_num
211
self.__headers = email.message_from_file(fp)
212
self.body = fp.read().strip()
214
self.__headers = {'title': "%d.%d. " % sec_num}
217
def __getattr__(self, name):
220
key = '-'.join(name.split('_'))
222
value = self.__headers[key]
225
setattr(self, name, value)
228
def __getitem__(self, key):
229
return getattr(self, key)
231
def load_version(self):
232
command = interpolate(SH_RLOG_H, self)
233
p = os.popen(command)
239
if line[:5] == 'head:':
240
version = line[5:].strip()
242
self.version = version
245
if not self.last_changed_date:
248
return os.stat(self.file)[stat.ST_MTIME]
252
def emit_marks(self):
253
mtime = self.getmtime()
254
if mtime >= now - DT_VERY_RECENT:
255
emit(MARK_VERY_RECENT, self)
256
elif mtime >= now - DT_RECENT:
257
emit(MARK_RECENT, self)
259
def show(self, edit=1):
260
emit(ENTRY_HEADER1, self)
262
emit(ENTRY_HEADER2, self)
265
for line in self.body.split('\n'):
266
# Allow the user to insert raw html into a FAQ answer
267
# (Skip Montanaro, with changes by Guido)
268
tag = line.rstrip().lower()
285
if not line[0].isspace():
293
if '/' in line or '@' in line:
294
line = translate(line, pre)
295
elif '<' in line or '&' in line:
297
if not pre and '*' in line:
298
line = emphasize(line)
305
emit(ENTRY_FOOTER, self)
306
if self.last_changed_date:
307
emit(ENTRY_LOGINFO, self)
312
entryclass = FaqEntry
314
__okprog = re.compile(OKFILENAME)
316
def __init__(self, dir=os.curdir):
321
if self.__files is not None:
323
self.__files = files = []
324
okprog = self.__okprog
325
for file in os.listdir(self.__dir):
326
if self.__okprog.match(file):
330
def good(self, file):
331
return self.__okprog.match(file)
333
def parse(self, file):
337
sec, num = m.group(1, 2)
338
return int(sec), int(num)
341
# XXX Caller shouldn't modify result
345
def open(self, file):
346
sec_num = self.parse(file)
348
raise InvalidFile(file)
351
except IOError as msg:
352
raise NoSuchFile(file, msg)
354
return self.entryclass(fp, file, sec_num)
358
def show(self, file, edit=1):
359
self.open(file).show(edit=edit)
361
def new(self, section):
362
if section not in SECTION_TITLES:
363
raise NoSuchSection(section)
365
for file in self.list():
366
sec, num = self.parse(file)
368
maxnum = max(maxnum, num)
369
sec_num = (section, maxnum+1)
370
file = NEWFILENAME % sec_num
371
return self.entryclass(None, file, sec_num)
376
self.ui = UserInput()
380
print('Content-type: text/html')
381
req = self.ui.req or 'home'
382
mname = 'do_%s' % req
384
meth = getattr(self, mname)
385
except AttributeError:
386
self.error("Bad request type %r." % (req,))
390
except InvalidFile as exc:
391
self.error("Invalid entry file name %s" % exc.file)
392
except NoSuchFile as exc:
393
self.error("No entry with file name %s" % exc.file)
394
except NoSuchSection as exc:
395
self.error("No section number %s" % exc.section)
398
def error(self, message, **kw):
399
self.prologue(T_ERROR)
402
def prologue(self, title, entry=None, **kw):
403
emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
409
self.prologue(T_HOME)
413
self.prologue("FAQ Wizard Debugging")
414
form = cgi.FieldStorage()
416
cgi.print_environ(os.environ)
417
cgi.print_directory()
418
cgi.print_arguments()
421
query = self.ui.query
423
self.error("Empty query string!")
425
if self.ui.querytype == 'simple':
426
query = re.escape(query)
428
elif self.ui.querytype in ('anykeywords', 'allkeywords'):
429
words = [_f for _f in re.split('\W+', query) if _f]
431
self.error("No keywords specified!")
433
words = [r'\b%s\b' % w for w in words]
434
if self.ui.querytype[:3] == 'any':
435
queries = ['|'.join(words)]
437
# Each of the individual queries must match
440
# Default to regular expression
442
self.prologue(T_SEARCH)
444
for query in queries:
445
if self.ui.casefold == 'no':
446
p = re.compile(query)
448
p = re.compile(query, re.IGNORECASE)
451
for file in self.dir.list():
453
entry = self.dir.open(file)
457
if not p.search(entry.title) and not p.search(entry.body):
462
emit(NO_HITS, self.ui, count=0)
463
elif len(hits) <= MAXHITS:
465
emit(ONE_HIT, count=1)
467
emit(FEW_HITS, count=len(hits))
468
self.format_all(hits, headers=0)
470
emit(MANY_HITS, count=len(hits))
471
self.format_index(hits)
475
files = self.dir.list()
476
self.last_changed(files)
477
self.format_index(files, localrefs=1)
478
self.format_all(files)
481
files = self.dir.list()
483
self.last_changed(files)
484
self.format_index(files, localrefs=1)
485
self.format_all(files, edit=0)
486
sys.exit(0) # XXX Hack to suppress epilogue
488
def last_changed(self, files):
491
entry = self.dir.open(file)
493
mtime = mtime = entry.getmtime()
496
print(time.strftime(LAST_CHANGED, time.localtime(latest)))
499
def format_all(self, files, edit=1, headers=1):
503
entry = self.dir.open(file)
506
if headers and entry.sec != sec:
509
title = SECTION_TITLES[sec]
512
emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n",
513
sec=sec, title=title)
514
entry.show(edit=edit)
517
self.prologue(T_INDEX)
518
files = self.dir.list()
519
self.last_changed(files)
520
self.format_index(files, add=1)
522
def format_index(self, files, add=0, localrefs=0):
526
entry = self.dir.open(file)
532
emit(INDEX_ADDSECTION, sec=sec)
533
emit(INDEX_ENDSECTION, sec=sec)
536
title = SECTION_TITLES[sec]
539
emit(INDEX_SECTION, sec=sec, title=title)
541
emit(LOCAL_ENTRY, entry)
543
emit(INDEX_ENTRY, entry)
547
emit(INDEX_ADDSECTION, sec=sec)
548
emit(INDEX_ENDSECTION, sec=sec)
554
days = float(self.ui.days)
556
cutoff = now - days * 24 * 3600
557
except OverflowError:
560
for file in self.dir.list():
561
entry = self.dir.open(file)
564
mtime = entry.getmtime()
566
list.append((mtime, file))
569
self.prologue(T_RECENT)
571
period = "%.2g hours" % (days*24)
573
period = "%.6g days" % days
575
emit(NO_RECENT, period=period)
577
emit(ONE_RECENT, period=period)
579
emit(SOME_RECENT, period=period, count=len(list))
580
self.format_all([mtime_file[1] for mtime_file in list], headers=0)
583
def do_roulette(self):
585
files = self.dir.list()
587
self.error("No entries.")
589
file = random.choice(files)
590
self.prologue(T_ROULETTE)
595
self.prologue(T_HELP)
599
entry = self.dir.open(self.ui.file)
600
self.prologue(T_SHOW)
606
sections = sorted(SECTION_TITLES.items())
607
for section, title in sections:
608
emit(ADD_SECTION, section=section, title=title)
612
self.prologue(T_DELETE)
616
entry = self.dir.open(self.ui.file)
617
self.prologue(T_LOG, entry)
619
self.rlog(interpolate(SH_RLOG, entry), entry)
621
def rlog(self, command, entry=None):
622
output = os.popen(command).read()
623
sys.stdout.write('<PRE>')
625
lines = output.split('\n')
626
while lines and not lines[-1]:
630
if line[:1] == '=' and len(line) >= 40 and \
631
line == line[0]*len(line):
635
if entry and athead and line[:9] == 'revision ':
636
rev = line[9:].split()
641
emit(REVISIONLINK, entry, rev=rev, line=line)
643
prev = "%d.%d" % (mami[0], mami[1]-1)
644
emit(DIFFLINK, entry, prev=prev, rev=rev)
646
emit(DIFFLINK, entry, prev=rev, rev=headrev)
653
if line[:1] == '-' and len(line) >= 20 and \
654
line == len(line) * line[0]:
656
sys.stdout.write('<HR>')
661
def do_revision(self):
662
entry = self.dir.open(self.ui.file)
666
self.error("Invalid revision number: %r." % (rev,))
667
self.prologue(T_REVISION, entry)
668
self.shell(interpolate(SH_REVISION, entry, rev=rev))
671
entry = self.dir.open(self.ui.file)
676
self.error("Invalid revision number: %r." % (rev,))
678
if not revparse(prev):
679
self.error("Invalid previous revision number: %r." % (prev,))
681
prev = '%d.%d' % (mami[0], mami[1])
682
self.prologue(T_DIFF, entry)
683
self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
685
def shell(self, command):
686
output = os.popen(command).read()
687
sys.stdout.write('<PRE>')
688
print(escape(output))
692
entry = self.dir.new(section=int(self.ui.section))
693
entry.version = '*new*'
694
self.prologue(T_EDIT)
696
emit(EDITFORM1, entry, editversion=entry.version)
697
emit(EDITFORM2, entry, load_my_cookie())
702
entry = self.dir.open(self.ui.file)
704
self.prologue(T_EDIT)
706
emit(EDITFORM1, entry, editversion=entry.version)
707
emit(EDITFORM2, entry, load_my_cookie())
712
send_my_cookie(self.ui)
713
if self.ui.editversion == '*new*':
714
sec, num = self.dir.parse(self.ui.file)
715
entry = self.dir.new(section=sec)
716
entry.version = "*new*"
717
if entry.file != self.ui.file:
718
self.error("Commit version conflict!")
719
emit(NEWCONFLICT, self.ui, sec=sec, num=num)
722
entry = self.dir.open(self.ui.file)
724
# Check that the FAQ entry number didn't change
725
if self.ui.title.split()[:1] != entry.title.split()[:1]:
726
self.error("Don't change the entry number please!")
728
# Check that the edited version is the current version
729
if entry.version != self.ui.editversion:
730
self.error("Commit version conflict!")
731
emit(VERSIONCONFLICT, entry, self.ui)
733
commit_ok = ((not PASSWORD
734
or self.ui.password == PASSWORD)
736
and '@' in self.ui.email
744
self.prologue(T_REVIEW)
746
entry.body = self.ui.body
747
entry.title = self.ui.title
749
emit(EDITFORM1, self.ui, entry)
756
emit(EDITFORM2, self.ui, entry, load_my_cookie())
759
def cantcommit(self):
760
self.prologue(T_CANTCOMMIT)
761
print(CANTCOMMIT_HEAD)
763
print(CANTCOMMIT_TAIL)
765
def errordetail(self):
766
if PASSWORD and self.ui.password != PASSWORD:
770
if not self.ui.author:
772
if not self.ui.email:
775
def commit(self, entry):
777
# Normalize line endings in body
778
if '\r' in self.ui.body:
779
self.ui.body = re.sub('\r\n?', '\n', self.ui.body)
780
# Normalize whitespace in title
781
self.ui.title = ' '.join(self.ui.title.split())
782
# Check that there were any changes
783
if self.ui.body == entry.body and self.ui.title == entry.title:
784
self.error("You didn't make any changes!")
787
# need to lock here because otherwise the file exists and is not writable (on NT)
788
command = interpolate(SH_LOCK, file=file)
789
p = os.popen(command)
798
except IOError as why:
799
self.error(CANTWRITE, file=file, why=why)
801
date = time.ctime(now)
802
emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0)
804
f.write(self.ui.body)
809
tf = tempfile.NamedTemporaryFile()
810
emit(LOGHEADER, self.ui, os.environ, date=date, _file=tf)
814
command = interpolate(SH_CHECKIN, file=file, tfn=tf.name)
815
log("\n\n" + command)
816
p = os.popen(command)
819
log("output: " + output)
820
log("done: " + str(sts))
821
log("TempFile:\n" + tf.read() + "end")
824
self.prologue(T_COMMITTED)
827
self.error(T_COMMITFAILED)
828
emit(COMMITFAILED, sts=sts)
829
print('<PRE>%s</PRE>' % escape(output))
836
entry = self.dir.open(file)