From 7fff5c0d18601bc1e0dd359586c3db81abb474eb Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Sun, 25 May 2025 10:55:49 +0200 Subject: [PATCH 1/2] Fix for ModuleBase ReplaceTokens #5332 Replaced the ReplaceTokens logic to replace all tokens in the string --- Oqtane.Client/Modules/ModuleBase.cs | 126 ++++++++++++---------------- 1 file changed, 54 insertions(+), 72 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 18a8a5e9..2dac151e 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -1,16 +1,16 @@ -using Microsoft.AspNetCore.Components; -using Oqtane.Shared; -using Oqtane.Models; -using System.Threading.Tasks; -using Oqtane.Services; using System; -using Oqtane.Enums; -using Oqtane.UI; using System.Collections.Generic; -using Microsoft.JSInterop; -using System.Linq; using System.Dynamic; -using System.Reflection; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Oqtane.Enums; +using Oqtane.Models; +using Oqtane.Services; +using Oqtane.Shared; +using Oqtane.UI; namespace Oqtane.Modules { @@ -424,72 +424,54 @@ namespace Oqtane.Modules public string ReplaceTokens(string content, object obj) { - var tokens = new List(); - var pos = content.IndexOf("["); - if (pos != -1) - { - if (content.IndexOf("]", pos) != -1) - { - var token = content.Substring(pos, content.IndexOf("]", pos) - pos + 1); - if (token.Contains(":")) - { - tokens.Add(token.Substring(1, token.Length - 2)); - } - } - pos = content.IndexOf("[", pos + 1); - } - if (tokens.Count != 0) - { - foreach (string token in tokens) - { - var segments = token.Split(":"); - if (segments.Length >= 2 && segments.Length <= 3) - { - var objectName = string.Join(":", segments, 0, segments.Length - 1); - var propertyName = segments[segments.Length - 1]; - var propertyValue = ""; + // Pattern: [Object:Property] or [Object:SubObject:Property] + var pattern = @"\[(\w+(?::\w+){1,2})\]"; - switch (objectName) - { - case "ModuleState": - propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null).ToString(); - break; - case "PageState": - propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null).ToString(); - break; - case "PageState:Alias": - propertyValue = PageState.Alias.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null).ToString(); - break; - case "PageState:Site": - propertyValue = PageState.Site.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null).ToString(); - break; - case "PageState:Page": - propertyValue = PageState.Page.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null).ToString(); - break; - case "PageState:User": - propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null).ToString(); - break; - case "PageState:Route": - propertyValue = PageState.Route.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null).ToString(); - break; - default: - if (obj != null && obj.GetType().Name == objectName) - { - propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null).ToString(); - } - break; - } - if (propertyValue != null) - { - content = content.Replace("[" + token + "]", propertyValue); - } + return Regex.Replace(content, pattern, match => + { + string token = match.Groups[1].Value; + var segments = token.Split(':'); + if (segments.Length < 2 || segments.Length > 3) + return match.Value; // Leave as is if not a valid token - } + string objectName = string.Join(":", segments, 0, segments.Length - 1); + string propertyName = segments[segments.Length - 1]; + string propertyValue = null; + + switch (objectName) + { + case "ModuleState": + propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null)?.ToString(); + break; + case "PageState": + propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null)?.ToString(); + break; + case "PageState:Alias": + propertyValue = PageState.Alias?.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null)?.ToString(); + break; + case "PageState:Site": + propertyValue = PageState.Site?.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null)?.ToString(); + break; + case "PageState:Page": + propertyValue = PageState.Page?.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null)?.ToString(); + break; + case "PageState:User": + propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null)?.ToString(); + break; + case "PageState:Route": + propertyValue = PageState.Route?.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null)?.ToString(); + break; + default: + if (obj != null && obj.GetType().Name == objectName) + { + propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null)?.ToString(); + } + break; } - } - return content; + + return propertyValue ?? match.Value; // If not found, leave token as is + }); } - // date methods public DateTime? UtcToLocal(DateTime? datetime) { From ef4fbcbb8a8fc5da4ee79fe9d9e8cdeac32476cd Mon Sep 17 00:00:00 2001 From: Leigh Pointer Date: Wed, 28 May 2025 17:30:19 +0200 Subject: [PATCH 2/2] Update ModuleBase.cs This method replaces all tokens in the format [Object:Property] or [Object:SubObject:Property] within a string. Efficient string parsing and reflection ensure flexibility with performance. It supports deeply nested properties, optional default fallback values (e.g. [PageState:User:Email|default@email.com]), and uses caching to optimize repeated token resolution without regex. --- Oqtane.Client/Modules/ModuleBase.cs | 114 +++++++++++++++++----------- 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 2dac151e..39681067 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Dynamic; using System.Linq; -using System.Text.RegularExpressions; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; @@ -424,54 +424,82 @@ namespace Oqtane.Modules public string ReplaceTokens(string content, object obj) { - // Pattern: [Object:Property] or [Object:SubObject:Property] - var pattern = @"\[(\w+(?::\w+){1,2})\]"; + // Using StringBuilder avoids the performance penalty of repeated string allocations + // that occur with string.Replace or string concatenation inside loops. + var sb = new StringBuilder(); + var cache = new Dictionary(); // Cache to store resolved tokens + int index = 0; - return Regex.Replace(content, pattern, match => + // Loop through content to find and replace all tokens + while (index < content.Length) { - string token = match.Groups[1].Value; - var segments = token.Split(':'); - if (segments.Length < 2 || segments.Length > 3) - return match.Value; // Leave as is if not a valid token - - string objectName = string.Join(":", segments, 0, segments.Length - 1); - string propertyName = segments[segments.Length - 1]; - string propertyValue = null; - - switch (objectName) + int start = content.IndexOf('[', index); // Find start of token + if (start == -1) { - case "ModuleState": - propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null)?.ToString(); - break; - case "PageState": - propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null)?.ToString(); - break; - case "PageState:Alias": - propertyValue = PageState.Alias?.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null)?.ToString(); - break; - case "PageState:Site": - propertyValue = PageState.Site?.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null)?.ToString(); - break; - case "PageState:Page": - propertyValue = PageState.Page?.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null)?.ToString(); - break; - case "PageState:User": - propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null)?.ToString(); - break; - case "PageState:Route": - propertyValue = PageState.Route?.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null)?.ToString(); - break; - default: - if (obj != null && obj.GetType().Name == objectName) - { - propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null)?.ToString(); - } - break; + sb.Append(content, index, content.Length - index); // Append remaining content + break; } - return propertyValue ?? match.Value; // If not found, leave token as is - }); + int end = content.IndexOf(']', start); // Find end of token + if (end == -1) + { + sb.Append(content, index, content.Length - index); // Append unmatched content + break; + } + + sb.Append(content, index, start - index); // Append content before token + + string token = content.Substring(start + 1, end - start - 1); // Extract token without brackets + string[] parts = token.Split('|', 2); // Separate default fallback if present + string key = parts[0]; + string fallback = parts.Length == 2 ? parts[1] : null; + + if (!cache.TryGetValue(token, out string replacement)) // Check cache first + { + replacement = "[" + token + "]"; // Default replacement is original token + string[] segments = key.Split(':'); + + if (segments.Length >= 2) + { + object current = GetTarget(segments[0], obj); // Start from root object + for (int i = 1; i < segments.Length && current != null; i++) + { + var type = current.GetType(); + var prop = type.GetProperty(segments[i]); + current = prop?.GetValue(current); + } + + if (current != null) + { + replacement = current.ToString(); + } + else if (fallback != null) + { + replacement = fallback; // Use fallback if available + } + } + cache[token] = replacement; // Store in cache + } + + sb.Append(replacement); // Append replacement value + index = end + 1; // Move index past token + } + + return sb.ToString(); } + + // Resolve the object instance for a given object name + // Easy to extend with additional object types + private object GetTarget(string name, object obj) + { + return name switch + { + "ModuleState" => ModuleState, + "PageState" => PageState, + _ => (obj != null && obj.GetType().Name == name) ? obj : null // Fallback to obj + }; + } + // date methods public DateTime? UtcToLocal(DateTime? datetime) {