#!/usr/bin/env python # import tornado.httpserver import tornado.ioloop import tornado.web import os import re import time import calendar import base64 import traceback import logging import cStringIO import json import cgi from urlparse import urlparse from xml.sax.saxutils import escape as saxescape # Hm, this is inconsistent. Fix it. import server_config account = server_config.account storage = server_config.storage from storage import WeaveStorageException from weave_handlers import WeaveHandler OPENSOCIAL_NS_URI = "http://ns.opensocial.org/2008/opensocial" class HTMLEscapeType(object): def escape(str): return cgi.escape(str) HTMLEscape = HTMLEscapeType() Escapes = {"htmlEscape":HTMLEscape} class XMLFormatHandler(object): # Gah, how to handle this API? We can't really introspect on people easily. def writeResponse(self, handler, escaper, responseType, responseObject): self.write("""\n""" % OPENSOCIAL_NS_URI) self.write("""<%s>\n""" % responseType) for key, value in responseObject.items(): self.writeKV(handler, escaper, key, value) self.write("""\n""" % responseType) self.write("""\n""") def writeKV(self, handler, escaper, key, value): try: keys = value.keys() if keys: for key in keys: self.writeKV(handler, escaper, key, value[key]) except AttributeError, e: # Not a mapping: serialize it directly tag = self.excapeXML(key) value = self.escapeXML(escaper.escape(value)) self.write("<%s>%s\n", tag, value, tag) def escapeXML(self, str): """ >>> XMLFormatHandler().escapeXML(u"ac&d\u00e9") "a<b>c&d&xe9;" """ # Hm, not sure what I really want here. #return str.encode("utf-8", "xmlcharrefreplace") return saxescape(str).encode("utf-8") class AtomFormatHandler(object): def write(self, handler, escaper, data): return "Atom Format Not Supported" class JSONFormatHandler(object): def write(self, handler, escaper, data): return json.dumps(obj) XMLFormat = XMLFormatHandler() AtomFormat = AtomFormatHandler() JSONFormat = JSONFormatHandler() Formats = {"xml":XMLFormat, "atom":AtomFormat, "json":JSONFormat} def resolveFormatArg(request): "Resolves an HTTP request to a formatter object; returns JSON by default" if 'format' in request.arguments: fmt = request.arguments['format'][0] if fmt in Formats: return Formats[fmt] return JSONFormat def resolveEscapeArg(request): "Resolves an HTTP request to an Escape object; returns HTML by default" if 'escapeType' in request.arguments: et = request.arguments['escapeType'][0] if et in Escapes: return Escapes[et] return HTMLEscape dateTimeRE = re.compile(r"""(?P\d\d\d\d) ([-])?(?P\d\d) ([-])?(?P\d\d) ( (T|\s+) (?P\d\d) ( ([:])?(?P\d\d) ( ([:])?(?P\d\d) ( ([.])?(?P\d+) )? )? )? )? ( (?PZ) | (?P[-+]) (?P\d\d) ([:])?(?P\d\d) )? $ """, re.VERBOSE) def resolveUpdatedSinceArg(request): """If the provided HTTP request header contains a 'updatedSince' value, parses the value and returns it as a number of seconds since 1970. >>> class R(object): pass >>> r = R() >>> r.arguments = {'updatedSince':['2002-05-06T13:12:13']} >>> time.gmtime(resolveUpdatedSinceArg(r)) time.struct_time(tm_year=2002, tm_mon=5, tm_mday=6, tm_hour=13, tm_min=12, tm_sec=13, tm_wday=0, tm_yday=126, tm_isdst=0) >>> r.arguments = {'updatedSince':['2002-05-06T13:12:13-00:00']} >>> time.gmtime(resolveUpdatedSinceArg(r)) time.struct_time(tm_year=2002, tm_mon=5, tm_mday=6, tm_hour=13, tm_min=12, tm_sec=13, tm_wday=0, tm_yday=126, tm_isdst=0) >>> r.arguments = {'updatedSince':['2002-05-06T13:12:13+07:30']} >>> time.gmtime(resolveUpdatedSinceArg(r)) time.struct_time(tm_year=2002, tm_mon=5, tm_mday=6, tm_hour=20, tm_min=42, tm_sec=13, tm_wday=0, tm_yday=126, tm_isdst=0) >>> r.arguments = {'updatedSince':['2002-05-06T13:12:13-07:30']} >>> time.gmtime(resolveUpdatedSinceArg(r)) time.struct_time(tm_year=2002, tm_mon=5, tm_mday=6, tm_hour=5, tm_min=42, tm_sec=13, tm_wday=0, tm_yday=126, tm_isdst=0) """ if 'updatedSince' in request.arguments: us = request.arguments['updatedSince'][0] # value space of US is an XML Schema DateTime #return time.strptime(us, "%Y-%m-%dT%H:%M:%S%Z") ma = dateTimeRE.match(us) m = ma.groupdict() year = 0 if m['year']: try: year = int(m['year']) except: pass month = 0 if m['month']: try: month = int(m['month']) except: pass day = 0 if m['day']: try: day = int(m['day']) except: pass minute = 0 if m['minute']: try: minute = int(m['minute']) except: pass hour = 0 if m['hour']: try: hour = int(m['hour']) except: pass if m['tzoffset'] and m['tzhour']: if m['tzoffset'] == '-': sign = -1 else: sign = 1 try: hour += int(m['tzhour']) * sign if m['tzminute']: minute += int(m['tzminute']) * sign except: pass seconds = 0 if m['second']: try: seconds = int(m['second']) if m['fraction']: seconds += float(m['fraction']) except: pass return calendar.timegm((year, month, day, hour, minute, seconds, 0, 0, -1)) # TODO handle time zone return None class OpenSocialPeopleHandler(WeaveHandler): def get(self, username, argument): # Some magical usernames: @me, -1 # for now we only support the '@self' argument # we could also expect to see // if argument == '@self': # handle parameters if 'userId' in self.request.arguments: raise tornado.web.HTTPError(400, "userId is not a legal argument for REST OpenSocial Get Person requests") escape = resolveEscapeArg(self.request) format = resolveFormatArg(self.request) updatedSince = resolveUpdatedSinceArg(self.request) ctx = storage.get_context(username) wbo = storage.get(ctx, 'profile', 'root') if wbo: profileData = wbo.payload # Filter the profile data through the access control policy for this request... # And the updatedSince.... # And return it format.write(self, escape, profileData) else: raise tornado.web.HTTPError(404, "Not Found") def put(self, name, collection=None): self.check_account_match(name) if not collection: raise tornado.web.HTTPError(400, "Missing required collection") ts = storage.handle_put(name, collection, self.request.body) self.write(str(ts)) if __name__ == '__main__': import doctest doctest.testmod()