Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit f1608c2

Browse files
committed
Add support for JsConfig.EscapeHtmlChars
1 parent 724c7da commit f1608c2

File tree

4 files changed

+95
-19
lines changed

4 files changed

+95
-19
lines changed

src/ServiceStack.Text/JsConfig.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ public static JsConfigScope CreateScope(string config, JsConfigScope scope = nul
123123
case "escapeunicode":
124124
scope.EscapeUnicode = boolValue;
125125
break;
126+
case "ehc":
127+
case "escapehtmlchars":
128+
scope.EscapeHtmlChars = boolValue;
129+
break;
126130
case "ipf":
127131
case "includepublicfields":
128132
scope.IncludePublicFields = boolValue;
@@ -748,15 +752,34 @@ public static bool EscapeUnicode
748752
get
749753
{
750754
return (JsConfigScope.Current != null ? JsConfigScope.Current.EscapeUnicode : null)
751-
?? sEscapeUnicode
752-
?? false;
755+
?? sEscapeUnicode
756+
?? false;
753757
}
754758
set
755759
{
756760
if (!sEscapeUnicode.HasValue) sEscapeUnicode = value;
757761
}
758762
}
759763

764+
/// <summary>
765+
/// Gets or sets a value indicating if HTML entity chars [&gt; &lt; &amp; = '] should be escaped as "\uXXXX".
766+
/// </summary>
767+
private static bool? sEscapeHtmlChars;
768+
public static bool EscapeHtmlChars
769+
{
770+
// obeying the use of ThreadStatic, but allowing for setting JsConfig once as is the normal case
771+
get
772+
{
773+
return (JsConfigScope.Current != null ? JsConfigScope.Current.EscapeHtmlChars : null)
774+
?? sEscapeHtmlChars
775+
?? false;
776+
}
777+
set
778+
{
779+
if (!sEscapeHtmlChars.HasValue) sEscapeHtmlChars = value;
780+
}
781+
}
782+
760783
/// <summary>
761784
/// Gets or sets a value indicating if the framework should call an error handler when
762785
/// an exception happens during the deserialization.
@@ -934,6 +957,7 @@ public static void Reset()
934957
sSkipDateTimeConversion = null;
935958
sAppendUtcOffset = null;
936959
sEscapeUnicode = null;
960+
sEscapeHtmlChars = null;
937961
sOnDeserializationError = null;
938962
sIncludePublicFields = null;
939963
HasSerializeFn = new HashSet<Type>();

src/ServiceStack.Text/JsConfigScope.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public void Dispose()
7676
public bool? AssumeUtc { get; set; }
7777
public bool? AppendUtcOffset { get; set; }
7878
public bool? EscapeUnicode { get; set; }
79+
public bool? EscapeHtmlChars { get; set; }
7980
public bool? PreferInterfaces { get; set; }
8081
public bool? IncludePublicFields { get; set; }
8182
public int? MaxDepth { get; set; }

src/ServiceStack.Text/Json/JsonUtils.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt
33

44
using System.IO;
5+
using System.Runtime.CompilerServices;
56

67
namespace ServiceStack.Text.Json
78
{
@@ -43,7 +44,11 @@ public static void WriteString(TextWriter writer, string value)
4344
writer.Write(Null);
4445
return;
4546
}
46-
if (!HasAnyEscapeChars(value))
47+
48+
var escapeHtmlChars = JsConfig.EscapeHtmlChars;
49+
var escapeUnicode = JsConfig.EscapeUnicode;
50+
51+
if (!HasAnyEscapeChars(value, escapeHtmlChars))
4752
{
4853
writer.Write(QuoteChar);
4954
writer.Write(value);
@@ -90,22 +95,46 @@ public static void WriteString(TextWriter writer, string value)
9095
continue;
9196
}
9297

98+
if (escapeHtmlChars)
99+
{
100+
switch (c)
101+
{
102+
case '<':
103+
writer.Write("\\u003c");
104+
continue;
105+
case '>':
106+
writer.Write("\\u003e");
107+
continue;
108+
case '&':
109+
writer.Write("\\u0026");
110+
continue;
111+
case '=':
112+
writer.Write("\\u003d");
113+
continue;
114+
case '\'':
115+
writer.Write("\\u0027");
116+
continue;
117+
}
118+
}
119+
93120
if (c.IsPrintable())
94121
{
95122
writer.Write(c);
96123
continue;
97124
}
98125

99126
// http://json.org/ spec requires any control char to be escaped
100-
if (JsConfig.EscapeUnicode || char.IsControl(c))
127+
if (escapeUnicode || char.IsControl(c))
101128
{
102129
// Default, turn into a \uXXXX sequence
103130
IntToHex(c, hexSeqBuffer);
104131
writer.Write("\\u");
105132
writer.Write(hexSeqBuffer);
106133
}
107134
else
135+
{
108136
writer.Write(c);
137+
}
109138
}
110139

111140
writer.Write(QuoteChar);
@@ -121,15 +150,16 @@ private static bool IsPrintable(this char c)
121150
/// Searches the string for one or more non-printable characters.
122151
/// </summary>
123152
/// <param name="value">The string to search.</param>
153+
/// <param name="escapeHtmlChars"></param>
124154
/// <returns>True if there are any characters that require escaping. False if the value can be written verbatim.</returns>
125155
/// <remarks>
126156
/// Micro optimizations: since quote and backslash are the only printable characters requiring escaping, removed previous optimization
127157
/// (using flags instead of value.IndexOfAny(EscapeChars)) in favor of two equality operations saving both memory and CPU time.
128158
/// Also slightly reduced code size by re-arranging conditions.
129159
/// TODO: Possible Linq-only solution requires profiling: return value.Any(c => !c.IsPrintable() || c == QuoteChar || c == EscapeChar);
130160
/// </remarks>
131-
private static bool HasAnyEscapeChars(string value)
132161
[MethodImpl(MethodImplOptions.AggressiveInlining)]
162+
private static bool HasAnyEscapeChars(string value, bool escapeHtmlChars)
133163
{
134164
var len = value.Length;
135165
for (var i = 0; i < len; i++)
@@ -138,7 +168,11 @@ private static bool HasAnyEscapeChars(string value)
138168

139169
// c is not printable
140170
// OR c is a printable that requires escaping (quote and backslash).
141-
if (!c.IsPrintable() || c == QuoteChar || c == EscapeChar) return true;
171+
if (!c.IsPrintable() || c == QuoteChar || c == EscapeChar)
172+
return true;
173+
174+
if (escapeHtmlChars && (c == '<' || c == '>' || c == '&' || c == '=' || c == '\\'))
175+
return true;
142176
}
143177
return false;
144178
}

tests/ServiceStack.Text.Tests/JsConfigTests.cs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@
44

55
namespace ServiceStack.Text.Tests
66
{
7+
public class Foo
8+
{
9+
public string FooBar { get; set; }
10+
}
11+
12+
public class Bar
13+
{
14+
public string FooBar { get; set; }
15+
}
16+
17+
[TestFixture]
18+
public class JsConfigAdhocTests
19+
{
20+
[Test]
21+
public void Can_escape_Html_Chars()
22+
{
23+
var dto = new Foo { FooBar = "<script>danger();</script>" };
24+
25+
Assert.That(dto.ToJson(), Is.EqualTo("{\"FooBar\":\"<script>danger();</script>\"}"));
26+
27+
JsConfig.EscapeHtmlChars = true;
28+
29+
Assert.That(dto.ToJson(), Is.EqualTo("{\"FooBar\":\"\\u003cscript\\u003edanger();\\u003c/script\\u003e\"}"));
30+
31+
JsConfig.Reset();
32+
}
33+
}
34+
735
[TestFixture]
836
public class JsConfigTests
937
{
@@ -37,25 +65,14 @@ public void Can_override_default_configuration()
3765
}
3866
}
3967

40-
public class Foo
41-
{
42-
public string FooBar { get; set; }
43-
}
44-
45-
public class Bar
46-
{
47-
public string FooBar { get; set; }
48-
}
49-
50-
5168
[TestFixture]
5269
public class SerializEmitLowerCaseUnderscoreNamesTests
5370
{
5471
[Test]
5572
public void TestJsonDataWithJsConfigScope()
5673
{
57-
using (JsConfig.With(emitLowercaseUnderscoreNames:true,
58-
propertyConvention:PropertyConvention.Lenient))
74+
using (JsConfig.With(emitLowercaseUnderscoreNames: true,
75+
propertyConvention: PropertyConvention.Lenient))
5976
AssertObjectJson();
6077
}
6178

0 commit comments

Comments
 (0)