Release v2.15.4
This commit is contained in:
1 parent
64aeb1b47e
commit
05c9341b08
25 files changed
+595
-31
No files matched your search
@@ -17,6 +17,7 @@ const AUDIT_EVENT_CATEGORIES = Object.freeze({
|
||||
RSS_FEEDS: 'rss-feeds',
|
||||
TIMETABLES: 'timetables',
|
||||
WEATHER: 'weather',
|
||||
MEDIA_ASSETS: 'media-assets',
|
||||
SCREEN_CONTROLS: 'screen-controls'
|
||||
});
|
||||
const AUDIT_CATEGORY_KEYS = Object.freeze(Object.values(AUDIT_EVENT_CATEGORIES));
|
||||
@@ -37,6 +38,7 @@ const AUDIT_CATEGORY_LABELS = Object.freeze({
|
||||
'rss-feeds': 'RSS Feeds',
|
||||
timetables: 'Timetables',
|
||||
weather: 'Weather',
|
||||
'media-assets': 'Media Assets',
|
||||
'screen-controls': 'Screen Controls'
|
||||
});
|
||||
const SCREEN_CONTROL_COMMAND_KEYS = Object.freeze(['pause', 'blackout', 'reload', 'navigation', 'moveclient', 'setclientname']);
|
||||
|
||||
@@ -417,6 +417,24 @@ async function buildTemplateContent(pool, template, body, filesByField, existing
|
||||
delete generic.timetable_display_mode;
|
||||
delete generic.timetable_max_items;
|
||||
|
||||
generic.date_filter = ['between', 'before', 'after'].includes(generic.date_filter) ? generic.date_filter : 'all';
|
||||
for (const field of ['date_from', 'date_to']) {
|
||||
const value = String(generic[field] || '').trim();
|
||||
const date = value ? new Date(value) : null;
|
||||
const active = generic.date_filter === 'between' || generic.date_filter === (field === 'date_from' ? 'after' : 'before');
|
||||
if (active && date && Number.isNaN(date.getTime())) {
|
||||
const error = new Error('Timetable date range must contain valid date-times.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
generic[field] = date && !Number.isNaN(date.getTime()) ? date.toISOString() : '';
|
||||
}
|
||||
if (generic.date_filter === 'between' && generic.date_from && generic.date_to && generic.date_from > generic.date_to) {
|
||||
const error = new Error('Timetable date range must end on or after its start.');
|
||||
error.statusCode = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
generic.text = normalizedText;
|
||||
generic.value = normalizedText;
|
||||
generic.type = region.region_type;
|
||||
|
||||
@@ -63,9 +63,16 @@ function isLive(entry, now) {
|
||||
return Boolean(start && end && now >= start && now < end);
|
||||
}
|
||||
|
||||
function getVisibleEntries(groupId, displayMode, maxItems, groups) {
|
||||
function getVisibleEntries(groupId, displayMode, maxItems, groups, config) {
|
||||
var now = new Date();
|
||||
var entries = getEntries(groupId, groups).slice().sort(function (left, right) {
|
||||
var range = config || {};
|
||||
var from = toDate(range.date_filter === 'between' || range.date_filter === 'after' ? range.date_from : '');
|
||||
var to = toDate(range.date_filter === 'between' || range.date_filter === 'before' ? range.date_to : '');
|
||||
var candidates = getEntries(groupId, groups).filter(function (entry) {
|
||||
var start = toDate(entry && entry.start_datetime);
|
||||
return (!from && !to) || Boolean(start && (!from || start >= from) && (!to || start <= to));
|
||||
});
|
||||
var entries = candidates.slice().sort(function (left, right) {
|
||||
var leftStart = toDate(left && left.start_datetime);
|
||||
var rightStart = toDate(right && right.start_datetime);
|
||||
return (leftStart ? leftStart.getTime() : 0) - (rightStart ? rightStart.getTime() : 0) || Number(left.id || 0) - Number(right.id || 0);
|
||||
@@ -83,7 +90,7 @@ function getVisibleEntries(groupId, displayMode, maxItems, groups) {
|
||||
});
|
||||
|
||||
if (!entries.length && mode === 'upcoming') {
|
||||
entries = getEntries(groupId, groups).filter(function (entry) {
|
||||
entries = candidates.filter(function (entry) {
|
||||
return isLive(entry, now);
|
||||
});
|
||||
}
|
||||
@@ -135,7 +142,7 @@ function renderRegion(region, regionContent) {
|
||||
var displayMode = regionContent && regionContent.display_mode ? regionContent.display_mode : 'upcoming';
|
||||
var maxItems = regionContent && regionContent.max_items !== undefined ? regionContent.max_items : 5;
|
||||
var group = getGroupById(groupId, groups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups, regionContent);
|
||||
var timeZone = group && (group.timezone || group.time_zone) ? String(group.timezone || group.time_zone) : '';
|
||||
var width = Math.max(1, Math.round(Number(region && region.pixelWidth ? region.pixelWidth : 0) || 1));
|
||||
var height = Math.max(1, Math.round(Number(region && region.pixelHeight ? region.pixelHeight : 0) || 1));
|
||||
|
||||
@@ -124,6 +124,7 @@ async function start() {
|
||||
// Middleware and route registration.
|
||||
registerMiddleware(app, {
|
||||
pool: pool,
|
||||
pages: pages,
|
||||
loadCurrentUser: loadCurrentUser,
|
||||
requireAuth: requireAuth,
|
||||
MEDIA_DIR: webConfig.mediaDir,
|
||||
|
||||
@@ -250,7 +250,7 @@ async function countMediaAssetReferences(pool, mediaPath) {
|
||||
const [slideRows] = await pool.query(
|
||||
`SELECT COUNT(*) AS ref_count
|
||||
FROM c_slides
|
||||
WHERE LOCATE(?, COALESCE(content_json, '')) > 0`,
|
||||
WHERE LOCATE(?, CAST(COALESCE(content_json, '') AS CHAR)) > 0`,
|
||||
[normalizedPath]
|
||||
);
|
||||
const [templateRows] = await pool.query(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const registerErrorPageMiddleware = require('#src/web/middleware/error-page');
|
||||
|
||||
module.exports = function registerMiddleware(app, deps) {
|
||||
const pool = deps.pool;
|
||||
@@ -28,6 +29,8 @@ module.exports = function registerMiddleware(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
registerErrorPageMiddleware(app, deps.pages);
|
||||
|
||||
app.use(function (req, res, next) {
|
||||
if (req.path === '/' || req.path === '/login' || req.path === '/logout' || req.path === '/forgot-password' || req.path === '/reset-password' || req.path === '/verify-email' || req.path === '/slides/popup-preview' || req.path.indexOf('/api/internal/slide-thumbnails/') === 0 || req.path === '/api/internal/sync/player-media' || req.path === '/api/internal/sync/player-font' || req.path === '/api/internal/sync/player-control') {
|
||||
return next();
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
function wantsHtmlErrorPage(req, res) {
|
||||
const isXhr = String(req.get && req.get('X-Requested-With') || '').toLowerCase() === 'xmlhttprequest';
|
||||
const requestPath = String(req.originalUrl || req.path || '');
|
||||
const acceptsHtml = !req.accepts || req.accepts('html');
|
||||
const contentType = String(res.get && res.get('Content-Type') || '').toLowerCase();
|
||||
|
||||
return !isXhr && requestPath.indexOf('/api/') !== 0 && acceptsHtml && (!contentType || contentType.indexOf('text/html') === 0);
|
||||
}
|
||||
|
||||
function createErrorPage(pages, req, statusCode, message) {
|
||||
const isServerError = statusCode >= 500;
|
||||
return pages.renderErrorPage({
|
||||
statusCode: statusCode,
|
||||
message: isServerError ? '' : String(message || ''),
|
||||
detail: isServerError ? 'The server could not complete the request.' : '',
|
||||
backUrl: req.currentUser ? '/dashboard' : '/login',
|
||||
backLabel: req.currentUser ? 'Back to dashboard' : 'Sign in'
|
||||
}, req.currentUser);
|
||||
}
|
||||
|
||||
module.exports = function registerErrorPageMiddleware(app, pages) {
|
||||
app.use(function (req, res, next) {
|
||||
const send = res.send;
|
||||
const sendStatus = res.sendStatus;
|
||||
|
||||
res.send = function (body) {
|
||||
const statusCode = Number(res.statusCode) || 200;
|
||||
const isHtml = typeof body === 'string' && /^\s*</.test(body);
|
||||
|
||||
if (statusCode >= 400 && statusCode < 600 && !isHtml && wantsHtmlErrorPage(req, res)) {
|
||||
return send.call(res, createErrorPage(pages, req, statusCode, body));
|
||||
}
|
||||
|
||||
return send.apply(res, arguments);
|
||||
};
|
||||
|
||||
res.sendStatus = function (statusCode) {
|
||||
if (statusCode >= 400 && statusCode < 600 && wantsHtmlErrorPage(req, res)) {
|
||||
res.statusCode = statusCode;
|
||||
return res.send(createErrorPage(pages, req, statusCode));
|
||||
}
|
||||
|
||||
return sendStatus.call(res, statusCode);
|
||||
};
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
@@ -98,15 +98,40 @@
|
||||
return Boolean(start && now < start);
|
||||
}
|
||||
|
||||
function toLocalInput(value) {
|
||||
var date = toDate(value);
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
function pad(part) { return String(part).padStart(2, '0'); }
|
||||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||
}
|
||||
|
||||
function getDateRangeConfig(regionId, card, current) {
|
||||
var config = {};
|
||||
['date_filter', 'date_from', 'date_to'].forEach(function (field) {
|
||||
var input = card && card.querySelector ? card.querySelector('[name="region_' + field + '_' + regionId + '"]') : null;
|
||||
config[field] = input ? input.value : current[field] || (field === 'date_filter' ? 'all' : '');
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
function isLive(entry, now) {
|
||||
var start = toDate(entry && entry.start_datetime);
|
||||
var end = toDate(entry && entry.end_datetime);
|
||||
return Boolean(start && end && now >= start && now < end);
|
||||
}
|
||||
|
||||
function getVisibleEntries(groupId, displayMode, maxItems, groups) {
|
||||
function getVisibleEntries(groupId, displayMode, maxItems, groups, config) {
|
||||
var now = new Date();
|
||||
var entries = getEntries(groupId, groups).slice().sort(function (left, right) {
|
||||
var range = config || {};
|
||||
var from = toDate(range.date_filter === 'between' || range.date_filter === 'after' ? range.date_from : '');
|
||||
var to = toDate(range.date_filter === 'between' || range.date_filter === 'before' ? range.date_to : '');
|
||||
var candidates = getEntries(groupId, groups).filter(function (entry) {
|
||||
var start = toDate(entry && entry.start_datetime);
|
||||
return (!from && !to) || Boolean(start && (!from || start >= from) && (!to || start <= to));
|
||||
});
|
||||
var entries = candidates.slice().sort(function (left, right) {
|
||||
var leftStart = toDate(left && left.start_datetime);
|
||||
var rightStart = toDate(right && right.start_datetime);
|
||||
return (leftStart ? leftStart.getTime() : 0) - (rightStart ? rightStart.getTime() : 0) || Number(left.id || 0) - Number(right.id || 0);
|
||||
@@ -124,7 +149,7 @@
|
||||
});
|
||||
|
||||
if (!entries.length && mode === 'upcoming') {
|
||||
entries = getEntries(groupId, groups).filter(function (entry) {
|
||||
entries = candidates.filter(function (entry) {
|
||||
return isLive(entry, now);
|
||||
});
|
||||
}
|
||||
@@ -167,6 +192,9 @@
|
||||
display_mode: String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming',
|
||||
value: String(current.text !== undefined ? current.text : current.value !== undefined ? current.value : ''),
|
||||
max_items: Math.max(1, Number(current.max_items || current.timetable_max_items || 5)),
|
||||
date_filter: ['between', 'before', 'after'].includes(current.date_filter) ? current.date_filter : 'all',
|
||||
date_from: current.date_from || '',
|
||||
date_to: current.date_to || '',
|
||||
font_family: current.font_family || region.font_family || getDefaultStyle().font_family,
|
||||
font_size: current.font_size || region.font_size || getDefaultStyle().font_size,
|
||||
font_color: current.font_color || region.font_color || getDefaultStyle().font_color
|
||||
@@ -230,8 +258,9 @@
|
||||
var displayMode = timetableDisplayModeInput ? timetableDisplayModeInput.value : String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
|
||||
var maxItems = Math.max(1, Number(timetableMaxItemsInput ? timetableMaxItemsInput.value : current.max_items || current.timetable_max_items || 5));
|
||||
var group = getGroupById(groupId, timetableGroups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, timetableGroups);
|
||||
if (!entries.length) {
|
||||
var range = getDateRangeConfig(regionId, card, current);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, timetableGroups, range);
|
||||
if (!entries.length && range.date_filter === 'all') {
|
||||
entries = getEntries(groupId, timetableGroups).slice(0, maxItems);
|
||||
}
|
||||
|
||||
@@ -267,9 +296,10 @@
|
||||
var currentGroupId = current.timetable_group_id === undefined || current.timetable_group_id === null || current.timetable_group_id === '' ? '' : Number(current.timetable_group_id);
|
||||
var currentDisplayMode = String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
|
||||
var currentMaxItems = Math.max(1, Number(current.max_items || current.timetable_max_items || 5));
|
||||
var dateFilter = ['between', 'before', 'after'].includes(current.date_filter) ? current.date_filter : 'all';
|
||||
var currentGroup = getGroupById(currentGroupId, timetableGroups);
|
||||
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, timetableGroups);
|
||||
if (!currentEntries.length) {
|
||||
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, timetableGroups, current);
|
||||
if (!currentEntries.length && dateFilter === 'all') {
|
||||
currentEntries = getEntries(currentGroupId, timetableGroups).slice(0, currentMaxItems);
|
||||
}
|
||||
var groupOptions = timetableGroups.map(function (group) {
|
||||
@@ -295,7 +325,7 @@
|
||||
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(currentValue) + '" />' +
|
||||
'</div>' +
|
||||
'<div class="row g-3">' +
|
||||
'<div class="col-12 col-lg-6">' +
|
||||
'<div class="col-12 col-lg-4">' +
|
||||
'<label class="form-label" for="region_timetable_group_id_' + region.id + '">Timetable group</label>' +
|
||||
'<select id="region_timetable_group_id_' + region.id + '" name="region_timetable_group_id_' + region.id + '" class="form-select">' +
|
||||
'<option value="">Select a group</option>' +
|
||||
@@ -310,12 +340,34 @@
|
||||
'<option value="both"' + (currentDisplayMode === 'both' ? ' selected' : '') + '>Both</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="col-6 col-lg-3">' +
|
||||
'<div class="col-6 col-lg-2">' +
|
||||
'<label class="form-label" for="region_timetable_max_items_' + region.id + '">Max items</label>' +
|
||||
'<input id="region_timetable_max_items_' + region.id + '" type="number" min="1" step="1" name="region_timetable_max_items_' + region.id + '" class="form-control" value="' + escapeHtml(currentMaxItems || 5) + '" />' +
|
||||
'</div>' +
|
||||
'<div class="col-12 col-lg-3">' +
|
||||
'<label class="form-label" for="region_date_filter_' + region.id + '">Date filter</label>' +
|
||||
'<select id="region_date_filter_' + region.id + '" name="region_date_filter_' + region.id + '" class="form-select">' +
|
||||
'<option value="all"' + (dateFilter === 'all' ? ' selected' : '') + '>All</option>' +
|
||||
'<option value="between"' + (dateFilter === 'between' ? ' selected' : '') + '>Between Dates</option>' +
|
||||
'<option value="before"' + (dateFilter === 'before' ? ' selected' : '') + '>Before Date</option>' +
|
||||
'<option value="after"' + (dateFilter === 'after' ? ' selected' : '') + '>After Date</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div data-timetable-date-range' + (dateFilter === 'all' ? ' hidden' : '') + '>' +
|
||||
'<div class="row g-3">' +
|
||||
['from', 'to'].map(function (boundary) {
|
||||
var field = 'date_' + boundary;
|
||||
var enabled = dateFilter === 'between' || dateFilter === (boundary === 'from' ? 'after' : 'before');
|
||||
var label = boundary === 'from' ? 'Starts on or after (local time)' : 'Starts on or before (local time)';
|
||||
return '<div class="col-12 col-md-6" data-timetable-boundary' + (!enabled ? ' hidden' : '') + '>' +
|
||||
'<label class="form-label" for="region_' + field + '_' + region.id + '">' + label + '</label>' +
|
||||
'<input id="region_' + field + '_' + region.id + '" type="datetime-local" step="1" class="form-control" data-timetable-date="' + field + '" value="' + escapeHtml(toLocalInput(current[field])) + '"' + (!enabled ? ' disabled' : '') + ' />' +
|
||||
'<input type="hidden" name="region_' + field + '_' + region.id + '" value="' + escapeHtml(current[field] || '') + '" />' +
|
||||
'</div>';
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="text-body-secondary small">Use the selected group and display mode to choose which timetable entry fields are available.</div>' +
|
||||
'<div class="api-region-placeholder-section schedule-placeholder-section" data-schedule-placeholder-chips>' +
|
||||
'<div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2">Available placeholders ' + renderPlaceholderInfo(region.id) + '</div>' +
|
||||
'<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</div>' +
|
||||
@@ -350,24 +402,50 @@
|
||||
panel.innerHTML = renderSchedulePlaceholderChips(current.group, current.entries);
|
||||
}
|
||||
|
||||
function updateDateRange(card) {
|
||||
var regionId = card.getAttribute('data-region-id');
|
||||
var config = getDateRangeConfig(regionId, card, {});
|
||||
var panel = card.querySelector('[data-timetable-date-range]');
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
panel.hidden = config.date_filter === 'all';
|
||||
panel.querySelectorAll('[data-timetable-date]').forEach(function (input) {
|
||||
var field = input.getAttribute('data-timetable-date');
|
||||
var enabled = config.date_filter === 'between' || config.date_filter === (field === 'date_from' ? 'after' : 'before');
|
||||
input.closest('[data-timetable-boundary]').hidden = !enabled;
|
||||
input.disabled = !enabled;
|
||||
var date = toDate(input.value);
|
||||
card.querySelector('[name="region_' + input.getAttribute('data-timetable-date') + '_' + regionId + '"]').value = date ? date.toISOString() : '';
|
||||
});
|
||||
var fromInput = panel.querySelector('[data-timetable-date="date_from"]');
|
||||
var toInput = panel.querySelector('[data-timetable-date="date_to"]');
|
||||
var from = toDate(fromInput.value);
|
||||
var to = toDate(toInput.value);
|
||||
toInput.setCustomValidity(config.date_filter === 'between' && from && to && from > to ? 'The end of the date range must be on or after its start.' : '');
|
||||
}
|
||||
|
||||
if (!window.__scheduleRegionPlaceholderRefreshInstalled) {
|
||||
window.__scheduleRegionPlaceholderRefreshInstalled = true;
|
||||
|
||||
document.addEventListener('change', function (event) {
|
||||
function handleTimetableInput(event) {
|
||||
var target = event.target;
|
||||
if (!target || !target.closest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.closest('select[name^="region_timetable_group_id_"], select[name^="region_timetable_display_mode_"], input[name^="region_timetable_max_items_"]')) {
|
||||
if (!target.closest('select[name^="region_timetable_group_id_"], select[name^="region_timetable_display_mode_"], input[name^="region_timetable_max_items_"], select[name^="region_date_filter_"], [data-timetable-date]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var card = target.closest('[data-region-id]');
|
||||
if (card) {
|
||||
updateDateRange(card);
|
||||
updateSchedulePlaceholderPanel(card);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
document.addEventListener('input', handleTimetableInput, true);
|
||||
document.addEventListener('change', handleTimetableInput, true);
|
||||
}
|
||||
|
||||
function buildEditorCardContext(context) {
|
||||
@@ -410,6 +488,7 @@
|
||||
tz_long: timezoneValues.tz_long,
|
||||
timeZone: timezoneValues.tz_long,
|
||||
max_items: timetableMaxItemsInput ? timetableMaxItemsInput.value || current.max_items : current.max_items,
|
||||
...getDateRangeConfig(region.id, card, current),
|
||||
existingContent: existingContent || {},
|
||||
timetableGroups: Array.isArray(timetableGroups) ? timetableGroups : []
|
||||
};
|
||||
@@ -423,7 +502,7 @@
|
||||
var displayMode = regionContent && regionContent.display_mode ? regionContent.display_mode : 'upcoming';
|
||||
var maxItems = regionContent && regionContent.max_items !== undefined ? regionContent.max_items : 5;
|
||||
var group = getGroupById(groupId, groups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups, regionContent);
|
||||
|
||||
if (!value) {
|
||||
return '<div class="slide-preview-region timetable" style="width:100%;height:100%;overflow:hidden;' + (style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '') + (style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '') + (style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '') + '"></div>';
|
||||
|
||||
@@ -334,6 +334,7 @@ function registerSettingsAndContentRoutes(app, deps) {
|
||||
uploadDir: deps.uploadDir,
|
||||
backgroundTaskQueue: deps.backgroundTaskQueue,
|
||||
setAuthMessageCookie: deps.setAuthMessageCookie,
|
||||
recordRequestAuditEvent: deps.recordRequestAuditEvent,
|
||||
requirePermission: deps.requirePermission
|
||||
});
|
||||
|
||||
@@ -344,6 +345,7 @@ function registerSettingsAndContentRoutes(app, deps) {
|
||||
uploadDir: deps.uploadDir,
|
||||
getAuditUserId: deps.getAuditUserId,
|
||||
setAuthMessageCookie: deps.setAuthMessageCookie,
|
||||
recordRequestAuditEvent: deps.recordRequestAuditEvent,
|
||||
requirePermission: deps.requirePermission,
|
||||
syncPlaylistUploadsOnChange: deps.syncPlaylistUploadsOnChange
|
||||
});
|
||||
|
||||
@@ -123,6 +123,7 @@ module.exports = function registerFontRoutes(app, deps) {
|
||||
const uploadDir = deps.uploadDir;
|
||||
const backgroundTaskQueue = deps.backgroundTaskQueue;
|
||||
const setAuthMessageCookie = deps.setAuthMessageCookie;
|
||||
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
||||
|
||||
const FONT_FAMILY_MAX_LENGTH = 255;
|
||||
|
||||
@@ -146,6 +147,13 @@ module.exports = function registerFontRoutes(app, deps) {
|
||||
}
|
||||
|
||||
const result = await fontLibrary.addFontFile(uploadDir, req.file, String(req.body && req.body.font_family || '').trim().slice(0, FONT_FAMILY_MAX_LENGTH));
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(pool, req, {
|
||||
category: 'media-assets', eventType: 'font.uploaded', actorUserId: req.currentUser && req.currentUser.id,
|
||||
targetType: 'font', targetId: result.font.id, targetLabel: result.font.family,
|
||||
details: { fileName: result.font.fileName, format: result.font.format }
|
||||
});
|
||||
}
|
||||
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
|
||||
await backgroundTaskQueue.enqueueTask({
|
||||
key: `font-sync:${result && result.font && result.font.id ? result.font.id : Date.now()}`,
|
||||
@@ -186,6 +194,14 @@ module.exports = function registerFontRoutes(app, deps) {
|
||||
return res.redirect('/settings/fonts?message=' + encodeURIComponent('That font was not found.'));
|
||||
}
|
||||
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(pool, req, {
|
||||
category: 'media-assets', eventType: enabled ? 'font.enabled' : 'font.disabled', actorUserId: req.currentUser && req.currentUser.id,
|
||||
targetType: 'font', targetId: updated.id, targetLabel: updated.family || updated.name,
|
||||
details: { enabled: Boolean(enabled) }
|
||||
});
|
||||
}
|
||||
|
||||
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
|
||||
await backgroundTaskQueue.enqueueTask({
|
||||
key: `font-sync:toggle:${updated && updated.id ? updated.id : Date.now()}`,
|
||||
@@ -222,6 +238,14 @@ module.exports = function registerFontRoutes(app, deps) {
|
||||
return res.redirect('/settings/fonts?message=' + encodeURIComponent('That font was not found.'));
|
||||
}
|
||||
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(pool, req, {
|
||||
category: 'media-assets', eventType: 'font.deleted', actorUserId: req.currentUser && req.currentUser.id,
|
||||
targetType: 'font', targetId: removed.id, targetLabel: removed.family || removed.name,
|
||||
details: { fileName: removed.fileName, format: removed.format }
|
||||
});
|
||||
}
|
||||
|
||||
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
|
||||
await backgroundTaskQueue.enqueueTask({
|
||||
key: `font-sync:delete:${removed && removed.id ? removed.id : Date.now()}`,
|
||||
|
||||
@@ -117,6 +117,7 @@ function filterAndSortAssets(assets, query) {
|
||||
|
||||
module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
const requireAccess = requireMediaAccess(deps.setAuthMessageCookie);
|
||||
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
||||
const pageSize = 24;
|
||||
if (!deps.pool || !deps.pages || !deps.upload || !deps.uploadDir) {
|
||||
throw new Error('registerMediaLibraryRoutes requires pool, pages, upload, and uploadDir.');
|
||||
@@ -196,7 +197,19 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
return res.redirect('/media-library?message=' + encodeURIComponent('File must be ' + limitLabel + ' or smaller.'));
|
||||
}
|
||||
await mediaLibrary.createImageVariants(req.file);
|
||||
await mediaLibrary.registerMediaAsset(deps.pool, Object.assign({}, req.file, { mediaType: mediaType }), '/media/uploads/' + req.file.filename, deps.getAuditUserId(req));
|
||||
const mediaPath = '/media/uploads/' + req.file.filename;
|
||||
await mediaLibrary.registerMediaAsset(deps.pool, Object.assign({}, req.file, { mediaType: mediaType }), mediaPath, deps.getAuditUserId(req));
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(deps.pool, req, {
|
||||
category: 'media-assets',
|
||||
eventType: 'media.uploaded',
|
||||
actorUserId: deps.getAuditUserId(req),
|
||||
targetType: 'media-asset',
|
||||
targetId: mediaPath,
|
||||
targetLabel: req.file.originalname || req.file.filename,
|
||||
details: { mediaPath: mediaPath, mediaType: mediaType, mimeType: mimeType, fileSize: Number(req.file.size) || 0 }
|
||||
});
|
||||
}
|
||||
res.redirect('/media-library?message=' + encodeURIComponent('Media uploaded.'));
|
||||
} catch (error) {
|
||||
if (req.file && req.file.path) {
|
||||
@@ -206,8 +219,8 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
}
|
||||
});
|
||||
|
||||
async function deleteMediaAsset(assetId) {
|
||||
const [rows] = await deps.pool.query('SELECT id, media_path FROM c_media_assets WHERE id = ?', [Number(assetId)]);
|
||||
async function deleteMediaAsset(assetId, req) {
|
||||
const [rows] = await deps.pool.query('SELECT id, media_path, original_name, media_type, file_size FROM c_media_assets WHERE id = ?', [Number(assetId)]);
|
||||
const asset = rows && rows[0];
|
||||
if (!asset) {
|
||||
return 'missing';
|
||||
@@ -218,6 +231,17 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
}
|
||||
const filePath = path.join(path.dirname(deps.uploadDir), asset.media_path.replace(/^\/media\//, ''));
|
||||
await deps.pool.query('DELETE FROM c_media_assets WHERE id = ?', [asset.id]);
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(deps.pool, req, {
|
||||
category: 'media-assets',
|
||||
eventType: 'media.deleted',
|
||||
actorUserId: deps.getAuditUserId(req),
|
||||
targetType: 'media-asset',
|
||||
targetId: asset.id,
|
||||
targetLabel: asset.original_name || asset.media_path,
|
||||
details: { mediaPath: asset.media_path, mediaType: asset.media_type || null, fileSize: Number(asset.file_size) || 0 }
|
||||
});
|
||||
}
|
||||
await fs.promises.unlink(filePath).catch(function (error) {
|
||||
if (error && error.code !== 'ENOENT') {
|
||||
throw error;
|
||||
@@ -250,7 +274,7 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
let deletedCount = 0;
|
||||
let inUseCount = 0;
|
||||
for (let index = 0; index < assetIds.length; index += 1) {
|
||||
const result = await deleteMediaAsset(assetIds[index]);
|
||||
const result = await deleteMediaAsset(assetIds[index], req);
|
||||
if (result === 'deleted') deletedCount += 1;
|
||||
if (result === 'in-use') inUseCount += 1;
|
||||
}
|
||||
@@ -263,7 +287,7 @@ module.exports = function registerMediaLibraryRoutes(app, deps) {
|
||||
|
||||
app.post('/media-library/:id/delete', requireAccess, deps.requirePermission('media-library.delete'), async function (req, res, next) {
|
||||
try {
|
||||
const result = await deleteMediaAsset(req.params.id);
|
||||
const result = await deleteMediaAsset(req.params.id, req);
|
||||
if (result === 'missing') {
|
||||
return res.redirect('/media-library?message=' + encodeURIComponent('Media asset not found.'));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const { buildPagination } = require('#src/web/lib/pagination');
|
||||
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('#src/web/lib/list-query');
|
||||
|
||||
module.exports = function registerWeatherIconRoutes(app, deps) {
|
||||
const recordRequestAuditEvent = deps.recordRequestAuditEvent;
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * 1024 * 1024, files: library.CONDITIONS.length * 2, fields: 2 } });
|
||||
const fields = library.CONDITIONS.flatMap(function (condition) {
|
||||
return [{ name: 'icon_' + condition.key, maxCount: 1 }, { name: 'night_icon_' + condition.key, maxCount: 1 }];
|
||||
@@ -57,8 +58,17 @@ module.exports = function registerWeatherIconRoutes(app, deps) {
|
||||
async function save(req, res, next) {
|
||||
try {
|
||||
const previousSets = library.readWeatherIconSets(deps.mediaDir);
|
||||
await library.saveWeatherIconSet(deps.mediaDir, req.body.name, Object.values(req.files || {}).flat(), req.params.id);
|
||||
const uploadedFiles = Object.values(req.files || {}).flat();
|
||||
const savedSet = await library.saveWeatherIconSet(deps.mediaDir, req.body.name, uploadedFiles, req.params.id);
|
||||
req.weatherIconRemovalOperations = library.collectWeatherIconRemovalOperations(previousSets, library.readWeatherIconSets(deps.mediaDir));
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(deps.pool, req, {
|
||||
category: 'media-assets', eventType: req.params.id ? 'weather-icon-set.updated' : 'weather-icon-set.created',
|
||||
actorUserId: req.currentUser && req.currentUser.id, targetType: 'weather-icon-set',
|
||||
targetId: savedSet.id, targetLabel: savedSet.name,
|
||||
details: { uploadedFileCount: uploadedFiles.length, iconCount: Object.keys(savedSet.icons || {}).length, nightIconCount: Object.keys(savedSet.nightIcons || {}).length }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
try { return await page(req, res, error.message, 400); } catch (pageError) { return next(pageError); }
|
||||
}
|
||||
@@ -74,6 +84,13 @@ module.exports = function registerWeatherIconRoutes(app, deps) {
|
||||
const enabled = String(req.body && req.body.enabled || '').trim() !== '0';
|
||||
const updated = library.setWeatherIconSetEnabled(deps.mediaDir, req.params.id, enabled);
|
||||
if (!updated) return res.redirect('/settings/weather-icons?message=' + encodeURIComponent('That weather icon set was not found.'));
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(deps.pool, req, {
|
||||
category: 'media-assets', eventType: enabled ? 'weather-icon-set.enabled' : 'weather-icon-set.disabled',
|
||||
actorUserId: req.currentUser && req.currentUser.id, targetType: 'weather-icon-set',
|
||||
targetId: updated.id, targetLabel: updated.name, details: { enabled: Boolean(enabled) }
|
||||
});
|
||||
}
|
||||
await sync();
|
||||
res.redirect('/settings/weather-icons?message=' + encodeURIComponent(enabled ? 'Weather icon set enabled.' : 'Weather icon set disabled.'));
|
||||
} catch (error) { next(error); }
|
||||
@@ -89,6 +106,13 @@ module.exports = function registerWeatherIconRoutes(app, deps) {
|
||||
});
|
||||
if (used) return await page(req, res, 'That icon set is still used by one or more slides.', 400);
|
||||
const removed = library.deleteWeatherIconSet(deps.mediaDir, req.params.id);
|
||||
if (typeof recordRequestAuditEvent === 'function') {
|
||||
await recordRequestAuditEvent(deps.pool, req, {
|
||||
category: 'media-assets', eventType: 'weather-icon-set.deleted', actorUserId: req.currentUser && req.currentUser.id,
|
||||
targetType: 'weather-icon-set', targetId: removed.id, targetLabel: removed.name,
|
||||
details: { iconCount: Object.keys(removed.icons || {}).length, nightIconCount: Object.keys(removed.nightIcons || {}).length }
|
||||
});
|
||||
}
|
||||
await sync(library.collectWeatherIconRemovalOperations([removed], []));
|
||||
res.redirect('/settings/weather-icons?message=' + encodeURIComponent('Weather icon set deleted.'));
|
||||
} catch (error) { next(error); }
|
||||
|
||||
Reference in new issue
Block a user