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);