using System.Linq.Dynamic.Core; using System.Net; using System.Text.RegularExpressions; using Nop.Core.Domain.Messages; namespace Nop.Services.Messages; /// /// Tokenizer /// public partial class Tokenizer : ITokenizer { #region Fields protected readonly MessageTemplatesSettings _messageTemplatesSettings; #endregion #region Ctor public Tokenizer(MessageTemplatesSettings messageTemplatesSettings) { _messageTemplatesSettings = messageTemplatesSettings; } #endregion #region Utilities /// /// Returns a new string in which all occurrences of a specified string in the current instance are replaced with another specified string /// /// Original string /// The string to be replaced /// The string to replace all occurrences of pattern string /// A string that is equivalent to the current string except that all instances of pattern are replaced with replacement string protected string Replace(string original, string pattern, string replacement) { //for case sensitive comparison use base string.Replace() method var stringComparison = _messageTemplatesSettings.CaseInvariantReplacement ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (stringComparison == StringComparison.Ordinal) return original.Replace(pattern, replacement); //or do some routine work here var count = 0; var position0 = 0; int position1; var inc = original.Length / pattern.Length * (replacement.Length - pattern.Length); var chars = new char[original.Length + Math.Max(0, inc)]; while ((position1 = original.IndexOf(pattern, position0, stringComparison)) != -1) { for (var i = position0; i < position1; ++i) chars[count++] = original[i]; for (var i = 0; i < replacement.Length; ++i) chars[count++] = replacement[i]; position0 = position1 + pattern.Length; } if (position0 == 0) return original; for (var i = position0; i < original.Length; ++i) chars[count++] = original[i]; return new string(chars, 0, count); } /// /// Replace tokens /// /// The template with token keys inside /// The sequence of tokens to use /// The value indicating whether tokens should be HTML encoded /// The value indicating whether string token values should be wrapped in quotes /// Text with all token keys replaces by token value protected string ReplaceTokens(string template, IEnumerable tokens, bool htmlEncode = false, bool stringWithQuotes = false) { foreach (var token in tokens) { var tokenValue = token.Value ?? string.Empty; //wrap the value in quotes if (stringWithQuotes && tokenValue is string) tokenValue = $"\"{tokenValue}\""; else { //do not encode URLs if (htmlEncode && !token.NeverHtmlEncoded) tokenValue = WebUtility.HtmlEncode(tokenValue.ToString()); } template = Replace(template, $@"%{token.Key}%", tokenValue.ToString()); } return template; } /// /// Resolve conditional statements and replace them with appropriate values /// /// The template with token keys inside /// The sequence of tokens to use /// Text with all conditional statements replaces by appropriate values protected string ReplaceConditionalStatements(string template, IEnumerable tokens) { //define regex rules var regexFullConditionalSatement = new Regex(@"(?:(?'Group' %if)|(?'Condition-Group' endif%)|(?! (%if|endif%)).)*(?(Group)(?!))", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.IgnoreCase); var regexCondition = new Regex(@"\s*\((?:(?'Group' \()|(?'-Group' \))|[^()])*(?(Group)(?!))\)\s*", RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline | RegexOptions.IgnoreCase); //find conditional statements in the original template var conditionalStatements = regexFullConditionalSatement.Matches(template) .SelectMany(match => match.Groups["Condition"].Captures.Select(capture => new { capture.Index, FullStatement = capture.Value, Condition = regexCondition.Match(capture.Value).Value })).ToList(); if (!conditionalStatements.Any()) return template; //replace conditional statements foreach (var statement in conditionalStatements.OrderBy(statement => statement.Index)) { var conditionIsMet = false; if (!string.IsNullOrEmpty(statement.Condition)) { try { //replace tokens (string values are wrap in quotes) var conditionString = ReplaceTokens(statement.Condition, tokens, stringWithQuotes: true); conditionIsMet = new[] { statement }.AsQueryable().Where(conditionString).Any(); } catch { // ignored } } template = template.Replace(conditionIsMet ? statement.Condition : statement.FullStatement, string.Empty); } template = template.Replace("%if", string.Empty).Replace("endif%", string.Empty); //return template with resolved conditional statements return template; } #endregion #region Methods /// /// Replace all of the token key occurrences inside the specified template text with corresponded token values /// /// The template with token keys inside /// The sequence of tokens to use /// The value indicating whether tokens should be HTML encoded /// Text with all token keys replaces by token value public string Replace(string template, IEnumerable tokens, bool htmlEncode) { ArgumentException.ThrowIfNullOrWhiteSpace(template); ArgumentNullException.ThrowIfNull(tokens); //replace conditional statements template = ReplaceConditionalStatements(template, tokens); //replace tokens template = ReplaceTokens(template, tokens, htmlEncode); return template; } #endregion }