Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance problems when huge number of portal aliases is in use #2514

Merged
merged 6 commits into from
Jan 8, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions DNN Platform/Library/Common/Utilities/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System.Text.RegularExpressions;

namespace DotNetNuke.Common.Utilities
{
Expand Down Expand Up @@ -41,6 +42,22 @@ public static string NormalizeString(this string input)
: Iso8859Encoding.GetString(Encoding.Convert(Encoding.UTF8, Iso8859Encoding, Encoding.UTF8.GetBytes(input))).ToLowerInvariant();
}

/// <summary>
/// Alternative to <see cref="string.Replace(string, string)"/> that supports case insensitive replacement
/// </summary>
/// <param name="source">The source.</param>
/// <param name="oldValue">The old value.</param>
/// <param name="newValue">The new value.</param>
/// <returns></returns>
public static string ReplaceIgnoreCase(this string source, string oldValue, string newValue)
{
if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(oldValue) || oldValue.Equals(newValue, System.StringComparison.OrdinalIgnoreCase))
{
return source;
}
return Regex.Replace(source, Regex.Escape(oldValue), newValue, RegexOptions.IgnoreCase);
}

private static readonly Encoding Iso8859Encoding = Encoding.GetEncoding("iso-8859-8");
}

Expand Down
43 changes: 33 additions & 10 deletions DNN Platform/Library/Entities/Urls/AdvancedUrlRewriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public class AdvancedUrlRewriter : UrlRewriterBase
private static readonly Regex AumDebugRegex = new Regex(@"(&|\?)_aumdebug=[A-Z]+(?:&|$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex RewritePathRx = new Regex("(?:&(?<parm>.[^&]+)=$)", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
private static readonly Regex UrlSlashesRegex = new Regex("[\\\\/]\\.\\.[\\\\/]", RegexOptions.Compiled);
private static readonly Regex AliasUrlRegex = new Regex(@"(?:^(?<http>http[s]{0,1}://){0,1})(?:(?<alias>_ALIAS_)(?<path>$|\?[\w]*|/[\w]*))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);

#region Private Members

Expand Down Expand Up @@ -152,31 +153,53 @@ public void ProcessTestRequestWithContext(HttpContext context,

private PortalAliasInfo GetPortalAlias(FriendlyUrlSettings settings, string requestUrl, out bool redirectAlias, out bool isPrimaryAlias, out string wrongAlias)
{
PortalAliasInfo alias = null;
PortalAliasInfo aliasInfo = null;
redirectAlias = false;
wrongAlias = null;
isPrimaryAlias = false;
OrderedDictionary portalRegexes = TabIndexController.GetPortalAliasRegexes(settings);
foreach (string regexPattern in portalRegexes.Keys)
OrderedDictionary portalAliases = TabIndexController.GetPortalAliases(settings);
foreach (string alias in portalAliases.Keys)
{
//split out the portal alias from the regex pattern representing that alias
var regex = RegexUtils.GetCachedRegex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
var aliasMatch = regex.Match(requestUrl);
var urlToMatch = requestUrl;
// in fact, requested url should contain alias
// for better performance, need to check whether we want to proceed with a whole url matching or not
// if alias is not a part of url -> let's proceed to the next iteration
var aliasIndex = urlToMatch.IndexOf(alias, StringComparison.InvariantCultureIgnoreCase);
if (aliasIndex < 0)
{
continue;
}
else
{
// we do not accept URL if the first occurence of alias is presented somewhere in the query string
var queryIndex = urlToMatch.IndexOf("?", StringComparison.InvariantCultureIgnoreCase);
if (queryIndex >= 0 && queryIndex < aliasIndex)
{
// alias is in the query string, go to the next alias
continue;
}
// we are fine here, lets prepare URL to be validated in regex
urlToMatch = urlToMatch.ReplaceIgnoreCase(alias, "_ALIAS_");
}
// check whether requested URL has the right URL format containing existing alias
// i.e. url is http://dnndev.me/site1/query?string=test, alias is dnndev.me/site1
// in the below expression we will validate following value http://_ALIAS_/query?string=test
var aliasMatch = AliasUrlRegex.Match(urlToMatch);
if (aliasMatch.Success)
{
//check for mobile browser and matching
var aliasEx = (PortalAliasInfo)portalRegexes[regexPattern];
var aliasEx = (PortalAliasInfo)portalAliases[alias];
redirectAlias = aliasEx.Redirect;
if (redirectAlias)
{
wrongAlias = aliasMatch.Groups["alias"].Value;
wrongAlias = alias;
}
isPrimaryAlias = aliasEx.IsPrimary;
alias = aliasEx;
aliasInfo = aliasEx;
break;
}
}
return alias;
return aliasInfo;
}

private void ProcessRequest(HttpContext context,
Expand Down
12 changes: 6 additions & 6 deletions DNN Platform/Library/Entities/Urls/CacheController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public class CacheController
private const string FriendlyUrlSettingsKey = "url_FriendlyUrlSettings";
private const string RedirectActionsKey = "url_ParameterRedirectActions_{0}";
private const string RewriteActionsKey = "url_ParameterRewriteActions_{0}";
private const string PortalAliasRegexesKey = "url_PortalAliasRegex";
private const string PortalAliasesKey = "url_PortalAliases";
private const string UserProfileActionsKey = "url_UserProfileActions";
private const string PortalModuleProvidersForTabKey = "url_ModuleProvidersForTab_{0}_{1}";
private const string PortalModuleProvidersAllTabsKey = "url_ModuleProvidersAllTabs_{0}";
Expand Down Expand Up @@ -433,9 +433,9 @@ internal static Dictionary<int, SharedList<ParameterRewriteAction>> GetParameter
return rewriteActions;
}

internal static OrderedDictionary GetPortalAliasesRegexesFromCache()
internal static OrderedDictionary GetPortalAliasesFromCache()
{
object raw = DataCache.GetCache(PortalAliasRegexesKey);
object raw = DataCache.GetCache(PortalAliasesKey);
return (raw != null) ? (OrderedDictionary)raw : null;
}

Expand Down Expand Up @@ -760,9 +760,9 @@ internal static void StorePortalAliasesInCache(SharedDictionary<string, PortalAl
SetPortalCache(PortalAliasListKey, aliases, settings);
}

internal static void StorePortalAliasesRegexesInCache(OrderedDictionary regexList, FriendlyUrlSettings settings)
internal static void StorePortalAliasesInCache(OrderedDictionary aliasList, FriendlyUrlSettings settings)
{
SetPortalCache(PortalAliasRegexesKey, regexList, settings);
SetPortalCache(PortalAliasesKey, aliasList, settings);
}

internal void StoreTabPathsInCache(int portalId, SharedDictionary<string, string> tabPathDictionary, FriendlyUrlSettings settings)
Expand Down Expand Up @@ -791,7 +791,7 @@ public static void FlushPageIndexFromCache()
DataCache.RemoveCache(UrlPortalsKey);
DataCache.RemoveCache(UserProfileActionsKey);
DataCache.RemoveCache(PortalAliasListKey);
DataCache.RemoveCache(PortalAliasRegexesKey);
DataCache.RemoveCache(PortalAliasesKey);
DataCache.RemoveCache(TabPathsKey);
}

Expand Down
34 changes: 14 additions & 20 deletions DNN Platform/Library/Entities/Urls/TabIndexController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -936,26 +936,22 @@ private static void AddToTabDict(SharedDictionary<string, string> tabIndex,
}
}

private static OrderedDictionary BuildPortalAliasesRegexDictionary()
private static OrderedDictionary BuildPortalAliasesDictionary()
{
var aliases = PortalAliasController.Instance.GetPortalAliases();
//create a new OrderedDictionary. We use this because we
//want to key by the correct regex pattern and return the
//portalAlias that matches, and we want to preserve the
//order of the items, such that the item with the most path separators (/)
//is at the front of the list.
var regexList = new OrderedDictionary(aliases.Count);
//this regex pattern, when formatted with the httpAlias, will match a request
//for this portalAlias
const string aliasRegexPattern = @"(?:^(?<http>http[s]{0,1}://){0,1})(?:(?<alias>_ALIAS_)(?<path>$|\?[\w]*|/[\w]*))";
var aliasList = new OrderedDictionary(aliases.Count);
var pathLengths = new List<int>();
foreach (string aliasKey in aliases.Keys)
{
PortalAliasInfo alias = aliases[aliasKey];
//regex escape the portal alias for inclusion into a regex pattern
string plainAlias = alias.HTTPAlias;
string escapedAlias = Regex.Escape(plainAlias);
var aliasesToAdd = new List<string> { escapedAlias };
var aliasesToAdd = new List<string> { plainAlias };
//check for existence of www. version of domain, if it doesn't have a www.
if (plainAlias.StartsWith("www.", StringComparison.InvariantCultureIgnoreCase))
{
Expand All @@ -965,7 +961,7 @@ private static OrderedDictionary BuildPortalAliasesRegexDictionary()
if (!aliases.Contains(noWWWVersion))
{
//there is no no-www version of the alias
aliasesToAdd.Add(Regex.Escape(noWWWVersion));
aliasesToAdd.Add(noWWWVersion);
}
}
}
Expand All @@ -974,7 +970,7 @@ private static OrderedDictionary BuildPortalAliasesRegexDictionary()
string wwwVersion = "www." + plainAlias;
if (!aliases.Contains(wwwVersion))
{
aliasesToAdd.Add(Regex.Escape(wwwVersion));
aliasesToAdd.Add(wwwVersion);
}
}
int count = 0;
Expand All @@ -984,8 +980,6 @@ private static OrderedDictionary BuildPortalAliasesRegexDictionary()
count++;
var aliasObject = new PortalAliasInfo(alias) { Redirect = count != 1 };

//format up the regex pattern by replacing the alias portion with the portal alias name
string regexPattern = aliasRegexPattern.Replace("_ALIAS_", aliasToAdd);
//work out how many path separators there are in the portalAlias (ie myalias/mychild = 1 path)
int pathLength = plainAlias.Split('/').GetUpperBound(0);
//now work out where in the list we should put this portalAlias regex pattern
Expand All @@ -1008,18 +1002,18 @@ private static OrderedDictionary BuildPortalAliasesRegexDictionary()
if (pathLengths.Count > 0 && insertPoint <= pathLengths.Count - 1)
{
//put the new regex pattern into the correct position
regexList.Insert(insertPoint, regexPattern, aliasObject);
aliasList.Insert(insertPoint, aliasToAdd, aliasObject);
pathLengths.Insert(insertPoint, pathLength);
}
else
{
//put the new regex pattern on the end of the list
regexList.Add(regexPattern, aliasObject);
aliasList.Add(aliasToAdd, aliasObject);
pathLengths.Add(pathLength);
}
}
}
return regexList;
return aliasList;
}

private static SharedDictionary<string, string> BuildTabDictionary(out PathSizes pathSizes,
Expand Down Expand Up @@ -1628,16 +1622,16 @@ internal static PortalAliasInfo GetPortalAliasByPortal(int portalId, string port
/// Returns an ordered dictionary of alias regex patterns. These patterns are used to identify a portal alias by getting a match.
/// </summary>
/// <returns></returns>
internal static OrderedDictionary GetPortalAliasRegexes(FriendlyUrlSettings settings)
internal static OrderedDictionary GetPortalAliases(FriendlyUrlSettings settings)
{
//object to return
OrderedDictionary regexList = CacheController.GetPortalAliasesRegexesFromCache();
if (regexList == null)
OrderedDictionary aliasList = CacheController.GetPortalAliasesFromCache();
if (aliasList == null)
{
regexList = BuildPortalAliasesRegexDictionary();
CacheController.StorePortalAliasesRegexesInCache(regexList, settings);
aliasList = BuildPortalAliasesDictionary();
CacheController.StorePortalAliasesInCache(aliasList, settings);
}
return regexList;
return aliasList;
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ private void ExecuteTestForTab(string test, TabInfo tab, FriendlyUrlSettings set
Guid.Empty);
}

Assert.AreEqual(expectedResult, testUrl);
Assert.IsTrue(expectedResult.Equals(testUrl, StringComparison.InvariantCultureIgnoreCase));
}

private void UpdateTabName(int tabId, string newName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
myalias.com, Parent
mychildalias.com/child, Child
192.168.1.1, IPAdress
localhost/vdir, VirtualDirectory
localhost/vdir, VirtualDirectory
MyAliasMultiCase.Com, MultiCase
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Test, UserName, VanityUrl, VanityUrlPrefix, Scheme, Page Name, Params, Expected Url,
No Vanity Url, test, , , http://, Activity Feed, &UserId={userId}, http://{alias}/ActivityFeed/UserId/{userId}
Vanity Url Default, test, testuser, , http://, Activity Feed, &UserId={userId}, http://{alias}/users/{vanityUrl}
Vanity Url members, test, testuser, members, http://, Activity Feed, &UserId={userId}, http://{alias}/members/{vanityUrl}
Vanity Url Messages, test, testuser, , http://, Messages, &UserId={userId}, http://{alias}/users/{vanityUrl}/Messages
No Vanity Url, test1, , , http://, Activity Feed, &UserId={userId}, http://{alias}/ActivityFeed/UserId/{userId}
Vanity Url Default, test1, testuser, , http://, Activity Feed, &UserId={userId}, http://{alias}/users/{vanityUrl}
Vanity Url members, test1, testuser, members, http://, Activity Feed, &UserId={userId}, http://{alias}/members/{vanityUrl}
Vanity Url Messages, test1, testuser, , http://, Messages, &UserId={userId}, http://{alias}/users/{vanityUrl}/Messages
Loading