############################################################################## # # Copyright (c) 2005 Zope Corporation and Contributors. # All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## #adapted from various trunk xmlrpc files jwashin 2005-06-27 #2005-06-26 removed JSONRPCNotifyResponse and got JSONRPCResponse to handle # 'notify' events #2005-06-26 let the publisher handle encoding #2005-06-27 set outgoing content-type the same as incoming content-type. #2005-09-08 updated to work with the new IResult idea (wsgi) #2005-10-09 unicode handling update #2006-03-09 enabled gzip compression for large responses #2006-05-10 removed gzip compression and (prematurely) enabled json-rpc 1.1 jmw __docformat__ = 'restructuredtext' from configfeature.fiveconfig import __compat__ from zope.app.publication.http import BaseHTTPPublication from interfaces import IMethodPublisher, IJSONRPCView, IJSONRPCPublisher,\ IJSONRPCRequest, IJSONReader, IJSONWriter from zope.interface import implements #from zope.publisher.http import IResult if __compat__.zope_pre_3_3: from zope.app.location import Location else: from zope.location.location import Location from zope.publisher.http import HTTPRequest, HTTPResponse, \ getCharsetUsingRequest, DirectResult from zope.publisher.browser import BrowserRequest from zope.security.proxy import isinstance from zope.publisher.interfaces.browser import IBrowserRequest from zope.publisher.interfaces.browser import IBrowserApplicationRequest from zope.component import getUtility try: from cStringIO import StringIO except ImportError: from StringIO import StringIO import traceback import logging DEBUG = logging.DEBUG logger = logging.getLogger() keyword_key = "pythonKwMaRkEr" #writeProfileData transcribes reads and writes files in the zope3 # instance directory for profiling use. # profiledata.py has response dicts that can be read as python # profiledata.js has request javascript objects in javascript writeProfileData = False compressionTrigger = 1024 compressionLevel = 6 JSONRPCPublication = BaseHTTPPublication class JSONRPCPublicationFactory(object): """a JSON-RPC Publication handler modeled after zope.app.publication.xmlrpc.XMLRPCPublicationFactory """ def __init__(self,db): self.__pub = JSONRPCPublication(db) def __call__(self): return self.__pub def intsort(item): return int(item[0]) class JSONRPCRequest(BrowserRequest): """a JSON-RPC Request modeled after zope.publisher.xmlrpc.XMLRPCRequest REQUEST from JSON-RPC client should have method params id """ jsonID = 'dummy' form = {} #IBrowserRequest is necessary because sometimes complete views may be #transported that may need to look-up e.g., icons. implements(IJSONRPCRequest,IBrowserRequest, IBrowserApplicationRequest) _args = () def _createResponse(self): """return a response""" return JSONRPCResponse() def processInputs(self): """take the converted request and make useful args of it""" json = getUtility(IJSONReader) stream = self._body_instream input = [] incoming = stream.read(1000) while incoming: input.append(incoming) incoming = stream.read(1000) input = ''.join(input) #make it unicode; we are at the boundary input = self._decode(input) data = json.read(input) if writeProfileData: infile = 'profiledata.js' t = open(infile,'a') t.write('%s\n' % input) t.close() logger.log(DEBUG, "processing inputs (%s)" % data) #print "processing inputs (%s)" % data functionstr = data['method'] # failure unless we split on '.' Why? function = functionstr.split('.') self.jsonID = data['id'] params = data['params'] if isinstance(params,list): args = params # now, look for keyword parameters, the old way kwargs = None notPositional = [] for k in args: if isinstance(k,dict): if k.has_key(keyword_key): if isinstance(k[keyword_key],dict): j = k[keyword_key] kwargs= j notPositional.append(k) if notPositional: for k in notPositional: args.remove(k) if kwargs: for m in kwargs.keys(): self.form[str(m)] = kwargs[m] elif isinstance(params,dict): #json-rpc 1.1 (to be proposed) #get the numeric params for positional params temp_positional = [] for key in params: if key.isdigit(): temp_positional.append((key,params[key])) temp_positional.sort(key=intsort) args = [] #make args from positional args and remove them from params for item in temp_positional: args.append(item[1]) del params[item[0]] #drop remaining named params into request.form for named_param in params: #named_param is unicode; python needs string for param names self.form[str(named_param)] = params[named_param] else: raise TypeError, 'Unsupported type for JSON-RPC "params" (%s)' \ % type(params) self._args = tuple(args) #make environment,cookies, etc., available to request.get() super(JSONRPCRequest,self).processInputs() self._environ['JSONRPC_MODE'] = True if function: self.setPathSuffix(function) def traverse(self,object): return super(BrowserRequest,self).traverse(object) def __getitem__(self,key): return self.get(key) class JSONRPCResponse(HTTPResponse): """JSON-RPC Response modeled after zope.publisher.xmlrpc.XMLRPCResponse """ #def getBase(self): # return True def setResult(self,result): """return { 'id' : matches id in request 'result' : the result or null if error 'error' : the error or null if result } """ id = self._request.jsonID if id is not None: result = premarshal(result) wrapper = {'id':id} wrapper['result'] = result wrapper['error'] = None if writeProfileData: outfile = 'profiledata.py' t = open(outfile,'a') t.write('%s\n' % wrapper) t.close() json = getUtility(IJSONWriter) encoding = getCharsetUsingRequest(self._request) result = json.write(wrapper) #body = JSONResult(result, encoding) body = self._prepareResult(result) super(JSONRPCResponse,self).setResult(body) logger.log(DEBUG,"%s" % result) else: self.setStatus(204) super(JSONRPCResponse,self).setResult('') def _prepareResult(self,result): #we've asked json to return unicode; result should be unicode encoding = getCharsetUsingRequest(self._request) or 'utf-8' #at outgoing boundary; encode it. if isinstance(result,unicode): body = result.encode(encoding) charset = encoding else: #something's wrong. json did not return unicode. raise TypeError, "JSON did not return unicode (%s)" % type(result) #we used to gzip compress here, but that should be decided elsewhere self.setHeader('content-type',"application/x-javascript;charset=%s" \ % charset) return body def handleException(self,exc_info): t, value = exc_info[:2] exc_data = [] for file, lineno, function, text in traceback.extract_tb(exc_info[2]): exc_data.append("%s %s %s %s %s" % (file, "line", lineno, "in", function)) exc_data.append("%s %s" % ( "=>", repr(text))) exc_data.append( "** %s: %s" % exc_info[:2]) logger.log(logging.ERROR,"\n".join(exc_data)) s = '%s: %s' % (getattr(t, '__name__', t), value) wrapper = {'id':self._request.jsonID} wrapper['result'] = None wrapper['error'] = s json = getUtility(IJSONWriter) result = json.write(wrapper) body = self._prepareResult(result) super(JSONRPCResponse,self).setResult(body) logger.log(DEBUG,"Exception: %s" % result) self.setStatus(200) def premarshal_dict(data): """return a non-proxied dict""" return dict([(premarshal(k), premarshal(v)) for (k, v) in data.items()]) def premarshal_list(data): """return a non-proxied list""" return map(premarshal, data) #note: no dates or datetimes in json, though supported by xmlrpc premarshal_dispatch_table = { dict: premarshal_dict, list: premarshal_list, tuple: premarshal_list, } premarshal_dispatch = premarshal_dispatch_table.get def premarshal(data): premarshaller = premarshal_dispatch(data.__class__) if premarshaller is not None: return premarshaller(data) return data class JSONRPCView(object): """A base JSON-RPC view that can be used as mix-in for JSON-RPC views. like zope.app.publisher.xmlrpc.XMLRPCView """ implements(IJSONRPCView) def __init__(self, context, request): self.context = context self.request = request class MethodPublisher(JSONRPCView, Location): """Base class for JSON-RPC views that publish methods like zope.app.publisher.xmlrpc.MethodPublisher """ implements(IMethodPublisher) def __getParent(self): return hasattr(self, '_parent') and self._parent or self.context def __setParent(self, parent): self._parent = parent __parent__ = property(__getParent, __setParent) class MethodTraverser(object): implements(IJSONRPCPublisher) __used_for__ = IMethodPublisher def __init__(self, context, request): self.context = context def publishTraverse(self, request, name): return getattr(self.context, name) class TestRequest(JSONRPCRequest): """modeled after zope.publisher.xmlrpc.TestRequest""" def __init__(self, body_instream=None, environ=None, response=None, **kw): _testEnv = { 'SERVER_URL': 'http://127.0.0.1', 'HTTP_HOST': '127.0.0.1', 'CONTENT_LENGTH': '0', 'GATEWAY_INTERFACE': 'TestFooInterface/1.0', } if environ: _testEnv.update(environ) if kw: _testEnv.update(kw) if body_instream is None: body_instream = StringIO('') super(TestRequest, self).__init__( body_instream, _testEnv, response)