// Version history helpers for the CMS: listing automatic backups from // .content-backups, safe backup-name validation and diff assembly. const fs = require('fs'); const path = require('path'); const { renderDiffHtml } = require('./cms-diff'); // Backup files are named `..json` const BACKUP_NAME_RE = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/; // WHY: the backup name arrives as a query parameter — only allow the exact // `..json` shape so path traversal (`../`) is impossible. function safeBackupName(fileKey, candidate) { if (typeof candidate !== 'string' || !candidate.startsWith(`${fileKey}.`) || !candidate.endsWith('.json')) return null; const ts = candidate.slice(fileKey.length + 1, -5); if (!BACKUP_NAME_RE.test(ts)) return null; return candidate; } function formatBackupTimestamp(fileKey, backupName) { const ts = backupName.slice(fileKey.length + 1, -5); const m = ts.match(BACKUP_NAME_RE); if (!m) return ts; return `${m[1]} ${m[2]}:${m[3]}:${m[4]}`; } function listVersions(backupDir, fileKey) { try { return fs.readdirSync(backupDir) .filter(name => safeBackupName(fileKey, name)) .map(name => { const full = path.join(backupDir, name); const stat = fs.statSync(full); return { name, size: stat.size, when: formatBackupTimestamp(fileKey, name) }; }) .sort((a, b) => b.name.localeCompare(a.name)); // newest first } catch { return []; } } function readBackupContent(backupDir, backupName) { return fs.readFileSync(path.join(backupDir, backupName), 'utf8'); } // Compare a backup with the current file content; returns both pretty texts and // the rendered diff HTML (backup = old/left, current = new/right). function buildVersionDiff(backupDir, currentFilePath, fileKey, backupName) { const backupText = readBackupContent(backupDir, backupName); const currentText = fs.readFileSync(currentFilePath, 'utf8'); return { backupName, when: formatBackupTimestamp(fileKey, backupName), backupText: backupText.trim(), currentText: currentText.trim(), diffHtml: renderDiffHtml(backupText, currentText), }; } module.exports = { safeBackupName, listVersions, readBackupContent, buildVersionDiff, formatBackupTimestamp };