Skip to content

Commit 6d955d1

Browse files
committed
Change to regex Matches instead of Replace
Also use ToJson which is more DRY.
1 parent 54e0101 commit 6d955d1

File tree

9 files changed

+125
-52
lines changed

9 files changed

+125
-52
lines changed

src/NuGetGallery/Controllers/AuthenticationController.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Net.Mail;
1010
using System.Security.Claims;
1111
using System.Threading.Tasks;
12+
using System.Web;
1213
using System.Web.Mvc;
1314
using NuGet.Services.Entities;
1415
using NuGet.Services.Messaging.Email;
@@ -527,7 +528,7 @@ public virtual async Task<ActionResult> LinkOrChangeExternalCredential(string re
527528
// The identity value contains cookie non-compliant characters like `<, >`(eg: John Doe <[email protected]>),
528529
// These need to be replaced so that they are not treated as HTML tags
529530
TempData["RawErrorMessage"] = string.Format(Strings.ChangeCredential_Failed,
530-
newCredential.Identity.Replace("<", "&lt;").Replace(">", "&gt;"),
531+
HttpUtility.HtmlEncode(newCredential.Identity),
531532
UriExtensions.GetExternalUrlAnchorTag("FAQs page", GalleryConstants.FAQLinks.MSALinkedToAnotherAccount));
532533
}
533534

src/NuGetGallery/Helpers/HtmlExtensions.cs

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Diagnostics;
77
using System.Linq;
88
using System.Linq.Expressions;
9+
using System.Text;
910
using System.Text.RegularExpressions;
1011
using System.Web;
1112
using System.Web.Mvc;
@@ -60,21 +61,34 @@ public static IHtmlString BreakWord(this HtmlHelper self, string text)
6061

6162
public static IHtmlString PreFormattedText(this HtmlHelper self, string text, IGalleryConfigurationService configurationService)
6263
{
63-
// Encode HTML entities. Important! Security!
64-
var encodedText = HttpUtility.HtmlEncode(text);
65-
// Turn HTTP and HTTPS URLs into links.
66-
// Source: https://stackoverflow.com/a/4750468
67-
string anchorEvaluator(Match match)
64+
void appendText(StringBuilder builder, string inputText)
65+
{
66+
var encodedText = HttpUtility.HtmlEncode(inputText);
67+
68+
// Replace new lines with the <br /> tag.
69+
encodedText = encodedText.Replace("\n", "<br />");
70+
71+
// Replace more than one space in a row with a space then &nbsp;.
72+
encodedText = RegexEx.TryReplaceWithTimeout(
73+
encodedText,
74+
" +",
75+
match => " " + string.Join(string.Empty, Enumerable.Repeat("&nbsp;", match.Value.Length - 1)),
76+
RegexOptions.None);
77+
78+
builder.Append(encodedText);
79+
}
80+
81+
void appendUrl(StringBuilder builder, string inputText)
6882
{
6983
string trimmedEntityValue = string.Empty;
70-
string trimmedAnchorValue = match.Value;
84+
string trimmedAnchorValue = inputText;
7185

7286
foreach (var trimmedEntity in _trimmedHtmlEntities)
7387
{
74-
if (match.Value.EndsWith(trimmedEntity))
88+
if (inputText.EndsWith(trimmedEntity))
7589
{
7690
// Remove trailing html entity from anchor URL
77-
trimmedAnchorValue = match.Value.Substring(0, match.Value.Length - trimmedEntity.Length);
91+
trimmedAnchorValue = inputText.Substring(0, inputText.Length - trimmedEntity.Length);
7892
trimmedEntityValue = trimmedEntity;
7993

8094
break;
@@ -85,36 +99,67 @@ string anchorEvaluator(Match match)
8599
{
86100
string anchorText = formattedUri;
87101
string siteRoot = configurationService.GetSiteRoot(useHttps: true);
102+
88103
// Format links to NuGet packages
89-
Match packageMatch = RegexEx.MatchWithTimeout(formattedUri, $@"({Regex.Escape(siteRoot)}\/packages\/(?<name>\w+([_.-]\w+)*(\/[0-9a-zA-Z-.]+)?)\/?$)", RegexOptions.IgnoreCase);
104+
Match packageMatch = RegexEx.MatchWithTimeout(
105+
formattedUri,
106+
$@"({Regex.Escape(siteRoot)}\/packages\/(?<name>\w+([_.-]\w+)*(\/[0-9a-zA-Z-.]+)?)\/?$)",
107+
RegexOptions.IgnoreCase);
90108
if (packageMatch != null && packageMatch.Groups["name"].Success)
91109
{
92110
anchorText = packageMatch.Groups["name"].Value;
93111
}
94112

95-
return $"<a href=\"{formattedUri}\" rel=\"nofollow\">{anchorText}</a>" + trimmedEntityValue;
113+
builder.AppendFormat(
114+
"<a href=\"{0}\" rel=\"nofollow\">{1}</a>{2}",
115+
HttpUtility.HtmlEncode(formattedUri),
116+
HttpUtility.HtmlEncode(anchorText),
117+
HttpUtility.HtmlEncode(trimmedEntityValue));
118+
}
119+
else
120+
{
121+
builder.Append(HttpUtility.HtmlEncode(inputText));
96122
}
97-
98-
return match.Value;
99123
}
100124

101-
encodedText = RegexEx.TryReplaceWithTimeout(
102-
encodedText,
125+
// Turn HTTP and HTTPS URLs into links.
126+
// Source: https://stackoverflow.com/a/4750468
127+
var matches = RegexEx.MatchesWithTimeout(
128+
text,
103129
@"((http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&amp;:/~\+#]*[\w\-\@?^=%&amp;/~\+#])?)",
104-
anchorEvaluator,
105130
RegexOptions.IgnoreCase);
106131

107-
// Replace new lines with the <br /> tag.
108-
encodedText = encodedText.Replace("\n", "<br />");
132+
var output = new StringBuilder(text.Length);
133+
var currentIndex = 0;
134+
135+
if (matches != null && matches.Count > 0)
136+
{
137+
foreach (Match match in matches)
138+
{
139+
// Encode the text literal before the URL, if any.
140+
var literalLength = match.Index - currentIndex;
141+
if (literalLength > 0)
142+
{
143+
var literal = text.Substring(currentIndex, literalLength);
144+
appendText(output, literal);
145+
}
146+
147+
// Encode the URL.
148+
var url = match.Value;
149+
appendUrl(output, url);
150+
151+
currentIndex = match.Index + match.Length;
152+
}
153+
}
109154

110-
// Replace more than one space in a row with a space then &nbsp;.
111-
encodedText = RegexEx.TryReplaceWithTimeout(
112-
encodedText,
113-
" +",
114-
match => " " + string.Join(string.Empty, Enumerable.Repeat("&nbsp;", match.Value.Length - 1)),
115-
RegexOptions.None);
155+
// Encode the text literal appearing after the last URL, if any.
156+
if (currentIndex < text.Length)
157+
{
158+
var literal = text.Substring(currentIndex, text.Length - currentIndex);
159+
appendText(output, literal);
160+
}
116161

117-
return self.Raw(encodedText);
162+
return self.Raw(output.ToString());
118163
}
119164

120165
public static IHtmlString ValidationSummaryFor(this HtmlHelper html, string key)

src/NuGetGallery/Helpers/RegexEx.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,20 @@ public static Match MatchWithTimeout(
4040
return null;
4141
}
4242
}
43+
44+
public static MatchCollection MatchesWithTimeout(
45+
string input,
46+
string pattern,
47+
RegexOptions options)
48+
{
49+
try
50+
{
51+
return Regex.Matches(input, pattern, options, Timeout);
52+
}
53+
catch (RegexMatchTimeoutException)
54+
{
55+
return null;
56+
}
57+
}
4358
}
4459
}

src/NuGetGallery/Views/Organizations/ManageOrganization.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101

102102
@section BottomScripts {
103103
<script type="text/javascript">
104-
var initialData = @Html.Raw(JsonConvert.SerializeObject(new
104+
var initialData = @Html.ToJson(new
105105
{
106106
AccountName = Model.AccountName,
107107
Members = Model.Members.Select(m => new
@@ -117,7 +117,7 @@
117117
UpdateMemberUrl = Url.UpdateOrganizationMember(Model.AccountName),
118118
DeleteMemberUrl = Url.DeleteOrganizationMember(Model.AccountName),
119119
ProfileUrlTemplate = Url.UserTemplate().LinkTemplate
120-
}));
120+
});
121121
</script>
122122
@ViewHelpers.SectionsScript(this)
123123
@Scripts.Render("~/Scripts/gallery/page-manage-organization.min.js")

src/NuGetGallery/Views/Packages/DisplayPackage.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,7 @@
10901090
</style>
10911091

10921092
<script type="text/javascript">
1093-
var packageManagers = [@Html.Raw(string.Join(",", packageManagers.Select(pm => "\"" + pm.Id + "\"")))];
1093+
var packageManagers = @Html.ToJson(packageManagers.Select(pm => pm.Id).ToList());
10941094
</script>
10951095

10961096
@Scripts.Render("~/Scripts/gallery/page-display-package.min.js")

src/NuGetGallery/Views/Packages/Manage.cshtml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,19 +80,20 @@
8080
var strings_RemovingOrganization = "@Html.Raw(Strings.ManagePackageOwners_RemovingOrganization)";
8181
var strings_RemovingSelf = "@Html.Raw(Strings.ManagePackageOwners_RemovingSelf)";
8282
83-
// We are using JsonConvert instead of Json to serialize JSON in this CSHTML file because otherwise, with a large number of versions, this will overflow the maximum output JSON length limit for Json.
83+
// We are using ToJson (which uses Newtonsoft.Json) instead of Json.Encode to serialize JSON in this CSHTML
84+
// file because otherwise, with a large number of versions, this will overflow the maximum output JSON length limit for Json.
8485
8586
// Set up delete section
86-
var versionListedState = @Html.Raw(JsonConvert.SerializeObject(Model.VersionListedStateDictionary));
87+
var versionListedState = @Html.ToJson(Model.VersionListedStateDictionary);
8788
8889
// Set up deprecation section
8990
var strings_SelectAlternateVersionOption = "Latest";
90-
var versionDeprecationState = @Html.Raw(JsonConvert.SerializeObject(Model.VersionDeprecationStateDictionary));
91+
var versionDeprecationState = @Html.ToJson(Model.VersionDeprecationStateDictionary);
9192
9293
$(function () {
9394
// Set up documentation section
9495
var readMeModel = {
95-
"Versions": @Html.Raw(JsonConvert.SerializeObject(Model.VersionReadMeStateDictionary)),
96+
"Versions": @Html.ToJson(Model.VersionReadMeStateDictionary),
9697
"Edit": @Html.Raw(Json.Encode(Model.ReadMe))
9798
};
9899

src/NuGetGallery/Views/Users/ApiKeys.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@
499499

500500
@section bottomScripts {
501501
<script type="text/javascript">
502-
var initialData = @Html.Raw(JsonConvert.SerializeObject(new
502+
var initialData = @Html.ToJson(new
503503
{
504504
ApiKeys = Model.ApiKeys,
505505
PackageOwners = Model.PackageOwners,
@@ -520,7 +520,7 @@
520520
ApiKeyNew = Url.Absolute("~/Content/gallery/img/api-key-new.svg"),
521521
ApiKeyNewFallback = Url.Absolute("~/Content/gallery/img/api-key-new-256x256.png"),
522522
}
523-
}));
523+
});
524524
</script>
525525
@Scripts.Render("~/Scripts/gallery/page-api-keys.min.js")
526526
}

src/NuGetGallery/Views/Users/Packages.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@
484484
var strings_RequiredSigner_AnyWithSignedResult = "@Html.Raw(Strings.RequiredSigner_AnyWithSignedResult)";
485485
var strings_RequiredSigner_Confirm = "@Html.Raw(Strings.RequiredSigner_Confirm)";
486486
487-
var initialData = @Html.Raw(JsonConvert.SerializeObject(new
487+
var initialData = @Html.ToJson(new
488488
{
489489
Owners = Model.Owners,
490490
ListedPackages = Model.ListedPackages
@@ -499,7 +499,7 @@
499499
.Select(r => GetSerializableOwnerRequest(r)),
500500
DefaultPackageIconUrl = Url.Absolute("~/Content/gallery/img/default-package-icon.svg"),
501501
PackageIconUrlFallback = Url.Absolute("~/Content/gallery/img/default-package-icon-256x256.png")
502-
}));
502+
});
503503
</script>
504504
@ViewHelpers.SectionsScript(this)
505505
@Scripts.Render("~/Scripts/gallery/page-manage-packages.min.js")

0 commit comments

Comments
 (0)