diff --git a/src/Umbraco.Infrastructure/Intall/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Infrastructure/Intall/InstallSteps/SetUmbracoVersionStep.cs index 17043f18a5..4e4e590284 100644 --- a/src/Umbraco.Infrastructure/Intall/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Infrastructure/Intall/InstallSteps/SetUmbracoVersionStep.cs @@ -25,30 +25,30 @@ namespace Umbraco.Web.Install.InstallSteps public override Task ExecuteAsync(object model) { - var security = _umbracoContextAccessor.GetRequiredUmbracoContext().Security; - if (security.IsAuthenticated() == false && _globalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) - { - security.PerformLogin(-1); - } - - if (security.IsAuthenticated()) - { - // when a user is already logged in, we need to check whether it's user 'zero' - // which is the legacy super user from v7 - and then we need to actually log the - // true super user in - but before that we need to log off, else audit events - // will try to reference user zero and fail - var userIdAttempt = security.GetUserId(); - if (userIdAttempt && userIdAttempt.Result == 0) - { - security.ClearCurrentLogin(); - security.PerformLogin(Constants.Security.SuperUserId); - } - } - else if (_globalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) - { - // for installs, we need to log the super user in - security.PerformLogin(Constants.Security.SuperUserId); - } + // var security = _umbracoContextAccessor.GetRequiredUmbracoContext().Security; + // if (security.IsAuthenticated() == false && _globalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) + // { + // security.PerformLogin(-1); + // } + // + // if (security.IsAuthenticated()) + // { + // // when a user is already logged in, we need to check whether it's user 'zero' + // // which is the legacy super user from v7 - and then we need to actually log the + // // true super user in - but before that we need to log off, else audit events + // // will try to reference user zero and fail + // var userIdAttempt = security.GetUserId(); + // if (userIdAttempt && userIdAttempt.Result == 0) + // { + // security.ClearCurrentLogin(); + // security.PerformLogin(Constants.Security.SuperUserId); + // } + // } + // else if (_globalSettings.ConfigurationStatus.IsNullOrWhiteSpace()) + // { + // // for installs, we need to log the super user in + // security.PerformLogin(Constants.Security.SuperUserId); + // } // Update configurationStatus _globalSettings.ConfigurationStatus = _umbracoVersion.SemanticVersion.ToSemanticString(); diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index 291a4a329d..8f5969801c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -155,7 +155,7 @@ namespace Umbraco.Core.Migrations.Install // this should probably be in a "using (new SqlCeEngine)" clause but not sure // of the side effects and it's been like this for quite some time now - _dbProviderFactoryCreator.CreateDatabase(); + _dbProviderFactoryCreator.CreateDatabase(Constants.DbProviderNames.SqlCe); } factory.Configure(EmbeddedDatabaseConnectionString, Constants.DbProviderNames.SqlCe); diff --git a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs index 5a3938d9cb..b41546668f 100644 --- a/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/BasicBulkSqlInsertProvider.cs @@ -8,6 +8,8 @@ namespace Umbraco.Core.Persistence /// public class BasicBulkSqlInsertProvider : IBulkSqlInsertProvider { + public string ProviderName => Constants.DatabaseProviders.SqlServer; + public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { var recordsA = records.ToArray(); diff --git a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs index 9d988e7dfe..8ec45d8403 100644 --- a/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/DbProviderFactoryCreator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Data.Common; +using System.Linq; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence @@ -9,21 +10,21 @@ namespace Umbraco.Core.Persistence { private readonly string _defaultProviderName; private readonly Func _getFactory; + private readonly IDictionary _embeddedDatabaseCreators; private readonly IDictionary _syntaxProviders; private readonly IDictionary _bulkSqlInsertProviders; - private readonly Action _createDatabaseAction; public DbProviderFactoryCreator(string defaultProviderName, Func getFactory, - IDictionary syntaxProviders, - IDictionary bulkSqlInsertProviders, - Action createDatabaseAction) + IEnumerable syntaxProviders, + IEnumerable bulkSqlInsertProviders, + IEnumerable embeddedDatabaseCreators) { _defaultProviderName = defaultProviderName; _getFactory = getFactory; - _syntaxProviders = syntaxProviders; - _bulkSqlInsertProviders = bulkSqlInsertProviders; - _createDatabaseAction = createDatabaseAction; + _embeddedDatabaseCreators = embeddedDatabaseCreators.ToDictionary(x=>x.ProviderName); + _syntaxProviders = syntaxProviders.ToDictionary(x=>x.ProviderName); + _bulkSqlInsertProviders = bulkSqlInsertProviders.ToDictionary(x=>x.ProviderName); } public DbProviderFactory CreateFactory() => CreateFactory(_defaultProviderName); @@ -57,9 +58,12 @@ namespace Umbraco.Core.Persistence return result; } - public void CreateDatabase() + public void CreateDatabase(string providerName) { - _createDatabaseAction(); + if(_embeddedDatabaseCreators.TryGetValue(providerName, out var creator)) + { + creator.Create(); + } } } } diff --git a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs index e41367288b..8630c4b4d3 100644 --- a/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/IBulkSqlInsertProvider.cs @@ -4,6 +4,7 @@ namespace Umbraco.Core.Persistence { public interface IBulkSqlInsertProvider { + string ProviderName { get; } int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs index 5806bb90ec..2474b1df40 100644 --- a/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/IDbProviderFactoryCreator.cs @@ -10,6 +10,6 @@ namespace Umbraco.Core.Persistence DbProviderFactory CreateFactory(string providerName); ISqlSyntaxProvider GetSqlSyntaxProvider(string providerName); IBulkSqlInsertProvider CreateBulkSqlInsertProvider(string providerName); - void CreateDatabase(); + void CreateDatabase(string providerName); } } diff --git a/src/Umbraco.Infrastructure/Persistence/IEmbeddedDatabaseCreator.cs b/src/Umbraco.Infrastructure/Persistence/IEmbeddedDatabaseCreator.cs new file mode 100644 index 0000000000..45a9617cb1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/IEmbeddedDatabaseCreator.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Persistence +{ + public interface IEmbeddedDatabaseCreator + { + string ProviderName { get; } + void Create(); + } + + public class NoopEmbeddedDatabaseCreator : IEmbeddedDatabaseCreator + { + public string ProviderName => Constants.DatabaseProviders.SqlServer; + + public void Create() + { + + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs index 4631aca889..e8126dd7f5 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerBulkSqlInsertProvider.cs @@ -8,12 +8,13 @@ using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence { - /// /// A bulk sql insert provider for Sql Server /// public class SqlServerBulkSqlInsertProvider : IBulkSqlInsertProvider { + public string ProviderName => Constants.DatabaseProviders.SqlServer; + public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { var recordsA = records.ToArray(); diff --git a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs index 9c2c6273c2..d81067f23c 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlServerDbProviderFactoryCreator.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.Persistence } } - public void CreateDatabase() + public void CreateDatabase(string providerName) { throw new NotSupportedException("Embedded databases are not supported"); } diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 58f9a4b68a..f37e22fc0a 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -14,6 +14,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// public interface ISqlSyntaxProvider { + string ProviderName { get; } + string EscapeString(string val); string GetWildcardPlaceholder(); diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 8d3a41e5bf..779e4362ae 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -16,6 +16,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { + public override string ProviderName => Constants.DatabaseProviders.SqlServer; + public ServerVersionInfo ServerVersion { get; private set; } public enum VersionName diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 3df97c0b4f..6095630161 100644 --- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -130,6 +130,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax DbTypeMap.Set(DbType.Binary, BlobColumnDefinition); } + public abstract string ProviderName { get; } + public virtual string EscapeString(string val) { return NPocoDatabaseExtensions.EscapeAtSymbols(val.Replace("'", "''")); diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index ebd91f52a2..c91ae530ce 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -141,6 +141,8 @@ namespace Umbraco.Core.Runtime NetworkHelper.MachineName); Logger.Debug("Runtime: {Runtime}", GetType().FullName); + AppDomain.CurrentDomain.SetData("DataDirectory", IOHelper.MapPath(Constants.SystemDirectories.Data)); + // application environment ConfigureUnhandledException(); _factory = Configure(register, timer); diff --git a/src/Umbraco.Persistance.SqlCe/SqlCeBulkSqlInsertProvider.cs b/src/Umbraco.Persistance.SqlCe/SqlCeBulkSqlInsertProvider.cs index e7c38ef5db..deb7f9d2c5 100644 --- a/src/Umbraco.Persistance.SqlCe/SqlCeBulkSqlInsertProvider.cs +++ b/src/Umbraco.Persistance.SqlCe/SqlCeBulkSqlInsertProvider.cs @@ -9,6 +9,8 @@ namespace Umbraco.Core.Persistence { public class SqlCeBulkSqlInsertProvider : IBulkSqlInsertProvider { + public string ProviderName => Constants.DatabaseProviders.SqlCe; + public int BulkInsertRecords(IUmbracoDatabase database, IEnumerable records) { var recordsA = records.ToArray(); diff --git a/src/Umbraco.Persistance.SqlCe/SqlCeEmbeddedDatabaseCreator.cs b/src/Umbraco.Persistance.SqlCe/SqlCeEmbeddedDatabaseCreator.cs new file mode 100644 index 0000000000..de6a7ff4d7 --- /dev/null +++ b/src/Umbraco.Persistance.SqlCe/SqlCeEmbeddedDatabaseCreator.cs @@ -0,0 +1,17 @@ +using Umbraco.Core; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Persistence; + +namespace Umbraco.Persistance.SqlCe +{ + public class SqlCeEmbeddedDatabaseCreator : IEmbeddedDatabaseCreator + { + public string ProviderName => Constants.DatabaseProviders.SqlCe; + + public void Create() + { + var engine = new System.Data.SqlServerCe.SqlCeEngine(DatabaseBuilder.EmbeddedDatabaseConnectionString); + engine.CreateDatabase(); + } + } +} diff --git a/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs b/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs index 7abcc60c86..081737d0db 100644 --- a/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Persistance.SqlCe/SqlCeSyntaxProvider.cs @@ -14,6 +14,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// public class SqlCeSyntaxProvider : MicrosoftSqlSyntaxProviderBase { + public override string ProviderName => Constants.DatabaseProviders.SqlCe; + public override Sql SelectTop(Sql sql, int top) { return new Sql(sql.SqlContext, sql.SQL.Insert(sql.SQL.IndexOf(' '), " TOP " + top), sql.Arguments); diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 67dc24e041..5100e2e21c 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -115,7 +115,7 @@ namespace Umbraco.Tests.Runtimes composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); - //composition.RegisterUnique(); // TODO new reference? + composition.RegisterUnique(); composition.RegisterUnique(_ => new MediaUrlProviderCollection(Enumerable.Empty())); // initialize some components only/individually diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs index bf6f537449..6f27f3e6bf 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoCoreServiceCollectionExtensions.cs @@ -138,6 +138,8 @@ namespace Umbraco.Web.Common.Extensions var typeFinder = CreateTypeFinder(logger, profiler, webHostEnvironment, entryAssembly, configs.TypeFinder()); RegisterDatabaseTypes(typeFinder); + var dbProviderFactoryCreator = serviceProvider.GetRequiredService(); + var coreRuntime = GetCoreRuntime( configs, umbracoVersion, @@ -147,7 +149,8 @@ namespace Umbraco.Web.Common.Extensions hostingEnvironment, backOfficeInfo, typeFinder, - requestCache); + requestCache, + dbProviderFactoryCreator); factory = coreRuntime.Configure(container); @@ -175,12 +178,8 @@ namespace Umbraco.Web.Common.Extensions private static IRuntime GetCoreRuntime( Configs configs, IUmbracoVersion umbracoVersion, IIOHelper ioHelper, Core.Logging.ILogger logger, IProfiler profiler, Core.Hosting.IHostingEnvironment hostingEnvironment, IBackOfficeInfo backOfficeInfo, - ITypeFinder typeFinder, IRequestCache requestCache) + ITypeFinder typeFinder, IRequestCache requestCache, IDbProviderFactoryCreator dbProviderFactoryCreator) { - var connectionStringConfig = configs.ConnectionStrings()[Core.Constants.System.UmbracoConnectionName]; - var dbProviderFactoryCreator = new SqlServerDbProviderFactoryCreator( - connectionStringConfig?.ProviderName, - DbProviderFactories.GetFactory); // Determine if we should use the sql main dom or the default var globalSettings = configs.Global(); diff --git a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs index ee420c1d12..461217627d 100644 --- a/src/Umbraco.Web.Common/Macros/MacroRenderer.cs +++ b/src/Umbraco.Web.Common/Macros/MacroRenderer.cs @@ -18,7 +18,7 @@ using Umbraco.Web.Common.Macros; namespace Umbraco.Web.Macros { - internal class MacroRenderer : IMacroRenderer + public class MacroRenderer : IMacroRenderer { private readonly IProfilingLogger _plogger; private readonly IUmbracoContextAccessor _umbracoContextAccessor; diff --git a/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs index b90c5b3907..47568d5a62 100644 --- a/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs +++ b/src/Umbraco.Web.Common/Macros/PartialViewMacroViewComponent.cs @@ -4,13 +4,14 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Mvc; namespace Umbraco.Web.Macros { /// /// Controller to render macro content for Partial View Macros /// - // [MergeParentContextViewData] // TODO is this important now it is a view Component + //[MergeParentContextViewData] // TODO is this requeired now it is a ViewComponent? [HideFromTypeFinder] // explicitly used: do *not* find and register it! internal class PartialViewMacroViewComponent : ViewComponent { diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index 9a9901394f..998760600a 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Data.Common; using System.Data.SqlClient; using System.Reflection; @@ -15,6 +16,10 @@ using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Migrations.Install; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Persistance.SqlCe; using Umbraco.Web.BackOffice.AspNetCore; using Umbraco.Web.Common.Extensions; using Umbraco.Web.Common.Filters; @@ -47,6 +52,25 @@ namespace Umbraco.Web.UI.BackOffice // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(x => new DbProviderFactoryCreator( + x.GetService().ConnectionStrings()[Core.Constants.System.UmbracoConnectionName]?.ProviderName, + DbProviderFactories.GetFactory, + x.GetServices(), + x.GetServices(), + x.GetServices() + )); + + + services.AddUmbracoConfiguration(_config); services.AddUmbracoRuntimeMinifier(_config); diff --git a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj index 79f194a4f9..09d3e81e55 100644 --- a/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj +++ b/src/Umbraco.Web.UI.NetCore/Umbraco.Web.UI.NetCore.csproj @@ -6,6 +6,7 @@ + @@ -48,7 +49,6 @@ - diff --git a/src/Umbraco.Web.UI.NetCore/appsettings.json b/src/Umbraco.Web.UI.NetCore/appsettings.json index 46dc20034b..6cc0a49b4b 100644 --- a/src/Umbraco.Web.UI.NetCore/appsettings.json +++ b/src/Umbraco.Web.UI.NetCore/appsettings.json @@ -1,7 +1,5 @@ { - "ConnectionStrings": { - "umbracoDbDSN": "" - }, + "ConnectionStrings": {}, "Logging": { "LogLevel": { "Default": "Information", @@ -12,111 +10,111 @@ "AllowedHosts": "*", "Umbraco": { "CMS": { - "Hosting": { - "Debug": true + "Hosting": { + "Debug": true + }, + "RuntimeMinification": { + "dataFolder": "App_Data/TEMP/Smidge", + "version": "637212411755687059" + }, + "Imaging": { + "Resize": { + "MaxWidth": 5000, + "MaxHeight": 5000 }, - "RuntimeMinification": { - "dataFolder": "App_Data/TEMP/Smidge", - "version": "637212411755687059" - }, - "Imaging": { - "Resize": { - "MaxWidth": 5000, - "MaxHeight": 5000 - }, - "Cache": { - "Folder": "../App_Data/Cache", - "MaxBrowserCacheDays": 7, - "MaxCacheDays": 365, - "CachedNameLength": 8 - } - }, - "HealthChecks": { - "DisabledChecks": [ - { - "id": "1B5D221B-CE99-4193-97CB-5F3261EC73DF", - "disabledBy": 1, - "disabledOn": "2020-03-15 19:19:10" - } - ], - "NotificationSettings": { - "Enabled": true, - "FirstRunTime": "", - "PeriodInHours": 24, - "NotificationMethods": { - "Email": { - "Enabled": true, - "Verbosity": "Summary", - "Settings": { - "RecipientEmail": "" - } - } - }, - "DisabledChecks": [ - { - "id": "1B5D221B-CE99-4193-97CB-5F3261EC73DF", - "disabledBy": 1, - "disabledOn": "2020-03-15 19:19:10" - } - ] - } - }, - "Tours": { - "EnableTours": true - }, - "Core": { - "Debug": {} - }, - "Content": { - "Errors": { - "Error404": { - "default": "1047", - "en-US": "$site/error [@name = 'error']", - "en-UK": "8560867F-B88F-4C74-A9A4-679D8E5B3BFC" - } - } - }, - "RequestHandler": { - "AddTrailingSlash": true, - "CharCollection": [ - { - "Char": " ", - "Replacement": "-" - }, - { - "Char": "\"", - "Replacement": "" - }, - { - "Char": "'", - "Replacement": "" - }, - { - "Char": "%", - "Replacement": "" - }, - { - "Char": ".", - "Replacement": "" - }, - { - "Char": ";", - "Replacement": "" - }, - { - "Char": "/", - "Replacement": "" - }, - { - "Char": "\\", - "Replacement": "" - }, - { - "Char": ":", - "Replacement": "" - } - ] + "Cache": { + "Folder": "../App_Data/Cache", + "MaxBrowserCacheDays": 7, + "MaxCacheDays": 365, + "CachedNameLength": 8 } + }, + "HealthChecks": { + "DisabledChecks": [ + { + "id": "1B5D221B-CE99-4193-97CB-5F3261EC73DF", + "disabledBy": 1, + "disabledOn": "2020-03-15 19:19:10" + } + ], + "NotificationSettings": { + "Enabled": true, + "FirstRunTime": "", + "PeriodInHours": 24, + "NotificationMethods": { + "Email": { + "Enabled": true, + "Verbosity": "Summary", + "Settings": { + "RecipientEmail": "" + } + } + }, + "DisabledChecks": [ + { + "id": "1B5D221B-CE99-4193-97CB-5F3261EC73DF", + "disabledBy": 1, + "disabledOn": "2020-03-15 19:19:10" + } + ] + } + }, + "Tours": { + "EnableTours": true + }, + "Core": { + "Debug": {} + }, + "Content": { + "Errors": { + "Error404": { + "default": "1047", + "en-US": "$site/error [@name = 'error']", + "en-UK": "8560867F-B88F-4C74-A9A4-679D8E5B3BFC" + } + } + }, + "RequestHandler": { + "AddTrailingSlash": true, + "CharCollection": [ + { + "Char": " ", + "Replacement": "-" + }, + { + "Char": "\"", + "Replacement": "" + }, + { + "Char": "'", + "Replacement": "" + }, + { + "Char": "%", + "Replacement": "" + }, + { + "Char": ".", + "Replacement": "" + }, + { + "Char": ";", + "Replacement": "" + }, + { + "Char": "/", + "Replacement": "" + }, + { + "Char": "\\", + "Replacement": "" + }, + { + "Char": ":", + "Replacement": "" + } + ] + } } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web/Macros/MacroRenderer.cs b/src/Umbraco.Web/Macros/MacroRenderer.cs new file mode 100644 index 0000000000..7f6a1cdbf3 --- /dev/null +++ b/src/Umbraco.Web/Macros/MacroRenderer.cs @@ -0,0 +1,421 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Events; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Macros; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Security; +using Umbraco.Core.Services; + +namespace Umbraco.Web.Macros +{ + internal class MacroRenderer : IMacroRenderer + { + private readonly IProfilingLogger _plogger; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IContentSettings _contentSettings; + private readonly ILocalizedTextService _textService; + private readonly AppCaches _appCaches; + private readonly IMacroService _macroService; + private readonly IIOHelper _ioHelper; + private readonly ICookieManager _cookieManager; + private readonly IMemberUserKeyProvider _memberUserKeyProvider; + private readonly ISessionManager _sessionManager; + private readonly IRequestAccessor _requestAccessor; + private readonly IHttpContextAccessor _httpContextAccessor; + + + public MacroRenderer( + IProfilingLogger plogger, + IUmbracoContextAccessor umbracoContextAccessor, + IContentSettings contentSettings, + ILocalizedTextService textService, + AppCaches appCaches, + IMacroService macroService, + IIOHelper ioHelper, + ICookieManager cookieManager, + IMemberUserKeyProvider memberUserKeyProvider, + ISessionManager sessionManager, + IRequestAccessor requestAccessor, + IHttpContextAccessor httpContextAccessor) + { + _plogger = plogger ?? throw new ArgumentNullException(nameof(plogger)); + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings)); + _textService = textService; + _appCaches = appCaches ?? throw new ArgumentNullException(nameof(appCaches)); + _macroService = macroService ?? throw new ArgumentNullException(nameof(macroService)); + _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); + _cookieManager = cookieManager; + _memberUserKeyProvider = memberUserKeyProvider; + _sessionManager = sessionManager; + _requestAccessor = requestAccessor; + _httpContextAccessor = httpContextAccessor; + } + + #region MacroContent cache + + // gets this macro content cache identifier + private string GetContentCacheIdentifier(MacroModel model, int pageId, string cultureName) + { + var id = new StringBuilder(); + + var alias = model.Alias; + id.AppendFormat("{0}-", alias); + //always add current culture to the key to allow variants to have different cache results + if (!string.IsNullOrEmpty(cultureName)) + { + // are there any unusual culture formats we'd need to handle? + id.AppendFormat("{0}-", cultureName); + } + + if (model.CacheByPage) + id.AppendFormat("{0}-", pageId); + + if (model.CacheByMember) + { + object key = 0; + + if (_umbracoContextAccessor.UmbracoContext.Security.IsAuthenticated()) + { + key = _memberUserKeyProvider.GetMemberProviderUserKey() ?? 0; + } + + id.AppendFormat("m{0}-", key); + } + + foreach (var value in model.Properties.Select(x => x.Value)) + id.AppendFormat("{0}-", value.Length <= 255 ? value : value.Substring(0, 255)); + + return id.ToString(); + } + + // gets this macro content from the cache + // ensuring that it is appropriate to use the cache + private MacroContent GetMacroContentFromCache(MacroModel model) + { + // only if cache is enabled + if (_umbracoContextAccessor.UmbracoContext.InPreviewMode || model.CacheDuration <= 0) return null; + + var cache = _appCaches.RuntimeCache; + var macroContent = cache.GetCacheItem(CacheKeys.MacroContentCacheKey + model.CacheIdentifier); + + if (macroContent == null) return null; + + _plogger.Debug("Macro content loaded from cache '{MacroCacheId}'", model.CacheIdentifier); + + // ensure that the source has not changed + // note: does not handle dependencies, and never has + var macroSource = GetMacroFile(model); // null if macro is not file-based + if (macroSource != null) + { + if (macroSource.Exists == false) + { + _plogger.Debug("Macro source does not exist anymore, ignore cache."); + return null; + } + + if (macroContent.Date < macroSource.LastWriteTime) + { + _plogger.Debug("Macro source has changed, ignore cache."); + return null; + } + } + + return macroContent; + } + + // stores macro content into the cache + private void AddMacroContentToCache(MacroModel model, MacroContent macroContent) + { + // only if cache is enabled + if (_umbracoContextAccessor.UmbracoContext.InPreviewMode || model.CacheDuration <= 0) return; + + // just make sure... + if (macroContent == null) return; + + // do not cache if it should cache by member and there's not member + if (model.CacheByMember) + { + var key = _memberUserKeyProvider.GetMemberProviderUserKey(); + if (key is null) return; + } + + // remember when we cache the content + macroContent.Date = DateTime.Now; + + var cache = _appCaches.RuntimeCache; + cache.Insert( + CacheKeys.MacroContentCacheKey + model.CacheIdentifier, + () => macroContent, + new TimeSpan(0, 0, model.CacheDuration) + ); + + _plogger.Debug("Macro content saved to cache '{MacroCacheId}'", model.CacheIdentifier); + } + + // gets the macro source file name + // null if the macro is not file-based, or not supported + internal static string GetMacroFileName(MacroModel model) + { + string filename = model.MacroSource; // partial views are saved with their full virtual path + + return string.IsNullOrEmpty(filename) ? null : filename; + } + + // gets the macro source file + // null if macro is not file-based + private FileInfo GetMacroFile(MacroModel model) + { + var filename = GetMacroFileName(model); + if (filename == null) return null; + + var mapped = _ioHelper.MapPath(filename); + if (mapped == null) return null; + + var file = new FileInfo(mapped); + return file.Exists ? file : null; + } + + // updates the model properties values according to the attributes + private static void UpdateMacroModelProperties(MacroModel model, IDictionary macroParams) + { + foreach (var prop in model.Properties) + { + var key = prop.Key.ToLowerInvariant(); + prop.Value = macroParams != null && macroParams.ContainsKey(key) + ? macroParams[key]?.ToString() ?? string.Empty + : string.Empty; + } + } + #endregion + + #region Render/Execute + + public MacroContent Render(string macroAlias, IPublishedContent content, IDictionary macroParams) + { + var m = _appCaches.RuntimeCache.GetCacheItem(CacheKeys.MacroFromAliasCacheKey + macroAlias, () => _macroService.GetByAlias(macroAlias)); + + if (m == null) + throw new InvalidOperationException("No macro found by alias " + macroAlias); + + var macro = new MacroModel(m); + + UpdateMacroModelProperties(macro, macroParams); + return Render(macro, content); + } + + private MacroContent Render(MacroModel macro, IPublishedContent content) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + + var macroInfo = $"Render Macro: {macro.Name}, cache: {macro.CacheDuration}"; + using (_plogger.DebugDuration(macroInfo, "Rendered Macro.")) + { + // parse macro parameters ie replace the special [#key], [$key], etc. syntaxes + foreach (var prop in macro.Properties) + prop.Value = ParseAttribute(prop.Value); + + var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; + macro.CacheIdentifier = GetContentCacheIdentifier(macro, content.Id, cultureName); + + // get the macro from cache if it is there + var macroContent = GetMacroContentFromCache(macro); + + // macroContent.IsEmpty may be true, meaning the macro produces no output, + // but still can be cached because its execution did not trigger any error. + // so we need to actually render, only if macroContent is null + if (macroContent != null) + return macroContent; + + // this will take care of errors + // it may throw, if we actually want to throw, so better not + // catch anything here and let the exception be thrown + var attempt = ExecuteMacroOfType(macro, content); + + // by convention ExecuteMacroByType must either throw or return a result + // just check to avoid internal errors + macroContent = attempt.Result; + if (macroContent == null) + throw new Exception("Internal error, ExecuteMacroOfType returned no content."); + + // add to cache if render is successful + // content may be empty but that's not an issue + if (attempt.Success) + { + // write to cache (if appropriate) + AddMacroContentToCache(macro, macroContent); + } + + return macroContent; + } + } + + /// + /// Executes a macro of a given type. + /// + private Attempt ExecuteMacroWithErrorWrapper(MacroModel macro, string msgIn, string msgOut, Func getMacroContent, Func msgErr) + { + using (_plogger.DebugDuration(msgIn, msgOut)) + { + return ExecuteProfileMacroWithErrorWrapper(macro, msgIn, getMacroContent, msgErr); + } + } + + /// + /// Executes a macro of a given type. + /// + private Attempt ExecuteProfileMacroWithErrorWrapper(MacroModel macro, string msgIn, Func getMacroContent, Func msgErr) + { + try + { + return Attempt.Succeed(getMacroContent()); + } + catch (Exception e) + { + _plogger.Warn(e, "Failed {MsgIn}", msgIn); + + var macroErrorEventArgs = new MacroErrorEventArgs + { + Name = macro.Name, + Alias = macro.Alias, + MacroSource = macro.MacroSource, + Exception = e, + Behaviour = _contentSettings.MacroErrorBehaviour + }; + + switch (macroErrorEventArgs.Behaviour) + { + case MacroErrorBehaviour.Inline: + // do not throw, eat the exception, display the trace error message + return Attempt.Fail(new MacroContent { Text = msgErr() }, e); + case MacroErrorBehaviour.Silent: + // do not throw, eat the exception, do not display anything + return Attempt.Fail(new MacroContent { Text = string.Empty }, e); + case MacroErrorBehaviour.Content: + // do not throw, eat the exception, display the custom content + return Attempt.Fail(new MacroContent { Text = macroErrorEventArgs.Html ?? string.Empty }, e); + //case MacroErrorBehaviour.Throw: + default: + // see http://issues.umbraco.org/issue/U4-497 at the end + // throw the original exception + throw; + } + } + } + + /// + /// Executes a macro. + /// + /// Returns an attempt that is successful if the macro ran successfully. If the macro failed + /// to run properly, the attempt fails, though it may contain a content. But for instance that content + /// should not be cached. In that case the attempt may also contain an exception. + private Attempt ExecuteMacroOfType(MacroModel model, IPublishedContent content) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + // ensure that we are running against a published node (ie available in XML) + // that may not be the case if the macro is embedded in a RTE of an unpublished document + + if (content == null) + return Attempt.Fail(new MacroContent { Text = "[macro failed (no content)]" }); + + var textService = _textService; + + return ExecuteMacroWithErrorWrapper(model, + $"Executing PartialView: MacroSource=\"{model.MacroSource}\".", + "Executed PartialView.", + () => ExecutePartialView(model, content), + () => textService.Localize("errors/macroErrorLoadingPartialView", new[] { model.MacroSource })); + } + + + #endregion + + #region Execute engines + + /// + /// Renders a PartialView Macro. + /// + /// The text output of the macro execution. + private MacroContent ExecutePartialView(MacroModel macro, IPublishedContent content) + { + var engine = new PartialViewMacroEngine(_umbracoContextAccessor, _httpContextAccessor, _ioHelper); + return engine.Execute(macro, content); + } + + #endregion + + #region Execution helpers + + // parses attribute value looking for [@requestKey], [%sessionKey] + // supports fallbacks eg "[@requestKey],[%sessionKey],1234" + private string ParseAttribute(string attributeValue) + { + // check for potential querystring/cookie variables + attributeValue = attributeValue.Trim(); + if (attributeValue.StartsWith("[") == false) + return attributeValue; + + var tokens = attributeValue.Split(',').Select(x => x.Trim()).ToArray(); + + // ensure we only process valid input ie each token must be [?x] and not eg a json array + // like [1,2,3] which we don't want to parse - however the last one can be a literal, so + // don't check on the last one which can be just anything - check all previous tokens + + char[] validTypes = { '@', '%' }; + if (tokens.Take(tokens.Length - 1).Any(x => + x.Length < 4 // ie "[?x]".Length - too short + || x[0] != '[' // starts with [ + || x[x.Length - 1] != ']' // ends with ] + || validTypes.Contains(x[1]) == false)) + { + return attributeValue; + } + + foreach (var token in tokens) + { + var isToken = token.Length > 4 && token[0] == '[' && token[token.Length - 1] == ']' && validTypes.Contains(token[1]); + + if (isToken == false) + { + // anything that is not a token is a value, use it + attributeValue = token; + break; + } + + var type = token[1]; + var name = token.Substring(2, token.Length - 3); + + switch (type) + { + case '@': + attributeValue = _requestAccessor.GetRequestValue(name); + break; + case '%': + attributeValue = _sessionManager.GetSessionValue(name); + if (string.IsNullOrEmpty(attributeValue)) + attributeValue = _cookieManager.GetCookieValue(name); + break; + } + + attributeValue = attributeValue?.Trim(); + if (string.IsNullOrEmpty(attributeValue) == false) + break; // got a value, use it + } + + return attributeValue; + } + + #endregion + + } + +} diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 36a997c2b8..0e5bc7a7bb 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -75,7 +75,7 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); - //composition.RegisterUnique(); //TODO... + composition.RegisterUnique(); composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f4450845c1..030e12d278 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -143,6 +143,7 @@ + diff --git a/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs b/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs index 4e4f5d6978..e3f2ce6b67 100644 --- a/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs +++ b/src/Umbraco.Web/UmbracoDbProviderFactoryCreator.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web } } - public void CreateDatabase() + public void CreateDatabase(string providerName) { var engine = new SqlCeEngine(DatabaseBuilder.EmbeddedDatabaseConnectionString); engine.CreateDatabase();