@xiboplayer/renderer 0.7.13 → 0.7.14

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.
Files changed (2) hide show
  1. package/package.json +4 -4
  2. package/src/layout.js +21 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiboplayer/renderer",
3
- "version": "0.7.13",
3
+ "version": "0.7.14",
4
4
  "description": "RendererLite - Fast, efficient XLF layout rendering engine",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -12,9 +12,9 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "pdfjs-dist": "^5.6.205",
15
- "@xiboplayer/utils": "0.7.13",
16
- "@xiboplayer/schedule": "0.7.13",
17
- "@xiboplayer/cache": "0.7.13"
15
+ "@xiboplayer/utils": "0.7.14",
16
+ "@xiboplayer/cache": "0.7.14",
17
+ "@xiboplayer/schedule": "0.7.14"
18
18
  },
19
19
  "devDependencies": {
20
20
  "jsdom": "^29.0.1",
package/src/layout.js CHANGED
@@ -10,6 +10,18 @@ import { createLogger, isDebug, PLAYER_API } from '@xiboplayer/utils';
10
10
 
11
11
  const log = createLogger('Layout');
12
12
 
13
+ // ── Safe interpolation helpers for HTML generation ─────────────────────────
14
+ // Use these instead of raw ${value} in template literals to prevent XSS.
15
+ // Our LayoutTranslator generates complete HTML documents as strings (unlike
16
+ // upstream Xibo players that use DOM APIs). This gives us pre-rendering and
17
+ // cross-context support (Node.js, arexibo, service worker) but requires
18
+ // manual sanitization at every interpolation point.
19
+ const SAFE_CSS_COLOR = /^(#[0-9a-fA-F]{3,8}|rgba?\(\s*[\d.,\s%]+\)|[a-zA-Z]{1,20}|transparent|inherit)$/;
20
+ export const safeCssColor = (v) => SAFE_CSS_COLOR.test(v) ? v : '#000000';
21
+ export const safeJsString = (v) => JSON.stringify(v);
22
+ export const safeHtmlAttr = (v) => String(v).replace(/[&"'<>]/g, c =>
23
+ ({ '&': '&amp;', '"': '&quot;', "'": '&#39;', '<': '&lt;', '>': '&gt;' }[c]));
24
+
13
25
  export class LayoutTranslator {
14
26
  constructor(xmds) {
15
27
  this.xmds = xmds;
@@ -29,7 +41,7 @@ export class LayoutTranslator {
29
41
 
30
42
  const width = parseInt(layoutEl.getAttribute('width') || '1920');
31
43
  const height = parseInt(layoutEl.getAttribute('height') || '1080');
32
- const bgcolor = layoutEl.getAttribute('bgcolor') || '#000000';
44
+ const bgcolor = safeCssColor(layoutEl.getAttribute('bgcolor') || '#000000');
33
45
 
34
46
  const regions = [];
35
47
  for (const regionEl of doc.querySelectorAll('region')) {
@@ -416,7 +428,7 @@ ${mediaJS}
416
428
  if (!iframe) {
417
429
  iframe = document.createElement('iframe');
418
430
  iframe.id = '${iframeId}';
419
- iframe.src = '${widgetUrl}';
431
+ iframe.src = ${safeJsString(widgetUrl)};
420
432
  iframe.style.width = '100%';
421
433
  iframe.style.height = '100%';
422
434
  iframe.style.border = 'none';
@@ -479,7 +491,7 @@ ${mediaJS}
479
491
  const region = document.getElementById('region_${regionId}');
480
492
  const img = document.createElement('img');
481
493
  img.className = 'media';
482
- img.src = '${imageSrc}';
494
+ img.src = ${safeJsString(imageSrc)};
483
495
  img.style.opacity = '0';
484
496
  region.innerHTML = '';
485
497
  region.appendChild(img);
@@ -506,8 +518,8 @@ ${mediaJS}
506
518
  const region = document.getElementById('region_${regionId}');
507
519
  const video = document.createElement('video');
508
520
  video.className = 'media';
509
- video.src = '${videoSrc}';
510
- video.dataset.filename = '${videoFilename}';
521
+ video.src = ${safeJsString(videoSrc)};
522
+ video.dataset.filename = ${safeJsString(videoFilename)};
511
523
  video.autoplay = true;
512
524
  video.muted = ${media.options.mute === '1' ? 'true' : 'false'};
513
525
  video.loop = false;
@@ -518,7 +530,7 @@ ${mediaJS}
518
530
 
519
531
  // Retry loading if cache completes while video is playing
520
532
  const retryOnCache = (event) => {
521
- if (event.detail.filename === '${videoFilename}' && video.error) {
533
+ if (event.detail.filename === ${safeJsString(videoFilename)} && video.error) {
522
534
  console.log('[Video] Cache complete, reloading:', '${videoFilename}');
523
535
  video.load();
524
536
  video.play();
@@ -595,7 +607,7 @@ ${mediaJS}
595
607
  const audio = document.createElement('audio');
596
608
  audio.id = '${audioId}';
597
609
  audio.className = 'media';
598
- audio.src = '${audioSrc}';
610
+ audio.src = ${safeJsString(audioSrc)};
599
611
  audio.autoplay = true;
600
612
  audio.loop = ${audioLoop};
601
613
  audio.volume = ${audioVolume};
@@ -734,7 +746,7 @@ ${mediaJS}
734
746
 
735
747
  // Render PDF with multi-page support
736
748
  try {
737
- const loadingTask = pdfjsLib.getDocument('${pdfSrc}');
749
+ const loadingTask = pdfjsLib.getDocument(${safeJsString(pdfSrc)});
738
750
  const pdf = await loadingTask.promise;
739
751
  const totalPages = pdf.numPages;
740
752
 
@@ -893,7 +905,7 @@ ${mediaJS}
893
905
  startFn = `() => {
894
906
  const region = document.getElementById('region_${regionId}');
895
907
  const iframe = document.createElement('iframe');
896
- iframe.src = '${url}';
908
+ iframe.src = ${safeJsString(url)};
897
909
  iframe.style.opacity = '0';
898
910
  region.innerHTML = '';
899
911
  region.appendChild(iframe);