You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nodebb/src/admin/search.js

143 lines
3.7 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const sanitizeHTML = require('sanitize-html');
const nconf = require('nconf');
const winston = require('winston');
const file = require('../file');
const { Translator } = require('../translator');
function filterDirectories(directories) {
return directories.map(
// get the relative path
// convert dir to use forward slashes
dir => dir.replace(/^.*(admin.*?).tpl$/, '$1').split(path.sep).join('/')
).filter(
// exclude .js files
// exclude partials
// only include subpaths
// exclude category.tpl, group.tpl, category-analytics.tpl
dir => (
!dir.endsWith('.js') &&
!dir.includes('/partials/') &&
/\/.*\//.test(dir) &&
!/manage\/(category|group|category-analytics)$/.test(dir)
)
);
}
async function getAdminNamespaces() {
const directories = await file.walk(path.resolve(nconf.get('views_dir'), 'admin'));
return filterDirectories(directories);
}
function sanitize(html) {
// reduce the template to just meaningful text
// remove all tags and strip out scripts, etc completely
return sanitizeHTML(html, {
allowedTags: [],
allowedAttributes: [],
});
}
function simplify(translations) {
return translations
// remove all mustaches
.replace(/(?:\{{1,2}[^}]*?\}{1,2})/g, '')
// collapse whitespace
.replace(/(?:[ \t]*[\n\r]+[ \t]*)+/g, '\n')
.replace(/[\t ]+/g, ' ');
}
function nsToTitle(namespace) {
return namespace.replace('admin/', '').split('/').map(str => str[0].toUpperCase() + str.slice(1)).join(' > ')
.replace(/[^a-zA-Z> ]/g, ' ');
}
const fallbackCache = {};
async function initFallback(namespace) {
const template = await fs.promises.readFile(path.resolve(nconf.get('views_dir'), `${namespace}.tpl`), 'utf8');
const title = nsToTitle(namespace);
let translations = sanitize(template);
translations = Translator.removePatterns(translations);
translations = simplify(translations);
translations += `\n${title}`;
return {
namespace: namespace,
translations: translations,
title: title,
};
}
async function fallback(namespace) {
if (fallbackCache[namespace]) {
return fallbackCache[namespace];
}
const params = await initFallback(namespace);
fallbackCache[namespace] = params;
return params;
}
async function initDict(language) {
const namespaces = await getAdminNamespaces();
return await Promise.all(namespaces.map(ns => buildNamespace(language, ns)));
}
async function buildNamespace(language, namespace) {
const translator = Translator.create(language);
try {
const translations = await translator.getTranslation(namespace);
if (!translations || !Object.keys(translations).length) {
return await fallback(namespace);
}
// join all translations into one string separated by newlines
let str = Object.keys(translations).map(key => translations[key]).join('\n');
str = sanitize(str);
let title = namespace;
title = title.match(/admin\/(.+?)\/(.+?)$/);
title = `[[admin/menu:section-${
title[1] === 'development' ? 'advanced' : title[1]
}]]${title[2] ? (` > [[admin/menu:${
title[1]}/${title[2]}]]`) : ''}`;
title = await translator.translate(title);
return {
namespace: namespace,
translations: `${str}\n${title}`,
title: title,
};
} catch (err) {
winston.error(err.stack);
return {
namespace: namespace,
translations: '',
};
}
}
const cache = {};
async function getDictionary(language) {
if (cache[language]) {
return cache[language];
}
const params = await initDict(language);
cache[language] = params;
return params;
}
module.exports.getDictionary = getDictionary;
module.exports.filterDirectories = filterDirectories;
module.exports.simplify = simplify;
module.exports.sanitize = sanitize;
require('../promisify')(module.exports);