From 353deb659f4bee708faf055091fbb1a699784981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 17 Oct 2025 14:55:30 +0200 Subject: [PATCH] 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 --- sao/CHANGELOG | 1 + sao/src/sao.less | 9 +++++-- sao/src/view/form.js | 61 ++++++++++++++++++++++++++++++-------------- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/sao/CHANGELOG b/sao/CHANGELOG index 58ed4c60b8..f52756736b 100644 --- a/sao/CHANGELOG +++ b/sao/CHANGELOG @@ -1,3 +1,4 @@ +* Use sandboxed iframe to display document (issue14290) * Add support for multiple button in the tree view * Support shortcuts on modal diff --git a/sao/src/sao.less b/sao/src/sao.less index f601e06110..b7890a555c 100644 --- a/sao/src/sao.less +++ b/sao/src/sao.less @@ -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; } } } diff --git a/sao/src/view/form.js b/sao/src/view/form.js index 4268f4d22f..3d62d718a0 100644 --- a/sao/src/view/form.js +++ b/sao/src/view/form.js @@ -4838,15 +4838,42 @@ function eval_pyson(value){ 'class': this.class_, }); - this.object = jQuery('', { + 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; }); }, });