Release v2.14.1
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m53s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 32s

This commit is contained in:
lzstealth committed 2026-10-11 18:54:43 +01:00
1 parent 635e1a9942
commit c3299abaac
174 files changed
+4483 -1687

No files matched your search

+33 -311
View File
@@ -1,28 +1,21 @@
// General sanitization and sizing helpers.
// Strip unsupported characters from a font family string.
function sanitizeFontFamily(value) {
return String(value || '').replace(/[^a-zA-Z0-9 ,\"-]/g, '').trim();
}
function normalizeStyleAttributeValue(value) {
return String(value || '')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(/'/g, "'");
}
function sanitizeFontSize(value) {
return Math.max(8, Number(value || 0) || 24);
}
function sanitizeTextColor(value, fallback) {
var raw = String(value || '').trim();
if (/^#[0-9a-fA-F]{6}$/.test(raw) || /^#[0-9a-fA-F]{3}$/.test(raw)) {
return raw;
}
return fallback || '#000000';
}
var playerRenderingShared = window.pulsePlayerRenderingShared;
var escapeHtml = playerRenderingShared.escapeHtml;
var normalizeStyleAttributeValue = playerRenderingShared.normalizeStyleAttributeValue;
var sanitizeFontFamily = playerRenderingShared.sanitizeFontFamily;
var sanitizeFontSize = playerRenderingShared.sanitizeFontSize;
var sanitizeTextColor = playerRenderingShared.sanitizeTextColor;
var sanitizeRichTextAttributes = playerRenderingShared.sanitizeRichTextAttributes;
var sanitizeRichText = playerRenderingShared.sanitizeRichText;
var renderEditorJsBlock = playerRenderingShared.renderEditorJsBlock;
var renderEditorJsListItem = playerRenderingShared.renderEditorJsListItem;
var renderEditorJsTable = playerRenderingShared.renderEditorJsTable;
var renderEditorJsContent = playerRenderingShared.renderEditorJsContent;
var parseMaybeJson = playerRenderingShared.parseMaybeJson;
var normalizeContentValue = playerRenderingShared.normalizeContentValue;
var fitCanvasSize = playerRenderingShared.fitCanvasSize;
var decorateRegionMarkup = playerRenderingShared.decorateRegionMarkup;
function getTemplateCanvasSize(template) {
return {
@@ -31,6 +24,17 @@ function getTemplateCanvasSize(template) {
};
}
function selectPlayerMediaSource(originalSource, variants, targetDimension) {
var original = String(originalSource || '').trim();
var target = Math.max(1, Number(targetDimension || 0) || 1);
var candidates = (Array.isArray(variants) ? variants : []).filter(function (variant) {
return variant && String(variant.url || '').trim() && Number(variant.width) >= target;
}).sort(function (left, right) {
return Number(left.width) - Number(right.width);
});
return candidates.length ? String(candidates[0].url) : original;
}
function getPlaylistRevision(data) {
if (data && data.revision) {
return String(data.revision);
@@ -44,36 +48,6 @@ function getPlaylistRevision(data) {
return String(Date.now());
}
function fitCanvasSize(canvasWidth, canvasHeight, maxWidth, maxHeight) {
var width = Math.max(1, Number(canvasWidth || 0) || 1920);
var height = Math.max(1, Number(canvasHeight || 0) || 1080);
var viewportWidth = Math.max(1, Number(maxWidth || 0) || width);
var viewportHeight = Math.max(1, Number(maxHeight || 0) || height);
var scale = Math.min(viewportWidth / width, viewportHeight / height);
return {
width: Math.round(width * scale),
height: Math.round(height * scale)
};
}
function hasPlayerAnimation(config) {
return Boolean(config && ['intro', 'outro', 'loop'].some(function (stepName) {
var preset = String((config[stepName] && config[stepName].preset) || '').trim();
return preset && preset !== 'none';
}));
}
function decorateRegionMarkup(markup, region) {
if (!markup || !region || !hasPlayerAnimation(region.animationConfig)) {
return markup;
}
var animationJson = escapeHtml(JSON.stringify(region.animationConfig));
return markup.replace(/^(\s*)<([a-z0-9-]+)(\s[^>]*)?>/i, function (match, leadingWhitespace, tagName, attributes) {
return leadingWhitespace + '<' + tagName + (attributes || '') + ' data-animation-json="' + animationJson + '">';
});
}
function setPlayerCanvasDimensions(canvasWidth, canvasHeight) {
if (!document || !document.documentElement) {
return;
@@ -85,264 +59,9 @@ function setPlayerCanvasDimensions(canvasWidth, canvasHeight) {
document.documentElement.style.setProperty('--player-canvas-height', height + 'px');
}
const ALLOWED_RICH_TEXT_TAGS = ['a', 'b', 'blockquote', 'br', 'code', 'col', 'colgroup', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'];
function sanitizeRichTextAttributes(tagName, attrText) {
const allowedAttributes = {
a: ['href', 'title', 'target', 'rel', 'class', 'style'],
blockquote: ['class', 'style'],
div: ['class', 'style'],
figure: ['class', 'style'],
figcaption: ['class', 'style'],
h1: ['class', 'style'],
h2: ['class', 'style'],
h3: ['class', 'style'],
h4: ['class', 'style'],
h5: ['class', 'style'],
h6: ['class', 'style'],
img: ['src', 'alt', 'title', 'width', 'height', 'class', 'style', 'loading', 'decoding'],
i: ['class', 'style', 'aria-hidden'],
col: ['class', 'style', 'span', 'width'],
colgroup: ['class', 'style', 'span'],
li: ['class', 'style'],
ol: ['class', 'style', 'start'],
p: ['class', 'style'],
pre: ['class', 'style'],
span: ['class', 'style'],
table: ['class', 'style'],
tbody: ['class', 'style'],
td: ['class', 'style', 'colspan', 'rowspan'],
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
thead: ['class', 'style'],
tr: ['class', 'style'],
ul: ['class', 'style']
};
const allowed = allowedAttributes[tagName] || [];
if (!allowed.length) {
return '';
}
if (tagName === 'img') {
const srcMatch = String(attrText || '').match(/\bsrc\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+))/i);
const srcValue = srcMatch ? String(srcMatch[2] !== undefined ? srcMatch[2] : srcMatch[3] !== undefined ? srcMatch[3] : srcMatch[4] !== undefined ? srcMatch[4] : '').trim() : '';
if (!srcValue || !/^(?:https?:\/\/|\/media\/|\/assets\/|\/[^/]|data:image\/)/i.test(srcValue)) {
return '';
}
}
const attrs = [];
String(attrText || '').replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) => {
const lowerKey = String(key || '').toLowerCase();
if (!allowed.includes(lowerKey)) {
return '';
}
const value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : '';
if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) {
return '';
}
if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) {
return '';
}
if (lowerKey === 'target') {
const targetValue = String(value || '').trim();
if (targetValue === '_blank') {
attrs.push(' target="_blank"');
if (!attrs.includes(' rel="noreferrer noopener"')) {
attrs.push(' rel="noreferrer noopener"');
}
return '';
}
}
attrs.push(' ' + lowerKey + '="' + escapeHtml(lowerKey === 'style' ? normalizeStyleAttributeValue(value) : value) + '"');
return '';
});
return attrs.join('');
}
// Remove unsafe markup while preserving richer rich-text formatting.
function sanitizeRichText(html) {
var output = String(html || '');
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
output = output.replace(/<style[\s\S]*?<\/style>/gi, '');
return output.replace(/<[^>]+>/g, function (tag) {
var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)([\s\S]*?)(\/?)>$/i);
if (!match) {
return '';
}
var closing = Boolean(match[1]);
var name = String(match[2] || '').toLowerCase();
var attrText = String(match[3] || '');
if (ALLOWED_RICH_TEXT_TAGS.indexOf(name) === -1) {
return '';
}
if (closing) {
return '</' + name + '>';
}
return '<' + name + sanitizeRichTextAttributes(name, attrText) + '>';
});
}
// Render a single Editor.js block to HTML.
function renderEditorJsBlock(block) {
if (!block || !block.type || !block.data) {
return '';
}
if (block.type === 'header') {
var level = Math.max(1, Math.min(6, Number(block.data.level || 2)));
return '<h' + level + '>' + sanitizeRichText(block.data.text || '') + '</h' + level + '>';
}
if (block.type === 'list') {
var tag = block.data.style === 'ordered' ? 'ol' : 'ul';
var items = Array.isArray(block.data.items) ? block.data.items : [];
return '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + items.map(function (item) { return renderEditorJsListItem(item, tag); }).join('') + '</' + tag + '>';
}
if (block.type === 'delimiter') {
return '<hr />';
}
if (block.type === 'code') {
return '<pre><code>' + escapeHtml(block.data.code || '') + '</code></pre>';
}
if (block.type === 'table') {
return renderEditorJsTable(block.data);
}
if (block.type === 'paragraph') {
return '<p>' + sanitizeRichText(block.data.text || '') + '</p>';
}
return '';
}
// Render a list item and any nested sub-items.
function renderEditorJsListItem(item, tag) {
if (item && typeof item === 'object') {
var content = item.content !== undefined ? item.content : (item.text !== undefined ? item.text : item.value !== undefined ? item.value : '');
var children = Array.isArray(item.items) ? item.items : Array.isArray(item.subItems) ? item.subItems : Array.isArray(item.children) ? item.children : [];
var nested = children.length ? '<' + tag + ' style="list-style-type:' + (tag === 'ol' ? 'decimal' : 'disc') + ';padding-left:1.4em;">' + children.map(function (child) { return renderEditorJsListItem(child, tag); }).join('') + '</' + tag + '>' : '';
return '<li>' + sanitizeRichText(content || '') + nested + '</li>';
}
return '<li>' + sanitizeRichText(item || '') + '</li>';
}
// Render an Editor.js table block.
function renderEditorJsTable(data) {
var rows = Array.isArray(data.content) ? data.content : Array.isArray(data.rows) ? data.rows : [];
if (!rows.length) {
return '';
}
var hasHeadings = Boolean(data.withHeadings);
var tableRows = rows.map(function (row, rowIndex) {
var cells = Array.isArray(row) ? row : [];
var cellTag = hasHeadings && rowIndex === 0 ? 'th' : 'td';
var cellAttrs = cellTag === 'th' ? ' scope="col"' : '';
return '<tr>' + cells.map(function (cell) {
return '<' + cellTag + cellAttrs + '>' + sanitizeRichText(cell || '') + '</' + cellTag + '>';
}).join('') + '</tr>';
}).join('');
return '<table class="ck-content-table">' + tableRows + '</table>';
}
function wrapRichTextParagraph(html) {
var raw = String(html || '').trim();
if (!raw) {
return '';
}
if (/^<\s*(?:p|div|h[1-6]|ul|ol|pre|table|blockquote|figure|hr|li)\b/i.test(raw)) {
return raw;
}
return '<p>' + raw + '</p>';
}
// Render Editor.js JSON or plain content safely.
function renderEditorJsContent(value) {
if (value && typeof value === 'object') {
if (Array.isArray(value.blocks)) {
return value.blocks.map(renderEditorJsBlock).join('');
}
if (value.value !== undefined) {
return renderEditorJsContent(value.value);
}
}
var raw = String(value || '');
try {
var parsed = JSON.parse(raw);
if (parsed && Array.isArray(parsed.blocks)) {
return parsed.blocks.map(renderEditorJsBlock).join('');
}
} catch (_error) {
// fall through to legacy HTML rendering
}
return wrapRichTextParagraph(sanitizeRichText(raw));
}
// Parse string values that look like JSON.
function parseMaybeJson(value) {
if (typeof value !== 'string') {
return value;
}
var raw = value.trim();
if (!raw) {
return value;
}
if (raw.charAt(0) !== '{' && raw.charAt(0) !== '[') {
return value;
}
try {
return JSON.parse(raw);
} catch (_error) {
return value;
}
}
// Normalize a slide region's stored content value.
function normalizeContentValue(value) {
var normalized;
if (!value || typeof value !== 'object') {
return {
type: 'text',
value: parseMaybeJson(value)
};
}
normalized = {};
Object.keys(value).forEach(function (key) {
normalized[key] = value[key];
});
if (normalized.value !== undefined) {
normalized.value = parseMaybeJson(normalized.value);
}
if (normalized.font_family !== undefined && normalized.font_family !== null) {
normalized.font_family = sanitizeFontFamily(normalized.font_family);
}
if (normalized.font_size !== undefined && normalized.font_size !== null) {
normalized.font_size = sanitizeFontSize(normalized.font_size);
}
if (normalized.font_color !== undefined && normalized.font_color !== null) {
normalized.font_color = sanitizeTextColor(normalized.font_color);
}
if (!normalized.type) {
normalized.type = 'text';
}
return normalized;
}
// Normalize a slide and its nested region content.
function normalizeSlide(slide) {
var normalized = {};
var content;
Object.keys(slide || {}).forEach(function (key) {
normalized[key] = slide[key];
});
normalized.content = {};
content = slide && slide.content && typeof slide.content === 'object' ? slide.content : {};
Object.keys(content).forEach(function (regionKey) {
normalized.content[regionKey] = normalizeContentValue(content[regionKey]);
});
var normalized = playerRenderingShared.normalizeSlide(slide);
if (normalized.use_video_duration && normalized.template && Array.isArray(normalized.template.regions)) {
normalized.template.regions.forEach(function (region) {
if (region && region.region_type === 'video' && normalized.content[region.region_key]) {
@@ -520,6 +239,8 @@ function getTemplateLayout(template) {
baseStyle: baseStyle,
pixelWidth: pixelWidth,
pixelHeight: pixelHeight,
canvasWidth: templateCanvas.width,
canvasHeight: templateCanvas.height,
fontFamily: region.font_family || null,
fontSize: region.font_size || null,
fontColor: region.font_color || null,
@@ -530,7 +251,7 @@ function getTemplateLayout(template) {
var layout = {
canvasWidth: canvasSize.width,
canvasHeight: canvasSize.height,
background: template.background_image_path ? '<img class="template-background" src="' + escapeHtml(template.background_image_path) + '" alt="" />' : '',
background: template.background_image_path ? '<img class="template-background" src="' + escapeHtml(selectPlayerMediaSource(template.background_image_path, template.background_image_variants, Math.max(templateCanvas.width, templateCanvas.height))) + '" alt="" />' : '',
backgroundColor: template.background_color || '#111111',
backgroundGradient: template.background_gradient || '',
regions: regions
@@ -641,7 +362,8 @@ function renderTemplateSlideMarkup(slide) {
animationConfig: normalizePlayerAnimationConfig(region.animationJson)
});
}).join('') : '';
const stageStyle = layout ? buildBackdropStyle(layout.backgroundColor || '#111111', template.background_image_path, layout.backgroundGradient) : '';
const backgroundPath = layout ? selectPlayerMediaSource(template.background_image_path, template.background_image_variants, Math.max(layout.canvasWidth, layout.canvasHeight)) : '';
const stageStyle = layout ? buildBackdropStyle(layout.backgroundColor || '#111111', backgroundPath, layout.backgroundGradient) : '';
if (layout) {
setPlayerCanvasDimensions(layout.canvasWidth, layout.canvasHeight);
}