#!/usr/bin/env python
import cgi, os, sys, new, MySQLdb, urllib, urlparse, time, sha, base64
from xml.dom import minidom
from xml.sax import saxutils
from minidomhack import parseString # workaround for Python 2.2, this is fixed in 2.3
# harcoded constants
BLOG_ID = 14
AUTHOR_ID = 2
USERNAME = "joe"
PASSWORD = "fred"
PRIVATE_KEY = os.environ['HTTP_HOST'] # feel free to change this to anything
REALM = "dive into atom"
# database stuff
from ConfigParser import ConfigParser
config = ConfigParser()
filename = os.path.expanduser('~/db.conf')
config.read([os.path.expanduser('~/db.conf')])
connectionparams = {}
for o in config.options('weblog'):
connectionparams[o] = config.get('weblog', o)
dbhandle = apply(MySQLdb.connect, (), connectionparams)
cursor = dbhandle.cursor()
def q(sql):
cursor.execute(sql)
def car(t): return t[0]
columnNames = map(car, cursor.description)
rows = []
for rowdata in cursor.fetchall():
thisrow = {}
for name, value in map(None, columnNames, rowdata):
thisrow[name] = value
rows.append(thisrow)
return rows
def x(sql):
cursor.execute(sql)
def escapeParam(s):
import types
if type(s) in types.StringTypes:
s = s.replace("'", "''").replace("\\", "\\\\")
s = "'%s'" % s
else:
s = str(s)
return s
def utcdate(datetime):
format = '%Y-%m-%dT%H:%M:%SZ'
if hasattr(datetime, 'strftime'):
return datetime.strftime(format)
else:
return time.strftime(format, datetime)
def insert(table, params):
keystr = ", ".join(params.keys())
valuestr = ", ".join(map(escapeParam, params.values()))
sql = "insert into %s (%s) values (%s)" % (table, keystr, valuestr)
x(sql)
def update(table, whereclause, params):
paramstr = ", ".join(["%s=%s" % (k, escapeParam(v)) for k, v in params.items()])
sql = "update %s set %s where %s" % (table, paramstr, whereclause)
print 'Content-type: text/plain'
print
print sql
x(sql)
# add useful 'first' method to minidom.Element class
def _first(self, path):
"""get first node, possibly recursively
this is like a poor-man's XPath; the path parameter can
look like this: "entry/title", which will find the first
'entry' node off self, then the first 'title' node off
the entry node"""
element = self
for name in path.split("/"):
elements = element.all(name)
if not elements:
return None
element = elements[0]
return element
minidom.Element.first = new.instancemethod(_first, None, minidom.Element)
# add useful 'text' method to minidom.Element class
def _text(self):
"""returns all text of a node in one string
text may be split into several Text nodes; minidom likes to create
separate Text nodes for carriage returns and ampersands and things"""
def isTextNode(node):
return isinstance(node, minidom.Text)
def getData(node):
return node.data
try:
return "".join(map(getData, filter(isTextNode, self.childNodes)))
except:
return ""
minidom.Element.text = new.instancemethod(_text, None, minidom.Element)
# duplicate 'getElementsByTagName' as 'all' because I'm lazy
minidom.Element.all = minidom.Element.getElementsByTagName
def buildEntry(params):
s = """"""
for k, v in params.items():
s += "\n"
if k == 'content':
s += ''
else:
s += '<%s>' % k
s += saxutils.escape(v)
s += "%s>" % k
s += "\n"
return s
# MT integration (and I use this word loosely)
def rebuild(blog_id=BLOG_ID, entry_id=None):
if entry_id:
cmd = 'cd /home/mark/web/diveintomark.org/mt/; ./mt-rebuild.pl -mode="entry" -blog_id="%s" -entry_id="%s"' % (blog_id, entry_id)
pipe = os.popen(cmd)
pipe.read()
pipe.close()
cmd = 'cd /home/mark/web/diveintomark.org/mt/; ./mt-rebuild.pl -mode="index" -blog_id="%s" -template="Main Index"' % blog_id
pipe = os.popen(cmd)
pipe.read()
pipe.close()
LOGFILE = '/home/mark/web/diveintomark.org/tmp/atom.log'
def log(line):
fsock = open(LOGFILE, 'a')
fsock.write(line)
fsock.write('\n')
fsock.close()
class Handler:
code_description = {404: 'Not found',
200: 'OK',
201: 'Created',
301: 'Permanent redirect',
302: 'Redirect',
500: 'Internal server error',
447: 'Atom unauthorized'}
def content_type(self, value):
print 'Content-type: %s' % value
def message(self, msg):
self.content_type('text/plain')
print
print msg
print ' ' * (512 - len(msg)) # workaround for IE
print
def getnonce(self):
timestamp = str(time.time())
etag = sha.new(os.environ['SCRIPT_FILENAME']).hexdigest()
privatekey = PRIVATE_KEY
return base64.encodestring("%s %s" % (timestamp, sha.new("%s:%s:%s" % (timestamp, etag, privatekey)).hexdigest())).strip()
def status(self, code, reauth=0, stop=0, msg=None):
print 'Status: %s %s' % (code, self.code_description.get(code, ''))
if reauth:
print 'Atom-Authentication-Info: nextnonce="%s"' % self.getnonce()
if msg:
self.message(msg)
if stop:
sys.exit(0)
class AtomHandler(Handler):
def __init__(self, blog_id=None, entry_id=None, method=os.environ.get('REQUEST_METHOD'), inputStream=sys.stdin):
log('----- begin -----')
# sanity-check blog_id
if not blog_id:
self.status(404, stop=1, msg="need blog_id")
try:
blog_id = int(blog_id)
except:
self.status(500, stop=1, msg="bogus blog_id")
if blog_id != BLOG_ID: # hardcoded
self.status(403, stop=1, msg="not allowed to edit that blog")
self.blog_id = blog_id
# sanity-check method
if not method:
self.status(500, stop=1, msg="no request method")
handler = getattr(self, 'do_%s' % method)
if not handler:
self.status(500, stop=1, msg="unknown request method")
self.method = method
# authenticate (POST/PUT/DELETE only for demonstration purposes, real implementation might also protect GET)
if method in ('POST', 'PUT', 'DELETE'):
self.authenticate()
# sanity-check entry_id
if method != 'POST':
try:
entry_id = int(entry_id)
except:
self.status(500, reauth=1, stop=1, msg="bogus entry_id")
data = q("""select 1 from mt_entry where entry_blog_id = %s and entry_id = %s limit 1""" % (self.blog_id, entry_id))
if not data:
self.status(404, reauth=1, stop=1, msg="unknown entry_id")
self.entry_id = entry_id
# parse body
if method in ('POST', 'PUT'):
try:
rawxml = inputStream.read()
log(rawxml)
body = parseString(rawxml).firstChild
except:
self.status(500, reauth=1, stop=1, msg="input is not well-formed XML")
else:
body = None
# call appropriate handler
handler(body)
log('---- end -----\n')
def authfailure(self, statuscode=447, msg=None):
self.status(statuscode)
print 'Atom-Authenticate: Digest realm="%s", qop="atom-auth", algorithm="SHA", nonce="%s"' % (REALM, self.getnonce())
if msg: self.message(msg)
sys.exit(0)
def authenticate(self):
log('authenticating')
authrequest = os.environ.get('HTTP_ATOM_AUTHORIZATION') or ''
if not authrequest:
self.authfailure(447, msg="Atom authentication required, see Atom-Authenticate header for details")
log(authrequest)
if not authrequest.startswith('Digest '):
self.authfailure(400, msg='''Atom-Authorization must start with "Digest "''')
authrequest = authrequest.split('Digest ', 1)[1]
authrequest = authrequest.strip()
items = authrequest.split(", ")
keyvalues = [i.split("=", 1) for i in items]
keyvalues = [(k.strip(), v.strip().replace('"', '')) for k, v in keyvalues]
params = dict(keyvalues)
if not params.get("username"):
self.authfailure(400, msg="Atom-Authorization must include username")
if not params.get("realm"):
self.authfailure(400, msg="Atom-Authorization must include realm")
if params.get("realm") != REALM:
self.authfailure(400, msg="Atom-Authorization includes wrong realm")
if not params.get("nonce"):
self.authfailure(400, msg="Atom-Authorization must include nonce")
# TODO - more here to validate that we gave out this nonce, that we gave it out recently, and that it hasn't already been used in a response
if not params.get("uri"):
self.authfailure(400, msg="Atom-Authorization must include uri")
if not params.get("qop"):
self.authfailure(400, msg="Atom-Authorization must include qop")
if params.get("qop") != "atom-auth":
self.authfailure(400, msg="Atom-Authorization includes unknown qop")
if not params.get("nc"):
self.authfailure(400, msg="Atom-Authorization must include nc")
if params.get("nc") != "00000001":
self.authfailure(400, msg="Atom-Authorization includes unacceptable nc value (should be 00000001)")
if not params.get("cnonce"):
self.authfailure(400, msg="Atom-Authorization must include cnonce")
if not params.get("response"):
self.authfailure(400, msg="Atom-Authorization must include response")
a1 = params["username"] + ":" + params["realm"] + ":" + PASSWORD
a2 = self.method + ":" + params["uri"]
raw = sha.new(a1).hexdigest() + ":" + params["nonce"] + ":" + params["nc"] + ":" + params["cnonce"] + ":" + params["qop"] + ":" + sha.new(a2).hexdigest()
# if params.get("raw"):
# log(params['raw']) # for debugging only
# log(raw)
# if raw != params["raw"]:
# log('raw does not match')
# else:
# log('raw data matches')
expectedresponse = sha.new(raw).hexdigest()
if expectedresponse != params.get('response'):
# log("expectedresponse=" + expectedresponse + ">>EOL")
# log("response=" + params.get('response') + ">>EOL")
# log('-----')
self.authfailure(403, msg="Authorization failed, Atom-Authorization response does not match expected value")
# log('authentication succeeded')
def _getentrytext(self, body):
content = body.first('content')
if not content:
return None
entry_text = ''
mode = content.getAttribute('mode') or 'xml'
if mode == 'xml':
children = [e for e in content.childNodes if isinstance(e, minidom.Element)]
if children:
firstchild = children[0]
entry_text = firstchild.toxml()
elif mode == 'base64':
pass # TODO
elif mode == 'escaped':
entry_text = content.text()
else:
self.status(500, reauth=1, stop=1, msg="invalid mode attribute on content")
return entry_text
def do_POST(self, body):
"""new entry"""
entry_title = body.first('title') or ''
if entry_title:
entry_title = entry_title.text()
entry_summary = body.first('summary') or ''
if entry_summary:
entry_summary = entry_summary.text()
entry_text = self._getentrytext(body)
now = time.gmtime()
insert("mt_entry",
{"entry_blog_id": BLOG_ID, # hardcoded
"entry_status": 2, # hardcoded ("publish")
"entry_author_id": AUTHOR_ID, # hardcoded ("atomuser")
"entry_allow_comments": 0, # hardcoded ("none")
"entry_allow_pings": 0, # hardcoded ("no")
"entry_convert_breaks": 0, # hardcoded ("no conversion")
# "entry_category_id": ???
"entry_title": entry_title,
"entry_excerpt": entry_summary,
"entry_text": entry_text,
"entry_created_on": utcdate(now),
"entry_modified_on": utcdate(now)
})
self.entry_id = q("""select max(entry_id) entry_id from mt_entry""")[0]["entry_id"]
rebuild(self.blog_id, self.entry_id)
self.status(201, reauth=1)
print 'Location: http://diveintomark.org/cgi-bin/atom.cgi/blog_id=%s/entry_id=%s' % (self.blog_id, self.entry_id)
self.message("new entry created")
def do_GET(self, body):
"""get entry"""
data = q("""select * from mt_entry where entry_blog_id = %(blog_id)s and entry_id = %(entry_id)s""" % self.__dict__)
if not data:
self.status(404, reauth=1, stop=1, msg="no such entry")
row = data[0]
params = {"title": row['entry_title'],
"summary": row['entry_excerpt'],
"issued": utcdate(row['entry_created_on']), # faked, MT has no way to store this
"created": utcdate(row['entry_created_on']),
"modified": utcdate(row['entry_modified_on']),
"link": "http://diveintomark.org/atom/archives/%06d.html" % row['entry_id'], # faked, in real life this would be calculated from <$MTEntryPermalink$>
"id": "http://diveintomark.org/cgi-bin/atom.cgi/blog_id=%s/entry_id=%s" % (self.blog_id, self.entry_id),
"content": row['entry_text']}
self.status(200, reauth=1)
self.content_type('application/xml')
print
print buildEntry(params)
def do_PUT(self, body):
"""edit entry"""
entry_text = self._getentrytext(body)
params = {}
title = body.first('title')
if title:
params['entry_title'] = title.text()
excerpt = body.first('summary')
if excerpt:
params['entry_excerpt'] = excerpt.text()
entry_text = self._getentrytext(body)
if entry_text:
params['entry_text'] = entry_text
if params:
params['entry_modified_on'] = utcdate(time.gmtime()).replace('T', ' ').replace('Z', '') # hack
update("mt_entry", "entry_blog_id = %s and entry_id = %s" % (self.blog_id, self.entry_id), params)
rebuild(self.blog_id, self.entry_id)
self.status(205, reauth=1, stop=1, msg="entry updated")
else:
self.status(200, reauth=1, stop=1, msg="no changes made")
def do_DELETE(self, body):
"""delete entry"""
x("""delete from mt_entry where entry_blog_id = %s and entry_id = %s""" % (self.blog_id, self.entry_id))
rebuild(self.blog_id)
self.status(200, reauth=1, stop=1, msg="entry deleted")
class ServiceHandler(Handler):
def __init__(self, service="edit", blog_id=None):
self.blog_id = blog_id # TODO - this is all wrong, search should go in AtomHandler
handler = getattr(self, "do_%s" % service)
if not handler:
self.status(500, reauth=1, stop=1, msg="unknown service type")
handler()
def do_edit(self):
"""introspection"""
self.status(200, reauth=1)
self.content_type('application/xml')
print
print ''''''
print ''' http://diveintomark.org/cgi-bin/atom.cgi/blog_id=14'''
print ''' http://diveintomark.org/cgi-bin/atom.cgi/blog_id=14/service=search'''
print ''''''
def do_search(self):
"""search entries"""
if not self.blog_id:
self.status(500, reauth=1, stop=1, msg="no blog_id specified")
all = None
recent = None
start_range = None
end_range = None
data = None
all = os.environ['QUERY_STRING'].count('atom-all')
if all:
data = q("""select entry_id, entry_title from mt_entry where entry_blog_id = %s order by entry_modified_on desc""" % self.blog_id)
else:
fs = cgi.FieldStorage()
try:
recent = int(fs['atom-recent'].value)
start_range = 0
end_range = recent - 1
except:
pass
if not recent:
try:
start_range = int(fs['atom-start-range'].value)
except:
self.status(500, reauth=1, stop=1, msg="no atom-start-range specified")
try:
end_range = int(fs['atom-end-range'].value)
except:
self.status(500, reauth=1, stop=1, msg="no atom-end-range specified")
data = q("""select entry_id, entry_title from mt_entry where entry_blog_id = %s order by entry_modified_on desc limit %s, %s""" % (self.blog_id, start_range, (1 + end_range - start_range)))
s = ''''''
for row in data:
s += '''\n\n %s\n http://diveintomark.org/cgi-bin/atom.cgi/blog_id=%s/entry_id=%s\n''' % (saxutils.escape(row['entry_title']), self.blog_id, row['entry_id'])
s += '''\n'''
self.status(200, reauth=1)
self.content_type('application/xml')
print
print s
def do_comment(self):
"""comment introspection"""
pass # TODO
#def local():
# method = sys.argv[1].upper()
# inputfile = sys.argv[2:] and sys.argv[2] or None
# if inputfile == '-':
# inputfile = None
# fsock = inputfile and open(inputfile) or sys.stdin
# entry_id = sys.argv[3:] and sys.argv[3] or None
# os.environ['SERVER_NAME'] = 'diveintomark.org'
# os.environ['REQUEST_URI'] = '/cgi-bin/atom.cgi'
# a = AtomHandler(BLOG_ID, entry_id, method, fsock)
# fsock.close()
def web():
pathinfo = os.environ.get('PATH_INFO') or ''
params = [q for q in pathinfo.split('/') if q.count('=')]
params = [q.split('=', 1) for q in params]
params = dict(params)
blog_id = params.get('blog_id')
service = params.get('service')
if service:
ServiceHandler(service, blog_id)
else:
entry_id = params.get('entry_id')
AtomHandler(blog_id, entry_id)
if __name__ == '__main__':
if os.environ.get('REQUEST_METHOD'):
web()
else:
local()