Commit eb32a211 authored by Romain Courteaud's avatar Romain Courteaud

Add the 'dataurl' sandbox.

This sandbox download the gadget HTML code and convert it as a dataurl which is loaded in an iframe.
This may help to bypass web browser mixed content policy or CSP restrictions (like inline js).
parent bf3bb35a
......@@ -63,6 +63,8 @@ module.exports = function (grunt) {
'Channel',
'XMLHttpRequest',
'MutationObserver',
'Blob',
'FileReader',
'Node'
]
}
......
......@@ -6,16 +6,70 @@
* http://www.renderjs.org/documentation
*/
(function (document, window, RSVP, DOMParser, Channel, MutationObserver,
Node) {
Node, FileReader, Blob) {
"use strict";
function readBlobAsDataURL(blob) {
var fr = new FileReader();
return new RSVP.Promise(function (resolve, reject) {
fr.addEventListener("load", function (evt) {
resolve(evt.target.result);
});
fr.addEventListener("error", reject);
fr.readAsDataURL(blob);
}, function () {
fr.abort();
});
}
function ajax(url) {
var xhr;
function resolver(resolve, reject) {
function handler() {
try {
if (xhr.readyState === 0) {
// UNSENT
reject(xhr);
} else if (xhr.readyState === 4) {
// DONE
if ((xhr.status < 200) || (xhr.status >= 300) ||
(!/^text\/html[;]?/.test(
xhr.getResponseHeader("Content-Type") || ""
))) {
reject(xhr);
} else {
resolve(xhr);
}
}
} catch (e) {
reject(e);
}
}
xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = handler;
xhr.setRequestHeader('Accept', 'text/html');
xhr.withCredentials = true;
xhr.send();
}
function canceller() {
if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
xhr.abort();
}
}
return new RSVP.Promise(resolver, canceller);
}
var gadget_model_dict = {},
javascript_registration_dict = {},
stylesheet_registration_dict = {},
gadget_loading_klass,
loading_klass_promise,
renderJS,
Monitor;
Monitor,
isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i');
/////////////////////////////////////////////////////////////////
// Helper functions
......@@ -555,6 +609,33 @@
]);
}
/////////////////////////////////////////////////////////////////
// privateDeclareDataUrlGadget
/////////////////////////////////////////////////////////////////
function privateDeclareDataUrlGadget(url, options, parent_gadget) {
return new RSVP.Queue()
.push(function () {
return ajax(url);
})
.push(function (xhr) {
// Insert a "base" element, in order to resolve all relative links
// which could get broken with a data url
var doc = (new DOMParser()).parseFromString(xhr.responseText,
'text/html'),
base = doc.createElement('base'),
blob;
base.href = url;
doc.head.insertBefore(base, doc.head.firstChild);
blob = new Blob([doc.documentElement.outerHTML],
{type: "text/html;charset=UTF-8"});
return readBlobAsDataURL(blob);
})
.push(function (data_url) {
return privateDeclareIframeGadget(data_url, options, parent_gadget);
});
}
/////////////////////////////////////////////////////////////////
// RenderJSGadget.declareGadget
/////////////////////////////////////////////////////////////////
......@@ -590,6 +671,8 @@
method = privateDeclarePublicGadget;
} else if (options.sandbox === "iframe") {
method = privateDeclareIframeGadget;
} else if (options.sandbox === "dataurl") {
method = privateDeclareDataUrlGadget;
} else {
throw new Error("Unsupported sandbox options '" +
options.sandbox + "'");
......@@ -702,8 +785,7 @@
/////////////////////////////////////////////////////////////////
renderJS.getAbsoluteURL = function (url, base_url) {
var doc, base, link,
html = "<!doctype><html><head></head></html>",
isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i');
html = "<!doctype><html><head></head></html>";
if (url && base_url && !isAbsoluteOrDataURL.test(url)) {
doc = (new DOMParser()).parseFromString(html, 'text/html');
......@@ -781,10 +863,9 @@
// renderJS.declareGadgetKlass
/////////////////////////////////////////////////////////////////
renderJS.declareGadgetKlass = function (url) {
var result,
xhr;
var result;
function parse() {
function parse(xhr) {
var tmp_constructor,
key,
parsed_html;
......@@ -830,50 +911,18 @@
return gadget_model_dict[url];
}
function resolver(resolve, reject) {
function handler() {
var tmp_result;
try {
if (xhr.readyState === 0) {
// UNSENT
reject(xhr);
} else if (xhr.readyState === 4) {
// DONE
if ((xhr.status < 200) || (xhr.status >= 300) ||
(!/^text\/html[;]?/.test(
xhr.getResponseHeader("Content-Type") || ""
))) {
reject(xhr);
} else {
tmp_result = parse();
resolve(tmp_result);
}
}
} catch (e) {
reject(e);
}
}
xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = handler;
xhr.setRequestHeader('Accept', 'text/html');
xhr.withCredentials = true;
xhr.send();
}
function canceller() {
if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
xhr.abort();
}
}
if (gadget_model_dict.hasOwnProperty(url)) {
// Return klass object if it already exists
result = RSVP.resolve(gadget_model_dict[url]);
} else {
// Fetch the HTML page and parse it
result = new RSVP.Promise(resolver, canceller);
result = new RSVP.Queue()
.push(function () {
return ajax(url);
})
.push(function (xhr) {
return parse(xhr);
});
}
return result;
};
......@@ -899,10 +948,9 @@
required_js_list: []
},
i,
element,
isAbsoluteURL = new RegExp('^(?:[a-z]+:)?//', 'i');
element;
if (!url || !isAbsoluteURL.test(url)) {
if (!url || !isAbsoluteOrDataURL.test(url)) {
throw new Error("The url should be absolute: " + url);
}
......@@ -1305,4 +1353,5 @@
}
bootstrap();
}(document, window, RSVP, DOMParser, Channel, MutationObserver, Node));
}(document, window, RSVP, DOMParser, Channel, MutationObserver, Node,
FileReader, Blob));
......@@ -3401,6 +3401,69 @@
});
});
/////////////////////////////////////////////////////////////////
// RenderJSGadget.declareGadget (dataurl)
/////////////////////////////////////////////////////////////////
test('dataurl provide an iframed gadget as callback parameter', function () {
// Check that declare gadget returns the gadget
var parent_gadget = new RenderJSGadget(),
topURL = "http://example.org/topGadget",
parsed = URI.parse(window.location.href),
parent_path = URI.build({protocol: parsed.protocol,
hostname: parsed.hostname,
port: parsed.port,
path: parsed.path}).toString(),
absolute_path = parent_path + "mixed_embedded.html",
data_url = "data:text/html;charset=utf-8;base64,PGh0bWw+PGhlYWQ+PGJhc2" +
"UgaHJlZj0iaHR0cDovLzEyNy4wLjAuMTo5MDAwL3Rlc3QvbWl4ZWRfZW1iZWRkZWQua" +
"HRtbCI+PGJhc2UgaHJlZj0iaHR0cDovLzEyNy4wLjAuMTo5MDAwIj48c2NyaXB0IHNy" +
"Yz0iLi4vbm9kZV9tb2R1bGVzL3JzdnAvZGlzdC9yc3ZwLTIuMC40LmpzIiB0eXBlPSJ" +
"0ZXh0L2phdmFzY3JpcHQiPjwvc2NyaXB0PjxzY3JpcHQgc3JjPSIuLi9kaXN0L3Jlbm" +
"RlcmpzLWxhdGVzdC5qcyIgdHlwZT0idGV4dC9qYXZhc2NyaXB0Ij48L3NjcmlwdD48L" +
"2hlYWQ+PGJvZHk+PHA+bXkgbWl4ZWQgZm9vPC9wPjwvYm9keT48L2h0bWw+";
// data:text/html;charset=utf-8;base64,
// PGh0bWw+PGJvZHk+PHA+Zm9vPC9wPjwvYm9keT48L2h0bWw+"
this.server.respondWith("GET", "/test/mixed_embedded.html", [200, {
"Content-Type": "text/html"
}, '<html><head>' +
'<base href="http://127.0.0.1:9000"></base>' +
'<script src="../node_modules/rsvp/dist/rsvp-2.0.4.js" ' +
'type="text/javascript"></script>' +
'<script src="../dist/renderjs-latest.js" ' +
'type="text/javascript"></script>' +
'</head><body><p>my mixed foo</p></body></html>']);
document.getElementById("qunit-fixture").textContent = "";
parent_gadget.__path = parent_path;
parent_gadget.__acquired_method_dict = {
getTopURL: function () {return topURL; }
};
stop();
parent_gadget.declareGadget(absolute_path, {
sandbox: 'dataurl',
element: document.getElementById('qunit-fixture')
})
.then(function (new_gadget) {
equal(new_gadget.__path, data_url);
ok(new_gadget instanceof RenderJSIframeGadget);
equal(
new_gadget.__element.innerHTML,
'<iframe src="' + data_url + '"></iframe>'
);
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
/////////////////////////////////////////////////////////////////
// RenderJSGadget.getDeclaredGadget
/////////////////////////////////////////////////////////////////
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment