diff --git a/.gitignore b/.gitignore
index 58d0278114..9bc3743918 100644
--- a/.gitignore
+++ b/.gitignore
@@ -311,4 +311,4 @@ __pycache__/
# When running mkdocs locally as dev
docs/__pycache__/
docs/env/
-docker-compose.yaml
+docker-compose.yaml
\ No newline at end of file
diff --git a/public/styles/style-bootstrap.css b/public/styles/style-bootstrap.css
index c8faec77c0..2827bd5fd0 100644
--- a/public/styles/style-bootstrap.css
+++ b/public/styles/style-bootstrap.css
@@ -272,7 +272,7 @@ body {
padding-top: 5px;
cursor: pointer;
position: absolute;
- right: 0;
+ right: 0;
margin-right: 10px;
}
@@ -348,7 +348,7 @@ body {
-ms-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
- -webkit-box-sizing:border-box;
+ -webkit-box-sizing:border-box;
}
.night #column_l {
@@ -562,7 +562,7 @@ body {
padding: 3px;
margin-right: 3px;
cursor: pointer;
- background-color: white;
+ background-color: white;
}
#id_dialogtitle {
@@ -761,7 +761,7 @@ body {
margin-left: 5px;
}
-/* Example if
is relplaced with then image can be defined in css
+/* Example if
is relplaced with then image can be defined in css
#NoMeshesPanel {
background: url(../images/info.png) no-repeat 23px 20px;
height: 48px;
@@ -2075,7 +2075,7 @@ nav .lbbuttonsel2 {
#d2notifyMsg,
#d2devNotes,
#d2devEvent,
-#d2runcmd,
+#d2runcmd,
#d2devMessage,
#d2smsText,
#d2emailSubject,
@@ -2657,7 +2657,7 @@ nav .lbbuttonsel2 {
.deskToolsBar:hover {
background-color: #EFE8B6;
}
-
+
.night .deskToolsBar {
color: #ddd;
}
@@ -3291,7 +3291,7 @@ nav .lbbuttonsel2 {
.sidebar .nav-link {
font-size: xx-large;
}
-
+
.card:hover, #p2AccountImage:hover, #p2canvas:hover {
background: #f3f5f7 !important;
}
@@ -3326,6 +3326,20 @@ nav .lbbuttonsel2 {
border: none;
}
+/* Shared styles for all custom icons */
+.custom-icon svg {
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: contain;
+ width: 1em !important;
+ height: 1em !important;
+ display: inline-block !important;
+}
+
+.custom-icon svg path {
+ display: none !important;
+}
+
/* hide .sidebar when on mobile */
@media (max-width: 768px) {
#page_leftbar {
@@ -3397,11 +3411,11 @@ nav .lbbuttonsel2 {
/* .select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered {
display: inline;
}
-
+
.select2-container--bootstrap-5 .select2-selection--multiple .select2-selection__rendered .select2-selection__choice {
display: inline-flex;
}
-
+
.select2-container--bootstrap-5 .select2-selection--multiple .select2-search {
display: inline !important;
} */
\ No newline at end of file
diff --git a/views/default3.handlebars b/views/default3.handlebars
index c70000ccb9..60ac1bed5b 100644
--- a/views/default3.handlebars
+++ b/views/default3.handlebars
@@ -597,6 +597,7 @@
Create login token
Switch theme
+ Icons Customization
@@ -2805,6 +2806,7 @@
QV('p2ServerActions', (siteRights & 21) && ((serverFeatures & 143) != 0));
QV('LeftMenuMyServer', (siteRights & 21) && ((serverFeatures & 64) != 0)); // 16 + 4 + 1
QV('MainMenuMyServer', siteRights & 21);
+ QV('accountCustomIconsSpan', (userinfo.siteadmin === 0xFFFFFFFF));
QV('p2ServerActionsBackup', (siteRights & 1) && ((serverFeatures & 1) != 0));
QV('p2ServerActionsRestore', (siteRights & 4) && ((serverFeatures & 2) != 0));
QV('p2ServerActionsVersion', (siteRights & 16) && ((serverFeatures & 4) != 0));
@@ -3823,6 +3825,8 @@
var webstate = JSON.parse(message.event.state);
for (var i in webstate) { localStorage.setItem(i, webstate[i]); }
+ customIconValues = loadCustomIconState();
+ applyIconCustomization(customIconValues);
// Update the web page
//if ((webstate.deskAspectRatio != null) && (webstate.deskAspectRatio != deskAspectRatio)) { deskAspectRatio = webstate.deskAspectRatio; deskAdjust(); }
@@ -18882,7 +18886,7 @@
if (rec.meshname) { x += addHtmlValue4("Device Group", EscapeHtml(rec.meshname)); }
if (rec.size) { x += addHtmlValue4("Size", format("{0} bytes", rec.size)); }
if (rec.startTime) { x += addHtmlValue4("Start Time", printTime(new Date(rec.startTime))); }
- if (rec.startTime && rec.lengthTime) { x += addHtmlValue4("End Time", printTime(new Date(rec.startTime + (rec.lengthTime * 1000)))); }
+ if (rec.startTime && rec.lengthTime) { x += addHtmlValue4("End Time", printTime(new Date(rec.startTime + (rec.lengthTime * 1000)))); }
if (rec.lengthTime) { x += addHtmlValue4("Duration", pad2(Math.floor(rec.lengthTime / 3600)) + ':' + pad2(Math.floor((rec.lengthTime % 3600) / 60)) + ':' + pad2(Math.floor(rec.lengthTime % 60))); }
if (rec.multiplex == true) { x += addHtmlValue4("Multiplexor", "Enabled"); }
if (rec.userids) { for (var i in rec.userids) { x += addHtmlValue4("User", rec.userids[i].split('/')[2]); } }
@@ -20818,11 +20822,152 @@
}
}
+ // --- Icons Customization ---
+ var customIconValues = {};
+ const customIconConfig = [
+ { key: 'myDevices', label: 'My Devices', elementId: 'LeftMenuMyDevices' },
+ { key: 'myAccount', label: 'My Account', elementId: 'LeftMenuMyAccount' },
+ { key: 'myEvents', label: 'My Events', elementId: 'LeftMenuMyEvents' },
+ { key: 'myFiles', label: 'My Files', elementId: 'LeftMenuMyFiles' },
+ { key: 'myUsers', label: 'My Users', elementId: 'LeftMenuMyUsers' },
+ { key: 'myServer', label: 'My Server', elementId: 'LeftMenuMyServer' }
+ ];
+
+ function loadCustomIconState() {
+ var raw = getstore('customIcons', '{}');
+ if ((typeof raw !== 'string') || (raw.length === 0)) { return {}; }
+ try { return JSON.parse(raw); } catch (ex) { return {}; }
+ }
+
+ function sanitizeCustomIconState(state) {
+ var sanitized = {};
+ if (state == null) { return sanitized; }
+ for (var i = 0; i < customIconConfig.length; i++) {
+ var key = customIconConfig[i].key;
+ var value = state[key];
+ if (typeof value === 'string') {
+ var trimmed = value.trim();
+ if (trimmed.length > 0) { sanitized[key] = trimmed; }
+ }
+ }
+ return sanitized;
+ }
+
+ function persistCustomIconState(state) {
+ var sanitized = sanitizeCustomIconState(state);
+ putstore('customIcons', JSON.stringify(sanitized));
+ customIconValues = sanitized;
+ applyIconCustomization(sanitized);
+ }
+
+ function showIconCustomization() {
+ customIconValues = loadCustomIconState();
+ var x = 'Upload custom SVG icons or provide a URL/data URL. Uploaded files are stored in the public icons directory.
';
+ for (var i = 0; i < customIconConfig.length; i++) {
+ var cfg = customIconConfig[i];
+ var currentValue = customIconValues[cfg.key] || '';
+ x += '';
+ }
+
+ setModalContent('xxAddAgent', 'Icons Customization', x, 'large');
+ showModal('xxAddAgentModal', 'idx_dlgOkButton', saveIconCustomization);
+ return false;
+ }
+
+ function triggerIconFileInput(iconKey) {
+ var fileInput = document.getElementById('iconFile_' + iconKey);
+ if (fileInput) { fileInput.click(); }
+ return false;
+ }
+
+ async function handleIconFileChange(iconKey, input) {
+ if (!input || !input.files || (input.files.length === 0)) { return; }
+ try {
+ var result = await uploadCustomIcon(iconKey, input.files[0]);
+ if ((result == null) || (result.path == null)) {
+ messagebox('Icons Customization', 'The server did not return a valid icon path.');
+ } else {
+ customIconValues[iconKey] = result.path;
+ var textInput = document.getElementById('iconInput_' + iconKey);
+ if (textInput) { textInput.value = result.path; }
+ persistCustomIconState(customIconValues);
+ }
+ } catch (ex) {
+ messagebox('Icons Customization', (ex && ex.message) ? ex.message : 'Failed to upload the icon.');
+ } finally {
+ input.value = '';
+ }
+ }
+
+ async function uploadCustomIcon(iconKey, file) {
+ var formData = new FormData();
+ formData.append('iconType', iconKey);
+ if (customIconValues && typeof customIconValues[iconKey] === 'string' && customIconValues[iconKey].length > 0) {
+ formData.append('previousIcon', customIconValues[iconKey]);
+ }
+ formData.append('iconFile', file);
+
+ var response = await fetch('customiconupload.ashx', { method: 'POST', body: formData, credentials: 'same-origin' });
+ if (!response.ok) {
+ var message = 'Failed to upload the icon.';
+ try {
+ var errorInfo = await response.json();
+ if (errorInfo && typeof errorInfo.error === 'string' && errorInfo.error.length > 0) { message = errorInfo.error; }
+ } catch (ex) { }
+ throw new Error(message);
+ }
+ return response.json();
+ }
+
+ function saveIconCustomization() {
+ var updatedState = {};
+ for (var i = 0; i < customIconConfig.length; i++) {
+ var cfg = customIconConfig[i];
+ var input = document.getElementById('iconInput_' + cfg.key);
+ if (input) { updatedState[cfg.key] = input.value; }
+ }
+ persistCustomIconState(updatedState);
+ if (xxModal) { xxModal.hide(); }
+ }
+
+ function applyIconCustomization(icons) {
+ var state = sanitizeCustomIconState(icons);
+ for (var i = 0; i < customIconConfig.length; i++) {
+ var cfg = customIconConfig[i];
+ var anchor = document.getElementById(cfg.elementId);
+ if (!anchor) { continue; }
+ var svg = anchor.querySelector('svg');
+ if (!svg) { continue; }
+ if (state[cfg.key]) {
+ anchor.classList.add('custom-icon');
+ svg.style.backgroundImage = `url(${state[cfg.key]})`;
+ } else {
+ anchor.classList.remove('custom-icon');
+ svg.style.removeProperty('background-image');
+ }
+ }
+ }
+
+ document.addEventListener('DOMContentLoaded', function () {
+ customIconValues = loadCustomIconState();
+ applyIconCustomization(customIconValues);
+ });
+
+ window.addEventListener('load', function () {
+ applyIconCustomization(customIconValues);
+ });
+
// Request Confirmation if closing while a desktop, terminal session is active
window.addEventListener('beforeunload', function (e) {
if (((desktop != null) && (xxcurrentView == 11)) || ((terminal != null) && (xxcurrentView == 12))) { e.preventDefault(); e.returnValue = ''; }
});
-
diff --git a/webserver.js b/webserver.js
index cbab56146e..885213c8f5 100644
--- a/webserver.js
+++ b/webserver.js
@@ -39,6 +39,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.net = require('net');
obj.tls = require('tls');
obj.path = require('path');
+ obj.os = require('os');
obj.bodyParser = require('body-parser');
obj.exphbs = require('express-handlebars');
obj.crypto = require('crypto');
@@ -94,6 +95,37 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
obj.renderLanguages = [];
obj.destroyedSessions = {}; // userid/req.session.x --> destroyed session time
+ const isWindowsPlatform = (obj.os.platform() === 'win32');
+ const safeUploadTempRoots = (function () {
+ const roots = [];
+ const addRoot = function (p) {
+ if (typeof p !== 'string') { return; }
+ var resolved;
+ try { resolved = obj.path.normalize(obj.path.resolve(p)); } catch (ex) { return; }
+ if (resolved.length === 0) { return; }
+ if ((resolved.length > 1) && resolved.endsWith(obj.path.sep)) { resolved = resolved.slice(0, -1); }
+ const comparison = isWindowsPlatform ? resolved.toLowerCase() : resolved;
+ const comparisonWithSep = comparison + obj.path.sep;
+ roots.push({ comparison: comparison, comparisonWithSep: comparisonWithSep });
+ };
+ addRoot(obj.os.tmpdir());
+ if (typeof obj.parent.filespath === 'string') { addRoot(obj.path.join(obj.parent.filespath, 'tmp')); }
+ return roots;
+ })();
+ function resolveSafeUploadTempPath(tempPath) {
+ if (typeof tempPath !== 'string') { return null; }
+ var resolvedPath;
+ try { resolvedPath = obj.path.normalize(obj.path.resolve(tempPath)); } catch (ex) { return null; }
+ var comparisonPath = isWindowsPlatform ? resolvedPath.toLowerCase() : resolvedPath;
+ var comparisonPathNoTrailing = comparisonPath;
+ if ((comparisonPathNoTrailing.length > 1) && comparisonPathNoTrailing.endsWith(obj.path.sep)) { comparisonPathNoTrailing = comparisonPathNoTrailing.slice(0, -1); }
+ for (var i = 0; i < safeUploadTempRoots.length; i++) {
+ var root = safeUploadTempRoots[i];
+ if ((comparisonPathNoTrailing === root.comparison) || comparisonPath.startsWith(root.comparisonWithSep)) { return resolvedPath; }
+ }
+ return null;
+ }
+
// Web relay sessions
var webRelayNextSessionId = 1;
var webRelaySessions = {} // UserId/SessionId/Host --> Web Relay Session
@@ -2863,12 +2895,12 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
res.set('Content-Type', 'text/html');
let url = domain.url;
if (Object.keys(req.query).length > 0) { url += "?" + Object.keys(req.query).map(function(key) { return encodeURIComponent(key) + "=" + encodeURIComponent(req.query[key]); }).join("&"); }
-
+
// check for relaystate is set, test against configured server name and accepted query params
if(req.body && req.body.RelayState !== undefined){
var relayState = decodeURIComponent(req.body.RelayState);
var serverName = (obj.getWebServerName(domain, req)).replaceAll('.','\\.');
-
+
var regexstr = `(?<=https:\\/\\/(?:.+?\\.)?${serverName}\\/?)` +
`.*((?<=([\\?&])gotodevicename=(.{64})|` +
`gotonode=(.{64})|` +
@@ -2888,13 +2920,13 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
`webrtc=|` +
`hide=|` +
`viewmode=(\\d+)(?=[\\&]|\\b)))`;
-
+
var regex = new RegExp(regexstr);
if(regex.test(relayState)){
url = relayState;
}
}
-
+
res.end('