// Copyright (C) 2010 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /** * @fileoverview ... TODO ihab.awad * @author kpreid@switchb.org * @author ihab.awad@gmail.com * @author jasvir@gmail.com * \@requires document, setTimeout, XMLHttpRequest * \@overrides window * \@provides caja */ var caja = (function () { var cajaBuildVersion = '4913'; var defaultServer = 'https://caja.appspot.com/'; var defaultFrameGroup; var readyQueue = []; var registeredImports = []; var nextId = 0; var UNREADY = 'UNREADY', PENDING = 'PENDING', READY = 'READY'; var state = UNREADY; var GUESS = 'GUESS'; var ajaxCounter = 1; var loaderDocument; function proxyFetchMaker(proxyServer) { return function (url, mime, callback) { if (!url) { callback(undefined); return; } var rndName = 'caja_ajax_' + ajaxCounter++; window[rndName] = function (result) { try { callback(result); } finally { // GC yourself window[rndName] = undefined; } }; // TODO(jasvir): Make it so this does not pollute the host page // namespace but rather just the loaderFrame installSyncScript(rndName, proxyServer ? String(proxyServer) : caja['server'] + '/cajole?url=' + encodeURIComponent(url) + '&input-mime-type=' + encodeURIComponent(mime) + '&transform=PROXY' + '&callback=' + encodeURIComponent(rndName) + '&alt=json-in-script'); }; } function xhrFetcher(url, mime, callback) { var request = new XMLHttpRequest(); request.open('GET', url, true); request.overrideMimeType(mime); request.onreadystatechange = function() { if(request.readyState == 4) { callback({ "html": request.responseText }); } }; request.send(); } var uriPolicies = { 'net': { 'rewriter': { 'NO_NETWORK': function () { return null; }, 'ALL': function (uri) { return String(uri); } }, 'fetcher': { 'USE_XHR': xhrFetcher, 'USE_AS_PROXY': proxyFetchMaker }, 'NO_NETWORK': { 'rewrite': function () { return null; }, 'fetch': function() { } }, 'ALL': { 'rewrite': function (uri) { return String(uri); }, 'fetch': proxyFetchMaker(undefined) }, 'only': policyOnly }, 'ATTRIBUTETYPES': undefined, 'LOADERTYPES': undefined, 'URIEFFECTS': undefined }; var caja = { // Normal entry points 'initialize': initialize, 'load': load, 'whenReady': whenReady, // URI policies 'policy': uriPolicies, // Reference to the taming frame in the default frameGroup 'iframe': null, // Reference to the USELESS object for function invocation (for testing) 'USELESS': undefined, // Taming functions for the default frameGroup 'tame': premature, 'tamesTo': premature, 'untame': premature, 'unwrapDom': premature, 'markReadOnlyRecord': premature, 'markFunction': premature, 'markCtor': premature, 'markXo4a': premature, 'grantMethod': premature, 'grantRead': premature, 'grantReadWrite': premature, 'makeDefensibleObject___': premature, 'makeDefensibleFunction___': premature, // Esoteric functions 'initFeralFrame': initFeralFrame, 'makeFrameGroup': makeFrameGroup, 'configure': makeFrameGroup, // unused, removed by Closure closureCanary: 1 }; // Internal functions made available to FrameGroup maker var cajaInt = { 'documentBaseUrl': documentBaseUrl, 'getId': getId, 'getImports': getImports, 'joinUrl': joinUrl, 'loadCajaFrame': loadCajaFrame, 'prepareContainerDiv': prepareContainerDiv, 'unregister': unregister }; //---------------- function premature() { throw new Error('Calling taming function before Caja is ready'); } /** * Returns a URI policy that allows one URI and denies the rest. */ function policyOnly(allowedUri) { allowedUri = String(allowedUri); return { 'rewrite': function (uri) { uri = String(uri); return uri === allowedUri ? uri : null; } }; } /** * Creates the default frameGroup with the given config. * See {@code makeFrameGroup} for config parameters. */ function initialize(config) { if (state !== UNREADY) { throw new Error('Caja cannot be initialized more than once'); } state = PENDING; makeFrameGroup(config, function (frameGroup) { defaultFrameGroup = frameGroup; caja['iframe'] = frameGroup['iframe']; caja['USELESS'] = frameGroup['USELESS']; for (var i in caja) { if (caja[i] === premature) { caja[i] = frameGroup[i]; } } state = READY; whenReady(null); }); } /** * Creates a guest frame in the default frameGroup. */ function load(div, uriPolicy, loadDone, domOpts) { uriPolicy = uriPolicy || caja['policy']['net']['NO_NETWORK']; if (state === UNREADY) { initialize({}); } whenReady(function () { defaultFrameGroup['makeES5Frame'](div, uriPolicy, loadDone, domOpts); }); } /** * Defers func until the default frameGroup is ready. */ function whenReady(opt_func) { if (typeof opt_func === 'function') { readyQueue.push(opt_func); } if (state === READY) { for (var i = 0; i < readyQueue.length; i++) { setTimeout(readyQueue[i], 0); } readyQueue = []; } } /** * Create a Caja frame group. A frame group maintains a relationship with a * Caja server and some configuration parameters. Most Web pages will only * need to create one frame group. * * Recognized configuration parameters are: * * server - the URL to a Caja server. Except for unique cases, * this must be the server from which the "caja.js" script was * sourced. * * resources - the URL to a directory containing the resource files. * If not specified, it defaults to the value of 'server'. * * debug - whether debugging is supported. At the moment, debug support * means that the files loaded by Caja are un-minified to help with * tracking down problems. * * forceES5Mode - If set to true or false, forces or prohibits ES5 * mode, rather than autodetecting browser capabilities. This * should be used strictly for debugging purposes. * * console - Optional user-supplied alternative to the browser's native * 'console' object. * * targetAttributePresets - Optional structure giving default and * whitelist for the 'target' parameter of anchors and forms. * * log - Optional user-supplied alternative to the browser's native * 'console.log' function. * * @param config an object literal containing configuration parameters. * @param frameGroupReady function to be called back with a reference to * the newly created frame group. */ function makeFrameGroup(config, frameGroupReady) { initFeralFrame(window); config = resolveConfig(config); caja['server'] = config['server']; // TODO(felix8a): this should be === false, but SES isn't ready, // and fails on non-ES5 browsers (frameGroupReady doesn't run) if (config['forceES5Mode'] !== true || unableToSES()) { initES53(config, frameGroupReady); } else { trySES(config, frameGroupReady); } } /** * Returns a full config based on the given partial config. */ function resolveConfig(partial) { partial = partial || {}; var full = {}; full['server'] = String( partial['server'] || partial['cajaServer'] || defaultServer); full['resources'] = String(partial['resources'] || full['server']); full['debug'] = !!partial['debug']; full['forceES5Mode'] = 'forceES5Mode' in partial ? !!partial['forceES5Mode'] : GUESS; if (partial['console']) { full['console'] = partial['console']; } else if (window['console'] && typeof(window['console']['log']) === 'function') { full['console'] = window['console']; } else { full['console'] = undefined; } full['log'] = partial['log'] || function (varargs) { full['console'] && full['console']['log'] .apply(full['console'], arguments); }; if (partial['targetAttributePresets']) { if (!partial['targetAttributePresets']['default']) { throw 'targetAttributePresets must contain a default'; } if (!partial['targetAttributePresets']['whitelist']) { throw 'targetAttributePresets must contain a whitelist'; } if (partial['targetAttributePresets']['whitelist']['length'] === 0) { throw 'targetAttributePresets.whitelist array must be nonempty'; } full['targetAttributePresets'] = partial['targetAttributePresets']; } return full; } function initFeralFrame(feralWin) { if (feralWin['Object']['FERAL_FRAME_OBJECT___'] === feralWin['Object']) { return; } feralWin['___'] = {}; feralWin['Object']['FERAL_FRAME_OBJECT___'] = feralWin['Object']; } //---------------- function initES53(config, frameGroupReady) { // TODO(felix8a): with api change, can start cajoler early too var guestMaker = makeFrameMaker(config, 'es53-guest-frame'); loadCajaFrame(config, 'es53-taming-frame', function (tamingWin) { var fg = tamingWin['ES53FrameGroup']( cajaInt, config, tamingWin, window, guestMaker); frameGroupReady(fg); }); } function trySES(config, frameGroupReady) { var guestMaker = makeFrameMaker(config, 'ses-guest-frame'); loadCajaFrame(config, 'ses-taming-frame', function (tamingWin) { if (canSES(tamingWin['ses'], config['forceES5Mode'])) { var fg = tamingWin['SESFrameGroup']( cajaInt, config, tamingWin, window, guestMaker); frameGroupReady(fg); } else { config['log']('Unable to use SES. Switching to ES53.'); // TODO(felix8a): set a cookie to remember this? initES53(config, frameGroupReady); } }); } function canSES(ses, force) { return (ses['ok']() || (force && ses['maxSeverity'] < ses['severities']['NOT_ISOLATED'])); } // Fast rejection of SES. If this works, repairES5 might still fail, and // we'll fall back to ES53 then. function unableToSES() { return !Object.getOwnPropertyNames; } //---------------- /** * Returns an object that wraps loadCajaFrame() with preload support. * Calling frameMaker.preload() will start creation of a new frame now, * and make it available to a later call to frameMaker.make(). */ function makeFrameMaker(config, filename) { var IDLE = 'IDLE', LOADING = 'LOADING', WAITING = 'WAITING'; var preState = IDLE, preWin, preReady; var self = { 'preload': function () { if (preState === IDLE) { preState = LOADING; preWin = null; loadCajaFrame(config, filename, function (win) { preWin = win; consumeIfReady(); }); } }, 'make': function (onReady) { if (preState === LOADING) { preState = WAITING; preReady = onReady; consumeIfReady(); } else { loadCajaFrame(config, filename, onReady); } } }; self['preload'](); return self; function consumeIfReady() { if (preState === WAITING && preWin) { var win = preWin, ready = preReady; preState = IDLE; preWin = null; preReady = null; ready(win); } } } //---------------- function loadCajaFrame(config, filename, frameReady) { var frameWin = createFrame(filename); // debuggable or minified. ?debug=1 is for shindig var suffix = config['debug'] ? '.js?debug=1' : '.opt.js'; var url = joinUrl( config['resources'], cajaBuildVersion + '/' + filename + suffix); // The particular interleaving of async events shown below has been found // necessary to get the right behavior on Firefox 3.6. Otherwise, the // iframe silently fails to invoke the cajaIframeDone___ callback. setTimeout(function () { frameWin['cajaIframeDone___'] = function () { versionCheck(config, frameWin, filename); frameReady(frameWin); }; // TODO(jasvir): Test what the latency doing this on all browsers is // and why its necessary setTimeout(function () { installAsyncScript(frameWin, url); }, 0); }, 0); } // Throws an error if frameWin has the wrong Caja version function versionCheck(config, frameWin, filename) { if (cajaBuildVersion !== frameWin['cajaBuildVersion']) { var message = 'Version error: caja.js version ' + cajaBuildVersion + ' does not match ' + filename + ' version ' + frameWin['cajaBuildVersion']; config['log'](message); throw new Error(message); } } function prepareContainerDiv(div, feralWin, domOpts) { if (div && feralWin['document'] !== div.ownerDocument) { throw '
provided for ES5 frame must be in main document'; } domOpts = domOpts || {}; var opt_idClass = domOpts ? domOpts['idClass'] : void 0; var idClass = opt_idClass || ('caja-guest-' + nextId++ + '___'); var inner = null; var outer = null; if (div) { inner = div.ownerDocument.createElement('div'); inner.style.display = 'block'; inner.style.position = 'relative'; outer = div.ownerDocument.createElement('div'); outer.style.position = 'relative'; outer.style.overflow = 'hidden'; outer.style.display = 'block'; outer.style.margin = '0'; outer.style.padding = '0'; // Move existing children (like static HTML produced by the cajoler) // into the inner container. while (div.firstChild) { inner.appendChild(div.firstChild); } outer.appendChild(inner); div.appendChild(outer); } return { 'idClass': idClass, 'inner': inner, 'outer': outer }; } // Creates a new iframe and returns its contentWindow. function createFrame(opt_className) { var frame = document.createElement('iframe'); frame.style.display = "none"; frame.width = 0; frame.height = 0; frame.className = opt_className || ''; var where = document.getElementsByTagName('script')[0]; where.parentNode.insertBefore(frame, where); return frame.contentWindow; } function installAsyncScript(frameWin, scriptUrl) { var frameDoc = frameWin['document']; var script = frameDoc.createElement('script'); script.setAttribute('type', 'text/javascript'); script.src = scriptUrl; frameDoc.body.appendChild(script); } // TODO(jasvir): This should pulled into a utility js file function escapeAttr(s) { var ampRe = /&/g; var ltRe = /[<]/g; var gtRe = />/g; var quotRe = /\"/g; return ('' + s).replace(ampRe, '&') .replace(ltRe, '<') .replace(gtRe, '>') .replace(quotRe, '"'); } function installSyncScript(name, url) { if (!loaderDocument) { loaderDocument = createFrame('loader-frame').document; } // TODO(jasvir): This assignment pins the parent's handler // function and, iiuc, this reference is never cleared out. var result = '' + ('