From 43928d4be630a910ece428665eef5f214ee73fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Mon, 20 Oct 2025 10:10:22 +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 (grafted from 7909b26211575e6b5dc02156dd1327bb73c55673) --HG-- branch : 6.0 --- sao/CHANGELOG | 1 + sao/src/sao.less | 7 +++++- sao/src/view/form.js | 59 +++++++++++++++++++++++++++++++------------- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/sao/CHANGELOG b/sao/CHANGELOG index 2858fa12af..e8468aec7f 100644 --- a/sao/CHANGELOG +++ b/sao/CHANGELOG @@ -1,3 +1,4 @@ +* Use sandboxed iframe to display document (issue14290) Version 6.0.66 - 2025-10-02 --------------------------- diff --git a/sao/src/sao.less b/sao/src/sao.less index e832ecc35d..d2d1f9385f 100644 --- a/sao/src/sao.less +++ b/sao/src/sao.less @@ -673,9 +673,14 @@ img.icon { } } .form-document { - object { + object, img { object-fit: scale-down; object-position: center top; + } + iframe { + border: 0; + } + object, iframe, img { width: 100%; height: 75vh; @media screen and (max-width: @screen-sm-max) { diff --git a/sao/src/view/form.js b/sao/src/view/form.js index 6264cd06f5..10e09cd0c9 100644 --- a/sao/src/view/form.js +++ b/sao/src/view/form.js @@ -4242,15 +4242,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); @@ -4266,31 +4293,29 @@ function eval_pyson(value){ filename = filename_field.get_client(record); } data.done(function(data) { - var url, blob; if (record !== this.record) { return; } + 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 - var object = this.object.clone(); - object.attr('data', url); - object.get(0).onload = function() { - 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; }.bind(this)); }, });