// 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); } }); };