Files
pulse-signage/src/web/routes/settings/fonts.js
T
lzstealth f0177e6628
Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m15s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s
Release 2.7.3
2026-08-15 11:43:07 +01:00

246 lines
9.1 KiB
JavaScript

// Settings route registration for managed fonts.
const fs = require('fs');
const { hasAnyPermission, PERMISSION_DENIED_MESSAGE } = require('#src/rbac');
const fontLibrary = require('#src/web/lib/media/font-library');
const { buildPagination } = require('../../lib/pagination');
const { createSearchMatcher, getSearchQuery, getSortDirectionQuery, getSortQuery, parsePageNumber, sortRows } = require('../../lib/list-query');
const LIST_PAGE_SIZE = 25;
function normalizeFontFamilyName(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
async function fetchUsedFontFamilies(pool, fonts) {
const usedFamilies = new Set();
for (let i = 0; i < (Array.isArray(fonts) ? fonts.length : 0); i += 1) {
const family = normalizeFontFamilyName(fonts[i] && (fonts[i].family || fonts[i].name));
if (!family) {
continue;
}
const [rows] = await pool.query(
'SELECT COUNT(*) AS match_count FROM c_slides WHERE LOCATE(LOWER(?), LOWER(COALESCE(content_json, ""))) > 0',
[family]
);
if (Number(rows && rows[0] && rows[0].match_count) > 0) {
usedFamilies.add(family);
}
}
return usedFamilies;
}
async function isFontUsed(pool, family) {
const normalizedFamily = normalizeFontFamilyName(family);
if (!normalizedFamily) {
return false;
}
const [rows] = await pool.query(
'SELECT COUNT(*) AS match_count FROM c_slides WHERE LOCATE(LOWER(?), LOWER(COALESCE(content_json, ""))) > 0',
[normalizedFamily]
);
return Number(rows && rows[0] && rows[0].match_count) > 0;
}
async function buildFontsPageData(pool, uploadDir, req, common) {
const library = fontLibrary.loadFontLibrary(uploadDir);
const usedFamilies = await fetchUsedFontFamilies(pool, library.fonts);
const search = common && typeof common.getSearchQuery === 'function' ? common.getSearchQuery(req) : getSearchQuery(req);
const sort = common && typeof common.getSortQuery === 'function' ? common.getSortQuery(req) : getSortQuery(req);
const direction = common && typeof common.getSortDirectionQuery === 'function' ? common.getSortDirectionQuery(req) : getSortDirectionQuery(req);
const searchableFonts = sortRows(library.fonts.map(function (font) {
const family = normalizeFontFamilyName(font.family || font.name);
return Object.assign({}, font, {
inUse: usedFamilies.has(family)
});
}), function (font) {
if (sort === 'file') {
return String(font && font.fileName || '').trim();
}
if (sort === 'format') {
return String(font && font.format || '').trim();
}
if (sort === 'status') {
return font && font.enabled ? 'Enabled' : 'Disabled';
}
return String(font && font.family || font && font.name || '').trim();
}, direction).filter(createSearchMatcher(search, [
'family',
'name',
'fileName',
'format',
function (font) {
return font && font.enabled ? 'enabled' : 'disabled';
}
]));
const pagination = buildPagination(searchableFonts.length, parsePageNumber(req && req.query && req.query.page), 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'fonts', 'Font pages');
const startIndex = (pagination.currentPage - 1) * LIST_PAGE_SIZE;
return {
fonts: searchableFonts.slice(startIndex, startIndex + LIST_PAGE_SIZE),
pagination: pagination,
stylesheetHref: library.stylesheetHref
};
}
function redirectToLogin(res, setAuthMessageCookie) {
if (typeof setAuthMessageCookie === 'function') {
setAuthMessageCookie(res, 'Please sign in to continue.');
return res.redirect('/login');
}
return res.redirect('/login');
}
function requireFontsAccess(setAuthMessageCookie) {
return function (req, res, next) {
if (!req.currentUser) {
return redirectToLogin(res, setAuthMessageCookie);
}
if (hasAnyPermission(req.currentUser, ['fonts.read', 'fonts.create', 'fonts.delete'])) {
return next();
}
const error = new Error(PERMISSION_DENIED_MESSAGE);
error.statusCode = 403;
error.expose = true;
next(error);
};
}
module.exports = function registerFontRoutes(app, deps) {
const pool = deps.pool;
const common = deps.common || {};
const pages = deps.pages;
const upload = deps.upload;
const uploadDir = deps.uploadDir;
const backgroundTaskQueue = deps.backgroundTaskQueue;
const setAuthMessageCookie = deps.setAuthMessageCookie;
const FONT_FAMILY_MAX_LENGTH = 255;
if (!pool || !pages || !upload || !uploadDir) {
throw new Error('registerFontRoutes requires pool, pages, upload, and uploadDir.');
}
app.get('/settings/fonts', requireFontsAccess(setAuthMessageCookie), async function (req, res, next) {
try {
const data = await buildFontsPageData(pool, uploadDir, req, common);
res.send(pages.renderFontsPage(data, req.query.message ? String(req.query.message) : '', req.currentUser));
} catch (error) {
next(error);
}
});
app.post('/settings/fonts', requireFontsAccess(setAuthMessageCookie), upload.single('font_file'), async function (req, res, next) {
try {
if (!req.file) {
return res.status(400).send('No font file was uploaded.');
}
const result = await fontLibrary.addFontFile(uploadDir, req.file, String(req.body && req.body.font_family || '').trim().slice(0, FONT_FAMILY_MAX_LENGTH));
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
await backgroundTaskQueue.enqueueTask({
key: `font-sync:${result && result.font && result.font.id ? result.font.id : Date.now()}`,
title: 'Font sync',
category: 'fonts',
taskType: 'font-sync',
payload: {
uploadDir: uploadDir,
operations: fontLibrary.collectFontLibrarySyncOperations(uploadDir)
},
persist: true
});
}
res.redirect('/settings/fonts?message=' + encodeURIComponent('Font uploaded.'));
} catch (error) {
if (req.file && req.file.path) {
try {
await fs.promises.unlink(req.file.path);
} catch (_cleanupError) {}
}
if (error && /already exists/i.test(String(error.message || ''))) {
const data = await buildFontsPageData(pool, uploadDir, req, common);
return res.status(400).send(pages.renderFontsPage(data, String(error.message || 'Font already exists.'), req.currentUser));
}
next(error);
}
});
app.post('/settings/fonts/:id/toggle', requireFontsAccess(setAuthMessageCookie), async function (req, res, next) {
try {
const enabled = String(req.body && req.body.enabled || '').trim() !== '0';
const updated = await fontLibrary.setFontEnabled(uploadDir, String(req.params.id || '').trim(), enabled);
if (!updated) {
return res.redirect('/settings/fonts?message=' + encodeURIComponent('That font was not found.'));
}
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
await backgroundTaskQueue.enqueueTask({
key: `font-sync:toggle:${updated && updated.id ? updated.id : Date.now()}`,
title: 'Font sync',
category: 'fonts',
taskType: 'font-sync',
payload: {
uploadDir: uploadDir,
operations: fontLibrary.collectFontLibrarySyncOperations(uploadDir)
},
persist: true
});
}
res.redirect('/settings/fonts?message=' + encodeURIComponent(enabled ? 'Font enabled.' : 'Font disabled.'));
} catch (error) {
next(error);
}
});
app.post('/settings/fonts/:id/delete', requireFontsAccess(setAuthMessageCookie), async function (req, res, next) {
try {
const fontId = String(req.params.id || '').trim();
const currentFont = fontLibrary.loadFontLibrary(uploadDir).fonts.find(function (font) {
return String(font.id || font.fileName || '').trim() === fontId || String(font.fileName || '').trim() === fontId;
});
const family = currentFont ? String(currentFont.family || currentFont.name || '').trim() : '';
if (family && await isFontUsed(pool, family)) {
return res.redirect('/settings/fonts?message=' + encodeURIComponent('That font is still used by one or more slides.'));
}
const removed = await fontLibrary.deleteFontFile(uploadDir, fontId);
if (!removed) {
return res.redirect('/settings/fonts?message=' + encodeURIComponent('That font was not found.'));
}
if (backgroundTaskQueue && typeof backgroundTaskQueue.enqueueTask === 'function') {
await backgroundTaskQueue.enqueueTask({
key: `font-sync:delete:${removed && removed.id ? removed.id : Date.now()}`,
title: 'Font sync',
category: 'fonts',
taskType: 'font-sync',
payload: {
uploadDir: uploadDir,
operations: [
removed.fileName ? { type: 'delete', uploadPath: '/media/fonts/' + removed.fileName } : null
].concat(fontLibrary.collectFontLibrarySyncOperations(uploadDir)).filter(Boolean)
},
persist: true
});
}
res.redirect('/settings/fonts?message=' + encodeURIComponent('Font removed.'));
} catch (error) {
next(error);
}
});
};