Use sandboxed iframe to display document

The HTML element used to display the document is based on the mimetype.
And by default a sandboxed iframe is used to isolate the unsafe content from
the parent context.

Closes #14290
This commit is contained in:
Cédric Krier
2025-10-17 14:55:30 +02:00
parent b5ca5de485
commit 353deb659f
3 changed files with 50 additions and 21 deletions
+1
View File
@@ -1,3 +1,4 @@
* Use sandboxed iframe to display document (issue14290)
* Add support for multiple button in the tree view
* Support shortcuts on modal
+7 -2
View File
@@ -1224,15 +1224,20 @@ img.icon {
}
}
.form-document {
object {
object, img {
object-fit: scale-down;
object-position: center top;
}
iframe {
border: 0;
}
object, iframe, img {
max-width: 100%;
min-height: 50vh;
height: 100%;
width: 100%;
@media screen and (max-width: @screen-xs-max) {
min-height: 25vh;
min-height: 50vh;
}
}
}
+42 -19
View File
@@ -4838,15 +4838,42 @@ function eval_pyson(value){
'class': this.class_,
});
this.object = jQuery('<object/>', {
this.content = this._create_content().appendTo(this.el);
},
_create_content: function(mimetype, url) {
let tag_name = 'iframe';
if (mimetype) {
if (mimetype.startsWith('image/')) {
tag_name = 'img';
} else if (mimetype == 'application/pdf') {
tag_name = 'object';
}
}
let content = jQuery(`<${tag_name}/>`, {
'class': 'center-block',
}).appendTo(this.el);
if (attributes.height) {
this.object.css('height', parseInt(attributes.height, 10));
});
if (tag_name == 'iframe') {
content.attr('sandbox', '');
}
if (attributes.width) {
this.object.css('width', parseInt(attributes.width, 10));
if (this.attributes.height) {
content.css('height', parseInt(this.attributes.height, 10));
}
if (this.attributes.width) {
content.css('width', parseInt(this.attributes.width, 10));
}
if (url) {
// set onload before data/src to be always called
content.get().onload = function() {
this.onload = null;
window.URL.revokeObjectURL(url);
};
if (tag_name== 'object') {
content.attr('data', url);
} else {
content.attr('src', url);
}
}
return content;
},
display: function() {
Sao.View.Form.Document._super.display.call(this);
@@ -4862,34 +4889,30 @@ function eval_pyson(value){
filename = filename_field.get_client(record);
}
data.done(data => {
var url, blob;
if (record !== this.record) {
return;
}
// in case onload was not yet triggered
window.URL.revokeObjectURL(this.object.attr('data'));
let url = this.content.attr('data') ||
this.content.attr('src');
window.URL.revokeObjectURL(url);
let mimetype;
if (!data) {
url = null;
} else {
var mimetype = Sao.common.guess_mimetype(filename);
mimetype = Sao.common.guess_mimetype(filename);
if (mimetype == 'application/octet-binary') {
mimetype = null;
}
blob = new Blob([data], {
let blob = new Blob([data], {
'type': mimetype,
});
url = window.URL.createObjectURL(blob);
}
// duplicate object to force refresh on buggy browsers
const object = this.object.clone();
// set onload before data to be always called
object.get(0).onload = function() {
this.onload = null;
window.URL.revokeObjectURL(url);
};
object.attr('data', url);
this.object.replaceWith(object);
this.object = object;
let content = this._create_content(mimetype, url);
this.content.replaceWith(content);
this.content = content;
});
},
});