@vonage/vivid 5.10.0 → 5.11.0

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.
@@ -0,0 +1,7 @@
1
+ export declare const ATTR_WHITESPACE: RegExp;
2
+ export declare const domPurifyConfig: {
3
+ ALLOWED_URI_REGEXP: RegExp;
4
+ };
5
+ export declare const sanitizeLinkHref: (url: string) => string;
6
+ export declare const sanitizeImageSrc: (url: string) => string;
7
+ export declare const escapeCssProperty: (value: string) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vonage/vivid",
3
- "version": "5.10.0",
3
+ "version": "5.11.0",
4
4
  "homepage": "https://vivid.deno.dev",
5
5
  "bugs": {
6
6
  "url": "https://github.com/Vonage/vivid-3/issues"
@@ -90,15 +90,15 @@
90
90
  "wait-on": "^8.0.5",
91
91
  "@repo/cem-analyzer-plugins": "1.0.0",
92
92
  "@repo/eslint-config": "1.0.0",
93
- "@repo/consts": "1.0.0",
94
93
  "@repo/eslint-plugin-repo": "1.0.0",
94
+ "@repo/consts": "1.0.0",
95
95
  "@repo/shared": "1.0.0",
96
96
  "@repo/stylelint-config": "1.0.0",
97
97
  "@repo/styles": "1.0.0",
98
98
  "@repo/tokens": "1.0.0",
99
- "@repo/vitest-config": "1.0.0",
100
99
  "@repo/tools": "1.0.0",
101
- "@repo/typescript-config": "1.0.0"
100
+ "@repo/typescript-config": "1.0.0",
101
+ "@repo/vitest-config": "1.0.0"
102
102
  },
103
103
  "customElements": "custom-elements.json",
104
104
  "scripts": {
@@ -11575,6 +11575,37 @@ class RteCoreImpl extends slottableRequest.RteFeatureImpl {
11575
11575
  }
11576
11576
  slottableRequest.featureFacade(RteCoreImpl);
11577
11577
 
11578
+ const DEFAULT_ALLOWED_URI_REGEXP = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i;
11579
+ const ALLOWED_URI_REGEXP_WITH_BLOB = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|blob):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i;
11580
+ const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
11581
+ const domPurifyConfig = {
11582
+ // Allow blobs
11583
+ ALLOWED_URI_REGEXP: ALLOWED_URI_REGEXP_WITH_BLOB
11584
+ };
11585
+ const sanitizeLinkHref = (url) => {
11586
+ if (!DEFAULT_ALLOWED_URI_REGEXP.test(url.replace(ATTR_WHITESPACE, ""))) {
11587
+ return "";
11588
+ }
11589
+ const anchor = document.createElement("a");
11590
+ anchor.setAttribute("href", url);
11591
+ const sanitizedAnchor = DOMPurify.sanitize(anchor, {
11592
+ RETURN_DOM: true,
11593
+ ...domPurifyConfig
11594
+ }).querySelector("a");
11595
+ /* v8 ignore next -- since href is already validated it's probably always present @preserve */
11596
+ return sanitizedAnchor.getAttribute("href") ?? "";
11597
+ };
11598
+ const sanitizeImageSrc = (url) => {
11599
+ const img = document.createElement("img");
11600
+ img.setAttribute("src", url);
11601
+ const sanitizedImg = DOMPurify.sanitize(img, {
11602
+ RETURN_DOM: true,
11603
+ ...domPurifyConfig
11604
+ }).querySelector("img");
11605
+ return sanitizedImg.getAttribute("src") ?? "";
11606
+ };
11607
+ const escapeCssProperty = (value) => value.replace(/[;!].*/, "");
11608
+
11578
11609
  const copy = (obj) => ({ ...obj });
11579
11610
  const parseRulesFromSchema = (schema) => ({
11580
11611
  nodes: Object.fromEntries(
@@ -11612,7 +11643,10 @@ class RteHtmlParser {
11612
11643
  }).content.toJSON() ?? [];
11613
11644
  }
11614
11645
  parseHtml(html, options) {
11615
- const dom = DOMPurify.sanitize(html, { RETURN_DOM: true });
11646
+ const dom = DOMPurify.sanitize(html, {
11647
+ RETURN_DOM: true,
11648
+ ...domPurifyConfig
11649
+ });
11616
11650
  const container = document.createDocumentFragment();
11617
11651
  container.appendChild(dom);
11618
11652
  options?.modifyDom?.(container);
@@ -12771,7 +12805,7 @@ class RteFontSizePickerFeatureImpl extends slottableRequest.RteFeatureImpl {
12771
12805
  toDOM: (mark) => {
12772
12806
  return [
12773
12807
  "span",
12774
- { style: `font-size: ${mark.attrs.size};` },
12808
+ { style: `font-size: ${escapeCssProperty(mark.attrs.size)};` },
12775
12809
  0
12776
12810
  ];
12777
12811
  },
@@ -13441,7 +13475,7 @@ class RteTextColorPickerFeatureImpl extends slottableRequest.RteFeatureImpl {
13441
13475
  return [
13442
13476
  "span",
13443
13477
  {
13444
- style: `color: ${color}`,
13478
+ style: `color: ${escapeCssProperty(color)};`,
13445
13479
  "data-text-color": color
13446
13480
  },
13447
13481
  0
@@ -13951,6 +13985,7 @@ const alignments = [
13951
13985
  label: "right"
13952
13986
  }
13953
13987
  ];
13988
+ const validTextAlign = (value) => alignments.find((a) => a.value === value)?.value ?? "left";
13954
13989
  class RteAlignmentFeatureImpl extends slottableRequest.RteFeatureImpl {
13955
13990
  constructor() {
13956
13991
  super(...arguments);
@@ -13962,10 +13997,10 @@ class RteAlignmentFeatureImpl extends slottableRequest.RteFeatureImpl {
13962
13997
  name: "textAlign",
13963
13998
  default: "left",
13964
13999
  fromDOM(dom) {
13965
- return dom.style.textAlign || "left";
14000
+ return validTextAlign(dom.style.textAlign);
13966
14001
  },
13967
14002
  toStyles(node) {
13968
- return [`text-align: ${node.attrs.textAlign}`];
14003
+ return [`text-align: ${validTextAlign(node.attrs.textAlign)}`];
13969
14004
  }
13970
14005
  })
13971
14006
  ];
@@ -14121,7 +14156,7 @@ class RteLinkFeatureImpl extends slottableRequest.RteFeatureImpl {
14121
14156
  ],
14122
14157
  toDOM(node) {
14123
14158
  const { href } = node.attrs;
14124
- return ["a", { href }, 0];
14159
+ return ["a", { href: sanitizeLinkHref(href) }, 0];
14125
14160
  }
14126
14161
  }
14127
14162
  }
@@ -14526,7 +14561,7 @@ class InlineImageView {
14526
14561
  });
14527
14562
  }
14528
14563
  renderImg(src) {
14529
- this.img.src = src;
14564
+ this.img.src = sanitizeImageSrc(src);
14530
14565
  this.setContent(this.img, { allowPopover: true });
14531
14566
  }
14532
14567
  update(node) {
@@ -14564,13 +14599,13 @@ class RteInlineImageFeatureImpl extends slottableRequest.RteFeatureImpl {
14564
14599
  },
14565
14600
  parseDOM: [
14566
14601
  {
14567
- tag: "img[src]",
14602
+ tag: "img[src],img[data-src]",
14568
14603
  getAttrs: (dom) => {
14569
14604
  const parseDimension = (dim) => {
14570
14605
  const value = parseInt(dim ?? "", 10);
14571
14606
  return isNaN(value) ? null : value;
14572
14607
  };
14573
- const srcAttr = dom.getAttribute("src");
14608
+ const srcAttr = dom.getAttribute("data-src") ?? dom.getAttribute("src");
14574
14609
  const imageUrl = this.config.parseUrlFromHtml ? this.config.parseUrlFromHtml(srcAttr) : srcAttr;
14575
14610
  if (imageUrl === null) {
14576
14611
  return false;
@@ -14592,8 +14627,14 @@ class RteInlineImageFeatureImpl extends slottableRequest.RteFeatureImpl {
14592
14627
  if (resolvedUrl === null) {
14593
14628
  return document.createTextNode("");
14594
14629
  }
14595
- const attrs = { src: resolvedUrl, alt };
14596
- if (size) attrs.style = `max-width: ${size}; height: auto;`;
14630
+ const attrs = {
14631
+ src: sanitizeImageSrc(resolvedUrl),
14632
+ "data-src": resolvedUrl,
14633
+ // Preserve src if it is dropped by sanitization
14634
+ alt
14635
+ };
14636
+ if (size)
14637
+ attrs.style = `max-width: ${escapeCssProperty(size)}; height: auto;`;
14597
14638
  if (naturalWidth) attrs.width = naturalWidth;
14598
14639
  if (naturalHeight) attrs.height = naturalHeight;
14599
14640
  return ["img", attrs];
@@ -11571,6 +11571,37 @@ class RteCoreImpl extends RteFeatureImpl {
11571
11571
  }
11572
11572
  featureFacade(RteCoreImpl);
11573
11573
 
11574
+ const DEFAULT_ALLOWED_URI_REGEXP = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i;
11575
+ const ALLOWED_URI_REGEXP_WITH_BLOB = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|blob):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i;
11576
+ const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g;
11577
+ const domPurifyConfig = {
11578
+ // Allow blobs
11579
+ ALLOWED_URI_REGEXP: ALLOWED_URI_REGEXP_WITH_BLOB
11580
+ };
11581
+ const sanitizeLinkHref = (url) => {
11582
+ if (!DEFAULT_ALLOWED_URI_REGEXP.test(url.replace(ATTR_WHITESPACE, ""))) {
11583
+ return "";
11584
+ }
11585
+ const anchor = document.createElement("a");
11586
+ anchor.setAttribute("href", url);
11587
+ const sanitizedAnchor = DOMPurify.sanitize(anchor, {
11588
+ RETURN_DOM: true,
11589
+ ...domPurifyConfig
11590
+ }).querySelector("a");
11591
+ /* v8 ignore next -- since href is already validated it's probably always present @preserve */
11592
+ return sanitizedAnchor.getAttribute("href") ?? "";
11593
+ };
11594
+ const sanitizeImageSrc = (url) => {
11595
+ const img = document.createElement("img");
11596
+ img.setAttribute("src", url);
11597
+ const sanitizedImg = DOMPurify.sanitize(img, {
11598
+ RETURN_DOM: true,
11599
+ ...domPurifyConfig
11600
+ }).querySelector("img");
11601
+ return sanitizedImg.getAttribute("src") ?? "";
11602
+ };
11603
+ const escapeCssProperty = (value) => value.replace(/[;!].*/, "");
11604
+
11574
11605
  const copy = (obj) => ({ ...obj });
11575
11606
  const parseRulesFromSchema = (schema) => ({
11576
11607
  nodes: Object.fromEntries(
@@ -11608,7 +11639,10 @@ class RteHtmlParser {
11608
11639
  }).content.toJSON() ?? [];
11609
11640
  }
11610
11641
  parseHtml(html, options) {
11611
- const dom = DOMPurify.sanitize(html, { RETURN_DOM: true });
11642
+ const dom = DOMPurify.sanitize(html, {
11643
+ RETURN_DOM: true,
11644
+ ...domPurifyConfig
11645
+ });
11612
11646
  const container = document.createDocumentFragment();
11613
11647
  container.appendChild(dom);
11614
11648
  options?.modifyDom?.(container);
@@ -12767,7 +12801,7 @@ class RteFontSizePickerFeatureImpl extends RteFeatureImpl {
12767
12801
  toDOM: (mark) => {
12768
12802
  return [
12769
12803
  "span",
12770
- { style: `font-size: ${mark.attrs.size};` },
12804
+ { style: `font-size: ${escapeCssProperty(mark.attrs.size)};` },
12771
12805
  0
12772
12806
  ];
12773
12807
  },
@@ -13437,7 +13471,7 @@ class RteTextColorPickerFeatureImpl extends RteFeatureImpl {
13437
13471
  return [
13438
13472
  "span",
13439
13473
  {
13440
- style: `color: ${color}`,
13474
+ style: `color: ${escapeCssProperty(color)};`,
13441
13475
  "data-text-color": color
13442
13476
  },
13443
13477
  0
@@ -13947,6 +13981,7 @@ const alignments = [
13947
13981
  label: "right"
13948
13982
  }
13949
13983
  ];
13984
+ const validTextAlign = (value) => alignments.find((a) => a.value === value)?.value ?? "left";
13950
13985
  class RteAlignmentFeatureImpl extends RteFeatureImpl {
13951
13986
  constructor() {
13952
13987
  super(...arguments);
@@ -13958,10 +13993,10 @@ class RteAlignmentFeatureImpl extends RteFeatureImpl {
13958
13993
  name: "textAlign",
13959
13994
  default: "left",
13960
13995
  fromDOM(dom) {
13961
- return dom.style.textAlign || "left";
13996
+ return validTextAlign(dom.style.textAlign);
13962
13997
  },
13963
13998
  toStyles(node) {
13964
- return [`text-align: ${node.attrs.textAlign}`];
13999
+ return [`text-align: ${validTextAlign(node.attrs.textAlign)}`];
13965
14000
  }
13966
14001
  })
13967
14002
  ];
@@ -14117,7 +14152,7 @@ class RteLinkFeatureImpl extends RteFeatureImpl {
14117
14152
  ],
14118
14153
  toDOM(node) {
14119
14154
  const { href } = node.attrs;
14120
- return ["a", { href }, 0];
14155
+ return ["a", { href: sanitizeLinkHref(href) }, 0];
14121
14156
  }
14122
14157
  }
14123
14158
  }
@@ -14522,7 +14557,7 @@ class InlineImageView {
14522
14557
  });
14523
14558
  }
14524
14559
  renderImg(src) {
14525
- this.img.src = src;
14560
+ this.img.src = sanitizeImageSrc(src);
14526
14561
  this.setContent(this.img, { allowPopover: true });
14527
14562
  }
14528
14563
  update(node) {
@@ -14560,13 +14595,13 @@ class RteInlineImageFeatureImpl extends RteFeatureImpl {
14560
14595
  },
14561
14596
  parseDOM: [
14562
14597
  {
14563
- tag: "img[src]",
14598
+ tag: "img[src],img[data-src]",
14564
14599
  getAttrs: (dom) => {
14565
14600
  const parseDimension = (dim) => {
14566
14601
  const value = parseInt(dim ?? "", 10);
14567
14602
  return isNaN(value) ? null : value;
14568
14603
  };
14569
- const srcAttr = dom.getAttribute("src");
14604
+ const srcAttr = dom.getAttribute("data-src") ?? dom.getAttribute("src");
14570
14605
  const imageUrl = this.config.parseUrlFromHtml ? this.config.parseUrlFromHtml(srcAttr) : srcAttr;
14571
14606
  if (imageUrl === null) {
14572
14607
  return false;
@@ -14588,8 +14623,14 @@ class RteInlineImageFeatureImpl extends RteFeatureImpl {
14588
14623
  if (resolvedUrl === null) {
14589
14624
  return document.createTextNode("");
14590
14625
  }
14591
- const attrs = { src: resolvedUrl, alt };
14592
- if (size) attrs.style = `max-width: ${size}; height: auto;`;
14626
+ const attrs = {
14627
+ src: sanitizeImageSrc(resolvedUrl),
14628
+ "data-src": resolvedUrl,
14629
+ // Preserve src if it is dropped by sanitization
14630
+ alt
14631
+ };
14632
+ if (size)
14633
+ attrs.style = `max-width: ${escapeCssProperty(size)}; height: auto;`;
14593
14634
  if (naturalWidth) attrs.width = naturalWidth;
14594
14635
  if (naturalHeight) attrs.height = naturalHeight;
14595
14636
  return ["img", attrs];