Commit 5c7f55db authored by Georg Brandl's avatar Georg Brandl

Remove faqwiz tool.

parent 04d259f0
FAQ Wizard
----------
Author: Guido van Rossum <guido@python.org>
Version: 1.0
Date: 6 April 1998
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 the bulk of the code is not an executable script; it's an
importable module. The actual script in cgi-bin is minimal.
Files:
faqw.py executable script to be edited and installed in cgi-bin
faqwiz.py main module, lives in same directory as FAQ entry files
faqconf.py main configuration module
faqcust.py additional local customization module (optional)
move-faqwiz.sh Script to move faqwiz entries.
What's New?
-----------
Version 1.0 corrects some minor bugs and uses tab-agnostic
indentation; it is otherwise unchanged from version 0.9.0.
Version 0.9.0 uses the re module (Perl style regular expressions) for
all its regular expression needs, instead of the regex and regsub
modules (Emacs style). This affects the syntax for regular
expressions entered by the user as search strings (with "regular
expression" checked), hence the version number jump.
Setup Information
-----------------
This assumes you are familiar with Python, with your http server, and
with running CGI scripts under your http server. You need Python 1.5
or better.
Select a place where the Python modules that constitute the FAQ wizard
will live (the directory where you unpacked it is an obvious choice).
This will be called the SRCDIR. This directory should not be writable
by other users of your system (since they would be able to execute
arbitrary code by invoking the FAQ wizard's CGI script).
Create a dedicated working directory, preferably one that's not
directly reachable from your http server. This will be called the
FAQDIR. Create a subdirectory named RCS. Make both the working
directory and the RCS subdirectory wrld-writable. (This is essential,
since the FAQ wizard runs as use nobody, and needs to create
additional files here!)
Edit faqconf.py to reflect your setup. You only need to edit the top
part, up till the line of all dashes. The comments should guide you
in your edits. (Actually, you can also choose to add your changes to
faqcust.py and leave faqconf.py alone. This is essential if you are
maintaining multiple FAQs; see below.)
Don't forget to edit the SECTION_TITLES variables to reflect the set
of section titles for your FAQ!
Next, edit faqw.py to reflect the pathname of your Python interpreter
and the values for SRCDIR and FAQDIR that you just chose. Then
install faqw.py in your cgi-bin directory. Make sure that it is
world-executable. You should now be able to connect to the FAQ wizard
by entering the following URL in your web client (subsituting the
appropriate host and port for "your.web.server", and perhaps
specifying a different directory for "cgi-bin" if local conventions so
dictate):
http://your.web.server/cgi-bin/faqw.py
If you are unable to get this working, check your server's error_log
file. The documentation for Python's cgi module in the Python Library
Reference Manual gives plentyu additional information about installing
and debugging CGI scripts, including setup debugging. This
documentation is repeated in the doc string in the cgi module; try
``import cgi; print cgi.__doc__''.
Assuming this works, you should now be able to add the first entry to
your FAQ using the FAQ wizard interface. This creates a file
faq01.001.htp in your working directory and an RCS revision history
file faq01.001.htp,v in the RCS subdirectory. You can now exercise
the other FAQ wizard features (search, index, whole FAQ, what's new,
roulette, and so on).
Maintaining Multiple FAQs
-------------------------
If you have multiple FAQs, you need a separate FAQDIR per FAQ, and a
different customization file per FAQ. The easiest thing to do would
be to have the faqcust.py for each FAQ live in the FAQDIR for that
FAQ, but that creates some security concerns, since the FAQDIR must be
world writable: *if* someone who breaks into your system (or a
legitimate user) manages to edit the faqcust.py file they can get
arbitrary code to execute through the FAQ wizard. Therefore, you will
need a more complex setup.
The best way is probably to have a directory that is only writable by
you for each FAQ, where you place the copy of faqcust.py for that FAQ,
and have a world-writable subdirectory DATA for the data. You then
set FAQDIR to point to the DATA directory and change the faqw.py
bootstrap script to add FAQDIR/.. to sys.path (in front of SRCDIR, so
the dummy faqcust.py from SRCDIR is ignored).
--Guido van Rossum (home page: http://www.python.org/~guido/)
"""FAQ Wizard customization module.
Edit this file to customize the FAQ Wizard. For normal purposes, you
should only have to change the FAQ section titles and the small group
of parameters below it.
"""
# Titles of FAQ sections
SECTION_TITLES = {
# SectionNumber : SectionTitle; need at least one entry
1: "General information and availability",
}
# Parameters you definitely want to change
SHORTNAME = "Generic" # FAQ name with "FAQ" omitted
PASSWORD = "" # Password for editing
OWNERNAME = "FAQ owner" # Name for feedback
OWNEREMAIL = "nobody@anywhere.org" # Email for feedback
HOMEURL = "http://www.python.org" # Related home page
HOMENAME = "Python home" # Name of related home page
RCSBINDIR = "/usr/local/bin/" # Directory containing RCS commands
# (must end in a slash)
# Parameters you can normally leave alone
MAXHITS = 10 # Max #hits to be shown directly
COOKIE_LIFETIME = 28*24*3600 # Cookie expiration in seconds
# (28*24*3600 = 28 days = 4 weeks)
PROCESS_PREFORMAT = 1 # toggle whether preformatted text
# will replace urls and emails with
# HTML links
# Markers appended to title to indicate recently change
# (may contain HTML, e.g. <IMG>); and corresponding
MARK_VERY_RECENT = " **" # Changed very recently
MARK_RECENT = " *" # Changed recently
DT_VERY_RECENT = 24*3600 # 24 hours
DT_RECENT = 7*24*3600 # 7 days
EXPLAIN_MARKS = """
<P>(Entries marked with ** were changed within the last 24 hours;
entries marked with * were changed within the last 7 days.)
<P>
"""
# Version -- don't change unless you edit faqwiz.py
WIZVERSION = "1.0.4" # FAQ Wizard version
import os, sys
if os.name in ['nt',]:
# On NT we'll probably be running python from a batch file,
# so sys.argv[0] is not helpful
FAQCGI = 'faq.bat' # Relative URL of the FAQ cgi script
# LOGNAME is not typically set on NT
os.environ[ 'LOGNAME' ] = "FAQWizard"
else:
# This parameter is normally overwritten with a dynamic value
FAQCGI = 'faqw.py' # Relative URL of the FAQ cgi script
FAQCGI = os.path.basename(sys.argv[0]) or FAQCGI
del os, sys
# Perl (re module) style regular expression to recognize FAQ entry
# files: group(1) should be the section number, group(2) should be the
# question number. Both should be fixed width so simple-minded
# sorting yields the right order.
OKFILENAME = r"^faq(\d\d)\.(\d\d\d)\.htp$"
# Format to construct a FAQ entry file name
NEWFILENAME = "faq%02d.%03d.htp"
# Load local customizations on top of the previous parameters
try:
from faqcust import *
except ImportError:
pass
# Calculated parameter names
COOKIE_NAME = SHORTNAME + "-FAQ-Wizard" # Name used for Netscape cookie
FAQNAME = SHORTNAME + " FAQ" # Name of the FAQ
# ----------------------------------------------------------------------
# Anything below this point normally needn't be changed; you would
# change this if you were to create e.g. a French translation or if
# you just aren't happy with the text generated by the FAQ Wizard.
# Most strings here are subject to substitution (string%dictionary)
# RCS commands
import os
if os.name in ['nt', ]:
SH_RLOG = RCSBINDIR + "rlog %(file)s < NUL"
SH_RLOG_H = RCSBINDIR + "rlog -h %(file)s < NUL"
SH_RDIFF = RCSBINDIR + "rcsdiff -r%(prev)s -r%(rev)s %(file)s < NUL"
SH_REVISION = RCSBINDIR + "co -p%(rev)s %(file)s < NUL"
### Have to use co -l, or the file is not marked rw on NT
SH_LOCK = RCSBINDIR + "co -l %(file)s < NUL"
SH_CHECKIN = RCSBINDIR + "ci -u %(file)s < %(tfn)s"
else:
SH_RLOG = RCSBINDIR + "rlog %(file)s </dev/null 2>&1"
SH_RLOG_H = RCSBINDIR + "rlog -h %(file)s </dev/null 2>&1"
SH_RDIFF = RCSBINDIR + "rcsdiff -r%(prev)s -r%(rev)s %(file)s </dev/null 2>&1"
SH_REVISION = RCSBINDIR + "co -p%(rev)s %(file)s </dev/null 2>&1"
SH_LOCK = RCSBINDIR + "rcs -l %(file)s </dev/null 2>&1"
SH_CHECKIN = RCSBINDIR + "ci -u %(file)s <%(tfn)s 2>&1"
del os
# Titles for various output pages (not subject to substitution)
T_HOME = FAQNAME + " Wizard " + WIZVERSION
T_ERROR = "Sorry, an error occurred"
T_ROULETTE = FAQNAME + " Roulette"
T_ALL = "The Whole " + FAQNAME
T_INDEX = FAQNAME + " Index"
T_SEARCH = FAQNAME + " Search Results"
T_RECENT = "What's New in the " + FAQNAME
T_SHOW = FAQNAME + " Entry"
T_LOG = "RCS log for %s entry" % FAQNAME
T_REVISION = "RCS revision for %s entry" % FAQNAME
T_DIFF = "RCS diff for %s entry" % FAQNAME
T_ADD = "Add an entry to the " + FAQNAME
T_DELETE = "Deleting an entry from the " + FAQNAME
T_EDIT = FAQNAME + " Edit Wizard"
T_REVIEW = T_EDIT + " - Review Changes"
T_COMMITTED = T_EDIT + " - Changes Committed"
T_COMMITFAILED = T_EDIT + " - Commit Failed"
T_CANTCOMMIT = T_EDIT + " - Commit Rejected"
T_HELP = T_EDIT + " - Help"
# Generic prologue and epilogue
PROLOGUE = '''
<HTML>
<HEAD>
<TITLE>%(title)s</TITLE>
</HEAD>
<BODY
BGCOLOR="#FFFFFF"
TEXT="#000000"
LINK="#AA0000"
VLINK="#906A6A">
<H1>%(title)s</H1>
'''
EPILOGUE = '''
<HR>
<A HREF="%(HOMEURL)s">%(HOMENAME)s</A> /
<A HREF="%(FAQCGI)s?req=home">%(FAQNAME)s Wizard %(WIZVERSION)s</A> /
Feedback to <A HREF="mailto:%(OWNEREMAIL)s">%(OWNERNAME)s</A>
</BODY>
</HTML>
'''
# Home page
HOME = """
<H2>Search the %(FAQNAME)s:</H2>
<BLOCKQUOTE>
<FORM ACTION="%(FAQCGI)s">
<INPUT TYPE=text NAME=query>
<INPUT TYPE=submit VALUE="Search"><BR>
<INPUT TYPE=radio NAME=querytype VALUE=simple CHECKED>
Simple string
/
<INPUT TYPE=radio NAME=querytype VALUE=regex>
Regular expression
/<BR>
<INPUT TYPE=radio NAME=querytype VALUE=anykeywords>
Keywords (any)
/
<INPUT TYPE=radio NAME=querytype VALUE=allkeywords>
Keywords (all)
<BR>
<INPUT TYPE=radio NAME=casefold VALUE=yes CHECKED>
Fold case
/
<INPUT TYPE=radio NAME=casefold VALUE=no>
Case sensitive
<BR>
<INPUT TYPE=hidden NAME=req VALUE=search>
</FORM>
</BLOCKQUOTE>
<HR>
<H2>Other forms of %(FAQNAME)s access:</H2>
<UL>
<LI><A HREF="%(FAQCGI)s?req=index">FAQ index</A>
<LI><A HREF="%(FAQCGI)s?req=all">The whole FAQ</A>
<LI><A HREF="%(FAQCGI)s?req=recent">What's new in the FAQ?</A>
<LI><A HREF="%(FAQCGI)s?req=roulette">FAQ roulette</A>
<LI><A HREF="%(FAQCGI)s?req=add">Add a FAQ entry</A>
<LI><A HREF="%(FAQCGI)s?req=delete">Delete a FAQ entry</A>
</UL>
"""
# Index formatting
INDEX_SECTION = """
<P>
<HR>
<H2>%(sec)s. %(title)s</H2>
<UL>
"""
INDEX_ADDSECTION = """
<P>
<LI><A HREF="%(FAQCGI)s?req=new&amp;section=%(sec)s">Add new entry</A>
(at this point)
"""
INDEX_ENDSECTION = """
</UL>
"""
INDEX_ENTRY = """\
<LI><A HREF="%(FAQCGI)s?req=show&amp;file=%(file)s">%(title)s</A>
"""
LOCAL_ENTRY = """\
<LI><A HREF="#%(sec)s.%(num)s">%(title)s</A>
"""
# Entry formatting
ENTRY_HEADER1 = """
<HR>
<H2><A NAME="%(sec)s.%(num)s">%(title)s</A>\
"""
ENTRY_HEADER2 = """\
</H2>
"""
ENTRY_FOOTER = """
<A HREF="%(FAQCGI)s?req=edit&amp;file=%(file)s">Edit this entry</A> /
<A HREF="%(FAQCGI)s?req=log&amp;file=%(file)s">Log info</A>
"""
ENTRY_LOGINFO = """
/ Last changed on %(last_changed_date)s by
<A HREF="mailto:%(last_changed_email)s">%(last_changed_author)s</A>
"""
# Search
NO_HITS = """
No hits.
"""
ONE_HIT = """
Your search matched the following entry:
"""
FEW_HITS = """
Your search matched the following %(count)s entries:
"""
MANY_HITS = """
Your search matched more than %(MAXHITS)s entries.
The %(count)s matching entries are presented here ordered by section:
"""
# RCS log and diff
LOG = """
Click on a revision line to see the diff between that revision and the
previous one.
"""
REVISIONLINK = """\
<A HREF="%(FAQCGI)s?req=revision&amp;file=%(file)s&amp;rev=%(rev)s"
>%(line)s</A>\
"""
DIFFLINK = """\
(<A HREF="%(FAQCGI)s?req=diff&amp;file=%(file)s&amp;\
prev=%(prev)s&amp;rev=%(rev)s"
>diff -r%(prev)s -r%(rev)s</A>)\
"""
# Recently changed entries
NO_RECENT = """
<HR>
No %(FAQNAME)s entries were changed in the last %(period)s.
"""
VIEW_MENU = """
<HR>
View entries changed in the last...
<UL>
<LI><A HREF="%(FAQCGI)s?req=recent&amp;days=1">24 hours</A>
<LI><A HREF="%(FAQCGI)s?req=recent&amp;days=2">2 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&amp;days=3">3 days</A>
<LI><A HREF="%(FAQCGI)s?req=recent&amp;days=7">week</A>
<LI><A HREF="%(FAQCGI)s?req=recent&amp;days=28">4 weeks</A>
<LI><A HREF="%(FAQCGI)s?req=recent&amp;days=365250">millennium</A>
</UL>
"""
ONE_RECENT = VIEW_MENU + """
The following %(FAQNAME)s entry was changed in the last %(period)s:
"""
SOME_RECENT = VIEW_MENU + """
The following %(count)s %(FAQNAME)s entries were changed
in the last %(period)s, most recently changed shown first:
"""
TAIL_RECENT = VIEW_MENU
# Last changed banner on "all" (strftime format)
LAST_CHANGED = "Last changed on %c %Z"
# "Compat" command prologue (this has no <BODY> tag)
COMPAT = """
<H1>The whole %(FAQNAME)s</H1>
See also the <A HREF="%(FAQCGI)s?req=home">%(FAQNAME)s Wizard</A>.
<P>
"""
# Editing
EDITHEAD = """
<A HREF="%(FAQCGI)s?req=help">Click for Help</A>
"""
REVIEWHEAD = EDITHEAD
EDITFORM1 = """
<FORM ACTION="%(FAQCGI)s" METHOD=POST>
<INPUT TYPE=hidden NAME=req VALUE=review>
<INPUT TYPE=hidden NAME=file VALUE=%(file)s>
<INPUT TYPE=hidden NAME=editversion VALUE=%(editversion)s>
<HR>
"""
EDITFORM2 = """
Title: <INPUT TYPE=text SIZE=70 NAME=title VALUE="%(title)s"><BR>
<TEXTAREA COLS=72 ROWS=20 NAME=body>%(body)s
</TEXTAREA><BR>
Log message (reason for the change):<BR>
<TEXTAREA COLS=72 ROWS=5 NAME=log>%(log)s
</TEXTAREA><BR>
Please provide the following information for logging purposes:
<TABLE FRAME=none COLS=2>
<TR>
<TD>Name:
<TD><INPUT TYPE=text SIZE=40 NAME=author VALUE="%(author)s">
<TR>
<TD>Email:
<TD><INPUT TYPE=text SIZE=40 NAME=email VALUE="%(email)s">
<TR>
<TD>Password:
<TD><INPUT TYPE=password SIZE=20 NAME=password VALUE="%(password)s">
</TABLE>
<INPUT TYPE=submit NAME=review VALUE="Preview Edit">
Click this button to preview your changes.
"""
EDITFORM3 = """
</FORM>
"""
COMMIT = """
<INPUT TYPE=submit NAME=commit VALUE="Commit">
Click this button to commit your changes.
<HR>
"""
NOCOMMIT_HEAD = """
To commit your changes, please correct the following errors in the
form below and click the Preview Edit button.
<UL>
"""
NOCOMMIT_TAIL = """
</UL>
<HR>
"""
CANTCOMMIT_HEAD = """
Some required information is missing:
<UL>
"""
NEED_PASSWD = "<LI>You must provide the correct password.\n"
NEED_AUTHOR = "<LI>You must enter your name.\n"
NEED_EMAIL = "<LI>You must enter your email address.\n"
NEED_LOG = "<LI>You must enter a log message.\n"
CANTCOMMIT_TAIL = """
</UL>
Please use your browser's Back command to correct the form and commit
again.
"""
NEWCONFLICT = """
<P>
You are creating a new entry, but the entry number specified is not
correct.
<P>
The two most common causes of this problem are:
<UL>
<LI>After creating the entry yourself, you went back in your browser,
edited the entry some more, and clicked Commit again.
<LI>Someone else started creating a new entry in the same section and
committed before you did.
</UL>
(It is also possible that the last entry in the section was physically
deleted, but this should not happen except through manual intervention
by the FAQ maintainer.)
<P>
<A HREF="%(FAQCGI)s?req=new&amp;section=%(sec)s">Click here to try
again.</A>
<P>
"""
VERSIONCONFLICT = """
<P>
You edited version %(editversion)s but the current version is %(version)s.
<P>
The two most common causes of this problem are:
<UL>
<LI>After committing a change, you went back in your browser,
edited the entry some more, and clicked Commit again.
<LI>Someone else started editing the same entry and committed
before you did.
</UL>
<P>
<A HREF="%(FAQCGI)s?req=show&amp;file=%(file)s">Click here to reload
the entry and try again.</A>
<P>
"""
CANTWRITE = """
Can't write file %(file)s (%(why)s).
"""
FILEHEADER = """\
Title: %(title)s
Last-Changed-Date: %(date)s
Last-Changed-Author: %(author)s
Last-Changed-Email: %(email)s
Last-Changed-Remote-Host: %(REMOTE_HOST)s
Last-Changed-Remote-Address: %(REMOTE_ADDR)s
"""
LOGHEADER = """\
Last-Changed-Date: %(date)s
Last-Changed-Author: %(author)s
Last-Changed-Email: %(email)s
Last-Changed-Remote-Host: %(REMOTE_HOST)s
Last-Changed-Remote-Address: %(REMOTE_ADDR)s
%(log)s
"""
COMMITTED = """
Your changes have been committed.
"""
COMMITFAILED = """
Exit status %(sts)s.
"""
# Add/Delete
ADD_HEAD = """
At the moment, new entries can only be added at the end of a section.
This is because the entry numbers are also their
unique identifiers -- it's a bad idea to renumber entries.
<P>
Click on the section to which you want to add a new entry:
<UL>
"""
ADD_SECTION = """\
<LI><A HREF="%(FAQCGI)s?req=new&amp;section=%(section)s">%(section)s. %(title)s</A>
"""
ADD_TAIL = """
</UL>
"""
ROULETTE = """
<P>Hit your browser's Reload button to play again.<P>
"""
DELETE = """
At the moment, there's no direct way to delete entries.
This is because the entry numbers are also their
unique identifiers -- it's a bad idea to renumber entries.
<P>
If you really think an entry needs to be deleted,
change the title to "(deleted)" and make the body
empty (keep the entry number in the title though).
"""
# Help file for the FAQ Edit Wizard
HELP = """
Using the %(FAQNAME)s Edit Wizard speaks mostly for itself. Here are
some answers to questions you are likely to ask:
<P><HR>
<H2>I can review an entry but I can't commit it.</H2>
The commit button only appears if the following conditions are met:
<UL>
<LI>The Name field is not empty.
<LI>The Email field contains at least an @ character.
<LI>The Log message box is not empty.
<LI>The Password field contains the proper password.
</UL>
<P><HR>
<H2>What is the password?</H2>
At the moment, only PSA members will be told the password. This is a
good time to join the PSA! See <A
HREF="http://www.python.org/psa/">the PSA home page</A>.
<P><HR>
<H2>Can I use HTML in the FAQ entry?</H2>
Yes, if you include it in &lt;HTML&rt; and &lt;/HTML&gt; tags.
<P>
Also, if you include a URL or an email address in the text it will
automatigally become an anchor of the right type. Also, *word*
is made italic (but only for single alphabetic words).
<P><HR>
<H2>How do I delineate paragraphs?</H2>
Use blank lines to separate paragraphs.
<P><HR>
<H2>How do I enter example text?</H2>
Any line that begins with a space or tab is assumed to be part of
literal text. Blocks of literal text delineated by blank lines are
placed inside &lt;PRE&gt;...&lt;/PRE&gt;.
"""
# Load local customizations again, in case they set some other variables
try:
from faqcust import *
except ImportError:
pass
# Add your customizations here -- modified copies of what's in faqconf.py.
#!/usr/bin/env python3
"""FAQ wizard bootstrap."""
# This is a longer version of the bootstrap script given at the end of
# faqwin.py; it prints timing statistics at the end of the regular CGI
# script's output (so you can monitor how it is doing).
# This script should be placed in your cgi-bin directory and made
# executable.
# You need to edit the first line and the lines that define FAQDIR and
# SRCDIR, below: change /usr/local/bin/python to where your Python
# interpreter lives, change the value for FAQDIR to where your FAQ
# lives, and change the value for SRCDIR to where your faqwiz.py
# module lives. The faqconf.py and faqcust.py files live there, too.
import os
t1 = os.times() # If this doesn't work, just get rid of the timing code!
try:
FAQDIR = "/usr/people/guido/python/FAQ"
SRCDIR = "/usr/people/guido/python/src/Tools/faqwiz"
import os, sys
os.chdir(FAQDIR)
sys.path.insert(0, SRCDIR)
import faqwiz
except SystemExit as n:
sys.exit(n)
except:
t, v, tb = sys.exc_info()
print()
import cgi
cgi.print_exception(t, v, tb)
"""Generic FAQ Wizard.
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
class InvalidFile(FileError):
pass
class NoSuchSection(FileError):
def __init__(self, section):
FileError.__init__(self, NEWFILENAME %(section, 1))
self.section = section
class NoSuchFile(FileError):
def __init__(self, file, why=None):
FileError.__init__(self, file)
self.why = why
def escape(s):
s = s.replace('&', '&amp;')
s = s.replace('<', '&lt;')
s = s.replace('>', '&gt;')
return s
def escapeq(s):
s = escape(s)
s = s.replace('"', '&quot;')
return s
def _interpolate(format, args, kw):
try:
quote = kw['_quote']
except KeyError:
quote = 1
d = (kw,) + args + (faqconf.__dict__,)
m = MagicDict(d, quote)
return format % m
def interpolate(format, *args, **kw):
return _interpolate(format, args, kw)
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 emphasize(line):
return re.sub(r'\*([a-zA-Z]+)\*', r'<I>\1</I>', line)
revparse_prog = None
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
logon = 0
def log(text):
if logon:
logfile = open("logfile", "a")
logfile.write(text + "\n")
logfile.close()
def load_cookies():
if 'HTTP_COOKIE' not in os.environ:
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.parse
value = urllib.parse.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.parse
value = urllib.parse.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), end=' ')
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 ''
class UserInput:
def __init__(self):
self.__form = cgi.FieldStorage()
#log("\n\nbody: " + self.body)
def __getattr__(self, name):
if name[0] == '_':
raise AttributeError
try:
value = self.__form[name].value
except (TypeError, KeyError):
value = ''
else:
value = value.strip()
setattr(self, name, value)
return value
def __getitem__(self, key):
return getattr(self, key)
class FaqEntry:
def __init__(self, fp, file, sec_num):
self.file = file
self.sec, self.num = sec_num
if fp:
import email
self.__headers = email.message_from_file(fp)
self.body = fp.read().strip()
else:
self.__headers = {'title': "%d.%d. " % sec_num}
self.body = ''
def __getattr__(self, name):
if name[0] == '_':
raise AttributeError
key = '-'.join(name.split('_'))
try:
value = self.__headers[key]
except KeyError:
value = ''
setattr(self, name, value)
return value
def __getitem__(self, key):
return getattr(self, key)
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 good(self, file):
return self.__okprog.match(file)
def parse(self, file):
m = self.good(file)
if not m:
return None
sec, num = m.group(1, 2)
return int(sec), int(num)
def list(self):
# XXX Caller shouldn't modify result
self.__fill()
return self.__files
def open(self, file):
sec_num = self.parse(file)
if not sec_num:
raise InvalidFile(file)
try:
fp = open(file)
except IOError as msg:
raise NoSuchFile(file, msg)
try:
return self.entryclass(fp, file, sec_num)
finally:
fp.close()
def show(self, file, edit=1):
self.open(file).show(edit=edit)
def new(self, section):
if section not in SECTION_TITLES:
raise NoSuchSection(section)
maxnum = 0
for file in self.list():
sec, num = self.parse(file)
if sec == section:
maxnum = max(maxnum, num)
sec_num = (section, maxnum+1)
file = NEWFILENAME % sec_num
return self.entryclass(None, file, sec_num)
class FaqWizard:
def __init__(self):
self.ui = UserInput()
self.dir = FaqDir()
def go(self):
print('Content-type: text/html')
req = self.ui.req or 'home'
mname = 'do_%s' % req
try:
meth = getattr(self, mname)
except AttributeError:
self.error("Bad request type %r." % (req,))
else:
try:
meth()
except InvalidFile as exc:
self.error("Invalid entry file name %s" % exc.file)
except NoSuchFile as exc:
self.error("No entry with file name %s" % exc.file)
except NoSuchSection as exc:
self.error("No section number %s" % exc.section)
self.epilogue()
def error(self, message, **kw):
self.prologue(T_ERROR)
emit(message, kw)
def prologue(self, title, entry=None, **kw):
emit(PROLOGUE, entry, kwdict=kw, title=escape(title))
def epilogue(self):
emit(EPILOGUE)
def do_home(self):
self.prologue(T_HOME)
emit(HOME)
def do_debug(self):
self.prologue("FAQ Wizard Debugging")
form = cgi.FieldStorage()
cgi.print_form(form)
cgi.print_environ(os.environ)
cgi.print_directory()
cgi.print_arguments()
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 = [_f for _f in re.split('\W+', query) if _f]
if not words:
self.error("No keywords specified!")
return
words = [r'\b%s\b' % w for w in 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 do_all(self):
self.prologue(T_ALL)
files = self.dir.list()
self.last_changed(files)
self.format_index(files, localrefs=1)
self.format_all(files)
def do_compat(self):
files = self.dir.list()
emit(COMPAT)
self.last_changed(files)
self.format_index(files, localrefs=1)
self.format_all(files, edit=0)
sys.exit(0) # XXX Hack to suppress epilogue
def last_changed(self, files):
latest = 0
for file in files:
entry = self.dir.open(file)
if entry:
mtime = mtime = entry.getmtime()
if mtime > latest:
latest = mtime
print(time.strftime(LAST_CHANGED, time.localtime(latest)))
emit(EXPLAIN_MARKS)
def format_all(self, files, edit=1, headers=1):
sec = 0
for file in files:
try:
entry = self.dir.open(file)
except NoSuchFile:
continue
if headers and entry.sec != sec:
sec = entry.sec
try:
title = SECTION_TITLES[sec]
except KeyError:
title = "Untitled"
emit("\n<HR>\n<H1>%(sec)s. %(title)s</H1>\n",
sec=sec, title=title)
entry.show(edit=edit)
def do_index(self):
self.prologue(T_INDEX)
files = self.dir.list()
self.last_changed(files)
self.format_index(files, add=1)
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([mtime_file[1] for mtime_file in 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 do_help(self):
self.prologue(T_HELP)
emit(HELP)
def do_show(self):
entry = self.dir.open(self.ui.file)
self.prologue(T_SHOW)
entry.show()
def do_add(self):
self.prologue(T_ADD)
emit(ADD_HEAD)
sections = sorted(SECTION_TITLES.items())
for section, title in sections:
emit(ADD_SECTION, section=section, title=title)
emit(ADD_TAIL)
def do_delete(self):
self.prologue(T_DELETE)
emit(DELETE)
def do_log(self):
entry = self.dir.open(self.ui.file)
self.prologue(T_LOG, entry)
emit(LOG, entry)
self.rlog(interpolate(SH_RLOG, entry), entry)
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_revision(self):
entry = self.dir.open(self.ui.file)
rev = self.ui.rev
mami = revparse(rev)
if not mami:
self.error("Invalid revision number: %r." % (rev,))
self.prologue(T_REVISION, entry)
self.shell(interpolate(SH_REVISION, entry, rev=rev))
def do_diff(self):
entry = self.dir.open(self.ui.file)
prev = self.ui.prev
rev = self.ui.rev
mami = revparse(rev)
if not mami:
self.error("Invalid revision number: %r." % (rev,))
if prev:
if not revparse(prev):
self.error("Invalid previous revision number: %r." % (prev,))
else:
prev = '%d.%d' % (mami[0], mami[1])
self.prologue(T_DIFF, entry)
self.shell(interpolate(SH_RDIFF, entry, rev=rev, prev=prev))
def shell(self, command):
output = os.popen(command).read()
sys.stdout.write('<PRE>')
print(escape(output))
print('</PRE>')
def do_new(self):
entry = self.dir.new(section=int(self.ui.section))
entry.version = '*new*'
self.prologue(T_EDIT)
emit(EDITHEAD)
emit(EDITFORM1, entry, editversion=entry.version)
emit(EDITFORM2, entry, load_my_cookie())
emit(EDITFORM3)
entry.show(edit=0)
def do_edit(self):
entry = self.dir.open(self.ui.file)
entry.load_version()
self.prologue(T_EDIT)
emit(EDITHEAD)
emit(EDITFORM1, entry, editversion=entry.version)
emit(EDITFORM2, entry, load_my_cookie())
emit(EDITFORM3)
entry.show(edit=0)
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 cantcommit(self):
self.prologue(T_CANTCOMMIT)
print(CANTCOMMIT_HEAD)
self.errordetail()
print(CANTCOMMIT_TAIL)
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()
try:
os.unlink(file)
except os.error:
pass
try:
f = open(file, 'w')
except IOError as why:
self.error(CANTWRITE, file=file, why=why)
return
date = time.ctime(now)
emit(FILEHEADER, self.ui, os.environ, date=date, _file=f, _quote=0)
f.write('\n')
f.write(self.ui.body)
f.write('\n')
f.close()
import tempfile
tf = tempfile.NamedTemporaryFile()
emit(LOGHEADER, self.ui, os.environ, date=date, _file=tf)
tf.flush()
tf.seek(0)
command = interpolate(SH_CHECKIN, file=file, tfn=tf.name)
log("\n\n" + command)
p = os.popen(command)
output = p.read()
sts = p.close()
log("output: " + output)
log("done: " + str(sts))
log("TempFile:\n" + tf.read() + "end")
if not sts:
self.prologue(T_COMMITTED)
emit(COMMITTED)
else:
self.error(T_COMMITFAILED)
emit(COMMITFAILED, sts=sts)
print('<PRE>%s</PRE>' % escape(output))
try:
os.unlink(tf.name)
except os.error:
pass
entry = self.dir.open(file)
entry.show()
wiz = FaqWizard()
wiz.go()
#!/bin/sh
#
# Christian Reis <kiko@async.com.br>
#
# Moves
#
# Example:
#
# blackjesus:~> ./move-faqwiz.sh 2\.1 3\.2
# Moving FAQ question 02.001 to 03.002
if [ x$2 = x ]; then
echo "Need 2 args: original_version final_version."
exit 2
fi
if [ ! -d data -o ! -d data/RCS ]; then
echo "Run this inside the faqwiz data/ directory's parent dir."
exit 2
fi
cut_n_pad() {
t=`echo $1 | cut -d. -f $2`
export $3=`echo $t | awk "{ tmp = \\$0; l = length(tmp); for (i = 0; i < $2-l+1; i++) { tmp = "0".tmp } print tmp }"`
}
cut_n_pad $1 1 prefix1
cut_n_pad $1 2 suffix1
cut_n_pad $2 1 prefix2
cut_n_pad $2 2 suffix2
if which tempfile >/dev/null; then
tmpfile=$(tempfile -d .)
elif [ -n "$RANDOM" ]; then
tmpfile=tmp$RANDOM.tmp
else
tmpfile=tmp$$.tmp
fi
file1=faq$prefix1.$suffix1.htp
file2=faq$prefix2.$suffix2.htp
echo "Moving FAQ question $prefix1.$suffix1 to $prefix2.$suffix2"
sed -e "s/$1\./$2\./g" data/$file1 > ${tmpfile}1
sed -e "s/$1\./$2\./g" data/RCS/$file1,v > ${tmpfile}2
if [ -f data/$file2 ]; then
echo "Target FAQ exists. Won't clobber."
exit 2
fi
mv ${tmpfile}1 data/$file2
mv ${tmpfile}2 data/RCS/$file2,v
mv data/$file1 data/$file1.orig
mv data/RCS/$file1,v data/RCS/$file1,v.orig
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment