@schukai/monster 4.136.0 → 4.136.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.136.0"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.136.1"}
@@ -29,6 +29,35 @@ try {
29
29
  }`,
30
30
  0,
31
31
  );
32
+ ViewerStyleSheet.insertRule(
33
+ `
34
+ @layer viewer {
35
+ :host {
36
+ max-width: 100%;
37
+ min-width: 0;
38
+ }
39
+
40
+ [data-monster-role="viewer"] {
41
+ max-width: 100%;
42
+ min-height: 0;
43
+ min-width: 0;
44
+ overflow: auto;
45
+ }
46
+
47
+ [data-monster-role="viewer"] :is(img, video, audio, object, embed, table, pre) {
48
+ max-width: 100%;
49
+ }
50
+
51
+ [data-monster-role="viewer"] [part="text"] {
52
+ overflow: hidden;
53
+ overflow-wrap: anywhere;
54
+ white-space: pre-wrap;
55
+ width: 100%;
56
+ }
57
+ }
58
+ `,
59
+ 1,
60
+ );
32
61
  } catch (e) {
33
62
  addAttributeToken(
34
63
  document.getRootNode().querySelector("html"),
@@ -12,6 +12,8 @@ div {
12
12
  div[data-monster-role="content-container"] {
13
13
  width: 100%;
14
14
  height: 100%;
15
+ max-width: 100%;
16
+ min-width: 0;
15
17
  overflow: auto;
16
18
  position: relative;
17
19
  display: flex;
@@ -25,7 +25,7 @@ try {
25
25
  HtmlStyleSheet.insertRule(
26
26
  `
27
27
  @layer html {
28
- div,img{transition:opacity .9s ease,transform .3s ease,filter .3s ease,box-shadow .3s ease,background-color .3s ease}div[data-monster-role=content-container]{display:flex;flex-direction:column;height:100%;overflow:auto;position:relative;width:100%}.privacyImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\")}.notFoundImage,.privacyImage{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--monster-bg-color-primary-4);-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:50%;mask-size:50%;-o-object-fit:contain;object-fit:contain}.notFoundImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\")}
28
+ div,img{transition:opacity .9s ease,transform .3s ease,filter .3s ease,box-shadow .3s ease,background-color .3s ease}div[data-monster-role=content-container]{display:flex;flex-direction:column;height:100%;max-width:100%;min-width:0;overflow:auto;position:relative;width:100%}.privacyImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath d='M5.338 1.59a61 61 0 0 0-2.837.856.48.48 0 0 0-.328.39c-.554 4.157.726 7.19 2.253 9.188a10.7 10.7 0 0 0 2.287 2.233c.346.244.652.42.893.533q.18.085.293.118a1 1 0 0 0 .101.025 1 1 0 0 0 .1-.025q.114-.034.294-.118c.24-.113.547-.29.893-.533a10.7 10.7 0 0 0 2.287-2.233c1.527-1.997 2.807-5.031 2.253-9.188a.48.48 0 0 0-.328-.39c-.651-.213-1.75-.56-2.837-.855C9.552 1.29 8.531 1.067 8 1.067c-.53 0-1.552.223-2.662.524zM5.072.56C6.157.265 7.31 0 8 0s1.843.265 2.928.56c1.11.3 2.229.655 2.887.87a1.54 1.54 0 0 1 1.044 1.262c.596 4.477-.787 7.795-2.465 9.99a11.8 11.8 0 0 1-2.517 2.453 7 7 0 0 1-1.048.625c-.28.132-.581.24-.829.24s-.548-.108-.829-.24a7 7 0 0 1-1.048-.625 11.8 11.8 0 0 1-2.517-2.453C1.928 10.487.545 7.169 1.141 2.692A1.54 1.54 0 0 1 2.185 1.43 63 63 0 0 1 5.072.56'/%3E%3Cpath d='M7.001 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.553.553 0 0 1-1.1 0z'/%3E%3C/svg%3E\")}.notFoundImage,.privacyImage{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--monster-bg-color-primary-4);-webkit-mask-position:center center;mask-position:center center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:50%;mask-size:50%;-o-object-fit:contain;object-fit:contain}.notFoundImage{-webkit-mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\");mask-image:url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M3.112 5.112a3 3 0 0 0-.17.613C1.266 6.095 0 7.555 0 9.318 0 11.366 1.708 13 3.781 13H11l-1-1H3.781C2.231 12 1 10.785 1 9.318c0-1.365 1.064-2.513 2.46-2.666l.446-.05v-.447q0-.113.018-.231zm2.55-1.45-.725-.725A5.5 5.5 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773a3.2 3.2 0 0 1-1.516 2.711l-.733-.733C14.498 11.378 15 10.626 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3c-.875 0-1.678.26-2.339.661z'/%3E%3Cpath d='m13.646 14.354-12-12 .708-.708 12 12z'/%3E%3C/svg%3E\")}
29
29
  }`,
30
30
  0,
31
31
  );
@@ -29,9 +29,13 @@ import "./viewer/message.mjs";
29
29
  import { getLocaleOfDocument } from "../../dom/locale.mjs";
30
30
  import { Button } from "../form/button.mjs";
31
31
  import { findTargetElementFromEvent } from "../../dom/events.mjs";
32
+ import { sanitizeHtml } from "../../dom/sanitize-html.mjs";
32
33
 
33
34
  export { Viewer };
34
35
 
36
+ const BLOCKED_HTML_RESOURCE_DATA_URL =
37
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
38
+
35
39
  /**
36
40
  * @private
37
41
  * @type {symbol}
@@ -732,7 +736,7 @@ class Viewer extends CustomElement {
732
736
  if (data instanceof Blob) {
733
737
  blobToText(data)
734
738
  .then((html) => {
735
- this.setOption("content", html);
739
+ this.setOption("content", sanitizeViewerHTMLContent(html));
736
740
  })
737
741
  .catch((error) => {
738
742
  this.dispatchEvent(
@@ -754,7 +758,7 @@ class Viewer extends CustomElement {
754
758
  return response.text();
755
759
  })
756
760
  .then((html) => {
757
- this.setOption("content", html);
761
+ this.setOption("content", sanitizeViewerHTMLContent(html));
758
762
  })
759
763
  .catch((error) => {
760
764
  this.dispatchEvent(
@@ -771,7 +775,7 @@ class Viewer extends CustomElement {
771
775
  throw new Error("HTMLElement or string expected");
772
776
  }
773
777
 
774
- this.setOption("content", data);
778
+ this.setOption("content", sanitizeViewerHTMLContent(data));
775
779
  }
776
780
 
777
781
  /**
@@ -787,7 +791,7 @@ class Viewer extends CustomElement {
787
791
  setPlainText(data) {
788
792
  const mkPreSpan = (text) => {
789
793
  const pre = document.createElement("pre");
790
- pre.innerText = text;
794
+ pre.textContent = text;
791
795
  pre.setAttribute("part", "text");
792
796
  return pre.outerHTML;
793
797
  };
@@ -795,10 +799,6 @@ class Viewer extends CustomElement {
795
799
  if (data instanceof Blob) {
796
800
  blobToText(data)
797
801
  .then((text) => {
798
- const div = document.createElement("div");
799
- div.innerHTML = text;
800
- text = div.innerText;
801
-
802
802
  this.setOption("content", mkPreSpan(text));
803
803
  })
804
804
  .catch((error) => {
@@ -812,9 +812,7 @@ class Viewer extends CustomElement {
812
812
  } else if (data instanceof HTMLElement) {
813
813
  data = data.outerText;
814
814
  } else if (isString(data)) {
815
- const div = document.createElement("div");
816
- div.innerHTML = data;
817
- data = div.innerText;
815
+ // text/plain should never be interpreted as HTML
818
816
  } else if (isURL(data)) {
819
817
  getGlobal()
820
818
  .fetch(data)
@@ -822,10 +820,6 @@ class Viewer extends CustomElement {
822
820
  return response.text();
823
821
  })
824
822
  .then((text) => {
825
- const div = document.createElement("div");
826
- div.innerHTML = text;
827
- text = div.innerText;
828
-
829
823
  this.setOption("content", mkPreSpan(text));
830
824
  })
831
825
  .catch((error) => {
@@ -910,6 +904,95 @@ function blobToText(blob) {
910
904
  });
911
905
  }
912
906
 
907
+ function sanitizeViewerHTMLContent(html) {
908
+ const sanitizedHTML = sanitizeHtml(html);
909
+ const parser = new DOMParser();
910
+ const doc = parser.parseFromString(sanitizedHTML, "text/html");
911
+
912
+ doc.querySelectorAll("*").forEach((element) => {
913
+ removeCrossOriginResourceAttribute(element, "src");
914
+ removeCrossOriginResourceAttribute(element, "srcset");
915
+ removeCrossOriginResourceAttribute(element, "poster");
916
+
917
+ if (element.hasAttribute("style")) {
918
+ const safeStyle = sanitizeStyleAttribute(element.getAttribute("style"));
919
+ if (safeStyle) {
920
+ element.setAttribute("style", safeStyle);
921
+ } else {
922
+ element.removeAttribute("style");
923
+ }
924
+ }
925
+ });
926
+
927
+ return doc.body.innerHTML;
928
+ }
929
+
930
+ function removeCrossOriginResourceAttribute(element, attributeName) {
931
+ const value = element.getAttribute(attributeName);
932
+ if (!value) {
933
+ return;
934
+ }
935
+
936
+ if (attributeName === "srcset") {
937
+ const sameOriginEntries = value
938
+ .split(",")
939
+ .map((entry) => entry.trim())
940
+ .filter(Boolean)
941
+ .filter((entry) => {
942
+ const [candidateUrl] = entry.split(/\s+/, 1);
943
+ return isSameOriginResourceUrl(candidateUrl);
944
+ });
945
+
946
+ if (sameOriginEntries.length > 0) {
947
+ element.setAttribute(attributeName, sameOriginEntries.join(", "));
948
+ return;
949
+ }
950
+ } else if (isSameOriginResourceUrl(value)) {
951
+ return;
952
+ }
953
+
954
+ if (
955
+ attributeName === "src" &&
956
+ element.tagName.toLowerCase() === "img" &&
957
+ !element.classList.contains("privacyImage")
958
+ ) {
959
+ element.setAttribute("src", BLOCKED_HTML_RESOURCE_DATA_URL);
960
+ element.classList.add("privacyImage");
961
+ return;
962
+ }
963
+
964
+ element.removeAttribute(attributeName);
965
+ }
966
+
967
+ function sanitizeStyleAttribute(styleValue) {
968
+ return styleValue.replace(/url\(([^)]+)\)/gi, (match, rawUrl) => {
969
+ const cleanedUrl = rawUrl.trim().replace(/^['"]|['"]$/g, "");
970
+ return isSameOriginResourceUrl(cleanedUrl) ? match : "";
971
+ });
972
+ }
973
+
974
+ function isSameOriginResourceUrl(value) {
975
+ if (!value) {
976
+ return true;
977
+ }
978
+
979
+ const trimmedValue = value.trim();
980
+ if (!trimmedValue || trimmedValue.startsWith("#")) {
981
+ return true;
982
+ }
983
+
984
+ try {
985
+ const url = new URL(trimmedValue, document.location.href);
986
+ return (
987
+ url.origin === document.location.origin ||
988
+ url.protocol === "data:" ||
989
+ url.protocol === "blob:"
990
+ );
991
+ } catch {
992
+ return true;
993
+ }
994
+ }
995
+
913
996
  /**
914
997
  * @private
915
998
  * @return {Select}
@@ -0,0 +1,99 @@
1
+ import * as chai from "chai";
2
+ import { chaiDom } from "../../../util/chai-dom.mjs";
3
+ import { initJSDOM } from "../../../util/jsdom.mjs";
4
+
5
+ chai.use(chaiDom);
6
+ const expect = chai.expect;
7
+
8
+ let Viewer;
9
+
10
+ describe("Viewer", function () {
11
+ before(function (done) {
12
+ initJSDOM()
13
+ .then(() => import("../../../../source/components/content/viewer.mjs"))
14
+ .then((module) => {
15
+ Viewer = module.Viewer;
16
+ done();
17
+ })
18
+ .catch((error) => done(error));
19
+ });
20
+
21
+ afterEach(() => {
22
+ const mocks = document.getElementById("mocks");
23
+ mocks.innerHTML = "";
24
+ });
25
+
26
+ it("renders text/plain without interpreting html", function (done) {
27
+ const mocks = document.getElementById("mocks");
28
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
29
+
30
+ setTimeout(() => {
31
+ try {
32
+ const viewer = document.getElementById("viewer");
33
+ expect(viewer).instanceof(Viewer);
34
+
35
+ viewer.setContent("<strong>raw</strong>\n<script>alert(1)</script>");
36
+
37
+ setTimeout(() => {
38
+ try {
39
+ const textNode = viewer.shadowRoot.querySelector('[part="text"]');
40
+ expect(textNode).to.not.equal(null);
41
+ expect(textNode.textContent).to.equal(
42
+ "<strong>raw</strong>\n<script>alert(1)</script>",
43
+ );
44
+ expect(textNode.innerHTML).to.contain("&lt;strong&gt;raw&lt;/strong&gt;");
45
+ done();
46
+ } catch (error) {
47
+ done(error);
48
+ }
49
+ }, 0);
50
+ } catch (error) {
51
+ done(error);
52
+ }
53
+ }, 0);
54
+ });
55
+
56
+ it("blocks cross-origin resources in html view", function (done) {
57
+ const mocks = document.getElementById("mocks");
58
+ mocks.innerHTML = `<monster-viewer id="viewer"></monster-viewer>`;
59
+
60
+ setTimeout(() => {
61
+ try {
62
+ const viewer = document.getElementById("viewer");
63
+ expect(viewer).instanceof(Viewer);
64
+
65
+ viewer.setContent(
66
+ `
67
+ <link rel="stylesheet" href="https://evil.example/tracker.css">
68
+ <script src="https://evil.example/tracker.js"></script>
69
+ <img id="external" src="https://evil.example/pixel.png">
70
+ <img id="local" src="/image.png">
71
+ `,
72
+ "text/html",
73
+ );
74
+
75
+ setTimeout(() => {
76
+ try {
77
+ const root = viewer.shadowRoot;
78
+ expect(root.querySelector("link")).to.equal(null);
79
+ expect(root.querySelector("script")).to.equal(null);
80
+
81
+ const externalImage = root.querySelector("#external");
82
+ expect(externalImage).to.not.equal(null);
83
+ expect(externalImage.classList.contains("privacyImage")).to.equal(true);
84
+ expect(externalImage.getAttribute("src")).to.match(/^data:image\/gif;base64,/);
85
+
86
+ const localImage = root.querySelector("#local");
87
+ expect(localImage).to.not.equal(null);
88
+ expect(localImage.getAttribute("src")).to.equal("/image.png");
89
+ done();
90
+ } catch (error) {
91
+ done(error);
92
+ }
93
+ }, 0);
94
+ } catch (error) {
95
+ done(error);
96
+ }
97
+ }, 0);
98
+ });
99
+ });
@@ -3,6 +3,7 @@ import "./prepare.js";
3
3
  import "../cases/components/layout/tabs.mjs";
4
4
  import "../cases/components/layout/slit-panel.mjs";
5
5
  import "../cases/components/layout/panel.mjs";
6
+ import "../cases/components/content/viewer.mjs";
6
7
  import "../cases/components/content/image-editor.mjs";
7
8
  import "../cases/components/form/buy-box.mjs";
8
9
  import "../cases/components/form/message-state-button.mjs";