|
CajaHostingModules
How to host Caja modules
How to host a Caja moduleThis document is intended for those who want to host Caja modules within pages that they serve. If you simply want to write Caja modules for someone else to host, check out the Cajoler docs. Here's a sample Caja module host page; the numbers at the left are footnotes and will be referred to in the text below. You may want to print out the source code and refer to it as you read the text. You can also download this code and sample gadgets. <html>
<head>
<title>A simple host page for Caja gadgets</title>
1 <!-- In ant-lib/com/google/caja/plugin/ -->
2 <script src="html-sanitizer-minified.js"></script>
3 <script src="domita-minified.js"></script>
<script>
(function(){
4 // Give the module a variable into which it can export the valija maker
var imports = ___.getNewModuleHandler().getImports();
imports.loader = {provide:___.func(function(v){valijaMaker = v;})};
})();
</script>
<!-- In ant-lib/com/google/caja/plugin/ -->
5 <script src="valija.co.js"></script>
<script>
6 var initValija = function(divId, extraOuters) {
// Make a copy of the standard objects
7 var imports = ___.copy(___.sharedImports);
// Reify the imports for use by Valija
8 imports.outers = imports;
// Create a fake document object, attach it to the given div,
// and put a reference in imports
9 var htmlContainer = document.getElementById(divId);
10 imports.htmlEmitter___ = new HtmlEmitter(htmlContainer);
11 imports.getCssContainer___ = function () {
return htmlContainer;
};
// The function attachDocumentStub() copies each property of
// imports.outers to tameWindow, a constructed object, then sets
// imports.outers = tameWindow
// so after this call, imports.outers !== imports. Also, if
// you set a property on imports.outers after this point,
// you have to grant access to it explicitly.
12 attachDocumentStub(
"-" + divId,
// Don't proxy urls
{ rewrite: function(uri, mimetype) { return uri; } },
imports,
document.getElementById(divId));
// Add the extra, possibly shared state
13 for (var i in extraOuters) {
if (!i.match(/___$/)) {
imports.outers[i] = extraOuters[i];
if (typeof imports.outers[i] === "function") {
// This taming assumes that functions don't use the keyword "this".
___.grantFunc(imports.outers, i);
}
}
}
// Create the Valija runtime instance
// This must be called after attachDocumentStub().
14 imports.$v = valijaMaker.CALL___(imports.outers);
// Use these imports
15 ___.getNewModuleHandler().setImports(imports);
};
</script>
</head>
<body>
<script>
// Some state to share between the gadgets
16 var shared = (function (x) {
return {
17 get: ___.func(function () { return x; }),
18 set: ___.func(function (y) { x = String(y); })
};
})("");
</script>
<script>
// Code for capturing, restoring, and calling a module function
// for sharing code between gadgets but having isolated instances
var sharedCode = (function () {
20 var oldModuleHandler = ___.getNewModuleHandler();
var module;
21 var capturingModuleHandler = ___.freeze({
handle: ___.frozenFunc(function handleOnly(newModule) {
module = newModule;
})});
return {
22 capture: function() { ___.setNewModuleHandler(capturingModuleHandler); },
23 restore: function() { ___.setNewModuleHandler(oldModuleHandler); },
24 inject: function() { module.instantiate(___, oldModuleHandler.getImports()); }
};
})();
// Prepare to capture the module function of the library
25 sharedCode.capture();
</script>
<!-- Load the library code -->
<script src="library.vo.js"></script>
26 <div id="gadget1" class="gadget1___ vdoc-body___"
style="position:relative; overflow: auto"></div>
<script>
// Restore the default new-module handler
27 sharedCode.restore();
// Set up the valija instance for gadget1
// and grant access to the getter
28 initValija("gadget1___", {get : shared.get});
// Create an instance of the library in gadget1's environment
29 sharedCode.inject();
</script>
<!-- Load the gadget -->
30 <script src="gadget1.vo.js"></script>
31 <div id="gadget2" class="gadget2___ vdoc-body___"
style="position:relative; overflow: auto"></div>
<script>
32 initValija("gadget2___", {set : shared.set});
33 sharedCode.inject();
</script>
34 <script src="gadget2.vo.js"></script>
</body>
</html>A Caja module host page is an HTML document. It must do a few things:
Optionally, the host page may supply some APIs for accessing shared state (16) or may instantiate code in the gadget's environment before loading the gadget library code like Prototype, jQuery, YUI, etc. (29) (33). The real substance of this file is in the code that sets up the virtual iframes (6-15). This function takes the id of a div and a mix-in object containing host-specific APIs and shared state. Next, it makes a copy (7) of the default set of objects, called imports, that JavaScript code assumes are universally accessible: JavaScript builtins like Object, Array, and Math. The valija library is written entirely in the small-but-secure "cajita" dialect of JavaScript, so it cannot modify imports directly---only modify its properties. In particular, it assumes that all the objects visible as global variables to a gadget are properties of an import named outers. By setting imports.outers equal to imports (8), all the builtins in imports become visible to the gadget as globals. Next, we add to imports a reference to the div containing the gadget and wrappers for the DOM objects like document and window (9-12). The function attachDocumentStub creates a new outers property by mixing the old one into a tameWindow object so that window is the global scope. Then we add the properties of the mix-in object to imports.outers (13). The cajoler assumes that the valija library is named $v (14), so we have to use that name when instantiating the library. This call must occur after attachDocumentStub so that it gets the proper outers object. Finally, we tell the module handler to use these imports (15). This step is necessary because <script> tags do not return a value; if they did, we'd return the module function instead and invoke it directly with these imports. The properties of extraImports may be host-specific APIs, like the OpenSocial APIs; they may also be objects that enable communication between gadgets. In this example, the host page creates a pair of closures for manipulating a string (16). The getter is added to gadget1's imports, while the setter is added to gadget2's imports. The variable get appears to be a built-in global function to gadget1, while gadget2 sees a global variable set. Thus the code in gadget2 can send a message to gadget1, but gadget1 cannot send a message back. All functions that gadgets need to call must be explicitly whitelisted; get and set are functions that only reference a string, so they can be whitelisted with the func method of the Caja runtime library (17) (18). In general, we may want to restrict or alter the behavior of functions, so simple whitelisting isn't sufficient. This process is called "taming" and forms part of the trusted code base; new tamed methods should only be provided to gadgets after an extensive security review. About half of the trusted code base (3) consists of taming the DOM; see src/com/google/caja/plugin/domita.js for examples of tamings ranging from trivial to intricate. A library like Prototype or YUI needs access to each gadget's document object, but we don't want to incur the overhead of loading the library multiple times as part of each gadget. If we load the library as its own, isolated gadget, we could extract any global variables it created and pass them to the gadgets rather like we did with get and set; however, this may allow gadgets to communicate that should be isolated from each other. A more serious issue is that a library loaded this way effectively closes over the document provided to the library's gadget, and cannot modify any other gadget's document! Instead, we capture the library's module function and invoke it on each gadget's imports. This has the cost of a separate instance of the library for each gadget, but has the benefit that the code is reused. To capture the module function, we swap out the default module handler (20)---which invokes the module---with one that copies the module reference so that it can be invoked multiple times (21). We turn on capturing (25) and load the library, then restore the old module handler (27), prepare the valija instance (28), and invoke the library module with that instance (29) (33). Finally, we load the gadget (30), (32) which sees its own instance of the library as a global variable, isolated from any other instance of the library. The divs playing the role of virtual iframes should have the same class and id (19) (22). The Cajoler takes HTML/CSS/JavaScript code and produces a single JavaScript function (21) (24) that renders the content by means of the DOM taming, and whose execution is mediated by the Caja runtime. The extension ".vo.js" means that the resulting "Valija object code" is just more-complicated JavaScript. |
Sign in to add a comment