diff pydershell/pydershell.py @ 21:1950b0b5bcd8

It's now possible to expose python class instances to JS, if they're declared the right way (for security purposes).
author Atul Varma <varmaa@toolness.com>
date Mon, 07 Sep 2009 15:33:35 -0700
parents 8dd18f864351
children 9413bebf2ee6
line wrap: on
line diff
--- a/pydershell/pydershell.py	Mon Sep 07 12:59:13 2009 -0700
+++ b/pydershell/pydershell.py	Mon Sep 07 15:33:35 2009 -0700
@@ -5,6 +5,7 @@
 import threading
 import traceback
 import weakref
+import types
 
 import pydermonkey
 
@@ -193,7 +194,15 @@
     lines.insert(0, "Traceback (most recent call last):")
     return '\n'.join(lines)
 
-class JSSandbox(object):
+class JsExposedObject(object):
+    """
+    Trivial base/mixin class for any Python classes that choose to
+    expose themselves to JS code.
+    """
+
+    pass
+
+class JsSandbox(object):
     """
     A JS runtime and associated functionality capable of securely
     loading and executing scripts.
@@ -215,12 +224,14 @@
         self.py_stack = None
         self.js_stack = None
         self.__py_to_js = {}
+        self.__type_protos = {}
         self.root = self.wrap_jsobject(root, root)
 
     def finish(self):
         for jsobj in self.__py_to_js.values():
             self.cx.clear_object_private(jsobj)
         del self.__py_to_js
+        del self.__type_protos
         del self.curr_exc
         del self.py_stack
         del self.js_stack
@@ -239,7 +250,7 @@
             self.py_stack = traceback.extract_stack()
             self.js_stack = cx.get_stack()
 
-    def __wrap_pycallable(self, func):
+    def __wrap_pycallable(self, func, pyproto=None):
         if func in self.__py_to_js:
             return self.__py_to_js[func]
 
@@ -248,18 +259,39 @@
         else:
             name = ""
 
-        def wrapper(func_cx, this, args):
-            try:
-                arglist = []
-                for arg in args:
-                    arglist.append(self.wrap_jsobject(arg))
-                # TODO: Fill in extra required params with pymonkey.undefined?
-                # or automatically throw an exception to calling js code?
-                return func(*arglist)
-            except pydermonkey.error:
-                raise
-            except Exception:
-                raise InternalError()
+        if pyproto:
+            def wrapper(func_cx, this, args):
+                try:
+                    arglist = []
+                    for arg in args:
+                        arglist.append(self.wrap_jsobject(arg))
+                    instance = func_cx.get_object_private(this)
+                    if instance is None or not isinstance(instance, pyproto):
+                        raise pydermonkey.error("Method type mismatch")
+
+                    # TODO: Fill in extra required params with
+                    # pymonkey.undefined?  or automatically throw an
+                    # exception to calling js code?
+                    return self.wrap_pyobject(func(instance, *arglist))
+                except pydermonkey.error:
+                    raise
+                except Exception:
+                    raise InternalError()
+        else:
+            def wrapper(func_cx, this, args):
+                try:
+                    arglist = []
+                    for arg in args:
+                        arglist.append(self.wrap_jsobject(arg))
+
+                    # TODO: Fill in extra required params with
+                    # pymonkey.undefined?  or automatically throw an
+                    # exception to calling js code?
+                    return self.wrap_pyobject(func(*arglist))
+                except pydermonkey.error:
+                    raise
+                except Exception:
+                    raise InternalError()
         wrapper.wrapped_pyobject = func
         wrapper.__name__ = name
 
@@ -268,17 +300,35 @@
 
         return jsfunc
 
+    def __wrap_pyinstance(self, value):
+        pyproto = type(value)
+        if pyproto not in self.__type_protos:
+            jsproto = self.cx.new_object()
+            for name in dir(pyproto):
+                attr = getattr(pyproto, name)
+                if (isinstance(attr, types.UnboundMethodType) and
+                    hasattr(attr, '__jsexposed__') and
+                    attr.__jsexposed__):
+                    jsmethod = self.__wrap_pycallable(attr, pyproto)
+                    self.cx.define_property(jsproto, name, jsmethod)
+            self.__type_protos[pyproto] = jsproto
+        return self.cx.new_object(value, self.__type_protos[pyproto])
+
     def wrap_pyobject(self, value):
+        if (isinstance(value, (int, basestring, float, bool)) or
+            value is pydermonkey.undefined or
+            value is None):
+            return value
         if isinstance(value, SafeJsObjectWrapper):
             # It's already wrapped, just unwrap it.
             return value.wrapped_jsobject
         elif callable(value):
             return self.__wrap_pycallable(value)
-        elif isinstance(value, object):
-            raise NotImplementedError("TODO")
+        elif isinstance(value, JsExposedObject):
+            return self.__wrap_pyinstance(value)
         else:
-            # Here's hoping it's a primitive value.
-            return value
+            raise TypeError("Can't expose objects of type '' to JS." %
+                            type(value).__name__)
 
     def wrap_jsobject(self, jsvalue, this=None):
         if this is None:
@@ -289,7 +339,15 @@
                 return self.cx.get_object_private(jsvalue).wrapped_pyobject
             return SafeJsFunctionWrapper(self, jsvalue, this)
         elif isinstance(jsvalue, pydermonkey.Object):
-            return SafeJsObjectWrapper(self, jsvalue, this)
+            # It's a wrapped Python object instance, just unwrap it.
+            instance = self.cx.get_object_private(jsvalue)
+            if instance:
+                if not isinstance(instance, JsExposedObject):
+                    raise AssertionError("Object private is not of type "
+                                         "JsExposedObject")
+                return instance
+            else:
+                return SafeJsObjectWrapper(self, jsvalue, this)
         else:
             # It's a primitive value.
             return jsvalue
@@ -312,7 +370,16 @@
         return retval
 
 if __name__ == '__main__':
-    sandbox = JSSandbox()
+    sandbox = JsSandbox()
+
+    class Baz(JsExposedObject):
+        def woozle(self, blap):
+            return blap + 5
+        woozle.__jsexposed__ = True
+
+    def baz():
+        return Baz()
+    sandbox.root.baz = baz
 
     def bar(obj):
         print obj.bar()
@@ -322,6 +389,13 @@
         return callback()
     sandbox.root.foo = foo
 
+    def ensureBaz(baz):
+        if not isinstance(baz, Baz):
+            print "Uhoh"
+        else:
+            print "ok"
+    sandbox.root.ensureBaz = ensureBaz
+
     def jsprint(string):
         print string
     sandbox.root['print'] = jsprint