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 not os
.environ
.has_key('HTTP_COOKIE'):
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
.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
.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
),
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
= rfc822
.Message(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)
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 not SECTION_TITLES
.has_key(section
):
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
, exc
:
391 self
.error("Invalid entry file name %s" % exc
.file)
392 except NoSuchFile
, exc
:
393 self
.error("No entry with file name %s" % exc
.file)
394 except NoSuchSection
, 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
= filter(None, re
.split('\W+', query
))
431 self
.error("No keywords specified!")
433 words
= map(lambda w
: r
'\b%s\b' % w
, 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(map(lambda (mtime
, file): file, 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
= SECTION_TITLES
.items()
608 for section
, title
in sections
:
609 emit(ADD_SECTION
, section
=section
, title
=title
)
613 self
.prologue(T_DELETE
)
617 entry
= self
.dir.open(self
.ui
.file)
618 self
.prologue(T_LOG
, entry
)
620 self
.rlog(interpolate(SH_RLOG
, entry
), entry
)
622 def rlog(self
, command
, entry
=None):
623 output
= os
.popen(command
).read()
624 sys
.stdout
.write('<PRE>')
626 lines
= output
.split('\n')
627 while lines
and not lines
[-1]:
631 if line
[:1] == '=' and len(line
) >= 40 and \
632 line
== line
[0]*len(line
):
636 if entry
and athead
and line
[:9] == 'revision ':
637 rev
= line
[9:].split()
642 emit(REVISIONLINK
, entry
, rev
=rev
, line
=line
)
644 prev
= "%d.%d" % (mami
[0], mami
[1]-1)
645 emit(DIFFLINK
, entry
, prev
=prev
, rev
=rev
)
647 emit(DIFFLINK
, entry
, prev
=rev
, rev
=headrev
)
654 if line
[:1] == '-' and len(line
) >= 20 and \
655 line
== len(line
) * line
[0]:
657 sys
.stdout
.write('<HR>')
662 def do_revision(self
):
663 entry
= self
.dir.open(self
.ui
.file)
667 self
.error("Invalid revision number: %r." % (rev
,))
668 self
.prologue(T_REVISION
, entry
)
669 self
.shell(interpolate(SH_REVISION
, entry
, rev
=rev
))
672 entry
= self
.dir.open(self
.ui
.file)
677 self
.error("Invalid revision number: %r." % (rev
,))
679 if not revparse(prev
):
680 self
.error("Invalid previous revision number: %r." % (prev
,))
682 prev
= '%d.%d' % (mami
[0], mami
[1])
683 self
.prologue(T_DIFF
, entry
)
684 self
.shell(interpolate(SH_RDIFF
, entry
, rev
=rev
, prev
=prev
))
686 def shell(self
, command
):
687 output
= os
.popen(command
).read()
688 sys
.stdout
.write('<PRE>')
693 entry
= self
.dir.new(section
=int(self
.ui
.section
))
694 entry
.version
= '*new*'
695 self
.prologue(T_EDIT
)
697 emit(EDITFORM1
, entry
, editversion
=entry
.version
)
698 emit(EDITFORM2
, entry
, load_my_cookie())
703 entry
= self
.dir.open(self
.ui
.file)
705 self
.prologue(T_EDIT
)
707 emit(EDITFORM1
, entry
, editversion
=entry
.version
)
708 emit(EDITFORM2
, entry
, load_my_cookie())
713 send_my_cookie(self
.ui
)
714 if self
.ui
.editversion
== '*new*':
715 sec
, num
= self
.dir.parse(self
.ui
.file)
716 entry
= self
.dir.new(section
=sec
)
717 entry
.version
= "*new*"
718 if entry
.file != self
.ui
.file:
719 self
.error("Commit version conflict!")
720 emit(NEWCONFLICT
, self
.ui
, sec
=sec
, num
=num
)
723 entry
= self
.dir.open(self
.ui
.file)
725 # Check that the FAQ entry number didn't change
726 if self
.ui
.title
.split()[:1] != entry
.title
.split()[:1]:
727 self
.error("Don't change the entry number please!")
729 # Check that the edited version is the current version
730 if entry
.version
!= self
.ui
.editversion
:
731 self
.error("Commit version conflict!")
732 emit(VERSIONCONFLICT
, entry
, self
.ui
)
734 commit_ok
= ((not PASSWORD
735 or self
.ui
.password
== PASSWORD
)
737 and '@' in self
.ui
.email
745 self
.prologue(T_REVIEW
)
747 entry
.body
= self
.ui
.body
748 entry
.title
= self
.ui
.title
750 emit(EDITFORM1
, self
.ui
, entry
)
757 emit(EDITFORM2
, self
.ui
, entry
, load_my_cookie())
760 def cantcommit(self
):
761 self
.prologue(T_CANTCOMMIT
)
762 print CANTCOMMIT_HEAD
764 print CANTCOMMIT_TAIL
766 def errordetail(self
):
767 if PASSWORD
and self
.ui
.password
!= PASSWORD
:
771 if not self
.ui
.author
:
773 if not self
.ui
.email
:
776 def commit(self
, entry
):
778 # Normalize line endings in body
779 if '\r' in self
.ui
.body
:
780 self
.ui
.body
= re
.sub('\r\n?', '\n', self
.ui
.body
)
781 # Normalize whitespace in title
782 self
.ui
.title
= ' '.join(self
.ui
.title
.split())
783 # Check that there were any changes
784 if self
.ui
.body
== entry
.body
and self
.ui
.title
== entry
.title
:
785 self
.error("You didn't make any changes!")
788 # need to lock here because otherwise the file exists and is not writable (on NT)
789 command
= interpolate(SH_LOCK
, file=file)
790 p
= os
.popen(command
)
800 self
.error(CANTWRITE
, file=file, why
=why
)
802 date
= time
.ctime(now
)
803 emit(FILEHEADER
, self
.ui
, os
.environ
, date
=date
, _file
=f
, _quote
=0)
805 f
.write(self
.ui
.body
)
810 tf
= tempfile
.NamedTemporaryFile()
811 emit(LOGHEADER
, self
.ui
, os
.environ
, date
=date
, _file
=tf
)
815 command
= interpolate(SH_CHECKIN
, file=file, tfn
=tf
.name
)
816 log("\n\n" + command
)
817 p
= os
.popen(command
)
820 log("output: " + output
)
821 log("done: " + str(sts
))
822 log("TempFile:\n" + tf
.read() + "end")
825 self
.prologue(T_COMMITTED
)
828 self
.error(T_COMMITFAILED
)
829 emit(COMMITFAILED
, sts
=sts
)
830 print '<PRE>%s</PRE>' % escape(output
)
837 entry
= self
.dir.open(file)