diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index d8721b0d19..d41813f7d8 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; diff --git a/src/Umbraco.Core/Composing/RuntimeHash.cs b/src/Umbraco.Core/Composing/RuntimeHash.cs index ae13b49915..4eb70cea1f 100644 --- a/src/Umbraco.Core/Composing/RuntimeHash.cs +++ b/src/Umbraco.Core/Composing/RuntimeHash.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using Umbraco.Cms.Core.Logging; @@ -40,7 +40,7 @@ namespace Umbraco.Cms.Core.Composing /// file properties (false) or the file contents (true). private string GetFileHash(IEnumerable<(FileSystemInfo fileOrFolder, bool scanFileContent)> filesAndFolders) { - using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) + using (_logger.DebugDuration("Determining hash of code files on disk", "Hash determined")) { // get the distinct file infos to hash var uniqInfos = new HashSet(); diff --git a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs index 26c3ee72c4..34af683fbe 100644 --- a/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/UnattendedSettings.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Umbraco.Cms.Core.Configuration.Models { @@ -24,6 +24,13 @@ namespace Umbraco.Cms.Core.Configuration.Models /// public bool UpgradeUnattended { get; set; } = false; + /// + /// Gets or sets a value indicating whether unattended package migrations are enabled. + /// + /// + /// This is true by default. + /// + public bool PackageMigrationsUnattended { get; set; } = true; /// /// Gets or sets a value to use for creating a user with a name for Unattended Installs diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 9ebbf21369..8915942a3b 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -7,6 +7,13 @@ namespace Umbraco.Cms.Core /// public static class Conventions { + public static class Migrations + { + public const string UmbracoUpgradePlanName = "Umbraco.Core"; + public const string KeyValuePrefix = "Umbraco.Core.Upgrader.State+"; + public const string UmbracoUpgradePlanKey = KeyValuePrefix + UmbracoUpgradePlanName; + } + public static class PermissionCategories { public const string ContentCategory = "content"; diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index 44ee99c420..ddff380c08 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -1,4 +1,4 @@ - namespace Umbraco.Cms.Core + namespace Umbraco.Cms.Core { public static partial class Constants { @@ -59,8 +59,7 @@ public const string RecycleBinMediaPathPrefix = "-1,-21,"; public const int DefaultLabelDataTypeId = -92; - public const string UmbracoConnectionName = "umbracoDbDSN"; - public const string UmbracoUpgradePlanName = "Umbraco.Core"; + public const string UmbracoConnectionName = "umbracoDbDSN"; } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index ec526d8c0f..eddb8c5b2c 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.HealthChecks; using Umbraco.Cms.Core.HealthChecks.NotificationMethods; using Umbraco.Cms.Core.Manifest; using Umbraco.Cms.Core.Media.EmbedProviders; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.Validators; using Umbraco.Cms.Core.Routing; @@ -32,7 +33,9 @@ namespace Umbraco.Cms.Core.DependencyInjection { builder.CacheRefreshers().Add(() => builder.TypeLoader.GetCacheRefreshers()); builder.DataEditors().Add(() => builder.TypeLoader.GetDataEditors()); - builder.Actions().Add(() => builder.TypeLoader.GetTypes()); + builder.Actions().Add(() => builder.TypeLoader.GetActions()); + builder.PackageMigrationPlans().Add(() => builder.TypeLoader.GetPackageMigrationPlans()); + // register known content apps builder.ContentApps() .Append() @@ -42,6 +45,7 @@ namespace Umbraco.Cms.Core.DependencyInjection .Append() .Append() .Append(); + // all built-in finders in the correct order, // devs can then modify this list on application startup builder.ContentFinders() @@ -116,6 +120,13 @@ namespace Umbraco.Cms.Core.DependencyInjection builder.BackOfficeAssets(); } + /// + /// Gets the package migration plans collection builder. + /// + /// The builder. + public static PackageMigrationPlanCollectionBuilder PackageMigrationPlans(this IUmbracoBuilder builder) + => builder.WithCollectionBuilder(); + /// /// Gets the actions collection builder. /// diff --git a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs index 4e45ea63d8..c3ef7af561 100644 --- a/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs +++ b/src/Umbraco.Core/Extensions/RuntimeStateExtensions.cs @@ -5,9 +5,26 @@ namespace Umbraco.Extensions { public static class RuntimeStateExtensions { + /// + /// Returns true if the installer is enabled based on the current runtime state + /// + /// + /// + public static bool EnableInstaller(this IRuntimeState state) + => state.Level == RuntimeLevel.Install || state.Level == RuntimeLevel.Upgrade || state.Level == RuntimeLevel.PackageMigrations; + /// /// Returns true if Umbraco is greater than /// public static bool UmbracoCanBoot(this IRuntimeState state) => state.Level > RuntimeLevel.BootFailed; + + /// + /// Returns true if the runtime state indicates that unattended boot logic should execute + /// + /// + /// + public static bool RunUnattendedBootLogic(this IRuntimeState state) + => (state.Reason == RuntimeLevelReason.UpgradeMigrations || state.Reason == RuntimeLevelReason.UpgradePackageMigrations) + && state.Level == RuntimeLevel.Run; } } diff --git a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs index e6e67f6153..6ac5432806 100644 --- a/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Core/Extensions/TypeLoaderExtensions.cs @@ -3,8 +3,10 @@ using System; using System.Collections.Generic; +using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Extensions @@ -12,19 +14,27 @@ namespace Umbraco.Extensions public static class TypeLoaderExtensions { /// - /// Gets all classes implementing . + /// Gets all types implementing . /// - public static IEnumerable GetDataEditors(this TypeLoader mgr) - { - return mgr.GetTypes(); - } + public static IEnumerable GetDataEditors(this TypeLoader mgr) => mgr.GetTypes(); /// - /// Gets all classes implementing ICacheRefresher. + /// Gets all types implementing ICacheRefresher. /// - public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) - { - return mgr.GetTypes(); - } + public static IEnumerable GetCacheRefreshers(this TypeLoader mgr) => mgr.GetTypes(); + + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetPackageMigrationPlans(this TypeLoader mgr) => mgr.GetTypes(); + + /// + /// Gets all types implementing + /// + /// + /// + public static IEnumerable GetActions(this TypeLoader mgr) => mgr.GetTypes(); } } diff --git a/src/Umbraco.Core/Migrations/MigrationPlan.cs b/src/Umbraco.Core/Migrations/MigrationPlan.cs index 19bd551075..05726f8448 100644 --- a/src/Umbraco.Core/Migrations/MigrationPlan.cs +++ b/src/Umbraco.Core/Migrations/MigrationPlan.cs @@ -25,9 +25,14 @@ namespace Umbraco.Cms.Core.Migrations public MigrationPlan(string name) { if (name == null) + { throw new ArgumentNullException(nameof(name)); + } + if (string.IsNullOrWhiteSpace(name)) + { throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); + } Name = name; } @@ -48,7 +53,7 @@ namespace Umbraco.Cms.Core.Migrations private MigrationPlan Add(string sourceState, string targetState, Type migration) { if (sourceState == null) - throw new ArgumentNullException(nameof(sourceState)); + throw new ArgumentNullException(nameof(sourceState), $"{nameof(sourceState)} is null, {nameof(MigrationPlan)}.{nameof(MigrationPlan.From)} must not have been called."); if (targetState == null) throw new ArgumentNullException(nameof(targetState)); if (string.IsNullOrWhiteSpace(targetState)) @@ -90,6 +95,9 @@ namespace Umbraco.Cms.Core.Migrations public MigrationPlan To(string targetState) => To(targetState); + + public MigrationPlan To(Guid targetState) + => To(targetState.ToString()); /// /// Adds a transition to a target state through a migration. /// @@ -97,12 +105,19 @@ namespace Umbraco.Cms.Core.Migrations where TMigration : IMigration => To(targetState, typeof(TMigration)); + public MigrationPlan To(Guid targetState) + where TMigration : IMigration + => To(targetState, typeof(TMigration)); + /// /// Adds a transition to a target state through a migration. /// public MigrationPlan To(string targetState, Type migration) => Add(_prevState, targetState, migration); + public MigrationPlan To(Guid targetState, Type migration) + => Add(_prevState, targetState.ToString(), migration); + /// /// Sets the starting state. /// diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs index 114aea8f24..576fbdc343 100644 --- a/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlan.cs @@ -1,14 +1,24 @@ using System; using System.Collections.Generic; using System.Text; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.Migrations; namespace Umbraco.Cms.Core.Packaging { - public abstract class PackageMigrationPlan : MigrationPlan + /// + /// Base class for package migration plans + /// + public abstract class PackageMigrationPlan : MigrationPlan, IDiscoverable { protected PackageMigrationPlan(string name) : base(name) { + // A call to From must be done first + From(string.Empty); + + DefinePlan(); } + + protected abstract void DefinePlan(); } } diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlanCollection.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollection.cs new file mode 100644 index 0000000000..2a17add2e6 --- /dev/null +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollection.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Packaging +{ + /// + /// A collection of + /// + public class PackageMigrationPlanCollection : BuilderCollectionBase + { + public PackageMigrationPlanCollection(IEnumerable items) : base(items) + { + } + } +} diff --git a/src/Umbraco.Core/Packaging/PackageMigrationPlanCollectionBuilder.cs b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollectionBuilder.cs new file mode 100644 index 0000000000..bf496852c6 --- /dev/null +++ b/src/Umbraco.Core/Packaging/PackageMigrationPlanCollectionBuilder.cs @@ -0,0 +1,9 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core.Packaging +{ + public class PackageMigrationPlanCollectionBuilder : LazyCollectionBuilderBase + { + protected override PackageMigrationPlanCollectionBuilder This => this; + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs index e9625892d4..0b0f9193fa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IKeyValueRepository.cs @@ -1,8 +1,15 @@ -using Umbraco.Cms.Core.Models; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Persistence.Repositories { public interface IKeyValueRepository : IReadRepository, IWriteRepository { + /// + /// Returns key/value pairs for all keys with the specified prefix. + /// + /// + /// + IReadOnlyDictionary FindByKeyPrefix(string keyPrefix); } } diff --git a/src/Umbraco.Core/RuntimeLevel.cs b/src/Umbraco.Core/RuntimeLevel.cs index f08a8b9d95..4330c33d94 100644 --- a/src/Umbraco.Core/RuntimeLevel.cs +++ b/src/Umbraco.Core/RuntimeLevel.cs @@ -32,9 +32,14 @@ /// Upgrade = 3, + /// + /// The runtime has detected that Package Migrations need to be executed. + /// + PackageMigrations = 4, + /// /// The runtime has detected an up-to-date Umbraco install and is running. /// - Run = 4 + Run = 100 } } diff --git a/src/Umbraco.Core/RuntimeLevelReason.cs b/src/Umbraco.Core/RuntimeLevelReason.cs index 863843a537..94192c83b2 100644 --- a/src/Umbraco.Core/RuntimeLevelReason.cs +++ b/src/Umbraco.Core/RuntimeLevelReason.cs @@ -1,4 +1,4 @@ -namespace Umbraco.Cms.Core +namespace Umbraco.Cms.Core { /// /// Describes the reason for the runtime level. @@ -65,6 +65,11 @@ /// UpgradeMigrations, + /// + /// Umbraco runs the current version but some package migrations have not run. + /// + UpgradePackageMigrations, + /// /// Umbraco is running. /// diff --git a/src/Umbraco.Core/Services/IKeyValueService.cs b/src/Umbraco.Core/Services/IKeyValueService.cs index 4c24ca19de..82fddedad3 100644 --- a/src/Umbraco.Core/Services/IKeyValueService.cs +++ b/src/Umbraco.Core/Services/IKeyValueService.cs @@ -1,4 +1,7 @@ -namespace Umbraco.Cms.Core.Services +using System.Collections; +using System.Collections.Generic; + +namespace Umbraco.Cms.Core.Services { /// /// Manages the simplified key/value store. @@ -11,6 +14,13 @@ /// Returns null if no value was found for the key. string GetValue(string key); + /// + /// Returns key/value pairs for all keys with the specified prefix. + /// + /// + /// + IReadOnlyDictionary FindByKeyPrefix(string keyPrefix); + /// /// Sets a value. /// diff --git a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs index 092b4c6b99..fc59d06016 100644 --- a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs @@ -38,7 +38,7 @@ namespace Umbraco.Cms.Core.Cache /// public void Handle(UmbracoApplicationStartingNotification notification) { - if (_runtimeState.Level < RuntimeLevel.Run) + if (_runtimeState.Level != RuntimeLevel.Run) { return; } diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index 7d0a933027..472321ac9e 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -112,7 +112,7 @@ namespace Umbraco.Cms.Infrastructure.Examine } } - private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level >= RuntimeLevel.Run; + private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; private void RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) { diff --git a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs index 05b016dbb6..7ec59e1c2e 100644 --- a/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs +++ b/src/Umbraco.Infrastructure/Examine/RebuildOnStartupHandler.cs @@ -47,7 +47,7 @@ namespace Umbraco.Cms.Infrastructure.Examine /// public void Handle(UmbracoRequestBeginNotification notification) { - if (_runtimeState.Level < RuntimeLevel.Run) + if (_runtimeState.Level != RuntimeLevel.Run) { return; } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs index a9ab97573a..691400121f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseBuilder.cs @@ -412,7 +412,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install /// configured and it is possible to connect to the database. /// Runs whichever migrations need to run. /// - public Result UpgradeSchemaAndData(MigrationPlan plan) + public Result UpgradeSchemaAndData(UmbracoPlan plan) { try { diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index be031d8667..18fe2e1ea2 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -27,7 +27,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// Initializes a new instance of the class. /// public UmbracoPlan(IUmbracoVersion umbracoVersion) - : base(Cms.Core.Constants.System.UmbracoUpgradePlanName) + : base(Core.Constants.Conventions.Migrations.UmbracoUpgradePlanName) { _umbracoVersion = umbracoVersion; DefinePlan(); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs index 8b9060a45d..27ff665e11 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/Upgrader.cs @@ -1,4 +1,5 @@ using System; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; @@ -13,10 +14,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// /// Initializes a new instance of the class. /// - public Upgrader(MigrationPlan plan) - { - Plan = plan; - } + public Upgrader(MigrationPlan plan) => Plan = plan; /// /// Gets the name of the migration plan. @@ -31,7 +29,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade /// /// Gets the key for the state value. /// - public virtual string StateValueKey => "Umbraco.Core.Upgrader.State+" + Name; + public virtual string StateValueKey => Constants.Conventions.Migrations.KeyValuePrefix + Name; /// /// Executes. diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs index ddfcae09f1..8d88b2d7df 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/AccessMapper.cs @@ -1,9 +1,10 @@ -using System; +using System; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Persistence.Mappers { + [MapperFor(typeof(PublicAccessEntry))] public sealed class AccessMapper : BaseMapper { diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs new file mode 100644 index 0000000000..a133d4066c --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/KeyValueMapper.cs @@ -0,0 +1,22 @@ +using System; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Persistence.Mappers +{ + [MapperFor(typeof(KeyValue))] + [MapperFor(typeof(IKeyValue))] + public sealed class KeyValueMapper : BaseMapper + { + public KeyValueMapper(Lazy sqlContext, MapperConfigurationStore maps) + : base(sqlContext, maps) + { } + + protected override void DefineMaps() + { + DefineMap(nameof(KeyValue.Identifier), nameof(KeyValueDto.Key)); + DefineMap(nameof(KeyValue.Value), nameof(KeyValueDto.Value)); + DefineMap(nameof(KeyValue.UpdateDate), nameof(KeyValueDto.UpdateDate)); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs index e797319810..8b993365a0 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/MapperCollectionBuilder.cs @@ -32,6 +32,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers Add(); Add(); Add(); + Add(); Add(); Add(); Add(); diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs index 6e08bad7c3..984c85f4fe 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/ExpressionVisitorBase.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -453,19 +453,19 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying else goto case "Contains**String"; - case "SqlWildcard": + case nameof(SqlExpressionExtensions.SqlWildcard): case "StartsWith": case "EndsWith": case "Contains**String": // see "Contains" above case "Equals": - case "SqlStartsWith": - case "SqlEndsWith": - case "SqlContains": - case "SqlEquals": - case "InvariantStartsWith": - case "InvariantEndsWith": - case "InvariantContains": - case "InvariantEquals": + case nameof(SqlExpressionExtensions.SqlStartsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): + case nameof(SqlExpressionExtensions.SqlContains): + case nameof(SqlExpressionExtensions.SqlEquals): + case nameof(StringExtensions.InvariantStartsWith): + case nameof(StringExtensions.InvariantEndsWith): + case nameof(StringExtensions.InvariantContains): + case nameof(StringExtensions.InvariantEquals): string compareValue; @@ -699,31 +699,31 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying { switch (verb) { - case "SqlWildcard": + case nameof(SqlExpressionExtensions.SqlWildcard): SqlParameters.Add(RemoveQuote(val)); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); case "Equals": - case "InvariantEquals": - case "SqlEquals": + case nameof(StringExtensions.InvariantEquals): + case nameof(SqlExpressionExtensions.SqlEquals): SqlParameters.Add(RemoveQuote(val)); return Visited ? string.Empty : SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); case "StartsWith": - case "InvariantStartsWith": - case "SqlStartsWith": + case nameof(StringExtensions.InvariantStartsWith): + case nameof(SqlExpressionExtensions.SqlStartsWith): SqlParameters.Add(RemoveQuote(val) + SqlSyntax.GetWildcardPlaceholder()); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); case "EndsWith": - case "InvariantEndsWith": - case "SqlEndsWith": + case nameof(StringExtensions.InvariantEndsWith): + case nameof(SqlExpressionExtensions.SqlEndsWith): SqlParameters.Add(SqlSyntax.GetWildcardPlaceholder() + RemoveQuote(val)); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); case "Contains": - case "InvariantContains": - case "SqlContains": + case nameof(StringExtensions.InvariantContains): + case nameof(SqlExpressionExtensions.SqlContains): var wildcardPlaceholder = SqlSyntax.GetWildcardPlaceholder(); SqlParameters.Add(wildcardPlaceholder + RemoveQuote(val) + wildcardPlaceholder); return Visited ? string.Empty : SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); diff --git a/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs b/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs index 03c5acf92f..99c4c3fd1a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/Querying/SqlExpressionExtensions.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Umbraco.Extensions; @@ -25,10 +25,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying return (value ?? fallbackValue).Equals(other ?? fallbackValue); } - public static bool SqlIn(this IEnumerable collection, T item) - { - return collection.Contains(item); - } + public static bool SqlIn(this IEnumerable collection, T item) => collection.Contains(item); public static bool SqlWildcard(this string str, string txt, TextColumnType columnType) { @@ -39,24 +36,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Querying return wildcardmatch.IsMatch(str); } - public static bool SqlContains(this string str, string txt, TextColumnType columnType) - { - return str.InvariantContains(txt); - } +#pragma warning disable IDE0060 // Remove unused parameter + public static bool SqlContains(this string str, string txt, TextColumnType columnType) => str.InvariantContains(txt); - public static bool SqlEquals(this string str, string txt, TextColumnType columnType) - { - return str.InvariantEquals(txt); - } + public static bool SqlEquals(this string str, string txt, TextColumnType columnType) => str.InvariantEquals(txt); - public static bool SqlStartsWith(this string str, string txt, TextColumnType columnType) - { - return str.InvariantStartsWith(txt); - } + public static bool SqlStartsWith(this string str, string txt, TextColumnType columnType) => str.InvariantStartsWith(txt); - public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) - { - return str.InvariantEndsWith(txt); - } + public static bool SqlEndsWith(this string str, string txt, TextColumnType columnType) => str.InvariantEndsWith(txt); +#pragma warning restore IDE0060 // Remove unused parameter } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs index 74988d2026..288f480ed1 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/KeyValueRepository.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Querying; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement @@ -19,6 +20,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement : base(scopeAccessor, AppCaches.NoCache, logger) { } + /// + public IReadOnlyDictionary FindByKeyPrefix(string keyPrefix) + => Get(Query().Where(entity => entity.Identifier.StartsWith(keyPrefix))) + .ToDictionary(x => x.Identifier, x => x.Value); + #region Overrides of IReadWriteQueryRepository public override void Save(IKeyValue entity) @@ -47,15 +53,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement return sql; } - protected override string GetBaseWhereClause() - { - return Cms.Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; - } + protected override string GetBaseWhereClause() => Core.Constants.DatabaseSchema.Tables.KeyValue + ".key = @id"; - protected override IEnumerable GetDeleteClauses() - { - return Enumerable.Empty(); - } + protected override IEnumerable GetDeleteClauses() => Enumerable.Empty(); protected override IKeyValue PerformGet(string id) { @@ -73,7 +73,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement protected override IEnumerable PerformGetByQuery(IQuery query) { - throw new NotSupportedException(); + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + return Database.Fetch(sql).Select(Map); } protected override void PersistNewItem(IKeyValue entity) diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs index f2cc2031d5..86ffc1b128 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseExtensions.cs @@ -1,6 +1,8 @@ -using System; +using System; +using System.Collections.Generic; using System.Linq; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.SqlSyntax; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Persistence @@ -9,24 +11,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence { public static UmbracoDatabase AsUmbracoDatabase(this IUmbracoDatabase database) { - var asDatabase = database as UmbracoDatabase; - if (asDatabase == null) throw new Exception("oops: database."); + if (database is not UmbracoDatabase asDatabase) + { + throw new Exception("oops: database."); + } + return asDatabase; } /// - /// Gets a key/value directly from the database, no scope, nothing. + /// Gets a dictionary of key/values directly from the database, no scope, nothing. /// /// Used by to determine the runtime state. - public static string GetFromKeyValueTable(this IUmbracoDatabase database, string key) + public static IReadOnlyDictionary GetFromKeyValueTable(this IUmbracoDatabase database, string keyPrefix) { if (database is null) return null; + // create the wildcard where clause + ISqlSyntaxProvider sqlSyntax = database.SqlContext.SqlSyntax; + var whereParam = sqlSyntax.GetStringColumnWildcardComparison( + sqlSyntax.GetQuotedColumnName("key"), + 0, + Querying.TextColumnType.NVarchar); + var sql = database.SqlContext.Sql() .Select() .From() - .Where(x => x.Key == key); - return database.FirstOrDefault(sql)?.Value; + .Where(whereParam, keyPrefix + sqlSyntax.GetWildcardPlaceholder()); + + return database.Fetch(sql) + .ToDictionary(x => x.Key, x => x.Value); } diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs index 3e34a77d7b..2e9fb6cebc 100644 --- a/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs +++ b/src/Umbraco.Infrastructure/Persistence/UmbracoPocoDataBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reflection; using NPoco; using Umbraco.Cms.Infrastructure.Persistence.Dtos; @@ -33,6 +33,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence { var columnInfo = base.GetColumnInfo(mi, type); + // TODO: Is this upgrade flag still relevant? It's a lot of hacking to just set this value + // including the interface method ConfigureForUpgrade for this one circumstance. if (_upgrading) { if (type == typeof(UserDto) && mi.Name == "TourData") columnInfo.IgnoreColumn = true; diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index f7065fe0b5..715b569b96 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Runtime { @@ -106,7 +107,7 @@ namespace Umbraco.Cms.Infrastructure.Runtime DoUnattendedInstall(); DetermineRuntimeLevel(); - if (State.Level <= RuntimeLevel.BootFailed) + if (!State.UmbracoCanBoot()) { return; // The exception will be rethrown by BootFailedMiddelware } @@ -117,17 +118,14 @@ namespace Umbraco.Cms.Infrastructure.Runtime throw new InvalidOperationException($"An instance of {typeof(IApplicationShutdownRegistry)} could not be resolved from the container, ensure that one if registered in your runtime before calling {nameof(IRuntime)}.{nameof(StartAsync)}"); } - - // if level is Run and reason is UpgradeMigrations, that means we need to perform an unattended upgrade - if (State.Reason == RuntimeLevelReason.UpgradeMigrations && State.Level == RuntimeLevel.Run) + if (State.RunUnattendedBootLogic()) { // do the upgrade DoUnattendedUpgrade(); // upgrade is done, set reason to Run DetermineRuntimeLevel(); - } // create & initialize the components diff --git a/src/Umbraco.Infrastructure/RuntimeState.cs b/src/Umbraco.Infrastructure/RuntimeState.cs index bef9adb76d..52313ed8a4 100644 --- a/src/Umbraco.Infrastructure/RuntimeState.cs +++ b/src/Umbraco.Infrastructure/RuntimeState.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Threading; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -7,6 +9,7 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Exceptions; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.Packaging; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Install; @@ -27,6 +30,7 @@ namespace Umbraco.Cms.Core private readonly ILogger _logger; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private readonly IEventAggregator _eventAggregator; + private readonly PackageMigrationPlanCollection _packageMigrationPlans; /// /// The initial @@ -48,7 +52,8 @@ namespace Umbraco.Cms.Core IUmbracoDatabaseFactory databaseFactory, ILogger logger, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, - IEventAggregator eventAggregator) + IEventAggregator eventAggregator, + PackageMigrationPlanCollection packageMigrationPlans) { _globalSettings = globalSettings; _unattendedSettings = unattendedSettings; @@ -57,6 +62,7 @@ namespace Umbraco.Cms.Core _logger = logger; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; _eventAggregator = eventAggregator; + _packageMigrationPlans = packageMigrationPlans; } @@ -101,55 +107,62 @@ namespace Umbraco.Cms.Core switch (GetUmbracoDatabaseState(_databaseFactory)) { - case UmbracoDatabaseState.CannotConnect: - { - // cannot connect to configured database, this is bad, fail - _logger.LogDebug("Could not connect to database."); + case UmbracoDatabaseState.CannotConnect: + { + // cannot connect to configured database, this is bad, fail + _logger.LogDebug("Could not connect to database."); - if (_globalSettings.Value.InstallMissingDatabase) - { - // ok to install on a configured but missing database - Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallMissingDatabase; - return; - } - - // else it is bad enough that we want to throw - Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase; - BootFailedException =new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); - throw BootFailedException; - } - case UmbracoDatabaseState.NotInstalled: + if (_globalSettings.Value.InstallMissingDatabase) { - // ok to install on an empty database + // ok to install on a configured but missing database Level = RuntimeLevel.Install; - Reason = RuntimeLevelReason.InstallEmptyDatabase; + Reason = RuntimeLevelReason.InstallMissingDatabase; return; } - case UmbracoDatabaseState.NeedsUpgrade: - { - // the db version does not match... but we do have a migration table - // so, at least one valid table, so we quite probably are installed & need to upgrade - // although the files version matches the code version, the database version does not - // which means the local files have been upgraded but not the database - need to upgrade - _logger.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco."); - Level = _unattendedSettings.Value.UpgradeUnattended ? RuntimeLevel.Run : RuntimeLevel.Upgrade; - Reason = RuntimeLevelReason.UpgradeMigrations; - } + // else it is bad enough that we want to throw + Reason = RuntimeLevelReason.BootFailedCannotConnectToDatabase; + BootFailedException = new BootFailedException("A connection string is configured but Umbraco could not connect to the database."); + throw BootFailedException; + } + case UmbracoDatabaseState.NotInstalled: + { + // ok to install on an empty database + Level = RuntimeLevel.Install; + Reason = RuntimeLevelReason.InstallEmptyDatabase; + return; + } + case UmbracoDatabaseState.NeedsUpgrade: + { + // the db version does not match... but we do have a migration table + // so, at least one valid table, so we quite probably are installed & need to upgrade + + // although the files version matches the code version, the database version does not + // which means the local files have been upgraded but not the database - need to upgrade + _logger.LogDebug("Has not reached the final upgrade step, need to upgrade Umbraco."); + Level = _unattendedSettings.Value.UpgradeUnattended ? RuntimeLevel.Run : RuntimeLevel.Upgrade; + Reason = RuntimeLevelReason.UpgradeMigrations; + } + break; + case UmbracoDatabaseState.NeedsPackageMigration: + + _logger.LogDebug("Package migrations need to execute."); + Level = _unattendedSettings.Value.PackageMigrationsUnattended ? RuntimeLevel.Run : RuntimeLevel.PackageMigrations; + Reason = RuntimeLevelReason.UpgradePackageMigrations; + break; case UmbracoDatabaseState.Ok: default: - { - // if we already know we want to upgrade, exit here - if (Level == RuntimeLevel.Upgrade) - return; + { + // if we already know we want to upgrade, exit here + if (Level == RuntimeLevel.Upgrade) + return; - // the database version matches the code & files version, all clear, can run - Level = RuntimeLevel.Run; - Reason = RuntimeLevelReason.Run; - } - break; + // the database version matches the code & files version, all clear, can run + Level = RuntimeLevel.Run; + Reason = RuntimeLevelReason.Run; + } + break; } } @@ -158,7 +171,8 @@ namespace Umbraco.Cms.Core Ok, CannotConnect, NotInstalled, - NeedsUpgrade + NeedsUpgrade, + NeedsPackageMigration } private UmbracoDatabaseState GetUmbracoDatabaseState(IUmbracoDatabaseFactory databaseFactory) @@ -178,10 +192,24 @@ namespace Umbraco.Cms.Core return UmbracoDatabaseState.NotInstalled; } - if (DoesUmbracoRequireUpgrade(database)) + // Make ONE SQL call to determine Umbraco upgrade vs package migrations state. + // All will be prefixed with the same key. + IReadOnlyDictionary keyValues = database.GetFromKeyValueTable(Constants.Conventions.Migrations.KeyValuePrefix); + + // This could need both an upgrade AND package migrations to execute but + // we will process them one at a time, first the upgrade, then the package migrations. + if (DoesUmbracoRequireUpgrade(keyValues)) { return UmbracoDatabaseState.NeedsUpgrade; } + + + // TODO: Can we save the result of this since we'll need to re-use it? + IReadOnlyList packagesRequiringMigration = DoesUmbracoRequirePackageMigrations(keyValues); + if (packagesRequiringMigration.Count > 0) + { + return UmbracoDatabaseState.NeedsPackageMigration; + } } return UmbracoDatabaseState.Ok; @@ -206,31 +234,46 @@ namespace Umbraco.Cms.Core public void DoUnattendedInstall() { - // unattended install is not enabled - if (_unattendedSettings.Value.InstallUnattended == false) return; + // unattended install is not enabled + if (_unattendedSettings.Value.InstallUnattended == false) + { + return; + } // no connection string set - if (_databaseFactory.Configured == false) return; + if (_databaseFactory.Configured == false) + { + return; + } - var connect = false; var tries = _globalSettings.Value.InstallMissingDatabase ? 2 : 5; - for (var i = 0;;) + + bool connect; + for (var i = 0; ;) { connect = _databaseFactory.CanConnect; - if (connect || ++i == tries) break; + if (connect || ++i == tries) + { + break; + } + _logger.LogDebug("Could not immediately connect to database, trying again."); Thread.Sleep(1000); } // could not connect to the database - if (connect == false) return; + if (connect == false) + { + return; + } using (var database = _databaseFactory.CreateDatabase()) { var hasUmbracoTables = database.IsUmbracoInstalled(); // database has umbraco tables, assume Umbraco is already installed - if (hasUmbracoTables) return; + if (hasUmbracoTables) + return; // all conditions fulfilled, do the install _logger.LogInformation("Starting unattended install."); @@ -263,12 +306,14 @@ namespace Umbraco.Cms.Core } } - private bool DoesUmbracoRequireUpgrade(IUmbracoDatabase database) + private bool DoesUmbracoRequireUpgrade(IReadOnlyDictionary keyValues) { var upgrader = new Upgrader(new UmbracoPlan(_umbracoVersion)); var stateValueKey = upgrader.StateValueKey; - CurrentMigrationState = database.GetFromKeyValueTable(stateValueKey); + _ = keyValues.TryGetValue(stateValueKey, out var value); + + CurrentMigrationState = value; FinalMigrationState = upgrader.Plan.FinalState; _logger.LogDebug("Final upgrade state is {FinalMigrationState}, database contains {DatabaseState}", FinalMigrationState, CurrentMigrationState ?? ""); @@ -276,6 +321,41 @@ namespace Umbraco.Cms.Core return CurrentMigrationState != FinalMigrationState; } + private IReadOnlyList DoesUmbracoRequirePackageMigrations(IReadOnlyDictionary keyValues) + { + var packageMigrationPlans = _packageMigrationPlans.ToList(); + + var result = new List(packageMigrationPlans.Count); + + foreach(PackageMigrationPlan plan in packageMigrationPlans) + { + string currentMigrationState = null; + var planKeyValueKey = Constants.Conventions.Migrations.KeyValuePrefix + plan.Name; + if (keyValues.TryGetValue(planKeyValueKey, out var value)) + { + currentMigrationState = value; + + if (plan.FinalState != value) + { + // Not equal so we need to run + result.Add(plan.Name); + } + } + else + { + // If there is nothing in the DB then we need to run + result.Add(plan.Name); + } + + _logger.LogDebug("Final package migration for {PackagePlan} state is {FinalMigrationState}, database contains {DatabaseState}", + plan.Name, + plan.FinalState, + currentMigrationState ?? ""); + } + + return result; + } + private bool TryDbConnect(IUmbracoDatabaseFactory databaseFactory) { // anything other than install wants a database - see if we can connect @@ -285,7 +365,8 @@ namespace Umbraco.Cms.Core for (var i = 0; ;) { canConnect = databaseFactory.CanConnect; - if (canConnect || ++i == tries) break; + if (canConnect || ++i == tries) + break; _logger.LogDebug("Could not immediately connect to database, trying again."); Thread.Sleep(1000); } diff --git a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs index bcd35ee7f4..7fda83a427 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/KeyValueService.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; @@ -25,6 +26,15 @@ namespace Umbraco.Cms.Core.Services.Implement } } + /// + public IReadOnlyDictionary FindByKeyPrefix(string keyPrefix) + { + using (var scope = _scopeProvider.CreateScope(autoComplete: true)) + { + return _repository.FindByKeyPrefix(keyPrefix); + } + } + /// public void SetValue(string key, string value) { diff --git a/src/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs new file mode 100644 index 0000000000..e8031d25b1 --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Core/RuntimeStateTests.cs @@ -0,0 +1,100 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Packaging; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Migrations; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class RuntimeStateTests : UmbracoIntegrationTest + { + private protected IRuntimeState RuntimeState { get; private set; } + + public override void Configure(IApplicationBuilder app) + { + base.Configure(app); + + RuntimeState = Services.GetRequiredService(); + } + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + PackageMigrationPlanCollectionBuilder migrations = builder.PackageMigrationPlans(); + migrations.Clear(); + migrations.Add(); + } + + [Test] + public void GivenPackageMigrationsExist_WhenLatestStateIsRegistered_ThenLevelIsRun() + { + // Add the final state to the keyvalue storage + IKeyValueService keyValueService = Services.GetRequiredService(); + keyValueService.SetValue( + Constants.Conventions.Migrations.KeyValuePrefix + TestMigrationPlan.TestMigrationPlanName, + TestMigrationPlan.TestMigrationFinalState.ToString()); + + RuntimeState.DetermineRuntimeLevel(); + + Assert.AreEqual(RuntimeLevel.Run, RuntimeState.Level); + Assert.AreEqual(RuntimeLevelReason.Run, RuntimeState.Reason); + } + + [Test] + public void GivenPackageMigrationsExist_WhenUnattendedMigrations_ThenLevelIsRun() + { + RuntimeState.DetermineRuntimeLevel(); + + Assert.AreEqual(RuntimeLevel.Run, RuntimeState.Level); + Assert.AreEqual(RuntimeLevelReason.UpgradePackageMigrations, RuntimeState.Reason); + } + + [Test] + public void GivenPackageMigrationsExist_WhenNotUnattendedMigrations_ThenLevelIsPackageMigrations() + { + var unattendedOptions = Services.GetRequiredService>(); + unattendedOptions.Value.PackageMigrationsUnattended = false; + + RuntimeState.DetermineRuntimeLevel(); + + Assert.AreEqual(RuntimeLevel.PackageMigrations, RuntimeState.Level); + Assert.AreEqual(RuntimeLevelReason.UpgradePackageMigrations, RuntimeState.Reason); + } + + private class TestMigrationPlan : PackageMigrationPlan + { + public const string TestMigrationPlanName = "Test"; + public static Guid TestMigrationFinalState => new Guid("BB02C392-4007-4A6C-A550-28BA2FF7E43D"); + + public TestMigrationPlan() : base(TestMigrationPlanName) + { + } + + protected override void DefinePlan() + { + To(TestMigrationFinalState); + } + } + + private class TestMigration : MigrationBase + { + public TestMigration(IMigrationContext context) : base(context) + { + } + + public override void Migrate() + { + + } + } + } +} diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs index 3437e1b286..eed30c19cf 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/KeyValueServiceTests.cs @@ -1,6 +1,7 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. +using System.Collections.Generic; using System.Threading; using NUnit.Framework; using Umbraco.Cms.Core.Services; @@ -19,6 +20,27 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services { private IKeyValueService KeyValueService => GetRequiredService(); + [Test] + public void Can_Query_For_Key_Prefix() + { + // Arrange + KeyValueService.SetValue("test1", "hello1"); + KeyValueService.SetValue("test2", "hello2"); + KeyValueService.SetValue("test3", "hello3"); + KeyValueService.SetValue("test4", "hello4"); + KeyValueService.SetValue("someotherprefix1", "helloagain1"); + // Act + IReadOnlyDictionary value = KeyValueService.FindByKeyPrefix("test"); + + // Assert + + Assert.AreEqual(4, value.Count); + Assert.AreEqual("hello1", value["test1"]); + Assert.AreEqual("hello2", value["test2"]); + Assert.AreEqual("hello3", value["test3"]); + Assert.AreEqual("hello4", value["test4"]); + } + [Test] public void GetValue_ForMissingKey_ReturnsNull() { diff --git a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs index 97108f5460..fd7f8eb971 100644 --- a/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs +++ b/src/Umbraco.Web.BackOffice/Authorization/BackOfficeHandler.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authorization; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Authorization { @@ -30,8 +31,7 @@ namespace Umbraco.Cms.Web.BackOffice.Authorization switch (_runtimeState.Level) { - case RuntimeLevel.Install: - case RuntimeLevel.Upgrade: + case var _ when _runtimeState.EnableInstaller(): return Task.FromResult(true); default: if (!_backOfficeSecurity.BackOfficeSecurity.IsAuthenticated()) diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs index 28871db452..6280631c83 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAreaRoutes.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Umbraco.Cms.Core; @@ -29,9 +29,8 @@ namespace Umbraco.Cms.Web.BackOffice.Install switch (_runtime.Level) { - case RuntimeLevel.Install: - case RuntimeLevel.Upgrade: - + case var _ when _runtime.EnableInstaller(): + endpoints.MapUmbracoRoute(installPathSegment, Cms.Core.Constants.Web.Mvc.InstallArea, "api", includeControllerNameInRoute: false); endpoints.MapUmbracoRoute(installPathSegment, Cms.Core.Constants.Web.Mvc.InstallArea, string.Empty, includeControllerNameInRoute: false); diff --git a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs index ee55f22888..429b204ec6 100644 --- a/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Install/InstallAuthorizeAttribute.cs @@ -1,9 +1,10 @@ -using System; +using System; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Install { @@ -44,8 +45,7 @@ namespace Umbraco.Cms.Web.BackOffice.Install { // if not configured (install or upgrade) then we can continue // otherwise we need to ensure that a user is logged in - return _runtimeState.Level == RuntimeLevel.Install - || _runtimeState.Level == RuntimeLevel.Upgrade + return _runtimeState.EnableInstaller() || (authorizationFilterContext.HttpContext.User?.Identity?.IsAuthenticated ?? false); } catch (Exception ex) diff --git a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs index 13d57ccc43..3f171d6439 100644 --- a/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/BackOfficeAreaRoutes.cs @@ -48,6 +48,7 @@ namespace Umbraco.Cms.Web.BackOffice.Routing { case RuntimeLevel.Install: case RuntimeLevel.Upgrade: + case RuntimeLevel.PackageMigrations: case RuntimeLevel.Run: MapMinimalBackOffice(endpoints); diff --git a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs index bda08d0d87..15012728d9 100644 --- a/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs +++ b/src/Umbraco.Web.BackOffice/Routing/PreviewRoutes.cs @@ -36,6 +36,7 @@ namespace Umbraco.Cms.Web.BackOffice.Routing { case RuntimeLevel.Install: case RuntimeLevel.Upgrade: + case RuntimeLevel.PackageMigrations: case RuntimeLevel.Run: endpoints.MapHub(GetPreviewHubRoute()); endpoints.MapUmbracoRoute(_umbracoPathSegment, Constants.Web.Mvc.BackOfficeArea, null); diff --git a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs index caf0b2fae0..7c5a89fa71 100644 --- a/src/Umbraco.Web.Common/Profiler/WebProfiler.cs +++ b/src/Umbraco.Web.Common/Profiler/WebProfiler.cs @@ -42,7 +42,7 @@ namespace Umbraco.Cms.Web.Common.Profiler public void UmbracoApplicationBeginRequest(HttpContext context, RuntimeLevel runtimeLevel) { - if (runtimeLevel < RuntimeLevel.Run) + if (runtimeLevel != RuntimeLevel.Run) { return; } @@ -55,7 +55,7 @@ namespace Umbraco.Cms.Web.Common.Profiler public void UmbracoApplicationEndRequest(HttpContext context, RuntimeLevel runtimeLevel) { - if (runtimeLevel < RuntimeLevel.Run) + if (runtimeLevel != RuntimeLevel.Run) { return; }