Merge pull request #6347 from umbraco/v8/bugfix/AB2684-purelive-model-regen

Lazily recompile PureLive models and lazily create models for nucache
This commit is contained in:
Warren Buckley
2019-09-18 10:47:04 +01:00
committed by GitHub
30 changed files with 642 additions and 507 deletions

View File

@@ -33,7 +33,6 @@ namespace Umbraco.Core.Configuration
/// </summary>
private static void ResetInternal()
{
GlobalSettingsExtensions.Reset();
_reservedPaths = null;
_reservedUrls = null;
HasSmtpServer = null;

View File

@@ -1,6 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
using System.Web.Routing;
using Umbraco.Core.IO;
@@ -9,22 +11,9 @@ namespace Umbraco.Core.Configuration
{
public static class GlobalSettingsExtensions
{
/// <summary>
/// Used in unit testing to reset all config items, this is automatically called by GlobalSettings.Reset()
/// </summary>
internal static void Reset()
{
_reservedUrlsCache = null;
_mvcArea = null;
}
private static readonly object Locker = new object();
//make this volatile so that we can ensure thread safety with a double check lock
private static volatile string _reservedUrlsCache;
private static string _reservedPathsCache;
private static HashSet<string> _reservedList = new HashSet<string>();
private static string _mvcArea;
/// <summary>
/// This returns the string of the MVC Area route.
/// </summary>
@@ -40,6 +29,13 @@ namespace Umbraco.Core.Configuration
{
if (_mvcArea != null) return _mvcArea;
_mvcArea = GetUmbracoMvcAreaNoCache(globalSettings);
return _mvcArea;
}
internal static string GetUmbracoMvcAreaNoCache(this IGlobalSettings globalSettings)
{
if (globalSettings.Path.IsNullOrWhiteSpace())
{
throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified");
@@ -48,95 +44,8 @@ namespace Umbraco.Core.Configuration
var path = globalSettings.Path;
if (path.StartsWith(SystemDirectories.Root)) // beware of TrimStart, see U4-2518
path = path.Substring(SystemDirectories.Root.Length);
_mvcArea = path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower();
return _mvcArea;
return path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower();
}
/// <summary>
/// Determines whether the specified URL is reserved or is inside a reserved path.
/// </summary>
/// <param name="globalSettings"></param>
/// <param name="url">The URL to check.</param>
/// <returns>
/// <c>true</c> if the specified URL is reserved; otherwise, <c>false</c>.
/// </returns>
internal static bool IsReservedPathOrUrl(this IGlobalSettings globalSettings, string url)
{
if (_reservedUrlsCache == null)
{
lock (Locker)
{
if (_reservedUrlsCache == null)
{
// store references to strings to determine changes
_reservedPathsCache = globalSettings.ReservedPaths;
_reservedUrlsCache = globalSettings.ReservedUrls;
// add URLs and paths to a new list
var newReservedList = new HashSet<string>();
foreach (var reservedUrlTrimmed in _reservedUrlsCache
.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim().ToLowerInvariant())
.Where(x => x.IsNullOrWhiteSpace() == false)
.Select(reservedUrl => IOHelper.ResolveUrl(reservedUrl).Trim().EnsureStartsWith("/"))
.Where(reservedUrlTrimmed => reservedUrlTrimmed.IsNullOrWhiteSpace() == false))
{
newReservedList.Add(reservedUrlTrimmed);
}
foreach (var reservedPathTrimmed in _reservedPathsCache
.Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim().ToLowerInvariant())
.Where(x => x.IsNullOrWhiteSpace() == false)
.Select(reservedPath => IOHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/"))
.Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false))
{
newReservedList.Add(reservedPathTrimmed);
}
// use the new list from now on
_reservedList = newReservedList;
}
}
}
//The url should be cleaned up before checking:
// * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/'
// * We shouldn't be comparing the query at all
var pathPart = url.Split(new[] {'?'}, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant();
if (pathPart.Contains(".") == false)
{
pathPart = pathPart.EnsureEndsWith('/');
}
// return true if url starts with an element of the reserved list
return _reservedList.Any(x => pathPart.InvariantStartsWith(x));
}
/// <summary>
/// Determines whether the current request is reserved based on the route table and
/// whether the specified URL is reserved or is inside a reserved path.
/// </summary>
/// <param name="globalSettings"></param>
/// <param name="url"></param>
/// <param name="httpContext"></param>
/// <param name="routes">The route collection to lookup the request in</param>
/// <returns></returns>
internal static bool IsReservedPathOrUrl(this IGlobalSettings globalSettings, string url, HttpContextBase httpContext, RouteCollection routes)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
if (routes == null) throw new ArgumentNullException(nameof(routes));
//check if the current request matches a route, if so then it is reserved.
//TODO: This value should be cached! Else this is doing double routing in MVC every request!
var route = routes.GetRouteData(httpContext);
if (route != null)
return true;
//continue with the standard ignore routine
return globalSettings.IsReservedPathOrUrl(url);
}
}
}

View File

@@ -11,7 +11,9 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Packaging;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
@@ -25,13 +27,14 @@ namespace Umbraco.Core.Packaging
private readonly ILocalizationService _localizationService;
private readonly IDataTypeService _dataTypeService;
private readonly PropertyEditorCollection _propertyEditors;
private readonly IScopeProvider _scopeProvider;
private readonly IEntityService _entityService;
private readonly IContentTypeService _contentTypeService;
private readonly IContentService _contentService;
public PackageDataInstallation(ILogger logger, IFileService fileService, IMacroService macroService, ILocalizationService localizationService,
IDataTypeService dataTypeService, IEntityService entityService, IContentTypeService contentTypeService,
IContentService contentService, PropertyEditorCollection propertyEditors)
IContentService contentService, PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider)
{
_logger = logger;
_fileService = fileService;
@@ -39,12 +42,13 @@ namespace Umbraco.Core.Packaging
_localizationService = localizationService;
_dataTypeService = dataTypeService;
_propertyEditors = propertyEditors;
_scopeProvider = scopeProvider;
_entityService = entityService;
_contentTypeService = contentTypeService;
_contentService = contentService;
}
#region Uninstall
#region Install/Uninstall
public UninstallationSummary UninstallPackageData(PackageDefinition package, int userId)
{
@@ -57,93 +61,97 @@ namespace Umbraco.Core.Packaging
var removedDataTypes = new List<IDataType>();
var removedLanguages = new List<ILanguage>();
//Uninstall templates
foreach (var item in package.Templates.ToArray())
using (var scope = _scopeProvider.CreateScope())
{
if (int.TryParse(item, out var nId) == false) continue;
var found = _fileService.GetTemplate(nId);
if (found != null)
//Uninstall templates
foreach (var item in package.Templates.ToArray())
{
removedTemplates.Add(found);
_fileService.DeleteTemplate(found.Alias, userId);
if (int.TryParse(item, out var nId) == false) continue;
var found = _fileService.GetTemplate(nId);
if (found != null)
{
removedTemplates.Add(found);
_fileService.DeleteTemplate(found.Alias, userId);
}
package.Templates.Remove(nId.ToString());
}
package.Templates.Remove(nId.ToString());
}
//Uninstall macros
foreach (var item in package.Macros.ToArray())
{
if (int.TryParse(item, out var nId) == false) continue;
var macro = _macroService.GetById(nId);
if (macro != null)
//Uninstall macros
foreach (var item in package.Macros.ToArray())
{
removedMacros.Add(macro);
_macroService.Delete(macro, userId);
if (int.TryParse(item, out var nId) == false) continue;
var macro = _macroService.GetById(nId);
if (macro != null)
{
removedMacros.Add(macro);
_macroService.Delete(macro, userId);
}
package.Macros.Remove(nId.ToString());
}
package.Macros.Remove(nId.ToString());
}
//Remove Document Types
var contentTypes = new List<IContentType>();
var contentTypeService = _contentTypeService;
foreach (var item in package.DocumentTypes.ToArray())
{
if (int.TryParse(item, out var nId) == false) continue;
var contentType = contentTypeService.Get(nId);
if (contentType == null) continue;
contentTypes.Add(contentType);
package.DocumentTypes.Remove(nId.ToString(CultureInfo.InvariantCulture));
}
//Order the DocumentTypes before removing them
if (contentTypes.Any())
{
// TODO: I don't think this ordering is necessary
var orderedTypes = (from contentType in contentTypes
orderby contentType.ParentId descending, contentType.Id descending
select contentType).ToList();
removedContentTypes.AddRange(orderedTypes);
contentTypeService.Delete(orderedTypes, userId);
}
//Remove Dictionary items
foreach (var item in package.DictionaryItems.ToArray())
{
if (int.TryParse(item, out var nId) == false) continue;
var di = _localizationService.GetDictionaryItemById(nId);
if (di != null)
//Remove Document Types
var contentTypes = new List<IContentType>();
var contentTypeService = _contentTypeService;
foreach (var item in package.DocumentTypes.ToArray())
{
removedDictionaryItems.Add(di);
_localizationService.Delete(di, userId);
if (int.TryParse(item, out var nId) == false) continue;
var contentType = contentTypeService.Get(nId);
if (contentType == null) continue;
contentTypes.Add(contentType);
package.DocumentTypes.Remove(nId.ToString(CultureInfo.InvariantCulture));
}
package.DictionaryItems.Remove(nId.ToString());
}
//Remove Data types
foreach (var item in package.DataTypes.ToArray())
{
if (int.TryParse(item, out var nId) == false) continue;
var dtd = _dataTypeService.GetDataType(nId);
if (dtd != null)
//Order the DocumentTypes before removing them
if (contentTypes.Any())
{
removedDataTypes.Add(dtd);
_dataTypeService.Delete(dtd, userId);
// TODO: I don't think this ordering is necessary
var orderedTypes = (from contentType in contentTypes
orderby contentType.ParentId descending, contentType.Id descending
select contentType).ToList();
removedContentTypes.AddRange(orderedTypes);
contentTypeService.Delete(orderedTypes, userId);
}
package.DataTypes.Remove(nId.ToString());
}
//Remove Langs
foreach (var item in package.Languages.ToArray())
{
if (int.TryParse(item, out var nId) == false) continue;
var lang = _localizationService.GetLanguageById(nId);
if (lang != null)
//Remove Dictionary items
foreach (var item in package.DictionaryItems.ToArray())
{
removedLanguages.Add(lang);
_localizationService.Delete(lang, userId);
if (int.TryParse(item, out var nId) == false) continue;
var di = _localizationService.GetDictionaryItemById(nId);
if (di != null)
{
removedDictionaryItems.Add(di);
_localizationService.Delete(di, userId);
}
package.DictionaryItems.Remove(nId.ToString());
}
package.Languages.Remove(nId.ToString());
//Remove Data types
foreach (var item in package.DataTypes.ToArray())
{
if (int.TryParse(item, out var nId) == false) continue;
var dtd = _dataTypeService.GetDataType(nId);
if (dtd != null)
{
removedDataTypes.Add(dtd);
_dataTypeService.Delete(dtd, userId);
}
package.DataTypes.Remove(nId.ToString());
}
//Remove Langs
foreach (var item in package.Languages.ToArray())
{
if (int.TryParse(item, out var nId) == false) continue;
var lang = _localizationService.GetLanguageById(nId);
if (lang != null)
{
removedLanguages.Add(lang);
_localizationService.Delete(lang, userId);
}
package.Languages.Remove(nId.ToString());
}
scope.Complete();
}
// create a summary of what was actually removed, for PackagingService.UninstalledPackage
@@ -164,14 +172,40 @@ namespace Umbraco.Core.Packaging
}
public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId)
{
using (var scope = _scopeProvider.CreateScope())
{
var installationSummary = new InstallationSummary
{
DataTypesInstalled = ImportDataTypes(compiledPackage.DataTypes.ToList(), userId),
LanguagesInstalled = ImportLanguages(compiledPackage.Languages, userId),
DictionaryItemsInstalled = ImportDictionaryItems(compiledPackage.DictionaryItems, userId),
MacrosInstalled = ImportMacros(compiledPackage.Macros, userId),
TemplatesInstalled = ImportTemplates(compiledPackage.Templates.ToList(), userId),
DocumentTypesInstalled = ImportDocumentTypes(compiledPackage.DocumentTypes, userId)
};
//we need a reference to the imported doc types to continue
var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x);
installationSummary.StylesheetsInstalled = ImportStylesheets(compiledPackage.Stylesheets, userId);
installationSummary.ContentInstalled = ImportContent(compiledPackage.Documents, importedDocTypes, userId);
scope.Complete();
return installationSummary;
}
}
#endregion
#region Content
public IEnumerable<IContent> ImportContent(IEnumerable<CompiledPackageDocument> docs, IDictionary<string, IContentType> importedDocumentTypes, int userId)
public IReadOnlyList<IContent> ImportContent(IEnumerable<CompiledPackageDocument> docs, IDictionary<string, IContentType> importedDocumentTypes, int userId)
{
return docs.SelectMany(x => ImportContent(x, -1, importedDocumentTypes, userId));
return docs.SelectMany(x => ImportContent(x, -1, importedDocumentTypes, userId)).ToList();
}
/// <summary>
@@ -352,7 +386,7 @@ namespace Umbraco.Core.Packaging
#region DocumentTypes
public IEnumerable<IContentType> ImportDocumentType(XElement docTypeElement, int userId)
public IReadOnlyList<IContentType> ImportDocumentType(XElement docTypeElement, int userId)
{
return ImportDocumentTypes(new[] { docTypeElement }, userId);
}
@@ -363,7 +397,7 @@ namespace Umbraco.Core.Packaging
/// <param name="docTypeElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation. Default is zero (admin).</param>
/// <returns>An enumerable list of generated ContentTypes</returns>
public IEnumerable<IContentType> ImportDocumentTypes(IEnumerable<XElement> docTypeElements, int userId)
public IReadOnlyList<IContentType> ImportDocumentTypes(IEnumerable<XElement> docTypeElements, int userId)
{
return ImportDocumentTypes(docTypeElements.ToList(), true, userId);
}
@@ -375,7 +409,7 @@ namespace Umbraco.Core.Packaging
/// <param name="importStructure">Boolean indicating whether or not to import the </param>
/// <param name="userId">Optional id of the User performing the operation. Default is zero (admin).</param>
/// <returns>An enumerable list of generated ContentTypes</returns>
public IEnumerable<IContentType> ImportDocumentTypes(IReadOnlyCollection<XElement> unsortedDocumentTypes, bool importStructure, int userId)
public IReadOnlyList<IContentType> ImportDocumentTypes(IReadOnlyCollection<XElement> unsortedDocumentTypes, bool importStructure, int userId)
{
var importedContentTypes = new Dictionary<string, IContentType>();
@@ -824,7 +858,7 @@ namespace Umbraco.Core.Packaging
/// <param name="dataTypeElements">Xml to import</param>
/// <param name="userId">Optional id of the user</param>
/// <returns>An enumerable list of generated DataTypeDefinitions</returns>
public IEnumerable<IDataType> ImportDataTypes(IReadOnlyCollection<XElement> dataTypeElements, int userId)
public IReadOnlyList<IDataType> ImportDataTypes(IReadOnlyCollection<XElement> dataTypeElements, int userId)
{
var dataTypes = new List<IDataType>();
@@ -953,13 +987,13 @@ namespace Umbraco.Core.Packaging
/// <param name="dictionaryItemElementList">Xml to import</param>
/// <param name="userId"></param>
/// <returns>An enumerable list of dictionary items</returns>
public IEnumerable<IDictionaryItem> ImportDictionaryItems(IEnumerable<XElement> dictionaryItemElementList, int userId)
public IReadOnlyList<IDictionaryItem> ImportDictionaryItems(IEnumerable<XElement> dictionaryItemElementList, int userId)
{
var languages = _localizationService.GetAllLanguages().ToList();
return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId);
}
private IEnumerable<IDictionaryItem> ImportDictionaryItems(IEnumerable<XElement> dictionaryItemElementList, List<ILanguage> languages, Guid? parentId, int userId)
private IReadOnlyList<IDictionaryItem> ImportDictionaryItems(IEnumerable<XElement> dictionaryItemElementList, List<ILanguage> languages, Guid? parentId, int userId)
{
var items = new List<IDictionaryItem>();
foreach (var dictionaryItemElement in dictionaryItemElementList)
@@ -1036,7 +1070,7 @@ namespace Umbraco.Core.Packaging
/// <param name="languageElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation</param>
/// <returns>An enumerable list of generated languages</returns>
public IEnumerable<ILanguage> ImportLanguages(IEnumerable<XElement> languageElements, int userId)
public IReadOnlyList<ILanguage> ImportLanguages(IEnumerable<XElement> languageElements, int userId)
{
var list = new List<ILanguage>();
foreach (var languageElement in languageElements)
@@ -1065,7 +1099,7 @@ namespace Umbraco.Core.Packaging
/// <param name="macroElements">Xml to import</param>
/// <param name="userId">Optional id of the User performing the operation</param>
/// <returns></returns>
public IEnumerable<IMacro> ImportMacros(IEnumerable<XElement> macroElements, int userId)
public IReadOnlyList<IMacro> ImportMacros(IEnumerable<XElement> macroElements, int userId)
{
var macros = macroElements.Select(ParseMacroElement).ToList();
@@ -1155,7 +1189,7 @@ namespace Umbraco.Core.Packaging
#region Stylesheets
public IEnumerable<IFile> ImportStylesheets(IEnumerable<XElement> stylesheetElements, int userId)
public IReadOnlyList<IFile> ImportStylesheets(IEnumerable<XElement> stylesheetElements, int userId)
{
var result = new List<IFile>();
@@ -1223,7 +1257,7 @@ namespace Umbraco.Core.Packaging
/// <param name="templateElements">Xml to import</param>
/// <param name="userId">Optional user id</param>
/// <returns>An enumerable list of generated Templates</returns>
public IEnumerable<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId)
public IReadOnlyList<ITemplate> ImportTemplates(IReadOnlyCollection<XElement> templateElements, int userId)
{
var templates = new List<ITemplate>();

View File

@@ -90,21 +90,8 @@ namespace Umbraco.Core.Packaging
public InstallationSummary InstallPackageData(PackageDefinition packageDefinition, CompiledPackage compiledPackage, int userId)
{
var installationSummary = new InstallationSummary
{
DataTypesInstalled = _packageDataInstallation.ImportDataTypes(compiledPackage.DataTypes.ToList(), userId),
LanguagesInstalled = _packageDataInstallation.ImportLanguages(compiledPackage.Languages, userId),
DictionaryItemsInstalled = _packageDataInstallation.ImportDictionaryItems(compiledPackage.DictionaryItems, userId),
MacrosInstalled = _packageDataInstallation.ImportMacros(compiledPackage.Macros, userId),
TemplatesInstalled = _packageDataInstallation.ImportTemplates(compiledPackage.Templates.ToList(), userId),
DocumentTypesInstalled = _packageDataInstallation.ImportDocumentTypes(compiledPackage.DocumentTypes, userId)
};
var installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId);
//we need a reference to the imported doc types to continue
var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x);
installationSummary.StylesheetsInstalled = _packageDataInstallation.ImportStylesheets(compiledPackage.Stylesheets, userId);
installationSummary.ContentInstalled = _packageDataInstallation.ImportContent(compiledPackage.Documents, importedDocTypes, userId);
installationSummary.Actions = CompiledPackageXmlParser.GetPackageActions(XElement.Parse(compiledPackage.Actions), compiledPackage.Name);
installationSummary.MetaData = compiledPackage;
installationSummary.FilesInstalled = packageDefinition.Files;

View File

@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core
@@ -15,12 +17,8 @@ namespace Umbraco.Core
/// <returns></returns>
public static bool IsLiveFactory(this IPublishedModelFactory factory) => factory is ILivePublishedModelFactory;
/// <summary>
/// Executes an action with a safe live factory
/// </summary>
/// <remarks>
/// <para>If the factory is a live factory, ensures it is refreshed and locked while executing the action.</para>
/// </remarks>
[Obsolete("This method is no longer used or necessary and will be removed from future")]
[EditorBrowsable(EditorBrowsableState.Never)]
public static void WithSafeLiveFactory(this IPublishedModelFactory factory, Action action)
{
if (factory is ILivePublishedModelFactory liveFactory)
@@ -37,5 +35,38 @@ namespace Umbraco.Core
action();
}
}
/// <summary>
/// Sets a flag to reset the ModelsBuilder models if the <see cref="IPublishedModelFactory"/> is <see cref="ILivePublishedModelFactory"/>
/// </summary>
/// <param name="factory"></param>
/// <param name="action"></param>
/// <remarks>
/// This does not recompile the pure live models, only sets a flag to tell models builder to recompile when they are requested.
/// </remarks>
internal static void WithSafeLiveFactoryReset(this IPublishedModelFactory factory, Action action)
{
if (factory is ILivePublishedModelFactory liveFactory)
{
lock (liveFactory.SyncRoot)
{
// TODO: Fix this in 8.3! - We need to change the ILivePublishedModelFactory interface to have a Reset method and then when we have an embedded MB
// version we will publicize the ResetModels (and change the name to Reset).
// For now, this will suffice and we'll use reflection, there should be no other implementation of ILivePublishedModelFactory.
// Calling ResetModels resets the MB flag so that the next time EnsureModels is called (which is called when nucache lazily calls CreateModel) it will
// trigger the recompiling of pure live models.
var resetMethod = liveFactory.GetType().GetMethod("ResetModels", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (resetMethod != null)
resetMethod.Invoke(liveFactory, null);
action();
}
}
else
{
action();
}
}
}
}

View File

@@ -1,6 +1,4 @@
using System.Web.Mvc;
using System.Web.Routing;
using Moq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
@@ -10,6 +8,7 @@ using Umbraco.Tests.TestHelpers;
namespace Umbraco.Tests.Configurations
{
[TestFixture]
public class GlobalSettingsTests : BaseWebTest
{
@@ -47,73 +46,18 @@ namespace Umbraco.Tests.Configurations
[TestCase("~/some-wacky/nestedPath", "/MyVirtualDir/NestedVDir/", "some-wacky-nestedpath")]
public void Umbraco_Mvc_Area(string path, string rootPath, string outcome)
{
var globalSettingsMock = Mock.Get(Factory.GetInstance<IGlobalSettings>()); //this will modify the IGlobalSettings instance stored in the container
globalSettingsMock.Setup(x => x.Path).Returns(IOHelper.ResolveUrl(path));
var globalSettings = SettingsForTests.GenerateMockGlobalSettings();
var globalSettingsMock = Mock.Get(globalSettings);
globalSettingsMock.Setup(x => x.Path).Returns(() => IOHelper.ResolveUrl(path));
SystemDirectories.Root = rootPath;
Assert.AreEqual(outcome, Current.Configs.Global().GetUmbracoMvcArea());
Assert.AreEqual(outcome, globalSettings.GetUmbracoMvcAreaNoCache());
}
[TestCase("/umbraco/editContent.aspx")]
[TestCase("/install/default.aspx")]
[TestCase("/install/")]
[TestCase("/install")]
[TestCase("/install/?installStep=asdf")]
[TestCase("/install/test.aspx")]
public void Is_Reserved_Path_Or_Url(string url)
{
var globalSettings = TestObjects.GetGlobalSettings();
Assert.IsTrue(globalSettings.IsReservedPathOrUrl(url));
}
[TestCase("/base/somebasehandler")]
[TestCase("/")]
[TestCase("/home.aspx")]
[TestCase("/umbraco-test")]
[TestCase("/install-test")]
[TestCase("/install.aspx")]
public void Is_Not_Reserved_Path_Or_Url(string url)
{
var globalSettings = TestObjects.GetGlobalSettings();
Assert.IsFalse(globalSettings.IsReservedPathOrUrl(url));
}
[TestCase("/Do/Not/match", false)]
[TestCase("/Umbraco/RenderMvcs", false)]
[TestCase("/Umbraco/RenderMvc", true)]
[TestCase("/Umbraco/RenderMvc/Index", true)]
[TestCase("/Umbraco/RenderMvc/Index/1234", true)]
[TestCase("/Umbraco/RenderMvc/Index/1234/9876", false)]
[TestCase("/api", true)]
[TestCase("/api/WebApiTest", true)]
[TestCase("/api/WebApiTest/1234", true)]
[TestCase("/api/WebApiTest/Index/1234", false)]
public void Is_Reserved_By_Route(string url, bool shouldMatch)
{
//reset the app config, we only want to test routes not the hard coded paths
var globalSettingsMock = Mock.Get(Factory.GetInstance<IGlobalSettings>()); //this will modify the IGlobalSettings instance stored in the container
globalSettingsMock.Setup(x => x.ReservedPaths).Returns("");
globalSettingsMock.Setup(x => x.ReservedUrls).Returns("");
var routes = new RouteCollection();
routes.MapRoute(
"Umbraco_default",
"Umbraco/RenderMvc/{action}/{id}",
new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional });
routes.MapRoute(
"WebAPI",
"api/{controller}/{id}",
new { controller = "WebApiTestController", action = "Index", id = UrlParameter.Optional });
var context = new FakeHttpContextFactory(url);
Assert.AreEqual(
shouldMatch,
globalSettingsMock.Object.IsReservedPathOrUrl(url, context.HttpContext, routes));
}
}
}

View File

@@ -11,6 +11,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Packaging;
using Umbraco.Core.Packaging;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
using File = System.IO.File;
@@ -45,7 +46,8 @@ namespace Umbraco.Tests.Packaging
Logger, ServiceContext.FileService, ServiceContext.MacroService, ServiceContext.LocalizationService,
ServiceContext.DataTypeService, ServiceContext.EntityService,
ServiceContext.ContentTypeService, ServiceContext.ContentService,
Factory.GetInstance<PropertyEditorCollection>());
Factory.GetInstance<PropertyEditorCollection>(),
Factory.GetInstance<IScopeProvider>());
private IPackageInstallation PackageInstallation => new PackageInstallation(
PackageDataInstallation,

View File

@@ -0,0 +1,80 @@
using System.Web.Mvc;
using System.Web.Routing;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Tests.TestHelpers;
using Umbraco.Web;
namespace Umbraco.Tests.Routing
{
[TestFixture]
public class RoutableDocumentFilterTests : BaseWebTest
{
[TestCase("/umbraco/editContent.aspx")]
[TestCase("/install/default.aspx")]
[TestCase("/install/")]
[TestCase("/install")]
[TestCase("/install/?installStep=asdf")]
[TestCase("/install/test.aspx")]
public void Is_Reserved_Path_Or_Url(string url)
{
var globalSettings = TestObjects.GetGlobalSettings();
var routableDocFilter = new RoutableDocumentFilter(globalSettings);
Assert.IsTrue(routableDocFilter.IsReservedPathOrUrl(url));
}
[TestCase("/base/somebasehandler")]
[TestCase("/")]
[TestCase("/home.aspx")]
[TestCase("/umbraco-test")]
[TestCase("/install-test")]
[TestCase("/install.aspx")]
public void Is_Not_Reserved_Path_Or_Url(string url)
{
var globalSettings = TestObjects.GetGlobalSettings();
var routableDocFilter = new RoutableDocumentFilter(globalSettings);
Assert.IsFalse(routableDocFilter.IsReservedPathOrUrl(url));
}
[TestCase("/Do/Not/match", false)]
[TestCase("/Umbraco/RenderMvcs", false)]
[TestCase("/Umbraco/RenderMvc", true)]
[TestCase("/Umbraco/RenderMvc/Index", true)]
[TestCase("/Umbraco/RenderMvc/Index/1234", true)]
[TestCase("/Umbraco/RenderMvc/Index/1234/9876", false)]
[TestCase("/api", true)]
[TestCase("/api/WebApiTest", true)]
[TestCase("/api/WebApiTest/1234", true)]
[TestCase("/api/WebApiTest/Index/1234", false)]
public void Is_Reserved_By_Route(string url, bool shouldMatch)
{
//reset the app config, we only want to test routes not the hard coded paths
var globalSettingsMock = Mock.Get(Factory.GetInstance<IGlobalSettings>()); //this will modify the IGlobalSettings instance stored in the container
globalSettingsMock.Setup(x => x.ReservedPaths).Returns("");
globalSettingsMock.Setup(x => x.ReservedUrls).Returns("");
var routableDocFilter = new RoutableDocumentFilter(globalSettingsMock.Object);
var routes = new RouteCollection();
routes.MapRoute(
"Umbraco_default",
"Umbraco/RenderMvc/{action}/{id}",
new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional });
routes.MapRoute(
"WebAPI",
"api/{controller}/{id}",
new { controller = "WebApiTestController", action = "Index", id = UrlParameter.Optional });
var context = new FakeHttpContextFactory(url);
Assert.AreEqual(
shouldMatch,
routableDocFilter.IsReservedPathOrUrl(url, context.HttpContext, routes));
}
}
}

View File

@@ -38,15 +38,11 @@ namespace Umbraco.Tests.Routing
_module = new UmbracoInjectedModule
(
globalSettings,
Mock.Of<IUmbracoContextAccessor>(),
Factory.GetInstance<IPublishedSnapshotService>(),
Factory.GetInstance<IUserService>(),
new UrlProviderCollection(new IUrlProvider[0]),
runtime,
logger,
null, // FIXME: PublishedRouter complexities...
Mock.Of<IVariationContextAccessor>(),
Mock.Of<IUmbracoContextFactory>()
Mock.Of<IUmbracoContextFactory>(),
new RoutableDocumentFilter(globalSettings)
);
runtime.Level = RuntimeLevel.Run;

View File

@@ -178,7 +178,7 @@ namespace Umbraco.Tests.TestHelpers
new PackagesRepository(contentService.Value, contentTypeService.Value, dataTypeService.Value, fileService.Value, macroService.Value, localizationService.Value,
new EntityXmlSerializer(contentService.Value, mediaService.Value, dataTypeService.Value, userService.Value, localizationService.Value, contentTypeService.Value, urlSegmentProviders), logger, "installedPackages.config"),
new PackageInstallation(
new PackageDataInstallation(logger, fileService.Value, macroService.Value, localizationService.Value, dataTypeService.Value, entityService.Value, contentTypeService.Value, contentService.Value, propertyEditorCollection),
new PackageDataInstallation(logger, fileService.Value, macroService.Value, localizationService.Value, dataTypeService.Value, entityService.Value, contentTypeService.Value, contentService.Value, propertyEditorCollection, scopeProvider),
new PackageFileInstallation(compiledPackageXmlParser, new ProfilingLogger(logger, new TestProfiler())),
compiledPackageXmlParser, Mock.Of<IPackageActionRunner>(),
new DirectoryInfo(IOHelper.GetRootDirectorySafe())));

View File

@@ -31,6 +31,7 @@ using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Tests.LegacyXmlPublishedCache;
using Umbraco.Tests.Testing.Objects.Accessors;
using Umbraco.Web.Cache;
namespace Umbraco.Tests.TestHelpers
{

View File

@@ -120,6 +120,7 @@
<Compile Include="Composing\CompositionTests.cs" />
<Compile Include="Composing\LightInjectValidation.cs" />
<Compile Include="Composing\ContainerConformingTests.cs" />
<Compile Include="Configurations\GlobalSettingsTests.cs" />
<Compile Include="CoreThings\CallContextTests.cs" />
<Compile Include="Components\ComponentTests.cs" />
<Compile Include="CoreThings\EnumExtensionsTests.cs" />
@@ -145,6 +146,7 @@
<Compile Include="PublishedContent\SolidPublishedSnapshot.cs" />
<Compile Include="PublishedContent\NuCacheTests.cs" />
<Compile Include="Routing\MediaUrlProviderTests.cs" />
<Compile Include="Routing\RoutableDocumentFilterTests.cs" />
<Compile Include="Runtimes\StandaloneTests.cs" />
<Compile Include="Routing\GetContentUrlsTests.cs" />
<Compile Include="Services\AmbiguousEventTests.cs" />
@@ -455,7 +457,6 @@
<Compile Include="Cache\DistributedCache\DistributedCacheTests.cs" />
<Compile Include="TestHelpers\TestWithDatabaseBase.cs" />
<Compile Include="TestHelpers\SettingsForTests.cs" />
<Compile Include="Configurations\GlobalSettingsTests.cs" />
<Compile Include="Routing\ContentFinderByAliasTests.cs" />
<Compile Include="Routing\ContentFinderByIdTests.cs" />
<Compile Include="Routing\ContentFinderByPageIdQueryTests.cs" />

View File

@@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.Repositories.Implement;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
using Umbraco.Web.PublishedCache;
@@ -86,11 +84,8 @@ namespace Umbraco.Web.Cache
// don't try to be clever - refresh all
MemberCacheRefresher.RefreshMemberTypes(AppCaches);
// we have to refresh models before we notify the published snapshot
// service of changes, else factories may try to rebuild models while
// we are using the database to load content into caches
_publishedModelFactory.WithSafeLiveFactory(() =>
// refresh the models and cache
_publishedModelFactory.WithSafeLiveFactoryReset(() =>
_publishedSnapshotService.Notify(payloads));
// now we can trigger the event

View File

@@ -62,11 +62,9 @@ namespace Umbraco.Web.Cache
TagsValueConverter.ClearCaches();
SliderValueConverter.ClearCaches();
// we have to refresh models before we notify the published snapshot
// service of changes, else factories may try to rebuild models while
// we are using the database to load content into caches
// refresh the models and cache
_publishedModelFactory.WithSafeLiveFactory(() =>
_publishedModelFactory.WithSafeLiveFactoryReset(() =>
_publishedSnapshotService.Notify(payloads));
base.Refresh(payloads);

View File

@@ -1,12 +1,8 @@
using System;
using System.Threading;
using Umbraco.Core;
using Umbraco.Core.Compose;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Sync;

View File

@@ -17,9 +17,11 @@ using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Packaging;
using Umbraco.Core.Persistence;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Web.Models;
using Umbraco.Web.Models.ContentEditing;
@@ -46,6 +48,7 @@ namespace Umbraco.Web.Editors
{
private readonly IEntityXmlSerializer _serializer;
private readonly PropertyEditorCollection _propertyEditors;
private readonly IScopeProvider _scopeProvider;
public ContentTypeController(IEntityXmlSerializer serializer,
ICultureDictionaryFactory cultureDictionaryFactory,
@@ -53,11 +56,13 @@ namespace Umbraco.Web.Editors
IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext, PropertyEditorCollection propertyEditors,
ServiceContext services, AppCaches appCaches,
IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper,
IScopeProvider scopeProvider)
: base(cultureDictionaryFactory, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
{
_serializer = serializer;
_propertyEditors = propertyEditors;
_scopeProvider = scopeProvider;
}
public int GetCount()
@@ -520,7 +525,7 @@ namespace Umbraco.Web.Editors
}
var dataInstaller = new PackageDataInstallation(Logger, Services.FileService, Services.MacroService, Services.LocalizationService,
Services.DataTypeService, Services.EntityService, Services.ContentTypeService, Services.ContentService, _propertyEditors);
Services.DataTypeService, Services.EntityService, Services.ContentTypeService, Services.ContentService, _propertyEditors, _scopeProvider);
var xd = new XmlDocument {XmlResolver = null};
xd.Load(filePath);

View File

@@ -135,13 +135,16 @@ namespace Umbraco.Web.Mvc
msg.Append(modelType.FullName);
msg.Append(".");
// raise event, to give model factories a chance at reporting
// raise event, to give model factories a chance at reporting
// the error with more details, and optionally request that
// the application restarts.
var args = new ModelBindingArgs(sourceType, modelType, msg);
ModelBindingException?.Invoke(Instance, args);
// TODO: with all of the tests I've done i don't think restarting the app here is required anymore,
// when I don't have this code enabled and i get a model binding error and just refresh, it fixes itself.
// We'll leave this for now though.
if (args.Restart)
{
msg.Append(" The application is restarting now.");

View File

@@ -1,20 +1,29 @@
using System;
using System.Configuration;
using System.Net;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Web.Composing;
namespace Umbraco.Web.Mvc
{
/// <summary>
/// An exception filter checking if we get a <see cref="ModelBindingException" /> or <see cref="InvalidCastException" /> with the same model. in which case it returns a redirect to the same page after 1 sec.
/// An exception filter checking if we get a <see cref="ModelBindingException" /> or <see cref="InvalidCastException" /> with the same model.
/// In which case it returns a redirect to the same page after 1 sec if not in debug mode.
/// </summary>
/// <remarks>
/// This is only enabled when running PureLive
/// </remarks>
internal class ModelBindingExceptionFilter : FilterAttribute, IExceptionFilter
{
private static readonly Regex GetPublishedModelsTypesRegex = new Regex("Umbraco.Web.PublishedModels.(\\w+)", RegexOptions.Compiled);
public void OnException(ExceptionContext filterContext)
{
if (!filterContext.ExceptionHandled
if (Current.PublishedModelFactory.IsLiveFactory()
&& ConfigurationManager.AppSettings["Umbraco.Web.DisableModelBindingExceptionFilter"] != "true"
&& !filterContext.ExceptionHandled
&& ((filterContext.Exception is ModelBindingException || filterContext.Exception is InvalidCastException)
&& IsMessageAboutTheSameModelType(filterContext.Exception.Message)))
{

View File

@@ -75,17 +75,11 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (draftData == null && publishedData == null)
throw new ArgumentException("Both draftData and publishedData cannot be null at the same time.");
if (draftData != null)
{
DraftContent = new PublishedContent(this, draftData, publishedSnapshotAccessor, variationContextAccessor);
DraftModel = DraftContent.CreateModel();
}
_publishedSnapshotAccessor = publishedSnapshotAccessor;
_variationContextAccessor = variationContextAccessor;
if (publishedData != null)
{
PublishedContent = new PublishedContent(this, publishedData, publishedSnapshotAccessor, variationContextAccessor);
PublishedModel = PublishedContent.CreateModel();
}
_draftData = draftData;
_publishedData = publishedData;
}
// clone
@@ -105,14 +99,10 @@ namespace Umbraco.Web.PublishedCache.NuCache
CreateDate = origin.CreateDate;
CreatorId = origin.CreatorId;
var originDraft = origin.DraftContent;
var originPublished = origin.PublishedContent;
DraftContent = originDraft == null ? null : new PublishedContent(this, originDraft);
PublishedContent = originPublished == null ? null : new PublishedContent(this, originPublished);
DraftModel = DraftContent?.CreateModel();
PublishedModel = PublishedContent?.CreateModel();
_draftData = origin._draftData;
_publishedData = origin._publishedData;
_publishedSnapshotAccessor = origin._publishedSnapshotAccessor;
_variationContextAccessor = origin._variationContextAccessor;
}
// everything that is common to both draft and published versions
@@ -131,15 +121,41 @@ namespace Umbraco.Web.PublishedCache.NuCache
public readonly DateTime CreateDate;
public readonly int CreatorId;
// draft and published version (either can be null, but not both)
// are the direct PublishedContent instances
public PublishedContent DraftContent;
public PublishedContent PublishedContent;
private ContentData _draftData;
private ContentData _publishedData;
private IVariationContextAccessor _variationContextAccessor;
private IPublishedSnapshotAccessor _publishedSnapshotAccessor;
public bool HasPublished => _publishedData != null;
public bool HasPublishedCulture(string culture) => _publishedData != null && _publishedData.CultureInfos.ContainsKey(culture);
// draft and published version (either can be null, but not both)
// are models not direct PublishedContent instances
public IPublishedContent DraftModel;
public IPublishedContent PublishedModel;
private IPublishedContent _draftModel;
private IPublishedContent _publishedModel;
private IPublishedContent GetModel(ref IPublishedContent model, ContentData contentData)
{
if (model != null) return model;
if (contentData == null) return null;
// create the model - we want to be fast, so no lock here: we may create
// more than 1 instance, but the lock below ensures we only ever return
// 1 unique instance - and locking is a nice explicit way to ensure this
var m = new PublishedContent(this, contentData, _publishedSnapshotAccessor, _variationContextAccessor).CreateModel();
// locking 'this' is not a best-practice but ContentNode is internal and
// we know what we do, so it is fine here and avoids allocating an object
lock (this)
{
return model = model ?? m;
}
}
public IPublishedContent DraftModel => GetModel(ref _draftModel, _draftData);
public IPublishedContent PublishedModel => GetModel(ref _publishedModel, _publishedData);
public ContentNodeKit ToKit()
=> new ContentNodeKit
@@ -147,8 +163,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
Node = this,
ContentTypeId = ContentType.Id,
DraftData = DraftContent?.ContentData,
PublishedData = PublishedContent?.ContentData
DraftData = _draftData,
PublishedData = _publishedData
};
}
}

View File

@@ -470,7 +470,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
private bool BuildKit(ContentNodeKit kit, out LinkedNode<ContentNode> parent)
{
// make sure parent exists
parent = GetParentLink(kit.Node);
parent = GetParentLink(kit.Node, null);
if (parent == null)
{
_logger.Warn<ContentStore>($"Skip item id={kit.Node.Id}, could not find parent id={kit.Node.ParentContentId}.");
@@ -506,6 +506,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
public int Count => _contentNodes.Count;
/// <summary>
/// Get the most recent version of the LinkedNode stored in the dictionary for the supplied key
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
/// <param name="dict"></param>
/// <param name="key"></param>
/// <returns></returns>
private static LinkedNode<TValue> GetHead<TKey, TValue>(ConcurrentDictionary<TKey, LinkedNode<TValue>> dict, TKey key)
where TValue : class
{
@@ -795,7 +803,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
var id = content.FirstChildContentId;
while (id > 0)
{
var link = GetRequiredLinkedNode(id, "child");
var link = GetRequiredLinkedNode(id, "child", null);
ClearBranchLocked(link.Value);
id = link.Value.NextSiblingContentId;
}
@@ -806,11 +814,16 @@ namespace Umbraco.Web.PublishedCache.NuCache
/// </summary>
/// <param name="id"></param>
/// <param name="description"></param>
/// <param name="gen">the generation requested, null for the latest stored</param>
/// <returns></returns>
private LinkedNode<ContentNode> GetRequiredLinkedNode(int id, string description)
private LinkedNode<ContentNode> GetRequiredLinkedNode(int id, string description, long? gen)
{
if (_contentNodes.TryGetValue(id, out var link) && link.Value != null)
return link;
if (_contentNodes.TryGetValue(id, out var link))
{
link = GetLinkedNodeGen(link, gen);
if (link != null && link.Value != null)
return link;
}
throw new PanicException($"failed to get {description} with id={id}");
}
@@ -818,11 +831,18 @@ namespace Umbraco.Web.PublishedCache.NuCache
/// <summary>
/// Gets the parent link node, may be null or root if ParentContentId is less than 0
/// </summary>
private LinkedNode<ContentNode> GetParentLink(ContentNode content)
/// <param name="gen">the generation requested, null for the latest stored</param>
private LinkedNode<ContentNode> GetParentLink(ContentNode content, long? gen)
{
if (content.ParentContentId < 0) return _root;
if (content.ParentContentId < 0)
{
var root = GetLinkedNodeGen(_root, gen);
return root;
}
_contentNodes.TryGetValue(content.ParentContentId, out var link);
if (_contentNodes.TryGetValue(content.ParentContentId, out var link))
link = GetLinkedNodeGen(link, gen);
return link;
}
@@ -830,17 +850,37 @@ namespace Umbraco.Web.PublishedCache.NuCache
/// Gets the linked parent node and if it doesn't exist throw a <see cref="PanicException"/>
/// </summary>
/// <param name="content"></param>
/// <param name="gen">the generation requested, null for the latest stored</param>
/// <returns></returns>
private LinkedNode<ContentNode> GetRequiredParentLink(ContentNode content)
private LinkedNode<ContentNode> GetRequiredParentLink(ContentNode content, long? gen)
{
return content.ParentContentId < 0 ? _root : GetRequiredLinkedNode(content.ParentContentId, "parent");
return content.ParentContentId < 0 ? _root : GetRequiredLinkedNode(content.ParentContentId, "parent", gen);
}
/// <summary>
/// Iterates over the LinkedNode's generations to find the correct one
/// </summary>
/// <param name="link"></param>
/// <param name="gen">The generation requested, use null to avoid the lookup</param>
/// <returns></returns>
private LinkedNode<TValue> GetLinkedNodeGen<TValue>(LinkedNode<TValue> link, long? gen)
where TValue : class
{
if (!gen.HasValue) return link;
//find the correct snapshot, find the first that is <= the requested gen
while (link != null && link.Gen > gen)
{
link = link.Next;
}
return link;
}
private void RemoveTreeNodeLocked(ContentNode content)
{
var parentLink = content.ParentContentId < 0
? _root
: GetRequiredLinkedNode(content.ParentContentId, "parent");
: GetRequiredLinkedNode(content.ParentContentId, "parent", null);
var parent = parentLink.Value;
@@ -863,14 +903,14 @@ namespace Umbraco.Web.PublishedCache.NuCache
if (content.NextSiblingContentId > 0)
{
var nextLink = GetRequiredLinkedNode(content.NextSiblingContentId, "next sibling");
var nextLink = GetRequiredLinkedNode(content.NextSiblingContentId, "next sibling", null);
var next = GenCloneLocked(nextLink);
next.PreviousSiblingContentId = content.PreviousSiblingContentId;
}
if (content.PreviousSiblingContentId > 0)
{
var prevLink = GetRequiredLinkedNode(content.PreviousSiblingContentId, "previous sibling");
var prevLink = GetRequiredLinkedNode(content.PreviousSiblingContentId, "previous sibling", null);
var prev = GenCloneLocked(prevLink);
prev.NextSiblingContentId = content.NextSiblingContentId;
}
@@ -883,9 +923,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
if (kit.Node.ParentContentId < 0)
return true;
var link = GetParentLink(kit.Node);
var link = GetParentLink(kit.Node, null);
var node = link?.Value;
return node?.PublishedModel != null;
return node != null && node.HasPublished;
}
private ContentNode GenCloneLocked(LinkedNode<ContentNode> link)
@@ -909,10 +949,12 @@ namespace Umbraco.Web.PublishedCache.NuCache
/// </summary>
private void AddTreeNodeLocked(ContentNode content, LinkedNode<ContentNode> parentLink = null)
{
parentLink = parentLink ?? GetRequiredParentLink(content);
parentLink = parentLink ?? GetRequiredParentLink(content, null);
var parent = parentLink.Value;
var currentGen = parentLink.Gen;
// if parent has no children, clone parent + add as first child
if (parent.FirstChildContentId < 0)
{
@@ -923,7 +965,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
// get parent's first child
var childLink = GetRequiredLinkedNode(parent.FirstChildContentId, "first child");
var childLink = GetRequiredLinkedNode(parent.FirstChildContentId, "first child", currentGen);
var child = childLink.Value;
// if first, clone parent + insert as first child
@@ -943,7 +985,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
}
// get parent's last child
var lastChildLink = GetRequiredLinkedNode(parent.LastChildContentId, "last child");
var lastChildLink = GetRequiredLinkedNode(parent.LastChildContentId, "last child", currentGen);
var lastChild = lastChildLink.Value;
// if last, clone parent + append as last child
@@ -968,7 +1010,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
while (child.NextSiblingContentId > 0)
{
// get next child
var nextChildLink = GetRequiredLinkedNode(child.NextSiblingContentId, "next child");
var nextChildLink = GetRequiredLinkedNode(child.NextSiblingContentId, "next child", currentGen);
var nextChild = nextChildLink.Value;
// if here, clone previous + append/insert
@@ -1072,21 +1114,15 @@ namespace Umbraco.Web.PublishedCache.NuCache
public IEnumerable<ContentNode> GetAtRoot(long gen)
{
var z = _root;
while (z != null)
{
if (z.Gen <= gen)
break;
z = z.Next;
}
if (z == null)
var root = GetLinkedNodeGen(_root, gen);
if (root == null)
yield break;
var id = z.Value.FirstChildContentId;
var id = root.Value.FirstChildContentId;
while (id > 0)
{
var link = GetRequiredLinkedNode(id, "sibling");
var link = GetRequiredLinkedNode(id, "root", gen);
yield return link.Value;
id = link.Value.NextSiblingContentId;
}
@@ -1097,13 +1133,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
// look ma, no lock!
var link = GetHead(dict, key);
while (link != null)
{
if (link.Gen <= gen)
return link.Value; // may be null
link = link.Next;
}
return null;
link = GetLinkedNodeGen(link, gen);
return link?.Value; // may be null
}
public IEnumerable<ContentNode> GetAll(long gen)
@@ -1113,17 +1144,9 @@ namespace Umbraco.Web.PublishedCache.NuCache
var links = _contentNodes.Values.ToArray();
foreach (var l in links)
{
var link = l;
while (link != null)
{
if (link.Gen <= gen)
{
if (link.Value != null)
yield return link.Value;
break;
}
link = link.Next;
}
var link = GetLinkedNodeGen(l, gen);
if (link?.Value != null)
yield return link.Value;
}
}
@@ -1131,14 +1154,8 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
var has = _contentNodes.Any(x =>
{
var link = x.Value;
while (link != null)
{
if (link.Gen <= gen && link.Value != null)
return true;
link = link.Next;
}
return false;
var link = GetLinkedNodeGen(x.Value, gen);
return link?.Value != null;
});
return has == false;
}

View File

@@ -233,8 +233,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// invariant content items)
// if there is no 'published' published content, no culture can be published
var hasPublished = _contentNode.PublishedContent != null;
if (!hasPublished)
if (!_contentNode.HasPublished)
return false;
// if there is a 'published' published content, and does not vary = published
@@ -247,7 +246,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
// there is a 'published' published content, and varies
// = depends on the culture
return _contentNode.PublishedContent.ContentData.CultureInfos.ContainsKey(culture);
return _contentNode.HasPublishedCulture(culture);
}
#endregion

View File

@@ -160,7 +160,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
_domainStore = new SnapDictionary<int, Domain>();
publishedModelFactory.WithSafeLiveFactory(LoadCachesOnStartup);
LoadCachesOnStartup();
Guid GetUid(ContentStore store, int id) => store.LiveSnapshot.Get(id)?.Uid ?? default;
int GetId(ContentStore store, Guid uid) => store.LiveSnapshot.Get(uid)?.Id ?? default;

View File

@@ -0,0 +1,207 @@
using System;
using System.IO;
using System.Web;
using System.Web.Routing;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Configuration;
using System.Threading;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.IO;
using System.Collections.Concurrent;
using Umbraco.Core.Collections;
namespace Umbraco.Web
{
/// <summary>
/// Utility class used to check if the current request is for a front-end request
/// </summary>
/// <remarks>
/// There are various checks to determine if this is a front-end request such as checking if the request is part of any reserved paths or existing MVC routes.
/// </remarks>
public sealed class RoutableDocumentFilter
{
public RoutableDocumentFilter(IGlobalSettings globalSettings)
{
_globalSettings = globalSettings;
}
private static readonly ConcurrentDictionary<string, bool> RouteChecks = new ConcurrentDictionary<string, bool>();
private readonly IGlobalSettings _globalSettings;
private object _locker = new object();
private bool _isInit = false;
private int? _routeCount;
private HashSet<string> _reservedList;
/// <summary>
/// Checks if the request is a document request (i.e. one that the module should handle)
/// </summary>
/// <param name="httpContext"></param>
/// <param name="uri"></param>
/// <returns></returns>
public bool IsDocumentRequest(HttpContextBase httpContext, Uri uri)
{
var maybeDoc = true;
var lpath = uri.AbsolutePath.ToLowerInvariant();
// handle directory-urls used for asmx
// TODO: legacy - what's the point really?
var asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase);
if (asmxPos >= 0)
{
// use uri.AbsolutePath, not path, 'cos path has been lowercased
httpContext.RewritePath(uri.AbsolutePath.Substring(0, asmxPos + 5), // filePath
uri.AbsolutePath.Substring(asmxPos + 5), // pathInfo
uri.Query.TrimStart('?'));
maybeDoc = false;
}
// a document request should be
// /foo/bar/nil
// /foo/bar/nil/
// /foo/bar/nil.aspx
// where /foo is not a reserved path
// if the path contains an extension that is not .aspx
// then it cannot be a document request
var extension = Path.GetExtension(lpath);
if (maybeDoc && extension.IsNullOrWhiteSpace() == false && extension != ".aspx")
maybeDoc = false;
// at that point, either we have no extension, or it is .aspx
// if the path is reserved then it cannot be a document request
if (maybeDoc && IsReservedPathOrUrl(lpath, httpContext, RouteTable.Routes))
maybeDoc = false;
//NOTE: No need to warn, plus if we do we should log the document, as this message doesn't really tell us anything :)
//if (!maybeDoc)
//{
// Logger.Warn<UmbracoModule>("Not a document");
//}
return maybeDoc;
}
/// <summary>
/// Determines whether the specified URL is reserved or is inside a reserved path.
/// </summary>
/// <param name="url">The URL to check.</param>
/// <returns>
/// <c>true</c> if the specified URL is reserved; otherwise, <c>false</c>.
/// </returns>
internal bool IsReservedPathOrUrl(string url)
{
LazyInitializer.EnsureInitialized(ref _reservedList, ref _isInit, ref _locker, () =>
{
// store references to strings to determine changes
var reservedPathsCache = _globalSettings.ReservedPaths;
var reservedUrlsCache = _globalSettings.ReservedUrls;
// add URLs and paths to a new list
var newReservedList = new HashSet<string>();
foreach (var reservedUrlTrimmed in reservedUrlsCache
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim().ToLowerInvariant())
.Where(x => x.IsNullOrWhiteSpace() == false)
.Select(reservedUrl => IOHelper.ResolveUrl(reservedUrl).Trim().EnsureStartsWith("/"))
.Where(reservedUrlTrimmed => reservedUrlTrimmed.IsNullOrWhiteSpace() == false))
{
newReservedList.Add(reservedUrlTrimmed);
}
foreach (var reservedPathTrimmed in NormalizePaths(reservedPathsCache.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)))
{
newReservedList.Add(reservedPathTrimmed);
}
foreach (var reservedPathTrimmed in NormalizePaths(ReservedPaths))
{
newReservedList.Add(reservedPathTrimmed);
}
// use the new list from now on
return newReservedList;
});
//The url should be cleaned up before checking:
// * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/'
// * We shouldn't be comparing the query at all
var pathPart = url.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant();
if (pathPart.Contains(".") == false)
{
pathPart = pathPart.EnsureEndsWith('/');
}
// return true if url starts with an element of the reserved list
return _reservedList.Any(x => pathPart.InvariantStartsWith(x));
}
private IEnumerable<string> NormalizePaths(IEnumerable<string> paths)
{
return paths
.Select(x => x.Trim().ToLowerInvariant())
.Where(x => x.IsNullOrWhiteSpace() == false)
.Select(reservedPath => IOHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/"))
.Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false);
}
/// <summary>
/// Determines whether the current request is reserved based on the route table and
/// whether the specified URL is reserved or is inside a reserved path.
/// </summary>
/// <param name="url"></param>
/// <param name="httpContext"></param>
/// <param name="routes">The route collection to lookup the request in</param>
/// <returns></returns>
internal bool IsReservedPathOrUrl(string url, HttpContextBase httpContext, RouteCollection routes)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
if (routes == null) throw new ArgumentNullException(nameof(routes));
//This is some rudimentary code to check if the route table has changed at runtime, we're basically just keeping a count
//of the routes. This isn't fail safe but there's no way to monitor changes to the route table. Else we need to create a hash
//of all routes and then recompare but that will be annoying to do on each request and then we might as well just do the whole MVC
//route on each request like we were doing before instead of caching the result of GetRouteData.
var changed = false;
using (routes.GetReadLock())
{
if (!_routeCount.HasValue || _routeCount.Value != routes.Count)
{
//the counts are not set or have changed, need to reset
changed = true;
}
}
if (changed)
{
using (routes.GetWriteLock())
{
_routeCount = routes.Count;
//try clearing each entry
foreach(var r in RouteChecks.Keys.ToList())
RouteChecks.TryRemove(r, out _);
}
}
var absPath = httpContext?.Request?.Url.AbsolutePath;
if (absPath.IsNullOrWhiteSpace())
return false;
//check if the current request matches a route, if so then it is reserved.
var hasRoute = RouteChecks.GetOrAdd(absPath, x => routes.GetRouteData(httpContext) != null);
if (hasRoute)
return true;
//continue with the standard ignore routine
return IsReservedPathOrUrl(url);
}
/// <summary>
/// This is used internally to track any registered callback paths for Identity providers. If the request path matches
/// any of the registered paths, then the module will let the request keep executing
/// </summary>
internal static readonly ConcurrentHashSet<string> ReservedPaths = new ConcurrentHashSet<string>();
}
}

View File

@@ -73,7 +73,7 @@ namespace Umbraco.Web.Runtime
// register accessors for cultures
composition.RegisterUnique<IDefaultCultureAccessor, DefaultCultureAccessor>();
composition.RegisterUnique<IVariationContextAccessor, HybridVariationContextAccessor>();
// register the http context and umbraco context accessors
// we *should* use the HttpContextUmbracoContextAccessor, however there are cases when
// we have no http context, eg when booting Umbraco or in background threads, so instead
@@ -125,6 +125,8 @@ namespace Umbraco.Web.Runtime
// register distributed cache
composition.RegisterUnique(f => new DistributedCache());
composition.RegisterUnique<RoutableDocumentFilter>();
// replace some services
composition.RegisterUnique<IEventMessagesFactory, DefaultEventMessagesFactory>();
composition.RegisterUnique<IEventMessagesAccessor, HybridEventMessagesAccessor>();

View File

@@ -101,7 +101,7 @@ namespace Umbraco.Web.Security
var path = (PathString) prop.GetValue(options);
if (path.HasValue)
{
UmbracoModule.ReservedPaths.TryAdd(path.ToString());
RoutableDocumentFilter.ReservedPaths.TryAdd(path.ToString());
}
}
}
@@ -112,7 +112,7 @@ namespace Umbraco.Web.Security
}
else
{
UmbracoModule.ReservedPaths.TryAdd(callbackPath);
RoutableDocumentFilter.ReservedPaths.TryAdd(callbackPath);
}
}
}

View File

@@ -230,6 +230,7 @@
<Compile Include="PublishedCache\NuCache\Snap\GenObj.cs" />
<Compile Include="PublishedCache\NuCache\Snap\GenRef.cs" />
<Compile Include="PublishedCache\NuCache\Snap\LinkedNode.cs" />
<Compile Include="RoutableDocumentFilter.cs" />
<Compile Include="Routing\DefaultMediaUrlProvider.cs" />
<Compile Include="Routing\IMediaUrlProvider.cs" />
<Compile Include="Routing\IPublishedRouter.cs" />

View File

@@ -1,16 +1,21 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Routing;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Events;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Web;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
namespace Umbraco.Web
{
/// <summary>
/// Class that encapsulates Umbraco information of a specific HTTP request
/// </summary>
@@ -285,7 +290,7 @@ namespace Umbraco.Web
_previewing = _previewToken.IsNullOrWhiteSpace() == false;
}
// say we render a macro or RTE in a give 'preview' mode that might not be the 'current' one,
// then due to the way it all works at the moment, the 'current' published snapshot need to be in the proper
// default 'preview' mode - somehow we have to force it. and that could be recursive.

View File

@@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Web;
using System.Web.Routing;
using Umbraco.Core;
@@ -10,14 +8,9 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Web.Routing;
using Umbraco.Web.Security;
using Umbraco.Core.Exceptions;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Persistence.FaultHandling;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Umbraco.Web.PublishedCache;
namespace Umbraco.Web
{
@@ -39,40 +32,26 @@ namespace Umbraco.Web
public class UmbracoInjectedModule : IHttpModule
{
private readonly IGlobalSettings _globalSettings;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IUserService _userService;
private readonly UrlProviderCollection _urlProviders;
private readonly IRuntimeState _runtime;
private readonly ILogger _logger;
private readonly IPublishedRouter _publishedRouter;
private readonly IVariationContextAccessor _variationContextAccessor;
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly RoutableDocumentFilter _routableDocumentLookup;
public UmbracoInjectedModule(
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedSnapshotService publishedSnapshotService,
IUserService userService,
UrlProviderCollection urlProviders,
IRuntimeState runtime,
ILogger logger,
IPublishedRouter publishedRouter,
IVariationContextAccessor variationContextAccessor,
IUmbracoContextFactory umbracoContextFactory)
IUmbracoContextFactory umbracoContextFactory,
RoutableDocumentFilter routableDocumentLookup)
{
_combinedRouteCollection = new Lazy<RouteCollection>(CreateRouteCollection);
_globalSettings = globalSettings;
_umbracoContextAccessor = umbracoContextAccessor;
_publishedSnapshotService = publishedSnapshotService;
_userService = userService;
_urlProviders = urlProviders;
_runtime = runtime;
_logger = logger;
_publishedRouter = publishedRouter;
_variationContextAccessor = variationContextAccessor;
_umbracoContextFactory = umbracoContextFactory;
_routableDocumentLookup = routableDocumentLookup;
}
#region HttpModule event handlers
@@ -182,18 +161,18 @@ namespace Umbraco.Web
var reason = EnsureRoutableOutcome.IsRoutable;
// ensure this is a document request
if (EnsureDocumentRequest(httpContext, uri) == false)
if (!_routableDocumentLookup.IsDocumentRequest(httpContext, context.OriginalRequestUrl))
{
reason = EnsureRoutableOutcome.NotDocumentRequest;
}
// ensure the runtime is in the proper state
// and deal with needed redirects, etc
else if (EnsureRuntime(httpContext, uri) == false)
else if (!EnsureRuntime(httpContext, uri))
{
reason = EnsureRoutableOutcome.NotReady;
}
// ensure Umbraco has documents to serve
else if (EnsureHasContent(context, httpContext) == false)
else if (!EnsureHasContent(context, httpContext))
{
reason = EnsureRoutableOutcome.NoContent;
}
@@ -201,55 +180,7 @@ namespace Umbraco.Web
return Attempt.If(reason == EnsureRoutableOutcome.IsRoutable, reason);
}
/// <summary>
/// Ensures that the request is a document request (i.e. one that the module should handle)
/// </summary>
/// <param name="httpContext"></param>
/// <param name="uri"></param>
/// <returns></returns>
private bool EnsureDocumentRequest(HttpContextBase httpContext, Uri uri)
{
var maybeDoc = true;
var lpath = uri.AbsolutePath.ToLowerInvariant();
// handle directory-urls used for asmx
// TODO: legacy - what's the point really?
var asmxPos = lpath.IndexOf(".asmx/", StringComparison.OrdinalIgnoreCase);
if (asmxPos >= 0)
{
// use uri.AbsolutePath, not path, 'cos path has been lowercased
httpContext.RewritePath(uri.AbsolutePath.Substring(0, asmxPos + 5), // filePath
uri.AbsolutePath.Substring(asmxPos + 5), // pathInfo
uri.Query.TrimStart('?'));
maybeDoc = false;
}
// a document request should be
// /foo/bar/nil
// /foo/bar/nil/
// /foo/bar/nil.aspx
// where /foo is not a reserved path
// if the path contains an extension that is not .aspx
// then it cannot be a document request
var extension = Path.GetExtension(lpath);
if (maybeDoc && extension.IsNullOrWhiteSpace() == false && extension != ".aspx")
maybeDoc = false;
// at that point, either we have no extension, or it is .aspx
// if the path is reserved then it cannot be a document request
if (maybeDoc && _globalSettings.IsReservedPathOrUrl(lpath, httpContext, _combinedRouteCollection.Value))
maybeDoc = false;
//NOTE: No need to warn, plus if we do we should log the document, as this message doesn't really tell us anything :)
//if (!maybeDoc)
//{
// Logger.Warn<UmbracoModule>("Not a document");
//}
return maybeDoc;
}
private bool EnsureRuntime(HttpContextBase httpContext, Uri uri)
{
@@ -505,36 +436,6 @@ namespace Umbraco.Web
#endregion
/// <summary>
/// This is used to be passed into the GlobalSettings.IsReservedPathOrUrl and will include some 'fake' routes
/// used to determine if a path is reserved.
/// </summary>
/// <remarks>
/// This is basically used to reserve paths dynamically
/// </remarks>
private readonly Lazy<RouteCollection> _combinedRouteCollection;
private RouteCollection CreateRouteCollection()
{
var routes = new RouteCollection();
foreach (var route in RouteTable.Routes)
routes.Add(route);
foreach (var reservedPath in UmbracoModule.ReservedPaths)
{
try
{
routes.Add("_umbreserved_" + reservedPath.ReplaceNonAlphanumericChars(""),
new Route(reservedPath.TrimStart('/'), new StopRoutingHandler()));
}
catch (Exception ex)
{
_logger.Error<UmbracoModule>("Could not add reserved path route", ex);
}
}
return routes;
}
}
}

View File

@@ -103,10 +103,5 @@ namespace Umbraco.Web
return end;
}
/// <summary>
/// This is used internally to track any registered callback paths for Identity providers. If the request path matches
/// any of the registered paths, then the module will let the request keep executing
/// </summary>
internal static readonly ConcurrentHashSet<string> ReservedPaths = new ConcurrentHashSet<string>();
}
}

View File

@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2005
# Visual Studio Version 16
VisualStudioVersion = 16.0.29209.152
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web.UI", "Umbraco.Web.UI\Umbraco.Web.UI.csproj", "{4C4C194C-B5E4-4991-8F87-4373E24CC19F}"
EndProject
@@ -123,6 +123,7 @@ Global
{31785BC3-256C-4613-B2F5-A1B0BDDED8C1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{31785BC3-256C-4613-B2F5-A1B0BDDED8C1}.Release|Any CPU.Build.0 = Release|Any CPU
{5D3B8245-ADA6-453F-A008-50ED04BFE770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5D3B8245-ADA6-453F-A008-50ED04BFE770}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5D3B8245-ADA6-453F-A008-50ED04BFE770}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5D3B8245-ADA6-453F-A008-50ED04BFE770}.Release|Any CPU.Build.0 = Release|Any CPU
{07FBC26B-2927-4A22-8D96-D644C667FECC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -130,6 +131,7 @@ Global
{07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{07FBC26B-2927-4A22-8D96-D644C667FECC}.Release|Any CPU.Build.0 = Release|Any CPU
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection