details:   https://code.tryton.org/tryton/commit/e4a4914c5bcc
branch:    7.0
user:      Cédric Krier <[email protected]>
date:      Mon Oct 20 10:10:07 2025 +0200
description:
        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)
diffstat:

 sao/CHANGELOG        |   1 +
 sao/src/sao.less     |   7 ++++-
 sao/src/view/form.js |  65 +++++++++++++++++++++++++++++++++++----------------
 3 files changed, 51 insertions(+), 22 deletions(-)

diffs (125 lines):

diff -r 179ccfc6acaa -r e4a4914c5bcc sao/CHANGELOG
--- a/sao/CHANGELOG     Thu Sep 25 17:32:00 2025 +0200
+++ b/sao/CHANGELOG     Mon Oct 20 10:10:07 2025 +0200
@@ -1,3 +1,4 @@
+* Use sandboxed iframe to display document (issue14290)
 
 Version 7.0.37 - 2025-10-02
 ---------------------------
diff -r 179ccfc6acaa -r e4a4914c5bcc sao/src/sao.less
--- a/sao/src/sao.less  Thu Sep 25 17:32:00 2025 +0200
+++ b/sao/src/sao.less  Mon Oct 20 10:10:07 2025 +0200
@@ -761,9 +761,14 @@
         }
     }
     .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 -r 179ccfc6acaa -r e4a4914c5bcc sao/src/view/form.js
--- a/sao/src/view/form.js      Thu Sep 25 17:32:00 2025 +0200
+++ b/sao/src/view/form.js      Mon Oct 20 10:10:07 2025 +0200
@@ -4510,15 +4510,42 @@
                 '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 (attributes.width) {
-                this.object.css('width', parseInt(attributes.width, 10));
-            }
+            });
+            if (tag_name == 'iframe') {
+                content.attr('sandbox', '');
+            }
+            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);
@@ -4534,34 +4561,30 @@
                 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;
             });
         },
     });

Reply via email to