From 56df8cec98f65b33fd645da1e237a235b71f479d Mon Sep 17 00:00:00 2001
From: "Evgeny @ SimpleX Chat"
<259188159+evgeny-simplex@users.noreply.github.com>
Date: Wed, 3 Jun 2026 20:22:06 +0000
Subject: [PATCH] atomic file updates, escape attributes
---
src/Simplex/Chat/Web.hs | 14 +++++++++-----
website/src/js/simplex-lib.jsc | 19 ++++++++++++++-----
2 files changed, 23 insertions(+), 10 deletions(-)
diff --git a/src/Simplex/Chat/Web.hs b/src/Simplex/Chat/Web.hs
index cf1bc9941f..eb53ce741d 100644
--- a/src/Simplex/Chat/Web.hs
+++ b/src/Simplex/Chat/Web.hs
@@ -26,7 +26,7 @@ where
import Control.Concurrent.STM (check)
import Control.Exception (SomeException, catch)
import Control.Logger.Simple
-import Control.Monad (forM_, unless, void, when)
+import Control.Monad (forM_, void, when)
import Control.Monad.Except (runExceptT)
import Data.Either (rights)
import Data.Int (Int64)
@@ -77,7 +77,7 @@ import Simplex.Chat.Types
import Simplex.Messaging.Agent.Store.Common (withTransaction)
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Parsers (defaultJSON)
-import System.Directory (createDirectoryIfMissing, listDirectory, removeFile)
+import System.Directory (createDirectoryIfMissing, listDirectory, removeFile, renameFile)
import System.FilePath (takeExtension, (>))
import UnliftIO.STM
@@ -261,7 +261,10 @@ renderGroupPreview WebPreviewConfig {webJsonDir, webPreviewItemCount} cc user gI
messages = msgs,
updatedAt = ts
}
- LB.writeFile (webJsonDir > fName) (J.encode preview)
+ let destPath = webJsonDir > fName
+ tmpPath = destPath <> ".tmp"
+ LB.writeFile tmpPath (J.encode preview)
+ renameFile tmpPath destPath
pure $ corsEntry publicGroupId <$> publicGroupAccess
Nothing -> pure Nothing
where
@@ -395,8 +398,9 @@ writeCorsConfig entries path =
removeStaleFiles :: FilePath -> S.Set FilePath -> IO ()
removeStaleFiles dir activeFiles = do
- let jsonFiles = S.filter (\f -> takeExtension f == ".json") . S.fromList
- allFiles <- jsonFiles <$> listDirectory dir
+ let isPreviewFile f = takeExtension f == ".json" && all isBase64Url (takeWhile (/= '.') f)
+ isBase64Url c = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_'
+ allFiles <- S.filter isPreviewFile . S.fromList <$> listDirectory dir
mapM_ (\f -> removeFile (dir > f)) $ S.difference allFiles activeFiles
toFormattedText :: Text -> Maybe MarkdownList
diff --git a/website/src/js/simplex-lib.jsc b/website/src/js/simplex-lib.jsc
index 6d0aa6454b..b4b2d86541 100644
--- a/website/src/js/simplex-lib.jsc
+++ b/website/src/js/simplex-lib.jsc
@@ -14,6 +14,15 @@ function escapeHtml(text) {
.replace(/\n/g, "
");
}
+function escapeAttr(text) {
+ return text
+ .replace(/&/g, "&")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
+ .replace(//g, ">");
+}
+
function getSimplexLinkDescr(linkType) {
switch (linkType) {
case 'contact': return 'SimpleX contact address';
@@ -71,17 +80,17 @@ function renderMarkdown(fts) {
break;
case 'uri':
let href = text.startsWith('http://') || text.startsWith('https://') || text.startsWith('simplex:/') ? text : 'https://' + text;
- html += `${escapeHtml(text)}`;
+ html += `${escapeHtml(text)}`;
break;
case 'hyperLink': {
const { showText, linkUri } = format;
- html += `${escapeHtml(showText ?? linkUri)}`;
+ html += `${escapeHtml(showText ?? linkUri)}`;
break;
}
case 'simplexLink': {
const { showText, linkType, simplexUri, smpHosts } = format;
const linkText = showText ? escapeHtml(showText) : getSimplexLinkDescr(linkType);
- html += `${linkText} (${viaHost(smpHosts)})`;
+ html += `${linkText} (${escapeHtml(viaHost(smpHosts))})`;
break;
}
case 'command':
@@ -91,10 +100,10 @@ function renderMarkdown(fts) {
html += `${escapeHtml(text)}`;
break;
case 'email':
- html += `${escapeHtml(text)}`;
+ html += `${escapeHtml(text)}`;
break;
case 'phone':
- html += `${escapeHtml(text)}`;
+ html += `${escapeHtml(text)}`;
break;
case 'unknown':
html += escapeHtml(text);