#!/usr/bin/env python3.0
"""
License under the GPLv3. If you do not have a copy of it, you can read it at http://www.gnu.org/licenses/gpl-3.0.html .
Instructions for use are located two comment strings down.
If you make any changes, please append a log of your name, date, contact information, and changes or other information here:
James Royston. May 30, 2009. purpleposeidon at gmail.com.
Initial version. I got the idea from the documentation from... some python CMS project. Unfortunately, I can't find it. It would be good if someone could, because they did a munch better job of it. Scratch that, this is cooler.
It was written using python3.0 (originally python2.5) on a FreeBSD machine.
"""
"""
TODO:
Better captcha?
Maybe use a REAL database?
Email-notification?
"""
"""
HOW TO SET UP A BOOK
[0]
First, change the variable 'src' to whatever is appropriate for you.
[1]
Create a directory for the html files in your book, under the 'src' directory defined in this program. Make the directory write-able by the server, and also every sub-directory.
[2]
Next, edit each html page. See the "How to Markup a File", below. You must provide for something like the value of "INSERTSTYLE", (defined way below in the program), to be included in the header. Then, you must mark out where the comments will be inserted. The file is going to have comments, then it must have a COM, and an ENDCOM.
If you don't provide labels for the COM's, they'll be provided automatically, and counted off as '0', '1', '2'... If you need to re-structure the comments, uhm, remember this. If you just insert an un-labeled COM section, everything below will be shoved down. If you are merely adding a new comment section, you can just give it a unique tag-name.
The tagnames are used to refer to the comment in the html, and in the databases, so be sure to not use excessive or odd characters.
[3]
Once you've marked up all of your files, you'll probably want to create a .index file. In the directory, create a file, named .index, that has the name of the file you want the user to be sent to. (You might want to provide a link inside the file reffered to be .index that points up a level...)
HOW TO MARKUP A FILE
These texts must be on a line, lonely and all bythemselves, just like me su zo'osi.
INSERTSTYLE - Put this inside the header somewhere, it is replaced with the required CSS and javascript
INSERTCSS - Put this inside the the header somewhere, it is where the CSS is embedded
INSERTJS - Put this inside the header somewhere, it is where the javascript is embedded
COM label - Use this to start a comment, giving it the tag 'label'
COMSEP label - Use this to end the previous comment, and start a new one
ENDCOM - Use this to end the comment
You don't have to include the label. If it is excluded, it is replaced with the comment-number.
These commands can (and probably should) be enclosed in comments, like so:
"""
### Configuration variables
src = '/home/public/docsrc'
basename = '/docs'
src_basename = '/docsrc/'
admin_passwords = '/home/protected/jbo_ralju'
bad_words = "lol rofl lmao wtf roflmao damn fuck shit cunt".split()+['lojban sucks']
allowed_tags = "b, u, i, strike, big, small, p, br, table, thead, tfoot, tbody, tr, td, ol, ul, li, blockquote, br, center, cite, code, dl, dt, dd, em, h1, h2, h3, h4, h5, h6, del, ins, em, strong, dfn, code, samp, kbd, pre, var, cite, blockquote, hr"
###And now the code begins
import time
start = time.time()
import shelve
import cgi
import cgitb; cgitb.enable()
import os
import sys
import re
form = cgi.FieldStorage()
use_html = False
html_message = "Content-type: text/html"
if 'HTTP_USER_AGENT' in os.environ:
ua = os.environ['HTTP_USER_AGENT']
crap_ie = 'MSIE' in ua
#crap_ie = 'MSIE 6' in ua or 'MSIE 5' in ua
else:
crap_ie = False
def html():
global use_html
use_html = True
print(html_message)
print("Cache-Control: public") #XXX - make this work with NFS's caching servers...
print()
def message(msg, newurl, t):
html()
print("""
")
# print("How do you write a proper 404 error?")
raise SystemExit()
def noban():
#Blacklist to keep spammers/trolls from writing comments
try:
bh = open('banhammer').read()
except: return
if os.environ['REMOTE_ADDR'] in bh:
html()
print("You are banned. GTFO.")
raise SystemExit
def str_comment(file_path, tag, com_num, com):
#name, text, date, IP
#pack = list(cgi.escape(field) for field in com)
name, text, date, ip, has_errata, *padding = com #This line is why I converted to 3.0 zo'o....nai
name, text, date, ip = (cgi.escape(field) for field in (name, text, date, ip))
text = format_comment(com[1])
head = '%s (%s) on %s' % (name, ip, date)
if has_errata:
head += ' has noted an error'
body = '
' % (tag, tag))
if comments:
i = 0
for com in comments:
print(str_comment(f, tag, i, com))
i += 1
else:
print("(No comment)")
print(" ")
comment_form(f, tag)
print("
")
print("
")
def close_paragraph():
print("
")
def file_dir():
d = os.walk(src).send(None)[2] #All the files. Only files.
for item in d:
if item.startswith('.') or '/' in item or '@' in item: continue
yield item
def safe_file_dir():
v = os.getcwd()
os.chdir(src)
for set in os.walk('./'):
dir_name = set[0].replace('./', '')
if dir_name and ('@' in dir_name or dir_name[0] == '.'): continue
for f in set[2]:
if '@' in f or f[0] == '.': continue
yield os.path.join(dir_name, f)
for d in set[1]:
if '@' in d or d[0] == '.': continue
yield os.path.join(dir_name, d)+'/'
os.chdir(v)
def dir_dir():
d = os.walk(src).send(None)[1] #All of the directories
for item in d:
a = os.walk(os.path.join(src, item)).send(None)
if len(a[1])+len(a[2]) and not item.startswith('.'):
yield item
def doc_dir(subpath=''):
try:
if subpath.endswith('/') or 1:
index_file = os.path.join(src, '.index')
use_this_file = open(index_file).read().strip()
if use_this_file in safe_file_dir():
show_file(use_this_file)
return
else:
html()
print("Hey! The .index file is invalid.")
print("
")
print("The index link is: ", repr(use_this_file))
print("Valid index links:")
for i in safe_file_dir():
print(' ', i)
print("
")
return
except IOError:
pass
#Print list of documents
html()
link = basename
if subpath: link = os.path.join(link, subpath)
print("""
Lojban Book Discussion
%s
""" % INSERTSTYLE)
file_headers = False
dir_headers = False
for d in dir_dir():
if not dir_headers:
print('
Directories:
')
if d and subpath:
print('Parent Directory ')
dir_headers = True
try:
#if 1:
name = d
detail = ''
f = open(os.path.join(src, d)+'/.name').read().strip()
f = f.split('\n')
name = f[0]
detail = f[1]
except:
pass
print('%s%s ' % (link, d, name, detail))
if not dir_headers and subpath:
print('Parent Directory')
for d in file_dir():
if not file_headers:
print("
Simple tags are allowed: %s. They can't have attributes, because I'm too lazy to write something that allows that. Two newlines are replaced with line breaks.
The comments are stored using shelve. You can open a comments database in python by using shelve.open.
""" % (allowed_tags, basename, src_basename) )
def load_comments(f):
if not '.comments' in f:
n = os.path.split(f)
name = os.path.join(n[0], '.'+n[1]+'.comments')
else:
if '.db' in f:
f = f.replace('.db', '')
name = f
try:
return shelve.open(os.path.join(src, name), writeback=True)
except Exception as e:
print("Unable to open database for comments! Send your site admin some love, and it will be fixed.")
print("Erruh:", e)
print("File:", os.path.join(os.path.join(src, name)))
raise SystemExit
allowed = allowed_tags.split(', ')
def replace_urls(text):
#http://stackoverflow.com/questions/476478/python-regular-expression-to-add-links-to-urls
#I am so lazy. I love it.
r1 = r"(\b(http|https)://([-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_()|]))"
r2 = r"((^|\b)www\.([-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_()|]))"
return re.sub(r2,r'\1',re.sub(r1,r'\1',text))
def format_comment(v):
v = cgi.escape(v)
return v
#XXXXXXXXXXXX oh god what did I do
#urlre = re.compile(r"https?://([-\w\.]+)+(:\d+)?(/([\w/_\.]*(\?\S+)?)?)?")
a = []
for i in allowed:
a.append("<%s>" % i)
a.append("%s>" % i)
#a.append("<%s />" % i)
#a.append("<%s/>" % i)
v = cgi.escape(v)
for i in a:
v = v.replace(cgi.escape(i), i)
v = v.replace('\r', '')
v = v.replace('\n\n', ' ')
v = replace_urls(v)
return v
def label(_for, lbl):
#Because IE sucks immensely. >:(
#http://www.adobe.com/cfusion/communityengine/index.cfm?event=showdetails&postId=161&productId=1
#Ain't workin'
if not crap_ie:
print('')
else:
print(lbl)
def comment_form(f, tag):
f = os.path.basename(f)
print('')
#TODO: Deny the password if it can be used with the given name to login gmail zo'o
JS = """
"""
#TODO: Open a file, hmm? Or, better yet...
CSS = """
"""
INSERTSTYLE = CSS + JS
def show_file(f):
html()
line_no = 0
started_coms = False
pure_lojban = ...
com_db = load_comments(f)
for line in open(os.path.join(src, f)):
if '' in line:
line = line.replace("", '')
if (pure_lojban in (True, ...)) and (line.strip().startswith("i ") or line.strip().startswith("ni'o") or line.strip().startswith(".i ")):
pure_lojban = True
if started_coms:
close_paragraph()
else:
#Begining of a pure-lojban text. It needs formatting.
started_coms = True
print("""
%s
%s
""" % (f, INSERTSTYLE)) #Should probably zoi-quote the html or something
line_no += 1
tag = str(line_no)
if tag in com_db:
c = com_db[tag]
else:
c = []
open_paragraph(basename+'/'+f, tag, c)
print(line, ' ')
elif line.startswith('COM'):
if pure_lojban == True:
raise Exception("I do not want you to mix lojban with non-lojban!")
pure_lojban = False
if line.startswith("COMSEP"):
if started_coms:
tag = line.strip()[6:].strip()
close_paragraph()
else:
tag = line.strip()[3:]
if tag == 'END': #Not really correct.
close_paragraph()
if not tag:
tag = str(line_no)
line_no += 1
if tag in com_db:
c = com_db[tag]
else:
c = []
open_paragraph(basename+'/'+f, tag, c)
started_coms = True
elif 'ENDCOM' == line.strip():
if pure_lojban == True:
raise Exception("I do not want you to mix lojban with non-lojban!")
close_paragraph()
elif 'INSERTSTYLE' == line.strip():
if pure_lojban == True:
raise Exception("I do not want you to mix lojban with non-lojban!")
print(INSERTSTYLE)
elif 'INSERTJS' == line.strip():
if pure_lojban == True:
raise Exception("I do not want you to mix lojban with non-lojban!")
print(JS)
elif 'INSERTCSS' == line.strip():
if pure_lojban == True:
raise Exception("I do not want you to mix lojban with non-lojban!")
print(CSS)
else:
print(line.replace('\n', ''))
if pure_lojban == True:
print(" ")
if pure_lojban == True:
#Stop the formatting! Aghh!
close_paragraph()
print("""
""")
def comment_strip(s):
#And please don't game the system. :(
return s.strip().lower().replace(' ', '').replace('\t', '').replace('\n', '').replace('"', '').replace("'", '').replace('&', '')
def save_comment(f, tag, comment, who, err, password=None):
noban() #Block banned addresses from commenting.
if len(comment) > 1024:
message("You can't save a comment that long.", "http://www.gutenberg.org/etext/2600", 4)
ip = os.environ["REMOTE_ADDR"]
now = time.asctime()
db = load_comments(f)
if comment_strip(comment) in bad_words:
redirect("http://images.google.com/images?q=%s" % cgi.escape(comment))
if not tag in db: val = []
else: val = db[tag]
naked_comment = comment_strip(comment)
for entry in val:
if comment_strip(entry[1]) == naked_comment:
html()
print("That has already been said here.")
if entry[1] == comment and entry[0] == who:
print("Silly you!")
raise SystemExit #No redirection, except maybe javascript history...
val.append([who, comment, now, ip, err, password])
db[tag] = val
db.sync()
#name, text, date, IP, has_an_error
def error_comment(com_list):
for com in com_list:
if com[4]:
return True
return False
def delete_comment(db, tag, num):
comment_list = db[tag]
comment_list.pop(num)
db[tag] = comment_list
db.sync()
def edit_comment(db, tag, com_num, value, by_admin):
if by_admin:
append_text = "\nEdited %s by admin." % (time.asctime())
else:
ip = os.environ["REMOTE_ADDR"]
append_text = "\nEdited %s from %s." % (time.asctime(), ip)
value += append_text
db[tag][com_num][1] = value
db.sync()
def modify_comment(f, link):
db = load_comments(f)
if 'tag' not in form:
html()
print("What? There is no line selected.")
raise SystemExit()
tag = cgi.escape(form['tag'].value) #A string
comment_list = db[tag]
if 'com_num' not in form:
html()
print("What? There is no comment selected.")
raise SystemExit()
try:
com_num = int(form['com_num'].value) #MUST be an int
except ValuError:
html()
print(".u'iru'e ga'i")
raise SystemExit()
if 'password' in form:
password = form['password'].value
real_password = comment_list[com_num][5]
time.sleep(2)
by_admin = False #Who edited it?
if password != real_password:
if password not in open(admin_passwords).read().split('\n'):
message("The password you entered is invalid.", link, 5)
#print("The password you entered is invalid.")
#raise SystemExit()
else:
by_admin = True
if "change" in form and not ("del" in form and form['del'].value == 'y'):
#Modify the comment.
edit_comment(db, tag, com_num, form["change"].value, by_admin)
message("Comment changed!", link, 5)
#print("Comment changed!")
#raise SystemExit()
else:
delete_comment(db, tag, com_num)
message("Dacheated!", link, 3)
#print("Dacheated!")
#raise SystemExit()
else:
html()
print("""
Delete comment?
%s
Edit Comment:
""" % (INSERTSTYLE, link, cgi.escape(comment_list[com_num][1]), tag, int(com_num)) )
def main():
if 'PATH_INFO' in os.environ:
e = os.environ['PATH_INFO'].replace('doc', '') #.replace('/', '')
if e.startswith('/'): e = e[1:]
if '#' in e:
e = e[:e.find('#')]
if e == '@SOURCE':
print("Content-type: text/plain;\n")
print(open('doc.cgi').read())
raise SystemExit
if e in safe_file_dir(): #We can trust the given path
if e.endswith('/'): #List a directory
subpath = ''
global src
e = e.strip('/')
src = os.path.join(src, e)
subpath = e
doc_dir(subpath)
else: #Show a file...
if 'modify' in form:
modify_comment(e, basename+'/'+e)
elif 'line' in form:
if not 'human' in form:
html()
print("You have not proven yourself to be human. Put a '1' in that little box.")
raise SystemExit
elif form['human'].value != '1':
html()
print("It seems you've forgotten your humanity.")
raise SystemExit
if not 'say' in form:
html()
print("What were you going to say?")
raise SystemExit
else:
say = str(form['say'].value)
if not 'name' in form:
name = 'Anon'
else:
name = str(form['name'].value)
err = False
if 'err' in form:
if form['err'].value.strip() == '1':
err = True
password = None
if 'password' in form:
password = form['password'].value
tag = str(form['line'].value)
sys.stdout.flush()
save_comment(e, tag, say, name, err, password=password)
redirect(basename+'/'+str(e)+'#'+str(tag))
else:
show_file(e)
raise SystemExit
elif e.replace('/', '') == '':
doc_dir()
else:
error404()
print('Content-type: text/plain;\n')
print("That doesn't seem safe.\n")
print("Here's a list of all the files and directories that may be accessed:")
for i in list(safe_file_dir()):
print(' *', i)
else:
doc_dir()
def show_comment_item(section, val):
username, text, date, ip, has_err, password, *other = val
print("----\n%r (%s) on %s, section %s. Error: %s. Password: %r"%(username, ip, date, section, has_err, password))
print(" >>",text)
def comment_dump(f):
db = load_comments(f)
if not len(db):
#print("No items in", f)
return
else:
print("\n\nItems in", f, ': =============================')
for key in db:
for com in db[key]:
show_comment_item(key, com)
if __name__ == '__main__':
if len(sys.argv) != 1:
print("")
import glob
os.chdir(src)
for arg in sys.argv[1:]:
arg = os.path.join(src, arg)
g = glob.glob(arg)
for arg2 in g:
comment_dump(arg2)
raise SystemExit
_e = None
if 1:
# try:
main()
# except Exception as e:
# _e = e
# finally:
if use_html and 1:
print("If you tried /doc_static/ instead, you could browse faster")
# print("Approximate script execution time:", time.time() - start)
# print("
Warning: Still developing features and such, don't become too attatched to your comments.