#!/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 += "" % 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()