diff --git a/common.js b/common.js index f1fdf105..ccddadd1 100644 --- a/common.js +++ b/common.js @@ -419,4 +419,19 @@ module.exports.uniqueArray = function (a) { } } return out; +} + +// Replace placeholders in a string with values from an object or a function +module.exports.replacePlaceholders = function (template, values) { + return template.replace(/\{(\w+)\}/g, (match, key) => { + if (typeof values === 'function') { + return values(key); + } + else if (values && typeof values === 'object') { + return values[key] !== undefined ? values[key] : match; + } + else { + return values !== undefined ? values : match; + } + }); } \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 3f3bf96f..da8a0b2e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -168,7 +168,6 @@ RUN cd meshcentral && npm install # Expose needed ports EXPOSE 80 443 - # These volumes will be created by default even without any declaration, this allows default persistence in Docker/Podman. VOLUME /opt/meshcentral/meshcentral-data VOLUME /opt/meshcentral/meshcentral-files diff --git a/meshmail.js b/meshmail.js index 2b4fcdee..2c88051e 100644 --- a/meshmail.js +++ b/meshmail.js @@ -116,6 +116,27 @@ module.exports.CreateMeshMail = function (parent, domain) { } } + // If no email template found, use the default translated email template + if (((htmlfile == null) || (txtfile == null)) && (lang != null) && (lang != 'en')) { + var translationsPath = obj.parent.path.join(obj.parent.webEmailsPath, 'translations'); + var translationsPathHtml = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', name + '_' + lang + '.html'); + var translationsPathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', name + '_' + lang + '.txt'); + if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathHtml) && obj.parent.fs.existsSync(translationsPathTxt)) { + htmlfile = obj.parent.fs.readFileSync(translationsPathHtml).toString(); + txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString(); + } + } + + // If no translated email template found, use the default email template + if ((htmlfile == null) || (txtfile == null)) { + var pathHtml = obj.parent.path.join(obj.parent.webEmailsPath, name + '.html'); + var pathTxt = obj.parent.path.join(obj.parent.webEmailsPath, name + '.txt'); + if (obj.parent.fs.existsSync(pathHtml) && obj.parent.fs.existsSync(pathTxt)) { + htmlfile = obj.parent.fs.readFileSync(pathHtml).toString(); + txtfile = obj.parent.fs.readFileSync(pathTxt).toString(); + } + } + // No email templates if ((htmlfile == null) || (txtfile == null)) { return null; } diff --git a/meshmessaging.js b/meshmessaging.js index 646ae4a2..9a722792 100644 --- a/meshmessaging.js +++ b/meshmessaging.js @@ -397,6 +397,7 @@ module.exports.CreateServer = function (parent) { function getTemplate(templateNumber, domain, lang) { parent.debug('email', 'Getting SMS template #' + templateNumber + ', lang: ' + lang); if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given. + if (lang != null) { lang = lang.split('-')[0]; } // Take the first part of the language, "xx-xx" var r = {}, emailsPath = null; if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; } @@ -404,7 +405,7 @@ module.exports.CreateServer = function (parent) { else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; } if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null } - // Get the non-english email if needed + // Get the non-english sms if needed var txtfile = null; if ((lang != null) && (lang != 'en')) { var translationsPath = obj.parent.path.join(emailsPath, 'translations'); @@ -414,7 +415,7 @@ module.exports.CreateServer = function (parent) { } } - // Get the english email + // Get the english sms if (txtfile == null) { var pathTxt = obj.parent.path.join(emailsPath, 'sms-messages.txt'); if (obj.parent.fs.existsSync(pathTxt)) { @@ -422,6 +423,23 @@ module.exports.CreateServer = function (parent) { } } + // If no english sms and a non-english language is requested, try to get the default translated sms + if (txtfile == null && (lang != null) && (lang != 'en')) { + var translationsPath = obj.parent.path.join(obj.parent.webEmailsPath, 'translations'); + var translationsPathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', 'sms-messages_' + lang + '.txt'); + if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) { + txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString(); + } + } + + // If no default translated sms, try to get the default english sms + if (txtfile == null) { + var pathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'sms-messages.txt'); + if (obj.parent.fs.existsSync(pathTxt)) { + txtfile = obj.parent.fs.readFileSync(pathTxt).toString(); + } + } + // No email templates if (txtfile == null) { return null; } diff --git a/meshscanner.js b/meshscanner.js index 6eda8192..5eb630af 100644 --- a/meshscanner.js +++ b/meshscanner.js @@ -162,7 +162,19 @@ module.exports.CreateMeshScanner = function (parent) { try { if ((typeof obj.parent.config.domains[''].title == 'string') && (obj.parent.config.domains[''].title.length > 0)) { name = obj.parent.config.domains[''].title; info = ''; - try { if ((typeof obj.parent.config.domains[''].title2 == 'string') && (obj.parent.config.domains[''].title2.length > 0)) { info = obj.parent.config.domains[''].title2; } } catch (ex) { } + try { + if ((typeof obj.parent.config.domains[''].title2 == 'string') && (obj.parent.config.domains[''].title2.length > 0)) { + info = obj.common.replacePlaceholders(obj.parent.config.domains[''].title2, { + 'serverversion': obj.parent.currentVer, + 'servername': obj.getWebServerName(domain, req), + 'agentsessions': Object.keys(parent.webserver.wsagents).length, + 'connectedusers': Object.keys(parent.webserver.wssessions).length, + 'userssessions': Object.keys(parent.webserver.wssessions2).length, + 'relaysessions': parent.webserver.relaySessionCount, + 'relaycount': Object.keys(parent.webserver.wsrelays).length + }); + } + } catch (ex) { } } } catch (ex) { } try { diff --git a/meshsms.js b/meshsms.js index 1746c23e..441e7c4d 100644 --- a/meshsms.js +++ b/meshsms.js @@ -169,6 +169,7 @@ module.exports.CreateMeshSMS = function (parent) { function getTemplate(templateNumber, domain, lang) { parent.debug('email', 'Getting SMS template #' + templateNumber + ', lang: ' + lang); if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given. + if (lang != null) { lang = lang.split('-')[0]; } // Take the first part of the language, "xx-xx" var r = {}, emailsPath = null; if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; } @@ -194,6 +195,23 @@ module.exports.CreateMeshSMS = function (parent) { } } + // If no english sms and a non-english language is requested, try to get the default translated sms + if (txtfile == null && (lang != null) && (lang != 'en')) { + var translationsPath = obj.parent.path.join(obj.parent.webEmailsPath, 'translations'); + var translationsPathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', 'sms-messages_' + lang + '.txt'); + if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathTxt)) { + txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString(); + } + } + + // If no default translated sms, try to get the default english sms + if (txtfile == null) { + var pathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'sms-messages.txt'); + if (obj.parent.fs.existsSync(pathTxt)) { + txtfile = obj.parent.fs.readFileSync(pathTxt).toString(); + } + } + // No email templates if (txtfile == null) { return null; } diff --git a/webserver.js b/webserver.js index 0d69306f..5c3cf3aa 100644 --- a/webserver.js +++ b/webserver.js @@ -3217,7 +3217,15 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF passRequirements: passRequirements, customui: customui, webcerthash: Buffer.from(obj.webCertificateFullHashs[domain.id], 'binary').toString('base64').replace(/\+/g, '@').replace(/\//g, '$'), - footer: (domain.footer == null) ? '' : domain.footer, + footer: (domain.footer == null) ? '' : obj.common.replacePlaceholders(domain.footer, { + 'serverversion': obj.parent.currentVer, + 'servername': obj.getWebServerName(domain, req), + 'agentsessions': Object.keys(parent.webserver.wsagents).length, + 'connectedusers': Object.keys(parent.webserver.wssessions).length, + 'userssessions': Object.keys(parent.webserver.wssessions2).length, + 'relaysessions': parent.webserver.relaySessionCount, + 'relaycount': Object.keys(parent.webserver.wsrelays).length + }), webstate: encodeURIComponent(webstate).replace(/'/g, '%27'), amtscanoptions: amtscanoptions, pluginHandler: (parent.pluginHandler == null) ? 'null' : parent.pluginHandler.prepExports(), @@ -3462,12 +3470,29 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF sessiontime: (args.sessiontime) ? args.sessiontime : 60, // Session time in minutes, 60 minutes is the default passRequirements: passRequirements, customui: customui, - footer: (domain.loginfooter == null) ? '' : domain.loginfooter, + footer: (domain.loginfooter == null) ? '' : obj.common.replacePlaceholders(domain.loginfooter, { + 'serverversion': obj.parent.currentVer, + 'servername': obj.getWebServerName(domain, req), + 'agentsessions': Object.keys(parent.webserver.wsagents).length, + 'connectedusers': Object.keys(parent.webserver.wssessions).length, + 'userssessions': Object.keys(parent.webserver.wssessions2).length, + 'relaysessions': parent.webserver.relaySessionCount, + 'relaycount': Object.keys(parent.webserver.wsrelays).length + }), hkey: encodeURIComponent(hardwareKeyChallenge).replace(/'/g, '%27'), messageid: msgid, flashErrors: JSON.stringify(flashErrors), passhint: passhint, - welcometext: domain.welcometext ? encodeURIComponent(domain.welcometext).split('\'').join('\\\'') : null, + + welcometext: domain.welcometext ? encodeURIComponent(obj.common.replacePlaceholders(domain.welcometext, { + 'serverversion': obj.parent.currentVer, + 'servername': obj.getWebServerName(domain, req), + 'agentsessions': Object.keys(parent.webserver.wsagents).length, + 'connectedusers': Object.keys(parent.webserver.wssessions).length, + 'userssessions': Object.keys(parent.webserver.wssessions2).length, + 'relaysessions': parent.webserver.relaySessionCount, + 'relaycount': Object.keys(parent.webserver.wsrelays).length + })).split('\'').join('\\\'') : null, welcomePictureFullScreen: ((typeof domain.welcomepicturefullscreen == 'boolean') ? domain.welcomepicturefullscreen : false), hwstate: hwstate, otpemail: otpemail, @@ -4208,7 +4233,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF if (typeof c.pid != 'string') { res.sendStatus(404); return; } // Check the expired time, expire message. - if ((c.e != null) && (c.e <= Date.now())) { render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } + if ((c.e != null) && (c.e <= Date.now())) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } obj.db.Get('deviceshare-' + c.pid, function (err, docs) { if ((err != null) || (docs == null) || (docs.length != 1)) { res.sendStatus(404); return; } @@ -4254,17 +4279,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF // Serve the guest sharing page function handleSharingRequestEx(req, res, domain, c) { // Check the expired time, expire message. - if ((c.expire != null) && (c.expire <= Date.now())) { render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } + if ((c.expire != null) && (c.expire <= Date.now())) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } // Check the public id obj.db.GetAllTypeNodeFiltered([c.nid], domain.id, 'deviceshare', null, function (err, docs) { // Check if any sharing links are present, expire message. - if ((err != null) || (docs.length == 0)) { render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } + if ((err != null) || (docs.length == 0)) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } // Search for the device share public identifier, expire message. var found = false; for (var i = 0; i < docs.length; i++) { if ((docs[i].publicid == c.pid) && ((docs[i].extrakey == null) || (docs[i].extrakey === c.k))) { found = true; } } - if (found == false) { render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } + if (found == false) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 12, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } // Get information about this node obj.db.Get(c.nid, function (err, nodes) { @@ -4272,7 +4297,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF var node = nodes[0]; // Check the start time, not yet valid message. - if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } + if ((c.start != null) && (c.expire != null) && ((c.start > Date.now()) || (c.start > c.expire))) { res.status(404); render(req, res, getRenderPage((domain.sitestyle >= 2) ? 'message2' : 'message', req, domain), getRenderArgs({ titleid: 2, msgid: 11, domainurl: encodeURIComponent(domain.url).replace(/'/g, '%27') }, req, domain)); return; } // If this is a web relay share, check if this feature is active if ((c.p == 8) || (c.p == 16)) { @@ -9360,6 +9385,15 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF xargs.title1 = domain.title1 ? domain.title1 : ''; xargs.title2 = (domain.title1 && domain.title2) ? domain.title2 : ''; } + xargs.title2 = obj.common.replacePlaceholders(xargs.title2, { + 'serverversion': obj.parent.currentVer, + 'servername': obj.getWebServerName(domain, req), + 'agentsessions': Object.keys(parent.webserver.wsagents).length, + 'connectedusers': Object.keys(parent.webserver.wssessions).length, + 'userssessions': Object.keys(parent.webserver.wssessions2).length, + 'relaysessions': parent.webserver.relaySessionCount, + 'relaycount': Object.keys(parent.webserver.wsrelays).length + }); xargs.extitle = encodeURIComponent(xargs.title).split('\'').join('\\\''); xargs.domainurl = domain.url; xargs.autocomplete = (domain.autocomplete === false) ? 'autocomplete=off x' : 'autocomplete'; // This option allows autocomplete to be turned off on the login page.