Skip to content

Commit

Permalink
Change to regex Matches instead of Replace
Browse files Browse the repository at this point in the history
Also use ToJson which is more DRY.
  • Loading branch information
joelverhagen committed Jun 5, 2020
1 parent 54e0101 commit 6d955d1
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 52 deletions.
3 changes: 2 additions & 1 deletion src/NuGetGallery/Controllers/AuthenticationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Net.Mail;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using NuGet.Services.Entities;
using NuGet.Services.Messaging.Email;
Expand Down Expand Up @@ -527,7 +528,7 @@ public virtual async Task<ActionResult> LinkOrChangeExternalCredential(string re
// The identity value contains cookie non-compliant characters like `<, >`(eg: John Doe <[email protected]>),
// These need to be replaced so that they are not treated as HTML tags
TempData["RawErrorMessage"] = string.Format(Strings.ChangeCredential_Failed,
newCredential.Identity.Replace("<", "&lt;").Replace(">", "&gt;"),
HttpUtility.HtmlEncode(newCredential.Identity),
UriExtensions.GetExternalUrlAnchorTag("FAQs page", GalleryConstants.FAQLinks.MSALinkedToAnotherAccount));
}

Expand Down
93 changes: 69 additions & 24 deletions src/NuGetGallery/Helpers/HtmlExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Mvc;
Expand Down Expand Up @@ -60,21 +61,34 @@ public static IHtmlString BreakWord(this HtmlHelper self, string text)

public static IHtmlString PreFormattedText(this HtmlHelper self, string text, IGalleryConfigurationService configurationService)
{
// Encode HTML entities. Important! Security!
var encodedText = HttpUtility.HtmlEncode(text);
// Turn HTTP and HTTPS URLs into links.
// Source: https://stackoverflow.com/a/4750468
string anchorEvaluator(Match match)
void appendText(StringBuilder builder, string inputText)
{
var encodedText = HttpUtility.HtmlEncode(inputText);

// Replace new lines with the <br /> tag.
encodedText = encodedText.Replace("\n", "<br />");

// Replace more than one space in a row with a space then &nbsp;.
encodedText = RegexEx.TryReplaceWithTimeout(
encodedText,
" +",
match => " " + string.Join(string.Empty, Enumerable.Repeat("&nbsp;", match.Value.Length - 1)),
RegexOptions.None);

builder.Append(encodedText);
}

void appendUrl(StringBuilder builder, string inputText)
{
string trimmedEntityValue = string.Empty;
string trimmedAnchorValue = match.Value;
string trimmedAnchorValue = inputText;

foreach (var trimmedEntity in _trimmedHtmlEntities)
{
if (match.Value.EndsWith(trimmedEntity))
if (inputText.EndsWith(trimmedEntity))
{
// Remove trailing html entity from anchor URL
trimmedAnchorValue = match.Value.Substring(0, match.Value.Length - trimmedEntity.Length);
trimmedAnchorValue = inputText.Substring(0, inputText.Length - trimmedEntity.Length);
trimmedEntityValue = trimmedEntity;

break;
Expand All @@ -85,36 +99,67 @@ string anchorEvaluator(Match match)
{
string anchorText = formattedUri;
string siteRoot = configurationService.GetSiteRoot(useHttps: true);

// Format links to NuGet packages
Match packageMatch = RegexEx.MatchWithTimeout(formattedUri, $@"({Regex.Escape(siteRoot)}\/packages\/(?<name>\w+([_.-]\w+)*(\/[0-9a-zA-Z-.]+)?)\/?$)", RegexOptions.IgnoreCase);
Match packageMatch = RegexEx.MatchWithTimeout(
formattedUri,
$@"({Regex.Escape(siteRoot)}\/packages\/(?<name>\w+([_.-]\w+)*(\/[0-9a-zA-Z-.]+)?)\/?$)",
RegexOptions.IgnoreCase);
if (packageMatch != null && packageMatch.Groups["name"].Success)
{
anchorText = packageMatch.Groups["name"].Value;
}

return $"<a href=\"{formattedUri}\" rel=\"nofollow\">{anchorText}</a>" + trimmedEntityValue;
builder.AppendFormat(
"<a href=\"{0}\" rel=\"nofollow\">{1}</a>{2}",
HttpUtility.HtmlEncode(formattedUri),
HttpUtility.HtmlEncode(anchorText),
HttpUtility.HtmlEncode(trimmedEntityValue));
}
else
{
builder.Append(HttpUtility.HtmlEncode(inputText));
}

return match.Value;
}

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

// Replace new lines with the <br /> tag.
encodedText = encodedText.Replace("\n", "<br />");
var output = new StringBuilder(text.Length);
var currentIndex = 0;

if (matches != null && matches.Count > 0)
{
foreach (Match match in matches)
{
// Encode the text literal before the URL, if any.
var literalLength = match.Index - currentIndex;
if (literalLength > 0)
{
var literal = text.Substring(currentIndex, literalLength);
appendText(output, literal);
}

// Encode the URL.
var url = match.Value;
appendUrl(output, url);

currentIndex = match.Index + match.Length;
}
}

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

return self.Raw(encodedText);
return self.Raw(output.ToString());
}

public static IHtmlString ValidationSummaryFor(this HtmlHelper html, string key)
Expand Down
15 changes: 15 additions & 0 deletions src/NuGetGallery/Helpers/RegexEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,20 @@ public static Match MatchWithTimeout(
return null;
}
}

public static MatchCollection MatchesWithTimeout(
string input,
string pattern,
RegexOptions options)
{
try
{
return Regex.Matches(input, pattern, options, Timeout);
}
catch (RegexMatchTimeoutException)
{
return null;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@

@section BottomScripts {
<script type="text/javascript">
var initialData = @Html.Raw(JsonConvert.SerializeObject(new
var initialData = @Html.ToJson(new
{
AccountName = Model.AccountName,
Members = Model.Members.Select(m => new
Expand All @@ -117,7 +117,7 @@
UpdateMemberUrl = Url.UpdateOrganizationMember(Model.AccountName),
DeleteMemberUrl = Url.DeleteOrganizationMember(Model.AccountName),
ProfileUrlTemplate = Url.UserTemplate().LinkTemplate
}));
});
</script>
@ViewHelpers.SectionsScript(this)
@Scripts.Render("~/Scripts/gallery/page-manage-organization.min.js")
Expand Down
2 changes: 1 addition & 1 deletion src/NuGetGallery/Views/Packages/DisplayPackage.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -1090,7 +1090,7 @@
</style>

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

@Scripts.Render("~/Scripts/gallery/page-display-package.min.js")
Expand Down
9 changes: 5 additions & 4 deletions src/NuGetGallery/Views/Packages/Manage.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,20 @@
var strings_RemovingOrganization = "@Html.Raw(Strings.ManagePackageOwners_RemovingOrganization)";
var strings_RemovingSelf = "@Html.Raw(Strings.ManagePackageOwners_RemovingSelf)";
// 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.
// We are using ToJson (which uses Newtonsoft.Json) instead of Json.Encode 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.
// Set up delete section
var versionListedState = @Html.Raw(JsonConvert.SerializeObject(Model.VersionListedStateDictionary));
var versionListedState = @Html.ToJson(Model.VersionListedStateDictionary);
// Set up deprecation section
var strings_SelectAlternateVersionOption = "Latest";
var versionDeprecationState = @Html.Raw(JsonConvert.SerializeObject(Model.VersionDeprecationStateDictionary));
var versionDeprecationState = @Html.ToJson(Model.VersionDeprecationStateDictionary);
$(function () {
// Set up documentation section
var readMeModel = {
"Versions": @Html.Raw(JsonConvert.SerializeObject(Model.VersionReadMeStateDictionary)),
"Versions": @Html.ToJson(Model.VersionReadMeStateDictionary),
"Edit": @Html.Raw(Json.Encode(Model.ReadMe))
};
Expand Down
4 changes: 2 additions & 2 deletions src/NuGetGallery/Views/Users/ApiKeys.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@

@section bottomScripts {
<script type="text/javascript">
var initialData = @Html.Raw(JsonConvert.SerializeObject(new
var initialData = @Html.ToJson(new
{
ApiKeys = Model.ApiKeys,
PackageOwners = Model.PackageOwners,
Expand All @@ -520,7 +520,7 @@
ApiKeyNew = Url.Absolute("~/Content/gallery/img/api-key-new.svg"),
ApiKeyNewFallback = Url.Absolute("~/Content/gallery/img/api-key-new-256x256.png"),
}
}));
});
</script>
@Scripts.Render("~/Scripts/gallery/page-api-keys.min.js")
}
4 changes: 2 additions & 2 deletions src/NuGetGallery/Views/Users/Packages.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@
var strings_RequiredSigner_AnyWithSignedResult = "@Html.Raw(Strings.RequiredSigner_AnyWithSignedResult)";
var strings_RequiredSigner_Confirm = "@Html.Raw(Strings.RequiredSigner_Confirm)";
var initialData = @Html.Raw(JsonConvert.SerializeObject(new
var initialData = @Html.ToJson(new
{
Owners = Model.Owners,
ListedPackages = Model.ListedPackages
Expand All @@ -499,7 +499,7 @@
.Select(r => GetSerializableOwnerRequest(r)),
DefaultPackageIconUrl = Url.Absolute("~/Content/gallery/img/default-package-icon.svg"),
PackageIconUrlFallback = Url.Absolute("~/Content/gallery/img/default-package-icon-256x256.png")
}));
});
</script>
@ViewHelpers.SectionsScript(this)
@Scripts.Render("~/Scripts/gallery/page-manage-packages.min.js")
Expand Down
Loading

0 comments on commit 6d955d1

Please sign in to comment.