From 5fcffcd608089d1a0f0f77e405e92be6eccaed5b Mon Sep 17 00:00:00 2001 From: si458 Date: Sat, 24 May 2025 12:47:28 +0100 Subject: [PATCH 1/5] fix email/sms/messaging customised templates #6994 Signed-off-by: si458 --- meshmail.js | 21 +++++++++++++++++++++ meshmessaging.js | 22 ++++++++++++++++++++-- meshsms.js | 18 ++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) 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/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; } From 89238303cba5d9474eb60a3f6f737d3c79e17d3a Mon Sep 17 00:00:00 2001 From: si458 Date: Mon, 26 May 2025 14:03:55 +0100 Subject: [PATCH 2/5] allow system variables in footer, loginfooter, welcometext, title2 #6634 Signed-off-by: si458 --- common.js | 16 ++++++++++++++++ meshscanner.js | 14 +++++++++++++- webserver.js | 40 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/common.js b/common.js index f1fdf105..d7b36c4c 100644 --- a/common.js +++ b/common.js @@ -419,4 +419,20 @@ 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) => { + console.log('match', match, 'key', key, 'values', values); + 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/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/webserver.js b/webserver.js index 0d69306f..099f399f 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, @@ -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. From 3b2aaccf1be0eb6ade5c9d73b1276c6951d677c3 Mon Sep 17 00:00:00 2001 From: si458 Date: Mon, 26 May 2025 14:15:48 +0100 Subject: [PATCH 3/5] remove console.log doh! #6634 Signed-off-by: si458 --- common.js | 1 - 1 file changed, 1 deletion(-) diff --git a/common.js b/common.js index d7b36c4c..ccddadd1 100644 --- a/common.js +++ b/common.js @@ -424,7 +424,6 @@ module.exports.uniqueArray = function (a) { // 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) => { - console.log('match', match, 'key', key, 'values', values); if (typeof values === 'function') { return values(key); } From cf183cfdae95e4aacf48f7c3a7856ced9c6a70d3 Mon Sep 17 00:00:00 2001 From: Simon Smith Date: Wed, 28 May 2025 12:17:41 +0100 Subject: [PATCH 4/5] update packages in docker image Signed-off-by: Simon Smith --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 279da057..a2ace6a5 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -90,7 +90,7 @@ RUN cd meshcentral && npm install # NOTE: ALL MODULES MUST HAVE A VERSION NUMBER AND THE VERSION MUST MATCH THAT USED IN meshcentral.js mainStart() RUN if ! [ -z "$INCLUDE_MONGODBTOOLS" ]; then cd meshcentral && npm install mongodb@4.17.2; fi -RUN if ! [ -z "$PREINSTALL_LIBS" ] && [ "$PREINSTALL_LIBS" == "true" ]; then cd meshcentral && npm install ssh2@1.16.0 semver@7.5.4 nodemailer@6.9.15 image-size@1.2.1 wildleek@2.0.0 otplib@12.0.1 yubikeyotp@0.2.0; fi +RUN if ! [ -z "$PREINSTALL_LIBS" ] && [ "$PREINSTALL_LIBS" == "true" ]; then cd meshcentral && npm install ssh2@1.16.0 semver@7.7.1 nodemailer@6.9.16 image-size@2.0.2 wildleek@2.0.0 otplib@12.0.1 yubikeyotp@0.2.0; fi EXPOSE 80 443 4433 From 5ef5e9ce0e5747de2919ce70403d14b977f7daf5 Mon Sep 17 00:00:00 2001 From: si458 Date: Wed, 28 May 2025 12:44:53 +0100 Subject: [PATCH 5/5] send 404 with expired/not yet valid sharing links #7062 Signed-off-by: si458 --- webserver.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webserver.js b/webserver.js index 099f399f..5c3cf3aa 100644 --- a/webserver.js +++ b/webserver.js @@ -4233,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; } @@ -4279,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) { @@ -4297,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)) {